diff --git a/.github/scripts/bench_metrics.py b/.github/scripts/bench_metrics.py index 2f240d8..439f54a 100644 --- a/.github/scripts/bench_metrics.py +++ b/.github/scripts/bench_metrics.py @@ -16,7 +16,7 @@ import math # Cost model and grid types come straight from the package so the benchmark can -# never drift from what production actually bills/uses (arm64, 2 GB -- issue #110). +# never drift from what production actually bills/uses (arm64, 4 GB -- issue #110/#193). from zagg.dispatch import LAMBDA_MEMORY_GB, LAMBDA_PRICE_PER_GB_SEC from zagg.grids.healpix import HealpixGrid from zagg.grids.rectilinear import RectilinearGrid @@ -73,6 +73,9 @@ "setup_s", "fanout_s", "finalize_s", + # Read-backend axis (issue #193): "inline" | "sidecar" -- the live matrix's + # A/B column. Null on frozen/legacy rows (codec/read axes predate it). + "index_backend", ] @@ -178,6 +181,7 @@ def build_record( # the target. None on targets that don't set it (legacy/frozen rows). "codec": context.get("codec"), "read": context.get("read"), + "index_backend": context.get("index_backend"), # Wall-time breakdown (issue #180): total end-to-end AOI dispatch wall # plus its phases, straight from the agg summary. "total_wall_s": summary.get("wall_time_s"), diff --git a/.github/scripts/plot_series.py b/.github/scripts/plot_series.py index 4515117..65fac69 100644 --- a/.github/scripts/plot_series.py +++ b/.github/scripts/plot_series.py @@ -59,14 +59,14 @@ def memory_fractions(sub: pd.DataFrame) -> list[float | None]: def _memory_cap_mb(sub: pd.DataFrame) -> float: """Lambda memory cap in MB, for the colorbar's fraction<->MB twin axis. - Reads ``memory_gb`` (uniform across the series -- the benchmark pins 2 GB) and + Reads ``memory_gb`` (uniform across the series -- the benchmark pins 4 GB) and falls back to 2048 MB when the column is absent or empty. """ if "memory_gb" in sub.columns: caps = sub["memory_gb"].dropna() if not caps.empty: return float(caps.iloc[0]) * 1024.0 - return 2.0 * 1024.0 + return 4.0 * 1024.0 # issue #193: benchmark pins 4 GB def _merge_history(df: pd.DataFrame) -> pd.DataFrame: @@ -97,6 +97,21 @@ def _frozen_history(df: pd.DataFrame) -> pd.DataFrame: return _merge_history(df[~_codec_mask(df)]) +def _matrix_mask(df: pd.DataFrame) -> pd.Series: + """Boolean mask of the live matrix rows (issue #193): those carrying a + non-null ``index_backend`` (inline|sidecar). Absent column (a pre-#193 + parquet) -> all False, so the matrix figures stay empty until its first + merge lands.""" + if "index_backend" not in df.columns: + return pd.Series(False, index=df.index) + return df["index_backend"].notna() + + +def _matrix_history(df: pd.DataFrame) -> pd.DataFrame: + """Retained merge points of the live inline-vs-sidecar matrix (issue #193).""" + return _merge_history(df[_matrix_mask(df)]) + + def _latest_of(hist: pd.DataFrame) -> pd.DataFrame: """Rows of the single most-recent run in an already-filtered/ordered history. @@ -317,6 +332,26 @@ def _codec_layout(hist: pd.DataFrame) -> tuple[list[list[str | None]], int, int] return grid, len(CODEC_ROWS), len(CODEC_COLS) +# Live matrix (issue #193): read-backend A/B x order. Retired the sharded/inner +# codec axis; those figures are frozen (see main()). +MATRIX_COLS = ["inline", "sidecar"] +MATRIX_ROWS = ["o9", "o10"] + + +def _matrix_layout(hist: pd.DataFrame) -> tuple[list[list[str | None]], int, int]: + """Place each live target in the fixed ``(order, index_backend)`` grid + (issue #193). Columns = ``MATRIX_COLS`` (inline, sidecar); rows = + ``MATRIX_ROWS`` (o9, o10). Empty cell -> ``None`` (order not landed yet).""" + by_slot: dict[tuple[str, str], str] = {} + meta = hist.dropna(subset=["target"]).drop_duplicates("target") + for _, r in meta.iterrows(): + by_slot[(str(r.get("grid_size", "")), str(r.get("index_backend", "")))] = r["target"] + grid: list[list[str | None]] = [ + [by_slot.get((order, backend)) for backend in MATRIX_COLS] for order in MATRIX_ROWS + ] + return grid, len(MATRIX_ROWS), len(MATRIX_COLS) + + def _draw_panel(ax, sub: pd.DataFrame, cost_col: str, cost_label: str, norm) -> None: """Draw one cost+runtime panel for a single target's merge history. @@ -517,6 +552,34 @@ def make_figure(df: pd.DataFrame, cost_col: str, cost_label: str, out_png: Path) ) +def make_matrix_latest_table(df: pd.DataFrame, out_png: Path) -> bool: + """Render the live inline-vs-sidecar latest-merge table (issue #193): the + ``index_backend.notna`` rows of the most recent merge. False until the new + matrix lands its first merge.""" + recs = _latest_of(_matrix_history(df)).to_dict(orient="records") + return _render_table(recs, _table_title("zagg inline vs sidecar (sharded, K=4)", recs), out_png) + + +def make_matrix_figure(df: pd.DataFrame, cost_col: str, cost_label: str, out_png: Path) -> bool: + """Render the live inline-vs-sidecar figure (issue #193): the + ``index_backend.notna`` rows in the fixed ``MATRIX_ROWS x MATRIX_COLS`` grid. + Returns False when no matrix rows are retained yet.""" + hist = _matrix_history(df) + if hist.empty or hist["target"].dropna().empty: + return False + layout, nrows, ncols = _matrix_layout(hist) + return _render_panel_grid( + hist, + layout, + nrows, + ncols, + cost_col, + cost_label, + f"zagg inline vs sidecar (sharded, K=4) \u2014 {cost_label} vs merge history", + out_png, + ) + + def make_codec_figure(df: pd.DataFrame, cost_col: str, cost_label: str, out_png: Path) -> bool: """Render the forward sharded-vs-inner figure (issue #133): the ``codec.notna`` rows in a fixed 2x3 grid (cols = sharded/inner, rows = o9->o11). Returns False @@ -540,52 +603,64 @@ def make_codec_figure(df: pd.DataFrame, cost_col: str, cost_label: str, out_png: def write_index( outdir: Path, - rendered: list[str], - codec_rendered: list[str], + matrix_rendered: list[str], *, - codec_table_png: bool = False, - latest_png: bool = False, + matrix_table_png: bool = False, has_md: bool = False, has_json: bool = False, ) -> None: - """Emit a minimal Pages index embedding the rendered figures. - - Order (issue #133): the forward sharded-vs-inner section on top -- its 2x3 - codec table then its codec charts -- then the frozen historical section - (latest table + frozen charts) below, so the new matrix leads.""" + """Emit a Pages index: the live inline-vs-sidecar matrix on top (issue + #193), then an ARCHIVED section embedding the retained (frozen) codec + + historical PNGs if they still exist in ``outdir`` (they persist on the + benchmarks branch; this PR stopped regenerating them).""" blocks: list[str] = [] - # --- forward sharded-vs-inner section (on top) --- links = [] if has_md: links.append('latest.md') if has_json: links.append('metrics.json') - if codec_table_png: + + # --- live matrix section (on top) --- + if matrix_table_png: block = ( - "
'
+ "
'
)
if links:
- block += f"\nMachine-readable: {' · '.join(links)}
" + block += f"\nMachine-readable: {' \u00b7 '.join(links)}
" blocks.append(block) blocks += [ - f'
'
- for name in codec_rendered
+ f'
'
+ for name in matrix_rendered
]
- # --- frozen historical section (below) ---
- if latest_png:
- block = (
- "
'
+
+ # --- archived section (below): retained frozen PNGs, embedded if present ---
+ archived: list[str] = []
+ if (outdir / "codec_table.png").exists():
+ archived.append(
+ "
'
)
- # The companions are linked above with the forward table; if there is no
- # forward table, link them here so they stay reachable.
- if links and not codec_table_png:
- block += f"\nMachine-readable: {' · '.join(links)}
" - blocks.append(block) - blocks += [ - f'
' for name in rendered
- ]
+ if (outdir / "latest_table.png").exists():
+ archived.append(
+ "
'
+ )
+ for name in FIGURES:
+ for suffix, tag in (("_codec", "sharded vs inner"), ("", "frozen")):
+ if (outdir / f"{name}{suffix}.png").exists():
+ archived.append(
+ f"
'
+ )
+ if archived:
+ blocks.append(
+ "The pre-#193 codec (sharded/inner) + historical series, retained " + "but no longer updated.
" + ) + blocks += archived + if not blocks: blocks = [ "No retained benchmark runs yet. Charts appear after the first merge to main.
" @@ -597,8 +672,8 @@ def write_index( "\n" "\narm64 · 2 GB · one shard/target · densest cell over the NEON SERC AOP box. "
- "Each point is a merge to main.
arm64 \u00b7 4 GB \u00b7 one shard/target \u00b7 densest cell over the NEON SERC "
+ "AOP box. Each point is a merge to main.