Skip to content

fix(flights): a deferred tile is not a failed one - #292

Merged
ChuckBuilds merged 1 commit into
mainfrom
fix/flights-tile-deferral
Aug 18, 2026
Merged

fix(flights): a deferred tile is not a failed one#292
ChuckBuilds merged 1 commit into
mainfrom
fix/flights-tile-deferral

Conversation

@ChuckBuilds

@ChuckBuilds ChuckBuilds commented Aug 18, 2026

Copy link
Copy Markdown
Owner

Follow-up to #284, which is now merged. Rebuilt on top of main so this is only the fix.

The bug

#284 moved tile fetching off the render thread — the right idea — so the render path now asks for tiles that aren't cached yet and gets None back. But the compositing loop counts those the same as a tile the server refused to serve, and the failure branch disables the map background for the rest of the session above 50%.

On a cold cache every tile is a deferral. Reproduced against the merged code, 110-tile grid:

FRAME 1  cold cache:                  returned=None   map_bg_enabled=True
FRAME 2  prefetch delivered 10/110:   returned=None   map_bg_enabled=False
FRAME 3  ALL 110 tiles cached:        returned=None   map_bg_enabled=False

Below the 50% bar it fails the other way — the partial composite is cached under a key of (centre, zoom, size), all static config, so the invalidation can never fire:

FRAME 2  60/110 present (45% missing): image returned, 46.8% black  -> cached
FRAME 3  ALL 110 tiles cached:         46.8% black, 0 tiles re-read
         identical object to frame 2:  True

Either way the map is gone for the session and only a config reload clears it. Before #284 this was unreachable: the render thread fetched inline, so a healthy server meant a 0% failure rate. The queue is only populated by a render, so every cold start goes through this — not an edge case.

The fix

  • Deferrals counted separately from failures. Only a genuine fetch failure (allow_network=True) counts toward the disable threshold, so the safety valve still fires for a server that really is refusing.
  • A composite with any tile missing is not cached. Costs one recomposition per frame from already-cached tiles until the prefetch catches up, then nothing.
  • _MAX_TILES_WANTED 64 → 256. The grid reaches 16×16 (max_tiles = 16 is applied per axis, despite the # 4x4 maximum comment beside it) and a default config already needs 110, so the render path could never queue a whole map in one pass — which kept frames partial long enough to trip the two bugs above.
  • Prefetch bounded at 10s per cycle, remainder returned to the queue. It previously drained the whole queue at _TILE_TIMEOUT_SECONDS = 3 each against PluginExecutor's 30s budget for the entire update, so a slow-but-succeeding server could overrun it and be recorded as a failed update. The existing cooldown only rescues a server that fails outright, not one that's merely slow.

Testing

New test_map_cold_start.py drives the real _get_map_background through a startup sequence. The existing test asserts by AST that display() passes allow_network=False — true, and never the problem; it never executes the function where the bug lives.

It follows the LEDMATRIX_CORE skip convention the sibling tests use, so it imports and exercises the real FlightTrackerPlugin (including the read-only display_width/display_height properties) rather than compiling methods out of the file.

Mutation-checked: 6 of its 8 checks fail against main. The 2 that pass are the behaviours deliberately preserved — the failure valve, and that a complete map is still cached.

Full flights suite passes: test_config_reload, test_map_cold_start, test_no_disk_write_on_poll, test_overhead_radius, test_tile_network_bound, test_tiles_off_render_thread, test_vegas_map_parity.

Manifest 1.12.10 → 1.12.11, plugins.json regenerated by the pre-commit hook.

Scope note

A total fetch failure returns earlier, at tiles_fetched == 0, without disabling anything. That predates #284 and is covered by the fetch cooldown, so I left it rather than widen the change — my first draft of the test assumed otherwise and caught me.

🤖 Generated with Claude Code

https://claude.ai/code/session_01STMbQE4YctTacQXfbYqKuW

@coderabbitai

coderabbitai Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b5e01c87-bd1a-4f49-afbb-22af888c557f


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codacy-production

codacy-production Bot commented Aug 18, 2026

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 13 complexity

Metric Results
Complexity 13

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@ChuckBuilds
ChuckBuilds force-pushed the fix/flights-tile-deferral branch from b6e24a0 to 2bc8301 Compare August 18, 2026 19:42
@ChuckBuilds ChuckBuilds changed the title fix(flights): fetch map tiles off the render thread, without losing the map fix(flights): a deferred tile is not a failed one Aug 18, 2026
#284 moved tile fetching off the render thread, so the render path now asks
for tiles that are not in the cache yet and gets None back. The compositing
loop counted those the same as a tile the server refused to serve, and the
failure branch disables the map background for the rest of the session
above 50%.

On a cold cache every tile is a deferral. Reproduced against the merged
code with a 110-tile grid: frame one returns nothing (protected by the
tiles_fetched == 0 early return), frame two sees 10 of 110 tiles and
switches the map off permanently, and frame three with all 110 cached
still returns None. Below the 50% bar it goes wrong the other way -- the
partial composite is cached under a key of centre, zoom and size, all of
which come from static config, so the invalidation never fires: a map
measured 46.8% black came back as the identical object after every tile was
available, re-reading no tiles. Either way the map is gone for the session
and only a config reload clears it.

Deferrals are now tracked separately and only genuine fetch failures count
toward the disable threshold, so the safety valve still fires for a server
that is actually refusing. A composite with any tile missing is not cached,
which costs a recomposition per frame from already-cached tiles until the
prefetch catches up, and nothing after that.

Two supporting fixes. _MAX_TILES_WANTED was 64 against a grid that reaches
16x16 and already needs 110 by default, so the render path could never
queue a whole map in one pass -- that is what kept frames partial long
enough to trip the two bugs above; it is now 256. And the prefetch drained
its whole queue with a 3s timeout per tile against PluginExecutor's 30s
budget for the entire update, so a slow-but-succeeding server could overrun
it and be recorded as a failed update; it now stops at 10s and puts the
remainder back on the queue. The existing cooldown only rescued the case
where the server fails outright, not where it is merely slow.

The new test drives the real _get_map_background through a startup
sequence. The existing test checks by AST that display() passes
allow_network=False, which is true and was never the problem -- it never
executes the function where the bug lives. It follows the LEDMATRIX_CORE
skip convention the sibling tests use, so it exercises the real class
rather than compiling methods out of the file.

Mutation-checked: 6 of its 8 checks fail against main, and the two that
pass are the behaviours deliberately preserved (the failure valve, and that
a complete map is still cached). Full flights suite passes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STMbQE4YctTacQXfbYqKuW
@ChuckBuilds
ChuckBuilds force-pushed the fix/flights-tile-deferral branch from 2bc8301 to 1c970ac Compare August 18, 2026 19:45
@ChuckBuilds
ChuckBuilds merged commit 73e02d6 into main Aug 18, 2026
4 checks passed
@ChuckBuilds
ChuckBuilds deleted the fix/flights-tile-deferral branch August 18, 2026 19:55
ChuckBuilds pushed a commit that referenced this pull request Aug 19, 2026
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