-
-
Notifications
You must be signed in to change notification settings - Fork 0
Output Formats
Every SPM command emits JSON Lines (JSONL): one self-contained JSON object per line. This page
documents the schemas produced by analyze (static) and the instrumented app
under run (runtime). Field names and types here are taken directly from the
toJson methods in lib/src/features/*/data/models/. For the meaning of each analysis metric, see
Extracted Features.
One JSON object per line, one line per rebuild scope — a State subclass, a
ConsumerWidget/HookConsumerWidget, or the builder callback of a state-management widget
(BlocBuilder, Consumer, Obx, …). The analyze output contains exactly these
18 fields (AnalysisResultModel.toJson):
{
"instanceId": "42",
"filePath": "lib/screens/shopping_cart_screen.dart",
"scopeName": "_ShoppingCartScreenState",
"scopeType": "State",
"treeNonConstWidgetCount": 24,
"treeMaxWidgetNestingDepth": 7,
"treeListRenderingStrategy": 2,
"rootBuildReturnsConstWidget": 0,
"treeConstWidgetCount": 2,
"helperReferenceCount": 2,
"usesLayoutDependentBuilder": 1,
"treeCyclomaticComplexity": 5,
"treeIterationCount": 1,
"treeMaxIterationNestingDepth": 1,
"iterationWidgetCount": 3,
"valueObjectAllocCount": 4,
"helperWidgetCount": 3,
"helperMaxWidgetNestingDepth": 2
}| Field | Type | Description |
|---|---|---|
instanceId |
string | ID for this scope: hash of the root-relative path plus the scope name (and, for non-State scopes, its type and occurrence index). Used to join with runtime profiler data. It is not unique across transplanted targets that share one file/class, so the benchmark pipeline keys by sample_id instead. |
filePath |
string | Source path relative to the analyzed project root. |
scopeName |
string | Class name of the scope, or <Widget>_builder for a builder callback (e.g. BlocBuilder_builder). (Named stateClassName before rebuild-scope support.)
|
scopeType |
string | Kind of rebuild scope: State, ConsumerWidget, or the builder widget name (BlocBuilder, BlocSelector, BlocConsumer, Consumer, Selector, Obx, GetX, GetBuilder, Observer). |
treeNonConstWidgetCount |
int | Non-const widget instantiations across reachable build bodies; excludes const boundaries and helper-body widgets. |
treeMaxWidgetNestingDepth |
int | Maximum widget depth composed across reachable custom-widget build bodies. |
treeListRenderingStrategy |
0/1/2 | Worst list-rendering form across builds and helpers: 0 none, 1 lazy (.builder/.separated/.custom, slivers), 2 eager O(N) (concrete children: on a scroll list, or runtime-length Column/Row/Wrap/Flex children). (Replaced the buildUsesListViewBuilder boolean on 2026-07-20.)
|
rootBuildReturnsConstWidget |
0/1 | Whether the root State.build directly returns a const widget. |
treeConstWidgetCount |
int | Const widget boundaries across reachable build bodies; helper-body const widgets belong to helperWidgetCount. (Named constConstructorRatio in JSONL produced before 2026-07-18.)
|
helperReferenceCount |
int | Widget-returning helper reference sites in reachable build bodies: invocations, tear-offs, and explicit getters. |
usesLayoutDependentBuilder |
0/1 | Whether builds or helpers use LayoutBuilder, CustomMultiChildLayout, or Flow. |
treeCyclomaticComplexity |
int | Complexity summed across reachable build bodies, plus helper-body decision points without extra helper base points. |
treeIterationCount |
int | Loops, collection-for elements, and linear collection operations across build and helper bodies. |
treeMaxIterationNestingDepth |
int | Maximum lexical iteration nesting within any analyzed build or helper body. |
iterationWidgetCount |
int | Non-const widgets built in per-element scopes across builds and helpers: loops, collection-op callbacks, and lazy builders. |
valueObjectAllocCount |
int | All non-const, non-widget constructor allocations across build and helper bodies. |
helperWidgetCount |
int | Widget instantiations in reachable helper bodies, including const widgets. |
helperMaxWidgetNestingDepth |
int | Maximum widget nesting within any individual reachable helper body. |
0/1fields are Dartbools serialized as1/0bytoJson. See Extracted Features for the precise definition of each metric.
Feature keys were renamed so every name states its scope: tree* = aggregated over the whole
static call tree, root* = root build() only, helper* = helper bodies only. Older JSONL
files can be normalized with this map:
| Old key (pre-2026-07-20) | Current key |
|---|---|
buildWidgetInstanceCount |
treeNonConstWidgetCount |
buildMaxWidgetNestingDepth |
treeMaxWidgetNestingDepth |
buildUsesListViewBuilder (bool, pre-07-20) / buildListRenderingStrategy
|
treeListRenderingStrategy |
buildReturnsConstWidget |
rootBuildReturnsConstWidget |
constConstructorRatio (pre-07-18) / constWidgetCount
|
treeConstWidgetCount |
buildHelperMethodCount |
helperReferenceCount |
buildCyclomaticComplexity |
treeCyclomaticComplexity |
buildIterationCount |
treeIterationCount |
buildMaxIterationNestingDepth |
treeMaxIterationNestingDepth |
helperMethodWidgetCount |
helperWidgetCount |
helperMethodMaxNestingDepth |
helperMaxWidgetNestingDepth |
usesLayoutDependentBuilder, iterationWidgetCount, valueObjectAllocCount, and the identity
keys (instanceId, filePath, stateClassName — since renamed to scopeName) are unchanged. The legacy boolean
buildUsesListViewBuilder maps onto the none/lazy levels of the ordinal only; it cannot
express eager (2).
analyze emits one row per rebuild scope, so a file can contribute several overlapping rows: a
State row counts the widgets built inside a nested BlocBuilder callback and the callback gets
its own row. That is deliberate — a parent rebuild re-runs the callback, while the callback row
measures the path its package can rebuild on its own.
Use --scope-types to narrow the output (--scope-types=State reproduces the pre-rebuild-scope
rows). inject only instruments State subclasses and skips rows of any other scopeType, so a
full-scope JSONL can be passed to it unchanged.
Two event types are emitted, distinguished by the event field. Which one you get depends on the
Flutter build mode (see inject): --profile → performance_metric,
--debug → dataflow-metric. Every event carries a timestamp (ISO-8601) and the instanceId
that joins back to the static-analysis record.
Performance event, emitted under --profile (PerformanceMetricsModel.toJson):
{
"timestamp": "2026-02-20T10:15:30.123Z",
"event": "performance_metric",
"instanceId": "42",
"buildSpan": 4123
}| Field | Type | Description |
|---|---|---|
timestamp |
string | ISO-8601 timestamp of the event. |
event |
string | Always "performance_metric". |
instanceId |
string | The instrumented State instance. |
buildSpan |
int | Build duration of the matched rebuild frame, in microseconds (0 on the 5-second timeout fallback). |
Dataflow event, emitted under --debug (DataflowMetricsModel.toJson):
{
"timestamp": "2026-02-20T10:15:30.456Z",
"event": "dataflow-metric",
"instanceId": "42",
"taintedRebuildCount": 5,
"totalWidgetCount": 42,
"maxNestingDepth": 7,
"taintedRatio": 0.119,
"opacityRebuildCount": 0,
"shaderMaskRebuildCount": 0,
"clipRRectRebuildCount": 1,
"clipOvalRebuildCount": 0,
"clipPathRebuildCount": 0,
"backdropFilterRebuildCount": 0
}| Field | Type | Description |
|---|---|---|
timestamp |
string | ISO-8601 timestamp of the event. |
event |
string | Always "dataflow-metric". |
instanceId |
string | The instrumented State instance. |
taintedRebuildCount |
int | Widgets that actually rebuilt as a result of the setState. |
totalWidgetCount |
int | Total widgets in the analyzed subtree. |
maxNestingDepth |
int | Maximum nesting depth of the analyzed subtree. |
taintedRatio |
double |
taintedRebuildCount / totalWidgetCount. |
opacityRebuildCount |
int | Rebuilt Opacity widgets. |
shaderMaskRebuildCount |
int | Rebuilt ShaderMask widgets. |
clipRRectRebuildCount |
int | Rebuilt ClipRRect widgets. |
clipOvalRebuildCount |
int | Rebuilt ClipOval widgets. |
clipPathRebuildCount |
int | Rebuilt ClipPath widgets. |
backdropFilterRebuildCount |
int | Rebuilt BackdropFilter widgets. |
Commands
Reference
Internals
Contributing