By Luyuan, from Xianyu Technology Team
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.
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.
Here is a simple code example:
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:
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.
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
.
The following figure shows the execution process inside gen_snapshot
.
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
.
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.
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.
AOT constructors and compute optimization instructions can be used in inline functions:
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()
.
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.
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.
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.
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.
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
.
Next, a series of Drop methods are performed. These methods remove redundant methods, fields, classes, and libraries. They are shown below:
DropFunctions()
DropFields()
DropTypes()
DropTypeParameters()
DropTypeArguments()
DropMetadata()
DropLibraryEntries()
DropClasses()
DropLibraries()
The specific call sequence is shown in the following figure:
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.
Then, all the functions in the code are traversed, and the useless function code is marked and deleted by the drop_function
.
Functions that need to be retained are written into the class.
A new call table of the class is generated, and unnecessary functions that may exist in the call table are deleted.
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.
The compilation enters the end stage after Tree Shaking is completed. The process includes code obfuscation and garbage collection.
The key code of the Dedup method is listed below:
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.
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.
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.
Exploring Memory Leaks in Flutter from the Rendering Process
An In-Depth Understanding of Flutter Compilation Principles and Optimizations
56 posts | 4 followers
FollowXianYu Tech - May 13, 2021
Alibaba Tech - July 11, 2019
XianYu Tech - September 8, 2020
Alibaba Clouder - February 8, 2021
XianYu Tech - September 4, 2020
HaydenLiu - December 5, 2022
56 posts | 4 followers
FollowExplore Web Hosting solutions that can power your personal website or empower your online business.
Learn MoreA low-code development platform to make work easier
Learn MoreExplore how our Web Hosting solutions help small and medium sized companies power their websites and online businesses.
Learn MoreHelp enterprises build high-quality, stable mobile apps
Learn MoreMore Posts by XianYu Tech