spm 0.4.0
spm: ^0.4.0 copied to clipboard
Unified static analysis and runtime profiling for Flutter rebuild scopes and their triggers.
Changelog #
0.4.0 #
Added #
Every analyze row now reports the files its metrics were computed from, and whether all of them
could be read.
- Three columns appended after the 14 metrics, so column order for existing consumers is unchanged:
dependencyFiles,unresolvedDependencies, andclosureResolved(1/0). Paths are relative to the analyzed project root and sorted; closure entries outside that root, such as the SDK and the pub cache, are dropped, since neither is editable by a commit in the analyzed repository. dependencyFileslists the transitive closure a row actually depends on, the declaring file included. A scope's metrics are not a function offilePath: helper methods and getters resolve across libraries, and every custom child widget'sbuild()is merged into the totals. Selecting revisions by "touched the declaring file" therefore drops real changes, and drops them hardest in well-composed code, where child trees are deepest.unresolvedDependencieslists closure libraries that could not be read, by path where one is known and by library URI otherwise. A non-empty list means the row is incomplete by an unknown amount rather than absent, so it can be rejected downstream.
Fixed #
- A closure library that resolves while carrying an error-severity diagnostic is now recorded as unresolved. Such a library resolves its types to null, so its widgets classify as value objects and its subtree lands in the wrong metrics. The scanned/skipped counts in the run summary never caught this: they guard only the file being scanned, not the files its metrics are read from. The index is still built, so the numbers this release emits are unchanged; what changes is that the row now says the numbers are untrustworthy.
- The library cache records its verdict alongside the index, and every lookup is attributed to the scope that made it. The cache lives for a whole run, so a second scope reaching a broken library through a cache hit used to be recorded as clean, and a shared dependency appeared only on the first row that touched it.
isolatelifts the bindings a rebuild scope closed over. A builder callback reads parameters and locals of the method it sits in, and a scope on a package-supplied base class such asGetViewreads members it inherits; neither travels with the transplanted source, so the isolated file referenced names nothing declared.- Lifting a promoted parameter to a field costs it its promotion, because Dart does not promote
fields. References whose promoted type was a proper subtype of the declared type are now wrapped,
so
state.walletsbecomes(state as WalletLoaded).walletsand the isolated file still compiles. - A field named
contextis no longer copied onto the generatedState, where it shadowedState.contextand broke the output. - Stripping nullability from a lifted field's type touched the whole type string, rewriting
(Wallet?, Wallet?)to(Wallet, Wallet)andMap<String, int?>toMap<String, int>. Only the trailing?is dropped now. monitorDataFlowandmonitorPerformancereturned their completer's future from inside atry, whichlints_coreflags and which never routed a rejection through thatcatchanyway. The return moved after the block; the guarded statements and the error path are unchanged.
Changed #
isolategenerates aninitStatethat seeds every lifted field from a conventionally named symbol: fieldwalletsis assignedfixtureWallets, and a cross-file project globalfoois assignedfooValue. The names a scope needs are predictable instead of being rediscovered per scope. A scope that brought its owninitStatekeeps it.isolaterunsdart formatover its output directory. The transplant concatenates fragments that keep their original indentation, so two runs used to differ in layout as well as in code. Formatting failures are ignored: an unparseable scope is still written out for inspection.TreeExtractor.extractreturns anExtractionSet<TreeFeaturesSet>record, pairing the feature set with its closure. This type is internal tolib/src/; the public API is unchanged.
0.3.0 #
Fixed #
Six build-tree metric defects, found by checking extracted values against what the analyzed source actually does. Every one of them changes numbers that 0.2.0 emitted, so metrics from the two versions cannot be compared or mixed in one dataset.
- Helpers returning a collection of widgets were skipped.
List<Widget> _buildRows()andList<DropdownMenuItem<T>> _buildItems()are widget factories, but the return type had to be aWidgetsubtype for the reference to count, andListis not one, so the reference went uncounted and the body was never read. SDK collection methods such astoListandcaststay excluded: their type saysList<Widget>but they build nothing. - A
constswap inside a helper body moved no metric. Helper const widgets were added tohelperWidgetCountalongside non-const ones, which erased the distinction. Const widgets in a helper now count towardtreeConstWidgetCount, andhelperWidgetCountcovers non-const helper widgets only, matching how build bodies were already split. - List widgets other than
ListViewandGridViewwere left unclassified.ReorderableListView,PageView, andListWheelScrollVieware now classified by constructor, andAnimatedList,AnimatedGrid, and the remaining sliver lists are treated as lazy by contract. Their lazy builders also mark the widgets they build as per-element cost. - Sliver laziness ignored the delegate.
SliverList(delegate: SliverChildListDelegate([...]))builds every child up front and is now eager (2). A builder delegate stays lazy (1). List.generateread as a single allocation. It is a factory constructor, so thegeneratecase in the method-invocation path never saw it. It now counts as iteration, and the widgets its callback builds count as per-element cost.- Local functions lost their per-element attribution. A local function declared above a loop and invoked inside it was read at its declaration site, outside any iteration scope, so a row built per element looked like a one-off. Bodies are now read at the first call site. A local function that is never referenced is still read once, at the end of the traversal.
Changed #
rootBuildReturnsConstWidgetnow requires every top-level return to be const. A single const return used to set it, so a build that returns a full tree on its common path andconst SizedBox.shrink()from a loading guard was recorded as a const build.
0.2.0 #
Changed #
- Documented SPM's research-dataset origin and planned 1.0.0 static screening direction: classify
UI changes as stable or faster (
0) or slower (1) from build-tree metrics without running or profiling the app.
Removed #
- Breaking: Removed the legacy
package:spm/features/profiler/presentation/compatibility exports. ImportSpmStateandSpmProfilerfrompackage:spm/spm.dart.
0.1.2 #
0.1.1 #
Changed #
- Declared Android as the only supported platform, so the pub.dev package page lists Android alone.
0.1.0 #
analyze now extracts metrics from every rebuild scope, not only State subclasses.
Added #
analyzeemits a row for each rebuild scope:Statesubclasses,ConsumerWidget/HookConsumerWidgetclasses, and the inline builder callbacks ofBlocBuilder,BlocSelector,BlocConsumer,Consumer,Selector,Obx,GetX,GetBuilder, andObserver— the same kindsisolatedetects.--scope-types/-sonanalyze(repeatable) narrows the emitted kinds;-s Statereproduces the previous output.- New
scopeTypecolumn on every JSONL row, and a per-type breakdown in the run summary.
Changed #
- Breaking (JSONL): the
stateClassNamecolumn is nowscopeName.injectreads either spelling, so manifests produced by earlier versions still work; other downstream consumers must be updated. injectskips manifest rows whosescopeTypeis notState, so a full-scopeanalyzeoutput can be passed to it unchanged.- Scope detection is shared between
analyzeandisolateinstead of duplicated: the kind lists live inAppConstantsand the predicates in the analysis feature's scope detector. - SPM now stands for Scope Performance Metrics (was "State Performance Metrics"), matching what
the tool measures. The package, the
spmexecutable, and every public identifier are unchanged.
Notes #
- Scopes nest, and their metrics overlap on purpose: a
Staterow counts the widgets built inside its nested builder callbacks and each callback gets its own row. Aggregations that sum rows per file should filter byscopeType. instanceIdvalues forStatescopes are unchanged, so existing joins with runtime profiler data still hold.
0.0.3 #
- Export
SpmProfilerfrom the publicpackage:spm/spm.dartAPI. - Add compatibility export paths for profiler imports under
package:spm/features/profiler/presentation/. - Restore support for benchmark and integration-test code that imports
SpmStateandSpmProfilerthrough the profiler presentation path.
0.0.2 #
- Add a public API example for
SpmState. - Document the
SpmStateconstructor for subclass usage. - Widen the analyzer dependency constraint.
- Link the published pub.dev package from the README and wiki.
0.0.1 #
- Initial pub.dev release of SPM.
- Adds CLI commands for Flutter rebuild analysis, validation, profiler injection, profile-mode runs, and rebuild-scope isolation.