Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 34 additions & 9 deletions .github/workflows/codspeed.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ name: CodSpeed
on:
push:
branches: [main]
paths-ignore: ["**.md", "docs/**"]
pull_request:
branches: [main]
paths-ignore: ["**.md", "docs/**"]
# Allows CodSpeed to trigger backtest performance analysis to generate
# initial baseline data.
workflow_dispatch:
Expand All @@ -20,11 +22,31 @@ concurrency:
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
runs-on: ubuntu-latest
name: Run CodSpeed benchmarks (${{ matrix.shard.name }})
runs-on: ubuntu-24.04
strategy:
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
# 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:
Comment thread
greptile-apps[bot] marked this conversation as resolved.
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:
Expand All @@ -37,36 +59,39 @@ jobs:
- name: Install tools (protoc, cargo-codspeed)
uses: taiki-e/install-action@v2
with:
tool: protoc@${{ env.PROTOC_VERSION }},cargo-codspeed@4.7.0
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
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
-p wacore -p wacore-binary -p wacore-libsignal
-p wacore-appstate -p wacore-noise
$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
run: cargo codspeed run $SHARD_PACKAGES

Comment thread
coderabbitai[bot] marked this conversation as resolved.
# 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
runs-on: ubuntu-latest
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.
Expand Down Expand Up @@ -97,7 +122,7 @@ jobs:
- name: Install tools (protoc, cargo-codspeed)
uses: taiki-e/install-action@v2
with:
tool: protoc@${{ env.PROTOC_VERSION }},cargo-codspeed@4.7.0
tool: protoc@${{ env.PROTOC_VERSION }},cargo-codspeed@5.0.1

- name: Cache Rust registry
uses: Swatinem/rust-cache@v2
Expand Down
34 changes: 12 additions & 22 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ cbc = { version = "0.2", features = ["alloc"] }
chrono = { version = "0.4", default-features = false }
compact_str = { version = "0.9", default-features = false }
ctr = { version = "0.10", default-features = false }
divan = { package = "codspeed-divan-compat", version = "4.7.0" }
divan = { package = "codspeed-divan-compat", version = "5.0.1" }
env_logger = { version = "0.11", default-features = false }
event-listener = { version = "5", default-features = false }
flate2 = { version = "1.1.9", default-features = false, features = ["zlib-rs"] }
Expand Down
25 changes: 16 additions & 9 deletions wacore/benches/send_receive_benchmark.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
use async_trait::async_trait;
use buffa::Message as ProtoMessage;
use std::collections::HashMap;

/// SipHash with fixed keys: the default RandomState seeds per process, so
/// bucket layout (and thus cache behavior) differed between benchmark runs.
type DetState = std::hash::BuildHasherDefault<std::hash::DefaultHasher>;
type DetHashMap<K, V> = HashMap<K, V, DetState>;
use std::hint::black_box;
use wacore::client::context::{GroupInfo, SendContextResolver};
use wacore::messages::MessageUtils;
Expand Down Expand Up @@ -143,15 +148,15 @@ impl Runtime for BenchRuntime {
struct MemIdentityStore {
key_pair: IdentityKeyPair,
reg_id: u32,
identities: std::sync::Arc<std::sync::Mutex<HashMap<ProtocolAddress, IdentityKey>>>,
identities: std::sync::Arc<std::sync::Mutex<DetHashMap<ProtocolAddress, IdentityKey>>>,
}

impl MemIdentityStore {
fn new(key_pair: IdentityKeyPair, reg_id: u32) -> Self {
Self {
key_pair,
reg_id,
identities: std::sync::Arc::new(std::sync::Mutex::new(HashMap::new())),
identities: std::sync::Arc::new(std::sync::Mutex::new(DetHashMap::default())),
}
}
}
Expand Down Expand Up @@ -188,7 +193,7 @@ impl IdentityKeyStore for MemIdentityStore {
}
}

struct MemPreKeyStore(HashMap<PreKeyId, PreKeyRecord>);
struct MemPreKeyStore(DetHashMap<PreKeyId, PreKeyRecord>);

#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
Expand All @@ -209,7 +214,7 @@ impl PreKeyStore for MemPreKeyStore {
}
}

struct MemSignedPreKeyStore(HashMap<SignedPreKeyId, SignedPreKeyRecord>);
struct MemSignedPreKeyStore(DetHashMap<SignedPreKeyId, SignedPreKeyRecord>);

#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
Expand All @@ -233,7 +238,9 @@ impl SignedPreKeyStore for MemSignedPreKeyStore {
/// Bench fixture wrapping shared session state — see `MemIdentityStore`
/// for the rationale.
#[derive(Clone, Default)]
struct MemSessionStore(std::sync::Arc<std::sync::Mutex<HashMap<ProtocolAddress, SessionRecord>>>);
struct MemSessionStore(
std::sync::Arc<std::sync::Mutex<DetHashMap<ProtocolAddress, SessionRecord>>>,
);

#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
Expand All @@ -250,7 +257,7 @@ impl SessionStore for MemSessionStore {
}
}

struct MemSenderKeyStore(HashMap<SenderKeyName, SenderKeyRecord>);
struct MemSenderKeyStore(DetHashMap<SenderKeyName, SenderKeyRecord>);

#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
Expand Down Expand Up @@ -300,8 +307,8 @@ impl User {
let spk_record =
SignedPreKeyRecord::new(spk_id, Timestamp::from_epoch_millis(0), &spk_pair, &spk_sig);

let mut prekeys = MemPreKeyStore(HashMap::new());
let mut signed_prekeys = MemSignedPreKeyStore(HashMap::new());
let mut prekeys = MemPreKeyStore(DetHashMap::default());
let mut signed_prekeys = MemSignedPreKeyStore(DetHashMap::default());
futures::executor::block_on(async {
prekeys.save_pre_key(pk_id, &pk_record).await.unwrap();
signed_prekeys
Expand All @@ -324,7 +331,7 @@ impl User {
prekeys,
signed_prekeys,
sessions: MemSessionStore::default(),
sender_keys: MemSenderKeyStore(HashMap::new()),
sender_keys: MemSenderKeyStore(DetHashMap::default()),
prekey_pair: pk_pair,
signed_prekey_pair: spk_pair,
signed_prekey_sig: spk_sig.to_vec(),
Expand Down
Loading
Loading