Skip to main content

starnix_core/task/
tracing.rs

1// Copyright 2025 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use crate::task::{Kernel, PidTable};
6use fuchsia_rcu::RcuReadScope;
7use starnix_logging::{log_debug, log_error, log_warn};
8use starnix_sync::{
9    LockDepMutex, LockDepRwLock, PidToKoidMapInnerLock, TracePerformanceEventManagerStateLock,
10};
11use starnix_uapi::{pid_t, tid_t};
12use std::collections::{HashMap, HashSet};
13use std::sync::atomic::{AtomicUsize, Ordering};
14use std::sync::{Arc, Weak};
15use zx::Koid;
16
17/// Sink for emitting task attribution events during tracing.
18pub trait TraceEventSink: Send + Sync {
19    /// Emits an association event between Linux pid/tid and Zircon identity.
20    /// `source` is "dump" for existing tasks snapshot, or "new_task" for dynamic spawns.
21    fn emit_association(
22        &self,
23        pid: pid_t,
24        tid: tid_t,
25        identity: ZirconIdentity,
26        source: &'static str,
27    );
28
29    /// Called after an initial dump of running tasks completes.
30    fn on_dump_complete(&self);
31}
32
33/// Production implementation of [`TraceEventSink`] that writes to Fuchsia tracing.
34#[derive(Default)]
35pub struct ProductionTraceEventSink;
36
37impl TraceEventSink for ProductionTraceEventSink {
38    fn emit_association(
39        &self,
40        pid: pid_t,
41        tid: tid_t,
42        identity: ZirconIdentity,
43        source: &'static str,
44    ) {
45        fuchsia_trace::instant!(
46            "starnix:meta",
47            source,
48            fuchsia_trace::Scope::Process,
49            "tid" => tid,
50            "pid" => pid,
51            "koid" => identity.thread.raw_koid().to_string().as_str(),
52            "process_koid" => identity.process.raw_koid().to_string().as_str()
53        );
54    }
55
56    fn on_dump_complete(&self) {
57        if fuchsia_trace::category_enabled(c"starnix:meta") {
58            if let Err(status) = fuchsia_trace::flush_buffer() {
59                log_warn!("fuchsia_trace::flush_buffer failed with status: {status}");
60            }
61        }
62    }
63}
64
65/// The Zircon koids backing one `Task`.
66///
67/// Throughout this module, "task" means `starnix_core::task::Task`, the Linux sense of
68/// the word (one thread of a thread group), never Zircon's task abstraction
69/// (job/process/thread).
70///
71/// Both koids are always present: a task is only recorded once its Zircon thread handle
72/// is attached and its thread group has a valid backing process. Tasks for which either
73/// half is unavailable (a task observed mid-creation by the initial seed, or a Starnix
74/// kernel thread with no backing Zircon process) are not stored; mid-creation tasks
75/// record themselves once their thread handle is attached.
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub struct ZirconIdentity {
78    /// The Zircon process koid.
79    pub process: Koid,
80    /// The Zircon thread koid.
81    pub thread: Koid,
82}
83
84/// The Linux identity behind one Zircon koid.
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86pub enum LinuxIdentity {
87    /// Identifies a specific Linux thread within a process.
88    Thread { pid: pid_t, tid: tid_t },
89    /// Identifies a Linux process.
90    Process { pid: pid_t },
91}
92
93/// Bidirectional map between Linux pids/tids and Zircon koids.
94///
95/// Entries are only added while recording is active; nothing is removed when a thread or
96/// process exits so that trace data can be resolved past a `Task`'s lifetime. The whole
97/// map is released when the last recording session ends. Additional work may be needed to
98/// handle pid reuse (https://fxbug.dev/322874557); currently new mapping information
99/// overwrites existing thread mappings.
100#[derive(Debug, Default)]
101struct PidKoidMap {
102    /// Forward table: Linux tid to the koids backing that task.
103    tid_to_koid: HashMap<tid_t, ZirconIdentity>,
104    /// Forward table: Linux pid to the backing Zircon process koid.
105    pid_to_koid: HashMap<pid_t, Koid>,
106    /// Reverse table: Zircon koid (thread or process) to its Linux identity.
107    koid_to_pid: HashMap<Koid, LinuxIdentity>,
108    /// Process-level negative cache: process koids known not to belong to this container.
109    ///
110    /// System-wide profiling produces many samples from native Fuchsia processes whose
111    /// koids can never resolve; by Zircon's task hierarchy (a process that is not a
112    /// Starnix process has no Starnix threads), one cached miss per process replaces
113    /// two failed lookups per sample. Entries are evicted if a task later records for
114    /// that process koid, and the whole set is released with the map.
115    unmapped_processes: HashSet<Koid>,
116}
117
118impl PidKoidMap {
119    fn insert(&mut self, pid: pid_t, tid: tid_t, identity: ZirconIdentity) {
120        self.tid_to_koid.insert(tid, identity);
121        self.pid_to_koid.entry(pid).or_insert(identity.process);
122        self.koid_to_pid.insert(identity.thread, LinuxIdentity::Thread { pid, tid });
123        // Threads of the same process all report the same process koid; keep the first
124        // entry so the process continues to resolve to the same pid.
125        self.koid_to_pid.entry(identity.process).or_insert(LinuxIdentity::Process { pid });
126        // Evict a stale negative entry in case a sample was resolved before this task
127        // recorded itself.
128        self.unmapped_processes.remove(&identity.process);
129    }
130
131    fn get_zircon_identity(&self, tid: tid_t) -> Option<&ZirconIdentity> {
132        self.tid_to_koid.get(&tid)
133    }
134
135    fn get_process_koid(&self, pid: pid_t) -> Option<Koid> {
136        self.pid_to_koid.get(&pid).copied()
137    }
138
139    fn get_linux_identity(&self, koid: Koid) -> Option<LinuxIdentity> {
140        self.koid_to_pid.get(&koid).copied()
141    }
142
143    /// Merges mappings from another map without overwriting existing entries, so a
144    /// seeding snapshot cannot clobber a fresher entry recorded by a concurrently
145    /// spawning task.
146    fn extend_from(&mut self, other: PidKoidMap) {
147        for (tid, identity) in other.tid_to_koid {
148            self.unmapped_processes.remove(&identity.process);
149            self.tid_to_koid.entry(tid).or_insert(identity);
150        }
151        for (pid, koid) in other.pid_to_koid {
152            self.pid_to_koid.entry(pid).or_insert(koid);
153        }
154        for (koid, identity) in other.koid_to_pid {
155            self.koid_to_pid.entry(koid).or_insert(identity);
156        }
157        self.unmapped_processes.extend(other.unmapped_processes);
158    }
159}
160
161/// Kernel-wide manager of pid/tid to koid mappings, shared by all recording clients
162/// (system tracing and CPU profiling). Stored in `kernel.trace_event_manager`.
163///
164/// The manager lives for the lifetime of the `Kernel`; whether recording is active is
165/// tracked by the session count inside it, not by its presence. Clients start recording
166/// by obtaining a [`PidKoidSession`] from [`TracePerformanceEventManager::open`] and stop
167/// by dropping it, so overlapping sessions from independent clients compose correctly:
168/// the map is seeded when the first session opens and released when the last one drops,
169/// and cleanup runs on every exit path because it is driven by `Drop`.
170///
171/// When no session is active, task creation fast-paths out on a single relaxed atomic
172/// load with no locks and no allocations.
173pub struct TracePerformanceEventManager {
174    /// Weak reference to the enclosing `Kernel`, upgraded only on the 0 -> 1 session
175    /// transition to seed the map from `kernel.pids`. Weak because the manager is a field
176    /// of the `Kernel` itself (a strong reference would be a cycle).
177    weak_kernel: Weak<Kernel>,
178
179    /// Number of live [`PidKoidSession`]s. Maintained exclusively under [`state_lock`] by
180    /// [`TracePerformanceEventManager::open`] and [`PidKoidSession::drop`].
181    active_sessions: AtomicUsize,
182
183    /// Serializes session lifecycle transitions (0 -> 1 initialization and 1 -> 0 cleanup).
184    state_lock: LockDepMutex<(), TracePerformanceEventManagerStateLock>,
185
186    /// The bidirectional mapping table. Readers take short read locks per lookup; the
187    /// only writers are task spawns while recording (one insert each) and the session
188    /// seed/release transitions.
189    map: LockDepRwLock<PidKoidMap, PidToKoidMapInnerLock>,
190
191    /// Sink for emitting task attribution events during tracing.
192    sink: Arc<dyn TraceEventSink>,
193}
194
195impl TracePerformanceEventManager {
196    /// Creates a new manager holding a weak reference to the enclosing `Kernel`.
197    pub fn new(weak_kernel: Weak<Kernel>) -> Self {
198        Self::new_with_sink(weak_kernel, Arc::new(ProductionTraceEventSink))
199    }
200
201    /// Creates a new manager with a custom event sink.
202    pub fn new_with_sink(weak_kernel: Weak<Kernel>, sink: Arc<dyn TraceEventSink>) -> Self {
203        Self {
204            weak_kernel,
205            active_sessions: AtomicUsize::new(0),
206            state_lock: LockDepMutex::new(()),
207            map: LockDepRwLock::new(PidKoidMap::default()),
208            sink,
209        }
210    }
211
212    /// Initializes a background observer that listens for Fuchsia trace state changes
213    /// and opens/closes a [`PidKoidSession`] accordingly.
214    pub fn init_trace_observer(&self, kernel: &Arc<Kernel>) {
215        let weak_kernel = Arc::downgrade(kernel);
216        kernel.kthreads.spawn_future(
217            move || async move {
218                let observer = fuchsia_trace_observer::TraceObserver::new();
219                let mut session: Option<PidKoidSession> = None;
220                if fuchsia_trace::is_enabled() {
221                    if let Some(kernel) = weak_kernel.upgrade() {
222                        session = Some(kernel.trace_event_manager.open());
223                        log_debug!(
224                            "trace-meta-observer: session active on startup: {}",
225                            session.is_some()
226                        );
227                    }
228                }
229                while let Ok(state) = observer.on_state_changed().await {
230                    match state {
231                        fuchsia_trace::TraceState::Started => {
232                            if let Some(kernel) = weak_kernel.upgrade() {
233                                session = Some(kernel.trace_event_manager.open());
234                                log_debug!(
235                                    "trace-meta-observer: session active on trace start: {}",
236                                    session.is_some()
237                                );
238                            }
239                        }
240                        fuchsia_trace::TraceState::Stopping
241                        | fuchsia_trace::TraceState::Stopped => {
242                            if session.take().is_some() {
243                                log_debug!("trace-meta-observer: session dropped on trace stop");
244                            }
245                        }
246                    }
247                }
248                log_warn!("trace-meta-observer loop terminated unexpectedly");
249            },
250            "trace-meta-observer",
251        );
252    }
253
254    /// Returns true if at least one session is actively recording mappings.
255    pub fn is_recording(&self) -> bool {
256        self.active_sessions.load(Ordering::Acquire) > 0
257    }
258
259    /// Opens a session. When the first session opens, the map is seeded with
260    /// all currently running `Task`s; tasks created afterwards record themselves via
261    /// `Task::record_pid_koid_mapping`. Recording continues until all active sessions
262    /// have been dropped.
263    pub fn open(self: &Arc<Self>) -> PidKoidSession {
264        self.start_session_internal();
265        PidKoidSession { manager: self.clone() }
266    }
267
268    /// Records the mapping for one `Task` if recording is active. Called from `Task`
269    /// creation, after the task's Zircon thread handle is attached, so the identity is
270    /// always complete.
271    pub fn record(&self, pid: pid_t, tid: tid_t, identity: ZirconIdentity) {
272        if self.active_sessions.load(Ordering::Acquire) == 0 {
273            return;
274        }
275        let mut map = self.map.write();
276        if self.active_sessions.load(Ordering::Acquire) == 0 {
277            return;
278        }
279        map.insert(pid, tid, identity);
280        self.sink.emit_association(pid, tid, identity, "new_task");
281    }
282
283    /// Looks up the Zircon koids recorded for a Linux tid.
284    pub(crate) fn get_zircon_identity(&self, tid: tid_t) -> Option<ZirconIdentity> {
285        self.map.read().get_zircon_identity(tid).copied()
286    }
287
288    /// Looks up the Zircon process koid recorded for a Linux pid.
289    pub(crate) fn get_process_koid(&self, pid: pid_t) -> Option<Koid> {
290        self.map.read().get_process_koid(pid)
291    }
292
293    /// Reverse resolution: maps sampled (process koid, thread koid) to Linux identity,
294    /// with negative caching for native Fuchsia processes.
295    fn resolve_koids(&self, pkoid: Koid, tkoid: Koid) -> Option<LinuxIdentity> {
296        {
297            let map = self.map.read();
298            if map.unmapped_processes.contains(&pkoid) {
299                return None;
300            }
301            if let Some(identity) = map.get_linux_identity(tkoid) {
302                return Some(identity);
303            }
304            // If `pkoid` is already a known Starnix process, the write-lock slow path
305            // below (`if !map.koid_to_pid.contains_key(&pkoid)`) will not insert it into
306            // `unmapped_processes`. Return `None` here under the read lock so an unmapped
307            // thread in a known process does not fall through and acquire the exclusive
308            // write lock on every subsequent sample for a guaranteed no-op.
309            if map.koid_to_pid.contains_key(&pkoid) {
310                return None;
311            }
312        }
313
314        // Unknown process (`pkoid` not in `koid_to_pid` or `unmapped_processes`): record
315        // it in the negative cache under the write lock, re-checking first in case a
316        // concurrent `record()` populated it meanwhile.
317        let mut map = self.map.write();
318        if let Some(identity) = map.get_linux_identity(tkoid) {
319            return Some(identity);
320        }
321        if !map.koid_to_pid.contains_key(&pkoid) {
322            map.unmapped_processes.insert(pkoid);
323        }
324        None
325    }
326
327    /// Increments the session count, seeding the map from the kernel pid table when this
328    /// is the first session.
329    fn start_session_internal(&self) {
330        let _guard = self.state_lock.lock();
331        let current = self.active_sessions.fetch_add(1, Ordering::AcqRel);
332        if current == 0 {
333            if let Some(kernel) = self.weak_kernel.upgrade() {
334                let snapshot = Self::snapshot_existing_tasks(&kernel.pids, &*self.sink);
335                self.map.write().extend_from(snapshot);
336                self.sink.on_dump_complete();
337            } else {
338                log_warn!("Kernel is shutting down, unable to snapshot running tasks");
339            }
340        }
341    }
342
343    /// Decrements the session count, releasing the map when the last session drops.
344    fn stop_session_internal(&self) {
345        let _guard = self.state_lock.lock();
346        if self.active_sessions.load(Ordering::Acquire) == 0 {
347            log_error!("session stopped without an active session");
348            return;
349        }
350        let current = self.active_sessions.fetch_sub(1, Ordering::AcqRel);
351        if current == 1 {
352            *self.map.write() = PidKoidMap::default();
353        }
354    }
355
356    /// Builds a map of all currently running `Task`s from the kernel pid table.
357    ///
358    /// A task is captured only if its identity is complete. A task observed mid-creation,
359    /// before its Zircon thread handle is attached, is skipped: it records itself moments
360    /// later (the executor attaches the thread handle and then calls
361    /// `Task::record_pid_koid_mapping` while this session is already active), and during
362    /// the gap its tid cannot appear in any trace data because its creator's syscall has
363    /// not yet returned. Starnix kernel threads, which have no backing Zircon process,
364    /// are intentionally excluded.
365    fn snapshot_existing_tasks(pid_table: &PidTable, sink: &dyn TraceEventSink) -> PidKoidMap {
366        let mut pid_map = PidKoidMap::default();
367
368        let scope = RcuReadScope::new();
369        let mut count = 0;
370        for pid in pid_table.running_task_ids(&scope) {
371            count += 1;
372            // Running `Task`s may exit at any time. Record one only if a snapshot of its
373            // running state can be obtained.
374            let Ok(task) = pid.get_task() else {
375                continue;
376            };
377            if let Some(identity) = task.get_zircon_identity() {
378                let pid_val = task.get_pid();
379                pid_map.insert(pid_val, pid.id, identity);
380                sink.emit_association(pid_val, pid.id, identity, "dump");
381            }
382        }
383
384        log_debug!("Initialized {} pid mappings. From {} ids", pid_map.tid_to_koid.len(), count);
385        pid_map
386    }
387}
388
389/// A reader's session and query interface for pid/koid mappings. Recording in the
390/// kernel stays active while at least one session is held across any client; dropping
391/// the session ends this client's interest. When all active sessions have been dropped,
392/// the shared map is released and recording ceases.
393///
394/// Resolution queries are only reachable through a session, which enforces at compile
395/// time that lookups happen while recording is active.
396#[must_use = "Recording stops when this guard is dropped"]
397pub struct PidKoidSession {
398    manager: Arc<TracePerformanceEventManager>,
399}
400
401impl PidKoidSession {
402    /// Forward resolution: maps a Linux tid to its thread koid, returning None on a miss.
403    pub fn resolve_tid_to_koid(&self, tid: tid_t) -> Option<Koid> {
404        if tid == 0 {
405            return Some(Koid::from_raw(0));
406        }
407        self.manager.get_zircon_identity(tid).map(|id| id.thread)
408    }
409
410    /// Forward resolution: maps a Linux pid to its process koid, returning None on a miss.
411    pub fn resolve_pid_to_koid(&self, pid: pid_t) -> Option<Koid> {
412        if pid == 0 {
413            return Some(Koid::from_raw(0));
414        }
415        self.manager.get_process_koid(pid)
416    }
417
418    /// Reverse resolution: maps sampled (process koid, thread koid) to Linux identity,
419    /// with negative caching for native Fuchsia processes. See
420    /// [`TracePerformanceEventManager::resolve_koids`].
421    pub fn resolve_koids(&self, pkoid: Koid, tkoid: Koid) -> Option<LinuxIdentity> {
422        self.manager.resolve_koids(pkoid, tkoid)
423    }
424}
425
426impl Drop for PidKoidSession {
427    fn drop(&mut self) {
428        self.manager.stop_session_internal();
429    }
430}
431
432#[cfg(test)]
433mod tests {
434    use super::*;
435    use crate::task::ZirconThread;
436    use crate::testing::{create_task, spawn_kernel_and_run};
437    use futures::channel::oneshot;
438    use std::sync::Mutex;
439
440    impl PidKoidMap {
441        fn is_empty(&self) -> bool {
442            self.tid_to_koid.is_empty() && self.pid_to_koid.is_empty()
443        }
444    }
445
446    struct TestTraceEventSink {
447        events: Arc<Mutex<Vec<(pid_t, tid_t, ZirconIdentity, String)>>>,
448        dump_completed: Arc<Mutex<bool>>,
449    }
450
451    impl TestTraceEventSink {
452        fn new() -> (Self, Arc<Mutex<Vec<(pid_t, tid_t, ZirconIdentity, String)>>>, Arc<Mutex<bool>>)
453        {
454            let events = Arc::new(Mutex::new(Vec::new()));
455            let dump_completed = Arc::new(Mutex::new(false));
456            (
457                Self { events: Arc::clone(&events), dump_completed: Arc::clone(&dump_completed) },
458                events,
459                dump_completed,
460            )
461        }
462    }
463
464    impl TraceEventSink for TestTraceEventSink {
465        fn emit_association(
466            &self,
467            pid: pid_t,
468            tid: tid_t,
469            identity: ZirconIdentity,
470            source: &'static str,
471        ) {
472            self.events.lock().unwrap().push((pid, tid, identity, source.to_string()));
473        }
474
475        fn on_dump_complete(&self) {
476            *self.dump_completed.lock().unwrap() = true;
477        }
478    }
479
480    impl TracePerformanceEventManager {
481        fn new_for_testing() -> Self {
482            Self::new(Weak::new())
483        }
484    }
485
486    fn identity(process: u64, thread: u64) -> ZirconIdentity {
487        ZirconIdentity { process: Koid::from_raw(process), thread: Koid::from_raw(thread) }
488    }
489
490    #[fuchsia::test]
491    async fn test_snapshot_existing_tasks() {
492        let (sender, receiver) = oneshot::channel();
493        spawn_kernel_and_run(async move |current_task| {
494            let kernel = current_task.kernel();
495            let pid = current_task.task.get_pid();
496            let tid = current_task.task.get_tid();
497            let identity = current_task.task.get_zircon_identity().unwrap();
498
499            // A task without an attached Zircon thread must be skipped by the seed.
500            let _another_current = create_task(&kernel, "another-task");
501
502            let session = kernel.trace_event_manager.open();
503
504            assert_eq!(session.resolve_tid_to_koid(tid), Some(identity.thread));
505            assert_eq!(session.resolve_pid_to_koid(pid), Some(identity.process));
506            assert_eq!(kernel.trace_event_manager.map.read().tid_to_koid.len(), 1);
507            assert_eq!(kernel.trace_event_manager.map.read().pid_to_koid.len(), 1);
508
509            sender.send(()).unwrap();
510        })
511        .await;
512        receiver.await.unwrap();
513    }
514
515    #[fuchsia::test]
516    fn test_forward_resolution() {
517        let manager = Arc::new(TracePerformanceEventManager::new_for_testing());
518        let session = manager.open();
519
520        manager.record(10, 10, identity(1001, 2001));
521        manager.record(10, 11, identity(1001, 2002));
522
523        // Worker thread whose leader (pid 20) never recorded a tid == 20 entry.
524        manager.record(20, 201, identity(2001, 3001));
525
526        // Zero resolves to zero.
527        assert_eq!(session.resolve_tid_to_koid(0), Some(Koid::from_raw(0)));
528        assert_eq!(session.resolve_pid_to_koid(0), Some(Koid::from_raw(0)));
529
530        // Mapped values.
531        assert_eq!(session.resolve_tid_to_koid(10), Some(Koid::from_raw(2001)));
532        assert_eq!(session.resolve_tid_to_koid(11), Some(Koid::from_raw(2002)));
533        assert_eq!(session.resolve_pid_to_koid(10), Some(Koid::from_raw(1001)));
534
535        // Process PID resolves even when leader TID is not mapped.
536        assert_eq!(session.resolve_pid_to_koid(20), Some(Koid::from_raw(2001)));
537        assert_eq!(session.resolve_tid_to_koid(201), Some(Koid::from_raw(3001)));
538        assert_eq!(session.resolve_tid_to_koid(20), None);
539
540        // Unmapped values return None.
541        assert_eq!(session.resolve_tid_to_koid(999), None);
542        assert_eq!(session.resolve_pid_to_koid(999), None);
543    }
544
545    #[fuchsia::test]
546    fn test_reverse_table_maintained() {
547        let manager = Arc::new(TracePerformanceEventManager::new_for_testing());
548        let _session = manager.open();
549        manager.record(10, 100, identity(1000, 2000));
550
551        assert_eq!(
552            manager.map.read().get_linux_identity(Koid::from_raw(2000)),
553            Some(LinuxIdentity::Thread { pid: 10, tid: 100 })
554        );
555        assert_eq!(
556            manager.map.read().get_linux_identity(Koid::from_raw(1000)),
557            Some(LinuxIdentity::Process { pid: 10 })
558        );
559        assert_eq!(manager.get_zircon_identity(100), Some(identity(1000, 2000)));
560    }
561
562    #[fuchsia::test]
563    fn test_seed_does_not_overwrite_fresh_record() {
564        let manager = Arc::new(TracePerformanceEventManager::new_for_testing());
565        let _session = manager.open();
566
567        // A task records a fresh entry, then a stale snapshot arrives with an outdated
568        // entry for the same tid: the fresh entry must win.
569        manager.record(1, 1, identity(101, 201));
570        let mut stale = PidKoidMap::default();
571        stale.insert(1, 1, identity(101, 999));
572        manager.map.write().extend_from(stale);
573
574        assert_eq!(manager.get_zircon_identity(1).map(|id| id.thread), Some(Koid::from_raw(201)));
575    }
576
577    #[fuchsia::test]
578    fn test_reverse_resolution() {
579        let manager = Arc::new(TracePerformanceEventManager::new_for_testing());
580        let session = manager.open();
581        manager.record(10, 100, identity(1000, 2000));
582
583        // Known thread koid resolves to its pid/tid.
584        assert_eq!(
585            session.resolve_koids(Koid::from_raw(1000), Koid::from_raw(2000)),
586            Some(LinuxIdentity::Thread { pid: 10, tid: 100 })
587        );
588
589        // Unknown thread koid under a known process returns None without negatively caching the process.
590        assert_eq!(session.resolve_koids(Koid::from_raw(1000), Koid::from_raw(9999)), None);
591        assert!(!manager.map.read().unmapped_processes.contains(&Koid::from_raw(1000)));
592
593        // Native Fuchsia process: returns None and the process lands in the negative cache.
594        assert_eq!(session.resolve_koids(Koid::from_raw(8), Koid::from_raw(7)), None);
595        assert!(manager.map.read().unmapped_processes.contains(&Koid::from_raw(8)));
596
597        // A task recording for a negatively cached process evicts the stale entry and
598        // resolves afterwards.
599        manager.record(30, 300, identity(8, 9));
600        assert!(!manager.map.read().unmapped_processes.contains(&Koid::from_raw(8)));
601        assert_eq!(
602            session.resolve_koids(Koid::from_raw(8), Koid::from_raw(9)),
603            Some(LinuxIdentity::Thread { pid: 30, tid: 300 })
604        );
605
606        // After the last session drops, the map and negative cache are released, so
607        // resolution returns None.
608        drop(session);
609        assert_eq!(manager.resolve_koids(Koid::from_raw(1000), Koid::from_raw(2000)), None);
610    }
611
612    #[fuchsia::test]
613    async fn test_overlapping_sessions() {
614        spawn_kernel_and_run(async move |current_task| {
615            let kernel = current_task.kernel();
616            let manager = &kernel.trace_event_manager;
617            assert!(!manager.is_recording());
618
619            // Two independent clients open overlapping sessions.
620            let session_a = manager.open();
621            let session_b = manager.open();
622            assert!(manager.is_recording());
623
624            manager.record(99, 999, identity(9900, 9901));
625            assert_eq!(session_b.resolve_tid_to_koid(999), Some(Koid::from_raw(9901)));
626
627            // Dropping one session must not disturb the other.
628            drop(session_a);
629            assert!(manager.is_recording());
630            assert_eq!(session_b.resolve_tid_to_koid(999), Some(Koid::from_raw(9901)));
631
632            // Dropping the last session releases the map and stops recording; the
633            // manager itself stays on the kernel.
634            drop(session_b);
635            assert!(!manager.is_recording());
636            assert!(manager.map.read().is_empty());
637        })
638        .await;
639    }
640
641    #[fuchsia::test]
642    async fn test_lifecycle() {
643        let (sender, receiver) = oneshot::channel();
644        spawn_kernel_and_run(async move |current_task| {
645            let kernel = current_task.kernel();
646            let session = kernel.trace_event_manager.open();
647
648            assert_eq!(kernel.trace_event_manager.map.read().tid_to_koid.len(), 1);
649
650            // Associate a thread with a new task.
651            let another_current = create_task(&kernel, "another-task");
652            let test_thread = another_current
653                .thread_group()
654                .process
655                .create_thread(b"my-new-test-thread")
656                .expect("test thread");
657
658            {
659                another_current
660                    .running_state()
661                    .thread
662                    .set(ZirconThread::new(Arc::new(test_thread)))
663                    .expect("test thread set");
664            }
665
666            assert_eq!(kernel.trace_event_manager.map.read().tid_to_koid.len(), 1);
667
668            // This is called by the task when it is all ready to run.
669            another_current.record_pid_koid_mapping();
670
671            // Now expect 2 mappings.
672            assert_eq!(kernel.trace_event_manager.map.read().tid_to_koid.len(), 2);
673            assert!(
674                kernel
675                    .trace_event_manager
676                    .get_zircon_identity(another_current.task.get_tid())
677                    .is_some()
678            );
679
680            drop(session);
681
682            // After the last session drops, record_pid_koid_mapping is a no-op.
683            another_current.record_pid_koid_mapping();
684            assert!(kernel.trace_event_manager.map.read().is_empty());
685            sender.send(()).unwrap();
686        })
687        .await;
688        receiver.await.unwrap();
689    }
690
691    #[fuchsia::test]
692    fn test_resolve_koids_avoids_write_lock_for_known_process() {
693        let manager = Arc::new(TracePerformanceEventManager::new_for_testing());
694        let _session = manager.open();
695        manager.record(10, 100, identity(1000, 2000));
696
697        // Hold a read lock on `map`. A query for an unmapped thread (9999) belonging to an
698        // already-known process (1000) must return `None` under the read lock without
699        // attempting to acquire the exclusive write lock (which would deadlock/block against
700        // `read_guard`).
701        let read_guard = manager.map.read();
702        let (sender, receiver) = std::sync::mpsc::channel();
703        let manager_clone = manager.clone();
704
705        std::thread::spawn(move || {
706            let res = manager_clone.resolve_koids(Koid::from_raw(1000), Koid::from_raw(9999));
707            let _ = sender.send(res);
708        });
709
710        let result = receiver.recv_timeout(std::time::Duration::from_millis(500));
711        drop(read_guard);
712        assert_eq!(result, Ok(None), "resolve_koids blocked on write lock for known process");
713    }
714
715    #[fuchsia::test]
716    async fn test_trace_sink_emits_dump_and_new_task() {
717        let (sender, receiver) = oneshot::channel();
718        spawn_kernel_and_run(async move |current_task| {
719            let kernel = current_task.kernel();
720            let pid = current_task.task.get_pid();
721            let tid = current_task.task.get_tid();
722            let identity = current_task.task.get_zircon_identity().unwrap();
723
724            let (sink, events, dump_completed) = TestTraceEventSink::new();
725            let manager = Arc::new(TracePerformanceEventManager::new_with_sink(
726                Arc::downgrade(&kernel),
727                Arc::new(sink),
728            ));
729
730            let session = manager.open();
731
732            // Initial dump should be completed and include current_task.
733            assert!(*dump_completed.lock().unwrap());
734            {
735                let dumped = events.lock().unwrap().clone();
736                assert_eq!(dumped.len(), 1);
737                assert_eq!(dumped[0], (pid, tid, identity, "dump".to_string()));
738            }
739
740            // Create a new task and associate a thread with it.
741            let another_current = create_task(&kernel, "another-task");
742            let test_thread = another_current
743                .thread_group()
744                .process
745                .create_thread(b"another-thread")
746                .expect("test thread");
747            let thread_koid = test_thread.koid().unwrap();
748            let process_koid = another_current.thread_group().process.koid().unwrap();
749            let another_identity = ZirconIdentity { process: process_koid, thread: thread_koid };
750
751            another_current
752                .running_state()
753                .thread
754                .set(ZirconThread::new(Arc::new(test_thread)))
755                .expect("test thread set");
756
757            let another_pid = another_current.task.get_pid();
758            let another_tid = another_current.task.get_tid();
759
760            manager.record(another_pid, another_tid, another_identity);
761
762            {
763                let all_events = events.lock().unwrap().clone();
764                assert_eq!(all_events.len(), 2);
765                assert_eq!(
766                    all_events[1],
767                    (another_pid, another_tid, another_identity, "new_task".to_string())
768                );
769            }
770
771            drop(session);
772            sender.send(()).unwrap();
773        })
774        .await;
775        receiver.await.unwrap();
776    }
777}