use core::str;
use std::error::Error;
use std::ffi::CString;
use std::fs::File;
use std::io::IsTerminal;
use std::io::Write;
use std::panic;
use std::path::PathBuf;
use std::process::Command;
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;
use clap::Parser;
use crossbeam_channel::bounded;
use crossbeam_channel::tick;
use inferno::flamegraph;
#[cfg(feature = "live-tui")]
use lightswitch::collector::LiveCollector;
use lightswitch::collector::{
AggregatorCollector, Collector, FirefoxProfilerCollector, NullCollector, PerfettoCollector,
PyroscopeCollector, StreamingCollector,
};
use lightswitch::debug_info::DebugInfoManager;
use lightswitch::profile::symbolize_profile;
use lightswitch::util::FileId;
use lightswitch_unwind_info::compact_unwind_info;
use nix::unistd::Gid;
use nix::unistd::Uid;
#[cfg(feature = "live-tui")]
use std::sync::mpsc;
use tracing::{Level, debug, error, info};
use tracing_subscriber::FmtSubscriber;
use tracing_subscriber::fmt::format::FmtSpan;
use lightswitch_capabilities::has_btf;
use lightswitch_capabilities::system_info::SystemInfo;
use lightswitch_metadata::metadata_provider::{
GlobalMetadataProvider, ThreadSafeGlobalMetadataProvider,
};
use lightswitch::debug_info::{
DebugInfoBackendFilesystem, DebugInfoBackendNull, DebugInfoBackendRemote,
};
use lightswitch::kernel::kernel_build_id;
use lightswitch::profile::{fold_profile, to_pprof};
use lightswitch::profiler::{Profiler, ProfilerConfig};
use lightswitch::server::{ProfileFileFormat, find_default_profile, start_profile_server};
use lightswitch_object::MappedObjectFile;
use lightswitch_object::kernel::kaslr_offset;
use lightswitch_unwind_info::CompactUnwindInfoBuilder;
mod args;
mod killswitch;
mod validators;
use crate::args::CliArgs;
use crate::args::Commands;
use crate::args::DebugInfoBackend;
use crate::args::FlamegraphAggregation;
use crate::args::LoggingLevel;
use crate::args::ProfileFormat;
use crate::args::ProfileSender;
use crate::args::ServerProfileFormat;
use crate::args::Symbolizer;
use crate::killswitch::KillSwitch;
const DEFAULT_SERVER_URL: &str = "http://localhost:4567";
static KILLSWITCH_POLL_INTERVAL: Duration = Duration::from_secs(5);
fn panic_thread_hook() {
let orig_hook = panic::take_hook();
panic::set_hook(Box::new(move |panic_info| {
orig_hook(panic_info);
std::process::exit(1);
}));
}
#[cfg(feature = "deadlock-detector")]
fn start_deadlock_detector() {
std::thread::spawn(move || {
loop {
std::thread::sleep(std::time::Duration::from_secs(1));
for deadlock in parking_lot::deadlock::check_deadlock() {
for deadlock in deadlock {
eprintln!(
"Found a deadlock! {:?}: {:?}",
deadlock.thread_id(),
deadlock.backtrace()
);
}
}
}
});
}
fn open_browser(url: &str) {
if let Ok(browser_bin) = std::env::var("BROWSER")
&& let Ok(output) = Command::new(browser_bin).arg(url).output()
&& output.status.success()
{
return;
}
let _ = Command::new("xdg-open").arg(url).output();
}
fn ensure_root() {
if !Uid::current().is_root() {
error!("root permissions are required to run lightswitch");
std::process::exit(1);
}
}
fn ensure_btf(btf_custom_path: &Option<String>) {
if btf_custom_path.is_none() && !has_btf() {
error!("Could not find kernel BTF. One can be provided --btf-custom-path");
std::process::exit(1)
}
}
fn main() -> Result<(), Box<dyn Error>> {
#[cfg(feature = "kubernetes")]
{
rustls::crypto::aws_lc_rs::default_provider()
.install_default()
.expect("failed to install rustls crypto provider");
}
panic_thread_hook();
let args = CliArgs::parse();
#[cfg(feature = "deadlock-detector")]
if args.enable_deadlock_detector {
start_deadlock_detector();
}
let level_filter = match args.logging {
LoggingLevel::Trace => Level::TRACE,
LoggingLevel::Debug => Level::DEBUG,
LoggingLevel::Info => Level::INFO,
LoggingLevel::Warn => Level::WARN,
LoggingLevel::Error => Level::ERROR,
};
#[cfg(feature = "live-tui")]
let live = args.live;
#[cfg(not(feature = "live-tui"))]
let live = false;
if !live {
let subscriber = FmtSubscriber::builder()
.with_max_level(level_filter)
.with_span_events(FmtSpan::ENTER | FmtSpan::CLOSE)
.with_ansi(std::io::stdout().is_terminal())
.finish();
tracing::subscriber::set_global_default(subscriber)
.expect("setting default subscriber failed");
}
match args.command {
None => {} Some(Commands::ObjectInfo { path }) => {
show_object_file_info(&path);
return Ok(());
}
Some(Commands::ShowUnwind { path }) => {
show_unwind_info(&path)?;
return Ok(());
}
Some(Commands::SystemInfo) => {
ensure_root();
ensure_btf(&args.btf_custom_path);
show_system_info(&args.btf_custom_path);
return Ok(());
}
Some(Commands::Server {
profile,
format,
no_open,
}) => {
if nix::unistd::geteuid().is_root() {
let target_uid = Uid::from_raw(std::env::var("SUDO_UID")?.parse()?);
let target_gid = Gid::from_raw(std::env::var("SUDO_GID")?.parse()?);
let target_user = CString::new(std::env::var("SUDO_USER")?)?;
nix::unistd::initgroups(&target_user, target_gid)?;
nix::unistd::setgid(target_gid)?;
nix::unistd::setuid(target_uid)?;
}
let profile = match profile {
Some(profile) => profile,
None => find_default_profile(&PathBuf::new())?,
};
let format = match format {
Some(ServerProfileFormat::Firefox) => ProfileFileFormat::Firefox,
Some(ServerProfileFormat::Perfetto) => ProfileFileFormat::Perfetto,
None => ProfileFileFormat::from_path(&profile).ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"cannot infer profile format; use --format firefox or --format perfetto",
)
})?,
};
if !profile.is_file() {
return Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("profile not found: {}", profile.to_string_lossy()),
)
.into());
}
let port = format.default_port();
let url = format!("http://127.0.0.1:{port}");
println!("Serving {} at {url}", profile.to_string_lossy());
if !no_open {
open_browser(&url);
}
start_profile_server(port, profile, format)?;
return Ok(());
}
}
ensure_root();
ensure_btf(&args.btf_custom_path);
let Ok(system_info) = SystemInfo::new(args.btf_custom_path.clone()) else {
error!("Failed to detect system info!");
std::process::exit(1)
};
if !system_info.has_minimal_requirements() {
error!("Some start up requirements could not be met!");
error!("system_info = {:?}", system_info);
std::process::exit(1);
}
let killswitch = KillSwitch::new(args.killswitch_path_override, args.unsafe_start);
if killswitch.enabled() {
info!(
"Killswitch enabled, exiting. \
Override the killswitch using the --unsafe-start \
flag if you are sure you want to proceed."
);
return Ok(());
}
let server_url = args.server_url.unwrap_or(DEFAULT_SERVER_URL.into());
debug!("server url: {}, token: {:?}", server_url, args.token);
let metadata_provider: ThreadSafeGlobalMetadataProvider =
Arc::new(Mutex::new(GlobalMetadataProvider::default()));
#[cfg(feature = "kubernetes")]
if args.kubernetes {
let node_name = args
.node_name
.clone()
.expect("--node-name must be provided when --kubernetes is enabled");
let k8s_cache =
lightswitch_metadata::containers::k8s_metadata_cache::K8sMetadataCache::new(
node_name.clone(),
)
.expect("failed to create k8s metadata cache");
let k8s_provider =
lightswitch_metadata::containers::k8s_metadata_provider::K8sMetadataProvider::new(
k8s_cache,
);
metadata_provider
.lock()
.unwrap()
.register_task_metadata_providers(vec![Box::new(k8s_provider)]);
info!(
"Kubernetes metadata provider registered for node {}",
node_name
);
}
let token = args.token;
let debug_info_manager: Box<dyn DebugInfoManager + Send> = match args.debug_info_backend {
DebugInfoBackend::None => Box::new(DebugInfoBackendNull {}),
DebugInfoBackend::Copy => Box::new(DebugInfoBackendFilesystem {
path: PathBuf::from("/tmp"),
}),
DebugInfoBackend::Remote => Box::new(DebugInfoBackendRemote::new(
token.clone(),
server_url.clone(),
Duration::from_millis(500),
Duration::from_secs(15),
)?),
};
let use_ring_buffers =
!args.force_perf_buffer && system_info.available_bpf_features.has_ring_buf;
let profiler_config = ProfilerConfig {
cache_dir_base: args.cache_dir_base,
libbpf_debug: args.libbpf_debug,
bpf_logging: args.bpf_logging,
duration: args.duration,
sample_freq: args.sample_freq,
perf_buffer_bytes: args.perf_buffer_bytes,
mapsize_info: args.mapsize_info,
mapsize_rate_limits: args.mapsize_rate_limits,
exclude_self: args.exclude_self,
debug_info_manager,
max_native_unwind_info_size_mb: args.max_native_unwind_info_size_mb,
use_ring_buffers,
use_task_pt_regs_helper: system_info.available_bpf_features.has_task_pt_regs_helper
&& system_info.available_bpf_features.has_get_current_task_btf,
btf_custom_path: args.btf_custom_path,
no_prealloc_bpf_hash_maps: args.no_prealloc_bpf_hash_maps
&& system_info
.available_bpf_features
.has_non_prealloc_hash_maps_in_tracing,
userspace_pid_ns_level: system_info.available_bpf_features.userspace_pid_ns_level,
..Default::default()
};
let (stop_signal_sender, stop_signal_receive) = bounded(1);
let profiler_stop_signal_sender = stop_signal_sender.clone();
ctrlc::set_handler(move || {
info!("received Ctrl+C, stopping...");
let _ = profiler_stop_signal_sender.send(());
})
.expect("Error setting Ctrl-C handler");
let killswitch_ticker = tick(KILLSWITCH_POLL_INTERVAL);
let killswitch_poll_thread = thread::Builder::new().name("killswitch-poll-thread".to_string());
let _ = killswitch_poll_thread.spawn({
let stop_signal_sender = stop_signal_sender.clone();
move || {
loop {
if killswitch_ticker.recv().is_ok() && killswitch.enabled() {
info!("killswitch detected. Sending stop signal to profiler.");
let _ = stop_signal_sender.send(());
break;
}
}
}
});
#[cfg(feature = "live-tui")]
if live {
let (tx, rx) = mpsc::channel::<String>();
let collector: Arc<Mutex<Box<dyn Collector + Send>>> =
Arc::new(Mutex::new(Box::new(LiveCollector::new(tx))));
let profiler_config = ProfilerConfig {
duration: Duration::MAX,
..profiler_config
};
let tui_stop_signal_sender = stop_signal_sender.clone();
let mut p = Profiler::new(profiler_config, stop_signal_receive, metadata_provider);
p.profile_pids(args.pids);
let profiler_thread = thread::Builder::new()
.name("profiler-thread".to_string())
.spawn(move || {
p.run(collector);
})
.expect("Failed to spawn profiler thread");
flamelens::run_from_live_stream(rx, &format!("sampling at {} Hz", args.sample_freq))?;
let _ = tui_stop_signal_sender.send(());
let _ = profiler_thread.join();
return Ok(());
}
let profile_path = |default_name: &str| {
args.profile_path.clone().unwrap_or_default().join(
args.profile_name
.clone()
.unwrap_or_else(|| default_name.into()),
)
};
let collector: Arc<Mutex<Box<dyn Collector + Send>>> =
Arc::new(Mutex::new(match args.sender {
ProfileSender::None => Box::new(NullCollector::new()),
ProfileSender::LocalDisk => match args.profile_format {
ProfileFormat::Firefox => Box::new(FirefoxProfilerCollector::new(
profile_path(ProfileFileFormat::Firefox.default_profile_name()),
args.sample_freq,
metadata_provider.clone(),
)),
ProfileFormat::Perfetto => Box::new(PerfettoCollector::new(
profile_path(ProfileFileFormat::Perfetto.default_profile_name()),
args.sample_freq,
args.symbolizer == Symbolizer::Local,
)),
_ => Box::new(AggregatorCollector::new()),
},
ProfileSender::Remote => Box::new(StreamingCollector::new(
token,
args.symbolizer == Symbolizer::Local,
&server_url,
ProfilerConfig::default().session_duration,
args.sample_freq,
metadata_provider.clone(),
)),
ProfileSender::Pyroscope => Box::new(PyroscopeCollector::new(
args.symbolizer == Symbolizer::Local,
&server_url,
&args.pyroscope_app_name,
args.pyroscope_tenant_id.clone(),
ProfilerConfig::default().session_duration,
args.sample_freq,
metadata_provider.clone(),
#[cfg(feature = "kubernetes")]
args.node_name.clone().unwrap_or_default(),
#[cfg(not(feature = "kubernetes"))]
String::new(),
)),
}));
let mut p = Profiler::new(
profiler_config,
stop_signal_receive,
metadata_provider.clone(),
);
p.profile_pids(args.pids);
let profile_duration = p.run(collector.clone());
let collector = collector.lock().unwrap();
let (mut profile, procs, objs) = collector.finish();
match args.sender {
ProfileSender::Remote | ProfileSender::Pyroscope | ProfileSender::None => {
return Ok(());
}
_ => {}
}
if matches!(
&args.profile_format,
ProfileFormat::Firefox | ProfileFormat::Perfetto
) {
return Ok(());
}
if args.symbolizer == Symbolizer::Local {
info!("Symbolizing profile...");
profile = symbolize_profile(&profile, procs, objs);
}
let profile_path = args.profile_path.unwrap_or(PathBuf::from(""));
match args.profile_format {
ProfileFormat::FlameGraph => {
let folded = fold_profile(
procs,
profile,
args.flamegraph_aggregation == FlamegraphAggregation::Function,
);
let mut options: flamegraph::Options<'_> = flamegraph::Options::default();
let data = folded.as_bytes();
let profile_name = args.profile_name.unwrap_or_else(|| "flame.svg".into());
let profile_path = profile_path.join(profile_name);
let f = File::create(&profile_path).unwrap();
match flamegraph::from_reader(&mut options, data, f) {
Ok(_) => {
eprintln!(
"Flamegraph profile successfully written to {}",
profile_path.to_string_lossy()
);
}
Err(e) => {
error!("Failed to generate flamegraph: {:?}", e);
}
}
}
ProfileFormat::Pprof => {
let mut buffer = Vec::new();
let pprof_profile = to_pprof(
profile,
procs,
objs,
&metadata_provider,
profile_duration,
args.sample_freq,
);
pprof_profile.encode(&mut buffer).unwrap();
let profile_name = args.profile_name.unwrap_or_else(|| "profile.pb".into());
let profile_path = profile_path.join(profile_name);
let mut pprof_file = File::create(&profile_path).unwrap();
match pprof_file.write_all(&buffer) {
Ok(_) => {
eprintln!(
"Pprof profile successfully written to {}",
profile_path.to_string_lossy()
);
}
Err(e) => {
error!("Failed to generate pprof: {:?}", e);
}
}
}
ProfileFormat::Firefox | ProfileFormat::Perfetto => unreachable!("handled above"),
ProfileFormat::None => {}
}
Ok(())
}
fn show_unwind_info(path: &str) -> Result<(), Box<dyn Error>> {
let file = File::open(path)?;
let mmap = unsafe { memmap2::Mmap::map(&file)? };
let unwind_info = compact_unwind_info(&mmap, None)?;
for compact_row in unwind_info {
let pc = compact_row.pc;
let cfa_type = compact_row.cfa_type;
let rbp_type = compact_row.rbp_type;
let cfa_offset = compact_row.cfa_offset;
let rbp_offset = compact_row.rbp_offset;
println!(
"pc: {:x} cfa_type: {:<2} rbp_type: {:<2} cfa_offset: {:<4}
rbp_offset: {:<4}",
pc, cfa_type as u8, rbp_type as u8, cfa_offset, rbp_offset
);
}
Ok(())
}
fn show_system_info(btf_custom_path: &Option<String>) {
println!(
"- system info: {:#?}",
SystemInfo::new(btf_custom_path.clone())
);
println!("- kernel build id: {:?}", kernel_build_id());
if let Ok(aslr_offset) = kaslr_offset() {
println!("- kernel ASLR offset: 0x{aslr_offset:x}");
}
println!("- has kernel BTF: {}", has_btf());
}
fn show_object_file_info(path: &str) {
let object_file = MappedObjectFile::from_path(&PathBuf::from(path)).unwrap();
println!("- build id: {:?}", object_file.build_id());
if let Ok(executable_id) = object_file.build_id().id() {
println!("- executable id: 0x{executable_id}");
}
let unwind_info = CompactUnwindInfoBuilder::with_callback(object_file.bytes(), None, |_| {});
println!("- unwind info: {:?}", unwind_info.unwrap().process());
println!(
"- debug info (dwarf): {:?}",
object_file.object().has_debug_info()
);
println!(
"- go: {:?} {:?}",
object_file.object().is_go(),
object_file.object().go_stop_unwinding_frames()
);
println!("- dynamic: {:?}", object_file.object().is_dynamic());
println!("- 64 bits: {:?}", object_file.object().is_64());
println!(
"- load segments: {:?}",
object_file.object().elf_load_segments()
);
println!("- file id: {:?}", FileId::new(PathBuf::from(path)));
}
#[cfg(test)]
mod tests {
use super::*;
use assert_cmd::Command;
use clap::Parser;
use rstest::rstest;
#[test]
fn verify_cli() {
use clap::CommandFactory;
CliArgs::command().debug_assert()
}
#[test]
fn show_unwind_propagates_errors() {
let error = show_unwind_info("/path/that/does/not/exist").unwrap_err();
assert!(error.to_string().contains("No such file or directory"));
}
#[test]
fn cli_help() {
#[allow(deprecated)]
let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap();
cmd.arg("--help");
cmd.assert().success();
let actual = String::from_utf8(cmd.unwrap().stdout).unwrap();
insta::assert_snapshot!(actual, @"
Usage: lightswitch [OPTIONS] [COMMAND]
Commands:
object-info
show-unwind
system-info
server
help Print this message or the help of the given subcommand(s)
Options:
--pids <PIDS>
Specific PIDs to profile
-D, --duration <DURATION>
How long this agent will run in seconds
[default: 18446744073709551615]
--libbpf-debug
Enable libbpf logs. This includes the BPF verifier output
--bpf-logging
Enable BPF programs logging
--btf-custom-path <BTF_CUSTOM_PATH>
Alternative BTF file
--logging <LOGGING>
Set lightswitch's logging level
[default: info]
[possible values: trace, debug, info, warn, error]
--sample-freq <SAMPLE_FREQ_IN_HZ>
Per-CPU Sampling Frequency in Hz
[default: 19]
--profile-format <PROFILE_FORMAT>
Output file for Flame Graph in SVG format
[default: flame-graph]
[possible values: none, flame-graph, firefox, perfetto, pprof]
--flamegraph-aggregation <FLAMEGRAPH_AGGREGATION>
What information to show in the flamegraph. Won't do anything for other profile formats
[default: function]
[possible values: function, all]
--profile-path <PROFILE_PATH>
Path for the generated profile
--profile-name <PROFILE_NAME>
Name for the generated profile
--sender <SENDER>
Where to write the profile
Possible values:
- none: Discard the profile. Used for kernel tests
- local-disk
- remote
- pyroscope
[default: local-disk]
--server-url <SERVER_URL>
--token <TOKEN>
--pyroscope-app-name <PYROSCOPE_APP_NAME>
Application name for Pyroscope
[default: lightswitch]
--pyroscope-tenant-id <PYROSCOPE_TENANT_ID>
Tenant ID for multi-tenant Pyroscope (sets `X-Scope-OrgID` header)
--perf-buffer-bytes <PERF_BUFFER_BYTES>
Size of each profiler perf buffer, in bytes (must be a power of 2)
[default: 524288]
--mapsize-info
Print eBPF map sizes after creation
--mapsize-rate-limits <MAPSIZE_RATE_LIMITS>
max number of rate limit entries
[default: 5000]
--exclude-self
Do not profile the profiler (myself)
--symbolizer <SYMBOLIZER>
[default: local]
[possible values: local, none]
--debug-info-backend <DEBUG_INFO_BACKEND>
[default: none]
[possible values: none, copy, remote]
--max-native-unwind-info-size-mb <MAX_NATIVE_UNWIND_INFO_SIZE_MB>
approximate max size in megabytes used for the BPF maps that hold unwind information
[default: 2147483647]
--enable-deadlock-detector
enable parking_lot's deadlock detector
--cache-dir-base <CACHE_DIR_BASE>
[default: /tmp]
--killswitch-path-override <KILLSWITCH_PATH_OVERRIDE>
Override the default path to the killswitch file (/tmp/lightswitch/killswitch) which prevents the profiler from starting
--unsafe-start
Force the profiler to start even if the system killswitch is enabled
--force-perf-buffer
force perf buffers even if ring buffers can be used
--no-prealloc-bpf-hash-maps
Do not preallocate BPF hash maps
--preload-thread-metadata
Read metadata for all threads when a new process is detected. This might be slow in older kernels.
--live
Launch live flamegraph TUI
--kubernetes
Enable Kubernetes pod-level profiling
--node-name <NODE_NAME>
Node name for Kubernetes metadata (required with --kubernetes)
-h, --help
Print help (see a summary with '-h')
-V, --version
Print version
");
}
#[rstest]
#[case::prime_19("19", "")]
#[case::non_prime_20(
"20",
"Sample frequency 20 is not prime - use 19 (before) or 23 (after) instead"
)]
#[case::non_prime_out_of_range1010("20000", "sample frequency not in allowed range")]
#[trace]
fn sample_freq_successes(#[case] desired_freq: String, #[case] expected_msg: String) {
let execname = env!("CARGO_PKG_NAME");
let argname = "--sample-freq";
let baseargs = vec![execname, argname];
let mut myargs = baseargs.clone();
myargs.push(desired_freq.as_str());
let result = CliArgs::try_parse_from(myargs.iter());
match result {
Ok(config) => {
assert_eq!(config.sample_freq, desired_freq.parse::<u64>().unwrap());
}
Err(err) => {
let actual_message = err.to_string();
assert!(actual_message.contains(expected_msg.as_str()));
}
}
}
}