×
Community Blog Exploring Memory Leaks in Flutter from the Rendering Process

Exploring Memory Leaks in Flutter from the Rendering Process

This article analyzes the memory allocation of Flutter, explains the rendering process, and proposes a solution for memory leaks based on the number of rendering trees.

By Xiaoxiang, from Xianyu Technology Team

Background

Memory utilization is one of the important indicators to judge the performance of an app. Developers reduce memory usage as much as possible and clear useless memory blocks to reduce the memory usage of the app. This is something developers always pursue. However, inevitably, the to-be-released objects are not released due to language usage or writing conditions. As a result, memory leaks occur, and the memory space is depleted, which leads to a system crash.

Different ways to make it easier for developers to analyze, discover, and solve memory leaks is almost a common feature for every platform, framework, and developer. For example, there are Apple's instruments and Linux's Kmemleak. However, for the Flutter community, a user-friendly memory leak tool is still not available.

For Flutter, Dart can form a rendering tree and then submit the rendering tree to Skia developed in C++. The rendering procedure is very long from the Dart layer to the C++ layer. Therefore, users must have a deep understanding of the whole rendering process to fully understand the memory usage.

Based on the Flutter rendering principle, this article analyzes the memory allocation of Flutter, explains the rendering process, and proposes a solution for memory leaks based on the number of rendering trees.

What Is Included in Flutter Memory?

Virtual Memory or Physical Memory

When it comes to memory, it usually refers to physical memory. When the same application runs on different machines or operating systems, the size of the allocated physical memory varies depending on the operating system and hardware conditions. Generally, virtual memory used by an application is roughly the same. The memory discussed in this article is virtual memory.

All objects operated in the code can be measured by virtual memory without focusing on if the object exists in physical memory or not. It is considered ideal when the objects consume less memory.

What Is Discussed When Talking about Flutter Memory?

Flutter uses three types of languages:

  1. The framework layer is written in Dart for application layer development when developers have access to the top layer.
  2. The engine layer is written in C/C++ and used for graphics rendering.
  3. The embedder layer is written in the embedding layer languages. For example, iOS uses Objective-C/swift, while Android uses Java.

When it comes to the memory used by Flutter from the perspective of the process, it refers to the sum memory of the three layers. For simplicity, the memory can be divided into DartVM and native memories according to the code that users can directly contact. DartVM memory refers to the memory used by the Dart virtual machine, while native memory contains running memories of the Engine and platform-related code.

1

Since the most direct objects that Flutter users can access are objects generated by Dart, users cannot figure out how to create and destroy objects in the Engine layer. For the answer, it is necessary to discuss the design of the Dart virtual machine binding layer.

How the Dart Binding Layer Works

Due to performance, cross-platform operation, or other reasons, scripting languages or virtual machine-based languages cause C/C++ or function objects to be bound with interfaces of specific language objects. Thus, C/C++ objects or functions can still be manipulated in languages. This API layer is called the binding layer. The examples are Lua binding and Javascript V8 binding, which can be embedded in an application easily.

During the initialization of a Dart virtual machine, a class or a function declared by C++ will be bound with a class or function of Dart. Then, they will be injected into the global traversal during the Dart runtime in sequence. When the Dart code runs a function, it directs to a specific C++ object or a function.

The following shows several common bindings of C++ classes and corresponding Dart classes:

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

Let's take ui.SceneBuilder as an example. The following diagram shows how Dart binds a C++ object instance and controls its parsing process. The rendering process of the Dart layer is about configuring the layer rendering tree and submitting it to the C++ layer. ui.SceneBuilder is the container for this rendering tree.

2

  1. When Dart code calls the constructor ui.SceneBuilder(), it calls the C++ method SceneBuilder_constructor.
  2. Dart calls the flutter::SceneBuilder_constructor and generates a C++ instance, sceneBuilder.
  3. Since flutter::SceneBuilder inherits from the memory count object RefCountedDartWrappable, the memory count increases by 1 after the object is generated.
  4. The sceneBuilder is encapsulated as the westpersitenthandle through the Dart API and is injected into the Dart context. After that, Dart can use this builder object to operate the C++ instance flutter::SceneBuilder.
  5. After running the application for a long time, the Dart virtual machine determines the builder object. If it is not referenced by any other objects (for example, builder = null, which indicates no availability), the object is collected and released by the Garbage Collection (GC). The memory count is subtracted by 1.
  6. When the memory counts 0, the C++ destructor is triggered, and the memory block the C++ instance directs to is collected.

As shown above, Dart encapsulates the C/C++ instances into WeakPersitentHandle and injects them into the Dart context. Thus, it controls the creation and release of C/C++ instances through the GC of the Dart virtual machine.

More directly, as long as the Dart object corresponding to the C/C++ instance can be collected normally by GC, the memory space that C/C++ points to will be normally released.

What Is WeakPersistentHandle?

As Dart objects often move in virtual machines due to fragmented GC organizing, the use of objects indirectly points to objects by using handles. Furthermore, C/C++ objects or instances are located outside the Dart virtual machine. Their lifecycle is not constrained by the scope and always exists in the whole Dart virtual machine for a long time. Thus, it is Persistence. Therefore, WeakPersistentHandle points to the lifecycle and persistent handles, which are used to encapsulate C/C++ instances in Dart. You can check all the WeakPersistentHandle objects in the Observatory tool provided by Flutter.

3

The Peer column is the pointer that encapsulates the C/C++ objects.

4

Availability of Dart Objects

GC releases Dart objects by determining whether the objects are still available. Availability means the objects are accessed through the reference chain between objects, starting from some root nodes. If the object can be accessed through the reference chain, it indicates that the object is available. Otherwise, it is not.

A yellow mark means the object is available, and a blue mark means the object is unavailable.

5

Imperceptible Memory Leak

It is difficult to perceive the disappearance of C/C++ objects from the Dart side because Dart objects do not have a unified destructor like C++. Once the object is referenced long-term by other objects due to reasons, such as circular reference, GC will not be able to release it. This will eventually lead to a memory leak.

Let's discuss this problem in Flutter. Flutter is a rendering engine. A Widget tree is constructed through Dart language. Then, the Widget tree is simplified into the Element tree, then into the RenderObject tree, and into the Layer tree at last through drawing. Next, the Layer tree is submitted to the C++ layer for rendering using Skia.

6

If a node of a Widget tree or an Element tree cannot be released for a long time, it may be hard to release its sub-nodes. Thus, the leaked memory space will expand rapidly.

7

For example, there are two interfaces, A and B. Interface A adds interface B through Navigator.push, and interface B rolls back to A through Navigator.pop. If the rendering tree of B cannot be released, although unraveled from the main rendering tree due to some writing methods on the B, the whole original subtree of B cannot be released.

Detecting Memory Leaks by Detecting Rendering Tree Nodes

Based on the case above, memory leaks caused by former interface releases can be detected by comparing the rendering node number of the current frame and the current memory.

In Dart code, a rendering tree is built by adding EngineLayer to ui.SceneBuilder. So, the number of EngineLayer in the C++ memory can be detected and compared with what is used in the current frame. If the number of EngineLayers in the memory surpasses what is used for a long time, it means there is a memory leak.

8

Let's use the case above as an example again. If there is no memory leak, two curves will reach almost the same value eventually, though they may fluctuate. The blue curve stands for the number of layers in use, and the orange curve stands for the number of layers in the memory.

When interface B rolls back to A with a memory leak, the rendering tree of B will not be released. The orange curve cannot conform to the blue curve. For rendering, if the Widget tree or Element tree cannot be collected by GC for a long time due to code, it is likely to cause a serious memory leak.

What Causes Memory Leaks?

Currently, scenarios of asynchronous code running (Feature, async/await,methodChan) have been in BuildContext for a long time. As a result, the element remains for a long time after being removed, resulting in the leakage in the associated widget and state.

9

Let's take another look at the memory leak example of interface B on the images below:

The difference between correct and incorrect code writing is that the BuildContext uses the asynchronous method Future before Navigator.pop is called. This causes a memory leak on interface B.

How Can You Find the Leak Point?

The current design of the Flutter memory leak detection tool compares the objects before and after entering the interface. Then, the tool finds out the unreleased objects and views the unreleased reference relationships (retained path or inbound references.) Next, it conducts analysis based on the source code to find the incorrect code.

The reference relationship of each leaked object can be viewed one by one with the Flutter Observatory. However, it is very complicated to find incorrect codes in all leaked objects in the Observatory. The number of layers generated is very large on the slightly more complex interface. For this reason, we have visualized how complicated positioning works.

We record all EngineLayer submitted to the Engine on a line chart frame by frame. If the number of layers in the memory is unusually greater than what is in use, there is a memory leak on the previous page.

10

Users can also fetch the structure of the Layer tree of the current page to determine which RenderObject tree generates the Layer tree. Users can continue to analyze which Element node generates the RenderObject node.

11

You can print the reference chain of WeakPersitentHandle for auxiliary analysis.

12

However, the pain point still exists today. It is still necessary to check the reference chain of the Handle and analyze the source code to define the problem quickly. This is the problem that needs to be solved urgently.

Summary and Prospects

  • Our method of exploring Flutter memory leaks from the perspective of the rendering tree can be used for different types of objects in Dart.
  • When writing code, developers must pay attention to asynchronous calls and whether the manipulated Element can be referenced or not.

Xianyu has been working on Flutter for a long time and is continuously making efforts in the Flutter toolchain. The important memory detection tool is being developed continuously, and you are welcome to follow our development on the tool!

0 0 0
Share on

XianYu Tech

43 posts | 1 followers

You may also like

Comments