Skip to content

test(sfu): add SFU capacity load test and client instrumentation - #63

Open
Harxhit wants to merge 1 commit into
mainfrom
test/sfu-capacity
Open

test(sfu): add SFU capacity load test and client instrumentation#63
Harxhit wants to merge 1 commit into
mainfrom
test/sfu-capacity

Conversation

@Harxhit

@Harxhit Harxhit commented Aug 27, 2026

Copy link
Copy Markdown
Owner

Summary by cubic

Adds an SFU capacity load test that drives a real media session and measures join and first-frame latency as viewers are added in batches. Results show latency and failures climbing past ~65 concurrent viewers.

Test setup and instrumentation

  • The Puppeteer script logs in, starts a broadcaster, then adds viewers in batches of 5 up to 100, writing P50/P99 latencies and failure counts to load-test/sfu-capacity-results.csv.
  • Broadcaster and viewer pages expose __csRoomId, __csLiveAt, __csJoinedAt, and __csFirstFrameAt on window so the test can time room creation, join, and first frame.
  • /broadcaster, /viewer, and /dashboard are no longer wrapped in ProtectedRoute, so they load without authentication in every environment.
  • Broadcaster connects its socket on mount and disconnects on unmount; ViewerPage switched from getSocket to connectSocket.
  • Backend now logs the socket disconnect reason and details at error level instead of info level.

Written for commit 24b6eae. Summary will update on new commits.

Review in cubic

- add load-test script and results CSV for SFU capacity testing
- expose window globals for test timing (__csSocket, __csRoomId,
  __csLiveAt, __csJoinedAt, __csFirstFrameAt)
- wire socket connect/disconnect lifecycle in Broadcaster; use
  connectSocket in ViewerPage
- unprotect broadcaster/viewer/dashboard routes for load testing
- log socket disconnect reason and details at error level

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@coderabbitai

coderabbitai Bot commented Aug 27, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1be0746b-3ad6-427f-8450-f0d87bb84bfe


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
C Security Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

7 issues found across 8 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="frontend/src/router/index.tsx">

<violation number="1" location="frontend/src/router/index.tsx:19">
P1: These routes are now siblings of `ProtectedRoute`, so React Router renders them without authentication or socket initialization. Move all three routes back inside the protected route.</violation>
</file>

<file name="load-test/sfu-capacity.js">

<violation number="1" location="load-test/sfu-capacity.js:13">
P0: This commits a reusable account password to the repository and makes the load test use that account in every environment. Read both values from `CROWDSTREAM_TEST_EMAIL` and `CROWDSTREAM_TEST_PASSWORD` instead of storing credentials in source.</violation>

<violation number="2" location="load-test/sfu-capacity.js:659">
P2: When `maxViewers` is not divisible by `batchSize`, the test stops below the requested viewer count and reports an incomplete capacity run. Launch a final batch sized to the remaining viewers, or otherwise loop until the actual count reaches `MAX_VIEWERS`.</violation>

<violation number="3" location="load-test/sfu-capacity.js:725">
P2: When signaling succeeds but no video frame arrives, the report records no failure because this count checks only `viewer.joined`. Count viewers without a finite `firstFrameLatencyMs` as failures so the capacity result reflects missing media.</violation>
</file>

<file name="backend/src/utils/socket.util.ts">

<violation number="1" location="backend/src/utils/socket.util.ts:144">
P2: Routine disconnections are now logged at `error` level. Disconnects are a normal event (user closes tab, network blip) and will be written to `logs/error.log` per the winston transport in `logging.ts` and counted by any error-based monitoring, flooding them with non-errors and masking real failures. Keep this at `info`, or add a separate debug/warn level if you need it prominent during the load test.</violation>
</file>

<file name="frontend/src/pages/ViewerPage.tsx">

<violation number="1" location="frontend/src/pages/ViewerPage.tsx:24">
P2: connectSocket() has side effects (opening the socket connection, attaching event listeners, mutating window.__csSocket) but is now called during the render body. Render-phase side effects violate React's purity contract and can open a connection for a render that is later discarded or re-run (e.g. under StrictMode/Suspense). Move the connection into the component's useEffect and keep the render body pure.</violation>
</file>

<file name="load-test/sfu-capacity-results.csv">

<violation number="1" location="load-test/sfu-capacity-results.csv:1">
P2: P50 equals P99 in every row for both join and first-frame latency, so these columns convey no actual latency distribution. Because the run used one viewer per batch, each 'percentile' is computed over a single sample and the P50/P99 labels are misleading to anyone reading the capacity results. Rerun with a meaningful batch size (or report per-viewer samples) so P50/P99 differ and the spread is visible.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread load-test/sfu-capacity.js
Comment on lines +13 to +14
const TEST_EMAIL = "harsxit04@gmail.com"
const TEST_PASSWORD = "@Harshit1308"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P0: This commits a reusable account password to the repository and makes the load test use that account in every environment. Read both values from CROWDSTREAM_TEST_EMAIL and CROWDSTREAM_TEST_PASSWORD instead of storing credentials in source.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At load-test/sfu-capacity.js, line 13:

<comment>This commits a reusable account password to the repository and makes the load test use that account in every environment. Read both values from `CROWDSTREAM_TEST_EMAIL` and `CROWDSTREAM_TEST_PASSWORD` instead of storing credentials in source.</comment>

<file context>
@@ -0,0 +1,854 @@
+const BASE_URL = arg("baseUrl", "http://localhost");
+// const TOKEN = arg("token", null);
+
+const TEST_EMAIL = "harsxit04@gmail.com"
+const TEST_PASSWORD = "@Harshit1308"
+
</file context>
Suggested change
const TEST_EMAIL = "harsxit04@gmail.com"
const TEST_PASSWORD = "@Harshit1308"
const TEST_EMAIL = process.env.CROWDSTREAM_TEST_EMAIL
const TEST_PASSWORD = process.env.CROWDSTREAM_TEST_PASSWORD

<Route path="/signin" element={<SignInPage />} />
<Route path="/signup" element={<SignUpPage />} />

<Route path="/broadcaster" element={<BroadcasterPage />} />

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: These routes are now siblings of ProtectedRoute, so React Router renders them without authentication or socket initialization. Move all three routes back inside the protected route.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At frontend/src/router/index.tsx, line 19:

<comment>These routes are now siblings of `ProtectedRoute`, so React Router renders them without authentication or socket initialization. Move all three routes back inside the protected route.</comment>

<file context>
@@ -16,10 +16,10 @@ export default function Router() {
         <Route path="/signin" element={<SignInPage />} />
         <Route path="/signup" element={<SignUpPage />} />
 
+        <Route path="/broadcaster" element={<BroadcasterPage />} />
+        <Route path="/viewer" element={<ViewerPage />} />
+        <Route path="/dashboard" element={<DashboardPage />} />
</file context>

Comment thread load-test/sfu-capacity.js

for (
let target = BATCH_SIZE;
target <= MAX_VIEWERS;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When maxViewers is not divisible by batchSize, the test stops below the requested viewer count and reports an incomplete capacity run. Launch a final batch sized to the remaining viewers, or otherwise loop until the actual count reaches MAX_VIEWERS.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At load-test/sfu-capacity.js, line 659:

<comment>When `maxViewers` is not divisible by `batchSize`, the test stops below the requested viewer count and reports an incomplete capacity run. Launch a final batch sized to the remaining viewers, or otherwise loop until the actual count reaches `MAX_VIEWERS`.</comment>

<file context>
@@ -0,0 +1,854 @@
+
+    for (
+      let target = BATCH_SIZE;
+      target <= MAX_VIEWERS;
+      target += BATCH_SIZE
+    ) {
</file context>

Comment thread load-test/sfu-capacity.js
const failed =
batch.filter(
(viewer) =>
!viewer.joined

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When signaling succeeds but no video frame arrives, the report records no failure because this count checks only viewer.joined. Count viewers without a finite firstFrameLatencyMs as failures so the capacity result reflects missing media.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At load-test/sfu-capacity.js, line 725:

<comment>When signaling succeeds but no video frame arrives, the report records no failure because this count checks only `viewer.joined`. Count viewers without a finite `firstFrameLatencyMs` as failures so the capacity result reflects missing media.</comment>

<file context>
@@ -0,0 +1,854 @@
+      const failed =
+        batch.filter(
+          (viewer) =>
+            !viewer.joined
+        ).length;
+      
</file context>

socket.on("disconnect", async (reason) => {
logger.info(`User disconnected ${socket.id} beacuse of ${reason}`)
socket.on("disconnect", async (reason, details) => {
logger.error(`User disconnected, ${socket.id} reason: ${reason} details: ${details}`)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Routine disconnections are now logged at error level. Disconnects are a normal event (user closes tab, network blip) and will be written to logs/error.log per the winston transport in logging.ts and counted by any error-based monitoring, flooding them with non-errors and masking real failures. Keep this at info, or add a separate debug/warn level if you need it prominent during the load test.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/src/utils/socket.util.ts, line 144:

<comment>Routine disconnections are now logged at `error` level. Disconnects are a normal event (user closes tab, network blip) and will be written to `logs/error.log` per the winston transport in `logging.ts` and counted by any error-based monitoring, flooding them with non-errors and masking real failures. Keep this at `info`, or add a separate debug/warn level if you need it prominent during the load test.</comment>

<file context>
@@ -140,8 +140,8 @@ io.on("connection", (socket) => {
-  socket.on("disconnect", async (reason) => {
-    logger.info(`User disconnected ${socket.id} beacuse of ${reason}`)
+  socket.on("disconnect", async (reason, details) => {
+    logger.error(`User disconnected, ${socket.id} reason: ${reason} details: ${details}`)
     handleDisconnect(socket)
     await stopFfmpegRecording(socket.id)
</file context>
Suggested change
logger.error(`User disconnected, ${socket.id} reason: ${reason} details: ${details}`)
logger.info(`User disconnected, ${socket.id} reason: ${reason} details: ${details}`)


export default function ViewerPage() {
const socket = getSocket()
const socket = connectSocket()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: connectSocket() has side effects (opening the socket connection, attaching event listeners, mutating window.__csSocket) but is now called during the render body. Render-phase side effects violate React's purity contract and can open a connection for a render that is later discarded or re-run (e.g. under StrictMode/Suspense). Move the connection into the component's useEffect and keep the render body pure.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At frontend/src/pages/ViewerPage.tsx, line 24:

<comment>connectSocket() has side effects (opening the socket connection, attaching event listeners, mutating window.__csSocket) but is now called during the render body. Render-phase side effects violate React's purity contract and can open a connection for a render that is later discarded or re-run (e.g. under StrictMode/Suspense). Move the connection into the component's useEffect and keep the render body pure.</comment>

<file context>
@@ -20,7 +21,7 @@ interface Log {
 
 export default function ViewerPage() {
-  const socket = getSocket()
+  const socket = connectSocket()
   const [searchParams] = useSearchParams();
 
</file context>

@@ -0,0 +1,101 @@
timestamp,viewers,joinP50,joinP99,firstFrameP50,firstFrameP99,failures

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: P50 equals P99 in every row for both join and first-frame latency, so these columns convey no actual latency distribution. Because the run used one viewer per batch, each 'percentile' is computed over a single sample and the P50/P99 labels are misleading to anyone reading the capacity results. Rerun with a meaningful batch size (or report per-viewer samples) so P50/P99 differ and the spread is visible.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At load-test/sfu-capacity-results.csv, line 1:

<comment>P50 equals P99 in every row for both join and first-frame latency, so these columns convey no actual latency distribution. Because the run used one viewer per batch, each 'percentile' is computed over a single sample and the P50/P99 labels are misleading to anyone reading the capacity results. Rerun with a meaningful batch size (or report per-viewer samples) so P50/P99 differ and the spread is visible.</comment>

<file context>
@@ -0,0 +1,101 @@
+timestamp,viewers,joinP50,joinP99,firstFrameP50,firstFrameP99,failures
+2026-08-19T03:33:16.173Z,1,341,341,362,362,0
+2026-08-19T03:33:20.123Z,2,346,346,388,388,0
</file context>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant