Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/dwarf-perf-regression.yml
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ jobs:
--results-dir "$DWARF_PERF_RESULTS_DIR" \
--result-name "base-${target}" \
--runs 10 \
--cargo-target-dir "$CARGO_TARGET_DIR" \
--cargo-target-dir "$CARGO_TARGET_DIR/base" \
--parse-target "$target"
done

Expand Down Expand Up @@ -196,7 +196,7 @@ jobs:
--results-dir "$DWARF_PERF_RESULTS_DIR" \
--result-name "head-${target}" \
--runs 10 \
--cargo-target-dir "$CARGO_TARGET_DIR" \
--cargo-target-dir "$CARGO_TARGET_DIR/head" \
--parse-target "$target"
done

Expand Down Expand Up @@ -239,7 +239,7 @@ jobs:
--results-dir "$DWARF_PERF_RESULTS_DIR" \
--result-name "head-${target}" \
--runs 10 \
--cargo-target-dir "$CARGO_TARGET_DIR" \
--cargo-target-dir "$CARGO_TARGET_DIR/head" \
--parse-target "$target"
done

Expand Down
88 changes: 74 additions & 14 deletions bins/dwarf-tool/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
use anyhow::Result;
use clap::{Parser, Subcommand};
use ghostscope_dwarf::{
AddressQueryResult, DwarfAnalyzer, FunctionQueryResult, ModuleLoadingEvent, ModuleLoadingStats,
SectionType, SourceLanguage, ValueAdapterOutcome, ValueAdapterReport, ValueAdapterStage,
ValueCapturePlan, ValuePresentation, VariableAccessPath,
AddressQueryResult, DwarfAnalyzer, FunctionQueryResult, ModuleAddress, ModuleLoadingEvent,
ModuleLoadingStats, SectionType, SourceLanguage, ValueAdapterOutcome, ValueAdapterReport,
ValueAdapterStage, ValueCapturePlan, ValuePresentation, VariableAccessPath,
};
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
Expand Down Expand Up @@ -164,6 +164,9 @@ enum Commands {
/// Number of runs
#[arg(long, default_value = "10")]
runs: usize,
/// Number of unmeasured warmup queries
#[arg(long, default_value = "10")]
warmup_runs: usize,
/// JSON output
#[arg(long)]
json: bool,
Expand Down Expand Up @@ -228,6 +231,7 @@ struct SourceLineBenchmarkResult {
source_location: String,
loading_time_ms: u64,
load_breakdown: LoadTimingBreakdown,
warmup_runs: usize,
address_count: usize,
total_variables: usize,
first_address: Option<String>,
Expand Down Expand Up @@ -433,13 +437,15 @@ async fn main() -> Result<()> {
Commands::BenchmarkSourceLine {
location,
runs,
warmup_runs,
json,
} => {
return run_source_line_benchmark(
cli.pid,
cli.target.as_deref(),
location,
*runs,
*warmup_runs,
*json,
)
.await;
Expand Down Expand Up @@ -1232,6 +1238,7 @@ async fn run_source_line_benchmark(
target_path: Option<&str>,
source: &str,
runs: usize,
warmup_runs: usize,
json: bool,
) -> Result<()> {
if runs == 0 {
Expand All @@ -1240,9 +1247,15 @@ async fn run_source_line_benchmark(

if !json {
if let Some(pid) = pid {
println!("Benchmarking source-line query for PID {pid} at {source} ({runs} runs)...\n");
println!(
"Benchmarking source-line query for PID {pid} at {source} \
({warmup_runs} warmup, {runs} measured runs)...\n"
);
} else if let Some(target) = target_path {
println!("Benchmarking source-line query for {target} at {source} ({runs} runs)...\n");
println!(
"Benchmarking source-line query for {target} at {source} \
({warmup_runs} warmup, {runs} measured runs)...\n"
);
}
}

Expand All @@ -1266,6 +1279,24 @@ async fn run_source_line_benchmark(
load_breakdown.index_time_ms,
load_breakdown.internal_total_time_ms
);
if warmup_runs > 0 {
print!("Warming: ");
std::io::Write::flush(&mut std::io::stdout()).unwrap();
}
}

for _ in 0..warmup_runs {
let _ = execute_source_line_query(&analyzer, file_path, line_number)?;
if !json {
print!(".");
std::io::Write::flush(&mut std::io::stdout()).unwrap();
}
}

if !json {
if warmup_runs > 0 {
println!();
}
print!("Querying: ");
std::io::Write::flush(&mut std::io::stdout()).unwrap();
}
Expand All @@ -1277,14 +1308,8 @@ async fn run_source_line_benchmark(
}

let start = Instant::now();
let addresses = analyzer.lookup_addresses_by_source_line(file_path, line_number);
let mut run_total_variables = 0usize;

for module_address in &addresses {
let pc_context = analyzer.resolve_pc(module_address)?;
run_total_variables += analyzer.visible_variables(&pc_context)?.len();
}

let (addresses, run_total_variables) =
execute_source_line_query(&analyzer, file_path, line_number)?;
query_times.push(start.elapsed());

if run_index == 0 {
Expand All @@ -1304,6 +1329,7 @@ async fn run_source_line_benchmark(
source_location: source.to_string(),
loading_time_ms: loading_time.as_millis() as u64,
load_breakdown,
warmup_runs,
address_count,
total_variables,
first_address,
Expand All @@ -1320,6 +1346,7 @@ async fn run_source_line_benchmark(
println!(" First address: {first_address}");
}
println!("\nResults:");
println!(" Warmup runs: {}", result.warmup_runs);
println!(" First run: {:.3}ms", result.benchmark.first_run_ms);
println!(" Average query time: {:.3}ms", result.benchmark.average_ms);
println!(" P50: {:.3}ms", result.benchmark.p50_ms);
Expand All @@ -1331,6 +1358,22 @@ async fn run_source_line_benchmark(
Ok(())
}

fn execute_source_line_query(
analyzer: &DwarfAnalyzer,
file_path: &str,
line_number: u32,
) -> Result<(Vec<ModuleAddress>, usize)> {
let addresses = analyzer.lookup_addresses_by_source_line(file_path, line_number);
let mut total_variables = 0usize;

for module_address in &addresses {
let pc_context = analyzer.resolve_pc(module_address)?;
total_variables += analyzer.visible_variables(&pc_context)?.len();
}

Ok((addresses, total_variables))
}

fn summarize_durations(durations: &[std::time::Duration]) -> Result<BenchmarkSummary> {
if durations.is_empty() {
return Err(anyhow::anyhow!("Cannot summarize an empty benchmark run"));
Expand Down Expand Up @@ -1463,7 +1506,8 @@ fn parse_address(address_str: &str) -> Result<u64> {

#[cfg(test)]
mod tests {
use super::{parse_source_line, percentile_nearest_rank};
use super::{parse_source_line, percentile_nearest_rank, Cli, Commands};
use clap::Parser;

#[test]
fn parses_source_line_with_colons_in_path() {
Expand All @@ -1478,6 +1522,22 @@ mod tests {
assert_eq!(percentile_nearest_rank(&samples, 0.50), 3.0);
assert_eq!(percentile_nearest_rank(&samples, 0.95), 5.0);
}

#[test]
fn source_line_benchmark_defaults_to_ten_warmup_queries() {
let cli =
Cli::try_parse_from(["dwarf-tool", "benchmark-source-line", "query_hotspot.c:17"])
.unwrap();

let Commands::BenchmarkSourceLine {
runs, warmup_runs, ..
} = cli.command
else {
panic!("expected source-line benchmark command");
};
assert_eq!(runs, 10);
assert_eq!(warmup_runs, 10);
}
}

#[derive(Debug, serde::Serialize)]
Expand Down
3 changes: 2 additions & 1 deletion config-zh.toml
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,8 @@ max_size_bytes = 0
# 默认值:4
max_nesting_depth = 4

# 每个嵌套序列节点最多采集多少个语义子元素,例如 Vec<String>。如果
# 每个嵌套序列节点最多采集多少个语义子元素,例如 Vec<String>,以及
# 嵌套 Hash 表中最多为多少个 bucket 采集 key/value sidecar。如果
# mem_dump_cap 无法容纳这么多子 payload,实际采集数量会更少。
# 有效范围:1 到 16。
# 默认值:4
Expand Down
5 changes: 3 additions & 2 deletions config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -154,8 +154,9 @@ max_size_bytes = 0
max_nesting_depth = 4

# Maximum semantic child elements captured for each nested sequence node, such
# as Vec<String>. The effective count can be lower when mem_dump_cap cannot fit
# this many child payloads.
# as Vec<String>, and buckets with key/value sidecars in nested hash tables.
# The effective count can be lower when mem_dump_cap cannot fit this many child
# payloads.
# Valid range: 1 to 16.
# Default: 4
max_sequence_elements = 4
Expand Down
12 changes: 7 additions & 5 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -559,13 +559,15 @@ enable_sysmon_for_target = true # Default
independent of the depth-32 limit for native inline DWARF structure
formatting.
- `max_sequence_elements` limits the semantic children statically emitted for
each nested sequence node. It is a per-node width limit, not a recursion
depth.
each nested sequence node and the captured buckets that receive repeated
key/value sidecars in a nested hash table. It is a per-node width limit, not
a recursion depth.

Both settings default to `4` and accept values from `1` through `16`. The
effective sequence count is also bounded by the runtime sequence length and the
argument's `ebpf.mem_dump_cap`. Increasing either setting can increase generated
eBPF program size, event size, and probe overhead.
effective collection width is also bounded by the runtime sequence length or
hash-table capacity and the argument's `ebpf.mem_dump_cap`. Increasing either
setting can increase generated eBPF program size, event size, and probe
overhead.

### Configuration Examples

Expand Down
10 changes: 5 additions & 5 deletions docs/limitations.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@ formatting has a maximum depth of 32.

**Rust** has targeted semantic support for common standard-library value
families. It is selected from the concrete target DWARF rather than from a
promised Rust ABI. Nested adapters compose for projected wrappers and bounded
sequences under configured depth and per-sequence element limits plus a shared
byte budget. Hash-table and B-Tree key/value adapters do not yet compose. See
[Rust Value Presentation](scripting.md#rust-value-presentation) for the current
type list, formatting behavior, and nesting boundary.
promised Rust ABI. Nested adapters compose for projected wrappers, bounded
sequences, and hash-table entries under configured depth and collection-width
limits plus a shared byte budget. B-Tree key/value adapters do not yet compose.
See [Rust Value Presentation](scripting.md#rust-value-presentation) for the
current type list, formatting behavior, and nesting boundary.

**C++** remains primarily DWARF-layout-oriented. Simple C-like layouts work
best; broader standard-library and language-specific semantics are not modeled
Expand Down
25 changes: 15 additions & 10 deletions docs/scripting.md
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,8 @@ Cell<String> Cell wrapper plus captured String bytes
Vec<String> each captured element uses the String adapter
Vec<CString> each captured element omits its trailing NUL
Vec<Vec<i32>> each captured element uses the Vec adapter
HashMap<String, Vec<i32>> captured keys and values use their adapters
HashSet<String> captured values use the String adapter
Option<String> only Some captures its String payload
Result<Request, String> only the active Ok or Err payload is captured
```
Expand All @@ -280,18 +282,21 @@ detects repeated DWARF type identities on the active path. Traversing an
ordinary Rust struct to reach an adapted field counts as one edge. Transitioning
into an active enum payload also counts as one edge. Each semantic sequence
reserves child captures for at most
`value_adapters.max_sequence_elements` elements. Both limits default to `4`;
the former controls recursion depth while the latter controls the width of
each sequence node. The root and every child share the argument's
`mem_dump_cap`; reducing that cap can reduce the captured element count or
fall back to the valid root presentation. Every child carries an independent
read-error or truncation status. See
`value_adapters.max_sequence_elements` elements. Nested hash tables use the
same setting to bound the number of captured buckets with key/value sidecars;
because a hash table is sparse, the bounded bucket prefix can contain fewer
occupied entries. Both limits default to `4`; the former controls recursion
depth while the latter controls each collection node's width. The root and
every child share the argument's `mem_dump_cap`; reducing that cap can reduce
the captured element or bucket count, or fall back to the valid root
presentation. Every child carries an independent read-error or truncation
status. See
[Value Adapter Limits](configuration.md#value-adapter-limits).

Hash-table and B-Tree entry traversal still formats key and value bytes using
their DWARF types. For example, `HashMap<String, Vec<i32>>` does not recursively
run adapters for its keys or values. Known nested fields remain accessible
explicitly through the DSL.
Hash-table entry traversal recursively applies recognized adapters to keys and
values while retaining native DWARF formatting for unrecognized fields. B-Tree
entry traversal still formats key and value bytes using their DWARF types;
known nested fields remain accessible explicitly through the DSL.

## Variables

Expand Down
11 changes: 6 additions & 5 deletions docs/zh/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -544,12 +544,13 @@ enable_sysmon_for_target = true # 默认开启
- `max_nesting_depth` 计算根值以下的语义子值边数;为了到达已适配字段而
穿过的普通 Rust struct,以及进入激活 enum payload 的转换也计算在内。
它不包含根本身,也不影响原生内联 DWARF 结构格式化的 32 层深度限制。
- `max_sequence_elements` 限制每个嵌套序列节点静态生成的语义子元素数量。
它是每个节点的宽度限制,不是递归深度。
- `max_sequence_elements` 限制每个嵌套序列节点静态生成的语义子元素数量,
以及嵌套 Hash 表中带重复 key/value sidecar 的已采集 bucket 数量。它是
每个节点的宽度限制,不是递归深度。

两项默认值均为 `4`,有效范围均为 `1..=16`。序列的实际采集数量还会受到
运行时序列长度和参数 `ebpf.mem_dump_cap` 的限制。提高任一配置都可能增加
生成的 eBPF 程序大小、事件大小和探针开销。
两项默认值均为 `4`,有效范围均为 `1..=16`。集合的实际采集宽度还会受到
运行时序列长度或 Hash 表容量,以及参数 `ebpf.mem_dump_cap` 的限制。提高
任一配置都可能增加生成的 eBPF 程序大小、事件大小和探针开销。

### 配置示例

Expand Down
6 changes: 3 additions & 3 deletions docs/zh/limitations.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ GhostScope 的脚本和编译器能力面只用于观测,不提供主动修改
32。

**Rust** 已针对常见标准库类型提供定向语义支持。这些适配器根据具体目标
的 DWARF 选择,而不是假设一个稳定的 Rust ABI。投影包装器和有界序列中的
嵌套 adapter 会在配置的递归深度、单序列元素数和共享字节预算内组合;
Hash 表和 B-Tree 的 key/value adapter 尚不会递归组合。当前类型列表、
的 DWARF 选择,而不是假设一个稳定的 Rust ABI。投影包装器、有界序列和
Hash 表条目中的嵌套 adapter 会在配置的递归深度、集合宽度和共享字节预算
内组合;B-Tree 的 key/value adapter 尚不会递归组合。当前类型列表、
格式化行为和嵌套边界见
[Rust 值展示](scripting.md#rust-值展示)。

Expand Down
16 changes: 10 additions & 6 deletions docs/zh/scripting.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,8 @@ Cell<String> 显示 Cell 包装并采集 String 字节
Vec<String> 每个已采集元素使用 String 适配器
Vec<CString> 每个已采集元素都会排除结尾 NUL
Vec<Vec<i32>> 每个已采集元素使用 Vec 适配器
HashMap<String, Vec<i32>> 已采集的 key/value 使用各自的适配器
HashSet<String> 已采集的值使用 String 适配器
Option<String> 只有 Some 会采集其 String payload
Result<Request, String> 只采集当前激活的 Ok 或 Err payload
```
Expand All @@ -258,15 +260,17 @@ struct 中没有可识别的语义后代,它仍使用原有的原生 DWARF 路
`value_adapters.max_nesting_depth` 条语义子值边,并检测当前路径中重复的
DWARF 类型标识。为了到达已适配字段而穿过一个普通 Rust struct 也算一条边。
从 enum 进入其 payload 同样算一条边。每个语义序列最多为
`value_adapters.max_sequence_elements` 个元素预留子采集槽。两项默认值均为
`4`;前者控制递归深度,后者控制每个序列节点的宽度。根值和所有子值共享
该参数的 `mem_dump_cap`;降低这个上限可能减少采集的元素数,或回退到仍然
`value_adapters.max_sequence_elements` 个元素预留子采集槽。嵌套 Hash 表也使用
该配置限制带 key/value sidecar 的 bucket 数;由于 Hash 表是稀疏的,有界
bucket 前缀中实际占用的条目可能更少。两项默认值均为 `4`;前者控制递归
深度,后者控制每个集合节点的宽度。根值和所有子值共享该参数的
`mem_dump_cap`;降低这个上限可能减少采集的元素或 bucket 数,或回退到仍然
有效的根值展示。每个子值都有独立的读取错误和截断状态。配置细节见
[Value Adapter 限制](configuration.md#value-adapter-限制)。

Hash 表和 B-Tree 的条目遍历仍按 key/value 的 DWARF 类型格式化字节。例如,
`HashMap<String, Vec<i32>>` 还不会为 key 或 value 递归运行适配器。已知的
嵌套字段仍可在 DSL 中显式访问。
Hash 表条目遍历会对 key/value 递归应用已识别的适配器,未识别字段仍使用
原生 DWARF 格式。B-Tree 条目遍历仍按 key/value 的 DWARF 类型格式化字节;
已知的嵌套字段仍可在 DSL 中显式访问。

## 变量

Expand Down
Loading
Loading