Skip to content

Output Formats

albertoodev edited this page Aug 24, 2026 · 8 revisions

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.

Static analysis JSONL (analyze)

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 reference

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/1 fields are Dart bools serialized as 1/0 by toJson. See Extracted Features for the precise definition of each metric.

Key migration (2026-07-20 rename)

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).

Package source in the closure

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. packageVersions is 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 whose pubspec.lock moved 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.

Closure fields

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.

Rebuild-scope rows

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.

Runtime profiler JSONL (run / instrumented app)

Two event types are emitted, distinguished by the event field. Which one you get depends on the Flutter build mode (see inject): --profileperformance_metric, --debugdataflow-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.

Clone this wiki locally