Skip to content
Draft
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
8 changes: 7 additions & 1 deletion NativeScript/runtime/EventLoop.mm
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,16 @@
// a CFRunLoop callback frame. Deliberately no catch(...): on Darwin it would
// also swallow NSExceptions, and bare entries (which may @throw on purpose)
// never come through here anyway.
// The pool scopes autoreleased objects to the callout: worker threads have no
// UIKit observer draining once per run-loop pass, so without it they would
// accumulate until the worker dies. Bare entries stay unwrapped - an
// @throw must not unwind through a pool this code owns.
template <typename F>
void RunGuarded(F&& body) {
try {
body();
@autoreleasepool {
body();
}
} catch (tns::NativeScriptException& ex) {
Log(@"NativeScript: uncaught NativeScriptException in event loop task: %s",
ex.getMessage().c_str());
Expand Down
49 changes: 30 additions & 19 deletions NativeScript/runtime/WorkerWrapper.mm
Original file line number Diff line number Diff line change
Expand Up @@ -147,36 +147,47 @@ static void PostToRuntimeLoop(Runtime* runtime, std::function<void()> fn, bool a
runLoop,
[](void* info) {
WorkerWrapper* w = static_cast<WorkerWrapper*>(info);
w->DrainPendingTasks();
// Autoreleased objects die with the callout; a worker has no UIKit
// observer draining a pool per run-loop pass.
@autoreleasepool {
w->DrainPendingTasks();
}
},
this);

this->workerIsolate_ = func();
// Autoreleased objects created during the boot phase (isolate creation and
// entry-script evaluation run before the loop starts) otherwise accumulate
// until the worker dies - the backing NSOperation's pool is the only drain.
@autoreleasepool {
this->workerIsolate_ = func();

this->DrainPendingTasks();
this->DrainPendingTasks();
}

// check again as it could terminate before this
if (!this->isTerminating_) {
CFRunLoopRun();
}
}

// The inspector must be gone before the Runtime (and with it the isolate)
// is deleted below.
this->DestroyInspector();

this->isDisposed_ = true;
Runtime* runtime = Runtime::GetCurrentRuntime();
if (runtime != nullptr) {
delete runtime;
} else {
// Runtime was never created (worker terminated before initialization).
// The runtime destructor normally handles this cleanup, so do it here.
int workerId = this->workerId_;
bool found;
auto state = Caches::Workers->Get(workerId, found);
if (found) {
Caches::Workers->Remove(workerId);
@autoreleasepool { // teardown garbage drains before the thread is gone
// The inspector must be gone before the Runtime (and with it the isolate)
// is deleted below.
this->DestroyInspector();

this->isDisposed_ = true;
Runtime* runtime = Runtime::GetCurrentRuntime();
if (runtime != nullptr) {
delete runtime;
} else {
// Runtime was never created (worker terminated before initialization).
// The runtime destructor normally handles this cleanup, so do it here.
int workerId = this->workerId_;
bool found;
auto state = Caches::Workers->Get(workerId, found);
if (found) {
Caches::Workers->Remove(workerId);
}
}
}
}
Expand Down
4 changes: 4 additions & 0 deletions TestFixtures/Marshalling/TNSAllocLog.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
- (instancetype)init;
- (void)dealloc;

// Creates an instance whose only reference is in the current autorelease pool,
// so its dealloc log marks when that pool drains.
+ (void)autoreleaseInstance;

@end

#endif /* TNSAllocLog_h */
6 changes: 6 additions & 0 deletions TestFixtures/Marshalling/TNSAllocLog.m
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,10 @@ - (void)dealloc {
TNSLog(@"TNSAllocLog dealloc");
}

+ (void)autoreleaseInstance {
// CFBridgingRetain moves the instance's ownership out of ARC so the
// CFAutorelease'd reference in the current pool is the only one left.
CFAutorelease(CFBridgingRetain([[TNSAllocLog alloc] init]));
}

@end
11 changes: 11 additions & 0 deletions TestRunner/app/tests/WorkerAutoreleasePoolTests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
describe("worker autorelease pools", function () {
it("drains autoreleased objects per event-loop callout, not at worker death", function (done) {
var worker = new Worker("~/tests/autoreleasePoolDrainWorker.js");
worker.onmessage = function (e) {
worker.terminate();
expect(e.data).toContain("TNSAllocLog init");
expect(e.data).toContain("TNSAllocLog dealloc");
done();
};
});
});
11 changes: 11 additions & 0 deletions TestRunner/app/tests/autoreleasePoolDrainWorker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// The autorelease happens inside one timer callout, and the report is sent
// from the NEXT one: the dealloc log can only be present in between if the
// worker drains a pool per callout. Without that the pool only drains at
// worker death, after the report is sent.
TNSClearOutput();
setTimeout(function () {
TNSAllocLog.autoreleaseInstance();
setTimeout(function () {
postMessage(TNSGetOutput());
}, 0);
}, 0);
2 changes: 2 additions & 0 deletions TestRunner/app/tests/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,8 @@ require("./NapiCoverageTests");
// Worker-isolate scoping of extended objc class names
require("./ExtendedClassNamingTests");

require("./WorkerAutoreleasePoolTests");

// Tests common for all runtimes (git submodule of NativeScript/common-runtime-tests-app).
require("../shared/index").runAllTests();

Expand Down
Loading