Files
Sprinter-SDCC/toolchain/size_check.py
T

160 lines
7.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
size_check.py — размерный регресс-тест: сравнение _CODE тестов с эталоном
docs/size_baseline.tsv.
Использование:
make size-check # собрать обязательные тесты и сверить с эталоном
make size-baseline # принять свежие размеры, сохранив необязательные
Логика: собираем _CODE из свежей .map каждой программы под tests/, сравниваем с
эталоном. Карты, оставшиеся от другой цели или более старые, чем входы
сборки, пропускаются. Любое отличие печатается; выход 1 — если есть РОСТ (случайное
разжирение libc), 0 — если только уменьшения/без изменений. Новые
программы (нет в эталоне) — предупреждение, добавить через --update.
--update заменяет размеры собранных тестов, сохраняя эталоны необязательных
тестов, которые сейчас не собирались.
Прямой запуск скрипта рассчитан на уже собранные тесты; Make-цели сами
собирают обязательный набор и передают его через --require.
ТОЛЬКО tests/, без отдельного репозитория Examples: регресс ловит разжирение libc, а для этого
хватает мелких тестов — каждый тянет свой кусок библиотеки и пересобирается
за секунды. Крупные приложения (`../Examples/mdview` компилируется минутами)
не показывают ничего сверх этого и лишь удлиняют цикл, поэтому и из
`make all` они убраны (см. корневой Makefile).
"""
import argparse
import glob
import os
from pathlib import Path
import re
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
BASELINE = os.path.join(ROOT, "docs", "size_baseline.tsv")
def collect():
"""Имя программы → размер _CODE (байт) по свежим .map своей цели."""
sizes = {}
skipped = set()
pats = [
os.path.join(ROOT, "tests", "*", ".sprinter-cc-*", "*.map"),
]
for pat in pats:
for map_path in glob.glob(pat):
name = os.path.splitext(os.path.basename(map_path))[0]
# .map должен лежать в своей папке .sprinter-cc-<name>
if os.path.basename(os.path.dirname(map_path)) != ".sprinter-cc-" + name:
continue
project = Path(map_path).parents[1]
makefile = project / "Makefile"
if not makefile.is_file():
continue
declared = re.search(r"^EXAMPLE\s*[:?+]?=\s*(\S+)",
makefile.read_text(), re.M)
if not declared or declared.group(1) != name:
# Например, старый .sprinter-cc-w3bankgfx рядом с w3bgfx.
continue
flags = makefile.read_text()
safe = "--safe" in flags
gfx = "--gfx" in flags
variant = "_safe" if safe else ""
# app.mk сам по себе не является входом линкера: его изменения
# учитываются сборкой через sdbg-config и CC_FLAGS. Проверяем
# только реальные входы, иначе правка run-цели объявит все карты
# устаревшими, хотя размер программы не менялся.
dependencies = [makefile, Path(ROOT) / "bin/sprinter-cc",
Path(ROOT) / "toolchain/mkexe/mkexe",
Path(ROOT) / f"lib/sprinter{variant}.lib"]
if gfx:
dependencies.append(Path(ROOT) / f"lib/bgi256{variant}.lib")
dependencies.extend(project.glob("*.c"))
dependencies.extend(project.glob("*.h"))
dependencies.extend((Path(ROOT) / "runtime").glob("*.s"))
if any(not path.is_file() or
os.path.getmtime(map_path) + 1 < path.stat().st_mtime
for path in dependencies):
skipped.add(name)
continue
with open(map_path, encoding="utf-8", errors="replace") as f:
for line in f:
m = re.match(r"^_CODE\s+([0-9A-Fa-f]+)\s+([0-9A-Fa-f]+)", line)
if m:
sizes[name] = int(m.group(2), 16)
break
return sizes, skipped
def load_baseline():
base = {}
if os.path.exists(BASELINE):
with open(BASELINE) as f:
for line in f:
line = line.strip()
if not line or line.startswith("#"):
continue
name, size = line.split("\t")
base[name] = int(size)
return base
def save_baseline(sizes):
accepted = load_baseline()
accepted.update(sizes)
with open(BASELINE, "w") as f:
f.write("# Эталон размеров _CODE (байт); обновление: "
"make size-baseline\n")
for name in sorted(accepted):
f.write(f"{name}\t{accepted[name]}\n")
def main():
parser = argparse.ArgumentParser(description="Проверка размера _CODE SDK-тестов")
parser.add_argument("--update", action="store_true")
parser.add_argument("--require", nargs="*", default=[], metavar="TEST")
args = parser.parse_args()
sizes, skipped = collect()
missing = set(args.require) - sizes.keys()
if missing:
print("size_check: нет свежей карты для обязательных тестов: " +
", ".join(sorted(missing)))
return 2
if not sizes:
print("size_check: свежие .map не найдены — сначала `make all`")
return 2
if args.update:
save_baseline(sizes)
print(f"size_check: эталон обновлён ({len(sizes)} свежих программ) → "
f"{os.path.relpath(BASELINE, ROOT)}")
return 0
base = load_baseline()
if not base:
print("size_check: эталона нет — создать: size_check.py --update")
return 2
grew = 0
for name in sorted(sizes):
new, old = sizes.get(name), base.get(name)
if old is None:
print(f" {name}: НОВЫЙ ({new} Б) — добавить в эталон (--update)")
elif new != old:
mark = "РОСТ" if new > old else "ок, меньше"
print(f" {name}: {old} -> {new} ({new - old:+d} Б) [{mark}]")
if new > old:
grew += 1
if skipped:
print(f"size_check: {len(skipped)} устаревших карт пропущено: " +
", ".join(sorted(skipped)))
if grew:
print(f"size_check: {grew} программ(ы) ВЫРОСЛИ — проверить правки libc")
return 1
print(f"size_check: OK ({len(sizes)} программ, роста нет)")
return 0
if __name__ == "__main__":
raise SystemExit(main())