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::LockDepRwLock;
9use starnix_uapi::{pid_t, tid_t};
10use std::collections::{HashMap, HashSet};
11use std::sync::atomic::{AtomicUsize, Ordering};
12use std::sync::{Arc, Mutex, Weak};
13use zx::Koid;
14
15/// The Zircon koids backing one `Task`.
16///
17/// Throughout this module, "task" means `starnix_core::task::Task`, the Linux sense of
18/// the word (one thread of a thread group), never Zircon's task abstraction
19/// (job/process/thread).
20///
21/// Both koids are always present: a task is only recorded once its Zircon thread handle
22/// is attached and its thread group has a valid backing process. Tasks for which either
23/// half is unavailable (a task observed mid-creation by the initial seed, or a Starnix
24/// kernel thread with no backing Zircon process) are not stored; mid-creation tasks
25/// record themselves once their thread handle is attached.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub struct ZirconIdentity {
28    /// The Zircon process koid.
29    pub process: Koid,
30    /// The Zircon thread koid.
31    pub thread: Koid,
32}
33
34/// The Linux identity behind one Zircon koid.
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum LinuxIdentity {
37    /// Identifies a specific Linux thread within a process.
38    Thread { pid: pid_t, tid: tid_t },
39    /// Identifies a Linux process.
40    Process { pid: pid_t },
41}
42
43/// Bidirectional map between Linux pids/tids and Zircon koids.
44///
45/// Entries are only added while recording is active; nothing is removed when a thread or
46/// process exits so that trace data can be resolved past a `Task`'s lifetime. The whole
47/// map is released when the last recording session ends. Additional work may be needed to
48/// handle pid reuse (https://fxbug.dev/322874557); currently new mapping information
49/// overwrites existing thread mappings.
50#[derive(Debug, Default)]
51struct PidKoidMap {
52    /// Forward table: Linux tid to the koids backing that task.
53    tid_to_koid: HashMap<tid_t, ZirconIdentity>,
54    /// Forward table: Linux pid to the backing Zircon process koid.
55    pid_to_koid: HashMap<pid_t, Koid>,
56    /// Reverse table: Zircon koid (thread or process) to its Linux identity.
57    koid_to_pid: HashMap<Koid, LinuxIdentity>,
58    /// Process-level negative cache: process koids known not to belong to this container.
59    ///
60    /// System-wide profiling produces many samples from native Fuchsia processes whose
61    /// koids can never resolve; by Zircon's task hierarchy (a process that is not a
62    /// Starnix process has no Starnix threads), one cached miss per process replaces
63    /// two failed lookups per sample. Entries are evicted if a task later records for
64    /// that process koid, and the whole set is released with the map.
65    unmapped_processes: HashSet<Koid>,
66}
67
68impl PidKoidMap {
69    fn insert(&mut self, pid: pid_t, tid: tid_t, identity: ZirconIdentity) {
70        self.tid_to_koid.insert(tid, identity);
71        self.pid_to_koid.entry(pid).or_insert(identity.process);
72        self.koid_to_pid.insert(identity.thread, LinuxIdentity::Thread { pid, tid });
73        // Threads of the same process all report the same process koid; keep the first
74        // entry so the process continues to resolve to the same pid.
75        self.koid_to_pid.entry(identity.process).or_insert(LinuxIdentity::Process { pid });
76        // Evict a stale negative entry in case a sample was resolved before this task
77        // recorded itself.
78        self.unmapped_processes.remove(&identity.process);
79    }
80
81    fn get_zircon_identity(&self, tid: tid_t) -> Option<&ZirconIdentity> {
82        self.tid_to_koid.get(&tid)
83    }
84
85    fn get_process_koid(&self, pid: pid_t) -> Option<Koid> {
86        self.pid_to_koid.get(&pid).copied()
87    }
88
89    fn get_linux_identity(&self, koid: Koid) -> Option<LinuxIdentity> {
90        self.koid_to_pid.get(&koid).copied()
91    }
92
93    /// Merges mappings from another map without overwriting existing entries, so a
94    /// seeding snapshot cannot clobber a fresher entry recorded by a concurrently
95    /// spawning task.
96    fn extend_from(&mut self, other: PidKoidMap) {
97        for (tid, identity) in other.tid_to_koid {
98            self.unmapped_processes.remove(&identity.process);
99            self.tid_to_koid.entry(tid).or_insert(identity);
100        }
101        for (pid, koid) in other.pid_to_koid {
102            self.pid_to_koid.entry(pid).or_insert(koid);
103        }
104        for (koid, identity) in other.koid_to_pid {
105            self.koid_to_pid.entry(koid).or_insert(identity);
106        }
107        self.unmapped_processes.extend(other.unmapped_processes);
108    }
109}
110
111/// Kernel-wide manager of pid/tid to koid mappings, shared by all recording clients
112/// (system tracing and CPU profiling). Stored in `kernel.trace_event_manager`.
113///
114/// The manager lives for the lifetime of the `Kernel`; whether recording is active is
115/// tracked by the session count inside it, not by its presence. Clients start recording
116/// by obtaining a [`PidKoidSession`] from [`TracePerformanceEventManager::open`] and stop
117/// by dropping it, so overlapping sessions from independent clients compose correctly:
118/// the map is seeded when the first session opens and released when the last one drops,
119/// and cleanup runs on every exit path because it is driven by `Drop`.
120///
121/// When no session is active, task creation fast-paths out on a single relaxed atomic
122/// load with no locks and no allocations.
123pub struct TracePerformanceEventManager {
124    /// Weak reference to the enclosing `Kernel`, upgraded only on the 0 -> 1 session
125    /// transition to seed the map from `kernel.pids`. Weak because the manager is a field
126    /// of the `Kernel` itself (a strong reference would be a cycle).
127    weak_kernel: Weak<Kernel>,
128
129    /// Number of live [`PidKoidSession`]s. Maintained exclusively under [`state_lock`] by
130    /// [`TracePerformanceEventManager::open`] and [`PidKoidSession::drop`].
131    active_sessions: AtomicUsize,
132
133    /// Serializes session lifecycle transitions (0 -> 1 initialization and 1 -> 0 cleanup).
134    state_lock: Mutex<()>,
135
136    /// The bidirectional mapping table. Readers take short read locks per lookup; the
137    /// only writers are task spawns while recording (one insert each) and the session
138    /// seed/release transitions.
139    map: LockDepRwLock<PidKoidMap, starnix_sync::PidToKoidMapInnerLock>,
140}
141
142impl TracePerformanceEventManager {
143    /// Creates a new manager holding a weak reference to the enclosing `Kernel`.
144    pub fn new(weak_kernel: Weak<Kernel>) -> Self {
145        Self {
146            weak_kernel,
147            active_sessions: AtomicUsize::new(0),
148            state_lock: Mutex::new(()),
149            map: LockDepRwLock::new(PidKoidMap::default()),
150        }
151    }
152
153    /// Returns true if at least one session is actively recording mappings.
154    pub fn is_recording(&self) -> bool {
155        self.active_sessions.load(Ordering::Acquire) > 0
156    }
157
158    /// Opens a session. When the first session opens, the map is seeded with
159    /// all currently running `Task`s; tasks created afterwards record themselves via
160    /// `Task::record_pid_koid_mapping`. Recording continues until all active sessions
161    /// have been dropped.
162    pub fn open(self: &Arc<Self>) -> PidKoidSession {
163        self.start_session_internal();
164        PidKoidSession { manager: self.clone() }
165    }
166
167    /// Records the mapping for one `Task` if recording is active. Called from `Task`
168    /// creation, after the task's Zircon thread handle is attached, so the identity is
169    /// always complete.
170    pub fn record(&self, pid: pid_t, tid: tid_t, identity: ZirconIdentity) {
171        if self.active_sessions.load(Ordering::Acquire) == 0 {
172            return;
173        }
174        let mut map = self.map.write();
175        if self.active_sessions.load(Ordering::Acquire) == 0 {
176            return;
177        }
178        map.insert(pid, tid, identity);
179    }
180
181    /// Looks up the Zircon koids recorded for a Linux tid.
182    pub(crate) fn get_zircon_identity(&self, tid: tid_t) -> Option<ZirconIdentity> {
183        self.map.read().get_zircon_identity(tid).copied()
184    }
185
186    /// Looks up the Zircon process koid recorded for a Linux pid.
187    pub(crate) fn get_process_koid(&self, pid: pid_t) -> Option<Koid> {
188        self.map.read().get_process_koid(pid)
189    }
190
191    /// Reverse resolution: maps sampled (process koid, thread koid) to Linux identity,
192    /// with negative caching for native Fuchsia processes.
193    fn resolve_koids(&self, pkoid: Koid, tkoid: Koid) -> Option<LinuxIdentity> {
194        {
195            let map = self.map.read();
196            if map.unmapped_processes.contains(&pkoid) {
197                return None;
198            }
199            if let Some(identity) = map.get_linux_identity(tkoid) {
200                return Some(identity);
201            }
202            // If `pkoid` is already a known Starnix process, the write-lock slow path
203            // below (`if !map.koid_to_pid.contains_key(&pkoid)`) will not insert it into
204            // `unmapped_processes`. Return `None` here under the read lock so an unmapped
205            // thread in a known process does not fall through and acquire the exclusive
206            // write lock on every subsequent sample for a guaranteed no-op.
207            if map.koid_to_pid.contains_key(&pkoid) {
208                return None;
209            }
210        }
211
212        // Unknown process (`pkoid` not in `koid_to_pid` or `unmapped_processes`): record
213        // it in the negative cache under the write lock, re-checking first in case a
214        // concurrent `record()` populated it meanwhile.
215        let mut map = self.map.write();
216        if let Some(identity) = map.get_linux_identity(tkoid) {
217            return Some(identity);
218        }
219        if !map.koid_to_pid.contains_key(&pkoid) {
220            map.unmapped_processes.insert(pkoid);
221        }
222        None
223    }
224
225    /// Increments the session count, seeding the map from the kernel pid table when this
226    /// is the first session.
227    fn start_session_internal(&self) {
228        let _guard = self.state_lock.lock().unwrap();
229        let current = self.active_sessions.fetch_add(1, Ordering::AcqRel);
230        if current == 0 {
231            if let Some(kernel) = self.weak_kernel.upgrade() {
232                let snapshot = Self::snapshot_existing_tasks(&kernel.pids);
233                self.map.write().extend_from(snapshot);
234            } else {
235                log_warn!("Kernel is shutting down, unable to snapshot running tasks");
236            }
237        }
238    }
239
240    /// Decrements the session count, releasing the map when the last session drops.
241    fn stop_session_internal(&self) {
242        let _guard = self.state_lock.lock().unwrap();
243        if self.active_sessions.load(Ordering::Acquire) == 0 {
244            log_error!("session stopped without an active session");
245            return;
246        }
247        let current = self.active_sessions.fetch_sub(1, Ordering::AcqRel);
248        if current == 1 {
249            *self.map.write() = PidKoidMap::default();
250        }
251    }
252
253    /// Builds a map of all currently running `Task`s from the kernel pid table.
254    ///
255    /// A task is captured only if its identity is complete. A task observed mid-creation,
256    /// before its Zircon thread handle is attached, is skipped: it records itself moments
257    /// later (the executor attaches the thread handle and then calls
258    /// `Task::record_pid_koid_mapping` while this session is already active), and during
259    /// the gap its tid cannot appear in any trace data because its creator's syscall has
260    /// not yet returned. Starnix kernel threads, which have no backing Zircon process,
261    /// are intentionally excluded.
262    fn snapshot_existing_tasks(pid_table: &PidTable) -> PidKoidMap {
263        let mut pid_map = PidKoidMap::default();
264
265        let scope = RcuReadScope::new();
266        let mut count = 0;
267        for pid in pid_table.running_task_ids(&scope) {
268            count += 1;
269            // Running `Task`s may exit at any time. Record one only if a snapshot of its
270            // running state can be obtained.
271            let Ok(task) = pid.get_task() else {
272                continue;
273            };
274            if let Some(identity) = task.get_zircon_identity() {
275                pid_map.insert(task.get_pid(), pid.id, identity);
276            }
277        }
278
279        log_debug!("Initialized {} pid mappings. From {} ids", pid_map.tid_to_koid.len(), count);
280        pid_map
281    }
282}
283
284/// A reader's session and query interface for pid/koid mappings. Recording in the
285/// kernel stays active while at least one session is held across any client; dropping
286/// the session ends this client's interest. When all active sessions have been dropped,
287/// the shared map is released and recording ceases.
288///
289/// Resolution queries are only reachable through a session, which enforces at compile
290/// time that lookups happen while recording is active.
291#[must_use = "Recording stops when this guard is dropped"]
292pub struct PidKoidSession {
293    manager: Arc<TracePerformanceEventManager>,
294}
295
296impl PidKoidSession {
297    /// Forward resolution: maps a Linux tid to its thread koid, returning None on a miss.
298    pub fn resolve_tid_to_koid(&self, tid: tid_t) -> Option<Koid> {
299        if tid == 0 {
300            return Some(Koid::from_raw(0));
301        }
302        self.manager.get_zircon_identity(tid).map(|id| id.thread)
303    }
304
305    /// Forward resolution: maps a Linux pid to its process koid, returning None on a miss.
306    pub fn resolve_pid_to_koid(&self, pid: pid_t) -> Option<Koid> {
307        if pid == 0 {
308            return Some(Koid::from_raw(0));
309        }
310        self.manager.get_process_koid(pid)
311    }
312
313    /// Reverse resolution: maps sampled (process koid, thread koid) to Linux identity,
314    /// with negative caching for native Fuchsia processes. See
315    /// [`TracePerformanceEventManager::resolve_koids`].
316    pub fn resolve_koids(&self, pkoid: Koid, tkoid: Koid) -> Option<LinuxIdentity> {
317        self.manager.resolve_koids(pkoid, tkoid)
318    }
319}
320
321impl Drop for PidKoidSession {
322    fn drop(&mut self) {
323        self.manager.stop_session_internal();
324    }
325}
326
327#[cfg(test)]
328mod tests {
329    use super::*;
330    use crate::task::ZirconThread;
331    use crate::testing::{create_task, spawn_kernel_and_run};
332    use futures::channel::oneshot;
333
334    impl PidKoidMap {
335        fn is_empty(&self) -> bool {
336            self.tid_to_koid.is_empty() && self.pid_to_koid.is_empty()
337        }
338    }
339
340    impl TracePerformanceEventManager {
341        fn new_for_testing() -> Self {
342            Self::new(Weak::new())
343        }
344    }
345
346    fn identity(process: u64, thread: u64) -> ZirconIdentity {
347        ZirconIdentity { process: Koid::from_raw(process), thread: Koid::from_raw(thread) }
348    }
349
350    #[fuchsia::test]
351    async fn test_snapshot_existing_tasks() {
352        let (sender, receiver) = oneshot::channel();
353        spawn_kernel_and_run(async move |current_task| {
354            let kernel = current_task.kernel();
355            let pid = current_task.task.get_pid();
356            let tid = current_task.task.get_tid();
357            let identity = current_task.task.get_zircon_identity().unwrap();
358
359            // A task without an attached Zircon thread must be skipped by the seed.
360            let _another_current = create_task(&kernel, "another-task");
361
362            let session = kernel.trace_event_manager.open();
363
364            assert_eq!(session.resolve_tid_to_koid(tid), Some(identity.thread));
365            assert_eq!(session.resolve_pid_to_koid(pid), Some(identity.process));
366            assert_eq!(kernel.trace_event_manager.map.read().tid_to_koid.len(), 1);
367            assert_eq!(kernel.trace_event_manager.map.read().pid_to_koid.len(), 1);
368
369            sender.send(()).unwrap();
370        })
371        .await;
372        receiver.await.unwrap();
373    }
374
375    #[fuchsia::test]
376    fn test_forward_resolution() {
377        let manager = Arc::new(TracePerformanceEventManager::new_for_testing());
378        let session = manager.open();
379
380        manager.record(10, 10, identity(1001, 2001));
381        manager.record(10, 11, identity(1001, 2002));
382
383        // Worker thread whose leader (pid 20) never recorded a tid == 20 entry.
384        manager.record(20, 201, identity(2001, 3001));
385
386        // Zero resolves to zero.
387        assert_eq!(session.resolve_tid_to_koid(0), Some(Koid::from_raw(0)));
388        assert_eq!(session.resolve_pid_to_koid(0), Some(Koid::from_raw(0)));
389
390        // Mapped values.
391        assert_eq!(session.resolve_tid_to_koid(10), Some(Koid::from_raw(2001)));
392        assert_eq!(session.resolve_tid_to_koid(11), Some(Koid::from_raw(2002)));
393        assert_eq!(session.resolve_pid_to_koid(10), Some(Koid::from_raw(1001)));
394
395        // Process PID resolves even when leader TID is not mapped.
396        assert_eq!(session.resolve_pid_to_koid(20), Some(Koid::from_raw(2001)));
397        assert_eq!(session.resolve_tid_to_koid(201), Some(Koid::from_raw(3001)));
398        assert_eq!(session.resolve_tid_to_koid(20), None);
399
400        // Unmapped values return None.
401        assert_eq!(session.resolve_tid_to_koid(999), None);
402        assert_eq!(session.resolve_pid_to_koid(999), None);
403    }
404
405    #[fuchsia::test]
406    fn test_reverse_table_maintained() {
407        let manager = Arc::new(TracePerformanceEventManager::new_for_testing());
408        let _session = manager.open();
409        manager.record(10, 100, identity(1000, 2000));
410
411        assert_eq!(
412            manager.map.read().get_linux_identity(Koid::from_raw(2000)),
413            Some(LinuxIdentity::Thread { pid: 10, tid: 100 })
414        );
415        assert_eq!(
416            manager.map.read().get_linux_identity(Koid::from_raw(1000)),
417            Some(LinuxIdentity::Process { pid: 10 })
418        );
419        assert_eq!(manager.get_zircon_identity(100), Some(identity(1000, 2000)));
420    }
421
422    #[fuchsia::test]
423    fn test_seed_does_not_overwrite_fresh_record() {
424        let manager = Arc::new(TracePerformanceEventManager::new_for_testing());
425        let _session = manager.open();
426
427        // A task records a fresh entry, then a stale snapshot arrives with an outdated
428        // entry for the same tid: the fresh entry must win.
429        manager.record(1, 1, identity(101, 201));
430        let mut stale = PidKoidMap::default();
431        stale.insert(1, 1, identity(101, 999));
432        manager.map.write().extend_from(stale);
433
434        assert_eq!(manager.get_zircon_identity(1).map(|id| id.thread), Some(Koid::from_raw(201)));
435    }
436
437    #[fuchsia::test]
438    fn test_reverse_resolution() {
439        let manager = Arc::new(TracePerformanceEventManager::new_for_testing());
440        let session = manager.open();
441        manager.record(10, 100, identity(1000, 2000));
442
443        // Known thread koid resolves to its pid/tid.
444        assert_eq!(
445            session.resolve_koids(Koid::from_raw(1000), Koid::from_raw(2000)),
446            Some(LinuxIdentity::Thread { pid: 10, tid: 100 })
447        );
448
449        // Unknown thread koid under a known process returns None without negatively caching the process.
450        assert_eq!(session.resolve_koids(Koid::from_raw(1000), Koid::from_raw(9999)), None);
451        assert!(!manager.map.read().unmapped_processes.contains(&Koid::from_raw(1000)));
452
453        // Native Fuchsia process: returns None and the process lands in the negative cache.
454        assert_eq!(session.resolve_koids(Koid::from_raw(8), Koid::from_raw(7)), None);
455        assert!(manager.map.read().unmapped_processes.contains(&Koid::from_raw(8)));
456
457        // A task recording for a negatively cached process evicts the stale entry and
458        // resolves afterwards.
459        manager.record(30, 300, identity(8, 9));
460        assert!(!manager.map.read().unmapped_processes.contains(&Koid::from_raw(8)));
461        assert_eq!(
462            session.resolve_koids(Koid::from_raw(8), Koid::from_raw(9)),
463            Some(LinuxIdentity::Thread { pid: 30, tid: 300 })
464        );
465
466        // After the last session drops, the map and negative cache are released, so
467        // resolution returns None.
468        drop(session);
469        assert_eq!(manager.resolve_koids(Koid::from_raw(1000), Koid::from_raw(2000)), None);
470    }
471
472    #[fuchsia::test]
473    async fn test_overlapping_sessions() {
474        spawn_kernel_and_run(async move |current_task| {
475            let kernel = current_task.kernel();
476            let manager = &kernel.trace_event_manager;
477            assert!(!manager.is_recording());
478
479            // Two independent clients open overlapping sessions.
480            let session_a = manager.open();
481            let session_b = manager.open();
482            assert!(manager.is_recording());
483
484            manager.record(99, 999, identity(9900, 9901));
485            assert_eq!(session_b.resolve_tid_to_koid(999), Some(Koid::from_raw(9901)));
486
487            // Dropping one session must not disturb the other.
488            drop(session_a);
489            assert!(manager.is_recording());
490            assert_eq!(session_b.resolve_tid_to_koid(999), Some(Koid::from_raw(9901)));
491
492            // Dropping the last session releases the map and stops recording; the
493            // manager itself stays on the kernel.
494            drop(session_b);
495            assert!(!manager.is_recording());
496            assert!(manager.map.read().is_empty());
497        })
498        .await;
499    }
500
501    #[fuchsia::test]
502    async fn test_lifecycle() {
503        let (sender, receiver) = oneshot::channel();
504        spawn_kernel_and_run(async move |current_task| {
505            let kernel = current_task.kernel();
506            let session = kernel.trace_event_manager.open();
507
508            assert_eq!(kernel.trace_event_manager.map.read().tid_to_koid.len(), 1);
509
510            // Associate a thread with a new task.
511            let another_current = create_task(&kernel, "another-task");
512            let test_thread = another_current
513                .thread_group()
514                .process
515                .create_thread(b"my-new-test-thread")
516                .expect("test thread");
517
518            {
519                another_current
520                    .running_state()
521                    .thread
522                    .set(ZirconThread::new(Arc::new(test_thread)))
523                    .expect("test thread set");
524            }
525
526            assert_eq!(kernel.trace_event_manager.map.read().tid_to_koid.len(), 1);
527
528            // This is called by the task when it is all ready to run.
529            another_current.record_pid_koid_mapping();
530
531            // Now expect 2 mappings.
532            assert_eq!(kernel.trace_event_manager.map.read().tid_to_koid.len(), 2);
533            assert!(
534                kernel
535                    .trace_event_manager
536                    .get_zircon_identity(another_current.task.get_tid())
537                    .is_some()
538            );
539
540            drop(session);
541
542            // After the last session drops, record_pid_koid_mapping is a no-op.
543            another_current.record_pid_koid_mapping();
544            assert!(kernel.trace_event_manager.map.read().is_empty());
545            sender.send(()).unwrap();
546        })
547        .await;
548        receiver.await.unwrap();
549    }
550
551    #[fuchsia::test]
552    fn test_resolve_koids_avoids_write_lock_for_known_process() {
553        let manager = Arc::new(TracePerformanceEventManager::new_for_testing());
554        let _session = manager.open();
555        manager.record(10, 100, identity(1000, 2000));
556
557        // Hold a read lock on `map`. A query for an unmapped thread (9999) belonging to an
558        // already-known process (1000) must return `None` under the read lock without
559        // attempting to acquire the exclusive write lock (which would deadlock/block against
560        // `read_guard`).
561        let read_guard = manager.map.read();
562        let (sender, receiver) = std::sync::mpsc::channel();
563        let manager_clone = manager.clone();
564
565        std::thread::spawn(move || {
566            let res = manager_clone.resolve_koids(Koid::from_raw(1000), Koid::from_raw(9999));
567            let _ = sender.send(res);
568        });
569
570        let result = receiver.recv_timeout(std::time::Duration::from_millis(500));
571        drop(read_guard);
572        assert_eq!(result, Ok(None), "resolve_koids blocked on write lock for known process");
573    }
574}