perf(nests): bound encoder CSD loop, single-alloc audio framing, cache catalog JSON
Three audit follow-ups, all in the audio publish hot path: Audit-3: MediaCodecOpusEncoder.encode could busy-loop on a buggy encoder that emits BUFFER_FLAG_CODEC_CONFIG on every dequeue. The existing format-change path has a one-shot `formatChangeAbsorbed` guard for exactly this reason; the new CSD-skip path didn't. Cap consecutive CSD skips per encode call at MAX_CSD_SKIPS_PER_CALL=4 (generous: Codec2 emits 1 OpusHead or 2 OpusHead+OpusTags in practice). On overshoot, log a warning and return ByteArray(0) — the broadcaster's `if (opus.isEmpty()) continue` already handles that contract. Audit-6: NestMoqLiteBroadcaster's per-frame send path allocated twice per Opus packet — once for the timestamp Varint and once for the concatenated `varint+opus` payload. At 50 fps × N speakers that's measurable young-gen pressure. Switch to the same single-allocation pattern PublisherStateImpl.send already uses: compute Varint.size(timestampUs), allocate the final payload buffer once, write the varint directly into it via Varint.writeTo, then copy the opus bytes. Drops audio-thread allocations from 3/frame to 1/frame (the third was the wrap in PublisherStateImpl.send, already optimised). Audit-7: MoqLiteNestsSpeaker.startBroadcasting and ReconnectingNestsSpeaker.runHotSwapIteration both encode the same fixed catalog JSON on every call — once per broadcast start and once per JWT-refresh hot-swap iteration. Cache the encoded bytes on MoqLiteHangCatalog.Companion.OPUS_MONO_48K_AUDIO_DATA_JSON_BYTES so the kotlinx.serialization run happens at class-init time and both call sites read a static reference. Trivial perf, but co-locates the "default Amethyst publish shape" in one place. https://claude.ai/code/session_014JfZJHSTvyYYWJbC9VbB47
This commit is contained in:
+31
@@ -91,6 +91,13 @@ class MediaCodecOpusEncoder(
|
||||
// without producing output, which would otherwise busy-spin at
|
||||
// 100 Hz against the 10 ms dequeue timeout).
|
||||
var formatChangeAbsorbed = false
|
||||
// CSD-skip iteration cap. Codec2 stacks have been observed to
|
||||
// emit OpusHead AND OpusTags as separate CSD buffers; some
|
||||
// future stack might emit more. Cap the per-call CSD-skip
|
||||
// count so a buggy encoder that emits CSD on every dequeue
|
||||
// can't busy-loop the per-frame budget. 4 is generous: the
|
||||
// expected case is 1 (OpusHead) or 2 (OpusHead + OpusTags).
|
||||
var csdSkipsThisCall = 0
|
||||
while (true) {
|
||||
val outputIndex = codec.dequeueOutputBuffer(bufferInfo, DEQUEUE_TIMEOUT_US)
|
||||
when {
|
||||
@@ -119,6 +126,20 @@ class MediaCodecOpusEncoder(
|
||||
"MediaCodecOpusEncoder skipped ${bufferInfo.size}-byte CODEC_CONFIG (OpusHead/OpusTags) — not an audio frame"
|
||||
}
|
||||
}
|
||||
csdSkipsThisCall += 1
|
||||
if (csdSkipsThisCall >= MAX_CSD_SKIPS_PER_CALL) {
|
||||
// A buggy encoder is emitting CSD on every
|
||||
// dequeue; bail this call rather than
|
||||
// busy-looping the per-frame budget.
|
||||
// Returning empty is the existing "warmup"
|
||||
// contract — broadcaster's
|
||||
// `if (opus.isEmpty()) continue` handles it
|
||||
// and the next encode call retries.
|
||||
com.vitorpamplona.quartz.utils.Log.w("NestTx") {
|
||||
"MediaCodecOpusEncoder hit MAX_CSD_SKIPS_PER_CALL=$MAX_CSD_SKIPS_PER_CALL; bailing this encode call (encoder may be misbehaving)"
|
||||
}
|
||||
return ByteArray(0)
|
||||
}
|
||||
// Don't return; loop to find the next output
|
||||
// buffer (the next dequeue may be the actual
|
||||
// first audio frame).
|
||||
@@ -163,6 +184,16 @@ class MediaCodecOpusEncoder(
|
||||
private const val DEQUEUE_TIMEOUT_US = 10_000L
|
||||
private const val FRAME_DURATION_US = 20_000L
|
||||
|
||||
/**
|
||||
* Per-encode-call cap on consecutive
|
||||
* [MediaCodec.BUFFER_FLAG_CODEC_CONFIG] outputs we'll skip
|
||||
* before bailing the call. Expected steady state is 0; the
|
||||
* one-time startup cost is 1 (OpusHead) or 2 (OpusHead +
|
||||
* OpusTags on Codec2). 4 leaves headroom for a future stack
|
||||
* that emits more without uncapping the loop entirely.
|
||||
*/
|
||||
private const val MAX_CSD_SKIPS_PER_CALL: Int = 4
|
||||
|
||||
private fun buildFormat(bitrate: Int): MediaFormat =
|
||||
MediaFormat
|
||||
.createAudioFormat(
|
||||
|
||||
Reference in New Issue
Block a user