concurrent-c-node 0.22.0__tar.gz → 0.23.0__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.0 → concurrent_c_node-0.23.0}/PKG-INFO +124 -7
- concurrent_c_node-0.23.0/README.md +284 -0
- {concurrent_c_node-0.22.0 → concurrent_c_node-0.23.0}/cc_node/__init__.py +277 -14
- concurrent_c_node-0.23.0/cc_node/benchmarks/vs_alts.py +420 -0
- concurrent_c_node-0.23.0/cc_node/magics.py +65 -0
- concurrent_c_node-0.23.0/cc_node/stdio_line.cjs +13 -0
- {concurrent_c_node-0.22.0 → concurrent_c_node-0.23.0}/concurrent_c_node.egg-info/PKG-INFO +124 -7
- {concurrent_c_node-0.22.0 → concurrent_c_node-0.23.0}/concurrent_c_node.egg-info/SOURCES.txt +4 -0
- concurrent_c_node-0.23.0/concurrent_c_node.egg-info/requires.txt +3 -0
- {concurrent_c_node-0.22.0 → concurrent_c_node-0.23.0}/pyproject.toml +10 -3
- concurrent_c_node-0.22.0/README.md +0 -171
- {concurrent_c_node-0.22.0 → concurrent_c_node-0.23.0}/cc_node/benchmarks/__init__.py +0 -0
- {concurrent_c_node-0.22.0 → concurrent_c_node-0.23.0}/cc_node/benchmarks/multi_domain.py +0 -0
- {concurrent_c_node-0.22.0 → concurrent_c_node-0.23.0}/cc_node/broker.cjs +0 -0
- {concurrent_c_node-0.22.0 → concurrent_c_node-0.23.0}/cc_node/examples/__init__.py +0 -0
- {concurrent_c_node-0.22.0 → concurrent_c_node-0.23.0}/cc_node/examples/bench_wire.py +0 -0
- {concurrent_c_node-0.22.0 → concurrent_c_node-0.23.0}/cc_node/examples/use_node.py +0 -0
- {concurrent_c_node-0.22.0 → concurrent_c_node-0.23.0}/concurrent_c_node.egg-info/dependency_links.txt +0 -0
- {concurrent_c_node-0.22.0 → concurrent_c_node-0.23.0}/concurrent_c_node.egg-info/top_level.txt +0 -0
- {concurrent_c_node-0.22.0 → concurrent_c_node-0.23.0}/setup.cfg +0 -0
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: concurrent-c-node
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.23.0
|
|
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
|
|
7
7
|
Project-URL: Documentation, https://github.com/sreekotay/concurrent-c/blob/main/pypi/cc-node/README.md
|
|
8
|
-
Keywords: javascript,node,npm,interop,concurrent-c
|
|
8
|
+
Keywords: javascript,node,npm,interop,concurrent-c,jupyter
|
|
9
|
+
Classifier: Framework :: IPython
|
|
10
|
+
Classifier: Framework :: Jupyter
|
|
9
11
|
Requires-Python: >=3.8
|
|
10
12
|
Description-Content-Type: text/markdown
|
|
13
|
+
Provides-Extra: jupyter
|
|
14
|
+
Requires-Dist: ipython>=7; extra == "jupyter"
|
|
11
15
|
|
|
12
16
|
# concurrent-c-node
|
|
13
17
|
|
|
@@ -36,9 +40,12 @@ semver.satisfies('1.2.3', '^1.0.0') # True
|
|
|
36
40
|
js.destroy() # or: with cc_node.create() as js:
|
|
37
41
|
```
|
|
38
42
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
The other direction (Python from Node):
|
|
44
|
+
[`concurrent-c-python`](https://www.npmjs.com/package/concurrent-c-python)
|
|
45
|
+
(in-process by default; vs pymport / ncp / pythonia in that README).
|
|
46
|
+
|
|
47
|
+
Every `create()` here is a separate Node — real addons, crash isolation,
|
|
48
|
+
measurable wire. N domains = N processes.
|
|
42
49
|
|
|
43
50
|
| | this package | CC hosted (`cc_js_new(false, …)`) |
|
|
44
51
|
|---|---|---|
|
|
@@ -49,13 +56,94 @@ real addons, crash isolation, measurable wire. N domains = N processes.
|
|
|
49
56
|
| Parallelism | N children | one process |
|
|
50
57
|
| Crash | child dies; parent lives | shared fate |
|
|
51
58
|
|
|
59
|
+
**Cheat sheet**
|
|
60
|
+
|
|
61
|
+
- Always a child `node`. A call blocks until JS answers; thenables wait
|
|
62
|
+
in the child. No `{ async: true }`.
|
|
63
|
+
- Scalars / `None` materialize; empty `{}` stays a handle; everything
|
|
64
|
+
else is a `JsHandle` until `str()` / attrs / a call.
|
|
65
|
+
- `import cc_node` then `%%js` or `cc_node.get()` — one session, not a
|
|
66
|
+
child per cell. `%load_ext` still works (idempotent).
|
|
67
|
+
- `cc_node.require('path')` is `get().require('path')`.
|
|
68
|
+
- `eval()` is one RTT, no extra globals. `%%js` / `eval_cell` install
|
|
69
|
+
cwd `require` once.
|
|
70
|
+
- `--bind` is `Object.assign(globalThis, …)` of names you name (wire
|
|
71
|
+
types only; no pickle). Missing names refuse.
|
|
72
|
+
- First Ctrl-C finishes the in-flight call (wire stays in sync).
|
|
73
|
+
Interrupt again to kill the child.
|
|
74
|
+
|
|
52
75
|
```
|
|
53
|
-
pip install concurrent-c-node
|
|
76
|
+
pip install concurrent-c-node # needs node on PATH
|
|
77
|
+
pip install 'concurrent-c-node[jupyter]' # magics (IPython)
|
|
54
78
|
python -m cc_node.examples.use_node
|
|
55
79
|
python -m cc_node.examples.bench_wire
|
|
56
80
|
python -m cc_node.benchmarks.multi_domain
|
|
81
|
+
python -m cc_node.benchmarks.vs_alts # vs DIY node / pythonmonkey / mini-racer
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Jupyter / Colab
|
|
85
|
+
|
|
86
|
+
Colab and the usual Jupyter kernel are **Python** — this package. Same
|
|
87
|
+
calling convention as a script: a cell blocks until Node answers;
|
|
88
|
+
thenables wait in the child. Magics and `get()` share **one** session
|
|
89
|
+
for the kernel, not a spawn per cell (~28ms). Child `console.log`
|
|
90
|
+
lands in the cell (inherited stdio, line-buffered).
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
%pip install concurrent-c-node
|
|
94
|
+
# if `node` is missing (typical Colab):
|
|
95
|
+
!apt-get install -y nodejs
|
|
96
|
+
|
|
97
|
+
import cc_node # magics register; no %load_ext
|
|
98
|
+
path = cc_node.require('path')
|
|
99
|
+
path.join('a', 'b') # 'a/b'
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
%%js
|
|
104
|
+
console.log('hi') # shows in the cell
|
|
105
|
+
require('path').join('a', 'b') # last expression comes back as Python
|
|
57
106
|
```
|
|
58
107
|
|
|
108
|
+
```python
|
|
109
|
+
xs = [1, 2, 3, 4]
|
|
110
|
+
|
|
111
|
+
%%js -b xs -t chunks
|
|
112
|
+
xs.map(x => x * 2) # wire types only; no pickle fallback
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
| | |
|
|
116
|
+
|---|---|
|
|
117
|
+
| `import cc_node` | registers magics; does **not** spawn until first `%%js` / `get()` / `require()` |
|
|
118
|
+
| `%load_ext cc_node` | same, idempotent |
|
|
119
|
+
| `%js 1+1` / `%%js` | eval on `cc_node.get()`; last expression is the result |
|
|
120
|
+
| `-b xs` / `--bind xs,n` | publish those Python names on `globalThis` for the cell |
|
|
121
|
+
| `-t chunks` / `--to` | store the result in the notebook namespace |
|
|
122
|
+
| `%js_stats` | handle-table size (spawns if needed) |
|
|
123
|
+
| `%js_reset` / `cc_node.reset()` | `destroy()` the session child; `%reset` does this too |
|
|
124
|
+
| `cc_node.get()` | the session the magics use; `create()` is still a private child |
|
|
125
|
+
| `cc_node.kernel()` | alias of `get()` |
|
|
126
|
+
| `cc_node.require('fs')` | `get().require('fs')` |
|
|
127
|
+
| `JsHandle` display | cheap `JsHandle #3` — repr does not cross the wire |
|
|
128
|
+
|
|
129
|
+
`eval()` is unchanged (one RTT, no extra globals). `%%js` / `eval_cell`
|
|
130
|
+
install cwd `require` once so cells look like Node (`require('path')`).
|
|
131
|
+
`--bind` is `Object.assign(globalThis, …)` — missing names and non-wire
|
|
132
|
+
types (`DataFrame`, a set) fail articulately. Reserved names
|
|
133
|
+
(`require`, `process`, `globalThis`, …) refuse so a bind cannot shadow
|
|
134
|
+
Node.
|
|
135
|
+
|
|
136
|
+
**Interrupt.** First Ctrl-C does not abandon the in-flight reply (that
|
|
137
|
+
would desync the wire and drop callbacks). The call finishes, the result
|
|
138
|
+
is discarded, the domain stays up. Interrupt again to **kill** the
|
|
139
|
+
child — cooperative `destroy()` cannot stop a JS `for (;;) {}`. Same
|
|
140
|
+
honesty as the rest of the bridge.
|
|
141
|
+
|
|
142
|
+
JS kernels (`tslab`, Deno Jupyter) are
|
|
143
|
+
[`concurrent-c-python`](https://www.npmjs.com/package/concurrent-c-python);
|
|
144
|
+
Colab is not that. There, default `create()` blocks the kernel thread —
|
|
145
|
+
`py.task` / `{ isolated: true }` when the loop must stay live.
|
|
146
|
+
|
|
59
147
|
## Measured
|
|
60
148
|
|
|
61
149
|
[`bench_wire`](https://github.com/sreekotay/concurrent-c/blob/main/pypi/cc-node/cc_node/examples/bench_wire.py)
|
|
@@ -73,6 +161,30 @@ python -m cc_node.benchmarks.multi_domain
|
|
|
73
161
|
Wire: line-JSON on dedicated fds (stdio stays yours). Bulk spill: private
|
|
74
162
|
0700 dir, 0600 files, removed with the bridge.
|
|
75
163
|
|
|
164
|
+
### Vs pythonmonkey / mini-racer / DIY node
|
|
165
|
+
|
|
166
|
+
Most “JS from Python” libraries are **not Node**. Bulk is a **sum** over
|
|
167
|
+
1M floats (`.length` on an in-process wrapper is free and lies). Snapshot:
|
|
168
|
+
[`cc_node_vs_alts_20260813.txt`](https://github.com/sreekotay/concurrent-c/blob/main/perf/baselines/cc_node_vs_alts_20260813.txt)
|
|
169
|
+
· harness: [`benchmarks/vs_alts.py`](https://github.com/sreekotay/concurrent-c/blob/main/pypi/cc-node/cc_node/benchmarks/vs_alts.py).
|
|
170
|
+
|
|
171
|
+
| | cc-node | DIY JSON stdio | `node -e` each | pythonmonkey | mini-racer |
|
|
172
|
+
|---|---|---|---|---|---|
|
|
173
|
+
| identity RTT | **20µs** | 39µs | 25ms | **<1µs** | 114µs |
|
|
174
|
+
| callback | **36µs** | — | — | 1µs | — |
|
|
175
|
+
| 8MB typed / list | **6.2ms shm** / 359ms | — / 197ms | — | — / 630ms proxy | — / 78ms |
|
|
176
|
+
| `require('fs')` | yes | yes | yes | no | no |
|
|
177
|
+
| process | child `node` | child `node` | new process/call | SpiderMonkey in-process | V8 isolate |
|
|
178
|
+
|
|
179
|
+
Tiny scalars: pythonmonkey’s in-process SM beats a child. Real Node
|
|
180
|
+
(`require('fs')`, native addons, callbacks, stdout stays yours): this
|
|
181
|
+
package. Isolated `node -e` per call is ~1000× a persistent child.
|
|
182
|
+
Optional engines SKIP if not importable — not package deps.
|
|
183
|
+
|
|
184
|
+
The other direction (Python from Node):
|
|
185
|
+
[`concurrent-c-python`](https://www.npmjs.com/package/concurrent-c-python)
|
|
186
|
+
vs pymport / ncp / pythonia.
|
|
187
|
+
|
|
76
188
|
## Surface
|
|
77
189
|
|
|
78
190
|
- Plain data (numbers, str, bool, `None`, lists, non-empty dicts) by
|
|
@@ -80,6 +192,9 @@ Wire: line-JSON on dedicated fds (stdio stays yours). Bulk spill: private
|
|
|
80
192
|
`String()`). Non-finite floats are tagged.
|
|
81
193
|
- Handles are per-domain. `stats()` / `release()` / idempotent
|
|
82
194
|
`destroy()`; afterwards: `bridge is closed`.
|
|
195
|
+
- `eval_cell(src, bindings=)` is the notebook door (`%%js`): cwd
|
|
196
|
+
`require` once, optional `globalThis` binds; `eval()` stays one RTT.
|
|
197
|
+
`get()` / `require()` / `eval()` at module level share that session.
|
|
83
198
|
- Crash isolation, not a sandbox. `destroy()` is cooperative; an
|
|
84
199
|
in-flight CPU-bound call finishes or you kill the child
|
|
85
200
|
([`bridge_stress.md`](https://github.com/sreekotay/concurrent-c/blob/main/stress/bridge/bridge_stress.md)).
|
|
@@ -176,7 +291,9 @@ Both bridges (npm OIDC + PyPI OIDC):
|
|
|
176
291
|
# fallbacks: --npm-local / --pypi-twine
|
|
177
292
|
```
|
|
178
293
|
|
|
179
|
-
Examples: `use_node`, `bench_wire`, `benchmarks.multi_domain
|
|
294
|
+
Examples: `use_node`, `bench_wire`, `benchmarks.multi_domain`,
|
|
295
|
+
`benchmarks.vs_alts`.
|
|
296
|
+
Jupyter: `import cc_node` then `%%js` (or `%load_ext cc_node`).
|
|
180
297
|
Stress: [`stress/bridge/`](https://github.com/sreekotay/concurrent-c/tree/main/stress/bridge).
|
|
181
298
|
Own hot path in C/CC → native module (40–90ns) —
|
|
182
299
|
[JS / Python interop](https://github.com/sreekotay/concurrent-c/blob/main/docs/js-py-modules.md).
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
# concurrent-c-node
|
|
2
|
+
|
|
3
|
+
Call Node (and npm packages) from Python.
|
|
4
|
+
Native types, exceptions, callbacks, and async all cross the boundary.
|
|
5
|
+
|
|
6
|
+
Part of [Concurrent-C](https://github.com/sreekotay/concurrent-c) — a
|
|
7
|
+
strict C11-superset preprocessor: `.ccs` lowers to plain C and compiles
|
|
8
|
+
with your host C compiler. (This bridge itself is pure Python stdlib —
|
|
9
|
+
no native build.)
|
|
10
|
+
|
|
11
|
+
Map of the three boundaries (CC hosts JS, native modules, this package
|
|
12
|
+
bridge):
|
|
13
|
+
[JS / Python interop](https://github.com/sreekotay/concurrent-c/blob/main/docs/js-py-modules.md).
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
import cc_node
|
|
17
|
+
|
|
18
|
+
js = cc_node.create() # always a child `node` process
|
|
19
|
+
_ = js.require('lodash') # cwd node_modules
|
|
20
|
+
_.chunk([1, 2, 3, 4, 5], 2) # [[1, 2], [3, 4], [5]]
|
|
21
|
+
|
|
22
|
+
semver = js.require('semver')
|
|
23
|
+
semver.satisfies('1.2.3', '^1.0.0') # True
|
|
24
|
+
|
|
25
|
+
js.destroy() # or: with cc_node.create() as js:
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The other direction (Python from Node):
|
|
29
|
+
[`concurrent-c-python`](https://www.npmjs.com/package/concurrent-c-python)
|
|
30
|
+
(in-process by default; vs pymport / ncp / pythonia in that README).
|
|
31
|
+
|
|
32
|
+
Every `create()` here is a separate Node — real addons, crash isolation,
|
|
33
|
+
measurable wire. N domains = N processes.
|
|
34
|
+
|
|
35
|
+
| | this package | CC hosted (`cc_js_new(false, …)`) |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| API | `cc_node.create()` | `.ccs` program |
|
|
38
|
+
| Where | child `node` | libnode in-process |
|
|
39
|
+
| Hot call | ~105µs RTT | sub-µs (needs libnode) |
|
|
40
|
+
| Bulk | shm (~9.5ms / 8MB) | in-process |
|
|
41
|
+
| Parallelism | N children | one process |
|
|
42
|
+
| Crash | child dies; parent lives | shared fate |
|
|
43
|
+
|
|
44
|
+
**Cheat sheet**
|
|
45
|
+
|
|
46
|
+
- Always a child `node`. A call blocks until JS answers; thenables wait
|
|
47
|
+
in the child. No `{ async: true }`.
|
|
48
|
+
- Scalars / `None` materialize; empty `{}` stays a handle; everything
|
|
49
|
+
else is a `JsHandle` until `str()` / attrs / a call.
|
|
50
|
+
- `import cc_node` then `%%js` or `cc_node.get()` — one session, not a
|
|
51
|
+
child per cell. `%load_ext` still works (idempotent).
|
|
52
|
+
- `cc_node.require('path')` is `get().require('path')`.
|
|
53
|
+
- `eval()` is one RTT, no extra globals. `%%js` / `eval_cell` install
|
|
54
|
+
cwd `require` once.
|
|
55
|
+
- `--bind` is `Object.assign(globalThis, …)` of names you name (wire
|
|
56
|
+
types only; no pickle). Missing names refuse.
|
|
57
|
+
- First Ctrl-C finishes the in-flight call (wire stays in sync).
|
|
58
|
+
Interrupt again to kill the child.
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
pip install concurrent-c-node # needs node on PATH
|
|
62
|
+
pip install 'concurrent-c-node[jupyter]' # magics (IPython)
|
|
63
|
+
python -m cc_node.examples.use_node
|
|
64
|
+
python -m cc_node.examples.bench_wire
|
|
65
|
+
python -m cc_node.benchmarks.multi_domain
|
|
66
|
+
python -m cc_node.benchmarks.vs_alts # vs DIY node / pythonmonkey / mini-racer
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Jupyter / Colab
|
|
70
|
+
|
|
71
|
+
Colab and the usual Jupyter kernel are **Python** — this package. Same
|
|
72
|
+
calling convention as a script: a cell blocks until Node answers;
|
|
73
|
+
thenables wait in the child. Magics and `get()` share **one** session
|
|
74
|
+
for the kernel, not a spawn per cell (~28ms). Child `console.log`
|
|
75
|
+
lands in the cell (inherited stdio, line-buffered).
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
%pip install concurrent-c-node
|
|
79
|
+
# if `node` is missing (typical Colab):
|
|
80
|
+
!apt-get install -y nodejs
|
|
81
|
+
|
|
82
|
+
import cc_node # magics register; no %load_ext
|
|
83
|
+
path = cc_node.require('path')
|
|
84
|
+
path.join('a', 'b') # 'a/b'
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
%%js
|
|
89
|
+
console.log('hi') # shows in the cell
|
|
90
|
+
require('path').join('a', 'b') # last expression comes back as Python
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
xs = [1, 2, 3, 4]
|
|
95
|
+
|
|
96
|
+
%%js -b xs -t chunks
|
|
97
|
+
xs.map(x => x * 2) # wire types only; no pickle fallback
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
| | |
|
|
101
|
+
|---|---|
|
|
102
|
+
| `import cc_node` | registers magics; does **not** spawn until first `%%js` / `get()` / `require()` |
|
|
103
|
+
| `%load_ext cc_node` | same, idempotent |
|
|
104
|
+
| `%js 1+1` / `%%js` | eval on `cc_node.get()`; last expression is the result |
|
|
105
|
+
| `-b xs` / `--bind xs,n` | publish those Python names on `globalThis` for the cell |
|
|
106
|
+
| `-t chunks` / `--to` | store the result in the notebook namespace |
|
|
107
|
+
| `%js_stats` | handle-table size (spawns if needed) |
|
|
108
|
+
| `%js_reset` / `cc_node.reset()` | `destroy()` the session child; `%reset` does this too |
|
|
109
|
+
| `cc_node.get()` | the session the magics use; `create()` is still a private child |
|
|
110
|
+
| `cc_node.kernel()` | alias of `get()` |
|
|
111
|
+
| `cc_node.require('fs')` | `get().require('fs')` |
|
|
112
|
+
| `JsHandle` display | cheap `JsHandle #3` — repr does not cross the wire |
|
|
113
|
+
|
|
114
|
+
`eval()` is unchanged (one RTT, no extra globals). `%%js` / `eval_cell`
|
|
115
|
+
install cwd `require` once so cells look like Node (`require('path')`).
|
|
116
|
+
`--bind` is `Object.assign(globalThis, …)` — missing names and non-wire
|
|
117
|
+
types (`DataFrame`, a set) fail articulately. Reserved names
|
|
118
|
+
(`require`, `process`, `globalThis`, …) refuse so a bind cannot shadow
|
|
119
|
+
Node.
|
|
120
|
+
|
|
121
|
+
**Interrupt.** First Ctrl-C does not abandon the in-flight reply (that
|
|
122
|
+
would desync the wire and drop callbacks). The call finishes, the result
|
|
123
|
+
is discarded, the domain stays up. Interrupt again to **kill** the
|
|
124
|
+
child — cooperative `destroy()` cannot stop a JS `for (;;) {}`. Same
|
|
125
|
+
honesty as the rest of the bridge.
|
|
126
|
+
|
|
127
|
+
JS kernels (`tslab`, Deno Jupyter) are
|
|
128
|
+
[`concurrent-c-python`](https://www.npmjs.com/package/concurrent-c-python);
|
|
129
|
+
Colab is not that. There, default `create()` blocks the kernel thread —
|
|
130
|
+
`py.task` / `{ isolated: true }` when the loop must stay live.
|
|
131
|
+
|
|
132
|
+
## Measured
|
|
133
|
+
|
|
134
|
+
[`bench_wire`](https://github.com/sreekotay/concurrent-c/blob/main/pypi/cc-node/cc_node/examples/bench_wire.py)
|
|
135
|
+
·
|
|
136
|
+
[`cc_node_bridge_py_20260810.txt`](https://github.com/sreekotay/concurrent-c/blob/main/perf/baselines/cc_node_bridge_py_20260810.txt):
|
|
137
|
+
|
|
138
|
+
| what | result |
|
|
139
|
+
|---|---|
|
|
140
|
+
| spawn (first eval) | 28ms |
|
|
141
|
+
| wire RTT | 105µs |
|
|
142
|
+
| Python callback round trip | 153µs |
|
|
143
|
+
| 8MB `array('d')` via shm | 9.5ms |
|
|
144
|
+
| same 8MB as JSON list | 499ms (~52×) |
|
|
145
|
+
|
|
146
|
+
Wire: line-JSON on dedicated fds (stdio stays yours). Bulk spill: private
|
|
147
|
+
0700 dir, 0600 files, removed with the bridge.
|
|
148
|
+
|
|
149
|
+
### Vs pythonmonkey / mini-racer / DIY node
|
|
150
|
+
|
|
151
|
+
Most “JS from Python” libraries are **not Node**. Bulk is a **sum** over
|
|
152
|
+
1M floats (`.length` on an in-process wrapper is free and lies). Snapshot:
|
|
153
|
+
[`cc_node_vs_alts_20260813.txt`](https://github.com/sreekotay/concurrent-c/blob/main/perf/baselines/cc_node_vs_alts_20260813.txt)
|
|
154
|
+
· harness: [`benchmarks/vs_alts.py`](https://github.com/sreekotay/concurrent-c/blob/main/pypi/cc-node/cc_node/benchmarks/vs_alts.py).
|
|
155
|
+
|
|
156
|
+
| | cc-node | DIY JSON stdio | `node -e` each | pythonmonkey | mini-racer |
|
|
157
|
+
|---|---|---|---|---|---|
|
|
158
|
+
| identity RTT | **20µs** | 39µs | 25ms | **<1µs** | 114µs |
|
|
159
|
+
| callback | **36µs** | — | — | 1µs | — |
|
|
160
|
+
| 8MB typed / list | **6.2ms shm** / 359ms | — / 197ms | — | — / 630ms proxy | — / 78ms |
|
|
161
|
+
| `require('fs')` | yes | yes | yes | no | no |
|
|
162
|
+
| process | child `node` | child `node` | new process/call | SpiderMonkey in-process | V8 isolate |
|
|
163
|
+
|
|
164
|
+
Tiny scalars: pythonmonkey’s in-process SM beats a child. Real Node
|
|
165
|
+
(`require('fs')`, native addons, callbacks, stdout stays yours): this
|
|
166
|
+
package. Isolated `node -e` per call is ~1000× a persistent child.
|
|
167
|
+
Optional engines SKIP if not importable — not package deps.
|
|
168
|
+
|
|
169
|
+
The other direction (Python from Node):
|
|
170
|
+
[`concurrent-c-python`](https://www.npmjs.com/package/concurrent-c-python)
|
|
171
|
+
vs pymport / ncp / pythonia.
|
|
172
|
+
|
|
173
|
+
## Surface
|
|
174
|
+
|
|
175
|
+
- Plain data (numbers, str, bool, `None`, lists, non-empty dicts) by
|
|
176
|
+
value; else a domain-owned handle (attrs, calls, `str()` →
|
|
177
|
+
`String()`). Non-finite floats are tagged.
|
|
178
|
+
- Handles are per-domain. `stats()` / `release()` / idempotent
|
|
179
|
+
`destroy()`; afterwards: `bridge is closed`.
|
|
180
|
+
- `eval_cell(src, bindings=)` is the notebook door (`%%js`): cwd
|
|
181
|
+
`require` once, optional `globalThis` binds; `eval()` stays one RTT.
|
|
182
|
+
`get()` / `require()` / `eval()` at module level share that session.
|
|
183
|
+
- Crash isolation, not a sandbox. `destroy()` is cooperative; an
|
|
184
|
+
in-flight CPU-bound call finishes or you kill the child
|
|
185
|
+
([`bridge_stress.md`](https://github.com/sreekotay/concurrent-c/blob/main/stress/bridge/bridge_stress.md)).
|
|
186
|
+
|
|
187
|
+
### Promises
|
|
188
|
+
|
|
189
|
+
Awaited in the child before the reply — no `async`/`await` on the
|
|
190
|
+
Python side. Same honesty as `concurrent-c-python`: a call blocks until
|
|
191
|
+
the other runtime answers.
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
fetchish = js.eval('async (x) => { return { doubled: x * 2 } }')
|
|
195
|
+
fetchish(21) # {'doubled': 42}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Callbacks
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
mapped = js.eval('(f) => [1, 2, 3].map(f)')(lambda x, *rest: x * 10)
|
|
202
|
+
# map passes (value, index, array) — take *rest
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Exceptions cross both ways with messages intact.
|
|
206
|
+
|
|
207
|
+
### Buffers
|
|
208
|
+
|
|
209
|
+
`bytes` / `array.array` / 1-D numpy → typed arrays (and back). Small
|
|
210
|
+
inline; large via shm.
|
|
211
|
+
|
|
212
|
+
```python
|
|
213
|
+
import array
|
|
214
|
+
total = js.eval('(a) => a.reduce((s, x) => s + x, 0)')
|
|
215
|
+
total(array.array('d', range(1_000_000)))
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## Common issues
|
|
219
|
+
|
|
220
|
+
**`Cannot find module`.** `require` / `import` resolve from the Python
|
|
221
|
+
process cwd (`node_modules` next to your program), not from this wheel’s
|
|
222
|
+
site-packages. `npm install lodash` in the project directory is the fix;
|
|
223
|
+
or `create(node=…)` / `CC_NODE_BIN` when the wrong Node is on `PATH`.
|
|
224
|
+
|
|
225
|
+
### Empty `{}` stays a handle
|
|
226
|
+
|
|
227
|
+
An empty object has to stay on the Node side — a materialized Python
|
|
228
|
+
`dict` would lose later property use that matches Node. So
|
|
229
|
+
`js.eval('({})')` returns a live handle:
|
|
230
|
+
|
|
231
|
+
```python
|
|
232
|
+
o = js.eval('({})') # JsHandle, not {}
|
|
233
|
+
js.eval('(o) => { o.x = 1; return o.x }')(o) # 1
|
|
234
|
+
js.eval('({a: 1})') # {'a': 1} — data return
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Non-empty plain objects still cross as Python `dict`s. Same-domain
|
|
238
|
+
handles chain (`h.update(…).digest(…)`).
|
|
239
|
+
|
|
240
|
+
**Wire cost vs tiny work.** Round trip is ~100µs; a one-line JS helper
|
|
241
|
+
on three numbers loses to pure Python. Prefer Python (or a native CC
|
|
242
|
+
module) for small/hot work; use the bridge when Node/npm owns the kernel.
|
|
243
|
+
Multi-core: `python -m cc_node.benchmarks.multi_domain` (~2.8× on 3
|
|
244
|
+
domains here).
|
|
245
|
+
|
|
246
|
+
## Choosing node
|
|
247
|
+
|
|
248
|
+
1. `create(node='/path/to/node')`
|
|
249
|
+
2. `CC_NODE_BIN`
|
|
250
|
+
3. `node` on `PATH`
|
|
251
|
+
|
|
252
|
+
Packages: cwd `node_modules`, same as Node itself.
|
|
253
|
+
|
|
254
|
+
From Concurrent-C (not Python): `cc_js_new(false, &a)` hosted
|
|
255
|
+
(libnode), or `cc_js_new(true, &a)` for this wire —
|
|
256
|
+
[`jsdemo.shcc`](https://github.com/sreekotay/concurrent-c/blob/main/examples/js/jsdemo.shcc).
|
|
257
|
+
|
|
258
|
+
## Publishing
|
|
259
|
+
|
|
260
|
+
**PyPI Trusted Publishing (OIDC)** — no API token:
|
|
261
|
+
|
|
262
|
+
1. [Publishing settings](https://pypi.org/manage/project/concurrent-c-node/settings/publishing/):
|
|
263
|
+
owner `sreekotay`, repo `concurrent-c`, workflow `publish-cc-node.yml`,
|
|
264
|
+
environment `pypi`
|
|
265
|
+
2. GitHub Environment `pypi`
|
|
266
|
+
3. Bump `pyproject.toml`, push, then:
|
|
267
|
+
|
|
268
|
+
```
|
|
269
|
+
gh workflow run publish-cc-node.yml
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Both bridges (npm OIDC + PyPI OIDC):
|
|
273
|
+
|
|
274
|
+
```
|
|
275
|
+
./scripts/publish_bridges.sh --publish --minor
|
|
276
|
+
# fallbacks: --npm-local / --pypi-twine
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
Examples: `use_node`, `bench_wire`, `benchmarks.multi_domain`,
|
|
280
|
+
`benchmarks.vs_alts`.
|
|
281
|
+
Jupyter: `import cc_node` then `%%js` (or `%load_ext cc_node`).
|
|
282
|
+
Stress: [`stress/bridge/`](https://github.com/sreekotay/concurrent-c/tree/main/stress/bridge).
|
|
283
|
+
Own hot path in C/CC → native module (40–90ns) —
|
|
284
|
+
[JS / Python interop](https://github.com/sreekotay/concurrent-c/blob/main/docs/js-py-modules.md).
|