Dedicated crypto ExecutionContext: eliminate CPU queueing between ECIES and DB#492
Dedicated crypto ExecutionContext: eliminate CPU queueing between ECIES and DB#492elliottdehn wants to merge 1 commit into
Conversation
Canton runs all work — ECIES encryption, DB callbacks, protocol orchestration, gRPC handlers — on one shared ExecutionContext (*-env-ec). When ECIES saturates all cores (38-43% of CPU per JFR profiling), DB callback threads can't be scheduled, causing queueing delays that dominate latency under load. This adds a dedicated *-crypto-ec thread pool for asymmetric crypto. When configured, encryptFor dispatches per-member ECIES to this pool via parTraverse + Future(...)(cryptoPool). The main EC stays free for DB callbacks and protocol orchestration — DB threads schedule instantly regardless of crypto load. This is the "poor man's bipartite" — captures the latency win (CPU queueing elimination) without separate machines. In our bipartite PoC benchmark, the latency improvement (p50 260ms → 38ms) was primarily driven by this queueing elimination, not by having more total cores. Wiring: - Environment.scala: creates cryptoExecutionContext alongside the main executionContext (same parallelism, separate pool) - SynchronizerCryptoClient: gains optional cryptoEc parameter, threaded through to SynchronizerSnapshotSyncCryptoApi - SynchronizerSnapshotSyncCryptoApi.encryptFor: when cryptoEc is set, dispatches per-member encryption to the crypto pool via parTraverse. Falls back to sequential on main EC otherwise. All existing call sites are unaffected (cryptoEc defaults to None). To enable: pass Environment.cryptoExecutionContext when constructing SynchronizerCryptoClient. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
I have hereby read the Digital Asset CLA and agree to its terms |
Benchmark Results: Thread pool separation doesn't help in single JVMBenchmarked shared pool vs split pool (same total threads) with 10 recipients, 16 concurrent transactions, real Postgres:
Split is consistently worse on latency and mostly worse on throughput. The problem: the transaction pipeline is sequential (encrypt → DB → decrypt → DB). When the main-pool thread dispatches crypto to the crypto pool and calls The bipartite architecture's latency win comes from a different mechanism: B handles many concurrent transactions, so while transaction A waits for its crypto response from node A, transaction B's DB callback runs on B's now-free core. This interleaving only works across separate machines where B never does crypto at all. Closing this PR — the dedicated crypto EC doesn't provide the benefit we hypothesized. The queueing contention requires truly separate machines (the full bipartite architecture) or restructuring Canton's per-transaction pipeline to be non-blocking (which would be a much larger change than thread pool separation). The PRs that DO help:
|
|
Closing — benchmark shows thread pool separation doesn't help when the per-transaction pipeline is sequential. See benchmark results in comments. |
Summary
Add a dedicated thread pool for CPU-bound asymmetric crypto (ECIES/ECDH), separate from Canton's main
ExecutionContext. Prevents crypto work from blocking DB callbacks, protocol orchestration, and gRPC handlers.The Problem
Canton runs everything on one shared
ExecutionContext(*-env-ec). JFR profiling shows ECIES/ECDH consumes 38-43% of CPU. When all cores are saturated with elliptic curve math, DB callback threads can't be scheduled — they sit in the CPU run queue behind ECIES operations.The DB operations themselves are fast (IO-wait, ~1.2% of CPU). But they can't start until a core is available. A 2ms DB write becomes 2ms + queueing delay. This queueing delay is invisible in CPU profiling but dominates end-to-end latency under load.
Our bipartite PoC benchmark showed p50 latency dropping from 260ms to 38ms — primarily from eliminating this queueing contention, not from having more total cores.
The Fix
Two separate thread pools:
DB callbacks run on the main EC, which is no longer saturated. Crypto runs on its own pool. No contention.
Changes
Environment.scalacryptoExecutionContextpool alongsideexecutionContextSyncCryptoApiParticipantProvider.scalaSynchronizerCryptoClientandSynchronizerSnapshotSyncCryptoApigain optionalcryptoEcparameter.encryptFordispatches to crypto pool viaparTraverse + Future(...)(cryptoPool)when set.All existing call sites unaffected (
cryptoEcdefaults toNone). To enable: passcryptoExecutionContextwhen constructingSynchronizerCryptoClient.Relation to other PRs
parTraverse, which implicitly parallelizes across crypto-pool threads.Test plan
EncryptedViewMessageFactorytests passSimplestPingIntegrationTestpassescryptoEcis None, behavior is identical to before (sequential on main EC)cryptoEcis set, encryption runs on the crypto pool (verify via thread names in logs)🤖 Generated with Claude Code