Summary
CServer::CreateLevelsForAllConChannels() takes its vecvecsData argument by value:
|
bool CreateLevelsForAllConChannels ( const int iNumClients, |
|
const CVector<int>& vecNumAudioChannels, |
|
const CVector<CVector<int16_t>> vecvecsData, |
|
CVector<uint16_t>& vecLevelsOut ); |
bool CreateLevelsForAllConChannels ( const int iNumClients,
const CVector<int>& vecNumAudioChannels,
const CVector<CVector<int16_t>> vecvecsData, // <-- by value
CVector<uint16_t>& vecLevelsOut );
It is called from CServer::OnTimer() — the per-frame realtime audio processing routine — on every frame that has connected clients:
const bool bSendChannelLevels = CreateLevelsForAllConChannels ( iNumClients, vecNumAudioChannels, vecvecsData, vecChannelLevels );
Since CVector<T> derives from std::vector<T>, the by-value parameter deep-copies the entire pre-allocated member vector, every audio frame. Note that vecvecsData is initialized in the CServer constructor to iMaxNumChannels (the -u setting) inner buffers of worst-case stereo size and is never shrunk — so the copy always covers the full configured channel limit, regardless of how many clients are actually connected. A server run with a high -u pays the full cost even when nearly empty.
This allocates in the realtime audio processing routine — which the server otherwise deliberately avoids (see the "no memory must be allocated" worst-case pre-allocation in the CServer constructor, directly above the vecvecsData.Init call).
Data flow and threading
(Context requested in the comments: where the source vector is populated, where the data is processed, and the subsequent flow.)
1. Where vecvecsData is populated — thread and trigger
vecvecsData is a pre-allocated CServer member. The constructor sizes it for the worst case (iMaxNumChannels inner buffers of 2 * DOUBLE_SYSTEM_FRAME_SIZE_SAMPLES samples each), under the explicit invariant that the realtime routine must not allocate:
|
// To avoid audio clitches, in the entire realtime timer audio processing |
|
// routine including the ProcessData no memory must be allocated. Since we |
|
// do not know the required sizes for the vectors, we allocate memory for |
|
// the worst case here: |
|
|
|
// allocate worst case memory for the temporary vectors |
|
vecChanIDsCurConChan.Init ( iMaxNumChannels ); |
|
vecvecfGains.Init ( iMaxNumChannels ); |
|
vecvecfPannings.Init ( iMaxNumChannels ); |
|
vecvecsData.Init ( iMaxNumChannels ); |
|
vecvecsData2.Init ( iMaxNumChannels ); |
|
vecvecsSendData.Init ( iMaxNumChannels ); |
|
vecvecfIntermediateProcBuf.Init ( iMaxNumChannels ); |
|
vecvecbyCodedData.Init ( iMaxNumChannels ); |
|
vecNumAudioChannels.Init ( iMaxNumChannels ); |
|
vecNumFrameSizeConvBlocks.Init ( iMaxNumChannels ); |
|
vecUseDoubleSysFraSizeConvBuf.Init ( iMaxNumChannels ); |
|
vecAudioComprType.Init ( iMaxNumChannels ); |
|
|
|
for ( i = 0; i < iMaxNumChannels; i++ ) |
|
{ |
|
// init vectors storing information of all channels |
|
vecvecfGains[i].Init ( iMaxNumChannels ); |
|
vecvecfPannings[i].Init ( iMaxNumChannels ); |
|
|
|
// we always use stereo audio buffers (which is the worst case) |
|
vecvecsData[i].Init ( 2 /* stereo */ * DOUBLE_SYSTEM_FRAME_SIZE_SAMPLES /* worst case buffer size */ ); |
|
vecvecsData2[i].Init ( 2 /* stereo */ * DOUBLE_SYSTEM_FRAME_SIZE_SAMPLES /* worst case buffer size */ ); |
Incoming network audio does not touch it directly: packets arrive on the socket thread and are placed into each channel's jitter buffer (CChannel). The socket thread never writes vecvecsData.
The trigger that populates it is the periodic frame tick. CHighPrecisionTimer (non-Windows: a dedicated QThread started at TimeCriticalPriority, clock_nanosleep absolute-deadline loop) emits timeout once per server frame — 64 or 128 samples at 48 kHz, i.e. every ~1.33 ms / ~2.67 ms:
|
void CHighPrecisionTimer::run() |
|
{ |
|
// loop until the thread shall be terminated |
|
while ( bRun ) |
|
{ |
|
// call processing routine by fireing signal |
|
|
|
//### TODO: BEGIN ###// |
|
// by emit a signal we leave the high priority thread -> maybe use some |
|
// other connection type to have something like a true callback, e.g. |
|
// "Qt::DirectConnection" -> Can this work? |
|
emit timeout(); |
|
//### TODO: END ###// |
|
|
|
// now wait until the next buffer shall be processed (we |
|
// use the "increment method" to make sure we do not introduce |
|
// a timing drift) |
|
# if defined( __APPLE__ ) || defined( __MACOSX ) |
|
mach_wait_until ( NextEnd ); |
|
|
|
NextEnd += Delay; |
|
# else |
|
clock_nanosleep ( CLOCK_MONOTONIC, TIMER_ABSTIME, &NextEnd, NULL ); |
|
|
|
NextEnd.tv_nsec += Delay; |
|
if ( NextEnd.tv_nsec >= 1000000000L ) |
|
{ |
|
NextEnd.tv_sec++; |
|
NextEnd.tv_nsec -= 1000000000L; |
|
} |
|
# endif |
|
} |
|
} |
Because the signal crosses threads, the default (queued) connection at server.cpp:268 delivers CServer::OnTimer() on the thread CServer lives in (the Qt main/event thread of the server process). OnTimer() is the entire per-frame pipeline and must complete within one frame period.
Inside OnTimer(), under QMutexLocker ( &Mutex ), DecodeReceiveData() pulls the coded frame for each connected channel from its jitter buffer (vecChannels[id].GetData(...)) and writes decoded PCM into vecvecsData[iChanCnt] — opus_custom_decode for OPUS, memcpy for raw audio, memset silence for lost packets:
|
if ( !bIsRawAudio ) |
|
{ |
|
// OPUS decode received data stream |
|
if ( CurOpusDecoder != nullptr ) |
|
{ |
|
iUnused = opus_custom_decode ( CurOpusDecoder, |
|
pCurCodedData, |
|
iCeltNumCodedBytes, |
|
&vecvecsData[iChanCnt][iOffset], |
|
iClientFrameSizeSamples ); |
|
} |
|
} |
|
else if ( pCurCodedData != nullptr ) |
|
{ |
|
// copy received raw data stream |
|
memcpy ( &vecvecsData[iChanCnt][iOffset], pCurCodedData, iCeltNumCodedBytes ); |
|
} |
|
else |
|
{ |
|
// lost packet - fill with silence |
|
memset ( &vecvecsData[iChanCnt][iOffset], 0, iCeltNumCodedBytes ); |
|
} |
With multithreading enabled the decode fans out over the thread pool (DecodeReceiveDataBlocks), but OnTimer() waits on all futures before leaving that block (server.cpp:713-718). So by the time execution reaches the call in question, population is complete and no other thread is writing to vecvecsData.
2. Where the data is processed — thread
The call at issue happens in the same OnTimer() invocation, on the same thread, immediately after the decode phase:
|
if ( iNumClients > 0 ) |
|
{ |
|
// calculate levels for all connected clients |
|
const bool bSendChannelLevels = CreateLevelsForAllConChannels ( iNumClients, vecNumAudioChannels, vecvecsData, vecChannelLevels ); |
CreateLevelsForAllConChannels() runs inline (never on the pool) and only reads the data: once every CHANNEL_LEVEL_UPDATE_INTERVAL (200) frames it passes vecvecsData[j] — as const CVector<short>& — to CChannel::UpdateAndGetLevelForMeterdB() to compute each client's meter level; on the other 199 frames the body does nothing but iFrameCount++:
|
bool CServer::CreateLevelsForAllConChannels ( const int iNumClients, |
|
const CVector<int>& vecNumAudioChannels, |
|
const CVector<CVector<int16_t>> vecvecsData, |
|
CVector<uint16_t>& vecLevelsOut ) |
|
{ |
|
bool bLevelsWereUpdated = false; |
|
|
|
// low frequency updates |
|
if ( iFrameCount > CHANNEL_LEVEL_UPDATE_INTERVAL ) |
|
{ |
|
iFrameCount = 0; |
|
bLevelsWereUpdated = true; |
|
|
|
for ( int j = 0; j < iNumClients; j++ ) |
|
{ |
|
// update and get signal level for meter in dB for each channel |
|
const double dCurSigLevelForMeterdB = vecChannels[vecChanIDsCurConChan[j]].UpdateAndGetLevelForMeterdB ( vecvecsData[j], |
|
iServerFrameSizeSamples, |
|
vecNumAudioChannels[j] > 1 ); |
|
|
|
// map value to integer for transmission via the protocol (4 bit available) |
|
vecLevelsOut[j] = static_cast<uint16_t> ( std::ceil ( dCurSigLevelForMeterdB ) ); |
|
} |
|
} |
|
|
|
// increment the frame counter needed for low frequency update trigger |
|
iFrameCount++; |
|
|
|
if ( bUseDoubleSystemFrameSize ) |
|
{ |
|
// additional increment needed for double frame size to get to the same time interval |
|
iFrameCount++; |
|
} |
|
|
|
return bLevelsWereUpdated; |
|
} |
The by-value parameter, however, deep-copies all iMaxNumChannels inner buffers on every call — including the 199-in-200 frames where the copy is never even read.
Since the decode futures have already been joined and the socket thread only writes to the jitter buffers, there is no concurrent writer at the call site — the copy cannot be serving as a thread-safety snapshot. The existing implementation gains nothing from it.
3. Subsequent flow
- Levels out: when the interval fires,
vecLevelsOut (= vecChannelLevels) is sent to every connected client via ConnLessProtocol.CreateCLChannelLevelListMes() (server.cpp:746-749) — the channel-level list protocol message that drives the client-side input meters.
vecvecsData afterwards, same tick: the very same member vector is then consumed by the rest of the frame pipeline:
- recording, if enabled:
emit AudioFrame ( ..., vecvecsData[iChanCnt] ) (server.cpp:754-758) — here the queued signal makes its own copy, which is exactly the one place a cross-thread copy is genuinely needed;
- mixing/encode/transmit:
MixEncodeTransmitData() reads it via const CVector<int16_t>& vecsData = vecvecsData[j]; (server.cpp:1037, 1099-1100) — i.e. the mixer accesses the identical data in the identical tick by const reference, which is the established access pattern for this vector;
- with
--delaypan, the frame is copied element-wise into vecvecsData2 for the next frame (server.cpp:793-802).
- Next tick overwrites the buffers in place; nothing outside
OnTimer()'s frame pipeline ever reads vecvecsData.
Impact / measurement
A standalone allocation-counting reproduction of the exact copy (150 channels, i.e. -u 150 = MAX_NUM_CHANNELS; 2×128 int16_t per buffer; ~375 frames/s at 128-sample double frame size) measured:
- ~56,600 heap allocations per second
- ~30 MB per second copied
purely from the by-value parameter. The copy is not elided at -O2. At the default -u 10 the same mechanism still costs ~4,100 allocations/s and ~2 MB/s. On any server with at least one client connected this is continuous, avoidable allocation and memory-bandwidth pressure on the most timing-critical code path in the server.
Fix
Pass vecvecsData by const reference (the function only reads it), matching the adjacent vecNumAudioChannels parameter and the const-reference access the mixer already uses on the same data in the same frame. Proposed in #3803.
The neighbouring vecNumAudioChannels is already const&, so this is just an oversight on the one parameter.
Summary
CServer::CreateLevelsForAllConChannels()takes itsvecvecsDataargument by value:jamulus/src/server.h
Lines 243 to 246 in 6b7a0ad
It is called from
CServer::OnTimer()— the per-frame realtime audio processing routine — on every frame that has connected clients:Since
CVector<T>derives fromstd::vector<T>, the by-value parameter deep-copies the entire pre-allocated member vector, every audio frame. Note thatvecvecsDatais initialized in theCServerconstructor toiMaxNumChannels(the-usetting) inner buffers of worst-case stereo size and is never shrunk — so the copy always covers the full configured channel limit, regardless of how many clients are actually connected. A server run with a high-upays the full cost even when nearly empty.This allocates in the realtime audio processing routine — which the server otherwise deliberately avoids (see the "no memory must be allocated" worst-case pre-allocation in the
CServerconstructor, directly above thevecvecsData.Initcall).Data flow and threading
(Context requested in the comments: where the source vector is populated, where the data is processed, and the subsequent flow.)
1. Where
vecvecsDatais populated — thread and triggervecvecsDatais a pre-allocatedCServermember. The constructor sizes it for the worst case (iMaxNumChannelsinner buffers of2 * DOUBLE_SYSTEM_FRAME_SIZE_SAMPLESsamples each), under the explicit invariant that the realtime routine must not allocate:jamulus/src/server.cpp
Lines 168 to 195 in 6b7a0ad
Incoming network audio does not touch it directly: packets arrive on the socket thread and are placed into each channel's jitter buffer (
CChannel). The socket thread never writesvecvecsData.The trigger that populates it is the periodic frame tick.
CHighPrecisionTimer(non-Windows: a dedicatedQThreadstarted atTimeCriticalPriority,clock_nanosleepabsolute-deadline loop) emitstimeoutonce per server frame — 64 or 128 samples at 48 kHz, i.e. every ~1.33 ms / ~2.67 ms:jamulus/src/util.cpp
Lines 345 to 377 in 6b7a0ad
Because the signal crosses threads, the default (queued) connection at
server.cpp:268deliversCServer::OnTimer()on the threadCServerlives in (the Qt main/event thread of the server process).OnTimer()is the entire per-frame pipeline and must complete within one frame period.Inside
OnTimer(), underQMutexLocker ( &Mutex ),DecodeReceiveData()pulls the coded frame for each connected channel from its jitter buffer (vecChannels[id].GetData(...)) and writes decoded PCM intovecvecsData[iChanCnt]—opus_custom_decodefor OPUS,memcpyfor raw audio,memsetsilence for lost packets:jamulus/src/server.cpp
Lines 981 to 1002 in 6b7a0ad
With multithreading enabled the decode fans out over the thread pool (
DecodeReceiveDataBlocks), butOnTimer()waits on all futures before leaving that block (server.cpp:713-718). So by the time execution reaches the call in question, population is complete and no other thread is writing tovecvecsData.2. Where the data is processed — thread
The call at issue happens in the same
OnTimer()invocation, on the same thread, immediately after the decode phase:jamulus/src/server.cpp
Lines 732 to 735 in 6b7a0ad
CreateLevelsForAllConChannels()runs inline (never on the pool) and only reads the data: once everyCHANNEL_LEVEL_UPDATE_INTERVAL(200) frames it passesvecvecsData[j]— asconst CVector<short>&— toCChannel::UpdateAndGetLevelForMeterdB()to compute each client's meter level; on the other 199 frames the body does nothing butiFrameCount++:jamulus/src/server.cpp
Lines 1676 to 1711 in 6b7a0ad
The by-value parameter, however, deep-copies all
iMaxNumChannelsinner buffers on every call — including the 199-in-200 frames where the copy is never even read.Since the decode futures have already been joined and the socket thread only writes to the jitter buffers, there is no concurrent writer at the call site — the copy cannot be serving as a thread-safety snapshot. The existing implementation gains nothing from it.
3. Subsequent flow
vecLevelsOut(=vecChannelLevels) is sent to every connected client viaConnLessProtocol.CreateCLChannelLevelListMes()(server.cpp:746-749) — the channel-level list protocol message that drives the client-side input meters.vecvecsDataafterwards, same tick: the very same member vector is then consumed by the rest of the frame pipeline:emit AudioFrame ( ..., vecvecsData[iChanCnt] )(server.cpp:754-758) — here the queued signal makes its own copy, which is exactly the one place a cross-thread copy is genuinely needed;MixEncodeTransmitData()reads it viaconst CVector<int16_t>& vecsData = vecvecsData[j];(server.cpp:1037,1099-1100) — i.e. the mixer accesses the identical data in the identical tick by const reference, which is the established access pattern for this vector;--delaypan, the frame is copied element-wise intovecvecsData2for the next frame (server.cpp:793-802).OnTimer()'s frame pipeline ever readsvecvecsData.Impact / measurement
A standalone allocation-counting reproduction of the exact copy (150 channels, i.e.
-u 150=MAX_NUM_CHANNELS; 2×128int16_tper buffer; ~375 frames/s at 128-sample double frame size) measured:purely from the by-value parameter. The copy is not elided at
-O2. At the default-u 10the same mechanism still costs ~4,100 allocations/s and ~2 MB/s. On any server with at least one client connected this is continuous, avoidable allocation and memory-bandwidth pressure on the most timing-critical code path in the server.Fix
Pass
vecvecsDatabyconstreference (the function only reads it), matching the adjacentvecNumAudioChannelsparameter and the const-reference access the mixer already uses on the same data in the same frame. Proposed in #3803.The neighbouring
vecNumAudioChannelsis alreadyconst&, so this is just an oversight on the one parameter.