Skip to content

Commit 00a0ad0

Browse files
committed
perf(client): reduce E2EE worker GC pressure and fix silent pipeline failures
1 parent e4f8189 commit 00a0ad0

File tree

2 files changed

+63
-27
lines changed

2 files changed

+63
-27
lines changed

packages/client/src/rtc/e2ee/EncryptionManager.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ import { isChrome } from '../../helpers/browsers';
22
import { type ScopedLogger, videoLoggerSystem } from '../../logger';
33

44
export type PerfReport = {
5-
encode: { fps: number };
5+
encode: { fps: number; maxCryptoMs: number };
66
decode: { userId: string; fps: number }[];
7+
decodeMaxCryptoMs: number;
78
};
89

910
const validateKeyLength = (rawKey: ArrayBuffer) => {
@@ -297,12 +298,13 @@ export class EncryptionManager {
297298
const report: PerfReport = {
298299
encode: e.data.encode,
299300
decode: e.data.decode,
301+
decodeMaxCryptoMs: e.data.decodeMaxCryptoMs,
300302
};
301303
const decodeInfo = report.decode
302304
.map((d) => `${d.userId}: ${d.fps}`)
303305
.join(', ');
304306
this.logger.info(
305-
`[perf] encode: ${report.encode.fps} fps | decode: [${decodeInfo}]`,
307+
`[perf] encode: ${report.encode.fps} fps (max crypto: ${report.encode.maxCryptoMs.toFixed(1)}ms) | decode: [${decodeInfo}] (max crypto: ${report.decodeMaxCryptoMs.toFixed(1)}ms)`,
306308
);
307309
this.onPerfReport?.(report);
308310
}

packages/client/src/rtc/e2ee/worker.ts

Lines changed: 59 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ const E2EE_VERSION = 1;
4747
const TRAILER_LEN = 12; // 4 frameCounter + 1 keyIndex + 2 clearBytes + 1 version + 4 magic
4848
const IV_LEN = 12;
4949
const RBSP_FLAG = 0x8000; // bit 15 of the 2-byte clearBytes field signals RBSP escaping
50+
const EMPTY_AAD = new Uint8Array(0);
5051
5152
// ---- Key Store ----
5253
@@ -160,14 +161,11 @@ function getLatestKey(userId) {
160161
return null;
161162
}
162163
163-
// IV = [8-byte prefix from SHA-256(rawKey + userId)][4-byte frame counter big-endian]
164-
// The prefix ensures unique IVs across users and key rotations.
165-
// The counter ensures unique IVs within a session.
166-
function buildIV(prefix, frameCounter) {
167-
const iv = new Uint8Array(IV_LEN);
164+
// Fill a pre-allocated IV buffer: [8-byte prefix][4-byte frame counter big-endian].
165+
// Called per-frame; the iv/ivView are allocated once per transform to reduce GC pressure.
166+
function fillIV(iv, ivView, prefix, frameCounter) {
168167
iv.set(prefix, 0);
169-
new DataView(iv.buffer).setUint32(8, frameCounter);
170-
return iv.buffer;
168+
ivView.setUint32(8, frameCounter);
171169
}
172170
173171
function nextFrameCounter(userId) {
@@ -299,6 +297,8 @@ let e2eeActive = true;
299297
let perfEnabled = false;
300298
let perfInterval = null;
301299
let encodeFrameCount = 0;
300+
let encodeMaxCryptoMs = 0;
301+
let decodeMaxCryptoMs = 0;
302302
// Map<userId, number>
303303
const decodeFrameCounts = new Map();
304304
@@ -312,10 +312,13 @@ function startPerfReport() {
312312
}
313313
self.postMessage({
314314
type: 'perf-report',
315-
encode: { fps: encodeFrameCount },
315+
encode: { fps: encodeFrameCount, maxCryptoMs: encodeMaxCryptoMs },
316316
decode,
317+
decodeMaxCryptoMs,
317318
});
318319
encodeFrameCount = 0;
320+
encodeMaxCryptoMs = 0;
321+
decodeMaxCryptoMs = 0;
319322
}, 1000);
320323
}
321324
@@ -326,16 +329,23 @@ function stopPerfReport() {
326329
perfInterval = null;
327330
}
328331
encodeFrameCount = 0;
332+
encodeMaxCryptoMs = 0;
333+
decodeMaxCryptoMs = 0;
329334
decodeFrameCounts.clear();
330335
}
331336
332337
// ---- Transforms ----
333338
334-
function encodeTransform(userId, codec) {
339+
async function encodeTransform(userId, codec) {
335340
const isNalu = codec === 'h264';
336-
// Pre-compute shared key IV prefix at transform creation (not per-frame)
341+
// Await IV prefix computation so the first frames aren't silently dropped
337342
const latestEntry = getLatestKey(userId);
338-
if (latestEntry && sharedKey) ensureIVPrefix(userId, latestEntry.keyIndex);
343+
if (latestEntry && sharedKey) await ensureIVPrefix(userId, latestEntry.keyIndex);
344+
345+
// Pre-allocate reusable IV buffer — safe because TransformStream
346+
// processes frames sequentially (next transform call waits for await).
347+
const iv = new Uint8Array(IV_LEN);
348+
const ivView = new DataView(iv.buffer);
339349
340350
return new TransformStream({
341351
async transform(frame, controller) {
@@ -358,16 +368,21 @@ function encodeTransform(userId, codec) {
358368
const src = new Uint8Array(frame.data);
359369
const clearBytes = getClearByteCount(codec, frame.type, src);
360370
const counter = nextFrameCounter(userId);
361-
const iv = buildIV(prefix, counter);
362-
const aad = clearBytes > 0 ? src.subarray(0, clearBytes) : new Uint8Array(0);
363-
const plaintext = src.subarray(clearBytes);
371+
fillIV(iv, ivView, prefix, counter);
372+
const aad = clearBytes > 0 ? src.subarray(0, clearBytes) : EMPTY_AAD;
373+
const plaintext = clearBytes > 0 ? src.subarray(clearBytes) : src;
364374
365375
try {
376+
const t0 = perfEnabled ? performance.now() : 0;
366377
const encrypted = await crypto.subtle.encrypt(
367378
{ name: 'AES-GCM', iv, additionalData: aad },
368379
cryptoKey,
369380
plaintext,
370381
);
382+
if (perfEnabled) {
383+
const dt = performance.now() - t0;
384+
if (dt > encodeMaxCryptoMs) encodeMaxCryptoMs = dt;
385+
}
371386
const ciphertext = new Uint8Array(encrypted);
372387
373388
if (isNalu && clearBytes > 0) {
@@ -394,7 +409,7 @@ function encodeTransform(userId, codec) {
394409
});
395410
}
396411
397-
function decodeTransform(userId) {
412+
async function decodeTransform(userId) {
398413
// Throttle failure notifications to avoid flooding the main thread.
399414
let lastFailureNotification = 0;
400415
const FAILURE_THROTTLE_MS = 1000;
@@ -407,8 +422,13 @@ function decodeTransform(userId) {
407422
}
408423
}
409424
410-
// Pre-compute shared key IV prefix at transform creation (not per-frame)
411-
if (sharedKey) ensureIVPrefix(userId, sharedKey.keyIndex);
425+
// Await IV prefix computation so the first frames aren't silently dropped
426+
if (sharedKey) await ensureIVPrefix(userId, sharedKey.keyIndex);
427+
428+
// Pre-allocate reusable IV buffer — safe because TransformStream
429+
// processes frames sequentially (next transform call waits for await).
430+
const iv = new Uint8Array(IV_LEN);
431+
const ivView = new DataView(iv.buffer);
412432
413433
return new TransformStream({
414434
async transform(frame, controller) {
@@ -445,8 +465,8 @@ function decodeTransform(userId) {
445465
}
446466
447467
const bodyEnd = src.length - TRAILER_LEN;
448-
const iv = buildIV(prefix, frameCounter);
449-
const aad = clearBytes > 0 ? src.subarray(0, clearBytes) : new Uint8Array(0);
468+
fillIV(iv, ivView, prefix, frameCounter);
469+
const aad = clearBytes > 0 ? src.subarray(0, clearBytes) : EMPTY_AAD;
450470
451471
try {
452472
let ciphertext;
@@ -456,11 +476,16 @@ function decodeTransform(userId) {
456476
ciphertext = src.subarray(clearBytes, bodyEnd);
457477
}
458478
479+
const t0 = perfEnabled ? performance.now() : 0;
459480
const decrypted = await crypto.subtle.decrypt(
460481
{ name: 'AES-GCM', iv, additionalData: aad },
461482
cryptoKey,
462483
ciphertext,
463484
);
485+
if (perfEnabled) {
486+
const dt = performance.now() - t0;
487+
if (dt > decodeMaxCryptoMs) decodeMaxCryptoMs = dt;
488+
}
464489
465490
const plaintext = new Uint8Array(decrypted);
466491
const dst = new Uint8Array(clearBytes + plaintext.length);
@@ -481,15 +506,22 @@ function decodeTransform(userId) {
481506
482507
// ---- Message handling ----
483508
484-
function setupTransform({ readable, writable, operation, userId, codec }) {
509+
async function setupTransform({ readable, writable, operation, userId, codec }) {
485510
const transform = operation === 'encode'
486-
? encodeTransform(userId, codec)
487-
: decodeTransform(userId);
488-
readable.pipeThrough(transform).pipeTo(writable);
511+
? await encodeTransform(userId, codec)
512+
: await decodeTransform(userId);
513+
readable.pipeThrough(transform).pipeTo(writable).catch((err) => {
514+
self.postMessage({
515+
type: 'error',
516+
message: 'Transform pipeline error (' + operation + ', ' + userId + '): ' + (err && err.message || err),
517+
});
518+
});
489519
}
490520
491521
addEventListener('rtctransform', ({ transformer: { readable, writable, options } }) => {
492-
setupTransform({ readable, writable, ...options });
522+
setupTransform({ readable, writable, ...options }).catch((err) => {
523+
self.postMessage({ type: 'error', message: 'Transform setup failed: ' + (err && err.message || err) });
524+
});
493525
});
494526
495527
addEventListener('message', ({ data }) => {
@@ -521,7 +553,9 @@ addEventListener('message', ({ data }) => {
521553
break;
522554
default:
523555
// Transform setup (Insertable Streams fallback path)
524-
setupTransform(data);
556+
setupTransform(data).catch((err) => {
557+
self.postMessage({ type: 'error', message: 'Transform setup failed: ' + (err && err.message || err) });
558+
});
525559
break;
526560
}
527561
});

0 commit comments

Comments
 (0)