penpot/performance/scripts/workspace-open.js
2026-06-12 14:55:43 +02:00

166 lines
5.9 KiB
JavaScript

// Workspace Open Performance Test (Read-heavy)
//
// Simulates many users opening the same file in the workspace editor.
// This is the most common read-heavy operation — loading a file and its
// dependencies (libraries, thumbnails).
//
// setup() creates one user, one project, and one file with a shape.
// All VUs login with the same user and read the same file concurrently.
//
// Flow (per VU iteration):
// Login → get-file → get-file-libraries → get-file-object-thumbnails
// → get-file-data-for-thumbnail
//
// Usage:
// k6 run scripts/workspace-open.js
// k6 run --vus 100 --iterations 20 scripts/workspace-open.js
// k6 run --env PENPOT_BASE_URL=http://localhost:6060 scripts/workspace-open.js
import { check, sleep, fail } from "k6";
import { uuidv4 } from "https://jslib.k6.io/k6-utils/1.4.0/index.js";
import { createClient } from "../lib/penpot-client.js";
// ---------------------------------------------------------------------------
// Configuration
// ---------------------------------------------------------------------------
const BASE_URL = __ENV.PENPOT_BASE_URL || "http://localhost:6060";
const OPEN_ITERATIONS = parseInt(__ENV.PENPOT_OPEN_ITERATIONS || "5");
export const options = {
scenarios: {
workspace_open: {
executor: "per-vu-iterations",
vus: 1,
iterations: 1,
maxDuration: "2m",
},
},
thresholds: {
http_req_duration: ["p(95)<5000"],
http_req_failed: ["rate<0.01"],
"http_req_duration{rpc_command:get-file}": ["p(95)<500"],
"http_req_duration{rpc_command:get-file-libraries}": ["p(95)<500"],
"http_req_duration{rpc_command:get-file-object-thumbnails}": ["p(95)<500"],
"http_req_duration{rpc_command:get-file-data-for-thumbnail}": ["p(95)<500"],
},
};
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function assertOk(res, label) {
const ok = check(res, {
[`${label} — status is 2xx`]: (r) => r.status >= 200 && r.status < 300,
});
if (!ok) {
let bodyStr = "";
try {
if (res.raw && res.raw.body) {
bodyStr = typeof res.raw.body === "string"
? res.raw.body.substring(0, 500)
: JSON.stringify(res.raw.body).substring(0, 500);
} else if (res.body) {
bodyStr = JSON.stringify(res.body).substring(0, 500);
}
} catch (e) {
bodyStr = "(could not read body)";
}
console.error(`FAIL: ${label} — status=${res.status} body=${bodyStr}`);
}
return ok;
}
// ---------------------------------------------------------------------------
// Setup — create one user + one file with data
// ---------------------------------------------------------------------------
export function setup() {
console.log(`Penpot Workspace Open Test`);
console.log(` Base URL: ${BASE_URL}`);
console.log(` Open iterations: ${OPEN_ITERATIONS}`);
console.log(``);
const client = createClient(BASE_URL);
// Verify backend reachable
if (client.getProfile().status === 0) fail(`Backend unreachable at ${BASE_URL}`);
// Create one demo user
const demoRes = client.rpc("POST", "create-demo-profile", {});
if (demoRes.status !== 200) fail("Failed to create demo profile");
const { email, password } = demoRes.json();
// Login
const loginRes = client.login(email, password);
if (loginRes.status !== 200) fail("Login failed");
// Create project + file
const teamId = client.getTeams().body[0].id;
const projectId = client.createProject(teamId, "WS Open Project").body.id;
const fileId = client.createFile(projectId, "WS Open File").body.id;
// Get file data and add a shape so it has meaningful content
const fileData = client.getFile(fileId).body;
const pageId = fileData.data.pages[0];
const shapeId = uuidv4();
const x = 50, y = 50, w = 300, h = 200;
client.updateFile(fileId, fileData.revn, fileData.vern, uuidv4(), [{
type: "add-obj", pageId, id: shapeId, frameId: pageId, parentId: pageId,
obj: {
id: shapeId, type: "rect", name: "Background",
x, y, width: w, height: h,
fillColor: "#cccccc", fillOpacity: 1,
rotation: 0, hidden: false, locked: false,
selrect: { x, y, width: w, height: h, x1: x, y1: y, x2: x + w, y2: y + h },
points: [{ x, y }, { x: x + w, y }, { x: x + w, y: y + h }, { x, y: y + h }],
transform: { a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 },
transformInverse: { a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 },
parentId: pageId, frameId: pageId,
},
}]);
console.log(` File ready: ${fileId} (page: ${pageId})`);
return { baseUrl: BASE_URL, email, password, fileId };
}
// ---------------------------------------------------------------------------
// Main VU Function — all VUs read the same file
// ---------------------------------------------------------------------------
export default function (data) {
const client = createClient(data.baseUrl);
// Login with shared user
if (!assertOk(client.login(data.email, data.password), "login")) fail("login failed");
sleep(0.5);
for (let i = 0; i < OPEN_ITERATIONS; i++) {
if (!assertOk(client.getFile(data.fileId), "get-file")) fail("get-file failed");
sleep(0.3);
if (!assertOk(client.getFileLibraries(data.fileId), "get-file-libraries")) fail("get-file-libraries failed");
sleep(0.2);
if (!assertOk(client.getFileObjectThumbnails(data.fileId), "get-file-object-thumbnails")) fail("get-file-object-thumbnails failed");
sleep(0.2);
if (!assertOk(client.getFileDataForThumbnail(data.fileId), "get-file-data-for-thumbnail")) fail("get-file-data-for-thumbnail failed");
sleep(1);
}
console.log(`VU ${__VU}: Completed ${OPEN_ITERATIONS} open iterations`);
}
// ---------------------------------------------------------------------------
// Teardown
// ---------------------------------------------------------------------------
export function teardown(data) {
console.log("Workspace open test complete.");
}