Skip to content

perf: split notification handler, boxed-slice children, interest-based props - #553

Merged
jlucaso1 merged 6 commits into
mainfrom
perf/futures-decode-props
Apr 15, 2026
Merged

perf: split notification handler, boxed-slice children, interest-based props#553
jlucaso1 merged 6 commits into
mainfrom
perf/futures-decode-props

Conversation

@jlucaso1

@jlucaso1 jlucaso1 commented Apr 15, 2026

Copy link
Copy Markdown
Collaborator

Summary

Three optimizations targeting handler futures, decoder layout, and props parsing.

Full optimization journey (original baseline -> now, all PRs combined):

Metric Original Now Delta
DHAT total bytes 37.9 MB 27.2 MB -10.7 MB (-28.3%)
DHAT total blocks 158K 88K -70K (-44.5%)

This PR alone:

Metric Before After Delta
DHAT total bytes 28.1 MB 27.2 MB -963 KB (-3.4%)
connect_to_ready bytes 3.57 MB 3.40 MB -166 KB (-4.6%)

Changes

1. Split notification handler into per-type async functions

  • Extract each notification type (encrypt, server_sync, account_sync, etc.) into a separate async fn
  • Compiler no longer sizes the dispatcher future for all 14 arms simultaneously
  • server_sync is now a sync fn (it only spawns a task, no .await)
  • Structural improvement for concurrent load; DHAT total unchanged (noise)

2. Box<[NodeRef]> for child nodes

  • NodeContentRef::Nodes changed from Box<Vec> to Box<[NodeRef]>
  • Boxed slice is 16 bytes (ptr + len) vs Vec's 24 bytes (ptr + len + cap)
  • Decoder builds Vec with exact capacity, converts via into_boxed_slice()
  • Structural improvement; DHAT total within noise

3. Interest-based AB props filtering (-963 KB)

  • PropsResponse now parses experiment props as lightweight (code, CompactString) tuples
  • Sampling props and the full AbPropConfig enum are skipped entirely
  • AbPropsCache uses an interest set: only watched config codes are retained
  • watch()/watch_many() register codes before connect; unregistered props are discarded
  • Architecture ready for future prop queries without materializing all ~1,200 props

Test plan

  • cargo test --all (872 tests pass)
  • cargo clippy --all --tests clean
  • DHAT profiling: -963 KB from props, structural improvements from handler split + boxed slice

Summary by CodeRabbit

  • Refactor

    • Reworked notification processing into dedicated handlers for clearer, more reliable event routing.
    • Reduced parsing/allocation overhead by changing internal child-node representation to a boxed-slice form.
  • New Features

    • A/B experiment handling now applies experiment-only updates with delta/full semantics and interest-based retention.
    • Added APIs to register which experiment codes to watch so only subscribed experiment entries are stored.
    • Props responses now surface experiment entries as code/value pairs and skip non-experiment props.

Extract each notification type's handling into a separate async fn
(handle_encrypt_notification, handle_server_sync_notification, etc.)
so the compiler doesn't size the dispatcher future for all 14 arms
simultaneously. server_sync is now a sync fn (it only spawns a task).

The per-task future size reduction benefits concurrent load scenarios
where many notification tasks are alive simultaneously. DHAT total
is unchanged (+0.9% noise) since the benchmark has low concurrency.
NodeContentRef::Nodes now stores Box<[NodeRef]> (boxed slice, 16 bytes)
instead of Box<Vec<NodeRef>> (boxed Vec, 24 bytes + heap header).
The decoder builds a Vec with exact capacity then converts to boxed
slice via into_boxed_slice(). Saves 8 bytes per node with children
and eliminates excess Vec capacity.

DHAT: within noise (+0.8%), structural improvement.
Previously PropsResponse parsed all ~1,200 AB config properties into
AbPropConfig enum structs, then AbPropsCache stored them all in a
HashMap. No features currently query individual prop values.

Now: props are parsed as lightweight (code, CompactString) tuples.
AbPropsCache uses an interest set -- only watched config codes are
retained, rest are discarded. watch()/watch_many() register codes.
This eliminates ~982KB of parsing + ~811KB of HashMap inserts.

The architecture is ready for future prop queries: call
ab_props.watch(PropCode::FOO) before connect, then
ab_props.is_enabled(PropCode::FOO) to read.

Measured impact:
- DHAT total: 28.1 MB -> 27.2 MB (-963 KB, -3.4%)
- connect_to_ready bytes: 3.57 MB -> 3.40 MB (-166 KB, -4.6%)
@coderabbitai

coderabbitai Bot commented Apr 15, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Refactors A/B props pipeline to consume experiment-only tuples and interest-based watches; updates client to pass delta flag and experiment props; replaces boxed Vec children with boxed slices in binary node types; and splits notification handling into dedicated helper functions preserving prior spawn-and-sync behavior.

Changes

Cohort / File(s) Summary
A/B props pipeline
src/client.rs, wacore/src/iq/props.rs, wacore/src/store/ab_props.rs
PropsResponse now exposes experiment_props: Vec<(u32, CompactString)>; client.fetch_props logs/use response.experiment_props.len() and calls ab_props.apply_props(response.delta_update, response.experiment_props.into_iter()).await; AbPropsCache::apply_responseapply_props(delta_update, props); added interest: RwLock<HashSet<u32>> and watch/watch_many; full-update now sets seeded after insertion; tests updated.
Notification handler refactor
src/handlers/notification.rs
Monolithic match split into dispatcher that calls new helpers: async handle_encrypt_notification, async handle_account_sync_notification, and sync handle_server_sync_notification (retains fire-and-forget spawn semantics); logic relocated verbatim into helpers (version filtering, connection cancellation, shutdown checks).
Binary node representation
wacore/binary/src/decoder.rs, wacore/binary/src/node.rs
Node children storage changed from Box<Vec<NodeRef>> to Box<[NodeRef]>; decoder now builds a Vec and converts to into_boxed_slice(); serializers and accessors adjusted to handle boxed slices.

Sequence Diagram(s)

sequenceDiagram
    participant Notif as NotificationHandler
    participant Backend as LocalBackendState
    participant ConnGen as ConnectionGenerator
    participant Sync as SyncTask
    Notif->>Backend: parse collection nodes / check version
    alt version supported
        Notif->>ConnGen: request connection (cancellable)
        ConnGen-->>Notif: connection or cancelled
        Notif->>Sync: spawn sync task with parsed nodes
        Sync->>Backend: perform sync operations
    else unsupported
        Notif--xBackend: skip sync (no spawn)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I nibbled tuples, sifted codes I prize,
Boxed slices neat, trimmed vectors from my pies,
I routed pings to helpers, spawned the sync so spry,
Watched only what I care for, let the rest pass by —
A happy rabbit hops and gives a joyful cry 🥕

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and accurately summarizes the three main optimizations in the PR: notification handler splitting, boxed-slice node children representation, and interest-based props filtering.
Docstring Coverage ✅ Passed Docstring coverage is 90.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/futures-decode-props

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@jlucaso1 jlucaso1 changed the title perf: split notification handler, boxed-slice children, interest-based props (-28.3% total) perf: split notification handler, boxed-slice children, interest-based props Apr 15, 2026
@jlucaso1

Copy link
Copy Markdown
Collaborator Author

@claude review

@claude

claude Bot commented Apr 15, 2026

Copy link
Copy Markdown

Claude finished @jlucaso1's task in 2m 39s —— View job


Code Review: PR #553

  • Read all changed files and diff
  • Review notification handler split
  • Review boxed-slice children change
  • Review interest-based props filtering
  • Post final review

Bug: Interest set is never populated — all AB props silently dropped

Severity: High

AbPropsCache now filters props through an interest set (watch() / watch_many()), but no production code ever calls watch() or watch_many(). Only test code uses them. This means interest is always empty, so apply_props() discards every prop:

// wacore/src/store/ab_props.rs:67-70
for (code, value) in props {
    if interest.contains(&code) {  // interest is always empty!
        map.insert(code, value);
    }
}

Meanwhile, the codebase actively queries ~12 different config codes via is_enabled_or() and get_int():

  • src/send.rsPRIVACY_TOKEN_ON_ALL_1_ON_1_MESSAGES, NCT_TOKEN_SEND_ENABLED, TCTOKEN_DURATION, TCTOKEN_NUM_BUCKETS, etc.
  • src/features/groups.rsPRIVACY_TOKEN_ON_GROUP_CREATE, PRIVACY_TOKEN_ON_GROUP_PARTICIPANT_ADD, PRIVACY_TOKEN_ONLY_CHECK_LID
  • src/features/contacts.rsPROFILE_PIC_PRIVACY_TOKEN

All of these will now silently fall through to their defaults, which could change behavior around privacy token inclusion, tctoken timing, and group operations.

Fix options:

  1. Add watch_many() calls for all config codes defined in config_codes during client initialization (before fetch_props)
  2. Pre-populate the interest set in AbPropsCache::new() with the known config_codes constants
  3. Remove the interest-based filtering for now and keep a simpler optimization (just the lightweight tuple parsing)

Fix this →


Change 1: Notification handler split — Looks good

The split of handle_notification_impl into per-type async functions is clean and well-motivated. Each match arm now calls a standalone function, so the compiler doesn't need to size the future for all 14 arms simultaneously. A few notes:

  • handle_server_sync_notification is correctly fn (not async fn) since it only spawns — good call.
  • handle_picture_notification, handle_status_notification, handle_disappearing_mode_notification, and handle_newsletter_notification are also correctly sync fns.
  • The Arc::clone(&node) for w:gp2 and newsletter is appropriate since those handlers need to pass ownership to event dispatch.

No issues found.


Change 2: Box<[NodeRef]> for child nodes — Looks good

NodeContentRef::Nodes changed from Box<Vec<NodeRef>> (40 bytes: Box ptr + Vec ptr + len + cap) to Box<[NodeRef]> (16 bytes: ptr + len). The decoder already builds with exact capacity via Vec::with_capacity(size), so converting via into_boxed_slice() never reallocates.

All downstream access patterns are correct:

  • NodeRef::children() now returns Some(nodes) directly instead of Some(nodes.as_slice()) — correct, since &Box<[T]> auto-derefs to &[T]
  • NodeContent::as_content_ref() collects into a Vec then converts with into_boxed_slice()
  • Serde serialization uses &**nodes instead of nodes.as_slice() — equivalent

No issues found.


Change 3: Interest-based props filtering — Design concern

Beyond the critical bug above, some design notes:

Good:

  • PropsResponse now stores lightweight Vec<(u32, CompactString)> tuples instead of Vec<AbPropConfig> — avoids allocating the full enum wrapper and skips sampling props during parsing. This is the real allocation win.
  • The let-chains syntax in the parser is clean:
    if let Some(code_str) = optional_attr(child, "config_code")
        && let Ok(code) = code_str.parse::<u32>()
        && code > 0
        && let Some(value) = optional_attr(child, "config_value")

Concern:

  • into_node() for PropsResponse no longer serializes experiment_props back into <prop> children. If this round-trip matters anywhere (e.g., forwarding the response), that's a regression. If into_node() is only used in tests, it's fine but the asymmetry should be documented.

Minor — test coverage reduced:

  • The old tests covered is_enabled_or with custom defaults, get_int with negative/unparseable values, case-insensitive "True" / "ENABLED", and empty string values. The new tests are leaner. Consider keeping the is_enabled_or default behavior test and the get_int edge case test since those are the boundary conditions most likely to regress.

Summary

Area Verdict
Notification handler split Approve — clean structural improvement
Box<[NodeRef]> for children Approve — correct, saves 24 bytes per node list
Interest-based props filtering Needs fix — interest set never populated, all props silently dropped

The lightweight tuple parsing in PropsResponse is a solid optimization on its own. The interest-based filtering in AbPropsCache is a good idea architecturally but needs the interest set to actually be wired up before this can merge.


@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 07ee4bf4b9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +67 to +69
for (code, value) in props {
if interest.contains(&code) {
map.insert(code, value);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve fetched AB props until interest set is populated

apply_props now filters every incoming prop through interest.contains(&code), but this commit does not register any production interest codes (watch/watch_many are only used in the test module). In the current runtime path (Client::fetch_props), full and delta responses therefore populate an empty cache, so all AB lookups fall back to defaults and experiment-driven behavior (including privacy/token config toggles) stops tracking server-provided values.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/client.rs`:
- Around line 1796-1798: The apply_props call currently seeds the cache only
with codes in interest so late watch() registrants never get an initial full
snapshot because is_seeded flips to true and future deltas are hashed; fix by
invalidating or clearing the seeded/hash state whenever interest grows (or when
a new watch is registered) so the next fetch will perform a full snapshot: add
logic in the watch registration path to compare previous interest size to new
interest and call a new/available method on ab_props (e.g.,
clear_seeded()/unset_seeded()/reset_hash_state) or modify apply_props to seed
with all known codes (not only watched) and ensure is_seeded is unset until full
snapshot covering new interest is stored. Ensure you reference and update
ab_props.apply_props, ab_props.is_seeded (or its setter/clear method), and the
watch()/interest growth code paths so late watchers receive the initial values.

In `@wacore/src/iq/props.rs`:
- Around line 507-510: The round-trip test is masking data loss because
PropsResponse::into_node() currently drops experiment_props; either update
PropsResponse::into_node() to include/serialize experiment_props into the Node
representation (so experiment_props survives round-trip), or change
test_props_response_protocol_node_round_trip to only assert metadata fields (and
not equality of experiment_props) to reflect that children are intentionally
omitted—locate and modify the PropsResponse::into_node() implementation to
include experiment_props serialization or adjust the test to scope assertions to
the preserved metadata fields accordingly.
- Around line 279-289: The loop that builds experiment_props currently swallows
malformed experiment <prop> entries; change it so that when a child has a
"config_code" attribute present but its value fails to parse into a positive u32
or its "config_value" is missing, the parser returns an error instead of
skipping the entry. Concretely, inside the for child in
node.get_children_by_tag("prop") block, use optional_attr(child, "config_code")
to detect presence, attempt parse and validate (>0), and if parse or validation
fails or optional_attr(child, "config_value") is None, return an Err (propagate
a descriptive parse error) rather than continuing; keep skipping props that lack
a config_code entirely (sampling props). This ensures Client::fetch_props won't
persist a new hash when experiment props are malformed.

In `@wacore/src/store/ab_props.rs`:
- Around line 62-71: The issue is that self.seeded.store(true,
Ordering::Release) is set before the full-update insert loop so readers may see
"seeded" while inserts are still in progress; in the non-delta branch (when
!delta_update) move the self.seeded.store(true, Ordering::Release) call to
immediately after the for (code, value) in props { ... } loop (i.e. clear the
map first as now, perform all map.insert(code, value) operations based on
interest, and only after the loop set self.seeded.store(true,
Ordering::Release)) so the seeded flag reflects completion of the full update.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7c123b2a-8eca-44ac-971f-0f2dd240091f

📥 Commits

Reviewing files that changed from the base of the PR and between 0c506d5 and 07ee4bf.

📒 Files selected for processing (6)
  • src/client.rs
  • src/handlers/notification.rs
  • wacore/binary/src/decoder.rs
  • wacore/binary/src/node.rs
  • wacore/src/iq/props.rs
  • wacore/src/store/ab_props.rs

Comment thread src/client.rs
Comment thread wacore/src/iq/props.rs
Comment on lines +279 to +289
// Parse experiment props as lightweight (code, value) tuples.
// Sampling props (missing config_code or config_value) are skipped.
let mut experiment_props = Vec::new();
for child in node.get_children_by_tag("prop") {
props.push(AbPropConfig::try_from_node_ref(child)?);
if let Some(code_str) = optional_attr(child, "config_code")
&& let Ok(code) = code_str.parse::<u32>()
&& code > 0
&& let Some(value) = optional_attr(child, "config_value")
{
experiment_props.push((code, CompactString::from(value.as_ref())));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't treat malformed experiment props as ignorable noise.

This loop now silently drops any <prop> that advertises config_code but has an invalid code or missing config_value. Because Client::fetch_props persists the new hash after a successful parse, one malformed watched prop becomes a silent stale-cache entry with no retry path on later delta fetches. Keep skipping pure sampling props, but fail the response once a node claims to be an experiment prop and then fails validation.

Proposed fix
-        for child in node.get_children_by_tag("prop") {
-            if let Some(code_str) = optional_attr(child, "config_code")
-                && let Ok(code) = code_str.parse::<u32>()
-                && code > 0
-                && let Some(value) = optional_attr(child, "config_value")
-            {
-                experiment_props.push((code, CompactString::from(value.as_ref())));
-            }
-        }
+        for child in node.get_children_by_tag("prop") {
+            let config_code = optional_attr(child, "config_code");
+            let event_code = optional_attr(child, "event_code");
+
+            if config_code.is_some() && event_code.is_some() {
+                return Err(anyhow::anyhow!(
+                    "prop has both config_code and event_code (attrs: {:?})",
+                    child.attrs
+                ));
+            }
+
+            if let Some(code_str) = config_code {
+                let code = code_str
+                    .parse::<u32>()
+                    .map_err(|e| anyhow::anyhow!("invalid config_code `{code_str}`: {e}"))?;
+                if code == 0 {
+                    return Err(anyhow::anyhow!("config_code must be >= 1"));
+                }
+
+                let value = optional_attr(child, "config_value")
+                    .ok_or_else(|| anyhow::anyhow!("missing config_value in prop"))?;
+                experiment_props.push((code, CompactString::from(value.as_ref())));
+            }
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Parse experiment props as lightweight (code, value) tuples.
// Sampling props (missing config_code or config_value) are skipped.
let mut experiment_props = Vec::new();
for child in node.get_children_by_tag("prop") {
props.push(AbPropConfig::try_from_node_ref(child)?);
if let Some(code_str) = optional_attr(child, "config_code")
&& let Ok(code) = code_str.parse::<u32>()
&& code > 0
&& let Some(value) = optional_attr(child, "config_value")
{
experiment_props.push((code, CompactString::from(value.as_ref())));
}
// Parse experiment props as lightweight (code, value) tuples.
// Sampling props (missing config_code or config_value) are skipped.
let mut experiment_props = Vec::new();
for child in node.get_children_by_tag("prop") {
let config_code = optional_attr(child, "config_code");
let event_code = optional_attr(child, "event_code");
if config_code.is_some() && event_code.is_some() {
return Err(anyhow::anyhow!(
"prop has both config_code and event_code (attrs: {:?})",
child.attrs
));
}
if let Some(code_str) = config_code {
let code = code_str
.parse::<u32>()
.map_err(|e| anyhow::anyhow!("invalid config_code `{code_str}`: {e}"))?;
if code == 0 {
return Err(anyhow::anyhow!("config_code must be >= 1"));
}
let value = optional_attr(child, "config_value")
.ok_or_else(|| anyhow::anyhow!("missing config_value in prop"))?;
experiment_props.push((code, CompactString::from(value.as_ref())));
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wacore/src/iq/props.rs` around lines 279 - 289, The loop that builds
experiment_props currently swallows malformed experiment <prop> entries; change
it so that when a child has a "config_code" attribute present but its value
fails to parse into a positive u32 or its "config_value" is missing, the parser
returns an error instead of skipping the entry. Concretely, inside the for child
in node.get_children_by_tag("prop") block, use optional_attr(child,
"config_code") to detect presence, attempt parse and validate (>0), and if parse
or validation fails or optional_attr(child, "config_value") is None, return an
Err (propagate a descriptive parse error) rather than continuing; keep skipping
props that lack a config_code entirely (sampling props). This ensures
Client::fetch_props won't persist a new hash when experiment props are
malformed.

Comment thread wacore/src/iq/props.rs
Comment on lines +507 to 510
experiment_props: vec![
(100, CompactString::from("value1")),
(200, CompactString::from("value2")),
],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

This round-trip test now hides data loss.

PropsResponse::into_node() currently drops experiment_props, so removing the equality check means test_props_response_protocol_node_round_trip no longer exercises the field introduced by this PR. Either restore child serialization or rename/scope the test to metadata-only parsing; as written, the test passes over a lossy round trip.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wacore/src/iq/props.rs` around lines 507 - 510, The round-trip test is
masking data loss because PropsResponse::into_node() currently drops
experiment_props; either update PropsResponse::into_node() to include/serialize
experiment_props into the Node representation (so experiment_props survives
round-trip), or change test_props_response_protocol_node_round_trip to only
assert metadata fields (and not equality of experiment_props) to reflect that
children are intentionally omitted—locate and modify the
PropsResponse::into_node() implementation to include experiment_props
serialization or adjust the test to scope assertions to the preserved metadata
fields accordingly.

Comment thread wacore/src/store/ab_props.rs
@github-actions

github-actions Bot commented Apr 15, 2026

Copy link
Copy Markdown

Benchmark Results

3 improvement(s):

Benchmark Current Baseline Change
binary_benchmark::unmarshal_group::bench_unmarshal large:setup_large_marshaled() 33,558 34,946 -4.0%
binary_benchmark::roundtrip_group::bench_roundtrip_auto large:setup_large_marshaled() 61,942 63,400 -2.3%
binary_benchmark::roundtrip_group::bench_roundtrip large:setup_large_marshaled() 61,874 63,329 -2.3%
56 unchanged benchmark(s)
Benchmark Current Baseline Change
reporting_token_benchmark::content_extraction_group::bench_content_extraction simple:setup_simple_message() 3,933 3,933 +0.0%
reporting_token_benchmark::content_extraction_group::bench_content_extraction extended:setup_extended_message() 12,038 12,038 +0.0%
reporting_token_benchmark::key_derivation_group::bench_key_derivation 43,414 43,414 +0.0%
reporting_token_benchmark::token_calculation_group::bench_token_calculation 19,365 19,365 +0.0%
reporting_token_benchmark::full_generation_group::bench_full_token_generation simple:setup_full_gen_simple() 68,478 68,478 +0.0%
reporting_token_benchmark::full_generation_group::bench_full_token_generation extended:setup_full_gen_extended() 76,578 76,578 +0.0%
reporting_token_benchmark::message_encoding_group::bench_message_encoding simple:setup_simple_message() 2,230 2,230 +0.0%
reporting_token_benchmark::message_encoding_group::bench_message_encoding extended:setup_extended_message() 5,988 5,988 +0.0%
send_receive_benchmark::dm_send::bench_dm_send text:setup_dm_send() 170,447 170,441 +0.0%
send_receive_benchmark::dm_recv::bench_dm_recv text:setup_dm_recv() 191,750 191,750 +0.0%
send_receive_benchmark::group_send::bench_group_send group_10:setup_group_send_10() 875,900 875,962 -0.0%
send_receive_benchmark::group_send::bench_group_send group_50:setup_group_send_50() 966,964 966,236 +0.1%
send_receive_benchmark::group_send::bench_group_send group_256:setup_group_send_256() 1,453,203 1,453,939 -0.1%
send_receive_benchmark::group_send_skdm::bench_group_send_skdm skdm_10:setup_group_skdm_10() 2,578,906 2,578,900 +0.0%
send_receive_benchmark::group_send_skdm::bench_group_send_skdm skdm_50:setup_group_skdm_50() 9,422,413 9,423,141 -0.0%
send_receive_benchmark::group_send_skdm::bench_group_send_skdm skdm_256:setup_group_skdm_256() 44,694,993 44,694,815 +0.0%
send_receive_benchmark::group_recv::bench_group_recv text:setup_group_recv() 12,574,413 12,727,944 -1.2%
binary_benchmark::marshal_group::bench_marshal_allocating 71,247 71,234 +0.0%
binary_benchmark::marshal_group::bench_marshal_auto_allocating 71,300 71,287 +0.0%
binary_benchmark::marshal_group::bench_marshal_exact_allocating 98,367 98,354 +0.0%
binary_benchmark::marshal_group::bench_marshal_reusing_buffer 78,801 78,788 +0.0%
binary_benchmark::marshal_group::bench_marshal_reusing_buffer_vec_writer 71,347 71,334 +0.0%
binary_benchmark::marshal_group::bench_marshal_long_string 7,518 7,527 -0.1%
binary_benchmark::marshal_group::bench_marshal_auto_long_string 7,561 7,570 -0.1%
binary_benchmark::marshal_group::bench_marshal_exact_long_string 9,273 9,282 -0.1%
binary_benchmark::marshal_group::bench_marshal_huge_bytes_allocating 530,504 530,509 -0.0%
binary_benchmark::marshal_group::bench_marshal_auto_huge_bytes_allocating 530,072 530,081 -0.0%
binary_benchmark::marshal_group::bench_marshal_exact_huge_bytes_allocating 531,427 531,436 -0.0%
binary_benchmark::marshal_group::bench_marshal_many_children_allocating 8,506,160 8,506,150 +0.0%
binary_benchmark::marshal_group::bench_marshal_auto_many_children_allocating 8,450,412 8,450,436 -0.0%
binary_benchmark::marshal_group::bench_marshal_exact_many_children_allocating 19,677,947 19,677,986 -0.0%
binary_benchmark::unmarshal_group::bench_unmarshal small:setup_small_marshaled() 2,468 2,464 +0.2%
binary_benchmark::unpack_group::bench_unpack_uncompressed 787 787 +0.0%
binary_benchmark::unpack_group::bench_unpack_compressed 526,732 526,732 +0.0%
binary_benchmark::attr_parser_group::bench_attr_parser attr_lookup:setup_attr_marshaled() 4,986 4,986 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip small:setup_small_marshaled() 5,315 5,311 +0.1%
binary_benchmark::roundtrip_group::bench_roundtrip_auto small:setup_small_marshaled() 5,347 5,343 +0.1%
binary_benchmark::roundtrip_group::bench_roundtrip_exact small:setup_small_marshaled() 6,734 6,730 +0.1%
binary_benchmark::roundtrip_group::bench_roundtrip_exact large:setup_large_marshaled() 85,564 87,023 -1.7%
binary_benchmark::child_iteration_group::bench_get_children_by_tag 477,570 477,570 +0.0%
binary_benchmark::jid_optimization_group::bench_jid_to_owned_access jid_access:setup_jid_heavy_marshaled() 11,563 11,574 -0.1%
libsignal_benchmark::dm_group::bench_dm_session_establishment setup:setup_dm_users() 17,250,466 17,300,657 -0.3%
libsignal_benchmark::dm_group::bench_dm_encrypt_first_message first_msg:setup_dm_session() 157,923 157,923 +0.0%
libsignal_benchmark::dm_group::bench_dm_decrypt_first_message decrypt_prekey:setup_dm_with_first_message() 5,511,084 5,511,084 +0.0%
libsignal_benchmark::dm_group::bench_dm_encrypt_subsequent_message subsequent:setup_established_dm_session() 158,737 158,737 +0.0%
libsignal_benchmark::group_messaging_group::bench_group_create_distribution_message create:setup_group_sender() 296,767 296,767 +0.0%
libsignal_benchmark::group_messaging_group::bench_group_encrypt_message encrypt:setup_group_with_distribution() 707,030 707,098 -0.0%
libsignal_benchmark::group_messaging_group::bench_group_decrypt_message decrypt:setup_group_with_encrypted_message() 12,541,156 12,613,164 -0.6%
libsignal_benchmark::conversation_group::bench_full_dm_conversation full:setup_conversation_data() 27,403,540 27,447,060 -0.2%
libsignal_benchmark::signature_group::bench_signature_creation sign:setup_keypair_with_message() 3,467,011 3,467,011 +0.0%
libsignal_benchmark::signature_group::bench_signature_verification verify:setup_keypair_with_message() 124,419,623 126,164,373 -1.4%
libsignal_benchmark::signature_group::bench_key_generation keygen 2,830,452 2,830,452 +0.0%
libsignal_benchmark::session_optimization_group::bench_decrypt_with_previous_session previous_session:setup_with_archived_sessions() 46,003 46,003 +0.0%
libsignal_benchmark::session_optimization_group::bench_out_of_order_decryption out_of_order:setup_out_of_order_messages() 5,090,932 5,090,932 +0.0%
libsignal_benchmark::session_optimization_group::bench_promote_matching_session promote:setup_promote_matching_session() 316,987 316,987 +0.0%
libsignal_benchmark::session_optimization_group::bench_message_key_eviction eviction:setup_message_key_eviction() 14,254,317 14,254,317 +0.0%

The interest-based filtering was silently dropping all props because
no production code called watch(). Pre-populate DEFAULT_INTEREST with
all 11 config codes from config_codes module.

Also: set seeded flag AFTER the insert loop (not before) so readers
don't see seeded=true while inserts are in progress.

Added regression test that verifies production config codes are
retained without explicit watch() calls.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@wacore/src/store/ab_props.rs`:
- Around line 59-64: In watch_many, replace the manual loop that inserts each
code into the interest write lock with a single extend call to express intent
more clearly: after acquiring let mut interest = self.interest.write().await,
call interest.extend(codes.iter().copied()) (or interest.extend(codes) if types
align) to add all codes at once; this keeps the method concise and idiomatic
while targeting the existing watch_many function and the interest collection.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 46a47fa9-5acc-4348-8986-ae959cd38d94

📥 Commits

Reviewing files that changed from the base of the PR and between 07ee4bf and d7b3d19.

📒 Files selected for processing (1)
  • wacore/src/store/ab_props.rs

Comment thread wacore/src/store/ab_props.rs

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d7b3d19de2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread wacore/src/iq/props.rs
Comment on lines 259 to 261
builder = builder.attr("delta_update", self.delta_update.to_string());

let prop_nodes: Vec<Node> = self.props.into_iter().map(|p| p.into_node()).collect();
builder = builder.children(prop_nodes);

builder.build()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include prop children when encoding PropsResponse

PropsResponse::into_node now builds <props> with only attributes and never emits any <prop> children, so experiment_props is always dropped during serialization. Any caller that uses ProtocolNode::into_node for fixtures, mocks, or round-trip transformations will produce an empty props payload and parse back an empty experiment_props, which silently changes behavior from the pre-change contract.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@wacore/src/store/ab_props.rs`:
- Around line 18-32: DEFAULT_INTEREST currently must be manually kept in sync
with config_codes in iq/props.rs; add a short cross-reference comment above
DEFAULT_INTEREST mentioning config_codes::(production list) and create a
compile-time/test-time assertion to catch drift: extend or add a unit test
(e.g., default_interest_retains_production_config_codes) to compare
DEFAULT_INTEREST.len() against the canonical count from config_codes (expose or
add a const like CONFIG_PRODUCTION_COUNT in that module or compute from a
central list) so the build/test fails if new production config codes are added
but not appended to DEFAULT_INTEREST.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: ec34b721-034e-4303-a815-45b5fe9ee9e7

📥 Commits

Reviewing files that changed from the base of the PR and between d7b3d19 and 9e71991.

📒 Files selected for processing (1)
  • wacore/src/store/ab_props.rs

Comment thread wacore/src/store/ab_props.rs Outdated
Replace duplicated DEFAULT_INTEREST list with config_codes::ALL.
Adding a new config code now only requires updating one place.

Also documents into_node() asymmetry (metadata only, no prop children).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
wacore/src/iq/props.rs (2)

519-540: 🧹 Nitpick | 🔵 Trivial

Make the lossy serialization contract explicit in this test.

PropsResponse::into_node() now drops experiment_props, so this is no longer a true round-trip test. Rename it to metadata-only round-trip or assert that parsed.experiment_props is empty so the data loss is intentional rather than implicit.

Suggested tweak
-    fn test_props_response_protocol_node_round_trip() {
+    fn test_props_response_metadata_round_trip() {
         let response = PropsResponse {
             ab_key: Some("test_ab_key".to_string()),
             hash: Some("hash123".to_string()),
             refresh: Some(7200),
             refresh_id: Some(42),
@@
         assert_eq!(parsed.hash, response.hash);
         assert_eq!(parsed.refresh, response.refresh);
         assert_eq!(parsed.refresh_id, response.refresh_id);
         assert_eq!(parsed.delta_update, response.delta_update);
+        assert!(parsed.experiment_props.is_empty());
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wacore/src/iq/props.rs` around lines 519 - 540, The test
test_props_response_protocol_node_round_trip currently relies on
PropsResponse::into_node which intentionally drops experiment_props, so make the
lossy serialization explicit by either renaming the test to something like
test_props_response_protocol_node_metadata_round_trip or by keeping the name and
adding an assertion that parsed.experiment_props is empty; locate the test
function and update its name and/or add
assert!(parsed.experiment_props.is_empty()) after the other assertions to
document the intentional data loss from PropsResponse::into_node and
PropsResponse::try_from_node.

301-308: ⚠️ Potential issue | 🟠 Major

Return an error once a <prop> claims to be an experiment prop but fails validation.

Right now a delta response that contains config_code but has an invalid code, a missing config_value, or conflicting experiment/sampling attrs is treated as a successful parse and the entry is just dropped. AbPropsCache::apply_props(true, ...) then keeps the previous watched value as if nothing changed, which can leave the client on stale config after a malformed update.

Proposed fix
         let mut experiment_props = Vec::new();
         for child in node.get_children_by_tag("prop") {
-            if let Some(code_str) = optional_attr(child, "config_code")
-                && let Ok(code) = code_str.parse::<u32>()
-                && code > 0
-                && let Some(value) = optional_attr(child, "config_value")
-            {
-                experiment_props.push((code, CompactString::from(value.as_ref())));
+            let config_code = optional_attr(child, "config_code");
+            let event_code = optional_attr(child, "event_code");
+
+            if config_code.is_some() && event_code.is_some() {
+                return Err(anyhow::anyhow!(
+                    "prop has both config_code and event_code (attrs: {:?})",
+                    child.attrs
+                ));
+            }
+
+            if let Some(code_str) = config_code {
+                let code = code_str
+                    .parse::<u32>()
+                    .map_err(|e| anyhow::anyhow!("invalid config_code `{code_str}`: {e}"))?;
+                if code == 0 {
+                    return Err(anyhow::anyhow!("config_code must be >= 1"));
+                }
+
+                let value = optional_attr(child, "config_value")
+                    .ok_or_else(|| anyhow::anyhow!("missing config_value in prop"))?;
+                experiment_props.push((code, CompactString::from(value.as_ref())));
             }
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wacore/src/iq/props.rs` around lines 301 - 308, The loop that collects
experiment_props currently ignores malformed <prop> entries (using
node.get_children_by_tag("prop"), optional_attr, and pushing into
experiment_props), causing silent drops; update that logic so that whenever
optional_attr(child, "config_code") is Some but parsing fails, parsed code is
zero, config_value is missing, or there are conflicting experiment/sampling
attributes, the function returns an Err immediately (propagating a descriptive
error) instead of skipping the entry—ensure you change the branch around the
code_str.parse::<u32>() check and the value presence check to return an error on
any validation failure so callers like AbPropsCache::apply_props(true, ...) can
detect malformed updates.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@wacore/src/iq/props.rs`:
- Around line 519-540: The test test_props_response_protocol_node_round_trip
currently relies on PropsResponse::into_node which intentionally drops
experiment_props, so make the lossy serialization explicit by either renaming
the test to something like test_props_response_protocol_node_metadata_round_trip
or by keeping the name and adding an assertion that parsed.experiment_props is
empty; locate the test function and update its name and/or add
assert!(parsed.experiment_props.is_empty()) after the other assertions to
document the intentional data loss from PropsResponse::into_node and
PropsResponse::try_from_node.
- Around line 301-308: The loop that collects experiment_props currently ignores
malformed <prop> entries (using node.get_children_by_tag("prop"), optional_attr,
and pushing into experiment_props), causing silent drops; update that logic so
that whenever optional_attr(child, "config_code") is Some but parsing fails,
parsed code is zero, config_value is missing, or there are conflicting
experiment/sampling attributes, the function returns an Err immediately
(propagating a descriptive error) instead of skipping the entry—ensure you
change the branch around the code_str.parse::<u32>() check and the value
presence check to return an error on any validation failure so callers like
AbPropsCache::apply_props(true, ...) can detect malformed updates.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7f2baebe-4374-4973-8b92-4595eedcce55

📥 Commits

Reviewing files that changed from the base of the PR and between 9e71991 and 2baf901.

📒 Files selected for processing (2)
  • wacore/src/iq/props.rs
  • wacore/src/store/ab_props.rs

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2baf901514

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread wacore/src/iq/props.rs
Comment on lines +260 to +262
/// Serializes metadata attrs only. Individual `<prop>` children are not
/// emitted since experiment_props stores lightweight (code, value) tuples
/// without the full AbPropConfig structure needed for node construction.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Serialize experiment props in PropsResponse::into_node

PropsResponse::try_from_node_ref still reads <prop> children into experiment_props, but into_node now intentionally emits only metadata attributes and never writes those children back. Any path that round-trips a response through into_node (e.g., protocol fixtures/mocks or transform pipelines) will silently lose all experiment values and then parse back as an empty prop set, which is a behavioral regression from the previous contract.

Useful? React with 👍 / 👎.

@jlucaso1
jlucaso1 merged commit 619483a into main Apr 15, 2026
13 checks passed
@jlucaso1
jlucaso1 deleted the perf/futures-decode-props branch April 15, 2026 21:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant