summaryrefslogtreecommitdiff
path: root/zjit/src/options.rs
diff options
context:
space:
mode:
Diffstat (limited to 'zjit/src/options.rs')
-rw-r--r--zjit/src/options.rs420
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);
+ }
+}