fix: correct web probe browser memory policy

This commit is contained in:
Codex
2026-06-30 13:05:24 +00:00
parent 37731f811c
commit edb3ad2d17
5 changed files with 189 additions and 52 deletions
@@ -3248,6 +3248,11 @@ function buildBrowserProcessReport(rows) {
cdpErrorCount: browserMetricNumber(cdp.errorCount),
heapUsedMb: page?.heapUsage ? bytesToMb(page.heapUsage.usedSize) : null,
heapTotalMb: page?.heapUsage ? bytesToMb(page.heapUsage.totalSize) : null,
effectiveHeapUsedMb: page?.effectiveMemory ? browserMetricNumber(page.effectiveMemory.effectiveHeapUsedMb) : null,
effectiveJsHeapUsedMb: page?.effectiveMemory ? browserMetricNumber(page.effectiveMemory.effectiveJsHeapUsedMb) : null,
heapUsedGrowthMb: page?.effectiveMemory ? browserMetricNumber(page.effectiveMemory.heapUsedGrowthMb) : null,
jsHeapUsedGrowthMb: page?.effectiveMemory ? browserMetricNumber(page.effectiveMemory.jsHeapUsedGrowthMb) : null,
baselineCapturedAt: page?.baseline?.capturedAt ?? null,
domNodes: page?.domCounters ? browserMetricNumber(page.domCounters.nodes) : null,
valuesRedacted: true,
};
@@ -3290,6 +3295,7 @@ function buildBrowserProcessReport(rows) {
growthRedMb: alertThresholds.browserRssGrowthRedMb,
growthWindowMs: alertThresholds.browserRssGrowthWindowMs,
responsivenessRedMs: alertThresholds.playwrightResponsivenessRedMs,
memoryRedPolicyScope: "per-page-effective-memory",
freezeBlockerCount: blockerEvents.length,
browserFreezePolicy,
valuesRedacted: true,
@@ -3334,6 +3340,10 @@ function compactBrowserFreezeBlockerEvent(item) {
processRssMb: numberOrNull(observed.processRssMb),
totalGrowthMb: numberOrNull(observed.totalGrowthMb),
processGrowthMb: numberOrNull(observed.processGrowthMb),
effectiveHeapUsedMb: numberOrNull(observed.effectiveHeapUsedMb),
effectiveJsHeapUsedMb: numberOrNull(observed.effectiveJsHeapUsedMb),
heapGrowthMb: numberOrNull(observed.heapGrowthMb),
jsHeapGrowthMb: numberOrNull(observed.jsHeapGrowthMb),
responsivenessLatencyMs: numberOrNull(observed.responsivenessLatencyMs),
responsivenessTimeout: observed.responsivenessTimeout === true,
cdpMetricsTimeoutCount: numberOrNull(observed.cdpMetricsTimeoutCount),
@@ -3370,6 +3380,11 @@ function compactBrowserFreezeBlockerEvent(item) {
responsivenessTimeout: page.responsivenessTimeout === true,
cdpTimeoutCount: numberOrNull(page.cdpTimeoutCount),
cdpErrorCount: numberOrNull(page.cdpErrorCount),
effectiveHeapUsedMb: numberOrNull(page.effectiveHeapUsedMb),
effectiveJsHeapUsedMb: numberOrNull(page.effectiveJsHeapUsedMb),
heapUsedGrowthMb: numberOrNull(page.heapUsedGrowthMb),
jsHeapUsedGrowthMb: numberOrNull(page.jsHeapUsedGrowthMb),
baselineCapturedAt: page.baselineCapturedAt ?? null,
valuesRedacted: true,
},
browserKill: {
@@ -3415,42 +3430,44 @@ function buildBrowserProcessFindings(report, runtimeAlerts = null) {
}
if (!summary || Number(summary.sampleCount ?? 0) <= 0) return findings;
const rootCauseSignals = browserRootCauseSignals(report, runtimeAlerts);
const maxTotalRssMb = Number(summary.maxTotalRssMb ?? 0);
const maxProcessRssMb = Number(summary.maxProcessRssMb ?? 0);
if (maxTotalRssMb >= alertThresholds.browserTotalRssRedMb || maxProcessRssMb >= alertThresholds.browserProcessRssRedMb) {
const pageEvents = Array.isArray(report.latestPageEvents) ? report.latestPageEvents : [];
const maxEffectiveHeapEvent = maxByNumber(pageEvents, (item) => Math.max(Number(item.effectiveHeapUsedMb ?? 0), Number(item.effectiveJsHeapUsedMb ?? 0)));
const maxEffectiveHeapMb = maxEffectiveHeapEvent ? Math.max(Number(maxEffectiveHeapEvent.effectiveHeapUsedMb ?? 0), Number(maxEffectiveHeapEvent.effectiveJsHeapUsedMb ?? 0)) : 0;
if (maxEffectiveHeapMb >= alertThresholds.browserProcessRssRedMb) {
findings.push({
id: "frontend-browser-memory-rss-red",
severity: "red",
summary: "Chromium RSS exceeded YAML red threshold during observation; browser memory growth is freeze evidence and must not be hidden by page refresh/fallback",
maxTotalRssMb,
maxProcessRssMb,
totalRssRedMb: alertThresholds.browserTotalRssRedMb,
summary: "Page effective memory exceeded YAML red threshold after subtracting page baseline; process RSS remains diagnostic evidence only",
maxEffectiveHeapMb,
processRssRedMb: alertThresholds.browserProcessRssRedMb,
memoryRedPolicyScope: "per-page-effective-memory",
maxEffectiveHeapEvent,
maxTotalRssSample: report.maxTotalRssSample,
maxProcessRssSample: report.maxProcessRssSample,
rootCause: "frontend_browser_process_memory_pressure",
rootCauseStatus: "confirmed-from-runner-process-rss",
rootCause: "frontend_browser_page_effective_memory_pressure",
rootCauseStatus: "confirmed-from-runner-page-effective-memory",
rootCauseConfidence: "high",
rootCauseSignals,
fallbackAllowed: false,
valuesRedacted: true,
});
}
const maxTotalRssGrowthMb = Number(summary.maxTotalRssGrowthMb ?? 0);
const maxProcessRssGrowthMb = Number(summary.maxProcessRssGrowthMb ?? 0);
if (maxTotalRssGrowthMb >= alertThresholds.browserRssGrowthRedMb || maxProcessRssGrowthMb >= alertThresholds.browserRssGrowthRedMb) {
const maxEffectiveGrowthEvent = maxByNumber(pageEvents, (item) => Math.max(Number(item.heapUsedGrowthMb ?? 0), Number(item.jsHeapUsedGrowthMb ?? 0)));
const maxEffectiveGrowthMb = maxEffectiveGrowthEvent ? Math.max(Number(maxEffectiveGrowthEvent.heapUsedGrowthMb ?? 0), Number(maxEffectiveGrowthEvent.jsHeapUsedGrowthMb ?? 0)) : 0;
if (maxEffectiveGrowthMb >= alertThresholds.browserRssGrowthRedMb) {
findings.push({
id: "frontend-browser-memory-growth-red",
severity: "red",
summary: "Chromium RSS grew beyond YAML window budget; this matches the reported freeze pattern of browser memory rapidly climbing",
maxTotalRssGrowthMb,
maxProcessRssGrowthMb,
summary: "Page effective memory grew beyond YAML window budget; this matches the reported freeze pattern without counting multi-page startup RSS",
maxEffectiveGrowthMb,
growthRedMb: alertThresholds.browserRssGrowthRedMb,
windowMs: alertThresholds.browserRssGrowthWindowMs,
memoryRedPolicyScope: "per-page-effective-memory",
maxEffectiveGrowthEvent,
maxGrowthSample: report.maxGrowthSample,
maxProcessGrowthSample: report.maxProcessGrowthSample,
rootCause: "frontend_browser_process_memory_leak_or_unbounded_render_growth",
rootCauseStatus: "confirmed-from-runner-process-rss-growth",
rootCause: "frontend_browser_page_memory_leak_or_unbounded_render_growth",
rootCauseStatus: "confirmed-from-runner-page-effective-memory-growth",
rootCauseConfidence: "high",
rootCauseSignals,
fallbackAllowed: false,
@@ -82,6 +82,7 @@ let browserProcessMonitorStop = null;
let browserProcessMonitorSeq = 0;
let browserFreezeBlocker = null;
const browserProcessHistory = [];
const browserPageRuntimeBaselines = new Map();
const browserFreezeSignalHistory = [];
const jsonlRotation = { stamp: compactFileTimestamp(startedAt), files: [] };
@@ -316,41 +317,65 @@ async function enforceBrowserFreezePolicy(sample) {
const processRssMb = Number(processSummary.maxProcessRssMb);
const totalGrowthMb = Number(growth.totalRssGrowthMb);
const processGrowthMb = Number(growth.maxProcessRssGrowthMb);
if (Number.isFinite(totalRssMb) && totalRssMb >= browserFreezePolicy.memory.totalRssBlockerMb) {
await triggerBrowserFreezeBlocker({
kind: "memory-total-rss",
rootCause: "frontend_browser_process_memory_pressure",
observed: { totalRssMb, processRssMb, totalGrowthMb, processGrowthMb, valuesRedacted: true },
threshold: { totalRssBlockerMb: browserFreezePolicy.memory.totalRssBlockerMb, valuesRedacted: true },
sample: browserProcessSampleRef(sample),
});
return;
}
if (Number.isFinite(processRssMb) && processRssMb >= browserFreezePolicy.memory.processRssBlockerMb) {
await triggerBrowserFreezeBlocker({
kind: "memory-process-rss",
rootCause: "frontend_browser_process_memory_pressure",
observed: { totalRssMb, processRssMb, totalGrowthMb, processGrowthMb, valuesRedacted: true },
threshold: { processRssBlockerMb: browserFreezePolicy.memory.processRssBlockerMb, valuesRedacted: true },
sample: browserProcessSampleRef(sample),
});
return;
}
if (
(Number.isFinite(totalGrowthMb) && totalGrowthMb >= browserFreezePolicy.memory.growthBlockerMb)
|| (Number.isFinite(processGrowthMb) && processGrowthMb >= browserFreezePolicy.memory.growthBlockerMb)
) {
await triggerBrowserFreezeBlocker({
kind: "memory-rss-growth",
rootCause: "frontend_browser_process_memory_leak_or_unbounded_render_growth",
observed: { totalRssMb, processRssMb, totalGrowthMb, processGrowthMb, valuesRedacted: true },
threshold: { growthBlockerMb: browserFreezePolicy.memory.growthBlockerMb, windowMs: browserFreezePolicy.blockerWindowMs, valuesRedacted: true },
sample: browserProcessSampleRef(sample),
});
return;
}
for (const pageMetric of Array.isArray(sample.pages) ? sample.pages : []) {
const effectiveMemory = pageMetric?.effectiveMemory && typeof pageMetric.effectiveMemory === "object" ? pageMetric.effectiveMemory : {};
const effectiveHeapUsedMb = Number(effectiveMemory.effectiveHeapUsedMb);
const effectiveJsHeapUsedMb = Number(effectiveMemory.effectiveJsHeapUsedMb);
const heapGrowthMb = Number(effectiveMemory.heapUsedGrowthMb);
const jsHeapGrowthMb = Number(effectiveMemory.jsHeapUsedGrowthMb);
if (
(Number.isFinite(effectiveHeapUsedMb) && effectiveHeapUsedMb >= browserFreezePolicy.memory.processRssBlockerMb)
|| (Number.isFinite(effectiveJsHeapUsedMb) && effectiveJsHeapUsedMb >= browserFreezePolicy.memory.processRssBlockerMb)
) {
await triggerBrowserFreezeBlocker({
kind: "memory-page-effective",
rootCause: "frontend_browser_page_effective_memory_pressure",
observed: {
pageRole: pageMetric?.pageRole ?? null,
pageId: pageMetric?.pageId ?? null,
pageEpoch: pageMetric?.pageEpoch ?? null,
totalRssMb,
processRssMb,
totalGrowthMb,
processGrowthMb,
effectiveHeapUsedMb: Number.isFinite(effectiveHeapUsedMb) ? effectiveHeapUsedMb : null,
effectiveJsHeapUsedMb: Number.isFinite(effectiveJsHeapUsedMb) ? effectiveJsHeapUsedMb : null,
baseline: pageMetric?.baseline ?? null,
valuesRedacted: true,
},
threshold: { processRssBlockerMb: browserFreezePolicy.memory.processRssBlockerMb, policyScope: "per-page-effective-memory", valuesRedacted: true },
sample: browserProcessSampleRef(sample),
page: browserPageMetricRef(pageMetric),
});
return;
}
if (
(Number.isFinite(heapGrowthMb) && heapGrowthMb >= browserFreezePolicy.memory.growthBlockerMb)
|| (Number.isFinite(jsHeapGrowthMb) && jsHeapGrowthMb >= browserFreezePolicy.memory.growthBlockerMb)
) {
await triggerBrowserFreezeBlocker({
kind: "memory-page-effective-growth",
rootCause: "frontend_browser_page_memory_leak_or_unbounded_render_growth",
observed: {
pageRole: pageMetric?.pageRole ?? null,
pageId: pageMetric?.pageId ?? null,
pageEpoch: pageMetric?.pageEpoch ?? null,
totalRssMb,
processRssMb,
totalGrowthMb,
processGrowthMb,
heapGrowthMb: Number.isFinite(heapGrowthMb) ? heapGrowthMb : null,
jsHeapGrowthMb: Number.isFinite(jsHeapGrowthMb) ? jsHeapGrowthMb : null,
baseline: pageMetric?.baseline ?? null,
valuesRedacted: true,
},
threshold: { growthBlockerMb: browserFreezePolicy.memory.growthBlockerMb, windowMs: browserFreezePolicy.blockerWindowMs, policyScope: "per-page-effective-memory", valuesRedacted: true },
sample: browserProcessSampleRef(sample),
page: browserPageMetricRef(pageMetric),
});
return;
}
const responsiveness = pageMetric?.responsiveness && typeof pageMetric.responsiveness === "object" ? pageMetric.responsiveness : {};
const responsivenessLatencyMs = Number(responsiveness.latencyMs);
if (responsiveness.timeout === true || (Number.isFinite(responsivenessLatencyMs) && responsivenessLatencyMs >= browserFreezePolicy.responsiveness.latencyBlockerMs)) {
@@ -546,6 +571,7 @@ function browserProcessSampleRef(sample) {
function browserPageMetricRef(pageMetric) {
const responsiveness = pageMetric?.responsiveness && typeof pageMetric.responsiveness === "object" ? pageMetric.responsiveness : {};
const cdp = pageMetric?.cdp && typeof pageMetric.cdp === "object" ? pageMetric.cdp : {};
const effectiveMemory = pageMetric?.effectiveMemory && typeof pageMetric.effectiveMemory === "object" ? pageMetric.effectiveMemory : {};
return {
pageRole: pageMetric?.pageRole ?? null,
pageId: pageMetric?.pageId ?? null,
@@ -555,6 +581,11 @@ function browserPageMetricRef(pageMetric) {
responsivenessTimeout: responsiveness.timeout === true,
cdpTimeoutCount: cdp.timeoutCount ?? null,
cdpErrorCount: cdp.errorCount ?? null,
effectiveHeapUsedMb: Number.isFinite(Number(effectiveMemory.effectiveHeapUsedMb)) ? Number(effectiveMemory.effectiveHeapUsedMb) : null,
effectiveJsHeapUsedMb: Number.isFinite(Number(effectiveMemory.effectiveJsHeapUsedMb)) ? Number(effectiveMemory.effectiveJsHeapUsedMb) : null,
heapUsedGrowthMb: Number.isFinite(Number(effectiveMemory.heapUsedGrowthMb)) ? Number(effectiveMemory.heapUsedGrowthMb) : null,
jsHeapUsedGrowthMb: Number.isFinite(Number(effectiveMemory.jsHeapUsedGrowthMb)) ? Number(effectiveMemory.jsHeapUsedGrowthMb) : null,
baselineCapturedAt: pageMetric?.baseline?.capturedAt ?? null,
valuesRedacted: true,
};
}
@@ -771,11 +802,72 @@ async function collectBrowserPageRuntimeMetrics(targetPage, { pageRole, targetPa
result.cdp.errorCount = 1;
} finally {
result.latencyMs = Date.now() - startedAtMs;
applyBrowserPageRuntimeBaseline(result);
if (session) await withHardTimeout(session.detach(), 1000, "CDP session detach exceeded 1000ms").catch(() => {});
}
return result;
}
function applyBrowserPageRuntimeBaseline(result) {
const key = [result.pageRole || "unknown", result.pageId || "unknown", Number.isFinite(Number(result.pageEpoch)) ? Number(result.pageEpoch) : 0].join(":");
const current = browserPageRuntimeMemorySnapshot(result);
const existing = browserPageRuntimeBaselines.get(key);
if (!existing && current) browserPageRuntimeBaselines.set(key, { ...current, capturedAt: new Date().toISOString(), source: "first-page-runtime-sample", valuesRedacted: true });
const baseline = browserPageRuntimeBaselines.get(key) || null;
result.baseline = baseline ? {
capturedAt: baseline.capturedAt,
source: baseline.source,
heapUsedMb: baseline.heapUsedMb,
jsHeapUsedMb: baseline.jsHeapUsedMb,
domNodes: baseline.domNodes,
valuesRedacted: true,
} : null;
result.effectiveMemory = browserPageEffectiveMemory(current, baseline);
}
function browserPageRuntimeMemorySnapshot(result) {
const heapUsedBytes = Number(result?.heapUsage?.usedSize);
const metrics = result?.performance?.metrics && typeof result.performance.metrics === "object" ? result.performance.metrics : {};
const jsHeapUsedBytes = Number(metrics.JSHeapUsedSize);
const domNodes = Number(result?.domCounters?.nodes ?? metrics.Nodes);
if (!Number.isFinite(heapUsedBytes) && !Number.isFinite(jsHeapUsedBytes) && !Number.isFinite(domNodes)) return null;
return {
heapUsedBytes: Number.isFinite(heapUsedBytes) ? heapUsedBytes : null,
heapUsedMb: Number.isFinite(heapUsedBytes) ? bytesToMb(heapUsedBytes) : null,
jsHeapUsedBytes: Number.isFinite(jsHeapUsedBytes) ? jsHeapUsedBytes : null,
jsHeapUsedMb: Number.isFinite(jsHeapUsedBytes) ? bytesToMb(jsHeapUsedBytes) : null,
domNodes: Number.isFinite(domNodes) ? domNodes : null,
valuesRedacted: true,
};
}
function browserPageEffectiveMemory(current, baseline) {
if (!current) return { available: false, baselineAvailable: Boolean(baseline), valuesRedacted: true };
const heapUsedGrowthBytes = numericDelta(current.heapUsedBytes, baseline?.heapUsedBytes);
const jsHeapUsedGrowthBytes = numericDelta(current.jsHeapUsedBytes, baseline?.jsHeapUsedBytes);
const domNodesGrowth = numericDelta(current.domNodes, baseline?.domNodes);
return {
available: true,
baselineAvailable: Boolean(baseline),
heapUsedMb: current.heapUsedMb,
jsHeapUsedMb: current.jsHeapUsedMb,
effectiveHeapUsedMb: Number.isFinite(heapUsedGrowthBytes) ? bytesToMb(heapUsedGrowthBytes) : current.heapUsedMb,
effectiveJsHeapUsedMb: Number.isFinite(jsHeapUsedGrowthBytes) ? bytesToMb(jsHeapUsedGrowthBytes) : current.jsHeapUsedMb,
heapUsedGrowthMb: Number.isFinite(heapUsedGrowthBytes) ? bytesToMb(heapUsedGrowthBytes) : null,
jsHeapUsedGrowthMb: Number.isFinite(jsHeapUsedGrowthBytes) ? bytesToMb(jsHeapUsedGrowthBytes) : null,
domNodes: current.domNodes,
domNodesGrowth: Number.isFinite(domNodesGrowth) ? domNodesGrowth : null,
valuesRedacted: true,
};
}
function numericDelta(current, baseline) {
const value = Number(current);
const base = Number(baseline);
if (!Number.isFinite(value) || !Number.isFinite(base)) return null;
return value - base;
}
function browserPageRuntimeMetricError(pageRole, targetPageId, pageEpoch, error) {
return {
pageRole,
@@ -1610,7 +1702,7 @@ function chromiumLaunchOptionsForProxy(proxy) {
}
function chromiumLowResourceArgs() {
return ["--disable-gpu", "--disable-gpu-compositing", "--no-sandbox"];
return [];
}
function browserProcessEnvWithoutProxy() {
@@ -395,7 +395,7 @@ function chromiumLaunchOptionsForProxy(proxy) {
}
function chromiumLowResourceArgs() {
return ["--disable-gpu", "--disable-gpu-compositing", "--no-sandbox"];
return [];
}
function browserProcessEnvWithoutProxy() {