pulseflow_flutter 0.2.1
pulseflow_flutter: ^0.2.1 copied to clipboard
App-side probes for PulseFlow: accurate frame timing, rebuild hotspots with source locations, HTTP capture, and memory-leak signals.
pulseflow_flutter #
App-side probes for PulseFlow — a problems-first Flutter performance lab.
This package runs inside your Flutter app, registers ext.pulseflow.* service
extensions, and feeds the PulseFlow dashboard with accurate frame timings, rebuild
hotspots, HTTP traffic, leaks, stalls, device context, and lab scenarios.
Without this package the dashboard can still show Build/Raster/memory/CPU via the VM Service. With it you unlock Widgets, richer Problems tips, Network capture, Leaks, Stalls, Scenarios, device context, and accurate engine frame timings.
How PulseFlow fits together #
Your Flutter app ── registerPulseFlow() ──► ext.pulseflow.* (VM Service)
▲
PulseFlow dashboard (browser) ◄── WebSocket ──► Dart bridge
| Piece | Role |
|---|---|
This package (pulseflow_flutter) |
In-app probes + service extensions |
| Dart bridge | Proxies the browser to the Dart VM Service (browsers cannot open arbitrary ws:// targets reliably) |
| Dashboard | Problems-first UI: ranked issues, frames, CPU, memory, network, report |
You need all three for the full experience.
Quick start (end-to-end) #
1. Add the package #
dependencies:
pulseflow_flutter: ^0.2.0
2. Register once in main() #
import 'package:flutter/material.dart';
import 'package:pulseflow_flutter/pulseflow_flutter.dart';
void main() {
registerPulseFlow(appPackage: 'my_app'); // use your pubspec `name`
runApp(const MyApp());
}
Call before runApp. Use your app’s package name from pubspec.yaml as
appPackage so source locations prefer your code over package:flutter.
Hot-restart (or full restart) after adding the package so extensions are registered.
3. Run the app in the right mode #
flutter run # debug — required for widget rebuild / source locations
# or
flutter run --profile # preferred for CPU and realistic frame timings
See Build-mode matrix below — rebuild tracking is debug-only.
4. Start the PulseFlow dashboard + bridge #
git clone https://github.com/ialirezairanmanesh/PulseFlow.git
cd PulseFlow
make run
# or: ./run.sh
- Dashboard:
http://127.0.0.1:3846 - Bridge:
http://127.0.0.1:3847
Requires Node 18+ (22 recommended) and a Dart SDK for the bridge.
5. Connect #
- Open the dashboard.
- Click Find running apps / Scan (also runs when the bridge connects).
- Pick your app → Connect.
- You land on Problems.
If discovery misses the app, paste the VM Service URL from flutter run, e.g.
ws://127.0.0.1:xxxxx/AUTH=/ws.
Use Try demo mode to explore the UI without a device.
What you get in the dashboard #
| Page | What it shows |
|---|---|
| Problems | Health verdict, “Fix this next”, ranked issues |
| Widgets | Rebuild hotspots (debug; route / app-only / during-jank filters) |
| Frames | Build / raster charts, markers, Perfetto export |
| CPU | Record 3/5/10s, top functions, flamegraph |
| Memory | Heap, snapshots / diff, leak signals |
| Network | HTTP list, waterfall (app HttpOverrides + VM HTTP profile) |
| Logs | Runtime / framework logs via the bridge |
| Tools | Debug overlays, stress actions, scenarios, baselines |
| Report / History | Session export, before/after verdict |
| AI | Optional review with your provider API key |
The bridge WebSocket is shared across routes — changing tabs does not reconnect.
What this package adds #
- Accurate frame timings via
SchedulerBinding.addTimingsCallback - Refresh-rate-aware budget from the active display (
1000 / refreshRate) - Widget source locations (
file:line) via the widget inspector (debug) - HTTP capture via a delegating
HttpOverrides(package:http, Dio IO adapter); sensitive query params and URI user-info are redacted - Leak signals from
FlutterMemoryAllocations(when Flutter enables allocation tracking — typically debug) - Device context (
getDeviceContext: platform, display, locale, text scale; optional enricher extras) - UI stall detection for main-isolate freezes (default threshold 250 ms)
- Custom lab scenarios via
registerPulseFlowScenario
registerPulseFlow options #
registerPulseFlow(
appPackage: 'my_app',
captureFrames: true, // engine frame timings (recommended)
captureNetwork: true, // HttpOverrides capture
trackLeaks: true, // FlutterMemoryAllocations when enabled
trackErrors: true, // overflow / assertion / exception signatures
trackImages: true, // image cache + oversized decode hints
trackStalls: true, // main-isolate freeze detection
stallThreshold: const Duration(milliseconds: 250),
deviceEnricher: () async => <String, Object?>{
'appVersion': '1.2.3',
// battery / connectivity / etc. — optional, no forced plugins
},
);
| Flag | Default | Notes |
|---|---|---|
appPackage |
null |
Restricts widget source URIs to this package when set |
captureFrames |
true |
SchedulerBinding.addTimingsCallback |
captureNetwork |
true |
Thin HttpClient wrapper; chains to any previous HttpOverrides |
trackLeaks |
true |
No-op unless kFlutterMemoryAllocationsEnabled |
trackErrors |
true |
Debug/profile |
trackImages |
true |
Debug/profile |
trackStalls |
true |
Debug/profile; threshold via stallThreshold |
deviceEnricher |
null |
Merged into getDeviceContext under extras |
Idempotent: calling again is safe; extensions register only once.
Check isPulseFlowRegistered if needed.
Device enricher only #
registerPulseFlow(
appPackage: 'my_app',
deviceEnricher: () async => <String, Object?>{
'appVersion': '1.2.3',
'batteryPercent': 72,
},
);
Route labels (better Widgets / Problems) #
Rebuilds group by route. Labels resolve in this order:
RouteSettings.name(named routes)RouterURI (MaterialApp.router/ go_router)- Nearest
*Page/*Screenancestor widget
Prefer naming routes:
Navigator.pushNamed(context, '/invoice');
// or
MaterialPageRoute(
settings: const RouteSettings(name: 'invoice'),
builder: (_) => const InvoicePage(),
);
Lab scenarios & stress #
Built-in scenarios (Tools page) #
| Id | What it does |
|---|---|
scrollStorm |
Rapid scroll jumps on primary scrollables |
routeThrash |
Push/pop lightweight routes |
listFlood |
Burst-append items into PulseFlowStressState.invoices |
animationFlood |
Spawn repeating animation controllers |
retainMemory |
Allocate and retain byte buffers |
networkBurst |
Parallel HTTP GETs by default, or your onNetworkBurst hook |
Wire stress state into your UI when useful:
final stress = PulseFlowStressState.instance;
// Listen / rebuild from stress.invoices during listFlood
stress.onNetworkBurst = (params) async {
// optional app-specific burst
};
Custom scenarios #
registerPulseFlowScenario(
const ScenarioInfo(
id: 'openInvoice',
label: 'Open invoice',
description: 'Navigate to invoice detail and scroll',
),
(params, {required shouldStop}) async {
// Drive your app; check shouldStop() between steps.
},
);
Do not reuse built-in ids (scrollStorm, routeThrash, …).
Stress RPCs (from the dashboard) #
injectInvoices— append demo invoices (count)spikeCpu— busy-loop (millis)allocateMemory— retain buffers (megabytes)
Scenarios are blocked in release. Stress RPCs are still registered in all modes; prefer debug/profile for lab work.
Service extensions reference #
All names are under ext.pulseflow.*. The dashboard / bridge call these for you.
| Extension | Purpose |
|---|---|
getFrameStats |
Accurate build/raster/vsync + refresh rate / budget + buildMode / probes |
getDeviceContext |
Platform, locale, display, optional extras |
getStallReport / resetStallProbe |
Main-isolate freeze events |
startWidgetProbe / stopWidgetProbe / resetWidgetProbe / setWidgetProbeFrozen / getHotWidgets |
Rebuild probe (10s rolling window; debug data) |
getRebuildCauses |
Rebuild roots + attributed descendants |
getNetworkLog |
Drain captured HTTP requests (redacted URIs) |
getLeakReport |
Outstanding objects by class |
getErrors |
Overflow / assertion / exception signatures |
getImageStats |
Image cache + oversized decodes |
injectInvoices / spikeCpu / allocateMemory |
Stress |
listScenarios / runScenario / stopScenario |
Built-in + custom lab scenarios |
Build-mode matrix #
Aligned with probeAvailability() in this package:
| Capability | Debug | Profile | Release |
|---|---|---|---|
| Frame timings | yes | yes | yes (if captureFrames) |
| Device context | yes | yes | yes |
| HTTP capture | yes | yes | yes (wrapper; prefer not for production) |
| Errors / images / stalls | yes | yes | no |
| Widget rebuild probe / source locations | yes | no* | no |
| Leak signals | when allocation tracking is on (usually debug) | rare | no |
| Scenarios | yes | yes | no |
| VM Service CPU / memory / timeline (dashboard) | yes | preferred for CPU | no VM Service |
*Rebuild sampling uses debugOnRebuildDirtyWidget, which only fires in debug.
Profile is still the right mode for CPU flamegraphs and more realistic frames.
Typical workflow #
registerPulseFlow+ hot-restart.- Use debug for Widgets / source locations; profile for CPU and frame realism (you can switch modes between passes).
make runin the PulseFlow repo.- Connect → exercise the slow screen → read Problems.
- Drill into Widgets / Frames / Network as needed.
- Run a scenario on Tools to reproduce jank.
- Fix → Record again → compare on Report / History.
Optional CI: the PulseFlow repo ships pulseflow_check (budget gate on P95 build / jank).
Troubleshooting #
| Symptom | Fix |
|---|---|
| “Widget probe not available” / empty Widgets | Call registerPulseFlow, hot-restart, run in debug (not profile/release) |
| App missing from Scan | Paste the full VM Service URL from the flutter run console |
| Empty Network panel | Keep captureNetwork: true; if you set HttpOverrides.global later, chain to the previous override |
| Wrong / missing source files | Set appPackage to your pubspec name; use debug |
| Scenarios disabled | Release mode, or extensions not registered — restart in debug/profile |
| Bridge / UI won’t connect | Confirm ports 3846 (UI) and 3847 (bridge); make stop then make run |
Example app #
Minimal app under example/:
cd example
flutter run
Development (package contributors) #
dependencies:
pulseflow_flutter:
path: ../pulseflow_flutter
# or
pulseflow_flutter:
git:
url: https://github.com/ialirezairanmanesh/pulseflow_flutter.git
flutter test
flutter analyze
dart pub publish --dry-run
Dashboard + bridge: PulseFlow.
License #
MIT — see LICENSE.