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
56 changes: 42 additions & 14 deletions bench.sh
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,6 @@ for i in "${!benchmark_names[@]}"; do
iteration_pass_data=()
iteration_memory=()
binary_size=0

for ((iter=1; iter<=ITERATIONS; iter++)); do
output_binary="$TEMP_DIR/bench_output_$$"

Expand Down Expand Up @@ -220,6 +219,11 @@ for i in "${!benchmark_names[@]}"; do
fi
fi

# Capture binary size from first successful iteration
if [[ $binary_size -eq 0 && -f "$output_binary" ]]; then
binary_size=$(stat -f%z "$output_binary" 2>/dev/null || stat -c%s "$output_binary" 2>/dev/null || echo 0)
fi

rm -f "$output_binary"
done

Expand Down Expand Up @@ -274,36 +278,60 @@ for i in "${!benchmark_names[@]}"; do

log_info " $name: time=${mean}ms (±${stddev}), mem=${mem_mean_mb}MB, binary=${binary_size_kb}KB (n=$count)"

# Extract and aggregate per-pass timing data
# Extract and aggregate per-pass timing data, source metrics, and memory
# Use Python to parse JSON and compute per-pass means
pass_json=$(python3 -c "
extra_json=$(python3 -c "
import json
import sys

pass_data = {}
source_metrics = None
peak_memory_samples = []

for json_str in sys.argv[1:]:
try:
data = json.loads(json_str)
for p in data.get('passes', []):
name = p['name']
pname = p['name']
duration = p['duration_ms']
if name not in pass_data:
pass_data[name] = []
pass_data[name].append(duration)
if pname not in pass_data:
pass_data[pname] = []
pass_data[pname].append(duration)
# Get source_metrics from first run (they're constant)
if source_metrics is None and 'source_metrics' in data:
source_metrics = data['source_metrics']
# Collect peak memory samples
if 'peak_memory_bytes' in data and data['peak_memory_bytes']:
peak_memory_samples.append(data['peak_memory_bytes'])
except:
pass

# Calculate means
result = {}
for name, durations in pass_data.items():
# Calculate means for passes
passes = {}
for pname, durations in pass_data.items():
mean = sum(durations) / len(durations) if durations else 0
result[name] = {'mean_ms': round(mean, 3)}
passes[pname] = {'mean_ms': round(mean, 3)}

result = {'passes': passes}
if source_metrics:
result['source_metrics'] = source_metrics
if peak_memory_samples:
result['peak_memory_bytes'] = int(sum(peak_memory_samples) / len(peak_memory_samples))

print(json.dumps(result))
" "${iteration_pass_data[@]}" 2>/dev/null || echo "{}")
" "${iteration_pass_data[@]}" 2>/dev/null || echo "{\"passes\":{}}")

# Extract components from the JSON
passes_json=$(echo "$extra_json" | python3 -c "import sys, json; d=json.load(sys.stdin); print(json.dumps(d.get('passes', {})))")
source_metrics_json=$(echo "$extra_json" | python3 -c "import sys, json; d=json.load(sys.stdin); sm=d.get('source_metrics'); print(json.dumps(sm) if sm else 'null')")

# Store result with all data (including memory and binary size from iteration tracking)
result_parts=("\"name\":\"$name\"" "\"iterations\":$count" "\"mean_ms\":$mean" "\"std_ms\":$stddev" "\"passes\":$passes_json")
[[ "$source_metrics_json" != "null" ]] && result_parts+=("\"source_metrics\":$source_metrics_json")
[[ "$mem_mean" -gt 0 ]] && result_parts+=("\"peak_memory_bytes\":$mem_mean")
[[ "$binary_size" -gt 0 ]] && result_parts+=("\"binary_size_bytes\":$binary_size")

# Store result with pass data and new metrics
all_results+=("{\"name\":\"$name\",\"iterations\":$count,\"mean_ms\":$mean,\"std_ms\":$stddev,\"peak_memory_bytes\":$mem_mean,\"memory_std_bytes\":$mem_stddev,\"binary_size_bytes\":$binary_size,\"passes\":$pass_json}")
all_results+=("{$(IFS=,; echo "${result_parts[*]}")}")
done

# Get metadata
Expand Down
2 changes: 2 additions & 0 deletions crates/rue/BUCK
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ rust_binary(
"//crates/rue-compiler:rue-compiler",
"//crates/rue-rir:rue-rir",
"//crates/rue-target:rue-target",
"//third-party:libc",
"//third-party:serde",
"//third-party:serde_json",
"//third-party:tracing",
Expand All @@ -26,6 +27,7 @@ rust_test(
"//crates/rue-compiler:rue-compiler",
"//crates/rue-rir:rue-rir",
"//crates/rue-target:rue-target",
"//third-party:libc",
"//third-party:serde",
"//third-party:serde_json",
"//third-party:tracing",
Expand Down
79 changes: 77 additions & 2 deletions crates/rue/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -586,19 +586,75 @@ fn print_timing_output(
time_passes: bool,
benchmark_json: bool,
target: &Target,
source_metrics: Option<timing::SourceMetrics>,
) {
if let Some(timing) = timing_data {
if benchmark_json {
// JSON output goes to stdout for easy capture
// Include metadata for historical analysis
println!("{}", timing.to_json(&target.to_string(), VERSION));
// Include metadata and source metrics for historical analysis
println!(
"{}",
timing.to_json_with_metrics(
&target.to_string(),
VERSION,
source_metrics,
get_peak_memory_bytes(),
)
);
} else if time_passes {
// Human-readable output goes to stderr
eprintln!("{}", timing.report());
}
}
}

/// Get peak memory usage in bytes (platform-specific).
///
/// Returns None if memory usage cannot be determined.
fn get_peak_memory_bytes() -> Option<u64> {
#[cfg(target_os = "linux")]
{
// On Linux, read from /proc/self/status
if let Ok(status) = fs::read_to_string("/proc/self/status") {
for line in status.lines() {
if line.starts_with("VmHWM:") {
// VmHWM is "high water mark" - peak resident set size
// Format: "VmHWM: 12345 kB"
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 2 {
if let Ok(kb) = parts[1].parse::<u64>() {
return Some(kb * 1024);
}
}
}
}
}
None
}

#[cfg(target_os = "macos")]
{
// On macOS, use rusage
use std::mem::MaybeUninit;
let mut rusage = MaybeUninit::uninit();
// SAFETY: rusage is properly aligned and getrusage is a standard POSIX call
let result = unsafe { libc::getrusage(libc::RUSAGE_SELF, rusage.as_mut_ptr()) };
if result == 0 {
// SAFETY: getrusage succeeded, so rusage is initialized
let rusage = unsafe { rusage.assume_init() };
// ru_maxrss is in bytes on macOS (unlike Linux where it's in KB)
Some(rusage.ru_maxrss as u64)
} else {
None
}
}

#[cfg(not(any(target_os = "linux", target_os = "macos")))]
{
None
}
}

fn main() {
let options = match parse_args() {
Some(opts) => opts,
Expand All @@ -624,6 +680,23 @@ fn main() {
let source_info = SourceInfo::new(&source, &options.source_path);
let formatter = DiagnosticFormatter::new(&source_info);

// Compute source metrics if benchmark JSON is requested
let source_metrics = if options.benchmark_json {
// We need token count, so do a quick lex
let lexer = Lexer::new(&source);
let token_count = match lexer.tokenize() {
Ok((tokens, _interner)) => tokens.len(),
Err(_) => 0, // If lexing fails, we'll get the error during compilation anyway
};
Some(timing::SourceMetrics {
bytes: source.len(),
lines: source.lines().count(),
tokens: token_count,
})
} else {
None
};

// Handle emit modes
if !options.emit_stages.is_empty() {
if let Err(()) = handle_emit(&source, &options, &formatter) {
Expand All @@ -634,6 +707,7 @@ fn main() {
options.time_passes,
options.benchmark_json,
&options.target,
source_metrics,
);
return;
}
Expand Down Expand Up @@ -700,6 +774,7 @@ fn main() {
options.time_passes,
options.benchmark_json,
&options.target,
source_metrics,
);
}
Err(errors) => {
Expand Down
59 changes: 59 additions & 0 deletions crates/rue/src/timing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,23 @@ pub struct BenchmarkTiming {
pub passes: Vec<PassTiming>,
/// Total compilation time in milliseconds.
pub total_ms: f64,
/// Source code metrics (lines, bytes, tokens).
#[serde(skip_serializing_if = "Option::is_none")]
pub source_metrics: Option<SourceMetrics>,
/// Peak memory usage in bytes (if available).
#[serde(skip_serializing_if = "Option::is_none")]
pub peak_memory_bytes: Option<u64>,
}

/// Source code metrics for throughput calculations.
#[derive(Debug, Clone, Serialize)]
pub struct SourceMetrics {
/// Number of bytes in the source file.
pub bytes: usize,
/// Number of lines in the source file.
pub lines: usize,
/// Number of tokens produced by the lexer.
pub tokens: usize,
}

/// Metadata about a benchmark run for historical analysis.
Expand Down Expand Up @@ -196,6 +213,23 @@ impl TimingData {
/// * `target` - The target platform string (e.g., "x86_64-linux")
/// * `version` - The compiler version string
pub fn to_benchmark_timing(&self, target: &str, version: &str) -> BenchmarkTiming {
self.to_benchmark_timing_with_metrics(target, version, None, None)
}

/// Generate structured timing data with optional source metrics and memory usage.
///
/// # Arguments
/// * `target` - The target platform string (e.g., "x86_64-linux")
/// * `version` - The compiler version string
/// * `source_metrics` - Optional source code metrics (bytes, lines, tokens)
/// * `peak_memory_bytes` - Optional peak memory usage in bytes
pub fn to_benchmark_timing_with_metrics(
&self,
target: &str,
version: &str,
source_metrics: Option<SourceMetrics>,
peak_memory_bytes: Option<u64>,
) -> BenchmarkTiming {
let inner = self.inner.lock().unwrap();

let total: Duration = inner.passes.values().sum();
Expand Down Expand Up @@ -231,6 +265,8 @@ impl TimingData {
metadata,
passes,
total_ms,
source_metrics,
peak_memory_bytes,
}
}

Expand Down Expand Up @@ -262,6 +298,29 @@ impl TimingData {
serde_json::to_string(&timing).unwrap_or_else(|_| "{}".to_string())
}

/// Generate JSON output with additional source metrics.
///
/// # Arguments
/// * `target` - The target platform string
/// * `version` - The compiler version string
/// * `source_metrics` - Source code metrics (bytes, lines, tokens)
/// * `peak_memory_bytes` - Optional peak memory usage
pub fn to_json_with_metrics(
&self,
target: &str,
version: &str,
source_metrics: Option<SourceMetrics>,
peak_memory_bytes: Option<u64>,
) -> String {
let timing = self.to_benchmark_timing_with_metrics(
target,
version,
source_metrics,
peak_memory_bytes,
);
serde_json::to_string(&timing).unwrap_or_else(|_| "{}".to_string())
}

/// Generate pretty-printed JSON output for benchmark timing.
///
/// Same as `to_json()` but with indentation for human readability.
Expand Down
Loading