diff --git a/lib/host/otel.sx b/lib/host/otel.sx index b42f6426..f8db6aca 100644 --- a/lib/host/otel.sx +++ b/lib/host/otel.sx @@ -376,8 +376,14 @@ (lt (otel/latest-trace)) (traces (otel/recent-traces))) (quasiquote - (div :id "otel-dashboard" :data-on-load "@get('/otel/stream')" + (div :id "otel-dashboard" + ;; The host serves single-body responses (no server-push SSE), so the + ;; dashboard stays live by reloading itself — every 3s it re-renders the + ;; latest metrics + trace. /otel/stream remains a snapshot of the newest + ;; span for any client that wants to poll it. + (meta :http-equiv "refresh" :content "3") (h1 "OpenTelemetry") + (p :style "font-size:0.8em;opacity:0.7" "live · auto-refreshes every 3s") (h2 "metrics") (unquote (otel/-metrics-strip m)) (h2 "latest trace") diff --git a/lib/host/serve.sh b/lib/host/serve.sh index 6afde9d5..300d5503 100755 --- a/lib/host/serve.sh +++ b/lib/host/serve.sh @@ -28,6 +28,28 @@ fi PORT="${HOST_PORT:-8910}" +# Self-warm the SERVING JIT. The FIRST HTTP request after a restart pays a one-time +# ~2.5s cost: the shared request path (session middleware + router + instrumented +# handlers + blog render) JIT-compiles under the http-listen resolver, a context a +# boot-time render can't reach. So once the server is answering, we GET the hot +# pages ONCE over real HTTP to absorb that compile before a visitor does (~2.5s -> +# ~78ms warm). The container image has no curl/wget, so we speak HTTP over bash's +# /dev/tcp. Runs detached (survives the exec below); exits after warming. +_warm_serving_jit() { + local path + # wait until /health answers 200 (the port opens only after all modules load) + for _ in $(seq 1 180); do + case "$( { exec 3<>/dev/tcp/127.0.0.1/"$PORT" && printf 'GET /health HTTP/1.0\r\nHost: x\r\n\r\n' >&3 && head -1 <&3; exec 3>&- 3<&-; } 2>/dev/null )" in + *200*) break ;; + esac + sleep 0.5 + done + for path in / /welcome /nt-live-encore /otel; do + { exec 3<>/dev/tcp/127.0.0.1/"$PORT" && printf 'GET %s HTTP/1.0\r\nHost: x\r\n\r\n' "$path" >&3 && cat <&3 >/dev/null; exec 3>&- 3<&-; } 2>/dev/null + done +} +_warm_serving_jit & + # Modules: every load line from conformance.sh's MODULES list, minus the ledger # (not needed to serve). server.sx supplies host/serve. MODULES=( @@ -195,14 +217,6 @@ EPOCH=1 echo "(epoch $EPOCH)" echo "(eval \"(define host/-serve-groups (list host/static-routes otel/routes host/feed-routes host/relations-routes (host/blog-write-routes (fn (tok) nil)) host/blog-routes))\")" EPOCH=$((EPOCH+1)) - # JIT warmup: render the hot pages once through a throwaway app so the FIRST real - # visitor after a restart doesn't eat the cold-compile cost. The blog render path - # (comp-fold + relations + typed-block) JIT-compiles on first call — that was the - # ~2.5s p99 on /:slug, vs ~78ms warm. Reset the otel ring after so warmup spans - # don't skew the live metrics. - echo "(epoch $EPOCH)" - echo "(eval \"(let ((warm (host/make-app host/-serve-groups))) (begin (warm (dream-request \\\"GET\\\" \\\"/\\\" {} \\\"\\\")) (warm (dream-request \\\"GET\\\" \\\"/welcome\\\" {} \\\"\\\")) (warm (dream-request \\\"GET\\\" \\\"/nt-live-encore\\\" {} \\\"\\\")) (warm (dream-request \\\"GET\\\" \\\"/otel\\\" {} \\\"\\\")) (otel/reset!) nil))\")" - EPOCH=$((EPOCH+1)) echo "(epoch $EPOCH)" echo "(eval \"(host/serve $PORT host/-serve-groups)\")" } | exec "$SX_SERVER"