Something something jsonDecode

by Slava Egorov

Language shapes the way we think, and determines what we can think about.

— Benjamin Lee Whorf

Sapir–Whorf hypothesis

aka linguistic relativity

I speak Spanish to God,
Italian to women,
French to men,
and German to my horse.

— Charles V (allegedly)

I make my mobile apps in Dart, and my backends in JavaScript (Java, C#, C++, Rust, etc)

— modern developer

🀬 jsonDecode

What are we comparing?


                    // JavaScript
                    let obj = JSON.parse(str);
                    // String into an Object/Array/...
                    // Implemented in C++, e.g. in V8:
                    //    src/json/json-parser.{h,cc}
                    

                    final map = jsonDecode(str);
                    // String into an Map/List/...
                    // Implemented in Dart, e.g.
                    // sdk/lib/_internal/
                    //   vm/lib/convert_patch.dart
                    //   wasm/lib/convert_patch.dart
                    //   wasm_js_compatibility/lib/convert_patch.dart
                    //   js_runtime/lib/convert_patch.dart
                    //   js_dev_runtime/patch/convert_patch.dart
                    

Web is complicated

  • Comes with fast JSON.parse
  • but that makes JS Object, not a Map
  • does not go into Wasm structs directly
  • takes a JS string not bytes

I will focus on native (aka Dart VM, Dart Native Runtime)

Optimize anything in 3 steps:

  1. Measure performance, if it is okay - you are done.
  2. Identify and remove some unnecessary work.
  3. Go to step 1.

                    // Measures JSON decoding speed in ns per byte
                    double measureSpeed(int N, String input, int byteLength) {
                      final sw = Stopwatch()..start();
                      for (var i = 0; i < N; i++) {
                        jsonDecode(input);
                      }
                      final usPerIteration = sw.elapsedMicroseconds / N;
                      final nsPerByte = (usPerIteration * 1000) / byteLength;
                      return nsPerByte;
                    }
                    

                    // Measure JSON decoding speed in ns per byte
                    function measureSpeed(N, input, byteLength) {
                      let start = performance.now();
                      for (let i = 0; i < N; i++) {
                        JSON.parse(input);
                      }
                      let end = performance.now();
                      let usPerIteration = (end - start) * 1000 / N;
                      let nsPerByte = (usPerIteration * 1000) / byteLength;
                      return nsPerByte;
                    }
                    

                    $ du -h github_events.json
                    64K github_event.json
                    $ v8 json-benchmark.js -- github_events.json
                    JSON.parse: 2.28 ns/byte
                    $ dart compile exe json-benchmark.dart
                    $ json-benchmark.exe github_events.json
                    JsonDecoder: 8.79 ns/byte
                    

should we rewrite JsonDecoder in C++?

maybe not yet

« I want to speak Dart to my horse! »

String ⇒ Map

but where does String come from?

Uint8List ⇒ String ⇒ Map

usually it arrives from the network in form of UTF8 bytes

Uint8List ⇒ Map

JSON.parse can only parse from string, but in Dart...

                        import 'dart:convert';

                        print(const JsonDecoder());
                        // Instance of 'JsonDecoder'
                        print(const Utf8Decoder().fuse(const JsonDecoder()));
                        // Instance of '_JsonUtf8Decoder' (surprise!)
                        

                    // Measures JSON decoding speed in ns per byte
                    double measureSpeed(int N, Uint8List input) {
                      final decoder =
                          const Utf8Decoder().fuse(const JsonDecoder());
                      // ...
                      for (var i = 0; i < N; i++) {
                        decoder.convert(input);
                      }
                      // ...
                    }
                    

                    // Measure JSON decoding speed in ns per byte
                    function measureSpeed(N, input /* ArrayBuffer */) {
                      // ...
                      for (let i = 0; i < N; i++) {
                        JSON.parse(decodeUtf8(input));
                        // decodeUtf8 just calls V8's String::NewFromUtf8
                      }
                      // ...
                    }
                    

                    $ v8 json-benchmark.js -- github_events.json
                    UTF8Decode+JSON.parse: 2.78 ns/byte
                    $ dart compile exe json-benchmark.dart
                    $ json-benchmark.exe github_events.json
                    JsonUtf8Decoder: 8.53 ns/byte
                    

time to looks closer


                    $ dart compile aot-snapshot json-benchmark.dart
                    $ perf record -g dartaotruntime json-benchmark.aot \
                        github_events.json
                    $ perf report
                    
41.82% _ChunkedJsonParser.parseString
19.27% _ChunkedJsonParser.parse
 6.84% Uint8List.[]
 5.44% _ChunkedJsonParser.parseStringToBuffer
 3.01% String.hashCode
 2.83% _LinkedHashMapMixin._insert
 2.40% _StringBase._createOneByteString
 2.10% _LinkedHashMapMixin._set
 1.90% _LinkedHashMapMixin._findValueOrInsertPoint
 1.77% _StringBase.createFromCharCodes
 1.46% allocateOneByteString
 1.13% _LinkedHashMapMixin.[]=
 1.02% _LinkedHashMapMixin._init
 0.95% _ChunkedJsonParser.parseNumber
                    

                        Uint8List bytes;

                        bytes[index];

                        // The expectation is that this is just a few
                        // CPU instructions: check index in bounds,
                        // load memory. Not a call!
                    

                        import 'dart:typed_data';

                        @pragma('vm:never-inline')
                        int foo(Uint8List bytes) => bytes[0];

                        void main() {
                          foo(Uint8List(1));
                        }
                    

                    $ dart compile exe                                         \
                        --extra-gen-snapshot-options                           \
                          --disassemble-optimized                              \
                        --extra-gen-snapshot-options                           \
                          --code-comments                                      \
                        --extra-gen-snapshot-options                           \
                          --print-flow-graph-filter=foo                        \
                        -v test.dart
                    

                        mov r2, r1         ;; r1: bytes
                        ldr r3, [r2, #15]  ;; load length
                        asr r0, r3, #1
                        movz r1, #0x0
                        cmp r1, r0         ;; length ≤ 0?
                        bcs ->oob
                        ldrb r0, [r2, #23] ;; load byte
                        ret                ;; return

                   oob: stp fp, lr, [sp, #-16]!
                        mov fp, sp
                        bl 0x101232568
                    

                        mov r2, r1         ;; r1: bytes  
                        ldr r3, [r2, #15]  ;; load length
                        asr r0, r3, #1                   
                        movz r1, #0x0                    
                        cmp r1, r0         ;; length ≤ 0?
                        bcs ->oob                        
                        ldrb r0, [r2, #23] ;; load byte
                        ret                ;; return

                   oob: stp fp, lr, [sp, #-16]!          
                        mov fp, sp                       
                        bl 0x101232568                   
                    

                    $ dart compile aot-snapshot                                \
                        --extra-gen-snapshot-options                           \
                          --dwarf-stack-traces                                 \
                        --extra-gen-snapshot-options                           \
                          --code-comments                                      \
                        --extra-gen-snapshot-options                           \
                          --write-code-comments-as-synthetic-source-to=code.il \
                        json-benchmark.dart
                    
       β”‚     B61
       β”‚     Loop 0
       β”‚     v183 <- LoadField(v2 T{_JsonUtf8Parser} . chunk)
  1.49%β”‚       mov  r12, QWORD PTR [rdx+0x3f]
       β”‚     v323 <- BoxInt64(v6)
       β”‚       mov  rax, rcx
  0.99%β”‚       add  rax, rax
  2.36%β”‚     ↓ jno  96
       β”‚     β†’ call stub _iso_stub_AllocateMintSharedWithoutFPURegsStub
       β”‚       mov  QWORD PTR [rax+0x7],rcx
       β”‚     v359 <- LoadClassId(v183) int64
  3.59%β”‚ 96:   mov  ecx, DWORD PTR [r12-0x1]
  4.16%β”‚       shr  ecx, 0xc
       β”‚     MoveArgument(v183, SP+1)
  0.12%β”‚       mov  QWORD PTR [rsp+0x8],r12
       β”‚     MoveArgument(v323, SP+0)
  0.16%β”‚       mov  QWORD PTR [rsp],rax
       β”‚     v184 <- DispatchTableCall(cid=v359 List.[], v183, v323)
  0.16%β”‚       mov  rax, QWORD PTR [r14+0x58]
 25.07%β”‚     β†’ call QWORD PTR [rax+rcx*8]
       β”‚     v324 <- UnboxInt64([non-speculative], v184)
  4.11%β”‚       sar  rax,1
       β”‚     goto:14 B67
  0.44%β”‚     ↓ jmp  e8
                    

                        // `T` is the type of the character container.
                        abstract class _ChunkedJsonParser<T> {
                          int getChar(int index);

                          // Various parsing methods written in terms of [getChar].
                        }
                    

                        class _JsonStringParser extends _ChunkedJsonParser<String> {
                          String chunk = '';
                          int getChar(int position) => chunk.codeUnitAt(position);
                        }

                        class _JsonUtf8Parser extends _ChunkedJsonParser<List<int>> {
                          static final Uint8List emptyChunk = Uint8List(0);
                          List<int> chunk = emptyChunk;

                          int getChar(int position) => chunk[position];
                        }
                    

Performance antipattern #1

Base class provides generic implementation which relies on small operation (getChar) overriden by subclasses.

Why: If more than one subclass is instantiated in the program then compiler will not be able to fully specialize the code.

Performance antipattern #2

Code which works with bytes is written in terms of List<int> instead of Uint8List

Why: List<T> has many different implementations. If compiler can't narrow it down to a specific implementation it will be forced to generate virtual calls for things like l[i] and l.length

commit df80cf91404e8e3b0f0a4eb271467448d126199e
Author: Slava Egorov <[email protected]>
Date:   Thu Mar 21 11:10:10 2024 +0000

[core] Improve JSON decoding performance

Avoid polymorphic character access by turning _ChunkedJsonParser
into a mixin instead of the base class.

TEST=ci

Change-Id: Id2080724e07d16e96734a80629c8bd8906dc590b
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/358445
Reviewed-by: Daco Harkes <[email protected]>
Commit-Queue: Slava Egorov <[email protected]>
                    

                        mixin _ChunkedJsonParser<T> on _JsonParserWithListener {
                          // Generic implementation in terms of [getChar]
                        }

                        class _JsonUtf8Parser
                          extends _JsonParserWithListener
                          with _ChunkedJsonParser<Uint8List> {
                          Uint8List chunk = emptyChunk;
                        }
                    

                        // To maintain API compatibility we still need to
                        // accept arbitrary List<int>
                        void parseChunk(List<int> value, int start, int end) {
                          if (value is Uint8List) {
                            chunk = value;
                          } else {
                            // Slow-path: copy value into fresh Uint8List.
                            // We assume this never happens.
                            chunk = Uint8List(end - start);
                            // ...
                          }
                        }
                    

                    $ dart compile exe json-benchmark.dart
                    $ json-benchmark.exe github_events.json
                    JsonUtf8Decoder: 5.95 ns/byte (~30% faster)
                    

Performance antipattern #3

Parsing from String rather than bytes

Why: For space savings String has two different implementations in Dart (one byte and two byte). Compiler usually does not know which reaches particulate place, so s.codeUnitAt(i) is a call or a (s is _OneByteString ? ... : ...) pattern.


                        @pragma('vm:never-inline')
                        int foo(String bytes) => bytes.codeUnitAt(0);

                        void main() {
                          foo("abcd");
                        }
                    

                        movz r0, #0x61 ;; πŸ˜‚
                        ret
                    

                        @pragma('vm:never-inline')
                        int foo(String bytes, int i) => bytes.codeUnitAt(i);

                        void main(List<String> args) {
                          foo("abcd", args.length);
                        }
                    

     ldr r2, [pp, #5960] ;; constant "abcd"
     mov r3, r1
     movz r0, #0x4
     cmp r1, r0
     bcs -> oob
     add tmp, r2, r3
     ldrb r0, [tmp, #15]
     ret

oob: stp fp, lr, [sp, #-16]!
     mov fp, sp
     bl 0x105232578
                    

                        @pragma('vm:never-inline')
                        int foo(String bytes, int i) => bytes.codeUnitAt(i);

                        void main(List<String> args) {
                          foo(args.first, args.length);
                        }
                    

                        ;; load class id from string object
                        ldr r1, [r3, #-1]
                        ubfm r1, r1, #12, #31
                        lsl r1, r1, #1
                        cmp r1, #0xba ;; compare cid == kTwoByteStringCid
                        bne ->2
                        ;; Load from _OneByteString
                     1: add tmp, r3, r2
                        ldrb r1, [tmp, #15]
                        mov r0, r1
                        b ->done
                        ;; Load from _TwoByteString
                     2: add tmp, r3, r2 lsl #1
                        ldrh r1, [tmp, #15]
                        mov r0, r1
                  done:
                        ret

parse bytes not strings!

Dart probably needs UTF-8 backed String,
but aaaaaanyway back to JSON.

                        while (...) {                      β”Œ->β–ˆβ–ˆβ–ˆ<-┐
                          if (...) {                       |  β–ˆβ–ˆβ–ˆ  |
                            continue;                      |  β–ˆβ–ˆβ–ˆ--β”˜
                          }                                |  β–ˆβ–ˆβ–ˆ
                          if (...) {                       |β”Œ-β–ˆβ–ˆβ–ˆ
                            return /* ... */;              || β–ˆβ–ˆβ–ˆ--> return
                          }                                |β””>β–ˆβ–ˆβ–ˆ
                          if (..) {                        |  β–ˆβ–ˆβ–ˆ-┐
                            throw /* ... */;               |  ... |
                          }                                |  β–ˆβ–ˆβ–ˆ |
                        }                                  β””--β–ˆβ–ˆβ–ˆ |
                                                                  |
                                                              β–ˆβ–ˆβ–ˆ<β”˜
                                                              β–ˆβ–ˆβ–ˆ--> throw
                    
(AOT code emition block ordering was suboptimal)

                        while (...) {                      β”Œ->β–ˆβ–ˆβ–ˆ<-┐
                          if (...) {                       |  β–ˆβ–ˆβ–ˆ  |
                            continue;                      |  β–ˆβ–ˆβ–ˆ--β”˜
                          }                                |  β–ˆβ–ˆβ–ˆ
                          if (...) {                       |β”Œ-β–ˆβ–ˆβ–ˆ
                            return /* ... */;              || β–‘β–‘β–‘--> return
                          }                                |β””>β–ˆβ–ˆβ–ˆ
                          if (..) {                        |  β–ˆβ–ˆβ–ˆ-┐
                            throw /* ... */;               |  ... |
                          }                                |  β–ˆβ–ˆβ–ˆ |
                        }                                  β””--β–ˆβ–ˆβ–ˆ |
                                                                  |
                                                              β–ˆβ–ˆβ–ˆ<β”˜
                                                              β–ˆβ–ˆβ–ˆ--> throw
                    
[vm] Use codegen block order in regalloc in AOT.
[vm/compiler] Improve AOT block scheduler
[vm/compiler] Move reorder_blocks onto the graph.
                    
did not move the needle much (maybe ~5%)

                      // In _ChunkedJsonParser.parse
                      while (position < length) {
                        int char = getChar(position);
                        switch (char) {
                          case SPACE:
                          case CARRIAGE_RETURN:
                          case NEWLINE:
                          case TAB:
                            position++;
                            break;
                          // other characters
                    

                        // In _ChunkedJsonParser.parseString
                        while (position < end) {
                          int char = getChar(position++);
                          bits |= char;
                          if (char > BACKSLASH) continue;
                          // Escape sequence? Use more complex parsing.
                          if (char == BACKSLASH) return handleEscapes(...);
                          if (char == QUOTE) return ...;  // end of string
                          if (char < SPACE) fail(...);  // invalid json
                        }
                    

Similar pattern of code:

  1. Read a character;
  2. Use a chain of ifs to categorize it.

Idea: could use lookup table instead!


                          do {
                            char = getChar(position);
                            position++;
                            bits |= char;
                            final attrs = _characterAttributes.codeUnitAt(char);
                            if ((attrs & simpleStringEndBit) != 0) break;
                          } while (position < end);
                          if (char == QUOTE) return ...; // end of string
                          if (char == BACKSLASH) return handleEscapes(...);
                          if (char < SPACE) fail(...); // invalid json
                    

                      static const String _characterAttributes =
                        '!!!!!!!!!##!!#!!!!!!!!!!!!!!!!!!" !                             '
                        '                            !                                   '
                        '                                                                '
                        '                                                                ';
                    

                      static const String _characterAttributes =
                        // This string length is 256. If `ch` is end
                        // a simple string (e.g. `ch` is QUOTE, BACKSLASH or a
                        // control character, then `_characterAttributes.codeUnitAt(ch)`
                        // will have [simpleStringEndBit] set.
                        // Similarly if `ch` is a whitespace (SPACE, CR, LF, TAB)
                        // then it has [whiteSpaceBit] set.
                    

This helped a bit, but:

  1. if ((attrs & simpleStringEndBit) != 0) was producing bad code;
  2. Bounds checks on getChar(position) were causing code quality issues;
  3. Interupt checks in loop headers were eating time;

so I landed some changes first

[vm] Fix pragma vm:unsafe:no-interrupts
[vm] Add pragma vm:unsafe:no-bounds-checks
[vm] Enable test pattern (a&b == 0) fusion in AOT on X64/ARM64
[vm/libs] Improve JsonUtf8Decoder performance.
                    
(This improved benchmark by another ~30-40%)
8.41% _ChunkedJsonParser.parseString
8.36% _ChunkedJsonParser.parse
8.07% String.hashCode
7.14% _LinkedHashMapMixin._insert
6.26% _ChunkedJsonParser.parseStringToBuffer
4.56% _LinkedHashMapMixin._set
4.36% _Utf8Decoder.convertChunked
3.99% _JsonUtf8Parser.getString
3.76% _LinkedHashMapMixin._findValueOrInsertPoint
3.13% _LinkedHashMapMixin._init
2.30% StringBuffer._addPart
2.21% _ChunkedJsonParser.parseStringEscape
2.19% allocateOneByteString
1.65% dart::Instance::CheckedHandle(dart::Zone*, dart::ObjectPtr)
1.60% dart::BootstrapNatives::DN_StringBuffer_createStringFromUint16Array(dart::Thread*, dart::Zone*, dart::NativeArguments*)
1.58% _OperatorEqualsAndHashCode._hashCode
1.55% _LinkedHashMapMixin.[]=
                        
8.41% _ChunkedJsonParser.parseString
8.36% _ChunkedJsonParser.parse
8.07% String.hashCode
7.14% _LinkedHashMapMixin._insert
6.26% _ChunkedJsonParser.parseStringToBuffer
4.56% _LinkedHashMapMixin._set
4.36% _Utf8Decoder.convertChunked
3.99% _JsonUtf8Parser.getString
3.76% _LinkedHashMapMixin._findValueOrInsertPoint
3.13% _LinkedHashMapMixin._init
2.30% StringBuffer._addPart
2.21% _ChunkedJsonParser.parseStringEscape
2.19% allocateOneByteString
1.65% dart::Instance::CheckedHandle(dart::Zone*, dart::ObjectPtr)
1.60% dart::BootstrapNatives::DN_StringBuffer_createStringFromUint16Array(dart::Thread*, dart::Zone*, dart::NativeArguments*)
1.58% _OperatorEqualsAndHashCode._hashCode
1.55% _LinkedHashMapMixin.[]=
                        
commit d91679987930f4fd6b0a5b3a3f328b30841ceea1
Author: Slava Egorov <[email protected]>
Date:   Tue Aug 13 12:00:48 2024 +0000

[vm/corelib] Optimize building of Maps in JSON decoder.

Instead of gradually adding key-value pairs into the Map as JSON
is being parsed collect all key values first and then allocate
the map with appropriate capacity.

Issue https://github.com/dart-lang/sdk/issues/55522

TEST=ci

Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/374564
Commit-Queue: Slava Egorov <[email protected]>
Reviewed-by: Lasse Nielsen <[email protected]>
                

                        $ dart compile exe json-benchmark.dart
                        $ json-benchmark.exe github_events.json
                        JsonUtf8Decoder: 3.5 ns/byte (~30% slower than V8)
                        
Input JSONns/bytevs V8
apache_builds.json3.96155.33%
canada.json12.78171.56%
citm_catalog.json3.92101.47%
github_events.json3.58129.16%
google_maps_api_compact_response.json8.83143.74%
google_maps_api_response.json4.97152.82%
gsoc-2018.json2.66133.21%
instruments.json4.67157.52%
marine_ik.json8.01159.51%
mesh.json6.28137.00%
mesh.pretty.json6.16189.29%
Input JSONns/bytevs V8
numbers.json3.8383.06%
random.json7.83112.51%
repeat.json5.87129.10%
semanticscholar-corpus.json6.61104.84%
tree-pretty.json4.93165.25%
twitter_api_compact_response.json5.44122.15%
twitter_api_response.json4.75118.97%
twitterescaped.json6.51167.86%
twitter.json5.35101.65%
twitter_timeline.json8.25253.34%
update-center.json5.0584.21%

More improvements are possible:

  1. Strings with escapes hit bad performance in StringBuffer;
  2. Can intern strings and speculate about the next possible Map key.

but what if we rethink this?

Fastest piece of code is
the one which does not need to run.

—  zen of optimization


                        #include "simdjson.h"

                        // On-Demand JSON: A Better Way to Parse Documents?
                        //       https://arxiv.org/abs/2312.17149
                        // This library uses SIMD to blaze through the
                        // document many characters at a time.

                        ondemand::parser parser;
                        ondemand::document doc = parser.iterate(json);
                        // Extract "created_at" from 100th status.
                        doc["statuses"][100]["created_at"]
                        // This does not actually parse most of the document.
                    

                        $ dart create -t console dart_simdjson
                        $ cd dart_simdjson
                        $ mkdir -p src/third_party/simdjson
                        $ curl ... # pull simdjson.{h,cpp}
                        $ mkdir hook
                        $ vi hook/build.dart
                    

Native Assets!

https://dart.dev/interop/c-interop#native-assets

// src/simdjson_api.cpp
extern "C" void* simdjson_parse(
    const uint8_t* data, size_t size) {
  ondemand::parser parser;
  ondemand::document doc = parser.iterate(input_data, size);

  // ...
}

                    // lib/simdjson_capi.dart
                    @Native<
                      Pointer<Void> Function(Pointer<Uint8>, Size)
                    >()
                    external Pointer<Void> simdjson_parse(
                        Pointer<Uint8> buf, int len);
                    

                        // hook/build.dart
                        CBuilder.library(
                          name: packageName,
                          assetName: 'simdjson_capi.dart',
                          language: Language.cpp,
                          sources: [
                            'src/simdjson_api.cpp',
                            'src/third_party/simdjson/simdjson.cpp',
                          ],
                          defines: {'SIMDJSON_EXCEPTIONS': '0'},
                          flags: ['--std=c++20', '-O3'],
                        );
                    

                        # Run in JIT mode. Will invoke build hook to build
                        # necessary native dependencies.
                    

                        $ dart --enable-experiment=native-assets run bin/benchmark.dart
                    

                        # Build in AOT mode. Will invoke build hook to build
                        # necessary native dependencies.
                    

                        $ dart --enable-experiment=native-assets build bin/benchmark.dart
                        $ tree bin/benchmark/
                        bin/benchmark/
                        β”œβ”€β”€ benchmark.exe
                        └── lib
                            └── libdart_simdjson.so
                    

One possible way to use simdjson:

  1. Take schema and generate layout descriptor;
  2. Use simdjson to parse JSON and inflate it into a native object based on the descriptor.
  3. Dart can then takes data from native object by fixed offsets.

Uint8List ⇒ String ⇒ Map

Uint8List ⇒ Map

we removed intermediate String

Uint8List ⇒ Map ⇒ X.fromJson

Maps are often also just intermediary objects

Uint8List ⇒ Map ⇒ X

So we would like to eliminate them

                    class Tweet {
                      final String? created_at;
                      final int id;
                    }

                    // binary layout:
                    //   created_at  const char* at offset  0
                    //   id          int64_t     at offset 16
                    

                    class Tweet {
                      final Pointer<Void> ptr;

                      Tweet._(this.ptr);

                      String? get created_at => util.loadOptionalString(ptr, 0);
                      int get id => util.loadInt(ptr, 16);

                      factory Tweet.fromJsonBytes(Uint8List bytes) =>
                        Tweet._(simdjson_parse(bytes, bytes.length, descriptor));
                      static final descriptor = 'created_at,s?,id,i!';
                    }
                    

Would be nice to generate with a macro!

... but alas: you can't yet augment fields


                        'Tweet': {
                          'created_at': Field.required(PrimitiveType('String')),
                          'id': Field.required(PrimitiveType('int')),
                          'text': Field.required(PrimitiveType('String')),
                          'user': Field.required(CompoundType('User')),
                           // ... 15 more fields
                        },
                        'User': {
                          'id': Field.required(PrimitiveType('int')),
                          'name': Field.required(PrimitiveType('String')),
                          // ... 30 more fields
                        }
                    

                        $ dart --enable-experiment=native-assets build bin/benchmark.dart
                        $ build/benchmark.exe twitter_timeline.json
                        1.62 ns/byte
                    
Program loads data which it does not access.

Tree shaker knows!

... but it can't prune descriptors

                        // runtime/docs/compiler/pragmas_recognized_by_compiler.md#declaring-a-static-weak-reference-intrinsic-method
                        @pragma('weak-tearoff-reference')
                        T Function()? weakRef<T>(T Function()? x) => x;

                        // tldr: `weakRef(f)` will become `null` in AOT
                        // if compiler figures out that `f` is not referenced
                        // from anywhere else.
                    

                        class Tweet {
                            static final String _descriptors = [
                              if (_weakRef(_offsetOf$created_at) != null)
                                'created_at,s!'
                              else
                                '.,s!',
                              ...
                            ].join(',');
                            static int _offsetOf$created_at() => 0;
                            String get created_at =>
                              utils.loadString(_data, _offsetOf$created_at());
                        }
                    

                        $ dart --enable-experiment=native-assets build bin/benchmark.dart
                        $ build/benchmark.exe twitter_timeline.json
                        0.70 ns/byte
                    
but not parsing much now: we don't access any fields :)

have a mental model

Can't improve performance if you don't understand the cost.

look for things to stop doing

That's the simplest way to optimize things.

Maybe jsonDecode is doing alright?

3ns/byte is ~300Mb/s. Give server-side Dart a try :)

pragmatic amalgamation

I want to write Dart but I don't want to reinvent the wheel.