Back to Articles
Flutter Perfetto Trace Profiling Guide

Flutter Perfetto Trace Profiling Guide

1. How Perfetto Tracing Works with Flutter

Architecture Overview

Flutter’s tracing system is built into the engine layer (fml/trace_event.h) and uses a macro-based instrumentation approach. The engine defines trace macros such as TRACE_EVENT0, TRACE_EVENT1, TRACE_EVENT_ASYNC_BEGIN0, TRACE_EVENT_ASYNC_END0, TRACE_FLOW_BEGIN, TRACE_FLOW_STEP, and TRACE_FLOW_END. The primary trace category used throughout the engine is "flutter".

The tracing pipeline works as follows:

  1. Engine-level macros (TRACE_EVENT0("flutter", "EventName")) instrument key operations throughout the Flutter engine — frame scheduling, rasterization, platform channel calls, pointer dispatch, etc.
  2. Dart VM timeline (dart:developer) provides application-level instrumentation via Timeline and TimelineTask classes.
  3. Platform integration routes trace events to the appropriate system tracer:
    • On Android: events go to atrace (via /sys/kernel/debug/tracing/trace_marker) when --trace-systrace is used, making them visible in Perfetto alongside kernel/system events.
    • On Fuchsia: macros forward to native system tracing via <lib/trace/event.h>.
    • On other platforms: events go to Dart’s timeline infrastructure through registered event handlers.

What Gets Traced

LayerWhat’s TracedExample Events
Dart VMGC pauses, isolate lifecycle, compilationGC, CompileFunction
FrameworkWidget builds, layouts, paints, semanticsbuild, layout, paint, Animate
EngineFrame scheduling, rasterization, platform channel dispatchBeginFrame, SubmitFrame, PipelineConsume, PointerEvent
GPU/RasterSkia/Impeller draw calls, shader compilation, texture uploadsDrawDisplayList, ShaderCompilation
PlatformSystem events, vsync, SurfaceFlinger (Android 12+ Frame Timeline)Choreographer#doFrame, SurfaceFlinger

The engine uses flow events (TRACE_FLOW_BEGIN/TRACE_FLOW_END) to connect related operations across threads — for example, linking a pointer event from the platform thread through the UI thread to the raster thread.

Flutter’s Thread Model in Traces

When viewing a Flutter trace, you will see these threads:

  • Platform Thread (main thread): Plugin code, platform channel handling, UIKit/Activity lifecycle
  • UI Thread: Dart code execution, widget build/layout/paint, animation ticks
  • Raster Thread (GPU thread): Skia/Impeller rendering commands, GPU submission
  • I/O Thread: Image decoding, asset loading, expensive I/O

Impeller vs Skia Tracing Differences

With Impeller (now default on iOS and Android API 29+):

  • All shaders are precompiled at engine build time, so you will NOT see runtime shader compilation events (a major source of jank with Skia).
  • Impeller tags and labels all graphics resources (textures, buffers), providing better GPU-level observability.
  • Impeller can capture and persist animations to disk without affecting per-frame rendering performance.

With Skia (legacy, still used on web):

  • --trace-skia flag enables Skia-internal trace events, which can be very verbose.
  • You will see shader compilation events appearing as jank spikes (dark red in DevTools).

2. Enabling and Disabling Tracing in Flutter

Build Mode Requirements

FeatureDebugProfileRelease
Timeline tracingLimitedFullDisabled
DevTools connectionYesYesNo
Performance overlayNoYesNo
Representative perf dataNoYesYes

Profile mode is required for meaningful tracing. Debug mode has expensive assertions and JIT compilation that distort performance. Release mode strips all tracing and debugging infrastructure.

# Run in profile mode (required for tracing)
flutter run --profile

Compile-Time Flags

Tracing is architecturally enabled/disabled at compile time based on build mode:

  • Profile mode: Timeline events are recorded by default. The Dart VM is AOT-compiled but retains service extensions for DevTools connectivity and tracing.
  • Release mode: All tracing infrastructure is compiled out. Timeline.startSync/finishSync calls become no-ops. Zero overhead.
  • Debug mode: Timeline is available but performance data is unreliable due to JIT and assertions.

Runtime Configuration via Flutter CLI Flags

These flags are passed to flutter run:

# Enable systrace integration (Android, iOS, macOS, Fuchsia)
flutter run --profile --trace-systrace

# Write trace directly to Perfetto protobuf file
flutter run --profile --trace-to-file=/path/to/trace.perfetto-trace

# Trace application startup then exit
flutter run --profile --trace-startup

# Enable Skia internal tracing (verbose, Skia renderer only)
flutter run --profile --trace-skia

# Filter Skia traces to specific categories
flutter run --profile --trace-skia-allowlist=skia.gpu,skia.shaders

# Filter all traces to specific prefixes
flutter run --profile --trace-allowlist=flutter,dart

# Use infinite trace buffer instead of ring buffer
flutter run --profile --endless-trace-buffer

# Combine multiple flags
flutter run --profile --trace-systrace --endless-trace-buffer --trace-skia

Framework-Level Debug Flags

These flags enable additional framework timeline events (available in profile and debug modes):

import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';

// Track individual widget builds in timeline
debugProfileBuildsEnabled = true;

// Track layout passes in timeline
debugProfileLayoutsEnabled = true;

// Track paint operations in timeline
debugProfilePaintsEnabled = true;

These are also toggleable from DevTools under “Enhance Tracing” options:

  • Track Widget Builds: Shows build() method events with widget names
  • Track Layouts: Displays render object layout events
  • Track Paints: Shows render object paint events

3. Getting Perfetto Traces for Dart Code

Method 1: --trace-to-file (Direct Perfetto Protobuf)

The most direct way to get a Perfetto-compatible trace:

flutter run --profile --trace-to-file=my_trace.perfetto-trace

This writes the timeline trace in Perfetto’s native protobuf format. The resulting file can be opened directly at ui.perfetto.dev.

Method 2: --trace-systrace (System Tracer Integration)

On Android, this routes Flutter trace events through atrace, making them appear alongside system-level events in a Perfetto system trace:

# Step 1: Start Flutter app with systrace
flutter run --profile --trace-systrace

# Step 2: Capture a Perfetto trace (separate terminal)
adb shell perfetto \
  -c - --txt \
  -o /data/misc/perfetto-traces/trace \
<<EOF
buffers: {
    size_kb: 63488
    fill_policy: RING_BUFFER
}
data_sources: {
    config {
        name: "linux.ftrace"
        ftrace_config {
            atrace_categories: "gfx"
            atrace_categories: "view"
            atrace_categories: "wm"
            atrace_categories: "am"
            atrace_apps: "*"
        }
    }
}
data_sources: {
    config {
        name: "linux.process_stats"
        target_buffer: 1
        process_stats_config {
            scan_all_processes_on_start: true
        }
    }
}
duration_ms: 10000
EOF

# Step 3: Pull the trace
adb pull /data/misc/perfetto-traces/trace ./trace.perfetto-trace

The key config entry is atrace_apps: "*" (or your specific package name) which enables app-level atrace events from Flutter.

Method 3: --trace-startup (Startup Profiling)

Captures trace events from the very beginning of app launch, then exits. Combine with --endless-trace-buffer to prevent early events from being overwritten (they are independent flags):

flutter run --profile --trace-startup --endless-trace-buffer

The trace is saved to the build directory (or $FLUTTER_TEST_OUTPUTS_DIR if set). This is useful for diagnosing slow startup times.

Method 4: DevTools Timeline Export

  1. Run the app in profile mode: flutter run --profile
  2. Open DevTools (the URL is printed in the console)
  3. Go to the Performance tab
  4. Interact with your app to generate frames
  5. Click the export button (upper-right corner) to download a .devtools snapshot

Note: DevTools exports are in its own format (.devtools files), not native Perfetto protobuf. For Perfetto analysis, use --trace-to-file or --trace-systrace.

Method 5: flutter drive for Profiled Integration Tests

flutter test does not support --profile or tracing flags. Use flutter drive instead:

flutter drive \
  --profile \
  --trace-systrace \
  --driver=test_driver/integration_test.dart \
  --target=integration_test/app_test.dart

4. Custom Trace Events in Dart Code

dart:developer Timeline API

The Timeline class provides synchronous event tracking. All events are emitted as “Complete” events in the trace.

Note on arguments type: All Timeline APIs accept Map? (unparameterized), not Map<String, String>?. Using <String, String>{} is a common convention for readability in traces but any Map type works.

Basic Usage: startSync / finishSync

import 'dart:developer';

void processData(List<Item> items) {
  Timeline.startSync('processData', arguments: <String, String>{
    'itemCount': '${items.length}',
  });

  // Do expensive work...
  for (final item in items) {
    transform(item);
  }

  Timeline.finishSync();
}

Important: startSync/finishSync must complete before returning to the event queue. They are for synchronous operations only. Always ensure finishSync is called (use try/finally).

void riskyOperation() {
  Timeline.startSync('riskyOperation');
  try {
    doSomethingThatMightThrow();
  } finally {
    Timeline.finishSync();
  }
}

Convenience: timeSync

Wraps a synchronous function with automatic start/finish:

import 'dart:developer';

int computeExpensiveValue() {
  return Timeline.timeSync('computeExpensiveValue', () {
    // Automatically wrapped in startSync/finishSync
    var result = 0;
    for (var i = 0; i < 1000000; i++) {
      result += heavyComputation(i);
    }
    return result;
  }, arguments: <String, String>{
    'description': 'Main computation loop',
  });
}

Instant Events: instantSync

For point-in-time markers (no duration):

import 'dart:developer';

void onUserAction(String action) {
  Timeline.instantSync('UserAction', arguments: <String, String>{
    'action': action,
    'timestamp': '${DateTime.now().millisecondsSinceEpoch}',
  });
}

Reading the Current Timeline Timestamp

import 'dart:developer';

int microseconds = Timeline.now;  // Microsecond-precision timestamp

TimelineTask for Asynchronous Operations

TimelineTask represents operations that span across event loop turns or isolates:

import 'dart:developer';

Future<void> fetchAndProcessData(String url) async {
  final task = TimelineTask();

  task.start('fetchAndProcessData', arguments: <String, String>{
    'url': url,
  });

  try {
    // Network fetch (crosses event loop boundaries)
    final response = await httpClient.get(Uri.parse(url));

    task.start('parseResponse');
    final data = parseJson(response.body);
    task.finish();

    task.start('processData');
    await processInBackground(data);
    task.finish();
  } finally {
    task.finish();  // Finish the outer task
  }
}

Cross-Isolate Task Tracking

import 'dart:developer';
import 'dart:isolate';

Future<void> processInIsolate(List<int> data) async {
  final task = TimelineTask();
  task.start('processInIsolate');

  // Get task ID for cross-isolate transfer
  final taskId = task.pass();  // Must call pass() - requires empty stack

  await Isolate.run(() {
    // Reconstruct task in the new isolate
    final childTask = TimelineTask.withTaskId(taskId);
    childTask.start('isolateWork');
    // ... do work ...
    childTask.finish();
  });

  task.start('afterIsolate');
  // Continue in parent isolate
  task.finish();
  task.finish();
}

Convenience Wrapper for Async Tracing

A small generic wrapper guarantees the task finishes even when the closure throws:

Future<T> traceAsync<T>(String label, Future<T> Function() closure) async {
  final task = TimelineTask();
  task.start(label);
  try {
    return await closure();
  } finally {
    task.finish();
  }
}

final result = await traceAsync('fetchAndParse', () async {
  final data = await fetch();
  return parse(data);
});

Flow Events

Flow events create visual arrows in the trace viewer connecting related timeline slices across different tracks/threads. Flow.begin({int? id}) optionally accepts an ID (auto-generated if omitted), Flow.step(int id) and Flow.end(int id) require the flow ID:

import 'dart:developer';

class EventPipeline {
  void produceEvent(String data) {
    final flow = Flow.begin();
    Timeline.startSync('produceEvent', flow: flow);
    // Queue the data for processing...
    enqueue(data, flow.id);
    Timeline.finishSync();
  }

  void processEvent(String data, int flowId) {
    final flow = Flow.step(flowId);
    Timeline.startSync('processEvent', flow: flow);
    // Process the data...
    Timeline.finishSync();
    // Forward to next stage
    forward(data, flowId);
  }

  void consumeEvent(String data, int flowId) {
    final flow = Flow.end(flowId);
    Timeline.startSync('consumeEvent', flow: flow);
    // Final consumption
    Timeline.finishSync();
  }
}

Flow events appear as arrows in Perfetto UI connecting the produceEvent -> processEvent -> consumeEvent slices, even if they occur on different threads.

Practical Example: Instrumenting a Widget

import 'dart:developer';
import 'package:flutter/material.dart';

class ProductListView extends StatefulWidget {
  const ProductListView({super.key, required this.products});
  final List<Product> products;

  @override
  State<ProductListView> createState() => _ProductListViewState();
}

class _ProductListViewState extends State<ProductListView> {
  late List<Product> _sortedProducts;

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

  void _sortProducts() {
    Timeline.timeSync('ProductListView._sortProducts', () {
      _sortedProducts = List.of(widget.products)
        ..sort((a, b) => a.name.compareTo(b.name));
    }, arguments: <String, String>{
      'count': '${widget.products.length}',
    });
  }

  @override
  Widget build(BuildContext context) {
    return Timeline.timeSync('ProductListView.build', () {
      return ListView.builder(
        itemCount: _sortedProducts.length,
        itemBuilder: (context, index) {
          return Timeline.timeSync('ProductTile.build', () {
            return ProductTile(product: _sortedProducts[index]);
          });
        },
      );
    });
  }
}

5. Perfetto UI for Analyzing Flutter Traces

Opening Traces

  1. Navigate to ui.perfetto.dev
  2. Drag and drop the trace file, or click “Open trace file” in the sidebar
  3. Supported formats:
    • Perfetto protobuf (.perfetto-trace, .pb) — native format from --trace-to-file
    • Chrome JSON trace format (legacy)
    • Android systrace format

Key Tracks to Examine for Flutter Apps

When you open a Flutter trace, look for these tracks:

Per-Thread Tracks:

  • 1.ui (UI Thread): Dart code execution. Look for build, layout, paint, Animate slices. Long slices here mean your Dart code is too expensive.
  • 1.raster (Raster Thread): GPU rendering. Look for DrawDisplayList, GPURasterizer::Draw. Long slices here mean the scene is too complex to render.
  • 1.io (I/O Thread): Image decoding, asset loading.
  • 1.platform (Platform Thread): Plugin calls, platform channel messages.

System Tracks (when using --trace-systrace on Android):

  • SurfaceFlinger: Compositor behavior, frame presentation
  • Expected Timeline / Actual Timeline (Android 12+): Frame-by-frame jank classification with color coding:
    • Green = smooth
    • Red = janky (app’s fault)
    • Yellow = janky (SurfaceFlinger’s fault)
    • Blue = dropped frame
  • Choreographer: Vsync timing, frame callbacks
  • CPU frequency/scheduling: Which CPU cores are active, thread migrations
ActionShortcut
Zoom inW
Zoom outS
Pan leftA
Pan rightD
Select eventClick
Pan (alternative)Shift + Drag
Zoom (alternative)Ctrl + MouseWheel
Next event.
Previous event,
Fit selection to viewF
Pin trackPin icon in track header
Find track by nameCtrl + P
Command paletteCtrl + Shift + P

Analyzing Frame Performance

  1. Find janky frames: Look for gaps or unusually long slices in the UI or Raster thread tracks.
  2. Measure duration: Click on a slice to see its duration in the “Current Selection” tab.
  3. Area selection: Click and drag across a time range to see aggregated statistics. Press R to convert a single selection into an area selection.
  4. Follow flow arrows: Flow events (arrows) connect related operations across threads — follow them to understand the full lifecycle of a frame.

SQL Queries

Perfetto UI supports SQL queries for programmatic analysis:

-- Find all slices longer than 16ms on the UI thread
SELECT ts, dur, name
FROM slice
WHERE dur > 16000000  -- nanoseconds
  AND track_id IN (
    SELECT id FROM track WHERE name LIKE '%ui%'
  )
ORDER BY dur DESC;

-- Frame timeline jank analysis (Android 12+)
SELECT *
FROM actual_frame_timeline_slice
WHERE jank_type != 'None'
ORDER BY ts;

-- Find custom timeline events
SELECT ts, dur, name
FROM slice
WHERE name LIKE 'ProductListView%'
ORDER BY ts;

6. Performance Overhead of Tracing

When Tracing is Disabled (Release Mode)

  • Zero overhead. All Timeline.startSync/finishSync calls compile to no-ops.
  • The Dart AOT compiler eliminates tracing code paths entirely.
  • No trace buffer allocation, no event serialization.

When Tracing is Enabled (Profile Mode)

  • Base cost: Each trace event has a non-negligible cost of approximately 1-10 microseconds per event. This includes:

    • String serialization for event names
    • Argument map construction (if provided)
    • Timestamp capture (Timeline.now uses monotonic clock)
    • Buffer insertion (lock-free ring buffer or growing buffer)
  • With --trace-systrace on Android: Additional overhead from JNI calls and kernel-space writes to /sys/kernel/debug/tracing/trace_marker. Each event traverses: Dart -> Engine -> JNI -> kernel.

  • With --trace-skia: Significant additional overhead due to the high volume of Skia-internal events. Skia tracing is disabled by default specifically because the event volume can itself cause jank. Use --trace-skia-allowlist to limit to specific categories.

  • With enhanced tracing (Track Widget Builds/Layouts/Paints): The DevTools documentation explicitly warns these options “may impact frame times.” Each widget build, layout pass, and paint operation generates a separate timeline event, which can be substantial in complex UIs.

Overhead Mitigation Strategies

  1. Use --trace-allowlist to filter events to specific prefixes you care about, reducing total event volume.
  2. Use --trace-skia-allowlist instead of --trace-skia to limit Skia events to specific categories (e.g., skia.gpu,skia.shaders).
  3. Use --endless-trace-buffer for long recording sessions to avoid ring buffer overwrites, but be aware of memory growth.
  4. Minimize custom trace events in hot loops — instrument at a coarser granularity for functions called per-frame.
  5. Arguments maps are the most expensive part — avoid passing large argument maps in high-frequency events.

Mode Comparison

ModeTracing AvailableOverheadUse Case
DebugLimited (unreliable)Very high (JIT, assertions)Development only
ProfileFullLow-moderate (1-10us/event)Performance analysis
ReleaseNone (compiled out)ZeroProduction

7. Flutter’s Tracing CLI Flags in Detail

--trace-systrace

Purpose: Routes Flutter timeline events to the platform’s system tracer instead of (or in addition to) the internal timeline.

Supported platforms: Android, iOS, macOS, Fuchsia.

How it works on Android: Events are written via atrace to /sys/kernel/debug/tracing/trace_marker, making them visible in system-wide Perfetto traces alongside kernel scheduler events, SurfaceFlinger, Choreographer, and other system components.

When to use: When you need to correlate Flutter frame timing with system-level behavior (CPU scheduling, compositor timing, thermal throttling, other app interference).

flutter run --profile --trace-systrace

--trace-skia

Purpose: Enables Skia rendering engine internal trace events.

Why disabled by default: Skia emits an extremely high volume of trace events, which can itself cause performance degradation. The documentation notes: “By default, Skia tracing is not enabled to reduce event volume.”

When to use: When diagnosing raster thread issues — shader compilation, texture upload, draw call optimization. Most useful with an allowlist.

# All Skia events (very verbose)
flutter run --profile --trace-skia

# Filtered Skia events (recommended)
flutter run --profile --trace-skia --trace-skia-allowlist=skia.gpu,skia.shaders

Note: With Impeller (default on iOS/Android 29+), --trace-skia is irrelevant since Impeller is not Skia-based.

--trace-skia-allowlist

Purpose: Comma-separated list of Skia trace event category prefixes to include. All others are filtered out.

flutter run --profile --trace-skia --trace-skia-allowlist=skia.gpu

--trace-allowlist

Purpose: General-purpose filter for ALL trace events (not just Skia). Only events whose names start with one of the specified prefixes are recorded.

# Only record events starting with "flutter" or "dart"
flutter run --profile --trace-allowlist=flutter,dart

--trace-startup

Purpose: Captures trace events from the very start of app initialization, then automatically exits. Saves the trace to the build directory.

Behavior: Combine with --endless-trace-buffer to prevent early events from being overwritten (they are independent flags that must be specified separately).

flutter run --profile --trace-startup

--trace-to-file

Purpose: Writes the timeline trace to a file in Perfetto’s native protobuf format.

Output format: The file is directly loadable in ui.perfetto.dev.

flutter run --profile --trace-to-file=/tmp/my_trace.perfetto-trace

--endless-trace-buffer

Purpose: Uses a growing trace buffer instead of the default ring buffer.

Default behavior: Flutter uses a ring buffer, which overwrites old events when full. This is fine for most profiling but loses early events in long sessions.

When to use: When you need to capture very old events (e.g., startup tracing) or very long recording sessions.

flutter run --profile --endless-trace-buffer

Warning: Memory usage grows unbounded. Only use for targeted profiling sessions.

--cache-startup-profile

Purpose: Caches the CPU profile collected before the first frame for startup analysis.

flutter run --profile --cache-startup-profile

8. Programmatic Trace Capture

Using dart:developer for Recording Control

The dart:developer library provides APIs to programmatically control timeline recording:

import 'dart:developer';

// Check if timeline recording is active
// Timeline events are only recorded when a listener (DevTools, systrace) is connected

// Get current timestamp for manual correlation
final timestamp = Timeline.now; // Microseconds

Programmatic Service Protocol Interaction

You can control tracing through the VM Service Protocol:

import 'dart:developer';

Future<void> captureTimeline() async {
  // Get the VM service URI
  final serviceInfo = await Service.getInfo();
  final uri = serviceInfo.serverUri;

  if (uri != null) {
    // Connect via WebSocket to the VM service
    // Use package:vm_service to interact programmatically
    print('VM Service available at: $uri');
  }
}

Using package:vm_service for Full Control

import 'package:vm_service/vm_service.dart';
import 'package:vm_service/vm_service_io.dart';
import 'dart:developer' as developer;

Future<void> captureTraceSegment() async {
  final info = await developer.Service.getInfo();
  final uri = info.serverWebSocketUri;
  if (uri == null) return;

  final service = await vmServiceConnectUri(uri.toString());

  // Get the VM reference
  final vm = await service.getVM();
  final isolateId = vm.isolates!.first.id!;

  // Clear previous timeline events
  await service.clearVMTimeline();

  // Set which timeline streams to record
  await service.setVMTimelineFlags(['Dart', 'Embedder', 'GC', 'API']);

  // --- Run the code you want to profile ---
  await performExpensiveOperation();
  // --- End profiled section ---

  // Retrieve the timeline
  final timeline = await service.getVMTimeline();

  // timeline.traceEvents contains the trace event list
  // Each event has name, cat, ph (phase), ts (timestamp), dur, args
  for (final event in timeline.traceEvents ?? []) {
    final json = event.json!;
    print('${json['name']}: ${json['dur']}us');
  }

  await service.dispose();
}

Timeline Streams

The following timeline streams can be enabled via setVMTimelineFlags:

StreamDescription
DartDart-level events (Timeline.startSync, TimelineTask)
EmbedderFlutter engine events (TRACE_EVENT macros)
GCGarbage collection events
APIVM service API calls
CompilerJIT/AOT compilation events
CompilerVerboseDetailed compilation events
DebuggerDebugger-related events
IsolateIsolate lifecycle events
VMVM-level events

Integration Test Timeline Capture

For automated performance testing with trace capture:

import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'dart:developer';

void main() {
  final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  testWidgets('scroll performance', (tester) async {
    await tester.pumpWidget(const MyApp());

    // Report timeline for this block
    await binding.traceAction(() async {
      // Perform the action to measure
      final listFinder = find.byType(ListView);
      await tester.fling(listFinder, const Offset(0, -500), 10000);
      await tester.pumpAndSettle();

      await tester.fling(listFinder, const Offset(0, 500), 10000);
      await tester.pumpAndSettle();
    }, reportKey: 'scroll_timeline');
  });
}

Run with flutter drive (not flutter test, which doesn’t support --profile or tracing flags):

flutter drive \
  --profile \
  --trace-systrace \
  --driver=test_driver/integration_test.dart \
  --target=integration_test/perf_test.dart

The traceAction method automatically starts/stops timeline recording and captures the trace for the specific action being measured.

Summary of Trace Capture Methods

MethodOutput FormatSystem EventsEase of Use
--trace-to-filePerfetto protobufNoSimple
--trace-systrace + PerfettoPerfetto protobufYes (full system)Moderate
--trace-startupBuild directory fileNoSimple
DevTools export.devtools formatNoGUI-based
VM Service APIJSON trace eventsNoProgrammatic
binding.traceAction()Test reportNoTest integration

Key Takeaways

  1. Always profile in profile mode (flutter run --profile). Debug mode data is unreliable; release mode has no tracing.

  2. Use --trace-to-file for quick Perfetto analysis — it produces native Perfetto protobuf files loadable directly at ui.perfetto.dev.

  3. Use --trace-systrace when you need system context — it reveals CPU scheduling, compositor behavior, and cross-process interactions that app-level tracing cannot show.

  4. Custom trace events via dart:developer have near-zero cost in release mode — the calls compile to no-ops, so you can leave instrumentation in production code without penalty.

  5. Be selective with --trace-skia — use allowlists to avoid the extreme verbosity that can itself cause jank. With Impeller, this flag is no longer relevant.

  6. Perfetto UI’s SQL queries are powerful — use them to find all slices exceeding your frame budget, analyze jank patterns, and extract statistical summaries from traces.

  7. Flow events (Flow.begin/step/end) are underused — they create visual arrows in Perfetto UI connecting causally related events across threads, making it much easier to follow a frame’s lifecycle from input to display.


Sources