Lonjakan CPU 100% dan worker thread yang tidak merespons (hang) pada engine simulator web kerap berakar dari kegagalan integrasi numerik saat memproses beban kompresi ekstrem. Ketika client mengirimkan parameter rasio kompresi tinggi melalui WebSocket, solver termodinamika dapat terjebak dalam infinite loop akibat floating-point divergence dan akumulasi step size sub-tick tanpa batas.

Gejala dan Arsitektur Masalah

Pada arsitektur engine simulator interaktif (serupa konsep combustionlab.net), backend worker menerima stream parameter siklus mesin—seperti bore, stroke, dan rasio kompresi (CR > 18:1)—secara real-time melalui WebSocket. Worker bertugas menghitung dinamika fluida ruang bakar menggunakan solver diferensial biasa (ODE).

Masalah muncul ketika nilai rasio kompresi melonjak secara instan. Worker node langsung mencapai penggunaan CPU 100%, event loop terblokir, heartbeat WebSocket terputus, dan simulasi berhenti total tanpa log fatal error.

Root Cause: Floating-Point Divergence dan Sub-Tick Drift

Solver menggunakan algoritma adaptif Runge-Kutta (RK45) untuk mempertahankan akurasi dengan mengevaluasi lokal truncation error (LTE). Persamaan diferensial untuk gradien tekanan silinder adalah:

dP/dθ = (γ - 1) * (dQ/dθ) / V(θ) - γ * (P / V(θ)) * (dV/dθ)

Ketika piston mendekati Top Dead Center (TDC) dengan rasio kompresi tinggi, volume silinder V(θ) mendekati nol, menyebabkan nilai turunan dP/dθ melonjak ekstrem (stiff problem). Solver adaptif merespons dengan mengecilkan step size (dt) secara eksponensial guna mempertahankan toleransi error.

Dua kegagalan sistemik terjadi bersamaan:

  1. Sub-Tick Step Drift: Nilai dt mengecil melewati presisi representasi floating-point IEEE 754 (misalnya dt < 1e-16), sehingga operasi t + dt menghasilkan t kembali. Waktu simulasi berhenti maju sementara loop integrasi terus berjalan.
  2. Math Divergence: Evaluasi intermediate stage RK4 menghasilkan NaN atau Infinity akibat pembagian dengan volume mendekati nol. Kondisi evaluasi toleransi error LTE < TOL menghasilkan nilai false secara konsisten, memaksa solver mengecilkan dt tanpa henti (infinite loop).

Isolasi Bug Menggunakan CPU Profiling

Analisis flamegraph menggunakan Node.js inspector / Linux perf menunjukkan alokasi waktu eksekusi terkonsentrasi penuh pada satu loop eksekusi:

# Jalankan worker dengan inspector aktif
node --cpu-prof --cpu-prof-name=engine_stall.cpuprofile worker.js

# Analisis stack teratas menggunakan clinic flame atau v8-profiler
clinic flame --on-cpu-prof engine_stall.cpuprofile

Flamegraph mengonfirmasi 99.4% siklus CPU dihabiskan di dalam blok fungsi RK45Solver.integrateStep(), tepatnya pada loop pengecekan batas error lokal tanpa adanya I/O interupsi.

Solusi: Algoritma Solver dengan Safety Clamp

Untuk menstabilkan komputasi, algoritma solver membutuhkan tiga pengaman: batas bawah step size (MIN_STEP), penanganan batas error invalid (NaN/Inf), dan batas maksimal iterasi per frame simulasi.

export class ThermodynamicSolver {
  constructor(config = {}) {
    this.minStep = config.minStep || 1e-7;
    this.maxStep = config.maxStep || 1e-2;
    this.maxSubSteps = config.maxSubSteps || 2000;
    this.tolerance = config.tolerance || 1e-5;
  }

  // Evaluasi derivatif P dan T terhadap waktu simulasi
  derivatives(t, state, params) {
    const { pressure, volume } = state;
    if (volume <= 1e-6) return { dP: 0, dT: 0 }; // Hindari singularitas

    const gamma = 1.35; // Rasio kalor spesifik
    const dV = params.getVolumeDerivative(t);
    const dQ = params.getHeatRelease(t);

    const dP = ((gamma - 1) * dQ - gamma * pressure * dV) / volume;
    return { dP, dT: dP / (pressure * 0.001) };
  }

  step(state, dtTarget, params) {
    let tAccumulated = 0;
    let iterations = 0;
    let dt = Math.min(this.maxStep, dtTarget);

    while (tAccumulated < dtTarget) {
      if (++iterations > this.maxSubSteps) {
        // Fallback: paksa konvergensi jika loop melebihi alokasi waktu budget
        tAccumulated = dtTarget;
        break;
      }

      // Clamp step size agar tidak terjadi floating-point precision loss
      dt = Math.max(this.minStep, Math.min(dt, dtTarget - tAccumulated));

      const k1 = this.derivatives(tAccumulated, state, params);
      const intermediateState = {
        pressure: state.pressure + k1.dP * dt,
        volume: params.getVolume(tAccumulated + dt)
      };
      const k2 = this.derivatives(tAccumulated + dt, intermediateState, params);

      // Hitung Local Truncation Error (LTE)
      const error = Math.abs((k2.dP - k1.dP) * dt * 0.5);

      if (Number.isNaN(error) || !Number.isFinite(error)) {
        // Recover dari divergence numerik: reset step dan clamp turunan
        dt = this.minStep;
        state.pressure = Math.max(101325, state.pressure); // Patok ke tekanan atmosfer
        tAccumulated += dt;
        continue;
      }

      if (error <= this.tolerance || dt <= this.minStep) {
        // Terima step dan perbarui state
        state.pressure += ((k1.dP + k2.dP) / 2) * dt;
        tAccumulated += dt;

        // Naikkan step size secara bertahap untuk mempertahankan throughput
        dt = Math.min(this.maxStep, dt * 1.5);
      } else {
        // Tolak step dan perkecil ukuran integrasi
        dt = Math.max(this.minStep, dt * 0.5);
      }
    }

    return state;
  }
}

Unit Test: Validasi Konvergensi dan Kinerja

Pengujian regresi berikut memvalidasi bahwa rasio kompresi ekstrem tidak menyebabkan infinite loop, melempar unhandled exception, atau membekukan thread.

import { strict as assert } from 'node:assert';
import { ThermodynamicSolver } from './thermodynamic-solver.js';

function runConvergenceTest() {
  const solver = new ThermodynamicSolver({
    minStep: 1e-6,
    maxSubSteps: 500,
    tolerance: 1e-4
  });

  const state = { pressure: 101325, volume: 0.00005 };
  const params = {
    getVolume: () => 0.000001, // Kondisi TDC kompresi tinggi (stiff)
    getVolumeDerivative: () => -0.05,
    getHeatRelease: () => 1200
  };

  const startTime = performance.now();
  
  // Integrasi 1 frame waktu simulasi (16ms / 60 FPS)
  const result = solver.step(state, 0.016, params);
  
  const executionTimeMs = performance.now() - startTime;

  // 1. Thread tidak terblokir (eksekusi di bawah 50ms)
  assert.ok(executionTimeMs < 50, `Execution too slow: ${executionTimeMs}ms`);

  // 2. Output numerik valid (tidak divergen ke NaN atau Infinity)
  assert.ok(Number.isFinite(result.pressure), 'Pressure must be finite');
  assert.ok(!Number.isNaN(result.pressure), 'Pressure must not be NaN');

  console.log('Regression test passed: Solver converges cleanly under stiff conditions.');
}

runConvergenceTest();

Kesimpulan dan Trade-Off

Pemberian minStep safety clamp dan maxSubSteps membatasi konsumsi siklus CPU saat ODE mengalami stiff stiffness. Pendekatan ini mengorbankan akurasi termodinamika mikro pada rentang derajat engkol ekstrem, namun menjamin persistensi koneksi WebSocket, stabilitas throughput engine simulator, dan keandalan runtime backend.