The author | Xiao Xiang

Edit | orange

New retail product | alibaba tao technology

background

As we all know, the level of memory is one of the important indicators to judge the performance of an APP. How to help developers analyze, expose and solve memory leak problems more easily is a “standard feature” that developers desperately need for almost every platform or framework. But for the Flutter community, there is a lack of a handy memory leak tool.

For flutter, there is a long rendering link from the dart layer to the c++ layer by forming a rendering tree and submitting it to skia for rendering. The user must have a thorough understanding of the entire rendering link to gain insight into the memory usage at this point in time. This paper proposes a solution to find memory leaks based on the number of render trees.

What the Flutter memory contains

Virtual memory or physical memory?

When we talk about memory, we usually talk about Physical memory. When the same application runs on different machines or operating systems, the amount of Physical memory allocated varies depending on the hardware conditions of different operating systems and machines, but generally speaking, The same is true for the Virtual Memory used by an application, which is what this article is talking about.

We can intuitively understand that all objects operated in the code can be measured by virtual memory, and do not care too much about whether the object exists in physical memory or not. As long as it can reduce the application of objects, as little as possible to hold objects, no matter white or black cats, can reduce objects, are “good cats”.

What are we talking about when discussing Flutter Memory

Flutter can be divided into three parts according to the language used:

  • The Framework layer is written in Dart, giving developers access to the top layer for application layer development
  • Engine layer, written by C/C++, mainly for graphics rendering
  • Embedder layer, written in an embedder-layer language such as Objective-C/ Swift for iOS and Java for Android

When we talk about the memory applied to flutter in terms of processes, we mean the sum of all the memory of the three.

DartVM refers to the memory occupied by the Dart VIRTUAL machine, and Native memory contains the memory used to run Engine and platform-specific code.

Since the most direct objects that Flutter users have access to are those generated in Dart, it seems that they have no control over the creation and destruction of Engine objects. Which brings us to the Dart virtual machine binding layer design.

How does the Dart binding layer work

For performance or cross-platform or other reasons, scripting languages or virtual machine based languages provide interfaces for binding C/C ++ or function objects to concrete language objects so that c/ C ++ objects or functions can then be manipulated in the language. This API is called the binding layer. For example: Lua binding is easiest to embed in applications, Binding for Javascript V8 engines, etc.

Dart virtual machine initialization will inject a C++ declared class or function and a function into the Dart runtime global traversal. When Dart code executes a function, it points to a specific C++ object or function.

Below are several c++ classes with common bindings and their Dart equivalents

flutter::EngineLayer –> ui.EngineLayer flutter::FrameInfo –> ui.FrameInfo flutter::CanvasImage –> ui.Image flutter::SceneBuilder –> ui.SceneBuilder flutter::Scene –> ui.Scene

Use the UI.SceneBuilder example to see how Dart binds an instance of a c++ object and controls the destructor of that c++ instance.

The Dart layer rendering process is the process of configuring the layer rendering tree and submitting it to the c++ layer for rendering. The UI.SceneBuilder is the container for the render tree

  • Dart code calls the c++ method SceneBuilder_constructor when it calls the constructor ui.scenebuilder ()

  • Call the constructor of flutter::SceneBuilder and generate a c++ instance SceneBuilder

  • Because flutter::SceneBuilder inherits from the memory count object RefCountedDartWrappable, the memory count will be increased by 1 when the object is generated

  • The generated c++ instance sceneBuilder uses Dart’s API to generate a WeakPersitentHandle and inject it into Dart. After that, Dart can use the builder object to manipulate the c++ flutter::SceneBuilder instance.

  • When the Dart virtual machine determines that the Dart object Builder is not referenced by any other object long after the program has run (for example, if builder=null is simply set to null, otherwise known as unreachable), the object is collected and released by Garbage Collection. The memory count will be reduced by one

  • When the memory count reaches zero, the c++ destructor is triggered, and eventually the memory block pointed to by the c++ instance is reclaimed

As can be seen, Dart uses Garbage Collection (GC) of the Dart VM to control the creation and release of C/C++ instances by encapsulating C/C++ instances as WeakPersitentHandle and injecting them into the Dart context.

To put it more bluntly, as long as the Dart object corresponding to the C/C++ instance is properly collected by GC, the memory space pointed to by C/C++ will be freed normally.

What is WeakPersistentHandle

Dart objects are often moved around in the VM due to GC defragmentation, so objects are not directly pointed to, but indirectly pointed to by means of a handle. Moreover, C/C ++ objects or instances are outside the Dart virtual machine and their life cycles are not scoped. WeakPersistentHandle is dedicated to lifecycle and Persistent handles that encapsulate C/C++ instances in Dart.

All The WeakPersistentHandle objects can be viewed in the Observatory tool provided by Flutter official.

The Peer column is a pointer that encapsulates a C/C ++ object.

!

Reachability of Dart objects

Dart object releases are released by the Garbage Collection by determining whether the object is available. Reachability refers to the access of objects through reference chains between objects starting from some root nodes. If objects can be accessed through reference chains, it indicates that objects are reachability, otherwise they are unreachable.

Yellow is accessible, blue is not.

An undetectable memory leak

The problem with this is that it is difficult to sense the death of C/C++ objects from the Dart side because Dart objects have no universal destructor like C++ classes. If an object is referenced for a long time by another object, for example, for circular references, GC will not be able to release it, resulting in a memory leak.

To amplify the problem a little bit, we know that flutter is a rendering engine. We build a Widget tree by writing Dart language, simplify it into Element tree, RenderObject tree and Layer tree, and submit the Layer tree to the C++ Layer. Then render using Skia.

If a node in a Wigdet or Element tree is unable to be freed for a long time, its children may also be unable to be freed, rapidly expanding the leaking memory space.

For example, there are two A and B interfaces. A interface adds B interface through navigator. push, and B interface reverts to A through navigator. pop. If B’s rendering tree is untangled from the main rendering tree because of some way of writing it, the entire subtree of B will be untangled.

Memory leaks are detected by detecting render tree nodes

Based on the above situation, we can actually judge the memory leak of the previous interface release by comparing the number of render nodes used in the current frame and the number of render nodes in the current memory.

Dart code builds the render tree by adding EngineLayer to UI.SceneBuilder, so we just check the number of EngineLayer in memory in c++ and compare the number of EngineLayer in the current frame. If the number of EngineLayer in memory is longer than the number in use, then we can determine that there is a memory leak.

Again, the previous pushB interface on page A is used as an example. In the normal case of no memory leak, the number of layers in use (blue) and the number of layers in memory (orange) have fluctuations, but both curves are relatively fit in the end.

However, if there is A memory leak on page B, the B tree cannot be freed at all after the interface A is returned, and the number of layers in memory (orange) cannot finally fit the blue curve (number of layers in use). In other words, for rendering, if the code causes the Widget tree or Element tree to not be collected by GC for A long time, This is likely to result in a serious memory leak.

What caused the memory leak?

The current scenarios that find asynchronously executed code (Feature, async/await,methodChan) hold the incoming BuildContext for a long time, causing the element to persist long after it is removed, eventually causing the associated widget and state to leak.

Moving on to the example of page B leaking

The difference between correct and incorrect writing is that it is only wrong to use the asynchronous method Future to reference BuildContext before calling navigator.pop, which causes a memory leak in the B interface.

How do you find leaks?

The current design idea of the Flutter memory leak detection tool is to find unreleased objects by comparing the objects before and after the entry of the interface. Unreleased objects are then checked (loose reference path or Inbound References), analyzed with the source code, and the error code is found.

Even though it is possible to view the references of each leak object one by one using the Observatory of Flutter, the resulting number of layers is very large for a slightly more complex interface. Trying to find the offending code in all of the Observatory’s leak objects is a daunting task.

To this end, we have visualized the complex positioning work.

Here, we record all the EngineLayer submitted to engine for each frame in the form of a broken line graph. If the number of layers in memory mentioned above is abnormally greater than the number of layers in use, it can be judged that there is a memory leak in the previous page.

Furthermore, you can also grab the layer tree structure of the current page to help locate the layer tree generated by which RenderObject tree, and further analyze the RenderObject node generated by which Element node.

Or you can print the reference chain of WeakPersitentHandle for auxiliary analysis.

However, the pain point still exists. It is still necessary to check the reference chain of Handle and analyze the source code to locate the problem quickly. This is the next issue that needs to be addressed.

conclusion

Our approach to flutter memory leaks from a render tree perspective can be generalized to other Dart objects of different types.

When writing code, developers need to be aware of asynchronous calls and whether the Element being manipulated will be referenced and cannot be freed.