blob: 9fda5eeacc5f65f5922f5d3e407504afc4d745e1 [file] [log] [blame]
// Copyright 2025 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use crate::task::{Kernel, PidTable};
use starnix_logging::{log_debug, log_error, log_info};
use starnix_sync::RwLock;
use starnix_uapi::{pid_t, tid_t};
use std::collections::HashMap;
use std::sync::{Arc, Weak};
use zx::Koid;
#[derive(Debug, Clone)]
pub struct KoidPair {
pub process: Option<Koid>,
pub thread: Option<Koid>,
}
/// The Linux pid/tid to Koid map is a thread safe hashmap.
pub type PidToKoidMap = Arc<RwLock<HashMap<tid_t, KoidPair>>>;
pub struct TracePerformanceEventManager {
// This is the map of pid/tid to koid where the tuple is the process koid, thread koid.
// This grows unbounded (in the range of valid tid_t values). Users of this struct should
// call |stop| and |clear| once the trace processing is completed to avoid holding memory.
map: PidToKoidMap,
// In order to reduce overhead when processing the trace events, make a local copy of the mappings
// that is not thread safe and use that as the first level cache. Since this makes copies of
// copies of mappings, |clear| should be called when the mappings are no longer needed.
local_map: HashMap<tid_t, KoidPair>,
// Hold a weak reference to the Kernel so we can make sure the pid to koid map is removed from
// the kernel when this object is dropped.
// This reference is also used to indicate this manager has been started.
weak_kernel: Weak<Kernel>,
}
impl Drop for TracePerformanceEventManager {
fn drop(&mut self) {
// Stop is idempotent, and does not error if not started, or already stopped.
self.stop();
}
}
impl TracePerformanceEventManager {
pub fn new() -> Self {
Self { map: PidToKoidMap::default(), local_map: HashMap::new(), weak_kernel: Weak::new() }
}
/// Registers the map with the pid_table so the pid/tid to koid mappings can be recorded when
/// new threads are created. Since processing the trace events could be done past a thread's
/// lifetime, no mappings are removed when a thread or process exits.
/// Additional work may be needed to handle pid reuse (https://fxbug.dev/322874557), currently
/// new mapping information overwrites existing mappings.
///
/// Calling |start| when this instance has already been started will panic.
///
/// NOTE: This will record all thread and process mappings until |stop| is called. The mapping will
/// continue to exist in memory until |clear| is called. It is expected that this is a relatively
/// short period of time, such as the time during capturing a performance trace.
pub fn start(&mut self, kernel: &Arc<Kernel>) {
// Provide a reference to the mapping to the kernel so it can be updated as
// new threads/processes are created.
if self.weak_kernel.upgrade().is_some() {
log_error!(
"TracePerformanceEventManager has already been started. Re-initializing mapping"
);
}
self.weak_kernel = Arc::downgrade(kernel);
*kernel.pid_to_koid_mapping.write() = Some(self.map.clone());
let kernel_pids = kernel.pids.read();
let existing_pid_map = Self::read_existing_pid_map(&*kernel_pids);
self.map.write().extend(existing_pid_map);
}
/// Clears the pid to koid map reference in the kernel passed in to |start|. Stop is a no-op
/// if start has not been called, or if stop has already been called.
pub fn stop(&mut self) {
if let Some(kernel) = self.weak_kernel.upgrade() {
log_info!("Stopping trace pid mapping. Notifier set to None.");
*kernel.pid_to_koid_mapping.write() = None;
self.weak_kernel = Weak::new();
}
}
/// Clears the pid-koid map. After starting, call |load_pid_mappings| to
/// initialize the table with existing task/process data.
pub fn clear(&mut self) {
self.map.write().clear();
self.local_map.clear();
}
// Look up the pid/tid from a local copy of the pid-koid mapping table, and only
// take a lock on the mapping table if there is a missing key from the local map.
// Any new keys are added to the local map.
fn get_mapping(&mut self, pid: pid_t) -> &KoidPair {
if self.local_map.is_empty() {
let shared_map = self.map.read().clone();
self.local_map.extend(shared_map);
}
if self.local_map.contains_key(&pid) {
return self.local_map.get(&pid).expect("pid should always have a KoidPair.");
}
// If there is a miss, check the shared mapping table. This would only happen in
// extreme cases where the tracing events are being mapped while new events are being
// created by new threads.
let shared_map = self.map.read();
if let Some(koid_pair) = shared_map.get(&pid) {
self.local_map.insert(pid, koid_pair.clone());
return self.local_map.get(&pid).expect("pid should always have a KoidPair.");
}
unreachable!("all pids including {pid} should have mappings")
}
/// Maps a "pid" to the koid. This is also referred to as the "Process Id" in Perfetto terms.
pub fn map_pid_to_koid(&mut self, pid: pid_t) -> Koid {
self.get_mapping(pid).process.expect("all pids should have a process koid.")
}
/// Maps a "tid" to the koid. This is also referred to as the "Thread Id" in Perfetto terms.
pub fn map_tid_to_koid(&mut self, tid: tid_t) -> Koid {
self.get_mapping(tid).thread.expect("all tids should have a thread koid.")
}
/// Use the kernel pid table to make a mapping from linux pid to koid for existing entries.
fn read_existing_pid_map(pid_table: &PidTable) -> HashMap<tid_t, KoidPair> {
let mut pid_map = HashMap::new();
let ids = pid_table.task_ids();
for tid in &ids {
let pair = pid_table.get_task(*tid).upgrade().map(|t| KoidPair {
process: t.thread_group().get_process_koid().ok(),
thread: t.thread.read().as_ref().and_then(|t| t.koid().ok()),
});
if let Some(pair) = pair {
// ignore entries with no process or thread.
if pair.process.is_some() || pair.thread.is_some() {
pid_map.insert(*tid, pair);
}
} else {
unreachable!("Empty mapping for {tid}.");
}
}
log_debug!("Initialized {} pid mappings. From {} ids", pid_map.len(), ids.len());
pid_map
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::testing::{create_task, spawn_kernel_and_run};
use futures::channel::oneshot;
#[fuchsia::test]
async fn test_initialize_pid_map() {
let (sender, receiver) = oneshot::channel();
spawn_kernel_and_run(async move |locked, current_task| {
let kernel = current_task.kernel();
let pid = current_task.task.tid;
let tkoid = current_task.thread.read().as_ref().and_then(|t| t.koid().ok());
let pkoid = current_task.thread_group().get_process_koid().ok();
let _another_current = create_task(locked, &kernel, "another-task");
let pid_map = TracePerformanceEventManager::read_existing_pid_map(&*kernel.pids.read());
assert!(tkoid.is_some());
assert_eq!(pid_map.len(), 2, "Expected 2 entries in pid_map got {pid_map:?}");
assert!(pid_map.contains_key(&pid));
let pair = pid_map.get(&pid).unwrap();
assert_eq!(pair.process, pkoid);
assert_eq!(pair.thread, tkoid);
sender.send(()).unwrap();
})
.await;
receiver.await.unwrap();
}
#[fuchsia::test]
fn test_mapping() {
let mut manager = TracePerformanceEventManager::new();
let mut map = HashMap::new();
map.insert(
1,
KoidPair { process: Some(Koid::from_raw(101)), thread: Some(Koid::from_raw(201)) },
);
map.insert(2, KoidPair { process: Some(Koid::from_raw(102)), thread: None });
manager.map.write().extend(map);
assert_eq!(manager.map_pid_to_koid(1), Koid::from_raw(101));
assert_eq!(manager.map_tid_to_koid(1), Koid::from_raw(201));
assert_eq!(manager.map_pid_to_koid(2), Koid::from_raw(102));
}
#[fuchsia::test]
#[should_panic]
fn test_unmapped_tid() {
let mut manager = TracePerformanceEventManager::new();
manager.map_tid_to_koid(2);
}
#[fuchsia::test]
async fn test_lifecycle() {
let (sender, receiver) = oneshot::channel();
spawn_kernel_and_run(async move |locked, current_task| {
let kernel = current_task.kernel();
let mut manager = TracePerformanceEventManager::new();
manager.start(&kernel);
let pid_map = manager.map.read().clone();
assert_eq!(pid_map.len(), 1, "Expected 1 entry in pid_map got {pid_map:?}");
// Associate a thread with a new task.
let another_current = create_task(locked, &kernel, "another-task");
let test_thread = another_current
.thread_group()
.process
.create_thread(b"my-new-test-thread")
.expect("test thread");
let mut thread = another_current.thread.write();
*thread = Some(Arc::new(test_thread));
drop(thread);
let pid_map = manager.map.read().clone();
let pid_dump = format!("{pid_map:?}");
assert_eq!(pid_map.len(), 1, "Expected 1 entry in pid_map got {pid_dump}");
// This is called by the task when it is all ready to run.
another_current.record_pid_koid_mapping();
// Now expect 2 mappings.
let pid_map = manager.map.read().clone();
let pid_dump = format!("{pid_map:?}");
assert_eq!(pid_map.len(), 2, "Expected 2 entries in pid_map got {pid_dump}");
// Read the mappings, if it is not present, it will panic.
let _ = manager.map_pid_to_koid(another_current.task.get_pid());
let _ = manager.map_pid_to_koid(another_current.task.get_tid());
manager.stop();
manager.clear();
assert!(manager.map.read().is_empty());
sender.send(()).unwrap();
})
.await;
receiver.await.unwrap();
}
}