Author: Lai Bin (Lai Yi)

For any technology stack, there will be an unavoidable hurdle, that is, performance optimization, and for how to perform performance optimization, the most important premise is to know the specific time distribution, to know the time distribution, you have to dot (timestamp), general performance dot are some scattered points, relatively messy, Tracing is a very elegant implementation of performance Tracing, which is presented in the form of waterfall streams. It is also known as a fire chart

Tracing Tracing as the name implies, Tracing the time distribution of each segment.

background

The diagram above is a part of the process of the Flutter Engine initialization process, which intuitively reflects the time distribution of each stage of the implementation process.

Tracing is one of the most powerful performance analysis tools in Chrome developer tools. It can record various activities between all Chrome processes. For example, you can record the call stack/time of C++ or JavaScript methods in each thread of each process, and not only that, you can also see the hierarchical relationship between view layers, The Trace Event Profiling Tool (About: Tracing) is introduced.

This paper will focus on the principles and practices of Flutter Engine, which can be divided into principles and practices. The principles will involve specific implementation, and the practices mainly include how to use, analyze and customize.

⚠️ : Flutter uses the word Timeline instead of Tracing. The Flutter Devtool also provides the Timeline tool (displaying the Tracing structure). These two terms are equivalent concepts. The Timeline mentioned below can be compared to Tracing.

The principle of article

The whole process of Timeline mainly includes initializing the Timeline and recording Tracing information.

Initialize the Timeline

The process for initializing the Timeline includes registering the Flag, setting the Flag, initializing the TimelineStream, and initializing the Timeline.

Registered Flag

A large number of flags are registered in Flutter to mark various functions. For the Timeline/Tracing function, the timeline_streams standard is as follows:

/path/to/engine/src/third_party/dart/runtime/vm/timeline.cc

DEFINE_FLAG(charp, timeline_streams, NULL, "Comma separated list of timeline streams to record. " "Valid values: all, API, Compiler, CompilerVerbose, Dart, " "Debugger, Embedder, GC, Isolate, and VM."); // After expansion:  charp FLAG_timeline_streams = Flags::Register_charp(&FLAG_timeline_streams, 'timeline_streams', NULL, "Comma separated list of timeline streams to record. " "Valid values: all, API, Compiler, CompilerVerbose, Dart, " "Debugger, Embedder, GC, Isolate, and VM.");Copy the code

Const char* charp;

The actual function is as follows:

/path/to/engine/src/third_party/dart/runtime/vm/flags.cc

const char* Flags::Register_charp(charp* addr,
                                  const char* name,
                                  const char* default_value,
                                  const char* comment) {
  ASSERT(Lookup(name) == NULL);
  Flag* flag = new Flag(name, comment, addr, Flag::kString);
  AddFlag(flag);
  return default_value;
}
Copy the code

Where addr_ is a union member, FLAG_timeline_streams starts with NULL as the default value of the current registered function.

The process of registering Flag is to define the FLAG_timeline_streams Flag.

Set the Flag

During the initialization of the Flutter Engine, the DartVm parameter can be passed through. For example, -trace-startup can be used to record the Tracing information during startup.

path/to/engine/src/flutter/runtime/dart_vm.cc

char* flags_error = Dart_SetVMFlags(args.size(), args.data());
Copy the code

Final call method:

/path/to/engine/src/third_party/dart/runtime/vm/flags.cc

char* Flags::ProcessCommandLineFlags(int number_of_vm_flags, const char** vm_flags) { ... while ((i < number_of_vm_flags) && IsValidFlag(vm_flags[i], kPrefix, kPrefixLen)) { const char* option = vm_flags[i] + kPrefixLen; Parse(option); i++; }... }Copy the code

The key step to validate Flag is SetFlagFromString in the Parse method

bool Flags::SetFlagFromString(Flag* flag, const char* argument) { ASSERT(! flag->IsUnrecognized()); switch (flag->type_) { ... case Flag::kString: { *flag->charp_ptr_ = argument == NULL ? NULL : strdup(argument); break; }... } flag->changed_ = true; return true; }Copy the code

Different variables will be set for different Flag types, and these variables are a union structure, as follows:

union {
    void* addr_;
    bool* bool_ptr_;
    int* int_ptr_;
    uint64_t* uint64_ptr_;
    charp* charp_ptr_;
    FlagHandler flag_handler_;
    OptionHandler option_handler_;
}
Copy the code

Depending on the nature of the union, different value types are available for different Flag types, so FLAG_timeline_streams values defined earlier will eventually be set to pass-through values. – trace_startup corresponding values for the Compiler, for example, the Dart, the Debugger, Embedder, GC, Isolate, VM.

The process of setting Flag is to specify the FLAG_timeline_streams value defined earlier.

TimelineStream initialization

FLAG_timeline_streams has a large number of type values, each defining a different Stream. The initialization process consists of three steps: Declare Stream, Get Stream, and Define Stream.

✎ Declare Stream

/path/to/engine/src/third_party/dart/runtime/vm/timeline.h

Stream declare #define TIMELINE_STREAM_DECLARE(name, fuchsia_name) \ static TimelineStream stream_##name##_; TIMELINE_STREAM_DECLARE (TIMELINE_STREAM_DECLARE) #undef TIMELINE_STREAM_DECLARE static TIMELINE_STREAM_DECLARE; static TimelineStream stream_Compiler_; static TimelineStream stream_Dart_; static TimelineStream stream_Embedder_; .Copy the code

The Timeline information in the Flutter Engine is stream_Embedder_. Other Timeline information includes Dart layer, API layer, etc. This article will focus on stream_Embedder_.

✎ Get Stream

/path/to/engine/src/third_party/dart/runtime/vm/timeline.h

Stream #define TIMELINE_STREAM_ACCESSOR(name, fuchsia_name) \ static TimelineStream* Get##name##Stream() { return &stream_##name##_; } TIMELINE_STREAM_LIST(TIMELINE_STREAM_ACCESSOR) #undef TIMELINE_STREAM_ACCESSOR GetAPIStream() { return &stream_API_; } static TimelineStream* GetDartStream() { return &stream_Dart_; } static TimelineStream* GetEmbedderStream() { return &stream_Embedder_; }...Copy the code

The corresponding static acquisition method is set.

Define Stream

/path/to/engine/src/third_party/dart/runtime/vm/timeline.cc

#define TIMELINE_STREAM_DEFINE(name, fuchsia_name) \ TimelineStream Timeline::stream_##name##_(#name, fuchsia_name, false); TIMELINE_STREAM_LIST(TIMELINE_STREAM_DEFINE) #undef TIMELINE_STREAM_DEFINE // TimelineStream Timeline::stream_API_("API", "dart:api", false); TimelineStream Timeline::stream_Dart_("Dart", "dart:dart", false); TimelineStream Timeline::stream_Embedder_("Embedder", "dart:embedder", false); .Copy the code

Timeline initialization

void Timeline::Init() { ASSERT(recorder_ == NULL); recorder_ = CreateTimelineRecorder(); ASSERT(recorder_ ! = NULL); enabled_streams_ = GetEnabledByDefaultTimelineStreams(); // Global overrides. #define TIMELINE_STREAM_FLAG_DEFAULT(name, fuchsia_name) \ stream_##name##_.set_enabled(HasStream(enabled_streams_, #name)); TIMELINE_STREAM_LIST(TIMELINE_STREAM_FLAG_DEFAULT) #undef TIMELINE_STREAM_FLAG_DEFAULT }Copy the code

1, create TimelineEventRecorder through CreateTimelineRecorder, if you need to start Tracing information creates TimelineEventEndlessRecorder, An unlimited amount of Trace information is recorded. 2. Set the set_enable function for the series of TimelineStream instances that you just created, and check whether the function is enable in the subsequent Timeline records.

Record the Timeline Information

The previous part mainly describes various information variables prepared for Timeline initialization, and this part mainly describes the process of recording Tracing information.

Tracing Tracing there are many methods to call Tracing messages, including recording synchronous events (TRACE_EVENT), asynchronous events (TRACE_EVENT_ASYNC), and event flows (TRACE_FLOW_). The following is about the call process of synchronous events. The whole process of other events is basically similar.

Synchronization events include TRACE_EVENT0, TRACE_EVENT1, and TRACE_EVENT2. The following uses TRACE_EVENT0 as an example:

{ TRACE_EVENT0("flutter", "Shell::CreateWithSnapshots"); } / / unfolds: : FML: : tracing: : TraceEvent0 (" flutter ", "Shell: : CreateWithSnapshots"); ::fml::tracing::ScopedInstantEnd __trace_end___LINE__("Shell::CreateWithSnapshots");Copy the code

It mainly includes two parts:

  • Record phase TraceEvent0, which records the current information
  • The flag ends ScopedInstantEnd, which is normally called at scope destructor time

TraceEvent0

TraceEvent0 will eventually call the following methods:

path/to/engine/src/third_party/dart/runtime/vm/dart_api_impl.cc

DART_EXPORT void Dart_TimelineEvent(const char* label, int64_t timestamp0, int64_t timestamp1_or_async_id, Dart_Timeline_Event_Type type, intptr_t argument_count, const char** argument_names, const char** argument_values) { ... TimelineStream* stream = Timeline::GetEmbedderStream(); ASSERT(stream ! = NULL); TimelineEvent* event = stream->StartEvent(); . switch (type) { case Dart_Timeline_Event_Begin: event->Begin(label, timestamp0); break; case Dart_Timeline_Event_End: event->End(label, timestamp0); break; . }... event->Complete(); }Copy the code

The whole process mainly includes four stages:

  • TimelineStream: : StartEvent: generate TimelineEvent, of which the Timeline: : GetEmbedderStream stream_Embedder_ () is the initialization phase.
  • TimelineEvent::Begin/End: records information such as the start and End time
  • TimelineEvent::Complete: Completes the current record
  • TimelineEventBlock: : Finish: report to record the information

✎ TimelineStream: : StartEvent

Stream ->StartEvent() will eventually call the following method to produce a TimelineEvent:

/path/to/engine/src/third_party/dart/runtime/vm/timeline.cc

TimelineEvent* TimelineEventRecorder::ThreadBlockStartEvent() { // Grab the current thread. OSThread* thread = OSThread::Current(); ASSERT(thread ! = NULL); Mutex* thread_block_lock = thread->timeline_block_lock(); . thread_block_lock->Lock(); // Will be held until CompleteEvent() is called... TimelineEventBlock* thread_block = thread->timeline_block(); if ((thread_block ! = NULL) && thread_block->IsFull()) { MutexLocker ml(&lock_); // Thread has a block and it is full: // 1) Mark it as finished. thread_block->Finish(); // 2) Allocate a new block. thread_block = GetNewBlockLocked(); thread->set_timeline_block(thread_block); } else if (thread_block == NULL) { MutexLocker ml(&lock_); // Thread has no block. Attempt to allocate one. thread_block = GetNewBlockLocked(); thread->set_timeline_block(thread_block); } if (thread_block ! = NULL) { // NOTE: We are exiting this function with the thread's block lock held. ASSERT(! thread_block->IsFull()); TimelineEvent* event = thread_block->StartEvent(); return event; }... thread_block_lock->Unlock(); return NULL; }Copy the code

1. The thread lock is called first and holds the recording process until CompleteEvent() is called. 2. If there is no TimelineEventBlock, one is created first and logged in the current thread. 3. If TimelineEventBlock is full, Finish (see below), create a new one, and record it. 4. Finally, a new TimelineEvent will be created in TimelineEventBlock. Each TimelineEventBlock will create a maximum of 64 TimelineEvents.

If it is TimelineEventEndlessRecorder create infinite TimelineEventBlock, otherwise there will be a limit.

✎ TimelineEvent: : Begin/End

/path/to/engine/src/third_party/dart/runtime/vm/timeline.cc

void TimelineEvent::Begin(const char* label,
                          int64_t micros,
                          int64_t thread_micros) {
  Init(kBegin, label);
  set_timestamp0(micros);
  set_thread_timestamp0(thread_micros);
}
Copy the code

These phases mainly record specific information, including:

Init: Record the name of the event tag, the type of the event (kBegin, kEnd). End is usually called during scope destructor (as discussed below). 2. Micros: Record the timestamp of running after the system is started. Thread_micros: Records the time stamp of the CPU running on this thread.

✎ TimelineEvent: : Complete

The final call method is as follows:

/path/to/engine/src/third_party/dart/runtime/vm/timeline.cc

void TimelineEventRecorder::ThreadBlockCompleteEvent(TimelineEvent* event) { ... // Grab the current thread. OSThread* thread = OSThread::Current(); ASSERT(thread ! = NULL); // Unlock the thread's block lock. Mutex* thread_block_lock = thread->timeline_block_lock(); . thread_block_lock->Unlock(); }Copy the code

The Complete method is called at the end of a record, and the synchronization Lock that started the Lock is finally released.

✎ TimelineEventBlock: : Finish

In TimelineStream: : StartEvent created in TimelineEventBlock mentioned, the default maximum is 64, after full will call Finsih method.

void TimelineEventBlock::Finish() {
...
  in_use_ = false;
#ifndef PRODUCT
  if (Service::timeline_stream.enabled()) {
    ServiceEvent service_event(NULL, ServiceEvent::kTimelineEvents);
    service_event.set_timeline_event_block(this);
    Service::HandleEvent(&service_event);
  }
#endif
}
Copy the code

The event information is sent to ServiceIsolate for processing. ServiceIsolate is a back-end service that is created during Dart VM initialization. Messages displayed by DevTool (including Tracing messages) can be obtained by communicating with ServiceIsolate.

ScopedInstantEnd

path/to/engine/flutter/fml/trace_event.h

class ScopedInstantEnd {
 public:
  ScopedInstantEnd(const char* str) : label_(str) {}
  ~ScopedInstantEnd() { TraceEventEnd(label_); }
 private:
  const char* label_;
  FML_DISALLOW_COPY_AND_ASSIGN(ScopedInstantEnd);
};
Copy the code

As you can see, TraceEventEnd is called in the destructor, which means that when out of scope, TraceEventEnd is called, and TraceEventEnd ultimately calls the TimelineEvent::End stage to record information.

The above is the routing process of the overall Tracing information, which uses a large number of macros. Macros are easy to implement in the development stage, but there are certain obstacles for reading the source code, and it is not intuitive to conduct code search.

practice

This section mainly introduces the use of Timeline, performance analysis, useful Debug parameters, and adding user-defined Tracing nodes.

Timeline using

The use of Timeline has been explained in detail in the official document. You can view the document directly Using the Timeline view-flutter.

Startup performance analysis

The Timeline tool can only analyze the runtime of a Flutter page after it is started. The whole Flutter startup process is not analyzed at all. The startup/initialization process is also critical.

There is little official documentation about startup performance analysis. Only this one, Measuring App Startup time-flutter, has been found so far.

Boot performance analysis includes three steps: adding boot performance parameters, obtaining Tracing information, and analyzing.

Add startup Parameters

The boot time Tracing information can be retrieved only after specific parameters have been added.

Sing sing sing sing sing sing sing sing Sing Sing Sing Sing Sing

flutter run --trace-startup --profile
Copy the code

The build/start_up_info.json file is generated by running the flutter App using the FLUTTER CLI parameters in the current folder.

Unfortunately, this file only produces four key timestamps, far from being able to analyze them. After following up the source code of Flutter Tools, the key source code is as follows:

path/to/flutter/packages/flutter_tools/lib/src/tracing.dart

/// Download the startup trace information from the given observatory client and /// store it to build/start_up_info.json. Future<void> downloadStartupTrace(VMService observatory, { bool awaitFirstFrame = true }) async { final Tracing tracing = Tracing(observatory); final Map<String, dynamic> timeline = await tracing.stopTracingAndDownloadTimeline( awaitFirstFrame: awaitFirstFrame, ); . final Map<String, dynamic> traceInfo = <String, dynamic>{ 'engineEnterTimestampMicros': engineEnterTimestampMicros, }; . traceInfo['timeToFrameworkInitMicros'] = timeToFrameworkInitMicros; . traceInfo['timeToFirstFrameRasterizedMicros'] = firstFrameRasterizedTimestampMicros - engineEnterTimestampMicros; . traceInfo['timeToFirstFrameMicros'] = timeToFirstFrameMicros; . traceInfo['timeAfterFrameworkInitMicros'] = firstFrameBuiltTimestampMicros - frameworkInitTimestampMicros; . traceInfoFile.writeAsStringSync(toPrettyJson(traceInfo)); }Copy the code

It can be seen that the key four timestamps are saved in the Map for output to the file. The most important point is that the entire timeline data has been obtained, so the following changes can be made:

/// Download the startup trace information from the given observatory client and /// store it to build/start_up_info.json. Future<void> downloadStartupTrace(VMService observatory, { bool awaitFirstFrame = true }) async { final Tracing tracing = Tracing(observatory); final Map<String, dynamic> timeline = await tracing.stopTracingAndDownloadTimeline( awaitFirstFrame: awaitFirstFrame, ); . / / the original start_up_info. Json generation traceInfoFile. WriteAsStringSync (toPrettyJson (traceInfo)); . TraceEventsFilePath = globals.fs.path.join(getBuildDirectory(), 'start_up_trace_events.json'); final File traceEventsFile = globals.fs.file(traceEventsFilePath); final List<Map<String, dynamic>> events = List<Map<String, dynamic>>.from((timeline['traceEvents'] as List<dynamic>).cast<Map<String, dynamic>>()); traceEventsFile.writeAsStringSync(toPrettyJson(events)); }Copy the code

After the upgrade, a build/start_up_trace_events.json file will be generated in the current directory and displayed in Chrome ://tracing. One note is that you need to regenerate the FLUTTER Command after modifying the FLUTTER Tools code. For details, see the documentation. Wiki · GitHub The flutter tool · flutter/flutter Wiki · GitHub

The above scenario can be used to analyze startup performance of the whole Flutter App. However, it cannot be used for the Add to App scenario because this scenario cannot be transparently transmitted through the Flutter CLI.

Yueshui Sing Add To App

In this scenario, parameters need to be passed through through the Platform layer.

Android

The method for transparent parameter transmission on Android is as follows:

path/to/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java

public FlutterEngine(
      @NonNull Context context,
      @NonNull FlutterLoader flutterLoader,
      @NonNull FlutterJNI flutterJNI,
      @NonNull PlatformViewsController platformViewsController,
      @Nullable String[] dartVmArgs,
      boolean automaticallyRegisterPlugins) {
......
}
Copy the code

You can instantiate FlutterEngine by adding –trace-startup to dartVmArgs.

new FlutterEngine(mPlatform.getApplication().getApplicationContext(),
                    FlutterLoader.getInstance(),new FlutterJNI(),new String[]{"--trace-startup"},true);
Copy the code

iOS

According to the iOS source code, the corresponding dartVmArgs parameter is not passed through in the construction parameters of FlutterEngine.mm. The real parameter conversion is as follows:

path/to/engine/src/flutter/shell/platform/darwin/common/command_line.mm

fml::CommandLine CommandLineFromNSProcessInfo() {
  std::vector<std::string> args_vector;
  for (NSString* arg in [NSProcessInfo processInfo].arguments) {
    args_vector.emplace_back(arg.UTF8String);
  }
  return fml::CommandLineFromIterators(args_vector.begin(), args_vector.end());
}
Copy the code

[NSProcessInfo processInfo].arguments [NSProcessInfo].arguments].arguments [NSProcessInfo].arguments].arguments

In most cases, however, XCode is not used to launch the App, so you need to modify the Engine code to implement parameter passing. PR was proposed to support pass-through of dartVm parameters.

path/to/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterDartProject.mm

- (instancetype)initWithDartVmArgs:(nullable NSArray<NSString*>*)args {
  return [self initWithPrecompiledDartBundle:nil dartVmArgs:args];
}
Copy the code

You can initialize flutterEngine. mm in the following ways:

_dartProject = [[FlutterDartProject alloc] initWithPrecompiledDartBundle:dartBundle dartVmArgs:@[@"--trace-startup"]];
_engine = [[FlutterEngine alloc] initWithName:@"io.flutter" project:_dartProject allowHeadlessExecution:YES];
Copy the code

Android Systrace

For Android devices, you can also use Android’s unique Systrace without changing any of the Flutter parameters.

Relevant reference documents: Understanding Systrace | Android Open Source Project Overview of the system tracing | Android Developers

Get the Tracing file

After adding startup parameters to Flutter, you need a tool to view them. The DevTool provided by Flutter provides this tool by default.

1. Get the Observatory address after startup. Attach –debug-uri= observatory_URL to the corresponding service and generate a debugger/profiler address. 3. After opening the debugger/ Profiler address, the default DevTool of Fluuter is displayed. Click the Timeline button to open the Tracing content.

Analyzing the Tracing file

The Tracing Event Profiling Tool (About: Tracing) can be viewed in The Chrome documentation.

The information displayed is intuitive. For startup performance analysis, time consumption of all parts can be seen intuitively. The following figure shows the general distribution of time consumption stages on iOS when Flutter is started.

The Debug parameter

Tracing Tracing time distribution mainly includes the time of each stage, but not all of the stages. This section describes two useful Debug parameters. Other relevant parameters refer to Debug flags: performance – Flutter

debugProfilePaintsEnabled

path/to/flutter/packages/flutter/lib/src/rendering/debug.dart

bool debugProfilePaintsEnabled = false;
Copy the code

This parameter displays the traversal of all Paint points during the Paint rendering phase. You can use this information to see if there are any useless node paints

debugProfileBuildsEnabled

path/to/flutter/packages/flutter/lib/src/widgets/debug.dart

bool debugProfileBuildsEnabled = false;
Copy the code

This parameter displays the traversal of all Widget node builds during the Widget Build phase. You can use this information to see if there are any useless node builds.

The figure above shows the build and paint phases. With this information, you need to analyze your Widget build/paint with your own business logic to see if it makes sense to perform useless operations and then optimize it.

Customize the Tracing node

If you need to check the time of a place that is not logged in by default, you can log in by yourself. For example, to view the duration of creating an IOSContext, perform the following operations:

std::unique_ptr<IOSContext> IOSContext::Create(IOSRenderingAPI rendering_api) { TRACE_EVENT0("flutter", "IOSContext::Create"); . FML_CHECK(false); return nullptr; }Copy the code

Finally, it will be reflected in Tracing, as shown below:

Afterword.

This article mainly analyzes the implementation of Flutter and some practices. Tracing is a standard format for Chrome implementation. Performance analysis of any technology stack can generate this standard format, and then open it to analyze using the existing Chrome DevTool. Very intuitive, can get twice the result with half the effort.