docs: publish reproducible README measurements

Generate all four README tables from reviewed source and build snapshots, distinguish collection and asset sizes from runtime results, and remove unsupported footprint claims. Add a consistency check in CI and document how to reproduce profiling and measurements.
This commit is contained in:
Juan Denis committed 2026-09-05 14:42:28 -04:00
1 parent d67e5d07d0
commit 2de3a971c5
10 files changed
+1402 -44

No files matched your search

+42
View File
@@ -0,0 +1,42 @@
name: Measurement tools and README tables
on:
push:
branches: [dev]
paths:
- 'README.md'
- 'docs/README*.md'
- 'docs/measurements/**'
- 'scripts/measure-repository.py'
- 'scripts/update-readme-measurements.py'
- 'scripts/profile-api.py'
- 'scripts/test/test_repository_measurements.py'
- 'scripts/test/test_profile_api.py'
- '.github/workflows/measurements-ci.yml'
pull_request:
branches: [main]
paths:
- 'README.md'
- 'docs/README*.md'
- 'docs/measurements/**'
- 'scripts/measure-repository.py'
- 'scripts/update-readme-measurements.py'
- 'scripts/profile-api.py'
- 'scripts/test/test_repository_measurements.py'
- 'scripts/test/test_profile_api.py'
- '.github/workflows/measurements-ci.yml'
jobs:
measurements:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Test measurement tools (stdlib only)
run: |
python -m unittest discover -s scripts/test -p test_repository_measurements.py
python -m unittest discover -s scripts/test -p test_profile_api.py
- name: Check translated README tables against reviewed snapshots
run: python scripts/update-readme-measurements.py
+12 -11
View File
@@ -44,25 +44,26 @@ English | [Español](docs/README.es.md) | [中文版](docs/README.zh-CN.md) | [P
## 📊 By the Numbers
Everything below is measured from this repository, not estimated.
<!-- BEGIN GENERATED MEASUREMENTS -->
Snapshot: **2026-09-05**. [Definitions, raw measurements and reproduction commands](docs/METRICS.md).
| | |
|---|---|
| **1,350+** REST endpoints | across 109 blueprints — `/api/v1/*`, with OpenAPI + Swagger UI at `/api/v1/docs` |
| **118** one-click app templates | bundled in the repo, no registry account needed |
| **4,300+** backend tests | the whole suite runs on every change, with a CI-enforced floor on the collected count |
| **1.75 MB** gzipped web UI | 60+ screens, assets served from your own box — no CDN |
| **~180 MB** resident | the entire panel, single process — sits comfortably beside your apps on a 1 GB VPS |
| **501 MB** container image | or install straight onto the host; Docker is optional for the panel itself |
| **$0** | MIT-licensed. No tiers, no seat limits, no upsell — and nothing phones home. |
| **1,212** core route declarations | in **104** blueprint declarations under `backend/app/api`; source inventory, excluding extensions |
| **118** bundled app templates | root-level app YAML files; database extension templates counted separately |
| **5,089** backend test cases collected | clean-checkout collection; this is not a claim that every case passed or ran |
| **0.83 MB** gzip, HTML-linked JS/CSS | scripts, stylesheets and modulepreloads listed in the production HTML |
| **3.31 MB** gzip, all built JS/CSS | includes lazy chunks, locale bundles and vendor shims; excludes fonts and images |
| **$0** license cost | MIT-licensed, without subscription or seat fees |
Self-hosted and Docker-native, on hardware you already pay for.
Gzip values sum separately compressed files at level 9; they are not measured page-load times. RAM usage and image size depend on the build, platform and workload; no universal footprint is claimed.
<!-- END GENERATED MEASUREMENTS -->
---
## 🚀 Quick Start
> ⏱️ Up and running in under 2 minutes
> Installation time depends on the server, network and required packages.
### Option 1: One-Line Install (Recommended)
@@ -114,7 +115,7 @@ See the [Installation Guide](docs/INSTALLATION.md) for step-by-step instructions
| **Disk** | 10 GB | 20+ GB |
| **Docker** | 24.0+ (optional for the panel itself) | Latest |
> The panel itself only needs ~180 MB of RAM and ~500 MB of disk — the rest is headroom for your apps. It runs happily on a 1 GB VPS, a spare laptop, or a Raspberry Pi (ARM64), so it's as much at home in a homelab as it is on production hardware.
> These requirements are sizing guidance, not a capacity benchmark. Allow additional RAM and disk for managed apps, images, databases, logs and backups. Measure your own workload using the [measurement guide](docs/METRICS.md).
---
+115
View File
@@ -0,0 +1,115 @@
# ServerKit measurements
The README tables are a dated snapshot, generated from the same JSON files for
English, Spanish, Portuguese and Chinese. They separate source inventory from
build sizes and runtime measurements. A count of collected tests is not a count
of passing tests, and compressed asset bytes are not a page-load benchmark.
## Recorded snapshot
- [Source inventory](measurements/repository.json): measured on September 5,
2026 with Python 3.11.7 on Windows. The clean collection count was also checked
on Linux. The base source revision, whether measured source has uncommitted
changes, and a hash of that measured source are recorded. The hash describes
the working files, so a pre-commit snapshot is not attributed to an unchanged
historical revision.
- [Production build inventory](measurements/frontend-build.json): Node version,
per-asset byte/gzip sizes and a content hash are recorded. Built from an
isolated copy of tracked/new frontend source after the request/form cleanup,
excluding ignored files and reusing the installed dependencies, with
`npm run build`. Its code hash matched the ordinary working-checkout build.
- Runtime memory and container image bytes are **unmeasured in this snapshot**.
The previous ~180 MB RAM and 501 MB image figures had no reproducible
environment or image digest, so they were removed instead of repeated.
### What each number includes
| Measurement | Definition |
| --- | --- |
| Core route declarations | Route decorators in Git-tracked Python under `backend/app/api/`. A function with two route decorators counts twice. This is source inventory, not the set of enabled runtime endpoints. |
| Core blueprint declarations | `Blueprint(...)` declarations in the same files. Extension blueprints and conditionally loaded routes are outside this count. |
| Explicit method/route pairs | Literal route methods, excluding automatic HEAD/OPTIONS. The JSON separately reports declarations whose methods cannot be resolved statically. |
| App templates | Git-tracked YAML files directly inside `backend/templates/`; nested database-extension templates are excluded. |
| Backend tests | Pytest collection from `backend/tests` using the existing clean-collection instrument; ignored installed extension copies do not inflate the count. Skipped/opt-in tests can still be collected. |
| HTML-linked code | JS/MJS/CSS referenced by script, stylesheet or modulepreload tags in production `index.html`. Later runtime requests, fonts and images are excluded. |
| All built code | Every JS/MJS/CSS file in the production output, including lazy chunks, translations, extension code and public vendor shims. HTML, fonts, images, source maps and other assets are excluded. |
| Gzip bytes | Sum of each file compressed independently with gzip level 9. MB means 1,000,000 bytes. These are reproducible compression estimates; actual transfer depends on server compression and browser caching. |
The old “1.75 MB web UI” number had no precise asset scope. The new HTML-linked
and all-code totals must not be interpreted as a measured speedup over that
old figure. Non-English locale chunks are already loaded on demand; their
build-size warnings alone do not show that every visitor downloads them.
## Refresh the source and build measurements
Use the backend Python environment with its dependencies installed. From the
repository root:
```bash
python scripts/measure-repository.py --collect-tests --output docs/measurements/repository.json
cd frontend
npm run build
npm run measure:build -- --output ../docs/measurements/frontend-build.json
cd ..
python scripts/update-readme-measurements.py --write
python scripts/update-readme-measurements.py
```
On Windows, `backend\venv\Scripts\python.exe` can replace `python`. Review the
snapshot and README diffs together. The final command checks agreement between
all translated tables and the snapshots; it does not pretend an old build
snapshot is a fresh measurement. Rebuild when publishing new size claims.
## Measure API latency and database work
The panel already records API timings through its analytics middleware. For a
bounded investigation, enable `SERVERKIT_PROFILE_REQUESTS=true` on an authorized
local or staging panel and restart it. This adds a `Server-Timing` response
header containing application elapsed time, SQL elapsed time and statement
count. It records neither SQL text nor parameters and creates no new database
table. The profiler is off by default and installs no SQL listeners when off.
Use the real HTTP sampler against the running panel:
```bash
python scripts/profile-api.py --base-url http://127.0.0.1:47927 --path /api/v1/system/health --samples 20 --warmup 2 --output .reviews/api-profile.json
```
For protected endpoints, supply a short-lived JWT through
`SERVERKIT_PROFILE_TOKEN`, or `SERVERKIT_PROFILE_API_KEY` only for routes that
already support API keys. Do not supply both. The sampler makes GET requests,
refuses redirects, and does not include bodies, credentials or query values in
its report. HTTP/transport failures are recorded and make the command fail.
Missing profiler headers produce `null` fields, never invented zero costs.
Reports contain sample count, response sizes/statuses, min/p50/p95/max latency,
and optional application/SQL distributions. Warmup is excluded from the latency
summary, but its requests and failures remain visible. Turn profiling off when
the investigation is finished.
Record panel revision, OS/architecture, Python version, database engine,
server/resource counts, cache state, concurrency and whether the panel is idle
or loaded. Compare the same dataset and environment before and after a change.
The sampler is sequential and does not establish throughput under concurrency.
Profiler SQL counts cover statements on the HTTP request thread through response
construction; background jobs and streamed body iteration are not included.
## Measure memory, images and browser loading separately
- **Memory:** record the exact process/container, workload, uptime and sampling
interval. Process RSS and Docker memory accounting are different measurements.
Include workers and enabled services; do not describe one idle sample as a
universal minimum or capacity guarantee.
- **Image size:** record image digest, architecture, build arguments and whether
bytes describe the local uncompressed image or registry transfer. A cached
image from a different commit is not a measurement of the current source.
- **Browser loading:** record cold/warm cache, device, connection, route, locale
and extension set. Use a production build and inspect actual requests and
navigation timings; source-file counts and total dist size cannot substitute
for this. Large assets should be optimized when that trace shows they affect
a relevant user flow.
No production latency, memory, capacity or percentage-speedup claim is made by
the source/build snapshot. Existing fleet tests enforce one metrics query per
reader at multiple fleet sizes; the opt-in profiler makes that kind of query
budget observable on a running panel too.
+12 -11
View File
@@ -44,25 +44,26 @@ ni el coste de las plataformas gestionadas.
## 📊 En Números
Todo lo siguiente está medido sobre este repositorio, no estimado.
<!-- BEGIN GENERATED MEASUREMENTS -->
Instantánea: **2026-09-05**. [Definiciones, medidas y comandos para reproducirlas](METRICS.md).
| | |
|---|---|
| **1.519** endpoints REST | repartidos en 107 blueprints — `/api/v1/*`, con OpenAPI y Swagger UI en `/api/v1/docs` |
| **106** plantillas de apps | incluidas en el repositorio, sin necesidad de cuenta en ningún registro |
| **2.633** pruebas de backend | la suite completa se ejecuta ante cada cambio |
| **1,75 MB** de interfaz web (gzip) | 65 pantallas, servidas desde tu propio servidor — sin CDN |
| **~180 MB** en memoria | el panel entero, un solo proceso — convive sin apreturas con tus apps en un VPS de 1 GB |
| **501 MB** de imagen de contenedor | o instálalo directamente en el host; Docker es opcional para el panel |
| **$0** | Licencia MIT. Sin planes, sin límite de usuarios, sin ventas adicionales — y nada se comunica hacia afuera. |
| **1.212** declaraciones de rutas del núcleo | en **104** declaraciones de blueprints de `backend/app/api`; inventario del código, sin extensiones |
| **118** plantillas de apps incluidas | archivos YAML de apps en el directorio raíz; las plantillas de extensiones de bases de datos se cuentan aparte |
| **5.089** casos de prueba de backend recopilados | recopilación de un checkout limpio; no significa que todos se hayan ejecutado o aprobado |
| **0,83 MB** gzip de JS/CSS enlazado por el HTML | scripts, estilos y precargas de módulos del HTML de producción |
| **3,31 MB** gzip de todo el JS/CSS generado | incluye módulos diferidos, idiomas y adaptadores de dependencias; excluye fuentes e imágenes |
| **$0** de licencia | Licencia MIT, sin suscripciones ni tarifas por usuario |
Autoalojado y nativo en Docker, sobre hardware que ya estás pagando.
Los valores gzip suman archivos comprimidos por separado a nivel 9; no son tiempos de carga medidos. La RAM y el tamaño de imagen dependen de la compilación, plataforma y carga de trabajo.
<!-- END GENERATED MEASUREMENTS -->
---
## 🚀 Inicio Rápido
> ⏱️ En funcionamiento en menos de 2 minutos
> El tiempo de instalación depende del servidor, la red y los paquetes necesarios.
### Opción 1: Instalación en Una Línea (Recomendada)
@@ -115,7 +116,7 @@ Consulta la [Guía de Instalación](INSTALLATION.md) para instrucciones paso a p
| **Disco** | 10 GB | 20+ GB |
| **Docker** | 24.0+ (opcional para el panel) | Última versión |
> El panel en sí solo usa ~180 MB de RAM y ~500 MB de disco — el resto es margen para tus apps. Funciona bien en un VPS de 1 GB, un portátil viejo o una Raspberry Pi (ARM64): pensado tanto para homelabs como para producción.
> Estos requisitos orientan el dimensionamiento; no son una prueba de capacidad. Reserva RAM y disco adicionales para apps, imágenes, bases de datos, registros y copias de seguridad. Consulta la [guía de medición](METRICS.md) para medir tu carga de trabajo.
---
+12 -11
View File
@@ -44,25 +44,26 @@ ou o custo de plataformas gerenciadas.
## 📊 Em Números
Tudo abaixo é medido a partir deste repositório, não estimado.
<!-- BEGIN GENERATED MEASUREMENTS -->
Registro: **2026-09-05**. [Definições, medições e comandos de reprodução](METRICS.md).
| | |
|---|---|
| **1.519** endpoints REST | distribuídos em 107 blueprints — `/api/v1/*`, com OpenAPI e Swagger UI em `/api/v1/docs` |
| **106** templates de aplicações | incluídos no repositório, sem precisar de conta em nenhum registro |
| **2.633** testes de backend | a suíte completa roda a cada alteração |
| **1,75 MB** de interface web (gzip) | 65 telas, servidas do seu próprio servidor — sem CDN |
| **~180 MB** residentes | o painel inteiro, um único processo — convive folgadamente com suas apps num VPS de 1 GB |
| **501 MB** de imagem de contêiner | ou instale direto no host; Docker é opcional para o painel |
| **$0** | Licença MIT. Sem planos, sem limite de usuários, sem upsell — e nada é enviado para fora. |
| **1.212** declarações de rotas do núcleo | em **104** declarações de blueprints em `backend/app/api`; inventário do código, sem extensões |
| **118** templates de apps incluídos | arquivos YAML de apps no diretório raiz; templates de extensões de banco são contados separadamente |
| **5.089** casos de teste de backend coletados | coleta de um checkout limpo; não significa que todos executaram ou passaram |
| **0,83 MB** gzip de JS/CSS vinculado pelo HTML | scripts, estilos e pré-carregamentos de módulos no HTML de produção |
| **3,31 MB** gzip de todo o JS/CSS gerado | inclui módulos sob demanda, idiomas e adaptadores de dependências; exclui fontes e imagens |
| **$0** de licença | Licença MIT, sem assinaturas ou taxas por usuário |
Auto-hospedado e nativo em Docker, em hardware que você já paga.
Os valores gzip somam arquivos comprimidos separadamente no nível 9; não são tempos de carregamento medidos. A RAM e o tamanho da imagem dependem da compilação, plataforma e carga de trabalho.
<!-- END GENERATED MEASUREMENTS -->
---
## 🚀 Início Rápido
> ⏱️ Pronto para usar em menos de 2 minutos
> O tempo de instalação depende do servidor, da rede e dos pacotes necessários.
### Opção 1: Instalação em Uma Linha (Recomendado)
@@ -115,7 +116,7 @@ Consulte o [Guia de Instalação](INSTALLATION.md) para instruções passo a pas
| **Disco** | 10 GB | 20+ GB |
| **Docker** | 24.0+ (opcional para o painel) | Mais recente |
> O painel em si usa apenas ~180 MB de RAM e ~500 MB de disco — o resto é margem para os seus apps. Funciona bem num VPS de 1 GB, num notebook antigo ou num Raspberry Pi (ARM64): feito tanto para homelabs quanto para produção.
> Estes requisitos orientam o dimensionamento; não são um teste de capacidade. Reserve RAM e disco adicionais para apps, imagens, bancos, logs e backups. Consulte o [guia de medição](METRICS.md) para medir sua carga de trabalho.
---
+12 -11
View File
@@ -44,25 +44,26 @@ Docker 容器和安全策略——无需 Kubernetes 的复杂性,
## 📊 数据一览
以下数据均实测自本仓库,并非估算。
<!-- BEGIN GENERATED MEASUREMENTS -->
测量快照:**2026-09-05**。[定义、原始测量结果和复现命令](METRICS.md)。
| | |
|---|---|
| **1,519** 个 REST 接口 | 分布在 107 个蓝图中 — `/api/v1/*`,并在 `/api/v1/docs` 提供 OpenAPI 与 Swagger UI |
| **106** 个一键应用模板 | 随仓库内置,无需注册任何应用市场账号 |
| **2,633** 项后端测试 | 每次变更都会跑完整套件 |
| **1.75 MB** 前端界面(gzip 后) | 65 个页面,静态资源全部由你自己的服务器提供 — 不依赖 CDN |
| **~180 MB** 常驻内存 | 整个面板仅一个进程 — 在 1 GB 内存的 VPS 上与你的应用从容共存 |
| **501 MB** 容器镜像 | 也可直接装在宿主机上;面板本身并不强制使用 Docker |
| **$0** | MIT 许可证。没有套餐分级、没有席位限制、没有增值推销 — 也不会向外回传任何数据。 |
| **1,212** 个核心路由声明 | 来自 `backend/app/api` 中 **104** 个蓝图声明;这是源码清单,不包括扩展 |
| **118** 个内置应用模板 | 模板目录根层级的应用 YAML 文件;数据库扩展模板另计 |
| **5,089** 个已收集的后端测试用例 | 基于干净检出的测试收集结果;不代表所有用例均已运行或通过 |
| **0.83 MB** gzip 的 HTML 直接引用 JS/CSS | 生产 HTML 中的脚本、样式表和模块预加载资源 |
| **3.31 MB** gzip 的全部构建 JS/CSS | 包括按需模块、语言包和依赖适配文件;不含字体和图片 |
| **$0** 许可费用 | MIT 许可证,无订阅或席位费用 |
自托管、Docker 原生,跑在你已经付过钱的机器上。
gzip 数值为各文件分别按级别 9 压缩后的总和,并非实测页面加载时间。内存占用与镜像大小取决于构建、平台和工作负载,不作通用占用保证。
<!-- END GENERATED MEASUREMENTS -->
---
## 🚀 快速开始
> ⏱️ 不到 2 分钟即可启动运行
> 安装时间取决于服务器、网络和所需的软件包。
### 方式一:一键安装(推荐)
@@ -113,7 +114,7 @@ docker compose up -d # 访问 http://localhost:5000
| **磁盘** | 10 GB | 20+ GB |
| **Docker** | 24.0+(面板本身可选) | 最新版 |
> 面板本身仅占用约 180 MB 内存和 500 MB 磁盘——其余都是为你的应用预留的空间。在 1 GB VPS、闲置笔记本或树莓派(ARM64)上都能流畅运行:既适合家庭实验室,也适合生产环境。
> 这些要求是资源规划建议,并非容量基准测试。请为托管应用、镜像、数据库、日志和备份预留额外内存与磁盘空间。可参考[测量指南](METRICS.md)评估自己的工作负载。
---
File diff suppressed because it is too large. Load diff
+20
View File
@@ -0,0 +1,20 @@
{
"schema_version": 1,
"measured_at_utc": "2026-09-05T18:41:48.234988+00:00",
"source_revision": "d67e5d07d0c8027d7611048ac74b371db43cd7d1",
"measured_source_has_uncommitted_changes": false,
"measured_source_sha256": "4e6556b3e881b20b53caf0e0c9d23956da514d95e0e6ec9a1bf43ffe0546a8b8",
"scope": "Git-tracked core API Python declarations and root-level bundled app YAML templates; excludes extensions, dependencies and generated/install copies.",
"python_version": "3.11.7",
"platform": "Windows",
"core_route_declarations": 1212,
"core_blueprint_declarations": 104,
"explicit_method_route_pairs_excluding_head_options": 1216,
"route_declarations_with_dynamic_methods": 0,
"core_api_source_files": 111,
"bundled_app_templates": 118,
"backend_tests_clean_collected": 5089,
"collection_scope": "backend/tests, SERVERKIT_CLEAN_COLLECT=1; collected cases, not passed tests",
"runtime_memory_bytes": null,
"container_image_bytes": null
}
@@ -0,0 +1,63 @@
"""Source counters and README snapshots must use explicit, consistent scopes."""
import importlib.util
from pathlib import Path
import unittest
def load(name, filename):
spec = importlib.util.spec_from_file_location(name, Path(__file__).parents[1] / filename)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
measure = load('measure_repository', 'measure-repository.py')
readmes = load('readme_measurements', 'update-readme-measurements.py')
class MeasurementTests(unittest.TestCase):
def test_counts_declarations_separately_from_methods(self):
result = measure.source_counts(['''
bp = Blueprint('test', __name__)
@bp.route('/a')
@bp.route('/b', methods=['GET', 'POST', 'HEAD', 'OPTIONS'])
def handler(): pass
@bp.delete('/c')
def remove(): pass
@bp.route('/d', methods=METHODS)
def dynamic(): pass
'''])
self.assertEqual(result['core_route_declarations'], 4)
self.assertEqual(result['core_blueprint_declarations'], 1)
self.assertEqual(result['explicit_method_route_pairs_excluding_head_options'], 4)
self.assertEqual(result['route_declarations_with_dynamic_methods'], 1)
def test_all_languages_render_the_same_snapshot_without_footprint_claims(self):
source = {'measured_at_utc': '2026-09-05T12:00:00Z', 'core_route_declarations': 1234,
'core_blueprint_declarations': 99, 'bundled_app_templates': 118,
'backend_tests_clean_collected': 5678}
build = {'html_linked_code': {'gzip_bytes': 800_000},
'all_code': {'gzip_bytes': 3_200_000}}
blocks = readmes.blocks(source, build)
self.assertEqual(len(blocks), 4)
for block in blocks.values():
self.assertIn('2026-09-05', block)
self.assertIn('**118**', block)
self.assertIn('**99**', block)
self.assertNotIn('180 MB', block)
self.assertNotIn('501 MB', block)
self.assertIn('**5,678**', blocks['README.md'])
self.assertIn('**5.678**', blocks['docs/README.es.md'])
self.assertIn('**0,80 MB**', blocks['docs/README.pt.md'])
self.assertIn('**3.20 MB**', blocks['docs/README.zh-CN.md'])
def test_unknown_test_count_cannot_be_published_as_zero(self):
with self.assertRaisesRegex(ValueError, 'collect-tests'):
readmes.blocks({'measured_at_utc': '2026-09-05', 'core_route_declarations': 1,
'core_blueprint_declarations': 1, 'bundled_app_templates': 1,
'backend_tests_clean_collected': None}, {})
if __name__ == '__main__':
unittest.main()
+106
View File
@@ -0,0 +1,106 @@
#!/usr/bin/env python3
"""Render all README measurement tables from the same reviewed JSON snapshots.
Run with --write after refreshing docs/measurements/*.json; without it, check
that all four language versions match. This validates documentation agreement,
not whether a stored build was rebuilt from the current checkout.
"""
import argparse
import json
from pathlib import Path
import re
ROOT = Path(__file__).resolve().parents[1]
BEGIN = '<!-- BEGIN GENERATED MEASUREMENTS -->'
END = '<!-- END GENERATED MEASUREMENTS -->'
def blocks(source, build):
date = source['measured_at_utc'][:10]
routes = source['core_route_declarations']
blueprints = source['core_blueprint_declarations']
templates = source['bundled_app_templates']
tests = source['backend_tests_clean_collected']
if tests is None:
raise ValueError('Refresh repository.json with --collect-tests before publishing counts')
linked = build['html_linked_code']['gzip_bytes'] / 1_000_000
total = build['all_code']['gzip_bytes'] / 1_000_000
en = [
f'Snapshot: **{date}**. [Definitions, raw measurements and reproduction commands](docs/METRICS.md).',
f'| **{routes:,}** core route declarations | in **{blueprints}** blueprint declarations under `backend/app/api`; source inventory, excluding extensions |',
f'| **{templates}** bundled app templates | root-level app YAML files; database extension templates counted separately |',
f'| **{tests:,}** backend test cases collected | clean-checkout collection; this is not a claim that every case passed or ran |',
f'| **{linked:.2f} MB** gzip, HTML-linked JS/CSS | scripts, stylesheets and modulepreloads listed in the production HTML |',
f'| **{total:.2f} MB** gzip, all built JS/CSS | includes lazy chunks, locale bundles and vendor shims; excludes fonts and images |',
'| **$0** license cost | MIT-licensed, without subscription or seat fees |',
'Gzip values sum separately compressed files at level 9; they are not measured page-load times. RAM usage and image size depend on the build, platform and workload; no universal footprint is claimed.',
]
es = [
f'Instantánea: **{date}**. [Definiciones, medidas y comandos para reproducirlas](METRICS.md).',
f'| **{routes:,}** declaraciones de rutas del núcleo | en **{blueprints}** declaraciones de blueprints de `backend/app/api`; inventario del código, sin extensiones |',
f'| **{templates}** plantillas de apps incluidas | archivos YAML de apps en el directorio raíz; las plantillas de extensiones de bases de datos se cuentan aparte |',
f'| **{tests:,}** casos de prueba de backend recopilados | recopilación de un checkout limpio; no significa que todos se hayan ejecutado o aprobado |',
f'| **{linked:.2f} MB** gzip de JS/CSS enlazado por el HTML | scripts, estilos y precargas de módulos del HTML de producción |',
f'| **{total:.2f} MB** gzip de todo el JS/CSS generado | incluye módulos diferidos, idiomas y adaptadores de dependencias; excluye fuentes e imágenes |',
'| **$0** de licencia | Licencia MIT, sin suscripciones ni tarifas por usuario |',
'Los valores gzip suman archivos comprimidos por separado a nivel 9; no son tiempos de carga medidos. La RAM y el tamaño de imagen dependen de la compilación, plataforma y carga de trabajo.',
]
pt = [
f'Registro: **{date}**. [Definições, medições e comandos de reprodução](METRICS.md).',
f'| **{routes:,}** declarações de rotas do núcleo | em **{blueprints}** declarações de blueprints em `backend/app/api`; inventário do código, sem extensões |',
f'| **{templates}** templates de apps incluídos | arquivos YAML de apps no diretório raiz; templates de extensões de banco são contados separadamente |',
f'| **{tests:,}** casos de teste de backend coletados | coleta de um checkout limpo; não significa que todos executaram ou passaram |',
f'| **{linked:.2f} MB** gzip de JS/CSS vinculado pelo HTML | scripts, estilos e pré-carregamentos de módulos no HTML de produção |',
f'| **{total:.2f} MB** gzip de todo o JS/CSS gerado | inclui módulos sob demanda, idiomas e adaptadores de dependências; exclui fontes e imagens |',
'| **$0** de licença | Licença MIT, sem assinaturas ou taxas por usuário |',
'Os valores gzip somam arquivos comprimidos separadamente no nível 9; não são tempos de carregamento medidos. A RAM e o tamanho da imagem dependem da compilação, plataforma e carga de trabalho.',
]
zh = [
f'测量快照:**{date}**。[定义、原始测量结果和复现命令](METRICS.md)。',
f'| **{routes:,}** 个核心路由声明 | 来自 `backend/app/api` 中 **{blueprints}** 个蓝图声明;这是源码清单,不包括扩展 |',
f'| **{templates}** 个内置应用模板 | 模板目录根层级的应用 YAML 文件;数据库扩展模板另计 |',
f'| **{tests:,}** 个已收集的后端测试用例 | 基于干净检出的测试收集结果;不代表所有用例均已运行或通过 |',
f'| **{linked:.2f} MB** gzip 的 HTML 直接引用 JS/CSS | 生产 HTML 中的脚本、样式表和模块预加载资源 |',
f'| **{total:.2f} MB** gzip 的全部构建 JS/CSS | 包括按需模块、语言包和依赖适配文件;不含字体和图片 |',
'| **$0** 许可费用 | MIT 许可证,无订阅或席位费用 |',
'gzip 数值为各文件分别按级别 9 压缩后的总和,并非实测页面加载时间。内存占用与镜像大小取决于构建、平台和工作负载,不作通用占用保证。',
]
result = {}
for path, rows in [('README.md', en), ('docs/README.es.md', es), ('docs/README.pt.md', pt), ('docs/README.zh-CN.md', zh)]:
if path.endswith(('.es.md', '.pt.md')):
for old, new in [(f'{routes:,}', f'{routes:,}'.replace(',', '.')),
(f'{tests:,}', f'{tests:,}'.replace(',', '.')),
(f'{linked:.2f} MB', f'{linked:.2f} MB'.replace('.', ',')),
(f'{total:.2f} MB', f'{total:.2f} MB'.replace('.', ','))]:
rows = [row.replace(old, new) for row in rows]
result[path] = '\n'.join([BEGIN, rows[0], '', '| | |', '|---|---|', *rows[1:-1], '', rows[-1], END])
return result
def update(write=False):
source = json.loads((ROOT / 'docs/measurements/repository.json').read_text(encoding='utf-8'))
build = json.loads((ROOT / 'docs/measurements/frontend-build.json').read_text(encoding='utf-8'))
stale = []
for path, block in blocks(source, build).items():
file = ROOT / path
original = file.read_text(encoding='utf-8')
if BEGIN in original:
result = re.sub(re.escape(BEGIN) + r'.*?' + re.escape(END), lambda _match: block, original, flags=re.S)
else:
result, count = re.subn(r'(^## 📊[^\n]*\n\n).*?(\n---\n)', lambda match: match[1] + block + '\n' + match[2], original, count=1, flags=re.S | re.M)
if count != 1:
raise ValueError(f'{path}: cannot find the measurements section')
if result != original:
stale.append(path)
if write:
file.write_text(result, encoding='utf-8')
if stale and not write:
raise SystemExit('README measurements are stale: ' + ', '.join(stale))
print(('Updated' if write else 'Checked') + ' all four README measurement tables.')
if __name__ == '__main__':
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--write', action='store_true')
update(parser.parse_args().write)