diff --git a/.github/workflows/bench-dispatch.yml b/.github/workflows/bench-dispatch.yml index d1116d964b5..6313e37c64a 100644 --- a/.github/workflows/bench-dispatch.yml +++ b/.github/workflows/bench-dispatch.yml @@ -64,4 +64,4 @@ jobs: uses: ./.github/workflows/sql-pr.yml secrets: inherit with: - benchmark_profile: "full" + matrix_preset: "pr-full" diff --git a/.github/workflows/bench-pr.yml b/.github/workflows/bench-pr.yml index b5b011150a6..9c8408cf901 100644 --- a/.github/workflows/bench-pr.yml +++ b/.github/workflows/bench-pr.yml @@ -139,3 +139,4 @@ jobs: secrets: inherit with: mode: "pr" + matrix_preset: "pr-full" diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index b1e29277f00..0045c1ad214 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -171,31 +171,11 @@ jobs: secrets: inherit with: mode: "develop" + matrix_preset: "develop" sql-vortex: uses: ./.github/workflows/sql-benchmarks.yml secrets: inherit with: mode: "develop" - benchmark_matrix: | - [ - { - "id": "vortex-queries", - "subcommand": "vortex", - "name": "Vortex queries", - "data_formats": ["parquet", "vortex"], - "pr_targets": [ - {"engine": "datafusion", "format": "parquet"}, - {"engine": "datafusion", "format": "vortex"}, - {"engine": "duckdb", "format": "parquet"}, - {"engine": "duckdb", "format": "vortex"} - ], - "develop_targets": [ - {"engine": "datafusion", "format": "parquet"}, - {"engine": "datafusion", "format": "vortex"}, - {"engine": "duckdb", "format": "parquet"}, - {"engine": "duckdb", "format": "vortex"} - ], - "iterations": "100" - } - ] + matrix_preset: "vortex" diff --git a/.github/workflows/nightly-bench.yml b/.github/workflows/nightly-bench.yml index b89395516be..4564ef681ca 100644 --- a/.github/workflows/nightly-bench.yml +++ b/.github/workflows/nightly-bench.yml @@ -23,49 +23,7 @@ jobs: with: mode: "develop" machine_type: ${{ matrix.machine_type.instance_name }} - benchmark_matrix: | - [ - { - "id": "tpch-nvme", - "subcommand": "tpch", - "name": "TPC-H on NVME", - "data_formats": ["parquet", "vortex"], - "pr_targets": [ - {"engine": "datafusion", "format": "parquet"}, - {"engine": "datafusion", "format": "vortex"}, - {"engine": "duckdb", "format": "parquet"}, - {"engine": "duckdb", "format": "vortex"} - ], - "develop_targets": [ - {"engine": "datafusion", "format": "parquet"}, - {"engine": "datafusion", "format": "vortex"}, - {"engine": "duckdb", "format": "parquet"}, - {"engine": "duckdb", "format": "vortex"} - ], - "scale_factor": "100" - }, - { - "id": "tpch-s3", - "subcommand": "tpch", - "name": "TPC-H on S3", - "local_dir": "vortex-bench/data/tpch/100.0", - "remote_storage": "s3://vortex-ci-benchmark-datasets/${{github.ref_name}}/${{github.run_id}}/tpch/100.0/", - "data_formats": ["parquet", "vortex"], - "pr_targets": [ - {"engine": "datafusion", "format": "parquet"}, - {"engine": "datafusion", "format": "vortex"}, - {"engine": "duckdb", "format": "parquet"}, - {"engine": "duckdb", "format": "vortex"} - ], - "develop_targets": [ - {"engine": "datafusion", "format": "parquet"}, - {"engine": "datafusion", "format": "vortex"}, - {"engine": "duckdb", "format": "parquet"}, - {"engine": "duckdb", "format": "vortex"} - ], - "scale_factor": "100.0" - } - ] + matrix_preset: "nightly" strategy: # A single run not should kill the others fail-fast: false diff --git a/.github/workflows/sql-benchmarks.yml b/.github/workflows/sql-benchmarks.yml index 59dd9571b36..552bb7d0e01 100644 --- a/.github/workflows/sql-benchmarks.yml +++ b/.github/workflows/sql-benchmarks.yml @@ -6,509 +6,45 @@ on: mode: required: true type: string + matrix_preset: + required: true + type: string + description: "Named benchmark matrix to run" machine_type: required: false type: string default: i7i.metal-24xl - benchmark_matrix: - required: false - type: string - description: "JSON string containing the full matrix configuration" - default: | - [ - { - "id": "clickbench-nvme", - "subcommand": "clickbench", - "name": "Clickbench on NVME", - "data_formats": ["parquet", "vortex", "vortex-compact", "duckdb"], - "pr_targets": [ - {"engine": "datafusion", "format": "parquet"}, - {"engine": "datafusion", "format": "vortex"}, - {"engine": "duckdb", "format": "parquet"}, - {"engine": "duckdb", "format": "vortex"}, - {"engine": "duckdb", "format": "duckdb"} - ], - "develop_targets": [ - {"engine": "datafusion", "format": "parquet"}, - {"engine": "datafusion", "format": "vortex"}, - {"engine": "datafusion", "format": "vortex-compact"}, - {"engine": "datafusion", "format": "lance"}, - {"engine": "duckdb", "format": "parquet"}, - {"engine": "duckdb", "format": "vortex"}, - {"engine": "duckdb", "format": "vortex-compact"}, - {"engine": "duckdb", "format": "duckdb"} - ] - }, - { - "id": "clickbench-sorted-nvme", - "subcommand": "clickbench-sorted", - "name": "Clickbench Sorted on NVME", - "data_formats": ["parquet", "vortex", "vortex-compact", "duckdb"], - "pr_targets": [ - {"engine": "datafusion", "format": "parquet"}, - {"engine": "datafusion", "format": "vortex"}, - {"engine": "duckdb", "format": "parquet"}, - {"engine": "duckdb", "format": "vortex"}, - {"engine": "duckdb", "format": "duckdb"} - ], - "develop_targets": [ - {"engine": "datafusion", "format": "parquet"}, - {"engine": "datafusion", "format": "vortex"}, - {"engine": "datafusion", "format": "vortex-compact"}, - {"engine": "datafusion", "format": "lance"}, - {"engine": "duckdb", "format": "parquet"}, - {"engine": "duckdb", "format": "vortex"}, - {"engine": "duckdb", "format": "vortex-compact"}, - {"engine": "duckdb", "format": "duckdb"} - ] - }, - { - "id": "tpch-nvme", - "subcommand": "tpch", - "name": "TPC-H SF=1 on NVME", - "data_formats": ["parquet", "vortex", "vortex-compact", "duckdb"], - "pr_targets": [ - {"engine": "datafusion", "format": "arrow"}, - {"engine": "datafusion", "format": "parquet"}, - {"engine": "datafusion", "format": "vortex"}, - {"engine": "datafusion", "format": "vortex-compact"}, - {"engine": "duckdb", "format": "parquet"}, - {"engine": "duckdb", "format": "vortex"}, - {"engine": "duckdb", "format": "vortex-compact"}, - {"engine": "duckdb", "format": "duckdb"} - ], - "develop_targets": [ - {"engine": "datafusion", "format": "arrow"}, - {"engine": "datafusion", "format": "parquet"}, - {"engine": "datafusion", "format": "vortex"}, - {"engine": "datafusion", "format": "vortex-compact"}, - {"engine": "datafusion", "format": "lance"}, - {"engine": "duckdb", "format": "parquet"}, - {"engine": "duckdb", "format": "vortex"}, - {"engine": "duckdb", "format": "vortex-compact"}, - {"engine": "duckdb", "format": "duckdb"} - ], - "scale_factor": "1.0", - "iterations": "10" - }, - { - "id": "tpch-s3", - "subcommand": "tpch", - "name": "TPC-H SF=1 on S3", - "local_dir": "vortex-bench/data/tpch/1.0", - "remote_storage": "s3://vortex-ci-benchmark-datasets/${{github.ref_name}}/${{github.run_id}}/tpch/1.0/", - "data_formats": ["parquet", "vortex", "vortex-compact"], - "pr_targets": [ - {"engine": "datafusion", "format": "parquet"}, - {"engine": "datafusion", "format": "vortex"}, - {"engine": "datafusion", "format": "vortex-compact"}, - {"engine": "duckdb", "format": "parquet"}, - {"engine": "duckdb", "format": "vortex"}, - {"engine": "duckdb", "format": "vortex-compact"} - ], - "develop_targets": [ - {"engine": "datafusion", "format": "parquet"}, - {"engine": "datafusion", "format": "vortex"}, - {"engine": "datafusion", "format": "vortex-compact"}, - {"engine": "duckdb", "format": "parquet"}, - {"engine": "duckdb", "format": "vortex"}, - {"engine": "duckdb", "format": "vortex-compact"} - ], - "scale_factor": "1.0", - "iterations": "10" - }, - { - "id": "tpch-nvme-10", - "subcommand": "tpch", - "name": "TPC-H SF=10 on NVME", - "data_formats": ["parquet", "vortex", "vortex-compact", "duckdb"], - "pr_targets": [ - {"engine": "datafusion", "format": "arrow"}, - {"engine": "datafusion", "format": "parquet"}, - {"engine": "datafusion", "format": "vortex"}, - {"engine": "datafusion", "format": "vortex-compact"}, - {"engine": "duckdb", "format": "parquet"}, - {"engine": "duckdb", "format": "vortex"}, - {"engine": "duckdb", "format": "vortex-compact"}, - {"engine": "duckdb", "format": "duckdb"} - ], - "develop_targets": [ - {"engine": "datafusion", "format": "arrow"}, - {"engine": "datafusion", "format": "parquet"}, - {"engine": "datafusion", "format": "vortex"}, - {"engine": "datafusion", "format": "vortex-compact"}, - {"engine": "datafusion", "format": "lance"}, - {"engine": "duckdb", "format": "parquet"}, - {"engine": "duckdb", "format": "vortex"}, - {"engine": "duckdb", "format": "vortex-compact"}, - {"engine": "duckdb", "format": "duckdb"} - ], - "scale_factor": "10.0", - "iterations": "10" - }, - { - "id": "tpch-s3-10", - "subcommand": "tpch", - "name": "TPC-H SF=10 on S3", - "local_dir": "vortex-bench/data/tpch/10.0", - "remote_storage": "s3://vortex-ci-benchmark-datasets/${{github.ref_name}}/${{github.run_id}}/tpch/10.0/", - "data_formats": ["parquet", "vortex", "vortex-compact"], - "pr_targets": [ - {"engine": "datafusion", "format": "parquet"}, - {"engine": "datafusion", "format": "vortex"}, - {"engine": "datafusion", "format": "vortex-compact"}, - {"engine": "duckdb", "format": "parquet"}, - {"engine": "duckdb", "format": "vortex"}, - {"engine": "duckdb", "format": "vortex-compact"} - ], - "develop_targets": [ - {"engine": "datafusion", "format": "parquet"}, - {"engine": "datafusion", "format": "vortex"}, - {"engine": "datafusion", "format": "vortex-compact"}, - {"engine": "duckdb", "format": "parquet"}, - {"engine": "duckdb", "format": "vortex"}, - {"engine": "duckdb", "format": "vortex-compact"} - ], - "scale_factor": "10.0", - "iterations": "10" - }, - { - "id": "tpcds-nvme", - "subcommand": "tpcds", - "name": "TPC-DS SF=1 on NVME", - "data_formats": ["parquet", "vortex", "vortex-compact", "duckdb"], - "pr_targets": [ - {"engine": "datafusion", "format": "parquet"}, - {"engine": "datafusion", "format": "vortex"}, - {"engine": "datafusion", "format": "vortex-compact"}, - {"engine": "duckdb", "format": "parquet"}, - {"engine": "duckdb", "format": "vortex"}, - {"engine": "duckdb", "format": "vortex-compact"}, - {"engine": "duckdb", "format": "duckdb"} - ], - "develop_targets": [ - {"engine": "datafusion", "format": "parquet"}, - {"engine": "datafusion", "format": "vortex"}, - {"engine": "datafusion", "format": "vortex-compact"}, - {"engine": "duckdb", "format": "parquet"}, - {"engine": "duckdb", "format": "vortex"}, - {"engine": "duckdb", "format": "vortex-compact"}, - {"engine": "duckdb", "format": "duckdb"} - ], - "scale_factor": "1.0" - }, - { - "id": "statpopgen", - "subcommand": "statpopgen", - "name": "Statistical and Population Genetics", - "local_dir": "vortex-bench/data/statpopgen", - "data_formats": ["parquet", "vortex", "vortex-compact"], - "pr_targets": [ - {"engine": "duckdb", "format": "parquet"}, - {"engine": "duckdb", "format": "vortex"}, - {"engine": "duckdb", "format": "vortex-compact"} - ], - "develop_targets": [ - {"engine": "duckdb", "format": "parquet"}, - {"engine": "duckdb", "format": "vortex"}, - {"engine": "duckdb", "format": "vortex-compact"} - ], - "scale_factor": "100" - }, - { - "id": "fineweb", - "subcommand": "fineweb", - "name": "FineWeb NVMe", - "data_formats": ["parquet", "vortex", "vortex-compact"], - "pr_targets": [ - {"engine": "datafusion", "format": "parquet"}, - {"engine": "datafusion", "format": "vortex"}, - {"engine": "datafusion", "format": "vortex-compact"}, - {"engine": "duckdb", "format": "parquet"}, - {"engine": "duckdb", "format": "vortex"}, - {"engine": "duckdb", "format": "vortex-compact"} - ], - "develop_targets": [ - {"engine": "datafusion", "format": "parquet"}, - {"engine": "datafusion", "format": "vortex"}, - {"engine": "datafusion", "format": "vortex-compact"}, - {"engine": "duckdb", "format": "parquet"}, - {"engine": "duckdb", "format": "vortex"}, - {"engine": "duckdb", "format": "vortex-compact"} - ], - "scale_factor": "100" - }, - { - "id": "fineweb-s3", - "subcommand": "fineweb", - "name": "FineWeb S3", - "local_dir": "vortex-bench/data/fineweb", - "remote_storage": "s3://vortex-ci-benchmark-datasets/${{github.ref_name}}/${{github.run_id}}/fineweb/", - "data_formats": ["parquet", "vortex", "vortex-compact"], - "pr_targets": [ - {"engine": "datafusion", "format": "parquet"}, - {"engine": "datafusion", "format": "vortex"}, - {"engine": "datafusion", "format": "vortex-compact"}, - {"engine": "duckdb", "format": "parquet"}, - {"engine": "duckdb", "format": "vortex"}, - {"engine": "duckdb", "format": "vortex-compact"} - ], - "develop_targets": [ - {"engine": "datafusion", "format": "parquet"}, - {"engine": "datafusion", "format": "vortex"}, - {"engine": "datafusion", "format": "vortex-compact"}, - {"engine": "duckdb", "format": "parquet"}, - {"engine": "duckdb", "format": "vortex"}, - {"engine": "duckdb", "format": "vortex-compact"} - ], - "scale_factor": "100" - }, - { - "id": "polarsignals", - "subcommand": "polarsignals", - "name": "PolarSignals Profiling", - "data_formats": ["vortex"], - "pr_targets": [ - {"engine": "datafusion", "format": "vortex"} - ], - "develop_targets": [ - {"engine": "datafusion", "format": "vortex"} - ], - "scale_factor": "1" - }, - { - "id": "appian-nvme", - "subcommand": "appian", - "name": "Appian on NVME", - "data_formats": ["parquet", "vortex", "vortex-compact", "duckdb"], - "pr_targets": [ - {"engine": "datafusion", "format": "parquet"}, - {"engine": "datafusion", "format": "vortex"}, - {"engine": "duckdb", "format": "parquet"}, - {"engine": "duckdb", "format": "vortex"}, - {"engine": "duckdb", "format": "duckdb"} - ], - "develop_targets": [ - {"engine": "datafusion", "format": "parquet"}, - {"engine": "datafusion", "format": "vortex"}, - {"engine": "datafusion", "format": "vortex-compact"}, - {"engine": "duckdb", "format": "parquet"}, - {"engine": "duckdb", "format": "vortex"}, - {"engine": "duckdb", "format": "vortex-compact"}, - {"engine": "duckdb", "format": "duckdb"} - ], - "iterations": "10" - } - ] - base_benchmark_matrix: - required: false - type: string - description: "JSON string containing the base matrix configuration" - default: | - [ - { - "id": "clickbench-nvme", - "subcommand": "clickbench", - "name": "Clickbench on NVME", - "data_formats": ["parquet", "vortex"], - "pr_targets": [ - {"engine": "datafusion", "format": "parquet"}, - {"engine": "datafusion", "format": "vortex"}, - {"engine": "duckdb", "format": "parquet"}, - {"engine": "duckdb", "format": "vortex"} - ], - "develop_targets": [ - {"engine": "datafusion", "format": "parquet"}, - {"engine": "datafusion", "format": "vortex"}, - {"engine": "datafusion", "format": "lance"}, - {"engine": "duckdb", "format": "parquet"}, - {"engine": "duckdb", "format": "vortex"} - ] - }, - { - "id": "clickbench-sorted-nvme", - "subcommand": "clickbench-sorted", - "name": "Clickbench Sorted on NVME", - "data_formats": ["parquet", "vortex"], - "pr_targets": [ - {"engine": "datafusion", "format": "parquet"}, - {"engine": "datafusion", "format": "vortex"}, - {"engine": "duckdb", "format": "parquet"}, - {"engine": "duckdb", "format": "vortex"} - ], - "develop_targets": [ - {"engine": "datafusion", "format": "parquet"}, - {"engine": "datafusion", "format": "vortex"}, - {"engine": "datafusion", "format": "lance"}, - {"engine": "duckdb", "format": "parquet"}, - {"engine": "duckdb", "format": "vortex"} - ] - }, - { - "id": "tpch-nvme", - "subcommand": "tpch", - "name": "TPC-H SF=1 on NVME", - "data_formats": ["parquet", "vortex"], - "pr_targets": [ - {"engine": "datafusion", "format": "arrow"}, - {"engine": "datafusion", "format": "parquet"}, - {"engine": "datafusion", "format": "vortex"}, - {"engine": "duckdb", "format": "parquet"}, - {"engine": "duckdb", "format": "vortex"} - ], - "develop_targets": [ - {"engine": "datafusion", "format": "arrow"}, - {"engine": "datafusion", "format": "parquet"}, - {"engine": "datafusion", "format": "vortex"}, - {"engine": "datafusion", "format": "lance"}, - {"engine": "duckdb", "format": "parquet"}, - {"engine": "duckdb", "format": "vortex"} - ], - "scale_factor": "1.0", - "iterations": "10" - }, - { - "id": "tpch-s3", - "subcommand": "tpch", - "name": "TPC-H SF=1 on S3", - "local_dir": "vortex-bench/data/tpch/1.0", - "remote_storage": "s3://vortex-ci-benchmark-datasets/${{github.ref_name}}/${{github.run_id}}/tpch/1.0/", - "data_formats": ["parquet", "vortex"], - "pr_targets": [ - {"engine": "datafusion", "format": "parquet"}, - {"engine": "datafusion", "format": "vortex"}, - {"engine": "duckdb", "format": "parquet"}, - {"engine": "duckdb", "format": "vortex"} - ], - "develop_targets": [ - {"engine": "datafusion", "format": "parquet"}, - {"engine": "datafusion", "format": "vortex"}, - {"engine": "duckdb", "format": "parquet"}, - {"engine": "duckdb", "format": "vortex"} - ], - "scale_factor": "1.0", - "iterations": "10" - }, - { - "id": "tpch-nvme-10", - "subcommand": "tpch", - "name": "TPC-H SF=10 on NVME", - "data_formats": ["parquet", "vortex"], - "pr_targets": [ - {"engine": "datafusion", "format": "arrow"}, - {"engine": "datafusion", "format": "parquet"}, - {"engine": "datafusion", "format": "vortex"}, - {"engine": "duckdb", "format": "parquet"}, - {"engine": "duckdb", "format": "vortex"} - ], - "develop_targets": [ - {"engine": "datafusion", "format": "arrow"}, - {"engine": "datafusion", "format": "parquet"}, - {"engine": "datafusion", "format": "vortex"}, - {"engine": "datafusion", "format": "lance"}, - {"engine": "duckdb", "format": "parquet"}, - {"engine": "duckdb", "format": "vortex"} - ], - "scale_factor": "10.0", - "iterations": "10" - }, - { - "id": "tpcds-nvme", - "subcommand": "tpcds", - "name": "TPC-DS SF=1 on NVME", - "data_formats": ["parquet", "vortex"], - "pr_targets": [ - {"engine": "datafusion", "format": "parquet"}, - {"engine": "datafusion", "format": "vortex"}, - {"engine": "duckdb", "format": "parquet"}, - {"engine": "duckdb", "format": "vortex"} - ], - "develop_targets": [ - {"engine": "datafusion", "format": "parquet"}, - {"engine": "datafusion", "format": "vortex"}, - {"engine": "duckdb", "format": "parquet"}, - {"engine": "duckdb", "format": "vortex"} - ], - "scale_factor": "1.0" - }, - { - "id": "statpopgen", - "subcommand": "statpopgen", - "name": "Statistical and Population Genetics", - "local_dir": "vortex-bench/data/statpopgen", - "data_formats": ["parquet", "vortex"], - "pr_targets": [ - {"engine": "duckdb", "format": "parquet"}, - {"engine": "duckdb", "format": "vortex"} - ], - "develop_targets": [ - {"engine": "duckdb", "format": "parquet"}, - {"engine": "duckdb", "format": "vortex"} - ], - "scale_factor": "100" - }, - { - "id": "fineweb", - "subcommand": "fineweb", - "name": "FineWeb NVMe", - "data_formats": ["parquet", "vortex"], - "pr_targets": [ - {"engine": "datafusion", "format": "parquet"}, - {"engine": "datafusion", "format": "vortex"}, - {"engine": "duckdb", "format": "parquet"}, - {"engine": "duckdb", "format": "vortex"} - ], - "develop_targets": [ - {"engine": "datafusion", "format": "parquet"}, - {"engine": "datafusion", "format": "vortex"}, - {"engine": "duckdb", "format": "parquet"}, - {"engine": "duckdb", "format": "vortex"} - ], - "scale_factor": "100" - }, - { - "id": "fineweb-s3", - "subcommand": "fineweb", - "name": "FineWeb S3", - "local_dir": "vortex-bench/data/fineweb", - "remote_storage": "s3://vortex-ci-benchmark-datasets/${{github.ref_name}}/${{github.run_id}}/fineweb/", - "data_formats": ["parquet", "vortex"], - "pr_targets": [ - {"engine": "datafusion", "format": "parquet"}, - {"engine": "datafusion", "format": "vortex"}, - {"engine": "duckdb", "format": "parquet"}, - {"engine": "duckdb", "format": "vortex"} - ], - "develop_targets": [ - {"engine": "datafusion", "format": "parquet"}, - {"engine": "datafusion", "format": "vortex"}, - {"engine": "duckdb", "format": "parquet"}, - {"engine": "duckdb", "format": "vortex"} - ], - "scale_factor": "100" - }, - { - "id": "polarsignals", - "subcommand": "polarsignals", - "name": "PolarSignals Profiling", - "data_formats": ["vortex"], - "pr_targets": [ - {"engine": "datafusion", "format": "vortex"} - ], - "develop_targets": [ - {"engine": "datafusion", "format": "vortex"} - ], - "scale_factor": "1" - } - ] - benchmark_profile: - required: false - type: string - description: "Benchmark profile to run: full or base" - default: "full" jobs: + resolve-matrix: + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + outputs: + benchmark_matrix: ${{ steps.resolve.outputs.benchmark_matrix }} + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + ref: ${{ inputs.mode == 'pr' && github.event.pull_request.head.sha || github.sha }} + persist-credentials: false + - name: Install uv + uses: spiraldb/actions/.github/actions/setup-uv@a746510eafaa926484c354541cfc49b2ec06cc63 # 0.18.6 + with: + sync: false + - name: Resolve benchmark matrix + id: resolve + shell: bash + env: + MATRIX_PRESET: ${{ inputs.matrix_preset }} + run: | + set -Eeuo pipefail + + matrix="$(uv run --project bench-orchestrator vx-bench matrix "$MATRIX_PRESET")" + echo "benchmark_matrix=$matrix" >> "$GITHUB_OUTPUT" + bench: + needs: resolve-matrix timeout-minutes: 120 env: VORTEX_EXPERIMENTAL_PATCHED_ARRAY: "1" @@ -518,7 +54,7 @@ jobs: strategy: fail-fast: false matrix: - include: ${{ fromJSON(inputs.benchmark_profile == 'base' && inputs.base_benchmark_matrix || inputs.benchmark_matrix) }} + include: ${{ fromJSON(needs.resolve-matrix.outputs.benchmark_matrix) }} runs-on: >- ${{ github.repository == 'vortex-data/vortex' @@ -530,12 +66,8 @@ jobs: with: sccache: s3 - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - if: inputs.mode == 'pr' with: - ref: ${{ github.event.pull_request.head.sha }} - - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - if: inputs.mode != 'pr' + ref: ${{ inputs.mode == 'pr' && github.event.pull_request.head.sha || github.sha }} - name: Setup benchmark environment run: sudo bash scripts/setup-benchmark.sh - uses: ./.github/actions/setup-rust @@ -555,21 +87,6 @@ jobs: - uses: ./.github/actions/system-info - - name: Resolve benchmark targets - id: targets - shell: bash - run: | - if [ "${{ inputs.mode }}" = "pr" ]; then - targets='${{ toJSON(matrix.pr_targets) }}' - else - targets='${{ toJSON(matrix.develop_targets) }}' - fi - { - echo 'targets_json<<__TARGETS_JSON__' - echo "$targets" - echo '__TARGETS_JSON__' - } >> "$GITHUB_OUTPUT" - - name: Build binaries shell: bash env: @@ -600,13 +117,14 @@ jobs: role-duration-seconds: 7200 - name: Upload data - if: matrix.remote_storage != null && (inputs.mode != 'pr' || github.event.pull_request.head.repo.fork == false) + if: matrix.remote_key != null && (inputs.mode != 'pr' || github.event.pull_request.head.repo.fork == false) shell: bash env: AWS_REGION: "us-east-1" + REMOTE_STORAGE: "s3://vortex-ci-benchmark-datasets/${{ github.ref_name }}/${{ github.run_id }}/${{ matrix.remote_key }}/" run: | - aws s3 rm --recursive ${{ matrix.remote_storage }} - aws s3 cp --recursive ${{matrix.local_dir}} ${{ matrix.remote_storage }} + aws s3 rm --recursive "$REMOTE_STORAGE" + aws s3 cp --recursive "${{ matrix.local_dir }}" "$REMOTE_STORAGE" - name: Setup Polar Signals if: inputs.mode != 'pr' || github.event.pull_request.head.repo.fork == false @@ -619,7 +137,7 @@ jobs: extra_args: "--off-cpu-threshold=0.03" # Personally tuned by @brancz - name: Run ${{ matrix.name }} benchmark - if: matrix.remote_storage == null || github.event.pull_request.head.repo.fork == true + if: matrix.remote_key == null || github.event.pull_request.head.repo.fork == true shell: bash env: OTEL_SERVICE_NAME: "vortex-bench" @@ -629,7 +147,7 @@ jobs: OTEL_RESOURCE_ATTRIBUTES: "bench-name=${{ matrix.id }}" run: | bash scripts/bench-taskset.sh uv run --project bench-orchestrator vx-bench run "${{ matrix.subcommand }}" \ - --targets-json '${{ steps.targets.outputs.targets_json }}' \ + --targets-json '${{ toJSON(matrix.targets) }}' \ --output results.json \ --gh-json-v3 results.v3.jsonl \ --no-build \ @@ -638,7 +156,7 @@ jobs: ${{ matrix.scale_factor && format('--opt scale-factor={0}', matrix.scale_factor) || '' }} - name: Run ${{ matrix.name }} benchmark (remote) - if: matrix.remote_storage != null && (inputs.mode != 'pr' || github.event.pull_request.head.repo.fork == false) + if: matrix.remote_key != null && (inputs.mode != 'pr' || github.event.pull_request.head.repo.fork == false) shell: bash env: AWS_REGION: "us-east-1" @@ -647,19 +165,20 @@ jobs: OTEL_EXPORTER_OTLP_ENDPOINT: "${{ (inputs.mode != 'pr' || github.event.pull_request.head.repo.fork == false) && secrets.OTEL_EXPORTER_OTLP_ENDPOINT || '' }}" OTEL_EXPORTER_OTLP_HEADERS: "${{ (inputs.mode != 'pr' || github.event.pull_request.head.repo.fork == false) && secrets.OTEL_EXPORTER_OTLP_HEADERS || '' }}" OTEL_RESOURCE_ATTRIBUTES: "bench-name=${{ matrix.id }}" + REMOTE_STORAGE: "s3://vortex-ci-benchmark-datasets/${{ github.ref_name }}/${{ github.run_id }}/${{ matrix.remote_key }}/" run: | bash scripts/bench-taskset.sh uv run --project bench-orchestrator vx-bench run "${{ matrix.subcommand }}" \ - --targets-json '${{ steps.targets.outputs.targets_json }}' \ + --targets-json '${{ toJSON(matrix.targets) }}' \ --output results.json \ --gh-json-v3 results.v3.jsonl \ --no-build \ --runner "ec2_${{ inputs.machine_type }}" \ ${{ matrix.iterations && format('--iterations {0}', matrix.iterations) || '' }} \ - --opt remote-data-dir=${{ matrix.remote_storage }} \ + --opt remote-data-dir="$REMOTE_STORAGE" \ ${{ matrix.scale_factor && format('--opt scale-factor={0}', matrix.scale_factor) || '' }} - name: Capture file sizes - if: matrix.remote_storage == null + if: matrix.remote_key == null shell: bash run: | uv run --no-project scripts/capture-file-sizes.py \ @@ -698,7 +217,7 @@ jobs: message: | # 🚨🚨🚨❌❌❌ SQL BENCHMARK FAILED ❌❌❌🚨🚨🚨 - Benchmark `${{ matrix.name }}` (${{ inputs.benchmark_profile }}) failed! Check the [workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) for details. + Benchmark `${{ matrix.name }}` (${{ inputs.matrix_preset }}) failed! Check the [workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) for details. comment-tag: bench-pr-comment-${{ matrix.id }} - name: Upload Benchmark Results diff --git a/.github/workflows/sql-pr.yml b/.github/workflows/sql-pr.yml index 45b0ed1d675..b14016030c0 100644 --- a/.github/workflows/sql-pr.yml +++ b/.github/workflows/sql-pr.yml @@ -5,7 +5,7 @@ name: PR SQL Benchmarks concurrency: group: >- - ${{ github.workflow }}-${{ inputs.benchmark_profile || 'base' }}-${{ + ${{ github.workflow }}-${{ inputs.matrix_preset || 'pr' }}-${{ github.head_ref || github.run_id }} cancel-in-progress: true @@ -13,20 +13,20 @@ concurrency: on: workflow_call: inputs: - benchmark_profile: + matrix_preset: required: false type: string - default: "base" + default: "pr" workflow_dispatch: inputs: - benchmark_profile: - description: "SQL benchmark profile to run" + matrix_preset: + description: "SQL benchmark matrix to run" required: false type: choice - default: "base" + default: "pr" options: - - "base" - - "full" + - "pr" + - "pr-full" permissions: contents: read @@ -39,4 +39,4 @@ jobs: secrets: inherit with: mode: "pr" - benchmark_profile: ${{ inputs.benchmark_profile || 'base' }} + matrix_preset: ${{ inputs.matrix_preset }} diff --git a/.github/workflows/sql-vortex-pr.yml b/.github/workflows/sql-vortex-pr.yml index 2b99eb0e4d9..f368bd01935 100644 --- a/.github/workflows/sql-vortex-pr.yml +++ b/.github/workflows/sql-vortex-pr.yml @@ -23,25 +23,4 @@ jobs: secrets: inherit with: mode: "pr" - benchmark_matrix: | - [ - { - "id": "vortex-queries", - "subcommand": "vortex", - "name": "Vortex queries", - "data_formats": ["parquet", "vortex"], - "pr_targets": [ - {"engine": "datafusion", "format": "parquet"}, - {"engine": "datafusion", "format": "vortex"}, - {"engine": "duckdb", "format": "parquet"}, - {"engine": "duckdb", "format": "vortex"} - ], - "develop_targets": [ - {"engine": "datafusion", "format": "parquet"}, - {"engine": "datafusion", "format": "vortex"}, - {"engine": "duckdb", "format": "parquet"}, - {"engine": "duckdb", "format": "vortex"} - ], - "iterations": "100" - } - ] + matrix_preset: "vortex" diff --git a/bench-orchestrator/README.md b/bench-orchestrator/README.md index 0b267008a85..0dd938f861e 100644 --- a/bench-orchestrator/README.md +++ b/bench-orchestrator/README.md @@ -70,6 +70,24 @@ vx-bench prepare-data [options] - `--formats-json`: Exact data formats as JSON, e.g. `'["parquet","vortex"]'` - `--opt`: Benchmark-specific options such as `scale-factor=10.0` + +### `matrix` - Render a CI Benchmark Matrix + +Emit the GitHub Actions `include:` array for a named preset. The benchmark workflows use this +to keep benchmark coverage in Python instead of copying large JSON matrices between YAML files. + +```bash +vx-bench matrix # list available presets +vx-bench matrix develop # emit compact JSON +vx-bench matrix pr-full # emit full pull-request coverage +vx-bench matrix nightly --pretty +``` + +**Options:** + +- `--list`: List available presets and exit +- `--pretty`: Pretty-print the JSON output + ### `compare` - Compare Results Compare benchmark results within a run or across multiple runs. Results are displayed in a pivot table format. @@ -137,6 +155,15 @@ vx-bench clean --older-than "30 days" [options] - `--keep-labeled`: Don't delete labeled runs (default: true) - `--dry-run, -n`: Show what would be deleted +## CI Benchmark Matrices + +CI benchmark entries live in `bench_orchestrator/benchmarks.py`. The `vx-bench matrix` command +renders one of the `develop`, `pr`, `pr-full`, `nightly`, or `vortex` presets as the JSON expected +by GitHub Actions. + +When adding coverage, update the benchmark declarations and the expected preset membership in +`tests/test_matrix.py`. + ## Example Workflows ### 1. Basic Performance Comparison diff --git a/bench-orchestrator/bench_orchestrator/benchmarks.py b/bench-orchestrator/bench_orchestrator/benchmarks.py new file mode 100644 index 00000000000..d75a0a4c307 --- /dev/null +++ b/bench-orchestrator/bench_orchestrator/benchmarks.py @@ -0,0 +1,144 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright the Vortex contributors + +"""SQL benchmark declarations used by CI matrices. + +Edit this file when changing benchmark coverage. Matrix rendering lives in +``bench_orchestrator.matrix`` so workflow shape and benchmark coverage do not drift together. +""" + +from .config import Benchmark, Engine, Format +from .matrix import ( + DEFAULTS, + STANDARD, + BenchmarkDef, + BenchmarkGroup, + Storage, + df, + duck, +) + + +def _tpch( + scale_factor: float | int, + storage: Storage, + *, + group: BenchmarkGroup, + run_in_pr: bool = True, + iterations: int | None = 10, +) -> BenchmarkDef: + suffix = "" if scale_factor in {1, 100} else f"-{int(scale_factor)}" + if storage is Storage.NVME: + target_set = df( + Format.ARROW, + Format.PARQUET, + Format.VORTEX, + Format.VORTEX_COMPACT, + Format.LANCE, + ) | duck(Format.PARQUET, Format.VORTEX, Format.VORTEX_COMPACT, Format.DUCKDB) + local_dir = None + remote_key = None + else: + target_set = STANDARD + local_dir = f"vortex-bench/data/tpch/{scale_factor:.1f}" + remote_key = f"tpch/{scale_factor:.1f}" + + name = f"TPC-H on {storage.label}" if scale_factor == 100 else f"TPC-H SF={scale_factor:g} on {storage.label}" + return BenchmarkDef( + id=f"tpch-{storage.value}{suffix}", + benchmark=Benchmark.TPCH, + name=name, + targets=target_set, + storage=storage, + scale_factor=scale_factor, + iterations=iterations, + group=group, + run_in_pr=run_in_pr, + local_dir=local_dir, + remote_key=remote_key, + ) + + +def _clickbench(benchmark: Benchmark, name: str) -> BenchmarkDef: + return BenchmarkDef( + id=f"{benchmark.value}-nvme", + benchmark=benchmark, + name=name, + targets=df(Format.PARQUET, Format.VORTEX, Format.VORTEX_COMPACT, Format.LANCE) + | duck(Format.PARQUET, Format.VORTEX, Format.VORTEX_COMPACT, Format.DUCKDB), + pr_targets=DEFAULTS | duck(Format.DUCKDB), + ) + + +def _fineweb(storage: Storage) -> BenchmarkDef: + if storage is Storage.NVME: + return BenchmarkDef( + id="fineweb", + benchmark=Benchmark.FINEWEB, + name="FineWeb NVMe", + targets=STANDARD, + scale_factor=100, + ) + return BenchmarkDef( + id="fineweb-s3", + benchmark=Benchmark.FINEWEB, + name="FineWeb S3", + targets=STANDARD, + storage=Storage.S3, + scale_factor=100, + local_dir="vortex-bench/data/fineweb", + remote_key="fineweb", + ) + + +BENCHMARKS: list[BenchmarkDef] = [ + _clickbench(Benchmark.CLICKBENCH, "Clickbench on NVME"), + _clickbench(Benchmark.CLICKBENCH_SORTED, "Clickbench Sorted on NVME"), + _tpch(1.0, Storage.NVME, group=BenchmarkGroup.REGULAR), + _tpch(1.0, Storage.S3, group=BenchmarkGroup.REGULAR), + _tpch(10.0, Storage.NVME, group=BenchmarkGroup.REGULAR), + _tpch(10.0, Storage.S3, group=BenchmarkGroup.REGULAR, run_in_pr=False), + _tpch(100, Storage.NVME, group=BenchmarkGroup.NIGHTLY, iterations=None), + _tpch(100.0, Storage.S3, group=BenchmarkGroup.NIGHTLY, iterations=None), + BenchmarkDef( + id="tpcds-nvme", + benchmark=Benchmark.TPCDS, + name="TPC-DS SF=1 on NVME", + targets=STANDARD | duck(Format.DUCKDB), + scale_factor=1.0, + ), + BenchmarkDef( + id="statpopgen", + benchmark=Benchmark.STATPOPGEN, + name="Statistical and Population Genetics", + targets=STANDARD.only(Engine.DUCKDB), + scale_factor=100, + local_dir="vortex-bench/data/statpopgen", + ), + _fineweb(Storage.NVME), + _fineweb(Storage.S3), + BenchmarkDef( + id="polarsignals", + benchmark=Benchmark.POLARSIGNALS, + name="PolarSignals Profiling", + targets=df(Format.VORTEX), + scale_factor=1, + ), + BenchmarkDef( + id="appian-nvme", + benchmark=Benchmark.APPIAN, + name="Appian on NVME", + targets=STANDARD | duck(Format.DUCKDB), + pr_targets=DEFAULTS | duck(Format.DUCKDB), + run_in_pr=False, + iterations=10, + ), + BenchmarkDef( + id="vortex-queries", + benchmark=Benchmark.VORTEX_QUERIES, + name="Vortex queries", + targets=DEFAULTS, + group=BenchmarkGroup.VORTEX, + iterations=100, + ), +] diff --git a/bench-orchestrator/bench_orchestrator/cli.py b/bench-orchestrator/bench_orchestrator/cli.py index b9d7f9bb2ef..783c48c5adb 100644 --- a/bench-orchestrator/bench_orchestrator/cli.py +++ b/bench-orchestrator/bench_orchestrator/cli.py @@ -3,6 +3,7 @@ """CLI for benchmark orchestration.""" +import json import subprocess from contextlib import contextmanager from datetime import datetime, timedelta @@ -15,6 +16,7 @@ from rich.console import Console from rich.table import Table +from .benchmarks import BENCHMARKS from .comparison import analyzer from .comparison.reporter import pivot_comparison_table from .config import ( @@ -29,6 +31,7 @@ parse_targets_json, resolve_axis_targets, ) +from .matrix import MATRIX_PRESETS, resolve_matrix from .runner.builder import BenchmarkBuilder from .runner.executor import BenchmarkExecutor from .storage.store import ResultStore @@ -214,6 +217,30 @@ def prepare_data( raise typer.Exit(1) from exc +@app.command("matrix") +def matrix( + preset: Annotated[ + str | None, + typer.Argument(help="Matrix preset to render; omit to list available presets"), + ] = None, + list_presets: Annotated[bool, typer.Option("--list", help="List available presets and exit")] = False, + pretty: Annotated[bool, typer.Option("--pretty", help="Pretty-print the JSON output")] = False, +) -> None: + """Emit a GitHub Actions benchmark matrix.""" + if preset is None or list_presets: + for name, description in MATRIX_PRESETS.items(): + console.print(f"[bold cyan]{name}[/bold cyan]: {description}") + return + + if preset not in MATRIX_PRESETS: + known = ", ".join(MATRIX_PRESETS) + console.print(f"[red]Unknown matrix preset '{preset}'. Available: {known}[/red]") + raise typer.Exit(1) + + entries = resolve_matrix(preset, BENCHMARKS) + typer.echo(json.dumps(entries, indent=2 if pretty else None)) + + @app.command() def run( benchmark: Annotated[Benchmark, typer.Argument(help="Benchmark suite to run")], diff --git a/bench-orchestrator/bench_orchestrator/matrix.py b/bench-orchestrator/bench_orchestrator/matrix.py new file mode 100644 index 00000000000..5d59a7017bd --- /dev/null +++ b/bench-orchestrator/bench_orchestrator/matrix.py @@ -0,0 +1,224 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright the Vortex contributors + +"""Render benchmark definitions as GitHub Actions matrices.""" + +from __future__ import annotations + +from collections.abc import Iterable +from dataclasses import dataclass +from enum import Enum + +from .config import Benchmark, BenchmarkTarget, Engine, Format + + +class Storage(Enum): + """Where a benchmark's data lives when it runs.""" + + NVME = "nvme" + S3 = "s3" + + @property + def label(self) -> str: + """Human-facing name used in benchmark display names.""" + return "NVME" if self is Storage.NVME else "S3" + + +class BenchmarkGroup(Enum): + """A separately scheduled group of benchmark definitions.""" + + REGULAR = "regular" + NIGHTLY = "nightly" + VORTEX = "vortex" + + +def _dedupe(targets: Iterable[BenchmarkTarget]) -> tuple[BenchmarkTarget, ...]: + """Normalize and de-duplicate targets, preserving first-seen order.""" + return tuple(dict.fromkeys(target.normalized() for target in targets)) + + +@dataclass(frozen=True) +class TargetSet: + """An ordered set of engine/format targets with small set algebra.""" + + targets: tuple[BenchmarkTarget, ...] = () + + def __post_init__(self) -> None: + object.__setattr__(self, "targets", _dedupe(self.targets)) + + def __or__(self, other: TargetSet) -> TargetSet: + """Return the ordered union of two target sets.""" + return TargetSet(self.targets + other.targets) + + def only(self, *engines: Engine) -> TargetSet: + """Restrict the target set to the given engines.""" + keep = set(engines) + return TargetSet(tuple(target for target in self.targets if target.engine in keep)) + + def formats(self) -> list[Format]: + """Return referenced formats in first-seen order.""" + return list(dict.fromkeys(target.format for target in self.targets)) + + def __iter__(self): + return iter(self.targets) + + def __len__(self) -> int: + return len(self.targets) + + +def targets(engine: Engine, *formats: Format) -> TargetSet: + """Build targets for one engine across several formats.""" + return TargetSet(tuple(BenchmarkTarget(engine=engine, format=fmt) for fmt in formats)) + + +def df(*formats: Format) -> TargetSet: + """Build DataFusion targets across several formats.""" + return targets(Engine.DATAFUSION, *formats) + + +def duck(*formats: Format) -> TargetSet: + """Build DuckDB targets across several formats.""" + return targets(Engine.DUCKDB, *formats) + + +STANDARD = df(Format.PARQUET, Format.VORTEX, Format.VORTEX_COMPACT) | duck( + Format.PARQUET, Format.VORTEX, Format.VORTEX_COMPACT +) +DEFAULTS = df(Format.PARQUET, Format.VORTEX) | duck(Format.PARQUET, Format.VORTEX) +_NOT_GENERATED = frozenset({Format.ARROW, Format.LANCE}) +_FORMAT_ORDER = ( + Format.ARROW, + Format.PARQUET, + Format.VORTEX, + Format.VORTEX_COMPACT, + Format.VORTEX_NATIVE, + Format.DUCKDB, + Format.LANCE, +) + + +@dataclass(frozen=True) +class BenchmarkDef: + """A benchmark and the canonical target superset it can run.""" + + id: str + benchmark: Benchmark + name: str + targets: TargetSet + storage: Storage = Storage.NVME + scale_factor: float | int | None = None + iterations: int | None = None + group: BenchmarkGroup = BenchmarkGroup.REGULAR + pr_targets: TargetSet | None = None + run_in_pr: bool = True + local_dir: str | None = None + remote_key: str | None = None + + @property + def subcommand(self) -> str: + """Return the ``vx-bench`` subcommand for this benchmark.""" + return self.benchmark.value + + +def _default_targets(benchmark: BenchmarkDef) -> TargetSet: + """Return the cheap default targets supported by a benchmark.""" + return TargetSet(tuple(target for target in benchmark.targets if target in DEFAULTS.targets)) + + +def _pr_full_targets(benchmark: BenchmarkDef) -> TargetSet: + """Return the full target set supported by pull-request runners.""" + if benchmark.pr_targets is not None: + return benchmark.pr_targets + return TargetSet(tuple(target for target in benchmark.targets if target.format is not Format.LANCE)) + + +def _pr_targets(benchmark: BenchmarkDef) -> TargetSet: + """Return the cheap PR lane while preserving Arrow coverage for regular TPC-H.""" + allowed = set(DEFAULTS) + if benchmark.benchmark is Benchmark.TPCH: + allowed.update(df(Format.ARROW)) + return TargetSet(tuple(target for target in _pr_full_targets(benchmark) if target in allowed)) + + +MATRIX_PRESETS = { + "develop": "Every regular SQL benchmark at full target coverage.", + "pr": "The quicker pull-request SQL benchmark matrix.", + "pr-full": "Every regular SQL benchmark at full PR target coverage.", + "nightly": "Large-scale SF=100 TPC-H on NVMe and S3 at default targets.", + "vortex": "The Vortex query suite run on pushes and pull requests.", +} + + +def _include_benchmark(preset: str, benchmark: BenchmarkDef) -> bool: + if preset == "nightly": + return benchmark.group is BenchmarkGroup.NIGHTLY + if preset == "vortex": + return benchmark.group is BenchmarkGroup.VORTEX + return benchmark.group is BenchmarkGroup.REGULAR and (preset != "pr" or benchmark.run_in_pr) + + +def _targets_for(preset: str, benchmark: BenchmarkDef) -> TargetSet: + if preset in {"develop", "vortex"}: + return benchmark.targets + if preset == "pr-full": + return _pr_full_targets(benchmark) + if preset == "pr": + return _pr_targets(benchmark) + return _default_targets(benchmark) + + +def _valid_for_storage(target_set: TargetSet, storage: Storage) -> TargetSet: + """Drop targets that are invalid for the storage backend.""" + if storage is Storage.S3: + return TargetSet(tuple(target for target in target_set if target.format is not Format.LANCE)) + return target_set + + +def _data_formats(target_set: TargetSet) -> list[Format]: + """Return data formats that the data-generation step must produce.""" + present = set(target_set.formats()) + return [fmt for fmt in _FORMAT_ORDER if fmt in present and fmt not in _NOT_GENERATED] + + +def _matrix_entry(benchmark: BenchmarkDef, run_targets: TargetSet, data_format_targets: TargetSet) -> dict[str, object]: + """Build one GitHub Actions ``include`` entry.""" + entry: dict[str, object] = { + "id": benchmark.id, + "subcommand": benchmark.subcommand, + "name": benchmark.name, + "targets": [target.to_dict() for target in run_targets], + "data_formats": [fmt.value for fmt in _data_formats(data_format_targets)], + } + if benchmark.scale_factor is not None: + entry["scale_factor"] = str(benchmark.scale_factor) + if benchmark.iterations is not None: + entry["iterations"] = str(benchmark.iterations) + if benchmark.local_dir is not None: + entry["local_dir"] = benchmark.local_dir + if benchmark.remote_key is not None: + entry["remote_key"] = benchmark.remote_key + return entry + + +def resolve_matrix(preset: str, benchmarks: Iterable[BenchmarkDef]) -> list[dict[str, object]]: + """Render a named preset as GitHub Actions matrix entries.""" + if preset not in MATRIX_PRESETS: + known = ", ".join(MATRIX_PRESETS) + raise ValueError(f"Unknown matrix preset {preset!r}. Available: {known}") + + entries: list[dict[str, object]] = [] + seen_ids: set[str] = set() + for benchmark in benchmarks: + if not _include_benchmark(preset, benchmark): + continue + if benchmark.id in seen_ids: + raise ValueError(f"Duplicate benchmark ID {benchmark.id!r} in matrix preset {preset!r}") + seen_ids.add(benchmark.id) + + run_targets = _valid_for_storage(_targets_for(preset, benchmark), benchmark.storage) + if len(run_targets) == 0: + raise ValueError(f"Benchmark {benchmark.id!r} resolved to no runnable targets") + data_format_targets = benchmark.targets if preset == "pr-full" else run_targets + data_format_targets = _valid_for_storage(data_format_targets, benchmark.storage) + entries.append(_matrix_entry(benchmark, run_targets, data_format_targets)) + return entries diff --git a/bench-orchestrator/tests/test_matrix.py b/bench-orchestrator/tests/test_matrix.py new file mode 100644 index 00000000000..4d8490fce01 --- /dev/null +++ b/bench-orchestrator/tests/test_matrix.py @@ -0,0 +1,103 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright the Vortex contributors + +"""Tests for CI benchmark matrices.""" + +import json +from dataclasses import replace +from typing import cast + +import pytest +from bench_orchestrator import cli as cli_module +from bench_orchestrator.benchmarks import BENCHMARKS +from bench_orchestrator.matrix import MATRIX_PRESETS, TargetSet, resolve_matrix +from typer.testing import CliRunner + +runner = CliRunner() + +REGULAR_IDS = ( + "clickbench-nvme", + "clickbench-sorted-nvme", + "tpch-nvme", + "tpch-s3", + "tpch-nvme-10", + "tpch-s3-10", + "tpcds-nvme", + "statpopgen", + "fineweb", + "fineweb-s3", + "polarsignals", + "appian-nvme", +) +EXPECTED_IDS = { + "develop": REGULAR_IDS, + "pr": tuple(benchmark_id for benchmark_id in REGULAR_IDS if benchmark_id not in {"tpch-s3-10", "appian-nvme"}), + "pr-full": REGULAR_IDS, + "nightly": ("tpch-nvme", "tpch-s3"), + "vortex": ("vortex-queries",), +} + + +def _entries(preset: str) -> list[dict[str, object]]: + return resolve_matrix(preset, BENCHMARKS) + + +def _targets(entry: dict[str, object]) -> set[tuple[str, str]]: + targets = cast("list[dict[str, str]]", entry["targets"]) + return {(target["engine"], target["format"]) for target in targets} + + +@pytest.mark.parametrize(("preset", "expected_ids"), EXPECTED_IDS.items()) +def test_matrix_presets(preset: str, expected_ids: tuple[str, ...]) -> None: + entries = _entries(preset) + ids = [entry["id"] for entry in entries] + + assert tuple(ids) == expected_ids + assert len(ids) == len(set(ids)) + assert all(entry["targets"] for entry in entries) + assert "${{" not in json.dumps(entries) + + +def test_pr_target_selection() -> None: + develop = {entry["id"]: entry for entry in _entries("develop")} + pr = {entry["id"]: entry for entry in _entries("pr")} + pr_full = {entry["id"]: entry for entry in _entries("pr-full")} + + assert _targets(pr["tpch-nvme"]) == { + ("datafusion", "arrow"), + ("datafusion", "parquet"), + ("datafusion", "vortex"), + ("duckdb", "parquet"), + ("duckdb", "vortex"), + } + assert ("datafusion", "lance") in _targets(develop["tpch-nvme"]) + assert all(("datafusion", "lance") not in _targets(entry) for entry in pr_full.values()) + assert "vortex-compact" in cast("list[str]", pr_full["clickbench-nvme"]["data_formats"]) + + +def test_resolver_rejects_empty_targets() -> None: + benchmark = replace(BENCHMARKS[0], id="empty", targets=TargetSet()) + + with pytest.raises(ValueError, match="Benchmark 'empty' resolved to no runnable targets"): + _ = resolve_matrix("develop", [benchmark]) + + +def test_resolver_rejects_duplicate_ids() -> None: + benchmark = BENCHMARKS[0] + + with pytest.raises(ValueError, match="Duplicate benchmark ID 'clickbench-nvme'"): + _ = resolve_matrix("develop", [benchmark, replace(benchmark, name="Duplicate")]) + + +def test_matrix_command() -> None: + result = runner.invoke(cli_module.app, ["matrix", "develop"]) + assert result.exit_code == 0 + assert json.loads(result.stdout) == _entries("develop") + + result = runner.invoke(cli_module.app, ["matrix", "does-not-exist"]) + assert result.exit_code == 1 + assert "Unknown matrix preset" in result.stdout + + +def test_preset_names_are_stable() -> None: + assert set(MATRIX_PRESETS) == set(EXPECTED_IDS)