Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move perf-map loading into the Process #36

Merged
merged 1 commit into from
Mar 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 1 addition & 31 deletions samply/src/import/perf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use linux_perf_data::linux_perf_event_reader;
use linux_perf_data::{DsoInfo, DsoKey, PerfFileReader, PerfFileRecord};
use linux_perf_event_reader::EventRecord;

use std::collections::{HashMap, HashSet};
use std::collections::HashMap;
use std::io::{Read, Seek};
use std::path::{Path, PathBuf};

Expand Down Expand Up @@ -51,28 +51,6 @@ pub fn convert<C: Read + Seek>(cursor: C, extra_dir: Option<&Path>) -> Result<Pr
Ok(profile)
}

/// Returns any pid associated with this event.
fn event_record_pid(r: &EventRecord<'_>) -> Option<i32> {
match r {
EventRecord::Sample(r) => r.pid,
EventRecord::Comm(r) => Some(r.pid),
EventRecord::Exit(r) => Some(r.pid),
EventRecord::Fork(r) => Some(r.pid),
EventRecord::Mmap(r) => Some(r.pid),
EventRecord::Mmap2(r) => Some(r.pid),
EventRecord::ContextSwitch(r) => match r {
linux_perf_event_reader::ContextSwitchRecord::In { prev_pid, .. } => *prev_pid,
linux_perf_event_reader::ContextSwitchRecord::Out { next_pid, .. } => *next_pid,
},
EventRecord::Lost(_)
| EventRecord::Throttle(_)
| EventRecord::Unthrottle(_)
| EventRecord::Raw(_) => None,
// Necessary because non-exhaustive.
_ => None,
}
}

fn convert_impl<U, C, R>(
file: PerfFileReader<R>,
extra_dir: Option<&Path>,
Expand Down Expand Up @@ -126,8 +104,6 @@ where
interpretation.clone(),
);

let mut loaded_perf_maps = HashSet::new();

let mut last_timestamp = 0;
let mut jitdumps: Vec<JitDump> = Vec::new();

Expand Down Expand Up @@ -189,12 +165,6 @@ where
}
}

if let Some(pid) = event_record_pid(&parsed_record) {
if loaded_perf_maps.insert(pid) {
converter.try_load_perf_map(pid);
}
}

match parsed_record {
EventRecord::Sample(e) => {
if attr_index == interpretation.main_event_attr_index {
Expand Down
225 changes: 120 additions & 105 deletions samply/src/linux_shared/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -255,111 +255,6 @@ where
}
}

fn process_perf_map_line(line: &str) -> Option<(u64, u64, &str)> {
let mut split = line.splitn(3, ' ');
let addr = split.next()?;
let len = split.next()?;
let symbol_name = split.next()?;
if symbol_name.is_empty() {
return None;
}
let addr = u64::from_str_radix(addr.trim_start_matches("0x"), 16).ok()?;
let len = u64::from_str_radix(len.trim_start_matches("0x"), 16).ok()?;
Some((addr, len, symbol_name))
}

/// Tries to load a perf mapping file that could have been generated by the process during
/// execution.
pub fn try_load_perf_map(&mut self, pid: i32) {
let name = format!("perf-{pid}.map");
let path = format!("/tmp/{name}");
let Ok(content) = fs::read_to_string(&path) else { return };

let process = self.processes.get_by_pid(pid, &mut self.profile);

// Read the map file and set everything up so that absolute addresses
// in JIT code get symbolicated to the right function name.

// There are three ways to put function names into the profile:
//
// 1. Function name without address ("label frame"),
// 2. Address with after-the-fact symbolicated function name, and
// 3. Address with up-front symbolicated function name.
//
// Having the address on the frame allows the assembly view in the
// Firefox profiler to compute the right hitcount per instruction.
// However, with a perf.map file, we don't the code bytes of the jitted
// code, so we have no way of displaying the instructions. So the code
// address is not overly useful information, and we could just discard
// it and use label frames for perf.map JIT frames (approach 1).
//
// We'll be using approach 3 here anyway, so our JIT frames will have
// both a function name and a code address.

// Create a fake "library" for the JIT code.
let lib_handle = self.profile.add_lib(LibraryInfo {
debug_name: name.clone(),
name,
debug_path: path.clone(),
path,
debug_id: DebugId::nil(),
code_id: None,
arch: None,
symbol_table: None,
});

let mut symbols = Vec::new();
let mut cumulative_address = 0;

for line in content.lines() {
let Some((addr, len, symbol_name)) = Self::process_perf_map_line(line) else { continue };

let start_address = addr;
let end_address = addr + len;
let (category, js_name) = self
.jit_category_manager
.classify_jit_symbol(symbol_name, &mut self.profile);

// Add this function to process.jit_functions so that it can be consulted for
// category information and JS function prepending.
process.jit_functions.insert(JitFunction {
start_address,
end_address,
category,
js_name,
});

// Pretend that all JIT code is laid out consecutively in our fake library.
// This relative address is used for symbolication whenever we add a frame
// to the profile.
let relative_address = cumulative_address;
cumulative_address += len as u32;

// Add this function to the "mappings" of the fake library so that the
// profile can translate an absolute frame address to a relative address
// in the fake JIT library.
self.profile.add_lib_mapping(
process.profile_process,
lib_handle,
start_address,
end_address,
relative_address,
);

// Add a symbol for this function to the fake library's symbol table.
// This symbol will be looked up when the address is added to the profile,
// based on the relative address.
symbols.push(Symbol {
address: relative_address,
size: Some(len as u32),
name: symbol_name.to_owned(),
});
}

self.profile
.set_lib_symbol_table(lib_handle, Arc::new(SymbolTable::new(symbols)));
}

pub fn finish(self) -> Profile {
self.profile
}
Expand All @@ -377,6 +272,8 @@ where
let is_main = pid == tid;
let process = self.processes.get_by_pid(pid, &mut self.profile);

process.maybe_load_perf_map(&mut self.profile, &mut self.jit_category_manager);

let mut stack = Vec::new();
Self::get_sample_stack::<C>(&e, &process.unwinder, &mut self.cache, &mut stack);

Expand Down Expand Up @@ -683,6 +580,9 @@ where
.expect("Can't handle context switch without time");
let is_main = pid == tid;
let process = self.processes.get_by_pid(pid, &mut self.profile);

process.maybe_load_perf_map(&mut self.profile, &mut self.jit_category_manager);

let process_handle = process.profile_process;
let thread = self
.threads
Expand Down Expand Up @@ -1479,6 +1379,8 @@ where
unwinder: U::default(),
jit_functions: JitFunctions(Vec::new()),
name: None,
has_looked_up_perf_map: false,
pid: pid as u32,
}
})
}
Expand Down Expand Up @@ -1525,6 +1427,119 @@ struct Process<U> {
pub unwinder: U,
pub jit_functions: JitFunctions,
pub name: Option<String>,
pid: u32,
has_looked_up_perf_map: bool,
}

impl<U> Process<U> {
/// Tries to load a perf mapping file that could have been generated by the process during
/// execution.
fn maybe_load_perf_map(
&mut self,
profile: &mut Profile,
jit_category_manager: &mut JitCategoryManager,
) {
if self.has_looked_up_perf_map {
return;
}
self.has_looked_up_perf_map = true;

let name = format!("perf-{}.map", self.pid);
let path = format!("/tmp/{name}");
let Ok(content) = fs::read_to_string(&path) else { return };

// Read the map file and set everything up so that absolute addresses
// in JIT code get symbolicated to the right function name.

// There are three ways to put function names into the profile:
//
// 1. Function name without address ("label frame"),
// 2. Address with after-the-fact symbolicated function name, and
// 3. Address with up-front symbolicated function name.
//
// Having the address on the frame allows the assembly view in the
// Firefox profiler to compute the right hitcount per instruction.
// However, with a perf.map file, we don't have the code bytes of the jitted
// code, so we have no way of displaying the instructions. So the code
// address is not overly useful information, and we could just discard
// it and use label frames for perf.map JIT frames (approach 1).
//
// We'll be using approach 3 here anyway, so our JIT frames will have
// both a function name and a code address.

// Create a fake "library" for the JIT code.
let lib_handle = profile.add_lib(LibraryInfo {
debug_name: name.clone(),
name,
debug_path: path.clone(),
path,
debug_id: DebugId::nil(),
code_id: None,
arch: None,
symbol_table: None,
});

let mut symbols = Vec::new();
let mut cumulative_address = 0;

for (addr, len, symbol_name) in content.lines().filter_map(process_perf_map_line) {
let start_address = addr;
let end_address = addr + len;

let (category, js_name) =
jit_category_manager.classify_jit_symbol(symbol_name, profile);

// Add this function to process.jit_functions so that it can be consulted for
// category information and JS function prepending.
self.jit_functions.insert(JitFunction {
start_address,
end_address,
category,
js_name,
});

// Pretend that all JIT code is laid out consecutively in our fake library.
// This relative address is used for symbolication whenever we add a frame
// to the profile.
let relative_address = cumulative_address;
cumulative_address += len as u32;

// Add this function to the "mappings" of the fake library so that the
// profile can translate an absolute frame address to a relative address
// in the fake JIT library.
profile.add_lib_mapping(
self.profile_process,
lib_handle,
start_address,
end_address,
relative_address,
);

// Add a symbol for this function to the fake library's symbol table.
// This symbol will be looked up when the address is added to the profile,
// based on the relative address.
symbols.push(Symbol {
address: relative_address,
size: Some(len as u32),
name: symbol_name.to_owned(),
});
}

profile.set_lib_symbol_table(lib_handle, Arc::new(SymbolTable::new(symbols)));
}
}

fn process_perf_map_line(line: &str) -> Option<(u64, u64, &str)> {
let mut split = line.splitn(3, ' ');
let addr = split.next()?;
let len = split.next()?;
let symbol_name = split.next()?;
if symbol_name.is_empty() {
return None;
}
let addr = u64::from_str_radix(addr.trim_start_matches("0x"), 16).ok()?;
let len = u64::from_str_radix(len.trim_start_matches("0x"), 16).ok()?;
Some((addr, len, symbol_name))
}

struct JitFunctions(Vec<JitFunction>);
Expand Down