diff --git a/packages/vm/src/engine/runtime.ts b/packages/vm/src/engine/runtime.ts index a15814b2..136b1508 100644 --- a/packages/vm/src/engine/runtime.ts +++ b/packages/vm/src/engine/runtime.ts @@ -10,7 +10,7 @@ import Sequencer from './sequencer'; import execute from './execute'; import ScratchBlocksConstants from './scratch-blocks-constants'; import TargetType from '../extension-support/target-type'; -import Thread from './thread'; +import Thread, {clearStackFrameFreeList} from './thread'; import log from '../util/log'; import maybeFormatMessage from '../util/maybe-format-message'; import StageLayering from './stage-layering'; @@ -2099,6 +2099,7 @@ class Runtime extends EventEmitter { }); this.targets.map(this.disposeTarget, this); + clearStackFrameFreeList(); this._monitorState = OrderedMap({}); this.emit(Runtime.RUNTIME_DISPOSED); this.ioDevices.clock.resetProjectTimer(); @@ -2203,6 +2204,7 @@ class Runtime extends EventEmitter { if (disposingTarget !== target) return true; // Allow target to do dispose actions. target.dispose(); + clearStackFrameFreeList(disposingTarget); // Remove from list of targets. return false; }); diff --git a/packages/vm/src/engine/thread.ts b/packages/vm/src/engine/thread.ts index 78542885..c4a8193d 100644 --- a/packages/vm/src/engine/thread.ts +++ b/packages/vm/src/engine/thread.ts @@ -41,10 +41,25 @@ class _StackFrame { * A context passed to block implementations. */ executionContext: unknown = null; + /** + * The target of blocks that this stack frame will execute. + * Stored as a weak reference to make GC happy. + */ + #target: WeakRef | null = null; + /** * The target of blocks that this thread will execute. + * + * Note that the stack frame only holds the weak reference to the target, + * so it may be null if the target has been garbage collected. */ - target: RenderedTarget | null = null; + get target (): RenderedTarget | null { + return this.#target?.deref() ?? null; + } + + set target (value: RenderedTarget | null) { + this.#target = value ? new WeakRef(value) : null; + } /** * @param warpMode Whether this level is in warp mode. Is set by some legacy blocks and @@ -412,6 +427,25 @@ class Thread { } } +/** + * Clear the stack frame recycle bin. + * @param target If provided, only remove frames referencing this target or whose target is no longer alive. + * If falsy, clear the entire free list. + */ +function clearStackFrameFreeList (target?: RenderedTarget) { + if (!target) { + _stackFrameFreeList.length = 0; + return; + } + for (let i = _stackFrameFreeList.length - 1; i >= 0; --i) { + const frameTarget = _stackFrameFreeList[i].target; + if (!frameTarget || frameTarget === target) { + _stackFrameFreeList.splice(i, 1); + } + } +} + export type {Thread, _StackFrame as ThreadStackFrame}; +export {clearStackFrameFreeList}; export default Thread;