Benchmarks效能基準

Client-side code paths that need no server, measured in release mode on macOS and Linux

不需要伺服器的客戶端程式路徑,在 macOS 與 Linux 上以 release 模式量測

Every push to main reruns the suite on Linux and records one data point per commit

每次 push 到 main 都會在 Linux 重跑一次,每個 commit 記錄一個資料點

History歷史趨勢

Live charts from Bencher: p50 wall clock per iteration on the main branch, ubuntu-24.04 testbed, log scale. The workflow moved to that image on 2026-10-26; earlier runs stay on the ubuntu-latest testbed in Bencher. Click a chart to open the interactive view with allocation counts and per-commit details.

來自 Bencher 的即時圖表:main 分支在 ubuntu-24.04 testbed 上每次迭代的 p50 wall clock,對數軸。workflow 在 2026-10-26 改用這個 image,之前的執行仍留在 Bencher 的 ubuntu-latest testbed。點擊圖表可開啟互動頁面,查看記憶體配置次數與每個 commit 的細節。

EventData latency on Bencher Connection string parsing latency on Bencher Append encoding and read decoding latency on Bencher Client-stack latency on Bencher

Environment量測環境

Both columns below were measured on the same commit on 2026-10-25; the client-stack section on 2026-10-26, on later code. Linux numbers come from a shared GitHub-hosted runner and vary between runs; treat differences under about 10% as noise.

下表兩欄在同一個 commit 於 2026-10-25 量測;client stack 一節是 2026-10-26 在之後的程式碼上量的。Linux 數字來自共用的 GitHub-hosted runner,每次會有浮動,10% 以內的差異視為雜訊。

macOSLinux
Machine機器Apple M1 Max (arm64), 10 cores, 64 GBGitHub ubuntu-latest, AMD EPYC 9V45 (x86_64), 4 vCPU, 16 GB
OS作業系統macOS 27.2 (Darwin 27.2.0)Ubuntu 24.04.5 LTS (6.17.0-1022-azure)
Swift6.4 (swiftlang-6.4.0.34.1)6.4.1 (swift-6.4.1-RELEASE)
Build建置模式ReleaseRelease
Source來源local run本機執行Benchmarks workflow, run 40121083776

Results量測結果

p50 is the median wall clock per iteration; Malloc the heap allocations per iteration; Instructions the CPU instructions per iteration (macOS only: Linux needs perf_event, which GitHub runners do not expose). Lower is better everywhere. On Linux the benchmark runtime interposer adds a fixed floor of 16 allocations to every benchmark, so a zero-allocation benchmark reports 16 there.

p50 是每次迭代 wall clock 的中位數;Malloc 是每次迭代的堆積配置次數;Instructions 是每次迭代的 CPU 指令數(僅 macOS:Linux 需要 perf_event,GitHub runner 不開放)。所有數字都是越低越好。Linux 上 benchmark runtime 的 interposer 會對每個 benchmark 固定多算 16 次配置,所以零配置的 benchmark 在 Linux 會顯示 16。

EventData

BenchmarkmacOS p50macOS MallocmacOS InstructionsLinux p50Linux Malloc
EventData/create-raw-data1,167 ns15,9353,555 ns17
EventData/create-codable-model1,208 ns26,7993,545 ns18
EventData/encode-codable-payload3,751 ns1334 K5,011 ns29
EventData/create-batch-100-raw78 µs501911 K357 µs517
EventData/create-batch-100-codable52 µs201582 K342 µs217

Connection string parsing連線字串解析

BenchmarkmacOS p50macOS MallocmacOS InstructionsLinux p50Linux Malloc
ClientSettings/parse-single-node9,007 ns11118 K8,927 ns44
ClientSettings/parse-cluster-seeds20 µs29272 K18 µs70

Append request encodingAppend 請求編碼

Append/encode-request/<events>x<payload> builds the request messages of one append (requestMessages()) and serialises each to bytes, as the gRPC serializer does before writing them to the stream. The events carry raw Data payloads, so JSON encoding is not included.

Append/encode-request/<筆數>x<payload> 建出一次 append 的請求訊息(requestMessages()),再把每則序列化成 bytes,跟 gRPC serializer 寫進 stream 前做的事一樣。事件用的是原始 Data payload,所以不含 JSON 編碼。

BenchmarkmacOS p50macOS MallocmacOS InstructionsLinux p50Linux Malloc
Append/encode-request/1x100B2,751 ns624 K2,153 ns24
Append/encode-request/1x4KB2,835 ns625 K2,263 ns24
Append/encode-request/1x64KB5,043 ns642 K3,085 ns24
Append/encode-request/100x100B89 µs2041,156 K112 µs222
Append/encode-request/100x4KB104 µs2041,292 K122 µs222
Append/encode-request/100x64KB284 µs2042,939 K206 µs222
Append/encode-request/1000x100B878 µs2,00411.5 M1.23 ms2,022
Append/encode-request/1000x4KB1.01 ms2,00412.9 M1.32 ms2,022
Append/encode-request/1000x64KB2.19 ms2,00429.4 M2.14 ms2,022

Read response decoding讀取回應解碼

Read/decode-response/<events>x<payload> parses one serialised ReadResp per event, as the server streams them, and turns each into a Streams.ReadResponse. Read/decode-model/<events> then decodes the RecordedEvents of such a read into a three-field Codable struct with decode(to:).

Read/decode-response/<筆數>x<payload> 依 server 串流的方式,每筆事件解析一則序列化的 ReadResp,再轉成 Streams.ReadResponse。Read/decode-model/<筆數> 接著用 decode(to:) 把這些 RecordedEvent 解碼成三個欄位的 Codable struct。

BenchmarkmacOS p50macOS MallocmacOS InstructionsLinux p50Linux Malloc
Read/decode-response/1x100B4,667 ns946 K3,135 ns27
Read/decode-response/1x4KB4,583 ns947 K3,225 ns27
Read/decode-response/1x64KB5,751 ns960 K3,685 ns27
Read/decode-response/100x100B321 µs9004,329 K289 µs1,116
Read/decode-response/100x4KB338 µs9004,436 K298 µs1,116
Read/decode-response/100x64KB574 µs9005,722 K380 µs1,116
Read/decode-response/1000x100B3.19 ms9,00043.2 M2.96 ms11,020
Read/decode-response/1000x4KB3.41 ms9,00044.4 M3.02 ms11,020
Read/decode-response/1000x64KB5.57 ms9,00057.3 M4.92 ms11,020
BenchmarkmacOS p50macOS MallocmacOS InstructionsLinux p50Linux Malloc
Read/decode-model/13,417 ns1233 K2,183 ns28
Read/decode-model/100231 µs1,2003,021 K183 µs1,216
Read/decode-model/10002.37 ms12,00030.0 M1.82 ms12,020

Client stack against an in-process serverClient stack(接 in-process server)

Client/* runs a real KurrentDBClient against an in-process server (Benchmarks/OfflineBenchmarks/InProcessServer.swift), plugged in through the package-level ClientSettings.transportProvider. Node selection, the retry wrapper, the shared connection, the generated gRPC client and the response sequences run as they do against a node. The server answers from responses serialised in advance and never parses a request, but like a node it takes each request whole before answering, and it fails a call whose request does not carry the expected number of messages. There are no sockets and no HTTP/2 framing, so what is left is the cost of the client and of grpc-swift. GRPC/* makes the same calls with the generated gRPC client alone, on the same kind of connection behind the same AnyClientTransport: Client/* minus GRPC/* is what the SDK adds above that transport, whose own forwarding both include; it forwards nothing per message. Both also include one copy of every message, each way, between the in-process transport's [UInt8] and the Posix transport's bytes type, which a real connection does not make. Measured on 2026-10-26 on the code of PR #182: macOS in two runs with the same allocation counts and p50s at most 9.4% apart (the second run against the first; the tables show the first); Linux in run 40282079929 (AMD EPYC 7763, 4 vCPU, Ubuntu 26.04).

Client/* 用真的 KurrentDBClient 接 in-process server(Benchmarks/OfflineBenchmarks/InProcessServer.swift),透過 package 層級的 ClientSettings.transportProvider 接上。node 選擇、retry、共用連線、生成的 gRPC client 與回應序列都跟接真 node 時一樣在跑。server 用事先序列化好的回應作答,從不解析請求,但會像真 node 一樣先收完整個請求再回應,請求的訊息數不對就讓呼叫失敗;沒有 socket、也沒有 HTTP/2 framing,所以剩下的就是 client 與 grpc-swift 本身的成本。GRPC/* 在同樣的連線、同樣的 AnyClientTransport 後面,只用生成的 gRPC client 做同樣的呼叫:Client/* 減掉 GRPC/* 就是 SDK 在這個 transport 之上多出來的部分,transport 本身的轉發兩邊都有,而且每則訊息不會另外轉發。兩者也都包含每則訊息在 in-process transport 的 [UInt8] 與 Posix transport 的 bytes 型別之間來回各複製一次,這是真連線不會有的。量測於 2026-10-26,使用 PR #182 的程式碼:macOS 跑了兩次,配置次數相同、p50 最多相差 9.4%(第二次對第一次;表中是第一次);Linux 為 run 40282079929(AMD EPYC 7763, 4 vCPU, Ubuntu 26.04)。

Two things these numbers do not show. Memory: the in-process transport queues messages without a limit, so the server can run ahead of a slow reader; against a node, the HTTP/2 flow-control window limits how far it can. The server's share of a deadline: requests reach the in-process server without their grpc-timeout header, so only the client's timer runs; a node runs its own on its own machine. Dropping the header copies the request metadata of a call with a deadline, which a real connection does not do.

這些數字不包含兩件事。記憶體:in-process transport 的佇列沒有上限,server 可以跑在讀得慢的一方前面;接真 node 時,HTTP/2 的 flow-control window 會限制 server 能超前多少。deadline 在 server 端的成本:請求送到 in-process server 時已拿掉 grpc-timeout header,所以只有 client 的計時 task 在跑;真 node 的計時在它自己的機器上。拿掉 header 會複製一次有 deadline 的呼叫的請求 metadata,這是真連線不會做的。

BenchmarkmacOS p50macOS MallocmacOS InstructionsLinux p50Linux Malloc
Client/append/1x100B58 µs110623 K142 µs122
Client/append/100x100B299 µs1,1074,841 K576 µs823
Client/append-with-deadline/1x100B70 µs129709 K208 µs141
Client/read-buffered/1x100B62 µs107655 K160 µs130
Client/read-buffered/100x100B730 µs1,61010.2 M1.23 ms1,636
Client/read-buffered/1000x100B6.54 ms15,11995.8 M10.97 ms15,151
Client/read-lazy/1x100B68 µs117755 K199 µs139
Client/read-lazy/100x100B996 µs1,80812.7 M2.62 ms1,836
Client/read-lazy/1000x100B9.27 ms17,114122.2 M24.92 ms18,015
BenchmarkmacOS p50macOS MallocmacOS InstructionsLinux p50Linux Malloc
GRPC/append/1x100B52 µs98593 K123 µs105
GRPC/append/100x100B284 µs9964,571 K479 µs708
GRPC/read/1x100B50 µs85570 K128 µs101
GRPC/read/100x100B521 µs1,4797,721 K830 µs1,496
GRPC/read/1000x100B4.92 ms14,08572.0 M6.95 ms14,109

Method量測方法

The suite is Benchmarks/OfflineBenchmarks, built with ordo-one/benchmark in release mode. The Benchmarks workflow runs it on ubuntu-24.04 on every push to main, publishes the markdown report to the job summary, and uploads two measures per benchmark to Bencher: latency (p50 wall clock in ns, bounds p0/p90) and the p50 malloc count, as allocations or, for the client-stack benchmarks, allocations-concurrent. On a push to main two thresholds can fail the run: a t-test on latency (upper boundary 0.99 over the last 64 runs, once a benchmark has 10) when the result lands outside the band of recent history, and a percentage test on allocations (upper boundary 1% against the previous run) when the count grows by more than 1%. Allocation counts are deterministic for a given toolchain and runner image, so growth comes from a code change, a new Swift patch release or a new runner image; each alerts once, and the next run compares against the new count. The client-stack benchmarks are the exception: they allocate on several concurrent tasks, and on the 4-vCPU runner a lazy read's count per iteration spreads over about 10%, so allocations-concurrent keeps their history without a threshold and the latency test covers them.

套件是 Benchmarks/OfflineBenchmarks,用 ordo-one/benchmark 以 release 模式建置。Benchmarks workflow 每次 push 到 main 就在 ubuntu-24.04 執行,把 markdown 報表寫進 job summary,並對每個 benchmark 上傳兩個 measure 到 Bencher:latency(p50 wall clock,單位 ns,區間 p0/p90)與 p50 配置次數;配置次數一般記為 allocations,client stack 的 benchmark 記為 allocations-concurrent。push 到 main 時有兩個門檻會讓執行失敗:latency 上的 t-test(最近 64 次、上界 0.99,累積滿 10 次才開始)在結果落在歷史區間外時觸發;allocations 上的 percentage 門檻(對前一次、上界 1%)在配置次數增加超過 1% 時觸發。同一套 toolchain 與 runner image 上的配置次數是固定的,所以增加只會來自程式碼變更、新的 Swift patch 版本或新的 runner image;每種情況只會觸發一次,下一次執行就以新的次數為基準。client stack 的 benchmark 是例外:它們在多個並行的 task 上配置記憶體,在 4 vCPU 的 runner 上 lazy read 每次迭代的配置次數分散約 10%,所以 allocations-concurrent 只保留歷史、不設門檻,由 latency 的門檻把關。

How to run the suite locally on macOS or Linux, filter benchmarks and compare baselines: BENCHMARKS.md.

在 macOS 或 Linux 本機執行、篩選 benchmark 與比較 baseline 的步驟見 BENCHMARKS.md。