Skip to main content

memscope_rs/capture/backends/
async_tracker.rs

1//! Async memory tracker implementation.
2//!
3//! This module contains async-specific memory tracking functionality
4//! including task tracking, efficiency scoring, and bottleneck analysis.
5
6use std::collections::HashMap;
7use std::future::Future;
8use std::sync::atomic::{AtomicU64, Ordering};
9use std::sync::{Arc, Mutex};
10use std::thread::ThreadId;
11
12use super::async_types::{
13    AsyncAllocation, AsyncError, AsyncResult, AsyncSnapshot, AsyncStats, TrackedFuture,
14};
15use super::task_profile::{TaskMemoryProfile, TaskType};
16
17/// Generate a new unique task ID.
18/// This ID is never recycled, ensuring unique identification.
19pub fn generate_unique_task_id() -> u64 {
20    crate::task_registry::generate_task_id()
21}
22
23/// Global thread ID counter for unique thread identification.
24static THREAD_COUNTER: AtomicU64 = AtomicU64::new(1);
25
26thread_local! {
27    static THREAD_ID: u64 = THREAD_COUNTER.fetch_add(1, Ordering::Relaxed);
28}
29
30/// Get the current thread's unique ID.
31/// This ID is assigned once per thread and never changes.
32pub fn current_thread_id() -> u64 {
33    THREAD_ID.with(|id| *id)
34}
35
36/// Context for tracking memory allocations.
37/// Captures both thread and task information for accurate attribution.
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
39pub struct TrackerContext {
40    pub thread_id: u64,
41    pub task_id: Option<u64>,
42    pub tokio_task_id: Option<u64>,
43}
44
45impl TrackerContext {
46    /// Capture the current tracking context.
47    /// Returns thread ID and task ID (if in a task context).
48    pub fn capture() -> Self {
49        let _suppression = crate::core::allocator::TrackingGuard::acquire();
50        let task_id_from_context = TASK_CONTEXT.try_with(|ctx| *ctx).ok().flatten();
51        let tokio_task_id = tokio::task::try_id().and_then(|id| id.to_string().parse().ok());
52
53        Self {
54            thread_id: current_thread_id(),
55            task_id: task_id_from_context.or_else(AsyncTracker::get_current_task),
56            tokio_task_id,
57        }
58    }
59}
60
61/// Task efficiency report
62#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
63pub struct TaskReport {
64    pub task_name: String,
65    pub task_type: TaskType,
66    pub efficiency_score: f64,
67    pub cpu_efficiency: f64,
68    pub memory_efficiency: f64,
69    pub io_efficiency: f64,
70    pub bottleneck: String,
71    pub recommendations: Vec<String>,
72}
73
74/// Resource ranking entry
75#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
76pub struct ResourceRanking {
77    pub task_name: String,
78    pub task_type: TaskType,
79    pub cpu_usage: f64,
80    pub memory_usage_mb: f64,
81    pub io_usage_mb: f64,
82    pub network_usage_mb: f64,
83    pub gpu_usage: f64,
84    pub overall_score: f64,
85}
86
87/// Global async tracker instance
88static GLOBAL_TRACKER: Mutex<Option<Arc<AsyncTracker>>> = Mutex::new(None);
89
90mod task_context;
91pub use task_context::TaskGuard;
92
93tokio::task_local! {
94    static TASK_CONTEXT: Option<u64>;
95}
96
97mod dealloc;
98mod future;
99mod queries;
100use future::{ContextualFuture, TaskLifecycleGuard};
101pub use queries::get_memory_snapshot;
102
103/// Numeric id of `thread_id`, when it refers to the calling thread.
104///
105/// `std::thread::ThreadId` cannot be converted to a number, so an id that belongs to
106/// another thread is reported as unknown rather than guessed.
107fn numeric_thread_id(thread_id: ThreadId) -> Option<u64> {
108    if thread_id == std::thread::current().id() {
109        Some(crate::utils::current_thread_id_u64())
110    } else {
111        None
112    }
113}
114
115/// Async memory tracker for task-aware memory tracking.
116pub struct AsyncTracker {
117    /// Live records per address, oldest generation first.
118    allocations: Arc<Mutex<HashMap<usize, Vec<AsyncAllocation>>>>,
119    /// Highest generation ever used per address, so a reused address gets the next
120    /// number instead of restarting at 1 (which would make generations meaningless).
121    generations: Arc<Mutex<HashMap<usize, u64>>>,
122    stats: Arc<Mutex<AsyncStats>>,
123    profiles: Arc<Mutex<HashMap<u64, TaskMemoryProfile>>>,
124    initialized: Arc<Mutex<bool>>,
125    event_store: Option<crate::event_store::SharedEventStore>,
126}
127
128impl AsyncTracker {
129    pub(crate) fn lifecycle_profiles(&self) -> Vec<TaskMemoryProfile> {
130        let _guard = crate::core::allocator::TrackingGuard::acquire();
131        self.profiles
132            .lock()
133            .map(|p| p.values().cloned().collect())
134            .unwrap_or_default()
135    }
136
137    pub fn new() -> Self {
138        Self {
139            allocations: Arc::new(Mutex::new(HashMap::new())),
140            generations: Arc::new(Mutex::new(HashMap::new())),
141            stats: Arc::new(Mutex::new(AsyncStats::default())),
142            profiles: Arc::new(Mutex::new(HashMap::new())),
143            initialized: Arc::new(Mutex::new(false)),
144            event_store: None,
145        }
146    }
147
148    pub fn set_current_task(task_id: u64) {
149        task_context::set(task_id);
150    }
151
152    pub fn clear_current_task() {
153        task_context::clear();
154    }
155
156    /// Get the current task ID from the thread-local storage.
157    ///
158    /// Note: This only returns the manually set task ID.
159    /// For automatic tokio task detection, use `track_in_tokio_task()`.
160    pub fn get_current_task() -> Option<u64> {
161        task_context::current()
162    }
163
164    /// Enter a task context with automatic cleanup.
165    /// Dropping the returned guard restores the enclosing live scope.
166    pub fn enter_task(task_id: u64) -> TaskGuard {
167        task_context::enter(task_id)
168    }
169
170    /// Execute a closure within a task context with automatic cleanup.
171    /// The task ID is automatically cleared after the closure completes,
172    /// even if the closure panics.
173    pub fn with_task<F, T>(task_id: u64, f: F) -> T
174    where
175        F: FnOnce() -> T,
176    {
177        let _guard = Self::enter_task(task_id);
178        f()
179    }
180
181    pub fn track_task_start(
182        &self,
183        task_id: u64,
184        name: String,
185        thread_id: ThreadId,
186    ) -> Result<(), AsyncError> {
187        self.track_task_start_internal(task_id, None, name, thread_id)?;
188        // For the public API, set the current task context. Async paths
189        // (track_in_tokio_task, spawn_tracked) call track_task_start_internal
190        // directly via track_task_start_with_tokio and do NOT want the
191        // context set here — ContextualFuture manages it per-poll.
192        // However, some callers call track_task_start directly (e.g. tests)
193        // and expect the context to be set. To support both, we set the
194        // context here only when NOT already inside a poll guard.
195        //
196        // Since track_in_tokio_task and spawn_tracked both go through
197        // track_task_start_with_tokio or track_task_start, we need a way
198        // to distinguish. The simplest: async paths call
199        // track_task_start_internal directly; external callers use
200        // track_task_start. So set context in track_task_start but NOT
201        // in track_task_start_with_tokio.
202        Self::set_current_task(task_id);
203        Ok(())
204    }
205
206    pub fn track_task_start_with_tokio(
207        &self,
208        task_id: u64,
209        tokio_task_id: u64,
210        name: String,
211        thread_id: ThreadId,
212    ) -> Result<(), AsyncError> {
213        // Do NOT set the current task context here. This method is called
214        // from async paths (track_in_tokio_task, spawn_tracked) where the
215        // ContextualFuture poll wrapper manages context installation.
216        self.track_task_start_internal(task_id, Some(tokio_task_id), name, thread_id)
217    }
218
219    fn track_task_start_internal(
220        &self,
221        task_id: u64,
222        tokio_task_id: Option<u64>,
223        name: String,
224        thread_id: ThreadId,
225    ) -> Result<(), AsyncError> {
226        {
227            let mut profiles = self
228                .profiles
229                .lock()
230                .map_err(|e| AsyncError::mutex_lock_failed("profiles", &e.to_string()))?;
231
232            if profiles.contains_key(&task_id) {
233                return Err(AsyncError::duplicate_task(task_id));
234            }
235
236            let mut profile = match tokio_task_id {
237                Some(id) => {
238                    TaskMemoryProfile::with_tokio_id(task_id, id, name, TaskType::default())
239                }
240                None => TaskMemoryProfile::new(task_id, name, TaskType::default()),
241            };
242            // Record the numeric thread id when it is genuinely known: only the
243            // thread performing this call can be mapped to a numeric id. The snapshot
244            // must never substitute the thread that happens to build it.
245            profile.set_thread_id_u64(numeric_thread_id(thread_id));
246            profiles.insert(task_id, profile);
247        }
248
249        let mut stats = self
250            .stats
251            .lock()
252            .map_err(|e| AsyncError::mutex_lock_failed("stats", &e.to_string()))?;
253        stats.total_tasks += 1;
254        stats.active_tasks += 1;
255
256        // Do NOT call set_current_task here. Context installation is the
257        // responsibility of the poll-level wrapper (ContextualFuture), not
258        // the metadata registration. Leaving an explicit context here would
259        // persist across await points and pollute other tasks' attribution.
260        Ok(())
261    }
262
263    /// Track a task end.
264    pub fn track_task_end(&self, task_id: u64) -> Result<(), AsyncError> {
265        {
266            let mut profiles = self
267                .profiles
268                .lock()
269                .map_err(|e| AsyncError::mutex_lock_failed("profiles", &e.to_string()))?;
270
271            let profile = profiles
272                .get_mut(&task_id)
273                .ok_or_else(|| AsyncError::task_not_found(task_id))?;
274
275            if profile.is_completed() {
276                return Ok(());
277            }
278
279            profile.mark_completed();
280        }
281
282        let mut stats = self
283            .stats
284            .lock()
285            .map_err(|e| AsyncError::mutex_lock_failed("stats", &e.to_string()))?;
286        stats.active_tasks = stats.active_tasks.saturating_sub(1);
287
288        // Remove ended base frames even when a nested guard is currently on top.
289        task_context::end(task_id);
290
291        Ok(())
292    }
293
294    /// Execute an async block within a tokio task context.
295    ///
296    /// This method automatically detects the tokio task ID and sets up tracking.
297    /// When the future completes, the task is automatically marked as ended.
298    ///
299    /// # Arguments
300    ///
301    /// * `name` - Task name for identification
302    /// * `future` - The async block to execute
303    ///
304    /// # Returns
305    ///
306    /// A tuple of (unique_task_id, output).
307    /// The unique_task_id is our internal ID (not the tokio task ID).
308    pub async fn track_in_tokio_task<F, T>(&self, name: String, future: F) -> (u64, T)
309    where
310        F: Future<Output = T>,
311    {
312        let unique_task_id = generate_unique_task_id();
313        let tokio_task_id = tokio::task::try_id().and_then(|id| id.to_string().parse().ok());
314        let thread_id = std::thread::current().id();
315        // Capture parent at call time, consistent with spawn_tracked.
316        let parent_id = crate::task_registry::TaskIdRegistry::current_task_id();
317
318        if let Some(tokio_id) = tokio_task_id {
319            if let Err(e) =
320                self.track_task_start_with_tokio(unique_task_id, tokio_id, name.clone(), thread_id)
321            {
322                tracing::warn!("Failed to track task start: {e}");
323            }
324        } else if let Err(e) =
325            self.track_task_start_internal(unique_task_id, None, name.clone(), thread_id)
326        {
327            tracing::warn!("Failed to track task start: {e}");
328        }
329
330        crate::task_registry::global_registry().register_explicit_task_with_parent(
331            unique_task_id,
332            &name,
333            parent_id,
334        );
335        let lifecycle = future::BorrowedLifecycleGuard::new(self, unique_task_id);
336
337        // Use the same poll-level wrapper as spawn_tracked for consistent
338        // per-poll context management.
339        let contextual = ContextualFuture::new(future, unique_task_id);
340        let output = contextual.await;
341
342        drop(lifecycle);
343
344        (unique_task_id, output)
345    }
346
347    /// Detect zombie tasks.
348    ///
349    /// A zombie task is a task that was started but never completed.
350    /// These tasks may indicate memory leaks or improper task cleanup.
351    ///
352    /// # Returns
353    ///
354    /// A vector of task IDs for zombie tasks.
355    pub fn detect_zombie_tasks(&self) -> Vec<u64> {
356        let _suppression = crate::core::allocator::TrackingGuard::acquire();
357        self.get_all_profiles()
358            .iter()
359            .filter(|p| !p.is_completed())
360            .map(|p| p.task_id)
361            .collect()
362    }
363
364    /// Get statistics about zombie tasks.
365    ///
366    /// Optimized to acquire the lock only once.
367    pub fn zombie_task_stats(&self) -> (usize, usize) {
368        let profiles = self.get_all_profiles();
369        let zombies = profiles.iter().filter(|p| !p.is_completed()).count();
370        let total = profiles.len();
371        (zombies, total)
372    }
373
374    pub fn track_allocation_auto(
375        &self,
376        ptr: usize,
377        size: usize,
378        var_name: Option<String>,
379        type_name: Option<String>,
380    ) {
381        if let Some(task_id) = Self::get_current_task() {
382            self.track_allocation_with_location(ptr, size, task_id, var_name, type_name, None);
383        }
384    }
385
386    /// Track an allocation associated with a task.
387    pub fn track_allocation(&self, ptr: usize, size: usize, task_id: u64) {
388        self.track_allocation_with_location(ptr, size, task_id, None, None, None);
389    }
390
391    /// Track an allocation with source location.
392    ///
393    /// The same address is reported more than once (hook plus `track!`, repeated
394    /// `track!`, reuse). Only the first sighting of an address/size pair opens a
395    /// record; a different size starts a new generation, so a release can always name
396    /// the binding it refers to and `total_memory - total_deallocated == active_memory`
397    /// keeps holding (plan A06 / §7.2).
398    pub fn track_allocation_with_location(
399        &self,
400        ptr: usize,
401        size: usize,
402        task_id: u64,
403        var_name: Option<String>,
404        type_name: Option<String>,
405        source_location: Option<super::async_types::SourceLocation>,
406    ) {
407        let mut previous: Option<(u64, u64)> = None;
408        let mut generation = 1u64;
409
410        {
411            let Ok(mut allocations) = self.allocations.lock() else {
412                tracing::error!("Failed to acquire allocations lock during track_allocation");
413                return;
414            };
415            let entries = allocations.entry(ptr).or_default();
416
417            if let Some(existing) = entries.last_mut() {
418                if existing.size == size {
419                    // Duplicate observation: refresh metadata, keep owner and counters.
420                    existing.var_name = var_name;
421                    existing.type_name = type_name;
422                    if source_location.is_some() {
423                        existing.source_location = source_location;
424                    }
425                    return;
426                }
427                // A different size cannot be proven to be the same block: an
428                // observation never sees the allocator. Close the superseded record and
429                // give the new one its own generation, exactly like the core tracker and
430                // the binding ledger (plan A06 follow-up).
431                previous = Some((existing.task_id, existing.size as u64));
432                entries.pop();
433            }
434
435            // Every record that is pushed gets the next generation for this address,
436            // so a late release can still name the binding it refers to.
437            if let Ok(mut generations) = self.generations.lock() {
438                let counter = generations.entry(ptr).or_insert(0);
439                *counter = counter.saturating_add(1);
440                generation = *counter;
441            }
442
443            entries.push(AsyncAllocation {
444                ptr,
445                size,
446                timestamp: Self::now(),
447                task_id,
448                var_name,
449                type_name,
450                source_location,
451                generation,
452            });
453        }
454
455        let size_u64 = size as u64;
456
457        if let Some((previous_task, previous_size)) = previous {
458            // The superseded record leaves its owner's live set (it is a lost block for
459            // that task), and the new record is a fresh allocation for the observer.
460            // Both sides are booked so `total_memory - total_deallocated ==
461            // active_memory` keeps holding (plan A06 follow-up).
462            if let Ok(mut profiles) = self.profiles.lock() {
463                if let Some(profile) = profiles.get_mut(&previous_task) {
464                    profile.record_deallocation(previous_size);
465                }
466                if let Some(profile) = profiles.get_mut(&task_id) {
467                    profile.record_allocation(size_u64);
468                    profile.set_thread_id_u64(Some(crate::utils::current_thread_id_u64()));
469                }
470            } else {
471                tracing::error!("Failed to acquire profiles lock during track_allocation");
472            }
473
474            if let Ok(mut stats) = self.stats.lock() {
475                stats.total_allocations += 1;
476                stats.total_memory += size;
477                stats.total_deallocated += previous_size;
478                stats.active_memory = stats
479                    .active_memory
480                    .saturating_sub(previous_size as usize)
481                    .saturating_add(size);
482                if stats.active_memory > stats.peak_memory {
483                    stats.peak_memory = stats.active_memory;
484                }
485            } else {
486                tracing::error!("Failed to acquire stats lock during track_allocation");
487            }
488
489            return;
490        }
491
492        if let Ok(mut profiles) = self.profiles.lock() {
493            if let Some(profile) = profiles.get_mut(&task_id) {
494                profile.record_allocation(size_u64);
495                // The thread that allocates is the best available evidence of where
496                // the task actually ran.
497                profile.set_thread_id_u64(Some(crate::utils::current_thread_id_u64()));
498            }
499        } else {
500            tracing::error!("Failed to acquire profiles lock during track_allocation");
501        }
502
503        if let Ok(mut stats) = self.stats.lock() {
504            stats.total_allocations += 1;
505            stats.total_memory += size;
506            stats.active_memory += size;
507            if stats.active_memory > stats.peak_memory {
508                stats.peak_memory = stats.active_memory;
509            }
510        } else {
511            tracing::error!("Failed to acquire stats lock during track_allocation");
512        }
513    }
514
515    pub fn get_stats(&self) -> AsyncStats {
516        let _guard = crate::core::allocator::TrackingGuard::acquire();
517        if let Some((_, rebuilt)) = self.rebuilt() {
518            return queries::stats(&rebuilt);
519        }
520        if let Ok(stats) = self.stats.lock() {
521            stats.clone()
522        } else {
523            tracing::error!("Failed to acquire stats lock in get_stats");
524            AsyncStats::default()
525        }
526    }
527
528    /// Take a snapshot of current state.
529    pub fn snapshot(&self) -> AsyncSnapshot {
530        // Suppress allocator tracking during the query.
531        let _guard = crate::core::allocator::TrackingGuard::acquire();
532        if let Some((captured_at, rebuilt)) = self.rebuilt() {
533            return queries::snapshot(captured_at, rebuilt);
534        }
535        queries::local_snapshot(self)
536    }
537
538    /// Get task memory profile.
539    ///
540    /// Connected trackers query their event source; standalone instances use local records.
541    pub fn get_task_profile(&self, task_id: u64) -> Option<TaskMemoryProfile> {
542        let _guard = crate::core::allocator::TrackingGuard::acquire();
543
544        if let Some((_, rebuilt)) = self.rebuilt() {
545            return rebuilt.profiles.into_iter().find(|p| p.task_id == task_id);
546        }
547        if let Ok(profiles) = self.profiles.lock() {
548            profiles.get(&task_id).cloned().map(|mut profile| {
549                profile.refresh_at(Self::now());
550                profile
551            })
552        } else {
553            tracing::error!("Failed to acquire profiles lock in get_task_profile");
554            None
555        }
556    }
557
558    /// Get all task profiles.
559    ///
560    /// Uses the same event projection as stats and snapshots when connected.
561    pub fn get_all_profiles(&self) -> Vec<TaskMemoryProfile> {
562        let _guard = crate::core::allocator::TrackingGuard::acquire();
563
564        if let Some((_, rebuilt)) = self.rebuilt() {
565            return rebuilt.profiles;
566        }
567        if let Ok(profiles) = self.profiles.lock() {
568            let captured_at = Self::now();
569            profiles
570                .values()
571                .cloned()
572                .map(|mut profile| {
573                    profile.refresh_at(captured_at);
574                    profile
575                })
576                .collect()
577        } else {
578            tracing::error!("Failed to acquire profiles lock in get_all_profiles");
579            Vec::new()
580        }
581    }
582
583    /// Check if tracker is initialized
584    pub fn is_initialized(&self) -> bool {
585        if let Ok(initialized) = self.initialized.lock() {
586            *initialized
587        } else {
588            tracing::error!("Failed to acquire initialized lock in is_initialized");
589            false
590        }
591    }
592
593    /// Mark tracker as initialized
594    pub fn set_initialized(&self) {
595        if let Ok(mut initialized) = self.initialized.lock() {
596            *initialized = true;
597        } else {
598            tracing::error!("Failed to acquire initialized lock in set_initialized");
599        }
600    }
601
602    /// Generate task efficiency report
603    pub fn analyze_task(&self, task_id: u64, task_type: TaskType) -> Option<TaskReport> {
604        let _suppression = crate::core::allocator::TrackingGuard::acquire();
605        let profile = self.get_task_profile(task_id)?;
606
607        let total_bytes = profile.total_bytes as f64;
608        let total_allocations = profile.total_allocations as f64;
609        let peak_memory = profile.peak_memory as f64;
610        let duration_ms = profile.duration_ns as f64 / 1_000_000.0;
611
612        let compute_efficiency = if duration_ms > 0.0 {
613            (total_allocations / duration_ms * 1000.0).min(1.0)
614        } else {
615            0.0
616        };
617
618        let cpu_efficiency = match task_type {
619            TaskType::CpuIntensive | TaskType::IoIntensive | TaskType::GpuCompute => {
620                compute_efficiency
621            }
622            TaskType::MemoryIntensive => {
623                if total_bytes > 0.0 {
624                    (peak_memory / total_bytes).min(1.0)
625                } else {
626                    0.0
627                }
628            }
629            TaskType::NetworkIntensive => {
630                if total_bytes > 0.0 {
631                    (total_allocations / total_bytes * 1000.0).min(1.0)
632                } else {
633                    0.0
634                }
635            }
636            _ => compute_efficiency,
637        };
638
639        let memory_efficiency = if total_bytes > 0.0 {
640            (total_allocations / total_bytes * 1000.0).min(1.0)
641        } else {
642            0.0
643        };
644
645        let io_efficiency = if duration_ms > 0.0 {
646            (total_bytes / duration_ms / 1_048_576.0).min(1.0)
647        } else {
648            0.0
649        };
650
651        let efficiency_score = (cpu_efficiency + memory_efficiency + io_efficiency) / 3.0;
652
653        let bottleneck = if duration_ms > 5000.0 {
654            "Execution Time".to_string()
655        } else if peak_memory > 100.0 * 1024.0 * 1024.0 {
656            "Memory".to_string()
657        } else if total_allocations > 10000.0 {
658            "Allocations".to_string()
659        } else {
660            "None".to_string()
661        };
662
663        let mut recommendations = Vec::new();
664        if duration_ms > 5000.0 {
665            recommendations.push("Consider optimizing task execution time".to_string());
666        }
667        if peak_memory > 100.0 * 1024.0 * 1024.0 {
668            recommendations.push("Reduce peak memory usage".to_string());
669        }
670        if total_allocations > 10000.0 {
671            recommendations.push("Reduce number of allocations".to_string());
672        }
673        if recommendations.is_empty() {
674            recommendations.push("Performance is good".to_string());
675        }
676
677        Some(TaskReport {
678            task_name: profile.task_name.clone(),
679            task_type,
680            efficiency_score,
681            cpu_efficiency,
682            memory_efficiency,
683            io_efficiency,
684            bottleneck,
685            recommendations,
686        })
687    }
688
689    /// Get resource rankings for all tasks
690    pub fn get_resource_rankings(&self) -> Vec<ResourceRanking> {
691        let _suppression = crate::core::allocator::TrackingGuard::acquire();
692        let profiles = self.get_all_profiles();
693
694        let mut rankings: Vec<ResourceRanking> = profiles
695            .into_iter()
696            .map(|profile| {
697                let memory_mb = profile.total_bytes as f64 / 1_048_576.0;
698                let peak_memory_mb = profile.peak_memory as f64 / 1_048_576.0;
699                let duration_ms = profile.duration_ns as f64 / 1_000_000.0;
700                let allocation_rate = profile.allocation_rate;
701
702                let overall_score = memory_mb * 0.3
703                    + peak_memory_mb * 0.2
704                    + allocation_rate * 0.0001
705                    + duration_ms * 0.0001;
706
707                ResourceRanking {
708                    task_name: profile.task_name.clone(),
709                    task_type: profile.task_type,
710                    cpu_usage: allocation_rate,
711                    memory_usage_mb: memory_mb,
712                    io_usage_mb: 0.0,
713                    network_usage_mb: 0.0,
714                    gpu_usage: 0.0,
715                    overall_score,
716                }
717            })
718            .collect();
719
720        rankings.sort_by(|a, b| {
721            b.overall_score
722                .partial_cmp(&a.overall_score)
723                .unwrap_or(std::cmp::Ordering::Equal)
724        });
725
726        rankings
727    }
728
729    /// Get current timestamp.
730    fn now() -> u64 {
731        std::time::SystemTime::now()
732            .duration_since(std::time::UNIX_EPOCH)
733            .unwrap_or_default()
734            .as_nanos() as u64
735    }
736}
737
738impl Default for AsyncTracker {
739    fn default() -> Self {
740        Self::new()
741    }
742}
743
744/// Initialize async memory tracking system
745pub fn initialize() -> AsyncResult<()> {
746    let mut global = GLOBAL_TRACKER.lock().map_err(|_| AsyncError::System {
747        operation: Arc::from("initialize"),
748        message: Arc::from("Failed to acquire global tracker lock"),
749    })?;
750
751    if global.is_none() {
752        let tracker = AsyncTracker::new();
753        tracker.set_initialized();
754        *global = Some(Arc::new(tracker));
755        tracing::info!("Async memory tracking system initialized");
756        Ok(())
757    } else {
758        Err(AsyncError::initialization(
759            "tracker",
760            "Already initialized",
761            true,
762        ))
763    }
764}
765
766/// Shutdown async memory tracking system
767pub fn shutdown() -> AsyncResult<()> {
768    let mut global = GLOBAL_TRACKER.lock().map_err(|_| AsyncError::System {
769        operation: Arc::from("shutdown"),
770        message: Arc::from("Failed to acquire global tracker lock"),
771    })?;
772
773    if global.is_some() {
774        *global = None;
775        tracing::info!("Async memory tracking system shutdown");
776        Ok(())
777    } else {
778        Err(AsyncError::initialization(
779            "tracker",
780            "Not initialized",
781            true,
782        ))
783    }
784}
785
786/// Reset global tracker state (for testing only)
787#[cfg(test)]
788pub fn reset_global_tracker() {
789    if let Ok(mut global) = GLOBAL_TRACKER.lock() {
790        *global = None;
791    } else {
792        tracing::error!("Failed to acquire global tracker lock in reset_global_tracker");
793    }
794}
795
796/// Register an existing AsyncTracker instance as the global singleton.
797/// This allows GlobalTracker to share its async_tracker instance with
798/// the async_tracker module, so `spawn_tracked()` can register task lifecycle
799/// events on the same instance that the renderer reads from.
800pub fn register_global(tracker: Arc<AsyncTracker>) -> AsyncResult<()> {
801    let mut global = GLOBAL_TRACKER.lock().map_err(|_| AsyncError::System {
802        operation: Arc::from("register_global"),
803        message: Arc::from("Failed to acquire global tracker lock"),
804    })?;
805    *global = Some(tracker);
806    Ok(())
807}
808
809/// Get the global async tracker (public for cross-module release notification).
810fn get_global_tracker() -> AsyncResult<Arc<AsyncTracker>> {
811    GLOBAL_TRACKER
812        .lock()
813        .map_err(|_| AsyncError::System {
814            operation: Arc::from("get_global_tracker"),
815            message: Arc::from("Failed to acquire global tracker lock"),
816        })?
817        .clone()
818        .ok_or_else(|| {
819            AsyncError::initialization("tracker", "Tracking system not initialized", true)
820        })
821}
822
823/// Notify the global AsyncTracker that a deallocation occurred.
824///
825/// This function is for explicit API calls (e.g. `track_current_deallocation`).
826/// It must NOT be called from the allocator's dealloc callback — doing so
827/// re-enters AsyncTracker locks and deadlocks with `shutdown()` and
828/// concurrent `track_allocation`. The event-stream replay
829/// (`rebuild_with_live_tracker`) is the authoritative source for exports.
830pub fn notify_deallocation(ptr: usize) {
831    let Ok(tracker) = get_global_tracker() else {
832        return;
833    };
834    tracker.track_deallocation(ptr);
835}
836
837/// Create a tracked future wrapper
838pub fn create_tracked<F>(future: F) -> TrackedFuture<F>
839where
840    F: Future,
841{
842    TrackedFuture::new(future)
843}
844
845/// Spawn a tracked async task with automatic context management.
846///
847/// Wraps a future with an automatically managed task ID.
848///
849/// Captures the parent at spawn time and restores context after every poll.
850///
851/// # Example
852///
853/// ```ignore
854/// let handle = spawn_tracked(async {
855///     let data = vec![1u8; 1024];
856///     tracker.track_as(&data, "buffer", file!(), line!());
857///     // ... async work
858/// });
859/// ```
860pub fn spawn_tracked<F>(future: F) -> tokio::task::JoinHandle<F::Output>
861where
862    F: Future + Send + 'static,
863    F::Output: Send + 'static,
864{
865    let task_id = generate_unique_task_id();
866
867    // Capture the parent task ID at spawn time from the spawning thread's
868    // context. This is critical: if we read it at first poll instead, a
869    // different task might have already set its context on this worker
870    // thread, producing a wrong parent-child edge.
871    let parent_id = crate::task_registry::TaskIdRegistry::current_task_id();
872
873    tokio::spawn(async move {
874        // Register task start with the global async tracker so profiles
875        // are populated for the dashboard renderer.
876        let tracker = get_global_tracker().ok();
877        let task_name = format!("spawned_task_{}", task_id);
878        if let Some(ref tracker) = tracker {
879            let thread_id = std::thread::current().id();
880            // Use track_task_start_with_tokio (which does NOT set TLS context)
881            // when we have a tokio task ID, or track_task_start_internal
882            // directly when we don't. Either way, no TLS context is installed
883            // here — the ContextualFuture poll wrapper handles it per-poll.
884            let tokio_id = tokio::task::try_id().and_then(|id| id.to_string().parse().ok());
885            if let Some(tokio_id) = tokio_id {
886                let _ = tracker.track_task_start_with_tokio(
887                    task_id,
888                    tokio_id,
889                    task_name.clone(),
890                    thread_id,
891                );
892            } else {
893                let _ =
894                    tracker.track_task_start_internal(task_id, None, task_name.clone(), thread_id);
895            }
896        }
897
898        // Register with TaskIdRegistry using the captured parent_id.
899        // We use register_explicit_task_with_parent so the registration
900        // does NOT read or write CURRENT_TASK_ID — the poll-level wrapper
901        // handles context installation per-poll.
902        crate::task_registry::global_registry()
903            .register_explicit_task_with_parent(task_id, &task_name, parent_id);
904
905        // The lifecycle guard ensures task-end cleanup runs in all cases:
906        // normal completion, cancellation (future dropped), and panic.
907        // When the guard drops, it calls unregister_task and track_task_end
908        // inside the task's poll context so the cleanup is attributed correctly.
909        let mut lifecycle = TaskLifecycleGuard::new(task_id, tracker.clone());
910
911        // Wrap the future with poll-level context management.
912        let contextual = ContextualFuture::new(future, task_id);
913        let result = contextual.await;
914
915        // The guard's Drop will run the cleanup. Set the output so the
916        // guard knows the task completed normally (not cancelled).
917        lifecycle.set_completed();
918        result
919    })
920}
921
922/// Check if async memory tracking is currently active
923pub fn is_tracking_active() -> bool {
924    GLOBAL_TRACKER.lock().is_ok_and(|global| global.is_some())
925}
926
927/// Track allocation for current task
928pub fn track_current_allocation(ptr: usize, size: usize) -> AsyncResult<()> {
929    let tracker = get_global_tracker()?;
930    let task_info = super::async_types::get_current_task();
931
932    if task_info.has_tracking_id() {
933        // `TaskId` is a u128: truncating it to 32 bits made long-running task ids
934        // collide. Report the unsupported width instead of losing the mapping.
935        match u64::try_from(task_info.primary_id()) {
936            Ok(task_id) => tracker.track_allocation(ptr, size, task_id),
937            Err(_) => tracing::warn!(
938                target: "memscope::async",
939                task_id = %task_info.primary_id(),
940                ptr = format_args!("{ptr:#x}"),
941                "async task id does not fit into u64; allocation is not attributed"
942            ),
943        }
944    }
945
946    Ok(())
947}
948
949/// Track deallocation for current task
950pub fn track_current_deallocation(ptr: usize) -> AsyncResult<()> {
951    let tracker = get_global_tracker()?;
952    tracker.track_deallocation(ptr);
953    Ok(())
954}
955
956#[cfg(test)]
957mod tests;