The Koru Profiler: One Annotation, a Full Chrome Trace
Profiling usually costs you something before it tells you anything: a library to pull in, timing calls to sprinkle, a build mode to remember, and the quiet worry that the instrumentation is skewing the numbers. Koru’s profiler asks for one annotation. You mark the import, pass a flag, and the whole program comes out as a standard Chrome Tracing file — every event transition on a timeline, framed by a start-to-end envelope, with the profiler nowhere in its own trace.
Using it
Two things turn it on. Annotate the import, and pass --profile:
import std/io
[profile]import std/profiler
pub event get-number {} -> i32
get-number -> 42
get-number(): n |> std/io:print.ln("Got number: {{ n:d }}") That is the whole program — pure Koru, no escape hatch. get-number -> 42 is the implementation: the event returns the value, and there is no separate function
body to write. Compile it and run:
koruc demo.k --profile
./a.out The program runs normally (Got number: 42) and drops a Chrome Tracing file at /tmp/koru_profile.json. Open it in chrome://tracing, or drag it into Perfetto — both read this format directly.
What comes out
This is the actual trace from the program above, verbatim:
{"traceEvents":[
{"name":"thread_name","ph":"M","pid":0,"tid":0,"args":{"name":"transitions"}},
{"name":"thread_name","ph":"M","pid":0,"tid":1,"args":{"name":"program"}},
{"name":"koru:start","cat":"meta","ph":"i","s":"g","ts":1784343728649665,"pid":0,"tid":1},
{"name":"demo:get-number","cat":"transition","ph":"X","ts":1784343728649672,"dur":1,"pid":0,"tid":0},
{"name":"std.io:print.ln","cat":"transition","ph":"X","ts":1784343728649673,"dur":1,"pid":0,"tid":0},
{"name":"koru:end","cat":"meta","ph":"i","s":"g","ts":1784343728649682,"pid":0,"tid":1},
{"name":"program","cat":"meta","ph":"X","ts":1784343728649665,"dur":17,"pid":0,"tid":1}
]} There are two lanes. The transitions lane (tid 0) is what your program did — get-number fired, then print.ln — each a real timed span. The program lane
(tid 1) is the envelope: the koru:start and koru:end anchors, and one
synthesized program span between them. That span’s dur of 17 covers every
transition inside it; the anchors are meta instants, not rows of work.
That distinction is deliberate. koru:start and koru:end are meta-events —
the frame, not the content. They mark where the run begins and ends; they are
never recorded as transitions. A trace has the things your program did, and the
envelope that says this was the run — and the two never masquerade as each other.
It is not a compiler feature — it is taps
Here is the part worth lingering on: the profiler is a library, not a builtin. It is written in Koru’s own observability primitive, the tap. A single universal tap watches every event transition in the program:
~[opaque]tap(* -> *) * -> * is “every transition.” That one tap is the whole transitions lane. The
start and end anchors are two more taps on the meta-events. The compiler grew no
special profiling machinery — it already had taps, and the profiler is what you
build with them.
And taps are not a builtin either. std/taps is itself a library — tap is an
ordinary [comptime|transform] event that rewrites the program’s flows at compile
time. In its own words: “Taps are not a language primitive — they’re built using
the same transform system available to any Koru library.” So this is not a library
resting on a privileged primitive. It is libraries all the way down to the one thing
the compiler actually provides: compile-time AST transformation. The profiler is a
library, built on taps, which are a library, built on transforms. Nothing in this
whole stack is a keyword.
The [opaque] is what makes it honest. An opaque tap is invisible to other taps —
so the profiler’s own writes (write-header, write-event, write-footer) are
never themselves profiled. The instrument does not perturb the measurement, and the
trace you get is your program, not your program plus the cost of watching it.
Zero-cost when it is off
Because the whole thing is gated on [profile] and --profile, a build
without the flag dead-strips every tap at compile time. There is no runtime check
that profiling is disabled, because there is no profiling code in the binary at all
— it is not compiled in and switched off, it is simply not there. You ship the same
program you profiled, minus the instrument.
The source
The whole profiler is one small library file: koru_std/profiler.kz. There is no compiler magic hidden elsewhere — what you see there is what runs.
Where it is today
The current profiler is single-threaded — it captures a clean timeline for a single-threaded program, which is where it earns its keep right now. Per-thread buffering for concurrent programs is the next step. What already holds is the shape: one annotation, a standard trace, an honest envelope, and an instrument built from the language’s own primitives rather than bolted onto the compiler.