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.
Libraries
- frameguard
- FrameGuard — automated Flutter performance regression detection.
- frameguard_test
- FrameGuard test helpers, matchers, and synthetic jank utilities.