diff --git a/security/lab12/cold-start.json b/security/lab12/cold-start.json new file mode 100644 index 000000000..28148a86b --- /dev/null +++ b/security/lab12/cold-start.json @@ -0,0 +1,66 @@ +{ + "spin": [ + { + "run": 1, + "ms": 646.4, + "attempts": 1, + "body": "{\"unix\":1783593682,\"iso\":\"2026-07-09T13:41:22+03:00\",\"hour_minute\":\"13:41\",\"timezone\":\"Europe/Moscow (UTC+3)\"}" + }, + { + "run": 2, + "ms": 522.8, + "attempts": 1, + "body": "{\"unix\":1783593683,\"iso\":\"2026-07-09T13:41:23+03:00\",\"hour_minute\":\"13:41\",\"timezone\":\"Europe/Moscow (UTC+3)\"}" + }, + { + "run": 3, + "ms": 520.5, + "attempts": 1, + "body": "{\"unix\":1783593684,\"iso\":\"2026-07-09T13:41:24+03:00\",\"hour_minute\":\"13:41\",\"timezone\":\"Europe/Moscow (UTC+3)\"}" + }, + { + "run": 4, + "ms": 522.5, + "attempts": 1, + "body": "{\"unix\":1783593685,\"iso\":\"2026-07-09T13:41:25+03:00\",\"hour_minute\":\"13:41\",\"timezone\":\"Europe/Moscow (UTC+3)\"}" + }, + { + "run": 5, + "ms": 521.9, + "attempts": 1, + "body": "{\"unix\":1783593686,\"iso\":\"2026-07-09T13:41:26+03:00\",\"hour_minute\":\"13:41\",\"timezone\":\"Europe/Moscow (UTC+3)\"}" + } + ], + "docker": [ + { + "run": 1, + "ms": 520.7, + "attempts": 1, + "body": "{\"notes\":4,\"status\":\"ok\"}" + }, + { + "run": 2, + "ms": 507, + "attempts": 1, + "body": "{\"notes\":4,\"status\":\"ok\"}" + }, + { + "run": 3, + "ms": 550.1, + "attempts": 1, + "body": "{\"notes\":4,\"status\":\"ok\"}" + }, + { + "run": 4, + "ms": 479.1, + "attempts": 1, + "body": "{\"notes\":4,\"status\":\"ok\"}" + }, + { + "run": 5, + "ms": 515.4, + "attempts": 1, + "body": "{\"notes\":4,\"status\":\"ok\"}" + } + ] +} diff --git a/security/lab12/docker-hyperfine.json b/security/lab12/docker-hyperfine.json new file mode 100644 index 000000000..7de5fd35e --- /dev/null +++ b/security/lab12/docker-hyperfine.json @@ -0,0 +1,170 @@ +{ + "results": [ + { + "command": "curl.exe -s -o NUL http://127.0.0.1:18082/health", + "mean": 0.023982005999999997, + "stddev": 0.0023385969218620932, + "median": 0.023237651999999994, + "user": 0.008349999999999998, + "system": 0.0072437500000000045, + "min": 0.020925451999999997, + "max": 0.03145585199999999, + "times": [ + 0.023697451999999994, + 0.022800751999999994, + 0.024393151999999994, + 0.026698351999999995, + 0.024728151999999996, + 0.029013251999999996, + 0.026318151999999997, + 0.025662951999999996, + 0.023708151999999996, + 0.026702151999999993, + 0.026135451999999997, + 0.028075551999999997, + 0.025629951999999998, + 0.026850251999999998, + 0.03145585199999999, + 0.026349751999999997, + 0.025734551999999994, + 0.023134751999999995, + 0.022737551999999994, + 0.022379751999999996, + 0.021994851999999995, + 0.022146151999999995, + 0.022820351999999995, + 0.023014251999999992, + 0.022273551999999995, + 0.022181051999999993, + 0.024210651999999992, + 0.023579251999999995, + 0.029711751999999994, + 0.024455051999999998, + 0.024906151999999994, + 0.024868551999999995, + 0.024106051999999996, + 0.023355251999999993, + 0.022938451999999998, + 0.022631351999999993, + 0.021631051999999998, + 0.021626351999999995, + 0.022025151999999992, + 0.023161451999999992, + 0.022371551999999996, + 0.022317751999999996, + 0.021761751999999995, + 0.023313851999999996, + 0.021994451999999998, + 0.021877051999999994, + 0.021564952, + 0.021743851999999994, + 0.021386951999999994, + 0.020925451999999997 + ], + "memory_usage_byte": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "exit_codes": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + } + ] +} diff --git a/security/lab12/lab6-baseline.Dockerfile b/security/lab12/lab6-baseline.Dockerfile new file mode 100644 index 000000000..955540b11 --- /dev/null +++ b/security/lab12/lab6-baseline.Dockerfile @@ -0,0 +1,22 @@ +FROM golang:1.24-alpine AS builder + +WORKDIR /src +COPY go.mod ./ +COPY *.go ./ +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \ + go build -trimpath -ldflags="-s -w" -o /out/quicknotes . +RUN mkdir -p /out/data && chmod 0777 /out/data + +FROM scratch + +COPY --from=builder /out/quicknotes /quicknotes +COPY --chmod=0777 --from=builder /out/data /data +COPY seed.json /seed.json +COPY --chown=65532:65532 seed.json /data/notes.json + +USER 65532:65532 +EXPOSE 8080 +ENV ADDR=:8080 +ENV DATA_PATH=/data/notes.json +ENV SEED_PATH=/seed.json +ENTRYPOINT ["/quicknotes"] diff --git a/security/lab12/spin-hyperfine.json b/security/lab12/spin-hyperfine.json new file mode 100644 index 000000000..a2f408e5c --- /dev/null +++ b/security/lab12/spin-hyperfine.json @@ -0,0 +1,170 @@ +{ + "results": [ + { + "command": "curl.exe -s -o NUL http://127.0.0.1:3000/time", + "mean": 0.024237527999999998, + "stddev": 0.0028874229115667866, + "median": 0.02313552, + "user": 0.010074999999999992, + "system": 0.010049999999999996, + "min": 0.021076269999999998, + "max": 0.03264947, + "times": [ + 0.02645137, + 0.03245797, + 0.029207669999999995, + 0.026317569999999995, + 0.02381537, + 0.022305869999999995, + 0.02220177, + 0.03264947, + 0.024158369999999995, + 0.03187107, + 0.03208187, + 0.02825567, + 0.02482507, + 0.023636969999999997, + 0.024488269999999996, + 0.025079669999999995, + 0.022722369999999995, + 0.022641069999999996, + 0.02222567, + 0.026005369999999996, + 0.02442997, + 0.02453357, + 0.02261247, + 0.022736469999999998, + 0.02184567, + 0.02296967, + 0.023883069999999996, + 0.023914469999999997, + 0.02204467, + 0.02242887, + 0.02390207, + 0.02285787, + 0.02585367, + 0.02278137, + 0.021737169999999997, + 0.023738769999999996, + 0.021808569999999996, + 0.023384069999999996, + 0.022918169999999998, + 0.022564769999999994, + 0.02180147, + 0.021076269999999998, + 0.02318147, + 0.022982569999999997, + 0.02308957, + 0.022484269999999997, + 0.02205337, + 0.022662369999999998, + 0.02374547, + 0.02245567 + ], + "memory_usage_byte": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "exit_codes": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + } + ] +} diff --git a/security/lab12/summary.json b/security/lab12/summary.json new file mode 100644 index 000000000..daed83d55 --- /dev/null +++ b/security/lab12/summary.json @@ -0,0 +1,79 @@ +{ + "versions": { + "spin": "3.4.0", + "tinygo": "0.41.0 container digest sha256:0376cd2d957e7304a1a653e2f7ef44cc27e1caa3a445073dfb09731ad4c19fec", + "wasmtime": "46.0.1", + "hyperfine": "1.20.0", + "docker": "29.2.0", + "host_go": "1.26.4 windows/amd64" + }, + "sizes_bytes": { + "spin_main_wasm": 369708, + "wasmtime_cli_main_wasm": 196189, + "docker_image_inspect_size": 2506341 + }, + "warm": { + "spin": { + "runs": 50, + "mean_ms": 24.24, + "min_ms": 21.08, + "max_ms": 32.65, + "p50_ms": 23.14, + "p95_ms": 32.08 + }, + "docker": { + "runs": 50, + "mean_ms": 23.98, + "min_ms": 20.93, + "max_ms": 31.46, + "p50_ms": 23.24, + "p95_ms": 29.01 + }, + "wasmtime_cli": { + "runs": 50, + "mean_ms": 30.95, + "min_ms": 24.06, + "max_ms": 212.35, + "p50_ms": 25.37, + "p95_ms": 40.45 + } + }, + "cold": { + "spin": { + "runs": 5, + "values_ms": [ + 646.4, + 522.8, + 520.5, + 522.5, + 521.9 + ], + "p50_ms": 522.5, + "p95_ms": 646.4 + }, + "docker": { + "runs": 5, + "values_ms": [ + 520.7, + 507.0, + 550.1, + 479.1, + 515.4 + ], + "p50_ms": 515.4, + "p95_ms": 550.1 + }, + "wasmtime_cli": { + "runs": 5, + "values_ms": [ + 1185.4, + 23.4, + 24.3, + 27.2, + 25.6 + ], + "p50_ms": 25.6, + "p95_ms": 1185.4 + } + } +} diff --git a/security/lab12/wasmtime-cold.json b/security/lab12/wasmtime-cold.json new file mode 100644 index 000000000..c695c73ec --- /dev/null +++ b/security/lab12/wasmtime-cold.json @@ -0,0 +1,27 @@ +[ + { + "run": 1, + "ms": 1185.4, + "body": "{\"unix\":1783593711,\"iso\":\"2026-07-09T13:41:51+03:00\",\"hour_minute\":\"13:41\",\"timezone\":\"Europe/Moscow (UTC+3)\"}" + }, + { + "run": 2, + "ms": 23.4, + "body": "{\"unix\":1783593711,\"iso\":\"2026-07-09T13:41:51+03:00\",\"hour_minute\":\"13:41\",\"timezone\":\"Europe/Moscow (UTC+3)\"}" + }, + { + "run": 3, + "ms": 24.3, + "body": "{\"unix\":1783593711,\"iso\":\"2026-07-09T13:41:51+03:00\",\"hour_minute\":\"13:41\",\"timezone\":\"Europe/Moscow (UTC+3)\"}" + }, + { + "run": 4, + "ms": 27.2, + "body": "{\"unix\":1783593711,\"iso\":\"2026-07-09T13:41:51+03:00\",\"hour_minute\":\"13:41\",\"timezone\":\"Europe/Moscow (UTC+3)\"}" + }, + { + "run": 5, + "ms": 25.6, + "body": "{\"unix\":1783593711,\"iso\":\"2026-07-09T13:41:51+03:00\",\"hour_minute\":\"13:41\",\"timezone\":\"Europe/Moscow (UTC+3)\"}" + } +] diff --git a/security/lab12/wasmtime-hyperfine.json b/security/lab12/wasmtime-hyperfine.json new file mode 100644 index 000000000..2e40de15b --- /dev/null +++ b/security/lab12/wasmtime-hyperfine.json @@ -0,0 +1,170 @@ +{ + "results": [ + { + "command": "wasmtime run --env REQUEST_METHOD=GET --env PATH_INFO=/time wasm-cli/main.wasm > NUL", + "mean": 0.030949400000000002, + "stddev": 0.02696123678402922, + "median": 0.025374240000000006, + "user": 0.015312500000000008, + "system": 0.010750000000000006, + "min": 0.024060540000000005, + "max": 0.21234684000000004, + "times": [ + 0.024060540000000005, + 0.028450840000000005, + 0.024281240000000003, + 0.06692904000000001, + 0.04045494, + 0.038689440000000005, + 0.21234684000000004, + 0.028354740000000003, + 0.028201040000000004, + 0.026109440000000005, + 0.026340840000000004, + 0.024797640000000003, + 0.025572340000000006, + 0.025320540000000002, + 0.02505264, + 0.024582540000000007, + 0.024647640000000005, + 0.024994040000000002, + 0.02560404, + 0.024811440000000004, + 0.02513614, + 0.027251140000000007, + 0.027606840000000007, + 0.02486034, + 0.028002640000000002, + 0.025903040000000002, + 0.025295340000000006, + 0.027411840000000007, + 0.02634074, + 0.025252240000000002, + 0.028863140000000002, + 0.027037340000000007, + 0.026311640000000004, + 0.025356840000000005, + 0.025116940000000004, + 0.025130640000000003, + 0.024558340000000005, + 0.025391640000000007, + 0.024554240000000005, + 0.024889940000000006, + 0.02532724, + 0.024982340000000006, + 0.024421740000000004, + 0.025167340000000003, + 0.025333940000000006, + 0.025190840000000006, + 0.026331940000000005, + 0.027404740000000004, + 0.027437440000000007, + 0.025999640000000004 + ], + "memory_usage_byte": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "exit_codes": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + } + ] +} diff --git a/submissions/lab12.md b/submissions/lab12.md new file mode 100644 index 000000000..f9c4e29d7 --- /dev/null +++ b/submissions/lab12.md @@ -0,0 +1,206 @@ +# Lab 12 - WebAssembly Containers: QuickNotes-Style Time Endpoint + +## Repository freshness + +I checked the public course repo before starting. `inno-devops-labs/DevOps-Intro` `main` was still: + +```text +8de962e7b49a7056104cc366609053f518eb2f70 refs/heads/main +``` + +`feature/lab12` was created from local `upstream/main` at that same commit. + +## Implemented files + +- [`wasm/moscow-time/main.go`](../wasm/moscow-time/main.go) +- [`wasm/moscow-time/go.mod`](../wasm/moscow-time/go.mod) +- [`wasm/moscow-time/go.sum`](../wasm/moscow-time/go.sum) +- [`wasm/moscow-time/spin.toml`](../wasm/moscow-time/spin.toml) +- [`wasm/moscow-time/main.wasm`](../wasm/moscow-time/main.wasm) +- [`wasm-cli/main.go`](../wasm-cli/main.go) +- [`wasm-cli/go.mod`](../wasm-cli/go.mod) +- [`wasm-cli/main.wasm`](../wasm-cli/main.wasm) +- [`security/lab12/lab6-baseline.Dockerfile`](../security/lab12/lab6-baseline.Dockerfile) +- [`security/lab12/summary.json`](../security/lab12/summary.json) +- [`security/lab12/spin-hyperfine.json`](../security/lab12/spin-hyperfine.json) +- [`security/lab12/docker-hyperfine.json`](../security/lab12/docker-hyperfine.json) +- [`security/lab12/cold-start.json`](../security/lab12/cold-start.json) +- [`security/lab12/wasmtime-cold.json`](../security/lab12/wasmtime-cold.json) +- [`security/lab12/wasmtime-hyperfine.json`](../security/lab12/wasmtime-hyperfine.json) + +## Test rig + +```text +OS: Microsoft Windows 11 Pro 10.0.26200, 64-bit +CPU: AMD Ryzen 7 5800H, 8 cores / 16 logical processors +RAM: 17,024,741,376 bytes +Docker: client 29.2.0, server 29.2.0 +Spin: 3.4.0 +TinyGo: 0.41.0 via tinygo/tinygo:0.41.0 +TinyGo image digest: sha256:0376cd2d957e7304a1a653e2f7ef44cc27e1caa3a445073dfb09731ad4c19fec +Wasmtime: 46.0.1 +Hyperfine: 1.20.0 +``` + +## Task 1 - Spin SDK endpoint + +The project was scaffolded with the current Spin template: + +```text +spin templates install --git https://github.com/spinframework/spin --update +mkdir -p wasm && cd wasm +spin new -t http-go moscow-time --accept-defaults +``` + +The handler uses `spinhttp.Handle(...)` from the generated SDK import path, `github.com/spinframework/spin-go-sdk/v2/http`. It returns Moscow time using a fixed UTC+3 zone so the RFC3339 value carries `+03:00` without relying on TinyGo time-zone data. + +Build evidence: + +```text +spin build +Building component moscow-time with `tinygo build -target=wasip1 -buildmode=c-shared -no-debug -o main.wasm .` +Finished building all Spin components + +main.wasm size: 369,708 bytes +``` + +`spin.toml` key settings: + +```toml +[[trigger.http]] +route = "/time" +component = "moscow-time" + +[component.moscow-time] +source = "main.wasm" +allowed_outbound_hosts = [] + +[component.moscow-time.build] +command = "tinygo build -target=wasip1 -buildmode=c-shared -no-debug -o main.wasm ." +``` + +Runtime proof: + +```text +spin up --listen 127.0.0.1:3000 +curl -s http://127.0.0.1:3000/time +{"unix":1783593281,"iso":"2026-07-09T13:34:41+03:00","hour_minute":"13:34","timezone":"Europe/Moscow (UTC+3)"} +``` + +### Design answers a-d + +**a) Browser WASM vs server WASM** + +`GOOS=js GOARCH=wasm` targets a browser JavaScript host. It assumes `wasm_exec.js`, browser APIs, and a JS event loop. TinyGo's `wasip1` target produces a server-side WASI module: there is no DOM, no browser storage, and no JS host API, but the module gets a portable system-interface ABI for server runtimes such as Spin and Wasmtime. + +**b) Why `-buildmode=c-shared`** + +Spin does not call a CLI-style `_start` function for HTTP components. It hosts a component that exports the ABI entry points expected by the Spin/wasi-http adapter and generated SDK. `-buildmode=c-shared` makes TinyGo emit the exported symbols the Spin host expects; without it, the module can build in the wrong shape and fail at request time. + +**c) `allowed_outbound_hosts = []`** + +Spin uses a capability model: a component receives only the host capabilities granted in the manifest. An empty outbound-host list means the component cannot open arbitrary network connections even if the code tries. Docker's `--network none` removes the container's network namespace access more broadly. Spin's model is narrower and more declarative: the manifest can allow no network, one host, or a small host allowlist. + +**d) TinyGo stdlib gaps hit** + +I avoided `time.LoadLocation("Europe/Moscow")` because TinyGo/WASI does not ship the normal host time-zone database. I used `time.FixedZone("MSK", 3*60*60)` instead. I also built the JSON with `fmt.Sprintf` rather than `encoding/json` over `map[string]any`, which avoids reflection-heavy paths that are more fragile in TinyGo. + +## Task 2 - Perf comparison vs Docker + +The Docker baseline is a Lab 6-style scratch image built from [`security/lab12/lab6-baseline.Dockerfile`](../security/lab12/lab6-baseline.Dockerfile). It runs the same QuickNotes app and exposes `/health`. + +Docker baseline evidence: + +```text +docker build --no-cache -f security/lab12/lab6-baseline.Dockerfile -t qn-lab12-docker:baseline app +docker image inspect qn-lab12-docker:baseline +id=sha256:68588dcd3f814470422b1c5b45efe4479db2052f760314ac17445c87e7385a3e +size=2506341 +user=65532:65532 +entrypoint=["/quicknotes"] + +curl -s http://127.0.0.1:18082/health +{"notes":4,"status":"ok"} +``` + +Warm latency commands: + +```text +hyperfine --warmup 5 --runs 50 --export-json security/lab12/spin-hyperfine.json "curl.exe -s -o NUL http://127.0.0.1:3000/time" +hyperfine --warmup 5 --runs 50 --export-json security/lab12/docker-hyperfine.json "curl.exe -s -o NUL http://127.0.0.1:18082/health" +``` + +Cold start method: stop the runtime, start it, poll until the first successful response, repeat 5 times. + +| Dimension | Lab 6 Docker | Lab 12 WASM/Spin | +|---|---:|---:| +| Artifact size | 2,506,341 bytes | 369,708 bytes | +| Cold start p50 | 515.4 ms | 522.5 ms | +| Cold start p95 | 550.1 ms | 646.4 ms | +| Warm latency p50 | 23.24 ms | 23.14 ms | +| Warm latency p95 | 29.01 ms | 32.08 ms | + +Cold start samples: + +```text +Spin: 646.4 ms, 522.8 ms, 520.5 ms, 522.5 ms, 521.9 ms +Docker: 520.7 ms, 507.0 ms, 550.1 ms, 479.1 ms, 515.4 ms +``` + +### Design answers e-g + +**e) Cold-start dominators** + +For Docker, the cold path is container creation: namespace/cgroup setup, process start, port publishing, and application initialization. Image extraction was already cached locally. For Spin, the cold path is host startup, manifest load, Wasmtime module instantiation, and the first wasi-http request path. + +**f) Where WASM is better vs Docker** + +WASM is strong for tiny request handlers, plugins, multi-tenant edge workloads, and code that benefits from fast instantiation with a strict capability sandbox. Docker is still right for full applications, background workers, services that need broad OS APIs, large language/runtime ecosystems, or workloads that expect normal Linux process/container semantics. + +**g) Multi-tenant safety** + +WASM makes host escape through ambient OS access harder. A compromised component cannot just invoke arbitrary syscalls, inspect `/proc`, open raw sockets, or fork helper processes unless the host explicitly grants those capabilities. That is a stronger default boundary for running untrusted tenant code than relying only on Linux namespaces and seccomp. + +## Bonus Task - Two WASM execution models + +The standalone CLI module lives in [`wasm-cli/`](../wasm-cli/). It has no Spin SDK and reads `REQUEST_METHOD` and `PATH_INFO` from the environment. + +Build and run commands: + +```text +cd wasm-cli +tinygo build -o main.wasm -target=wasi -no-debug ./main.go +wasmtime run --env REQUEST_METHOD=GET --env PATH_INFO=/time main.wasm +{"unix":1783593330,"iso":"2026-07-09T13:35:30+03:00","hour_minute":"13:35","timezone":"Europe/Moscow (UTC+3)"} +``` + +Bonus size and cold-start comparison: + +| Dimension | Spin wasi-http component | Bare Wasmtime CLI | +|---|---:|---:| +| Module size | 369,708 bytes | 196,189 bytes | +| Cold/per-invocation p50 | 522.5 ms to start Spin and first HTTP response | 25.6 ms per `wasmtime run` | +| Cold/per-invocation p95 | 646.4 ms | 1185.4 ms, including first-run host/cache outlier | +| Warm/process p50 | 23.14 ms HTTP request through persistent Spin server | 25.37 ms per process invocation | + +Wasmtime samples: + +```text +wasmtime run samples: 1185.4 ms, 23.4 ms, 24.3 ms, 27.2 ms, 25.6 ms +hyperfine p50: 25.37 ms +hyperfine p95: 40.45 ms +``` + +### Design answers h-j + +**h) Why the Spin component cannot run under bare `wasmtime run`** + +The Spin component is a wasi-http component hosted by Spin's trigger system. It exports an HTTP handler shape, not a standalone `_start` CLI entrypoint. Bare `wasmtime run` expects a command-style WASI module, so the Spin component needs a wasi-http host such as Spin rather than the plain CLI runner. + +**i) What Spin adds over bare Wasmtime** + +Spin adds the HTTP server loop, manifest parsing, route matching, component loading, wasi-http integration, outbound-host policy, logging, and runtime management. It uses Wasmtime internally, but it turns a component into an HTTP application platform. + +**j) When each execution model fits** + +Per-invocation `wasmtime run` fits batch-style jobs, filters, command hooks, and CGI-shaped request handlers where startup-per-request is acceptable. Spin's persistent wasi-http model fits API endpoints and edge services where routing, policy, and repeated HTTP requests should be handled by a long-lived host. diff --git a/wasm-cli/go.mod b/wasm-cli/go.mod new file mode 100644 index 000000000..7a6bd35eb --- /dev/null +++ b/wasm-cli/go.mod @@ -0,0 +1,3 @@ +module quicknotes-wasm-cli + +go 1.24 diff --git a/wasm-cli/main.go b/wasm-cli/main.go new file mode 100644 index 000000000..82691dfd0 --- /dev/null +++ b/wasm-cli/main.go @@ -0,0 +1,33 @@ +package main + +import ( + "fmt" + "os" + "time" +) + +func main() { + method := os.Getenv("REQUEST_METHOD") + if method == "" { + method = "GET" + } + path := os.Getenv("PATH_INFO") + if path == "" { + path = "/time" + } + + if method != "GET" || path != "/time" { + fmt.Printf(`{"error":%q,"method":%q,"path":%q}`+"\n", "not found", method, path) + os.Exit(1) + } + + moscowZone := time.FixedZone("MSK", 3*60*60) + moscow := time.Now().In(moscowZone) + fmt.Printf( + `{"unix":%d,"iso":%q,"hour_minute":%q,"timezone":%q}`+"\n", + moscow.Unix(), + moscow.Format(time.RFC3339), + moscow.Format("15:04"), + "Europe/Moscow (UTC+3)", + ) +} diff --git a/wasm-cli/main.wasm b/wasm-cli/main.wasm new file mode 100644 index 000000000..2b20d5135 Binary files /dev/null and b/wasm-cli/main.wasm differ diff --git a/wasm/moscow-time/.gitignore b/wasm/moscow-time/.gitignore new file mode 100644 index 000000000..b56501047 --- /dev/null +++ b/wasm/moscow-time/.gitignore @@ -0,0 +1,2 @@ +main.wasm +.spin/ diff --git a/wasm/moscow-time/go.mod b/wasm/moscow-time/go.mod new file mode 100644 index 000000000..accfb4154 --- /dev/null +++ b/wasm/moscow-time/go.mod @@ -0,0 +1,7 @@ +module github.com/moscow_time + +go 1.22 + +require github.com/spinframework/spin-go-sdk/v2 v2.2.1 + +require github.com/julienschmidt/httprouter v1.3.0 // indirect diff --git a/wasm/moscow-time/go.sum b/wasm/moscow-time/go.sum new file mode 100644 index 000000000..447c042d5 --- /dev/null +++ b/wasm/moscow-time/go.sum @@ -0,0 +1,4 @@ +github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/spinframework/spin-go-sdk/v2 v2.2.1 h1:ceAbRU+D3xmyZ8ScDLeFoT763ikFIUEmSjgsrD11v8k= +github.com/spinframework/spin-go-sdk/v2 v2.2.1/go.mod h1:vocVZB4qlTG8C5yoliKIAJCuv4x7sqK0GmVkWeD9N/A= diff --git a/wasm/moscow-time/main.go b/wasm/moscow-time/main.go new file mode 100644 index 000000000..dfa982292 --- /dev/null +++ b/wasm/moscow-time/main.go @@ -0,0 +1,31 @@ +package main + +import ( + "fmt" + "net/http" + "time" + + spinhttp "github.com/spinframework/spin-go-sdk/v2/http" +) + +func init() { + spinhttp.Handle(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + + moscowZone := time.FixedZone("MSK", 3*60*60) + moscow := time.Now().In(moscowZone) + body := fmt.Sprintf( + `{"unix":%d,"iso":%q,"hour_minute":%q,"timezone":%q}`, + moscow.Unix(), + moscow.Format(time.RFC3339), + moscow.Format("15:04"), + "Europe/Moscow (UTC+3)", + ) + + w.Header().Set("Content-Type", "application/json") + fmt.Fprintln(w, body) + }) +} diff --git a/wasm/moscow-time/main.wasm b/wasm/moscow-time/main.wasm new file mode 100644 index 000000000..2671d76f1 Binary files /dev/null and b/wasm/moscow-time/main.wasm differ diff --git a/wasm/moscow-time/spin.toml b/wasm/moscow-time/spin.toml new file mode 100644 index 000000000..8874c31b8 --- /dev/null +++ b/wasm/moscow-time/spin.toml @@ -0,0 +1,20 @@ +#:schema https://schemas.spinframework.dev/spin/manifest-v2/latest.json + +spin_manifest_version = 2 + +[application] +name = "moscow-time" +version = "0.1.0" +authors = ["BearAx "] +description = "" + +[[trigger.http]] +route = "/time" +component = "moscow-time" + +[component.moscow-time] +source = "main.wasm" +allowed_outbound_hosts = [] +[component.moscow-time.build] +command = "tinygo build -target=wasip1 -buildmode=c-shared -no-debug -o main.wasm ." +watch = ["**/*.go", "go.mod"]