perf(coverage): find a line by table lookup, not by rescanning the file (#963)

cbm_refine_regions_with_pp_lines asks cbm_line_is_toplevel_macro_call about
every preprocessor-parsed line in a region. That check walked the source from
byte 0 to find the line, so the refinement cost bytes times lines — on exactly
the whole-file-ERROR C shape the refinement exists to narrow.

Build one line-offset table per file, and hand the byte span to the macro
check. The line-based form stays for the two callers that ask about a whole
region, where a single walk is cheaper than a table. A file with no table (the
allocation failed) still gets the old walk, so the answer never changes.

Measured on one C file whose file-scope brace is split by an #ifndef, padded to
5k / 10k / 20k / 40k lines. Release build, one machine, seconds:

  lines     main   branch   this commit
   5000     0.98     1.14     0.98
  10000     3.30     4.02     3.41
  20000    11.64    14.77    12.56
  40000    44.04    58.58    46.90

The branch ran 33% over main at 40k lines and now runs 6.5% over. The reported
ranges do not change: 8-8, where main reports the whole file as 5-40000.

A profile of the 40k run no longer reaches the macro check. What is left is
cbm_line_contains and cbm_span_contains_callable_def, which main carries too
and this branch does not touch. The fixture holds 40k definitions, which makes
those two cost more here than on a smaller file.

parse_coverage, extraction, pipeline, mcp and index_resilience: 964 passed,
4 skipped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Joshua Richter <jrichter5781@gmail.com>
This commit is contained in:
Joshua Richter
2026-09-05 16:24:25 -04:00
co-authored by Claude Opus 5
parent f871178b8c
commit b04f54506a
+77 -23
View File
@@ -1208,27 +1208,12 @@ static void cbm_subtract_recovered_regions(cbm_error_regions_t *regs, const CBMD
* missing from the graph; it's a benign call the grammar can't parse without the
* preprocessor. True if the [start_line, end_line] span contains a call `NAME(` to
* a file-defined function-like macro (Macro label + a parameter signature). */
static bool cbm_span_is_macro_invocation(const char *src, int src_len, uint32_t start_line,
uint32_t end_line, const CBMDefArray *defs) {
if (!src || src_len <= 0 || !defs || start_line == 0 || end_line < start_line) {
static bool cbm_byte_span_is_macro_invocation(const char *src, int src_len, int span_start,
int span_end, const CBMDefArray *defs) {
if (!src || src_len <= 0 || !defs || span_start < 0 || span_end > src_len ||
span_start >= span_end) {
return false;
}
int span_start = 0;
uint32_t line = 1;
while (span_start < src_len && line < start_line) {
if (src[span_start++] == '\n') {
line++;
}
}
if (line != start_line) {
return false;
}
int span_end = span_start;
while (span_end < src_len && line <= end_line) {
if (src[span_end++] == '\n') {
line++;
}
}
for (int di = 0; di < defs->count; di++) {
const CBMDefinition *d = &defs->items[di];
/* Function-like macros only: an object-like macro (#define PI 3.14) has no
@@ -1256,6 +1241,66 @@ static bool cbm_span_is_macro_invocation(const char *src, int src_len, uint32_t
return false;
}
/* Byte offset where every 1-based line starts, so finding a line's span costs
* one table read instead of a walk from the start of the file.
*
* The table holds line_count + 2 entries. Entry [L] is where line L starts, and
* the last entry is the end of the source, which gives the final line somewhere
* to stop. A line the file never reaches starts at the end of the source, so its
* span is empty and nothing can match inside it — the same answer the walk gives.
*
* Returns NULL when the allocation fails; a caller then falls back to the walk. */
static int *cbm_build_line_offsets(const char *src, int src_len, uint32_t line_count) {
int *offsets = (int *)malloc(((size_t)line_count + 2) * sizeof(int));
if (!offsets) {
return NULL;
}
for (uint32_t l = 0; l <= line_count + 1; l++) {
offsets[l] = src_len;
}
offsets[0] = 0;
offsets[1] = 0;
uint32_t line = 1;
for (int i = 0; i < src_len; i++) {
if (src[i] != '\n') {
continue;
}
line++;
if (line > line_count + 1) {
break;
}
offsets[line] = i + 1;
}
return offsets;
}
/* Same question by line number, for the few callers that ask about one region
* rather than every line of a file. This form walks the source to find the span,
* which is why the per-line caller below uses a table instead. */
static bool cbm_span_is_macro_invocation(const char *src, int src_len, uint32_t start_line,
uint32_t end_line, const CBMDefArray *defs) {
if (!src || src_len <= 0 || !defs || start_line == 0 || end_line < start_line) {
return false;
}
int span_start = 0;
uint32_t line = 1;
while (span_start < src_len && line < start_line) {
if (src[span_start++] == '\n') {
line++;
}
}
if (line != start_line) {
return false;
}
int span_end = span_start;
while (span_end < src_len && line <= end_line) {
if (src[span_end++] == '\n') {
line++;
}
}
return cbm_byte_span_is_macro_invocation(src, src_len, span_start, span_end, defs);
}
/* True if [rs, re] is fully enclosed by an extracted callable definition (a
* Function/Method body). A macro invocation INSIDE a real function body is an
* expression-level use where nothing is missing (#1071). A TOP-LEVEL invocation
@@ -1327,9 +1372,12 @@ static void cbm_push_trimmed_run(cbm_error_regions_t *out, uint32_t start, uint3
* An invocation INSIDE a function body is the benign #1071 case and is left
* alone here — cbm_subtract_macro_invocation_regions handles it later. */
static bool cbm_line_is_toplevel_macro_call(const char *src, int src_len, uint32_t line,
const CBMDefArray *defs) {
return cbm_span_is_macro_invocation(src, src_len, line, line, defs) &&
!cbm_region_inside_callable(line, line, defs);
const int *line_offsets, const CBMDefArray *defs) {
bool is_call = line_offsets
? cbm_byte_span_is_macro_invocation(src, src_len, line_offsets[line],
line_offsets[line + 1], defs)
: cbm_span_is_macro_invocation(src, src_len, line, line, defs);
return is_call && !cbm_region_inside_callable(line, line, defs);
}
/* Cut every raw region down to the lines the preprocessed parse could not
@@ -1344,13 +1392,18 @@ static void cbm_refine_regions_with_pp_lines(cbm_error_regions_t *regs, const ui
uint32_t line_count, const char *src, int src_len,
const CBMDefArray *defs) {
cbm_error_regions_t out = {{0}, {0}, 0, regs->dropped};
/* One offset table for the whole file. The macro check below runs once per
* line, and without the table each of those calls walks the source from byte
* 0 to find its line — bytes times lines, on exactly the whole-file-error
* shape this refinement exists to narrow. */
int *line_offsets = cbm_build_line_offsets(src, src_len, line_count);
for (int i = 0; i < regs->count; i++) {
uint32_t run_start = 0;
uint32_t run_end = 0;
uint32_t end = regs->ends[i] < line_count ? regs->ends[i] : line_count;
for (uint32_t line = regs->starts[i]; line <= end; line++) {
if ((map[line] & CBM_LINE_PP_PARSED) &&
!cbm_line_is_toplevel_macro_call(src, src_len, line, defs)) {
!cbm_line_is_toplevel_macro_call(src, src_len, line, line_offsets, defs)) {
if (run_start != 0) {
cbm_push_trimmed_run(&out, run_start, run_end, map, line_count);
run_start = 0;
@@ -1366,6 +1419,7 @@ static void cbm_refine_regions_with_pp_lines(cbm_error_regions_t *regs, const ui
cbm_push_trimmed_run(&out, run_start, run_end, map, line_count);
}
}
free(line_offsets);
*regs = out;
}