Skip to content

Commit 158b8cb

Browse files
nirvdrumrwstaunermaximecb
authored
YJIT: Add compilation log (#11818)
* YJIT: Add `--yjit-compilation-log` flag to print out the compilation log at exit. * YJIT: Add an option to enable the compilation log at runtime. * YJIT: Fix a typo in the `IseqPayload` docs. * YJIT: Add stubs for getting the YJIT compilation log in memory. * YJIT: Add a compilation log based on a circular buffer to cap the log size. * YJIT: Allow specifying either a file or directory name for the YJIT compilation log. The compilation log will be populated as compilation events occur. If a directory is supplied, then a filename based on the PID will be used as the write target. If a file name is supplied instead, the log will be written to that file. * YJIT: Add JIT compilation of C function substitutions to the compilation log. * YJIT: Add compilation events to the circular buffer even if output is sent to a file. Previously, the two modes were treated as being exclusive of one another. However, it could be beneficial to log all events to a file while also allowing for direct access of the last N events via `RubyVM::YJIT.compilation_log`. * YJIT: Make timestamps the first element in the YJIT compilation log tuple. * YJIT: Stream log to stderr if `--yjit-compilation-log` is supplied without an argument. * YJIT: Eagerly compute compilation log messages to avoid hanging on to references that may GC. * YJIT: Log all compiled blocks, not just the method entry points. * YJIT: Remove all compilation events other than block compilation to slim down the log. * YJIT: Replace circular buffer iterator with a consuming loop. * YJIT: Support `--yjit-compilation-log=quiet` as a way to activate the in-memory log without printing it. Co-authored-by: Randy Stauner <randy.stauner@shopify.com> * YJIT: Promote the compilation log to being the one YJIT log. Co-authored-by: Randy Stauner <randy.stauner@shopify.com> * Update doc/yjit/yjit.md * Update doc/yjit/yjit.md --------- Co-authored-by: Randy Stauner <randy.stauner@shopify.com> Co-authored-by: Maxime Chevalier-Boisvert <maximechevalierb@gmail.com>
1 parent c78e298 commit 158b8cb

File tree

10 files changed

+299
-7
lines changed

10 files changed

+299
-7
lines changed

doc/yjit/yjit.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,12 @@ You can dump statistics about compilation and execution by running YJIT with the
166166
./miniruby --yjit-stats myscript.rb
167167
```
168168

169+
You can see what YJIT has compiled by running YJIT with the `--yjit-log` command-line option:
170+
171+
```sh
172+
./miniruby --yjit-log myscript.rb
173+
```
174+
169175
The machine code generated for a given method can be printed by adding `puts RubyVM::YJIT.disasm(method(:method_name))` to a Ruby script. Note that no code will be generated if the method is not compiled.
170176

171177
<h3 id="command-line-options">Command-Line Options</h3>
@@ -181,6 +187,8 @@ YJIT supports all command-line options supported by upstream CRuby, but also add
181187
compiled, lower values mean less code is compiled (default 200K)
182188
- `--yjit-stats`: print statistics after the execution of a program (incurs a run-time cost)
183189
- `--yjit-stats=quiet`: gather statistics while running a program but don't print them. Stats are accessible through `RubyVM::YJIT.runtime_stats`. (incurs a run-time cost)
190+
- `--yjit-log[=file|dir]`: log all compilation events to the specified file or directory. If no name is supplied, the last 1024 log entries will be printed to stderr when the application exits.
191+
- `--yjit-log=quiet`: gather a circular buffer of recent YJIT compilations. The compilation log entries are accessible through `RubyVM::YJIT.log` and old entries will be discarded if the buffer is not drained quickly. (incurs a run-time cost)
184192
- `--yjit-disable`: disable YJIT despite other `--yjit*` flags for lazily enabling it with `RubyVM::YJIT.enable`
185193
- `--yjit-code-gc`: enable code GC (disabled by default as of Ruby 3.3).
186194
It will cause all machine code to be discarded when the executable memory size limit is hit, meaning JIT compilation will then start over.

yjit.c

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1226,15 +1226,18 @@ rb_yjit_set_exception_return(rb_control_frame_t *cfp, void *leave_exit, void *le
12261226
// Primitives used by yjit.rb
12271227
VALUE rb_yjit_stats_enabled_p(rb_execution_context_t *ec, VALUE self);
12281228
VALUE rb_yjit_print_stats_p(rb_execution_context_t *ec, VALUE self);
1229+
VALUE rb_yjit_log_enabled_p(rb_execution_context_t *c, VALUE self);
1230+
VALUE rb_yjit_print_log_p(rb_execution_context_t *c, VALUE self);
12291231
VALUE rb_yjit_trace_exit_locations_enabled_p(rb_execution_context_t *ec, VALUE self);
12301232
VALUE rb_yjit_get_stats(rb_execution_context_t *ec, VALUE self, VALUE key);
12311233
VALUE rb_yjit_reset_stats_bang(rb_execution_context_t *ec, VALUE self);
1234+
VALUE rb_yjit_get_log(rb_execution_context_t *ec, VALUE self);
12321235
VALUE rb_yjit_disasm_iseq(rb_execution_context_t *ec, VALUE self, VALUE iseq);
12331236
VALUE rb_yjit_insns_compiled(rb_execution_context_t *ec, VALUE self, VALUE iseq);
12341237
VALUE rb_yjit_code_gc(rb_execution_context_t *ec, VALUE self);
12351238
VALUE rb_yjit_simulate_oom_bang(rb_execution_context_t *ec, VALUE self);
12361239
VALUE rb_yjit_get_exit_locations(rb_execution_context_t *ec, VALUE self);
1237-
VALUE rb_yjit_enable(rb_execution_context_t *ec, VALUE self, VALUE gen_stats, VALUE print_stats);
1240+
VALUE rb_yjit_enable(rb_execution_context_t *ec, VALUE self, VALUE gen_stats, VALUE print_stats, VALUE gen_compilation_log, VALUE print_compilation_log);
12381241

12391242
// Preprocessed yjit.rb generated during build
12401243
#include "yjit.rbinc"

yjit.rb

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ def self.stats_enabled?
1818
Primitive.rb_yjit_stats_enabled_p
1919
end
2020

21+
# Check if `--yjit-log` is used.
22+
def self.log_enabled?
23+
Primitive.rb_yjit_log_enabled_p
24+
end
25+
2126
# Check if rb_yjit_trace_exit_locations_enabled_p is enabled.
2227
def self.trace_exit_locations_enabled? # :nodoc:
2328
Primitive.rb_yjit_trace_exit_locations_enabled_p
@@ -28,15 +33,22 @@ def self.reset_stats!
2833
Primitive.rb_yjit_reset_stats_bang
2934
end
3035

31-
# Enable \YJIT compilation. `stats` option decides whether to enable \YJIT stats or not.
36+
# Enable \YJIT compilation. `stats` option decides whether to enable \YJIT stats or not. `compilation_log` decides
37+
# whether to enable \YJIT compilation logging or not.
3238
#
39+
# `stats`:
3340
# * `false`: Disable stats.
3441
# * `true`: Enable stats. Print stats at exit.
3542
# * `:quiet`: Enable stats. Do not print stats at exit.
36-
def self.enable(stats: false)
43+
#
44+
# `log`:
45+
# * `false`: Don't enable the log.
46+
# * `true`: Enable the log. Print log at exit.
47+
# * `:quiet`: Enable the log. Do not print log at exit.
48+
def self.enable(stats: false, log: false)
3749
return false if enabled?
3850
at_exit { print_and_dump_stats } if stats
39-
Primitive.rb_yjit_enable(stats, stats != :quiet)
51+
Primitive.rb_yjit_enable(stats, stats != :quiet, log, log != :quiet)
4052
end
4153

4254
# If --yjit-trace-exits is enabled parse the hashes from
@@ -173,6 +185,16 @@ def self.stats_string
173185
strio.string
174186
end
175187

188+
# Return an array of log entries.
189+
# Return `nil` when option is not passed or unavailable.
190+
def self.log
191+
return nil unless log_enabled?
192+
193+
Primitive.rb_yjit_get_log.map do |timestamp, path|
194+
[Time.at(timestamp), path]
195+
end
196+
end
197+
176198
# Produce disassembly for an iseq. This requires a `--enable-yjit=dev` build.
177199
def self.disasm(iseq) # :nodoc:
178200
# If a method or proc is passed in, get its iseq

yjit/src/codegen.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,7 @@ macro_rules! perf_call {
458458
}
459459

460460
use crate::codegen::JCCKinds::*;
461+
use crate::log::Log;
461462

462463
#[allow(non_camel_case_types, unused)]
463464
pub enum JCCKinds {
@@ -1223,6 +1224,8 @@ pub fn gen_single_block(
12231224
asm_comment!(asm, "reg_mapping: {:?}", asm.ctx.get_reg_mapping());
12241225
}
12251226

1227+
Log::add_block_with_chain_depth(blockid, asm.ctx.get_chain_depth());
1228+
12261229
// Mark the start of an ISEQ for --yjit-perf
12271230
jit_perf_symbol_push!(jit, &mut asm, &get_iseq_name(iseq), PerfMap::ISEQ);
12281231

yjit/src/core.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1763,7 +1763,7 @@ pub struct IseqPayload {
17631763
// Basic block versions
17641764
pub version_map: VersionMap,
17651765

1766-
// Indexes of code pages used by this this ISEQ
1766+
// Indexes of code pages used by this ISEQ
17671767
pub pages: HashSet<usize>,
17681768

17691769
// List of ISEQ entry codes

yjit/src/cruby.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@
8484

8585
use std::convert::From;
8686
use std::ffi::{CString, CStr};
87+
use std::fmt::{Debug, Formatter};
8788
use std::os::raw::{c_char, c_int, c_uint};
8889
use std::panic::{catch_unwind, UnwindSafe};
8990

@@ -563,6 +564,18 @@ impl From<*const rb_callable_method_entry_t> for VALUE {
563564
}
564565
}
565566

567+
impl From<&str> for VALUE {
568+
fn from(value: &str) -> Self {
569+
rust_str_to_ruby(value)
570+
}
571+
}
572+
573+
impl From<String> for VALUE {
574+
fn from(value: String) -> Self {
575+
rust_str_to_ruby(&value)
576+
}
577+
}
578+
566579
impl From<VALUE> for u64 {
567580
fn from(value: VALUE) -> Self {
568581
let VALUE(uimm) = value;
@@ -594,7 +607,6 @@ impl From<VALUE> for u16 {
594607
}
595608

596609
/// Produce a Ruby string from a Rust string slice
597-
#[cfg(feature = "disasm")]
598610
pub fn rust_str_to_ruby(str: &str) -> VALUE {
599611
unsafe { rb_utf8_str_new(str.as_ptr() as *const _, str.len() as i64) }
600612
}
@@ -625,6 +637,12 @@ pub struct SourceLocation {
625637
pub line: c_int,
626638
}
627639

640+
impl Debug for SourceLocation {
641+
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
642+
f.write_fmt(format_args!("{}:{}", self.file.to_string_lossy(), self.line))
643+
}
644+
}
645+
628646
/// Make a [SourceLocation] at the current spot.
629647
macro_rules! src_loc {
630648
() => {

yjit/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ mod stats;
1515
mod utils;
1616
mod yjit;
1717
mod virtualmem;
18+
mod log;

yjit/src/log.rs

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
use crate::core::BlockId;
2+
use crate::cruby::*;
3+
use crate::options::*;
4+
use crate::yjit::yjit_enabled_p;
5+
6+
use std::fmt::{Display, Formatter};
7+
use std::os::raw::c_long;
8+
use crate::utils::iseq_get_location;
9+
10+
type Timestamp = f64;
11+
12+
#[derive(Clone, Debug)]
13+
pub struct LogEntry {
14+
/// The time when the block was compiled.
15+
pub timestamp: Timestamp,
16+
17+
/// The log message.
18+
pub message: String,
19+
}
20+
21+
impl Display for LogEntry {
22+
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
23+
write!(f, "{:15.6}: {}", self.timestamp, self.message)
24+
}
25+
}
26+
27+
pub type Log = CircularBuffer<LogEntry, 1024>;
28+
static mut LOG: Option<Log> = None;
29+
30+
impl Log {
31+
pub fn init() {
32+
unsafe {
33+
LOG = Some(Log::new());
34+
}
35+
}
36+
37+
pub fn get_instance() -> &'static mut Log {
38+
unsafe {
39+
LOG.as_mut().unwrap()
40+
}
41+
}
42+
43+
pub fn has_instance() -> bool {
44+
unsafe {
45+
LOG.as_mut().is_some()
46+
}
47+
}
48+
49+
pub fn add_block_with_chain_depth(block_id: BlockId, chain_depth: u8) {
50+
if !Self::has_instance() {
51+
return;
52+
}
53+
54+
let print_log = get_option!(log);
55+
let timestamp = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs_f64();
56+
57+
let location = iseq_get_location(block_id.iseq, block_id.idx);
58+
let index = block_id.idx;
59+
let message = if chain_depth > 0 {
60+
format!("{} (index: {}, chain_depth: {})", location, index, chain_depth)
61+
} else {
62+
format!("{} (index: {})", location, index)
63+
};
64+
65+
let entry = LogEntry {
66+
timestamp,
67+
message
68+
};
69+
70+
if let Some(output) = print_log {
71+
match output {
72+
LogOutput::Stderr => {
73+
eprintln!("{}", entry);
74+
}
75+
76+
LogOutput::File(fd) => {
77+
use std::os::unix::io::{FromRawFd, IntoRawFd};
78+
use std::io::Write;
79+
80+
// Write with the fd opened during boot
81+
let mut file = unsafe { std::fs::File::from_raw_fd(fd) };
82+
writeln!(file, "{}", entry).unwrap();
83+
file.flush().unwrap();
84+
file.into_raw_fd(); // keep the fd open
85+
}
86+
87+
LogOutput::MemoryOnly => () // Don't print or write anything
88+
}
89+
}
90+
91+
Self::get_instance().push(entry);
92+
}
93+
}
94+
95+
pub struct CircularBuffer<T, const N: usize> {
96+
buffer: Vec<Option<T>>,
97+
head: usize,
98+
tail: usize,
99+
size: usize
100+
}
101+
102+
impl<T: Clone, const N: usize> CircularBuffer<T, N> {
103+
pub fn new() -> Self {
104+
Self {
105+
buffer: vec![None; N],
106+
head: 0,
107+
tail: 0,
108+
size: 0
109+
}
110+
}
111+
112+
pub fn push(&mut self, value: T) {
113+
self.buffer[self.head] = Some(value);
114+
self.head = (self.head + 1) % N;
115+
if self.size == N {
116+
self.tail = (self.tail + 1) % N;
117+
} else {
118+
self.size += 1;
119+
}
120+
}
121+
122+
pub fn pop(&mut self) -> Option<T> {
123+
if self.size == 0 {
124+
return None;
125+
}
126+
127+
let value = self.buffer[self.tail].take();
128+
self.tail = (self.tail + 1) % N;
129+
self.size -= 1;
130+
value
131+
}
132+
133+
pub fn len(&self) -> usize {
134+
self.size
135+
}
136+
}
137+
138+
139+
//===========================================================================
140+
141+
/// Primitive called in yjit.rb
142+
/// Check if log generation is enabled
143+
#[no_mangle]
144+
pub extern "C" fn rb_yjit_log_enabled_p(_ec: EcPtr, _ruby_self: VALUE) -> VALUE {
145+
if get_option!(log).is_some() {
146+
return Qtrue;
147+
} else {
148+
return Qfalse;
149+
}
150+
}
151+
152+
/// Primitive called in yjit.rb.
153+
/// Export all YJIT log entries as a Ruby array.
154+
#[no_mangle]
155+
pub extern "C" fn rb_yjit_get_log(_ec: EcPtr, _ruby_self: VALUE) -> VALUE {
156+
with_vm_lock(src_loc!(), || rb_yjit_get_log_array())
157+
}
158+
159+
fn rb_yjit_get_log_array() -> VALUE {
160+
if !yjit_enabled_p() || get_option!(log).is_none() {
161+
return Qnil;
162+
}
163+
164+
let log = Log::get_instance();
165+
let array = unsafe { rb_ary_new_capa(log.len() as c_long) };
166+
167+
while log.len() > 0 {
168+
let entry = log.pop().unwrap();
169+
170+
unsafe {
171+
let entry_array = rb_ary_new_capa(2);
172+
rb_ary_push(entry_array, rb_float_new(entry.timestamp));
173+
rb_ary_push(entry_array, entry.message.into());
174+
rb_ary_push(array, entry_array);
175+
}
176+
}
177+
178+
return array;
179+
}

0 commit comments

Comments
 (0)