Skip to content

WASM: add a WebAssembly/JupyterLite smoke-test job to CI #933

Open

Description

@mmcky

Note

Updated 2026-08-21. The seed is now on main via #938 (ci/wasm/environment.yml, ci/wasm/smoke_test.py, the Playwright harness ci/wasm/test_jupyterlite.py, and the native gate in ci_wasm_smoke.yml), so the Approach below names it instead of "the config from #928", and a task list for the runner PR has been added. What that list corrects: the parked job (2026-08-19 comment) still needs triggers, the gini expectation in both suite files is inverted now that numba 0.67.0 build 1 serialises parallel=True (patch 0010, emscripten-forge/recipes#6293), the two jitted-generator tests will hang without a real guard, test_simplex_grid fails on a warm cache (#944), and smoke_test.py is not yet shipped into the kernel. The original text is preserved in the edit history.

Part of #925 (Phase 3 — keep it working).

Goal

Browser support must not regress once established: add a wasm job to GitHub Actions that runs a smoke suite in the real browser stack.

Approach

This job is also the natural watchpoint for upstreaming: the blog post signals intent to merge the WASM engine into Numba mainline, and once that ships, the patched-recipe caveats (the recipe now carries ten patches, e.g. the serial fallback for parallel=True added by patch 0010 and the ufunc caching disabled by patch 0007) may start dissolving. Until then the job tests the released conda-forge noarch quantecon, not repo source, so per-PR triggering is not useful long-term.

Tasks for the runner PR

  1. Add the parked wasm job with its own triggers. The snippet in the 2026-08-19 comment is a job block and has no on:; ci_wasm_smoke.yml's on: is push/pull_request to main. Either add schedule + workflow_dispatch to that shared on: block and guard the job with if: github.event_name != 'pull_request', or move it to its own workflow file — in which case needs: native no longer resolves (it only sees jobs in the same workflow) and must be dropped or replaced. Iterate under pull_request first, as the 2026-08-19 comment describes, then flip.
  2. Flip the gini expectation. numba 0.67.0 build 1 on emscripten-forge-4x (patch 0010, Fallback Numba parallel JIT to serial on Emscripten emscripten-forge/recipes#6293, merged 2026-08-18) demotes @njit(parallel=True) to the serial pipeline, and the bare numba pin resolves to it. test_gini_coefficient's xfail(IS_EMSCRIPTEN, strict=True) in smoke_test.py would XPASS and fail; test_gini_fails_on_emscripten in test_jupyterlite.py asserts an error and would fail. Both become plain pass assertions (browser-unverified; inferred from the merged patch). PR Rewrite Gini coefficient in O(n log n) #937's O(n log n) rewrite (WASM: remove @njit(parallel=True) from gini_coefficient via an O(n log n) rewrite #926) is a performance change on top of this, not a prerequisite.
  3. Guard the two jitted-generator tests. test_support_enumeration and test_vertex_enumeration in smoke_test.py call @njit generators (support_enumeration.py:96, vertex_enumeration.py:118) that never return on Emscripten (WASM: support_enumeration and vertex_enumeration hang in the browser — root cause is jitted generators (numba.np.linalg._LAPACK verified working) #927, WASM: support_enumeration and vertex_enumeration hang in the browser — root cause is jitted generators (numba.np.linalg._LAPACK verified working) #927 (comment)). Use pytest.mark.skipif(IS_EMSCRIPTEN, ...) or xfail(IS_EMSCRIPTEN, run=False, strict=True) — a plain xfail still executes the test, and pytest-timeout (SIGALRM / threading.Timer) cannot interrupt a native loop in the single-threaded kernel. In test_jupyterlite.py, skip test_support_enumeration or run it last: Playwright's EXEC_MS (180 s) bounds it host-side, but the module-scoped kernel stays wedged and the following test fails spuriously. Add the minimal bare @njit generator test (for i in range(n): yield i) alongside, guarded the same way, as the isolated upstream repro and regression watch.
  4. Mark test_simplex_grid as expected to fail on a warm cache and cite WASM: cache=True functions that link a cache-restored callee fail with "no compiled object yet" (simplex_grid → num_compositions_jit → comb_jit) #944. Its comment currently says "(WASM: audit 32-bit intp behaviour on wasm32 (overflow guards, simplex_index wrapping, dtypes) #929)"; the actual failure is RuntimeError: no compiled object yet for comb_jit (WASM: cache=True functions that link a cache-restored callee fail with "no compiled object yet" (simplex_grid → num_compositions_jit → comb_jit) #944, reported upstream as Numba: RuntimeError: no compiled object yet emscripten-forge/recipes#6309), which triggers only when a cache=True caller links a callee restored from the persistent cache — a cold single-session run passes. A regression test needs a second session or a pre-seeded /drive/.cache/numba.
  5. Ship smoke_test.py into the kernel and run it in-kernel. The parked job runs only the Playwright harness (6 cells: kernel boot, import, tauchen, np.linalg.solve, support_enumeration, gini), so a green run fills at most 5 of WASM: JupyterLite proof-of-concept deployment and browser smoke suite #928's 13 rows. WASM: JupyterLite environment config and browser smoke suite #938 assigned in-kernel pytest execution of smoke_test.py (16 tests) here (commit 1e0c598 also dropped pytest from environment.yml, so it has to be added back). Copy the file into the site contents, invoke pytest inside the xeus-python kernel, and surface per-test results.
  6. Bump mamba-org/setup-micromamba@v2 to @v3 in the parked YAML (v3.0.0 moved to Node 24; the inputs used — micromamba-binary-path, init-shell, generate-run-shell — are unchanged). actions/checkout@v7 and actions/setup-python@v7 are current.

Expected outcome of the first browser run once 1–6 are done, on numba 0.67.0 build 1: test_gini_coefficient passes; test_support_enumeration, test_vertex_enumeration and the bare-generator test are skipped/xfail(run=False) pending the upstream generator report (#927); test_simplex_grid passes cold and fails warm (#944); everything else is expected to pass. Record the per-function table in #928.

Acceptance criteria

Related

Activity

  1. mmcky commented on Aug 19, 2026

    @mmcky
    ContributorAuthor

    Parking the Emscripten/JupyterLite runner job removed from PR #938 in the split (see the review there for context). Below is that job with the review fixes already folded in, ready to seed this issue's PR — it drops --with-deps (the browser install hung at apt-get for the full 6-hour job limit on the one run), adds pytest-timeout (the --timeout flag was previously passed without its plugin), makes the native gate real via needs: native, drops the _site cache (unpinned quantecon made the key meaningless and the build re-ran regardless), adds a server-readiness check before Playwright starts, bumps setup-python to v7, and puts upper bounds on the unpinned build tools (the one run resolved jupyterlite-xeus to 5.0.0, which the --XeusAddon.environment_file flag has never been exercised against).

    Development approach: iterate in the PR under a pull_request trigger so each attempt shows red/green, then flip to schedule + workflow_dispatch just before merge — workflow_dispatch only works once the workflow file exists on the default branch, and after merge gh workflow run --ref <branch> can still exercise branch versions. Note the job tests the released conda-forge quantecon (not repo source) until the package lands on emscripten-forge-4x, so per-PR triggering isn't useful long-term.

    The Playwright harness it runs (ci/wasm/test_jupyterlite.py, reworked to sentinel-driven execution in PR #938) and the kernel env (ci/wasm/environment.yml) are already on main once #938 merges. The job to append to .github/workflows/ci_wasm_smoke.yml:

      # ------------------------------------------------------------------
      # Emscripten / JupyterLite.
      # Builds a real JupyterLite site from emscripten-forge packages
      # (xeus-python WASM kernel + numba + quantecon), then drives it
      # headlessly with Playwright to verify the stack end-to-end.
      # ------------------------------------------------------------------
      wasm:
        name: Smoke suite (Emscripten / JupyterLite)
        runs-on: ubuntu-latest
        timeout-minutes: 45
        needs: native
        steps:
        - uses: actions/checkout@v7
    
        - uses: actions/setup-python@v7
          with:
            python-version: "3.11"
    
        - name: Install micromamba
          uses: mamba-org/setup-micromamba@v2
          with:
            micromamba-binary-path: /usr/local/bin/micromamba
            init-shell: none
            generate-run-shell: false
    
        - name: Install JupyterLite build tools
          run: |
            pip install \
              "jupyterlite-core>=0.4,<0.9" \
              "jupyterlite-xeus>=4.5,<6" \
              pytest \
              pytest-timeout \
              pytest-playwright
    
        - name: Install Playwright browser
          run: python -m playwright install chromium
    
        - name: Build JupyterLite site
          run: |
            jupyter lite build \
              --XeusAddon.environment_file=ci/wasm/environment.yml \
              --output-dir=_site
    
        - name: Start local server
          run: |
            python -m http.server 8000 --directory _site &
            until curl -sf http://localhost:8000/ >/dev/null; do sleep 1; done
    
        - name: Run Playwright smoke tests (Emscripten kernel)
          run: |
            pytest ci/wasm/test_jupyterlite.py \
              --browser chromium \
              --timeout=900 \
              -v

    Remaining known unknowns for the first iterations: the JupyterLab selectors in test_jupyterlite.py (launcher card, console prompt, output area) have never executed against a real JupyterLite build, and neither has the jupyterlite-xeus 5.x build flag — expect a few rounds. Once the job is green, record the per-function results table in #928.

  2. mmcky commented on Aug 21, 2026

    @mmcky
    ContributorAuthor

    Notes for the runner PR, following #938 and the finding in #927 (#927 (comment)):

    • test_support_enumeration in ci/wasm/smoke_test.py will hang in the browser (jitted-generator bug, not LAPACK). Guard it so a hang can't eat the job's time limit — either a per-test timeout via pytest-timeout (add it to the kernel env) or xfail(IS_EMSCRIPTEN, strict=True) like test_gini_coefficient.
    • Add a minimal @njit generator test (for i in range(n): yield i) alongside it, so the mechanism is isolated from support_enumeration the way the bare np.linalg.solve test isolates LAPACK. That becomes the upstream repro and the regression watch.
    • The bare np.linalg.solve test should pass on Emscripten (verified on the numba-ecosystem demo), so it can stay unguarded.

    The parked runner job above already has the review fixes (no --with-deps, timeout-minutes, needs: native, schedule/dispatch triggers).

  3. mmcky commented on Aug 21, 2026

    @mmcky
    ContributorAuthor

    Corrections to my 2026-08-21 note above (#933 (comment)); the details are now in the body's task list.

    The unguarded bare np.linalg.solve test and the minimal @njit generator isolation test both stand.

  4. mmcky commented on Sep 9, 2026

    @mmcky
    ContributorAuthor

    Two tasks to add to the runner PR, both found while re-checking #925 against live state on 2026-09-09.

    1. The gini xfail will hard-fail the first browser run. ci/wasm/smoke_test.py marks test_gini_coefficient as xfail(IS_EMSCRIPTEN, strict=True) with the reason "@njit(parallel=True) not supported on Emscripten". Two independent changes have falsified that reason: patch 0010 has been in the emscripten-forge recipe since 2026-08-18 and demotes parallel=True to the serial pipeline rather than failing, and PR #937 (merged 2026-09-09) removed parallel=True from gini_coefficient altogether. Under strict=True an unexpected pass is reported as a failure, so this test is now guaranteed to go red the moment the runner executes — for being correct. The existing task list mentions "gini expectations inverted"; this is the specific form it takes. Drop the marker and assert the value plus a timing.

    2. coverage is not packaged for WASM. #925 has been claiming that "the test tooling (pytest, pandas, coverage) is itself packaged for WASM". pytest and pandas are emscripten-forge recipes; coverage is not among the 639, and conda-forge's coverage is not noarch because it builds a C tracer per platform. The browser job should either plan for the pure-Python tracer or accept that it reports no coverage. #925 is corrected.

    While here: the suite's test_import_time is the instrument for #930's unrecorded gate. For a native reference point, import quantecon on 0c7c816 measures 1.85 s cold against 0.65 s with the Numba on-disk cache warm, so roughly 1.2 s is eager compilation — four gufuncs plus the eagerly-signed comb_jit. In the browser the four gufuncs pay that every session, since patch 0007 force-disables cache=True for vectorize/guvectorize. Native seconds are not browser seconds, so treat it as a lower bound rather than the gate.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    CIinfrastructureCI, build, packaging, and repo tooling

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions