@@ -3,79 +3,202 @@ package com.scorbutics.rubyvm
33import kotlin.test.Test
44import kotlin.test.assertEquals
55import kotlin.test.assertTrue
6+ import kotlinx.atomicfu.atomic
67import kotlinx.cinterop.ExperimentalForeignApi
7- import kotlinx.cinterop.alloc
8- import kotlinx.cinterop.convert
9- import kotlinx.cinterop.memScoped
10- import kotlinx.cinterop.ptr
118import platform.posix.*
129
1310/* *
14- * Smoke tests for Ruby VM on Kotlin/Native.
11+ * Stress test for Ruby VM GC and signal handling on Kotlin/Native.
12+ *
13+ * This test verifies that the Ruby VM can handle:
14+ * 1. Rapid script execution with heavy Ruby object allocation
15+ * 2. Concurrent Ruby GC without crashes
16+ * 3. Signal masking preventing Ruby GC signals from crashing native threads
17+ *
18+ * Context: Ruby's GC uses signals (SIGPROF/SIGALRM) to pause threads during
19+ * marking phase. When these signals hit non-Ruby threads, they can cause segfaults.
20+ * This test ensures the signal masking in ruby_vm_execute_sync prevents this.
1521 *
1622 * Note: Ruby cannot be re-initialized after cleanup in the same process,
17- * so all tests share a single interpreter lifecycle within one test method.
23+ * so all test phases share a single interpreter lifecycle within one test method.
1824 */
1925@OptIn(ExperimentalForeignApi ::class )
20- class RubyVMSmokeTest {
26+ class RubyVMGCStressTest {
2127
2228 private fun createListener (): LogListener {
2329 return object : LogListener {
2430 override fun onLogMessage (logMessage : LogMessage ) {
2531 val prefix = if (logMessage.isError()) " ERROR" else " LOG"
26- println (" [$prefix ][${logMessage.source} ] ${logMessage.message} " )
32+ if (logMessage.isError() || logMessage.message.contains(" ERROR" ) || logMessage.message.contains(" WARN" )) {
33+ println (" [$prefix ][${logMessage.source} ] ${logMessage.message} " )
34+ }
2735 }
2836 }
2937 }
3038
3139 @Test
32- fun testInterpreterLifecycle () {
33- println (" === Test: Interpreter Lifecycle ===" )
40+ fun testGCStress () {
41+ println (" === Ruby VM GC Stress Test (Native) ===" )
3442
35- // Phase 1: Bootstrap and create
36- println (" Bootstrapping Ruby VM paths..." )
3743 val paths = RubyVMPaths .getDefaultPaths()
38- println (" rubyBaseDir: ${paths.rubyBaseDir} " )
39- println (" nativeLibsDir: ${paths.nativeLibsDir} " )
40-
41- println (" Creating interpreter..." )
4244 val interpreter = RubyInterpreter .create(
4345 appPath = " ." ,
4446 rubyBaseDir = paths.rubyBaseDir,
4547 nativeLibsDir = paths.nativeLibsDir,
4648 listener = createListener()
4749 )
48- println (" Interpreter created successfully" )
4950
5051 try {
51- // Phase 2: Execute a single script
52- val latch = NativeCountDownLatch (1 )
53- var resultCode = - 1
52+ // Phase 1: Rapid script execution with GC
53+ testRapidScriptExecutionWithGC(interpreter)
54+
55+ // Phase 2: Synchronous execution with GC
56+ testSynchronousExecutionWithGC(interpreter)
57+
58+ // Phase 3: Memory pressure with GC
59+ testMemoryPressureWithGC(interpreter)
60+
61+ } finally {
62+ println ()
63+ println (" Cleaning up interpreter..." )
64+ interpreter.destroy()
65+ println (" Cleanup complete" )
66+ }
67+ }
68+
69+ private fun testRapidScriptExecutionWithGC (interpreter : RubyInterpreter ) {
70+ println ()
71+ println (" --- Phase 1: Rapid Script Execution with GC ---" )
72+
73+ val scriptCount = 50
74+ val completedCount = atomic(0 )
75+ val failureCount = atomic(0 )
76+ val latch = NativeCountDownLatch (scriptCount)
5477
55- val script = RubyScript .fromContent(" puts 'Hello from Ruby on Kotlin/Native!'" )
56- println (" Enqueuing script..." )
57- fflush(null )
78+ println (" Starting stress test with $scriptCount scripts..." )
79+
80+ repeat(scriptCount) { i ->
81+ val script = RubyScript .fromContent("""
82+ # Script $i - Do some work and trigger GC
83+ sum = 0
84+ 100.times do |n|
85+ sum += n
86+ # Allocate some objects to trigger GC
87+ arr = Array.new(100) { |x| "string_#{x}_#{n}" }
88+ end
89+
90+ # Explicitly trigger Ruby GC on some scripts
91+ ${if (i % 5 == 0 ) " GC.start" else " # No explicit GC" }
92+
93+ puts "Script $i completed: sum=#{sum}"
94+ """ .trimIndent())
5895
5996 interpreter.enqueue(script) { exitCode ->
60- println (" Script completed with exit code: $exitCode " )
61- resultCode = exitCode
97+ if (exitCode == 0 ) {
98+ completedCount.incrementAndGet()
99+ } else {
100+ failureCount.incrementAndGet()
101+ println (" Script $i FAILED with exit code: $exitCode " )
102+ }
62103 latch.countDown()
63104 script.close()
64105 }
65106
66- println ( " Enqueue returned, waiting for script (timeout: 30s)... " )
67- fflush( null )
68- val completed = latch.await( 30 )
107+ // Small delay to stagger script submissions
108+ usleep( 10_000u ) // 10ms
109+ }
69110
70- assertTrue(completed, " Script did not complete within 30 seconds" )
71- assertEquals(0 , resultCode, " Script exited with non-zero code" )
72- println (" Single script execution PASSED" )
111+ println (" Waiting for scripts to complete..." )
112+ val completed = latch.await(60 )
73113
74- } finally {
75- // Phase 3: Destroy
76- println (" Destroying interpreter..." )
77- interpreter.destroy()
78- println (" Interpreter destroyed successfully" )
114+ println ()
115+ println (" Phase 1 Results:" )
116+ println (" Scripts completed: ${completedCount.value} /$scriptCount " )
117+ println (" Scripts failed: ${failureCount.value} " )
118+ println (" Timeout: ${! completed} " )
119+
120+ assertTrue(completed, " Test timed out waiting for scripts to complete" )
121+ assertEquals(0 , failureCount.value, " Some scripts failed" )
122+ assertEquals(scriptCount, completedCount.value, " Not all scripts completed" )
123+
124+ println (" Phase 1 PASSED" )
125+ }
126+
127+ private fun testSynchronousExecutionWithGC (interpreter : RubyInterpreter ) {
128+ println ()
129+ println (" --- Phase 2: Synchronous Execution with GC ---" )
130+
131+ val completedCount = atomic(0 )
132+ val scriptCount = 20
133+ val latch = NativeCountDownLatch (scriptCount)
134+
135+ repeat(scriptCount) { i ->
136+ val script = RubyScript .fromContent("""
137+ puts "Sync script $i starting"
138+ arr = []
139+ 1000.times { |n| arr << "item_#{n}" }
140+ ${if (i % 3 == 0 ) " GC.start" else " " }
141+ puts "Sync script $i completed"
142+ """ .trimIndent())
143+
144+ interpreter.enqueue(script) { exitCode ->
145+ println (" Script $i completed with exit code: $exitCode " )
146+ completedCount.incrementAndGet()
147+ latch.countDown()
148+ script.close()
149+ }
150+
151+ usleep(50_000u ) // 50ms
152+ }
153+
154+ println (" Waiting for all callbacks to complete..." )
155+ val completed = latch.await(60 )
156+ assertTrue(completed, " Test timed out waiting for callbacks" )
157+
158+ println ()
159+ println (" Phase 2 PASSED (completed: ${completedCount.value} /$scriptCount )" )
160+ }
161+
162+ private fun testMemoryPressureWithGC (interpreter : RubyInterpreter ) {
163+ println ()
164+ println (" --- Phase 3: Memory Pressure + GC ---" )
165+
166+ val scriptCount = 10
167+ val latch = NativeCountDownLatch (scriptCount)
168+ val errors = atomic(0 )
169+
170+ println (" Executing memory-intensive scripts..." )
171+
172+ repeat(scriptCount) { i ->
173+ val script = RubyScript .fromContent("""
174+ # Allocate significant memory
175+ big_arrays = []
176+ 10.times do
177+ arr = Array.new(10000) { |n| "string_data_#{n}_#{rand(1000)}" }
178+ big_arrays << arr
179+ GC.start if rand(3) == 0
180+ end
181+
182+ # Force full GC
183+ GC.start
184+
185+ puts "Memory test $i complete"
186+ """ .trimIndent())
187+
188+ interpreter.enqueue(script) { exitCode ->
189+ if (exitCode != 0 ) {
190+ errors.incrementAndGet()
191+ }
192+ latch.countDown()
193+ script.close()
194+ }
195+
196+ usleep(100_000u ) // 100ms
79197 }
198+
199+ assertTrue(latch.await(30 ), " Memory pressure test timed out" )
200+ assertEquals(0 , errors.value, " Scripts failed under memory pressure" )
201+
202+ println (" Phase 3 PASSED" )
80203 }
81204}
0 commit comments