193 lines
6.0 KiB
Python
Executable File
193 lines
6.0 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Lightweight repo performance profiling helper (stdlib only)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
from typing import Dict, Iterable, List, Tuple
|
|
|
|
EXT_WEIGHTS = {
|
|
".js": 1.0,
|
|
".jsx": 1.0,
|
|
".ts": 1.0,
|
|
".tsx": 1.0,
|
|
".css": 0.7,
|
|
".map": 2.0,
|
|
}
|
|
|
|
|
|
def iter_files(root: Path) -> Iterable[Path]:
|
|
for dirpath, dirnames, filenames in os.walk(root):
|
|
dirnames[:] = [d for d in dirnames if d not in {".git", "node_modules", ".next", "dist", "build", "coverage", "__pycache__"}]
|
|
for filename in filenames:
|
|
path = Path(dirpath) / filename
|
|
if path.is_file():
|
|
yield path
|
|
|
|
|
|
def get_large_files(root: Path, threshold_bytes: int) -> List[Tuple[str, int]]:
|
|
large: List[Tuple[str, int]] = []
|
|
for file_path in iter_files(root):
|
|
size = file_path.stat().st_size
|
|
if size >= threshold_bytes:
|
|
large.append((str(file_path.relative_to(root)), size))
|
|
return sorted(large, key=lambda item: item[1], reverse=True)
|
|
|
|
|
|
def count_dependencies(root: Path) -> Dict[str, int]:
|
|
counts = {"node_dependencies": 0, "python_dependencies": 0, "go_dependencies": 0}
|
|
|
|
package_json = root / "package.json"
|
|
if package_json.exists():
|
|
try:
|
|
data = json.loads(package_json.read_text(encoding="utf-8"))
|
|
deps = data.get("dependencies", {})
|
|
dev_deps = data.get("devDependencies", {})
|
|
counts["node_dependencies"] = len(deps) + len(dev_deps)
|
|
except Exception:
|
|
pass
|
|
|
|
requirements = root / "requirements.txt"
|
|
if requirements.exists():
|
|
lines = [ln.strip() for ln in requirements.read_text(encoding="utf-8", errors="ignore").splitlines()]
|
|
counts["python_dependencies"] = sum(1 for ln in lines if ln and not ln.startswith("#"))
|
|
|
|
go_mod = root / "go.mod"
|
|
if go_mod.exists():
|
|
lines = go_mod.read_text(encoding="utf-8", errors="ignore").splitlines()
|
|
in_require_block = False
|
|
go_count = 0
|
|
for ln in lines:
|
|
s = ln.strip()
|
|
if s.startswith("require ("):
|
|
in_require_block = True
|
|
continue
|
|
if in_require_block and s == ")":
|
|
in_require_block = False
|
|
continue
|
|
if in_require_block and s and not s.startswith("//"):
|
|
go_count += 1
|
|
elif s.startswith("require ") and not s.endswith("("):
|
|
go_count += 1
|
|
counts["go_dependencies"] = go_count
|
|
|
|
return counts
|
|
|
|
|
|
def bundle_indicators(root: Path) -> Dict[str, object]:
|
|
indicators = {
|
|
"build_dirs_present": [],
|
|
"bundle_like_files": 0,
|
|
"estimated_bundle_weight": 0.0,
|
|
}
|
|
for d in ["dist", "build", ".next", "out"]:
|
|
if (root / d).exists():
|
|
indicators["build_dirs_present"].append(d)
|
|
|
|
bundle_files = 0
|
|
weight = 0.0
|
|
for path in iter_files(root):
|
|
ext = path.suffix.lower()
|
|
if ext in EXT_WEIGHTS:
|
|
bundle_files += 1
|
|
size_kb = path.stat().st_size / 1024.0
|
|
weight += size_kb * EXT_WEIGHTS[ext]
|
|
|
|
indicators["bundle_like_files"] = bundle_files
|
|
indicators["estimated_bundle_weight"] = round(weight, 2)
|
|
return indicators
|
|
|
|
|
|
def format_size(num_bytes: int) -> str:
|
|
units = ["B", "KB", "MB", "GB"]
|
|
value = float(num_bytes)
|
|
for unit in units:
|
|
if value < 1024.0 or unit == units[-1]:
|
|
return f"{value:.1f}{unit}"
|
|
value /= 1024.0
|
|
return f"{num_bytes}B"
|
|
|
|
|
|
def build_report(root: Path, threshold_bytes: int) -> Dict[str, object]:
|
|
large = get_large_files(root, threshold_bytes)
|
|
deps = count_dependencies(root)
|
|
bundles = bundle_indicators(root)
|
|
return {
|
|
"root": str(root),
|
|
"large_file_threshold_bytes": threshold_bytes,
|
|
"large_files": large,
|
|
"dependency_counts": deps,
|
|
"bundle_indicators": bundles,
|
|
}
|
|
|
|
|
|
def print_text(report: Dict[str, object]) -> None:
|
|
print("Performance Profile Report")
|
|
print(f"Root: {report['root']}")
|
|
print(f"Large-file threshold: {format_size(int(report['large_file_threshold_bytes']))}")
|
|
print("")
|
|
|
|
dep_counts = report["dependency_counts"]
|
|
print("Dependency Counts")
|
|
print(f"- Node: {dep_counts['node_dependencies']}")
|
|
print(f"- Python: {dep_counts['python_dependencies']}")
|
|
print(f"- Go: {dep_counts['go_dependencies']}")
|
|
print("")
|
|
|
|
bundle = report["bundle_indicators"]
|
|
print("Bundle Indicators")
|
|
print(f"- Build directories present: {', '.join(bundle['build_dirs_present']) or 'none'}")
|
|
print(f"- Bundle-like files: {bundle['bundle_like_files']}")
|
|
print(f"- Estimated weighted bundle size: {bundle['estimated_bundle_weight']} KB")
|
|
print("")
|
|
|
|
print("Large Files")
|
|
large_files = report["large_files"]
|
|
if not large_files:
|
|
print("- None above threshold")
|
|
else:
|
|
for rel_path, size in large_files[:20]:
|
|
print(f"- {rel_path}: {format_size(size)}")
|
|
|
|
|
|
def parse_args() -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(
|
|
description="Analyze a project directory for common performance risk indicators."
|
|
)
|
|
parser.add_argument("path", help="Directory to analyze")
|
|
parser.add_argument(
|
|
"--large-file-threshold-kb",
|
|
type=int,
|
|
default=512,
|
|
help="Threshold in KB for reporting large files (default: 512)",
|
|
)
|
|
parser.add_argument(
|
|
"--json",
|
|
action="store_true",
|
|
help="Print JSON output instead of text",
|
|
)
|
|
return parser.parse_args()
|
|
|
|
|
|
def main() -> int:
|
|
args = parse_args()
|
|
root = Path(args.path).expanduser().resolve()
|
|
if not root.exists() or not root.is_dir():
|
|
raise SystemExit(f"Path is not a directory: {root}")
|
|
|
|
threshold = max(1, args.large_file_threshold_kb) * 1024
|
|
report = build_report(root, threshold)
|
|
|
|
if args.json:
|
|
print(json.dumps(report, indent=2))
|
|
else:
|
|
print_text(report)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|