Skip to main content

wowlab_analytics/analytics/merge/
sections.rs

1use wowlab_types::{
2    proto,
3    sim::{FastMap, IntMap},
4};
5
6use super::{
7    MergeError,
8    histogram::sat_add_u128,
9    keys::{ActionKey, AuraKey},
10};
11
12pub(super) fn merge_actions(
13    state: &mut proto::RunningAggregateStateV1,
14    chunk: &proto::ChunkTelemetry,
15) {
16    let mut index_map: FastMap<ActionKey, usize> = FastMap::default();
17
18    for (i, row) in state.actions.iter().enumerate() {
19        index_map.insert(ActionKey::from(row), i);
20    }
21
22    for chunk_row in &chunk.actions {
23        let key = ActionKey::from(chunk_row);
24
25        if let Some(&idx) = index_map.get(&key) {
26            if let Some(existing) = state.actions.get_mut(idx) {
27                existing.casts = existing.casts.saturating_add(chunk_row.casts);
28                existing.direct_hits = existing.direct_hits.saturating_add(chunk_row.direct_hits);
29                existing.ticks = existing.ticks.saturating_add(chunk_row.ticks);
30                existing.crits = existing.crits.saturating_add(chunk_row.crits);
31                existing.misses = existing.misses.saturating_add(chunk_row.misses);
32                existing.dodges = existing.dodges.saturating_add(chunk_row.dodges);
33                existing.parries = existing.parries.saturating_add(chunk_row.parries);
34                existing.total_damage_x10 =
35                    sat_add_u128(existing.total_damage_x10, chunk_row.total_damage_x10);
36                existing.execute_time_ms = existing
37                    .execute_time_ms
38                    .saturating_add(chunk_row.execute_time_ms);
39                existing.resource_spent_x100 =
40                    sat_add_u128(existing.resource_spent_x100, chunk_row.resource_spent_x100);
41                existing.resource_gained_x100 = sat_add_u128(
42                    existing.resource_gained_x100,
43                    chunk_row.resource_gained_x100,
44                );
45            }
46        } else {
47            let new_idx = state.actions.len();
48
49            state.actions.push(*chunk_row);
50            index_map.insert(key, new_idx);
51        }
52    }
53}
54
55pub(super) fn merge_auras(
56    state: &mut proto::RunningAggregateStateV1,
57    chunk: &proto::ChunkTelemetry,
58) {
59    let mut index_map: FastMap<AuraKey, usize> = FastMap::default();
60
61    for (i, row) in state.auras.iter().enumerate() {
62        index_map.insert(AuraKey::from(row), i);
63    }
64
65    for chunk_row in &chunk.auras {
66        let key = AuraKey::from(chunk_row);
67
68        if let Some(&idx) = index_map.get(&key) {
69            if let Some(existing) = state.auras.get_mut(idx) {
70                existing.uptime_ms = existing.uptime_ms.saturating_add(chunk_row.uptime_ms);
71                existing.applications =
72                    existing.applications.saturating_add(chunk_row.applications);
73                existing.refreshes = existing.refreshes.saturating_add(chunk_row.refreshes);
74                existing.stack_seconds_x100 =
75                    sat_add_u128(existing.stack_seconds_x100, chunk_row.stack_seconds_x100);
76            }
77        } else {
78            let new_idx = state.auras.len();
79
80            state.auras.push(*chunk_row);
81            index_map.insert(key, new_idx);
82        }
83    }
84}
85
86pub(super) fn merge_resources(
87    state: &mut proto::RunningAggregateStateV1,
88    chunk: &proto::ChunkTelemetry,
89) {
90    let mut index_map: IntMap<u32, usize> = IntMap::default();
91
92    for (i, row) in state.resources.iter().enumerate() {
93        index_map.insert(row.resource_type, i);
94    }
95
96    for chunk_row in &chunk.resources {
97        if let Some(&idx) = index_map.get(&chunk_row.resource_type) {
98            if let Some(existing) = state.resources.get_mut(idx) {
99                existing.gained_x100 = sat_add_u128(existing.gained_x100, chunk_row.gained_x100);
100                existing.spent_x100 = sat_add_u128(existing.spent_x100, chunk_row.spent_x100);
101                existing.wasted_x100 = sat_add_u128(existing.wasted_x100, chunk_row.wasted_x100);
102                existing.time_at_cap_ms = existing
103                    .time_at_cap_ms
104                    .saturating_add(chunk_row.time_at_cap_ms);
105                existing.starved_time_ms = existing
106                    .starved_time_ms
107                    .saturating_add(chunk_row.starved_time_ms);
108
109                for source_row in &chunk_row.by_source {
110                    if let Some(existing_source) = existing
111                        .by_source
112                        .iter_mut()
113                        .find(|row| row.source_spell_id == source_row.source_spell_id)
114                    {
115                        existing_source.gained_x100 = existing_source
116                            .gained_x100
117                            .saturating_add(source_row.gained_x100);
118                        existing_source.wasted_x100 = existing_source
119                            .wasted_x100
120                            .saturating_add(source_row.wasted_x100);
121                    } else {
122                        existing.by_source.push(*source_row);
123                    }
124                }
125            }
126        } else {
127            let new_idx = state.resources.len();
128
129            // #t(rust_clone_in_loop) cold per-chunk merge; one clone per resource type
130            state.resources.push(chunk_row.clone());
131            index_map.insert(chunk_row.resource_type, new_idx);
132        }
133    }
134}
135
136pub(super) fn merge_cooldowns(
137    state: &mut proto::RunningAggregateStateV1,
138    chunk: &proto::ChunkTelemetry,
139) {
140    let mut index_map: IntMap<u32, usize> = IntMap::default();
141
142    for (i, row) in state.cooldowns.iter().enumerate() {
143        index_map.insert(row.spell_id, i);
144    }
145
146    for chunk_row in &chunk.cooldowns {
147        if let Some(&idx) = index_map.get(&chunk_row.spell_id) {
148            if let Some(existing) = state.cooldowns.get_mut(idx) {
149                existing.uses = existing.uses.saturating_add(chunk_row.uses);
150                existing.possible_uses = existing
151                    .possible_uses
152                    .saturating_add(chunk_row.possible_uses);
153                existing.drift_sum_ms =
154                    existing.drift_sum_ms.saturating_add(chunk_row.drift_sum_ms);
155                existing.max_drift_ms = existing.max_drift_ms.max(chunk_row.max_drift_ms);
156            }
157        } else {
158            let new_idx = state.cooldowns.len();
159
160            state.cooldowns.push(*chunk_row);
161            index_map.insert(chunk_row.spell_id, new_idx);
162        }
163    }
164}
165
166pub(super) fn merge_execution(
167    state: &mut proto::RunningAggregateStateV1,
168    chunk: &proto::ChunkTelemetry,
169) {
170    let Some(chunk_exec) = &chunk.execution else {
171        return;
172    };
173
174    let state_exec = state.execution.get_or_insert_with(|| proto::ExecutionData {
175        active_time_ms: 0,
176        idle_time_ms: 0,
177        gcd_locked_time_ms: 0,
178        idle_gcd_count: 0,
179        queue_lag_sum_ms: 0,
180        queue_lag_count: 0,
181        action_count_per_bucket: vec![],
182        action_bucket_samples: vec![],
183    });
184
185    state_exec.active_time_ms = state_exec
186        .active_time_ms
187        .saturating_add(chunk_exec.active_time_ms);
188    state_exec.idle_time_ms = state_exec
189        .idle_time_ms
190        .saturating_add(chunk_exec.idle_time_ms);
191    state_exec.gcd_locked_time_ms = state_exec
192        .gcd_locked_time_ms
193        .saturating_add(chunk_exec.gcd_locked_time_ms);
194    state_exec.idle_gcd_count = state_exec
195        .idle_gcd_count
196        .saturating_add(chunk_exec.idle_gcd_count);
197    state_exec.queue_lag_sum_ms = state_exec
198        .queue_lag_sum_ms
199        .saturating_add(chunk_exec.queue_lag_sum_ms);
200    state_exec.queue_lag_count = state_exec
201        .queue_lag_count
202        .saturating_add(chunk_exec.queue_lag_count);
203
204    let max_len = state_exec
205        .action_count_per_bucket
206        .len()
207        .max(chunk_exec.action_count_per_bucket.len());
208
209    state_exec.action_count_per_bucket.resize(max_len, 0);
210
211    for (slot, &val) in state_exec
212        .action_count_per_bucket
213        .iter_mut()
214        .zip(chunk_exec.action_count_per_bucket.iter())
215    {
216        *slot = slot.saturating_add(val);
217    }
218
219    let max_len = state_exec
220        .action_bucket_samples
221        .len()
222        .max(chunk_exec.action_bucket_samples.len());
223
224    state_exec.action_bucket_samples.resize(max_len, 0);
225
226    for (slot, &val) in state_exec
227        .action_bucket_samples
228        .iter_mut()
229        .zip(chunk_exec.action_bucket_samples.iter())
230    {
231        *slot = slot.saturating_add(val);
232    }
233}
234
235pub(super) fn merge_damage_profile(
236    state: &mut proto::RunningAggregateStateV1,
237    chunk: &proto::ChunkTelemetry,
238) {
239    let Some(chunk_dp) = &chunk.damage_profile else {
240        return;
241    };
242
243    let state_dp = state
244        .damage_profile
245        .get_or_insert_with(|| proto::DamageProfileData {
246            direct_damage_x10: 0,
247            periodic_damage_x10: 0,
248            pet_damage_x10: 0,
249            by_target: vec![],
250        });
251
252    state_dp.direct_damage_x10 =
253        sat_add_u128(state_dp.direct_damage_x10, chunk_dp.direct_damage_x10);
254    state_dp.periodic_damage_x10 =
255        sat_add_u128(state_dp.periodic_damage_x10, chunk_dp.periodic_damage_x10);
256    state_dp.pet_damage_x10 = sat_add_u128(state_dp.pet_damage_x10, chunk_dp.pet_damage_x10);
257
258    let mut index_map: IntMap<u32, usize> = IntMap::default();
259
260    for (i, t) in state_dp.by_target.iter().enumerate() {
261        index_map.insert(t.target_id, i);
262    }
263
264    for chunk_t in &chunk_dp.by_target {
265        if let Some(&idx) = index_map.get(&chunk_t.target_id) {
266            if let Some(existing) = state_dp.by_target.get_mut(idx) {
267                existing.total_damage_x10 =
268                    sat_add_u128(existing.total_damage_x10, chunk_t.total_damage_x10);
269            }
270        } else {
271            let new_idx = state_dp.by_target.len();
272
273            state_dp.by_target.push(*chunk_t);
274            index_map.insert(chunk_t.target_id, new_idx);
275        }
276    }
277}
278
279pub(super) fn merge_dps_buckets(
280    state: &mut proto::RunningAggregateStateV1,
281    chunk: &proto::ChunkTelemetry,
282) {
283    if state.bucket_ms == 0 && chunk.bucket_ms != 0 {
284        state.bucket_ms = chunk.bucket_ms;
285    }
286
287    let max_len = state
288        .dps_bucket_sums_x10
289        .len()
290        .max(chunk.dps_bucket_sums_x10.len());
291
292    state.dps_bucket_sums_x10.resize(max_len, 0);
293
294    for (slot, &val) in state
295        .dps_bucket_sums_x10
296        .iter_mut()
297        .zip(chunk.dps_bucket_sums_x10.iter())
298    {
299        *slot = sat_add_u128(*slot, val);
300    }
301
302    let max_len = state
303        .dps_bucket_samples
304        .len()
305        .max(chunk.dps_bucket_samples.len());
306
307    state.dps_bucket_samples.resize(max_len, 0);
308
309    for (slot, &val) in state
310        .dps_bucket_samples
311        .iter_mut()
312        .zip(chunk.dps_bucket_samples.iter())
313    {
314        *slot = slot.saturating_add(val);
315    }
316}
317
318pub(super) fn merge_representative(
319    state: &mut proto::RunningAggregateStateV1,
320    chunk: &proto::ChunkTelemetry,
321) {
322    // Multi-chunk aggregation has no exact representative: a chunk-local winner isn't the multi-chunk mean.
323    if state.chunks_total != 1 {
324        state.representative = None;
325        state.representative_meta = None;
326
327        return;
328    }
329
330    let candidate_dps_x10 = chunk.representative_dps_x10;
331    let candidate_timeline = &chunk.representative;
332
333    if candidate_timeline.is_none() {
334        return;
335    }
336
337    let candidate_chunk_index = chunk.chunk_index;
338    let candidate_iteration_index: u32 = 0;
339
340    state.representative.clone_from(candidate_timeline);
341    state.representative_meta = Some(proto::RepresentativeMeta {
342        chunk_index: candidate_chunk_index,
343        iteration_index: candidate_iteration_index,
344        winner_dps_x10: candidate_dps_x10,
345    });
346}
347
348pub(super) fn merge_dictionary(
349    state: &mut proto::RunningAggregateStateV1,
350    chunk: &proto::ChunkTelemetry,
351) -> Result<(), MergeError> {
352    let Some(chunk_dict) = &chunk.dictionary else {
353        return Ok(());
354    };
355
356    let state_dict = state
357        .dictionary
358        .get_or_insert_with(|| proto::DictionaryView {
359            spell_ids: vec![],
360            aura_ids: vec![],
361            targets: vec![],
362            units: vec![],
363        });
364
365    for &id in &chunk_dict.spell_ids {
366        if !state_dict.spell_ids.contains(&id) {
367            state_dict.spell_ids.push(id);
368        }
369    }
370
371    for &id in &chunk_dict.aura_ids {
372        if !state_dict.aura_ids.contains(&id) {
373            state_dict.aura_ids.push(id);
374        }
375    }
376
377    let known_ids: IntMap<u32, usize> = state_dict
378        .targets
379        .iter()
380        .enumerate()
381        .map(|(i, t)| (t.id, i))
382        .collect();
383
384    let mut new_indices = Vec::with_capacity(chunk_dict.targets.len());
385
386    for (i, chunk_t) in chunk_dict.targets.iter().enumerate() {
387        if let Some(existing_index) = known_ids.get(&chunk_t.id).copied() {
388            if state_dict.targets.get(existing_index) != Some(chunk_t) {
389                return Err(MergeError::dictionary_label_conflict(chunk_t.id));
390            }
391        } else {
392            new_indices.push(i);
393        }
394    }
395
396    let new_targets: Vec<_> = new_indices
397        .iter()
398        .filter_map(|&i| chunk_dict.targets.get(i).cloned())
399        .collect();
400
401    state_dict.targets.extend(new_targets);
402
403    let mut known_units: FastMap<(i32, u32), usize> = state_dict
404        .units
405        .iter()
406        .enumerate()
407        .map(|(index, unit)| ((unit.kind, unit.id), index))
408        .collect();
409    let mut new_units = Vec::with_capacity(chunk_dict.units.len());
410
411    for unit in &chunk_dict.units {
412        let key = (unit.kind, unit.id);
413
414        if let Some(index) = known_units.get(&key).copied() {
415            if state_dict.units.get(index) != Some(unit) {
416                return Err(MergeError::dictionary_label_conflict(
417                    unit.target_id.unwrap_or(unit.id),
418                ));
419            }
420        } else {
421            known_units.insert(key, state_dict.units.len() + new_units.len());
422            new_units.push(unit);
423        }
424    }
425
426    state_dict.units.extend(new_units.into_iter().cloned());
427
428    Ok(())
429}
430
431pub(super) fn merge_instrumentation_coverage(
432    state: &mut proto::RunningAggregateStateV1,
433    chunk: &proto::ChunkTelemetry,
434) {
435    let Some(chunk_ic) = &chunk.instrumentation_coverage else {
436        return;
437    };
438
439    let state_ic = state
440        .instrumentation_coverage
441        .get_or_insert(proto::InstrumentationCoverage {
442            has_resource_cap_time: true,
443            has_resource_starvation_time: true,
444            has_cooldown_drift: true,
445            has_phase_markers: true,
446            has_proc_aggregates: true,
447        });
448
449    state_ic.has_resource_cap_time =
450        state_ic.has_resource_cap_time && chunk_ic.has_resource_cap_time;
451    state_ic.has_resource_starvation_time =
452        state_ic.has_resource_starvation_time && chunk_ic.has_resource_starvation_time;
453    state_ic.has_cooldown_drift = state_ic.has_cooldown_drift && chunk_ic.has_cooldown_drift;
454    state_ic.has_phase_markers = state_ic.has_phase_markers && chunk_ic.has_phase_markers;
455    state_ic.has_proc_aggregates = state_ic.has_proc_aggregates && chunk_ic.has_proc_aggregates;
456}