diff --git a/.changeset/trace-ids-math-random.md b/.changeset/trace-ids-math-random.md new file mode 100644 index 0000000000..6add5c975c --- /dev/null +++ b/.changeset/trace-ids-math-random.md @@ -0,0 +1,5 @@ +--- +"@solidjs/web": patch +--- + +Server trace and span ids (the W3C `traceparent` a render mints when the request carries none) are drawn from `Math.random` instead of `crypto.getRandomValues`. The ids need to be unique and well spread, not unguessable, which is also OpenTelemetry's default; the two `getRandomValues` calls cost about 10 µs per streamed render on Node, around 14% of the CPU between a request and its first chunk. diff --git a/packages/web/src/trace.ts b/packages/web/src/trace.ts index ec0f5e57ea..49799476cd 100644 --- a/packages/web/src/trace.ts +++ b/packages/web/src/trace.ts @@ -169,13 +169,17 @@ function parseTraceparent( return { traceId, parentId, sampled: (parseInt(flags, 16) & 1) === 1 }; } +// Trace and span ids have to be unique and well spread, not unguessable — +// W3C Trace Context asks for randomness, and OpenTelemetry's default +// IdGenerator draws them from Math.random. `crypto.getRandomValues` went +// through Node's RandomBytesJob on every render and cost ~10 µs of a +// streamed shell's ~73 µs of CPU (2026-10-16 profile); four Math.random +// words cost under a microsecond. function randomHex(bytes: number): string { - const buffer = new Uint8Array(bytes); - crypto.getRandomValues(buffer); let out = ""; - for (let i = 0; i < buffer.length; i++) - out += (buffer[i] < 16 ? "0" : "") + buffer[i].toString(16); - return out; + for (let i = 0; i < bytes; i += 4) + out += ((Math.random() * 0x100000000) >>> 0).toString(16).padStart(8, "0"); + return out.length === bytes * 2 ? out : out.slice(0, bytes * 2); } function randomTraceId(): string {