npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@zakkster/lite-trace

v1.1.0

Published

Zero-dependency nested-span tracer: preallocated struct-of-arrays span pool with stack-based begin/end, counters, instants, async events, ring-buffer mode, and Chrome Trace Event export for chrome://tracing / Perfetto.

Readme

@zakkster/lite-trace

npm version Zero-GC sponsor npm bundle size npm downloads npm total downloads license tests deps types

A zero-dependency nested-span tracer. The other profilers in this ecosystem report flat buckets -- total time in a phase, GC counts, draw calls. This one captures the structure of a frame: which span ran inside which, and how much time each one owns by itself. You get a flamegraph, a flat profile, and Chrome-trace JSON you can drop straight into Perfetto -- with counters, point-in-time instants, and async spans that cross frame boundaries all on the same timeline.

How it works

Spans live in a preallocated struct-of-arrays pool (parent, start, end, depth, tagId, plus args, type, asyncId, counterVal for the v1.1 entry kinds). begin() opens a span beneath whatever is currently open; end() closes the most recent one. Tags are interned to integer ids the first time they're seen, so repeated tags cost a Map.get and nothing more -- begin/end allocate nothing in steady state. Counters, instants, and async begin/end share the same pool and the same reset() / drop / ring semantics as spans; they just carry a different type byte. The output methods build arrays and objects, but they run when you read results, not on the hot path.

Install

npm i @zakkster/lite-trace

No dependencies. No peers.

Trace a frame

import { Tracer } from '@zakkster/lite-trace';

const tr = new Tracer(2048);

function frame() {
  tr.reset();
  tr.begin('frame');
    tr.span('update', update);
    tr.begin('render');
      tr.span('cull', cull);
      tr.span('draw', draw);
      tr.span('post', post);
    tr.end();
  tr.end();

  drawFlamegraph(tr);          // see below
  requestAnimationFrame(frame);
}

span(tag, fn) is the safe form -- the matching end runs in a finally, so a throw inside fn can't desync the stack. Prefer it unless you need the raw begin/end pair.

Read the results

Flat profile (summary)

const s = tr.summary();
// s.byTag.render -> { count, totalMs, selfMs, maxMs, avgMs }

selfMs excludes time spent in child spans. A render span that mostly waits on cull + draw + post shows a small self time; the children show where the frame actually went. s also carries spans, overflow, maxDepth, and wallMs. Counters, instants, and async entries are excluded from summary and forEach -- they're span-only views.

Flamegraph (forEach)

forEach walks spans in record order with no allocation, giving you everything a bar needs:

function drawFlamegraph(tr) {
  const t0 = /* earliest start */;
  tr.forEach((id, tag, start, end, dur, depth, parent) => {
    const x = (start - t0) * pxPerMs;
    const y = depth * rowHeight;
    const w = dur * pxPerMs;
    ctx.fillRect(x, y, w, rowHeight - 1);   // x = when, y = depth, w = duration
  });
}

Nested tree (tree)

const roots = tr.tree();
// roots[0] -> { tag:'frame', durMs, children:[ { tag:'render', children:[...] } ] }

Nodes include an args field when the corresponding span was opened with args.

Chrome trace / Perfetto (toChromeTrace)

The one that plugs into existing pro tooling. toChromeTrace() returns the Chrome Trace Event format: X (complete) for spans, i (instant) for markers, C (counter) for sampled values, and b/e (nestable async begin/end) for cross-frame operations -- all in microseconds.

const blob = new Blob([JSON.stringify(tr.toChromeTrace())], { type: 'application/json' });
// download it, then drag the .json into https://ui.perfetto.dev or chrome://tracing

You get a real, zoomable flamegraph rendered by Perfetto -- from a tracer that added zero dependencies and no allocation to your frame.

Beyond span pairs

The additions below are the v1.1 surface. The v1.0 API (begin / end / span / reset / forEach / summary / tree / toChromeTrace) is unchanged and remains fully backward-compatible.

Args on spans

Attach any small key-value object to a span; Perfetto shows it as a tooltip when you hover the bar, and tree() surfaces it as an args field on the node.

tr.begin('draw', { batches: 128, mode: 'instanced' });
// ...
tr.end();

tr.span('physics', step, { bodies: 4096 });

Consumers that don't pass args pay nothing -- the SoA column stays null.

Ring-buffer mode

Default mode drops new spans when the pool fills. Ring mode overwrites the oldest instead:

const tr = new Tracer(2048, { mode: 'ring' });

Now begin() never returns -1, and at any point you can dump the last N events. That's what "something bad just happened, give me the last four seconds" looks like in production.

Counters (counter)

Numeric samples at the current timestamp; Chrome Trace C events. In Perfetto they become a chart track above the flamegraph -- a running plot alongside the spans.

tr.counter('frameTimeMs', 14.3);
tr.counter('activeSignals', graph.count);
tr.counter('drawCalls', gl.stats.draws);

Counters don't appear in forEach or summary (both are span-only). Reach them via forEachRaw if you want to render them yourself.

Instants (instant)

Point-in-time markers with no duration; Chrome Trace i events. Optional args.

tr.instant('gc-pause', { reason: 'scavenge', pauseMs: 1.4 });
tr.instant('input', { key: 'w' });

Rendered as vertical lines in Perfetto and in the demo's flamegraph canvas.

Async events (beginAsync / endAsync)

Operations that cross frame boundaries -- asset loads, streaming queries, WebSocket round-trips. beginAsync returns a monotonic integer asyncId; pass it back to endAsync when the operation completes.

const id = tr.beginAsync('asset-load', { url: 'level.bin' });
// ... later, possibly many frames later ...
tr.endAsync(id, { status: 'ok', bytes: 82531 });

The id is a plain number, not an object, so nothing allocates for the correlation. A mismatched endAsync produces garbage in Perfetto but never throws -- this is visual debugging, not runtime validation.

forEachRaw -- every entry

forEach walks spans only. forEachRaw walks every entry (span, counter, instant, async begin/end) in record order, and hands you the type, args, asyncId, and counterVal alongside the usual fields.

tr.forEachRaw((id, tag, start, end, type, depth, parent, args, asyncId, counterVal) => {
  // type: 0=span, 1=instant, 2=counter, 3=asyncBegin, 4=asyncEnd
});

Chrome trace metadata

toChromeTrace({ metadata: true }) prepends M (process_name, thread_name) events using options.name (default 'main'). Multi-track traces from several tracers become readable in Perfetto without a custom viewer.

const tr = new Tracer(4096, { name: 'game-loop' });
// ...
const json = JSON.stringify(tr.toChromeTrace({ pid: 1, tid: 1, metadata: true }));

Off by default so pre-1.1 consumers see identical output.

Balance and overflow

Every begin needs one end. Two rules keep that safe:

  • On overflow in drop mode (more than capacity spans), a begin still returns -- as -1 -- and still consumes one end, so the open-stack never desyncs. Dropped spans are counted in tr.overflowed. In ring mode overflow never happens; the oldest entry is overwritten instead.
  • A stray end() with nothing open is ignored, not an underflow.

capacity is rounded up to a power of two.

Testing

npm test        # node --test

37 tests across four files, driven by an injected deterministic clock. Covers begin/end balance, nesting, self-time accounting, per-tag aggregation, tree() shape, toChromeTrace microsecond output, overflow safety, stable tag ids across reset(), and every v1.1 surface: ring mode (overwrite + reset + mixed entry types), counters (recording, Chrome trace, exclusion from forEach/summary), instants (recording, exclusion, args), args on begin/span/tree, async monotonic ids and b/e pair emission, metadata events (opt-in + off-by-default), and forEachRaw across all entry types.

The demo (demo/index.html) runs a live workload -- frame with nested update, render, draw×N, post children -- while counters track frame time and span count, instants mark simulated GC pauses, and async spans span frame boundaries. Export a Chrome trace and drop it into Perfetto.

License

MIT (c) Zahary Shinikchiev