Skip to content

Commit 080836f

Browse files
eregonSamSaffronbnoordhuis
authored
Fix TruffleRuby backend after #325 (#328)
* Cleanup code in lib/mini_racer.rb and remove tabs * Fix the truffleruby backend by restoring the logic which used to be shared in lib/mini_racer.rb * See #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. * Revert "Add MiniRacer::Platform.set_flags! for the truffleruby backend (#326)" * This reverts commit a268a2c. * Now it's defined in "shared" code like before. * Move #low_memory_notification and #idle_notification from Isolate to Context * Adjust to MiniRacer::SnapshotError#initialize changes * Support overwriting for #attach for the new #test_attach_non_object test * Pass MiniRacerTest#test_estimated_size_when_disposed on truffleruby * Skip a failing test which seems hard to fix * Convert JS Map to Ruby Hash and handle Map Iterator * Also improve test for clarity. * Exclude CRuby-only test * Tweak #test_symbol_support to allow the original behavior * Until the desired behavior is clarified. * Extend #test_map and fix behavior for the Map#values() case * Update test/mini_racer_test.rb Co-authored-by: Ben Noordhuis <[email protected]> --------- Co-authored-by: Sam <[email protected]> Co-authored-by: Ben Noordhuis <[email protected]>
1 parent 0017e57 commit 080836f

File tree

4 files changed

+447
-52
lines changed

4 files changed

+447
-52
lines changed

lib/mini_racer.rb

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,14 +75,12 @@ def write_heap_snapshot(file_or_io)
7575

7676
if String === file_or_io
7777
f = File.open(file_or_io, "w")
78-
implicit = true
78+
implicit = true
7979
else
8080
f = file_or_io
8181
end
8282

83-
if !(File === f)
84-
raise ArgumentError, "file_or_io"
85-
end
83+
raise ArgumentError, "file_or_io" unless File === f
8684

8785
f.write(heap_snapshot())
8886
ensure

lib/mini_racer/shared.rb

Lines changed: 380 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,380 @@
1+
# This code used to be shared in lib/mini_racer.rb
2+
# but was moved to the extension with https://github.com/rubyjs/mini_racer/pull/325.
3+
# So now this is effectively duplicate logic with C/C++ code.
4+
# Maybe one day it can be actually shared again between both backends.
5+
6+
module MiniRacer
7+
8+
MARSHAL_STACKDEPTH_DEFAULT = 2**9-2
9+
MARSHAL_STACKDEPTH_MAX_VALUE = 2**10-2
10+
11+
class FailedV8Conversion
12+
attr_reader :info
13+
def initialize(info)
14+
@info = info
15+
end
16+
end
17+
18+
# helper class returned when we have a JavaScript function
19+
class JavaScriptFunction
20+
def to_s
21+
"JavaScript Function"
22+
end
23+
end
24+
25+
class Isolate
26+
def initialize(snapshot = nil)
27+
unless snapshot.nil? || snapshot.is_a?(Snapshot)
28+
raise ArgumentError, "snapshot must be a Snapshot object, passed a #{snapshot.inspect}"
29+
end
30+
31+
# defined in the C class
32+
init_with_snapshot(snapshot)
33+
end
34+
end
35+
36+
class Platform
37+
class << self
38+
def set_flags!(*args, **kwargs)
39+
flags_to_strings([args, kwargs]).each do |flag|
40+
# defined in the C class
41+
set_flag_as_str!(flag)
42+
end
43+
end
44+
45+
private
46+
47+
def flags_to_strings(flags)
48+
flags.flatten.map { |flag| flag_to_string(flag) }.flatten
49+
end
50+
51+
# normalize flags to strings, and adds leading dashes if needed
52+
def flag_to_string(flag)
53+
if flag.is_a?(Hash)
54+
flag.map do |key, value|
55+
"#{flag_to_string(key)} #{value}"
56+
end
57+
else
58+
str = flag.to_s
59+
str = "--#{str}" unless str.start_with?('--')
60+
str
61+
end
62+
end
63+
end
64+
end
65+
66+
# eval is defined in the C class
67+
class Context
68+
69+
class ExternalFunction
70+
def initialize(name, callback, parent)
71+
unless String === name
72+
raise ArgumentError, "parent_object must be a String"
73+
end
74+
parent_object, _ , @name = name.rpartition(".")
75+
@callback = callback
76+
@parent = parent
77+
@parent_object_eval = nil
78+
@parent_object = nil
79+
80+
unless parent_object.empty?
81+
@parent_object = parent_object
82+
83+
@parent_object_eval = ""
84+
prev = ""
85+
first = true
86+
parent_object.split(".").each do |obj|
87+
prev << obj
88+
if first
89+
@parent_object_eval << "if (typeof #{prev} !== 'object' || typeof #{prev} !== 'function') { #{prev} = {} };\n"
90+
else
91+
@parent_object_eval << "#{prev} = #{prev} || {};\n"
92+
end
93+
prev << "."
94+
first = false
95+
end
96+
@parent_object_eval << "#{parent_object};"
97+
end
98+
notify_v8
99+
end
100+
end
101+
102+
def initialize(max_memory: nil, timeout: nil, isolate: nil, ensure_gc_after_idle: nil, snapshot: nil, marshal_stack_depth: nil)
103+
options ||= {}
104+
105+
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)
106+
107+
@functions = {}
108+
@timeout = nil
109+
@max_memory = nil
110+
@current_exception = nil
111+
@timeout = timeout
112+
@max_memory = max_memory
113+
@marshal_stack_depth = marshal_stack_depth
114+
115+
# false signals it should be fetched if requested
116+
@isolate = isolate || false
117+
118+
@ensure_gc_after_idle = ensure_gc_after_idle
119+
120+
if @ensure_gc_after_idle
121+
@last_eval = nil
122+
@ensure_gc_thread = nil
123+
@ensure_gc_mutex = Mutex.new
124+
end
125+
126+
@disposed = false
127+
128+
@callback_mutex = Mutex.new
129+
@callback_running = false
130+
@thread_raise_called = false
131+
@eval_thread = nil
132+
133+
# defined in the C class
134+
init_unsafe(isolate, snapshot)
135+
end
136+
137+
def isolate
138+
return @isolate if @isolate != false
139+
# defined in the C class
140+
@isolate = create_isolate_value
141+
end
142+
143+
def eval(str, options=nil)
144+
raise(ContextDisposedError, 'attempted to call eval on a disposed context!') if @disposed
145+
146+
filename = options && options[:filename].to_s
147+
148+
@eval_thread = Thread.current
149+
isolate_mutex.synchronize do
150+
@current_exception = nil
151+
timeout do
152+
eval_unsafe(str, filename)
153+
end
154+
end
155+
ensure
156+
@eval_thread = nil
157+
ensure_gc_thread if @ensure_gc_after_idle
158+
end
159+
160+
def call(function_name, *arguments)
161+
raise(ContextDisposedError, 'attempted to call function on a disposed context!') if @disposed
162+
163+
@eval_thread = Thread.current
164+
isolate_mutex.synchronize do
165+
timeout do
166+
call_unsafe(function_name, *arguments)
167+
end
168+
end
169+
ensure
170+
@eval_thread = nil
171+
ensure_gc_thread if @ensure_gc_after_idle
172+
end
173+
174+
def dispose
175+
return if @disposed
176+
isolate_mutex.synchronize do
177+
return if @disposed
178+
dispose_unsafe
179+
@disposed = true
180+
@isolate = nil # allow it to be garbage collected, if set
181+
end
182+
end
183+
184+
185+
def attach(name, callback)
186+
raise(ContextDisposedError, 'attempted to call function on a disposed context!') if @disposed
187+
188+
wrapped = lambda do |*args|
189+
begin
190+
191+
r = nil
192+
193+
begin
194+
@callback_mutex.synchronize{
195+
@callback_running = true
196+
}
197+
r = callback.call(*args)
198+
ensure
199+
@callback_mutex.synchronize{
200+
@callback_running = false
201+
}
202+
end
203+
204+
# wait up to 2 seconds for this to be interrupted
205+
# will very rarely be called cause #raise is called
206+
# in another mutex
207+
@callback_mutex.synchronize {
208+
if @thread_raise_called
209+
sleep 2
210+
end
211+
}
212+
213+
r
214+
215+
ensure
216+
@callback_mutex.synchronize {
217+
@thread_raise_called = false
218+
}
219+
end
220+
end
221+
222+
isolate_mutex.synchronize do
223+
external = ExternalFunction.new(name, wrapped, self)
224+
@functions["#{name}"] = external
225+
end
226+
end
227+
228+
private
229+
230+
def ensure_gc_thread
231+
@last_eval = Process.clock_gettime(Process::CLOCK_MONOTONIC)
232+
@ensure_gc_mutex.synchronize do
233+
@ensure_gc_thread = nil if !@ensure_gc_thread&.alive?
234+
return if !Thread.main.alive? # avoid "can't alloc thread" exception
235+
@ensure_gc_thread ||= Thread.new do
236+
ensure_gc_after_idle_seconds = @ensure_gc_after_idle / 1000.0
237+
done = false
238+
while !done
239+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
240+
241+
if @disposed
242+
@ensure_gc_thread = nil
243+
break
244+
end
245+
246+
if !@eval_thread && ensure_gc_after_idle_seconds < now - @last_eval
247+
@ensure_gc_mutex.synchronize do
248+
isolate_mutex.synchronize do
249+
if !@eval_thread
250+
low_memory_notification if !@disposed
251+
@ensure_gc_thread = nil
252+
done = true
253+
end
254+
end
255+
end
256+
end
257+
sleep ensure_gc_after_idle_seconds if !done
258+
end
259+
end
260+
end
261+
end
262+
263+
def stop_attached
264+
@callback_mutex.synchronize{
265+
if @callback_running
266+
@eval_thread.raise ScriptTerminatedError, "Terminated during callback"
267+
@thread_raise_called = true
268+
end
269+
}
270+
end
271+
272+
def timeout(&blk)
273+
return blk.call unless @timeout
274+
275+
mutex = Mutex.new
276+
done = false
277+
278+
rp,wp = IO.pipe
279+
280+
t = Thread.new do
281+
begin
282+
result = rp.wait_readable(@timeout/1000.0)
283+
if !result
284+
mutex.synchronize do
285+
stop unless done
286+
end
287+
end
288+
rescue => e
289+
STDERR.puts e
290+
STDERR.puts "FAILED TO TERMINATE DUE TO TIMEOUT"
291+
end
292+
end
293+
294+
rval = blk.call
295+
mutex.synchronize do
296+
done = true
297+
end
298+
299+
wp.close
300+
301+
# ensure we do not leak a thread in state
302+
t.join
303+
t = nil
304+
305+
rval
306+
ensure
307+
# exceptions need to be handled
308+
wp&.close
309+
t&.join
310+
rp&.close
311+
end
312+
313+
def check_init_options!(isolate:, snapshot:, max_memory:, marshal_stack_depth:, ensure_gc_after_idle:, timeout:)
314+
assert_option_is_nil_or_a('isolate', isolate, Isolate)
315+
assert_option_is_nil_or_a('snapshot', snapshot, Snapshot)
316+
317+
assert_numeric_or_nil('max_memory', max_memory, min_value: 10_000, max_value: 2**32-1)
318+
assert_numeric_or_nil('marshal_stack_depth', marshal_stack_depth, min_value: 1, max_value: MARSHAL_STACKDEPTH_MAX_VALUE)
319+
assert_numeric_or_nil('ensure_gc_after_idle', ensure_gc_after_idle, min_value: 1)
320+
assert_numeric_or_nil('timeout', timeout, min_value: 1)
321+
322+
if isolate && snapshot
323+
raise ArgumentError, 'can only pass one of isolate and snapshot options'
324+
end
325+
end
326+
327+
def assert_numeric_or_nil(option_name, object, min_value:, max_value: nil)
328+
if max_value && object.is_a?(Numeric) && object > max_value
329+
raise ArgumentError, "#{option_name} must be less than or equal to #{max_value}"
330+
end
331+
332+
if object.is_a?(Numeric) && object < min_value
333+
raise ArgumentError, "#{option_name} must be larger than or equal to #{min_value}"
334+
end
335+
336+
if !object.nil? && !object.is_a?(Numeric)
337+
raise ArgumentError, "#{option_name} must be a number, passed a #{object.inspect}"
338+
end
339+
end
340+
341+
def assert_option_is_nil_or_a(option_name, object, klass)
342+
unless object.nil? || object.is_a?(klass)
343+
raise ArgumentError, "#{option_name} must be a #{klass} object, passed a #{object.inspect}"
344+
end
345+
end
346+
end
347+
348+
# `size` and `warmup!` public methods are defined in the C class
349+
class Snapshot
350+
def initialize(str = '')
351+
# ensure it first can load
352+
begin
353+
ctx = MiniRacer::Context.new
354+
ctx.eval(str)
355+
rescue MiniRacer::RuntimeError => e
356+
raise MiniRacer::SnapshotError, e.message, e.backtrace
357+
end
358+
359+
@source = str
360+
361+
# defined in the C class
362+
load(str)
363+
end
364+
365+
def warmup!(src)
366+
# we have to do something here
367+
# we are bloating memory a bit but it is more correct
368+
# than hitting an exception when attempty to compile invalid source
369+
begin
370+
ctx = MiniRacer::Context.new
371+
ctx.eval(@source)
372+
ctx.eval(src)
373+
rescue MiniRacer::RuntimeError => e
374+
raise MiniRacer::SnapshotError, e.message, e.backtrace
375+
end
376+
377+
warmup_unsafe!(src)
378+
end
379+
end
380+
end

0 commit comments

Comments
 (0)