speednode 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ab3e330c3cd4bf4518c54e6666ff13ee722b553e4de7d8be2602b984811da9fc
4
+ data.tar.gz: '08218aa8e443770808a35de088c869bfc0d227493beba5f60b98ddcdfe35b51a'
5
+ SHA512:
6
+ metadata.gz: 3f41ebd4a9b509dc81f815c7dff64ff3ce8ae15a57fa085038ce5cdb139ab37f0f0b26cded89f52450e4e39a0f69afd98238a7ed02d20db50e7d395872617f67
7
+ data.tar.gz: cb7d8667d95403426e7964e4e2be7d1608a3d2ad43fe1b36a694f125bd9595dfe3ba5b73d983d57215fdc637b2a141d8536f642435082005cbbdc5487af1b2e1
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2016 John Hawthorn
4
+ Copyright (c) 2019-2024 Jan Biedermann
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,128 @@
1
+ # Speednode
2
+
3
+ A fast runtime for ExecJS using node js. Works on Linux, BSDs, MacOS and Windows.
4
+ Inspired by [execjs-fastnode](https://github.com/jhawthorn/execjs-fastnode).
5
+
6
+ ### Installation
7
+
8
+ In Gemfile:
9
+ `gem 'speednode'`, then `bundle install`
10
+
11
+ ### Configuration
12
+
13
+ Speednode provides one node based runtime `Speednode` which runs scripts in node vms.
14
+ The runtime can be chosen by:
15
+
16
+ ```ruby
17
+ ExecJS.runtime = ExecJS::Runtimes::Speednode
18
+ ```
19
+ If node cant find node modules for the permissive contexts (see below), its possible to set the load path before assigning the runtime:
20
+ ```ruby
21
+ ENV['NODE_PATH'] = './node_modules'
22
+ ```
23
+
24
+ ### Contexts
25
+
26
+ Each ExecJS context runs in a node vm. Speednode offers two kinds of contexts:
27
+ - a compatible context, which is compatible with default ExecJS behavior.
28
+ - a permissive context, which is more permissive and allows to `require` node modules.
29
+
30
+ #### Compatible
31
+ A compatible context can be created with the standard `ExecJS.compile` or code can be executed within a compatible context by using the standard `ExecJS.eval` or `ExecJS.exec`.
32
+ Example for a compatible context:
33
+ ```ruby
34
+ compat_context = ExecJS.compile('Test = "test"')
35
+ compat_context.eval('1+1')
36
+ ```
37
+ #### Permissive
38
+ A permissive context can be created with `ExecJS.permissive_compile` or code can be executed within a permissive context by using
39
+ `ExecJS.permissive_eval` or `ExecJS.permissive_exec`.
40
+ Example for a permissive context:
41
+ ```ruby
42
+ perm_context = ExecJS.permissive_compile('Test = "test"')
43
+ perm_context.eval('1+1')
44
+ ```
45
+ Evaluation in a permissive context:
46
+ ```ruby
47
+ ExecJS.permissive_eval('1+1')
48
+ ```
49
+
50
+ #### Stopping Contexts
51
+ Contexts can be stopped programmatically. If all contexts of a VM are stopped, the VM itself will be shut down with Node exiting, freeing memory and resources.
52
+ ```ruby
53
+ context = ExecJS.compile('Test = "test"') # will start a node process
54
+ ExecJS::Runtimes::Speednode.stop_context(context) # will kill the node process
55
+ ```
56
+
57
+ ### Precompiling and Storing scripts for repeated execution
58
+
59
+ Scripts can be precompiled and stored for repeated execution, which leads to a significant performance improvement, especially for larger scripts:
60
+ ```ruby
61
+ context = ExecJS.compile('Test = "test"')
62
+ context.add_script(key: 'super', source: some_large_javascript) # will compile and store the script
63
+ context.eval_script(key: 'super') # will run the precompiled script in the context
64
+ ```
65
+ For the actual performance benefit see below.
66
+
67
+ ### Async function support
68
+
69
+ Its possible to call async functions synchronously from ruby using Context#await:
70
+ ```ruby
71
+ context = ExecJS.compile('')
72
+ context.eval <<~JAVASCRIPT
73
+ async function foo(val) {
74
+ return new Promise(function (resolve, reject) { resolve(val); });
75
+ }
76
+ JAVASCRIPT
77
+
78
+ context.await("foo('test')") # => 'test'
79
+ ```
80
+
81
+ ### Attaching ruby methods to Permissive Contexts
82
+
83
+ Ruby methods can be attached to Permissive Contexts using Context#attach:
84
+ ```ruby
85
+ context = ExecJS.permissive_compile(SOURCE)
86
+ context.attach('foo') { |v| v }
87
+ context.await('foo("bar")')
88
+ ```
89
+ The attached method is reflected in the context by a async javascript function. From within javascript the ruby method is best called using await:
90
+ ```javascript
91
+ r = await foo('test');
92
+ ```
93
+ or via context#await as in above example.
94
+ Attaching and calling ruby methods to/from permissive contexts is not that fast. It is recommended to use it sparingly.
95
+
96
+ ### Benchmarks
97
+
98
+ Highly scientific, maybe.
99
+
100
+ 1000 rounds using speednode 0.8.0 with node 20.11.0 on a older CPU on Linux
101
+ (ctx = using context, scsc = using precompiled scripts):
102
+ ```
103
+ ExecJS CoffeeScript eval: real
104
+ Speednode Node.js (V8): 0.324355
105
+ Speednode Node.js (V8) ctx: 0.276933
106
+ Speednode Node.js (V8) scsc: 0.239678
107
+ mini_racer (0.8.0): 0.204274
108
+ mini_racer (0.8.0) ctx: 0.134327
109
+
110
+ Eval overhead benchmark: real
111
+ Speednode Node.js (V8): 0.064198
112
+ Speednode Node.js (V8) ctx: 0.065521
113
+ Speednode Node.js (V8) scsc: 0.050652
114
+ mini_racer (0.8.0): 0.010653
115
+ mini_racer (0.8.0) ctx: 0.007764
116
+ ```
117
+
118
+ To run benchmarks:
119
+ - clone repo
120
+ - `bundle install`
121
+ - `bundle exec rake bench`
122
+
123
+ ### Tests
124
+
125
+ To run tests:
126
+ - clone repo
127
+ - `bundle install`
128
+ - `bundle exec rake`
@@ -0,0 +1,224 @@
1
+ require 'ffi'
2
+
3
+ module Speednode
4
+ module WindowsyThings
5
+ extend FFI::Library
6
+
7
+ ffi_lib :kernel32, :user32
8
+
9
+ ERROR_IO_PENDING = 997
10
+ ERROR_PIPE_CONNECTED = 535
11
+ ERROR_SUCCESS = 0
12
+
13
+ FILE_FLAG_OVERLAPPED = 0x40000000
14
+
15
+ INFINITE = 0xFFFFFFFF
16
+ INVALID_HANDLE_VALUE = FFI::Pointer.new(-1).address
17
+
18
+ PIPE_ACCESS_DUPLEX = 0x00000003
19
+ PIPE_READMODE_BYTE = 0x00000000
20
+ PIPE_READMODE_MESSAGE = 0x00000002
21
+ PIPE_TYPE_BYTE = 0x00000000
22
+ PIPE_TYPE_MESSAGE = 0x00000004
23
+ PIPE_WAIT = 0x00000000
24
+
25
+ QS_ALLINPUT = 0x04FF
26
+
27
+ typedef :uintptr_t, :handle
28
+
29
+ attach_function :ConnectNamedPipe, [:handle, :pointer], :ulong
30
+ attach_function :CreateEvent, :CreateEventA, [:pointer, :ulong, :ulong, :string], :handle
31
+ attach_function :CreateNamedPipe, :CreateNamedPipeA, [:string, :ulong, :ulong, :ulong, :ulong, :ulong, :ulong, :pointer], :handle
32
+ attach_function :DisconnectNamedPipe, [:handle], :bool
33
+ attach_function :FlushFileBuffers, [:handle], :bool
34
+ attach_function :GetLastError, [], :ulong
35
+ attach_function :GetOverlappedResult, [:handle, :pointer, :pointer, :bool], :bool
36
+ attach_function :MsgWaitForMultipleObjects, [:ulong, :pointer, :ulong, :ulong, :ulong], :ulong
37
+ attach_function :ReadFile, [:handle, :buffer_out, :ulong, :pointer, :pointer], :bool
38
+ attach_function :SetEvent, [:handle], :bool
39
+ attach_function :WaitForMultipleObjects, [:ulong, :pointer, :ulong, :ulong], :ulong
40
+ attach_function :WriteFile, [:handle, :buffer_in, :ulong, :pointer, :pointer], :bool
41
+ end
42
+
43
+ class AttachPipe
44
+ include Speednode::WindowsyThings
45
+
46
+ CONNECTING_STATE = 0
47
+ READING_STATE = 1
48
+ WRITING_STATE = 2
49
+ INSTANCES = 4
50
+ PIPE_TIMEOUT = 5000
51
+ BUFFER_SIZE = 65536
52
+
53
+ class Overlapped < FFI::Struct
54
+ layout(
55
+ :Internal, :uintptr_t,
56
+ :InternalHigh, :uintptr_t,
57
+ :Offset, :ulong,
58
+ :OffsetHigh, :ulong,
59
+ :hEvent, :uintptr_t
60
+ )
61
+ end
62
+
63
+ def initialize(pipe_name, block)
64
+ @run_block = block
65
+ @full_pipe_name = "\\\\.\\pipe\\#{pipe_name}"
66
+ @instances = 1
67
+ @events = []
68
+ @events_pointer = FFI::MemoryPointer.new(:uintptr_t, @instances + 1)
69
+ @pipe = {}
70
+ end
71
+
72
+ def run
73
+ @running = true
74
+ create_instance
75
+ while_loop
76
+ end
77
+
78
+ def stop
79
+ @running = false
80
+ warn("DisconnectNamedPipe failed with #{GetLastError()}") if !DisconnectNamedPipe(@pipe[:instance])
81
+ end
82
+
83
+ private
84
+
85
+ def create_instance
86
+ @events[0] = CreateEvent(nil, 1, 1, nil)
87
+ raise "CreateEvent failed with #{GetLastError()}" unless @events[0]
88
+
89
+ overlap = Overlapped.new
90
+ overlap[:hEvent] = @events[0]
91
+
92
+ @pipe = { overlap: overlap, instance: nil, request: FFI::Buffer.new(1, BUFFER_SIZE), bytes_read: 0, reply: FFI::Buffer.new(1, BUFFER_SIZE), bytes_to_write: 0, state: nil, pending_io: false }
93
+ @pipe[:instance] = CreateNamedPipe(@full_pipe_name,
94
+ PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED,
95
+ PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT,
96
+ 4,
97
+ BUFFER_SIZE,
98
+ BUFFER_SIZE,
99
+ PIPE_TIMEOUT,
100
+ nil)
101
+ raise "CreateNamedPipe failed with #{GetLastError()}" if @pipe[:instance] == INVALID_HANDLE_VALUE
102
+ @pipe[:pending_io] = connect_to_new_client
103
+ @pipe[:state] = @pipe[:pending_io] ? CONNECTING_STATE : READING_STATE
104
+
105
+ @events_pointer.write_array_of_ulong_long(@events)
106
+ nil
107
+ end
108
+
109
+ def while_loop
110
+ while @running
111
+ # this sleep gives other ruby threads a chance to run
112
+ # ~10ms is a ruby thread time slice, so we choose something a bit larger
113
+ # that ruby or the os is free to switch threads
114
+ sleep 0.010 if @pipe[:state] != WRITING_STATE && @pipe[:state] != READING_STATE
115
+
116
+ i = MsgWaitForMultipleObjects(@instances, @events_pointer, 0, 1, QS_ALLINPUT) if @pipe[:state] != WRITING_STATE
117
+
118
+ if i > 0
119
+ next
120
+ end
121
+
122
+ if @pipe[:pending_io]
123
+ bytes_transferred = FFI::MemoryPointer.new(:ulong)
124
+ success = GetOverlappedResult(@pipe[:instance], @pipe[:overlap], bytes_transferred, false)
125
+
126
+ case @pipe[:state]
127
+ when CONNECTING_STATE
128
+ raise "Error #{GetLastError()}" unless success
129
+ @pipe[:state] = READING_STATE
130
+ when READING_STATE
131
+ if !success || bytes_transferred.read_ulong == 0
132
+ disconnect_and_reconnect(i)
133
+ next
134
+ else
135
+ @pipe[:bytes_read] = bytes_transferred.read_ulong
136
+ @pipe[:state] = WRITING_STATE
137
+ end
138
+ when WRITING_STATE
139
+ if !success || bytes_transferred.read_ulong != @pipe[:bytes_to_write]
140
+ disconnect_and_reconnect(i)
141
+ next
142
+ else
143
+ @pipe[:state] = READING_STATE
144
+ end
145
+ else
146
+ raise "Invalid pipe state."
147
+ end
148
+ end
149
+
150
+ case @pipe[:state]
151
+ when READING_STATE
152
+ bytes_read = FFI::MemoryPointer.new(:ulong)
153
+ success = ReadFile(@pipe[:instance], @pipe[:request], BUFFER_SIZE, bytes_read, @pipe[:overlap].to_ptr)
154
+ if success && bytes_read.read_ulong != 0
155
+ @pipe[:pending_io] = false
156
+ @pipe[:state] = WRITING_STATE
157
+ next
158
+ end
159
+
160
+ err = GetLastError()
161
+ if !success && err == ERROR_IO_PENDING
162
+ @pipe[:pending_io] = true
163
+ next
164
+ end
165
+
166
+ disconnect_and_reconnect
167
+ when WRITING_STATE
168
+ @pipe[:reply] = @run_block.call(@pipe[:request].get_string(0))
169
+ @pipe[:bytes_to_write] = @pipe[:reply].bytesize
170
+ bytes_written = FFI::MemoryPointer.new(:ulong)
171
+ success = WriteFile(@pipe[:instance], @pipe[:reply], @pipe[:bytes_to_write], bytes_written, @pipe[:overlap].to_ptr)
172
+
173
+ if success && bytes_written.read_ulong == @pipe[:bytes_to_write]
174
+ @pipe[:pending_io] = false
175
+ @pipe[:state] = READING_STATE
176
+ next
177
+ end
178
+
179
+ err = GetLastError()
180
+
181
+ if !success && err == ERROR_IO_PENDING
182
+ @pipe[:pending_io] = true
183
+ next
184
+ end
185
+
186
+ disconnect_and_reconnect
187
+ else
188
+ raise "Invalid pipe state."
189
+ end
190
+ end
191
+ end
192
+
193
+ def disconnect_and_reconnect
194
+ FlushFileBuffers(@pipe[:instance])
195
+ warn("DisconnectNamedPipe failed with #{GetLastError()}") if !DisconnectNamedPipe(@pipe[:instance])
196
+
197
+ @pipe[:pending_io] = connect_to_new_client
198
+
199
+ @pipe[:state] = @pipe[:pending_io] ? CONNECTING_STATE : READING_STATE
200
+ end
201
+
202
+ def connect_to_new_client
203
+ pending_io = false
204
+ @pipe[:request].clear
205
+ @pipe[:reply].clear
206
+ connected = ConnectNamedPipe(@pipe[:instance], @pipe[:overlap].to_ptr)
207
+ last_error = GetLastError()
208
+ raise "ConnectNamedPipe failed with #{last_error} - #{connected}" if connected != 0
209
+
210
+ case last_error
211
+ when ERROR_IO_PENDING
212
+ pending_io = true
213
+ when ERROR_PIPE_CONNECTED
214
+ SetEvent(@pipe[:overlap][:hEvent])
215
+ when ERROR_SUCCESS
216
+ pending_io = true
217
+ else
218
+ raise "ConnectNamedPipe failed with error #{last_error}"
219
+ end
220
+
221
+ pending_io
222
+ end
223
+ end
224
+ end
@@ -0,0 +1,43 @@
1
+ require 'socket'
2
+
3
+ module Speednode
4
+ class AttachSocket
5
+
6
+ attr_reader :socket
7
+
8
+ def initialize(socket_path, block)
9
+ @socket_path = socket_path
10
+ @run_block = block
11
+ end
12
+
13
+ def run
14
+ @running = true
15
+ client = nil
16
+ ret = nil
17
+ @socket = UNIXServer.new(@socket_path)
18
+
19
+ while @running do
20
+ if ret
21
+ begin
22
+ client = @socket.accept_nonblock
23
+ request = client.gets("\x04")
24
+ result = @run_block.call(request)
25
+ client.write result
26
+ client.flush
27
+ client.close
28
+ rescue Errno::EAGAIN, Errno::EWOULDBLOCK
29
+ end
30
+ end
31
+ ret = begin
32
+ IO.select([@socket], nil, nil, nil) || next
33
+ rescue Errno::EBADF
34
+ end
35
+ end
36
+ end
37
+
38
+ def stop
39
+ @running = false
40
+ @socket.close
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,23 @@
1
+ module Speednode
2
+ class << self
3
+ attr_accessor :node_paths
4
+
5
+ def set_node_paths
6
+ np_sep = Gem.win_platform? ? ';' : ':'
7
+ existing_node_path = ENV['NODE_PATH']
8
+ temp_node_path = ''
9
+ if existing_node_path.nil? || existing_node_path.empty?
10
+ temp_node_path = Speednode.node_paths.join(np_sep)
11
+ else
12
+ if existing_node_path.end_with?(np_sep)
13
+ temp_node_path = existing_node_path + Speednode.node_paths.join(np_sep)
14
+ else
15
+ temp_node_path = existing_node_path + np_sep + Speednode.node_paths.join(np_sep)
16
+ end
17
+ end
18
+ ENV['NODE_PATH'] = temp_node_path.split(np_sep).uniq.join(np_sep)
19
+ end
20
+ end
21
+
22
+ self.node_paths = []
23
+ end
@@ -0,0 +1,19 @@
1
+ module ExecJS
2
+ class << self
3
+ def permissive_bench(source, options = {})
4
+ runtime.permissive_bench(source, options)
5
+ end
6
+
7
+ def permissive_exec(source, options = {})
8
+ runtime.permissive_exec(source, options)
9
+ end
10
+
11
+ def permissive_eval(source, options = {})
12
+ runtime.permissive_eval(source, options)
13
+ end
14
+
15
+ def permissive_compile(source, options = {})
16
+ runtime.permissive_compile(source, options)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,23 @@
1
+ module ExecJS
2
+ # Abstract base class for runtimes
3
+ class Runtime
4
+ def permissive_bench(source, options = {})
5
+ context = permissive_compile("", options)
6
+ context.bench(source, options)
7
+ end
8
+
9
+ def permissive_exec(source, options = {})
10
+ context = permissive_compile("", options)
11
+ context.exec(source, options)
12
+ end
13
+
14
+ def permissive_eval(source, options = {})
15
+ context = permissive_compile("", options)
16
+ context.eval(source, options)
17
+ end
18
+
19
+ def permissive_compile(source, options = {})
20
+ context_class.new(self, source, options.merge({permissive: true}))
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,11 @@
1
+ module ExecJS
2
+ module Runtimes
3
+ Speednode = Speednode::Runtime.new(
4
+ name: 'Speednode Node.js (V8)',
5
+ command: %w[node nodejs],
6
+ runner_path: File.join(File.dirname(__FILE__), 'runner.js'),
7
+ encoding: 'UTF-8'
8
+ )
9
+ runtimes.unshift(Speednode)
10
+ end
11
+ end
@@ -0,0 +1,41 @@
1
+ module Speednode
2
+ class NodeCommand
3
+ def self.which(command)
4
+ Array(command).find do |name|
5
+ name, args = name.split(/\s+/, 2)
6
+ path = locate_executable(name)
7
+
8
+ next unless path
9
+
10
+ args ? "#{path} #{args}" : path
11
+ end
12
+ end
13
+
14
+ def self.cached(command)
15
+ @cached_command ||= which(command)
16
+ end
17
+
18
+ private
19
+
20
+ def self.locate_executable(command)
21
+ commands = Array(command)
22
+ if ExecJS.windows? && File.extname(command) == ""
23
+ ENV['PATHEXT'].split(File::PATH_SEPARATOR).each { |p|
24
+ commands << (command + p)
25
+ }
26
+ end
27
+
28
+ commands.find { |cmd|
29
+ if File.executable? cmd
30
+ cmd
31
+ else
32
+ path = ENV['PATH'].split(File::PATH_SEPARATOR).find { |p|
33
+ full_path = File.join(p, cmd)
34
+ File.executable?(full_path) && File.file?(full_path)
35
+ }
36
+ path && File.expand_path(cmd, path)
37
+ end
38
+ }
39
+ end
40
+ end
41
+ end