Interactive envelope plot via WebAgg (browser-based)
plot_envelope.py grows a --show flag. With it, matplotlib's WebAgg backend spins up a localhost server with a real interactive figure (zoom, pan, hover, axes lock) — equivalent to a matlab plot window without needing tkinter or Qt locally. tools/show_envelope.sh is a NixOS-aware wrapper: it locates libstdc++.so.6 in /nix/store (numpy's prebuilt wheel needs it on LD_LIBRARY_PATH) and then exec's the python script with --show. Default port 8988, override via --port. Bound to 0.0.0.0 so the figure is reachable over WG too. tornado is added to dev deps because WebAgg requires it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
69c09f4404
commit
cc37fc6c4d
4 changed files with 93 additions and 8 deletions
|
|
@ -14,6 +14,7 @@ dev = [
|
|||
"pytest-asyncio>=0.23",
|
||||
"httpx>=0.27",
|
||||
"matplotlib>=3.8",
|
||||
"tornado>=6", # required by matplotlib's WebAgg interactive backend
|
||||
]
|
||||
|
||||
[tool.uv]
|
||||
|
|
|
|||
|
|
@ -1,8 +1,15 @@
|
|||
"""Plot a single episode's envelope.
|
||||
|
||||
Reads ``telemetry-proc.jsonl`` and ``labels.jsonl`` from an episode directory
|
||||
and renders a 3-panel PNG: CPU%, RSS, IO write rate, with phase bands
|
||||
underneath. Saves to ``<episode_dir>/envelope.png``.
|
||||
and renders a 3-panel chart: CPU%, RSS, IO write rate, with phase bands
|
||||
underneath.
|
||||
|
||||
Two modes:
|
||||
|
||||
- Default: render to ``<episode_dir>/envelope.png``.
|
||||
- ``--show``: serve interactively via matplotlib's WebAgg backend
|
||||
(zoom/pan/hover in the browser). On NixOS, run via
|
||||
``tools/show_envelope.sh`` so libstdc++ is on LD_LIBRARY_PATH.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -13,12 +20,6 @@ import os
|
|||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import matplotlib
|
||||
|
||||
matplotlib.use("Agg")
|
||||
import matplotlib.pyplot as plt
|
||||
from matplotlib.patches import Patch
|
||||
|
||||
|
||||
PHASE_COLORS = {
|
||||
"clean": "#9bd09b",
|
||||
|
|
@ -38,8 +39,33 @@ def main() -> int:
|
|||
parser = argparse.ArgumentParser(prog="plot_envelope")
|
||||
parser.add_argument("episode_dir", type=Path)
|
||||
parser.add_argument("--out", type=Path, default=None)
|
||||
parser.add_argument(
|
||||
"--show",
|
||||
action="store_true",
|
||||
help="open an interactive plot in your browser via WebAgg "
|
||||
"(localhost server)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--port",
|
||||
type=int,
|
||||
default=8988,
|
||||
help="port for the WebAgg server (default 8988)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
# Pick backend BEFORE importing pyplot.
|
||||
import matplotlib
|
||||
if args.show:
|
||||
matplotlib.use("WebAgg")
|
||||
# Bind to all interfaces so it works over the WG overlay too.
|
||||
matplotlib.rcParams["webagg.address"] = "0.0.0.0"
|
||||
matplotlib.rcParams["webagg.port"] = args.port
|
||||
matplotlib.rcParams["webagg.open_in_browser"] = True
|
||||
else:
|
||||
matplotlib.use("Agg")
|
||||
import matplotlib.pyplot as plt
|
||||
from matplotlib.patches import Patch
|
||||
|
||||
d: Path = args.episode_dir
|
||||
if not d.exists():
|
||||
print(f"no such directory: {d}", file=sys.stderr)
|
||||
|
|
@ -120,6 +146,13 @@ def main() -> int:
|
|||
)
|
||||
fig.tight_layout()
|
||||
|
||||
if args.show:
|
||||
print(f"WebAgg interactive plot starting on port {args.port}...")
|
||||
print(f"open: http://127.0.0.1:{args.port}/")
|
||||
print("(ctrl-C in this terminal to stop the server)")
|
||||
plt.show()
|
||||
return 0
|
||||
|
||||
out = args.out or (d / "envelope.png")
|
||||
fig.savefig(out, dpi=120)
|
||||
print(f"wrote {out}")
|
||||
|
|
|
|||
32
tools/show_envelope.sh
Executable file
32
tools/show_envelope.sh
Executable file
|
|
@ -0,0 +1,32 @@
|
|||
#!/usr/bin/env bash
|
||||
# Open an interactive envelope plot in your browser (matplotlib WebAgg).
|
||||
#
|
||||
# On NixOS, the prebuilt numpy wheel needs libstdc++ from gcc-lib on
|
||||
# LD_LIBRARY_PATH; this wrapper finds it automatically.
|
||||
#
|
||||
# Usage:
|
||||
# tools/show_envelope.sh <episode_dir> [--port 8988]
|
||||
#
|
||||
# Then open the printed URL (or your browser may pop open automatically).
|
||||
# Ctrl-C in this terminal to stop the server.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
# Locate libstdc++.so.6 in /nix/store. Prefer the gcc-lib derivation.
|
||||
LIBSTDC=$(find /nix/store -maxdepth 4 -name 'libstdc++.so.6' -path '*gcc*-lib/lib/*' 2>/dev/null | head -1)
|
||||
if [[ -z "${LIBSTDC:-}" ]]; then
|
||||
# Fall back to any libstdc++ in /nix/store.
|
||||
LIBSTDC=$(find /nix/store -maxdepth 4 -name 'libstdc++.so.6' 2>/dev/null | head -1)
|
||||
fi
|
||||
if [[ -z "${LIBSTDC:-}" ]]; then
|
||||
echo "could not locate libstdc++.so.6 in /nix/store" >&2
|
||||
echo "(numpy needs it; install via 'nix-shell -p stdenv.cc.cc.lib' or set LD_LIBRARY_PATH manually)" >&2
|
||||
exit 1
|
||||
fi
|
||||
LIB_DIR="$(dirname "$LIBSTDC")"
|
||||
|
||||
LD_LIBRARY_PATH="$LIB_DIR${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" \
|
||||
exec uv run python tools/plot_envelope.py --show "$@"
|
||||
19
uv.lock
generated
19
uv.lock
generated
|
|
@ -39,6 +39,7 @@ dev = [
|
|||
{ name = "matplotlib" },
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-asyncio" },
|
||||
{ name = "tornado" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
|
|
@ -53,6 +54,7 @@ dev = [
|
|||
{ name = "matplotlib", specifier = ">=3.8" },
|
||||
{ name = "pytest", specifier = ">=8" },
|
||||
{ name = "pytest-asyncio", specifier = ">=0.23" },
|
||||
{ name = "tornado", specifier = ">=6" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -806,6 +808,23 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/0b/c9/584bc9651441b4ba60cc4d557d8a547b5aff901af35bda3a4ee30c819b82/starlette-1.0.0-py3-none-any.whl", hash = "sha256:d3ec55e0bb321692d275455ddfd3df75fff145d009685eb40dc91fc66b03d38b", size = 72651, upload-time = "2026-03-22T18:29:45.111Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tornado"
|
||||
version = "6.5.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f8/f1/3173dfa4a18db4a9b03e5d55325559dab51ee653763bb8745a75af491286/tornado-6.5.5.tar.gz", hash = "sha256:192b8f3ea91bd7f1f50c06955416ed76c6b72f96779b962f07f911b91e8d30e9", size = 516006, upload-time = "2026-03-10T21:31:02.067Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/59/8c/77f5097695f4dd8255ecbd08b2a1ed8ba8b953d337804dd7080f199e12bf/tornado-6.5.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:487dc9cc380e29f58c7ab88f9e27cdeef04b2140862e5076a66fb6bb68bb1bfa", size = 445983, upload-time = "2026-03-10T21:30:44.28Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/5e/7625b76cd10f98f1516c36ce0346de62061156352353ef2da44e5c21523c/tornado-6.5.5-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:65a7f1d46d4bb41df1ac99f5fcb685fb25c7e61613742d5108b010975a9a6521", size = 444246, upload-time = "2026-03-10T21:30:46.571Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/04/7b5705d5b3c0fab088f434f9c83edac1573830ca49ccf29fb83bf7178eec/tornado-6.5.5-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e74c92e8e65086b338fd56333fb9a68b9f6f2fe7ad532645a290a464bcf46be5", size = 447229, upload-time = "2026-03-10T21:30:48.273Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/01/74e034a30ef59afb4097ef8659515e96a39d910b712a89af76f5e4e1f93c/tornado-6.5.5-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:435319e9e340276428bbdb4e7fa732c2d399386d1de5686cb331ec8eee754f07", size = 448192, upload-time = "2026-03-10T21:30:51.22Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/00/fe9e02c5a96429fce1a1d15a517f5d8444f9c412e0bb9eadfbe3b0fc55bf/tornado-6.5.5-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3f54aa540bdbfee7b9eb268ead60e7d199de5021facd276819c193c0fb28ea4e", size = 448039, upload-time = "2026-03-10T21:30:53.52Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/9e/656ee4cec0398b1d18d0f1eb6372c41c6b889722641d84948351ae19556d/tornado-6.5.5-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:36abed1754faeb80fbd6e64db2758091e1320f6bba74a4cf8c09cd18ccce8aca", size = 447445, upload-time = "2026-03-10T21:30:55.541Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/76/4921c00511f88af86a33de770d64141170f1cfd9c00311aea689949e274e/tornado-6.5.5-cp39-abi3-win32.whl", hash = "sha256:dd3eafaaeec1c7f2f8fdcd5f964e8907ad788fe8a5a32c4426fbbdda621223b7", size = 448582, upload-time = "2026-03-10T21:30:57.142Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/23/f6c6112a04d28eed765e374435fb1a9198f73e1ec4b4024184f21faeb1ad/tornado-6.5.5-cp39-abi3-win_amd64.whl", hash = "sha256:6443a794ba961a9f619b1ae926a2e900ac20c34483eea67be4ed8f1e58d3ef7b", size = 448990, upload-time = "2026-03-10T21:30:58.857Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/c8/876602cbc96469911f0939f703453c1157b0c826ecb05bdd32e023397d4e/tornado-6.5.5-cp39-abi3-win_arm64.whl", hash = "sha256:2c9a876e094109333f888539ddb2de4361743e5d21eece20688e3e351e4990a6", size = 448016, upload-time = "2026-03-10T21:31:00.43Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.15.0"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue