lodestar icon indicating copy to clipboard operation
lodestar copied to clipboard

feat: provide first-class ssz support on api

Open nflaig opened this issue 1 year ago • 3 comments

Motivation

Initial discussion around this has started in https://github.com/ChainSafe/lodestar/issues/5128 to provide first-class support for SSZ in both request and response bodies but besides that, there are plenty of other shortcomings in the way we currently define routes and handle server / client responses. In a lot of cases, Lodestar was not following the spec (e.g. not setting all required headers) causing incompatibilities with other clients. The main reason for this is because our API spec tests were incomplete, headers were not checked at all or if we support SSZ on all required routes as per spec, and addressing those shortcomings requires workarounds like overriding handlers on both the client and server.

Description

This PR changes the structure of how routes are defined and makes it trivial to add SSZ support to any route. The only exception are routes with data payloads that cannot be represented as a SSZ type due to lack of JSON mapping, meaning it is not possible to translate the JSON payload into a SSZ-serialized payload.

The second major change is how we handle requests and responses on the server and client. While previously if you would request a state as SSZ from the server, the client would only be able to handle that format and if the server didn't support it, the request would fail. Now, we use content type negotiation between client and server which works based on headers and status codes to allow a more flexible and robust handling of requests and responses.

The third redesign in this PR is related to the API client, it is now possible to provide options like HTTP timeout per request and the response object is much more complete and self-contained, e.g. it's no longer needed to call ApiError.assert(...) everywhere (more examples below). This refactoring allows for further extensions in the future like a per route strategy for sending requests to fallback nodes and picking responses based on metadata, e.g. pick the most profitable block from all connected beacon nodes.

BREAKING CHANGES

While there is are no breaking changes to the validator client and beacon node, the client exported from @lodestar/api package changed significantly, and there are minor other changes to exported types but those are not as relevant for most consumers.

These are the most notable changes

Client initialization

before

const api = getClient({urls, getAbortSignal: () => opts.abortController.signal}, {config, logger});

after

const api = getClient({urls, globalInit: {signal: opts.abortController.signal}}, {config, logger});

Request with value

before

const res = await api.beacon.getGenesis();
ApiError.assert(res, "Can not fetch genesis data from beacon node");
const genesis = res.response.data;

after (.value() will assert if request was successful and throw detailed error)

const genesis = (await api.beacon.getGenesis()).value()

Request without value

before

ApiError.assert(await beaconClient.submitPoolVoluntaryExit(signedVoluntaryExit));

after (response object is self-contained, no utility like ApiError.assert is required anymore)

(await beaconClient.submitPoolVoluntaryExit({signedVoluntaryExit})).assertOk();

Passing arguments

before

api.validator.produceAttestationData(committeeIndex, slot)

after (args are passed as object, it is now possible to pass per request options, like timeout)

api.validator.produceAttestationData({committeeIndex, slot}, {timeoutMs: 4000})

TODO

  • [ ] review proofs routes, do we wanna follow spec proposal?
  • [ ] review lightclient routes, spec suggests to get fork version from attested_header slot, previous work from #4641? test against https://eth-light.xyz/, Access-Control-Expose-Headers: Eth-Consensus-Version header required?
  • [ ] review network identity route, use enr package / add ssz support?
  • [ ] move @param from method jsdoc to property
  • [ ] more documentation, update examples, update reasoning about API definitions
  • [ ] more http client tests, mostly in regard to retries
  • [ ] retry mechanism in http client if we receive a 415 we need to cache (per url x operationId) that server does not support it, resend different content type and on subsequent requests only sent supported content type
  • [ ] default per route config to use spec compliant content types, e.g. if spec does not define SSZ request body, do not use it by default. Should be configurable to always use SSZ in a Lodestar only setups
  • [ ] add option to server to disable JSON responses to avoid potential DoS vector, how to handle routes that do not support SSZ?
  • [ ] ensure Lodestar is compatible with latest checkpointz release (#164, #165)
  • [ ] final compatibility check with other clients and older Lodestar version using kurtosis

Closes https://github.com/ChainSafe/lodestar/issues/5128 Closes https://github.com/ChainSafe/lodestar/issues/5244 Closes https://github.com/ChainSafe/lodestar/issues/5826 Closes https://github.com/ChainSafe/lodestar/issues/6110 Closes https://github.com/ChainSafe/lodestar/issues/6564 Closes https://github.com/ChainSafe/lodestar/issues/6634

nflaig avatar May 08 '24 12:05 nflaig

Performance Report

✔️ no performance regression detected

Full benchmark results
Benchmark suite Current: f64a6919e77b664bb999ed4bf3807f5b0329be06 Previous: f6d3bce80297023aaf890f201f5e94f1e701e46c Ratio
getPubkeys - index2pubkey - req 1000 vs - 250000 vc 589.50 us/op 437.23 us/op 1.35
getPubkeys - validatorsArr - req 1000 vs - 250000 vc 41.008 us/op 45.598 us/op 0.90
BLS verify - blst-native 1.1170 ms/op 1.2364 ms/op 0.90
BLS verifyMultipleSignatures 3 - blst-native 2.3842 ms/op 2.6262 ms/op 0.91
BLS verifyMultipleSignatures 8 - blst-native 5.0184 ms/op 5.7950 ms/op 0.87
BLS verifyMultipleSignatures 32 - blst-native 18.460 ms/op 21.259 ms/op 0.87
BLS verifyMultipleSignatures 64 - blst-native 36.379 ms/op 41.856 ms/op 0.87
BLS verifyMultipleSignatures 128 - blst-native 72.226 ms/op 83.166 ms/op 0.87
BLS deserializing 10000 signatures 792.42 ms/op 865.99 ms/op 0.92
BLS deserializing 100000 signatures 7.7903 s/op 8.6819 s/op 0.90
BLS verifyMultipleSignatures - same message - 3 - blst-native 1.0824 ms/op 1.2617 ms/op 0.86
BLS verifyMultipleSignatures - same message - 8 - blst-native 1.2962 ms/op 1.4281 ms/op 0.91
BLS verifyMultipleSignatures - same message - 32 - blst-native 2.2675 ms/op 2.3466 ms/op 0.97
BLS verifyMultipleSignatures - same message - 64 - blst-native 3.4997 ms/op 3.3842 ms/op 1.03
BLS verifyMultipleSignatures - same message - 128 - blst-native 5.0360 ms/op 6.3080 ms/op 0.80
BLS aggregatePubkeys 32 - blst-native 23.419 us/op 25.600 us/op 0.91
BLS aggregatePubkeys 128 - blst-native 89.758 us/op 98.296 us/op 0.91
notSeenSlots=1 numMissedVotes=1 numBadVotes=10 60.355 ms/op 52.524 ms/op 1.15
notSeenSlots=1 numMissedVotes=0 numBadVotes=4 51.397 ms/op 49.313 ms/op 1.04
notSeenSlots=2 numMissedVotes=1 numBadVotes=10 28.590 ms/op 30.841 ms/op 0.93
getSlashingsAndExits - default max 73.860 us/op 86.798 us/op 0.85
getSlashingsAndExits - 2k 238.36 us/op 262.42 us/op 0.91
proposeBlockBody type=full, size=empty 4.8298 ms/op 5.3879 ms/op 0.90
isKnown best case - 1 super set check 476.00 ns/op 296.00 ns/op 1.61
isKnown normal case - 2 super set checks 459.00 ns/op 283.00 ns/op 1.62
isKnown worse case - 16 super set checks 457.00 ns/op 288.00 ns/op 1.59
InMemoryCheckpointStateCache - add get delete 3.9890 us/op 4.5270 us/op 0.88
validate api signedAggregateAndProof - struct 2.4035 ms/op 2.6922 ms/op 0.89
validate gossip signedAggregateAndProof - struct 2.4231 ms/op 2.6615 ms/op 0.91
validate gossip attestation - vc 640000 1.1168 ms/op 1.2730 ms/op 0.88
batch validate gossip attestation - vc 640000 - chunk 32 129.95 us/op 148.92 us/op 0.87
batch validate gossip attestation - vc 640000 - chunk 64 115.85 us/op 131.35 us/op 0.88
batch validate gossip attestation - vc 640000 - chunk 128 109.88 us/op 121.28 us/op 0.91
batch validate gossip attestation - vc 640000 - chunk 256 104.31 us/op 117.85 us/op 0.89
pickEth1Vote - no votes 822.80 us/op 1.0410 ms/op 0.79
pickEth1Vote - max votes 7.4370 ms/op 7.5142 ms/op 0.99
pickEth1Vote - Eth1Data hashTreeRoot value x2048 15.983 ms/op 15.522 ms/op 1.03
pickEth1Vote - Eth1Data hashTreeRoot tree x2048 20.618 ms/op 18.609 ms/op 1.11
pickEth1Vote - Eth1Data fastSerialize value x2048 391.51 us/op 507.22 us/op 0.77
pickEth1Vote - Eth1Data fastSerialize tree x2048 4.9297 ms/op 5.4166 ms/op 0.91
bytes32 toHexString 584.00 ns/op 428.00 ns/op 1.36
bytes32 Buffer.toString(hex) 423.00 ns/op 248.00 ns/op 1.71
bytes32 Buffer.toString(hex) from Uint8Array 518.00 ns/op 347.00 ns/op 1.49
bytes32 Buffer.toString(hex) + 0x 429.00 ns/op 247.00 ns/op 1.74
Object access 1 prop 0.32400 ns/op 0.14200 ns/op 2.28
Map access 1 prop 0.32400 ns/op 0.13600 ns/op 2.38
Object get x1000 5.1270 ns/op 5.9290 ns/op 0.86
Map get x1000 5.7940 ns/op 6.7710 ns/op 0.86
Object set x1000 22.221 ns/op 32.003 ns/op 0.69
Map set x1000 18.502 ns/op 21.918 ns/op 0.84
Return object 10000 times 0.29940 ns/op 0.28940 ns/op 1.03
Throw Error 10000 times 2.7003 us/op 3.3436 us/op 0.81
fastMsgIdFn sha256 / 200 bytes 1.9480 us/op 2.1720 us/op 0.90
fastMsgIdFn h32 xxhash / 200 bytes 400.00 ns/op 229.00 ns/op 1.75
fastMsgIdFn h64 xxhash / 200 bytes 463.00 ns/op 270.00 ns/op 1.71
fastMsgIdFn sha256 / 1000 bytes 5.9180 us/op 7.2060 us/op 0.82
fastMsgIdFn h32 xxhash / 1000 bytes 519.00 ns/op 359.00 ns/op 1.45
fastMsgIdFn h64 xxhash / 1000 bytes 525.00 ns/op 346.00 ns/op 1.52
fastMsgIdFn sha256 / 10000 bytes 49.188 us/op 63.989 us/op 0.77
fastMsgIdFn h32 xxhash / 10000 bytes 1.8240 us/op 1.7950 us/op 1.02
fastMsgIdFn h64 xxhash / 10000 bytes 1.2720 us/op 1.1890 us/op 1.07
send data - 1000 256B messages 9.6901 ms/op 12.076 ms/op 0.80
send data - 1000 512B messages 12.695 ms/op 16.345 ms/op 0.78
send data - 1000 1024B messages 24.748 ms/op 25.663 ms/op 0.96
send data - 1000 1200B messages 23.267 ms/op 26.256 ms/op 0.89
send data - 1000 2048B messages 28.857 ms/op 31.082 ms/op 0.93
send data - 1000 4096B messages 26.275 ms/op 31.245 ms/op 0.84
send data - 1000 16384B messages 66.563 ms/op 68.924 ms/op 0.97
send data - 1000 65536B messages 239.77 ms/op 209.24 ms/op 1.15
enrSubnets - fastDeserialize 64 bits 1.1500 us/op 1.0990 us/op 1.05
enrSubnets - ssz BitVector 64 bits 536.00 ns/op 356.00 ns/op 1.51
enrSubnets - fastDeserialize 4 bits 339.00 ns/op 149.00 ns/op 2.28
enrSubnets - ssz BitVector 4 bits 532.00 ns/op 356.00 ns/op 1.49
prioritizePeers score -10:0 att 32-0.1 sync 2-0 119.88 us/op 145.26 us/op 0.83
prioritizePeers score 0:0 att 32-0.25 sync 2-0.25 132.79 us/op 161.54 us/op 0.82
prioritizePeers score 0:0 att 32-0.5 sync 2-0.5 290.26 us/op 263.32 us/op 1.10
prioritizePeers score 0:0 att 64-0.75 sync 4-0.75 402.58 us/op 388.34 us/op 1.04
prioritizePeers score 0:0 att 64-1 sync 4-1 632.65 us/op 573.15 us/op 1.10
array of 16000 items push then shift 1.3114 us/op 1.6185 us/op 0.81
LinkedList of 16000 items push then shift 6.4460 ns/op 7.1390 ns/op 0.90
array of 16000 items push then pop 78.471 ns/op 105.99 ns/op 0.74
LinkedList of 16000 items push then pop 6.3520 ns/op 6.9530 ns/op 0.91
array of 24000 items push then shift 1.9371 us/op 2.3856 us/op 0.81
LinkedList of 24000 items push then shift 6.8480 ns/op 7.1220 ns/op 0.96
array of 24000 items push then pop 108.70 ns/op 130.44 ns/op 0.83
LinkedList of 24000 items push then pop 5.9420 ns/op 6.9330 ns/op 0.86
intersect bitArray bitLen 8 5.1180 ns/op 6.4760 ns/op 0.79
intersect array and set length 8 37.368 ns/op 46.424 ns/op 0.80
intersect bitArray bitLen 128 25.120 ns/op 30.410 ns/op 0.83
intersect array and set length 128 546.91 ns/op 674.88 ns/op 0.81
bitArray.getTrueBitIndexes() bitLen 128 2.3160 us/op 1.8290 us/op 1.27
bitArray.getTrueBitIndexes() bitLen 248 4.2190 us/op 3.4320 us/op 1.23
bitArray.getTrueBitIndexes() bitLen 512 6.6690 us/op 6.3250 us/op 1.05
Buffer.concat 32 items 1.0340 us/op 1.0040 us/op 1.03
Uint8Array.set 32 items 1.9890 us/op 1.6220 us/op 1.23
Buffer.copy 2.1000 us/op 1.8650 us/op 1.13
Uint8Array.set - with subarray 2.6590 us/op 2.5360 us/op 1.05
Uint8Array.set - without subarray 1.9350 us/op 1.5370 us/op 1.26
Set add up to 64 items then delete first 1.8317 us/op 2.2738 us/op 0.81
OrderedSet add up to 64 items then delete first 2.8560 us/op 3.2732 us/op 0.87
Set add up to 64 items then delete last 2.0576 us/op 2.4468 us/op 0.84
OrderedSet add up to 64 items then delete last 3.1295 us/op 3.7159 us/op 0.84
Set add up to 64 items then delete middle 2.0606 us/op 2.6005 us/op 0.79
OrderedSet add up to 64 items then delete middle 4.5305 us/op 5.2622 us/op 0.86
Set add up to 128 items then delete first 4.0679 us/op 5.3942 us/op 0.75
OrderedSet add up to 128 items then delete first 6.3876 us/op 8.4613 us/op 0.75
Set add up to 128 items then delete last 3.9427 us/op 5.1035 us/op 0.77
OrderedSet add up to 128 items then delete last 6.0037 us/op 7.4257 us/op 0.81
Set add up to 128 items then delete middle 3.9538 us/op 4.8495 us/op 0.82
OrderedSet add up to 128 items then delete middle 12.195 us/op 13.555 us/op 0.90
Set add up to 256 items then delete first 7.9707 us/op 10.311 us/op 0.77
OrderedSet add up to 256 items then delete first 12.642 us/op 16.243 us/op 0.78
Set add up to 256 items then delete last 7.7424 us/op 9.9024 us/op 0.78
OrderedSet add up to 256 items then delete last 11.960 us/op 15.398 us/op 0.78
Set add up to 256 items then delete middle 7.6989 us/op 9.4761 us/op 0.81
OrderedSet add up to 256 items then delete middle 35.261 us/op 40.874 us/op 0.86
transfer serialized Status (84 B) 1.4510 us/op 1.3200 us/op 1.10
copy serialized Status (84 B) 1.3260 us/op 1.0670 us/op 1.24
transfer serialized SignedVoluntaryExit (112 B) 1.7050 us/op 1.4100 us/op 1.21
copy serialized SignedVoluntaryExit (112 B) 1.4430 us/op 1.1360 us/op 1.27
transfer serialized ProposerSlashing (416 B) 2.4110 us/op 1.8180 us/op 1.33
copy serialized ProposerSlashing (416 B) 2.5550 us/op 1.7270 us/op 1.48
transfer serialized Attestation (485 B) 2.6740 us/op 2.0400 us/op 1.31
copy serialized Attestation (485 B) 2.5730 us/op 1.5100 us/op 1.70
transfer serialized AttesterSlashing (33232 B) 2.5240 us/op 2.0600 us/op 1.23
copy serialized AttesterSlashing (33232 B) 5.2930 us/op 4.7460 us/op 1.12
transfer serialized Small SignedBeaconBlock (128000 B) 3.2070 us/op 2.3990 us/op 1.34
copy serialized Small SignedBeaconBlock (128000 B) 10.907 us/op 14.611 us/op 0.75
transfer serialized Avg SignedBeaconBlock (200000 B) 3.3110 us/op 2.5990 us/op 1.27
copy serialized Avg SignedBeaconBlock (200000 B) 14.317 us/op 19.612 us/op 0.73
transfer serialized BlobsSidecar (524380 B) 3.3520 us/op 2.7050 us/op 1.24
copy serialized BlobsSidecar (524380 B) 74.426 us/op 195.29 us/op 0.38
transfer serialized Big SignedBeaconBlock (1000000 B) 3.3540 us/op 2.5690 us/op 1.31
copy serialized Big SignedBeaconBlock (1000000 B) 245.78 us/op 133.33 us/op 1.84
pass gossip attestations to forkchoice per slot 2.5806 ms/op 3.0969 ms/op 0.83
forkChoice updateHead vc 100000 bc 64 eq 0 417.76 us/op 503.51 us/op 0.83
forkChoice updateHead vc 600000 bc 64 eq 0 2.5545 ms/op 3.0522 ms/op 0.84
forkChoice updateHead vc 1000000 bc 64 eq 0 4.1840 ms/op 5.1607 ms/op 0.81
forkChoice updateHead vc 600000 bc 320 eq 0 2.8793 ms/op 3.0994 ms/op 0.93
forkChoice updateHead vc 600000 bc 1200 eq 0 2.5512 ms/op 3.1527 ms/op 0.81
forkChoice updateHead vc 600000 bc 7200 eq 0 2.8927 ms/op 3.5698 ms/op 0.81
forkChoice updateHead vc 600000 bc 64 eq 1000 9.5913 ms/op 11.294 ms/op 0.85
forkChoice updateHead vc 600000 bc 64 eq 10000 9.1835 ms/op 11.405 ms/op 0.81
forkChoice updateHead vc 600000 bc 64 eq 300000 12.143 ms/op 14.888 ms/op 0.82
computeDeltas 500000 validators 300 proto nodes 2.8810 ms/op 3.4913 ms/op 0.83
computeDeltas 500000 validators 1200 proto nodes 2.8887 ms/op 3.5217 ms/op 0.82
computeDeltas 500000 validators 7200 proto nodes 2.9350 ms/op 3.4650 ms/op 0.85
computeDeltas 750000 validators 300 proto nodes 4.5187 ms/op 5.1907 ms/op 0.87
computeDeltas 750000 validators 1200 proto nodes 4.3749 ms/op 5.0985 ms/op 0.86
computeDeltas 750000 validators 7200 proto nodes 4.5147 ms/op 5.1138 ms/op 0.88
computeDeltas 1400000 validators 300 proto nodes 8.1111 ms/op 9.6697 ms/op 0.84
computeDeltas 1400000 validators 1200 proto nodes 8.2289 ms/op 9.5734 ms/op 0.86
computeDeltas 1400000 validators 7200 proto nodes 8.1873 ms/op 9.5305 ms/op 0.86
computeDeltas 2100000 validators 300 proto nodes 12.444 ms/op 14.375 ms/op 0.87
computeDeltas 2100000 validators 1200 proto nodes 12.438 ms/op 14.643 ms/op 0.85
computeDeltas 2100000 validators 7200 proto nodes 13.156 ms/op 14.482 ms/op 0.91
altair processAttestation - 250000 vs - 7PWei normalcase 1.5701 ms/op 1.6363 ms/op 0.96
altair processAttestation - 250000 vs - 7PWei worstcase 2.4092 ms/op 2.4593 ms/op 0.98
altair processAttestation - setStatus - 1/6 committees join 70.664 us/op 84.204 us/op 0.84
altair processAttestation - setStatus - 1/3 committees join 139.21 us/op 169.63 us/op 0.82
altair processAttestation - setStatus - 1/2 committees join 202.03 us/op 240.32 us/op 0.84
altair processAttestation - setStatus - 2/3 committees join 258.94 us/op 317.10 us/op 0.82
altair processAttestation - setStatus - 4/5 committees join 411.60 us/op 459.19 us/op 0.90
altair processAttestation - setStatus - 100% committees join 499.73 us/op 551.53 us/op 0.91
altair processBlock - 250000 vs - 7PWei normalcase 5.0384 ms/op 3.3260 ms/op 1.51
altair processBlock - 250000 vs - 7PWei normalcase hashState 24.417 ms/op 25.603 ms/op 0.95
altair processBlock - 250000 vs - 7PWei worstcase 40.972 ms/op 38.902 ms/op 1.05
altair processBlock - 250000 vs - 7PWei worstcase hashState 70.434 ms/op 77.824 ms/op 0.91
phase0 processBlock - 250000 vs - 7PWei normalcase 2.0397 ms/op 1.7408 ms/op 1.17
phase0 processBlock - 250000 vs - 7PWei worstcase 25.442 ms/op 26.447 ms/op 0.96
altair processEth1Data - 250000 vs - 7PWei normalcase 269.87 us/op 285.63 us/op 0.94
getExpectedWithdrawals 250000 eb:1,eth1:1,we:0,wn:0,smpl:15 4.8610 us/op 5.1000 us/op 0.95
getExpectedWithdrawals 250000 eb:0.95,eth1:0.1,we:0.05,wn:0,smpl:219 18.791 us/op 17.372 us/op 1.08
getExpectedWithdrawals 250000 eb:0.95,eth1:0.3,we:0.05,wn:0,smpl:42 7.5530 us/op 7.3090 us/op 1.03
getExpectedWithdrawals 250000 eb:0.95,eth1:0.7,we:0.05,wn:0,smpl:18 5.4610 us/op 5.2740 us/op 1.04
getExpectedWithdrawals 250000 eb:0.1,eth1:0.1,we:0,wn:0,smpl:1020 79.469 us/op 71.142 us/op 1.12
getExpectedWithdrawals 250000 eb:0.03,eth1:0.03,we:0,wn:0,smpl:11777 879.22 us/op 659.50 us/op 1.33
getExpectedWithdrawals 250000 eb:0.01,eth1:0.01,we:0,wn:0,smpl:16384 658.24 us/op 919.78 us/op 0.72
getExpectedWithdrawals 250000 eb:0,eth1:0,we:0,wn:0,smpl:16384 635.80 us/op 882.51 us/op 0.72
getExpectedWithdrawals 250000 eb:0,eth1:0,we:0,wn:0,nocache,smpl:16384 2.0881 ms/op 2.0678 ms/op 1.01
getExpectedWithdrawals 250000 eb:0,eth1:1,we:0,wn:0,smpl:16384 1.1506 ms/op 1.5872 ms/op 0.72
getExpectedWithdrawals 250000 eb:0,eth1:1,we:0,wn:0,nocache,smpl:16384 2.9574 ms/op 3.4745 ms/op 0.85
Tree 40 250000 create 188.12 ms/op 226.08 ms/op 0.83
Tree 40 250000 get(125000) 108.83 ns/op 142.21 ns/op 0.77
Tree 40 250000 set(125000) 582.99 ns/op 626.82 ns/op 0.93
Tree 40 250000 toArray() 14.479 ms/op 14.612 ms/op 0.99
Tree 40 250000 iterate all - toArray() + loop 13.360 ms/op 14.818 ms/op 0.90
Tree 40 250000 iterate all - get(i) 41.355 ms/op 50.057 ms/op 0.83
MutableVector 250000 create 8.0741 ms/op 8.8068 ms/op 0.92
MutableVector 250000 get(125000) 5.6550 ns/op 6.1020 ns/op 0.93
MutableVector 250000 set(125000) 168.21 ns/op 200.48 ns/op 0.84
MutableVector 250000 toArray() 2.9482 ms/op 3.5648 ms/op 0.83
MutableVector 250000 iterate all - toArray() + loop 3.0301 ms/op 3.7118 ms/op 0.82
MutableVector 250000 iterate all - get(i) 1.3529 ms/op 1.6437 ms/op 0.82
Array 250000 create 2.5422 ms/op 3.0178 ms/op 0.84
Array 250000 clone - spread 1.2958 ms/op 1.5576 ms/op 0.83
Array 250000 get(125000) 0.56000 ns/op 0.41100 ns/op 1.36
Array 250000 set(125000) 0.57100 ns/op 0.42900 ns/op 1.33
Array 250000 iterate all - loop 73.831 us/op 107.26 us/op 0.69
effectiveBalanceIncrements clone Uint8Array 300000 21.890 us/op 29.851 us/op 0.73
effectiveBalanceIncrements clone MutableVector 300000 302.00 ns/op 127.00 ns/op 2.38
effectiveBalanceIncrements rw all Uint8Array 300000 160.15 us/op 198.73 us/op 0.81
effectiveBalanceIncrements rw all MutableVector 300000 56.212 ms/op 68.041 ms/op 0.83
phase0 afterProcessEpoch - 250000 vs - 7PWei 73.472 ms/op 83.048 ms/op 0.88
phase0 beforeProcessEpoch - 250000 vs - 7PWei 41.412 ms/op 31.003 ms/op 1.34
altair processEpoch - mainnet_e81889 349.15 ms/op 366.70 ms/op 0.95
mainnet_e81889 - altair beforeProcessEpoch 43.344 ms/op 63.927 ms/op 0.68
mainnet_e81889 - altair processJustificationAndFinalization 5.4490 us/op 12.420 us/op 0.44
mainnet_e81889 - altair processInactivityUpdates 6.0817 ms/op 5.3669 ms/op 1.13
mainnet_e81889 - altair processRewardsAndPenalties 47.765 ms/op 43.752 ms/op 1.09
mainnet_e81889 - altair processRegistryUpdates 1.2610 us/op 1.9120 us/op 0.66
mainnet_e81889 - altair processSlashings 773.00 ns/op 357.00 ns/op 2.17
mainnet_e81889 - altair processEth1DataReset 698.00 ns/op 323.00 ns/op 2.16
mainnet_e81889 - altair processEffectiveBalanceUpdates 1.3016 ms/op 1.0533 ms/op 1.24
mainnet_e81889 - altair processSlashingsReset 2.1160 us/op 3.9660 us/op 0.53
mainnet_e81889 - altair processRandaoMixesReset 3.7030 us/op 4.0530 us/op 0.91
mainnet_e81889 - altair processHistoricalRootsUpdate 757.00 ns/op 500.00 ns/op 1.51
mainnet_e81889 - altair processParticipationFlagUpdates 8.1650 us/op 1.7700 us/op 4.61
mainnet_e81889 - altair processSyncCommitteeUpdates 833.00 ns/op 385.00 ns/op 2.16
mainnet_e81889 - altair afterProcessEpoch 81.442 ms/op 91.269 ms/op 0.89
capella processEpoch - mainnet_e217614 1.3016 s/op 1.2545 s/op 1.04
mainnet_e217614 - capella beforeProcessEpoch 251.04 ms/op 236.64 ms/op 1.06
mainnet_e217614 - capella processJustificationAndFinalization 14.354 us/op 13.504 us/op 1.06
mainnet_e217614 - capella processInactivityUpdates 15.877 ms/op 15.848 ms/op 1.00
mainnet_e217614 - capella processRewardsAndPenalties 251.81 ms/op 237.27 ms/op 1.06
mainnet_e217614 - capella processRegistryUpdates 14.001 us/op 12.729 us/op 1.10
mainnet_e217614 - capella processSlashings 919.00 ns/op 336.00 ns/op 2.74
mainnet_e217614 - capella processEth1DataReset 752.00 ns/op 352.00 ns/op 2.14
mainnet_e217614 - capella processEffectiveBalanceUpdates 11.492 ms/op 4.8556 ms/op 2.37
mainnet_e217614 - capella processSlashingsReset 1.8080 us/op 2.8540 us/op 0.63
mainnet_e217614 - capella processRandaoMixesReset 3.2660 us/op 4.3130 us/op 0.76
mainnet_e217614 - capella processHistoricalRootsUpdate 1.8580 us/op 374.00 ns/op 4.97
mainnet_e217614 - capella processParticipationFlagUpdates 2.1380 us/op 2.7090 us/op 0.79
mainnet_e217614 - capella afterProcessEpoch 208.01 ms/op 227.19 ms/op 0.92
phase0 processEpoch - mainnet_e58758 343.11 ms/op 363.08 ms/op 0.95
mainnet_e58758 - phase0 beforeProcessEpoch 113.92 ms/op 110.05 ms/op 1.04
mainnet_e58758 - phase0 processJustificationAndFinalization 11.447 us/op 13.711 us/op 0.83
mainnet_e58758 - phase0 processRewardsAndPenalties 41.907 ms/op 30.215 ms/op 1.39
mainnet_e58758 - phase0 processRegistryUpdates 7.8330 us/op 7.5050 us/op 1.04
mainnet_e58758 - phase0 processSlashings 709.00 ns/op 292.00 ns/op 2.43
mainnet_e58758 - phase0 processEth1DataReset 762.00 ns/op 295.00 ns/op 2.58
mainnet_e58758 - phase0 processEffectiveBalanceUpdates 1.0921 ms/op 936.03 us/op 1.17
mainnet_e58758 - phase0 processSlashingsReset 2.5730 us/op 2.8560 us/op 0.90
mainnet_e58758 - phase0 processRandaoMixesReset 3.3930 us/op 3.3800 us/op 1.00
mainnet_e58758 - phase0 processHistoricalRootsUpdate 715.00 ns/op 339.00 ns/op 2.11
mainnet_e58758 - phase0 processParticipationRecordUpdates 2.6250 us/op 2.5390 us/op 1.03
mainnet_e58758 - phase0 afterProcessEpoch 66.811 ms/op 76.070 ms/op 0.88
phase0 processEffectiveBalanceUpdates - 250000 normalcase 1.1969 ms/op 1.5165 ms/op 0.79
phase0 processEffectiveBalanceUpdates - 250000 worstcase 0.5 1.4137 ms/op 2.0190 ms/op 0.70
altair processInactivityUpdates - 250000 normalcase 19.743 ms/op 16.041 ms/op 1.23
altair processInactivityUpdates - 250000 worstcase 19.963 ms/op 16.713 ms/op 1.19
phase0 processRegistryUpdates - 250000 normalcase 5.9800 us/op 5.7490 us/op 1.04
phase0 processRegistryUpdates - 250000 badcase_full_deposits 310.29 us/op 228.62 us/op 1.36
phase0 processRegistryUpdates - 250000 worstcase 0.5 100.54 ms/op 100.61 ms/op 1.00
altair processRewardsAndPenalties - 250000 normalcase 41.277 ms/op 39.287 ms/op 1.05
altair processRewardsAndPenalties - 250000 worstcase 43.382 ms/op 35.754 ms/op 1.21
phase0 getAttestationDeltas - 250000 normalcase 5.8250 ms/op 7.1623 ms/op 0.81
phase0 getAttestationDeltas - 250000 worstcase 6.0663 ms/op 6.8960 ms/op 0.88
phase0 processSlashings - 250000 worstcase 82.388 us/op 77.847 us/op 1.06
altair processSyncCommitteeUpdates - 250000 98.197 ms/op 121.35 ms/op 0.81
BeaconState.hashTreeRoot - No change 477.00 ns/op 278.00 ns/op 1.72
BeaconState.hashTreeRoot - 1 full validator 77.787 us/op 126.72 us/op 0.61
BeaconState.hashTreeRoot - 32 full validator 1.0224 ms/op 1.3585 ms/op 0.75
BeaconState.hashTreeRoot - 512 full validator 11.859 ms/op 14.307 ms/op 0.83
BeaconState.hashTreeRoot - 1 validator.effectiveBalance 107.61 us/op 121.69 us/op 0.88
BeaconState.hashTreeRoot - 32 validator.effectiveBalance 1.8280 ms/op 1.6098 ms/op 1.14
BeaconState.hashTreeRoot - 512 validator.effectiveBalance 17.873 ms/op 22.730 ms/op 0.79
BeaconState.hashTreeRoot - 1 balances 70.828 us/op 112.80 us/op 0.63
BeaconState.hashTreeRoot - 32 balances 637.10 us/op 1.1711 ms/op 0.54
BeaconState.hashTreeRoot - 512 balances 6.0512 ms/op 10.947 ms/op 0.55
BeaconState.hashTreeRoot - 250000 balances 126.60 ms/op 181.47 ms/op 0.70
aggregationBits - 2048 els - zipIndexesInBitList 20.197 us/op 22.387 us/op 0.90
byteArrayEquals 32 48.601 ns/op 51.585 ns/op 0.94
Buffer.compare 32 39.040 ns/op 43.624 ns/op 0.89
byteArrayEquals 1024 1.2293 us/op 1.5263 us/op 0.81
Buffer.compare 1024 42.737 ns/op 50.653 ns/op 0.84
byteArrayEquals 16384 20.004 us/op 24.305 us/op 0.82
Buffer.compare 16384 229.89 ns/op 237.79 ns/op 0.97
byteArrayEquals 123687377 148.90 ms/op 183.50 ms/op 0.81
Buffer.compare 123687377 5.3052 ms/op 6.0234 ms/op 0.88
byteArrayEquals 32 - diff last byte 45.230 ns/op 50.493 ns/op 0.90
Buffer.compare 32 - diff last byte 37.207 ns/op 47.045 ns/op 0.79
byteArrayEquals 1024 - diff last byte 1.1992 us/op 1.5264 us/op 0.79
Buffer.compare 1024 - diff last byte 42.991 ns/op 52.418 ns/op 0.82
byteArrayEquals 16384 - diff last byte 19.065 us/op 24.291 us/op 0.78
Buffer.compare 16384 - diff last byte 199.93 ns/op 232.56 ns/op 0.86
byteArrayEquals 123687377 - diff last byte 144.79 ms/op 188.41 ms/op 0.77
Buffer.compare 123687377 - diff last byte 5.3415 ms/op 6.0995 ms/op 0.88
byteArrayEquals 32 - random bytes 4.7420 ns/op 5.1090 ns/op 0.93
Buffer.compare 32 - random bytes 38.456 ns/op 47.362 ns/op 0.81
byteArrayEquals 1024 - random bytes 4.7450 ns/op 5.0890 ns/op 0.93
Buffer.compare 1024 - random bytes 36.523 ns/op 45.510 ns/op 0.80
byteArrayEquals 16384 - random bytes 4.7490 ns/op 5.0910 ns/op 0.93
Buffer.compare 16384 - random bytes 36.632 ns/op 45.615 ns/op 0.80
byteArrayEquals 123687377 - random bytes 7.6000 ns/op 6.4800 ns/op 1.17
Buffer.compare 123687377 - random bytes 39.750 ns/op 47.040 ns/op 0.85
regular array get 100000 times 29.522 us/op 43.336 us/op 0.68
wrappedArray get 100000 times 29.514 us/op 32.542 us/op 0.91
arrayWithProxy get 100000 times 9.8555 ms/op 13.244 ms/op 0.74
ssz.Root.equals 44.457 ns/op 45.598 ns/op 0.97
byteArrayEquals 43.789 ns/op 45.036 ns/op 0.97
Buffer.compare 9.1600 ns/op 10.231 ns/op 0.90
shuffle list - 16384 els 5.5299 ms/op 6.2584 ms/op 0.88
shuffle list - 250000 els 82.124 ms/op 91.626 ms/op 0.90
processSlot - 1 slots 11.634 us/op 12.946 us/op 0.90
processSlot - 32 slots 3.0960 ms/op 3.1327 ms/op 0.99
getEffectiveBalanceIncrementsZeroInactive - 250000 vs - 7PWei 44.325 ms/op 37.518 ms/op 1.18
getCommitteeAssignments - req 1 vs - 250000 vc 1.7234 ms/op 2.1545 ms/op 0.80
getCommitteeAssignments - req 100 vs - 250000 vc 3.4083 ms/op 4.1512 ms/op 0.82
getCommitteeAssignments - req 1000 vs - 250000 vc 3.6896 ms/op 4.4712 ms/op 0.83
findModifiedValidators - 10000 modified validators 232.84 ms/op 239.38 ms/op 0.97
findModifiedValidators - 1000 modified validators 146.07 ms/op 175.03 ms/op 0.83
findModifiedValidators - 100 modified validators 134.29 ms/op 160.00 ms/op 0.84
findModifiedValidators - 10 modified validators 130.43 ms/op 157.60 ms/op 0.83
findModifiedValidators - 1 modified validators 141.85 ms/op 149.14 ms/op 0.95
findModifiedValidators - no difference 142.66 ms/op 154.94 ms/op 0.92
compare ViewDUs 3.0274 s/op 2.9386 s/op 1.03
compare each validator Uint8Array 1.7207 s/op 1.5263 s/op 1.13
compare ViewDU to Uint8Array 793.16 ms/op 1.0231 s/op 0.78
migrate state 1000000 validators, 24 modified, 0 new 584.03 ms/op 586.24 ms/op 1.00
migrate state 1000000 validators, 1700 modified, 1000 new 813.54 ms/op 825.28 ms/op 0.99
migrate state 1000000 validators, 3400 modified, 2000 new 1.0144 s/op 1.0262 s/op 0.99
migrate state 1500000 validators, 24 modified, 0 new 578.41 ms/op 590.49 ms/op 0.98
migrate state 1500000 validators, 1700 modified, 1000 new 811.02 ms/op 838.99 ms/op 0.97
migrate state 1500000 validators, 3400 modified, 2000 new 997.92 ms/op 1.0279 s/op 0.97
RootCache.getBlockRootAtSlot - 250000 vs - 7PWei 6.2000 ns/op 4.4100 ns/op 1.41
state getBlockRootAtSlot - 250000 vs - 7PWei 791.07 ns/op 700.08 ns/op 1.13
computeProposers - vc 250000 5.8960 ms/op 7.5503 ms/op 0.78
computeEpochShuffling - vc 250000 82.224 ms/op 90.922 ms/op 0.90
getNextSyncCommittee - vc 250000 95.063 ms/op 128.01 ms/op 0.74
computeSigningRoot for AttestationData 20.048 us/op 22.777 us/op 0.88
hash AttestationData serialized data then Buffer.toString(base64) 1.1073 us/op 1.4687 us/op 0.75
toHexString serialized data 713.72 ns/op 860.22 ns/op 0.83
Buffer.toString(base64) 116.97 ns/op 169.29 ns/op 0.69

by benchmarkbot/action

github-actions[bot] avatar May 08 '24 13:05 github-actions[bot]

My major concern upto this point is the the lack of response codes from the routes definitions. Now use will never know any endpoint can response with 200, 206, 503, or 404.

We are putting alls calls in a promise box and saying it would be either fullfil or fail. But in case of failure user don't know what could be expected status codes.

This was a major information missing in earlier implementation and we added that in last refactor. I don't think loosing that context is a worthwhile.

We could add this by extending the Endpoint type but I honestly kinda happy that those are removed.

The reasons why I think those were not great are

  • not correctness checked against spec, e.g. getHeader claims it only returns 200 while the spec also has 204 defined (the spec also has no 404 while our types do...)
  • I have only found a single case in the code where we actually use the status code directly
  • we don't enforce those status codes anywhere to ensure implementation correctness, in current unstable, it's not even possible to set the status code from the server impl (although we can do that now)
  • the information communicated by the status code types is not very detailed, e.g. it says what status codes can be returned but not why those might be returned, making it hardly actionable without looking at the spec which contains more details

I personally don't think we should add status codes to each endpoint for reasons listed above but if we feel strongly about it could still be done.

nflaig avatar May 23 '24 20:05 nflaig

Request times from validator client point of view stable (on the left) vs. feat1 (on the right)

Last 7 days, rate interval 1d

getAttesterDuties

image

getProposerDuties

image

prepareBeaconCommitteeSubnet

image

produceAttestationData

image

submitPoolAttestations

image

produceBlockV3

image

publishBlockV2

image

nflaig avatar May 24 '24 08:05 nflaig

If something is not aligned withe specs, we should fix that rather remove that behavior. At least we had the flexibility specifying the status codes for endpoints and now we don't have it. So how can we check the correctness against specs now?

we don't enforce those status codes anywhere to ensure implementation correctness, in current unstable, it's not even possible to set the status code from the server impl (although we can do that now)

That was continuous improvement, when earlier refactor was done there was no multiple status codes for that point from server. So we have those only for client side.

the information communicated by the status code types is not very detailed, e.g. it says what status codes can be returned but not why those might be returned.

That's upto the documentation of the API. As long as our API client specify a status code, user should be able to see the docs for what it's meant.

My major concern on this approach is not yet addressed, by introducing a pattern of implicitly error we at the API client side deciding what response from server is an error and what is success. Without documenting those status codes our API client can't be generic and we have to write custom code to handle response for such endpoints.

nazarhussain avatar May 30 '24 11:05 nazarhussain

If something is not aligned withe specs, we should fix that rather remove that behavior. At least we had the flexibility specifying the status codes for endpoints and now we don't have it. So how can we check the correctness against specs now?

Status codes were only defined as types, this makes it impossible to test against spec. We can define runtime status codes (specifically for errors) as js values and derive the res.status type from those but I still don't see much value in checking those against the spec as error status codes are not used anywhere, and in addition those pretty much depend on the server implementation, meaning the server decides what error status code to return.

Testing correctness of status code is pretty conditional and can't be covered by checking against the spec, e.g. for getHealth we have e2e tests to ensure the status codes are correct which relies on beacon node being in a certain state.

https://github.com/ChainSafe/lodestar/blob/8b6ecc44c1892115d0e7396b2a5f0ae8fc0d9e6f/packages/beacon-node/test/e2e/api/impl/beacon/node/endpoints.test.ts#L152-L154

Fun fact regarding this, the status codes for getHealth were actually broken after https://github.com/ChainSafe/lodestar/pull/4994 (it always returned 200) and later fixed in https://github.com/ChainSafe/lodestar/pull/5790, which adds to my point that we didn't ensure any correctness by typing status codes.

That's upto the documentation of the API. As long as our API client specify a status code, user should be able to see the docs for what it's meant.

We could add this as part of js doc, then we could at least some context to it, e.g. getBlock returns 404 if block is not found. But I am still not convinced by this either in terms of effort maintaining this vs. value we get from it + correctness against spec is still not ensured.

Without documenting those status codes our API client can't be generic and we have to write custom code to handle response for such endpoints.

Can you point out where in the code we now require custom code? I honestly can't follow your argument, at least in the Lodestar code base typing the status code or not is irrelevant (and I reviewed all api calls multiple times)

nflaig avatar May 30 '24 15:05 nflaig

Status codes were only defined as types,

Because we didn't intended to work on the specs testing for API that time. As we are focused on it now converting status codes to values and deriving types from there is not a big issue.

Fun fact regarding this, the status codes for getHealth were actually broken after https://github.com/ChainSafe/lodestar/pull/4994 (it always returned 200) and later fixed in https://github.com/ChainSafe/lodestar/pull/5790, which adds to my point that we didn't ensure any correctness by typing status codes.

I suggest be more diligent while stating any PR broke any behavior. #4994 didn't broke any behavior, the scope of that PR was to refactor implementation of that time to status oriented response object. The feature for the custom sync status was added later.

The whole focus of previous API refactoring cycle was to convert API responses to status code based distinguishable object. Here is the objectives defined at that time https://github.com/ChainSafe/lodestar/issues/4993

  1. The API specs tend to specify two key information 1) Status Code 2) Response body
  2. The API client implementation only resolves with the response body and skip the the other piece of information
  3. For endpoints which are not returning body API client resolves to undefined
  4. For the endpoints which are returning multiple status codes e.g. GET /eth/v1/node/health, POST /eth/v1/beacon/blocks there are no way to identify what response code was returned the API call.

And here was the proposed solution:

  1. Resolve API calls with the either response body or status code. When response is available we resolve with it otherwise status code. This will introduce minimum change, but could introduce some confusion the API client users.
  2. Create a new response structure given below and always resolves with it.
{ok: boolean, status: number, [body,result]?: T}

Using ApiError.assert was also thoughtful decision to treat errors as first class objects, otherwise avoiding those would be a one line change.

@wemeetagain was fully involved into it and agreed to that approach. I feel we are reverting back to previous state without considering why we did the earlier refactor.

I align and fully upto with following refactor:

  1. Consolidating route definitions into one object
  2. Refactor in how we initialize API clients
  3. SSZ response support for the API endpoints

For these I think we are disregarding the previous effort without any proper reason and not considering a flexible structure for any future use cases.

  1. Removing status code information from the routes definitions.
  2. Removing ability from API client to handle success cases

As @wemeetagain was involved in earlier refactoring and in this one, he can better decide on above points, so I would inclined towards his judgement.

nazarhussain avatar May 31 '24 08:05 nazarhussain

I suggest be more diligent while stating any PR broke any behavior. https://github.com/ChainSafe/lodestar/pull/4994 didn't broke any behavior, the scope of that PR was to refactor implementation of that time to status oriented response object. The feature for the custom sync status was added later.

This can easily be verified by checking out the commit and querying the api while the node is syncing, it returns a 200 status code even though it should return 206. This is not hard to confirm as it literally hard coded to 200 in the handler. https://github.com/ChainSafe/lodestar/blob/460ba59b9f55afdf3d262978b96e63d7ef1f8d50/packages/api/src/beacon/server/node.ts#L23

And the server side implementation does not work, as it was based on passing down the req and res but the caller never did this. https://github.com/ChainSafe/lodestar/blob/460ba59b9f55afdf3d262978b96e63d7ef1f8d50/packages/beacon-node/src/api/impl/node/index.ts#L105

While before that PR, the status code was set in the handler correctly, see PR https://github.com/ChainSafe/lodestar/pull/3349. Although it was quite hacky as the method return the status code as a value which is not the interface of getHealth method.

With this PR, we finally have the status code (status) as a first-class citizen and can properly set it in the server implementation. https://github.com/ChainSafe/lodestar/blob/fd1936818f055547fcc17df26e81f996a5b9b22f/packages/beacon-node/src/api/impl/node/index.ts#L76

So from a server point of view, this is a strict improvement compared to previous (hacky) approaches by overriding handlers.

The whole focus of previous API refactoring cycle was to convert API responses to status code based distinguishable object. Here is the objectives defined at that time https://github.com/ChainSafe/lodestar/issues/4993

We are still able to handle all cases outlined in the PR, the res.status still works as before and can be accessed for error or success cases if needed.


I wanna address your remaining concerns but I am not clear about the expectation here

  1. Removing status code information from the routes definitions.

Can you provide an example where this is useful, generally looking at the spec, there is one success response (status 200) and a bunch of error responses (4xx, 5xx).

For success responses, there is no requirement to look at the status code, it's either res.ok=true|false, this was previously asserted by ApiError.assert as well, we never looked at the status code specifically because it's not needed for all apis with the exception of submitBlindedBlock which this PR acutally fixes (https://github.com/ChainSafe/lodestar/issues/5826) and getHealth which doesn't really change in this PR either as you can still do res.status to get the status code.

For error responses, nothing really changes either in this PR, we just have a more self-contained response object now, the error assertion is still the same !res.ok --> throw ApiError, the only difference is built-in .asserOk() vs. ApiError.assert but considering in most place of the code we now have just a single line vs. 3 lines to resolve a response, I personally like it more.

We can go down that road to specify status code like this (or similar) and check it against spec

successCodes: [200], // <-- this would be 200 for all apis besides getHealth / submitBlindedBlock
errorCodes: [404, 500]

but I don't see much value right now as we don't use status codes in any meaningful way right now, we should identify a good use case first before increasing implementation complexity and maintenance.

  1. Removing ability from API client to handle success cases

I don't see how this ability is removed? If anything we have less type hacks now when handling getHealth response, see my previous comment https://github.com/ChainSafe/lodestar/pull/6749#discussion_r1621221385. Can you please provide a concrete example for this, it might help me understand what you mean here

nflaig avatar May 31 '24 09:05 nflaig

Codecov Report

Attention: Patch coverage is 92.37816% with 416 lines in your changes missing coverage. Please review.

Project coverage is 62.77%. Comparing base (f6d3bce) to head (907f221).

Additional details and impacted files
@@             Coverage Diff              @@
##           unstable    #6749      +/-   ##
============================================
+ Coverage     62.19%   62.77%   +0.58%     
============================================
  Files           571      580       +9     
  Lines         60099    61306    +1207     
  Branches       1978     2116     +138     
============================================
+ Hits          37376    38486    +1110     
- Misses        22680    22781     +101     
+ Partials         43       39       -4     

codecov[bot] avatar Jun 05 '24 17:06 codecov[bot]

Metrics from feat3 in addition to what's already in https://github.com/ChainSafe/lodestar/pull/6749#issuecomment-2128847283, most notable difference if of course in the REST API latency, especially if we look at it from VC point of view as it includes latency over the wire, and not just serialization / deserialization.

image

image

Other metrics look good to me as well (no regressions), or even slightly better across the board (lower gc, eventloop lag, etc.)

nflaig avatar Jun 07 '24 11:06 nflaig

:tada: This PR is included in v1.20.0 :tada:

wemeetagain avatar Jul 11 '24 16:07 wemeetagain