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 starnix_logging::{log_debug, log_error, log_info};
7use starnix_sync::RwLock;
8use starnix_uapi::{pid_t, tid_t};
9use std::collections::HashMap;
10use std::sync::{Arc, Weak};
11use zx::{AsHandleRef, Koid};
12
13#[derive(Debug, Clone)]
14pub struct KoidPair {
15    pub process: Option<Koid>,
16    pub thread: Option<Koid>,
17}
18
19/// The Linux pid/tid to Koid map is a thread safe hashmap.
20pub type PidToKoidMap = Arc<RwLock<HashMap<tid_t, KoidPair>>>;
21
22pub struct TracePerformanceEventManager {
23    // This is the map of pid/tid to koid where the tuple is the process koid, thread koid.
24    // This grows unbounded (in the range of valid tid_t values). Users of this struct should
25    // call |stop| and |clear| once the trace processing is completed to avoid holding memory.
26    map: PidToKoidMap,
27
28    // In order to reduce overhead when processing the trace events, make a local copy of the mappings
29    // that is not thread safe and use that as the first level cache. Since this makes copies of
30    // copies of mappings, |clear| should be called when the mappings are no longer needed.
31    local_map: HashMap<tid_t, KoidPair>,
32
33    // Hold a weak reference to the Kernel so we can make sure the pid to koid map is removed from
34    // the kernel when this object is dropped.
35    // This reference is also used to indicate this manager has been started.
36    weak_kernel: Weak<Kernel>,
37}
38
39impl Drop for TracePerformanceEventManager {
40    fn drop(&mut self) {
41        // Stop is idempotent, and does not error if not started, or already stopped.
42        self.stop();
43    }
44}
45
46impl TracePerformanceEventManager {
47    pub fn new() -> Self {
48        Self { map: PidToKoidMap::default(), local_map: HashMap::new(), weak_kernel: Weak::new() }
49    }
50
51    /// Registers the map with the pid_table so the pid/tid to koid mappings can be recorded when
52    /// new threads are created. Since processing the trace events could be done past a thread's
53    /// lifetime, no mappings are removed when a thread or process exits.
54    /// Additional work may be needed to handle pid reuse (https://fxbug.dev/322874557), currently
55    /// new mapping information overwrites existing mappings.
56    ///
57    /// Calling |start| when this instance has already been started will panic.
58    ///
59    /// NOTE: This will record all thread and process mappings until |stop| is called. The mapping will
60    /// continue to exist in memory until |clear| is called. It is expected that this is a relatively
61    /// short period of time, such as the time during capturing a performance trace.
62    pub fn start(&mut self, kernel: &Arc<Kernel>) {
63        // Provide a reference to the mapping to the kernel so it can be updated as
64        // new threads/processes are created.
65
66        if self.weak_kernel.upgrade().is_some() {
67            log_error!(
68                "TracePerformanceEventManager has already been started. Re-initializing mapping"
69            );
70        }
71
72        self.weak_kernel = Arc::downgrade(kernel);
73        *kernel.pid_to_koid_mapping.write() = Some(self.map.clone());
74
75        let kernel_pids = kernel.pids.read();
76        let existing_pid_map = Self::read_existing_pid_map(&*kernel_pids);
77        self.map.write().extend(existing_pid_map);
78    }
79
80    /// Clears the pid to koid map reference in the kernel passed in to |start|. Stop is a no-op
81    /// if start has not been called, or if stop has already been called.
82    pub fn stop(&mut self) {
83        if let Some(kernel) = self.weak_kernel.upgrade() {
84            log_info!("Stopping trace pid mapping. Notifier set to None.");
85            *kernel.pid_to_koid_mapping.write() = None;
86            self.weak_kernel = Weak::new();
87        }
88    }
89
90    /// Clears the pid-koid map. After starting, call |load_pid_mappings| to
91    /// initialize the table with existing task/process data.
92    pub fn clear(&mut self) {
93        self.map.write().clear();
94        self.local_map.clear();
95    }
96
97    // Look up the pid/tid from a local copy of the pid-koid mapping table, and only
98    // take a lock on the mapping table if there is a missing key from the local map.
99    // Any new keys are added to the local map.
100    fn get_mapping(&mut self, pid: pid_t) -> &KoidPair {
101        if self.local_map.is_empty() {
102            let shared_map = self.map.read().clone();
103            self.local_map.extend(shared_map);
104        }
105
106        if self.local_map.contains_key(&pid) {
107            return self.local_map.get(&pid).expect("pid should always have a KoidPair.");
108        }
109
110        // If there is a miss, check the shared mapping table. This would only happen in
111        // extreme cases where the tracing events are being mapped while new events are being
112        // created by new threads.
113        let shared_map = self.map.read();
114        if let Some(koid_pair) = shared_map.get(&pid) {
115            self.local_map.insert(pid, koid_pair.clone());
116            return self.local_map.get(&pid).expect("pid should always have a KoidPair.");
117        }
118
119        unreachable!("all pids including {pid} should have mappings")
120    }
121
122    /// Maps a "pid" to the koid. This is also referred to as the "Process Id" in Perfetto terms.
123    pub fn map_pid_to_koid(&mut self, pid: pid_t) -> Koid {
124        self.get_mapping(pid).process.expect("all pids should have a process koid.")
125    }
126
127    /// Maps a "tid" to the koid. This is also referred to as the "Thread Id" in Perfetto terms.
128    pub fn map_tid_to_koid(&mut self, tid: tid_t) -> Koid {
129        self.get_mapping(tid).thread.expect("all tids should have a thread koid.")
130    }
131
132    /// Use the kernel pid table to make a mapping from linux pid to koid for existing entries.
133    fn read_existing_pid_map(pid_table: &PidTable) -> HashMap<tid_t, KoidPair> {
134        let mut pid_map = HashMap::new();
135
136        let ids = pid_table.task_ids();
137        for tid in &ids {
138            let pair = pid_table.get_task(*tid).upgrade().map(|t| KoidPair {
139                process: t.thread_group().get_process_koid().ok(),
140                thread: t.thread.read().as_ref().and_then(|t| t.get_koid().ok()),
141            });
142            if let Some(pair) = pair {
143                // ignore entries with no process or thread.
144                if pair.process.is_some() || pair.thread.is_some() {
145                    pid_map.insert(*tid, pair);
146                }
147            } else {
148                unreachable!("Empty mapping for {tid}.");
149            }
150        }
151
152        log_debug!("Initialized {} pid mappings. From {} ids", pid_map.len(), ids.len());
153        pid_map
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160    use crate::testing::{create_task, spawn_kernel_and_run};
161    use futures::channel::oneshot;
162
163    #[fuchsia::test]
164    async fn test_initialize_pid_map() {
165        let (sender, receiver) = oneshot::channel();
166        spawn_kernel_and_run(async move |locked, current_task| {
167            let kernel = current_task.kernel();
168            let pid = current_task.task.tid;
169            let tkoid = current_task.thread.read().as_ref().and_then(|t| t.get_koid().ok());
170            let pkoid = current_task.thread_group().get_process_koid().ok();
171
172            let _another_current = create_task(locked, &kernel, "another-task");
173
174            let pid_map = TracePerformanceEventManager::read_existing_pid_map(&*kernel.pids.read());
175
176            assert!(tkoid.is_some());
177            assert_eq!(pid_map.len(), 2, "Expected 2 entries in pid_map got {pid_map:?}");
178            assert!(pid_map.contains_key(&pid));
179
180            let pair = pid_map.get(&pid).unwrap();
181            assert_eq!(pair.process, pkoid);
182            assert_eq!(pair.thread, tkoid);
183            sender.send(()).unwrap();
184        })
185        .await;
186        receiver.await.unwrap();
187    }
188
189    #[fuchsia::test]
190    fn test_mapping() {
191        let mut manager = TracePerformanceEventManager::new();
192        let mut map = HashMap::new();
193        map.insert(
194            1,
195            KoidPair { process: Some(Koid::from_raw(101)), thread: Some(Koid::from_raw(201)) },
196        );
197        map.insert(2, KoidPair { process: Some(Koid::from_raw(102)), thread: None });
198        manager.map.write().extend(map);
199
200        assert_eq!(manager.map_pid_to_koid(1), Koid::from_raw(101));
201        assert_eq!(manager.map_tid_to_koid(1), Koid::from_raw(201));
202        assert_eq!(manager.map_pid_to_koid(2), Koid::from_raw(102));
203    }
204
205    #[fuchsia::test]
206    #[should_panic]
207    fn test_unmapped_tid() {
208        let mut manager = TracePerformanceEventManager::new();
209
210        manager.map_tid_to_koid(2);
211    }
212
213    #[fuchsia::test]
214    async fn test_lifecycle() {
215        let (sender, receiver) = oneshot::channel();
216        spawn_kernel_and_run(async move |locked, current_task| {
217            let kernel = current_task.kernel();
218            let mut manager = TracePerformanceEventManager::new();
219
220            manager.start(&kernel);
221
222            let pid_map = manager.map.read().clone();
223            assert_eq!(pid_map.len(), 1, "Expected 1 entry in pid_map got {pid_map:?}");
224
225            // Associate a thread with a new task.
226            let another_current = create_task(locked, &kernel, "another-task");
227            let test_thread = another_current
228                .thread_group()
229                .process
230                .create_thread(b"my-new-test-thread")
231                .expect("test thread");
232
233            let mut thread = another_current.thread.write();
234            *thread = Some(Arc::new(test_thread));
235            drop(thread);
236
237            let pid_map = manager.map.read().clone();
238            let pid_dump = format!("{pid_map:?}");
239            assert_eq!(pid_map.len(), 1, "Expected 1 entry in pid_map got {pid_dump}");
240
241            // This is called by the task when it is all ready to run.
242            another_current.record_pid_koid_mapping();
243
244            // Now expect 2 mappings.
245            let pid_map = manager.map.read().clone();
246            let pid_dump = format!("{pid_map:?}");
247            assert_eq!(pid_map.len(), 2, "Expected 2 entries in pid_map got {pid_dump}");
248
249            // Read the mappings, if it is not present, it will panic.
250            let _ = manager.map_pid_to_koid(another_current.task.get_pid());
251            let _ = manager.map_pid_to_koid(another_current.task.get_tid());
252
253            manager.stop();
254
255            manager.clear();
256            assert!(manager.map.read().is_empty());
257            sender.send(()).unwrap();
258        })
259        .await;
260        receiver.await.unwrap();
261    }
262}