diff --git a/docs/research/peer-assisted-media.md b/docs/research/peer-assisted-media.md index d095bb7..fe3d005 100644 --- a/docs/research/peer-assisted-media.md +++ b/docs/research/peer-assisted-media.md @@ -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 diff --git a/scripts/peer-assisted-benchmark.ts b/scripts/peer-assisted-benchmark.ts index 0a7d33d..fcf43dc 100644 --- a/scripts/peer-assisted-benchmark.ts +++ b/scripts/peer-assisted-benchmark.ts @@ -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, 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 { }, 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 { 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: "", }); diff --git a/tests/peer-assisted-benchmark.test.ts b/tests/peer-assisted-benchmark.test.ts index b230a3a..771f48d 100644 --- a/tests/peer-assisted-benchmark.test.ts +++ b/tests/peer-assisted-benchmark.test.ts @@ -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 = { 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');