-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathjournalProbe.html
More file actions
108 lines (97 loc) · 7.11 KB
/
Copy pathjournalProbe.html
File metadata and controls
108 lines (97 loc) · 7.11 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
<!DOCTYPE html>
<html lang="ko">
<head><meta charset="utf-8"><title>probe: journal</title>
<style>body{font:14px/1.5 system-ui;margin:2rem;max-width:52rem} pre{background:#f4f4f5;padding:1rem;border-radius:6px;white-space:pre-wrap}</style></head>
<body>
<h1>probe: machineJournal (WAL) - 강제종료해도 마지막 커밋으로 부활하나</h1>
<p>진짜 OS의 죽음 내성. 개념 실측(2026-07-12)이 확정한 설계: 커밋 단위는 <b>문장이 아니라 유휴</b>다
(churnProbe: no-op 문장조차 ~95p를 더럽히고 그 집합은 97% 고정. 유휴 배치가 총 쓰기 88% 절감).
이 probe는 승격된 계약(<code>rt.enableJournal</code>)을 그대로 실측한다: hibernate(clean save) 없이
커널을 버려도 새 커널이 <code>recover()</code>로 부활해야 한다.</p>
<pre id="out">실행 중...</pre>
<script type="module">
import { bootSession } from "../../../index.js";
const out = document.getElementById("out");
const checks = []; const timings = {};
const check = (name, pass, info = "") => { checks.push({ name, pass: !!pass, info: String(info) }); out.textContent += `\n${pass ? "PASS" : "FAIL"} ${name}${info ? " (" + info + ")" : ""}`; };
const report = async () => {
const ok = checks.length > 0 && checks.every((c) => c.pass);
try { await fetch("/gateReport", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ok, checks, timings }) }); } catch (e) {}
};
try {
const opfs = await navigator.storage.getDirectory();
try { await opfs.removeEntry("journalProbe", { recursive: true }); } catch (e) {}
const dir = await opfs.getDirectoryHandle("journalProbe", { create: true });
// 커널 A: 저널을 켜고 일한다. hibernate는 부르지 않는다(= 강제종료 시나리오).
const a = await bootSession({});
const ja = a.rt.enableJournal({ dir, reactive: a.reactive, idleMs: 300 });
ja.start();
a.rt.run("log = []\nx = 0");
for (let i = 1; i <= 5; i++) a.rt.run(`x += ${i}\nlog.append(('step', ${i}, x))\nbig_${i} = bytearray(${i} * 40000)`);
check("커널 A: 작업 완료", a.rt.run("x") === 15 && a.rt.run("len(log)") === 5, `x=15, log=5`);
// 유휴 커밋: 실행이 멈추고 idleMs가 지나면 저절로 커밋된다(수동 저장 없음).
// 커밋은 비동기라 REPL을 막지 않지만, 완료까지는 시간이 든다(OPFS 파일 생성). 완료를 기다린다.
let t = performance.now();
for (let i = 0; i < 60 && ja.commits === 0; i++) await new Promise((res) => setTimeout(res, 100));
timings.idleCommitMs = Math.round(performance.now() - t); // 유휴 판정 + 커밋 완료까지
ja.stop();
timings.commits = ja.commits;
timings.pagesWritten = ja.pagesWritten;
check("유휴 커밋: 손대지 않아도 저절로 저장된다", ja.commits >= 1, `커밋 ${ja.commits}회, 쓴 페이지 ${ja.pagesWritten}p, 유휴판정+커밋 ${timings.idleCommitMs}ms`);
// A를 그냥 버린다(clean save/hibernate 호출 없음 = 크래시 등가)
// 커널 B: 리플레이 + 저널 재생 = 부활
t = performance.now();
const b = await bootSession({});
const jb = b.rt.enableJournal({ dir, reactive: b.reactive });
const rec = await jb.recover();
timings.recoverMs = Math.round(performance.now() - t);
check("커널 B: recover()로 부활(강제종료 내성)", !!rec && b.rt.run("x") === 15 && b.rt.run("len(log)") === 5,
rec ? `x=${b.rt.run("x")}, log=${b.rt.run("len(log)")}, ${rec.pages}p/${rec.mb}MB, ${timings.recoverMs}ms` : "저널 없음");
check("부활 후 연속 실행", b.rt.run("x += 100\nx") === 115);
check("부활 상태의 배열 생존", b.rt.run("len(big_5)") === 200000);
// 저널이 없으면 첫 부팅이다(예외가 아니라 null = 계약)
const emptyDir = await opfs.getDirectoryHandle("journalProbeEmpty", { create: true });
const jc = b.rt.enableJournal({ dir: emptyDir, reactive: b.reactive });
check("저널 없음 -> null(첫 부팅, 예외 아님)", (await jc.recover()) === null);
// CAS dedupe: 같은 내용의 페이지는 blob을 늘리지 않는다
const blobDir = await dir.getDirectoryHandle("blob");
let blobCount = 0; for await (const _ of blobDir.keys()) blobCount++;
timings.blobCount = blobCount;
check("CAS dedupe 동작(blob = 고유 내용 수)", blobCount > 0 && blobCount <= ja.pagesWritten, `blob ${blobCount} <= 쓴 ${ja.pagesWritten}`);
// 세대(HEAD/PREV): 명시적 2커밋으로 두 세대를 만들고 값(x)으로 구분한다.
// 커밋 단위는 유휴지만 commit()은 수동 경계로도 계약이다(중요한 지점을 명시 저장).
const gj = b.rt.enableJournal({ dir, reactive: b.reactive });
b.rt.run("x = 100");
await gj.commit(); // 세대 1 = x가 100
b.rt.run("x = 200");
await gj.commit(); // 세대 2 = x가 200 (세대 1은 PREV로 밀린다)
check("커널 B: 2세대 커밋(HEAD=200, PREV=100)", gj.commits === 2, `커밋 ${gj.commits}회`);
// 유효한 세대 1(x=100) JSON을 지금 확보해 둔다(뒤 검사들이 HEAD를 덮어쓰기 전에).
const hf = await dir.getFileHandle("HEAD.json");
const pf = await dir.getFileHandle("PREV.json");
const gen1Json = await (await pf.getFile()).text(); // x=100 세대(유효)
// (a) HEAD를 파손시키면 PREV(세대 1: x=100)로 후퇴 부활해야 한다(첫 부팅 위장 금지).
let hw = await hf.createWritable(); await hw.write("{ 이건 JSON이 아니다"); await hw.close();
const c = await bootSession({});
const rec2 = await c.rt.enableJournal({ dir, reactive: c.reactive }).recover();
check("HEAD 파손 -> PREV 세대로 후퇴 부활(마지막 커밋만 유실)", !!rec2 && rec2.fallback === true && c.rt.run("x") === 100,
rec2 ? `fallback, x=${c.rt.run("x")}` : "부활 실패");
// (b) 경계 지문 가드: 유효하지만 h0만 틀린 HEAD는 후퇴 없이 즉시 예외(환경 불일치).
const headJson = JSON.parse(gen1Json); headJson.h0 = "deadbeef";
hw = await hf.createWritable(); await hw.write(JSON.stringify(headJson)); await hw.close();
const bad = await c.rt.enableJournal({ dir, reactive: c.reactive }).recover().then(() => null, (e) => String(e));
check("경계 지문(h0) 불일치 -> 명시적 예외(조용한 오염 방지)", !!bad && bad.includes("h0"), String(bad).slice(0, 80));
// (c) 손상과 첫 부팅의 구분: HEAD/PREV 둘 다 파손이면 첫 부팅(null)이 아니라 명시적 예외.
hw = await hf.createWritable(); await hw.write("깨진 HEAD"); await hw.close();
let pw = await pf.createWritable(); await pw.write("깨진 PREV"); await pw.close();
const corrupt = await c.rt.enableJournal({ dir, reactive: c.reactive }).recover().then(() => null, (e) => String(e));
check("HEAD+PREV 파손 -> 첫 부팅 위장 없이 예외", !!corrupt && corrupt.includes("파손"), String(corrupt).slice(0, 80));
await opfs.removeEntry("journalProbe", { recursive: true });
await opfs.removeEntry("journalProbeEmpty", { recursive: true });
} catch (e) {
check("예외 없음", false, String(e).slice(0, 300));
}
await report();
</script>
</body>
</html>