The Koru Profiler: One Annotation, a Full Chrome Trace

· 6 min read

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.