stardust: a page with a CSS divide-by-zero (33,554,432 px layout) crashes full-page screenshots — crawl.mjs hides it, stitch-shot.mjs would too

Open
#602 0 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

Nobody has claimed this yet.

Assessment

Difficulty
4/5
Estimated time
3-5 days
Newbie friendliness
35/100
Issue type
Bug
Clarity
Mostly clear
Activity status
Active
Tech stack
javascript, nodejs
Domain
tooling

Research direction

Start with the screenshot try/catch in skills/extract/scripts/crawl.mjs, where fullPage: true falls back to a viewport capture on the same page after a crash. The reproduction is probe.mjs, run under a memory limit against the URL in the issue. Done means the crawl measures page size before capturing, skips the full-page capture when the layout overflows, reports such pages as a warning rather than OK, and stitch-shot.mjs has a height cap. Fixing crawl.mjs alone does not cover the other capture scripts the issue lists.

Written by the indexing model from the issue text.

Description

bug

Summary

One page breaks every full-page screenshot in stardust. Its own CSS divides by zero, so Chrome lays the page out at 33,554,432 px, the layout maximum, in both directions. A full-page screenshot then takes several GB of memory. On a 12 GiB OF1 Labs container the VM hangs. On a laptop the Chrome tab crashes. Four production Replica runs for this site were lost on 2026-11-13.

crawl.mjs makes it worse in three ways:

  • It never checks the page size before the screenshot.
  • Its viewport fallback retries on the tab that crashed.
  • It then reports the page as OK, so the agent retries it.

The page

https://www.fondazionemenarini-minuti.com/ed-sc-prev/ is a draft copy of "Edizione Scientifica". It is listed in the WordPress sitemap but not linked from any menu.

Its Elementor stylesheet wp-content/uploads/elementor/css/post-7347.css has two widgets with a column count of 0:

.elementor-element-4664162 .jet-smart-listing__posts .jet-smart-listing__post-wrapper { flex: 0 0 calc( 100% / 0 ); max-width: calc( 100% / 0 ); }
.elementor-element-9191ba7 .jet-smart-listing__posts .jet-smart-listing__post-wrapper { flex: 0 0 calc( 100% / 0 ); max-width: calc( 100% / 0 ); }

100% / 0 is infinite, and Chrome clamps it to 33,554,432 px. The card's flex: 1 0 100% thumbnail and the image's aspect ratio (1024 / 471) carry that width down. The page grows to the same height.

In a 1440 × 900 window:

value
scrollWidth × scrollHeight 33554432 × 33554432
.jet-smart-listing__post-wrapper 33,554,432 × 12,737,285
img.jet-smart-listing__post-thumbnail-img 25,501,368 × 11,730,630

Proof: rewrite that one value to 100% at load time (context.route on post-7347.css). The page then measures 1440 × 2,862. The full-page screenshot takes 283 ms, and the tab peaks at 327 MB. Other pages of the site are normal: / is 2,328 px tall, /edizione-artistica/ is 4,182 px and /ricerca-arte/ is 12,074 px.

What happens

skills/extract/scripts/crawl.mjs has the same code at cd00021f, 8d2695d7 and main 4e420deb:

try {
  await page.screenshot({ path: shotPath, fullPage: true, timeout: 30000 });
} catch {
  screenshotMode = 'viewport';
  try { await page.screenshot({ path: shotPath, fullPage: false, timeout: 30000 }); }
  catch { screenshotMode = 'failed'; }
}

OF1 Labs Replica image (Chrome stable, channel: 'chrome', 8 GiB limit):

used MiB 1873
used MiB 5189     <- page.screenshot() started
used MiB 8434
shot fail page.screenshot: Protocol error (Page.captureScreenshot): Unable to capture screenshot

On a 12 GiB standard-4 container:

run memory crawl step
e4c4a141 2.1 → 11.94 GiB at 11:42, stuck for hours recapture of 5 pages incl. /ed-sc-prev/, --wait slow
ffc64803 2.2 → 11.92 GiB at 13:01, stuck 47 min first crawl of 100 pages, --concurrency 3
57351e1b 2.6 → 11.93 GiB at 13:28, ~20 min one-page recrawl of /ed-sc-prev/, --wait slow
af04d2fc 2.1 → 11.84 GiB at 13:35, stuck one-page crawl2 of /ed-sc-prev/

In 57351e1b the first crawl's screenshot failed fast (screenshotMode: "failed"). The vision gate marked the page "suspect", the agent recrawled it alone, and the retry took the VM. The run-bg timeout (600 s) killed it. In the other three runs nothing ran any more, the Replica heartbeat stopped, and OF1 Labs failed the runs as lost_container.

macOS, system Chrome, one-page crawl (STARDUST_LIVE_STATE=0, fresh profile, RSS of the process tree sampled every second):

peak
Chrome renderer process for the page 3.8 GB (a second run: 7.4 GB)
whole crawl process tree 4.7 GB
same page with the CSS fixed 327 MB

The renderer ran at 98 % CPU for about 60 s. A Playwright page.on('crash') handler fired: the renderer died. Then:

  • The fullPage: false fallback failed too, because it reused the dead page.
  • The crawl still printed [crawl] OK ed-sc-prev, 1/1 captured, 0 failed and exited 0.
  • The only trace is screenshot: null and _signals.screenshotMode: "failed" in the page record. _crawl-log.json#crawl.failures is empty.

On a fresh page, a viewport screenshot of the same URL works (51 ms, 349 MB peak).

Repro

// node probe.mjs https://www.fondazionemenarini-minuti.com/ed-sc-prev/
import { chromium } from 'playwright';
const b = await chromium.launch({ headless: true, channel: 'chrome' });
const p = await (await b.newContext({ viewport: { width: 1440, height: 900 } })).newPage();
p.on('crash', () => console.log('renderer crashed'));
await p.goto(process.argv[2], { waitUntil: 'load', timeout: 120000 });
console.log('size', await p.evaluate(() => [document.documentElement.scrollWidth, document.documentElement.scrollHeight]));
await p.screenshot({ path: '/tmp/s.png', fullPage: true, timeout: 60000 })
  .then(() => console.log('shot ok'), (e) => console.log('shot fail', e.message.slice(0, 200)));
await b.close();

Run it under a memory limit (docker run --memory=8g …) and watch free -m, or watch the renderer's RSS on a desktop.

Proposal

  1. Measure before the screenshot, and skip the full page when the layout is broken.

    const MAX_SHOT_HEIGHT = 30000; // px
    const { w, h } = await page.evaluate(() => ({
      w: document.documentElement.scrollWidth,
      h: Math.max(document.documentElement.scrollHeight, document.body?.scrollHeight ?? 0),
    }));
    rec._signals.pageSize = { w, h };
    const overflow = h >= 33554432 || w > viewport.width * 4;
    if (overflow) rec._signals.layoutOverflow = true;
    if (overflow || h > MAX_SHOT_HEIGHT) { /* viewport screenshot, or a stitched one capped at MAX_SHOT_HEIGHT */ }
    else { /* fullPage as today */ }
    

    A clip alone does not cap the height. page.screenshot({ clip: { x: 0, y: 0, width: 1440, height: 30000 } }) without fullPage: true returns a 1440 × 900 image, because Playwright clips to the viewport. That was tested on this page (68 ms, no memory spike). So either record it as screenshotMode: 'viewport', or capture the capped height as scrolled chunks (as stitch-shot.mjs does), up to MAX_SHOT_HEIGHT.

  2. Make the fallback survive a crash. Listen for page.on('crash'). Run any fallback screenshot on a new page in a new context, never on the page that threw.

  3. Report the failure. A page with screenshotMode: 'failed' or layoutOverflow: true is a WARN line in the crawl output, not OK. Add it to _crawl-log.json#crawl.failures (or a degraded list) with the reason. The vision gate can then show "layout overflow: CSS defect on the source", not "suspect", which invites a retry.

  4. Use one shared size guard in every capture script. The same unguarded fullPage: true appears in:

    • diff/scripts/visual-diff.mjs
    • deploy/scripts/render-harness.mjs
    • qa/scripts/checks/browse.mjs
    • reskin/scripts/capture-content.mjs
    • reskin/scripts/donor-probe.mjs
    • spec/scripts/spec-capture.mjs
    • spec/scripts/spec-sheet.mjs

    replica/scripts/stitch-shot.mjs needs it too, although it does not use fullPage. It scrolls while (y < totalH) in 900 px chunks and calls stitchRows, which allocates Buffer.alloc(width * totalH * 4). At totalH = 33554432 that is about 37,000 chunks and a buffer of about 193 GB. This comes from reading the code. It was not run on this page. gate.sh takes every live capture with this script. So the fidelity gate hits the page even with the crawl fixed. A shared helper in diff/scripts/live-session.mjs can serve all of them.

  5. The style census passed. style-census.mjs measured this page at 1440 (17 s) and 360 (18 s) with no error.
    Its memory use was not sampled.

For a replica of this page

The source page itself is broken: a visitor's browser lays it out the same way. A replica must not copy the bug. That needs an inconsistency-register entry ("render the listing at 1 column, not 100% / 0"), or the page leaves the roster as a draft.

On the OF1 Labs side

Labs is adding a memory guard to the Replica image (earlyoom, Chrome killed first) and replacing a hung container. With those, this page costs a killed browser instead of the run. The page still gets no screenshot, and today the agent still retries it, so the fixes above are still needed.

Dominant language
JavaScript
Stars
324
Forks
99
Avg merge
2d 7h
Merged PRs (30d)
121

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from adobe/skills

All issues in adobe/skills

Similar issues

More JavaScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.