pulseflow_flutter 0.2.6
pulseflow_flutter: ^0.2.6 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.
Prefer working from your coding agent (Cursor, Claude Code, Command Code)? PulseFlow ships an MCP server so the agent can read the session directly — see Use it from your coding agent (MCP).
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 |
| MCP server (optional) | Lets your coding agent read the live session directly (Cursor / Claude / Command Code) |
You need the first three for the full experience; the MCP server is optional and lives in the PulseFlow repo.
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) #
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 |
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).
Use it from your coding agent (MCP) #
PulseFlow ships a local Model Context Protocol server, so an agent that speaks MCP — Cursor, Claude Code, Command Code — can read the live session for you: rank the problems, explain each one, and propose fixes, using the same ranking as the Problems page. It proxies the same bridge, so you run nothing extra beyond PulseFlow itself.
You still need the PulseFlow bridge running. Clone the repo and start it (the first run installs the Node and Dart dependencies):
git clone https://github.com/ialirezairanmanesh/PulseFlow.git
cd PulseFlow
make run # Dart bridge on :3847 (+ dashboard on :3846)
Only the bridge matters for the MCP; the dashboard UI is optional and you can ignore
its browser tab. (Later, make bridge starts just the bridge once the dependencies are
installed.)
Register the MCP server #
The server is published on npm as pulseflow-mcp, so there is nothing to install —
your MCP client runs it with npx.
Cursor — .cursor/mcp.json in your project, or ~/.cursor/mcp.json to use it everywhere:
{
"mcpServers": {
"pulseflow": {
"type": "stdio",
"command": "npx",
"args": ["-y", "pulseflow-mcp"]
}
}
}
Command Code — cmd mcp add pulseflow -- npx -y pulseflow-mcp.
Claude Code / other MCP clients — the same mcpServers entry. Restart the client (or
toggle the server) after adding it.
Developing from a checkout instead? Point the command at <repo>/docs/pulseflow/mcp/run.sh.
Ask the agent #
Connect to your app first — pulseflow_connect (or pass {"mock": true} for a
device-free demo). Then ask in plain language, e.g.:
What are the top performance problems right now, and what should I fix first?
The agent calls pulseflow_report / pulseflow_problems and answers with the ranked
issues and concrete fixes. Edit the code, re-measure, repeat.
Tools: session pulseflow_status, pulseflow_discover, pulseflow_connect,
pulseflow_disconnect; findings pulseflow_report, pulseflow_problems,
pulseflow_widgets, pulseflow_frames, pulseflow_cpu, pulseflow_memory,
pulseflow_network, pulseflow_device, pulseflow_errors, pulseflow_logs;
baselines pulseflow_snapshot / pulseflow_compare; controls pulseflow_debug_option,
pulseflow_hot_widgets.
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 |
| Won't connect, "stale ADB forward" / HTTP 503 | A leftover adb forward points at a dead device port. Run adb forward --remove-all, then flutter run again and use the fresh URL |
| 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.