informix-db/tests/benchmarks/test_codec_perf.py
Ryan Malloy 90ce035a00 Phase 21: Performance benchmarks (2026.05.04.5)
Adds tests/benchmarks/ with pytest-benchmark coverage of the hot codec
paths and end-to-end SELECT/INSERT/pool/async round-trips. Establishes
a committed baseline.json so PRs can be regression-checked at review
via --benchmark-compare.

* test_codec_perf.py (16): decode/encode_param/parse_tuple_payload
  micro-benchmarks - run without container, suitable for pre-merge CI.
* test_select_perf.py (4): SELECT round-trips - 1-row latency floor,
  10-row, 1k-row full fetch, parameterized.
* test_insert_perf.py (3): single-row INSERT, executemany 100 / 1000.
* test_pool_perf.py (3): cold connect, pool acquire/release, pool
  acquire + query + release.
* test_async_perf.py (2): async round-trip overhead, 10x concurrent.
* baseline.json: committed snapshot, 28 measurements.
* benchmark pytest marker, gated off by default.
* Makefile: bench / bench-codec / bench-save targets;
  test-integration excludes benchmarks for speed.

Headline numbers (dev container loopback):
* decode(int): 181 ns
* parse_tuple 5 cols: 2.87 µs/row
* SELECT 1 round-trip: 177 µs
* Pool acquire+query+release: 295 µs
* Cold connect: 11.2 ms (72x slower than pool)

UTF-8 decode carries no measurable cost vs iso-8859-1 - confirms
Phase 20 didn't regress anything.

Total: 69 unit + 211 integration + 28 benchmark = 308 tests.
2026-05-04 17:21:12 -06:00

200 lines
5.7 KiB
Python

"""Codec micro-benchmarks — no server required.
These measure the tight inner loops the driver hits on every row:
``decode()`` per cell, ``parse_tuple_payload()`` per row,
``encode_param()`` per parameter. A 1M-row fetch hits ``decode()``
5-10M times; a 1% slowdown there is *visible*.
The fixtures synthesize realistic byte payloads — no need for the
Docker container. This makes the benchmarks usable in CI's pre-merge
job (which doesn't run integration tests).
"""
from __future__ import annotations
import datetime
import struct
from io import BytesIO
import pytest
from informix_db._protocol import IfxStreamReader
from informix_db._resultset import ColumnInfo, parse_tuple_payload
from informix_db._types import IfxType
from informix_db.converters import decode, encode_param
pytestmark = pytest.mark.benchmark
# ---------------------------------------------------------------------------
# decode() — per-value dispatch
# ---------------------------------------------------------------------------
def test_decode_int(benchmark) -> None:
"""Hot path: per-cell INT decode. ~5M calls/sec is the kind of speed
a 1M-row fetch with 5 INT columns needs."""
raw = struct.pack("!i", 42)
benchmark(decode, int(IfxType.INT), raw)
def test_decode_smallint(benchmark) -> None:
raw = struct.pack("!h", 100)
benchmark(decode, int(IfxType.SMALLINT), raw)
def test_decode_bigint(benchmark) -> None:
raw = struct.pack("!q", 1234567890123)
benchmark(decode, int(IfxType.BIGINT), raw)
def test_decode_float(benchmark) -> None:
raw = struct.pack("!d", 3.14159)
benchmark(decode, int(IfxType.FLOAT), raw)
def test_decode_date(benchmark) -> None:
raw = struct.pack("!i", 45678)
benchmark(decode, int(IfxType.DATE), raw)
def test_decode_varchar_short(benchmark) -> None:
"""20-byte ASCII string — typical name column."""
raw = b"hello world example "
benchmark(decode, int(IfxType.VARCHAR), raw)
def test_decode_varchar_long(benchmark) -> None:
"""255-byte VARCHAR — max non-LVARCHAR length."""
raw = b"x" * 255
benchmark(decode, int(IfxType.VARCHAR), raw)
def test_decode_varchar_utf8(benchmark) -> None:
"""Multi-byte UTF-8 decode — exercise Phase 20 path."""
raw = "café résumé naïve Zürich".encode()
benchmark(decode, int(IfxType.VARCHAR), raw, "utf-8")
# ---------------------------------------------------------------------------
# encode_param() — parameter-binding hot path
# ---------------------------------------------------------------------------
def test_encode_int(benchmark) -> None:
benchmark(encode_param, 42)
def test_encode_str_ascii(benchmark) -> None:
benchmark(encode_param, "hello world example", "iso-8859-1")
def test_encode_str_utf8(benchmark) -> None:
benchmark(encode_param, "café résumé naïve", "utf-8")
def test_encode_float(benchmark) -> None:
benchmark(encode_param, 3.14159)
def test_encode_date(benchmark) -> None:
benchmark(encode_param, datetime.date(2026, 5, 4))
def test_encode_datetime(benchmark) -> None:
benchmark(encode_param, datetime.datetime(2026, 5, 4, 12, 30, 45))
# ---------------------------------------------------------------------------
# parse_tuple_payload() — per-row decode
# ---------------------------------------------------------------------------
def _build_systables_row_payload() -> bytes:
"""Synthesize the SQ_TUPLE bytes a typical systables row produces.
Layout: [short warn=0][int size][payload][optional pad]
Payload has columns: tabname VARCHAR(128), owner VARCHAR(32),
tabid INT, partnum INT, ncols INT.
"""
payload = bytearray()
# tabname VARCHAR: [byte len][bytes] — single-byte length prefix per
# the discovered tuple format
name = b"systables"
payload.append(len(name))
payload.extend(name)
# owner VARCHAR
owner = b"informix"
payload.append(len(owner))
payload.extend(owner)
# tabid INT
payload.extend(struct.pack("!i", 1))
# partnum INT
payload.extend(struct.pack("!i", 1048578))
# ncols INT
payload.extend(struct.pack("!i", 32))
out = bytearray()
out.extend(struct.pack("!h", 0)) # warn
out.extend(struct.pack("!i", len(payload)))
out.extend(payload)
if len(payload) & 1:
out.append(0) # even-byte pad
return bytes(out)
_SYSTABLES_COLUMNS = [
ColumnInfo(
name="tabname",
type_code=int(IfxType.VARCHAR),
raw_type_code=int(IfxType.VARCHAR),
encoded_length=128,
),
ColumnInfo(
name="owner",
type_code=int(IfxType.VARCHAR),
raw_type_code=int(IfxType.VARCHAR),
encoded_length=32,
),
ColumnInfo(
name="tabid",
type_code=int(IfxType.INT),
raw_type_code=int(IfxType.INT),
encoded_length=4,
),
ColumnInfo(
name="partnum",
type_code=int(IfxType.INT),
raw_type_code=int(IfxType.INT),
encoded_length=4,
),
ColumnInfo(
name="ncols",
type_code=int(IfxType.INT),
raw_type_code=int(IfxType.INT),
encoded_length=4,
),
]
def test_parse_tuple_5cols_iso8859(benchmark) -> None:
"""Decode a 5-column row (2 VARCHAR + 3 INT) — typical `systables` shape."""
payload = _build_systables_row_payload()
def run() -> tuple:
reader = IfxStreamReader(BytesIO(payload))
return parse_tuple_payload(reader, _SYSTABLES_COLUMNS)
benchmark(run)
def test_parse_tuple_5cols_utf8(benchmark) -> None:
"""Same shape, UTF-8 codec path — verify Phase 20 isn't a bottleneck."""
payload = _build_systables_row_payload()
def run() -> tuple:
reader = IfxStreamReader(BytesIO(payload))
return parse_tuple_payload(reader, _SYSTABLES_COLUMNS, encoding="utf-8")
benchmark(run)