Skip to main content

rustial_engine/
tile_lifecycle.rs

1//! Tile lifecycle diagnostics for the tile pipeline.
2//!
3//! The types in this module make a tile observable across the core engine
4//! stages: selection, queueing, dispatch, completion, promotion, visible use,
5//! cancellation, and eviction.
6//!
7//! The tracker is intentionally lightweight:
8//!
9//! - active per-tile state is bounded by the number of tracked tiles
10//! - recent events are stored in a ring buffer
11//! - recent terminal records are also stored in a ring buffer
12//! - timing metrics are recorded in frame counts for deterministic tests
13
14use rustial_math::TileId;
15use std::collections::{HashMap, VecDeque};
16
17const DEFAULT_RECENT_EVENT_CAPACITY: usize = 2048;
18const DEFAULT_RECENT_TERMINAL_RECORD_CAPACITY: usize = 512;
19
20/// One lifecycle transition observed for a tile.
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub struct TileLifecycleEvent {
23    /// Frame index at which the transition was observed.
24    pub frame: u64,
25    /// Tile affected by the transition.
26    pub tile: TileId,
27    /// Kind of transition recorded.
28    pub kind: TileLifecycleEventKind,
29}
30
31/// Lifecycle transition kind.
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum TileLifecycleEventKind {
34    /// Tile entered the desired set for the current frame.
35    Selected,
36    /// Tile was inserted into the pending queue.
37    Queued,
38    /// Tile request was dispatched to the source transport.
39    Dispatched,
40    /// Tile fetch completed and returned to the engine.
41    Completed,
42    /// Tile payload passed validation / decode stage.
43    Decoded,
44    /// Tile was promoted into the cache as renderable payload.
45    PromotedToCache,
46    /// Tile was used as the exact visible tile.
47    UsedAsExact,
48    /// Tile was used as fallback imagery for another target.
49    UsedAsFallback,
50    /// Tile request was cancelled because it became stale.
51    CancelledAsStale,
52    /// Tile was evicted while still pending.
53    EvictedWhilePending,
54    /// Tile was evicted after it had renderable payload.
55    EvictedAfterRenderableUse,
56    /// Tile failed to load or validate.
57    Failed,
58}
59
60/// Per-tile lifecycle record with first-observed timings.
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct TileLifecycleRecord {
63    /// Tile tracked by this record.
64    pub tile: TileId,
65    /// First frame where the tile was selected.
66    pub first_selected_frame: Option<u64>,
67    /// First frame where the tile entered the pending queue.
68    pub first_queued_frame: Option<u64>,
69    /// First frame where the tile was dispatched.
70    pub first_dispatched_frame: Option<u64>,
71    /// First frame where the tile completed.
72    pub first_completed_frame: Option<u64>,
73    /// First frame where the tile payload passed validation / decode.
74    pub first_decoded_frame: Option<u64>,
75    /// First frame where the tile was promoted to the cache.
76    pub first_promoted_frame: Option<u64>,
77    /// First frame where the tile became renderable in the visible set.
78    pub first_renderable_frame: Option<u64>,
79    /// First frame where the tile was used as the exact visible tile.
80    pub first_exact_frame: Option<u64>,
81    /// First frame where the tile was used as fallback imagery.
82    pub first_fallback_frame: Option<u64>,
83    /// Frame delta between first queue and first dispatch.
84    pub queued_frames_to_dispatch: Option<u64>,
85    /// Frame delta between first dispatch and first completion.
86    pub in_flight_frames_to_complete: Option<u64>,
87    /// Frame delta between first completion and first visible renderable use.
88    pub completion_to_visible_use_frames: Option<u64>,
89    /// Most recent frame where any lifecycle event was recorded.
90    pub last_event_frame: u64,
91    /// Terminal transition, if the record has completed.
92    pub terminal_event: Option<TileLifecycleEventKind>,
93}
94
95impl TileLifecycleRecord {
96    fn new(tile: TileId) -> Self {
97        Self {
98            tile,
99            first_selected_frame: None,
100            first_queued_frame: None,
101            first_dispatched_frame: None,
102            first_completed_frame: None,
103            first_decoded_frame: None,
104            first_promoted_frame: None,
105            first_renderable_frame: None,
106            first_exact_frame: None,
107            first_fallback_frame: None,
108            queued_frames_to_dispatch: None,
109            in_flight_frames_to_complete: None,
110            completion_to_visible_use_frames: None,
111            last_event_frame: 0,
112            terminal_event: None,
113        }
114    }
115}
116
117/// Snapshot of current lifecycle diagnostics.
118#[derive(Debug, Clone, Default, PartialEq, Eq)]
119pub struct TileLifecycleDiagnostics {
120    /// Active tile records still being tracked.
121    pub active_records: Vec<TileLifecycleRecord>,
122    /// Recently completed terminal records.
123    pub recent_terminal_records: Vec<TileLifecycleRecord>,
124    /// Recent lifecycle events in chronological order.
125    pub recent_events: Vec<TileLifecycleEvent>,
126}
127
128#[derive(Debug)]
129pub(crate) struct TileLifecycleTracker {
130    current_frame: u64,
131    active_records: HashMap<TileId, TileLifecycleRecord>,
132    recent_terminal_records: VecDeque<TileLifecycleRecord>,
133    recent_events: VecDeque<TileLifecycleEvent>,
134    recent_event_capacity: usize,
135    recent_terminal_capacity: usize,
136}
137
138impl Default for TileLifecycleTracker {
139    fn default() -> Self {
140        Self {
141            current_frame: 0,
142            active_records: HashMap::new(),
143            recent_terminal_records: VecDeque::with_capacity(
144                DEFAULT_RECENT_TERMINAL_RECORD_CAPACITY,
145            ),
146            recent_events: VecDeque::with_capacity(DEFAULT_RECENT_EVENT_CAPACITY),
147            recent_event_capacity: DEFAULT_RECENT_EVENT_CAPACITY,
148            recent_terminal_capacity: DEFAULT_RECENT_TERMINAL_RECORD_CAPACITY,
149        }
150    }
151}
152
153impl TileLifecycleTracker {
154    /// Start a new engine frame for lifecycle timing.
155    pub fn begin_frame(&mut self, frame: u64) {
156        self.current_frame = frame;
157    }
158
159    /// Record that a tile entered the desired set.
160    pub fn record_selected(&mut self, tile: TileId) {
161        let frame = self.current_frame;
162        let mut emit = false;
163        {
164            let record = self.ensure_record(tile);
165            if record.first_selected_frame.is_none() {
166                record.first_selected_frame = Some(frame);
167                emit = true;
168            }
169            record.last_event_frame = frame;
170        }
171        if emit {
172            self.push_event(tile, TileLifecycleEventKind::Selected);
173        }
174    }
175
176    /// Record that a tile entered the pending queue.
177    pub fn record_queued(&mut self, tile: TileId) {
178        let frame = self.current_frame;
179        let mut emit = false;
180        {
181            let record = self.ensure_record(tile);
182            if record.first_queued_frame.is_none() {
183                record.first_queued_frame = Some(frame);
184                emit = true;
185            }
186            record.last_event_frame = frame;
187        }
188        if emit {
189            self.push_event(tile, TileLifecycleEventKind::Queued);
190        }
191    }
192
193    /// Record that a tile request was dispatched.
194    pub fn record_dispatched(&mut self, tile: TileId) {
195        let frame = self.current_frame;
196        let mut emit = false;
197        {
198            let record = self.ensure_record(tile);
199            if record.first_dispatched_frame.is_none() {
200                record.first_dispatched_frame = Some(frame);
201                if let Some(queued_frame) = record.first_queued_frame {
202                    record.queued_frames_to_dispatch = Some(frame.saturating_sub(queued_frame));
203                }
204                emit = true;
205            }
206            record.last_event_frame = frame;
207        }
208        if emit {
209            self.push_event(tile, TileLifecycleEventKind::Dispatched);
210        }
211    }
212
213    /// Record that a tile fetch completed.
214    pub fn record_completed(&mut self, tile: TileId) {
215        let frame = self.current_frame;
216        let mut emit = false;
217        {
218            let record = self.ensure_record(tile);
219            if record.first_completed_frame.is_none() {
220                record.first_completed_frame = Some(frame);
221                if let Some(dispatched_frame) = record.first_dispatched_frame {
222                    record.in_flight_frames_to_complete =
223                        Some(frame.saturating_sub(dispatched_frame));
224                }
225                emit = true;
226            }
227            record.last_event_frame = frame;
228        }
229        if emit {
230            self.push_event(tile, TileLifecycleEventKind::Completed);
231        }
232    }
233
234    /// Record that a tile payload passed validation / decode.
235    pub fn record_decoded(&mut self, tile: TileId) {
236        let frame = self.current_frame;
237        let mut emit = false;
238        {
239            let record = self.ensure_record(tile);
240            if record.first_decoded_frame.is_none() {
241                record.first_decoded_frame = Some(frame);
242                emit = true;
243            }
244            record.last_event_frame = frame;
245        }
246        if emit {
247            self.push_event(tile, TileLifecycleEventKind::Decoded);
248        }
249    }
250
251    /// Record that a tile was promoted into the cache.
252    pub fn record_promoted_to_cache(&mut self, tile: TileId) {
253        let frame = self.current_frame;
254        let mut emit = false;
255        {
256            let record = self.ensure_record(tile);
257            if record.first_promoted_frame.is_none() {
258                record.first_promoted_frame = Some(frame);
259                emit = true;
260            }
261            record.last_event_frame = frame;
262        }
263        if emit {
264            self.push_event(tile, TileLifecycleEventKind::PromotedToCache);
265        }
266    }
267
268    /// Record that a tile was used exactly in the visible set.
269    pub fn record_used_as_exact(&mut self, tile: TileId) {
270        let frame = self.current_frame;
271        let mut emit = false;
272        {
273            let record = self.ensure_record(tile);
274            if record.first_renderable_frame.is_none() {
275                record.first_renderable_frame = Some(frame);
276                if let Some(completed_frame) = record.first_completed_frame {
277                    record.completion_to_visible_use_frames =
278                        Some(frame.saturating_sub(completed_frame));
279                }
280            }
281            if record.first_exact_frame.is_none() {
282                record.first_exact_frame = Some(frame);
283                emit = true;
284            }
285            record.last_event_frame = frame;
286        }
287        if emit {
288            self.push_event(tile, TileLifecycleEventKind::UsedAsExact);
289        }
290    }
291
292    /// Record that a tile was used as fallback imagery.
293    pub fn record_used_as_fallback(&mut self, tile: TileId) {
294        let frame = self.current_frame;
295        let mut emit = false;
296        {
297            let record = self.ensure_record(tile);
298            if record.first_renderable_frame.is_none() {
299                record.first_renderable_frame = Some(frame);
300                if let Some(completed_frame) = record.first_completed_frame {
301                    record.completion_to_visible_use_frames =
302                        Some(frame.saturating_sub(completed_frame));
303                }
304            }
305            if record.first_fallback_frame.is_none() {
306                record.first_fallback_frame = Some(frame);
307                emit = true;
308            }
309            record.last_event_frame = frame;
310        }
311        if emit {
312            self.push_event(tile, TileLifecycleEventKind::UsedAsFallback);
313        }
314    }
315
316    /// Record a non-terminal failure for a tile.
317    pub fn record_failed(&mut self, tile: TileId) {
318        let frame = self.current_frame;
319        {
320            let record = self.ensure_record(tile);
321            record.last_event_frame = frame;
322        }
323        self.push_event(tile, TileLifecycleEventKind::Failed);
324    }
325
326    /// Record that a tile was cancelled as stale and finalize the record.
327    pub fn record_cancelled_as_stale(&mut self, tile: TileId) {
328        self.finalize(tile, TileLifecycleEventKind::CancelledAsStale);
329    }
330
331    /// Record that a tile was evicted while pending and finalize the record.
332    pub fn record_evicted_while_pending(&mut self, tile: TileId) {
333        self.finalize(tile, TileLifecycleEventKind::EvictedWhilePending);
334    }
335
336    /// Record that a tile was evicted after becoming renderable and finalize the record.
337    pub fn record_evicted_after_renderable_use(&mut self, tile: TileId) {
338        self.finalize(tile, TileLifecycleEventKind::EvictedAfterRenderableUse);
339    }
340
341    /// Return a snapshot of the current lifecycle diagnostics.
342    pub fn diagnostics(&self) -> TileLifecycleDiagnostics {
343        let mut active_records: Vec<_> = self.active_records.values().cloned().collect();
344        active_records.sort_by_key(|record| (record.tile.zoom, record.tile.y, record.tile.x));
345
346        TileLifecycleDiagnostics {
347            active_records,
348            recent_terminal_records: self.recent_terminal_records.iter().cloned().collect(),
349            recent_events: self.recent_events.iter().copied().collect(),
350        }
351    }
352
353    fn ensure_record(&mut self, tile: TileId) -> &mut TileLifecycleRecord {
354        self.active_records
355            .entry(tile)
356            .or_insert_with(|| TileLifecycleRecord::new(tile))
357    }
358
359    fn finalize(&mut self, tile: TileId, kind: TileLifecycleEventKind) {
360        let frame = self.current_frame;
361        let mut record = self
362            .active_records
363            .remove(&tile)
364            .unwrap_or_else(|| TileLifecycleRecord::new(tile));
365        record.last_event_frame = frame;
366        record.terminal_event = Some(kind);
367        self.push_event(tile, kind);
368        self.recent_terminal_records.push_back(record);
369        while self.recent_terminal_records.len() > self.recent_terminal_capacity {
370            let _ = self.recent_terminal_records.pop_front();
371        }
372    }
373
374    fn push_event(&mut self, tile: TileId, kind: TileLifecycleEventKind) {
375        self.recent_events.push_back(TileLifecycleEvent {
376            frame: self.current_frame,
377            tile,
378            kind,
379        });
380        while self.recent_events.len() > self.recent_event_capacity {
381            let _ = self.recent_events.pop_front();
382        }
383    }
384}