Optimasi yang sudah diraih dengan kerja keras tidak bertahan dengan sendirinya: setiap refactor kecil, upgrade dependency, atau penulisan ulang konfigurasi bisa melambatkan aplikasi tanpa membuat satu pun test fungsional gagal. Performance regression testing memperlakukan kinerja sebagai asersi kelas satu — benchmark berjalan otomatis di pipeline, hasilnya dibandingkan dengan baseline yang tersimpan, dan build gagal bila perubahannya melampaui ambang yang ditetapkan. Chapter penutup section ini merakit komponen pemeriksanya dari contoh yang bisa dihitung ulang: statistik pembanding yang tahan derau, kebijakan pengelolaan baseline, dan gerbang keputusan di CI.
Regresi yang Diam-diam #
Regresi performa jarang datang sebagai satu perubahan besar. Pola yang jauh lebih umum: setiap rilis membawa perlambatan kecil yang lolos review karena tidak ada yang salah secara fungsional. Hitungannya lurus — perlambatan 2% per rilis selama 26 rilis menghasilkan waktu respons sekitar 1,67 kali lipat awal, tanpa satu pun rilis yang tampak berbahaya. Sumber klasiknya beragam: parser yang ditulis ulang ternyata lebih lambat, upgrade library mengubah struktur data internal, konfigurasi GC yang hilang saat berkas deployment disusun ulang. Test fungsional memang hijau semuanya; hanya angka yang tahu.
Anatomi Pemeriksa Regresi #
| Komponen | Tugas | Wujud lazim |
|---|---|---|
| Suite benchmark | mengukur metrik yang sama setiap saat | class @Benchmark JMH |
| Baseline | angka referensi yang dikunci | hasil JSON JMH di-commit di repo |
| Statistik pembanding | median kandidat vs baseline + pita derau | skrip pemeriksa |
| Gerbang keputusan | PASS/FAIL yang menghentikan pipeline | test JUnit di job CI |
Dua keputusan statistik menentukan mutu gerbang ini. Pertama, bandingkan median dari beberapa iterasi, bukan satu angka — median tahan terhadap sampel ekstrem saat penjadwalan OS menyela pengukuran. Kedua, tetapkan pita derau (misalnya 10%): perubahan yang lebih kecil dari pita tersebut dianggap bising dan tidak boleh menggagalkan build. Menghubungkan JMH ke CI cukup dengan test runner biasa:
import org.junit.jupiter.api.Test;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class RegresiBenchTest {
private static final double PITA_DERAU = 0.10;
@Test
void tidakAdaRegresiMelampauiPita() throws Exception {
// Suite JMH dijalankan, hasil ditulis sebagai JSON
org.openjdk.jmh.Main.main(new String[] {
".*Bench.*", "-rf", "json", "-rff", "target/jmh-result.json"
});
// Median hasil dibandingkan median baseline yang tersimpan;
// bila delta melampaui PITA_DERAU, assertTrue menggagalkan pipeline.
String hasil = Files.readString(Path.of("target/jmh-result.json"));
String baseline = Files.readString(Path.of("baseline/jmh.json"));
assertTrue(bandingkanMedian(hasil, baseline) <= PITA_DERAU,
"regresi performa melampaui pita derau");
}
private static double bandingkanMedian(String hasil, String baseline) {
// ekstraksi median dari JSON hasil dan baseline
return 0.0; // disederhanakan untuk ilustrasi
}
}Logika pembandingnya bisa kita bedah dalam bentuk murni — tiga metrik beku, median, pita derau, dan keputusan CI:
import java.util.Arrays;
/**
* Pemeriksa regresi performa atas metrik beku (canned): median baseline
* dibandingkan median kandidat dengan pita derau, lalu keputusan CI diambil.
* Seluruh angka adalah data contoh deterministik, bukan pengukuran nyata.
*/
public class PemeriksaRegresi {
record Metrik(String nama, double[] baseline, double[] kandidat) {}
record Lingkungan(String runner, int core) {}
// Median: kuat terhadap sampel ekstrem, lebih stabil daripada satu angka
static double median(double[] x) {
double[] s = x.clone();
Arrays.sort(s);
int n = s.length;
return (n % 2 == 1) ? s[n / 2] : (s[n / 2 - 1] + s[n / 2]) / 2.0;
}
static String status(double delta, double pita) {
if (delta > pita) {
return "REGRESI";
}
if (delta < -pita) {
return "MEMBAIK";
}
return "STABIL";
}
public static void main(String[] args) {
double pita = 0.10; // perubahan di bawah 10% dianggap derau
Metrik[] semua = {
new Metrik("parseJson", new double[] {4.1, 4.3, 4.2}, new double[] {5.0, 5.2, 4.9}),
new Metrik("queryCache", new double[] {1.20, 1.25, 1.22}, new double[] {1.23, 1.29, 1.24}),
new Metrik("serializeDto", new double[] {8.4, 8.6, 8.5}, new double[] {6.9, 7.1, 7.0}),
};
System.out.println("== Pemeriksa regresi: median vs pita derau 10% ==");
System.out.printf("%-13s %9s %9s %8s %9s%n", "metrik", "baseline", "kandidat", "delta", "status");
int jumlahRegresi = 0;
for (Metrik m : semua) {
double base = median(m.baseline());
double kand = median(m.kandidat());
double delta = (kand - base) / base;
String st = status(delta, pita);
if (st.equals("REGRESI")) {
jumlahRegresi++;
}
System.out.printf("%-13s %9.2f %9.2f %+7.1f%% %9s%n",
m.nama(), base, kand, delta * 100, st);
}
System.out.println();
if (jumlahRegresi > 0) {
System.out.println("KEPUTUSAN CI: FAIL (" + jumlahRegresi
+ " regresi melampaui pita derau)");
} else {
System.out.println("KEPUTUSAN CI: PASS (tidak ada regresi melampaui pita derau)");
}
System.out.println();
// ---- Pemeriksaan lingkungan eksekusi ----
System.out.println("== Pemeriksaan lingkungan eksekusi ==");
Lingkungan asal = new Lingkungan("runner-a", 8);
Lingkungan kini = new Lingkungan("runner-b", 4);
System.out.println("baseline: " + asal);
System.out.println("kandidat : " + kini);
if (!asal.equals(kini)) {
System.out.println("status : TIDAK SAMPAI - angka lintas lingkungan");
System.out.println(" tidak dibandingkan");
}
System.out.println();
System.out.println("Kesimpulan: kunci baseline di repositori, bandingkan");
System.out.println("hanya dalam lingkungan sama, dan gagalkan build saat");
System.out.println("regresi melampaui pita derau.");
}
}Output yang Dihasilkan:
== Pemeriksa regresi: median vs pita derau 10% ==
metrik baseline kandidat delta status
parseJson 4.20 5.00 +19.0% REGRESI
queryCache 1.22 1.24 +1.6% STABIL
serializeDto 8.50 7.00 -17.6% MEMBAIK
KEPUTUSAN CI: FAIL (1 regresi melampaui pita derau)
== Pemeriksaan lingkungan eksekusi ==
baseline: Lingkungan[runner=runner-a, core=8]
kandidat : Lingkungan[runner=runner-b, core=4]
status : TIDAK SAMPAI - angka lintas lingkungan
tidak dibandingkan
Kesimpulan: kunci baseline di repositori, bandingkan
hanya dalam lingkungan sama, dan gagalkan build saat
regresi melampaui pita derau.Mari kita telaah bagian-bagian kuncinya:
- Baris 14-20 (
median): array disalin lalu diurutkan agar data masukan tidak berubah; nilai tengah diambil — untuk jumlah sampel genap, dua nilai tengah dirata-rata. Bandingkan dengan satu angka tunggal: cukup satu iterasi tersela penjadwalan untuk menyesatkan kesimpulan. - Baris 22-30 (
status): klasifikasi tiga arah — naik melampaui pita berarti REGRESI, turun melampaui pita berarti MEMBAIK, sisanya STABIL. Peningkatan juga perlu dilaporkan agar baseline bisa dipromosikan sadar. - Baris 41-54 (loop keputusan):
parseJsonnaik 19% dan membuat keputusan CI FAIL;queryCachenaik 1,6% — di dalam pita derau, sengaja tidak menggagalkan apa pun;serializeDtoturun 17,6% menjadi MEMBAIK. - Baris 63-72 (pemeriksaan lingkungan): baseline dari runner 8 core dibandingkan kandidat dari runner 4 core — perbandingan seperti itu tidak sah, maka statusnya TIDAK SAMPAI, bukan FAIL.
Mengelola Baseline dan Lingkungan #
Baseline adalah kontrak yang berubah lewat keputusan, bukan kebetulan. Praktik yang bekerja: hasil benchmark disimpan sebagai berkas di repositori; baseline baru di-commit sebagai perubahan tersendiri yang menyertakan penjelasan (misalnya “percepat serializeDto dengan record”) dan direview seperti kode; angka baseline tidak pernah ditimpa diam-diam oleh job CI. Lingkungan eksekusi ikut dikunci — runner yang sama, JDK yang sama, ukuran heap yang sama — karena angka dari mesin berbeda membandingkan dua hal sekaligus: kode dan perangkat kerasnya.
Dengan gerbang ini, siklus satu putaran penuh section ini tertutup: ukur dengan profil, buktikan dengan benchmark, setel dengan tuning, lalu kunci dengan regresi otomatis — dan saat angkanya memburuk, data sudah menunggu di riwayat pipeline. Perjalanan belajar Java kita berlanjut ke section berikutnya tentang fitur-fitur bahasa dan API terbaru di Modern Java.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
PemeriksaRegresi.java
Unduh berkasimport java.util.Arrays;
/**
* Pemeriksa regresi performa atas metrik beku (canned): median baseline
* dibandingkan median kandidat dengan pita derau, lalu keputusan CI diambil.
* Seluruh angka adalah data contoh deterministik, bukan pengukuran nyata.
*/
public class PemeriksaRegresi {
record Metrik(String nama, double[] baseline, double[] kandidat) {}
record Lingkungan(String runner, int core) {}
// Median: kuat terhadap sampel ekstrem, lebih stabil daripada satu angka
static double median(double[] x) {
double[] s = x.clone();
Arrays.sort(s);
int n = s.length;
return (n % 2 == 1) ? s[n / 2] : (s[n / 2 - 1] + s[n / 2]) / 2.0;
}
static String status(double delta, double pita) {
if (delta > pita) {
return "REGRESI";
}
if (delta < -pita) {
return "MEMBAIK";
}
return "STABIL";
}
public static void main(String[] args) {
double pita = 0.10; // perubahan di bawah 10% dianggap derau
Metrik[] semua = {
new Metrik("parseJson", new double[] {4.1, 4.3, 4.2}, new double[] {5.0, 5.2, 4.9}),
new Metrik("queryCache", new double[] {1.20, 1.25, 1.22}, new double[] {1.23, 1.29, 1.24}),
new Metrik("serializeDto", new double[] {8.4, 8.6, 8.5}, new double[] {6.9, 7.1, 7.0}),
};
System.out.println("== Pemeriksa regresi: median vs pita derau 10% ==");
System.out.printf("%-13s %9s %9s %8s %9s%n", "metrik", "baseline", "kandidat", "delta", "status");
int jumlahRegresi = 0;
for (Metrik m : semua) {
double base = median(m.baseline());
double kand = median(m.kandidat());
double delta = (kand - base) / base;
String st = status(delta, pita);
if (st.equals("REGRESI")) {
jumlahRegresi++;
}
System.out.printf("%-13s %9.2f %9.2f %+7.1f%% %9s%n",
m.nama(), base, kand, delta * 100, st);
}
System.out.println();
if (jumlahRegresi > 0) {
System.out.println("KEPUTUSAN CI: FAIL (" + jumlahRegresi
+ " regresi melampaui pita derau)");
} else {
System.out.println("KEPUTUSAN CI: PASS (tidak ada regresi melampaui pita derau)");
}
System.out.println();
// ---- Pemeriksaan lingkungan eksekusi ----
System.out.println("== Pemeriksaan lingkungan eksekusi ==");
Lingkungan asal = new Lingkungan("runner-a", 8);
Lingkungan kini = new Lingkungan("runner-b", 4);
System.out.println("baseline: " + asal);
System.out.println("kandidat : " + kini);
if (!asal.equals(kini)) {
System.out.println("status : TIDAK SAMPAI - angka lintas lingkungan");
System.out.println(" tidak dibandingkan");
}
System.out.println();
System.out.println("Kesimpulan: kunci baseline di repositori, bandingkan");
System.out.println("hanya dalam lingkungan sama, dan gagalkan build saat");
System.out.println("regresi melampaui pita derau.");
}
}