frameguard 0.6.0
frameguard: ^0.6.0 copied to clipboard
FrameGuard — performance regressions, testable. Capture Flutter frame timings, classify jank, enforce budgets, compare baselines, and fail CI on measurable UI regressions. Local by default. No telemetry.
FrameGuard
Performance regressions, testable.
Automated Flutter UI performance regression detection — budgets, baselines, and evidence-backed reports you can enforce in CI.
Docs · pub.dev · CLI · Changelog · Sponsor
Status #
| Version | 0.6.0 |
| Repository | github.com/theworker02/frameguard |
| Package | pub.dev/packages/frameguard |
| Docs site | theworker02.github.io/frameguard |
| License | MIT |
| Telemetry | None — local by default |
Why FrameGuard? #
Flutter already has excellent profilers. DevTools answers:
What happened during this profiling session?
FrameGuard answers:
Did performance regress, where, why is that likely, and should this build fail?
| DevTools | FrameGuard | |
|---|---|---|
| Goal | Interactive exploration | Automation & regression gates |
| Output | Timelines you interpret | Budgets, baselines, matchers, CI exit codes |
| Audience | Humans in a profiling session | Humans and pipelines |
| Telemetry | N/A | None — local by default |
FrameGuard is not a thin DevTools wrapper. It turns documented Flutter frame timings into reproducible explanations.
Features #
- Session capture via
SchedulerBinding.addTimingsCallback/FrameTiming - Refresh-rate-aware budgets (60 / 90 / 120 / 144 Hz — no hardcoded 16.67 ms dogma)
- Jank severity (healthy / minor / major / severe)
- Percentiles — p50 / p90 / p95 / p99, histograms, streaks
- Build vs raster classification (derived, never claimed as certainty)
- Regions & rebuild counts (
FrameGuardRegion) - Traces, markers, sync tasks
- Explainability + recommendations tied to evidence (
FG001–FG010) - JSON / text / HTML reports (versioned schema)
- CSV · JUnit · SARIF · Markdown exporters for CI / PR comments
- Baselines & golden files (never silently overwritten)
- Multi-run statistics (median, MAD, CI; outliers flagged, not deleted)
- Test matchers +
FrameGuardTest.measure - CLI for compare / check / export / summary / doctor / baseline / history / suggest-budget
- Local history (JSONL) for gradual drift without a backend
- Reusable GitHub Action (
.github/actions/frameguard-check) - Optional overlay & runtime budget watcher
- Capability model — Unavailable beats fake zeros
Install #
# pubspec.yaml
dependencies:
frameguard: ^0.6.0
For tests-only usage you can keep it under dev_dependencies. Test helpers live in a separate library:
import 'package:frameguard/frameguard.dart';
import 'package:frameguard/frameguard_test.dart';
flutter pub get
Quick start #
import 'package:flutter/material.dart';
import 'package:frameguard/frameguard.dart';
void main() {
FrameGuard.initialize(
config: FrameGuardConfig(
samplingMode: SamplingMode.balanced,
defaultBudget: FrameBudget.forRefreshRate(60, maxJankRate: 0.02),
),
);
runApp(
const FrameGuardScope(
child: FrameGuardOverlay(
compact: true,
child: MyApp(),
),
),
);
}
Capture a session #
final session = FrameGuard.startSession(name: 'home_scroll');
// …interact with the app…
final report = await session.stop();
debugPrint(report.summary());
await report.writeJson(File('reports/home_scroll.json'));
await report.writeHtml(File('reports/home_scroll.html'));
Assert in tests #
testWidgets('product list stays within budget', (tester) async {
await tester.pumpWidget(const App());
final report = await FrameGuardTest.measure(
tester,
name: 'product_list',
action: () async {
await tester.fling(find.byType(ListView), const Offset(0, -1000), 1000);
await tester.pumpAndSettle();
},
);
expect(
report,
meetsFrameBudget(
FrameBudget.forRefreshRate(
60,
maxJankFrames: 2,
maxJankRate: 0.01,
),
),
);
});
Budgets & baselines #
final budget = FrameBudget(
maxJankFrames: 2,
maxJankRate: 0.01,
maxP95FrameTime: const Duration(milliseconds: 16),
maxP99FrameTime: const Duration(milliseconds: 24),
);
final evaluation = report.evaluate(budget);
if (!evaluation.passed) {
fail(evaluation.summary());
}
Optional project YAML (Dart config remains primary):
cp frameguard.yaml.example frameguard.yaml
dart run frameguard config validate
Update baselines deliberately:
dart run frameguard baseline update reports/catalog.json --out baselines/catalog.json
dart run frameguard check reports/current.json --baseline baselines/catalog.json
Exit codes: 0 pass · 1 regression · 2 invalid config/report.
CLI #
dart run frameguard help
dart run frameguard init
dart run frameguard doctor
dart run frameguard list
dart run frameguard explain report.json
dart run frameguard frames report.json --janky-only
dart run frameguard top report.json
dart run frameguard check reports/ --baseline baselines/ --profile mid_range --require-profile
dart run frameguard suggest-budget reports/home.json --format dart
dart run frameguard history append reports/home.json
dart run frameguard summary report.json --format markdown
dart run frameguard batch reports/ --format junit --out-dir reports/exports
dart run frameguard watch reports/ --once
dart run frameguard completions --shell bash
Full command map: doc/cli.md · docs/cli.html.
CI #
Gate performance reports in GitHub Actions:
- uses: theworker02/frameguard/.github/actions/frameguard-check@main
with:
report: reports/
baseline: baselines/
profile: mid_range
require-profile: 'true'
Or call the CLI directly:
dart run frameguard check reports/ --baseline baselines/ --require-profile --profile mid_range
dart run frameguard summary reports/catalog.json --format markdown >> "$GITHUB_STEP_SUMMARY"
More: doc/ci.md.
Regions, traces, markers #
FrameGuardRegion(
name: 'product_grid',
child: ProductGrid(),
);
await FrameGuard.trace('open_product', () async {
await Navigator.of(context).push(...);
});
FrameGuard.mark('products_loaded', metadata: {'count': 42});
await FrameGuard.measureTask('parse_catalog', () => parseCatalog(data));
Scenarios & statistics #
final result = await FrameScenarioRunner(
scenario: const FrameScenario(name: 'catalog_scroll', warmupFrames: 30),
runs: 5,
budget: FrameBudget.forRefreshRate(120),
).runAggregated((i) async {
await scrollCatalog();
});
debugPrint(result.summary());
Details: doc/scenarios.md.
Platform support #
| Platform | Frame timings | Image cache | Overlay | Notes |
|---|---|---|---|---|
| Android / iOS | Yes | Yes | Yes | Prefer profile for gates |
| Desktop | Yes | Yes | Yes | |
| Web | Best-effort | Best-effort | Yes | Absolute ms budgets vary by browser |
Native extras (JankStats, signposts, GPU counters) are Unavailable until real adapters ship — we will not invent zeros. See FrameGuard.capabilities and platform adapters.
Privacy #
- No analytics
- No telemetry
- No accounts
- No required dashboard
- Reports stay on disk under your control
Documentation #
| Product site | Guide, CLI, diagnostics, brand |
| Brand kit | Voice, color, logo |
| Docs index | Package guides |
| API cookbook | Common workflows |
| CI integration | Pipelines & Action |
| Diagnostics FG001–FG010 | What each finding means |
| Contributing | Dev workflow |
| Changelog | SemVer history |
| Security | Vulnerability reporting |
| Code of Conduct | Community norms |
| Sponsor | thanks.dev / GitHub Sponsors |
API reference (after publish): pub.dev/documentation/frameguard
Example & benchmarks #
cd example && flutter run --profile
flutter test
flutter test benchmark/overhead_benchmark.dart
The example app includes intentional jank scenarios (rebuild storm, CPU stall, raster stress, …) so you can validate FrameGuard against known behavior.
Roadmap (honest) #
Shipped foundations: sessions, budgets, baselines, CI, explainability, exporters, scenario stats, local history, budget suggestions.
Next (when evidence exists — never as fake features):
- Deeper GC / memory correlation where public APIs allow
- Android JankStats / iOS signpost adapters
- DevTools extension
- Historical trend dashboards (still local-first)
Contributing #
PRs welcome. Please read CONTRIBUTING.md and the Code of Conduct.
dart format .
flutter analyze
flutter test
Bug reports & ideas: Issues
License #
MIT — see LICENSE.
Support development #
FrameGuard — make performance regressions testable the way functional regressions already are.
