Percobaan ulang menyelamatkan baris data, tetapi tidak gratis: transformasi kini menanggung 38 percobaan untuk 32 event — seberapa cepat pipeline ini, dan macet di tahap mana? Jam dinding tidak menjawab dengan stabil karena JIT dan penjadwal thread mengubah angka antar eksekusi. Bagian 4 memakai model biaya deterministik — biaya per tahap tetap, item dari statistik eksekusi nyata — lalu menghitung throughput, profil kemacetan, dan dua strategi perbaikan sebelum menutup delapan proyek Course Java.

Model Biaya Tahap #

Model biaya menetapkan biaya per item sebagai angka tetap pada enum; jumlah item dibaca dari hasil eksekusi sungguhan, dan perkaliannya menjadi statistik yang stabil. Proporsinya meniru kenyataan: membaca dan menulis murah, memeriksa format sedang, transformasi paling mahal karena memanggil layanan tarif dan menanggung percobaan ulang:

TahapBiaya unit per item
baca2
validasi5
transformasi12
agregasi5
tulis2
/**
 * Tahap pipeline beserta biaya unit simulasi untuk memroses satu item.
 * Angka biaya adalah model tetap — bukan hasil pengukuran jam dinding —
 * sehingga seluruh perhitungan performa pada Bagian 4 bersifat deterministik.
 */
public enum TahapPipeline {
    BACA("baca", 2),
    VALIDASI("validasi", 5),
    TRANSFORMASI("transformasi", 12),
    AGREGASI("agregasi", 5),
    TULIS("tulis", 2);

    private final String label;
    private final long biayaUnit;

    TahapPipeline(String label, long biayaUnit) {
        this.label = label;
        this.biayaUnit = biayaUnit;
    }

    /** Nama tahap untuk laporan profil. */
    public String label() {
        return label;
    }

    /** Biaya unit simulasi per satu item yang diproses tahap ini. */
    public long biayaUnit() {
        return biayaUnit;
    }
}

Satu baris profil merangkai tahap dengan jumlah item yang diproses:

java BarisProfil.java
/**
 * Satu baris tabel profil: tahap, jumlah item yang diproses, dan biaya totalnya.
 * Item dihitung dari eksekusi pipeline nyata; biaya per item dari model tetap.
 */
public record BarisProfil(TahapPipeline tahap, long itemDiproses) {

    /** Biaya unit per item menurut model tahap. */
    public long biayaUnit() {
        return tahap().biayaUnit();
    }

    /** Total biaya tahap: jumlah item dikali biaya unit. */
    public long total() {
        return itemDiproses() * tahap().biayaUnit();
    }
}

Dua keputusan pentingnya:

  1. Biaya menempel pada tahap: mengubah model berarti mengubah satu angka, dan seluruh perhitungan ikut berubah.
  2. BarisProfil.total(): item dari statistik nyata dikali biaya model tetap — dua sumber stabil menghasilkan angka teruji.

Menghitung Throughput #

Throughput menjawab “berapa baris selesai per satuan biaya”: jumlah baris dibagi waktu unit. Pada jalur paralel, model membagi kerja tahap paralel — validasi dan transformasi — kepada sejumlah pekerja dengan pembagian dibulatkan ke atas; tahap baca, agregasi, dan tulis tetap utuh di main. Seluruh rumus tinggal di satu kelas utilitas:

import java.util.List;

/**
 * Pengukur performa deterministik atas hasil pipeline nyata.
 * Item per tahap dibaca dari statistik HasilPipeline (angka hasil eksekusi),
 * biaya per item dari model tetap TahapPipeline — tidak ada jam dinding yang
 * diukur, sehingga seluruh angka stabil antar eksekusi dan siap diuji.
 */
public final class ProfilPipeline {

    /** Biaya penjadwalan simulasi setiap task yang dikirim ke executor. */
    public static final long BIAYA_PENJADWALAN = 2;

    private ProfilPipeline() {
        // Kelas utilitas: seluruh anggota static.
    }

    /** Menyusun profil per tahap dari statistik hasil pipeline nyata. */
    public static List<BarisProfil> profil(HasilPipeline hasil) {
        long barisLaporan = hasil.laporan().size() + hasil.laporanGalat().size();
        return List.of(
                new BarisProfil(TahapPipeline.BACA, hasil.jumlahBaris()),
                new BarisProfil(TahapPipeline.VALIDASI, hasil.jumlahBaris()),
                new BarisProfil(TahapPipeline.TRANSFORMASI, hasil.jumlahPercobaan()),
                new BarisProfil(TahapPipeline.AGREGASI, hasil.jumlahSah()),
                new BarisProfil(TahapPipeline.TULIS, barisLaporan));
    }

    /** Total biaya seluruh tahap bila dikerjakan sekuensial. */
    public static long totalBiaya(List<BarisProfil> profil) {
        return profil.stream().mapToLong(BarisProfil::total).sum();
    }

    /** Tahap dengan total biaya terbesar — kandidat kemacetan utama. */
    public static BarisProfil kemacetan(List<BarisProfil> profil) {
        BarisProfil terbesar = profil.get(0);
        for (BarisProfil b : profil) {
            if (b.total() > terbesar.total()) {
                terbesar = b;
            }
        }
        return terbesar;
    }

    /** Total biaya tahap yang berjalan paralel per baris (validasi + transformasi). */
    public static long biayaTahapParalel(List<BarisProfil> profil) {
        return profil.stream()
                .filter(b -> b.tahap() == TahapPipeline.VALIDASI
                        || b.tahap() == TahapPipeline.TRANSFORMASI)
                .mapToLong(BarisProfil::total)
                .sum();
    }

    /** Total biaya tahap yang berjalan sekuensial di main (baca + agregasi + tulis). */
    public static long biayaTahapSekuensial(List<BarisProfil> profil) {
        return totalBiaya(profil) - biayaTahapParalel(profil);
    }

    /**
     * Simulasi waktu dinding bila tahap paralel dikerjakan W pekerja:
     * kerja paralel dibagi rata ke atas (ceil), tahap sekuensial tetap utuh.
     * Lebar 1 berarti sekuensial: hasil sama dengan totalBiaya.
     */
    public static long waktuDinding(List<BarisProfil> profil, int lebarParalel) {
        long gelombang = (biayaTahapParalel(profil) + lebarParalel - 1) / lebarParalel;
        return biayaTahapSekuensial(profil) + gelombang;
    }

    /** Throughput model: baris selesai per unit biaya simulasi. */
    public static double throughput(int jumlahBaris, long waktuUnit) {
        return (double) jumlahBaris / waktuUnit;
    }

    /** Biaya penjadwalan bila tiap baris dikirim sebagai satu task. */
    public static long biayaPenjadwalanPerBaris(int jumlahBaris) {
        return jumlahBaris * BIAYA_PENJADWALAN;
    }

    /** Biaya penjadwalan bila baris dikelompokkan menjadi batch berukuran tetap. */
    public static long biayaPenjadwalanPerBatch(int jumlahBaris, int ukuranBatch) {
        int jumlahTask = (jumlahBaris + ukuranBatch - 1) / ukuranBatch;
        return jumlahTask * BIAYA_PENJADWALAN;
    }
}

Empat keputusan pentingnya:

  1. profil(hasil): counter per tahap dari statistik nyata (36 baris, 38 percobaan, 28 sah, 26 laporan); transformasi memakai jumlahPercobaan() agar beban retry ikut terhitung.
  2. waktuDinding(profil, lebar): pembagian bulat ke atas; lebar 1 identik dengan totalBiaya.
  3. throughput(jumlahBaris, waktuUnit): pembagian murni tanpa jam — 900 unit memberi 36/900, alias 4,00 baris per 100 unit.

Profil Kemacetan #

Demo menjalankan pipeline pada virtual thread per baris, lalu mencetak profil, throughput, simulasi paralelisme, dan batching:

import java.util.List;
import java.util.Locale;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * Bagian 4: DemoPerforma — profil biaya per tahap, throughput model,
 * simulasi lebar paralelisme, dan ilustrasi batching penjadwalan.
 */
public class DemoPerforma {

    public static void main(String[] args) {
        List<String> isi = DataContoh.isiBerkas();
        HasilPipeline hasil;
        try (ExecutorService eksekutor = Executors.newVirtualThreadPerTaskExecutor()) {
            hasil = Pipeline.jalankanDenganRetry(isi, eksekutor, KebijakanRetry.baku());
        }

        List<BarisProfil> profil = ProfilPipeline.profil(hasil);
        long total = ProfilPipeline.totalBiaya(profil);
        BarisProfil macet = ProfilPipeline.kemacetan(profil);
        long sekuensial = ProfilPipeline.biayaTahapSekuensial(profil);

        System.out.println("== Profil biaya per tahap (" + isi.size()
                + " baris, model unit simulasi) ==");
        System.out.printf(Locale.US, "%-13s%6s%11s%8s%n",
                "tahap", "item", "biaya/unit", "total");
        for (BarisProfil b : profil) {
            System.out.printf(Locale.US, "%-13s%6d%11d%8d%n",
                    b.tahap().label(), b.itemDiproses(), b.biayaUnit(), b.total());
        }
        System.out.printf(Locale.US, "%-13s%6s%11s%8d%n", "total", "", "", total);
        System.out.println("kemacetan           : " + macet.tahap().label()
                + " — " + macet.total() + " unit ("
                + String.format(Locale.US, "%.1f%%", 100.0 * macet.total() / total)
                + " dari total)");
        System.out.println("unit tunggu backoff : " + hasil.unitTungguTotal()
                + " unit — waktu tunggu task, bukan beban pemroses");
        System.out.println();
        System.out.printf(Locale.US,
                "throughput sekuensial : %d baris / %d unit = %.2f baris per 100 unit%n",
                isi.size(), total, 100.0 * isi.size() / total);
        System.out.println();

        System.out.println("== Simulasi lebar paralelisme tahap mahal ==");
        System.out.printf(Locale.US, "%6s%12s%14s%9s%n",
                "lebar", "waktu-unit", "baris/100unit", "speedup");
        for (int lebar : new int[] {1, 4, 12, 36}) {
            long waktu = ProfilPipeline.waktuDinding(profil, lebar);
            System.out.printf(Locale.US, "%6d%12d%14.2f%8.2fx%n",
                    lebar, waktu, 100.0 * isi.size() / waktu, (double) total / waktu);
        }
        System.out.printf(Locale.US,
                "batas Amdahl: tahap sekuensial %d unit (%.1f%%) "
                        + "membatasi speedup maksimum %.2fx%n",
                sekuensial, 100.0 * sekuensial / total, (double) total / sekuensial);
        System.out.println();

        System.out.println("== Ilustrasi batching penjadwalan ("
                + ProfilPipeline.BIAYA_PENJADWALAN + " unit per task) ==");
        long perBaris = ProfilPipeline.biayaPenjadwalanPerBaris(isi.size());
        long perBatch = ProfilPipeline.biayaPenjadwalanPerBatch(isi.size(), 6);
        System.out.println("penjadwalan per baris : " + perBaris + " unit ("
                + isi.size() + " task)");
        System.out.println("penjadwalan per batch : " + perBatch
                + " unit (6 task berisi 6 baris)");
        System.out.printf(Locale.US, "penghematan           : %d unit (%.1f%% dari total %d)%n",
                perBaris - perBatch, 100.0 * (perBaris - perBatch) / total, total);
    }
}

Output yang Dihasilkan:

text
== Profil biaya per tahap (36 baris, model unit simulasi) ==
tahap          item biaya/unit   total
baca             36          2      72
validasi         36          5     180
transformasi     38         12     456
agregasi         28          5     140
tulis            26          2      52
total                              900
kemacetan           : transformasi — 456 unit (50.7% dari total)
unit tunggu backoff : 160 unit — waktu tunggu task, bukan beban pemroses

throughput sekuensial : 36 baris / 900 unit = 4.00 baris per 100 unit

== Simulasi lebar paralelisme tahap mahal ==
 lebar  waktu-unit baris/100unit  speedup
     1         900          4.00    1.00x
     4         423          8.51    2.13x
    12         317         11.36    2.84x
    36         282         12.77    3.19x
batas Amdahl: tahap sekuensial 264 unit (29.3%) membatasi speedup maksimum 3.41x

== Ilustrasi batching penjadwalan (2 unit per task) ==
penjadwalan per baris : 72 unit (36 task)
penjadwalan per batch : 12 unit (6 task berisi 6 baris)
penghematan           : 60 unit (6.7% dari total 900)

Empat pengamatan dari keluaran tersebut:

  1. Kemacetan di transformasi: 456 unit atau 50,7% dari total. Itemnya 38, bukan 36 — dua baris transien dicoba dua kali dan dua baris timeout tiga kali.
  2. Backoff bukan beban pemroses: 160 unit tunggu memperlambat baris tanpa menambah biaya pemrosesan model.
  3. Perlebar paralelisme tahap mahal: lebar 4 memangkas waktu dinding 900 menjadi 423 unit, lebar 36 mencapai 282 unit — speedup berhenti di 3,19x karena batas Amdahl: 29,3% pekerjaan sekuensial membatasi speedup maksimum 3,41x.
  4. Batching task: 36 task tunggal memakan 72 unit penjadwalan; 6 batch berisi 6 baris hanya 12 unit.

Benchmark Nyata dengan JMH #

Model biaya melatih cara berpikir, tetapi keputusan produksi membutuhkan angka dari JVM sungguhan. Tiga masalah menanti pengukur: JIT membutuhkan warm-up, compiler berhak menghapus perhitungan yang hasilnya tak pernah dipakai, dan penjadwal thread serta pengumpul sampah menimbulkan derau. JMH mengatasi ketiganya lewat fork terpisah, iterasi warm-up terukur, dan Blackhole yang memaksa hasil tetap diukur. Padanan benchmark untuk pipeline kita:

java
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Warmup;
import org.openjdk.jmh.infra.Blackhole;

/** Benchmark JMH: pipeline lengkap dalam satuan operasi per detik. */
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.SECONDS)
@Fork(1)
@Warmup(iterations = 3)
@Measurement(iterations = 5)
public class BenchmarkPipeline {

    @Benchmark
    public void pipelineLengkap(Blackhole lubang) {
        HasilPipeline hasil;
        try (ExecutorService eksekutor = Executors.newVirtualThreadPerTaskExecutor()) {
            hasil = Pipeline.jalankanDenganRetry(DataContoh.isiBerkas(),
                    eksekutor, KebijakanRetry.baku());
        }
        lubang.consume(hasil);   // hasil dipakai agar tidak dihapus compiler
    }
}

JMH bukan bagian JDK: ditambahkan sebagai dependensi Maven atau Gradle, dijalankan oleh runner bawaannya; panduan lengkap ada di gudang resminya.

Harness Pengujian Berbasis main #

Deterministik berarti teruji — seluruh angka Bagian 4 diperiksa oleh harness PASS/FAIL berikut:

java UjiPipeline.java
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * Bagian 4: harness pengujian model performa — PASS/FAIL tanpa pustaka
 * eksternal. Keluar dengan exit code 1 bila ada kegagalan.
 */
public class UjiPipeline {

    private static int lulus = 0;
    private static int gagal = 0;

    public static void main(String[] args) {
        HasilPipeline virtual = jalankanVirtual();

        ujiProfilNyata(virtual);
        ujiKemacetanDanThroughput(virtual);
        ujiSimulasiParalel(virtual);
        ujiBatching();
        ujiDeterminisme(virtual);

        System.out.println();
        System.out.println("Ringkasan: " + lulus + " lulus, " + gagal + " gagal");
        if (gagal > 0) {
            System.exit(1); // exit code memberi sinyal ke proses pemanggil
        }
    }

    /** Satu pernyataan uji: catat PASS bila kondisi benar, FAIL bila salah. */
    private static void cek(String nama, boolean kondisi) {
        if (kondisi) {
            lulus++;
            System.out.println("PASS: " + nama);
        } else {
            gagal++;
            System.out.println("FAIL: " + nama);
        }
    }

    /** Menjalankan pipeline pada virtual thread per baris. */
    private static HasilPipeline jalankanVirtual() {
        try (ExecutorService eksekutor = Executors.newVirtualThreadPerTaskExecutor()) {
            return Pipeline.jalankanDenganRetry(DataContoh.isiBerkas(),
                    eksekutor, KebijakanRetry.baku());
        }
    }

    /** Statistik pipeline nyata menjadi dasar profil; angkanya pasti. */
    private static void ujiProfilNyata(HasilPipeline hasil) {
        List<BarisProfil> profil = ProfilPipeline.profil(hasil);
        cek("pipeline memproses 36 baris", hasil.jumlahBaris() == 36);
        cek("28 baris sah, 4 ditolak validasi, 4 gagal tetap",
                hasil.jumlahSah() == 28 && hasil.jumlahDitolak() == 4
                        && hasil.jumlahGagalTetap() == 4);
        cek("transformasi menanggung 38 percobaan (32 event, 6 percobaan ulang)",
                hasil.jumlahPercobaan() == 38);
        cek("backoff menunggu total 160 unit", hasil.unitTungguTotal() == 160);
        cek("tahap tulis memroses 26 baris laporan (18 utama + 8 galat)",
                hasil.laporan().size() == 18 && hasil.laporanGalat().size() == 8);
        cek("total biaya model sekuensial 900 unit",
                ProfilPipeline.totalBiaya(profil) == 900);
    }

    /** Kemacetan diidentifikasi dari angka model, bukan tebakan. */
    private static void ujiKemacetanDanThroughput(HasilPipeline hasil) {
        List<BarisProfil> profil = ProfilPipeline.profil(hasil);
        BarisProfil macet = ProfilPipeline.kemacetan(profil);
        cek("kemacetan berada di transformasi (456 unit)",
                macet.tahap() == TahapPipeline.TRANSFORMASI && macet.total() == 456);
        cek("throughput sekuensial tepat 4.00 baris per 100 unit",
                100.0 * hasil.jumlahBaris() / ProfilPipeline.waktuDinding(profil, 1) == 4.0);
        cek("throughput lebar 4 melampaui throughput sekuensial",
                ProfilPipeline.throughput(hasil.jumlahBaris(),
                        ProfilPipeline.waktuDinding(profil, 4))
                        > ProfilPipeline.throughput(hasil.jumlahBaris(),
                        ProfilPipeline.waktuDinding(profil, 1)));
    }

    /** Simulasi waktu dinding: monoton menurun, dibatasi batas Amdahl. */
    private static void ujiSimulasiParalel(HasilPipeline hasil) {
        List<BarisProfil> profil = ProfilPipeline.profil(hasil);
        long total = ProfilPipeline.totalBiaya(profil);
        long sekuensial = ProfilPipeline.biayaTahapSekuensial(profil);
        cek("lebar 1 sama dengan total sekuensial (900 unit)",
                ProfilPipeline.waktuDinding(profil, 1) == total);
        cek("lebar 4 menghasilkan 423 unit waktu dinding",
                ProfilPipeline.waktuDinding(profil, 4) == 423);
        boolean monoton = true;
        long sebelumnya = Long.MAX_VALUE;
        for (int lebar : new int[] {1, 4, 12, 36}) {
            long waktu = ProfilPipeline.waktuDinding(profil, lebar);
            if (waktu > sebelumnya || waktu < sekuensial) {
                monoton = false;
            }
            sebelumnya = waktu;
        }
        cek("waktu dinding menurun monoton dan tak pernah di bawah batas Amdahl",
                monoton);
        cek("speedup lebar 36 masih di bawah batas Amdahl",
                (double) total / ProfilPipeline.waktuDinding(profil, 36)
                        < (double) total / sekuensial);
    }

    /** Batching menurunkan jumlah task penjadwalan secara tepat. */
    private static void ujiBatching() {
        cek("penjadwalan per baris memakan 72 unit",
                ProfilPipeline.biayaPenjadwalanPerBaris(36) == 72);
        cek("penjadwalan batch 6 memakan 12 unit",
                ProfilPipeline.biayaPenjadwalanPerBatch(36, 6) == 12);
    }

    /** Regresi Bagian 2: hasil dan profil identik lintas jenis executor. */
    private static void ujiDeterminisme(HasilPipeline virtual) {
        HasilPipeline pool;
        try (ExecutorService eksekutor = Executors.newFixedThreadPool(4)) {
            pool = Pipeline.jalankanDenganRetry(DataContoh.isiBerkas(),
                    eksekutor, KebijakanRetry.baku());
        }
        cek("hasil identik lintas executor (virtual vs pool tetap 4)",
                virtual.equals(pool));
        cek("profil biaya identik lintas executor",
                ProfilPipeline.totalBiaya(ProfilPipeline.profil(pool)) == 900);
    }
}

Output yang Dihasilkan:

text
PASS: pipeline memproses 36 baris
PASS: 28 baris sah, 4 ditolak validasi, 4 gagal tetap
PASS: transformasi menanggung 38 percobaan (32 event, 6 percobaan ulang)
PASS: backoff menunggu total 160 unit
PASS: tahap tulis memroses 26 baris laporan (18 utama + 8 galat)
PASS: total biaya model sekuensial 900 unit
PASS: kemacetan berada di transformasi (456 unit)
PASS: throughput sekuensial tepat 4.00 baris per 100 unit
PASS: throughput lebar 4 melampaui throughput sekuensial
PASS: lebar 1 sama dengan total sekuensial (900 unit)
PASS: lebar 4 menghasilkan 423 unit waktu dinding
PASS: waktu dinding menurun monoton dan tak pernah di bawah batas Amdahl
PASS: speedup lebar 36 masih di bawah batas Amdahl
PASS: penjadwalan per baris memakan 72 unit
PASS: penjadwalan batch 6 memakan 12 unit
PASS: hasil identik lintas executor (virtual vs pool tetap 4)
PASS: profil biaya identik lintas executor

Ringkasan: 17 lulus, 0 gagal

Lima kelompok diperiksa: statistik pipeline nyata (36 baris, 38 percobaan, 900 unit), kemacetan dan throughput, simulasi paralel yang menurun monoton, batching 72 menjadi 12 unit, serta determinisme lintas executor — hasil virtual thread identik dengan pool tetap 4. Tujuh belas pemeriksaan lulus; exit code 1 memberi sinyal kegagalan.

Padanan JUnit 5 #

JUnit bukan bagian JDK, jadi harness di atas memakai main murni; padanannya (blok tampilan tanpa klaim eksekusi) memetakan cek(nama, kondisi) ke assertTrue dan perbandingan angka ke assertEquals:

java
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

class ProfilPipelineTest {

    @Test
    void totalBiayaDanKemacetanStabil() {
        HasilPipeline hasil = jalankan();
        List<BarisProfil> profil = ProfilPipeline.profil(hasil);

        assertEquals(900, ProfilPipeline.totalBiaya(profil));
        assertEquals(TahapPipeline.TRANSFORMASI, ProfilPipeline.kemacetan(profil).tahap());
    }

    @Test
    void throughputNaikBilaLebarDiperbesar() {
        HasilPipeline hasil = jalankan();
        List<BarisProfil> profil = ProfilPipeline.profil(hasil);
        double sekuensial = ProfilPipeline.throughput(hasil.jumlahBaris(),
                ProfilPipeline.waktuDinding(profil, 1));
        double lebarEmpat = ProfilPipeline.throughput(hasil.jumlahBaris(),
                ProfilPipeline.waktuDinding(profil, 4));

        assertTrue(lebarEmpat > sekuensial);
    }

    private static HasilPipeline jalankan() {
        try (ExecutorService eksekutor = Executors.newVirtualThreadPerTaskExecutor()) {
            return Pipeline.jalankanDenganRetry(DataContoh.isiBerkas(),
                    eksekutor, KebijakanRetry.baku());
        }
    }
}

Bagian ini menutup proyek kedelapan dan terakhir Course Java: dari aplikasi catatan baris perintah, perpustakaan berbasis OOP, keuangan pribadi, REST API tanpa framework, inventaris, desktop Swing, dan web Spring Boot, hingga pipeline asinkron yang paralel, tahan galat, dan terukur ini. Langkah berikutnya menanti di section Sumber Belajar — dokumentasi resmi, roadmap pendalaman, dan bacaan lanjutan untuk membawa kedelapan aplikasi tersebut lebih jauh.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

BarisData.java

Unduh berkas
java
/** Satu baris berkas mentah: nomor urut (mulai dari 1) dan teks aslinya. */
public record BarisData(int nomor, String teks) {}

BarisProfil.java

Unduh berkas
java
/**
 * Satu baris tabel profil: tahap, jumlah item yang diproses, dan biaya totalnya.
 * Item dihitung dari eksekusi pipeline nyata; biaya per item dari model tetap.
 */
public record BarisProfil(TahapPipeline tahap, long itemDiproses) {

    /** Biaya unit per item menurut model tahap. */
    public long biayaUnit() {
        return tahap().biayaUnit();
    }

    /** Total biaya tahap: jumlah item dikali biaya unit. */
    public long total() {
        return itemDiproses() * tahap().biayaUnit();
    }
}

CatatanPercobaan.java

Unduh berkas
java
/** Satu kejadian pada jejak percobaan ulang; diurutkan menurut baris lalu percobaan. */
public record CatatanPercobaan(int nomorBaris, int percobaanKe, String kejadian) {}

DataContoh.java

Unduh berkas
java
import java.util.List;

/**
 * Isi berkas penjualan contoh: 36 baris data kuartal pertama 2026.
 * Dua puluh enam baris normal, empat baris cacat format, dan enam baris
 * berpenanda kondisi layanan untuk menguji percobaan ulang.
 */
public final class DataContoh {

    private DataContoh() {
        // Kelas utilitas: seluruh anggota static.
    }

    /** Isi berkas contoh; pada aplikasi nyata berasal dari Files.readAllLines. */
    public static List<String> isiBerkas() {
        return List.of(
                "2026-01-05;Bandung;Budi;Elektronik;1250000",
                "2026-01-07;Surabaya;Siti;Pakaian;340000",
                "2026-01-09;bandung;Dewi;Buku;95000",
                "2026-01-12;Yogyakarta;Budi;Peralatan;780000",
                "2026-01-15;Medan;Siti;Elektronik;2100000",
                "2026-01-18;Bandung;Dewi;Buku;128000",
                "2026-01-20;Medan;Siti;Pakaian;-150000",
                "2026-01-21;Makassar;Budi;Pakaian;450000",
                "2026-01-24;surabaya;Siti;Peralatan;620000",
                "2026-01-27;Yogyakarta;Dewi;Elektronik;1850000",
                "2026-02-02;Bandung;Siti;Pakaian;295000",
                "2026-02-05;Bandung;Dewi;Buku",
                "2026-02-08;Surabaya;Budi;Elektronik;990000",
                "2026-02-11;Makassar;Siti;Camilan;88000",
                "2026-02-14;Yogyakarta;Dewi;Pakaian;380000",
                "2026-02-17;Medan;Budi;Elektronik;1720000",
                "2026-02-20;bandung;Siti;Buku;112000",
                "2026-02-23;Surabaya;Dewi;Peralatan;690000",
                "2026-02-26;Makassar;Budi;Pakaian;410000",
                "2026-02-28;Yogyakarta;Siti;Elektronik;1580000",
                "2026-03-04;Medan;Dewi;Buku;64000",
                "2026-03-07;Bandung;Budi;Peralatan;830000",
                "2026-03-10;Surabaya;Siti;Elektronik;1350000",
                "2026-03-13;Makassar;Dewi;Buku;88000",
                "2026-03-16;Yogyakarta;Budi;Pakaian;365000",
                "2026-03-19;Bandung;Siti;Peralatan;575000",
                "2026-03-22;medan;Dewi;Elektronik;1980000",
                "2026-03-25;Surabaya;Budi;Buku;102000",
                "2026-03-28;Makassar;Siti;Pakaian;430000",
                "2026-13-31;Yogyakarta;Dewi;Peralatan;715000",
                "2026-03-02;Bandung;Siti;Buku;140000;transien",
                "2026-03-08;Medan;Dewi;Elektronik;870000;transien",
                "2026-03-12;Surabaya;Budi;Peralatan;430000;timeout",
                "2026-03-18;Yogyakarta;Dewi;Buku;95000;timeout",
                "2026-03-22;Makassar;Siti;Pakaian;310000;rusak",
                "2026-03-29;Bandung;Budi;Elektronik;1120000;rusak");
    }
}

DemoPerforma.java

Unduh berkas
java
import java.util.List;
import java.util.Locale;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * Bagian 4: DemoPerforma — profil biaya per tahap, throughput model,
 * simulasi lebar paralelisme, dan ilustrasi batching penjadwalan.
 */
public class DemoPerforma {

    public static void main(String[] args) {
        List<String> isi = DataContoh.isiBerkas();
        HasilPipeline hasil;
        try (ExecutorService eksekutor = Executors.newVirtualThreadPerTaskExecutor()) {
            hasil = Pipeline.jalankanDenganRetry(isi, eksekutor, KebijakanRetry.baku());
        }

        List<BarisProfil> profil = ProfilPipeline.profil(hasil);
        long total = ProfilPipeline.totalBiaya(profil);
        BarisProfil macet = ProfilPipeline.kemacetan(profil);
        long sekuensial = ProfilPipeline.biayaTahapSekuensial(profil);

        System.out.println("== Profil biaya per tahap (" + isi.size()
                + " baris, model unit simulasi) ==");
        System.out.printf(Locale.US, "%-13s%6s%11s%8s%n",
                "tahap", "item", "biaya/unit", "total");
        for (BarisProfil b : profil) {
            System.out.printf(Locale.US, "%-13s%6d%11d%8d%n",
                    b.tahap().label(), b.itemDiproses(), b.biayaUnit(), b.total());
        }
        System.out.printf(Locale.US, "%-13s%6s%11s%8d%n", "total", "", "", total);
        System.out.println("kemacetan           : " + macet.tahap().label()
                + " — " + macet.total() + " unit ("
                + String.format(Locale.US, "%.1f%%", 100.0 * macet.total() / total)
                + " dari total)");
        System.out.println("unit tunggu backoff : " + hasil.unitTungguTotal()
                + " unit — waktu tunggu task, bukan beban pemroses");
        System.out.println();
        System.out.printf(Locale.US,
                "throughput sekuensial : %d baris / %d unit = %.2f baris per 100 unit%n",
                isi.size(), total, 100.0 * isi.size() / total);
        System.out.println();

        System.out.println("== Simulasi lebar paralelisme tahap mahal ==");
        System.out.printf(Locale.US, "%6s%12s%14s%9s%n",
                "lebar", "waktu-unit", "baris/100unit", "speedup");
        for (int lebar : new int[] {1, 4, 12, 36}) {
            long waktu = ProfilPipeline.waktuDinding(profil, lebar);
            System.out.printf(Locale.US, "%6d%12d%14.2f%8.2fx%n",
                    lebar, waktu, 100.0 * isi.size() / waktu, (double) total / waktu);
        }
        System.out.printf(Locale.US,
                "batas Amdahl: tahap sekuensial %d unit (%.1f%%) "
                        + "membatasi speedup maksimum %.2fx%n",
                sekuensial, 100.0 * sekuensial / total, (double) total / sekuensial);
        System.out.println();

        System.out.println("== Ilustrasi batching penjadwalan ("
                + ProfilPipeline.BIAYA_PENJADWALAN + " unit per task) ==");
        long perBaris = ProfilPipeline.biayaPenjadwalanPerBaris(isi.size());
        long perBatch = ProfilPipeline.biayaPenjadwalanPerBatch(isi.size(), 6);
        System.out.println("penjadwalan per baris : " + perBaris + " unit ("
                + isi.size() + " task)");
        System.out.println("penjadwalan per batch : " + perBatch
                + " unit (6 task berisi 6 baris)");
        System.out.printf(Locale.US, "penghematan           : %d unit (%.1f%% dari total %d)%n",
                perBaris - perBatch, 100.0 * (perBaris - perBatch) / total, total);
    }
}

EventPenjualan.java

Unduh berkas
java
import java.math.BigDecimal;
import java.time.LocalDate;

/**
 * Kontrak keluaran tahap validasi: event penjualan yang lolos pemeriksaan.
 * Komponen penanda (string kosong bila tidak ada) menyimpan simulasi kondisi
 * layanan: "transien", "timeout", atau "rusak".
 */
public record EventPenjualan(int nomorBaris, LocalDate tanggal, String kota,
                             String pelanggan, String kategori, BigDecimal nilai,
                             String penanda) {}

GalatProsesException.java

Unduh berkas
java
/**
 * Akar hierarki galat pemrosesan baris.
 * Turunan RuntimeException agar task paralel bebas melempar tanpa deklarasi checked.
 */
public class GalatProsesException extends RuntimeException {

    public GalatProsesException(String pesan) {
        super(pesan);
    }

    /** Bentuk berantai: penyebab teknis disimpan agar bisa dirunut kemudian. */
    public GalatProsesException(String pesan, Throwable penyebab) {
        super(pesan, penyebab);
    }
}

GalatSementaraException.java

Unduh berkas
java
/** Galat yang layak dicoba ulang: layanan sedang sibuk atau tidak merespons. */
public class GalatSementaraException extends GalatProsesException {

    public GalatSementaraException(String pesan) {
        super(pesan);
    }

    public GalatSementaraException(String pesan, Throwable penyebab) {
        super(pesan, penyebab);
    }
}

GalatTetapException.java

Unduh berkas
java
/** Galat yang tidak akan sembuh dicoba ulang; menyimpan penyebab teknisnya. */
public class GalatTetapException extends GalatProsesException {

    public GalatTetapException(String pesan, Throwable penyebab) {
        super(pesan, penyebab);
    }
}

HasilBaris.java

Unduh berkas
java
/**
 * Kontrak hasil pemrosesan satu baris: nilai data, bukan exception.
 * Varian terkunci lewat sealed sehingga compiler memaksa setiap switch
 * menangani semua kemungkinan — termasuk varian GagalTetap yang ditambahkan
 * ketika kebijakan percobaan ulang menyerah.
 */
public sealed interface HasilBaris permits HasilBaris.Sah, HasilBaris.TidakSah,
        HasilBaris.GagalTetap {

    /** Nomor baris sumber — dipakai pelaporan galat dan pengurutan hasil. */
    int nomorBaris();

    /** Baris selesai diproses dan membawa kejadian siap agregasi. */
    record Sah(KejadianTerproses kejadian) implements HasilBaris {
        @Override
        public int nomorBaris() {
            return kejadian.nomorBaris();
        }
    }

    /** Baris ditolak validator; alasan ditulis untuk laporan galat. */
    record TidakSah(int nomorBaris, String alasan) implements HasilBaris {
        @Override
        public int nomorBaris() {
            return nomorBaris;
        }
    }

    /** Baris gagal diproses setelah kebijakan retry menyerah; akar masalah dirunut. */
    record GagalTetap(int nomorBaris, int percobaan, String pesan, String penyebabAkar)
            implements HasilBaris {
        @Override
        public int nomorBaris() {
            return nomorBaris;
        }
    }
}

HasilPipeline.java

Unduh berkas
java
import java.util.List;

/**
 * Hasil pipeline utuh: statistik, laporan utama, laporan galat, dan jejak percobaan.
 * Komponen jumlahPercobaan menghitung seluruh percobaan transformasi (termasuk
 * percobaan ulang) — angka penting bagi model biaya Bagian 4.
 */
public record HasilPipeline(int jumlahBaris,
                            int jumlahSah,
                            int jumlahDitolak,
                            int jumlahGagalTetap,
                            long jumlahPercobaan,
                            long unitTungguTotal,
                            int jumlahKota,
                            int jumlahKategori,
                            List<String> laporan,
                            List<String> laporanGalat,
                            List<CatatanPercobaan> jejakPercobaan) {}

KebijakanRetry.java

Unduh berkas
java
/**
 * Kebijakan percobaan ulang: batas percobaan dan backoff eksponensial dalam
 * "unit biaya simulasi" — bukan sleep nyata — agar keluaran tetap deterministik.
 */
public record KebijakanRetry(int percobaanMaks, long unitTungguDasar) {

    /** Kebijakan baku: maksimal 3 percobaan, tunggu dasar 20 unit. */
    public static KebijakanRetry baku() {
        return new KebijakanRetry(3, 20);
    }

    /** Panjang tunggu setelah percobaan ke-N gagal: 20, 40, 80, ... unit. */
    public long unitTunggu(int percobaanKe) {
        return unitTungguDasar << (percobaanKe - 1);
    }
}

KejadianTerproses.java

Unduh berkas
java
import java.math.BigDecimal;
import java.time.LocalDate;

/**
 * Kontrak keluaran tahap transformasi: data siap agregasi.
 * Kota sudah dibakukan (huruf kapital) dan nilai berjasa sudah dihitung.
 */
public record KejadianTerproses(int nomorBaris, LocalDate tanggal, String kota,
                                String pelanggan, String kategori, BigDecimal nilai,
                                BigDecimal nilaiBerjasa) {}

Pipeline.java

Unduh berkas
java
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.net.ConnectException;
import java.time.LocalDate;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.LongAdder;
import java.util.stream.Collectors;

/** Pipeline tahan galat terukur: baca -> validasi -> transformasi (retry) -> agregasi -> tulis. */
public final class Pipeline {

    /** Kategori yang dikenal validator; di luar daftar ini baris ditolak. */
    private static final Set<String> KATEGORI_DIKENAL =
            Set.of("Elektronik", "Pakaian", "Buku", "Peralatan");

    /** Penanda kondisi layanan yang boleh menempel pada baris data. */
    private static final Set<String> PENANDA_DIKENAL =
            Set.of("transien", "timeout", "rusak");

    /** Faktor biaya layanan 2% yang dihitung tahap transformasi. */
    private static final BigDecimal BIAYA_LAYANAN = new BigDecimal("1.02");

    private Pipeline() {
        // Kelas utilitas: seluruh anggota static.
    }

    /** Tahap baca: memberi nomor urut pada tiap baris berkas. */
    public static List<BarisData> baca(List<String> isiBerkas) {
        List<BarisData> baris = new ArrayList<>(isiBerkas.size());
        for (int i = 0; i < isiBerkas.size(); i++) {
            baris.add(new BarisData(i + 1, isiBerkas.get(i)));
        }
        return baris;
    }

    /**
     * Tahap validasi lalu transformasi dengan percobaan ulang untuk satu baris.
     * Setiap percobaan gagal tercatat sebagai jejak; hasil akhir tetap nilai data.
     */
    public static HasilBaris prosesDenganRetry(BarisData baris, KebijakanRetry kebijakan,
            ConcurrentLinkedQueue<CatatanPercobaan> jejak, LongAdder totalTunggu,
            LongAdder percobaanTotal) {
        // --- tahap validasi ---
        String[] segmen = baris.teks().split(";", -1);
        if (segmen.length != 5 && segmen.length != 6) {
            return new HasilBaris.TidakSah(baris.nomor(),
                    "jumlah segmen " + segmen.length + ", harus 5 atau 6");
        }
        LocalDate tanggal;
        try {
            tanggal = LocalDate.parse(segmen[0]);
        } catch (DateTimeParseException e) {
            return new HasilBaris.TidakSah(baris.nomor(),
                    "tanggal tidak terurai: '" + segmen[0] + "'");
        }
        String kota = segmen[1].trim();
        String pelanggan = segmen[2].trim();
        String kategori = segmen[3].trim();
        if (kota.isEmpty()) {
            return new HasilBaris.TidakSah(baris.nomor(), "kota kosong");
        }
        if (pelanggan.isEmpty()) {
            return new HasilBaris.TidakSah(baris.nomor(), "nama pelanggan kosong");
        }
        if (!KATEGORI_DIKENAL.contains(kategori)) {
            return new HasilBaris.TidakSah(baris.nomor(),
                    "kategori tidak dikenal: '" + kategori + "'");
        }
        BigDecimal nilai;
        try {
            nilai = new BigDecimal(segmen[4].trim());
        } catch (NumberFormatException e) {
            return new HasilBaris.TidakSah(baris.nomor(),
                    "nilai tidak terurai: '" + segmen[4].trim() + "'");
        }
        if (nilai.signum() <= 0) {
            return new HasilBaris.TidakSah(baris.nomor(),
                    "nilai harus positif: " + nilai.toPlainString());
        }
        String penanda = segmen.length == 6 ? segmen[5].trim() : "";
        if (!penanda.isEmpty() && !PENANDA_DIKENAL.contains(penanda)) {
            return new HasilBaris.TidakSah(baris.nomor(),
                    "penanda tidak dikenal: '" + penanda + "'");
        }
        EventPenjualan event = new EventPenjualan(baris.nomor(), tanggal, kota,
                pelanggan, kategori, nilai, penanda);

        // --- tahap transformasi dengan percobaan ulang ---
        for (int percobaan = 1; percobaan <= kebijakan.percobaanMaks(); percobaan++) {
            percobaanTotal.increment();
            try {
                KejadianTerproses kejadian = prosesDenganLayanan(event, percobaan);
                if (percobaan > 1) {
                    jejak.add(new CatatanPercobaan(baris.nomor(), percobaan,
                            "berhasil setelah percobaan ulang"));
                }
                return new HasilBaris.Sah(kejadian);
            } catch (GalatTetapException e) {
                // Galat tetap tidak layak dicoba ulang: gagal cepat.
                jejak.add(new CatatanPercobaan(baris.nomor(), percobaan,
                        "gagal tetap, tanpa percobaan ulang: " + e.getMessage()));
                return new HasilBaris.GagalTetap(baris.nomor(), percobaan,
                        e.getMessage(), akar(e));
            } catch (GalatSementaraException e) {
                if (percobaan == kebijakan.percobaanMaks()) {
                    jejak.add(new CatatanPercobaan(baris.nomor(), percobaan,
                            "percobaan habis: " + e.getMessage()));
                    return new HasilBaris.GagalTetap(baris.nomor(), percobaan,
                            "percobaan ulang habis: " + e.getMessage(), akar(e));
                }
                long tunggu = kebijakan.unitTunggu(percobaan);
                totalTunggu.add(tunggu);
                jejak.add(new CatatanPercobaan(baris.nomor(), percobaan,
                        "gagal sementara: " + e.getMessage() + "; tunggu " + tunggu + " unit"));
            }
        }
        throw new IllegalStateException("tidak tercapai: loop selalu mengembalikan hasil");
    }

    /** Transformasi satu event; layanan simulasi melempar galat sesuai penanda. */
    private static KejadianTerproses prosesDenganLayanan(EventPenjualan event, int percobaan) {
        String penanda = event.penanda();
        if ("transien".equals(penanda) && percobaan == 1) {
            throw new GalatSementaraException("layanan tarif sedang sibuk");
        }
        if ("timeout".equals(penanda)) {
            throw new GalatSementaraException("layanan tarif tidak merespons",
                    new ConnectException("waktu tunggu koneksi habis"));
        }
        if ("rusak".equals(penanda)) {
            throw new GalatTetapException("sambungan gudang data terputus",
                    new ConnectException("koneksi ditolak oleh gudang data"));
        }
        return ubah(event);
    }

    /** Tahap transformasi: bakukan kota dan hitung nilai berjasa 2%. */
    public static KejadianTerproses ubah(EventPenjualan event) {
        BigDecimal berjasa = event.nilai().multiply(BIAYA_LAYANAN)
                .setScale(2, RoundingMode.HALF_EVEN);
        return new KejadianTerproses(event.nomorBaris(), event.tanggal(),
                event.kota().toUpperCase(Locale.ROOT), event.pelanggan(),
                event.kategori(), event.nilai(), berjasa);
    }

    /** Tahap agregasi: ringkasan terurut dari seluruh kejadian. */
    public static RingkasanAgregasi agregasi(List<KejadianTerproses> kejadian) {
        Map<String, BigDecimal> perKota = kejadian.stream()
                .collect(Collectors.groupingBy(KejadianTerproses::kota,
                        TreeMap::new,
                        Collectors.reducing(BigDecimal.ZERO,
                                KejadianTerproses::nilaiBerjasa, BigDecimal::add)));
        Map<String, Long> perKategori = kejadian.stream()
                .collect(Collectors.groupingBy(KejadianTerproses::kategori,
                        TreeMap::new, Collectors.counting()));
        BigDecimal totalNilai = kejadian.stream()
                .map(KejadianTerproses::nilai)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        BigDecimal totalBerjasa = kejadian.stream()
                .map(KejadianTerproses::nilaiBerjasa)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        return new RingkasanAgregasi(kejadian.size(), totalNilai, totalBerjasa,
                perKota, perKategori);
    }

    /** Tahap tulis: susun laporan utama dari ringkasan dan hasil per baris. */
    public static List<String> tulis(RingkasanAgregasi ringkasan, List<HasilBaris> hasil) {
        int sah = 0;
        int ditolak = 0;
        int gagal = 0;
        for (HasilBaris h : hasil) {
            switch (h) {
                case HasilBaris.Sah _ -> sah++;
                case HasilBaris.TidakSah _ -> ditolak++;
                case HasilBaris.GagalTetap _ -> gagal++;
            }
        }
        List<String> laporan = new ArrayList<>();
        laporan.add("=== Laporan pipeline penjualan ===");
        laporan.add("baris dibaca     : " + hasil.size());
        laporan.add("baris sah        : " + sah);
        laporan.add("ditolak validasi : " + ditolak);
        laporan.add("gagal tetap      : " + gagal);
        laporan.add(String.format(Locale.US, "total nilai     : %,14.2f", ringkasan.totalNilai()));
        laporan.add(String.format(Locale.US, "total berjasa   : %,14.2f", ringkasan.totalBerjasa()));
        laporan.add("nilai berjasa per kota:");
        ringkasan.nilaiBerjasaPerKota().forEach((kota, nilai) ->
                laporan.add(String.format(Locale.US, "  %-12s %,14.2f", kota, nilai)));
        laporan.add("jumlah transaksi per kategori:");
        ringkasan.jumlahPerKategori().forEach((kategori, jumlah) ->
                laporan.add(String.format(Locale.US, "  %-12s %6d", kategori, jumlah)));
        return laporan;
    }

    /** Laporan galat terurut nomor baris: penolakan validasi dan kegagalan tetap. */
    public static List<String> tulisGalat(List<HasilBaris> hasil) {
        List<String> galat = new ArrayList<>();
        for (HasilBaris h : hasil) {
            switch (h) {
                case HasilBaris.Sah _ -> { }
                case HasilBaris.TidakSah(int nomor, String alasan) ->
                        galat.add(String.format(Locale.US,
                                "baris %2d  DITOLAK VALIDASI : %s", nomor, alasan));
                case HasilBaris.GagalTetap(int nomor, int percobaan, String pesan,
                        String penyebabAkar) ->
                        galat.add(String.format(Locale.US,
                                "baris %2d  GAGAL TETAP      : %s (percobaan %d; akar: %s)",
                                nomor, pesan, percobaan, penyebabAkar));
            }
        }
        return galat;
    }

    /** Menjalankan pipeline dengan percobaan ulang secara paralel pada eksekutor. */
    public static HasilPipeline jalankanDenganRetry(List<String> isiBerkas,
            ExecutorService eksekutor, KebijakanRetry kebijakan) {
        List<BarisData> baris = baca(isiBerkas);
        Map<Integer, HasilBaris> hasilPerNomor = new ConcurrentHashMap<>();
        // Jejak percobaan ditulis banyak thread sekaligus tanpa penguncian manual.
        ConcurrentLinkedQueue<CatatanPercobaan> jejak = new ConcurrentLinkedQueue<>();
        LongAdder totalTunggu = new LongAdder();
        // Penghitung percobaan transformasi: ditambah banyak thread bersamaan.
        LongAdder percobaanTotal = new LongAdder();
        List<CompletableFuture<Void>> tugas = new ArrayList<>(baris.size());
        for (BarisData b : baris) {
            tugas.add(CompletableFuture.runAsync(() -> hasilPerNomor.put(b.nomor(),
                    prosesDenganRetry(b, kebijakan, jejak, totalTunggu, percobaanTotal)),
                    eksekutor));
        }
        CompletableFuture.allOf(tugas.toArray(new CompletableFuture<?>[0])).join();
        List<HasilBaris> terurut = new ArrayList<>(new TreeMap<>(hasilPerNomor).values());
        // Urutan kedatangan jejak tidak menentu: urutkan menurut baris lalu percobaan.
        List<CatatanPercobaan> jejakTerurut = jejak.stream()
                .sorted(Comparator.comparingInt(CatatanPercobaan::nomorBaris)
                        .thenComparingInt(CatatanPercobaan::percobaanKe))
                .toList();
        return rakitHasil(baris.size(), terurut, jejakTerurut, totalTunggu.sum(),
                percobaanTotal.sum());
    }

    /** Merakit hasil per baris menjadi statistik, laporan, dan laporan galat. */
    private static HasilPipeline rakitHasil(int jumlahBaris, List<HasilBaris> hasil,
            List<CatatanPercobaan> jejak, long unitTunggu, long jumlahPercobaan) {
        List<KejadianTerproses> kejadian = new ArrayList<>(hasil.size());
        int ditolak = 0;
        int gagal = 0;
        for (HasilBaris h : hasil) {
            switch (h) {
                case HasilBaris.Sah(var k) -> kejadian.add(k);
                case HasilBaris.TidakSah _ -> ditolak++;
                case HasilBaris.GagalTetap _ -> gagal++;
            }
        }
        RingkasanAgregasi ringkasan = agregasi(kejadian);
        return new HasilPipeline(jumlahBaris, kejadian.size(), ditolak, gagal, jumlahPercobaan,
                unitTunggu, ringkasan.nilaiBerjasaPerKota().size(),
                ringkasan.jumlahPerKategori().size(),
                tulis(ringkasan, hasil), tulisGalat(hasil), jejak);
    }

    /** Merunut penyebab paling dalam dari rangkaian exception berantai. */
    private static String akar(Throwable galat) {
        Throwable kini = galat;
        while (kini.getCause() != null) {
            kini = kini.getCause();
        }
        return kini.getClass().getSimpleName() + ": " + kini.getMessage();
    }
}

ProfilPipeline.java

Unduh berkas
java
import java.util.List;

/**
 * Pengukur performa deterministik atas hasil pipeline nyata.
 * Item per tahap dibaca dari statistik HasilPipeline (angka hasil eksekusi),
 * biaya per item dari model tetap TahapPipeline — tidak ada jam dinding yang
 * diukur, sehingga seluruh angka stabil antar eksekusi dan siap diuji.
 */
public final class ProfilPipeline {

    /** Biaya penjadwalan simulasi setiap task yang dikirim ke executor. */
    public static final long BIAYA_PENJADWALAN = 2;

    private ProfilPipeline() {
        // Kelas utilitas: seluruh anggota static.
    }

    /** Menyusun profil per tahap dari statistik hasil pipeline nyata. */
    public static List<BarisProfil> profil(HasilPipeline hasil) {
        long barisLaporan = hasil.laporan().size() + hasil.laporanGalat().size();
        return List.of(
                new BarisProfil(TahapPipeline.BACA, hasil.jumlahBaris()),
                new BarisProfil(TahapPipeline.VALIDASI, hasil.jumlahBaris()),
                new BarisProfil(TahapPipeline.TRANSFORMASI, hasil.jumlahPercobaan()),
                new BarisProfil(TahapPipeline.AGREGASI, hasil.jumlahSah()),
                new BarisProfil(TahapPipeline.TULIS, barisLaporan));
    }

    /** Total biaya seluruh tahap bila dikerjakan sekuensial. */
    public static long totalBiaya(List<BarisProfil> profil) {
        return profil.stream().mapToLong(BarisProfil::total).sum();
    }

    /** Tahap dengan total biaya terbesar — kandidat kemacetan utama. */
    public static BarisProfil kemacetan(List<BarisProfil> profil) {
        BarisProfil terbesar = profil.get(0);
        for (BarisProfil b : profil) {
            if (b.total() > terbesar.total()) {
                terbesar = b;
            }
        }
        return terbesar;
    }

    /** Total biaya tahap yang berjalan paralel per baris (validasi + transformasi). */
    public static long biayaTahapParalel(List<BarisProfil> profil) {
        return profil.stream()
                .filter(b -> b.tahap() == TahapPipeline.VALIDASI
                        || b.tahap() == TahapPipeline.TRANSFORMASI)
                .mapToLong(BarisProfil::total)
                .sum();
    }

    /** Total biaya tahap yang berjalan sekuensial di main (baca + agregasi + tulis). */
    public static long biayaTahapSekuensial(List<BarisProfil> profil) {
        return totalBiaya(profil) - biayaTahapParalel(profil);
    }

    /**
     * Simulasi waktu dinding bila tahap paralel dikerjakan W pekerja:
     * kerja paralel dibagi rata ke atas (ceil), tahap sekuensial tetap utuh.
     * Lebar 1 berarti sekuensial: hasil sama dengan totalBiaya.
     */
    public static long waktuDinding(List<BarisProfil> profil, int lebarParalel) {
        long gelombang = (biayaTahapParalel(profil) + lebarParalel - 1) / lebarParalel;
        return biayaTahapSekuensial(profil) + gelombang;
    }

    /** Throughput model: baris selesai per unit biaya simulasi. */
    public static double throughput(int jumlahBaris, long waktuUnit) {
        return (double) jumlahBaris / waktuUnit;
    }

    /** Biaya penjadwalan bila tiap baris dikirim sebagai satu task. */
    public static long biayaPenjadwalanPerBaris(int jumlahBaris) {
        return jumlahBaris * BIAYA_PENJADWALAN;
    }

    /** Biaya penjadwalan bila baris dikelompokkan menjadi batch berukuran tetap. */
    public static long biayaPenjadwalanPerBatch(int jumlahBaris, int ukuranBatch) {
        int jumlahTask = (jumlahBaris + ukuranBatch - 1) / ukuranBatch;
        return jumlahTask * BIAYA_PENJADWALAN;
    }
}

RingkasanAgregasi.java

Unduh berkas
java
import java.math.BigDecimal;
import java.util.Map;

/**
 * Kontrak keluaran tahap agregasi: ringkasan seluruh kejadian.
 * Peta memakai TreeMap sehingga iterasi selalu terurut (deterministik).
 */
public record RingkasanAgregasi(int jumlahKejadian,
                                BigDecimal totalNilai,
                                BigDecimal totalBerjasa,
                                Map<String, BigDecimal> nilaiBerjasaPerKota,
                                Map<String, Long> jumlahPerKategori) {}

TahapPipeline.java

Unduh berkas
java
/**
 * Tahap pipeline beserta biaya unit simulasi untuk memroses satu item.
 * Angka biaya adalah model tetap — bukan hasil pengukuran jam dinding —
 * sehingga seluruh perhitungan performa pada Bagian 4 bersifat deterministik.
 */
public enum TahapPipeline {
    BACA("baca", 2),
    VALIDASI("validasi", 5),
    TRANSFORMASI("transformasi", 12),
    AGREGASI("agregasi", 5),
    TULIS("tulis", 2);

    private final String label;
    private final long biayaUnit;

    TahapPipeline(String label, long biayaUnit) {
        this.label = label;
        this.biayaUnit = biayaUnit;
    }

    /** Nama tahap untuk laporan profil. */
    public String label() {
        return label;
    }

    /** Biaya unit simulasi per satu item yang diproses tahap ini. */
    public long biayaUnit() {
        return biayaUnit;
    }
}

UjiPipeline.java

Unduh berkas
java
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * Bagian 4: harness pengujian model performa — PASS/FAIL tanpa pustaka
 * eksternal. Keluar dengan exit code 1 bila ada kegagalan.
 */
public class UjiPipeline {

    private static int lulus = 0;
    private static int gagal = 0;

    public static void main(String[] args) {
        HasilPipeline virtual = jalankanVirtual();

        ujiProfilNyata(virtual);
        ujiKemacetanDanThroughput(virtual);
        ujiSimulasiParalel(virtual);
        ujiBatching();
        ujiDeterminisme(virtual);

        System.out.println();
        System.out.println("Ringkasan: " + lulus + " lulus, " + gagal + " gagal");
        if (gagal > 0) {
            System.exit(1); // exit code memberi sinyal ke proses pemanggil
        }
    }

    /** Satu pernyataan uji: catat PASS bila kondisi benar, FAIL bila salah. */
    private static void cek(String nama, boolean kondisi) {
        if (kondisi) {
            lulus++;
            System.out.println("PASS: " + nama);
        } else {
            gagal++;
            System.out.println("FAIL: " + nama);
        }
    }

    /** Menjalankan pipeline pada virtual thread per baris. */
    private static HasilPipeline jalankanVirtual() {
        try (ExecutorService eksekutor = Executors.newVirtualThreadPerTaskExecutor()) {
            return Pipeline.jalankanDenganRetry(DataContoh.isiBerkas(),
                    eksekutor, KebijakanRetry.baku());
        }
    }

    /** Statistik pipeline nyata menjadi dasar profil; angkanya pasti. */
    private static void ujiProfilNyata(HasilPipeline hasil) {
        List<BarisProfil> profil = ProfilPipeline.profil(hasil);
        cek("pipeline memproses 36 baris", hasil.jumlahBaris() == 36);
        cek("28 baris sah, 4 ditolak validasi, 4 gagal tetap",
                hasil.jumlahSah() == 28 && hasil.jumlahDitolak() == 4
                        && hasil.jumlahGagalTetap() == 4);
        cek("transformasi menanggung 38 percobaan (32 event, 6 percobaan ulang)",
                hasil.jumlahPercobaan() == 38);
        cek("backoff menunggu total 160 unit", hasil.unitTungguTotal() == 160);
        cek("tahap tulis memroses 26 baris laporan (18 utama + 8 galat)",
                hasil.laporan().size() == 18 && hasil.laporanGalat().size() == 8);
        cek("total biaya model sekuensial 900 unit",
                ProfilPipeline.totalBiaya(profil) == 900);
    }

    /** Kemacetan diidentifikasi dari angka model, bukan tebakan. */
    private static void ujiKemacetanDanThroughput(HasilPipeline hasil) {
        List<BarisProfil> profil = ProfilPipeline.profil(hasil);
        BarisProfil macet = ProfilPipeline.kemacetan(profil);
        cek("kemacetan berada di transformasi (456 unit)",
                macet.tahap() == TahapPipeline.TRANSFORMASI && macet.total() == 456);
        cek("throughput sekuensial tepat 4.00 baris per 100 unit",
                100.0 * hasil.jumlahBaris() / ProfilPipeline.waktuDinding(profil, 1) == 4.0);
        cek("throughput lebar 4 melampaui throughput sekuensial",
                ProfilPipeline.throughput(hasil.jumlahBaris(),
                        ProfilPipeline.waktuDinding(profil, 4))
                        > ProfilPipeline.throughput(hasil.jumlahBaris(),
                        ProfilPipeline.waktuDinding(profil, 1)));
    }

    /** Simulasi waktu dinding: monoton menurun, dibatasi batas Amdahl. */
    private static void ujiSimulasiParalel(HasilPipeline hasil) {
        List<BarisProfil> profil = ProfilPipeline.profil(hasil);
        long total = ProfilPipeline.totalBiaya(profil);
        long sekuensial = ProfilPipeline.biayaTahapSekuensial(profil);
        cek("lebar 1 sama dengan total sekuensial (900 unit)",
                ProfilPipeline.waktuDinding(profil, 1) == total);
        cek("lebar 4 menghasilkan 423 unit waktu dinding",
                ProfilPipeline.waktuDinding(profil, 4) == 423);
        boolean monoton = true;
        long sebelumnya = Long.MAX_VALUE;
        for (int lebar : new int[] {1, 4, 12, 36}) {
            long waktu = ProfilPipeline.waktuDinding(profil, lebar);
            if (waktu > sebelumnya || waktu < sekuensial) {
                monoton = false;
            }
            sebelumnya = waktu;
        }
        cek("waktu dinding menurun monoton dan tak pernah di bawah batas Amdahl",
                monoton);
        cek("speedup lebar 36 masih di bawah batas Amdahl",
                (double) total / ProfilPipeline.waktuDinding(profil, 36)
                        < (double) total / sekuensial);
    }

    /** Batching menurunkan jumlah task penjadwalan secara tepat. */
    private static void ujiBatching() {
        cek("penjadwalan per baris memakan 72 unit",
                ProfilPipeline.biayaPenjadwalanPerBaris(36) == 72);
        cek("penjadwalan batch 6 memakan 12 unit",
                ProfilPipeline.biayaPenjadwalanPerBatch(36, 6) == 12);
    }

    /** Regresi Bagian 2: hasil dan profil identik lintas jenis executor. */
    private static void ujiDeterminisme(HasilPipeline virtual) {
        HasilPipeline pool;
        try (ExecutorService eksekutor = Executors.newFixedThreadPool(4)) {
            pool = Pipeline.jalankanDenganRetry(DataContoh.isiBerkas(),
                    eksekutor, KebijakanRetry.baku());
        }
        cek("hasil identik lintas executor (virtual vs pool tetap 4)",
                virtual.equals(pool));
        cek("profil biaya identik lintas executor",
                ProfilPipeline.totalBiaya(ProfilPipeline.profil(pool)) == 900);
    }
}