Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 58 additions & 19 deletions docs/BUILDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,9 +132,9 @@ target's build command.
The build ships in two flavors (selected by `NODEJS_MOBILE_FLAVOR`, default
`full`). **`full`** is the general-purpose binary all consumers get. **`lite`**
is a smaller binary for consumers that don't need the full feature set, built by
layering feature-drops — and one V8 configuration change, [pointer
compression](#pointer-compression-lite-only) — on top of the full configure, so
the full binary and its test gate are unchanged.
layering feature-drops — and on Android one V8 configuration change, [pointer
compression](#pointer-compression-android-lite-only) — on top of the full
configure, so the full binary and its test gate are unchanged.

What `lite` drops (all already-available upstream `configure` flags, so no extra
patch-stack surface):
Expand All @@ -159,28 +159,32 @@ Measured shipping sizes (arm64, after symbol strip):
both flavors; no
`--v8-lite-mode` (Android keeps the JIT and V8's native WASM for undici).

Those figures were measured before pointer compression was turned on; it aims
at the heap rather than at the binary, and the release CI re-measures the
shipping artifacts on every release.
Those figures were measured before pointer compression was turned on for
Android; it aims at the heap rather than at the binary, and the release CI
re-measures the shipping artifacts on every release.

`build-id` (`-Wl,--build-id=sha1`) is emitted on the Android `libnode.so` in
**both** flavors so crash reporters (e.g. Sentry) can symbolicate native
crashes. The safeguard for `intl=none` is running your own application's test
suite against the lite binary — it catches `Intl` breakage from future
dependency changes.

### Pointer compression (lite only)
### Pointer compression (Android lite only)

`lite` also configures V8 with `--experimental-enable-pointer-compression`.
V8 then stores tagged pointers as 32-bit offsets from a per-isolate 4 GB
*cage* base instead of as full 64-bit addresses, so every object field, array
element and map slot on the JS heap halves. The lever here is **runtime memory,
not file size**: V8's own measurements put the saving at roughly 40% of the JS
heap ([v8.dev/blog/pointer-compression](https://v8.dev/blog/pointer-compression)),
Android `lite` also configures V8 with
`--experimental-enable-pointer-compression`. V8 then stores tagged pointers as
32-bit offsets from a per-isolate 4 GB *cage* base instead of as full 64-bit
addresses, so every object field, array element and map slot on the JS heap
halves. The lever here is **runtime memory, not file size**: V8's own
measurements put the saving at roughly 40% of the JS heap
([v8.dev/blog/pointer-compression](https://v8.dev/blog/pointer-compression)),
which on a phone is the margin between staying resident and being reaped by the
low-memory killer (Android) or jetsam (iOS).
low-memory killer.

Why it is `lite`-only, when the flag is available to both flavors:
**iOS does not get it** — the cage reservation cannot succeed there at all; see
[iOS cannot reserve the cage](#ios-cannot-reserve-the-cage) below.

Why it is `lite`-only on Android, when the flag is available to both flavors:

- **It changes the V8 ABI.** A native addon that includes V8's headers directly
(`v8.h`, NAN, node-addon-api's V8 escape hatches) has to be compiled with the
Expand Down Expand Up @@ -209,15 +213,50 @@ not the copy `configure` already wrote into `config.gypi`: ungated, a 32-bit
lite build would ship a `process.config` claiming compression it doesn't have —
`tools/test.py` derives its `$pointer_compression` status variable from exactly
that, and `test-max-old-space-size-percentage` and
`test-experimental-shared-value-conveyor` branch on it. iOS needs no such gate —
every slice it builds is arm64.
`test-experimental-shared-value-conveyor` branch on it.

Each isolate (the main one, plus one per `Worker`) reserves its own cage, so
the cost is 4 GB of *address space* per isolate — reserved, not committed, and
harmless on a 64-bit address space. Node's shared-cage mode
the cost is 4 GB of *address space* per isolate. Node's shared-cage mode
(`--experimental-pointer-compression-shared-cage`) is deliberately left off,
matching upstream's default.

#### iOS cannot reserve the cage

The address space that costs nothing on Android is exactly what iOS will not
hand out, so the flag is **not** passed in `tools/ios_framework_prepare.sh`.

V8 does not ask for 4 GB. The cage has to be 4 GB-*aligned* as well as
4 GB long, and V8 gets that alignment by over-reserving and trimming —
`OS::Allocate` in `deps/v8/src/base/platform/platform-posix.cc` requests
`size + (alignment - page_size)`, so one `mmap` of just under **8 GB**,
`PROT_NONE`, before it unmaps the misaligned head and tail.

An iOS process does not have 8 GB of address space to give. Without the
[`com.apple.developer.kernel.extended-virtual-addressing`](https://developer.apple.com/documentation/bundleresources/entitlements/com.apple.developer.kernel.extended-virtual-addressing)
entitlement the kernel caps a process at `ARM64_MIN_MAX_ADDRESS`-derived
limits — **7.375 GB** usable on devices with more than 3 GB of RAM, and less
below that, after the 4 GB `PAGE_ZERO` and the 4 GB shared region are taken out
([the arithmetic, with the kernel
constants](https://alwaysprocessing.blog/2022/02/20/size-matters)). The
reservation is therefore larger than the entire address space of the process
and fails on every device, not marginally and not only on small ones.
`VirtualMemoryCage::InitReservation` returns false and
`IsolateGroup::Initialize` calls `V8::FatalProcessOutOfMemory(... "Failed to
reserve virtual memory for process-wide V8 pointer compression cage")`, which
aborts the process during `Isolate` init — i.e. the app crashes a second into
launch, before any embedder JS runs.

The entitlement would lift the cap ("jumbo mode", full 64-bit address space),
but it is not something a runtime library can require: it is a restricted
capability that every consuming app would have to add to its own provisioning
profile, and it would still spend 4 GB of address space per isolate. Shipping
iOS `lite` uncompressed is the cheaper trade.

This is invisible to the iOS **simulator**, which is a macOS process and has no
such cap — the simulator legs of `curated-tests-ios` pass on a build that
cannot start on any physical device. Treat iOS simulator green as no evidence
at all about address-space behaviour.


---

Expand Down
11 changes: 6 additions & 5 deletions docs/FAQ.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,17 +132,18 @@ plugins expose an app-data-dir call that does this for you.

Node native modules, which contain native code, are able to run on nodejs-mobile, as long as they can be cross-compiled for the target platform / CPU. The cross-compiling feature is integrated into the plugins and instructions can be found in the [nodejs-mobile-cordova](https://github.com/nodejs-mobile/nodejs-mobile-cordova#native-modules) or in the [nodejs-mobile-react-native](https://github.com/nodejs-mobile/nodejs-mobile-react-native#native-modules) README, but only Linux and MacOS development machines are currently supported. Modules that contain custom build steps and platform specific code may need workarounds/changes to get them to work. We've created a github repository so that the workarounds/changes can be discussed and shared: https://github.com/nodejs-mobile/nodejs-mobile-module-compat

One caveat if you ship the **lite** flavor: it is built with V8 pointer
One caveat if you ship the **Android lite** flavor: it is built with V8 pointer
compression, which changes the V8 ABI (see ["Pointer compression" in
BUILDING.md](./BUILDING.md#pointer-compression-lite-only) on the recipe
BUILDING.md](./BUILDING.md#pointer-compression-android-lite-only) on the recipe
branch). Modules written against **N-API** — the ABI `libnode` exports, and
what most maintained modules use — are unaffected. A module that includes V8's
headers directly (`v8.h`, NAN) has to be compiled with the same defines the
library was: the shipped `include/node` headers carry no `config.gypi`, so add
`-DV8_COMPRESS_POINTERS -DV8_31BIT_SMIS_ON_64BIT_ARCH` to that module's build
for the lite flavor (and nothing extra for full). Compiled without them and
loaded into lite, it reads object fields at the wrong offsets. The `full`
flavor keeps the upstream-standard ABI.
for Android lite. Compiled without them and loaded into that binary, it reads
object fields at the wrong offsets. **iOS lite and both `full` flavors keep the
upstream-standard ABI** and need nothing extra — iOS cannot enable pointer
compression at all, for the address-space reason documented in BUILDING.md.

## How can I improve Node.js load times?

Expand Down
2 changes: 1 addition & 1 deletion expected-tree.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@
# run prepare.sh (or regenerate-patches.py, which prints the hash) and
# update the value below in the same commit. If you forget, CI fails and
# prints the hash it got.
0c75ab41501de7107495f5184e239cade1268a78
7c521dbcac21b26079e185d32277373ffe081f5a
20 changes: 9 additions & 11 deletions mobile-src/tools/ios_framework_prepare.sh
Original file line number Diff line number Diff line change
Expand Up @@ -45,17 +45,15 @@ V8_DISABLE_MAGLEV="--v8-disable-maglev"
LITE_FLAGS=""
if [ "$FLAVOR" = "lite" ]; then
INTL="none"
# lite additionally drops features size-constrained consumers don't need, and
# compresses V8 pointers to 32 bits inside a 4GB cage — the lever on the JS
# heap rather than on the binary. No arch gate is needed here the way there is
# in android_configure.py: every slice this script builds is arm64, and
# upstream only force-disables pointer compression for 32-bit target_arch.
# It is lite-only because it changes the V8 ABI — an addon that includes V8
# headers directly must be compiled with the matching
# V8_COMPRESS_POINTERS/V8_31BIT_SMIS_ON_64BIT_ARCH defines or it reads object
# fields at the wrong offsets. N-API addons are unaffected (that ABI hides V8
# layout). See "The lite variant" in docs/BUILDING.md on the recipe branch.
LITE_FLAGS="--without-amaro --without-inspector --without-sqlite --experimental-enable-pointer-compression"
# lite additionally drops features size-constrained consumers don't need.
# Deliberately no --experimental-enable-pointer-compression, unlike Android
# lite: V8 reserves the 4GB cage as one mmap of 4GB + (4GB - page) which it
# then trims to alignment, and an iOS app without the
# com.apple.developer.kernel.extended-virtual-addressing entitlement gets at
# most 7.375GB of address space, so that reservation can never succeed and V8
# aborts the process during Isolate init. See "Pointer compression" in
# docs/BUILDING.md on the recipe branch.
LITE_FLAGS="--without-amaro --without-inspector --without-sqlite"
fi

declare -a outputs_common=(
Expand Down
Loading