Latar Belakang: Crash Senyap pada Worker Thread
Saat mem-porting pipeline AST berskala besar seperti React Compiler ke Rust (merujuk pada investigasi terkait arsitektur parser dan traversal pohon sintaksis), tantangan stabilitas sering muncul pada tahap pengujian beban ekstrem. Gejala yang umum dihadapi adalah worker thread berhenti tiba-tiba tanpa mencetak stack trace Rust standar (panic_hook tidak terpicu) atau proses worker mati dengan kode status SIGSEGV (Segmentation Fault).
Masalah ini terjadi ketika worker memproses input JSX atau ekspresi JavaScript dengan tingkat nesting ekstrem (ribuan elemen bersarang). Compiler worker default biasanya dijalankan di dalam thread pool (misalnya via Rayon atau thread OS biasa) dengan ukuran stack terbatas. Ketika konsumsi stack melewati batas, OS guard page terpicu dan langsung menghentikan proses.
Root Cause: Rekursi Implisit dan Default Thread Stack
Secara default, thread pada Linux dan macOS dialokasikan dengan stack sekitar 2 MB hingga 8 MB. Pada implementasi traversal pohon sintaksis konvensional, pola Visitor umumnya ditulis secara rekursif:
fn visit_node(&mut self, node: &AstNode) {
self.process(node);
for child in &node.children {
self.visit_node(child);
}
}Setiap pemanggilan fungsi rekursif mengalokasikan stack frame baru yang berisi pointer, variabel lokal, register spill, dan status alinyemen. Pada payload JSX bersarang sedalam 15.000 level:
- Frame overhead rata-rata: 128 hingga 512 byte per pemanggilan.
- Total kebutuhan stack: 15.000 × 256 byte ≈ 3.84 MB.
Jika kedalaman meningkat atau frame lokal memuat struktur data enum yang besar (seperti varian AstNode yang belum dioptimasi ukurannya menggunakan Box), stack terlewati seketika. Rust tidak dapat melakukan stack unwinding reguler karena tidak ada sisa memori stack untuk menjalankan rutinitas panic runtime.
Langkah Isolasi dan Debugging via rust-lldb
Untuk mengonfirmasi bahwa terminasi thread disebabkan oleh stack overflow dan bukan memory corruption murni, isolasi proses worker menggunakan debugger lldb atau rust-lldb.
1. Buat Payload Reproduksi Minimal
Hindari debugging menggunakan payload bundel penuh. Gunakan generator sederhana untuk memproduksi AST bersarang:
// Generator payload minimal: <div><div>...</div></div>
let mut payload = String::from("<div>");
for _ in 0..20_000 {
payload.push_str("<div>");
}
payload.push_str("leaf");
for _ in 0..20_000 {
payload.push_str("</div>");
}
payload.push_str("</div>");2. Eksekusi via Debugger
Jalankan binary worker di bawah kendali rust-lldb:
cargo build
rust-lldb -- ./target/debug/compiler_worker --input deep_nested.jsonSaat crash terjadi, debugger menangkap sinyal EXC_BAD_ACCESS atau SIGSEGV:
* thread #3, name = 'worker-pool-1', stop reason = EXC_BAD_ACCESS (code=2, address=0x700008f5bfb8)
frame #0: 0x000000010001a4e2 compiler_worker::visitor::visit_element
(lldb) bt 10
* frame #0: compiler_worker::visitor::visit_element
frame #1: compiler_worker::visitor::visit_element
frame #2: compiler_worker::visitor::visit_element
frame #3: compiler_worker::visitor::visit_element
frame #4: compiler_worker::visitor::visit_element
frame #5: compiler_worker::visitor::visit_element
frame #6: compiler_worker::visitor::visit_element
frame #7: compiler_worker::visitor::visit_element
frame #8: compiler_worker::visitor::visit_element
frame #9: compiler_worker::visitor::visit_elementBacktrace yang berulang pada frame yang sama mengonfirmasi terjadinya rekursi tak terbatas atau kehabisan alokasi call stack.
Solusi: Iterative Traversal dengan Explicit Heap Stack
Solusi yang benar secara arsitektural bukan memperbesar ukuran thread stack via environment variable RUST_MIN_STACK, melainkan memindahkan alokasi context traversal dari thread stack (terbatas) ke heap memory (dinamis, dibatasi oleh RAM sistem). Selain itu, pasang depth limit guard untuk menolak payload abnormal sebelum menghabiskan resource sistem.
Implementasi Traversal Iteratif
Berikut adalah implementasi AST traversal berbasis explicit stack (Vec) dan guard pelindung:
#[derive(Debug, PartialEq, Eq)]
pub enum AstNode {
Element {
tag: String,
children: Vec<AstNode>,
},
Text(String),
}
pub struct TraversalConfig {
pub max_depth: usize,
}
impl Default for TraversalConfig {
fn default() -> Self {
Self {
// Batas kedalaman aman traversal compiler
max_depth: 5_000,
}
}
}
#[derive(Debug, PartialEq, Eq)]
pub enum TraversalError {
MaxDepthExceeded(usize),
}
/// Traversal iteratif eksplisit pada heap untuk mencegah thread stack overflow.
pub fn walk_ast_iterative<F>(
root: &AstNode,
config: &TraversalConfig,
mut visitor: F,
) -> Result<(), TraversalError>
where
F: FnMut(&AstNode, usize),
{
// ponytail: Vec dialokasikan pada heap, aman untuk jutaan node.
let mut stack: Vec<(&AstNode, usize)> = Vec::new();
stack.push((root, 0));
while let Some((current_node, current_depth)) = stack.pop() {
if current_depth > config.max_depth {
return Err(TraversalError::MaxDepthExceeded(current_depth));
}
visitor(current_node, current_depth);
if let AstNode::Element { children, .. } = current_node {
// Push anak dalam urutan terbalik agar diproses berurutan dari kiri ke kanan
for child in children.iter().rev() {
stack.push((child, current_depth + 1));
}
}
}
Ok(())
}Unit Test: Verifikasi Kedalaman Ekstrem
Kode uji berikut membuktikan bahwa traversal iteratif mampu menangani kedalaman 50.000 node tanpa memicu stack overflow, serta memastikan depth limit berfungsi sesuai kontrak API.
#[cfg(test)]
mod tests {
use super::*;
fn build_deep_ast(depth: usize) -> AstNode {
let mut current = AstNode::Text("leaf".to_string());
for i in 0..depth {
current = AstNode::Element {
tag: format!("div_{}", i),
children: vec![current],
};
}
current
}
#[test]
fn test_iterative_traversal_deep_nesting_success() {
let target_depth = 50_000;
let root = build_deep_ast(target_depth);
let config = TraversalConfig {
max_depth: 60_000,
};
let mut visited_count = 0;
let result = walk_ast_iterative(&root, &config, |_node, _depth| {
visited_count += 1;
});
assert!(result.is_ok());
assert_eq!(visited_count, target_depth + 1);
}
#[test]
fn test_depth_limit_guard_triggers() {
let root = build_deep_ast(100);
let config = TraversalConfig {
max_depth: 50,
};
let result = walk_ast_iterative(&root, &config, |_node, _depth| {});
assert_eq!(result, Err(TraversalError::MaxDepthExceeded(51)));
}
}Trade-off dan Pertimbangan Desain
- Alokasi Heap vs Stack: Alokasi
Vecpada heap memiliki amortized cost yang sedikit lebih tinggi daripada penambahan pointer stack frame mentah. Namun, stabilitas proses dan isolasi terhadap input crash jauh lebih diprioritaskan pada production compiler. - Node Optimization: Jika AST node berukuran besar karena varian enum yang tidak seimbang, gunakan
Box<T>pada varian yang besar untuk memperkecil ukuran footprint saat elemen dimuat ke dalam memori. - Two-pass Processing: Jika AST memerlukan manipulasi post-order (keluar dari node setelah seluruh anak selesai diproses), struktur stack iteratif dapat diperluas menggunakan frame status:
enum Action { Enter(&Node), Leave(&Node) }.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!