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:
- Engine-level macros (
TRACE_EVENT0("flutter", "EventName")) instrument key operations throughout the Flutter engine — frame scheduling, rasterization, platform channel calls, pointer dispatch, etc. - Dart VM timeline (
dart:developer) provides application-level instrumentation viaTimelineandTimelineTaskclasses. - 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-systraceis 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.
- On Android: events go to
What Gets Traced
| Layer | What’s Traced | Example Events |
|---|---|---|
| Dart VM | GC pauses, isolate lifecycle, compilation | GC, CompileFunction |
| Framework | Widget builds, layouts, paints, semantics | build, layout, paint, Animate |
| Engine | Frame scheduling, rasterization, platform channel dispatch | BeginFrame, SubmitFrame, PipelineConsume, PointerEvent |
| GPU/Raster | Skia/Impeller draw calls, shader compilation, texture uploads | DrawDisplayList, ShaderCompilation |
| Platform | System 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-skiaflag 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
| Feature | Debug | Profile | Release |
|---|---|---|---|
| Timeline tracing | Limited | Full | Disabled |
| DevTools connection | Yes | Yes | No |
| Performance overlay | No | Yes | No |
| Representative perf data | No | Yes | Yes |
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/finishSynccalls 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
- Run the app in profile mode:
flutter run --profile - Open DevTools (the URL is printed in the console)
- Go to the Performance tab
- Interact with your app to generate frames
- Click the export button (upper-right corner) to download a
.devtoolssnapshot
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
- Navigate to ui.perfetto.dev
- Drag and drop the trace file, or click “Open trace file” in the sidebar
- Supported formats:
- Perfetto protobuf (
.perfetto-trace,.pb) — native format from--trace-to-file - Chrome JSON trace format (legacy)
- Android systrace format
- Perfetto protobuf (
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 forbuild,layout,paint,Animateslices. Long slices here mean your Dart code is too expensive.1.raster(Raster Thread): GPU rendering. Look forDrawDisplayList,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 presentationExpected 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
Navigation Controls
| Action | Shortcut |
|---|---|
| Zoom in | W |
| Zoom out | S |
| Pan left | A |
| Pan right | D |
| Select event | Click |
| Pan (alternative) | Shift + Drag |
| Zoom (alternative) | Ctrl + MouseWheel |
| Next event | . |
| Previous event | , |
| Fit selection to view | F |
| Pin track | Pin icon in track header |
| Find track by name | Ctrl + P |
| Command palette | Ctrl + Shift + P |
Analyzing Frame Performance
- Find janky frames: Look for gaps or unusually long slices in the UI or Raster thread tracks.
- Measure duration: Click on a slice to see its duration in the “Current Selection” tab.
- Area selection: Click and drag across a time range to see aggregated statistics. Press
Rto convert a single selection into an area selection. - 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/finishSynccalls 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.nowuses monotonic clock) - Buffer insertion (lock-free ring buffer or growing buffer)
-
With
--trace-systraceon 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-allowlistto 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
- Use
--trace-allowlistto filter events to specific prefixes you care about, reducing total event volume. - Use
--trace-skia-allowlistinstead of--trace-skiato limit Skia events to specific categories (e.g.,skia.gpu,skia.shaders). - Use
--endless-trace-bufferfor long recording sessions to avoid ring buffer overwrites, but be aware of memory growth. - Minimize custom trace events in hot loops — instrument at a coarser granularity for functions called per-frame.
- Arguments maps are the most expensive part — avoid passing large argument maps in high-frequency events.
Mode Comparison
| Mode | Tracing Available | Overhead | Use Case |
|---|---|---|---|
| Debug | Limited (unreliable) | Very high (JIT, assertions) | Development only |
| Profile | Full | Low-moderate (1-10us/event) | Performance analysis |
| Release | None (compiled out) | Zero | Production |
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:
| Stream | Description |
|---|---|
Dart | Dart-level events (Timeline.startSync, TimelineTask) |
Embedder | Flutter engine events (TRACE_EVENT macros) |
GC | Garbage collection events |
API | VM service API calls |
Compiler | JIT/AOT compilation events |
CompilerVerbose | Detailed compilation events |
Debugger | Debugger-related events |
Isolate | Isolate lifecycle events |
VM | VM-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
| Method | Output Format | System Events | Ease of Use |
|---|---|---|---|
--trace-to-file | Perfetto protobuf | No | Simple |
--trace-systrace + Perfetto | Perfetto protobuf | Yes (full system) | Moderate |
--trace-startup | Build directory file | No | Simple |
| DevTools export | .devtools format | No | GUI-based |
| VM Service API | JSON trace events | No | Programmatic |
binding.traceAction() | Test report | No | Test integration |
Key Takeaways
-
Always profile in profile mode (
flutter run --profile). Debug mode data is unreliable; release mode has no tracing. -
Use
--trace-to-filefor quick Perfetto analysis — it produces native Perfetto protobuf files loadable directly at ui.perfetto.dev. -
Use
--trace-systracewhen you need system context — it reveals CPU scheduling, compositor behavior, and cross-process interactions that app-level tracing cannot show. -
Custom trace events via
dart:developerhave near-zero cost in release mode — the calls compile to no-ops, so you can leave instrumentation in production code without penalty. -
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. -
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.
-
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.