diff options
Diffstat (limited to 'zjit/src/options.rs')
| -rw-r--r-- | zjit/src/options.rs | 420 |
1 files changed, 369 insertions, 51 deletions
diff --git a/zjit/src/options.rs b/zjit/src/options.rs index 94a6988a4f..5ddaee1951 100644 --- a/zjit/src/options.rs +++ b/zjit/src/options.rs @@ -1,20 +1,41 @@ -use std::{ffi::{CStr, CString}, ptr::null}; +//! Configurable options for ZJIT. + +use std::{ffi::{CStr, CString}, fs::File, ptr::null}; use std::os::raw::{c_char, c_int, c_uint}; use crate::cruby::*; +use crate::stats::Counter; use std::collections::HashSet; +/// Type of symbols to dump into /tmp/perf-{pid}.map +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum PerfMap { + /// Dump one symbol per ISEQ + ISEQ, + /// Dump one symbol per HIR instruction + HIR, +} + +/// Default --zjit-num-profiles +const DEFAULT_NUM_PROFILES: NumProfiles = 5; +pub type NumProfiles = u16; + +/// Default --zjit-call-threshold. This should be large enough to avoid compiling +/// warmup code, but small enough to perform well on micro-benchmarks. +pub const DEFAULT_CALL_THRESHOLD: CallThreshold = 30; +pub type CallThreshold = u64; + /// Number of calls to start profiling YARV instructions. /// They are profiled `rb_zjit_call_threshold - rb_zjit_profile_threshold` times, /// which is equal to --zjit-num-profiles. #[unsafe(no_mangle)] #[allow(non_upper_case_globals)] -pub static mut rb_zjit_profile_threshold: u64 = 1; +pub static mut rb_zjit_profile_threshold: CallThreshold = DEFAULT_CALL_THRESHOLD - DEFAULT_NUM_PROFILES as CallThreshold; /// Number of calls to compile ISEQ with ZJIT at jit_compile() in vm.c. /// --zjit-call-threshold=1 compiles on first execution without profiling information. #[unsafe(no_mangle)] #[allow(non_upper_case_globals)] -pub static mut rb_zjit_call_threshold: u64 = 2; +pub static mut rb_zjit_call_threshold: CallThreshold = DEFAULT_CALL_THRESHOLD; /// ZJIT command-line options. This is set before rb_zjit_init() sets /// ZJITState so that we can query some options while loading builtins. @@ -26,15 +47,29 @@ pub struct Options { /// Note that the command line argument is expressed in MiB and not bytes. pub exec_mem_bytes: usize, + /// Hard limit of ZJIT's total memory usage. + /// Note that the command line argument is expressed in MiB and not bytes. + pub mem_bytes: usize, + /// Number of times YARV instructions should be profiled. - pub num_profiles: u8, + pub num_profiles: NumProfiles, - /// Enable YJIT statsitics + /// Enable ZJIT statistics pub stats: bool, + /// Print stats on exit (when stats is also true) + pub print_stats: bool, + + /// Print stats to file on exit (when stats is also true) + pub print_stats_file: Option<std::path::PathBuf>, + /// Enable debug logging pub debug: bool, + // Whether to enable JIT at boot. This option prevents other + // ZJIT tuning options from enabling ZJIT at boot. + pub disable: bool, + /// Turn off the HIR optimizer pub disable_hir_opt: bool, @@ -44,40 +79,69 @@ pub struct Options { /// Dump High-level IR after optimization, right before codegen. pub dump_hir_opt: Option<DumpHIR>, - pub dump_hir_graphviz: bool, + /// Dump High-level IR to the given file in Graphviz format after optimization + pub dump_hir_graphviz: Option<std::path::PathBuf>, + + /// Dump High-level IR in Iongraph JSON format after optimization to /tmp/zjit-iongraph-{$PID} + pub dump_hir_iongraph: bool, /// Dump low-level IR - pub dump_lir: bool, + pub dump_lir: Option<HashSet<DumpLIR>>, /// Dump all compiled machine code. - pub dump_disasm: bool, + pub dump_disasm: Option<DumpDisasm>, + + /// Trace and write side exit source maps to /tmp for stackprof. + pub trace_side_exits: Option<TraceExits>, + + /// Frequency of tracing side exits. + pub trace_side_exits_sample_interval: usize, + + /// Trace compilation phases as Perfetto duration events. + pub trace_compiles: bool, + + /// Trace invalidation events as Perfetto duration events. + pub trace_invalidation: bool, /// Dump code map to /tmp for performance profilers. - pub perf: bool, + pub perf: Option<PerfMap>, /// List of ISEQs that can be compiled, identified by their iseq_get_location() pub allowed_iseqs: Option<HashSet<String>>, /// Path to a file where compiled ISEQs will be saved. - pub log_compiled_iseqs: Option<String>, + pub log_compiled_iseqs: Option<std::path::PathBuf>, + + /// Maximum number of versions per ISEQ + pub max_versions: usize, } impl Default for Options { fn default() -> Self { Options { exec_mem_bytes: 64 * 1024 * 1024, - num_profiles: 1, + mem_bytes: 128 * 1024 * 1024, + num_profiles: DEFAULT_NUM_PROFILES, stats: false, + print_stats: false, + print_stats_file: None, debug: false, + disable: false, disable_hir_opt: false, dump_hir_init: None, dump_hir_opt: None, - dump_hir_graphviz: false, - dump_lir: false, - dump_disasm: false, - perf: false, + dump_hir_graphviz: None, + dump_hir_iongraph: false, + dump_lir: None, + dump_disasm: None, + trace_side_exits: None, + trace_side_exits_sample_interval: 0, + trace_compiles: false, + trace_invalidation: false, + perf: None, allowed_iseqs: None, log_compiled_iseqs: None, + max_versions: 2, } } } @@ -85,20 +149,41 @@ impl Default for Options { /// `ruby --help` descriptions for user-facing options. Do not add options for ZJIT developers. /// Note that --help allows only 80 chars per line, including indentation, and it also puts the /// description in a separate line if the option name is too long. 80-char limit --> | (any character beyond this `|` column fails the test) -pub const ZJIT_OPTIONS: &'static [(&str, &str)] = &[ - // TODO: Hide --zjit-exec-mem-size from ZJIT_OPTIONS once we add --zjit-mem-size (Shopify/ruby#686) - ("--zjit-exec-mem-size=num", - "Size of executable memory block in MiB (default: 64)."), +pub const ZJIT_OPTIONS: &[(&str, &str)] = &[ + ("--zjit-mem-size=num", + "Max amount of memory that ZJIT can use in MiB (default: 128)."), ("--zjit-call-threshold=num", - "Number of calls to trigger JIT (default: 2)."), + "Number of calls to trigger JIT (default: 30)."), ("--zjit-num-profiles=num", - "Number of profiled calls before JIT (default: 1, max: 255)."), - ("--zjit-stats", "Enable collecting ZJIT statistics."), - ("--zjit-perf", "Dump ISEQ symbols into /tmp/perf-{}.map for Linux perf."), + "Number of profiled calls before JIT (default: 5)."), + ("--zjit-stats-quiet", + "Collect ZJIT stats and suppress output."), + ("--zjit-stats[=file]", + "Collect ZJIT stats (=file to write to a file)."), + ("--zjit-disable", + "Disable ZJIT for lazily enabling it with RubyVM::ZJIT.enable."), + ("--zjit-perf[=iseq|hir]", + "Dump symbols for Linux perf /tmp/perf-{}.map (default: iseq)."), ("--zjit-log-compiled-iseqs=path", "Log compiled ISEQs to the file. The file will be truncated."), + ("--zjit-trace-exits[=counter]", + "Record source on side-exit. `Counter` picks specific counter."), + ("--zjit-trace-exits-sample-rate=num", + "Frequency at which to record side exits. Must be `usize`."), + ("--zjit-trace-compiles", + "Record compilation phases as Perfetto trace events."), + ("--zjit-trace-invalidation", + "Record invalidation events as Perfetto trace events."), ]; +#[derive(Copy, Clone, Debug)] +pub enum TraceExits { + // Trace all exits + All, + // Trace exits for a specific `Counter` + Counter(Counter), +} + #[derive(Clone, Copy, Debug)] pub enum DumpHIR { // Dump High-level IR without Snapshot @@ -109,6 +194,59 @@ pub enum DumpHIR { Debug, } +/// --zjit-dump-lir values. Using snake_case to stringify the exact filter value. +#[allow(non_camel_case_types)] +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] +pub enum DumpLIR { + /// Dump the initial LIR + init, + /// Dump LIR after {arch}_split + split, + /// Dump LIR after alloc_regs + alloc_regs, + /// Dump LIR after compile_exits + compile_exits, + /// Dump LIR after resolve_parallel_mov + resolve_parallel_mov, + /// Dump LIR after {arch}_scratch_split + scratch_split, + /// Dump live intervals grid before alloc_regs + live_intervals, +} + +#[derive(Clone, Copy, Debug)] +pub enum DumpDisasm { + Stdout, + File(std::os::unix::io::RawFd), +} + +/// All compiler stages for --zjit-dump-lir=all. +const DUMP_LIR_ALL: &[DumpLIR] = &[ + DumpLIR::init, + DumpLIR::split, + DumpLIR::alloc_regs, + DumpLIR::compile_exits, + DumpLIR::resolve_parallel_mov, + DumpLIR::scratch_split, + DumpLIR::live_intervals, +]; + +/// Maximum value for --zjit-mem-size/--zjit-exec-mem-size in MiB. +/// We set 1TiB just to avoid overflow. We could make it smaller. +const MAX_MEM_MIB: usize = 1024 * 1024; + +/// Macro to dump LIR if --zjit-dump-lir is specified +macro_rules! asm_dump { + ($asm:expr, $target:ident) => { + if let Some(crate::options::Options { dump_lir: Some(dump_lirs), .. }) = unsafe { crate::options::OPTIONS.as_ref() } { + if dump_lirs.contains(&crate::options::DumpLIR::$target) { + println!("LIR {}:\n{}", stringify!($target), $asm); + } + } + }; +} +pub(crate) use asm_dump; + /// Macro to get an option value by name macro_rules! get_option { // Unsafe is ok here because options are initialized @@ -119,6 +257,14 @@ macro_rules! get_option { } pub(crate) use get_option; +/// Macro to reference an option value by name. +macro_rules! get_option_ref { + ($option_name:ident) => { + unsafe { crate::options::OPTIONS.as_ref() }.unwrap().$option_name.as_ref() + }; +} +pub(crate) use get_option_ref; + /// Set default values to ZJIT options. Setting Some to OPTIONS will make `#with_jit` /// enable the JIT hook while not enabling compilation yet. #[unsafe(no_mangle)] @@ -147,11 +293,11 @@ fn parse_jit_list(path_like: &str) -> HashSet<String> { } } } else { - eprintln!("Failed to read JIT list from '{}'", path_like); + eprintln!("Failed to read JIT list from '{path_like}'"); } eprintln!("JIT list:"); for item in &result { - eprintln!(" {}", item); + eprintln!(" {item}"); } result } @@ -179,17 +325,19 @@ fn parse_option(str_ptr: *const std::os::raw::c_char) -> Option<()> { ("", "") => {}, // Simply --zjit ("mem-size", _) => match opt_val.parse::<usize>() { - Ok(n) => { - // Reject 0 or too large values that could overflow. - // The upper bound is 1 TiB but we could make it smaller. - if n == 0 || n > 1024 * 1024 { - return None - } + Ok(n) if (1..=MAX_MEM_MIB).contains(&n) => { + // Convert from MiB to bytes internally for convenience + options.mem_bytes = n * 1024 * 1024; + } + _ => return None, + }, + ("exec-mem-size", _) => match opt_val.parse::<usize>() { + Ok(n) if (1..=MAX_MEM_MIB).contains(&n) => { // Convert from MiB to bytes internally for convenience options.exec_mem_bytes = n * 1024 * 1024; } - Err(_) => return None, + _ => return None, }, ("call-threshold", _) => match opt_val.parse() { @@ -208,41 +356,151 @@ fn parse_option(str_ptr: *const std::os::raw::c_char) -> Option<()> { Err(_) => return None, }, + ("max-versions", _) => match opt_val.parse() { + Ok(n) => options.max_versions = n, + Err(_) => return None, + }, + + ("stats-quiet", _) => { + options.stats = true; + options.print_stats = false; + } + ("stats", "") => { options.stats = true; + options.print_stats = true; + } + ("stats", path) => { + // Truncate the file if it exists + std::fs::OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(path) + .map_err(|e| eprintln!("Failed to open file '{}': {}", path, e)) + .ok(); + let canonical_path = std::fs::canonicalize(opt_val).unwrap_or_else(|_| opt_val.into()); + options.stats = true; + options.print_stats_file = Some(canonical_path); } + ("trace-exits", exits) => { + options.trace_side_exits = match exits { + "" => Some(TraceExits::All), + name => Some(Counter::get(name).map(TraceExits::Counter)?), + } + } + + ("trace-exits-sample-rate", sample_interval) => { + // If not already set, then set it to `TraceExits::All` by default. + if options.trace_side_exits.is_none() { + options.trace_side_exits = Some(TraceExits::All); + } + // `sample_interval ` must provide a string that can be validly parsed to a `usize`. + options.trace_side_exits_sample_interval = sample_interval.parse::<usize>().ok()?; + } + + ("trace-compiles", "") => options.trace_compiles = true, + + ("trace-invalidation", "") => options.trace_invalidation = true, + ("debug", "") => options.debug = true, + ("disable", "") => options.disable = true, + ("disable-hir-opt", "") => options.disable_hir_opt = true, // --zjit-dump-hir dumps the actual input to the codegen, which is currently the same as --zjit-dump-hir-opt. ("dump-hir" | "dump-hir-opt", "") => options.dump_hir_opt = Some(DumpHIR::WithoutSnapshot), ("dump-hir" | "dump-hir-opt", "all") => options.dump_hir_opt = Some(DumpHIR::All), ("dump-hir" | "dump-hir-opt", "debug") => options.dump_hir_opt = Some(DumpHIR::Debug), - ("dump-hir-graphviz", "") => options.dump_hir_graphviz = true, ("dump-hir-init", "") => options.dump_hir_init = Some(DumpHIR::WithoutSnapshot), ("dump-hir-init", "all") => options.dump_hir_init = Some(DumpHIR::All), ("dump-hir-init", "debug") => options.dump_hir_init = Some(DumpHIR::Debug), - ("dump-lir", "") => options.dump_lir = true, + ("dump-hir-graphviz", "") => options.dump_hir_graphviz = Some("/dev/stderr".into()), + ("dump-hir-graphviz", _) => { + // Truncate the file if it exists + std::fs::OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(opt_val) + .map_err(|e| eprintln!("Failed to open file '{opt_val}': {e}")) + .ok(); + let opt_val = std::fs::canonicalize(opt_val).unwrap_or_else(|_| opt_val.into()); + options.dump_hir_graphviz = Some(opt_val); + } + + ("dump-hir-iongraph", "") => options.dump_hir_iongraph = true, + + ("dump-lir", "") => options.dump_lir = Some(HashSet::from([DumpLIR::init])), + ("dump-lir", filters) => { + let mut dump_lirs = HashSet::new(); + for filter in filters.split(',') { + let dump_lir = match filter { + "all" => { + for &dump_lir in DUMP_LIR_ALL { + dump_lirs.insert(dump_lir); + } + continue; + } + "init" => DumpLIR::init, + "split" => DumpLIR::split, + "alloc_regs" => DumpLIR::alloc_regs, + "compile_exits" => DumpLIR::compile_exits, + "resolve_parallel_mov" => DumpLIR::resolve_parallel_mov, + "scratch_split" => DumpLIR::scratch_split, + "live_intervals" => DumpLIR::live_intervals, + _ => { + let valid_options = DUMP_LIR_ALL.iter().map(|opt| format!("{opt:?}")).collect::<Vec<_>>().join(", "); + eprintln!("invalid --zjit-dump-lir option: '{filter}'"); + eprintln!("valid --zjit-dump-lir options: all, {valid_options}"); + return None; + } + }; + dump_lirs.insert(dump_lir); + } + options.dump_lir = Some(dump_lirs); + } + + ("dump-disasm", _) => { + if !cfg!(feature = "disasm") { + eprintln!("WARNING: the {opt_name} option works best when ZJIT is built in dev mode, i.e. ./configure --enable-zjit=dev"); + } - ("dump-disasm", "") => options.dump_disasm = true, + match opt_val { + "" => options.dump_disasm = Some(DumpDisasm::Stdout), + directory => { + let path = format!("{directory}/zjit_{}.log", std::process::id()); + match File::options().create(true).append(true).open(&path) { + Ok(file) => { + use std::os::unix::io::IntoRawFd; + eprintln!("ZJIT disasm dump: {path}"); + options.dump_disasm = Some(DumpDisasm::File(file.into_raw_fd())); + } + Err(err) => eprintln!("Failed to create {path}: {err}"), + } + } + } + } - ("perf", "") => options.perf = true, + ("perf", "" | "iseq") => options.perf = Some(PerfMap::ISEQ), + ("perf", "hir") => options.perf = Some(PerfMap::HIR), - ("allowed-iseqs", _) if opt_val != "" => options.allowed_iseqs = Some(parse_jit_list(opt_val)), - ("log-compiled-iseqs", _) if opt_val != "" => { + ("allowed-iseqs", _) if !opt_val.is_empty() => options.allowed_iseqs = Some(parse_jit_list(opt_val)), + ("log-compiled-iseqs", _) if !opt_val.is_empty() => { // Truncate the file if it exists std::fs::OpenOptions::new() .create(true) .write(true) .truncate(true) .open(opt_val) - .map_err(|e| eprintln!("Failed to open file '{}': {}", opt_val, e)) + .map_err(|e| eprintln!("Failed to open file '{opt_val}': {e}")) .ok(); - options.log_compiled_iseqs = Some(opt_val.into()); + let opt_val = std::fs::canonicalize(opt_val).unwrap_or_else(|_| opt_val.into()); + options.log_compiled_iseqs = Some(opt_val); } _ => return None, // Option name not recognized @@ -259,12 +517,27 @@ fn update_profile_threshold() { unsafe { rb_zjit_profile_threshold = 0; } } else { // Otherwise, profile instructions at least once. - let num_profiles = get_option!(num_profiles) as u64; - unsafe { rb_zjit_profile_threshold = rb_zjit_call_threshold.saturating_sub(num_profiles).max(1) }; + let num_profiles = get_option!(num_profiles); + unsafe { rb_zjit_profile_threshold = rb_zjit_call_threshold.saturating_sub(num_profiles.into()).max(1) }; } } -/// Print YJIT options for `ruby --help`. `width` is width of option parts, and +/// Update --zjit-call-threshold for testing +#[cfg(test)] +pub fn set_call_threshold(call_threshold: CallThreshold) { + unsafe { rb_zjit_call_threshold = call_threshold; } + rb_zjit_prepare_options(); + update_profile_threshold(); +} + +/// Enable --zjit-stats for testing +#[cfg(test)] +pub fn enable_zjit_stats() { + rb_zjit_prepare_options(); + unsafe { OPTIONS.as_mut() }.unwrap().stats = true; +} + +/// Print ZJIT options for `ruby --help`. `width` is width of option parts, and /// `columns` is indent width of descriptions. #[unsafe(no_mangle)] pub extern "C" fn rb_zjit_show_usage(help: c_int, highlight: c_int, width: c_uint, columns: c_int) { @@ -289,15 +562,13 @@ macro_rules! debug { } pub(crate) use debug; -/// Return Qtrue if --zjit* has been specified. For the `#with_jit` hook, -/// this becomes Qtrue before ZJIT is actually initialized and enabled. +/// Return true if ZJIT should be enabled at boot. #[unsafe(no_mangle)] -pub extern "C" fn rb_zjit_option_enabled_p(_ec: EcPtr, _self: VALUE) -> VALUE { - // If any --zjit* option is specified, OPTIONS becomes Some. - if unsafe { OPTIONS.is_some() } { - Qtrue +pub extern "C" fn rb_zjit_option_enable() -> bool { + if unsafe { OPTIONS.as_ref() }.is_some_and(|opts| !opts.disable) { + true } else { - Qfalse + false } } @@ -305,9 +576,56 @@ pub extern "C" fn rb_zjit_option_enabled_p(_ec: EcPtr, _self: VALUE) -> VALUE { #[unsafe(no_mangle)] pub extern "C" fn rb_zjit_stats_enabled_p(_ec: EcPtr, _self: VALUE) -> VALUE { // Builtin zjit.rb calls this even if ZJIT is disabled, so OPTIONS may not be set. - if unsafe { OPTIONS.as_ref() }.map_or(false, |opts| opts.stats) { + if unsafe { OPTIONS.as_ref() }.is_some_and(|opts| opts.stats) { + Qtrue + } else { + Qfalse + } +} + +/// Return Qtrue if stats should be printed at exit. +#[unsafe(no_mangle)] +pub extern "C" fn rb_zjit_print_stats_p(_ec: EcPtr, _self: VALUE) -> VALUE { + // Builtin zjit.rb calls this even if ZJIT is disabled, so OPTIONS may not be set. + if unsafe { OPTIONS.as_ref() }.is_some_and(|opts| opts.stats && opts.print_stats) { Qtrue } else { Qfalse } } + +/// Return path if stats should be printed at exit to a specified file, else Qnil. +#[unsafe(no_mangle)] +pub extern "C" fn rb_zjit_get_stats_file_path_p(_ec: EcPtr, _self: VALUE) -> VALUE { + if let Some(opts) = unsafe { OPTIONS.as_ref() } { + if let Some(ref path) = opts.print_stats_file { + return rust_str_to_ruby(path.as_os_str().to_str().unwrap()); + } + } + Qnil +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_dump_disasm_path() { + unsafe { OPTIONS = Some(Options::default()); } + + let dir = std::env::temp_dir(); + let expected_path = dir.join(format!("zjit_{}.log", std::process::id())); + let option = CString::new(format!("dump-disasm={}", dir.display())).unwrap(); + + assert!(parse_option(option.as_ptr()).is_some()); + + let options = unsafe { OPTIONS.as_ref() }.unwrap(); + match options.dump_disasm { + Some(DumpDisasm::File(fd)) => assert!(fd >= 0), + _ => panic!("expected dump-disasm file output"), + } + assert!(expected_path.exists()); + + let _ = std::fs::remove_file(expected_path); + } +} |
