diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 30396dcd6..02e0a4243 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,5 +1,9 @@ # Release Notes +## 8.1.4 - Mar 30 2026 + +- Performance: `JsonValue.Parse` now copies unescaped string runs in bulk instead of appending character-by-character, reducing `StringBuilder.Append` calls for strings with few or no escape sequences + ## 8.1.3 - Mar 23 2026 - Fix JSON `/* ... */` comment parser: `*` or `/` characters inside the comment body no longer cause premature termination and parse failure diff --git a/src/FSharp.Data.Json.Core/JsonValue.fs b/src/FSharp.Data.Json.Core/JsonValue.fs index 9f9c52b70..21be4f008 100644 --- a/src/FSharp.Data.Json.Core/JsonValue.fs +++ b/src/FSharp.Data.Json.Core/JsonValue.fs @@ -315,8 +315,18 @@ type private JsonParser(jsonText: string) = ensure (i < s.Length && s.[i] = '"') i <- i + 1 + // Track start of current unescaped run; flush as a bulk chunk when an escape or end is hit. + // This avoids per-character StringBuilder.Append calls for strings with few/no escapes, + // which is the common case in real-world JSON. + let mutable chunkStart = i + + let inline flushChunk upTo = + if upTo > chunkStart then + buf.Append(s, chunkStart, upTo - chunkStart) |> ignore + while i < s.Length && s.[i] <> '"' do if s.[i] = '\\' then + flushChunk i ensure (i + 1 < s.Length) match s.[i + 1] with @@ -371,10 +381,12 @@ type private JsonParser(jsonText: string) = | _ -> throw () i <- i + 2 // skip past \ and next char + chunkStart <- i else - buf.Append(s.[i]) |> ignore i <- i + 1 + // Flush any remaining unescaped characters + flushChunk i ensure (i < s.Length && s.[i] = '"') i <- i + 1 let str = buf.ToString()