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

@forgeax/engine-profiler

v0.3.8

Published

CPU profile capture, bounded recording, and deterministic offline analysis for forgeax-engine.

Readme

@forgeax/engine-profiler

One capability: opt-in, bounded CPU capture with a schema-valid offline artifact. Start a capture, drive the App and Render loop, finish it, then inspect the same ProfileCapture in memory, through the CLI, or through the Remote profiler root.

One-screen takeoff

import {
  buildProfileModel,
  compareProfileCaptures,
  createProfiler,
  validateProfileCapture,
} from '@forgeax/engine-profiler';

const profiler = createProfiler();
const started = profiler.startCapture({ frameLimit: 120, eventLimit: 1024 });
if (!started.ok) throw started.error;

// Drive the App or Render loop here.
const finished = started.value.finish();
if (!finished.ok) throw finished.error;

const checked = validateProfileCapture(finished.value);
if (!checked.ok) throw checked.error;
const model = buildProfileModel(checked.value);
if (!model.ok) throw model.error;
console.log(model.value.summary.completeness.status, model.value.summary.recordCount);

Pass the same profiler to createApp({ renderer, world, profiler }) or to the canvas-form options. The capability is opt-in; an App without it does not create capture records or artifacts.

Progressive disclosure

| Need | Public entry | Result | |:--|:--|:--| | Start a bounded capture | createProfiler().startCapture(limits) | Result<RecorderSession, ProfilerError> | | Finish and retain the artifact | session.finish() | Result<ProfileCapture, ProfilerError> | | Validate persisted JSON | validateProfileCapture(value) | schema and semantic validation | | Build an offline summary | buildProfileModel(capture) | frame and phase projections | | Query from a shell | forgeax debug profile summary --artifact artifact.json --json | structured JSON on stdout |

ProfileCapture is the portable boundary. It carries the fixed version, time unit, bounded frame and event evidence, owner phase catalog, and completeness status. complete, partial, and overflow are explicit outcomes; an overflow artifact remains useful and records its affected frame range.

Compare two imported captures without adding product policy. The projection preserves each side's summary and completeness, unions phases by the full source / parentSource / parentPhase / phase identity, and leaves an absent or null fact unavailable. Present count and skip deltas, plus p95 duration deltas when both sides use a compatible time unit, are right minus left:

const comparison = compareProfileCaptures(leftJson, rightJson);
if (!comparison.ok) {
  console.error(comparison.error.detail.side, comparison.error.detail.path);
} else {
  console.log(comparison.value.phases);
}

Limits and allocation evidence

Always set both frameLimit and eventLimit to positive safe integers. The recorder retains bounded scalar event storage during frames and materializes fixed records only when finish() builds the artifact, reports dropped events after overflow, and never presents an overflow artifact as complete. A host can pass allocationReport to createProfiler to count profiler-owned event object allocations; the deterministic D-6 gate requires zero allocations while the profiler is off.

const allocationReport = { profilerEventObjectAllocations: 0 };
const profiler = createProfiler({ allocationReport });

The phase catalog is exposed as profiler.phaseCatalog. App and Render owners publish their catalogs; consumers should read that relation rather than copy phase names into another list. registerPhaseCatalog(source, phases) returns an idempotent lease disposer. The App frame-loop and Renderer release their own definitions at terminal teardown, so reusing a Profiler does not retain a dead owner. A finished ProfileCapture keeps an immutable catalog snapshot.

The default capture detail is owner, which records the stable App/Render phases with the normal profiler overhead budget. For a bounded attribution pass, opt into nested; Render then records producer-owned children such as record/validation and record/graph-execute, and each child carries its parentSource and parentPhase so offline totals do not double-count nested time.

const started = profiler.startCapture({
  frameLimit: 120,
  eventLimit: 2048,
  detail: 'nested',
});

Offline and CLI workflow

forgeax debug profile summary --artifact profile-capture.json --json
forgeax debug profile frame --artifact profile-capture.json --frame-id 12 --json
forgeax debug profile phase --artifact profile-capture.json --source render --phase record --json
forgeax debug profile compare --left-file before.json --right-file after.json --json

The CLI reads one ProfileCapture JSON object from --file or two objects from compare --left-file/--right-file and emits structured JSON. Compare input failures identify the left or right side. It does not reconnect to a live App. The same artifact can be checked before analysis:

This is a copyable schema-valid complete artifact. The owner catalog is part of the artifact, so a consumer can validate and analyze it without a separately maintained phase list.

{
  "schemaVersion": "1.0",
  "captureId": "capture-0001",
  "timeUnit": "microseconds",
  "frameLimit": 1,
  "eventLimit": 8,
  "phaseCatalog": {
    "app": [
      "frame-total",
      "world-update-primary",
      "draw-source",
      "world-update-injected",
      "renderer-draw"
    ],
    "render": ["extract", "bind-groups", "features", "sort", "record"]
  },
  "records": [
    {
      "kind": "phase",
      "source": "app",
      "frameId": 1,
      "phase": "frame-total",
      "startMicros": 1000,
      "endMicros": 1100,
      "durationMicros": 100
    }
  ],
  "completeness": {
    "status": "complete",
    "retainedEventCount": 1,
    "droppedEventCount": 0
  }
}
const input = JSON.parse(text);
const result = validateProfileCapture(input);
if (!result.ok) {
  console.error(result.error.code, result.error.detail.path, result.error.hint);
}

Errors and boundaries

Expected failures return Result; inspect .ok, then use .error.code, .error.expected, .error.hint, and .error.detail. The closed error union is defined in packages/profiler/src/errors.ts; keep exhaustive handling there instead of parsing messages or reproducing the member list in a consumer.

Profiler owns capture records, bounds, allocation evidence, and offline projections. It does not own GPU timestamps, ECS scheduling, a browser UI, a network method, or a live trace service. Remote exposure remains the host's explicit profiler root opt-in through the existing eval and introspect methods.

GPU pass timing stays outside ProfileCapture. The Render package owns the opt-in, draw() receipt, receipt-bound observation, pass facts, and recovery; the benchmark package owns its separate fail-closed validator. App and Runtime may transparently forward gpuPassTiming, but they do not create a session or second controller. Membership timing is producer-specific and is not generic accepted evidence. Keep CPU phase names, units, and schema unchanged when a GPU timing capability is present.

Public surface

| Entry | Purpose | |:--|:--| | createProfiler(options?) | Creates an opt-in bounded recorder with an optional sink, clock, catalog, and allocation report. | | profiler.registerPhaseCatalog(source, phases) | Registers one owner catalog and returns its idempotent removal lease. | | ProfileCapture | Versioned artifact accepted by validation, model building, and the CLI. | | validateProfileCapture(value) | Validates schema and semantic invariants before offline use. | | buildProfileModel(capture) | Projects retained records into summaries without changing the artifact. | | compareProfileCaptures(left, right) | Projects two validated captures into side summaries and a deterministic phase union. | | createProfileClock() | Supplies the default monotonic microsecond clock. |

The package root is the only supported import path for these entries. See schema/profile-capture.schema.json for the artifact contract and scripts/bench/profiler-overhead.mjs for the deterministic D-6 consumer gate. The gate alternates profiler-off/on windows, warms each new capture before sampling, keeps finalization outside the measured window, and uses $median(((p95_{on,i} - p95_{off,i}) / p95_{off,i}) \times 100)$ across paired groups. The owner-mode ceiling is 20%; allocation, overflow, and phase-catalog evidence remain hard-failing.