From 0df18c7a711dc9fd892b171999903be8690b3512 Mon Sep 17 00:00:00 2001 From: Vladimir Vukicevic Date: Thu, 6 Jun 2024 12:54:42 -0700 Subject: [PATCH 01/36] Implement cross-platform Eventpipe CoreCLR event parsing --- Cargo.lock | 55 +- Cargo.toml | 2 +- eventpipe-rs/Cargo.toml | 13 + eventpipe-rs/examples/dump-nettrace.rs | 59 ++ eventpipe-rs/src/coreclr_enums.rs | 141 ++++ eventpipe-rs/src/coreclr_events.rs | 342 +++++++++ eventpipe-rs/src/helpers.rs | 29 + eventpipe-rs/src/lib.rs | 723 ++++++++++++++++++ fxprof-processed-profile/src/timestamp.rs | 4 + .../binaries/libsamply_mac_preload.dylib | Bin 119896 -> 120184 bytes .../libsamply_mac_preload_arm64.dylib | Bin 54360 -> 54648 bytes samply-mac-preload/src/lib.rs | 20 + samply/Cargo.toml | 1 + .../resources/libsamply_mac_preload.dylib.gz | Bin 12421 -> 12618 bytes samply/src/mac/process_launcher.rs | 9 + samply/src/mac/profiler.rs | 55 +- samply/src/mac/sampler.rs | 5 +- samply/src/mac/task_profiler.rs | 34 +- .../shared/coreclr/dotnet_trace_manager.rs | 285 +++++++ samply/src/shared/coreclr/eventpipe.rs | 103 +++ samply/src/shared/coreclr/events.rs | 121 +++ samply/src/shared/coreclr/markers.rs | 178 +++++ samply/src/shared/coreclr/mod.rs | 47 ++ samply/src/shared/coreclr/provider.rs | 95 +++ samply/src/shared/mod.rs | 1 + samply/src/shared/timestamp_converter.rs | 6 + samply/src/windows/coreclr.rs | 401 +++------- samply/src/windows/etw_coreclr.rs | 257 +++++++ samply/src/windows/etw_gecko.rs | 34 +- samply/src/windows/mod.rs | 1 + samply/src/windows/profile_context.rs | 19 + 31 files changed, 2721 insertions(+), 319 deletions(-) create mode 100644 eventpipe-rs/Cargo.toml create mode 100644 eventpipe-rs/examples/dump-nettrace.rs create mode 100644 eventpipe-rs/src/coreclr_enums.rs create mode 100644 eventpipe-rs/src/coreclr_events.rs create mode 100644 eventpipe-rs/src/helpers.rs create mode 100644 eventpipe-rs/src/lib.rs create mode 100644 samply/src/shared/coreclr/dotnet_trace_manager.rs create mode 100644 samply/src/shared/coreclr/eventpipe.rs create mode 100644 samply/src/shared/coreclr/events.rs create mode 100644 samply/src/shared/coreclr/markers.rs create mode 100644 samply/src/shared/coreclr/mod.rs create mode 100644 samply/src/shared/coreclr/provider.rs create mode 100644 samply/src/windows/etw_coreclr.rs diff --git a/Cargo.lock b/Cargo.lock index 0c8c3b30d..b8c2edd82 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -118,6 +118,12 @@ version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +[[package]] +name = "array-init" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d62b7694a562cdf5a74227903507c56ab2cc8bdd1f781ed5cb4cf9c9f810bfc" + [[package]] name = "arrayvec" version = "0.7.4" @@ -198,6 +204,30 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597bb81c80a54b6a4381b23faba8d7774b144c94cbd1d6fe3f1329bd776554ab" +[[package]] +name = "binrw" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173901312e9850391d4d7c1318c4e099fdc037d61870fca427429830efdb4e5f" +dependencies = [ + "array-init", + "binrw_derive", + "bytemuck", +] + +[[package]] +name = "binrw_derive" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb515fdd6f8d3a357c8e19b8ec59ef53880807864329b1cb1cba5c53bf76557e" +dependencies = [ + "either", + "owo-colors", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -272,6 +302,12 @@ version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +[[package]] +name = "bytemuck" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78834c15cb5d5efe3452d58b1e8ba890dd62d21907f867f383358198e56ebca5" + [[package]] name = "byteorder" version = "1.5.0" @@ -601,6 +637,16 @@ dependencies = [ "windows", ] +[[package]] +name = "eventpipe" +version = "0.1.0" +dependencies = [ + "binrw", + "bitflags 2.5.0", + "num-derive", + "num-traits", +] + [[package]] name = "fallible-iterator" version = "0.2.0" @@ -1377,6 +1423,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "owo-colors" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" + [[package]] name = "parking_lot" version = "0.12.2" @@ -2369,7 +2421,7 @@ dependencies = [ [[package]] name = "usamply" -version = "0.12.7" +version = "0.12.8" dependencies = [ "bitflags 2.5.0", "byteorder", @@ -2381,6 +2433,7 @@ dependencies = [ "dirs", "env_logger", "etw-reader", + "eventpipe", "flate2", "framehop", "fs4", diff --git a/Cargo.toml b/Cargo.toml index 77d3cb006..88ae4d042 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ members = [ "tools/benchmarks", "tools/dump_table", "tools/query_api" -] +, "eventpipe-rs"] exclude = ["etw-reader"] # Should not be compiled on non-Windows # Config for 'cargo dist' diff --git a/eventpipe-rs/Cargo.toml b/eventpipe-rs/Cargo.toml new file mode 100644 index 000000000..43eb1a3cd --- /dev/null +++ b/eventpipe-rs/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "eventpipe" +version = "0.1.0" +edition = "2021" + +[dependencies] +binrw = "0.13.3" +bitflags = "2.4.2" +num-traits = "0.2" +num-derive = "0.4" + +[[example]] +name = "dump-nettrace" \ No newline at end of file diff --git a/eventpipe-rs/examples/dump-nettrace.rs b/eventpipe-rs/examples/dump-nettrace.rs new file mode 100644 index 000000000..eb83b92aa --- /dev/null +++ b/eventpipe-rs/examples/dump-nettrace.rs @@ -0,0 +1,59 @@ +#![allow(unused)] +use std::fs::File; + +use coreclr_events::CoreClrEvent; +use eventpipe::*; + +// https://github.com/microsoft/perfview/blob/main/src/TraceEvent/EventPipe/EventPipeFormat.md + +fn main() { + // open file as binary, argv[1] + let mut file = File::open(std::env::args().nth(1).unwrap()).unwrap(); + + let mut reader = EventPipeParser::new(file).expect("Failed to make EventPipeParser"); + + loop { + match reader.next_event() { + Ok(Some(event)) => { + if event.provider_name == "Microsoft-DotNETCore-SampleProfiler" { + continue; + } + + match eventpipe::decode_event(&event) { + DecodedEvent::CoreClrEvent(coreclr_event) => { + match coreclr_event { + CoreClrEvent::MethodLoad(event) => { + println!( + "MethodLoad: 0x{:16x} -- {}.{}", + event.method_start_address, + event.method_namespace, + event.method_name + ); + } + CoreClrEvent::GcTriggered(event) => { + println!("GcTriggered: {:?}", event.reason); + } + CoreClrEvent::GcAllocationTick(event) => { + //println!("GcAllocationTick: {:?}", event); + } + } + } + DecodedEvent::UnknownEvent => { + //println!("Unknown event: {} / {}", event.provider_name, event.event_id); + } + } + + //println!("{} -- ({} {}){:?}", name, event); + //println!("{} -- ({} {})", name, event.provider_name, event.event_id); + } + Ok(None) => { + println!("EOF"); + break; + } + Err(e) => { + println!("Error: {:?}", e); + break; + } + } + } +} diff --git a/eventpipe-rs/src/coreclr_enums.rs b/eventpipe-rs/src/coreclr_enums.rs new file mode 100644 index 000000000..cb51d7469 --- /dev/null +++ b/eventpipe-rs/src/coreclr_enums.rs @@ -0,0 +1,141 @@ +use bitflags::bitflags; + +use std::fmt::Display; + +use binrw::{BinRead, BinReaderExt, NullWideString}; +use num_derive::{FromPrimitive, ToPrimitive}; + +#[derive(BinRead, Debug, FromPrimitive, Clone, Copy)] +#[br(repr = u32)] +pub enum GcReason { + AllocSmall = 0, + Induced = 1, + LowMemory = 2, + Empty = 3, + AllocLargeObjectHeap = 4, + OutOfSpaceSmallObjectHeap = 5, + OutOfSpaceLargeObjectHeap = 6, + InducedNotForced = 7, + Stress = 8, + InducedLowMemory = 9, +} + +impl Display for GcReason { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + GcReason::AllocSmall => f.write_str("Small object heap allocation"), + GcReason::Induced => f.write_str("Induced"), + GcReason::LowMemory => f.write_str("Low memory"), + GcReason::Empty => f.write_str("Empty"), + GcReason::AllocLargeObjectHeap => f.write_str("Large object heap allocation"), + GcReason::OutOfSpaceSmallObjectHeap => { + f.write_str("Out of space (for small object heap)") + } + GcReason::OutOfSpaceLargeObjectHeap => { + f.write_str("Out of space (for large object heap)") + } + GcReason::InducedNotForced => f.write_str("Induced but not forced as blocking"), + GcReason::Stress => f.write_str("Stress"), + GcReason::InducedLowMemory => f.write_str("Induced low memory"), + } + } +} + +#[derive(BinRead, Debug, FromPrimitive, Clone, Copy)] +#[br(repr = u32)] +pub enum GcAllocationKind { + Small = 0, + Large = 1, + Pinned = 2, +} + +impl Display for GcAllocationKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + GcAllocationKind::Small => f.write_str("Small"), + GcAllocationKind::Large => f.write_str("Large"), + GcAllocationKind::Pinned => f.write_str("Pinned"), + } + } +} + +#[derive(BinRead, Debug, FromPrimitive, Clone, Copy)] +#[br(repr = u32)] +pub enum GcType { + Blocking = 0, + Background = 1, + BlockingDuringBackground = 2, +} + +impl Display for GcType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + GcType::Blocking => f.write_str("Blocking GC"), + GcType::Background => f.write_str("Background GC"), + GcType::BlockingDuringBackground => f.write_str("Blocking GC during background GC"), + } + } +} + +#[derive(BinRead, Debug, FromPrimitive, Clone, Copy)] +#[br(repr = u32)] +pub enum GcSuspendEeReason { + Other = 0, + GC = 1, + AppDomainShutdown = 2, + CodePitching = 3, + Shutdown = 4, + Debugger = 5, + GcPrep = 6, + DebuggerSweep = 7, +} + +impl Display for GcSuspendEeReason { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + GcSuspendEeReason::Other => f.write_str("Other"), + GcSuspendEeReason::GC => f.write_str("GC"), + GcSuspendEeReason::AppDomainShutdown => f.write_str("AppDomain shutdown"), + GcSuspendEeReason::CodePitching => f.write_str("Code pitching"), + GcSuspendEeReason::Shutdown => f.write_str("Shutdown"), + GcSuspendEeReason::Debugger => f.write_str("Debugger"), + GcSuspendEeReason::GcPrep => f.write_str("GC prep"), + GcSuspendEeReason::DebuggerSweep => f.write_str("Debugger sweep"), + } + } +} + +bitflags! { + #[derive(PartialEq, Eq)] + pub struct CoreClrMethodFlags: u32 { + const dynamic = 0x1; + const generic = 0x2; + const has_shared_generic_code = 0x4; + const jitted = 0x8; + const jit_helper = 0x10; + const profiler_rejected_precompiled_code = 0x20; + const ready_to_run_rejected_precompiled_code = 0x40; + + // next three bits are the tiered compilation level + const opttier_bit0 = 0x80; + const opttier_bit1 = 0x100; + const opttier_bit2 = 0x200; + + // extent flags/value (hot/cold) + const extent_bit_0 = 0x10000000; // 0x1 == cold, 0x0 = hot + const extent_bit_1 = 0x20000000; // always 0 for now looks like + const extent_bit_2 = 0x40000000; + const extent_bit_3 = 0x80000000; + + const _ = !0; + } + + #[derive(PartialEq, Eq)] + pub struct TieredCompilationSettings: u32 { + const none = 0x0; + const quick_jit = 0x1; + const quick_jit_for_loops = 0x2; + const tiered_pgo = 0x4; + const ready_to_run = 0x8; + } +} diff --git a/eventpipe-rs/src/coreclr_events.rs b/eventpipe-rs/src/coreclr_events.rs new file mode 100644 index 000000000..090527ed8 --- /dev/null +++ b/eventpipe-rs/src/coreclr_events.rs @@ -0,0 +1,342 @@ +// CoreCLR needs from ETW: +// +// DOTNetProvider: +// +// CLRMethod | CLRMethodRundown +// - MethodLoad_V1 (136), MethodUnLoad_V1 (137), MethodDCStart_V1 (137 -- Rundown), MethodDCEnd_V1 (138 -- Rundown) +// - MethodLoadVerbose_V1 (143) | MethodDCStartVerbose_V1 (141) +// - MethodDCEndVerbose (144) +// - ModuleLoad_V2 (152) | ModuleUnload_V2 (153) | ModuleDCStart_V2 (153) | ModuleDCEnd_V2 (154) +// - DomainModuleLoad_V1 (151) | DomainModuleDCStart_V1 (151 - rundown) | DomainModuleDCEnd_V1 (152 - rundown) +// Type +// - BulkType +// CLRStack (doesn't exist) +// - CLRStackWalk +// GarbageCollection +// - GCAllocationTick_V2 (10) +// - GCSampledObjectAllocation +// - Triggered +// - GCSuspendEE (9) +// - GCSuspendEEEnd (8) +// - GCRestartEEBegin (7) +// - GCRestartEEEnd (3) +// - "win:Start" - GCStart_V1 - 1 +// - "win:Stop" - GCEnd_V1 - 2 +// - SetGCHandle +// - DestroyGCHandle +// - GCFinalizersBegin (14) | GCFinalizersEnd (13) | FinalizeObject +// - GCCreateSegment (5) | GCFreeSegment (6) | GCDynamicEvent | GCHeapStats (4) +// XXX AppDomains +// - AppDomainLoad_V1 (156) | AppDomainUnLoad_V1 (157) +// - AppDomainDCStart_V1 (157 -- rundown) | AppDomainDCEnd_V1 (158 -- rundown) +// +// don't need: +// +// CLRRuntimeInformation +// CLRLoader + +use bitflags::bitflags; + +use std::{ + fmt::Display, + io::{Cursor, Read, Seek}, +}; + +use crate::coreclr_enums::*; +use crate::{MetadataDefinition, NettraceEvent}; +use binrw::{BinRead, BinReaderExt, NullWideString}; +use num_derive::{FromPrimitive, ToPrimitive}; + +#[derive(BinRead, Debug)] +#[br(little, import { version: u32, app_domain: bool })] +pub struct ModuleLoadUnloadEvent { + pub module_id: u64, + pub assembly_id: u64, + #[br(if(app_domain))] + pub app_domain_id: Option, + pub module_flags: u32, + pub _reserved1: u32, + pub module_il_path: NullWideString, + pub module_native_path: NullWideString, + #[br(if(version >= 1))] + pub clr_instance_id: Option, + #[br(if(version >= 2))] + pub managed_pdb_signature: [u8; 16], + #[br(if(version >= 2))] + pub managed_pdb_age: u32, + #[br(if(version >= 2))] + pub managed_pdb_build_path: NullWideString, + #[br(if(version >= 2))] + pub native_pdb_signature: [u8; 16], + #[br(if(version >= 2))] + pub native_pdb_age: u32, + #[br(if(version >= 2))] + pub native_pdb_build_path: NullWideString, +} + +#[derive(BinRead, Debug)] +#[br(little, import { version: u32, verbose: bool })] +pub struct MethodLoadUnloadEvent { + pub method_id: u64, + pub module_id: u64, + pub method_start_address: u64, + pub method_size: u32, + pub method_token: u32, + pub method_flags: u32, + + #[br(if(verbose))] + pub method_namespace: NullWideString, + + #[br(if(verbose))] + pub method_name: NullWideString, + + #[br(if(verbose))] + pub method_signature: NullWideString, + + #[br(if(version >= 1, None))] + pub clr_instance_id: Option, + + #[br(if(version >= 2, None))] + pub re_jit_id: Option, +} + +#[derive(BinRead, Debug)] +#[br(little, import { version: u32 })] +pub struct GcTriggeredEvent { + pub reason: GcReason, + pub clr_instance_id: u16, +} + +#[derive(BinRead, Debug)] +#[br(little, import { version: u32 })] +pub struct GcStartEvent { + pub count: u32, + #[br(if(version >= 1, None))] + pub depth: Option, + pub reason: GcReason, + #[br(if(version >= 1, None))] + pub gc_type: Option, + #[br(if(version >= 1, None))] + pub clr_instance_id: Option, + #[br(if(version >= 2, None))] + pub client_sequence_number: Option, +} + +#[derive(BinRead, Debug)] +#[br(little, import { version: u32 })] +pub struct GcEndEvent { + pub count: u32, + pub depth: u32, + #[br(if(version >= 1, None))] + pub reason: Option, +} + +#[derive(BinRead, Debug)] +#[br(little, import { version: u32 })] +pub struct GcAllocationTickEvent { + pub allocation_amount: u32, + pub allocation_kind: GcAllocationKind, + pub clr_instance_id: u16, + #[br(if(version >= 2))] + pub allocation_amount64: u64, + #[br(if(version >= 2))] + pub type_id: u64, // pointer + #[br(if(version >= 2))] + pub type_name: NullWideString, + #[br(if(version >= 2))] + pub heap_index: u32, + #[br(if(version >= 3))] + pub address: Option, // pointer + #[br(if(version >= 4))] + pub object_size: Option, +} + +#[derive(BinRead, Debug)] +#[br(little, import { version: u32 })] +pub struct GcSampledObjectAllocationEvent { + pub address: u64, // pointer + pub type_id: u64, // pointer + pub object_count_for_type_sample: u32, + pub total_size_for_type_sample: u64, + pub clr_instance_id: u16, +} + +#[derive(BinRead, Debug)] +#[br(little, import { version: u32 })] +pub struct ReadyToRunGetEntryPointEvent { + pub method_id: u64, + pub method_namespace: NullWideString, + pub method_name: NullWideString, + pub method_signature: NullWideString, + pub entry_point: u64, + pub clr_instance_id: u16, +} + +pub enum CoreClrEvent { + ModuleLoad(ModuleLoadUnloadEvent), + ModuleUnload(ModuleLoadUnloadEvent), + MethodLoad(MethodLoadUnloadEvent), + MethodUnload(MethodLoadUnloadEvent), + GcTriggered(GcTriggeredEvent), + GcAllocationTick(GcAllocationTickEvent), + GcSampledObjectAllocation(GcSampledObjectAllocationEvent), + ReadyToRunGetEntryPoint(ReadyToRunGetEntryPointEvent), +} + +pub fn decode_coreclr_event(event: &NettraceEvent) -> Option { + if event.provider_name != "Microsoft-Windows-DotNETRuntime" + && event.provider_name != "Microsoft-Windows-DotNETRuntimeRundown" + { + return None; + } + + let is_rundown = event.provider_name.ends_with("Rundown"); + match event.provider_name.as_str() { + "Microsoft-Windows-DotNETRuntime" => {} + //"Microsoft-Windows-DotNETRuntimeRundown" => decode_coreclr_event(event, payload), + _ => return None, + } + + let mut payload = Cursor::new(&event.payload); + + match event.event_id { + // 151: DomainModuleLoad + // 152: ModuleLoad + // 153: ModuleUnload + 151 => { + // DomainModuleLoad + let ev = ModuleLoadUnloadEvent::read_le_args( + &mut payload, + binrw::args! { version: event.event_version, app_domain: true }, + ) + .unwrap(); + Some(CoreClrEvent::ModuleLoad(ev)) + } + 152 => { + // ModuleLoad + let ev = ModuleLoadUnloadEvent::read_le_args( + &mut payload, + binrw::args! { version: event.event_version, app_domain: false }, + ) + .unwrap(); + Some(CoreClrEvent::ModuleLoad(ev)) + } + 153 => { + // ModuleUnload + let ev = ModuleLoadUnloadEvent::read_le_args( + &mut payload, + binrw::args! { version: event.event_version, app_domain: false }, + ) + .unwrap(); + Some(CoreClrEvent::ModuleUnload(ev)) + } + 159 => { + // R2RGetEntryPoint + let ev = ReadyToRunGetEntryPointEvent::read_le_args( + &mut payload, + binrw::args! { version: event.event_version }, + ) + .unwrap(); + Some(CoreClrEvent::ReadyToRunGetEntryPoint(ev)) + } + 141 => { + // MethodLoad + let ev = MethodLoadUnloadEvent::read_le_args( + &mut payload, + binrw::args! { version: event.event_version, verbose: false }, + ) + .unwrap(); + Some(CoreClrEvent::MethodLoad(ev)) + } + 142 => { + // MethodUnload + let ev = MethodLoadUnloadEvent::read_le_args( + &mut payload, + binrw::args! { version: event.event_version, verbose: false }, + ) + .unwrap(); + Some(CoreClrEvent::MethodUnload(ev)) + } + 143 => { + // MethodLoadVerbose + let ev = MethodLoadUnloadEvent::read_le_args( + &mut payload, + binrw::args! { version: event.event_version, verbose: true }, + ) + .unwrap(); + Some(CoreClrEvent::MethodLoad(ev)) + } + 144 => { + // MethodUnloadVerbose + let ev = MethodLoadUnloadEvent::read_le_args( + &mut payload, + binrw::args! { version: event.event_version, verbose: true }, + ) + .unwrap(); + Some(CoreClrEvent::MethodUnload(ev)) + } + 35 => { + // GCTriggered + let ev = GcTriggeredEvent::read_le_args( + &mut payload, + binrw::args! { version: event.event_version }, + ) + .unwrap(); + Some(CoreClrEvent::GcTriggered(ev)) + } + 1 => { + // GCStart + None + } + 2 => { + // GCStop + None + } + 3 => { + // GCRestartEEEnd + None + } + 7 => { + // GCRestartEEBegin + None + } + 8 => { + // GCSuspendEEEnd + None + } + 9 => { + // GCSuspendEEBegin + None + } + 10 => { + let ev = GcAllocationTickEvent::read_le_args( + &mut payload, + binrw::args! { version: event.event_version }, + ) + .unwrap(); + Some(CoreClrEvent::GcAllocationTick(ev)) + } + 20 | 30 => { + // High | Low, do we really need both of them? + let ev = GcSampledObjectAllocationEvent::read_le_args( + &mut payload, + binrw::args! { version: event.event_version }, + ) + .unwrap(); + Some(CoreClrEvent::GcSampledObjectAllocation(ev)) + } + // 13: GCFinalizersEnd + // 14: GCFinalizersBegin + // 82: CLRStackWalk (we should never see this I don't think) + // 83: AppDomainMemAllocated + // 84: AppDomainMemSurvived + // 85: ThreadCreated + // 86: ThreadTerminated + // 87: ThreadDomainEnter + // 154: AssemblyLoad + // 155: AssemblyUnload + // 156: AppDomainLoad + // 157: AppDomainUnload + _ => None, + } +} diff --git a/eventpipe-rs/src/helpers.rs b/eventpipe-rs/src/helpers.rs new file mode 100644 index 000000000..e59170d57 --- /dev/null +++ b/eventpipe-rs/src/helpers.rs @@ -0,0 +1,29 @@ +use std::io::{Read, Seek}; + +use binrw::{BinReaderExt, BinResult}; + +pub fn parse_varint_u64(reader: &mut R) -> BinResult { + let mut result = 0; + let mut shift = 0; + loop { + let byte: u8 = reader.read_le()?; + result |= ((byte & 0x7f) as u64) << shift; + if byte & 0x80 == 0 { + break; + } + shift += 7; + } + Ok(result) +} + +pub fn parse_varint_u32(reader: &mut R) -> BinResult { + parse_varint_u64(reader).map(|x| x as u32) +} + +pub fn parse_varint_i64(reader: &mut R) -> BinResult { + parse_varint_u64(reader).map(|x| unsafe { std::mem::transmute(x) }) +} + +pub fn parse_varint_i32(reader: &mut R) -> BinResult { + parse_varint_u64(reader).map(|x| unsafe { std::mem::transmute(x as u32) }) +} diff --git a/eventpipe-rs/src/lib.rs b/eventpipe-rs/src/lib.rs new file mode 100644 index 000000000..297c0cc67 --- /dev/null +++ b/eventpipe-rs/src/lib.rs @@ -0,0 +1,723 @@ +#![allow(unused)] +mod helpers; + +mod coreclr_enums; +mod coreclr_events; + +pub mod coreclr { + pub use super::coreclr_enums::*; + pub use super::coreclr_events::*; +} + +use binrw::io::TakeSeekExt; +use binrw::{binrw, BinRead, BinReaderExt, BinResult, NullWideString}; +use std::collections::HashMap; +use std::fmt::Display; +use std::fs::File; +use std::io::{BufRead, Cursor, Read, Seek, SeekFrom}; +use std::mem; + +use helpers::*; + +// https://github.com/microsoft/perfview/blob/main/src/TraceEvent/EventPipe/EventPipeFormat.md + +#[derive(BinRead)] +#[br(little)] +pub struct NettraceString { + length: u32, + + #[br(count = length)] + bytes: Vec, +} + +impl NettraceString { + fn as_str(&self) -> &str { + std::str::from_utf8(&self.bytes).unwrap() + } +} + +impl std::fmt::Debug for NettraceString { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "\"{}\"", self.as_str()) + } +} + +impl std::fmt::Display for NettraceString { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "\"{}\"", self.as_str()) + } +} + +#[derive(BinRead, Debug)] +#[br(little, magic = b"Nettrace")] +pub struct NettraceHeader { + ident: NettraceString, +} + +#[derive(BinRead, Debug, Eq, PartialEq)] +#[br(repr(u8))] +pub enum NettraceTag { + Invalid = 0, + NullReference = 1, + BeginPrivateObject = 5, + EndObject = 6, +} + +// Type objects have a NullReference tag as their type; every object will start with a type object, +// so pull it out into a separate struct instead of as part of the enum +#[derive(BinRead, Debug)] +#[br(little, magic = b"\x05\x01")] +pub struct NettraceTypeObject { + version: u32, + minimum_reader_version: u32, + type_name: NettraceString, + end_object: NettraceTag, +} + +#[derive(BinRead, Debug, Clone, Copy)] +#[br(little)] +pub struct NettraceTime { + year: u16, + month: u16, + day_of_week: u16, + day: u16, + hour: u16, + minute: u16, + second: u16, + millisecond: u16, +} + +#[derive(BinRead, Debug, Clone, Copy)] +#[br(little)] +pub struct NettraceTraceObject { + sync_time_utc: NettraceTime, + sync_time_qpc: u64, + qpc_frequency: u64, + pointer_suze: u32, + process_id: u32, + number_of_processors: u32, + expected_cpu_sampling_rate: u32, +} + +#[derive(BinRead, Debug)] +#[br(little)] +pub struct NettraceEventBlockHeader { + size: u16, + flags: u16, + min_timestamp: u64, + #[br(pad_after = size - 20)] + max_timestamp: u64, +} + +#[derive(BinRead, Debug)] +#[br(little)] +pub struct NettraceEventBlock { + #[br(align_after = 4)] + size: u32, + + header: NettraceEventBlockHeader, +} + +#[derive(BinRead, Debug)] +#[br(little)] +pub struct StackStack { + size: u32, + + // TODO -- support 32-bit here + #[br(count = size / 8)] + stack: Vec, +} + +#[derive(BinRead, Debug)] +#[br(little)] +pub struct StackBlock { + #[br(align_after = 4)] + size: u32, + + first_id: u32, + count: u32, + + #[br(count = count)] + stacks: Vec, +} + +#[derive(BinRead, Debug)] +#[br(little)] +pub struct ThreadSequenceNumber { + thread_id: u64, + sequence_number: u32, +} + +#[derive(BinRead, Debug)] +#[br(little)] +pub struct SequencePointBlock { + #[br(align_after = 4)] + size: u32, + + timestamp: u64, + thread_count: u32, + + #[br(count = thread_count)] + thread_sequence_numbers: Vec, +} + +// This header can be either compressed or uncompressed. +// The uncompressed header can be parsed directly; the compressed +// header needs manual parsing (see parse_compressed_header below). +#[derive(BinRead, Debug, Default, Clone)] +#[br(little)] +pub struct EventBlobHeader { + size: u32, + raw_metadata_id: u32, // high bit is "IsSorted" flag + sequence_number: u32, + thread_id: u64, + capture_thread_id: u64, + processor_number: u32, + stack_id: u32, + timestamp: u64, + activity_id: [u8; 16], + related_activity_id: [u8; 16], + payload_size: u32, + + // at the end to not screw up alignment + #[br(calc = raw_metadata_id & 0x7fffffff)] + metadata_id: u32, + #[br(calc = raw_metadata_id & 0x80000000 != 0)] + is_sorted: bool, +} + +impl Display for EventBlobHeader { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "EventBlobHeader {{ metadata_id: {}, stack: {}, payload_size: {}, seqno: {:?}, thread_id: {} ({}), proc: {:?}, timestamp: {} }}", + self.metadata_id, self.stack_id, self.payload_size, + if self.sequence_number == u32::MAX { None } else { Some(self.sequence_number) }, + self.thread_id, self.capture_thread_id, + if self.processor_number == u32::MAX { None } else { Some(self.processor_number) }, + self.timestamp) + } +} + +#[derive(BinRead, Debug, PartialEq, Default)] +#[br(little, repr=u32)] +pub enum MetadataTypeCode { + #[default] + Empty = 0, + Object = 1, + DBNull = 2, + Boolean = 3, + Char = 4, + SByte = 5, + Byte = 6, + Int16 = 7, + UInt16 = 8, + Int32 = 9, + UInt32 = 10, + Int64 = 11, + UInt64 = 12, + Single = 13, + Double = 14, + Decimal = 15, + DateTime = 16, + String = 18, + Array = 19, +} + +#[derive(BinRead, Debug)] +#[br(little)] +pub struct MetadataFieldDefinition { + type_code: MetadataTypeCode, + + #[br(if(type_code == MetadataTypeCode::Array))] + array_type_code: MetadataTypeCode, + + #[br(if(type_code == MetadataTypeCode::Object || array_type_code == MetadataTypeCode::Object))] + definition: Option, + + field_name: NullWideString, +} + +#[derive(BinRead, Debug, Default)] +#[br(little)] +pub struct MetadataPayloadDefinition { + field_count: u32, + #[br(count = field_count)] + fields: Vec, +} + +#[derive(BinRead, Debug)] +#[br(little)] +pub struct MetadataDefinition { + id: u32, + provider_name: NullWideString, + event_id: u32, + event_name: NullWideString, + keywords: u64, + version: u32, + level: u32, + + // either v1, or replaced with v2 data from tag + fields: MetadataPayloadDefinition, + + // filled in based on opcode from tag + #[br(ignore)] + opcode: Option, + // following this, there may be additional tag fields -- based on the size specified + // in the header. We can't access that in binrw, so it'll have to get handled + // manually +} + +#[derive(BinRead, Debug, PartialEq, Default)] +#[br(little, repr=u8)] +pub enum MetadataTag { + #[default] + Invalid = 0, + OpCode = 1, + V2Params = 2, +} + +#[derive(BinRead, Debug)] +#[br(little)] +pub struct MetadataTaggedData { + size: u32, // this actually seems to be junk? + + tag: MetadataTag, + + #[br(if(tag == MetadataTag::OpCode))] + opcode: u8, + + #[br(if(tag == MetadataTag::V2Params))] + fields_v2: MetadataPayloadDefinition, +} + +#[derive(Debug)] +pub struct NettraceEvent { + pub provider_name: String, + pub event_id: u32, + pub event_name: Option, + pub event_keywords: u64, + pub event_version: u32, + pub event_level: u32, + pub event_opcode: Option, + + pub sequence_number: u32, + pub thread_id: u64, + pub capture_thread_id: u64, + pub processor_number: Option, + pub stack: Vec, + pub timestamp: u64, + pub activity_id: [u8; 16], + pub related_activity_id: [u8; 16], + + pub payload: Vec, +} + +trait ReadExactlyExt { + fn read_exactly(&mut self, len: u64) -> Vec; +} + +impl ReadExactlyExt for T { + fn read_exactly(&mut self, len: u64) -> Vec { + let mut buf = vec![0; len as usize]; + self.read_exact(&mut buf) + .expect("Failed to read exact bytes"); + buf + } +} + +pub type EventPipeError = binrw::Error; + +struct EventBlobIter { + data: Cursor>, + header: NettraceEventBlockHeader, + compressed_headers: bool, + blob_size: u64, + prev_header: EventBlobHeader, +} + +impl EventBlobIter { + pub fn new(block: NettraceEventBlock, mut data: Vec) -> Result { + //eprintln!("EventBlobIter::new: {:?}", block); + let compressed_headers = (block.header.flags & 1) != 0; + let blob_size = (block.size - block.header.size as u32) as u64; + Ok(EventBlobIter { + data: Cursor::new(data), + header: block.header, + compressed_headers, + blob_size, + prev_header: Default::default(), + }) + } + + fn parse_compressed_header( + reader: &mut R, + prev_header: &mut EventBlobHeader, + ) -> BinResult { + //eprintln!("\nPREV {:?}", prev_header); + let flags: u8 = reader.read_le()?; + fn is_set(flags: u8, bit: u8) -> bool { + (flags & (1 << bit)) != 0 + } + + //eprintln!("flags: 0b{:b}", flags); + + let mut header = EventBlobHeader::default(); + header.metadata_id = if is_set(flags, 0) { + parse_varint_u32(reader)? + } else { + prev_header.metadata_id + }; + if is_set(flags, 1) { + header.sequence_number = prev_header + .sequence_number + .wrapping_add_signed(parse_varint_i32(reader)?); + header.capture_thread_id = parse_varint_u64(reader)?; + header.processor_number = parse_varint_u32(reader)?; + } else { + header.sequence_number = prev_header.sequence_number; + header.capture_thread_id = prev_header.capture_thread_id; + header.processor_number = prev_header.processor_number; + } + + if header.metadata_id != 0 { + header.sequence_number = header.sequence_number.wrapping_add(1); + } + + header.thread_id = if is_set(flags, 2) { + parse_varint_u64(reader)? + } else { + prev_header.thread_id + }; + header.stack_id = if is_set(flags, 3) { + parse_varint_u32(reader)? + } else { + prev_header.stack_id + }; + header.timestamp = prev_header + .timestamp + .wrapping_add_signed(parse_varint_i64(reader)?); + header.activity_id = if is_set(flags, 4) { + reader.read_le()? + } else { + prev_header.activity_id + }; + header.related_activity_id = if is_set(flags, 5) { + reader.read_le()? + } else { + prev_header.related_activity_id + }; + header.is_sorted = is_set(flags, 6); + header.payload_size = if is_set(flags, 7) { + parse_varint_u32(reader)? + } else { + prev_header.payload_size + }; + + header.raw_metadata_id = if header.is_sorted { (1 << 31) } else { 0 } | header.metadata_id; // set is_sorted bit + + //eprintln!("{} [flags 0b{:b}]", header, flags); + + *prev_header = header.clone(); + + Ok(header) + } + + fn parse_header( + reader: &mut R, + prev_header: &mut EventBlobHeader, + is_compressed: bool, + ) -> BinResult { + if is_compressed { + Self::parse_compressed_header(reader, prev_header) + } else { + eprintln!("parsing uncompressed header"); + reader.read_le() + } + } +} + +// Assuming block is a +impl Iterator for EventBlobIter { + type Item = (EventBlobHeader, Vec); + + fn next(&mut self) -> Option { + if self.data.position() >= self.blob_size { + return None; + } + + let header = EventBlobIter::parse_header( + &mut self.data, + &mut self.prev_header, + self.compressed_headers, + ) + .expect("Failed to read EventBlobHeader"); + let payload = self.data.read_exactly(header.payload_size as u64); + + if !self.compressed_headers && header.payload_size & 3 != 0 { + let alignment_skip = 4 - (header.payload_size & 3); + self.data + .seek(SeekFrom::Current(alignment_skip as i64)) + .expect("Seek failed"); + } + + Some((header, payload)) + } +} + +pub struct EventPipeParser { + stream: R, + metadata_map: HashMap, + stack_map: HashMap>, + trace_info: Option, + cur_event_blob_iter: Option, +} + +impl EventPipeParser +where + R: Read + Seek, +{ + pub fn new(mut stream: R) -> Result { + let file_header = NettraceHeader::read(&mut stream)?; + if file_header.ident.bytes != b"!FastSerialization.1" { + return Err(EventPipeError::BadMagic { + found: Box::new(file_header.ident.bytes), + pos: stream.stream_position().unwrap(), + }); + } + + Ok(EventPipeParser { + stream, + metadata_map: HashMap::new(), + stack_map: HashMap::new(), + trace_info: None, + cur_event_blob_iter: None, + }) + } + + fn make_err(&mut self, message: &str) -> EventPipeError { + EventPipeError::AssertFail { + pos: self.stream.stream_position().unwrap(), + message: message.to_string(), + } + } + + // Return the NettraceTraceObject for this stream, parsing until it's available if necessary. + // It will be the first thing in the stream, so we won't miss it + pub fn trace_info(&mut self) -> Result { + if let Some(trace_info) = self.trace_info { + return Ok(trace_info); + } + + // If we don't have it, we're going to assume that it's going to be the first object + let Some(type_object) = self.advance_to_next_object()? else { + return Err(self.make_err("Expected NettraceTraceObject")); + }; + + if type_object.type_name.as_str() != "TraceObject" { + return Err(self.make_err("Expected TraceObject")); + } + + let trace_info = NettraceTraceObject::read(&mut self.stream)?; + self.trace_info = Some(trace_info.clone()); + + self.read_object_end()?; + + Ok(trace_info) + } + + fn read_object_end(&mut self) -> Result<(), EventPipeError> { + let end_tag = NettraceTag::read(&mut self.stream)?; + if end_tag != NettraceTag::EndObject { + return Err(self.make_err("Expected EndObject tag")); + } + + Ok(()) + } + + fn advance_to_next_object(&mut self) -> Result, EventPipeError> { + let start_tag = NettraceTag::read(&mut self.stream)?; + if start_tag == NettraceTag::NullReference { + // stream done + return Ok(None); + } + + if start_tag != NettraceTag::BeginPrivateObject { + return Err(self.make_err("Expected BeginPrivateObject tag")); + } + + // so much effort spent in NettraceTypeObject, when it's just one of 4 things + let obj_type = NettraceTypeObject::read(&mut self.stream)?; + Ok(Some(obj_type)) + } + + fn parse_event( + &mut self, + header: EventBlobHeader, + payload: Vec, + ) -> Result, EventPipeError> { + let metadata_id = header.metadata_id; + let metadata_def = + self.metadata_map + .get(&metadata_id) + .ok_or_else(|| EventPipeError::AssertFail { + pos: 0, + message: format!("Metadata definition {} not found", metadata_id), + })?; + + let mut event = NettraceEvent { + provider_name: metadata_def.provider_name.to_string(), + event_id: metadata_def.event_id, + event_name: if metadata_def.event_name.len() > 0 { + Some(metadata_def.event_name.to_string()) + } else { + None + }, + event_keywords: metadata_def.keywords, + event_version: metadata_def.version, + event_level: metadata_def.level, + event_opcode: metadata_def.opcode, + + sequence_number: header.sequence_number, + thread_id: header.thread_id, + capture_thread_id: header.capture_thread_id, + processor_number: if header.processor_number != u32::MAX { + Some(header.processor_number) + } else { + None + }, + stack: self + .stack_map + .get(&header.stack_id) + .cloned() + .unwrap_or_default(), + timestamp: header.timestamp, + activity_id: header.activity_id, + related_activity_id: header.related_activity_id, + + payload: payload, + }; + + Ok(Some(event)) + } + + pub fn next_event(&mut self) -> Result, EventPipeError> { + // If we're inside an event block already, keep iterating through it + if let Some(cur_event_iter) = self.cur_event_blob_iter.as_mut() { + if let Some((header, payload)) = cur_event_iter.next() { + return self.parse_event(header, payload); + } + + self.cur_event_blob_iter = None; + + // we don't read this when we read the event blob; we could, but we don't + self.read_object_end()?; + } + + loop { + // Keep reading from the data until we get to an EventBlock, in which case we'll + // jump back out into the above iterator. + // + // Anything that's not an EventBlock, we need to parse into internal data structures so we can + // expose proper events from the EventBlock. + + let obj_type = self.advance_to_next_object()?; + let Some(obj_type) = obj_type else { + return Ok(None); + }; + + let obj_type_name = obj_type.type_name.as_str(); + + match obj_type_name { + "Trace" => { + let trace_object = NettraceTraceObject::read(&mut self.stream)?; + eprintln!("Trace: {:?}", trace_object); + self.trace_info = Some(trace_object.clone()); + } + "MetadataBlock" => { + //eprintln!("MetadataBlock"); + let metadata_block = NettraceEventBlock::read(&mut self.stream)?; + let metadata_block_data = self.stream.read_exactly( + metadata_block.size as u64 - metadata_block.header.size as u64, + ); + self.handle_metadata_block(EventBlobIter::new( + metadata_block, + metadata_block_data, + )?)?; + } + "StackBlock" => { + //eprintln!("StackBlock"); + let stack_block = StackBlock::read(&mut self.stream)?; + let mut stack_id = stack_block.first_id; + for stack in stack_block.stacks { + self.stack_map.insert(stack_id, stack.stack); + stack_id += 1; + } + } + "SPBlock" => { + //eprintln!("SPBlock"); + let sp_block = SequencePointBlock::read(&mut self.stream)?; + } + "EventBlock" => { + //eprintln!("EventBlock"); + let event_block = NettraceEventBlock::read(&mut self.stream)?; + let event_block_data = self + .stream + .read_exactly(event_block.size as u64 - event_block.header.size as u64); + self.cur_event_blob_iter = + Some(EventBlobIter::new(event_block, event_block_data)?); + + // jump into the iterator at the start of this + return self.next_event(); + } + unknown => { + eprintln!("Unknown object type: {}", unknown); + return Err(self.make_err("Unknown object type")); + } + } + + self.read_object_end()?; + } + } + + fn handle_metadata_block(&mut self, mut iter: EventBlobIter) -> Result<(), EventPipeError> { + while let Some((header, mut data)) = iter.next() { + let mut payload = Cursor::new(&data); + let payload_size = header.payload_size as u64; + let mut metadata_def = MetadataDefinition::read(&mut payload)?; + + while payload.position() < payload_size { + let tag_data = MetadataTaggedData::read(&mut payload)?; + if tag_data.tag == MetadataTag::OpCode { + metadata_def.opcode = Some(tag_data.opcode); + } else if tag_data.tag == MetadataTag::V2Params { + assert_eq!( + metadata_def.fields.field_count, 0, + "Found v2 fields, but v1 fields were not empty" + ); + metadata_def.fields = tag_data.fields_v2; + } + } + + self.metadata_map.insert(metadata_def.id, metadata_def); + } + + Ok(()) + } +} + +pub trait ReaderTrait: Read + Seek + BinReaderExt {} + +pub enum DecodedEvent { + CoreClrEvent(coreclr_events::CoreClrEvent), + UnknownEvent, +} + +pub fn decode_event(event: &NettraceEvent) -> DecodedEvent { + match event.provider_name.as_str() { + "Microsoft-Windows-DotNETRuntime" | "Microsoft-Windows-DotNETRuntimeRundown" => { + coreclr_events::decode_coreclr_event(event) + .map(|x| DecodedEvent::CoreClrEvent(x)) + .unwrap_or_else(|| DecodedEvent::UnknownEvent) + } + _ => DecodedEvent::UnknownEvent, + } +} diff --git a/fxprof-processed-profile/src/timestamp.rs b/fxprof-processed-profile/src/timestamp.rs index a1705cd5c..19b9f512f 100644 --- a/fxprof-processed-profile/src/timestamp.rs +++ b/fxprof-processed-profile/src/timestamp.rs @@ -18,6 +18,10 @@ impl Timestamp { nanos: (millis * 1_000_000.0) as u64, } } + + pub fn as_nanos_since_reference(&self) -> u64 { + self.nanos + } } impl Serialize for Timestamp { diff --git a/samply-mac-preload/binaries/libsamply_mac_preload.dylib b/samply-mac-preload/binaries/libsamply_mac_preload.dylib index 7a8815204e0f4e33149d0b1fe4d4d4926c2debd3..3b8b0553d537f8bb1a892ced8ecf48b0e233dc02 100755 GIT binary patch delta 7491 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 Ig_~CV4^sjuvH$=8 delta 6304 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 * if let Ok(path) = unsafe { CStr::from_ptr(path) }.to_str() { detect_and_send_jitdump_path(path); detect_and_send_marker_file_path(path); + detect_and_send_nettrace_file_path(path); } // Call the original. @@ -153,6 +155,24 @@ fn detect_and_send_marker_file_path(path: &str) { let _ = sender.send(&message_bytes, []); } +fn detect_and_send_nettrace_file_path(path: &str) { + if path.len() > 256 - 12 || !path.ends_with(".nettrace") || !filename_starts_with(path, "samply-dotnet-") { + return; + } + + let channel_sender = CHANNEL_SENDER.lock(); + let Some(sender) = channel_sender.as_ref() else { + return; + }; + let pid = unsafe { libc::getpid() }; + let mut message_bytes = [0; 256]; + message_bytes[0..7].copy_from_slice(b"NetTrac"); + message_bytes[7..11].copy_from_slice(&pid.to_le_bytes()); + message_bytes[11] = path.len() as u8; + message_bytes[12..][..path.len()].copy_from_slice(path.as_bytes()); + let _ = sender.send(&message_bytes, []); +} + #[allow(non_camel_case_types)] pub struct InterposeEntry { _new: *const (), diff --git a/samply/Cargo.toml b/samply/Cargo.toml index 7046516a7..5201f387d 100644 --- a/samply/Cargo.toml +++ b/samply/Cargo.toml @@ -50,6 +50,7 @@ log = "0.4.21" env_logger = "0.11.3" cfg-if = "1.0.0" fs4 = "0.8.3" +eventpipe = { version = "0.1", path = "../eventpipe-rs" } [target.'cfg(any(target_os = "android", target_os = "macos", target_os = "linux"))'.dependencies] diff --git a/samply/resources/libsamply_mac_preload.dylib.gz b/samply/resources/libsamply_mac_preload.dylib.gz index 0032c3f1f3429f484fc0dd8ed2ab1f9f6048d27c..fbc0cd5bbbd4dd67a75fdc48482a90e774ef4359 100644 GIT binary patch literal 12618 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 diff --git a/samply/src/mac/process_launcher.rs b/samply/src/mac/process_launcher.rs index 0a65bb9a0..4782a1945 100644 --- a/samply/src/mac/process_launcher.rs +++ b/samply/src/mac/process_launcher.rs @@ -217,6 +217,14 @@ impl TaskAccepter { let path = &marker_file_info[5..][..len]; ReceivedStuff::MarkerFilePath(pid, OsStr::from_bytes(path).into()) } + (b"NetTrac", dotnet_trace_file_info) => { + let pid_bytes = &dotnet_trace_file_info[0..4]; + let pid = + u32::from_le_bytes([pid_bytes[0], pid_bytes[1], pid_bytes[2], pid_bytes[3]]); + let len = dotnet_trace_file_info[4] as usize; + let path = &dotnet_trace_file_info[5..][..len]; + ReceivedStuff::DotnetTracePath(pid, OsStr::from_bytes(path).into()) + } (other, _) => { panic!("Unexpected message: {:?}", other); } @@ -229,6 +237,7 @@ pub enum ReceivedStuff { AcceptedTask(AcceptedTask), JitdumpPath(u32, PathBuf), MarkerFilePath(u32, PathBuf), + DotnetTracePath(u32, PathBuf), } pub struct AcceptedTask { diff --git a/samply/src/mac/profiler.rs b/samply/src/mac/profiler.rs index 36e8c57e3..62b662d87 100644 --- a/samply/src/mac/profiler.rs +++ b/samply/src/mac/profiler.rs @@ -13,9 +13,10 @@ use super::error::SamplingError; use super::process_launcher::{ ExistingProcessRunner, MachError, ReceivedStuff, RootTaskRunner, TaskAccepter, TaskLauncher, }; -use super::sampler::{JitdumpOrMarkerPath, Sampler, TaskInit, TaskInitOrShutdown}; +use super::sampler::{ProcessSpecificPath, Sampler, TaskInit, TaskInitOrShutdown}; use super::time::get_monotonic_timestamp; use crate::server::{start_server_main, ServerProps}; +use crate::shared::coreclr::CoreClrProviderProps; use crate::shared::recording_props::{ ProcessLaunchProps, ProfileCreationProps, RecordingMode, RecordingProps, }; @@ -61,9 +62,33 @@ pub fn start_recording( // If we set it, we'll also set unlink_aux_files=true to avoid leaving files // behind in the temp directory. But if it's set manually, assume the user // knows what they're doing and will specify the arg as needed. - if !env_vars.iter().any(|p| p.0 == "DOTNET_PerfMapEnabled") { - env_vars.push(("DOTNET_PerfMapEnabled".into(), "3".into())); - unlink_aux_files = true; + //if !env_vars.iter().any(|p| p.0 == "DOTNET_PerfMapEnabled") { + // env_vars.push(("DOTNET_PerfMapEnabled".into(), "3".into())); + // unlink_aux_files = true; + //} + + let coreclr_props = CoreClrProviderProps { + event_stacks: profile_creation_props.coreclr.event_stacks, + gc_markers: profile_creation_props.coreclr.gc_markers, + gc_suspensions: profile_creation_props.coreclr.gc_suspensions, + gc_detailed_allocs: profile_creation_props.coreclr.gc_detailed_allocs, + is_attach: false, + }; + let provider_args = crate::shared::coreclr::coreclr_provider_args(coreclr_props); + + let mut add_env_var = |key: &str, value: &str| { + eprintln!("{key}={value}"); + env_vars.push((key.into(), value.into())); + }; + + add_env_var("DOTNET_EnableEventPipe", "1"); + add_env_var( + "DOTNET_EventPipeOutputPath", + "/tmp/samply-dotnet-{pid}.nettrace", + ); + add_env_var("DOTNET_EventPipeConfig", &provider_args.join(",")); + if profile_creation_props.coreclr.event_stacks { + add_env_var("DOTNET_EventPipeEnableStackwalk", "1"); } } @@ -133,7 +158,7 @@ pub fn start_recording( match path_senders_per_pid.entry(pid) { Entry::Occupied(mut entry) => { let send_result = - entry.get_mut().send(JitdumpOrMarkerPath::JitdumpPath(path)); + entry.get_mut().send(ProcessSpecificPath::JitdumpPath(path)); if send_result.is_err() { // The task is probably already dead. The path arrived too late. entry.remove(); @@ -151,7 +176,25 @@ pub fn start_recording( Entry::Occupied(mut entry) => { let send_result = entry .get_mut() - .send(JitdumpOrMarkerPath::MarkerFilePath(path)); + .send(ProcessSpecificPath::MarkerFilePath(path)); + if send_result.is_err() { + // The task is probably already dead. The path arrived too late. + entry.remove(); + } + } + Entry::Vacant(_entry) => { + eprintln!( + "Received a marker file path for pid {pid} which I don't have a task for." + ); + } + } + } + Ok(ReceivedStuff::DotnetTracePath(pid, path)) => { + match path_senders_per_pid.entry(pid) { + Entry::Occupied(mut entry) => { + let send_result = entry + .get_mut() + .send(ProcessSpecificPath::DotnetTracePath(path)); if send_result.is_err() { // The task is probably already dead. The path arrived too late. entry.remove(); diff --git a/samply/src/mac/sampler.rs b/samply/src/mac/sampler.rs index 0fe87e574..a85448627 100644 --- a/samply/src/mac/sampler.rs +++ b/samply/src/mac/sampler.rs @@ -15,9 +15,10 @@ use crate::shared::recycling::ProcessRecycler; use crate::shared::timestamp_converter::TimestampConverter; use crate::shared::unresolved_samples::UnresolvedStacks; -pub enum JitdumpOrMarkerPath { +pub enum ProcessSpecificPath { JitdumpPath(PathBuf), MarkerFilePath(PathBuf), + DotnetTracePath(PathBuf), } #[derive(Debug, Clone)] @@ -31,7 +32,7 @@ pub struct TaskInit { pub start_time_mono: u64, pub task: mach_port_t, pub pid: u32, - pub path_receiver: Receiver, + pub path_receiver: Receiver, } pub struct Sampler { diff --git a/samply/src/mac/task_profiler.rs b/samply/src/mac/task_profiler.rs index 746e25418..8e9479197 100644 --- a/samply/src/mac/task_profiler.rs +++ b/samply/src/mac/task_profiler.rs @@ -31,8 +31,9 @@ use super::kernel_error::{IntoResult, KernelError}; use super::proc_maps::{ DyldInfo, DyldInfoManager, Modification, ModuleSvmaInfo, StackwalkerRef, VmSubData, }; -use super::sampler::{JitdumpOrMarkerPath, TaskInit}; +use super::sampler::{ProcessSpecificPath, TaskInit}; use super::thread_profiler::{get_thread_id, get_thread_name, ThreadProfiler}; +use crate::shared::coreclr::DotnetTraceManager; use crate::shared::jit_category_manager::JitCategoryManager; use crate::shared::jit_function_recycler::JitFunctionRecycler; use crate::shared::jitdump_manager::JitDumpManager; @@ -106,8 +107,9 @@ pub struct TaskProfiler { main_thread_label_frame: FrameInfo, ignored_errors: Vec, unwinder: UnwinderNative, - path_receiver: Receiver, + path_receiver: Receiver, jitdump_manager: JitDumpManager, + dotnet_trace_manager: DotnetTraceManager, marker_file_paths: Vec<(ThreadHandle, PathBuf)>, unresolved_samples: UnresolvedSamples, lib_mapping_ops: LibMappingOpQueue, @@ -289,6 +291,7 @@ impl TaskProfiler { unwinder: UnwinderNative::new(), path_receiver, jitdump_manager: JitDumpManager::new(profile_creation_props.unlink_aux_files), + dotnet_trace_manager: DotnetTraceManager::new(profile_creation_props.unlink_aux_files), marker_file_paths: Vec::new(), lib_mapping_ops: Default::default(), unresolved_samples: Default::default(), @@ -570,9 +573,9 @@ impl TaskProfiler { } pub fn check_received_paths(&mut self) { - while let Ok(jitdump_or_marker_file_path) = self.path_receiver.try_recv() { - match jitdump_or_marker_file_path { - JitdumpOrMarkerPath::JitdumpPath(jitdump_path) => { + while let Ok(process_specific_path) = self.path_receiver.try_recv() { + match process_specific_path { + ProcessSpecificPath::JitdumpPath(jitdump_path) => { // TODO: Detect which thread the jitdump file is opened on, and use that thread's // thread handle so that the JitFunctionAdd markers are put on that thread in the profile. self.jitdump_manager.add_jitdump_path( @@ -581,7 +584,7 @@ impl TaskProfiler { None, ); } - JitdumpOrMarkerPath::MarkerFilePath(marker_file_path) => { + ProcessSpecificPath::MarkerFilePath(marker_file_path) => { // count the number of - characters in marker_file_path let marker_info = marker_file::parse_marker_file_path(&marker_file_path); let thread_handle = if marker_info.tid.is_some() { @@ -596,6 +599,13 @@ impl TaskProfiler { self.marker_file_paths .push((thread_handle, marker_file_path)); } + ProcessSpecificPath::DotnetTracePath(dotnet_trace_path) => { + self.dotnet_trace_manager.add_dotnet_trace_path( + self.main_thread_handle, + dotnet_trace_path, + None, + ); + } } } } @@ -641,12 +651,22 @@ impl TaskProfiler { } else { None }; - let jitdump_lib_ops = self.jitdump_manager.finish( + let mut jitdump_lib_ops = self.jitdump_manager.finish( + jit_category_manager, + profile, + self.jit_function_recycler.as_mut(), + &self.timestamp_converter, + ); + + let dotnet_lib_ops = self.dotnet_trace_manager.finish( jit_category_manager, profile, self.jit_function_recycler.as_mut(), &self.timestamp_converter, ); + + jitdump_lib_ops.extend(dotnet_lib_ops); + let mut marker_spans = Vec::new(); for (thread_handle, marker_file_path) in self.marker_file_paths { if let Ok(marker_spans_from_this_file) = diff --git a/samply/src/shared/coreclr/dotnet_trace_manager.rs b/samply/src/shared/coreclr/dotnet_trace_manager.rs new file mode 100644 index 000000000..fae278b1f --- /dev/null +++ b/samply/src/shared/coreclr/dotnet_trace_manager.rs @@ -0,0 +1,285 @@ +use std::path::{Path, PathBuf}; +use std::sync::Arc; + +use crate::shared::jit_category_manager::JitCategoryManager; +use crate::shared::jit_function_recycler::JitFunctionRecycler; +use crate::shared::lib_mappings::{LibMappingAdd, LibMappingInfo, LibMappingOp, LibMappingOpQueue}; +use crate::shared::timestamp_converter::TimestampConverter; +use debugid::CodeId; +use eventpipe::{EventPipeParser, NettraceEvent}; +use fxprof_processed_profile::{ + CategoryHandle, LibraryHandle, LibraryInfo, MarkerTiming, Profile, Symbol, SymbolTable, + ThreadHandle, +}; +use wholesym::samply_symbols::debug_id_and_code_id_for_jitdump; + +use super::{eventpipe_event_to_coreclr_event, CoreClrEvent}; + +pub struct DotnetTraceManager { + pending_trace_paths: Vec, + processors: Vec, + unlink_after_open: bool, +} + +impl DotnetTraceManager { + pub fn new(unlink_after_open: bool) -> Self { + DotnetTraceManager { + pending_trace_paths: Vec::new(), + processors: Vec::new(), + unlink_after_open, + } + } + + pub fn add_dotnet_trace_path( + &mut self, + thread: ThreadHandle, + path: impl Into, + fallback_dir: Option, + ) { + let path: PathBuf = path.into(); + eprintln!("Adding dotnet trace path: {:?}", path); + self.pending_trace_paths.push(path); + } + + pub fn process_pending_records( + &mut self, + jit_category_manager: &mut JitCategoryManager, + profile: &mut Profile, + mut recycler: Option<&mut JitFunctionRecycler>, + timestamp_converter: &TimestampConverter, + ) { + self.pending_trace_paths.retain_mut(|path| { + fn trace_reader_for_path( + path: &Path, + unlink_after_open: bool, + ) -> Option<(EventPipeParser, PathBuf)> { + let mut file = std::fs::File::open(path).ok()?; + let mut reader = EventPipeParser::new(file).ok()?; + if unlink_after_open { + std::fs::remove_file(&path).ok()?; + } + Some((reader, path.into())) + } + let Some((reader, actual_path)) = trace_reader_for_path(path, self.unlink_after_open) + else { + return true; + }; + + let (debug_id, code_id_bytes) = debug_id_and_code_id_for_jitdump(123, 234, 0); + let code_id = CodeId::from_binary(&code_id_bytes); + let name = path + .file_name() + .unwrap_or(path.as_os_str()) + .to_string_lossy() + .into_owned(); + let path = path.to_string_lossy().into_owned(); + + let lib_handle = profile.add_lib(LibraryInfo { + debug_name: name.clone(), + debug_path: path.clone(), + name, + path, + debug_id, + code_id: Some(code_id.to_string()), + arch: None, + symbol_table: None, + }); + + self.processors + .push(SingleDotnetTraceProcessor::new(reader, lib_handle)); + false // "Do not retain", i.e. remove from pending_jitdump_paths + }); + + for jitdump in &mut self.processors { + jitdump.process_pending_records( + jit_category_manager, + profile, + recycler.as_deref_mut(), + timestamp_converter, + ); + } + } + + pub fn finish( + mut self, + jit_category_manager: &mut JitCategoryManager, + profile: &mut Profile, + recycler: Option<&mut JitFunctionRecycler>, + timestamp_converter: &TimestampConverter, + ) -> Vec { + self.process_pending_records(jit_category_manager, profile, recycler, timestamp_converter); + self.processors + .into_iter() + .map(|processor| processor.finish(profile)) + .collect() + } +} + +struct SingleDotnetTraceProcessor { + /// Some() until end + reader: Option>, + lib_handle: LibraryHandle, + lib_mapping_ops: LibMappingOpQueue, + symbols: Vec, + + /// The relative_address of the next JIT function. + /// + /// We define the relative address space for Jitdump files as follows: + /// Pretend that all JIT code is located in sequence, without gaps, in + /// the order of JIT_CODE_LOAD entries in the file. A given JIT function's + /// relative address is the sum of the `code_size`s of all the `JIT_CODE_LOAD` + /// entries that came before it in the file. + cumulative_address: u32, +} + +impl SingleDotnetTraceProcessor { + pub fn new(reader: EventPipeParser, lib_handle: LibraryHandle) -> Self { + Self { + reader: Some(reader), + lib_handle, + lib_mapping_ops: Default::default(), + symbols: Default::default(), + cumulative_address: 0, + } + } + + pub fn process_pending_records( + &mut self, + jit_category_manager: &mut JitCategoryManager, + profile: &mut Profile, + mut recycler: Option<&mut JitFunctionRecycler>, + timestamp_converter: &TimestampConverter, + ) { + if self.reader.is_none() { + return; + } + + let mut last_timestamp = 0; + + loop { + let event = self.reader.as_mut().unwrap().next_event(); + match event { + Ok(Some(ne)) => { + last_timestamp = ne.timestamp; + if let Some(coreclr_event) = eventpipe_event_to_coreclr_event(0, &ne) { + self.process_coreclr_event( + &coreclr_event, + jit_category_manager, + profile, + &mut recycler, + timestamp_converter, + ); + } else { + /* + match ne.event_id { + 144 | 145 | 146 | 150 | 151 | 152 | 153 | 154 | 155 | 160 | 187 => { } + _ => { + eprintln!("Unknown event: {} / {}", ne.provider_name, ne.event_id); + } + } + */ + } + } + Ok(None) => { + // last_timestamp is wrong here, but there's no explicit "close" (I don't think?) + // assume that the last event that we get has a timestamp that's roughly OK for + // end of process + self.lib_mapping_ops + .push(last_timestamp, LibMappingOp::Clear); + self.close_and_commit_symbol_table(profile); + return; + } + _ => {} + } + } + } + + fn process_coreclr_event( + &mut self, + coreclr_event: &CoreClrEvent, + jit_category_manager: &mut JitCategoryManager, + profile: &mut Profile, + recycler: &mut Option<&mut JitFunctionRecycler>, + timestamp_converter: &TimestampConverter, + ) { + match coreclr_event { + CoreClrEvent::MethodLoad(event) => { + let start_avma = event.start_address; + let end_avma = event.start_address + event.size as u64; + + let relative_address_at_start = self.cumulative_address; + self.cumulative_address += event.size; + + let symbol_name = event.name.to_string(); + self.symbols.push(Symbol { + address: relative_address_at_start, + size: if event.size == 0 { + None + } else { + Some(event.size) + }, + name: symbol_name.clone(), + }); + + let (lib_handle, relative_address_at_start) = + if let Some(recycler) = recycler.as_deref_mut() { + recycler.recycle( + start_avma, + end_avma, + relative_address_at_start, + &symbol_name, + self.lib_handle, + ) + } else { + (self.lib_handle, relative_address_at_start) + }; + + eprintln!( + "MethodLoad: addr = 0x{:x} symbol_name = {:?} size = {}", + start_avma, symbol_name, event.size + ); + + let (category, js_frame) = + jit_category_manager.classify_jit_symbol(&symbol_name, profile); + self.lib_mapping_ops.push( + event.common.timestamp, + LibMappingOp::Add(LibMappingAdd { + start_avma, + end_avma, + relative_address_at_start, + info: LibMappingInfo::new_jit_function(lib_handle, category, js_frame), + }), + ); + } + CoreClrEvent::ReadyToRunMethodEntryPoint(event) => { + let address = event.start_address; + let name = format!("{}.{}", event.name.namespace, event.name.name); + + // Can't actually do anything with this, as we don't have a size. These methods just + // won't be seen in the profile when we're using tracing only (like when attaching). + } + CoreClrEvent::GcAllocationTick(event) => {} + CoreClrEvent::MethodUnload(_) => {} + CoreClrEvent::GcTriggered(event) => {} + CoreClrEvent::GcSampledObjectAllocation(event) => {} + CoreClrEvent::GcStart(event) => {} + CoreClrEvent::GcEnd(event) => {} + } + } + + fn close_and_commit_symbol_table(&mut self, profile: &mut Profile) { + if self.reader.is_none() { + // We're already closed. + return; + } + + let symbol_table = SymbolTable::new(std::mem::take(&mut self.symbols)); + profile.set_lib_symbol_table(self.lib_handle, Arc::new(symbol_table)); + self.reader = None; + } + + pub fn finish(mut self, profile: &mut Profile) -> LibMappingOpQueue { + self.close_and_commit_symbol_table(profile); + self.lib_mapping_ops + } +} diff --git a/samply/src/shared/coreclr/eventpipe.rs b/samply/src/shared/coreclr/eventpipe.rs new file mode 100644 index 000000000..ed5321cab --- /dev/null +++ b/samply/src/shared/coreclr/eventpipe.rs @@ -0,0 +1,103 @@ +use super::{CoreClrEvent, CoreClrMethodName, ReadyToRunMethodEntryPointEvent}; +use eventpipe::{coreclr::decode_coreclr_event, NettraceEvent}; + +type EventPipeEvent = eventpipe::coreclr::CoreClrEvent; + +// Given a NettraceEvent, convert it to a samply cross-platform CoreClrEvent. +pub fn eventpipe_event_to_coreclr_event( + process_id: u32, + ne: &NettraceEvent, +) -> Option { + // Convert the NettraceEvent to a eventpipe CoreClrEvent, then convert that to + // a samply CoreClrEvent + let Some(event) = decode_coreclr_event(ne) else { + return None; + }; + + let common = super::EventCommon { + timestamp: ne.timestamp, + process_id, + thread_id: ne.thread_id as u32, + stack: if ne.stack.len() > 0 { + Some(ne.stack.clone()) + } else { + None + }, + }; + + match event { + EventPipeEvent::ModuleLoad(event) => { + eprintln!("ModuleLoad: {:?}", event); + None + } + EventPipeEvent::ModuleUnload(_) => None, + EventPipeEvent::ReadyToRunGetEntryPoint(method) => { + let name = if method.method_name.is_empty() { + format!("JIT[0x{:x}]", method.entry_point) + } else { + method.method_name.to_string() + }; + Some(CoreClrEvent::ReadyToRunMethodEntryPoint( + ReadyToRunMethodEntryPointEvent { + common, + start_address: method.entry_point, + name: CoreClrMethodName { + name, + namespace: method.method_namespace.to_string(), + signature: method.method_signature.to_string(), + }, + }, + )) + } + EventPipeEvent::MethodLoad(method) => { + let name = if method.method_name.is_empty() { + format!("JIT[0x{:x}]", method.method_start_address) + } else { + method.method_name.to_string() + }; + Some(CoreClrEvent::MethodLoad(super::MethodLoadEvent { + common, + start_address: method.method_start_address, + size: method.method_size, + name: CoreClrMethodName { + name, + namespace: method.method_namespace.to_string(), + signature: method.method_signature.to_string(), + }, + })) + } + EventPipeEvent::MethodUnload(method) => { + Some(CoreClrEvent::MethodUnload(super::MethodUnloadEvent { + common, + start_address: method.method_start_address, + size: method.method_size, + })) + } + EventPipeEvent::GcTriggered(gc) => { + Some(CoreClrEvent::GcTriggered(super::GcTriggeredEvent { + common, + reason: gc.reason, + clr_instance_id: gc.clr_instance_id, + })) + } + EventPipeEvent::GcAllocationTick(tick) => Some(CoreClrEvent::GcAllocationTick( + super::GcAllocationTickEvent { + common, + kind: tick.allocation_kind, + size: tick.allocation_amount64, + type_name: Some(format!("Type[{}]", tick.type_id)), + type_namespace: None, + }, + )), + EventPipeEvent::GcSampledObjectAllocation(alloc) => Some( + CoreClrEvent::GcSampledObjectAllocation(super::GcSampledObjectAllocationEvent { + common, + address: alloc.address, + type_name: Some(format!("Type[{}]", alloc.type_id)), + type_namespace: None, + object_count: alloc.object_count_for_type_sample, + total_size: alloc.total_size_for_type_sample, + }), + ), + } +} diff --git a/samply/src/shared/coreclr/events.rs b/samply/src/shared/coreclr/events.rs new file mode 100644 index 000000000..6ce4e0cc6 --- /dev/null +++ b/samply/src/shared/coreclr/events.rs @@ -0,0 +1,121 @@ +pub use eventpipe::coreclr::{GcAllocationKind, GcReason, GcType}; + +use super::CoreClrMethodName; + +#[derive(Debug)] +pub enum CoreClrEvent { + MethodLoad(MethodLoadEvent), + MethodUnload(MethodUnloadEvent), + GcTriggered(GcTriggeredEvent), + GcAllocationTick(GcAllocationTickEvent), + GcSampledObjectAllocation(GcSampledObjectAllocationEvent), + GcStart(GcStartEvent), + GcEnd(GcEndEvent), + ReadyToRunMethodEntryPoint(ReadyToRunMethodEntryPointEvent), +} + +impl CoreClrEvent { + #[allow(unused)] + pub fn with_stack(mut self, stack: Vec) -> Self { + match self { + CoreClrEvent::MethodLoad(ref mut e) => { + e.common.stack = Some(stack); + } + CoreClrEvent::MethodUnload(ref mut e) => { + e.common.stack = Some(stack); + } + CoreClrEvent::GcTriggered(ref mut e) => { + e.common.stack = Some(stack); + } + CoreClrEvent::GcAllocationTick(ref mut e) => { + e.common.stack = Some(stack); + } + CoreClrEvent::GcSampledObjectAllocation(ref mut e) => { + e.common.stack = Some(stack); + } + CoreClrEvent::GcStart(ref mut e) => { + e.common.stack = Some(stack); + } + CoreClrEvent::GcEnd(ref mut e) => { + e.common.stack = Some(stack); + } + CoreClrEvent::ReadyToRunMethodEntryPoint(ref mut e) => { + e.common.stack = Some(stack); + } + } + self + } +} + +#[derive(Debug)] +pub struct EventCommon { + pub timestamp: u64, + pub process_id: u32, + pub thread_id: u32, + pub stack: Option>, +} + +#[derive(Debug)] +pub struct MethodLoadEvent { + pub common: EventCommon, + pub start_address: u64, + pub size: u32, + pub name: CoreClrMethodName, +} + +#[derive(Debug)] +pub struct MethodUnloadEvent { + pub common: EventCommon, + pub start_address: u64, + pub size: u32, +} + +#[derive(Debug)] +pub struct GcTriggeredEvent { + pub common: EventCommon, + pub reason: GcReason, + pub clr_instance_id: u16, +} + +#[derive(Debug)] +pub struct GcAllocationTickEvent { + pub common: EventCommon, + pub kind: GcAllocationKind, + pub size: u64, + pub type_name: Option, + pub type_namespace: Option, +} + +#[derive(Debug)] +pub struct GcSampledObjectAllocationEvent { + pub common: EventCommon, + pub address: u64, + pub type_name: Option, + pub type_namespace: Option, + pub object_count: u32, // number of objects in this sample + pub total_size: u64, // total size of all objects +} + +#[derive(Debug)] +pub struct GcStartEvent { + pub common: EventCommon, + pub count: u32, + pub reason: GcReason, + pub depth: Option, + pub gc_type: Option, +} + +#[derive(Debug)] +pub struct GcEndEvent { + pub common: EventCommon, + pub count: u32, + pub depth: u32, + pub reason: Option, +} + +#[derive(Debug)] +pub struct ReadyToRunMethodEntryPointEvent { + pub common: EventCommon, + pub start_address: u64, + pub name: CoreClrMethodName, +} diff --git a/samply/src/shared/coreclr/markers.rs b/samply/src/shared/coreclr/markers.rs new file mode 100644 index 000000000..dd816f4d4 --- /dev/null +++ b/samply/src/shared/coreclr/markers.rs @@ -0,0 +1,178 @@ +use fxprof_processed_profile::{ + MarkerDynamicField, MarkerFieldFormat, MarkerLocation, MarkerSchema, MarkerSchemaField, + MarkerStaticField, ProfilerMarker, +}; +use serde_json::json; + +// String is type name +#[derive(Debug, Clone)] +pub struct CoreClrGcAllocTickMarker(pub String, pub usize, pub usize); + +impl ProfilerMarker for CoreClrGcAllocTickMarker { + const MARKER_TYPE_NAME: &'static str = "GC Alloc Tick"; + + fn json_marker_data(&self) -> serde_json::Value { + json!({ + "type": Self::MARKER_TYPE_NAME, + "clrtype": self.0, + "totalsize": self.1, + "objcount": self.2, + }) + } + + fn schema() -> MarkerSchema { + MarkerSchema { + type_name: Self::MARKER_TYPE_NAME, + locations: vec![ + MarkerLocation::MarkerChart, + MarkerLocation::MarkerTable, + MarkerLocation::TimelineMemory, + ], + chart_label: Some("GC Alloc"), + tooltip_label: Some("GC Alloc: {marker.data.clrtype} ({marker.data.size})"), + table_label: Some("GC Alloc"), + fields: vec![ + MarkerSchemaField::Dynamic(MarkerDynamicField { + key: "clrtype", + label: "CLR Type", + format: MarkerFieldFormat::String, + searchable: true, + }), + MarkerSchemaField::Dynamic(MarkerDynamicField { + key: "size", + label: "Total size of all objects", + format: MarkerFieldFormat::Bytes, + searchable: false, + }), + MarkerSchemaField::Dynamic(MarkerDynamicField { + key: "objcount", + label: "Number of objects allocated", + format: MarkerFieldFormat::Integer, + searchable: false, + }), + MarkerSchemaField::Static(MarkerStaticField { + label: "Description", + value: "CoreCLR GC Allocation Tick", + }), + ], + } + } +} + +#[derive(Debug, Clone)] +pub struct CoreClrGcAllocMarker(pub String, pub usize); + +impl ProfilerMarker for CoreClrGcAllocMarker { + const MARKER_TYPE_NAME: &'static str = "GC Alloc"; + + fn json_marker_data(&self) -> serde_json::Value { + json!({ + "type": Self::MARKER_TYPE_NAME, + "clrtype": self.0, + "size": self.1, + }) + } + + fn schema() -> MarkerSchema { + MarkerSchema { + type_name: Self::MARKER_TYPE_NAME, + locations: vec![ + MarkerLocation::MarkerChart, + MarkerLocation::MarkerTable, + MarkerLocation::TimelineMemory, + ], + chart_label: Some("GC Alloc"), + tooltip_label: Some("GC Alloc: {marker.data.clrtype} ({marker.data.size})"), + table_label: Some("GC Alloc"), + fields: vec![ + MarkerSchemaField::Dynamic(MarkerDynamicField { + key: "clrtype", + label: "CLR Type", + format: MarkerFieldFormat::String, + searchable: true, + }), + MarkerSchemaField::Dynamic(MarkerDynamicField { + key: "size", + label: "Size", + format: MarkerFieldFormat::Bytes, + searchable: false, + }), + MarkerSchemaField::Static(MarkerStaticField { + label: "Description", + value: "CoreCLR GC Allocation", + }), + ], + } + } +} + +#[derive(Debug, Clone)] +pub struct CoreClrGcMarker(); + +impl ProfilerMarker for CoreClrGcMarker { + const MARKER_TYPE_NAME: &'static str = "GC"; + + fn json_marker_data(&self) -> serde_json::Value { + json!({ + "type": Self::MARKER_TYPE_NAME, + }) + } + + fn schema() -> MarkerSchema { + MarkerSchema { + type_name: Self::MARKER_TYPE_NAME, + locations: vec![ + MarkerLocation::MarkerChart, + MarkerLocation::MarkerTable, + MarkerLocation::TimelineMemory, + ], + chart_label: Some("GC"), + tooltip_label: Some("GC"), + table_label: Some("GC"), + fields: vec![MarkerSchemaField::Static(MarkerStaticField { + label: "Description", + value: "CoreCLR GC", + })], + } + } +} + +#[derive(Debug, Clone)] +pub struct CoreClrGcEventMarker(pub String); + +impl ProfilerMarker for CoreClrGcEventMarker { + const MARKER_TYPE_NAME: &'static str = "GC Event"; + + fn json_marker_data(&self) -> serde_json::Value { + json!({ + "type": Self::MARKER_TYPE_NAME, + "event": self.0, + }) + } + + fn schema() -> MarkerSchema { + MarkerSchema { + type_name: Self::MARKER_TYPE_NAME, + locations: vec![ + MarkerLocation::MarkerChart, + MarkerLocation::MarkerTable, + MarkerLocation::TimelineMemory, + ], + chart_label: Some("{marker.data.event}"), + tooltip_label: Some("{marker.data.event}"), + table_label: Some("{marker.data.event}"), + fields: vec![ + MarkerSchemaField::Dynamic(MarkerDynamicField { + key: "event", + label: "Event", + format: MarkerFieldFormat::String, + searchable: true, + }), + MarkerSchemaField::Static(MarkerStaticField { + label: "Description", + value: "CoreCLR Generic GC Event", + }), + ], + } + } +} diff --git a/samply/src/shared/coreclr/mod.rs b/samply/src/shared/coreclr/mod.rs new file mode 100644 index 000000000..7e7e33cf6 --- /dev/null +++ b/samply/src/shared/coreclr/mod.rs @@ -0,0 +1,47 @@ +mod dotnet_trace_manager; +mod eventpipe; +mod events; +mod markers; +mod provider; + +use std::fmt::Display; + +pub use dotnet_trace_manager::*; +pub use eventpipe::*; +pub use events::*; +pub use markers::*; +pub use provider::*; + +#[derive(Debug, Clone)] +pub struct CoreClrProviderProps { + pub is_attach: bool, + pub gc_markers: bool, + pub gc_suspensions: bool, + pub gc_detailed_allocs: bool, + pub event_stacks: bool, +} + +pub(crate) struct SavedMarkerInfo { + pub start_timestamp_raw: u64, + pub name: String, + pub description: String, +} + +#[derive(Debug, Clone)] +pub struct CoreClrMethodName { + pub name: String, + pub namespace: String, + pub signature: String, +} + +impl Display for CoreClrMethodName { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{name} [{namespace}] \u{2329}{signature}\u{232a}", + name = self.name, + namespace = self.namespace, + signature = self.signature + ) + } +} diff --git a/samply/src/shared/coreclr/provider.rs b/samply/src/shared/coreclr/provider.rs new file mode 100644 index 000000000..4e6e8fb5e --- /dev/null +++ b/samply/src/shared/coreclr/provider.rs @@ -0,0 +1,95 @@ +use super::CoreClrProviderProps; + +#[allow(unused)] +mod constants { + pub const CORECLR_GC_KEYWORD: u64 = 0x1; // https://learn.microsoft.com/en-us/dotnet/fundamentals/diagnostics/runtime-garbage-collection-events + pub const CORECLR_GC_HANDLE_KEYWORD: u64 = 0x2; + pub const CORECLR_BINDER_KEYWORD: u64 = 0x4; // https://learn.microsoft.com/en-us/dotnet/fundamentals/diagnostics/runtime-loader-binder-events + pub const CORECLR_LOADER_KEYWORD: u64 = 0x8; // https://learn.microsoft.com/en-us/dotnet/fundamentals/diagnostics/runtime-loader-binder-events + pub const CORECLR_JIT_KEYWORD: u64 = 0x10; // https://learn.microsoft.com/en-us/dotnet/fundamentals/diagnostics/runtime-method-events + pub const CORECLR_NGEN_KEYWORD: u64 = 0x20; // https://learn.microsoft.com/en-us/dotnet/fundamentals/diagnostics/runtime-method-events + pub const CORECLR_RUNDOWN_START_KEYWORD: u64 = 0x00000040; + pub const CORECLR_INTEROP_KEYWORD: u64 = 0x2000; // https://learn.microsoft.com/en-us/dotnet/fundamentals/diagnostics/runtime-interop-events + pub const CORECLR_CONTENTION_KEYWORD: u64 = 0x4000; + pub const CORECLR_EXCEPTION_KEYWORD: u64 = 0x8000; // https://learn.microsoft.com/en-us/dotnet/fundamentals/diagnostics/runtime-exception-events + pub const CORECLR_THREADING_KEYWORD: u64 = 0x10000; // https://learn.microsoft.com/en-us/dotnet/fundamentals/diagnostics/runtime-thread-events + pub const CORECLR_JIT_TO_NATIVE_METHOD_MAP_KEYWORD: u64 = 0x20000; + pub const CORECLR_GC_SAMPLED_OBJECT_ALLOCATION_HIGH_KEYWORD: u64 = 0x200000; // https://medium.com/criteo-engineering/build-your-own-net-memory-profiler-in-c-allocations-1-2-9c9f0c86cefd + pub const CORECLR_GC_HEAP_AND_TYPE_NAMES: u64 = 0x1000000; + pub const CORECLR_GC_SAMPLED_OBJECT_ALLOCATION_LOW_KEYWORD: u64 = 0x2000000; + pub const CORECLR_STACK_KEYWORD: u64 = 0x40000000; // https://learn.microsoft.com/en-us/dotnet/framework/performance/stack-etw-event (note: says .NET Framework, but applies to CoreCLR also) + pub const CORECLR_COMPILATION_KEYWORD: u64 = 0x1000000000; + pub const CORECLR_COMPILATION_DIAGNOSTIC_KEYWORD: u64 = 0x2000000000; + pub const CORECLR_TYPE_DIAGNOSTIC_KEYWORD: u64 = 0x8000000000; +} + +/// Given a set of CoreClrProviderProps, return the list of appropriate +/// provider strings for xperf or dotnet-trace. +pub fn coreclr_provider_args(props: CoreClrProviderProps) -> Vec { + let mut providers = vec![]; + + // Enabling all the DotNETRuntime keywords is very expensive. In particular, + // enabling the NGenKeyword causes info to be generated for every NGen'd method; we should + // instead use the native PDB info from ModuleLoad events to get this information. + // + // Also enabling the rundown keyword causes a bunch of DCStart/DCEnd events to be generated, + // which is only useful if we're tracing an already running process. + // if STACK is enabled, then every CoreCLR event will also generate a stack event right afterwards + use constants::*; + let mut info_keywords = CORECLR_LOADER_KEYWORD; + info_keywords |= + CORECLR_COMPILATION_DIAGNOSTIC_KEYWORD | CORECLR_JIT_TO_NATIVE_METHOD_MAP_KEYWORD; + if props.event_stacks { + info_keywords |= CORECLR_STACK_KEYWORD; + } + if props.gc_markers || props.gc_suspensions || props.gc_detailed_allocs { + info_keywords |= CORECLR_GC_KEYWORD; + } + + let mut verbose_keywords = CORECLR_JIT_KEYWORD | CORECLR_NGEN_KEYWORD; + + // if we're attaching, ask for a rundown of method info at the start of collection + let rundown_verbose_keywords = if props.is_attach { + CORECLR_LOADER_KEYWORD | CORECLR_JIT_KEYWORD | CORECLR_RUNDOWN_START_KEYWORD + } else { + 0 + }; + + if props.gc_detailed_allocs { + info_keywords |= CORECLR_GC_SAMPLED_OBJECT_ALLOCATION_HIGH_KEYWORD + | CORECLR_GC_SAMPLED_OBJECT_ALLOCATION_LOW_KEYWORD; + } + + verbose_keywords = verbose_keywords | info_keywords; + info_keywords = 0; + + if info_keywords != 0 { + providers.push(format!( + "Microsoft-Windows-DotNETRuntime:0x{:x}:4", + info_keywords + )); + } + + if verbose_keywords != 0 { + // For some reason, we don't get JIT MethodLoad (non-Verbose) in Info level, + // even though we should. This is OK though, because non-Verbose MethodLoad doesn't + // include the method string names (we would have to pull it out based on MethodID, + // and I'm not sure which events include the mapping -- MethodJittingStarted is also + // verbose). + providers.push(format!( + "Microsoft-Windows-DotNETRuntime:0x{:x}:5", + verbose_keywords + )); + } + + if rundown_verbose_keywords != 0 { + providers.push(format!( + "Microsoft-Windows-DotNETRuntimeRundown:0x{:x}:5", + rundown_verbose_keywords + )); + } + + //providers.push(format!("Microsoft-Windows-DotNETRuntime")); + + providers +} diff --git a/samply/src/shared/mod.rs b/samply/src/shared/mod.rs index 92a8c40db..4cb46bbe0 100644 --- a/samply/src/shared/mod.rs +++ b/samply/src/shared/mod.rs @@ -1,4 +1,5 @@ pub mod context_switch; +pub mod coreclr; pub mod ctrl_c; pub mod included_processes; pub mod jit_category_manager; diff --git a/samply/src/shared/timestamp_converter.rs b/samply/src/shared/timestamp_converter.rs index f471526fb..6b55d1d19 100644 --- a/samply/src/shared/timestamp_converter.rs +++ b/samply/src/shared/timestamp_converter.rs @@ -21,4 +21,10 @@ impl TimestampConverter { (time_us * 1000).saturating_sub(self.reference_raw * self.raw_to_ns_factor), ) } + + #[allow(unused)] + pub fn convert_to_raw(&self, timestamp: Timestamp) -> u64 { + (timestamp.as_nanos_since_reference() / self.raw_to_ns_factor) + .saturating_add(self.reference_raw) + } } diff --git a/samply/src/windows/coreclr.rs b/samply/src/windows/coreclr.rs index 15b557c51..6344371ac 100644 --- a/samply/src/windows/coreclr.rs +++ b/samply/src/windows/coreclr.rs @@ -1,10 +1,8 @@ use std::{collections::HashMap, convert::TryInto, fmt::Display}; -use bitflags::bitflags; +use eventpipe::coreclr::{GcSuspendEeReason, GcType}; use fxprof_processed_profile::*; -use num_derive::FromPrimitive; use num_traits::FromPrimitive; -use serde_json::json; use etw_reader::{self, schema::TypedEvent}; use etw_reader::{ @@ -12,32 +10,30 @@ use etw_reader::{ parser::{Parser, TryParse}, }; +use crate::shared::coreclr::*; use crate::shared::process_sample_data::SimpleMarker; use crate::shared::recording_props::{CoreClrProfileProps, ProfileCreationProps}; + use crate::windows::profile_context::{KnownCategory, ProfileContext}; use super::elevated_helper::ElevatedRecordingProps; -struct SavedMarkerInfo { - start_timestamp_raw: u64, - name: String, - description: String, -} - pub struct CoreClrContext { - props: CoreClrProfileProps, + pub props: CoreClrProviderProps, + pub unknown_event_markers: bool, + last_marker_on_thread: HashMap, gc_markers_on_thread: HashMap>, - unknown_event_markers: bool, } impl CoreClrContext { - pub fn new(profile_creation_props: ProfileCreationProps) -> Self { + pub fn new(props: CoreClrProviderProps, unknown_event_markers: bool) -> Self { Self { - props: profile_creation_props.coreclr, + props, + unknown_event_markers, + last_marker_on_thread: HashMap::new(), gc_markers_on_thread: HashMap::new(), - unknown_event_markers: profile_creation_props.unknown_event_markers, } } @@ -74,229 +70,6 @@ impl CoreClrContext { } } -bitflags! { - #[derive(PartialEq, Eq)] - pub struct CoreClrMethodFlagsMap: u32 { - const dynamic = 0x1; - const generic = 0x2; - const has_shared_generic_code = 0x4; - const jitted = 0x8; - const jit_helper = 0x10; - const profiler_rejected_precompiled_code = 0x20; - const ready_to_run_rejected_precompiled_code = 0x40; - - // next three bits are the tiered compilation level - const opttier_bit0 = 0x80; - const opttier_bit1 = 0x100; - const opttier_bit2 = 0x200; - - // extent flags/value (hot/cold) - const extent_bit_0 = 0x10000000; // 0x1 == cold, 0x0 = hot - const extent_bit_1 = 0x20000000; // always 0 for now looks like - const extent_bit_2 = 0x40000000; - const extent_bit_3 = 0x80000000; - - const _ = !0; - } - #[derive(PartialEq, Eq)] - pub struct TieredCompilationSettingsMap: u32 { - const None = 0x0; - const QuickJit = 0x1; - const QuickJitForLoops = 0x2; - const TieredPGO = 0x4; - const ReadyToRun = 0x8; - } -} - -#[allow(unused)] -mod constants { - pub const CORECLR_GC_KEYWORD: u64 = 0x1; // https://learn.microsoft.com/en-us/dotnet/fundamentals/diagnostics/runtime-garbage-collection-events - pub const CORECLR_GC_HANDLE_KEYWORD: u64 = 0x2; - pub const CORECLR_BINDER_KEYWORD: u64 = 0x4; // https://learn.microsoft.com/en-us/dotnet/fundamentals/diagnostics/runtime-loader-binder-events - pub const CORECLR_LOADER_KEYWORD: u64 = 0x8; // https://learn.microsoft.com/en-us/dotnet/fundamentals/diagnostics/runtime-loader-binder-events - pub const CORECLR_JIT_KEYWORD: u64 = 0x10; // https://learn.microsoft.com/en-us/dotnet/fundamentals/diagnostics/runtime-method-events - pub const CORECLR_NGEN_KEYWORD: u64 = 0x20; // https://learn.microsoft.com/en-us/dotnet/fundamentals/diagnostics/runtime-method-events - pub const CORECLR_RUNDOWN_START_KEYWORD: u64 = 0x00000040; - pub const CORECLR_INTEROP_KEYWORD: u64 = 0x2000; // https://learn.microsoft.com/en-us/dotnet/fundamentals/diagnostics/runtime-interop-events - pub const CORECLR_CONTENTION_KEYWORD: u64 = 0x4000; - pub const CORECLR_EXCEPTION_KEYWORD: u64 = 0x8000; // https://learn.microsoft.com/en-us/dotnet/fundamentals/diagnostics/runtime-exception-events - pub const CORECLR_THREADING_KEYWORD: u64 = 0x10000; // https://learn.microsoft.com/en-us/dotnet/fundamentals/diagnostics/runtime-thread-events - pub const CORECLR_JIT_TO_NATIVE_METHOD_MAP_KEYWORD: u64 = 0x20000; - pub const CORECLR_GC_SAMPLED_OBJECT_ALLOCATION_HIGH_KEYWORD: u64 = 0x200000; // https://medium.com/criteo-engineering/build-your-own-net-memory-profiler-in-c-allocations-1-2-9c9f0c86cefd - pub const CORECLR_GC_HEAP_AND_TYPE_NAMES: u64 = 0x1000000; - pub const CORECLR_GC_SAMPLED_OBJECT_ALLOCATION_LOW_KEYWORD: u64 = 0x2000000; - pub const CORECLR_STACK_KEYWORD: u64 = 0x40000000; // https://learn.microsoft.com/en-us/dotnet/framework/performance/stack-etw-event (note: says .NET Framework, but applies to CoreCLR also) - pub const CORECLR_COMPILATION_KEYWORD: u64 = 0x1000000000; - pub const CORECLR_COMPILATION_DIAGNOSTIC_KEYWORD: u64 = 0x2000000000; - pub const CORECLR_TYPE_DIAGNOSTIC_KEYWORD: u64 = 0x8000000000; -} - -#[derive(Debug, Clone, FromPrimitive)] -enum GcReason { - AllocSmall = 0, - Induced, - LowMemory, - Empty, - AllocLarge, - OutOfSpaceSmallObjectHeap, - OutOfSpaceLargeObjectHeap, - InducedNoForce, - Stress, - InducedLowMemory, -} - -impl Display for GcReason { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - GcReason::AllocSmall => f.write_str("Small object heap allocation"), - GcReason::Induced => f.write_str("Induced"), - GcReason::LowMemory => f.write_str("Low memory"), - GcReason::Empty => f.write_str("Empty"), - GcReason::AllocLarge => f.write_str("Large object heap allocation"), - GcReason::OutOfSpaceSmallObjectHeap => { - f.write_str("Out of space (for small object heap)") - } - GcReason::OutOfSpaceLargeObjectHeap => { - f.write_str("Out of space (for large object heap)") - } - GcReason::InducedNoForce => f.write_str("Induced but not forced as blocking"), - GcReason::Stress => f.write_str("Stress"), - GcReason::InducedLowMemory => f.write_str("Induced low memory"), - } - } -} - -#[derive(Debug, Clone, FromPrimitive)] -enum GcSuspendEeReason { - Other = 0, - GC, - AppDomainShutdown, - CodePitching, - Shutdown, - Debugger, - GcPrep, - DebuggerSweep, -} - -impl Display for GcSuspendEeReason { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - GcSuspendEeReason::Other => f.write_str("Other"), - GcSuspendEeReason::GC => f.write_str("GC"), - GcSuspendEeReason::AppDomainShutdown => f.write_str("AppDomain shutdown"), - GcSuspendEeReason::CodePitching => f.write_str("Code pitching"), - GcSuspendEeReason::Shutdown => f.write_str("Shutdown"), - GcSuspendEeReason::Debugger => f.write_str("Debugger"), - GcSuspendEeReason::GcPrep => f.write_str("GC prep"), - GcSuspendEeReason::DebuggerSweep => f.write_str("Debugger sweep"), - } - } -} - -#[derive(Debug, Clone, FromPrimitive)] -enum GcType { - Blocking, - Background, - BlockingDuringBackground, -} - -impl Display for GcType { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - GcType::Blocking => f.write_str("Blocking GC"), - GcType::Background => f.write_str("Background GC"), - GcType::BlockingDuringBackground => f.write_str("Blocking GC during background GC"), - } - } -} -// String is type name -#[derive(Debug, Clone)] -pub struct CoreClrGcAllocMarker(pub String, usize); - -impl ProfilerMarker for CoreClrGcAllocMarker { - const MARKER_TYPE_NAME: &'static str = "GC Alloc"; - - fn json_marker_data(&self) -> serde_json::Value { - json!({ - "type": Self::MARKER_TYPE_NAME, - "clrtype": self.0, - "size": self.1, - }) - } - - fn schema() -> MarkerSchema { - MarkerSchema { - type_name: Self::MARKER_TYPE_NAME, - locations: vec![ - MarkerLocation::MarkerChart, - MarkerLocation::MarkerTable, - MarkerLocation::TimelineMemory, - ], - chart_label: Some("GC Alloc"), - tooltip_label: Some("GC Alloc: {marker.data.clrtype} ({marker.data.size} bytes)"), - table_label: Some("GC Alloc"), - fields: vec![ - MarkerSchemaField::Dynamic(MarkerDynamicField { - key: "clrtype", - label: "CLR Type", - format: MarkerFieldFormat::String, - searchable: true, - }), - MarkerSchemaField::Dynamic(MarkerDynamicField { - key: "size", - label: "Size", - format: MarkerFieldFormat::Bytes, - searchable: false, - }), - MarkerSchemaField::Static(MarkerStaticField { - label: "Description", - value: "GC Allocation.", - }), - ], - } - } -} - -#[derive(Debug, Clone)] -pub struct CoreClrGcEventMarker(pub String); - -impl ProfilerMarker for CoreClrGcEventMarker { - const MARKER_TYPE_NAME: &'static str = "GC Event"; - - fn json_marker_data(&self) -> serde_json::Value { - json!({ - "type": Self::MARKER_TYPE_NAME, - "event": self.0, - }) - } - - fn schema() -> MarkerSchema { - MarkerSchema { - type_name: Self::MARKER_TYPE_NAME, - locations: vec![ - MarkerLocation::MarkerChart, - MarkerLocation::MarkerTable, - MarkerLocation::TimelineMemory, - ], - chart_label: Some("{marker.data.event}"), - tooltip_label: Some("{marker.data.event}"), - table_label: Some("{marker.data.event}"), - fields: vec![ - MarkerSchemaField::Dynamic(MarkerDynamicField { - key: "event", - label: "Event", - format: MarkerFieldFormat::String, - searchable: true, - }), - MarkerSchemaField::Static(MarkerStaticField { - label: "Description", - value: "Generic GC Event.", - }), - ], - } - } -} - #[derive(Debug, Clone, PartialEq, Eq)] struct DisplayUnknownIfNone<'a, T>(pub &'a Option); @@ -310,72 +83,20 @@ impl<'a, T: Display> Display for DisplayUnknownIfNone<'a, T> { } pub fn coreclr_xperf_args(props: &ElevatedRecordingProps) -> Vec { - let mut providers = vec![]; - if !props.coreclr.any_enabled() { - return providers; - } - - // Enabling all the DotNETRuntime keywords is very expensive. In particular, - // enabling the NGenKeyword causes info to be generated for every NGen'd method; we should - // instead use the native PDB info from ModuleLoad events to get this information. - // - // Also enabling the rundown keyword causes a bunch of DCStart/DCEnd events to be generated, - // which is only useful if we're tracing an already running process. - // if STACK is enabled, then every CoreCLR event will also generate a stack event right afterwards - use constants::*; - let mut info_keywords = CORECLR_LOADER_KEYWORD; - if props.coreclr.event_stacks { - info_keywords |= CORECLR_STACK_KEYWORD; - } - if props.coreclr.gc_markers || props.coreclr.gc_suspensions || props.coreclr.gc_detailed_allocs - { - info_keywords |= CORECLR_GC_KEYWORD; + return vec![]; } - let verbose_keywords = CORECLR_JIT_KEYWORD | CORECLR_NGEN_KEYWORD; - - // if we're attaching, ask for a rundown of method info at the start of collection - let rundown_verbose_keywords = if props.is_attach { - CORECLR_LOADER_KEYWORD | CORECLR_JIT_KEYWORD | CORECLR_RUNDOWN_START_KEYWORD - } else { - 0 + // copy matching property names from props.coreclr into CoreClrProviderProperties + let coreclr_props = CoreClrProviderProps { + is_attach: props.is_attach, + gc_markers: props.coreclr.gc_markers, + gc_suspensions: props.coreclr.gc_suspensions, + gc_detailed_allocs: props.coreclr.gc_detailed_allocs, + event_stacks: props.coreclr.event_stacks, }; - if props.coreclr.gc_detailed_allocs { - info_keywords |= CORECLR_GC_SAMPLED_OBJECT_ALLOCATION_HIGH_KEYWORD - | CORECLR_GC_SAMPLED_OBJECT_ALLOCATION_LOW_KEYWORD; - } - - if info_keywords != 0 { - providers.push(format!( - "Microsoft-Windows-DotNETRuntime:0x{:x}:4", - info_keywords - )); - } - - if verbose_keywords != 0 { - // For some reason, we don't get JIT MethodLoad (non-Verbose) in Info level, - // even though we should. This is OK though, because non-Verbose MethodLoad doesn't - // include the method string names (we would have to pull it out based on MethodID, - // and I'm not sure which events include the mapping -- MethodJittingStarted is also - // verbose). - providers.push(format!( - "Microsoft-Windows-DotNETRuntime:0x{:x}:5", - verbose_keywords - )); - } - - if rundown_verbose_keywords != 0 { - providers.push(format!( - "Microsoft-Windows-DotNETRuntimeRundown:0x{:x}:5", - rundown_verbose_keywords - )); - } - - //providers.push(format!("Microsoft-Windows-DotNETRuntime")); - - providers + coreclr_provider_args(coreclr_props) } pub fn handle_coreclr_event( @@ -737,3 +458,87 @@ pub fn handle_coreclr_event( coreclr_context.set_last_event_for_thread(tid, marker_handle); } } + +pub fn handle_new_coreclr_event( + context: &mut ProfileContext, + coreclr_context: &mut CoreClrContext, + event: &CoreClrEvent, + is_in_time_range: bool, +) { + let (gc_markers, gc_suspensions, gc_allocs) = ( + coreclr_context.props.gc_markers, + coreclr_context.props.gc_suspensions, + coreclr_context.props.gc_detailed_allocs, + ); + + // Handle events that we need to handle whether in time range or not first + + match event { + CoreClrEvent::MethodLoad(e) => { + let method_name = e.name.to_string(); + context.handle_coreclr_method_load( + e.common.timestamp, + e.common.process_id, + method_name, + e.start_address, + e.size, + ); + } + CoreClrEvent::MethodUnload(e) => { + // don't care + } + CoreClrEvent::GcTriggered(e) if is_in_time_range && gc_markers => { + let mh = context.add_thread_instant_marker( + e.common.timestamp, + e.common.thread_id, + KnownCategory::CoreClrGc, + "GC Trigger", + CoreClrGcEventMarker(format!("GC Trigger: {}", e.reason)), + ); + coreclr_context.set_last_event_for_thread(e.common.thread_id, mh); + } + CoreClrEvent::GcAllocationTick(e) if is_in_time_range && gc_allocs => {} + CoreClrEvent::GcSampledObjectAllocation(e) if is_in_time_range && gc_allocs => { + let mh = context.add_thread_instant_marker( + e.common.timestamp, + e.common.thread_id, + KnownCategory::CoreClrGc, + "GC Alloc", + CoreClrGcAllocMarker( + format!("{}", DisplayUnknownIfNone(&e.type_name)), + e.total_size as usize, + ), + ); + coreclr_context.set_last_event_for_thread(e.common.thread_id, mh); + } + CoreClrEvent::GcStart(e) if is_in_time_range && gc_markers => { + // TODO: use gc_type_str as the name + coreclr_context.save_gc_marker( + e.common.thread_id, + e.common.timestamp, + "GC", + "GC".to_owned(), + format!( + "{}: {} (GC #{}, gen{})", + DisplayUnknownIfNone(&e.gc_type), + e.reason, + e.count, + DisplayUnknownIfNone(&e.depth) + ), + ); + } + CoreClrEvent::GcEnd(e) if is_in_time_range && gc_markers => { + if let Some(info) = coreclr_context.remove_gc_marker(e.common.thread_id, "GC") { + context.add_thread_interval_marker( + info.start_timestamp_raw, + e.common.timestamp, + e.common.thread_id, + KnownCategory::CoreClrGc, + &info.name, + CoreClrGcEventMarker(info.description), + ); + } + } + _ => {} + } +} diff --git a/samply/src/windows/etw_coreclr.rs b/samply/src/windows/etw_coreclr.rs new file mode 100644 index 000000000..3074a57ec --- /dev/null +++ b/samply/src/windows/etw_coreclr.rs @@ -0,0 +1,257 @@ +use std::{collections::HashMap, convert::TryInto, fmt::Display}; + +use eventpipe::coreclr::{GcSuspendEeReason, GcType}; +use fxprof_processed_profile::*; +use num_traits::FromPrimitive; + +use etw_reader::{self, schema::TypedEvent}; +use etw_reader::{ + event_properties_to_string, + parser::{Parser, TryParse}, +}; + +use crate::shared::coreclr::*; +use crate::shared::process_sample_data::SimpleMarker; +use crate::shared::recording_props::{CoreClrProfileProps, ProfileCreationProps}; + +use crate::windows::profile_context::{KnownCategory, ProfileContext}; + +use super::elevated_helper::ElevatedRecordingProps; + +pub struct CoreClrEtwConverter { + last_event_on_thread: HashMap, +} + +impl CoreClrEtwConverter { + pub fn new() -> Self { + Self { + last_event_on_thread: HashMap::new(), + } + } + + pub fn remaining_clr_events_on_threads( + &mut self, + ) -> std::collections::hash_map::IntoIter { + std::mem::take(&mut self.last_event_on_thread).into_iter() + } + + pub fn etw_event_to_coreclr_event( + &mut self, + coreclr_context: &mut CoreClrContext, + s: &TypedEvent, + parser: &mut Parser, + ) -> Option { + let timestamp_raw = s.timestamp() as u64; + + let mut name_parts = s.name().splitn(3, '/'); + let provider = name_parts.next().unwrap(); + let task = name_parts.next().unwrap(); + let opcode = name_parts.next().unwrap(); + + match provider { + "Microsoft-Windows-DotNETRuntime" | "Microsoft-Windows-DotNETRuntimeRundown" => {} + _ => { + panic!("Unexpected event {}", s.name()) + } + } + + let pid = s.process_id(); + let tid = s.thread_id(); + + // ETW CoreCLR stackwalk events are a separate event that comes after the event to which + // it should be attached. Our cross-platform CoreCLR events have the stack as an optional + // part of every event. So, if we're recording stacks from ETW, instead of returning + // non-stackwalk events directly, we store them as the last event for a given thread so + // that we can attach a stack. + // + // If we have a pending event and we get a stackwalk event, we'll attach the stack and + // return the pending event. + // If we get a non-stackwalk event, we'll store it, and still return this previous + // pending event. + let pending_event = self.last_event_on_thread.remove(&tid); + + // Handle StackWalk events outside of the big match below for cleanliness + if (task, opcode) == ("CLRStack", "CLRStackWalk") { + // If the STACK keyword is enabled, we get a CLRStackWalk following each CLR event that supports stacks. Not every event + // does. The info about which does and doesn't is here: https://github.com/dotnet/runtime/blob/main/src/coreclr/vm/ClrEtwAllMeta.lst + // Current dotnet (8.0.x) seems to have a bug where `MethodJitMemoryAllocatedForCode` events will fire a stackwalk, + // but the event itself doesn't end up in the trace. (https://github.com/dotnet/runtime/issues/102004) + + // if we don't have anything to attach this stack to, just skip it + if pending_event.is_none() { + return None; + } + + let pending_event = pending_event.unwrap(); + + // "Stack" is explicitly declared as length 2 in the manifest, so the first two addresses are in here, rest + // are in user data buffer. + let first_addresses: Vec = parser.parse("Stack"); + let address_iter = first_addresses + .chunks_exact(8) + .chain(parser.buffer.chunks_exact(8)) + .map(|chunk| u64::from_le_bytes(chunk.try_into().unwrap())); + + return Some(pending_event.with_stack(address_iter.collect())); + } + + let common = EventCommon { + timestamp: timestamp_raw, + process_id: pid, + thread_id: tid, + stack: None, + }; + + let new_event = match (task, opcode) { + ("CLRMethod" | "CLRMethodRundown", method_event) => match method_event { + "MethodLoadVerbose" | "MethodDCStartVerbose" => { + let method_basename: String = parser.parse("MethodName"); + let method_namespace: String = parser.parse("MethodNamespace"); + let method_signature: String = parser.parse("MethodSignature"); + + let method_start_address: u64 = parser.parse("MethodStartAddress"); + let method_size: u32 = parser.parse("MethodSize"); + + Some(CoreClrEvent::MethodLoad(MethodLoadEvent { + common, + start_address: method_start_address, + size: method_size, + name: CoreClrMethodName { + name: method_basename, + namespace: method_namespace, + signature: method_signature, + }, + })) + } + "ModuleLoad" | "ModuleDCStart" | "ModuleUnload" | "ModuleDCEnd" | _ => None, + }, + ("GarbageCollection", gc_event) => { + match gc_event { + "GCSampledObjectAllocation" => { + // If High/Low flags are set, then we get one of these for every alloc. Otherwise only + // when a threshold is hit. (100kb) The count and size are aggregates in that case. + let type_id: u64 = parser.parse("TypeID"); // TODO: convert to str, with bulk type data + let address: u64 = parser.parse("Address"); + let object_count: u32 = parser.parse("ObjectCountForTypeSample"); + let total_size: u64 = parser.parse("TotalSizeForTypeSample"); + + Some(CoreClrEvent::GcSampledObjectAllocation( + GcSampledObjectAllocationEvent { + common, + address, + type_name: Some(format!("Type[{}]", type_id)), + type_namespace: None, + object_count, + total_size, + }, + )) + } + "Triggered" => { + let reason: u32 = parser.parse("Reason"); + let reason = GcReason::from_u32(reason).unwrap_or_else(|| { + eprintln!("Unknown CLR GC Triggered reason: {}", reason); + GcReason::Empty + }); + + Some(CoreClrEvent::GcTriggered(GcTriggeredEvent { + common, + reason, + clr_instance_id: 0, + })) + } + "GCSuspendEEBegin" => { + // Reason, Count + //let _count: u32 = parser.parse("Count"); + let reason: u32 = parser.parse("Reason"); + + let _reason = GcSuspendEeReason::from_u32(reason).unwrap_or_else(|| { + eprintln!("Unknown CLR GCSuspendEEBegin reason: {}", reason); + GcSuspendEeReason::Other + }); + + //Some(CoreClrEvent::GcSuspendEeBegin(GcSuspendEeBeginEvent { + // common, + // reason, + //})) + // TODO + None + } + "GCSuspendEEEnd" => { + // TODO + None + } + "GCRestartEEBegin" => { + // TODO + None + } + "GCRestartEEEnd" => { + // TODO + None + } + "win:Start" => { + let count: u32 = parser.parse("Count"); + let depth: Option = parser.try_parse("Depth").ok(); + let reason: u32 = parser.parse("Reason"); + let gc_type = parser.try_parse("Type").ok().and_then(GcType::from_u32); + + let reason = GcReason::from_u32(reason).unwrap_or_else(|| { + eprintln!("Unknown CLR GCStart reason: {}", reason); + GcReason::Empty + }); + + Some(CoreClrEvent::GcStart(GcStartEvent { + common, + count, + depth, + reason, + gc_type, + })) + } + "win:Stop" => { + let count: u32 = parser.parse("Count"); + let depth: u32 = parser.parse("Depth"); + let reason = parser.try_parse("Reason").ok().and_then(GcReason::from_u32); + Some(CoreClrEvent::GcEnd(GcEndEvent { + common, + count, + depth, + reason, + })) + } + "SetGCHandle" => { + // TODO + None + } + "DestroyGCHandle" => { + // TODO + None + } + "GCFinalizersBegin" | "GCFinalizersEnd" | "FinalizeObject" => { + // TODO: create an interval + None + } + "GCCreateSegment" | "GCFreeSegment" | "GCDynamicEvent" | "GCHeapStats" => { + // don't care + None + } + _ => { + // don't care + None + } + } + } + ("CLRRuntimeInformation", _) => None, + ("CLRLoader", _) => { + // AppDomain, Assembly, Module Load/Unload + None + } + _ => None, + }; + + if new_event.is_some() { + self.last_event_on_thread.insert(tid, new_event.unwrap()); + } + + pending_event + } +} diff --git a/samply/src/windows/etw_gecko.rs b/samply/src/windows/etw_gecko.rs index e9f21f6d7..3dc8f6f82 100644 --- a/samply/src/windows/etw_gecko.rs +++ b/samply/src/windows/etw_gecko.rs @@ -11,8 +11,11 @@ use etw_reader::{ use fxprof_processed_profile::debugid; use uuid::Uuid; +use super::coreclr::CoreClrContext; use super::profile_context::ProfileContext; -use crate::windows::coreclr; +use crate::shared::coreclr::CoreClrProviderProps; +use crate::windows::coreclr::{self, handle_new_coreclr_event}; +use crate::windows::etw_coreclr::CoreClrEtwConverter; use crate::windows::profile_context::KnownCategory; pub fn profile_pid_from_etl_file(context: &mut ProfileContext, etl_file: &Path) { @@ -25,7 +28,18 @@ pub fn profile_pid_from_etl_file(context: &mut ProfileContext, etl_file: &Path) let demand_zero_faults = false; //pargs.contains("--demand-zero-faults"); - let mut core_clr_context = coreclr::CoreClrContext::new(context.creation_props()); + let coreclr_props = context.creation_props().coreclr; + let mut core_clr_context = CoreClrContext::new( + CoreClrProviderProps { + is_attach: false, + gc_markers: coreclr_props.gc_markers, + gc_suspensions: coreclr_props.gc_suspensions, + gc_detailed_allocs: coreclr_props.gc_detailed_allocs, + event_stacks: coreclr_props.event_stacks, + }, + false, + ); + let mut core_clr_converter = CoreClrEtwConverter::new(); let result = open_trace(etl_file, |e| { let Ok(s) = schema_locator.event_schema(e) else { @@ -385,9 +399,21 @@ pub fn profile_pid_from_etl_file(context: &mut ProfileContext, etl_file: &Path) ); } dotnet_event if dotnet_event.starts_with("Microsoft-Windows-DotNETRuntime") => { - let is_in_range = context.is_in_time_range(timestamp_raw); + let is_in_time_range = context.is_in_time_range(timestamp_raw); // Note: No "/" at end of event name, because we want DotNETRuntimeRundown as well - coreclr::handle_coreclr_event(context, &mut core_clr_context, &s, &mut parser, is_in_range); + //coreclr::handle_coreclr_event(context, &mut core_clr_context, &s, &mut parser, is_in_time_range); + if let Some(event) = core_clr_converter.etw_event_to_coreclr_event( + &mut core_clr_context, + &s, + &mut parser, + ) { + handle_new_coreclr_event( + context, + &mut core_clr_context, + &event, + is_in_time_range, + ); + } } _ => { if !context.is_in_time_range(timestamp_raw) { diff --git a/samply/src/windows/mod.rs b/samply/src/windows/mod.rs index 49074f9b2..d17e3dcff 100644 --- a/samply/src/windows/mod.rs +++ b/samply/src/windows/mod.rs @@ -2,6 +2,7 @@ mod chrome_etw_flags; mod console; mod coreclr; mod elevated_helper; +mod etw_coreclr; mod etw_gecko; mod firefox_etw_flags; mod gfx; diff --git a/samply/src/windows/profile_context.rs b/samply/src/windows/profile_context.rs index 0fe863c9d..2e8abe2ae 100644 --- a/samply/src/windows/profile_context.rs +++ b/samply/src/windows/profile_context.rs @@ -14,6 +14,7 @@ use serde_json::{json, Value}; use uuid::Uuid; use super::chrome_etw_flags::KeywordNames; +use super::coreclr::CoreClrContext; use super::winutils; use crate::shared::context_switch::{ ContextSwitchHandler, OffCpuSampleGroup, ThreadContextSwitchData, @@ -1839,6 +1840,24 @@ impl ProfileContext { self.profile } + + pub fn convert_raw_timestamp(&self, ts_raw: u64) -> Timestamp { + self.timestamp_converter.convert_time(ts_raw) + } + + pub fn add_thread_marker( + &mut self, + tid: u32, + timing: MarkerTiming, + known_category: KnownCategory, + name: &str, + marker: impl ProfilerMarker, + ) -> MarkerHandle { + let thread = self.threads.get_mut(&tid).unwrap(); + let category = self.categories.get(known_category, &mut self.profile); + self.profile + .add_marker(thread.handle, category, name, marker, timing) + } } struct PeInfo { From b8be0bcc46ab77f00f144d9cd1dfd0d1791b76b8 Mon Sep 17 00:00:00 2001 From: Vladimir Vukicevic Date: Mon, 24 Jun 2024 13:11:00 -0700 Subject: [PATCH 02/36] rundown ModuleDCEnd --- eventpipe-rs/src/coreclr_events.rs | 35 +++++++--- .../shared/coreclr/dotnet_trace_manager.rs | 29 +++++++- samply/src/shared/coreclr/eventpipe.rs | 68 ++++++++++++++++++- samply/src/shared/coreclr/events.rs | 53 ++++++++++++--- samply/src/shared/coreclr/provider.rs | 4 +- samply/src/shared/lib_mappings.rs | 4 ++ 6 files changed, 167 insertions(+), 26 deletions(-) diff --git a/eventpipe-rs/src/coreclr_events.rs b/eventpipe-rs/src/coreclr_events.rs index 090527ed8..a18c14a72 100644 --- a/eventpipe-rs/src/coreclr_events.rs +++ b/eventpipe-rs/src/coreclr_events.rs @@ -181,23 +181,20 @@ pub enum CoreClrEvent { GcAllocationTick(GcAllocationTickEvent), GcSampledObjectAllocation(GcSampledObjectAllocationEvent), ReadyToRunGetEntryPoint(ReadyToRunGetEntryPointEvent), + MethodDCEnd(MethodLoadUnloadEvent), } pub fn decode_coreclr_event(event: &NettraceEvent) -> Option { - if event.provider_name != "Microsoft-Windows-DotNETRuntime" - && event.provider_name != "Microsoft-Windows-DotNETRuntimeRundown" - { - return None; - } - - let is_rundown = event.provider_name.ends_with("Rundown"); match event.provider_name.as_str() { - "Microsoft-Windows-DotNETRuntime" => {} - //"Microsoft-Windows-DotNETRuntimeRundown" => decode_coreclr_event(event, payload), - _ => return None, + "Microsoft-Windows-DotNETRuntime" => decode_coreclr_regular_event(event), + "Microsoft-Windows-DotNETRuntimeRundown" => decode_coreclr_rundown_event(event), + _ => None, } +} +fn decode_coreclr_regular_event(event: &NettraceEvent) -> Option { let mut payload = Cursor::new(&event.payload); + //eprintln!("Regular: {:?}", event.event_id); match event.event_id { // 151: DomainModuleLoad @@ -340,3 +337,21 @@ pub fn decode_coreclr_event(event: &NettraceEvent) -> Option { _ => None, } } + +fn decode_coreclr_rundown_event(event: &NettraceEvent) -> Option { + let mut payload = Cursor::new(&event.payload); + + //eprintln!("RUNDOWN: {:?}", event.event_id); + match event.event_id { + 144 => { + // MethodDCStartVerbose | MethodDCEndVerbose + let ev = MethodLoadUnloadEvent::read_le_args( + &mut payload, + binrw::args! { version: event.event_version, verbose: true }, + ) + .unwrap(); + Some(CoreClrEvent::MethodDCEnd(ev)) + } + _ => None, + } +} diff --git a/samply/src/shared/coreclr/dotnet_trace_manager.rs b/samply/src/shared/coreclr/dotnet_trace_manager.rs index fae278b1f..85685099d 100644 --- a/samply/src/shared/coreclr/dotnet_trace_manager.rs +++ b/samply/src/shared/coreclr/dotnet_trace_manager.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::path::{Path, PathBuf}; use std::sync::Arc; @@ -13,7 +14,7 @@ use fxprof_processed_profile::{ }; use wholesym::samply_symbols::debug_id_and_code_id_for_jitdump; -use super::{eventpipe_event_to_coreclr_event, CoreClrEvent}; +use super::{eventpipe_event_to_coreclr_event, CoreClrEvent, ModuleLoadUnloadEvent}; pub struct DotnetTraceManager { pending_trace_paths: Vec, @@ -122,6 +123,8 @@ struct SingleDotnetTraceProcessor { lib_mapping_ops: LibMappingOpQueue, symbols: Vec, + modules: HashMap, + /// The relative_address of the next JIT function. /// /// We define the relative address space for Jitdump files as follows: @@ -139,6 +142,7 @@ impl SingleDotnetTraceProcessor { lib_handle, lib_mapping_ops: Default::default(), symbols: Default::default(), + modules: Default::default(), cumulative_address: 0, } } @@ -203,6 +207,15 @@ impl SingleDotnetTraceProcessor { timestamp_converter: &TimestampConverter, ) { match coreclr_event { + CoreClrEvent::ModuleLoad(event) => { + let module_id = event.module_id; + self.modules.insert(module_id, event.clone()); + } + CoreClrEvent::ModuleUnload(event) => { + let module_id = event.module_id; + //if let Some(module) = self.modules.remove(&module_id) { + //} + } CoreClrEvent::MethodLoad(event) => { let start_avma = event.start_address; let end_avma = event.start_address + event.size as u64; @@ -241,8 +254,19 @@ impl SingleDotnetTraceProcessor { let (category, js_frame) = jit_category_manager.classify_jit_symbol(&symbol_name, profile); + let start_ts = if event.dc_end { + if let Some(module) = self.modules.get(&event.module_id) { + module.common.timestamp + } else { + eprintln!("Module not found: {}", event.module_id); + event.common.timestamp + } + } else { + event.common.timestamp + }; + self.lib_mapping_ops.push( - event.common.timestamp, + start_ts, LibMappingOp::Add(LibMappingAdd { start_avma, end_avma, @@ -280,6 +304,7 @@ impl SingleDotnetTraceProcessor { pub fn finish(mut self, profile: &mut Profile) -> LibMappingOpQueue { self.close_and_commit_symbol_table(profile); + self.lib_mapping_ops.sort(); self.lib_mapping_ops } } diff --git a/samply/src/shared/coreclr/eventpipe.rs b/samply/src/shared/coreclr/eventpipe.rs index ed5321cab..4b7019521 100644 --- a/samply/src/shared/coreclr/eventpipe.rs +++ b/samply/src/shared/coreclr/eventpipe.rs @@ -27,10 +27,25 @@ pub fn eventpipe_event_to_coreclr_event( match event { EventPipeEvent::ModuleLoad(event) => { - eprintln!("ModuleLoad: {:?}", event); - None + Some(CoreClrEvent::ModuleLoad(super::ModuleLoadUnloadEvent { + common, + module_id: event.module_id, + assembly_id: event.assembly_id, + app_domain_id: event.app_domain_id, + module_il_path: event.module_il_path.to_string(), + module_native_path: event.module_native_path.to_string(), + })) + } + EventPipeEvent::ModuleUnload(event) => { + Some(CoreClrEvent::ModuleUnload(super::ModuleLoadUnloadEvent { + common, + module_id: event.module_id, + assembly_id: event.assembly_id, + app_domain_id: event.app_domain_id, + module_il_path: event.module_il_path.to_string(), + module_native_path: event.module_native_path.to_string(), + })) } - EventPipeEvent::ModuleUnload(_) => None, EventPipeEvent::ReadyToRunGetEntryPoint(method) => { let name = if method.method_name.is_empty() { format!("JIT[0x{:x}]", method.entry_point) @@ -55,8 +70,53 @@ pub fn eventpipe_event_to_coreclr_event( } else { method.method_name.to_string() }; + let tier = (method.method_flags >> 7) & 0x7; + let tier = match tier { + 1 => super::MethodCompilationTier::MinOptJitted, + 2 => super::MethodCompilationTier::Optimized, + 3 => super::MethodCompilationTier::QuickJitted, + 4 => super::MethodCompilationTier::OptimizedTier1, + 5 => super::MethodCompilationTier::OptimizedTier1OSR, + 6 => super::MethodCompilationTier::InstrumentedTier, + 7 => super::MethodCompilationTier::InstrumentedTierOptimized, + _ => super::MethodCompilationTier::Unknown, + }; + + Some(CoreClrEvent::MethodLoad(super::MethodLoadEvent { + common, + module_id: method.module_id, + start_address: method.method_start_address, + size: method.method_size, + name: CoreClrMethodName { + name, + namespace: method.method_namespace.to_string(), + signature: method.method_signature.to_string(), + }, + tier, + dc_end: false, + })) + } + EventPipeEvent::MethodDCEnd(method) => { + let name = if method.method_name.is_empty() { + format!("JIT[0x{:x}]", method.method_start_address) + } else { + method.method_name.to_string() + }; + let tier = (method.method_flags >> 7) & 0x7; + let tier = match tier { + 1 => super::MethodCompilationTier::MinOptJitted, + 2 => super::MethodCompilationTier::Optimized, + 3 => super::MethodCompilationTier::QuickJitted, + 4 => super::MethodCompilationTier::OptimizedTier1, + 5 => super::MethodCompilationTier::OptimizedTier1OSR, + 6 => super::MethodCompilationTier::InstrumentedTier, + 7 => super::MethodCompilationTier::InstrumentedTierOptimized, + _ => super::MethodCompilationTier::Unknown, + }; + Some(CoreClrEvent::MethodLoad(super::MethodLoadEvent { common, + module_id: method.module_id, start_address: method.method_start_address, size: method.method_size, name: CoreClrMethodName { @@ -64,6 +124,8 @@ pub fn eventpipe_event_to_coreclr_event( namespace: method.method_namespace.to_string(), signature: method.method_signature.to_string(), }, + tier, + dc_end: true, })) } EventPipeEvent::MethodUnload(method) => { diff --git a/samply/src/shared/coreclr/events.rs b/samply/src/shared/coreclr/events.rs index 6ce4e0cc6..66b5e2d70 100644 --- a/samply/src/shared/coreclr/events.rs +++ b/samply/src/shared/coreclr/events.rs @@ -1,9 +1,13 @@ +#![allow(unused)] + pub use eventpipe::coreclr::{GcAllocationKind, GcReason, GcType}; use super::CoreClrMethodName; #[derive(Debug)] pub enum CoreClrEvent { + ModuleLoad(ModuleLoadUnloadEvent), + ModuleUnload(ModuleLoadUnloadEvent), MethodLoad(MethodLoadEvent), MethodUnload(MethodUnloadEvent), GcTriggered(GcTriggeredEvent), @@ -18,6 +22,12 @@ impl CoreClrEvent { #[allow(unused)] pub fn with_stack(mut self, stack: Vec) -> Self { match self { + CoreClrEvent::ModuleLoad(ref mut e) => { + e.common.stack = Some(stack); + } + CoreClrEvent::ModuleUnload(ref mut e) => { + e.common.stack = Some(stack); + } CoreClrEvent::MethodLoad(ref mut e) => { e.common.stack = Some(stack); } @@ -47,7 +57,7 @@ impl CoreClrEvent { } } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct EventCommon { pub timestamp: u64, pub process_id: u32, @@ -55,29 +65,54 @@ pub struct EventCommon { pub stack: Option>, } -#[derive(Debug)] +#[derive(Debug, Clone)] +pub enum MethodCompilationTier { + Unknown, + MinOptJitted, + Optimized, + QuickJitted, + OptimizedTier1, + OptimizedTier1OSR, + InstrumentedTier, + InstrumentedTierOptimized, +} + +#[derive(Debug, Clone)] +pub struct ModuleLoadUnloadEvent { + pub common: EventCommon, + pub module_id: u64, + pub assembly_id: u64, + pub app_domain_id: Option, + pub module_il_path: String, + pub module_native_path: String, +} + +#[derive(Debug, Clone)] pub struct MethodLoadEvent { pub common: EventCommon, + pub module_id: u64, pub start_address: u64, pub size: u32, pub name: CoreClrMethodName, + pub tier: MethodCompilationTier, + pub dc_end: bool, } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct MethodUnloadEvent { pub common: EventCommon, pub start_address: u64, pub size: u32, } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct GcTriggeredEvent { pub common: EventCommon, pub reason: GcReason, pub clr_instance_id: u16, } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct GcAllocationTickEvent { pub common: EventCommon, pub kind: GcAllocationKind, @@ -86,7 +121,7 @@ pub struct GcAllocationTickEvent { pub type_namespace: Option, } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct GcSampledObjectAllocationEvent { pub common: EventCommon, pub address: u64, @@ -96,7 +131,7 @@ pub struct GcSampledObjectAllocationEvent { pub total_size: u64, // total size of all objects } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct GcStartEvent { pub common: EventCommon, pub count: u32, @@ -105,7 +140,7 @@ pub struct GcStartEvent { pub gc_type: Option, } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct GcEndEvent { pub common: EventCommon, pub count: u32, @@ -113,7 +148,7 @@ pub struct GcEndEvent { pub reason: Option, } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct ReadyToRunMethodEntryPointEvent { pub common: EventCommon, pub start_address: u64, diff --git a/samply/src/shared/coreclr/provider.rs b/samply/src/shared/coreclr/provider.rs index 4e6e8fb5e..8846a488b 100644 --- a/samply/src/shared/coreclr/provider.rs +++ b/samply/src/shared/coreclr/provider.rs @@ -50,9 +50,9 @@ pub fn coreclr_provider_args(props: CoreClrProviderProps) -> Vec { // if we're attaching, ask for a rundown of method info at the start of collection let rundown_verbose_keywords = if props.is_attach { - CORECLR_LOADER_KEYWORD | CORECLR_JIT_KEYWORD | CORECLR_RUNDOWN_START_KEYWORD + CORECLR_LOADER_KEYWORD | CORECLR_JIT_KEYWORD | CORECLR_NGEN_KEYWORD | CORECLR_RUNDOWN_START_KEYWORD } else { - 0 + CORECLR_JIT_KEYWORD | CORECLR_NGEN_KEYWORD }; if props.gc_detailed_allocs { diff --git a/samply/src/shared/lib_mappings.rs b/samply/src/shared/lib_mappings.rs index 7e8aa4325..729c27713 100644 --- a/samply/src/shared/lib_mappings.rs +++ b/samply/src/shared/lib_mappings.rs @@ -139,6 +139,10 @@ impl LibMappingOpQueue { pub fn into_iter(self) -> LibMappingOpQueueIter { LibMappingOpQueueIter(self.0.into_iter().peekable()) } + + pub fn sort(&mut self) { + self.0.sort_by_key(|(timestamp, _)| *timestamp); + } } pub struct LibMappingOpQueueIter(Peekable>); From fdaaef70e158a256298586127168e9d388577bb7 Mon Sep 17 00:00:00 2001 From: Vladimir Vukicevic Date: Thu, 18 Jul 2024 11:11:14 -0700 Subject: [PATCH 03/36] Capture dotnet trace events on mac with --coreclr --- Cargo.lock | 1 + eventpipe-rs/Cargo.toml | 1 + eventpipe-rs/src/coreclr_events.rs | 5 +++ eventpipe-rs/src/lib.rs | 4 +- samply/src/mac/process_launcher.rs | 26 +++++++++++-- samply/src/mac/profiler.rs | 37 +++++++++++++++---- .../shared/coreclr/dotnet_trace_manager.rs | 12 ++++-- samply/src/shared/recording_props.rs | 12 ++++++ 8 files changed, 82 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b8c2edd82..b8596ea74 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -643,6 +643,7 @@ version = "0.1.0" dependencies = [ "binrw", "bitflags 2.5.0", + "log", "num-derive", "num-traits", ] diff --git a/eventpipe-rs/Cargo.toml b/eventpipe-rs/Cargo.toml index 43eb1a3cd..cd2b242db 100644 --- a/eventpipe-rs/Cargo.toml +++ b/eventpipe-rs/Cargo.toml @@ -8,6 +8,7 @@ binrw = "0.13.3" bitflags = "2.4.2" num-traits = "0.2" num-derive = "0.4" +log = "0.4.21" [[example]] name = "dump-nettrace" \ No newline at end of file diff --git a/eventpipe-rs/src/coreclr_events.rs b/eventpipe-rs/src/coreclr_events.rs index a18c14a72..f1b9f78bd 100644 --- a/eventpipe-rs/src/coreclr_events.rs +++ b/eventpipe-rs/src/coreclr_events.rs @@ -330,10 +330,15 @@ fn decode_coreclr_regular_event(event: &NettraceEvent) -> Option { // 85: ThreadCreated // 86: ThreadTerminated // 87: ThreadDomainEnter + // 145: MethodJittingStarted + // 146: MethodJitMemoryAllocatedForCode // 154: AssemblyLoad // 155: AssemblyUnload // 156: AppDomainLoad // 157: AppDomainUnload + // 160: R2RGetEntryPointStart + // 187: RuntimeInformationStart + // 190: MethodILToNativeMap _ => None, } } diff --git a/eventpipe-rs/src/lib.rs b/eventpipe-rs/src/lib.rs index 297c0cc67..889ffb106 100644 --- a/eventpipe-rs/src/lib.rs +++ b/eventpipe-rs/src/lib.rs @@ -93,7 +93,7 @@ pub struct NettraceTraceObject { sync_time_utc: NettraceTime, sync_time_qpc: u64, qpc_frequency: u64, - pointer_suze: u32, + pointer_size: u32, process_id: u32, number_of_processors: u32, expected_cpu_sampling_rate: u32, @@ -629,7 +629,7 @@ where match obj_type_name { "Trace" => { let trace_object = NettraceTraceObject::read(&mut self.stream)?; - eprintln!("Trace: {:?}", trace_object); + log::trace!("Trace: {:?}", trace_object); self.trace_info = Some(trace_object.clone()); } "MetadataBlock" => { diff --git a/samply/src/mac/process_launcher.rs b/samply/src/mac/process_launcher.rs index 4782a1945..234690ded 100644 --- a/samply/src/mac/process_launcher.rs +++ b/samply/src/mac/process_launcher.rs @@ -19,7 +19,7 @@ pub use super::mach_ipc::{mach_port_t, MachError, OsIpcSender}; use super::mach_ipc::{mach_task_self, BlockingMode, OsIpcMultiShotServer, MACH_PORT_NULL}; pub trait RootTaskRunner { - fn run_root_task(&self) -> Result; + fn run_root_task(&mut self) -> Result; } pub struct TaskLauncher { @@ -30,7 +30,7 @@ pub struct TaskLauncher { } impl RootTaskRunner for TaskLauncher { - fn run_root_task(&self) -> Result { + fn run_root_task(&mut self) -> Result { // Ignore Ctrl+C while the subcommand is running. The signal still reaches the process // under observation while we continue to record it. (ctrl+c will send the SIGINT signal // to all processes in the foreground process group). @@ -266,10 +266,11 @@ impl AcceptedTask { pub struct ExistingProcessRunner { pid: u32, + aux_child: Option, } impl RootTaskRunner for ExistingProcessRunner { - fn run_root_task(&self) -> Result { + fn run_root_task(&mut self) -> Result { let ctrl_c_receiver = CtrlC::observe_oneshot(); eprintln!("Profiling {}, press Ctrl-C to stop...", self.pid); @@ -278,6 +279,14 @@ impl RootTaskRunner for ExistingProcessRunner { .blocking_recv() .expect("Ctrl+C receiver failed"); + if let Some(aux_child) = self.aux_child.as_mut() { + let aux_pid = aux_child.id(); + unsafe { + libc::kill(aux_pid as i32, libc::SIGINT); + } + aux_child.wait().expect("Failed to wait on aux child process"); + } + eprintln!("Done."); Ok(ExitStatus::default()) @@ -318,6 +327,15 @@ impl ExistingProcessRunner { // TODO: find all its children - ExistingProcessRunner { pid } + ExistingProcessRunner { pid, aux_child: None } + } + + pub fn new_with_aux_child(pid: u32, task_accepter: &mut TaskAccepter, aux_child: Child) -> ExistingProcessRunner { + let runner = Self::new(pid, task_accepter); + + ExistingProcessRunner { + aux_child: Some(aux_child), + ..runner + } } } diff --git a/samply/src/mac/profiler.rs b/samply/src/mac/profiler.rs index 62b662d87..7dc65109c 100644 --- a/samply/src/mac/profiler.rs +++ b/samply/src/mac/profiler.rs @@ -2,7 +2,8 @@ use std::collections::hash_map::Entry; use std::collections::HashMap; use std::fs::File; use std::io::BufWriter; -use std::process::ExitStatus; +use std::path::PathBuf; +use std::process::{ExitStatus, Stdio}; use std::thread; use std::time::Duration; @@ -33,7 +34,7 @@ pub fn start_recording( let mut task_accepter = TaskAccepter::new()?; - let root_task_runner: Box = match recording_mode { + let mut root_task_runner: Box = match recording_mode { RecordingMode::All => { eprintln!("Error: Profiling all processes is not supported on macOS."); eprintln!("You can only profile processes which you launch via samply, or attach to via --pid."); @@ -42,7 +43,32 @@ pub fn start_recording( RecordingMode::Pid(pid) => { profile_name = format!("pid {pid}"); - Box::new(ExistingProcessRunner::new(pid, &mut task_accepter)) + if profile_creation_props.coreclr.any_enabled() { + let coreclr_props = CoreClrProviderProps { + is_attach: true, + ..profile_creation_props.coreclr.to_provider_props() + }; + + let provider_args = crate::shared::coreclr::coreclr_provider_args(coreclr_props); + + let child = std::process::Command::new("dotnet-trace") + .arg("collect") + .arg("-p") + .arg(pid.to_string()) + .arg("-o") + .arg("profile.dottrace") + .arg("--providers") + .arg(&provider_args.join(",")) + .stdout(Stdio::null()) + .spawn() + .expect("Failed to spawn dotnet-trace"); + + task_accepter.queue_received_stuff(ReceivedStuff::DotnetTracePath(pid, PathBuf::from("profile.dottrace"))); + + Box::new(ExistingProcessRunner::new_with_aux_child(pid, &mut task_accepter, child)) + } else { + Box::new(ExistingProcessRunner::new(pid, &mut task_accepter)) + } } RecordingMode::Launch(process_launch_props) => { profile_name = process_launch_props @@ -68,11 +94,8 @@ pub fn start_recording( //} let coreclr_props = CoreClrProviderProps { - event_stacks: profile_creation_props.coreclr.event_stacks, - gc_markers: profile_creation_props.coreclr.gc_markers, - gc_suspensions: profile_creation_props.coreclr.gc_suspensions, - gc_detailed_allocs: profile_creation_props.coreclr.gc_detailed_allocs, is_attach: false, + ..profile_creation_props.coreclr.to_provider_props() }; let provider_args = crate::shared::coreclr::coreclr_provider_args(coreclr_props); diff --git a/samply/src/shared/coreclr/dotnet_trace_manager.rs b/samply/src/shared/coreclr/dotnet_trace_manager.rs index 85685099d..df9bb557a 100644 --- a/samply/src/shared/coreclr/dotnet_trace_manager.rs +++ b/samply/src/shared/coreclr/dotnet_trace_manager.rs @@ -193,7 +193,13 @@ impl SingleDotnetTraceProcessor { self.close_and_commit_symbol_table(profile); return; } - _ => {} + Err(err) => { + eprintln!("Got error: {:?}", err); + self.lib_mapping_ops + .push(last_timestamp, LibMappingOp::Clear); + self.close_and_commit_symbol_table(profile); + return; + } } } } @@ -247,7 +253,7 @@ impl SingleDotnetTraceProcessor { (self.lib_handle, relative_address_at_start) }; - eprintln!( + log::trace!( "MethodLoad: addr = 0x{:x} symbol_name = {:?} size = {}", start_avma, symbol_name, event.size ); @@ -258,7 +264,7 @@ impl SingleDotnetTraceProcessor { if let Some(module) = self.modules.get(&event.module_id) { module.common.timestamp } else { - eprintln!("Module not found: {}", event.module_id); + log::trace!("Module already unloaded {}, using event timestamp...", event.module_id); event.common.timestamp } } else { diff --git a/samply/src/shared/recording_props.rs b/samply/src/shared/recording_props.rs index 4f3e7d557..6f45dea5f 100644 --- a/samply/src/shared/recording_props.rs +++ b/samply/src/shared/recording_props.rs @@ -4,6 +4,8 @@ use std::time::Duration; use serde_derive::{Deserialize, Serialize}; +use super::coreclr::CoreClrProviderProps; + #[derive(Debug, Default, Clone, Copy, Serialize, Deserialize)] pub struct CoreClrProfileProps { pub enabled: bool, @@ -21,6 +23,16 @@ impl CoreClrProfileProps { || self.gc_detailed_allocs || self.event_stacks } + + pub fn to_provider_props(&self) -> CoreClrProviderProps { + CoreClrProviderProps { + event_stacks: self.event_stacks, + gc_markers: self.gc_markers, + gc_suspensions: self.gc_suspensions, + gc_detailed_allocs: self.gc_detailed_allocs, + is_attach: false, + } + } } /// Properties which are meaningful both for recording a fresh process From 6fa1f9e6afa54244662ac4e6c1526336a3909dd3 Mon Sep 17 00:00:00 2001 From: Vladimir Vukicevic Date: Thu, 18 Jul 2024 11:16:51 -0700 Subject: [PATCH 04/36] Add events to dump-nettrace --- eventpipe-rs/examples/dump-nettrace.rs | 28 +++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/eventpipe-rs/examples/dump-nettrace.rs b/eventpipe-rs/examples/dump-nettrace.rs index eb83b92aa..b74a628cf 100644 --- a/eventpipe-rs/examples/dump-nettrace.rs +++ b/eventpipe-rs/examples/dump-nettrace.rs @@ -1,8 +1,8 @@ #![allow(unused)] use std::fs::File; -use coreclr_events::CoreClrEvent; use eventpipe::*; +use eventpipe::coreclr::CoreClrEvent; // https://github.com/microsoft/perfview/blob/main/src/TraceEvent/EventPipe/EventPipeFormat.md @@ -15,9 +15,9 @@ fn main() { loop { match reader.next_event() { Ok(Some(event)) => { - if event.provider_name == "Microsoft-DotNETCore-SampleProfiler" { - continue; - } + //if event.provider_name == "Microsoft-DotNETCore-SampleProfiler" { + // continue; + //} match eventpipe::decode_event(&event) { DecodedEvent::CoreClrEvent(coreclr_event) => { @@ -36,10 +36,28 @@ fn main() { CoreClrEvent::GcAllocationTick(event) => { //println!("GcAllocationTick: {:?}", event); } + CoreClrEvent::ModuleLoad(event) => { + println!("ModuleLoad: {:?}", event); + } + CoreClrEvent::ModuleUnload(event) => { + println!("ModuleUnload: {:?}", event); + } + CoreClrEvent::MethodUnload(event) => { + println!("MethodUnload: {:?}", event); + } + CoreClrEvent::GcSampledObjectAllocation(event) => { + println!("GcSampledObjectAllocation: {:?}", event); + } + CoreClrEvent::ReadyToRunGetEntryPoint(event) => { + println!("ReadyToRunGetEntryPoint: {:?}", event); + } + CoreClrEvent::MethodDCEnd(event) => { + println!("MethodDCEnd: {:?}", event); + } } } DecodedEvent::UnknownEvent => { - //println!("Unknown event: {} / {}", event.provider_name, event.event_id); + println!("Unknown: {} / {}", event.provider_name, event.event_id); } } From 90d07c427c098cd28e57edf262c5fd5a74c3fd83 Mon Sep 17 00:00:00 2001 From: Vladimir Vukicevic Date: Mon, 29 Jul 2024 12:54:40 -0700 Subject: [PATCH 05/36] Update eventpipe and coreclr code for new samply changes --- Cargo.lock | 490 +++++++++++------- eventpipe-rs/examples/dump-nettrace.rs | 29 +- samply/src/mac/codesign_setup.rs | 22 +- samply/src/mac/profiler.rs | 7 +- samply/src/mac/task_profiler.rs | 7 +- samply/src/main.rs | 13 +- samply/src/server.rs | 2 +- .../shared/coreclr/dotnet_trace_manager.rs | 105 ++-- samply/src/shared/coreclr/eventpipe.rs | 74 +-- samply/src/shared/coreclr/markers.rs | 250 +++++---- samply/src/shared/lib_mappings.rs | 14 +- samply/src/windows/etw_coreclr.rs | 9 +- samply/src/windows/etw_gecko.rs | 21 +- samply/src/windows/profile_context.rs | 20 +- 14 files changed, 605 insertions(+), 458 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b8596ea74..60a5f5821 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,12 +13,12 @@ dependencies = [ [[package]] name = "addr2line" -version = "0.22.0" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +checksum = "7b9d03130b08257bc8110b0df827d8b137fdf67a95e2459eaace2e13fecf1d72" dependencies = [ - "fallible-iterator 0.3.0", - "gimli 0.29.0", + "fallible-iterator", + "gimli 0.30.0", ] [[package]] @@ -240,28 +240,16 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" -[[package]] -name = "bitvec" -version = "0.19.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55f93d0ef3363c364d5976646a38f04cf67cfe1d4c8d160cdea02cab2c116b33" -dependencies = [ - "funty 1.1.0", - "radium 0.5.3", - "tap", - "wyz 0.2.0", -] - [[package]] name = "bitvec" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" dependencies = [ - "funty 2.0.0", - "radium 0.7.0", + "funty", + "radium", "tap", - "wyz 0.5.1", + "wyz", ] [[package]] @@ -316,9 +304,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.6.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" +checksum = "a12916984aab3fa6e39d655a33e09c0071eb36d6ab3aea5c2d78551f1df6d952" [[package]] name = "bytesize" @@ -377,11 +365,17 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "clap" -version = "4.5.4" +version = "4.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" +checksum = "64acc1846d54c1fe936a78dc189c34e28d3f5afc348403f28ecf53660b9b8462" dependencies = [ "clap_builder", "clap_derive", @@ -389,9 +383,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.2" +version = "4.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" +checksum = "6fb8393d67ba2e7bfaf28a23458e4e2b543cc73a99595511eb207fdb8aede942" dependencies = [ "anstream", "anstyle", @@ -401,9 +395,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.4" +version = "4.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" +checksum = "2bac35c6dafb060fd4d275d9a4ffae97917c13a6327903a8be2153cd964f7085" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -493,7 +487,7 @@ version = "3.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "672465ae37dc1bc6380a6547a8883d5dd397b0f1faaad4f265726cc7042a5345" dependencies = [ - "nix", + "nix 0.28.0", "windows-sys 0.52.0", ] @@ -516,23 +510,22 @@ dependencies = [ ] [[package]] -name = "derive_more" -version = "0.99.17" +name = "dirs" +version = "5.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", + "dirs-sys", ] [[package]] -name = "dirs" -version = "5.0.1" +name = "dirs-next" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +checksum = "cf36e65a80337bea855cd4ef9b8401ffce06a7baedf2e85ec467b1ac3f6e82b6" dependencies = [ - "dirs-sys", + "cfg-if", + "dirs-sys-next", ] [[package]] @@ -547,6 +540,17 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + [[package]] name = "dump-table" version = "0.1.0" @@ -634,7 +638,7 @@ dependencies = [ "num-derive", "num-traits", "once_cell", - "windows", + "windows 0.56.0", ] [[package]] @@ -648,12 +652,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "fallible-iterator" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" - [[package]] name = "fallible-iterator" version = "0.3.0" @@ -705,18 +703,16 @@ dependencies = [ [[package]] name = "framehop" -version = "0.11.2" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1940574e932d1ed75aab25420312c0019d8dd91c6125bd51420272cd072008e" +checksum = "0fd28d2036d4fd99e3629487baca659e5af1c5d554e320168613be79028610fc" dependencies = [ "arrayvec", "cfg-if", - "fallible-iterator 0.3.0", - "gimli 0.29.0", + "fallible-iterator", + "gimli 0.30.0", "macho-unwind-info", "pe-unwind-info", - "thiserror", - "thiserror-no-std", ] [[package]] @@ -729,12 +725,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "funty" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7" - [[package]] name = "funty" version = "2.0.0" @@ -882,11 +872,11 @@ checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "gimli" -version = "0.29.0" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" +checksum = "e2e1d97fbe9722ba9bbd0c97051c2956e726562b61f86a25a4360398a40edfc9" dependencies = [ - "fallible-iterator 0.3.0", + "fallible-iterator", "stable_deref_trait", ] @@ -999,9 +989,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "1.3.1" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d" +checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" dependencies = [ "bytes", "futures-channel", @@ -1037,9 +1027,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.3" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa" +checksum = "3ab92f4f49ee4fb4f997c784b7a2e0fa70050211e0b6a287f898c3c9785ca956" dependencies = [ "bytes", "futures-channel", @@ -1122,9 +1112,9 @@ dependencies = [ [[package]] name = "lazy_static" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" @@ -1313,7 +1303,19 @@ checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" dependencies = [ "bitflags 2.5.0", "cfg-if", - "cfg_aliases", + "cfg_aliases 0.1.1", + "libc", +] + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.5.0", + "cfg-if", + "cfg_aliases 0.2.1", "libc", ] @@ -1389,9 +1391,9 @@ dependencies = [ [[package]] name = "object" -version = "0.35.0" +version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8ec7ab813848ba4522158d5517a6093db1ded27575b070f4177b8d12b41db5e" +checksum = "081b846d1d56ddfc18fdf1a922e4f6e07a11768ea1b92dec44e42b72712ccfce" dependencies = [ "crc32fast", "flate2", @@ -1424,6 +1426,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "os-release" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f29ae2f71b53ec19cc23385f8e4f3d90975195aa3d09171ba3bef7159bec27" +dependencies = [ + "lazy_static", +] + [[package]] name = "owo-colors" version = "3.5.0" @@ -1450,7 +1461,7 @@ dependencies = [ "libc", "redox_syscall 0.5.1", "smallvec", - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -1469,9 +1480,9 @@ dependencies = [ [[package]] name = "pdb2" -version = "0.9.0" +version = "0.9.1" dependencies = [ - "fallible-iterator 0.2.0", + "fallible-iterator", "scroll", "uuid", ] @@ -1533,6 +1544,28 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +[[package]] +name = "platform-dirs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e188d043c1a692985f78b5464853a263f1a27e5bd6322bad3a4078ee3c998a38" +dependencies = [ + "dirs-next", +] + +[[package]] +name = "plist" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016" +dependencies = [ + "base64", + "indexmap", + "quick-xml", + "serde", + "time", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -1588,6 +1621,15 @@ dependencies = [ "serde_json", ] +[[package]] +name = "quick-xml" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d3a6e5838b60e0e8fa7a43f22ade549a37d61f8bdbe636d0d7816191de969c2" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "1.0.36" @@ -1597,12 +1639,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "radium" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8" - [[package]] name = "radium" version = "0.7.0" @@ -1869,12 +1905,11 @@ dependencies = [ [[package]] name = "ruzstd" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5174a470eeb535a721ae9fdd6e291c2411a906b96592182d05217591d5c5cf7b" +checksum = "5022b253619b1ba797f243056276bed8ed1a73b0f5a7ce7225d524067644bf8f" dependencies = [ "byteorder", - "derive_more", "twox-hash", ] @@ -1895,7 +1930,7 @@ dependencies = [ [[package]] name = "samply-api" -version = "0.23.0" +version = "0.23.1" dependencies = [ "anyhow", "assert-json-diff", @@ -1913,19 +1948,19 @@ dependencies = [ [[package]] name = "samply-symbols" -version = "0.22.0" +version = "0.23.0" dependencies = [ - "addr2line 0.22.0", + "addr2line 0.23.0", "anyhow", "bitflags 2.5.0", - "bitvec 1.0.1", + "bitvec", "bytesize", "cpp_demangle", "debugid", "elsa", "flate2", "futures", - "gimli 0.29.0", + "gimli 0.30.0", "linux-perf-data", "lzma-rs", "macho-unwind-info", @@ -1933,7 +1968,7 @@ dependencies = [ "memmap2", "msvc-demangler", "nom", - "object 0.35.0", + "object 0.36.1", "pdb-addr2line", "rangemap", "rustc-demangle", @@ -1961,9 +1996,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "scroll" -version = "0.11.0" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04c565b551bafbef4157586fa379538366e4385d42082f255bfd96e4fe8519da" +checksum = "6ab8598aa408498679922eff7fa985c25d58a90771bd6be794434c5277eab1a6" [[package]] name = "security-framework-sys" @@ -1977,18 +2012,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.202" +version = "1.0.204" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395" +checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.202" +version = "1.0.204" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838" +checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" dependencies = [ "proc-macro2", "quote", @@ -1997,9 +2032,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.117" +version = "1.0.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" +checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5" dependencies = [ "itoa", "ryu", @@ -2018,6 +2053,12 @@ dependencies = [ "serde", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "slab" version = "0.4.9" @@ -2162,9 +2203,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tar" -version = "0.4.40" +version = "0.4.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b16afcea1f22891c49a00c751c7b63b2233284064f11a200fc624137c51e2ddb" +checksum = "cb797dad5fb5b76fcf519e702f4a589483b5ef06567f160c392832c1f5e44909" dependencies = [ "filetime", "libc", @@ -2185,44 +2226,24 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.61" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.61" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", "syn 2.0.64", ] -[[package]] -name = "thiserror-impl-no-std" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58e6318948b519ba6dc2b442a6d0b904ebfb8d411a3ad3e07843615a72249758" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "thiserror-no-std" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3ad459d94dd517257cc96add8a43190ee620011bb6e6cdc82dafd97dfafafea" -dependencies = [ - "thiserror-impl-no-std", -] - [[package]] name = "time" version = "0.3.36" @@ -2230,10 +2251,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", + "itoa", "num-conv", "powerfmt", "serde", "time-core", + "time-macros", ] [[package]] @@ -2242,6 +2265,16 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -2259,9 +2292,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.37.0" +version = "1.38.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" +checksum = "eb2caba9f80616f438e09748d5acda951967e1ea58508ef53d9c6402485a46df" dependencies = [ "backtrace", "bytes", @@ -2276,9 +2309,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" dependencies = [ "proc-macro2", "quote", @@ -2322,7 +2355,6 @@ dependencies = [ "tokio", "tower-layer", "tower-service", - "tracing", ] [[package]] @@ -2343,7 +2375,6 @@ version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ - "log", "pin-project-lite", "tracing-core", ] @@ -2422,7 +2453,7 @@ dependencies = [ [[package]] name = "usamply" -version = "0.12.8" +version = "0.12.9" dependencies = [ "bitflags 2.5.0", "byteorder", @@ -2431,7 +2462,6 @@ dependencies = [ "crossbeam-channel", "ctrlc", "debugid", - "dirs", "env_logger", "etw-reader", "eventpipe", @@ -2442,6 +2472,7 @@ dependencies = [ "fxhash", "fxprof-processed-profile", "http-body-util", + "humantime", "hyper", "hyper-util", "lazy_static", @@ -2453,22 +2484,26 @@ dependencies = [ "memmap2", "memoffset", "mio", - "nix", + "nix 0.29.0", "nix-base32", "num-derive", "num-traits", "num_cpus", - "object 0.35.0", + "object 0.36.1", "once_cell", "opener", + "os-release", "parking_lot", "percent-encoding", + "platform-dirs", + "plist", "rand", "rangemap", "runas", "serde", "serde_derive", "serde_json", + "shlex", "sysctl", "tempfile", "thiserror", @@ -2478,7 +2513,8 @@ dependencies = [ "uuid", "which 6.0.1", "wholesym", - "windows", + "windows 0.58.0", + "winver", ] [[package]] @@ -2489,9 +2525,9 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "uuid" -version = "1.8.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" +checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" dependencies = [ "getrandom", ] @@ -2651,7 +2687,7 @@ dependencies = [ [[package]] name = "wholesym" -version = "0.5.0" +version = "0.7.0" dependencies = [ "bytes", "core-foundation", @@ -2680,6 +2716,22 @@ dependencies = [ "wholesym", ] +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + [[package]] name = "winapi-util" version = "0.1.8" @@ -2689,14 +2741,39 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows" version = "0.56.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1de69df01bdf1ead2f4ac895dc77c9351aefff65b2f3db429a343f9cbf05e132" dependencies = [ - "windows-core", - "windows-targets 0.52.5", + "windows-core 0.56.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +dependencies = [ + "windows-core 0.58.0", + "windows-targets 0.52.6", ] [[package]] @@ -2705,10 +2782,23 @@ version = "0.56.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4698e52ed2d08f8658ab0c39512a7c00ee5fe2688c65f8c0a4f06750d729f2a6" dependencies = [ - "windows-implement", - "windows-interface", - "windows-result", - "windows-targets 0.52.5", + "windows-implement 0.56.0", + "windows-interface 0.56.0", + "windows-result 0.1.1", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +dependencies = [ + "windows-implement 0.58.0", + "windows-interface 0.58.0", + "windows-result 0.2.0", + "windows-strings", + "windows-targets 0.52.6", ] [[package]] @@ -2722,6 +2812,17 @@ dependencies = [ "syn 2.0.64", ] +[[package]] +name = "windows-implement" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.64", +] + [[package]] name = "windows-interface" version = "0.56.0" @@ -2733,13 +2834,43 @@ dependencies = [ "syn 2.0.64", ] +[[package]] +name = "windows-interface" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.64", +] + [[package]] name = "windows-result" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "749f0da9cc72d82e600d8d2e44cadd0b9eedb9038f71a1c58556ac1c5791813b" dependencies = [ - "windows-targets 0.52.5", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result 0.2.0", + "windows-targets 0.52.6", ] [[package]] @@ -2757,7 +2888,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -2777,18 +2908,18 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.5", - "windows_aarch64_msvc 0.52.5", - "windows_i686_gnu 0.52.5", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", - "windows_i686_msvc 0.52.5", - "windows_x86_64_gnu 0.52.5", - "windows_x86_64_gnullvm 0.52.5", - "windows_x86_64_msvc 0.52.5", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] @@ -2799,9 +2930,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" @@ -2811,9 +2942,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" @@ -2823,15 +2954,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" @@ -2841,9 +2972,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" @@ -2853,9 +2984,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" @@ -2865,9 +2996,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" @@ -2877,9 +3008,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winreg" @@ -2898,10 +3029,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" [[package]] -name = "wyz" -version = "0.2.0" +name = "winver" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" +checksum = "9e0e7162b9e282fd75a0a832cce93994bdb21208d848a418cd05a5fdd9b9ab33" +dependencies = [ + "windows 0.48.0", +] [[package]] name = "wyz" @@ -2925,28 +3059,28 @@ dependencies = [ [[package]] name = "yaxpeax-arch" -version = "0.2.8" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f005c964432a1f9ee04598e094a3eb5f7568f6b33e89a2762d7bef6fbe8b255" +checksum = "36274fcc5403da2a7636ffda4d02eca12a1b2b8267b9d2e04447bd2ccfc72082" dependencies = [ "num-traits", ] [[package]] name = "yaxpeax-arm" -version = "0.2.5" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0430c0047803b6aabfa3cb62f84a78d05b933e62bfcee97c7491bf634df9123" +checksum = "e1c6a2af41f88546a08df3bc77aadf7263884d6dffdac5b32dea7dc2df23f241" dependencies = [ - "bitvec 0.19.6", + "bitvec", "yaxpeax-arch", ] [[package]] name = "yaxpeax-x86" -version = "1.2.2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9107477944697db42c41326f82d4c65b769b32512cdad1e086f36f0e0f25ff45" +checksum = "9a9a30b7dd533c7b1a73eaf7c4ea162a7a632a2bb29b9fff47d8f2cc8513a883" dependencies = [ "cfg-if", "num-traits", diff --git a/eventpipe-rs/examples/dump-nettrace.rs b/eventpipe-rs/examples/dump-nettrace.rs index b74a628cf..8b7a93344 100644 --- a/eventpipe-rs/examples/dump-nettrace.rs +++ b/eventpipe-rs/examples/dump-nettrace.rs @@ -57,7 +57,34 @@ fn main() { } } DecodedEvent::UnknownEvent => { - println!("Unknown: {} / {}", event.provider_name, event.event_id); + let mut handled = false; + + if event.provider_name == "Microsoft-Windows-DotNETRuntime" { + handled = true; + match event.event_id { + 145 => println!("MethodJittingStarted [Unhandled]"), + 146 => println!("MemoryAllocatedForJitCode [Unhandled]"), + _ => handled = false, + } + } else if event.provider_name == "Microsoft-Windows-DotNETRuntimeRundown" { + handled = true; + match event.event_id { + 10 => println!("Rundown: GCSettingsRundown [Unhandled]"), + 146 => println!("Rundown: DCEndComplete [Unhandled] @ {}", event.timestamp), + 148 => println!("Rundown: DCEndInit [Unhandled] @ {}", event.timestamp), + 150 => println!("Rundown: MethodDCEndILToNativeMap_V1 [Unhandled]"), + 152 => println!("Rundown: DomainModuleDCEnd [Unhandled]"), + 154 => println!("Rundown: ModuleDCEnd [Unhandled] @ {}", event.timestamp), + 156 => println!("Rundown: AssemblyDCEnd [Unhandled]"), + 158 => println!("Rundown: AppDomainDCEnd [Unhandled]"), + 187 => println!("Rundown: RuntimeInformationDCStart [Unhandled]"), + _ => handled = false, + } + } + + if !handled { + println!("Unknown: {} / {}", event.provider_name, event.event_id); + } } } diff --git a/samply/src/mac/codesign_setup.rs b/samply/src/mac/codesign_setup.rs index 418116d58..6af3b09b1 100644 --- a/samply/src/mac/codesign_setup.rs +++ b/samply/src/mac/codesign_setup.rs @@ -10,9 +10,10 @@ const ENTITLEMENTS_XML: &str = r#" "#; -pub fn codesign_setup() { - print!( - r#" +pub fn codesign_setup(skip_prompt: bool) { + if !skip_prompt { + print!( + r#" On macOS, attaching to an existing process is only allowed to binaries with the com.apple.security.cs.debugger entitlement. The samply binary will be signed with this entitlement for your local machine only. The following command @@ -27,14 +28,15 @@ entitlements.xml contains: Press any key to continue, or Ctrl-C to cancel. "#, - env::current_exe().unwrap().display(), - ENTITLEMENTS_XML - ); + env::current_exe().unwrap().display(), + ENTITLEMENTS_XML + ); - let mut input = String::new(); - std::io::stdin() - .read_line(&mut input) - .expect("Failed to read input?"); + let mut input = String::new(); + std::io::stdin() + .read_line(&mut input) + .expect("Failed to read input?"); + } let mut entitlements_file = tempfile::Builder::new() .prefix("samply_entitlements") diff --git a/samply/src/mac/profiler.rs b/samply/src/mac/profiler.rs index 97f3096a5..16c39205a 100644 --- a/samply/src/mac/profiler.rs +++ b/samply/src/mac/profiler.rs @@ -1,7 +1,8 @@ use std::collections::hash_map::Entry; use std::collections::HashMap; use std::fs::File; -use std::process::ExitStatus; +use std::path::PathBuf; +use std::process::{ExitStatus, Stdio}; use std::thread; use std::time::Duration; @@ -38,9 +39,7 @@ pub fn start_recording( eprintln!("You can only profile processes which you launch via samply, or attach to via --pid."); std::process::exit(1) } - RecordingMode::Pid(pid: u32) => { - profile_name = format!("pid {pid}"); - + RecordingMode::Pid(pid) => { if profile_creation_props.coreclr.any_enabled() { let coreclr_props = CoreClrProviderProps { is_attach: true, diff --git a/samply/src/mac/task_profiler.rs b/samply/src/mac/task_profiler.rs index 255263cc0..2d245c11d 100644 --- a/samply/src/mac/task_profiler.rs +++ b/samply/src/mac/task_profiler.rs @@ -615,11 +615,7 @@ impl TaskProfiler { .push((thread_handle, marker_file_path)); } ProcessSpecificPath::DotnetTracePath(dotnet_trace_path) => { - self.dotnet_trace_manager.add_dotnet_trace_path( - self.main_thread_handle, - dotnet_trace_path, - None, - ); + self.dotnet_trace_manager.add_dotnet_trace_path(dotnet_trace_path); } } } @@ -676,7 +672,6 @@ impl TaskProfiler { let dotnet_lib_ops = self.dotnet_trace_manager.finish( jit_category_manager, profile, - self.jit_function_recycler.as_mut(), &self.timestamp_converter, ); diff --git a/samply/src/main.rs b/samply/src/main.rs index f285b1e51..86193ff79 100644 --- a/samply/src/main.rs +++ b/samply/src/main.rs @@ -95,7 +95,7 @@ enum Action { /// Codesign the samply binary on macOS to allow attaching to processes. #[cfg(target_os = "macos")] - Setup, + Setup(SetupArgs), } #[derive(Debug, Args)] @@ -333,6 +333,13 @@ struct SymbolArgs { simpleperf_binary_cache: Option, } +#[derive(Debug, Args, Clone)] +pub struct SetupArgs { + /// Don't wait for confirmation to codesign. + #[arg(short = 'y', long)] + yes: bool, +} + #[derive(Debug, Args, Clone)] pub struct ProfileCreationArgs { /// Set a custom name for the recorded profile. @@ -491,8 +498,8 @@ fn main() { } #[cfg(target_os = "macos")] - Action::Setup => { - mac::codesign_setup::codesign_setup(); + Action::Setup(SetupArgs { yes }) => { + mac::codesign_setup::codesign_setup(yes); } } } diff --git a/samply/src/server.rs b/samply/src/server.rs index 1114d8899..8d4285537 100644 --- a/samply/src/server.rs +++ b/samply/src/server.rs @@ -148,7 +148,7 @@ async fn start_server( Some(s) => s.trim_end_matches('/'), //None => "https://profiler.firefox.com", // At some point a real URL - None => "https://profiler-dot-unity-eng-arch-dev.uw.r.appspot.com/", + None => "https://profiler.cds.internal.unity3d.com/", }; let encoded_profile_url = utf8_percent_encode(&profile_url, BAD_CHARS).to_string(); diff --git a/samply/src/shared/coreclr/dotnet_trace_manager.rs b/samply/src/shared/coreclr/dotnet_trace_manager.rs index df9bb557a..49d1f2a90 100644 --- a/samply/src/shared/coreclr/dotnet_trace_manager.rs +++ b/samply/src/shared/coreclr/dotnet_trace_manager.rs @@ -1,9 +1,8 @@ -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::path::{Path, PathBuf}; use std::sync::Arc; use crate::shared::jit_category_manager::JitCategoryManager; -use crate::shared::jit_function_recycler::JitFunctionRecycler; use crate::shared::lib_mappings::{LibMappingAdd, LibMappingInfo, LibMappingOp, LibMappingOpQueue}; use crate::shared::timestamp_converter::TimestampConverter; use debugid::CodeId; @@ -33,12 +32,10 @@ impl DotnetTraceManager { pub fn add_dotnet_trace_path( &mut self, - thread: ThreadHandle, path: impl Into, - fallback_dir: Option, ) { let path: PathBuf = path.into(); - eprintln!("Adding dotnet trace path: {:?}", path); + log::info!("Adding dotnet trace path: {:?}", path); self.pending_trace_paths.push(path); } @@ -46,7 +43,6 @@ impl DotnetTraceManager { &mut self, jit_category_manager: &mut JitCategoryManager, profile: &mut Profile, - mut recycler: Option<&mut JitFunctionRecycler>, timestamp_converter: &TimestampConverter, ) { self.pending_trace_paths.retain_mut(|path| { @@ -54,14 +50,14 @@ impl DotnetTraceManager { path: &Path, unlink_after_open: bool, ) -> Option<(EventPipeParser, PathBuf)> { - let mut file = std::fs::File::open(path).ok()?; - let mut reader = EventPipeParser::new(file).ok()?; + let file = std::fs::File::open(path).ok()?; + let reader = EventPipeParser::new(file).ok()?; if unlink_after_open { std::fs::remove_file(&path).ok()?; } Some((reader, path.into())) } - let Some((reader, actual_path)) = trace_reader_for_path(path, self.unlink_after_open) + let Some((reader, _actual_path)) = trace_reader_for_path(path, self.unlink_after_open) else { return true; }; @@ -91,11 +87,10 @@ impl DotnetTraceManager { false // "Do not retain", i.e. remove from pending_jitdump_paths }); - for jitdump in &mut self.processors { - jitdump.process_pending_records( + for nettrace in &mut self.processors { + nettrace.process_pending_records( jit_category_manager, profile, - recycler.as_deref_mut(), timestamp_converter, ); } @@ -105,10 +100,9 @@ impl DotnetTraceManager { mut self, jit_category_manager: &mut JitCategoryManager, profile: &mut Profile, - recycler: Option<&mut JitFunctionRecycler>, timestamp_converter: &TimestampConverter, ) -> Vec { - self.process_pending_records(jit_category_manager, profile, recycler, timestamp_converter); + self.process_pending_records(jit_category_manager, profile, timestamp_converter); self.processors .into_iter() .map(|processor| processor.finish(profile)) @@ -124,6 +118,7 @@ struct SingleDotnetTraceProcessor { symbols: Vec, modules: HashMap, + seen_method_loads: HashSet<(u64, String)>, /// The relative_address of the next JIT function. /// @@ -143,6 +138,7 @@ impl SingleDotnetTraceProcessor { lib_mapping_ops: Default::default(), symbols: Default::default(), modules: Default::default(), + seen_method_loads: Default::default(), cumulative_address: 0, } } @@ -151,7 +147,6 @@ impl SingleDotnetTraceProcessor { &mut self, jit_category_manager: &mut JitCategoryManager, profile: &mut Profile, - mut recycler: Option<&mut JitFunctionRecycler>, timestamp_converter: &TimestampConverter, ) { if self.reader.is_none() { @@ -170,7 +165,6 @@ impl SingleDotnetTraceProcessor { &coreclr_event, jit_category_manager, profile, - &mut recycler, timestamp_converter, ); } else { @@ -194,7 +188,7 @@ impl SingleDotnetTraceProcessor { return; } Err(err) => { - eprintln!("Got error: {:?}", err); + log::trace!("dotnet trace manager got error, ignoring: {:?}", err); self.lib_mapping_ops .push(last_timestamp, LibMappingOp::Clear); self.close_and_commit_symbol_table(profile); @@ -209,16 +203,16 @@ impl SingleDotnetTraceProcessor { coreclr_event: &CoreClrEvent, jit_category_manager: &mut JitCategoryManager, profile: &mut Profile, - recycler: &mut Option<&mut JitFunctionRecycler>, - timestamp_converter: &TimestampConverter, + _timestamp_converter: &TimestampConverter, ) { match coreclr_event { - CoreClrEvent::ModuleLoad(event) => { - let module_id = event.module_id; - self.modules.insert(module_id, event.clone()); + CoreClrEvent::ModuleLoad(_event) => { + //let module_id = event.module_id; + //log::trace!("Loading module {} {} at {}", module_id, event.module_il_path, event.common.timestamp); + //self.modules.insert(module_id, event.clone()); } - CoreClrEvent::ModuleUnload(event) => { - let module_id = event.module_id; + CoreClrEvent::ModuleUnload(_event) => { + //let module_id = event.module_id; //if let Some(module) = self.modules.remove(&module_id) { //} } @@ -226,6 +220,17 @@ impl SingleDotnetTraceProcessor { let start_avma = event.start_address; let end_avma = event.start_address + event.size as u64; + let msig = (event.start_address, event.name.name.clone()); + if !event.dc_end { + self.seen_method_loads.insert(msig); + } else { + if self.seen_method_loads.contains(&msig) { + // we already saw a normal MethodLoad for this; skip it, so that + // we don't flag this method as being valid from 0 time + return; + } + } + let relative_address_at_start = self.cumulative_address; self.cumulative_address += event.size; @@ -240,38 +245,22 @@ impl SingleDotnetTraceProcessor { name: symbol_name.clone(), }); - let (lib_handle, relative_address_at_start) = - if let Some(recycler) = recycler.as_deref_mut() { - recycler.recycle( - start_avma, - end_avma, - relative_address_at_start, - &symbol_name, - self.lib_handle, - ) - } else { - (self.lib_handle, relative_address_at_start) - }; + let lib_handle = self.lib_handle; log::trace!( - "MethodLoad: addr = 0x{:x} symbol_name = {:?} size = {}", - start_avma, symbol_name, event.size + "MethodLoad: addr = 0x{:x} symbol_name = {:?} size = {} (dcend = {})", + start_avma, symbol_name, event.size, event.dc_end ); let (category, js_frame) = jit_category_manager.classify_jit_symbol(&symbol_name, profile); - let start_ts = if event.dc_end { - if let Some(module) = self.modules.get(&event.module_id) { - module.common.timestamp - } else { - log::trace!("Module already unloaded {}, using event timestamp...", event.module_id); - event.common.timestamp - } - } else { - event.common.timestamp - }; + // If this is a method we haven't seen before but we see it in the DCEnd + // rundown, assume that it's valid for the entire range of the trace. + // This isn't necessarily correct, but it's the best we can do given + // the information we get. + let start_ts = if event.dc_end { 0 } else { event.common.timestamp }; - self.lib_mapping_ops.push( + self.lib_mapping_ops.push_unsorted( start_ts, LibMappingOp::Add(LibMappingAdd { start_avma, @@ -281,19 +270,16 @@ impl SingleDotnetTraceProcessor { }), ); } - CoreClrEvent::ReadyToRunMethodEntryPoint(event) => { - let address = event.start_address; - let name = format!("{}.{}", event.name.namespace, event.name.name); - + CoreClrEvent::ReadyToRunMethodEntryPoint(_event) => { // Can't actually do anything with this, as we don't have a size. These methods just // won't be seen in the profile when we're using tracing only (like when attaching). } - CoreClrEvent::GcAllocationTick(event) => {} - CoreClrEvent::MethodUnload(_) => {} - CoreClrEvent::GcTriggered(event) => {} - CoreClrEvent::GcSampledObjectAllocation(event) => {} - CoreClrEvent::GcStart(event) => {} - CoreClrEvent::GcEnd(event) => {} + CoreClrEvent::GcAllocationTick(_event) => {} + CoreClrEvent::MethodUnload(_event) => {} + CoreClrEvent::GcTriggered(_event) => {} + CoreClrEvent::GcSampledObjectAllocation(_event) => {} + CoreClrEvent::GcStart(_event) => {} + CoreClrEvent::GcEnd(_event) => {} } } @@ -310,7 +296,6 @@ impl SingleDotnetTraceProcessor { pub fn finish(mut self, profile: &mut Profile) -> LibMappingOpQueue { self.close_and_commit_symbol_table(profile); - self.lib_mapping_ops.sort(); self.lib_mapping_ops } } diff --git a/samply/src/shared/coreclr/eventpipe.rs b/samply/src/shared/coreclr/eventpipe.rs index 4b7019521..eb838f125 100644 --- a/samply/src/shared/coreclr/eventpipe.rs +++ b/samply/src/shared/coreclr/eventpipe.rs @@ -25,26 +25,23 @@ pub fn eventpipe_event_to_coreclr_event( }, }; - match event { - EventPipeEvent::ModuleLoad(event) => { - Some(CoreClrEvent::ModuleLoad(super::ModuleLoadUnloadEvent { + match &event { + EventPipeEvent::ModuleLoad(m) | + EventPipeEvent::ModuleUnload(m) => { + let e = super::ModuleLoadUnloadEvent { common, - module_id: event.module_id, - assembly_id: event.assembly_id, - app_domain_id: event.app_domain_id, - module_il_path: event.module_il_path.to_string(), - module_native_path: event.module_native_path.to_string(), - })) - } - EventPipeEvent::ModuleUnload(event) => { - Some(CoreClrEvent::ModuleUnload(super::ModuleLoadUnloadEvent { - common, - module_id: event.module_id, - assembly_id: event.assembly_id, - app_domain_id: event.app_domain_id, - module_il_path: event.module_il_path.to_string(), - module_native_path: event.module_native_path.to_string(), - })) + module_id: m.module_id, + assembly_id: m.assembly_id, + app_domain_id: m.app_domain_id, + module_il_path: m.module_il_path.to_string(), + module_native_path: m.module_native_path.to_string(), + }; + + match &event { + EventPipeEvent::ModuleLoad(_) => Some(CoreClrEvent::ModuleLoad(e)), + EventPipeEvent::ModuleUnload(_) => Some(CoreClrEvent::ModuleUnload(e)), + _ => unreachable!() + } } EventPipeEvent::ReadyToRunGetEntryPoint(method) => { let name = if method.method_name.is_empty() { @@ -64,44 +61,17 @@ pub fn eventpipe_event_to_coreclr_event( }, )) } - EventPipeEvent::MethodLoad(method) => { - let name = if method.method_name.is_empty() { - format!("JIT[0x{:x}]", method.method_start_address) - } else { - method.method_name.to_string() - }; - let tier = (method.method_flags >> 7) & 0x7; - let tier = match tier { - 1 => super::MethodCompilationTier::MinOptJitted, - 2 => super::MethodCompilationTier::Optimized, - 3 => super::MethodCompilationTier::QuickJitted, - 4 => super::MethodCompilationTier::OptimizedTier1, - 5 => super::MethodCompilationTier::OptimizedTier1OSR, - 6 => super::MethodCompilationTier::InstrumentedTier, - 7 => super::MethodCompilationTier::InstrumentedTierOptimized, - _ => super::MethodCompilationTier::Unknown, - }; - - Some(CoreClrEvent::MethodLoad(super::MethodLoadEvent { - common, - module_id: method.module_id, - start_address: method.method_start_address, - size: method.method_size, - name: CoreClrMethodName { - name, - namespace: method.method_namespace.to_string(), - signature: method.method_signature.to_string(), - }, - tier, - dc_end: false, - })) - } + EventPipeEvent::MethodLoad(method) | EventPipeEvent::MethodDCEnd(method) => { let name = if method.method_name.is_empty() { format!("JIT[0x{:x}]", method.method_start_address) } else { method.method_name.to_string() }; + let dc_end = match &event { + EventPipeEvent::MethodDCEnd(_) => true, + _ => false, + }; let tier = (method.method_flags >> 7) & 0x7; let tier = match tier { 1 => super::MethodCompilationTier::MinOptJitted, @@ -125,7 +95,7 @@ pub fn eventpipe_event_to_coreclr_event( signature: method.method_signature.to_string(), }, tier, - dc_end: true, + dc_end })) } EventPipeEvent::MethodUnload(method) => { diff --git a/samply/src/shared/coreclr/markers.rs b/samply/src/shared/coreclr/markers.rs index dd816f4d4..e5d7b760b 100644 --- a/samply/src/shared/coreclr/markers.rs +++ b/samply/src/shared/coreclr/markers.rs @@ -1,178 +1,220 @@ use fxprof_processed_profile::{ - MarkerDynamicField, MarkerFieldFormat, MarkerLocation, MarkerSchema, MarkerSchemaField, - MarkerStaticField, ProfilerMarker, + MarkerFieldFormat, MarkerFieldSchema, MarkerLocation, MarkerSchema, MarkerStaticField, StaticSchemaMarker, + StringHandle, CategoryHandle, Profile }; use serde_json::json; // String is type name #[derive(Debug, Clone)] -pub struct CoreClrGcAllocTickMarker(pub String, pub usize, pub usize); +pub struct CoreClrGcAllocTickMarker(pub StringHandle, pub usize, pub usize, pub CategoryHandle); -impl ProfilerMarker for CoreClrGcAllocTickMarker { - const MARKER_TYPE_NAME: &'static str = "GC Alloc Tick"; - - fn json_marker_data(&self) -> serde_json::Value { - json!({ - "type": Self::MARKER_TYPE_NAME, - "clrtype": self.0, - "totalsize": self.1, - "objcount": self.2, - }) - } +impl StaticSchemaMarker for CoreClrGcAllocTickMarker { + const UNIQUE_MARKER_TYPE_NAME: &'static str = "GC Alloc Tick"; fn schema() -> MarkerSchema { MarkerSchema { - type_name: Self::MARKER_TYPE_NAME, + type_name: Self::UNIQUE_MARKER_TYPE_NAME.into(), locations: vec![ MarkerLocation::MarkerChart, MarkerLocation::MarkerTable, MarkerLocation::TimelineMemory, ], - chart_label: Some("GC Alloc"), - tooltip_label: Some("GC Alloc: {marker.data.clrtype} ({marker.data.size})"), - table_label: Some("GC Alloc"), + chart_label: Some("GC Alloc".into()), + tooltip_label: Some("GC Alloc: {marker.data.clrtype} ({marker.data.size} bytes)".into()), + table_label: Some("GC Alloc".into()), fields: vec![ - MarkerSchemaField::Dynamic(MarkerDynamicField { - key: "clrtype", - label: "CLR Type", + MarkerFieldSchema { + key: "clrtype".into(), + label: "CLR Type".into(), format: MarkerFieldFormat::String, searchable: true, - }), - MarkerSchemaField::Dynamic(MarkerDynamicField { - key: "size", - label: "Total size of all objects", + }, + MarkerFieldSchema { + key: "size".into(), + label: "Total size of all objects".into(), format: MarkerFieldFormat::Bytes, searchable: false, - }), - MarkerSchemaField::Dynamic(MarkerDynamicField { - key: "objcount", - label: "Number of objects allocated", + }, + MarkerFieldSchema { + key: "objcount".into(), + label: "Number of objects allocated".into(), format: MarkerFieldFormat::Integer, searchable: false, - }), - MarkerSchemaField::Static(MarkerStaticField { - label: "Description", - value: "CoreCLR GC Allocation Tick", - }), + }, + ], + static_fields: vec![MarkerStaticField { + label: "Description".into(), + value: "CoreCLR GC Allocation Tick".into(), + }, ], } } -} -#[derive(Debug, Clone)] -pub struct CoreClrGcAllocMarker(pub String, pub usize); + fn name(&self, profile: &mut Profile) -> StringHandle { + profile.intern_string(Self::UNIQUE_MARKER_TYPE_NAME) + } -impl ProfilerMarker for CoreClrGcAllocMarker { - const MARKER_TYPE_NAME: &'static str = "GC Alloc"; + fn category(&self, _profile: &mut Profile) -> CategoryHandle { + self.3 + } + + fn string_field_value(&self, _field_index: u32) -> StringHandle { + self.0 + } - fn json_marker_data(&self) -> serde_json::Value { - json!({ - "type": Self::MARKER_TYPE_NAME, - "clrtype": self.0, - "size": self.1, - }) + fn number_field_value(&self, field_index: u32) -> f64 { + if field_index == 1 { + self.1 as f64 + } else if field_index == 2 { + self.2 as f64 + } else { + panic!("Unexpected field_index"); + } } +} + +#[derive(Debug, Clone)] +pub struct CoreClrGcAllocMarker(pub StringHandle, pub usize, pub CategoryHandle); + +impl StaticSchemaMarker for CoreClrGcAllocMarker { + const UNIQUE_MARKER_TYPE_NAME: &'static str = "GC Alloc"; fn schema() -> MarkerSchema { MarkerSchema { - type_name: Self::MARKER_TYPE_NAME, + type_name: Self::UNIQUE_MARKER_TYPE_NAME.into(), locations: vec![ MarkerLocation::MarkerChart, MarkerLocation::MarkerTable, MarkerLocation::TimelineMemory, ], - chart_label: Some("GC Alloc"), - tooltip_label: Some("GC Alloc: {marker.data.clrtype} ({marker.data.size})"), - table_label: Some("GC Alloc"), + chart_label: Some("GC Alloc".into()), + tooltip_label: Some( + "GC Alloc: {marker.data.clrtype} ({marker.data.size} bytes)".into(), + ), + table_label: Some("GC Alloc".into()), fields: vec![ - MarkerSchemaField::Dynamic(MarkerDynamicField { - key: "clrtype", - label: "CLR Type", + MarkerFieldSchema { + key: "clrtype".into(), + label: "CLR Type".into(), format: MarkerFieldFormat::String, searchable: true, - }), - MarkerSchemaField::Dynamic(MarkerDynamicField { - key: "size", - label: "Size", + }, + MarkerFieldSchema { + key: "size".into(), + label: "Size".into(), format: MarkerFieldFormat::Bytes, searchable: false, - }), - MarkerSchemaField::Static(MarkerStaticField { - label: "Description", - value: "CoreCLR GC Allocation", - }), + }, ], + static_fields: vec![MarkerStaticField { + label: "Description".into(), + value: "GC Allocation".into(), + }], } } -} -#[derive(Debug, Clone)] -pub struct CoreClrGcMarker(); + fn name(&self, profile: &mut Profile) -> StringHandle { + profile.intern_string("GC Alloc") + } -impl ProfilerMarker for CoreClrGcMarker { - const MARKER_TYPE_NAME: &'static str = "GC"; + fn category(&self, _profile: &mut Profile) -> CategoryHandle { + self.2 + } - fn json_marker_data(&self) -> serde_json::Value { - json!({ - "type": Self::MARKER_TYPE_NAME, - }) + fn string_field_value(&self, _field_index: u32) -> StringHandle { + self.0 } + fn number_field_value(&self, _field_index: u32) -> f64 { + self.1 as f64 + } +} + +#[derive(Debug, Clone)] +pub struct CoreClrGcEventMarker(pub StringHandle, pub StringHandle, pub CategoryHandle); + +impl StaticSchemaMarker for CoreClrGcEventMarker { + const UNIQUE_MARKER_TYPE_NAME: &'static str = "GC Event"; + fn schema() -> MarkerSchema { MarkerSchema { - type_name: Self::MARKER_TYPE_NAME, + type_name: Self::UNIQUE_MARKER_TYPE_NAME.into(), locations: vec![ MarkerLocation::MarkerChart, MarkerLocation::MarkerTable, MarkerLocation::TimelineMemory, ], - chart_label: Some("GC"), - tooltip_label: Some("GC"), - table_label: Some("GC"), - fields: vec![MarkerSchemaField::Static(MarkerStaticField { - label: "Description", - value: "CoreCLR GC", - })], + chart_label: Some("{marker.data.event}".into()), + tooltip_label: Some("{marker.data.event}".into()), + table_label: Some("{marker.data.event}".into()), + fields: vec![MarkerFieldSchema { + key: "event".into(), + label: "Event".into(), + format: MarkerFieldFormat::String, + searchable: true, + }], + static_fields: vec![MarkerStaticField { + label: "Description".into(), + value: "CoreCLR GC Event".into(), + }], } } -} -#[derive(Debug, Clone)] -pub struct CoreClrGcEventMarker(pub String); + fn name(&self, _profile: &mut Profile) -> StringHandle { + self.0 + } + + fn category(&self, _profile: &mut Profile) -> CategoryHandle { + self.2 + } -impl ProfilerMarker for CoreClrGcEventMarker { - const MARKER_TYPE_NAME: &'static str = "GC Event"; + fn string_field_value(&self, _field_index: u32) -> StringHandle { + self.1 + } - fn json_marker_data(&self) -> serde_json::Value { - json!({ - "type": Self::MARKER_TYPE_NAME, - "event": self.0, - }) + fn number_field_value(&self, _field_index: u32) -> f64 { + unreachable!() } +} + +#[derive(Debug, Clone)] +pub struct CoreClrGcMarker(pub CategoryHandle); + +impl StaticSchemaMarker for CoreClrGcMarker { + const UNIQUE_MARKER_TYPE_NAME: &'static str = "GC"; fn schema() -> MarkerSchema { MarkerSchema { - type_name: Self::MARKER_TYPE_NAME, + type_name: Self::UNIQUE_MARKER_TYPE_NAME.into(), locations: vec![ MarkerLocation::MarkerChart, MarkerLocation::MarkerTable, MarkerLocation::TimelineMemory, ], - chart_label: Some("{marker.data.event}"), - tooltip_label: Some("{marker.data.event}"), - table_label: Some("{marker.data.event}"), - fields: vec![ - MarkerSchemaField::Dynamic(MarkerDynamicField { - key: "event", - label: "Event", - format: MarkerFieldFormat::String, - searchable: true, - }), - MarkerSchemaField::Static(MarkerStaticField { - label: "Description", - value: "CoreCLR Generic GC Event", - }), - ], + chart_label: Some("GC".into()), + tooltip_label: Some("GC".into()), + table_label: Some("GC".into()), + fields: vec![], + static_fields: vec![MarkerStaticField { + label: "Description".into(), + value: "CoreCLR GC".into(), + }], } } + + fn name(&self, profile: &mut Profile) -> StringHandle { + profile.intern_string(Self::UNIQUE_MARKER_TYPE_NAME) + } + + fn category(&self, _profile: &mut Profile) -> CategoryHandle { + self.0 + } + + fn string_field_value(&self, _field_index: u32) -> StringHandle { + unreachable!() + } + + fn number_field_value(&self, _field_index: u32) -> f64 { + unreachable!() + } } diff --git a/samply/src/shared/lib_mappings.rs b/samply/src/shared/lib_mappings.rs index ee75c67c9..9b0536c21 100644 --- a/samply/src/shared/lib_mappings.rs +++ b/samply/src/shared/lib_mappings.rs @@ -132,22 +132,30 @@ impl LibMappingsHierarchy { } #[derive(Debug, Clone, Default)] -pub struct LibMappingOpQueue(Vec<(u64, LibMappingOp)>); +pub struct LibMappingOpQueue(Vec<(u64, LibMappingOp)>, bool); impl LibMappingOpQueue { pub fn push(&mut self, timestamp: u64, op: LibMappingOp) { self.0.push((timestamp, op)); } + pub fn push_unsorted(&mut self, timestamp: u64, op: LibMappingOp) { + self.0.push((timestamp, op)); + self.1 = true; + } + pub fn is_empty(&self) -> bool { self.0.is_empty() } - pub fn into_iter(self) -> LibMappingOpQueueIter { + pub fn into_iter(mut self) -> LibMappingOpQueueIter { + if self.1 /* unsorted */ { + self.sort(); + } LibMappingOpQueueIter(self.0.into_iter().peekable()) } - pub fn sort(&mut self) { + fn sort(&mut self) { self.0.sort_by_key(|(timestamp, _)| *timestamp); } } diff --git a/samply/src/windows/etw_coreclr.rs b/samply/src/windows/etw_coreclr.rs index 3074a57ec..85d63be4d 100644 --- a/samply/src/windows/etw_coreclr.rs +++ b/samply/src/windows/etw_coreclr.rs @@ -16,6 +16,7 @@ use crate::shared::recording_props::{CoreClrProfileProps, ProfileCreationProps}; use crate::windows::profile_context::{KnownCategory, ProfileContext}; +use super::coreclr::CoreClrContext; use super::elevated_helper::ElevatedRecordingProps; pub struct CoreClrEtwConverter { @@ -104,16 +105,20 @@ impl CoreClrEtwConverter { let new_event = match (task, opcode) { ("CLRMethod" | "CLRMethodRundown", method_event) => match method_event { - "MethodLoadVerbose" | "MethodDCStartVerbose" => { + "MethodLoadVerbose" | "MethodDCStartVerbose" | "MethodDCEndVerbose" => { let method_basename: String = parser.parse("MethodName"); let method_namespace: String = parser.parse("MethodNamespace"); let method_signature: String = parser.parse("MethodSignature"); + let module_id: u64 = parser.parse("ModuleID"); let method_start_address: u64 = parser.parse("MethodStartAddress"); let method_size: u32 = parser.parse("MethodSize"); + let dc_end = method_event == "MethodDCEndVerbose"; + Some(CoreClrEvent::MethodLoad(MethodLoadEvent { common, + module_id, start_address: method_start_address, size: method_size, name: CoreClrMethodName { @@ -121,6 +126,8 @@ impl CoreClrEtwConverter { namespace: method_namespace, signature: method_signature, }, + tier: MethodCompilationTier::Unknown, // TODO + dc_end })) } "ModuleLoad" | "ModuleDCStart" | "ModuleUnload" | "ModuleDCEnd" | _ => None, diff --git a/samply/src/windows/etw_gecko.rs b/samply/src/windows/etw_gecko.rs index eaafa167f..25bfbc641 100644 --- a/samply/src/windows/etw_gecko.rs +++ b/samply/src/windows/etw_gecko.rs @@ -14,9 +14,9 @@ use uuid::Uuid; use super::coreclr::CoreClrContext; use super::profile_context::ProfileContext; use crate::shared::coreclr::CoreClrProviderProps; -use crate::windows::coreclr::{self, handle_new_coreclr_event}; +use crate::windows::coreclr::handle_new_coreclr_event; use crate::windows::etw_coreclr::CoreClrEtwConverter; -use crate::windows::profile_context::KnownCategory; +use crate::windows::profile_context::{KnownCategory, PeInfo}; pub fn process_etl_files( context: &mut ProfileContext, @@ -28,7 +28,7 @@ pub fn process_etl_files( let processing_start_timestamp = Instant::now(); - let mut core_clr_context = CoreClrContext::new(context.creation_props()); + let mut core_clr_context = CoreClrContext::new(context); let result = process_trace( etl_file, @@ -70,17 +70,6 @@ fn process_trace( let demand_zero_faults = false; //pargs.contains("--demand-zero-faults"); let mut pending_image_info: Option<((u32, u64), PeInfo)> = None; - let coreclr_props = context.creation_props().coreclr; - let mut core_clr_context = CoreClrContext::new( - CoreClrProviderProps { - is_attach: false, - gc_markers: coreclr_props.gc_markers, - gc_suspensions: coreclr_props.gc_suspensions, - gc_detailed_allocs: coreclr_props.gc_detailed_allocs, - event_stacks: coreclr_props.event_stacks, - }, - false, - ); let mut core_clr_converter = CoreClrEtwConverter::new(); open_trace(etl_file, |e| { @@ -512,13 +501,13 @@ fn process_trace( } let is_in_range = context.is_in_time_range(timestamp_raw); if let Some(event) = core_clr_converter.etw_event_to_coreclr_event( - &mut core_clr_context, + core_clr_context, &s, &mut parser, ) { handle_new_coreclr_event( context, - &mut core_clr_context, + core_clr_context, &event, is_in_range, ); diff --git a/samply/src/windows/profile_context.rs b/samply/src/windows/profile_context.rs index 4f3fb50fe..33af5c201 100644 --- a/samply/src/windows/profile_context.rs +++ b/samply/src/windows/profile_context.rs @@ -1,7 +1,5 @@ use std::collections::{BTreeMap, HashMap, VecDeque}; -use std::path::{Path, PathBuf}; -use std::str::FromStr; -use std::sync::Arc; +use std::path::Path; use debugid::DebugId; use fxprof_processed_profile::{ @@ -13,8 +11,6 @@ use fxprof_processed_profile::{ use shlex::Shlex; use wholesym::PeCodeId; -use super::chrome_etw_flags::KeywordNames; -use super::coreclr::CoreClrContext; use super::chrome::KeywordNames; use super::winutils; use crate::shared::context_switch::{ @@ -1894,20 +1890,6 @@ impl ProfileContext { pub fn convert_raw_timestamp(&self, ts_raw: u64) -> Timestamp { self.timestamp_converter.convert_time(ts_raw) } - - pub fn add_thread_marker( - &mut self, - tid: u32, - timing: MarkerTiming, - known_category: KnownCategory, - name: &str, - marker: impl ProfilerMarker, - ) -> MarkerHandle { - let thread = self.threads.get_mut(&tid).unwrap(); - let category = self.categories.get(known_category, &mut self.profile); - self.profile - .add_marker(thread.handle, category, name, marker, timing) - } } #[derive(Debug, Clone)] From 57dcd178a3b9adff24ed1b1c980b82d906b081e1 Mon Sep 17 00:00:00 2001 From: Vladimir Vukicevic Date: Tue, 30 Jul 2024 08:06:29 -0700 Subject: [PATCH 06/36] Fix server URL --- samply/src/server.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samply/src/server.rs b/samply/src/server.rs index 8d4285537..10d90bdde 100644 --- a/samply/src/server.rs +++ b/samply/src/server.rs @@ -148,7 +148,7 @@ async fn start_server( Some(s) => s.trim_end_matches('/'), //None => "https://profiler.firefox.com", // At some point a real URL - None => "https://profiler.cds.internal.unity3d.com/", + None => "https://profiler.cds.internal.unity3d.com", }; let encoded_profile_url = utf8_percent_encode(&profile_url, BAD_CHARS).to_string(); From b2c480479a4cbaec562ed9953e406e31336116ed Mon Sep 17 00:00:00 2001 From: Vladimir Vukicevic Date: Wed, 31 Jul 2024 12:33:12 -0700 Subject: [PATCH 07/36] Handle ImageID without checksum safely --- samply/src/windows/etw_gecko.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/samply/src/windows/etw_gecko.rs b/samply/src/windows/etw_gecko.rs index 25bfbc641..1614fd5a0 100644 --- a/samply/src/windows/etw_gecko.rs +++ b/samply/src/windows/etw_gecko.rs @@ -30,6 +30,8 @@ pub fn process_etl_files( let mut core_clr_context = CoreClrContext::new(context); + log::info!("Processing {}...", etl_file.to_string_lossy()); + let result = process_trace( etl_file, context, @@ -42,6 +44,7 @@ pub fn process_etl_files( } for extra_etl_file in extra_etl_filenames { + log::info!("Processing {}...", etl_file.to_string_lossy()); let result = process_trace( extra_etl_file, context, @@ -256,7 +259,16 @@ fn process_trace( let pid = s.process_id(); // there isn't a ProcessId field here let image_base: u64 = parser.try_parse("ImageBase").unwrap(); let image_timestamp: u32 = parser.try_parse("TimeDateStamp").unwrap(); - let image_checksum: u32 = parser.try_parse("ImageChecksum").unwrap(); + let image_checksum: u32 = if let Ok(checksum) = parser.try_parse("ImageChecksum") { + checksum + } else { + // This happens for pid 0 -- there's no ImageChecksum in these events. These will be seen + // when importing/processing a whole-system ETL trace without filtering to processes. + if pid != 0 { + log::warn!("Missing ImageChecksum in KernelTraceControl/ImageID for pid {} image_base {:#x}", pid, image_base); + } + 0 + }; let image_size: u32 = parser.try_parse("ImageSize").unwrap(); let mut info = PeInfo::new_with_size_and_checksum(image_size, image_checksum); info.image_timestamp = Some(image_timestamp); From 8bc87518b2ab73e53d868dbbae255e63659648b3 Mon Sep 17 00:00:00 2001 From: Vladimir Vukicevic Date: Wed, 31 Jul 2024 12:33:54 -0700 Subject: [PATCH 08/36] Increase xperf buffer size --- samply/src/windows/xperf.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/samply/src/windows/xperf.rs b/samply/src/windows/xperf.rs index dea0a7431..4f97c6fe9 100644 --- a/samply/src/windows/xperf.rs +++ b/samply/src/windows/xperf.rs @@ -65,6 +65,10 @@ impl Xperf { user_providers.sort_unstable(); user_providers.dedup(); + // Will go to the elevated process so won't be seen, but one day we'll be able to + // run in-process if running as admin + log::trace!("xperf user providers: {:?}", user_providers); + let xperf_path = self.get_xperf_path()?; // start xperf.exe, logging to the same location as the output file, just with a .etl // extension. @@ -99,6 +103,11 @@ impl Xperf { xperf.arg("-f"); xperf.arg(&kernel_etl_file); + // 1MB buffers. Default is 64Kb; 1MB is max. Default minimum buffer count + // is 64, max is 320, which seems totally reasonable for modern systems. + xperf.arg("-BufferSize"); + xperf.arg("1024"); + let user_etl_file = if !user_providers.is_empty() { let mut user_etl_file = output_path.to_owned(); if user_etl_file.extension() == Some(OsStr::new("gz")) { @@ -115,6 +124,11 @@ impl Xperf { xperf.arg("-f"); xperf.arg(&user_etl_file); + // 1MB buffers. Default is 64Kb; 1MB is max. Default minimum buffer count + // is 64, max is 320, which seems totally reasonable for modern systems. + xperf.arg("-BufferSize"); + xperf.arg("1024"); + Some(user_etl_file) } else { None From 7f6c799be1e90ba95c6f41182501a1fdfb688445 Mon Sep 17 00:00:00 2001 From: Vladimir Vukicevic Date: Wed, 31 Jul 2024 12:34:15 -0700 Subject: [PATCH 09/36] Coreclr fixes for unmerged ETL files --- samply/src/windows/coreclr.rs | 3 + samply/src/windows/etw_coreclr.rs | 91 +++++++++++++++++++++++++++---- samply/src/windows/profiler.rs | 4 +- 3 files changed, 86 insertions(+), 12 deletions(-) diff --git a/samply/src/windows/coreclr.rs b/samply/src/windows/coreclr.rs index c49822183..29bdfc766 100644 --- a/samply/src/windows/coreclr.rs +++ b/samply/src/windows/coreclr.rs @@ -25,6 +25,8 @@ pub struct CoreClrContext { last_marker_on_thread: HashMap, gc_markers_on_thread: HashMap>, + + pub last_n: String, } impl CoreClrContext { @@ -35,6 +37,7 @@ impl CoreClrContext { last_marker_on_thread: HashMap::new(), gc_markers_on_thread: HashMap::new(), + last_n: String::new(), } } diff --git a/samply/src/windows/etw_coreclr.rs b/samply/src/windows/etw_coreclr.rs index 85d63be4d..f92f9f74f 100644 --- a/samply/src/windows/etw_coreclr.rs +++ b/samply/src/windows/etw_coreclr.rs @@ -46,8 +46,13 @@ impl CoreClrEtwConverter { let mut name_parts = s.name().splitn(3, '/'); let provider = name_parts.next().unwrap(); - let task = name_parts.next().unwrap(); - let opcode = name_parts.next().unwrap(); + let mut task = name_parts.next().unwrap(); + let mut opcode = name_parts.next().unwrap(); + + //if coreclr_context.last_n != s.name() { + // eprintln!("'{}' => '{}/{}/{}'", s.name(), provider, task, opcode); + // coreclr_context.last_n = s.name().to_string(); + //} match provider { "Microsoft-Windows-DotNETRuntime" | "Microsoft-Windows-DotNETRuntimeRundown" => {} @@ -56,6 +61,70 @@ impl CoreClrEtwConverter { } } + + // When working with merged ETL files, the proper task and opcode names appear here, e.g. "CLRMethod/MethodLoadVerbose" or + // "CLRMethodRundown/MethodDCStartVerbose". When working with the unmerged user ETL, these show up as e.g. "Method /DCStartVerbose". + // Not clear where those names come from the Etw .man file in CoreCLR does have entries for e.g. RuntimePublisher.MethodDCStartVerboseOpcodeMessage + // as "DCStartVerbose", but I'm not sure how/why those are referenced here and not in the merged ETL. xperf -a dumper on the unmerged + // ETL shows the same (correct) names as the merged ETL. + // + // We try to hack around this by converting the unmerged name to the converted one here. + if task.ends_with(" ") || opcode.ends_with(" ") { + task = task.trim(); + opcode = opcode.trim(); + + // Some of these are technically not correct; e.g. the task should be CLRMethodRundown if it's the + // rundown provider, but we handle them the same below. + match task { + "Method" => { + task = "CLRMethod"; + opcode = match opcode { + "LoadVerbose" => "MethodLoadVerbose", + "UnloadVerbose" => "MethodUnloadVerbose", + "DCStartVerbose" => "MethodDCStartVerbose", + "DCEndVerbose" => "MethodDCEndVerbose", + "JittingStarted" => "MethodJittingStarted", + _ => opcode.trim(), + }; + }, + "Loader" => { + task = "CLRLoader"; + opcode = match opcode { + "ModuleDCStart" => "ModuleDCStart", + _ => opcode.trim(), + }; + }, + "Runtime" => { + task = "CLRRuntimeInformation"; + opcode = match opcode { + _ => opcode.trim(), + }; + }, + "GC" => { + task = "GarbageCollection"; + opcode = match opcode { + "PerHeapHisory" => opcode, + "GCDynamicEvent" => opcode, + "Start" => "win:Start", + "Stop" => "win:Stop", + "RestartEEStart" => "GCRestartEEBegin", + "RestartEEStop" => "GCRestartEEEnd", + "SuspendEEStart" => "GCSuspendEEBegin", + "SuspendEEStop" => "GCSuspendEEEnd", + _ => opcode.trim(), + }; + }, + "ClrStack" => { + task = "CLRStack"; + opcode = match opcode { + "Walk" => "CLRStackWalk", + _ => opcode.trim(), + }; + }, + _ => {}, + } + } + let pid = s.process_id(); let tid = s.thread_id(); @@ -116,6 +185,8 @@ impl CoreClrEtwConverter { let dc_end = method_event == "MethodDCEndVerbose"; + //log::trace!("{}: @ {:x} {}::{} {}", opcode, method_start_address, method_namespace, method_basename, method_signature); + Some(CoreClrEvent::MethodLoad(MethodLoadEvent { common, module_id, @@ -130,7 +201,11 @@ impl CoreClrEtwConverter { dc_end })) } - "ModuleLoad" | "ModuleDCStart" | "ModuleUnload" | "ModuleDCEnd" | _ => None, + _ => None, + }, + ("CLRLoader" | "CLRLoaderRundown", loader_event) => match loader_event { + // AppDomain, Assembly, Module Load/Unload + "ModuleDCStart" | _ => None, }, ("GarbageCollection", gc_event) => { match gc_event { @@ -156,7 +231,7 @@ impl CoreClrEtwConverter { "Triggered" => { let reason: u32 = parser.parse("Reason"); let reason = GcReason::from_u32(reason).unwrap_or_else(|| { - eprintln!("Unknown CLR GC Triggered reason: {}", reason); + log::warn!("Unknown CLR GC Triggered reason: {}", reason); GcReason::Empty }); @@ -172,7 +247,7 @@ impl CoreClrEtwConverter { let reason: u32 = parser.parse("Reason"); let _reason = GcSuspendEeReason::from_u32(reason).unwrap_or_else(|| { - eprintln!("Unknown CLR GCSuspendEEBegin reason: {}", reason); + log::warn!("Unknown CLR GCSuspendEEBegin reason: {}", reason); GcSuspendEeReason::Other }); @@ -202,7 +277,7 @@ impl CoreClrEtwConverter { let gc_type = parser.try_parse("Type").ok().and_then(GcType::from_u32); let reason = GcReason::from_u32(reason).unwrap_or_else(|| { - eprintln!("Unknown CLR GCStart reason: {}", reason); + log::warn!("Unknown CLR GCStart reason: {}", reason); GcReason::Empty }); @@ -248,10 +323,6 @@ impl CoreClrEtwConverter { } } ("CLRRuntimeInformation", _) => None, - ("CLRLoader", _) => { - // AppDomain, Assembly, Module Load/Unload - None - } _ => None, }; diff --git a/samply/src/windows/profiler.rs b/samply/src/windows/profiler.rs index ed81937cb..63181eef4 100644 --- a/samply/src/windows/profiler.rs +++ b/samply/src/windows/profiler.rs @@ -128,11 +128,11 @@ pub fn start_recording( let (kernel_output_file, user_output_file) = elevated_helper .stop_xperf() - .expect("Should have produced a merged ETL file"); + .expect("Should have produced ETL file(s)"); elevated_helper.shutdown(); - eprintln!("Processing ETL trace..."); + eprintln!("Processing ETL files(s)..."); let output_file = recording_props.output_file.clone(); From 2c7a7227eef26980ea9a9eaf3f384ca960d603d8 Mon Sep 17 00:00:00 2001 From: Vladimir Vukicevic Date: Wed, 31 Jul 2024 12:48:21 -0700 Subject: [PATCH 10/36] Bump version number --- samply/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samply/Cargo.toml b/samply/Cargo.toml index 8903f014c..d3d39f3de 100644 --- a/samply/Cargo.toml +++ b/samply/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "usamply" -version = "0.12.9" +version = "0.12.10-prerelease.1" authors = ["Markus Stange ", "Vladimir Vukicevic "] edition = "2021" rust-version = "1.75" # needed by wholesym -> fs4 From 8d4ae263b6381f171f80c545ab512efc9a6a27a0 Mon Sep 17 00:00:00 2001 From: Vladimir Vukicevic Date: Wed, 14 Aug 2024 17:25:49 -0400 Subject: [PATCH 11/36] Remove nettrace file after processing --- samply/src/shared/coreclr/dotnet_trace_manager.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/samply/src/shared/coreclr/dotnet_trace_manager.rs b/samply/src/shared/coreclr/dotnet_trace_manager.rs index 49d1f2a90..29e25a6bd 100644 --- a/samply/src/shared/coreclr/dotnet_trace_manager.rs +++ b/samply/src/shared/coreclr/dotnet_trace_manager.rs @@ -75,7 +75,7 @@ impl DotnetTraceManager { debug_name: name.clone(), debug_path: path.clone(), name, - path, + path: path.clone(), debug_id, code_id: Some(code_id.to_string()), arch: None, @@ -84,6 +84,9 @@ impl DotnetTraceManager { self.processors .push(SingleDotnetTraceProcessor::new(reader, lib_handle)); + + let _ = std::fs::remove_file(&path).is_err_and(|e| { log::warn!("Failed to remove {}: {}", path, e); true } ); + false // "Do not retain", i.e. remove from pending_jitdump_paths }); From 8f8b78efaf144beebd859e6e32312c4ad2ba9677 Mon Sep 17 00:00:00 2001 From: Vladimir Vukicevic Date: Wed, 14 Aug 2024 17:27:03 -0400 Subject: [PATCH 12/36] version --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 60a5f5821..0d37dc40d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2453,7 +2453,7 @@ dependencies = [ [[package]] name = "usamply" -version = "0.12.9" +version = "0.12.10-prerelease.1" dependencies = [ "bitflags 2.5.0", "byteorder", From c6d15556cbf427e44101c363773264f85a5aa50e Mon Sep 17 00:00:00 2001 From: Vladimir Vukicevic Date: Wed, 14 Aug 2024 17:27:18 -0400 Subject: [PATCH 13/36] ifdef'd code to use dotnet trace for launching --- samply/src/mac/profiler.rs | 93 ++++++++++++++++++++++++++++---------- 1 file changed, 69 insertions(+), 24 deletions(-) diff --git a/samply/src/mac/profiler.rs b/samply/src/mac/profiler.rs index 16c39205a..ba404a4ac 100644 --- a/samply/src/mac/profiler.rs +++ b/samply/src/mac/profiler.rs @@ -32,6 +32,9 @@ pub fn start_recording( let output_file = recording_props.output_file.clone(); let mut task_accepter = TaskAccepter::new()?; + let mut should_look_for_nettrace = 0; + + let nettrace_path = format!("samply-coreclr-{}.dottrace", uuid::Uuid::new_v4()); let mut root_task_runner: Box = match recording_mode { RecordingMode::All => { @@ -53,14 +56,14 @@ pub fn start_recording( .arg("-p") .arg(pid.to_string()) .arg("-o") - .arg("profile.dottrace") + .arg(nettrace_path.clone()) .arg("--providers") .arg(&provider_args.join(",")) .stdout(Stdio::null()) .spawn() .expect("Failed to spawn dotnet-trace"); - task_accepter.queue_received_stuff(ReceivedStuff::DotnetTracePath(pid, PathBuf::from("profile.dottrace"))); + task_accepter.queue_received_stuff(ReceivedStuff::DotnetTracePath(pid, PathBuf::from(nettrace_path.clone()))); Box::new(ExistingProcessRunner::new_with_aux_child(pid, &mut task_accepter, child)) } else { @@ -75,7 +78,7 @@ pub fn start_recording( iteration_count, } = process_launch_props; - if profile_creation_props.coreclr.any_enabled() { + let task_launcher = if profile_creation_props.coreclr.any_enabled() { // We need to set DOTNET_PerfMapEnabled=3 in the environment if it's not already set. // If we set it, we'll also set unlink_aux_files=true to avoid leaving files // behind in the temp directory. But if it's set manually, assume the user @@ -91,29 +94,60 @@ pub fn start_recording( }; let provider_args = crate::shared::coreclr::coreclr_provider_args(coreclr_props); - let mut add_env_var = |key: &str, value: &str| { - eprintln!("{key}={value}"); - env_vars.push((key.into(), value.into())); - }; + if true { + let mut add_env_var = |key: &str, value: &str| { + eprintln!("{key}={value}"); + env_vars.push((key.into(), value.into())); + }; - add_env_var("DOTNET_EnableEventPipe", "1"); - add_env_var( - "DOTNET_EventPipeOutputPath", - "/tmp/samply-dotnet-{pid}.nettrace", - ); - add_env_var("DOTNET_EventPipeConfig", &provider_args.join(",")); - if profile_creation_props.coreclr.event_stacks { - add_env_var("DOTNET_EventPipeEnableStackwalk", "1"); - } - } + add_env_var("DOTNET_EnableEventPipe", "1"); + add_env_var( + "DOTNET_EventPipeOutputPath", + "/tmp/samply-dotnet-{pid}.nettrace", + ); + //add_env_var("DOTNET_EventPipeConfig", &provider_args.join(",")); + add_env_var("DOTNET_EventPipeConfig", &provider_args[0]); + if profile_creation_props.coreclr.event_stacks { + add_env_var("DOTNET_EventPipeEnableStackwalk", "1"); + } - let task_launcher = TaskLauncher::new( - &command_name, - &args, - iteration_count, - &env_vars, - task_accepter.extra_env_vars(), - )?; + TaskLauncher::new( + &command_name, + &args, + iteration_count, + &env_vars, + task_accepter.extra_env_vars(), + )? + } else { + let mut trace_args: Vec = Vec::new(); + + trace_args.push("trace".into()); + trace_args.push("collect".into()); + trace_args.push("-o".into()); + trace_args.push(nettrace_path.clone().into()); + trace_args.push("--providers".into()); + trace_args.push(provider_args.join(",").into()); + trace_args.push("--".into()); + trace_args.push(command_name); + trace_args.extend(args); + + TaskLauncher::new( + &"dotnet".into(), + &trace_args, + iteration_count, + &env_vars, + task_accepter.extra_env_vars(), + )? + } + } else { + TaskLauncher::new( + &command_name, + &args, + iteration_count, + &env_vars, + task_accepter.extra_env_vars(), + )? + }; Box::new(task_launcher) } @@ -145,6 +179,12 @@ pub fn start_recording( let timeout = Duration::from_secs_f64(1.0); match task_accepter.next_message(timeout) { Ok(ReceivedStuff::AcceptedTask(accepted_task)) => { + if should_look_for_nettrace == 2 { + should_look_for_nettrace = 1; + accepted_task.start_execution(); + continue; + } + let pid = accepted_task.get_id(); let (path_sender, path_receiver) = unbounded(); let send_result = task_sender.send(TaskInitOrShutdown::TaskInit(TaskInit { @@ -153,6 +193,11 @@ pub fn start_recording( pid, path_receiver, })); + + if path_senders_per_pid.is_empty() && should_look_for_nettrace == 1 { + task_accepter.queue_received_stuff(ReceivedStuff::DotnetTracePath(pid, PathBuf::from(nettrace_path.clone()))); + } + eprintln!("New task: {}", pid); path_senders_per_pid.insert(pid, path_sender); if send_result.is_err() { // The sampler has already shut down. This task arrived too late. From 9491402a548a1298d335dc3fb24a5d4c249c998d Mon Sep 17 00:00:00 2001 From: Vladimir Vukicevic Date: Wed, 14 Aug 2024 17:28:13 -0400 Subject: [PATCH 14/36] misc clippy fixes --- samply/src/mac/profiler.rs | 2 +- .../src/shared/coreclr/dotnet_trace_manager.rs | 17 +++++++---------- samply/src/shared/coreclr/eventpipe.rs | 2 +- samply/src/shared/coreclr/markers.rs | 1 - samply/src/shared/coreclr/mod.rs | 1 - samply/src/shared/coreclr/provider.rs | 2 +- 6 files changed, 10 insertions(+), 15 deletions(-) diff --git a/samply/src/mac/profiler.rs b/samply/src/mac/profiler.rs index ba404a4ac..99d651ea8 100644 --- a/samply/src/mac/profiler.rs +++ b/samply/src/mac/profiler.rs @@ -25,7 +25,7 @@ use crate::shared::symbol_props::SymbolProps; pub fn start_recording( recording_mode: RecordingMode, recording_props: RecordingProps, - mut profile_creation_props: ProfileCreationProps, + profile_creation_props: ProfileCreationProps, symbol_props: SymbolProps, server_props: Option, ) -> Result { diff --git a/samply/src/shared/coreclr/dotnet_trace_manager.rs b/samply/src/shared/coreclr/dotnet_trace_manager.rs index 29e25a6bd..db7479158 100644 --- a/samply/src/shared/coreclr/dotnet_trace_manager.rs +++ b/samply/src/shared/coreclr/dotnet_trace_manager.rs @@ -6,10 +6,9 @@ use crate::shared::jit_category_manager::JitCategoryManager; use crate::shared::lib_mappings::{LibMappingAdd, LibMappingInfo, LibMappingOp, LibMappingOpQueue}; use crate::shared::timestamp_converter::TimestampConverter; use debugid::CodeId; -use eventpipe::{EventPipeParser, NettraceEvent}; +use eventpipe::{EventPipeParser}; use fxprof_processed_profile::{ - CategoryHandle, LibraryHandle, LibraryInfo, MarkerTiming, Profile, Symbol, SymbolTable, - ThreadHandle, + LibraryHandle, LibraryInfo, Profile, Symbol, SymbolTable, }; use wholesym::samply_symbols::debug_id_and_code_id_for_jitdump; @@ -53,7 +52,7 @@ impl DotnetTraceManager { let file = std::fs::File::open(path).ok()?; let reader = EventPipeParser::new(file).ok()?; if unlink_after_open { - std::fs::remove_file(&path).ok()?; + std::fs::remove_file(path).ok()?; } Some((reader, path.into())) } @@ -226,12 +225,10 @@ impl SingleDotnetTraceProcessor { let msig = (event.start_address, event.name.name.clone()); if !event.dc_end { self.seen_method_loads.insert(msig); - } else { - if self.seen_method_loads.contains(&msig) { - // we already saw a normal MethodLoad for this; skip it, so that - // we don't flag this method as being valid from 0 time - return; - } + } else if self.seen_method_loads.contains(&msig) { + // we already saw a normal MethodLoad for this; skip it, so that + // we don't flag this method as being valid from 0 time + return; } let relative_address_at_start = self.cumulative_address; diff --git a/samply/src/shared/coreclr/eventpipe.rs b/samply/src/shared/coreclr/eventpipe.rs index eb838f125..736e8f708 100644 --- a/samply/src/shared/coreclr/eventpipe.rs +++ b/samply/src/shared/coreclr/eventpipe.rs @@ -18,7 +18,7 @@ pub fn eventpipe_event_to_coreclr_event( timestamp: ne.timestamp, process_id, thread_id: ne.thread_id as u32, - stack: if ne.stack.len() > 0 { + stack: if !ne.stack.is_empty() { Some(ne.stack.clone()) } else { None diff --git a/samply/src/shared/coreclr/markers.rs b/samply/src/shared/coreclr/markers.rs index e5d7b760b..b0a2c4a08 100644 --- a/samply/src/shared/coreclr/markers.rs +++ b/samply/src/shared/coreclr/markers.rs @@ -2,7 +2,6 @@ use fxprof_processed_profile::{ MarkerFieldFormat, MarkerFieldSchema, MarkerLocation, MarkerSchema, MarkerStaticField, StaticSchemaMarker, StringHandle, CategoryHandle, Profile }; -use serde_json::json; // String is type name #[derive(Debug, Clone)] diff --git a/samply/src/shared/coreclr/mod.rs b/samply/src/shared/coreclr/mod.rs index 7e7e33cf6..2f81051f0 100644 --- a/samply/src/shared/coreclr/mod.rs +++ b/samply/src/shared/coreclr/mod.rs @@ -9,7 +9,6 @@ use std::fmt::Display; pub use dotnet_trace_manager::*; pub use eventpipe::*; pub use events::*; -pub use markers::*; pub use provider::*; #[derive(Debug, Clone)] diff --git a/samply/src/shared/coreclr/provider.rs b/samply/src/shared/coreclr/provider.rs index 8846a488b..1a38fd441 100644 --- a/samply/src/shared/coreclr/provider.rs +++ b/samply/src/shared/coreclr/provider.rs @@ -60,7 +60,7 @@ pub fn coreclr_provider_args(props: CoreClrProviderProps) -> Vec { | CORECLR_GC_SAMPLED_OBJECT_ALLOCATION_LOW_KEYWORD; } - verbose_keywords = verbose_keywords | info_keywords; + verbose_keywords |= info_keywords; info_keywords = 0; if info_keywords != 0 { From 51ff2e3c1e73cc4a857cb761950c7ffdccea4c4a Mon Sep 17 00:00:00 2001 From: Vladimir Vukicevic Date: Wed, 14 Aug 2024 17:39:22 -0400 Subject: [PATCH 15/36] misc clippy fixes --- samply/src/shared/coreclr/dotnet_trace_manager.rs | 13 ++++++------- samply/src/shared/coreclr/markers.rs | 8 ++++---- samply/src/shared/coreclr/mod.rs | 3 ++- samply/src/windows/coreclr.rs | 1 - samply/src/windows/etw_coreclr.rs | 13 +++---------- samply/src/windows/etw_gecko.rs | 1 - 6 files changed, 15 insertions(+), 24 deletions(-) diff --git a/samply/src/shared/coreclr/dotnet_trace_manager.rs b/samply/src/shared/coreclr/dotnet_trace_manager.rs index db7479158..58d22b4d2 100644 --- a/samply/src/shared/coreclr/dotnet_trace_manager.rs +++ b/samply/src/shared/coreclr/dotnet_trace_manager.rs @@ -208,15 +208,14 @@ impl SingleDotnetTraceProcessor { _timestamp_converter: &TimestampConverter, ) { match coreclr_event { - CoreClrEvent::ModuleLoad(_event) => { - //let module_id = event.module_id; + CoreClrEvent::ModuleLoad(event) => { + let module_id = event.module_id; //log::trace!("Loading module {} {} at {}", module_id, event.module_il_path, event.common.timestamp); - //self.modules.insert(module_id, event.clone()); + self.modules.insert(module_id, event.clone()); } - CoreClrEvent::ModuleUnload(_event) => { - //let module_id = event.module_id; - //if let Some(module) = self.modules.remove(&module_id) { - //} + CoreClrEvent::ModuleUnload(event) => { + let module_id = event.module_id; + self.modules.remove(&module_id); } CoreClrEvent::MethodLoad(event) => { let start_avma = event.start_address; diff --git a/samply/src/shared/coreclr/markers.rs b/samply/src/shared/coreclr/markers.rs index b0a2c4a08..11539db6d 100644 --- a/samply/src/shared/coreclr/markers.rs +++ b/samply/src/shared/coreclr/markers.rs @@ -5,7 +5,7 @@ use fxprof_processed_profile::{ // String is type name #[derive(Debug, Clone)] -pub struct CoreClrGcAllocTickMarker(pub StringHandle, pub usize, pub usize, pub CategoryHandle); +pub(crate) struct CoreClrGcAllocTickMarker(pub StringHandle, pub usize, pub usize, pub CategoryHandle); impl StaticSchemaMarker for CoreClrGcAllocTickMarker { const UNIQUE_MARKER_TYPE_NAME: &'static str = "GC Alloc Tick"; @@ -73,7 +73,7 @@ impl StaticSchemaMarker for CoreClrGcAllocTickMarker { } #[derive(Debug, Clone)] -pub struct CoreClrGcAllocMarker(pub StringHandle, pub usize, pub CategoryHandle); +pub(crate) struct CoreClrGcAllocMarker(pub StringHandle, pub usize, pub CategoryHandle); impl StaticSchemaMarker for CoreClrGcAllocMarker { const UNIQUE_MARKER_TYPE_NAME: &'static str = "GC Alloc"; @@ -130,7 +130,7 @@ impl StaticSchemaMarker for CoreClrGcAllocMarker { } #[derive(Debug, Clone)] -pub struct CoreClrGcEventMarker(pub StringHandle, pub StringHandle, pub CategoryHandle); +pub(crate) struct CoreClrGcEventMarker(pub StringHandle, pub StringHandle, pub CategoryHandle); impl StaticSchemaMarker for CoreClrGcEventMarker { const UNIQUE_MARKER_TYPE_NAME: &'static str = "GC Event"; @@ -177,7 +177,7 @@ impl StaticSchemaMarker for CoreClrGcEventMarker { } #[derive(Debug, Clone)] -pub struct CoreClrGcMarker(pub CategoryHandle); +pub(crate) struct CoreClrGcMarker(pub CategoryHandle); impl StaticSchemaMarker for CoreClrGcMarker { const UNIQUE_MARKER_TYPE_NAME: &'static str = "GC"; diff --git a/samply/src/shared/coreclr/mod.rs b/samply/src/shared/coreclr/mod.rs index 2f81051f0..4d935d953 100644 --- a/samply/src/shared/coreclr/mod.rs +++ b/samply/src/shared/coreclr/mod.rs @@ -6,10 +6,10 @@ mod provider; use std::fmt::Display; -pub use dotnet_trace_manager::*; pub use eventpipe::*; pub use events::*; pub use provider::*; +pub use markers::*; #[derive(Debug, Clone)] pub struct CoreClrProviderProps { @@ -20,6 +20,7 @@ pub struct CoreClrProviderProps { pub event_stacks: bool, } +#[allow(dead_code)] pub(crate) struct SavedMarkerInfo { pub start_timestamp_raw: u64, pub name: String, diff --git a/samply/src/windows/coreclr.rs b/samply/src/windows/coreclr.rs index 29bdfc766..6f1fb6160 100644 --- a/samply/src/windows/coreclr.rs +++ b/samply/src/windows/coreclr.rs @@ -13,7 +13,6 @@ use etw_reader::{ use crate::shared::coreclr::*; use crate::shared::process_sample_data::SimpleMarker; -use crate::shared::recording_props::{CoreClrProfileProps, ProfileCreationProps}; use crate::windows::profile_context::{KnownCategory, ProfileContext}; diff --git a/samply/src/windows/etw_coreclr.rs b/samply/src/windows/etw_coreclr.rs index f92f9f74f..3f9298aec 100644 --- a/samply/src/windows/etw_coreclr.rs +++ b/samply/src/windows/etw_coreclr.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, convert::TryInto, fmt::Display}; +use std::{collections::HashMap, convert::TryInto}; use eventpipe::coreclr::{GcSuspendEeReason, GcType}; use fxprof_processed_profile::*; @@ -6,18 +6,13 @@ use num_traits::FromPrimitive; use etw_reader::{self, schema::TypedEvent}; use etw_reader::{ - event_properties_to_string, parser::{Parser, TryParse}, }; use crate::shared::coreclr::*; -use crate::shared::process_sample_data::SimpleMarker; -use crate::shared::recording_props::{CoreClrProfileProps, ProfileCreationProps}; -use crate::windows::profile_context::{KnownCategory, ProfileContext}; use super::coreclr::CoreClrContext; -use super::elevated_helper::ElevatedRecordingProps; pub struct CoreClrEtwConverter { last_event_on_thread: HashMap, @@ -69,7 +64,7 @@ impl CoreClrEtwConverter { // ETL shows the same (correct) names as the merged ETL. // // We try to hack around this by converting the unmerged name to the converted one here. - if task.ends_with(" ") || opcode.ends_with(" ") { + if task.ends_with(' ') || opcode.ends_with(' ') { task = task.trim(); opcode = opcode.trim(); @@ -96,9 +91,7 @@ impl CoreClrEtwConverter { }, "Runtime" => { task = "CLRRuntimeInformation"; - opcode = match opcode { - _ => opcode.trim(), - }; + opcode = opcode.trim(); }, "GC" => { task = "GarbageCollection"; diff --git a/samply/src/windows/etw_gecko.rs b/samply/src/windows/etw_gecko.rs index 1614fd5a0..68c55850e 100644 --- a/samply/src/windows/etw_gecko.rs +++ b/samply/src/windows/etw_gecko.rs @@ -13,7 +13,6 @@ use uuid::Uuid; use super::coreclr::CoreClrContext; use super::profile_context::ProfileContext; -use crate::shared::coreclr::CoreClrProviderProps; use crate::windows::coreclr::handle_new_coreclr_event; use crate::windows::etw_coreclr::CoreClrEtwConverter; use crate::windows::profile_context::{KnownCategory, PeInfo}; From 792995a653d334154e6c1454163d72a18839bb5d Mon Sep 17 00:00:00 2001 From: Vladimir Vukicevic Date: Wed, 14 Aug 2024 17:42:01 -0400 Subject: [PATCH 16/36] --amend --- samply/src/shared/coreclr/markers.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/samply/src/shared/coreclr/markers.rs b/samply/src/shared/coreclr/markers.rs index 11539db6d..b0a2c4a08 100644 --- a/samply/src/shared/coreclr/markers.rs +++ b/samply/src/shared/coreclr/markers.rs @@ -5,7 +5,7 @@ use fxprof_processed_profile::{ // String is type name #[derive(Debug, Clone)] -pub(crate) struct CoreClrGcAllocTickMarker(pub StringHandle, pub usize, pub usize, pub CategoryHandle); +pub struct CoreClrGcAllocTickMarker(pub StringHandle, pub usize, pub usize, pub CategoryHandle); impl StaticSchemaMarker for CoreClrGcAllocTickMarker { const UNIQUE_MARKER_TYPE_NAME: &'static str = "GC Alloc Tick"; @@ -73,7 +73,7 @@ impl StaticSchemaMarker for CoreClrGcAllocTickMarker { } #[derive(Debug, Clone)] -pub(crate) struct CoreClrGcAllocMarker(pub StringHandle, pub usize, pub CategoryHandle); +pub struct CoreClrGcAllocMarker(pub StringHandle, pub usize, pub CategoryHandle); impl StaticSchemaMarker for CoreClrGcAllocMarker { const UNIQUE_MARKER_TYPE_NAME: &'static str = "GC Alloc"; @@ -130,7 +130,7 @@ impl StaticSchemaMarker for CoreClrGcAllocMarker { } #[derive(Debug, Clone)] -pub(crate) struct CoreClrGcEventMarker(pub StringHandle, pub StringHandle, pub CategoryHandle); +pub struct CoreClrGcEventMarker(pub StringHandle, pub StringHandle, pub CategoryHandle); impl StaticSchemaMarker for CoreClrGcEventMarker { const UNIQUE_MARKER_TYPE_NAME: &'static str = "GC Event"; @@ -177,7 +177,7 @@ impl StaticSchemaMarker for CoreClrGcEventMarker { } #[derive(Debug, Clone)] -pub(crate) struct CoreClrGcMarker(pub CategoryHandle); +pub struct CoreClrGcMarker(pub CategoryHandle); impl StaticSchemaMarker for CoreClrGcMarker { const UNIQUE_MARKER_TYPE_NAME: &'static str = "GC"; From 8d047222628e9fe39972d7b6b9508ff1fc2c8a2a Mon Sep 17 00:00:00 2001 From: Vladimir Vukicevic Date: Mon, 19 Aug 2024 13:36:17 -0700 Subject: [PATCH 17/36] Start moving around eventpipe-rs --- eventpipe-rs/Cargo.toml | 4 + eventpipe-rs/examples/dump-nettrace.rs | 7 +- .../{coreclr_enums.rs => coreclr/enums.rs} | 0 eventpipe-rs/src/coreclr/events.rs | 186 +++++ eventpipe-rs/src/coreclr/mod.rs | 7 + .../nettrace.rs} | 189 +---- eventpipe-rs/src/eventpipe/mod.rs | 261 +++++++ eventpipe-rs/src/eventpipe/parser.rs | 403 +++++++++++ eventpipe-rs/src/lib.rs | 657 +----------------- 9 files changed, 874 insertions(+), 840 deletions(-) rename eventpipe-rs/src/{coreclr_enums.rs => coreclr/enums.rs} (100%) create mode 100644 eventpipe-rs/src/coreclr/events.rs create mode 100644 eventpipe-rs/src/coreclr/mod.rs rename eventpipe-rs/src/{coreclr_events.rs => coreclr/nettrace.rs} (51%) create mode 100644 eventpipe-rs/src/eventpipe/mod.rs create mode 100644 eventpipe-rs/src/eventpipe/parser.rs diff --git a/eventpipe-rs/Cargo.toml b/eventpipe-rs/Cargo.toml index cd2b242db..9f58b5dfd 100644 --- a/eventpipe-rs/Cargo.toml +++ b/eventpipe-rs/Cargo.toml @@ -10,5 +10,9 @@ num-traits = "0.2" num-derive = "0.4" log = "0.4.21" +[target.'cfg(windows)'.dependencies] +etw-reader = { path = "../etw-reader" } + +# linux-perf-data = "0.10.1" [[example]] name = "dump-nettrace" \ No newline at end of file diff --git a/eventpipe-rs/examples/dump-nettrace.rs b/eventpipe-rs/examples/dump-nettrace.rs index 8b7a93344..0f185451f 100644 --- a/eventpipe-rs/examples/dump-nettrace.rs +++ b/eventpipe-rs/examples/dump-nettrace.rs @@ -1,8 +1,9 @@ #![allow(unused)] use std::fs::File; -use eventpipe::*; -use eventpipe::coreclr::CoreClrEvent; +use ::eventpipe::*; +use ::eventpipe::eventpipe::EventPipeParser; +use ::eventpipe::coreclr::CoreClrEvent; // https://github.com/microsoft/perfview/blob/main/src/TraceEvent/EventPipe/EventPipeFormat.md @@ -19,7 +20,7 @@ fn main() { // continue; //} - match eventpipe::decode_event(&event) { + match ::eventpipe::decode_event(&event) { DecodedEvent::CoreClrEvent(coreclr_event) => { match coreclr_event { CoreClrEvent::MethodLoad(event) => { diff --git a/eventpipe-rs/src/coreclr_enums.rs b/eventpipe-rs/src/coreclr/enums.rs similarity index 100% rename from eventpipe-rs/src/coreclr_enums.rs rename to eventpipe-rs/src/coreclr/enums.rs diff --git a/eventpipe-rs/src/coreclr/events.rs b/eventpipe-rs/src/coreclr/events.rs new file mode 100644 index 000000000..b9d371a42 --- /dev/null +++ b/eventpipe-rs/src/coreclr/events.rs @@ -0,0 +1,186 @@ +// CoreCLR needs from ETW: +// +// DOTNetProvider: +// +// CLRMethod | CLRMethodRundown +// - MethodLoad_V1 (136), MethodUnLoad_V1 (137), MethodDCStart_V1 (137 -- Rundown), MethodDCEnd_V1 (138 -- Rundown) +// - MethodLoadVerbose_V1 (143) | MethodDCStartVerbose_V1 (141) +// - MethodDCEndVerbose (144) +// - ModuleLoad_V2 (152) | ModuleUnload_V2 (153) | ModuleDCStart_V2 (153) | ModuleDCEnd_V2 (154) +// - DomainModuleLoad_V1 (151) | DomainModuleDCStart_V1 (151 - rundown) | DomainModuleDCEnd_V1 (152 - rundown) +// Type +// - BulkType +// CLRStack (doesn't exist) +// - CLRStackWalk +// GarbageCollection +// - GCAllocationTick_V2 (10) +// - GCSampledObjectAllocation +// - Triggered +// - GCSuspendEE (9) +// - GCSuspendEEEnd (8) +// - GCRestartEEBegin (7) +// - GCRestartEEEnd (3) +// - "win:Start" - GCStart_V1 - 1 +// - "win:Stop" - GCEnd_V1 - 2 +// - SetGCHandle +// - DestroyGCHandle +// - GCFinalizersBegin (14) | GCFinalizersEnd (13) | FinalizeObject +// - GCCreateSegment (5) | GCFreeSegment (6) | GCDynamicEvent | GCHeapStats (4) +// XXX AppDomains +// - AppDomainLoad_V1 (156) | AppDomainUnLoad_V1 (157) +// - AppDomainDCStart_V1 (157 -- rundown) | AppDomainDCEnd_V1 (158 -- rundown) +// +// don't need: +// +// CLRRuntimeInformation +// CLRLoader + +use bitflags::bitflags; + +use std::{ + fmt::Display, + io::{Cursor, Read, Seek}, +}; + +use binrw::{BinRead, BinReaderExt, NullWideString}; +use num_derive::{FromPrimitive, ToPrimitive}; +use crate::eventpipe::{MetadataDefinition, NettraceEvent}; +use super::*; + +#[derive(BinRead, Debug)] +#[br(little, import { version: u32, app_domain: bool })] +pub struct ModuleLoadUnloadEvent { + pub module_id: u64, + pub assembly_id: u64, + #[br(if(app_domain))] + pub app_domain_id: Option, + pub module_flags: u32, + pub _reserved1: u32, + pub module_il_path: NullWideString, + pub module_native_path: NullWideString, + #[br(if(version >= 1))] + pub clr_instance_id: Option, + #[br(if(version >= 2))] + pub managed_pdb_signature: [u8; 16], + #[br(if(version >= 2))] + pub managed_pdb_age: u32, + #[br(if(version >= 2))] + pub managed_pdb_build_path: NullWideString, + #[br(if(version >= 2))] + pub native_pdb_signature: [u8; 16], + #[br(if(version >= 2))] + pub native_pdb_age: u32, + #[br(if(version >= 2))] + pub native_pdb_build_path: NullWideString, +} + +#[derive(BinRead, Debug)] +#[br(little, import { version: u32, verbose: bool })] +pub struct MethodLoadUnloadEvent { + pub method_id: u64, + pub module_id: u64, + pub method_start_address: u64, + pub method_size: u32, + pub method_token: u32, + pub method_flags: u32, + + #[br(if(verbose))] + pub method_namespace: NullWideString, + + #[br(if(verbose))] + pub method_name: NullWideString, + + #[br(if(verbose))] + pub method_signature: NullWideString, + + #[br(if(version >= 1, None))] + pub clr_instance_id: Option, + + #[br(if(version >= 2, None))] + pub re_jit_id: Option, +} + +#[derive(BinRead, Debug)] +#[br(little, import { version: u32 })] +pub struct GcTriggeredEvent { + pub reason: GcReason, + pub clr_instance_id: u16, +} + +#[derive(BinRead, Debug)] +#[br(little, import { version: u32 })] +pub struct GcStartEvent { + pub count: u32, + #[br(if(version >= 1, None))] + pub depth: Option, + pub reason: GcReason, + #[br(if(version >= 1, None))] + pub gc_type: Option, + #[br(if(version >= 1, None))] + pub clr_instance_id: Option, + #[br(if(version >= 2, None))] + pub client_sequence_number: Option, +} + +#[derive(BinRead, Debug)] +#[br(little, import { version: u32 })] +pub struct GcEndEvent { + pub count: u32, + pub depth: u32, + #[br(if(version >= 1, None))] + pub reason: Option, +} + +#[derive(BinRead, Debug)] +#[br(little, import { version: u32 })] +pub struct GcAllocationTickEvent { + pub allocation_amount: u32, + pub allocation_kind: GcAllocationKind, + pub clr_instance_id: u16, + #[br(if(version >= 2))] + pub allocation_amount64: u64, + #[br(if(version >= 2))] + pub type_id: u64, // pointer + #[br(if(version >= 2))] + pub type_name: NullWideString, + #[br(if(version >= 2))] + pub heap_index: u32, + #[br(if(version >= 3))] + pub address: Option, // pointer + #[br(if(version >= 4))] + pub object_size: Option, +} + +#[derive(BinRead, Debug)] +#[br(little, import { version: u32 })] +pub struct GcSampledObjectAllocationEvent { + pub address: u64, // pointer + pub type_id: u64, // pointer + pub object_count_for_type_sample: u32, + pub total_size_for_type_sample: u64, + pub clr_instance_id: u16, +} + +#[derive(BinRead, Debug)] +#[br(little, import { version: u32 })] +pub struct ReadyToRunGetEntryPointEvent { + pub method_id: u64, + pub method_namespace: NullWideString, + pub method_name: NullWideString, + pub method_signature: NullWideString, + pub entry_point: u64, + pub clr_instance_id: u16, +} + +pub enum CoreClrEvent { + ModuleLoad(ModuleLoadUnloadEvent), + ModuleUnload(ModuleLoadUnloadEvent), + MethodLoad(MethodLoadUnloadEvent), + MethodUnload(MethodLoadUnloadEvent), + GcTriggered(GcTriggeredEvent), + GcAllocationTick(GcAllocationTickEvent), + GcSampledObjectAllocation(GcSampledObjectAllocationEvent), + ReadyToRunGetEntryPoint(ReadyToRunGetEntryPointEvent), + MethodDCEnd(MethodLoadUnloadEvent), +} + diff --git a/eventpipe-rs/src/coreclr/mod.rs b/eventpipe-rs/src/coreclr/mod.rs new file mode 100644 index 000000000..ddb222997 --- /dev/null +++ b/eventpipe-rs/src/coreclr/mod.rs @@ -0,0 +1,7 @@ +mod enums; +mod events; +mod nettrace; + +pub use enums::*; +pub use events::*; +pub use nettrace::*; \ No newline at end of file diff --git a/eventpipe-rs/src/coreclr_events.rs b/eventpipe-rs/src/coreclr/nettrace.rs similarity index 51% rename from eventpipe-rs/src/coreclr_events.rs rename to eventpipe-rs/src/coreclr/nettrace.rs index f1b9f78bd..35b7709f0 100644 --- a/eventpipe-rs/src/coreclr_events.rs +++ b/eventpipe-rs/src/coreclr/nettrace.rs @@ -1,188 +1,5 @@ -// CoreCLR needs from ETW: -// -// DOTNetProvider: -// -// CLRMethod | CLRMethodRundown -// - MethodLoad_V1 (136), MethodUnLoad_V1 (137), MethodDCStart_V1 (137 -- Rundown), MethodDCEnd_V1 (138 -- Rundown) -// - MethodLoadVerbose_V1 (143) | MethodDCStartVerbose_V1 (141) -// - MethodDCEndVerbose (144) -// - ModuleLoad_V2 (152) | ModuleUnload_V2 (153) | ModuleDCStart_V2 (153) | ModuleDCEnd_V2 (154) -// - DomainModuleLoad_V1 (151) | DomainModuleDCStart_V1 (151 - rundown) | DomainModuleDCEnd_V1 (152 - rundown) -// Type -// - BulkType -// CLRStack (doesn't exist) -// - CLRStackWalk -// GarbageCollection -// - GCAllocationTick_V2 (10) -// - GCSampledObjectAllocation -// - Triggered -// - GCSuspendEE (9) -// - GCSuspendEEEnd (8) -// - GCRestartEEBegin (7) -// - GCRestartEEEnd (3) -// - "win:Start" - GCStart_V1 - 1 -// - "win:Stop" - GCEnd_V1 - 2 -// - SetGCHandle -// - DestroyGCHandle -// - GCFinalizersBegin (14) | GCFinalizersEnd (13) | FinalizeObject -// - GCCreateSegment (5) | GCFreeSegment (6) | GCDynamicEvent | GCHeapStats (4) -// XXX AppDomains -// - AppDomainLoad_V1 (156) | AppDomainUnLoad_V1 (157) -// - AppDomainDCStart_V1 (157 -- rundown) | AppDomainDCEnd_V1 (158 -- rundown) -// -// don't need: -// -// CLRRuntimeInformation -// CLRLoader - -use bitflags::bitflags; - -use std::{ - fmt::Display, - io::{Cursor, Read, Seek}, -}; - -use crate::coreclr_enums::*; -use crate::{MetadataDefinition, NettraceEvent}; -use binrw::{BinRead, BinReaderExt, NullWideString}; -use num_derive::{FromPrimitive, ToPrimitive}; - -#[derive(BinRead, Debug)] -#[br(little, import { version: u32, app_domain: bool })] -pub struct ModuleLoadUnloadEvent { - pub module_id: u64, - pub assembly_id: u64, - #[br(if(app_domain))] - pub app_domain_id: Option, - pub module_flags: u32, - pub _reserved1: u32, - pub module_il_path: NullWideString, - pub module_native_path: NullWideString, - #[br(if(version >= 1))] - pub clr_instance_id: Option, - #[br(if(version >= 2))] - pub managed_pdb_signature: [u8; 16], - #[br(if(version >= 2))] - pub managed_pdb_age: u32, - #[br(if(version >= 2))] - pub managed_pdb_build_path: NullWideString, - #[br(if(version >= 2))] - pub native_pdb_signature: [u8; 16], - #[br(if(version >= 2))] - pub native_pdb_age: u32, - #[br(if(version >= 2))] - pub native_pdb_build_path: NullWideString, -} - -#[derive(BinRead, Debug)] -#[br(little, import { version: u32, verbose: bool })] -pub struct MethodLoadUnloadEvent { - pub method_id: u64, - pub module_id: u64, - pub method_start_address: u64, - pub method_size: u32, - pub method_token: u32, - pub method_flags: u32, - - #[br(if(verbose))] - pub method_namespace: NullWideString, - - #[br(if(verbose))] - pub method_name: NullWideString, - - #[br(if(verbose))] - pub method_signature: NullWideString, - - #[br(if(version >= 1, None))] - pub clr_instance_id: Option, - - #[br(if(version >= 2, None))] - pub re_jit_id: Option, -} - -#[derive(BinRead, Debug)] -#[br(little, import { version: u32 })] -pub struct GcTriggeredEvent { - pub reason: GcReason, - pub clr_instance_id: u16, -} - -#[derive(BinRead, Debug)] -#[br(little, import { version: u32 })] -pub struct GcStartEvent { - pub count: u32, - #[br(if(version >= 1, None))] - pub depth: Option, - pub reason: GcReason, - #[br(if(version >= 1, None))] - pub gc_type: Option, - #[br(if(version >= 1, None))] - pub clr_instance_id: Option, - #[br(if(version >= 2, None))] - pub client_sequence_number: Option, -} - -#[derive(BinRead, Debug)] -#[br(little, import { version: u32 })] -pub struct GcEndEvent { - pub count: u32, - pub depth: u32, - #[br(if(version >= 1, None))] - pub reason: Option, -} - -#[derive(BinRead, Debug)] -#[br(little, import { version: u32 })] -pub struct GcAllocationTickEvent { - pub allocation_amount: u32, - pub allocation_kind: GcAllocationKind, - pub clr_instance_id: u16, - #[br(if(version >= 2))] - pub allocation_amount64: u64, - #[br(if(version >= 2))] - pub type_id: u64, // pointer - #[br(if(version >= 2))] - pub type_name: NullWideString, - #[br(if(version >= 2))] - pub heap_index: u32, - #[br(if(version >= 3))] - pub address: Option, // pointer - #[br(if(version >= 4))] - pub object_size: Option, -} - -#[derive(BinRead, Debug)] -#[br(little, import { version: u32 })] -pub struct GcSampledObjectAllocationEvent { - pub address: u64, // pointer - pub type_id: u64, // pointer - pub object_count_for_type_sample: u32, - pub total_size_for_type_sample: u64, - pub clr_instance_id: u16, -} - -#[derive(BinRead, Debug)] -#[br(little, import { version: u32 })] -pub struct ReadyToRunGetEntryPointEvent { - pub method_id: u64, - pub method_namespace: NullWideString, - pub method_name: NullWideString, - pub method_signature: NullWideString, - pub entry_point: u64, - pub clr_instance_id: u16, -} - -pub enum CoreClrEvent { - ModuleLoad(ModuleLoadUnloadEvent), - ModuleUnload(ModuleLoadUnloadEvent), - MethodLoad(MethodLoadUnloadEvent), - MethodUnload(MethodLoadUnloadEvent), - GcTriggered(GcTriggeredEvent), - GcAllocationTick(GcAllocationTickEvent), - GcSampledObjectAllocation(GcSampledObjectAllocationEvent), - ReadyToRunGetEntryPoint(ReadyToRunGetEntryPointEvent), - MethodDCEnd(MethodLoadUnloadEvent), -} +use super::*; +use crate::*; pub fn decode_coreclr_event(event: &NettraceEvent) -> Option { match event.provider_name.as_str() { @@ -359,4 +176,4 @@ fn decode_coreclr_rundown_event(event: &NettraceEvent) -> Option { } _ => None, } -} +} \ No newline at end of file diff --git a/eventpipe-rs/src/eventpipe/mod.rs b/eventpipe-rs/src/eventpipe/mod.rs new file mode 100644 index 000000000..f5c7f2c9d --- /dev/null +++ b/eventpipe-rs/src/eventpipe/mod.rs @@ -0,0 +1,261 @@ +use binrw::io::TakeSeekExt; +use binrw::{binrw, BinRead, BinReaderExt, BinResult, NullWideString}; +use std::collections::HashMap; +use std::fmt::Display; +use std::fs::File; +use std::io::{BufRead, Cursor, Read, Seek, SeekFrom}; +use std::mem; + +use crate::helpers::*; + +pub mod parser; +pub use parser::*; + +// https://github.com/microsoft/perfview/blob/main/src/TraceEvent/EventPipe/EventPipeFormat.md + +#[derive(BinRead)] +#[br(little)] +pub struct NettraceString { + length: u32, + + #[br(count = length)] + bytes: Vec, +} + +impl NettraceString { + fn as_str(&self) -> &str { + std::str::from_utf8(&self.bytes).unwrap() + } +} + +impl std::fmt::Debug for NettraceString { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "\"{}\"", self.as_str()) + } +} + +impl std::fmt::Display for NettraceString { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "\"{}\"", self.as_str()) + } +} + +#[derive(BinRead, Debug)] +#[br(little, magic = b"Nettrace")] +pub struct NettraceHeader { + ident: NettraceString, +} + +#[derive(BinRead, Debug, Eq, PartialEq)] +#[br(repr(u8))] +pub enum NettraceTag { + Invalid = 0, + NullReference = 1, + BeginPrivateObject = 5, + EndObject = 6, +} + +// Type objects have a NullReference tag as their type; every object will start with a type object, +// so pull it out into a separate struct instead of as part of the enum +#[derive(BinRead, Debug)] +#[br(little, magic = b"\x05\x01")] +pub struct NettraceTypeObject { + version: u32, + minimum_reader_version: u32, + type_name: NettraceString, + end_object: NettraceTag, +} + +#[derive(BinRead, Debug, Clone, Copy)] +#[br(little)] +pub struct NettraceTime { + year: u16, + month: u16, + day_of_week: u16, + day: u16, + hour: u16, + minute: u16, + second: u16, + millisecond: u16, +} + +#[derive(BinRead, Debug, Clone, Copy)] +#[br(little)] +pub struct NettraceTraceObject { + sync_time_utc: NettraceTime, + sync_time_qpc: u64, + qpc_frequency: u64, + pointer_size: u32, + process_id: u32, + number_of_processors: u32, + expected_cpu_sampling_rate: u32, +} + +#[derive(BinRead, Debug)] +#[br(little)] +pub struct NettraceEventBlockHeader { + size: u16, + flags: u16, + min_timestamp: u64, + #[br(pad_after = size - 20)] + max_timestamp: u64, +} + +#[derive(BinRead, Debug)] +#[br(little)] +pub struct NettraceEventBlock { + #[br(align_after = 4)] + size: u32, + + header: NettraceEventBlockHeader, +} + +// This header can be either compressed or uncompressed. +// The uncompressed header can be parsed directly; the compressed +// header needs manual parsing (see parse_compressed_header below). +#[derive(BinRead, Debug, Default, Clone)] +#[br(little)] +pub struct EventBlobHeader { + size: u32, + raw_metadata_id: u32, // high bit is "IsSorted" flag + sequence_number: u32, + thread_id: u64, + capture_thread_id: u64, + processor_number: u32, + stack_id: u32, + timestamp: u64, + activity_id: [u8; 16], + related_activity_id: [u8; 16], + payload_size: u32, + + // at the end to not screw up alignment + #[br(calc = raw_metadata_id & 0x7fffffff)] + metadata_id: u32, + #[br(calc = raw_metadata_id & 0x80000000 != 0)] + is_sorted: bool, +} + +impl Display for EventBlobHeader { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "EventBlobHeader {{ metadata_id: {}, stack: {}, payload_size: {}, seqno: {:?}, thread_id: {} ({}), proc: {:?}, timestamp: {} }}", + self.metadata_id, self.stack_id, self.payload_size, + if self.sequence_number == u32::MAX { None } else { Some(self.sequence_number) }, + self.thread_id, self.capture_thread_id, + if self.processor_number == u32::MAX { None } else { Some(self.processor_number) }, + self.timestamp) + } +} + +#[derive(BinRead, Debug, PartialEq, Default)] +#[br(little, repr=u32)] +pub enum MetadataTypeCode { + #[default] + Empty = 0, + Object = 1, + DBNull = 2, + Boolean = 3, + Char = 4, + SByte = 5, + Byte = 6, + Int16 = 7, + UInt16 = 8, + Int32 = 9, + UInt32 = 10, + Int64 = 11, + UInt64 = 12, + Single = 13, + Double = 14, + Decimal = 15, + DateTime = 16, + String = 18, + Array = 19, +} + +#[derive(BinRead, Debug)] +#[br(little)] +pub struct MetadataFieldDefinition { + type_code: MetadataTypeCode, + + #[br(if(type_code == MetadataTypeCode::Array))] + array_type_code: MetadataTypeCode, + + #[br(if(type_code == MetadataTypeCode::Object || array_type_code == MetadataTypeCode::Object))] + definition: Option, + + field_name: NullWideString, +} + +#[derive(BinRead, Debug, Default)] +#[br(little)] +pub struct MetadataPayloadDefinition { + field_count: u32, + #[br(count = field_count)] + fields: Vec, +} + +#[derive(BinRead, Debug)] +#[br(little)] +pub struct MetadataDefinition { + id: u32, + provider_name: NullWideString, + event_id: u32, + event_name: NullWideString, + keywords: u64, + version: u32, + level: u32, + + // either v1, or replaced with v2 data from tag + fields: MetadataPayloadDefinition, + + // filled in based on opcode from tag + #[br(ignore)] + opcode: Option, + // following this, there may be additional tag fields -- based on the size specified + // in the header. We can't access that in binrw, so it'll have to get handled + // manually +} + +#[derive(BinRead, Debug, PartialEq, Default)] +#[br(little, repr=u8)] +pub enum MetadataTag { + #[default] + Invalid = 0, + OpCode = 1, + V2Params = 2, +} + +#[derive(BinRead, Debug)] +#[br(little)] +pub struct MetadataTaggedData { + size: u32, // this actually seems to be junk? + + tag: MetadataTag, + + #[br(if(tag == MetadataTag::OpCode))] + opcode: u8, + + #[br(if(tag == MetadataTag::V2Params))] + fields_v2: MetadataPayloadDefinition, +} + +#[derive(Debug)] +pub struct NettraceEvent { + pub provider_name: String, + pub event_id: u32, + pub event_name: Option, + pub event_keywords: u64, + pub event_version: u32, + pub event_level: u32, + pub event_opcode: Option, + + pub sequence_number: u32, + pub thread_id: u64, + pub capture_thread_id: u64, + pub processor_number: Option, + pub stack: Vec, + pub timestamp: u64, + pub activity_id: [u8; 16], + pub related_activity_id: [u8; 16], + + pub payload: Vec, +} diff --git a/eventpipe-rs/src/eventpipe/parser.rs b/eventpipe-rs/src/eventpipe/parser.rs new file mode 100644 index 000000000..fd9617885 --- /dev/null +++ b/eventpipe-rs/src/eventpipe/parser.rs @@ -0,0 +1,403 @@ +use binrw::io::TakeSeekExt; +use binrw::{binrw, BinRead, BinReaderExt, BinResult, NullWideString}; +use std::collections::HashMap; +use std::fmt::Display; +use std::fs::File; +use std::io::{BufRead, Cursor, Read, Seek, SeekFrom}; +use std::mem; + +use super::*; +use crate::*; + +trait ReadExactlyExt { + fn read_exactly(&mut self, len: u64) -> Vec; +} + +impl ReadExactlyExt for T { + fn read_exactly(&mut self, len: u64) -> Vec { + let mut buf = vec![0; len as usize]; + self.read_exact(&mut buf) + .expect("Failed to read exact bytes"); + buf + } +} + +pub type EventPipeError = binrw::Error; + +struct EventBlobIter { + data: Cursor>, + header: NettraceEventBlockHeader, + compressed_headers: bool, + blob_size: u64, + prev_header: EventBlobHeader, +} + +impl EventBlobIter { + pub fn new(block: NettraceEventBlock, mut data: Vec) -> Result { + //eprintln!("EventBlobIter::new: {:?}", block); + let compressed_headers = (block.header.flags & 1) != 0; + let blob_size = (block.size - block.header.size as u32) as u64; + Ok(EventBlobIter { + data: Cursor::new(data), + header: block.header, + compressed_headers, + blob_size, + prev_header: Default::default(), + }) + } + + fn parse_compressed_header( + reader: &mut R, + prev_header: &mut EventBlobHeader, + ) -> BinResult { + //eprintln!("\nPREV {:?}", prev_header); + let flags: u8 = reader.read_le()?; + fn is_set(flags: u8, bit: u8) -> bool { + (flags & (1 << bit)) != 0 + } + + //eprintln!("flags: 0b{:b}", flags); + + let mut header = EventBlobHeader::default(); + header.metadata_id = if is_set(flags, 0) { + parse_varint_u32(reader)? + } else { + prev_header.metadata_id + }; + if is_set(flags, 1) { + header.sequence_number = prev_header + .sequence_number + .wrapping_add_signed(parse_varint_i32(reader)?); + header.capture_thread_id = parse_varint_u64(reader)?; + header.processor_number = parse_varint_u32(reader)?; + } else { + header.sequence_number = prev_header.sequence_number; + header.capture_thread_id = prev_header.capture_thread_id; + header.processor_number = prev_header.processor_number; + } + + if header.metadata_id != 0 { + header.sequence_number = header.sequence_number.wrapping_add(1); + } + + header.thread_id = if is_set(flags, 2) { + parse_varint_u64(reader)? + } else { + prev_header.thread_id + }; + header.stack_id = if is_set(flags, 3) { + parse_varint_u32(reader)? + } else { + prev_header.stack_id + }; + header.timestamp = prev_header + .timestamp + .wrapping_add_signed(parse_varint_i64(reader)?); + header.activity_id = if is_set(flags, 4) { + reader.read_le()? + } else { + prev_header.activity_id + }; + header.related_activity_id = if is_set(flags, 5) { + reader.read_le()? + } else { + prev_header.related_activity_id + }; + header.is_sorted = is_set(flags, 6); + header.payload_size = if is_set(flags, 7) { + parse_varint_u32(reader)? + } else { + prev_header.payload_size + }; + + header.raw_metadata_id = if header.is_sorted { (1 << 31) } else { 0 } | header.metadata_id; // set is_sorted bit + + //eprintln!("{} [flags 0b{:b}]", header, flags); + + *prev_header = header.clone(); + + Ok(header) + } + + fn parse_header( + reader: &mut R, + prev_header: &mut EventBlobHeader, + is_compressed: bool, + ) -> BinResult { + if is_compressed { + Self::parse_compressed_header(reader, prev_header) + } else { + eprintln!("parsing uncompressed header"); + reader.read_le() + } + } +} + +// Assuming block is a +impl Iterator for EventBlobIter { + type Item = (EventBlobHeader, Vec); + + fn next(&mut self) -> Option { + if self.data.position() >= self.blob_size { + return None; + } + + let header = EventBlobIter::parse_header( + &mut self.data, + &mut self.prev_header, + self.compressed_headers, + ) + .expect("Failed to read EventBlobHeader"); + let payload = self.data.read_exactly(header.payload_size as u64); + + if !self.compressed_headers && header.payload_size & 3 != 0 { + let alignment_skip = 4 - (header.payload_size & 3); + self.data + .seek(SeekFrom::Current(alignment_skip as i64)) + .expect("Seek failed"); + } + + Some((header, payload)) + } +} + +pub struct EventPipeParser { + stream: R, + metadata_map: HashMap, + stack_map: HashMap>, + trace_info: Option, + cur_event_blob_iter: Option, +} + +impl EventPipeParser +where + R: Read + Seek, +{ + pub fn new(mut stream: R) -> Result { + let file_header = NettraceHeader::read(&mut stream)?; + if file_header.ident.bytes != b"!FastSerialization.1" { + return Err(EventPipeError::BadMagic { + found: Box::new(file_header.ident.bytes), + pos: stream.stream_position().unwrap(), + }); + } + + Ok(EventPipeParser { + stream, + metadata_map: HashMap::new(), + stack_map: HashMap::new(), + trace_info: None, + cur_event_blob_iter: None, + }) + } + + fn make_err(&mut self, message: &str) -> EventPipeError { + EventPipeError::AssertFail { + pos: self.stream.stream_position().unwrap(), + message: message.to_string(), + } + } + + // Return the NettraceTraceObject for this stream, parsing until it's available if necessary. + // It will be the first thing in the stream, so we won't miss it + pub fn trace_info(&mut self) -> Result { + if let Some(trace_info) = self.trace_info { + return Ok(trace_info); + } + + // If we don't have it, we're going to assume that it's going to be the first object + let Some(type_object) = self.advance_to_next_object()? else { + return Err(self.make_err("Expected NettraceTraceObject")); + }; + + if type_object.type_name.as_str() != "TraceObject" { + return Err(self.make_err("Expected TraceObject")); + } + + let trace_info = NettraceTraceObject::read(&mut self.stream)?; + self.trace_info = Some(trace_info.clone()); + + self.read_object_end()?; + + Ok(trace_info) + } + + fn read_object_end(&mut self) -> Result<(), EventPipeError> { + let end_tag = NettraceTag::read(&mut self.stream)?; + if end_tag != NettraceTag::EndObject { + return Err(self.make_err("Expected EndObject tag")); + } + + Ok(()) + } + + fn advance_to_next_object(&mut self) -> Result, EventPipeError> { + let start_tag = NettraceTag::read(&mut self.stream)?; + if start_tag == NettraceTag::NullReference { + // stream done + return Ok(None); + } + + if start_tag != NettraceTag::BeginPrivateObject { + return Err(self.make_err("Expected BeginPrivateObject tag")); + } + + // so much effort spent in NettraceTypeObject, when it's just one of 4 things + let obj_type = NettraceTypeObject::read(&mut self.stream)?; + Ok(Some(obj_type)) + } + + fn parse_event( + &mut self, + header: EventBlobHeader, + payload: Vec, + ) -> Result, EventPipeError> { + let metadata_id = header.metadata_id; + let metadata_def = + self.metadata_map + .get(&metadata_id) + .ok_or_else(|| EventPipeError::AssertFail { + pos: 0, + message: format!("Metadata definition {} not found", metadata_id), + })?; + + let mut event = NettraceEvent { + provider_name: metadata_def.provider_name.to_string(), + event_id: metadata_def.event_id, + event_name: if metadata_def.event_name.len() > 0 { + Some(metadata_def.event_name.to_string()) + } else { + None + }, + event_keywords: metadata_def.keywords, + event_version: metadata_def.version, + event_level: metadata_def.level, + event_opcode: metadata_def.opcode, + + sequence_number: header.sequence_number, + thread_id: header.thread_id, + capture_thread_id: header.capture_thread_id, + processor_number: if header.processor_number != u32::MAX { + Some(header.processor_number) + } else { + None + }, + stack: self + .stack_map + .get(&header.stack_id) + .cloned() + .unwrap_or_default(), + timestamp: header.timestamp, + activity_id: header.activity_id, + related_activity_id: header.related_activity_id, + + payload: payload, + }; + + Ok(Some(event)) + } + + pub fn next_event(&mut self) -> Result, EventPipeError> { + // If we're inside an event block already, keep iterating through it + if let Some(cur_event_iter) = self.cur_event_blob_iter.as_mut() { + if let Some((header, payload)) = cur_event_iter.next() { + return self.parse_event(header, payload); + } + + self.cur_event_blob_iter = None; + + // we don't read this when we read the event blob; we could, but we don't + self.read_object_end()?; + } + + loop { + // Keep reading from the data until we get to an EventBlock, in which case we'll + // jump back out into the above iterator. + // + // Anything that's not an EventBlock, we need to parse into internal data structures so we can + // expose proper events from the EventBlock. + + let obj_type = self.advance_to_next_object()?; + let Some(obj_type) = obj_type else { + return Ok(None); + }; + + let obj_type_name = obj_type.type_name.as_str(); + + match obj_type_name { + "Trace" => { + let trace_object = NettraceTraceObject::read(&mut self.stream)?; + log::trace!("Trace: {:?}", trace_object); + self.trace_info = Some(trace_object.clone()); + } + "MetadataBlock" => { + //eprintln!("MetadataBlock"); + let metadata_block = NettraceEventBlock::read(&mut self.stream)?; + let metadata_block_data = self.stream.read_exactly( + metadata_block.size as u64 - metadata_block.header.size as u64, + ); + self.handle_metadata_block(EventBlobIter::new( + metadata_block, + metadata_block_data, + )?)?; + } + "StackBlock" => { + //eprintln!("StackBlock"); + let stack_block = StackBlock::read(&mut self.stream)?; + let mut stack_id = stack_block.first_id; + for stack in stack_block.stacks { + self.stack_map.insert(stack_id, stack.stack); + stack_id += 1; + } + } + "SPBlock" => { + //eprintln!("SPBlock"); + let sp_block = SequencePointBlock::read(&mut self.stream)?; + } + "EventBlock" => { + //eprintln!("EventBlock"); + let event_block = NettraceEventBlock::read(&mut self.stream)?; + let event_block_data = self + .stream + .read_exactly(event_block.size as u64 - event_block.header.size as u64); + self.cur_event_blob_iter = + Some(EventBlobIter::new(event_block, event_block_data)?); + + // jump into the iterator at the start of this + return self.next_event(); + } + unknown => { + eprintln!("Unknown object type: {}", unknown); + return Err(self.make_err("Unknown object type")); + } + } + + self.read_object_end()?; + } + } + + fn handle_metadata_block(&mut self, mut iter: EventBlobIter) -> Result<(), EventPipeError> { + while let Some((header, mut data)) = iter.next() { + let mut payload = Cursor::new(&data); + let payload_size = header.payload_size as u64; + let mut metadata_def = MetadataDefinition::read(&mut payload)?; + + while payload.position() < payload_size { + let tag_data = MetadataTaggedData::read(&mut payload)?; + if tag_data.tag == MetadataTag::OpCode { + metadata_def.opcode = Some(tag_data.opcode); + } else if tag_data.tag == MetadataTag::V2Params { + assert_eq!( + metadata_def.fields.field_count, 0, + "Found v2 fields, but v1 fields were not empty" + ); + metadata_def.fields = tag_data.fields_v2; + } + } + + self.metadata_map.insert(metadata_def.id, metadata_def); + } + + Ok(()) + } +} diff --git a/eventpipe-rs/src/lib.rs b/eventpipe-rs/src/lib.rs index 889ffb106..695120eac 100644 --- a/eventpipe-rs/src/lib.rs +++ b/eventpipe-rs/src/lib.rs @@ -1,13 +1,9 @@ #![allow(unused)] -mod helpers; -mod coreclr_enums; -mod coreclr_events; +mod helpers; +pub mod eventpipe; -pub mod coreclr { - pub use super::coreclr_enums::*; - pub use super::coreclr_events::*; -} +pub mod coreclr; use binrw::io::TakeSeekExt; use binrw::{binrw, BinRead, BinReaderExt, BinResult, NullWideString}; @@ -18,105 +14,7 @@ use std::io::{BufRead, Cursor, Read, Seek, SeekFrom}; use std::mem; use helpers::*; - -// https://github.com/microsoft/perfview/blob/main/src/TraceEvent/EventPipe/EventPipeFormat.md - -#[derive(BinRead)] -#[br(little)] -pub struct NettraceString { - length: u32, - - #[br(count = length)] - bytes: Vec, -} - -impl NettraceString { - fn as_str(&self) -> &str { - std::str::from_utf8(&self.bytes).unwrap() - } -} - -impl std::fmt::Debug for NettraceString { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "\"{}\"", self.as_str()) - } -} - -impl std::fmt::Display for NettraceString { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "\"{}\"", self.as_str()) - } -} - -#[derive(BinRead, Debug)] -#[br(little, magic = b"Nettrace")] -pub struct NettraceHeader { - ident: NettraceString, -} - -#[derive(BinRead, Debug, Eq, PartialEq)] -#[br(repr(u8))] -pub enum NettraceTag { - Invalid = 0, - NullReference = 1, - BeginPrivateObject = 5, - EndObject = 6, -} - -// Type objects have a NullReference tag as their type; every object will start with a type object, -// so pull it out into a separate struct instead of as part of the enum -#[derive(BinRead, Debug)] -#[br(little, magic = b"\x05\x01")] -pub struct NettraceTypeObject { - version: u32, - minimum_reader_version: u32, - type_name: NettraceString, - end_object: NettraceTag, -} - -#[derive(BinRead, Debug, Clone, Copy)] -#[br(little)] -pub struct NettraceTime { - year: u16, - month: u16, - day_of_week: u16, - day: u16, - hour: u16, - minute: u16, - second: u16, - millisecond: u16, -} - -#[derive(BinRead, Debug, Clone, Copy)] -#[br(little)] -pub struct NettraceTraceObject { - sync_time_utc: NettraceTime, - sync_time_qpc: u64, - qpc_frequency: u64, - pointer_size: u32, - process_id: u32, - number_of_processors: u32, - expected_cpu_sampling_rate: u32, -} - -#[derive(BinRead, Debug)] -#[br(little)] -pub struct NettraceEventBlockHeader { - size: u16, - flags: u16, - min_timestamp: u64, - #[br(pad_after = size - 20)] - max_timestamp: u64, -} - -#[derive(BinRead, Debug)] -#[br(little)] -pub struct NettraceEventBlock { - #[br(align_after = 4)] - size: u32, - - header: NettraceEventBlockHeader, -} +use crate::eventpipe::NettraceEvent; #[derive(BinRead, Debug)] #[br(little)] @@ -161,560 +59,17 @@ pub struct SequencePointBlock { thread_sequence_numbers: Vec, } -// This header can be either compressed or uncompressed. -// The uncompressed header can be parsed directly; the compressed -// header needs manual parsing (see parse_compressed_header below). -#[derive(BinRead, Debug, Default, Clone)] -#[br(little)] -pub struct EventBlobHeader { - size: u32, - raw_metadata_id: u32, // high bit is "IsSorted" flag - sequence_number: u32, - thread_id: u64, - capture_thread_id: u64, - processor_number: u32, - stack_id: u32, - timestamp: u64, - activity_id: [u8; 16], - related_activity_id: [u8; 16], - payload_size: u32, - - // at the end to not screw up alignment - #[br(calc = raw_metadata_id & 0x7fffffff)] - metadata_id: u32, - #[br(calc = raw_metadata_id & 0x80000000 != 0)] - is_sorted: bool, -} - -impl Display for EventBlobHeader { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "EventBlobHeader {{ metadata_id: {}, stack: {}, payload_size: {}, seqno: {:?}, thread_id: {} ({}), proc: {:?}, timestamp: {} }}", - self.metadata_id, self.stack_id, self.payload_size, - if self.sequence_number == u32::MAX { None } else { Some(self.sequence_number) }, - self.thread_id, self.capture_thread_id, - if self.processor_number == u32::MAX { None } else { Some(self.processor_number) }, - self.timestamp) - } -} - -#[derive(BinRead, Debug, PartialEq, Default)] -#[br(little, repr=u32)] -pub enum MetadataTypeCode { - #[default] - Empty = 0, - Object = 1, - DBNull = 2, - Boolean = 3, - Char = 4, - SByte = 5, - Byte = 6, - Int16 = 7, - UInt16 = 8, - Int32 = 9, - UInt32 = 10, - Int64 = 11, - UInt64 = 12, - Single = 13, - Double = 14, - Decimal = 15, - DateTime = 16, - String = 18, - Array = 19, -} - -#[derive(BinRead, Debug)] -#[br(little)] -pub struct MetadataFieldDefinition { - type_code: MetadataTypeCode, - - #[br(if(type_code == MetadataTypeCode::Array))] - array_type_code: MetadataTypeCode, - - #[br(if(type_code == MetadataTypeCode::Object || array_type_code == MetadataTypeCode::Object))] - definition: Option, - - field_name: NullWideString, -} - -#[derive(BinRead, Debug, Default)] -#[br(little)] -pub struct MetadataPayloadDefinition { - field_count: u32, - #[br(count = field_count)] - fields: Vec, -} - -#[derive(BinRead, Debug)] -#[br(little)] -pub struct MetadataDefinition { - id: u32, - provider_name: NullWideString, - event_id: u32, - event_name: NullWideString, - keywords: u64, - version: u32, - level: u32, - - // either v1, or replaced with v2 data from tag - fields: MetadataPayloadDefinition, - - // filled in based on opcode from tag - #[br(ignore)] - opcode: Option, - // following this, there may be additional tag fields -- based on the size specified - // in the header. We can't access that in binrw, so it'll have to get handled - // manually -} - -#[derive(BinRead, Debug, PartialEq, Default)] -#[br(little, repr=u8)] -pub enum MetadataTag { - #[default] - Invalid = 0, - OpCode = 1, - V2Params = 2, -} - -#[derive(BinRead, Debug)] -#[br(little)] -pub struct MetadataTaggedData { - size: u32, // this actually seems to be junk? - - tag: MetadataTag, - - #[br(if(tag == MetadataTag::OpCode))] - opcode: u8, - - #[br(if(tag == MetadataTag::V2Params))] - fields_v2: MetadataPayloadDefinition, -} - -#[derive(Debug)] -pub struct NettraceEvent { - pub provider_name: String, - pub event_id: u32, - pub event_name: Option, - pub event_keywords: u64, - pub event_version: u32, - pub event_level: u32, - pub event_opcode: Option, - - pub sequence_number: u32, - pub thread_id: u64, - pub capture_thread_id: u64, - pub processor_number: Option, - pub stack: Vec, - pub timestamp: u64, - pub activity_id: [u8; 16], - pub related_activity_id: [u8; 16], - - pub payload: Vec, -} - -trait ReadExactlyExt { - fn read_exactly(&mut self, len: u64) -> Vec; -} - -impl ReadExactlyExt for T { - fn read_exactly(&mut self, len: u64) -> Vec { - let mut buf = vec![0; len as usize]; - self.read_exact(&mut buf) - .expect("Failed to read exact bytes"); - buf - } -} - -pub type EventPipeError = binrw::Error; - -struct EventBlobIter { - data: Cursor>, - header: NettraceEventBlockHeader, - compressed_headers: bool, - blob_size: u64, - prev_header: EventBlobHeader, -} - -impl EventBlobIter { - pub fn new(block: NettraceEventBlock, mut data: Vec) -> Result { - //eprintln!("EventBlobIter::new: {:?}", block); - let compressed_headers = (block.header.flags & 1) != 0; - let blob_size = (block.size - block.header.size as u32) as u64; - Ok(EventBlobIter { - data: Cursor::new(data), - header: block.header, - compressed_headers, - blob_size, - prev_header: Default::default(), - }) - } - - fn parse_compressed_header( - reader: &mut R, - prev_header: &mut EventBlobHeader, - ) -> BinResult { - //eprintln!("\nPREV {:?}", prev_header); - let flags: u8 = reader.read_le()?; - fn is_set(flags: u8, bit: u8) -> bool { - (flags & (1 << bit)) != 0 - } - - //eprintln!("flags: 0b{:b}", flags); - - let mut header = EventBlobHeader::default(); - header.metadata_id = if is_set(flags, 0) { - parse_varint_u32(reader)? - } else { - prev_header.metadata_id - }; - if is_set(flags, 1) { - header.sequence_number = prev_header - .sequence_number - .wrapping_add_signed(parse_varint_i32(reader)?); - header.capture_thread_id = parse_varint_u64(reader)?; - header.processor_number = parse_varint_u32(reader)?; - } else { - header.sequence_number = prev_header.sequence_number; - header.capture_thread_id = prev_header.capture_thread_id; - header.processor_number = prev_header.processor_number; - } - - if header.metadata_id != 0 { - header.sequence_number = header.sequence_number.wrapping_add(1); - } - - header.thread_id = if is_set(flags, 2) { - parse_varint_u64(reader)? - } else { - prev_header.thread_id - }; - header.stack_id = if is_set(flags, 3) { - parse_varint_u32(reader)? - } else { - prev_header.stack_id - }; - header.timestamp = prev_header - .timestamp - .wrapping_add_signed(parse_varint_i64(reader)?); - header.activity_id = if is_set(flags, 4) { - reader.read_le()? - } else { - prev_header.activity_id - }; - header.related_activity_id = if is_set(flags, 5) { - reader.read_le()? - } else { - prev_header.related_activity_id - }; - header.is_sorted = is_set(flags, 6); - header.payload_size = if is_set(flags, 7) { - parse_varint_u32(reader)? - } else { - prev_header.payload_size - }; - - header.raw_metadata_id = if header.is_sorted { (1 << 31) } else { 0 } | header.metadata_id; // set is_sorted bit - - //eprintln!("{} [flags 0b{:b}]", header, flags); - - *prev_header = header.clone(); - - Ok(header) - } - - fn parse_header( - reader: &mut R, - prev_header: &mut EventBlobHeader, - is_compressed: bool, - ) -> BinResult { - if is_compressed { - Self::parse_compressed_header(reader, prev_header) - } else { - eprintln!("parsing uncompressed header"); - reader.read_le() - } - } -} - -// Assuming block is a -impl Iterator for EventBlobIter { - type Item = (EventBlobHeader, Vec); - - fn next(&mut self) -> Option { - if self.data.position() >= self.blob_size { - return None; - } - - let header = EventBlobIter::parse_header( - &mut self.data, - &mut self.prev_header, - self.compressed_headers, - ) - .expect("Failed to read EventBlobHeader"); - let payload = self.data.read_exactly(header.payload_size as u64); - - if !self.compressed_headers && header.payload_size & 3 != 0 { - let alignment_skip = 4 - (header.payload_size & 3); - self.data - .seek(SeekFrom::Current(alignment_skip as i64)) - .expect("Seek failed"); - } - - Some((header, payload)) - } -} - -pub struct EventPipeParser { - stream: R, - metadata_map: HashMap, - stack_map: HashMap>, - trace_info: Option, - cur_event_blob_iter: Option, -} - -impl EventPipeParser -where - R: Read + Seek, -{ - pub fn new(mut stream: R) -> Result { - let file_header = NettraceHeader::read(&mut stream)?; - if file_header.ident.bytes != b"!FastSerialization.1" { - return Err(EventPipeError::BadMagic { - found: Box::new(file_header.ident.bytes), - pos: stream.stream_position().unwrap(), - }); - } - - Ok(EventPipeParser { - stream, - metadata_map: HashMap::new(), - stack_map: HashMap::new(), - trace_info: None, - cur_event_blob_iter: None, - }) - } - - fn make_err(&mut self, message: &str) -> EventPipeError { - EventPipeError::AssertFail { - pos: self.stream.stream_position().unwrap(), - message: message.to_string(), - } - } - - // Return the NettraceTraceObject for this stream, parsing until it's available if necessary. - // It will be the first thing in the stream, so we won't miss it - pub fn trace_info(&mut self) -> Result { - if let Some(trace_info) = self.trace_info { - return Ok(trace_info); - } - - // If we don't have it, we're going to assume that it's going to be the first object - let Some(type_object) = self.advance_to_next_object()? else { - return Err(self.make_err("Expected NettraceTraceObject")); - }; - - if type_object.type_name.as_str() != "TraceObject" { - return Err(self.make_err("Expected TraceObject")); - } - - let trace_info = NettraceTraceObject::read(&mut self.stream)?; - self.trace_info = Some(trace_info.clone()); - - self.read_object_end()?; - - Ok(trace_info) - } - - fn read_object_end(&mut self) -> Result<(), EventPipeError> { - let end_tag = NettraceTag::read(&mut self.stream)?; - if end_tag != NettraceTag::EndObject { - return Err(self.make_err("Expected EndObject tag")); - } - - Ok(()) - } - - fn advance_to_next_object(&mut self) -> Result, EventPipeError> { - let start_tag = NettraceTag::read(&mut self.stream)?; - if start_tag == NettraceTag::NullReference { - // stream done - return Ok(None); - } - - if start_tag != NettraceTag::BeginPrivateObject { - return Err(self.make_err("Expected BeginPrivateObject tag")); - } - - // so much effort spent in NettraceTypeObject, when it's just one of 4 things - let obj_type = NettraceTypeObject::read(&mut self.stream)?; - Ok(Some(obj_type)) - } - - fn parse_event( - &mut self, - header: EventBlobHeader, - payload: Vec, - ) -> Result, EventPipeError> { - let metadata_id = header.metadata_id; - let metadata_def = - self.metadata_map - .get(&metadata_id) - .ok_or_else(|| EventPipeError::AssertFail { - pos: 0, - message: format!("Metadata definition {} not found", metadata_id), - })?; - - let mut event = NettraceEvent { - provider_name: metadata_def.provider_name.to_string(), - event_id: metadata_def.event_id, - event_name: if metadata_def.event_name.len() > 0 { - Some(metadata_def.event_name.to_string()) - } else { - None - }, - event_keywords: metadata_def.keywords, - event_version: metadata_def.version, - event_level: metadata_def.level, - event_opcode: metadata_def.opcode, - - sequence_number: header.sequence_number, - thread_id: header.thread_id, - capture_thread_id: header.capture_thread_id, - processor_number: if header.processor_number != u32::MAX { - Some(header.processor_number) - } else { - None - }, - stack: self - .stack_map - .get(&header.stack_id) - .cloned() - .unwrap_or_default(), - timestamp: header.timestamp, - activity_id: header.activity_id, - related_activity_id: header.related_activity_id, - - payload: payload, - }; - - Ok(Some(event)) - } - - pub fn next_event(&mut self) -> Result, EventPipeError> { - // If we're inside an event block already, keep iterating through it - if let Some(cur_event_iter) = self.cur_event_blob_iter.as_mut() { - if let Some((header, payload)) = cur_event_iter.next() { - return self.parse_event(header, payload); - } - - self.cur_event_blob_iter = None; - - // we don't read this when we read the event blob; we could, but we don't - self.read_object_end()?; - } - - loop { - // Keep reading from the data until we get to an EventBlock, in which case we'll - // jump back out into the above iterator. - // - // Anything that's not an EventBlock, we need to parse into internal data structures so we can - // expose proper events from the EventBlock. - - let obj_type = self.advance_to_next_object()?; - let Some(obj_type) = obj_type else { - return Ok(None); - }; - - let obj_type_name = obj_type.type_name.as_str(); - - match obj_type_name { - "Trace" => { - let trace_object = NettraceTraceObject::read(&mut self.stream)?; - log::trace!("Trace: {:?}", trace_object); - self.trace_info = Some(trace_object.clone()); - } - "MetadataBlock" => { - //eprintln!("MetadataBlock"); - let metadata_block = NettraceEventBlock::read(&mut self.stream)?; - let metadata_block_data = self.stream.read_exactly( - metadata_block.size as u64 - metadata_block.header.size as u64, - ); - self.handle_metadata_block(EventBlobIter::new( - metadata_block, - metadata_block_data, - )?)?; - } - "StackBlock" => { - //eprintln!("StackBlock"); - let stack_block = StackBlock::read(&mut self.stream)?; - let mut stack_id = stack_block.first_id; - for stack in stack_block.stacks { - self.stack_map.insert(stack_id, stack.stack); - stack_id += 1; - } - } - "SPBlock" => { - //eprintln!("SPBlock"); - let sp_block = SequencePointBlock::read(&mut self.stream)?; - } - "EventBlock" => { - //eprintln!("EventBlock"); - let event_block = NettraceEventBlock::read(&mut self.stream)?; - let event_block_data = self - .stream - .read_exactly(event_block.size as u64 - event_block.header.size as u64); - self.cur_event_blob_iter = - Some(EventBlobIter::new(event_block, event_block_data)?); - - // jump into the iterator at the start of this - return self.next_event(); - } - unknown => { - eprintln!("Unknown object type: {}", unknown); - return Err(self.make_err("Unknown object type")); - } - } - - self.read_object_end()?; - } - } - - fn handle_metadata_block(&mut self, mut iter: EventBlobIter) -> Result<(), EventPipeError> { - while let Some((header, mut data)) = iter.next() { - let mut payload = Cursor::new(&data); - let payload_size = header.payload_size as u64; - let mut metadata_def = MetadataDefinition::read(&mut payload)?; - - while payload.position() < payload_size { - let tag_data = MetadataTaggedData::read(&mut payload)?; - if tag_data.tag == MetadataTag::OpCode { - metadata_def.opcode = Some(tag_data.opcode); - } else if tag_data.tag == MetadataTag::V2Params { - assert_eq!( - metadata_def.fields.field_count, 0, - "Found v2 fields, but v1 fields were not empty" - ); - metadata_def.fields = tag_data.fields_v2; - } - } - - self.metadata_map.insert(metadata_def.id, metadata_def); - } - - Ok(()) - } -} - pub trait ReaderTrait: Read + Seek + BinReaderExt {} pub enum DecodedEvent { - CoreClrEvent(coreclr_events::CoreClrEvent), + CoreClrEvent(coreclr::CoreClrEvent), UnknownEvent, } pub fn decode_event(event: &NettraceEvent) -> DecodedEvent { match event.provider_name.as_str() { "Microsoft-Windows-DotNETRuntime" | "Microsoft-Windows-DotNETRuntimeRundown" => { - coreclr_events::decode_coreclr_event(event) + coreclr::decode_coreclr_event(event) .map(|x| DecodedEvent::CoreClrEvent(x)) .unwrap_or_else(|| DecodedEvent::UnknownEvent) } From 5540bfa2396fe2d41066fe9dd360a327b642cafb Mon Sep 17 00:00:00 2001 From: Vladimir Vukicevic Date: Mon, 19 Aug 2024 14:27:41 -0700 Subject: [PATCH 18/36] Move etw parsing into eventpipe-rs --- eventpipe-rs/src/coreclr/etw.rs | 321 +++++++++++++++++++++++++++++ eventpipe-rs/src/coreclr/events.rs | 53 +++-- eventpipe-rs/src/coreclr/mod.rs | 8 +- eventpipe-rs/src/eventpipe/mod.rs | 43 ++++ eventpipe-rs/src/lib.rs | 50 +---- 5 files changed, 417 insertions(+), 58 deletions(-) create mode 100644 eventpipe-rs/src/coreclr/etw.rs diff --git a/eventpipe-rs/src/coreclr/etw.rs b/eventpipe-rs/src/coreclr/etw.rs new file mode 100644 index 000000000..de24eb970 --- /dev/null +++ b/eventpipe-rs/src/coreclr/etw.rs @@ -0,0 +1,321 @@ +use std::future::pending; +use std::{collections::HashMap, convert::TryInto}; + +use crate::EventMetadata; +use super::*; +use num_traits::FromPrimitive; + +use etw_reader::{self, schema::TypedEvent}; +use etw_reader::parser::{Parser, TryParse}; + +pub struct CoreClrEtwConverter { + last_event_on_thread: HashMap, +} + +impl CoreClrEtwConverter { + pub fn new() -> Self { + Self { + last_event_on_thread: HashMap::new(), + } + } + + pub fn remaining_clr_events_on_threads( + &mut self, + ) -> std::collections::hash_map::IntoIter { + std::mem::take(&mut self.last_event_on_thread).into_iter() + } + + pub fn etw_event_to_coreclr_event( + &mut self, + s: &TypedEvent, + parser: &mut Parser, + ) -> Option<(EventMetadata, CoreClrEvent)> { + let timestamp_raw = s.timestamp() as u64; + + let mut name_parts = s.name().splitn(3, '/'); + let provider = name_parts.next().unwrap(); + let mut task = name_parts.next().unwrap(); + let mut opcode = name_parts.next().unwrap(); + + match provider { + "Microsoft-Windows-DotNETRuntime" | "Microsoft-Windows-DotNETRuntimeRundown" => {} + _ => { + panic!("Unexpected event {}", s.name()) + } + } + + // When working with merged ETL files, the proper task and opcode names appear here, e.g. "CLRMethod/MethodLoadVerbose" or + // "CLRMethodRundown/MethodDCStartVerbose". When working with the unmerged user ETL, these show up as e.g. "Method /DCStartVerbose". + // Not clear where those names come from the Etw .man file in CoreCLR does have entries for e.g. RuntimePublisher.MethodDCStartVerboseOpcodeMessage + // as "DCStartVerbose", but I'm not sure how/why those are referenced here and not in the merged ETL. xperf -a dumper on the unmerged + // ETL shows the same (correct) names as the merged ETL. + // + // We try to hack around this by converting the unmerged name to the converted one here. + if task.ends_with(' ') || opcode.ends_with(' ') { + task = task.trim(); + opcode = opcode.trim(); + + // Some of these are technically not correct; e.g. the task should be CLRMethodRundown if it's the + // rundown provider, but we handle them the same below. + match task { + "Method" => { + task = "CLRMethod"; + opcode = match opcode { + "LoadVerbose" => "MethodLoadVerbose", + "UnloadVerbose" => "MethodUnloadVerbose", + "DCStartVerbose" => "MethodDCStartVerbose", + "DCEndVerbose" => "MethodDCEndVerbose", + "JittingStarted" => "MethodJittingStarted", + _ => opcode.trim(), + }; + }, + "Loader" => { + task = "CLRLoader"; + opcode = match opcode { + "ModuleDCStart" => "ModuleDCStart", + _ => opcode.trim(), + }; + }, + "Runtime" => { + task = "CLRRuntimeInformation"; + opcode = opcode.trim(); + }, + "GC" => { + task = "GarbageCollection"; + opcode = match opcode { + "PerHeapHisory" => opcode, + "GCDynamicEvent" => opcode, + "Start" => "win:Start", + "Stop" => "win:Stop", + "RestartEEStart" => "GCRestartEEBegin", + "RestartEEStop" => "GCRestartEEEnd", + "SuspendEEStart" => "GCSuspendEEBegin", + "SuspendEEStop" => "GCSuspendEEEnd", + _ => opcode.trim(), + }; + }, + "ClrStack" => { + task = "CLRStack"; + opcode = match opcode { + "Walk" => "CLRStackWalk", + _ => opcode.trim(), + }; + }, + _ => {}, + } + } + + let pid = s.process_id(); + let tid = s.thread_id(); + + // ETW CoreCLR stackwalk events are a separate event that comes after the event to which + // it should be attached. Our cross-platform CoreCLR events have the stack as an optional + // part of every event. So, if we're recording stacks from ETW, instead of returning + // non-stackwalk events directly, we store them as the last event for a given thread so + // that we can attach a stack. + // + // If we have a pending event and we get a stackwalk event, we'll attach the stack and + // return the pending event. + // If we get a non-stackwalk event, we'll store it, and still return this previous + // pending event. + let pending_event = self.last_event_on_thread.remove(&tid); + + // Handle StackWalk events outside of the big match below for cleanliness + if (task, opcode) == ("CLRStack", "CLRStackWalk") { + // If the STACK keyword is enabled, we get a CLRStackWalk following each CLR event that supports stacks. Not every event + // does. The info about which does and doesn't is here: https://github.com/dotnet/runtime/blob/main/src/coreclr/vm/ClrEtwAllMeta.lst + // Current dotnet (8.0.x) seems to have a bug where `MethodJitMemoryAllocatedForCode` events will fire a stackwalk, + // but the event itself doesn't end up in the trace. (https://github.com/dotnet/runtime/issues/102004) + + // if we don't have anything to attach this stack to, just skip it + if pending_event.is_none() { + return None; + } + + let pending_event = pending_event.unwrap(); + + // "Stack" is explicitly declared as length 2 in the manifest, so the first two addresses are in here, rest + // are in user data buffer. + let first_addresses: Vec = parser.parse("Stack"); + let address_iter = first_addresses + .chunks_exact(8) + .chain(parser.buffer.chunks_exact(8)) + .map(|chunk| u64::from_le_bytes(chunk.try_into().unwrap())); + + return Some((pending_event.0.with_stack(address_iter.collect()), pending_event.1)); + } + + let common = EventMetadata { + timestamp: timestamp_raw, + process_id: pid, + thread_id: tid, + stack: None, + }; + + let new_event = match (task, opcode) { + ("CLRMethod" | "CLRMethodRundown", method_event) => match method_event { + "MethodLoadVerbose" | "MethodDCStartVerbose" | "MethodDCEndVerbose" => { + let method_name: String = parser.parse("MethodName"); + let method_namespace: String = parser.parse("MethodNamespace"); + let method_signature: String = parser.parse("MethodSignature"); + let module_id: u64 = parser.parse("ModuleID"); + let method_id: u64 = parser.parse("MethodID"); + let method_flags: u32 = parser.parse("MethodFlags"); + let method_token: u32 = parser.parse("MethodToken"); + let clr_instance_id: Option = parser.try_parse("ClrInstanceID").ok(); + let re_jit_id: Option = parser.try_parse("ReJITID").ok(); + + let method_start_address: u64 = parser.parse("MethodStartAddress"); + let method_size: u32 = parser.parse("MethodSize"); + + let dc_end = method_event == "MethodDCEndVerbose"; + + //log::trace!("{}: @ {:x} {}::{} {}", opcode, method_start_address, method_namespace, method_basename, method_signature); + + Some(CoreClrEvent::MethodLoad(MethodLoadUnloadEvent { + module_id, + method_start_address, + method_size, + method_name, + method_namespace, + method_signature, + method_id, + method_token, + method_flags, + clr_instance_id, + re_jit_id, + })) + } + _ => None, + }, + ("CLRLoader" | "CLRLoaderRundown", loader_event) => match loader_event { + // AppDomain, Assembly, Module Load/Unload + "ModuleDCStart" | _ => None, + }, + ("GarbageCollection", gc_event) => { + match gc_event { + "GCSampledObjectAllocation" => { + // If High/Low flags are set, then we get one of these for every alloc. Otherwise only + // when a threshold is hit. (100kb) The count and size are aggregates in that case. + let type_id: u64 = parser.parse("TypeID"); // TODO: convert to str, with bulk type data + let address: u64 = parser.parse("Address"); + let object_count_for_type_sample: u32 = parser.parse("ObjectCountForTypeSample"); + let total_size_for_type_sample: u64 = parser.parse("TotalSizeForTypeSample"); + let clr_instance_id: u16 = parser.parse("ClrInstanceID"); + + Some(CoreClrEvent::GcSampledObjectAllocation( + GcSampledObjectAllocationEvent { + address, + type_id, + object_count_for_type_sample, + total_size_for_type_sample, + clr_instance_id, + }, + )) + } + "Triggered" => { + let reason: u32 = parser.parse("Reason"); + let reason = GcReason::from_u32(reason).unwrap_or_else(|| { + log::warn!("Unknown CLR GC Triggered reason: {}", reason); + GcReason::Empty + }); + + Some(CoreClrEvent::GcTriggered(GcTriggeredEvent { + reason, + clr_instance_id: 0, + })) + } + "GCSuspendEEBegin" => { + // Reason, Count + //let _count: u32 = parser.parse("Count"); + let reason: u32 = parser.parse("Reason"); + + let _reason = GcSuspendEeReason::from_u32(reason).unwrap_or_else(|| { + log::warn!("Unknown CLR GCSuspendEEBegin reason: {}", reason); + GcSuspendEeReason::Other + }); + + //Some(CoreClrEvent::GcSuspendEeBegin(GcSuspendEeBeginEvent { + // common, + // reason, + //})) + // TODO + None + } + "GCSuspendEEEnd" => { + // TODO + None + } + "GCRestartEEBegin" => { + // TODO + None + } + "GCRestartEEEnd" => { + // TODO + None + } + "win:Start" => { + let count: u32 = parser.parse("Count"); + let depth: Option = parser.try_parse("Depth").ok(); + let reason: u32 = parser.parse("Reason"); + let gc_type = parser.try_parse("Type").ok().and_then(GcType::from_u32); + let clr_instance_id: Option = parser.try_parse("ClrInstanceID").ok(); + let client_sequence_number: Option = parser.try_parse("ClientSequenceNumber").ok(); + + let reason = GcReason::from_u32(reason).unwrap_or_else(|| { + log::warn!("Unknown CLR GCStart reason: {}", reason); + GcReason::Empty + }); + + Some(CoreClrEvent::GcStart(GcStartEvent { + count, + depth, + reason, + gc_type, + clr_instance_id, + client_sequence_number, + })) + } + "win:Stop" => { + let count: u32 = parser.parse("Count"); + let depth: u32 = parser.parse("Depth"); + let reason = parser.try_parse("Reason").ok().and_then(GcReason::from_u32); + Some(CoreClrEvent::GcEnd(GcEndEvent { + count, + depth, + reason, + })) + } + "SetGCHandle" => { + // TODO + None + } + "DestroyGCHandle" => { + // TODO + None + } + "GCFinalizersBegin" | "GCFinalizersEnd" | "FinalizeObject" => { + // TODO: create an interval + None + } + "GCCreateSegment" | "GCFreeSegment" | "GCDynamicEvent" | "GCHeapStats" => { + // don't care + None + } + _ => { + // don't care + None + } + } + } + ("CLRRuntimeInformation", _) => None, + _ => None, + }; + + if new_event.is_some() { + self.last_event_on_thread.insert(tid, (common, new_event.unwrap())); + } + + pending_event + } +} diff --git a/eventpipe-rs/src/coreclr/events.rs b/eventpipe-rs/src/coreclr/events.rs index b9d371a42..18f143827 100644 --- a/eventpipe-rs/src/coreclr/events.rs +++ b/eventpipe-rs/src/coreclr/events.rs @@ -42,11 +42,21 @@ use std::{ io::{Cursor, Read, Seek}, }; -use binrw::{BinRead, BinReaderExt, NullWideString}; +use binrw::{BinRead, BinReaderExt, BinResult, NullWideString}; use num_derive::{FromPrimitive, ToPrimitive}; use crate::eventpipe::{MetadataDefinition, NettraceEvent}; use super::*; +#[binrw::parser(reader, endian)] +fn parse_null_wide_string_to_string() -> BinResult { + let result = NullWideString::read_options(reader, endian, ())?; + if result.0.is_empty() { + Ok(String::new()) + } else { + Ok(result.to_string()) + } +} + #[derive(BinRead, Debug)] #[br(little, import { version: u32, app_domain: bool })] pub struct ModuleLoadUnloadEvent { @@ -56,22 +66,24 @@ pub struct ModuleLoadUnloadEvent { pub app_domain_id: Option, pub module_flags: u32, pub _reserved1: u32, - pub module_il_path: NullWideString, - pub module_native_path: NullWideString, + #[br(parse_with = parse_null_wide_string_to_string)] + pub module_il_path: String, + #[br(parse_with = parse_null_wide_string_to_string)] + pub module_native_path: String, #[br(if(version >= 1))] pub clr_instance_id: Option, #[br(if(version >= 2))] pub managed_pdb_signature: [u8; 16], #[br(if(version >= 2))] pub managed_pdb_age: u32, - #[br(if(version >= 2))] - pub managed_pdb_build_path: NullWideString, + #[br(if(version >= 2), parse_with = parse_null_wide_string_to_string)] + pub managed_pdb_build_path: String, #[br(if(version >= 2))] pub native_pdb_signature: [u8; 16], #[br(if(version >= 2))] pub native_pdb_age: u32, - #[br(if(version >= 2))] - pub native_pdb_build_path: NullWideString, + #[br(if(version >= 2), parse_with = parse_null_wide_string_to_string)] + pub native_pdb_build_path: String, } #[derive(BinRead, Debug)] @@ -84,14 +96,14 @@ pub struct MethodLoadUnloadEvent { pub method_token: u32, pub method_flags: u32, - #[br(if(verbose))] - pub method_namespace: NullWideString, + #[br(if(verbose), parse_with = parse_null_wide_string_to_string)] + pub method_namespace: String, - #[br(if(verbose))] - pub method_name: NullWideString, + #[br(if(verbose), parse_with = parse_null_wide_string_to_string)] + pub method_name: String, - #[br(if(verbose))] - pub method_signature: NullWideString, + #[br(if(verbose), parse_with = parse_null_wide_string_to_string)] + pub method_signature: String, #[br(if(version >= 1, None))] pub clr_instance_id: Option, @@ -141,8 +153,8 @@ pub struct GcAllocationTickEvent { pub allocation_amount64: u64, #[br(if(version >= 2))] pub type_id: u64, // pointer - #[br(if(version >= 2))] - pub type_name: NullWideString, + #[br(if(version >= 2), parse_with = parse_null_wide_string_to_string)] + pub type_name: String, #[br(if(version >= 2))] pub heap_index: u32, #[br(if(version >= 3))] @@ -165,9 +177,12 @@ pub struct GcSampledObjectAllocationEvent { #[br(little, import { version: u32 })] pub struct ReadyToRunGetEntryPointEvent { pub method_id: u64, - pub method_namespace: NullWideString, - pub method_name: NullWideString, - pub method_signature: NullWideString, + #[br(parse_with = parse_null_wide_string_to_string)] + pub method_namespace: String, + #[br(parse_with = parse_null_wide_string_to_string)] + pub method_name: String, + #[br(parse_with = parse_null_wide_string_to_string)] + pub method_signature: String, pub entry_point: u64, pub clr_instance_id: u16, } @@ -178,6 +193,8 @@ pub enum CoreClrEvent { MethodLoad(MethodLoadUnloadEvent), MethodUnload(MethodLoadUnloadEvent), GcTriggered(GcTriggeredEvent), + GcStart(GcStartEvent), + GcEnd(GcEndEvent), GcAllocationTick(GcAllocationTickEvent), GcSampledObjectAllocation(GcSampledObjectAllocationEvent), ReadyToRunGetEntryPoint(ReadyToRunGetEntryPointEvent), diff --git a/eventpipe-rs/src/coreclr/mod.rs b/eventpipe-rs/src/coreclr/mod.rs index ddb222997..3ee833af2 100644 --- a/eventpipe-rs/src/coreclr/mod.rs +++ b/eventpipe-rs/src/coreclr/mod.rs @@ -2,6 +2,12 @@ mod enums; mod events; mod nettrace; +#[cfg(windows)] +mod etw; + pub use enums::*; pub use events::*; -pub use nettrace::*; \ No newline at end of file +pub use nettrace::*; + +#[cfg(windows)] +pub use etw::*; \ No newline at end of file diff --git a/eventpipe-rs/src/eventpipe/mod.rs b/eventpipe-rs/src/eventpipe/mod.rs index f5c7f2c9d..79f9a188d 100644 --- a/eventpipe-rs/src/eventpipe/mod.rs +++ b/eventpipe-rs/src/eventpipe/mod.rs @@ -238,6 +238,49 @@ pub struct MetadataTaggedData { fields_v2: MetadataPayloadDefinition, } +#[derive(BinRead, Debug)] +#[br(little)] +pub struct StackStack { + size: u32, + + // TODO -- support 32-bit here + #[br(count = size / 8)] + stack: Vec, +} + +#[derive(BinRead, Debug)] +#[br(little)] +pub struct StackBlock { + #[br(align_after = 4)] + size: u32, + + first_id: u32, + count: u32, + + #[br(count = count)] + stacks: Vec, +} + +#[derive(BinRead, Debug)] +#[br(little)] +pub struct ThreadSequenceNumber { + thread_id: u64, + sequence_number: u32, +} + +#[derive(BinRead, Debug)] +#[br(little)] +pub struct SequencePointBlock { + #[br(align_after = 4)] + size: u32, + + timestamp: u64, + thread_count: u32, + + #[br(count = thread_count)] + thread_sequence_numbers: Vec, +} + #[derive(Debug)] pub struct NettraceEvent { pub provider_name: String, diff --git a/eventpipe-rs/src/lib.rs b/eventpipe-rs/src/lib.rs index 695120eac..e38abe0a1 100644 --- a/eventpipe-rs/src/lib.rs +++ b/eventpipe-rs/src/lib.rs @@ -16,47 +16,19 @@ use std::mem; use helpers::*; use crate::eventpipe::NettraceEvent; -#[derive(BinRead, Debug)] -#[br(little)] -pub struct StackStack { - size: u32, - - // TODO -- support 32-bit here - #[br(count = size / 8)] - stack: Vec, -} - -#[derive(BinRead, Debug)] -#[br(little)] -pub struct StackBlock { - #[br(align_after = 4)] - size: u32, - - first_id: u32, - count: u32, - - #[br(count = count)] - stacks: Vec, +#[derive(Debug)] +pub struct EventMetadata { + pub timestamp: u64, + pub process_id: u32, + pub thread_id: u32, + pub stack: Option>, } -#[derive(BinRead, Debug)] -#[br(little)] -pub struct ThreadSequenceNumber { - thread_id: u64, - sequence_number: u32, -} - -#[derive(BinRead, Debug)] -#[br(little)] -pub struct SequencePointBlock { - #[br(align_after = 4)] - size: u32, - - timestamp: u64, - thread_count: u32, - - #[br(count = thread_count)] - thread_sequence_numbers: Vec, +impl EventMetadata { + fn with_stack(mut self, stack: Vec) -> Self { + self.stack = Some(stack); + self + } } pub trait ReaderTrait: Read + Seek + BinReaderExt {} From 4eb64291872085db4bad1655010cd7021a28f7dd Mon Sep 17 00:00:00 2001 From: Vladimir Vukicevic Date: Mon, 19 Aug 2024 14:35:46 -0700 Subject: [PATCH 19/36] Rename eventpipe-rs to coreclr-tracing --- Cargo.toml | 5 +++-- {eventpipe-rs => coreclr-tracing}/Cargo.toml | 2 +- .../examples/dump-nettrace.rs | 14 ++++++++++---- .../src/coreclr/enums.rs | 0 .../src/coreclr/etw.rs | 0 .../src/coreclr/events.rs | 0 .../src/coreclr/mod.rs | 0 .../src/coreclr/nettrace.rs | 0 .../src/eventpipe/mod.rs | 0 .../src/eventpipe/parser.rs | 0 {eventpipe-rs => coreclr-tracing}/src/helpers.rs | 0 {eventpipe-rs => coreclr-tracing}/src/lib.rs | 0 samply/Cargo.toml | 2 +- 13 files changed, 15 insertions(+), 8 deletions(-) rename {eventpipe-rs => coreclr-tracing}/Cargo.toml (92%) rename {eventpipe-rs => coreclr-tracing}/examples/dump-nettrace.rs (91%) rename {eventpipe-rs => coreclr-tracing}/src/coreclr/enums.rs (100%) rename {eventpipe-rs => coreclr-tracing}/src/coreclr/etw.rs (100%) rename {eventpipe-rs => coreclr-tracing}/src/coreclr/events.rs (100%) rename {eventpipe-rs => coreclr-tracing}/src/coreclr/mod.rs (100%) rename {eventpipe-rs => coreclr-tracing}/src/coreclr/nettrace.rs (100%) rename {eventpipe-rs => coreclr-tracing}/src/eventpipe/mod.rs (100%) rename {eventpipe-rs => coreclr-tracing}/src/eventpipe/parser.rs (100%) rename {eventpipe-rs => coreclr-tracing}/src/helpers.rs (100%) rename {eventpipe-rs => coreclr-tracing}/src/lib.rs (100%) diff --git a/Cargo.toml b/Cargo.toml index 88ae4d042..0ac0c0b32 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,8 +11,9 @@ members = [ "wholesym-addr2line", "tools/benchmarks", "tools/dump_table", - "tools/query_api" -, "eventpipe-rs"] + "tools/query_api", + "coreclr-tracing" +] exclude = ["etw-reader"] # Should not be compiled on non-Windows # Config for 'cargo dist' diff --git a/eventpipe-rs/Cargo.toml b/coreclr-tracing/Cargo.toml similarity index 92% rename from eventpipe-rs/Cargo.toml rename to coreclr-tracing/Cargo.toml index 9f58b5dfd..5f739dcfe 100644 --- a/eventpipe-rs/Cargo.toml +++ b/coreclr-tracing/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "eventpipe" +name = "coreclr-tracing" version = "0.1.0" edition = "2021" diff --git a/eventpipe-rs/examples/dump-nettrace.rs b/coreclr-tracing/examples/dump-nettrace.rs similarity index 91% rename from eventpipe-rs/examples/dump-nettrace.rs rename to coreclr-tracing/examples/dump-nettrace.rs index 0f185451f..89d55a979 100644 --- a/eventpipe-rs/examples/dump-nettrace.rs +++ b/coreclr-tracing/examples/dump-nettrace.rs @@ -1,9 +1,9 @@ #![allow(unused)] use std::fs::File; -use ::eventpipe::*; -use ::eventpipe::eventpipe::EventPipeParser; -use ::eventpipe::coreclr::CoreClrEvent; +use coreclr_tracing::*; +use coreclr_tracing::coreclr::*; +use coreclr_tracing::eventpipe::*; // https://github.com/microsoft/perfview/blob/main/src/TraceEvent/EventPipe/EventPipeFormat.md @@ -20,7 +20,7 @@ fn main() { // continue; //} - match ::eventpipe::decode_event(&event) { + match coreclr_tracing::decode_event(&event) { DecodedEvent::CoreClrEvent(coreclr_event) => { match coreclr_event { CoreClrEvent::MethodLoad(event) => { @@ -55,6 +55,12 @@ fn main() { CoreClrEvent::MethodDCEnd(event) => { println!("MethodDCEnd: {:?}", event); } + CoreClrEvent::GcStart(event) => { + println!("GcStart: {:?}", event); + } + CoreClrEvent::GcEnd(event) => { + println!("GcEnd: {:?}", event); + } } } DecodedEvent::UnknownEvent => { diff --git a/eventpipe-rs/src/coreclr/enums.rs b/coreclr-tracing/src/coreclr/enums.rs similarity index 100% rename from eventpipe-rs/src/coreclr/enums.rs rename to coreclr-tracing/src/coreclr/enums.rs diff --git a/eventpipe-rs/src/coreclr/etw.rs b/coreclr-tracing/src/coreclr/etw.rs similarity index 100% rename from eventpipe-rs/src/coreclr/etw.rs rename to coreclr-tracing/src/coreclr/etw.rs diff --git a/eventpipe-rs/src/coreclr/events.rs b/coreclr-tracing/src/coreclr/events.rs similarity index 100% rename from eventpipe-rs/src/coreclr/events.rs rename to coreclr-tracing/src/coreclr/events.rs diff --git a/eventpipe-rs/src/coreclr/mod.rs b/coreclr-tracing/src/coreclr/mod.rs similarity index 100% rename from eventpipe-rs/src/coreclr/mod.rs rename to coreclr-tracing/src/coreclr/mod.rs diff --git a/eventpipe-rs/src/coreclr/nettrace.rs b/coreclr-tracing/src/coreclr/nettrace.rs similarity index 100% rename from eventpipe-rs/src/coreclr/nettrace.rs rename to coreclr-tracing/src/coreclr/nettrace.rs diff --git a/eventpipe-rs/src/eventpipe/mod.rs b/coreclr-tracing/src/eventpipe/mod.rs similarity index 100% rename from eventpipe-rs/src/eventpipe/mod.rs rename to coreclr-tracing/src/eventpipe/mod.rs diff --git a/eventpipe-rs/src/eventpipe/parser.rs b/coreclr-tracing/src/eventpipe/parser.rs similarity index 100% rename from eventpipe-rs/src/eventpipe/parser.rs rename to coreclr-tracing/src/eventpipe/parser.rs diff --git a/eventpipe-rs/src/helpers.rs b/coreclr-tracing/src/helpers.rs similarity index 100% rename from eventpipe-rs/src/helpers.rs rename to coreclr-tracing/src/helpers.rs diff --git a/eventpipe-rs/src/lib.rs b/coreclr-tracing/src/lib.rs similarity index 100% rename from eventpipe-rs/src/lib.rs rename to coreclr-tracing/src/lib.rs diff --git a/samply/Cargo.toml b/samply/Cargo.toml index d3d39f3de..727cea57d 100644 --- a/samply/Cargo.toml +++ b/samply/Cargo.toml @@ -52,7 +52,7 @@ cfg-if = "1.0.0" fs4 = "0.8.3" humantime = "2.1.0" shlex = "1.3.0" -eventpipe = { version = "0.1", path = "../eventpipe-rs" } +coreclr-tracing = { version = "0.1", path = "../coreclr-tracing" } [target.'cfg(any(target_os = "android", target_os = "macos", target_os = "linux"))'.dependencies] From c26809c7a08f7bad76cf925a7f650f97b451d347 Mon Sep 17 00:00:00 2001 From: Vladimir Vukicevic Date: Tue, 20 Aug 2024 11:02:13 -0700 Subject: [PATCH 20/36] More coreclr-tracing refactoring --- coreclr-tracing/examples/dump-nettrace.rs | 5 +-- .../src/coreclr/{nettrace.rs => eventpipe.rs} | 5 ++- coreclr-tracing/src/coreclr/events.rs | 2 +- coreclr-tracing/src/coreclr/mod.rs | 9 +--- coreclr-tracing/src/lib.rs | 41 ++++--------------- coreclr-tracing/src/{ => nettrace}/helpers.rs | 0 .../src/{eventpipe => nettrace}/mod.rs | 27 +++++++++--- .../src/{eventpipe => nettrace}/parser.rs | 1 + 8 files changed, 39 insertions(+), 51 deletions(-) rename coreclr-tracing/src/coreclr/{nettrace.rs => eventpipe.rs} (98%) rename coreclr-tracing/src/{ => nettrace}/helpers.rs (100%) rename coreclr-tracing/src/{eventpipe => nettrace}/mod.rs (91%) rename coreclr-tracing/src/{eventpipe => nettrace}/parser.rs (99%) diff --git a/coreclr-tracing/examples/dump-nettrace.rs b/coreclr-tracing/examples/dump-nettrace.rs index 89d55a979..369769a80 100644 --- a/coreclr-tracing/examples/dump-nettrace.rs +++ b/coreclr-tracing/examples/dump-nettrace.rs @@ -2,8 +2,7 @@ use std::fs::File; use coreclr_tracing::*; -use coreclr_tracing::coreclr::*; -use coreclr_tracing::eventpipe::*; +use coreclr_tracing::nettrace::*; // https://github.com/microsoft/perfview/blob/main/src/TraceEvent/EventPipe/EventPipeFormat.md @@ -20,7 +19,7 @@ fn main() { // continue; //} - match coreclr_tracing::decode_event(&event) { + match coreclr_tracing::nettrace::decode_event(&event) { DecodedEvent::CoreClrEvent(coreclr_event) => { match coreclr_event { CoreClrEvent::MethodLoad(event) => { diff --git a/coreclr-tracing/src/coreclr/nettrace.rs b/coreclr-tracing/src/coreclr/eventpipe.rs similarity index 98% rename from coreclr-tracing/src/coreclr/nettrace.rs rename to coreclr-tracing/src/coreclr/eventpipe.rs index 35b7709f0..6e0f8f525 100644 --- a/coreclr-tracing/src/coreclr/nettrace.rs +++ b/coreclr-tracing/src/coreclr/eventpipe.rs @@ -1,5 +1,8 @@ +use std::io::Cursor; +use binrw::*; + use super::*; -use crate::*; +use crate::nettrace::*; pub fn decode_coreclr_event(event: &NettraceEvent) -> Option { match event.provider_name.as_str() { diff --git a/coreclr-tracing/src/coreclr/events.rs b/coreclr-tracing/src/coreclr/events.rs index 18f143827..797885502 100644 --- a/coreclr-tracing/src/coreclr/events.rs +++ b/coreclr-tracing/src/coreclr/events.rs @@ -44,7 +44,7 @@ use std::{ use binrw::{BinRead, BinReaderExt, BinResult, NullWideString}; use num_derive::{FromPrimitive, ToPrimitive}; -use crate::eventpipe::{MetadataDefinition, NettraceEvent}; +use crate::nettrace::{MetadataDefinition, NettraceEvent}; use super::*; #[binrw::parser(reader, endian)] diff --git a/coreclr-tracing/src/coreclr/mod.rs b/coreclr-tracing/src/coreclr/mod.rs index 3ee833af2..b6ef9b8c6 100644 --- a/coreclr-tracing/src/coreclr/mod.rs +++ b/coreclr-tracing/src/coreclr/mod.rs @@ -1,13 +1,8 @@ mod enums; mod events; -mod nettrace; - +pub mod eventpipe; #[cfg(windows)] -mod etw; +pub mod etw; pub use enums::*; pub use events::*; -pub use nettrace::*; - -#[cfg(windows)] -pub use etw::*; \ No newline at end of file diff --git a/coreclr-tracing/src/lib.rs b/coreclr-tracing/src/lib.rs index e38abe0a1..f5774ea0d 100644 --- a/coreclr-tracing/src/lib.rs +++ b/coreclr-tracing/src/lib.rs @@ -1,20 +1,11 @@ -#![allow(unused)] +// The coreclr module contains the various coreclr event types, as well +// as the tools to convert ETW and eventpipe events into the generic event +// corelcr types. +mod coreclr; +pub use coreclr::*; -mod helpers; -pub mod eventpipe; - -pub mod coreclr; - -use binrw::io::TakeSeekExt; -use binrw::{binrw, BinRead, BinReaderExt, BinResult, NullWideString}; -use std::collections::HashMap; -use std::fmt::Display; -use std::fs::File; -use std::io::{BufRead, Cursor, Read, Seek, SeekFrom}; -use std::mem; - -use helpers::*; -use crate::eventpipe::NettraceEvent; +// The nettrace module handles parsing of nettrace files. +pub mod nettrace; #[derive(Debug)] pub struct EventMetadata { @@ -30,21 +21,3 @@ impl EventMetadata { self } } - -pub trait ReaderTrait: Read + Seek + BinReaderExt {} - -pub enum DecodedEvent { - CoreClrEvent(coreclr::CoreClrEvent), - UnknownEvent, -} - -pub fn decode_event(event: &NettraceEvent) -> DecodedEvent { - match event.provider_name.as_str() { - "Microsoft-Windows-DotNETRuntime" | "Microsoft-Windows-DotNETRuntimeRundown" => { - coreclr::decode_coreclr_event(event) - .map(|x| DecodedEvent::CoreClrEvent(x)) - .unwrap_or_else(|| DecodedEvent::UnknownEvent) - } - _ => DecodedEvent::UnknownEvent, - } -} diff --git a/coreclr-tracing/src/helpers.rs b/coreclr-tracing/src/nettrace/helpers.rs similarity index 100% rename from coreclr-tracing/src/helpers.rs rename to coreclr-tracing/src/nettrace/helpers.rs diff --git a/coreclr-tracing/src/eventpipe/mod.rs b/coreclr-tracing/src/nettrace/mod.rs similarity index 91% rename from coreclr-tracing/src/eventpipe/mod.rs rename to coreclr-tracing/src/nettrace/mod.rs index 79f9a188d..5bf46dae3 100644 --- a/coreclr-tracing/src/eventpipe/mod.rs +++ b/coreclr-tracing/src/nettrace/mod.rs @@ -1,12 +1,11 @@ -use binrw::io::TakeSeekExt; +#![allow(unused)] use binrw::{binrw, BinRead, BinReaderExt, BinResult, NullWideString}; -use std::collections::HashMap; use std::fmt::Display; -use std::fs::File; use std::io::{BufRead, Cursor, Read, Seek, SeekFrom}; -use std::mem; -use crate::helpers::*; +use crate::coreclr; + +mod helpers; pub mod parser; pub use parser::*; @@ -302,3 +301,21 @@ pub struct NettraceEvent { pub payload: Vec, } + +pub trait ReaderTrait: Read + Seek + BinReaderExt {} + +pub enum DecodedEvent { + CoreClrEvent(coreclr::CoreClrEvent), + UnknownEvent, +} + +pub fn decode_event(event: &NettraceEvent) -> DecodedEvent { + match event.provider_name.as_str() { + "Microsoft-Windows-DotNETRuntime" | "Microsoft-Windows-DotNETRuntimeRundown" => { + coreclr::eventpipe::decode_coreclr_event(event) + .map(|x| DecodedEvent::CoreClrEvent(x)) + .unwrap_or_else(|| DecodedEvent::UnknownEvent) + } + _ => DecodedEvent::UnknownEvent, + } +} \ No newline at end of file diff --git a/coreclr-tracing/src/eventpipe/parser.rs b/coreclr-tracing/src/nettrace/parser.rs similarity index 99% rename from coreclr-tracing/src/eventpipe/parser.rs rename to coreclr-tracing/src/nettrace/parser.rs index fd9617885..0f305c6d9 100644 --- a/coreclr-tracing/src/eventpipe/parser.rs +++ b/coreclr-tracing/src/nettrace/parser.rs @@ -7,6 +7,7 @@ use std::io::{BufRead, Cursor, Read, Seek, SeekFrom}; use std::mem; use super::*; +use super::helpers::*; use crate::*; trait ReadExactlyExt { From 04f4a68cc37a97e6696b8f040b8cb47cc716dd1b Mon Sep 17 00:00:00 2001 From: Vladimir Vukicevic Date: Tue, 20 Aug 2024 13:34:44 -0700 Subject: [PATCH 21/36] CoreCLR refactor mostly complete --- Cargo.lock | 25 +- coreclr-tracing/examples/dump-nettrace.rs | 2 +- coreclr-tracing/src/coreclr/enums.rs | 4 +- coreclr-tracing/src/coreclr/etw.rs | 11 +- coreclr-tracing/src/coreclr/eventpipe.rs | 46 ++- coreclr-tracing/src/coreclr/events.rs | 31 +- coreclr-tracing/src/lib.rs | 10 +- coreclr-tracing/src/nettrace/mod.rs | 6 +- coreclr-tracing/src/nettrace/parser.rs | 2 +- samply/src/mac/task_profiler.rs | 8 +- samply/src/shared/coreclr/eventpipe.rs | 135 ------- ..._manager.rs => eventpipe_trace_manager.rs} | 81 ++--- samply/src/shared/coreclr/events.rs | 156 --------- samply/src/shared/coreclr/mod.rs | 7 +- samply/src/windows/coreclr.rs | 43 +-- samply/src/windows/etw_coreclr.rs | 328 ------------------ samply/src/windows/etw_gecko.rs | 11 +- samply/src/windows/mod.rs | 1 - 18 files changed, 153 insertions(+), 754 deletions(-) delete mode 100644 samply/src/shared/coreclr/eventpipe.rs rename samply/src/shared/coreclr/{dotnet_trace_manager.rs => eventpipe_trace_manager.rs} (81%) delete mode 100644 samply/src/shared/coreclr/events.rs delete mode 100644 samply/src/windows/etw_coreclr.rs diff --git a/Cargo.lock b/Cargo.lock index 0d37dc40d..3af8da744 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -433,6 +433,18 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +[[package]] +name = "coreclr-tracing" +version = "0.1.0" +dependencies = [ + "binrw", + "bitflags 2.5.0", + "etw-reader", + "log", + "num-derive", + "num-traits", +] + [[package]] name = "cpp_demangle" version = "0.4.3" @@ -641,17 +653,6 @@ dependencies = [ "windows 0.56.0", ] -[[package]] -name = "eventpipe" -version = "0.1.0" -dependencies = [ - "binrw", - "bitflags 2.5.0", - "log", - "num-derive", - "num-traits", -] - [[package]] name = "fallible-iterator" version = "0.3.0" @@ -2459,12 +2460,12 @@ dependencies = [ "byteorder", "cfg-if", "clap", + "coreclr-tracing", "crossbeam-channel", "ctrlc", "debugid", "env_logger", "etw-reader", - "eventpipe", "flate2", "framehop", "fs4", diff --git a/coreclr-tracing/examples/dump-nettrace.rs b/coreclr-tracing/examples/dump-nettrace.rs index 369769a80..795b721d9 100644 --- a/coreclr-tracing/examples/dump-nettrace.rs +++ b/coreclr-tracing/examples/dump-nettrace.rs @@ -20,7 +20,7 @@ fn main() { //} match coreclr_tracing::nettrace::decode_event(&event) { - DecodedEvent::CoreClrEvent(coreclr_event) => { + DecodedEvent::CoreClrEvent((meta, coreclr_event)) => { match coreclr_event { CoreClrEvent::MethodLoad(event) => { println!( diff --git a/coreclr-tracing/src/coreclr/enums.rs b/coreclr-tracing/src/coreclr/enums.rs index cb51d7469..7099fbc6f 100644 --- a/coreclr-tracing/src/coreclr/enums.rs +++ b/coreclr-tracing/src/coreclr/enums.rs @@ -2,8 +2,8 @@ use bitflags::bitflags; use std::fmt::Display; -use binrw::{BinRead, BinReaderExt, NullWideString}; -use num_derive::{FromPrimitive, ToPrimitive}; +use binrw::BinRead; +use num_derive::FromPrimitive; #[derive(BinRead, Debug, FromPrimitive, Clone, Copy)] #[br(repr = u32)] diff --git a/coreclr-tracing/src/coreclr/etw.rs b/coreclr-tracing/src/coreclr/etw.rs index de24eb970..696ff765f 100644 --- a/coreclr-tracing/src/coreclr/etw.rs +++ b/coreclr-tracing/src/coreclr/etw.rs @@ -1,4 +1,3 @@ -use std::future::pending; use std::{collections::HashMap, convert::TryInto}; use crate::EventMetadata; @@ -37,12 +36,13 @@ impl CoreClrEtwConverter { let mut task = name_parts.next().unwrap(); let mut opcode = name_parts.next().unwrap(); - match provider { - "Microsoft-Windows-DotNETRuntime" | "Microsoft-Windows-DotNETRuntimeRundown" => {} + let is_rundown = match provider { + "Microsoft-Windows-DotNETRuntime" => false, + "Microsoft-Windows-DotNETRuntimeRundown" => true, _ => { panic!("Unexpected event {}", s.name()) } - } + }; // When working with merged ETL files, the proper task and opcode names appear here, e.g. "CLRMethod/MethodLoadVerbose" or // "CLRMethodRundown/MethodDCStartVerbose". When working with the unmerged user ETL, these show up as e.g. "Method /DCStartVerbose". @@ -150,6 +150,7 @@ impl CoreClrEtwConverter { process_id: pid, thread_id: tid, stack: None, + is_rundown, }; let new_event = match (task, opcode) { @@ -168,8 +169,6 @@ impl CoreClrEtwConverter { let method_start_address: u64 = parser.parse("MethodStartAddress"); let method_size: u32 = parser.parse("MethodSize"); - let dc_end = method_event == "MethodDCEndVerbose"; - //log::trace!("{}: @ {:x} {}::{} {}", opcode, method_start_address, method_namespace, method_basename, method_signature); Some(CoreClrEvent::MethodLoad(MethodLoadUnloadEvent { diff --git a/coreclr-tracing/src/coreclr/eventpipe.rs b/coreclr-tracing/src/coreclr/eventpipe.rs index 6e0f8f525..ad269396a 100644 --- a/coreclr-tracing/src/coreclr/eventpipe.rs +++ b/coreclr-tracing/src/coreclr/eventpipe.rs @@ -2,9 +2,9 @@ use std::io::Cursor; use binrw::*; use super::*; -use crate::nettrace::*; +use crate::{nettrace::*, EventMetadata}; -pub fn decode_coreclr_event(event: &NettraceEvent) -> Option { +pub fn decode_coreclr_event(event: &NettraceEvent) -> Option<(EventMetadata, CoreClrEvent)> { match event.provider_name.as_str() { "Microsoft-Windows-DotNETRuntime" => decode_coreclr_regular_event(event), "Microsoft-Windows-DotNETRuntimeRundown" => decode_coreclr_rundown_event(event), @@ -12,10 +12,22 @@ pub fn decode_coreclr_event(event: &NettraceEvent) -> Option { } } -fn decode_coreclr_regular_event(event: &NettraceEvent) -> Option { +fn to_event_metadata(event: &NettraceEvent, is_rundown: bool) -> EventMetadata { + EventMetadata { + timestamp: event.timestamp, + process_id: u32::MAX, // note: nettrace events don't include the process id, because they come from a single process + thread_id: event.thread_id as u32, + stack: None, + is_rundown, + } +} + +fn decode_coreclr_regular_event(event: &NettraceEvent) -> Option<(EventMetadata, CoreClrEvent)> { let mut payload = Cursor::new(&event.payload); //eprintln!("Regular: {:?}", event.event_id); + let meta = to_event_metadata(event, false); + match event.event_id { // 151: DomainModuleLoad // 152: ModuleLoad @@ -27,7 +39,7 @@ fn decode_coreclr_regular_event(event: &NettraceEvent) -> Option { binrw::args! { version: event.event_version, app_domain: true }, ) .unwrap(); - Some(CoreClrEvent::ModuleLoad(ev)) + Some((meta, CoreClrEvent::ModuleLoad(ev))) } 152 => { // ModuleLoad @@ -36,7 +48,7 @@ fn decode_coreclr_regular_event(event: &NettraceEvent) -> Option { binrw::args! { version: event.event_version, app_domain: false }, ) .unwrap(); - Some(CoreClrEvent::ModuleLoad(ev)) + Some((meta, CoreClrEvent::ModuleLoad(ev))) } 153 => { // ModuleUnload @@ -45,7 +57,7 @@ fn decode_coreclr_regular_event(event: &NettraceEvent) -> Option { binrw::args! { version: event.event_version, app_domain: false }, ) .unwrap(); - Some(CoreClrEvent::ModuleUnload(ev)) + Some((meta, CoreClrEvent::ModuleUnload(ev))) } 159 => { // R2RGetEntryPoint @@ -54,7 +66,7 @@ fn decode_coreclr_regular_event(event: &NettraceEvent) -> Option { binrw::args! { version: event.event_version }, ) .unwrap(); - Some(CoreClrEvent::ReadyToRunGetEntryPoint(ev)) + Some((meta, CoreClrEvent::ReadyToRunGetEntryPoint(ev))) } 141 => { // MethodLoad @@ -63,7 +75,7 @@ fn decode_coreclr_regular_event(event: &NettraceEvent) -> Option { binrw::args! { version: event.event_version, verbose: false }, ) .unwrap(); - Some(CoreClrEvent::MethodLoad(ev)) + Some((meta, CoreClrEvent::MethodLoad(ev))) } 142 => { // MethodUnload @@ -72,7 +84,7 @@ fn decode_coreclr_regular_event(event: &NettraceEvent) -> Option { binrw::args! { version: event.event_version, verbose: false }, ) .unwrap(); - Some(CoreClrEvent::MethodUnload(ev)) + Some((meta, CoreClrEvent::MethodUnload(ev))) } 143 => { // MethodLoadVerbose @@ -81,7 +93,7 @@ fn decode_coreclr_regular_event(event: &NettraceEvent) -> Option { binrw::args! { version: event.event_version, verbose: true }, ) .unwrap(); - Some(CoreClrEvent::MethodLoad(ev)) + Some((meta, CoreClrEvent::MethodLoad(ev))) } 144 => { // MethodUnloadVerbose @@ -90,7 +102,7 @@ fn decode_coreclr_regular_event(event: &NettraceEvent) -> Option { binrw::args! { version: event.event_version, verbose: true }, ) .unwrap(); - Some(CoreClrEvent::MethodUnload(ev)) + Some((meta, CoreClrEvent::MethodUnload(ev))) } 35 => { // GCTriggered @@ -99,7 +111,7 @@ fn decode_coreclr_regular_event(event: &NettraceEvent) -> Option { binrw::args! { version: event.event_version }, ) .unwrap(); - Some(CoreClrEvent::GcTriggered(ev)) + Some((meta, CoreClrEvent::GcTriggered(ev))) } 1 => { // GCStart @@ -131,7 +143,7 @@ fn decode_coreclr_regular_event(event: &NettraceEvent) -> Option { binrw::args! { version: event.event_version }, ) .unwrap(); - Some(CoreClrEvent::GcAllocationTick(ev)) + Some((meta, CoreClrEvent::GcAllocationTick(ev))) } 20 | 30 => { // High | Low, do we really need both of them? @@ -140,7 +152,7 @@ fn decode_coreclr_regular_event(event: &NettraceEvent) -> Option { binrw::args! { version: event.event_version }, ) .unwrap(); - Some(CoreClrEvent::GcSampledObjectAllocation(ev)) + Some((meta, CoreClrEvent::GcSampledObjectAllocation(ev))) } // 13: GCFinalizersEnd // 14: GCFinalizersBegin @@ -163,9 +175,11 @@ fn decode_coreclr_regular_event(event: &NettraceEvent) -> Option { } } -fn decode_coreclr_rundown_event(event: &NettraceEvent) -> Option { +fn decode_coreclr_rundown_event(event: &NettraceEvent) -> Option<(EventMetadata, CoreClrEvent)> { let mut payload = Cursor::new(&event.payload); + let meta = to_event_metadata(event, true); + //eprintln!("RUNDOWN: {:?}", event.event_id); match event.event_id { 144 => { @@ -175,7 +189,7 @@ fn decode_coreclr_rundown_event(event: &NettraceEvent) -> Option { binrw::args! { version: event.event_version, verbose: true }, ) .unwrap(); - Some(CoreClrEvent::MethodDCEnd(ev)) + Some((meta, CoreClrEvent::MethodDCEnd(ev))) } _ => None, } diff --git a/coreclr-tracing/src/coreclr/events.rs b/coreclr-tracing/src/coreclr/events.rs index 797885502..6f5d8bda7 100644 --- a/coreclr-tracing/src/coreclr/events.rs +++ b/coreclr-tracing/src/coreclr/events.rs @@ -35,16 +35,12 @@ // CLRRuntimeInformation // CLRLoader -use bitflags::bitflags; +// This is only here because there's a macro definition of "version" below that is unused, but is +// present so that the parsing code can pass consistent variables to everything. I don't know how to +// mark it as unused in just that definition. +#![allow(unused_variables)] -use std::{ - fmt::Display, - io::{Cursor, Read, Seek}, -}; - -use binrw::{BinRead, BinReaderExt, BinResult, NullWideString}; -use num_derive::{FromPrimitive, ToPrimitive}; -use crate::nettrace::{MetadataDefinition, NettraceEvent}; +use binrw::{BinRead, BinResult, NullWideString}; use super::*; #[binrw::parser(reader, endian)] @@ -57,7 +53,7 @@ fn parse_null_wide_string_to_string() -> BinResult { } } -#[derive(BinRead, Debug)] +#[derive(BinRead, Debug, Clone)] #[br(little, import { version: u32, app_domain: bool })] pub struct ModuleLoadUnloadEvent { pub module_id: u64, @@ -86,7 +82,7 @@ pub struct ModuleLoadUnloadEvent { pub native_pdb_build_path: String, } -#[derive(BinRead, Debug)] +#[derive(BinRead, Debug, Clone)] #[br(little, import { version: u32, verbose: bool })] pub struct MethodLoadUnloadEvent { pub method_id: u64, @@ -112,14 +108,14 @@ pub struct MethodLoadUnloadEvent { pub re_jit_id: Option, } -#[derive(BinRead, Debug)] +#[derive(BinRead, Debug, Clone)] #[br(little, import { version: u32 })] pub struct GcTriggeredEvent { pub reason: GcReason, pub clr_instance_id: u16, } -#[derive(BinRead, Debug)] +#[derive(BinRead, Debug, Clone)] #[br(little, import { version: u32 })] pub struct GcStartEvent { pub count: u32, @@ -134,7 +130,7 @@ pub struct GcStartEvent { pub client_sequence_number: Option, } -#[derive(BinRead, Debug)] +#[derive(BinRead, Debug, Clone)] #[br(little, import { version: u32 })] pub struct GcEndEvent { pub count: u32, @@ -143,7 +139,7 @@ pub struct GcEndEvent { pub reason: Option, } -#[derive(BinRead, Debug)] +#[derive(BinRead, Debug, Clone)] #[br(little, import { version: u32 })] pub struct GcAllocationTickEvent { pub allocation_amount: u32, @@ -163,7 +159,7 @@ pub struct GcAllocationTickEvent { pub object_size: Option, } -#[derive(BinRead, Debug)] +#[derive(BinRead, Debug, Clone)] #[br(little, import { version: u32 })] pub struct GcSampledObjectAllocationEvent { pub address: u64, // pointer @@ -173,7 +169,7 @@ pub struct GcSampledObjectAllocationEvent { pub clr_instance_id: u16, } -#[derive(BinRead, Debug)] +#[derive(BinRead, Debug, Clone)] #[br(little, import { version: u32 })] pub struct ReadyToRunGetEntryPointEvent { pub method_id: u64, @@ -187,6 +183,7 @@ pub struct ReadyToRunGetEntryPointEvent { pub clr_instance_id: u16, } +#[derive(Debug, Clone)] pub enum CoreClrEvent { ModuleLoad(ModuleLoadUnloadEvent), ModuleUnload(ModuleLoadUnloadEvent), diff --git a/coreclr-tracing/src/lib.rs b/coreclr-tracing/src/lib.rs index f5774ea0d..685399bb9 100644 --- a/coreclr-tracing/src/lib.rs +++ b/coreclr-tracing/src/lib.rs @@ -7,17 +7,25 @@ pub use coreclr::*; // The nettrace module handles parsing of nettrace files. pub mod nettrace; -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct EventMetadata { pub timestamp: u64, pub process_id: u32, pub thread_id: u32, pub stack: Option>, + pub is_rundown: bool, } impl EventMetadata { + #[allow(unused)] fn with_stack(mut self, stack: Vec) -> Self { self.stack = Some(stack); self } + + #[allow(unused)] + pub fn with_pid(mut self, pid: u32) -> Self { + self.process_id = pid; + self + } } diff --git a/coreclr-tracing/src/nettrace/mod.rs b/coreclr-tracing/src/nettrace/mod.rs index 5bf46dae3..ee6efa125 100644 --- a/coreclr-tracing/src/nettrace/mod.rs +++ b/coreclr-tracing/src/nettrace/mod.rs @@ -3,7 +3,7 @@ use binrw::{binrw, BinRead, BinReaderExt, BinResult, NullWideString}; use std::fmt::Display; use std::io::{BufRead, Cursor, Read, Seek, SeekFrom}; -use crate::coreclr; +use crate::{coreclr, EventMetadata}; mod helpers; @@ -305,7 +305,7 @@ pub struct NettraceEvent { pub trait ReaderTrait: Read + Seek + BinReaderExt {} pub enum DecodedEvent { - CoreClrEvent(coreclr::CoreClrEvent), + CoreClrEvent((EventMetadata, coreclr::CoreClrEvent)), UnknownEvent, } @@ -313,7 +313,7 @@ pub fn decode_event(event: &NettraceEvent) -> DecodedEvent { match event.provider_name.as_str() { "Microsoft-Windows-DotNETRuntime" | "Microsoft-Windows-DotNETRuntimeRundown" => { coreclr::eventpipe::decode_coreclr_event(event) - .map(|x| DecodedEvent::CoreClrEvent(x)) + .map(DecodedEvent::CoreClrEvent) .unwrap_or_else(|| DecodedEvent::UnknownEvent) } _ => DecodedEvent::UnknownEvent, diff --git a/coreclr-tracing/src/nettrace/parser.rs b/coreclr-tracing/src/nettrace/parser.rs index 0f305c6d9..5f6afdccf 100644 --- a/coreclr-tracing/src/nettrace/parser.rs +++ b/coreclr-tracing/src/nettrace/parser.rs @@ -216,7 +216,7 @@ where } let trace_info = NettraceTraceObject::read(&mut self.stream)?; - self.trace_info = Some(trace_info.clone()); + self.trace_info = Some(trace_info); self.read_object_end()?; diff --git a/samply/src/mac/task_profiler.rs b/samply/src/mac/task_profiler.rs index 2d245c11d..bf4d70366 100644 --- a/samply/src/mac/task_profiler.rs +++ b/samply/src/mac/task_profiler.rs @@ -34,7 +34,7 @@ use super::proc_maps::{ }; use super::sampler::{ProcessSpecificPath, TaskInit}; use super::thread_profiler::{get_thread_id, get_thread_name, ThreadProfiler}; -use crate::shared::coreclr::DotnetTraceManager; +use crate::shared::coreclr::EventpipeTraceManager; use crate::shared::jit_category_manager::JitCategoryManager; use crate::shared::jit_function_recycler::JitFunctionRecycler; use crate::shared::jitdump_manager::JitDumpManager; @@ -111,7 +111,7 @@ pub struct TaskProfiler { unwinder: UnwinderNative, path_receiver: Receiver, jitdump_manager: JitDumpManager, - dotnet_trace_manager: DotnetTraceManager, + dotnet_trace_manager: EventpipeTraceManager, marker_file_paths: Vec<(ThreadHandle, PathBuf)>, unresolved_samples: UnresolvedSamples, lib_mapping_ops: LibMappingOpQueue, @@ -306,7 +306,7 @@ impl TaskProfiler { unwinder: UnwinderNative::new(), path_receiver, jitdump_manager: JitDumpManager::new(profile_creation_props.unlink_aux_files), - dotnet_trace_manager: DotnetTraceManager::new(profile_creation_props.unlink_aux_files), + dotnet_trace_manager: EventpipeTraceManager::new(profile_creation_props.unlink_aux_files), marker_file_paths: Vec::new(), lib_mapping_ops: Default::default(), unresolved_samples: Default::default(), @@ -615,7 +615,7 @@ impl TaskProfiler { .push((thread_handle, marker_file_path)); } ProcessSpecificPath::DotnetTracePath(dotnet_trace_path) => { - self.dotnet_trace_manager.add_dotnet_trace_path(dotnet_trace_path); + self.dotnet_trace_manager.add_dotnet_trace_path(dotnet_trace_path, self.pid); } } } diff --git a/samply/src/shared/coreclr/eventpipe.rs b/samply/src/shared/coreclr/eventpipe.rs deleted file mode 100644 index 736e8f708..000000000 --- a/samply/src/shared/coreclr/eventpipe.rs +++ /dev/null @@ -1,135 +0,0 @@ -use super::{CoreClrEvent, CoreClrMethodName, ReadyToRunMethodEntryPointEvent}; -use eventpipe::{coreclr::decode_coreclr_event, NettraceEvent}; - -type EventPipeEvent = eventpipe::coreclr::CoreClrEvent; - -// Given a NettraceEvent, convert it to a samply cross-platform CoreClrEvent. -pub fn eventpipe_event_to_coreclr_event( - process_id: u32, - ne: &NettraceEvent, -) -> Option { - // Convert the NettraceEvent to a eventpipe CoreClrEvent, then convert that to - // a samply CoreClrEvent - let Some(event) = decode_coreclr_event(ne) else { - return None; - }; - - let common = super::EventCommon { - timestamp: ne.timestamp, - process_id, - thread_id: ne.thread_id as u32, - stack: if !ne.stack.is_empty() { - Some(ne.stack.clone()) - } else { - None - }, - }; - - match &event { - EventPipeEvent::ModuleLoad(m) | - EventPipeEvent::ModuleUnload(m) => { - let e = super::ModuleLoadUnloadEvent { - common, - module_id: m.module_id, - assembly_id: m.assembly_id, - app_domain_id: m.app_domain_id, - module_il_path: m.module_il_path.to_string(), - module_native_path: m.module_native_path.to_string(), - }; - - match &event { - EventPipeEvent::ModuleLoad(_) => Some(CoreClrEvent::ModuleLoad(e)), - EventPipeEvent::ModuleUnload(_) => Some(CoreClrEvent::ModuleUnload(e)), - _ => unreachable!() - } - } - EventPipeEvent::ReadyToRunGetEntryPoint(method) => { - let name = if method.method_name.is_empty() { - format!("JIT[0x{:x}]", method.entry_point) - } else { - method.method_name.to_string() - }; - Some(CoreClrEvent::ReadyToRunMethodEntryPoint( - ReadyToRunMethodEntryPointEvent { - common, - start_address: method.entry_point, - name: CoreClrMethodName { - name, - namespace: method.method_namespace.to_string(), - signature: method.method_signature.to_string(), - }, - }, - )) - } - EventPipeEvent::MethodLoad(method) | - EventPipeEvent::MethodDCEnd(method) => { - let name = if method.method_name.is_empty() { - format!("JIT[0x{:x}]", method.method_start_address) - } else { - method.method_name.to_string() - }; - let dc_end = match &event { - EventPipeEvent::MethodDCEnd(_) => true, - _ => false, - }; - let tier = (method.method_flags >> 7) & 0x7; - let tier = match tier { - 1 => super::MethodCompilationTier::MinOptJitted, - 2 => super::MethodCompilationTier::Optimized, - 3 => super::MethodCompilationTier::QuickJitted, - 4 => super::MethodCompilationTier::OptimizedTier1, - 5 => super::MethodCompilationTier::OptimizedTier1OSR, - 6 => super::MethodCompilationTier::InstrumentedTier, - 7 => super::MethodCompilationTier::InstrumentedTierOptimized, - _ => super::MethodCompilationTier::Unknown, - }; - - Some(CoreClrEvent::MethodLoad(super::MethodLoadEvent { - common, - module_id: method.module_id, - start_address: method.method_start_address, - size: method.method_size, - name: CoreClrMethodName { - name, - namespace: method.method_namespace.to_string(), - signature: method.method_signature.to_string(), - }, - tier, - dc_end - })) - } - EventPipeEvent::MethodUnload(method) => { - Some(CoreClrEvent::MethodUnload(super::MethodUnloadEvent { - common, - start_address: method.method_start_address, - size: method.method_size, - })) - } - EventPipeEvent::GcTriggered(gc) => { - Some(CoreClrEvent::GcTriggered(super::GcTriggeredEvent { - common, - reason: gc.reason, - clr_instance_id: gc.clr_instance_id, - })) - } - EventPipeEvent::GcAllocationTick(tick) => Some(CoreClrEvent::GcAllocationTick( - super::GcAllocationTickEvent { - common, - kind: tick.allocation_kind, - size: tick.allocation_amount64, - type_name: Some(format!("Type[{}]", tick.type_id)), - type_namespace: None, - }, - )), - EventPipeEvent::GcSampledObjectAllocation(alloc) => Some( - CoreClrEvent::GcSampledObjectAllocation(super::GcSampledObjectAllocationEvent { - common, - address: alloc.address, - type_name: Some(format!("Type[{}]", alloc.type_id)), - type_namespace: None, - object_count: alloc.object_count_for_type_sample, - total_size: alloc.total_size_for_type_sample, - }), - ), - } -} diff --git a/samply/src/shared/coreclr/dotnet_trace_manager.rs b/samply/src/shared/coreclr/eventpipe_trace_manager.rs similarity index 81% rename from samply/src/shared/coreclr/dotnet_trace_manager.rs rename to samply/src/shared/coreclr/eventpipe_trace_manager.rs index 58d22b4d2..c87d7465c 100644 --- a/samply/src/shared/coreclr/dotnet_trace_manager.rs +++ b/samply/src/shared/coreclr/eventpipe_trace_manager.rs @@ -5,24 +5,23 @@ use std::sync::Arc; use crate::shared::jit_category_manager::JitCategoryManager; use crate::shared::lib_mappings::{LibMappingAdd, LibMappingInfo, LibMappingOp, LibMappingOpQueue}; use crate::shared::timestamp_converter::TimestampConverter; +use coreclr_tracing::{CoreClrEvent, EventMetadata, ModuleLoadUnloadEvent}; use debugid::CodeId; -use eventpipe::{EventPipeParser}; +use coreclr_tracing::nettrace::EventPipeParser; use fxprof_processed_profile::{ LibraryHandle, LibraryInfo, Profile, Symbol, SymbolTable, }; use wholesym::samply_symbols::debug_id_and_code_id_for_jitdump; -use super::{eventpipe_event_to_coreclr_event, CoreClrEvent, ModuleLoadUnloadEvent}; - -pub struct DotnetTraceManager { - pending_trace_paths: Vec, +pub struct EventpipeTraceManager { + pending_trace_paths: Vec<(u32, PathBuf)>, processors: Vec, unlink_after_open: bool, } -impl DotnetTraceManager { +impl EventpipeTraceManager { pub fn new(unlink_after_open: bool) -> Self { - DotnetTraceManager { + EventpipeTraceManager { pending_trace_paths: Vec::new(), processors: Vec::new(), unlink_after_open, @@ -32,10 +31,11 @@ impl DotnetTraceManager { pub fn add_dotnet_trace_path( &mut self, path: impl Into, + process_id: u32, ) { let path: PathBuf = path.into(); - log::info!("Adding dotnet trace path: {:?}", path); - self.pending_trace_paths.push(path); + log::info!("Adding dotnet trace path: {:?} for pid {}", path, process_id); + self.pending_trace_paths.push((process_id, path)); } pub fn process_pending_records( @@ -44,7 +44,7 @@ impl DotnetTraceManager { profile: &mut Profile, timestamp_converter: &TimestampConverter, ) { - self.pending_trace_paths.retain_mut(|path| { + self.pending_trace_paths.retain_mut(|(process_id, path)| { fn trace_reader_for_path( path: &Path, unlink_after_open: bool, @@ -82,7 +82,7 @@ impl DotnetTraceManager { }); self.processors - .push(SingleDotnetTraceProcessor::new(reader, lib_handle)); + .push(SingleDotnetTraceProcessor::new(reader, lib_handle, *process_id)); let _ = std::fs::remove_file(&path).is_err_and(|e| { log::warn!("Failed to remove {}: {}", path, e); true } ); @@ -119,7 +119,7 @@ struct SingleDotnetTraceProcessor { lib_mapping_ops: LibMappingOpQueue, symbols: Vec, - modules: HashMap, + modules: HashMap, seen_method_loads: HashSet<(u64, String)>, /// The relative_address of the next JIT function. @@ -130,10 +130,13 @@ struct SingleDotnetTraceProcessor { /// relative address is the sum of the `code_size`s of all the `JIT_CODE_LOAD` /// entries that came before it in the file. cumulative_address: u32, + + // The process ID of this process + process_id: u32, } impl SingleDotnetTraceProcessor { - pub fn new(reader: EventPipeParser, lib_handle: LibraryHandle) -> Self { + pub fn new(reader: EventPipeParser, lib_handle: LibraryHandle, process_id: u32) -> Self { Self { reader: Some(reader), lib_handle, @@ -142,6 +145,7 @@ impl SingleDotnetTraceProcessor { modules: Default::default(), seen_method_loads: Default::default(), cumulative_address: 0, + process_id, } } @@ -162,22 +166,14 @@ impl SingleDotnetTraceProcessor { match event { Ok(Some(ne)) => { last_timestamp = ne.timestamp; - if let Some(coreclr_event) = eventpipe_event_to_coreclr_event(0, &ne) { + if let Some((event_meta, coreclr_event)) = coreclr_tracing::eventpipe::decode_coreclr_event(&ne) { self.process_coreclr_event( + &event_meta.with_pid(self.process_id), &coreclr_event, jit_category_manager, profile, timestamp_converter, ); - } else { - /* - match ne.event_id { - 144 | 145 | 146 | 150 | 151 | 152 | 153 | 154 | 155 | 160 | 187 => { } - _ => { - eprintln!("Unknown event: {} / {}", ne.provider_name, ne.event_id); - } - } - */ } } Ok(None) => { @@ -202,27 +198,34 @@ impl SingleDotnetTraceProcessor { fn process_coreclr_event( &mut self, - coreclr_event: &CoreClrEvent, + event_meta: &EventMetadata, + event_coreclr: &CoreClrEvent, jit_category_manager: &mut JitCategoryManager, profile: &mut Profile, _timestamp_converter: &TimestampConverter, ) { - match coreclr_event { + match event_coreclr { CoreClrEvent::ModuleLoad(event) => { let module_id = event.module_id; //log::trace!("Loading module {} {} at {}", module_id, event.module_il_path, event.common.timestamp); - self.modules.insert(module_id, event.clone()); + self.modules.insert(module_id, (event_meta.clone(), event.clone())); } CoreClrEvent::ModuleUnload(event) => { let module_id = event.module_id; self.modules.remove(&module_id); } - CoreClrEvent::MethodLoad(event) => { - let start_avma = event.start_address; - let end_avma = event.start_address + event.size as u64; + CoreClrEvent::MethodLoad(event) | + CoreClrEvent::MethodDCEnd(event) => { + let dc_end = match event_coreclr { + CoreClrEvent::MethodDCEnd(_) => true, + _ => false, + }; + + let start_avma = event.method_start_address; + let end_avma = event.method_start_address + event.method_size as u64; - let msig = (event.start_address, event.name.name.clone()); - if !event.dc_end { + let msig = (event.method_start_address, event.method_name.clone()); + if !dc_end { self.seen_method_loads.insert(msig); } else if self.seen_method_loads.contains(&msig) { // we already saw a normal MethodLoad for this; skip it, so that @@ -231,15 +234,15 @@ impl SingleDotnetTraceProcessor { } let relative_address_at_start = self.cumulative_address; - self.cumulative_address += event.size; + self.cumulative_address += event.method_size; - let symbol_name = event.name.to_string(); + let symbol_name = event.method_name.to_string(); self.symbols.push(Symbol { address: relative_address_at_start, - size: if event.size == 0 { + size: if event.method_size == 0 { None } else { - Some(event.size) + Some(event.method_size) }, name: symbol_name.clone(), }); @@ -247,8 +250,8 @@ impl SingleDotnetTraceProcessor { let lib_handle = self.lib_handle; log::trace!( - "MethodLoad: addr = 0x{:x} symbol_name = {:?} size = {} (dcend = {})", - start_avma, symbol_name, event.size, event.dc_end + "MethodLoad: addr = 0x{:x} symbol_name = {:?} size = {} {}", + start_avma, symbol_name, event.method_size, if dc_end { "(DCEnd)" } else { "" } ); let (category, js_frame) = @@ -257,7 +260,7 @@ impl SingleDotnetTraceProcessor { // rundown, assume that it's valid for the entire range of the trace. // This isn't necessarily correct, but it's the best we can do given // the information we get. - let start_ts = if event.dc_end { 0 } else { event.common.timestamp }; + let start_ts = if dc_end { 0 } else { event_meta.timestamp }; self.lib_mapping_ops.push_unsorted( start_ts, @@ -269,7 +272,7 @@ impl SingleDotnetTraceProcessor { }), ); } - CoreClrEvent::ReadyToRunMethodEntryPoint(_event) => { + CoreClrEvent::ReadyToRunGetEntryPoint(_event) => { // Can't actually do anything with this, as we don't have a size. These methods just // won't be seen in the profile when we're using tracing only (like when attaching). } diff --git a/samply/src/shared/coreclr/events.rs b/samply/src/shared/coreclr/events.rs deleted file mode 100644 index 66b5e2d70..000000000 --- a/samply/src/shared/coreclr/events.rs +++ /dev/null @@ -1,156 +0,0 @@ -#![allow(unused)] - -pub use eventpipe::coreclr::{GcAllocationKind, GcReason, GcType}; - -use super::CoreClrMethodName; - -#[derive(Debug)] -pub enum CoreClrEvent { - ModuleLoad(ModuleLoadUnloadEvent), - ModuleUnload(ModuleLoadUnloadEvent), - MethodLoad(MethodLoadEvent), - MethodUnload(MethodUnloadEvent), - GcTriggered(GcTriggeredEvent), - GcAllocationTick(GcAllocationTickEvent), - GcSampledObjectAllocation(GcSampledObjectAllocationEvent), - GcStart(GcStartEvent), - GcEnd(GcEndEvent), - ReadyToRunMethodEntryPoint(ReadyToRunMethodEntryPointEvent), -} - -impl CoreClrEvent { - #[allow(unused)] - pub fn with_stack(mut self, stack: Vec) -> Self { - match self { - CoreClrEvent::ModuleLoad(ref mut e) => { - e.common.stack = Some(stack); - } - CoreClrEvent::ModuleUnload(ref mut e) => { - e.common.stack = Some(stack); - } - CoreClrEvent::MethodLoad(ref mut e) => { - e.common.stack = Some(stack); - } - CoreClrEvent::MethodUnload(ref mut e) => { - e.common.stack = Some(stack); - } - CoreClrEvent::GcTriggered(ref mut e) => { - e.common.stack = Some(stack); - } - CoreClrEvent::GcAllocationTick(ref mut e) => { - e.common.stack = Some(stack); - } - CoreClrEvent::GcSampledObjectAllocation(ref mut e) => { - e.common.stack = Some(stack); - } - CoreClrEvent::GcStart(ref mut e) => { - e.common.stack = Some(stack); - } - CoreClrEvent::GcEnd(ref mut e) => { - e.common.stack = Some(stack); - } - CoreClrEvent::ReadyToRunMethodEntryPoint(ref mut e) => { - e.common.stack = Some(stack); - } - } - self - } -} - -#[derive(Debug, Clone)] -pub struct EventCommon { - pub timestamp: u64, - pub process_id: u32, - pub thread_id: u32, - pub stack: Option>, -} - -#[derive(Debug, Clone)] -pub enum MethodCompilationTier { - Unknown, - MinOptJitted, - Optimized, - QuickJitted, - OptimizedTier1, - OptimizedTier1OSR, - InstrumentedTier, - InstrumentedTierOptimized, -} - -#[derive(Debug, Clone)] -pub struct ModuleLoadUnloadEvent { - pub common: EventCommon, - pub module_id: u64, - pub assembly_id: u64, - pub app_domain_id: Option, - pub module_il_path: String, - pub module_native_path: String, -} - -#[derive(Debug, Clone)] -pub struct MethodLoadEvent { - pub common: EventCommon, - pub module_id: u64, - pub start_address: u64, - pub size: u32, - pub name: CoreClrMethodName, - pub tier: MethodCompilationTier, - pub dc_end: bool, -} - -#[derive(Debug, Clone)] -pub struct MethodUnloadEvent { - pub common: EventCommon, - pub start_address: u64, - pub size: u32, -} - -#[derive(Debug, Clone)] -pub struct GcTriggeredEvent { - pub common: EventCommon, - pub reason: GcReason, - pub clr_instance_id: u16, -} - -#[derive(Debug, Clone)] -pub struct GcAllocationTickEvent { - pub common: EventCommon, - pub kind: GcAllocationKind, - pub size: u64, - pub type_name: Option, - pub type_namespace: Option, -} - -#[derive(Debug, Clone)] -pub struct GcSampledObjectAllocationEvent { - pub common: EventCommon, - pub address: u64, - pub type_name: Option, - pub type_namespace: Option, - pub object_count: u32, // number of objects in this sample - pub total_size: u64, // total size of all objects -} - -#[derive(Debug, Clone)] -pub struct GcStartEvent { - pub common: EventCommon, - pub count: u32, - pub reason: GcReason, - pub depth: Option, - pub gc_type: Option, -} - -#[derive(Debug, Clone)] -pub struct GcEndEvent { - pub common: EventCommon, - pub count: u32, - pub depth: u32, - pub reason: Option, -} - -#[derive(Debug, Clone)] -pub struct ReadyToRunMethodEntryPointEvent { - pub common: EventCommon, - pub start_address: u64, - pub name: CoreClrMethodName, -} diff --git a/samply/src/shared/coreclr/mod.rs b/samply/src/shared/coreclr/mod.rs index 4d935d953..1f8c8ef1d 100644 --- a/samply/src/shared/coreclr/mod.rs +++ b/samply/src/shared/coreclr/mod.rs @@ -1,15 +1,12 @@ -mod dotnet_trace_manager; -mod eventpipe; -mod events; +mod eventpipe_trace_manager; mod markers; mod provider; use std::fmt::Display; -pub use eventpipe::*; -pub use events::*; pub use provider::*; pub use markers::*; +pub(crate) use eventpipe_trace_manager::*; #[derive(Debug, Clone)] pub struct CoreClrProviderProps { diff --git a/samply/src/windows/coreclr.rs b/samply/src/windows/coreclr.rs index 6f1fb6160..7de0d8e53 100644 --- a/samply/src/windows/coreclr.rs +++ b/samply/src/windows/coreclr.rs @@ -1,7 +1,7 @@ use bitflags::bitflags; use std::{collections::HashMap, convert::TryInto, fmt::Display}; -use eventpipe::coreclr::{GcSuspendEeReason, GcType}; +use coreclr_tracing::{CoreClrEvent, EventMetadata, GcReason, GcSuspendEeReason, GcType}; use fxprof_processed_profile::*; use num_traits::FromPrimitive; @@ -505,10 +505,11 @@ pub fn handle_coreclr_event( } } -pub fn handle_new_coreclr_event( +pub fn handle_coreclr_tracing_event( context: &mut ProfileContext, coreclr_context: &mut CoreClrContext, - event: &CoreClrEvent, + event_meta: &EventMetadata, + event_coreclr: &CoreClrEvent, is_in_time_range: bool, ) { let (gc_markers, gc_suspensions, gc_allocs) = ( @@ -519,37 +520,37 @@ pub fn handle_new_coreclr_event( // Handle events that we need to handle whether in time range or not first - match event { + match event_coreclr { CoreClrEvent::MethodLoad(e) => { - let method_name = e.name.to_string(); + let method_name = e.method_name.to_string(); context.handle_coreclr_method_load( - e.common.timestamp, - e.common.process_id, + event_meta.timestamp, + event_meta.process_id, method_name, - e.start_address, - e.size, + e.method_start_address, + e.method_size, ); } - CoreClrEvent::MethodUnload(e) => { + CoreClrEvent::MethodUnload(_e) => { // don't care } CoreClrEvent::GcTriggered(e) if is_in_time_range && gc_markers => { let category = context.known_category(KnownCategory::CoreClrGc); let mh = context.add_thread_instant_marker( - e.common.timestamp, - e.common.thread_id, + event_meta.timestamp, + event_meta.thread_id, CoreClrGcMarker(category)); - coreclr_context.set_last_event_for_thread(e.common.thread_id, mh); + coreclr_context.set_last_event_for_thread(event_meta.thread_id, mh); } CoreClrEvent::GcAllocationTick(e) if is_in_time_range && gc_allocs => {} CoreClrEvent::GcSampledObjectAllocation(e) if is_in_time_range && gc_allocs => { - let type_name_str = context.intern_profile_string(&format!("{}", DisplayUnknownIfNone(&e.type_name))); + let type_name_str = context.intern_profile_string(&format!("type{}", e.type_id)); let category = context.known_category(KnownCategory::CoreClrGc); let mh = context.add_thread_instant_marker( - e.common.timestamp, - e.common.thread_id, - CoreClrGcAllocMarker(type_name_str, e.total_size as usize, category)); - coreclr_context.set_last_event_for_thread(e.common.thread_id, mh); + event_meta.timestamp, + event_meta.thread_id, + CoreClrGcAllocMarker(type_name_str, e.total_size_for_type_sample as usize, category)); + coreclr_context.set_last_event_for_thread(event_meta.thread_id, mh); } CoreClrEvent::GcStart(e) if is_in_time_range && gc_markers => { // TODO: save this as a CoreClrGcDetailedMarker, instead of putting @@ -557,7 +558,7 @@ pub fn handle_new_coreclr_event( /* // TODO: use gc_type_str as the name coreclr_context.save_gc_marker( - e.common.thread_id, + event_meta.thread_id, e.common.timestamp, format!( "{}: {} (GC #{}, gen{})", @@ -571,11 +572,11 @@ pub fn handle_new_coreclr_event( } CoreClrEvent::GcEnd(e) if is_in_time_range && gc_markers => { /* - if let Some(info) = coreclr_context.remove_gc_marker(e.common.thread_id, "GC") { + if let Some(info) = coreclr_context.remove_gc_marker(event_meta.thread_id, "GC") { context.add_thread_interval_marker( info.start_timestamp_raw, e.common.timestamp, - e.common.thread_id, + event_meta.thread_id, KnownCategory::CoreClrGc, &info.name, CoreClrGcEventMarker(info.description), diff --git a/samply/src/windows/etw_coreclr.rs b/samply/src/windows/etw_coreclr.rs deleted file mode 100644 index 3f9298aec..000000000 --- a/samply/src/windows/etw_coreclr.rs +++ /dev/null @@ -1,328 +0,0 @@ -use std::{collections::HashMap, convert::TryInto}; - -use eventpipe::coreclr::{GcSuspendEeReason, GcType}; -use fxprof_processed_profile::*; -use num_traits::FromPrimitive; - -use etw_reader::{self, schema::TypedEvent}; -use etw_reader::{ - parser::{Parser, TryParse}, -}; - -use crate::shared::coreclr::*; - - -use super::coreclr::CoreClrContext; - -pub struct CoreClrEtwConverter { - last_event_on_thread: HashMap, -} - -impl CoreClrEtwConverter { - pub fn new() -> Self { - Self { - last_event_on_thread: HashMap::new(), - } - } - - pub fn remaining_clr_events_on_threads( - &mut self, - ) -> std::collections::hash_map::IntoIter { - std::mem::take(&mut self.last_event_on_thread).into_iter() - } - - pub fn etw_event_to_coreclr_event( - &mut self, - coreclr_context: &mut CoreClrContext, - s: &TypedEvent, - parser: &mut Parser, - ) -> Option { - let timestamp_raw = s.timestamp() as u64; - - let mut name_parts = s.name().splitn(3, '/'); - let provider = name_parts.next().unwrap(); - let mut task = name_parts.next().unwrap(); - let mut opcode = name_parts.next().unwrap(); - - //if coreclr_context.last_n != s.name() { - // eprintln!("'{}' => '{}/{}/{}'", s.name(), provider, task, opcode); - // coreclr_context.last_n = s.name().to_string(); - //} - - match provider { - "Microsoft-Windows-DotNETRuntime" | "Microsoft-Windows-DotNETRuntimeRundown" => {} - _ => { - panic!("Unexpected event {}", s.name()) - } - } - - - // When working with merged ETL files, the proper task and opcode names appear here, e.g. "CLRMethod/MethodLoadVerbose" or - // "CLRMethodRundown/MethodDCStartVerbose". When working with the unmerged user ETL, these show up as e.g. "Method /DCStartVerbose". - // Not clear where those names come from the Etw .man file in CoreCLR does have entries for e.g. RuntimePublisher.MethodDCStartVerboseOpcodeMessage - // as "DCStartVerbose", but I'm not sure how/why those are referenced here and not in the merged ETL. xperf -a dumper on the unmerged - // ETL shows the same (correct) names as the merged ETL. - // - // We try to hack around this by converting the unmerged name to the converted one here. - if task.ends_with(' ') || opcode.ends_with(' ') { - task = task.trim(); - opcode = opcode.trim(); - - // Some of these are technically not correct; e.g. the task should be CLRMethodRundown if it's the - // rundown provider, but we handle them the same below. - match task { - "Method" => { - task = "CLRMethod"; - opcode = match opcode { - "LoadVerbose" => "MethodLoadVerbose", - "UnloadVerbose" => "MethodUnloadVerbose", - "DCStartVerbose" => "MethodDCStartVerbose", - "DCEndVerbose" => "MethodDCEndVerbose", - "JittingStarted" => "MethodJittingStarted", - _ => opcode.trim(), - }; - }, - "Loader" => { - task = "CLRLoader"; - opcode = match opcode { - "ModuleDCStart" => "ModuleDCStart", - _ => opcode.trim(), - }; - }, - "Runtime" => { - task = "CLRRuntimeInformation"; - opcode = opcode.trim(); - }, - "GC" => { - task = "GarbageCollection"; - opcode = match opcode { - "PerHeapHisory" => opcode, - "GCDynamicEvent" => opcode, - "Start" => "win:Start", - "Stop" => "win:Stop", - "RestartEEStart" => "GCRestartEEBegin", - "RestartEEStop" => "GCRestartEEEnd", - "SuspendEEStart" => "GCSuspendEEBegin", - "SuspendEEStop" => "GCSuspendEEEnd", - _ => opcode.trim(), - }; - }, - "ClrStack" => { - task = "CLRStack"; - opcode = match opcode { - "Walk" => "CLRStackWalk", - _ => opcode.trim(), - }; - }, - _ => {}, - } - } - - let pid = s.process_id(); - let tid = s.thread_id(); - - // ETW CoreCLR stackwalk events are a separate event that comes after the event to which - // it should be attached. Our cross-platform CoreCLR events have the stack as an optional - // part of every event. So, if we're recording stacks from ETW, instead of returning - // non-stackwalk events directly, we store them as the last event for a given thread so - // that we can attach a stack. - // - // If we have a pending event and we get a stackwalk event, we'll attach the stack and - // return the pending event. - // If we get a non-stackwalk event, we'll store it, and still return this previous - // pending event. - let pending_event = self.last_event_on_thread.remove(&tid); - - // Handle StackWalk events outside of the big match below for cleanliness - if (task, opcode) == ("CLRStack", "CLRStackWalk") { - // If the STACK keyword is enabled, we get a CLRStackWalk following each CLR event that supports stacks. Not every event - // does. The info about which does and doesn't is here: https://github.com/dotnet/runtime/blob/main/src/coreclr/vm/ClrEtwAllMeta.lst - // Current dotnet (8.0.x) seems to have a bug where `MethodJitMemoryAllocatedForCode` events will fire a stackwalk, - // but the event itself doesn't end up in the trace. (https://github.com/dotnet/runtime/issues/102004) - - // if we don't have anything to attach this stack to, just skip it - if pending_event.is_none() { - return None; - } - - let pending_event = pending_event.unwrap(); - - // "Stack" is explicitly declared as length 2 in the manifest, so the first two addresses are in here, rest - // are in user data buffer. - let first_addresses: Vec = parser.parse("Stack"); - let address_iter = first_addresses - .chunks_exact(8) - .chain(parser.buffer.chunks_exact(8)) - .map(|chunk| u64::from_le_bytes(chunk.try_into().unwrap())); - - return Some(pending_event.with_stack(address_iter.collect())); - } - - let common = EventCommon { - timestamp: timestamp_raw, - process_id: pid, - thread_id: tid, - stack: None, - }; - - let new_event = match (task, opcode) { - ("CLRMethod" | "CLRMethodRundown", method_event) => match method_event { - "MethodLoadVerbose" | "MethodDCStartVerbose" | "MethodDCEndVerbose" => { - let method_basename: String = parser.parse("MethodName"); - let method_namespace: String = parser.parse("MethodNamespace"); - let method_signature: String = parser.parse("MethodSignature"); - let module_id: u64 = parser.parse("ModuleID"); - - let method_start_address: u64 = parser.parse("MethodStartAddress"); - let method_size: u32 = parser.parse("MethodSize"); - - let dc_end = method_event == "MethodDCEndVerbose"; - - //log::trace!("{}: @ {:x} {}::{} {}", opcode, method_start_address, method_namespace, method_basename, method_signature); - - Some(CoreClrEvent::MethodLoad(MethodLoadEvent { - common, - module_id, - start_address: method_start_address, - size: method_size, - name: CoreClrMethodName { - name: method_basename, - namespace: method_namespace, - signature: method_signature, - }, - tier: MethodCompilationTier::Unknown, // TODO - dc_end - })) - } - _ => None, - }, - ("CLRLoader" | "CLRLoaderRundown", loader_event) => match loader_event { - // AppDomain, Assembly, Module Load/Unload - "ModuleDCStart" | _ => None, - }, - ("GarbageCollection", gc_event) => { - match gc_event { - "GCSampledObjectAllocation" => { - // If High/Low flags are set, then we get one of these for every alloc. Otherwise only - // when a threshold is hit. (100kb) The count and size are aggregates in that case. - let type_id: u64 = parser.parse("TypeID"); // TODO: convert to str, with bulk type data - let address: u64 = parser.parse("Address"); - let object_count: u32 = parser.parse("ObjectCountForTypeSample"); - let total_size: u64 = parser.parse("TotalSizeForTypeSample"); - - Some(CoreClrEvent::GcSampledObjectAllocation( - GcSampledObjectAllocationEvent { - common, - address, - type_name: Some(format!("Type[{}]", type_id)), - type_namespace: None, - object_count, - total_size, - }, - )) - } - "Triggered" => { - let reason: u32 = parser.parse("Reason"); - let reason = GcReason::from_u32(reason).unwrap_or_else(|| { - log::warn!("Unknown CLR GC Triggered reason: {}", reason); - GcReason::Empty - }); - - Some(CoreClrEvent::GcTriggered(GcTriggeredEvent { - common, - reason, - clr_instance_id: 0, - })) - } - "GCSuspendEEBegin" => { - // Reason, Count - //let _count: u32 = parser.parse("Count"); - let reason: u32 = parser.parse("Reason"); - - let _reason = GcSuspendEeReason::from_u32(reason).unwrap_or_else(|| { - log::warn!("Unknown CLR GCSuspendEEBegin reason: {}", reason); - GcSuspendEeReason::Other - }); - - //Some(CoreClrEvent::GcSuspendEeBegin(GcSuspendEeBeginEvent { - // common, - // reason, - //})) - // TODO - None - } - "GCSuspendEEEnd" => { - // TODO - None - } - "GCRestartEEBegin" => { - // TODO - None - } - "GCRestartEEEnd" => { - // TODO - None - } - "win:Start" => { - let count: u32 = parser.parse("Count"); - let depth: Option = parser.try_parse("Depth").ok(); - let reason: u32 = parser.parse("Reason"); - let gc_type = parser.try_parse("Type").ok().and_then(GcType::from_u32); - - let reason = GcReason::from_u32(reason).unwrap_or_else(|| { - log::warn!("Unknown CLR GCStart reason: {}", reason); - GcReason::Empty - }); - - Some(CoreClrEvent::GcStart(GcStartEvent { - common, - count, - depth, - reason, - gc_type, - })) - } - "win:Stop" => { - let count: u32 = parser.parse("Count"); - let depth: u32 = parser.parse("Depth"); - let reason = parser.try_parse("Reason").ok().and_then(GcReason::from_u32); - Some(CoreClrEvent::GcEnd(GcEndEvent { - common, - count, - depth, - reason, - })) - } - "SetGCHandle" => { - // TODO - None - } - "DestroyGCHandle" => { - // TODO - None - } - "GCFinalizersBegin" | "GCFinalizersEnd" | "FinalizeObject" => { - // TODO: create an interval - None - } - "GCCreateSegment" | "GCFreeSegment" | "GCDynamicEvent" | "GCHeapStats" => { - // don't care - None - } - _ => { - // don't care - None - } - } - } - ("CLRRuntimeInformation", _) => None, - _ => None, - }; - - if new_event.is_some() { - self.last_event_on_thread.insert(tid, new_event.unwrap()); - } - - pending_event - } -} diff --git a/samply/src/windows/etw_gecko.rs b/samply/src/windows/etw_gecko.rs index 68c55850e..5b46d684f 100644 --- a/samply/src/windows/etw_gecko.rs +++ b/samply/src/windows/etw_gecko.rs @@ -11,11 +11,10 @@ use etw_reader::{ use fxprof_processed_profile::debugid; use uuid::Uuid; -use super::coreclr::CoreClrContext; +use super::coreclr::{CoreClrContext, handle_coreclr_tracing_event}; use super::profile_context::ProfileContext; -use crate::windows::coreclr::handle_new_coreclr_event; -use crate::windows::etw_coreclr::CoreClrEtwConverter; use crate::windows::profile_context::{KnownCategory, PeInfo}; +use coreclr_tracing::etw::CoreClrEtwConverter; pub fn process_etl_files( context: &mut ProfileContext, @@ -512,14 +511,14 @@ fn process_trace( } let is_in_range = context.is_in_time_range(timestamp_raw); if let Some(event) = core_clr_converter.etw_event_to_coreclr_event( - core_clr_context, &s, &mut parser, ) { - handle_new_coreclr_event( + handle_coreclr_tracing_event( context, core_clr_context, - &event, + &event.0, + &event.1, is_in_range, ); } diff --git a/samply/src/windows/mod.rs b/samply/src/windows/mod.rs index 99c90c840..5cdc7477e 100644 --- a/samply/src/windows/mod.rs +++ b/samply/src/windows/mod.rs @@ -1,7 +1,6 @@ mod chrome; mod coreclr; mod elevated_helper; -mod etw_coreclr; mod etw_gecko; mod firefox; mod gfx; From e4b1e30aa78b4b18d258523b6db0d9dfdc190754 Mon Sep 17 00:00:00 2001 From: Vladimir Vukicevic Date: Mon, 2 Sep 2024 10:42:40 +0100 Subject: [PATCH 22/36] Add --auto-upload-profile flag --- samply-api/src/lib.rs | 3 +++ samply/src/main.rs | 5 +++++ samply/src/server.rs | 4 +++- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/samply-api/src/lib.rs b/samply-api/src/lib.rs index 13936f11c..ef15f4e64 100644 --- a/samply-api/src/lib.rs +++ b/samply-api/src/lib.rs @@ -204,6 +204,9 @@ impl<'a, H: FileAndPathHelper> Api<'a, H> { } else if request_url == "/asm/v1" { let asm_api = AsmApi::new(self.symbol_manager); asm_api.query_api_json(request_json_data).await + } else if request_url == "/auto-upload-reply/v1" { + println!("AUTO_UPLOAD_REPLY: {}", request_json_data); + "".to_owned() } else { json!({ "error": format!("Unrecognized URL {request_url}") }).to_string() } diff --git a/samply/src/main.rs b/samply/src/main.rs index 86193ff79..24bfb9ce0 100644 --- a/samply/src/main.rs +++ b/samply/src/main.rs @@ -299,6 +299,10 @@ struct ServerArgs { /// Print debugging output. #[arg(short, long)] verbose: bool, + + /// Auto-upload the profile and print the result URL + #[arg(long)] + auto_upload_profile: bool, } /// Arguments describing where to obtain symbol files. @@ -705,6 +709,7 @@ impl ServerArgs { port_selection, verbose: self.verbose, open_in_browser, + auto_upload_profile: self.auto_upload_profile, } } } diff --git a/samply/src/server.rs b/samply/src/server.rs index 10d90bdde..63943a454 100644 --- a/samply/src/server.rs +++ b/samply/src/server.rs @@ -33,6 +33,7 @@ pub struct ServerProps { pub port_selection: PortSelection, pub verbose: bool, pub open_in_browser: bool, + pub auto_upload_profile: bool, } #[tokio::main] @@ -136,6 +137,7 @@ async fn start_server( let path_prefix = format!("/{token}"); let server_origin = format!("http://{addr}"); let symbol_server_url = format!("{server_origin}{path_prefix}"); + let and_auto_upload_reply_url = if server_props.auto_upload_profile { format!("&autoUploadReplyUrl={symbol_server_url}/auto-upload-reply/v1") } else { "".to_owned() }; let mut template_values: HashMap<&'static str, String> = HashMap::new(); template_values.insert("SERVER_URL", server_origin.clone()); template_values.insert("PATH_PREFIX", path_prefix.clone()); @@ -155,7 +157,7 @@ async fn start_server( let encoded_symbol_server_url = utf8_percent_encode(&symbol_server_url, BAD_CHARS).to_string(); let profiler_url = format!( - "{profiler_origin}/from-url/{encoded_profile_url}/?symbolServer={encoded_symbol_server_url}" + "{profiler_origin}/from-url/{encoded_profile_url}/?symbolServer={encoded_symbol_server_url}{and_auto_upload_reply_url}" ); template_values.insert("PROFILER_URL", profiler_url.clone()); template_values.insert("PROFILE_URL", profile_url); From 793cc5012cf94e7c717d9553e3036020d8c56d7e Mon Sep 17 00:00:00 2001 From: Vladimir Vukicevic Date: Mon, 2 Sep 2024 16:14:06 +0100 Subject: [PATCH 23/36] Split build/release --- .github/workflows/build.yml | 186 ++++++++++++++++++++++++++++++++++ .github/workflows/release.yml | 161 ++--------------------------- 2 files changed, 193 insertions(+), 154 deletions(-) create mode 100644 .github/workflows/build.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 000000000..2b3e7347b --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,186 @@ +# Copyright 2022-2024, axodotdev +# SPDX-License-Identifier: MIT or Apache-2.0 +# +# CI that: +# +# * checks for a Git Tag that looks like a release +# * builds artifacts with cargo-dist (archives, installers, hashes) +# * uploads those artifacts to temporary workflow zip +# * on success, uploads the artifacts to a GitHub Release +# +# Note that the GitHub Release will be created with a generated +# title/body based on your changelogs. + +name: Build + +permissions: + contents: write + +on: + workflow_dispatch: + workflow_call: + outputs: + publishing: + description: Publishing + value: {{ jobs.plan.outputs.publishing }} + build_global_result: + description: Build Global Result + value: {{ jobs.build_global_artifacts.result }} + build_local_result: + description: Build Local Result + value: {{ jobs.build_local_artifacts.result }} + tag: + description: Tag + value: {{ jobs.plan.outputs.tag }} + tag_flag: + description: Tag Flag + value: {{ jobs.plan.outputs.tag_flag }} + +jobs: + # Run 'cargo dist plan' (or host) to determine what tasks we need to do + plan: + runs-on: ubuntu-latest + outputs: + val: ${{ steps.plan.outputs.manifest }} + tag: ${{ !github.event.pull_request && github.ref_name || '' }} + tag-flag: ${{ !github.event.pull_request && format('--tag={0}', github.ref_name) || '' }} + publishing: ${{ !github.event.pull_request }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - name: Install cargo-dist + # we specify bash to get pipefail; it guards against the `curl` command + # failing. otherwise `sh` won't catch that `curl` returned non-0 + shell: bash + run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.13.1/cargo-dist-installer.sh | sh" + # sure would be cool if github gave us proper conditionals... + # so here's a doubly-nested ternary-via-truthiness to try to provide the best possible + # functionality based on whether this is a pull_request, and whether it's from a fork. + # (PRs run on the *source* but secrets are usually on the *target* -- that's *good* + # but also really annoying to build CI around when it needs secrets to work right.) + - id: plan + run: | + cargo dist ${{ (!github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name)) || 'plan' }} --output-format=json > plan-dist-manifest.json + echo "cargo dist ran successfully" + cat plan-dist-manifest.json + echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT" + - name: "Upload dist-manifest.json" + uses: actions/upload-artifact@v4 + with: + name: artifacts-plan-dist-manifest + path: plan-dist-manifest.json + + # Build and packages all the platform-specific things + build-local-artifacts: + name: build-local-artifacts (${{ join(matrix.targets, ', ') }}) + # Let the initial task tell us to not run (currently very blunt) + needs: + - plan + if: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix.include != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') }} + strategy: + fail-fast: false + # Target platforms/runners are computed by cargo-dist in create-release. + # Each member of the matrix has the following arguments: + # + # - runner: the github runner + # - dist-args: cli flags to pass to cargo dist + # - install-dist: expression to run to install cargo-dist on the runner + # + # Typically there will be: + # - 1 "global" task that builds universal installers + # - N "local" tasks that build each platform's binaries and platform-specific installers + matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }} + runs-on: ${{ matrix.runner }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - uses: swatinem/rust-cache@v2 + with: + key: ${{ join(matrix.targets, '-') }} + - name: Install cargo-dist + run: ${{ matrix.install_dist }} + # Get the dist-manifest + - name: Fetch local artifacts + uses: actions/download-artifact@v4 + with: + pattern: artifacts-* + path: target/distrib/ + merge-multiple: true + - name: Install dependencies + run: | + ${{ matrix.packages_install }} + - name: Build artifacts + run: | + # Actually do builds and make zips and whatnot + cargo dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json + echo "cargo dist ran successfully" + - id: cargo-dist + name: Post-build + # We force bash here just because github makes it really hard to get values up + # to "real" actions without writing to env-vars, and writing to env-vars has + # inconsistent syntax between shell and powershell. + shell: bash + run: | + # Parse out what we just built and upload it to scratch storage + echo "paths<> "$GITHUB_OUTPUT" + jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT" + echo "EOF" >> "$GITHUB_OUTPUT" + + cp dist-manifest.json "$BUILD_MANIFEST_NAME" + - name: "Upload artifacts" + uses: actions/upload-artifact@v4 + with: + name: artifacts-build-local-${{ join(matrix.targets, '_') }} + path: | + ${{ steps.cargo-dist.outputs.paths }} + ${{ env.BUILD_MANIFEST_NAME }} + + # Build and package all the platform-agnostic(ish) things + build-global-artifacts: + needs: + - plan + - build-local-artifacts + runs-on: "ubuntu-20.04" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - name: Install cargo-dist + shell: bash + run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.13.1/cargo-dist-installer.sh | sh" + # Get all the local artifacts for the global tasks to use (for e.g. checksums) + - name: Fetch local artifacts + uses: actions/download-artifact@v4 + with: + pattern: artifacts-* + path: target/distrib/ + merge-multiple: true + - id: cargo-dist + shell: bash + run: | + cargo dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json + echo "cargo dist ran successfully" + + # Parse out what we just built and upload it to scratch storage + echo "paths<> "$GITHUB_OUTPUT" + jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT" + echo "EOF" >> "$GITHUB_OUTPUT" + + cp dist-manifest.json "$BUILD_MANIFEST_NAME" + - name: "Upload artifacts" + uses: actions/upload-artifact@v4 + with: + name: artifacts-build-global + path: | + ${{ steps.cargo-dist.outputs.paths }} + ${{ env.BUILD_MANIFEST_NAME }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 89e761fcf..ec47c302f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -44,161 +44,14 @@ on: pull_request: jobs: - # Run 'cargo dist plan' (or host) to determine what tasks we need to do - plan: - runs-on: ubuntu-latest - outputs: - val: ${{ steps.plan.outputs.manifest }} - tag: ${{ !github.event.pull_request && github.ref_name || '' }} - tag-flag: ${{ !github.event.pull_request && format('--tag={0}', github.ref_name) || '' }} - publishing: ${{ !github.event.pull_request }} - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - steps: - - uses: actions/checkout@v4 - with: - submodules: recursive - - name: Install cargo-dist - # we specify bash to get pipefail; it guards against the `curl` command - # failing. otherwise `sh` won't catch that `curl` returned non-0 - shell: bash - run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.13.1/cargo-dist-installer.sh | sh" - # sure would be cool if github gave us proper conditionals... - # so here's a doubly-nested ternary-via-truthiness to try to provide the best possible - # functionality based on whether this is a pull_request, and whether it's from a fork. - # (PRs run on the *source* but secrets are usually on the *target* -- that's *good* - # but also really annoying to build CI around when it needs secrets to work right.) - - id: plan - run: | - cargo dist ${{ (!github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name)) || 'plan' }} --output-format=json > plan-dist-manifest.json - echo "cargo dist ran successfully" - cat plan-dist-manifest.json - echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT" - - name: "Upload dist-manifest.json" - uses: actions/upload-artifact@v4 - with: - name: artifacts-plan-dist-manifest - path: plan-dist-manifest.json - - # Build and packages all the platform-specific things - build-local-artifacts: - name: build-local-artifacts (${{ join(matrix.targets, ', ') }}) - # Let the initial task tell us to not run (currently very blunt) - needs: - - plan - if: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix.include != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') }} - strategy: - fail-fast: false - # Target platforms/runners are computed by cargo-dist in create-release. - # Each member of the matrix has the following arguments: - # - # - runner: the github runner - # - dist-args: cli flags to pass to cargo dist - # - install-dist: expression to run to install cargo-dist on the runner - # - # Typically there will be: - # - 1 "global" task that builds universal installers - # - N "local" tasks that build each platform's binaries and platform-specific installers - matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }} - runs-on: ${{ matrix.runner }} - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json - steps: - - uses: actions/checkout@v4 - with: - submodules: recursive - - uses: swatinem/rust-cache@v2 - with: - key: ${{ join(matrix.targets, '-') }} - - name: Install cargo-dist - run: ${{ matrix.install_dist }} - # Get the dist-manifest - - name: Fetch local artifacts - uses: actions/download-artifact@v4 - with: - pattern: artifacts-* - path: target/distrib/ - merge-multiple: true - - name: Install dependencies - run: | - ${{ matrix.packages_install }} - - name: Build artifacts - run: | - # Actually do builds and make zips and whatnot - cargo dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json - echo "cargo dist ran successfully" - - id: cargo-dist - name: Post-build - # We force bash here just because github makes it really hard to get values up - # to "real" actions without writing to env-vars, and writing to env-vars has - # inconsistent syntax between shell and powershell. - shell: bash - run: | - # Parse out what we just built and upload it to scratch storage - echo "paths<> "$GITHUB_OUTPUT" - jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT" - echo "EOF" >> "$GITHUB_OUTPUT" - - cp dist-manifest.json "$BUILD_MANIFEST_NAME" - - name: "Upload artifacts" - uses: actions/upload-artifact@v4 - with: - name: artifacts-build-local-${{ join(matrix.targets, '_') }} - path: | - ${{ steps.cargo-dist.outputs.paths }} - ${{ env.BUILD_MANIFEST_NAME }} - - # Build and package all the platform-agnostic(ish) things - build-global-artifacts: - needs: - - plan - - build-local-artifacts - runs-on: "ubuntu-20.04" - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json - steps: - - uses: actions/checkout@v4 - with: - submodules: recursive - - name: Install cargo-dist - shell: bash - run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.13.1/cargo-dist-installer.sh | sh" - # Get all the local artifacts for the global tasks to use (for e.g. checksums) - - name: Fetch local artifacts - uses: actions/download-artifact@v4 - with: - pattern: artifacts-* - path: target/distrib/ - merge-multiple: true - - id: cargo-dist - shell: bash - run: | - cargo dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json - echo "cargo dist ran successfully" - - # Parse out what we just built and upload it to scratch storage - echo "paths<> "$GITHUB_OUTPUT" - jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT" - echo "EOF" >> "$GITHUB_OUTPUT" - - cp dist-manifest.json "$BUILD_MANIFEST_NAME" - - name: "Upload artifacts" - uses: actions/upload-artifact@v4 - with: - name: artifacts-build-global - path: | - ${{ steps.cargo-dist.outputs.paths }} - ${{ env.BUILD_MANIFEST_NAME }} + build: + uses: ./.github/workflows/build.yml # Determines if we should publish/announce host: needs: - - plan - - build-local-artifacts - - build-global-artifacts + - build # Only run if we're "publishing", and only if local and global didn't fail (skipped is fine) - if: ${{ always() && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }} + if: ${{ always() && needs.build.outputs.publishing == 'true' && (needs.build.outputs.global_artifacts_result == 'skipped' || needs.build.outputs.global_artifacts_result == 'success') && (needs.build.outputs.local_artifacts_result == 'skipped' || needs.build.outputs.local_artifacts_result == 'success') }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} runs-on: "ubuntu-20.04" @@ -221,7 +74,7 @@ jobs: - id: host shell: bash run: | - cargo dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json + cargo dist host ${{ needs.build.outputs.tag_flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json echo "artifacts uploaded and released successfully" cat dist-manifest.json echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT" @@ -235,7 +88,7 @@ jobs: # Create a GitHub Release while uploading all files to it announce: needs: - - plan + - build - host # use "always() && ..." to allow us to wait for all publish jobs while # still allowing individual publish jobs to skip themselves (for prereleases). @@ -261,7 +114,7 @@ jobs: - name: Create GitHub Release uses: ncipollo/release-action@v1 with: - tag: ${{ needs.plan.outputs.tag }} + tag: ${{ needs.build.outputs.tag }} name: ${{ fromJson(needs.host.outputs.val).announcement_title }} body: ${{ fromJson(needs.host.outputs.val).announcement_github_body }} prerelease: ${{ fromJson(needs.host.outputs.val).announcement_is_prerelease }} From 2cfb8407d5e78074e8b56e40be2d7b0a4f14eef4 Mon Sep 17 00:00:00 2001 From: Vladimir Vukicevic Date: Mon, 2 Sep 2024 16:19:14 +0100 Subject: [PATCH 24/36] Test build --- .github/workflows/build.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2b3e7347b..fae0fa733 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,6 +17,9 @@ permissions: contents: write on: + push: + paths: + - './.github/workflows/build.yml' workflow_dispatch: workflow_call: outputs: From ae84e18cef657a96d648dc29b69866d294d0a1b3 Mon Sep 17 00:00:00 2001 From: Vladimir Vukicevic Date: Mon, 2 Sep 2024 16:40:18 +0100 Subject: [PATCH 25/36] Sigh --- .github/workflows/build.yml | 189 ---------------------------------- .github/workflows/release.yml | 161 +++++++++++++++++++++++++++-- 2 files changed, 154 insertions(+), 196 deletions(-) delete mode 100644 .github/workflows/build.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index fae0fa733..000000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,189 +0,0 @@ -# Copyright 2022-2024, axodotdev -# SPDX-License-Identifier: MIT or Apache-2.0 -# -# CI that: -# -# * checks for a Git Tag that looks like a release -# * builds artifacts with cargo-dist (archives, installers, hashes) -# * uploads those artifacts to temporary workflow zip -# * on success, uploads the artifacts to a GitHub Release -# -# Note that the GitHub Release will be created with a generated -# title/body based on your changelogs. - -name: Build - -permissions: - contents: write - -on: - push: - paths: - - './.github/workflows/build.yml' - workflow_dispatch: - workflow_call: - outputs: - publishing: - description: Publishing - value: {{ jobs.plan.outputs.publishing }} - build_global_result: - description: Build Global Result - value: {{ jobs.build_global_artifacts.result }} - build_local_result: - description: Build Local Result - value: {{ jobs.build_local_artifacts.result }} - tag: - description: Tag - value: {{ jobs.plan.outputs.tag }} - tag_flag: - description: Tag Flag - value: {{ jobs.plan.outputs.tag_flag }} - -jobs: - # Run 'cargo dist plan' (or host) to determine what tasks we need to do - plan: - runs-on: ubuntu-latest - outputs: - val: ${{ steps.plan.outputs.manifest }} - tag: ${{ !github.event.pull_request && github.ref_name || '' }} - tag-flag: ${{ !github.event.pull_request && format('--tag={0}', github.ref_name) || '' }} - publishing: ${{ !github.event.pull_request }} - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - steps: - - uses: actions/checkout@v4 - with: - submodules: recursive - - name: Install cargo-dist - # we specify bash to get pipefail; it guards against the `curl` command - # failing. otherwise `sh` won't catch that `curl` returned non-0 - shell: bash - run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.13.1/cargo-dist-installer.sh | sh" - # sure would be cool if github gave us proper conditionals... - # so here's a doubly-nested ternary-via-truthiness to try to provide the best possible - # functionality based on whether this is a pull_request, and whether it's from a fork. - # (PRs run on the *source* but secrets are usually on the *target* -- that's *good* - # but also really annoying to build CI around when it needs secrets to work right.) - - id: plan - run: | - cargo dist ${{ (!github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name)) || 'plan' }} --output-format=json > plan-dist-manifest.json - echo "cargo dist ran successfully" - cat plan-dist-manifest.json - echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT" - - name: "Upload dist-manifest.json" - uses: actions/upload-artifact@v4 - with: - name: artifacts-plan-dist-manifest - path: plan-dist-manifest.json - - # Build and packages all the platform-specific things - build-local-artifacts: - name: build-local-artifacts (${{ join(matrix.targets, ', ') }}) - # Let the initial task tell us to not run (currently very blunt) - needs: - - plan - if: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix.include != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') }} - strategy: - fail-fast: false - # Target platforms/runners are computed by cargo-dist in create-release. - # Each member of the matrix has the following arguments: - # - # - runner: the github runner - # - dist-args: cli flags to pass to cargo dist - # - install-dist: expression to run to install cargo-dist on the runner - # - # Typically there will be: - # - 1 "global" task that builds universal installers - # - N "local" tasks that build each platform's binaries and platform-specific installers - matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }} - runs-on: ${{ matrix.runner }} - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json - steps: - - uses: actions/checkout@v4 - with: - submodules: recursive - - uses: swatinem/rust-cache@v2 - with: - key: ${{ join(matrix.targets, '-') }} - - name: Install cargo-dist - run: ${{ matrix.install_dist }} - # Get the dist-manifest - - name: Fetch local artifacts - uses: actions/download-artifact@v4 - with: - pattern: artifacts-* - path: target/distrib/ - merge-multiple: true - - name: Install dependencies - run: | - ${{ matrix.packages_install }} - - name: Build artifacts - run: | - # Actually do builds and make zips and whatnot - cargo dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json - echo "cargo dist ran successfully" - - id: cargo-dist - name: Post-build - # We force bash here just because github makes it really hard to get values up - # to "real" actions without writing to env-vars, and writing to env-vars has - # inconsistent syntax between shell and powershell. - shell: bash - run: | - # Parse out what we just built and upload it to scratch storage - echo "paths<> "$GITHUB_OUTPUT" - jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT" - echo "EOF" >> "$GITHUB_OUTPUT" - - cp dist-manifest.json "$BUILD_MANIFEST_NAME" - - name: "Upload artifacts" - uses: actions/upload-artifact@v4 - with: - name: artifacts-build-local-${{ join(matrix.targets, '_') }} - path: | - ${{ steps.cargo-dist.outputs.paths }} - ${{ env.BUILD_MANIFEST_NAME }} - - # Build and package all the platform-agnostic(ish) things - build-global-artifacts: - needs: - - plan - - build-local-artifacts - runs-on: "ubuntu-20.04" - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json - steps: - - uses: actions/checkout@v4 - with: - submodules: recursive - - name: Install cargo-dist - shell: bash - run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.13.1/cargo-dist-installer.sh | sh" - # Get all the local artifacts for the global tasks to use (for e.g. checksums) - - name: Fetch local artifacts - uses: actions/download-artifact@v4 - with: - pattern: artifacts-* - path: target/distrib/ - merge-multiple: true - - id: cargo-dist - shell: bash - run: | - cargo dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json - echo "cargo dist ran successfully" - - # Parse out what we just built and upload it to scratch storage - echo "paths<> "$GITHUB_OUTPUT" - jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT" - echo "EOF" >> "$GITHUB_OUTPUT" - - cp dist-manifest.json "$BUILD_MANIFEST_NAME" - - name: "Upload artifacts" - uses: actions/upload-artifact@v4 - with: - name: artifacts-build-global - path: | - ${{ steps.cargo-dist.outputs.paths }} - ${{ env.BUILD_MANIFEST_NAME }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ec47c302f..89e761fcf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -44,14 +44,161 @@ on: pull_request: jobs: - build: - uses: ./.github/workflows/build.yml + # Run 'cargo dist plan' (or host) to determine what tasks we need to do + plan: + runs-on: ubuntu-latest + outputs: + val: ${{ steps.plan.outputs.manifest }} + tag: ${{ !github.event.pull_request && github.ref_name || '' }} + tag-flag: ${{ !github.event.pull_request && format('--tag={0}', github.ref_name) || '' }} + publishing: ${{ !github.event.pull_request }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - name: Install cargo-dist + # we specify bash to get pipefail; it guards against the `curl` command + # failing. otherwise `sh` won't catch that `curl` returned non-0 + shell: bash + run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.13.1/cargo-dist-installer.sh | sh" + # sure would be cool if github gave us proper conditionals... + # so here's a doubly-nested ternary-via-truthiness to try to provide the best possible + # functionality based on whether this is a pull_request, and whether it's from a fork. + # (PRs run on the *source* but secrets are usually on the *target* -- that's *good* + # but also really annoying to build CI around when it needs secrets to work right.) + - id: plan + run: | + cargo dist ${{ (!github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name)) || 'plan' }} --output-format=json > plan-dist-manifest.json + echo "cargo dist ran successfully" + cat plan-dist-manifest.json + echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT" + - name: "Upload dist-manifest.json" + uses: actions/upload-artifact@v4 + with: + name: artifacts-plan-dist-manifest + path: plan-dist-manifest.json + + # Build and packages all the platform-specific things + build-local-artifacts: + name: build-local-artifacts (${{ join(matrix.targets, ', ') }}) + # Let the initial task tell us to not run (currently very blunt) + needs: + - plan + if: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix.include != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') }} + strategy: + fail-fast: false + # Target platforms/runners are computed by cargo-dist in create-release. + # Each member of the matrix has the following arguments: + # + # - runner: the github runner + # - dist-args: cli flags to pass to cargo dist + # - install-dist: expression to run to install cargo-dist on the runner + # + # Typically there will be: + # - 1 "global" task that builds universal installers + # - N "local" tasks that build each platform's binaries and platform-specific installers + matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }} + runs-on: ${{ matrix.runner }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - uses: swatinem/rust-cache@v2 + with: + key: ${{ join(matrix.targets, '-') }} + - name: Install cargo-dist + run: ${{ matrix.install_dist }} + # Get the dist-manifest + - name: Fetch local artifacts + uses: actions/download-artifact@v4 + with: + pattern: artifacts-* + path: target/distrib/ + merge-multiple: true + - name: Install dependencies + run: | + ${{ matrix.packages_install }} + - name: Build artifacts + run: | + # Actually do builds and make zips and whatnot + cargo dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json + echo "cargo dist ran successfully" + - id: cargo-dist + name: Post-build + # We force bash here just because github makes it really hard to get values up + # to "real" actions without writing to env-vars, and writing to env-vars has + # inconsistent syntax between shell and powershell. + shell: bash + run: | + # Parse out what we just built and upload it to scratch storage + echo "paths<> "$GITHUB_OUTPUT" + jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT" + echo "EOF" >> "$GITHUB_OUTPUT" + + cp dist-manifest.json "$BUILD_MANIFEST_NAME" + - name: "Upload artifacts" + uses: actions/upload-artifact@v4 + with: + name: artifacts-build-local-${{ join(matrix.targets, '_') }} + path: | + ${{ steps.cargo-dist.outputs.paths }} + ${{ env.BUILD_MANIFEST_NAME }} + + # Build and package all the platform-agnostic(ish) things + build-global-artifacts: + needs: + - plan + - build-local-artifacts + runs-on: "ubuntu-20.04" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - name: Install cargo-dist + shell: bash + run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.13.1/cargo-dist-installer.sh | sh" + # Get all the local artifacts for the global tasks to use (for e.g. checksums) + - name: Fetch local artifacts + uses: actions/download-artifact@v4 + with: + pattern: artifacts-* + path: target/distrib/ + merge-multiple: true + - id: cargo-dist + shell: bash + run: | + cargo dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json + echo "cargo dist ran successfully" + + # Parse out what we just built and upload it to scratch storage + echo "paths<> "$GITHUB_OUTPUT" + jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT" + echo "EOF" >> "$GITHUB_OUTPUT" + + cp dist-manifest.json "$BUILD_MANIFEST_NAME" + - name: "Upload artifacts" + uses: actions/upload-artifact@v4 + with: + name: artifacts-build-global + path: | + ${{ steps.cargo-dist.outputs.paths }} + ${{ env.BUILD_MANIFEST_NAME }} # Determines if we should publish/announce host: needs: - - build + - plan + - build-local-artifacts + - build-global-artifacts # Only run if we're "publishing", and only if local and global didn't fail (skipped is fine) - if: ${{ always() && needs.build.outputs.publishing == 'true' && (needs.build.outputs.global_artifacts_result == 'skipped' || needs.build.outputs.global_artifacts_result == 'success') && (needs.build.outputs.local_artifacts_result == 'skipped' || needs.build.outputs.local_artifacts_result == 'success') }} + if: ${{ always() && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} runs-on: "ubuntu-20.04" @@ -74,7 +221,7 @@ jobs: - id: host shell: bash run: | - cargo dist host ${{ needs.build.outputs.tag_flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json + cargo dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json echo "artifacts uploaded and released successfully" cat dist-manifest.json echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT" @@ -88,7 +235,7 @@ jobs: # Create a GitHub Release while uploading all files to it announce: needs: - - build + - plan - host # use "always() && ..." to allow us to wait for all publish jobs while # still allowing individual publish jobs to skip themselves (for prereleases). @@ -114,7 +261,7 @@ jobs: - name: Create GitHub Release uses: ncipollo/release-action@v1 with: - tag: ${{ needs.build.outputs.tag }} + tag: ${{ needs.plan.outputs.tag }} name: ${{ fromJson(needs.host.outputs.val).announcement_title }} body: ${{ fromJson(needs.host.outputs.val).announcement_github_body }} prerelease: ${{ fromJson(needs.host.outputs.val).announcement_is_prerelease }} From 9a5a65e3a70ed50b0c4d559d4ddedea1ea67203e Mon Sep 17 00:00:00 2001 From: Vladimir Vukicevic Date: Mon, 2 Sep 2024 16:43:25 +0100 Subject: [PATCH 26/36] prerelease 2 --- samply/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samply/Cargo.toml b/samply/Cargo.toml index 727cea57d..1554fd17b 100644 --- a/samply/Cargo.toml +++ b/samply/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "usamply" -version = "0.12.10-prerelease.1" +version = "0.12.10-prerelease.2" authors = ["Markus Stange ", "Vladimir Vukicevic "] edition = "2021" rust-version = "1.75" # needed by wholesym -> fs4 From 50af6d7f9378deb02be74b9e4c79c7bd266799ce Mon Sep 17 00:00:00 2001 From: Vladimir Vukicevic Date: Tue, 3 Sep 2024 10:46:07 +0100 Subject: [PATCH 27/36] Have samply exit after auto-upload; version bump --- Cargo.lock | 2 +- samply-api/src/lib.rs | 7 +++++-- samply/Cargo.toml | 2 +- samply/src/main.rs | 3 ++- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3af8da744..d48412286 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2454,7 +2454,7 @@ dependencies = [ [[package]] name = "usamply" -version = "0.12.10-prerelease.1" +version = "0.12.10-prerelease.3" dependencies = [ "bitflags 2.5.0", "byteorder", diff --git a/samply-api/src/lib.rs b/samply-api/src/lib.rs index ef15f4e64..b00588b1d 100644 --- a/samply-api/src/lib.rs +++ b/samply-api/src/lib.rs @@ -205,8 +205,11 @@ impl<'a, H: FileAndPathHelper> Api<'a, H> { let asm_api = AsmApi::new(self.symbol_manager); asm_api.query_api_json(request_json_data).await } else if request_url == "/auto-upload-reply/v1" { - println!("AUTO_UPLOAD_REPLY: {}", request_json_data); - "".to_owned() + use std::io::Write; + + println!("SAMPLY_AUTO_UPLOAD_REPLY: {}", request_json_data); + let _ = std::io::stdout().flush(); + std::process::exit(0); } else { json!({ "error": format!("Unrecognized URL {request_url}") }).to_string() } diff --git a/samply/Cargo.toml b/samply/Cargo.toml index 1554fd17b..5ad5b9361 100644 --- a/samply/Cargo.toml +++ b/samply/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "usamply" -version = "0.12.10-prerelease.2" +version = "0.12.10-prerelease.3" authors = ["Markus Stange ", "Vladimir Vukicevic "] edition = "2021" rust-version = "1.75" # needed by wholesym -> fs4 diff --git a/samply/src/main.rs b/samply/src/main.rs index 24bfb9ce0..ae9beb450 100644 --- a/samply/src/main.rs +++ b/samply/src/main.rs @@ -300,7 +300,8 @@ struct ServerArgs { #[arg(short, long)] verbose: bool, - /// Auto-upload the profile and print the result URL + /// Auto-upload the profile and print the result URL as JSON prefixed + /// with SAMPLY_AUTO_UPLOAD_URL. The samply process will exit after the upload. #[arg(long)] auto_upload_profile: bool, } From a65fa7f10fc49fabdcd5ccfa977790ad76e44326 Mon Sep 17 00:00:00 2001 From: Vladimir Vukicevic Date: Tue, 3 Sep 2024 14:36:25 +0100 Subject: [PATCH 28/36] Search for xperf.exe in the default install location --- samply/src/main.rs | 2 +- samply/src/windows/xperf.rs | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/samply/src/main.rs b/samply/src/main.rs index ae9beb450..f51ffb941 100644 --- a/samply/src/main.rs +++ b/samply/src/main.rs @@ -301,7 +301,7 @@ struct ServerArgs { verbose: bool, /// Auto-upload the profile and print the result URL as JSON prefixed - /// with SAMPLY_AUTO_UPLOAD_URL. The samply process will exit after the upload. + /// with SAMPLY_AUTO_UPLOAD_REPLY. The samply process will exit after the upload. #[arg(long)] auto_upload_profile: bool, } diff --git a/samply/src/windows/xperf.rs b/samply/src/windows/xperf.rs index 4f97c6fe9..a87f4cb79 100644 --- a/samply/src/windows/xperf.rs +++ b/samply/src/windows/xperf.rs @@ -40,7 +40,16 @@ impl Xperf { if let Some(p) = self.xperf_path.clone() { return Ok(p); } - let xperf_path = which::which("xperf").map_err(|_| XPERF_NOT_FOUND_ERROR_MSG)?; + let xperf_path = which::which("xperf") + .or_else(|_| { + let pf = std::env::var("ProgramFiles(x86)").map_err(|_| XPERF_NOT_FOUND_ERROR_MSG)?; + let xperf_install_path = PathBuf::from(pf).join("Windows Kits/10/Windows Performance Toolkit/xperf.exe"); + if xperf_install_path.exists() { + Ok(xperf_install_path) + } else { + Err(XPERF_NOT_FOUND_ERROR_MSG) + } + })?; self.xperf_path = Some(xperf_path.clone()); Ok(xperf_path) } From 9f2f8bda9d1d8dc36827638c0fe2e7a337abd78a Mon Sep 17 00:00:00 2001 From: Vladimir Vukicevic Date: Tue, 3 Sep 2024 14:37:09 +0100 Subject: [PATCH 29/36] Bump version --- Cargo.lock | 2 +- samply/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d48412286..bd903fed4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2454,7 +2454,7 @@ dependencies = [ [[package]] name = "usamply" -version = "0.12.10-prerelease.3" +version = "0.12.10-prerelease.4" dependencies = [ "bitflags 2.5.0", "byteorder", diff --git a/samply/Cargo.toml b/samply/Cargo.toml index 5ad5b9361..c97f8602e 100644 --- a/samply/Cargo.toml +++ b/samply/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "usamply" -version = "0.12.10-prerelease.3" +version = "0.12.10-prerelease.4" authors = ["Markus Stange ", "Vladimir Vukicevic "] edition = "2021" rust-version = "1.75" # needed by wholesym -> fs4 From 1dd71d6d492fde40173909b49a02e0fe77507286 Mon Sep 17 00:00:00 2001 From: Vladimir Vukicevic Date: Tue, 3 Sep 2024 16:48:39 +0100 Subject: [PATCH 30/36] Fix CoreCLR method name --- samply/src/windows/coreclr.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/samply/src/windows/coreclr.rs b/samply/src/windows/coreclr.rs index 7de0d8e53..648c67f45 100644 --- a/samply/src/windows/coreclr.rs +++ b/samply/src/windows/coreclr.rs @@ -522,7 +522,10 @@ pub fn handle_coreclr_tracing_event( match event_coreclr { CoreClrEvent::MethodLoad(e) => { - let method_name = e.method_name.to_string(); + let method_basename = e.method_name.to_string(); + let method_namespace = e.method_namespace.to_string(); + let method_signature = e.method_signature.to_string(); + let method_name = format!("{method_basename} [{method_namespace}] \u{2329}{method_signature}\u{232a}"); context.handle_coreclr_method_load( event_meta.timestamp, event_meta.process_id, From 52c85654ddf9a2812368739ceb199ebae5cb485e Mon Sep 17 00:00:00 2001 From: Vladimir Vukicevic Date: Wed, 4 Sep 2024 08:42:36 +0100 Subject: [PATCH 31/36] Bump to pre.5 --- Cargo.lock | 2 +- samply/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bd903fed4..852f055cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2454,7 +2454,7 @@ dependencies = [ [[package]] name = "usamply" -version = "0.12.10-prerelease.4" +version = "0.12.10-prerelease.5" dependencies = [ "bitflags 2.5.0", "byteorder", diff --git a/samply/Cargo.toml b/samply/Cargo.toml index c97f8602e..b757ac7e5 100644 --- a/samply/Cargo.toml +++ b/samply/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "usamply" -version = "0.12.10-prerelease.4" +version = "0.12.10-prerelease.5" authors = ["Markus Stange ", "Vladimir Vukicevic "] edition = "2021" rust-version = "1.75" # needed by wholesym -> fs4 From 8f5a00d43b3284229fa6b27c22230cc4e22714f0 Mon Sep 17 00:00:00 2001 From: Vladimir Vukicevic Date: Thu, 5 Sep 2024 13:47:16 +0100 Subject: [PATCH 32/36] Fixes for coreclr method name rendering --- .../shared/coreclr/eventpipe_trace_manager.rs | 13 +++++++++++- samply/src/shared/coreclr/mod.rs | 20 ++++++++++++------- samply/src/windows/coreclr.rs | 4 ++-- 3 files changed, 27 insertions(+), 10 deletions(-) diff --git a/samply/src/shared/coreclr/eventpipe_trace_manager.rs b/samply/src/shared/coreclr/eventpipe_trace_manager.rs index c87d7465c..ccf2697e6 100644 --- a/samply/src/shared/coreclr/eventpipe_trace_manager.rs +++ b/samply/src/shared/coreclr/eventpipe_trace_manager.rs @@ -2,6 +2,7 @@ use std::collections::{HashMap, HashSet}; use std::path::{Path, PathBuf}; use std::sync::Arc; +use crate::shared::coreclr::CoreClrMethodName; use crate::shared::jit_category_manager::JitCategoryManager; use crate::shared::lib_mappings::{LibMappingAdd, LibMappingInfo, LibMappingOp, LibMappingOpQueue}; use crate::shared::timestamp_converter::TimestampConverter; @@ -216,6 +217,12 @@ impl SingleDotnetTraceProcessor { } CoreClrEvent::MethodLoad(event) | CoreClrEvent::MethodDCEnd(event) => { + // eventpipe traces on Mac/Linux only include R2R method loads as DCEnd events + // during end rundown. This is broken; they should be at worst DCStart events + // in a start rundown, and ideally regular MethodLoad events as long as the r2r + // shared library is loaded dynamically. We still have to deal with this, though: + // the one thing we don't get is an accurate time range for the r2r methods. We + // assume they start at 0 time, which is not correct for later-loaded assemblies. let dc_end = match event_coreclr { CoreClrEvent::MethodDCEnd(_) => true, _ => false, @@ -236,7 +243,11 @@ impl SingleDotnetTraceProcessor { let relative_address_at_start = self.cumulative_address; self.cumulative_address += event.method_size; - let symbol_name = event.method_name.to_string(); + let symbol_name = CoreClrMethodName::format( + &event.method_name, + &event.method_namespace, + &event.method_signature, + ); self.symbols.push(Symbol { address: relative_address_at_start, size: if event.method_size == 0 { diff --git a/samply/src/shared/coreclr/mod.rs b/samply/src/shared/coreclr/mod.rs index 1f8c8ef1d..49db00743 100644 --- a/samply/src/shared/coreclr/mod.rs +++ b/samply/src/shared/coreclr/mod.rs @@ -31,14 +31,20 @@ pub struct CoreClrMethodName { pub signature: String, } -impl Display for CoreClrMethodName { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, +impl CoreClrMethodName { + pub fn format(name: &str, namespace: &str, signature: &str) -> String { + // \u{2329} \u{232a} are fancy angle brackets + format!( "{name} [{namespace}] \u{2329}{signature}\u{232a}", - name = self.name, - namespace = self.namespace, - signature = self.signature + name = name, + namespace = namespace, + signature = signature ) } } + +impl Display for CoreClrMethodName { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&CoreClrMethodName::format(&self.name, &self.namespace, &self.signature)) + } +} diff --git a/samply/src/windows/coreclr.rs b/samply/src/windows/coreclr.rs index 648c67f45..e9c45bcda 100644 --- a/samply/src/windows/coreclr.rs +++ b/samply/src/windows/coreclr.rs @@ -221,7 +221,7 @@ pub fn handle_coreclr_event( // there's some stuff in MethodFlags -- might be tiered JIT info? // also ClrInstanceID -- we probably won't have more than one runtime, but maybe. - let method_name = format!("{method_basename} [{method_namespace}] \u{2329}{method_signature}\u{232a}"); + let method_name = CoreClrMethodName::format(&method_basename, &method_namespace, &method_signature); context.handle_coreclr_method_load(timestamp_raw, pid, method_name, method_start_address, method_size); handled = true; @@ -525,7 +525,7 @@ pub fn handle_coreclr_tracing_event( let method_basename = e.method_name.to_string(); let method_namespace = e.method_namespace.to_string(); let method_signature = e.method_signature.to_string(); - let method_name = format!("{method_basename} [{method_namespace}] \u{2329}{method_signature}\u{232a}"); + let method_name = CoreClrMethodName::format(&method_basename, &method_namespace, &method_signature); context.handle_coreclr_method_load( event_meta.timestamp, event_meta.process_id, From 5ea9c6a2032fe58b180cdedecb9f7bbf36822916 Mon Sep 17 00:00:00 2001 From: Vladimir Vukicevic Date: Mon, 9 Sep 2024 09:44:02 -0700 Subject: [PATCH 33/36] Support coreclr args on non-windows as well --- samply/src/main.rs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/samply/src/main.rs b/samply/src/main.rs index f51ffb941..64b8ff797 100644 --- a/samply/src/main.rs +++ b/samply/src/main.rs @@ -263,13 +263,9 @@ struct RecordArgs { #[derive(ValueEnum, Copy, Clone, Debug, PartialEq, Eq)] enum CoreClrArgs { Enabled, - #[cfg(target_os = "windows")] GcMarkers, - #[cfg(target_os = "windows")] GcSuspendedThreads, - #[cfg(target_os = "windows")] GcDetailedAllocs, - #[cfg(target_os = "windows")] EventStacks, } @@ -734,13 +730,9 @@ fn to_coreclr_profile_props(coreclr_args: &[CoreClrArgs]) -> CoreClrProfileProps #[allow(clippy::needless_update)] CoreClrProfileProps { enabled: coreclr_args.contains(&CoreClrArgs::Enabled), - #[cfg(target_os = "windows")] gc_markers: coreclr_args.contains(&CoreClrArgs::GcMarkers), - #[cfg(target_os = "windows")] gc_suspensions: coreclr_args.contains(&CoreClrArgs::GcSuspendedThreads), - #[cfg(target_os = "windows")] gc_detailed_allocs: coreclr_args.contains(&CoreClrArgs::GcDetailedAllocs), - #[cfg(target_os = "windows")] event_stacks: coreclr_args.contains(&CoreClrArgs::EventStacks), ..Default::default() } From 893fa5d1eb7b876e232dd8d2286d28d280c4fd6f Mon Sep 17 00:00:00 2001 From: Vladimir Vukicevic Date: Mon, 9 Sep 2024 09:52:19 -0700 Subject: [PATCH 34/36] Remove old unused function --- samply/src/windows/coreclr.rs | 369 ---------------------------------- 1 file changed, 369 deletions(-) diff --git a/samply/src/windows/coreclr.rs b/samply/src/windows/coreclr.rs index e9c45bcda..7b4eb9d75 100644 --- a/samply/src/windows/coreclr.rs +++ b/samply/src/windows/coreclr.rs @@ -136,375 +136,6 @@ pub fn coreclr_xperf_args(props: &ElevatedRecordingProps) -> Vec { coreclr_provider_args(coreclr_props) } -pub fn handle_coreclr_event( - context: &mut ProfileContext, - coreclr_context: &mut CoreClrContext, - s: &TypedEvent, - parser: &mut Parser, - is_in_time_range: bool, -) { - let (gc_markers, gc_suspensions, gc_allocs, event_stacks) = ( - coreclr_context.props.gc_markers, - coreclr_context.props.gc_suspensions, - coreclr_context.props.gc_detailed_allocs, - coreclr_context.props.event_stacks, - ); - - let pid = s.process_id(); - let tid = s.thread_id(); - - if !context.is_interesting_process(pid, None, None) { - return; - } - - let timestamp_raw = s.timestamp() as u64; - - let mut name_parts = s.name().splitn(3, '/'); - let provider = name_parts.next().unwrap(); - let task = name_parts.next().unwrap(); - let opcode = name_parts.next().unwrap(); - - match provider { - "Microsoft-Windows-DotNETRuntime" | "Microsoft-Windows-DotNETRuntimeRundown" => {} - _ => { - panic!("Unexpected event {}", s.name()) - } - } - - // TODO -- we may need to use the rundown provider if we trace running processes - // https://learn.microsoft.com/en-us/dotnet/framework/performance/clr-etw-providers - - // We get DbgID_RSDS for ReadyToRun loaded images, along with PDB files. We also get ModuleLoad events for the same: - // this means we can ignore the ModuleLoadEvents because we'll get dbginfo already mapped properly when the image - // is loaded. - - let mut handled = false; - - //eprintln!("event: {} [pid: {} tid: {}] {}", timestamp_raw, s.pid(), s.tid(), dotnet_event); - - // If we get a non-stackwalk event followed by a non-stackwalk event for a given thread, - // clear out any marker that may have been created to make sure the stackwalk doesn't - // get attached to the wrong thing. - if (task, opcode) != ("CLRStack", "CLRStackWalk") { - coreclr_context.remove_last_event_for_thread(tid); - } - - match (task, opcode) { - ("CLRMethod" | "CLRMethodRundown", method_event) => { - match method_event { - // there's MethodDCStart & MethodDCStartVerbose & MethodLoad - // difference between *Verbose and not, is Verbose includes the names - - "MethodLoadVerbose" | "MethodDCStartVerbose" - // | "R2RGetEntryPoint" // not sure we need this? R2R methods should be covered by PDB files - => { - // R2RGetEntryPoint shares a lot of fields with MethodLoadVerbose - let is_r2r = method_event == "R2RGetEntryPoint"; - - //let method_id: u64 = parser.parse("MethodID"); - //let clr_instance_id: u32 = parser.parse("ClrInstanceID"); // v1/v2 only - - let method_basename: String = parser.parse("MethodName"); - let method_namespace: String = parser.parse("MethodNamespace"); - let method_signature: String = parser.parse("MethodSignature"); - - let method_start_address: u64 = if is_r2r { parser.parse("EntryPoint") } else { parser.parse("MethodStartAddress") }; - let method_size: u32 = parser.parse("MethodSize"); // TODO: R2R doesn't have a size? - - // There's a v0, v1, and v2 version of this event. There are rules in `eventtrace.cpp` in the runtime - // that describe the rules, but basically: - // - during a first-JIT, only a v1 (not v0 and not v2+) MethodLoad is emitted. - // - during a re-jit, a v2 event is emitted. - // - v2 contains a "NativeCodeId" field which will be nonzero in v2. - // - the unique key for a method extent is MethodId + MethodCodeId + extent (hot/cold) - - // there's some stuff in MethodFlags -- might be tiered JIT info? - // also ClrInstanceID -- we probably won't have more than one runtime, but maybe. - - let method_name = CoreClrMethodName::format(&method_basename, &method_namespace, &method_signature); - - context.handle_coreclr_method_load(timestamp_raw, pid, method_name, method_start_address, method_size); - handled = true; - } - "ModuleLoad" | "ModuleDCStart" | - "ModuleUnload" | "ModuleDCEnd" => { - // do we need this for ReadyToRun code? - - //let module_id: u64 = parser.parse("ModuleID"); - //let assembly_id: u64 = parser.parse("AssemblyId"); - //let managed_pdb_signature: u?? = parser.parse("ManagedPdbSignature"); - //let managed_pdb_age: u?? = parser.parse("ManagedPdbAge"); - //let managed_pdb_path: String = parser.parse("ManagedPdbPath"); - //let native_pdb_signature: u?? = parser.parse("NativePdbSignature"); - //let native_pdb_age: u?? = parser.parse("NativePdbAge"); - //let native_pdb_path: String = parser.parse("NativePdbPath"); - handled = true; - } - _ => { - // don't care about any other CLRMethod events - handled = true; - } - } - } - ("Type", "BulkType") => { - //