Built-in Profiler
Live visibility into scheduler throughput, memory allocations, message rates, and per-actor statistics — with zero code changes. Inspired by Go's net/http/pprof.
Quick Start
Set the HEW_PPROF environment variable, then open the URL in your browser:
HEW_PPROF=:6060 hew run my_program.hew
# Open http://localhost:6060/That's it. No imports, no annotations, no config files. The profiler is built into the runtime and activated purely via environment variable.
How it works
When HEW_PPROF is set, the runtime spawns two additional OS threads alongside the normal actor scheduler:
- Sampler thread — captures a metrics snapshot every second into a ring buffer (5 minutes of history)
- HTTP server thread — serves a self-contained dashboard and JSON API
These are plain OS threads, not Hew actors, so the profiler remains responsive even if the actor scheduler is overloaded or deadlocked. The profiler has no external dependencies — the dashboard is a single HTML page with inline JavaScript, embedded in the binary at compile time.
Activation
The HEW_PPROF variable accepts a bind address:
| Value | Effect |
|---|---|
| (unset) | Profiler disabled (default) |
| :6060 | Listen on all interfaces, port 6060 |
| 127.0.0.1:6060 | Listen on localhost only |
| 0.0.0.0:9090 | Listen on all interfaces, port 9090 |
Dashboard
The dashboard is a live-updating single-page app with five panels:
- Scheduler — tasks spawned/completed rates, active workers, work-steal rate
- Memory — bytes live (area chart), peak bytes, allocation count and rate
- Messages — send/receive rates, work steal count and rate
- Allocation Timeline — allocation rate over time (full-width area chart)
- Actors — table of live actors sorted by message count, with dispatch time averages, mailbox depth, and high-water marks
All panels auto-update every second via fetch polling. No WebSocket required.
JSON API
The profiler exposes four JSON endpoints for programmatic access or custom tooling:
GET /api/metrics
Current snapshot of all scheduler and memory counters:
{
"timestamp_secs": 42,
"tasks_spawned": 1500,
"tasks_completed": 1498,
"steals": 23,
"messages_sent": 5000,
"messages_received": 4998,
"active_workers": 3,
"alloc_count": 12345,
"bytes_live": 48576,
"peak_bytes_live": 65536
}GET /api/actors
Per-actor statistics and mailbox depths:
[
{
"id": 1,
"pid": 1,
"state": "idle",
"msgs": 3000,
"time_ns": 3170039,
"mbox_depth": 0,
"mbox_hwm": 2921
}
]| Field | Meaning |
|---|---|
| state | idle, runnable, running, blocked, stopping, crashed, stopped |
| msgs | Total messages dispatched to this actor |
| time_ns | Cumulative nanoseconds spent in dispatch |
| mbox_depth | Current mailbox queue depth |
| mbox_hwm | Mailbox high-water mark (max depth ever observed) |
GET /api/memory
Current allocator statistics (alloc/dealloc counts, bytes allocated/freed/live, peak).
GET /api/metrics/history
Time-series ring buffer with up to 300 entries (5 minutes at 1-second intervals). Each entry contains all scheduler and memory counters, using short keys (t=timestamp, ts=tasks spawned, bl=bytes live, etc.) for compact transfer.
What to look for
Mailbox backpressure
If an actor's mbox_depth is consistently high or mbox_hwm is much larger than expected, the actor is receiving messages faster than it can process them. Consider splitting work across multiple actors, using bounded mailboxes, or reducing message volume.
Hot actors
Sort the actors table by message count or total time to find bottlenecks. An actor with high time_ns but low msgs has expensive per-message processing. An actor with high msgs but low time_ns is fast but heavily trafficked.
Memory leaks
Watch the Memory panel's bytes-live chart. In a healthy program, bytes live should stabilize after startup. A steadily growing line suggests a leak — likely actors accumulating state or collections that are never freed.
Scheduler saturation
If active_workers equals the total worker count (CPU cores) continuously, the scheduler is saturated. A high steal rate indicates load imbalance across workers.
Performance impact
The profiling allocator adds one AtomicU64::fetch_add per allocation and deallocation (~5–15 ns on x86_64). Per-message dispatch timing adds one Instant::now() pair per message (~20 ns). These counters are always active regardless of whether HEW_PPROF is set — the overhead is negligible for most workloads.
The HTTP server, sampler thread, and actor registry are only active when HEW_PPROF is set.
Example
actor Counter {
let count: Int;
receive fn tick() {
count = count + 1;
}
receive fn report() {
println(f"Counter: {count}");
}
}
fn main() {
let counter = spawn Counter(count: 0);
var i = 0;
while i < 3000 {
counter.tick();
i = i + 1;
}
sleep_ms(2000);
counter.report();
}HEW_PPROF=:6060 hew run profiler_demo.hew
# [hew-pprof] dashboard at http://0.0.0.0:6060/
# Open http://localhost:6060/ to see live chartsProfile Export
The profiler can export profiles in two standard formats for use with external tools like go tool pprof and gprof.
pprof (Google protobuf format)
Download a pprof-compatible heap profile while the program is running:
curl -o heap.pb.gz http://localhost:6060/debug/pprof/heap
# Analyze with Go's pprof tool
go tool pprof heap.pb.gz
# Or the standalone pprof tool
pprof -top heap.pb.gz
pprof -text heap.pb.gzThe heap profile models each actor as a "function" with two value types: alloc_objects (messages processed) and alloc_space (estimated bytes). A [runtime] entry captures global allocator statistics.
Flat profile (gprof-style text)
View a human-readable flat profile directly in your terminal:
curl http://localhost:6060/debug/pprof/profileFlat profile:
Total processing time: 3.2 ms across 3 actor(s)
Memory: 184.4 KB live (194.4 KB peak), 549 allocations
% cumulative self self total
time time(ms) time(ms) calls us/call us/call name
62.5 2.0 2.0 2000 1.0 1.0 Actor#1 (pid=1, state=idle)
25.0 2.8 0.8 800 1.0 1.0 Actor#2 (pid=2, state=idle)
12.5 3.2 0.4 200 2.0 2.0 Actor#3 (pid=3, state=idle)
Mailbox summary:
Actor Depth HWM
#1 0 150
#2 0 80
#3 0 20Automatic export on exit
Set HEW_PROF_OUTPUT to automatically write profile files when the program exits. This works independently of HEW_PPROF — no dashboard needed.
# Write pprof heap profile on exit
HEW_PROF_OUTPUT=pprof ./my_hew_program
# Write gprof-style flat profile on exit
HEW_PROF_OUTPUT=flat ./my_hew_program
# Write both files
HEW_PROF_OUTPUT=both ./my_hew_program| Value | Files written |
|---|---|
| pprof | hew-profile.pb.gz |
| flat | hew-profile.txt |
| both | Both files |
Files are written to the current working directory when the scheduler shuts down.