Skip to content

Commit 5fcc4fc

Browse files
MendixMauclaude
andauthored
doctor: name the slow step instead of "None needed"; sync-folder check; plain-words advice (#596)
Fast process starts but one bench step 5x+ and 2 s+ over reference -> that step's fix (git: core.untrackedCache / core.fsmonitor). Project inside OneDrive/Dropbox/Google Drive/iCloud (whole path components) -> health WARN and "move the project". Low memory -> restart first, --cleanup as alternative. Bench git step warms up once. speed-setup.md: never say deno/mxbuild/helper to users. Review: MERGE-AFTER-FIX — sync-folder match on whole path components, bench wording, skill order fixed; re-reviewed MERGE (nit: long header comment) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013jQvaonym9V1wKKfAEPmRr
1 parent 60e369b commit 5fcc4fc

4 files changed

Lines changed: 80 additions & 8 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ Sections dated before 2026-09-19 predate the cycle and stay as they are. Named m
1919

2020
## Unreleased
2121

22+
- fix(doctor, speed-setup): **doctor no longer says "None needed" next to a verdict of "5x slower"**: when process starts are fast but one bench step is 5x+ the reference and 2 s+ slower, the Recommended fix names that step (slow git status: `git config core.untrackedCache true` / `core.fsmonitor true`, no admin rights); a project inside a OneDrive / Dropbox / Google Drive / iCloud folder gets a Machine health warning and "move the project to a local folder" as the fix; low-memory advice now offers "restart the computer" before `--cleanup`; the bench's git step runs one untimed warm-up so a stale index after a copy or clone no longer reads as slow (field, ab-581/gk: 18.3 s cold, 31 ms warm); `skills/speed-setup.md` tells Claude never to say deno, mxbuild or helper process to a user. Field run: ab-581/gk on Linux, bench 14.7 s vs 10.9 s, "None needed" correctly; a project under a OneDrive path, move advice shown — Maurits (mauritscgvisser)
2223
- fix(gate-check, gate-cache): **the closeout verdict cache also keys on file CONTENT, so an overwrite that keeps an old mtime (`cp -p`, `rsync -t`, `tar x`, `unzip`, `touch -d`) can no longer replay a stale verdict**: a signed triage.md copied over a FAIL one still printed the cached Stage 0 FAIL (PR #589 review); the cache now stores one `find | sort | xargs cksum | cksum` signature of the project files (skipping .git, node_modules, .mxtk, mprcontents, deployment, *.mpr, files over 1 MB) and any difference, or a signature that cannot be computed, re-evaluates; new `tests/wave2/test-gate-cache.sh` (hit, edit/delete/waiver miss, cp -p miss, TTL 0, 'not evaluated' line); `MXTK_GATE_CACHE_TRACE=1` prints `gate-cache: hit|miss` on --closeout. Field run: ab-581/base copy (197 MB, 633 hashed files) - signature 28 ms, cache hit 1464 -> 1498 ms vs 2567-2786 ms full evaluation - PR #589 reviewer, OlafFede (#583) — OlafFede (#583)
2324
- perf(gate-check, closeout): **a Stage close starts about 70% fewer processes (Stage 3 on a small project: `--closeout` 820 -> 127 starts, `gate-check <dir> 3` 560 -> 275, no-stage run 626 -> 338; verdict lines unchanged)**: (1) `--closeout` replays the evaluation of the stage query that just ran (variables snapshotted in `<project>/.mxtk/gate-cache`, valid only while nothing under the project or the toolkit is newer than that run and the key matches; any doubt re-evaluates; `MXTK_GATE_CACHE_TTL_MIN=0` turns it off); (2) a stage query no longer evaluates obligation/artifact rows owed from later stages and says how many it skipped (the summary line is labelled the same way); (3) the register is parsed once per run instead of one awk per lookup; (4) the toolkit `git fetch` runs at most once per 15 min (`MXTK_FETCH_TTL_MIN`, `MXTK_NO_FETCH=1` unchanged); plus pure-bash trimming in the close-out block and the obligation reverse check. Reported by OlafFede (#583). — OlafFede (#583)
2425
- new(doctor, speed-setup): **`bin/doctor.sh --bench [<project>]`** times 50 process starts, git status, python/node start, one gate-check, an mxcli check and (on a COPY of the model) mx check against measured reference constants, prints one verdict line and writes `.claude/loop/bench-<timestamp>.tsv` for comparing machines; a **Machine health** section (normal run and `--bench`) warns on leftover mxbuild `deno` helpers and free memory under 2 GB (mxbuild is killed with exit 137 when memory runs out), `--cleanup` stops exactly those helpers after a typed y (no tty: prints the command); both runs end with ONE **Recommended fix** block (Gate mode: check, else WSL, dev container, or Claude in the cloud; antivirus-exclusion advice removed, it needed IT); new `skills/speed-setup.md`. Field run: ab-581/gk on Linux 4 cores, 46 orphan deno helpers found, bench 10.4 s vs 10.9 s reference — Maurits (mauritscgvisser)

‎bin/doctor.sh‎

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -447,9 +447,34 @@ recommended_fix() {
447447
return 0
448448
fi
449449
fi
450+
if [ -n "${MH_SYNC:-}" ]; then
451+
echo " Move the project out of $MH_SYNC to a plain local folder (for example C:\\dev\\<project> or ~/dev/<project>),"
452+
echo " then reopen it there. A sync client re-reads and uploads every file git and the build touch, which"
453+
echo " slows every step and can lock the model file mid-write."
454+
return 0
455+
fi
450456
if [ "$_rf_slow" = 0 ] && [ "${MH_BAD:-0}" = 1 ]; then
451-
echo " Free up memory first (see Machine health): bin/doctor.sh --cleanup stops leftover mxbuild helpers,"
452-
echo " asking before it stops anything. Process starts are not slow here${RF_SPAWN_MS:+ ($RF_SPAWN_MS ms each)}."
457+
echo " Free up memory first (see Machine health). Simplest: restart the computer, which also ends Mendix"
458+
echo " build processes that were left running. Or, without a restart: bin/doctor.sh --cleanup, which asks"
459+
echo " before it stops anything. Process starts are not slow here${RF_SPAWN_MS:+ ($RF_SPAWN_MS ms each)}."
460+
return 0
461+
fi
462+
# One step far slower than the reference while process starts are fine: name that step, never "None needed".
463+
# Threshold: the slowest step is 5x+ the reference AND costs 2 s+ extra (B_TOP_* from --bench; 0 otherwise).
464+
if [ "$_rf_slow" = 0 ] && [ "${B_TOP_F10:-0}" -ge 50 ] && [ "${B_TOP_EXCESS:-0}" -ge 2000 ]; then
465+
case "$B_TOP_ID" in
466+
git)
467+
echo " git is slow in this project (git status: $(( (B_TOP_EXCESS + BENCH_REF_GIT_MS) / 100 / 10 )).$(( (B_TOP_EXCESS + BENCH_REF_GIT_MS) / 100 % 10 )) s; normal is well under 1 s)."
468+
echo " Turn on git's own file cache for this project (no install, no admin rights):"
469+
echo " git config core.untrackedCache true"
470+
[ "$MH_OS" != linux ] && echo " git config core.fsmonitor true"
471+
echo " Then run doctor --bench again. Still slow: check that no large build output or source dump is"
472+
echo " tracked in git (git count-objects -vH)." ;;
473+
*)
474+
echo " One step is much slower than the reference: $B_TOP_LABEL. Process starts are fine, so this is"
475+
echo " probably a one-off (first run after a restart or an update). Run doctor --bench once more; if"
476+
echo " the same step is still slow, send the results file to the toolkit maintainers." ;;
477+
esac
453478
return 0
454479
fi
455480
if [ "$_rf_slow" = 0 ]; then
@@ -593,6 +618,17 @@ machine_health() {
593618
else
594619
ok "no leftover mxbuild helper processes"
595620
fi
621+
# Project inside a file-sync folder (OneDrive is where Windows puts Documents and Desktop by default).
622+
MH_SYNC=""; _mh_pd=""
623+
[ -n "${PROJECT_DIR:-}" ] && _mh_pd="$(cd "$PROJECT_DIR" 2>/dev/null && pwd)"
624+
# Whole path components only: ~/dev/dropbox-client or a repo named OneDrive-connector must not match.
625+
case "$_mh_pd/" in
626+
*/OneDrive/*|*/"OneDrive - "*/*) MH_SYNC=OneDrive ;;
627+
*/Dropbox/*|*/"Dropbox ("*/*) MH_SYNC=Dropbox ;;
628+
*/"Google Drive"/*|*/GoogleDrive/*|*/"My Drive"/*) MH_SYNC="Google Drive" ;;
629+
*/"Mobile Documents"/*|*/"iCloud Drive"/*) MH_SYNC=iCloud ;;
630+
esac
631+
[ -n "$MH_SYNC" ] && advise "the project is in a folder synced by $MH_SYNC: $_mh_pd"
596632
MH_BAD="$_mh_bad"
597633
if [ "$_mh_bad" = 1 ]; then
598634
note "When memory runs out the operating system kills mxbuild (exit 137), and the build gate then reads"
@@ -644,7 +680,7 @@ if [ "$CLEANUP" = 1 ]; then
644680
exit 0
645681
fi
646682

647-
# --bench: ~1 minute, changes nothing in the project. Times the things every toolkit script is made of,
683+
# --bench: ~1 minute, changes nothing in the project (only git's own stat cache, via the warm-up below). Times the things every toolkit script is made of,
648684
# against this table's reference column, so "the toolkit is slow here" becomes a number a colleague can
649685
# compare. Writes <project>/.claude/loop/bench-<timestamp>.tsv (one row per step, concatenable).
650686
#
@@ -699,15 +735,16 @@ b_fmt_ms() {
699735
}
700736

701737
# b_row <id> <label> <this-ms|""> <ref-ms> <note> — print the table row, append the TSV row, feed the verdict.
702-
B_SPAWN_FAC=""; B_TOP_ID=""; B_TSV=""; B_TOT_THIS=0; B_TOT_REF=0; B_NMEAS=0; B_TOP_EXCESS=0; B_TOP_LABEL=""; B_SPAWN_THIS=""; B_NOTES=""
738+
B_SPAWN_FAC=""; B_TOP_ID=""; B_TSV=""; B_TOT_THIS=0; B_TOT_REF=0; B_NMEAS=0; B_TOP_EXCESS=0; B_TOP_LABEL=""; B_TOP_F10=0; B_SPAWN_THIS=""; B_NOTES=""
703739
b_row() {
704740
_r_id="$1"; _r_label="$2"; _r_this="$3"; _r_ref="$4"; _r_note="$5"
705741
b_fmt_ms "$_r_ref"; _r_reffmt="$B_FMT"
706742
if [ -n "$_r_this" ]; then
707743
b_fmt_ms "$_r_this"; _r_thisfmt="$B_FMT"
744+
_r_f10=0
708745
if [ "$_r_ref" -gt 0 ]; then _r_f10=$(( _r_this * 10 / _r_ref )); _r_fac="$((_r_f10 / 10)).$((_r_f10 % 10))"; else _r_fac="-"; fi
709746
B_TOT_THIS=$((B_TOT_THIS + _r_this)); B_TOT_REF=$((B_TOT_REF + _r_ref)); B_NMEAS=$((B_NMEAS + 1))
710-
if [ $((_r_this - _r_ref)) -gt "$B_TOP_EXCESS" ]; then B_TOP_EXCESS=$((_r_this - _r_ref)); B_TOP_LABEL="$_r_label"; B_TOP_ID="$_r_id"; fi
747+
if [ $((_r_this - _r_ref)) -gt "$B_TOP_EXCESS" ]; then B_TOP_EXCESS=$((_r_this - _r_ref)); B_TOP_LABEL="$_r_label"; B_TOP_ID="$_r_id"; B_TOP_F10="${_r_f10:-0}"; fi
711748
[ "$_r_id" = spawn50 ] && B_SPAWN_THIS="$_r_this" && B_SPAWN_FAC="$_r_fac"
712749
else
713750
_r_thisfmt="not measured"; _r_fac="-"
@@ -738,11 +775,15 @@ if [ "$BENCH" = 1 ]; then
738775
if [ -n "$_t0" ] && [ -n "$_t1" ]; then b_row spawn50 "50 process starts" "$((_t1 - _t0))" "$BENCH_REF_SPAWN50_MS" ""
739776
else b_row spawn50 "50 process starts" "" "$BENCH_REF_SPAWN50_MS" "no sub-second clock"; fi
740777

741-
# 2. git status (GIT_OPTIONAL_LOCKS=0: git must not refresh the index of the project it measures)
778+
# 2. git status. One untimed warm-up first: a copied or freshly cloned project has a stale index, and the
779+
# first git status there pays a one-off full re-read (field, 2026-10-29: 18.3 s, then 0.03 s). The warm-up
780+
# is an ordinary `git status`, which only refreshes git's own file-stat cache (.git/index), exactly as
781+
# the user's next git status would; no tracked or untracked project file changes. The timed run is warm.
742782
if [ -z "$B_P" ]; then b_row git "git status" "" "$BENCH_REF_GIT_MS" "no project given"
743783
elif ! command -v git >/dev/null 2>&1; then b_row git "git status" "" "$BENCH_REF_GIT_MS" "git not installed"
744784
elif ! git -C "$B_P" rev-parse --git-dir >/dev/null 2>&1; then b_row git "git status" "" "$BENCH_REF_GIT_MS" "project is not a git repository"
745785
else
786+
git -C "$B_P" status --porcelain >/dev/null 2>&1 < /dev/null
746787
b_clock; _t0="$B_MS"; GIT_OPTIONAL_LOCKS=0 git -C "$B_P" status --porcelain >/dev/null 2>&1 < /dev/null; b_clock; _t1="$B_MS"
747788
if [ -n "$_t0" ] && [ -n "$_t1" ]; then b_row git "git status" "$((_t1 - _t0))" "$BENCH_REF_GIT_MS" ""
748789
else b_row git "git status" "" "$BENCH_REF_GIT_MS" "no sub-second clock"; fi

‎skills/speed-setup.md‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ The user complains about speed, asks to update the toolkit and check whether it
77

88
## Steps (in order; the user is asked before every change, except the read-only ones)
99
0. Update first. Check that the toolkit clone was pulled (`git -C <toolkit> status -sb` shows it is not behind; if it is, ask before pulling). Then run `<toolkit>/bin/sync-project.sh <project>`. Report in plain words what sync changed, and every locally modified script it KEPT (it never overwrites those; say which, and that `--upgrade-bin <name>` replaces one after a backup, only if the user asks).
10-
1. Run `bin/doctor.sh --bench .` from the project root. It changes nothing and takes about a minute. Read the table (step, this machine, reference, factor), the "Machine health" lines and the closing "Recommended fix" block.
11-
2. VALIDATE first: is this machine slower than it should be? Use the verdict line and the Machine health lines. If the verdict says about as fast as the reference, Machine health is clean and the block says "None needed", say so in ONE line ("This machine is already fast: <verdict>") and STOP. Do not propose a fix for a fast machine. Otherwise say the result in ONE plain sentence, from the verdict line, for example: "This machine is 6x slower than the reference, mainly because every process start takes 100 ms instead of 2 ms." If Machine health warned (leftover helpers, under 2 GB free), say that first: run `bin/doctor.sh --cleanup`, which asks before stopping anything.
10+
1. Run `bin/doctor.sh --bench .` from the project root. It changes no project file (only git's own stat cache, as any git status does) and takes about a minute. Read the table (step, this machine, reference, factor), the "Machine health" lines and the closing "Recommended fix" block.
11+
2. VALIDATE first: is this machine slower than it should be? Use the verdict line and the Machine health lines. If the verdict says about as fast as the reference, Machine health is clean and the block says "None needed", say so in ONE line ("This machine is already fast: <verdict>") and STOP. Do not propose a fix for a fast machine. Otherwise say the result in ONE plain sentence, from the verdict line, for example: "This machine is 6x slower than the reference, mainly because every process start takes 100 ms instead of 2 ms." If Machine health warned, say that first, in words a non-developer knows. Users do not know what deno, mxbuild or a helper process is; never use those words to them. Wrong: "47 leftover deno helpers, run --cleanup?" Right: "Mendix build processes that were left running hold 3 GB of memory. Restarting your computer clears them; or I can stop just those now, if you say yes (`bin/doctor.sh --cleanup`)." For a project in a OneDrive/Dropbox/iCloud folder, the fix is moving the project to a local folder (the Recommended fix says how).
1212
3. Only when step 2 found a problem: carry out the "Recommended fix" with the user. It names ONE option, the first that works here:
1313
- **Remove `Gate mode: full` from PROJECT.md** (the quick `mx check` per write is the default). Show the line, ask, remove it. Nothing else changes.
14+
- **Move the project out of a synced folder** (OneDrive, Dropbox, Google Drive, iCloud). Ask, then move or re-clone it to the local folder the block names, and reopen it there. This comes before WSL, a dev container or the cloud: a synced folder slows every option, so move first and bench again.
15+
- **Turn on git's file cache** (git status slow, process starts fine): the `git config` lines the block prints, in the project, after asking.
1416
- **Run the toolkit in WSL** (Windows, WSL already present). Guide: open the WSL shell, clone the project inside the WSL filesystem (not under /mnt/c), run `bin/doctor.sh` there. Ask before each step.
1517
- **Use a dev container** (docker or podman already runs). Guide through the project's container setup; ask before each step.
1618
- **Let Claude work in the cloud** (Claude Code on the web). The user pulls the result into Studio Pro with `git pull`. Nothing is installed locally.

‎tests/wave2/test-doctor-bench.sh‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,5 +206,33 @@ chk "fix b: wsl.exe --status failing -> next option" "$WORK/rf.out" "Use a dev c
206206
rm -f "$WORK/bin/uname" "$WORK/bin/wsl.exe"
207207
chkno "no IT-dependent advice anywhere in the output" "$WORK/rf.out" "exclusion\|ask IT" -i
208208

209+
# 7. Fast process starts but one step far slower -> name the step, never "None needed" (field, 2026-10-29:
210+
# verdict "5x slower, mainly git status" next to "None needed"). A fake git sleeps 2.5 s on status only.
211+
printf '#!/bin/sh\ncase "$*" in *status*) sleep 2.5 ;; esac\nexit 0\n' > "$WORK/bin/git"; chmod +x "$WORK/bin/git"
212+
rf bash "$DOCTOR" --bench "$P"
213+
chk "slow step: git status slow, starts fast -> git cache advice" "$WORK/rf.out" "git config core.untrackedCache true"
214+
chkno "slow step: not 'None needed'" "$WORK/rf.out" "None needed"
215+
rm -f "$WORK/bin/git"
216+
# 8. Project inside a sync folder (OneDrive holds Documents/Desktop by default on Windows) -> move it
217+
mkdir -p "$WORK/OneDrive - Contoso/proj"; printf '# P\n' > "$WORK/OneDrive - Contoso/proj/PROJECT.md"
218+
rf bash "$DOCTOR" --bench "$WORK/OneDrive - Contoso/proj"
219+
chk "sync folder: health line names OneDrive" "$WORK/rf.out" "folder synced by OneDrive"
220+
chk "sync folder: fix is moving the project" "$WORK/rf.out" "Move the project out of OneDrive"
221+
# 8b. Only whole path components match: a repo NAMED like a sync client is not inside one
222+
mkdir -p "$WORK/dev/OneDrive-connector" "$WORK/dev/dropbox-client"
223+
printf '# P\n' > "$WORK/dev/OneDrive-connector/PROJECT.md"; printf '# P\n' > "$WORK/dev/dropbox-client/PROJECT.md"
224+
rf bash "$DOCTOR" --bench "$WORK/dev/OneDrive-connector"
225+
chkno "sync folder: repo named OneDrive-connector is not flagged" "$WORK/rf.out" "folder synced by"
226+
rf bash "$DOCTOR" --bench "$WORK/dev/dropbox-client"
227+
chkno "sync folder: repo named dropbox-client is not flagged" "$WORK/rf.out" "folder synced by"
228+
# 8c. The sync-folder WARN does not change doctor's exit code
229+
( PATH="$SAFEPATH" MXTK_PROC_MEMINFO="$WORK/meminfo-high" FAKE_PS_FILE="$WORK/ps-clean.txt" bash "$DOCTOR" --quick "$WORK/OneDrive - Contoso/proj" > "$WORK/q-od.out" 2>&1 ); _rc_od=$?
230+
( PATH="$SAFEPATH" MXTK_PROC_MEMINFO="$WORK/meminfo-high" FAKE_PS_FILE="$WORK/ps-clean.txt" bash "$DOCTOR" --quick "$WORK/dev/dropbox-client" > "$WORK/q-nod.out" 2>&1 ); _rc_nod=$?
231+
grep -q 'folder synced by OneDrive' "$WORK/q-od.out" && [ "$_rc_od" = "$_rc_nod" ] \
232+
&& ok "sync folder: WARN shown, exit code unchanged ($_rc_od)" || bad "sync folder: rc $_rc_nod -> $_rc_od or no WARN"
233+
# 9. Low-memory advice offers a restart, the plain option, besides --cleanup
234+
rf env MXTK_PROC_MEMINFO="$WORK/meminfo-low" bash "$DOCTOR" --bench "$P"
235+
chk "low memory: restart offered" "$WORK/rf.out" "restart the computer"
236+
209237
echo "PASS=$PASS FAIL=$FAIL"
210238
[ "$FAIL" -eq 0 ]

0 commit comments

Comments
 (0)