FlutterCodeX: A Code Coverage Solution for Flutter

By Junai at Xianyu Technology

Background

In recent years, after going through an architecture upgrade, Xianyu has used Flutter to implement a large number of pages. With continuous business iteration, the package size has also increased — the installation package size of Xianyu for Android and iOS has greatly increased compared with that of last year. And out of all this increase, 20% of the package size can be accounted to Flutter. To reduce this size, Xianyu needs to take Flutter-side engineering governance into consideration. Although Flutter is making continuous efforts to reduce the package size, there are no mature solution available yet. Hence, what we can do now is to remove invalid code segments.

Manual inspection largely relies on the business familiarity of developers and can go wrong due to negligence. Therefore, we need accurate statistics on code coverage information to enable code downsizing for businesses.

This article describes a code coverage solution for Flutter, that is, FlutterCodeX. This solution obtains code coverage by collecting and uploading statistics, according to the vcode instrumentation during class compilation and runtime data aggregation. FlutterCodeX promotes the deprecation of abandoned services to reduce the package size. At the same time, it monitors and improves engineering health.

Exploration of Code Instrumentation

For the calculation of the code coverage, the main difficulty lies in how to accurately determine whether a class has been called. Typically, we can add a code snippet when each class is initialized in order to mark that the class has been called. To achieve this, the easiest way is to add the code snippet to the constructor function. However, this method is extremely costly and does not support automation. So, are there any non-invasive instrumentation methods to counter this? The following compares different instrumentation solutions for iOS, Android, and Flutter.

iOS

In iOS, when ObjC calls the class initialization method for the first time, the system automatically marks the class as called after the initialization is completed. The RW_INITIALIZED bit, which is the 30th bit in the the flags field of data in metaClass, records whether the class is initialized. In this way, iOS can traverse all classes, and then aggregate and upload the resulting data at an appropriate time.

static BOOL MOCClassIsInitilatized(Class cls) {
void *metaClass = (__bridge void *)object_getClass(cls);
class_rw_t *rw = *(class_rw_t **)((uintptr_t)metaClass + 4 * sizeof(uintptr_t));
if(((class_rw_t *)((uintptr_t)rw & FAST_DATA_MASK))->flags & RW_INITIALIZED) {
return YES;
}
return NO;
}

Android

In Android, the Java language eliminates the need to intrude into the original code. Instead, we can introduce instrumentation code by adding static code blocks, and then add a compilation plug-in in buildscript, so that the code can be inserted when all class files are traversed during compilation.

public class A {
static {
// todo report class A initialize
}
}

Flutter

Flutter is somewhat different from the Android and iOS solutions. Dart does not have Java static code blocks or ObjC-like system flags. Then, where can we insert the code without intruding into the original code?

Theoretically, the initialization sequence of Dart Class is as follows:

  1. Class variables initialize on declaration (no static)
  2. Initializer list
  3. Superclass’s constructor
  4. MainClass’s constructor

By overriding the constructors, we can directly intrude into the original code. The diversity of Dart constructors also increases the difficulty of instrumentation automation. Therefore, overriding the constructors is not the best choice. Then, we are wondering, based on the initialization sequence, can we add new class members so that the instrumentation code can be called during initialization? For example,

class A {
bool isCodeX = ReportUtil.addCallTime('A');
// ...biz
}

However, in Dart, for a class with constant constructors, all its members are required to be final, and the members must be initialized in phases 1 and 2 or in the input parameters of the constructors. Even if a class is defined with the Extends clause or the With clause, the class also requires that all its subclasses and variables of its Mixins are final. Common components in Flutter, such as widgets, all use constant constructors. Therefore, instrumentation code cannot be inserted as class members.

class A {
final num x, y;
const A(this.x, this.y);
}

Code injection fails.

Is there any other way to do this? Can we hook up all the constructors through Aspect Oriented Programming (AOP)? Yes. AspectD, which has just been opened source by the Xianyu technical team, is ideal to this problem.

AspectD is an AOP programming framework for Dart, which implements dill transformation through Transform. AspectD can conveniently inject code in a non-invasive way.

In Flutter v1.12.13, the code injection test passes for these types of constructors, that is, constant constructors, no constructors, and constructors named as ClassName.identifier. The AspectD code is as follows:

@Aspect()
@pragma("vm:entry-point")
class CodeXExecute {
@pragma("vm:entry-point")
CodeXExecute();
@Call("package:flutter_codex_demo/test.dart", "A", "+A")
@pragma("vm:entry-point")
void _incrementA(PointCut pointcut) {
pointcut.proceed();
// todo report class A initialize
}
}

The working principle of AspectD will not be described in detail here. For more information, see https://github.com/alibaba-flutter/aspectd

Overall Design

The FlutterCodeX code coverage SDK consists of a compile-time instrumentation plug-in and a runtime data collection module.

The Code Instrumentation Plug-in

During compilation, the plug-in uses build_runner, CodeXGenerator, and CodeAstVisitor to perform Abstract Syntax Tree (AST) parsing on all classes in the project, traverse all class constructors, and automatically generate PointCut Execute class files and Hook class constructors. After a constructor is executed, the plug-in records class calling information for code instrumentation, and generates a complete class list of the project in the constructed package. The key code snippet is as follows:

CodeAstVisitor:// visit all class
void visitClassDeclaration(ClassDeclaration node) {
SourceNode sourceNode = SourceNode(source_path, node.name?.name);
node.members.forEach((ClassMember member) {
// find all constructor
if (member is ConstructorDeclaration) {
String constructorName = member.name?.name;
if (constructorName == null || constructorName.isEmpty) {
// ClassName Constructor
constructorName = sourceNode.name;
} else {
// ClassName.identifier Constructor
constructorName = (sourceNode.name ?? '') + "\\." + constructorName;
}
sourceNode.constructor.add(constructorName);
return;
}});
CodeXGenerator.collector.codeList[sourceNode.key()] = sourceNode;
}

The code generated by AspectD Execute is shown in the following figure. Class A has two constructors, and therefore two AspectD AOP functions are generated.

Runtime Data Collection Module

During runtime, after each class in the project is initialized, the addCallTime method will be automatically called to cache class calling information. Meanwhile, the data file collected will be compressed and uploaded when the user exits. Currently, Alibaba Cloud Object Storage Service (OSS) file upload is used to upload the file. The data collection module sets a sampling rate based on the active users of the app. The sampling hits at least 50,000 unique visitors (UVs).

Data Aggregation and Output

Finally, after the code runs for a period of time, we can summarize the resulting data and compare it with the class list in the constructed package to obtain code coverage data and drive package size shrinkage.

Take a Demo project as an example:

Summary

FlutterCodeX will soon be released in the Xianyu app. Combined with the code coverage data of Android and iOS clients, we will effectively promote the deprecation of abandoned services, help to shrink the package, and monitor and improve the long-term health of the project.

Original Source:

Follow me to keep abreast with the latest technology news, industry insights, and developer trends.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store