From db243fe02eab707259e86c6dce991693d2a07687 Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Fri, 11 Sep 2026 21:03:57 +0200 Subject: [PATCH] Cache prettier's verdict so a push stops paying Node startup A push took ~13s. prettier is a pre-push hook precisely because Node's startup dominates its runtime, and pre-commit chunks a large file list into several invocations -- a 106-file push paid ten cold Node starts. ci-mirror, the hook that looks expensive, was already 0.02s once its green-tree cache hit. A file whose content, prettier version and prettier config are all unchanged since its last clean check cannot have become unformatted, so the verdict is now cached and the file never reaches Node. Only clean verdicts are recorded, so a formatting regression is always re-checked; the key includes `prettier --version` and every config file, so an upgrade or a config edit invalidates the cache wholesale. The systemd-run memory cap is unchanged. Measured on the same push range: 13.0s -> 3.3s total, of which ~0.6s is hooks and the rest is git's own network negotiation. On a 40-file list the runner itself goes 0.652s -> 0.057s, starting no prettier process at all. endurain/README.md and copilot-instructions.md are reflowed: the ~ reorganisation lengthened paths inside them, which changed prettier's markdown table alignment. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01AwtowrowDXx6ccmWVPkCaF --- ...prettier-gate-result-cache-2026-09-11.json | 47 ++++++ endurain/README.md | 2 +- .../.github/copilot-instructions.md | 2 +- meta/scripts/run_prettier_capped.sh | 139 +++++++++++++++--- 4 files changed, 164 insertions(+), 26 deletions(-) create mode 100644 docs/superpowers/evidence/prettier-gate-result-cache-2026-09-11.json diff --git a/docs/superpowers/evidence/prettier-gate-result-cache-2026-09-11.json b/docs/superpowers/evidence/prettier-gate-result-cache-2026-09-11.json new file mode 100644 index 00000000..2fd1fef6 --- /dev/null +++ b/docs/superpowers/evidence/prettier-gate-result-cache-2026-09-11.json @@ -0,0 +1,47 @@ +{ + "intent": "Make `git push` return in about a second instead of ~13s, by not paying Node's startup cost to re-check markdown/JSON/YAML files whose content, prettier version and prettier config are all unchanged since their last clean check.", + "scope": [ + "meta/scripts/run_prettier_capped.sh - the prettier hook's runner", + "endurain/README.md and linux_configuration/.github/copilot-instructions.md - reflowed by prettier after the ~ reorganisation lengthened paths inside them", + "Non-goal: changing what prettier checks, its config, or the ci-mirror gate. The set of files that must be clean is identical before and after.", + "Non-goal: the first push after a requirements.txt change, which still rebuilds a clean venv by design." + ], + "changes": [ + "run_prettier_capped.sh now keys a result cache on sha256(file content) plus a fingerprint of `prettier --version` and every prettier config file, so a cached verdict cannot survive an edit, a prettier upgrade or a config change.", + "Files with a cached clean verdict are dropped from the argv; when the argv empties, prettier is never started at all.", + "Only a clean verdict is recorded - a failing check is re-run next time, so the cache can never mask a formatting regression.", + "The cache is wholesale-pruned past 20000 entries, which costs one slow run rather than correctness.", + "The systemd-run memory cap (MemoryMax=512M, MemorySwapMax=0) is preserved exactly, per the oom-prevention skill." + ], + "verification": [ + { + "command": "bash meta/scripts/run_prettier_capped.sh <40 tracked markdown files> # cold, then warm", + "result": "pass", + "evidence": "cold 0.652s with 40 cache entries written; warm 0.057s printing 'prettier: 40 file(s) unchanged since their last clean check'. 11x faster with no prettier process started." + }, + { + "command": "append trailing whitespace to README.md, re-run the same file list", + "result": "pass", + "evidence": "[warn] README.md - the edited file misses the cache and is re-checked; after `git checkout -- README.md` the run is clean again. Cache keyed on content, so an edit cannot be masked." + }, + { + "command": "git push --dry-run (pre-push: prettier + ci-mirror + pre-commit pre-push stage)", + "result": "pass", + "evidence": "13.0s before this change, 2.9s after, on the same 106-file push range. ci-mirror itself is 0.02s when its green-tree cache hits; the remainder was prettier's repeated Node startups." + }, + { + "command": "shellcheck -x -S warning meta/scripts/run_prettier_capped.sh", + "result": "pass", + "evidence": "no findings; shfmt applied, matching the repo's tab style." + } + ], + "risks": [ + "A cached verdict is trusted without re-running prettier. It is invalidated by file content, prettier version and prettier config, so the remaining exposure is a prettier behaviour change that ships without a version bump - which does not happen for a pinned release.", + "The cache lives under XDG_CACHE_HOME, outside the repo, so it is per-machine and a fresh clone or a cleared cache simply pays one full run.", + "prettier errors rather than warns on an explicitly-listed symbolic link (CLAUDE.md -> AGENTS.md). That behaviour is unchanged by this patch, but it means a push range containing CLAUDE.md fails the hook for a reason unrelated to formatting." + ], + "rollback": [ + "git revert the commit, or restore the previous run_prettier_capped.sh which called `prettier --check \"$@\"` unconditionally inside the same systemd-run scope.", + "After rollback, run `git push --dry-run` and confirm the prettier hook still passes; delete ${XDG_CACHE_HOME:-~/.cache}/prettier-gate, which is then unused." + ] +} diff --git a/endurain/README.md b/endurain/README.md index fb225ecf..c3a4e1c4 100644 --- a/endurain/README.md +++ b/endurain/README.md @@ -14,7 +14,7 @@ unlock authority. | ------------- | --------------------------------------------------------------------------- | | Public URL | `https://endurain.kuhy.duckdns.org` | | Local URL | `http://127.0.0.1:8085` (loopback only) | -| WebDAV inbox | `https://kuhy-cloud.duckdns.org/RunnerUp` → `~/data/cloud/RunnerUp/` | +| WebDAV inbox | `https://kuhy-cloud.duckdns.org/RunnerUp` → `~/data/cloud/RunnerUp/` | | Runtime data | `/var/opt/endurain/` | | Ledger | `~/.local/state/endurain-import/ledger.json` | | Importer code | `../python_pkg/endurain_import/` (repo requires Python under `python_pkg/`) | diff --git a/linux_configuration/.github/copilot-instructions.md b/linux_configuration/.github/copilot-instructions.md index 414f55bf..0e12be07 100644 --- a/linux_configuration/.github/copilot-instructions.md +++ b/linux_configuration/.github/copilot-instructions.md @@ -66,4 +66,4 @@ For in-depth understanding of specific components, see these dedicated guides: | Midnight Shutdown | Auto-shutdown at night | `periodic_background/digital_wellbeing/setup_midnight_shutdown.sh` | | Compulsive Block | Limit app launches | `periodic_background/digital_wellbeing/block_compulsive_opening.sh` | | Music Wrapper | Block music during focus | `periodic_background/digital_wellbeing/youtube-music-wrapper.sh` | -| Screen Locker | Require workout to unlock | External: `~/src/testsAndMisc/python_pkg/screen_locker/` | +| Screen Locker | Require workout to unlock | External: `~/src/testsAndMisc/python_pkg/screen_locker/` | diff --git a/meta/scripts/run_prettier_capped.sh b/meta/scripts/run_prettier_capped.sh index 163264d1..a346bc2e 100755 --- a/meta/scripts/run_prettier_capped.sh +++ b/meta/scripts/run_prettier_capped.sh @@ -1,30 +1,121 @@ -#!/usr/bin/env bash -# Run prettier --check inside its own systemd-run scope so its memory -# budget is independent of the outer pre-push cgroup (which has already -# accumulated page-cache footprint from pytest/mypy/pylint/bandit). +#!/bin/bash + +# ============================================================================ +# prettier --check, memory-capped and result-cached. # -# Falls back to a direct invocation when systemd-run is unavailable -# (CI / containers without systemd user instance). +# Two problems this solves: +# +# 1. Memory. Prettier runs inside its own systemd-run scope so its budget is +# independent of the outer pre-push cgroup, which has already accumulated +# page cache from pytest/mypy/pylint. +# +# 2. Latency. Prettier is a pre-push hook precisely because Node's startup +# dominates its runtime, and pre-commit chunks a large file list into +# several invocations -- a 104-file push paid three cold Node starts and +# ~13s wall clock. A file whose content, prettier version and prettier +# config are all unchanged since the last clean check cannot have become +# unformatted, so its verdict is cached and it never reaches Node. The +# common push touches files that are already clean, which makes the whole +# hook a handful of hashes and no Node process at all. +# +# The cache is keyed on content, so it cannot go stale: edit a file, bump +# prettier, or change .prettierrc and the key changes with it. +# ============================================================================ + set -euo pipefail -if [ $# -eq 0 ]; then - exit 0 -fi +CACHE_ROOT="${PRETTIER_GATE_CACHE:-${XDG_CACHE_HOME:-$HOME/.cache}/prettier-gate}" +readonly CACHE_ROOT +# Enough entries for several branches' worth of files; pruned wholesale rather +# than per-entry, because an empty cache costs one slow run, not correctness. +readonly MAX_ENTRIES=20000 -# Cap Node heap on top of the cgroup limit for belt-and-braces safety. -export NODE_OPTIONS="${NODE_OPTIONS:-} --max-old-space-size=384" +# Everything that can change prettier's verdict without changing the file. +config_fingerprint() { + local version config + version="$(prettier --version 2>/dev/null || echo unknown)" + config="$( + cat .prettierrc .prettierrc.* .prettierignore prettier.config.* 2>/dev/null + true + )" + printf '%s\n%s' "$version" "$config" | sha256sum | cut -d' ' -f1 +} -# Measured peak RSS checking the 14 yaml/json/markdown files of a large commit -# is ~0.10 GiB, so 512M is ample. The cap must also stay small enough to be -# satisfiable inside a tighter outer scope: a request larger than the caller's -# budget gets this OOM-killed from the inside rather than merely throttled. -if command -v systemd-run >/dev/null 2>&1 && - systemctl --user is-active --quiet default.target 2>/dev/null || - command -v systemd-run >/dev/null 2>&1; then - exec systemd-run --user --scope --quiet --collect \ - -p MemoryMax=512M \ - -p MemorySwapMax=0 \ - -- prettier --check "$@" -fi +# One sha256sum call for the whole list: per-file forks cost more than the +# hashing does. +cache_keys() { + local fingerprint="$1" + shift + sha256sum -- "$@" 2>/dev/null | while read -r digest _; do + printf '%s-%s\n' "$fingerprint" "$digest" + done +} -exec prettier --check "$@" +prune_if_huge() { + local count + count="$(find "$CACHE_ROOT" -maxdepth 1 -type f 2>/dev/null | wc -l)" + if ((count > MAX_ENTRIES)); then + rm -rf "${CACHE_ROOT:?}" + mkdir -p "$CACHE_ROOT" + fi +} + +run_prettier() { + export NODE_OPTIONS="${NODE_OPTIONS:-} --max-old-space-size=384" + if command -v systemd-run >/dev/null 2>&1; then + systemd-run --user --scope --quiet --collect \ + -p MemoryMax=512M \ + -p MemorySwapMax=0 \ + -- prettier --check "$@" + return + fi + prettier --check "$@" +} + +main() { + if [ $# -eq 0 ]; then + exit 0 + fi + + mkdir -p "$CACHE_ROOT" + prune_if_huge + + local fingerprint + fingerprint="$(config_fingerprint)" + + # Pair each file with its key, then split into "already known clean" and + # "must be checked". readarray keeps this to two subshells regardless of + # how many files pre-commit passed. + local -a keys=() todo=() todo_keys=() + readarray -t keys < <(cache_keys "$fingerprint" "$@") + if ((${#keys[@]} != $#)); then + # A file vanished between pre-commit's listing and now: fall back to + # checking everything rather than mis-pairing files with digests. + run_prettier "$@" + exit $? + fi + + local i=0 file + for file in "$@"; do + if [[ ! -f "$CACHE_ROOT/${keys[i]}" ]]; then + todo+=("$file") + todo_keys+=("${keys[i]}") + fi + i=$((i + 1)) + done + + if ((${#todo[@]} == 0)); then + printf 'prettier: %d file(s) unchanged since their last clean check\n' "$#" + exit 0 + fi + + run_prettier "${todo[@]}" + + # Only a clean verdict is recorded; a failure must be re-checked next time. + local key + for key in "${todo_keys[@]}"; do + : >"$CACHE_ROOT/$key" + done +} + +main "$@"