-
-
Notifications
You must be signed in to change notification settings - Fork 0
Output Formats
analyze and the instrumented app write JSON Lines: one self-contained JSON object per line. The
field names and types below come from the toJson methods in
lib/src/features/*/data/models/. Extracted Features explains how each static
metric is calculated.
One JSON object is written for each 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
21 fields (AnalysisResultModel.toJson): 18 identity and metric fields, then three that describe
which files the metrics were read from.
{
"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,
"closureResolved": 1,
"dependencyFiles": [
"lib/screens/shopping_cart_screen.dart",
"lib/widgets/cart_line_item.dart"
],
"unresolvedDependencies": []
}| 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 and class, so a consumer that isolates several scopes out of one class needs a key of its own. |
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/.useDelegate, or a list that is lazy by contract), 2 eager O(N) (concrete children: on a scroll list, a SliverChildListDelegate, or runtime-length Column/Row/Wrap/Flex children). Scroll lists are ListView, GridView, ReorderableListView, PageView, ListWheelScrollView. (Replaced the buildUsesListViewBuilder boolean on 2026-07-20; widened beyond ListView/GridView/slivers on 2026-08-15.)
|
rootBuildReturnsConstWidget |
0/1 | Whether every top-level return of the root State.build is a const widget. (Any one const return set it before 2026-08-15.)
|
treeConstWidgetCount |
int | Const widget boundaries across reachable build bodies and helper bodies alike. (Named constConstructorRatio in JSONL produced before 2026-07-18; helper-body const widgets went to helperWidgetCount before 2026-08-15.)
|
helperReferenceCount |
int | Widget-returning helper reference sites in reachable build bodies: invocations, tear-offs, and explicit getters. Helpers returning List<Widget> count; SDK pass-throughs such as toList do not. |
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, linear collection operations, and List.generate 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, List.generate, and lazy builders. Includes widgets built by a local function called from inside such a scope. |
valueObjectAllocCount |
int | All non-const, non-widget constructor allocations across build and helper bodies. |
helperWidgetCount |
int | Non-const widget instantiations in reachable helper bodies; const ones are counted by treeConstWidgetCount. (Included const widgets before 2026-08-15.)
|
helperMaxWidgetNestingDepth |
int | Maximum widget nesting within any individual reachable helper body. |
closureResolved |
0/1 | Whether every file in dependencyFiles was read successfully. 0 when unresolvedDependencies is non-empty. (Added in 0.4.0.)
|
dependencyFiles |
string[] | Sorted, root-relative files whose contents produced the metrics, the declaring file included. (Added in 0.4.0.) |
unresolvedDependencies |
string[] | Sorted closure entries that could not be read: unresolvable, or resolved while carrying an error-severity diagnostic. Paths where one is known, library URIs otherwise. (Added in 0.4.0.) |
packageVersions |
object | Resolved version of every package the closure entered, keyed by package name, read from the name-version segment of its pub-cache path. A package with no version in its path contributes nothing, which is what a path or git dependency looks like. (Added in 0.7.0.)
|
walkedWidgetClasses |
string[] | Sorted libraryUri#Name of every non-SDK class whose build body was walked. Diff it against carriedUiDeclarations on the matching isolate row: for every declaration walked here, the transplant has to carry the source, or the two rows describe different trees. (Added in 0.7.0.)
|
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).
From 0.7.0 the closure reaches into packages. A widget from a state-management or design-system package is followed the way a repo-local one is, so its subtree is counted in place rather than dropped. Two consequences belong on the row rather than in a reader's head:
- A package file does not appear in
dependencyFiles. Its absolute path names a pub cache on one machine, and a row carrying it could not be compared with one produced anywhere else.packageVersionsis what the row keeps instead. - A package version is now an input to the metrics. Analyse the same project against one resolved
package_config.json(analyze --package-config), or two runs whosepubspec.lockmoved between them differ with no source edit to explain it.
dart: and package:flutter/ are still never walked. See
analyze for why that boundary is a correctness
decision.
The 14 metrics on a row are not a function of filePath. TreeExtractor follows helper methods and
getters across libraries and merges every custom child widget's build() into the totals, so an
edit two files away moves the numbers while the scope's own file stays untouched. dependencyFiles
names those files, which is what a query over a project's history needs: selecting commits by
"touched the declaring file" misses real changes, and misses them hardest where child trees are
deepest.
unresolvedDependencies covers the other direction. A closure file that will not resolve makes a
row wrong rather than absent: an unreadable child contributes nothing and its whole subtree
disappears from the totals, while a child that resolves with error-severity diagnostics has its
types come back null and its widgets counted as value objects. The scanned/skipped counts in the
analyze summary do not catch either case, because that gate guards only the file being scanned.
Treat a row with a non-empty unresolvedDependencies as incomplete by an unknown amount, and do not
compare it against another run: the difference measures resolution state, not a code change.
Both lists are filtered to the analyzed project root. The closure reaches into the SDK and the pub cache, and neither is part of the source under analysis.
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