feat(scan): size portions from measured per-answer cost and isolate the kill culprit

The 7.8 trace showed the completion loss was a sizer death spiral, not
expensive families: after two kills the budget fell to 3 sink visits and
never recovered, because portions finishing in 25-30 s sat under an assumed
70 s fixed start and read as "no information", so the budget could only
shrink.

- taint.sc streams each answer's own cost (`ms`); the backend carries it as a
  reserved `__timing__` list through shards, partitions and reuse.
- The sizer budgets predicted seconds from per-family seconds-per-visit rates
  learned from those costs, and measures the fixed start as the wall time the
  answers do not explain (median), instead of assuming it.
- A kill names its culprit: the first unanswered id in request order. The ids
  behind it never started and are re-queued at once; the culprit is asked
  alone after the rest. One that still fails alone after more than a
  portion's target is reported as `query_too_expensive`, apart from
  `query_failed`.
- Portions grow id by id from the queue head, and a single-portion scan now
  goes through the queue too, so a kill there re-asks its remainder.
- Fix: the reuse wrapper wrapped only `run_batch`, so since 7.8 every
  portioned query bypassed the graph-reuse cache and its source-root
  normalization. `run_batch_once` is wrapped too.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
This commit is contained in:
Marius Ciepluch
2026-09-23 07:55:33 +02:00
co-authored by Claude Opus 5.5
parent c1a689f4b3
commit 1b28465a61
10 changed files with 427 additions and 143 deletions
+4 -1
View File
@@ -11,7 +11,7 @@ from dataclasses import dataclass
from pathlib import Path
from typing import Any
from ..cpg.backend import _DeadlineExpired, _remove_owned_tree
from ..cpg.backend import TIMING, _DeadlineExpired, _remove_owned_tree
from ..preprocess import LANGUAGE_BY_EXTENSION
from .contracts import ExecutionBudget
from .layout import is_test_path, is_vendored, layout_facts
@@ -125,6 +125,9 @@ class PartitionGraph:
for rid, rows in raw.items():
if rid in selected and isinstance(rows, list):
answers[rid] = self._normalize(rows)
elif rid == TIMING and isinstance(rows, list):
# Carried, not normalized: it names request ids and costs, never a path.
answers.setdefault(TIMING, []).extend(rows)
elif rid == "__census__" and isinstance(rows, list):
expected = set(next(p.paths for p in self.partitions if p.language == language))
normalized = self._normalize(rows)