Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
4576bf1
test/wasip1: verify thread-local compiler root ABI
cpunion Sep 24, 2026
23fb43d
runtime/wasip1: prepare thread-local compiler roots for GC
cpunion Sep 24, 2026
97febf1
runtime/wasip1: add opt-in stop-the-world GC for WAMR pthreads
cpunion Sep 24, 2026
63e003d
style/wasm: format threaded GC build constraints
cpunion Sep 24, 2026
f1047a3
docs/wasm: name remaining work by scope
cpunion Sep 24, 2026
d20367c
test/wasip1: preserve WAMR panic recovery across pthreads
cpunion Sep 24, 2026
506ab10
docs/wasm: use scope names in acceptance tools
cpunion Sep 24, 2026
9476d8f
runtime/wasip1: grow threaded GC across disjoint libc arenas
cpunion Sep 24, 2026
ce94af7
build/wasip1: default to GC when pthread mode is selected
cpunion Sep 24, 2026
788e8cd
build/wasip1: run pthread programs through WAMR
cpunion Sep 24, 2026
912d13a
test/wasip1: run W32 acceptance slice on WAMR threads
cpunion Sep 24, 2026
d3e41c3
test/wasip1: qualify minimal pthread programs on WAMR
cpunion Sep 24, 2026
a34d192
build/wasip1: grant WAMR filesystem test directories
cpunion Sep 24, 2026
e5e530d
test/wasip1: qualify select stress with 128 WAMR threads
cpunion Sep 24, 2026
7947c58
test/wasip1: accept archive-free WAMR importer error
cpunion Sep 24, 2026
9e3c98e
test/wasip1: stabilize WAMR working directory and stress environment
cpunion Sep 24, 2026
ee583e1
runtime/wasip1: poll hosted condition waits for threaded GC
cpunion Sep 24, 2026
eeb9d19
test/wasm: allow bounded worker suite 180 seconds
cpunion Sep 24, 2026
9c39d03
wasip1: initialize pthread startup and bound worker stacks
cpunion Sep 24, 2026
017aa75
test/wasm: allow memory64 browser worker startup time
cpunion Sep 24, 2026
cabbb32
runtime/wasm: address threaded worker review and W32 profiling
cpunion Sep 24, 2026
38fd060
test/os/exec: accept WAMR devnull failure on WASI
cpunion Sep 24, 2026
2ebd83a
test/wasm: bound slow WAMR TLS acceptance separately
cpunion Sep 24, 2026
9700663
test/wasm: use portable TLS and go/build fixtures
cpunion Sep 24, 2026
2cbad2f
test/wasm: account for WAMR host capabilities and crypto cost
cpunion Sep 24, 2026
18ffee8
runtime/wasm: root TLS slots used by sync.Pool
cpunion Sep 24, 2026
9115621
test/wasm: allow full EC32 public test package to finish
cpunion Sep 25, 2026
0e27af4
runtime/wasm: release retired single-worker roots eagerly
cpunion Sep 25, 2026
57e490b
test/wasm: bound DSA and RSA acceptance cases
cpunion Sep 25, 2026
3a3ab3f
test/wasm: budget measured WAMR package deadlines
cpunion Sep 25, 2026
010d032
runtime/wasm: clear retired browser fiber storage before release
cpunion Sep 25, 2026
8ef2392
runtime/wasm: keep initial WASI thread alive after Goexit
cpunion Sep 25, 2026
bcf6235
runtime/wasip1: exclude timer service from Goexit deadlock count
cpunion Sep 25, 2026
e826cb4
test/wasip1: preserve GOROOT test order after restacking
cpunion Sep 26, 2026
9294e4b
runtime/wasm: avoid defer on system-fiber callback poll
cpunion Sep 27, 2026
0dc7eae
test/wasip1: accept WAMR deadlock exit translation
cpunion Sep 27, 2026
becbc21
test/wasm: stabilize retired fiber finalizer and cover host contracts
cpunion Sep 28, 2026
81a3b13
test/wasm: cover WAMR working-directory failure
cpunion Sep 28, 2026
0ccbe88
fix(wasip1): account for GC metadata at arena size boundaries
cpunion Sep 29, 2026
97523ce
test(wasm): distinguish blocking filesystem calls from host latency
cpunion Sep 29, 2026
e702f81
ci: use Ubuntu archive mirrors and bound apt downloads
cpunion Sep 29, 2026
c6c3ce4
runtime/wasip1: keep parked main thread GC-responsive
cpunion Sep 30, 2026
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
23 changes: 17 additions & 6 deletions .github/actions/setup-deps/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -819,21 +819,32 @@ runs:
shell: bash
run: |
set -euo pipefail
python3 "$GITHUB_ACTION_PATH/ubuntu_apt_test.py"
sudo python3 "$GITHUB_ACTION_PATH/ubuntu_apt.py" /etc/apt "$(dpkg --print-architecture)"
apt_get() {
local limit=10m
if [[ "$1" == "update" ]]; then
limit=5m
fi
# Per-download timeouts alone do not bound an entire mirror-list
# transaction. Fail with diagnostics before exhausting the job.
sudo timeout --kill-after=30s "$limit" apt-get "$@"
}
# Install LLVM if requested
if [[ "${{ inputs.install-llvm }}" == "true" ]]; then
echo "deb http://apt.llvm.org/$(lsb_release -cs)/ llvm-toolchain-$(lsb_release -cs)-${{inputs.llvm-version}} main" | sudo tee /etc/apt/sources.list.d/llvm.list
echo "deb https://apt.llvm.org/$(lsb_release -cs)/ llvm-toolchain-$(lsb_release -cs)-${{inputs.llvm-version}} main" | sudo tee /etc/apt/sources.list.d/llvm.list
# Some Linux runners resolve IPv6 addresses without an IPv6 route.
# Retry transient DNS errors, but keep HTTPS and bounded failures.
wget -4 --retry-on-host-error --tries=3 --timeout=15 --waitretry=2 -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add -
sudo apt-get update
sudo apt-get install -y llvm-${{inputs.llvm-version}}-dev clang-${{inputs.llvm-version}} libclang-${{inputs.llvm-version}}-dev lld-${{inputs.llvm-version}} libunwind-${{inputs.llvm-version}}-dev libc++-${{inputs.llvm-version}}-dev
apt_get update
apt_get install -y llvm-${{inputs.llvm-version}}-dev clang-${{inputs.llvm-version}} libclang-${{inputs.llvm-version}}-dev lld-${{inputs.llvm-version}} libunwind-${{inputs.llvm-version}}-dev libc++-${{inputs.llvm-version}}-dev
echo "PATH=/usr/lib/llvm-${{inputs.llvm-version}}/bin:$PATH" >> $GITHUB_ENV
else
sudo apt-get update
apt_get update
fi

# Install common dependencies
sudo apt-get install -y cmake pkg-config libgc-dev libssl-dev zlib1g-dev libffi-dev libcjson-dev libuv1-dev
apt_get install -y cmake pkg-config libgc-dev libssl-dev zlib1g-dev libffi-dev libcjson-dev libuv1-dev

# Install optional deps for demos.
#
Expand All @@ -843,4 +854,4 @@ runs:
libsqlite3-dev # for github.com/goplus/lib/c/sqlite
python3.12-dev # for github.com/goplus/lib/py
)
sudo apt-get install -y "${opt_deps[@]}"
apt_get install -y "${opt_deps[@]}"
36 changes: 36 additions & 0 deletions .github/actions/setup-deps/ubuntu_apt.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#!/usr/bin/env python3

"""Configure the runner's Ubuntu mirror lists and bounded APT downloads."""

import pathlib
import sys


def configure(apt_dir, architecture):
if architecture in ("amd64", "i386"):
archive = "https://archive.ubuntu.com/ubuntu/"
security = "https://security.ubuntu.com/ubuntu/"
else:
archive = security = "https://ports.ubuntu.com/ubuntu-ports/"

# The runner's mirror+file source selected a malformed https:/ URL and
# then stalled on another mirror for the entire 45-minute Wasm job.
# Preserve source suites, components and signing keys; replace only the
# two Ubuntu mirror lists when the runner already uses them.
for name, url in (("apt-mirrors.txt", archive),
("apt-mirrors-security.txt", security)):
path = apt_dir / name
if path.exists():
path.write_text(url + "\n")

(apt_dir / "apt.conf.d" / "80-llgo-downloads").write_text('''\
Acquire::ForceIPv4 "true";
Acquire::Retries "3";
Acquire::http::Timeout "30";
Acquire::https::Timeout "30";
APT::Update::Error-Mode "any";
''')


if __name__ == "__main__":
configure(pathlib.Path(sys.argv[1]), sys.argv[2])
48 changes: 48 additions & 0 deletions .github/actions/setup-deps/ubuntu_apt_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#!/usr/bin/env python3

import pathlib
import tempfile
import unittest

from ubuntu_apt import configure


class UbuntuAPTTest(unittest.TestCase):
def test_runner_mirror_lists(self):
for arch, archive, security in (
("amd64", "https://archive.ubuntu.com/ubuntu/",
"https://security.ubuntu.com/ubuntu/"),
("arm64", "https://ports.ubuntu.com/ubuntu-ports/",
"https://ports.ubuntu.com/ubuntu-ports/"),
):
with self.subTest(arch=arch), tempfile.TemporaryDirectory() as tmp:
root = pathlib.Path(tmp)
(root / "apt.conf.d").mkdir()
for name in ("apt-mirrors.txt", "apt-mirrors-security.txt"):
(root / name).write_text(
"https:/us.archive.ubuntu.com/ubuntu\n"
"https://mirrors.edge.kernel.org/ubuntu\n")
sources = root / "ubuntu.sources"
source_text = "URIs: mirror+file:/etc/apt/apt-mirrors.txt\nSigned-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg\n"
sources.write_text(source_text)
third_party = root / "custom-mirrors.txt"
third_party.write_text("https://apt.llvm.org/noble/\n")
configure(root, arch)
configure(root, arch)
self.assertEqual((root / "apt-mirrors.txt").read_text(), archive + "\n")
self.assertEqual((root / "apt-mirrors-security.txt").read_text(), security + "\n")
self.assertEqual(sources.read_text(), source_text)
self.assertEqual(third_party.read_text(), "https://apt.llvm.org/noble/\n")

def test_direct_sources_need_no_mirror_lists(self):
with tempfile.TemporaryDirectory() as tmp:
root = pathlib.Path(tmp)
(root / "apt.conf.d").mkdir()
configure(root, "amd64")
self.assertFalse((root / "apt-mirrors.txt").exists())
self.assertFalse((root / "apt-mirrors-security.txt").exists())
self.assertTrue((root / "apt.conf.d" / "80-llgo-downloads").is_file())


if __name__ == "__main__":
unittest.main()
2 changes: 1 addition & 1 deletion .github/workflows/llgo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -571,7 +571,7 @@ jobs:

iwasm --stack-size=819200000 --heap-size=800000000 hello.wasm

- name: Test WASI pthread backend (WAMR)
- name: Test WASI pthread backend and threaded GC (WAMR)
shell: bash
run: python3 dev/test_wasm_wasi_threads.py

Expand Down
23 changes: 22 additions & 1 deletion cmd/internal/run/run_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
package run

import "testing"
import (
"os"
"path/filepath"
"testing"

"github.com/xgo-dev/llgo/internal/mockable"
)

func TestRunAndCmpTestBuildFlagsAreIndependent(t *testing.T) {
if runGoBuildFlags == cmpTestGoBuildFlags {
Expand All @@ -19,3 +25,18 @@ func TestRunAndCmpTestBuildFlagsAreIndependent(t *testing.T) {
t.Fatal("run or cmptest build flags are bound to the wrong command")
}
}

func TestRunPreservesNativeExitCode(t *testing.T) {
program := filepath.Join(t.TempDir(), "exit.go")
if err := os.WriteFile(program, []byte("package main\nimport \"os\"\nfunc main() { os.Exit(23) }\n"), 0600); err != nil {
t.Fatal(err)
}
mockable.EnableMock()
defer mockable.DisableMock()
defer func() {
if recovered := recover(); recovered != "exit" || mockable.ExitCode() != 23 {
t.Errorf("llgo run exit = (%v, %d), want (exit, 23)", recovered, mockable.ExitCode())
}
}()
runCmd(Cmd, []string{program})
}
6 changes: 4 additions & 2 deletions dev/build_iwasm.sh
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,9 @@ mkdir -p wasm-micro-runtime/product-mini/platforms/${PLATFORM}/build
cd wasm-micro-runtime/product-mini/platforms/${PLATFORM}/build

# The test helper executes Wasm bytecode only, so AOT is unnecessary; LLGo's
# generated modules require reference-types support.
# generated modules require reference-types support. WAMR 2.4.5's debug
# interpreter leaves a termination signal after a caught Wasm exception;
# a later branch then aborts LLVM's SjLj path for Go panic/recover.
cmake "${CMAKE_GENERATOR_ARGS[@]}" \
-D WAMR_BUILD_EXCE_HANDLING=1 \
-D WAMR_BUILD_AOT=0 \
Expand All @@ -144,7 +146,7 @@ cmake "${CMAKE_GENERATOR_ARGS[@]}" \
-D WAMR_BUILD_LIB_WASI_THREADS=1 \
-D WAMR_BUILD_LIB_PTHREAD=1 \
-D CMAKE_BUILD_TYPE=Debug \
-D WAMR_BUILD_DEBUG_INTERP=1 \
-D WAMR_BUILD_DEBUG_INTERP=0 \
..

echo "Compiling iwasm..."
Expand Down
9 changes: 6 additions & 3 deletions dev/test_wasm_single_worker.sh
Original file line number Diff line number Diff line change
Expand Up @@ -306,10 +306,11 @@ run_llgo_test() {
# This command builds and runs two packages. CI spent 258 seconds on the
# first WASI package alone, then hit the old 300-second aggregate limit
# while building the second. Budget 300 seconds per package; each binary
# still has its own 30-second test deadline and bounded host runner.
# has a finite 90-second test deadline and bounded host runner. The EC32
# reflection tests exceeded 30 seconds as a complete package on CI.
echo "testing public llgo test command for ${target}"
run_with_timeout_limit 600s env TMPDIR="${temp_dir}" "${llgo_cmd}" test -target "${target}" -emulator \
-v -count=1 -timeout=30s "${test_fixture}" "${secondary_test_fixture}" 2>&1 | tee "${output}"
-v -count=1 -timeout=90s "${test_fixture}" "${secondary_test_fixture}" 2>&1 | tee "${output}"
grep -Fq "PASS" "${output}"
grep -Fq "TestScheduler" "${output}"
grep -Fq "wasm secondary package ok" "${output}"
Expand Down Expand Up @@ -357,7 +358,9 @@ run_llgo_go_profile_test() {
local pattern="${3:-}"
local fixture="${4:-${test_fixture}}"
local output="${work_dir}/${name}.out"
local test_args=(-v -count=1 -timeout=30s)
# The GOOS=js route runs the same reflection-heavy package as EC32.
# Keep its package deadline aligned with the public target route above.
local test_args=(-v -count=1 -timeout=90s)
if [[ -n "${pattern}" ]]; then
test_args+=(-run "${pattern}")
fi
Expand Down
148 changes: 125 additions & 23 deletions dev/test_wasm_wasi_threads.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,39 +14,141 @@
IWASM = os.environ.get("IWASM", "iwasm")


def run_probe(env, directory, name, fixture, tags, marker, timeout,
max_threads=8, expected_exit=0):
module = pathlib.Path(directory) / f"{name}.wasm"
command = [LLGO, "build", "-target", "wasi"]
if tags:
command += ["-tags", tags]
command += ["-o", str(module), str(ROOT / "internal/build/testdata" / fixture)]
subprocess.run(
command,
check=True,
env=env,
timeout=180,
)
result = subprocess.run(
[IWASM, f"--max-threads={max_threads}", "--stack-size=1048576",
"--heap-size=0", "--dir=" + str(ROOT), "--dir=/tmp", str(module)],
capture_output=True,
text=True,
timeout=timeout,
)
print(result.stdout, end="")
print(result.stderr, end="")
allowed_exits = (expected_exit,) if isinstance(expected_exit, int) else expected_exit
if result.returncode not in allowed_exits or (
marker not in result.stdout.splitlines()
and marker not in result.stderr.splitlines()
):
raise SystemExit(f"WAMR {name} probe failed with exit code {result.returncode}")


def run_llgo(env, args, marker, timeout=180):
result = subprocess.run([LLGO, *args], capture_output=True, text=True,
env=env, timeout=timeout)
print(result.stdout, end="")
print(result.stderr, end="")
if result.returncode != 0 or (
marker not in result.stdout.splitlines()
and marker not in result.stderr.splitlines()
):
raise SystemExit(f"LLGo {' '.join(args)} failed with exit code {result.returncode}")


def run_arena_boundaries(env, directory):
module = pathlib.Path(directory) / "gc-arena.wasm"
subprocess.run(
[LLGO, "build", "-target", "wasi", "-o", str(module),
str(ROOT / "internal/build/testdata/wasm-wasi-gc-arena")],
check=True, env=env, timeout=180,
)
# Each size needs a fresh heap: an earlier oversized arena could mask
# the bug by satisfying a later allocation from its unused capacity.
for size in ((32 << 20) - (128 << 10), (32 << 20) - 1,
32 << 20, (32 << 20) + 1, 33 << 20):
result = subprocess.run(
[IWASM, "--max-threads=8", "--stack-size=1048576", "--heap-size=0",
str(module), str(size)], capture_output=True, text=True, timeout=180,
)
print(f"WAMR arena boundary: {size} bytes")
print(result.stdout, end="")
print(result.stderr, end="")
lines = result.stdout.splitlines() + result.stderr.splitlines()
if result.returncode != 0 or "wasi gc arena boundary ok" not in lines:
raise SystemExit(f"WAMR arena boundary {size} failed: {result.returncode}")


def main():
if shutil.which(IWASM) is None:
iwasm = shutil.which(IWASM)
if iwasm is None:
raise SystemExit(f"WAMR runner not found: {IWASM}")

env = os.environ.copy()
env["LLGO_ROOT"] = str(ROOT)
env["LLGO_WASI_THREADS"] = "1"
env["PATH"] = str(pathlib.Path(iwasm).resolve().parent) + os.pathsep + env["PATH"]
with tempfile.TemporaryDirectory(prefix="llgo-wasi-threads-") as directory:
module = pathlib.Path(directory) / "threads.wasm"
run_probe(env, directory, "startup", "wasm-wasi-thread-startup", "nogc",
"wasi thread startup ok", 30, max_threads=32)
# WAMR can translate the terminal Wasm exception to process status 1
# instead of preserving the guest's status 2. Both are nonzero exits.
deadlock_exits = (1, 2)
run_probe(env, directory, "main-goexit", "wasm-wasi-main-goexit", "nogc",
"fatal error: no goroutines (main called runtime.Goexit) - deadlock!",
30, expected_exit=deadlock_exits)
run_probe(env, directory, "main-goexit-gc", "wasm-wasi-main-goexit", "",
"fatal error: no goroutines (main called runtime.Goexit) - deadlock!",
30, expected_exit=deadlock_exits)
run_probe(env, directory, "main-goexit-collect",
"wasm-wasi-main-goexit-gc", "", "wasi goexit gc collected",
30, expected_exit=deadlock_exits)
run_probe(env, directory, "main-goexit-timer", "wasm-wasi-main-goexit-timer",
"nogc", "fatal error: no goroutines (main called runtime.Goexit) - deadlock!",
30, expected_exit=deadlock_exits)
run_probe(env, directory, "main-goexit-timer-gc", "wasm-wasi-main-goexit-timer",
"", "fatal error: no goroutines (main called runtime.Goexit) - deadlock!",
30, expected_exit=deadlock_exits)
run_probe(env, directory, "threads", "wasm-wasi-threads", "nogc",
"wasi threads ok", 30)
run_probe(env, directory, "threaded-gc", "wasm-wasi-threaded-gc",
"", "wasi threaded gc ok", 180)
run_arena_boundaries(env, directory)
run_llgo(env, ["run", "-target", "wasi", "-emulator",
str(ROOT / "internal/build/testdata/wasm-wasi-threads")],
"wasi threads ok")
run_llgo(env, ["run", "-target", "wasi", "-emulator",
str(ROOT / "internal/build/testdata/wasm-wasi-threaded-fs")],
"wasi threaded filesystem ok")
run_llgo(env, ["test", "-target", "wasi", "-emulator",
str(ROOT / "test/std/errors")], "PASS")
run_llgo(env, ["test", "-target", "wasi", "-emulator", "-run",
"^TestPoolAfterGC$", str(ROOT / "test/std/sync")], "PASS",
timeout=300)
run_llgo(env, ["test", "-target", "wasi", "-emulator",
str(ROOT / "test/std/go/importer")], "PASS")
run_llgo(env, ["test", "-target", "wasi", "-emulator", "-run",
"^(TestTBasicMethods|FuzzExample)$",
str(ROOT / "test/std/testing")], "PASS")
run_llgo(env, ["test", "-target", "wasi", "-emulator",
str(ROOT / "test/std/weak")], "PASS")
run_llgo(env, ["test", "-target", "wasi", "-emulator",
"-run", "^TestConcurrentSelectProposeReplyStress$",
str(ROOT / "test")], "PASS")
subprocess.run(
[LLGO, "build", "-target", "wasi", "-tags", "nogc", "-o", str(module),
str(ROOT / "internal/build/testdata/wasm-wasi-threads")],
check=True,
env=env,
timeout=180,
["go", "run", "./dev/wasmstdlib", "-profile", "W32-WASI",
"-llgo", LLGO, "-report", str(pathlib.Path(directory) / "w32-wamr.json")],
check=True, cwd=ROOT, env=env, timeout=600,
)
result = subprocess.run(
# 52 probe workers, main, and the timer service can overlap while
# WAMR retires their host slots. Go's live-G count is not a join.
[IWASM, "--max-threads=64", "--stack-size=1048576",
"--heap-size=67108864", str(module)],
capture_output=True,
text=True,
timeout=30,
goroot = subprocess.check_output(["go", "env", "GOROOT"], env=env,
text=True).strip()
subprocess.run(
["go", "test", "./test/goroot", "-run", "^TestGoRootRunCases$",
"-count=1", "-args", "-goroot", goroot, "-llgo", LLGO,
"-wasm-profile", "W32-WASI", "-directive-mode", "ci",
"-case", r"^helloworld\.go$", "-min-swap-free-mib=0"],
check=True, cwd=ROOT, env=env, timeout=180,
)
print(result.stdout, end="")
print(result.stderr, end="")
marker = "wasi threads ok"
if result.returncode != 0 or (
marker not in result.stdout.splitlines()
and marker not in result.stderr.splitlines()
):
raise SystemExit(f"WAMR WASI pthread probe failed with exit code {result.returncode}")


if __name__ == "__main__":
Expand Down
Loading
Loading