fix(backend): eliminate N+1 query patterns in portfolio and projects #1420 - #1465
Merged
Cedarich merged 2 commits intoSep 23, 2026
Merged
Conversation
…1420) ## Five worst endpoints fixed | Endpoint | Before (10 items) | After | |-----------------------------------|-------------------|-------| | POST /portfolio/snapshots (main) | 10 price calls | 1 | | POST /portfolio/snapshots (fallback) | 10 price calls | 1 | | GET /portfolio/account/:key | 10 price calls | 1 | | GET /portfolio/allocation | 10 price calls | 1 | | GET /projects (list) | N sequential RPC | concurrent + capped | ## Changes ### Batch price fetching (portfolio N+1 fixes) - Add PriceService.getPricesForAssets(codes[]) - single batched lookup regardless of portfolio size - Add StellarBalanceService.getAssetValuesUsd(assets[]) - replaces four separate Promise.all(items.map(getPrice)) patterns with one call - Fix portfolio.service.ts createSnapshot, getPortfolioSummaryForAccount, and getAssetAllocation to use the new batch method ### Projects list fix - projects.service.ts listProjects: preload all on-chain states with a single concurrent Promise.all before mapping instead of one fetchOnChainState per row inside a map ### Sequential loop fix - refreshMaterializedSnapshots: replace sequential for-await loop with Promise.allSettled batches (concurrency cap = 10) ### Query profiling middleware (off by default) - Extend QueryProfilerService with AsyncLocalStorage-based per-request call counter and runInContext() / trackCall() helpers - Add QueryCountMiddleware to wrap requests and log final call count - Register in AppModule behind QUERY_PROFILING=true env flag - Update ProfilingModule to export new middleware ### Regression test - Add portfolio-n-plus-one.spec.ts asserting getPricesForAssets is called exactly once for portfolios of 1, 5, and 10 assets (7 tests, all pass) ### Documentation - Add QUERY_PROFILING.md: how to enable, what gets logged, architecture, five fixed endpoints with before/after counts, test instructions Closes Pulsefy#1420
|
@ifygreg01-best Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #1420
Audited the four modules called out in the issue (grants, projects, contributor-feed, portfolio). Grants and contributor-feed are pure in-memory (no DB/RPC calls in loops). The five worst patterns were all in portfolio and projects.
Five Worst Endpoints — Before / After
POST /portfolio/snapshots(primary path)getAssetValuesUsd()batchPOST /portfolio/snapshots(fallback path)getAssetValuesUsd()batchGET /portfolio/account/:keygetAssetValuesUsd()batchGET /portfolio/allocationgetAssetValuesUsd()batchGET /projects+refreshMaterializedSnapshotsPromise.allSettledChanges
Batch price fetching (fixes #1, 2, 3, 4)
PriceService.getPricesForAssets(codes[])— single batched lookup; when a real price-feed API is wired in, this is the only place to changeStellarBalanceService.getAssetValuesUsd(assets[])— replaces four independentPromise.all(items.map(getPrice))patterns with one callportfolio.service.ts—createSnapshot,getPortfolioSummaryForAccount,getAssetAllocationall updatedProjects list fix (fix #5a)
projects.service.ts listProjects: preload all on-chain states with a single concurrentPromise.allbefore mapping, instead of onefetchOnChainStateper row inside a.map()Sequential refresh loop fix (fix #5b)
refreshMaterializedSnapshots: replace sequentialforloop withPromise.allSettledbatches (concurrency cap = 10)Query profiling middleware (off by default)
QueryProfilerServicewithAsyncLocalStorage-based per-request call counter (trackCall,runInContext)QueryCountMiddlewarewraps each request and logs the final call countAppModulebehindQUERY_PROFILING=true— zero overhead in productionProfilingModuleupdated to export new middlewareTests
New regression test:
src/portfolio/portfolio-n-plus-one.spec.tsSpies on
getPricesForAssetsand asserts it is called exactly once regardless of portfolio size:Documentation
apps/backend/QUERY_PROFILING.md— covers how to enable profiling mode, what gets logged, the middleware architecture, the five fixed endpoints with before/after counts, and how to run the regression test.Enable with: