From edb3ad2d17107c5e695686e731b0eb4902b3549e Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 30 Jun 2026 13:05:24 +0000 Subject: [PATCH] fix: correct web probe browser memory policy --- ...6-0106050514-workbench-realtime-runtime.md | 25 +++ .../PJ2026-01060508-web-probe-sentinel.md | 3 + .../hwlab-node-web-observe-analyzer-source.ts | 51 ++++-- .../hwlab-node-web-observe-runner-source.ts | 160 ++++++++++++++---- .../src/hwlab-node-web-probe-runner-source.ts | 2 +- 5 files changed, 189 insertions(+), 52 deletions(-) diff --git a/project-management/PJ2026-01/specs/PJ2026-0106050514-workbench-realtime-runtime.md b/project-management/PJ2026-01/specs/PJ2026-0106050514-workbench-realtime-runtime.md index acff3f57..7fbb09b0 100644 --- a/project-management/PJ2026-01/specs/PJ2026-0106050514-workbench-realtime-runtime.md +++ b/project-management/PJ2026-01/specs/PJ2026-0106050514-workbench-realtime-runtime.md @@ -237,3 +237,28 @@ SPEC: PJ2026-0106050514 Workbench实时运行面 draft-2026-06-30-p0-1297-spec-f | P4 rollout smoke | PR 合并、JD01 滚动上线、web-probe smoke 和 issue closeout | smoke 通过或明确记录 blocker;父 issue 回填 PR、版本、截图/报告和 OTel 证据 | P0 未完成前,不得把 P1-P4 标记为可执行完成;P1-P4 中任何稳定语义、字段族、状态机或验收口径变化,必须先更新本 SPEC,再更新对应子 issue 和代码。 + +### 7.1 #1315 纠偏完成定义 + +#1315 不再以“降低 web-probe/Playwright 启动内存占用”作为修复方向;web-probe 的浏览器资源占用只作为观测 baseline。真正完成标准是:OpenCode 可整模块迁移的能力在 HWLAB Vue Workbench 中 100% 完成机械复制/语义重写/生产接入,且所有新增策略参数都由 YAML 或 runtime projection 控制。 + +| 编号 | 迁移模块 | OpenCode 对照能力 | HWLAB 目标接入点 | 完成口径 | +| --- | --- | --- | --- | --- | +| M01 | ErrorRuntime | typed error、diagnostic envelope、用户可见错误 | `web/hwlab-cloud-web/src/utils/workbench-error-runtime.ts`、Workbench store、消息面板 | REST/SSE/storage/health/projection 错误统一归一化并暴露 root cause/recovery action | +| M02 | RefreshQueueRuntime | queue、single-flight、cooldown、请求风暴抑制 | `workbench-refresh-runtime.ts`、`src/stores/workbench.ts` | session/message/turn/trace/health refresh 进入同一 keyed queue | +| M03 | SseTransportRuntime | EventSource lifecycle、cursor、gap 分类 | `workbench-stream-transport.ts`、Workbench store | SSE reconnect/gap-fill 不绕过 queue,不触发无界 REST fan-out | +| M04 | ScopedKeyRuntime | scope key、cache key、trace key | `workbench-key.ts`、store/runtime modules | session/trace/message/part 的 key 生成规则生产路径共用 | +| M05 | TimelineRuntime | 稳定 row identity、增量替换、跨 session 隔离 | server-state reducer、timeline composables、Vue message rendering | 刷新和第二轮消息不产生堆叠、重复或串线 | +| M06 | StorageRuntime | safe storage、namespace、quota/security 降级 | `workbench-storage-runtime.ts`、store、session rail | localStorage/sessionStorage 访问不再直接散落在组件和 store | +| M07 | ScrollRuntime | bottom follow、anchor、pane 内滚动 | `useWorkbenchScrollRuntime.ts`、Conversation/Trace panels | 滚动状态由 reusable runtime 管理,页面不靠 document 无限拉长 | +| M08 | HealthRuntime | health probe cache、诊断投影 | `workbench-health-runtime.ts`、store refresh path | 健康探测不放大请求风暴,并输出可读诊断 | +| M09 | SessionCacheRuntime | bounded session cache、retain active scope | `workbench-session-cache.ts`、store reducer 后处理 | session cache 可裁剪,不误删 active/current trace | +| M10 | RealtimeEventCoalescing | 事件合并、sequence/order 可见 | `workbench-realtime-runtime.ts`、SSE handler | burst event 先合并再进入 store/reducer | +| M11 | OTel/RUM Evidence | root cause、request family、scoped key | runtime diagnostic fields、web-probe report | monitor 再次报红时能直接看到根因摘要 | +| M12 | SentinelFreezeRuntime | no fallback、no auto refresh、kill browser | UniDesk web-probe observe runner/analyzer | 页面无响应或页面有效内存超过 YAML policy 时 blocker red | +| M13 | BrowserMemoryBaseline | per-page baseline、effective memory | web-probe page runtime metrics | 多页面启动 RSS 不作为总额阻塞;阻塞按每页 baseline 后的有效内存判断 | +| M14 | ConfigProjection | YAML/source-of-truth 控参 | YAML policy parser、runtime projection | SPEC/源码不新增硬编码阈值、超时、并发、缓存容量 | +| M15 | SmokeRuntime | 原入口 smoke、第二轮消息 | `web-probe observe` JD01 `/workbench` | smoke 覆盖 `hi` 和 `看看有什么hwpod可用`,且页面保持可操作 | +| M16 | RolloutEvidence | PR、rollout、monitor/report evidence | GitHub issue/PR、JD01 rollout、monitor report | #1315 回填 PR、commit、rollout、web-probe report 和剩余风险 | + +上述表格的“完成”必须同时满足源码存在、生产路径接入、最小验证通过和 issue 证据回填;只复制模块、只保留测试、只记录 TODO 或只调整探针资源参数均不得计为完成。 diff --git a/project-management/PJ2026-01/specs/PJ2026-01060508-web-probe-sentinel.md b/project-management/PJ2026-01/specs/PJ2026-01060508-web-probe-sentinel.md index 58f8e6da..d87292f5 100644 --- a/project-management/PJ2026-01/specs/PJ2026-01060508-web-probe-sentinel.md +++ b/project-management/PJ2026-01/specs/PJ2026-01060508-web-probe-sentinel.md @@ -64,6 +64,7 @@ Web哨兵必须遵循 UniDesk YAML-first ops。目标 node/lane、public origin - 多实例 Web 哨兵不得用一个 Pod、一个 PVC 或一个 SQLite index 伪装隔离。共享 dashboard domain 可以按 route prefix 暴露,但 runtime Deployment、Service、PVC、SQLite、GitOps path、Argo Application、metrics label 和 report index 必须按 sentinel id 独立。 - SQLite index、dashboard、metrics 和 maintenance 状态不得替代 `samples.jsonl`、`control.jsonl`、network/artifact JSONL 或 `analysis/report.json` 成为探针事实源。 - Dashboard 不负责修复 Workbench projection、trace timing、runner/envreuse 或 git mirror 慢路径;它只把 observe/analyze 已采集事实组织成可读的值守和分析入口。 +- Web 哨兵不得通过降低 Playwright/Chromium 启动资源、禁用浏览器能力或自动刷新页面来规避 Workbench 卡死和内存膨胀;浏览器进程 RSS 是诊断证据,阻塞判定必须优先使用页面级 baseline 后的有效内存、CDP/Playwright 响应性和 YAML policy。 ## 3. 术语表 @@ -94,6 +95,8 @@ Web哨兵必须遵循 UniDesk YAML-first ops。目标 node/lane、public origin | monitor-web | 独立于 sentinel-runner 的 Vue 3 + TypeScript + Vite 展示和聚合层,承载 `monitor.pikapython.com` root、多哨兵总览、趋势曲线、时间线、单哨兵详情和受控截图验收。 | | cadence freshness | 根据 YAML cadence、scheduler heartbeat、latest run age、active/planned run 和 analyzed report 更新时间计算的运行新鲜度;它默认是非阻塞值守告警,只有真正导致 run/report 不产生或业务链路不可用时才升级为 blocker。 | | env reuse | CI/CD 复用既有 env image、依赖缓存、BuildKit 层和未受影响服务的发布产物,以避免无关重建;小范围变更应在 status/closeout 中暴露 build/reuse 摘要。 | +| 页面内存 baseline | web-probe 在每个 Playwright page/page epoch 上采集的初始浏览器运行指标;浏览器启动、空页和未加载目标 URL 前的基础成本只作为 baseline,不计入该页有效内存预算。 | +| 页面有效内存 | 同一 page/page epoch 当前 JS heap、DOM counter 或 runtime memory 指标扣除页面内存 baseline 后的增长量;是否 blocker 由 YAML browserFreezePolicy 控制。 | ## 4. 系统边界和接口 diff --git a/scripts/src/hwlab-node-web-observe-analyzer-source.ts b/scripts/src/hwlab-node-web-observe-analyzer-source.ts index 4c2ffcec..9b24df4b 100644 --- a/scripts/src/hwlab-node-web-observe-analyzer-source.ts +++ b/scripts/src/hwlab-node-web-observe-analyzer-source.ts @@ -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, diff --git a/scripts/src/hwlab-node-web-observe-runner-source.ts b/scripts/src/hwlab-node-web-observe-runner-source.ts index 55c4c40e..0ff78c81 100644 --- a/scripts/src/hwlab-node-web-observe-runner-source.ts +++ b/scripts/src/hwlab-node-web-observe-runner-source.ts @@ -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() { diff --git a/scripts/src/hwlab-node-web-probe-runner-source.ts b/scripts/src/hwlab-node-web-probe-runner-source.ts index 1da08752..bff5110c 100644 --- a/scripts/src/hwlab-node-web-probe-runner-source.ts +++ b/scripts/src/hwlab-node-web-probe-runner-source.ts @@ -395,7 +395,7 @@ function chromiumLaunchOptionsForProxy(proxy) { } function chromiumLowResourceArgs() { - return ["--disable-gpu", "--disable-gpu-compositing", "--no-sandbox"]; + return []; } function browserProcessEnvWithoutProxy() {