Skip to content

Commit

Permalink
Fix the truffleruby backend by restoring the logic which used to be s…
Browse files Browse the repository at this point in the history
…hared in lib/mini_racer.rb

* See rubyjs#325
* I copied lib/mini_racer.rb from a268a2c (just before that PR)
  and removed the duplicated definitions with what's left on master in lib/mini_racer.rb.
* This brings it down to `5 failures, 6 errors` vs `10 failures, 60 errors` before.
  • Loading branch information
eregon committed Jan 8, 2025
1 parent 3db971f commit 577b4ae
Show file tree
Hide file tree
Showing 2 changed files with 382 additions and 0 deletions.
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

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} === 'undefined') { #{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)
end

def isolate
return @isolate if @isolate != false
# defined in the C class
@isolate = create_isolate_value
end

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
isolate.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.new, 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.new, e.message, e.backtrace
end

warmup_unsafe!(src)
end
end
end
2 changes: 2 additions & 0 deletions lib/mini_racer/truffleruby.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# frozen_string_literal: true

require_relative 'shared'

module MiniRacer

class Context
Expand Down

0 comments on commit 577b4ae

Please sign in to comment.