Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ jobs:
# The workflow file itself is a build input (it carries the configure flags
# for web/build64 below), so hash it too — a change to the build steps must
# miss a stale cache rather than skip the rebuild and reuse old archives.
key: wasm-${{ runner.os }}-${{ hashFiles('.github/workflows/pages.yml', 'CMakeLists.txt', 'web/CMakeLists.txt', 'web/stage_playground_imgui_samples.cmake', 'src/**', 'include/**', 'modules/**', 'daslib/**', 'utils/daspkg/**', 'examples/games/**', 'examples/graphics/**', 'examples/pathTracer/**', 'web/examples/ui/samples/**', 'tutorials/**', 'dastest/**') }}
key: wasm-${{ runner.os }}-${{ hashFiles('.github/workflows/pages.yml', 'CMakeLists.txt', 'web/CMakeLists.txt', 'web/stage_playground_imgui_samples.cmake', 'src/**', 'include/**', 'modules/**', 'daslib/**', 'utils/daslang/**', 'utils/daspkg/**', 'examples/games/**', 'examples/graphics/**', 'examples/pathTracer/**', 'web/examples/ui/samples/**', 'tutorials/**', 'dastest/**') }}

# Host daslang — one games-capable build (dasLLVM for cross-compile + dasGlfw
# + dasOpenGL shared modules for the games) that serves BOTH das2rst and the
Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/playground-e2e.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
name: Playground e2e (no WASM)

# Fast PR gate: builds the site without WASM (skips the 5-10 min Emscripten
# step) and runs the Playwright suite minus any specs tagged `@wasm`. The full
# WASM suite is reserved for the master-only workflow (forthcoming).
# step) and runs the Playwright suite minus any specs tagged `@wasm`. The
# tagged specs run nightly against the deployed site (nightly_playground.yml,
# job wasm_specs).

on:
pull_request:
Expand Down
3 changes: 3 additions & 0 deletions include/daScript/misc/job_que.h
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ namespace das {
void Clear(uint32_t count = 1);
int addRef( LineInfo * at = nullptr );
int releaseRef( LineInfo * at = nullptr );
// live refs; <=1 (the joiner's own) means no dispatched job/thread can
// ever notify — waitForJob's deadlock-vs-long-job discriminator
int refCount() const { return mRef.load(); }
int size() const;
int append(int size);
bool isValid() const { return mMagic==uint32_t(STATUS_MAGIC); }
Expand Down
63 changes: 63 additions & 0 deletions plans/playground_wedge_followups.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# playground wedge handling - follow-ups and standing caveats

The wedge arc (deferred `Module::Shutdown` under a live browser loop + the refcount-gated
main-thread join in `waitForJob`) fixed the nightly's interpreter wedges. What it
deliberately did NOT settle is ledgered here.

## The bounded join (`waitForJob`, module_builtin_jobque.cpp)

Active only under `__EMSCRIPTEN__ && __EMSCRIPTEN_PTHREADS__` AND on the browser main
thread: the threaded playground interpreter and the daspkg `release wasm` builds. Native,
single-threaded wasm, worker-thread joins, `waitForJobWithTimeout`, and every
Channel/LockBox/Stream wait are untouched.

- **The throw fires only when nothing can ever notify**: a stall window with
`refCount() <= 1` - every dispatched job/thread holds a ref via the capture macros, so
the only throwable state is appended-but-never-dispatched (the wedge class). A long or
starved job holds a ref and the join waits forever, as on native; after the first stall
window it logs one stderr line (mirrored to the devtools console by the run frame).
This gate is what makes the throw safe: with no holders there is no `with_*` scope guard
left to terminate through and no later writer into the joined status's stack frame.
- **Remaining stall subclass, by design**: a join held by genuinely stuck work (a job
parked on a channel the joiner was supposed to fill) still freezes the tab - visible in
the console via the stall line, not recoverable in-page. The structural fix is the
`PROXY_TO_PTHREAD` arc below.
- **Channel/Stream blocking pops on the main thread are a sibling wedge class** -
`for_each_clone` over a channel nothing fills parks the tab with no bound at all. Not
covered by this arc.

## The deferred Module::Shutdown and loop teardown (utils/daslang/main.cpp)

Emscripten `daslang` binary only, browser-loop path only.

- **Teardown order at the loop's natural end**: script `shutdown()` (exceptions now
printed, not swallowed) -> bounded global-que drain (3s) -> Context delete -> deferred
`Module::Shutdown`. On a drain timeout the loop's Context AND the modules are
deliberately leaked with a log line - freeing memory under running jobs is heap
corruption, and `~Module_JobQue`/`~JobQue` join workers unbounded. One program per frame
makes the leak inert in the playground; a long-lived multi-run embedder would accumulate.
- **Unverified seam: a second `callMain` in one wasm instance while the first run's loop is
live.** The superseded path leaves modules alive on purpose, so run 2 reaches
`Module::Initialize` on already-initialized modules and `g_envTotal` drifts up by one
(suppressed leak dumps, atexit audit trip on exit). No current embedding can do it - the
run frame refuses second runs, `_interp.html` and the node test call `callMain` once.
- **Thread affinity**: the deferred `Module::Shutdown` runs on the thread that services the
emscripten main loop, which today is the thread that ran `Module::Initialize`. The
`PROXY_TO_PTHREAD` arc moves daslang main onto a pthread - the tick's shutdown must move
with it or `daScriptEnvironment`'s thread-local bound env is null there.

## The structural gap: user code can still freeze the page

`while true {}` in pasted code wedges the tab: the interpreter runs on the run frame's main
thread, same-origin frames share the tab's thread, and a frozen thread runs no parent-side
watchdog. The refcount gate narrows the join case to stuck-work-only; compute loops and
blocking pops remain.

Structural fix: run daslang main on a pthread (`-sPROXY_TO_PTHREAD`, plus offscreen
canvas/framebuffer for the GL path). The page thread stays live, the parent gets a real
kill/restart switch (destroying the frame already terminates its workers), and every
blocking join becomes legal on what is then a real thread. Own arc: GLFW event proxying,
AudioWorklet interplay, and the pthread-pool budget all need the build-and-browser loop.
The debug rig from the wedge arc applies (threaded `web/build_mt` staged into
`site/playground`, COOP/COEP server over `site/`, Chromium trace + name-section
symbolication - mind the bare `-s` in CMakeCommon's Release flags stripping wasm names).
6 changes: 6 additions & 0 deletions site/LAWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,9 @@ compacted, or cited as rules.
(one run's cmd covering several per-clip rows), Boris ruled the rendered cmd identifies
the RUN, not the row - "2. agree" to amending the first rule rather than storing per-clip
argvs.

- **2026-08-31** (`REVIEW.md`): review-round flashlight items 2 and 4. Boris ruled ("yes")
the stated-suite-run rule is tip-pinned per PR with a restatement duty on later edits;
and ("yes") `site/README.md` is blessed as `site/`'s architecture doc (the skill takes a
carve-out rather than a doc split). The artifact-list and "page"-definition rewrites rode
along as auditor-identified defects.
13 changes: 8 additions & 5 deletions site/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ site/
| +-- samples/ # multi-file sample bundles (gitignored, mirrored from web/examples/ui/samples)
| +-- *.{js,css} # other vendored bits from web/examples/ui/src/ for local-dev (gitignored)
+-- tests/
| +-- playground/ # Playwright e2e suite (28 specs, ~5 s no-WASM)
| +-- playground/ # Playwright e2e suite (no-WASM lane per PR, @wasm nightly)
+-- doc/ # Sphinx HTML output (gitignored, deployed by CI)
```

Expand Down Expand Up @@ -318,8 +318,10 @@ The landing chart and `benchmarks.html` read them directly - no rebuild needed.
Specs cover: dropdowns, tab strip CRUD, multi-file persistence, share-URL
round-trip, splitter drag, hero up playground handoff, engine toggle, the
shared runtime module, and dead-page revival. Tests that need the daslang
runtime carry `@wasm` in their title and only run when the WASM artifacts
are staged; the per-PR lane runs the rest with `--grep-invert '@wasm'`.
runtime carry `@wasm` in their title; the per-PR lane runs the rest with
`--grep-invert '@wasm'`, and `nightly_playground.yml`'s `wasm_specs` job runs
the tagged ones against the DEPLOYED site — an `@wasm` assertion that depends
on an undeployed runtime change stays red until the artifact ships.

```bash
# Start the dev server from site/ (so paths resolve like prod).
Expand All @@ -335,8 +337,9 @@ npx playwright test # full suite, requires WASM at site/play

CI runs the no-WASM subset on every PR via
[`.github/workflows/playground-e2e.yml`](../.github/workflows/playground-e2e.yml).
The `@wasm`-tagged specs are gated on the WASM build being present locally -
no dedicated CI tier yet.
The `@wasm`-tagged specs run nightly against the deployed site
([`nightly_playground.yml`](../.github/workflows/nightly_playground.yml),
`wasm_specs`), and locally against a staged WASM build.

## Common gotchas

Expand Down
18 changes: 10 additions & 8 deletions site/REVIEW.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

**Read `REVIEW_COMMON.md` (repo root) first - its contract binds this checklist.** Architecture doc:
`README.md`. **A Playwright spec (`*.spec.js`), wherever the diff puts it, answers to the
`tests/playground/` checklist.** A page is an `.html` or `.md` file under this folder, together
with what the scripts it loads render into it.
`tests/playground/` checklist.** A page is an `.html` or `.md` file under this folder that a
visitor navigates to, together with what the scripts it loads render into it - not a
machine-only harness document (`playground/run-frame.html`), and not editor content.

**Never show on a page a hand-written shell command, flag, or output line invented for
illustration - show only a command the run actually executed.** A rendered `cmd` identifies
Expand Down Expand Up @@ -58,17 +59,18 @@ a defect - link every embedded sample.**

**A daslang sample embedded in a page not written in gen2 is a defect.**

**A diff that changes `playground/` (this folder) or `examples/_interp.html` (this folder)
states a run of the WASM-staged Playwright suite (`tests/playground/`, this folder) in its PR
body or commit message, naming the passes and any failures, in the same change.** The no-WASM
lane cannot see a broken runtime path, and every sample on the page runs through that path.
**A PR whose diff changes `playground/` (this folder) or `examples/_interp.html` (this folder)
states, in the PR body, a run of the WASM-staged Playwright suite (`tests/playground/`, this
folder) against the branch tip, naming the passes and any failures; a later edit to those
files restates the run.** The no-WASM lane cannot see a broken runtime path, and a run
recorded mid-branch describes a tree that no longer ships.

**A stated Playwright run names every sample the diff changed: for each, the spec that loaded
it, or - when no spec loads it - that it was opened and run by hand in the playground.**

**A stated Playwright run names the runtime artifacts it used: built from this change when
the diff touches `src/`, `include/`, `daslib/`, `modules/`, `dastest/`, or
`web/CMakeLists.txt` (all repo root); the deployed ones otherwise.**
the diff touches any source compiled into the WASM runtime (`daslang_static` - its `main()`
lives in `utils/daslang/`) or the web build; the deployed ones otherwise.**

**A diff that puts a measurement number - a rate, a duration, a size, a score some run
produced, never a fixture or run parameter - on a page without rendering it from live data,
Expand Down
12 changes: 12 additions & 0 deletions site/playground/forge-skin.css
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,18 @@ body {
}
.button_header:hover { background: #f4b04a !important; }
.button_header:disabled { opacity: 0.5; cursor: default; }
/* Run-button progress/busy states (paintRunButton in main.js; base rules in
main.css). Loading shows a dim base so the amber fill reads as progress;
busy keeps the amber base under the sliding stripe. */
#run.pg-loading {
background: rgba(232, 161, 58, 0.18) !important;
color: var(--fg-dim) !important;
opacity: 1;
}
#run.pg-loading::before {
background: rgba(232, 161, 58, 0.55);
}
#run.pg-busy { opacity: 1; }
.button_header--ghost {
background: transparent !important;
color: var(--fg-dim) !important;
Expand Down
4 changes: 2 additions & 2 deletions site/playground/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -114,11 +114,11 @@
<script type="text/javascript" src="./playground-init.js"></script>
<!-- Owns run-frame.html (one frame per program). Must precede main.js, whose
pageInit hands it the run host and whose run path delegates to it. -->
<script type="text/javascript" src="./playground-runner.js?v=5"></script>
<script type="text/javascript" src="./playground-runner.js?v=10"></script>
<!-- Compiles the editor's code on the build service for the wasm engine.
Reads its payload from playground-share.js, so both agree on one hash. -->
<script type="text/javascript" src="./playground-wasm.js?v=2"></script>
<script type="text/javascript" src="./main.js?v=8"></script>
<script type="text/javascript" src="./main.js?v=12"></script>
<script type="text/javascript" src="./playground-tabs.js?v=4"></script>
<script type="text/javascript" src="./playground-share.js?v=3"></script>
<script type="text/javascript" src="./playground-splitter.js?v=3"></script>
Expand Down
Loading
Loading