perf(runtime): cache the derived runtime key

`getRuntimeKey` keys caches, stores, and persisted state across the whole UI,
so it runs on store reads, event handling, and render paths. Until the runtime
endpoint is explicitly initialised, every call re-derived the key by trimming
two injected globals and constructing three URL objects.

In a streaming capture this made `readInjectedLocalOrigin` the single most
expensive application function: 315 ms of self time, 12% of all main-thread
busy time. After the change it does not appear in the profile at all, and the
same capture went from two long tasks to none, with the longest task dropping
from 210 ms to 47 ms.

The key depends only on the active API base URL and two injected globals, and
`switchRuntimeEndpoint` writes the injected API base URL at runtime, so the
cache is validated against the raw untrimmed values rather than memoised
outright. That comparison allocates nothing and still recomputes as soon as any
input changes. Tests cover both directions, including an operation-count
assertion that repeated calls construct no URLs.

The streaming profiler also reports output-normalised metrics, because response
length varies between runs and makes per-second totals incomparable.
This commit is contained in:
Bohdan Triapitsyn
2026-08-03 15:31:22 +03:00
parent 0d603649dc
commit 107fe45248
3 changed files with 139 additions and 3 deletions
+16 -1
View File
@@ -187,6 +187,9 @@ const REPORTED_METRICS = [
{ key: "layoutsPerSecond", label: "Layouts/sec", unit: "", lowerIsBetter: true },
{ key: "framesPerSecond", label: "Animation frames/sec", unit: "", lowerIsBetter: false },
{ key: "streamSeconds", label: "Stream duration", unit: "s", lowerIsBetter: true },
{ key: "renderedCharacters", label: "Rendered characters", unit: "", lowerIsBetter: false },
{ key: "busyMsPerKilochar", label: "Busy per 1k chars", unit: "ms", lowerIsBetter: true },
{ key: "recalcStylePerKilochar", label: "Style recalcs per 1k", unit: "", lowerIsBetter: true },
{ key: "nodeGrowth", label: "DOM node growth", unit: "", lowerIsBetter: true },
{ key: "listenerGrowth", label: "Listener growth", unit: "", lowerIsBetter: true },
{ key: "heapGrowthMbPerSecond", label: "Heap growth", unit: "MB/s", lowerIsBetter: true },
@@ -429,6 +432,8 @@ const main = async () => {
.some((entry) => entry.metric.startsWith("ui.message_list") && entry.count > 0)
const renderedStream = renderedAfter.messages > renderedBefore.messages && messageListRendered
const renderedCharacterGrowth = renderedAfter.characters - renderedBefore.characters
const tasks = summarizeLongTasks(traceEvents)
const delta = (name) => Number(after[name] ?? 0) - Number(before[name] ?? 0)
const perSecond = (name) => round(delta(name) / elapsedSeconds)
@@ -448,7 +453,7 @@ const main = async () => {
renderedStream,
renderedMessagesBefore: renderedBefore.messages,
renderedMessagesAfter: renderedAfter.messages,
renderedCharacterGrowth: renderedAfter.characters - renderedBefore.characters,
renderedCharacterGrowth,
traceComplete,
disposableSession: !options.keepSession && !options.session,
metrics: {
@@ -458,6 +463,16 @@ const main = async () => {
recalcStylePerSecond: perSecond("RecalcStyleCount"),
layoutsPerSecond: perSecond("LayoutCount"),
framesPerSecond: round(Number(probe?.counters?.rafScheduled ?? 0) / elapsedSeconds),
// Response length varies between runs even for an identical prompt, so
// per-second and total figures are not comparable across captures.
// Normalising by rendered output is what makes two runs contrastable.
renderedCharacters: renderedCharacterGrowth,
busyMsPerKilochar: renderedCharacterGrowth > 0
? round((delta("TaskDuration") * 1000) / (renderedCharacterGrowth / 1000))
: 0,
recalcStylePerKilochar: renderedCharacterGrowth > 0
? round(delta("RecalcStyleCount") / (renderedCharacterGrowth / 1000))
: 0,
streamSeconds,
recordedSeconds: round(elapsedSeconds),
nodeStart: Number(before.Nodes ?? 0),