UME-Rich Flutter debugging tool

UME-Rich Flutter debugging tool

background

At present, the Flutter business scene on the author side of Watermelon Video has covered 80% (including the video playback scene). The core scene on the user side, including my Tab, is also Flutter. During the development process, some problems were exposed, debugging was difficult, and the IDE was left. Afterwards, it s like catching the blind, PM design QA acceptance process can not get useful information, I found a circle on the market, and there is no powerful debugging tool like iOS Flex, such as view size, level display, real-time modification of instance object properties , Network request crawling, log printing, file viewing, etc. Therefore, the Flutter basic team of Watermelon Video decided to develop UME to solve the above problems.

Introduction

UME (pronunciation: oil rice~) is a Flutter debugging toolkit, which integrates a wealth of debugging gadgets, designing UI, network, monitoring, performance, logger, etc., whether it is R&D, PM, or QA can be used.

Functions that have been implemented so far

Next, we will introduce the use effect and core implementation of some core functions in detail:

Detailed module

Widget information

You can view the size, name, file path, and number of lines of code of the currently selected widget. With this tool, even if you are not responsible for the development of this functional module, you can quickly find the current code.

So how can I get the information of the selected current widget, and the size RenderObjectcan be obtained by passing it. What about the code position of the widget? By WidgetInspectorServicethe getSelectedSummaryWidgetwill be able to get a json string, we look at its structure:

{
    "description":"Text",
    "type":"_ElementDiagnosticableTreeNode",
    "style":"dense",
    "hasChildren":true,
    "allowWrap":false,
    "locationId":0,
    "creationLocation":{
        "file":"file:///Users/.../example/lib/home/widgets/category_card.dart",
        "line":69,
        "column":15,
        "parameterLocations":[
            {
                "file":null,
                "line":70,
                "column":24,
                "name":"data"
            },
            ... 
        ]
    },
    "createdByLocalProject":true,
    "children":[
        {
            "description":"RichText",
            "type":"_ElementDiagnosticableTreeNode",
            "style":"dense",
            "allowWrap":false,
            "locationId":1,
            "creationLocation":{
                "file":"file://../packages/flutter/lib/src/widgets/text.dart",
                "line":425,
                "column":21,
                "parameterLocations":[
                    {
                        "file":null,
                        "line":426,
                        "column":7,
                        "name":"textAlign"
                    },
                   ...
                ]
            },
            "children":[],
            "widgetRuntimeType":"RichText",
            "stateful":false
        }
    ],
    "widgetRuntimeType":"Text",
    "stateful":false
}
 

Because there is too much data, a part is omitted, and then the required part can be found according to the corresponding key.

Widget level

You can view the tree level of the currently selected widget and the detailed build chain of its renderObject.

The acquisition of a build to the selected widget chain is relatively simple, by InspectorSelectionacquiring the current currentElement, and then use the debugGetDiagnosticChainmethod you can get the entire build a chain.

RenderObjectThe information is also nice to get through currentElementto get the current RenderObject, and then use the toStringmethod can get.

ShowCode

You can view the page code of the current page.

The main realization involves the following key points:

  1. Get the file name of the current page widget
  2. Find and read the script based on the file name of the dart script

Obtaining the file name is mainly WidgetInspectorServicerealized by using .

The read script is mainly used for VMServiceimplementation.

Get the current page widget file name
  • We get the renderObjectlist of the current page by traversing , and filter out the target widget we want according to the size.
  • As explained in the Widget information, we can get the json string through WidgetInspectorServicethe getSelectedSummaryWidgetmethod in.
  • The value of "creationLocation" extracted is the file address of the current widget in the development process.
  • The last part of the address string we intercepted is the file name where the current page code is located.
Find and read the script
  • VMServiceThe getScriptsmethod in can get the ID and file name of all library files under the current thread.
  • We can get the target library file id by comparing the file name.
  • Through VMServicethe getObjectmethod, we can get the object corresponding to the current id. We can get the library object by passing in the id of the library file we just got, read the sourceproperties of the object, and it is our source code.

Memory leak

LeakDetector is used to detect flutter memory leaks, and the overall implementation idea is LeakCannarysimilar to the tools of the Android platform . Use Expandoweak reference to hold the object to be detected, and use VMService to get the reference chain of the leaked object, and finally store and display the leaked information locally.

Dart VM Service  A set of web services provided by Dart. The data transfer protocol is JSON-RPC 2.0. Through the interface it provides, we can get some important information inside the Dart virtual machine. The whole process is described below:

  1. Get VMService service
  • ObservatoryUri

    • By Service.``getInfo``()acquiring ServiceProtocolInfo, removed fromserverUri
    • Convert the above http format uri format to ws://format through vm_servicethe util tool method inconvertToWebSocketUrl()
    • Get the VmService service object, vm_service_iothere is a vmServiceConnectUri()method in the file, observatoryUriyou can get a VmService object by passing in one
  1. Get isolateId
  • Get the VM object through the getVM method of VmService, all IsolateRef are stored in the VM object

    • By Service.getIsolateID(Isolate.current)getting it, it is only valid under debug, and null under release
  1. Get libraryId
  • After getting the isolateId in step 2, then call the VmService to getIsolateget the corresponding Isolate object.
  • Traverse the library field of Isolate, which is a List of LibraryRef, and then take the uri of the current Library to the uri that matches the LibraryRef in the List to get the id of the LibraryRef.
  • Hold the isolateId and the Id of the LibraryRef and call the getObject method of VmService to get the Library. The id field is the libraryId we are looking for (in fact, the id of the LibraryRef should be the same, which can actually be tested).
  1. Get objectId

Due to the getInstance(isolateId, classId, limit)performance and limit limitations of the invoke(isolateId, targetId, selector, argumentIds, disableBreakpoints)method , we use the method instead . With the help of the Library top-level function, we can get the libraryId, which is the targetId in the invoke method. Finally, we only need to temporarily store the target object and then retrieve it through the invoke method. We got the InstanceRef of the object, and then got the id field is the objectId we are looking for.

  1. Leak Judgment
  • Get the Obj instance of Expando's object through the getObject(isolateId, objectId) method. Its real type is actually an Instance.
  • Traverse the fields of Instance to find the _data (the type of _data is ObjRef, you can get its corresponding Instance instance) field (how to find _data? You can use the FieldRef field of BoundField, and then match the name of FieldRef to'_data'), in expando_path.dartWe can see the specific implementation of Expando, the _data field is a List.
  • Traverse the _data field, if all are null, it means that the key objects we observe are all released; if the element is not null, then the element is converted to an Instance object (in fact, it is a WeakProperty), and the propertyKey field is our actual Objects that have not been recycled.
  1. Get reference path
  • VmService has a getRetainingPathmethod to get the reference chain of an object directly, but only one.
  • It should be noted that after using Expando to detect the memory leak, the reference of Expando to the original object is released.
  • The id of the Instance will expire, and the maximum cache size of VmService for it is 8192, so don't save the id but save the object.
  1. Trigger GC
  • VmService has a getAllocationProfile(isolateId, gc=true)method through which to trigger dart vm to perform gc. This is also the method finally called by the trigger gc button on the Dev Tools tool. According to the test, FULL GC is triggered.
  1. Trigger timing
  • Route detection

    • With the help of the NavigatorObservermechanism provided by the framework , you can easily monitor the incoming and outgoing pages of the stack, and trigger the leak detection of the route in the didPop, didRemove, and didReplace methods.
  • Widget/State detection

    • Generally, the Widget/State in the page is not detected, but only the Widget and State corresponding to the real page. The framework does not provide a global monitoring mechanism for page destruction. Here we use hook_annotation(this will be explained later) to hook two points: RouteRootState's initState method, which records the page object to be detected; State's dispose method, if it is a page we have recorded, triggers the detection process.

Memory view

Memory can be used to view the current occupancy of Dart VM objects.

If you need to get vm memory, you have to rely on Dart VM. As mentioned above, you can get it through the interface provided by vm_service.

By Future<MemoryUsage> getMemoryUsagewill be able to obtain current isolateinformation occupied, look at MemoryUsagethe structure, each attribute has a detailed explanation will not repeat them here.

///The amount of non-Dart memory that is retained by Dart objects. For
///example, memory associated with Dart objects through APIs such as
///Dart_NewWeakPersistentHandle and Dart_NewExternalTypedData.  This usage is
///only as accurate as the values supplied to these APIs from the VM embedder
///or native extensions. This external memory applies GC pressure, but is
///separate from heapUsage and heapCapacity.
int externalUsage;

///The total capacity of the heap in bytes. This is the amount of memory used
///by the Dart heap from the perspective of the operating system.
int heapCapacity;

///The current heap memory usage in bytes. Heap usage is always less than or
///equal to the heap capacity.
int heapUsage;
 

How to get the memory information of each class object?

By getAllocationProfileobtaining the assignment information by membersacquiring information of each class in the heap occupied properties.

Align ruler

The alignment ruler is used to measure a coordinate position of the screen where the current widget is located. After turning on the snap switch, the nearest widget can be automatically snapped.

The ruler is still very simple to display the current coordinates. You can change Positionedthe position through the coordinates of the gesture movement , and calculate the current distance through the size of the screen. The following will focus on the realization of automatic adsorption.

To absorb the nearest widget, you must find the widget where the current location is located, and then draw a size range of the current widget, and finally set the position of the ruler, then how to find the widget at the current coordinate?

Through globalKey, we can get one of the current page RenderObject, and then debugDescribeChildrenget all of its child nodes, and then describeApproximatePaintClipget to the current object coordinate system, Rectand then according to some coordinate conversion, determine whether it is in the current coordinate range, and finally RenderObjectSort according to the size, so that we can know that the smallest one must be the nearest widget in the current coordinate position. After getting the nearest widget, we only need to set the center position of the ruler to the four corners closest to the widget. can.

Color straws

You can view the color of any pixel on the current page to facilitate UI debugging.

This function is divided into two steps at first, 1. Enlarge the background 2. Get the color value of the current pixel

How to enlarge the picture

In Flutter in order to add some effects to the picture, we can use BackdropFilter, in fact, add a layer of filter effects, found parameters is not much by ImageFilterwill be able to add specific filters, want to do a zoom effect , We can use ImageFilter.matrixit, it can magnify the background picture, the filterQualityparameters can be used to set the quality of the magnification effect, how to magnify the corresponding position and the magnification multiple?

By Matrix4then it may be provided by our gesture to move the location, plus scalebe able to calculate its matrix parameters, and assigned toImageFilter.matrix can be obtained amplification.

How to get the picture pixel and color value

If you want to take a screenshot in Flutter, you must use RepaintBoundaryit, and globalKeywe can get the current screenshot of the current screen with cooperation .

RenderRepaintBoundary boundary = rootKey.currentContext.findRenderObject();
Image image = await boundary.toImage();
ByteData byteData = await image.toByteData(format: ui.ImageByteFormat.png);
Uint8List pngBytes = byteData.buffer.asUint8List();
snapshot = img.decodeImage(pngBytes);
 

After obtaining the shots, we need to get into the picture by moving the location of the current pixel value, and can be Imageof getPixelSafeto get to the pixel color values encoded with Uint32 a (#AABBGGRR), and finally we just need to abgrconvert argbit All right.

int abgrToArgb(int argbColor) {
  int r = (argbColor >> 16) & 0xFF;
  int b = argbColor & 0xFF;
  return (argbColor & 0xFF00FF00) | (b << 16) | r;
}
 

Network debugging

When debugging the Flutter network, it is very troublesome to mock data or view requests. You need to connect to a proxy and use a packet capture tool to perform these operations. If you want to simply complete these operations on your mobile phone, the network debugging module currently supports Features:

  • Supports crawling of all network requests

  • Data supports structured display, long press can be copied to clipboard

  • Collection request, displayed separately; clear non-favorite list

  • Request filtering and search (support partial matching, regular matching)

  • Request to export curl

  • Persistence and export HAR

  • mock response content

    • Full har file mapping
    • Modify a single field
  • Structured information long press to copy

Seeing this, you may ask how this intercepted all network requests?

Here, Dart's instrumentation at compile time is used to achieve the hook effect of a specific API (in fact, it is to replace the implementation of a method to add your own implementation). Due to space problems, I will not expand the specific process of Hook for the time being~ There will be another article detailing this.

Flutter all network requests are taking the package:http/src/base_client.dartmiddle BaseClientclass _sendUnstreamed, so we just need to hook_sendUnstreamed method can intercept all network requests.

Logger

The logs printed using the debugprint function will be displayed, especially some logs of the player. It is very convenient to view the logs without an IDE.

There are two ways to intercept print:

  • There is a runZonedmethod in Dart that can specify a zone for the execution object. Zone represents the environment range of a code execution. Zone is similar to a code execution sandbox. Different sandboxes are isolated. The sandbox can capture, intercept or modify some Code behaviors, such as log output, Timer creation, and microtask scheduling behaviors can be captured in Zone. At the same time, Zone can also capture all unhandled exceptions. runZoned(...) method definition:
R runZoned<R>(R body(), {
    Map zoneValues, 
    ZoneSpecification zoneSpecification,
    Function onError
}) 
zoneValues: Zone  zone[key] 
 

zoneSpecification: Some configuration of Zone, you can customize some code behaviors, such as intercepting log output behavior.

In this way, all actions that call the print method to output logs will be intercepted.

runZoned(() => runApp(MyApp()), zoneSpecification: new ZoneSpecification(
    print: (Zone self, ZoneDelegate parent, Zone zone, String line) {
      print(line);
}));
 
  • The way of hooking

Since the print method's hook may call log print to print caused by an infinite loop, where we just hook debugPrintmethod, package:flutter/src/foundation/print.dartthe debugPrintThrottledconduct hook can be.

Channel Monitor

You can view all channel calls, including method name, time, parameters, and return results.

hook package:flutter/src/services/platform_channel.dartin MethodChannelclassinvokeMethod method may be.

currently existing problems

At present, only the preliminary version has been completed. Many functions need to be improved and more new functions; I will continue to in-depth in some details; now the network debugging, channel monitoring, and Logger functions depend on the hook scheme, and the subsequent hook schemes are also Will consider open source.

summary

The above introduces some of the core functions and implementations of UME, and there are many rich functions that will not be expanded here due to space problems. There will be more interesting things later, and some core functions will be considered open source in the future.

join us

We are the R&D team responsible for the basic technology of Flutter, the watermelon video client. We are deeply involved in Flutter engineering and R&D tools to support rapid business iteration while improving the efficiency of Flutter development, debugging and packaging.

If you are passionate about technology, welcome to join the watermelon video Flutter basic technical team or the watermelon basic business team. At present, we have recruitment needs in Shanghai, Beijing, Hangzhou, and we can contact the email address : tech@bytedance.com for introductory information ; email subject: name-working years-watermelon-iOS/Android .

More sharing

An example of Go compiler code optimization bug location and repair analysis

ByteDance Breaks Federal Learning: Open source Fedlearner framework, 209% increase in advertising efficiency

Tik Tok Quality Construction-iOS Startup Optimization "Principles"

iOS performance optimization practice: How Toutiao Douyin achieves OOM crash rate reduction by 50%+


Welcome to pay attention to " ByteDance Technical Team "

Resume delivery contact email " tech@bytedance.com "