qsv benchmark dashboard

Interactive charts of the qsv benchmark suite, rendered with qsv viz — a live showcase of the command charting its own performance.

qsv 21.1.0Platform aarch64-apple-darwinCPU Apple M2 Pro (12 cores)RAM 32 GiBDataset NYC 311 — 1M rows × 41 cols (520 MB)
Read me first. Throughput is records/sec = 1,000,000 rows ÷ mean of 3 runs (2 warmups) via hyperfine. Cross-version numbers are not strictly apples-to-apples — commands gain features over time, so a command's benchmark in an old release may do less work than today's. Read the historical charts (the trend, the flagship deep-dives and the heatmap) as broad trajectory, not exact speedup. count gets its own panel below (not the shared-scale charts) because with an index it just reads a stored row count (tens of millions of records/sec) and would flatten every other bar.

The index advantage

Build an index once and qsv can skip the opening scan on every run after. The payoff is lopsided — a ~6x jump for stats and ~3x for search, but next to nothing for streaming commands — so only the standouts are shown here.

see the qsv viz command
figs.append(viz("bar", index_src,
                ["--x", "command", "--y", "recs_per_sec", "--series", "index_status",
                 "--title", "The index advantage", "--y-title", "records/sec"],
                "index_advantage", "The index advantage",
                "Build an index once and qsv can skip the opening scan on every run after. "
                "The payoff is lopsided — a ~6x jump for stats and ~3x for search, but next "
                "to nothing for streaming commands — so only the standouts are shown here."))
view on GitHub ↗ (gen_benchmark_viz.py, lines 563–569)

count with an index is effectively instant

The index advantage at its extreme. With an index, count doesn't scan at all — it reads a row count qsv already stored, a ~10x jump from ~9M to ~90M rows/sec. It sits on its own axis precisely because that number would flatten every other bar on the page.

see the qsv viz command
figs.append(viz("bar", prep_count(),
                ["--x", "index_status", "--y", "recs_per_sec",
                 "--title", "count: the index-read superpower", "--y-title", "records/sec"],
                "count_callout", "count with an index is effectively instant",
                "The index advantage at its extreme. With an index, count doesn't scan at all — "
                "it reads a row count qsv already stored, a ~10x jump from ~9M to ~90M rows/sec. "
                "It sits on its own axis precisely because that number would flatten every "
                "other bar on the page."))
view on GitHub ↗ (gen_benchmark_viz.py, lines 570–577)

The index superpowers

Some commands don't just skip the opening scan with an index — they skip almost all the work. slice and sample seek straight to the rows they need; schema reuses cached statistics. That's a different order of magnitude: schema runs 78x faster, and slice and sample tens of times over — versus the low single digits for the scan-skippers above.

see the qsv viz command
figs.append(viz("bar", sp_src,
                ["--x", "command", "--y", "speedup",
                 "--title", "The index superpowers — times faster with an index",
                 "--y-title", "× faster with an index"],
                "index_superpowers", "The index superpowers",
                "Some commands don't just skip the opening scan with an index — they skip almost "
                "all the work. slice and sample seek straight to the rows they need; schema reuses "
                f"cached statistics. That's a different order of magnitude: {sp_top_cmd} runs "
                f"{sp_top_x:.0f}x faster, and slice and sample tens of times over — versus the "
                "low single digits for the scan-skippers above."))
view on GitHub ↗ (gen_benchmark_viz.py, lines 580–589)

sqlp tuning: the schema-cache knob

sqlp infers a schema before it runs. Cache that schema and the re-inference cost disappears on the next query — worth about a third more throughput on these aggregations, for a one-line option.

see the qsv viz command
figs.append(viz("bar", prep_sqlp(),
                ["--x", "name", "--y", "recs_per_sec",
                 "--title", "sqlp tuning: the Polars schema-cache knob",
                 "--y-title", "records/sec"],
                "sqlp_tuning", "sqlp tuning: the schema-cache knob",
                "sqlp infers a schema before it runs. Cache that schema and the re-inference "
                "cost disappears on the next query — worth about a third more throughput on "
                "these aggregations, for a one-line option."))
view on GitHub ↗ (gen_benchmark_viz.py, lines 590–597)

The long view — every release

Records/sec for the marquee commands across all 59 releases since 0.113.0. Each line follows the fastest path available at the time: the plain scan early on, then the indexed variant once search and searchset learned to use an index at 10.0.0 — the visible step up. stats, frequency and validate carried their index from launch, so those lines run flat-to-up with no step. Broad trajectory only (see the note above); count is omitted for scale.

see the qsv viz command
figs.append(viz("line", prep_trend(),
                ["--x", "version", "--y", "recs_per_sec", "--series", "command",
                 "--title", f"Throughput across every release ({first_release} → {latest_release})",
                 "--y-title", "records/sec"],
                "trend", "The long view — every release",
                f"Records/sec for the marquee commands across all {n_releases} releases since "
                f"{first_release}. Each line follows the fastest path available at the time: the "
                "plain scan early on, then the indexed variant once search and searchset learned "
                "to use an index at 10.0.0 — the visible step up. stats, frequency and validate "
                "carried their index from launch, so those lines run flat-to-up with no step. "
                "Broad trajectory only (see the note above); count is omitted for scale."))
view on GitHub ↗ (gen_benchmark_viz.py, lines 598–608)

Flagship deep-dive: stats

qsv's most-used command, indexed to its own launch speed. Over the years stats kept adding statistics — cardinality, quartiles, MAD, skewness — yet never got slower: plain stats now runs 1.8x its first-release speed, and the full --everything pass with an index 5.7x. Features grew; the curve still points up. (The odd single-release dip is a failed benchmark run, not a regression.)

see the qsv viz command
figs.append(viz("line", stats_src,
                ["--x", "version", "--y", "rel", "--series", "name",
                 "--title", "Flagship deep-dive: stats got richer AND faster",
                 "--y-title", "speed vs first release (1.0 = launch)"],
                "stats_growth", "Flagship deep-dive: stats",
                "qsv's most-used command, indexed to its own launch speed. Over the years stats "
                "kept adding statistics — cardinality, quartiles, MAD, skewness — yet never got "
                f"slower: plain stats now runs {s_base:.1f}x its first-release speed, and the "
                f"full --everything pass with an index {s_heavy:.1f}x. Features grew; the curve "
                "still points up. (The odd single-release dip is a failed benchmark run, not a "
                "regression.)"))
view on GitHub ↗ (gen_benchmark_viz.py, lines 616–626)

Flagship deep-dive: frequency

The same story for qsv's second flagship. As frequency gained sorted, case-insensitive and unlimited modes, throughput climbed rather than eroded — base frequency is now 1.5x its launch speed and the indexed run 2.0x. Newer modes join partway, each measured from its own debut; the transient dips are measurement artifacts, not regressions.

see the qsv viz command
figs.append(viz("line", freq_src,
                ["--x", "version", "--y", "rel", "--series", "name",
                 "--title", "Flagship deep-dive: frequency held its ground",
                 "--y-title", "speed vs first release (1.0 = launch)"],
                "freq_growth", "Flagship deep-dive: frequency",
                "The same story for qsv's second flagship. As frequency gained sorted, "
                "case-insensitive and unlimited modes, throughput climbed rather than eroded — "
                f"base frequency is now {f_base:.1f}x its launch speed and the indexed run "
                f"{f_idx:.1f}x. Newer modes join partway, each measured from its own debut; the "
                "transient dips are measurement artifacts, not regressions."))
view on GitHub ↗ (gen_benchmark_viz.py, lines 627–636)

Flagship deep-dive: validate

qsv's data-quality workhorse, each variant indexed to its own launch speed. As validate gained modes — the fast no-schema structural pass, batch validation, dynamic-enum lookups, newer JSON Schema drafts — full JSON-Schema validation kept pace: validate_index now runs 1.3x its first-release speed. The no-schema structural path — the most parse-bound variant — dipped ~40% below launch from 17.0.0 through 21.0.0, when qsv's CSV parser (the csv-nose fork) moved to its 1.0.x line, then recovered sharply in 21.1.0 to 1.1x (back above launch) once csv-nose 1.1.0 landed SIMD UTF-8 validation and memchr-based scanning. Full-schema and no-schema paths are shown base vs index, each normalized to its own debut. (The sharp single-release spikes down — e.g. at 1.0.0 and 2.2.1 — are failed benchmark runs, not regressions.)

see the qsv viz command
figs.append(viz("line", validate_src,
                ["--x", "version", "--y", "rel", "--series", "name",
                 "--title", "Flagship deep-dive: validate",
                 "--y-title", "speed vs first release (1.0 = launch)"],
                "validate_growth", "Flagship deep-dive: validate",
                "qsv's data-quality workhorse, each variant indexed to its own launch speed. As "
                "validate gained modes — the fast no-schema structural pass, batch validation, "
                "dynamic-enum lookups, newer JSON Schema drafts — full JSON-Schema validation kept "
                f"pace: validate_index now runs {v_idx:.1f}x its first-release speed. The no-schema "
                "structural path — the most parse-bound variant — dipped ~40% below launch from "
                "17.0.0 through 21.0.0, when qsv's CSV parser (the csv-nose fork) moved to its 1.0.x "
                f"line, then recovered sharply in 21.1.0 to {v_ns_idx:.1f}x (back above launch) once "
                "csv-nose 1.1.0 landed SIMD UTF-8 validation and memchr-based scanning. Full-schema "
                "and no-schema paths are shown base vs index, each normalized to its own debut. (The "
                "sharp single-release spikes down — e.g. at 1.0.0 and 2.2.1 — are failed benchmark "
                "runs, not regressions.)"))
view on GitHub ↗ (gen_benchmark_viz.py, lines 637–652)

Relative throughput heatmap

The indexed marquee commands over the recent window, each row normalized to its own peak (1.0 = that command's fastest release). Normalizing per row lets a 90M-rows/sec count and a 600k-rows/sec frequency share one canvas — the colour shows trajectory, not absolute speed.

see the qsv viz command
figs.append(viz("heatmap", prep_heatmap(hm_versions),
                ["--x", "version", "--y", "name", "--z", "rel",
                 "--title", "Relative throughput vs each command's recent peak"],
                "heatmap", "Relative throughput heatmap",
                "The indexed marquee commands over the recent window, each row normalized to its "
                "own peak (1.0 = that command's fastest release). Normalizing per row lets a "
                "90M-rows/sec count and a 600k-rows/sec frequency share one canvas — the colour "
                "shows trajectory, not absolute speed."))
view on GitHub ↗ (gen_benchmark_viz.py, lines 653–660)

Where the suite spends its time

The whole suite by wall-clock, family then benchmark. The biggest tiles are where a speedup would move the needle most — a map of where the optimization effort is best spent.

see the qsv viz command
figs.append(viz("treemap", prep_treemap(),
                ["--cols", "family,name", "--value", "mean", "--agg", "sum",
                 "--title", "Where the suite spends time (mean run time)"],
                "time_spent", "Where the suite spends its time",
                "The whole suite by wall-clock, family then benchmark. The biggest tiles are "
                "where a speedup would move the needle most — a map of where the optimization "
                "effort is best spent."))
view on GitHub ↗ (gen_benchmark_viz.py, lines 661–667)

Biggest speedups this release

The 15 benchmarks that improved most over the previous release. Bigger, brighter bubbles are larger wins — the percentage cut in mean run time from one version to the next.

see the qsv viz command
figs.append(viz("scatter", prep_gainers(),
                ["--x", "name", "--y", "delta (%)", "--size", "delta (%)", "--color", "delta (%)",
                 "--title", "Biggest speedups this release", "--y-title", "% faster vs previous version"],
                "gainers", "Biggest speedups this release",
                "The 15 benchmarks that improved most over the previous release. Bigger, brighter "
                "bubbles are larger wins — the percentage cut in mean run time from one version "
                "to the next."))
view on GitHub ↗ (gen_benchmark_viz.py, lines 668–674)

Change distribution by family

The wider view behind the speedups: the spread of per-release change within each family (above zero = faster). Most families cluster just north of zero — steady, unglamorous progress. Extreme outliers (|Δ| > 100%, usually measurement noise) are omitted; the y-axis is fixed to [-10, 55] so the boxes stay readable, clipping one bad-CI-run point (frequency_ignorecase, -79%).

see the qsv viz command
figs.append(viz("box", prep_delta_box(),
                ["--y", "delta (%)", "--x", "family",
                 "--title", "Release-over-release change by command family",
                 "--y-title", "% faster vs previous version",
                 # the real per-family boxes all sit within [-6, +51]; fix the axis to that
                 # window (with light padding) so a lone bad-CI-run outlier can't squash them
                 "--y-range=-10:55",
                 "--annotation",
                 "1 point clipped: frequency_ignorecase -79% (a bad CI run)"],
                "change_by_family", "Change distribution by family",
                "The wider view behind the speedups: the spread of per-release change within each "
                "family (above zero = faster). Most families cluster just north of zero — steady, "
                f"unglamorous progress. Extreme outliers (|Δ| > {int(DELTA_CLAMP)}%, usually "
                "measurement noise) are omitted; the y-axis is fixed to [-10, 55] so the boxes "
                "stay readable, clipping one bad-CI-run point (frequency_ignorecase, -79%)."))
view on GitHub ↗ (gen_benchmark_viz.py, lines 675–689)