Skip to content
Open
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
16 changes: 16 additions & 0 deletions tests/standalone/test-runner/cpp/TestRuntime.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,22 @@ namespace {
rt.instrumentation().collectGarbage("Nitro runtime test");
return jsi::Value::undefined();
}));
// JSI may destroy a native function's state on any thread, e.g. Hermes' concurrent GC thread.
// This takes that state out of the function and destroys it on a new thread right away. On
// Android, that thread is not attached to the JVM, like Hermes' GC thread.
runtime->global().setProperty(
*runtime, "destroyOnBackgroundThread",
jsi::Function::createFromHostFunction(*runtime, jsi::PropNameID::forAscii(*runtime, "destroyOnBackgroundThread"), 1,
[](jsi::Runtime& rt, const jsi::Value&, const jsi::Value* args, size_t count) {
if (count != 1 || !args[0].isObject() || !args[0].getObject(rt).isFunction(rt))
throw jsi::JSError(rt, "destroyOnBackgroundThread(...) expects a function");
jsi::Function function = args[0].getObject(rt).getFunction(rt);
if (!function.isHostFunction(rt))
throw jsi::JSError(rt, "destroyOnBackgroundThread(...) expects a native function");
jsi::HostFunctionType state = std::move(function.getHostFunction(rt));
std::thread([state = std::move(state)]() mutable { state = nullptr; }).join();
return jsi::Value::undefined();
}));
phase = "loading JS test bundle";
const auto timeout = std::chrono::milliseconds(timeoutMs);
const auto timeoutMessage = "Timed out after " + std::to_string(timeoutMs) + "ms";
Expand Down
35 changes: 35 additions & 0 deletions tests/standalone/test-runner/js/memory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,43 @@ function getHermesExternalMemorySize(): number {
return hermesInternal.getInstrumentedStats().js_externalBytes
}

/**
* Creates a pending JS Promise that keeps the native `then(onResolved, onRejected)` listeners Nitro
* adds to it, instead of handing them to the JS engine, so a test can destroy their native state.
*/
function createListenedPromise(): { promise: Promise<number>; listeners: ((value: unknown) => void)[] } {
const listeners: ((value: unknown) => void)[] = []
const promise = new Promise<number>(() => {})
Object.defineProperty(promise, 'then', {
value: (onResolved: (value: unknown) => void, onRejected: (value: unknown) => void) => {
listeners.push(onResolved, onRejected)
},
})
return { promise, listeners }
}

export const cases = {
...casesForTestObjects('Memory', {
'awaitAndGetPromise(...) listeners can be destroyed on another thread while pending': async (testObject) => {
const { promise, listeners } = createListenedPromise()
// Destroying the listeners rejects the native Promise if nothing else holds it.
testObject.awaitAndGetPromise(promise).catch(() => {})
expect(listeners).to.have.length(2)

for (const listener of listeners) destroyOnBackgroundThread(listener)
await yieldToEventLoop()
},

'awaitAndGetPromise(...) listeners can be destroyed on another thread after resolving': async (testObject) => {
const { promise, listeners } = createListenedPromise()
const result = testObject.awaitAndGetPromise(promise)
expect(listeners).to.have.length(2)

listeners[0]?.(42)
expect(await result).to.equal(42)
for (const listener of listeners) destroyOnBackgroundThread(listener)
},

'createNativeCallback(...) callbacks still work after gc()': async (testObject) => {
const onCalled = recordCallback<(num: number) => void>()
const callbacks: ((num: number) => void)[] = []
Expand Down
2 changes: 2 additions & 0 deletions tests/standalone/test-runner/js/utils/testObjects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ export type TestObject = TestObjectCpp | TestObjectSwiftKotlin
declare global {
var __nitroTestPlatform: 'apple' | 'android'
function gc(): void
/** Destroys a native function's state on a new native thread, like Hermes' concurrent GC can. */
function destroyOnBackgroundThread(nativeFunction: (...args: never[]) => unknown): void
}

export const HybridChild = new TestObjectSwiftKotlin().createChild()
Expand Down
Loading