feat: add web probe performance hotspot analysis
This commit is contained in:
@@ -0,0 +1,436 @@
|
||||
// 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;
|
||||
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");
|
||||
} 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 profile = stopped?.profile || null;
|
||||
const summary = summarizeCpuProfile(profile);
|
||||
const profileFile = path.join(captureDir, "profile.cpuprofile");
|
||||
const summaryFile = path.join(captureDir, "summary.json");
|
||||
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;
|
||||
}
|
||||
`;
|
||||
}
|
||||
Reference in New Issue
Block a user