1use serde::{Deserialize, Serialize};
7use std::cell::Cell;
8use std::collections::{HashMap, HashSet};
9use std::sync::atomic::{AtomicU64, Ordering};
10use std::sync::Arc;
11use std::sync::RwLock;
12
13thread_local! {
15 static CURRENT_TASK_ID: Cell<Option<u64>> = const { Cell::new(None) };
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
20pub enum TaskStatus {
21 Running,
23 Completed,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct TaskNode {
30 pub id: u64,
32 pub name: String,
34 pub memory_usage: u64,
36 pub allocation_count: usize,
38 pub status: TaskStatus,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct TaskEdge {
45 pub from: u64,
47 pub to: u64,
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct TaskGraph {
54 pub nodes: Vec<TaskNode>,
56 pub edges: Vec<TaskEdge>,
58}
59
60#[derive(Debug, Clone)]
62pub struct TaskMeta {
63 pub id: u64,
65 pub parent: Option<u64>,
67 pub tokio_id: Option<u64>,
69 pub name: String,
71 pub created_at: u64,
73 pub completed_at: Option<u64>,
75 pub status: TaskStatus,
77 pub memory_usage: u64,
79 pub allocation_count: usize,
81}
82
83impl TaskMeta {
84 pub fn new(id: u64, parent: Option<u64>, name: String) -> Self {
86 Self {
87 id,
88 parent,
89 tokio_id: None,
90 name,
91 created_at: Self::now(),
92 completed_at: None,
93 status: TaskStatus::Running,
94 memory_usage: 0,
95 allocation_count: 0,
96 }
97 }
98
99 fn now() -> u64 {
101 use std::time::{SystemTime, UNIX_EPOCH};
102 SystemTime::now()
103 .duration_since(UNIX_EPOCH)
104 .unwrap_or_default()
105 .as_nanos() as u64
106 }
107
108 pub fn mark_completed(&mut self) {
110 self.completed_at.get_or_insert_with(Self::now);
111 self.status = TaskStatus::Completed;
112 }
113
114 pub fn record_allocation(&mut self, size: usize) {
116 self.memory_usage += size as u64;
117 self.allocation_count += 1;
118 }
119}
120
121#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize)]
123pub struct TaskLedgerStats {
124 pub allocation_count: u64,
126 pub allocated_bytes: u64,
128 pub deallocation_count: u64,
130 pub deallocated_bytes: u64,
132 pub live_bytes: u64,
134}
135
136#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
142#[serde(rename_all = "snake_case")]
143pub enum ChargeOutcome {
144 Charged,
146 Unattributed,
148 Deferred,
151}
152
153#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize)]
155pub struct ChargeQuality {
156 pub deferred_charges: u64,
158 pub deferred_bytes: u64,
160 pub unattributed_charges: u64,
162 pub unattributed_bytes: u64,
164 pub unknown_charges: u64,
166 pub unknown_bytes: u64,
168}
169
170impl ChargeQuality {
171 #[must_use]
173 pub fn is_complete(&self) -> bool {
174 self.deferred_charges == 0 && self.unattributed_charges == 0 && self.unknown_charges == 0
175 }
176}
177
178static TASK_COUNTER: AtomicU64 = AtomicU64::new(1);
180
181static GLOBAL_REGISTRY: std::sync::OnceLock<TaskIdRegistry> = std::sync::OnceLock::new();
183
184pub fn global_registry() -> &'static TaskIdRegistry {
186 GLOBAL_REGISTRY.get_or_init(TaskIdRegistry::new)
187}
188
189pub fn generate_task_id() -> u64 {
192 TASK_COUNTER
193 .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |id| id.checked_add(1))
194 .expect("process task identity space exhausted")
195}
196
197pub struct TaskGuard {
201 task_id: u64,
202 parent: Option<u64>,
203 tasks: Arc<RwLock<HashMap<u64, TaskMeta>>>,
204}
205
206impl TaskGuard {
207 fn new(task_id: u64, parent: Option<u64>, tasks: Arc<RwLock<HashMap<u64, TaskMeta>>>) -> Self {
209 Self {
210 task_id,
211 parent,
212 tasks,
213 }
214 }
215}
216
217impl Drop for TaskGuard {
218 fn drop(&mut self) {
219 TaskIdRegistry::complete_task(&self.tasks, self.task_id);
220
221 CURRENT_TASK_ID.with(|cell| {
226 if cell.get() == Some(self.task_id) {
227 cell.set(self.parent);
228 }
229 });
230 }
231}
232
233pub struct TaskIdRegistry {
235 tasks: Arc<RwLock<HashMap<u64, TaskMeta>>>,
237 used_ids: Arc<RwLock<HashSet<u64>>>,
239 skipped_charges: std::sync::atomic::AtomicU64,
242 skipped_bytes: std::sync::atomic::AtomicU64,
245 unattributed_charges: std::sync::atomic::AtomicU64,
248 unattributed_bytes: std::sync::atomic::AtomicU64,
250 deferred_by_task: dashmap::DashMap<u64, (u64, u64)>,
253}
254
255impl TaskIdRegistry {
256 pub fn new() -> Self {
258 Self {
259 tasks: Arc::new(RwLock::new(HashMap::new())),
260 used_ids: Arc::new(RwLock::new(HashSet::new())),
261 skipped_charges: std::sync::atomic::AtomicU64::new(0),
262 skipped_bytes: std::sync::atomic::AtomicU64::new(0),
263 unattributed_charges: std::sync::atomic::AtomicU64::new(0),
264 unattributed_bytes: std::sync::atomic::AtomicU64::new(0),
265 deferred_by_task: dashmap::DashMap::new(),
266 }
267 }
268
269 pub fn task_scope(&self, name: &str) -> TaskGuard {
301 let parent = Self::current_task_id();
302 let task_id = self.spawn_task(parent, name.to_string());
303 TaskGuard::new(task_id, parent, Arc::clone(&self.tasks))
304 }
305
306 pub fn register_explicit_task(&self, task_id: u64, name: &str) {
319 TASK_COUNTER.fetch_max(task_id.saturating_add(1), Ordering::Relaxed);
320 let parent = Self::current_task_id();
321 let mut meta = TaskMeta::new(task_id, parent, name.to_string());
322
323 if let Some(tokio_id) = self.get_tokio_task_id() {
324 meta.tokio_id = Some(tokio_id);
325 }
326
327 if let Ok(mut tasks) = self.tasks.write() {
328 tasks.insert(task_id, meta);
329 }
330 if let Ok(mut used_ids) = self.used_ids.write() {
331 used_ids.insert(task_id);
332 }
333 CURRENT_TASK_ID.set(Some(task_id));
334 }
335
336 pub fn register_explicit_task_with_parent(
349 &self,
350 task_id: u64,
351 name: &str,
352 parent_id: Option<u64>,
353 ) {
354 TASK_COUNTER.fetch_max(task_id.saturating_add(1), Ordering::Relaxed);
355 let mut meta = TaskMeta::new(task_id, parent_id, name.to_string());
356
357 if let Some(tokio_id) = self.get_tokio_task_id() {
358 meta.tokio_id = Some(tokio_id);
359 }
360
361 if let Ok(mut tasks) = self.tasks.write() {
362 tasks.insert(task_id, meta);
363 }
364 if let Ok(mut used_ids) = self.used_ids.write() {
365 used_ids.insert(task_id);
366 }
367 }
370
371 pub fn unregister_task(&self, task_id: u64) {
376 if let Ok(mut tasks) = self.tasks.write() {
377 if let Some(meta) = tasks.get_mut(&task_id) {
378 meta.mark_completed();
379 }
380 }
381 CURRENT_TASK_ID.with(|cell| {
382 if cell.get() == Some(task_id) {
383 cell.set(None);
384 }
385 });
386 }
387
388 fn spawn_task(&self, parent: Option<u64>, name: String) -> u64 {
399 let task_id = generate_task_id();
400
401 let mut meta = TaskMeta::new(task_id, parent, name);
402
403 if let Some(tokio_id) = self.get_tokio_task_id() {
405 meta.tokio_id = Some(tokio_id);
406 }
407
408 if let Ok(mut tasks) = self.tasks.write() {
410 tasks.insert(task_id, meta);
411 }
412
413 if let Ok(mut used_ids) = self.used_ids.write() {
415 used_ids.insert(task_id);
416 }
417
418 CURRENT_TASK_ID.set(Some(task_id));
420
421 task_id
422 }
423
424 fn complete_task(tasks: &RwLock<HashMap<u64, TaskMeta>>, task_id: u64) {
430 if let Ok(mut tasks) = tasks.write() {
431 if let Some(meta) = tasks.get_mut(&task_id) {
432 meta.mark_completed();
433 }
434 }
435
436 }
440
441 pub fn record_allocation(&self, size: usize) {
447 if let Some(task_id) = Self::current_task_id() {
448 if let Ok(mut tasks) = self.tasks.write() {
449 if let Some(meta) = tasks.get_mut(&task_id) {
450 meta.record_allocation(size);
451 }
452 }
453 }
454 }
455
456 pub fn try_record_allocation(&self, size: usize) -> ChargeOutcome {
464 let Some(task_id) = Self::current_task_id() else {
465 self.unattributed_charges.fetch_add(1, Ordering::Relaxed);
466 self.unattributed_bytes
467 .fetch_add(size as u64, Ordering::Relaxed);
468 return ChargeOutcome::Unattributed;
469 };
470
471 match self.tasks.try_write() {
472 Ok(mut tasks) => match tasks.get_mut(&task_id) {
473 Some(meta) => {
474 meta.record_allocation(size);
475 ChargeOutcome::Charged
476 }
477 None => ChargeOutcome::Unattributed,
479 },
480 Err(_) => {
481 self.skipped_charges.fetch_add(1, Ordering::Relaxed);
482 self.skipped_bytes.fetch_add(size as u64, Ordering::Relaxed);
483 let mut entry = self.deferred_by_task.entry(task_id).or_insert((0, 0));
487 entry.0 += 1;
488 entry.1 += size as u64;
489 ChargeOutcome::Deferred
490 }
491 }
492 }
493
494 #[must_use]
498 pub fn deferred_tasks(&self) -> std::collections::HashMap<u64, (u64, u64)> {
499 let _suppression = crate::core::allocator::TrackingGuard::acquire();
503 self.deferred_by_task
504 .iter()
505 .map(|entry| (*entry.key(), *entry.value()))
506 .collect()
507 }
508
509 pub fn charge_quality(&self) -> ChargeQuality {
511 ChargeQuality {
512 deferred_charges: self.skipped_charges.load(Ordering::Relaxed),
513 deferred_bytes: self.skipped_bytes.load(Ordering::Relaxed),
514 unattributed_charges: self.unattributed_charges.load(Ordering::Relaxed),
515 unattributed_bytes: self.unattributed_bytes.load(Ordering::Relaxed),
516 ..ChargeQuality::default()
517 }
518 }
519
520 pub fn current_task_id() -> Option<u64> {
521 CURRENT_TASK_ID.get()
522 }
523
524 pub fn set_current_task_id(&self, task_id: u64) {
528 CURRENT_TASK_ID.set(Some(task_id));
529 }
530
531 pub fn set_current_task_id_opt(&self, task_id: Option<u64>) {
535 CURRENT_TASK_ID.set(task_id);
536 }
537
538 pub fn clear(&self) {
540 if let Ok(mut tasks) = self.tasks.write() {
541 tasks.clear();
542 }
543 if let Ok(mut used_ids) = self.used_ids.write() {
544 used_ids.clear();
545 }
546 CURRENT_TASK_ID.set(None);
547 }
548
549 pub fn get_task(&self, task_id: u64) -> Option<TaskMeta> {
559 if let Ok(tasks) = self.tasks.read() {
560 tasks.get(&task_id).cloned()
561 } else {
562 None
563 }
564 }
565
566 pub fn get_all_tasks(&self) -> Vec<TaskMeta> {
568 if let Ok(tasks) = self.tasks.read() {
569 tasks.values().cloned().collect()
570 } else {
571 Vec::new()
572 }
573 }
574
575 pub fn get_children(&self, parent_id: u64) -> Vec<u64> {
585 if let Ok(tasks) = self.tasks.read() {
586 tasks
587 .values()
588 .filter(|meta| meta.parent == Some(parent_id))
589 .map(|meta| meta.id)
590 .collect()
591 } else {
592 Vec::new()
593 }
594 }
595
596 pub fn get_parent(&self, task_id: u64) -> Option<u64> {
606 if let Ok(tasks) = self.tasks.read() {
607 tasks.get(&task_id).and_then(|meta| meta.parent)
608 } else {
609 None
610 }
611 }
612
613 fn get_tokio_task_id(&self) -> Option<u64> {
615 None
618 }
619
620 pub fn export_graph(&self) -> TaskGraph {
626 let mut nodes = Vec::new();
627 let mut edges = Vec::new();
628
629 if let Ok(tasks) = self.tasks.read() {
630 for meta in tasks.values() {
632 nodes.push(TaskNode {
633 id: meta.id,
634 name: meta.name.clone(),
635 memory_usage: meta.memory_usage,
636 allocation_count: meta.allocation_count,
637 status: meta.status,
638 });
639 }
640
641 for meta in tasks.values() {
643 if let Some(parent_id) = meta.parent {
644 edges.push(TaskEdge {
645 from: parent_id,
646 to: meta.id,
647 });
648 }
649 }
650 }
651
652 TaskGraph { nodes, edges }
653 }
654
655 pub fn get_stats(&self) -> TaskRegistryStats {
657 if let Ok(tasks) = self.tasks.read() {
658 let total = tasks.len();
659 let running = tasks
660 .values()
661 .filter(|m| m.status == TaskStatus::Running)
662 .count();
663 let completed = tasks
664 .values()
665 .filter(|m| m.status == TaskStatus::Completed)
666 .count();
667
668 TaskRegistryStats {
669 total_tasks: total,
670 running_tasks: running,
671 completed_tasks: completed,
672 }
673 } else {
674 TaskRegistryStats::default()
675 }
676 }
677}
678
679impl Default for TaskIdRegistry {
680 fn default() -> Self {
681 Self::new()
682 }
683}
684
685#[derive(Debug, Clone, Default)]
687pub struct TaskRegistryStats {
688 pub total_tasks: usize,
689 pub running_tasks: usize,
690 pub completed_tasks: usize,
691}
692
693#[cfg(test)]
694mod tests {
695 use super::*;
696 use serial_test::serial;
699
700 #[test]
701 fn test_task_id_generation() {
702 let id1 = generate_task_id();
703 let id2 = generate_task_id();
704 assert!(id2 > id1);
705 }
706
707 #[test]
708 #[serial]
709 fn test_spawn_task() {
710 let registry = global_registry();
711 registry.clear();
712
713 let task_id = registry.spawn_task(None, "test_task".to_string());
714
715 let meta = registry.get_task(task_id);
716 assert!(meta.is_some());
717 assert_eq!(meta.unwrap().name, "test_task");
718 }
719
720 #[test]
721 #[serial]
722 fn test_parent_child() {
723 let registry = global_registry();
724 registry.clear();
725
726 {
728 let _parent = registry.task_scope("parent");
729 let parent_id = TaskIdRegistry::current_task_id().unwrap();
730
731 {
732 let _child = registry.task_scope("child");
733 let child_id = TaskIdRegistry::current_task_id().unwrap();
734
735 assert_eq!(registry.get_parent(child_id), Some(parent_id));
736 assert_eq!(registry.get_children(parent_id), vec![child_id]);
737 }
738 }
739 }
740
741 #[test]
742 #[serial]
743 fn test_current_task() {
744 let registry = global_registry();
745 registry.clear();
746
747 assert_eq!(TaskIdRegistry::current_task_id(), None);
748
749 {
750 let _task = registry.task_scope("test");
751 let task_id = TaskIdRegistry::current_task_id();
752 assert!(task_id.is_some());
753 }
754
755 assert_eq!(TaskIdRegistry::current_task_id(), None);
756 }
757
758 #[test]
759 #[serial]
760 fn test_complete_task() {
761 let registry = global_registry();
762 registry.clear();
763
764 let task_id;
765
766 {
767 let _task = registry.task_scope("test");
768 task_id = TaskIdRegistry::current_task_id().unwrap();
769
770 let meta = registry.get_task(task_id).unwrap();
771 assert_eq!(meta.status, TaskStatus::Running);
772 }
773
774 let meta = registry.get_task(task_id).unwrap();
776 assert_eq!(meta.status, TaskStatus::Completed);
777 }
778
779 #[test]
780 #[serial]
781 fn test_stats() {
782 let registry = global_registry();
783 registry.clear();
784
785 {
786 let _t1 = registry.task_scope("task1");
787 let _t2 = registry.task_scope("task2");
788
789 let stats = registry.get_stats();
790 assert_eq!(stats.total_tasks, 2);
791 assert_eq!(stats.running_tasks, 2);
792 }
793
794 let stats = registry.get_stats();
795 assert_eq!(stats.completed_tasks, 2);
796 assert_eq!(stats.running_tasks, 0);
797 }
798
799 #[test]
800 #[serial]
801 fn test_export_graph() {
802 let registry = global_registry();
803 registry.clear();
804
805 {
806 let _parent = registry.task_scope("parent");
807 {
808 let _child = registry.task_scope("child");
809 }
810 }
811
812 let graph = registry.export_graph();
813
814 assert_eq!(graph.nodes.len(), 2);
815 assert_eq!(graph.edges.len(), 1);
816 }
817
818 #[test]
823 #[serial]
824 fn test_nested_scope_restores_parent_context() {
825 let registry = global_registry();
826 registry.clear();
828
829 {
830 let _parent = registry.task_scope("parent");
831 let parent_id = TaskIdRegistry::current_task_id()
832 .expect("the parent scope must install a current task");
833
834 {
835 let _child = registry.task_scope("child");
836 assert_ne!(
837 TaskIdRegistry::current_task_id(),
838 Some(parent_id),
839 "the child scope must take over the current context"
840 );
841 }
842
843 assert_eq!(
844 TaskIdRegistry::current_task_id(),
845 Some(parent_id),
846 "ending the child scope must restore the parent task"
847 );
848 }
849
850 assert_eq!(
851 TaskIdRegistry::current_task_id(),
852 None,
853 "ending the outermost scope must restore the empty context"
854 );
855 }
856}
857
858#[cfg(test)]
859mod ledger_tests;
860
861mod ledger;
862pub use ledger::{TaskAccountingSnapshot, TaskLifecycleQuality};
863
864mod charge_quality;
865
866#[cfg(all(test, feature = "tracking-allocator"))]
867mod allocator_tests;