×
Community Blog The Tree Shaking Mechanism in Flutter

The Tree Shaking Mechanism in Flutter

This article explores the Tree Shaking mechanism during the compilation process using the Flutter Engine source code.

By Luyuan, from Xianyu Technology Team

Background

The Xianyu Technology Team needs to connect FaaS code with Flutter business code to achieve the best development experience during unifying Flutter projects. This way, the same code deployed in FaaS can also be directly applied to the main project of the business code, achieving the real project unification. To this end, we implement code decoupling for FaaS and Flutter code through the Remote Procedure Call (RPC), while project decoupling relies on the Tree Shaking mechanism of Flutter and Dart during compilation. We need to understand how Tree Shaking works to avoid potential problems. This article explores the Tree Shaking mechanism during the compilation process using the Flutter Engine source code.

Basic Knowledge

Tree Shaking is a dead code elimination technology that originated from LISP in the 1990s. The idea says all possible execution processes of a program can be represented by a tree of function calls, so functions that have never been called can be eliminated. This idea was first applied to JavaScript in Google Closure Tools and then to the Google dart2js compiler.

Tree Shaking is also used in Flutter to reduce the final packet size. Flutter provides three construction modes. For each mode, the Flutter compiler has different optimizations on the output binary files. The Tree Shaking mechanism can't be triggered in the Debug mode. Among the Ahead of Time (AOT) products compiled in Profile or Release mode, several important products intuitively show that the Tree Shaking mechanism is working.

  • app.dill: This is the product built by Dart code. It is a binary bytecode. The content inside uses the source code of Dart code and can be viewed through "strings."
  • snapshot_blob.bin.d: This file is a collection of all Dart files involved in the compilation, including our business code, the third-party database code defined in pubspec.yaml, and all imported native "package" code from Flutter and Dart.

Research on Tree Shaking Mechanism

Demo of the Simplifying Code

Here is a simple code example:

1

The code is very simple, containing an unused method _unused. Next, we compile the code in Profile mode, and view the final compiled product through DevTools, which is shown in the following figure:

2

The _unused method is not contained in functions, which means this part of the unneeded code is eliminated during the compilation process. In addition to functions, lib and imported dart files undergo similar Tree Shaking processing during Flutter compilation. The section below explains how it works.

Code Parsing

This sequence diagram of the "flutter run" command execution was designed by an experienced developer called Gityuan. In this diagram, the entire compilation process is relatively long because the binary executable file gen_snapshot is called by GenSnapshot.run() to generate machine codes. The corresponding source code is in the director of third_party/dart/runtime/bin/gen_snapshot.cc.

3

The following figure shows the execution process inside gen_snapshot.

4

The Tree Shaking mechanism works in the compilation stage using the CompileAll() method. The following section explores how the Flutter compiler truncates the code. You may make a comparison query of the source code. The path is third_party/dart/runtime/vm/compiler/aot/precompiler.cc.

Compilation Stage

The preparation is necessary, which requires the object pool to be retained until the AOT compilation is completed. Thus, a long-standing handle is needed. It uses StackZone.

5

The class hierarchy needs to be stable before compilation to use Class Hierarchy Analysis (CHA). At the same time, functions must remain, even when the class of functions is not finalized in entry point finding. CHA is the optimization on the compiler. It can change virtual calls to direct calls based on the analysis of class hierarchies.

6

AOT constructors and compute optimization instructions can be used in inline functions:

7

The next step is to generate a stub code. First, use StubCode::InterpretCall to obtain the object pool. Then, results are stored in object_store and obtained through several methods, including StubCode::Build. Next, the method names of dynamic functions are collected and then added to the beginnings of C++ allocation and calls as roots through AddRoots(). At the same time, all objects with @ pragma ('vm:entry-point') are also added as roots through AddAnnotatedRoots().

8

After that, the code starts to compile. Iterate() is the core of compilation. In the example below, the previously found root is used as the target to traverse all callers that have added the target.

9

The main call chain of this method is listed below:

ProcessFunction
==> CompileFunction
==> PrecompileFunctionHelper
==> PrecompileParsedFunctionHelper.Compile

The compilation is now completed. Useless code is eliminated in the Tree Shaking stage.

Tree Shaking Stage

During the compilation process above, the call information, such as function and class, has been output. According to the information, the compiler can distinguish unnecessary code. The section below uses Function processing as an example:

  • TraceForRetainedFunctions()

In this method, after obtaining handles like Class and Library, the code in each packet is processed with the units of libraries. Functions of all classes are traversed for processing.

10

Through AddTypesOf(const Function& function), the called function is added to the functions_to_retain_ pool. At the same time, type parameters in the Function are read and added to the corresponding typeargs_to_retain_ and types_to_retain_ pools through the AddType method, and similarly for the DropTypeArguments and DropTypeParameters. These parameters are used for type information in the Tree Shaking operation.

11

Class information is processed by method AddTypesOf(const Class& cls). The process is similar to the process of functions, so it will not be introduced here.

  • FinalizeDispatchTable()

In this method, entries for serializing the scheduling table are created before the Drop method is executed because the compiler may clear references to Code objects. At the same time, the scheduling table builder is deleted to ensure that no new entries are added afterward.

  • ReplaceFunctionStaticCallEntries()

In this method, the call entries of static functions are replaced by the anonymous internal class StaticCallTableEntryFixer.

  • Drop

Next, a series of Drop methods are performed. These methods remove redundant methods, fields, classes, and libraries. They are shown below:

  1. DropFunctions()
  2. DropFields()
  3. DropTypes()
  4. DropTypeParameters()
  5. DropTypeArguments()
  6. DropMetadata()
  7. DropLibraryEntries()
  8. DropClasses()
  9. DropLibraries()

The specific call sequence is shown in the following figure:

12

Since the internal implementation ideas of these methods are similar, we used DropFunctions method as an example.

The major part of this method is the functions_to_retain_ pool mentioned above. It determines whether a function has a root caller. If the function object is not contained in the pool, the function can be eliminated. Then, the remaining functions are written back to the class, and the call table of the class is updated.

The drop_function declared in the method is used to eliminate the function.

13

Then, all the functions in the code are traversed, and the useless function code is marked and deleted by the drop_function.

14

Functions that need to be retained are written into the class.

15

A new call table of the class is generated, and unnecessary functions that may exist in the call table are deleted.

16

The last step is the processing of some edge cases, such as inline functions, which are not described in detail in this article. After the Drop stage is completed, the code that can be eliminated is put into the deletion pool. Then, during the end stage of compilation, the size of binary files is further reduced.

End Stage

The compilation enters the end stage after Tree Shaking is completed. The process includes code obfuscation and garbage collection.

17

The key code of the Dedup method is listed below:

18

A lot of repeated data deletion works are done within this method. In AOT mode, binder runs after Tree Shaking while all targets have been compiled. Therefore, binder replaces all static calls with direct calls of targets, which further reduces the binary file size. At this moment, all compilation works have been completed, and Tree Shaking has completed the mission.

Additional Information

In Flutter 1.20, the unused icon fonts in the project can be removed with the Tree Shaking mechanism to reduce the packet size to around 100 KB. However, this method is implemented in the build_system, not in the compilation stage described above. In the build_system, this method optimizes assets. For related physical records (PR), please see this page.

Summary

This article explores the Tree Shaking mechanism during the compilation process using the Flutter Engine source code. This mechanism provides a theoretical basis for project decoupling, simplifies the implementation of project unification, and inspires us to reduce the packet size.

0 0 0
Share on

XianYu Tech

56 posts | 4 followers

You may also like

Comments