MEASUREMENT
bench/bff/latency.mjs
The script that produced the numbers in the article, and the record of the run. The file is read from the repository at build time — this is the code that was run, not a copy of it.
- Cited in
- /en/system-design/api-design/bff
- How to run it
node bench/bff/measure.mjs > bench/bff/measurements.json node bench/bff/storage.mjs > bench/bff/storage.json node bench/bff/latency.mjs > bench/bff/latency.json
The run below is recorded in Russian. It is a lab record, kept in the language it was written in; the numbers, the tables and the code read the same either way.
Record of the run
Замеры для статьи «Backend for Frontend»
Не симуляция. Каждое число статьи взято из прогона этих файлов против
next start перед настоящим Go API перед PostgreSQL — кроме одной величины,
которая задана намеренно (см. ниже).
| Файл | Что меряет |
|---|---|
measure.mjs |
что скрипт из собственного источника страницы может и чего не может достать, когда сессия живёт в HttpOnly-cookie за BFF |
storage.mjs |
две другие колонки той же таблицы: что достаёт тот же скрипт, когда токен лежит в localStorage и когда — в куке без HttpOnly |
latency.mjs |
как цена экрана растёт с числом обходов, которые делает браузер |
measurements.json, storage.json, latency.json |
своды, из которых взяты числа статьи |
node bench/bff/measure.mjs > bench/bff/measurements.json
node bench/bff/storage.mjs > bench/bff/storage.json
node bench/bff/latency.mjs > bench/bff/latency.json
Одно число снято не этими скриптами. Поле serverLegLoopback в
latency.json (0,71 мс) получено двадцатью запросами curl к
http://localhost:8091/health с той же машины, а не из браузера, — это
записано в самом файле, в поле tool. Статья обязана называть его так же:
обращение к соседнему сервису с машины, а не «внутри датацентра».
Что здесь измерено, а что задано
Задержка на стороне браузера вводится намеренно
(Network.emulateNetworkConditions через CDP), потому что замер по петле
обратной связи сказал бы, что обход бесплатен, — а это ровно та иллюзия,
которую замер призван сломать. Введённая величина названа в тексте статьи;
измеряется не она, а то, как от неё растёт цена экрана.
Почему этот каталог появился поздно
Скрипты лежали в tmp/, а tmp/ стоит в .gitignore — в репозитории их не
было, и числа статьи нельзя было перезапустить. Правило «числа в статьях
берутся отсюда, а не из головы» для уроков выполнялось, для статей — нет.
Script
96 lines/**
* Measures how the cost of a screen scales with the number of round trips the
* browser makes, and what one of those round trips costs on the server side.
*
* The browser-side latency is injected deliberately (CDP
* Network.emulateNetworkConditions), because a loopback measurement would say
* a round trip is free — which is exactly the illusion this measurement is
* meant to break. The injected figure is stated in the article; the scaling it
* produces is measured, not modelled.
*
* node bench/bff/latency.mjs > bench/bff/latency.json
*/
import { chromium } from "@playwright/test";
import { randomUUID } from "node:crypto";
const BASE = process.env.BASE_URL ?? "http://localhost:3417";
const API = process.env.DE_API_URL ?? "http://localhost:8091";
const PASSWORD = "correct horse battery staple";
const INJECTED_MS = Number(process.env.INJECTED_LATENCY_MS ?? 150);
const REPEATS = 3;
const browser = await chromium.launch({
executablePath: process.env.PLAYWRIGHT_CHROMIUM_PATH || undefined,
});
const context = await browser.newContext();
const page = await context.newPage();
const name = `lat-${randomUUID().slice(0, 8)}`;
await page.goto(`${BASE}/register`);
await page.getByLabel("Имя пользователя").fill(name);
await page.getByLabel("Email").fill(`${name}@example.dev`);
await page.getByLabel("Пароль").fill(PASSWORD);
await page.getByRole("button", { name: "Создать аккаунт" }).click();
await page.getByTestId("account-name").waitFor();
// Server-side leg first, with no injected latency: this is what one hop
// costs when both ends sit in the same datacentre.
const serverLeg = await page.evaluate(async (apiUrl) => {
const samples = [];
for (let i = 0; i < 20; i++) {
const t0 = performance.now();
await fetch(`${apiUrl}/health`, { cache: "no-store" });
samples.push(performance.now() - t0);
}
samples.sort((a, b) => a - b);
return { medianMs: samples[Math.floor(samples.length / 2)], samples: samples.length };
}, API);
// Now injure the network the way a mobile connection is injured.
const cdp = await context.newCDPSession(page);
await cdp.send("Network.emulateNetworkConditions", {
offline: false,
latency: INJECTED_MS,
downloadThroughput: -1,
uploadThroughput: -1,
});
async function timeSequential(count) {
const runs = [];
for (let r = 0; r < REPEATS; r++) {
const ms = await page.evaluate(async (n) => {
const t0 = performance.now();
for (let i = 0; i < n; i++) {
// A distinct URL each time so nothing can be served from a cache.
await fetch(`/api/auth/me?probe=${i}-${Math.random()}`, {
credentials: "same-origin",
cache: "no-store",
});
}
return performance.now() - t0;
}, count);
runs.push(Math.round(ms));
}
runs.sort((a, b) => a - b);
return { roundTrips: count, medianMs: runs[Math.floor(runs.length / 2)], runs };
}
const sequential = [];
for (const n of [1, 2, 3, 6]) sequential.push(await timeSequential(n));
await browser.close();
process.stdout.write(
JSON.stringify(
{
injectedLatencyMs: INJECTED_MS,
repeats: REPEATS,
serverLegNoInjectedLatency: serverLeg,
sequential,
recordedAt: new Date().toISOString(),
},
null,
2,
),
);