Skip to content

fix(reports): time-budget tiled screenshot to fail cleanly instead of hitting Celery kill - #42118

Merged
eschutho merged 1 commit into
masterfrom
fix-tile-wait-budget
Jul 31, 2026
Merged

fix(reports): time-budget tiled screenshot to fail cleanly instead of hitting Celery kill#42118
eschutho merged 1 commit into
masterfrom
fix-tile-wait-budget

Conversation

@eschutho

@eschutho eschutho commented Jul 16, 2026

Copy link
Copy Markdown
Member

SUMMARY

Cumulative per-tile readiness waits in take_tiled_screenshot() have no bound tied to the running Celery task's own time limit: each tile's wait_for_function runs at the full configured load_wait (SCREENSHOT_LOAD_WAIT, default 60s, commonly raised to 600s), so a slow dashboard with N tiles can wait up to N × load_wait and get SIGKILLed mid-capture (SoftTimeLimitExceeded) instead of the report failing cleanly and notifying owners. #42253/#42427 bounded the non-tiled readiness wait this way; the tiled loop remained uncapped.

This PR has been reworked from its original revision to sit on top of the merged #42253/#42427 readiness work and is now scoped to tiled-path budgeting only:

  • Reuses fix(reports): make #42253 screenshot readiness production-safe #42427's runtime budget helper (resolve_screenshot_task_budget_seconds) instead of shipping a second derivation — one budget policy, both paths. The tiled operation derives a single wall-clock budget from the running Celery task's own soft/hard limit at the start of the capture.
  • None fallback differs from the non-tiled path, deliberately. Outside Celery (e.g. synchronous thumbnail generation) the helper returns None, which the non-tiled path treats as "keep the configured timeout" — correct for its single bounded wait. The tiled path instead falls back to a fixed total ceiling (TILED_SCREENSHOT_TOTAL_WAIT_BUDGET_SECONDS = 1440s), because per-tile waits accumulate: with N tiles, "keep the configured timeout" would allow N × load_wait total. Sized against the longest report-task hard limit observed in production (1740s) minus the same 300s cleanup margin the runtime derivation reserves.
  • Each tile's readiness-wait timeout is capped at the remaining budget, recomputed after the mandatory scroll-settle sleep (which itself consumes wall-clock time) so a tile can't overrun the budget by up to one settle interval. The cosmetic per-tile animation wait is capped or skipped the same way (skipped, not raised, since it isn't a readiness check).
  • Budget exhaustion raises TiledScreenshotBudgetExceededError — before any further tile is captured — and is exempted from the function's return None fallback, so callers fail the report loudly instead of receiving a partial/unchecked screenshot. No budget floor: a budget already exhausted by setup raises before the first tile, matching the non-tiled path's raise-before-capture semantics (the original revision's 30s floor is dropped for consistency).
  • The new error subclasses ScreenshotTaskBudgetExceededError (base class moved to screenshot_utils.py, re-exported from webdriver.py for compatibility) so callers can catch the whole budget-error family with one type.
  • Diagnostics: the per-tile timeout WARNING now includes budget context (waited vs. requested load_wait, elapsed vs. total budget, tiles captured so far), and a per-tile DEBUG line logs the readiness-wait/animation-wait timing breakdown so a slow dashboard can be profiled from logs alone. Log levels follow the fix(screenshots): downgrade screenshot timeout logs from ERROR to WARNING #38130/chore(playwright): Using warning for timeouts #38441 precedent (WARNING for chart-loading slowness, ERROR reserved for system faults).

Dropped from the original revision (superseded or split out):

Relationship to #42624: that PR centralizes a full report-level deadline and credits this PR's budget design; this PR is the minimal, targeted tiled-path protection that composes with the merged readiness work today and rebases cleanly under #42624 whenever it lands.

BEFORE/AFTER SCREENSHOTS OR ANIMATED GIF

Not applicable; backend screenshot orchestration and diagnostics only.

TESTING INSTRUCTIONS

pytest -q tests/unit_tests/utils/test_screenshot_utils.py   # 47 passed
pytest -q tests/unit_tests/utils/                           # 723 passed
ruff check superset/utils/screenshot_utils.py superset/utils/webdriver.py tests/unit_tests/utils/test_screenshot_utils.py
ruff format --check superset/utils/screenshot_utils.py superset/utils/webdriver.py tests/unit_tests/utils/test_screenshot_utils.py
mypy --check-untyped-defs superset/utils/screenshot_utils.py superset/utils/webdriver.py  # no new errors vs master baseline

New coverage (TestTileWaitBudget): per-tile timeout shrinks as the budget depletes; the readiness wait is capped with the budget recomputed after the scroll-settle sleep; exhaustion raises TiledScreenshotBudgetExceededError (WARNING not ERROR) and stops capturing/combining; exhaustion before the first tile raises without capture (no floor); outside Celery the fixed total fallback caps the first tile's wait; inside Celery the #42427-derived budget caps it; fast dashboards under budget behave exactly as before; the error subclasses ScreenshotTaskBudgetExceededError; per-tile timing DEBUG lines carry the log context.

ADDITIONAL INFORMATION

@dosubot dosubot Bot added the alert-reports Namespace | Anything related to the Alert & Reports feature label Jul 16, 2026
@bito-code-review

bito-code-review Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

AI Code Review is in progress (usually takes 3 to 15 minutes unless it's a very large PR).

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers a full AI review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

# under that ceiling for the rest of the pipeline that runs after tiling
# completes: combining tiles into one image, building the PDF, and
# uploading/delivering the notification.
TILED_SCREENSHOT_TOTAL_WAIT_BUDGET_SECONDS = 1440 # 1740s limit - 300s margin

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggestion: Add an explicit type annotation to this new module-level constant (for example, annotate it as an integer) to satisfy the type-hint requirement for relevant variables. [custom_rule]

Severity Level: Minor 🧹

Why it matters? ⭐

This is a new module-level constant introduced without a type annotation, and the custom rule requires type hints for relevant variables that can be annotated. An int annotation would satisfy the rule.

Rule source 📖

.cursor/rules/dev-standard.mdc (line 28)

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** superset/utils/screenshot_utils.py
**Line:** 54:54
**Comment:**
	*Custom Rule: Add an explicit type annotation to this new module-level constant (for example, annotate it as an integer) to satisfy the type-hint requirement for relevant variables.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎


class TestTileWaitBudget:
@pytest.fixture
def mock_page(self):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggestion: Add explicit parameter and return type annotations to this fixture method signature. [custom_rule]

Severity Level: Minor 🧹

Why it matters? ⭐

The new fixture method omits both parameter and return type annotations, which violates the Python type-hints rule for newly added code.

Rule source 📖

.cursor/rules/dev-standard.mdc (line 28)

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** tests/unit_tests/utils/test_screenshot_utils.py
**Line:** 405:405
**Comment:**
	*Custom Rule: Add explicit parameter and return type annotations to this fixture method signature.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

page.screenshot.return_value = b"fake_screenshot_data"
return page

def test_per_tile_wait_shrinks_as_budget_depletes(self, mock_page, monkeypatch):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggestion: Add type annotations for all parameters and an explicit return type on this new test method. [custom_rule]

Severity Level: Minor 🧹

Why it matters? ⭐

The added test method has unannotated parameters and no return type annotation, so it matches the type-hints violation rule.

Rule source 📖

.cursor/rules/dev-standard.mdc (line 28)

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** tests/unit_tests/utils/test_screenshot_utils.py
**Line:** 419:419
**Comment:**
	*Custom Rule: Add type annotations for all parameters and an explicit return type on this new test method.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

assert timeouts == [100 * 1000, 50 * 1000, 10 * 1000]
assert timeouts == sorted(timeouts, reverse=True)

def test_budget_exhausted_raises_and_stops_capturing(self, mock_page, monkeypatch):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggestion: Add type hints to this method signature, including fixture argument types and a return type. [custom_rule]

Severity Level: Minor 🧹

Why it matters? ⭐

This newly added test method is missing type annotations on its parameters and return type, which is a real violation of the type-hints rule.

Rule source 📖

.cursor/rules/dev-standard.mdc (line 28)

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** tests/unit_tests/utils/test_screenshot_utils.py
**Line:** 445:445
**Comment:**
	*Custom Rule: Add type hints to this method signature, including fixture argument types and a return type.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

assert error_args[3] == 1000
assert error_args[4] == 1000

def test_fast_dashboard_matches_default_behavior(self, mock_page):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggestion: Add parameter and return type annotations to this test method declaration. [custom_rule]

Severity Level: Minor 🧹

Why it matters? ⭐

This new test method omits type hints for its argument and return value, so the suggestion correctly identifies a type-hints violation.

Rule source 📖

.cursor/rules/dev-standard.mdc (line 28)

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** tests/unit_tests/utils/test_screenshot_utils.py
**Line:** 481:481
**Comment:**
	*Custom Rule: Add parameter and return type annotations to this test method declaration.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

# under that ceiling for the rest of the pipeline that runs after tiling
# completes: combining tiles into one image, building the PDF, and
# uploading/delivering the notification.
TILED_SCREENSHOT_TOTAL_WAIT_BUDGET_SECONDS = 1440 # 1740s limit - 300s margin

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggestion: The hardcoded 1440-second budget is not aligned with per-schedule Celery time_limit/soft_time_limit overrides, which can be configured lower via working_timeout. In those deployments this function can still run longer than the actual worker limit and get killed before failing cleanly. Derive the budget from the active report/task timeout when available (or enforce a minimum of configured task limits), and only fall back to a static default when no runtime limit is accessible. [possible bug]

Severity Level: Critical 🚨
❌ Report screenshot tasks can be killed mid-tiling.
⚠️ Budgeted waits ignore per-schedule Celery time limits.
Steps of Reproduction ✅
1. Configure a report schedule with a lower `working_timeout` via the `working_timeout`
field exposed in the Reports API (`superset/reports/api.py:148, 206`), e.g. 600 seconds.

2. Celery beat uses that `working_timeout` to set per-schedule `time_limit` and
`soft_time_limit` in `scheduler()` at `superset/tasks/scheduler.py:23-35`, so the worker
enforces a shorter execution window than the global 1740s default.

3. When the schedule fires, `reports.execute` (`superset/tasks/scheduler.py:38-56`) runs
`AsyncExecuteReportScheduleCommand.run()` (`superset/commands/report/execute.py:22-77`),
which calls `_get_screenshots()` (`superset/commands/report/execute.py:520-79`) and
ultimately `BaseScreenshot.get_screenshot()` (`superset/utils/screenshots.py:53-62`) using
`WebDriverPlaywright` and `take_tiled_screenshot()` for large dashboards.

4. `take_tiled_screenshot()` applies the fixed `TILED_SCREENSHOT_TOTAL_WAIT_BUDGET_SECONDS
= 1440` (`superset/utils/screenshot_utils.py:54, 61-83, 93-121`); on slow, many-tile
dashboards this allows up to ~1440s of tiling before `TiledScreenshotBudgetExceededError`,
but the Celery worker may hit its lower per-schedule `time_limit`/`soft_time_limit` first,
yielding `SoftTimeLimitExceeded` or a hard kill instead of the intended clean budget
exhaustion failure.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** superset/utils/screenshot_utils.py
**Line:** 54:54
**Comment:**
	*Possible Bug: The hardcoded 1440-second budget is not aligned with per-schedule Celery `time_limit`/`soft_time_limit` overrides, which can be configured lower via `working_timeout`. In those deployments this function can still run longer than the actual worker limit and get killed before failing cleanly. Derive the budget from the active report/task timeout when available (or enforce a minimum of configured task limits), and only fall back to a static default when no runtime limit is accessible.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

# dashboard degrades gracefully instead of exceeding it.
# Only check viewport-visible spinners to avoid blocking on
# virtualization placeholders rendered for off-screen charts.
tile_load_wait = min(load_wait, remaining_budget)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggestion: The spinner timeout is derived from a stale remaining-budget value that was computed before the mandatory scroll-settle sleep. This lets each tile wait up to one extra settle interval beyond the declared global budget, so total runtime can still exceed the intended cap. Recompute the remaining budget immediately before wait_for_function (after settle wait) and cap spinner wait with that refreshed value. [logic error]

Severity Level: Major ⚠️
⚠️ Tiled screenshots may exceed configured global wait budget.
⚠️ Celery task margin eroded on heavily tiled dashboards.
Steps of Reproduction ✅
1. Schedule a dashboard report so Celery runs `reports.execute` at
`superset/tasks/scheduler.py:38-56`, which invokes
`AsyncExecuteReportScheduleCommand.run()` at `superset/tasks/scheduler.py:131-135`.

2. Inside `AsyncExecuteReportScheduleCommand._get_screenshots()`
(`superset/commands/report/execute.py:520-79`), a `DashboardScreenshot` or
`ChartScreenshot` is constructed and `BaseScreenshot.get_screenshot()`
(`superset/utils/screenshots.py:53-62`) is called.

3. `BaseScreenshot.driver()` returns `WebDriverPlaywright` when enabled
(`superset/utils/screenshots.py:37-41`), whose `get_screenshot()` calls
`take_tiled_screenshot()` (`superset/utils/webdriver.py:40-51, 213-219, 399-49`) for large
dashboards.

4. In `take_tiled_screenshot()` (`superset/utils/screenshot_utils.py:61-121`),
`remaining_budget` is computed before `page.wait_for_timeout(SCROLL_SETTLE_TIMEOUT_MS)` at
line ~93, then `tile_load_wait = min(load_wait, remaining_budget)` at line ~99 uses that
stale value; this lets each tile spend `SCROLL_SETTLE_TIMEOUT_MS` extra per loop beyond
the nominal `TILED_SCREENSHOT_TOTAL_WAIT_BUDGET_SECONDS`, so total wall-clock time can
exceed the configured budget before `TiledScreenshotBudgetExceededError` is raised.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** superset/utils/screenshot_utils.py
**Line:** 218:218
**Comment:**
	*Logic Error: The spinner timeout is derived from a stale remaining-budget value that was computed before the mandatory scroll-settle sleep. This lets each tile wait up to one extra settle interval beyond the declared global budget, so total runtime can still exceed the intended cap. Recompute the remaining budget immediately before `wait_for_function` (after settle wait) and cap spinner wait with that refreshed value.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

eschutho added a commit that referenced this pull request Jul 16, 2026
…'s log_context

Renamed the execution_id parameter/suffix mechanism added in the previous
commit to log_context / " [context]", matching the mechanism used by the
concurrent fix-tile-wait-budget PR (#42118) for the same call chain
(BaseReportState._get_screenshots -> BaseScreenshot.get_screenshot ->
WebDriverProxy subclasses -> take_tiled_screenshot). Both PRs touch
take_tiled_screenshot()'s signature, so using the same parameter name and
suffix format keeps the eventual rebase mechanical instead of producing a
semantic conflict and two different correlation-id formats in the logs.

The call site in execute.py now passes log_context=f"execution_id=..."
(rather than a bare execution_id kwarg), matching #42118's convention of
a self-describing context string (it uses cache_key=... for thumbnails).

Only the correlation-id plumbing changed -- the per-chart unready-state
diagnostics (chart id + waiting_on_database/spinner_mounted/nothing_mounted)
added in the previous commit are unchanged.

Co-Authored-By: Claude <noreply@anthropic.com>
@bito-code-review

bito-code-review Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Code Review Agent Run #75d1aa

Actionable Suggestions - 0
Additional Suggestions - 1
  • superset/utils/webdriver.py - 1
    • Test coverage gap for log_context · Line 446-446
      The `log_context` parameter is correctly passed through to `take_tiled_screenshot()`. However, no existing test exercises a non-None `log_context` value on this code path, leaving the append behavior unverified in the webdriver layer. The screenshot_utils tests cover `context_suffix` formatting separately, but this integration point in `get_screenshot()` lacks direct coverage per rule [6262].
Review Details
  • Files reviewed - 6 · Commit Range: 4fca509..573b8a4
    • superset/commands/report/execute.py
    • superset/utils/screenshot_utils.py
    • superset/utils/screenshots.py
    • superset/utils/webdriver.py
    • tests/unit_tests/utils/test_screenshot_utils.py
    • tests/unit_tests/utils/webdriver_test.py
  • Files skipped - 0
  • Tools
    • MyPy (Static Code Analysis) - ✔︎ Successful
    • Astral Ruff (Static Code Analysis) - ✔︎ Successful
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers a full AI review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

AI Code Review powered by Bito Logo

@rusackas

Copy link
Copy Markdown
Member

Thanks @eschutho, this looks like a solid fix.

Pre-commit's failing since ruff-format wants to reformat webdriver_test.py, so a pre-commit run --all-files (or using the pre-commit hook when committing locally) should clear that.

The rest of the failures (changes, frontend-build, playwright, etc.) look like the change-detector step hit a GitHub 503, so that's probably just a flaky rerun rather than anything in the diff. I just opened a PR for this separately so it retries.

Codeant's logic-error thread on the stale remaining_budget calc (computed before the scroll-settle sleep) is worth a look. The type-annotation nits from the same bot I wouldn't necessarily worry about, but there's a pile of open bot threads to assess/address/resolve, if you don't mind.

@eschutho
eschutho force-pushed the fix-tile-wait-budget branch from 573b8a4 to d64e368 Compare July 23, 2026 01:05
@codecov

codecov Bot commented Jul 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 11.76471% with 30 lines in your changes missing coverage. Please review.
✅ Project coverage is 65.43%. Comparing base (0981b11) to head (df77884).
⚠️ Report is 4 commits behind head on master.

Files with missing lines Patch % Lines
superset/utils/screenshot_utils.py 11.76% 30 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master   #42118      +/-   ##
==========================================
- Coverage   65.44%   65.43%   -0.02%     
==========================================
  Files        2810     2810              
  Lines      159362   159391      +29     
  Branches    36374    36377       +3     
==========================================
- Hits       104302   104301       -1     
- Misses      53018    53047      +29     
- Partials     2042     2043       +1     
Flag Coverage Δ
hive 38.08% <11.76%> (-0.02%) ⬇️
mysql 57.80% <11.76%> (-0.02%) ⬇️
postgres 57.85% <11.76%> (-0.03%) ⬇️
presto 39.97% <11.76%> (-0.02%) ⬇️
python 59.23% <11.76%> (-0.03%) ⬇️
sqlite 57.48% <11.76%> (-0.02%) ⬇️
unit 100.00% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment on lines +356 to +405
# past the Celery task time limit and getting SIGKILLed.
tile_start = time.monotonic()
elapsed = tile_start - start_time
remaining_budget = wait_budget_seconds - elapsed
if remaining_budget <= 0:
# A customer-side chart-loading issue (a slow/hung dashboard),
# not a Superset system fault, so this is a WARNING rather
# than an ERROR -- consistent with #38130/#38441, which
# deliberately downgraded screenshot timeout logs the same way.
logger.warning(
"Tiled screenshot time budget exhausted on tile %s/%s: "
"%s/%s tiles captured so far, %.1fs elapsed of a %.1fs "
"budget. Aborting instead of capturing remaining tiles "
"unchecked.%s",
i + 1,
num_tiles,
len(screenshot_tiles),
num_tiles,
elapsed,
wait_budget_seconds,
context_suffix,
)
raise TiledScreenshotBudgetExceededError(
f"Tiled screenshot budget of "
f"{wait_budget_seconds:.1f}s exhausted "
f"after {len(screenshot_tiles)}/{num_tiles} tiles"
)

# Calculate scroll position to show this tile's content
scroll_y = dashboard_top + (i * tile_height)

page.evaluate(f"window.scrollTo(0, {scroll_y})")
logger.debug(
"Scrolled window to %s for tile %s/%s", scroll_y, i + 1, num_tiles
"Scrolled window to %s for tile %s/%s%s",
scroll_y,
i + 1,
num_tiles,
context_suffix,
)
# Wait for scroll to settle and content to load
page.wait_for_timeout(SCROLL_SETTLE_TIMEOUT_MS)
# Wait for every chart holder visible in the current viewport to reach
# a terminal state (rendered chart or error/empty state). Only check
# a terminal state (rendered chart or error/empty state), capped at
# whatever remains of the total time budget so a slow dashboard
# degrades gracefully instead of exceeding it. Only check
# viewport-visible chart holders to avoid blocking on virtualization
# placeholders rendered for off-screen charts. A holder that hasn't
# mounted anything yet does not satisfy this check -- unlike checking
# for the absence of `.loading`, which passes vacuously in that case.
tile_load_wait = min(load_wait, remaining_budget)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggestion: The remaining budget is computed before the mandatory scroll-settle delay, but then reused for wait_for_function without recalculating. When the budget is nearly exhausted, this can overrun the intended total budget by at least the settle delay (and potentially trigger Celery time-limit kills again). Recompute remaining_budget after page.wait_for_timeout(...) (or include settle wait in the cap) before deriving the per-tile readiness timeout. [logic error]

Severity Level: Major ⚠️
- ⚠️ Tiled screenshot budget enforcement can overshoot configured budget.
- ⚠️ Long-running report captures may still approach Celery limits.
- ⚠️ Logging claims strict capping but implementation exceeds budget.
Steps of Reproduction ✅
1. In `superset/utils/screenshot_utils.py`, note the tiled capture loop in
`take_tiled_screenshot()` starting around line 351, where `tile_start`, `elapsed`, and
`remaining_budget` are computed (lines 356-359) and checked against zero (line 360).

2. Still in the same function, observe that after the budget check, the code scrolls and
then waits for scroll settle via `page.wait_for_timeout(SCROLL_SETTLE_TIMEOUT_MS)` (lines
385-396), and only afterwards calls `page.wait_for_function(...)` using `tile_load_wait =
min(load_wait, remaining_budget)` (line 405), where `remaining_budget` is the value
computed before the settle wait.

3. Write a unit test or small harness that calls `take_tiled_screenshot()` (function
defined starting around line 272 in this file) with:

   - `_resolve_wait_budget_seconds()` (lines 80-133) patched to return a small budget, for
   example 2.0 seconds.

   - `load_wait` set to a larger value, for example 60.

   - A fake or real Playwright `Page` whose `wait_for_timeout` implementation actually
   sleeps for `SCROLL_SETTLE_TIMEOUT_MS` (1000 milliseconds), and whose
   `wait_for_function` can also sleep for the requested timeout.

4. In that test, drive enough elapsed time before entering a given tile iteration (for
example by patching `time.monotonic()` or by performing real sleeps) so that, at the
budget check on line 356, `remaining_budget` is a small positive value (for example 0.1
seconds). Then let the code run:

   - The loop passes the `remaining_budget > 0` check and logs the warning context.

   - It calls `page.wait_for_timeout(SCROLL_SETTLE_TIMEOUT_MS)` (line 396), consuming 1.0
   additional seconds while still holding the stale `remaining_budget` value.

   - It then computes `tile_load_wait = min(load_wait, remaining_budget)` (line 405) using
   that stale value and passes it to `page.wait_for_function`, so the total elapsed time
   since `start_time` exceeds the intended budget `wait_budget_seconds` by at least the
   scroll-settle duration, demonstrating that the total budget is not actually capped by
   `remaining_budget` as documented.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** superset/utils/screenshot_utils.py
**Line:** 356:405
**Comment:**
	*Logic Error: The remaining budget is computed before the mandatory scroll-settle delay, but then reused for `wait_for_function` without recalculating. When the budget is nearly exhausted, this can overrun the intended total budget by at least the settle delay (and potentially trigger Celery time-limit kills again). Recompute `remaining_budget` after `page.wait_for_timeout(...)` (or include settle wait in the cap) before deriving the per-tile readiness timeout.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

@bito-code-review bito-code-review Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review Agent Run #1c7538

Actionable Suggestions - 1
  • tests/unit_tests/utils/test_screenshot_utils.py - 1
Additional Suggestions - 2
  • superset/mcp_service/screenshot/pooled_screenshot.py - 1
    • Unused parameter dead code · Line 52-57
      The `log_context` parameter is accepted but never used — it is not forwarded to `_get_screenshot_internal()` (line 71-73) nor any inner driver call. Accepting a parameter with no functional use is dead code that creates API surface noise.
  • superset/utils/screenshots.py - 1
    • Missing parameter docstring · Line 192-197
      The `log_context` parameter on `driver()` lacks documentation. Compare to `WebDriverProxy.get_screenshot()` which documents it as: `:param log_context: Optional identifier (e.g. report execution id, or a cache key for thumbnails) included in log lines for tracing.`
Review Details
  • Files reviewed - 6 · Commit Range: 8db513a..d64e368
    • superset/mcp_service/screenshot/pooled_screenshot.py
    • superset/utils/screenshot_utils.py
    • superset/utils/screenshots.py
    • superset/utils/webdriver.py
    • tests/unit_tests/utils/test_screenshot_utils.py
    • tests/unit_tests/utils/webdriver_test.py
  • Files skipped - 0
  • Tools
    • MyPy (Static Code Analysis) - ✔︎ Successful
    • Astral Ruff (Static Code Analysis) - ✔︎ Successful
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers a full AI review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

AI Code Review powered by Bito Logo

Comment on lines +815 to +819
@property
def request(self):
raise RuntimeError("boom")

with patch("superset.utils.screenshot_utils.current_task", _BrokenTask()):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Exception uses string literal directly

Exception raised with string literal directly instead of assigning to a variable first.

Code suggestion
Check the AI-generated fix before applying
Suggested change
@property
def request(self):
raise RuntimeError("boom")
with patch("superset.utils.screenshot_utils.current_task", _BrokenTask()):
def request(self):
error_msg = "boom"
raise RuntimeError(error_msg)
with patch("superset.utils.screenshot_utils.current_task", _BrokenTask()):

Code Review Run #1c7538


Should Bito avoid suggestions like this for future reviews? (Manage Rules)

  • Yes, avoid them

@bito-code-review

bito-code-review Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Code Review Agent Run #0f1bed

Actionable Suggestions - 0
Filtered by Review Rules

Bito filtered these suggestions based on rules created automatically for your feedback. Manage rules.

  • superset/utils/screenshot_utils.py - 1
    • Documentation contradicts Celery API · Line 96-96
Review Details
  • Files reviewed - 3 · Commit Range: d64e368..245cb8b
    • superset/utils/screenshot_utils.py
    • tests/unit_tests/utils/test_screenshot_utils.py
    • tests/unit_tests/utils/webdriver_test.py
  • Files skipped - 0
  • Tools
    • MyPy (Static Code Analysis) - ✔︎ Successful
    • Astral Ruff (Static Code Analysis) - ✔︎ Successful
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers a full AI review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

AI Code Review powered by Bito Logo

@eschutho

Copy link
Copy Markdown
Member Author

Thanks for the review, @rusackas — went through the bot threads:

  • CodeAnt's "stale remaining_budget" finding (Major, both threads): confirmed real. remaining_budget was computed once per tile before the scroll-settle sleep, then reused (stale) to cap the readiness wait after it, letting each tile overrun the budget by up to one settle interval. Fixed in bcb5957: recompute elapsed/remaining budget after the settle sleep (and re-check for exhaustion there too) before deriving the readiness timeout. Added a regression test (test_readiness_wait_uses_budget_recomputed_after_scroll_settle) that fails against the old code.
  • Bito's "Documentation contradicts Celery API" (filtered/muted, screenshot_utils.py:96): this turned out to be a real bug, not just docs — current_task.request.timelimit is (time_limit, soft_time_limit), i.e. (hard, soft), confirmed against celery.app.amqp/celery.app.task.Context/celery.worker.strategy source. My original unpacking had it backwards. Joe independently caught and fixed the same thing in 245cb8b before I got to it — my rebase picked that up, and I aligned the mock/tests to match Celery's real tuple order (not just my implementation) so a regression here would fail the test.
  • Bito's "test coverage gap for log_context" (webdriver.py): added test_tiled_path_forwards_non_none_log_context, asserting a non-None log_context passed to WebDriverPlaywright.get_screenshot() reaches take_tiled_screenshot() unchanged.
  • CodeAnt's type-annotation nits on test fixtures/methods: leaving these as-is per your comment that they're not necessary.
  • Pre-commit / ruff-format: clean against the pinned ruff 0.9.7 (verified directly, not just my sandbox's older local ruff, which briefly fought your formatting fix in the other direction — reverted before pushing).

Pushed in bcb5957. CI failures I see now (playwright-tests-experimental) look unrelated — an embedded-dashboard pivot-table test, not touching this code path.

@netlify

netlify Bot commented Jul 23, 2026

Copy link
Copy Markdown

Deploy Preview for superset-docs-preview ready!

Name Link
🔨 Latest commit ebf735c
🔍 Latest deploy log https://app.netlify.com/projects/superset-docs-preview/deploys/6a6ce196a615a10008d7437a
😎 Deploy Preview https://deploy-preview-42118--superset-docs-preview.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@bito-code-review

bito-code-review Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Code Review Agent Run #608ba4

Actionable Suggestions - 0
Review Details
  • Files reviewed - 3 · Commit Range: 245cb8b..bcb5957
    • superset/utils/screenshot_utils.py
    • tests/unit_tests/utils/test_screenshot_utils.py
    • tests/unit_tests/utils/webdriver_test.py
  • Files skipped - 0
  • Tools
    • MyPy (Static Code Analysis) - ✔︎ Successful
    • Astral Ruff (Static Code Analysis) - ✔︎ Successful
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers a full AI review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

AI Code Review powered by Bito Logo

Comment on lines +482 to +490
animation_wait_elapsed = 0.0
if animation_wait > 0:
page.wait_for_timeout(animation_wait * 1000)
elapsed = time.monotonic() - start_time
remaining_budget = wait_budget_seconds - elapsed
tile_animation_wait = max(0, min(animation_wait, remaining_budget))
if tile_animation_wait > 0:
animation_wait_start = time.monotonic()
page.wait_for_timeout(tile_animation_wait * 1000)
animation_wait_elapsed = time.monotonic() - animation_wait_start

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggestion: After the capped animation wait completes, the code proceeds directly to page.screenshot() without checking the remaining budget. If the animation wait consumes the final available time, the tile is still captured after the hard budget has expired, and if this is the last tile the function returns successfully despite exceeding the task limit. Recheck the budget before capture and raise TiledScreenshotBudgetExceededError when it is exhausted. [incorrect control flow]

Severity Level: Major ⚠️
- ❌ Tiled report capture can exceed its safe task budget.
- ⚠️ Celery cleanup time can be consumed by screenshot capture.
- ⚠️ Last tiles may succeed after budget exhaustion.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** superset/utils/screenshot_utils.py
**Line:** 482:490
**Comment:**
	*Incorrect Control Flow: After the capped animation wait completes, the code proceeds directly to `page.screenshot()` without checking the remaining budget. If the animation wait consumes the final available time, the tile is still captured after the hard budget has expired, and if this is the last tile the function returns successfully despite exceeding the task limit. Recheck the budget before capture and raise `TiledScreenshotBudgetExceededError` when it is exhausted.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

@eschutho
eschutho force-pushed the fix-tile-wait-budget branch from ebf735c to dc3672b Compare July 31, 2026 20:26
@bito-code-review

bito-code-review Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Code Review Agent Run #165fdb

Actionable Suggestions - 0
Filtered by Review Rules

Bito filtered these suggestions based on rules created automatically for your feedback. Manage rules.

  • tests/unit_tests/utils/test_screenshot_utils.py - 1
Review Details
  • Files reviewed - 3 · Commit Range: dc3672b..dc3672b
    • superset/utils/screenshot_utils.py
    • superset/utils/webdriver.py
    • tests/unit_tests/utils/test_screenshot_utils.py
  • Files skipped - 0
  • Tools
    • MyPy (Static Code Analysis) - ✔︎ Successful
    • Astral Ruff (Static Code Analysis) - ✔︎ Successful
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers a full AI review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

AI Code Review powered by Bito Logo

… hitting Celery kill

Cumulative per-tile readiness waits in take_tiled_screenshot() have no
bound tied to the running Celery task's time limit: each tile's
wait_for_function runs at the full configured load_wait, so a slow
dashboard with N tiles can wait up to N * load_wait and get SIGKILLed
mid-capture (SoftTimeLimitExceeded) instead of the report failing
cleanly and notifying owners.

Reworked from this PR's original revision to sit on top of the merged
#42253/#42427 readiness work and reuse its runtime budget helper rather
than shipping a second one:

- Derive one wall-clock budget for the whole tiled operation from
  resolve_screenshot_task_budget_seconds() (the #42427 helper the
  non-tiled path already uses). When it returns None (no Celery task
  context, e.g. synchronous thumbnail generation), fall back to a fixed
  total ceiling (TILED_SCREENSHOT_TOTAL_WAIT_BUDGET_SECONDS = 1440s)
  instead of "keep the configured timeout" -- unlike the non-tiled
  path's single wait, per-tile waits accumulate, so the operation still
  needs one bounded total.
- Cap each tile's readiness-wait timeout at the remaining budget,
  recomputed after the mandatory scroll-settle sleep (which itself
  consumes wall-clock time) so a tile can't overrun by up to one settle
  interval. Cap or skip the cosmetic per-tile animation wait the same
  way.
- Raise TiledScreenshotBudgetExceededError the moment the budget is
  exhausted -- before any further tile is captured -- and exempt it from
  the function's return-None fallback so callers fail the report loudly
  instead of receiving a partial/unchecked screenshot. No budget floor:
  a budget already exhausted by setup raises before the first tile,
  matching the non-tiled path's raise-before-capture semantics.
- Make the new error a subclass of ScreenshotTaskBudgetExceededError
  (moved to screenshot_utils.py, re-exported from webdriver.py) so
  callers can catch the whole budget-error family with one type.
- Enrich the per-tile timeout WARNING with budget context (waited vs
  requested load_wait, elapsed vs total budget, tiles captured) and add
  a per-tile DEBUG timing breakdown so slow dashboards can be profiled
  from logs alone.

Co-Authored-By: Claude <noreply@anthropic.com>
@eschutho
eschutho force-pushed the fix-tile-wait-budget branch from dc3672b to df77884 Compare July 31, 2026 21:53
@eschutho

Copy link
Copy Markdown
Member Author

Went through the two open CodeAnt findings against the current (reworked) revision:

1. "Stale remaining_budget reused after the scroll-settle sleep" — already fixed; the comment references the previous revision. Its line anchors point at code that no longer exists (e.g. _resolve_wait_budget_seconds, removed when this PR was reduced to reuse #42427's shared budget helper). The current revision recomputes elapsed/remaining after page.wait_for_timeout(SCROLL_SETTLE_TIMEOUT_MS) and re-checks exhaustion there before deriving tile_load_wait — see the "Recompute the remaining budget after the scroll-settle sleep" block in take_tiled_screenshot, pinned by test_readiness_wait_uses_budget_recomputed_after_scroll_settle. Resolving as fixed.

2. "No budget re-check between the animation wait and page.screenshot()" — accurate as a control-flow observation, but intentional; declining the suggested change. Reasoning:

  • The animation wait is capped at min(animation_wait, remaining_budget), so it can drain the budget to exactly zero but cannot overshoot it — and the budget is task limit − cleanup margin (min(300s, 20%)), so even at full consumption the entire margin remains for capture/combine/PDF/delivery, which is precisely what the margin exists to cover. The finding's claim that the function can return "despite exceeding the task limit" doesn't hold: no wait in this path can run past the budget, let alone the limit.
  • The tile captured after that wait is fully readiness-verified — its capped readiness wait already completed within budget. page.screenshot() is a synchronous snapshot, not a wait. The budget-exhaustion raise exists to prevent unverifiable future waits, which doesn't apply here.
  • The only behavioral effect of adding the suggested re-check would be on the last tile: a complete, verified, within-budget capture would be converted into a hard report failure at the finish line (mid-loop tiles are already covered by the next iteration's top-of-loop check, which raises before any work on the next tile). That trades a rare successful report for a failure with no reduction in SIGKILL risk.
  • This is the documented design in the code: the animation wait is "cosmetic settling, not a readiness check, so we simply skip it (rather than raise) once the budget runs out."

Also in the latest push: refreshed a comment in the budget-exhaustion except clause that still described the pre-#42273 caller behavior ("callers treat return None as fall back to a standard, unchecked screenshot") — since #42273 merged, callers fail the capture loudly instead. No behavior change; CI re-running on the amended head.

@aminghadersohi aminghadersohi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reviewed at df77884. Full scan of the tiled budget path, both CRUX behaviors traced to boundaries, and all 10 open bot threads adjudicated on merit. No net-new issues above NIT; CI shows no failures (several jobs still pending). Not approving solely because 10 reviewer threads remain unresolved — all adjudicated below as non-blocking (nits / false-positives / already-addressed at HEAD).

Budget math + None/falsy-zero degrade — correct. wait_budget_seconds = resolve_screenshot_task_budget_seconds(...), then if wait_budget_seconds is None: wait_budget_seconds = float(TILED_SCREENSHOT_TOTAL_WAIT_BUDGET_SECONDS). Per tile: remaining_budget = wait_budget_seconds - (time.monotonic() - start_time), tile_load_wait = min(load_wait, remaining_budget).

  • in-task: derived from request.timelimit (soft preferred, else hard) minus min(300, limit*0.2) margin.
  • no-task / None: degrades to the fixed 1440s ceiling — not 0, not unbounded. The is None check (not if not budget) correctly avoids conflating a legitimate 0 budget with unset.
  • exhausted (<= 0): _raise_if_budget_exhausted uses strict > 0, so a 0/negative remaining raises immediately on the pre-tile check.
  • tiny-limit / margin≈budget: budget = max(0.0, limit - margin) floors at 0, which then raises cleanly on the first check.

Cleanup margin before the hard kill — correct. The budget subtracts the same 300s (or 20%) margin the runtime derivation reserves, so the clean raise fires well before Celery's hard limit, leaving room for combine + PDF + delivery. The margin, not a bare == hard_limit, is what makes the SIGKILL avoidable.

Fail-loud — correct, no swallowing. except TiledScreenshotBudgetExceededError: raise is ordered before the generic except Exception: return None, so exhaustion is never degraded to an anonymous None. At the caller (webdriver.py:554) the error is a RuntimeError subclass — neither PlaywrightTimeout nor PlaywrightError — so it propagates past the except clauses at 658–663 and out through finally: context.close() to fail the capture loudly.

Non-tiled / #42253 non-regression — correct. webdriver.py is a pure refactor: the ScreenshotTaskBudgetExceededError class moved into screenshot_utils.py and is re-imported. The non-tiled readiness raise (webdriver.py:368) and its #42427 degrade guard are untouched; the tiled error subclasses the shared base so it fails the report the same way.

Tests — non-vacuous. Reverting the raise guard fails exactly the three raise-on-exhaustion tests (Rule 26 holds). Coverage includes raise-on-exhaustion, None-degrade to fixed ceiling, budget recomputed after scroll-settle, in-Celery derivation (real 120→96s margin math, not a mock echo), and within-budget success. The _FakeClock + real resolve_screenshot_task_budget_seconds (patching only current_task/monotonic) asserts against a real computed budget. seconds→ms (* 1000) is consistent throughout.

Open threads (10, all unresolved):

  • Threads 1–5 (type-annotation nits on the constant + test methods): cosmetic and consistent with the file's existing unannotated constants/tests. Non-blocking.
  • Thread 6 (1440 vs per-schedule time_limit): false positive — the budget is derived from current_task.request.timelimit, which reflects per-invocation overrides; 1440 is only the no-task fallback.
  • Threads 7 & 8 (stale budget before scroll-settle): already addressed at HEAD — remaining_budget is recomputed after wait_for_timeout(SCROLL_SETTLE_TIMEOUT_MS) (lines 415–418) and the readiness cap uses the refreshed value (line 428), with a dedicated regression test. These reference pre-fix commits.
  • Thread 9 (f-string in raise): stylistic, non-issue.
  • Thread 10 (capture after animation wait without re-check): false positive — the animation wait is capped at remaining_budget, so it ends at the budget rather than past it, and the reserved 300s margin covers the in-flight tile capture + combine + delivery; the tile's readiness was already verified before the wait.

Recommend resolving the above threads; no code change is required for any of them.

wait_budget_seconds = resolve_screenshot_task_budget_seconds(log_context)
if wait_budget_seconds is None:
wait_budget_seconds = float(TILED_SCREENSHOT_TOTAL_WAIT_BUDGET_SECONDS)
start_time = time.monotonic()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Non-blocking (owner's call) — tiled budget clock resets instead of accounting for pre-capture time.

wait_budget_seconds is derived from the Celery task limit, but elapsed is measured from this local start_time, so page.goto (bounded 60s), the 3s SELENIUM_HEADSTART, the 30s element.wait_for, and dimension probing all run before the clock starts — the tiled path effectively gets a fresh full budget. The non-tiled _wait_for_charts_ready avoids this by threading screenshot_started_at from the top of get_screenshot and subtracting already-elapsed time; the tiled call site doesn't pass it.

Not a correctness bug: in the common case the 20%/300s margin (and the soft→hard gap) absorbs the difference, and a soft-limit overrun is still caught cleanly as SoftTimeLimitExceeded rather than the SIGKILL this PR targets. It's just a looser guarantee than the non-tiled path — worth a conscious decision.

If you want both paths on one clock, it's 3 coordinated one-liners (⚠️ applying the suggestion below alone will NameError until the other two land):

  1. add screenshot_started_at: float | None = None to the take_tiled_screenshot signature;
  2. the change below;
  3. pass screenshot_started_at=screenshot_started_at from the take_tiled_screenshot(...) call in webdriver.py.
Suggested change
start_time = time.monotonic()
start_time = (
screenshot_started_at if screenshot_started_at is not None else time.monotonic()
)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good catch, and agreed it deserved both-paths-on-one-clock: implemented exactly as suggested (all three coordinated changes) in follow-up #42661, with tests pinning that pre-capture elapsed time now reduces the first tile's capped wait, and that the omitted-anchor default is unchanged.

eschutho added a commit that referenced this pull request Jul 31, 2026
…eenshot capture logs

The screenshot capture code is reached by two call paths that identify
their runs differently: scheduled reports carry an execution_id (already
threaded end-to-end via log_context since #42253), while thumbnails and
direct PDF/screenshot downloads are identified by their cache_key -- which
was never passed down. BaseScreenshot.compute_and_cache had the cache_key
in hand and get_screenshot already accepted a log_context parameter, but
the two were never connected, so every capture-layer log line produced by
a thumbnail or direct-download run is anonymous: there is no way to join
"trying to generate screenshot" / webdriver navigation / readiness /
capture-result log lines to the cached digest they were computing.

This threads the existing optional log_context through the remaining
capture-layer log lines, and populates it on the thumbnail path:

- screenshots.py: compute_and_cache passes
  log_context=f"cache_key={cache_key}" into get_screenshot and
  resize_image; the thumbnail lifecycle log lines (generate/fail/resize/
  cache-updated) now include the cache_key; driver() accepts log_context
  for its Playwright-unavailable fallback notice.
- webdriver.py: the non-tiled Playwright log lines (navigation, headstart,
  element/chart-container waits, screenshot result), the entire
  WebDriverSelenium.get_screenshot path, and find_unexpected_errors (both
  engines) now append the context suffix.
- screenshot_utils.py: the non-budget tiled log lines (dimensions, tile
  count, scroll, capture, skip, combine) and combine_screenshot_tiles gain
  the same suffix.

Log-line/plumbing only -- no behavior change. Split out of #42118 per its
scope reduction to tiled-path budgeting; the readiness log lines added by

Co-Authored-By: Claude <noreply@anthropic.com>

@rebenitez1802 rebenitez1802 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

left a comment otherwise, LGTM

@eschutho
eschutho merged commit 6929d03 into master Jul 31, 2026
60 checks passed
@eschutho
eschutho deleted the fix-tile-wait-budget branch July 31, 2026 22:37
@bito-code-review

Copy link
Copy Markdown
Contributor

Bito Automatic Review Skipped – PR Already Merged

Bito scheduled an automatic review for this pull request, but the review was skipped because this PR was merged before the review could be run.
No action is needed if you didn't intend to review it. To get a review, you can type /review in a comment and save it

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

Labels

alert-reports Namespace | Anything related to the Alert & Reports feature preset-io size/L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants