mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-10 17:16:36 +00:00
feat(vegas): prefetch_gate on by default, after an A/B/C soak on hdpi
Two runs per arm, about 81,000 frames each, order A B C C B A: A step 1 as is 0.90% late, 20.1 per 10k two+ refreshes late B switch_interval_ms 1 0.78% late, 15.8 per 10k C prefetch_gate 0.60% late, 2.5 per 10k No freezes in any arm, and the next group was ready at every strip extension, so parking the prefetch thread (3-6s per 8-minute run) cost nothing visible. The gate is now on unless vegas_scroll.prefetch_gate is false; on a stock binding it cannot work and says so at INFO once a run rather than warning on every install. switch_interval_ms stays off. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -128,6 +128,8 @@ Read by `src/vegas_mode/config.py` (`VegasScrollConfig.from_config`). See
|
|||||||
| `min_cut_gap` | int, `6` |
|
| `min_cut_gap` | int, `6` |
|
||||||
| `continuous_scroll` | bool, `true` |
|
| `continuous_scroll` | bool, `true` |
|
||||||
| `offscreen_prefetch` | bool, `true` — render every plugin's ticker content on the background thread, each on its own canvas. `false` restores handing canvas-bound plugins to the render thread, one pause at a time. Temporary; see [OFFSCREEN_RENDERING.md](OFFSCREEN_RENDERING.md) |
|
| `offscreen_prefetch` | bool, `true` — render every plugin's ticker content on the background thread, each on its own canvas. `false` restores handing canvas-bound plugins to the render thread, one pause at a time. Temporary; see [OFFSCREEN_RENDERING.md](OFFSCREEN_RENDERING.md) |
|
||||||
|
| `prefetch_gate` | bool, `true` — let that background thread run Python only while the render thread is waiting for the panel, so the render thread never waits for the GIL when a refresh comes round. Only takes effect with the rebuilt rgbmatrix binding (`scripts/build_rgbmatrix_nogil.sh`). See [OFFSCREEN_RENDERING.md](OFFSCREEN_RENDERING.md) |
|
||||||
|
| `switch_interval_ms` | float, `0` — experimental: shorten Python's GIL switch interval to this many ms while Vegas runs. `0` leaves the default (5 ms) alone |
|
||||||
| `smooth_scroll` | bool, `true` — move a whole number of pixels per panel refresh, locked to vsync. `scroll_speed` is snapped to the nearest speed the panel can show that way (at 95Hz: 95, 47.5, 31.7 px/s…), measured against the panel's real refresh rate once scrolling starts |
|
| `smooth_scroll` | bool, `true` — move a whole number of pixels per panel refresh, locked to vsync. `scroll_speed` is snapped to the nearest speed the panel can show that way (at 95Hz: 95, 47.5, 31.7 px/s…), measured against the panel's real refresh rate once scrolling starts |
|
||||||
| `sub_pixel_blend` | bool, `false` — the older smoothing: advance by elapsed time and blend neighbouring pixel columns. Looks anti-aliased in the web preview but shimmers on the panel and is not locked to the refresh. Overrides `smooth_scroll` when on |
|
| `sub_pixel_blend` | bool, `false` — the older smoothing: advance by elapsed time and blend neighbouring pixel columns. Looks anti-aliased in the web preview but shimmers on the panel and is not locked to the refresh. Overrides `smooth_scroll` when on |
|
||||||
| `extend_threshold_screens` | float, `2.0` |
|
| `extend_threshold_screens` | float, `2.0` |
|
||||||
|
|||||||
@@ -21,6 +21,28 @@ moved to the prefetch thread still needs the GIL, and the render thread waits
|
|||||||
for it (risk 5 below). The late rate did not improve overall. The 1–2 s
|
for it (risk 5 below). The late rate did not improve overall. The 1–2 s
|
||||||
freezes appear in both builds and have a separate, not yet identified cause.
|
freezes appear in both builds and have a separate, not yet identified cause.
|
||||||
|
|
||||||
|
The GIL fix, measured on hdpi (90 px/s, `pwm_bits` 8, preview open, 8-minute
|
||||||
|
runs after a 2-minute warm-up, order A B C C B A, 2026-09-24). Each arm pools
|
||||||
|
two runs, about 81,000 frames:
|
||||||
|
|
||||||
|
| arm | late | by 1 | 2 | 3–5 | 6+ | 2+ late per 10k frames | freezes |
|
||||||
|
|---|---|---|---|---|---|---|---|
|
||||||
|
| A: step 1 as is | 0.90% | 575 | 64 | 91 | 9 | 20.1 | 0 |
|
||||||
|
| B: `switch_interval_ms` 1 | 0.78% | 510 | 105 | 23 | 2 | 15.8 | 0 |
|
||||||
|
| C: `prefetch_gate` | **0.60%** | 471 | 11 | 7 | 2 | **2.5** | 0 |
|
||||||
|
|
||||||
|
The gate removes the frames the render thread spent waiting for the GIL, and
|
||||||
|
it costs the prefetch nothing that shows: it parked the thread for 3–6 s per
|
||||||
|
run, and the next group was ready at every strip extension in every arm.
|
||||||
|
`prefetch_gate` is therefore on by default; `switch_interval_ms` stays an
|
||||||
|
off-by-default experiment. What is left is almost all one refresh late, which
|
||||||
|
is the per-frame budget (a 6.75 ms p50 blit in a refresh the panel holds at
|
||||||
|
83–85 Hz while rendering), not contention.
|
||||||
|
|
||||||
|
The runs restart the service, so the hourly sports refresh never fell inside
|
||||||
|
one. That refresh is its own case: about twenty ESPN chunk-fetch threads at
|
||||||
|
once, which the gate does not cover (it gates only the prefetch thread).
|
||||||
|
|
||||||
## The problem
|
## The problem
|
||||||
|
|
||||||
Vegas mode builds its ticker from every plugin's content. Most of that work
|
Vegas mode builds its ticker from every plugin's content. Most of that work
|
||||||
@@ -285,9 +307,8 @@ updates are disabled while sync is active.
|
|||||||
it, and a waiting thread only gets it back after the switch interval
|
it, and a waiting thread only gets it back after the switch interval
|
||||||
(default 5 ms). Expect some single-refresh late frames while a prefetch
|
(default 5 ms). Expect some single-refresh late frames while a prefetch
|
||||||
runs. Measure with the soak. A render process separate from plugin work
|
runs. Measure with the soak. A render process separate from plugin work
|
||||||
is the structural answer (the "native presenter" step). Two opt-in
|
is the structural answer (the "native presenter" step). Two experiments
|
||||||
experiments try to get most of the way first, both off by default until
|
get most of the way first (results under Status, above):
|
||||||
the soak says otherwise:
|
|
||||||
- `vegas_scroll.switch_interval_ms` lowers the switch interval for a Vegas
|
- `vegas_scroll.switch_interval_ms` lowers the switch interval for a Vegas
|
||||||
run (1 ms is the obvious try), so the render thread waits at most that
|
run (1 ms is the obvious try), so the render thread waits at most that
|
||||||
long behind bytecode. It does nothing for a C call that keeps the GIL.
|
long behind bytecode. It does nothing for a C call that keeps the GIL.
|
||||||
@@ -297,7 +318,8 @@ updates are disabled while sync is active.
|
|||||||
parks it the rest of the time. That covers C calls too, since the gate is
|
parks it the rest of the time. That covers C calls too, since the gate is
|
||||||
checked before each one starts. It never parks the thread while it holds
|
checked before each one starts. It never parks the thread while it holds
|
||||||
a lock the render thread takes, and never for more than 50 ms. It needs
|
a lock the render thread takes, and never for more than 50 ms. It needs
|
||||||
the rebuilt binding, which releases the GIL during the swap.
|
the rebuilt binding, which releases the GIL during the swap. On by
|
||||||
|
default.
|
||||||
|
|
||||||
## What this does not fix
|
## What this does not fix
|
||||||
|
|
||||||
|
|||||||
@@ -87,16 +87,17 @@ class VegasModeConfig:
|
|||||||
# 5ms. Plugin rendering on the prefetch thread and plugin updates hold the
|
# 5ms. Plugin rendering on the prefetch thread and plugin updates hold the
|
||||||
# GIL in Pillow and Python code, and a frame waiting its turn for 5ms at a
|
# GIL in Pillow and Python code, and a frame waiting its turn for 5ms at a
|
||||||
# time misses its refresh. 0 leaves the interpreter default alone.
|
# time misses its refresh. 0 leaves the interpreter default alone.
|
||||||
# Experimental: measured with scripts/frame_soak.py before it gets a default.
|
# Experimental. On hdpi it did less than prefetch_gate (0.90% -> 0.78% late
|
||||||
|
# against 0.60%; see docs/OFFSCREEN_RENDERING.md), so it stays off.
|
||||||
switch_interval_ms: float = 0.0
|
switch_interval_ms: float = 0.0
|
||||||
|
|
||||||
# Let the prefetch thread run Python only while the render thread is
|
# Let the prefetch thread run Python only while the render thread is
|
||||||
# blocked waiting for vsync, and park it the rest of the time, so the
|
# blocked waiting for vsync, and park it the rest of the time, so the
|
||||||
# render thread never waits for the GIL when its refresh comes round. Needs
|
# render thread never waits for the GIL when its refresh comes round. Needs
|
||||||
# a binding that releases the GIL in SwapOnVSync; ignored otherwise.
|
# a binding that releases the GIL in SwapOnVSync; off otherwise. On hdpi
|
||||||
# Experimental: see src/common/render_gate.py and measure with
|
# it cut frames two or more refreshes late eightfold, and late frames
|
||||||
# scripts/frame_soak.py before it gets a default.
|
# overall from 0.90% to 0.60%. See src/common/render_gate.py.
|
||||||
prefetch_gate: bool = False
|
prefetch_gate: bool = True
|
||||||
|
|
||||||
# Keep one continuous strip, extending it with the next group of plugins as
|
# Keep one continuous strip, extending it with the next group of plugins as
|
||||||
# the scroll approaches the end, instead of composing a fresh strip and
|
# the scroll approaches the end, instead of composing a fresh strip and
|
||||||
@@ -229,7 +230,7 @@ class VegasModeConfig:
|
|||||||
continuous_scroll=vegas_config.get('continuous_scroll', True),
|
continuous_scroll=vegas_config.get('continuous_scroll', True),
|
||||||
offscreen_prefetch=bool(vegas_config.get('offscreen_prefetch', True)),
|
offscreen_prefetch=bool(vegas_config.get('offscreen_prefetch', True)),
|
||||||
switch_interval_ms=float(vegas_config.get('switch_interval_ms', 0.0) or 0.0),
|
switch_interval_ms=float(vegas_config.get('switch_interval_ms', 0.0) or 0.0),
|
||||||
prefetch_gate=bool(vegas_config.get('prefetch_gate', False)),
|
prefetch_gate=bool(vegas_config.get('prefetch_gate', True)),
|
||||||
extend_threshold_screens=float(
|
extend_threshold_screens=float(
|
||||||
vegas_config.get('extend_threshold_screens', 2.0)),
|
vegas_config.get('extend_threshold_screens', 2.0)),
|
||||||
auto_trim=vegas_config.get('auto_trim', True),
|
auto_trim=vegas_config.get('auto_trim', True),
|
||||||
|
|||||||
@@ -340,12 +340,14 @@ class VegasModeCoordinator:
|
|||||||
if getattr(self.display_manager, 'render_gate', None) is not None:
|
if getattr(self.display_manager, 'render_gate', None) is not None:
|
||||||
return
|
return
|
||||||
releases = render_gate.swap_releases_gil()
|
releases = render_gate.swap_releases_gil()
|
||||||
|
if releases is None:
|
||||||
|
logger.debug("Vegas: no prefetch gate -- no hardware binding loaded")
|
||||||
|
return
|
||||||
if not releases:
|
if not releases:
|
||||||
logger.warning(
|
# On by default, so this is every stock install: say so once per
|
||||||
"Vegas: prefetch_gate ignored -- %s",
|
# run, not as a warning.
|
||||||
"this rgbmatrix binding keeps the GIL in SwapOnVSync "
|
logger.info("Vegas: no prefetch gate -- this rgbmatrix binding keeps "
|
||||||
"(scripts/build_rgbmatrix_nogil.sh)" if releases is False
|
"the GIL in SwapOnVSync (scripts/build_rgbmatrix_nogil.sh)")
|
||||||
else "no hardware binding loaded")
|
|
||||||
return
|
return
|
||||||
gate = render_gate.RenderGate()
|
gate = render_gate.RenderGate()
|
||||||
# Locks the render thread takes too: never park the prefetch holding one.
|
# Locks the render thread takes too: never park the prefetch holding one.
|
||||||
|
|||||||
@@ -311,10 +311,16 @@ class TestVegasWiring:
|
|||||||
c.display_manager = type("DM", (), {"render_gate": None})()
|
c.display_manager = type("DM", (), {"render_gate": None})()
|
||||||
return c
|
return c
|
||||||
|
|
||||||
def test_off_by_default(self, monkeypatch):
|
def test_on_by_default(self, monkeypatch):
|
||||||
monkeypatch.setattr(render_gate, "swap_releases_gil", lambda: True)
|
monkeypatch.setattr(render_gate, "swap_releases_gil", lambda: True)
|
||||||
c = self._coordinator()
|
c = self._coordinator()
|
||||||
c._install_render_gate()
|
c._install_render_gate()
|
||||||
|
assert isinstance(c.display_manager.render_gate, RenderGate)
|
||||||
|
|
||||||
|
def test_can_be_turned_off(self, monkeypatch):
|
||||||
|
monkeypatch.setattr(render_gate, "swap_releases_gil", lambda: True)
|
||||||
|
c = self._coordinator(prefetch_gate=False)
|
||||||
|
c._install_render_gate()
|
||||||
assert c.display_manager.render_gate is None
|
assert c.display_manager.render_gate is None
|
||||||
|
|
||||||
def test_installed_for_the_run_and_removed_after(self, monkeypatch):
|
def test_installed_for_the_run_and_removed_after(self, monkeypatch):
|
||||||
@@ -338,12 +344,12 @@ class TestVegasWiring:
|
|||||||
|
|
||||||
def test_read_from_config(self):
|
def test_read_from_config(self):
|
||||||
from src.vegas_mode.config import VegasModeConfig
|
from src.vegas_mode.config import VegasModeConfig
|
||||||
on = VegasModeConfig.from_config(
|
off = VegasModeConfig.from_config(
|
||||||
{"display": {"vegas_scroll": {"prefetch_gate": True}}})
|
{"display": {"vegas_scroll": {"prefetch_gate": False}}})
|
||||||
assert on.prefetch_gate is True
|
assert off.prefetch_gate is False
|
||||||
assert on.to_dict()["prefetch_gate"] is True
|
assert off.to_dict()["prefetch_gate"] is False
|
||||||
assert VegasModeConfig.from_config(
|
assert VegasModeConfig.from_config(
|
||||||
{"display": {"vegas_scroll": {}}}).prefetch_gate is False
|
{"display": {"vegas_scroll": {}}}).prefetch_gate is True
|
||||||
|
|
||||||
def test_the_prefetch_runs_inside_the_gate(self):
|
def test_the_prefetch_runs_inside_the_gate(self):
|
||||||
from src.vegas_mode.render_pipeline import RenderPipeline
|
from src.vegas_mode.render_pipeline import RenderPipeline
|
||||||
|
|||||||
Reference in New Issue
Block a user