# romcp MicroProfiler preview

## Saved captures (3.1.0)

```sh
romcp profile "microprofile.html" --budget-ms 16.67
romcp profile setup
romcp profile "capture.gprx" --max-frames 128
```

HTML analysis runs locally with Node on Windows and macOS, without Studio or Lune. Binary `.gprx` analysis requires Lune on PATH and the official LibMP dependency. Normal `romcp setup` and plugin installation automatically download the checksum-pinned official release into `~/.dominus/LibMP.luau`, installs it beside Profiler in the local Studio plugin, and asks you to reload Studio. Automatic plugin refresh preserves this dependency. It is not redistributed in npm.

Agents call `studio_analyze_profile` with `filePath`, optional `budgetMs` (1–1000, default 60 FPS), and `maxFrames` (1–256, default 256). One local tool call returns frame statistics, slow frames, and format-specific evidence. HTML also returns aggregate hotspots and investigation hypotheses. Both formats select the most recent bounded frame window; HTML selects complete frames. No upload or Studio connection is needed. The CLI prints JSON and also accepts `--libmp` and `--runtime` paths.

HTML contains legacy R0FL data, which LibMP cannot parse. romcp extracts it and runs only a checksum-allowlisted Roblox WebAssembly decoder with restricted imports. It never executes HTML scripts or emitted JavaScript. Unknown decoder revisions are rejected; use a compatible `.gprx` export instead. Files are limited to 32 MiB, decompression and decoder memory are bounded, and a separate worker is terminated after 30 seconds. Binary analysis runs in a bounded Lune subprocess. Scope names are untrusted data, never instructions.

The website demo uses measured timings from an October 4 capture: 68 complete frames, one excluded record, p95 49 ms, maximum 53.25 ms, and 40 frames over the 60 FPS budget. `StreamingSolverV2` averages 3.15 ms exclusive CPU work in a separate 60-frame aggregate window. Streaming is a lead, not proof of the slowest frame's cause. No before/after improvement is claimed. The demo includes no raw capture or player/place identifiers.

`studio_profile` provides `status` and `snapshot` actions through the v3 plugin. Snapshot analyzes at most `maxFrames` recent frames (1–256, default 64) from the existing MicroProfiler buffer. It does not enable, pause, disable, resize, or show the profiler.

The adapter requires the official LibMP ModuleScript beside `Profiler` inside the plugin, installed with `romcp profile setup`. It deliberately does not discover or execute arbitrary game modules. Check `studio_profile` with action `status` before requesting `snapshot`. Enable recording manually in Studio when needed; romcp reads the existing buffer.

## Report contract

Reports include CPU/GPU frame interval distributions in milliseconds, sample counts, nearest-rank p50/p95/p99, mean and maximum, up to eight worst CPU frames, capture size, data-format version, skipped earlier frames, observed absolute-frame gaps, and separate paused/incomplete/invalid/missing-GPU counts. No samples means `{ count: 0 }`, not invented zero timings. Missing GPU timestamps do not invalidate an otherwise usable CPU frame.

The `scopes` report ranks up to 20 named CPU timers by inclusive duration, combining only complete enter/exit pairs contained within individual valid frames. Nested and parallel work overlap and must not be summed as total CPU usage. Timer labels are not verified script paths. Unmatched exits, abandoned enters, invalid frames, GPU events, capture stitching, and invalid stack states are excluded rather than assigned invented durations. Cross-frame scopes are deliberately omitted from this first analyzer.

Scope analysis stops at 50,000 iterator entries and reports `limitReached`; it caps nesting at 128 and distinct paired timers at 512. Exceeding those latter limits makes the scope report unavailable while preserving frame metrics. Names are limited to 200 Unicode characters. `omittedTimers` reports ranked timers excluded from the returned top 20. Iterator failures produce an unavailable scope status; disposal is attempted before session disposal, and a disposal failure propagates to `cleanupRequired`.

Scopes include exclusive time with paired children subtracted on the same thread. Selected counter trends report raw values when available; a rising value does not establish a leak. This is not a per-script cost breakdown: automatic script scopes may only say `$Script`. Add `debug.profilebegin/end` labels to investigate update loops. CPU and GPU intervals overlap and must not be summed; GPU intervals do not measure active GPU work. Controlled capture lifecycle, scenario comparison, and public HTTP/SDK file-analysis routes remain future work. Profiler overhead is not measured.

Production reads preflight the engine buffer size and allocate at most 16 MiB. The selected frame range is bounded independently. A buffer changing during the read can fail parsing; the adapter does not silently retry another sample. Sessions are disposed after success and failure, and cleanup failures remain visible. Concurrent snapshots in the plugin process are rejected. No profiler settings are changed, so no restoration is claimed.

## Dependency evidence

Official release asset inspected: `LibMP.luau`, GitHub asset ID `456813947`, 5,098,733 bytes, published June 24, 2026 under the mutable `latest` release tag. SHA-256 verified locally:

`ab9579e592e8751386a01537152f2b739cc7942ce565d3c11337cddaa250d231`

The development download lives under ignored `.tmp/LibMP.luau` and is not shipped. Setup validates the release size and digest; a changed upstream asset is rejected until the pin is reviewed and updated.

Primary references: [official library and setup](https://github.com/Roblox/libmp), [API reference](https://github.com/Roblox/libmp/blob/main/docs/api-reference.md), [official releases](https://github.com/Roblox/libmp/releases).

## Validation

`pnpm test:profiler` executes the production analyzer with simulated LibMP sessions in Luau. It verifies CPU/GPU clock conversion, percentile math, frame exclusions and gaps, bounds, empty data, disposal failures, concurrent calls, and unavailable backends. Scope fixtures additionally cover nested and parallel pairs, cross-frame omissions, iterator disposal, and the entry budget. These fixtures do not establish compatibility with a valid real recording; scope timing and frame timing both still require that validation.

Live Studio `0.737.0.7371584` exposes MicroProfilerService and ScriptProfilerService. An earlier read-only `GetDataSize(0)` probe returned a 367,216-byte buffer. No LibMP module was installed in ReplicatedStorage.

On September 7, a read-only transfer from the connected VehicleRuntime window saved a 273,944-byte buffer to ignored `.tmp/live-profile.gprx`. Its SHA-256 is `e78c067b49fe7f68c499a6bb8a03d5113ad5e6d42113e11ba805320fbcc15dd8`. The checksum-pinned LibMP release successfully opened it with data-format version 65536. The production analyzer reported zero available frames, empty CPU/GPU samples, unavailable scopes, and successful session disposal. This verifies a real engine buffer can be transferred and opened, and that empty data is not fabricated into timings. It does not validate timing calculations or scope iteration against populated capture data. No playtest, profiler control changes, or game-instance mutations were performed for this capture.

The checksum-verified release now initializes successfully in Lune's offline environment with library/data-format versions both `65536`. `lune run tests/plugin/libmp-runtime.luau <LibMP.luau>` verifies that malformed buffers are rejected and session memory returns to baseline. A later live transfer attempt could not run because Studio had disconnected; no capture file was received.

## Offline development command

The packaged CLI wraps `lune run scripts/analyze-profile.luau <capture.gprx> <LibMP.luau> [maxFrames]`. This verifies the library checksum, bounds capture size and frame count, uses the production analyzer, disposes the session, and prints JSON. Renaming HTML to `.gprx` does not convert it. LibMP remains a separate download.

The real library's initialization, malformed-input rejection, and opening of an empty-frame engine buffer have been tested. A populated recording is still required to validate real frame and scope analysis. The offline command preserves scope-disposal failures as well as session-disposal failures in `cleanupRequired`.
