Skip to content

fix(renderer): deliver ResizeObserver updates through bounded rendering turns - #1593

Open
lanyue-llk wants to merge 1 commit into
mainfrom
codex/webfetch-observers
Open

lanyue-llk wants to merge 1 commit into
mainfrom
codex/webfetch-observers

Conversation

@lanyue-llk

@lanyue-llk lanyue-llk commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor
PR #1593 at a glance

Problem

Moli did not run layout-backed ResizeObserver delivery as part of document rendering updates. Pages that materialize content from measured size changes could therefore return an incomplete DOM; in the Browser Eval MUI grid case, the four required rows were absent.

Adding real-layout delivery also requires browser-observable scheduling rules: gather every observer before invoking callbacks, preserve microtask and IndexedDB transaction ordering, bound same-depth feedback, and carry finite deeper work into a later rendering turn.

Change

ResizeObserver delivery now runs through the existing exact-document rendering-update queue. A task exits before layout when its document has no observation targets. When targets exist, each observation round uses a fresh layout, freezes all active records before callbacks, publishes the resulting geometry, and runs the callback microtask checkpoint at the rendering-update boundary.

Same-depth feedback reports the required loop error and retries after a deduplicated 16 ms internal browser task. Deeper finite work retains its depth across a named 32-round fairness boundary. Fresh shallower work resets the pending minimum depth. Legacy delivery keeps the surrounding checkpoint in control.

The implementation stays inside the existing layout, exact-document task, and timer owners. It adds no polling, site rule, benchmark case branch, or speculative layout cache.

Reproduce and verify

The focused tests cover:

  • a rendering task with no observation target exiting without a layout pass;
  • observe, cancel before the real-layout turn, then observe a different element;
  • an IndexedDB transaction remaining usable in a Promise microtask after both legacy and real-layout delivery;
  • finite same-depth work reaching the next paced opportunity and stopping after disconnect;
  • sustained feedback yielding instead of making a rendering-source hot loop;
  • fresh depth-zero work replacing a pending deep continuation;
  • the exact 32-round boundary and finite 40-level continuation;
  • all observers being gathered before callbacks, including callback-time observe() and disconnect() changes.
cargo nextest run --locked -p moli-renderer-v8 --lib \
  -E 'test(resize_observer_) | test(indexed_db_transaction_stays_active_past_legacy_resize_observer_microtasks) | test(indexed_db_transaction_created_by_real_resize_observer_survives_callback_microtasks)' \
  --no-fail-fast

Browser Eval reproduction

dynamic-104-mui-data-grid is the frozen Browser Eval case that exposed the missing virtualized rows. Build this PR, bind that executable and SHA-256 as moli-candidate, then run the case through the certified native-fetch entry point:

uv run --frozen python scripts/assemble_webfetch_dataset.py \
  --assembly evaluation/webfetch-seven-family-430/dataset-assembly.json \
  --profile /absolute/path/to/report-profile.json \
  --output /tmp/webfetch-430.json

uv run --frozen python scripts/run_certified_benchmark.py \
  --dataset /tmp/webfetch-430.json \
  --output /tmp/moli-1593-browser-eval \
  --work-root /tmp/moli-1593-browser-eval-work \
  --providers moli-candidate \
  --case-ids dynamic-104-mui-data-grid \
  --provider-config /absolute/path/to/moli-candidate-providers.json \
  --workers 1 \
  --retrieval-mode native

Moli 1.1.15 returned none of the four required grid records. Repository fixtures isolate the ResizeObserver behavior; the Browser Eval command checks the complete native-fetch and scoring path against the frozen task.

Dependency

Independent PR targeting main.

@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Spider Bench A/B

✅ All benchmark browser service runs completed; results are informational.

Public HEAD: 184 / 240 rows; 38 / 48 sites produced rows; 10 unexpected empty sites.

✅ Deterministic fixture contract is clean.

Common ancestor 14d3c08cc8bc → HEAD 9e83f5cb3b84; benchmark order: head-first; workflow: success.

Public 48-site run · informational

Metric Base HEAD Δ
Run status success success —
Extracted rows / contract 184 / 240 184 / 240 0 (0.00%)
Contract fill 76.67% 76.67% 0.00 pp (0.00%)
Sites with rows 38 / 48 38 / 48 0 (0.00%)
Sites meeting row contract 36 / 48 36 / 48 0 (0.00%)
Unexpected empty sites 10 10 0 (0.00%)
Partial-row sites 2 2 0 (0.00%)
Site outcomes extracted: 37; HTTP error + rows: 1; empty: 7; HTTP error + empty: 1; snapshot error + empty: 2 extracted: 37; HTTP error + rows: 1; empty: 7; HTTP error + empty: 1; snapshot error + empty: 2 —
Observed duration 48.25 s 37.74 s -10.51 s (-21.79%)
Estimated CPU 41.06 s 36.61 s -4.45 s (-10.84%)
Peak PSS 248.21 MiB 275.39 MiB +27.18 MiB (+10.95%)
Peak RSS 251.78 MiB 278.95 MiB +27.18 MiB (+10.79%)
Peak threads 26 27 +1 (+3.85%)
Mismatched sites 0 0 0
Timeout with items 0 0 0
Category rows Base HEAD Δ
news 35 / 40 35 / 40 0
stocks 20 / 40 20 / 40 0
tech 35 / 40 35 / 40 0
sports 27 / 40 27 / 40 0
games 37 / 40 37 / 40 0
life 30 / 40 30 / 40 0

Public-site content, timing, and memory are noisy. This single A/B run reports evidence only; site outcome counts explain missing rows without treating them as a deterministic regression.

CPU and memory timelines

These are bounded 40-point views of the same complete process-tree samples used by the HTML report. CPU uses 100% per occupied logical core; memory lines are RSS first and PSS second when complete PSS samples are available.

Base

xychart-beta
    title "Base CPU"
    x-axis "Elapsed seconds" [1.536, 2.537, 4.04, 5.54, 6.541, 8.041, 9.541, 11.043, 12.045, 13.547, 15.049, 16.051, 16.551, 17.551, 19.052, 20.054, 21.556, 23.057, 24.557, 25.558, 26.059, 27.061, 28.562, 29.564, 31.067, 32.569, 33.571, 35.072, 36.574, 37.575, 39.078, 40.581, 42.082, 43.083, 44.585, 45.587, 46.088, 47.089]
    y-axis "CPU percent" 0 --> 200
    line [125.83, 113.87, 133.75, 123.82, 103.84, 105.99, 106.02, 125.97, 31.93, 129.72, 13.98, 3.99, 0, 0, 5.99, 2, 21.96, 65.9, 94.01, 127.78, 99.82, 101.78, 113.99, 73.86, 105.79, 90.01, 75.84, 2, 119.8, 121.83, 9.98, 99.89, 101.88, 99.88, 123.83, 155.84, 41.91, 129.97]
Loading
xychart-beta
    title "Base memory: RSS then PSS"
    x-axis "Elapsed seconds" [0.035, 1.536, 2.537, 4.04, 5.54, 6.541, 8.041, 9.541, 11.043, 12.045, 13.547, 15.049, 16.051, 16.551, 17.551, 19.052, 20.054, 21.556, 23.057, 24.557, 25.558, 26.059, 27.061, 28.562, 29.564, 31.067, 32.569, 33.571, 35.072, 36.574, 37.575, 39.078, 40.581, 42.082, 43.083, 44.585, 45.587, 46.088, 47.089, 48.252]
    y-axis "Memory MiB" 0 --> 500
    line [13.14, 98.36, 101.32, 135.82, 140.85, 140.59, 141.31, 153.6, 148.01, 120.26, 161.07, 116, 120.04, 119.8, 118.7, 118.7, 118.61, 125.37, 147.8, 144.04, 183.09, 251.78, 185.32, 211.06, 144.09, 149.27, 132.64, 149.58, 140.64, 198.91, 154.58, 128.32, 173.19, 177.63, 177.73, 163.33, 144.52, 141.13, 157.56, 0]
    line [9.6, 94.67, 97.6, 132.15, 136.87, 136.9, 137.57, 149.95, 144.3, 116.51, 157.23, 111.91, 116.2, 115.96, 114.86, 114.86, 114.77, 121.53, 143.96, 140.2, 179.31, 248.21, 181.51, 207.15, 140.24, 145.57, 128.86, 145.81, 136.93, 195.26, 150.89, 124.69, 169.55, 173.99, 174.09, 159.69, 140.85, 137.55, 153.36, 0]
Loading

HEAD

xychart-beta
    title "HEAD CPU"
    x-axis "Elapsed seconds" [0.536, 1.036, 2.037, 3.039, 4.04, 5.042, 5.542, 6.543, 7.543, 8.543, 9.544, 10.545, 11.548, 12.549, 13.551, 15.054, 16.056, 17.058, 18.059, 19.061, 20.062, 21.063, 22.063, 23.065, 24.566, 25.566, 26.568, 27.57, 28.571, 29.572, 30.572, 31.574, 32.574, 34.077, 35.077, 36.078, 37.08, 37.58]
    y-axis "CPU percent" 0 --> 200
    line [6, 27.98, 103.86, 109.84, 145.69, 139.89, 133.96, 107.81, 128.02, 106.02, 109.89, 127.75, 77.85, 83.86, 85.83, 45.91, 107.82, 59.87, 113.74, 127.76, 114.02, 101.89, 113.96, 57.86, 108.1, 97.93, 103.8, 103.88, 21.98, 147.73, 89.97, 9.98, 8, 107.93, 103.95, 109.94, 133.67, 157.79]
Loading
xychart-beta
    title "HEAD memory: RSS then PSS"
    x-axis "Elapsed seconds" [0.036, 0.536, 1.036, 2.037, 3.039, 4.04, 5.042, 5.542, 6.543, 7.543, 8.543, 9.544, 10.545, 11.548, 12.549, 13.551, 15.054, 16.056, 17.058, 18.059, 19.061, 20.062, 21.063, 22.063, 23.065, 24.566, 25.566, 26.568, 27.57, 28.571, 29.572, 30.572, 31.574, 32.574, 34.077, 35.077, 36.078, 37.08, 37.58, 37.737]
    y-axis "Memory MiB" 0 --> 500
    line [13.17, 54.75, 83.42, 103.18, 146.64, 136.3, 155.45, 121.73, 129.57, 133.61, 158.87, 150.69, 152.3, 132.56, 127.24, 156.42, 119.84, 133.16, 152.77, 139.24, 159, 278.95, 184.59, 210.19, 142.36, 146.93, 131.12, 138.23, 134.91, 139.35, 143.16, 148.77, 141.11, 126.55, 175.39, 156.54, 143.96, 167.12, 168, 0]
    line [9.76, 51.18, 79.83, 99.55, 143.01, 133.24, 152.24, 118.07, 126.33, 130.16, 155.22, 147.07, 148.63, 128.86, 123.33, 152.72, 116.29, 129.5, 149, 137.25, 155.32, 275.39, 180.89, 206.56, 138.79, 143.27, 127.52, 134.69, 131.21, 135.64, 133.23, 145.13, 137.46, 122.89, 172.23, 151.64, 140.45, 163.42, 164.57, 0]
Loading

Deterministic fixture · required contract

The fixture exercises 15 routes: 8 are expected to emit rows and 7 intentionally exercise empty, loading, or timeout behavior.

Metric Base HEAD Δ
Run status success success —
Extracted rows / contract 40 / 40 40 / 40 0 (0.00%)
Contract fill 100.00% 100.00% 0.00 pp (0.00%)
Sites with rows 8 / 15 8 / 15 0 (0.00%)
Sites meeting row contract 15 / 15 15 / 15 0 (0.00%)
Unexpected empty sites 0 0 0
Partial-row sites 0 0 0
Site outcomes extracted: 7; HTTP error + rows: 1; empty: 3; timeout + empty: 4 extracted: 7; HTTP error + rows: 1; empty: 3; timeout + empty: 4 —
Observed duration 19.85 s 19.85 s +0.00 s (+0.01%)
Estimated CPU 0.41 s 0.41 s 0.00 s (0.00%)
Peak PSS 65.93 MiB 66.75 MiB +0.81 MiB (+1.24%)
Peak RSS 69.41 MiB 70.47 MiB +1.06 MiB (+1.53%)
Peak threads 18 18 0 (0.00%)
Mismatched sites 0 0 0
Timeout with items 0 0 0

Full HTML, JSON, CSV, logs, and page snapshots: workflow run and full spider-bench-results artifact.

The public-web A/B is informational. The exact HEAD fixture contract runs as a separate required CI check; timing and public-site content deltas remain non-blocking.

@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Sequential Navigation Soak A/B

✅ HEAD completed the 200-navigation resilience and memory observation.

One browser process, one target, and one CDP session navigate CSDN → SegmentFault → Huaban → example.com repeatedly. This run issued 200 Page.navigate commands per binary.

Common ancestor 14d3c08cc8 → HEAD 9e83f5cb3b; benchmark order: head-first; workflow: success.

Session resilience

Metric Base HEAD
Attempted / planned 200 / 200 200 / 200
Direct observable pass 197 / 200 196 / 200
Failures / recovered 3 / 3 4 / 4
Recovery failures 0 0
Lifecycle order violations 0 0
Network order violations 0 0
Wall time 588.3 s 554.4 s

A public-page failure is reported separately from an unrecoverable session. The soak requires all navigation attempts, zero failed recovery, zero lifecycle/network ordering violations, and complete resource evidence.

Process-tree memory

Metric Base HEAD Δ
Boundary samples 200 200 0
Boundary RSS samples 200 200 0
Boundary PSS samples 200 200 0
Periodic peak RSS 271.9 MiB 264.5 MiB -7.4 MiB
Periodic peak PSS 268.1 MiB 261.0 MiB -7.1 MiB
RSS · first 10 avg 174.0 MiB 169.1 MiB -4.9 MiB
RSS · last 10 avg 181.2 MiB 184.7 MiB +3.5 MiB
RSS · last − first window +7.1 MiB +15.6 MiB +8.4 MiB
RSS · warm slope / 100 nav +5.6 MiB +2.5 MiB -3.1 MiB
PSS · first 10 avg 170.3 MiB 165.6 MiB -4.6 MiB
PSS · last 10 avg 177.6 MiB 180.9 MiB +3.3 MiB
PSS · last − first window +7.3 MiB +15.3 MiB +8.0 MiB
PSS · warm slope / 100 nav +5.6 MiB +2.5 MiB -3.1 MiB
Peak file descriptors 57 57 0
Peak threads 29 29 0

HEAD memory by 50-navigation quarter

HEAD range Avg RSS Final RSS Peak RSS Avg PSS Final PSS Peak FD
1–50 179.9 MiB 187.8 MiB 231.9 MiB 176.4 MiB 184.3 MiB 52
51–100 181.5 MiB 128.6 MiB 224.2 MiB 177.9 MiB 125.1 MiB 52
101–150 182.6 MiB 160.3 MiB 228.1 MiB 179.1 MiB 157.0 MiB 53
151–200 185.2 MiB 220.5 MiB 242.2 MiB 181.6 MiB 216.7 MiB 52

Raw reports and per-navigation boundary samples: workflow run and full sequential-navigation-soak-results artifact.

Public-site timing and memory are observational. Use the A/B deltas and trend shape as evidence, not as a deterministic performance threshold.

@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

CI Regression Report

Source CI run · source state at render: in_progress · artifacts: 6/6

Check Status Signal
Release regression ✅ HEAD/base failures 0/0; raw binary +0.009316%
Frontend differential ✅ 1,020/1,020 cases matched; 0 issues
Agent episodes ✅ 8/8 Moli episodes passed; 0 failures
Runtime/CDP contracts ✅ 26 contract cases; 0 failures
CDP smoke ✅ 53/53 groups passed; 625 scenarios
WebMainBench ✅ 545/545 completed; 0 unexpected failures
Release regression — ✅ HEAD/base failures 0/0; raw binary +0.009316%

Package and image size

Metric Base HEAD Delta Delta %
Raw binary 150.20 MiB 150.22 MiB +14.33 KiB +0.009316%
Stripped binary 103.28 MiB 103.29 MiB +12.00 KiB +0.011346%
gzip binary 51.58 MiB 51.59 MiB +9.91 KiB +0.018766%
Rootfs 157.11 MiB 157.12 MiB +10.00 KiB +0.006216%
gzip rootfs 60.49 MiB 60.50 MiB +10.02 KiB +0.016173%

Startup latency and PSS

Case Base p50 HEAD p50 Delta Base PSS HEAD PSS Delta
Serve ready 26.78 ms 27.03 ms +0.25 ms 5.53 MiB 4.63 MiB −920.50 KiB
CDP first page 47.20 ms 50.27 ms +3.07 ms 4.92 MiB 4.61 MiB −316.00 KiB
CDP warm page 11.30 ms 12.82 ms +1.51 ms 49.32 MiB 50.50 MiB +1.18 MiB
CLI about:blank 14.26 ms 15.73 ms +1.47 ms 4.15 MiB 4.05 MiB −109.00 KiB
CLI local JS 19.73 ms 19.08 ms −0.65 ms 4.99 MiB 4.29 MiB −715.50 KiB

Concurrency matrix

Concurrency Base p50 HEAD p50 Delta Base PSS HEAD PSS Delta HEAD failures Unstable B/H
1 17.10 ms 17.37 ms +0.26 ms 846.00 KiB 861.00 KiB +15.00 KiB 0 0/0
5 20.68 ms 19.97 ms −0.71 ms 23.34 MiB 28.33 MiB +4.99 MiB 0 0/0
10 20.24 ms 20.16 ms −0.08 ms 28.30 MiB 28.29 MiB −15.50 KiB 0 0/0
25 20.41 ms 19.87 ms −0.54 ms 28.29 MiB 12.52 MiB −15.77 MiB 0 0/0
100 20.67 ms 20.72 ms +0.04 ms 12.39 MiB 18.55 MiB +6.16 MiB 0 0/0
Frontend differential — ✅ 1,020/1,020 cases matched; 0 issues
Match DOM mismatch Diagnostic mismatch Moli error Reference error Infrastructure error Mismatched frames Duration
1,020 0 0 0 0 0 0 7m 5.81s
Agent episodes · Moli vs Chromium — ✅ 8/8 Moli episodes passed; 0 failures
Fast contract Episodes Passed Assertions Ready p50 Episode p50
Moli 8 8 47/47 2.98 ms 23.84 ms
Chromium 8 8 47/47 241.98 ms 78.88 ms
Operation p50 Chromium Moli Delta Delta %
navigate 41.23 ms 5.46 ms −35.77 ms −86.76%
observe 0.73 ms 0.98 ms +0.26 ms +35.04%
fill 4.03 ms 3.14 ms −0.90 ms −22.22%
click 5.30 ms 3.72 ms −1.58 ms −29.83%
Idle-resource episode Peak RSS Peak PSS Average CPU Peak CPU Peak processes Sampler
Moli 66.47 MiB 63.10 MiB 0.72% 9.99% 1 ✅
Chromium 1276.13 MiB 406.23 MiB 0.53% 4.01% 13 ⚠️ PSS partial
Runtime and CDP session contracts — ✅ 26 contract cases; 0 failures
Suite Cases Failures Median case p50 Median case p95 Median peak PSS p50
Synthetic fetch 13 0 17.31 ms 18.45 ms 654.00 KiB
Long-lived CDP session 13 0 18.12 ms 18.70 ms —
CDP smoke — ✅ 53/53 groups passed; 625 scenarios

Workers: 4 · cumulative group time: 144.31 s · failed groups: 0

Slowest group Scenarios Duration Status
scrollbar-visibility 22 15.38 s ✅
target-lifecycle 24 12.06 s ✅
chromium-cdp 56 9.13 s ✅
dom-input 27 8.86 s ✅
multi-page 45 8.06 s ✅
WebMainBench · 545 pages — ✅ 545/545 completed; 0 unexpected failures
Completed Successful Expected DNS failure Unexpected failures Panics Crashes Timeouts Empty output
545/545 544 1 0 0 0 0 0

Frozen HTML · Linux · --wait done · 45 seconds per page · no retries. Content quality is evaluated separately.

Diagnostics: webmainbench-results — every page's Markdown, stderr, and exit status. Source run and artifacts

All artifact fields are parsed by the trusted default-branch renderer; missing or invalid inputs remain visible as unavailable.

@lanyue-llk lanyue-llk left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

复核提交 bd49a5afe436,发现一项后续通知丢失问题,建议修正后再合入,具体位置和最小场景见行内评论。

把通知交给 rendering 队列、使用 flattened-tree depth,以及给深层有限更新保留 continuation depth,方向都有依据。当前退出逻辑只为 32 轮预算耗尽安排 continuation;同深度变化被跳过、正常结束当前 broadcast 时,还需要下一次 rendering opportunity。

已追踪 observe/DOM mutation 入队、广播抑制、layout 发布与任务结束路径,并实测 Chrome 对照:首次通知 50、回调改为 60 后,得到一次 loop error,随后仍收到 60。此处 Moli 的结论来自提交源码的调度路径;本机缺少原生开发工具,未运行该提交的 V8 测试。

Comment thread moli-renderer-v8/src/script_vm/rendering_update.rs

@lanyue-llk lanyue-llk left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

复审 ceb46d382471:上次的 skipped observations 后续通知问题已修正。现在预算耗尽继续携带原 depth,broadcast 已结束但仍有 skipped 目标时从 depth=0 开始下一轮;新增测试也检查了第二轮交付及 disconnect 后收敛。

还发现一个本轮通知目标集合未固定的问题,具体场景见行内评论。当前实现把 gather 与逐个 callback 的执行混在一起,前一个 callback 能把新目标塞进后一个 observer 的当前批次。建议先修正这处批次语义再合入。

已用隔离 Chrome 执行最小对照场景,结果为 first → second:[b] → loop error → second:[c]。Moli 的差异结论来自最新提交的目标列表与广播调用链;本机未运行原生 V8 测试。

Comment thread moli-renderer-v8/src/context_bootstrap/resize_observer_runtime.rs Outdated
@lanyue-llk

Copy link
Copy Markdown
Contributor Author

Current-main integration exposed two pre-existing OnDemand ResizeObserver controls in Nextest. This was a real semantic conflict rather than a flaky retry:

  • the PR correctly moved delivery onto the document rendering source, so the two controls now drive one production rendering turn instead of assuming synchronous delivery at the end of eval();
  • the rendering task no longer creates an unsolicited layout pass. OnDemand ResizeObserver delivery reads the most recently published layout, preserving frozen writing-mode axes, unzoomed dimensions and the zero-extra-layout contract.

The focused local Rust run could not link because this macOS host has no Apple Command Line Tools. cargo fmt --all -- --check and git diff --check pass. The existing Nextest job now exercises the two exact badcases together with the rest of the ResizeObserver suite; no temporary CI-only test or site-specific branch was added.

@lanyue-llk lanyue-llk left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR moves real-layout ResizeObserver delivery onto the document rendering-update source and implements the spec's gather -> broadcast -> relayout -> gather loop with flat-tree depth, a loop-error ErrorEvent, a per-callback microtask checkpoint, and a 32-round budget that yields and resumes at the current depth. The core loop is a clear improvement and matches the spec ordering: everything is gathered before any callback runs, the shallowest delivered depth is computed across all observers, and skipped observations produce the error and a depth-0 retry. I am requesting changes for three reasons. The per-observer scheduled flag can now get stuck, which makes an observer go permanently silent. The new checkpoint call also runs end-of-checkpoint work inside the legacy microtask flush. And a sustained feedback loop now re-queues itself with no frame pacing.

Findings not anchored to a diff line:

  • [major] CI: Nextest is red. moli-core::wpt_compat fontface_basic and fontface_idlharness fail all 3 tries with fetch readiness timed out after 5000 ms while waiting for response headers. I found neither failure in recent main runs or in the neighbouring PRs I checked (1591, 1592, 1594, 1595). This PR changes rendering-update work, and its fresh layout pass now runs reconcile_document_web_fonts_for_layout from rendering turns, so please triage this before merging. Do not assume it is a flake.
  • [major] OnDemand is no longer on demand, and resource use went up. In OnDemand mode, every published layout now runs queue_resize_observer_checks (layout.rs). That queues a ResizeObserver rendering update, and the update runs at least one more full with_fresh_layout_pass before it measures anything, even when no observation is active. So any page with an observer pays roughly double the layout cost per publish, plus up to 32 fresh layouts per turn. The Spider A/B in this PR shows HEAD at +23.7% duration, +17.5% CPU and +14.9% peak PSS, with an identical row count. That run is informational and noisy, but it is consistent with this change. Consider skipping the fresh layout when the published snapshot is current (layout not dirty), or gathering first and laying out only when an observation is active.
  • [minor] The tests do not cover the regressions above. No test covers re-observing after an observer drained while it was unregistered (the scheduled-flag bug), the non-real-layout path together with IndexedDB or end-of-checkpoint ordering, or a new shallow change arriving while a resumed depth-N continuation is pending. resize_observer_deep_growth_yields_without_masking_real_feedback asserts only created > 1 && created < 120, which implicitly depends on the magic 32 and does not pin the resume depth. The PR description says the tests are the causal proof for the MUI grid case. The fetch_dump tests do approximate that case, but the description's Browser Eval reproduction is external and unverifiable here.
  • [nit] Both callers ignore the skipped element returned by deliver_resize_observer. In the gathered path minimum_depth is None, so the depth filter in build_resize_observer_entries_at_depth only ever runs with Some(0), where depth <= 0 cannot be true for an Element. That branch is dead, so the API can drop the depth parameter and the (entries, depth, skipped) triple from the legacy path.

Verdict: changes requested (posted as a comment because GitHub does not allow requesting changes on your own PR).

Automated review by Claude Code.

Comment thread moli-renderer-v8/src/context_bootstrap/resize_observer_runtime.rs Outdated
Comment thread moli-renderer-v8/src/context_bootstrap/resize_observer_runtime.rs Outdated
Comment thread moli-renderer-v8/src/script_vm/rendering_update.rs Outdated
Comment thread moli-renderer-v8/src/native_bridge/context_host/rendering_updates.rs Outdated
Comment thread moli-renderer-v8/src/script_vm/rendering_update.rs Outdated
Comment thread moli-renderer-v8/src/script_vm/layout.rs Outdated

@lanyue-llk lanyue-llk left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review of the updated version (2ae7f4e12)

This PR delivers real-layout ResizeObserver notifications through the document rendering-update source. It implements the gather -> broadcast -> relayout -> gather loop with flat-tree depth, the loop-error ErrorEvent, and a named 32-round budget per turn. The new commit 2ae7f4e12 fixes most of round 1: the stuck scheduled flag, the misplaced checkpoint, unpaced retries, payload-blind depth coalescing, the magic constant and the guard field. Every one of those fixes except the IndexedDB one has a regression test that would fail without it.

Verdict: request changes. The OnDemand layout-cost finding from round 1 is still unaddressed. The new IndexedDB regression test never reaches the legacy path it is named for.

Previous findings

# Finding Status Evidence
1 [major] Per-observer scheduled flag stuck, observer goes silent FIXED queue_resize_observer_delivery (resize_observer_runtime.rs:784-794) now returns on the real-layout path before it reads or sets the flag. resize_observer_reobserve_after_a_cancelled_real_layout_delivery would fail on the old code, because the second turn would return false.
2 [major] Per-callback checkpoint on the legacy microtask path deactivates IDB transactions FIXED in code, test does not cover it The checkpoint moved into broadcast_resize_observer_rendering_update (line 918), and deliver_resize_observer no longer has one. The regression test runs under the harness default LayoutPolicy::OnDemand, so it never takes the legacy flush (see inline).
3 [major] Sustained same-depth feedback spins the rendering source FIXED An undelivered result now arms a 16 ms Window timer instead of an immediate re-queue (rendering_update.rs:117-145). Both feedback tests now assert that no turn is ready before the timer fires, and both would fail on the old code. New minor finding about how the timer is implemented: see inline.
4 [minor] Depth-blind coalescing causes false loop errors FIXED queue_resize_observer_rendering_update_for_target merges min(pending, new) (rendering_updates.rs:276-289), and the generic coalescer compares payloads again. resize_observer_fresh_shallow_work_resets_a_pending_deep_continuation would fail on the old code: the root is skipped at depth N and fires an error.
5 [nit] Magic 32 FIXED RESIZE_OBSERVER_ROUNDS_PER_RENDERING_TURN is documented as a fairness bound, and the deep-growth test now asserts the exact budget + 1 boundary.
6 [nit] Opaque guard condition and pub(crate) field FIXED, pushback accepted is_broadcasting_resize_observers_for is shared by both call sites, and the field is private behind begin/restore. The author pushed back on RAII: the fallible loop is captured into a Result before restore_resize_observer_broadcast, so a ? cannot skip the restore.
7 [major] CI Nextest red (fontface_basic, fontface_idlharness) RESOLVED Nextest passes on 2ae7f4e12 (run 39731395791). The new base contains 41e608f91 fix(renderer): load and retire web fonts by child Document owner. The failures do not reproduce, and I can no longer attribute them to this PR. Some A/B jobs (Build release · common ancestor, Spider fixture, Frontend differential, Agent episodes, Runtime/CDP contracts) were still pending at review time.
8 [major] OnDemand is no longer on demand: extra full layouts per publish and per turn NOT FIXED, no response The code is unchanged, see inline. No Spider A/B has been posted for the new head. The latest one is for 7d1ff5f9f and still shows +23.7% duration, +17.5% CPU and +14.9% peak PSS.
9 [minor] Tests miss the regressions MOSTLY FIXED Re-observe, depth reset, exact budget and paced retries are now covered. The legacy-path IDB case is still uncovered (#2).
10 [nit] Dead minimum_depth filter and ignored skipped in the legacy path NOT FIXED resize_observer_runtime.rs:310 still passes Some(0) only on the legacy path, where depth <= 0 never holds for an Element. Line 915 still discards skipped.

New findings

  • [minor] The IndexedDB regression test does not exercise the legacy path (indexed_db.rs:4447).
  • [minor] The pacing retry is a page-visible, page-cancellable setTimeout-class timer, and it is not deduplicated (rendering_updates.rs:313).
  • [nit] The dead depth parameter is still present (resize_observer_runtime.rs:310).

Not anchorable:

  • [minor] Docs and description are now stale. LayoutPolicy::OnDemand's doc comment (moli-page-types/src/layout.rs:9, "Only screenshot, screencast, and PDF output build layout") is false now that ResizeObserver rendering turns build and publish layouts. The author's PR comment of 2026-10-20 claims "the rendering task no longer creates an unsolicited layout pass ... zero-extra-layout contract". The following commit ba4247b6b reverted that by adding with_fresh_layout_pass before every gather. The rewritten description's "Problem" section lists four bugs that this PR's own earlier revision introduced (stuck flag, legacy checkpoint, spin, depth merge) as if they were the problem being solved. The real motivation (main never delivered real-layout observations through rendering turns, which lost the MUI grid rows) is buried under "Browser Eval integration". Please update the doc comment and rewrite the Problem section against main.
  • [nit] Stack hygiene. b3976852e ("preserve on-demand layout during resize delivery": removes the layout pass) and ba4247b6b ("measure resize updates after layout": restores it) cancel each other out. 162bab8b1, c735e7c45 and 2ae7f4e12 are review fixups to the same files. Please squash before merge so that bisect does not land on the intermediate states.
  • Merge check: git merge-tree --write-tree origin/main refs/review/r2-1593 is clean.

Verdict: changes requested (posted as a comment because GitHub does not allow requesting changes on your own PR).

Automated review by Claude Code.

Comment thread moli-renderer-v8/src/script_vm/rendering_update.rs
Comment thread moli-renderer-v8/src/script_vm/tests/indexed_db.rs
Comment thread moli-renderer-v8/src/native_bridge/context_host/rendering_updates.rs Outdated
Comment thread moli-renderer-v8/src/context_bootstrap/resize_observer_runtime.rs Outdated
@lanyue-llk
lanyue-llk force-pushed the codex/webfetch-observers branch from c1b2fcb to 9e83f5c Compare October 22, 2026 11:06
@lanyue-llk

Copy link
Copy Markdown
Contributor Author

Handled the non-inline review items in 9e83f5cb3:

  • corrected the public LayoutPolicy::OnDemand contract to include active ResizeObserver rendering updates;
  • rewrote the PR description around the behavior missing from main and the frozen Browser Eval case, rather than intermediate defects from earlier revisions;
  • kept the no-target early exit and its layout-pass assertion;
  • consolidated the final tree into one commit so the cancelled intermediate approaches cannot enter main through a rebase merge.

I did not add a layout-generation cache. Once a document has active observation targets, a fresh layout is the input needed to detect size changes. Reusing the previous layout without a complete style/layout invalidation contract could silently miss the exact virtual-content update this PR fixes. The current boundary is smaller and testable: no targets means no layout; active targets use a bounded 32-round observation loop and deduplicated delayed retries.

@lanyue-llk lanyue-llk left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Final re-review (P0/P1 only) (9e83f5cb3)

Verdict: approve. No P0/P1 issues remain. 9e83f5cb3 squashes the stack into one commit and addresses all three round-2 items.

Previous P0/P1-level findings

# Finding Status Evidence at 9e83f5cb3
1 Stuck per-observer scheduled flag FIXED (r2) queue_resize_observer_delivery returns on the real-layout path before it reads or sets the flag (resize_observer_runtime.rs:763-768).
2 Legacy-path checkpoint deactivates IDB transactions; the regression test was vacuous FIXED The checkpoint runs only in broadcast_resize_observer_rendering_update (resize_observer_runtime.rs:948). The test now calls set_layout_policy(Mock) (indexed_db.rs:4451), so observe() queues resize_observer_flush_callback as a host microtask ahead of the Promise.then. With the old deliver_resize_observer checkpoint, run_end_of_microtask_checkpoint_tasks would deactivate tx and the test would see throw:TransactionInactiveError. A real-layout twin (indexed_db.rs:4494) covers a transaction created inside the RO callback.
3 Same-depth feedback spins the rendering source FIXED (r2) Paced 16 ms retry; tests assert that no turn is ready before the deadline.
4 Pacing retry is a page-cancellable Function timer and is not deduplicated FIXED It now uses queue_internal_task_after (timers.rs:286), which builds an InternalFunction task that cancel_window_timer refuses to cancel (timers.rs:895-898). resize_observer_retry_targets coalesces repeat arms and is cleared in the callback (rendering_updates.rs:12-26, 355-373). resize_observer_same_depth_feedback_still_reports_a_loop_error now clears every page timer id before advancing and still requires the retry turn.
5 Depth-blind coalescing causes false loop errors FIXED (r2) min(pending, new) merge (rendering_updates.rs:293-308).
6 OnDemand cost: unconditional full layout per RO turn PARTIAL, not blocking A consumed RO turn now checks resize_observer_target_has_observations and returns before any layout when the document has no observed targets (script_vm/rendering_update.rs:80-88). queue_resize_observer_checks only queues for observers with targets. The reobserve test asserts zero added layout passes on the empty turn. With live targets, each turn still runs one fresh full layout plus one per delivering round. HTML "update the rendering" requires up-to-date layout before each gather, and the author declined a generation cache, which I accept. I traced the publish → RO turn → publish path: the broadcast guard (layout.rs:262-266) stops the RO layout from re-queueing itself, and RO is the only rendering-update kind that calls with_fresh_layout_pass, so it cannot ping-pong with IntersectionObserver turns. No Spider A/B exists for this head yet (Build release jobs are pending). The only A/B, +23.7% duration, is for 7d1ff5f9f, which predates both the per-turn layout and this early exit, so I cannot attribute a severe regression to the current code.
7 Nextest red (fontface) RESOLVED (r2) Not attributable to this PR.

New findings

No P0/P1 issues found in the squashed commit. Hang/starvation check: rounds are bounded at 32 per turn. A budget continuation re-queues immediately, so unbounded deep growth keeps yielding between Page tasks instead of hanging. Same-depth feedback retries only through the deduplicated 16 ms internal timer. The no-target early exit cannot drop notifications, because a document without observed targets has nothing to deliver.

CI and merge

  • CDP smoke (Chromium) failed on inspector-routing (raw_cdp_navigation_replacement_during_active_javascript: TimeoutError). That job drives Playwright's Chrome for Testing as a control, not the Moli renderer, so it is unrelated to this ResizeObserver change.
  • Clippy, Format, Node, Python and Windows jobs pass.
  • Nextest, Build release (HEAD and ancestor) and the A/B jobs are still pending. This approval assumes Nextest goes green. Please post the Spider A/B for this head before merging.
  • git merge-tree against origin/main is clean, and GitHub reports MERGEABLE.

Verdict: approve (posted as a comment; self-approval is not allowed).

Automated review by Claude Code.

@lanyue-llk

Copy link
Copy Markdown
Contributor Author

Current-head Browser Eval badcase is green.

  • PR head: 9e83f5cb3b84b388815844dd7e230a38e88cf384
  • candidate binary: moli 1.1.19, SHA-256 9e9198fee1749e137b3167fd472b51bbac38366f0c75da6b1f6c71101affb7a6
  • frozen task: dynamic-104-mui-data-grid
  • result: PASS in 17,965.52 ms, no retry, within the 45,000 ms task deadline
  • output: 14,114 bytes; all five required facts were present, including MUI X Data Grid, Why choose the MUI X Data Grid?, First name, Jon Snow, and API
  • body scoring: all five regions passed; the four-region minimum and 2,200-character floor were satisfied

Reproduction uses Browser Eval's provider adapter rather than a hand-written fixture:

uv run --frozen python scripts/run_certified_benchmark.py \
  --dataset <frozen-430-dataset.json> \
  --output <output-dir> \
  --work-root <work-dir> \
  --providers moli-candidate \
  --case-ids dynamic-104-mui-data-grid \
  --workers 1 \
  --max-failure-retries 0 \
  --provider-config configs/providers.headless.json \
  --retrieval-mode native \
  --diagnostic

The current Chromium-reference smoke failure is unrelated to this code path: it timed out in raw_cdp_navigation_replacement_during_active_javascript, while this PR changes Web Fetch observer readiness. The benchmark badcase above exercises the changed path directly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant