1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
name: CodSpeed
on:
push:
branches:
paths-ignore:
pull_request:
branches:
paths-ignore:
# Allows CodSpeed to trigger backtest performance analysis to generate
# initial baseline data.
workflow_dispatch:
permissions:
contents: read
id-token: write # for OpenID Connect authentication with CodSpeed
concurrency:
group: codspeed-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
env:
CARGO_TERM_COLOR: never
PROTOC_VERSION: "3.25.3"
# Freeze glibc malloc's adaptive thresholds: mmap/trim/arena decisions vary
# with allocation history and read as spurious instruction/memory deltas
# under the deterministic instruments (codspeed.io/docs -> reducing-variance).
MALLOC_ARENA_MAX: "1"
MALLOC_MMAP_THRESHOLD_: "131072"
MALLOC_TRIM_THRESHOLD_: "131072"
MALLOC_TOP_PAD_: "131072"
jobs:
benchmarks:
name: Run CodSpeed benchmarks (${{ matrix.shard.name }})
runs-on: ubuntu-24.04
strategy:
# One shard failing must not cancel the other: CodSpeed merges shards
# into a single run, and a cancelled shard leaves it incomplete.
fail-fast: false
# Shards split by package within one workflow (same OIDC auth), halving
# the serial wall time; CodSpeed merges shard results into one run
# (codspeed.io/docs -> sharded-benchmarks).
matrix:
shard:
- name: core
packages: -p wacore -p wacore-noise
- name: proto-signal
packages: -p wacore-binary -p wacore-libsignal -p wacore-appstate
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly-2026-06-16
- name: Install tools (protoc, cargo-codspeed)
uses: taiki-e/install-action@v2
with:
tool: protoc@${{ env.PROTOC_VERSION }},cargo-codspeed@5.0.1
- name: Cache Rust registry
uses: Swatinem/rust-cache@v2
with:
prefix-key: ${{ runner.os }}-cargo-codspeed-${{ matrix.shard.name }}
cache-targets: "false"
# simulation and memory share the same instrumented build, so one
# build covers both instruments.
- name: Build the benchmark targets
env:
SHARD_PACKAGES: ${{ matrix.shard.packages }}
run: >
cargo codspeed build -m simulation -m memory
$SHARD_PACKAGES
# Both instruments run serially in a single invocation so each
# benchmark uploads as one run carrying CPU and memory metrics.
- name: Run the benchmarks
uses: CodSpeedHQ/action@v4
env:
SHARD_PACKAGES: ${{ matrix.shard.packages }}
with:
mode: simulation,memory
run: cargo codspeed run $SHARD_PACKAGES
# Integration benches drive the real async client against the bartender mock
# server, so they need the service container + MOCK_SERVER_URL. Kept as a
# separate job so the unit-bench job above stays fast and mock-server-free.
integration-benchmarks:
name: Run CodSpeed integration benchmarks
# Fork PRs get no secrets, so they can't pull the private mock-server image.
# `credentials.password` rejects the empty string while the job template is
# being prepared, so the job fails before any step runs and a step-level
# `if:` cannot save it — the guard has to sit here. Same condition as e2e.yml.
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
runs-on: ubuntu-24.04
# These benches drive the real client (connect/reconnect handshakes) under
# Valgrind, so they are slow; cap the job so a stuck run can't hang CI
# indefinitely. A healthy run finishes well under this even on slow runners.
timeout-minutes: 30
permissions:
contents: read
id-token: write # for OpenID Connect authentication with CodSpeed
services:
mock-server:
# Pinned by digest, not `:latest`: send_and_receive measures a live
# round-trip and the mock server is the other end of it, so a tag moving
# under us shifts the instruction counts with no code change and
# pollutes the CodSpeed baseline. Same freeze rationale as the malloc
# thresholds above. Generated from .github/bartender-image.txt by
# scripts/ci/sync-bartender-image.sh.
image: ghcr.io/whiskeysockets-devtools/bartender@sha256:9f3a49dd9f944414fbda50571bba5f66f2ab6fdc48001e6803d4c09a1042c78e
credentials:
username: ${{ github.actor }}
password: ${{ secrets.BARTENDER_GHCR_TOKEN }}
ports:
- 8080:8080
env:
CHATSTATE_TTL_SECS: "3"
options: --log-driver none
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly-2026-06-16
- name: Install tools (protoc, cargo-codspeed)
uses: taiki-e/install-action@v2
with:
tool: protoc@${{ env.PROTOC_VERSION }},cargo-codspeed@5.0.1
- name: Cache Rust registry
uses: Swatinem/rust-cache@v2
with:
prefix-key: ${{ runner.os }}-cargo-codspeed-integration
cache-targets: "false"
- name: Wait for mock server
run: |
for i in $(seq 1 30); do
if curl -sk https://localhost:8080/ > /dev/null 2>&1; then
echo "Mock server is ready"
exit 0
fi
sleep 1
done
echo "Mock server failed to become ready"
exit 1
# Integration benches run only the simulation (instruction-count)
# instrument, NOT memory. send_and_receive drives a live async round-trip,
# whose in-flight pipeline buffers (TLS/websocket/noise/decrypt/dispatch)
# straddle divan sample boundaries, so its peakMemoryBytes tracked runner
# scheduling/CPU rather than code (freeCalls ~= 2x allocCalls; cross-CPU
# runs tripped a false memory regression on every send-path PR). Simulation
# counts are deterministic under Valgrind regardless of host CPU and stay a
# meaningful end-to-end signal; the deterministic memory signal lives in the
# single-threaded unit benches (other job), which keep the memory instrument.
- name: Build the benchmark targets
run: cargo codspeed build -m simulation -p bench-integration
# `-p bench-integration` is required: `tests/bench-integration` is not a
# workspace default-member, so a bare `cargo codspeed run` discovers no
# benchmarks for it ("No benchmarks found for the simulation mode").
- name: Run the benchmarks
uses: CodSpeedHQ/action@v4
env:
MOCK_SERVER_URL: "wss://127.0.0.1:8080/ws/chat"
RUST_LOG: warn
with:
mode: simulation
run: cargo codspeed run -p bench-integration