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