// SPEC: pikasTech/unidesk#1436 WEB-PROBE performance probe. // Responsibility: Source string for page PerformanceObserver and CPU profile capture helpers. export function nodeWebObserveRunnerPerformanceSource(): string { return String.raw` async function installPagePerformanceProbe(targetPage, pageRole = "control", targetPageId = pageId) { const installer = (input) => { const win = window; const existing = win.__UNIDESK_WEB_PROBE_PERFORMANCE__; if (existing && existing.version === 1) { existing.pageRole = input.pageRole; existing.pageId = input.pageId; existing.pageEpoch = input.pageEpoch; existing.installedAgainAt = Date.now(); return { ok: true, alreadyInstalled: true, supportedTypes: existing.supportedTypes || [], valuesRedacted: true }; } const buffer = []; const supportedTypes = []; const maxBufferSize = 2000; const push = (event) => { buffer.push({ probeVersion: 1, pageRole: input.pageRole, pageId: input.pageId, pageEpoch: input.pageEpoch, url: location.href, path: location.pathname, timeOrigin: Math.round(performance.timeOrigin || 0), now: Math.round(performance.now()), observedAt: Date.now(), ...event, valuesRedacted: true }); while (buffer.length > maxBufferSize) buffer.shift(); }; const num = (value) => Number.isFinite(Number(value)) ? Number(value) : null; const text = (value, limit = 240) => String(value || "").slice(0, limit); const compactAttribution = (attribution) => Array.from(attribution || []).slice(0, 12).map((item) => ({ name: text(item?.name, 120), entryType: text(item?.entryType, 80), startTime: num(item?.startTime), duration: num(item?.duration), containerType: text(item?.containerType, 80), containerName: text(item?.containerName, 160), containerId: text(item?.containerId, 120), containerSrc: text(item?.containerSrc, 260), valuesRedacted: true })); const compactScript = (script) => ({ invoker: text(script?.invoker, 180), invokerType: text(script?.invokerType, 80), sourceURL: text(script?.sourceURL, 360), sourceFunctionName: text(script?.sourceFunctionName, 180), sourceCharPosition: num(script?.sourceCharPosition), lineNumber: num(script?.lineNumber), columnNumber: num(script?.columnNumber), startTime: num(script?.startTime), duration: num(script?.duration), executionStart: num(script?.executionStart), forcedStyleAndLayoutDuration: num(script?.forcedStyleAndLayoutDuration), pauseDuration: num(script?.pauseDuration), valuesRedacted: true }); const observe = (type, handler) => { try { const observer = new PerformanceObserver((list) => { for (const entry of list.getEntries()) { try { handler(entry); } catch (error) { push({ kind: "observer-error", observedEntryType: type, message: text(error && error.message, 240) }); } } }); observer.observe({ type, buffered: true }); supportedTypes.push(type); return observer; } catch { return null; } }; const observers = []; const longTask = observe("longtask", (entry) => push({ kind: "longtask", name: text(entry.name, 120), entryType: text(entry.entryType, 80), startTime: num(entry.startTime), duration: num(entry.duration), attribution: compactAttribution(entry.attribution), })); if (longTask) observers.push(longTask); const loaf = observe("long-animation-frame", (entry) => push({ kind: "long-animation-frame", name: text(entry.name, 120), entryType: text(entry.entryType, 80), startTime: num(entry.startTime), duration: num(entry.duration), renderStart: num(entry.renderStart), styleAndLayoutStart: num(entry.styleAndLayoutStart), blockingDuration: num(entry.blockingDuration), firstUIEventTimestamp: num(entry.firstUIEventTimestamp), scripts: Array.from(entry.scripts || []).slice(0, 24).map(compactScript), })); if (loaf) observers.push(loaf); let lastTick = performance.now(); const tickIntervalMs = Math.max(50, Number(input.eventLoopProbeIntervalMs || 250)); const eventLoopGapRedMs = Math.max(100, Number(input.eventLoopGapRedMs || 1000)); const timer = setInterval(() => { const now = performance.now(); const gap = now - lastTick - tickIntervalMs; lastTick = now; if (gap >= eventLoopGapRedMs) { push({ kind: "event-loop-gap", startTime: Math.max(0, now - gap), duration: Math.round(gap), thresholdMs: eventLoopGapRedMs }); } }, tickIntervalMs); win.__UNIDESK_WEB_PROBE_PERFORMANCE__ = { version: 1, pageRole: input.pageRole, pageId: input.pageId, pageEpoch: input.pageEpoch, installedAt: Date.now(), supportedTypes, buffer, observers, timer, drain() { const items = buffer.splice(0, buffer.length); return { ok: true, count: items.length, supportedTypes, items, valuesRedacted: true }; } }; return { ok: true, alreadyInstalled: false, supportedTypes, valuesRedacted: true }; }; await targetPage.addInitScript(installer, { pageRole, pageId: targetPageId, pageEpoch: pageRole === "observer" ? observerPageEpoch : controlPageEpoch, eventLoopGapRedMs: alertThresholds.eventLoopGapRedMs }); if (!targetPage.isClosed()) { await withHardTimeout(targetPage.evaluate(installer, { pageRole, pageId: targetPageId, pageEpoch: pageRole === "observer" ? observerPageEpoch : controlPageEpoch, eventLoopGapRedMs: alertThresholds.eventLoopGapRedMs }), 3000, "installPagePerformanceProbe evaluate exceeded 3000ms"); } } async function drainPagePerformanceEvents(targetPage, { reason, groupSeq, pageRole, targetPageId, pageEpoch }) { if (!targetPage || targetPage.isClosed()) return { ok: false, reason: "page-closed", count: 0, valuesRedacted: true }; const timeoutMs = Math.max(1000, Math.min(3000, Number(alertThresholds.playwrightResponsivenessRedMs) || 3000)); const drained = await withHardTimeout(targetPage.evaluate(() => { const probe = window.__UNIDESK_WEB_PROBE_PERFORMANCE__; if (!probe || typeof probe.drain !== "function") return { ok: false, reason: "performance-probe-not-installed", count: 0, valuesRedacted: true }; return probe.drain(); }), timeoutMs, "drainPagePerformanceEvents exceeded " + timeoutMs + "ms").catch((error) => ({ ok: false, reason: isTimeoutErrorMessage(error?.message) ? "drain-timeout" : "drain-error", error: errorSummary(error), count: 0, valuesRedacted: true })); const items = Array.isArray(drained?.items) ? drained.items : []; for (const item of items) { await appendJsonl(files.performanceEvents, eventRecord("performance-event", { reason, sampleGroupSeq: groupSeq, pageRole, pageId: targetPageId, pageEpoch: Number.isFinite(Number(pageEpoch)) ? Number(pageEpoch) : 0, performance: compactPerformanceEvent(item), valuesRedacted: true, })); } if (drained?.ok === false && drained.reason !== "performance-probe-not-installed") { await appendJsonl(files.performanceEvents, eventRecord("performance-drain-error", { reason, sampleGroupSeq: groupSeq, pageRole, pageId: targetPageId, pageEpoch: Number.isFinite(Number(pageEpoch)) ? Number(pageEpoch) : 0, drain: drained, valuesRedacted: true, })); } return { ok: drained?.ok === true, count: items.length, reason: drained?.reason ?? null, supportedTypes: Array.isArray(drained?.supportedTypes) ? drained.supportedTypes.slice(0, 8) : [], valuesRedacted: true }; } function compactPerformanceEvent(item) { const value = item && typeof item === "object" ? item : {}; const scripts = Array.isArray(value.scripts) ? value.scripts.slice(0, 24).map(compactPerformanceScript) : []; const attribution = Array.isArray(value.attribution) ? value.attribution.slice(0, 12).map(compactPerformanceAttribution) : []; return { kind: truncate(value.kind, 80), name: truncate(value.name, 160), entryType: truncate(value.entryType, 80), startTime: numberOrNull(value.startTime), duration: numberOrNull(value.duration), blockingDuration: numberOrNull(value.blockingDuration), renderStart: numberOrNull(value.renderStart), styleAndLayoutStart: numberOrNull(value.styleAndLayoutStart), firstUIEventTimestamp: numberOrNull(value.firstUIEventTimestamp), thresholdMs: numberOrNull(value.thresholdMs), timeOrigin: numberOrNull(value.timeOrigin), observedAt: numberOrNull(value.observedAt), now: numberOrNull(value.now), path: truncate(value.path, 220), url: safeUrl(value.url), scripts, attribution, valuesRedacted: true, }; } function compactPerformanceScript(script) { const value = script && typeof script === "object" ? script : {}; return { invoker: truncate(value.invoker, 180), invokerType: truncate(value.invokerType, 80), sourceURL: safeUrl(value.sourceURL), sourceFunctionName: truncate(value.sourceFunctionName, 180), sourceCharPosition: numberOrNull(value.sourceCharPosition), lineNumber: numberOrNull(value.lineNumber), columnNumber: numberOrNull(value.columnNumber), startTime: numberOrNull(value.startTime), duration: numberOrNull(value.duration), executionStart: numberOrNull(value.executionStart), forcedStyleAndLayoutDuration: numberOrNull(value.forcedStyleAndLayoutDuration), pauseDuration: numberOrNull(value.pauseDuration), valuesRedacted: true, }; } function compactPerformanceAttribution(item) { const value = item && typeof item === "object" ? item : {}; return { name: truncate(value.name, 120), entryType: truncate(value.entryType, 80), startTime: numberOrNull(value.startTime), duration: numberOrNull(value.duration), containerType: truncate(value.containerType, 80), containerName: truncate(value.containerName, 160), containerId: truncate(value.containerId, 120), containerSrc: safeUrl(value.containerSrc), valuesRedacted: true, }; } async function capturePerformanceProfile(command) { const durationMs = boundedInteger(command.durationMs, 5000, 100, 600000); const timeoutMs = Math.max(3000, Math.min(15000, Number(alertThresholds.playwrightResponsivenessRedMs) || 5000)); const captureId = safeId(command.label || command.id || ("perf-" + Date.now().toString(36))) || ("perf-" + Date.now().toString(36)); const targetPage = page && !page.isClosed() ? page : observerPage && !observerPage.isClosed() ? observerPage : null; if (!targetPage) throw new Error("performanceCapture requires an open page"); const targetPageRole = targetPage === observerPage ? "observer" : "control"; const targetPageId = targetPageRole === "observer" ? observerPageId : pageId; const targetPageEpoch = targetPageRole === "observer" ? observerPageEpoch : controlPageEpoch; const captureDir = path.join(dirs.performanceCaptures, captureId); await mkdir(captureDir, { recursive: true, mode: 0o700 }); await installPagePerformanceProbe(targetPage, targetPageRole, targetPageId) .catch((error) => appendJsonl(files.errors, eventRecord("performance-capture-probe-install-error", { commandId: command.id, pageRole: targetPageRole, pageId: targetPageId, error: errorSummary(error), valuesRedacted: true }))); const beforeDrain = await drainPagePerformanceEvents(targetPage, { reason: "performanceCapture-before", groupSeq: sampleSeq, pageRole: targetPageRole, targetPageId, pageEpoch: targetPageEpoch }) .catch((error) => ({ ok: false, error: errorSummary(error), count: 0, valuesRedacted: true })); let session = null; const startedAtMs = Date.now(); const startedAt = new Date(startedAtMs).toISOString(); let pageClockStart = null; let stopped = null; let captureError = null; try { pageClockStart = await withHardTimeout(targetPage.evaluate(() => ({ timeOrigin: Math.round(performance.timeOrigin || 0), now: Math.round(performance.now()), url: location.href, path: location.pathname, title: document.title, valuesRedacted: true })), timeoutMs, "performanceCapture page clock exceeded " + timeoutMs + "ms") .catch((error) => ({ error: errorSummary(error), valuesRedacted: true })); session = await withHardTimeout(targetPage.context().newCDPSession(targetPage), timeoutMs, "performanceCapture newCDPSession exceeded " + timeoutMs + "ms"); await withHardTimeout(session.send("Profiler.enable"), timeoutMs, "Profiler.enable exceeded " + timeoutMs + "ms"); await withHardTimeout(session.send("Profiler.start"), timeoutMs, "Profiler.start exceeded " + timeoutMs + "ms"); await appendJsonl(files.performanceEvents, eventRecord("performance-capture-started", { captureId, durationMs, pageRole: targetPageRole, pageId: targetPageId, pageEpoch: targetPageEpoch, pageClock: pageClockStart, currentUrl: pageUrl(targetPage), beforeDrain, valuesRedacted: true, })); await sleep(durationMs); stopped = await withHardTimeout(session.send("Profiler.stop"), Math.max(timeoutMs, 5000), "Profiler.stop exceeded " + Math.max(timeoutMs, 5000) + "ms"); } catch (error) { captureError = error; } finally { if (session) { await withHardTimeout(session.send("Profiler.disable"), 1000, "Profiler.disable exceeded 1000ms").catch(() => {}); await withHardTimeout(session.detach(), 1000, "performanceCapture CDP session detach exceeded 1000ms").catch(() => {}); } } const completedAtMs = Date.now(); const afterDrain = await drainPagePerformanceEvents(targetPage, { reason: "performanceCapture-after", groupSeq: sampleSeq, pageRole: targetPageRole, targetPageId, pageEpoch: targetPageEpoch }) .catch((error) => ({ ok: false, error: errorSummary(error), count: 0, valuesRedacted: true })); const summaryFile = path.join(captureDir, "summary.json"); if (captureError) { const failureKind = isTimeoutErrorMessage(captureError?.message) ? "performance-capture-cdp-timeout" : "performance-capture-failed"; const summaryPayload = { ok: false, captureId, type: "performance-cpu-profile", commandId: command.id, label: truncate(command.label || "", 200), startedAt, completedAt: new Date(completedAtMs).toISOString(), durationMs: completedAtMs - startedAtMs, requestedDurationMs: durationMs, pageRole: targetPageRole, pageId: targetPageId, pageEpoch: targetPageEpoch, pageClockStart, currentUrl: pageUrl(targetPage), beforeDrain, afterDrain, failureKind, error: errorSummary(captureError), valuesRedacted: true, }; await writeFile(summaryFile, JSON.stringify(summaryPayload, null, 2) + "\n", { mode: 0o600 }); const summaryMeta = await fileMeta(summaryFile); artifactSeq += 1; const artifact = { seq: artifactSeq, sampleSeq, ts: new Date().toISOString(), kind: "performance-cpu-profile-failed", captureId, commandId: command.id, summaryPath: summaryFile, summaryByteCount: summaryMeta.byteCount, summarySha256: summaryMeta.sha256, pageRole: targetPageRole, pageId: targetPageId, durationMs: summaryPayload.durationMs, failureKind, valuesRedacted: true, }; await appendJsonl(files.artifacts, artifact); await appendJsonl(files.performanceEvents, eventRecord("performance-capture-failed", { captureId, pageRole: targetPageRole, pageId: targetPageId, pageEpoch: targetPageEpoch, artifact, failureKind, error: summaryPayload.error, valuesRedacted: true, })); const wrapped = captureError instanceof Error ? captureError : new Error(String(captureError)); wrapped.details = summaryPayload; throw wrapped; } const profile = stopped?.profile || null; const summary = summarizeCpuProfile(profile); const profileFile = path.join(captureDir, "profile.cpuprofile"); const summaryPayload = { ok: true, captureId, type: "performance-cpu-profile", commandId: command.id, label: truncate(command.label || "", 200), startedAt, completedAt: new Date(completedAtMs).toISOString(), durationMs: completedAtMs - startedAtMs, requestedDurationMs: durationMs, pageRole: targetPageRole, pageId: targetPageId, pageEpoch: targetPageEpoch, pageClockStart, currentUrl: pageUrl(targetPage), beforeDrain, afterDrain, profile: summary, valuesRedacted: true, }; await writeFile(profileFile, JSON.stringify(profile || {}, null, 2) + "\n", { mode: 0o600 }); await writeFile(summaryFile, JSON.stringify(summaryPayload, null, 2) + "\n", { mode: 0o600 }); const [profileMeta, summaryMeta] = await Promise.all([fileMeta(profileFile), fileMeta(summaryFile)]); artifactSeq += 1; const artifact = { seq: artifactSeq, sampleSeq, ts: new Date().toISOString(), kind: "performance-cpu-profile", captureId, commandId: command.id, path: profileFile, summaryPath: summaryFile, byteCount: profileMeta.byteCount, sha256: profileMeta.sha256, summaryByteCount: summaryMeta.byteCount, summarySha256: summaryMeta.sha256, pageRole: targetPageRole, pageId: targetPageId, durationMs: summaryPayload.durationMs, topFunctions: summary.topFunctions.slice(0, 8), topStacks: summary.topStacks.slice(0, 5), valuesRedacted: true, }; await appendJsonl(files.artifacts, artifact); await appendJsonl(files.performanceEvents, eventRecord("performance-capture-completed", { captureId, pageRole: targetPageRole, pageId: targetPageId, pageEpoch: targetPageEpoch, artifact, summary: { ...summary, topFunctions: summary.topFunctions.slice(0, 12), topStacks: summary.topStacks.slice(0, 8), valuesRedacted: true }, valuesRedacted: true, })); return { ...summaryPayload, artifact, valuesRedacted: true }; } function summarizeCpuProfile(profile) { const nodes = Array.isArray(profile?.nodes) ? profile.nodes : []; const samples = Array.isArray(profile?.samples) ? profile.samples : []; const timeDeltas = Array.isArray(profile?.timeDeltas) ? profile.timeDeltas : []; const byId = new Map(); const parentById = new Map(); for (const node of nodes) { const id = Number(node?.id); if (!Number.isFinite(id)) continue; byId.set(id, node); for (const childIdRaw of Array.isArray(node.children) ? node.children : []) { const childId = Number(childIdRaw); if (Number.isFinite(childId)) parentById.set(childId, id); } } const functionRows = new Map(); const stackRows = new Map(); const addFunction = (frame, deltaMs, field) => { const compact = compactCpuCallFrame(frame); const key = cpuFrameKey(compact); const row = functionRows.get(key) || { ...compact, key, sampleCount: 0, selfTimeMs: 0, totalTimeMs: 0, valuesRedacted: true }; if (field === "selfTimeMs") row.sampleCount += 1; row[field] += deltaMs; functionRows.set(key, row); }; const nodePath = (id) => { const out = []; const seen = new Set(); let current = id; while (Number.isFinite(Number(current)) && byId.has(Number(current)) && !seen.has(Number(current)) && out.length < 64) { seen.add(Number(current)); out.push(byId.get(Number(current))); current = parentById.get(Number(current)); } return out.reverse(); }; let totalTimeMs = 0; for (let index = 0; index < samples.length; index += 1) { const nodeId = Number(samples[index]); if (!byId.has(nodeId)) continue; const deltaUs = Number(timeDeltas[index]); const deltaMs = Number.isFinite(deltaUs) && deltaUs > 0 ? deltaUs / 1000 : 0; totalTimeMs += deltaMs; const pathNodes = nodePath(nodeId); const leaf = pathNodes[pathNodes.length - 1] || byId.get(nodeId); addFunction(leaf?.callFrame, deltaMs, "selfTimeMs"); for (const item of pathNodes) addFunction(item?.callFrame, deltaMs, "totalTimeMs"); const stackFrames = pathNodes.map((item) => compactCpuCallFrame(item?.callFrame)).filter((item) => item.functionName || item.url).slice(-10); const stackKey = stackFrames.map(cpuFrameLabel).join(" <- "); if (stackKey) { const row = stackRows.get(stackKey) || { key: stackKey, sampleCount: 0, selfTimeMs: 0, leaf: stackFrames[stackFrames.length - 1] || null, frames: stackFrames, valuesRedacted: true }; row.sampleCount += 1; row.selfTimeMs += deltaMs; stackRows.set(stackKey, row); } } const rounded = (value) => Number((Number(value || 0)).toFixed(2)); const topFunctions = Array.from(functionRows.values()) .map((item) => ({ ...item, selfTimeMs: rounded(item.selfTimeMs), totalTimeMs: rounded(item.totalTimeMs), selfPercent: totalTimeMs > 0 ? Number(((item.selfTimeMs / totalTimeMs) * 100).toFixed(2)) : 0, totalPercent: totalTimeMs > 0 ? Number(((item.totalTimeMs / totalTimeMs) * 100).toFixed(2)) : 0 })) .sort((left, right) => Number(right.selfTimeMs || 0) - Number(left.selfTimeMs || 0) || Number(right.totalTimeMs || 0) - Number(left.totalTimeMs || 0)) .slice(0, 50); const topStacks = Array.from(stackRows.values()) .map((item) => ({ ...item, selfTimeMs: rounded(item.selfTimeMs), selfPercent: totalTimeMs > 0 ? Number(((item.selfTimeMs / totalTimeMs) * 100).toFixed(2)) : 0 })) .sort((left, right) => Number(right.selfTimeMs || 0) - Number(left.selfTimeMs || 0)) .slice(0, 40); return { nodeCount: nodes.length, sampleCount: samples.length, timeDeltaCount: timeDeltas.length, totalTimeMs: rounded(totalTimeMs), topFunctions, topStacks, valuesRedacted: true, }; } function compactCpuCallFrame(frame) { const value = frame && typeof frame === "object" ? frame : {}; return { functionName: truncate(value.functionName || "(anonymous)", 180), url: safeUrl(value.url || ""), scriptId: value.scriptId === undefined || value.scriptId === null ? null : String(value.scriptId).slice(0, 80), lineNumber: numberOrNull(value.lineNumber), columnNumber: numberOrNull(value.columnNumber), valuesRedacted: true, }; } function cpuFrameKey(frame) { return [frame.functionName || "(anonymous)", frame.url || frame.scriptId || "", frame.lineNumber ?? "", frame.columnNumber ?? ""].join("@"); } function cpuFrameLabel(frame) { const location = frame.url || frame.scriptId || "-"; const line = frame.lineNumber === null || frame.lineNumber === undefined ? "" : ":" + String(Number(frame.lineNumber) + 1); return String(frame.functionName || "(anonymous)") + "@" + location + line; } `; }