Skip to content

Commit 96e1ff2

Browse files
committed
refactor: move the builtin layer's isolate state into RuntimeState
BuiltinLoader kept two Isolate*-keyed process-wide maps (isolateToPrimordials, isolateToBuiltinRequire) and NsBuiltinModules a third (isolateToRealm), each behind its own mutex -- the same shape RuntimeState exists to remove. The primordials lookup runs on every builtin call, so that one took a lock on a hot path to reach state that was never actually shared. All three become per-runtime state: a BuiltinRealm holding the two handles as v8::Globals, and RealmState, which was already a per-runtime struct with a destructor. Reaching either is now an isolate data-slot read plus a vector index, and both are released with the runtime while its isolate is alive. GetRealm can now return null (the runtime has begun tearing down), so its four callers degrade rather than resurrect state teardown already released; Instantiate keeps its contract of leaving an exception pending. With nothing left to release per isolate, disposeIsolate and IsolateDisposer are deleted along with the DestroyRuntime call site. RealmState and the BuiltinLoader handles are consequently destroyed at m_state->Clear() instead; neither destructor runs JS or touches anything torn down in between, and ~RealmState only deletes v8::Persistents, which never call into V8.
1 parent 1903726 commit 96e1ff2

9 files changed

Lines changed: 62 additions & 122 deletions

File tree

test-app/runtime/CMakeLists.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,6 @@ add_library(
176176
src/main/cpp/FieldAccessor.cpp
177177
src/main/cpp/File.cpp
178178
src/main/cpp/Interop.cpp
179-
src/main/cpp/IsolateDisposer.cpp
180179
src/main/cpp/IsolateTracked.cpp
181180
src/main/cpp/JEnv.cpp
182181
src/main/cpp/DesugaredInterfaceCompanionClassNameResolver.cpp

test-app/runtime/src/main/cpp/BuiltinLoader.cpp

Lines changed: 24 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
#include "ArgConverter.h"
77
#include "NsBuiltinModules.h"
8+
#include "RuntimeState.h"
89
#include "robin_hood.h"
910

1011
using namespace v8;
@@ -36,13 +37,14 @@ constexpr const char* kPrimordialsParamName = "primordials";
3637
constexpr size_t kParamCount = 5;
3738

3839
/*
39-
* Per-isolate intrinsics snapshot and builtin require. Worker runtimes
40-
* initialize on their own threads, so every access is under the mutex.
40+
* This runtime's intrinsics snapshot and builtin require. Per-runtime state
41+
* rather than an isolate-keyed shared map, so reaching it needs no lock and it
42+
* is released with the runtime, while the isolate is still alive.
4143
*/
42-
std::mutex primordialsMutex;
43-
robin_hood::unordered_map<Isolate*, Persistent<Object>*> isolateToPrimordials;
44-
std::mutex builtinRequireMutex;
45-
robin_hood::unordered_map<Isolate*, Persistent<v8::Function>*> isolateToBuiltinRequire;
44+
struct BuiltinRealm {
45+
v8::Global<v8::Object> primordials;
46+
v8::Global<v8::Function> builtinRequire;
47+
};
4648

4749
/*
4850
* The `require` every builtin receives: builtin specifiers only, so a builtin
@@ -70,12 +72,13 @@ void BuiltinRequireCallback(const FunctionCallbackInfo<Value>& info) {
7072
MaybeLocal<v8::Function> GetBuiltinRequire(Local<Context> context) {
7173
Isolate* isolate = v8::Isolate::GetCurrent();
7274

73-
{
74-
std::lock_guard<std::mutex> lock(builtinRequireMutex);
75-
auto it = isolateToBuiltinRequire.find(isolate);
76-
if (it != isolateToBuiltinRequire.end()) {
77-
return it->second->Get(isolate);
78-
}
75+
auto* realm = RuntimeState::For<BuiltinRealm>(isolate);
76+
if (realm == nullptr) {
77+
return MaybeLocal<v8::Function>();
78+
}
79+
80+
if (!realm->builtinRequire.IsEmpty()) {
81+
return realm->builtinRequire.Get(isolate);
7982
}
8083

8184
Local<v8::Function> require;
@@ -85,10 +88,7 @@ MaybeLocal<v8::Function> GetBuiltinRequire(Local<Context> context) {
8588
return MaybeLocal<v8::Function>();
8689
}
8790

88-
{
89-
std::lock_guard<std::mutex> lock(builtinRequireMutex);
90-
isolateToBuiltinRequire.emplace(isolate, new Persistent<v8::Function>(isolate, require));
91-
}
91+
realm->builtinRequire.Reset(isolate, require);
9292
return require;
9393
}
9494

@@ -191,12 +191,13 @@ MaybeLocal<Value> CallBuiltin(Local<Context> context, BuiltinId id, Local<Value>
191191
MaybeLocal<Object> GetPrimordials(Local<Context> context) {
192192
Isolate* isolate = v8::Isolate::GetCurrent();
193193

194-
{
195-
std::lock_guard<std::mutex> lock(primordialsMutex);
196-
auto it = isolateToPrimordials.find(isolate);
197-
if (it != isolateToPrimordials.end()) {
198-
return it->second->Get(isolate);
199-
}
194+
auto* realm = RuntimeState::For<BuiltinRealm>(isolate);
195+
if (realm == nullptr) {
196+
return MaybeLocal<Object>();
197+
}
198+
199+
if (!realm->primordials.IsEmpty()) {
200+
return realm->primordials.Get(isolate);
200201
}
201202

202203
Local<Value> result;
@@ -207,10 +208,7 @@ MaybeLocal<Object> GetPrimordials(Local<Context> context) {
207208
}
208209

209210
Local<Object> primordials = result.As<Object>();
210-
{
211-
std::lock_guard<std::mutex> lock(primordialsMutex);
212-
isolateToPrimordials.emplace(isolate, new Persistent<Object>(isolate, primordials));
213-
}
211+
realm->primordials.Reset(isolate, primordials);
214212
return primordials;
215213
}
216214

@@ -226,22 +224,4 @@ MaybeLocal<Value> BuiltinLoader::RunBuiltin(Local<Context> context, BuiltinId id
226224
return CallBuiltin(context, id, binding, primordials);
227225
}
228226

229-
void BuiltinLoader::onDisposeIsolate(Isolate* isolate) {
230-
{
231-
std::lock_guard<std::mutex> lock(primordialsMutex);
232-
auto it = isolateToPrimordials.find(isolate);
233-
if (it != isolateToPrimordials.end()) {
234-
delete it->second;
235-
isolateToPrimordials.erase(it);
236-
}
237-
}
238-
239-
std::lock_guard<std::mutex> lock(builtinRequireMutex);
240-
auto it = isolateToBuiltinRequire.find(isolate);
241-
if (it != isolateToBuiltinRequire.end()) {
242-
delete it->second;
243-
isolateToBuiltinRequire.erase(it);
244-
}
245-
}
246-
247227
} // namespace tns

test-app/runtime/src/main/cpp/BuiltinLoader.h

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,6 @@ class BuiltinLoader {
2727
static v8::MaybeLocal<v8::Value> RunBuiltin(
2828
v8::Local<v8::Context> context, BuiltinId id,
2929
v8::Local<v8::Value> binding = v8::Local<v8::Value>());
30-
31-
static void onDisposeIsolate(v8::Isolate* isolate);
3230
};
3331

3432
} // namespace tns

test-app/runtime/src/main/cpp/IsolateDisposer.cpp

Lines changed: 0 additions & 21 deletions
This file was deleted.

test-app/runtime/src/main/cpp/IsolateDisposer.h

Lines changed: 0 additions & 15 deletions
This file was deleted.

test-app/runtime/src/main/cpp/NsBuiltinModules.cpp

Lines changed: 32 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22

33
#include <android/log.h>
44

5-
#include <mutex>
65
#include <vector>
76

87
#include "ArgConverter.h"
98
#include "BuiltinLoader.h"
9+
#include "RuntimeState.h"
1010
#include "console/Console.h"
1111
#include "robin_hood.h"
1212

@@ -50,11 +50,9 @@ bool HasPrefix(const std::string& specifier, const char* prefix) {
5050

5151
/*
5252
* A builtin module is a singleton per realm, so every cache here is per
53-
* isolate: workers get their own exports objects and their own synthetic
53+
* runtime: workers get their own exports objects and their own synthetic
5454
* modules. The process-global g_moduleRegistry deliberately holds none of
55-
* this. Worker runtimes initialize on their own threads, so the map itself is
56-
* under the mutex; the state it holds is only ever touched from its isolate's
57-
* thread.
55+
* this. Touched only from its own runtime's thread.
5856
*/
5957
struct RealmState {
6058
robin_hood::unordered_map<std::string, Persistent<Object>*> exports;
@@ -76,16 +74,13 @@ struct RealmState {
7674
}
7775
};
7876

79-
std::mutex realmsMutex;
80-
robin_hood::unordered_map<Isolate*, RealmState*> isolateToRealm;
81-
82-
RealmState& GetRealm(Isolate* isolate) {
83-
std::lock_guard<std::mutex> lock(realmsMutex);
84-
auto it = isolateToRealm.find(isolate);
85-
if (it == isolateToRealm.end()) {
86-
it = isolateToRealm.emplace(isolate, new RealmState()).first;
87-
}
88-
return *it->second;
77+
/*
78+
* This runtime's realm. Per-runtime state rather than an isolate-keyed shared
79+
* map, so reaching it needs no lock and it is released with the runtime. Null
80+
* once the runtime has begun tearing down.
81+
*/
82+
RealmState* GetRealm(Isolate* isolate) {
83+
return RuntimeState::For<RealmState>(isolate);
8984
}
9085

9186
MaybeLocal<Object> BuildBinding(Local<Context> context, BuiltinId builtin) {
@@ -119,7 +114,13 @@ MaybeLocal<Object> BuildBinding(Local<Context> context, BuiltinId builtin) {
119114
*/
120115
bool Instantiate(Local<Context> context, const Registration& requested) {
121116
Isolate* isolate = v8::Isolate::GetCurrent();
122-
RealmState& realm = GetRealm(isolate);
117+
RealmState* realmState = GetRealm(isolate);
118+
if (realmState == nullptr) {
119+
isolate->ThrowException(Exception::Error(ArgConverter::ConvertToV8String(
120+
isolate, "Cannot load a builtin module: the runtime is shutting down")));
121+
return false;
122+
}
123+
RealmState& realm = *realmState;
123124

124125
/*
125126
* A shim reaches its ns: module through the builtin require, so the graph
@@ -241,7 +242,11 @@ MaybeLocal<Object> NsBuiltinModules::GetExports(Local<Context> context,
241242
}
242243

243244
Isolate* isolate = v8::Isolate::GetCurrent();
244-
RealmState& realm = GetRealm(isolate);
245+
RealmState* realmState = GetRealm(isolate);
246+
if (realmState == nullptr) {
247+
return MaybeLocal<Object>();
248+
}
249+
RealmState& realm = *realmState;
245250
auto it = realm.exports.find(specifier);
246251
if (it == realm.exports.end()) {
247252
if (!Instantiate(context, *registration)) {
@@ -258,7 +263,11 @@ MaybeLocal<Object> NsBuiltinModules::GetExports(Local<Context> context,
258263
MaybeLocal<Module> NsBuiltinModules::GetModule(Local<Context> context,
259264
const std::string& specifier) {
260265
Isolate* isolate = v8::Isolate::GetCurrent();
261-
RealmState& realm = GetRealm(isolate);
266+
RealmState* realmState = GetRealm(isolate);
267+
if (realmState == nullptr) {
268+
return MaybeLocal<Module>();
269+
}
270+
RealmState& realm = *realmState;
262271

263272
auto it = realm.modules.find(specifier);
264273
if (it != realm.modules.end()) {
@@ -310,7 +319,11 @@ MaybeLocal<Module> NsBuiltinModules::GetModule(Local<Context> context,
310319

311320
Local<v8::Function> NsBuiltinModules::GetFormatFunc(Local<Context> context) {
312321
Isolate* isolate = v8::Isolate::GetCurrent();
313-
RealmState& realm = GetRealm(isolate);
322+
RealmState* realmState = GetRealm(isolate);
323+
if (realmState == nullptr) {
324+
return Local<v8::Function>();
325+
}
326+
RealmState& realm = *realmState;
314327
if (realm.format != nullptr) {
315328
return realm.format->Get(isolate);
316329
}
@@ -338,13 +351,4 @@ Local<v8::Function> NsBuiltinModules::GetFormatFunc(Local<Context> context) {
338351
return format.As<v8::Function>();
339352
}
340353

341-
void NsBuiltinModules::onDisposeIsolate(Isolate* isolate) {
342-
std::lock_guard<std::mutex> lock(realmsMutex);
343-
auto it = isolateToRealm.find(isolate);
344-
if (it != isolateToRealm.end()) {
345-
delete it->second;
346-
isolateToRealm.erase(it);
347-
}
348-
}
349-
350354
} // namespace tns

test-app/runtime/src/main/cpp/NsBuiltinModules.h

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,6 @@ class NsBuiltinModules {
5757
* module could not be built; callers degrade instead of failing the log.
5858
*/
5959
static v8::Local<v8::Function> GetFormatFunc(v8::Local<v8::Context> context);
60-
61-
static void onDisposeIsolate(v8::Isolate* isolate);
6260
};
6361

6462
} // namespace tns

test-app/runtime/src/main/cpp/Runtime.cpp

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
#include "FrameCallbacks.h"
2222
#include "Interop.h"
2323
#include "IsolateTracked.h"
24-
#include "IsolateDisposer.h"
2524
#include "JType.h"
2625
#include "JsArgConverter.h"
2726
#include "JsArgToArrayConverter.h"
@@ -1023,10 +1022,10 @@ void Runtime::DestroyRuntime() {
10231022
m_eventLoop->Shutdown();
10241023
}
10251024
if (m_napiEnv != nullptr) {
1026-
// After Shutdown so no queued Node-API entry can run against a dying env,
1027-
// and before disposeIsolate: the env's reference lists hold v8::Globals,
1028-
// so its teardown needs the isolate alive and locked. The Locker is
1029-
// reentrant for the worker path, which already holds it here.
1025+
// After Shutdown so no queued Node-API entry can run against a dying env.
1026+
// The env's reference lists hold v8::Globals, so its teardown needs the
1027+
// isolate alive and locked; the Locker is reentrant for the worker path,
1028+
// which already holds it here.
10301029
v8::Locker locker(m_isolate);
10311030
NapiEnv::Destroy(static_cast<NapiEnv*>(m_napiEnv));
10321031
m_napiEnv = nullptr;
@@ -1052,8 +1051,6 @@ void Runtime::DestroyRuntime() {
10521051
CallbackHandlers::RemoveIsolateEntries(m_isolate);
10531052
FrameCallbacks::RemoveIsolateEntries(m_isolate);
10541053

1055-
tns::disposeIsolate(m_isolate);
1056-
10571054
// V8 does not run weak callbacks when an isolate is disposed, so anything
10581055
// still bound to one has to be deleted explicitly, here, while the isolate
10591056
// is alive and its destructors can still touch v8::Global handles.

test-app/runtime/src/main/cpp/js/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,8 @@ module.exports = somethingTheCallSiteNeeds;
7474

7575
`primordials.js` runs first in every isolate — lazily, on the first
7676
`RunBuiltin` call, which happens during runtime init — and its frozen,
77-
null-prototype export is cached per isolate (`BuiltinLoader`, released from
78-
`disposeIsolate`) and handed to every other builtin, so a builtin that
77+
null-prototype export is cached per runtime (`BuiltinLoader`, in
78+
`RuntimeState`) and handed to every other builtin, so a builtin that
7979
compiles later in the isolate's life still sees intrinsics as they were before
8080
user code ran.
8181

0 commit comments

Comments
 (0)