Skip to content

perf: parallelize investment scanning with bounded concurrency - #951

Merged
dangershony merged 1 commit into
mainfrom
perf/bounded-parallel-investment-scan
Aug 26, 2026
Merged

perf: parallelize investment scanning with bounded concurrency#951
dangershony merged 1 commit into
mainfrom
perf/bounded-parallel-investment-scan

Conversation

@dangershony

Copy link
Copy Markdown
Member

Split out of #949 (per review discussion on the ProjectInvestmentsService change) so the scan-perf work gets focused review.

Problem

ScanDynamicTypeInvestments awaited each spent-fund check sequentially — O(funders × stages) indexer round-trips dominated the "Syncing funding data…" time. But naively parallelizing with unbounded Task.WhenAll (as originally proposed in #949) is risky: projects can have hundreds of investors, and Fund projects multiply that by stage count — thousands of simultaneous HTTP requests would risk socket exhaustion and indexer rate limiting.

Changes

  • ScanDynamicTypeInvestments: collect all stage work items first, then run spent-fund checks in parallel with bounded concurrency (RunBoundedAsync, SemaphoreSlim capped at 12 concurrent indexer round-trips). Ordering is preserved (work-item index), so date-bucket grouping is byte-for-byte identical to the sequential version.
  • GetProjectInvestmentsTransactionsAsync: fetch tx hex + tx info concurrently per investment; the per-investment fan-out (previously already unbounded Task.WhenAll over all investors) now also goes through the bound.
  • ScanInvestTypeInvestments: the pre-existing unbounded per-stage Task.WhenAll is bounded too.

Bounded parallelism captures nearly all the latency win (it comes from overlapping round-trips, not N-way parallelism) with none of the failure risk at scale.

Testing

  • New unit test ScanFullInvestments_ManyFundInvestors_LimitsConcurrentIndexerRequests: 30 Fund investors × 3 stages with instrumented mocks tracking max in-flight indexer calls — asserts overlap happens (>1) and never exceeds 2 × cap (hex+info pairs share a slot).
  • Existing grouping regression tests (FundInvestorsWithShiftedSchedules, FundMixedBucket) unchanged and green — confirms functional equivalence of the restructured loop.
  • Full SDK suite: 334 passed, 14 skipped.

Suggested UAT validation

MultiFundClaimAndRecoverTest (dynamic scan end-to-end incl. spent-type discrimination) and MultiInvestClaimAndRecoverTest (Invest path + FundersView).

Split out of #949 for focused review.

- ScanDynamicTypeInvestments: collect spent-fund work items and run them
  in parallel instead of O(funders x stages) sequential awaits - this
  dominated the 'Syncing funding data...' time
- GetProjectInvestmentsTransactionsAsync: fetch tx hex + info concurrently
- All scan fan-outs now go through RunBoundedAsync (SemaphoreSlim capped
  at 12 concurrent indexer round-trips), including the two pre-existing
  unbounded Task.WhenAll spots. Projects can have hundreds of investors
  (x stages for Fund projects); unbounded WhenAll could fire thousands
  of simultaneous HTTP requests
- Add unit test asserting max in-flight indexer requests stays within
  the bound while still overlapping round-trips
foreach (var stage in stageDataList)
{
var tasks = investmentsResult.Value.Select(tuple =>
var taskFactories = investmentsResult.Value.Select(tuple =>

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the stage.StageIndex + 2 is suspicious here (needs more investigation)

@dangershony
dangershony merged commit aad89dd into main Aug 26, 2026
3 checks passed
@dangershony
dangershony deleted the perf/bounded-parallel-investment-scan branch August 26, 2026 18:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant