Performance Audit β tunecamp / sidecamp / graphofone β
Date: 2026-07-22 Scope: tunecamp/ (backend + webapp), tunecamp-sidecamp/apps/sidecamp, tunecamp-sidecamp/apps/graphofone.
Status legend: [ ] open Β· [x] fixed Β· [~] in progress
Common pattern across all three β
Sequential I/O where concurrency is trivial (per-file await in for loops), missing list virtualization, missing pagination/caching on large list endpoints.
tunecamp (backend Node + SQLite, webapp React/Vite) β
High β
- [ ]
src/server/modules/subsonic/subsonic.service.ts:186getAlbumList()β unbounded JOIN+GROUP BY over all albums, sort/paginate done in JS on every request. Fix:LIMIT/OFFSETin SQL or TTL cache. - [x]
src/server/modules/subsonic/subsonic.service.ts:75-76formatAlbum()β N+1:isStarred()/getItemRating()per album row, no bulk prefetch (tracks already do this viaformatTracksBulk). Fixed:formatAlbumsBulkprefetches starredSet+ ratingsMap. - [x]
src/server/modules/subsonic/subsonic.service.ts:80-90formatArtist()β same N+1, doubled (starred + rating), zero bulk prefetch. Fixed: addedformatArtistsBulkwith prefetched maps;getIndexes/search/getStarrednow call it instead of loopingformatArtist. - [ ]
src/server/modules/subsonic/subsonic.service.ts:198-204search()β per-albumdb.getTracks()call just to compute songCount/duration. Fix: aggregate query (JOIN+GROUP BY). - [x]
src/server/routes/library/albums.ts:296β album covers served withmaxAge: 0(cache disabled) while tracks/artists usemaxAge: 86400000(tracks.ts:397,artists.ts:295,368). Fixed: aligned to86400000.
Medium β
- [ ]
src/server/modules/media/media-engine.ts:53-74sendStreamResult()β noCache-Control/ETagon audio stream responses. - [ ]
src/server/modules/catalog/catalog.service.ts:217-230batchDeleteTracks()β eachdeleteTrack()re-triggerssyncRelease(albumId); N tracks same album = N re-syncs. Fix: dedupe album_ids, sync once after loop. - [ ]
src/server/modules/storage/storage-usage.service.ts:33-55,159dirSize()β sequentialawait fs.statper file, uncached, run on every admin overview request. Fix:Promise.all+ short TTL cache. - [ ]
webapp/src/components/layout/MainLayout.tsx:6-11βCheckoutModal(pulls inethers),AuthModal,PlaylistModal,UnlockModal, admin modals statically imported into app shell for every visitor. Fix:React.lazy(). - [x]
webapp/vite.config.ts:18-19βvite-plugin-node-polyfillsincludesdgram/child_process/os/zlib/stream, leftover from removed ZEN/ZEN stack. Fixed: trimmed frominclude, onlybuffer/crypto/fs/path/process/util/url/eventsremain (used byethers/other libs). Also removed the dead"zen" β src/zen.jsalias (file didn't exist, never imported).
Low β
- [ ]
src/server/repositories/album.repository.ts:99-115getWithStats()β no LIMIT/pagination. - [ ]
src/server/repositories/album.repository.ts:79-93getLibraryAlbums()β silent hardcodedLIMIT 1000fallback, nohasMoresignal. - [ ]
src/server/routes/api/misc.ts:19-25,93getFilteredChangelog()βfs.readFileSync+ parse on every request, no cache. - [ ]
src/server/repositories/artist.repository.ts:121,301β no LIMIT on artist listing/search. - [ ]
src/server/core/database.ts:1322-1360β fulltrackstable scan into memory on every boot, unconditional. - [ ]
webapp/srcβ no virtualization on Library/Releases/Search grids (not urgent, backend caps ~1000 rows).
Verified clean: listener/interval cleanup, single shared SQLite connection, batch insert paths (transactions + chunking), federated-discovery crawler (Promise.allSettled + depth caps), no lodash/moment, route-level React.lazy already used for pages.
sidecamp (Electron app) β
High β
- [x]
organizer.ts:50-83scanDir()β sequential per-fileawait fs.stat+await parseFile(), no concurrency. Fixed: batched withCONCURRENCY=8(split intoscanOne()+ chunkedPromise.all), mirrorstrack-meta.ts. - [x]
electron/peer/daemon.ts:63-96scanFolders()β same sequential per-file loop, runs on every daemon start + everyrescanAndSendManifest(). Fixed: sameCONCURRENCY=8chunkedPromise.allpattern; progress now emitted once per batch. - [x]
electron/main.ts:449-463+peer/daemon.tsβ everytorrent:seed/torrent:removetriggers a fullrescanAndSendManifest()(full re-walk + re-parse) instead of incremental single-file update. Fixed: addedrefreshAndSendManifest()β updatesmagnetUrion cachedfileIndexentries and resends, no disk re-scan/re-parse. - [ ]
electron/providers/network.ts:12-129getPeers/getPeerTracks/getCatalogTracksβ unbounded/unpaginated, rendered into un-virtualized table.
Medium β
- [ ]
electron/peer/daemon.ts:217-232handleRequest()β audio streamed as base64-in-JSON over WebSocket (~33% overhead). Fix: binary WS frames. - [ ]
electron/organizer-cache.ts:33-61,electron/track-meta.ts:53-69β whole-cache JSON file read/rewritten on everycacheGet/cachePut, never pruned. - [ ]
src/App.tsx:1905-1966β Library table unvirtualized;currentTime(1/sec) forces full re-render of whole table. - [ ]
src/App.tsx:95-2751β single ~2750-lineAppcomponent, ~50useStatehooks, everything re-renders on any state tick. - [ ]
electron/providers/search.ts:132-172searchArchiveOrg()β up to 10 extra metadata calls per search, zero caching. - [ ] No HTTP keep-alive anywhere in
electron/(network.ts,uploader/index.tsuse plain axios, no sharedAgent). - [ ]
electron/providers/ytdlp.ts:23-68β synchronousfs.existsSync/mkdirSync/copyFileSync/chmodSyncblock Electron main process on every download/search. - [ ]
src/services/platform/capacitorAdapter.ts:5-10,154-159β listener arrays only ever.push(), no unsubscribe; remount duplicates callbacks.
Low β
- [ ]
electron/providers/network.ts:47-48,98-99β blockingfs.existsSyncin async functions. - [ ]
electron/providers/network.ts,electron/uploader/index.tsβ notimeouton axios calls. - [ ]
src/App.tsx:1662,2423β.some()inside.map()render callback, O(nΒ·m). Fix: precomputeSet(already done forselectedSet). - [ ]
vite.config.tsβ no route-based code splitting; ~386KB bundle loads eagerly. - [ ]
electron/organizer.tsorganize:scanβ cache keyed off top-level dir mtime only; subdir changes may not bump it. - [ ]
electron/beatport.ts/musicbrainz.tsgenre-fill loop β sequential w/ 1400ms sleep, intentional rate-limit (informational only, ~47min for 2000 tracks).
Verified clean: no keep-alive agent misconfig found elsewhere; preload.ts correctly does removeAllListeners dedup.
graphofone (Electron app) β
High β
- [x]
electron/library.ts:174-180+src/App.tsx:90βsaveLibrary()rewrites entire library JSON to disk per track analyzed in loop = O(nΒ²) I/O. Fixed: addedupdateTrackMetaBatch()(library.ts), wired IPC (main.ts/preload.ts/env.d.ts),handleAnalyzenow accumulates updates and flushes once. - [x]
src/App.tsx:89-102handleAnalyze()βsetLibrary(prev => prev.map(...))per-track inside loop = O(nΒ²) re-renders. Fixed together with the above β singlesetLibrary/setMetaafter the loop.
Medium β
- [ ]
src/App.tsx:70-108,electron/library.ts:106-131β analyze/import loops strictly sequential (awaitinfor), no concurrency for CPU/I/O-bound decode/parse. - [ ]
electron/library.ts:48-69findAudioFiles()β sequentialfs.statper file instead ofPromise.allper directory level, or skip viawithFileTypes: true. - [ ]
src/components/LibraryPanel.tsx:31-35β full unfiltered/unpaginated table, no virtualization. - [ ]
src/components/LibraryPanel.tsx:31-35βfilteredrecomputed via.filter()on every render, nouseMemo.
Low β
- [ ]
electron/library.ts:20-21,39-44saveLibrary()β no debounce (unlikesaveGraph, which is debounced). - [ ]
electron/main.ts:82-112app:update-checkβ no persisted cache across launches (minor). - [ ]
src/App.tsx:151libraryForGraphβ.map()recomputed every render, not memoized, defeatsGraphViewmemoization. - [ ]
electron/library.ts:169-172readAudioFile()β full-file read + buffer copy over IPC; fine at current scale. - [ ]
src/App.tsx:60-63β.filter()full-array scan just for early-return check.
Verified clean: LogViewer/ProgressBar/QuickTour listener cleanup correct; saveGraph IPC properly debounced; no DB (flat-file JSON, N/A for indexes).
Not audited: graph-ui/audio-engine workspace packages (consumed via vite.config.ts:10-11 aliases) β outside apps/graphofone scope, but GraphView is a hot-path component in this app's render tree.
Fix order (proposed) β
- graphofone: O(nΒ²)
saveLibrary/setLibraryper-track loop β cheapest fix, worst algorithmic complexity, in active analyze hot path. - sidecamp: sequential file scans (
organizer.ts,peer/daemon.ts) β same batching fix reusable across both. - tunecamp: subsonic N+1 (
formatAlbum/formatArtist) β mirror existingformatTracksBulkpattern already in the codebase. - tunecamp: album cover
maxAge: 0β one-line fix, immediate cache win. - sidecamp: full-rescan on single torrent seed/remove β incremental index update.