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

167 lines
5.8 KiB
JavaScript

// Font Upload Performance Test
//
// Tests the font upload flow: chunked upload of TTF + OTF files followed by
// creating a font variant. Exercises storage pipeline and font processing.
//
// setup() creates N demo profiles.
// Each VU picks its user, uploads fonts, and creates a variant.
//
// Usage:
// k6 run scripts/font-upload.js
// k6 run --vus 50 --iterations 5 scripts/font-upload.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";
export const options = {
scenarios: {
font_upload: {
executor: "per-vu-iterations",
vus: 1,
iterations: 1,
maxDuration: "2m",
},
},
thresholds: {
http_req_duration: ["p(95)<15000"],
http_req_failed: ["rate<0.01"],
"http_req_duration{rpc_command:create-upload-session}": ["p(95)<1000"],
"http_req_duration{rpc_command:upload-chunk}": ["p(95)<5000"],
"http_req_duration{rpc_command:create-font-variant}": ["p(95)<10000"],
},
};
// ---------------------------------------------------------------------------
// Test Data
// ---------------------------------------------------------------------------
const fontTtf = open("../../backend/test/backend_tests/test_files/font-1.ttf", "b");
const fontOtf = open("../../backend/test/backend_tests/test_files/font-1.otf", "b");
// ---------------------------------------------------------------------------
// 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 user pool
// ---------------------------------------------------------------------------
export function setup() {
const vuCount = (options.scenarios.font_upload && options.scenarios.font_upload.vus) || __ENV.K6_VUS || 1;
console.log(`Penpot Font Upload Test`);
console.log(` Base URL: ${BASE_URL}`);
console.log(` VUs: ${vuCount}`);
console.log(``);
const client = createClient(BASE_URL);
if (client.getProfile().status === 0) fail(`Backend unreachable at ${BASE_URL}`);
const users = [];
for (let i = 0; i < vuCount; i++) {
const res = client.rpc("POST", "create-demo-profile", {});
if (res.status !== 200) fail(`Failed to create demo profile ${i + 1}/${vuCount}`);
users.push(res.json());
}
console.log(` Created ${users.length} demo profiles`);
return { baseUrl: BASE_URL, users };
}
// ---------------------------------------------------------------------------
// Main VU Function
// ---------------------------------------------------------------------------
export default function (data) {
const client = createClient(data.baseUrl);
// Pick user from pool
const user = data.users[__VU - 1];
if (!user) fail(`No user for VU ${__VU}`);
// Login
if (!assertOk(client.login(user.email, user.password), "login")) fail("login failed");
const teamId = client.getTeams().body[0].id;
sleep(0.5);
const fontId = uuidv4();
const fontFamily = `PerfFont-${uuidv4().substring(0, 8)}`;
const chunkSize = 50 * 1024; // 50 KB
// Upload TTF via chunked upload
const ttfChunks = Math.ceil(fontTtf.byteLength / chunkSize);
const ttfSessionRes = client.createUploadSession(ttfChunks);
if (!assertOk(ttfSessionRes, "create-upload-session (ttf)")) fail("create-upload-session failed");
const ttfSessionId = ttfSessionRes.sessionId;
for (let i = 0; i < ttfChunks; i++) {
const chunk = fontTtf.slice(i * chunkSize, Math.min((i + 1) * chunkSize, fontTtf.byteLength));
if (!assertOk(client.uploadChunk(ttfSessionId, i, chunk, "font-1.ttf", "font/ttf"), `upload-chunk ttf ${i}`)) fail("ttf chunk failed");
sleep(0.1);
}
// Upload OTF via chunked upload
const otfChunks = Math.ceil(fontOtf.byteLength / chunkSize);
const otfSessionRes = client.createUploadSession(otfChunks);
if (!assertOk(otfSessionRes, "create-upload-session (otf)")) fail("create-upload-session (otf) failed");
const otfSessionId = otfSessionRes.sessionId;
for (let i = 0; i < otfChunks; i++) {
const chunk = fontOtf.slice(i * chunkSize, Math.min((i + 1) * chunkSize, fontOtf.byteLength));
if (!assertOk(client.uploadChunk(otfSessionId, i, chunk, "font-1.otf", "font/otf"), `upload-chunk otf ${i}`)) fail("otf chunk failed");
sleep(0.1);
}
sleep(0.5);
// Create font variant
if (!assertOk(client.rpc("POST", "create-font-variant", {
"team-id": teamId,
"font-id": fontId,
"font-family": fontFamily,
"font-weight": 400,
"font-style": "normal",
uploads: { "font/ttf": ttfSessionId, "font/otf": otfSessionId },
}), "create-font-variant")) fail("create-font-variant failed");
console.log(`VU ${__VU}: Font "${fontFamily}" created`);
}
// ---------------------------------------------------------------------------
// Teardown
// ---------------------------------------------------------------------------
export function teardown(data) {
console.log("Font upload test complete.");
}