フューチャー技術ブログ

VSCode DevToolsによるWidget Buildタイムラインの確認

はじめに

こんにちは。TIGの藤田です。
Dart/Flutter連載 の2日目として、VSCodeのDevToolsを使用したWidget Buildの可視化についてご紹介します。

Flutterアプリの開発では、ウィジェットのビルド単位を考えてコードを記述/改修すると思います。
AndroidStudioのPerformance機能を使ってウィジェットのリビルドを確認している例は見かけるのですが、VSCodeでの確認方法を見かけなかったため調べてみました。予想以上に高機能で、今回使わなかった機能も含めて活用どころがありそうです。

内容

1. VSCode Dart DevTools
2. Widget Buildをタイムラインで確認する
3. 実装のWidget Buildへの影響を確認

VSCode Dart DevTools

Flutter公式のDevToolsは、VSCodeのDart Extension, Flutter Extensionのインストールと共にインストールされます。レイアウト構造を可視化/編集できるFlutter Inspector がよく使われると思いますが、他にもCPUやメモリ、Networkの可視化など多機能です。今回は、Performance view機能を使ってWidget Buildをタイムラインで確認してみます。

Widget Build をタイムラインで確認する

  1. devTools起動: 公式手順に従って、アプリの起動後にDevToolsを起動します。
  2. DevToolsのPerformanceタブを開きます。performance_tab.png
  3. 「Enhance Tracing」から、Widget Builds, Layouts, PaintsをTrackするように設定します。EnhanceTracing.png
  4. アプリを実行すると、タイムラインにFrameごとの処理時間が表示されます(#1)。Frame Time(UI)は、Dart VM内でビルドされるLayer treeと描画コマンドを含む軽量オブジェクトの作成時間を表しています。これらオブジェクトがGPUに渡されることでレンダリングが行われ、その実行時間が、Frame Time(Raster)になります。
  5. バーグラフをクリックすると、UIイベント, Raster(GPU)イベントそれぞれの内訳を確認することができます。UIイベントは、実装Dartコードを直接反映していて、Widgetレベルで実行イベントを確認できます。(#2)
  6. Raster(GPU)イベント(#3)は、UIイベントから作成されます。アプリのパフォーマンスを考える上では、UIグラフに課題がなくても、GPUグラフに課題があることもあります。
  7. 「Performance Overlay」ボタン(#4)をONにすると、アプリ画面に重ねる形で、UIグラフとGPUグラフを確認できます。

【補足】 公式ページに紹介されるパフォーマンス診断では、UIスレッドとGPUスレッドのプロファイルから実装に落とし込んで対処することを説明しており、実機を使用したprofile modeにて行うことを前提としています。今回はiOSシミュレータにて、DevToolsの使い方と、ソースコードがプロファイルに与える影響の確認方法を見てみたいと思います。

image.png

実装のWidget Buildへの影響を確認

例として、アニメーションの実装方法によるWidget Buildパターンの違いをタイムラインで確認します。今回はiOSシミュレータ(iPhone 13)を使用しています。

1) 全体ビルド(アンチパターン)。
bodyのアニメーションのためにsetState()することで、レイアウト全体をビルドしてしまっています。

import 'package:flutter/material.dart';

void main() => runApp(App());

class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Page(),
checkerboardOffscreenLayers: true,
);
}
}

class Page extends StatefulWidget {
@override
PageState createState() => PageState();
}

class PageState extends State<Page> with TickerProviderStateMixin {
late final _controller = AnimationController(vsync: this, duration: const Duration(seconds: 2));

@override
void initState() {
super.initState();
_controller.addListener(() => setState(() {}));
}

@override
void dispose() {
_controller.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('test animation'),
),
body: Center(
child: Opacity(
opacity: _controller.value,
child: Image.asset('assets/dash.png'),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
_controller
..reset()
..forward();
},
child: const Icon(Icons.refresh),
),
);
}
}

画面はこのようになります。Overlayされたグラフの上段がRaster(GPU)スレッド, 下段がUIスレッドを表しています。16msおきに補助ラインが引かれていますが、おおよそ16msを超えるFrameは描画されずにJankとなります。UIスレッド側に多くのJankが見られることから、この実装には課題がありそうだと分かります。

Frame実行時間のタイムラインを見ても、UIグラフに赤色のJank(slow frame)が多くなっています。
test1.png

UIイベントの内訳を見てみましょう。連続する2Frameをクローズアップしていますが、アニメーションには関係のないAppBarやFloatingActionButtonも、Frame毎にビルドしてしまっていることが分かります。今回はビルド対象が小さいですが、対象が大きければ更にコストがかかりそうです。

test1_ui.png

GPUイベントも確認してみます。こちらは、赤いグラフが見られなかったことからも大きな課題はなさそうです。

test1_raster.png

2) コードの改善
Frame毎のビルド範囲をアニメーション部分に限定するにはAnimatedBuilder等を用いる方法があります。ただし今回のケースは、以下のようにImage ウィジェットを使用することで、Frame毎のビルドをなくすことができます。

import 'package:flutter/material.dart';

void main() => runApp(App());

class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Page(),
checkerboardOffscreenLayers: true,
);
}
}

class Page extends StatefulWidget {
@override
PageState createState() => PageState();
}

class PageState extends State<Page> with TickerProviderStateMixin {
late final _controller = AnimationController(vsync: this, duration: const Duration(seconds: 2));
late final _animation = CurvedAnimation(parent: _controller, curve: Curves.easeIn);

@override
void initState() {
super.initState();
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('test animation')),
body: Center(
child: Image.asset(
'assets/dash.png',
opacity: _animation,
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
_controller
..reset()
..forward();
},
child: const Icon(Icons.refresh),
),
);
}
}

アプリ画面は以下になります。下段UIスレッドから、Jankがほぼなくなりました。少し見にくいですが、各グラフに平均実行時間が表示されていて、GPUスレッドは5.4ms/frame, UIスレッドは7.9ms/frameとなっています(改修前は、GPUスレッドが4.1ms/frame, UIスレッドが19.7ms/frameでした)。。

Frame実行時間のタイムラインを見ても、UIグラフに赤色のJank(slow frame)が見られません。平均43FPSとなっており、改修前の28FPSより改善しています。
test4.png

UIイベントの内訳を見てみると、Frame毎の「Build」処理自体がなくなっていることが分かります。
test4_ui.png

GPUイベントについては、画面Overlayグラフからもわかるように、改修前より少し実行時間が増えていますが、Jankは見られず課題はなさそうです。
test4_raster.png

まとめ

  • VSCodeのDevToolsを使って、Dart VM上のDartコード実行によるビルド(UIスレッド)と、GPU上のレンダリング(Rasterスレッド)のFrame毎の実行時間をタイムラインで可視化できます。
  • Jank Frameを1つの指標として、UIスレッド(Dartコード)の内訳を確認することで、実装コードの改善に利用できます。
  • 効果的なパフォーマンス改善には、他の観点も必要となります。
    • I/O処理(I/Oスレッド)は、パフォーマンス上コストが高くUIスレッドやGPUスレッドをブロックするため、その考慮が必要。
    • CPUやメモリメトリクスの考慮(DevToolsのうち、今回取り上げていない機能)
    • 実機(ユーザーが使用し得る一番遅いデバイス)での確認
  • パフォーマンス改善については、公式ページも参考に、今回紹介できなかった機能も活用していきたいところです。

参考リンク