Skip to content

Instantly share code, notes, and snippets.

@ivanminutillo
Created February 4, 2026 09:29
Show Gist options
  • Select an option

  • Save ivanminutillo/892b5eea4f4c5cda5c256141f5c811dd to your computer and use it in GitHub Desktop.

Select an option

Save ivanminutillo/892b5eea4f4c5cda5c256141f5c811dd to your computer and use it in GitHub Desktop.
k6_liveview.js
// Bonfire LiveView Load Test for k6
//
// Tests: "Can the server handle N concurrent LiveView connections to feed/explore?"
//
// Usage:
// 1. Login to Bonfire in browser, copy session cookie from DevTools
// 2. Run: k6 run -e COOKIE="your_cookie" -e HOST="localhost:4000" benchmarks/load_test/k6_liveview.js
//
// Options:
// HOST - Target host (default: localhost:4000)
// COOKIE - Session cookie value (required)
// HTTPS - Set to "1" for https/wss (default: http/ws)
// VUS - Number of virtual users (default: 50)
// DURATION - Hold duration in seconds (default: 120)
import http from "k6/http";
import ws from "k6/ws";
import { check, sleep } from "k6";
import { Counter, Trend } from "k6/metrics";
// Custom metrics
const wsConnections = new Counter("ws_connections");
const wsErrors = new Counter("ws_errors");
const feedLoadTime = new Trend("feed_load_time", true);
const wsConnectTime = new Trend("ws_connect_time", true);
// Config from environment
const host = __ENV.HOST || "localhost:4000";
const cookie = __ENV.COOKIE;
const https = __ENV.HTTPS === "1";
const protocol = https ? "https" : "http";
const wsProtocol = https ? "wss" : "ws";
const vus = parseInt(__ENV.VUS || "500");
const duration = parseInt(__ENV.DURATION || "120");
export const options = {
scenarios: {
liveview_load: {
executor: "constant-vus",
vus: vus,
duration: `${duration}s`,
},
},
thresholds: {
http_req_duration: ["p(95)<5000"], // 95% of requests under 5s
ws_connections: ["count>0"],
},
};
export default function () {
if (!cookie) {
console.error("COOKIE env var required. Get it from browser DevTools after logging in.");
return;
}
// Step 1: Load feed/explore page
const startFeed = Date.now();
const feedRes = http.get(`${protocol}://${host}/feed/explore`, {
cookies: { bonfire: cookie },
});
feedLoadTime.add(Date.now() - startFeed);
const feedOk = check(feedRes, {
"feed loaded": (r) => r.status === 200,
"has LiveView": (r) => r.body.includes("data-phx-session"),
});
if (!feedOk) {
console.log(`Feed failed: ${feedRes.status}`);
wsErrors.add(1);
return;
}
// Step 2: Extract LiveView tokens
const body = feedRes.body;
const csrfMatch = body.match(/name="csrf-token"[^>]*content="([^"]+)"/);
const sessionMatch = body.match(/data-phx-session="([^"]+)"/);
const staticMatch = body.match(/data-phx-static="([^"]+)"/);
const idMatch = body.match(/data-phx-main[^>]*id="([^"]+)"/);
const csrfToken = csrfMatch ? csrfMatch[1] : null;
const phxSession = sessionMatch ? sessionMatch[1] : null;
const phxStatic = staticMatch ? staticMatch[1] : null;
const phxId = idMatch ? idMatch[1] : "phx-main";
if (!phxSession) {
console.log("No LiveView session found");
wsErrors.add(1);
return;
}
// Step 3: Connect WebSocket
const wsUrl = `${wsProtocol}://${host}/live/websocket?vsn=2.0.0`;
const startWs = Date.now();
const res = ws.connect(
wsUrl,
{ headers: { Cookie: `bonfire=${cookie}` } },
function (socket) {
socket.on("open", function () {
wsConnectTime.add(Date.now() - startWs);
wsConnections.add(1);
// Join LiveView
socket.send(
JSON.stringify([
"1",
"1",
`lv:${phxId}`,
"phx_join",
{
url: `${protocol}://${host}/feed/explore`,
params: { _csrf_token: csrfToken, _mounts: 0 },
session: phxSession,
static: phxStatic,
},
])
);
});
socket.on("message", function (msg) {
// Handle messages silently, just keep connection alive
});
socket.on("error", function (e) {
console.log(`WebSocket error: ${e}`);
wsErrors.add(1);
});
// Send heartbeats every 30s
socket.setInterval(function () {
socket.send(
JSON.stringify([null, `${Date.now()}`, "phoenix", "heartbeat", {}])
);
}, 30000);
// Hold connection for test duration minus buffer
socket.setTimeout(function () {
socket.close();
}, (duration - 5) * 1000);
}
);
check(res, { "ws connected": (r) => r && r.status === 101 });
}
export function handleSummary(data) {
const summary = {
vus: vus,
duration_s: duration,
feed_requests: data.metrics.http_reqs ? data.metrics.http_reqs.values.count : 0,
feed_p95_ms: data.metrics.feed_load_time ? data.metrics.feed_load_time.values["p(95)"] : null,
ws_connections: data.metrics.ws_connections ? data.metrics.ws_connections.values.count : 0,
ws_errors: data.metrics.ws_errors ? data.metrics.ws_errors.values.count : 0,
ws_connect_p95_ms: data.metrics.ws_connect_time ? data.metrics.ws_connect_time.values["p(95)"] : null,
};
return {
stdout: `
=====================================
Bonfire LiveView Load Test Results
=====================================
VUs: ${summary.vus}
Duration: ${summary.duration_s}s
Feed Requests: ${summary.feed_requests}
Feed p95: ${summary.feed_p95_ms ? summary.feed_p95_ms.toFixed(0) + "ms" : "N/A"}
WS Connections: ${summary.ws_connections}
WS Errors: ${summary.ws_errors}
WS Connect p95: ${summary.ws_connect_p95_ms ? summary.ws_connect_p95_ms.toFixed(0) + "ms" : "N/A"}
=====================================
`,
"benchmarks/output/k6_results.json": JSON.stringify(summary, null, 2),
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment