NAME
====

Selkie::Trace - runtime span tracing (Chrome Trace JSON / slow-span JSONL)

SYNOPSIS
========

```raku
Selkie::Trace.init(mode => 'trace', trace-path => $path);
my $span = Selkie::Trace.enabled
    ?? Selkie::Trace.start('widget.render', cat => 'render',
        args => %(rows => 4))
    !! Nil;
# ... work ...
$span.finish(ok => True) with $span;
Selkie::Trace.shutdown;   # drains + writes the JSON tail
```

DESCRIPTION
===========

Three modes: `off` (default — `.enabled` is a state-var check and `.start` returns the type object), `slow` (synchronous JSONL of spans over their threshold — cheap enough for day-to-day use), and `trace` (complete Chrome Trace capture — every span, for deliberate profiling sessions only).

In `trace` mode, events are enqueued lock-free on a `Channel` and a dedicated writer thread owns all JSON serialization and file I/O, so span capture on hot paths costs an arg snapshot + a channel send, not a file write. `shutdown`/`reset` close the channel, join the writer, and only then append the JSON tail — the file is complete and valid once `shutdown` returns. Producers racing shutdown lose their events (sends on the closed channel are swallowed), which is the intended "capture ends now" semantic.

Span discipline
---------------

Create spans **after** any cheap early-return guard, never before: a method whose common case is a no-op (an idempotent destroy, an already-loaded load) must not emit an event that records nothing — per-frame no-op spans dominated a real capture at 27 events/frame before this rule.

### sub drain-trace-writer

```raku
sub drain-trace-writer() returns Nil
```

Close the channel and join the writer so every queued event is on disk, leaving the handle ready for the JSON tail. Callers hold `$lock`. Idempotent.

