Skip to content

perf: parallelize investment scanning with bounded concurrency#951

Open
dangershony wants to merge 1 commit into
mainfrom
perf/bounded-parallel-investment-scan
Open

perf: parallelize investment scanning with bounded concurrency#951
dangershony wants to merge 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)

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