Worker queue in-memory yang memproses puluhan ribu job per detik sering kali mengalami bottleneck bukan karena batas komputasi logika bisnis, melainkan karena topologi alokasi memori. Antrian yang dibangun di atas array objek konvensional menyimpan referensi pointer ke lokasi heap yang tersebar. Pola akses ini memicu pointer chasing, membebani CPU dengan siklus tunggu data dari DRAM (cache miss), serta memicu siklus Garbage Collection (GC) yang melambungkan latensi p99.

Akar Masalah: Pointer Chasing dan Tekanan Garbage Collection

Saat worker mengambil sekumpulan task dalam batch melalui struktur seperti Array<Job> atau []*Job, CPU membaca array berisi pointer 64-bit. Setiap dereferensi pointer mengarah ke blok memori heap yang tidak berurutan. Karakteristik arsitektur CPU modern mengandalkan cache line (umumnya 64 byte). Ketika CPU memuat sebuah pointer ke L1/L2 cache, data payload yang sebenarnya berada di alamat berbeda, memaksa CPU mengambil data langsung dari RAM dengan biaya latensi 60 hingga 100 siklus (berbanding 4 siklus untuk L1 cache hit).

Masalah ini diperparah di runtime dengan managed memory (seperti Node.js V8, Go, atau Java Virtual Machine). Setiap instansiasi objek job pada antrian dialokasikan ke generasi muda (young generation / eden space). Saat batch selesai diproses, ribuan objek mati harus di-scan dan dibersihkan oleh GC:

  • V8 / Node.js: Memicu siklus Scavenge berulang dan Mark-Sweep major GC, menghasilkan stop-the-world (STW) mikro yang menghentikan event loop.
  • Go: Penumpukan pointer meningkatkan durasi heap scan phase pada GC konyuren, mencuri kuota CPU dari worker goroutine (GC Pacer work-stealing).

Desain Memori: Object-Oriented vs Data-Oriented Design (DOD)

Solusi teknis untuk mengatasi masalah ini adalah beralih dari Array of Objects (pointer-heavy) ke Data-Oriented Design berbasis contiguous memory buffer. Dalam pendekatan ini, seluruh payload job dipadatkan ke dalam blok memori tunggal yang berurutan (flat buffer atau TypedArray).

Prinsip: Satu alokasi buffer besar di awal (pre-allocated), nol dereferensi pointer saat pembacaan batch, dan nol objek baru yang didaftarkan ke GC engine.

Implementasi Worker Queue dengan Contiguous Buffer

Berikut implementasi in-memory worker queue di Node.js menggunakan ArrayBuffer dan DataView. Setiap task memiliki layout biner tetap (fixed-width struct) sebesar 16 byte: 4 byte untuk jobId (uint32), 4 byte untuk type (uint32), dan 8 byte untuk timestamp (float64).

// Layout per task: 16 bytes
// [0..3]: Job ID (Uint32)
// [4..7]: Job Type (Uint32)
// [8..15]: Timestamp (Float64)
const TASK_SIZE_BYTES = 16;

class ContiguousBatchQueue {
  constructor(capacity) {
    this.capacity = capacity;
    this.buffer = new ArrayBuffer(capacity * TASK_SIZE_BYTES);
    this.view = new DataView(this.buffer);
    this.head = 0;
    this.tail = 0;
    this.length = 0;
  }

  enqueue(jobId, jobType, timestamp) {
    if (this.length >= this.capacity) {
      throw new Error("Queue buffer overflow");
    }
    const byteOffset = this.tail * TASK_SIZE_BYTES;
    this.view.setUint32(byteOffset, jobId, true);
    this.view.setUint32(byteOffset + 4, jobType, true);
    this.view.setFloat64(byteOffset + 8, timestamp, true);

    this.tail = (this.tail + 1) % this.capacity;
    this.length++;
  }

  processBatch(batchSize, handler) {
    const processCount = Math.min(batchSize, this.length);
    for (let i = 0; i < processCount; i++) {
      const byteOffset = this.head * TASK_SIZE_BYTES;
      
      // Pembacaan sekuensial dari memory buffer yang sama:
      // Memaksimalkan L1/L2 prefetching hardware CPU
      const jobId = this.view.getUint32(byteOffset, true);
      const jobType = this.view.getUint32(byteOffset + 4, true);
      const timestamp = this.view.getFloat64(byteOffset + 8, true);

      handler(jobId, jobType, timestamp);

      this.head = (this.head + 1) % this.capacity;
      this.length--;
    }
    return processCount;
  }
}

// Eksekusi batch tanpa alokasi objek per task
const queue = new ContiguousBatchQueue(100000);
for (let i = 0; i < 50000; i++) {
  queue.enqueue(i, 1, performance.now());
}

queue.processBatch(50000, (id, type, ts) => {
  // Operasi komputasi berlangsung tanpa heap pointer chasing
});

Validasi Kinerja Menggunakan Linux perf dan Profiler

Untuk memverifikasi bahwa perubahan memory layout benar-benar memperbaiki utilisasi CPU cache, lakukan benchmarking proses batch menggunakan Linux perf:

# Rekam metrik cache untuk implementasi Object Array vs Contiguous Buffer
perf stat -e cache-misses,cache-references,L1-dcache-load-misses,cycles,instructions node worker-benchmark.js

Pada arsitektur Array of Objects tipikal di beban 1.000.000 task:

  • Rasio cache-misses umumnya berkisar antara 15% hingga 35% karena lompatan alamat heap.
  • Instruksi per siklus (IPC) berada di bawah 1.0 akibat CPU pipeline stall menunggu DRAM.

Pada arsitektur Contiguous Buffer:

  • L1-dcache-load-misses turun drastis (sering kali di bawah 2%) karena hardware prefetcher membaca baris cache berurutan secara otomatis.
  • Eksekusi flag V8 --trace-gc menunjukkan nol log minor/major GC selama iterasi batch berlangsung karena tidak ada alokasi objek heap baru.

Mitigasi Latensi p99 dan Trade-Offs

Pengurangan alokasi objek dan optimalisasi cache locality berdampak langsung pada pemangkasan tail latency (p99 dan p99.9). Lonjakan latensi mendadak yang biasanya diakibatkan oleh GC pause saat antrian penuh dapat dihilangkan secara deterministik.

Trade-Offs dan Batasan Desain:

  1. Fixed Schema: Contiguous buffer paling optimal untuk payload terstruktur atau numerik dengan panjang byte tetap. Jika task mengandung string berukuran variabel, Anda memerlukan secondary string table (offset-based) atau arena allocator.
  2. Ergonomi Kode: Anda kehilangan kemudahan serialisasi objek langsung (seperti JSON.parse). Akses data memerlukan perhitungan byte offset manual atau layer schema biner (misalnya FlatBuffers).
  3. Throughput vs Simplicity: Gunakan pola ini pada jalur kritis (hot path) dengan volume transaksi tinggi. Menerapkannya pada antrian bertarget IO eksternal lambat (seperti panggilan HTTP third-party) tidak akan memberikan dampak performa yang signifikan.