Skip to content

[perf] Append-only routesList leaks route entries (and their captured module graphs) across module reloads #8

Description

@MrSociety404

Problem

RegisterRoute pushes every registered route into the module-level array routesList (declared line 651), and nothing anywhere ever removes an entry — the only operations on it are .push (line 671) and .map (line 689 in the public getRegisteredRoutes). This is out of step with the rest of the registration lifecycle: the framework's RegisteringProxy tracks registrations per module and prunes them on unload via unregisterModule (interface-core proxies.js:152), and the API implementation's registeredRoutes Map is deleted on unregister (implementations/api/index.js:52-54). AntelopeJS is a dynamic/hot-reload modular framework, so whenever a controller-defining module is reloaded or unloaded, its decorators re-run RegisterRoute with fresh incrementing ids, appending brand-new entries to routesList while the old ones are never released.

Location: src/index.ts:671
Severity: 🟢 low
Category: unbounded-growth

Performance impact

Each stale entry is a shallow copy of a RouteHandler that references the controller callback, its proto (prototype), and the computed parameters/properties closures, so every leaked entry pins the entire previous version of that module's controller graph in memory, preventing GC. In long-running dev sessions (frequent hot reloads) or any production setup that dynamically swaps modules, routesList and retained memory grow without bound, and getRegisteredRoutes() returns an ever-growing array of duplicated/stale routes while allocating a larger array on every call.

Suggested fix

Eliminate the parallel routesList and derive getRegisteredRoutes() directly from the already-pruned routesProxy.registered map, which is the single source of truth. That map stores { module, args } keyed by the string id, and RegisterRoute passes the enriched handler as the sole arg (routesProxy.register(id.toString(), enriched)), so getRegisteredRoutes can iterate routesProxy.registered and, for each [id, { args: [handler] }], emit { id, location, method, callbackName: handler.callback.name || 'anonymous', parameters, properties, priority, module }. This makes stale entries disappear automatically on unregister/unregisterModule and removes the growing-array allocation. If routesList must be retained for its numeric ids, instead wire routesProxy.onUnregister((id) => routesList.delete(id)) (switching routesList to a Map keyed by the same string id) so entries are removed on the same lifecycle event that already cleans up the real routes.

Verification notes

Confirmed in src/index.ts: routesList (line 651) is only ever pushed to (671) and mapped over (689) — no removal path — whereas its sibling routesProxy (a RegisteringProxy) is properly pruned on teardown, since interface-core's ModuleDestroyed handler calls proxy.unregisterModule (proxies.js:167) which the api implementation uses to delete from registeredRoutes and drop the real handler (implementations/api/index.js:52-54). Because ReloadModule re-runs the controller decorators (ProcessCallback line 731 -> RegisterRoute with a fresh nextId), every reload appends new RouteHandler shallow copies that retain callback/proto/parameters, pinning the prior module version's controller graph while old entries are never freed, and getRegisteredRoutes() returns an ever-growing duplicated/stale array. It's a real, correctly-diagnosed unbounded leak, but calibrated low because it only accrues on the reload/unload path (not the request hot path), the per-reload footprint is small, and the only observed consumer is a debug utility.


Found by an automated AI performance review (multi-agent analysis + independent adversarial verification of each finding).

Metadata

Metadata

Assignees

No one assigned

    Labels

    performancePerformance improvement

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions