|
1 | | -"""Standalone CodSpeed benchmark module: the RESULT-CARDINALITY (top-N) sweep — NOT integrated (not in |
2 | | -pyproject, not in CI, not committed). Run under each build's interpreter and compare: |
| 1 | +"""Standalone CodSpeed benchmark: the RESULT-CARDINALITY (rows-to-Python) sweep. Run under each build: |
3 | 2 |
|
4 | 3 | M=/Users/evert/projects/duckdb-python/main/.venv-release/bin/python |
5 | 4 | C=/Users/evert/projects/duckdb-python/wt-codspeed/.venv-release/bin/python |
6 | 5 | cd /Users/evert/projects/duckdb-python/wt-codspeed |
7 | 6 | $M -m pytest benchmarks/test_cardinality_perf.py --codspeed --codspeed-mode=walltime -o addopts= -p no:cacheprovider |
8 | 7 | $C -m pytest benchmarks/test_cardinality_perf.py --codspeed --codspeed-mode=walltime -o addopts= -p no:cacheprovider |
9 | 8 |
|
10 | | -WHY THIS MODULE (adopted from iqmo-org/bareduckdb): hold the SOURCE fixed and sweep only the number of rows |
11 | | -materialized to Python via ORDER BY ... LIMIT n for n in {100, 1k, 10k, 100k}, through fetchall / df / |
12 | | -to_arrow_table. The engine cost (scan the fixed SRC + top-N heap) stays ~constant, so the walltime delta |
13 | | -across n is dominated by the per-row binding conversion -> a clean per-row slope. The n=100 end is the |
14 | | -noise-free overhead regime (the natural instruction-count-gate point); the n=100k end is throughput. |
15 | | -
|
16 | | -A clean monotone slope (and ~parity slope between the two builds) is the signal we report; a build whose slope |
17 | | -is steeper has a per-row conversion regression. Source held constant rules out scan-cost as the confound (a |
18 | | -cleaner axis than varying range(), which also changes scan cost). |
19 | | -
|
20 | | -numpy/pandas/pyarrow are pinned to the SAME versions in both .venv-release, so the A/B delta is purely the binding. |
| 9 | +REDESIGN NOTE: the first version swept `ORDER BY a DESC LIMIT n` over a fixed source. That was wrong: |
| 10 | +the engine's full top-N SORT (~3-14ms, itself variable) dominated and swamped the per-row conversion |
| 11 | +signal, and the numbers came out non-monotone. This version pre-materializes the fixed source table ONCE |
| 12 | +and sweeps `SELECT * FROM src LIMIT n` with NO ORDER BY: a plain LIMIT early-stops the scan at n rows, so |
| 13 | +the engine cost is cheap and monotone in n, and the rows-to-Python CONVERSION is the dominant n-varying |
| 14 | +cost. That gives a clean, monotone per-row slope; the A/B delta at each n isolates the binding, and a build |
| 15 | +whose slope is steeper has a per-row conversion regression. n=100 is the overhead regime (the natural |
| 16 | +instruction-count-gate point); n=100_000 is throughput. |
| 17 | +
|
| 18 | +3 columns (BIGINT, DOUBLE, VARCHAR) so per-row conversion is non-trivial. numpy/pandas/pyarrow are pinned to |
| 19 | +the SAME versions in both .venv-release, so the A/B delta is purely the binding. |
21 | 20 | """ |
22 | 21 |
|
23 | 22 | import duckdb |
24 | 23 | import pytest |
25 | 24 |
|
26 | | -SRC = 200_000 # fixed source size -> constant engine scan + top-N across all n |
| 25 | +SRC_ROWS = 200_000 |
27 | 26 | LIMITS = [100, 1_000, 10_000, 100_000] |
28 | 27 |
|
29 | | -# 3 columns (BIGINT, DOUBLE, VARCHAR) so the per-row conversion is non-trivial; source is a fixed inline |
30 | | -# subquery (no table state) and ORDER BY forces a full scan + top-N of the same SRC rows every time. |
31 | | -_SRC_SUBQ = f"(SELECT i::BIGINT AS a, (i * 1.5)::DOUBLE AS b, ('s_' || i) AS s FROM range({SRC}) t(i))" |
32 | | - |
33 | | - |
34 | | -def _query(n): |
35 | | - return f"SELECT a, b, s FROM {_SRC_SUBQ} ORDER BY a DESC LIMIT {n}" |
36 | 28 |
|
37 | | - |
38 | | -@pytest.fixture |
| 29 | +@pytest.fixture(scope="module") |
39 | 30 | def con(): |
| 31 | + # Fixed source materialized ONCE (module-scoped): building it per test would add noise, and it must be |
| 32 | + # identical across the n sweep. `SELECT * FROM src LIMIT n` then reads only the first n rows. |
40 | 33 | c = duckdb.connect() |
| 34 | + c.execute( |
| 35 | + "CREATE TABLE src AS " |
| 36 | + f"SELECT i::BIGINT AS a, (i * 1.5)::DOUBLE AS b, ('s_' || i) AS s FROM range({SRC_ROWS}) t(i)" |
| 37 | + ) |
41 | 38 | yield c |
42 | 39 | c.close() |
43 | 40 |
|
44 | 41 |
|
| 42 | +def _query(n): |
| 43 | + # No ORDER BY: a plain LIMIT early-stops the scan at n rows -> engine cost cheap and monotone in n, so the |
| 44 | + # per-row binding conversion dominates the n-varying signal (unlike the old ORDER BY top-N sort). |
| 45 | + return f"SELECT a, b, s FROM src LIMIT {n}" |
| 46 | + |
| 47 | + |
45 | 48 | @pytest.mark.parametrize("n", LIMITS) |
46 | 49 | def test_limit_fetchall(benchmark, con, n): |
47 | 50 | q = _query(n) |
|
0 commit comments