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

Tidy tidy #101

Merged
merged 4 commits into from
Nov 3, 2024
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
10 changes: 5 additions & 5 deletions src/collector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ use std::sync::{Arc, Mutex};
use std::time::Duration;
use tracing::{debug, span, Level};

use crate::process::ObjectFileInfo;
use crate::process::ProcessInfo;
use crate::profile::raw_to_processed;
use crate::profile::AggregatedProfile;
use crate::profile::AggregatedSample;
use crate::profile::RawAggregatedProfile;
use crate::profile::{symbolize_profile, to_pprof};
use crate::profiler::AggregatedProfile;
use crate::profiler::AggregatedSample;
use crate::profiler::ObjectFileInfo;
use crate::profiler::ProcessInfo;
use crate::profiler::RawAggregatedProfile;
use lightswitch_object::ExecutableId;

use lightswitch_metadata_provider::metadata_provider::ThreadSafeGlobalMetadataProvider;
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ pub mod bpf;
pub mod collector;
pub mod ksym;
pub mod perf_events;
pub mod process;
pub mod profile;
pub mod profiler;
pub mod unwind_info;
Expand Down
30 changes: 12 additions & 18 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,12 +150,12 @@ struct Cli {
)]
show_info: Option<String>,
/// How long this agent will run in seconds
#[arg(short='D', long, default_value = Duration::MAX.as_secs().to_string(),
#[arg(short='D', long, default_value = ProfilerConfig::default().duration.as_secs().to_string(),
value_parser = parse_duration)]
duration: Duration,
/// Enable libbpf logs. This includes the BPF verifier output
#[arg(long)]
libbpf_logs: bool,
libbpf_debug: bool,
/// Enable BPF programs logging
#[arg(long)]
bpf_logging: bool,
Expand All @@ -165,7 +165,7 @@ struct Cli {
// Verification for this option guarantees the only possible selections
// are prime numbers up to and including 1001
/// Per-CPU Sampling Frequency in Hz
#[arg(long, default_value_t = 19, value_name = "SAMPLE_FREQ_IN_HZ",
#[arg(long, default_value_t = ProfilerConfig::default().sample_freq, value_name = "SAMPLE_FREQ_IN_HZ",
value_parser = sample_freq_in_range,
)]
sample_freq: u16,
Expand All @@ -182,34 +182,29 @@ struct Cli {
#[arg(long, default_value_t, value_enum)]
sender: ProfileSender,
// Buffer Sizes with defaults
#[arg(long, default_value_t = 512 * 1024, value_name = "PERF_BUFFER_BYTES",
#[arg(long, default_value_t = ProfilerConfig::default().perf_buffer_bytes, value_name = "PERF_BUFFER_BYTES",
help="Size of each profiler perf buffer, in bytes (must be a power of 2)",
value_parser = value_is_power_of_two)]
perf_buffer_bytes: usize,
// Print out info on eBPF map sizes
#[arg(long, help = "Print eBPF map sizes after creation")]
mapsize_info: bool,
// eBPF map stacks
#[arg(
long,
default_value_t = 100000,
help = "max number of individual \
stacks to capture before aggregation"
default_value_t = ProfilerConfig::default().mapsize_stacks,
help = "max number of individual stacks to capture before aggregation"
)]
mapsize_stacks: u32,
// eBPF map aggregated_stacks
#[arg(
long,
default_value_t = 10000,
help = "Derived from constant MAX_AGGREGATED_STACKS_ENTRIES - max \
number of unique stacks after aggregation"
default_value_t = ProfilerConfig::default().mapsize_aggregated_stacks,
help = "max number of unique stacks after aggregation"
)]
mapsize_aggregated_stacks: u32,
// eBPF map unwind_info_chunks
#[arg(
long,
default_value_t = 5000,
help = "max number of chunks allowed inside a shard"
default_value_t = ProfilerConfig::default().mapsize_rate_limits,
help = "max number of rate limit entries"
)]
mapsize_rate_limits: u32,
// Exclude myself from profiling
Expand Down Expand Up @@ -310,8 +305,7 @@ fn main() -> Result<(), Box<dyn Error>> {
}));

let profiler_config = ProfilerConfig {
// NOTE the difference in this arg name from the actual config name
libbpf_debug: args.libbpf_logs,
libbpf_debug: args.libbpf_debug,
bpf_logging: args.bpf_logging,
duration: args.duration,
sample_freq: args.sample_freq,
Expand Down Expand Up @@ -426,7 +420,7 @@ mod tests {
cmd.assert().success();
let actual = String::from_utf8(cmd.unwrap().stdout).unwrap();
insta::assert_yaml_snapshot!(actual, @r#"
"Usage: lightswitch [OPTIONS]\n\nOptions:\n --pids <PIDS>\n Specific PIDs to profile\n\n --tids <TIDS>\n Specific TIDs to profile (these can be outside the PIDs selected above)\n\n --show-unwind-info <PATH_TO_BINARY>\n Show unwind info for given binary\n\n --show-info <PATH_TO_BINARY>\n Show build ID for given binary\n\n -D, --duration <DURATION>\n How long this agent will run in seconds\n \n [default: 18446744073709551615]\n\n --libbpf-logs\n Enable libbpf logs. This includes the BPF verifier output\n\n --bpf-logging\n Enable BPF programs logging\n\n --logging <LOGGING>\n Set lightswitch's logging level\n \n [default: info]\n [possible values: trace, debug, info, warn, error]\n\n --sample-freq <SAMPLE_FREQ_IN_HZ>\n Per-CPU Sampling Frequency in Hz\n \n [default: 19]\n\n --profile-format <PROFILE_FORMAT>\n Output file for Flame Graph in SVG format\n \n [default: flame-graph]\n [possible values: none, flame-graph, pprof]\n\n --profile-path <PROFILE_PATH>\n Path for the generated profile\n\n --profile-name <PROFILE_NAME>\n Name for the generated profile\n\n --sender <SENDER>\n Where to write the profile\n \n [default: local-disk]\n\n Possible values:\n - none: Discard the profile. Used for kernel tests\n - local-disk\n - remote\n\n --perf-buffer-bytes <PERF_BUFFER_BYTES>\n Size of each profiler perf buffer, in bytes (must be a power of 2)\n \n [default: 524288]\n\n --mapsize-info\n Print eBPF map sizes after creation\n\n --mapsize-stacks <MAPSIZE_STACKS>\n max number of individual stacks to capture before aggregation\n \n [default: 100000]\n\n --mapsize-aggregated-stacks <MAPSIZE_AGGREGATED_STACKS>\n Derived from constant MAX_AGGREGATED_STACKS_ENTRIES - max number of unique stacks after aggregation\n \n [default: 10000]\n\n --mapsize-rate-limits <MAPSIZE_RATE_LIMITS>\n max number of chunks allowed inside a shard\n \n [default: 5000]\n\n --exclude-self\n Do not profile the profiler (myself)\n\n --symbolizer <SYMBOLIZER>\n [default: local]\n [possible values: local, none]\n\n -h, --help\n Print help (see a summary with '-h')\n"
"Usage: lightswitch [OPTIONS]\n\nOptions:\n --pids <PIDS>\n Specific PIDs to profile\n\n --tids <TIDS>\n Specific TIDs to profile (these can be outside the PIDs selected above)\n\n --show-unwind-info <PATH_TO_BINARY>\n Show unwind info for given binary\n\n --show-info <PATH_TO_BINARY>\n Show build ID for given binary\n\n -D, --duration <DURATION>\n How long this agent will run in seconds\n \n [default: 18446744073709551615]\n\n --libbpf-debug\n Enable libbpf logs. This includes the BPF verifier output\n\n --bpf-logging\n Enable BPF programs logging\n\n --logging <LOGGING>\n Set lightswitch's logging level\n \n [default: info]\n [possible values: trace, debug, info, warn, error]\n\n --sample-freq <SAMPLE_FREQ_IN_HZ>\n Per-CPU Sampling Frequency in Hz\n \n [default: 19]\n\n --profile-format <PROFILE_FORMAT>\n Output file for Flame Graph in SVG format\n \n [default: flame-graph]\n [possible values: none, flame-graph, pprof]\n\n --profile-path <PROFILE_PATH>\n Path for the generated profile\n\n --profile-name <PROFILE_NAME>\n Name for the generated profile\n\n --sender <SENDER>\n Where to write the profile\n \n [default: local-disk]\n\n Possible values:\n - none: Discard the profile. Used for kernel tests\n - local-disk\n - remote\n\n --perf-buffer-bytes <PERF_BUFFER_BYTES>\n Size of each profiler perf buffer, in bytes (must be a power of 2)\n \n [default: 524288]\n\n --mapsize-info\n Print eBPF map sizes after creation\n\n --mapsize-stacks <MAPSIZE_STACKS>\n max number of individual stacks to capture before aggregation\n \n [default: 100000]\n\n --mapsize-aggregated-stacks <MAPSIZE_AGGREGATED_STACKS>\n max number of unique stacks after aggregation\n \n [default: 10000]\n\n --mapsize-rate-limits <MAPSIZE_RATE_LIMITS>\n max number of rate limit entries\n \n [default: 5000]\n\n --exclude-self\n Do not profile the profiler (myself)\n\n --symbolizer <SYMBOLIZER>\n [default: local]\n [possible values: local, none]\n\n -h, --help\n Print help (see a summary with '-h')\n"
"#);
}

Expand Down
263 changes: 263 additions & 0 deletions src/process.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
use std::collections::HashMap;
use std::fs;
use std::fs::File;
use std::os::fd::AsRawFd;
use std::path::PathBuf;
use std::process;

use tracing::debug;

use lightswitch_object::BuildId;
use lightswitch_object::ElfLoad;
use lightswitch_object::ExecutableId;

pub type Pid = i32;

/// What type of mapping we are dealing with.
#[derive(Debug, Clone, PartialEq)]
pub enum ExecutableMappingType {
/// An object file that got loaded from disk.
FileBacked,
/// Note file backed, typically produced by a JIT runtime.
Anonymous,
/// Special mapping to optimise certain system calls.
Vdso,
}

#[derive(Clone)]
pub enum ProcessStatus {
Running,
Exited,
}

#[derive(Clone)]
pub struct ProcessInfo {
pub status: ProcessStatus,
pub mappings: ExecutableMappings,
}

/// Stores information for a executable mapping with all
/// the information we need to do everything symbolization
/// related.
#[derive(Debug, Clone)]
pub struct ExecutableMapping {
pub executable_id: ExecutableId,
pub build_id: Option<BuildId>,
pub kind: ExecutableMappingType,
pub start_addr: u64,
pub end_addr: u64,
pub offset: u64,
pub load_address: u64,
pub main_exec: bool,
pub soft_delete: bool,
}

#[derive(Clone)]
pub struct ExecutableMappings(pub Vec<ExecutableMapping>);

impl ExecutableMappings {
/// Find the executable mapping a given virtual address falls into.
pub fn for_address(&self, virtual_address: u64) -> Option<ExecutableMapping> {
for mapping in &self.0 {
if (mapping.start_addr..mapping.end_addr).contains(&virtual_address) {
return Some(mapping.clone());
}
}

None
}
}

impl ExecutableMapping {
/// Soft delete a mapping. We don't want to delete it straight away as we
/// might need it for a bit longer for normalization and / or local symbolization.
pub fn mark_as_deleted(
&mut self,
object_files: &mut HashMap<ExecutableId, ObjectFileInfo>,
) -> bool {
// The executable mapping can be removed at a later time, and function might be called multiple
// times. To avoid this, we keep track of whether this mapping has been soft deleted.
if self.soft_delete {
return false;
}
self.soft_delete = true;

if let Some(object_file) = object_files.get_mut(&self.executable_id) {
// Object files are also soft deleted, so do not try to decrease the reference count
// if it's already zero.
if object_file.references == 0 {
return false;
}

object_file.references -= 1;

if object_file.references == 0 {
debug!(
"object file with path {} can be deleted",
object_file.path.display()
);
return true;
}

debug_assert!(
object_file.references >= 0,
"Reference count for {} is negative: {}",
object_file.path.display(),
object_file.references,
);
}
false
}
}

pub struct ObjectFileInfo {
pub file: fs::File,
pub path: PathBuf,
pub elf_load_segments: Vec<ElfLoad>,
pub is_dyn: bool,
pub references: i64,
pub native_unwind_info_size: Option<u64>,
}

impl Clone for ObjectFileInfo {
fn clone(&self) -> Self {
ObjectFileInfo {
file: self.open_file_from_procfs_fd(),
path: self.path.clone(),
elf_load_segments: self.elf_load_segments.clone(),
is_dyn: self.is_dyn,
references: self.references,
native_unwind_info_size: self.native_unwind_info_size,
}
}
}

impl ObjectFileInfo {
/// Files might be removed at any time from the file system and they won't
/// be accessible anymore with their path. We work around this by doing the
/// following:
///
/// - We open object files as soon as we learn about them, that way we increase
/// the reference count of the file in the kernel. Files won't really be deleted
/// until the reference count drops to zero.
/// - In order to re-open files even if they've been deleted, we can use the procfs
/// interface, as long as their reference count hasn't reached zero and the kernel
/// hasn't removed the file from the file system and the various caches.
fn open_file_from_procfs_fd(&self) -> File {
let raw_fd = self.file.as_raw_fd();
File::open(format!("/proc/{}/fd/{}", process::id(), raw_fd)).expect(
"re-opening the file from procfs will never fail as we have an already opened file",
)
}

/// Returns the procfs path for this file descriptor. See comment above.
pub fn open_file_path(&self) -> PathBuf {
let raw_fd = self.file.as_raw_fd();
PathBuf::from(format!("/proc/{}/fd/{}", process::id(), raw_fd))
}

/// For a virtual address return the offset within the object file. This is
/// necessary for off-host symbolization. In order to do this we must check every
/// `PT_LOAD` segment.
pub fn normalized_address(
&self,
virtual_address: u64,
mapping: &ExecutableMapping,
) -> Option<u64> {
let offset = virtual_address - mapping.start_addr + mapping.offset;

for segment in &self.elf_load_segments {
let address_range = segment.p_vaddr..(segment.p_vaddr + segment.p_memsz);
if address_range.contains(&offset) {
return Some(offset - segment.p_offset + segment.p_vaddr);
}
}

None
}
}

#[cfg(test)]
mod tests {
use super::*;

/// This tests ensures that cloning an `ObjectFileInfo` succeeds to
/// open the file even if it's been deleted. This works because we
/// always keep at least one open file descriptor to prevent the kernel
/// from freeing the resource, effectively removing the file from the
/// file system.
#[test]
fn test_object_file_clone() {
use std::fs::remove_file;
use std::io::Read;

let named_tmpfile = tempfile::NamedTempFile::new().unwrap();
let file_path = named_tmpfile.path();
let file = File::open(file_path).unwrap();

let object_file_info = ObjectFileInfo {
file,
path: file_path.to_path_buf(),
elf_load_segments: vec![],
is_dyn: false,
references: 1,
native_unwind_info_size: None,
};

remove_file(file_path).unwrap();

let mut object_file_info_copy = object_file_info.clone();
let mut buf = String::new();
// This would fail without the procfs hack.
object_file_info_copy.file.read_to_string(&mut buf).unwrap();
}

#[test]
fn test_address_normalization() {
let mut object_file_info = ObjectFileInfo {
file: File::open("/").unwrap(),
path: "/".into(),
elf_load_segments: vec![],
is_dyn: false,
references: 0,
native_unwind_info_size: None,
};

let mapping = ExecutableMapping {
executable_id: 0x0,
build_id: None,
kind: ExecutableMappingType::FileBacked,
start_addr: 0x100,
end_addr: 0x100 + 100,
offset: 0x0,
load_address: 0x0,
main_exec: false,
soft_delete: false,
};

// no elf segments
assert!(object_file_info
.normalized_address(0x110, &mapping)
.is_none());

// matches an elf segment
object_file_info.elf_load_segments = vec![ElfLoad {
p_offset: 0x1,
p_vaddr: 0x0,
p_memsz: 0x20,
}];
assert_eq!(
object_file_info.normalized_address(0x110, &mapping),
Some(0xF)
);
// does not match any elf segments
object_file_info.elf_load_segments = vec![ElfLoad {
p_offset: 0x0,
p_vaddr: 0x0,
p_memsz: 0x5,
}];
assert!(object_file_info
.normalized_address(0x110, &mapping)
.is_none());
}
}
Loading