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
11 changes: 11 additions & 0 deletions .claude/skills/mama-style-review/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,17 @@ grep -rn 'Color\.YELLOW' mama/ | grep -v 'utils/system.py'
- A new ~3-line helper duplicating something in util.py is a finding.

### Code shape
- **Lean and mean: cache repeated/invariant calculations.** Never compute the
same value twice. Flag a `sum()` / probe / `.encode()` / `stat` / glob
evaluated twice in one expression (classically in a comprehension's filter
AND the value it builds), or recomputed every loop iteration when it is
loop-invariant - hoist it out and compute once. Process-constant results
(terminal encoding, cpu count, a compiled regex) get memoized a single time,
not re-derived per call. The fix is usually both faster AND fewer lines.
```bash
# smell: same call in the filter and the body of one comprehension
grep -rn 'for .* if .*\b\(sum\|len\|glob\|encode\|stat\)\b' mama/ tests/
```
- Long functions are a smell. If you find yourself adding a third large
responsibility to a function (e.g. another inline artifactory probe inside
`_load`), extract a helper.
Expand Down
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,13 @@ mama.cmake

packages/
bin/

# Local CMake config-overhead benchmark (clones + build dirs)
bench/repos/
bench/_out/

# pytest tmp_path artifacts (repo-local, see tests/conftest.py)
.pytest_tmp/

# Claude Code agent worktrees (transient)
.claude/worktrees/
7 changes: 7 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ keep biting future-Claude. Update as the codebase teaches new lessons.
- **Yellow output goes through `warning(text)`** (from `mama.utils.system`),
not `console(text, color=Color.YELLOW)`. The helper exists so warnings have
a single chokepoint and a consistent shape.
- **Lean and mean: cache repeated/invariant calculations.** Never compute the
same value twice. A `sum()` / probe / `.encode()` / `stat` evaluated twice in
one expression (e.g. in a filter AND the value it builds), or recomputed per
loop iteration when it's loop-invariant, is a finding - hoist it out and
compute once. Results that are constant for the whole process (terminal
encoding, cpu count, a compiled regex) get memoized a single time, not
re-derived per call. Less work and less code, same result.

### Examples

Expand Down
216 changes: 133 additions & 83 deletions README.md

Large diffs are not rendered by default.

107 changes: 107 additions & 0 deletions bench/bench_config_overhead.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
"""Measure CMake CONFIGURE overhead (no compilation) across real repos, to judge config-speed
ideas on data. For each repo in repos.txt it reports three numbers with the VS generator:

cold - fresh build dir (full compiler detection)
seeded - mama's ABI seed injected (skips the ABI try_compile)
warm - configure twice, 2nd run (detection cached) = the pre-warm ceiling

The cold->warm gap is the headroom a parallel detection pre-warm could capture. This is a manual
benchmark (clones + runs real cmake); it is NOT part of the unit suite.

python bench/bench_config_overhead.py # all repos in repos.txt
python bench/bench_config_overhead.py ReCpp # just one
"""
import os, re, sys, time, shutil, subprocess, tempfile

HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(HERE)
sys.path.insert(0, ROOT)
from mama import cmake_compiler_cache as cc # noqa: E402

REPOS_DIR = os.path.join(HERE, 'repos')
OUT_DIR = os.path.join(HERE, '_out') # build dirs + seeds, repo-local + gitignored
_DETECT = re.compile(r'identification is|compiler ABI info|compile features')


def sh(args, cwd=None):
return subprocess.run(args, cwd=cwd, capture_output=True, text=True)


def vs_generator():
out = sh(['cmake', '--help']).stdout
vs = [l.split('=')[0].strip().lstrip('* ') for l in out.splitlines() if 'Visual Studio' in l and '=' in l]
return vs[0] if vs else ''


def cmake_ver():
out = sh(['cmake', '--version']).stdout
m = re.search(r'(\d+\.\d+\.\d+)', out)
return m.group(1) if m else 'unknown'


def configure(src, build, gen, extra=()):
args = ['cmake']
if gen: args += ['-G', gen, '-A', 'x64']
args += [*extra, '-S', src, '-B', build]
t = time.monotonic()
cp = sh(args)
return time.monotonic() - t, (_DETECT.findall(cp.stdout).__len__()), cp.returncode == 0


def fresh(prefix):
os.makedirs(OUT_DIR, exist_ok=True)
return tempfile.mkdtemp(prefix=prefix, dir=OUT_DIR) # repo-local, not system temp


def bench_repo(name, src, gen, ver):
cf = lambda b: os.path.join(b, 'CMakeFiles', ver)
# cold
b1 = fresh(f'{name}_cold_'); cold, dcold, ok1 = configure(src, b1, gen)
# warm (2nd configure of same dir)
configure(src, b1, gen); warm, dwarm, _ = configure(src, b1, gen)
# seeded: publish from the cold build, inject the warm state into a fresh dir
seed = fresh(f'{name}_seed_'); cc.publish(seed, cf(b1))
b2 = fresh(f'{name}_seeded_'); cc.inject(seed, b2, cf(b2), src)
seeded, dseed, ok2 = configure(src, b2, gen)
for d in (b1, b2, seed): shutil.rmtree(d, ignore_errors=True)
return cold, seeded, warm, dcold, dseed, dwarm, ok1 and ok2


def clone_missing(only):
os.makedirs(REPOS_DIR, exist_ok=True)
repos = []
for line in open(os.path.join(HERE, 'repos.txt'), encoding='utf-8'):
line = line.strip()
if not line or line.startswith('#'): continue
parts = line.split()
name, url = parts[0], parts[1]
if only and name not in only: continue
branch = parts[2] if len(parts) > 2 else None
subdir = parts[3] if len(parts) > 3 else ''
dest = os.path.join(REPOS_DIR, name)
if not os.path.isdir(dest):
args = ['git', 'clone', '--depth=1', '--recurse-submodules']
if branch: args += ['--branch', branch]
print(f'cloning {name} ...')
if sh(args + [url, dest]).returncode != 0:
print(f' skip {name} (clone failed - private/auth?)'); continue
repos.append((name, os.path.join(dest, subdir) if subdir else dest))
return repos


def main():
only = set(sys.argv[1:])
gen, ver = vs_generator(), cmake_ver()
print(f'generator: {gen or "(default)"} cmake: {ver}\n')
print(f'{"repo":<12}{"cold":>7}{"seeded":>8}{"warm":>7} {"cold->seeded":>13}{"cold->warm":>12} detect c/s/w')
for name, src in clone_missing(only):
if not os.path.exists(os.path.join(src, 'CMakeLists.txt')):
print(f'{name:<12} no CMakeLists.txt at {src}'); continue
cold, seeded, warm, dc, ds, dw, ok = bench_repo(name, src, gen, ver)
pct = lambda a, b: f'{(1 - b / a) * 100:4.0f}%' if a else ' -'
print(f'{name:<12}{cold:6.1f}s{seeded:7.1f}s{warm:6.1f}s {pct(cold, seeded):>13}{pct(cold, warm):>12}'
f' {dc}/{ds}/{dw}{"" if ok else " (configure FAILED)"}')


if __name__ == '__main__':
main()
6 changes: 6 additions & 0 deletions bench/repos.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# CMake config-overhead benchmark manifest: one "name url" per line (shallow-cloned into repos/).
# Lines starting with # are ignored. A 3rd field (optional) is the relative subdir holding the
# top CMakeLists.txt. These are public repos, so no auth is needed; private repos would clone via git SSH.
ReCpp https://github.com/RedFox20/ReCpp.git
udp_quality https://github.com/RedFox20/udp_quality.git
googletest https://github.com/RedFox20/googletest.git
1 change: 1 addition & 0 deletions docs/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.cast
Binary file added docs/demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading