1use crate::time::Instant;
2use std::collections::BTreeMap;
3use std::fmt;
4use std::num::NonZeroUsize;
5use std::sync::Arc;
6
7use crate::{AppFeature, Measurements, ResourceId};
8use crate::{CogsMeasurement, CogsRecorder, Value};
9
10#[derive(Clone)]
14pub struct Cogs {
15 recorder: Arc<dyn CogsRecorder>,
16}
17
18impl Cogs {
19 pub fn new<T>(recorder: T) -> Self
21 where
22 T: CogsRecorder + 'static,
23 {
24 Self {
25 recorder: Arc::new(recorder),
26 }
27 }
28
29 pub fn noop() -> Self {
33 Self::new(crate::NoopRecorder)
34 }
35}
36
37impl Cogs {
38 pub fn timed<F: Into<FeatureWeights>>(&self, resource: ResourceId, weights: F) -> Token {
64 Token {
65 resource,
66 features: weights.into(),
67 measurements: Measurements::start(),
68 recorder: Some(Arc::clone(&self.recorder)),
69 }
70 }
71}
72
73#[must_use]
77pub struct Token {
78 resource: ResourceId,
79 features: FeatureWeights,
80 measurements: Measurements,
81 recorder: Option<Arc<dyn CogsRecorder>>,
82}
83
84impl Token {
85 pub fn noop() -> Self {
89 Self {
90 resource: ResourceId::Relay,
91 features: FeatureWeights::none(),
92 measurements: Measurements::start(),
93 recorder: None,
94 }
95 }
96
97 pub fn cancel(&mut self) {
99 self.update(FeatureWeights::none());
101 }
102
103 pub fn start_category(&mut self, category: impl Category) -> CategoryToken<'_> {
110 CategoryToken {
111 parent: self,
112 start: Instant::now(),
113 category: category.name(),
114 }
115 }
116
117 pub fn update<T: Into<FeatureWeights>>(&mut self, features: T) {
138 self.features = features.into();
139 }
140}
141
142impl Drop for Token {
143 fn drop(&mut self) {
144 let Some(recorder) = self.recorder.as_mut() else {
145 return;
146 };
147
148 let measurements = self.measurements.finish();
149
150 for measurement in measurements {
151 for (feature, ratio) in self.features.weights() {
152 let time = measurement.duration.mul_f32(ratio);
153 recorder.record(CogsMeasurement {
154 resource: self.resource,
155 feature,
156 category: measurement.category,
157 value: Value::Time(time),
158 });
159 }
160 }
161 }
162}
163
164impl fmt::Debug for Token {
165 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
166 f.debug_struct("CogsToken")
167 .field("resource", &self.resource)
168 .field("features", &self.features)
169 .finish()
170 }
171}
172
173pub trait Category {
175 fn name(&self) -> &'static str;
177}
178
179impl Category for &'static str {
180 fn name(&self) -> &'static str {
181 self
182 }
183}
184
185#[must_use]
189pub struct CategoryToken<'a> {
190 parent: &'a mut Token,
191 start: Instant,
192 category: &'static str,
193}
194
195impl Drop for CategoryToken<'_> {
196 fn drop(&mut self) {
197 self.parent
198 .measurements
199 .add(self.start.elapsed(), self.category);
200 }
201}
202
203impl fmt::Debug for CategoryToken<'_> {
204 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
205 f.debug_struct("CategoryToken")
206 .field("resource", &self.parent.resource)
207 .field("features", &self.parent.features)
208 .field("category", &self.category)
209 .finish()
210 }
211}
212
213#[derive(Clone)]
217pub struct FeatureWeights(BTreeMap<AppFeature, NonZeroUsize>);
218
219impl FeatureWeights {
220 pub fn new(feature: AppFeature) -> Self {
222 Self::builder().weight(feature, 1).build()
223 }
224
225 pub fn none() -> Self {
227 Self::builder().build()
228 }
229
230 pub fn builder() -> FeatureWeightsBuilder {
232 FeatureWeightsBuilder(Self(Default::default()))
233 }
234
235 pub fn merge(mut self, other: Self) -> Self {
237 for (feature, weight) in other.0.into_iter() {
238 if let Some(w) = self.0.get_mut(&feature) {
239 *w = w.saturating_add(weight.get());
240 } else {
241 self.0.insert(feature, weight);
242 }
243 }
244
245 self
246 }
247
248 pub fn weights(&self) -> impl Iterator<Item = (AppFeature, f32)> + '_ {
268 let total_weight: usize = self.0.values().map(|weight| weight.get()).sum();
269
270 self.0.iter().filter_map(move |(feature, weight)| {
271 if total_weight == 0 {
272 return None;
273 }
274
275 let ratio = (weight.get() as f32 / total_weight as f32).clamp(0.0, 1.0);
276 Some((*feature, ratio))
277 })
278 }
279
280 pub fn is_empty(&self) -> bool {
291 self.0.is_empty()
292 }
293}
294
295impl fmt::Debug for FeatureWeights {
296 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
297 write!(f, "FeatureWeights(")?;
298
299 let mut first = true;
300 for (feature, weight) in self.weights() {
301 if !first {
302 first = false;
303 write!(f, ", ")?;
304 }
305 write!(f, "{feature:?}={weight:.2}")?;
306 }
307 write!(f, ")")
308 }
309}
310
311impl From<AppFeature> for FeatureWeights {
312 fn from(value: AppFeature) -> Self {
313 Self::new(value)
314 }
315}
316
317pub struct FeatureWeightsBuilder(FeatureWeights);
319
320impl FeatureWeightsBuilder {
321 pub fn add_weight(&mut self, feature: AppFeature, weight: usize) -> &mut Self {
323 let Some(weight) = NonZeroUsize::new(weight) else {
324 return self;
325 };
326
327 if let Some(previous) = self.0.0.get_mut(&feature) {
328 *previous = previous.saturating_add(weight.get());
329 } else {
330 self.0.0.insert(feature, weight);
331 }
332
333 self
334 }
335
336 pub fn weight(&mut self, feature: AppFeature, weight: usize) -> &mut Self {
338 if let Some(weight) = NonZeroUsize::new(weight) {
339 self.0.0.insert(feature, weight);
340 } else {
341 self.0.0.remove(&feature);
342 }
343 self
344 }
345
346 pub fn build(&mut self) -> FeatureWeights {
348 std::mem::replace(self, FeatureWeights::builder()).0
349 }
350}
351
352#[cfg(test)]
353mod tests {
354 use std::collections::HashMap;
355
356 use super::*;
357 use crate::test::TestRecorder;
358
359 #[test]
360 fn test_cogs_simple() {
361 let recorder = TestRecorder::default();
362 let cogs = Cogs::new(recorder.clone());
363
364 drop(cogs.timed(ResourceId::Relay, AppFeature::Spans));
365
366 let measurements = recorder.measurements();
367 insta::assert_debug_snapshot!(measurements, @r###"
368 [
369 CogsMeasurement {
370 resource: Relay,
371 feature: Spans,
372 category: None,
373 value: Time(
374 100ns,
375 ),
376 },
377 ]
378 "###);
379 }
380
381 #[test]
382 fn test_cogs_multiple_weights() {
383 let recorder = TestRecorder::default();
384 let cogs = Cogs::new(recorder.clone());
385
386 let f = FeatureWeights::builder()
387 .weight(AppFeature::Spans, 1)
388 .weight(AppFeature::Transactions, 1)
389 .weight(AppFeature::MetricsSpans, 0) .add_weight(AppFeature::MetricsSpans, 1)
391 .weight(AppFeature::Transactions, 0) .build();
393 {
394 let _token = cogs.timed(ResourceId::Relay, f);
395 crate::time::advance_millis(50);
396 }
397
398 let measurements = recorder.measurements();
399 insta::assert_debug_snapshot!(measurements, @r###"
400 [
401 CogsMeasurement {
402 resource: Relay,
403 feature: Spans,
404 category: None,
405 value: Time(
406 25ms,
407 ),
408 },
409 CogsMeasurement {
410 resource: Relay,
411 feature: MetricsSpans,
412 category: None,
413 value: Time(
414 25ms,
415 ),
416 },
417 ]
418 "###);
419 }
420
421 #[test]
422 fn test_cogs_categorized() {
423 let recorder = TestRecorder::default();
424 let cogs = Cogs::new(recorder.clone());
425
426 let features = FeatureWeights::builder()
427 .weight(AppFeature::Spans, 1)
428 .weight(AppFeature::Errors, 1)
429 .build();
430
431 {
432 let mut token = cogs.timed(ResourceId::Relay, features);
433 crate::time::advance_millis(10);
434 crate::with!(token, "s1", {
435 crate::time::advance_millis(6);
436 });
437 crate::time::advance_millis(20);
438 let _category = token.start_category("s2");
439 crate::time::advance_millis(12);
440 }
441
442 let measurements = recorder.measurements();
443 insta::assert_debug_snapshot!(measurements, @r###"
444 [
445 CogsMeasurement {
446 resource: Relay,
447 feature: Errors,
448 category: None,
449 value: Time(
450 15ms,
451 ),
452 },
453 CogsMeasurement {
454 resource: Relay,
455 feature: Spans,
456 category: None,
457 value: Time(
458 15ms,
459 ),
460 },
461 CogsMeasurement {
462 resource: Relay,
463 feature: Errors,
464 category: Some(
465 "s1",
466 ),
467 value: Time(
468 3ms,
469 ),
470 },
471 CogsMeasurement {
472 resource: Relay,
473 feature: Spans,
474 category: Some(
475 "s1",
476 ),
477 value: Time(
478 3ms,
479 ),
480 },
481 CogsMeasurement {
482 resource: Relay,
483 feature: Errors,
484 category: Some(
485 "s2",
486 ),
487 value: Time(
488 6ms,
489 ),
490 },
491 CogsMeasurement {
492 resource: Relay,
493 feature: Spans,
494 category: Some(
495 "s2",
496 ),
497 value: Time(
498 6ms,
499 ),
500 },
501 ]
502 "###);
503 }
504
505 #[test]
506 fn test_app_features_none() {
507 let a = FeatureWeights::none();
508 assert_eq!(a.weights().count(), 0);
509 }
510
511 #[test]
512 fn test_app_features_new() {
513 let a = FeatureWeights::new(AppFeature::Spans);
514 assert_eq!(
515 a.weights().collect::<Vec<_>>(),
516 vec![(AppFeature::Spans, 1.0)]
517 );
518 }
519
520 #[test]
521 fn test_app_features_merge() {
522 let a = FeatureWeights::builder()
523 .weight(AppFeature::Spans, 1)
524 .weight(AppFeature::Transactions, 2)
525 .build();
526
527 let b = FeatureWeights::builder()
528 .weight(AppFeature::Spans, 2)
529 .weight(AppFeature::Unattributed, 5)
530 .build();
531
532 let c = FeatureWeights::merge(FeatureWeights::none(), FeatureWeights::merge(a, b));
533
534 let weights: HashMap<_, _> = c.weights().collect();
535 assert_eq!(
536 weights,
537 HashMap::from([
538 (AppFeature::Spans, 0.3),
539 (AppFeature::Transactions, 0.2),
540 (AppFeature::Unattributed, 0.5),
541 ])
542 )
543 }
544}