mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
629 lines
21 KiB
JavaScript
629 lines
21 KiB
JavaScript
/*
|
|
* Run locally:
|
|
* Requires k6 and a running Appwrite instance.
|
|
*
|
|
* tests/benchmarks/http-local.sh
|
|
*
|
|
* Open http://127.0.0.1:5665 while the benchmark is running.
|
|
*/
|
|
import http from 'k6/http';
|
|
import { check, group, sleep } from 'k6';
|
|
import encoding from 'k6/encoding';
|
|
import { Counter, Trend } from 'k6/metrics';
|
|
|
|
const ENDPOINT = (__ENV.APPWRITE_ENDPOINT || 'http://localhost/v1').replace(/\/+$/, '');
|
|
const CONSOLE_PROJECT = __ENV.APPWRITE_CONSOLE_PROJECT || 'console';
|
|
const REGION = __ENV.APPWRITE_REGION || 'default';
|
|
const REDIRECT_URL = __ENV.APPWRITE_BENCHMARK_REDIRECT_URL || 'http://localhost';
|
|
const PASSWORD = __ENV.APPWRITE_BENCHMARK_PASSWORD || 'Password123!';
|
|
const WORKER_TIMEOUT_MS = Number(__ENV.APPWRITE_WORKER_TIMEOUT_MS || 120000);
|
|
const ITERATIONS = Number(__ENV.APPWRITE_BENCHMARK_ITERATIONS || 1);
|
|
const VUS = Number(__ENV.APPWRITE_BENCHMARK_VUS || 1);
|
|
const SUMMARY_PATH = __ENV.APPWRITE_BENCHMARK_SUMMARY_PATH || '/tmp/appwrite-k6-summary.json';
|
|
const PREVIOUS_SUMMARY_PATH = __ENV.APPWRITE_BENCHMARK_PREVIOUS_SUMMARY_PATH || '';
|
|
const PREVIOUS_SUMMARY = PREVIOUS_SUMMARY_PATH ? loadPreviousSummary(PREVIOUS_SUMMARY_PATH) : null;
|
|
|
|
export const httpWaiting = new Trend('appwrite_http_waiting', true);
|
|
export const apiDuration = new Trend('appwrite_api_duration', true);
|
|
export const apiWaiting = new Trend('appwrite_api_waiting', true);
|
|
export const flowFailures = new Counter('appwrite_benchmark_flow_failures');
|
|
|
|
export const options = {
|
|
scenarios: {
|
|
curated_flows: {
|
|
executor: 'shared-iterations',
|
|
exec: 'curatedFlows',
|
|
vus: VUS,
|
|
iterations: ITERATIONS,
|
|
maxDuration: __ENV.APPWRITE_BENCHMARK_MAX_DURATION || '30m',
|
|
},
|
|
},
|
|
thresholds: {
|
|
http_req_failed: ['rate<0.05'],
|
|
appwrite_api_duration: ['p(95)<2000'],
|
|
appwrite_benchmark_flow_failures: ['count<1'],
|
|
},
|
|
};
|
|
|
|
const API_SCOPES = [
|
|
'sessions.write',
|
|
'users.read',
|
|
'users.write',
|
|
'teams.read',
|
|
'teams.write',
|
|
'databases.read',
|
|
'databases.write',
|
|
'collections.read',
|
|
'collections.write',
|
|
'tables.read',
|
|
'tables.write',
|
|
'attributes.read',
|
|
'attributes.write',
|
|
'columns.read',
|
|
'columns.write',
|
|
'indexes.read',
|
|
'indexes.write',
|
|
'documents.read',
|
|
'documents.write',
|
|
'rows.read',
|
|
'rows.write',
|
|
'files.read',
|
|
'files.write',
|
|
'buckets.read',
|
|
'buckets.write',
|
|
'functions.read',
|
|
'functions.write',
|
|
'log.read',
|
|
'log.write',
|
|
'executions.read',
|
|
'executions.write',
|
|
'locale.read',
|
|
'avatars.read',
|
|
'rules.read',
|
|
'rules.write',
|
|
'migrations.read',
|
|
'migrations.write',
|
|
'vcs.read',
|
|
'vcs.write',
|
|
'assistant.read',
|
|
'tokens.read',
|
|
'tokens.write',
|
|
'platforms.read',
|
|
'platforms.write',
|
|
'oauth2.read',
|
|
'oauth2.write',
|
|
];
|
|
|
|
const BASE_PERMISSIONS = [
|
|
'read("any")',
|
|
'create("any")',
|
|
'update("any")',
|
|
'delete("any")',
|
|
];
|
|
|
|
const ITEM_PERMISSIONS = [
|
|
'read("any")',
|
|
'update("any")',
|
|
'delete("any")',
|
|
];
|
|
|
|
export function setup() {
|
|
const runId = unique('run');
|
|
const consoleEmail = __ENV.APPWRITE_ADMIN_EMAIL || `bench-admin-${runId}@example.com`;
|
|
const consolePassword = __ENV.APPWRITE_ADMIN_PASSWORD || PASSWORD;
|
|
|
|
const consoleHeaders = {
|
|
'Content-Type': 'application/json',
|
|
'X-Appwrite-Project': CONSOLE_PROJECT,
|
|
};
|
|
|
|
const account = rawRequest('POST', '/account', {
|
|
userId: unique('admin'),
|
|
email: consoleEmail,
|
|
password: consolePassword,
|
|
name: 'Benchmark Admin',
|
|
}, consoleHeaders, 'setup.account.create');
|
|
|
|
if (![201, 409].includes(account.status)) {
|
|
failResponse(account, 'Unable to create or reuse the benchmark console account');
|
|
}
|
|
|
|
const session = rawRequest('POST', '/account/sessions/email', {
|
|
email: consoleEmail,
|
|
password: consolePassword,
|
|
}, consoleHeaders, 'setup.account.session');
|
|
|
|
assertStatus(session, [201], 'console session created');
|
|
|
|
const consoleSessionHeaders = {
|
|
...consoleHeaders,
|
|
Cookie: cookieHeader(session),
|
|
};
|
|
|
|
const team = setupApi('POST', '/teams', {
|
|
teamId: unique('team'),
|
|
name: `Benchmark Team ${runId}`,
|
|
}, consoleSessionHeaders, [201], 'setup.teams.create');
|
|
|
|
const teamId = team.json('$id');
|
|
const project = setupApi('POST', '/projects', {
|
|
projectId: unique('project'),
|
|
name: `Benchmark Project ${runId}`,
|
|
teamId,
|
|
region: REGION,
|
|
}, consoleSessionHeaders, [201], 'setup.projects.create');
|
|
|
|
const projectId = project.json('$id');
|
|
const key = setupApi('POST', `/projects/${projectId}/keys`, {
|
|
keyId: unique('key'),
|
|
name: 'Benchmark API key',
|
|
scopes: API_SCOPES,
|
|
}, consoleSessionHeaders, [201], 'setup.projects.keys.create');
|
|
|
|
const apiHeaders = {
|
|
'Content-Type': 'application/json',
|
|
'X-Appwrite-Project': projectId,
|
|
'X-Appwrite-Key': key.json('secret'),
|
|
};
|
|
|
|
const platform = setupApi('POST', '/project/platforms/web', {
|
|
platformId: unique('web'),
|
|
name: 'Benchmark web',
|
|
hostname: hostnameFromUrl(REDIRECT_URL),
|
|
}, apiHeaders, [201, 409], 'setup.project.platforms.web.create');
|
|
|
|
const tablesDb = setupTablesDb(apiHeaders);
|
|
|
|
return {
|
|
runId,
|
|
teamId,
|
|
projectId,
|
|
databaseId: tablesDb.databaseId,
|
|
tableId: tablesDb.tableId,
|
|
consoleSessionHeaders,
|
|
apiHeaders,
|
|
platformStatus: platform.status,
|
|
};
|
|
}
|
|
|
|
function setupTablesDb(apiHeaders) {
|
|
const databaseId = unique('tdb');
|
|
const tableId = unique('tbl');
|
|
|
|
setupApi('POST', '/tablesdb', { databaseId, name: 'Benchmark TablesDB' }, apiHeaders, [201], 'setup.tablesdb.create');
|
|
setupApi('POST', `/tablesdb/${databaseId}/tables`, {
|
|
tableId,
|
|
name: 'Benchmark Table',
|
|
permissions: BASE_PERMISSIONS,
|
|
rowSecurity: false,
|
|
}, apiHeaders, [201], 'setup.tablesdb.tables.create');
|
|
|
|
const columns = [
|
|
['string', 'title', { size: 128 }],
|
|
['integer', 'quantity', { min: 0, max: 100000 }],
|
|
['email', 'email', {}],
|
|
['boolean', 'active', {}],
|
|
];
|
|
|
|
for (const [type, key, extra] of columns) {
|
|
setupApi('POST', `/tablesdb/${databaseId}/tables/${tableId}/columns/${type}`, {
|
|
key,
|
|
required: false,
|
|
array: false,
|
|
...extra,
|
|
}, apiHeaders, [202], `setup.tablesdb.columns.${type}.create`);
|
|
waitForStatus(`/tablesdb/${databaseId}/tables/${tableId}/columns/${key}`, apiHeaders, 'available', WORKER_TIMEOUT_MS, `setup.tablesdb.columns.${type}.wait`);
|
|
}
|
|
|
|
return { databaseId, tableId };
|
|
}
|
|
|
|
export function curatedFlows(data) {
|
|
const ctx = { ...data };
|
|
|
|
try {
|
|
group('account flow', () => accountFlow(ctx));
|
|
group('tablesdb rows flow', () => tablesDbFlow(ctx));
|
|
group('storage files and tokens flow', () => storageFlow(ctx));
|
|
group('functions control-plane flow', () => computeFlow(ctx));
|
|
} catch (error) {
|
|
flowFailures.add(1);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export function teardown(data) {
|
|
if (data && data.projectId && data.consoleSessionHeaders) {
|
|
rawRequest('DELETE', `/projects/${data.projectId}`, null, data.consoleSessionHeaders, 'teardown.projects.delete');
|
|
}
|
|
|
|
if (data && data.teamId && data.consoleSessionHeaders) {
|
|
rawRequest('DELETE', `/teams/${data.teamId}`, null, data.consoleSessionHeaders, 'teardown.teams.delete');
|
|
}
|
|
}
|
|
|
|
function accountFlow(ctx) {
|
|
const userId = unique('user');
|
|
const email = `bench-user-${unique('mail')}@example.com`;
|
|
const headers = projectHeaders(ctx.projectId);
|
|
|
|
api('POST', '/account', {
|
|
userId,
|
|
email,
|
|
password: PASSWORD,
|
|
name: 'Benchmark User',
|
|
}, headers, [201], 'account.create');
|
|
|
|
const session = api('POST', '/account/sessions/email', {
|
|
email,
|
|
password: PASSWORD,
|
|
}, headers, [201], 'account.sessions.email.create');
|
|
|
|
const sessionHeaders = {
|
|
...headers,
|
|
Cookie: cookieHeader(session),
|
|
};
|
|
|
|
ctx.userId = userId;
|
|
ctx.userEmail = email;
|
|
ctx.sessionHeaders = sessionHeaders;
|
|
|
|
api('GET', '/account', null, sessionHeaders, [200], 'account.get');
|
|
api('GET', '/account/logs', null, sessionHeaders, [200], 'account.logs.list');
|
|
api('PATCH', '/account/prefs', { prefs: { benchmark: true, runId: ctx.runId } }, sessionHeaders, [200], 'account.prefs.update');
|
|
api('PATCH', '/account/name', { name: 'Benchmark User Updated' }, sessionHeaders, [200], 'account.name.update');
|
|
api('PATCH', '/account/password', { password: `${PASSWORD}2`, oldPassword: PASSWORD }, sessionHeaders, [200], 'account.password.update');
|
|
}
|
|
|
|
function tablesDbFlow(ctx) {
|
|
requireSession(ctx, 'tablesDbFlow');
|
|
|
|
const databaseId = ctx.databaseId;
|
|
const tableId = ctx.tableId;
|
|
const rowId = unique('row');
|
|
|
|
api('POST', `/tablesdb/${databaseId}/tables/${tableId}/rows`, {
|
|
rowId,
|
|
data: tablePayload(),
|
|
permissions: ITEM_PERMISSIONS,
|
|
}, ctx.sessionHeaders, [201], 'tablesdb.rows.create');
|
|
api('GET', `/tablesdb/${databaseId}/tables/${tableId}/rows`, null, ctx.sessionHeaders, [200], 'tablesdb.rows.list');
|
|
api('GET', `/tablesdb/${databaseId}/tables/${tableId}/rows/${rowId}`, null, ctx.sessionHeaders, [200], 'tablesdb.rows.get');
|
|
api('PATCH', `/tablesdb/${databaseId}/tables/${tableId}/rows/${rowId}`, {
|
|
data: { title: 'Benchmark Row Updated' },
|
|
}, ctx.sessionHeaders, [200], 'tablesdb.rows.update');
|
|
api('PATCH', `/tablesdb/${databaseId}/tables/${tableId}/rows/${rowId}/quantity/increment`, {
|
|
value: 1,
|
|
}, ctx.sessionHeaders, [200], 'tablesdb.rows.increment');
|
|
api('PATCH', `/tablesdb/${databaseId}/tables/${tableId}/rows/${rowId}/quantity/decrement`, {
|
|
value: 1,
|
|
}, ctx.sessionHeaders, [200], 'tablesdb.rows.decrement');
|
|
api('DELETE', `/tablesdb/${databaseId}/tables/${tableId}/rows/${rowId}`, null, ctx.sessionHeaders, [204], 'tablesdb.rows.delete');
|
|
}
|
|
|
|
function storageFlow(ctx) {
|
|
requireSession(ctx, 'storageFlow');
|
|
|
|
const bucketId = unique('bucket');
|
|
const fileId = unique('file');
|
|
|
|
api('POST', '/storage/buckets', {
|
|
bucketId,
|
|
name: 'Benchmark Bucket',
|
|
permissions: BASE_PERMISSIONS,
|
|
fileSecurity: false,
|
|
enabled: true,
|
|
maximumFileSize: 30000000,
|
|
allowedFileExtensions: [],
|
|
compression: 'none',
|
|
encryption: false,
|
|
antivirus: false,
|
|
}, ctx.apiHeaders, [201], 'storage.buckets.create');
|
|
|
|
const multipartHeaders = { ...ctx.sessionHeaders };
|
|
delete multipartHeaders['Content-Type'];
|
|
|
|
const upload = http.post(`${ENDPOINT}/storage/buckets/${bucketId}/files`, {
|
|
fileId,
|
|
file: http.file(onePixelPng(), 'benchmark.png', 'image/png'),
|
|
...flattenMultipartArray('permissions', ITEM_PERMISSIONS),
|
|
}, {
|
|
headers: multipartHeaders,
|
|
tags: { name: 'storage.files.create' },
|
|
});
|
|
|
|
httpWaiting.add(upload.timings.waiting, { name: 'storage.files.create' });
|
|
apiDuration.add(upload.timings.duration, { name: 'storage.files.create' });
|
|
apiWaiting.add(upload.timings.waiting, { name: 'storage.files.create' });
|
|
assertStatus(upload, [201], 'storage file created');
|
|
|
|
api('GET', `/storage/buckets/${bucketId}/files`, null, ctx.sessionHeaders, [200], 'storage.files.list');
|
|
api('GET', `/storage/buckets/${bucketId}/files/${fileId}`, null, ctx.sessionHeaders, [200], 'storage.files.get');
|
|
api('GET', `/storage/buckets/${bucketId}/files/${fileId}/view`, null, ctx.sessionHeaders, [200], 'storage.files.view');
|
|
api('GET', `/storage/buckets/${bucketId}/files/${fileId}/download`, null, ctx.sessionHeaders, [200], 'storage.files.download');
|
|
api('GET', `/storage/buckets/${bucketId}/files/${fileId}/preview`, null, ctx.sessionHeaders, [200], 'storage.files.preview');
|
|
api('PUT', `/storage/buckets/${bucketId}/files/${fileId}`, {
|
|
name: 'benchmark-renamed.png',
|
|
permissions: ITEM_PERMISSIONS,
|
|
}, ctx.sessionHeaders, [200], 'storage.files.update');
|
|
|
|
const token = api('POST', `/tokens/buckets/${bucketId}/files/${fileId}`, {}, ctx.apiHeaders, [201], 'tokens.files.create');
|
|
api('GET', `/tokens/buckets/${bucketId}/files/${fileId}`, null, ctx.apiHeaders, [200], 'tokens.files.list');
|
|
api('GET', `/tokens/${token.json('$id')}`, null, ctx.apiHeaders, [200], 'tokens.get');
|
|
api('PATCH', `/tokens/${token.json('$id')}`, { expire: null }, ctx.apiHeaders, [200], 'tokens.update');
|
|
api('DELETE', `/tokens/${token.json('$id')}`, null, ctx.apiHeaders, [204], 'tokens.delete');
|
|
|
|
api('DELETE', `/storage/buckets/${bucketId}/files/${fileId}`, null, ctx.sessionHeaders, [204], 'storage.files.delete');
|
|
api('DELETE', `/storage/buckets/${bucketId}`, null, ctx.apiHeaders, [204], 'storage.buckets.delete');
|
|
}
|
|
|
|
function computeFlow(ctx) {
|
|
requireSession(ctx, 'computeFlow');
|
|
|
|
const functionId = unique('fn');
|
|
let functionVariableId;
|
|
|
|
api('POST', '/functions', {
|
|
functionId,
|
|
name: 'Benchmark Function',
|
|
runtime: __ENV.APPWRITE_BENCHMARK_RUNTIME || 'node-22',
|
|
execute: ['any'],
|
|
events: [],
|
|
schedule: '',
|
|
timeout: 15,
|
|
enabled: true,
|
|
logging: true,
|
|
entrypoint: 'index.js',
|
|
commands: 'npm install',
|
|
scopes: ['users.read'],
|
|
}, ctx.apiHeaders, [201], 'functions.create');
|
|
api('GET', '/functions/runtimes', null, ctx.sessionHeaders, [200], 'functions.runtimes.list');
|
|
api('GET', '/functions/specifications', null, ctx.apiHeaders, [200], 'functions.specifications.list');
|
|
const functionVariable = api('POST', `/functions/${functionId}/variables`, {
|
|
variableId: 'unique()',
|
|
key: 'BENCHMARK',
|
|
value: 'true',
|
|
secret: false,
|
|
}, ctx.apiHeaders, [201], 'functions.variables.create');
|
|
functionVariableId = functionVariable.json('$id');
|
|
|
|
api('PUT', `/functions/${functionId}/variables/${functionVariableId}`, {
|
|
key: 'BENCHMARK',
|
|
value: 'updated',
|
|
secret: false,
|
|
}, ctx.apiHeaders, [200], 'functions.variables.update');
|
|
api('GET', `/functions/${functionId}/variables/${functionVariableId}`, null, ctx.apiHeaders, [200], 'functions.variables.get');
|
|
api('DELETE', `/functions/${functionId}/variables/${functionVariableId}`, null, ctx.apiHeaders, [204], 'functions.variables.delete');
|
|
api('DELETE', `/functions/${functionId}`, null, ctx.apiHeaders, [204], 'functions.delete');
|
|
}
|
|
|
|
function api(method, path, body, headers, expected, name) {
|
|
const response = rawRequest(method, path, body, headers, name);
|
|
apiDuration.add(response.timings.duration, { name });
|
|
apiWaiting.add(response.timings.waiting, { name });
|
|
assertStatus(response, expected, name);
|
|
return response;
|
|
}
|
|
|
|
function setupApi(method, path, body, headers, expected, name) {
|
|
const response = rawRequest(method, path, body, headers, name);
|
|
assertStatus(response, expected, name);
|
|
return response;
|
|
}
|
|
|
|
function rawRequest(method, path, body, headers, name) {
|
|
const params = {
|
|
headers,
|
|
tags: { name },
|
|
};
|
|
const payload = body === null || body === undefined ? null : JSON.stringify(body);
|
|
const response = http.request(method, `${ENDPOINT}${path}`, payload, params);
|
|
httpWaiting.add(response.timings.waiting, { name });
|
|
|
|
return response;
|
|
}
|
|
|
|
function waitForStatus(path, headers, wantedStatus, timeoutMs, name) {
|
|
const started = Date.now();
|
|
|
|
while (Date.now() - started < timeoutMs) {
|
|
const response = rawRequest('GET', path, null, headers, name);
|
|
if (response.status === 200) {
|
|
const status = response.json('status');
|
|
if (status === wantedStatus) {
|
|
return response;
|
|
}
|
|
if (status === 'failed') {
|
|
throw new Error(`${path} failed while waiting for ${wantedStatus}`);
|
|
}
|
|
}
|
|
sleep(0.5);
|
|
}
|
|
|
|
throw new Error(`Timed out waiting for ${path} to become ${wantedStatus}`);
|
|
}
|
|
|
|
function assertStatus(response, expected, name) {
|
|
const ok = check(response, {
|
|
[`${name} status ${expected.join('|')}`]: (r) => expected.includes(r.status),
|
|
});
|
|
|
|
if (!ok) {
|
|
failResponse(response, `${name} returned an unexpected status`);
|
|
}
|
|
}
|
|
|
|
function failResponse(response, message) {
|
|
throw new Error(`${message}. Status: ${response.status}. Body: ${response.body}`);
|
|
}
|
|
|
|
function cookieHeader(response) {
|
|
return response.headers['Set-Cookie'] || response.headers['set-cookie'] || '';
|
|
}
|
|
|
|
function projectHeaders(projectId) {
|
|
return {
|
|
'Content-Type': 'application/json',
|
|
'X-Appwrite-Project': projectId,
|
|
};
|
|
}
|
|
|
|
function requireSession(ctx, flow) {
|
|
if (!ctx.sessionHeaders || typeof ctx.sessionHeaders !== 'object') {
|
|
throw new Error(`accountFlow must run before ${flow}`);
|
|
}
|
|
}
|
|
|
|
function tablePayload() {
|
|
return {
|
|
title: 'Benchmark Row',
|
|
quantity: 1,
|
|
email: 'row@example.com',
|
|
active: true,
|
|
};
|
|
}
|
|
|
|
function onePixelPng() {
|
|
return encoding.b64decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVR4nGNgAAIAAAUAAXpeqz8AAAAASUVORK5CYII=', 'std', 'b');
|
|
}
|
|
|
|
function flattenMultipartArray(key, values) {
|
|
const output = {};
|
|
values.forEach((value, index) => {
|
|
output[`${key}[${index}]`] = value;
|
|
});
|
|
return output;
|
|
}
|
|
|
|
function unique(prefix) {
|
|
return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9-]/g, '-')
|
|
.slice(0, 36);
|
|
}
|
|
|
|
function hostnameFromUrl(value) {
|
|
return value.replace(/^https?:\/\//, '').split('/')[0].split(':')[0];
|
|
}
|
|
|
|
export function handleSummary(data) {
|
|
const lines = [
|
|
'Appwrite curated benchmark review',
|
|
'',
|
|
'Before',
|
|
'',
|
|
summaryTable(PREVIOUS_SUMMARY),
|
|
'',
|
|
'After',
|
|
'',
|
|
summaryTable(data),
|
|
'',
|
|
'Delta',
|
|
'',
|
|
deltaTable(PREVIOUS_SUMMARY, data),
|
|
'',
|
|
];
|
|
|
|
return {
|
|
stdout: `${lines.join('\n')}\n`,
|
|
[SUMMARY_PATH]: JSON.stringify(data, null, 2),
|
|
};
|
|
}
|
|
|
|
function summaryTable(data) {
|
|
return [
|
|
'| Scenario | P50 (ms) | P95 (ms) | Requests | RPS |',
|
|
'| --- | ---: | ---: | ---: | ---: |',
|
|
summaryRow(data, 'API total', 'appwrite_api_duration'),
|
|
].join('\n');
|
|
}
|
|
|
|
function summaryRow(data, label, metric, iterationsMetric = null, rpsMetric = null) {
|
|
const values = data && data.metrics[metric] && data.metrics[metric].values;
|
|
if (!values || values.count === 0) {
|
|
return `| ${label} | n/a | n/a | n/a | n/a |`;
|
|
}
|
|
|
|
const iterations = iterationsMetric
|
|
? trendMetric(data, iterationsMetric, 'count')
|
|
: values.count;
|
|
const rps = rpsMetric ? trendMetric(data, rpsMetric, 'rate') : null;
|
|
|
|
return `| ${label} | ${formatDetailValue(values.med)} | ${formatDetailValue(values['p(95)'])} | ${formatCount(iterations)} | ${formatRate(rps)} |`;
|
|
}
|
|
|
|
function loadPreviousSummary(path) {
|
|
let contents;
|
|
try {
|
|
contents = open(path);
|
|
} catch (error) {
|
|
console.warn(`Missing benchmark summary at ${path}: ${error.message}`);
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
return JSON.parse(contents);
|
|
} catch (error) {
|
|
console.warn(`Invalid benchmark summary at ${path}: ${error.message}`);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function deltaTable(before, after) {
|
|
return [
|
|
'| Scenario | P95 delta (ms) |',
|
|
'| --- | ---: |',
|
|
...[
|
|
['API total', 'appwrite_api_duration'],
|
|
].map(([label, metric]) => {
|
|
const beforeP95 = trendMetric(before, metric, 'p(95)');
|
|
const afterP95 = trendMetric(after, metric, 'p(95)');
|
|
return `| ${label} | ${formatDelta(beforeP95, afterP95)} |`;
|
|
}),
|
|
].join('\n');
|
|
}
|
|
|
|
function trendMetric(data, metric, stat) {
|
|
return data && data.metrics[metric] && data.metrics[metric].values
|
|
? data.metrics[metric].values[stat]
|
|
: null;
|
|
}
|
|
|
|
function formatDetailValue(value) {
|
|
if (value === null || value === undefined || Number.isNaN(value)) {
|
|
return 'n/a';
|
|
}
|
|
|
|
return `${Number(value).toFixed(2)}`;
|
|
}
|
|
|
|
function formatDelta(before, after) {
|
|
if (before === null || before === undefined || after === null || after === undefined || Number.isNaN(before) || Number.isNaN(after)) {
|
|
return 'n/a';
|
|
}
|
|
|
|
const delta = round(after - before);
|
|
const sign = delta > 0 ? '+' : '';
|
|
return `${sign}${delta}`;
|
|
}
|
|
|
|
function formatCount(value) {
|
|
if (value === null || value === undefined || Number.isNaN(value)) {
|
|
return 'n/a';
|
|
}
|
|
|
|
return `${Math.round(value)}`;
|
|
}
|
|
|
|
function formatRate(value) {
|
|
if (value === null || value === undefined || Number.isNaN(value)) {
|
|
return 'n/a';
|
|
}
|
|
|
|
return `${Number(value).toFixed(2)}`;
|
|
}
|
|
|
|
function round(value) {
|
|
return Math.round((value || 0) * 100) / 100;
|
|
}
|