Skip to content

Commit 9862261

Browse files
committed
feat(wasm): cap pthread pool at 2; bump core/gdal to beta.16
Each pthread is a Web Worker with non-trivial memory and startup cost. PTHREAD_POOL_SIZE is now Math.min(navigator.hardwareConcurrency || 1, 2) so the heap-duplication and worker-spawn cost stays predictable instead of scaling with the device's core count. GDAL_NUM_THREADS pinned to '1' on mt builds so GDAL's own threading doesn't compete with the capped pool. PTHREAD_POOL_SIZE_STRICT=2 still aborts on overflow. Docs (docs/api/performance.md, website/docs/api/configuration/performance.md) updated: defaults table, tunable section, and "common mistakes" item. Bumps cpp.js, @cpp.js/package-gdal, @cpp.js/package-gdal-wasm to 2.0.0-beta.16.
1 parent 834f278 commit 9862261

7 files changed

Lines changed: 37 additions & 27 deletions

File tree

cppjs-core/cpp.js/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "cpp.js",
3-
"version": "2.0.0-beta.15",
3+
"version": "2.0.0-beta.16",
44
"license": "MIT",
55
"homepage": "https://cpp.js.org",
66
"repository": "https://github.com/bugra9/cpp.js.git",

cppjs-core/cpp.js/src/actions/buildWasm.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export default async function buildWasm(target, options = {}) {
3030

3131
if (target.runtime === 'mt' && !emccFlags.includes('-pthread')) {
3232
emccFlags.push('-pthread');
33-
emccFlags.push('-sPTHREAD_POOL_SIZE=navigator.hardwareConcurrency');
33+
emccFlags.push('-sPTHREAD_POOL_SIZE=Math.min(navigator.hardwareConcurrency || 1, 2)');
3434
emccFlags.push('-sPTHREAD_POOL_SIZE_STRICT=2');
3535
}
3636

cppjs-packages/cppjs-package-gdal/cppjs-package-gdal-wasm/cppjs.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export default {
5151
DXF_FEATURE_LIMIT_PER_BLOCK: '-1',
5252
GDAL_ENABLE_DEPRECATED_DRIVER_GTM: 'YES',
5353
CPL_LOG_ERRORS: 'ON',
54-
GDAL_NUM_THREADS: (state, target) => (target.runtime === 'st' ? '0' : 'ALL_CPUS'),
54+
GDAL_NUM_THREADS: (state, target) => (target.runtime === 'st' ? '0' : '1'),
5555
}
5656
}
5757
}

cppjs-packages/cppjs-package-gdal/cppjs-package-gdal-wasm/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@cpp.js/package-gdal-wasm",
3-
"version": "2.0.0-beta.15",
3+
"version": "2.0.0-beta.16",
44
"nativeVersion": "3.12.4",
55
"description": "This package provides the precompiled GDAL, built using Cpp.js, for seamless integration into JavaScript, WebAssembly, and React Native projects. It enables advanced geospatial data processing, including raster and vector data manipulation, format conversion, and spatial analysis, ensuring high performance and cross-platform compatibility. Perfect for web, server-side, and mobile geospatial applications.",
66
"homepage": "https://github.com/bugra9/cpp.js/tree/main/packages/cppjs-package-gdal#readme",

cppjs-packages/cppjs-package-gdal/cppjs-package-gdal/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@cpp.js/package-gdal",
3-
"version": "2.0.0-beta.15",
3+
"version": "2.0.0-beta.16",
44
"nativeVersion": "3.12.4",
55
"description": "This package provides the precompiled GDAL, built using Cpp.js, for seamless integration into JavaScript, WebAssembly, and React Native projects. It enables advanced geospatial data processing, including raster and vector data manipulation, format conversion, and spatial analysis, ensuring high performance and cross-platform compatibility. Perfect for web, server-side, and mobile geospatial applications.",
66
"homepage": "https://github.com/bugra9/cpp.js/tree/main/packages/cppjs-package-gdal#readme",

docs/api/performance.md

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ The rule: **if your build runs and your app works, the defaults are fine**. Only
1414
| `-O0` | (debug) | No optimization | ✅ Already debug — fine |
1515
| `-msimd128` | wasm | SIMD128 instruction set | 🔒 Already optimal |
1616
| `-sMEMORY64=1` | wasm64 only | 64-bit memory | 🔒 Set by target.arch, not flag override |
17-
| `-pthread` + `-sPTHREAD_POOL_SIZE=navigator.hardwareConcurrency` | mt only | Thread pool sized to device CPU at module load |`PTHREAD_POOL_SIZE` is tunable (see below) |
17+
| `-pthread` + `-sPTHREAD_POOL_SIZE=Math.min(navigator.hardwareConcurrency \|\| 1, 2)` | mt only | Thread pool capped at 2 workers (1 if `hardwareConcurrency` is unavailable) |`PTHREAD_POOL_SIZE` is tunable (see below) |
1818
| `-sPTHREAD_POOL_SIZE_STRICT=2` | mt only | Abort if more pthreads requested than pool (no dynamic growth) | ⚠️ Drop to `1` (warn + grow) or `0` (silent grow) only if your code spawns unbounded threads |
1919
| `-lembind` | always | Embind binding lib | 🔒 Required |
2020
| `-Wl,--whole-archive` | always | Link all objects | 🔒 Required for static lib symbol retention |
@@ -87,30 +87,35 @@ target: { arch: 'wasm64' }
8787

8888
Don't try to push wasm32 past 4GB — Wasm spec doesn't allow it.
8989

90-
### `PTHREAD_POOL_SIZE` (default: `navigator.hardwareConcurrency`)
90+
### `PTHREAD_POOL_SIZE` (default: `Math.min(navigator.hardwareConcurrency || 1, 2)`)
9191

92-
The pool is sized to the device's CPU count at module load — `navigator.hardwareConcurrency` is evaluated as a JS expression by Emscripten at runtime, not baked in at build time. Most apps need no override.
92+
The default expression is evaluated by Emscripten at module load (not baked at build time):
9393

94-
Paired with `-sPTHREAD_POOL_SIZE_STRICT=2`: if your code requests more pthreads than the pool, the runtime **aborts**. No dynamic growth, no main-thread deadlock risk. If your thread count is bounded by `hardwareConcurrency` you're fine; otherwise either bump the pool or relax strictness.
94+
- Caps the pool at **2 worker threads**, even on 16-core devices.
95+
- Falls back to **1** if `navigator.hardwareConcurrency` is unavailable (older browsers, Node, edge runtimes).
9596

96-
Override only when:
97+
Why the cap? Each pthread is a Web Worker — non-trivial memory footprint, startup latency, and WASM heap duplication. For typical workloads two workers (main + one) covers parallelism; going past 2 trades memory and load time for diminishing returns. Bump only when you've measured CPU-bound parallelism that scales further.
9798

98-
- **Background tasks where you want the main thread responsive** — cap below CPU count:
99+
Paired with `-sPTHREAD_POOL_SIZE_STRICT=2`: if your code requests more pthreads than the pool, the runtime **aborts**. No dynamic growth, no main-thread deadlock risk. If you bump the pool, also relax strictness or keep your spawn count under the new cap.
100+
101+
Override scenarios:
102+
103+
- **CPU-bound parallelism that benefits from more workers** (tile-by-tile image processing, parallel decoding) — bump the pool and relax strict mode:
99104
```js
100105
targetSpecs: [{
101106
platform: 'wasm', runtime: 'mt',
102-
specs: { emccFlags: ['-sPTHREAD_POOL_SIZE=2'] },
107+
specs: { emccFlags: ['-sPTHREAD_POOL_SIZE=8', '-sPTHREAD_POOL_SIZE_STRICT=1'] },
103108
}]
104109
```
105-
- **Workloads that spawn more threads than CPU count**bump pool size, or relax strict mode:
110+
- **Want even less memory pressure**pin to one worker:
106111
```js
107112
targetSpecs: [{
108113
platform: 'wasm', runtime: 'mt',
109-
specs: { emccFlags: ['-sPTHREAD_POOL_SIZE=16', '-sPTHREAD_POOL_SIZE_STRICT=1'] },
114+
specs: { emccFlags: ['-sPTHREAD_POOL_SIZE=1'] },
110115
}]
111116
```
112117

113-
Spawning more than `hardwareConcurrency` doesn't speed things up — context-switching costs dominate.
118+
Past `hardwareConcurrency` real cores, context-switching costs dominate anyway.
114119

115120
### `RESERVED_FUNCTION_POINTERS` (default: 200)
116121

@@ -207,7 +212,7 @@ Your C++ might not throw, but std::vector / std::string / std::map can. Removing
207212

208213
### "I'll bump `PTHREAD_POOL_SIZE` to 32 for max parallelism"
209214

210-
Going past `hardwareConcurrency` adds context-switching overhead. The default already matches the device CPU count at runtime — bumping it past that doesn't help. Bump only when your code spawns more concurrent threads than the device has cores AND you've measured a benefit (you'll also need `-sPTHREAD_POOL_SIZE_STRICT=1` so the extra threads don't trip the strict abort).
215+
Each pthread is a Web Worker with its own WASM instance — bumping pool size to 32 inflates memory and module startup time without a matching speedup. The default caps at 2 deliberately: enough to parallelize the hot path, cheap to spin up. Bump only when measurement shows your workload scales (e.g. embarrassingly parallel image / geo / crypto loops), and pair the bump with `-sPTHREAD_POOL_SIZE_STRICT=1` so spawning more than the new cap doesn't trip the strict abort.
211216

212217
## Profiling
213218

website/docs/api/configuration/performance.md

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ The rule: **if your build runs and your app works, the defaults are fine**. Only
1414
| `-O0` | (debug) | No optimization | ✅ Already debug — fine |
1515
| `-msimd128` | wasm | SIMD128 instruction set | 🔒 Already optimal |
1616
| `-sMEMORY64=1` | wasm64 only | 64-bit memory | 🔒 Set by `target.arch`, not flag override |
17-
| `-pthread` + `-sPTHREAD_POOL_SIZE=navigator.hardwareConcurrency` | mt only | Thread pool sized to device CPU at module load |`PTHREAD_POOL_SIZE` is tunable (see below) |
17+
| `-pthread` + `-sPTHREAD_POOL_SIZE=Math.min(navigator.hardwareConcurrency \|\| 1, 2)` | mt only | Thread pool capped at 2 workers (1 if `hardwareConcurrency` is unavailable) |`PTHREAD_POOL_SIZE` is tunable (see below) |
1818
| `-sPTHREAD_POOL_SIZE_STRICT=2` | mt only | Abort if more pthreads requested than pool (no dynamic growth) | ⚠️ Drop to `1` (warn + grow) or `0` (silent grow) only if your code spawns unbounded threads |
1919
| `-lembind` | always | Embind binding lib | 🔒 Required |
2020
| `-Wl,--whole-archive` | always | Link all objects | 🔒 Required for static lib symbol retention |
@@ -87,30 +87,35 @@ target: { arch: 'wasm64' }
8787

8888
Don't try to push wasm32 past 4GB — Wasm spec doesn't allow it.
8989

90-
### `PTHREAD_POOL_SIZE` (default: `navigator.hardwareConcurrency`)
90+
### `PTHREAD_POOL_SIZE` (default: `Math.min(navigator.hardwareConcurrency || 1, 2)`)
9191

92-
The pool is sized to the device's CPU count at module load — `navigator.hardwareConcurrency` is evaluated as a JS expression by Emscripten at runtime, not baked in at build time. Most apps need no override.
92+
The default expression is evaluated by Emscripten at module load (not baked at build time):
9393

94-
Paired with `-sPTHREAD_POOL_SIZE_STRICT=2`: if your code requests more pthreads than the pool, the runtime **aborts**. No dynamic growth, no main-thread deadlock risk. If your thread count is bounded by `hardwareConcurrency` you're fine; otherwise either bump the pool or relax strictness.
94+
- Caps the pool at **2 worker threads**, even on 16-core devices.
95+
- Falls back to **1** if `navigator.hardwareConcurrency` is unavailable (older browsers, Node, edge runtimes).
9596

96-
Override only when:
97+
Why the cap? Each pthread is a Web Worker — non-trivial memory footprint, startup latency, and WASM heap duplication. For typical workloads two workers (main + one) covers parallelism; going past 2 trades memory and load time for diminishing returns. Bump only when you've measured CPU-bound parallelism that scales further.
9798

98-
- **Background tasks where you want the main thread responsive** — cap below CPU count:
99+
Paired with `-sPTHREAD_POOL_SIZE_STRICT=2`: if your code requests more pthreads than the pool, the runtime **aborts**. No dynamic growth, no main-thread deadlock risk. If you bump the pool, also relax strictness or keep your spawn count under the new cap.
100+
101+
Override scenarios:
102+
103+
- **CPU-bound parallelism that benefits from more workers** (tile-by-tile image processing, parallel decoding) — bump the pool and relax strict mode:
99104
```js
100105
targetSpecs: [{
101106
platform: 'wasm', runtime: 'mt',
102-
specs: { emccFlags: ['-sPTHREAD_POOL_SIZE=2'] },
107+
specs: { emccFlags: ['-sPTHREAD_POOL_SIZE=8', '-sPTHREAD_POOL_SIZE_STRICT=1'] },
103108
}]
104109
```
105-
- **Workloads that spawn more threads than CPU count**bump pool size, or relax strict mode:
110+
- **Want even less memory pressure**pin to one worker:
106111
```js
107112
targetSpecs: [{
108113
platform: 'wasm', runtime: 'mt',
109-
specs: { emccFlags: ['-sPTHREAD_POOL_SIZE=16', '-sPTHREAD_POOL_SIZE_STRICT=1'] },
114+
specs: { emccFlags: ['-sPTHREAD_POOL_SIZE=1'] },
110115
}]
111116
```
112117

113-
Spawning more than `hardwareConcurrency` doesn't speed things up — context-switching costs dominate.
118+
Past `hardwareConcurrency` real cores, context-switching costs dominate anyway.
114119

115120
### `RESERVED_FUNCTION_POINTERS` (default: 200)
116121

@@ -207,7 +212,7 @@ Your C++ might not throw, but `std::vector` / `std::string` / `std::map` can. Re
207212

208213
### "I'll bump `PTHREAD_POOL_SIZE` to 32 for max parallelism"
209214

210-
Going past `hardwareConcurrency` adds context-switching overhead. The default already matches the device CPU count at runtime — bumping it past that doesn't help. Bump only when your code spawns more concurrent threads than the device has cores AND you've measured a benefit (you'll also need `-sPTHREAD_POOL_SIZE_STRICT=1` so the extra threads don't trip the strict abort).
215+
Each pthread is a Web Worker with its own WASM instance — bumping pool size to 32 inflates memory and module startup time without a matching speedup. The default caps at 2 deliberately: enough to parallelize the hot path, cheap to spin up. Bump only when measurement shows your workload scales (e.g. embarrassingly parallel image / geo / crypto loops), and pair the bump with `-sPTHREAD_POOL_SIZE_STRICT=1` so spawning more than the new cap doesn't trip the strict abort.
211216

212217
## Profiling
213218

0 commit comments

Comments
 (0)