isomorfeus-speednode 0.2.11 → 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 917120184f53418bf061f3fbb7020f9ebbfd33ddc24475f06544aca2b8787b2a
4
- data.tar.gz: 9c1e6cdc8c62dbf4db3d1bb27bad4b392ff520de3ca2c76586eec3ce9c43bb50
3
+ metadata.gz: 1bb65853d666ae62943ed28b887dc6792c83c34bd7ed090d7bc817a42e40c63e
4
+ data.tar.gz: '0227178ecf8760e07f32043beba064258da9efa6baa221c96b0437ee12a6fc80'
5
5
  SHA512:
6
- metadata.gz: 4e6d3aa1bc6654ac85c39681fb71ce699174fa6c65366a91732fff9d3a8f3f2fef6c967f080e17e94b3af1215764d2d978e23666ffa44521a01d100d42d1c6e3
7
- data.tar.gz: db5a4fcab7ce656f507f1674e397c9a084867b24353c4ffabe294c33694234c86a85d5b9e737d9f8b88a373daa059b0a30e03f5cd5ece4e8a4c52d51a702280b
6
+ metadata.gz: ee26a18eb0e0209f543ce01cf240824edd3d00c8590a57c72a0422680c48127318a4111f7417f424b48ae8ffbc72b43c3bac7c03da8ee62baa265a49880471e8
7
+ data.tar.gz: a9da737b51f2b115bac2c218d2abc0618b443daa2cc2cd380991a30b4b4a998cb6c2497356d30279f04e717c86cf61a557173506b19859b711ca895a10880d75
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2016 John Hawthorn
4
+ Copyright (c) 2019 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 CHANGED
@@ -1,83 +1,127 @@
1
- # isomorfeus-speednode
2
-
3
- A fast runtime for execjs using node js.
4
- Inspired by [execjs-fastnode](https://github.com/jhawthorn/execjs-fastnode).
5
-
6
- ### Community and Support
7
- At the [Isomorfeus Framework Project](http://isomorfeus.com)
8
-
9
- ### Tested
10
- [TravisCI](https://travis-ci.org): [![Build Status](https://travis-ci.org/isomorfeus/isomorfeus-speednode.svg?branch=master)](https://travis-ci.org/isomorfeus/isomorfeus-speednode)
11
-
12
- ### Installation
13
-
14
- In Gemfile:
15
- `gem 'isomorfeus-speednode'`, then `bundle install`
16
-
17
- ### Configuration
18
-
19
- Isomorfeus-speednode provides one node based runtime `Speednode` which runs scripts in node vms.
20
- The runtime can be chosen by:
21
-
22
- ```ruby
23
- ExecJS.runtime = ExecJS::Runtimes::Speednode
24
- ```
25
- If node cant find node modules for the permissive contexts (see below), its possible to set the load path before assigning the runtime:
26
- ```ruby
27
- ENV['NODE_PATH'] = './node_modules'
28
- ```
29
- ### Contexts
30
-
31
- Each ExecJS context runs in a node vm. Speednode offers two kinds of contexts:
32
- - a compatible context, which is compatible with default ExecJS behavior.
33
- - a permissive context, which is more permissive and allows to `require` node modules.
34
-
35
- #### Compatible
36
- A compatible context can be created with the standard `ExecJS.compile` or code can be executed within a compatible context by using the standard
37
- `ExecJS.eval` or `ExecJS.exec`.
38
- Example for a compatible context:
39
- ```ruby
40
- compat_context = ExecJS.compile('Test = "test"')
41
- compat_context.eval('1+1')
42
- ```
43
- #### Permissive
44
- A permissive context can be created with `ExecJS.permissive_compile` or code can be executed within a permissive context by using
45
- `ExecJS.permissive_eval` or `ExecJS.permissive_exec`.
46
- Example for a permissive context:
47
- ```ruby
48
- perm_context = ExecJS.permissive_compile('Test = "test"')
49
- perm_context.eval('1+1')
50
- ```
51
- Evaluation in a permissive context:
52
- ```ruby
53
- ExecJS.permissive_eval('1+1')
54
- ```
55
-
56
- ### Benchmarks
57
-
58
- Highly scientific, maybe.
59
- ```
60
- standard ExecJS CoffeeScript call benchmark, but 1000 rounds:
61
- user system total real
62
- Isomorfeus Speednode Compatible Node.js (V8) 0.042263 0.017215 0.059478 ( 0.442855)
63
- Node.js (V8) fast 0.222875 0.087109 0.309984 ( 0.806736)
64
- mini_racer (V8) 0.425273 0.013478 0.438751 ( 0.304434)
65
-
66
-
67
- call overhead benchmark, 1000 rounds:
68
- user system total real
69
- Isomorfeus Speednode Compatible Node.js (V8) 0.023060 0.010358 0.033418 ( 0.059640)
70
- Node.js (V8) fast 0.191454 0.081396 0.272850 ( 0.368568)
71
- mini_racer (V8) 0.017091 0.002494 0.019585 ( 0.019584)
72
- ```
73
-
74
- To run benchmarks:
75
- - clone repo
76
- - `bundle install`
77
- - `bundle exec rake bench`
78
-
79
- ### Tests
80
- To run tests:
81
- - clone repo
82
- - `bundle install`
1
+ # isomorfeus-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
+ ### Community and Support
7
+ At the [Isomorfeus Framework Project](http://isomorfeus.com)
8
+
9
+ ### Tested
10
+ [TravisCI](https://travis-ci.org): [![Build Status](https://travis-ci.org/isomorfeus/isomorfeus-speednode.svg?branch=master)](https://travis-ci.org/isomorfeus/isomorfeus-speednode)
11
+
12
+ ### Installation
13
+
14
+ In Gemfile:
15
+ `gem 'isomorfeus-speednode'`, then `bundle install`
16
+
17
+ ### Configuration
18
+
19
+ Isomorfeus-speednode provides one node based runtime `Speednode` which runs scripts in node vms.
20
+ The runtime can be chosen by:
21
+
22
+ ```ruby
23
+ ExecJS.runtime = ExecJS::Runtimes::Speednode
24
+ ```
25
+ If node cant find node modules for the permissive contexts (see below), its possible to set the load path before assigning the runtime:
26
+ ```ruby
27
+ ENV['NODE_PATH'] = './node_modules'
28
+ ```
29
+
30
+ ### Contexts
31
+
32
+ Each ExecJS context runs in a node vm. Speednode offers two kinds of contexts:
33
+ - a compatible context, which is compatible with default ExecJS behavior.
34
+ - a permissive context, which is more permissive and allows to `require` node modules.
35
+
36
+ #### Compatible
37
+ 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`.
38
+ Example for a compatible context:
39
+ ```ruby
40
+ compat_context = ExecJS.compile('Test = "test"')
41
+ compat_context.eval('1+1')
42
+ ```
43
+ #### Permissive
44
+ A permissive context can be created with `ExecJS.permissive_compile` or code can be executed within a permissive context by using
45
+ `ExecJS.permissive_eval` or `ExecJS.permissive_exec`.
46
+ Example for a permissive context:
47
+ ```ruby
48
+ perm_context = ExecJS.permissive_compile('Test = "test"')
49
+ perm_context.eval('1+1')
50
+ ```
51
+ Evaluation in a permissive context:
52
+ ```ruby
53
+ ExecJS.permissive_eval('1+1')
54
+ ```
55
+
56
+ ### Async function support
57
+
58
+ Its possible to call async functions synchronously from ruby using Context#await:
59
+ ```ruby
60
+ context = ExecJS.compile('')
61
+ context.eval <<~JAVASCRIPT
62
+ async function foo(val) {
63
+ return new Promise(function (resolve, reject) { resolve(val); });
64
+ }
65
+ JAVASCRIPT
66
+
67
+ context.await("foo('test')") # => 'test'
68
+ ```
69
+
70
+ ### Attaching ruby methods to Permissive Contexts
71
+
72
+ Ruby methods can be attached to Permissive Contexts using Context#attach:
73
+ ```ruby
74
+ context = ExecJS.permissive_compile(SOURCE)
75
+ context.attach('foo') { |v| v }
76
+ context.await('foo("bar")')
77
+ ```
78
+ The attached method is reflected in the context by a async javascript function. From within javascript the ruby method is best called using await:
79
+ ```javascript
80
+ r = await foo('test');
81
+ ```
82
+ or via context#await as in above example.
83
+ Attaching and calling ruby methods to/from permissive contexts is not that fast. It is recommended to use it sparingly.
84
+
85
+ ### Benchmarks
86
+
87
+ Highly scientific, maybe.
88
+
89
+ 1000 rounds using node 16.6.2.:
90
+ ```
91
+ standard ExecJS CoffeeScript eval benchmark:
92
+ user system total real
93
+ Isomorfeus Speednode Node.js (V8) Windows 0.079000 0.031000 0.110000 ( 0.518821)
94
+ Isomorfeus Speednode Node.js (V8) Linux 0.092049 0.039669 0.131718 ( 0.546846)
95
+ mini_racer (V8) 0.4.0 Linux only 0.776317 0.062923 0.839240 ( 0.480490)
96
+ Node.js (V8) Linux 0.330156 0.354536 52.523972 ( 51.203355)
97
+
98
+ evel overhead benchmark:
99
+ user system total real
100
+ Isomorfeus Speednode Node.js (V8) Windows 0.032000 0.031000 0.063000 ( 0.227634)
101
+ Isomorfeus Speednode Node.js (V8) Linux 0.085135 0.022002 0.107137 ( 0.153055)
102
+ mini_racer (V8) 0.4.0 Linux only 0.083562 0.115612 0.199174 ( 0.128455)
103
+ Node.js (V8) Linux 0.273238 0.265101 30.169976 ( 29.636668)
104
+ ```
105
+ minify discourse benchmark from mini_racer:
106
+ ```
107
+ minify discourse_app_minified.js:
108
+ user system total real
109
+ isomorfeus-speednode Windows 0.016000 0.078000 0.094000 ( 11.828518)
110
+ isomorfeus-speednode Linux 0.043747 0.000000 15.931284 ( 11.860528)
111
+ mini_racer 0.043471 0.000000 15.974214 ( 11.923735)
112
+ node 0.043695 0.000000 15.812040 ( 11.781835)
113
+ ```
114
+
115
+ To run benchmarks:
116
+ - clone repo
117
+ - `cd ruby`
118
+ - `bundle install`
119
+ - `bundle exec rake bench`
120
+
121
+ ### Tests
122
+
123
+ To run tests:
124
+ - clone repo
125
+ - `cd ruby`
126
+ - `bundle install`
83
127
  - `bundle exec rake test`
@@ -0,0 +1,226 @@
1
+ require 'ffi'
2
+
3
+ module Isomorfeus
4
+ module Speednode
5
+ module WindowsyThings
6
+ extend FFI::Library
7
+
8
+ ffi_lib :kernel32, :user32
9
+
10
+ ERROR_IO_PENDING = 997
11
+ ERROR_PIPE_CONNECTED = 535
12
+ ERROR_SUCCESS = 0
13
+
14
+ FILE_FLAG_OVERLAPPED = 0x40000000
15
+
16
+ INFINITE = 0xFFFFFFFF
17
+ INVALID_HANDLE_VALUE = FFI::Pointer.new(-1).address
18
+
19
+ PIPE_ACCESS_DUPLEX = 0x00000003
20
+ PIPE_READMODE_BYTE = 0x00000000
21
+ PIPE_READMODE_MESSAGE = 0x00000002
22
+ PIPE_TYPE_BYTE = 0x00000000
23
+ PIPE_TYPE_MESSAGE = 0x00000004
24
+ PIPE_WAIT = 0x00000000
25
+
26
+ QS_ALLINPUT = 0x04FF
27
+
28
+ typedef :uintptr_t, :handle
29
+
30
+ attach_function :ConnectNamedPipe, [:handle, :pointer], :ulong
31
+ attach_function :CreateEvent, :CreateEventA, [:pointer, :ulong, :ulong, :string], :handle
32
+ attach_function :CreateNamedPipe, :CreateNamedPipeA, [:string, :ulong, :ulong, :ulong, :ulong, :ulong, :ulong, :pointer], :handle
33
+ attach_function :DisconnectNamedPipe, [:handle], :bool
34
+ attach_function :FlushFileBuffers, [:handle], :bool
35
+ attach_function :GetLastError, [], :ulong
36
+ attach_function :GetOverlappedResult, [:handle, :pointer, :pointer, :bool], :bool
37
+ attach_function :MsgWaitForMultipleObjects, [:ulong, :pointer, :ulong, :ulong, :ulong], :ulong
38
+ attach_function :ReadFile, [:handle, :buffer_out, :ulong, :pointer, :pointer], :bool
39
+ attach_function :SetEvent, [:handle], :bool
40
+ attach_function :WaitForMultipleObjects, [:ulong, :pointer, :ulong, :ulong], :ulong
41
+ attach_function :WriteFile, [:handle, :buffer_in, :ulong, :pointer, :pointer], :bool
42
+ end
43
+
44
+ class AttachPipe
45
+ include Isomorfeus::Speednode::WindowsyThings
46
+
47
+ CONNECTING_STATE = 0
48
+ READING_STATE = 1
49
+ WRITING_STATE = 2
50
+ INSTANCES = 4
51
+ PIPE_TIMEOUT = 5000
52
+ BUFFER_SIZE = 65536
53
+
54
+ class Overlapped < FFI::Struct
55
+ layout(
56
+ :Internal, :uintptr_t,
57
+ :InternalHigh, :uintptr_t,
58
+ :Offset, :ulong,
59
+ :OffsetHigh, :ulong,
60
+ :hEvent, :uintptr_t
61
+ )
62
+ end
63
+
64
+ def initialize(pipe_name, block)
65
+ @run_block = block
66
+ @full_pipe_name = "\\\\.\\pipe\\#{pipe_name}"
67
+ @instances = 1
68
+ @events = []
69
+ @events_pointer = FFI::MemoryPointer.new(:uintptr_t, @instances + 1)
70
+ @pipe = {}
71
+ end
72
+
73
+ def run
74
+ @running = true
75
+ create_instance
76
+ while_loop
77
+ end
78
+
79
+ def stop
80
+ @running = false
81
+ STDERR.puts("DisconnectNamedPipe failed with #{GetLastError()}") if !DisconnectNamedPipe(@pipe[:instance])
82
+ end
83
+
84
+ private
85
+
86
+ def create_instance
87
+ @events[0] = CreateEvent(nil, 1, 1, nil)
88
+ raise "CreateEvent failed with #{GetLastError()}" unless @events[0]
89
+
90
+ overlap = Overlapped.new
91
+ overlap[:hEvent] = @events[0]
92
+
93
+ @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 }
94
+ @pipe[:instance] = CreateNamedPipe(@full_pipe_name,
95
+ PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED,
96
+ PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT,
97
+ 4,
98
+ BUFFER_SIZE,
99
+ BUFFER_SIZE,
100
+ PIPE_TIMEOUT,
101
+ nil)
102
+ raise "CreateNamedPipe failed with #{GetLastError()}" if @pipe[:instance] == INVALID_HANDLE_VALUE
103
+ @pipe[:pending_io] = connect_to_new_client
104
+ @pipe[:state] = @pipe[:pending_io] ? CONNECTING_STATE : READING_STATE
105
+
106
+ @events_pointer.write_array_of_ulong_long(@events)
107
+ nil
108
+ end
109
+
110
+ def while_loop
111
+ while @running
112
+ # this sleep gives other ruby threads a chance to run
113
+ # ~10ms is a ruby thread time slice, so we choose something a bit larger
114
+ # that ruby or the os is free to switch threads
115
+ sleep 0.010 if @pipe[:state] != WRITING_STATE && @pipe[:state] != READING_STATE
116
+
117
+ i = MsgWaitForMultipleObjects(@instances, @events_pointer, 0, 1, QS_ALLINPUT) if @pipe[:state] != WRITING_STATE
118
+
119
+ if i > 0
120
+ next
121
+ end
122
+
123
+ if @pipe[:pending_io]
124
+ bytes_transferred = FFI::MemoryPointer.new(:ulong)
125
+ success = GetOverlappedResult(@pipe[:instance], @pipe[:overlap], bytes_transferred, false)
126
+
127
+ case @pipe[:state]
128
+ when CONNECTING_STATE
129
+ raise "Error #{GetLastError()}" unless success
130
+ @pipe[:state] = READING_STATE
131
+ when READING_STATE
132
+ if !success || bytes_transferred.read_ulong == 0
133
+ disconnect_and_reconnect(i)
134
+ next
135
+ else
136
+ @pipe[:bytes_read] = bytes_transferred.read_ulong
137
+ @pipe[:state] = WRITING_STATE
138
+ end
139
+ when WRITING_STATE
140
+ if !success || bytes_transferred.read_ulong != @pipe[:bytes_to_write]
141
+ disconnect_and_reconnect(i)
142
+ next
143
+ else
144
+ @pipe[:state] = READING_STATE
145
+ end
146
+ else
147
+ raise "Invalid pipe state."
148
+ end
149
+ end
150
+
151
+ case @pipe[:state]
152
+ when READING_STATE
153
+ bytes_read = FFI::MemoryPointer.new(:ulong)
154
+ success = ReadFile(@pipe[:instance], @pipe[:request], BUFFER_SIZE, bytes_read, @pipe[:overlap].to_ptr)
155
+ if success && bytes_read.read_ulong != 0
156
+ @pipe[:pending_io] = false
157
+ @pipe[:state] = WRITING_STATE
158
+ next
159
+ end
160
+
161
+ err = GetLastError()
162
+ if !success && err == ERROR_IO_PENDING
163
+ @pipe[:pending_io] = true
164
+ next
165
+ end
166
+
167
+ disconnect_and_reconnect
168
+ when WRITING_STATE
169
+ @pipe[:reply] = @run_block.call(@pipe[:request].get_string(0))
170
+ @pipe[:bytes_to_write] = @pipe[:reply].bytesize
171
+ bytes_written = FFI::MemoryPointer.new(:ulong)
172
+ success = WriteFile(@pipe[:instance], @pipe[:reply], @pipe[:bytes_to_write], bytes_written, @pipe[:overlap].to_ptr)
173
+
174
+ if success && bytes_written.read_ulong == @pipe[:bytes_to_write]
175
+ @pipe[:pending_io] = false
176
+ @pipe[:state] = READING_STATE
177
+ next
178
+ end
179
+
180
+ err = GetLastError()
181
+
182
+ if !success && err == ERROR_IO_PENDING
183
+ @pipe[:pending_io] = true
184
+ next
185
+ end
186
+
187
+ disconnect_and_reconnect
188
+ else
189
+ raise "Invalid pipe state."
190
+ end
191
+ end
192
+ end
193
+
194
+ def disconnect_and_reconnect
195
+ FlushFileBuffers(@pipe[:instance])
196
+ STDERR.puts("DisconnectNamedPipe failed with #{GetLastError()}") if !DisconnectNamedPipe(@pipe[:instance])
197
+
198
+ @pipe[:pending_io] = connect_to_new_client
199
+
200
+ @pipe[:state] = @pipe[:pending_io] ? CONNECTING_STATE : READING_STATE
201
+ end
202
+
203
+ def connect_to_new_client
204
+ pending_io = false
205
+ @pipe[:request].clear
206
+ @pipe[:reply].clear
207
+ connected = ConnectNamedPipe(@pipe[:instance], @pipe[:overlap].to_ptr)
208
+ last_error = GetLastError()
209
+ raise "ConnectNamedPipe failed with #{last_error} - #{connected}" if connected != 0
210
+
211
+ case last_error
212
+ when ERROR_IO_PENDING
213
+ pending_io = true
214
+ when ERROR_PIPE_CONNECTED
215
+ SetEvent(@pipe[:overlap][:hEvent])
216
+ when ERROR_SUCCESS
217
+ pending_io = true
218
+ else
219
+ raise "ConnectNamedPipe failed with error #{last_error}"
220
+ end
221
+
222
+ pending_io
223
+ end
224
+ end
225
+ end
226
+ end