speednode 0.8.1

Sign up to get free protection for your applications and to get access to all the features.
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