test(media): gate explicit cap3 topology (#168)
Preserve the cap2 baseline while requiring observed Host3 and relay3 topology in explicit cap3 benchmark runs.
This commit is contained in:
1 parent
b81aa75cda
commit
0389364731
3 files changed
+139
-15
No files matched your search
@@ -255,6 +255,11 @@ no fatal error. The run proves the two-child control path and bound on local
|
||||
Chrome only. It does not prove shared encoding, CPU/GPU cost, visual quality,
|
||||
games, mobile resources, public networking, or endurance.
|
||||
|
||||
The loopback runner retains cap2 by default; `BENCHMARK_EXPECTED_ENDPOINT_CAP=3`
|
||||
configures only its local server and gate. A 2026-08-22 Chrome 151 two-second
|
||||
smoke passed with three Viewers at Host2/relay1 and six at Host3/relay3, with all
|
||||
Viewers decoding. This is not a production-default, SFU-capacity, or resource claim.
|
||||
|
||||
Peer multicast research such as SplitStream demonstrates why load-balanced,
|
||||
failure-tolerant overlays normally introduce multiple trees and content
|
||||
striping. Those mechanisms are intentionally excluded: needing them is a reason
|
||||
|
||||
@@ -6,7 +6,11 @@ import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
|
||||
import WebSocket from "ws";
|
||||
import type { ParticipantRouteAssignment } from "../src/shared/protocol";
|
||||
import {
|
||||
DEFAULT_PEER_RELAY_DOWNSTREAM_EDGES,
|
||||
MAX_PEER_RELAY_DOWNSTREAM_EDGES,
|
||||
type ParticipantRouteAssignment,
|
||||
} from "../src/shared/protocol";
|
||||
import {
|
||||
createScreenerServer,
|
||||
type ScreenerServer,
|
||||
@@ -30,6 +34,7 @@ type PageRole = "host" | "viewer";
|
||||
export interface BenchmarkConfig {
|
||||
chromePath: string;
|
||||
viewerCounts: number[];
|
||||
expectedEndpointCap: number;
|
||||
profileId: ProfileId;
|
||||
durationMs: number;
|
||||
settleMs: number;
|
||||
@@ -358,6 +363,22 @@ function parseBoolean(value: string | undefined, fallback: boolean, name: string
|
||||
throw new Error(`${name} must be true, false, 1, or 0`);
|
||||
}
|
||||
|
||||
export function parseExpectedEndpointCap(value: string | undefined): number {
|
||||
const parsed = value?.trim()
|
||||
? Number(value)
|
||||
: DEFAULT_PEER_RELAY_DOWNSTREAM_EDGES;
|
||||
if (
|
||||
!Number.isSafeInteger(parsed) ||
|
||||
parsed < DEFAULT_PEER_RELAY_DOWNSTREAM_EDGES ||
|
||||
parsed > MAX_PEER_RELAY_DOWNSTREAM_EDGES
|
||||
) {
|
||||
throw new Error(
|
||||
`BENCHMARK_EXPECTED_ENDPOINT_CAP must be an integer from ${DEFAULT_PEER_RELAY_DOWNSTREAM_EDGES} to ${MAX_PEER_RELAY_DOWNSTREAM_EDGES}`,
|
||||
);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
export function parseBenchmarkConfig(
|
||||
environment: NodeJS.ProcessEnv = process.env,
|
||||
): BenchmarkConfig {
|
||||
@@ -395,6 +416,9 @@ export function parseBenchmarkConfig(
|
||||
return {
|
||||
chromePath,
|
||||
viewerCounts,
|
||||
expectedEndpointCap: parseExpectedEndpointCap(
|
||||
environment.BENCHMARK_EXPECTED_ENDPOINT_CAP,
|
||||
),
|
||||
profileId,
|
||||
durationMs:
|
||||
parseNumber(
|
||||
@@ -694,7 +718,12 @@ export function buildRunChecks(
|
||||
summary: NonNullable<BenchmarkRun["summary"]>,
|
||||
viewerCount: number,
|
||||
profileId: ProfileId,
|
||||
expectedEndpointCap = DEFAULT_PEER_RELAY_DOWNSTREAM_EDGES,
|
||||
): RunCheck[] {
|
||||
const endpointCapObserved =
|
||||
summary.maxHostAssignedChildren === expectedEndpointCap &&
|
||||
(viewerCount < expectedEndpointCap * 2 ||
|
||||
summary.maxRelayActiveMediaEdges === expectedEndpointCap);
|
||||
const sfuConsistencyCheck: RunCheck = summary.sfuPublicationObserved
|
||||
? {
|
||||
name: "sfu-route-consistency",
|
||||
@@ -712,22 +741,33 @@ export function buildRunChecks(
|
||||
return [
|
||||
{
|
||||
name: "host-active-media-edges",
|
||||
passed: summary.maxHostActiveMediaEdges <= 2,
|
||||
passed: summary.maxHostActiveMediaEdges <= expectedEndpointCap,
|
||||
actual: summary.maxHostActiveMediaEdges,
|
||||
expected: "<= 2",
|
||||
expected: `<= ${expectedEndpointCap}`,
|
||||
},
|
||||
{
|
||||
name: "host-assigned-children",
|
||||
passed: summary.maxHostAssignedChildren <= 2,
|
||||
passed: summary.maxHostAssignedChildren <= expectedEndpointCap,
|
||||
actual: summary.maxHostAssignedChildren,
|
||||
expected: "<= 2",
|
||||
expected: `<= ${expectedEndpointCap}`,
|
||||
},
|
||||
{
|
||||
name: "relay-active-media-edges",
|
||||
passed: summary.maxRelayActiveMediaEdges <= 2,
|
||||
passed: summary.maxRelayActiveMediaEdges <= expectedEndpointCap,
|
||||
actual: summary.maxRelayActiveMediaEdges,
|
||||
expected: "<= 2",
|
||||
expected: `<= ${expectedEndpointCap}`,
|
||||
},
|
||||
...(expectedEndpointCap > DEFAULT_PEER_RELAY_DOWNSTREAM_EDGES &&
|
||||
viewerCount >= expectedEndpointCap
|
||||
? [
|
||||
{
|
||||
name: "endpoint-cap-observed",
|
||||
passed: endpointCapObserved,
|
||||
actual: endpointCapObserved,
|
||||
expected: `${expectedEndpointCap} Host children and required relay fanout`,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
name: "all-viewers-decoded",
|
||||
passed: summary.everyViewerDecoded,
|
||||
@@ -752,20 +792,23 @@ export function buildRunChecks(
|
||||
];
|
||||
}
|
||||
|
||||
export function buildRecoveryCheck(recovery: RecoveryResult): RunCheck {
|
||||
export function buildRecoveryCheck(
|
||||
recovery: RecoveryResult,
|
||||
expectedEndpointCap = DEFAULT_PEER_RELAY_DOWNSTREAM_EDGES,
|
||||
): RunCheck {
|
||||
const passed =
|
||||
recovery.triggered &&
|
||||
recovery.error === undefined &&
|
||||
recovery.recoveredAtEpochMs !== undefined &&
|
||||
recovery.maxHostActiveMediaEdges !== undefined &&
|
||||
recovery.maxHostActiveMediaEdges <= 2 &&
|
||||
recovery.maxHostActiveMediaEdges <= expectedEndpointCap &&
|
||||
recovery.maxHostAssignedChildren !== undefined &&
|
||||
recovery.maxHostAssignedChildren <= 2;
|
||||
recovery.maxHostAssignedChildren <= expectedEndpointCap;
|
||||
return {
|
||||
name: "relay-recovery-correctness",
|
||||
passed,
|
||||
actual: passed,
|
||||
expected: "decoded frames resume with host media edges and assigned children <= 2",
|
||||
expected: `decoded frames resume with host media edges and assigned children <= ${expectedEndpointCap}`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -777,6 +820,7 @@ export function buildBenchmarkInitScript(options: {
|
||||
width: number;
|
||||
height: number;
|
||||
frameRate: number;
|
||||
expectedEndpointCap: number;
|
||||
}): string {
|
||||
const serialized = JSON.stringify(options);
|
||||
return `(() => {
|
||||
@@ -835,7 +879,7 @@ export function buildBenchmarkInitScript(options: {
|
||||
}
|
||||
if (
|
||||
!Array.isArray(value.childPeerIds) ||
|
||||
value.childPeerIds.length > 2 ||
|
||||
value.childPeerIds.length > options.expectedEndpointCap ||
|
||||
!value.childPeerIds.every(isOpaqueId) ||
|
||||
new Set(value.childPeerIds).size !== value.childPeerIds.length ||
|
||||
(value.sfuPublicationGeneration !== null &&
|
||||
@@ -2030,6 +2074,7 @@ async function runCase(
|
||||
width: captureResolution.width,
|
||||
height: captureResolution.height,
|
||||
frameRate: capture.maxFramerate,
|
||||
expectedEndpointCap: config.expectedEndpointCap,
|
||||
};
|
||||
const hostPage = await createPage(cdp, baseUrl, {
|
||||
...commonInit,
|
||||
@@ -2088,7 +2133,12 @@ async function runCase(
|
||||
samples.push(await samplePages(cdp, pages, measurementStartedAt));
|
||||
|
||||
const summary = summarizeSamples(samples, viewerCount);
|
||||
const checks = buildRunChecks(summary, viewerCount, config.profileId);
|
||||
const checks = buildRunChecks(
|
||||
summary,
|
||||
viewerCount,
|
||||
config.profileId,
|
||||
config.expectedEndpointCap,
|
||||
);
|
||||
if (config.qualityControlSmoke && viewerCount >= 3) {
|
||||
const qualityControl = await runQualityControlSmoke(
|
||||
cdp,
|
||||
@@ -2129,7 +2179,7 @@ async function runCase(
|
||||
config.recoveryTimeoutMs,
|
||||
signal,
|
||||
);
|
||||
checks.push(buildRecoveryCheck(recovery));
|
||||
checks.push(buildRecoveryCheck(recovery, config.expectedEndpointCap));
|
||||
}
|
||||
return {
|
||||
viewerCount,
|
||||
@@ -2211,6 +2261,7 @@ export async function main(): Promise<number> {
|
||||
},
|
||||
configuration: {
|
||||
viewerCounts: config.viewerCounts,
|
||||
expectedEndpointCap: config.expectedEndpointCap,
|
||||
profileId: config.profileId,
|
||||
durationMs: config.durationMs,
|
||||
settleMs: config.settleMs,
|
||||
@@ -2266,6 +2317,7 @@ export async function main(): Promise<number> {
|
||||
ALLOWED_ORIGINS: baseUrl,
|
||||
ROOM_DATABASE_PATH: "",
|
||||
PEER_ASSISTED_MEDIA: "true",
|
||||
MAX_PEER_RELAY_DOWNSTREAM_EDGES: String(config.expectedEndpointCap),
|
||||
MAX_VIEWERS_PER_ROOM: String(Math.max(...config.viewerCounts)),
|
||||
STUN_URLS: "",
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
everyViewerRecoveredMedia,
|
||||
mergeRecoveryHostPeaks,
|
||||
parseBenchmarkConfig,
|
||||
parseExpectedEndpointCap,
|
||||
parseViewerCounts,
|
||||
summarizeSamples,
|
||||
} from "../scripts/peer-assisted-benchmark";
|
||||
@@ -142,7 +143,7 @@ interface ObserverSnapshot {
|
||||
maxAssignedChildren: number;
|
||||
}
|
||||
|
||||
function createObserverHarness() {
|
||||
function createObserverHarness(expectedEndpointCap = 2) {
|
||||
const context: Record<string, unknown> = {
|
||||
WebSocket: FakeWebSocket,
|
||||
RTCPeerConnection: class FakePeerConnection {},
|
||||
@@ -162,6 +163,7 @@ function createObserverHarness() {
|
||||
width: 1280,
|
||||
height: 720,
|
||||
frameRate: 30,
|
||||
expectedEndpointCap,
|
||||
}).replace(
|
||||
'const statsModule = import("/src/client/webrtc/stats.ts");',
|
||||
"const statsModule = new Promise(() => undefined);",
|
||||
@@ -230,6 +232,19 @@ describe("peer topology loopback configuration", () => {
|
||||
).toThrow(/selected viewer count/);
|
||||
});
|
||||
|
||||
it("keeps cap2 by default and accepts only an explicit cap3 expectation", () => {
|
||||
expect(parseExpectedEndpointCap(undefined)).toBe(2);
|
||||
for (const value of ["1", "2.5", "4"]) {
|
||||
expect(() => parseExpectedEndpointCap(value)).toThrow(/integer from 2 to 3/);
|
||||
}
|
||||
expect(
|
||||
parseBenchmarkConfig({
|
||||
CHROME_PATH: "chrome",
|
||||
BENCHMARK_EXPECTED_ENDPOINT_CAP: "3",
|
||||
}).expectedEndpointCap,
|
||||
).toBe(3);
|
||||
});
|
||||
|
||||
it("requires a relay-sized case for the optional quality control smoke", () => {
|
||||
expect(() =>
|
||||
parseBenchmarkConfig({
|
||||
@@ -298,6 +313,38 @@ describe("peer topology loopback observations", () => {
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("requires an explicit cap3 run to exercise cap3 fanout", () => {
|
||||
const initial = [
|
||||
page("host", "host", 3, 0),
|
||||
...Array.from({ length: 6 }, (_, index) =>
|
||||
page("viewer", `viewer-${index + 1}`, index === 0 ? 3 : 0, 1),
|
||||
),
|
||||
];
|
||||
const final = structuredClone(initial);
|
||||
for (const viewer of final.filter((entry) => entry.role === "viewer")) {
|
||||
viewer.connections.at(-1)!.receiveTotals!.framesTotal = 20;
|
||||
}
|
||||
const summary = summarizeSamples(
|
||||
[
|
||||
{ atEpochMs: 2_000, elapsedMs: 0, pages: initial },
|
||||
{ atEpochMs: 4_000, elapsedMs: 2_000, pages: final },
|
||||
],
|
||||
6,
|
||||
);
|
||||
const checks = buildRunChecks(summary, 6, "720p30", 3);
|
||||
expect(checks.find((check) => check.name === "endpoint-cap-observed")?.passed).toBe(true);
|
||||
const underused = {
|
||||
...summary,
|
||||
maxHostAssignedChildren: 2,
|
||||
maxRelayActiveMediaEdges: 2,
|
||||
};
|
||||
expect(
|
||||
buildRunChecks(underused, 6, "720p30", 3).find(
|
||||
(check) => check.name === "endpoint-cap-observed",
|
||||
)?.passed,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("fails continuity when frames freeze or stats collection errors", () => {
|
||||
const frozenInitial = [
|
||||
page("host", "host", 1, 0),
|
||||
@@ -830,6 +877,25 @@ describe("peer topology loopback observations", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("accepts a three-child route only for an explicit cap3 observer", () => {
|
||||
const assignment = routeAssignment(["child_peer_0001", "child_peer_0002", "child_peer_0003"]);
|
||||
for (const [cap, accepted] of [[2, false], [3, true]] as const) {
|
||||
const observer = createObserverHarness(cap);
|
||||
const socket = observer.socket();
|
||||
authenticate(socket);
|
||||
socket.emitMessage({
|
||||
type: "authenticated",
|
||||
role: "host",
|
||||
peerId: "host_peer_0001",
|
||||
mediaMode: "peer-assisted",
|
||||
routeRevision: 1,
|
||||
routeAssignment: assignment,
|
||||
qualitySettings: lowQualitySettings,
|
||||
});
|
||||
expect(observer.snapshot().routeAssignment !== null).toBe(accepted);
|
||||
}
|
||||
});
|
||||
|
||||
it("clears hybrid state only from the current authenticated socket", () => {
|
||||
const observer = createObserverHarness();
|
||||
const previous = observer.socket();
|
||||
@@ -891,6 +957,7 @@ describe("peer topology loopback observations", () => {
|
||||
width: 1280,
|
||||
height: 720,
|
||||
frameRate: 30,
|
||||
expectedEndpointCap: 2,
|
||||
});
|
||||
expect(source).toContain('"width":1280');
|
||||
expect(source).toContain('"frameRate":30');
|
||||
|
||||
Reference in new issue
Block a user