fixup! Keep unfinished retarget routes in a bounded job queue (#127)
build / mod (push) Failing after 22s
build / mod (push) Failing after 22s
The planner caches were keyed by (kind, relief Arc pointer, start, aim): two jobs with the same relief but different known-units snapshots could reuse each other's price grid and route. Key relief sampling by its Arc pointer but key prices and routes by the whole knowledge version: adaptive tick plus the known-units and relief Arc pointers. Regression creates two same-start, same-aim jobs on one relief with different known snapshots and checks their routes differ, while a third job of one version still reuses the route cache. Core release, adapter and WASM contract pass.
This commit is contained in:
@@ -68,6 +68,29 @@ impl KnownRelief {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl RouteJob {
|
||||
pub(super) fn for_test(
|
||||
drone: u32,
|
||||
aim: DVec3,
|
||||
unit: u32,
|
||||
version: u64,
|
||||
known: Arc<Vec<(u32, DVec3)>>,
|
||||
relief: Option<Arc<KnownRelief>>,
|
||||
) -> Self {
|
||||
Self { drone, aim, unit, version, known, relief }
|
||||
}
|
||||
}
|
||||
|
||||
/// The knowledge a route job was made with: its adaptive tick and the pointers to its known-units and
|
||||
/// relief snapshots. A planner cache keyed only by one of them could mix two jobs' pictures (#127).
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
|
||||
struct KnowledgeVersion {
|
||||
tick: u64,
|
||||
known: usize,
|
||||
relief: Option<usize>,
|
||||
}
|
||||
|
||||
/// How far a station jams.
|
||||
pub const EW_RADIUS: f64 = 350.0;
|
||||
/// Heat a tick on (20 s to overheat), and cooling a tick off (45 s from hot).
|
||||
@@ -379,11 +402,12 @@ impl Sim {
|
||||
let cell = raid::CELL.max(2.0 * span / raid::CELLS as f64);
|
||||
let centre = (lo + hi) / 2.0;
|
||||
let grid = raid::Grid { min: centre - DVec3::new(span, 0.0, span), cell, size: (2.0 * span / cell) as i32 + 1 };
|
||||
// Jobs may carry different relief versions; sample each only once and keep prices and routes per
|
||||
// version, so a pending job is planned on the land its own knowledge version knew (#127).
|
||||
// Jobs may carry different knowledge versions (their own known units and relief); keep the
|
||||
// sampled relief, prices and routes per version, so jobs of one adaptive tick share caches and
|
||||
// jobs of another do not (#127).
|
||||
let mut reliefs: HashMap<Option<usize>, Option<Vec<f32>>> = HashMap::new();
|
||||
let mut prices: HashMap<(DroneKind, Option<usize>), Vec<f32>> = HashMap::new();
|
||||
let mut routes: HashMap<(DroneKind, Option<usize>, (i32, i32), (i32, i32)), Vec<DVec3>> = HashMap::new();
|
||||
let mut prices: HashMap<(DroneKind, KnowledgeVersion), Vec<f32>> = HashMap::new();
|
||||
let mut routes: HashMap<(DroneKind, KnowledgeVersion, (i32, i32), (i32, i32)), Vec<DVec3>> = HashMap::new();
|
||||
let mut work = 0usize;
|
||||
while let Some(job) = jobs.front().cloned() {
|
||||
if work >= raid::WORK_PER_TICK {
|
||||
@@ -398,8 +422,12 @@ impl Sim {
|
||||
continue;
|
||||
};
|
||||
let (pos, kind) = (d.pos, d.kind);
|
||||
let version = job.relief.as_ref().map(|r| Arc::as_ptr(r) as usize);
|
||||
let land = reliefs.entry(version).or_insert_with(|| Self::retarget_relief(&job, &grid));
|
||||
let version = KnowledgeVersion {
|
||||
tick: job.version,
|
||||
known: Arc::as_ptr(&job.known) as usize,
|
||||
relief: job.relief.as_ref().map(|r| Arc::as_ptr(r) as usize),
|
||||
};
|
||||
let land = reliefs.entry(version.relief).or_insert_with(|| Self::retarget_relief(&job, &grid));
|
||||
let land = land.as_deref();
|
||||
let key = (kind, version, grid.index(pos), grid.index(job.aim));
|
||||
let route = match routes.get(&key) {
|
||||
|
||||
@@ -346,6 +346,51 @@ fn pending_retarget_keeps_its_relief_version_after_a_new_plan() {
|
||||
assert_ne!(retarget_route(&control, id), retarget_route(&walled, id), "the test does not distinguish the relief versions");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn same_relief_different_known_snapshots_do_not_share_prices_or_routes() {
|
||||
let flat = flat_relief();
|
||||
let mut sim = Sim::new(3);
|
||||
let start = DVec3::new(-1000.0, 100.0, 0.0);
|
||||
let aim = DVec3::new(1000.0, 70.0, 0.0);
|
||||
let threat = sim.id();
|
||||
let threat_at = DVec3::new(0.0, 70.0, 0.0);
|
||||
sim.sams.push(Sam::new(threat, SamKind::S400, threat_at, 0.0));
|
||||
let target = sim.id();
|
||||
sim.sams.push(Sam::new(target, SamKind::S400, aim, 0.0));
|
||||
let known_all = std::sync::Arc::new(vec![(threat, threat_at), (target, aim)]);
|
||||
let known_target = std::sync::Arc::new(vec![(target, aim)]);
|
||||
|
||||
let mut add = |known: std::sync::Arc<Vec<(u32, DVec3)>>, version: u64| -> u32 {
|
||||
let mut d = sim.make_drone(DroneKind::Kh22, start, DVec3::X, aim);
|
||||
d.smart = true;
|
||||
d.err = DVec3::ZERO;
|
||||
d.target = aim;
|
||||
d.route = vec![start, aim];
|
||||
let id = d.id;
|
||||
sim.queue.push((0, d));
|
||||
sim.retargets.push_back(super::ew::RouteJob::for_test(id, aim, target, version, known, Some(flat.clone())));
|
||||
id
|
||||
};
|
||||
// Same relief, same start and aim. Version 1 knows the threat on the direct line; version 2 does not.
|
||||
let first = add(known_all.clone(), 1);
|
||||
let other = add(known_target, 2);
|
||||
// A second job of version 1: it may reuse the first one's price grid and route.
|
||||
let same = add(known_all, 1);
|
||||
sim.tick = 1;
|
||||
for _ in 0..10 {
|
||||
if sim.retargets.is_empty() {
|
||||
break;
|
||||
}
|
||||
sim.adapt();
|
||||
sim.tick += 1;
|
||||
}
|
||||
assert!(sim.retargets.is_empty(), "the jobs were not finished");
|
||||
let route = |id| sim.queue.iter().find(|(_, d)| d.id == id).unwrap().1.route.clone();
|
||||
let (r1, r2, r3) = (route(first), route(other), route(same));
|
||||
assert_ne!(r1, r2, "jobs with different known snapshots shared a route");
|
||||
assert_eq!(r1, r3, "jobs of one knowledge version did not reuse their route cache");
|
||||
}
|
||||
|
||||
// #31: a big cache of fresh columns is not scanned on every answer
|
||||
#[test]
|
||||
fn big_ground_cache_is_swept_rarely() {
|
||||
|
||||
@@ -384,8 +384,10 @@ Lancet, баллистика и decoy, как и раньше, летят пря
|
||||
Повторный план использует снимок разведки исходного выбора цели и известный рельеф **той же версии
|
||||
задания**: `KnownRelief` (сетка и высоты) лежит в `Arc` внутри `RouteJob`, `Sim::known_relief` хранит
|
||||
тот же `Arc` только для новых заданий. Завершение другой умной волны заменяет глобальный указатель,
|
||||
но не снимок ожидающего job; цены и кэш маршрутов в `replan_retargets` ключуются версией рельефа.
|
||||
За пределами известной сетки — Unknown, новая карта не раскрывается.
|
||||
но не снимок ожидающего job. Выборка рельефа, цены и кэш маршрутов в `replan_retargets` ключуются
|
||||
версией знаний целиком — adapt tick плюс указатели на снимки known units и рельефа, — а не только
|
||||
рельефом: два job с одинаковым рельефом, но разными known units, не делят price/route grid. За
|
||||
пределами известной сетки — Unknown, новая карта не раскрывается.
|
||||
|
||||
Очередь — рантайм-состояние планировщика. Дроны и очередь запуска в текущий `state_text` не входят,
|
||||
поэтому формат save не менялся и старые saves читаются как прежде; запись replay по-прежнему хранит
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
| #120 | Конечное горение, powered/coast, сопротивление и снижение доступного манёвра, один остаточный pk по фактической скорости; swept-встреча; события burnout/energy loss; согласованное прекращение факела/дыма/звука | Игровой профиль, без аэродинамики и реальных параметров; сравнительная выборка 12 рейдов и расширенная сетка 48 рейдов (6 вариантов × 8 seed, батареи/дальности/классы) записаны в measurements; без подтверждённого дефекта параметры не менялись; прослушивание остаётся |
|
||||
| #121 | On/Off в команде и карте, save/replay; Off не создаёт own measurements и сохраняет сетевой приём; раскрытие работающего радара наблюдателем; SEAD использует last-known и возраст | Условный наблюдатель по расстоянию, без радиотехнической модели и ошибки пеленга; выбор на карте ещё не проверен в GUI |
|
||||
| #122 | Воспроизведены два класса: tick fuel exhaustion на двух максимальных составах, HD (полный обход height-cache на каждом взрыве заменён удалением нужных ключей; max135.3M/300M) и остановка в UI event на втором крупном приказе (переснимок known_units на каждом дроне заменён одним снимком на приказ и O(журнал+группировка); max command 32.9M/1e9, `/stats` до/после совпал); нативные тесты эквивалентности и второго приказа; WASM fixture с 4 принятыми приказами; callback diagnostics/profile | Требуется приёмка на Windows; без подтверждённого build identity нельзя утверждать ту же причину пользовательского сеанса; FPS/GPU не измерялись |
|
||||
| #127 | Узкая часть: массовое перенацеливание не теряет необработанные маршруты. Задания (ID дрона, новая цель, версия знаний) живут в FIFO-очереди между тиками; бюджет `WORK_PER_TICK` тот же, задания снимаются/заменяются без роста очереди, старый безопасный маршрут остаётся до готовности нового, а при отмене задания используется прямой полёт как fallback. Снимок рельефа принадлежит версии задания (`Arc<KnownRelief>` в job), поэтому поздняя умная волна не переписывает ожидающий маршрут; цены и кэш маршрутов ключуются версией. Регрессии: 40+ ретаргетов, продвижение каждый бюджетный тик, отсутствие вечных пустых маршрутов, детерминизм повторного seed, отсутствие дублей и сохранение рельефа pending job после замены `known_relief` | Полный #127 (EnemyPicture, общая модель возможностей батареи/связи, честное разделение наблюдения и истины, структурированное намерение, объяснимость) не реализован. Рельеф повторного плана — выборка из сетки версии задания; вне её Unknown. Очередь — рантайм-состояние планировщика: drones/queue не входят в текущий формат save, поэтому старые saves не меняются; отдельная durable-очередь потребовала бы нового формата |
|
||||
| #127 | Узкая часть: массовое перенацеливание не теряет необработанные маршруты. Задания (ID дрона, новая цель, версия знаний) живут в FIFO-очереди между тиками; бюджет `WORK_PER_TICK` тот же, задания снимаются/заменяются без роста очереди, старый безопасный маршрут остаётся до готовности нового, а при отмене задания используется прямой полёт как fallback. Снимок рельефа принадлежит версии задания (`Arc<KnownRelief>` в job), поэтому поздняя умная волна не переписывает ожидающий маршрут; рельеф, цены и кэш маршрутов ключуются версией знаний целиком (adapt tick + указатели на known/relief snapshots), поэтому jobs с одним рельефом, но разными known snapshots не делят кэш. Регрессии: 40+ ретаргетов, продвижение каждый бюджетный тик, отсутствие вечных пустых маршрутов, детерминизм повторного seed, отсутствие дублей, сохранение рельефа pending job после замены `known_relief` и разделение кэша по known snapshots | Полный #127 (EnemyPicture, общая модель возможностей батареи/связи, честное разделение наблюдения и истины, структурированное намерение, объяснимость) не реализован. Рельеф повторного плана — выборка из сетки версии задания; вне её Unknown. Очередь — рантайм-состояние планировщика: drones/queue не входят в текущий формат save, поэтому старые saves не меняются; отдельная durable-очередь потребовала бы нового формата |
|
||||
|
||||
По #127 реализована только устойчивая очередь маршрутов, а не полный «EnemyPicture». Очередь не
|
||||
сохраняется в autosave: текущий формат вообще не хранит дронов и очередь запуска, поэтому изменение
|
||||
|
||||
Reference in New Issue
Block a user