Hummer Engine Optimization Series-Unveiling the most powerful memory leak detection tool

Flutter can be said to be the most popular mobile cross-platform solution in the past two years. Whether it is an innovative application or an old flagship application, Flutter technology is more or less tried.

Currently, the most common problem reported by the Flutter business team is that Flutter's memory usage is too high .

The reason for the high memory usage of Flutter is more complicated, and you need to open another topic to make it clear. A brief summary of the conclusions of our investigation: Dart Heap memory management and Flutter Widget design integration lead to high business memory, and its core problem engine design makes it easy for developers to step on memory leaks. During the development process, memory leaks are common and difficult to locate. The main two reasons are summarized:

  • Flutter's design of rendering three trees, as well as the characteristics of Dart's various asynchronous programming, cause the object reference relationship to be more convoluted and analysis difficult

  • Dart "closure", "instance method" can be assigned and passed, resulting in the class being held by the method context, and leakage will occur inadvertently. A typical example is to register a listener without de-registering, resulting in the leakage of the class object where the listener is located

Developers enjoy the convenience of Flutter development, but unknowingly suffer the consequences of memory leaks. Therefore, we urgently need a set of efficient memory leak detection tools to get rid of this dilemma.

Inventory of several memory leak detection solutions I have learned:

  1. Monitor State for leaks: Leak detection for State. But is State the object that accounts for the largest proportion of Flutter memory leaks? Objects of StatelessWidget can also reference a lot of memory

  2. Monitor the number of layers: compare the number of layers in use and memory to determine whether there is a memory leak. Is the scheme accurate in determining memory leaks? The Layer object is too far away from the business Widget and it is too difficult to trace the source

  3. Expando weak reference leak determination: Determine whether a specific object is leaked and return the reference chain. But we don't know which object should be monitored most in Flutter, and which object leakage is the main problem?

  4. Memory leak detection based on Heap Snapshot: Compare the growth of Heap objects of the Dart virtual machine at two different time points, and detect the suspicious objects that have leaked using two indicators: "class memory increment" and "object memory number". This is a general solution, but it is more valuable to efficiently locate the leaked object (Image, Layer). At present, the two problems of "determine the detection target" and "detection timing" are not easy to solve, so manual investigation and confirmation are required one by one, which is not efficient.

With reference to Android, LeakCanary can accurately and efficiently detect Activity memory leaks and solve the main problem of memory leaks. Can we also implement a set of such tools in Flutter? This should be a better plan. Before answering this question, first think about why LeakCanary chooses Activity as the object of memory leak monitoring and can solve the main memory leak problem?

We conclude that it meets at least the following three conditions:

  1. The memory referenced by the leaked object is large enough: the memory referenced by the Activity object is very large, which is the main problem of memory leakage

  2. Ability to fully define memory leaks: Activity has a clear life cycle and exact recovery timing, leaks are fully defined, can be automated, and improve efficiency

  3. The risk of leakage is high: the Activity base class is Context, which is passed as a parameter and is used very frequently, and there is a high risk of leakage

The three conditions reflect the necessity of monitoring objects and the operability of monitoring tools.

Following this idea, if we can find objects that meet the above three conditions in Flutter and monitor them, then we can build a set of Flutter's LeakCanary tools to solve the main problem of memory leaks in Flutter.

Looking back at the memory leak problem that has been solved recently from the actual project, the memory surge is reflected in the Image, Picture objects, as shown in the figure below.

Although Image and Picture have a high memory footprint and are the main contributors to memory leaks, they cannot be targeted for our monitoring because they obviously do not meet the three conditions listed above:

  1. The memory occupancy is large, because the number of objects is large, and it is added up, not caused by a certain Image reference

  2. Unable to define when it is leaked, there is no clear life cycle

  3. It will not be passed as a commonly used parameter, and the place of use is relatively fixed, such as RawImage Widget

In-depth analysis of Flutter rendering, concluded that the root cause of Image and Picture leaks is the BuildContext leak. And BuildContext just meets the three conditions listed above (detailed later), which seems to be the object we are looking for, and it seems good to implement a set of monitoring BuildContex leaks.

Please remember these 3 conditions, we will often use them later in the description.

Why monitor BuildContext

What memory is referenced by BuildContext?

BuildContext is the base class of Element. It directly references Widget and RenderObject. The relationship between its classes is also the relationship between Element Tree, Widget Tree, and RenderObject Tree formed by them. The class relationship is shown in the figure below.

Focus on Element Tree:

  • The construction of the three trees is built by Element's mount / unmount method

  • The parent and child Elements strongly refer to each other, so Element leakage will cause the entire Element Tree to leak, together with strong references to the corresponding Widget Tree and RenderObject Tree, which is quite considerable.

  • The field that strongly references Widget and RenderObject in Element will not be actively set to null, so the release of the three trees depends on the Element being recycled by the GC

Widget Tree represents the referenced Widget, such as RawImage Widget that references Image.

RenderObject Tree will generate Layer Tree, and will strongly reference ui.EngineLayer (c++ allocates memory), so Layer-related rendering memory will be held by this tree.

In summary, BuildContext refers to 3 trees in Flutter. therefore:

  1. The memory referenced by BuildContext is large and meets condition 1

  2. BuildContext is frequently used in business code, passed as a parameter, etc., has a high risk of leakage, and meets condition 3

How to monitor BuildContext

Can the leak of BuildContext be fully defined?

From the perspective of the life cycle of Element:

The important point is to determine when the Element will be discarded by the Element Tree, and will not be used again, and will be recycled by the subsequent GC.

The finalizeTree processing code is as follows:

// flutter_sdk/packages/flutter/lib/src/rendering/binding.dart
mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureBinding, RendererBinding, SemanticsBinding {
  @override
  void drawFrame() {
    ...
    try {
      if (renderViewElement != null)
        buildOwner.buildScope(renderViewElement);
      super.drawFrame();
      // 每一帧最后回收从 Element 树中移除的 Element
      buildOwner.finalizeTree();
    } finally {

    }
  }
}

// flutter_sdk/packages/flutter/lib/src/widgets/framework.dart
class BuildOwner {
  ...
  void finalizeTree() {
    try {
      // _inactiveElements 中记录不再使用的 Element
      lockState(() {
        _inactiveElements._unmountAll(); // this unregisters the GlobalKeys
      });
    } catch() {
    }
  }
  ...
}

// flutter_sdk/packages/flutter/lib/src/widgets/framework.dart
class _InactiveElements {
  ...
  void _unmountAll() {
    _locked = true;
    // 将 Element 拷贝到临时变量 elements 中
    final List<Element> elements = _elements.toList()..sort(Element._sort);
    // 清空 _elements,当前方法执行完,elements 也会被回收,则全部 Element 正常情况下都会被 GC 回收。
    _elements.clear();
    try {
      elements.reversed.forEach(_unmount);
    } finally {

      assert(_elements.isEmpty);
      _locked = false;
    }
  }
  ...
}
复制代码

The finalize stage _inactiveElements saves the Elements that are discarded by the Element Tree and will no longer be used; after the unmount method is executed, it is waiting to be reclaimed by the GC.

Therefore, Element leakage can be defined as: After umount is executed, and there are still references to these Elements after GC, it means that the Element has a memory leak. Meet condition 2.

Memory leak detection tool

Tool description

We have 2 requirements for memory leak tools:

  1. accurate. Including core object leak detection: image, layer, state, which can solve more than 90% of Flutter's memory leak problems

  2. Efficient. No sense of business, automatic detection, optimized reference chain, and quickly locate the source of leakage

accurate

From the above description, BuildContext is undoubtedly the object most likely to cause a large memory leak, and it is the best object to monitor. In order to improve accuracy, we also monitor the most commonly used State objects.

Why add monitoring of the State object?

Because business logic control is implemented in State, the transfer of "closures or methods" implemented in business can easily lead to State leakage. Examples are as follows.

class MainApp extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return _MainAppState();
  }
}

class _MainAppState extends State<MainApp> {
  @override
  void initState() {
    super.initState();
    // 注册这个回调,这个回调如果没有被反注册或者被其他上下文持有,都会导致 _MainAppState 泄漏。
    xxxxManager.addListerner(handleAction);
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
    );
  }

  // 1个回调
  void handleAction() {
    ...
  }
}
复制代码

Which memory associated with State will be leaked?

Combined with the following code, the leak will definitely lead to the leak of the associated Widget, and if the memory associated with the Widget is an Image or GIF, the leaked memory will also be large. At the same time, the State may also be associated with some other strongly referenced memory.

// flutter_sdk/packages/flutter/lib/src/widgets/framework.dart
abstract class State<T extends StatefulWidget> with Diagnosticable {
  // 强引用对应的 Widget 泄漏
  T _widget;
  // unmount 时候,_element = null, 不会导致泄漏
  StatefulElement _element;
  ...
}

// flutter_sdk/packages/flutter/lib/src/widgets/framework.dart
class StatefulElement extends ComponentElement {
  ...
  @override
  void unmount() {
    ...
    _state.dispose();
    _state._element = null;
    // 其他地方持有,则导致泄漏。unmount 后 State 仍被持有,可作为一个泄漏定义。
    _state = null;
  }
  ...
}
复制代码

Therefore, our plan will monitor the BuildContext associated with large memory and the State of business operations to improve the accuracy of the entire plan.

Efficient

How to realize automatic and efficient memory leak detection?

First of all, how do we know whether an object is leaking? Taking BuildContext as an example, we adopt a method similar to "Java Object Weak Reference" to determine object leakage:

  1. Put the inactiveElements of the finalizeTree stage in the weak Reference map

  2. Check the weak Reference map after Full GC, if it still holds the unreleased Element, it is judged as a leak

  3. Output the size associated with the leaked Element, the corresponding Widget, and the leaked reference chain information

Although Dart does not directly provide the "weak reference" detection capability, our Hummer engine fully implements the "weak reference leak detection" function from the bottom. Here is a brief introduction to its leak detection interface:

// 添加需要检测泄漏的对象,类似将对象放到若引用map中
external void leakAdd(Object suspect, {
    String tag: '',
});
// 检测之前放入的对象是否发生了泄漏,会进行 FullGc
external void leakCheck({
    Object? callback,
    String tag: '',
    bool clear: true,
});
external void leakClear({
    String tag: '',
});
external String leakCount();
external List<String> leakTags();
复制代码

Therefore, to realize automatic detection, we only need to clarify the timing of leakAdd() and leakCheck() calls.

timing of leakAdd

The timing of BuildContext is in the unmount process of finalizeTree:

// flutter_sdk/packages/flutter/lib/src/widgets/framework.dart
class _InactiveElements {
  ...
  void _unmount(Element element) {
        element.visitChildren((Element child) {
      assert(child._parent == element);
      _unmount(child);
    });

    // BuildContext 泄漏 leakAdd() 时机
    if (!kReleaseMode && debugMemoryLeakCheckEnabled && null != debugLeakAddCallback) {
      debugLeakAddCallback(_state);
    }

    element.unmount();
    ...
  }
  ...
}
复制代码

The timing of State is in the unmount process of the corresponding StatefulElement:

// flutter_sdk/packages/flutter/lib/src/widgets/framework.dart
class StatefulElement extends ComponentElement {
  @override
  void unmount() {
    _state.dispose();
    _state._element = null;

    // State 泄漏 leakAdd() 时机
    if (!kReleaseMode && debugMemoryLeakCheckEnabled && null != debugLeakAddCallback) {
      debugLeakAddCallback(_state);
    }

    _state = null;
  }
}
复制代码

LeakCheck timing

LeakCheck is essentially a point of time to detect whether there is a leak. We believe that Page exit is an appropriate time to perform memory leak detection in units of business pages. The sample code is as follows:

// flutter_sdk/packages/flutter/lib/src/widgets/navigator.dart
abstract class Route<T> {
  _navigator = null;
  // BuilContext, State leakCheck时机
  if (!kReleaseMode && debugMemoryLeakCheckEnabled && null != debugLeakCheckCallback) {
    debugLeakCheckCallback();
  }
}
复制代码

Tool implementation

Automated memory leaks with Page as the unit, according to usage scenarios, provide three memory leak detection tools.

  1. Hummer engine's deeply customized DevTools resource panel display, which can automatically/manually trigger memory leak detection

  2. Independent APP-side memory leak display, when Page leaks, the leaked object details will pop up

  3. Hummer engine seagull laboratory automatic detection, automatic memory leak details are given in the report

Tools 1 and 2 provide memory leak detection capabilities during the development process. Tool 3 can be used as an APP routine health test, automated testing and outputting test report results.

Anomaly detection example

Simulate the StatelessWidget in the Demo, and the leakage caused by the StatefulWidget being held by the BuildContext. The reason for the leak was that it was held statically and the Timer was held abnormally.

// 验证 StatelessWidget 泄漏
class StatelessImageWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // 模拟静态持有 BuildContext 导致泄漏
    MyApp.sBuildContext.add(context);

    return Center(
        child: Image(
          image: NetworkImage("https://avatars2.githubusercontent.com/u/20411648?s=460&v=4"),
          width: 200.0,
        )
    );
  }
}

class StatefulImageWidget extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return _StatefulImageWidgetState();
  }
}

// 验证 StatefulWidget 泄漏
class _StatefulImageWidgetState extends State<StatefulImageWidget> {
  @override
  Widget build(BuildContext context) {
    if (context is ComponentElement) {
      print("sBuildContext add :" + context.widget.toString());
    }

    // 模拟被 Timer 异步持有 BuildContext 导致泄漏,延时 1h 用于说明问题
    Timer(Duration(seconds: 60 * 60), () {
      print("zw context:" + context.toString());
    });

    return Center(
        child: Image(
          image: NetworkImage("https://avatars2.githubusercontent.com/u/20411648?s=460&v=4"),
          width: 200.0,
        )
    );
  }
}
复制代码

Enter the two Widget pages to exit, and check the leaked results.

Tool 1-DevTools resource panel display:

StatefulElement leak detection, it can be seen that the StatefulImageWidget is held asynchronously by the Timer, which leads to leakage.

StatelessElement leak detection, it can be seen that StatelessImageWidget is statically held, which leads to leakage.

Tool 2-Independent app leak display:

The aggregation page displays all the leaked objects, and the details page displays the leaked objects and the object reference chain.

According to the leak chain given by the tool, the source of the leak can be found quickly.

Business combat

A certain content-based service of UC is characterized by multiple graphics, text and video content, and consumes a lot of memory. Previously, based on Flutter's native Observatory tools, we solved some State and BuildContext leaks (which took a long time and was quite painful). In order to verify the practical value of the tool, we restore the memory leak problem to verify. It turns out that the problems that we have been investigating hard before can be detected in an instant, and the efficiency is greatly improved. Compared with the Observatory tool for troubleshooting, it is simply a difference. Based on the new tools, we have successively discovered many memory leaks that were not detected before.

The leaked StatefulElent in this example corresponds to a heavyweight page, the Element Tree is very deep, and the associated leaked memory is considerable. After we solved this problem, the business crash rate due to OOM dropped significantly.

Our fellow developers of another pure Flutter APP reported that they knew that memory would increase and there would be leaks in some scenarios, but there was no effective means to detect and solve them. Connected to our tool for testing, the results detected a number of memory leaks in different scenarios.

Business classmates recognize this very much, which also gave us great encouragement to make this set of tools, because it can quickly solve practical problems and empower business.

to sum up

Starting from the reality of Flutter memory leaks, I summarized that the main memory consumption is mainly Image, Layer and the need to explore a set of efficient memory leak detection solutions. By learning from Android's leak-canary, we summarized the three conditions for finding leaked monitoring objects; by analyzing the three trees rendered by Flutter, we determined BuildContext as the monitoring object. In order to improve the accuracy of the detection tools, we added State monitoring and analyzed the necessity. Finally explored a set of efficient memory leak detection tools, its advantages are:

  • More accurate: including core leaked objects widget, Layer, State; directly monitor the source of the leak; fully define memory leaks

  • More efficient: automatic detection of leaked objects, shorter and direct reference chain

  • Business unawareness: reduce development burden

This is the industry's first set of memory leak detection tools with complete logic, high practical value, and efficient automation. It can be described as the strongest Flutter memory leak detection tool solution.

This solution can cover all the memory leak problems we currently encounter, greatly improve the efficiency of memory leak detection, and escort our business to Flutter. The current implementation of the solution is based on the Hummer engine and runs in debug and profile mode. In the future, online release mode detection will be explored to cover scenes that cannot be reproduced locally.

 

Guess you like

Origin blog.csdn.net/u013491829/article/details/109330808
Recommended