diff --git a/examples/demo-01-image-strategy/demo.sh b/examples/demo-01-image-strategy/demo.sh index 36ea2a7..3f7572c 100755 --- a/examples/demo-01-image-strategy/demo.sh +++ b/examples/demo-01-image-strategy/demo.sh @@ -10,8 +10,9 @@ # # Image strategy is the lowest-hanging-fruit performance and security win in # containerized C++. A multi-stage build leaves the toolchain (GCC, ld, headers, -# build deps) OUT of the runtime image — a ~26× size drop from the naive -# single-stage baseline to ubi-micro, and a matching cut in CVE surface — with +# build deps) OUT of the runtime image — a ~20-26× size drop from the naive +# single-stage baseline to ubi-micro (the script prints the exact multiplier +# for this build), and a matching cut in CVE surface — with # NO measurable p50 penalty. LTO plus a representative PGO profile then buys a # further few percent on the hot path, essentially for free once the pipeline # is in place. @@ -99,6 +100,8 @@ callout \ "Ports: bench containers on ${PORT_BASE}+ (one per variant)" \ "Load: hey -n 5000 -c 50 per variant → p50/p95/p99" \ "PGO: $([[ $DO_PGO -eq 1 ]] && echo 'enabled (two-pass build + training run)' || echo 'skipped (--no-pgo)')" +callout "" "The service itself is one file — the SAME binary in every variant:" +code_ref "src/main.cpp" 65 "httplib routing (/, /echo, /healthz, /metrics) — constexpr-lean, zero-alloc startup" pause "Ready to build? Press Enter" # ── Step 1: Build the image variants ──────────────────────────────────────── @@ -107,29 +110,37 @@ callout "First run compiles from source; later runs hit the podman layer cache." "Never proceed to measurement after a failed build — a broken image would" \ "just show up as a bogus benchmark row." -log_step "Building UBI multi-stage (LTO on, no PGO)" -if ! podman build -f Containerfile.ubi-multistage -t "${IMG_PREFIX}:ubi-multistage" .; then - log_err "ubi-multistage build failed — stopping (nothing valid to measure)." - exit 1 +if should_build "${IMG_PREFIX}:ubi-multistage"; then + log_step "Building UBI multi-stage (LTO on, no PGO)" + if ! podman build -f Containerfile.ubi-multistage -t "${IMG_PREFIX}:ubi-multistage" .; then + log_err "ubi-multistage build failed — stopping (nothing valid to measure)." + exit 1 + fi fi -log_step "Building UBI-micro (fully-static binary, production answer)" -if ! podman build -f Containerfile.ubi-micro -t "${IMG_PREFIX}:ubi-micro" .; then - log_err "ubi-micro build failed — stopping (nothing valid to measure)." - exit 1 +if should_build "${IMG_PREFIX}:ubi-micro"; then + log_step "Building UBI-micro (fully-static binary, production answer)" + if ! podman build -f Containerfile.ubi-micro -t "${IMG_PREFIX}:ubi-micro" .; then + log_err "ubi-micro build failed — stopping (nothing valid to measure)." + exit 1 + fi fi -log_step "Building UBI-micro-glibc-mismatch (TEACHING REFERENCE — intentionally fails at runtime)" -if ! podman build -f Containerfile.ubi-micro-glibc-mismatch -t "${IMG_PREFIX}:ubi-micro-glibc-mismatch" .; then - log_err "ubi-micro-glibc-mismatch build failed — stopping (this variant must BUILD;" - log_err "it's only meant to fail at RUNTIME, which is the lesson)." - exit 1 +if should_build "${IMG_PREFIX}:ubi-micro-glibc-mismatch"; then + log_step "Building UBI-micro-glibc-mismatch (TEACHING REFERENCE — intentionally fails at runtime)" + if ! podman build -f Containerfile.ubi-micro-glibc-mismatch -t "${IMG_PREFIX}:ubi-micro-glibc-mismatch" .; then + log_err "ubi-micro-glibc-mismatch build failed — stopping (this variant must BUILD;" + log_err "it's only meant to fail at RUNTIME, which is the lesson)." + exit 1 + fi fi -log_step "Building naive single-stage (anti-pattern)" -if ! podman build -f Containerfile.single-stage-naive -t "${IMG_PREFIX}:single-stage-naive" .; then - log_err "single-stage-naive build failed — stopping (nothing valid to measure)." - exit 1 +if should_build "${IMG_PREFIX}:single-stage-naive"; then + log_step "Building naive single-stage (anti-pattern)" + if ! podman build -f Containerfile.single-stage-naive -t "${IMG_PREFIX}:single-stage-naive" .; then + log_err "single-stage-naive build failed — stopping (nothing valid to measure)." + exit 1 + fi fi callout "" "Four images built. The single-stage one still ships GCC, ld, and the" \ @@ -139,6 +150,9 @@ pause # ── Step 2: PGO two-pass build (optional) ──────────────────────────────────── if [[ $DO_PGO -eq 1 ]]; then + if [[ "${DEMO_NO_BUILD:-0}" == "1" ]] && image_exists "${IMG_PREFIX}:pgo"; then + log_info "Reusing existing ${IMG_PREFIX}:pgo (DEMO_NO_BUILD=1) — skipping the two-pass PGO build." + else demo_step "PGO two-pass build: instrument → train → rebuild with the profile" callout "GCC PGO is three phases run back-to-back: compile an instrumented" \ "binary, drive it with a representative workload to gather .gcda profile" \ @@ -200,6 +214,7 @@ if [[ $DO_PGO -eq 1 ]]; then "below is PGO alone — nothing else changed." fi pause + fi else log_info "PGO skipped (--no-pgo)." fi @@ -217,7 +232,20 @@ podman images \ | sort -u \ | column -t \ || true -callout "" "The naive single-stage image is ~26× the size of ubi-micro, and almost" \ + +# Compute the naive-vs-micro multiplier from the ACTUAL image bytes rather +# than hard-coding a number that drifts as base images change. inspect gives +# raw bytes; awk turns it into a clean "NN×". +naive_b=$(podman image inspect -f '{{.Size}}' "${IMG_PREFIX}:single-stage-naive" 2>/dev/null || echo 0) +micro_b=$(podman image inspect -f '{{.Size}}' "${IMG_PREFIX}:ubi-micro" 2>/dev/null || echo 0) +if [[ "${micro_b}" -gt 0 && "${naive_b}" -gt 0 ]]; then + RATIO=$(awk -v n="$naive_b" -v m="$micro_b" 'BEGIN{printf "%.0f", n/m}') + RATIO_TXT="~${RATIO}× the size of ubi-micro" +else + RATIO_TXT="many times the size of ubi-micro" +fi + +callout "" "The naive single-stage image is ${RATIO_TXT}, and almost" \ "all of that gap is the toolchain sitting in production: GCC, ld, headers," \ "build deps — none needed at runtime, all of them CVE surface. Multi-stage" \ "drops them; ubi-micro also statically links libstdc++ for the smallest floor." \ diff --git a/examples/demo-02-stl-layout/demo.sh b/examples/demo-02-stl-layout/demo.sh index 80f4a22..16444fd 100755 --- a/examples/demo-02-stl-layout/demo.sh +++ b/examples/demo-02-stl-layout/demo.sh @@ -87,16 +87,20 @@ callout \ "Sizes: 64 · 1024 · 16384 · 262144 Ops: point lookup · iterate-sum" \ "Pressure: cgroup memory.max=$MEMORY_LIMIT, no swap" \ "Outputs: results-baseline.json · results-pressured.json + table" +callout "" "The four containers and both benchmarks live in one file:" +code_ref "src/main.cpp" 39 "container choices + BM_Lookup_Hit / BM_IterateAndSum (Google Benchmark)" # ── Step 1: Build the bench image ─────────────────────────────────────────── demo_step "Build the benchmark image" callout "First build ~3-5 min (Conan pulls boost + Google Benchmark)." \ "Subsequent runs hit the podman layer cache (~30s for both phases)." -if ! podman build -f Containerfile -t "$IMAGE" .; then - log_err "podman build failed — nothing to benchmark. Stopping here." - exit 1 +if should_build "$IMAGE"; then + if ! podman build -f Containerfile -t "$IMAGE" .; then + log_err "podman build failed — nothing to benchmark. Stopping here." + exit 1 + fi + log_ok "Image built: $IMAGE" fi -log_ok "Image built: $IMAGE" pause # ── Step 2: Phase 1 — baseline (no memory limit) ──────────────────────────── diff --git a/examples/demo-03-io-uring-grpc/demo.sh b/examples/demo-03-io-uring-grpc/demo.sh index 5da8cf0..0b31a5a 100755 --- a/examples/demo-03-io-uring-grpc/demo.sh +++ b/examples/demo-03-io-uring-grpc/demo.sh @@ -62,6 +62,7 @@ while [[ $# -gt 0 ]]; do case "$1" in --keep) KEEP_UP=1; shift ;; --clean) CLEAN_ONLY=1; shift ;; + --stack-down) CLEAN_ONLY=1; shift ;; # demo-03's clean is already image-preserving --production) USE_PRODUCTION=1; shift ;; --no-pause) export DEMO_NO_PAUSE=1; shift ;; -h|--help) sed -n '2,45p' "$0"; exit 0 ;; @@ -69,10 +70,19 @@ while [[ $# -gt 0 ]]; do esac done +# The presentation cockpit can ask us to leave the stack up (DEMO_KEEP_STACK=1) +# so the next run of a stack demo skips bring-up. Same effect as --keep. +[[ "${DEMO_KEEP_STACK:-0}" == "1" ]] && KEEP_UP=1 + require podman OBS="$REPO_ROOT/observability/compose.yml" +# Absolute paths for the tutorial dashboard mounts (relative paths in the +# included observability/compose.yml would resolve against this demo's dir). +export OBS_DASHBOARDS_DIR="$REPO_ROOT/observability/grafana/dashboards" +export OBS_PROVIDER_FILE="$REPO_ROOT/observability/grafana/otel-provisioning/tutorial-dashboards.yaml" + if (( USE_PRODUCTION )); then # Verify the one-time host setup is in place before bringing up. # Each check is short; failures point at the corresponding @@ -150,12 +160,21 @@ callout \ "io_uring direct: :9000 (raw liburing submission/completion ring)" \ "Asio io_uring: :9001 (same kernel calls, executor abstraction)" \ "Grafana: $GRAFANA_URL (anonymous viewer)" +callout "" "Three server heads, three source files worth opening:" +code_ref "src/grpc_async_server.cpp" 67 "async gRPC completion-queue worker loop (Proceed() state machine)" +code_ref "src/echo_uring.cpp" 1 "raw liburing submission/completion ring (multishot on kernels ≥6.0)" +code_ref "proto/echo.proto" 1 "the Echo service contract" # ── Step 1: Build and bring up ───────────────────────────────────────── demo_step "Build the demo image and bring up the stack + LGTM backend" callout "First build is ~30-45 min (OTel + gRPC + asio compiled from source" \ "under the override profile). Warm rebuilds are ~2-3 min." -if ! "${COMPOSE[@]}" up -d --build; then +BUILD_FLAG="--build" +if [[ "${DEMO_NO_BUILD:-0}" == "1" ]] && image_exists "cpp-tut/demo-03:latest"; then + log_info "Reusing cpp-tut/demo-03:latest (DEMO_NO_BUILD=1) — starting without --build" + BUILD_FLAG="" +fi +if ! "${COMPOSE[@]}" up -d ${BUILD_FLAG}; then log_err "compose up failed — not going any further (nothing to load)." "${COMPOSE[@]}" logs --tail=40 demo-03-svc 2>&1 || true exit 1 @@ -276,12 +295,18 @@ pause # ── Step 5: Grafana — the gRPC instrumentation ───────────────────────── demo_step "Inspect the gRPC instrumentation in Grafana" grafana_callout "$GRAFANA_URL" \ - "Explore → Prometheus / Tempo (no prebuilt demo-03 dashboard)" \ - "demo3.grpc.latency — per-method latency histogram (Prometheus)" \ - "demo3.grpc.requests — request counter, rolls up by status code" \ - "demo3.tcp.iouring.connections — direct liburing connection gauge" \ - "demo3.tcp.asio.connections — Asio backend connection gauge" \ - "Traces (Tempo) — drill into individual RPC spans" + "'Demo 03 — io_uring + async gRPC' — Tutorial folder (Dashboards → Browse)" \ + "gRPC request rate (stat) — demo3_grpc_requests_total" \ + "gRPC latency p50/95/99 (timeseries) — demo3_grpc_latency_milliseconds histogram" \ + "TCP conns/s (timeseries) — io_uring direct vs Asio" \ + "Recent gRPC traces (table) — Tempo, click a row for the span tree" \ + "Service logs (logs) — Loki" +callout "" "Prefer Explore? Paste these into a Prometheus Explore query:" \ + " sum(rate(demo3_grpc_requests_total[1m]))" \ + " histogram_quantile(0.99, sum(rate(demo3_grpc_latency_milliseconds_bucket[1m])) by (le))" \ + " sum(rate(demo3_tcp_iouring_connections_total[1m]))" \ + " sum(rate(demo3_tcp_asio_connections_total[1m]))" \ + "…and in a Tempo Explore, TraceQL: { resource.service.name=\"demo-03-svc\" }" callout "The TCP echo servers are deliberately un-instrumented (they're the" \ "'floor'); only the gRPC path carries OTel, so you can see what the" \ "semantics cost — in latency AND in observable surface area." @@ -290,7 +315,14 @@ pause # ── Teardown ──────────────────────────────────────────────────────────── echo if (( KEEP_UP == 0 )); then - pause "Press Enter to tear down the stack (or Ctrl-C to leave it running)" + # Under the presentation cockpit, DON'T invite Ctrl-C here — a Ctrl-C at + # this prompt would SIGINT the whole orchestrated run. Tear down quietly + # (the EXIT trap does the actual work) and let the cockpit move on. + if [[ "${DEMO_ORCHESTRATED:-0}" == "1" ]]; then + log_info "Orchestrated run — tearing down demo-03 stack and continuing." + else + pause "Explore Grafana now if you like, then press Enter to tear down the stack" + fi else log_ok "Demo 03 complete — stack left up (--keep)." fi diff --git a/examples/demo-04-observability/demo.sh b/examples/demo-04-observability/demo.sh index 6bf75f0..abb8396 100755 --- a/examples/demo-04-observability/demo.sh +++ b/examples/demo-04-observability/demo.sh @@ -27,6 +27,7 @@ # ./demo.sh full run (build, bring up, load, verify) # ./demo.sh --workload-only skip build/bring-up; drive an already-up stack # ./demo.sh --bpftrace also run the kernel-level bpftrace view (sudo) +# ./demo.sh --keep leave the stack up at the end (fast re-runs) # ./demo.sh --no-pause never stop for Enter (unattended) # ./demo.sh --clean tear the stack down and remove the image # ============================================================================ @@ -42,23 +43,38 @@ source "$(cd ../../scripts/lib && pwd)/_helpers.sh" OBS_COMPOSE="$(cd ../../observability && pwd)/compose.yml" COMPOSE=(podman compose -f compose.yml -f "$OBS_COMPOSE") +# Absolute paths for the tutorial dashboard mounts. Relative paths in the +# included observability/compose.yml would resolve against THIS demo's dir +# (the compose project dir), so the dashboards must be passed in as absolutes. +OBS_DIR="$(dirname "$OBS_COMPOSE")" +export OBS_DASHBOARDS_DIR="$OBS_DIR/grafana/dashboards" +export OBS_PROVIDER_FILE="$OBS_DIR/grafana/otel-provisioning/tutorial-dashboards.yaml" + GRAFANA_URL="http://127.0.0.1:3000" SVC_URL="http://127.0.0.1:18401" WORKLOAD_ONLY=0 DO_BPFTRACE=0 DO_CLEAN=0 +DO_STACK_DOWN=0 +KEEP_UP=0 while [[ $# -gt 0 ]]; do case "$1" in --workload-only) WORKLOAD_ONLY=1; shift;; --bpftrace) DO_BPFTRACE=1; shift;; + --keep) KEEP_UP=1; shift;; --no-pause) export DEMO_NO_PAUSE=1; shift;; --clean) DO_CLEAN=1; shift;; + --stack-down) DO_STACK_DOWN=1; shift;; -h|--help) sed -n '2,33p' "$0"; exit 0;; *) log_err "unknown arg: $1"; exit 2;; esac done +# The presentation cockpit can ask us to leave the stack up (DEMO_KEEP_STACK=1) +# so the next run skips bring-up. Same effect as --keep. +[[ "${DEMO_KEEP_STACK:-0}" == "1" ]] && KEEP_UP=1 + if [[ $DO_CLEAN -eq 1 ]]; then "${COMPOSE[@]}" down -v 2>/dev/null || true podman rmi -f cpp-tut/demo-04:latest 2>/dev/null || true @@ -66,6 +82,14 @@ if [[ $DO_CLEAN -eq 1 ]]; then exit 0 fi +# --stack-down: tear down the running stack but KEEP the image (so a re-run +# doesn't recompile). Used by the cockpit to free :3000 for the other stack demo. +if [[ $DO_STACK_DOWN -eq 1 ]]; then + "${COMPOSE[@]}" down -v 2>/dev/null || true + log_ok "Stack down (image kept)." + exit 0 +fi + require podman HAVE_HEY=1; command -v hey >/dev/null 2>&1 || HAVE_HEY=0 HAVE_JQ=1; command -v jq >/dev/null 2>&1 || HAVE_JQ=0 @@ -85,7 +109,12 @@ if [[ $WORKLOAD_ONLY -eq 0 ]]; then demo_step "Build the service and bring up the LGTM stack" callout "First run compiles opentelemetry-cpp from source (~10-20 min)." \ "Later runs hit the podman layer cache (~2-3 min)." - if ! "${COMPOSE[@]}" up -d --build; then + BUILD_FLAG="--build" + if [[ "${DEMO_NO_BUILD:-0}" == "1" ]] && image_exists "cpp-tut/demo-04:latest"; then + log_info "Reusing cpp-tut/demo-04:latest (DEMO_NO_BUILD=1) — starting without --build" + BUILD_FLAG="" + fi + if ! "${COMPOSE[@]}" up -d ${BUILD_FLAG}; then log_err "compose up failed — not going any further (nothing to observe)." "${COMPOSE[@]}" logs --tail=40 demo-04-svc 2>&1 || true exit 1 @@ -121,6 +150,8 @@ callout "Every GET / does three things in ~40 lines of C++:" \ " • starts a span 'handle_request' with a child span 'compute' (TRACE)" \ " • increments demo.requests and records demo.request.duration (METRICS)" \ " • emits a 'request handled' log record (LOGS)" +code_ref "src/main.cpp" 162 "the GET / handler — StartSpan, counter->Add, hist->Record, EmitLogRecord" +code_ref "src/main.cpp" 153 "provider wiring — GetTracer/GetMeter/GetLogger, same as Java/Go" echo printf ' Priming a few requests: ' for _ in 1 2 3 4 5; do curl -sf "$SVC_URL/" >/dev/null 2>&1 && printf '.'; done @@ -247,6 +278,16 @@ log_info " Service: $SVC_URL" # ── Teardown ──────────────────────────────────────────────────────────────── echo -pause "Press Enter to tear down the stack (or Ctrl-C to leave it running)" -"${COMPOSE[@]}" down -v 2>/dev/null || true +if (( KEEP_UP == 1 )); then + log_ok "Stack left running (Grafana $GRAFANA_URL). Tear down with: ./demo.sh --clean" +else + # Under the presentation cockpit, don't invite Ctrl-C — it would SIGINT the + # whole orchestrated run. Tear down quietly and let the cockpit continue. + if [[ "${DEMO_ORCHESTRATED:-0}" == "1" ]]; then + log_info "Orchestrated run — tearing down demo-04 stack and continuing." + else + pause "Explore Grafana now if you like, then press Enter to tear down the stack" + fi + "${COMPOSE[@]}" down -v 2>/dev/null || true +fi log_ok "Demo 04 complete." diff --git a/examples/demo-05-isolation/demo.sh b/examples/demo-05-isolation/demo.sh index 5d801de..76de504 100755 --- a/examples/demo-05-isolation/demo.sh +++ b/examples/demo-05-isolation/demo.sh @@ -84,6 +84,8 @@ callout \ "tenant-b: the 'noisy neighbor' — pegs CPU in a tight loop, no limits" \ "Images: $IMG_A · $IMG_B" \ "Knobs: cpu.weight (bound interference) · cpuset.cpus (dedicate CPUs)" +callout "" "Both tenants are the SAME binary — behaviour differs only by cgroup:" +code_ref "src/main.cpp" 84 "the request-path CPU work + tuned httplib thread pool (twin source)" # ── Step 1: Check the host — cgroup v2 controller delegation ───────────── demo_step "Check the host: cgroup v2 controller delegation" @@ -133,13 +135,17 @@ pause demo_step "Build both tenants" callout "tenant-a is the HTTP service we probe; tenant-b is the CPU/memory" \ "hog. First run adds a Conan + Containerfile build (~2-3 min)." -if ! podman build --target tenant-a -t "$IMG_A" .; then - log_err "tenant-a build failed — cannot measure anything without it." - exit 1 +if should_build "$IMG_A"; then + if ! podman build --target tenant-a -t "$IMG_A" .; then + log_err "tenant-a build failed — cannot measure anything without it." + exit 1 + fi fi -if ! podman build --target tenant-b -t "$IMG_B" .; then - log_err "tenant-b build failed — cannot run the noisy-neighbor scenarios." - exit 1 +if should_build "$IMG_B"; then + if ! podman build --target tenant-b -t "$IMG_B" .; then + log_err "tenant-b build failed — cannot run the noisy-neighbor scenarios." + exit 1 + fi fi # Detect NUMA topology so we can decide whether the 'pinned' scenario diff --git a/examples/demo-06-memory-and-allocators/demo.sh b/examples/demo-06-memory-and-allocators/demo.sh index 160c245..20ccfef 100755 --- a/examples/demo-06-memory-and-allocators/demo.sh +++ b/examples/demo-06-memory-and-allocators/demo.sh @@ -83,16 +83,21 @@ callout \ "Variants: std::allocator · std::pmr (monotonic+sync_pool) · mimalloc" \ "Workload: synthetic JSON-shaped tree builder (many small allocs)" \ "Config: iterations=$ITERATIONS depth=$DEPTH branch=$BRANCH values=$VALUES" +callout "" "One workload, three allocator paths selected at compile time:" +code_ref "src/workload.cpp" 101 "build_node_pmr — the std::pmr path (monotonic arena + sync pool)" +code_ref "src/main.cpp" 4 "compile-time variant select: std::allocator · std::pmr · mimalloc" # ── Step 1: Build the 3-variant image ─────────────────────────────────────── demo_step "Build the 3-variant image" callout "First run compiles all three variants (~3-5 min on a clean cache)." \ "mimalloc's CMake build is fast; cached rebuilds are ~30s (app only)." -if ! podman build -t "$IMAGE" -f Containerfile .; then - log_err "podman build failed — nothing to compare. Stopping here." - exit 1 +if should_build "$IMAGE"; then + if ! podman build -t "$IMAGE" -f Containerfile .; then + log_err "podman build failed — nothing to compare. Stopping here." + exit 1 + fi + log_ok "Image built: $IMAGE" fi -log_ok "Image built: $IMAGE" pause # ── Step 2: Run all three variants back-to-back ───────────────────────────── diff --git a/examples/demo-07-quality-pipeline/demo.sh b/examples/demo-07-quality-pipeline/demo.sh index 63f22f4..9810d23 100755 --- a/examples/demo-07-quality-pipeline/demo.sh +++ b/examples/demo-07-quality-pipeline/demo.sh @@ -121,6 +121,10 @@ callout \ "Service: demo07-svc (links the library)" \ "Stages: analyzer → tests → asan → abi (Containerfile targets)" \ "Reports: reports/ (pulled from each image, for CI gating)" +callout "" "The ABI-bearing library and its test are the code to open:" +code_ref "src/include/demo07/channel.hpp" 27 "VirtualChannel (vtable ABI) vs StaticChannel (CRTP) — what abidiff watches" +code_ref "src/lib/channel.cpp" 1 "the compiled library body" +code_ref "tests/test_channel.cpp" 1 "the unit test the 'tests' stage gates on" # ── --abi-bless: promote reports/current.abi to abi-reference/ ────────────── # This is the operational counterpart to --abi-only. After running --abi-only diff --git a/examples/demo.sh b/examples/demo.sh new file mode 100755 index 0000000..51861d6 --- /dev/null +++ b/examples/demo.sh @@ -0,0 +1,224 @@ +#!/usr/bin/env bash +# ============================================================================ +# Presentation driver — run all seven demos from one terminal. +# +# Lives in examples/ and drives the per-demo scripts beside it. This is the +# on-stage cockpit. It does NOT reimplement any demo; it runs each demo's own +# examples/demo-0X-*/demo.sh with the TTY inherited, so every per-demo pause / +# callout / code_ref still works exactly as when run standalone. The seven +# scripts remain fully usable on their own. +# +# Run from the examples/ directory: ./demo.sh (or examples/demo.sh) +# +# Order follows the DECK, not the directory numbers: +# Demo 1 → 2 → 6 → 3 → 4 → 5 → 7 +# (Demos 3 and 4 each bring up the shared LGTM stack on http://127.0.0.1:3000.) +# +# Usage: +# ./demo.sh interactive menu (pick a demo, or run all) +# ./demo.sh --all run every demo in deck order, pausing between +# ./demo.sh --no-pause never stop for Enter (unattended / auto-test) +# ./demo.sh --ide=clion open every code_ref in CLion as it's cued +# ./demo.sh --rebuild force rebuilds (default reuses existing images) +# ./demo.sh --keep-stack leave the LGTM stack up for fast re-runs (demos 3/4) +# ./demo.sh --clean run each demo's own --clean, then exit +# ./demo.sh -h this help +# +# By default already-built images are REUSED (fast on stage); the stack demos +# (3, 4) tear their stacks down between steps without inviting Ctrl-C. With +# --keep-stack (or 'k' in the menu) a stack demo leaves its stack running so +# the next run of it is instant; the cockpit frees the OTHER stack demo first +# since demos 3 and 4 both bind Grafana on :3000. +# +# Env: +# DEMO_IDE=clion same as --ide=clion (code_ref opens in CLion) +# DEMO_NO_BUILD=1 reuse existing images (set by default here) +# DEMO_KEEP_STACK=1 same as --keep-stack +# ============================================================================ + +set -euo pipefail + +# This script lives in examples/ and drives the per-demo demo.sh scripts that +# sit beside it (examples/demo-0X-*/demo.sh). Paths below are relative to here. +EXAMPLES_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SELF="$EXAMPLES_DIR/$(basename "${BASH_SOURCE[0]}")" # absolute path to this script (used by -h after cd) +cd "$EXAMPLES_DIR" + +# shellcheck source=../scripts/lib/_helpers.sh +source "$EXAMPLES_DIR/../scripts/lib/_helpers.sh" + +# Listed in DECK order; the selector you type is the canonical Demo number +# (so "Demo 4 — OTel" in the deck is what you press), which is field 1. +# Fields: num | dir | short name | tagline | needs-LGTM-stack(1/0) +DEMOS=( + "1|demo-01-image-strategy|Image Strategy|UBI vs ubi-micro vs scratch + multi-stage + PGO|0" + "2|demo-02-stl-layout|STL Layout Under Pressure|data-structure layout & cache behaviour|0" + "6|demo-06-memory-and-allocators|Memory Management & Allocators|PMR, huge pages, cgroups v2, OOM|0" + "3|demo-03-io-uring-grpc|io_uring + Async gRPC|async I/O + gRPC on the LGTM stack|1" + "4|demo-04-observability|OTel Observability Stack|one C++ binary → traces/metrics/logs|1" + "5|demo-05-isolation|Noisy-Neighbor Isolation|cgroup cpu.weight, the noisy neighbour|0" + "7|demo-07-quality-pipeline|Quality Pipeline|analyzer, ASan, ABI, coverage gates|0" +) + +DO_ALL=0 +DO_CLEAN=0 +# Default to reusing already-built images (skip build when the image exists). +# Pass --rebuild to force every demo to rebuild. +export DEMO_NO_BUILD=1 +while [[ $# -gt 0 ]]; do + case "$1" in + --all) DO_ALL=1; shift;; + --no-pause) export DEMO_NO_PAUSE=1; shift;; + --ide=*) export DEMO_IDE="${1#*=}"; shift;; + --ide) export DEMO_IDE="$2"; shift 2;; + --rebuild) export DEMO_NO_BUILD=0; shift;; + --keep-stack) export DEMO_KEEP_STACK=1; shift;; + --clean) DO_CLEAN=1; shift;; + -h|--help) sed -n '2,37p' "$SELF"; exit 0;; + *) log_err "unknown arg: $1"; exit 2;; + esac +done + +require podman +for c in hey jq curl; do + command -v "$c" >/dev/null 2>&1 || log_warn "'$c' not on PATH — some demos degrade without it" +done + +# Look up a demo row by its canonical Demo number; echoes the pipe-row or fails. +demo_row_by_num() { + local want="$1" row + for row in "${DEMOS[@]}"; do + [[ "${row%%|*}" == "$want" ]] && { printf '%s' "$row"; return 0; } + done + return 1 +} + +# free_other_stack — demos 3 and 4 each bind Grafana on :3000, +# so only one stack can be up at a time. Before starting a stack demo while +# keep-stack is on, tear down the OTHER stack demo so its ports are free. +free_other_stack() { + local keep_dir="$1" row dir stack + for row in "${DEMOS[@]}"; do + IFS='|' read -r _ dir _ _ stack <<<"$row" + if [[ "$stack" == "1" && "$dir" != "$keep_dir" ]]; then + log_info "keep-stack: freeing ports from $dir before starting the next stack demo" + # --stack-down tears the stack down but KEEPS the image (unlike --clean, + # which removes it and would force a slow recompile on the next run). + ( cd "$dir" && ./demo.sh --stack-down ) >/dev/null 2>&1 || true + fi + done +} + +run_one() { # run_one + local row + row="$(demo_row_by_num "$1")" || { log_warn "no Demo #$1"; return 1; } + local num dir name tag stack + IFS='|' read -r num dir name tag stack <<<"$row" + + hr + printf '%s▶ Demo %s: %s%s\n' "$C_BOLD$C_YELLOW" "$num" "$name" "$C_RESET" + callout "$tag" "path: examples/$dir/" + if [[ "$stack" == "1" ]]; then + callout "" "This demo brings up the shared LGTM stack:" \ + " Grafana http://127.0.0.1:3000 · Tempo :3200 · Prometheus :9090 · Loki :3100" + fi + hr + + if [[ ! -x "$dir/demo.sh" ]]; then + log_err "$dir/demo.sh not found or not executable — skipping." + return 1 + fi + + # With keep-stack on, ensure the other stack demo isn't holding :3000. + if [[ "${DEMO_KEEP_STACK:-0}" == "1" && "$stack" == "1" ]]; then + free_other_stack "$dir" + fi + + # Run in a subshell so a demo's `cd`/traps/`set -e` can't leak back here. + # TTY is inherited, so the demo's own pause/callout/code_ref work as usual. + # A non-zero exit is reported but does NOT abort the whole session. + ( cd "$dir" && ./demo.sh ) || log_warn "Demo '$name' exited non-zero (rc=$?) — continuing." +} + +clean_all() { + local row num dir name + for row in "${DEMOS[@]}"; do + IFS='|' read -r num dir name _ _ <<<"$row" + log_step "Cleaning Demo $num — $name" + ( cd "$dir" && ./demo.sh --clean ) || log_warn "clean of '$name' returned non-zero — continuing." + done + log_ok "All demos cleaned." +} + +run_all() { + banner \ + "C++20/23 Performance Under Container Constraints — full demo run" \ + "Seven demos in DECK order: 1 → 2 → 6 → 3 → 4 → 5 → 7" \ + "(deck order, not numeric — the numbers jump on purpose to match the talk)." + # DEMO_ORCHESTRATED tells the stack demos (3, 4) to tear down quietly and + # continue instead of prompting 'Press Enter / Ctrl-C to leave running' — + # a Ctrl-C there would kill this whole run. + export DEMO_ORCHESTRATED=1 + local i num next_num next_name + for i in "${!DEMOS[@]}"; do + num="${DEMOS[$i]%%|*}" + run_one "$num" + if (( i < ${#DEMOS[@]} - 1 )); then + IFS='|' read -r next_num _ next_name _ _ <<<"${DEMOS[$((i + 1))]}" + pause "Demo $num done. Next in the deck → Demo $next_num ($next_name). Press Enter" + fi + done + unset DEMO_ORCHESTRATED + log_ok "All seven demos complete." +} + +menu() { + while :; do + banner \ + "C++ Container Optimization — presentation cockpit" \ + "Pick a demo to run, or 'a' for all in deck order." + local row num name tag stack marker + for row in "${DEMOS[@]}"; do + IFS='|' read -r num _ name tag stack <<<"$row" + marker=""; [[ "$stack" == "1" ]] && marker=" ${C_DIM}[LGTM stack]${C_RESET}" + printf ' %sDemo %s%s) %-32s %s%s%s%b\n' \ + "$C_BOLD$C_GREEN" "$num" "$C_RESET" "$name" "$C_DIM" "$tag" "$C_RESET" "$marker" + done + printf ' %sa%s) run ALL in deck order\n' "$C_BOLD$C_GREEN" "$C_RESET" + local keep_state; [[ "${DEMO_KEEP_STACK:-0}" == "1" ]] && keep_state="ON" || keep_state="off" + printf ' %sk%s) toggle keep-stack (leave LGTM up for fast re-runs) — now: %s%s%s\n' \ + "$C_BOLD$C_GREEN" "$C_RESET" "$C_BOLD" "$keep_state" "$C_RESET" + printf ' %sc%s) clean every demo (images/stacks)\n' "$C_BOLD$C_GREEN" "$C_RESET" + printf ' %sq%s) quit\n' "$C_BOLD$C_GREEN" "$C_RESET" + [[ -n "${DEMO_IDE:-}" ]] && callout "" "DEMO_IDE=$DEMO_IDE — code_ref callouts will open in the IDE." + printf '\n %s⏎ choice:%s ' "$C_BOLD$C_BLUE" "$C_RESET" + local choice; read -r choice || { echo; break; } + case "$choice" in + [1-7]) + if demo_row_by_num "$choice" >/dev/null; then + run_one "$choice"; pause "Back to the menu — press Enter" + else + log_warn "no Demo #$choice" + fi;; + a|A) run_all; pause "Back to the menu — press Enter";; + k|K) + if [[ "${DEMO_KEEP_STACK:-0}" == "1" ]]; then + unset DEMO_KEEP_STACK; log_info "keep-stack OFF — stacks tear down after each demo." + else + export DEMO_KEEP_STACK=1; log_info "keep-stack ON — stack demos leave the LGTM stack running." + fi;; + c|C) clean_all; pause "Back to the menu — press Enter";; + q|Q|"") break;; + *) log_warn "unrecognised choice: $choice";; + esac + done + log_ok "Bye." +} + +if [[ $DO_CLEAN -eq 1 ]]; then + clean_all +elif [[ $DO_ALL -eq 1 ]]; then + run_all +else + menu +fi diff --git a/observability/compose.yml b/observability/compose.yml index 2fa9d9f..c806557 100644 --- a/observability/compose.yml +++ b/observability/compose.yml @@ -48,10 +48,17 @@ services: tmpfs: - /data volumes: - # Optional: drop pre-built dashboards into this directory and - # they'll show up under the 'Tutorial' folder in Grafana on - # next start. Empty for now; demo 4 mounts its own. - - ./grafana/dashboards:/otel-lgtm/grafana/conf/provisioning/dashboards/tutorial:ro,Z + # Pre-built dashboards → the 'Tutorial' folder in Grafana, plus the + # provider yaml that actually tells Grafana to load them. + # + # These paths are absolute via env vars because relative paths in a + # compose file resolve against the PROJECT directory — which, when this + # file is included from a demo (podman compose -f -f ), is + # the DEMO's directory, not observability/. The demos export + # OBS_DASHBOARDS_DIR / OBS_PROVIDER_FILE to absolute paths; the defaults + # below are correct for a standalone `compose -f observability/compose.yml`. + - ${OBS_DASHBOARDS_DIR:-./grafana/dashboards}:/otel-lgtm/grafana/conf/provisioning/dashboards/tutorial:ro,Z + - ${OBS_PROVIDER_FILE:-./grafana/otel-provisioning/tutorial-dashboards.yaml}:/otel-lgtm/grafana/conf/provisioning/dashboards/tutorial-dashboards.yaml:ro,Z networks: - obs diff --git a/observability/grafana/dashboards/demo-03-io-uring-grpc.json b/observability/grafana/dashboards/demo-03-io-uring-grpc.json new file mode 100644 index 0000000..fa695b2 --- /dev/null +++ b/observability/grafana/dashboards/demo-03-io-uring-grpc.json @@ -0,0 +1,78 @@ +{ + "annotations": { "list": [] }, + "editable": false, + "graphTooltip": 0, + "schemaVersion": 39, + "title": "Demo 03 — io_uring + async gRPC", + "uid": "tutorial-demo-03", + "tags": ["tutorial", "demo-03"], + "timezone": "browser", + "time": { "from": "now-15m", "to": "now" }, + "refresh": "10s", + "panels": [ + { + "id": 1, + "type": "stat", + "title": "gRPC request rate", + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "gridPos": { "x": 0, "y": 0, "w": 6, "h": 6 }, + "targets": [ + { + "expr": "sum(rate(demo3_grpc_requests_total[1m]))", + "legendFormat": "req/s", + "refId": "A" + } + ] + }, + { + "id": 2, + "type": "timeseries", + "title": "gRPC Echo latency p50/p95/p99 (ms)", + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "gridPos": { "x": 6, "y": 0, "w": 18, "h": 6 }, + "targets": [ + { "expr": "histogram_quantile(0.50, sum(rate(demo3_grpc_latency_milliseconds_bucket[1m])) by (le))", "legendFormat": "p50", "refId": "A" }, + { "expr": "histogram_quantile(0.95, sum(rate(demo3_grpc_latency_milliseconds_bucket[1m])) by (le))", "legendFormat": "p95", "refId": "B" }, + { "expr": "histogram_quantile(0.99, sum(rate(demo3_grpc_latency_milliseconds_bucket[1m])) by (le))", "legendFormat": "p99", "refId": "C" } + ] + }, + { + "id": 3, + "type": "timeseries", + "title": "TCP echo connections/s — io_uring direct vs Asio", + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "gridPos": { "x": 0, "y": 6, "w": 12, "h": 8 }, + "targets": [ + { "expr": "sum(rate(demo3_tcp_iouring_connections_total[1m]))", "legendFormat": "io_uring direct (:9000)", "refId": "A" }, + { "expr": "sum(rate(demo3_tcp_asio_connections_total[1m]))", "legendFormat": "Asio io_uring (:9001)", "refId": "B" } + ] + }, + { + "id": 4, + "type": "table", + "title": "Recent gRPC traces (Tempo)", + "datasource": { "type": "tempo", "uid": "tempo" }, + "gridPos": { "x": 12, "y": 6, "w": 12, "h": 8 }, + "targets": [ + { + "queryType": "traceqlSearch", + "query": "{ resource.service.name=\"demo-03-svc\" }", + "refId": "A" + } + ] + }, + { + "id": 5, + "type": "logs", + "title": "Service logs (Loki)", + "datasource": { "type": "loki", "uid": "loki" }, + "gridPos": { "x": 0, "y": 14, "w": 24, "h": 8 }, + "targets": [ + { + "expr": "{service_name=\"demo-03-svc\"}", + "refId": "A" + } + ] + } + ] +} diff --git a/observability/grafana/otel-provisioning/tutorial-dashboards.yaml b/observability/grafana/otel-provisioning/tutorial-dashboards.yaml new file mode 100644 index 0000000..d6e3f9f --- /dev/null +++ b/observability/grafana/otel-provisioning/tutorial-dashboards.yaml @@ -0,0 +1,19 @@ +# Grafana dashboard provider for the tutorial dashboards. +# +# Mounted into the otel-lgtm bundle's Grafana provisioning directory +# (/otel-lgtm/grafana/conf/provisioning/dashboards/) so Grafana loads every +# *.json under the sibling `tutorial/` directory into a "Tutorial" folder at +# startup. Without this file the JSONs are just sitting on disk — Grafana +# only provisions dashboards a provider points at. +apiVersion: 1 + +providers: + - name: 'tutorial' + orgId: 1 + folder: 'Tutorial' + type: file + disableDeletion: true + allowUiUpdates: false + options: + path: /otel-lgtm/grafana/conf/provisioning/dashboards/tutorial + foldersFromFilesStructure: false diff --git a/scripts/lib/_helpers.sh b/scripts/lib/_helpers.sh index 4b57f38..47c5e93 100755 --- a/scripts/lib/_helpers.sh +++ b/scripts/lib/_helpers.sh @@ -134,6 +134,67 @@ callout() { for line in "$@"; do printf ' %s%s%s\n' "$C_DIM" "$line" "$C_RESET"; done } +# ── Build-skip helpers ────────────────────────────────────────────────── +# For live presentations: when images are already built, rebuilding (even +# with a warm layer cache) wastes stage time. The presentation cockpit sets +# DEMO_NO_BUILD=1; each demo guards its build step with should_build so a +# missing image still builds, but an existing one is reused. + +# image_exists — true if a local image with this tag exists. +image_exists() { podman image exists "$1" 2>/dev/null; } + +# should_build — return non-zero (skip) when DEMO_NO_BUILD=1 AND the +# image already exists; zero (build) otherwise. Usage: +# if should_build "$IMAGE"; then podman build ... ; fi +should_build() { + if [[ "${DEMO_NO_BUILD:-0}" == "1" ]] && image_exists "$1"; then + log_info "Reusing existing image $1 (DEMO_NO_BUILD=1) — skipping build" + return 1 + fi + return 0 +} + +# ── Code-examination helpers ──────────────────────────────────────────── +# The recurring "now flip to the IDE and look at THIS code" moment. Always +# prints a clickable file:line. When DEMO_IDE=clion is set, ALSO opens the +# file at the line in CLion via the JetBrains Toolbox launcher — opt-in so +# standalone / CI runs stay quiet. Path is resolved to absolute against the +# caller's $PWD (demo.sh scripts cd into their own dir first) so CLion opens +# the right file regardless of where the demo was launched from. +DEMO_IDE="${DEMO_IDE:-}" + +# _clion_bin — echo a runnable CLion launcher, or return non-zero. +_clion_bin() { + if command -v clion >/dev/null 2>&1; then command -v clion; return 0; fi + local t="$HOME/.local/share/JetBrains/Toolbox/scripts/clion" + if [[ -x "$t" ]]; then printf '%s' "$t"; return 0; fi + return 1 +} + +# code_ref [line] ["caption"] — "examine this in the IDE" callout. +code_ref() { + local path="$1" line="${2:-}" caption="${3:-}" + local abs="$path" + [[ "$abs" != /* ]] && abs="$PWD/$path" + local disp="$path"; [[ -n "$line" ]] && disp="$path:$line" + printf ' %s▸ Code:%s %s%s%s' "$C_BOLD$C_GREEN" "$C_RESET" "$C_GREEN" "$disp" "$C_RESET" + [[ -n "$caption" ]] && printf ' %s— %s%s' "$C_DIM" "$caption" "$C_RESET" + printf '\n' + if [[ "$DEMO_IDE" == "clion" ]]; then + local bin + if bin="$(_clion_bin)"; then + if [[ -n "$line" ]]; then + "$bin" --line "$line" "$abs" >/dev/null 2>&1 & + else + "$bin" "$abs" >/dev/null 2>&1 & + fi + disown 2>/dev/null || true + else + log_warn "DEMO_IDE=clion but no CLion launcher found (skipping open)" + fi + fi +} + # grafana_callout "Dashboard name" "panel/thing to look at" ... # The recurring "now switch to Grafana and look at X" moment. grafana_callout() {