Repository navigation
Description
Summary
The desktop backend child process aborts with JavaScript heap out of memory after about 4 hours of uptime. The crash happens while the backend writes the usage-scan cache. That cache has grown to 307 MB, and it looks like the backend serializes it into one string and then UTF-8 encodes it on the main heap.
Environment
- T3 Code (Nightly)
0.1.9-nightly.20261110.4465, Electron, ARM64 - macOS 27.2 (26B123), Mac17,6
- Backend child uptime at crash: 14,291,290 ms (~4 h)
What happened
The desktop-backend-child log (~/.t3/userdata/logs/server-child.log) shows:
[...] Mark-Compact (reduce) 1452.3 (1717.8) -> 1452.1 (1618.6) MB, ... last resort; GC in old space requested
[...] Mark-Compact (reduce) 1452.1 (1618.6) -> 1448.4 (1588.3) MB, ... last resort; GC in old space requested
FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory
...
pid=<backend> killed by SIGABRT
The desktop shell restarted the backend. The window survived.
Top of the native stack (macOS crash report, main thread):
node::OOMErrorHandler(char const*, v8::OOMDetails const&)
...
v8::String::Utf8Length(v8::Isolate*) const
node::encoding_binding::BindingData::EncodeUtf8String(...)
<JS frames>
...
node::fs::FSReqCallback::Resolve(v8::Local<v8::Value>)
node::fs::AfterStringPath(uv_fs_s*)
uv__work_done
So a JS continuation that runs after an fs call returns a path (mkdtemp, realpath or mkdir with recursive) UTF-8 encodes a very large string, and that allocation does not fit.
Evidence that it is the usage-scan cache write
State of ~/.t3/userdata/ right after the crash:
usage-scan-cache-v11.json 307M (mtime = crash minute)
usage-scan-cache-v11.json.g4Znez/ empty dir, created at the crash minute
usage-scan-cache-v5.json 221M (stale, from 20 Oct)
usage-scan-cache.json 147M (stale, from 11 Oct)
The empty usage-scan-cache-v11.json.XXXXXX folder looks like the mkdtemp step of an atomic write (temp folder, write, rename). The process died between mkdtemp and the write. That matches the AfterStringPath, then EncodeUtf8String stack. A 307 MB JSON string plus a buffer of the same size, on a heap that already holds about 1.45 GB, exceeds the heap limit.
I did not capture a JS stack, so the exact function is inferred from the temp-folder name and the timing.
Expected
The cache write should not be able to crash the backend, whatever the cache size.
Suggested fixes
- Stream the cache to disk (for example NDJSON, or a stream with incremental JSON serialization) instead of
JSON.stringifyof the whole object followed bywriteFile. Or move the cache into the existing SQLite store. - Bound the cache: evict or compact old entries so it does not grow without limit (it went from 147 MB to 307 MB over 3 cache versions in about 1 month).
- Delete superseded cache versions (
usage-scan-cache.json,-v5.json) on migration. They waste 368 MB here. - Clean up the orphaned
*.json.XXXXXXtemp folder on startup. - Add a regression test that writes a cache larger than the heap headroom (or that asserts the writer never builds the full payload as one string).
Activity
fix(server): remove superseded usage-scan caches and stale temp folders