Files
pikasTech-unidesk/scripts/src/hwlab-node-web-observe-runner-performance-source.ts
T

497 lines
22 KiB
TypeScript

// 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;
}
`;
}