Flutter 性能优化的利器 —— Tracing

对于任何技术栈,都会有一个绕不过去的坎,那就是性能优化,而对于如何进行性能优化,最重要的前提就是需要知道具体的耗时分布,要知道耗时分布,就得打点(时间戳),一般的性能打点都是一些散点,比较凌乱,而本文要讲的 Tracing 则是性能打点的一种非常优雅的实现,它以瀑布流的形式呈现,非常直观,还有一个更直观的名字叫 火焰图

Tracing 顾名思义 —— 追踪每段耗时分布。

背景


上面这张图是 Flutter Engine 初始化过程中的一部分流程,非常直观的反应了执行流程中每个阶段的耗时分布。

Tracing 是 Chrome 开发者工具中强大的性能分析工具之一,它能记录 Chrome 所有进程间的各种活动。例如能记录每个进程中每个线程里 C++ 或者 JavaScript 方法的调用栈/耗时,不仅仅如此,还能看到视图 Layer 之间的层级关系,相关文档介绍 The Trace Event Profiling Tool (about:tracing)。

本文会专注在 Flutter Engine 中 Tracing 原理与实践,会分为原理篇与实践篇,原理篇会涉及到具体实现,实践篇主要包括如何使用、分析、定制。

⚠️:Flutter 中用 Timeline 这个词代替了 Tracing,Flutter Devtool 也提供了 Timeline 工具(展示的就是 Tracing 结构的信息)。这两个词是一个对等的概念,下文提到的 Timeline 可以和 Tracing 对等。

原理篇


整个 Timeline 的过程主要包括初始化 Timeline 与记录 Tracing 信息两个部分。

 

 初始化 Timeline

初始化 Timeline 包括四个过程:注册 Flag、设置 Flag、TimelineStream 初始化、Timeline 初始化。

注册 Flag

Flutter 中会注册非常多的 Flag 用于各种功能标记,对于 Timeline/Tracing 功能就是 timeline_streams 标实,具体如下:

/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.");


// 展开后:
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.");

其中 charp 为 typedef const char* charp;

真正执行的函数如下:

/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;
}

其中 addr_ 是一个 union 成员,初始值为当前注册函数的默认值为 NULL,即 FLAG_timeline_streams 初始值为 NULL。


注册 Flag 的过程就是定义了 FLAG_timeline_streams 标记。

设置 Flag

在 Flutter Engine 初始化的过程中,可以进行 DartVm 参数的透传,例如 —trace-startup,这个参数就可以记录启动时 Tracing 信息,会由如下方法进行设置:

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

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

最终调用方法:

/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++;
}
...
}

这里主要会进行 Flag 的有效性验证,关键步骤为 Parse 方法中的 SetFlagFromString

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;
}

会针对不同 Flag Type 设置不同变量,而这些变量是一个 union 结构体,如下:

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

根据 union 的特性,针对不同的 Flag Type,会得到不同值类型,可见之前定义的 FLAG_timeline_streams 值最终就会设置成透传的值。例如 —trace_startup 对应的值为 Compiler,Dart,Debugger,Embedder,GC,Isolate,VM。

设置 Flag 的过程就是具体设置了之前定义的 FLAG_timeline_streams 值。

TimelineStream 初始化

在 FLAG_timeline_streams 中非常多的类型值,每种都定义了不同的 Stream,初始化过程包括三个步骤:Declare Stream(申明)、Get Stream(获取)、Define Stream(定义)。

 Declare Stream

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

// stream 申明
#define TIMELINE_STREAM_DECLARE(name, fuchsia_name)                            \
  static TimelineStream stream_##name##_;
  TIMELINE_STREAM_LIST(TIMELINE_STREAM_DECLARE)
#undef TIMELINE_STREAM_DECLARE


// 展开后
static TimelineStream stream_API_;
static TimelineStream stream_Compiler_;
static TimelineStream stream_Dart_;
static TimelineStream stream_Embedder_;
....

Flutter Engine 中的 Timeline 信息为 stream_Embedder_,其它的 Timeline 也包括 Dart 层、API 层等等,本文主要会关注在 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


// 展开后
static TimelineStream* GetAPIStream() { return &stream_API_; }
static TimelineStream* GetDartStream() { return &stream_Dart_; }
static TimelineStream* GetEmbedderStream() { return &stream_Embedder_; }
...

设置了相应的静态获取方法。

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);
...


Timeline 初始化

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
}
  1. 通过 CreateTimelineRecorder 创建 TimelineEventRecorder,如果需要获取启动 Tracing 信息会创建 TimelineEventEndlessRecorder,会记录无上限的 Trace 信息。

  2. 设置刚才创建的一系列 TimelineStream 实例的 set_enable 函数,后续在进行 Timeline 记录的时候都会查询是否 enable。


  记录 Timeline 信息


上一部分主要讲了 Timeline 初始化准备的各种信息变量,这部分主要会讲记录 Tracing 信息的过程。


记录 Tracing 信息有非常多的调用方法,包括记录同步事件(TRACE_EVENT)、异步事件(TRACE_EVENT_ASYNC)、事件流(TRACE_FLOW_)。以下讲同步事件的调用过程,其他事件整个流程基本类似。


同步事件包括 TRACE_EVENT0 、TRACE_EVENT1、TRACE_EVENT2 等,以 TRACE_EVENT0 调用为例:

{
    TRACE_EVENT0("flutter", "Shell::CreateWithSnapshots");
}


// 展开后
::fml::tracing::TraceEvent0("flutter", "Shell::CreateWithSnapshots");
::fml::tracing::ScopedInstantEnd __trace_end___LINE__("Shell::CreateWithSnapshots");

主要包括两个部分:

  • 记录阶段 TraceEvent0,记录当前信息

  • 标记结束 ScopedInstantEnd ,一般在作用域析构时调用

TraceEvent0

TraceEvent0 最终会调用如下方法:

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();
}

整个过程主要包括四个阶段:

  • TimelineStream::StartEvent:生成 TimelineEvent,其中Timeline::GetEmbedderStream() 即为初始化阶段的 stream_Embedder_。

  • TimelineEvent::Begin/End:记录起始、结束的时间等信息

  • TimelineEvent::Complete:完成当前记录

  • TimelineEventBlock::Finish:上报记录的信息

✎ TimelineStream::StartEvent

stream->StartEvent() 最终会调用如下方法产生 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(); // 会一直持有,直到调用 CompleteEvent()
...
  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;
}
  1. 首先会调用线程锁,一直持有本次记录过程,直到调用 CompleteEvent()。

  2. 如果没有 TimelineEventBlock ,则首先会创建一个,并记录在当前线程中。

  3. 如果 TimelineEventBlock 满了,会先 Finish (见下文分析),再创建一个新的,并记录。

  4. 最后都会在 TimelineEventBlock 中创建一个新的 TimelineEvent,每个 TimelineEventBlock 创建的 TimelineEvent 会有数量限制,最多为 64 个。

⚠️:如果为 TimelineEventEndlessRecorder,则会无限创建 TimelineEventBlock,否则会有数量限制。

✎ 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);
}

这些阶段主要是记录具体的信息,包括:

  1. Init: 记录事件标签名,事情类型(kBegin,kEnd),End 一般会在作用域析构时调用(下面会分析)。

  2. micros: 记录系统启动后运行的时间戳。

  3. thread_micros: 记录该线程CPU运行的时间戳。

✎ TimelineEvent::Complete

最终调用方法如下:

/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();
}

一次记录结束后会调用 Complete 方法,并最终会释放一开始 Lock 的同步锁。

✎ TimelineEventBlock::Finish

在 TimelineStream::StartEvent 中创建的TimelineEventBlock 提到,默认最多是 64 个,满了之后会调用 Finsih 方法。

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
}

最终会将事件信息发送给 ServiceIsolate 来处理,关于 ServiceIsolate 简单可以理解为后端服务,是由 Dart VM 初始化的时候创建的, DevTool 显示的信息(包括 Tracing 信息)都会和 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);
};

可以看到析构函数中会调用 TraceEventEnd 方法,也就是说离开了作用域就会调用 TraceEventEnd 方法,而 TraceEventEnd 方法最终调用的就是 TimelineEvent::End 阶段进行信息记录。

以上就是整体的 Tracing 信息的路由过程,实现上使用了大量的宏,宏在开发阶段还是方便实现,不过对于阅读源码来说会有一定的障碍,不能直观的进行代码搜索查找。

实践篇


主要介绍 Timeline 的使用、启动性能分析、有用的 Debug 参数介绍、以及添加自定义 Tracing 节点。

 ▐  Timeline 使用

Timeline 的使用在官方文档中已经有详细的说明,Using the Timeline view - Flutter 直接看文档即可。

  启动性能分析

Timeline 工具仅仅只能分析 Flutter 页面启动之后的运行时情况,整个 Flutter 的启动过程完全是无法分析的,而启动/初始化过程也是比较关键的一环。


对于启动性能分析,官方文档描述甚少,目前只发现了这一处,Measuring app startup time - Flutter。


启动性能分析包括三个步骤:添加启动性能参数、获取 Tracing 信息、分析。

添加启动参数

只有添加了特定的参数后才能获取启动时 Tracing 信息。

✎ Flutter App 场景

flutter run --trace-startup --profile

主要是通过 flutter cli 命令行参数运行 Flutter App,最终会在当前目录下生成 build/start_up_info.json 文件。


可惜的是这个文件只产出了四个关键的 Timestamp,远远达不到能够分析的地步,跟进 Flutter Tools 源码后,关键源码如下:

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));
}

可以看到关键的四个 Timestamp 被保存在 Map 进行输出到文件,最关键的一点是整个 timeline 数据其实都已经拿到了,于是可以进行如下改造:

/// 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,
  );
......
// 原来的 start_up_info.json 生成
    traceInfoFile.writeAsStringSync(toPrettyJson(traceInfo));
......
// 新增 start_up_trace_events.json 生成
    final String 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));
}

改造后会在当前目录下生成 build/start_up_trace_events.json 文件,并通过 chrome://tracing 打开查看。有一个注意点,在改动 flutter tools 代码后,需要重新生成 flutter command ,具体可以看文档。The flutter tool · flutter/flutter Wiki · GitHub

上面这个场景对于整个 Flutter App 来讲是完全可以进行启动性能分析了,但是对于 Add to App 的场景还是无法满足,因为这种场景无法通过 flutter cli 来进行参数透传。

✎ Add To App 场景

对于这种场景,需要通过 Platform 层去透传参数。

Android

Android 侧参数透传方法如下:

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) {
......
}

通过实例化 FlutterEngine 时的构造参数 dartVmArgs 中添加 --trace-startup 即可。

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

iOS 侧通过源码查看,对应的 FlutterEngine.mm 的构造参数中是没有对应的 dartVmArgs 参数透传。真正参数转换的地方如下:

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());
}

通过 [NSProcessInfo processInfo].arguments 拿的命令行参数,无法通过自定义加入参数实现,对于从 XCode 启动 App 的可以通过编辑 schema 添加参数实现,示例如下:

但是绝大多数情况下,不会通过 XCode 来启动 App,因此还是需要通过修改 Engine 代码来实现参数传递。对此提了 PR 来支持 dartVm 参数的透传。

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];
}

初始化 FlutterEngine.mm 中可以通过如下方式初始化:

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

对于 Android 设备来讲,还可以用 Android 独有的 Systrace 来看,不需要改任何 Flutter 相关的参数。


相关参考文档:

Understanding Systrace  |  Android Open Source Project
Overview of system tracing  |  Android Developers

获取 Tracing 文件

添加了启动参数之后,需要有工具进行查看,Flutter 默认提供的 DevTool 默认就能进行查看,按如下步骤:

  1. 拿到启动后的 Observatory 地址。

  2. 通过 flutter attach --debug-uri=observatory_url attach 到对应的服务,会生成一个 debugger/profiler 地址。

  3. 打开 debugger/profiler 地址后就是 Fluuter 默认的 DevTool 工具,点击 timeline 按钮即可打开 Tracing 内容。

分析 Tracing 文件

关于 Tracing 工具的使用可以查看相关 Chrome 文档, The Trace Event Profiling Tool (about:tracing)。


展示的信息比较直观,对于启动性能分析,能非常直观的看到各个部分的耗时情况,下图是 Flutter 启动时 iOS 上的各个耗时阶段的大致分布,图的左边,可以看到各个阶段执行对应的线程。

  Debug 参数

上面介绍了如何获取 Tracing 的方法,生成的 Tracing 耗时分布主要包括各个阶段的耗时,但是还并不是包含所有的阶段,介绍两个有用的 Debug 参数,其他相关参数参考文档 Debug flags: performance - Flutter

debugProfilePaintsEnabled

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

bool debugProfilePaintsEnabled = false;

这个参数会在渲染 Paint 阶段,显示所有 Paint 时节点的遍历情况,可以根据这些信息查看是否有无用的节点 Paint

debugProfileBuildsEnabled

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

bool debugProfileBuildsEnabled = false;

这个参数会在 Widget Build 阶段,显示所有 Widget 节点 Build 时的遍历情况,可以根据这些信息查看是否有无用的节点 Build。

上图把 build、paint 阶段的过程全都显示出来了,有了这些信息后,还需要结合自身的业务逻辑分析 Widget Build/Paint 是否合理,是否执行了无用的操作,然后进行优化。

自定义 Tracing 节点

对于默认没有打点的地方,如果自己需要查看其耗时,则可以自行进行打点。例如需要查看创建 IOSContext 的耗时,则可以进行如下打点:

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

最终会反应在 Tracing 上,如下图:


后记


本文主要分析了 Tracing 在 Flutter 上的实现以及一些实践,Tracing 是 Chrome 实现的一种标准格式,任何技术栈的性能分析都可以生成这种标准格式,然后利用现成的 Chrome DevTool 工具打开即可分析,非常直观,能启到事半功倍的效果。

链接:

[The Trace Event Profiling Tool]

https://www.chromium.org/developers/how-tos/trace-event-profiling-tool

[Using the Timeline view]

https://flutter.dev/docs/development/tools/devtools/timeline
[Measuring app startup time]

https://flutter.dev/docs/testing/debugging?#measuring-app-startup-time
[flutter tool] 

https://github.com/flutter/flutter/wiki/The-flutter-tool
[Understanding Systrace]

https://source.android.com/devices/tech/debug/systrace
[Overview of system tracing]

https://developer.android.com/topic/performance/tracing
[Debug flags: performance]

https://flutter.dev/docs/testing/code-debugging?#debug-flags-performance
[Tracing 格式标准]

https://docs.google.com/document/u/0/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/mobilebasic

手淘客户端-小程序与跨平台技术团队

欢迎大家加入手淘客户端-小程序与跨平台技术团队!

我们是广泛支撑淘系业务的基础团队,团队正在维护的有手淘 H5 容器、Weex、淘宝小程序容器、淘宝首页都在用的动态化模板容器、还有正在做的 Flutter !淘系的基础架构、中间件、平台支撑是隔壁的兄弟团队,这里有丰富的业务场景和技术生态,机会与挑战并存,欢迎优秀的小伙伴加入,一起为新零售业务打造稳定的底盘!简历投递:laibin.lb@alibaba-inc.com

✿  拓展阅读

作者|来斌(来一)

编辑|橙子君

出品|阿里巴巴新零售淘系技术

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值