Files
discourse/package.json
T
Sérgio Saquetim 8160411cd2 PERF: Prevent unbounded growth of store identity map (#37163)
Fixes a memory leak reported by @small-lovely-cat in
https://meta.discourse.org/t/perf-there-seems-to-be-a-memory-leakage-in-frontend-store/393510/1

## Problem

The store's identity map holds strong references to every model instance
ever fetched during a session. These references are never released,
causing memory to grow unbounded as users navigate through the
application.

Models for topics, posts, categories, and other resources accumulate
indefinitely, even after the user has navigated away and the app no
longer needs them.

## Why not FIFO/LRU?

Traditional cache eviction strategies don't work well for an identity
map. If we evict an entry while the app still holds a reference to that
model (e.g., it's rendered in a component), the object remains in memory
anyway. Worse, the next fetch for that ID creates a *new*
instance—consuming more memory and breaking identity semantics where the
same ID should always return the same object.

## Solution

Introduce `WeakValueMap`, a Map that holds weak references to its
values. When a model instance is no longer referenced anywhere else in
the application, it becomes eligible for garbage collection, and the
identity map entry is automatically cleaned up.

**Key implementation details:**
- `WeakRef` wraps cached values, allowing GC when unreferenced
- `FinalizationRegistry` removes stale map entries after values are
collected
- Probabilistic sweep (~1% of `get()` calls) as a backup cleanup
mechanism

This makes the identity map self-regulating—it naturally shrinks when
models are no longer in use, while still preserving identity semantics
for active models.
2026-01-19 18:14:54 -03:00

87 lines
4.3 KiB
JSON

{
"private": true,
"repository": "https://github.com/discourse/discourse",
"author": "Discourse",
"license": "GPL-2.0-only",
"devDependencies": {
"@babel/plugin-proposal-decorators": "^7.28.0",
"@discourse/lint-configs": "^2.37.1",
"@discourse/moment-timezone-names-translations": "^1.0.0",
"@fortawesome/fontawesome-free": "7.1.0",
"@glint/ember-tsc": "^1.0.8",
"@glint/tsserver-plugin": "^2.0.8",
"@rdil/parallel-prettier": "^3.0.0",
"@swc/core": "^1.15.7",
"chrome-launcher": "^1.2.1",
"chrome-remote-interface": "^0.33.3",
"concurrently": "^9.2.1",
"ember-template-lint": "7.9.3",
"esbuild": "^0.25.11",
"eslint": "9.39.2",
"jsdoc": "^4.0.5",
"lefthook": "^2.0.13",
"licensee": "^11.1.1",
"lint-to-the-future": "^2.6.4",
"lint-to-the-future-ember-template": "^3.1.0",
"lint-to-the-future-eslint": "^3.3.0",
"playwright": "1.57.0",
"prettier": "3.8.0",
"puppeteer-core": "^24.34.0",
"stylelint": "17.0.0",
"terser": "^5.44.1",
"typescript": "^5.9.3"
},
"scripts": {
"dev": "concurrently \"bin/ember-cli server --environment=development\" \"RAILS_ENV=development bin/rails server\"",
"lint": "concurrently \"pnpm:lint:*(!fix)\" --names \"lint:\"",
"lint:fix": "concurrently \"pnpm:lint:*:fix\" --names \"fix:\"",
"lint:css": "pnpm stylelint 'app/assets/stylesheets/**/*.scss' $(script/list_bundled_plugins '/assets/stylesheets/**/*.scss') 'themes/**/*.scss'",
"lint:css:fix": "pnpm stylelint --fix 'app/assets/stylesheets/**/*.scss' $(script/list_bundled_plugins '/assets/stylesheets/**/*.scss') 'themes/**/*.scss'",
"lint:js": "eslint ./frontend $(script/list_bundled_plugins) ./themes --cache --no-error-on-unmatched-pattern",
"lint:js:fix": "eslint --fix ./frontend $(script/list_bundled_plugins) ./themes --no-error-on-unmatched-pattern",
"lint:hbs": "ember-template-lint 'frontend/**/*.gjs' 'plugins/*/assets/javascripts/**/*.gjs' 'plugins/*/admin/assets/javascripts/**/*.gjs' 'themes/**/*.gjs'",
"lint:hbs:fix": "ember-template-lint 'frontend/**/*.gjs' 'plugins/*/assets/javascripts/**/*.gjs' 'plugins/*/admin/assets/javascripts/**/*.gjs' 'themes/**/*.gjs' --fix",
"lint:prettier": "pnpm pprettier --list-different 'app/assets/stylesheets/**/*.scss' 'frontend/**/*.{js,gjs,css}' $(script/list_bundled_plugins '/assets/stylesheets/**/*.scss') $(script/list_bundled_plugins '/{assets,admin/assets,test}/javascripts/**/*.{js,gjs}') 'themes/**/*.{js,gjs,scss}'",
"lint:prettier:fix": "pnpm prettier -w --no-error-on-unmatched-pattern 'app/assets/stylesheets/**/*.scss' 'frontend/**/*.{js,gjs,css}' $(script/list_bundled_plugins '/assets/stylesheets/**/*.scss') $(script/list_bundled_plugins '/{assets,admin/assets,test}/javascripts/**/*.{js,gjs}') 'themes/**/*.{js,gjs,scss}'",
"lint:types": "ember-tsc -b",
"lttf:ignore": "lint-to-the-future ignore",
"lttf:output": "lint-to-the-future output -o ./lint-progress/",
"lint-progress": "pnpm lttf:output && npx html-pages ./lint-progress --no-cache",
"ember": "pnpm --dir=frontend/discourse ember",
"playwright-install": "playwright install --no-shell chromium"
},
"engines": {
"node": ">= 20",
"npm": "please-use-pnpm",
"yarn": "please-use-pnpm",
"pnpm": "^10"
},
"packageManager": "pnpm@10.28.0",
"pnpm": {
"patchedDependencies": {
"ember-this-fallback@0.4.0": "patches/ember-this-fallback@0.4.0.patch",
"babel-plugin-debug-macros@0.3.4": "patches/babel-plugin-debug-macros@0.3.4.patch",
"licensee@11.1.1": "patches/licensee@11.1.1.patch",
"@ember-compat/tracked-built-ins@0.9.1": "patches/@ember-compat__tracked-built-ins@0.9.1.patch",
"ember-source@6.6.0": "patches/ember-source@6.6.0.patch",
"http-proxy@1.18.1": "patches/http-proxy@1.18.1.patch"
},
"peerDependencyRules": {
"allowedVersions": {
"lint-to-the-future-eslint>eslint": "*",
"@mixer/parallel-prettier>prettier": "*",
"lint-to-the-future-ember-template>ember-template-lint": "*",
"ember-this-fallback>ember-source": "*",
"ember-this-fallback>ember-cli-htmlbars": "*",
"@fullcalendar/moment-timezone>moment-timezone": "*"
},
"ignoreMissing": [
"webpack"
]
},
"overrides": {
"@ember/test-waiters": "4.1.1"
}
}
}