Skip to content

Replace per-pair gain/pan mutex locking with single snapshot - #293

Open
ann0see wants to merge 1 commit into
mainfrom
perf/server-gain-pan-snapshot
Open

Replace per-pair gain/pan mutex locking with single snapshot#293
ann0see wants to merge 1 commit into
mainfrom
perf/server-gain-pan-snapshot

Conversation

@ann0see

@ann0see ann0see commented Aug 8, 2026

Copy link
Copy Markdown
Owner

Short description of changes

Replace the per-pair GetGain()/GetPan() calls in the server audio mixing loop with a single mutex-guarded snapshot per channel per frame (CChannel::GetGainsAndPannings()). The old code acquired the channel mutex 2N times per frame for every connected channel (O(N²) lock/unlock operations per server tick); the new code acquires it once per channel and copies both arrays.

CHANGELOG: SKIP

Context: Fixes an issue?

No issue; performance optimization. The mutex cost is quadratic in the number of connected channels, so it is only measurable on large servers (measured with 50 and 100 clients).

Does this change need documentation? What needs to be documented and how?

No.

Status of this Pull Request

Working implementation; benchmarked with 50 and 100 real client processes.

What is missing until this pull request can be merged?

Review. Two open points for reviewers:

  • CChannel::GetGain()/GetPan() now have no remaining callers (dead code) — should they be removed or kept as public API?
  • The snapshot buffers are initialized to MAX_NUM_CHANNELS while the sibling vecvecfGains uses iMaxNumChannels; functionally harmless (the copy assignment resizes) but asymmetric.

Benchmark

Server CPU: mutex-per-pair (baseline) vs single snapshot per channel

Clients Baseline mean Patched mean Delta
50 40.59% 36.15% −4.44 pp (−10.9%)
100 60.47% 56.53% −3.94 pp (−6.5%)

Method: headless server pinned to a single core, jackd -d dummy (48 kHz / 128 samples), real client processes streaming silence over UDP, server CPU read from /proc/<pid>/stat (utime+stime) over 40 s, 3 interleaved runs per build, zero overlap between the two groups. The absolute saving is ~4 pp of one core at both 50 and 100 clients; the server baseline itself grows with N because the audio mix is also O(N²).

Checklist

  • I've verified that this Pull Request follows the general code principles
  • I tested my code and it does what I want
  • My code follows the style guide
  • I waited some time after this Pull Request was opened and all GitHub checks completed without errors.
  • I've filled all the content above

Summary by CodeRabbit

  • Performance

    • Improved handling of channel gain and panning data by retrieving multiple channel values together.
    • Reduced repeated synchronization during channel processing, which may improve responsiveness in scenarios involving multiple connected channels.
  • Reliability

    • Gain and panning values now remain aligned with the requested channel order.
    • Invalid or out-of-range channel IDs return zero values consistently.

@ann0see

ann0see commented Aug 8, 2026

Copy link
Copy Markdown
Owner Author

Review: correctness, safety, and measurement

What changed

  • src/channel.cpp / channel.h: new CChannel::GetGainsAndPannings() — locks Mutex once and copies the channel's gain/pan arrays into caller-provided vectors.
  • src/server.h: two new snapshot buffers (vecvecfGainsSnapshot, vecvecfPanningsSnapshot).
  • src/server.cpp DecodeReceiveData: one snapshot is taken per channel per frame; the inner (i,j) mixing loop reads the snapshot instead of calling GetGain()/GetPan() for every pair.

Why

Every server frame the audio mix runs an O(N²) nested loop over the connected channels. Each GetGain()/GetPan() call performed QMutexLocker locker(&Mutex), i.e. 2N² mutex acquisitions per frame at 375 frames/sec (128-sample buffers). At N=100 that is ~7.5 million lock/unlock operations per second, pure CPU overhead on the server. The snapshot approach is also a strictly more consistent read than the previous 2N independent locked reads.

Review findings

  • Bounds: channel vecfGains/vecfPannings are sized MAX_NUM_CHANNELS (150, channel.cpp:51-52); the snapshot copy via operator= inherits that size, so indexing by channel ID (vecChanIDsCurConChan[j], always < 150) is in bounds. The destination vecvecfGains[iChanCnt] is sized iMaxNumChannels and indexed by j < iNumClients. Safe.
  • Thread safety: writers SetGain/SetPan still hold the same Mutex (channel.cpp:314/340); the snapshot buffers are only touched in the server audio path. No new races.
  • No per-pair locking remains in the loop: GetFadeInGain() is a lock-free inline read (channel.h:129).
  • Formatting: clang-format clean on the changed lines (the two channel.cpp warnings are a pre-existing unrelated TODO block).

Measurement

Setup: headless server pinned to one core (taskset -c 0, no multithreading), jackd -d dummy -r 48000 -p 128, real client processes streaming silence over UDP, server CPU read from /proc/<pid>/stat (utime+stime), 40 s per run, 3 interleaved runs of each build built from the identical tree (via git stash + rebuild). All clients connected and stable in every run.

Server CPU: mutex-per-pair (baseline) vs single snapshot per channel

Run N Baseline Patched
1 50 38.91% 36.30%
2 50 40.22% 35.75%
3 50 42.64% 36.40%
mean 50 40.59% 36.15%
1 100 61.95% 57.01%
2 100 60.22% 56.25%
3 100 59.23% 56.33%
mean 100 60.47% 56.53%

Result: −4.44 pp (−10.9%) at 50 clients, −3.94 pp (−6.5%) at 100 clients, zero overlap between the groups. The saving is worth it exactly on the large servers that hit the quadratic path; it is within measurement noise on typical small (<15 client) servers.

Open questions for reviewers

  1. CChannel::GetGain()/GetPan() now have zero callers in the tree — remove them, or keep as public API?
  2. Snapshot init uses MAX_NUM_CHANNELS while sibling vecvecfGains uses iMaxNumChannels — harmless (copy resizes) but asymmetric; align for consistency?

@mcfnord mcfnord 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.

🤖 AI: The inline comments below are a 7-suggestion set that turns the per-channel snapshot into a compacted fill: GetGainsAndPannings() takes the connected-ID list and writes vecvecfGains[iChanCnt]/vecvecfPannings[iChanCnt] directly, in connected-channel order, under the same single lock. Same lock count as this PR (one per channel per frame), and additionally: iNumClients floats copied per array instead of 150 — the copy no longer grows with MAX_NUM_CHANNELS on small servers; the two snapshot members, their four Init lines and the re-index pass are deleted; the range guard GetGain()/GetPan() had (0 for an ID outside [0,150)) is kept, where the unchecked [vecChanIDsCurConChan[j]] read dropped it; and open question 2 disappears — there is no snapshot buffer left to size. The suggestions only work as a batch (apply all 7 in one commit).

Verified on the full set applied to c4b162d8: headless build, gcc 11.4 -Wall -Wextra, zero warnings, server boots; an A/B harness comparing the new getter against the still-present GetGain()/GetPan() returns byte-identical floats for 37 scrambled in-range IDs and 0 for out-of-range IDs; clang-format-14 -style=file is byte-clean on all four files (control: the pristine head files are also byte-clean, so the check is discriminating).

On open question 1: git grep at c4b162d8 finds zero callers of GetGain/GetPan — removable, and with the compacted fill nothing needs them. No suggestion attached because their lines are outside this PR's diff.

Separate observation, also outside the diff: the remaining per-pair accessor traffic is fade-in. GetFadeInGain() runs twice per channel pair in DecodeReceiveData, and its two inputs are constant for the frame — every writer (PutAudioData, OnProtocolMessageReceived) and OnTimer's decode phase hold CServer::Mutex. A per-frame fade-in vector (one read per connected channel per frame, taken where vecChanIDsCurConChan is built) would make that N reads instead of 2N²−N, and is a natural follow-up to this PR's pattern — now jamulussoftware/jamulus#3945.

One number in the benchmark worth a look before merge: the removed lock count is 2N² per frame — quadrupling from 50 to 100 clients — yet the measured saving stays ~4 pp at both. On a separate rig (12-core x86-64, isolated microbenchmark of only this refresh term: N=150, 64-sample ticks, 18 replicates) the full recompute is 575.9 µs/tick against 41.2 µs for identical arithmetic without the mutex — the mutex is ~93% of the term and scales as the lock count. A per-region timer around the fill-plus-mix block at N=50 and N=100 would show where the missing ~3× went; the per-channel 2×150-float copy, which the compacted fill removes, is one candidate to test. Related measurements on this term: #3895.

Comment thread src/channel.h Outdated
Comment thread src/channel.cpp Outdated
Comment on lines +359 to +367
void CChannel::GetGainsAndPannings ( CVector<float>& vecGains, CVector<float>& vecPannings )
{
QMutexLocker locker ( &Mutex );

// copy the gain and pan values under a single lock instead of acquiring the
// mutex once per value
vecGains = vecfGains;
vecPannings = vecfPannings;
}

@mcfnord mcfnord Aug 23, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🤖 AI: [2/7] Filling the caller's arrays compacted under the single lock copies iNumClients values per array instead of 150, and keeps the range guard the replaced GetGain()/GetPan() had — the caller's unchecked [vecChanIDsCurConChan[j]] indexing (suggestion 6/7 removes it) otherwise turns a future out-of-range ID into an out-of-bounds read on the mix path where the old code degraded to silence.

Suggested change
void CChannel::GetGainsAndPannings ( CVector<float>& vecGains, CVector<float>& vecPannings )
{
QMutexLocker locker ( &Mutex );
// copy the gain and pan values under a single lock instead of acquiring the
// mutex once per value
vecGains = vecfGains;
vecPannings = vecfPannings;
}
void CChannel::GetGainsAndPannings ( const CVector<int>& vecChanIDs, const int iNumClients, CVector<float>& vecGains, CVector<float>& vecPannings )
{
QMutexLocker locker ( &Mutex );
// copy the gain and pan values of the connected channels under a
// single lock, compacted to the caller's channel order; out-of-range
// channel IDs get zero gain and pan
for ( int j = 0; j < iNumClients; j++ )
{
const int iChanID = vecChanIDs[j];
if ( ( iChanID >= 0 ) && ( iChanID < MAX_NUM_CHANNELS ) )
{
vecGains[j] = vecfGains[iChanID];
vecPannings[j] = vecfPannings[iChanID];
}
else
{
vecGains[j] = 0;
vecPannings[j] = 0;
}
}
}

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

The comment would probably need to be changed anyway as we don't care about what happened before the change (it's all visible in git). From performance standpoint, it would probably be better to first set vecGains/vecPannings[0..MAX_NUM_CHANNELS] = 0 with some memset and then only fill the ones where the if would fire - but the access pattern is potentially non contiguous anyway.

Comment thread src/server.h Outdated
Comment on lines +282 to +283
CVector<CVector<float>> vecvecfGainsSnapshot;
CVector<CVector<float>> vecvecfPanningsSnapshot;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🤖 AI: [3/7] With the compacted fill the getter writes vecvecfGains[iChanCnt]/vecvecfPannings[iChanCnt] directly, so these two members (2 × 150 × 150 floats ≈ 180 KB of permanent state for a value that lives inside one DecodeReceiveData call) are no longer needed.

Suggested change
CVector<CVector<float>> vecvecfGainsSnapshot;
CVector<CVector<float>> vecvecfPanningsSnapshot;

@ann0see ann0see Sep 11, 2026

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Probably true.. Will need to go through the code again.

Comment thread src/server.cpp Outdated
Comment on lines +177 to +178
vecvecfGainsSnapshot.Init ( iMaxNumChannels );
vecvecfPanningsSnapshot.Init ( iMaxNumChannels );

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🤖 AI: [4/7] Snapshot members deleted (3/7).

Suggested change
vecvecfGainsSnapshot.Init ( iMaxNumChannels );
vecvecfPanningsSnapshot.Init ( iMaxNumChannels );

Comment thread src/server.cpp Outdated
Comment thread src/server.cpp Outdated
Comment thread src/server.cpp Outdated
@ann0see

ann0see commented Sep 10, 2026

Copy link
Copy Markdown
Owner Author

git grep at c4b162d finds zero callers of GetGain/GetPan — removable, and with the compacted fill nothing needs them.

We can probably remove the dead code.

@coderabbitai

coderabbitai Bot commented Sep 10, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 82f84c4c-7a75-43a3-8a3f-bcc844f9461d

📥 Commits

Reviewing files that changed from the base of the PR and between 292506e and 68e86fb.

📒 Files selected for processing (3)
  • src/channel.cpp
  • src/channel.h
  • src/server.cpp
 __________________________________________________________
< This regex looks like you sneezed on the keyboard-twice. >
 ----------------------------------------------------------
  \
   \   \
        \ /\
        ( )
      .( o ).
✨ Finishing Touches
📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/server-gain-pan-snapshot

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.

@ann0see

ann0see commented Sep 10, 2026

Copy link
Copy Markdown
Owner Author

Seems like I can't fully apply the suggestions. Anyway. Maybe open a. Ew PR to my repo such that I can see the full changes?

@ann0see

ann0see commented Sep 10, 2026

Copy link
Copy Markdown
Owner Author

Pre-existing, not introduced here; a per-frame fade-in vector (one read per connected channel per frame, taken where vecChanIDsCurConChan is built) would remove both the O(N²) reads and the mid-frame inconsistency, and is a natural follow-up to this PR's pattern.

Please open an upstream issue.

@ann0see

ann0see commented Sep 10, 2026

Copy link
Copy Markdown
Owner Author

Further todo: check the memory layout in how far we may want to interlieve the layout for pan and gain to avoid cache misses. CC: @coderabbitai find out if the layout of vecsPan/gain should be interlieved as struct GainPan { float gain, pan; };
CVector vecfGainPan;

@coderabbitai

coderabbitai Bot commented Sep 10, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline src/channel.h --items all --match 'GetGainsAndPannings|GetGain|GetPan'
ast-grep outline src/channel.cpp --items all --match 'GetGainsAndPannings|GetGain|GetPan|SetGain|SetPan'
ast-grep outline src/server.h --items all --match 'vecvecfGainsSnapshot|vecvecfPanningsSnapshot|vecvecfGains|vecvecfPannings'
ast-grep outline src/server.cpp --items all --match 'DecodeReceiveData'

printf '\n--- relevant declarations and implementations ---\n'
rg -n -C 8 'GetGainsAndPannings|GetGain\s*\(|GetPan\s*\(|SetGain\s*\(|SetPan\s*\(|vecvecfGainsSnapshot|vecvecfPanningsSnapshot|vecvecfGains|vecvecfPannings' \
  src/channel.h src/channel.cpp src/server.h src/server.cpp

printf '\n--- CVector declaration ---\n'
fd -t f -a '.*\.(h|hpp)$' src | xargs -r rg -n -C 4 'class CVector|template\s*<.*>\s*class CVector'

Length of output: 18877


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- CChannel snapshot implementation ---'
sed -n '355,385p' src/channel.cpp

printf '%s\n' '--- CServer initialization ---'
sed -n '155,205p' src/server.cpp

printf '%s\n' '--- CServer snapshot and compaction path ---'
sed -n '870,930p' src/server.cpp

printf '%s\n' '--- CServer mono and stereo reads ---'
sed -n '1028,1122p' src/server.cpp

printf '%s\n' '--- CVector methods ---'
sed -n '145,205p' src/util.h

printf '%s\n' '--- PR diff ---'
git diff --find-renames -- src/channel.h src/channel.cpp src/server.h src/server.cpp

printf '%s\n' '--- Snapshot identifiers and declarations ---'
rg -n -C 5 'vecvecfGainsSnapshot|vecvecfPanningsSnapshot' src

Length of output: 14421


@ann0see I recommend that you keep separate gain and pan arrays.

vecfGains and vecfPannings are each at most 600 bytes for 150 channels. Together, they use about 1.2 KiB. Both arrays fit easily in L1 cache.

The mixer loads gain and pan once per source channel before it enters the per-sample loops. Therefore, this access is not a cache-sensitive inner-sample operation.

A GainPan struct can improve locality in the stereo path because that path reads both values together. However, it does not materially reduce cache misses at this array size. It also makes the mono path load pan data that it does not use.

The current structure-of-arrays layout is therefore appropriate:

CVector<float> vecfGains;
CVector<float> vecfPannings;

Use CVector<GainPan> only if a benchmark on the supported target CPUs shows a repeatable improvement. Measure mono and stereo servers separately, with delay panning both enabled and disabled. The expected difference is likely below measurement noise.

One separate issue needs correction before build verification. The inspected checkout declares the four-argument compacting CChannel::GetGainsAndPannings() in src/channel.h, but src/server.cpp calls a two-argument version and uses snapshot members not declared in src/server.h.

You are interacting with an AI system.

Comment thread src/channel.cpp Outdated
{
QMutexLocker locker ( &Mutex );

// copy the gain and pan values under a single lock instead of acquiring the

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

To be rephrased...

@ann0see

ann0see commented Sep 11, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai re-review.

@coderabbitai

coderabbitai Bot commented Sep 11, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- changed files ---'
git diff --name-status HEAD^ HEAD

printf '%s\n' '--- pull request diff ---'
git diff --find-renames HEAD^ HEAD -- src/channel.h src/channel.cpp src/server.h src/server.cpp

printf '%s\n' '--- relevant symbol outlines ---'
ast-grep outline src/channel.h --items all --match 'GetGainsAndPannings|GetGain|GetPan'
ast-grep outline src/channel.cpp --items all --match 'GetGainsAndPannings|GetGain|GetPan|SetGain|SetPan'
ast-grep outline src/server.h --items all --match 'vecvecfGains|vecvecfPannings|Snapshot'
ast-grep outline src/server.cpp --items all --match 'DecodeReceiveData'

printf '%s\n' '--- declarations, implementations, and call sites ---'
rg -n -C 6 '\b(GetGainsAndPannings|GetGain|GetPan|SetGain|SetPan)\s*\(|vecvecf(Gains|Pannings)(Snapshot)?\b' \
  src/channel.h src/channel.cpp src/server.h src/server.cpp

printf '%s\n' '--- direct calls across source tree ---'
rg -n -C 3 '\.(GetGainsAndPannings|GetGain|GetPan)\s*\(' src

printf '%s\n' '--- relevant server paths ---'
sed -n '150,215p' src/server.cpp
sed -n '850,950p' src/server.cpp
sed -n '1000,1140p' src/server.cpp

Length of output: 34221


@ann0see Re-review complete. I found no blocking correctness, safety, or build-consistency issue in the current diff.

The prior mismatch is fixed. CChannel::GetGainsAndPannings() has matching declaration, definition, and call signatures. CServer also declares and initializes both snapshot buffer members.

The snapshot size is valid. Channel gain and pan vectors use MAX_NUM_CHANNELS, and the snapshot vectors use the same size. The copies therefore do not require capacity growth in the real-time path.

The locking change is safe. SetGain() and SetPan() use CChannel::Mutex. The snapshot method uses the same mutex to copy both arrays as one consistent pair.

The indexed reads are valid for channel IDs. Each snapshot row is private to iChanCnt, so parallel channel processing does not share a snapshot row.

CChannel::GetGain() and CChannel::GetPan() have no active callers in src. Their removal is a safe cleanup, but it is not required for this change.

I did not execute a build or runtime test in this review environment.

You are interacting with an AI system.

@ann0see

ann0see commented Sep 11, 2026

Copy link
Copy Markdown
Owner Author

Todo: check call sites manually

@ann0see
ann0see force-pushed the perf/server-gain-pan-snapshot branch 2 times, most recently from 1e29f99 to af33621 Compare September 11, 2026 18:34

@ann0see ann0see left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

I believe that applying the comments somehow went wrong?

Acquire the channel mutex once per channel and copy the gain/pan values
of all connected channels (compacted into the caller's channel order,
with bounds guard) instead of acquiring it twice per channel pair
(O(N^2) lock/unlock operations per server frame).

The values are written directly into vecvecfGains/vecvecfPannings, no
intermediate snapshot buffers are needed. Drop the now unused
CChannel::GetGain()/GetPan().
@ann0see
ann0see force-pushed the perf/server-gain-pan-snapshot branch from af33621 to 68e86fb Compare September 11, 2026 20:25

@ann0see ann0see left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Seems more in line with what I expected now. @coderabbitai review

Comment thread src/channel.cpp
@ann0see
ann0see marked this pull request as ready for review September 11, 2026 20:30
@coderabbitai

coderabbitai Bot commented Sep 11, 2026

Copy link
Copy Markdown

🤖 Completed: Generate docstrings for PR #293View PR #321

@ann0see

ann0see commented Sep 11, 2026

Copy link
Copy Markdown
Owner Author

@mcfnord I think it's worth benchmarking this rigorously. On my machine I couldn't reproduce the gains from the start - however I believe that my PC is too noisy anyway. So follow the same methodology as the initial one - then refine it with fixed cpu frequency, warm up phase etc. I believe that we should definitely see improvements

@ann0see

ann0see commented Sep 11, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai it's more important that you do a thorough code review!

@coderabbitai

coderabbitai Bot commented Sep 11, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- PR commit and changed files ---'
git status --short
git diff --name-status HEAD^ HEAD
git diff --check HEAD^ HEAD
git diff --find-renames --unified=80 HEAD^ HEAD -- \
  src/channel.h src/channel.cpp src/server.h src/server.cpp

printf '%s\n' '--- symbol outlines ---'
ast-grep outline src/channel.h --items all --match 'CChannel|GetGainsAndPannings|SetGain|SetPan|GetFadeInGain'
ast-grep outline src/channel.cpp --items all --match 'CChannel|GetGainsAndPannings|SetGain|SetPan'
ast-grep outline src/server.h --items all --match 'CServer|DecodeReceiveData|vecvecfGains|vecvecfPannings'
ast-grep outline src/server.cpp --items all --match 'CServer|DecodeReceiveData'

printf '%s\n' '--- exact API declarations, definitions, calls, and member use ---'
rg -n -C 8 -P '\b(GetGainsAndPannings|GetGain|GetPan|SetGain|SetPan|GetFadeInGain)\s*\(|\bvecvecf(Gains|Pannings)(Snapshot)?\b' src

printf '%s\n' '--- channel constants, vectors, mutex, and relevant implementations ---'
rg -n -C 10 -P '\b(MAX_NUM_CHANNELS|vecfGains|vecfPannings|Mutex)\b' src/channel.h src/channel.cpp

printf '%s\n' '--- DecodeReceiveData and adjacent mixing implementation ---'
rg -n -A 330 -B 30 -P '^\s*void\s+CServer::DecodeReceiveData\s*\(' src/server.cpp

printf '%s\n' '--- CVector declaration and resize/copy/index semantics ---'
fd -t f -a 'util.h' . -x sh -c '
  for f do
    printf "\n--- %s ---\n" "$f"
    rg -n -C 12 -P "class\s+CVector\b|struct\s+CVector\b|CVector\s*<|operator=|Init\s*\(|Resize\s*\(|SetSize\s*\(|operator\[\]" "$f"
  done
' sh {} +

printf '%s\n' '--- related server synchronization and parallel-dispatch context ---'
rg -n -C 10 -P '\b(DecodeReceiveData|QMutex|Mutex|Thread|Process|Parallel|vecChanIDsCurConChan|iNumClients)\b' src/server.h src/server.cpp

Length of output: 94711


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- repository state and change separation ---'
git status --short
printf '%s\n' 'Committed diff (HEAD^..HEAD):'
git diff --check HEAD^ HEAD
git diff --stat HEAD^ HEAD
printf '%s\n' 'Working-tree diff:'
git diff --check
git diff --stat
git diff -- src/channel.h src/channel.cpp src/server.h src/server.cpp

printf '%s\n' '--- all repository references to old and new accessors ---'
rg -n -C 5 -P '\b(GetGainsAndPannings|GetGain|GetPan)\s*\(' . \
  -g '!**/.git/**' -g '!**/build/**' -g '!**/dist/**'

printf '%s\n' '--- constructor, timer, and processing-phase dispatch ---'
sed -n '45,225p' src/server.cpp
sed -n '620,815p' src/server.cpp

printf '%s\n' '--- server state and processing declarations ---'
sed -n '100,325p' src/server.h

printf '%s\n' '--- connected-channel list construction and lifecycle ---'
sed -n '1400,1545p' src/server.cpp
rg -n -C 12 -P '\b(vecChanIDsCurConChan|iNumClients|iMaxNumChannels)\b' src/server.cpp src/server.h

printf '%s\n' '--- thread-pool and block-processing definitions and use ---'
rg -n -C 16 -P '\b(DecodeReceiveDataBlocks|MixEncodeTransmitDataBlocks|ProcessData|ThreadPool|QtConcurrent|run\s*\(|waitForDone|iStartChanCnt|iStopChanCnt)\b' src

printf '%s\n' '--- fade-in field declaration and every read/write site ---'
rg -n -C 8 -P '\b(iFadeInCnt|iFadeInCntMax|GetFadeInGain)\b' src/channel.h src/channel.cpp src/server.cpp

printf '%s\n' '--- channel count constraints and MAX_NUM_CHANNELS definition ---'
rg -n -C 6 -P '^\s*#\s*define\s+MAX_NUM_CHANNELS\b|\bMAX_NUM_CHANNELS\b|\biMaxNumChannels\b' \
  src/global.h src/server.h src/server.cpp src/channel.h src/channel.cpp

Length of output: 134038


@ann0see Thorough review complete. I found no blocking issue in the committed diff.

I verified these items:

  • GetGainsAndPannings() has matching declaration, definition, and call signature.
  • The repository has one call site for GetGainsAndPannings(). It has no remaining calls to GetGain() or GetPan().
  • OnTimer() bounds iNumClients by iMaxNumChannels.
  • The server preallocates the channel-ID vector and every gain/pan row to iMaxNumChannels.
  • The channel gain/pan backing arrays have MAX_NUM_CHANNELS elements.
  • The snapshot loop is in bounds under the existing invariant: iNumClients <= iMaxNumChannels <= MAX_NUM_CHANNELS.
  • The new snapshot path performs no vector resize or allocation in the real-time path.
  • SetGain(), SetPan(), and GetGainsAndPannings() use the same CChannel::Mutex.
  • Each decode task writes a distinct snapshot row.
  • OnTimer() waits for all decode tasks before it starts mix tasks. A mix task cannot read a partially written snapshot row.
  • The snapshot holds gain and pan values from one mutex-protected state. This is more consistent than the former separate getter calls.
  • git diff --check HEAD^ HEAD found no whitespace errors.

GetFadeInGain() remains an unlocked read. That behavior existed before this PR. The new snapshot method does not make it worse.

I did not run a build or runtime test in this environment.

You are interacting with an AI system.

@mcfnord

mcfnord commented Sep 12, 2026

Copy link
Copy Markdown

🤖 AI: Rebenchmarked on three machines, and the flat saving is the thing to chase: this term is quadratic in N, so −4.44 pp at 50 clients and −3.94 pp at 100 cannot both be it. A quadratic term puts the 50-client row at a quarter of the 100-client one; his sits 4.51× above that.

Isolated first. A harness replaying the locked getter against one lock per channel, same matrices byte-for-byte, gives a constant 41.06 instructions per removed call on a Xeon 8280 (68.5 on a Pi 4), unchanged from N=50 to N=150.

Then end-to-end, one core, no multithreading, 20 s warm-up discarded, 60 s measured, runs interleaved, every point verified to be delivering ≥99% of expected frames to every client:

N baseline patched saving
50 47.521 ±0.277 44.482 ±0.435 −3.038 pp
75 77.483 ±0.732 70.211 ±0.539 −7.272 pp

×2.39 from 50 to 75 against ×2.25 predicted, no overlap between groups. At N=25 the term is 0.69 pp and sits under this rig's noise floor, so that row is unresolvable rather than small. Harnesses and raw output: supporting gist.

The capacity question answers itself on the way: ramping until the server stops delivering one mixed packet per client per frame, baseline holds 90 clients and misses at 95; patched holds 95 and misses at 100 — both knees landing identically on a repeat run an hour later. So a single core of this Xeon carries 100 clients on neither build, and any single-core figure at that count comes from a server already behind its frame deadline.

Meanwhile CodeRabbit's re-review certified that CServer "declares and initializes both snapshot buffer members" nine minutes after #320 deleted them — and sized them MAX_NUM_CHANNELS, where the rows are iMaxNumChannels. Reviewing variables that no longer exist isn't static analysis, it's a séance.

@ann0see

ann0see commented Sep 12, 2026

Copy link
Copy Markdown
Owner Author

After I've manually added the dockstrings I assume it's ready for an upstream PR

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants