wowlab_analytics/analytics/merge/
sections.rs1use 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 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 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}