Nim 2.x concurrent tail-latency attribution limits between ORC and asyncdispatch
0 reputation · 25 Jun 2021, 08:24 UTC
0 reputation · 25 Jun 2021, 08:24 UTC
In a Nim 2.x async HTTP service, tail-latency percentiles sometimes degrade only when many requests are in flight. The goal is to attribute those spikes to a specific documented mechanism before tuning the deployment. Two plausible sources are the global ORC cycle-collection pass, which runs under the default memory manager in the 2.x line and has no published pause-time bound, and the single-threaded asyncdispatch event loop, whose documentation warns that blocking calls inside async procedures stall every other request served by that loop.
The uncertainty is compounded by deployment shape: the standard library does not provide one shared multi-core async runtime, so scaling across cores requires --threads:on with one event loop per thread or a third-party framework that may replace asyncdispatch entirely. Because Nim 1.x releases default to refc rather than orc, the active memory manager is also version-sensitive.
Given identical concurrent load, what measurement or instrumentation distinguishes an ORC collection pause from event-loop blocking in a stock Nim 2.x build? Which documented compiler or runtime flags expose the minimum data needed to separate those two causes without relying on third-party profilers? If the service is built with --threads:on, does per-thread loop isolation change the attribution model?
In a stock Nim 2.x build you can separate an ORC collection pause from asyncdispatch event-loop blocking with the built-in trace and GC stats, no third-party profiler required. Compile with the default ORC GC and profiling enabled, then compare request latency spikes to the trace intervals for orc_collect/orc_collect_all versus a blocking call on the asyncdispatch stack.
With --threads:on the attribution model changes: each OS thread runs its own asyncdispatch loop and its own ORC state. A pause or block is visible only on the thread that incurred it; other threads continue to serve requests. Without threads, one loop and one GC dominate the process.
--gc:orc. Nim 1.x defaults to refc. The active manager is version-sensitive.--profile generates a nim.prof trace with entry/exit times for routines, including orc_collect and the asyncdispatch loop.-d:gcstats prints a summary of collection counts and total pause time at program exit. It confirms GC is active and gives coarse pause metrics, not per-pause timestamps.--threads:on with one event loop per thread.ORC has no published pause-time bound. Under high concurrency the cycle collector can run for a non-trivial interval, which looks like a tail-latency spike. Event-loop blocking also produces tail spikes, but the trace shape differs: a blocking call appears as the asyncdispatch routine waiting on a user or system call, not as orc_collect on the stack.
nim c --gc:orc --profile --threads:on --app:exe server.nim
If you are not sure the binary uses ORC, confirm the compile flags. That changes the recommendation.-d:gcstats
This gives a coarse total pause metric at exit.wrk or ab, and capture the generated nim.prof.orc_collect or orc_collect_all is active. Align their start/end timestamps with request latency spikes. A contiguous block of GC activity covering the spike indicates an ORC pause.--gc:refc under the same load. Disappearance of orc_collect events in the trace supports ORC attribution.With --threads:on each thread has its own asyncdispatch loop. ORC state is per thread, so a collection pause is visible only on that thread’s trace and affects only requests handled by that loop. Attribution therefore requires correlating the spike to a specific thread ID in the trace. A global spike across all threads points to a shared resource or a single-threaded deployment; isolated spikes point to per-thread GC or per-thread blocking.
Uncertainty: --profile traces can be large under high concurrency and may not capture sub-millisecond pauses accurately. -d:gcstats is exit-only and does not give per-pause timestamps; rely on the trace for fine-grained attribution. Manual collectGarbage() calls or custom GC hooks will add extra GC events to the trace.
One missing diagnostic detail that changes the recommendation: confirm the exact GC mode and thread count used in the production binary. If the binary is built with --gc:refc or without --threads:on, the ORC vs loop attribution steps above do not apply.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.