Overview
wacore is the core WhatsApp protocol implementation for whatsapp-rust. It’s designed to be platform-agnostic with no runtime dependencies on Tokio or specific databases, making it portable across different async runtimes and storage backends.
Philosophy
wacore contains all the core logic for:- Binary protocol encoding/decoding
- Cryptographic primitives (AES-GCM, Signal Protocol)
- IQ protocol types and specifications
- Runtime abstraction (
Runtimetrait for pluggable async executors) - Network abstractions (
Transport,TransportFactory,HttpClienttraits) - State management traits (
Backend,SignalStore,AppSyncStore, etc.) - Message builders and parsers
futures, async-trait, async-lock, and async-channel for async primitives. This makes wacore portable to any async runtime, including WASM targets. The main whatsapp-rust crate provides concrete implementations (Tokio runtime, SQLite storage, ureq HTTP client, Tokio WebSocket transport).
Key Exports
Re-exported Crates
Derive Macros
EmptyNode- For protocol nodes with only a tag (no attributes)ProtocolNode- For protocol nodes with string attributesStringEnum- For enums with string representations
Framing
Core Modules
Protocol & Binary
Binary protocol
Type-safe protocol node builders and parsers
xml
XML utilities for protocol nodes
Cryptography
Signal Protocol
Signal Protocol implementation for E2E encryption
noise
Noise Protocol XX for handshake encryption
IQ Protocol
blocklist- Block/unblock contactschatstate- Typing indicators, presencecontacts- Contact synchronizationdirty- Dirty bit checkinggroups- Group management operationskeepalive- Connection keepalivemediaconn- Media server connectionsmex- Message Extension queriespassive- Passive IQ handlingprekeys- Prekey distributionprivacy- Privacy settingsprops- Server properties and A/B experiment configs. The companionabpropsmodule ships the typed flag registry, andprops::WATCHEDlists the flags the library itself readsspam_report- Spam reportingtctoken- Temporary client tokensusync- User synchronization. Ships a typed query/response model (UsyncQuery,UsyncProtocol,UsyncResponse, …) covering every USync subprotocol observed in WhatsApp Web (device lists, contact/LID/username lookup, status, bot profiles, features). See USync
A/B props registry
wacore::iq::abprops is an auto-generated, vendored snapshot of WhatsApp Web’s A/B-props registry. Each WA Web registry becomes a pub mod (currently web), and each flag becomes a typed pub const AbProp named after its key in screaming snake case (for example web::PRIVACY_TOKEN_SENDING_ON_GROUP_CREATE). Every entry carries:
name— the wire name the server uses in<prop config_code="…"/>code— the numericconfig_codevalue_type—AbPropType::Bool,Int,Float, orStrdefault— the registry default applied when the server omits the flag
AbPropsCache (is_enabled, get, get_int, watch, watch_many) instead of raw u32 codes. The library only materializes the consts you reference, so the rest of the ~2,000-flag registry adds no binary weight.
wacore::iq::props::WATCHED is the slice of flags the library itself reads — useful as a starting point if you want to extend the cache’s interest set. A small props::stale module preserves flags the client still references but that the current WA Web bundle no longer ships.
Message Handling
messages
Message encryption and decryption
send
Message sending logic
download
Media download and decryption
upload
Media encryption and upload preparation
State Management
Device- Core device state structureDeviceCommand- State mutation commandstraits- Backend trait definitions (Backend,SessionStore, etc.)ab_props- In-memory A/B experiment property cache (AbPropsCache), populated fromfetch_props()on each connection. Features query this cache using typed flag constants from theabpropsregistry (e.g., privacy token attachment on group operations).
Runtime & Networking
runtime module defines the Runtime trait that all async operations go through:
Helper functions
Theruntime module also provides two runtime-agnostic helper functions that work with any Runtime implementation:
timeout — Race a future against a deadline using the runtime’s sleep implementation:
Ok(value) if the future completes before the duration, or Err(Elapsed) if it times out. This is the runtime-agnostic replacement for tokio::time::timeout — the library uses it internally for phash validation, media retry timeouts, session establishment, and IQ response waiting.
blocking — Offload a blocking closure and return its result:
Runtime::spawn_blocking with a oneshot channel to ferry the closure’s return value back to the caller. On WASM, the closure runs inline since there is only one thread.
The net module defines the networking abstractions:
Transport— active connection for sending/receiving raw bytesTransportFactory— creates new transport instances and event streamsHttpClient— HTTP request execution (buffered and streaming)TransportEvent—Connected,DataReceived(Bytes),Disconnected
target_arch = "wasm32"), all Send bounds are automatically removed.
Connection & Pairing
handshake
Noise Protocol handshake
pair
QR code pairing
pair_code
Phone number pairing
shortcake
SHORTCAKE_PASSKEY companion-linking crypto (ephemeral identity commit/reveal, verification code, HKDF/AES-GCM pairing envelope, handoff proof)
net
Transport and HTTP client traits
shortcake is pure and platform-agnostic (no Tokio, wasm-buildable) — it only builds/parses the deterministic protocol payloads. The one non-reproducible step, obtaining a WebAuthn assertion, lives in whatsapp_rust::passkey (the PasskeyAuthenticator seam). See Authentication — Passkey linking.
Specialized Features
appstate
App state synchronization (contacts, settings)
history_sync
Message history synchronization
usync
User device list synchronization
prekeys
Signal Protocol prekey generation
history_sync types
HistoryMsgSecretRecord carries the per-message secret extracted by the history-sync pipeline for E2E key expansion.
SecretBytes avoids heap allocation for secrets ≤32 bytes (covering the typical 32-byte Signal message secret) and falls back to Vec<u8> for larger values.
Deref<Target=[u8]>, From<&[u8]>, From<Vec<u8>>, PartialEq, Eq, and Debug. Use as_slice() or deref for reads; into_vec() to convert to Vec<u8>.
For Bytes-owned input, wacore::history_sync also exports:
own_user scopes from_me classification to the local account’s JID (pass None if that check should be skipped). retain_blob controls whether the returned HistorySyncResult keeps a Bytes handle on the original compressed input (compressed_bytes, consumed by LazyHistorySync) — pass false to drop it immediately after parsing.
process_history_sync_bytes is a Bytes-based sibling of process_history_sync (unchanged) — both accept every record. process_history_sync_bytes_filtered additionally takes a record_filter predicate that runs against a borrowed HistoryMsgSecretRecordRef<'a> before its owned HistoryMsgSecretRecord counterpart is allocated, so a caller that owns a retention policy (for example, dropping records outside a retention window) can reject them without paying for materialization:
conversation_index is a zero-based counter shared across records from the same conversation, letting a filter cache per-conversation classification instead of recomputing it per record. See Architecture — RAM optimization layers for the measured allocation win.
If you want to skip the intermediate owned HistoryMsgSecretRecord entirely, wacore::history_sync also exports a streaming visitor path:
process_history_sync_bytes_with_record_visitor takes a plain FnMut closure for the common case. The closure builds your own row directly from the borrowed HistoryMsgSecretRecordRef. This ensures the owned HistoryMsgSecretRecord is never allocated. process_history_sync_bytes_with_record_sink takes a full HistoryMsgSecretRecordVisitor implementation instead of a closure. The visit return value reports the byte size you retained for that record for accounting. The optional reserve and retained_item_size hooks let you size your own collection (e.g., a batched SQL insert buffer) up front rather than growing it one record at a time. Reach for process_history_sync_bytes_filtered (above) when a simple accept/reject predicate is enough. Reach for the visitor/sink pair when you also want to avoid materializing the owned record.
Time
time module centralizes all timestamp handling. It exposes two independent clocks that should not be confused:
- Wall clock (
TimeProvider,now_millis,now_utc) — answers “what time is it?”. May jump backwards across NTP syncs, manual adjustments, or leap-second smearing. Backed bychrono::Utc::now()on native targets. - Monotonic clock (
MonotonicProvider,Instant) — answers “how much time passed?”. Never moves backwards and is immune to NTP adjustments. Backed bystd::time::Instanton native targets.
Wall-clock functions
Custom wall-clock provider
Implement theTimeProvider trait and call set_time_provider before any time functions are used:
set_time_provider returns Err if a provider has already been set. The provider uses OnceLock internally, so it can only be configured once per process.wasm32 targets the epoch fallback is not cached, so a later set_time_provider call still takes effect even if a timestamp was read during startup. A typical browser embedder wires Date.now() through wasm-bindgen:
Instant
Instant is a portable monotonic instant that replaces std::time::Instant, which is unavailable on wasm32-unknown-unknown. On native targets it wraps std::time::Instant via the default MonotonicProvider and exposes nanosecond resolution. The Instant type is Copy and supports Add<Duration> and Sub<Instant> (returning Duration), with saturating arithmetic to prevent overflow.
Custom monotonic provider
Implement theMonotonicProvider trait and call set_monotonic_provider before any Instant is captured. The provider must return nanoseconds since an arbitrary fixed reference and never return a smaller value than a previous call.
wasm32 targets without a registered provider, the fallback derives nanos from the wall clock (clamped to non-decreasing) and quantizes to milliseconds. Embedders targeting browsers, Node, or WASI should register a sub-millisecond provider for accurate latency measurements.
set_monotonic_provider returns Err if a provider has already been set. Like the wall-clock provider, it uses OnceLock internally and can only be configured once per process.Utilities
client- Client context traitsib- Identity byte utilitiesproto_helpers- Protobuf conversion helpersreporting_token- Reporting token generationrequest- Request building utilitiesstanza- Common stanza builderssticker_pack- Sticker pack creation helpers (see sticker packs)time- Pluggable wall-clock and monotonic-clock providers, plus portableInstant(see time above)types- Common type definitions (JID, events, messages). Includestypes::jidutilities for zero-allocation JID comparison (cmp_for_lock_order), buffer-reusing address formatting (write_protocol_address_to), and in-place sorted deduplication (sort_dedup_by_user,sort_dedup_by_device)version- WhatsApp version constantswebp- WebP format utilities (animated sticker detection)
webp module
Thewebp module provides utilities for working with WebP image files. It is re-exported as whatsapp_rust::webp.
is_animated
Detects whether a WebP file contains animation frames by parsing RIFF/VP8X headers and scanning for ANIM/ANMF chunks.&[u8]
required
Raw WebP file bytes.
true if the WebP file is animated, false otherwise (including for invalid or too-short input).
Example:
This function is used internally by
create_sticker_pack_zip to set the is_animated field on each sticker proto entry. You can also use it directly when you need to classify WebP files before processing.Submodule Packages
wacore is split into several workspace crates:wacore-binary
Location:wacore/binary
Binary protocol encoding/decoding using WhatsApp’s custom format.
CompactString- Re-export ofcompact_str::CompactString, used byJid.user,NodeValue::String, andNodeContent::Stringjid::{Jid, JidRef, Server, JidExt}- WhatsApp JID types.Serveris an enum (#[repr(u8)]) with variants for all known WhatsApp server domains (Pn,Lid,Group,Broadcast,Newsletter,Hosted,HostedLid,Messenger,Interop,Bot,Legacy).JidExtprovides helper methods (is_group(),is_newsletter(), etc.) for both owned and borrowed JID types.jid::parse_jid_ref(s: &str) -> Option<JidRef<'_>>parses the common user/group/LID/bot shapes directly into a borrowedJidRefwith no allocation; it returnsNonefor edge cases the compatibility fallback handles, so callers that need those cases parse vias.parse::<Jid>()instead.Jid’s ownFromStrimpl is built on top ofparse_jid_refnode::{Node, NodeRef, NodeStr, NodeValue, ValueRef, OwnedNodeRef, AttrsVec}- Protocol node types.Node(owned) for building outgoing stanzas,NodeRef(borrowed) for reading received stanzas,NodeStrfor borrowed-or-inline decoded strings,OwnedNodeReffor yoke-based zero-copy self-referential nodes shared asArc<OwnedNodeRef>.AttrsVecisSmallVec<[(Cow<'static, str>, NodeValue); 2]>, the inline-capable backing store forAttrs(≤2 attrs stay on the stack; see Binary Protocol). The entireNodeReftype family (NodeRef,NodeStr,ValueRef,JidRef,NodeContentRef,OwnedNodeRef) implementsserde::Serialize, producing output identical to their owned counterparts — enabling zero-copy serialization without converting toNodefirstbuilder::NodeBuilder- Fluent node builder withnew(&'static str)/new_dynamic(String),attr(),jid_attr(),children(),bytes(),string_content(), andapply_content()chaining methodsmarshal::*- Binary marshaling functionsattrs::{AttrParser, AttrParserRef}- Attribute parsing utilities for ownedNodeand borrowedNodeRefrespectivelytoken- Token dictionary
wacore-libsignal
Location:wacore/libsignal
Signal Protocol implementation for end-to-end encryption.
core- Core session cipher logiccrypto- Cryptographic primitives (HKDF, HMAC, AES)protocol- Protocol message typesstore- Store trait definitions
wacore-noise
Location:wacore/noise
Noise Protocol implementation for handshake encryption. Supports the XX, IK, and XXfallback patterns to match WhatsApp Web’s behavior on cold start, resumed reconnects, and server-driven recovery.
NoiseState- Generic Noise state machineNoiseHandshake- WhatsApp-specific handshake wrapperXxHandshakeState- Three-message XX handshake (cold start / fallback)IkHandshakeState- Resumed IK handshake using a cached server staticXxFallbackHandshakeState- Server-driven recovery from a stale IK static, continuing the existing transcriptIkServerHelloOutcome- EitherContinue(IK succeeds) orFallback(pivot into XXfallback)VerifiedServerCertChain- Output of XX/XXfallback; persisted by the client to enable IK on the next connectHandshakeUtils- Protocol message building/parsingframing- WebSocket frame encodingbuild_edge_routing_preintro- Edge routing helper
wacore-appstate
Location:wacore/appstate
App state synchronization for contacts, settings, and metadata.
process_snapshot- Process full state snapshotsprocess_patch- Apply incremental patchesMutation- State mutation recordsLTHash- LTHash implementation for integrityexpand_app_state_keys- Key derivation
wacore-derive
Location:wacore/derive
Procedural macros for protocol node generation.
Usage in main library
The mainwhatsapp-rust crate uses wacore modules throughout:
Design Principles
Platform-Agnostic
No dependencies on:- Tokio or any async runtime — uses only
futures,async-trait,async-lock,async-channel - Specific database implementations
- File system operations
- Runtime: Tokio (default), async-std, smol, WASM, etc. — implement
Runtime(4 methods) - Storage: SQLite (default), PostgreSQL, in-memory, etc. — implement
Backend(4 sub-traits) - Transport: Tokio WebSocket (default), custom protocols — implement
TransportFactory+Transport - HTTP client: ureq (default), reqwest, surf, etc. — implement
HttpClient
Type Safety
Strong typing throughout:JidwithServerenum for WhatsApp identifiers — server type is an enum variant, not a stringNode/NodeReffor protocol messages (owned / borrowed)- Validated newtypes (e.g.,
GroupSubjectwith length limits) - Enum variants with
StringEnumfor protocol values
Zero-copy where possible
Cow<'static, str>for ownedNode.tagandAttrskeys — known protocol strings (from the token dictionary) are borrowed as static references with zero heap allocation, while unknown strings fall back to ownedStringNodeStr<'a>for borrowedNodeRef.tag,AttrsRefkeys,ValueRef::String, andJidRef.user— a borrowed-or-inline string type where theOwnedvariant usesCompactString(inline up to 24 bytes) instead of heap-allocatedString, reducing allocation pressure during decodingServerenum (#[repr(u8)]) forJid.server— aCopytype that requires zero allocation, replacing the previousCow<'static, str>string-based server fieldOwnedNodeRef— yoke-based self-referential node that owns the decompressed network buffer whileNodeRefborrows string/byte payloads directly from it. Received stanzas flow through the system asArc<OwnedNodeRef>for cheap shared zero-copy access- Zero-copy
Serialize— the entireNodeReftype family (NodeRef,NodeStr,ValueRef,JidRef,NodeContentRef,OwnedNodeRef) implementsserde::Serialize, producing output identical to their owned counterparts. This allows serializing received stanzas directly from the network buffer without converting to ownedNodetypes first. See Binary Protocol — Zero-copy serialization for details NodeReffor borrowed node parsingAttrParserReffor attribute iterationmarshal_reffor encoding without cloning
Benchmarks
The project includes two categories of benchmarks, both divan suites tracked on CodSpeed: protocol-level, measured under the instruction-count and memory instruments, and integration-level, real client operations measured under the instruction-count instrument only.Protocol benchmarks (divan / CodSpeed)
wacore includes a suite of divan benchmarks — through thecodspeed-divan-compat harness — that measure core protocol operations. Locally they run as ordinary divan wall-time benchmarks; in CI the codspeed.yml workflow runs them under CodSpeed’s deterministic instrumentation for low-noise regression tracking, sharded across two jobs (wacore/wacore-noise and wacore-binary/wacore-libsignal/wacore-appstate).
Prerequisites
- Nightly Rust (the project pins
nightly-2026-06-16) - Nothing else for a local
cargo bench— the CodSpeed runner supplies the Valgrind-based instrumentation in CI
Bench auto-discovery is off (
autobenches = false): only the explicit [[bench]] targets in each crate’s Cargo.toml are built, so a stray file under benches/ is never compiled as a phantom bench.Available suites
Running protocol benchmarks
Integration benchmarks
Thebench-integration suite (tests/bench-integration/benches/integration.rs) drives real client operations end-to-end against a mock server. It is a divan bench target as well, so it reports through CodSpeed rather than the custom timing/allocation binary it replaced — under the instruction-count instrument only, so it yields no allocation figures. It installs a DeterministicAlloc global allocator whose realloc always allocates-and-copies — the system allocator’s in-place growth depends on live heap layout and would otherwise be charged to the benchmark as run-to-run memory noise.
Scenarios
Running integration benchmarks
These benchmarks cannot run without the mock server (Bartender) reachable atMOCK_SERVER_URL, so in practice they execute in CI under cargo codspeed run:
Integration benchmarks require the
danger-skip-tls-verify feature, which is enabled automatically via the bench-integration crate’s Cargo.toml. tests/bench-integration is not a workspace default member, so -p bench-integration is required — a bare invocation discovers no benchmarks for it.Allocation optimizations
The library includes several allocation-reduction strategies that the integration benchmarks track:- Shared thread-local zlib pool — The one-shot binary protocol decompressor (
decompress_zlib_pooled) and the streamingInflateReader(used byNodeStream, see Binary Protocol — Streaming Decode) share one pool ofzlib_rs::Inflatestates per thread instead of keeping separate thread-local states, so a thread that runs both doesn’t retain two blocks. Each state is ~47.5 KB.warm_pool()builds this thread’s state ahead of its first compressed payload, while the heap is still fresh, rather than leaving the first large frame to find one.set_pool_retention(n)sets how many states a thread parks between uses (default1,0= never park; takes effect on the next park).drain_pool()releases this thread’s parked states, returning their memory.parked_states()reports how many this thread currently holds CompactStringfor JIDs — JID user fields usecompact_str::CompactStringwhich stores strings up to 24 bytes inline (no heap allocation), covering the vast majority of phone numbers and LID identifiersServerenum — JID server fields use a#[repr(u8)]enum instead of heap-allocated strings, making JID construction and comparison zero-allocation- Zero-copy node decoding — Received stanzas are decoded as
NodeRefborrowing directly from the network buffer viaOwnedNodeRef(yoke-based self-referential type), avoiding cloning string/byte payloads during decode Arc<str>sharing inLidPnEntry—LidPnEntry.lidand.phone_numberareArc<str>instead ofString.LidPnCachereuses the entry’s ownArcs as the map keys for both lookup directions, so each identifier is allocated once per mapping rather than once as key and again inside the entry. Because this cache is unbounded by design, the saving compounds over the contact base (~40–80 B per mapping, ~0.5–1 MB for 10k contacts). Constructors acceptimpl Into<Arc<str>>, soStringand&strcall sites are unaffected; direct field reads returnArc<str>— use&*entry.lidfor&strcomparisons. Wire/persistence format is unchanged.- Pre-allocated buffers — History sync decompression uses a
compressed_size_hintwith a 4x multiplier for buffer pre-allocation, reducingVecreallocation during decompression - Inline attribute storage —
AttrsusesAttrsVec(SmallVec<[...; 2]>) instead ofVec. Nodes with ≤2 attributes (the common per-recipient fanout shapestoandenc) keep their attributes on the stack alongside the node, eliminating the per-node allocation for attribute storage. Allocation count: −27% per DM stanza (15→11 allocs); −40% for group fanout with 800 participants (4012→2412 allocs) - Secret-presence pre-scan in history sync — before buffa decodes a
HistorySyncMsg(30+ fields, String allocations), a shallow varint walk checks whether the message carriesmessage_secretat any level. Messages without a secret are skipped entirely. Bench (20k messages, secret-dense fixture): −29.5% allocations (56,020 → 39,520), −4.3% allocated bytes. Production blobs are secret-sparse so the saving is larger in practice - Inline storage in
HistoryMsgSecretRecord—chat_idisArc<str>(allocated once per conversation, shared across all records in that conversation),msg_idisCompactString(inline for typical 20–22 char WA IDs on 64-bit targets; smaller inline limit on 32-bit/wasm32), andsecretisSecretBytes(inline for secrets ≤32 bytes, heap for larger) - Filter-before-materialize in history sync —
process_history_sync_bytes_filteredruns a caller-supplied retention predicate against a borrowedHistoryMsgSecretRecordRefbefore the ownedHistoryMsgSecretRecordis built; a rejected record is never materialized. Bench (500-conversation blob, upstream PR’s rejection-heavy fixture): allocation churn 21.61 MiB → 14.00 MiB, allocation count ~84k → ~26k. The reduction scales with how much of the record set the predicate rejects — the defaultprocess_history_sync_bytes(accept-all) sees none of it - Streaming visitor/sink for history-sync records —
process_history_sync_bytes_with_record_visitorandprocess_history_sync_bytes_with_record_sink(via theHistoryMsgSecretRecordVisitortrait) go a step further than the filter predicate above. You can build your own storage row directly from the borrowedHistoryMsgSecretRecordRef. This ensures the intermediate ownedHistoryMsgSecretRecordis never allocated at all. Bench (allocator-instrumented synthetic history extraction): 20.20 MiB → 14.43 MiB allocated (-28.6%); CodSpeed history stream-drain memory: 243.8 KB → 115.2 KB (2.1× less) - Borrowed JID parsing —
jid::parse_jid_refparses common protocol JIDs into aJidRefwith zero allocation, falling back toJid’s owned parser only for edge cases. Cut allocations 4,093 → 97 in the history-sync task’s JID classification path - In-place buffered media decrypt — non-streaming
HttpClientdownloads now authenticate the already-buffered response body and decrypt AES-256-CBC in place (DownloadUtils::verify_and_decrypt_in_place, see download), truncating the MAC/padding tail instead of allocating a second file-sized output buffer. Streaming clients are unaffected. Buffered download/decrypt span: 4.355 MiB → 2.146 MiB - Stack arrays in the MLow codec’s analysis internals —
smpl_nlsf2asizes its scratch buffers by the fixed LPC order (16); it now stores three of its fourVecs as stack arrays instead.smpl_lsf_quant::get_maxi_kbounds itsusedmask byn ≤ 17and now stores it on the stack too, the same fixsmpl_celp::smpl_get_maxi_kalready carries.CelpEncoder::encode_subframeused to allocate avec![0.0f32; 160]scratch buffer fresh inside its rate loop; it now pools that buffer and hoists it out of the loop. None of the three touch floating-point arithmetic — thevoip::mlowgolden-checksum tests pin that. Whole-framemlow_encode: 633 → 534 allocations (−15.6%), 519.8 KB → 499.2 KB (−4.0%) (#1320) - Range storage and inline return for
participant_list_hash—MessageUtils::participant_list_hashrenders every device into one shared arena and sorts range views over it instead of over the individual devices. On 64-bit targets the ranges are nowVec<(u32, u32)>instead ofusizepairs, halving the bytes moved per sort compare; on 32-bit targets such aswasm32,usizeandu32are the same width, so this part of the change is a no-op there. The sort and hash also read the arena as raw bytes instead ofstr, skipping the UTF-8 boundary re-checkstrindexing pays on every probe. The ten-byte result (2:plus eight base64 characters) is returned as aCompactString— every holder in the participant-list-hash pipeline (ResolvedGroupDevices::phash,ResolvedDmDevices::phash,phash_for_stanza,GroupQueryIq::phash) already carried it as one, so returningStringcost an allocation made only to be converted at each call site. This is separate fromUserDeviceList::phash, the server-provided usync device-list hash, which staysOption<String>. 8-device group: 3 allocations / 426 B → 2 / 352 B. 1600 devices: 3 / 83.21 KB → 2 / 70.4 KB. This is a deliberate breaking change:participant_list_hashreturnsCompactStringwhere it returnedStringbefore, andGroupQueryIq::phash/with_phashmove toCompactStringwith it (#1326) - Attributes carried by value in pairing acks —
PairUtils::build_ack_nodeandbuild_ack_node_refclone or convert theto/idNodeValuedirectly instead of rendering each throughto_string()first. ANodeValuealready holds either an inlineCompactStringor a structuredJid, so the round trip throughStringcopied bytes that were already in the right shape. A pairing ack now costs 0Stringallocations instead of 3 (#1326) - Inline
<enc>node storage in message classification —classify_incoming_messagecollects a received stanza’s<enc>nodes into aSmallVec<[&NodeRef; 4]>instead of a heap-allocatedVec. A fan-out addressed to us carries at most one<enc>per copy we can read — direct children plus this device’s entry under<participants><to>— so every stanza shape observed in practice stays within the four inline slots and allocates nothing. The bound is on the storage, not the input: a stanza with more than four matching<enc>nodes would still spill to the heap (#1326) - Inline PN↔LID pairs and tctoken candidates in history sync —
HistoryLidMapping.phone_number/.lidandTcTokenCandidate.idmove fromStringtoCompactString, andTcTokenCandidate.tc_tokenfromVec<u8>toSmallVec<[u8; 32]>. Phone and LID user parts are typically 11–16 digits and live captures put a tctoken at 16–24 bytes, so both fit inline for the common case on 64-bit targets (CompactString’s inline capacity is smaller on 32-bit/wasm32; a longer value in either type spills to the heap) — the PN↔LID harvest (plus the two indexesdedupe_lid_mappingsclones them into for conflict resolution) and the tctoken candidate extraction allocate nothing for typical inputs.extract_conversation_fieldsalso reuses one borrowedparse_jid_refscan per conversation for both the PN/LID guess and the tctoken chat-kind guard, instead of building an ownedJidjust to readserver. On thewhatsapp-rustside,HistorySecretSeedCollectornow caches the previous group message’s resolved(raw participant, Arc<str> sender)pair, since group history arrives in bursts from the same sender — a repeat of the raw field reuses the cachedArcinstead of re-parsing the JID and re-rendering it. Bench (bench_process_history_sync, rebuilt mixed DM/group fixture): 7507 → 3507 allocations (-53%). This is a deliberate breaking change: both new typesDereftostr/[u8], so a call site that consumes them as&str/&[u8]via deref coercion (method calls,.parse(), comparisons against&str) compiles unchanged; a call site typed explicitly as&String/&Vec<u8>does not coerce and needs to borrow as&str/&[u8]instead, and code that moves a field out into an ownedString/Vecneeds.into_string()/.into_vec()(#1349)
CI integration
Both benchmark types run from a single workflow,.github/workflows/codspeed.yml, on every push to main and on pull requests. CodSpeed stores the baselines and reports per-PR deltas.
Protocol benchmarks:
- Sharded across two jobs —
wacore/wacore-noiseandwacore-binary/wacore-libsignal/wacore-appstate— so one shard failing doesn’t cancel the other; CodSpeed merges the shards into one run - Run under both the simulation (instruction-count) and memory instruments
MALLOC_*environment variables freeze glibc malloc’s adaptive thresholds, whose allocation-history-dependent decisions otherwise read as spurious deltas- The
wacore/wacore-noiseshard passes--features voip-mlow,bench-internals. Without afeaturesentry, a CodSpeed shard builds its bench targets under only the crate’s default features. Bothvoip_benchmarkandsframe_varint_benchmarkdeclarerequired-features, and cargo silently skips a bench target whose required features are off instead of erroring. Before this fix, the entire VoIP media plane — MLow encode/decode, E2E-SRTP, SFrame, and thecodec_stagesper-stage rows — built nowhere and uploaded nothing (#1320)
- Run in their own job with a Bartender mock server as a Docker service container
- Simulation instrument only, no memory instrument:
send_and_receivedrives a live async round-trip whose in-flight pipeline buffers straddle divan sample boundaries, so its memory figure tracked runner scheduling rather than code and tripped false regressions. The deterministic memory signal lives in the single-threaded unit benches instead
Next steps
waproto
Protocol Buffers message definitions
Binary Protocol
Type-safe protocol node pattern
Architecture
IqSpec request/response pairing
State Management
Device state and commands