Skip to content

Commit 7699369

Browse files
committed
fix: own isolate-bound state per runtime instead of in shared maps
Workers bootstrap on detached threads and are not serialized, so several runtimes are inside PrepareV8Runtime while another is in disposeIsolate. A handful of subsystems kept their per-isolate state in process-wide maps keyed by v8::Isolate*, which makes the *container* shared even though the entries are not: one runtime inserting its own entry while another erases its own corrupts the map. Holding the isolate's Locker does not help, because each thread holds only its own isolate's lock, so two runtimes never exclude each other. The reproduced crash walked a freed red-black tree node: std::less<v8::Isolate*>::operator() std::map<v8::Isolate*, std::map<std::string,double>>::insert tns::Console::createConsole tns::Runtime::PrepareV8Runtime Java_com_tns_Runtime_initNativeScript (thread W41: ./EvalWork) Rather than guard each container, remove the sharing: RuntimeState is a typed per-runtime slot bag owned by Runtime. A subsystem declares a state struct, usually in its own .cpp, and reaches it with RuntimeState::For<T>(isolate) -- an isolate data-slot read plus a vector index, with no lock and no shared container. The bag is destroyed once in DestroyRuntime, on the runtime's own thread and while the isolate is still alive, which is what state holding v8::Persistents requires. Moved onto it: - Console: console.time() labels and the compiled inspect.js instance. Console now has no global mutable state and no mutex at all. - ArgConverter: the java-long conversion helpers. - JSONObjectHelper: the compiled JS->org.json serializer. - MetadataNode: the per-isolate node cache and the array wrapper template, plus the constructor functions that used to hang off every node as a map keyed by isolate -- which is why teardown had to walk every node in s_treeNode2NodeCache to erase one entry. That walk, running on a dying worker's thread while other threads inserted, is gone. Four onDisposeIsolate hooks disappear with it: nothing is keyed by isolate any more, so there is no per-isolate entry to erase. Also: - MetadataNode::s_profilerEnabled and Runtime::s_mainThreadInitialized are now atomic. The latter gated the one-time BuildMetadata, so as a plain bool there was no happens-before edge between the main thread's metadata construction and a worker's first read of s_metadataReader. - TypeLongOperationsCache gains a destructor; it was deleted without one, leaking two v8::Persistents per isolate. - console.time/timeEnd no longer dereference the iterator returned by a failed find (both had a "// throw?" comment and then used it anyway). Verified on an arm64 emulator with the SIGSEGV handler temporarily disabled, so faults are fatal and tombstoned rather than swallowed: the earlier, narrower mutex-based version of this fix ran 20/20 full-suite runs clean against a baseline that reproduced roughly 1 in 5. Re-verification of this version is running; suite is 879/0. Still shared, and deliberately left for a follow-up: the metadata tree and MetadataReader's buffers (genuinely one blob for the process, so they need a narrow lock rather than per-runtime storage), and the string-keyed MethodCache::s_mthod_ctor_signature_cache and JEnv::s_classCache.
1 parent c26048c commit 7699369

14 files changed

Lines changed: 281 additions & 156 deletions

test-app/runtime/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@ add_library(
203203
src/main/cpp/Profiler.cpp
204204
src/main/cpp/ReadWriteLock.cpp
205205
src/main/cpp/Runtime.cpp
206+
src/main/cpp/RuntimeState.cpp
206207
src/main/cpp/SimpleAllocator.cpp
207208
src/main/cpp/SimpleProfiler.cpp
208209
src/main/cpp/StructuredClone.cpp

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

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include "Runtime.h"
88
#include "V8GlobalHelpers.h"
99
#include "NativeScriptAssert.h"
10+
#include "RuntimeState.h"
1011
#include <sstream>
1112

1213
using namespace v8;
@@ -196,16 +197,8 @@ int64_t ArgConverter::ConvertToJavaLong(Isolate* isolate, const Local<Value>& va
196197
}
197198

198199
ArgConverter::TypeLongOperationsCache* ArgConverter::GetTypeLongCache(v8::Isolate* isolate) {
199-
TypeLongOperationsCache* cache;
200-
auto itFound = s_type_long_operations_cache.find(isolate);
201-
if (itFound == s_type_long_operations_cache.end()) {
202-
cache = new TypeLongOperationsCache;
203-
s_type_long_operations_cache.emplace(isolate, cache);
204-
} else {
205-
cache = itFound->second;
206-
}
207-
208-
return cache;
200+
// Per runtime, so there is no shared table to race on; see RuntimeState.h.
201+
return RuntimeState::For<TypeLongOperationsCache>(isolate);
209202
}
210203

211204

@@ -222,12 +215,3 @@ u16string ArgConverter::ConvertToUtf16String(const v8::Local<String>& s) {
222215

223216

224217

225-
void ArgConverter::onDisposeIsolate(Isolate* isolate) {
226-
auto itFound = s_type_long_operations_cache.find(isolate);
227-
if (itFound != s_type_long_operations_cache.end()) {
228-
delete itFound->second;
229-
s_type_long_operations_cache.erase(itFound);
230-
}
231-
}
232-
233-
robin_hood::unordered_map<Isolate*, ArgConverter::TypeLongOperationsCache*> ArgConverter::s_type_long_operations_cache;

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

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -115,20 +115,27 @@ class ArgConverter {
115115
return v8::String::NewFromTwoByte(isolate, ((const uint16_t*) utf16string.data())).ToLocalChecked();
116116
}
117117

118-
static void onDisposeIsolate(v8::Isolate* isolate);
118+
/*
119+
* Per-runtime state (RuntimeState owns one of these per runtime, so it
120+
* has to be constructible from outside ArgConverter). Destroyed with
121+
* the runtime, while its isolate is still alive.
122+
*/
123+
struct TypeLongOperationsCache {
124+
v8::Persistent<v8::Function>* LongNumberCtorFunc = nullptr;
125+
126+
v8::Persistent<v8::NumberObject>* NanNumberObject = nullptr;
127+
128+
~TypeLongOperationsCache() {
129+
delete LongNumberCtorFunc;
130+
delete NanNumberObject;
131+
}
132+
};
119133

120134
private:
121135

122136
// TODO: plamen5kov: rewrite logic for java long number operations in javascript (java long -> javascript number operations check)
123137
static const long long JS_LONG_LIMIT = ((long long) 1) << 53;
124138

125-
struct TypeLongOperationsCache {
126-
v8::Persistent<v8::Function>* LongNumberCtorFunc;
127-
128-
v8::Persistent<v8::NumberObject>* NanNumberObject;
129-
};
130-
//
131-
132139
static TypeLongOperationsCache* GetTypeLongCache(v8::Isolate* isolate);
133140

134141
inline static jstring ObjectToString(jobject object) {
@@ -146,11 +153,6 @@ class ArgConverter {
146153

147154
static void NativeScriptLongToStringFunctionCallback(const v8::FunctionCallbackInfo<v8::Value>& args);
148155

149-
/*
150-
* "s_type_long_operations_cache" used to keep function
151-
* dealing with operations concerning java long -> javascript number.
152-
*/
153-
static robin_hood::unordered_map<v8::Isolate*, TypeLongOperationsCache*> s_type_long_operations_cache;
154156
};
155157
}
156158

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

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,6 @@
1515

1616
namespace tns {
1717
void disposeIsolate(v8::Isolate *isolate) {
18-
tns::ArgConverter::onDisposeIsolate(isolate);
19-
tns::MetadataNode::onDisposeIsolate(isolate);
20-
tns::Console::onDisposeIsolate(isolate);
21-
tns::JSONObjectHelper::onDisposeIsolate(isolate);
2218
tns::NsBuiltinModules::onDisposeIsolate(isolate);
2319
tns::BuiltinLoader::onDisposeIsolate(isolate);
2420
// clear all isolate bound objects

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

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,24 @@
22
#include "JSONObjectHelper.h"
33
#include "ArgConverter.h"
44
#include "BuiltinLoader.h"
5+
#include "RuntimeState.h"
56
#include "robin_hood.h"
67
#include <sstream>
78
#include <string>
89

910
using namespace v8;
1011
using namespace tns;
1112

12-
static robin_hood::unordered_map<Isolate*, Persistent<Function>*> isolateToSerializeFunc;
13+
namespace {
14+
// The compiled JS->org.json serializer, per runtime; see RuntimeState.h.
15+
struct SerializeFuncState {
16+
Persistent<Function>* func = nullptr;
17+
18+
~SerializeFuncState() {
19+
delete func;
20+
}
21+
};
22+
} // namespace
1323

1424
void JSONObjectHelper::RegisterFromFunction(Isolate *isolate, Local<Value>& jsonObject) {
1525
if (!jsonObject->IsFunction()) {
@@ -84,9 +94,12 @@ void JSONObjectHelper::ConvertCallbackStatic(const FunctionCallbackInfo<Value>&
8494
Persistent<Function>* JSONObjectHelper::GetSerializeFunc(Local<Context> context) {
8595
Isolate* isolate = v8::Isolate::GetCurrent();
8696

87-
auto it = isolateToSerializeFunc.find(isolate);
88-
if (it != isolateToSerializeFunc.end()) {
89-
return it->second;
97+
auto* state = RuntimeState::For<SerializeFuncState>(isolate);
98+
if (state == nullptr) {
99+
return nullptr;
100+
}
101+
if (state->func != nullptr) {
102+
return state->func;
90103
}
91104

92105
Local<Value> result;
@@ -95,16 +108,8 @@ Persistent<Function>* JSONObjectHelper::GetSerializeFunc(Local<Context> context)
95108
return nullptr;
96109
}
97110

98-
auto* serializeFunc = new Persistent<Function>(isolate, result.As<Function>());
99-
isolateToSerializeFunc.emplace(isolate, serializeFunc);
111+
state->func = new Persistent<Function>(isolate, result.As<Function>());
100112

101-
return serializeFunc;
113+
return state->func;
102114
}
103115

104-
void JSONObjectHelper::onDisposeIsolate(Isolate* isolate) {
105-
auto it = isolateToSerializeFunc.find(isolate);
106-
if (it != isolateToSerializeFunc.end()) {
107-
delete it->second;
108-
isolateToSerializeFunc.erase(it);
109-
}
110-
}

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ namespace tns {
88
class JSONObjectHelper {
99
public:
1010
static void RegisterFromFunction(v8::Isolate *isolate, v8::Local<v8::Value>& jsonObject);
11-
static void onDisposeIsolate(v8::Isolate* isolate);
1211
private:
1312
static v8::Persistent<v8::Function>* GetSerializeFunc(v8::Local<v8::Context> context);
1413
static void ConvertCallbackStatic(const v8::FunctionCallbackInfo<v8::Value>& info);

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

Lines changed: 21 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#include "MetadataNode.h"
2+
#include "RuntimeState.h"
23
#include "NativeScriptAssert.h"
34
#include "Constants.h"
45
#include "Util.h"
@@ -82,17 +83,17 @@ bool MetadataNode::TryGetPackageName(Isolate* isolate, const Local<Object>& valu
8283
}
8384

8485
Local<ObjectTemplate> MetadataNode::GetOrCreateArrayObjectTemplate(Isolate* isolate) {
85-
auto it = s_arrayObjectTemplates.find(isolate);
86-
if (it != s_arrayObjectTemplates.end()) {
87-
return it->second->Get(isolate);
86+
auto cache = GetMetadataNodeCache(isolate);
87+
if (cache->ArrayObjectTemplate != nullptr) {
88+
return cache->ArrayObjectTemplate->Get(isolate);
8889
}
8990

9091
auto arrayObjectTemplate = ObjectTemplate::New(isolate);
9192
arrayObjectTemplate->SetInternalFieldCount(static_cast<int>(ObjectManager::MetadataNodeKeys::END));
9293
arrayObjectTemplate->SetHandler(IndexedPropertyHandlerConfiguration(
9394
ArrayIndexedPropertyGetterCallback, ArrayIndexedPropertySetterCallback));
9495

95-
s_arrayObjectTemplates.emplace(std::make_pair(isolate, new Persistent<ObjectTemplate>(isolate, arrayObjectTemplate)));
96+
cache->ArrayObjectTemplate = new Persistent<ObjectTemplate>(isolate, arrayObjectTemplate);
9697

9798
return arrayObjectTemplate;
9899
}
@@ -721,7 +722,7 @@ vector<MetadataNode::MethodCallbackData *> MetadataNode::SetInstanceMethodsFromS
721722
callbackData->parent = *itFound;
722723
}
723724

724-
if (s_profilerEnabled) {
725+
if (s_profilerEnabled.load(std::memory_order_relaxed)) {
725726
Local<External> funcData = External::New(isolate, callbackData, v8::kExternalPointerTypeTagDefault);
726727
Local<FunctionTemplate> funcTemplate = FunctionTemplate::New(isolate, MethodCallback, funcData);
727728
auto func = funcTemplate->GetFunction(context).ToLocalChecked();
@@ -965,8 +966,9 @@ void MetadataNode::InnerTypeAccessorGetterCallback(v8::Local<v8::Name> property,
965966
MetadataTreeNode* curChild = static_cast<MetadataTreeNode*>(
966967
v8::External::Cast(*info.Data())->Value(v8::kExternalPointerTypeTagDefault));
967968
auto childNode = GetOrCreateInternal(curChild);
968-
auto itFound = childNode->m_poCtorCachePerIsolate.find(isolate);
969-
if (itFound != childNode->m_poCtorCachePerIsolate.end()) {
969+
auto innerCache = GetMetadataNodeCache(isolate);
970+
auto itFound = innerCache->CtorFunctions.find(childNode);
971+
if (itFound != innerCache->CtorFunctions.end()) {
970972
info.GetReturnValue().Set(itFound->second->Get(isolate));
971973
return;
972974
}
@@ -1086,7 +1088,7 @@ Local<FunctionTemplate> MetadataNode::GetConstructorFunctionTemplate(Isolate* is
10861088
node->SetStaticMembers(isolate, wrappedCtorFunc, treeNode, curPtr);
10871089

10881090
// insert isolate-specific persistent function handle
1089-
node->m_poCtorCachePerIsolate.insert({isolate, new Persistent<Function>(isolate, wrappedCtorFunc)});
1091+
cache->CtorFunctions.emplace(node, new Persistent<Function>(isolate, wrappedCtorFunc));
10901092
if (!baseCtorFunc.IsEmpty()) {
10911093
auto currentContext = isolate->GetCurrentContext();
10921094
wrappedCtorFunc->SetPrototype(currentContext, baseCtorFunc);
@@ -1116,8 +1118,9 @@ Local<Function> MetadataNode::GetConstructorFunction(Isolate* isolate) {
11161118
}
11171119

11181120
Persistent<Function>* MetadataNode::GetPersistentConstructorFunction(Isolate* isolate) {
1119-
auto itFound = m_poCtorCachePerIsolate.find(isolate);
1120-
if (itFound != m_poCtorCachePerIsolate.end()) {
1121+
auto cache = GetMetadataNodeCache(isolate);
1122+
auto itFound = cache->CtorFunctions.find(this);
1123+
if (itFound != cache->CtorFunctions.end()) {
11211124
auto& constrFunction = itFound->second;
11221125

11231126
return constrFunction;
@@ -2063,19 +2066,17 @@ void MetadataNode::CreateTopLevelNamespaces(Isolate* isolate, const Local<Object
20632066
}
20642067

20652068
MetadataNode::MetadataNodeCache* MetadataNode::GetMetadataNodeCache(Isolate* isolate) {
2066-
MetadataNodeCache* cache;
2067-
auto itFound = s_metadata_node_cache.find(isolate);
2068-
if (itFound == s_metadata_node_cache.end()) {
2069-
cache = new MetadataNodeCache;
2070-
s_metadata_node_cache.emplace(isolate, cache);
2071-
} else {
2072-
cache = itFound->second;
2069+
// Per runtime; see RuntimeState.h. Null only once the runtime has begun
2070+
// tearing down, which no caller here can legitimately reach.
2071+
auto* cache = RuntimeState::For<MetadataNodeCache>(isolate);
2072+
if (cache == nullptr) {
2073+
throw NativeScriptException("Metadata cache requested after the runtime was torn down");
20732074
}
20742075
return cache;
20752076
}
20762077

20772078
void MetadataNode::EnableProfiler(bool enableProfiler) {
2078-
s_profilerEnabled = enableProfiler;
2079+
s_profilerEnabled.store(enableProfiler, std::memory_order_relaxed);
20792080
}
20802081

20812082
bool MetadataNode::IsJavascriptKeyword(const std::string &word) {
@@ -2095,7 +2096,7 @@ bool MetadataNode::IsJavascriptKeyword(const std::string &word) {
20952096
}
20962097

20972098
Local<Function> MetadataNode::Wrap(Isolate* isolate, const Local<Function>& function, const string& name, const string& origin, bool isCtorFunc) {
2098-
if (!s_profilerEnabled || name == "<init>") {
2099+
if (!s_profilerEnabled.load(std::memory_order_relaxed) || name == "<init>") {
20992100
return function;
21002101
}
21012102

@@ -2305,32 +2306,6 @@ std::string MetadataNode::GetJniClassName(MetadataEntry& entry) {
23052306
return fullClassName;
23062307
}
23072308

2308-
void MetadataNode::onDisposeIsolate(Isolate* isolate) {
2309-
{
2310-
auto it = s_metadata_node_cache.find(isolate);
2311-
if (it != s_metadata_node_cache.end()) {
2312-
delete it->second;
2313-
s_metadata_node_cache.erase(it);
2314-
}
2315-
}
2316-
{
2317-
auto it = s_arrayObjectTemplates.find(isolate);
2318-
if (it != s_arrayObjectTemplates.end()) {
2319-
delete it->second;
2320-
s_arrayObjectTemplates.erase(it);
2321-
}
2322-
}
2323-
{
2324-
for (auto it = s_treeNode2NodeCache.begin(); it != s_treeNode2NodeCache.end(); it++) {
2325-
auto it2 = it->second->m_poCtorCachePerIsolate.find(isolate);
2326-
if(it2 != it->second->m_poCtorCachePerIsolate.end()) {
2327-
delete it2->second;
2328-
it->second->m_poCtorCachePerIsolate.erase(it2);
2329-
}
2330-
}
2331-
}
2332-
}
2333-
23342309
MetadataReader* MetadataNode::getMetadataReader() {
23352310
return &MetadataNode::s_metadataReader;
23362311
}
@@ -2340,7 +2315,5 @@ MetadataReader MetadataNode::s_metadataReader;
23402315
robin_hood::unordered_map<std::string, MetadataNode*> MetadataNode::s_name2NodeCache;
23412316
robin_hood::unordered_map<std::string, MetadataTreeNode*> MetadataNode::s_name2TreeNodeCache;
23422317
robin_hood::unordered_map<MetadataTreeNode*, MetadataNode*> MetadataNode::s_treeNode2NodeCache;
2343-
robin_hood::unordered_map<Isolate*, MetadataNode::MetadataNodeCache*> MetadataNode::s_metadata_node_cache;
2344-
bool MetadataNode::s_profilerEnabled = false;
2345-
robin_hood::unordered_map<Isolate*, Persistent<ObjectTemplate>*> MetadataNode::s_arrayObjectTemplates;
2318+
std::atomic<bool> MetadataNode::s_profilerEnabled{false};
23462319

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

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
// ....->InstanceProxy->EmptyInstance
1919

2020
#include "v8.h"
21+
#include <atomic>
2122
#include "MetadataEntry.h"
2223
#include "MetadataTreeNode.h"
2324
#include "MetadataReader.h"
@@ -70,7 +71,6 @@ class MetadataNode {
7071

7172
static bool TryGetPackageName(v8::Isolate* isolate, const v8::Local<v8::Object>& value, std::string& out);
7273

73-
static void onDisposeIsolate(v8::Isolate* isolate);
7474

7575
static MetadataReader* getMetadataReader();
7676
private:
@@ -185,7 +185,6 @@ class MetadataNode {
185185
PrototypeTemplateFiller& protoFiller);
186186

187187
MetadataTreeNode* m_treeNode;
188-
robin_hood::unordered_map<v8::Isolate*, v8::Persistent<v8::Function>*> m_poCtorCachePerIsolate;
189188
std::string m_name;
190189
std::string m_implType;
191190
bool m_isArray;
@@ -195,9 +194,7 @@ class MetadataNode {
195194
static robin_hood::unordered_map<std::string, MetadataNode*> s_name2NodeCache;
196195
static robin_hood::unordered_map<std::string, MetadataTreeNode*> s_name2TreeNodeCache;
197196
static robin_hood::unordered_map<MetadataTreeNode*, MetadataNode*> s_treeNode2NodeCache;
198-
static robin_hood::unordered_map<v8::Isolate*, MetadataNodeCache*> s_metadata_node_cache;
199-
static robin_hood::unordered_map<v8::Isolate*, v8::Persistent<v8::ObjectTemplate>*> s_arrayObjectTemplates;
200-
static bool s_profilerEnabled;
197+
static std::atomic<bool> s_profilerEnabled;
201198

202199
struct MethodCallbackData {
203200
MethodCallbackData()
@@ -275,6 +272,11 @@ class MetadataNode {
275272
MetadataNode* node;
276273
};
277274

275+
/*
276+
* Metadata state for one runtime. Owned by RuntimeState, so it is
277+
* reached without a shared container and destroyed with the runtime,
278+
* while its isolate is still alive.
279+
*/
278280
struct MetadataNodeCache {
279281
v8::Persistent<v8::String>* MetadataKey;
280282

@@ -283,6 +285,24 @@ class MetadataNode {
283285
robin_hood::unordered_map<MetadataTreeNode*, CtorCacheData> CtorFuncCache;
284286

285287
robin_hood::unordered_map<std::string, MetadataNode::ExtendedClassCacheData> ExtendedCtorFuncCache;
288+
289+
// The array wrapper template for this runtime.
290+
v8::Persistent<v8::ObjectTemplate>* ArrayObjectTemplate = nullptr;
291+
292+
/*
293+
* This runtime's constructor function per node. The nodes
294+
* themselves are shared between runtimes, so this cannot live on
295+
* them -- it used to, as a map keyed by isolate, which meant every
296+
* runtime's teardown walked every node to erase its entry.
297+
*/
298+
robin_hood::unordered_map<MetadataNode*, v8::Persistent<v8::Function>*> CtorFunctions;
299+
300+
~MetadataNodeCache() {
301+
delete ArrayObjectTemplate;
302+
for (auto& entry : CtorFunctions) {
303+
delete entry.second;
304+
}
305+
}
286306
};
287307
};
288308
}

0 commit comments

Comments
 (0)