Skip to main content

wowlab_engine_sim/telemetry/
merge.rs

1//! Parallel-chunk merge of two telemetry accumulators.
2
3use wowlab_types::stats::simd;
4
5use super::TelemetryAccumulator;
6
7impl TelemetryAccumulator {
8    /// Merge another accumulator into this one (for parallel execution).
9    pub fn merge(&mut self, mut other: Self) {
10        // Must run before sums are updated below.
11        let combined_sum = self.dps_sum + other.dps_sum;
12        let combined_count = self.iteration_count + other.iteration_count;
13        // Chan's parallel-Welford combine (must run before counts/means are overwritten): m2 = m2_a + m2_b + delta^2 * n_a * n_b / n.
14        let n_a = f64::from(self.iteration_count);
15        let n_b = f64::from(other.iteration_count);
16
17        if combined_count > 0 {
18            let n = f64::from(combined_count);
19            let mean_delta = other.dps_welford_mean - self.dps_welford_mean;
20
21            self.dps_m2 += other.dps_m2 + mean_delta * mean_delta * n_a * n_b / n;
22            self.dps_welford_mean =
23                (n_a * self.dps_welford_mean + n_b * other.dps_welford_mean) / n;
24        }
25
26        self.representative_candidates
27            .append(&mut other.representative_candidates);
28        // A representative captured against a chunk-local mean is not exact for the combined mean; the execution layer must reselect and rerun.
29        self.representative_dps = None;
30        self.representative_sink = None;
31
32        self.iteration_count = combined_count;
33        self.dps_sum = combined_sum;
34        self.dps_min = self.dps_min.min(other.dps_min);
35        self.dps_max = self.dps_max.max(other.dps_max);
36
37        for (k, v) in other.spell_totals {
38            let entry = self.spell_totals.entry(k).or_default();
39
40            entry.damage += v.damage;
41            entry.casts += v.casts;
42            entry.crits += v.crits;
43            entry.hits += v.hits;
44            entry.ticks += v.ticks;
45        }
46
47        if let Err(e) = self.dps_histogram.add(&other.dps_histogram) {
48            tracing::warn!(error = %e, "failed to merge DPS histogram across chunks");
49        }
50
51        for (k, v) in other.aura_totals {
52            let entry = self.aura_totals.entry(k).or_default();
53
54            entry.uptime_ms += v.uptime_ms;
55            entry.applications += v.applications;
56            entry.refreshes += v.refreshes;
57        }
58
59        for (k, v) in other.resource_totals {
60            let entry = self.resource_totals.entry(k).or_default();
61
62            entry.total_gained += v.total_gained;
63            entry.total_spent += v.total_spent;
64            entry.total_wasted += v.total_wasted;
65
66            for (source_spell_id, source) in v.gained_by_source {
67                let merged_source = entry.gained_by_source.entry(source_spell_id).or_default();
68
69                merged_source.gained += source.gained;
70                merged_source.wasted += source.wasted;
71            }
72        }
73
74        for (k, v) in other.cooldown_totals {
75            let entry = self.cooldown_totals.entry(k).or_default();
76
77            entry.total_uses += v.total_uses;
78        }
79
80        self.total_duration_ms += other.total_duration_ms;
81        self.gcd_locked_ms_total += other.gcd_locked_ms_total;
82        self.direct_damage_total += other.direct_damage_total;
83        self.periodic_damage_total += other.periodic_damage_total;
84        self.pet_damage_total += other.pet_damage_total;
85
86        for (target, damage) in other.damage_by_target {
87            *self.damage_by_target.entry(target).or_insert(0.0) += damage;
88        }
89
90        for (target, metadata) in other.targets {
91            self.targets.entry(target).or_insert(metadata);
92        }
93
94        if other.bucket_sums.len() > self.bucket_sums.len() {
95            self.bucket_sums.resize(other.bucket_sums.len(), 0.0);
96        }
97
98        simd::add_f64_slices(&mut self.bucket_sums, &other.bucket_sums);
99
100        if other.bucket_samples.len() > self.bucket_samples.len() {
101            self.bucket_samples.resize(other.bucket_samples.len(), 0);
102        }
103
104        for (dst, src) in self.bucket_samples.iter_mut().zip(&other.bucket_samples) {
105            *dst += *src;
106        }
107
108        self.trace_extras_enabled = self.trace_extras_enabled || other.trace_extras_enabled;
109    }
110}