From 3c2a5db6779dc189882614cb00c03f6c8149e0ed Mon Sep 17 00:00:00 2001 From: Yuma Takino <36617106+tappe9@users.noreply.github.com> Date: Thu, 1 Oct 2026 10:09:23 -0700 Subject: [PATCH 1/2] docs: simplify bilingual READMEs and preserve detailed guides Organize both entry points around verified results, interpretation and one safe container-only example. Move technical setup to linked bilingual guides, preserve generated data, and distinguish historical Flask results from the latest publication. Add documentation navigation, parity and cleanup regressions. Refs #74. --- README.ja.md | 289 +++---------------- README.md | 301 ++++---------------- docs/IMPLEMENTATIONS.md | 26 +- docs/LOCAL-DEVELOPMENT.ja.md | 222 +++++++++++++++ docs/LOCAL-DEVELOPMENT.md | 228 +++++++++++++++ tests/test_benchmark_implementation_docs.py | 8 + tests/test_benchmark_readme_navigation.py | 143 ++++++++++ 7 files changed, 712 insertions(+), 505 deletions(-) create mode 100644 docs/LOCAL-DEVELOPMENT.ja.md create mode 100644 docs/LOCAL-DEVELOPMENT.md create mode 100644 tests/test_benchmark_readme_navigation.py diff --git a/README.ja.md b/README.ja.md index ddc529f..565019f 100644 --- a/README.ja.md +++ b/README.ja.md @@ -2,19 +2,15 @@ [English](README.md) · [結果サイト](https://tappe9.github.io/simple-api-benchmark/) -**Go・Rust・Node.js・Pythonを、同じAPI・同じ制限・同じ負荷で比較します。** +**同じAPI・リソース上限・負荷設定で、APIスタック全体を比較します。** -Simple API Benchmarkには、同じエンドポイント、同じDockerリソース制限、同じ検証ルールを使う9つのAPI実装があります。普遍的な最速言語を決めることではなく、誰でも理解できて、自分でも再実行できる小さな比較を目指します。 +Go・Rust・Node.js・Pythonの、小さく再現可能な比較です。 +まず検証済みの結果を見るか、APIを1つローカルで試してみてください。 -> **現在の状態:** v0.1.0は初期の4構成を収録したリリースです。現在の`main`は9実装がCI対象です。公式比較は引き続き`eight-stack-v1`の8実装で、Pagesに検証済み結果と履歴を公開しています。Java / Spring Bootは検証対象として登録済みですが、公式測定・公開結果には含まれません。 - -8構成の最初の完全な公式結果は、2026年9月15日に -[Issue #50 / PR #58](https://github.com/tappe9/simple-api-benchmark/pull/58)で公開しました。 -「結果」には、最新の**検証済み公開結果**に含まれる比較グループを明示します。 -実装完了・比較グループ有効化・結果公開は、それぞれ別の到達点です。 -過去の`four-stack-v1`レポートとv0.1.0のリリースは変更せず維持します。 -公開の検証手順は[比較グループの切替手順](docs/IMPLEMENTATIONS.md#eight-stack-rollout-boundary-50)、 -進捗と検証記録は[Issue #50](https://github.com/tappe9/simple-api-benchmark/issues/50)を参照してください。 +実装の登録、CI対象、公式の比較グループ、結果公開は、それぞれ別です。 +現在の状態は[登録・比較グループガイド](docs/IMPLEMENTATIONS.md#registered-and-measured-implementations)を参照してください。 +Java / Spring Bootは登録済みでCI対象ですが、[Issue #73](https://github.com/tappe9/simple-api-benchmark/issues/73)で公式比較への採用を保留しています。 +[v0.1.0のリリース](docs/RELEASING.md)は、初期の4構成のスナップショットとして維持します。 ## 結果 @@ -72,250 +68,57 @@ Source: `c1d8e81b959e2c0ee0e5c4a1d8a7491790479f93` · [Actions run](https://gith -## 比較対象 - -| 言語 | フレームワーク | -|---|---| -| Go | Gin | -| Go | Echo | -| Rust | Actix Web | -| Rust | Axum | -| Node.js | Fastify | -| Node.js | Express | -| Python | FastAPI | -| Python | Flask | - -各実装は、同じ3つのベンチマーク用エンドポイントを提供します。 - -| テスト | エンドポイント | 簡単な説明 | -|---|---|---| -| JSON | `GET /json` | 小さなJSONを返す | -| PostgreSQL | `GET /db/42` | 1行取得してJSONで返す | -| CPU | `GET /cpu` | Fibonacci(30)を計算して返す | - -`GET /health`は起動確認だけに使用します。 - -有効な`eight-stack-v1`は上記8実装を比較します。過去の`four-stack-v1`はGo / Gin、Rust / Actix Web、Node.js / Fastify、Python / FastAPIの4実装だけを含みます。追加frameworkを過去の結果へ挿入したり、存在しない計測値をゼロ表示したりはしません。 - -## 同じ条件 - -すべての実装で、次の条件を揃えます。 - -- 同じAPI仕様 -- 同じ入力と期待する出力 -- 同じCPU・メモリ制限 -- 1つのserver processまたはworker -- 同じPostgreSQLデータ、SQL、pool上限 -- 同じ負荷設定 -- 各テストを3回実行し、中央の結果を表示 - -計測前にAPIの返却内容を自動確認します。HTTPエラーやタイムアウトが発生した結果は、正常な結果として公開しません。 - -## ローカルPostgreSQL環境 - -Docker Compose v2とMakeが必要です。共通DBには、ダイジェストで固定した公式の`postgres:18.6-bookworm`イメージを使用します。Composeプロジェクト内の`benchmark`ネットワーク上で`postgres`サービスとして動作し、ホスト側のポートは公開しません。 - -| 設定 | 値 | -|---|---| -| Service / host | `postgres` | -| 内部ポート | `5432` | -| Database | `benchmark` | -| User | `benchmark` | -| Password | `benchmark` | - -これらは理解しやすさを優先したローカル専用の初期値であり、本番用の認証情報ではありません。APIサービスは、`docker-compose.yml`の共通設定`DATABASE_HOST`、`DATABASE_PORT`、`DATABASE_NAME`、`DATABASE_USER`、`DATABASE_PASSWORD`を使用します。 - -```bash -make db-up # PostgreSQLを起動し、healthy状態とfixtureを確認する -make db-check # 起動中DBのfixtureを確認する -make db-reset # 現在のDB状態を破棄し、fixtureを再作成する -make test-db # 起動・リセット・クリーンアップのacceptance checkを実行する -make down # コンテナとプロジェクトネットワークを削除する -``` - -PostgreSQLのデータは`tmpfs`上に置かれ、環境を再作成した際に引き継がれません。`database/init.sql`から、常に同じ`items`テーブルと`42 | Item 42 | 4200`の1行を作成します。 - -## Go / Gin実装 +## 結果の読み方 -Go / Gin実装は`apps/go-gin/`にあり、現在はGo 1.27.1、Gin 1.12.0、pgx/v5 5.10.0を使用します。server processは1つで、PostgreSQLのpool上限は10接続です。Docker ComposeではAPIコンテナを1 CPU・512 MBに制限し、非rootユーザー`65532:65532`で実行します。ポート`8080`はloopback interfaceだけに公開します。 +各スタックは、同じ[API仕様](docs/API-CONTRACT.md)を実装します。 -```bash -docker compose up --detach --build --wait go-gin -curl http://127.0.0.1:8080/health -curl http://127.0.0.1:8080/json -curl http://127.0.0.1:8080/db/42 -curl http://127.0.0.1:8080/cpu -make down -``` - -Goのformat、unit test、vet、コンテナ起動、API仕様、リソース制限、クリーンアップをまとめて確認するには、次を実行します。 - -```bash -make test-go-gin -``` - -## Go / Echo実装 - -Go / Echo実装は`apps/go-echo/`にあり、Go 1.27.1、Echo v5.3.1、pgx/v5 5.10.0を固定しています。Gin baselineとGo runtime・PostgreSQL driverを揃えたまま比較できるようにし、Gin側を暗黙にupgradeしていません。同じSQLとfixtureを使い、HTTP受付前にPostgreSQLへ接続し、pool上限は10接続です。 - -1つの`net/http` server processでEcho routerを提供します。比較と無関係なmiddleware、response cache、CPU処理の事前計算は追加せず、`/cpu`はrequestごとに直接再帰でFibonacci(30)を計算します。production imageにはstatic binaryだけを配置し、非rootの`65532:65532`で実行します。Composeから共通の1 CPU・512 MB、capability drop、no-new-privileges、loopback限定port公開、restart policyを継承します。 - -```bash -docker compose up --detach --build --wait go-echo -curl http://127.0.0.1:8080/health -curl http://127.0.0.1:8080/json -curl http://127.0.0.1:8080/db/42 -curl http://127.0.0.1:8080/cpu -make down -make test-go-echo -``` - -`make test-go-echo`ではformat、unit tests、vet、production image、実PostgreSQL/API acceptance、BIGINT境界、起動失敗、SIGTERMでのgraceful shutdown、resource/process isolation、cleanupを確認します。共通contract suiteも変更せずEchoへ適用します。将来のGin対Echoの結果も、このリポジトリで固定した条件下のframework/routerを含むAPIスタック全体の比較であり、Goフレームワークの普遍的な順位を示すものではありません。 - -## Rust / Actix Web実装 - -Rust実装は`apps/rust-actix/`にあり、Rust 1.98.1、Actix Web 4.15.0、SQLx 0.9.0、Serde 1.0.228、serde_json 1.0.145を固定しています。推移的な依存も`Cargo.lock`で固定します。1つのActix workerでポート`8080`を受け付け、通常のSerde値からJSONを生成します。`/cpu`では毎request直接再帰でFibonacci(30)を計算します。SQLx poolはHTTP受付前にPostgreSQLへ接続し、上限は10接続です。 - -Dockerでは公開済みの`rust:1.98.0-bookworm`をdigest固定し、誤コンパイル修正を含むcompiler 1.98.1を明示導入して、`cargo +1.98.1 build --release --locked`でビルドします。実行用のDebian Bookworm slimもdigest固定し、release binaryのみをコピーして`65532:65532`で実行します。Cargoやソースコードは含めません。ComposeはPostgreSQLのhealthy状態を待ち、capabilities削除と権限昇格禁止、1 CPU・512 MB、loopback限定公開、restartなしを適用します。 - -```bash -docker compose up --detach --build --wait rust-actix -curl http://127.0.0.1:8080/health -curl http://127.0.0.1:8080/json -curl http://127.0.0.1:8080/db/42 -curl http://127.0.0.1:8080/cpu -make down -make test-rust-actix -``` +| エンドポイント | 処理 | +| --- | --- | +| `GET /json` | 小さなJSONを返す | +| `GET /db/42` | PostgreSQLから1行取得し、JSONで返す | +| `GET /cpu` | 直接再帰でFibonacci(30)を計算する | -acceptance targetにはRustup、Python 3、Docker Compose v2、Makeが必要です。format、locked Rust tests、警告をエラーにするClippy、実DB・API、BIGINT境界、起動失敗、SIGTERM終了、container・network削除を検証します。各APIは同じホストポート`8080`を使うため、1つずつ起動してください。 +`GET /health`は起動確認専用です。共通のリソース上限、DBのfixture、pool上限、負荷設定は +[測定方法](docs/METHODOLOGY.md)に記載しています。1つのserver processまたはworkerでも、 +request threadの構成が同一とは限りません。計測前に契約テストを実行し、HTTPエラーや +タイムアウトのある結果を正常な公式結果として公開しません。 -## Rust / Axum実装 +比較にはランタイム、HTTPサーバー、JSONライブラリ、DBドライバー、コンテナ設定も含まれます。 +共有GitHub-hosted環境での計測で、メモリもAPIコンテナのサンプル値です。 +ある回で速かったスタックが、言語やフレームワークの普遍的な順位を示すわけではありません。 +表示されたsourceとprofileで一緒に計測した対象だけを比較し、新しい実装を過去のレポートに +挿入したり、存在しない値をゼロとして扱ったりしないでください。 -`apps/rust-axum/`はAxum 0.8.9とTokio 1.53.1を使います。既存のActix実装は変更せず、Rust 1.98.1、SQLx 0.9.0、Serde 1.0.228、serde_json 1.0.145、digest固定のbuilder・runtimeイメージを揃えています。推移的依存はcommitした`Cargo.lock`で固定します。 +## APIを1つ試す -1つのserver processが明示的なTokio `current_thread` executorを使います。native Serde応答、共通のparameterized SQL、最大10接続のpool、requestごとの直接再帰によるFibonacci(30)を維持し、CPU offload、response cache、追加async workerは使いません。SIGINT/SIGTERMでは処理中のrequest完了を待ってからpoolを閉じます。非rootのrelease imageは共通の1 CPU・512 MiB制限を継承します。 +このリポジトリをcloneし、ルートディレクトリで実行します。 +起動済みのDocker daemon、BuildKitと`up --wait`に対応したDocker Compose v2、curlが必要です。 +この例では、Go・Rust・Node.js・Python・Javaをホストへ入れる必要はありません。 +ツールチェーンはイメージのビルドで用意します。`8080`ポートを空け、APIは1つずつ起動してください。 ```bash -make test-rust-axum # Rust検証と実PostgreSQL・container acceptance -make test-contract CONTRACT_IMPL=rust-axum # 変更しない共通contract -make axum-diagnostic # commit済みの作業ツリーで実行。結果は公開しない +docker compose --project-name sab-example up --detach --build --wait go-gin +curl --fail http://127.0.0.1:8080/health +curl --fail http://127.0.0.1:8080/json +curl --fail http://127.0.0.1:8080/db/42 +curl --fail http://127.0.0.1:8080/cpu +docker compose --project-name sab-example down --remove-orphans --volumes ``` -port 8080を空け、各commandを順番に実行してください。Axum専用diagnosticは既存の固定済み負荷ツールとexternal readinessを使い、source・version情報と生データを`.cache/axum-diagnostic/`へ保存します。request errorやcleanup失敗を成功扱いせず、短縮profileの数値を公式結果として公開しません。通常CIのAxum jobで必須実行するため、一時的な開発workflowは不要です。実行方式、検証範囲、保存先の制約は[Axum実装・diagnosticガイド](docs/AXUM.md)を参照してください。 - -## Node.js / Fastify実装 - -Node実装は`apps/node-fastify/`にあり、Node.js 24.20.0 LTS、Fastify 5.12.3、pg 8.23.0を使用します。直接依存と`package-lock.json`を固定し、公式の`node:24.20.0-bookworm-slim`イメージもdigestで固定します。再現性と保守性のため、LTSランタイムと安定版のFastify 5系を採用しています。 - -production modeでNode processを直接1つ起動し、PostgreSQLへの確認queryが成功してからHTTP接続を受け付けます。pool上限は10接続です。コンテナは非rootユーザー`node`で実行し、Linux capabilitiesを削除し、共通の1 CPU・512 MB制限を適用します。公開先は`127.0.0.1:8080`だけです。`/json`は通常のobjectをシリアライズし、`/cpu`は毎request直接再帰でFibonacci(30)を計算します。終了時はHTTP serverとpoolを閉じます。 - -各APIは同じローカルポートを使うため、1つずつ起動してください。 - -```bash -docker compose up --detach --build --wait node-fastify -curl http://127.0.0.1:8080/health -curl http://127.0.0.1:8080/json -curl http://127.0.0.1:8080/db/42 -curl http://127.0.0.1:8080/cpu -make down -make test-node-fastify -``` - -`make test-node-fastify`にはNode.js 24.20.0、npm、Python 3、Docker Compose v2、Makeが必要です。focused tests、構文検証、image・API確認(実DBの更新・異常系を含む)、resource・process確認、正常終了、container・networkの削除を実行します。 - -## Python / FastAPI実装 - -Python実装は`apps/python-fastapi/`にあり、Python 3.14.7、FastAPI 0.141.1、Uvicorn 0.52.4、asyncpg 0.31.0を使用します。実行時依存と開発用依存は、バージョンとSHA256 hashを固定したlockファイルで管理します。Dockerの両stageは、index digestで固定した公式の`python:3.14.7-slim-bookworm`イメージを使用します。 - -Uvicornを直接1 workerで起動し、標準のasyncioイベントループとHTTP/1.1実装のh11を使用します。PostgreSQL接続確認後にHTTP受付を開始し、asyncpg poolの上限は10接続です。通常のPython値からJSONを生成し、signed BIGINTのIDも数値として正確に返します。`/cpu`は毎request直接再帰でFibonacci(30)を計算し、cacheや事前計算は使用しません。終了時はlifespanでpoolを閉じます。 - -productionコンテナは非rootユーザー`10001:10001`で実行し、testsと開発用依存を含めません。Linux capabilitiesを削除し、1 CPU・512 MBに制限します。ComposeはPostgreSQLのhealthy状態を待ち、`127.0.0.1:8080`だけに公開し、`/health`を確認します。自動restartは行いません。 - -```bash -docker compose up --detach --build --wait python-fastapi -curl http://127.0.0.1:8080/health -curl http://127.0.0.1:8080/json -curl http://127.0.0.1:8080/db/42 -curl http://127.0.0.1:8080/cpu -make down -make test-python-fastapi PYTHON=python3.14 -``` - -acceptance targetにはPOSIX環境のPython 3.14.7、Docker Compose v2、Makeが必要です。一時virtual environmentへhash検証付きで開発用依存をinstallし、Ruff、focused pytest tests、実Dockerサービス、DB更新・異常系、資源制限、1 worker、起動失敗、SIGTERM終了、container・network削除を確認します。Dockerを使わないfocused testsは[Contributing](CONTRIBUTING.md)を参照してください。 - -## Java / Spring Boot実装 - -`apps/java-spring-boot/`はTemurin 25.0.4.1+1、Spring Boot 4.1.1、 -Spring MVC / Tomcat、JDBC / HikariCP、チェックサムを検証するGradle 9.7.1を使用します。 -共通のAPI、単一プロセス、1 CPU・512 MiB、DB接続上限10を維持します。 -**9番目の登録済み実装ですが、公式測定対象への追加ではありません。** -`eight-stack-v1`と公開済みの測定値は変更しません。 - -```bash -make test-java-spring-boot -make test-contract CONTRACT_IMPL=java-spring-boot -``` - -受入テストには、POSIXホスト上の固定JDK、Python 3.10以降、Make、Docker Compose v2が必要です。 -コンテナのみを使う共通契約テストには、ホストのJDKは不要です。 -固定依存関係、起動・終了の検証、公式測定と分ける理由は -[Java実装ガイド](docs/JAVA-SPRING-BOOT.md)を参照してください。 - -9実装すべてを、共通のAPI契約で検証できます。 - -```bash -make test-contract # 9実装を1つずつ順番に検証 -make test-contract CONTRACT_IMPL=go-echo # 1実装を同じ契約で検証 -``` - -HTTP status、JSONの内容・型、規定のerror response、応答の再現性を確認し、 -失敗時にも分離された検証環境を片付けます。必要な環境、起動済みAPIのbase URLを指定する -方法、cleanupの制約は[共通contractの実行ガイド](CONTRIBUTING.md#shared-contract-checks)を -参照してください。ローカルのbenchmark runnerは利用可能です。PRでは同じ検証と公開しない短縮benchmarkを実行します。 -公式benchmarkは、性能に関わる変更を取り込んだ後、または再現性などの調査が必要な場合に、 -[trusted mainで明示的に実行を依頼します](docs/AUTOMATION.md#when-to-request-an-official-benchmark)。 -週次実行や変更を検知しての自動計測は行いません。上流で新しいversionが公開されただけでは、 -このリポジトリの固定versionは更新されません。手動起動後も、既存の検証済み結果の自動公開と -Pages反映まで実行するため、計測だけの操作ではありません。公開方式の見直しとmain保護は別途検討します。 -active official cohortは`eight-stack-v1`で、Issue #50で最初の検証済み結果を公開済みです。その後の結果更新にも、新しい完全な公式計測と検証が必要です。 -過去の4構成のレポートは書き換えません。 - -公式結果の監査・push成功後に、共通Pages workflowを直接呼び出します。[認可と復旧手順](docs/AUTOMATION.md#github-pages)を参照してください。表示の復旧だけのために再計測する必要はありません。製品リリースは -[maintainerが明示的に実施する別の操作](docs/RELEASING.md)であり、Pagesの副作用として作成しません。 - -## ローカルでの計測 - -変更をcommitした作業ツリー、Python 3.10以上、curl、ローカルのDocker Compose v2で実行します。 - -```bash -make benchmark -``` - -SHA256とversionを固定したoha 1.16.0を検証・導入し、APIを1つずつビルド・起動します。 -共通contractの成功後、各endpointを規定条件でwarm-upし、3回ずつ計測します。 -全計測とcleanupが成功した場合だけ、`results/latest.json`をatomicに保存します。 -途中失敗では既存結果を保持します。生成ファイルはローカル結果であり、公式公開結果ではありません。 - -focused testsは`make test-benchmark`、短縮診断は`make benchmark-smoke`です。 -短縮診断は`latest.json`を更新しません。必要環境、単位、結果形式、期限、メモリサンプリングの制約は -[実行・結果形式ガイド](docs/BENCHMARK.md)を参照してください。 - -## ドキュメント - -- [結果サイト](https://tappe9.github.io/simple-api-benchmark/) -- [アーキテクチャ](ARCHITECTURE.md) -- [Axum実装・diagnostic](docs/AXUM.md) -- [API仕様](docs/API-CONTRACT.md) -- [測定方法](docs/METHODOLOGY.md) -- [実行・結果形式](docs/BENCHMARK.md) -- [ロードマップ](ROADMAP.md) -- [コントリビューション](CONTRIBUTING.md) -- [セキュリティ](SECURITY.md) +Go / Ginと依存先のPostgreSQLが、固定fixtureで起動します。ホストへ公開するのはAPIのloopbackポートだけです。 +最後のコマンドは**`sab-example`の全コンテナ、ネットワーク、孤立コンテナ、volumeを削除します**。 +PostgreSQLはtmpfsを使うため、DBコンテナ停止時にローカルのデータは失われます。 +このプロジェクト名は使い捨ての例専用にし、途中で中断した場合も同じcleanupを実行してください。 +このHTTPアクセスは動作を試す例であり、ベンチマーク結果ではありません。 -## 重要な注意点 +## 詳しいガイド -このプロジェクトが比較するのは、プログラミング言語やフレームワーク単体ではなくAPIスタック全体です。結果には、フレームワーク、ランタイム、HTTPサーバー、JSONライブラリ、PostgreSQLドライバー、コンテナ設定の違いも含まれます。「今回の測定ではRust / Actix Webが最速だった」という結果は「Rustは常に最速」という意味ではなく、将来GinとEchoに差が出ても、それだけでGoフレームワークの普遍的な順位は決まりません。 +- [ローカル環境・実装の詳細・cleanup](docs/LOCAL-DEVELOPMENT.ja.md) +- [共通契約テスト](CONTRIBUTING.md#shared-contract-checks)と[コントリビューション](CONTRIBUTING.md) +- [ローカル計測と結果形式](docs/BENCHMARK.md) +- [公式benchmarkの実行依頼](docs/AUTOMATION.md#when-to-request-an-official-benchmark): 明示的な依頼でのみ起動し、監査済み結果の公開とPages反映まで行います +- [Pagesの認可・復旧手順](docs/AUTOMATION.md#github-pages): 表示の復旧だけのために再計測する必要はありません +- [アーキテクチャ](ARCHITECTURE.md) · [ロードマップ](ROADMAP.md) · [セキュリティ](SECURITY.md) ## ライセンス diff --git a/README.md b/README.md index c82a9fb..8da9d32 100644 --- a/README.md +++ b/README.md @@ -2,19 +2,16 @@ [日本語](README.ja.md) · [Results site](https://tappe9.github.io/simple-api-benchmark/) -**Go vs Rust vs Node.js vs Python — same API, same limits, simple results.** +**Compare complete API stacks with the same API, resource caps and load profile.** -Simple API Benchmark contains nine API implementations with the same endpoints, Docker resource limits, and validation rules. The goal is not to declare a universal winner. The goal is to make a small, repeatable comparison that anyone can understand. +A small, reproducible comparison of Go, Rust, Node.js and Python stacks. +Start with the verified results below, or try one API locally. -> **Project status:** v0.1.0 is released as the original four-stack snapshot. Current `main` includes nine CI-covered implementations. The active `eight-stack-v1` cohort has verified published results and a Pages dashboard with history navigation. Java / Spring Boot is registered for validation but is not part of the official cohort or published results. - -The first complete eight-stack official result was published on September 15, 2026 -under [Issue #50 / PR #58](https://github.com/tappe9/simple-api-benchmark/pull/58). -The Results section identifies the latest **verified publication**; implementation, -cohort activation and result publication are separate milestones. Historical -`four-stack-v1` reports and the v0.1.0 release remain unchanged. See the -[cohort rollout guide](docs/IMPLEMENTATIONS.md#eight-stack-rollout-boundary-50) -and [Issue #50](https://github.com/tappe9/simple-api-benchmark/issues/50) for rollout verification. +Registration, CI coverage, official cohort membership and publication are separate. +The [registry and cohort guide](docs/IMPLEMENTATIONS.md#registered-and-measured-implementations) +is the current status reference. Java / Spring Boot is registered and CI-covered, +with official admission deferred in [Issue #73](https://github.com/tappe9/simple-api-benchmark/issues/73). +The [v0.1.0 release](docs/RELEASING.md) remains the original four-stack snapshot. ## Results @@ -72,255 +69,59 @@ Cohort: `eight-stack-v1` · Definition: `simple-api-v1` · API readiness: `exter -## What is compared? - -| Language | Framework | -|---|---| -| Go | Gin | -| Go | Echo | -| Rust | Actix Web | -| Rust | Axum | -| Node.js | Fastify | -| Node.js | Express | -| Python | FastAPI | -| Python | Flask | - -Each implementation provides the same three benchmark endpoints: - -| Test | Endpoint | Simple explanation | -|---|---|---| -| JSON | `GET /json` | Return a small JSON response | -| PostgreSQL | `GET /db/42` | Read one row and return it as JSON | -| CPU | `GET /cpu` | Calculate Fibonacci(30) and return the result | - -A separate `GET /health` endpoint is used only to check readiness. - -The active `eight-stack-v1` cohort includes all eight implementations above. Historical `four-stack-v1` reports contain only Go / Gin, Rust / Actix Web, Node.js / Fastify, and Python / FastAPI; the other frameworks are never inserted into historical results or displayed as zero-valued measurements. - -## Same conditions - -Every implementation uses: - -- the same API contract; -- the same input and expected output; -- the same CPU and memory limits; -- one server process or worker; -- the same PostgreSQL data, SQL, and pool limit; -- the same load settings; -- three benchmark runs, with the middle result shown. - -Contract tests run before measurements. A result with request errors or timeouts is not published as a valid result. +## What the results mean -## Local PostgreSQL environment +Each stack serves the same [API contract](docs/API-CONTRACT.md): -Docker Compose v2 and Make are required. The shared database uses the official `postgres:18.6-bookworm` image pinned by digest. It runs as the `postgres` service on the project-scoped `benchmark` network and does not publish a host port. +| Endpoint | Work | +| --- | --- | +| `GET /json` | Return a small JSON response | +| `GET /db/42` | Read one PostgreSQL row and return JSON | +| `GET /cpu` | Calculate Fibonacci(30) by direct recursion | -| Setting | Value | -|---|---| -| Service / host | `postgres` | -| Internal port | `5432` | -| Database | `benchmark` | -| User | `benchmark` | -| Password | `benchmark` | +`GET /health` is for readiness only. The [methodology](docs/METHODOLOGY.md) +describes the common resource caps, database fixture, pool limit and load settings. +One server process or worker does not imply identical request-thread models. +Contract checks precede measurement; request errors or timeouts prevent a valid publication. -These are intentionally simple local-only defaults, not production credentials. API services use the common `DATABASE_HOST`, `DATABASE_PORT`, `DATABASE_NAME`, `DATABASE_USER`, and `DATABASE_PASSWORD` settings from `docker-compose.yml`. +These results compare the complete stack, including the runtime, HTTP server, +JSON library, database driver and container configuration. Shared GitHub-hosted +hardware and sampled API-container memory limit interpretation. A faster stack +in one run is not a universal language or framework ranking. Compare only the +members measured together under the displayed source and profile; never insert +new implementations into historical reports or interpret missing values as zero. -```bash -make db-up # start PostgreSQL, wait for health, and validate the fixture -make db-check # validate the exact fixture in the running database -make db-reset # discard all current DB state and recreate the fixture -make test-db # run the complete startup, reset, and cleanup acceptance check -make down # remove containers and the project network -``` - -PostgreSQL data lives on `tmpfs`. It is never reused across a recreated environment, and `database/init.sql` always creates the same `items` table and row `42 | Item 42 | 4200`. - -## Go / Gin implementation - -The Go / Gin implementation lives in `apps/go-gin/` and currently uses Go 1.27.1, Gin 1.12.0, and pgx/v5 5.10.0. It runs one server process with a PostgreSQL pool capped at 10 connections. Docker Compose limits the API container to 1 CPU and 512 MB, runs it as non-root user `65532:65532`, and publishes port `8080` only on the loopback interface. +## Try one API locally -```bash -docker compose up --detach --build --wait go-gin -curl http://127.0.0.1:8080/health -curl http://127.0.0.1:8080/json -curl http://127.0.0.1:8080/db/42 -curl http://127.0.0.1:8080/cpu -make down -``` - -Run the complete Go formatting, unit-test, vet, container, API-contract, resource-limit, and cleanup checks with: +From a clone of this repository, open a terminal at its root. You need a running +Docker daemon, Docker Compose v2 with BuildKit and `up --wait` support, and curl. +No host Go, Rust, Node.js, Python or Java installation is needed for this example. +Leave port `8080` free and run one API at a time; the image build supplies its toolchain. ```bash -make test-go-gin +docker compose --project-name sab-example up --detach --build --wait go-gin +curl --fail http://127.0.0.1:8080/health +curl --fail http://127.0.0.1:8080/json +curl --fail http://127.0.0.1:8080/db/42 +curl --fail http://127.0.0.1:8080/cpu +docker compose --project-name sab-example down --remove-orphans --volumes ``` -## Go / Echo implementation - -The Go / Echo implementation lives in `apps/go-echo/` and pins Go 1.27.1, Echo v5.3.1, and pgx/v5 5.10.0 so it stays directly comparable with the Gin baseline without silently upgrading the Go runtime or PostgreSQL driver. It uses the same SQL and fixture, connects before serving HTTP, and caps the PostgreSQL pool at 10 connections. - -One `net/http` server process serves the Echo router. The implementation adds no unrelated middleware, response cache, or CPU precomputation; `/cpu` performs direct recursive Fibonacci(30) for every request. The production image contains only the statically built binary, runs as non-root `65532:65532`, and inherits the shared 1 CPU / 512 MB isolation envelope, capability drop, no-new-privileges policy, loopback-only port publication, and restart policy from Compose. - -```bash -docker compose up --detach --build --wait go-echo -curl http://127.0.0.1:8080/health -curl http://127.0.0.1:8080/json -curl http://127.0.0.1:8080/db/42 -curl http://127.0.0.1:8080/cpu -make down -make test-go-echo -``` - -`make test-go-echo` runs formatting, unit tests, vet, production image and real PostgreSQL/API acceptance checks, BIGINT boundaries, startup failure, graceful SIGTERM shutdown, resource/process isolation, and cleanup. The shared contract suite runs against Echo unchanged. A future Gin-versus-Echo result is still a complete-stack comparison of framework/router behavior under this repository's fixed conditions, not a universal ranking of Go frameworks. - -## Rust / Actix Web implementation - -The Rust implementation lives in `apps/rust-actix/` and pins Rust 1.98.1, Actix Web 4.15.0, SQLx 0.9.0, Serde 1.0.228, and serde_json 1.0.145. `Cargo.lock` fixes the transitive dependency graph. One Actix worker serves port `8080`, uses native Serde response values, and performs direct recursive Fibonacci(30) for every CPU request. The SQLx pool connects before HTTP startup and allows at most 10 PostgreSQL connections. - -The production Dockerfile uses the published `rust:1.98.0-bookworm` builder pinned by digest and explicitly installs compiler 1.98.1, which fixes a compiler miscompilation. It builds with `cargo +1.98.1 build --release --locked`. The digest-pinned Debian Bookworm slim runtime contains the release binary, not Cargo or the source tree, and runs as `65532:65532`. Compose waits for healthy PostgreSQL, drops capabilities, disallows privilege escalation, applies 1 CPU / 512 MB limits, publishes only `127.0.0.1:8080`, and disables restarts. - -```bash -docker compose up --detach --build --wait rust-actix -curl http://127.0.0.1:8080/health -curl http://127.0.0.1:8080/json -curl http://127.0.0.1:8080/db/42 -curl http://127.0.0.1:8080/cpu -make down -make test-rust-actix -``` - -The acceptance target requires Rustup, Python 3, Docker Compose v2, and Make. It runs formatting, locked Rust tests, Clippy with warnings denied, real DB and API checks, BIGINT boundaries, startup failure, SIGTERM shutdown, and container/network cleanup. Run API services sequentially because they share host port `8080`. - -## Rust / Axum implementation - -`apps/rust-axum/` adds Axum 0.8.9 and Tokio 1.53.1 without changing the Actix implementation. Rust 1.98.1, SQLx 0.9.0, Serde 1.0.228, serde_json 1.0.145, and the digest-pinned builder/runtime bases match that baseline. The committed lockfile fixes transitive dependencies. - -One direct server process uses an explicit Tokio `current_thread` executor. Native Serde responses, the shared parameterized SQL and pool maximum of 10, and per-request direct recursive Fibonacci(30) are preserved. There is no CPU offload, response cache, or additional async worker. SIGINT/SIGTERM drain accepted requests before closing the pool. The non-root release image inherits the shared 1 CPU / 512 MiB isolation envelope. - -```bash -make test-rust-axum # Rust gates and real PostgreSQL/container acceptance -make test-contract CONTRACT_IMPL=rust-axum # unchanged shared contract -make axum-diagnostic # clean committed source; never publishes results -``` - -Run these commands sequentially with port 8080 free. The Axum-only diagnostic uses the existing pinned load generator and external readiness, records provenance, versions and raw evidence under `.cache/axum-diagnostic/`, and fails on request errors or cleanup failure. Its short profile is not an official performance result. It is a required step in Axum's normal CI job; no temporary development workflow is needed. See [Axum implementation and diagnostic](docs/AXUM.md) for runtime details, test coverage, and output guarantees. - -## Node.js / Fastify implementation - -The Node implementation lives in `apps/node-fastify/` and uses Node.js 24.20.0 LTS, Fastify 5.12.3, and pg 8.23.0. Direct dependencies and `package-lock.json` are pinned; the official `node:24.20.0-bookworm-slim` image is also pinned by digest. The LTS runtime and stable Fastify 5 release line keep this baseline reproducible and maintainable. - -It starts one Node process directly in production mode, waits for a PostgreSQL readiness query, and uses a pool capped at 10 connections. The container runs as non-root user `node`, drops Linux capabilities, and uses the shared 1 CPU / 512 MB limits. Only `127.0.0.1:8080` is published. `/json` uses native objects and `/cpu` calculates Fibonacci(30) by direct recursion on every request. Shutdown closes the HTTP server and pool. - -Start only one API at a time because implementations share the local port: - -```bash -docker compose up --detach --build --wait node-fastify -curl http://127.0.0.1:8080/health -curl http://127.0.0.1:8080/json -curl http://127.0.0.1:8080/db/42 -curl http://127.0.0.1:8080/cpu -make down -make test-node-fastify -``` - -`make test-node-fastify` requires Node.js 24.20.0, npm, Python 3, Docker Compose v2, and Make. It runs focused tests, syntax validation, production image build, exact API responses against PostgreSQL, BIGINT boundaries, sanitized DB errors, resource/process checks, graceful shutdown, and container/network cleanup. - -## Python / FastAPI implementation - -The Python implementation lives in `apps/python-fastapi/` and uses Python 3.14.7, FastAPI 0.141.1, Uvicorn 0.52.4, and asyncpg 0.31.0. Runtime and development dependencies have exact, SHA256-verified lock files. Both Docker stages use the official `python:3.14.7-slim-bookworm` image pinned by index digest. - -Uvicorn runs directly with one worker, the standard asyncio event loop, and the h11 HTTP/1.1 implementation. Startup checks PostgreSQL before accepting HTTP requests; the asyncpg pool has at most 10 connections. Responses are serialized from ordinary Python values, including exact signed BIGINT IDs. Every `/cpu` request computes Fibonacci(30) by direct recursion, without caching or precomputation. Lifespan shutdown closes the pool. - -The production container runs as non-root user `10001:10001`, excludes tests and development dependencies, drops Linux capabilities, and uses 1 CPU / 512 MB. Compose waits for PostgreSQL health, publishes only `127.0.0.1:8080`, probes `/health`, and does not restart the service. - -```bash -docker compose up --detach --build --wait python-fastapi -curl http://127.0.0.1:8080/health -curl http://127.0.0.1:8080/json -curl http://127.0.0.1:8080/db/42 -curl http://127.0.0.1:8080/cpu -make down -make test-python-fastapi PYTHON=python3.14 -``` - -The complete acceptance target requires Python 3.14.7 on a POSIX host, Docker Compose v2, and Make. It installs the hash-locked development dependencies in a temporary virtual environment, runs Ruff and focused pytest tests, and verifies the real Docker service, DB errors and updates, resources, one worker, startup failure, SIGTERM shutdown, and container/network cleanup. See [Contributing](CONTRIBUTING.md) for focused tests without Docker. - -## Java / Spring Boot implementation - -`apps/java-spring-boot/` uses Temurin 25.0.4.1+1, Spring Boot 4.1.1, -Spring MVC/Tomcat, JDBC/HikariCP, and checksum-verified Gradle 9.7.1. -It follows the same API, single-process, 1 CPU / 512 MiB and ten-connection -limits. It is the **ninth registered implementation**, not an additional official -result. `eight-stack-v1` and all published measurements remain unchanged. - -```bash -make test-java-spring-boot -make test-contract CONTRACT_IMPL=java-spring-boot -``` - -The acceptance target needs the exact JDK, Python 3.10+, Make and Docker Compose -v2 on a POSIX host. The container-only contract target does not need a host JDK. -See the [Java implementation guide](docs/JAVA-SPRING-BOOT.md) for build integrity, -startup/shutdown behavior, supported build paths and the unmeasured boundary. - -All nine API implementations and the shared contract suite are available: - -```bash -make test-contract # all nine registered APIs, one at a time -make test-contract CONTRACT_IMPL=go-echo # one API with the same contract -``` - -The suite checks exact statuses, JSON content and types, documented errors, and -repeated responses, and cleans up its isolated environments even after failure. -See [the shared contract guide](CONTRIBUTING.md#shared-contract-checks) for -requirements, standalone base-URL checks, and cleanup limits. The local benchmark runner -is available, and pull requests run the same checks plus a non-publishing smoke benchmark. -Official benchmarks are [explicitly requested on trusted main](docs/AUTOMATION.md#when-to-request-an-official-benchmark) -after performance-relevant changes, or for a justified reproducibility investigation. -There is no weekly or automatic change-triggered measurement. Updating upstream -packages alone does not update this repository's pinned versions. Each dispatch -still includes the existing audited automatic publication and Pages deployment; -it is not a measurement-only operation. Publication redesign and main protection -remain separate decisions. -The active official cohort is `eight-stack-v1`, with its first verified publication -completed under Issue #50. Each subsequent result update still requires a new, -complete, verified official run; historical four-stack reports are not rewritten. - -Official publication calls the shared Pages deployment directly after a successful audited push. See [deployment authorization and recovery](docs/AUTOMATION.md#github-pages); presentation recovery does not require another benchmark. Product releases are -separate [maintainer-initiated operations](docs/RELEASING.md), never a Pages side effect. - -## Run the local benchmark - -With a clean committed source tree, Python 3.10+, curl and local Docker Compose v2: - -```bash -make benchmark -``` - -It verifies/installs checksum-pinned oha 1.16.0, builds and starts one API at a time, -runs the shared contract, performs the fixed warm-up and three measured runs per -endpoint, and atomically writes `results/latest.json` only after all results and -cleanup succeed. Existing results survive any partial failure. The generated file -is a local result, not an official publication. - -Use `make test-benchmark` for focused tests or `make benchmark-smoke` for a short -diagnostic that never replaces `latest.json`. See [the benchmark guide](docs/BENCHMARK.md) -for requirements, exact units, result schema, deadlines and memory-sampling limitations. - -## Documentation - -- [Results site](https://tappe9.github.io/simple-api-benchmark/) -- [Architecture](ARCHITECTURE.md) -- [Axum implementation and diagnostic](docs/AXUM.md) -- [API contract](docs/API-CONTRACT.md) -- [Benchmark methodology](docs/METHODOLOGY.md) -- [Running benchmarks and result format](docs/BENCHMARK.md) -- [Roadmap](ROADMAP.md) -- [Contributing](CONTRIBUTING.md) -- [Security](SECURITY.md) - -## Important limitation - -This project compares complete API stacks, not programming languages or frameworks in isolation. Results include the framework, runtime, HTTP server, JSON library, PostgreSQL driver, and container configuration. A result such as “Rust / Actix Web was fastest in this run” does not mean “Rust is always fastest,” and a future Gin-versus-Echo difference would not establish a universal ordering between those Go frameworks. +This starts Go / Gin and its PostgreSQL dependency with the fixed test fixture. +Only the API's loopback port is published. The last command **deletes all +`sab-example` containers, its network, orphan containers and volumes**. PostgreSQL +uses tmpfs, so its local data is lost when the DB container stops. Use that project +only for this disposable example; run the same cleanup after an interrupted session. +These requests are a local demonstration, not a benchmark result. + +## Go deeper + +- [Local setup, implementation details and cleanup](docs/LOCAL-DEVELOPMENT.md) +- [Shared contract checks](CONTRIBUTING.md#shared-contract-checks) and [contributing](CONTRIBUTING.md) +- [Local benchmarks and result format](docs/BENCHMARK.md) +- [Requesting an official benchmark](docs/AUTOMATION.md#when-to-request-an-official-benchmark): explicit requests only; a dispatch includes audited publication and Pages deployment +- [Pages authorization and recovery](docs/AUTOMATION.md#github-pages): presentation recovery does not require remeasurement +- [Architecture](ARCHITECTURE.md) · [Roadmap](ROADMAP.md) · [Security](SECURITY.md) ## License diff --git a/docs/IMPLEMENTATIONS.md b/docs/IMPLEMENTATIONS.md index 379b21f..e6d993b 100644 --- a/docs/IMPLEMENTATIONS.md +++ b/docs/IMPLEMENTATIONS.md @@ -69,20 +69,22 @@ success proves those checks passed, not official measured performance. See the ### Published results and runtime changes -The [verified report published on 2026-09-15](../results/history/2026-09-15T04-14-09Z-34925168324-1.json) -contains Axum and Flask, with Flask measured using Waitress 3.0.2. It is still the -[latest published result](../results/latest.json) at this documentation update. -The report's `metadata.source_commit`, runtime versions and conditions describe -that measurement, not whichever code is now on main. - +The [latest verified publication](../results/latest.json) is identified by the +[generated README results](../README.md#results) and [Japanese results](../README.ja.md#結果). +Use that report's `metadata.source_commit`, runtime versions and conditions, +not whichever code is now on main, to identify what was actually measured. + +The [historical report published on 2026-09-15](../results/history/2026-09-15T04-14-09Z-34925168324-1.json) +contains Axum and Flask, with Flask measured using Waitress 3.0.2. +That dated Waitress result is not a Gunicorn performance measurement. Current Flask uses Gunicorn with one `gthread` request worker and one request thread, plus its arbiter process; see the [Flask runtime guide](FLASK.md). -The recorded Waitress result is not a Gunicorn performance measurement. -The runtime change in PR #77 did not rerun the official benchmark. Never replace -recorded server/version metadata with current pins, relabel old measurements or -infer a performance change from CI. A new official result requires a separately -authorized complete run and validated publication. README and Pages continue to -display the selected report's measured configuration until then. +The runtime change in PR #77 did not itself rerun the official benchmark; later +publications record their own measured server and version. Never replace recorded +metadata with current pins, relabel old measurements or infer a performance change +from CI. A new official result requires a separately authorized complete run and +validated publication. README and Pages display the selected report's measured +configuration, while historical reports retain their original configuration. ### Java registration boundary (#67) diff --git a/docs/LOCAL-DEVELOPMENT.ja.md b/docs/LOCAL-DEVELOPMENT.ja.md new file mode 100644 index 0000000..1edfd25 --- /dev/null +++ b/docs/LOCAL-DEVELOPMENT.ja.md @@ -0,0 +1,222 @@ +# ローカル開発と実装の詳細 + +[English](LOCAL-DEVELOPMENT.md) · [READMEへ戻る](../README.ja.md) + +以下のコマンドは、すべてリポジトリのルートで実行します。 +コンテナだけで試す最短の例は、[READMEのクイックスタート](../README.ja.md#apiを1つ試す)を参照してください。 +手動起動の例には、BuildKitと`up --wait`に対応したDocker Compose v2、起動済みのDocker daemon、 +curlが必要です。`make`を使う例にはMakeも必要です。コンテナのビルドには、各言語のツールチェーンを +ホストへ入れる必要はありません。完全な受入テストにはPythonと各実装の固定ホストツールチェーンも +必要です。正確な前提条件は[Contributing](../CONTRIBUTING.md)を参照してください。 + +APIと受入テストはホストの`8080`ポートを共有するため、順番に実行してください。 +使い捨てのbenchmark用Composeプロジェクトだけを指定します。 +**`make down`は選択したプロジェクトのコンテナ、ネットワーク、孤立コンテナ、volumeを削除します。 +`make db-reset`は同じ削除を行ってからPostgreSQLを再作成します。どちらもtmpfs上のDB状態を失います。** +`COMPOSE_PROJECT_NAME`や`COMPOSE`を変更した場合は、起動とcleanupで同じ指定を使ってください。 +残す必要のあるデータやサービスのプロジェクトには実行しないでください。手動操作を中断した場合も、 +対象プロジェクトを確認してcleanupしてください。 + +登録、CI対象、有効な比較グループ、公開済み測定結果は別です。 +共通の状態説明は[登録・比較グループガイド](IMPLEMENTATIONS.md#registered-and-measured-implementations)にまとめています。 +実行方式と検証の専用ガイド: + +- [Rust / Axum](AXUM.md) +- [Node.js / Express](NODE-EXPRESS.md) +- [Python / Flask](FLASK.md) +- [Java / Spring Boot](JAVA-SPRING-BOOT.md) + +以下にbaseline実装と共通のローカル操作を記載します。 + +## ローカルPostgreSQL環境 + +Docker Compose v2とMakeが必要です。共通DBには、ダイジェストで固定した公式の`postgres:18.6-bookworm`イメージを使用します。Composeプロジェクト内の`benchmark`ネットワーク上で`postgres`サービスとして動作し、ホスト側のポートは公開しません。 + +| 設定 | 値 | +|---|---| +| Service / host | `postgres` | +| 内部ポート | `5432` | +| Database | `benchmark` | +| User | `benchmark` | +| Password | `benchmark` | + +これらは理解しやすさを優先したローカル専用の初期値であり、本番用の認証情報ではありません。APIサービスは、`docker-compose.yml`の共通設定`DATABASE_HOST`、`DATABASE_PORT`、`DATABASE_NAME`、`DATABASE_USER`、`DATABASE_PASSWORD`を使用します。 + +```bash +make db-up # PostgreSQLを起動し、healthy状態とfixtureを確認する +make db-check # 起動中DBのfixtureを確認する +make db-reset # 現在のDB状態を破棄し、fixtureを再作成する +make test-db # 起動・リセット・クリーンアップのacceptance checkを実行する +make down # コンテナとプロジェクトネットワークを削除する +``` + +PostgreSQLのデータは`tmpfs`上に置かれ、環境を再作成した際に引き継がれません。`database/init.sql`から、常に同じ`items`テーブルと`42 | Item 42 | 4200`の1行を作成します。 + +## Go / Gin実装 + +Go / Gin実装は`apps/go-gin/`にあり、現在はGo 1.27.1、Gin 1.12.0、pgx/v5 5.10.0を使用します。server processは1つで、PostgreSQLのpool上限は10接続です。Docker ComposeではAPIコンテナを1 CPU・512 MBに制限し、非rootユーザー`65532:65532`で実行します。ポート`8080`はloopback interfaceだけに公開します。 + +```bash +docker compose up --detach --build --wait go-gin +curl http://127.0.0.1:8080/health +curl http://127.0.0.1:8080/json +curl http://127.0.0.1:8080/db/42 +curl http://127.0.0.1:8080/cpu +make down +``` + +Goのformat、unit test、vet、コンテナ起動、API仕様、リソース制限、クリーンアップをまとめて確認するには、次を実行します。 + +```bash +make test-go-gin +``` + +## Go / Echo実装 + +Go / Echo実装は`apps/go-echo/`にあり、Go 1.27.1、Echo v5.3.1、pgx/v5 5.10.0を固定しています。Gin baselineとGo runtime・PostgreSQL driverを揃えたまま比較できるようにし、Gin側を暗黙にupgradeしていません。同じSQLとfixtureを使い、HTTP受付前にPostgreSQLへ接続し、pool上限は10接続です。 + +1つの`net/http` server processでEcho routerを提供します。比較と無関係なmiddleware、response cache、CPU処理の事前計算は追加せず、`/cpu`はrequestごとに直接再帰でFibonacci(30)を計算します。production imageにはstatic binaryだけを配置し、非rootの`65532:65532`で実行します。Composeから共通の1 CPU・512 MB、capability drop、no-new-privileges、loopback限定port公開、restart policyを継承します。 + +```bash +docker compose up --detach --build --wait go-echo +curl http://127.0.0.1:8080/health +curl http://127.0.0.1:8080/json +curl http://127.0.0.1:8080/db/42 +curl http://127.0.0.1:8080/cpu +make down +make test-go-echo +``` + +`make test-go-echo`ではformat、unit tests、vet、production image、実PostgreSQL/API acceptance、BIGINT境界、起動失敗、SIGTERMでのgraceful shutdown、resource/process isolation、cleanupを確認します。共通contract suiteも変更せずEchoへ適用します。将来のGin対Echoの結果も、このリポジトリで固定した条件下のframework/routerを含むAPIスタック全体の比較であり、Goフレームワークの普遍的な順位を示すものではありません。 + +## Rust / Actix Web実装 + +Rust実装は`apps/rust-actix/`にあり、Rust 1.98.1、Actix Web 4.15.0、SQLx 0.9.0、Serde 1.0.228、serde_json 1.0.145を固定しています。推移的な依存も`Cargo.lock`で固定します。1つのActix workerでポート`8080`を受け付け、通常のSerde値からJSONを生成します。`/cpu`では毎request直接再帰でFibonacci(30)を計算します。SQLx poolはHTTP受付前にPostgreSQLへ接続し、上限は10接続です。 + +Dockerでは公開済みの`rust:1.98.0-bookworm`をdigest固定し、誤コンパイル修正を含むcompiler 1.98.1を明示導入して、`cargo +1.98.1 build --release --locked`でビルドします。実行用のDebian Bookworm slimもdigest固定し、release binaryのみをコピーして`65532:65532`で実行します。Cargoやソースコードは含めません。ComposeはPostgreSQLのhealthy状態を待ち、capabilities削除と権限昇格禁止、1 CPU・512 MB、loopback限定公開、restartなしを適用します。 + +```bash +docker compose up --detach --build --wait rust-actix +curl http://127.0.0.1:8080/health +curl http://127.0.0.1:8080/json +curl http://127.0.0.1:8080/db/42 +curl http://127.0.0.1:8080/cpu +make down +make test-rust-actix +``` + +acceptance targetにはRustup、Python 3、Docker Compose v2、Makeが必要です。format、locked Rust tests、警告をエラーにするClippy、実DB・API、BIGINT境界、起動失敗、SIGTERM終了、container・network削除を検証します。各APIは同じホストポート`8080`を使うため、1つずつ起動してください。 + +## Rust / Axum実装 + +`apps/rust-axum/`はAxum 0.8.9とTokio 1.53.1を使います。既存のActix実装は変更せず、Rust 1.98.1、SQLx 0.9.0、Serde 1.0.228、serde_json 1.0.145、digest固定のbuilder・runtimeイメージを揃えています。推移的依存はcommitした`Cargo.lock`で固定します。 + +1つのserver processが明示的なTokio `current_thread` executorを使います。native Serde応答、共通のparameterized SQL、最大10接続のpool、requestごとの直接再帰によるFibonacci(30)を維持し、CPU offload、response cache、追加async workerは使いません。SIGINT/SIGTERMでは処理中のrequest完了を待ってからpoolを閉じます。非rootのrelease imageは共通の1 CPU・512 MiB制限を継承します。 + +```bash +make test-rust-axum # Rust検証と実PostgreSQL・container acceptance +make test-contract CONTRACT_IMPL=rust-axum # 変更しない共通contract +make axum-diagnostic # commit済みの作業ツリーで実行。結果は公開しない +``` + +port 8080を空け、各commandを順番に実行してください。Axum専用diagnosticは既存の固定済み負荷ツールとexternal readinessを使い、source・version情報と生データを`.cache/axum-diagnostic/`へ保存します。request errorやcleanup失敗を成功扱いせず、短縮profileの数値を公式結果として公開しません。通常CIのAxum jobで必須実行するため、一時的な開発workflowは不要です。実行方式、検証範囲、保存先の制約は[Axum実装・diagnosticガイド](AXUM.md)を参照してください。 + +## Node.js / Fastify実装 + +Node実装は`apps/node-fastify/`にあり、Node.js 24.20.0 LTS、Fastify 5.12.3、pg 8.23.0を使用します。直接依存と`package-lock.json`を固定し、公式の`node:24.20.0-bookworm-slim`イメージもdigestで固定します。再現性と保守性のため、LTSランタイムと安定版のFastify 5系を採用しています。 + +production modeでNode processを直接1つ起動し、PostgreSQLへの確認queryが成功してからHTTP接続を受け付けます。pool上限は10接続です。コンテナは非rootユーザー`node`で実行し、Linux capabilitiesを削除し、共通の1 CPU・512 MB制限を適用します。公開先は`127.0.0.1:8080`だけです。`/json`は通常のobjectをシリアライズし、`/cpu`は毎request直接再帰でFibonacci(30)を計算します。終了時はHTTP serverとpoolを閉じます。 + +各APIは同じローカルポートを使うため、1つずつ起動してください。 + +```bash +docker compose up --detach --build --wait node-fastify +curl http://127.0.0.1:8080/health +curl http://127.0.0.1:8080/json +curl http://127.0.0.1:8080/db/42 +curl http://127.0.0.1:8080/cpu +make down +make test-node-fastify +``` + +`make test-node-fastify`にはNode.js 24.20.0、npm、Python 3、Docker Compose v2、Makeが必要です。focused tests、構文検証、image・API確認(実DBの更新・異常系を含む)、resource・process確認、正常終了、container・networkの削除を実行します。 + +## Python / FastAPI実装 + +Python実装は`apps/python-fastapi/`にあり、Python 3.14.7、FastAPI 0.141.1、Uvicorn 0.52.4、asyncpg 0.31.0を使用します。実行時依存と開発用依存は、バージョンとSHA256 hashを固定したlockファイルで管理します。Dockerの両stageは、index digestで固定した公式の`python:3.14.7-slim-bookworm`イメージを使用します。 + +Uvicornを直接1 workerで起動し、標準のasyncioイベントループとHTTP/1.1実装のh11を使用します。PostgreSQL接続確認後にHTTP受付を開始し、asyncpg poolの上限は10接続です。通常のPython値からJSONを生成し、signed BIGINTのIDも数値として正確に返します。`/cpu`は毎request直接再帰でFibonacci(30)を計算し、cacheや事前計算は使用しません。終了時はlifespanでpoolを閉じます。 + +productionコンテナは非rootユーザー`10001:10001`で実行し、testsと開発用依存を含めません。Linux capabilitiesを削除し、1 CPU・512 MBに制限します。ComposeはPostgreSQLのhealthy状態を待ち、`127.0.0.1:8080`だけに公開し、`/health`を確認します。自動restartは行いません。 + +```bash +docker compose up --detach --build --wait python-fastapi +curl http://127.0.0.1:8080/health +curl http://127.0.0.1:8080/json +curl http://127.0.0.1:8080/db/42 +curl http://127.0.0.1:8080/cpu +make down +make test-python-fastapi PYTHON=python3.14 +``` + +acceptance targetにはPOSIX環境のPython 3.14.7、Docker Compose v2、Makeが必要です。一時virtual environmentへhash検証付きで開発用依存をinstallし、Ruff、focused pytest tests、実Dockerサービス、DB更新・異常系、資源制限、1 worker、起動失敗、SIGTERM終了、container・network削除を確認します。Dockerを使わないfocused testsは[Contributing](../CONTRIBUTING.md)を参照してください。 + +## Java / Spring Boot実装 + +`apps/java-spring-boot/`はTemurin 25.0.4.1+1、Spring Boot 4.1.1、 +Spring MVC / Tomcat、JDBC / HikariCP、チェックサムを検証するGradle 9.7.1を使用します。 +共通のAPI、単一プロセス、1 CPU・512 MiB、DB接続上限10を維持します。 +登録済みでCI対象ですが、Javaの公式測定結果はありません。 +[Javaの登録と公式測定の区別](IMPLEMENTATIONS.md#java-registration-boundary-67)を参照してください。 + +```bash +make test-java-spring-boot +make test-contract CONTRACT_IMPL=java-spring-boot +``` + +受入テストには、POSIXホスト上の固定JDK、Python 3.10以降、Make、Docker Compose v2が必要です。 +コンテナのみを使う共通契約テストには、ホストのJDKは不要です。 +固定依存関係、起動・終了の検証、公式測定と分ける理由は +[Java実装ガイド](JAVA-SPRING-BOOT.md)を参照してください。 + +## 共通契約と公式運用 + +登録済みのすべての実装を、共通のAPI契約で検証できます。 + +```bash +make test-contract # 登録済みの実装を1つずつ順番に検証 +make test-contract CONTRACT_IMPL=go-echo # 1実装を同じ契約で検証 +``` + +HTTP status、JSONの内容・型、規定のerror response、応答の再現性を確認し、 +失敗時にも分離された検証環境を片付けます。必要な環境、起動済みAPIのbase URLを指定する +方法、cleanupの制約は[共通contractの実行ガイド](../CONTRIBUTING.md#shared-contract-checks)を +参照してください。ローカルのbenchmark runnerは利用可能です。PRでは同じ検証と公開しない短縮benchmarkを実行します。 +公式benchmarkは、性能に関わる変更を取り込んだ後、または再現性などの調査が必要な場合に、 +[trusted mainで明示的に実行を依頼します](AUTOMATION.md#when-to-request-an-official-benchmark)。 +週次実行や変更を検知しての自動計測は行いません。上流で新しいversionが公開されただけでは、 +このリポジトリの固定versionは更新されません。手動起動後も、既存の検証済み結果の自動公開と +Pages反映まで実行するため、計測だけの操作ではありません。公開方式の見直しとmain保護は別途検討します。 +現在の比較対象と初回公開の経緯は、[登録・比較グループガイド](IMPLEMENTATIONS.md#registered-and-measured-implementations)を参照してください。 +結果更新には新しい完全な公式計測と検証が必要です。過去のレポートは書き換えません。 + +公式結果の監査・push成功後に、共通Pages workflowを直接呼び出します。[認可と復旧手順](AUTOMATION.md#github-pages)を参照してください。表示の復旧だけのために再計測する必要はありません。製品リリースは +[maintainerが明示的に実施する別の操作](RELEASING.md)であり、Pagesの副作用として作成しません。 + +## ローカルでの計測 + +変更をcommitした作業ツリー、Python 3.10以上、curl、ローカルのDocker Compose v2で実行します。 + +```bash +make benchmark +``` + +SHA256とversionを固定したoha 1.16.0を検証・導入し、APIを1つずつビルド・起動します。 +共通contractの成功後、各endpointを規定条件でwarm-upし、3回ずつ計測します。 +全計測とcleanupが成功した場合だけ、`results/latest.json`をatomicに保存します。 +途中失敗では既存結果を保持します。生成ファイルはローカル結果であり、公式公開結果ではありません。 + +focused testsは`make test-benchmark`、短縮診断は`make benchmark-smoke`です。 +短縮診断は`latest.json`を更新しません。必要環境、単位、結果形式、期限、メモリサンプリングの制約は +[実行・結果形式ガイド](BENCHMARK.md)を参照してください。 + diff --git a/docs/LOCAL-DEVELOPMENT.md b/docs/LOCAL-DEVELOPMENT.md new file mode 100644 index 0000000..afd6dac --- /dev/null +++ b/docs/LOCAL-DEVELOPMENT.md @@ -0,0 +1,228 @@ +# Local development and implementation details + +[日本語](LOCAL-DEVELOPMENT.ja.md) · [Back to README](../README.md) + +Run every command below from the repository root. For the shortest container-only +example, use the [README quick start](../README.md#try-one-api-locally). +Docker Compose v2 with BuildKit and `up --wait` support, a running Docker daemon, +and curl are needed for manual container examples; examples using `make` also +need Make. Container builds do not need each language's toolchain on the host. +Full acceptance targets additionally need Python and the pinned host toolchain +for that implementation; see [Contributing](../CONTRIBUTING.md) for exact prerequisites. + +Run services and acceptance targets sequentially: they share host port `8080`. +Use only a disposable benchmark Compose project. **`make down` removes the +selected project's containers, network, orphan containers and volumes; +`make db-reset` does that before recreating PostgreSQL. Both discard its tmpfs +DB state.** If you set `COMPOSE_PROJECT_NAME` or override `COMPOSE`, keep the same +selection for startup and cleanup. Never point these commands at a project whose +data or services you need to keep. Retry scoped cleanup after interrupted manual work. + +Registration, CI coverage, active cohort membership and published measurements +are separate; the [registry guide](IMPLEMENTATIONS.md#registered-and-measured-implementations) +is the shared status reference. Dedicated runtime and verification guides: + +- [Rust / Axum](AXUM.md) +- [Node.js / Express](NODE-EXPRESS.md) +- [Python / Flask](FLASK.md) +- [Java / Spring Boot](JAVA-SPRING-BOOT.md) + +The baseline implementations and common local operations are documented below. + +## Local PostgreSQL environment + +Docker Compose v2 and Make are required. The shared database uses the official `postgres:18.6-bookworm` image pinned by digest. It runs as the `postgres` service on the project-scoped `benchmark` network and does not publish a host port. + +| Setting | Value | +|---|---| +| Service / host | `postgres` | +| Internal port | `5432` | +| Database | `benchmark` | +| User | `benchmark` | +| Password | `benchmark` | + +These are intentionally simple local-only defaults, not production credentials. API services use the common `DATABASE_HOST`, `DATABASE_PORT`, `DATABASE_NAME`, `DATABASE_USER`, and `DATABASE_PASSWORD` settings from `docker-compose.yml`. + +```bash +make db-up # start PostgreSQL, wait for health, and validate the fixture +make db-check # validate the exact fixture in the running database +make db-reset # discard all current DB state and recreate the fixture +make test-db # run the complete startup, reset, and cleanup acceptance check +make down # remove containers and the project network +``` + +PostgreSQL data lives on `tmpfs`. It is never reused across a recreated environment, and `database/init.sql` always creates the same `items` table and row `42 | Item 42 | 4200`. + +## Go / Gin implementation + +The Go / Gin implementation lives in `apps/go-gin/` and currently uses Go 1.27.1, Gin 1.12.0, and pgx/v5 5.10.0. It runs one server process with a PostgreSQL pool capped at 10 connections. Docker Compose limits the API container to 1 CPU and 512 MB, runs it as non-root user `65532:65532`, and publishes port `8080` only on the loopback interface. + +```bash +docker compose up --detach --build --wait go-gin +curl http://127.0.0.1:8080/health +curl http://127.0.0.1:8080/json +curl http://127.0.0.1:8080/db/42 +curl http://127.0.0.1:8080/cpu +make down +``` + +Run the complete Go formatting, unit-test, vet, container, API-contract, resource-limit, and cleanup checks with: + +```bash +make test-go-gin +``` + +## Go / Echo implementation + +The Go / Echo implementation lives in `apps/go-echo/` and pins Go 1.27.1, Echo v5.3.1, and pgx/v5 5.10.0 so it stays directly comparable with the Gin baseline without silently upgrading the Go runtime or PostgreSQL driver. It uses the same SQL and fixture, connects before serving HTTP, and caps the PostgreSQL pool at 10 connections. + +One `net/http` server process serves the Echo router. The implementation adds no unrelated middleware, response cache, or CPU precomputation; `/cpu` performs direct recursive Fibonacci(30) for every request. The production image contains only the statically built binary, runs as non-root `65532:65532`, and inherits the shared 1 CPU / 512 MB isolation envelope, capability drop, no-new-privileges policy, loopback-only port publication, and restart policy from Compose. + +```bash +docker compose up --detach --build --wait go-echo +curl http://127.0.0.1:8080/health +curl http://127.0.0.1:8080/json +curl http://127.0.0.1:8080/db/42 +curl http://127.0.0.1:8080/cpu +make down +make test-go-echo +``` + +`make test-go-echo` runs formatting, unit tests, vet, production image and real PostgreSQL/API acceptance checks, BIGINT boundaries, startup failure, graceful SIGTERM shutdown, resource/process isolation, and cleanup. The shared contract suite runs against Echo unchanged. A future Gin-versus-Echo result is still a complete-stack comparison of framework/router behavior under this repository's fixed conditions, not a universal ranking of Go frameworks. + +## Rust / Actix Web implementation + +The Rust implementation lives in `apps/rust-actix/` and pins Rust 1.98.1, Actix Web 4.15.0, SQLx 0.9.0, Serde 1.0.228, and serde_json 1.0.145. `Cargo.lock` fixes the transitive dependency graph. One Actix worker serves port `8080`, uses native Serde response values, and performs direct recursive Fibonacci(30) for every CPU request. The SQLx pool connects before HTTP startup and allows at most 10 PostgreSQL connections. + +The production Dockerfile uses the published `rust:1.98.0-bookworm` builder pinned by digest and explicitly installs compiler 1.98.1, which fixes a compiler miscompilation. It builds with `cargo +1.98.1 build --release --locked`. The digest-pinned Debian Bookworm slim runtime contains the release binary, not Cargo or the source tree, and runs as `65532:65532`. Compose waits for healthy PostgreSQL, drops capabilities, disallows privilege escalation, applies 1 CPU / 512 MB limits, publishes only `127.0.0.1:8080`, and disables restarts. + +```bash +docker compose up --detach --build --wait rust-actix +curl http://127.0.0.1:8080/health +curl http://127.0.0.1:8080/json +curl http://127.0.0.1:8080/db/42 +curl http://127.0.0.1:8080/cpu +make down +make test-rust-actix +``` + +The acceptance target requires Rustup, Python 3, Docker Compose v2, and Make. It runs formatting, locked Rust tests, Clippy with warnings denied, real DB and API checks, BIGINT boundaries, startup failure, SIGTERM shutdown, and container/network cleanup. Run API services sequentially because they share host port `8080`. + +## Rust / Axum implementation + +`apps/rust-axum/` adds Axum 0.8.9 and Tokio 1.53.1 without changing the Actix implementation. Rust 1.98.1, SQLx 0.9.0, Serde 1.0.228, serde_json 1.0.145, and the digest-pinned builder/runtime bases match that baseline. The committed lockfile fixes transitive dependencies. + +One direct server process uses an explicit Tokio `current_thread` executor. Native Serde responses, the shared parameterized SQL and pool maximum of 10, and per-request direct recursive Fibonacci(30) are preserved. There is no CPU offload, response cache, or additional async worker. SIGINT/SIGTERM drain accepted requests before closing the pool. The non-root release image inherits the shared 1 CPU / 512 MiB isolation envelope. + +```bash +make test-rust-axum # Rust gates and real PostgreSQL/container acceptance +make test-contract CONTRACT_IMPL=rust-axum # unchanged shared contract +make axum-diagnostic # clean committed source; never publishes results +``` + +Run these commands sequentially with port 8080 free. The Axum-only diagnostic uses the existing pinned load generator and external readiness, records provenance, versions and raw evidence under `.cache/axum-diagnostic/`, and fails on request errors or cleanup failure. Its short profile is not an official performance result. It is a required step in Axum's normal CI job; no temporary development workflow is needed. See [Axum implementation and diagnostic](AXUM.md) for runtime details, test coverage, and output guarantees. + +## Node.js / Fastify implementation + +The Node implementation lives in `apps/node-fastify/` and uses Node.js 24.20.0 LTS, Fastify 5.12.3, and pg 8.23.0. Direct dependencies and `package-lock.json` are pinned; the official `node:24.20.0-bookworm-slim` image is also pinned by digest. The LTS runtime and stable Fastify 5 release line keep this baseline reproducible and maintainable. + +It starts one Node process directly in production mode, waits for a PostgreSQL readiness query, and uses a pool capped at 10 connections. The container runs as non-root user `node`, drops Linux capabilities, and uses the shared 1 CPU / 512 MB limits. Only `127.0.0.1:8080` is published. `/json` uses native objects and `/cpu` calculates Fibonacci(30) by direct recursion on every request. Shutdown closes the HTTP server and pool. + +Start only one API at a time because implementations share the local port: + +```bash +docker compose up --detach --build --wait node-fastify +curl http://127.0.0.1:8080/health +curl http://127.0.0.1:8080/json +curl http://127.0.0.1:8080/db/42 +curl http://127.0.0.1:8080/cpu +make down +make test-node-fastify +``` + +`make test-node-fastify` requires Node.js 24.20.0, npm, Python 3, Docker Compose v2, and Make. It runs focused tests, syntax validation, production image build, exact API responses against PostgreSQL, BIGINT boundaries, sanitized DB errors, resource/process checks, graceful shutdown, and container/network cleanup. + +## Python / FastAPI implementation + +The Python implementation lives in `apps/python-fastapi/` and uses Python 3.14.7, FastAPI 0.141.1, Uvicorn 0.52.4, and asyncpg 0.31.0. Runtime and development dependencies have exact, SHA256-verified lock files. Both Docker stages use the official `python:3.14.7-slim-bookworm` image pinned by index digest. + +Uvicorn runs directly with one worker, the standard asyncio event loop, and the h11 HTTP/1.1 implementation. Startup checks PostgreSQL before accepting HTTP requests; the asyncpg pool has at most 10 connections. Responses are serialized from ordinary Python values, including exact signed BIGINT IDs. Every `/cpu` request computes Fibonacci(30) by direct recursion, without caching or precomputation. Lifespan shutdown closes the pool. + +The production container runs as non-root user `10001:10001`, excludes tests and development dependencies, drops Linux capabilities, and uses 1 CPU / 512 MB. Compose waits for PostgreSQL health, publishes only `127.0.0.1:8080`, probes `/health`, and does not restart the service. + +```bash +docker compose up --detach --build --wait python-fastapi +curl http://127.0.0.1:8080/health +curl http://127.0.0.1:8080/json +curl http://127.0.0.1:8080/db/42 +curl http://127.0.0.1:8080/cpu +make down +make test-python-fastapi PYTHON=python3.14 +``` + +The complete acceptance target requires Python 3.14.7 on a POSIX host, Docker Compose v2, and Make. It installs the hash-locked development dependencies in a temporary virtual environment, runs Ruff and focused pytest tests, and verifies the real Docker service, DB errors and updates, resources, one worker, startup failure, SIGTERM shutdown, and container/network cleanup. See [Contributing](../CONTRIBUTING.md) for focused tests without Docker. + +## Java / Spring Boot implementation + +`apps/java-spring-boot/` uses Temurin 25.0.4.1+1, Spring Boot 4.1.1, +Spring MVC/Tomcat, JDBC/HikariCP, and checksum-verified Gradle 9.7.1. +It follows the same API, single-process, 1 CPU / 512 MiB and ten-connection +limits. It is registered and CI-covered, with no official Java performance result. +See the [Java registration boundary](IMPLEMENTATIONS.md#java-registration-boundary-67). + +```bash +make test-java-spring-boot +make test-contract CONTRACT_IMPL=java-spring-boot +``` + +The acceptance target needs the exact JDK, Python 3.10+, Make and Docker Compose +v2 on a POSIX host. The container-only contract target does not need a host JDK. +See the [Java implementation guide](JAVA-SPRING-BOOT.md) for build integrity, +startup/shutdown behavior, supported build paths and the unmeasured boundary. + +## Shared contract and official operations + +All registered API implementations are covered by the shared contract suite: + +```bash +make test-contract # all registered APIs, one at a time +make test-contract CONTRACT_IMPL=go-echo # one API with the same contract +``` + +The suite checks exact statuses, JSON content and types, documented errors, and +repeated responses, and cleans up its isolated environments even after failure. +See [the shared contract guide](../CONTRIBUTING.md#shared-contract-checks) for +requirements, standalone base-URL checks, and cleanup limits. The local benchmark runner +is available, and pull requests run the same checks plus a non-publishing smoke benchmark. +Official benchmarks are [explicitly requested on trusted main](AUTOMATION.md#when-to-request-an-official-benchmark) +after performance-relevant changes, or for a justified reproducibility investigation. +There is no weekly or automatic change-triggered measurement. Updating upstream +packages alone does not update this repository's pinned versions. Each dispatch +still includes the existing audited automatic publication and Pages deployment; +it is not a measurement-only operation. Publication redesign and main protection +remain separate decisions. +See the [registry and cohort guide](IMPLEMENTATIONS.md#registered-and-measured-implementations) +for current membership and the original rollout history. Each result update +requires a new, complete, verified official run; historical reports are not rewritten. + +Official publication calls the shared Pages deployment directly after a successful audited push. See [deployment authorization and recovery](AUTOMATION.md#github-pages); presentation recovery does not require another benchmark. Product releases are +separate [maintainer-initiated operations](RELEASING.md), never a Pages side effect. + +## Run the local benchmark + +With a clean committed source tree, Python 3.10+, curl and local Docker Compose v2: + +```bash +make benchmark +``` + +It verifies/installs checksum-pinned oha 1.16.0, builds and starts one API at a time, +runs the shared contract, performs the fixed warm-up and three measured runs per +endpoint, and atomically writes `results/latest.json` only after all results and +cleanup succeed. Existing results survive any partial failure. The generated file +is a local result, not an official publication. + +Use `make test-benchmark` for focused tests or `make benchmark-smoke` for a short +diagnostic that never replaces `latest.json`. See [the benchmark guide](BENCHMARK.md) +for requirements, exact units, result schema, deadlines and memory-sampling limitations. + diff --git a/tests/test_benchmark_implementation_docs.py b/tests/test_benchmark_implementation_docs.py index 4ea6e1d..c83b4af 100644 --- a/tests/test_benchmark_implementation_docs.py +++ b/tests/test_benchmark_implementation_docs.py @@ -95,6 +95,14 @@ def test_flask_runtime_change_is_distinct_from_dated_measurement(self): flask, "IMPLEMENTATIONS.md#published-results-and-runtime-changes" ) + def test_publication_guidance_does_not_freeze_a_historical_report_as_latest(self): + boundary = section(self.canonical, "Published results and runtime changes") + self.assertIn("latest verified publication", boundary) + self.assertIn("../results/latest.json", boundary) + self.assertIn("historical", boundary) + self.assertNotIn("It is still the", boundary) + self.assertNotIn("until then", boundary) + def test_java_boundary_does_not_claim_official_results(self): java = " ".join(section(self.canonical, "Java registration boundary (#67)").split()) self.assertIn("`java-spring-boot`", java) diff --git a/tests/test_benchmark_readme_navigation.py b/tests/test_benchmark_readme_navigation.py new file mode 100644 index 0000000..dda0ce9 --- /dev/null +++ b/tests/test_benchmark_readme_navigation.py @@ -0,0 +1,143 @@ +"""Keep the bilingual entry points small, safe, and connected to public guides.""" + +import re +import unittest +from pathlib import Path +from urllib.parse import unquote, urlsplit + +ROOT = Path(__file__).resolve().parents[1] +START = "" +END = "" + + +def authored_text(text): + """Ignore the separately tested generated report, including its table/charts.""" + before, result = text.split(START) + _, after = result.split(END) + return before + after + + +def links(text): + return re.findall(r"\[[^\]]*\]\(([^)]+)\)", text) + + +def anchors(text): + """The simple ATX headings used by these guides use GitHub-style anchors.""" + return { + re.sub(r"[^\w\- ]", "", heading.lower()).replace(" ", "-") + for heading in re.findall(r"(?m)^#{1,6} (.+)$", text) + } + + +class ReadmeNavigationTests(unittest.TestCase): + def test_both_languages_link_to_same_essential_public_guides(self): + shared = { + "docs/IMPLEMENTATIONS.md#registered-and-measured-implementations", + "docs/API-CONTRACT.md", + "docs/METHODOLOGY.md", + "docs/BENCHMARK.md", + "docs/AUTOMATION.md#when-to-request-an-official-benchmark", + "docs/RELEASING.md", + "CONTRIBUTING.md#shared-contract-checks", + } + for filename, guide in ( + ("README.md", "docs/LOCAL-DEVELOPMENT.md"), + ("README.ja.md", "docs/LOCAL-DEVELOPMENT.ja.md"), + ): + with self.subTest(filename=filename): + text = authored_text((ROOT / filename).read_text()) + self.assertTrue(shared.issubset(links(text)), shared - set(links(text))) + self.assertIn(guide, links(text)) + + def test_quick_start_commands_match_and_need_no_host_language_toolchain(self): + commands = [] + for filename in ("README.md", "README.ja.md"): + text = authored_text((ROOT / filename).read_text()) + blocks = re.findall(r"```bash\n(.*?)```", text, re.DOTALL) + self.assertEqual(len(blocks), 1, filename) + commands.append([line for line in blocks[0].splitlines() if line.strip()]) + self.assertEqual(commands[0], commands[1]) + self.assertEqual( + commands[0][0], + "docker compose --project-name sab-example up --detach --build --wait go-gin", + ) + self.assertEqual( + commands[0][-1], + "docker compose --project-name sab-example down --remove-orphans --volumes", + ) + self.assertEqual( + commands[0][1:-1], + [ + f"curl --fail http://127.0.0.1:8080/{endpoint}" + for endpoint in ("health", "json", "db/42", "cpu") + ], + ) + + def test_quick_start_explains_prerequisites_and_destructive_cleanup(self): + for filename, words in ( + ("README.md", ("Docker", "Compose v2", "curl", "8080", "tmpfs", "deletes", "volumes")), + ("README.ja.md", ("Docker", "Compose v2", "curl", "8080", "tmpfs", "削除", "volume")), + ): + with self.subTest(filename=filename): + text = authored_text((ROOT / filename).read_text()) + for word in words: + self.assertIn(word, text) + + def test_moved_guides_preserve_setup_and_verification_entry_points(self): + for filename in ("docs/LOCAL-DEVELOPMENT.md", "docs/LOCAL-DEVELOPMENT.ja.md"): + with self.subTest(filename=filename): + path = ROOT / filename + self.assertTrue(path.is_file(), filename) + text = path.read_text() + for command in ( + "make db-reset", + "make down", + "make test-go-gin", + "make test-go-echo", + "make test-rust-actix", + "make test-rust-axum", + "make test-node-fastify", + "make test-python-fastapi PYTHON=python3.14", + "make test-java-spring-boot", + "make test-contract", + "make benchmark", + ): + self.assertIn(command, text) + for guide in ("AXUM.md", "FLASK.md", "NODE-EXPRESS.md", "JAVA-SPRING-BOOT.md"): + self.assertIn(guide, links(text)) + self.assertIn("../CONTRIBUTING.md#shared-contract-checks", links(text)) + for invariant in ( + "tmpfs", + "current_thread", + "Cargo.lock", + "DATABASE_PASSWORD", + "127.0.0.1:8080", + "SIGTERM", + "SHA256", + ): + self.assertIn(invariant, text) + + def test_readme_and_moved_guide_local_links_and_anchors_resolve(self): + for filename in ( + "README.md", + "README.ja.md", + "docs/LOCAL-DEVELOPMENT.md", + "docs/LOCAL-DEVELOPMENT.ja.md", + ): + path = ROOT / filename + self.assertTrue(path.is_file(), filename) + for target in links(path.read_text()): + with self.subTest(filename=filename, target=target): + url = urlsplit(target) + if url.scheme or url.netloc: + continue + destination = (path.parent / unquote(url.path)).resolve() if url.path else path + self.assertTrue(destination.exists(), target) + if url.fragment and destination.suffix == ".md": + self.assertIn( + unquote(url.fragment), anchors(destination.read_text()), target + ) + + +if __name__ == "__main__": + unittest.main() From 168ed1692b8f341bc13fb65529c0c375e23908a3 Mon Sep 17 00:00:00 2001 From: Yuma Takino <36617106+tappe9@users.noreply.github.com> Date: Thu, 1 Oct 2026 10:11:32 -0700 Subject: [PATCH 2/2] docs: remove extra trailing blank lines from moved guides --- docs/LOCAL-DEVELOPMENT.ja.md | 1 - docs/LOCAL-DEVELOPMENT.md | 1 - 2 files changed, 2 deletions(-) diff --git a/docs/LOCAL-DEVELOPMENT.ja.md b/docs/LOCAL-DEVELOPMENT.ja.md index 1edfd25..619f401 100644 --- a/docs/LOCAL-DEVELOPMENT.ja.md +++ b/docs/LOCAL-DEVELOPMENT.ja.md @@ -219,4 +219,3 @@ SHA256とversionを固定したoha 1.16.0を検証・導入し、APIを1つず focused testsは`make test-benchmark`、短縮診断は`make benchmark-smoke`です。 短縮診断は`latest.json`を更新しません。必要環境、単位、結果形式、期限、メモリサンプリングの制約は [実行・結果形式ガイド](BENCHMARK.md)を参照してください。 - diff --git a/docs/LOCAL-DEVELOPMENT.md b/docs/LOCAL-DEVELOPMENT.md index afd6dac..b2610f1 100644 --- a/docs/LOCAL-DEVELOPMENT.md +++ b/docs/LOCAL-DEVELOPMENT.md @@ -225,4 +225,3 @@ is a local result, not an official publication. Use `make test-benchmark` for focused tests or `make benchmark-smoke` for a short diagnostic that never replaces `latest.json`. See [the benchmark guide](BENCHMARK.md) for requirements, exact units, result schema, deadlines and memory-sampling limitations. -