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:

Enable the profiler
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:

ValueEffect
(unset)Profiler disabled (default)
Listen on all interfaces, port 6060
Listen on localhost only
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:

Response
{
  "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:

Response
[
  {
    "id": 1,
    "pid": 1,
    "state": "idle",
    "msgs": 3000,
    "time_ns": 3170039,
    "mbox_depth": 0,
    "mbox_hwm": 2921
  }
]
FieldMeaning
stateidle, runnable, running, blocked, stopping, crashed, stopped
msgsTotal messages dispatched to this actor
time_nsCumulative nanoseconds spent in dispatch
mbox_depthCurrent mailbox queue depth
mbox_hwmMailbox 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

profiler_demo.hew
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();
}
Run with profiler
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 charts

Profile 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:

Download and analyze
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.gz

The 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:

View flat profile
curl http://localhost:6060/debug/pprof/profile
Example output
Flat 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       20

Automatic 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.

Auto-export on exit
# 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
ValueFiles written
hew-profile.pb.gz
hew-profile.txt
Both files

Files are written to the current working directory when the scheduler shuts down.