diff --git a/api/node/__test__/api.spec.mts b/api/node/__test__/api.spec.mts index 628893e9f83..86ea57995a6 100644 --- a/api/node/__test__/api.spec.mts +++ b/api/node/__test__/api.spec.mts @@ -4,6 +4,11 @@ import test from 'ava' import * as path from 'node:path'; import { fileURLToPath } from 'url'; +import { setFlagsFromString } from 'v8'; +import { runInNewContext } from 'vm'; + +setFlagsFromString('--expose_gc'); +const gc = runInNewContext('gc'); import { loadFile, loadSource, CompileError } from '../index.js' @@ -159,3 +164,50 @@ test('loadSource component instances and modules are sealed', (t) => { test.no_such_callback = () => { }; }, { instanceOf: TypeError }); }) + +test('callback closure cyclic references do not prevent GC', async (t) => { + + // Setup: + // A component instance with a callback installed from JS: + // The callback captures the surrounding environment, which + // includes an extra reference to the component instance itself + // --> a cyclic reference + // + // Note: WeakRef's deref is used to observe the GC. This means that we must + // separate the test into different jobs with await, to permit for collection. + // (See https://tc39.es/ecma262/multipage/managing-memory.html#sec-weak-ref.prototype.deref) + + let demo_module = loadFile(path.join(dirname, "resources/test-gc.slint")) as any; + let demo = new demo_module.Test(); + t.is(demo.check, "initial value"); + t.true(Object.hasOwn(demo, "say_hello")); + + let demo_weak = new WeakRef(demo); + + function scope() { + let copy = demo; + copy.say_hello = () => { + console.log(copy.check); + }; + } + scope(); + + t.true(demo_weak.deref() !== undefined); + + // After the first GC, the instance should not have been collected because the + // current environment's demo variable is a strong reference. + await new Promise(resolve => setTimeout(resolve, 0)); + gc(); + + t.true(demo_weak.deref() !== undefined); + + // Clear the strong reference here + demo = null; + + // After the this GC call, the instance should have been collected. Strong references + // in Rust should not keep it alive. + await new Promise(resolve => setTimeout(resolve, 0)); + gc(); + + t.is(demo_weak.deref(), undefined, "The demo instance should have been collected and the weak ref should deref to undefined"); +}) diff --git a/api/node/__test__/resources/test-gc.slint b/api/node/__test__/resources/test-gc.slint new file mode 100644 index 00000000000..db2ce3a28c0 --- /dev/null +++ b/api/node/__test__/resources/test-gc.slint @@ -0,0 +1,10 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial + + +export component Test { + callback say_hello(); + in-out property check: "initial value"; +}