Skip to main content

atmos/os_lib/js/
cycle_gc.rs

1//! 参照カウントでは回収できない循環ゴミの回収(試行削除 / trial deletion)。
2//!
3//! 【2026-10-19】JS の値は `Rc<RefCell<…>>` の参照カウントで管理しており、
4//! 「関数呼び出しのスコープ → 中で作った関数 → そのスコープ」のような循環は
5//! ページを閉じるまで解放されなかった。YouTube の視聴ページ(kevlar_base 10.8MB)の
6//! 実行中に 512MB のヒープを使い切っていた。
7//!
8//! ## 方式
9//! 生きている `Obj` と `Scope` をすべて台帳([`Registry`])に登録しておき、回収時に
10//! 1. 台帳の全節点について、節点どうしの参照(辺)の本数を数える(内部参照数)。
11//! 2. 強参照数 − 内部参照数 > 0 の節点は、台帳の外(Rust のスタック・大域・DOM 側の
12//!    リスナ等)から参照されている「根」。
13//! 3. 根から辺をたどって届く節点に印を付ける。印の無い節点は外から届かない循環ゴミ。
14//! 4. ゴミの中身(プロパティ・変数・原型など)を空にして循環を断ち、参照カウントで
15//!    解放させる。
16//!
17//! ## 安全性
18//! - 数える辺は「実在する強参照」だけ(数え漏れは安全側: その先の節点が根に見える)。
19//!   共有され得る中間の `Rc`(Promise の状態など)の先は数えない。
20//! - 借用中で中身を読めない節点は根として扱い、その先も数えない(同じく安全側)。
21//! - Rust のスタックが保持している値は強参照数に表れるので、実行の途中でも回収できる。
22//! - JS は 1 つのスレッド(ブラウザのアプリのプロセス)でしか動かない前提。台帳自体は
23//!   spin ロックで守るが、回収は台帳の全節点を触るので、別スレッドで同時に JS が動く
24//!   構成になったら台帳をスレッドごとに分ける必要がある。
25//! - `Obj`・`Scope` を `Rc` から取り出したり、中身ごと入れ替えたりしてはならない
26//!   (台帳の番地と中身の番号がずれる)。
27
28use alloc::rc::Rc;
29use alloc::vec::Vec;
30use core::cell::RefCell;
31
32use super::interp::Scope;
33use super::value::{Obj, ObjKind, Value};
34
35use core::sync::atomic::{AtomicUsize, Ordering};
36
37/// 前回の回収から登録した数。
38static MADE: AtomicUsize = AtomicUsize::new(0);
39/// 次に回収する登録数(前回の生存数に比例させ、回収の手間を償却で O(1) にする)。
40static NEXT_AT: AtomicUsize = AtomicUsize::new(MIN_INTERVAL);
41/// 回収の最小間隔(登録数)。
42const MIN_INTERVAL: usize = 100_000;
43
44/// 回収する頃合いか(呼び出し経路から頻繁に呼ぶので原子変数だけ見る)。
45pub fn should_collect() -> bool {
46    MADE.load(Ordering::Relaxed) >= NEXT_AT.load(Ordering::Relaxed)
47}
48
49/// 頃合いなら回収する。
50pub fn maybe_collect() -> Option<GcStats> {
51    if !should_collect() {
52        return None;
53    }
54    let st = collect(true);
55    let live = (st.objs + st.scopes).saturating_sub(st.garbage_objs + st.garbage_scopes);
56    NEXT_AT.store(MIN_INTERVAL + live, Ordering::Relaxed);
57    Some(st)
58}
59
60/// 未登録を表す台帳の番号。
61pub const NO_SLOT: u32 = u32::MAX;
62
63/// 生きている `Obj`・`Scope` の台帳(`Rc::as_ptr` の番地。空きは 0)。
64struct Registry {
65    objs: Vec<usize>,
66    obj_free: Vec<u32>,
67    scopes: Vec<usize>,
68    scope_free: Vec<u32>,
69}
70
71static REGISTRY: spin::Mutex<Registry> = spin::Mutex::new(Registry {
72    objs: Vec::new(),
73    obj_free: Vec::new(),
74    scopes: Vec::new(),
75    scope_free: Vec::new(),
76});
77
78fn put(list: &mut Vec<usize>, free: &mut Vec<u32>, p: usize) -> u32 {
79    if let Some(i) = free.pop() {
80        if let Some(e) = list.get_mut(i as usize) {
81            *e = p;
82            return i;
83        }
84    }
85    list.push(p);
86    (list.len() - 1) as u32
87}
88
89/// 作った `Obj` を台帳に載せる(`value.rs` の構築関数が呼ぶ)。
90pub fn track_obj(rc: Rc<RefCell<Obj>>) -> Rc<RefCell<Obj>> {
91    MADE.fetch_add(1, Ordering::Relaxed);
92    let p = Rc::as_ptr(&rc) as usize;
93    let slot = {
94        let mut r = REGISTRY.lock();
95        let Registry { objs, obj_free, .. } = &mut *r;
96        put(objs, obj_free, p)
97    };
98    if let Ok(mut b) = rc.try_borrow_mut() {
99        b.gc_slot = slot;
100    }
101    rc
102}
103
104/// 作った `Scope` を台帳に載せる。
105pub fn track_scope(rc: Rc<RefCell<Scope>>) -> Rc<RefCell<Scope>> {
106    MADE.fetch_add(1, Ordering::Relaxed);
107    let p = Rc::as_ptr(&rc) as usize;
108    let slot = {
109        let mut r = REGISTRY.lock();
110        let Registry { scopes, scope_free, .. } = &mut *r;
111        put(scopes, scope_free, p)
112    };
113    if let Ok(mut b) = rc.try_borrow_mut() {
114        b.gc_slot = slot;
115    }
116    rc
117}
118
119/// `Obj` の解放時に台帳から外す。
120pub fn untrack_obj(slot: u32) {
121    if slot == NO_SLOT {
122        return;
123    }
124    let mut r = REGISTRY.lock();
125    if let Some(e) = r.objs.get_mut(slot as usize) {
126        *e = 0;
127        r.obj_free.push(slot);
128    }
129}
130
131/// `Scope` の解放時に台帳から外す。
132pub fn untrack_scope(slot: u32) {
133    if slot == NO_SLOT {
134        return;
135    }
136    let mut r = REGISTRY.lock();
137    if let Some(e) = r.scopes.get_mut(slot as usize) {
138        *e = 0;
139        r.scope_free.push(slot);
140    }
141}
142
143
144/// 台帳の節点(一時的に強参照を 1 本持つ)。
145enum Node {
146    O(Rc<RefCell<Obj>>),
147    S(Rc<RefCell<Scope>>),
148}
149
150impl Node {
151    fn strong(&self) -> usize {
152        match self {
153            Node::O(r) => Rc::strong_count(r),
154            Node::S(r) => Rc::strong_count(r),
155        }
156    }
157}
158
159/// 節点が直接持つ強参照の先(節点番号)を `out` に積む。読めなければ false。
160/// 節点番号は `Obj` が台帳の番号、`Scope` が `n_objs + 台帳の番号`。
161/// 番号を読めない(借用中・未登録の)先は積まない(安全側)。
162fn children(n: &Node, n_objs: usize, out: &mut Vec<usize>) -> bool {
163    let obj = |o: &Rc<RefCell<Obj>>, out: &mut Vec<usize>| {
164        if let Ok(b) = o.try_borrow() {
165            if b.gc_slot != NO_SLOT {
166                out.push(b.gc_slot as usize);
167            }
168        }
169    };
170    let scope = |s: &Rc<RefCell<Scope>>, out: &mut Vec<usize>| {
171        if let Ok(b) = s.try_borrow() {
172            if b.gc_slot != NO_SLOT {
173                out.push(n_objs + b.gc_slot as usize);
174            }
175        }
176    };
177    let val = |v: &Value, out: &mut Vec<usize>| {
178        if let Value::Object(o) = v {
179            obj(o, out);
180        }
181    };
182    match n {
183        Node::S(s) => {
184            let Ok(b) = s.try_borrow() else { return false };
185            if let Some(p) = &b.parent {
186                scope(p, out);
187            }
188            for v in b.vars.values() {
189                val(v, out);
190            }
191            true
192        }
193        Node::O(o) => {
194            let Ok(b) = o.try_borrow() else { return false };
195            if let Some(p) = &b.proto {
196                obj(p, out);
197            }
198            for v in b.props.values() {
199                val(v, out);
200            }
201            for (_, a) in b.accessors.iter() {
202                if let Some(g) = &a.get {
203                    val(g, out);
204                }
205                if let Some(s) = &a.set {
206                    val(s, out);
207                }
208            }
209            match &b.kind {
210                ObjKind::Array(items) | ObjKind::SetObj(items) => {
211                    for v in items {
212                        val(v, out);
213                    }
214                }
215                ObjKind::MapObj(entries) => {
216                    for (k, v) in entries {
217                        val(k, out);
218                        val(v, out);
219                    }
220                }
221                ObjKind::Function(f) => {
222                    scope(&f.closure, out);
223                    if let Some(bt) = &f.bound_this {
224                        val(bt, out);
225                    }
226                }
227                ObjKind::Proxy { target, handler } => {
228                    obj(target, out);
229                    obj(handler, out);
230                }
231                ObjKind::Bound { target, bound_this, bound_args } => {
232                    val(target, out);
233                    val(bound_this, out);
234                    for v in bound_args {
235                        val(v, out);
236                    }
237                }
238                // Promise・Generator・RegExp 等は共有され得る中間の Rc 越しなので数えない(安全側)。
239                _ => {}
240            }
241            true
242        }
243    }
244}
245
246/// 回収の結果。
247#[derive(Debug, Default, Clone, Copy)]
248pub struct GcStats {
249    pub objs: usize,
250    pub scopes: usize,
251    pub garbage_objs: usize,
252    pub garbage_scopes: usize,
253}
254
255/// 循環ゴミを探す。`free` が真なら回収もする(偽なら数えるだけ)。
256pub fn collect(free: bool) -> GcStats {
257    // 1. 台帳の各節点に一時的な強参照を持つ。強参照を増やすだけで解放は起きないので、
258    //    ロックを持ったまま行ってよい(解放時の台帳操作と競合しない)。
259    let (nodes, n_objs) = {
260        let r = REGISTRY.lock();
261        MADE.store(0, Ordering::Relaxed);
262        let n_objs = r.objs.len();
263        let mut nodes: Vec<Option<Node>> = Vec::with_capacity(n_objs + r.scopes.len());
264        for &p in &r.objs {
265            nodes.push((p != 0).then(|| {
266                // SAFETY: 台帳の番地は `Rc::as_ptr` で得たもので、解放(Drop)時に台帳から
267                // 外す。ロック中なので外される途中の節点は無い。
268                Node::O(unsafe {
269                    let p = p as *const RefCell<Obj>;
270                    Rc::increment_strong_count(p);
271                    Rc::from_raw(p)
272                })
273            }));
274        }
275        for &p in &r.scopes {
276            nodes.push((p != 0).then(|| {
277                // SAFETY: 同上。
278                Node::S(unsafe {
279                    let p = p as *const RefCell<Scope>;
280                    Rc::increment_strong_count(p);
281                    Rc::from_raw(p)
282                })
283            }));
284        }
285        (nodes, n_objs)
286    };
287    let n = nodes.len();
288    let mut stats = GcStats::default();
289    for (i, x) in nodes.iter().enumerate() {
290        if x.is_some() {
291            if i < n_objs {
292                stats.objs += 1;
293            } else {
294                stats.scopes += 1;
295            }
296        }
297    }
298
299    // 2. 内部参照数を数える。読めない節点は根にする。
300    // 状態: 下位 31 ビットが内部参照数、最上位ビットが「根・到達済み」の印。
301    const MARK: u32 = 1 << 31;
302    let mut st: Vec<u32> = alloc::vec![0; n];
303    let mut buf: Vec<usize> = Vec::new();
304    for (i, x) in nodes.iter().enumerate() {
305        let Some(x) = x else { continue };
306        buf.clear();
307        if !children(x, n_objs, &mut buf) {
308            st[i] |= MARK;
309            continue;
310        }
311        for &j in &buf {
312            if j < n && nodes[j].is_some() && (st[j] & !MARK) < (MARK - 1) {
313                st[j] += 1;
314            }
315        }
316    }
317
318    // 3. 根(外から参照がある、または読めない)から印を付ける。
319    let mut stack: Vec<usize> = Vec::new();
320    for (i, x) in nodes.iter().enumerate() {
321        let Some(x) = x else { continue };
322        // 強参照数には、ここで持っている一時的な 1 本が含まれる。
323        let internal = (st[i] & !MARK) as usize;
324        if st[i] & MARK != 0 || x.strong().saturating_sub(1) > internal {
325            st[i] |= MARK;
326            stack.push(i);
327        }
328    }
329    while let Some(i) = stack.pop() {
330        let Some(x) = &nodes[i] else { continue };
331        buf.clear();
332        if !children(x, n_objs, &mut buf) {
333            continue;
334        }
335        for &j in &buf {
336            if j < n && nodes[j].is_some() && st[j] & MARK == 0 {
337                st[j] |= MARK;
338                stack.push(j);
339            }
340        }
341    }
342    drop(stack);
343    drop(buf);
344
345    // 4. 印の無い節点がゴミ。中身を取り出して循環を断つ。
346    let mut taken: Vec<Value> = Vec::new();
347    let mut taken_scopes: Vec<Rc<RefCell<Scope>>> = Vec::new();
348    let mut taken_kinds: Vec<ObjKind> = Vec::new();
349    for (i, x) in nodes.iter().enumerate() {
350        let Some(x) = x else { continue };
351        if st[i] & MARK != 0 {
352            continue;
353        }
354        match x {
355            Node::O(o) => {
356                stats.garbage_objs += 1;
357                if free {
358                    if let Ok(mut b) = o.try_borrow_mut() {
359                        if let Some(p) = b.proto.take() {
360                            taken.push(Value::Object(p));
361                        }
362                        let props = core::mem::take(&mut b.props);
363                        taken.extend(props.into_iter().map(|(_, v)| v));
364                        let acc = core::mem::take(&mut b.accessors);
365                        for (_, a) in acc {
366                            taken.extend(a.get);
367                            taken.extend(a.set);
368                        }
369                        taken_kinds.push(core::mem::replace(&mut b.kind, ObjKind::Plain));
370                    }
371                }
372            }
373            Node::S(s) => {
374                stats.garbage_scopes += 1;
375                if free {
376                    if let Ok(mut b) = s.try_borrow_mut() {
377                        if let Some(p) = b.parent.take() {
378                            taken_scopes.push(p);
379                        }
380                        let vars = core::mem::take(&mut b.vars);
381                        taken.extend(vars.into_values());
382                    }
383                }
384            }
385        }
386    }
387    drop(st);
388    // 取り出した参照とゴミの節点を手放す(ここで参照カウントが 0 になり解放される)。
389    drop(taken);
390    drop(taken_scopes);
391    drop(taken_kinds);
392    // 節点の一時的な強参照を手放す。ゴミはここで解放され、Drop が台帳から外す
393    // (ロックは持っていない)。
394    drop(nodes);
395    stats
396}