Pengujian regresi performa pada pipeline CI/CD sering menghasilkan alarm palsu (flaky test). Masalah utama berakar pada metrik yang digunakan: durasi waktu nyata (wall-clock time) lewat std::chrono. Di lingkungan virtualisasi bersama (shared runner), metrik waktu dipengaruhi oleh faktor eksternal di luar efisiensi algoritma.
Akar Masalah Flaky Benchmark di Lingkungan CI
Pengukuran waktu eksekusi murni berbasis nanodetik tidak deterministik karena beberapa faktor sistem operasi dan perangkat keras:
- Dynamic Voltage and Frequency Scaling (DVFS): Fitur seperti Intel Turbo Boost atau AMD Precision Boost mengubah frekuensi inti prosesor secara dinamis tergantung beban termal dan daya.
- Noisy Neighbors: Pada VM multi-tenant, instruksi proses lain merebut bandwidth memori, L3 cache, dan jatah waktu CPU scheduler.
- Context Switching dan Penjadwalan: Kernel memindahkan thread antar-inti secara acak, memicu cache invalidation dan penalti interupsi (IRQ).
Solusi deterministik untuk mengukur efisiensi kode adalah beralih dari satuan waktu ke metrik eksekusi perangkat keras menggunakan Performance Monitoring Unit (PMU).
PMU: Menghitung Siklus dan Instruksi Perangkat Keras
PMU adalah register perangkat keras di dalam CPU yang mencatat event internal tanpa overhead software runtime. Dua metrik paling stabil untuk mendeteksi regresi kode adalah:
PERF_COUNT_HW_INSTRUCTIONS: Jumlah instruksi mesin yang dieksekusi. Bersifat sangat deterministik untuk jalur eksekusi kode yang sama.PERF_COUNT_HW_CPU_CYCLES: Siklus aktif CPU yang dihabiskan untuk thread terkait. Tidak terpengaruh saat thread berada dalam kondisi tidur (sleep) atau preempted oleh thread lain jika diisolasi ke PID proses.PERF_COUNT_HW_CACHE_MISSES: Mendeteksi degradasi struktur data yang merusak lokalitas memori (data locality).
Implementasi Test Harness Minimal via perf_event_open
Linux menyediakan syscall perf_event_open untuk berinteraksi langsung dengan register PMU tanpa dependensi pustaka luar. Harness berikut mengukur siklus CPU pada thread pengujian dan memverifikasi batas atas siklus eksekusi.
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <linux/perf_event.h>
#include <sys/ioctl.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <vector>
#include <numeric>
// ponytail: syscall wrapper tanpa abstraction layer, upgrade ke libpfm4 jika butuh dynamic event string.
static long perf_event_open(struct perf_event_attr *hw_event, pid_t pid,
int cpu, int group_fd, unsigned long flags) {
return syscall(__NR_perf_event_open, hw_event, pid, cpu, group_fd, flags);
}
class CycleCounter {
int fd = -1;
public:
CycleCounter() {
struct perf_event_attr pe;
std::memset(&pe, 0, sizeof(pe));
pe.type = PERF_TYPE_HARDWARE;
pe.size = sizeof(pe);
pe.config = PERF_COUNT_HW_CPU_CYCLES;
pe.disabled = 1;
pe.exclude_kernel = 1; // Ukur user space saja
pe.exclude_hv = 1;
// Ukur calling thread saja (pid 0), inti manapun (-1)
fd = perf_event_open(&pe, 0, -1, -1, 0);
if (fd == -1) {
std::cerr << "Gagal membuka PMU counter. Cek perf_event_paranoid.\n";
std::exit(2);
}
}
~CycleCounter() {
if (fd != -1) close(fd);
}
void start() {
ioctl(fd, PERF_EVENT_IOC_RESET, 0);
ioctl(fd, PERF_EVENT_IOC_ENABLE, 0);
}
uint64_t stop() {
ioctl(fd, PERF_EVENT_IOC_DISABLE, 0);
uint64_t count = 0;
if (read(fd, &count, sizeof(count)) == -1) {
return 0;
}
return count;
}
};
void target_algorithm(std::vector<int>& data) {
for (auto& val : data) {
val = (val * 1664525 + 1013904223); // LCG step
}
}
int main() {
std::vector<int> buffer(10000, 42);
CycleCounter pmu;
pmu.start();
target_algorithm(buffer);
uint64_t cycles = pmu.stop();
std::cout << "Siklus CPU terpakai: " << cycles << "\n";
// Batas anggaran siklus: Regresi algoritmik terdeteksi jika siklus melampaui baseline + margin
constexpr uint64_t MAX_ALLOWED_CYCLES = 35000;
if (cycles > MAX_ALLOWED_CYCLES) {
std::cerr << "FAIL: Regresi siklus terdeteksi! Limit: "
<< MAX_ALLOWED_CYCLES << ", Realisasi: " << cycles << "\n";
return 1;
}
std::cout << "PASS: Performa memenuhi batas siklus deterministik.\n";
return 0;
}
Dilewati: tracking hierarki multi-event file descriptor group. Tambahkan ketika memverifikasi korelasi rasio Cycles-per-Instruction (CPI) atau Cache Misses sekaligus dalam satu run.
Alternatif: Custom Counters Google Benchmark
Jika proyek sudah menggunakan framework Google Benchmark, integrasikan PMU via API register custom counters tanpa menulis loop benchmark manual:
#include <benchmark/benchmark.h>
// Lazy alternative: manfaatkan built-in hooks jika framework sudah terpasang.
static void BM_AlgorithmicTest(benchmark::State& state) {
std::vector<int> buffer(1000, 1);
for (auto _ : state) {
for (auto& x : buffer) {
benchmark::DoNotOptimize(x *= 3);
}
}
// Google benchmark dapat membaca hardware counter melalui flag:
// --benchmark_perf_counters=CYCLES,INSTRUCTIONS
}
BENCHMARK(BM_AlgorithmicTest);
BENCHMARK_MAIN();
Konfigurasi Runner CI: Core Pinning dan Hak Akses
Pengukuran PMU pada bare-metal runner atau dedicated CI agent tetap memerlukan isolasi sistem operasi agar cache L1/L2 tidak terganggu thread daemon lain.
1. Izin Akses PMU Tanpa Root
Kernel Linux secara default membatasi akses event hardware untuk pengguna unprivileged. Turunkan level restriksi pada host CI:
sudo sysctl -w kernel.perf_event_paranoid=1
Nilai 1 mengizinkan perekaman event CPU user-space tanpa privilege root.
2. Core Pinning via taskset
Cegah kernel scheduler memindahkan thread uji ke inti CPU lain selama proses berlangsung:
# Jalankan biner uji terisolasi pada CPU Core 2
taskset -c 2 ./cycle_regression_test
3. Isolasi Kernel via isolcpus (Opsional, Dedicated Bare-metal)
Pada dedicated server CI, kecualikan core tertentu dari scheduler global kernel melalui boot parameters di GRUB (/etc/default/grub):
GRUB_CMDLINE_LINUX_DEFAULT="isolcpus=2,3 nohz_full=2,3 rcu_nocbs=2,3"
Kombinasi isolcpus dan taskset memastikan runner mengeksekusi kode uji secara deterministik tanpa interupsi OS timer ticks.
Batasan Teknis dan Trade-offs
- Virtual PMU di VM Cloud: Instance cloud publik standar (misal AWS EC2 t-series atau GCP e2) sering kali menonaktifkan virtual PMU (vPMU). Pengujian PMU memerlukan bare-metal instance (misal AWS
.metal) atau dedicated hypervisor dengan vPMU aktif. - Arsitektur Mikro Berbeda: Nilai
MAX_ALLOWED_CYCLESbersifat spesifik per generasi CPU (misal Intel Zen 4 vs Skylake). Definisikan batas baseline berdasarkan arsitektur agen runner CI yang konsisten, atau gunakan rasio instruksi (INSTRUCTIONS) yang bersifat deterministik platform-agnostik pada kompilasi biner yang identik.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!