isomorfeus-speednode 0.3.1 → 0.4.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1e0315df4f6d4730a62a4189ef7f12fcaf01e187d323ca999aa27cd0254ee7ea
4
- data.tar.gz: '0711819b4c9280ff1dace7f9f7432f961383bb82cf543a0b3981d01e1a83b2be'
3
+ metadata.gz: 9ae800090e3226f94d53570baf613f33123e4f95bbb3e384b33d373a17328b60
4
+ data.tar.gz: 3036d03ea6149df04d5cb53c6d2058893e6d2a61a2ea96f0a24c1d3b694705a0
5
5
  SHA512:
6
- metadata.gz: 706dded2560521f21e3e130cfa266bb725f9891b88cca7331ad5f33e3ea054186a3832a4d6aa7144428085b77bfb1fcd1a4ce09b3fd2dcfc7872e3930b3c974b
7
- data.tar.gz: '018b37989b54160cdb3e89ece63dd6feb9a423dd845229472e5d5b75864487d86c0fbcd46369d0ec70e8521afa7ae0f7b91d909344475d3e8b5cb2c8b4692b0b'
6
+ metadata.gz: 13ccaa36e365de0b48dbc5124f8f4ff3ef123ca2177d745c895828aa5037c2d6dba1510803cb2434358117ddd3824c854bc3a44df72c5ca684176077f109c3c8
7
+ data.tar.gz: e66f98d834d55218c99744b66bde19a680506fd543093e1a0316234480aa46c880262cf33f16cc3722df6a31255c4f8059f4914e4ce40d9485de4233d7aeb282
data/LICENSE CHANGED
@@ -1,22 +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.
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,99 +1,127 @@
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
- ### Benchmarks
57
-
58
- Highly scientific, maybe.
59
-
60
- 1000 rounds on Linux using node 16.1.0:
61
- ```
62
- standard ExecJS CoffeeScript call benchmark:
63
- user system total real
64
- Node.js (V8) fast 0.234199 0.093775 0.327974 ( 0.877142)
65
- Isomorfeus Speednode Node.js (V8) 0.108882 0.048014 0.156896 ( 0.587261)
66
- mini_racer (V8) 0.801764 0.105020 0.906784 ( 0.515463)
67
- Node.js (V8) 0.420842 0.293893 52.471348 ( 50.990930)
68
-
69
- call overhead benchmark:
70
- user system total real
71
- Node.js (V8) fast 0.192230 0.086253 0.278483 ( 0.372702)
72
- Isomorfeus Speednode Node.js (V8) 0.101266 0.034985 0.136251 ( 0.214831)
73
- mini_racer (V8) 0.163080 0.052209 0.215289 ( 0.141151)
74
- Node.js (V8) 0.354932 0.218117 29.016030 ( 28.410343)
75
- ```
76
-
77
- 1000 rounds on Windows 10 using node 16.1.0:
78
- ```
79
- standard ExecJS CoffeeScript call benchmark:
80
- user system total real
81
- Isomorfeus Speednode Node.js (V8) 0.031000 0.016000 0.047000 ( 0.548920)
82
- Node.js (V8) 1.172000 2.594000 3.766000 ( 91.619422)
83
-
84
- call overhead benchmark:
85
- user system total real
86
- Isomorfeus Speednode Node.js (V8) 0.063000 0.000000 0.063000 ( 0.162426)
87
- Node.js (V8) 0.656000 2.516000 3.172000 ( 63.766556)
88
- ```
89
-
90
- To run benchmarks:
91
- - clone repo
92
- - `bundle install`
93
- - `bundle exec rake bench`
94
-
95
- ### Tests
96
- To run tests:
97
- - clone repo
98
- - `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`
99
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