import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * Simulasi sampling profiler pada dua versi kode: sebelum dan sesudah
 * hasil hitungPajak di-cache. Sampler mencatat method terdalam setiap
 * INTERVAL satuan kerja; seluruh angka berupa penghitung operasi.
 */
public class ProfilerSampling {

    static final int INTERVAL = 4; // 1 sampel tiap 4 satuan kerja

    record Frame(String method, int unit) {}

    public static void main(String[] args) {
        // Versi A: hitungPajak dikerjakan ulang untuk setiap laporan
        List<Frame> versiA = new ArrayList<>();
        versiA.add(new Frame("muatData", 6));
        for (int l = 1; l <= 4; l++) {
            versiA.add(new Frame("formatBaris", 2));
            versiA.add(new Frame("hitungPajak", 7));
        }
        versiA.add(new Frame("simpanLaporan", 4));

        // Versi B: hitungPajak dihitung sekali, laporan berikutnya kena cache
        List<Frame> versiB = new ArrayList<>();
        versiB.add(new Frame("muatData", 6));
        versiB.add(new Frame("hitungPajak", 7));
        for (int l = 1; l <= 4; l++) {
            versiB.add(new Frame("formatBaris", 2));
            versiB.add(new Frame("hitungPajak (cache)", 1));
        }
        versiB.add(new Frame("simpanLaporan", 4));

        Map<String, Integer> sampelA = profilkan(versiA);
        Map<String, Integer> sampelB = profilkan(versiB);

        int totalA = totalUnit(versiA);
        int totalB = totalUnit(versiB);

        System.out.println("Interval sampling = " + INTERVAL + " satuan kerja");
        System.out.println();
        System.out.println("Versi A (tanpa cache): total " + totalA + " satuan, "
            + jumlahSampel(sampelA) + " sampel");
        cetakTabel(sampelA);
        cetakBar(sampelA, jumlahSampel(sampelA));
        System.out.println();
        System.out.println("Versi B (dengan cache): total " + totalB + " satuan, "
            + jumlahSampel(sampelB) + " sampel");
        cetakTabel(sampelB);
        cetakBar(sampelB, jumlahSampel(sampelB));
        System.out.println();
        double porsiA = 100.0 * sampelA.getOrDefault("hitungPajak", 0) / jumlahSampel(sampelA);
        double porsiB = 100.0 * sampelB.getOrDefault("hitungPajak", 0) / jumlahSampel(sampelB);
        System.out.printf("Pekerjaan turun %.1f%%, porsi sampel hitungPajak turun dari "
            + "%.1f%% menjadi %.1f%%%n", 100.0 * (totalA - totalB) / totalA, porsiA, porsiB);
        System.out.println("Sampel adalah taksiran; satuan kerja adalah kebenaran penuh");
    }

    static Map<String, Integer> profilkan(List<Frame> alur) {
        Map<String, Integer> sampel = new LinkedHashMap<>();
        int tick = 0; // sesi profiling berdiri sendiri, penghitung mulai dari 0
        for (Frame f : alur) {
            for (int u = 0; u < f.unit(); u++) {
                tick++;
                if (tick % INTERVAL == 0) {
                    sampel.merge(f.method(), 1, Integer::sum);
                }
            }
        }
        return sampel;
    }

    static int totalUnit(List<Frame> alur) {
        int t = 0;
        for (Frame f : alur) {
            t += f.unit();
        }
        return t;
    }

    static int jumlahSampel(Map<String, Integer> sampel) {
        int n = 0;
        for (int v : sampel.values()) {
            n += v;
        }
        return n;
    }

    static void cetakTabel(Map<String, Integer> sampel) {
        for (var e : sampel.entrySet()) {
            System.out.printf("  %-19s %2d sampel%n", e.getKey(), e.getValue());
        }
    }

    // Flame graph mini: lebar batang sebanding jumlah sampel
    static void cetakBar(Map<String, Integer> sampel, int totalSampel) {
        System.out.println("  flame graph mini:");
        for (var e : sampel.entrySet()) {
            int lebar = Math.round(20.0f * e.getValue() / totalSampel);
            System.out.println("    " + "#".repeat(lebar) + " " + e.getKey());
        }
    }
}
