Skip to content

Commit c08a91b

Browse files
authored
feat!: upgrade V8 to 14.9.207.39 (#1987)
1 parent c22ad60 commit c08a91b

334 files changed

Lines changed: 6856 additions & 63352 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/npm_release.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ on:
1515
env:
1616
NPM_TAG: "next"
1717
EMULATOR_NAME: "runtime-emu"
18-
NDK_VERSION: r27d
18+
NDK_VERSION: r29
1919
ANDROID_API: 33
2020
ANDROID_ABI: x86_64
2121
NDK_ARCH: linux
@@ -93,6 +93,8 @@ jobs:
9393
NPM_TAG=$(node ./scripts/get-npm-tag.js)
9494
echo NPM_VERSION=$NPM_VERSION >> $GITHUB_OUTPUT
9595
echo NPM_TAG=$NPM_TAG >> $GITHUB_OUTPUT
96+
- name: Fetch prebuilt V8
97+
run: ./download_v8.sh
9698
- name: Build npm package
9799
run: ./gradlew -PgitCommitVersion=${{ github.sha }} -PnoCCache --stacktrace
98100
- name: Upload npm package artifact
@@ -144,6 +146,8 @@ jobs:
144146
run: |
145147
npm install
146148
npm install --prefix ./test-app/tools
149+
- name: Fetch prebuilt V8
150+
run: ./download_v8.sh
147151
- name: SBG tests
148152
run: ./gradlew runSbgTests --stacktrace
149153
- name: Enable KVM

.github/workflows/pull_request.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ on:
55
env:
66
NPM_TAG: "pr"
77
EMULATOR_NAME: "runtime-emu"
8-
NDK_VERSION: r27d
8+
NDK_VERSION: r29
99
ANDROID_API: 33
1010
ANDROID_ABI: x86_64
1111
NDK_ARCH: linux
@@ -67,6 +67,8 @@ jobs:
6767
NPM_TAG=$(node ./scripts/get-npm-tag.js)
6868
echo NPM_VERSION=$NPM_VERSION >> $GITHUB_OUTPUT
6969
echo NPM_TAG=$NPM_TAG >> $GITHUB_OUTPUT
70+
- name: Fetch prebuilt V8
71+
run: ./download_v8.sh
7072
- name: Build npm package
7173
run: ./gradlew -PgitCommitVersion=${{ github.sha }} -PnoCCache --stacktrace
7274
- name: Upload npm package artifact
@@ -112,6 +114,8 @@ jobs:
112114
run: |
113115
npm install
114116
npm install --prefix ./test-app/tools
117+
- name: Fetch prebuilt V8
118+
run: ./download_v8.sh
115119
- name: SBG tests
116120
run: ./gradlew runSbgTests --stacktrace
117121
- name: Enable KVM

.gitignore

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,20 @@ thumbs.db
2424
android-runtime.iml
2525
test-app/build-tools/*.log
2626
test-app/analytics/build-statistics.json
27-
package-lock.json
27+
package-lock.json
28+
29+
# V8 prebuilts and the headers that must match them, installed by
30+
# download_v8.sh from the release pinned in V8_RELEASE.
31+
#
32+
# The headers are ignored along with the libraries on purpose: keeping a
33+
# vendored copy in git is how it drifts out of step with the binaries it
34+
# describes. Sourcing both from one verified artifact makes that impossible.
35+
/.v8-prebuilt/
36+
test-app/runtime/src/main/libs/*/libv8_monolith.a
37+
test-app/runtime/src/main/libs/.v8-release-stamp
38+
test-app/runtime/src/main/cpp/include/**
39+
!test-app/runtime/src/main/cpp/include/zip.h
40+
!test-app/runtime/src/main/cpp/include/zipconf.h
41+
test-app/runtime/src/main/cpp/v8_inspector/src/
42+
test-app/runtime/src/main/cpp/v8_inspector/third_party/
43+
test-app/runtime/src/main/cpp/v8_inspector/absl/

V8_RELEASE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
v8-14.9.207.39-3

build.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ adb version
1111
echo "Update submodule"
1212
git submodule update --init
1313

14+
echo "Fetch the prebuilt V8 (no-op once in place; V8_SKIP_DOWNLOAD=1 to skip)"
15+
./download_v8.sh
16+
1417
echo "Cleanup old build and test artefacts"
1518
rm -rf consoleLog.txt
1619
rm -rf test-app/dist/*.xml

docs/README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,12 @@
22

33
- [Error handling](error-handling.md) — global `error`/`unhandledrejection` events, `reportError`, catching Java exceptions in JS (`error.nativeException`), forwarding JS throws to Java callers (`interop.escapeException`), JS stacks on Java exceptions (`com.tns.JavaScriptStackTrace`), configuration flags, and crash-reporter integration.
44
- [Implementing additional Chrome DevTools protocol Domains](extending-inspector.md)
5+
6+
## Knowledge
7+
8+
Notes on work that is done, kept because the reasoning is expensive to
9+
reconstruct rather than because anything needs doing.
10+
11+
- [V8 10.3 → 14.9 migration](knowledge/v8-14-migration.md) — the API changes and
12+
their site counts, why each non-default gn arg exists, the accessor rules that
13+
are not mechanical, and the traps that only show up at runtime.

docs/knowledge/v8-14-migration.md

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
# V8 10.3 → 14.9 migration notes (Android)
2+
3+
Pinned version: **14.9.207.39** (`branch-heads/14.9`).
4+
The libraries are built by [NativeScript/v8-buildscripts](https://github.com/NativeScript/v8-buildscripts)
5+
and installed here by `download_v8.sh` from the release pinned in `V8_RELEASE`. The iOS runtime moved to the same
6+
version; where the two runtimes hit the same API change the notes are kept in
7+
sync.
8+
9+
## What ships
10+
11+
`libv8_monolith.a` per ABI under `test-app/runtime/src/main/libs/<abi>/`, plus
12+
the public headers under `test-app/runtime/src/main/cpp/include/` and the
13+
vendored V8 internals under `test-app/runtime/src/main/cpp/v8_inspector/`.
14+
All three come from the same release artifact; they are a matched set and
15+
must never be updated separately.
16+
17+
## Build configuration
18+
19+
The gn args reproduce the 10.3 build: JIT and WebAssembly on, i18n off. What is
20+
new, and why:
21+
22+
- **`use_allocator_shim=false`** — the shim interposes on `malloc` through
23+
linker `--wrap` flags that the embedder would also have to pass. Without it
24+
the link fails on `__real_realpath` / `__real_getcwd`. PartitionAlloc itself
25+
stays enabled (V8 depends on the target); it just no longer replaces malloc.
26+
- **`use_thin_lto=false`**`is_official_build` now turns ThinLTO on, and a
27+
ThinLTO build emits LLVM bitcode rather than object code, which the app's NDK
28+
cannot link.
29+
- **`chrome_pgo_phase=0`**`is_official_build` also turns PGO on, and
30+
standalone V8 has no `tools/update_pgo_profiles.py`.
31+
- **`v8_enable_temporal_support=false`** — Temporal is implemented in Rust and
32+
pulls in a Rust sysroot this build does not link. 10.3 had no Temporal.
33+
- **`v8_array_buffer_internal_field_count=2`** and
34+
**`v8_array_buffer_view_internal_field_count=2`** — both default to `0` in
35+
14.9 and defaulted to `2` in 10.3. When an `ArrayBuffer`/`SharedArrayBuffer`/
36+
typed array is marshalled to a Java NIO buffer,
37+
`JSToJavaObjectsConverter` calls `ObjectManager::Link` on the buffer object
38+
itself, and `Link` stores its `JSInstanceInfo` in internal field 0. With zero
39+
internal fields every such conversion throws *"Trying to link invalid 'this'
40+
to a Java object"*. `v8-array-buffer.h` still falls back to `2` when the macro
41+
is undefined, which is what the runtime compiles against, so leaving the gn
42+
default in place also puts the two sides out of agreement.
43+
- **`v8_enable_sandbox=false`** — pinned rather than left to default. It would
44+
otherwise follow pointer compression (on for 64-bit) and change the object
45+
layout the runtime compiles against, which is a much larger change than a
46+
version bump.
47+
- **`android_ndk_root`** pinned to the NDK the runtime is built with — see
48+
below.
49+
50+
The build deletes its output directory before each run. ninja never
51+
removes outputs orphaned by a config change, so reusing one across V8 versions
52+
silently keeps stale objects, and the packaging step would vendor them.
53+
54+
### The NDK has to match on both sides
55+
56+
V8's bundled NDK (CIPD `30.0.14608247`) is newer than any released one. Its
57+
libc++ exports symbols the runtime's `libc++_static.a` does not — the link fails
58+
on `std::__ndk1::__hash_memory`, referenced from `liveedit.cc`. `libc++` is only
59+
ABI-compatible with itself across a static link, so V8 is built against the same
60+
NDK the runtime uses, via the `android_ndk_root` gn arg (made overridable by
61+
`android_build.patch`).
62+
63+
**The runtime moved from NDK r27d to r29.** This is forced, not optional: V8
64+
14.9's `src/base/atomicops.h` uses `std::atomic_ref` unconditionally, and
65+
r27d ships libc++ 18, which does not implement it (`__cpp_lib_atomic_ref` is
66+
commented out in its `<version>`). The runtime compiles those headers because
67+
`v8_inspector` vendors V8 internals. r29 is the first released NDK with a
68+
libc++ new enough. `minSdk` is unchanged at 21.
69+
70+
### Building on macOS
71+
72+
Chromium asserts a Linux host for Android targets. Everything below that assert
73+
still handles macOS — the host-arch block maps arm64 hosts to the
74+
`darwin-x86_64` NDK tag deliberately, and `android_toolchain_root` is only read
75+
for the (host-independent) sysroot — so `android_build.patch` relaxes
76+
the assert. It also lowers `min_supported_sdk_version` from 23 to 21; that floor
77+
exists for Java/dex tooling and this build produces only the native
78+
`v8_monolith` target.
79+
80+
Two things the macOS clang package does not carry, both handled by buildscripts' `fetch_v8.sh`:
81+
82+
- the Android **compiler-rt builtins** (`libclang_rt.builtins-*-android.a`),
83+
which only the Linux clang package bundles — they are extracted from it;
84+
- a `darwin-x86_64` directory in the CIPD NDK, which only ships `linux-x86_64`
85+
symlinked (only relevant when `android_ndk_root` is left at its default).
86+
87+
Because not every ABI can be rebuilt on every host, the gradle builds accept
88+
`-Pabis=arm64-v8a,x86_64` to restrict `abiFilters` to the ones that have a
89+
current `libv8_monolith.a`.
90+
91+
**The 32-bit ABIs cannot be built on an Apple Silicon host.** `armeabi-v7a` and
92+
`x86` need mksnapshot to run V8's simulator for a 32-bit target, and
93+
`v8config.h` hard-errors with *"Target architecture arm is only supported on arm
94+
and ia32 host"*. The Linux x64 path is unaffected — it builds mksnapshot as a
95+
32-bit x86 host binary — so those two ABIs have to come from a Linux x64
96+
builder, which is where CI builds them anyway.
97+
98+
## API changes applied to the runtime
99+
100+
| Change | Sites | Migration |
101+
|---|---|---|
102+
| `Context/Object/Function/Promise/Message::GetIsolate()` removed | 46 | `v8::Isolate::GetCurrent()` |
103+
| `External::New` / `External::Value()` take a type tag | 47 | `v8::kExternalPointerTypeTagDefault` |
104+
| `PropertyCallbackInfo::This()` removed | 25 | `Holder()`, or a function-backed accessor — see below |
105+
| `Object/ObjectTemplate/Function::SetAccessor` | 21 | `SetNativeDataProperty` / `SetAccessorProperty` |
106+
| Accessor callbacks take `Local<Name>` | 23 | was `Local<String>` |
107+
| `Object::CreationContext()` | 7 | `GetCreationContext(isolate).ToLocalChecked()` |
108+
| `ScriptOrigin` no longer takes an `Isolate*` | 8 | drop the first argument |
109+
| `AccessControl` removed | 5 | drop the argument |
110+
| `GetInternalField` returns `Local<Data>` | 4 | `.As<v8::Value>()` |
111+
| Interceptor callbacks return `v8::Intercepted` | 2 | see below |
112+
| `SetIndexedPropertyHandler` | 1 | `SetHandler(IndexedPropertyHandlerConfiguration(...))` |
113+
| `V8Inspector::connect` needs a trust level | 3 | `kFullyTrusted` |
114+
| `V8ConsoleMessage::createForConsoleAPI` takes a span | 2 | `{args.data(), args.size()}` |
115+
| `FunctionCallbackInfo` is no longer copyable | 1 | `ArgsWrapper` holds a reference |
116+
117+
`unistd.h` also has to be included explicitly in `ModuleInternal.cpp` and
118+
`WorkerWrapper.cpp`; `usleep`/`read` used to arrive transitively through headers
119+
that no longer pull it in.
120+
121+
### Accessors that are inherited need a real accessor pair
122+
123+
`PropertyCallbackInfo` no longer exposes the receiver at all, and
124+
`SetNativeDataProperty` is not a drop-in replacement for `SetAccessor` on
125+
anything that is inherited from. Six accessors in `MetadataNode` are installed
126+
on an object other than the one they are read through:
127+
128+
- on the **constructor function**, which derived constructors inherit:
129+
`class`, `nullObject`, and static fields;
130+
- on the **implementation object**, which instances inherit: `super`;
131+
- on the **prototype template**, which instances inherit: instance fields and
132+
properties.
133+
134+
All six are now `SetAccessorProperty` with `FunctionTemplate`-backed
135+
getter/setter, whose `FunctionCallbackInfo::This()` still returns the receiver.
136+
Static fields matter most: they have a setter, and `SetNativeDataProperty`
137+
installs something data-like, so `Derived.baseField = x` would shadow the base's
138+
property with an own data property and never reach the native setter.
139+
140+
The accessors that stay `SetNativeDataProperty` are the ones installed as own
141+
properties on the object they are read through — the array wrapper's `length`,
142+
the package object's children, inner types on a constructor, and the `URL*`
143+
instance templates. For those `Holder() == This()`.
144+
145+
The rule of thumb: if an accessor lives on anything that is inherited from, use
146+
`SetAccessorProperty` with function-backed callbacks. `SetNativeDataProperty` is
147+
only safe where nothing inherits it.
148+
149+
This costs an allocation the old API did not: each converted accessor now needs
150+
a real `Function` object rather than an `AccessorInfo`. On the prototype
151+
template that is deferred to instantiation, but static fields are materialised
152+
eagerly, so a class with many static fields pays for them when its constructor
153+
function is first built.
154+
155+
#### One guard could not be translated directly
156+
157+
`FieldAccessorGetter/SetterCallback` used `thiz->StrictEquals(info.Holder())` to
158+
detect an instance field being read straight off the prototype. Function-backed
159+
accessors have no holder, so the check is now
160+
`!objectManager->IsJsRuntimeObject(thiz)` — being a runtime-managed object is
161+
the property that actually distinguishes an instance from the prototype. The
162+
two agree for every receiver the old check could see; the new one additionally
163+
returns `undefined` (rather than reaching `GetJavaField` with a non-instance)
164+
for something like `Object.create(SomeClass.prototype).field`.
165+
166+
### Interceptors
167+
168+
Only one interceptor pair exists here — the array wrapper's indexed
169+
getter/setter. Both handle the access completely, so both return
170+
`Intercepted::kYes`; neither ever fell through to the ordinary lookup. The
171+
setter's `PropertyCallbackInfo` also changed from `<Value>` to `<Boolean>`, so
172+
its old `GetReturnValue().Set(value)` is dropped — the return value is now the
173+
strict-mode success flag, not the stored value.
174+
175+
The conversion rule in general: a path that set a return value or threw becomes
176+
`kYes`; a path that returned without setting one, **including falling off the
177+
end**, becomes `kNo`. Getting it backwards is silent in both directions.
178+
179+
### V8 flags must be set before `V8::Initialize()`
180+
181+
`V8::Initialize()` calls `FlagList::FreezeFlags()`, and changing a flag
182+
afterwards aborts the process. `PrepareV8Runtime` used to apply
183+
`Constants::V8_STARTUP_FLAGS` per isolate, after initialization; it now happens
184+
once in `InitializeV8()`, before `V8::Initialize()`. `Runtime::Init` has already
185+
read the flags out of the Java config by then.
186+
187+
## Things that did *not* need changing
188+
189+
- **Resurrecting finalizers.** `ObjectManager` uses
190+
`WeakCallbackType::kFinalizer` in four places. Upstream removed it right after
191+
10.3.22; buildscripts' `v8_resurrecting_finalizers.patch` restores it. See
192+
the iOS runtime's `docs/knowledge/v8-resurrecting-finalizers.md` for the patch
193+
design.
194+
195+
From `v8-14.9.207.39-2` that patch also lifts the
196+
`DisallowJavascriptExecution` scope `Heap::CollectGarbage` now holds across
197+
the whole collection, because entering JS from a GC callback is a
198+
`GRACEFUL_FATAL` in 14.9 where 10.3 allowed it. This runtime does not depend
199+
on the lift — `JSObjectFinalizer` makes one runtime-internal JNI call
200+
(`makeInstanceWeakAndCheckIfAlive`) and Java has no synchronous destructor
201+
that could re-enter JS — but it shares the patch, so pin a release that
202+
carries it.
203+
- **Teardown disposal.** The runtime never used
204+
`Isolate::VisitHandlesWithClassIds` or `SetWrapperClassId`, so the registry
205+
the iOS runtime had to grow is not needed here.
206+
207+
## Test status
208+
209+
The runtime test suite on an API 35 arm64 emulator: **594 tests, 0 failures,
210+
0 errors, 5 skipped.** All five skips are pre-existing `xit()` in the checked-in
211+
suite (`testNativeModules`, `exceptionHandlingTests` SIGABRT, `testArrays`
212+
memory leak, `TNS require` index.json, `TNS Workers` circular postMessage).
213+
214+
### Inspector
215+
216+
Verified end to end against the debug build over the runtime's WebSocket
217+
(`<package>-inspectorServer`, an abstract local socket): `Runtime.enable`
218+
(execution context creation and replay of stored console messages, with stack
219+
traces), `Runtime.evaluate` for arithmetic, object serialisation and Java
220+
interop, `Debugger.enable`, and exception reporting through `exceptionDetails`.
221+
This is the part most exposed to a stale `v8_inspector` tree, since it compiles
222+
against V8 internals rather than the public API.
223+
224+
## Known follow-ups
225+
226+
- All four ABIs now come from the pinned release, but only buildscripts' CI can
227+
produce the 32-bit two: they need an ia32-capable Linux x64 host (see above),
228+
so a local macOS build cannot regenerate them.
229+
- **`V8_STATIC_ROOTS` is deliberately not defined by the runtime.** V8 is built
230+
with it, and it would let `Value::IsUndefined()`/`IsNull()` and friends use
231+
the inline static-root comparison instead of reading the map. It is left off
232+
because the root addresses are hardcoded constants in `v8-internal.h` that
233+
must match the library exactly (`kBuildDependentTheHoleValue` even varies with
234+
`V8_ENABLE_WEBASSEMBLY`), and 10.3 had no such fast path either — so this is
235+
parity, and enabling it is a measurable but separate change.
236+
- On-device compiled-code caches written by 10.3 are stale. `TryLoadScriptCache`
237+
validates them only by comparing mtimes with the `.js` file, so an app update
238+
(which rewrites the scripts) discards them and they are regenerated. If the
239+
mtimes did happen to match, V8 rejects the data by its own version hash and
240+
recompiles from source — correct, but `SaveScriptCache` is only called on the
241+
no-cache path, so that file would never be refreshed. Pre-existing behaviour,
242+
newly reachable on a version bump.
243+
- Maglev is enabled by default in 14.9 and did not exist in 10.3, so the
244+
monolith carries a whole extra compiler tier. `v8_enable_maglev=false` is the
245+
lever if the size matters more than the warm-up performance.

0 commit comments

Comments
 (0)