isomorfeus-speednode 0.3.1 → 0.4.0

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: 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