[performance] Streaming чанков снимает busy-guard акустического worker: растёт очередь устаревших jobs и снимков мира #116

Closed
opened 2026-11-01 10:51:23 +00:00 by chexit · 1 comment
Owner

🤖 Filed by Codex, AI-агент Владимира (chexit).

Ревью kimi-work @ cd3c4ed76d3308ec1d87341b962ff04aadada293, 1 ноября 2026. Новая регрессия исправления #110, не повтор исходной ошибки инвалидации кэша.

Причина

В Acoustics::step при любом изменении Changes::residency повышаются seq/floor и выполняется self.in_flight = None (patch 0044, строка 235). Но выполняемая задача не отменяется и worker не освобождается.

Дальше тот же step видит отсутствие in_flight и отправляет ещё один Job. Канал в spawn() — неограниченный std::sync::mpsc::channel. Следующая смена residency повторяет это. Каждый job удерживает Arc<dyn Geometry>; в игре это Snapshot с таблицей Arc<Chunk>. Сам acoustic_snapshot() пересоздаётся по content_revision, то есть при streaming это могут быть разные снимки.

Поздние результаты действительно отбрасываются через floor, однако уже поставленные задачи всё равно занимают память и просчитываются последовательно, задерживая актуальную акустику. Проверка stale-result не заменяет ограничения очереди.

Подтверждено нативно

Запущены исходные acoustics.rs и его подмодули из стека патчей 0044 в изолированном std-only harness. Макет Chunk нужен только для компиляции неиспользуемого Snapshot; тестовый Geometry сам предоставляет walker. Во время работы worker его Geometry удерживается на Condvar; inline-вызовы главного потока не блокируются. Это моделирует worker, не успевший закончить при поступлении новых чанков.

32 вызова step с одним источником и последовательными residency дали:

unfinished jobs retaining geometry=32
completed=0
skipped_busy=0

Проверка ограниченного количества незавершённых задач падает. Реальный FPS, объём памяти в МБ и частота такого отставания на Mac не измерялись; воспроизведено именно отсутствие backpressure, а не заявлена постоянная утечка.

Минимальное ядро воспроизведения (в tests acoustics, используя существующий src):

struct Gate { ready: std::sync::Mutex<bool>, wake: std::sync::Condvar }
impl Geometry for Gate {
    fn walker(&self) -> Box<dyn FnMut(i32,i32,i32)->Option<f32> + '_> {
        if std::thread::current().name() == Some("acoustics") {
            let mut ready = self.ready.lock().unwrap();
            while !*ready { ready = self.wake.wait(ready).unwrap(); }
        }
        Box::new(|_,_,_| Some(0.0))
    }
}
let gate = Arc::new(Gate { ready: false.into(), wake: Default::default() });
let g: Arc<dyn Geometry> = gate.clone();
let mut a = Acoustics::new(false);
for residency in 0..32 {
    a.step(Some(&g), [0.5,70.5,0.5], &[src(1,90.5)],
        &Changes { residency, ..Default::default() });
}
let held = Arc::strong_count(&g) - 2; // local gate + g
*gate.ready.lock().unwrap() = true;
gate.wake.notify_all();
assert!(held <= 2, "retained jobs={held}"); // сейчас 32

Исправление и приёмка

Развести «ответ этого job уже устарел» и «worker закончил job». Не очищать физическую занятость только из-за инвалидации. Альтернатива — ограниченная очередь с заменой pending job на последний снимок и/или отмена устаревшей работы.

  • При удержанном worker и сотнях смен residency число выполняемых/ожидающих jobs и удерживаемых снимков остаётся ограниченным.
  • После освобождения worker актуальная задача не ждёт просчёта всей устаревшей очереди.
  • Ответ старого snapshot по-прежнему не применяется; main thread не блокируется на отправке.
  • Повторить для overflow/reset: там есть аналогичные сбросы in_flight; не потерять общую гарантию ограниченности.

Существующие 41 тест акустики проходят; текущий regression-test #110 проверяет отбрасывание ответа и eventual refinement, но не размер очереди при последовательных изменениях residency.

🤖 Filed by Codex, AI-агент Владимира (chexit). Ревью `kimi-work` @ `cd3c4ed76d3308ec1d87341b962ff04aadada293`, 1 ноября 2026. **Новая регрессия исправления #110**, не повтор исходной ошибки инвалидации кэша. ### Причина В `Acoustics::step` при любом изменении `Changes::residency` повышаются `seq`/`floor` **и выполняется `self.in_flight = None`** ([patch 0044, строка 235](https://gitea.maoniao.team/chexit/airdef/src/commit/cd3c4ed76d3308ec1d87341b962ff04aadada293/horizon-patches/0044-Acoustics-follows-chunk-residency-and-voice-lifecycle-110-111-112.patch#L235)). Но выполняемая задача не отменяется и worker не освобождается. Дальше тот же `step` видит отсутствие `in_flight` и отправляет ещё один `Job`. Канал в `spawn()` — неограниченный `std::sync::mpsc::channel`. Следующая смена residency повторяет это. Каждый job удерживает `Arc<dyn Geometry>`; в игре это `Snapshot` с таблицей `Arc<Chunk>`. Сам `acoustic_snapshot()` пересоздаётся по content_revision, то есть при streaming это могут быть разные снимки. Поздние результаты действительно отбрасываются через floor, однако **уже поставленные задачи всё равно занимают память и просчитываются последовательно**, задерживая актуальную акустику. Проверка stale-result не заменяет ограничения очереди. ### Подтверждено нативно Запущены исходные `acoustics.rs` и его подмодули из стека патчей 0044 в изолированном std-only harness. Макет Chunk нужен только для компиляции неиспользуемого Snapshot; тестовый Geometry сам предоставляет walker. Во время работы worker его Geometry удерживается на Condvar; inline-вызовы главного потока не блокируются. Это моделирует worker, не успевший закончить при поступлении новых чанков. 32 вызова `step` с одним источником и последовательными residency дали: ``` unfinished jobs retaining geometry=32 completed=0 skipped_busy=0 ``` Проверка ограниченного количества незавершённых задач падает. Реальный FPS, объём памяти в МБ и частота такого отставания на Mac **не измерялись**; воспроизведено именно отсутствие backpressure, а не заявлена постоянная утечка. Минимальное ядро воспроизведения (в tests acoustics, используя существующий `src`): ```rust struct Gate { ready: std::sync::Mutex<bool>, wake: std::sync::Condvar } impl Geometry for Gate { fn walker(&self) -> Box<dyn FnMut(i32,i32,i32)->Option<f32> + '_> { if std::thread::current().name() == Some("acoustics") { let mut ready = self.ready.lock().unwrap(); while !*ready { ready = self.wake.wait(ready).unwrap(); } } Box::new(|_,_,_| Some(0.0)) } } let gate = Arc::new(Gate { ready: false.into(), wake: Default::default() }); let g: Arc<dyn Geometry> = gate.clone(); let mut a = Acoustics::new(false); for residency in 0..32 { a.step(Some(&g), [0.5,70.5,0.5], &[src(1,90.5)], &Changes { residency, ..Default::default() }); } let held = Arc::strong_count(&g) - 2; // local gate + g *gate.ready.lock().unwrap() = true; gate.wake.notify_all(); assert!(held <= 2, "retained jobs={held}"); // сейчас 32 ``` ### Исправление и приёмка Развести «ответ этого job уже устарел» и «worker закончил job». Не очищать физическую занятость только из-за инвалидации. Альтернатива — ограниченная очередь с заменой pending job на последний снимок и/или отмена устаревшей работы. - При удержанном worker и сотнях смен residency число выполняемых/ожидающих jobs и удерживаемых снимков остаётся ограниченным. - После освобождения worker актуальная задача не ждёт просчёта всей устаревшей очереди. - Ответ старого snapshot по-прежнему не применяется; main thread не блокируется на отправке. - Повторить для overflow/reset: там есть аналогичные сбросы in_flight; не потерять общую гарантию ограниченности. Существующие 41 тест акустики проходят; текущий regression-test #110 проверяет отбрасывание ответа и eventual refinement, но не размер очереди при последовательных изменениях residency.
chexit added the area:audiotype:performancepriority:P2 labels 2026-11-01 10:51:23 +00:00
Author
Owner

🤖 Agent: Codex — работа Владимира (chexit).

Исправлено в https://gitea.maoniao.team/chexit/horizon-airdef eae00eafc, включено в опубликованный AirDef pin 6397299.

Busy/in_flight сохраняется через residency/overflow/reset до реального завершения job. Медленный worker не получает неограниченную очередь snapshots; regression с 200 invalidations проверяет один незавершённый job.

Targeted regressions и полный client --lib проходят; после обновления v0.2.97 — 1227 passed, 42 ignored. Эти проверки не являются субъективным прослушиванием или замером пропусков аудио.

Issue оставлен открытым согласно просьбе автора.

🤖 Agent: Codex — работа Владимира (chexit). Исправлено в [https://gitea.maoniao.team/chexit/horizon-airdef eae00eafc](https://gitea.maoniao.team/chexit/horizon-airdef/commit/eae00eafc5479570797cdbb328c9f5b10102ebd3), включено в опубликованный AirDef pin [6397299](https://gitea.maoniao.team/chexit/airdef/commit/6397299). Busy/in_flight сохраняется через residency/overflow/reset до реального завершения job. Медленный worker не получает неограниченную очередь snapshots; regression с 200 invalidations проверяет один незавершённый job. Targeted regressions и полный client --lib проходят; после обновления v0.2.97 — 1227 passed, 42 ignored. Эти проверки не являются субъективным прослушиванием или замером пропусков аудио. Issue оставлен открытым согласно просьбе автора.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: chexit/airdef#116