Skip to content

Fix backpressure stall and row loss, strict-mode quote check, SWAR count; scan with Aemi kernels; gate CI - #1

Merged
g-cqd merged 12 commits into
mainfrom
fix/quote-in-unquoted-field
Oct 24, 2026
Merged

g-cqd merged 12 commits into
mainfrom
fix/quote-in-unquoted-field

Conversation

@g-cqd

@g-cqd g-cqd commented Oct 24, 2026

Copy link
Copy Markdown
Owner

Correctness (each has a test that failed first)

  • decodeWithBackpressure hung past the high-water mark (3,200 of 20,000 rows, then nothing): recordConsumed had no caller. TLC reproduces the deadlock (Formal/BackpressureOld.tla).
  • Both backpressure streams used .bufferingNewest and dropped rows for a slow consumer (12,800 of 200,000 delivered). They now pull rows on demand (AsyncThrowingStream(unfolding:)); BackpressureController is removed. MemoryLimitConfiguration keeps its fields; only batchSize acts.
  • Strict mode missed a quote inside an unquoted field unless it sat in the last 64 bytes of the buffer.
  • countNewlinesApprox overcounted (SWAR borrow). Formal/Swar.lean proves the replacement exact.

Performance (interleaved release A/B, no benchmark slower)

Raw parse 1M rows 87 -> 76 ms; 500-byte fields decode 22.5 -> 13.1 ms, encode 23.1 -> 13.9 ms; 10 KB fields 21.3 -> 3.8 ms. Field scanning is now FieldScan: an inline head, then AemiKernels.firstIndexOfAny / firstIndexOfByte. Aemi's kernel alone was 1.9x slower on 2-byte fields, which is why the head stays.

CI

Gates on deadwood, dolly (baseline) and arcleak, re-checks the formal models (TLC + Lean), and runs the optimized test suite. Replaces the SwiftStaticAnalysis jobs that ran with continue-on-error.

Known: the five platform builds already fail on main (-warnings-as-errors vs -suppress-warnings under Xcode 26.6); not touched here.

AemiKernels is now a dependency of the CSVCoder target (it was a macro-only dependency).

g-cqd added 6 commits October 24, 2026 23:42
…tall nor drop rows

decodeWithBackpressure parked its producer past the high-water mark and nothing ever woke it (recordConsumed had no caller): 3,200 of 20,000 rows, then a hang. Both streams used .bufferingNewest, which silently drops the oldest rows when the consumer is slower: 12,800 of 200,000 reached a slow consumer, and even a fast one lost rows in the benchmark. The producer Task also ignored stream termination.

AsyncThrowingStream(unfolding:) runs the parser on the consumer's pull, so there is no queue, no controller and no background task. BackpressureController is removed; MemoryLimitConfiguration keeps its fields for source compatibility, and only batchSize still acts. Interleaved release A/B, 9 rounds, loaded machine: rows 304 -> 280 ms, batches 231 -> 238 ms (ranges overlap); the old stream delivered 97,369 of 100,000.
findByte flags the byte above a real match when it is 0x01 after the xor, so countNewlinesApprox counted an LF followed by 0x0B twice: 56 of 131 random inputs of length 0...130 disagreed with a byte-by-byte count. exactMatchMask carries no bit across a lane; Formal/Swar.lean proves it exact in all eight lanes (bv_decide) and proves the classic form over-reports. findByte keeps its cheaper form for the any-match and first-match callers, where it is exact, so the encoder hot path is unchanged.
deadwood 0.11.0 found SingleValueEncoder.allValues and reset, SIMDScanner.StructuralPosition.isDelimiter and the tab constant, and HardwareInfo.markdownSummary and jsonSummary unreferenced.
… test the optimized build

The two SwiftStaticAnalysis jobs ran with continue-on-error and never failed a build; Scripts/analyzers.sh (the AemiJSON script, tools pinned by version and SHA-256, dolly against a baseline of 290 existing clones) replaces them. Scripts/formal.sh runs TLC on the backpressure protocol (the shipped-before design must still deadlock, its replacement must hold) and Lean on the SWAR proofs; each breaks when its model is broken. test-release runs swift test -c release --enable-testable-imports.
…, and scan fields with Aemi's kernels

Strict mode rejects a quote in an unquoted field, but the 64-byte SIMD jump skipped the field's prefix without looking for quotes, so the check only worked in the last 64 bytes of the buffer: 20 rows of ab"cd,ef were accepted, 1 to 3 rows were rejected. The jump now also stops at a quote.

The field scan is FieldScan: 16 bytes inline, then AemiKernels.firstIndexOfAny (NEON) or firstIndexOfByte (memchr). The previous scanner was scalar below 64 bytes. Hybrid probe, ns per field, previous scanner vs FieldScan: 2 B 4.4/4.0, 5 B 6.2/4.9, 10 B 9.8/7.1, 20 B 17.9/18.0, 40 B 34/22, 100 B 77/22, 400 B 310/36, 4,000 B 3,145/243; calling Aemi alone was 1.9x slower at 2 bytes, which is why the inline head stays. Benchmarks, interleaved release A/B, 7 rounds: Raw Parse 1M 87.3 -> 75.5 ms, 500-byte fields 22.5 -> 13.1, 10 KB fields 21.3 -> 3.8, 200 columns 6.0 -> 4.3, simple/complex/quoted/numeric/Unicode decodes 1.00-1.02x (ranges overlap). SIMDScanner.findNextStructural and findNextQuote are removed with their tests; FieldScanTests compares the scan against a byte-by-byte search at every length and position.
…then Aemi's kernel

needsQuoting had two copies of the same loop (BufferedCSVWriter and SIMDScanner, which dolly reported) and the 64-byte path extracted its mask lane by lane. The probe, ns per call old / scalar-16 + Aemi / swar-64 + Aemi: 24 B 8.3 / 21.6 / 7.4, 40 B 9.5 / 20.4 / 8.7, 64 B 46.8 / 16.9 / 12.1, 400 B 306 / 37 / 30, 4,000 B 3,099 / 231 / 230; a 16-byte scalar head would have regressed 24-63 byte fields 2-3x, so the head is the SWAR test. Interleaved release A/B, 11 rounds: Encode 10K 14.8 -> 14.1 ms, 500-byte fields 23.1 -> 13.9, 100K sequential 130 -> 127, the other encode benchmarks 0.95-0.99x. FieldScanTests compares the new test with a byte-by-byte one at every length and position up to 140.
Comment thread .github/workflows/ci.yml
run: |
sudo mv /tmp/swa /usr/local/bin/swa
sudo chmod +x /usr/local/bin/swa
curl -fsSL --retry 3 -o elan-init.sh https://raw.githubusercontent.com/leanprover/elan/227caca133724d5516bee25c2aeb3e609478f2d8/elan-init.sh
Comment thread Scripts/analyzers.sh
Comment on lines +55 to +56
curl -fsSL --retry 3 -o "$scratch/$name.tar.gz" \
"https://github.com/g-cqd/$tool/releases/download/v$version/$name.tar.gz" \
Comment thread Scripts/formal.sh
Comment on lines +29 to +30
curl -fsSL --retry 3 -o "$JAR.part" \
"https://github.com/tlaplus/tlaplus/releases/download/v$TLA_VERSION/tla2tools.jar" \
g-cqd added 6 commits October 25, 2026 00:20
swift-format from Swift 6.4 wants @testable imports sorted into one block; the Xcode 26.6 formatter that the Format Check job runs wants regular imports first. The pre-commit hook uses the newer one, so this commit skips it; CI is the gate.
At 50,000 rows and 20,000 iterations per row the debug run took over a minute and hit the time limit on CI.
…ock parallel test

The slow-consumer tests decoded up to 100,000 rows and kept every core busy for about a second. On the 3-core CI runner that cut the parallel decode in the speed test to 0.2x of sequential (it is 1.31x there without them). A 100 KB row estimate puts the pause threshold at 8 rows, so 300 rows reach it; the four delivery tests still fail against the previous implementation.
Tens of thousands of #expect calls per test kept a core busy in the debug build, which the wall-clock parallel test on the 3-core runner feels.
Debug timing of the wall-clock parallel test changed because the sequential path got 5x faster there (387 -> 77 ms on the runner, probe branch output) while the parallel path did not (257 -> 196 ms). Release, where the number means something, is 55 vs 43 ms. CHANGELOG and .version for 1.0.1.
@g-cqd
g-cqd merged commit fff57c1 into main Oct 24, 2026
4 checks passed
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
C Security Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

@g-cqd
g-cqd deleted the fix/quote-in-unquoted-field branch October 24, 2026 23:49
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