Skip to content

fix(parser): avoid quadratic diagnostic accumulation - #263

Merged
fortmarek merged 1 commit into
masterfrom
codex/linear-diagnostic-parsing
Oct 27, 2026
Merged

fortmarek merged 1 commit into
masterfrom
codex/linear-diagnostic-parsing

Conversation

@fortmarek

@fortmarek fortmarek commented Oct 27, 2026 •

Copy link
Copy Markdown
Collaborator

Motivation

Notice parsing repeatedly copies growing strings, arrays, and dictionaries. A diagnostic with a long continuation or a section with many messages therefore requires quadratic work even though its output is only proportional to its input. This makes large activity logs unnecessarily expensive to parse.

Matched release benchmarks below quantify all four affected paths while preserving their output. The regression fixtures also assert diagnostic contents, filtering, and ordering.

Root cause and approach

The previous diagnostic reduction interpolated the entire accumulated diagnostic for every continuation line. The notice reduction similarly evaluated flatten + notices for each message. The location reduction copied its dictionary while accumulating diagnostics, and removing messages already classified as Clang warnings scanned every warning title for every message.

This change removes those accumulation and lookup costs:

  • Keep the current diagnostic's continuation lines as substrings and join them once when the diagnostic ends. Store completed diagnostics directly in one mutable dictionary.
  • Flatten notice arrays with flatMap, preserving order without rebuilding the growing result for every message.
  • Use a set of Clang warning titles for membership checks, preserving the behavior of filtering every message whose title matches an already classified warning.

No diagnostic output is truncated and no new input limit is imposed. The implementation preserves Unicode, ignored preamble, omitted empty carriage-return segments, last-occurrence-wins behavior for duplicate locations, and the existing preference for an error marker when both marker types occur in one diagnostic. Public APIs and serialization formats are unchanged.

Raising downstream timeouts would only move the failure threshold. Truncation would discard diagnostic evidence. Fixing accumulation in the library makes the improvement available to every consumer without requiring a consumer-specific workaround.

Before/after benchmarks

Both revisions were measured on the same Apple M3 Pro running macOS 26.3 with Apple Swift 6.4 (swiftlang-6.4.0.34.1), using release builds. Before is unmodified implementation d723980ca700f96414c37d37697f96e8cdeff20f; after is this PR's 687d3869687c80eaf57b093151968acd3ce3eb2f. The exact same four synthetic test fixtures were used for both revisions. No private activity logs are required to reproduce these cases.

Each cell is the median of three separate test-process runs, measured by the tests' existing Date() timer immediately around the parser call. Compilation, fixture construction, and output assertions are excluded. Runs were sequential, with all after samples collected before all before samples; no separate warm-up was discarded. These are local parser benchmarks, not end-to-end activity-log timings or a guarantee for other machines.

Synthetic workload Before (median) After (median) Speedup
One diagnostic with 40,000 × 1 KiB continuation lines 30.508 s 2.522 s 12.1×
40,000 distinct diagnostic locations 53.349 s 0.154 s 345.5×
60,000 notices, preserving every title and its order 18.464 s 0.068 s 271.6×
40,000 Clang warnings, a duplicate title, and an unmatched note 11.142 s 0.156 s 71.4×

Raw samples in execution order (seconds):

Synthetic workload Before: runs 1, 2, 3 After: runs 1, 2, 3
One diagnostic with 40,000 × 1 KiB continuation lines 30.508, 32.769, 24.555 2.470, 2.717, 2.522
40,000 distinct diagnostic locations 53.349, 67.780, 25.812 0.166, 0.154, 0.150
60,000 notices, preserving every title and its order 13.743, 20.580, 18.464 0.068, 0.087, 0.062
40,000 Clang warnings, a duplicate title, and an unmatched note 9.819, 11.142, 14.875 0.156, 0.562, 0.155

To reproduce, use separate checkouts at the two revisions, copy this PR's Tests/XCLogParserTests/ParserTests.swift into the baseline checkout, and temporarily add print("BENCHMARK \(#function) seconds=\(elapsed)") after each of its four let elapsed = Date().timeIntervalSince(start) statements. Run the following in each checkout, then repeat with --skip-build for samples two and three:

swift test -c release --filter 'ParserTests.testMany|ParserTests.testSwiftDiagnosticDetailsWith'

Keep the test assertions enabled: output-content and ordering checks validate the result alongside the timer. The baseline can exit unsuccessfully because these regression tests intentionally impose a ten-second parser ceiling; its printed elapsed time still records the completed parser call. The temporary timing prints are not part of the committed change.

Tests and validation

Seven new tests cover compatibility and four large-input shapes: a diagnostic with 40,000 continuation lines of 1 KiB each, 40,000 distinct locations, 60,000 notices, and 40,000 Clang warnings with a repeated title and an unmatched note. Performance guards use a generous ten-second ceiling rather than a tight microbenchmark threshold. All fixtures are synthetic and created in the tests. The new tests are registered in the existing Linux manifest without changing its unrelated test inventory.

  • macOS debug suite: swift test passed all 95 tests. The notice-volume test completed in 0.26 seconds; the large-continuation test completed in 4.48 seconds including fixture construction and full-output comparison.
  • Debug regression verification: the same 60,000-notice test failed against unmodified upstream code, with 30.50 seconds measured inside notice parsing.
  • Release benchmarks: all twelve fixed-version test executions passed. Baseline failures were confined to the ten-second timing guards; output assertions passed.
  • Linux: all seven new regressions passed in swift:5.7.1, matching the upstream CI toolchain version, in a local ARM64 container. The large-continuation test completed in 3.04 seconds and the notice-volume test in 0.32 seconds.
  • Linux full-suite limitation: a separate full run exceeded a 120-second guard while executing the unchanged LogFinderTests.testGetLogManifestPathWithWorkspace. The cause of the local-container stall was not established. The complete Linux suite is therefore not claimed as passing; upstream CI runs on AMD64.
  • Static checks: strict SwiftLint passed for the edited implementation and test files; git diff --check passed.

Compatibility and rollout

Ship this in a library release and let consumers update their dependency normally. No data migration, API transition, or wire-format change is required. Existing timeout and retry policies can remain in place. Reverting the dependency version restores the previous behavior, including its poor performance on the synthetic large-input cases.

@fortmarek
fortmarek merged commit ccf5e63 into master Oct 27, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants