Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add readonly mode #428

Merged
merged 1 commit into from
Nov 24, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ Bootsnap.setup(
load_path_cache: true, # Optimize the LOAD_PATH with a cache
compile_cache_iseq: true, # Compile Ruby code into ISeq cache, breaks coverage reporting.
compile_cache_yaml: true, # Compile YAML into a cache
readonly: true, # Use the caches but don't update them on miss or stale entries.
)
```

Expand All @@ -77,6 +78,7 @@ well together, and are both included in a newly-generated Rails applications by
- `DISABLE_BOOTSNAP` allows to entirely disable bootsnap.
- `DISABLE_BOOTSNAP_LOAD_PATH_CACHE` allows to disable load path caching.
- `DISABLE_BOOTSNAP_COMPILE_CACHE` allows to disable ISeq and YAML caches.
- `BOOTSNAP_READONLY` configure bootsnap to not update the cache on miss or stale entries.
- `BOOTSNAP_LOG` configure bootsnap to log all caches misses to STDERR.
- `BOOTSNAP_IGNORE_DIRECTORIES` a comma separated list of directories that shouldn't be scanned.
Useful when you have large directories of non-ruby files inside `$LOAD_PATH`.
Expand Down
31 changes: 23 additions & 8 deletions ext/bootsnap/bootsnap.c
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,11 @@ static ID instrumentation_method;
static VALUE sym_miss;
static VALUE sym_stale;
static bool instrumentation_enabled = false;
static bool readonly = false;

/* Functions exposed as module functions on Bootsnap::CompileCache::Native */
static VALUE bs_instrumentation_enabled_set(VALUE self, VALUE enabled);
static VALUE bs_readonly_set(VALUE self, VALUE enabled);
static VALUE bs_compile_option_crc32_set(VALUE self, VALUE crc32_v);
static VALUE bs_rb_fetch(VALUE self, VALUE cachedir_v, VALUE path_v, VALUE handler, VALUE args);
static VALUE bs_rb_precompile(VALUE self, VALUE cachedir_v, VALUE path_v, VALUE handler);
Expand Down Expand Up @@ -166,6 +168,7 @@ Init_bootsnap(void)
rb_global_variable(&sym_stale);

rb_define_module_function(rb_mBootsnap, "instrumentation_enabled=", bs_instrumentation_enabled_set, 1);
rb_define_module_function(rb_mBootsnap_CompileCache_Native, "readonly=", bs_readonly_set, 1);
rb_define_module_function(rb_mBootsnap_CompileCache_Native, "coverage_running?", bs_rb_coverage_running, 0);
rb_define_module_function(rb_mBootsnap_CompileCache_Native, "fetch", bs_rb_fetch, 4);
rb_define_module_function(rb_mBootsnap_CompileCache_Native, "precompile", bs_rb_precompile, 3);
Expand All @@ -182,6 +185,13 @@ bs_instrumentation_enabled_set(VALUE self, VALUE enabled)
return enabled;
}

static VALUE
bs_readonly_set(VALUE self, VALUE enabled)
{
readonly = RTEST(enabled);
return enabled;
}

/*
* Bootsnap's ruby code registers a hook that notifies us via this function
* when compile_option changes. These changes invalidate all existing caches.
Expand Down Expand Up @@ -945,12 +955,17 @@ try_input_to_storage(VALUE arg)
static int
bs_input_to_storage(VALUE handler, VALUE args, VALUE input_data, VALUE pathval, VALUE * storage_data)
{
int state;
struct i2s_data i2s_data = {
.handler = handler,
.input_data = input_data,
.pathval = pathval,
};
*storage_data = rb_protect(try_input_to_storage, (VALUE)&i2s_data, &state);
return state;
if (readonly) {
*storage_data = rb_cBootsnap_CompileCache_UNCOMPILABLE;
return 0;
} else {
int state;
struct i2s_data i2s_data = {
.handler = handler,
.input_data = input_data,
.pathval = pathval,
};
*storage_data = rb_protect(try_input_to_storage, (VALUE)&i2s_data, &state);
return state;
}
}
4 changes: 4 additions & 0 deletions lib/bootsnap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ def setup(
development_mode: true,
load_path_cache: true,
ignore_directories: nil,
readonly: false,
compile_cache_iseq: true,
compile_cache_yaml: true,
compile_cache_json: true
Expand All @@ -49,6 +50,7 @@ def setup(
cache_path: "#{cache_dir}/bootsnap/load-path-cache",
development_mode: development_mode,
ignore_directories: ignore_directories,
readonly: readonly,
)
end

Expand All @@ -57,6 +59,7 @@ def setup(
iseq: compile_cache_iseq,
yaml: compile_cache_yaml,
json: compile_cache_json,
readonly: readonly,
)
end

Expand Down Expand Up @@ -101,6 +104,7 @@ def default_setup
compile_cache_iseq: !ENV["DISABLE_BOOTSNAP_COMPILE_CACHE"],
compile_cache_yaml: !ENV["DISABLE_BOOTSNAP_COMPILE_CACHE"],
compile_cache_json: !ENV["DISABLE_BOOTSNAP_COMPILE_CACHE"],
readonly: !!ENV["BOOTSNAP_READONLY"],
ignore_directories: ignore_directories,
)

Expand Down
6 changes: 5 additions & 1 deletion lib/bootsnap/compile_cache.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ def UNCOMPILABLE.inspect
Error = Class.new(StandardError)
PermissionError = Class.new(Error)

def self.setup(cache_dir:, iseq:, yaml:, json:)
def self.setup(cache_dir:, iseq:, yaml:, json:, readonly: false)
if iseq
if supported?
require_relative("compile_cache/iseq")
Expand All @@ -37,6 +37,10 @@ def self.setup(cache_dir:, iseq:, yaml:, json:)
warn("[bootsnap/setup] JSON parsing caching is not supported on this implementation of Ruby")
end
end

if supported? && defined?(Bootsnap::CompileCache::Native)
Bootsnap::CompileCache::Native.readonly = readonly
end
end

def self.permission_error(path)
Expand Down
4 changes: 2 additions & 2 deletions lib/bootsnap/load_path_cache.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,13 @@ class << self
alias_method :enabled?, :enabled
remove_method(:enabled)

def setup(cache_path:, development_mode:, ignore_directories:)
def setup(cache_path:, development_mode:, ignore_directories:, readonly: false)
unless supported?
warn("[bootsnap/setup] Load path caching is not supported on this implementation of Ruby") if $VERBOSE
return
end

store = Store.new(cache_path)
store = Store.new(cache_path, readonly: readonly)

@loaded_features_index = LoadedFeaturesIndex.new

Expand Down
5 changes: 3 additions & 2 deletions lib/bootsnap/load_path_cache/store.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ class Store
NestedTransactionError = Class.new(StandardError)
SetOutsideTransactionNotAllowed = Class.new(StandardError)

def initialize(store_path)
def initialize(store_path, readonly: false)
@store_path = store_path
@txn_mutex = Mutex.new
@dirty = false
@readonly = readonly
load_data
end

Expand Down Expand Up @@ -63,7 +64,7 @@ def mark_for_mutation!
end

def commit_transaction
if @dirty
if @dirty && !@readonly
dump_data
@dirty = false
end
Expand Down
31 changes: 31 additions & 0 deletions test/compile_cache_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
class CompileCacheTest < Minitest::Test
include(TmpdirHelper)

def teardown
super
Bootsnap::CompileCache::Native.readonly = false
end

def test_compile_option_crc32
# Just assert that this works.
Bootsnap::CompileCache::Native.compile_option_crc32 = 0xffffffff
Expand Down Expand Up @@ -112,6 +117,32 @@ def test_recache_when_size_different
load(path)
end

def test_dont_store_cache_after_a_miss_when_readonly
Bootsnap::CompileCache::Native.readonly = true

path = Help.set_file("a.rb", "a = a = 3", 100)
output = RubyVM::InstructionSequence.compile_file(path)
Bootsnap::CompileCache::ISeq.expects(:input_to_storage).never
Bootsnap::CompileCache::ISeq.expects(:storage_to_output).never
Bootsnap::CompileCache::ISeq.expects(:input_to_output).once.returns(output)

load(path)
end

def test_dont_store_cache_after_a_stale_when_readonly
path = Help.set_file("a.rb", "a = a = 3", 100)
load(path)

Bootsnap::CompileCache::Native.readonly = true

output = RubyVM::InstructionSequence.compile_file(path)
Bootsnap::CompileCache::ISeq.expects(:input_to_storage).never
Bootsnap::CompileCache::ISeq.expects(:storage_to_output).once.returns(output)
Bootsnap::CompileCache::ISeq.expects(:input_to_output).never

load(path)
end

def test_invalid_cache_file
path = Help.set_file("a.rb", "a = a = 3", 100)
cp = Help.cache_path("#{@tmp_dir}-iseq", path)
Expand Down
6 changes: 6 additions & 0 deletions test/setup_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ def test_default_setup
compile_cache_yaml: true,
compile_cache_json: true,
ignore_directories: nil,
readonly: false,
)

Bootsnap.default_setup
Expand All @@ -39,6 +40,7 @@ def test_default_setup_with_ENV_not_dev
compile_cache_yaml: true,
compile_cache_json: true,
ignore_directories: nil,
readonly: false,
)

Bootsnap.default_setup
Expand All @@ -55,6 +57,7 @@ def test_default_setup_with_DISABLE_BOOTSNAP_LOAD_PATH_CACHE
compile_cache_yaml: true,
compile_cache_json: true,
ignore_directories: nil,
readonly: false,
)

Bootsnap.default_setup
Expand All @@ -71,6 +74,7 @@ def test_default_setup_with_DISABLE_BOOTSNAP_COMPILE_CACHE
compile_cache_yaml: false,
compile_cache_json: false,
ignore_directories: nil,
readonly: false,
)

Bootsnap.default_setup
Expand All @@ -94,6 +98,7 @@ def test_default_setup_with_BOOTSNAP_LOG
compile_cache_yaml: true,
compile_cache_json: true,
ignore_directories: nil,
readonly: false,
)
Bootsnap.expects(:logger=).with($stderr.method(:puts))

Expand All @@ -111,6 +116,7 @@ def test_default_setup_with_BOOTSNAP_IGNORE_DIRECTORIES
compile_cache_yaml: true,
compile_cache_json: true,
ignore_directories: %w[foo bar],
readonly: false,
)

Bootsnap.default_setup
Expand Down