From 016d1d1857b10355104947303e3c28de2f8c826f Mon Sep 17 00:00:00 2001 From: Liefran Satrio Sim Date: Thu, 7 May 2026 14:36:59 +0700 Subject: [PATCH] =?UTF-8?q?Fix=20O(n=C2=B2)=20performance=20in=20ServerEve?= =?UTF-8?q?ntParser.parse()?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The parse() method had quadratic performance when receiving large SSE events delivered in small chunks (e.g., URLSession's ~8KB didReceiveData callbacks): 1. `buffer + data` created a new Data allocation on every call, copying the entire accumulated buffer each time. 2. `splitBuffer` scanned the entire buffer for separators on every chunk, even when no separator could possibly be present in the new data. For a 1.5MB SSE event arriving in ~180 chunks, total work was ~135MB of data copying/scanning, causing 15-20s delays that triggered timeouts. Fix: - Use `buffer.append(data)` for amortized O(1) in-place append - Only scan the tail region (new data + separator overlap) for separators - Only call splitBuffer when a separator is actually detected This reduces per-chunk work from O(buffer_size) to O(chunk_size), making the overall complexity O(n) instead of O(n²). Fixes #48 --- Sources/EventSource/EventParser.swift | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/Sources/EventSource/EventParser.swift b/Sources/EventSource/EventParser.swift index 24b20c0..27770d9 100644 --- a/Sources/EventSource/EventParser.swift +++ b/Sources/EventSource/EventParser.swift @@ -26,7 +26,31 @@ struct ServerEventParser: EventParser { static let colon: UInt8 = 0x3A mutating func parse(_ data: Data) -> [EVEvent] { - let (separatedMessages, remainingData) = splitBuffer(for: buffer + data) + // Append in-place (amortized O(1) when buffer has capacity) + buffer.append(data) + + // Quick check: only scan the newly added region + small overlap for separator. + // This avoids O(n) full-buffer scan on every chunk when no separator is present. + let separators: [[UInt8]] = [[Self.lf, Self.lf], [Self.cr, Self.lf, Self.cr, Self.lf]] + let maxSeparatorLength = 4 // \r\n\r\n is the longest + let searchStart = max(buffer.startIndex, buffer.endIndex - data.count - maxSeparatorLength) + let tailRegion = buffer[searchStart...] + + var hasSeparator = false + for separator in separators { + if tailRegion.range(of: Data(separator)) != nil { + hasSeparator = true + break + } + } + + guard hasSeparator else { + // No separator in the new region — nothing to parse yet + return [] + } + + // Separator found — do the full split (only runs when we have complete messages) + let (separatedMessages, remainingData) = splitBuffer(for: buffer) buffer = remainingData return parseBuffer(for: separatedMessages) }