Skip to content

Commit 200afcb

Browse files
committed
refactor: optimize git history precomputation, permit empty build files, and add thread-safety to PR ingestion
1 parent 9d854f6 commit 200afcb

14 files changed

Lines changed: 914 additions & 147 deletions

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,6 @@ outputs/crossrepo/global_symbols.sqlite
8585
outputs/crossrepo/global_symbols.sqlite-shm
8686
outputs/crossrepo/global_symbols.sqlite-wal
8787
outputs/reindexed_commits/*
88+
outputs/non_strict_backup_20260626_165907/*
89+
outputs/profiles/*
90+
outputs/reindexed_pr/*

scripts/nanochat_data/extract_git_history.py

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -285,22 +285,39 @@ def compute_file_local_commit_indices(
285285
repo_path: str,
286286
commit_hashes: list[str],
287287
) -> dict[tuple[str, str], int]:
288+
indices, _files_by_commit = precompute_cpp_file_changes(repo_path, commit_hashes)
289+
return indices
290+
291+
292+
def precompute_cpp_file_changes(
293+
repo_path: str,
294+
commit_hashes: list[str],
295+
) -> tuple[dict[tuple[str, str], int], dict[str, list[str]]]:
296+
"""Precompute changed C/C++ files and per-file temporal indices once.
297+
298+
This is on the hot path for large repos. The old flow called
299+
``git diff-tree --name-status`` once while building ``file_local_commit_index``
300+
and then again inside ``get_commit_diffs`` for every commit. Keep the same
301+
semantics, but carry the file list forward so each commit pays that git
302+
subprocess cost only once.
303+
"""
288304
counters: dict[str, int] = {}
289305
indices: dict[tuple[str, str], int] = {}
306+
files_by_commit: dict[str, list[str]] = {}
290307
for commit_hash in reversed(commit_hashes):
291-
file_diffs = get_commit_diffs(repo_path, commit_hash)
292-
if not file_diffs:
308+
files = get_commit_cpp_files(repo_path, commit_hash)
309+
if not files:
293310
continue
294-
for item in file_diffs:
295-
filepath = item["filepath"]
311+
files_by_commit[commit_hash] = files
312+
for filepath in files:
296313
next_index = counters.get(filepath, 0)
297314
indices[(commit_hash, filepath)] = next_index
298315
counters[filepath] = next_index + 1
299-
return indices
316+
return indices, files_by_commit
300317

301318

302-
def get_commit_diffs(repo_path: str, commit_hash: str) -> Optional[list[dict]]:
303-
"""Get per-file diffs for C/C++ files in a commit."""
319+
def get_commit_cpp_files(repo_path: str, commit_hash: str) -> Optional[list[str]]:
320+
"""Return modified C/C++ file paths for a commit without reading blobs/diffs."""
304321
name_status = run_git(
305322
repo_path, ["diff-tree", "--no-commit-id", "-r", "--name-status", commit_hash]
306323
)
@@ -326,7 +343,19 @@ def get_commit_diffs(repo_path: str, commit_hash: str) -> Optional[list[dict]]:
326343

327344
if not files or len(files) > MAX_FILES_PER_COMMIT:
328345
return None
346+
return files
347+
329348

349+
def get_commit_diffs(
350+
repo_path: str,
351+
commit_hash: str,
352+
files: Optional[list[str]] = None,
353+
) -> Optional[list[dict]]:
354+
"""Get per-file diffs for C/C++ files in a commit."""
355+
if files is None:
356+
files = get_commit_cpp_files(repo_path, commit_hash)
357+
if not files:
358+
return None
330359
results = []
331360
for filepath in files:
332361
old_content = run_git(
@@ -427,7 +456,9 @@ def process_repo(
427456
print(f" [{repo_name}] No commits found")
428457
return stats
429458

430-
file_local_commit_indices = compute_file_local_commit_indices(repo_path, commits)
459+
file_local_commit_indices, cpp_files_by_commit = precompute_cpp_file_changes(
460+
repo_path, commits
461+
)
431462

432463
for i, commit_hash in enumerate(commits):
433464
if i > 0 and i % 1000 == 0:
@@ -459,7 +490,11 @@ def process_repo(
459490
):
460491
continue
461492

462-
file_diffs = get_commit_diffs(repo_path, commit_hash)
493+
file_diffs = get_commit_diffs(
494+
repo_path,
495+
commit_hash,
496+
files=cpp_files_by_commit.get(commit_hash),
497+
)
463498
if not file_diffs:
464499
continue
465500

scripts/pr_ingest/export_pr_parquet.py

Lines changed: 166 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
99
The output is intentionally compatible with the existing conveyor layout:
1010
11-
outputs/reindexed_pr/{1024,2048,4096,...}/pr_discussions_<offset>.parquet
11+
outputs/reindexed_pr/{1024,2048,4096,...}/pr_discussions_<repo>_<offset>.parquet
1212
1313
RULE #1: every stage uses the existing materializer/packer path and raises on
1414
missing input, empty output, or malformed store rows. There is no fallback path.
@@ -21,6 +21,7 @@
2121
import sqlite3
2222
import sys
2323
import tempfile
24+
import time
2425
from pathlib import Path
2526

2627
REPO_ROOT = Path(__file__).resolve().parents[2]
@@ -37,6 +38,7 @@
3738

3839
DEFAULT_STORE = REPO_ROOT / "outputs" / "pr_ingest" / "prs.sqlite"
3940
DEFAULT_OUTPUT_ROOT = REPO_ROOT / "outputs" / "reindexed_pr"
41+
DEFAULT_MANIFEST = DEFAULT_OUTPUT_ROOT / "_done.json"
4042
ZSTD_LEVELS = (1024, 2048, 4096, 8192, 16384)
4143

4244

@@ -79,6 +81,24 @@ def _iter_pr_keys(
7981
yield from conn.execute(sql, params)
8082

8183

84+
def _count_pr_keys(
85+
conn: sqlite3.Connection,
86+
*,
87+
repo: str | None,
88+
offset: int,
89+
limit: int | None,
90+
) -> int:
91+
sql = "SELECT COUNT(*) AS n FROM (SELECT 1 FROM prs"
92+
params: list[object] = []
93+
if repo:
94+
sql += " WHERE repo=?"
95+
params.append(repo)
96+
sql += " ORDER BY repo, pr_number LIMIT ? OFFSET ?)"
97+
params.append(-1 if limit is None else int(limit))
98+
params.append(int(offset))
99+
return int(conn.execute(sql, params).fetchone()["n"])
100+
101+
82102
def _write_pr_jsonl(
83103
conn: sqlite3.Connection,
84104
out_jsonl: Path,
@@ -136,6 +156,29 @@ def _append_output(shard_name: str, packed: Path, target_length: int, output_roo
136156
return sr._parquet_stats(dest, target_length)
137157

138158

159+
def _repo_slug(repo: str | None) -> str:
160+
if not repo:
161+
return "all"
162+
return repo.replace("/", "__").replace(":", "_")
163+
164+
165+
def _shard_name(repo: str | None, offset: int) -> str:
166+
return f"pr_discussions_{_repo_slug(repo)}_{int(offset):08d}"
167+
168+
169+
def _load_manifest(path: Path) -> dict:
170+
if not path.exists():
171+
return {"done": {}, "failed": {}}
172+
return json.loads(path.read_text(encoding="utf-8"))
173+
174+
175+
def _save_manifest(path: Path, manifest: dict) -> None:
176+
path.parent.mkdir(parents=True, exist_ok=True)
177+
tmp = path.with_suffix(".json.tmp")
178+
tmp.write_text(json.dumps(manifest, indent=2, sort_keys=True), encoding="utf-8")
179+
tmp.replace(path)
180+
181+
139182
def export_pr_parquet(args: argparse.Namespace) -> dict:
140183
store = Path(args.store)
141184
if not store.exists():
@@ -146,32 +189,35 @@ def export_pr_parquet(args: argparse.Namespace) -> dict:
146189
raise ValueError("--target-lengths produced no lengths")
147190

148191
conn = connect(str(store), create=False)
149-
shard_name = f"pr_discussions_{int(args.offset):08d}"
150-
with tempfile.TemporaryDirectory(prefix="pr_parquet_export_") as tmp:
151-
work = Path(tmp)
152-
jsonl = work / f"{shard_name}.jsonl"
153-
n_docs = _write_pr_jsonl(
154-
conn,
155-
jsonl,
156-
repo=args.repo,
157-
offset=int(args.offset),
158-
limit=args.limit,
159-
)
160-
tok = sr.stage_materialize(
161-
shard_name,
162-
jsonl,
163-
work,
164-
memory_limit_gb=float(args.memory_limit_gb),
165-
)
166-
routed = route_by_fit(tok, lengths, work / "routed")
167-
if not routed:
168-
raise RuntimeError(f"no PR docs routed for {jsonl}")
169-
per_length: dict[str, dict] = {}
170-
for length, route_parquet in sorted(routed.items()):
171-
packed = sr.stage_pack(shard_name, route_parquet, length, work)
172-
per_length[str(length)] = _append_output(
173-
shard_name, packed, length, output_root,
192+
try:
193+
shard_name = _shard_name(args.repo, int(args.offset))
194+
with tempfile.TemporaryDirectory(prefix="pr_parquet_export_") as tmp:
195+
work = Path(tmp)
196+
jsonl = work / f"{shard_name}.jsonl"
197+
n_docs = _write_pr_jsonl(
198+
conn,
199+
jsonl,
200+
repo=args.repo,
201+
offset=int(args.offset),
202+
limit=args.limit,
174203
)
204+
tok = sr.stage_materialize(
205+
shard_name,
206+
jsonl,
207+
work,
208+
memory_limit_gb=float(args.memory_limit_gb),
209+
)
210+
routed = route_by_fit(tok, lengths, work / "routed")
211+
if not routed:
212+
raise RuntimeError(f"no PR docs routed for {jsonl}")
213+
per_length: dict[str, dict] = {}
214+
for length, route_parquet in sorted(routed.items()):
215+
packed = sr.stage_pack(shard_name, route_parquet, length, work)
216+
per_length[str(length)] = _append_output(
217+
shard_name, packed, length, output_root,
218+
)
219+
finally:
220+
conn.close()
175221
return {
176222
"source": "pr",
177223
"store": str(store),
@@ -182,6 +228,88 @@ def export_pr_parquet(args: argparse.Namespace) -> dict:
182228
}
183229

184230

231+
def export_pr_parquet_batches(args: argparse.Namespace) -> dict:
232+
store = Path(args.store)
233+
if not store.exists():
234+
raise FileNotFoundError(f"--store does not exist: {store}")
235+
manifest_path = Path(args.manifest)
236+
manifest = _load_manifest(manifest_path)
237+
resume = not args.no_resume
238+
batch_size = int(args.batch_size)
239+
if batch_size <= 0:
240+
raise ValueError("--batch-size must be > 0")
241+
242+
conn = connect(str(store), create=False)
243+
try:
244+
offset = int(args.offset)
245+
max_shards = args.max_shards
246+
shards: list[dict] = []
247+
totals_by_length: dict[str, dict[str, int]] = {}
248+
while True:
249+
shard_name = _shard_name(args.repo, offset)
250+
done_key = f"{_repo_slug(args.repo)}:{offset}"
251+
n_keys = _count_pr_keys(
252+
conn,
253+
repo=args.repo,
254+
offset=offset,
255+
limit=batch_size,
256+
)
257+
if n_keys == 0:
258+
break
259+
if resume and done_key in manifest.get("done", {}):
260+
info = manifest["done"][done_key]
261+
shards.append({"shard": shard_name, "skipped": True, **info})
262+
else:
263+
shard_args = argparse.Namespace(**vars(args))
264+
shard_args.limit = batch_size
265+
shard_args.offset = offset
266+
try:
267+
info = export_pr_parquet(shard_args)
268+
except Exception as exc:
269+
manifest.setdefault("failed", {})[done_key] = {
270+
"offset": offset,
271+
"limit": batch_size,
272+
"repo": args.repo,
273+
"stage": "export_pr_parquet",
274+
"detail": str(exc)[:2000],
275+
"ts": time.strftime("%Y-%m-%dT%H:%M:%S"),
276+
}
277+
_save_manifest(manifest_path, manifest)
278+
raise
279+
manifest.setdefault("done", {})[done_key] = info
280+
manifest.get("failed", {}).pop(done_key, None)
281+
_save_manifest(manifest_path, manifest)
282+
shards.append(info)
283+
284+
last = shards[-1]
285+
for length, st in last.get("lengths", {}).items():
286+
agg = totals_by_length.setdefault(
287+
length,
288+
{"rows": 0, "valid_tokens": 0, "pad_tokens": 0, "capacity_tokens": 0},
289+
)
290+
for key in ("rows", "valid_tokens", "pad_tokens", "capacity_tokens"):
291+
agg[key] += int(st.get(key, 0))
292+
293+
offset += batch_size
294+
if max_shards is not None and len(shards) >= int(max_shards):
295+
break
296+
return {
297+
"source": "pr",
298+
"store": str(store),
299+
"output_root": str(Path(args.output_root)),
300+
"manifest": str(manifest_path),
301+
"repo": args.repo,
302+
"start_offset": int(args.offset),
303+
"batch_size": batch_size,
304+
"next_offset": offset,
305+
"shards": shards,
306+
"n_shards": len(shards),
307+
"lengths": totals_by_length,
308+
}
309+
finally:
310+
conn.close()
311+
312+
185313
def parse_args(argv: list[str]) -> argparse.Namespace:
186314
p = argparse.ArgumentParser(description=__doc__)
187315
p.add_argument("--store", default=str(DEFAULT_STORE))
@@ -190,13 +318,24 @@ def parse_args(argv: list[str]) -> argparse.Namespace:
190318
p.add_argument("--repo", default=None, help="Optional owner/repo filter.")
191319
p.add_argument("--offset", type=int, default=0)
192320
p.add_argument("--limit", type=int, default=10_000)
321+
p.add_argument("--all", action="store_true",
322+
help="Export every PR row from --offset in resumable batches.")
323+
p.add_argument("--batch-size", type=int, default=10_000,
324+
help="Rows per shard when --all is set.")
325+
p.add_argument("--max-shards", type=int, default=None,
326+
help="Optional cap on number of shards exported in this run.")
327+
p.add_argument("--manifest", default=str(DEFAULT_MANIFEST),
328+
help="Resume manifest for --all batched export.")
329+
p.add_argument("--no-resume", action="store_true",
330+
help="Ignore completed PR export shards in --manifest.")
193331
p.add_argument("--memory-limit-gb", type=float, default=10.0)
194332
return p.parse_args(argv)
195333

196334

197335
def main(argv: list[str]) -> int:
198336
args = parse_args(argv)
199-
print(json.dumps(export_pr_parquet(args), indent=2, sort_keys=True))
337+
result = export_pr_parquet_batches(args) if args.all else export_pr_parquet(args)
338+
print(json.dumps(result, indent=2, sort_keys=True))
200339
return 0
201340

202341

0 commit comments

Comments
 (0)