minigraf 0.22.0

Zero-config, single-file, embedded graph database with bi-temporal Datalog queries
Documentation
name: Nightly Benchmarks

on:
  schedule:
    - cron: '0 2 * * *'  # 02:00 UTC nightly
  workflow_dispatch:       # allow manual runs

permissions:
  contents: read
  issues: write

jobs:
  compile:
    name: bench / compile
    runs-on: ubuntu-latest
    timeout-minutes: 120
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@stable

      - name: Cache cargo registry
        uses: actions/cache@v4
        with:
          path: |
            ~/.cargo/registry
            ~/.cargo/git
          key: ${{ runner.os }}-cargo-bench-${{ hashFiles('**/Cargo.lock') }}
          restore-keys: |
            ${{ runner.os }}-cargo-bench-

      - name: Cache bench target
        uses: actions/cache@v4
        with:
          path: target
          key: ${{ runner.os }}-bench-target-${{ hashFiles('**/Cargo.lock', 'benches/**', 'src/**/*.rs') }}

      - name: Compile benchmarks
        run: cargo bench --no-run

  benchmark:
    name: bench / ${{ matrix.name }}
    runs-on: ubuntu-latest
    needs: [compile]
    timeout-minutes: 360
    strategy:
      fail-fast: false   # one timeout must not cancel sibling jobs
      matrix:
        include:
          # ── Sequential: insert ──────────────────────────────────────────────
          - name: insert
            filter: "^insert/"
            file: bench_insert.txt
            threshold: "0.99"

          # ── Sequential: insert_file ─────────────────────────────────────────
          - name: insert_file
            filter: "^insert_file/"
            file: bench_insert_file.txt
            threshold: "0.99"

          # ── Sequential: query (includes query_extras) ───────────────────────
          - name: query
            filter: "^query/"
            file: bench_query.txt
            threshold: "0.99"

          # ── Sequential: time_travel ─────────────────────────────────────────
          - name: time_travel
            filter: "^time_travel/"
            file: bench_time_travel.txt
            threshold: "0.99"

          # ── Sequential: recursion ───────────────────────────────────────────
          - name: recursion
            filter: "^recursion/"
            file: bench_recursion.txt
            threshold: "0.99"

          # ── Sequential: open ────────────────────────────────────────────────
          - name: open
            filter: "^open/"
            file: bench_open.txt
            threshold: "0.99"

          # ── Sequential: checkpoint ──────────────────────────────────────────
          - name: checkpoint
            filter: "^checkpoint"
            file: bench_checkpoint.txt
            threshold: "0.99"

          # ── Concurrent (in-memory): readers ────────────────────────────────
          # threshold=0.999: concurrent benchmarks are noisy on shared CI runners
          - name: concurrent/readers
            filter: "^concurrent/readers/"
            file: bench_concurrent_readers.txt
            threshold: "0.999"

          # ── Concurrent (in-memory): readers_plus_writer ────────────────────
          - name: concurrent/readers_plus_writer
            filter: "^concurrent/readers_plus_writer/"
            file: bench_concurrent_mixed.txt
            threshold: "0.999"

          # ── Concurrent (in-memory): serialized_writers ─────────────────────
          - name: concurrent/serialized_writers
            filter: "^concurrent/serialized_writers/"
            file: bench_concurrent_writers.txt
            threshold: "0.999"

          # ── Concurrent (file-backed): readers ──────────────────────────────
          - name: concurrent_file/readers
            filter: "^concurrent_file/readers/"
            file: bench_concurrent_file_readers.txt
            threshold: "0.999"

          # ── Concurrent (file-backed): readers_plus_writer ──────────────────
          - name: concurrent_file/readers_plus_writer
            filter: "^concurrent_file/readers_plus_writer/"
            file: bench_concurrent_file_mixed.txt
            threshold: "0.999"

          # ── Concurrent (file-backed): serialized_writers ───────────────────
          - name: concurrent_file/serialized_writers
            filter: "^concurrent_file/serialized_writers/"
            file: bench_concurrent_file_writers.txt
            threshold: "0.999"

          # ── Concurrent: B+tree scan ─────────────────────────────────────────
          - name: concurrent_btree_scan
            filter: "^concurrent_btree_scan"
            file: bench_concurrent_btree_scan.txt
            threshold: "0.999"

          # ── Negation: not / not-join ────────────────────────────────────────
          - name: negation
            filter: "^negation/"
            file: bench_negation.txt
            threshold: "0.99"

          # ── Disjunction: or / or-join ───────────────────────────────────────
          - name: disjunction
            filter: "^disjunction/"
            file: bench_disjunction.txt
            threshold: "0.99"

          # ── Aggregation (includes aggregation_extras) ───────────────────────
          - name: aggregation
            filter: "^aggregation/"
            file: bench_aggregation.txt
            threshold: "0.99"

          # ── Expression filters / binding ────────────────────────────────────
          - name: expr
            filter: "^expr/"
            file: bench_expr.txt
            threshold: "0.99"

          # ── Window functions ────────────────────────────────────────────────
          - name: window
            filter: "^window/"
            file: bench_window.txt
            threshold: "0.99"

          # ── Temporal metadata ───────────────────────────────────────────────
          - name: temporal_metadata
            filter: "^temporal_metadata/"
            file: bench_temporal_metadata.txt
            threshold: "0.99"

          # ── User-defined functions ──────────────────────────────────────────
          - name: udf
            filter: "^udf/"
            file: bench_udf.txt
            threshold: "0.99"

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@stable

      - name: Cache cargo registry
        uses: actions/cache@v4
        with:
          path: |
            ~/.cargo/registry
            ~/.cargo/git
          key: ${{ runner.os }}-cargo-bench-${{ hashFiles('**/Cargo.lock') }}
          restore-keys: |
            ${{ runner.os }}-cargo-bench-

      - name: Restore bench target
        uses: actions/cache/restore@v4
        with:
          path: target
          key: ${{ runner.os }}-bench-target-${{ hashFiles('**/Cargo.lock', 'benches/**', 'src/**/*.rs') }}

      - name: Run benchmarks (${{ matrix.name }})
        run: |
          set -o pipefail
          CARGO_TERM_COLOR=never cargo bench -- "${{ matrix.filter }}" \
            2>&1 | tee ${{ matrix.file }}

      - name: Install Bencher CLI
        uses: bencherdev/bencher@v0.4.25

      - name: Upload to Bencher
        id: bencher
        continue-on-error: true
        run: |
          bencher run \
            --project minigraf \
            --token "${{ secrets.BENCHER_API_TOKEN }}" \
            --branch main \
            --testbed ubuntu-latest \
            --threshold-measure latency \
            --threshold-test t_test \
            --threshold-upper-boundary ${{ matrix.threshold }} \
            --err \
            --adapter rust_criterion \
            --file ${{ matrix.file }}

      - name: Open regression issue
        if: steps.bencher.outcome == 'failure'
        uses: actions/github-script@v6
        with:
          script: |
            const today = new Date().toISOString().slice(0, 10);
            const title = `Benchmark regression - ${today}`;

            const { data: openIssues } = await github.rest.issues.listForRepo({
              owner: context.repo.owner,
              repo: context.repo.repo,
              labels: 'performance',
              state: 'open',
              per_page: 100,
            });
            const existing = openIssues.filter(i =>
              i.title.startsWith('Benchmark regression')
            );
            if (existing.length > 0) {
              console.log(`Skipping: open regression issue already exists (#${existing[0].number})`);
              return;
            }

            const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
            await github.rest.issues.create({
              owner: context.repo.owner,
              repo: context.repo.repo,
              title: title,
              body: [
                `Bencher detected a performance regression in the nightly benchmark run (${{ matrix.name }}).`,
                '',
                `**Run:** ${runUrl}`,
                '',
                'Please review the Bencher dashboard for details on which benchmarks regressed.',
              ].join('\n'),
              labels: ['performance'],
            });