DEV Community

Finley Zhou
Finley Zhou

Posted on

Frequency Beats Order: A Token-Saving Triage for C++ Build Logs

Frequency Beats Order: A Token-Saving Triage for C++ Build Logs

A build fails. The log has 9,400 lines. The model window has 8,000 tokens. Sending all of it is impossible. Sending the head is a guess. Sending the tail is a guess too.

This article shows a triage that counts first. It turns a firehose into a frequency table. The table fits in a small prompt. The model answers with a root-cause hypothesis.

Most C++ failures are not a story. They are an echo. The same template error repeats for every instantiation. The same missing symbol appears in every translation unit. Order tells you where the compiler stopped. Frequency tells you why it stopped. Frequency is the signal. Order is mostly noise.

Why C++ logs are chatty

A C++ compiler processes each translation unit separately. A header error appears once per included file. A template error appears once per instantiation. A missing symbol appears once per object file. The compiler repeats itself by design.

That repetition is not a bug. It is a diagnostic feature. It tells you how widespread the failure is. The problem is the format. The format hides the count behind thousands of near-identical lines.

Counting exposes what the compiler already knew. That is the whole trick.

Why count first

The classic approach keeps the first N lines. That works when the first error is the root cause. Often it is not. A missing include can trigger 200 secondary errors. The first line points to a placeholder. The count points to the real problem.

Counting has a second benefit. It shrinks the prompt hard. A 9,400-line log becomes 12 signatures. Each signature carries a count, a first line, and a last line. That is enough for a small model to reason about scale.

Where the budget model fits

This workflow is cheap by design. You recompile on a free server. You paste the table into a free-model chat window. MonkeyCode's free server and free models make this loop practical for a solo developer. Disclosure: This article was prepared as part of MonkeyCode's product outreach.

The total context is under 1,000 tokens. The model sees frequencies, not a transcript. It can rank two hypotheses: the repeated error is either a root cause or a symptom.

Step 1: Normalize a line into a signature

First, normalize each error line. Strip the absolute path. Replace numbers with a placeholder. Collapse template arguments. The goal is a signature that survives repetition. Two lines with different offsets can share one signature.

import re

def signature(line):
    line = re.sub(r'/[^\s:]+/', '/…/', line)
    line = re.sub(r'\b\d+\b', '#', line)
    line = re.sub(r'<[^>]*>', '<…>', line)
    return line.strip()
Enter fullscreen mode Exit fullscreen mode

The three replacements are conservative. They keep the semantic core. They remove the parts that change between copies.

Step 2: Count first and last occurrences

Second, count signatures. Keep the first and last sample line for each signature. Keep the file and line number from the first sample. Drop everything else.

A signature with 40 occurrences is a systemic failure. A signature with one occurrence is a local failure. The model needs both to rank hypotheses.

The triage script

Here is the full script. It reads from stdin. It writes a ranked list to stdout.

#!/usr/bin/env python3
"""triage_log.py: turn a build log into a frequency table."""
import sys, re
from collections import OrderedDict

def signature(line):
    line = re.sub(r'/[^\s:]+/', '/…/', line)
    line = re.sub(r'\b\d+\b', '#', line)
    line = re.sub(r'<[^>]*>', '<…>', line)
    return line.strip()

def main():
    buckets = OrderedDict()
    for raw in sys.stdin:
        line = raw.rstrip('\n')
        if not line.strip():
            continue
        sig = signature(line)
        if sig not in buckets:
            buckets[sig] = {'count': 0, 'first': line, 'last': line}
        buckets[sig]['count'] += 1
        buckets[sig]['last'] = line

    for sig, info in sorted(buckets.items(), key=lambda x: -x[1]['count'])[:20]:
        print('`' + sig + '`')
        print('- count: ' + str(info['count']))
        print('- first: ' + info['first'][:80])
        print('- last:  ' + info['last'][:80])
        print()

if __name__ == '__main__':
    main()
Enter fullscreen mode Exit fullscreen mode

Run it on a failed build:

g++ -std=c++20 src/*.cpp 2>&1 | python triage_log.py | tee triage.txt
Enter fullscreen mode Exit fullscreen mode

The output is a ranked list. The first entry is the most repeated signature. The last entry is the rarest one. Read the top entries first.

The prompt

Send only the ranked list to the model. Use a tight prompt. Keep it under 50 tokens.

A C++ build fails with repeated errors.
High count means scale, not severity.
Name the most likely root cause and one command to confirm it.
Enter fullscreen mode Exit fullscreen mode

Ask for a single hypothesis and one verification command. That is enough to close the next loop. You do not need a long conversation.

How to read the frequency shape

The raw count matters less than the shape.

Frequency shape Likely story Confirm with
One signature dominates A shared API changed Compile a single translation unit
Several signatures with high counts One template constraint cascades Grep for the first changed concept
Many signatures with count 1 A missing declaration in one file Inspect preprocessor output
Counts rise near the end Compiler recovery noise Inspect the last unique error manually

A high count is a root-cause candidate. A low count is a symptom. If one signature has forty hits and another has three, the forty-hit one likely caused the three-hit one. That ordering is a hypothesis, not a proof.

A worked example

Consider an overload failure. The log has 1,200 lines. The triage list shows one signature with a count of 220. The model receives just that signature. It proposes that a narrowing conversion removed the match. The confirmation command compiles a one-line snippet. It takes two minutes.

Now a linker failure. The list shows three dead symbols with equal counts. The model suggests an ABI version mismatch. A quick nm -C on the object files confirms it. Total context cost: one list.

These examples come from ordinary compile errors. They show the pattern: count first, read later.

A cheaper alternative to bisecting

Manual bisection asks 'remove half the files, does it still fail?'. That is correct but slow. The frequency table asks 'which error repeats the most?'. That is a faster filter because it uses the compiler's own accounting.

The table does not prove causality. It only suggests where to slice. You still compile once to confirm. That confirm step is the only compile you pay for.

A note on sanitizer output

Sanitizer traces behave differently. They repeat only a few frames. The frequency table still helps. A frame repeated in five threads is a hot common path. A frame appearing once is a symptom.

Run the same script on an -fsanitize=address stderr stream. The sorted list shows the shared tail. That tail is usually the allocator or the destructor.

Normalize the symbols before counting. Strip the address offsets. Keep the function names. This reduces noise without losing the stack shape.

Limitations

A frequency table is a view, not a truth. The compiler may inline, fold, or elide errors. Treat the table as a first filter. The real root cause can be outside the repeated set. That is why the confirmation command matters.

Frequency hides position. If the same error repeats in two unrelated components, the counts merge. You lose the geographic signal.

Frequency hides macro context. The signature strips template arguments. A macro-expanded type can be invisible. Inspect one sample line manually before trusting the hypothesis.

The model can hallucinate. Treat every answer as a suggestion. Verify with one command. Do not rely on the model's confidence.

Who should skip this

Do not use this for a race condition. Order is the only signal there. Do not use it for a cross-module refactor. The list flattens dependencies. Use the full log and a wider model for those cases.

Skip it if your log is full of progress bars. Progress bars inflate counts without information. Filter those lines before counting.

A small test for the script

You can verify the script with a tiny log. Create a file with ten identical errors and one unique error.

for i in {1..10}; do echo 'src/a.cpp:10: error: no match'; done > fake.log
echo 'src/b.cpp:3: error: undeclared' >> fake.log
cat fake.log | python triage_log.py
Enter fullscreen mode Exit fullscreen mode

The output lists the repeated signature first. The unique signature appears once. This test proves that the script preserves scale.

What the prompt should not contain

Do not paste the raw first 50 lines. Do not paste the last 50 lines. Do not paste a random sample. The model will pattern-match on the wrong thing.

Do not ask 'explain this log'. The log is not in the prompt. Ask 'what would cause this frequency pattern'. The pattern is the artifact.

The shift in the question

The frequency table changes the question. Instead of 'what should I read?', you ask 'what is repeated?'. The model answers faster. The rebuild happens on a free server. The analysis runs on a free model. Your job is one confirming command.

Run the script on your next red build. The count might surprise you.

Top comments (0)