Skip to main content

wowlab_engine_sim/telemetry/
representative.rs

1//! Representative-iteration timeline capture and trace-extra sampling.
2
3use wowlab_engine_telemetry::{
4    EncounterTelemetryEvent, ResourceEventKind, TelemetrySink, TimelineEventIndex,
5};
6use wowlab_types::{
7    numeric::f64_to_u32_saturating_round,
8    proto,
9    sim::{ActorId, ChunkAuraWindow, ChunkCooldownWindow, ChunkTimelineMarker, GroupId, PullId},
10};
11
12use super::{TelemetryAccumulator, collect_aura_intervals, telemetry_preview_f32};
13
14const RESOURCE_SAMPLE_BUCKET_MS: u32 = 100;
15
16#[derive(Clone, Debug)]
17pub(crate) struct RepresentativeTimeline {
18    pub(crate) markers: Vec<ChunkTimelineMarker>,
19    pub(crate) aura_windows: Vec<ChunkAuraWindow>,
20    pub(crate) cooldown_windows: Vec<ChunkCooldownWindow>,
21    pub(crate) resource_samples: Vec<RepresentativeResourceSample>,
22    pub(crate) aura_intervals: Vec<RepresentativeAuraInterval>,
23    pub(crate) encounter_events: Vec<RepresentativeEncounterEvent>,
24}
25
26#[derive(Clone, Debug)]
27pub(crate) struct RepresentativeEncounterEvent {
28    pub(crate) sequence: u64,
29    pub(crate) event: EncounterTelemetryEvent,
30}
31
32#[derive(Clone, Debug)]
33pub(crate) struct RepresentativeResourceSample {
34    pub(crate) time_ms: u32,
35    pub(crate) resource_type: u8,
36    pub(crate) current: f32,
37    pub(crate) max: f32,
38    pub(crate) gain: f32,
39    pub(crate) loss: f32,
40}
41
42#[derive(Clone, Debug)]
43pub(crate) struct RepresentativeAuraInterval {
44    pub(crate) start_ms: u32,
45    pub(crate) end_ms: u32,
46    pub(crate) open_ended: bool,
47    pub(crate) aura_id: u32,
48    pub(crate) source: ActorId,
49    pub(crate) affected: ActorId,
50    pub(crate) pull: PullId,
51    pub(crate) group: Option<GroupId>,
52}
53
54#[derive(Default)]
55// #t(rust_similar_structs) resource buckets accumulate values before sample time and resource identity are attached
56struct ResourceBucket {
57    current: f32,
58    max: f32,
59    gain: f32,
60    loss: f32,
61}
62
63fn bucket_resource_samples(
64    events: &[wowlab_engine_telemetry::ResourceEvent],
65) -> Vec<RepresentativeResourceSample> {
66    let mut buckets: std::collections::BTreeMap<(u32, u8), ResourceBucket> =
67        std::collections::BTreeMap::new();
68
69    for ev in events {
70        let bucket = ev.time_ms / RESOURCE_SAMPLE_BUCKET_MS;
71        let slot = buckets.entry((bucket, ev.resource_type)).or_default();
72
73        slot.current = telemetry_preview_f32(ev.current);
74        slot.max = telemetry_preview_f32(ev.max);
75
76        match ev.kind {
77            ResourceEventKind::Gain { wasted } => {
78                slot.gain += telemetry_preview_f32(ev.amount - wasted);
79            }
80            ResourceEventKind::Spend => {
81                slot.loss += telemetry_preview_f32(ev.amount);
82            }
83        }
84    }
85
86    buckets
87        .into_iter()
88        .map(|((bucket, rt), slot)| RepresentativeResourceSample {
89            time_ms: bucket * RESOURCE_SAMPLE_BUCKET_MS,
90            resource_type: rt,
91            current: slot.current,
92            max: slot.max,
93            gain: slot.gain,
94            loss: slot.loss,
95        })
96        .collect()
97}
98
99fn collect_aura_intervals_with_open_ended(
100    events: &[wowlab_engine_telemetry::AuraEvent],
101    encounter_end_ms: u32,
102) -> Vec<RepresentativeAuraInterval> {
103    let mut out: Vec<RepresentativeAuraInterval> = Vec::new();
104
105    collect_aura_intervals(events, |event, start, end| {
106        out.push(RepresentativeAuraInterval {
107            start_ms: start,
108            end_ms: end.unwrap_or(encounter_end_ms),
109            open_ended: end.is_none(),
110            aura_id: event.aura_id,
111            source: event.source,
112            affected: event.affected,
113            pull: event.pull,
114            group: event.group,
115        });
116    });
117    out.sort_by_key(|w| {
118        (
119            w.start_ms,
120            w.aura_id,
121            super::actor_sort_key(w.affected),
122            super::actor_sort_key(w.source),
123        )
124    });
125
126    out
127}
128
129impl TelemetryAccumulator {
130    pub(crate) fn capture_representative(&mut self, sink: &TelemetrySink, duration_ms: u32) {
131        let mut markers = Vec::with_capacity(sink.casts.len() + sink.damage.len());
132        let mut encounter_events = Vec::with_capacity(sink.encounter.len());
133
134        for (sequence, event_index) in sink.timeline_order().iter().enumerate() {
135            let sequence = u64::try_from(sequence).expect("timeline event count fits u64");
136
137            match *event_index {
138                TimelineEventIndex::Cast(index) => {
139                    let Some(event) = sink.casts.get(index) else {
140                        continue;
141                    };
142
143                    markers.push(ChunkTimelineMarker {
144                        time_ms: event.time_ms,
145                        sequence,
146                        kind: proto::MarkerKind::Cast,
147                        spell_or_aura_id: event.spell_id,
148                        target: event.scope.target.0.into(),
149                        amount: 0,
150                        is_crit: false,
151                        source: Some(event.scope.source),
152                        pull: Some(event.scope.pull),
153                        group: Some(event.scope.group),
154                    });
155                }
156                TimelineEventIndex::Damage(index) => {
157                    let Some(event) = sink.damage.get(index) else {
158                        continue;
159                    };
160
161                    markers.push(ChunkTimelineMarker {
162                        time_ms: event.time_ms,
163                        sequence,
164                        kind: proto::MarkerKind::Damage,
165                        spell_or_aura_id: event.spell_id,
166                        target: event.scope.target.0.into(),
167                        amount: f64_to_u32_saturating_round(event.amount.max(0.0)),
168                        is_crit: event.is_crit,
169                        source: Some(event.scope.source),
170                        pull: Some(event.scope.pull),
171                        group: Some(event.scope.group),
172                    });
173                }
174                TimelineEventIndex::Encounter(index) => {
175                    let Some(event) = sink.encounter.get(index) else {
176                        continue;
177                    };
178
179                    encounter_events.push(RepresentativeEncounterEvent {
180                        sequence,
181                        // #t(rust_clone_in_loop) the representative outlives the cleared iteration sink
182                        event: event.clone(),
183                    });
184                }
185                _ => {}
186            }
187        }
188
189        markers.sort_by_key(|m| (m.time_ms, m.sequence));
190
191        let mut aura_windows = Vec::new();
192
193        collect_aura_intervals(&sink.auras, |event, start, end| {
194            aura_windows.push(ChunkAuraWindow {
195                aura_id: event.aura_id,
196                target: match event.affected {
197                    ActorId::Enemy(enemy) => u32::from(enemy.0),
198                    ActorId::Player | ActorId::External | ActorId::Pet(_) => 0,
199                },
200                start_ms: start,
201                end_ms: end.unwrap_or(duration_ms),
202                source: Some(event.source),
203                affected: Some(event.affected),
204                pull: Some(event.pull),
205                group: event.group,
206            });
207        });
208
209        let cooldown_windows: Vec<ChunkCooldownWindow> = sink
210            .cooldowns
211            .iter()
212            .map(|event| ChunkCooldownWindow {
213                spell_id: event.spell_id,
214                start_ms: event.time_ms,
215                duration_ms: event.duration_ms,
216            })
217            .collect();
218
219        let (resource_samples, aura_intervals) = if self.trace_extras_enabled {
220            (
221                bucket_resource_samples(&sink.resources),
222                collect_aura_intervals_with_open_ended(&sink.auras, duration_ms),
223            )
224        } else {
225            (Vec::new(), Vec::new())
226        };
227
228        self.representative_sink = Some(RepresentativeTimeline {
229            markers,
230            aura_windows,
231            cooldown_windows,
232            resource_samples,
233            aura_intervals,
234            encounter_events,
235        });
236    }
237}
238
239#[cfg(test)]
240mod tests;