Repository navigation
[Bug]: JSON deserialization is ~7x slower than MessagePack, and far worse on WebAssembly #352
Description
What type of issue is this?
- Bug report
- Feature request
Prerequisites
- I have written a descriptive issue title
- I have verified that I am running the latest version of the SysML2.NET
- I have searched open and closed issues to ensure it has not already been reported
Description
Deserializing the same model costs ~7x more through SysML2.NET.Serializer.Json than through
SysML2.NET.Serializer.MessagePack. Same DTOs, same generator, only the wire format differs. With
#347 fixed, this is now the largest cost in the SysML v2 load path.
| Elements | Server JSON | Server MsgPack | WASM JSON | WASM MsgPack |
|---|---|---|---|---|
| 5 000 | 78 ms | 16 ms | 2.23 s | 242 ms |
| 50 000 | 169 ms | 130 ms | 17.54 s | 2.65 s |
| 100 000 | 280 ms | 76 ms | 40.07 s | 5.12 s |
WASM throughput is ~2 500 elements/sec for JSON against ~19 000 for MessagePack, flat across sizes.
Under Blazor WebAssembly that is 40 s of fully blocked UI thread at 100 000 elements.
Causes
-
Whole-payload DOM.
DeSerializer.cs:90—JsonDocument.Parse(stream)materialises all
57.9 MB plus metadata and holds it live while the DTO list grows. MessagePack streams. -
@typeread twice, allocating both times.DeSerializer.cs:184-186does
TryGetProperty("@type", …)+GetString()to select the deserializer; the selected
deserializer then re-reads and re-allocates it to validate (PartUsageDeSerializer.cs:65-73) —
a check that cannot fail.DeSerializer.cs:184also uses the UTF-16 literal"@type"where the
generated code correctly uses"@type"u8, so the hottest lookup in the library transcodes on
every comparison. -
Guid.Parse(x.GetString())for every id and every reference
(PartUsageDeSerializer.cs:79-87, and inside each reference-array loop). Allocates 36 chars,
transcodes, parses back to 16 bytes.TryGetGuidreads straight from UTF-8. Scales with edges,
not elements. The MessagePack equivalent isReadGuidBin16— 16 raw bytes. -
TryGetPropertyper declared property — a linear scan each time, so O(declared × present).
~240 name comparisons perPartUsagewhere a streaming reader does ~22. The MessagePack
formatter never searches; a positionalswitchdetermines identity. -
ILoggercreated per element (PartUsageDeSerializer.cs:63), andLogDebugallocates its
object[]and boxes theGuidat the call site even underNullLogger. Minor in a dense
payload; proportional to absent properties, so worse on real sparse models.
Proposed fix. Generated code changes belong in the templates, not the AutoGen files.
Stage 1 — allocations. "@type"u8 in DeSerializer.cs; drop the redundant @type
re-validation; TryGetGuid instead of Guid.Parse(GetString()); hoist the logger out of the
per-element method; guard or remove the "property not found" logs.
Stage 2 — per-object parsing. Replace JsonDocument.Parse(stream) with a Utf8JsonReader loop
calling JsonDocument.ParseValue(ref reader) per element and disposing each immediately (buffers
return to the pool). Peak memory drops from whole-payload to one element. Every generated signature
stays intact — best value per unit of risk.
Stage 3 — streaming reads. Regenerate deserializers to take ref Utf8JsonReader and switch on
the properties actually present via ValueTextEquals("…"u8), with Skip() for unknowns. Turns
O(declared × present) into O(present), removes the DOM, and structurally eliminates the
absent-property logging. Breaking: Func<JsonElement, …> cannot become Utf8JsonReader-based (a
ref struct cannot be a generic argument), so the provider surface needs a custom delegate and all
175 deserializers regenerating. For dispatch, @type is written first unconditionally
(PartUsageSerializer.cs:62-65), so read property 1 and dispatch; fall back to a rewind (the
reader is a struct, so a copy is a free snapshot) when a third-party producer puts it elsewhere.
System Configuration
- SysML2.NET version: 0.22.0 (
5588929e) - Environment (Operating system, version and so on): Windows 11 26100 x64; browser runs are Chrome
headless, Jiterpreter (default), no AOT - .NET Framework version: .NET 10.0.11 —
net10.0on the server,dotnet.wasmin the browser - Additional information: assembler timings excluded; POCO assembly is format-independent
(1.14 s JSON vs 1.24 s MessagePack at 50 000 elements), so the entire gap is the deserializer.
AOT (wasm-tools) was not tested and may change the WASM figures.
Activity
Fix #352: Json Deserialization performance improvement
Fix #352: Json Deserialization performance improvement (#353)