onyxweb 0.2.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.
Files changed (82) hide show
  1. onyxweb-0.2.0/.cargo/config.toml +3 -0
  2. onyxweb-0.2.0/.github/workflows/ci.yml +142 -0
  3. onyxweb-0.2.0/.github/workflows/publish.yml +235 -0
  4. onyxweb-0.2.0/.gitignore +33 -0
  5. onyxweb-0.2.0/.python-version +1 -0
  6. onyxweb-0.2.0/BENCHMARKS.md +106 -0
  7. onyxweb-0.2.0/Cargo.lock +1979 -0
  8. onyxweb-0.2.0/Cargo.toml +47 -0
  9. onyxweb-0.2.0/LICENSE +28 -0
  10. onyxweb-0.2.0/PKG-INFO +352 -0
  11. onyxweb-0.2.0/README.md +321 -0
  12. onyxweb-0.2.0/pyproject.toml +131 -0
  13. onyxweb-0.2.0/python/onyxweb/__init__.py +1509 -0
  14. onyxweb-0.2.0/python/onyxweb/__main__.py +377 -0
  15. onyxweb-0.2.0/python/onyxweb/_download_chrome.py +268 -0
  16. onyxweb-0.2.0/python/onyxweb/_logging.py +73 -0
  17. onyxweb-0.2.0/python/onyxweb/_onyxweb.pyi +160 -0
  18. onyxweb-0.2.0/python/onyxweb/config.py +709 -0
  19. onyxweb-0.2.0/python/onyxweb/presets/__init__.py +33 -0
  20. onyxweb-0.2.0/python/onyxweb/presets/full/__init__.py +17 -0
  21. onyxweb-0.2.0/python/onyxweb/presets/full/stealth.py +33 -0
  22. onyxweb-0.2.0/python/onyxweb/presets/shell/__init__.py +17 -0
  23. onyxweb-0.2.0/python/onyxweb/presets/shell/archival.py +31 -0
  24. onyxweb-0.2.0/python/onyxweb/presets/shell/recon.py +41 -0
  25. onyxweb-0.2.0/python/onyxweb/presets/shell/stealth.py +286 -0
  26. onyxweb-0.2.0/python/onyxweb/py.typed +0 -0
  27. onyxweb-0.2.0/rust-toolchain.toml +3 -0
  28. onyxweb-0.2.0/src/chrome.rs +141 -0
  29. onyxweb-0.2.0/src/client.rs +758 -0
  30. onyxweb-0.2.0/src/config.rs +1010 -0
  31. onyxweb-0.2.0/src/dom.rs +361 -0
  32. onyxweb-0.2.0/src/engine.rs +1026 -0
  33. onyxweb-0.2.0/src/error.rs +144 -0
  34. onyxweb-0.2.0/src/hash.rs +57 -0
  35. onyxweb-0.2.0/src/lib.rs +86 -0
  36. onyxweb-0.2.0/src/pool.rs +1046 -0
  37. onyxweb-0.2.0/src/result.rs +178 -0
  38. onyxweb-0.2.0/src/runtime.rs +25 -0
  39. onyxweb-0.2.0/tests/conftest.py +32 -0
  40. onyxweb-0.2.0/tests/test_actions.py +501 -0
  41. onyxweb-0.2.0/tests/test_anti_bot.py +419 -0
  42. onyxweb-0.2.0/tests/test_async_concurrency.py +81 -0
  43. onyxweb-0.2.0/tests/test_async_module_level.py +46 -0
  44. onyxweb-0.2.0/tests/test_async_smoke.py +103 -0
  45. onyxweb-0.2.0/tests/test_batch.py +75 -0
  46. onyxweb-0.2.0/tests/test_batch_errors.py +99 -0
  47. onyxweb-0.2.0/tests/test_cli.py +228 -0
  48. onyxweb-0.2.0/tests/test_config.py +361 -0
  49. onyxweb-0.2.0/tests/test_console_capture.py +268 -0
  50. onyxweb-0.2.0/tests/test_dialog_handling.py +145 -0
  51. onyxweb-0.2.0/tests/test_dom.py +145 -0
  52. onyxweb-0.2.0/tests/test_domino_smoke.py +247 -0
  53. onyxweb-0.2.0/tests/test_emulation.py +54 -0
  54. onyxweb-0.2.0/tests/test_error_context.py +82 -0
  55. onyxweb-0.2.0/tests/test_fetch.py +132 -0
  56. onyxweb-0.2.0/tests/test_gauntlet.py +250 -0
  57. onyxweb-0.2.0/tests/test_image_format.py +62 -0
  58. onyxweb-0.2.0/tests/test_per_fetch_blocking.py +202 -0
  59. onyxweb-0.2.0/tests/test_per_fetch_headers_no_leak.py +80 -0
  60. onyxweb-0.2.0/tests/test_per_fetch_headers_validate.py +82 -0
  61. onyxweb-0.2.0/tests/test_per_fetch_nav_blocking.py +252 -0
  62. onyxweb-0.2.0/tests/test_per_fetch_referer.py +116 -0
  63. onyxweb-0.2.0/tests/test_per_fetch_scripts.py +178 -0
  64. onyxweb-0.2.0/tests/test_pool_integrity.py +108 -0
  65. onyxweb-0.2.0/tests/test_post_load_results.py +136 -0
  66. onyxweb-0.2.0/tests/test_post_load_scripts.py +320 -0
  67. onyxweb-0.2.0/tests/test_post_load_settle.py +105 -0
  68. onyxweb-0.2.0/tests/test_proxy.py +227 -0
  69. onyxweb-0.2.0/tests/test_response_async_edge.py +94 -0
  70. onyxweb-0.2.0/tests/test_response_cert.py +34 -0
  71. onyxweb-0.2.0/tests/test_response_hashes.py +72 -0
  72. onyxweb-0.2.0/tests/test_response_headers.py +125 -0
  73. onyxweb-0.2.0/tests/test_response_metadata.py +55 -0
  74. onyxweb-0.2.0/tests/test_response_redirects.py +68 -0
  75. onyxweb-0.2.0/tests/test_runtime_config.py +154 -0
  76. onyxweb-0.2.0/tests/test_same_doc_nav.py +132 -0
  77. onyxweb-0.2.0/tests/test_smoke.py +50 -0
  78. onyxweb-0.2.0/tests/test_stealth.py +325 -0
  79. onyxweb-0.2.0/tests/test_threading.py +80 -0
  80. onyxweb-0.2.0/tests/test_wait_until.py +106 -0
  81. onyxweb-0.2.0/tests/urls_bench_big.txt +50 -0
  82. onyxweb-0.2.0/uv.lock +886 -0
@@ -0,0 +1,3 @@
1
+ # No V8 anymore. Chromium is a subprocess now, not a linked library.
2
+ # This file is empty/unused in the CDP engine era. Kept as a placeholder so
3
+ # anyone consulting .cargo/ knows no special env vars are required.
@@ -0,0 +1,142 @@
1
+ # Cross-platform wheel matrix. Wheels are uploaded as artifacts — no auto-
2
+ # publish. Trigger: push to master, PRs, and manual workflow_dispatch.
3
+ #
4
+ # Platforms target what `onyxweb-download-chrome` can stage a binary for:
5
+ # linux x86_64 / aarch64, darwin x86_64 / arm64, windows x86_64. Adding a
6
+ # platform requires both a matrix entry here AND a PLATFORMS row in
7
+ # python/onyxweb/_download_chrome.py.
8
+
9
+ name: CI
10
+
11
+ on:
12
+ push:
13
+ branches: [master]
14
+ pull_request:
15
+ workflow_dispatch:
16
+
17
+ permissions:
18
+ contents: read
19
+
20
+ jobs:
21
+ lint:
22
+ # Static gates: ruff, mypy strict, cargo fmt, cargo clippy. Fast — runs
23
+ # on every PR / push to master alongside the wheel-build matrix.
24
+ runs-on: ubuntu-22.04
25
+ steps:
26
+ - uses: actions/checkout@v4
27
+ - uses: astral-sh/setup-uv@v3
28
+ - name: Sync dev deps (builds Rust ext via cache-keys)
29
+ run: uv sync --group dev
30
+ - name: Ruff
31
+ run: uv run ruff check python/ tests/
32
+ - name: Mypy (strict)
33
+ run: uv run mypy python/onyxweb/ tests/
34
+ - name: Cargo fmt
35
+ run: cargo fmt --all -- --check
36
+ - name: Cargo clippy
37
+ run: cargo clippy --all-targets -- -D warnings
38
+
39
+ linux:
40
+ runs-on: ubuntu-22.04
41
+ strategy:
42
+ fail-fast: false
43
+ matrix:
44
+ target: [x86_64, aarch64]
45
+ steps:
46
+ - uses: actions/checkout@v4
47
+ - uses: actions/setup-python@v5
48
+ with:
49
+ python-version: "3.11"
50
+ - name: Build wheel (manylinux_2_28)
51
+ uses: PyO3/maturin-action@v1
52
+ with:
53
+ target: ${{ matrix.target }}
54
+ args: --release --out dist --find-interpreter
55
+ manylinux: "2_28"
56
+ sccache: true
57
+ - uses: actions/upload-artifact@v4
58
+ with:
59
+ name: wheels-linux-${{ matrix.target }}
60
+ path: dist
61
+
62
+ macos:
63
+ runs-on: ${{ matrix.runner }}
64
+ strategy:
65
+ fail-fast: false
66
+ matrix:
67
+ include:
68
+ - runner: macos-latest
69
+ target: aarch64
70
+ steps:
71
+ - uses: actions/checkout@v4
72
+ - uses: actions/setup-python@v5
73
+ with:
74
+ python-version: "3.11"
75
+ - name: Build wheel
76
+ uses: PyO3/maturin-action@v1
77
+ with:
78
+ target: ${{ matrix.target }}
79
+ args: --release --out dist --find-interpreter
80
+ sccache: true
81
+ - uses: actions/upload-artifact@v4
82
+ with:
83
+ name: wheels-macos-${{ matrix.target }}
84
+ path: dist
85
+
86
+ windows:
87
+ runs-on: windows-latest
88
+ steps:
89
+ - uses: actions/checkout@v4
90
+ - uses: actions/setup-python@v5
91
+ with:
92
+ python-version: "3.11"
93
+ architecture: x64
94
+ - name: Build wheel
95
+ uses: PyO3/maturin-action@v1
96
+ with:
97
+ target: x64
98
+ args: --release --out dist --find-interpreter
99
+ sccache: true
100
+ - uses: actions/upload-artifact@v4
101
+ with:
102
+ name: wheels-windows-x86_64
103
+ path: dist
104
+
105
+ sdist:
106
+ runs-on: ubuntu-latest
107
+ steps:
108
+ - uses: actions/checkout@v4
109
+ - uses: PyO3/maturin-action@v1
110
+ with:
111
+ command: sdist
112
+ args: --out dist
113
+ - uses: actions/upload-artifact@v4
114
+ with:
115
+ name: sdist
116
+ path: dist
117
+
118
+ test:
119
+ # Smoke-test the linux x86_64 wheel end-to-end: install into a fresh venv,
120
+ # fetch chrome-headless-shell via the new `onyxweb --install` path, run
121
+ # the default pytest suite. Skips the benchmark tests (-m 'not benchmark'
122
+ # from pyproject addopts).
123
+ needs: [linux]
124
+ runs-on: ubuntu-22.04
125
+ steps:
126
+ - uses: actions/checkout@v4
127
+ - uses: actions/setup-python@v5
128
+ with:
129
+ python-version: "3.11"
130
+ - uses: actions/download-artifact@v4
131
+ with:
132
+ name: wheels-linux-x86_64
133
+ path: dist
134
+ - name: Install wheel + dev deps
135
+ run: |
136
+ python -m pip install --upgrade pip
137
+ python -m pip install onyxweb --find-links dist --force-reinstall
138
+ python -m pip install --group dev
139
+ - name: Fetch chrome-headless-shell
140
+ run: onyxweb --install
141
+ - name: Run default test suite
142
+ run: pytest tests/
@@ -0,0 +1,235 @@
1
+ # Manual publish to (Test)PyPI via Trusted Publishing (OIDC). Builds the full
2
+ # wheel matrix + sdist, smoke-tests the release wheel, then publishes to the
3
+ # chosen index. Run it from the Actions tab ("Run workflow" -> pick testpypi
4
+ # (default) or pypi). Requires a matching GitHub Environment (testpypi / pypi)
5
+ # and a PyPI trusted publisher.
6
+ #
7
+ # testpypi runs publish a unique dev version (BASE.dev<run_number>) so retries
8
+ # never collide with PyPI's immutable filenames; pypi runs publish the
9
+ # pyproject version as-is and tag the commit.
10
+ name: Publish
11
+
12
+ on:
13
+ workflow_dispatch:
14
+ inputs:
15
+ repository:
16
+ description: "Index to publish to"
17
+ type: choice
18
+ options: [testpypi, pypi]
19
+ default: testpypi
20
+
21
+ # Never run two publishes to the same index at once, and never cancel one
22
+ # mid-upload (a half-finished upload is exactly what we want to avoid).
23
+ concurrency:
24
+ group: publish-${{ inputs.repository }}
25
+ cancel-in-progress: false
26
+
27
+ permissions:
28
+ contents: read
29
+
30
+ jobs:
31
+ version:
32
+ # Single source of the published version. For testpypi it is made unique per
33
+ # run so re-publishing never hits PyPI's immutable-filename wall. Only
34
+ # pyproject.toml carries it (maturin reads the wheel version from [project]);
35
+ # Cargo.toml stays valid semver. Hand the file to every build job as an
36
+ # artifact so the rewrite happens once, with no per-platform shell.
37
+ runs-on: ubuntu-latest
38
+ outputs:
39
+ version: ${{ steps.calc.outputs.version }}
40
+ steps:
41
+ - uses: actions/checkout@v4
42
+ - id: calc
43
+ run: |
44
+ BASE=$(python3 -c "import re;print(re.search(r'(?m)^version = \"([^\"]+)\"', open('pyproject.toml').read()).group(1))")
45
+ if [ "${{ inputs.repository }}" = "testpypi" ]; then
46
+ VERSION="${BASE}.dev${{ github.run_number }}"
47
+ else
48
+ VERSION="$BASE"
49
+ fi
50
+ echo "version=$VERSION" >> "$GITHUB_OUTPUT"
51
+ echo "Publishing $VERSION to ${{ inputs.repository }}"
52
+ - name: Pin version into pyproject.toml
53
+ env:
54
+ V: ${{ steps.calc.outputs.version }}
55
+ run: |
56
+ python3 - <<'EOF'
57
+ import os, re
58
+ v = os.environ["V"]
59
+ s = open("pyproject.toml").read()
60
+ s = re.sub(r'(?m)^version = "[^"]+"', f'version = "{v}"', s, count=1)
61
+ open("pyproject.toml", "w").write(s)
62
+ EOF
63
+ - uses: actions/upload-artifact@v4
64
+ with:
65
+ name: version-files
66
+ path: pyproject.toml
67
+
68
+ linux:
69
+ needs: version
70
+ runs-on: ubuntu-22.04
71
+ strategy:
72
+ fail-fast: false
73
+ matrix:
74
+ target: [x86_64, aarch64]
75
+ steps:
76
+ - uses: actions/checkout@v4
77
+ - uses: actions/download-artifact@v4
78
+ with:
79
+ name: version-files
80
+ path: .
81
+ - name: Build wheel (manylinux_2_28)
82
+ uses: PyO3/maturin-action@v1
83
+ with:
84
+ target: ${{ matrix.target }}
85
+ manylinux: "2_28"
86
+ args: --release --out dist -i 3.10 3.11 3.12 3.13
87
+ sccache: true
88
+ - uses: actions/upload-artifact@v4
89
+ with:
90
+ name: wheels-linux-${{ matrix.target }}
91
+ path: dist
92
+
93
+ macos:
94
+ # One job + --find-interpreter builds all versions. A python-version matrix
95
+ # would duplicate wheel filenames across artifacts and corrupt the publish
96
+ # merge (PyPI then rejects them as "Trailing data").
97
+ needs: version
98
+ runs-on: macos-latest
99
+ steps:
100
+ - uses: actions/checkout@v4
101
+ - uses: actions/download-artifact@v4
102
+ with:
103
+ name: version-files
104
+ path: .
105
+ - uses: actions/setup-python@v5
106
+ with:
107
+ python-version: "3.x"
108
+ - uses: PyO3/maturin-action@v1
109
+ with:
110
+ target: aarch64
111
+ args: --release --out dist --find-interpreter
112
+ sccache: true
113
+ - uses: actions/upload-artifact@v4
114
+ with:
115
+ name: wheels-macos-aarch64
116
+ path: dist
117
+
118
+ windows:
119
+ # One job (see macos): a python-version matrix duplicates wheels across
120
+ # artifacts and corrupts the publish merge.
121
+ needs: version
122
+ runs-on: windows-latest
123
+ steps:
124
+ - uses: actions/checkout@v4
125
+ - uses: actions/download-artifact@v4
126
+ with:
127
+ name: version-files
128
+ path: .
129
+ - uses: actions/setup-python@v5
130
+ with:
131
+ python-version: "3.x"
132
+ architecture: x64
133
+ - uses: PyO3/maturin-action@v1
134
+ with:
135
+ target: x64
136
+ args: --release --out dist --find-interpreter
137
+ sccache: true
138
+ - uses: actions/upload-artifact@v4
139
+ with:
140
+ name: wheels-windows-x86_64
141
+ path: dist
142
+
143
+ sdist:
144
+ needs: version
145
+ runs-on: ubuntu-latest
146
+ steps:
147
+ - uses: actions/checkout@v4
148
+ - uses: actions/download-artifact@v4
149
+ with:
150
+ name: version-files
151
+ path: .
152
+ - uses: PyO3/maturin-action@v1
153
+ with:
154
+ command: sdist
155
+ args: --out dist
156
+ - uses: actions/upload-artifact@v4
157
+ with:
158
+ name: sdist
159
+ path: dist
160
+
161
+ smoke:
162
+ # Gate the publish on the ACTUAL release wheel importing and fetching a real
163
+ # page (catches ABI / packaging breakage a unit suite on a dev build won't).
164
+ needs: [version, linux]
165
+ runs-on: ubuntu-22.04
166
+ steps:
167
+ - uses: actions/setup-python@v5
168
+ with:
169
+ python-version: "3.12"
170
+ - uses: actions/download-artifact@v4
171
+ with:
172
+ name: wheels-linux-x86_64
173
+ path: dist
174
+ - name: Install the built wheel
175
+ run: |
176
+ python -m pip install --upgrade pip
177
+ python -m pip install --pre onyxweb --find-links dist --force-reinstall
178
+ - name: Fetch chrome-headless-shell
179
+ run: onyxweb --install
180
+ - name: Smoke test
181
+ run: |
182
+ python -c "import onyxweb; r = onyxweb.fetch('https://example.com'); assert r.status_code == 200, r.status_code; assert 'Example Domain' in r; print('smoke OK', r.status_code)"
183
+
184
+ publish:
185
+ name: Publish to ${{ inputs.repository }}
186
+ needs: [version, linux, macos, windows, sdist, smoke]
187
+ runs-on: ubuntu-latest
188
+ environment:
189
+ name: ${{ inputs.repository }}
190
+ url: ${{ inputs.repository == 'testpypi' && 'https://test.pypi.org/p/onyxweb' || 'https://pypi.org/p/onyxweb' }}
191
+ permissions:
192
+ id-token: write
193
+ contents: read
194
+ steps:
195
+ # Only wheel + sdist artifacts (not version-files) into dist.
196
+ - uses: actions/download-artifact@v4
197
+ with:
198
+ pattern: wheels-*
199
+ path: dist
200
+ merge-multiple: true
201
+ - uses: actions/download-artifact@v4
202
+ with:
203
+ name: sdist
204
+ path: dist
205
+ - uses: pypa/gh-action-pypi-publish@release/v1.14
206
+ with:
207
+ packages-dir: dist
208
+ # Tolerate files already uploaded (e.g. a retry after a partial run).
209
+ skip-existing: true
210
+ # Empty string → real PyPI; only set for TestPyPI.
211
+ repository-url: ${{ inputs.repository == 'testpypi' && 'https://test.pypi.org/legacy/' || '' }}
212
+
213
+ tag:
214
+ # Tag real releases (pypi only) with v<version> for traceability.
215
+ needs: [version, publish]
216
+ if: inputs.repository == 'pypi'
217
+ runs-on: ubuntu-latest
218
+ permissions:
219
+ contents: write
220
+ steps:
221
+ - uses: actions/checkout@v4
222
+ with:
223
+ fetch-depth: 0
224
+ - name: Tag the release commit
225
+ env:
226
+ V: v${{ needs.version.outputs.version }}
227
+ run: |
228
+ git config user.name "github-actions"
229
+ git config user.email "github-actions@github.com"
230
+ if git rev-parse "$V" >/dev/null 2>&1; then
231
+ echo "Tag $V already exists, skipping."
232
+ else
233
+ git tag -a "$V" -m "Release $V"
234
+ git push origin "$V"
235
+ fi
@@ -0,0 +1,33 @@
1
+ # Rust
2
+ /target/
3
+
4
+ # Python
5
+ __pycache__/
6
+ *.pyc
7
+ *.pyo
8
+ *.egg-info/
9
+ dist/
10
+ *.so
11
+ *.pyd
12
+
13
+ # IDE
14
+ .idea/
15
+
16
+ # Env
17
+ .venv/
18
+ .env
19
+
20
+ # Maturin
21
+ *.whl
22
+
23
+ # OS
24
+ .DS_Store
25
+
26
+ # Ruff
27
+ .ruff_cache/
28
+
29
+ CLAUDE.md
30
+
31
+ # Bundled chromium binaries — populated at wheel build time (`uv run onyxweb-download-chrome`).
32
+ # Not committed to git (too large); maturin `include` picks them up from disk.
33
+ python/onyxweb/_binaries/
@@ -0,0 +1 @@
1
+ 3.11
@@ -0,0 +1,106 @@
1
+ # Screenshot engine benchmarks
2
+
3
+ BBOT-scale subdomain screenshot pumping. One workload, multiple engines, honest numbers.
4
+
5
+ ## Test setup
6
+
7
+ - **Host**: Linux 6.19.10-203.nobara.fc43 (Nobara 43 / Fedora 43), 16 CPU cores, 24 GB free RAM
8
+ - **URLs**: 48–50 URLs from `servo_spike/urls_bench_big.txt` (mixed public sites; `gnu.org` and `google.com` excluded from `/tmp/urls_stable.txt` for the CDP/CEF head-to-head — the first due to network flakiness, the second due to headless anti-bot variance)
9
+ - **Viewport**: 1200×800
10
+ - **Timeout**: 30–45s per URL
11
+ - **Build**: all release-mode with `lto = "thin"`, `codegen-units = 1`
12
+ - **Measurement**: `/usr/bin/time` for wall + peak RSS
13
+ - **Metric**: URL/second sustained (ok_count / wall_time)
14
+
15
+ Important caveat on RSS: `/usr/bin/time` captures peak RSS of the wrapped process only — for chromiumoxide it UNDERCOUNTS (Chromium is a subprocess not counted); for cef_spike warm mode it's accurate (everything in-process).
16
+
17
+ ## Headline table — fully rendered URL → PNG
18
+
19
+ | Engine | Best Config | **URL/s** | Peak per-proc RAM | Binary + runtime | Compat |
20
+ |---|---|---|---|---|---|
21
+ | Servo 0.1.0 (in-process) | cold P=8 | 1.13 | 1.6 GB | 121 MB (self-contained) | 45–46/50 |
22
+ | Chromium CLI (fork per URL) | cold P=16 | 4.51 | 310 MB | system chromium | 50/50 |
23
+ | Chromium via Python CDP | warm P=16 | 5.46 | ~500 MB | system chromium + Python | 50/50 |
24
+ | **CEF via tauri-apps/cef-rs (Option A)** | **cold P=16** | **6.21** | 331 MB | 4 MB bin + 1.3 GB libcef.so | 48/48 |
25
+ | **Chromium via chromiumoxide (CDP)** | warm P=32 | **9.30** | ~430 MB | 6 MB + system chromium | 50/50 |
26
+ | **Chromium via chromiumoxide + chrome-headless-shell** | **warm P=16** | **8.66** | ~400 MB | 6 MB + 190 MB shell | 47–48/48 |
27
+
28
+ **Winner for BBOT-scale**: chromiumoxide (Rust-native CDP client). With bundled chrome-headless-shell it's self-contained, 8.66 URL/s, ~350 MB wheel. The "bundle CEF" path (Option A) is ~30% slower AND ~4× larger to distribute.
29
+
30
+ ## Deep data: Option A vs Option C head-to-head (48 URLs stable list)
31
+
32
+ ### Option A — CEF via tauri-apps/cef-rs (in-process)
33
+
34
+ | Config | Wall (s) | OK/48 | URL/s | Peak MB |
35
+ |---|---|---|---|---|
36
+ | cef cold P=1 | 63.63 | 48 | 0.75 | 330 |
37
+ | cef cold P=4 | 17.08 | 48 | 2.81 | 328 |
38
+ | cef cold P=8 | 10.08 | 48 | 4.76 | 330 |
39
+ | **cef cold P=16** | **7.73** | 48 | **6.21** | 331 |
40
+ | cef warm P=1 | 47.91 | 48 | 1.00 | 1023 |
41
+ | cef warm P=4 | 16.36 | 48 | 2.93 | 1025 |
42
+ | cef warm P=8 | 15.30 | 48 | 3.14 | 1024 |
43
+ | cef warm P=16 | 10.76 | 48 | 4.46 | 999 |
44
+
45
+ **Counterintuitive finding**: warm mode (one CEF init, N browsers in flight) is SLOWER than cold (N separate CEF processes) at P=16. Reason: CEF's single main thread serializes paint/event dispatch for all browsers in one instance. Cold-parallel gives each browser its own main thread. For BBOT-scale, **always use cold-process parallelism with CEF, not warm-batch**.
46
+
47
+ ### Option C — chromiumoxide (CDP via WebSocket)
48
+
49
+ | Config | Wall (median, 3 runs) | OK/48 | URL/s |
50
+ |---|---|---|---|
51
+ | sys chromium P=16 PNG | 6.22 | 48/48 | 7.72 |
52
+ | **headless-shell 148 P=16 PNG** | **5.43** | 47/48 | **8.66** |
53
+ | headless-shell 148 P=16 PNG+HTML | 5.60 | 48/48 | 8.57 |
54
+ | sys chromium P=8 PNG | 20.49 | 47/48 | 2.29 (straggler-dominated) |
55
+ | headless-shell 148 P=8 PNG | 7.81 | 47/48 | 6.02 |
56
+ | headless-shell 148 P=8 PNG+HTML | 7.45 | 48/48 | 6.44 |
57
+
58
+ **chrome-headless-shell 148** (Google's automation-optimized chromium variant) is ~12% faster than the full system chromium 146 at the same P. HTML extraction via `page.content()` (post-JS `document.documentElement.outerHTML`) is essentially free — same P, same wall time.
59
+
60
+ ## Decision-rubric recap
61
+
62
+ | Concern | Option A (CEF) | Option C (chromiumoxide) |
63
+ |---|---|---|
64
+ | Peak throughput | 6.21 URL/s | **8.66 URL/s** (40% higher) |
65
+ | Distribution size | ~1.5 GB (libcef.so + resources + bin) | ~350 MB (wheel + bundled headless-shell) |
66
+ | Bundle chromium with wheel | ✓ (that's the point) | ✓ also possible (ship headless-shell alongside) |
67
+ | Headless server (no X) | ✗ (needs Xvfb) | **✓** (headless-first) |
68
+ | Build complexity | High (wrap macros, multi-process dispatch, sandbox, cache-path collision handling) | Low (one async function) |
69
+ | Rust code lines for basic use | ~500 | ~200 |
70
+ | Deeper integration possible | ✓ in-process API | ✗ via CDP only |
71
+ | Version pinning | ✓ (link libcef version) | ✓ (pin headless-shell download) |
72
+ | Time to working spike | ~1 day | ~2 hours |
73
+ | External deps at runtime | libcef + libX11 + lots | chrome binary + glibc |
74
+
75
+ Option A (CEF) has NO win that matters for BBOT-scale headless screenshot pumping. Its unique strength — in-process API — is unused by a fetch-load-screenshot-exit workflow. Its unique cost — 4× larger distribution, X11 requirement, multi-process dispatch complexity — is paid every day.
76
+
77
+ ## Conclusion
78
+
79
+ **Build onyxweb on chromiumoxide, not CEF.** Ship chrome-headless-shell 148 alongside the wheel (~350 MB total). Target:
80
+
81
+ - ~9 URL/s per process at sweet-spot concurrency (P=16–32)
82
+ - HTML extraction free-of-charge via `page.content()`
83
+ - ~400 MB RSS per process; 4 parallel processes handle ~35 URL/s, network-bound beyond
84
+ - Wheel distribution: unpacks `chrome-headless-shell` into site-packages
85
+ - Python API: `onyxweb.Client()`, `client.screenshot(url)`, `client.batch(urls, concurrency=16)`
86
+
87
+ 10,000 URLs → ~18 min single process, ~5 min with 4 parallel processes.
88
+
89
+ ## Reproduction
90
+
91
+ ```bash
92
+ # Option A — CEF
93
+ cd experiments/cef_spike
94
+ cargo build --release # first build downloads CEF 146 binaries (~300MB)
95
+ DISPLAY=:0 LD_LIBRARY_PATH=$(find target -name libcef.so | xargs dirname) ./bench.sh
96
+
97
+ # Option C — chromiumoxide + chrome-headless-shell
98
+ cd experiments/chromiumoxide_spike
99
+ cargo build --release # 90s
100
+ curl -sSL -o /tmp/hs.zip https://storage.googleapis.com/chrome-for-testing-public/148.0.7778.56/linux64/chrome-headless-shell-linux64.zip
101
+ unzip -q /tmp/hs.zip -d /tmp
102
+ HS=/tmp/chrome-headless-shell-linux64/chrome-headless-shell
103
+ ./target/release/chromiumoxide_spike --chrome $HS --out-dir /tmp/shots --concurrency 16 --mode both < urls.txt
104
+ ```
105
+
106
+ All raw benchmark artifacts under `experiments/{cef_spike,chromiumoxide_spike,servo_spike}/bench_*/`.