Sitelet https://github.com/sdcorejs/sdcorejs-angular/pull/71
Skip to content

test(image-editor): stop the full-resolution export spec from timing out under load - #71

Merged
sdcorejs merged 1 commit into
mainfrom
claude/upbeat-tereshkova-feabd5
Oct 1, 2026
Merged

sdcorejs merged 1 commit into
mainfrom
claude/upbeat-tereshkova-feabd5

Conversation

@sdcorejs

@sdcorejs sdcorejs commented Oct 1, 2026

Copy link
Copy Markdown
Owner

Summary

SdImageEditor export exports from the full-resolution source, not the downscaled preview timed out at 5 s in full-suite runs, both locally and in CI. It caused 3 of the 4 red CI runs on 2026-09-29/30 (runs 36562656580, 36666512326 and 36693626376, all on commits that never touched the image editor). When it timed out, its leftover async code also failed the next spec, with editor not rendered, Expected 'loading' to be 'ready' or Expected $[0] = 60 to equal 2400.

This PR makes the spec independent of machine load without changing what it proves, and stops a timed-out spec from failing another one. Only spec files change. There is no library or public API change, so there is no changelog entry.

Root cause

  • The spec pushes a 2400 × 2000 image through several full-resolution browser steps. Chrome encodes canvas.toBlob() PNG on the main thread in idle time only: with no idle time it waits up to 1 s to start and up to 5.7 s more to finish. Image decodes also stall on a loaded machine.
  • Measured times for the same work: about 0.7 s on its own; 7.1 s inside a full-suite run on a loaded machine, where one createImageBitmap stalled for 6 s; 6.7 s when the main thread was kept busy on purpose, which reproduces the timeout.
  • Ruled out: fake timers (every jasmine.clock().install() is undone in a finally) and missing teardown (no leaked intervals, animation-frame loops or CSS animations were running).
  • Cascade: the timed-out spec kept running against the module-level fixture/host. Jasmine 5.5 reports late errors and expectations on whichever spec is running at that moment.

Changes

  • image-editor.fixtures.spec.ts
    • grayPng() writes a large source as PNG bytes with CompressionStream, so there is no idle-time canvas encode. The image is opaque, so the editor also skips its transparency scan. The browser still decodes it.
    • decode(blob, region) reads back only a region. pixel() now throws for a pixel that was not read back.
    • stopPolling() parks every until() left over from a finished spec, instead of letting it throw into the next one.
  • image-editor.component.spec.ts
    • The export spec uses grayPng() and a 3 × 1 region read. Its assertions are unchanged, and it now also asserts the decoded size (2400 × 2000).
    • It gets an explicit 30 s budget (LARGE_IMAGE_TIMEOUT) for its inherent 4.8 MP work. This is a budget, not a blind bump: with no idle time the new version takes up to about 4 s, and one decode stalled for 6 s in a full-suite run.
    • beforeAll probes the encodable formats once, so that cost never lands on whichever spec loads an image first.
    • afterEach calls stopPolling().
  • Rolled out to v20, v21 and v22 with npm run sync. The SYNC-STATUS.md timestamps are refreshed, and the v22 files are LF.

Verification

On this HEAD (8f57269, rebased on main 6e0509a), Node 22.22.3:

  • npx ng test sdcorejs-angular --watch=false --browsers=ChromeHeadlessCI --code-coverage (v19): 6168/6168 passed, seed 240905. Coverage gate passed: 87.61 / 76.54 / 88.07 / 89.93.
  • npm run lint (v19): all files pass.
  • npm run check:sync: passed.

Before the rebase, on the same change:

  • Focused image-editor specs: 114/114.
  • Full v19 suite: 5970/5970 twice (seeds 10960 and 82141). The export spec took 245 ms in-suite.
  • Old vs new export workload: 6738 ms → 3873 ms with the main thread kept busy, 1180 ms → 654 ms idle.
  • The fixture code type-checks with TypeScript 5.7, 5.8, 5.9 and 6.0 (the v22 line uses 6.0).

Not run locally: the v20, v21 and v22 Karma suites (the compatibility jobs cover them), and root test:scripts (no script reads these specs).

Risks and rollback

  • If this spec ever hangs, the 30 s budget only makes it fail later.
  • Other image-editor specs reached 2–3.3 s in the full suite and keep the 5 s budget. stopPolling() covers leftover polls; leftover code paused somewhere else could still touch the next spec's fixture.
  • Rollback: revert this commit. It changes only specs.

Follow-ups (not in this PR)

  • The editor encodes the same output twice when Apply overlaps the size estimate. This was seen once in the suite.
  • image-editor source decoding reports a truncated file as decode-failed timed out on its own in CI run 36605910408.

🤖 Generated with Claude Code

…out under load

The spec pushed a 2400 x 2000 image through several full-resolution browser
steps. Chrome encodes canvas.toBlob() PNG on the main thread in idle time only
(it waits up to 1 s to start and up to 5.7 s more to finish), and image
decodes can stall on a loaded machine. The spec takes about 0.5 s on an idle
machine but went past the 5 s default in full-suite runs, both locally and in
CI (3 of the 4 red CI runs on 2026-09-29/30).

- Write the large source as PNG bytes (grayPng) instead of encoding a canvas.
  It is opaque, so the editor also skips its transparency scan.
- Read back only the pixels the spec checks (decode(blob, region)). The
  assertions are unchanged, and the decoded size is now asserted too.
- Give this spec an explicit 30 s budget for its inherent 4.8 MP work.
- Probe the encodable formats once in beforeAll, so that cost never lands on
  whichever spec loads an image first.

A timed-out spec kept running and, through the module-level fixture/host,
failed the next spec ("editor not rendered", "Expected $[0] = 60 to equal
2400"), because Jasmine reports late errors and expectations on the spec that
is running. stopPolling() in afterEach now parks every until() left over from
a finished spec.

Verification: focused image-editor specs 114/114; full v19 suite 5970/5970
twice (seeds 10960, 82141); npm run check:sync passed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sdcorejs
sdcorejs merged commit 2385701 into main Oct 1, 2026
6 checks passed
@sdcorejs
sdcorejs deleted the claude/upbeat-tereshkova-feabd5 branch October 1, 2026 08:39
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.

2 participants