Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
239 changes: 239 additions & 0 deletions .github/workflows/demo.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
# .github/workflows/demo.yml — the demo harness gates.
#
# Two jobs with very different costs, on purpose.
#
# demo-render every push and PR, ~10 s, no Postgres, no browser, no Docker.
# Re-renders the committed demos/fixtures/*.ansi and compares
# byte-for-byte against demos/fixtures/expected/*.svg. Because
# the SVG renderer is deterministic this is a genuine gate, not
# a smoke test — but it proves only that the RENDERER still
# works on frozen input. It cannot notice that pg_ash's output
# changed shape underneath it. Only demo-refresh catches that.
#
# demo-refresh nightly, on demand, and whenever demos/** or sql/** changes.
# ~3 min. Seeds a real PostgreSQL, re-captures every scene, and
# records the reel. `make -C demos rot` compares the fresh
# reader contract with fixtures/shape.tsv: a changed capture
# shape means a reader was renamed, a column moved, or the
# installer path shifted. That is the check that would have
# caught the demo pointing at an installer path the release
# stamp had emptied, and the landing page advertising a reader
# 2.0 removed — both of which shipped.
#
# ZERO container-registry traffic. The previous harness required a Docker Hub
# pull, and `auth.docker.io` returns 503 in sandboxed runners, which is the root
# cause of "I recorded it on my laptop". ubuntu-latest already ships a
# PostgreSQL cluster and postgresql-client; we start that. No `services:` block.
#
# Fonts are vendored under demos/fonts and consumed BY PATH (agg --font-dir,
# ansi2svg --font). No fontconfig, no `apt install fonts-*`, no Homebrew cask.
# A missing font is a hard exit 6, never a silent fallback into a serif face.

name: demo

on:
push:
pull_request:
schedule:
# 04:20 UTC nightly — after the test matrix, before anyone is awake to be
# surprised by a stale GIF.
- cron: '20 4 * * *'
workflow_dispatch:

permissions:
contents: read

concurrency:
group: demo-${{ github.ref }}
cancel-in-progress: true

jobs:
# =========================================================================
# Job A — the fast gate. Runs on everything.
# =========================================================================
demo-render:
name: renderer (fixtures, no database)
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v6

- uses: actions/setup-python@v6
with:
python-version: '3.12'

- name: Install the tier-1 renderer dependencies
# fonttools + brotli are all `make check` needs. Deliberately NOT
# installing Postgres, tmux, ffmpeg or a browser here: if this job ever
# needs them, the "it only runs on my laptop" problem has come back.
run: python -m pip install --quiet fonttools brotli

- name: Assert the vendored font is present and is JetBrains Mono
run: |
python - <<'PY'
from fontTools.ttLib import TTFont
import sys
for p in ("demos/fonts/JetBrainsMono-Regular.ttf",
"demos/fonts/JetBrainsMono-Bold.ttf"):
f = TTFont(p, lazy=True)
fam = f["name"].getDebugName(1) or ""
assert "JetBrains Mono" in fam, (p, fam)
# ash.chart() draws with these; a face without them renders tofu.
cmap = f.getBestCmap()
for cp in (0x2588, 0x2593, 0x2591, 0x2592, 0x00B7):
assert cp in cmap, (p, hex(cp))
print("vendored font OK")
PY

- name: No reader removed in 2.0 may be referenced under demos/
# Assembled from fragments so this workflow file does not itself trip
# the grep it is running.
run: |
set -Eeuo pipefail
RE="top_$(printf 'waits')|query_$(printf 'waits')|top_by_$(printf 'type')|timeline_$(printf 'chart')"
if grep -rIEl "$RE" demos/ > /tmp/hits 2>/dev/null; then
echo "demos/ references a reader that 2.0 removed:" >&2
cat /tmp/hits >&2
exit 1
fi
echo "no removed-reader references"

- name: make check — re-render fixtures and compare byte-for-byte
run: ASH_SVG_ONLY=1 make -C demos check

# =========================================================================
# Job B — the real gate. Nightly, on demand, or when the demo/SQL changes.
# =========================================================================
demo-refresh:
name: capture + reel (real PostgreSQL)
runs-on: ubuntu-latest
timeout-minutes: 25
if: >-
github.event_name == 'schedule' ||
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'push' && github.ref == 'refs/heads/main') ||
github.event_name == 'pull_request'
steps:
- uses: actions/checkout@v6

- uses: actions/setup-python@v6
with:
python-version: '3.12'

- name: Decide whether this run needs the full pass
# On push/PR, only when demos/** or sql/** actually changed. Schedules
# and manual dispatches always run.
id: gate
run: |
set -Eeuo pipefail
if [ "${{ github.event_name }}" = "schedule" ] \
|| [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "run=yes" >> "$GITHUB_OUTPUT"; exit 0
fi
base="${{ github.event.pull_request.base.sha || github.event.before }}"
if [ -z "$base" ] || [ "$base" = "0000000000000000000000000000000000000000" ]; then
echo "run=yes" >> "$GITHUB_OUTPUT"; exit 0
fi
# A shallow checkout need not contain a merge base. Fetch the exact
# base tree and use a two-tree diff; any fetch/diff error must fail
# this step, never turn the expensive gate into a false green.
git fetch --no-tags --depth=1 origin "$base"
changed="$(git diff --name-only "$base" HEAD -- demos sql)"
if [ -n "$changed" ]; then echo "run=yes" >> "$GITHUB_OUTPUT"
else echo "run=no" >> "$GITHUB_OUTPUT"; fi

- name: Start the runner's own PostgreSQL cluster
if: steps.gate.outputs.run == 'yes'
# ubuntu-latest ships PostgreSQL and postgresql-client already. Starting
# the preinstalled cluster costs about two seconds and ZERO registry
# traffic, which is the whole point.
run: |
set -Eeuo pipefail
sudo systemctl start postgresql.service
for _ in $(seq 1 30); do pg_isready -q && break; sleep 1; done
pg_isready
# Ubuntu's local socket uses peer authentication. Give the runner's
# actual OS user a matching PostgreSQL role, then publish that
# connection identity to all later steps.
ci_role="$(id -un)"
sudo -u postgres createuser --superuser "$ci_role"
# pg_stat_statements gives ash.top('query_id') its query text. pg_cron
# is deliberately NOT installed: the harness drives sampling itself,
# which is both the portable path and the honest one for the RDS /
# Cloud SQL / Supabase / Neon users who cannot have pg_cron either.
sudo -u postgres psql -v ON_ERROR_STOP=1 -c \
"alter system set shared_preload_libraries = 'pg_stat_statements'"
sudo systemctl restart postgresql.service
for _ in $(seq 1 30); do pg_isready -q && break; sleep 1; done
pg_isready
PGHOST=/var/run/postgresql PGUSER="$ci_role" \
psql -v ON_ERROR_STOP=1 -d postgres -Atqc \
"select current_user" | grep -Fx "$ci_role"
{
echo "PGHOST=/var/run/postgresql"
echo "PGUSER=$ci_role"
} >> "$GITHUB_ENV"

- name: Install the tier-3 dependencies
if: steps.gate.outputs.run == 'yes'
run: |
set -Eeuo pipefail
sudo apt-get update -qq
sudo apt-get install -y -qq tmux ffmpeg gifsicle postgresql-client
python -m pip install --quiet asciinema pillow fonttools brotli
# agg ships a single release binary. No cargo build, no Docker Hub,
# no registry auth. Pin its digest so a changed download fails loud.
AGG_VER="1.7.0"
AGG_SHA256="b72c773c22ef73149540f5728b37290dffe88418f0a16ceab29bc8e43699abf7"
agg_tmp="$RUNNER_TEMP/agg"
curl -fsSL -o "$agg_tmp" \
"https://github.com/asciinema/agg/releases/download/v${AGG_VER}/agg-x86_64-unknown-linux-gnu"
printf '%s %s\n' "$AGG_SHA256" "$agg_tmp" | sha256sum -c -
sudo install -m 0755 "$agg_tmp" /usr/local/bin/agg
agg --version

- name: make doctor
if: steps.gate.outputs.run == 'yes'
run: make -C demos doctor

- name: make all — seed, capture, stills, reel
if: steps.gate.outputs.run == 'yes'
env:
ASH_BACKEND: local
run: make -C demos all

- name: Demo-rot alarm
if: steps.gate.outputs.run == 'yes'
# `make all` has just re-captured every scene against a real database,
# so demos/out/raw holds today's bytes. This compares the READER
# CONTRACT in those bytes -- column names, column count, report keys,
# the fixed label vocabularies -- against demos/fixtures/shape.tsv.
#
# NOT `git diff --exit-code demos/fixtures`, which is the obvious thing
# and does not work: `make all` does not write fixtures/ at all, so that
# diff is always clean and the alarm never fires. And if it were made to
# write them, every number in a capture is a real measurement, so the
# diff would be red on every single run instead -- a check that is always
# red is a check nobody reads. See demos/render/scene_shape.py.
run: make -C demos rot

- name: Upload the artifacts a human can eyeball
if: steps.gate.outputs.run == 'yes' && always()
uses: actions/upload-artifact@v6
with:
name: demo-assets
if-no-files-found: warn
retention-days: 14
path: |
assets/*.svg
assets/*.png
assets/ash_demo.gif
assets/ash_demo.mp4
demos/out/transcript.log
demos/out/stderr.log
demos/out/window.env
demos/out/shots/*.ansi

- name: Teardown
if: always() && steps.gate.outputs.run == 'yes'
run: make -C demos down
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,25 @@ from ash.chart(
);
```

<img src="assets/chart.svg" alt="ash.chart() rendering Average Active Sessions per minute, stacked by wait event, in 24-bit color">

`ash.chart()` — Average Active Sessions per minute, stacked by wait event, in
24-bit color straight out of `psql`. The glyph varies per series (`█ ▓ ░ ▒ ·`)
as well as the color, so the ranking still reads correctly for colorblind
viewers and in a monochrome terminal.

<img src="assets/top_event.svg" alt="ash.top('wait_event') ranking the wait-event breakdown for an incident window by Average Active Sessions">

`ash.top('wait_event')` — the wait-event breakdown for the incident window,
ranked by Average Active Sessions, with the share of total active time. When
`Lock:transactionid` dominates, the database is not short of capacity — writers
are queueing behind one another's row locks.

![pg_ash 2.0 investigation demo](assets/ash_demo.gif)

Every image above is real `ash.*` output over real samples, regenerated with
`make -C demos all`. See [demos/README.md](demos/README.md).

For the latest stable v1.5 tag, check out `v1.5` first and use:

```sql
Expand Down
Binary file added assets/ash_demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/ash_demo.mp4
Binary file not shown.
Binary file added assets/chart.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions assets/chart.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/compare.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions assets/compare.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/periods.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions assets/periods.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/report.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions assets/report.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/status.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions assets/status.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/summary.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions assets/summary.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/top_event.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions assets/top_event.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/top_query.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions assets/top_query.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions demos/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# demos/ working directory: captures, frames, casts, logs, window.env.
# Everything the harness produces that is NOT a committed deliverable.
# The deliverables live in ../assets/ and in demos/fixtures/.
out/

# Local, machine-specific overrides (paths to a repo copy, a scratch asset dir).
# Absent in a normal checkout; see lib/env.sh.
env.local

# Left behind by `make check` when the renderer drifts, for eyeballing.
*.drift.svg

# Renderer imports during `make check`.
__pycache__/
32 changes: 0 additions & 32 deletions demos/Dockerfile

This file was deleted.

Loading