From 7d7291cde4c716dfd2c890338983d71722de797a Mon Sep 17 00:00:00 2001 From: KernelDeimos Date: Mon, 15 Sep 2025 03:19:46 -0400 Subject: [PATCH] fix: add unlink() methods to clear ECMAP --- src/backend/src/filesystem/ECMAP.js | 29 +++++++++++++++++++++++++++-- src/backend/src/util/context.js | 19 +++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/src/backend/src/filesystem/ECMAP.js b/src/backend/src/filesystem/ECMAP.js index 466cadcf..8edb871c 100644 --- a/src/backend/src/filesystem/ECMAP.js +++ b/src/backend/src/filesystem/ECMAP.js @@ -26,6 +26,21 @@ class ECMAP { // identifier association caches this.path_to_uuid = {}; this.uuid_to_path = {}; + + this.unlinked = false; + } + + /** + * unlink() clears all references from this ECMAP to ensure that it will be + * GC'd. This is called by ECMAP.arun() after the callback has resolved. + */ + unlink () { + this.unlink = true; + this.uuid_to_fsNodeContext = null; + this.path_to_fsNodeContext = null; + this.id_to_fsNodeContext = null; + this.path_to_uuid = null; + this.uuid_to_path = null; } get logPrefix () { @@ -38,6 +53,8 @@ class ECMAP { } get_fsNodeContext_from_selector (selector) { + if ( this.unlinked ) return null; + this.log('GET', selector.describe()); const retvalue = (() => { let value; @@ -69,6 +86,8 @@ class ECMAP { } store_fsNodeContext_to_selector (selector, node) { + if ( this.unlinked ) return null; + this.log('STORE', selector.describe()); if ( selector instanceof NodeUIDSelector ) { this.uuid_to_fsNodeContext[selector.value] = node; @@ -82,16 +101,22 @@ class ECMAP { } store_fsNodeContext (node) { + if ( this.unlinked ) return; + this.store_fsNodeContext_to_selector(node.selector, node); } static async arun (cb) { let context = Context.get(); if ( ! context.get(this.SYMBOL) ) { + const ins = new this(); context = context.sub({ - [this.SYMBOL]: new this(), + [this.SYMBOL]: ins, }); - return await context.arun(cb); + const result = await context.arun(cb); + ins.unlink(); + context.unlink(); + return result; } return await cb(); } diff --git a/src/backend/src/util/context.js b/src/backend/src/util/context.js index bbbfcd87..733448bc 100644 --- a/src/backend/src/util/context.js +++ b/src/backend/src/util/context.js @@ -71,10 +71,29 @@ class Context { static sub (values, opt_name) { return this.get().sub(values, opt_name); } + + #dead = false; + + /** + * Clears this context's values and unlinks from its parent. This context + * will become empty. This is to ensure contexts that aren't used anymore + * get garbage collected. This was added to prevent memory leaks due to + * ECMAP, where currently we're not sure what's holding a reference back + * to the ECMAP (or perhaps its subcontext). + */ + unlink () { + // Settings `values_` to an empty object should clear any references + // that were inside it while avoiding errors if .get() happens to be + // called by a lingering asynchronous function. + this.values_ = {}; + this.#dead = true; + } + get (k) { return this.values_[k]; } set (k, v) { + if ( this.#dead ) return; this.values_[k] = v; } sub (values, opt_name) {