concurrent-c-node 0.22.1__tar.gz → 0.23.1__tar.gz
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.
- {concurrent_c_node-0.22.1 → concurrent_c_node-0.23.1}/PKG-INFO +67 -41
- {concurrent_c_node-0.22.1 → concurrent_c_node-0.23.1}/README.md +66 -40
- {concurrent_c_node-0.22.1 → concurrent_c_node-0.23.1}/cc_node/__init__.py +80 -23
- {concurrent_c_node-0.22.1 → concurrent_c_node-0.23.1}/cc_node/benchmarks/vs_alts.py +75 -2
- {concurrent_c_node-0.22.1 → concurrent_c_node-0.23.1}/cc_node/magics.py +7 -7
- concurrent_c_node-0.23.1/cc_node/stdio_line.cjs +13 -0
- {concurrent_c_node-0.22.1 → concurrent_c_node-0.23.1}/concurrent_c_node.egg-info/PKG-INFO +67 -41
- {concurrent_c_node-0.22.1 → concurrent_c_node-0.23.1}/concurrent_c_node.egg-info/SOURCES.txt +1 -0
- {concurrent_c_node-0.22.1 → concurrent_c_node-0.23.1}/pyproject.toml +2 -2
- {concurrent_c_node-0.22.1 → concurrent_c_node-0.23.1}/cc_node/benchmarks/__init__.py +0 -0
- {concurrent_c_node-0.22.1 → concurrent_c_node-0.23.1}/cc_node/benchmarks/multi_domain.py +0 -0
- {concurrent_c_node-0.22.1 → concurrent_c_node-0.23.1}/cc_node/broker.cjs +0 -0
- {concurrent_c_node-0.22.1 → concurrent_c_node-0.23.1}/cc_node/examples/__init__.py +0 -0
- {concurrent_c_node-0.22.1 → concurrent_c_node-0.23.1}/cc_node/examples/bench_wire.py +0 -0
- {concurrent_c_node-0.22.1 → concurrent_c_node-0.23.1}/cc_node/examples/use_node.py +0 -0
- {concurrent_c_node-0.22.1 → concurrent_c_node-0.23.1}/concurrent_c_node.egg-info/dependency_links.txt +0 -0
- {concurrent_c_node-0.22.1 → concurrent_c_node-0.23.1}/concurrent_c_node.egg-info/requires.txt +0 -0
- {concurrent_c_node-0.22.1 → concurrent_c_node-0.23.1}/concurrent_c_node.egg-info/top_level.txt +0 -0
- {concurrent_c_node-0.22.1 → concurrent_c_node-0.23.1}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: concurrent-c-node
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.23.1
|
|
4
4
|
Summary: JavaScript and npm packages from Python over the Concurrent-C bridge: one spawned Node child per domain, host-controlled lifetime.
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Repository, https://github.com/sreekotay/concurrent-c
|
|
@@ -28,15 +28,19 @@ bridge):
|
|
|
28
28
|
[JS / Python interop](https://github.com/sreekotay/concurrent-c/blob/main/docs/js-py-modules.md).
|
|
29
29
|
|
|
30
30
|
```python
|
|
31
|
-
import
|
|
32
|
-
|
|
33
|
-
js = cc_node.create() # always a child `node` process
|
|
34
|
-
_ = js.require('lodash') # cwd node_modules
|
|
31
|
+
from cc_node import require # one session child (lazy)
|
|
32
|
+
_ = require('lodash') # cwd node_modules
|
|
35
33
|
_.chunk([1, 2, 3, 4, 5], 2) # [[1, 2], [3, 4], [5]]
|
|
34
|
+
```
|
|
36
35
|
|
|
36
|
+
A private child (`create()`) is still there when you want N Nodes or
|
|
37
|
+
an explicit lifetime — not required for the first call.
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
import cc_node
|
|
41
|
+
js = cc_node.create() # a private Node, not the session
|
|
37
42
|
semver = js.require('semver')
|
|
38
43
|
semver.satisfies('1.2.3', '^1.0.0') # True
|
|
39
|
-
|
|
40
44
|
js.destroy() # or: with cc_node.create() as js:
|
|
41
45
|
```
|
|
42
46
|
|
|
@@ -44,12 +48,13 @@ The other direction (Python from Node):
|
|
|
44
48
|
[`concurrent-c-python`](https://www.npmjs.com/package/concurrent-c-python)
|
|
45
49
|
(in-process by default; vs pymport / ncp / pythonia in that README).
|
|
46
50
|
|
|
47
|
-
Every `create()`
|
|
48
|
-
measurable wire. N domains = N processes.
|
|
51
|
+
Every `create()` is a separate Node — real addons, crash isolation,
|
|
52
|
+
measurable wire. N domains = N processes. `require()` / `get()` share
|
|
53
|
+
one session for the process (the Jupyter kernel).
|
|
49
54
|
|
|
50
55
|
| | this package | CC hosted (`cc_js_new(false, …)`) |
|
|
51
56
|
|---|---|---|
|
|
52
|
-
| API | `
|
|
57
|
+
| API | `require()` / `create()` | `.ccs` program |
|
|
53
58
|
| Where | child `node` | libnode in-process |
|
|
54
59
|
| Hot call | ~105µs RTT | sub-µs (needs libnode) |
|
|
55
60
|
| Bulk | shm (~9.5ms / 8MB) | in-process |
|
|
@@ -60,10 +65,12 @@ measurable wire. N domains = N processes.
|
|
|
60
65
|
|
|
61
66
|
- Always a child `node`. A call blocks until JS answers; thenables wait
|
|
62
67
|
in the child. No `{ async: true }`.
|
|
68
|
+
- `from cc_node import require` is the session. `create()` is a private
|
|
69
|
+
child. `reset()` / `%js_reset` / `%reset` / atexit tear the session down.
|
|
63
70
|
- Scalars / `None` materialize; empty `{}` stays a handle; everything
|
|
64
71
|
else is a `JsHandle` until `str()` / attrs / a call.
|
|
65
|
-
-
|
|
66
|
-
|
|
72
|
+
- Notebook: `import cc_node` registers `%%js` (no `%load_ext`). Same
|
|
73
|
+
session as `require()`.
|
|
67
74
|
- `eval()` is one RTT, no extra globals. `%%js` / `eval_cell` install
|
|
68
75
|
cwd `require` once.
|
|
69
76
|
- `--bind` is `Object.assign(globalThis, …)` of names you name (wire
|
|
@@ -73,47 +80,58 @@ measurable wire. N domains = N processes.
|
|
|
73
80
|
|
|
74
81
|
```
|
|
75
82
|
pip install concurrent-c-node # needs node on PATH
|
|
76
|
-
pip install 'concurrent-c-node[jupyter]' #
|
|
83
|
+
pip install 'concurrent-c-node[jupyter]' # IPython (%%js); require() does not need this
|
|
77
84
|
python -m cc_node.examples.use_node
|
|
78
85
|
python -m cc_node.examples.bench_wire
|
|
79
86
|
python -m cc_node.benchmarks.multi_domain
|
|
80
|
-
python -m cc_node.benchmarks.vs_alts # vs DIY node / pythonmonkey / mini-racer
|
|
87
|
+
python -m cc_node.benchmarks.vs_alts # vs pythonia / DIY node / pythonmonkey / mini-racer
|
|
81
88
|
```
|
|
82
89
|
|
|
83
90
|
## Jupyter / Colab
|
|
84
91
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
kernel, not a spawn per cell (~28ms).
|
|
92
|
+
Same verb as pythonia: `require`. No `%load_ext`, no `create()`, no
|
|
93
|
+
`destroy()` for the happy path. One session child for the kernel;
|
|
94
|
+
`console.log` lands in the cell.
|
|
89
95
|
|
|
90
96
|
```python
|
|
91
97
|
%pip install concurrent-c-node
|
|
92
98
|
# if `node` is missing (typical Colab):
|
|
93
99
|
!apt-get install -y nodejs
|
|
94
|
-
%load_ext cc_node
|
|
95
100
|
|
|
101
|
+
from cc_node import require
|
|
102
|
+
require('lodash').chunk([1, 2, 3, 4, 5], 2)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`import cc_node` also registers `%%js` (IPython already running; the
|
|
106
|
+
`[jupyter]` extra is only if you want magics without IPython already
|
|
107
|
+
installed).
|
|
108
|
+
|
|
109
|
+
```python
|
|
96
110
|
%%js
|
|
97
|
-
|
|
98
|
-
|
|
111
|
+
console.log('hi') # shows in the cell
|
|
112
|
+
require('lodash').chunk([1, 2, 3, 4, 5], 2)
|
|
99
113
|
```
|
|
100
114
|
|
|
101
115
|
```python
|
|
102
116
|
xs = [1, 2, 3, 4]
|
|
103
117
|
|
|
104
118
|
%%js -b xs -t chunks
|
|
105
|
-
xs.map(x => x * 2)
|
|
119
|
+
xs.map(x => x * 2) # wire types only; no pickle fallback
|
|
106
120
|
```
|
|
107
121
|
|
|
108
122
|
| | |
|
|
109
123
|
|---|---|
|
|
110
|
-
|
|
|
111
|
-
|
|
|
124
|
+
| `from cc_node import require` | session `require`; spawns on first call |
|
|
125
|
+
| `import cc_node` | registers magics; does **not** spawn until first `require()` / `%%js` |
|
|
126
|
+
| `%load_ext cc_node` | same, idempotent; not required |
|
|
127
|
+
| `%js 1+1` / `%%js` | eval on the same session; last expression is the result |
|
|
112
128
|
| `-b xs` / `--bind xs,n` | publish those Python names on `globalThis` for the cell |
|
|
113
129
|
| `-t chunks` / `--to` | store the result in the notebook namespace |
|
|
114
130
|
| `%js_stats` | handle-table size (spawns if needed) |
|
|
115
|
-
| `%js_reset` | `destroy()` the
|
|
116
|
-
| `cc_node.
|
|
131
|
+
| `%js_reset` / `cc_node.reset()` | `destroy()` the session child; `%reset` does this too |
|
|
132
|
+
| `cc_node.get()` | the session the magics use; `create()` is still a private child |
|
|
133
|
+
| `cc_node.kernel()` | alias of `get()` |
|
|
134
|
+
| `cc_node.require('fs')` | `get().require('fs')` |
|
|
117
135
|
| `JsHandle` display | cheap `JsHandle #3` — repr does not cross the wire |
|
|
118
136
|
|
|
119
137
|
`eval()` is unchanged (one RTT, no extra globals). `%%js` / `eval_cell`
|
|
@@ -151,25 +169,30 @@ Colab is not that. There, default `create()` blocks the kernel thread —
|
|
|
151
169
|
Wire: line-JSON on dedicated fds (stdio stays yours). Bulk spill: private
|
|
152
170
|
0700 dir, 0600 files, removed with the bridge.
|
|
153
171
|
|
|
154
|
-
### Vs pythonmonkey / mini-racer / DIY node
|
|
172
|
+
### Vs pythonia / pythonmonkey / mini-racer / DIY node
|
|
155
173
|
|
|
156
|
-
Most “JS from Python” libraries are **not Node**.
|
|
157
|
-
|
|
174
|
+
Most “JS from Python” libraries are **not Node**. pythonia (PyPI
|
|
175
|
+
[`javascript`](https://pypi.org/project/javascript/), JSPyBridge) is the
|
|
176
|
+
packaged peer that is: `require()` on import, one child. Bulk is a
|
|
177
|
+
**sum** over 1M floats (`.length` on an in-process wrapper is free and
|
|
178
|
+
lies). Snapshot:
|
|
158
179
|
[`cc_node_vs_alts_20260813.txt`](https://github.com/sreekotay/concurrent-c/blob/main/perf/baselines/cc_node_vs_alts_20260813.txt)
|
|
159
180
|
· harness: [`benchmarks/vs_alts.py`](https://github.com/sreekotay/concurrent-c/blob/main/pypi/cc-node/cc_node/benchmarks/vs_alts.py).
|
|
160
181
|
|
|
161
|
-
| | cc-node | DIY JSON stdio | `node -e` each | pythonmonkey | mini-racer |
|
|
162
|
-
|
|
163
|
-
| identity RTT | **20µs** |
|
|
164
|
-
| callback | **
|
|
165
|
-
| 8MB typed / list | **
|
|
166
|
-
| `require('fs')` | yes | yes | yes | no | no |
|
|
167
|
-
| process | child `node` | child `node` | new process/call | SpiderMonkey in-process | V8 isolate |
|
|
182
|
+
| | cc-node | pythonia | DIY JSON stdio | `node -e` each | pythonmonkey | mini-racer |
|
|
183
|
+
|---|---|---|---|---|---|---|
|
|
184
|
+
| identity RTT | **20µs** | 42µs | 19µs | 23ms | **<1µs** | 104µs |
|
|
185
|
+
| callback | **40µs** | 106µs | — | — | 1µs | — |
|
|
186
|
+
| 8MB typed / list | **7.5ms shm** / 266ms | — / 481ms | — / 155ms | — | — / 609ms | — / 78ms |
|
|
187
|
+
| `require('fs')` | yes | yes | yes | yes | no | no |
|
|
188
|
+
| process | child `node` | child `node` | child `node` | new process/call | SpiderMonkey in-process | V8 isolate |
|
|
168
189
|
|
|
169
190
|
Tiny scalars: pythonmonkey’s in-process SM beats a child. Real Node
|
|
170
191
|
(`require('fs')`, native addons, callbacks, stdout stays yours): this
|
|
171
|
-
package
|
|
172
|
-
|
|
192
|
+
package — ~2× pythonia on identity, shm for bulk, Python callables are
|
|
193
|
+
sync (pythonia’s JS side sees a Promise). Isolated `node -e` per call is
|
|
194
|
+
~1000× a persistent child. Optional engines SKIP if not importable —
|
|
195
|
+
not package deps.
|
|
173
196
|
|
|
174
197
|
The other direction (Python from Node):
|
|
175
198
|
[`concurrent-c-python`](https://www.npmjs.com/package/concurrent-c-python)
|
|
@@ -184,6 +207,7 @@ vs pymport / ncp / pythonia.
|
|
|
184
207
|
`destroy()`; afterwards: `bridge is closed`.
|
|
185
208
|
- `eval_cell(src, bindings=)` is the notebook door (`%%js`): cwd
|
|
186
209
|
`require` once, optional `globalThis` binds; `eval()` stays one RTT.
|
|
210
|
+
`get()` / `require()` / `eval()` at module level share that session.
|
|
187
211
|
- Crash isolation, not a sandbox. `destroy()` is cooperative; an
|
|
188
212
|
in-flight CPU-bound call finishes or you kill the child
|
|
189
213
|
([`bridge_stress.md`](https://github.com/sreekotay/concurrent-c/blob/main/stress/bridge/bridge_stress.md)).
|
|
@@ -222,9 +246,11 @@ total(array.array('d', range(1_000_000)))
|
|
|
222
246
|
## Common issues
|
|
223
247
|
|
|
224
248
|
**`Cannot find module`.** `require` / `import` resolve from the Python
|
|
225
|
-
process cwd (`node_modules` next to your program), not from
|
|
226
|
-
site-packages
|
|
227
|
-
|
|
249
|
+
process cwd (`node_modules` next to your notebook or program), not from
|
|
250
|
+
this wheel’s site-packages, and this package does **not** `npm install`
|
|
251
|
+
on a miss (pythonia does). `npm install lodash` in that directory is
|
|
252
|
+
the fix; or `create(node=…)` / `CC_NODE_BIN` when the wrong Node is on
|
|
253
|
+
`PATH`.
|
|
228
254
|
|
|
229
255
|
### Empty `{}` stays a handle
|
|
230
256
|
|
|
@@ -282,7 +308,7 @@ Both bridges (npm OIDC + PyPI OIDC):
|
|
|
282
308
|
|
|
283
309
|
Examples: `use_node`, `bench_wire`, `benchmarks.multi_domain`,
|
|
284
310
|
`benchmarks.vs_alts`.
|
|
285
|
-
Jupyter:
|
|
311
|
+
Jupyter: `from cc_node import require` (or `import cc_node` then `%%js`).
|
|
286
312
|
Stress: [`stress/bridge/`](https://github.com/sreekotay/concurrent-c/tree/main/stress/bridge).
|
|
287
313
|
Own hot path in C/CC → native module (40–90ns) —
|
|
288
314
|
[JS / Python interop](https://github.com/sreekotay/concurrent-c/blob/main/docs/js-py-modules.md).
|
|
@@ -13,15 +13,19 @@ bridge):
|
|
|
13
13
|
[JS / Python interop](https://github.com/sreekotay/concurrent-c/blob/main/docs/js-py-modules.md).
|
|
14
14
|
|
|
15
15
|
```python
|
|
16
|
-
import
|
|
17
|
-
|
|
18
|
-
js = cc_node.create() # always a child `node` process
|
|
19
|
-
_ = js.require('lodash') # cwd node_modules
|
|
16
|
+
from cc_node import require # one session child (lazy)
|
|
17
|
+
_ = require('lodash') # cwd node_modules
|
|
20
18
|
_.chunk([1, 2, 3, 4, 5], 2) # [[1, 2], [3, 4], [5]]
|
|
19
|
+
```
|
|
21
20
|
|
|
21
|
+
A private child (`create()`) is still there when you want N Nodes or
|
|
22
|
+
an explicit lifetime — not required for the first call.
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
import cc_node
|
|
26
|
+
js = cc_node.create() # a private Node, not the session
|
|
22
27
|
semver = js.require('semver')
|
|
23
28
|
semver.satisfies('1.2.3', '^1.0.0') # True
|
|
24
|
-
|
|
25
29
|
js.destroy() # or: with cc_node.create() as js:
|
|
26
30
|
```
|
|
27
31
|
|
|
@@ -29,12 +33,13 @@ The other direction (Python from Node):
|
|
|
29
33
|
[`concurrent-c-python`](https://www.npmjs.com/package/concurrent-c-python)
|
|
30
34
|
(in-process by default; vs pymport / ncp / pythonia in that README).
|
|
31
35
|
|
|
32
|
-
Every `create()`
|
|
33
|
-
measurable wire. N domains = N processes.
|
|
36
|
+
Every `create()` is a separate Node — real addons, crash isolation,
|
|
37
|
+
measurable wire. N domains = N processes. `require()` / `get()` share
|
|
38
|
+
one session for the process (the Jupyter kernel).
|
|
34
39
|
|
|
35
40
|
| | this package | CC hosted (`cc_js_new(false, …)`) |
|
|
36
41
|
|---|---|---|
|
|
37
|
-
| API | `
|
|
42
|
+
| API | `require()` / `create()` | `.ccs` program |
|
|
38
43
|
| Where | child `node` | libnode in-process |
|
|
39
44
|
| Hot call | ~105µs RTT | sub-µs (needs libnode) |
|
|
40
45
|
| Bulk | shm (~9.5ms / 8MB) | in-process |
|
|
@@ -45,10 +50,12 @@ measurable wire. N domains = N processes.
|
|
|
45
50
|
|
|
46
51
|
- Always a child `node`. A call blocks until JS answers; thenables wait
|
|
47
52
|
in the child. No `{ async: true }`.
|
|
53
|
+
- `from cc_node import require` is the session. `create()` is a private
|
|
54
|
+
child. `reset()` / `%js_reset` / `%reset` / atexit tear the session down.
|
|
48
55
|
- Scalars / `None` materialize; empty `{}` stays a handle; everything
|
|
49
56
|
else is a `JsHandle` until `str()` / attrs / a call.
|
|
50
|
-
-
|
|
51
|
-
|
|
57
|
+
- Notebook: `import cc_node` registers `%%js` (no `%load_ext`). Same
|
|
58
|
+
session as `require()`.
|
|
52
59
|
- `eval()` is one RTT, no extra globals. `%%js` / `eval_cell` install
|
|
53
60
|
cwd `require` once.
|
|
54
61
|
- `--bind` is `Object.assign(globalThis, …)` of names you name (wire
|
|
@@ -58,47 +65,58 @@ measurable wire. N domains = N processes.
|
|
|
58
65
|
|
|
59
66
|
```
|
|
60
67
|
pip install concurrent-c-node # needs node on PATH
|
|
61
|
-
pip install 'concurrent-c-node[jupyter]' #
|
|
68
|
+
pip install 'concurrent-c-node[jupyter]' # IPython (%%js); require() does not need this
|
|
62
69
|
python -m cc_node.examples.use_node
|
|
63
70
|
python -m cc_node.examples.bench_wire
|
|
64
71
|
python -m cc_node.benchmarks.multi_domain
|
|
65
|
-
python -m cc_node.benchmarks.vs_alts # vs DIY node / pythonmonkey / mini-racer
|
|
72
|
+
python -m cc_node.benchmarks.vs_alts # vs pythonia / DIY node / pythonmonkey / mini-racer
|
|
66
73
|
```
|
|
67
74
|
|
|
68
75
|
## Jupyter / Colab
|
|
69
76
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
kernel, not a spawn per cell (~28ms).
|
|
77
|
+
Same verb as pythonia: `require`. No `%load_ext`, no `create()`, no
|
|
78
|
+
`destroy()` for the happy path. One session child for the kernel;
|
|
79
|
+
`console.log` lands in the cell.
|
|
74
80
|
|
|
75
81
|
```python
|
|
76
82
|
%pip install concurrent-c-node
|
|
77
83
|
# if `node` is missing (typical Colab):
|
|
78
84
|
!apt-get install -y nodejs
|
|
79
|
-
%load_ext cc_node
|
|
80
85
|
|
|
86
|
+
from cc_node import require
|
|
87
|
+
require('lodash').chunk([1, 2, 3, 4, 5], 2)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`import cc_node` also registers `%%js` (IPython already running; the
|
|
91
|
+
`[jupyter]` extra is only if you want magics without IPython already
|
|
92
|
+
installed).
|
|
93
|
+
|
|
94
|
+
```python
|
|
81
95
|
%%js
|
|
82
|
-
|
|
83
|
-
|
|
96
|
+
console.log('hi') # shows in the cell
|
|
97
|
+
require('lodash').chunk([1, 2, 3, 4, 5], 2)
|
|
84
98
|
```
|
|
85
99
|
|
|
86
100
|
```python
|
|
87
101
|
xs = [1, 2, 3, 4]
|
|
88
102
|
|
|
89
103
|
%%js -b xs -t chunks
|
|
90
|
-
xs.map(x => x * 2)
|
|
104
|
+
xs.map(x => x * 2) # wire types only; no pickle fallback
|
|
91
105
|
```
|
|
92
106
|
|
|
93
107
|
| | |
|
|
94
108
|
|---|---|
|
|
95
|
-
|
|
|
96
|
-
|
|
|
109
|
+
| `from cc_node import require` | session `require`; spawns on first call |
|
|
110
|
+
| `import cc_node` | registers magics; does **not** spawn until first `require()` / `%%js` |
|
|
111
|
+
| `%load_ext cc_node` | same, idempotent; not required |
|
|
112
|
+
| `%js 1+1` / `%%js` | eval on the same session; last expression is the result |
|
|
97
113
|
| `-b xs` / `--bind xs,n` | publish those Python names on `globalThis` for the cell |
|
|
98
114
|
| `-t chunks` / `--to` | store the result in the notebook namespace |
|
|
99
115
|
| `%js_stats` | handle-table size (spawns if needed) |
|
|
100
|
-
| `%js_reset` | `destroy()` the
|
|
101
|
-
| `cc_node.
|
|
116
|
+
| `%js_reset` / `cc_node.reset()` | `destroy()` the session child; `%reset` does this too |
|
|
117
|
+
| `cc_node.get()` | the session the magics use; `create()` is still a private child |
|
|
118
|
+
| `cc_node.kernel()` | alias of `get()` |
|
|
119
|
+
| `cc_node.require('fs')` | `get().require('fs')` |
|
|
102
120
|
| `JsHandle` display | cheap `JsHandle #3` — repr does not cross the wire |
|
|
103
121
|
|
|
104
122
|
`eval()` is unchanged (one RTT, no extra globals). `%%js` / `eval_cell`
|
|
@@ -136,25 +154,30 @@ Colab is not that. There, default `create()` blocks the kernel thread —
|
|
|
136
154
|
Wire: line-JSON on dedicated fds (stdio stays yours). Bulk spill: private
|
|
137
155
|
0700 dir, 0600 files, removed with the bridge.
|
|
138
156
|
|
|
139
|
-
### Vs pythonmonkey / mini-racer / DIY node
|
|
157
|
+
### Vs pythonia / pythonmonkey / mini-racer / DIY node
|
|
140
158
|
|
|
141
|
-
Most “JS from Python” libraries are **not Node**.
|
|
142
|
-
|
|
159
|
+
Most “JS from Python” libraries are **not Node**. pythonia (PyPI
|
|
160
|
+
[`javascript`](https://pypi.org/project/javascript/), JSPyBridge) is the
|
|
161
|
+
packaged peer that is: `require()` on import, one child. Bulk is a
|
|
162
|
+
**sum** over 1M floats (`.length` on an in-process wrapper is free and
|
|
163
|
+
lies). Snapshot:
|
|
143
164
|
[`cc_node_vs_alts_20260813.txt`](https://github.com/sreekotay/concurrent-c/blob/main/perf/baselines/cc_node_vs_alts_20260813.txt)
|
|
144
165
|
· harness: [`benchmarks/vs_alts.py`](https://github.com/sreekotay/concurrent-c/blob/main/pypi/cc-node/cc_node/benchmarks/vs_alts.py).
|
|
145
166
|
|
|
146
|
-
| | cc-node | DIY JSON stdio | `node -e` each | pythonmonkey | mini-racer |
|
|
147
|
-
|
|
148
|
-
| identity RTT | **20µs** |
|
|
149
|
-
| callback | **
|
|
150
|
-
| 8MB typed / list | **
|
|
151
|
-
| `require('fs')` | yes | yes | yes | no | no |
|
|
152
|
-
| process | child `node` | child `node` | new process/call | SpiderMonkey in-process | V8 isolate |
|
|
167
|
+
| | cc-node | pythonia | DIY JSON stdio | `node -e` each | pythonmonkey | mini-racer |
|
|
168
|
+
|---|---|---|---|---|---|---|
|
|
169
|
+
| identity RTT | **20µs** | 42µs | 19µs | 23ms | **<1µs** | 104µs |
|
|
170
|
+
| callback | **40µs** | 106µs | — | — | 1µs | — |
|
|
171
|
+
| 8MB typed / list | **7.5ms shm** / 266ms | — / 481ms | — / 155ms | — | — / 609ms | — / 78ms |
|
|
172
|
+
| `require('fs')` | yes | yes | yes | yes | no | no |
|
|
173
|
+
| process | child `node` | child `node` | child `node` | new process/call | SpiderMonkey in-process | V8 isolate |
|
|
153
174
|
|
|
154
175
|
Tiny scalars: pythonmonkey’s in-process SM beats a child. Real Node
|
|
155
176
|
(`require('fs')`, native addons, callbacks, stdout stays yours): this
|
|
156
|
-
package
|
|
157
|
-
|
|
177
|
+
package — ~2× pythonia on identity, shm for bulk, Python callables are
|
|
178
|
+
sync (pythonia’s JS side sees a Promise). Isolated `node -e` per call is
|
|
179
|
+
~1000× a persistent child. Optional engines SKIP if not importable —
|
|
180
|
+
not package deps.
|
|
158
181
|
|
|
159
182
|
The other direction (Python from Node):
|
|
160
183
|
[`concurrent-c-python`](https://www.npmjs.com/package/concurrent-c-python)
|
|
@@ -169,6 +192,7 @@ vs pymport / ncp / pythonia.
|
|
|
169
192
|
`destroy()`; afterwards: `bridge is closed`.
|
|
170
193
|
- `eval_cell(src, bindings=)` is the notebook door (`%%js`): cwd
|
|
171
194
|
`require` once, optional `globalThis` binds; `eval()` stays one RTT.
|
|
195
|
+
`get()` / `require()` / `eval()` at module level share that session.
|
|
172
196
|
- Crash isolation, not a sandbox. `destroy()` is cooperative; an
|
|
173
197
|
in-flight CPU-bound call finishes or you kill the child
|
|
174
198
|
([`bridge_stress.md`](https://github.com/sreekotay/concurrent-c/blob/main/stress/bridge/bridge_stress.md)).
|
|
@@ -207,9 +231,11 @@ total(array.array('d', range(1_000_000)))
|
|
|
207
231
|
## Common issues
|
|
208
232
|
|
|
209
233
|
**`Cannot find module`.** `require` / `import` resolve from the Python
|
|
210
|
-
process cwd (`node_modules` next to your program), not from
|
|
211
|
-
site-packages
|
|
212
|
-
|
|
234
|
+
process cwd (`node_modules` next to your notebook or program), not from
|
|
235
|
+
this wheel’s site-packages, and this package does **not** `npm install`
|
|
236
|
+
on a miss (pythonia does). `npm install lodash` in that directory is
|
|
237
|
+
the fix; or `create(node=…)` / `CC_NODE_BIN` when the wrong Node is on
|
|
238
|
+
`PATH`.
|
|
213
239
|
|
|
214
240
|
### Empty `{}` stays a handle
|
|
215
241
|
|
|
@@ -267,7 +293,7 @@ Both bridges (npm OIDC + PyPI OIDC):
|
|
|
267
293
|
|
|
268
294
|
Examples: `use_node`, `bench_wire`, `benchmarks.multi_domain`,
|
|
269
295
|
`benchmarks.vs_alts`.
|
|
270
|
-
Jupyter:
|
|
296
|
+
Jupyter: `from cc_node import require` (or `import cc_node` then `%%js`).
|
|
271
297
|
Stress: [`stress/bridge/`](https://github.com/sreekotay/concurrent-c/tree/main/stress/bridge).
|
|
272
298
|
Own hot path in C/CC → native module (40–90ns) —
|
|
273
299
|
[JS / Python interop](https://github.com/sreekotay/concurrent-c/blob/main/docs/js-py-modules.md).
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
"""cc-node: JavaScript (and every npm package) from Python.
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
_ = js.require('lodash') # resolved from YOUR cwd's node_modules
|
|
3
|
+
from cc_node import require
|
|
4
|
+
_ = require('lodash') # session child; cwd node_modules
|
|
6
5
|
_.chunk([1, 2, 3, 4, 5], 2) # [[1, 2], [3, 4], [5]]
|
|
6
|
+
|
|
7
|
+
import cc_node
|
|
8
|
+
js = cc_node.create() # a private child, not the session
|
|
7
9
|
js.destroy()
|
|
8
10
|
|
|
9
11
|
The mirror of the cc-python bridge, same rules pointed the other way:
|
|
@@ -19,8 +21,8 @@ Handles never cross domains; every door after destroy() answers
|
|
|
19
21
|
articulately; destroy is idempotent and `with cc_node.create() as js:`
|
|
20
22
|
scopes it.
|
|
21
23
|
|
|
22
|
-
Notebooks (Jupyter / Colab):
|
|
23
|
-
|
|
24
|
+
Notebooks (Jupyter / Colab): `from cc_node import require` or
|
|
25
|
+
`import cc_node` then `%%js` — one session, same calling convention.
|
|
24
26
|
"""
|
|
25
27
|
import array
|
|
26
28
|
import atexit
|
|
@@ -36,10 +38,12 @@ import sys
|
|
|
36
38
|
import tempfile
|
|
37
39
|
|
|
38
40
|
__all__ = [
|
|
39
|
-
"create", "
|
|
41
|
+
"create", "get", "reset", "kernel", "reset_kernel",
|
|
42
|
+
"require", "eval", "eval_cell", "import_module", "stats",
|
|
43
|
+
"JsError", "JsHandle",
|
|
40
44
|
"load_ipython_extension", "unload_ipython_extension", "__version__",
|
|
41
45
|
]
|
|
42
|
-
__version__ = "0.
|
|
46
|
+
__version__ = "0.23.1"
|
|
43
47
|
|
|
44
48
|
_NO_NODE = (
|
|
45
49
|
"cc-node: no node executable (install Node, or set CC_NODE_BIN). "
|
|
@@ -161,12 +165,14 @@ class Bridge:
|
|
|
161
165
|
# numbers in the child, so the broker learns them from the env.
|
|
162
166
|
req_r, req_w = os.pipe()
|
|
163
167
|
resp_r, resp_w = os.pipe()
|
|
168
|
+
preload = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
|
169
|
+
"stdio_line.cjs")
|
|
164
170
|
env = dict(os.environ,
|
|
165
171
|
CC_WIRE_IN=str(req_r), CC_WIRE_OUT=str(resp_w),
|
|
166
172
|
CC_NODE_SHM_DIR=self._shm_dir)
|
|
167
173
|
try:
|
|
168
174
|
self._p = subprocess.Popen(
|
|
169
|
-
[node, broker],
|
|
175
|
+
[node, "-r", preload, broker],
|
|
170
176
|
pass_fds=(req_r, resp_w),
|
|
171
177
|
env=env,
|
|
172
178
|
cwd=os.getcwd(),
|
|
@@ -645,19 +651,19 @@ def create(node=None):
|
|
|
645
651
|
return Bridge(node=node)
|
|
646
652
|
|
|
647
653
|
|
|
648
|
-
|
|
654
|
+
_session = None
|
|
649
655
|
|
|
650
656
|
|
|
651
|
-
def
|
|
652
|
-
"""
|
|
653
|
-
|
|
654
|
-
global
|
|
655
|
-
b =
|
|
657
|
+
def get(node=None):
|
|
658
|
+
"""Session domain: one child for this process (the Jupyter kernel).
|
|
659
|
+
Lazy. Magics share it. `create()` is still a private child."""
|
|
660
|
+
global _session
|
|
661
|
+
b = _session
|
|
656
662
|
if b is not None and not b.closed:
|
|
657
663
|
if node is not None and node != b._node_bin:
|
|
658
664
|
raise JsError(
|
|
659
|
-
"cc-node:
|
|
660
|
-
"
|
|
665
|
+
"cc-node: get() already live with a different node; "
|
|
666
|
+
"reset() / %js_reset first")
|
|
661
667
|
return b
|
|
662
668
|
b = create(node=node)
|
|
663
669
|
try:
|
|
@@ -665,15 +671,15 @@ def kernel(node=None):
|
|
|
665
671
|
except Exception:
|
|
666
672
|
b.destroy()
|
|
667
673
|
raise
|
|
668
|
-
|
|
674
|
+
_session = b
|
|
669
675
|
return b
|
|
670
676
|
|
|
671
677
|
|
|
672
|
-
def
|
|
673
|
-
"""Destroy the
|
|
674
|
-
global
|
|
675
|
-
b =
|
|
676
|
-
|
|
678
|
+
def reset():
|
|
679
|
+
"""Destroy the session domain. Next get() / %%js spawns."""
|
|
680
|
+
global _session
|
|
681
|
+
b = _session
|
|
682
|
+
_session = None
|
|
677
683
|
if b is not None:
|
|
678
684
|
try:
|
|
679
685
|
b.destroy()
|
|
@@ -681,10 +687,61 @@ def reset_kernel():
|
|
|
681
687
|
pass
|
|
682
688
|
|
|
683
689
|
|
|
690
|
+
kernel = get
|
|
691
|
+
reset_kernel = reset
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
def require(name):
|
|
695
|
+
return get().require(name)
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
def eval(src): # noqa: A001 — session eval, pythonia-shaped
|
|
699
|
+
return get().eval(src)
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
def eval_cell(src, bindings=None):
|
|
703
|
+
return get().eval_cell(src, bindings)
|
|
704
|
+
|
|
705
|
+
|
|
706
|
+
def import_module(name):
|
|
707
|
+
return get().import_module(name)
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
def stats():
|
|
711
|
+
return get().stats()
|
|
712
|
+
|
|
713
|
+
|
|
684
714
|
def load_ipython_extension(ip):
|
|
685
715
|
from .magics import load_ipython_extension as _load
|
|
686
716
|
_load(ip)
|
|
717
|
+
if getattr(ip, "_cc_node_lifecycle", False):
|
|
718
|
+
return
|
|
719
|
+
ip._cc_node_lifecycle = True
|
|
720
|
+
orig = getattr(ip, "reset", None)
|
|
721
|
+
if orig is not None:
|
|
722
|
+
def _reset(*args, **kwargs):
|
|
723
|
+
reset()
|
|
724
|
+
return orig(*args, **kwargs)
|
|
725
|
+
ip.reset = _reset
|
|
687
726
|
|
|
688
727
|
|
|
689
728
|
def unload_ipython_extension(ip):
|
|
690
|
-
|
|
729
|
+
reset()
|
|
730
|
+
|
|
731
|
+
|
|
732
|
+
def _boot_ipython():
|
|
733
|
+
# Already in a kernel: IPython is in sys.modules. Do not import it
|
|
734
|
+
# just because the extra is installed — scripts stay spawn-free.
|
|
735
|
+
if "IPython" not in sys.modules:
|
|
736
|
+
return
|
|
737
|
+
try:
|
|
738
|
+
from IPython import get_ipython
|
|
739
|
+
except ImportError:
|
|
740
|
+
return
|
|
741
|
+
ip = get_ipython()
|
|
742
|
+
if ip is None:
|
|
743
|
+
return
|
|
744
|
+
load_ipython_extension(ip)
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
_boot_ipython()
|
|
@@ -6,8 +6,9 @@ Real Node (this package)
|
|
|
6
6
|
`require('fs')`, native addons, the npm in cwd, crash isolation.
|
|
7
7
|
Honest alternatives: drive a `node` child yourself. Spawn-per-call
|
|
8
8
|
(`node -e`) is what people write first. A persistent JSON-stdio loop
|
|
9
|
-
is the DIY that actually competes.
|
|
10
|
-
|
|
9
|
+
is the DIY that actually competes. pythonia (PyPI `javascript`,
|
|
10
|
+
JSPyBridge) is the packaged peer with the same DX — `require()` on
|
|
11
|
+
import, one child. PyExecJS was eval-a-string; abandoned.
|
|
11
12
|
|
|
12
13
|
A JS engine inside CPython (not Node)
|
|
13
14
|
pythonmonkey — SpiderMonkey + CommonJS `require` of *pure JS*.
|
|
@@ -218,6 +219,77 @@ def bench_cc_node():
|
|
|
218
219
|
js.destroy()
|
|
219
220
|
|
|
220
221
|
|
|
222
|
+
def bench_pythonia():
|
|
223
|
+
"""JSPyBridge: PyPI `javascript`, npm twin `pythonia`. Real Node child.
|
|
224
|
+
A Python callable the JS side sees is a Promise (await in the child)."""
|
|
225
|
+
try:
|
|
226
|
+
t0 = time.perf_counter()
|
|
227
|
+
from javascript import eval_js, require
|
|
228
|
+
eval_js("return 1")
|
|
229
|
+
_result("pythonia.spawn_ms", round((time.perf_counter() - t0) * 1000))
|
|
230
|
+
except ImportError:
|
|
231
|
+
_result("pythonia.rtt_us", "SKIP not installed")
|
|
232
|
+
_cap("pythonia.require_path", False, "not installed")
|
|
233
|
+
_cap("pythonia.require_fs", False, "not installed")
|
|
234
|
+
return
|
|
235
|
+
|
|
236
|
+
f = eval_js("return (x) => x")
|
|
237
|
+
f(1)
|
|
238
|
+
dt = _time_loop(500, f)
|
|
239
|
+
_result("pythonia.rtt_us", _us(dt, 500))
|
|
240
|
+
|
|
241
|
+
try:
|
|
242
|
+
g = eval_js(
|
|
243
|
+
"return async (cb) => { const v = await cb(21); return v * 2 }")
|
|
244
|
+
if g(lambda x: x + 1) != 44:
|
|
245
|
+
raise RuntimeError("callback mismatch")
|
|
246
|
+
dt = _time_loop(200, lambda _i: g(lambda x: x + 1))
|
|
247
|
+
_result("pythonia.callback_roundtrip_us", _us(dt, 200))
|
|
248
|
+
_cap("pythonia.python_callback", True,
|
|
249
|
+
"JS sees a Promise; await in the child")
|
|
250
|
+
except Exception as e:
|
|
251
|
+
_result("pythonia.callback_roundtrip_us",
|
|
252
|
+
"SKIP %s" % type(e).__name__)
|
|
253
|
+
_cap("pythonia.python_callback", False, str(e)[:80])
|
|
254
|
+
|
|
255
|
+
_result("pythonia.bulk_8mb_shm_ms", "SKIP JSON IPC, no shm")
|
|
256
|
+
try:
|
|
257
|
+
biglist = _bulk_list()
|
|
258
|
+
sm = eval_js("return " + SUM_JS)
|
|
259
|
+
got = sm(biglist)
|
|
260
|
+
expect = _bulk_sum_expect(biglist)
|
|
261
|
+
if abs(float(got) - expect) > 1e-3:
|
|
262
|
+
raise RuntimeError("pythonia bulk sum mismatch")
|
|
263
|
+
t0 = time.perf_counter()
|
|
264
|
+
for _ in range(3):
|
|
265
|
+
sm(biglist)
|
|
266
|
+
_result("pythonia.bulk_8mb_json_list_ms",
|
|
267
|
+
round((time.perf_counter() - t0) / 3 * 1000))
|
|
268
|
+
except Exception as e:
|
|
269
|
+
_result("pythonia.bulk_8mb_json_list_ms",
|
|
270
|
+
"SKIP %s" % type(e).__name__)
|
|
271
|
+
|
|
272
|
+
_cap("pythonia.require_path",
|
|
273
|
+
require("path").join("a", "b") == "a/b")
|
|
274
|
+
fd, tmp = tempfile.mkstemp()
|
|
275
|
+
try:
|
|
276
|
+
os.write(fd, b"hi")
|
|
277
|
+
os.close(fd)
|
|
278
|
+
got = require("fs").readFileSync(tmp, "utf8")
|
|
279
|
+
_cap("pythonia.require_fs", got == "hi")
|
|
280
|
+
finally:
|
|
281
|
+
try:
|
|
282
|
+
os.unlink(tmp)
|
|
283
|
+
except OSError:
|
|
284
|
+
pass
|
|
285
|
+
try:
|
|
286
|
+
then = eval_js(
|
|
287
|
+
"return async (x) => { await Promise.resolve(); return x * 2 }")
|
|
288
|
+
_cap("pythonia.thenable", then(21) == 42)
|
|
289
|
+
except Exception as e:
|
|
290
|
+
_cap("pythonia.thenable", False, str(e)[:80])
|
|
291
|
+
|
|
292
|
+
|
|
221
293
|
def bench_diy_stdio():
|
|
222
294
|
t0 = time.perf_counter()
|
|
223
295
|
diy = DiyStdio()
|
|
@@ -408,6 +480,7 @@ def main():
|
|
|
408
480
|
print("SKIP need node on PATH")
|
|
409
481
|
return 1
|
|
410
482
|
bench_cc_node()
|
|
483
|
+
bench_pythonia()
|
|
411
484
|
bench_diy_stdio()
|
|
412
485
|
bench_spawn_each()
|
|
413
486
|
bench_pythonmonkey()
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
from IPython.core.magic import Magics, line_cell_magic, line_magic, magics_class
|
|
3
3
|
from IPython.core.magic_arguments import argument, magic_arguments, parse_argstring
|
|
4
4
|
|
|
5
|
-
from . import JsError,
|
|
5
|
+
from . import JsError, get, reset
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
def _bind_map(names, ns):
|
|
@@ -30,7 +30,7 @@ class CcNodeMagics(Magics):
|
|
|
30
30
|
@argument("code", nargs="*", help="JS source (line magic)")
|
|
31
31
|
@line_cell_magic
|
|
32
32
|
def js(self, line, cell=None):
|
|
33
|
-
"""Eval JavaScript on the
|
|
33
|
+
"""Eval JavaScript on the session domain (`cc_node.get()`).
|
|
34
34
|
|
|
35
35
|
Line: %js 1 + 1
|
|
36
36
|
Cell: %%js then a body; last expression is the result.
|
|
@@ -45,20 +45,20 @@ class CcNodeMagics(Magics):
|
|
|
45
45
|
raise JsError("cc-node: empty %js / %%js")
|
|
46
46
|
ns = self.shell.user_ns
|
|
47
47
|
bindings = _bind_map(args.bind, ns)
|
|
48
|
-
result =
|
|
48
|
+
result = get().eval_cell(src, bindings or None)
|
|
49
49
|
if args.to:
|
|
50
50
|
ns[args.to] = result
|
|
51
51
|
return result
|
|
52
52
|
|
|
53
53
|
@line_magic
|
|
54
54
|
def js_reset(self, line):
|
|
55
|
-
"""Destroy the
|
|
56
|
-
|
|
55
|
+
"""Destroy the session Node child. Next %%js / get() spawns again."""
|
|
56
|
+
reset()
|
|
57
57
|
|
|
58
58
|
@line_magic
|
|
59
59
|
def js_stats(self, line):
|
|
60
|
-
"""Handle-table size of the
|
|
61
|
-
return
|
|
60
|
+
"""Handle-table size of the session domain (spawns if needed)."""
|
|
61
|
+
return get().stats()
|
|
62
62
|
|
|
63
63
|
|
|
64
64
|
def load_ipython_extension(ip):
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/* Long-lived child: inherited stdout is often a pipe (Jupyter, nohup).
|
|
3
|
+
* Node then block-buffers console.log until the buffer fills or exit.
|
|
4
|
+
* Blocking writes make print land in the cell as it happens. TTY is
|
|
5
|
+
* already blocking — this is a no-op there. */
|
|
6
|
+
function block(s) {
|
|
7
|
+
try {
|
|
8
|
+
if (s && s._handle && typeof s._handle.setBlocking === 'function')
|
|
9
|
+
s._handle.setBlocking(true);
|
|
10
|
+
} catch (e) { /* ignore */ }
|
|
11
|
+
}
|
|
12
|
+
block(process.stdout);
|
|
13
|
+
block(process.stderr);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: concurrent-c-node
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.23.1
|
|
4
4
|
Summary: JavaScript and npm packages from Python over the Concurrent-C bridge: one spawned Node child per domain, host-controlled lifetime.
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Repository, https://github.com/sreekotay/concurrent-c
|
|
@@ -28,15 +28,19 @@ bridge):
|
|
|
28
28
|
[JS / Python interop](https://github.com/sreekotay/concurrent-c/blob/main/docs/js-py-modules.md).
|
|
29
29
|
|
|
30
30
|
```python
|
|
31
|
-
import
|
|
32
|
-
|
|
33
|
-
js = cc_node.create() # always a child `node` process
|
|
34
|
-
_ = js.require('lodash') # cwd node_modules
|
|
31
|
+
from cc_node import require # one session child (lazy)
|
|
32
|
+
_ = require('lodash') # cwd node_modules
|
|
35
33
|
_.chunk([1, 2, 3, 4, 5], 2) # [[1, 2], [3, 4], [5]]
|
|
34
|
+
```
|
|
36
35
|
|
|
36
|
+
A private child (`create()`) is still there when you want N Nodes or
|
|
37
|
+
an explicit lifetime — not required for the first call.
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
import cc_node
|
|
41
|
+
js = cc_node.create() # a private Node, not the session
|
|
37
42
|
semver = js.require('semver')
|
|
38
43
|
semver.satisfies('1.2.3', '^1.0.0') # True
|
|
39
|
-
|
|
40
44
|
js.destroy() # or: with cc_node.create() as js:
|
|
41
45
|
```
|
|
42
46
|
|
|
@@ -44,12 +48,13 @@ The other direction (Python from Node):
|
|
|
44
48
|
[`concurrent-c-python`](https://www.npmjs.com/package/concurrent-c-python)
|
|
45
49
|
(in-process by default; vs pymport / ncp / pythonia in that README).
|
|
46
50
|
|
|
47
|
-
Every `create()`
|
|
48
|
-
measurable wire. N domains = N processes.
|
|
51
|
+
Every `create()` is a separate Node — real addons, crash isolation,
|
|
52
|
+
measurable wire. N domains = N processes. `require()` / `get()` share
|
|
53
|
+
one session for the process (the Jupyter kernel).
|
|
49
54
|
|
|
50
55
|
| | this package | CC hosted (`cc_js_new(false, …)`) |
|
|
51
56
|
|---|---|---|
|
|
52
|
-
| API | `
|
|
57
|
+
| API | `require()` / `create()` | `.ccs` program |
|
|
53
58
|
| Where | child `node` | libnode in-process |
|
|
54
59
|
| Hot call | ~105µs RTT | sub-µs (needs libnode) |
|
|
55
60
|
| Bulk | shm (~9.5ms / 8MB) | in-process |
|
|
@@ -60,10 +65,12 @@ measurable wire. N domains = N processes.
|
|
|
60
65
|
|
|
61
66
|
- Always a child `node`. A call blocks until JS answers; thenables wait
|
|
62
67
|
in the child. No `{ async: true }`.
|
|
68
|
+
- `from cc_node import require` is the session. `create()` is a private
|
|
69
|
+
child. `reset()` / `%js_reset` / `%reset` / atexit tear the session down.
|
|
63
70
|
- Scalars / `None` materialize; empty `{}` stays a handle; everything
|
|
64
71
|
else is a `JsHandle` until `str()` / attrs / a call.
|
|
65
|
-
-
|
|
66
|
-
|
|
72
|
+
- Notebook: `import cc_node` registers `%%js` (no `%load_ext`). Same
|
|
73
|
+
session as `require()`.
|
|
67
74
|
- `eval()` is one RTT, no extra globals. `%%js` / `eval_cell` install
|
|
68
75
|
cwd `require` once.
|
|
69
76
|
- `--bind` is `Object.assign(globalThis, …)` of names you name (wire
|
|
@@ -73,47 +80,58 @@ measurable wire. N domains = N processes.
|
|
|
73
80
|
|
|
74
81
|
```
|
|
75
82
|
pip install concurrent-c-node # needs node on PATH
|
|
76
|
-
pip install 'concurrent-c-node[jupyter]' #
|
|
83
|
+
pip install 'concurrent-c-node[jupyter]' # IPython (%%js); require() does not need this
|
|
77
84
|
python -m cc_node.examples.use_node
|
|
78
85
|
python -m cc_node.examples.bench_wire
|
|
79
86
|
python -m cc_node.benchmarks.multi_domain
|
|
80
|
-
python -m cc_node.benchmarks.vs_alts # vs DIY node / pythonmonkey / mini-racer
|
|
87
|
+
python -m cc_node.benchmarks.vs_alts # vs pythonia / DIY node / pythonmonkey / mini-racer
|
|
81
88
|
```
|
|
82
89
|
|
|
83
90
|
## Jupyter / Colab
|
|
84
91
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
kernel, not a spawn per cell (~28ms).
|
|
92
|
+
Same verb as pythonia: `require`. No `%load_ext`, no `create()`, no
|
|
93
|
+
`destroy()` for the happy path. One session child for the kernel;
|
|
94
|
+
`console.log` lands in the cell.
|
|
89
95
|
|
|
90
96
|
```python
|
|
91
97
|
%pip install concurrent-c-node
|
|
92
98
|
# if `node` is missing (typical Colab):
|
|
93
99
|
!apt-get install -y nodejs
|
|
94
|
-
%load_ext cc_node
|
|
95
100
|
|
|
101
|
+
from cc_node import require
|
|
102
|
+
require('lodash').chunk([1, 2, 3, 4, 5], 2)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`import cc_node` also registers `%%js` (IPython already running; the
|
|
106
|
+
`[jupyter]` extra is only if you want magics without IPython already
|
|
107
|
+
installed).
|
|
108
|
+
|
|
109
|
+
```python
|
|
96
110
|
%%js
|
|
97
|
-
|
|
98
|
-
|
|
111
|
+
console.log('hi') # shows in the cell
|
|
112
|
+
require('lodash').chunk([1, 2, 3, 4, 5], 2)
|
|
99
113
|
```
|
|
100
114
|
|
|
101
115
|
```python
|
|
102
116
|
xs = [1, 2, 3, 4]
|
|
103
117
|
|
|
104
118
|
%%js -b xs -t chunks
|
|
105
|
-
xs.map(x => x * 2)
|
|
119
|
+
xs.map(x => x * 2) # wire types only; no pickle fallback
|
|
106
120
|
```
|
|
107
121
|
|
|
108
122
|
| | |
|
|
109
123
|
|---|---|
|
|
110
|
-
|
|
|
111
|
-
|
|
|
124
|
+
| `from cc_node import require` | session `require`; spawns on first call |
|
|
125
|
+
| `import cc_node` | registers magics; does **not** spawn until first `require()` / `%%js` |
|
|
126
|
+
| `%load_ext cc_node` | same, idempotent; not required |
|
|
127
|
+
| `%js 1+1` / `%%js` | eval on the same session; last expression is the result |
|
|
112
128
|
| `-b xs` / `--bind xs,n` | publish those Python names on `globalThis` for the cell |
|
|
113
129
|
| `-t chunks` / `--to` | store the result in the notebook namespace |
|
|
114
130
|
| `%js_stats` | handle-table size (spawns if needed) |
|
|
115
|
-
| `%js_reset` | `destroy()` the
|
|
116
|
-
| `cc_node.
|
|
131
|
+
| `%js_reset` / `cc_node.reset()` | `destroy()` the session child; `%reset` does this too |
|
|
132
|
+
| `cc_node.get()` | the session the magics use; `create()` is still a private child |
|
|
133
|
+
| `cc_node.kernel()` | alias of `get()` |
|
|
134
|
+
| `cc_node.require('fs')` | `get().require('fs')` |
|
|
117
135
|
| `JsHandle` display | cheap `JsHandle #3` — repr does not cross the wire |
|
|
118
136
|
|
|
119
137
|
`eval()` is unchanged (one RTT, no extra globals). `%%js` / `eval_cell`
|
|
@@ -151,25 +169,30 @@ Colab is not that. There, default `create()` blocks the kernel thread —
|
|
|
151
169
|
Wire: line-JSON on dedicated fds (stdio stays yours). Bulk spill: private
|
|
152
170
|
0700 dir, 0600 files, removed with the bridge.
|
|
153
171
|
|
|
154
|
-
### Vs pythonmonkey / mini-racer / DIY node
|
|
172
|
+
### Vs pythonia / pythonmonkey / mini-racer / DIY node
|
|
155
173
|
|
|
156
|
-
Most “JS from Python” libraries are **not Node**.
|
|
157
|
-
|
|
174
|
+
Most “JS from Python” libraries are **not Node**. pythonia (PyPI
|
|
175
|
+
[`javascript`](https://pypi.org/project/javascript/), JSPyBridge) is the
|
|
176
|
+
packaged peer that is: `require()` on import, one child. Bulk is a
|
|
177
|
+
**sum** over 1M floats (`.length` on an in-process wrapper is free and
|
|
178
|
+
lies). Snapshot:
|
|
158
179
|
[`cc_node_vs_alts_20260813.txt`](https://github.com/sreekotay/concurrent-c/blob/main/perf/baselines/cc_node_vs_alts_20260813.txt)
|
|
159
180
|
· harness: [`benchmarks/vs_alts.py`](https://github.com/sreekotay/concurrent-c/blob/main/pypi/cc-node/cc_node/benchmarks/vs_alts.py).
|
|
160
181
|
|
|
161
|
-
| | cc-node | DIY JSON stdio | `node -e` each | pythonmonkey | mini-racer |
|
|
162
|
-
|
|
163
|
-
| identity RTT | **20µs** |
|
|
164
|
-
| callback | **
|
|
165
|
-
| 8MB typed / list | **
|
|
166
|
-
| `require('fs')` | yes | yes | yes | no | no |
|
|
167
|
-
| process | child `node` | child `node` | new process/call | SpiderMonkey in-process | V8 isolate |
|
|
182
|
+
| | cc-node | pythonia | DIY JSON stdio | `node -e` each | pythonmonkey | mini-racer |
|
|
183
|
+
|---|---|---|---|---|---|---|
|
|
184
|
+
| identity RTT | **20µs** | 42µs | 19µs | 23ms | **<1µs** | 104µs |
|
|
185
|
+
| callback | **40µs** | 106µs | — | — | 1µs | — |
|
|
186
|
+
| 8MB typed / list | **7.5ms shm** / 266ms | — / 481ms | — / 155ms | — | — / 609ms | — / 78ms |
|
|
187
|
+
| `require('fs')` | yes | yes | yes | yes | no | no |
|
|
188
|
+
| process | child `node` | child `node` | child `node` | new process/call | SpiderMonkey in-process | V8 isolate |
|
|
168
189
|
|
|
169
190
|
Tiny scalars: pythonmonkey’s in-process SM beats a child. Real Node
|
|
170
191
|
(`require('fs')`, native addons, callbacks, stdout stays yours): this
|
|
171
|
-
package
|
|
172
|
-
|
|
192
|
+
package — ~2× pythonia on identity, shm for bulk, Python callables are
|
|
193
|
+
sync (pythonia’s JS side sees a Promise). Isolated `node -e` per call is
|
|
194
|
+
~1000× a persistent child. Optional engines SKIP if not importable —
|
|
195
|
+
not package deps.
|
|
173
196
|
|
|
174
197
|
The other direction (Python from Node):
|
|
175
198
|
[`concurrent-c-python`](https://www.npmjs.com/package/concurrent-c-python)
|
|
@@ -184,6 +207,7 @@ vs pymport / ncp / pythonia.
|
|
|
184
207
|
`destroy()`; afterwards: `bridge is closed`.
|
|
185
208
|
- `eval_cell(src, bindings=)` is the notebook door (`%%js`): cwd
|
|
186
209
|
`require` once, optional `globalThis` binds; `eval()` stays one RTT.
|
|
210
|
+
`get()` / `require()` / `eval()` at module level share that session.
|
|
187
211
|
- Crash isolation, not a sandbox. `destroy()` is cooperative; an
|
|
188
212
|
in-flight CPU-bound call finishes or you kill the child
|
|
189
213
|
([`bridge_stress.md`](https://github.com/sreekotay/concurrent-c/blob/main/stress/bridge/bridge_stress.md)).
|
|
@@ -222,9 +246,11 @@ total(array.array('d', range(1_000_000)))
|
|
|
222
246
|
## Common issues
|
|
223
247
|
|
|
224
248
|
**`Cannot find module`.** `require` / `import` resolve from the Python
|
|
225
|
-
process cwd (`node_modules` next to your program), not from
|
|
226
|
-
site-packages
|
|
227
|
-
|
|
249
|
+
process cwd (`node_modules` next to your notebook or program), not from
|
|
250
|
+
this wheel’s site-packages, and this package does **not** `npm install`
|
|
251
|
+
on a miss (pythonia does). `npm install lodash` in that directory is
|
|
252
|
+
the fix; or `create(node=…)` / `CC_NODE_BIN` when the wrong Node is on
|
|
253
|
+
`PATH`.
|
|
228
254
|
|
|
229
255
|
### Empty `{}` stays a handle
|
|
230
256
|
|
|
@@ -282,7 +308,7 @@ Both bridges (npm OIDC + PyPI OIDC):
|
|
|
282
308
|
|
|
283
309
|
Examples: `use_node`, `bench_wire`, `benchmarks.multi_domain`,
|
|
284
310
|
`benchmarks.vs_alts`.
|
|
285
|
-
Jupyter:
|
|
311
|
+
Jupyter: `from cc_node import require` (or `import cc_node` then `%%js`).
|
|
286
312
|
Stress: [`stress/bridge/`](https://github.com/sreekotay/concurrent-c/tree/main/stress/bridge).
|
|
287
313
|
Own hot path in C/CC → native module (40–90ns) —
|
|
288
314
|
[JS / Python interop](https://github.com/sreekotay/concurrent-c/blob/main/docs/js-py-modules.md).
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "concurrent-c-node"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.23.1"
|
|
8
8
|
description = "JavaScript and npm packages from Python over the Concurrent-C bridge: one spawned Node child per domain, host-controlled lifetime."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.8"
|
|
@@ -26,6 +26,6 @@ Documentation = "https://github.com/sreekotay/concurrent-c/blob/main/pypi/cc-nod
|
|
|
26
26
|
packages = ["cc_node", "cc_node.examples", "cc_node.benchmarks"]
|
|
27
27
|
|
|
28
28
|
[tool.setuptools.package-data]
|
|
29
|
-
cc_node = ["broker.cjs"]
|
|
29
|
+
cc_node = ["broker.cjs", "stdio_line.cjs"]
|
|
30
30
|
"cc_node.examples" = ["*.py"]
|
|
31
31
|
"cc_node.benchmarks" = ["*.py"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{concurrent_c_node-0.22.1 → concurrent_c_node-0.23.1}/concurrent_c_node.egg-info/requires.txt
RENAMED
|
File without changes
|
{concurrent_c_node-0.22.1 → concurrent_c_node-0.23.1}/concurrent_c_node.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|