1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
/// Global event counter with decay for proportion tracking.
///
/// Users create one `FlexProportionGlobal` and multiple `FlexProportionEntity`
/// instances. Each entity tracks its share of the global total with temporal
/// decay — old activity fades, recent activity dominates.
///
/// # Usage
///
/// The user is responsible for calling `global.record()` once per event.
/// Entities take the current period as a plain `u64` — no mutable reference
/// to the global is needed.
///
/// ```ignore
/// let mut global = FlexProportionGlobal::new(1000);
/// let mut entity_a = FlexProportionEntity::new();
///
/// // Record an event for entity A
/// global.record();
/// entity_a.record(global.period());
///
/// // Query fraction
/// let frac = entity_a.fraction(global.total(), global.period());
/// ```
///
/// # Use Cases
/// - "What fraction of total traffic goes to each venue?"
/// - Fair-share scheduling input
/// - Dynamic load distribution tracking
#[derive(Debug, Clone)]
pub struct FlexProportionGlobal {
total: u64,
half_life: u64,
period: u64,
}
/// Per-entity event counter for proportion tracking.
///
/// Decoupled from the global tracker — takes plain values instead of
/// references. The user calls `global.record()` separately.
#[derive(Debug, Clone)]
pub struct FlexProportionEntity {
count: u64,
period: u64,
}
impl FlexProportionGlobal {
/// Creates a new global tracker.
///
/// `half_life_events` is the number of global events after which old
/// contributions decay by half.
#[inline]
pub fn new(half_life_events: u64) -> Result<Self, crate::ConfigError> {
if half_life_events == 0 {
return Err(crate::ConfigError::Invalid(
"half_life_events must be positive",
));
}
Ok(Self {
total: 0,
half_life: half_life_events,
period: 0,
})
}
/// Records a global event. Call this once per event, before recording
/// on the entity.
#[inline]
pub fn record(&mut self) {
self.total += 1;
if self.total % self.half_life == 0 {
self.period += 1;
}
}
/// Total global events recorded.
#[inline]
#[must_use]
pub fn total(&self) -> u64 {
self.total
}
/// Current decay period.
#[inline]
#[must_use]
pub fn period(&self) -> u64 {
self.period
}
}
impl FlexProportionEntity {
/// Creates a new entity tracker.
#[inline]
#[must_use]
pub fn new() -> Self {
Self {
count: 0,
period: 0,
}
}
/// Records an event for this entity.
///
/// Pass the current global period (from `global.period()`). The entity
/// applies decay catch-up if the period has advanced, then increments
/// its count.
///
/// **Important:** Call `global.record()` separately — this method does
/// NOT update the global tracker.
#[inline]
pub fn record(&mut self, current_period: u64) {
while self.period < current_period {
self.count /= 2;
self.period += 1;
}
self.count += 1;
}
/// Fraction of global total attributed to this entity (0.0 to 1.0).
///
/// Pass the current global total and period. Returns 0.0 if total is zero.
#[inline]
#[must_use]
pub fn fraction(&self, total: u64, current_period: u64) -> f64 {
if total == 0 {
return 0.0;
}
// Decay count to current period
let mut count = self.count;
let mut period = self.period;
while period < current_period {
count /= 2;
period += 1;
}
count as f64 / total as f64
}
/// This entity's current (possibly decayed) event count.
#[inline]
#[must_use]
pub fn count(&self) -> u64 {
self.count
}
/// Resets this entity's count.
#[inline]
pub fn reset(&mut self) {
self.count = 0;
self.period = 0;
}
}
impl Default for FlexProportionEntity {
#[inline]
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn single_entity_full_share() {
let mut global = FlexProportionGlobal::new(100).unwrap();
let mut entity = FlexProportionEntity::new();
for _ in 0..50 {
global.record();
entity.record(global.period());
}
let frac = entity.fraction(global.total(), global.period());
assert!(frac > 0.0, "single entity should have positive fraction");
}
#[test]
fn equal_entities_equal_share() {
let mut global = FlexProportionGlobal::new(1000).unwrap();
let mut e1 = FlexProportionEntity::new();
let mut e2 = FlexProportionEntity::new();
for _ in 0..100 {
global.record();
e1.record(global.period());
global.record();
e2.record(global.period());
}
let f1 = e1.fraction(global.total(), global.period());
let f2 = e2.fraction(global.total(), global.period());
assert!(
(f1 - f2).abs() < 0.1,
"equal entities should have equal fraction: {f1} vs {f2}"
);
}
#[test]
fn new_entity_ramps_up() {
let mut global = FlexProportionGlobal::new(100).unwrap();
let mut old = FlexProportionEntity::new();
for _ in 0..50 {
global.record();
old.record(global.period());
}
let mut new = FlexProportionEntity::new();
for _ in 0..10 {
global.record();
new.record(global.period());
}
let f_new = new.fraction(global.total(), global.period());
assert!(f_new > 0.0, "new entity should have some fraction");
}
#[test]
#[allow(clippy::float_cmp)]
fn empty_global() {
let global = FlexProportionGlobal::new(100).unwrap();
let entity = FlexProportionEntity::new();
assert_eq!(entity.fraction(global.total(), global.period()), 0.0);
}
#[test]
fn reset_entity() {
let mut global = FlexProportionGlobal::new(100).unwrap();
let mut entity = FlexProportionEntity::new();
for _ in 0..20 {
global.record();
entity.record(global.period());
}
entity.reset();
assert_eq!(entity.count(), 0);
}
#[test]
fn default_entity() {
let entity = FlexProportionEntity::default();
assert_eq!(entity.count(), 0);
}
#[test]
fn rejects_zero_half_life() {
assert!(matches!(
FlexProportionGlobal::new(0),
Err(crate::ConfigError::Invalid(_))
));
}
}