497 lines
22 KiB
TypeScript
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;
|
|
}
|
|
`;
|
|
}
|