saluki_common/resource_tracking/
groups.rs1use std::{
2 cell::RefCell,
3 collections::HashMap,
4 future::Future,
5 marker::PhantomData,
6 pin::Pin,
7 ptr::NonNull,
8 sync::{Mutex, OnceLock},
9 task::{Context, Poll},
10};
11
12use pin_project::pin_project;
13
14use super::stats::{thread_cpu_time_nanos, ResourceStats};
15
16static REGISTRY: OnceLock<ResourceGroupRegistry> = OnceLock::new();
17static ROOT_GROUP: ResourceStats = ResourceStats::new();
18
19thread_local! {
20 pub(super) static CURRENT_GROUP: RefCell<NonNull<ResourceStats>> = RefCell::new(NonNull::from(&ROOT_GROUP));
21}
22
23#[derive(Clone, Copy)]
28pub struct ResourceGroupToken {
29 group_ptr: NonNull<ResourceStats>,
30}
31
32impl ResourceGroupToken {
33 fn new(group_ptr: NonNull<ResourceStats>) -> Self {
34 Self { group_ptr }
35 }
36
37 pub fn current() -> Self {
39 CURRENT_GROUP.with(|current_group| {
40 let group_ptr = current_group.borrow();
41 Self::new(*group_ptr)
42 })
43 }
44
45 #[cfg(test)]
46 fn ptr_eq(&self, other: &Self) -> bool {
47 self.group_ptr == other.group_ptr
48 }
49
50 pub fn root() -> Self {
52 Self::new(NonNull::from(&ROOT_GROUP))
53 }
54
55 pub fn enter(&self) -> ResourceTrackingGuard<'_> {
57 let thread_cpu_usage_start = thread_cpu_time_nanos().unwrap_or(0);
59
60 CURRENT_GROUP.with(|current_group| {
62 let mut group_ptr = current_group.borrow_mut();
63 let previous_group_ptr = *group_ptr;
64 *group_ptr = self.group_ptr;
65
66 ResourceTrackingGuard {
67 previous_group_ptr,
68 thread_cpu_usage_start,
69 _token: PhantomData,
70 }
71 })
72 }
73}
74
75unsafe impl Send for ResourceGroupToken {}
77
78unsafe impl Sync for ResourceGroupToken {}
81
82pub struct ResourceTrackingGuard<'a> {
89 previous_group_ptr: NonNull<ResourceStats>,
90 thread_cpu_usage_start: u64,
91 _token: PhantomData<&'a ResourceGroupToken>,
92}
93
94impl Drop for ResourceTrackingGuard<'_> {
95 fn drop(&mut self) {
96 let thread_cpu_usage_end = thread_cpu_time_nanos().unwrap_or(0);
98 let cpu_usage_delta = thread_cpu_usage_end.saturating_sub(self.thread_cpu_usage_start);
99
100 CURRENT_GROUP.with(|current_group| {
102 let mut group_ptr = current_group.borrow_mut();
103
104 if cpu_usage_delta != 0 {
106 unsafe { group_ptr.as_ref().track_cpu_time(cpu_usage_delta) }
109 }
110
111 *group_ptr = self.previous_group_ptr;
112 });
113 }
114}
115
116#[pin_project]
129#[must_use = "futures do nothing unless you `.await` or poll them"]
130pub struct Tracked<Inner> {
131 token: ResourceGroupToken,
132
133 #[pin]
134 inner: Inner,
135}
136
137impl<Inner> Tracked<Inner> {
138 pub fn into_parts(self) -> (ResourceGroupToken, Inner) {
140 (self.token, self.inner)
141 }
142}
143
144impl<Inner> Future for Tracked<Inner>
145where
146 Inner: Future,
147{
148 type Output = Inner::Output;
149
150 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
151 let this = self.project();
152 let _enter = this.token.enter();
153
154 this.inner.poll(cx)
155 }
156}
157
158pub trait Track: Sized {
160 fn track_resources(self, token: ResourceGroupToken) -> Tracked<Self> {
181 Tracked { token, inner: self }
182 }
183
184 fn in_current_resource_group(self) -> Tracked<Self> {
212 Tracked {
213 token: ResourceGroupToken::current(),
214 inner: self,
215 }
216 }
217}
218
219impl<T: Sized> Track for T {}
220
221pub struct ResourceGroupRegistry {
252 resource_groups: Mutex<HashMap<String, Box<ResourceStats>>>,
253}
254
255impl ResourceGroupRegistry {
256 fn new() -> Self {
257 in_root_resource_group(|| Self {
258 resource_groups: Mutex::new(HashMap::with_capacity(4)),
259 })
260 }
261
262 pub fn global() -> &'static Self {
264 REGISTRY.get_or_init(Self::new)
265 }
266
267 pub fn allocator_installed() -> bool {
269 ROOT_GROUP.has_allocated()
275 }
276
277 pub fn register_resource_group<S>(&self, name: S) -> ResourceGroupToken
282 where
283 S: AsRef<str>,
284 {
285 in_root_resource_group(|| {
286 let mut resource_groups = self.resource_groups.lock().unwrap();
287 match resource_groups.get(name.as_ref()) {
288 Some(stats) => ResourceGroupToken::new(NonNull::from(&**stats)),
289 None => {
290 let resource_group_stats = Box::new(ResourceStats::new());
291 let token = ResourceGroupToken::new(NonNull::from(&*resource_group_stats));
292
293 resource_groups.insert(name.as_ref().to_string(), resource_group_stats);
294
295 token
296 }
297 }
298 })
299 }
300
301 pub fn visit_resource_groups<F>(&self, mut f: F)
303 where
304 F: FnMut(&str, &ResourceStats),
305 {
306 in_root_resource_group(|| {
307 f("root", &ROOT_GROUP);
308
309 let resource_groups = self.resource_groups.lock().unwrap();
310 for (name, stats) in resource_groups.iter() {
311 f(name, stats);
312 }
313 });
314 }
315}
316
317fn in_root_resource_group<F, R>(f: F) -> R
318where
319 F: FnOnce() -> R,
320{
321 let token = ResourceGroupToken::root();
322 let _enter = token.enter();
323 f()
324}
325
326#[cfg(test)]
327mod tests {
328 use std::{
329 cell::Cell,
330 future::Future,
331 pin::Pin,
332 rc::Rc,
333 sync::Arc,
334 task::{Context, Poll, Wake, Waker},
335 };
336
337 use super::{ResourceGroupRegistry, ResourceGroupToken, Track};
338
339 struct NoopWaker;
340
341 impl Wake for NoopWaker {
342 fn wake(self: Arc<Self>) {}
343 }
344
345 fn poll_to_completion<F: Future>(future: F) -> F::Output {
347 let mut future = Box::pin(future);
348 let waker = Waker::from(Arc::new(NoopWaker));
349 let mut cx = Context::from_waker(&waker);
350 loop {
351 if let Poll::Ready(output) = future.as_mut().poll(&mut cx) {
352 return output;
353 }
354 }
355 }
356
357 struct RecordCurrentGroup {
359 expected: ResourceGroupToken,
360 matched: Rc<Cell<bool>>,
361 }
362
363 impl Future for RecordCurrentGroup {
364 type Output = ();
365
366 fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Self::Output> {
367 self.matched.set(ResourceGroupToken::current().ptr_eq(&self.expected));
368 Poll::Ready(())
369 }
370 }
371
372 #[cfg(target_os = "linux")]
375 fn cpu_time_nanos_for(registry: &ResourceGroupRegistry, target: &str) -> u64 {
376 use crate::resource_tracking::ResourceStatsSnapshot;
377
378 let mut cpu_time_nanos = 0;
379 registry.visit_resource_groups(|name, stats| {
380 if name == target {
381 cpu_time_nanos = stats.snapshot_delta(&ResourceStatsSnapshot::empty()).cpu_time_nanos;
382 }
383 });
384 cpu_time_nanos
385 }
386
387 #[cfg(target_os = "linux")]
388 fn burn_cpu() {
389 let mut sum = 0u64;
390 for i in 0..20_000_000u64 {
391 sum = sum.wrapping_add(i);
392 }
393 std::hint::black_box(sum);
394 }
395
396 #[test]
397 fn existing_group() {
398 let registry = ResourceGroupRegistry::new();
399 let token = registry.register_resource_group("test");
400 let token2 = registry.register_resource_group("test");
401 let token3 = registry.register_resource_group("test2");
402
403 assert!(token.ptr_eq(&token2));
404 assert!(!token.ptr_eq(&token3));
405 }
406
407 #[test]
408 fn visit_resource_groups() {
409 let registry = ResourceGroupRegistry::new();
410 let _token = registry.register_resource_group("my-group");
411
412 let mut visited = Vec::new();
413 registry.visit_resource_groups(|name, _stats| {
414 visited.push(name.to_string());
415 });
416
417 assert_eq!(visited.len(), 2);
418 assert_eq!(visited[0], "root");
419 assert_eq!(visited[1], "my-group");
420 }
421
422 #[test]
423 fn enter_swaps_current_group_and_restores_previous_on_drop() {
424 let registry = ResourceGroupRegistry::new();
425 let group = registry.register_resource_group("group-a");
426 let previous = ResourceGroupToken::current();
427
428 {
429 let _guard = group.enter();
430 assert!(
431 ResourceGroupToken::current().ptr_eq(&group),
432 "entering a group should make it the current group"
433 );
434 }
435
436 assert!(
437 ResourceGroupToken::current().ptr_eq(&previous),
438 "dropping the guard should restore the previously-entered group"
439 );
440 }
441
442 #[test]
443 fn nested_groups_restore_in_lifo_order() {
444 let registry = ResourceGroupRegistry::new();
445 let outer = registry.register_resource_group("outer");
446 let inner = registry.register_resource_group("inner");
447 let root = ResourceGroupToken::current();
448
449 let outer_guard = outer.enter();
450 assert!(ResourceGroupToken::current().ptr_eq(&outer));
451
452 {
453 let _inner_guard = inner.enter();
454 assert!(ResourceGroupToken::current().ptr_eq(&inner));
455 }
456
457 assert!(ResourceGroupToken::current().ptr_eq(&outer));
459
460 drop(outer_guard);
461 assert!(ResourceGroupToken::current().ptr_eq(&root));
462 }
463
464 #[test]
465 fn tracked_future_enters_attached_group_during_poll() {
466 let registry = ResourceGroupRegistry::new();
467 let group = registry.register_resource_group("tracked");
468 let previous = ResourceGroupToken::current();
469
470 let matched = Rc::new(Cell::new(false));
471 let future = RecordCurrentGroup {
472 expected: group,
473 matched: Rc::clone(&matched),
474 }
475 .track_resources(group);
476
477 poll_to_completion(future);
478
479 assert!(
480 matched.get(),
481 "the attached group should be the current group while the future is polled"
482 );
483 assert!(
484 ResourceGroupToken::current().ptr_eq(&previous),
485 "the previous group should be restored once the poll returns"
486 );
487 }
488
489 #[test]
490 fn in_current_resource_group_captures_group_at_attach_time() {
491 let registry = ResourceGroupRegistry::new();
492 let group = registry.register_resource_group("captured");
493
494 let matched = Rc::new(Cell::new(false));
495 let future = {
496 let _guard = group.enter();
498 RecordCurrentGroup {
499 expected: group,
500 matched: Rc::clone(&matched),
501 }
502 .in_current_resource_group()
503 };
504
505 assert!(!ResourceGroupToken::current().ptr_eq(&group));
507
508 poll_to_completion(future);
510 assert!(matched.get());
511 }
512
513 #[cfg(target_os = "linux")]
514 #[test]
515 fn cpu_time_is_attributed_to_the_entered_group() {
516 let registry = ResourceGroupRegistry::new();
517 let busy = registry.register_resource_group("busy");
518 let _idle = registry.register_resource_group("idle");
519
520 {
521 let _guard = busy.enter();
522 burn_cpu();
523 }
524
525 assert!(
527 cpu_time_nanos_for(®istry, "busy") > 0,
528 "the entered group should accrue the CPU time spent inside the guard"
529 );
530 assert_eq!(
531 cpu_time_nanos_for(®istry, "idle"),
532 0,
533 "a group that was never entered should accrue no CPU time"
534 );
535 }
536
537 #[cfg(target_os = "linux")]
538 #[test]
539 fn tracked_future_attributes_poll_cpu_time_to_its_group() {
540 let registry = ResourceGroupRegistry::new();
541 let group = registry.register_resource_group("worker");
542
543 struct BurnCpu;
544
545 impl Future for BurnCpu {
546 type Output = ();
547
548 fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Self::Output> {
549 burn_cpu();
550 Poll::Ready(())
551 }
552 }
553
554 poll_to_completion(BurnCpu.track_resources(group));
555
556 assert!(
557 cpu_time_nanos_for(®istry, "worker") > 0,
558 "CPU time spent polling a tracked future should be attributed to its group"
559 );
560 }
561}