anthropics / anthropics/claude-agent-sdk-typescript

libc probe calls process.report.getReport() per query(), blocking the event loop 0.5–5s on Linux

Aperta
#391 1 commento 0 reazioni 0 assegnatari Vedi su GitHub
Lingua principale
Shell
Stelle
1.8k
Fork
226
Metriche di merge delle PR
Nessuna PR unita negli ultimi 30g

Descrizione

## Summary

On Linux the SDK detects glibc-vs-musl by calling `process.report.getReport()` (to read `header.glibcVersionRuntime`) so it can pick the correct bundled `claude` binary. That probe is wired as a **default parameter of the binary-resolver**, so it runs **once per `query()`** — i.e. on *every* prompt/turn, not once per process.

`process.report.getReport()` is expensive: it synchronously walks the entire heap, walks every libuv handle (each open socket triggers `getpeername`/`getsockname`), and enumerates **all** network interfaces. On a busy Linux container this **blocks the single Node event-loop thread for 0.5–5 s per `query()`**, stalling every other in-flight request on that process (in our case concurrent SSE streams plus k8s liveness/readiness and `/metrics`).

It reproduces only on Linux — `getReport()` is Linux-gated in the probe, so macOS/Windows never pay it and it's easy to miss in local dev.

## Environment

- `@anthropic-ai/claude-agent-sdk` **0.3.198** (confirmed still present in **0.3.220** — the probe is unchanged)
- Node.js 20.x (glibc Linux container, multi-tenant pod)
- Long-lived server process issuing many `query()` calls

## Where it is

In the bundled `sdk.mjs` the probe is (minified) roughly:

```js
function D6() { // libc probe
if (process.platform !== "linux") return false;
let e = typeof process.report?.getReport === "function"
? process.report.getReport() // <-- expensive, synchronous, blocks the loop
: null;
return e != null && e.header?.glibcVersionRuntime === void 0; // true => musl
}

function cI(e, t = process.platform, r = process.arch, o = existsSync, n = D6()) {
// ^ D6() as a DEFAULT PARAM => re-evaluated on every call that omits `n`
// ...picks `${pkg}-linux-${arch}-musl` vs `${pkg}-linux-${arch}`
}
```

Because `n = D6()` is a default parameter, the resolver re-probes on each invocation. This is the resolver added to fix the wrong-binary bugs (#296 / #323 / #324) — the correctness fix is right; the problem is only that its libc probe is neither cached nor cheap, and runs on the request path per `query()`.

## Impact / evidence

On-pod CPU profile (flamegraph), stack `getReport ← ← ← query()`:

| pod state | `getReport` self-time |
|---|---|
| cold (first turn) | ~540 ms |
| warm | ~2.6 s |
| warm (later) | ~5.3 s |

Variance tracks heap size, number of libuv handles/sockets, number of network interfaces, and CPU contention. Every millisecond here is event-loop time denied to all other concurrent work in the process.

## Suggested fix

libc does not change during a process's lifetime, so the detection result should be computed **at most once per process**:

1. **Memoize** the libc-detection result (module-level cache) instead of re-probing per `query()` / evaluating it as a default parameter.
2. Prefer a **cheaper** detection than a full diagnostic report — e.g. `process.report.getReport()` is far more than is needed to read one header field; consider `detect-libc`, reading `process.report.header.glibcVersionRuntime` via a network-excluded report, or an `ldd --version` fallback.
3. If a report call is kept, pass **`excludeNetwork: true`** (or set `process.report.excludeNetwork = true`) so the slowest section (network-interface enumeration) is skipped — it isn't needed for libc detection.

Any one of (1) removes the per-`query()` repetition; (1)+(3) also shrinks the single remaining call.

## Workarounds (for anyone hitting this now)

- `process.report.excludeNetwork = true` at process startup — strips the network section from the report the SDK triggers (no effect on `header.glibcVersionRuntime`, which is what the probe reads).

Guida per i contributori

Nessuna guida per i contributori indicizzata per questo repository

Valutazione

Questa issue non è ancora stata valutata.

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.