-
-
Notifications
You must be signed in to change notification settings - Fork 0
Extracted Features
analyze records these build-tree features for each rebuild scope: a State subclass, a
ConsumerWidget or HookConsumerWidget, or a supported state-management builder callback. The
analyzer emits 18 fields. Output Formats lists their JSON keys and wire types.
All build metrics below are computed by one traversal (TreeExtractor); identity fields are
assigned during scope discovery. The metric scopes are easiest to understand from these
traversal rules:
-
Root. Analysis starts at the scope's body: the
build()body of aStatesubclass or consumer widget, or the callback body of a builder-pattern widget. Scopes nest, and the metrics overlap deliberately — aBlocBuildercallback inside aState.build()is counted in theStaterow (a parent rebuild re-runs it) and gets a row of its own. -
Custom child widgets. Every non-const instantiation of a custom widget class (any
class outside the Dart SDK and
package:flutter) is followed: that class's ownbuild()body is resolved and analyzed too, breadth-first, transitively through grandchildren. A customStatefulWidgetchild has nobuild()of its own, so its state class'sbuild()is what reruns on rebuild, so the traversal resolves the State class (from thecreateState()body, falling back to the library'sextends State<Widget>declaration) and analyzes that instead. Each class is analyzed once per record, even if instantiated many times. -
Helper methods. A reference to a widget-returning callable inside the widget tree (nested in
widget arguments or in a
returnexpression) is treated as a helper. Three reference forms count: a direct invocation (_buildRow()), a method tear-off (items.map(_buildRow),itemBuilder: _buildRow), and an explicit widget-returning getter (_header). A helper may return oneWidgetor a collection of them.List<Widget> _buildRows()andList<DropdownMenuItem<T>> _buildItems()are widget factories: the caller splices the result straight intochildren:oritems:, so those bodies run on every rebuild and are analyzed like any other helper. SDK collection methods are excluded even when their type saysList<Widget>: initems.map(_buildRow).toList(),toListbuilds nothing, so only_buildRowcounts. Resolution is element-based, so instance methods, static methods of other classes, and top-level widget functions all resolve; plain widget fields (synthetic getters) are data, not helpers, and are excluded. Helper bodies feed thehelper*widget features, and their execution cost signals (iterations, per-element widgets, allocations, list-rendering strategy, layout-dependent builders, and decision points) merge into the corresponding aggregate output features, because helpers run as part ofbuild(). Helper→helper chains are followed, and helpers of custom child widgets are analyzed as well (deduped per class + member). -
Local functions. A function declared inside a body (
Widget row(int i) => …;) belongs to that body rather than being a helper of its own, so it is never resolved or deduped across classes. Its widgets are recorded at the call site. A row builder declared just above the loop that uses it costs one row per element, and reading the body where it is written would put those widgets outside every per-element scope. The body is read once, at the first call site; a local function that is declared and never referenced is read at the end of the traversal so it still contributes its widgets. -
Const boundary. A
constwidget instantiation is a canonicalized subtree that Flutter's reconciliation skips, so it carries no per-rebuild cost: it is counted once as a const unit, its children are not entered, and a const custom child'sbuild()is not recursed into. This holds wherever the widget is written, whether in a build body, a child build, or a helper body, so a const widget in a helper counts as a const unit rather than a helper widget. -
Non-widget allocations. Instantiations of non-
Widgettypes (EdgeInsets,TextStyle,BorderRadius, application models, …) are not counted as widgets and add no widget nesting depth. Non-const instances incrementvalueObjectAllocCount; const instances do not. Their arguments are still searched for widgets nested inside them.List.generateandIterable.generateare the exception worth naming. They are factory constructors rather than method calls, but the callback still runs once per element, so they open a per-element scope like any loop does. - Broken files. A file with compile errors is skipped entirely (and reported in the summary): its unresolved types would silently classify every widget as a value object and emit a near-zero garbage row.
"Across the build tree" below always means: the root build() plus every custom child widget's
build() reached by rule 2.
| Feature | Type | Description |
|---|---|---|
instanceId |
String |
Stable ID for the record: a 31-bit hash of "<root-relative file path>:<class name>" for a State scope, or "<root-relative file path>:<scopeType>:<scopeName>#<n>" for any other rebuild scope (forward slashes on every platform), rendered as hex. Relative to the analyzed project root, so the ID is stable across machines and checkout directories. Used to join static features with runtime profiler events emitted under the same ID. (Hashed from the absolute path before 2026-07-20; older JSONL files carry machine-specific IDs.)
|
filePath |
String |
Source file containing the rebuild scope, relative to the analyzed project root (the same path the ID hashes). |
scopeName |
String |
Class name of the scope (e.g. _MyWidgetState), 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). |
| Feature | Type | Description |
|---|---|---|
treeNonConstWidgetCount |
int |
Total non-const widget instantiations across the build tree. Excludes const widgets (see treeConstWidgetCount), value objects, and widgets built inside helper bodies (those go to helperWidgetCount). |
treeMaxWidgetNestingDepth |
int |
Deepest widget nesting level reached across the build tree. Depth composes across classes: a child widget instantiated at depth d whose own build reaches internal depth k yields an absolute depth of d + k (for a StatefulWidget child, k is measured in its State class's build). A const widget occupies its own level but its subtree adds nothing. Custom widgets instantiated inside helper bodies restart from depth 0 (helper nesting is tracked separately in helperMaxWidgetNestingDepth). |
treeConstWidgetCount |
int |
Number of const widget instantiations across the build tree, as a raw count, not a ratio. Because of the const boundary rule, an entire const subtree counts as 1. Helper bodies are included. A const widget written in a helper is still a const unit, which is what makes a const edit inside a helper visible in the metrics at all. (Emitted as constConstructorRatio before 2026-07-18; older JSONL files use that key. Helper-body const widgets were folded into helperWidgetCount before 2026-08-15.)
|
rootBuildReturnsConstWidget |
bool (0/1) |
Whether every top-level return of the root build() is a const instantiation. Applies to return const …; statements and expression bodies (Widget build(_) => const …;) alike. Every exit has to qualify because of the guard pattern. A build that returns const SizedBox.shrink() while loading and a full tree otherwise pays the full cost on the path that actually runs, so it does not count as a const build. Returns inside closures never set it, whether the closure sits in widget arguments or in the build preamble. (Any single const return was enough before 2026-08-15.)
|
Both features aggregate across the whole tree (root build, custom child builds, and helper bodies); a match anywhere counts.
| Feature | Type | Description |
|---|---|---|
treeListRenderingStrategy |
int (0/1/2) |
The most expensive list-shaped rendering form reachable from build(), ranked by UI-thread build cost and aggregated as the max. 0 = none: no list-shaped multi-child rendering, including a fixed-arity children: literal (an if-element is an O(1) branch, not a scaling child count). 1 = lazy / viewport-bounded: a scroll list built through .builder/.separated/.custom/.useDelegate, or a list widget that is lazy by contract (AnimatedList, AnimatedGrid, and the sliver lists). 2 = eager / O(N): a scroll list fed a concrete children: (every child builds on every rebuild, no viewport culling), a sliver list fed a SliverChildListDelegate (that delegate materializes the whole child list, so the sliver is only nominally lazy), or a Column/Row/Wrap/Flex whose children: is runtime-length. The scroll lists are ListView, GridView, ReorderableListView, PageView, and ListWheelScrollView, and for all of them laziness follows the constructor rather than the class. Runtime-length is decided by the shape of the expression, not an op whitelist: a spread or for-element inside the literal, or any non-literal expression such as a bare list variable, items.map(…).toList(), List.generate(…), or a helper call. Because the aggregate is a max, one eager list anywhere in the tree hides the strategy of every other list in it. (Replaces the boolean buildUsesListViewBuilder on 2026-07-20; the boolean missed every eager form, including SingleChildScrollView over a mapped Column. Only ListView/GridView/SliverList/SliverGrid were recognized before 2026-08-15, and the sliver delegate was ignored.)
|
usesLayoutDependentBuilder |
bool (0/1) |
The tree instantiates LayoutBuilder, CustomMultiChildLayout, or Flow, widgets whose build depends on incoming layout constraints. |
| Feature | Type | Description |
|---|---|---|
treeCyclomaticComplexity |
int |
Cyclomatic complexity summed over every build body in the tree (root + custom children), plus the decision points of every analyzed helper body (each helper contributes its complexity − 1, so the shared +1 base is not re-added per helper). Each build body starts at 1 and gains +1 per if (statement or collection-if element), for (statement or collection-for element, for-in included), while, do, switch case (statement cases and switch-expression cases; statement default excluded), catch, &&, ||, ??, and ternary ?:. Because it is a sum, a tree of n build methods has a floor of n. |
treeIterationCount |
int |
Number of iteration constructs across all build bodies and helper bodies: for / while / do statements, collection-for elements inside literals, calls to linear collection ops (forEach, map, where, any, every, reduce, fold, expand, generate, sort, firstWhere, lastWhere, singleWhere), and the List.generate / Iterable.generate constructors. Each op in a chain like .where(…).map(…) counts individually. (List.generate is a constructor, not a method call, and went uncounted before 2026-08-15.)
|
treeMaxIterationNestingDepth |
int |
Maximum lexical nesting depth of those iteration constructs within any single analyzed body, covering build bodies and helper bodies. Chained collection ops count as sequential, not nested (only the callback argument runs per element). Nesting is not composed across classes: a child widget created inside a parent's loop does not nest the child's own loops. |
helperReferenceCount |
int |
Number of reference sites of widget-returning helpers inside build bodies (root + custom children): direct invocations, method tear-offs, and explicit getter reads (rule 3 above), whether the helper returns one widget or a collection of them. Reference sites, not distinct methods: calling _buildRow() three times counts 3. Not counted: references made from inside helper bodies, linear collection ops that happen to yield a Widget (e.g. firstWhere on a List<Widget>, which is an iteration, see treeIterationCount), SDK collection methods that pass widgets through without building any (toList, cast, followedBy, …), any member named build (e.g. super.build(context) from the keep-alive mixin), plain widget fields (synthetic getters), and invocations of function-typed variables or closures. That last case includes a function held in a field, so a callback assigned in a constructor initializer and invoked as widget.leading(item) stays out of reach. |
iterationWidgetCount |
int |
Non-const widget instantiations inside a per-element scope, summed across build bodies and helper bodies: a loop body, a linear-collection-op callback (.map((e) => Card(...))), a List.generate callback, or the argument list of a lazy list constructor (itemBuilder runs per visible element). The per-element cost multiplier of a rebuild: distinguishes ten one-shot widgets from one widget built ten times. Widgets built by a local function count here when that function is called from inside the scope, which is the usual shape when a row builder is declared above the loop that uses it. (Added 2026-07-20; motivated by loop-cost findings in performance/energy prediction literature.)
|
valueObjectAllocCount |
int |
Non-const, non-widget instance creations (EdgeInsets, TextStyle, BoxDecoration, …) across build and helper bodies. This is the allocation/GC pressure paid on every rebuild. const value objects are canonicalized and excluded, so const-ing a padding lowers this count. (Added 2026-07-20; motivated by allocation-density findings in energy prediction literature.)
|
Cover the bodies of widget-returning helpers reached from a build body (rule 3 above): helpers of
the root State class and of every analyzed custom child widget (instance methods, getters,
static methods, and top-level widget functions), returning a widget or a collection of widgets,
followed transitively through helper→helper chains, each body analyzed once.
| Feature | Type | Description |
|---|---|---|
helperWidgetCount |
int |
Non-const widget instantiations inside all analyzed helper bodies. This is the same split treeNonConstWidgetCount applies to build bodies: const units in a helper go to treeConstWidgetCount, so the three counts never overlap. Custom widgets instantiated in a helper still contribute their own build() tree to the tree-scoped metrics. (Const widgets were counted here too before 2026-08-15, which left const-ness inside a helper invisible in every feature.)
|
helperMaxWidgetNestingDepth |
int |
Maximum widget nesting depth reached inside any single helper body, measured from 0 at the helper body's root. Not composed with the depth of the call site in build(). |
boolfeatures are serialized as1/0in the JSONL. See Output Formats for the wire format.
Commands
Reference
Internals
Contributing