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

Fix TruffleRuby backend after #325 #328

Merged
merged 13 commits into from
Jan 9, 2025
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
6 changes: 2 additions & 4 deletions lib/mini_racer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -75,14 +75,12 @@ def write_heap_snapshot(file_or_io)

if String === file_or_io
f = File.open(file_or_io, "w")
implicit = true
implicit = true
else
f = file_or_io
end

if !(File === f)
raise ArgumentError, "file_or_io"
end
raise ArgumentError, "file_or_io" unless File === f

f.write(heap_snapshot())
ensure
Expand Down
380 changes: 380 additions & 0 deletions lib/mini_racer/shared.rb
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
Comment on lines +8 to +9
Copy link
Collaborator

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.


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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

init_unsafe no longer exists

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does, it's defined in lib/mini_racer/truffleruby.rb.

end

def isolate
return @isolate if @isolate != false
# defined in the C class
@isolate = create_isolate_value
end
Comment on lines +137 to +141
Copy link
Collaborator

Choose a reason for hiding this comment

The 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)

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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).

Copy link
Collaborator

Choose a reason for hiding this comment

The 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)

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.

Copy link
Collaborator

Choose a reason for hiding this comment

The 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
Loading
Loading