instar 1.3.229 → 1.3.230
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +11 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/devProfileNode.d.ts +62 -0
- package/dist/commands/devProfileNode.d.ts.map +1 -0
- package/dist/commands/devProfileNode.js +179 -0
- package/dist/commands/devProfileNode.js.map +1 -0
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +2 -0
- package/dist/scaffold/templates.js.map +1 -1
- package/package.json +1 -1
- package/scripts/docs-coverage.mjs +14 -4
- package/scripts/generate-builtin-manifest.cjs +1 -0
- package/src/data/builtin-manifest.json +42 -34
- package/src/scaffold/templates.ts +2 -0
- package/upgrades/1.3.230.md +66 -0
- package/upgrades/dev-profile-node-tool.eli16.md +42 -0
- package/upgrades/side-effects/dev-profile-node-tool.md +85 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: minor -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
Adds `instar dev:profile-node [pid]` — a contributor/agent dev command that
|
|
9
|
+
CPU-profiles a **running** node process and prints its hottest JS functions
|
|
10
|
+
(function + `file:line` + self-time %).
|
|
11
|
+
|
|
12
|
+
**Why this exists (the technique that cracked a real bug).** macOS `sample` — and
|
|
13
|
+
lsof snapshots, empty `gh run --log`, port-grep — cannot symbolicate a node
|
|
14
|
+
process's **JS** frames; they show only native/V8 frames, so a busy agent server's
|
|
15
|
+
actual hot *function* stays invisible. The way to see it is the node process's own
|
|
16
|
+
introspection: `SIGUSR1` opens node's inspector on the running process, then a CDP
|
|
17
|
+
CPU profile over the inspector websocket reports the exact JS call frames. This is
|
|
18
|
+
literally how the systemic agent-server hot-loop was pinned — `StateManager.listSessions`
|
|
19
|
+
burning ~30% of CPU in `readFileUtf8`, invisible to every other tool. This command
|
|
20
|
+
turns that hard-won procedure into one reusable command.
|
|
21
|
+
|
|
22
|
+
**What it does:** resolves the pid (or auto-finds the hottest `node` process) →
|
|
23
|
+
`SIGUSR1` → probes `127.0.0.1:9229-9235` for the inspector target → captures a CPU
|
|
24
|
+
profile (default 5s, `--duration`) → prints the top frames by self-time (`--top`).
|
|
25
|
+
The biggest non-idle frame is the hot path. Read-only aside from the SIGUSR1, which
|
|
26
|
+
opens the inspector on localhost for the process's lifetime (it closes on the next
|
|
27
|
+
restart) — the command says so in its output.
|
|
28
|
+
|
|
29
|
+
Pairs with `dev:ci-failures` and `dev:preflight` as the contributor dev-loop
|
|
30
|
+
toolkit (friction → tooling).
|
|
31
|
+
|
|
32
|
+
## What to Tell Your User
|
|
33
|
+
|
|
34
|
+
Nothing required — it is a developer/agent tool for working on Instar. When an
|
|
35
|
+
agent server is mysteriously burning CPU, the instar dev:profile-node command
|
|
36
|
+
prints the exact JS function responsible, which native profilers cannot show.
|
|
37
|
+
|
|
38
|
+
## Summary of New Capabilities
|
|
39
|
+
|
|
40
|
+
- `instar dev:profile-node [pid] [--duration <sec>] [--top <n>]` — CPU-profile a
|
|
41
|
+
running node process (SIGUSR1 + inspector + CDP) and print its hottest JS frames.
|
|
42
|
+
No pid → profiles the hottest node process.
|
|
43
|
+
|
|
44
|
+
## Evidence
|
|
45
|
+
|
|
46
|
+
- Built directly from the technique that pinned the real hot-loop: after `sample`,
|
|
47
|
+
lsof, grep, and log-diffing all failed, SIGUSR1 + a CDP CPU profile showed 30.8%
|
|
48
|
+
`readFileUtf8` + 7% `listSessions` (StateManager) — the JS frame nothing else
|
|
49
|
+
could see — leading to the cache fix.
|
|
50
|
+
- Live end-to-end smoke: profiling a spawned CPU-busy node correctly reported the
|
|
51
|
+
hot frame (`listOnTimeout`, where the busy loop ran).
|
|
52
|
+
- Tests: `tests/unit/devProfileNode.test.ts` — pure `aggregateHotFrames` (ranking,
|
|
53
|
+
file:line normalization, anonymous-frame fallback, empty profile, topN) +
|
|
54
|
+
`findInspectorTarget` (first-hit / none) + `runDevProfileNode` orchestration with
|
|
55
|
+
injected deps (happy path, no-pid→hottest, no-node→exit1, signal-fail→exit1,
|
|
56
|
+
no-inspector→exit1, capture-throw→exit1).
|
|
57
|
+
|
|
58
|
+
## Also in this change
|
|
59
|
+
|
|
60
|
+
Fixes a real false-negative in `scripts/docs-coverage.mjs`: a command's coverage
|
|
61
|
+
was keyed on its camelCase **file** name (`devProfileNode`), so the needle
|
|
62
|
+
`instar devProfileNode` never matched the shipped colon-command
|
|
63
|
+
`instar dev:profile-node` — every `dev:*` command (ci-failures, preflight,
|
|
64
|
+
profile-node) read as undocumented. The matcher now also tries a kebab form and
|
|
65
|
+
treats `:`/`-` as interchangeable, so colon-commands count. (The new command
|
|
66
|
+
surfaced this — friction → structural fix.)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# A stethoscope for a node process that's running hot
|
|
2
|
+
|
|
3
|
+
## The one-sentence version
|
|
4
|
+
When one of our programs is burning the CPU for no obvious reason, the usual tools can tell you *that* it's busy but not *what line of code* is doing it. This new command — `instar dev:profile-node` — presses a stethoscope to the running program and prints the exact function that's hogging the CPU.
|
|
5
|
+
|
|
6
|
+
## The problem it solves
|
|
7
|
+
Picture a car idling in the driveway but somehow guzzling gas. You pop the hood with your usual gauge — it says "engine's working hard" but won't tell you *which part*. That's macOS's built-in profiler on a Node.js program: it sees the engine block (the low-level machinery) but can't read the actual code, so the real culprit stays hidden.
|
|
8
|
+
|
|
9
|
+
This bit us for real: three of our agent servers were each burning ~50–60% CPU, bouncing them never helped, and four different tools all came back "busy, but I can't tell you why."
|
|
10
|
+
|
|
11
|
+
## The trick that worked (now a one-liner)
|
|
12
|
+
Node programs have a hidden superpower: send them a specific nudge (the SIGUSR1 signal) and they open a little diagnostic port on themselves. Connect to that port, record a few seconds of "what function is running right now," and it tells you — in plain code terms — exactly where the time goes.
|
|
13
|
+
|
|
14
|
+
That's how we finally caught it: **30% of the CPU was in `readFileUtf8`, inside `listSessions`** — a function re-reading every session file off the disk, over and over. Invisible to everything else; obvious once we looked through the right lens.
|
|
15
|
+
|
|
16
|
+
`instar dev:profile-node` bakes that whole trick into one command:
|
|
17
|
+
- Point it at a process (or let it auto-find the hottest one).
|
|
18
|
+
- It nudges the process, records ~5 seconds, and prints the hottest functions with their percentages.
|
|
19
|
+
- The biggest non-idle line is your culprit.
|
|
20
|
+
|
|
21
|
+
## What it looks like
|
|
22
|
+
```
|
|
23
|
+
Hottest JS frames (self-time) for pid 12673:
|
|
24
|
+
87.6% listOnTimeout node:internal/timers:546
|
|
25
|
+
9.9% (program)
|
|
26
|
+
2.5% (garbage collector)
|
|
27
|
+
```
|
|
28
|
+
(That's from a test process running a busy loop inside a timer — and it correctly fingered the timer callback.)
|
|
29
|
+
|
|
30
|
+
## The safety notes
|
|
31
|
+
1. The only thing it changes is opening that diagnostic port — and only on **localhost** (your own machine), only until the process next restarts. The command tells you it did this.
|
|
32
|
+
2. It **samples** the process — it never kills it, restarts it, or edits anything.
|
|
33
|
+
3. It's a developer tool, run on demand — nothing in the running system uses it.
|
|
34
|
+
|
|
35
|
+
## Why it matters
|
|
36
|
+
The hard part of fixing a mystery slowdown isn't the fix — it's *finding* the cause. This turns a clever, hard-won debugging trick into a reusable one-liner, so the next time any node process runs hot, the answer is one command away instead of an afternoon of dead ends. (Same spirit as our `dev:ci-failures` tool: when a workaround saves the day, make it permanent.)
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
**Rendered (verified HTTP 200):** https://echo.dawn-tunnel.dev/view/e5433071-bf6f-4cf0-b97a-5e16d9f7b160?sig=2c015aaeedc96ebb20d57b326f44d88eaa93dd2bcba0b4e8347a33d23f4d2b4b
|
|
41
|
+
|
|
42
|
+
*See also: the full success-story + meta-insights writeup for this run.*
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Side-Effects Review — `instar dev:profile-node`
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `dev-profile-node-tool`
|
|
4
|
+
**Date:** `2026-06-03`
|
|
5
|
+
**Author:** `echo`
|
|
6
|
+
**Second-pass reviewer:** `not required (dev-only CLI; read-only sampling; no server/runtime surface)`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
A new dev CLI command that CPU-profiles a running node process via SIGUSR1 + the
|
|
11
|
+
node inspector + a CDP CPU profile, and prints the hottest JS frames. New file
|
|
12
|
+
`src/commands/devProfileNode.ts` + a cli.ts registration + a manifest entry +
|
|
13
|
+
awareness lines. No server, route, config, or runtime-behavior change.
|
|
14
|
+
|
|
15
|
+
## Decision-point inventory
|
|
16
|
+
|
|
17
|
+
1. **pid resolution** — explicit `[pid]` digits win; otherwise auto-pick the
|
|
18
|
+
hottest `node` process (`ps -Aceo pcpu,pid,comm`). No node found → exit 1.
|
|
19
|
+
2. **Inspector discovery** — probe ports 9229–9235 (node's default + increments
|
|
20
|
+
when the default is taken); first target with a `webSocketDebuggerUrl` wins;
|
|
21
|
+
none → exit 1 with a clear message.
|
|
22
|
+
3. **Profile aggregation** — `aggregateHotFrames` sums per-node `hitCount` by call
|
|
23
|
+
frame, normalizes the path to `dist/…`/`src/…`, +1 to the 0-based line, and
|
|
24
|
+
returns top-N by self-time. Idle/native frames are kept on purpose (seeing
|
|
25
|
+
"48% idle, 30% readFileUtf8" is the signal).
|
|
26
|
+
4. **Exit codes** — 0 on a successful print (even "no samples"); 1 only on an
|
|
27
|
+
operational failure (no node, signal failed, no inspector, capture threw).
|
|
28
|
+
|
|
29
|
+
## 1. The one real side effect — SIGUSR1 opens the inspector
|
|
30
|
+
|
|
31
|
+
SIGUSR1 tells node to start its inspector on `127.0.0.1` (localhost-only) for the
|
|
32
|
+
life of the process. Implications, all bounded:
|
|
33
|
+
- **Localhost-only** — the inspector binds 127.0.0.1, not a public interface; it is
|
|
34
|
+
not remotely reachable.
|
|
35
|
+
- **Transient** — it closes when the process restarts; it does not persist.
|
|
36
|
+
- **Disclosed** — the command prints that it left the inspector open on the pid.
|
|
37
|
+
- **No state mutation** — profiling samples the process; it does not change its
|
|
38
|
+
behavior, memory, or files.
|
|
39
|
+
|
|
40
|
+
This is the same standard procedure node documents for live debugging; it is the
|
|
41
|
+
*only* way to symbolicate JS frames on a running process. The benefit (pinning a
|
|
42
|
+
hot loop that no other tool can see) outweighs a transient localhost debug port on
|
|
43
|
+
a dev box. Operators who object can simply not run the command.
|
|
44
|
+
|
|
45
|
+
## 2. Over/under-block
|
|
46
|
+
|
|
47
|
+
Not applicable — it gates nothing and changes no behavior. A wrong pid or a
|
|
48
|
+
non-node target fails cleanly with exit 1; it never acts on the target beyond
|
|
49
|
+
SIGUSR1 + reading the profile.
|
|
50
|
+
|
|
51
|
+
## 3. Blast radius
|
|
52
|
+
|
|
53
|
+
Zero runtime surface. The command runs only when a contributor/agent invokes it.
|
|
54
|
+
No server code path imports it; it is a leaf CLI command behind a dynamic import
|
|
55
|
+
(like `dev:ci-failures`). The `ws` dependency is already present (`^8.19.0`).
|
|
56
|
+
|
|
57
|
+
## 4. Migration parity
|
|
58
|
+
|
|
59
|
+
CLAUDE.md template (`generateClaudeMd`) + `site/.../cli.md` + the builtin-manifest
|
|
60
|
+
(`cli:dev-profile-node`) are updated in lockstep so the awareness + discoverability
|
|
61
|
+
checks stay green. No config/hook/skill migration — it is a pure CLI addition
|
|
62
|
+
(same shape as `dev:ci-failures`, which needed none).
|
|
63
|
+
|
|
64
|
+
## 5. Testability
|
|
65
|
+
|
|
66
|
+
`aggregateHotFrames` and `findInspectorTarget` are pure/dep-injected; the
|
|
67
|
+
SIGUSR1 / inspector-http / ws-CDP boundary is behind `ProfileNodeDeps` so the
|
|
68
|
+
orchestration is unit-tested without a real process. A live end-to-end smoke
|
|
69
|
+
(profiling a spawned busy node) confirmed the real path.
|
|
70
|
+
|
|
71
|
+
## 6. What it does NOT do
|
|
72
|
+
|
|
73
|
+
- Does not kill, restart, or modify the target (only SIGUSR1 + sample).
|
|
74
|
+
- Does not run as part of the server or any job — invoked on demand only.
|
|
75
|
+
- Does not expose a remote/public debug port (inspector is localhost).
|
|
76
|
+
|
|
77
|
+
## 7. Collateral fix — docs-coverage matcher
|
|
78
|
+
|
|
79
|
+
Adding the command surfaced a false-negative in `scripts/docs-coverage.mjs`: it
|
|
80
|
+
matched a command by its camelCase file name, so colon-commands (`dev:*`) never
|
|
81
|
+
matched their documented form and read as undocumented (the floor was sitting
|
|
82
|
+
right at the limit, so the new command tipped it). The matcher now also tries a
|
|
83
|
+
kebab form and normalizes `:`↔`-`. Audit-only script (the weekly docs-coverage
|
|
84
|
+
report); no runtime/behavior effect. It re-classifies the three `dev:*` commands
|
|
85
|
+
as documented, which is correct (they ARE in cli.md + the CLAUDE.md template).
|