Skip to content

한국어 | English

Systems Index | Previous: Core Design Decisions | Next: Synchronization Model

Core hot path

What this chapter defines — The scope of the Core code that runs once per message (the hot path), what that code must not do, how state is cached for it, and the performance gates a hot-path change has to pass.

1. Why a separate contract

Most of the Core's consistency contracts (reconnect, generation, pair readiness, request correlation) are implemented as general paths that re-interpret the current state. Those paths were designed to run once per connection change; run once per message, they cost tens of percent of throughput. Contract tests do not see the difference: every contract test stays green even when general-path work is placed inside the message path — re-resolving the selected pipe through its endpoint string, consulting the pair table under its mutex, allocating temporary vectors.

The hot path is therefore governed by rules that differ from consistency code, and this chapter fixes those rules.

2. Scope of the hot path

The hot path is the whole call tree from the following public entry points down to a pipe write or a pipe read. This table is normative: a change to a function in it (or to one of its callees) is subject to the rules of §3 and the gates of §5, and a change that inserts a new function into this tree must update the table.

Entry point Path
zlink_send (PAIR, DEALER) submit_completion_aware_part → blocking goes through send_completion_submit_blocking, a refused DONTWAIT through register_send_writable_wait_after_failuretry_admit_send_parts_scopedxsend_selected_pipe / xsend_configured_endpoint / send_direct_with_retrylb_t::sendpipe_topipe_t::write_*
zlink_send_rid (ROUTER, STREAM) as above, through the send_direct_with_retry branch
zlink_request (DEALER, ROUTER) request_part_commonsubmit_pull_blocking_requestrequest_admission_submit_blockingtry_admit_send_parts_scopedarm_socket_pending_request_timeout
zlink_reply (ROUTER) public_router_reply_submitcheckout_router_reply_targetsend_public_router_reply_with_waitretain_reply_transport_pipesend_completion_staged_frames_on_pipe
zlink_recv / zlink_router_recv recv_dealer_record / router_recv_part_implrecv_common / recv_routedfq_t::recvpipepipe_t::readreclassify_transport_pair_application_headend_public_part_receive_delivery_hold
zlink_completion_recv process_submit_commandsprepare_completion_pull when blocking with a nonzero timeout → socket_completion::recv
zlink_poll / zlink_poller_wait get_events_internalprocess_commandsxhas_in / xhas_out
I/O thread → socket delivery pipe_t::flushactivate_read command → xread_activatedfq_t::activated; process_async_mailbox

3. What the hot path must not do

Code on the hot path does none of the following. Exceptions are the opaque reply token lookup below and the fallback path of §4.

  1. Heap allocation. No per-message temporary std::vector or std::string, no new, no make_shared. Buffers that are needed reuse member scratch owned by the socket, the load balancer or the pipe.
  2. Resolving identity through strings. A pipe is never looked up by building an endpoint identifier or a routing ID string. The pipe_t* obtained at selection time is used as is, within the same send scope.
  3. Socket-level table lookups and their mutexes. Socket-level containers — the transport pair table, pending queue maps, route history — are not searched per message. The state the message path asks for is answered by the caches of §4. The lookup and mutex in checkout_router_reply_target that resolve an opaque reply token are an exception. The Core request/reply owner uses this lookup to validate the reply target and ownership.
  4. Unconditional side work. Work that is only occasionally needed — releasing a hold, reclassifying a head, flushing deferred controls — first checks an atomic flag and takes a lock only when the flag says so.
  5. Peeking that puts the reader to sleep. Receive-side code that inspects a pipe head looks only within the prefetched range. A probe that parks the ypipe produces an activate_read command round trip per message.
  6. Fixed-duration sleeps. A retry waits by parking on the socket mailbox (wait_submit_progress). A fixed slice sleep stretches every flush wait of a framed transport (WS, WSS) to the slice length.
  7. Missed wake-ups by temporary owners. Every path on which an async executor consumes commands on the socket's behalf and then detaches re-arms the public poller with rearm_primary_signaler(). Without it a poller sleeps until its own timeout.

Allowed: atomic loads and stores, fixed-size stack arrays, the send/recv scope already held, and the endpoint's established C2 owner/turn. _out_sync is used only on the cold paths defined by the synchronization model; the per-message write, read and flush do not take it.

4. State caches and the fallback path

Socket-level state that the message path needs is published into the pipe as atomics at the point where it changes, and the message path reads only that cache.

Question Cache Published at
Is this pipe the Application lane of a ready transport pair pipe_t::transport_pair_application_ready_cached() set at pair admission, cleared at the first physical detach
Is this pipe lifecycle-active pipe_t::is_lifecycle_active() (mirror of _state) every transition that leaves active
Is a public whole-message receive delivery hold published _public_part_receive_delivery_hold_active (atomic) hold begin/end

Only when the cache cannot answer (the selected pipe detached meanwhile, or backpressure requires a retry wait) does the code fall back to the general path. The fallback keeps these rules:

  • The first attempt admits directly to the selected pipe. Only a retryable refusal (EAGAIN, ENOTCONN, EHOSTUNREACH, ECONNREFUSED) commits that selection to the configured endpoint and enters the wait loop. Any other failure returns immediately with the errno the general path would have produced.
  • ZLINK_SEND_FLAGS_DONTWAIT does not enter the wait loop. Whether a token is issued is decided by the target and admission judgment of the common socket submit contract (only backpressure and an unready target issue a token; a RID with no route returns NOT_CONNECTED without one); what this document owns is only the cost boundary: token registration exists only on the refusal path, never on the successful submit path. The endpoint commit rule applies only to blocking ZLINK_SEND_FLAGS_NONE send and to REQUEST.
  • Wait-token registration and the WRITABLE record publish lie outside the per-message success path. Both run only on a refusal and on a credit or attach wake, so they fall within the allowance of §3 (the fallback path), and the §3 prohibitions on the success path still hold.
  • The contract of the fallback (a blocking send selects its target once per record, and retries stay on the same endpoint) is unchanged by the existence of the fast path.

5. Performance gates

Both gates are separate from the contract tests. §5.1 must pass for every change to the hot path; a change for which it did not run is unverified. §5.2 is the gate run during release preparation and is not a precondition of an individual change — the per-change judgment is owned by §5.1 alone. This section owns that execution obligation and CONTRIBUTING cites it.

5.1 Instruction-count gate (hotpath_gate)

core/tests/perf/hotpath_gate measures, under callgrind, the number of instructions executed per message. Unlike wall-clock throughput it is deterministic, so no re-measurement is needed. The measured cells and their reference values are checked in as core/tests/perf/hotpath_reference.json; a cell that moves by more than ±5% fails.

cell Measures
dealer_dealer_inproc DEALER→DEALER one-way, send+recv instructions per message
dealer_router_reqrep_inproc DEALER request → ROUTER reply → completion, instructions per request
pair_inproc PAIR one-way
router_router_tcp ROUTER↔ROUTER one-way (count-2 negative control)

The reference is the value of a verified release or an approved change; an intended cost increase is recorded only by a supervisory decision with its rationale. Implementation work does not edit the reference. Where valgrind is unavailable the test is not registered; in that case "gate not run" must appear in the report and does not count as green.

5.2 Release comparison gate

The C perf comparison against the previous Core release is performed by bindings/c/perf/perf_regression_gate.py and judged in two stages. Release procedures cite this gate; this section owns the criteria.

  1. The 5% per cell (pattern, transport, size, metric) is the measurement tolerance: throughput and bandwidth >= 0.95, latency <= 1.05.
  2. For each (pattern, transport), message sizes 64, 256, 1024, 65536 are all run and the geometric mean of the per-size ratios must be >= 1.0 for throughput and bandwidth and <= 1.0 for latency. No pattern/transport may average below the previous release.

The gate passes only when both the cell judgement and the aggregate judgement pass. A failing cell is never handled by relaxing the gate or deferring it. If the benchmark's measurement is wrong (for example reporting saturated queue depth as latency), the benchmark is fixed, not the gate.

6. Change procedure

  1. A change to a hot-path function states in its diff which entry point of the §2 table it belongs to.
  2. Code that violates any item of §3 is rewritten in the cache / fallback form of §4.
  3. The §5.1 gate is run before and after the change and its result recorded. Release preparation also runs §5.2.
  4. Adding a new entry point or a new per-message function adds a row to the §2 table and a cell to §5.1.

Systems Index | Previous: Core Design Decisions | Next: Synchronization Model