-
Notifications
You must be signed in to change notification settings - Fork 93
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
Fix TruffleRuby backend after #325 #328
Changes from all commits
1ded387
211e15f
ce39f78
acd0876
cf497ba
0b51010
a6ea5b2
aea0d22
1f56838
015f657
2cec0e7
ab4c4f5
21b7ed5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,380 @@ | ||
# This code used to be shared in lib/mini_racer.rb | ||
# but was moved to the extension with https://github.com/rubyjs/mini_racer/pull/325. | ||
# So now this is effectively duplicate logic with C/C++ code. | ||
# Maybe one day it can be actually shared again between both backends. | ||
|
||
module MiniRacer | ||
|
||
MARSHAL_STACKDEPTH_DEFAULT = 2**9-2 | ||
MARSHAL_STACKDEPTH_MAX_VALUE = 2**10-2 | ||
|
||
class FailedV8Conversion | ||
attr_reader :info | ||
def initialize(info) | ||
@info = info | ||
end | ||
end | ||
|
||
# helper class returned when we have a JavaScript function | ||
class JavaScriptFunction | ||
def to_s | ||
"JavaScript Function" | ||
end | ||
end | ||
|
||
class Isolate | ||
def initialize(snapshot = nil) | ||
unless snapshot.nil? || snapshot.is_a?(Snapshot) | ||
raise ArgumentError, "snapshot must be a Snapshot object, passed a #{snapshot.inspect}" | ||
end | ||
|
||
# defined in the C class | ||
init_with_snapshot(snapshot) | ||
end | ||
end | ||
|
||
class Platform | ||
class << self | ||
def set_flags!(*args, **kwargs) | ||
flags_to_strings([args, kwargs]).each do |flag| | ||
# defined in the C class | ||
set_flag_as_str!(flag) | ||
end | ||
end | ||
|
||
private | ||
|
||
def flags_to_strings(flags) | ||
flags.flatten.map { |flag| flag_to_string(flag) }.flatten | ||
end | ||
|
||
# normalize flags to strings, and adds leading dashes if needed | ||
def flag_to_string(flag) | ||
if flag.is_a?(Hash) | ||
flag.map do |key, value| | ||
"#{flag_to_string(key)} #{value}" | ||
end | ||
else | ||
str = flag.to_s | ||
str = "--#{str}" unless str.start_with?('--') | ||
str | ||
end | ||
end | ||
end | ||
end | ||
|
||
# eval is defined in the C class | ||
class Context | ||
|
||
class ExternalFunction | ||
def initialize(name, callback, parent) | ||
unless String === name | ||
raise ArgumentError, "parent_object must be a String" | ||
end | ||
parent_object, _ , @name = name.rpartition(".") | ||
@callback = callback | ||
@parent = parent | ||
@parent_object_eval = nil | ||
@parent_object = nil | ||
|
||
unless parent_object.empty? | ||
@parent_object = parent_object | ||
|
||
@parent_object_eval = "" | ||
prev = "" | ||
first = true | ||
parent_object.split(".").each do |obj| | ||
prev << obj | ||
if first | ||
@parent_object_eval << "if (typeof #{prev} !== 'object' || typeof #{prev} !== 'function') { #{prev} = {} };\n" | ||
else | ||
@parent_object_eval << "#{prev} = #{prev} || {};\n" | ||
end | ||
prev << "." | ||
first = false | ||
end | ||
@parent_object_eval << "#{parent_object};" | ||
end | ||
notify_v8 | ||
end | ||
end | ||
|
||
def initialize(max_memory: nil, timeout: nil, isolate: nil, ensure_gc_after_idle: nil, snapshot: nil, marshal_stack_depth: nil) | ||
options ||= {} | ||
|
||
check_init_options!(isolate: isolate, snapshot: snapshot, max_memory: max_memory, marshal_stack_depth: marshal_stack_depth, ensure_gc_after_idle: ensure_gc_after_idle, timeout: timeout) | ||
|
||
@functions = {} | ||
@timeout = nil | ||
@max_memory = nil | ||
@current_exception = nil | ||
@timeout = timeout | ||
@max_memory = max_memory | ||
@marshal_stack_depth = marshal_stack_depth | ||
|
||
# false signals it should be fetched if requested | ||
@isolate = isolate || false | ||
|
||
@ensure_gc_after_idle = ensure_gc_after_idle | ||
|
||
if @ensure_gc_after_idle | ||
@last_eval = nil | ||
@ensure_gc_thread = nil | ||
@ensure_gc_mutex = Mutex.new | ||
end | ||
|
||
@disposed = false | ||
|
||
@callback_mutex = Mutex.new | ||
@callback_running = false | ||
@thread_raise_called = false | ||
@eval_thread = nil | ||
|
||
# defined in the C class | ||
init_unsafe(isolate, snapshot) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. init_unsafe no longer exists There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It does, it's defined in |
||
end | ||
|
||
def isolate | ||
return @isolate if @isolate != false | ||
# defined in the C class | ||
@isolate = create_isolate_value | ||
end | ||
Comment on lines
+137
to
+141
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Isolates no longer exist (or rather, are not exposed in the Ruby API) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right, I took a quick look but it's not so easy to remove so that could be done separately and I think best to get the CI green first (e.g. to avoid regressions). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I only followed the recent changes partially, but why has the Isolates API been removed? I have never used them but they have a big section in the README (https://github.com/rubyjs/mini_racer#shared-isolates). If they are gone for good, the README needs to be cleaned up to and it's a big fat breaking change too, but not mentioned in the CHANGELOG for yesterdays 0.17.0.pre6. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. README update in #331. The changelog was Sam's work, I think? |
||
|
||
def eval(str, options=nil) | ||
raise(ContextDisposedError, 'attempted to call eval on a disposed context!') if @disposed | ||
|
||
filename = options && options[:filename].to_s | ||
|
||
@eval_thread = Thread.current | ||
isolate_mutex.synchronize do | ||
@current_exception = nil | ||
timeout do | ||
eval_unsafe(str, filename) | ||
end | ||
end | ||
ensure | ||
@eval_thread = nil | ||
ensure_gc_thread if @ensure_gc_after_idle | ||
end | ||
|
||
def call(function_name, *arguments) | ||
raise(ContextDisposedError, 'attempted to call function on a disposed context!') if @disposed | ||
|
||
@eval_thread = Thread.current | ||
isolate_mutex.synchronize do | ||
timeout do | ||
call_unsafe(function_name, *arguments) | ||
end | ||
end | ||
ensure | ||
@eval_thread = nil | ||
ensure_gc_thread if @ensure_gc_after_idle | ||
end | ||
|
||
def dispose | ||
return if @disposed | ||
isolate_mutex.synchronize do | ||
return if @disposed | ||
dispose_unsafe | ||
@disposed = true | ||
@isolate = nil # allow it to be garbage collected, if set | ||
end | ||
end | ||
|
||
|
||
def attach(name, callback) | ||
raise(ContextDisposedError, 'attempted to call function on a disposed context!') if @disposed | ||
|
||
wrapped = lambda do |*args| | ||
begin | ||
|
||
r = nil | ||
|
||
begin | ||
@callback_mutex.synchronize{ | ||
@callback_running = true | ||
} | ||
r = callback.call(*args) | ||
ensure | ||
@callback_mutex.synchronize{ | ||
@callback_running = false | ||
} | ||
end | ||
|
||
# wait up to 2 seconds for this to be interrupted | ||
# will very rarely be called cause #raise is called | ||
# in another mutex | ||
@callback_mutex.synchronize { | ||
if @thread_raise_called | ||
sleep 2 | ||
end | ||
} | ||
|
||
r | ||
|
||
ensure | ||
@callback_mutex.synchronize { | ||
@thread_raise_called = false | ||
} | ||
end | ||
end | ||
|
||
isolate_mutex.synchronize do | ||
external = ExternalFunction.new(name, wrapped, self) | ||
@functions["#{name}"] = external | ||
end | ||
end | ||
|
||
private | ||
|
||
def ensure_gc_thread | ||
@last_eval = Process.clock_gettime(Process::CLOCK_MONOTONIC) | ||
@ensure_gc_mutex.synchronize do | ||
@ensure_gc_thread = nil if !@ensure_gc_thread&.alive? | ||
return if !Thread.main.alive? # avoid "can't alloc thread" exception | ||
@ensure_gc_thread ||= Thread.new do | ||
ensure_gc_after_idle_seconds = @ensure_gc_after_idle / 1000.0 | ||
done = false | ||
while !done | ||
now = Process.clock_gettime(Process::CLOCK_MONOTONIC) | ||
|
||
if @disposed | ||
@ensure_gc_thread = nil | ||
break | ||
end | ||
|
||
if !@eval_thread && ensure_gc_after_idle_seconds < now - @last_eval | ||
@ensure_gc_mutex.synchronize do | ||
isolate_mutex.synchronize do | ||
if !@eval_thread | ||
low_memory_notification if !@disposed | ||
@ensure_gc_thread = nil | ||
done = true | ||
end | ||
end | ||
end | ||
end | ||
sleep ensure_gc_after_idle_seconds if !done | ||
end | ||
end | ||
end | ||
end | ||
|
||
def stop_attached | ||
@callback_mutex.synchronize{ | ||
if @callback_running | ||
@eval_thread.raise ScriptTerminatedError, "Terminated during callback" | ||
@thread_raise_called = true | ||
end | ||
} | ||
end | ||
|
||
def timeout(&blk) | ||
return blk.call unless @timeout | ||
|
||
mutex = Mutex.new | ||
done = false | ||
|
||
rp,wp = IO.pipe | ||
|
||
t = Thread.new do | ||
begin | ||
result = rp.wait_readable(@timeout/1000.0) | ||
if !result | ||
mutex.synchronize do | ||
stop unless done | ||
end | ||
end | ||
rescue => e | ||
STDERR.puts e | ||
STDERR.puts "FAILED TO TERMINATE DUE TO TIMEOUT" | ||
end | ||
end | ||
|
||
rval = blk.call | ||
mutex.synchronize do | ||
done = true | ||
end | ||
|
||
wp.close | ||
|
||
# ensure we do not leak a thread in state | ||
t.join | ||
t = nil | ||
|
||
rval | ||
ensure | ||
# exceptions need to be handled | ||
wp&.close | ||
t&.join | ||
rp&.close | ||
end | ||
|
||
def check_init_options!(isolate:, snapshot:, max_memory:, marshal_stack_depth:, ensure_gc_after_idle:, timeout:) | ||
assert_option_is_nil_or_a('isolate', isolate, Isolate) | ||
assert_option_is_nil_or_a('snapshot', snapshot, Snapshot) | ||
|
||
assert_numeric_or_nil('max_memory', max_memory, min_value: 10_000, max_value: 2**32-1) | ||
assert_numeric_or_nil('marshal_stack_depth', marshal_stack_depth, min_value: 1, max_value: MARSHAL_STACKDEPTH_MAX_VALUE) | ||
assert_numeric_or_nil('ensure_gc_after_idle', ensure_gc_after_idle, min_value: 1) | ||
assert_numeric_or_nil('timeout', timeout, min_value: 1) | ||
|
||
if isolate && snapshot | ||
raise ArgumentError, 'can only pass one of isolate and snapshot options' | ||
end | ||
end | ||
|
||
def assert_numeric_or_nil(option_name, object, min_value:, max_value: nil) | ||
if max_value && object.is_a?(Numeric) && object > max_value | ||
raise ArgumentError, "#{option_name} must be less than or equal to #{max_value}" | ||
end | ||
|
||
if object.is_a?(Numeric) && object < min_value | ||
raise ArgumentError, "#{option_name} must be larger than or equal to #{min_value}" | ||
end | ||
|
||
if !object.nil? && !object.is_a?(Numeric) | ||
raise ArgumentError, "#{option_name} must be a number, passed a #{object.inspect}" | ||
end | ||
end | ||
|
||
def assert_option_is_nil_or_a(option_name, object, klass) | ||
unless object.nil? || object.is_a?(klass) | ||
raise ArgumentError, "#{option_name} must be a #{klass} object, passed a #{object.inspect}" | ||
end | ||
end | ||
end | ||
|
||
# `size` and `warmup!` public methods are defined in the C class | ||
class Snapshot | ||
def initialize(str = '') | ||
# ensure it first can load | ||
begin | ||
ctx = MiniRacer::Context.new | ||
ctx.eval(str) | ||
rescue MiniRacer::RuntimeError => e | ||
raise MiniRacer::SnapshotError, e.message, e.backtrace | ||
end | ||
|
||
@source = str | ||
|
||
# defined in the C class | ||
load(str) | ||
end | ||
|
||
def warmup!(src) | ||
# we have to do something here | ||
# we are bloating memory a bit but it is more correct | ||
# than hitting an exception when attempty to compile invalid source | ||
begin | ||
ctx = MiniRacer::Context.new | ||
ctx.eval(@source) | ||
ctx.eval(src) | ||
rescue MiniRacer::RuntimeError => e | ||
raise MiniRacer::SnapshotError, e.message, e.backtrace | ||
end | ||
|
||
warmup_unsafe!(src) | ||
end | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FWIW, these are gone; the C code accepts
marshal_stack_depth
but otherwise ignores it.