From fecb50f22101f3bb12826d238f8f11ac424a6474 Mon Sep 17 00:00:00 2001 From: Vittoria Date: Sun, 3 May 2026 14:18:28 +0200 Subject: [PATCH] add benchmark script for jsonable_encoder lazy allocation fix --- scripts/bench_jsonable_encoder.py | 69 +++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 scripts/bench_jsonable_encoder.py diff --git a/scripts/bench_jsonable_encoder.py b/scripts/bench_jsonable_encoder.py new file mode 100644 index 0000000000..f4eadc931d --- /dev/null +++ b/scripts/bench_jsonable_encoder.py @@ -0,0 +1,69 @@ +""" +Benchmark: lazy allowed_keys allocation in jsonable_encoder. + +Usage: + uv run python scripts/bench_jsonable_encoder.py + +Run against both branches to compare: + git stash # unpatched + uv run python scripts/bench_jsonable_encoder.py + git stash pop # patched + uv run python scripts/bench_jsonable_encoder.py + +Reference results (20 rounds x 300 iters, mean, Python 3.12, FastAPI 0.136.1): + + Payload mean µs/call stdev + ------------------------------------------------------------ + small dict (3 keys) before: 5.37µs 0.95 + after: 4.93µs 0.65 -8.2% + large dict (300 items, nested) + before: 12,158.80µs 557.40 + after: 11,431.07µs 506.29 -6.0% +""" + +import statistics +import timeit +from typing import Any + +from fastapi.encoders import jsonable_encoder + +LARGE_ITEMS: list[dict[str, Any]] = [ + { + "id": i, + "name": f"item-{i}", + "values": list(range(25)), + "meta": {"active": True, "group": i % 10, "tag": f"t{i % 5}"}, + } + for i in range(300) +] +LARGE_METADATA: dict[str, Any] = { + "source": "benchmark", + "version": 1, + "flags": {"a": True, "b": False, "c": True}, + "notes": ["x" * 50, "y" * 50, "z" * 50], +} +LARGE_PAYLOAD: dict[str, Any] = {"items": LARGE_ITEMS, "metadata": LARGE_METADATA} +SMALL_PAYLOAD: dict[str, Any] = {"name": "foo", "value": 123} + +ROUNDS = 20 +ITERS = 300 + + +def bench(payload: dict[str, Any]) -> tuple[float, float]: + times = [] + for _ in range(ROUNDS): + t = timeit.timeit(lambda: jsonable_encoder(payload), number=ITERS) + times.append(t / ITERS * 1e6) + return statistics.mean(times), statistics.stdev(times) + + +if __name__ == "__main__": + print(f"{'Payload':<35} {'mean µs/call':>14} {'stdev':>8}") + print("-" * 60) + for label, payload in [ + ("small dict (3 keys)", SMALL_PAYLOAD), + ("large dict (300 items, nested)", LARGE_PAYLOAD), + ]: + mean, sd = bench(payload) + print(f"{label:<35} {mean:>12.2f}µs {sd:>6.2f}") + print(f"\n({ROUNDS} rounds x {ITERS} iters each)")