diff --git a/Makefile b/Makefile index 55e0f3885..768bcb6a2 100644 --- a/Makefile +++ b/Makefile @@ -1,13 +1,14 @@ # SPDX-License-Identifier: MPL-2.0 -# Global options. +# =========================== Makefile options. =============================== + +# Global build options. ARCH ?= x86_64 BENCHMARK ?= none BOOT_METHOD ?= grub-rescue-iso BOOT_PROTOCOL ?= multiboot2 BUILD_SYSCALL_TEST ?= 0 ENABLE_KVM ?= 1 -GDB_TCP_PORT ?= 1234 INTEL_TDX ?= 0 MEM ?= 8G RELEASE ?= 0 @@ -16,7 +17,14 @@ LOG_LEVEL ?= error SCHEME ?= "" SMP ?= 1 OSTD_TASK_STACK_SIZE_IN_PAGES ?= 64 -# End of global options. +# End of global build options. + +# GDB debugging and profiling options. +GDB_TCP_PORT ?= 1234 +GDB_PROFILE_FORMAT ?= folded +GDB_PROFILE_COUNT ?= 200 +GDB_PROFILE_INTERVAL ?= 0.1 +# End of GDB options. # The Makefile provides a way to run arbitrary tests in the kernel # mode using the kernel command line. @@ -26,6 +34,8 @@ EXTRA_BLOCKLISTS_DIRS ?= "" SYSCALL_TEST_DIR ?= /tmp # End of auto test features. +# ========================= End of Makefile options. ========================== + CARGO_OSDK := ~/.cargo/bin/cargo-osdk CARGO_OSDK_ARGS := --target-arch=$(ARCH) --kcmd-args="ostd.log_level=$(LOG_LEVEL)" @@ -189,11 +199,21 @@ endif .PHONY: gdb_server gdb_server: initramfs $(CARGO_OSDK) - @cargo osdk run $(CARGO_OSDK_ARGS) -G --vsc --gdb-server-addr :$(GDB_TCP_PORT) + @cargo osdk run $(CARGO_OSDK_ARGS) --gdb-server --gdb-wait-client --gdb-vsc \ + --gdb-server-addr :$(GDB_TCP_PORT) .PHONY: gdb_client gdb_client: $(CARGO_OSDK) - @cd kernel && cargo osdk debug $(CARGO_OSDK_ARGS) --remote :$(GDB_TCP_PORT) + @cargo osdk debug $(CARGO_OSDK_ARGS) --remote :$(GDB_TCP_PORT) + +.PHONY: profile_server +profile_server: initramfs $(CARGO_OSDK) + @cargo osdk run $(CARGO_OSDK_ARGS) --gdb-server --gdb-server-addr :$(GDB_TCP_PORT) + +.PHONY: profile_client +profile_client: $(CARGO_OSDK) + @cargo osdk profile $(CARGO_OSDK_ARGS) --remote :$(GDB_TCP_PORT) \ + --samples $(GDB_PROFILE_COUNT) --interval $(GDB_PROFILE_INTERVAL) --format $(GDB_PROFILE_FORMAT) .PHONY: test test: diff --git a/osdk/Cargo.lock b/osdk/Cargo.lock index 681d6b55b..0594daf56 100644 --- a/osdk/Cargo.lock +++ b/osdk/Cargo.lock @@ -35,6 +35,21 @@ version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "0.6.12" @@ -98,6 +113,12 @@ dependencies = [ "wait-timeout", ] +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + [[package]] name = "bitflags" version = "1.3.2" @@ -124,6 +145,12 @@ dependencies = [ "serde", ] +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + [[package]] name = "bytemuck" version = "1.17.0" @@ -149,9 +176,11 @@ name = "cargo-osdk" version = "0.8.3" dependencies = [ "assert_cmd", + "chrono", "clap", "env_logger", "indexmap", + "indicatif", "lazy_static", "linux-bzimage-builder", "log", @@ -166,12 +195,35 @@ dependencies = [ "toml", ] +[[package]] +name = "cc" +version = "1.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07b1695e2c7e8fc85310cde85aeaab7e3097f593c91d209d3f9df76c928100f0" +dependencies = [ + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets", +] + [[package]] name = "clap" version = "4.5.1" @@ -218,6 +270,25 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +[[package]] +name = "console" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "unicode-width", + "windows-sys", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "core2" version = "0.4.0" @@ -283,6 +354,12 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + [[package]] name = "env_filter" version = "0.1.0" @@ -344,6 +421,29 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "indexmap" version = "2.2.3" @@ -354,12 +454,43 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "indicatif" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "763a5a8f45087d6bcea4222e7b72c291a054edf80e4ef6efd2a4979878c7bea3" +dependencies = [ + "console", + "instant", + "number_prefix", + "portable-atomic", + "unicode-width", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + [[package]] name = "itoa" version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" +[[package]] +name = "js-sys" +version = "0.3.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" +dependencies = [ + "wasm-bindgen", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -419,12 +550,33 @@ version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + [[package]] name = "once_cell" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "portable-atomic" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d30538d42559de6b034bc76fd6dd4c38961b1ee5c6c56e3808c50128fdbc22ce" + [[package]] name = "predicates" version = "3.1.0" @@ -647,6 +799,12 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + [[package]] name = "utf8parse" version = "0.2.1" @@ -668,6 +826,70 @@ dependencies = [ "libc", ] +[[package]] +name = "wasm-bindgen" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.52.0" diff --git a/osdk/Cargo.toml b/osdk/Cargo.toml index 5001dabce..bc4943a01 100644 --- a/osdk/Cargo.toml +++ b/osdk/Cargo.toml @@ -19,8 +19,10 @@ version = "0.2.0" [dependencies] clap = { version = "4.4.17", features = ["cargo", "derive"] } +chrono = "0.4.38" env_logger = "0.11.0" indexmap = "2.2.1" +indicatif = "0.17.8" # For a commandline progress bar lazy_static = "1.4.0" log = "0.4.20" quote = "1.0.35" diff --git a/osdk/src/cli.rs b/osdk/src/cli.rs index ba052e362..f88abf31f 100644 --- a/osdk/src/cli.rs +++ b/osdk/src/cli.rs @@ -2,13 +2,13 @@ use std::path::PathBuf; -use clap::{crate_version, Args, Parser}; +use clap::{crate_version, Args, Parser, ValueEnum}; use crate::{ arch::Arch, commands::{ execute_build_command, execute_debug_command, execute_forwarded_command, - execute_new_command, execute_run_command, execute_test_command, + execute_new_command, execute_profile_command, execute_run_command, execute_test_command, }, config::{ manifest::{ProjectType, TomlManifest}, @@ -46,6 +46,12 @@ pub fn main() { debug_args, ); } + OsdkSubcommand::Profile(profile_args) => { + execute_profile_command( + &load_config(&profile_args.common_args).run.build.profile, + profile_args, + ); + } OsdkSubcommand::Test(test_args) => { execute_test_command(&load_config(&test_args.common_args), test_args); } @@ -79,6 +85,8 @@ pub enum OsdkSubcommand { Run(RunArgs), #[command(about = "Debug a remote target via GDB")] Debug(DebugArgs), + #[command(about = "Profile a remote GDB debug target to collect stack traces for flame graph")] + Profile(ProfileArgs), #[command(about = "Execute kernel mode unit test by starting a VMM")] Test(TestArgs), #[command(about = "Check a local package and all of its dependencies for errors")] @@ -168,19 +176,25 @@ pub struct RunArgs { #[derive(Debug, Args, Clone, Default)] pub struct GdbServerArgs { - /// Whether to enable QEMU GDB server for debugging #[arg( - long = "enable-gdb", - short = 'G', - help = "Enable QEMU GDB server for debugging", + long = "gdb-server", + help = "Enable the QEMU GDB server for debugging", default_value_t )] - pub is_gdb_enabled: bool, + pub enabled: bool, #[arg( - long = "vsc", + long = "gdb-wait-client", + help = "Let the QEMU GDB server wait for the GDB client before execution", + default_value_t, + requires = "enabled" + )] + pub wait_client: bool, + #[arg( + long = "gdb-vsc", help = "Generate a '.vscode/launch.json' for debugging with Visual Studio Code \ (only works when '--enable-gdb' is enabled)", - default_value_t + default_value_t, + requires = "enabled" )] pub vsc_launch_file: bool, #[arg( @@ -188,9 +202,10 @@ pub struct GdbServerArgs { help = "The network address on which the GDB server listens, \ it can be either a path for the UNIX domain socket or a TCP port on an IP address.", value_name = "ADDR", - default_value = ".aster-gdb-socket" + default_value = ".aster-gdb-socket", + requires = "enabled" )] - pub gdb_server_addr: String, + pub host_addr: String, } #[derive(Debug, Parser)] @@ -205,6 +220,120 @@ pub struct DebugArgs { pub common_args: CommonArgs, } +#[derive(Debug, Parser)] +pub struct ProfileArgs { + #[arg( + long, + help = "Specify the address of the remote target", + default_value = ".aster-gdb-socket" + )] + pub remote: String, + #[arg(long, help = "The number of samples to collect", default_value = "200")] + pub samples: usize, + #[arg( + long, + help = "The interval between samples in seconds", + default_value = "0.1" + )] + pub interval: f64, + #[arg( + long, + help = "Parse a collected JSON profile file into other formats", + value_name = "PATH", + conflicts_with = "samples", + conflicts_with = "interval" + )] + pub parse: Option, + #[command(flatten)] + pub out_args: DebugProfileOutArgs, + #[command(flatten)] + pub common_args: CommonArgs, +} + +#[derive(Clone, Copy, Debug, ValueEnum)] +pub enum ProfileFormat { + /// The raw stack trace log parsed from GDB in JSON + Json, + /// The folded stack trace for a + /// [flame graph](https://github.com/brendangregg/FlameGraph) + Folded, +} + +impl ProfileFormat { + pub fn file_extension(&self) -> &'static str { + match self { + ProfileFormat::Json => "json", + ProfileFormat::Folded => "folded", + } + } +} + +#[derive(Debug, Parser)] +pub struct DebugProfileOutArgs { + #[arg(long, help = "The output format for the profile data")] + format: Option, + #[arg( + long, + help = "The mask of the CPU to generate traces for in the output profile data", + default_value_t = u128::MAX + )] + pub cpu_mask: u128, + #[arg( + long, + help = "The path to the output profile data file", + value_name = "PATH" + )] + output: Option, +} + +impl DebugProfileOutArgs { + /// Get the output format for the profile data. + /// + /// If the user does not specify the format, it will be inferred from the + /// output file extension. If the output file does not have an extension, + /// the default format is folded stack traces. + pub fn format(&self) -> ProfileFormat { + self.format.unwrap_or_else(|| { + if self.output.is_some() { + match self.output.as_ref().unwrap().extension() { + Some(ext) if ext == "folded" => ProfileFormat::Folded, + Some(ext) if ext == "json" => ProfileFormat::Json, + _ => ProfileFormat::Folded, + } + } else { + ProfileFormat::Folded + } + }) + } + + /// Get the output path for the profile data. + /// + /// If the user does not specify the output path, it will be generated from + /// the current time stamp and the format. The caller can provide a hint + /// output path to the file to override the file name. + pub fn output_path(&self, hint: Option<&PathBuf>) -> PathBuf { + self.output.clone().unwrap_or_else(|| { + use chrono::{offset::Local, DateTime}; + let file_stem = if let Some(hint) = hint { + format!( + "{}", + hint.parent() + .unwrap() + .join(hint.file_stem().unwrap()) + .display() + ) + } else { + let crate_name = crate::util::get_current_crate_info().name; + let time_stamp = std::time::SystemTime::now(); + let time_stamp: DateTime = time_stamp.into(); + let time_stamp = time_stamp.format("%H%M%S"); + format!("{}-profile-{}", crate_name, time_stamp) + }; + PathBuf::from(format!("{}.{}", file_stem, self.format().file_extension())) + }) + } +} + #[derive(Debug, Parser)] pub struct TestArgs { #[arg( diff --git a/osdk/src/commands/mod.rs b/osdk/src/commands/mod.rs index 22f6b074c..875bf4a65 100644 --- a/osdk/src/commands/mod.rs +++ b/osdk/src/commands/mod.rs @@ -5,13 +5,14 @@ mod build; mod debug; mod new; +mod profile; mod run; mod test; mod util; pub use self::{ build::execute_build_command, debug::execute_debug_command, new::execute_new_command, - run::execute_run_command, test::execute_test_command, + profile::execute_profile_command, run::execute_run_command, test::execute_test_command, }; use crate::arch::get_default_arch; diff --git a/osdk/src/commands/profile.rs b/osdk/src/commands/profile.rs new file mode 100644 index 000000000..2e6bf00a7 --- /dev/null +++ b/osdk/src/commands/profile.rs @@ -0,0 +1,363 @@ +// SPDX-License-Identifier: MPL-2.0 + +//! OSDK profile command implementation. +//! +//! The profile command is used to collect stack traces when running the target +//! kernel in QEMU. It attaches to the GDB server initiated with [`super::run`] +//! and collects the stack trace periodically. The collected data can be +//! further analyzed using tools like +//! [flame graph](https://github.com/brendangregg/FlameGraph). + +use crate::{ + cli::{ProfileArgs, ProfileFormat}, + commands::util::bin_file_name, + util::{get_current_crate_info, get_target_directory}, +}; +use regex::Regex; +use std::{collections::HashMap, fs::File, io::Write, path::PathBuf, process::Command}; + +pub fn execute_profile_command(_profile: &str, args: &ProfileArgs) { + if let Some(parse_input) = &args.parse { + do_parse_stack_traces(parse_input, args); + } else { + do_collect_stack_traces(args); + } +} + +fn do_parse_stack_traces(target_file: &PathBuf, args: &ProfileArgs) { + let out_args = &args.out_args; + let in_file = File::open(target_file).expect("Failed to open input file"); + let profile: Profile = + serde_json::from_reader(in_file).expect("Failed to parse the input JSON file"); + let out_file = File::create(out_args.output_path(Some(target_file))) + .expect("Failed to create output file"); + + let out_format = out_args.format(); + if matches!(out_format, ProfileFormat::Json) { + println!("Warning: parsing JSON profile to the same format."); + return; + } + profile.serialize_to(out_format, out_args.cpu_mask, out_file); +} + +fn do_collect_stack_traces(args: &ProfileArgs) { + let file_path = get_target_directory() + .join("osdk") + .join(get_current_crate_info().name) + .join(bin_file_name()); + + let remote = &args.remote; + let samples = &args.samples; + let interval = &args.interval; + + let mut profile_buffer = ProfileBuffer::new(); + + println!("Profiling \"{}\" at \"{}\".", file_path.display(), remote); + use indicatif::{ProgressIterator, ProgressStyle}; + let style = ProgressStyle::default_bar().progress_chars("#>-"); + for _ in (0..*samples).progress_with_style(style) { + // Use GDB to halt the remote, get stack traces, and resume + let output = Command::new("gdb") + .args([ + "-batch", + "-ex", + "set pagination 0", + "-ex", + &format!("file {}", file_path.display()), + "-ex", + &format!("target remote {}", remote), + "-ex", + "thread apply all bt -frame-arguments presence -frame-info short-location", + ]) + .output() + .expect("Failed to execute gdb"); + + for line in String::from_utf8_lossy(&output.stdout).lines() { + profile_buffer.append_raw_line(line); + } + + // Sleep between samples + std::thread::sleep(std::time::Duration::from_secs_f64(*interval)); + } + + let out_args = &args.out_args; + let out_path = out_args.output_path(None); + println!( + "Profile data collected. Writing the output to \"{}\".", + out_path.display() + ); + + let out_file = File::create(out_path).expect("Failed to create output file"); + profile_buffer + .cur_profile + .serialize_to(out_args.format(), out_args.cpu_mask, out_file); +} + +#[derive(Debug, Default, Serialize, Deserialize)] +struct Profile { + // Index 0: capture; Index 1: CPU ID; Index 2: stack frame + stack_traces: Vec>>, +} + +impl Profile { + fn serialize_to(&self, format: ProfileFormat, cpu_mask: u128, mut target: W) { + match format { + ProfileFormat::Folded => { + let mut folded = HashMap::new(); + + // Process each stack trace and fold it for flame graph format + for capture in &self.stack_traces { + for (cpu_id, stack) in capture { + if *cpu_id >= 128 || cpu_mask & (1u128 << *cpu_id) == 0 { + continue; + } + + // Fold the stack trace + let folded_key = stack.iter().rev().cloned().collect::>().join(";"); + *folded.entry(folded_key).or_insert(0) += 1; + } + } + + // Write the folded traces + for (key, count) in folded { + writeln!(&mut target, "{} {}", key, count) + .expect("Failed to write folded output"); + } + } + ProfileFormat::Json => { + // Filter out the stack traces based on the CPU mask + let filtered_traces = self + .stack_traces + .iter() + .map(|capture| { + capture + .iter() + .filter(|(cpu_id, _)| { + **cpu_id < 128 && cpu_mask & (1u128 << **cpu_id) != 0 + }) + .map(|(cpu_id, stack)| (*cpu_id, stack.clone())) + .collect::>() + }) + .collect::>(); + + let filtered = Profile { + stack_traces: filtered_traces, + }; + + serde_json::to_writer(target, &filtered).expect("Failed to write JSON output"); + } + } + } +} + +#[derive(Debug)] +struct ProfileBuffer { + cur_profile: Profile, + // Pre-compile regex patterns for cleaning the input. + hex_in_pattern: Regex, + impl_pattern: Regex, + // The state + cur_cpu: Option, +} + +impl ProfileBuffer { + fn new() -> Self { + Self { + cur_profile: Profile::default(), + hex_in_pattern: Regex::new(r"0x[0-9a-f]+ in").unwrap(), + impl_pattern: Regex::new(r"::\{.*?\}").unwrap(), + cur_cpu: None, + } + } + + fn append_raw_line(&mut self, line: &str) { + // Lines starting with '#' are stack frames + if !line.starts_with('#') { + // Otherwise it may initiate a new capture or a new CPU stack trace + + // Check if this is a new CPU trace (starts with `Thread` and contains `CPU#N`) + if line.starts_with("Thread") { + let cpu_id_idx = line.find("CPU#").unwrap(); + let cpu_id = line[cpu_id_idx + 4..] + .split_whitespace() + .next() + .unwrap() + .parse::() + .unwrap(); + self.cur_cpu = Some(cpu_id); + + // if the new CPU id is already in the stack traces, start a new capture + match self.cur_profile.stack_traces.last() { + Some(capture) => { + if capture.contains_key(&cpu_id) { + self.cur_profile.stack_traces.push(HashMap::new()); + } + } + None => { + self.cur_profile.stack_traces.push(HashMap::new()); + } + } + } + + return; + } + + // Clean the input line + let mut processed = line.trim().to_string(); + + // Remove everything between angle brackets '<...>' + processed = Self::remove_generics(&processed); + + // Remove "::impl{}" and hex addresses + processed = self.impl_pattern.replace_all(&processed, "").to_string(); + processed = self.hex_in_pattern.replace_all(&processed, "").to_string(); + + // Remove unnecessary parts like "()" and "(...)" + processed = processed.replace("(...)", ""); + processed = processed.replace("()", ""); + + // Split the line by spaces and expect the second part to be the function name + let parts: Vec<&str> = processed.split_whitespace().collect(); + if parts.len() > 1 { + let func_name = parts[1].to_string(); + + // Append the function name to the latest stack trace + let current_capture = self.cur_profile.stack_traces.last_mut().unwrap(); + let cur_cpu = self.cur_cpu.unwrap(); + current_capture.entry(cur_cpu).or_default().push(func_name); + } + } + + fn remove_generics(line: &str) -> String { + let mut result = String::new(); + let mut bracket_depth = 0; + + for c in line.chars() { + match c { + '<' => bracket_depth += 1, + '>' => { + if bracket_depth > 0 { + bracket_depth -= 1; + } + } + _ => { + if bracket_depth == 0 { + result.push(c); + } + } + } + } + + result + } +} + +#[cfg(test)] +#[test] +fn test_profile_parse_raw() { + let test_case = r#" +0xffffffff880b0f6f in aster_nix::sched::priority_scheduler::{impl#4}::pick_next_current (self=0xffffffff88489808 <_ZN4ostd2mm14heap_allocator10HEAP_SPACE17h85a5340e6564f69dE.llvm.15305379556759765072+992480>) at src/sched/priority_scheduler.rs:156 +156 let next_entity = if !self.real_time_entities.is_empty() { + +Thread 2 (Thread 1.2 (CPU#1 [running])): +#0 ostd::sync::spin::SpinLock, ostd::sync::spin::PreemptDisabled>::acquire_lock, ostd::sync::spin::PreemptDisabled> (...) +#1 ostd::sync::spin::SpinLock, ostd::sync::spin::PreemptDisabled>::lock, ostd::sync::spin::PreemptDisabled> (...) +#2 aster_nix::sched::priority_scheduler::{impl#1}::local_mut_rq_with (...) +#3 0xffffffff8826b205 in ostd::task::scheduler::reschedule (...) +#4 ostd::task::scheduler::yield_now () +#5 0xffffffff880a92c5 in ostd::task::Task::yield_now () +#6 aster_nix::thread::Thread::yield_now () +#7 aster_nix::ap_init::ap_idle_thread () +#8 core::ops::function::Fn::call () +#9 0xffffffff880b341e in alloc::boxed::{impl#50}::call<(), (dyn core::ops::function::Fn<(), Output=()> + core::marker::Send + core::marker::Sync), alloc::alloc::Global> (...) +#10 aster_nix::thread::kernel_thread::create_new_kernel_task::{closure#0} () +#11 0xffffffff882a3ea8 in alloc::boxed::{impl#50}::call<(), (dyn core::ops::function::Fn<(), Output=()> + core::marker::Send + core::marker::Sync), alloc::alloc::Global> (...) +#12 ostd::task::{impl#2}::build::kernel_task_entry () +#13 0x0000000000000000 in ?? () + +Thread 1 (Thread 1.1 (CPU#0 [running])): +#0 aster_nix::sched::priority_scheduler::{impl#1}::local_mut_rq_with (...) +#1 0xffffffff8826b205 in ostd::task::scheduler::reschedule (...) +#2 ostd::task::scheduler::yield_now () +#3 0xffffffff880a92c5 in ostd::task::Task::yield_now () +#4 aster_nix::thread::Thread::yield_now () +#5 aster_nix::ap_init::ap_idle_thread () +#6 core::ops::function::Fn::call () +#7 0xffffffff880b341e in alloc::boxed::{impl#50}::call<(), (dyn core::ops::function::Fn<(), Output=()> + core::marker::Send + core::marker::Sync), alloc::alloc::Global> (...) +#8 aster_nix::thread::kernel_thread::create_new_kernel_task::{closure#0} () +#9 0xffffffff882a3ea8 in alloc::boxed::{impl#50}::call<(), (dyn core::ops::function::Fn<(), Output=()> + core::marker::Send + core::marker::Sync), alloc::alloc::Global> (...) +#10 ostd::task::{impl#2}::build::kernel_task_entry () +#11 0x0000000000000000 in ?? () +[Inferior 1 (process 1) detached] +0xffffffff880b0f6f in aster_nix::sched::priority_scheduler::{impl#4}::pick_next_current (self=0xffffffff88489808 <_ZN4ostd2mm14heap_allocator10HEAP_SPACE17h85a5340e6564f69dE.llvm.15305379556759765072+992480>) at src/sched/priority_scheduler.rs:156 +156 let next_entity = if !self.real_time_entities.is_empty() { + +Thread 2 (Thread 1.2 (CPU#1 [running])): +#0 0xffffffff880b0f6f in aster_nix::sched::priority_scheduler::{impl#4}::pick_next_current (...) +#1 0xffffffff8826b3e0 in ostd::task::scheduler::yield_now::{closure#0} (...) +#2 ostd::task::scheduler::reschedule::{closure#0} (...) +#3 0xffffffff880b0cff in aster_nix::sched::priority_scheduler::{impl#1}::local_mut_rq_with (...) +#4 0xffffffff8826b205 in ostd::task::scheduler::reschedule (...) +#5 ostd::task::scheduler::yield_now () +#6 0xffffffff880a92c5 in ostd::task::Task::yield_now () +#7 aster_nix::thread::Thread::yield_now () +#8 aster_nix::ap_init::ap_idle_thread () +#9 core::ops::function::Fn::call () +#10 0xffffffff880b341e in alloc::boxed::{impl#50}::call<(), (dyn core::ops::function::Fn<(), Output=()> + core::marker::Send + core::marker::Sync), alloc::alloc::Global> (...) +#11 aster_nix::thread::kernel_thread::create_new_kernel_task::{closure#0} () +#12 0xffffffff882a3ea8 in alloc::boxed::{impl#50}::call<(), (dyn core::ops::function::Fn<(), Output=()> + core::marker::Send + core::marker::Sync), alloc::alloc::Global> (...) +#13 ostd::task::{impl#2}::build::kernel_task_entry () +#14 0x0000000000000000 in ?? () + +Thread 1 (Thread 1.1 (CPU#0 [running])): +#0 ostd::arch::x86::interrupts_ack (...) +#1 0xffffffff8828d704 in ostd::trap::handler::call_irq_callback_functions (...) +#2 0xffffffff88268e48 in ostd::arch::x86::trap::trap_handler (...) +#3 0xffffffff88274db6 in __from_kernel () +#4 0x0000000000000001 in ?? () +#5 0x0000000000000001 in ?? () +#6 0x00000000000001c4 in ?? () +#7 0xffffffff882c8580 in ?? () +#8 0x0000000000000002 in ?? () +#9 0xffffffff88489808 in _ZN4ostd2mm14heap_allocator10HEAP_SPACE17h85a5340e6564f69dE.llvm.15305379556759765072 () +#10 0x0000000000000000 in ?? () +[Inferior 1 (process 1) detached] +"#; + + let mut buffer = ProfileBuffer::new(); + for line in test_case.lines() { + buffer.append_raw_line(line); + } + + let profile = &buffer.cur_profile; + assert_eq!(profile.stack_traces.len(), 2); + assert_eq!(profile.stack_traces[0].len(), 2); + assert_eq!(profile.stack_traces[1].len(), 2); + + let stack00 = profile.stack_traces[0].get(&0).unwrap(); + assert_eq!(stack00.len(), 12); + assert_eq!( + stack00[0], + "aster_nix::sched::priority_scheduler::local_mut_rq_with" + ); + assert_eq!(stack00[11], "??"); + + let stack01 = profile.stack_traces[0].get(&1).unwrap(); + assert_eq!(stack01.len(), 14); + assert_eq!(stack01[9], "alloc::boxed::call"); + + let stack10 = profile.stack_traces[1].get(&0).unwrap(); + assert_eq!(stack10.len(), 11); + assert_eq!( + stack10[9], + "_ZN4ostd2mm14heap_allocator10HEAP_SPACE17h85a5340e6564f69dE.llvm.15305379556759765072" + ); + + let stack11 = profile.stack_traces[1].get(&1).unwrap(); + assert_eq!(stack11.len(), 15); + assert_eq!( + stack11[0], + "aster_nix::sched::priority_scheduler::pick_next_current" + ); + assert_eq!(stack11[14], "??"); +} diff --git a/osdk/src/commands/run.rs b/osdk/src/commands/run.rs index e751956c8..af5adb60e 100644 --- a/osdk/src/commands/run.rs +++ b/osdk/src/commands/run.rs @@ -8,7 +8,7 @@ use crate::{ }; pub fn execute_run_command(config: &Config, gdb_server_args: &GdbServerArgs) { - if gdb_server_args.is_gdb_enabled { + if gdb_server_args.enabled { use std::env; env::set_var( "RUSTFLAGS", @@ -21,19 +21,19 @@ pub fn execute_run_command(config: &Config, gdb_server_args: &GdbServerArgs) { let target_name = get_current_crate_info().name; let mut config = config.clone(); - if gdb_server_args.is_gdb_enabled { + if gdb_server_args.enabled { let qemu_gdb_args = { - let gdb_stub_addr = gdb_server_args.gdb_server_addr.as_str(); + let gdb_stub_addr = gdb_server_args.host_addr.as_str(); match gdb::stub_type_of(gdb_stub_addr) { gdb::StubAddrType::Unix => { format!( - " -chardev socket,path={},server=on,wait=off,id=gdb0 -gdb chardev:gdb0 -S", + " -chardev socket,path={},server=on,wait=off,id=gdb0 -gdb chardev:gdb0", gdb_stub_addr ) } gdb::StubAddrType::Tcp => { format!( - " -gdb tcp:{} -S", + " -gdb tcp:{}", gdb::tcp_addr_util::format_tcp_addr(gdb_stub_addr) ) } @@ -41,6 +41,10 @@ pub fn execute_run_command(config: &Config, gdb_server_args: &GdbServerArgs) { }; config.run.qemu.args += &qemu_gdb_args; + if gdb_server_args.wait_client { + config.run.qemu.args += " -S"; + } + // Ensure debug info added when debugging in the release profile. if config.run.build.profile.contains("release") { config @@ -53,7 +57,7 @@ pub fn execute_run_command(config: &Config, gdb_server_args: &GdbServerArgs) { let _vsc_launch_file = gdb_server_args.vsc_launch_file.then(|| { vsc::check_gdb_config(gdb_server_args); let profile = super::util::profile_name_adapter(&config.run.build.profile); - vsc::VscLaunchConfig::new(profile, &gdb_server_args.gdb_server_addr) + vsc::VscLaunchConfig::new(profile, &gdb_server_args.host_addr) }); let default_bundle_directory = osdk_output_directory.join(target_name); @@ -205,7 +209,7 @@ mod vsc { use crate::{error::Errno, error_msg}; use std::process::exit; - if !args.is_gdb_enabled { + if !args.enabled { error_msg!( "No need for a VSCode launch file without launching GDB server,\ pass '-h' for help" @@ -214,7 +218,7 @@ mod vsc { } // check GDB server address - let gdb_stub_addr = args.gdb_server_addr.as_str(); + let gdb_stub_addr = args.host_addr.as_str(); if gdb_stub_addr.is_empty() { error_msg!("GDB server address is required to generate a VSCode launch file"); exit(Errno::ParseMetadata as _); diff --git a/osdk/tests/commands/run.rs b/osdk/tests/commands/run.rs index 693f68e9c..e4f996d5d 100644 --- a/osdk/tests/commands/run.rs +++ b/osdk/tests/commands/run.rs @@ -79,7 +79,13 @@ mod qemu_gdb_feature { path.to_string_lossy().to_string() }; - let mut instance = cargo_osdk(["run", "-G", "--gdb-server-addr", unix_socket.as_str()]); + let mut instance = cargo_osdk([ + "run", + "--gdb-server", + "--gdb-wait-client", + "--gdb-server-addr", + unix_socket.as_str(), + ]); instance.current_dir(&workspace.os_dir()); let sock = unix_socket.clone(); @@ -123,7 +129,14 @@ mod qemu_gdb_feature { let workspace = workspace::WorkSpace::new(WORKSPACE, kernel_name); let addr = ":50001"; - let mut instance = cargo_osdk(["run", "-G", "--vsc", "--gdb-server-addr", addr]); + let mut instance = cargo_osdk([ + "run", + "--gdb-server", + "--gdb-wait-client", + "--gdb-vsc", + "--gdb-server-addr", + addr, + ]); instance.current_dir(&workspace.os_dir()); let dir = workspace.os_dir();