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
2 changes: 1 addition & 1 deletion .github/workflows/zjit-macos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ jobs:
rustup install ${{ matrix.rust_version }} --profile minimal
rustup default ${{ matrix.rust_version }}

- uses: taiki-e/install-action@37f7c5781271959fb65b6b35224e28652ff2b63d # v2.87.0
- uses: taiki-e/install-action@742a3317eac7bd62f91cd888b4eead5e784ba833 # v2.87.1
with:
tool: nextest@0.9
if: ${{ matrix.test_task == 'zjit-check' }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/zjit-ubuntu.yml
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ jobs:
ruby-version: '3.1'
bundler: none

- uses: taiki-e/install-action@37f7c5781271959fb65b6b35224e28652ff2b63d # v2.87.0
- uses: taiki-e/install-action@742a3317eac7bd62f91cd888b4eead5e784ba833 # v2.87.1
with:
tool: nextest@0.9
if: ${{ matrix.test_task == 'zjit-check' }}
Expand Down
11 changes: 10 additions & 1 deletion box.c
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ static bool tmp_dir_has_dirsep;

/* process-private 0700 directory for box-local copies of extensions */
static char box_ext_tmp_dir[MAXPATHLEN];
static rb_pid_t box_ext_tmp_dir_owner;
static unsigned long box_ext_seq;

#define BOX_TMP_PREFIX "_ruby_box_"
Expand Down Expand Up @@ -574,6 +575,13 @@ system_tmpdir(void)
static void
ensure_box_ext_tmp_dir(void)
{
/* A forked child inherits the parent's directory path; discard it so
* that each process creates its own directory and removes only that
* one at teardown. */
if (box_ext_tmp_dir[0] && box_ext_tmp_dir_owner != getpid()) {
box_ext_tmp_dir[0] = '\0';
box_ext_seq = 0;
}
if (box_ext_tmp_dir[0]) return;

int last_errno = 0;
Expand All @@ -592,6 +600,7 @@ ensure_box_ext_tmp_dir(void)
}
if (mkdir(path, 0700) == 0) {
strlcpy(box_ext_tmp_dir, path, sizeof(box_ext_tmp_dir));
box_ext_tmp_dir_owner = getpid();
return;
}
last_errno = errno;
Expand Down Expand Up @@ -852,7 +861,7 @@ rb_box_unload_local_extensions(void)
ext = next;
}
#endif
if (box_ext_tmp_dir[0]) {
if (box_ext_tmp_dir[0] && box_ext_tmp_dir_owner == getpid()) {
rmdir(box_ext_tmp_dir);
box_ext_tmp_dir[0] = '\0';
}
Expand Down
71 changes: 53 additions & 18 deletions file.c
Original file line number Diff line number Diff line change
Expand Up @@ -2253,14 +2253,25 @@ rb_file_executable_real_p(VALUE obj, VALUE fname)

/*
* call-seq:
* File.file?(file) -> true or false
*
* Returns +true+ if the named +file+ exists and is a regular file.
*
* +file+ can be an IO object.
* File.file?(object) -> true or false
*
* Returns whether the given +object+, a string path or IO object,
* represents a filesystem entry that exists and is a regular file;
* see File.ftype:
*
* # Paths.
* File.file?('README.md') # => true
* File.file?('doc/') # => false
* File.file?('nosuch') # => false
* # IO objects.
* file = File.new('README.md')
* File.file?(file) # => true
* dir = Dir.new('doc/')
* File.file?(dir) # => false
* # Clean up.
* file.close
* dir.close
*
* If the +file+ argument is a symbolic link, it will resolve the symbolic link
* and use the file referenced by the link.
*/

static VALUE
Expand Down Expand Up @@ -7010,16 +7021,36 @@ rb_stat_ww(VALUE obj)
}

/*
* call-seq:
* stat.executable? -> true or false
* call-seq:
* executable? -> true or false
*
* Returns <code>true</code> if <i>stat</i> is executable or if the
* operating system doesn't distinguish executable files from
* nonexecutable files. The tests are made using the effective owner of
* the process.
* Returns whether the filesystem entry represented by +self+
* exists and is executable;
* raises Errno::ENOENT if the entry does not exist.
*
* On Windows, the entry is executable if its path has file extension
* +.bat+, +.cmd+, +.com+, or +.exe+:
*
* File.stat("testfile").executable? #=> false
* File.stat('win32/rtname.cmd').executable? # => true
* File.stat('win32/file.c').executable? # => false
*
* On other systems, the entry is executable if it has the execute/search
* permission for the effective user and group id of the current process;
* see {Permissions}[rdoc-ref:file/filesystem_modes.md@Permissions].
*
* These examples use
* a {helper method}[rdoc-ref:file/filesystem_modes.md@Helper+Method], +mode+,
* that displays a mode both in octal digits and in characters:
*
* File.stat('.').executable? # => true
* mode('.') # => "040775 drwxrwxr-x"
* File.stat('bin/gem').executable? # => true
* mode('bin/gem') # => "100775 -rwxrwxr-x"
* File.stat('/etc/passwd').executable? # => false
* mode('/etc/passwd') # => "100644 -rw-r--r--"
*
* Note that some filesystem settings may cause this method to return +true+
* even though the entry is not executable by the effective user/group.
*/

static VALUE
Expand Down Expand Up @@ -7080,12 +7111,16 @@ rb_stat_X(VALUE obj)

/*
* call-seq:
* stat.file? -> true or false
* file? -> true or false
*
* Returns whether +self+ represents a filesystem entry that exists and is a regular file;
* see File::Stat.ftype:
*
* Returns <code>true</code> if <i>stat</i> is a regular file (not
* a device file, pipe, socket, etc.).
* # Paths.
* File.stat('README.md').file? # => true
* File.stat('doc/').file? # => false
* File.stat('nosuch').file? # Raises Errno::ENOENT: No such file or directory.
*
* File.stat("testfile").file? #=> true
*
*/

Expand Down
46 changes: 39 additions & 7 deletions pathname_builtin.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1670,9 +1670,19 @@ def fnmatch(pattern, ...) File.fnmatch(pattern, @path, ...) end
def fnmatch?(pattern, ...) File.fnmatch?(pattern, @path, ...) end

# call-seq:
# pathname.ftype -> string
# ftype -> string
#
# Returns the string type of the object at the path in +self+:
# Returns the string type of the object at the path in <tt>self</tt>, one of:
#
# - <tt>'file'</tt>.
# - <tt>'directory'</tt>.
# - <tt>'characterSpecial'</tt>.
# - <tt>'blockSpecial'</tt>.
# - <tt>'fifo'</tt>.
# - <tt>'link'</tt>.
# - <tt>'socket'</tt>.
#
# Examples:
#
# Pathname('README.md').ftype # => "file"
# Pathname('lib').ftype # => "directory"
Expand Down Expand Up @@ -2280,12 +2290,33 @@ def empty?
# call-seq:
# executable? -> true or false
#
# Returns whether the entry represented by `self` is executable;
# calls FileTest.executable? with argument `self.to_s`:
# Returns whether the entry represented by `self` exists and is executable.
#
# On Windows, the entry is executable if its path has file extension
# `.bat`, `.cmd`, `.com`, or `.exe`:
#
# ```ruby
# Pathname('bin/gem').executable? # => true
# mode('bin/gem') # => "100775 -rwxrwxr-x"
# Pathname('.').executable? # => true
# mode('.') # => "040775 drwxrwxr-x"
# Pathname('nosuch').executable? # => false
# ```
#
# On other systems, the entry is executable if it has the execute/search
# permission for the effective user and group id of the current process;
# see {Permissions}[rdoc-ref:file/filesystem_modes.md@Permissions].
#
# These examples use
# a {helper method}[rdoc-ref:file/filesystem_modes.md@Helper+Method], `mode`,
# that displays a mode both in octal digits and in characters:
#
# ```ruby
# Pathname('bin/gem').executable? # => true
# Pathname('README.md').executable? # => false
# Pathname('bin/gem').executable? # => true
# mode('bin/gem') # => "100775 -rwxrwxr-x"
# Pathname('.').executable? # => true
# mode('.') # => "040775 drwxrwxr-x"
# Pathname('nosuch').executable? # => false
# ```
#
def executable?() FileTest.executable?(@path) end
Expand Down Expand Up @@ -2375,7 +2406,8 @@ def directory?() FileTest.directory?(@path) end
# call-seq:
# file? -> true or false
#
# Returns whether the entry at the path in `self` exists and is a regular file:
# Returns whether the entry at the path in `self` exists and is a regular file;
# see #ftype:
#
# ```ruby
# Pathname('README.md').file? # => true
Expand Down
12 changes: 12 additions & 0 deletions test/ruby/test_box.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1301,6 +1301,18 @@ def test_loading_extension_from_deep_path_in_user_box
end
end

def test_extension_loading_survives_fork_in_user_box
omit "fork is not supported" unless Process.respond_to?(:fork)

assert_ruby_status([ENV_ENABLE_BOX], "#{<<~"begin;"}\n#{<<~'end;'}")
begin;
Ruby::Box.new.require "digest/md5"
pid = fork {Ruby::Box.new.require "digest/sha2"}
raise "extension loading failed in the child" unless Process.wait2(pid)[1].success?
Ruby::Box.new.require "digest/sha2"
end;
end

def test_root_box_iclasses_should_be_boxable
assert_separately([ENV_ENABLE_BOX], __FILE__, __LINE__, "#{<<~"begin;"}\n#{<<~'end;'}", ignore_stderr: true)
begin;
Expand Down
17 changes: 11 additions & 6 deletions vm.c
Original file line number Diff line number Diff line change
Expand Up @@ -526,8 +526,10 @@ yjit_compile(rb_execution_context_t *ec)
const rb_iseq_t *iseq = CFP_ISEQ(ec->cfp);
struct rb_iseq_constant_body *body = ISEQ_BODY(iseq);

// Increment the ISEQ's call counter and trigger JIT compilation if not compiled
if (body->jit_entry == NULL) {
// Increment the ISEQ's call counter and trigger JIT compilation if not compiled.
// Stop incrementing when not compiling (out of executable memory) so that
// ISEQs that failed to compile don't keep dirtying CoW pages after fork.
if (body->jit_entry == NULL && rb_yjit_compiling_p) {
body->jit_entry_calls++;
if (rb_yjit_threshold_hit(iseq, body->jit_entry_calls)) {
rb_yjit_compile_iseq(iseq, ec, false);
Expand All @@ -546,7 +548,7 @@ zjit_compile(rb_execution_context_t *ec)
const rb_iseq_t *iseq = CFP_ISEQ(ec->cfp);
struct rb_iseq_constant_body *body = ISEQ_BODY(iseq);

if (body->jit_entry == NULL) {
if (body->jit_entry == NULL && rb_zjit_compiling_p) {
body->jit_entry_calls++;

// At profile-threshold, rewrite some of the YARV instructions
Expand Down Expand Up @@ -607,7 +609,9 @@ jit_compile_exception(rb_execution_context_t *ec)
struct rb_iseq_constant_body *body = ISEQ_BODY(iseq);

#if USE_ZJIT
if (body->jit_exception == NULL && rb_zjit_enabled_p) {
// rb_zjit_compiling_p is false until ZJIT is enabled, so no
// rb_zjit_enabled_p check is needed here.
if (body->jit_exception == NULL && rb_zjit_compiling_p) {
body->jit_exception_calls++;

// At profile-threshold, rewrite some of the YARV instructions
Expand All @@ -624,8 +628,9 @@ jit_compile_exception(rb_execution_context_t *ec)
#endif

#if USE_YJIT
// Increment the ISEQ's call counter and trigger JIT compilation if not compiled
if (body->jit_exception == NULL && rb_yjit_enabled_p) {
// Increment the ISEQ's call counter and trigger JIT compilation if not compiled.
// Like the ZJIT branch above, no rb_yjit_enabled_p check is needed here.
if (body->jit_exception == NULL && rb_yjit_compiling_p) {
body->jit_exception_calls++;
if (body->jit_exception_calls == rb_yjit_call_threshold) {
rb_yjit_compile_iseq(iseq, ec, true);
Expand Down
17 changes: 17 additions & 0 deletions vm_insnhelper.c
Original file line number Diff line number Diff line change
Expand Up @@ -1874,12 +1874,29 @@ vm_throw(const rb_execution_context_t *ec, rb_control_frame_t *reg_cfp,
}
}

// Fallback for YJIT. Prepare throw data and return it.
VALUE
rb_vm_throw(const rb_execution_context_t *ec, rb_control_frame_t *reg_cfp, rb_num_t throw_state, VALUE throwobj)
{
return vm_throw(ec, reg_cfp, throw_state, throwobj);
}

// Fallback for ZJIT. Make a longjmp and unwind to the most recent vm_exec().
VALUE
rb_zjit_throw(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp, rb_num_t throw_state, VALUE throwobj)
{
VALUE val = vm_throw(ec, reg_cfp, throw_state, throwobj);

// vm_throw() has set ec->tag->state. On the longjmp path, vm_exec() reads the throw data from
// ec->errinfo instead of vm_exec_core()'s return value like THROW_EXCEPTION()'s path does, so
// we need to put it there instead.
enum ruby_tag_type state = ec->tag->state;
ec->errinfo = val;
EC_JUMP_TAG(ec, state);

UNREACHABLE_RETURN(Qundef);
}

static inline void
vm_expandarray(struct rb_control_frame_struct *cfp, VALUE ary, rb_num_t num, int flag)
{
Expand Down
2 changes: 2 additions & 0 deletions yjit.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ extern uint64_t rb_yjit_cold_threshold;
extern uint64_t rb_yjit_live_iseq_count;
extern uint64_t rb_yjit_iseq_alloc_count;
extern bool rb_yjit_enabled_p;
extern bool rb_yjit_compiling_p;
void rb_yjit_incr_counter(const char *counter_name);
void rb_yjit_invalidate_all_method_lookup_assumptions(void);
void rb_yjit_cme_invalidate(rb_callable_method_entry_t *cme);
Expand Down Expand Up @@ -58,6 +59,7 @@ void rb_yjit_mark_all_executable(void);
// In these builds, YJIT could never be turned on. Provide dummy implementations.

#define rb_yjit_enabled_p false
#define rb_yjit_compiling_p false
static inline void rb_yjit_incr_counter(const char *counter_name) {}
static inline void rb_yjit_invalidate_all_method_lookup_assumptions(void) {}
static inline void rb_yjit_cme_invalidate(rb_callable_method_entry_t *cme) {}
Expand Down
31 changes: 26 additions & 5 deletions yjit/src/yjit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,15 @@ use crate::log::Log;
#[no_mangle]
pub static mut rb_yjit_enabled_p: bool = false;

/// Whether YJIT is compiling. Starts as false until YJIT is enabled, so the
/// interpreter doesn't need to check rb_yjit_enabled_p before it. Set back to
/// false when we run out of executable memory, in which case the interpreter
/// stops incrementing ISEQ call counters so that ISEQs that will never be
/// compiled stop dirtying CoW pages after fork.
#[allow(non_upper_case_globals)]
#[no_mangle]
pub static mut rb_yjit_compiling_p: bool = false;

// Time when YJIT was yjit was initialized (see yjit_init)
pub static mut YJIT_INIT_TIME: Option<Instant> = None;

Expand All @@ -37,6 +46,14 @@ pub fn yjit_enabled_p() -> bool {
unsafe { rb_yjit_enabled_p }
}

/// Whether we have run out of executable memory. Used by the C code to
/// decide whether to keep incrementing ISEQ call counters.
pub fn out_of_memory_p() -> bool {
let cb = CodegenGlobals::get_inline_cb();
let ocb = CodegenGlobals::get_outlined_cb();
cb.has_dropped_bytes() || ocb.unwrap().has_dropped_bytes()
}

/// Register specialized codegen for builtin C method entries.
/// Must be called at boot before ruby_init_prelude() since the prelude
/// could redefine core methods (e.g. Kernel.prepend via bundler).
Expand Down Expand Up @@ -77,7 +94,10 @@ fn yjit_init() {

// YJIT enabled and initialized successfully
assert!(unsafe{ !rb_yjit_enabled_p });
unsafe { rb_yjit_enabled_p = true; }
unsafe {
rb_yjit_enabled_p = true;
rb_yjit_compiling_p = true;
}
});

if let Err(_) = result {
Expand Down Expand Up @@ -179,10 +199,11 @@ pub extern "C" fn rb_yjit_iseq_gen_entry_point(iseq: IseqPtr, ec: EcPtr, jit_exc

let maybe_code_ptr = with_compile_time(|| { gen_entry_point(iseq, ec, jit_exception) });

match maybe_code_ptr {
Some(ptr) => ptr,
None => std::ptr::null(),
}
// Stop compiling if we ran out of executable memory so that the
// interpreter stops incrementing ISEQ call counters.
unsafe { rb_yjit_compiling_p = !out_of_memory_p(); }

maybe_code_ptr.unwrap_or(std::ptr::null())
}

/// Free and recompile all existing JIT code
Expand Down
Loading