atmos/os_lib/web_engine/cache.rs
1// web_engine/cache.rs - HTTP disk cache (SylFS /cache/browser/<host>/<path_hash>)
2
3use alloc::string::String;
4use alloc::vec::Vec;
5
6/// URL を SylFS のキャッシュパスへ変換する
7pub(super) fn cache_path_for(host: &str, path: &str) -> String {
8 // 名前の組み立ては純粋モジュールへ切り出した(試験あり)。
9 super::cache_path::cache_path_for(host, path)
10}
11
12/// ネットワーク取得済みバイト列をキャッシュに書き込む
13pub(super) fn write_cache(host: &str, path: &str, data: &[u8]) {
14 // 【2026-07-31】以前あったサイズ上限(約 85KB)は撤去した。**もう実態と合っていない**。
15 // SylFS はエクステント方式へ移行済みで、1 ファイルの上限は
16 // 実質ボリュームサイズまで拡大している(`kernel/fs/mod.rs` の
17 // `MAX_INLINE_EXTENTS` + オーバーフローセクタ)。
18 //
19 // 古い上限が残っていたため、Font Awesome の woff2(101088 バイト)や
20 // Noto Sans JP(5.3MB)が**キャッシュされず毎回ダウンロードし直し**に
21 // なっていた。非同期取得は完了しているのに、次のレイアウトで
22 // キャッシュ命中しないので永久に「取得中」のままだった。
23 // 【2026-08-25】**空データは保存しない**。
24 //
25 // 一度 0 バイトを書くと、以後は毎回キャッシュ命中して
26 // **恒久的に空が返り続ける**(フォントなら永久に豆腐、
27 // CSS なら永久に素の HTML)。ディスクキャッシュは
28 // 再起動をまたぐので、自力では回復しない。
29 //
30 // 呼び出しは 6 箇所あり、各所で番人を書くと必ずどこかが漏れる。
31 // **入口で一度だけ守る**(実際 `font_queue` の 1 箇所は
32 // `!bytes.is_empty()` を持っていたが、まとめ取得側は持っていなかった)。
33 if data.is_empty() {
34 crate::warn!("[CACHE] 空データは保存しない host={} path={}", host, path);
35 return;
36 }
37 let cp = cache_path_for(host, path);
38 // 【2026-08-05】取得を 4 並列にしてから、複数ワーカーが同時にここへ入り
39 // ビットマップを壊していた(`kernel::fs::FS_LOCK` の説明を参照)。
40 // 保存と読み返しの間も手放さない(間に別スレッドが割り込むと
41 // 「保存したのに読めない」が再発する)。
42 let _fs_guard = crate::kernel::fs::FS_LOCK.lock();
43 let fs = crate::kernel::fs::get_fs();
44 // 保存結果を握り潰さない。失敗したら次回また取得しに行くので、
45 // なぜキャッシュが効かないのか分かるようにしておく。
46 let _t = super::perf::start();
47 let saved = fs.save_file(&cp, data, "browser_cache");
48 super::perf::add(super::perf::Slot::CacheWrite, _t);
49 // 【2026-08-03】保存は成功を返すのに、次のレイアウトで読み出すと
50 // 見つからない事象を追う(フォント 8 本取得成功・キャッシュ命中 1 件)。
51 // **不整合なときだけ**記録する(成功時は黙る。L-3)。
52 if saved.is_ok() {
53 match fs.read_file(&cp) {
54 Some((_m, d)) if d.len() == data.len() => {}
55 Some((_m, d)) => {
56 crate::warn!(
57 "[CACHE] 保存直後の読み返しでサイズが不一致 key={} 書込={} 読出={}",
58 cp,
59 data.len(),
60 d.len()
61 );
62 }
63 None => {
64 crate::warn!(
65 "[CACHE] 保存は成功したのに読み返せない key={} bytes={}",
66 cp,
67 data.len()
68 );
69 }
70 }
71 }
72 if let Err(e) = saved {
73 crate::warn!(
74 "[CACHE] 保存に失敗 host={} path={} bytes={} err={}",
75 host,
76 path,
77 data.len(),
78 e
79 );
80 }
81}
82
83/// キャッシュからバイト列を読み込む。存在しなければ None
84pub(super) fn read_cache(host: &str, path: &str) -> Option<Vec<u8>> {
85 let cp = cache_path_for(host, path);
86 // 【2026-08-05】取得を 4 並列にしてから、複数ワーカーが同時にここへ入り
87 // ビットマップを壊していた(`kernel::fs::FS_LOCK` の説明を参照)。
88 // 保存と読み返しの間も手放さない(間に別スレッドが割り込むと
89 // 「保存したのに読めない」が再発する)。
90 let _fs_guard = crate::kernel::fs::FS_LOCK.lock();
91 let fs = crate::kernel::fs::get_fs();
92 // 【2026-07-29】空のキャッシュエントリは「取得済み」として扱わない。
93 // 過去に 0 バイトを書き込んでしまった場合、それを返し続けると
94 // 恒久的に空の CSS/JS が使われる(再起動をまたいで残るため自己回復しない)。
95 // 空はミス扱いにして再取得させる。書き込み側でも空は保存しない。
96 let _t = super::perf::start();
97 let got = fs.read_file(&cp).map(|(_meta, data)| data);
98 super::perf::add(super::perf::Slot::CacheRead, _t);
99 got.filter(|d| !d.is_empty())
100}
101
102/// /cache/browser/ 以下の全キャッシュを削除する
103pub fn clear_browser_cache() {
104 // 一覧取得と削除の間に別スレッドが書き込むと、消したはずの枠へ
105 // 中途半端な状態が残る。操作全体でロックを保持する。
106 let _fs_guard = crate::kernel::fs::FS_LOCK.lock();
107 let fs = crate::kernel::fs::get_fs();
108 let files = fs.list_dir("/cache/browser/");
109 for meta in files {
110 let name = meta.get_filename();
111 if name.starts_with("/cache/browser/") {
112 let _ = fs.delete_file(name);
113 }
114 }
115 crate::info!("[SYS] BROWSER: cache cleared");
116}