Skip to content

Commit c10c15b

Browse files
committed
랜딩 히어로를 라이브 데모로: 그림 대신 진짜 CPython이 그 자리에서 돈다
히어로의 코드 블록은 지금까지 그림이었다. 이 프로젝트의 유일하게 강한 증거는 "진짜로 돈다"인데, 그걸 보려면 데모 페이지로 넘어가야 했다. 이제 히어로에서 바로 돌린다: 버튼을 누르면 이 탭에서 CPython이 부팅되고, 100만 원소 상태를 체크포인트하고, 코드가 그것을 파괴하고, 힙을 시간여행으로 되돌린다. 부팅 ms, 복원 ms, len(data) 값은 하나도 하드코딩이 아니라 그 자리에서 측정한다. 엔진은 누르기 전에 내려받지 않는다(동적 import). 랜딩 첫 로드는 파이썬 없이 가볍고, 증거를 보겠다고 누른 사람만 수 MB를 받는다. 데모 페이지로의 이동도, 두 번째 부팅도 없다. 랜딩이 실행 표면이 됐으므로 예제 게이트에 편입한다("/"로 연다. 랜딩의 상대 경로는 배포 루트 기준이라 examples/index.html 경로로 열면 자산이 어긋난다). 게이트 7/7 GREEN, 복원 13.0ms 실측.
1 parent 64290e2 commit c10c15b

3 files changed

Lines changed: 94 additions & 10 deletions

File tree

examples/demo.css

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,11 @@ sns-links{display:contents}
8080
.btn{display:inline-block;background:linear-gradient(111deg,var(--ctaFrom),var(--ctaTo));color:#fff;border-radius:8px;padding:.6rem 1.1rem;font-weight:700;text-decoration:none}
8181
.ghostBtn{background:transparent;color:var(--text);border:1px solid var(--border);font-weight:400}
8282
.chip{background:var(--deep);border:1px dashed #14453f;color:var(--teal);border-radius:8px;padding:.55rem .9rem;cursor:pointer;user-select:none}
83-
.heroTerm{max-width:34rem;margin:1.8rem auto 0;text-align:left;box-shadow:0 0 0 1px var(--border),0 18px 55px rgba(42,107,242,.18)}
83+
/* 히어로 데모 패널: 가운데 놓되 코드와 출력은 왼쪽 정렬을 지킨다(코드는 가운데 정렬하면 안 읽힌다). */
84+
.heroDemo{max-width:41rem;margin:1.8rem auto 0;text-align:left;box-shadow:0 18px 55px rgba(42,107,242,.14)}
85+
.heroDemo .term{min-height:0}
86+
.heroRun{margin:.8rem 0 0;gap:.8rem}
87+
.heroRun .status{flex:1;min-width:14rem;font-size:.86rem;color:var(--dim)}
8488
h2{font-size:1.15rem;color:var(--dim);letter-spacing:.06em;text-transform:uppercase;margin:2.6rem 0 .2rem;padding-top:.6rem}
8589
.stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(9.5rem,1fr));gap:1rem;margin:2rem 0 1rem;padding:1.1rem 0;border-top:1px solid var(--border);border-bottom:1px solid var(--border)}
8690
.stat b{display:block;color:var(--teal);font-size:1.5rem}

examples/index.html

Lines changed: 83 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,22 @@ <h1>Real Python in your browser tab.<br><span class="accent">No server.</span></
3939
<a class="btn ghostBtn" href="https://github.com/eddmpython/pyproc">GitHub</a>
4040
<code class="chip" id="installChip" title="click to copy">npm install pyproc</code>
4141
</div>
42-
<div class="term heroTerm" aria-hidden="true"><span class="dim"># the machine demo, in 5 lines</span>
43-
<span class="ok">&gt;&gt;&gt;</span> data = train_for_ten_minutes()
44-
<span class="dim"># close the tab. reopen tomorrow.</span>
45-
<span class="ok">&gt;&gt;&gt;</span> data.score() <span class="dim"># still here. nothing re-ran.</span>
46-
<span class="ok">&gt;&gt;&gt;</span> %undo <span class="dim"># time-travel the heap, ~1ms</span></div>
42+
43+
<!-- 히어로 데모는 그림이 아니라 진짜다: 누르면 이 탭에서 CPython이 부팅되고, 아래 숫자(부팅 ms,
44+
복원 ms, 값)는 전부 그 자리에서 측정된다. 엔진은 누르기 전에 내려받지 않는다(동적 import):
45+
랜딩의 첫 로드는 파이썬 없이 가볍고, 증거가 필요한 사람만 비용을 낸다. -->
46+
<div class="panel heroDemo">
47+
<div class="term" id="heroTerm" aria-live="polite"><span class="dim"># checkpoint, wreck it, time-travel back. live, in this tab.</span>
48+
<span class="ok">&gt;&gt;&gt;</span> data = list(range(1_000_000)) <span class="dim"># the prepared state</span>
49+
<span class="ok">&gt;&gt;&gt;</span> cp = checkpoint() <span class="dim"># a point to return to</span>
50+
<span class="ok">&gt;&gt;&gt;</span> data.clear() <span class="dim"># an agent wrecks it</span>
51+
<span class="ok">&gt;&gt;&gt;</span> restore(cp) <span class="dim"># changed pages only</span>
52+
<span class="ok">&gt;&gt;&gt;</span> len(data) <span class="dim"># whole again, no re-run</span></div>
53+
<div class="row heroRun">
54+
<button id="runHero">Run this in your browser</button>
55+
<span class="status" id="heroStatus">Real CPython, booted here. Nothing downloads until you press it.</span>
56+
</div>
57+
</div>
4758
</section>
4859

4960
<section class="stats" aria-label="measured results">
@@ -112,11 +123,76 @@ <h2 id="install">Install</h2>
112123
<span><a href="https://github.com/eddmpython/pyproc">GitHub</a> · <a href="https://www.npmjs.com/package/pyproc">npm</a> · <a href="https://buymeacoffee.com/eddmpython">support</a></span>
113124
</footer>
114125
</div>
115-
<script>
116-
// 설치 칩 클릭 = 클립보드 복사(랜딩의 유일한 JS. 실패해도 무해).
126+
<script type="module">
127+
// 설치 칩 클릭 = 클립보드 복사(실패해도 무해).
117128
document.getElementById("installChip").addEventListener("click", async (e) => {
118129
try { await navigator.clipboard.writeText("npm install pyproc"); e.target.textContent = "copied!"; setTimeout(() => { e.target.textContent = "npm install pyproc"; }, 1200); } catch (err) {}
119130
});
131+
132+
// 히어로 라이브 데모. index.js는 배포 루트에 있다(pages.yml이 올리고, 로컬 serve.mjs는 "/"를
133+
// 이 페이지로 매핑한다). import를 클릭 시점으로 미루는 게 핵심이다: 엔진(수 MB)은 증거를
134+
// 보겠다고 누른 사람만 받는다. 숫자는 하나도 하드코딩하지 않는다. 전부 지금 여기서 잰다.
135+
const term = document.getElementById("heroTerm");
136+
const status = document.getElementById("heroStatus");
137+
const runBtn = document.getElementById("runHero");
138+
const gateMode = new URLSearchParams(location.search).has("gate");
139+
const print = (html) => { term.insertAdjacentHTML("beforeend", "\n" + html); };
140+
const gateReport = (ok) => {
141+
if (!gateMode) return;
142+
fetch("/gateReport", {
143+
method: "POST", headers: { "Content-Type": "application/json" },
144+
body: JSON.stringify({ ok: !!ok, checks: [{ name: document.title, pass: !!ok, info: term.textContent.slice(-200) }] }),
145+
}).catch(() => {});
146+
};
147+
148+
async function runHeroDemo() {
149+
runBtn.disabled = true;
150+
status.textContent = "Downloading CPython (WebAssembly)...";
151+
try {
152+
const t0 = performance.now();
153+
const { boot } = await import("./index.js");
154+
const rt = await boot();
155+
const bootMs = Math.round(performance.now() - t0);
156+
const version = rt.run("import sys; sys.version.split()[0]");
157+
status.innerHTML = `CPython <b class="ok">${version}</b> booted in this tab in <b class="ok">${bootMs}ms</b>`;
158+
print("");
159+
print(`<span class="dim"># live now, in this tab</span>`);
160+
161+
const reactive = rt.enableReactive();
162+
rt.run("data = list(range(1_000_000))");
163+
const sp = reactive.stackSave();
164+
const cp = reactive.checkpoint();
165+
const prepared = rt.run("len(data)");
166+
print(`<span class="ok">&gt;&gt;&gt;</span> len(data) <span class="ok">${prepared.toLocaleString()}</span> <span class="dim"># prepared, checkpoint saved</span>`);
167+
168+
rt.run("data.clear()");
169+
const wrecked = rt.run("len(data)");
170+
print(`<span class="ok">&gt;&gt;&gt;</span> len(data) <span class="err">${wrecked}</span> <span class="dim"># the agent wrecked it</span>`);
171+
172+
reactive.checkpoint(); // 실행 경계를 닫는다(복원을 건전하게 만드는 계약)
173+
const tr = performance.now();
174+
reactive.restoreLive(cp.index, sp);
175+
const restoreMs = performance.now() - tr;
176+
const restored = rt.run("len(data)");
177+
print(`<span class="ok">&gt;&gt;&gt;</span> len(data) <span class="ok">${restored.toLocaleString()}</span> <span class="dim"># restored in ${restoreMs.toFixed(1)}ms, no re-run</span>`);
178+
179+
const ok = prepared === 1000000 && wrecked === 0 && restored === 1000000;
180+
status.innerHTML += ok
181+
? ` · heap time-travel in <b class="ok">${restoreMs.toFixed(1)}ms</b>`
182+
: ` · <span class="err">unexpected result</span>`;
183+
runBtn.textContent = "Run again";
184+
runBtn.disabled = false;
185+
runBtn.onclick = () => location.reload();
186+
gateReport(ok);
187+
} catch (e) {
188+
status.innerHTML = `<span class="err">Failed: ${e}</span> (needs Chromium/Edge)`;
189+
runBtn.disabled = false;
190+
gateReport(false);
191+
}
192+
}
193+
194+
runBtn.addEventListener("click", runHeroDemo, { once: true });
195+
if (gateMode) runHeroDemo(); // 예제 게이트가 랜딩도 사람처럼 연다(?gate=1)
120196
</script>
121197
</body>
122198
</html>

tests/browser/examples.mjs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@ const TIMEOUT_MS = Number(process.env.PYPROC_GATE_TIMEOUT || 240000);
1414
// brandGate: 예제가 쓰는 브랜드 자산(마크 SVG + demo.css 팔레트)이 실제로 그려지는지 먼저 본다.
1515
// 이 층의 실패는 조용하다(파싱 실패 = 이미지가 사라지고, 색만 초기값으로 돌아간다). 파이썬을
1616
// 안 띄우므로 몇 초면 끝난다: 예제 5쪽을 다 돌리기 전에 진열장이 깨졌는지부터 알려준다.
17-
const PAGES = ["tests/browser/brandGate.html", "examples/basic.html", "examples/agentSandbox.html", "examples/terminal.html", "examples/machine.html", "examples/processOs.html"];
17+
// 빈 문자열 = 랜딩("/"). 랜딩 히어로가 진짜로 CPython을 부팅해 체크포인트/복원을 돌리므로 예제와
18+
// 같은 급의 실행 표면이다. 랜딩은 배포 루트 기준 상대 경로를 쓰니 반드시 "/"로 열어야 한다
19+
// (examples/index.html 경로로 열면 assets/와 index.js가 어긋난다. serve.mjs가 "/"를 랜딩에 매핑한다).
20+
const PAGES = ["tests/browser/brandGate.html", "", "examples/basic.html", "examples/agentSandbox.html", "examples/terminal.html", "examples/machine.html", "examples/processOs.html"];
21+
const label = (page) => page || "/ (랜딩 히어로 라이브 데모)";
1822

1923
const browser = findBrowser();
2024
let resolveReport = null;
@@ -44,7 +48,7 @@ for (const page of PAGES) {
4448
try { rmSync(profile, { recursive: true, force: true }); } catch (e) {}
4549
const info = ((result.checks && result.checks[0] && result.checks[0].info) || "").replaceAll("\n", " | ").slice(-150);
4650
if (result.ok !== true) failed++;
47-
console.log(` ${result.ok === true ? "PASS" : "FAIL"} ${page}${result.timedOut ? " (타임아웃)" : ""}${info ? "\n " + info : ""}`);
51+
console.log(` ${result.ok === true ? "PASS" : "FAIL"} ${label(page)}${result.timedOut ? " (타임아웃)" : ""}${info ? "\n " + info : ""}`);
4852
}
4953
server.close();
5054
console.log(`\n결과: ${PAGES.length - failed}/${PAGES.length} ${failed ? "RED" : "GREEN"}`);

0 commit comments

Comments
 (0)