Skip to content

Memory optimization#1120

Merged
Klakurka merged 4 commits intoPayButton:masterfrom
Fabcien:memory_optimization
Mar 3, 2026
Merged

Memory optimization#1120
Klakurka merged 4 commits intoPayButton:masterfrom
Fabcien:memory_optimization

Conversation

@Fabcien
Copy link
Collaborator

@Fabcien Fabcien commented Mar 3, 2026

Instead of downloading the whole history of an address then commit the transactions to the database, store them periodically if there are enough to fill a batch. This frees the memory and avoid memory peaks when syncing a big address.

Includes some trivial memory optimizations as well which are low hanging fruits.

Summary by CodeRabbit

  • Chores
    • Optimized batch processing of transactions for more efficient incremental yielding and synchronization
    • Enhanced memory management during batch operations to reduce resource consumption
    • Improved completion tracking and progress reporting during address synchronization

Fabcien added 4 commits March 3, 2026 09:59
It's O(n) instead of O(2n)
This changes the logic in fetchLatestTxsForAddresses to yield the chronik transactions periodically instead of waiting for the full address transactions to be fetched.

With this implementation, there is a race between the download and the processing, which will drain the transactions every 500ms up to the point where there is not enough remaining txs to fill a batch.

This means that the tx buffer memory can grow up to batch size (20) + how many txs can be downloaded during the polling delay. This is not a hard limit but is a clear win when processing addresses with thousands of  transactions.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 3, 2026

📝 Walkthrough

Walkthrough

Added a new polling delay constant and refactored chronikService batching logic to emit transactions incrementally with per-address progress tracking. Implements race-based worker completion detection and completion-driven flush for improved batch emission timing, alongside memory management optimizations.

Changes

Cohort / File(s) Summary
Constants Configuration
constants/index.ts
Added TX_BATCH_POLLING_DELAY constant (value: 500) for configuring transaction batch polling behavior.
Chronik Service Batching
services/chronikService.ts
Refactored batch emission logic to yield transactions and completed addresses incrementally using race-based worker completion detection. Introduces per-address progress tracking (addressesSynced), completion-driven flush when all workers finish, and switches from slice-based to splice-based buffering for memory efficiency. Replaces map-based transaction lookups with batch tuples (row + raw).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • Incremental syncing #1119: Modifies the same batching and commit logic in chronikService with related changes to batch tuple handling and per-address completion tracking mechanisms.

Suggested reviewers

  • Klakurka

Poem

🐰 Hop, hop—batch tuples race,
Workers sync with polling grace,
Memory cleared, emissions bright,
Incremental yields take flight! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'Memory optimization' is vague and overly generic, using non-descriptive language that doesn't clearly convey the main algorithmic change (batched emission with polling delay) or the specific problem being addressed. Consider a more specific title that reflects the main change, such as 'Implement batched transaction yielding with polling delay to reduce memory peaks' or 'Optimize memory by yielding transactions periodically during address sync'.
✅ Passed checks (2 passed)
Check name Status Explanation
Description check ✅ Passed The description explains the core improvement and mentions additional optimizations, but is missing required template sections like 'Related to #' (issue link) and 'Test plan' details, though it provides adequate context of the changes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
services/chronikService.ts (1)

883-887: Use indexed tuple lookup to avoid repeated linear scans.

commitTuples.find(...) inside the createdTxs loop is O(n²) for the batch. Build a lookup once and do O(1) access.

♻️ Proposed refactor
     if (createdTxs.length > 0) {
+      const tupleByHash = new Map<string, { row: Prisma.TransactionUncheckedCreateInput, raw: Tx, addressString: string }>()
+      for (const t of commitTuples) {
+        if (!tupleByHash.has(t.row.hash)) tupleByHash.set(t.row.hash, t)
+      }
+
       const triggerBatch: BroadcastTxData[] = []
       for (const createdTx of createdTxs) {
-        const tuple = commitTuples.find(t => t.row.hash === createdTx.hash)
+        const tuple = tupleByHash.get(createdTx.hash)
         if (tuple == null) {
           continue
         }
         const bd = this.broadcastIncomingTx(createdTx.address.address, tuple.raw, createdTx)
         triggerBatch.push(bd)
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/chronikService.ts` around lines 883 - 887, The loop currently calls
commitTuples.find(...) for every createdTx causing O(n²) behavior; instead,
before iterating createdTxs build a lookup map keyed by row.hash (e.g., const
commitTupleByHash = new Map(commitTuples.map(t => [t.row.hash, t]))) and then
inside the createdTxs loop replace commitTuples.find(...) with a constant-time
lookup (commitTupleByHash.get(createdTx.hash)); keep using the same tuple
variable and pass tuple.raw into broadcastIncomingTx(createdTx.address.address,
tuple.raw, createdTx) and handle the null/undefined case as before.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@services/chronikService.ts`:
- Around line 425-448: The loop can exit before emitting pending per-address
completions because the break only checks allWorkersDone and chronikTxs.length;
update the loop logic so the break also requires completedAddresses to be empty
(or check completedAddresses.length in the break guard), and/or move the "Yield
completed addresses" block to run immediately before the final break; ensure
both remaining chronikTxs (chronikTxs.splice(0)) and
completedAddresses.splice(0) are emitted when allWorkersDone is true so no
completedAddresses are dropped—adjust while condition and final break to depend
on allWorkersDone && chronikTxs.length === 0 && completedAddresses.length === 0
and keep the existing batching logic using TX_EMIT_BATCH_SIZE.

---

Nitpick comments:
In `@services/chronikService.ts`:
- Around line 883-887: The loop currently calls commitTuples.find(...) for every
createdTx causing O(n²) behavior; instead, before iterating createdTxs build a
lookup map keyed by row.hash (e.g., const commitTupleByHash = new
Map(commitTuples.map(t => [t.row.hash, t]))) and then inside the createdTxs loop
replace commitTuples.find(...) with a constant-time lookup
(commitTupleByHash.get(createdTx.hash)); keep using the same tuple variable and
pass tuple.raw into broadcastIncomingTx(createdTx.address.address, tuple.raw,
createdTx) and handle the null/undefined case as before.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7f67067 and 5238be4.

📒 Files selected for processing (2)
  • constants/index.ts
  • services/chronikService.ts

Comment on lines +425 to +448
while (!allWorkersDone || chronikTxs.length > 0) {
// Yield batches if buffer is large enough
while (chronikTxs.length >= TX_EMIT_BATCH_SIZE) {
const chronikTxsSlice = chronikTxs.splice(0, TX_EMIT_BATCH_SIZE)
yield { chronikTxs: chronikTxsSlice, addressesSynced: [] }
}

// If workers are done, yield any remaining transactions (even if < batch size)
if (allWorkersDone && chronikTxs.length > 0) {
// This clears chronikTxs so the below check for length === 0 is true
const remaining = chronikTxs.splice(0)
yield { chronikTxs: remaining, addressesSynced: [] }
}

// Yield completed addresses if any
if (completedAddresses.length > 0) {
const completed = completedAddresses.splice(0)
yield { chronikTxs: [], addressesSynced: completed }
}

// If workers are done and no more transactions, break
if (allWorkersDone && chronikTxs.length === 0) {
break
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Per-address completion events can be skipped by the loop exit condition.

The loop guard/break only checks allWorkersDone and chronikTxs.length. If workers finish after the completedAddresses check in an iteration, pending completed addresses may not be yielded incrementally before exit.

🔧 Proposed fix
-      while (!allWorkersDone || chronikTxs.length > 0) {
+      while (!allWorkersDone || chronikTxs.length > 0 || completedAddresses.length > 0) {
         // Yield batches if buffer is large enough
         while (chronikTxs.length >= TX_EMIT_BATCH_SIZE) {
           const chronikTxsSlice = chronikTxs.splice(0, TX_EMIT_BATCH_SIZE)
           yield { chronikTxs: chronikTxsSlice, addressesSynced: [] }
         }
@@
-        if (allWorkersDone && chronikTxs.length === 0) {
+        if (allWorkersDone && chronikTxs.length === 0 && completedAddresses.length === 0) {
           break
         }

Also applies to: 451-459

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/chronikService.ts` around lines 425 - 448, The loop can exit before
emitting pending per-address completions because the break only checks
allWorkersDone and chronikTxs.length; update the loop logic so the break also
requires completedAddresses to be empty (or check completedAddresses.length in
the break guard), and/or move the "Yield completed addresses" block to run
immediately before the final break; ensure both remaining chronikTxs
(chronikTxs.splice(0)) and completedAddresses.splice(0) are emitted when
allWorkersDone is true so no completedAddresses are dropped—adjust while
condition and final break to depend on allWorkersDone && chronikTxs.length === 0
&& completedAddresses.length === 0 and keep the existing batching logic using
TX_EMIT_BATCH_SIZE.

@Klakurka Klakurka self-requested a review March 3, 2026 17:19
@Klakurka Klakurka added the enhancement (behind the scenes) Stuff that users won't see label Mar 3, 2026
@Klakurka Klakurka added this to the Phase 3 milestone Mar 3, 2026
@Klakurka Klakurka merged commit 06d9c3e into PayButton:master Mar 3, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement (behind the scenes) Stuff that users won't see

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants