Saat tim mengembangkan struktur data cepat atau komponen berorientasi performa, masalah utamanya bukan sekadar bagaimana menjalankan benchmark, tetapi bagaimana membuat hasil benchmark cukup stabil untuk dipakai sebagai sinyal CI. Tanpa kontrol lingkungan, baseline yang konsisten, dan aturan evaluasi yang jelas, angka benchmark di CI mudah berubah karena noise, throttling CPU, atau variasi runner, bukan karena perubahan kode.

Dalam konteks OpenZL sebagai proyek yang mengevaluasi struktur data, algoritma, atau library performa, pendekatan yang paling berguna adalah memperlakukan benchmark sebagai bagian dari developer tooling dan automation. Artinya, benchmark tidak dijalankan seragam di semua event, melainkan dipisah menjadi smoke benchmark untuk validasi cepat dan full benchmark untuk analisis tren dan regresi yang lebih akurat.

Mengapa CI benchmark sering tidak stabil

CI benchmark gagal menjadi alat pengambilan keputusan ketika tim menganggapnya sama dengan unit test. Unit test biasanya bersifat biner: lulus atau gagal. Benchmark berbeda, karena hasilnya dipengaruhi banyak variabel di luar kode aplikasi.

Sumber noise yang paling umum

  • Runner bersama: mesin CI dipakai banyak job lain sehingga CPU, cache, dan I/O tidak konsisten.
  • Dynamic CPU scaling: frekuensi CPU berubah tergantung beban dan kebijakan host.
  • Warmup tidak memadai: runtime, allocator, JIT, atau cache belum stabil saat pengukuran dimulai.
  • Dataset berubah: ukuran input, distribusi data, atau seed acak tidak dikunci.
  • Perubahan dependency/toolchain: update compiler, runtime, atau library memengaruhi hasil.
  • Metrik terlalu sempit: hanya menyimpan satu angka rata-rata tanpa variansi, p95, atau jumlah iterasi.

Karena itu, target realistis dari CI benchmark bukan menghasilkan angka yang identik di setiap run, melainkan menghasilkan sinyal yang cukup repeatable untuk mendeteksi perubahan bermakna.

Prinsip desain benchmark yang repeatable

1. Uji skenario yang representatif, bukan sintetis berlebihan

Untuk evaluasi struktur data cepat di OpenZL, benchmark sebaiknya merepresentasikan operasi yang benar-benar penting: misalnya insert, lookup, iterasi, merge, atau update pada ukuran dataset tertentu. Hindari benchmark yang terlalu kecil sehingga overhead framework benchmark lebih dominan daripada operasi inti.

Praktiknya, pilih beberapa profil input yang jelas:

  • Kecil untuk smoke benchmark di PR.
  • Menengah untuk validasi harian.
  • Besar untuk release atau investigasi performa.

2. Kunci input dan sumber acak

Jika benchmark menggunakan data acak, gunakan seed tetap dan simpan parameter generator. Tujuannya bukan menghilangkan semua variasi, tetapi memastikan perubahan hasil tidak berasal dari distribusi input yang berubah antar-run.

benchmark_case: map_lookup_dense
seed: 42
input_size: 100000
key_distribution: uniform
iterations: 30
warmup_iterations: 10

3. Pisahkan warmup dan measurement

Warmup penting terutama bila ada cache, inisialisasi allocator, atau optimasi runtime. Jalankan beberapa iterasi warmup yang tidak dihitung, lalu baru ukur. Jika framework benchmark yang dipakai sudah menyediakan fase warmup, manfaatkan itu dan jangan mencampur logika warmup ke hasil final.

4. Ukur lebih dari satu metrik

Untuk struktur data dan library performa, satu metrik tidak cukup. Minimal simpan:

  • Waktu eksekusi per operasi atau total skenario.
  • Throughput bila relevan.
  • Variansi atau simpangan baku.
  • Jumlah sampel dan iterasi.
  • Ukuran input.
  • Informasi environment seperti commit, branch, runner label, dan toolchain.

Tanpa metadata ini, hasil benchmark sulit dibandingkan secara adil.

5. Gunakan perbandingan relatif, bukan angka absolut saja

Angka absolut dari CI sering dipengaruhi mesin. Karena itu, keputusan lulus/gagal lebih aman bila memakai perubahan relatif terhadap baseline yang setara, misalnya terhadap commit utama terakhir pada runner dan profil benchmark yang sama.

Arsitektur workflow CI benchmark yang stabil

Untuk repo kecil sampai menengah, workflow yang praktis biasanya dibagi menjadi tiga lapis:

  1. PR benchmark: cepat, murah, fokus deteksi kasar.
  2. Nightly benchmark: lebih lengkap, dipakai untuk tren dan investigasi regresi.
  3. Release benchmark: hasil yang lebih ketat untuk dokumentasi performa dan validasi sebelum rilis.

Smoke benchmark di Pull Request

Smoke benchmark bertujuan menjawab: apakah perubahan ini jelas lebih lambat atau merusak karakteristik dasar? Jangan jalankan seluruh matriks benchmark di setiap PR, karena itu mahal dan justru meningkatkan noise akibat antrean panjang.

Karakteristik smoke benchmark:

  • Dataset kecil-menengah.
  • Jumlah iterasi terbatas.
  • Hanya skenario inti.
  • Waktu eksekusi singkat.
  • Threshold regresi longgar tapi berguna, misalnya untuk mendeteksi penurunan besar.

Full benchmark di Nightly

Nightly benchmark cocok untuk menghasilkan data yang lebih stabil karena tidak terikat latensi feedback PR. Di sini tim bisa menjalankan lebih banyak iterasi, beberapa ukuran input, dan mungkin runner yang lebih terkontrol. Hasil nightly idealnya disimpan agar bisa divisualisasikan per commit atau per hari.

Benchmark saat release

Release benchmark dipakai ketika tim memerlukan snapshot performa yang lebih dapat dipertanggungjawabkan. Misalnya sebelum menandai versi yang memperkenalkan struktur data baru di OpenZL, tim ingin membandingkan implementasi lama vs baru pada konfigurasi yang sama. Pada tahap ini, benchmark sebaiknya berjalan pada runner khusus atau self-hosted agar baseline lebih stabil.

Kontrol variabel lingkungan di CI

Jika tujuan Anda adalah membuat CI benchmark stabil, bagian ini lebih penting daripada pemilihan framework benchmark itu sendiri.

Prioritaskan runner yang konsisten

Runner self-hosted atau dedicated biasanya lebih stabil dibanding shared runner. Jika itu belum memungkinkan, setidaknya gunakan label runner yang sama untuk benchmark, dan jangan campur hasil dari kelas mesin yang berbeda ke dalam baseline yang sama.

Kunci toolchain dan dependency

Simpan informasi compiler, runtime, dependency lockfile, dan opsi build. Jika toolchain berubah, tandai sebagai baseline baru. Membandingkan benchmark lintas toolchain tanpa penanda khusus sering menghasilkan alarm palsu.

Kurangi pekerjaan lain dalam job benchmark

Jangan mencampur benchmark dengan lint, test, packaging, dan upload artifact berat dalam satu job jika tidak perlu. Benchmark sebaiknya dijalankan setelah build siap, sehingga gangguan I/O dan CPU lain minimal.

Gunakan mode build yang konsisten

Pastikan mode optimasi build yang dipakai selalu sama. Perbedaan mode debug dan release dapat membuat benchmark tidak bermakna. Selain itu, cache build memang berguna untuk mempercepat pipeline, tetapi jangan sampai cache membuat hasil benchmark sulit direproduksi atau tidak jelas asal binarinya.

Simpan metadata environment

Minimal sertakan metadata berikut di setiap hasil:

  • commit SHA
  • branch atau tag
  • event CI: PR, nightly, release
  • runner label atau machine class
  • OS dan arsitektur
  • toolchain atau runtime
  • timestamp

Metadata ini penting saat tim menyelidiki regresi yang terlihat hanya pada subset environment tertentu.

Baseline, threshold regresi, dan keputusan lulus/gagal

Jangan jadikan setiap fluktuasi sebagai kegagalan

Kesalahan umum adalah menolak PR hanya karena benchmark berubah 1-2%. Pada kebanyakan CI umum, perubahan sekecil itu sering tidak cukup dapat dipercaya. Lebih aman menggunakan threshold yang disesuaikan dengan stabilitas runner dan pentingnya benchmark.

Pilih baseline yang eksplisit

Beberapa opsi baseline yang umum:

  • Commit utama terakhir: sederhana, cocok untuk PR comparison.
  • Median beberapa run nightly terbaru: lebih tahan noise.
  • Baseline per release: cocok untuk laporan performa jangka panjang.

Untuk repo kecil-menengah, pendekatan yang praktis adalah:

  • PR membandingkan hasil terhadap baseline dari branch utama terbaru.
  • Nightly menyegarkan baseline statistik menggunakan beberapa run terakhir.
  • Release memakai baseline yang dibekukan untuk versi target.

Gunakan dua level threshold

Model yang cukup efektif:

  • Warning threshold: perubahan dicatat atau dikomentari di PR, tetapi tidak memblokir merge.
  • Fail threshold: perubahan cukup besar sehingga job gagal.

Ini membantu mengurangi false positive sekaligus tetap memberi visibilitas dini.

Contoh kebijakan: smoke benchmark di PR hanya gagal jika regresi melewati ambang besar dan konsisten pada metrik inti. Perubahan kecil cukup diberi komentar agar reviewer bisa mengecek konteks perubahan.

Format output metrik yang mudah diautomasi

Hasil benchmark sebaiknya tidak hanya berupa teks bebas di log. Gunakan format terstruktur agar mudah diparsing, dibandingkan, disimpan, dan divisualisasikan.

Contoh format JSON

{
  "suite": "openzl-data-structures",
  "scenario": "map_lookup_dense",
  "commit": "abc1234",
  "event": "pull_request",
  "runner": "self-hosted-x86_64",
  "toolchain": "pinned",
  "input_size": 100000,
  "warmup_iterations": 10,
  "measurement_iterations": 30,
  "metrics": {
    "time_ns_mean": 1250000,
    "time_ns_stddev": 43000,
    "throughput_ops_per_sec": 800000,
    "samples": 30
  }
}

Jika satu job menghasilkan banyak skenario, simpan sebagai array JSON atau NDJSON agar mudah diproses per baris.

Kolom minimum yang sebaiknya ada

  • nama suite dan skenario
  • parameter input
  • metrik utama dan variansi
  • commit dan branch
  • jenis pipeline
  • runner dan toolchain
  • status perbandingan terhadap baseline

Penyimpanan hasil dan visualisasi tren

Tanpa penyimpanan historis, benchmark CI hanya memberi snapshot sesaat. Padahal keputusan performa yang baik hampir selalu membutuhkan konteks tren.

Pilihan penyimpanan yang realistis

  • Artifact CI: paling mudah, cocok untuk investigasi jangka pendek.
  • Branch atau folder khusus hasil benchmark: sederhana, tetapi perlu disiplin agar repo tidak bengkak.
  • Object storage atau database time-series: lebih rapi untuk tren jangka panjang.
  • GitHub Pages atau dashboard statis: praktis untuk visualisasi jika data sudah diekspor ke JSON/CSV.

Untuk repo kecil-menengah, kombinasi yang sering cukup adalah:

  • Artifact CI untuk setiap run.
  • Job nightly yang menggabungkan hasil ke file historis terstruktur.
  • Dashboard sederhana yang memplot waktu per skenario terhadap commit atau tanggal.

Apa yang perlu divisualisasikan

  • tren mean atau median per skenario
  • rentang variansi
  • perubahan setelah merge tertentu
  • perbandingan beberapa implementasi
  • regresi yang berulang pada runner tertentu

Visualisasi sederhana pun cukup berguna asalkan konsisten. Tujuan utamanya adalah membantu tim membedakan anomali sesaat dari tren penurunan nyata.

Contoh workflow GitHub Actions

Contoh berikut menunjukkan pemisahan smoke benchmark untuk PR dan full benchmark untuk jadwal malam. Nama script dan command dibuat generik agar bisa disesuaikan dengan tooling benchmark yang dipakai tim OpenZL.

name: benchmark

on:
  pull_request:
  schedule:
    - cron: '0 2 * * *'
  workflow_dispatch:
  push:
    tags:
      - 'v*'

jobs:
  smoke-benchmark:
    if: github.event_name == 'pull_request'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Setup toolchain
        run: ./ci/setup-toolchain.sh
      - name: Build benchmark target
        run: ./ci/build-bench.sh
      - name: Run smoke benchmark
        run: ./ci/run-bench.sh --profile smoke --output bench-results.json
      - name: Compare with baseline
        run: ./ci/compare-bench.sh bench-results.json baseline/smoke.json
      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: smoke-benchmark-results
          path: bench-results.json

  nightly-benchmark:
    if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Setup toolchain
        run: ./ci/setup-toolchain.sh
      - name: Build benchmark target
        run: ./ci/build-bench.sh
      - name: Run full benchmark
        run: ./ci/run-bench.sh --profile full --output bench-results.json
      - name: Persist benchmark history
        run: ./ci/persist-bench.sh bench-results.json
      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: nightly-benchmark-results
          path: bench-results.json

  release-benchmark:
    if: startsWith(github.ref, 'refs/tags/v')
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Setup toolchain
        run: ./ci/setup-toolchain.sh
      - name: Build benchmark target
        run: ./ci/build-bench.sh
      - name: Run release benchmark
        run: ./ci/run-bench.sh --profile release --output bench-results.json
      - name: Compare with release baseline
        run: ./ci/compare-bench.sh bench-results.json baseline/release.json
      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: release-benchmark-results
          path: bench-results.json

Poin penting dari contoh di atas:

  • Job benchmark dipisahkan berdasarkan event.
  • Setup, build, run, compare, dan persist dibagi menjadi script terpisah agar mudah diulang lokal.
  • Output disimpan ke file terstruktur, bukan hanya log terminal.

Contoh GitLab CI

stages:
  - build
  - benchmark

build_bench:
  stage: build
  script:
    - ./ci/setup-toolchain.sh
    - ./ci/build-bench.sh
  artifacts:
    paths:
      - build/

smoke_benchmark:
  stage: benchmark
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
  script:
    - ./ci/run-bench.sh --profile smoke --output bench-results.json
    - ./ci/compare-bench.sh bench-results.json baseline/smoke.json
  artifacts:
    paths:
      - bench-results.json

nightly_benchmark:
  stage: benchmark
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
  script:
    - ./ci/run-bench.sh --profile full --output bench-results.json
    - ./ci/persist-bench.sh bench-results.json
  artifacts:
    paths:
      - bench-results.json

Strukturnya sama: bedakan jenis benchmark berdasarkan sumber pipeline, dan hindari menjalankan benchmark mahal di semua jalur.

Kapan benchmark dijalankan di PR, nightly, dan release

Di PR

  • Jalankan smoke benchmark untuk skenario inti.
  • Gunakan threshold lebih longgar.
  • Tampilkan ringkasan delta terhadap baseline di komentar PR atau summary job.
  • Hindari benchmark terlalu lama yang memperlambat review.

Di nightly

  • Jalankan full benchmark dengan lebih banyak iterasi.
  • Simpan hasil ke histori.
  • Gunakan untuk memperbarui baseline statistik.
  • Deteksi tren penurunan bertahap yang tidak terlihat di PR.

Di release

  • Jalankan suite benchmark yang lebih ketat dan terdokumentasi.
  • Pastikan environment semirip mungkin dengan baseline release.
  • Gunakan hasil untuk catatan rilis, validasi performa, atau keputusan rollback.

Anti-pattern umum

Menggagalkan PR karena noise kecil

Jika threshold terlalu agresif, tim akan mengabaikan benchmark karena terlalu banyak false alarm. Mulailah dari kebijakan konservatif, lalu ketatkan setelah melihat distribusi hasil nyata.

Mencampur hasil dari runner berbeda

Baseline dari mesin A tidak boleh dipakai untuk menilai hasil dari mesin B tanpa penyesuaian. Simpan identitas runner sebagai dimensi data.

Benchmark tanpa metadata

Angka benchmark tanpa commit, input, dan toolchain hampir tidak berguna untuk investigasi.

Menjalankan benchmark yang terlalu kecil

Untuk operasi yang sangat cepat, overhead harness, scheduler, atau timer bisa mendominasi. Solusinya adalah memperbesar batch kerja atau mengukur banyak operasi per iterasi.

Tidak ada jalur reproduksi lokal

Developer perlu bisa menjalankan command benchmark yang mirip dengan CI. Jika pipeline hanya hidup di YAML dan sulit direproduksi, investigasi regresi menjadi lambat.

Tips debugging saat hasil benchmark tiba-tiba berubah

  1. Bandingkan metadata: cek runner, branch, toolchain, dan input.
  2. Ulangi run: lihat apakah perubahan konsisten di run berikutnya.
  3. Jalankan benchmark lokal pada commit sebelum dan sesudah perubahan.
  4. Cek perubahan dependency atau opsi build yang ikut ter-merge.
  5. Periksa distribusi, bukan hanya mean: lonjakan variansi sering menunjukkan masalah environment.
  6. Lihat skenario spesifik: regresi bisa hanya muncul di ukuran input tertentu.

Checklist implementasi untuk repo kecil-menengah

  • Tentukan 3-10 skenario benchmark yang benar-benar penting.
  • Pisahkan profil smoke, full, dan release.
  • Kunci seed, ukuran input, dan mode build.
  • Simpan hasil dalam JSON atau format terstruktur lain.
  • Tambahkan metadata commit, runner, toolchain, dan event CI.
  • Gunakan baseline eksplisit per jenis benchmark.
  • Terapkan warning threshold dan fail threshold.
  • Upload artifact pada setiap run.
  • Simpan histori nightly untuk analisis tren.
  • Sediakan script lokal yang sama dengan command CI.
  • Dokumentasikan cara membaca hasil dan kapan alarm dianggap valid.

Penutup

Membangun CI benchmark stabil untuk evaluasi struktur data cepat tidak bergantung pada satu tool tertentu, melainkan pada disiplin desain benchmark, kontrol environment, dan automation yang konsisten. Dalam konteks OpenZL, pendekatan paling efektif adalah memisahkan benchmark cepat untuk PR dari benchmark penuh untuk nightly dan release, lalu menyimpan hasil secara terstruktur agar bisa dibandingkan terhadap baseline dan divisualisasikan dari waktu ke waktu.

Jika diterapkan dengan benar, benchmark CI tidak lagi menjadi angka acak di log pipeline, tetapi berubah menjadi alat engineering yang membantu tim mendeteksi regresi performa lebih awal, mengevaluasi perubahan struktur data dengan lebih objektif, dan membuat keputusan release dengan dasar yang lebih kuat.