Skip to content

Conversation

@mateuszsikora
Copy link
Contributor

@mateuszsikora mateuszsikora commented Oct 22, 2025

What?

I added an implementation of BlobDictionary that replaces our HashDictionary and TruncatedHashDictionary.

Benchmarks

The BlobDictionary has a threshold parameter that defines how many items an array must contain before it is converted into a map. The assumption here is that maps are slower than arrays when the number of items is small.

However, benchmark results show that this parameter doesn’t have a large impact. Even if we have n = 1M keys, after the first split (for hash-based keys), we end up with a map containing n keys, where all values are array nodes of length 1. In cases with long key collisions, this effect occurs deeper in the tree.

Therefore, it is important that the threshold value remains greater than zero.

Next steps

Since benchmark results indicate that the performance of BlobDictionary is comparable to StringHashDictionary, I’d like to merge it in its current form.
In subsequent PRs, I plan to:

  • optimize BytesBlob.isEqualTo by introducing a fastIsEqualTo variant that operates on U48 chunks instead of single bytes;
  • refine the internal structure to reduce single-element nodes and long paths.

Benchmark results

set operation

long collision keys

  • fastest: StringHashDictionary
  • slowest BlobDictionary(3)
{
  "name": "Comparing set operation in two hash dicts using long collition keys and BlobDictionary(n: [0: 20))",
  "date": "2025-10-27T11:43:17.285Z",
  "version": null,
  "results": [
    {
      "name": "StringHashDictionary",
      "ops": 1626.57,
      "margin": 0.43,
      "percentSlower": 0
    },
    {
      "name": "BlobDictionary(0)",
      "ops": 1577.57,
      "margin": 1.69,
      "percentSlower": 3.01
    },
    {
      "name": "BlobDictionary(1)",
      "ops": 1594.77,
      "margin": 0.26,
      "percentSlower": 1.96
    },
    {
      "name": "BlobDictionary(2)",
      "ops": 1573.65,
      "margin": 0.31,
      "percentSlower": 3.25
    },
    {
      "name": "BlobDictionary(3)",
      "ops": 1452.48,
      "margin": 3.74,
      "percentSlower": 10.7
    },
    {
      "name": "BlobDictionary(4)",
      "ops": 1471.33,
      "margin": 0.57,
      "percentSlower": 9.54
    },
    {
      "name": "BlobDictionary(5)",
      "ops": 1493.33,
      "margin": 1.46,
      "percentSlower": 8.19
    },
    {
      "name": "BlobDictionary(6)",
      "ops": 1573.14,
      "margin": 0.27,
      "percentSlower": 3.28
    },
    {
      "name": "BlobDictionary(7)",
      "ops": 1553.24,
      "margin": 2.2,
      "percentSlower": 4.51
    },
    {
      "name": "BlobDictionary(8)",
      "ops": 1568.75,
      "margin": 0.31,
      "percentSlower": 3.55
    },
    {
      "name": "BlobDictionary(9)",
      "ops": 1510.92,
      "margin": 0.31,
      "percentSlower": 7.11
    },
    {
      "name": "BlobDictionary(10)",
      "ops": 1487.96,
      "margin": 2.33,
      "percentSlower": 8.52
    },
    {
      "name": "BlobDictionary(11)",
      "ops": 1555.08,
      "margin": 0.18,
      "percentSlower": 4.4
    },
    {
      "name": "BlobDictionary(12)",
      "ops": 1556.98,
      "margin": 0.28,
      "percentSlower": 4.28
    },
    {
      "name": "BlobDictionary(13)",
      "ops": 1513.14,
      "margin": 3.18,
      "percentSlower": 6.97
    },
    {
      "name": "BlobDictionary(14)",
      "ops": 1501.18,
      "margin": 2.43,
      "percentSlower": 7.71
    },
    {
      "name": "BlobDictionary(15)",
      "ops": 1563.37,
      "margin": 0.24,
      "percentSlower": 3.89
    },
    {
      "name": "BlobDictionary(16)",
      "ops": 1507.59,
      "margin": 4.78,
      "percentSlower": 7.31
    },
    {
      "name": "BlobDictionary(17)",
      "ops": 1570.31,
      "margin": 0.27,
      "percentSlower": 3.46
    },
    {
      "name": "BlobDictionary(18)",
      "ops": 1576.92,
      "margin": 0.58,
      "percentSlower": 3.05
    },
    {
      "name": "BlobDictionary(19)",
      "ops": 1547.4,
      "margin": 0.79,
      "percentSlower": 4.87
    }
  ],
  "fastest": {
    "name": "StringHashDictionary",
    "index": 0
  },
  "slowest": {
    "name": "BlobDictionary(3)",
    "index": 4
  }
}

hash keys

  • fastest: StringHashDictionary
  • slowest BlobDictionary(0)
{
  "name": "Comparing set operation in two hash dicts using hash keys and BlobDictionary(n: [0: 20))",
  "date": "2025-10-27T11:45:10.298Z",
  "version": null,
  "results": [
    {
      "name": "StringHashDictionary",
      "ops": 4986.52,
      "margin": 0.24,
      "percentSlower": 0
    },
    {
      "name": "BlobDictionary(0)",
      "ops": 2266.64,
      "margin": 0.21,
      "percentSlower": 54.54
    },
    {
      "name": "BlobDictionary(1)",
      "ops": 4889.74,
      "margin": 0.18,
      "percentSlower": 1.94
    },
    {
      "name": "BlobDictionary(2)",
      "ops": 4838.67,
      "margin": 2.13,
      "percentSlower": 2.96
    },
    {
      "name": "BlobDictionary(3)",
      "ops": 4767.69,
      "margin": 4.87,
      "percentSlower": 4.39
    },
    {
      "name": "BlobDictionary(4)",
      "ops": 4884.92,
      "margin": 1.29,
      "percentSlower": 2.04
    },
    {
      "name": "BlobDictionary(5)",
      "ops": 4819.73,
      "margin": 2.42,
      "percentSlower": 3.34
    },
    {
      "name": "BlobDictionary(6)",
      "ops": 4860.08,
      "margin": 2.31,
      "percentSlower": 2.54
    },
    {
      "name": "BlobDictionary(7)",
      "ops": 4948.61,
      "margin": 0.32,
      "percentSlower": 0.76
    },
    {
      "name": "BlobDictionary(8)",
      "ops": 4715.45,
      "margin": 3.99,
      "percentSlower": 5.44
    },
    {
      "name": "BlobDictionary(9)",
      "ops": 4853.03,
      "margin": 1.32,
      "percentSlower": 2.68
    },
    {
      "name": "BlobDictionary(10)",
      "ops": 4714.02,
      "margin": 1.73,
      "percentSlower": 5.46
    },
    {
      "name": "BlobDictionary(11)",
      "ops": 4903.86,
      "margin": 0.47,
      "percentSlower": 1.66
    },
    {
      "name": "BlobDictionary(12)",
      "ops": 4887,
      "margin": 0.35,
      "percentSlower": 2
    },
    {
      "name": "BlobDictionary(13)",
      "ops": 4812.43,
      "margin": 1.93,
      "percentSlower": 3.49
    },
    {
      "name": "BlobDictionary(14)",
      "ops": 4806.48,
      "margin": 2.53,
      "percentSlower": 3.61
    },
    {
      "name": "BlobDictionary(15)",
      "ops": 4593.87,
      "margin": 5.33,
      "percentSlower": 7.87
    },
    {
      "name": "BlobDictionary(16)",
      "ops": 4901.81,
      "margin": 0.46,
      "percentSlower": 1.7
    },
    {
      "name": "BlobDictionary(17)",
      "ops": 4729.55,
      "margin": 2.91,
      "percentSlower": 5.15
    },
    {
      "name": "BlobDictionary(18)",
      "ops": 4853.14,
      "margin": 0.41,
      "percentSlower": 2.67
    },
    {
      "name": "BlobDictionary(19)",
      "ops": 4716.73,
      "margin": 2.85,
      "percentSlower": 5.41
    }
  ],
  "fastest": {
    "name": "StringHashDictionary",
    "index": 0
  },
  "slowest": {
    "name": "BlobDictionary(0)",
    "index": 1
  }
}

get operation

long collision keys

  • fastest: BlobDictionary(17)
  • slowest BlobDictionary(14)
{
  "name": "Comparing get operation in two hash dicts using long collition keys and BlobDictionary(n: [0: 20))",
  "date": "2025-10-27T12:02:46.554Z",
  "version": null,
  "results": [
    {
      "name": "StringHashDictionary",
      "ops": 1719.22,
      "margin": 0.29,
      "percentSlower": 0.26
    },
    {
      "name": "BlobDictionary(0)",
      "ops": 1657.11,
      "margin": 0.28,
      "percentSlower": 3.86
    },
    {
      "name": "BlobDictionary(1)",
      "ops": 1596.15,
      "margin": 2.48,
      "percentSlower": 7.4
    },
    {
      "name": "BlobDictionary(2)",
      "ops": 1640.16,
      "margin": 3.27,
      "percentSlower": 4.85
    },
    {
      "name": "BlobDictionary(3)",
      "ops": 1573.84,
      "margin": 0.27,
      "percentSlower": 8.7
    },
    {
      "name": "BlobDictionary(4)",
      "ops": 1572.14,
      "margin": 2.79,
      "percentSlower": 8.79
    },
    {
      "name": "BlobDictionary(5)",
      "ops": 1627.69,
      "margin": 2.47,
      "percentSlower": 5.57
    },
    {
      "name": "BlobDictionary(6)",
      "ops": 1593.22,
      "margin": 2.04,
      "percentSlower": 7.57
    },
    {
      "name": "BlobDictionary(7)",
      "ops": 1612.69,
      "margin": 0.57,
      "percentSlower": 6.44
    },
    {
      "name": "BlobDictionary(8)",
      "ops": 1624.41,
      "margin": 1.91,
      "percentSlower": 5.76
    },
    {
      "name": "BlobDictionary(9)",
      "ops": 1657.33,
      "margin": 1.64,
      "percentSlower": 3.85
    },
    {
      "name": "BlobDictionary(10)",
      "ops": 1630.56,
      "margin": 0.47,
      "percentSlower": 5.41
    },
    {
      "name": "BlobDictionary(11)",
      "ops": 1586.42,
      "margin": 2.21,
      "percentSlower": 7.97
    },
    {
      "name": "BlobDictionary(12)",
      "ops": 1612.57,
      "margin": 1.09,
      "percentSlower": 6.45
    },
    {
      "name": "BlobDictionary(13)",
      "ops": 1625.55,
      "margin": 1.49,
      "percentSlower": 5.7
    },
    {
      "name": "BlobDictionary(14)",
      "ops": 1548.51,
      "margin": 1.97,
      "percentSlower": 10.17
    },
    {
      "name": "BlobDictionary(15)",
      "ops": 1668.49,
      "margin": 1.81,
      "percentSlower": 3.2
    },
    {
      "name": "BlobDictionary(16)",
      "ops": 1700.54,
      "margin": 0.33,
      "percentSlower": 1.35
    },
    {
      "name": "BlobDictionary(17)",
      "ops": 1723.73,
      "margin": 0.21,
      "percentSlower": 0
    },
    {
      "name": "BlobDictionary(18)",
      "ops": 1690.07,
      "margin": 0.24,
      "percentSlower": 1.95
    },
    {
      "name": "BlobDictionary(19)",
      "ops": 1718.33,
      "margin": 0.19,
      "percentSlower": 0.31
    }
  ],
  "fastest": {
    "name": "BlobDictionary(17)",
    "index": 18
  },
  "slowest": {
    "name": "BlobDictionary(14)",
    "index": 15
  }
}

hash keys

  • fastest: BlobDictionary(2)
  • slowest BlobDictionary(0)
{
  "name": "Comparing set operation in two hash dicts using hash keys and BlobDictionary(n: [0: 20))",
  "date": "2025-10-27T12:04:37.857Z",
  "version": null,
  "results": [
    {
      "name": "StringHashDictionary",
      "ops": 5110.1,
      "margin": 2.29,
      "percentSlower": 0.86
    },
    {
      "name": "BlobDictionary(0)",
      "ops": 3952.59,
      "margin": 0.44,
      "percentSlower": 23.32
    },
    {
      "name": "BlobDictionary(1)",
      "ops": 5094.56,
      "margin": 2.09,
      "percentSlower": 1.16
    },
    {
      "name": "BlobDictionary(2)",
      "ops": 5154.48,
      "margin": 0.61,
      "percentSlower": 0
    },
    {
      "name": "BlobDictionary(3)",
      "ops": 5076.33,
      "margin": 2.49,
      "percentSlower": 1.52
    },
    {
      "name": "BlobDictionary(4)",
      "ops": 5153.97,
      "margin": 0.73,
      "percentSlower": 0.01
    },
    {
      "name": "BlobDictionary(5)",
      "ops": 5009.27,
      "margin": 1.95,
      "percentSlower": 2.82
    },
    {
      "name": "BlobDictionary(6)",
      "ops": 5043.05,
      "margin": 2.13,
      "percentSlower": 2.16
    },
    {
      "name": "BlobDictionary(7)",
      "ops": 5030.88,
      "margin": 2.56,
      "percentSlower": 2.4
    },
    {
      "name": "BlobDictionary(8)",
      "ops": 5037.44,
      "margin": 0.9,
      "percentSlower": 2.27
    },
    {
      "name": "BlobDictionary(9)",
      "ops": 5063.72,
      "margin": 1.82,
      "percentSlower": 1.76
    },
    {
      "name": "BlobDictionary(10)",
      "ops": 4752.33,
      "margin": 3.4,
      "percentSlower": 7.8
    },
    {
      "name": "BlobDictionary(11)",
      "ops": 5071.32,
      "margin": 0.34,
      "percentSlower": 1.61
    },
    {
      "name": "BlobDictionary(12)",
      "ops": 5018.51,
      "margin": 1.07,
      "percentSlower": 2.64
    },
    {
      "name": "BlobDictionary(13)",
      "ops": 4997.86,
      "margin": 0.73,
      "percentSlower": 3.04
    },
    {
      "name": "BlobDictionary(14)",
      "ops": 5107.2,
      "margin": 0.29,
      "percentSlower": 0.92
    },
    {
      "name": "BlobDictionary(15)",
      "ops": 5078.11,
      "margin": 0.61,
      "percentSlower": 1.48
    },
    {
      "name": "BlobDictionary(16)",
      "ops": 4949.73,
      "margin": 0.98,
      "percentSlower": 3.97
    },
    {
      "name": "BlobDictionary(17)",
      "ops": 5082.24,
      "margin": 0.38,
      "percentSlower": 1.4
    },
    {
      "name": "BlobDictionary(18)",
      "ops": 5062.47,
      "margin": 0.52,
      "percentSlower": 1.79
    },
    {
      "name": "BlobDictionary(19)",
      "ops": 5072.07,
      "margin": 0.36,
      "percentSlower": 1.6
    }
  ],
  "fastest": {
    "name": "BlobDictionary(2)",
    "index": 3
  },
  "slowest": {
    "name": "BlobDictionary(0)",
    "index": 1
  }
}

delete operation

long collision keys

  • fastest: BlobDictionary(0)
  • slowest BlobDictionary(2)
{
  "name": "Comparing delete operation in two hash dicts using long collition keys and BlobDictionary(n: [0: 20))",
  "date": "2025-10-27T12:07:36.511Z",
  "version": null,
  "results": [
    {
      "name": "StringHashDictionary",
      "ops": 2621.68,
      "margin": 0.17,
      "percentSlower": 0.27
    },
    {
      "name": "BlobDictionary(0)",
      "ops": 2628.71,
      "margin": 0.41,
      "percentSlower": 0
    },
    {
      "name": "BlobDictionary(1)",
      "ops": 2607.55,
      "margin": 0.2,
      "percentSlower": 0.8
    },
    {
      "name": "BlobDictionary(2)",
      "ops": 2584.75,
      "margin": 1.65,
      "percentSlower": 1.67
    },
    {
      "name": "BlobDictionary(3)",
      "ops": 2614.55,
      "margin": 0.18,
      "percentSlower": 0.54
    },
    {
      "name": "BlobDictionary(4)",
      "ops": 2616.47,
      "margin": 0.35,
      "percentSlower": 0.47
    },
    {
      "name": "BlobDictionary(5)",
      "ops": 2623.49,
      "margin": 0.14,
      "percentSlower": 0.2
    },
    {
      "name": "BlobDictionary(6)",
      "ops": 2586.4,
      "margin": 2.16,
      "percentSlower": 1.61
    },
    {
      "name": "BlobDictionary(7)",
      "ops": 2617.8,
      "margin": 0.15,
      "percentSlower": 0.42
    },
    {
      "name": "BlobDictionary(8)",
      "ops": 2622.01,
      "margin": 0.12,
      "percentSlower": 0.25
    },
    {
      "name": "BlobDictionary(9)",
      "ops": 2617.52,
      "margin": 0.16,
      "percentSlower": 0.43
    },
    {
      "name": "BlobDictionary(10)",
      "ops": 2623.32,
      "margin": 0.11,
      "percentSlower": 0.21
    },
    {
      "name": "BlobDictionary(11)",
      "ops": 2618.92,
      "margin": 0.19,
      "percentSlower": 0.37
    },
    {
      "name": "BlobDictionary(12)",
      "ops": 2627.89,
      "margin": 0.13,
      "percentSlower": 0.03
    },
    {
      "name": "BlobDictionary(13)",
      "ops": 2624.04,
      "margin": 0.11,
      "percentSlower": 0.18
    },
    {
      "name": "BlobDictionary(14)",
      "ops": 2619.39,
      "margin": 0.16,
      "percentSlower": 0.35
    },
    {
      "name": "BlobDictionary(15)",
      "ops": 2620.6,
      "margin": 0.21,
      "percentSlower": 0.31
    },
    {
      "name": "BlobDictionary(16)",
      "ops": 2613.54,
      "margin": 0.18,
      "percentSlower": 0.58
    },
    {
      "name": "BlobDictionary(17)",
      "ops": 2624.35,
      "margin": 0.12,
      "percentSlower": 0.17
    },
    {
      "name": "BlobDictionary(18)",
      "ops": 2620.31,
      "margin": 0.16,
      "percentSlower": 0.32
    },
    {
      "name": "BlobDictionary(19)",
      "ops": 2612.29,
      "margin": 0.18,
      "percentSlower": 0.62
    }
  ],
  "fastest": {
    "name": "BlobDictionary(0)",
    "index": 1
  },
  "slowest": {
    "name": "BlobDictionary(2)",
    "index": 3
  }
}

hash keys

  • fastest: BlobDictionary(2)
  • slowest BlobDictionary(0)
{
  "name": "Comparing set operation in two hash dicts using hash keys and BlobDictionary(n: [0: 20))",
  "date": "2025-10-27T12:09:27.814Z",
  "version": null,
  "results": [
    {
      "name": "StringHashDictionary",
      "ops": 6072.84,
      "margin": 1.36,
      "percentSlower": 0.79
    },
    {
      "name": "BlobDictionary(0)",
      "ops": 4370.48,
      "margin": 0.14,
      "percentSlower": 28.6
    },
    {
      "name": "BlobDictionary(1)",
      "ops": 6105.81,
      "margin": 0.2,
      "percentSlower": 0.26
    },
    {
      "name": "BlobDictionary(2)",
      "ops": 6121.44,
      "margin": 0.27,
      "percentSlower": 0
    },
    {
      "name": "BlobDictionary(3)",
      "ops": 6111.73,
      "margin": 0.19,
      "percentSlower": 0.16
    },
    {
      "name": "BlobDictionary(4)",
      "ops": 6090.02,
      "margin": 0.19,
      "percentSlower": 0.51
    },
    {
      "name": "BlobDictionary(5)",
      "ops": 6101.43,
      "margin": 0.21,
      "percentSlower": 0.33
    },
    {
      "name": "BlobDictionary(6)",
      "ops": 6102.89,
      "margin": 0.25,
      "percentSlower": 0.3
    },
    {
      "name": "BlobDictionary(7)",
      "ops": 6081.68,
      "margin": 0.28,
      "percentSlower": 0.65
    },
    {
      "name": "BlobDictionary(8)",
      "ops": 5662.35,
      "margin": 3.64,
      "percentSlower": 7.5
    },
    {
      "name": "BlobDictionary(9)",
      "ops": 5893.01,
      "margin": 3.8,
      "percentSlower": 3.73
    },
    {
      "name": "BlobDictionary(10)",
      "ops": 6047.9,
      "margin": 0.6,
      "percentSlower": 1.2
    },
    {
      "name": "BlobDictionary(11)",
      "ops": 5655.5,
      "margin": 4.66,
      "percentSlower": 7.61
    },
    {
      "name": "BlobDictionary(12)",
      "ops": 5990.74,
      "margin": 0.28,
      "percentSlower": 2.14
    },
    {
      "name": "BlobDictionary(13)",
      "ops": 5978.28,
      "margin": 0.18,
      "percentSlower": 2.34
    },
    {
      "name": "BlobDictionary(14)",
      "ops": 5977.2,
      "margin": 0.4,
      "percentSlower": 2.36
    },
    {
      "name": "BlobDictionary(15)",
      "ops": 5963.96,
      "margin": 0.43,
      "percentSlower": 2.57
    },
    {
      "name": "BlobDictionary(16)",
      "ops": 5869.93,
      "margin": 2.51,
      "percentSlower": 4.11
    },
    {
      "name": "BlobDictionary(17)",
      "ops": 6035.67,
      "margin": 0.41,
      "percentSlower": 1.4
    },
    {
      "name": "BlobDictionary(18)",
      "ops": 6022.56,
      "margin": 0.61,
      "percentSlower": 1.62
    },
    {
      "name": "BlobDictionary(19)",
      "ops": 6111.29,
      "margin": 0.21,
      "percentSlower": 0.17
    }
  ],
  "fastest": {
    "name": "BlobDictionary(2)",
    "index": 3
  },
  "slowest": {
    "name": "BlobDictionary(0)",
    "index": 1
  }
}

@mateuszsikora mateuszsikora marked this pull request as draft October 22, 2025 12:25
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 22, 2025

📝 Walkthrough

Summary by CodeRabbit

Release Notes

  • New Features

    • Introduced BlobDictionary for efficient byte-based key storage with optimized hybrid list/map structure.
    • Added iteration methods and size property to TruncatedHashDictionary.
  • Refactor

    • HashDictionary now uses BlobDictionary internally for improved performance.
    • Updated TypeScript type annotations for better generator compliance.
  • Chores

    • StringHashDictionary marked as deprecated.
  • Tests

    • Added comprehensive test coverage for BlobDictionary operations.
    • Added benchmark suites for dictionary operation comparisons.

Walkthrough

Adds a new BlobDictionary implementation for byte-keyed maps using 6-byte chunking and hybrid List/Map node storage and a helper bytesAsU48. Replaces internal HashDictionary usage with BlobDictionary in TruncatedHashDictionary, adds a new HashDictionary subclass of BlobDictionary and a deprecated StringHashDictionary alias, changes iterator return types from Generator to Iterator in ImmutableHashDictionary, adds unit tests for BlobDictionary, multiple benchmark scripts under benchmarks/, refines a generator type annotation in BytesBlob.chunks, and replaces a Node assert usage with a local deepEqual in a test runner.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

  • Areas requiring extra attention:
    • packages/core/collections/blob-dictionary.ts (core algorithms, List↔Map conversion, chunking invariants, mutation and deletion semantics)
    • packages/core/collections/hash-dictionary.ts and packages/core/collections/truncated-hash-dictionary.ts (API compatibility, constructor/representation changes)
    • packages/core/collections/blob-dictionary.test.ts (iteration order, edge-case coverage)
    • New benchmarks under benchmarks/... (correct setup, lifecycle hooks, and result persistence)
    • Exports in packages/core/collections/index.ts (public API surface changes)

Possibly related PRs

Suggested reviewers

  • skoszuta
  • tomusdrw

Poem

🐰 I nibble six bytes, bright and small,
I hop through lists and maps and all.
Keys get chunked and tucked away,
I benchmark, test, and nibble all day.
New dictionaries—what a merry ball!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.09% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'BlobDictionary' directly identifies the main feature being added in this pull request.
Description check ✅ Passed The description clearly explains the implementation of BlobDictionary, its purpose, threshold parameters, benchmark findings, and next steps.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch ms-blob-dict

Tip

📝 Customizable high-level summaries are now available in beta!

You can now customize how CodeRabbit generates the high-level summary in your pull requests — including its content, structure, tone, and formatting.

  • Provide your own instructions using the high_level_summary_instructions setting.
  • Format the summary however you like (bullet lists, tables, multi-section layouts, contributor stats, etc.).
  • Use high_level_summary_in_walkthrough to move the summary from the description to the walkthrough section.

Example instruction:

"Divide the high-level summary into five sections:

  1. 📝 Description — Summarize the main change in 50–60 words, explaining what was done.
  2. 📓 References — List relevant issues, discussions, documentation, or related PRs.
  3. 📦 Dependencies & Requirements — Mention any new/updated dependencies, environment variable changes, or configuration updates.
  4. 📊 Contributor Summary — Include a Markdown table showing contributions:
    | Contributor | Lines Added | Lines Removed | Files Changed |
  5. ✔️ Additional Notes — Add any extra reviewer context.
    Keep each section concise (under 200 words) and use bullet or numbered lists for clarity."

Note: This feature is currently in beta for Pro-tier users, and pricing will be announced later.


Comment @coderabbitai help to get the list of available commands and usage tips.

@mateuszsikora mateuszsikora marked this pull request as ready for review October 28, 2025 07:14
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/core/bytes/bytes.ts (1)

166-170: Guard against size <= 0 to prevent infinite loop.

If size is 0 or negative, i += size never progresses. Add a precondition check.

-  *chunks(size: number): Generator<BytesBlob, undefined, void> {
-    for (let i = 0; i < this.length; i += size) {
+  *chunks(size: number): Generator<BytesBlob, undefined, void> {
+    // Require a positive, non-zero chunk size
+    check`${Number.isInteger(size) && size > 0} BytesBlob.chunks(size) requires a positive integer size`;
+    for (let i = 0; i < this.length; i += size) {
       yield BytesBlob.blobFrom(this.raw.subarray(i, i + size));
     }
   }
🧹 Nitpick comments (15)
packages/core/trie/trie.test.ts (1)

356-357: Make leaves comparison order-insensitive (align with earlier pattern).

leaves() iteration order isn’t guaranteed. Compare sorted keys instead to avoid flakiness.

Based on learnings

-    assert.deepStrictEqual(Array.from(actual.nodes.leaves()), Array.from(trie.nodes.leaves()));
+    const actualLeavesSorted = Array.from(actual.nodes.leaves())
+      .map((x) => x.getKey().toString())
+      .sort();
+    const expectedLeavesSorted = Array.from(trie.nodes.leaves())
+      .map((x) => x.getKey().toString())
+      .sort();
+    assert.deepStrictEqual(actualLeavesSorted, expectedLeavesSorted);
benchmarks/bytes/bytes-to-number.ts (2)

48-49: Sanity-check both converters produce identical results before benchmarking.

Avoid benchmarking apples vs oranges; assert equality once outside the measured hot path.

-const arr = generateArrayOfHashes(ARRAY_SIZE, HASH_SIZE);
+const arr = generateArrayOfHashes(ARRAY_SIZE, HASH_SIZE);
+// Sanity-check: both methods must agree
+{
+  const ok = arr.every((x) => bytesAsU48WithBitOps(x.raw) === bytesAsU48WithoutBitOps(x.raw));
+  if (!ok) {
+    throw new Error("bytesAsU48WithBitOps and bytesAsU48WithoutBitOps disagree");
+  }
+}

33-36: Deterministic, full‑range byte generation (optional).

For reproducible runs and full byte range:

  • Use a seeded PRNG (e.g., mulberry32).
  • Use 256 to include 0xff.
-function generateHash<T extends number>(len: T): Bytes<T> {
+function mulberry32(seed: number) {
+  return function () {
+    let t = (seed += 0x6d2b79f5);
+    t = Math.imul(t ^ (t >>> 15), t | 1);
+    t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
+    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
+  };
+}
+const rand = mulberry32(0xC0FFEE);
+
+function generateHash<T extends number>(len: T): Bytes<T> {
   const result: number[] = [];
   for (let i = 0; i < len; i += 1) {
-    const val = Math.floor(Math.random() * 255);
+    const val = Math.floor(rand() * 256);
     result.push(val);
   }
   return Bytes.fromNumbers(result, len);
 }
benchmarks/collections/hash-dict-vs-blob-dict_set.ts (1)

39-47: Confirm benchmark semantics: cold inserts vs steady‑state updates.

The map is created once per test case and reused across iterations; subsequent runs measure overwrites, not pure inserts. If you want cold insert performance, construct the map inside the measured function or reset between cycles (if supported by the harness).

Option A (cold insert per invocation):

-      add(`BlobDictionary(${threshold})`, () => {
-        const map = BlobDictionary.new<OpaqueHash, number>(threshold);
-
-        return () => {
-          for (let k = 0; k < NO_OF_KEYS; k += 1) {
-            map.set(LONG_COLLISION_KEYS[k], k);
-          }
-        };
-      }),
+      add(`BlobDictionary(${threshold})`, () => {
+        return () => {
+          const map = BlobDictionary.new<OpaqueHash, number>(threshold);
+          for (let k = 0; k < NO_OF_KEYS; k += 1) {
+            map.set(LONG_COLLISION_KEYS[k], k);
+          }
+        };
+      }),

If steady‑state updates are intended, consider updating the benchmark name/description to reflect that.

Also applies to: 75-83

benchmarks/collections/hash-dict-vs-blob-dict_delete.ts (2)

53-53: Nit: fix typo in suite title (“collition” → “collision”).

-    `Comparing delete operation in two hash dicts using long collition keys and BlobDictionary(n: [${MIN_THRESHOLD}: ${MAX_THRESHOLD}))`,
+    `Comparing delete operation in two hash dicts using long collision keys and BlobDictionary(n: [${MIN_THRESHOLD}: ${MAX_THRESHOLD}))`,

94-96: Title mismatch: this suite benchmarks delete, not set.

-    `Comparing set operation in two hash dicts using hash keys and BlobDictionary(n: [${MIN_THRESHOLD}: ${MAX_THRESHOLD}))`,
+    `Comparing delete operation in two hash dicts using hash keys and BlobDictionary(n: [${MIN_THRESHOLD}: ${MAX_THRESHOLD}))`,
benchmarks/collections/hash-dict-vs-blob-dict_get.ts (2)

53-53: Nit: fix typo in suite title (“collition” → “collision”).

-    `Comparing get operation in two hash dicts using long collition keys and BlobDictionary(n: [${MIN_THRESHOLD}: ${MAX_THRESHOLD}))`,
+    `Comparing get operation in two hash dicts using long collision keys and BlobDictionary(n: [${MIN_THRESHOLD}: ${MAX_THRESHOLD}))`,

94-96: Title mismatch: this suite benchmarks get, not set.

-    `Comparing set operation in two hash dicts using hash keys and BlobDictionary(n: [${MIN_THRESHOLD}: ${MAX_THRESHOLD}))`,
+    `Comparing get operation in two hash dicts using hash keys and BlobDictionary(n: [${MIN_THRESHOLD}: ${MAX_THRESHOLD}))`,
packages/core/collections/hash-dictionary.ts (1)

109-121: Avoid duplication: export the threshold constant for reuse.

BLOB_DICTIONARY_THRESHOLD is also defined in truncated-hash-dictionary.ts. Prefer a single exported source to prevent drift.

-const BLOB_DICTIONARY_THRESHOLD = 5;
+export const BLOB_DICTIONARY_THRESHOLD = 5;

Then import and reuse it in packages/core/collections/truncated-hash-dictionary.ts. Based on learnings.

packages/core/collections/blob-dictionary.test.ts (2)

7-7: Fix test typos.

-const TRESHOLDS = [0, 5, 10];
+const THRESHOLDS = [0, 5, 10];
@@
-    it("should distuingigh shorted chunk and padded with 0s", () => {
+    it("should distinguish short chunk vs the same chunk padded with 0s", () => {

Also update for (const threshold of TRESHOLDS) usages accordingly.

Also applies to: 29-37


10-27: Add a max-value edge case for bytesAsU48.

To guard against sign/bitwise pitfalls, add an all-0xff 6‑byte case.

   describe("bytesAsU48", () => {
@@
     it("should convert empty bytes to 0", () => {
       const bytes = BytesBlob.parseBlob("0x").raw;
       const expectedResult = 0;
 
       const result = bytesAsU48(bytes);
 
       assert.strictEqual(result, expectedResult);
     });
 
+    it("should handle 0xffffffffffff (6 bytes all ones)", () => {
+      const bytes = BytesBlob.parseBlob("0xffffffffffff").raw;
+      const expectedResult = (2 ** 48 - 1) * 8 + 6;
+      const result = bytesAsU48(bytes);
+      assert.strictEqual(result, expectedResult);
+    });

This will surface any 32‑bit bitwise overflow issues in the implementation. Based on learnings.

packages/core/collections/blob-dictionary.ts (4)

346-358: Make bytesAsU48 purely arithmetic; avoid 32‑bit bitwise sign issues.

Bitwise shifts coerce to signed 32‑bit and can introduce negative intermediates. Pure arithmetic keeps it simple and still exact (max ≈ 2^51 < 2^53).

 export function bytesAsU48(bytes: Uint8Array): number {
   const len = bytes.length;

   check`${len <= CHUNK_SIZE} Length has to be <= ${CHUNK_SIZE}, got: ${len}`;

-  let value = bytes[3] | (bytes[2] << 8) | (bytes[1] << 16) | (bytes[0] << 24);
-
-  for (let i = 4; i < bytes.length; i++) {
-    value = value * 256 + bytes[i];
-  }
+  let value = 0;
+  for (let i = 0; i < len; i++) {
+    value = value * 256 + bytes[i];
+  }

   return value * 8 + len;
 }

187-215: Early‑return in get() for ListChildren to avoid extra iterations.

When at a ListChildren node, the remainder subkey is definitive: return the found leaf, the node leaf if remainder is empty, or undefined. This avoids repeatedly advancing the generator without changing node depth.

   get(key: K): V | undefined {
-    let node: MaybeNode<K, V> = this.root;
-    const pathChunksGenerator = key.chunks(CHUNK_SIZE);
-    let depth = 0;
-
-    while (node !== undefined) {
-      const maybePathChunk = pathChunksGenerator.next().value;
-
-      if (node.children instanceof ListChildren) {
-        const subkey = BytesBlob.blobFrom(key.raw.subarray(depth * CHUNK_SIZE));
-        const child = node.children.find(subkey);
-        if (child !== null) {
-          return child.value;
-        }
-      }
-
-      if (maybePathChunk === undefined) {
-        return node.getLeaf()?.value;
-      }
-
-      if (node.children instanceof MapChildren) {
-        const pathChunk: KeyChunk = asOpaqueType(maybePathChunk);
-        node = node.children.getChild(pathChunk);
-        depth += 1;
-      }
-    }
-
-    return undefined;
+    let node: MaybeNode<K, V> = this.root;
+    const pathChunks = key.chunks(CHUNK_SIZE);
+    let depth = 0;
+
+    for (;;) {
+      if (node === undefined) return undefined;
+
+      if (node.children instanceof ListChildren) {
+        const subkey = BytesBlob.blobFrom(key.raw.subarray(depth * CHUNK_SIZE));
+        const child = node.children.find(subkey);
+        if (child !== null) return child.value;
+        return subkey.length === 0 ? node.getLeaf()?.value : undefined;
+      }
+
+      const maybePathChunk = pathChunks.next().value;
+      if (maybePathChunk === undefined) {
+        return node.getLeaf()?.value;
+      }
+
+      const pathChunk: KeyChunk = asOpaqueType(maybePathChunk);
+      node = (node.children as MapChildren<K, V>).getChild(pathChunk);
+      depth += 1;
+    }
   }

119-129: Compute keyChunk only for MapChildren branch to avoid unused work.

keyChunk is unused when the current node has ListChildren and we return early; compute it lazily in the MapChildren branch.

-      const keyChunk: KeyChunk = asOpaqueType(maybeKeyChunk);
-
       if (node.children instanceof ListChildren) {
         const subkey = BytesBlob.blobFrom(key.raw.subarray(CHUNK_SIZE * depth));
         const leaf = value !== undefined ? node.children.insert(subkey, { key, value }) : node.children.remove(subkey);
@@
-      if (children instanceof MapChildren) {
-        const maybeNode = children.getChild(keyChunk);
+      if (children instanceof MapChildren) {
+        const keyChunk: KeyChunk = asOpaqueType(maybeKeyChunk);
+        const maybeNode = children.getChild(keyChunk);

Also applies to: 138-156


69-71: Default threshold: consider > 0 as per benchmark note.

Benchmarks indicate threshold 0 underperforms; consider a safer default (e.g., 2–3) or validate input to discourage 0 in production paths.

Do we want to change the default or at least assert non‑negative and document recommended values?

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a46bfc7 and c575c11.

📒 Files selected for processing (12)
  • benchmarks/bytes/bytes-to-number.ts (1 hunks)
  • benchmarks/collections/hash-dict-vs-blob-dict_delete.ts (1 hunks)
  • benchmarks/collections/hash-dict-vs-blob-dict_get.ts (1 hunks)
  • benchmarks/collections/hash-dict-vs-blob-dict_set.ts (1 hunks)
  • bin/test-runner/w3f/preimages.ts (1 hunks)
  • packages/core/bytes/bytes.ts (1 hunks)
  • packages/core/collections/blob-dictionary.test.ts (1 hunks)
  • packages/core/collections/blob-dictionary.ts (1 hunks)
  • packages/core/collections/hash-dictionary.ts (2 hunks)
  • packages/core/collections/index.ts (1 hunks)
  • packages/core/collections/truncated-hash-dictionary.ts (3 hunks)
  • packages/core/trie/trie.test.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
packages/core/**/*.ts

⚙️ CodeRabbit configuration file

packages/core/**/*.ts: Core packages must not import any JAM-related packages
(i.e. packages defined under packages/jam/**)

Files:

  • packages/core/bytes/bytes.ts
  • packages/core/collections/index.ts
  • packages/core/collections/blob-dictionary.test.ts
  • packages/core/trie/trie.test.ts
  • packages/core/collections/blob-dictionary.ts
  • packages/core/collections/truncated-hash-dictionary.ts
  • packages/core/collections/hash-dictionary.ts
**/*.ts

⚙️ CodeRabbit configuration file

**/*.ts: rules from ./CODESTYLE.md should be adhered to.

**/*.ts: Any function whose documentation mention it must not be used in production code,
can be safely used in *.test.ts files. Other usage should be carefuly reviewed
and the comment must explain why it's safe to use there.

**/*.ts: as conversions must not be used. Suggest using tryAs conversion methods.

**/*.ts: Classes with static Codec field must have private constructor and static create method.

**/*.ts: Casting a bigint (or U64) using Number(x) must have an explanation comment why
it is safe.

**/*.ts: When making changes to code with comments containing links (in classes, constants, methods, etc.)
to graypaper.fluffylabs.dev, ensure those links point to the current version for this update.

Files:

  • packages/core/bytes/bytes.ts
  • packages/core/collections/index.ts
  • bin/test-runner/w3f/preimages.ts
  • packages/core/collections/blob-dictionary.test.ts
  • benchmarks/collections/hash-dict-vs-blob-dict_set.ts
  • benchmarks/bytes/bytes-to-number.ts
  • packages/core/trie/trie.test.ts
  • benchmarks/collections/hash-dict-vs-blob-dict_get.ts
  • benchmarks/collections/hash-dict-vs-blob-dict_delete.ts
  • packages/core/collections/blob-dictionary.ts
  • packages/core/collections/truncated-hash-dictionary.ts
  • packages/core/collections/hash-dictionary.ts
🧠 Learnings (1)
📚 Learning: 2025-06-10T12:11:27.374Z
Learnt from: tomusdrw
PR: FluffyLabs/typeberry#419
File: packages/core/trie/nodesDb.ts:26-33
Timestamp: 2025-06-10T12:11:27.374Z
Learning: In the trie implementation (`packages/core/trie/nodesDb.ts`), the `leaves()` method intentionally yields both `NodeType.Leaf` and `NodeType.EmbedLeaf` nodes by filtering out only `NodeType.Branch` nodes. Both leaf types are valid leaf nodes that should be included in leaf iteration.

Applied to files:

  • packages/core/trie/trie.test.ts
🧬 Code graph analysis (9)
bin/test-runner/w3f/preimages.ts (1)
packages/core/utils/test.ts (1)
  • deepEqual (43-195)
packages/core/collections/blob-dictionary.test.ts (2)
packages/core/bytes/bytes.ts (2)
  • BytesBlob (18-171)
  • Bytes (176-242)
packages/core/collections/blob-dictionary.ts (1)
  • bytesAsU48 (346-358)
benchmarks/collections/hash-dict-vs-blob-dict_set.ts (4)
packages/core/bytes/bytes.ts (1)
  • Bytes (176-242)
packages/core/collections/blob-dictionary.ts (1)
  • BlobDictionary (6-335)
packages/core/hash/hash.ts (1)
  • OpaqueHash (18-18)
packages/core/collections/hash-dictionary.ts (1)
  • StringHashDictionary (27-105)
benchmarks/bytes/bytes-to-number.ts (2)
packages/core/bytes/bytes.ts (1)
  • Bytes (176-242)
packages/misc/benchmark/setup.ts (2)
  • configure (12-15)
  • save (17-36)
benchmarks/collections/hash-dict-vs-blob-dict_get.ts (5)
packages/core/bytes/bytes.ts (1)
  • Bytes (176-242)
packages/misc/benchmark/setup.ts (3)
  • add (1-1)
  • configure (12-15)
  • save (17-36)
packages/core/collections/blob-dictionary.ts (1)
  • BlobDictionary (6-335)
packages/core/hash/hash.ts (1)
  • OpaqueHash (18-18)
packages/core/collections/hash-dictionary.ts (1)
  • StringHashDictionary (27-105)
benchmarks/collections/hash-dict-vs-blob-dict_delete.ts (4)
packages/core/bytes/bytes.ts (1)
  • Bytes (176-242)
packages/core/collections/blob-dictionary.ts (1)
  • BlobDictionary (6-335)
packages/core/hash/hash.ts (1)
  • OpaqueHash (18-18)
packages/core/collections/hash-dictionary.ts (1)
  • StringHashDictionary (27-105)
packages/core/collections/blob-dictionary.ts (3)
packages/core/bytes/bytes.ts (2)
  • BytesBlob (18-171)
  • TEST_COMPARE_USING (19-21)
packages/core/utils/opaque.ts (2)
  • asOpaqueType (37-39)
  • Opaque (33-35)
packages/core/utils/debug.ts (2)
  • assertNever (36-38)
  • check (15-26)
packages/core/collections/truncated-hash-dictionary.ts (3)
packages/core/hash/hash.ts (6)
  • OpaqueHash (18-18)
  • TruncatedHash (27-27)
  • HASH_SIZE (10-10)
  • HASH_SIZE (11-11)
  • TRUNCATED_HASH_SIZE (14-14)
  • TRUNCATED_HASH_SIZE (15-15)
packages/core/bytes/bytes.ts (1)
  • Bytes (176-242)
packages/core/collections/blob-dictionary.ts (3)
  • BlobDictionary (6-335)
  • entries (295-299)
  • TEST_COMPARE_USING (54-58)
packages/core/collections/hash-dictionary.ts (2)
packages/core/hash/hash.ts (1)
  • OpaqueHash (18-18)
packages/core/collections/blob-dictionary.ts (1)
  • BlobDictionary (6-335)
⏰ Context from checks skipped due to timeout of 120000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: benchmarks (22.x)
  • GitHub Check: state_transition (22.x)
  • GitHub Check: run (22.x)
  • GitHub Check: run (22.x)
  • GitHub Check: test (22.x)
🔇 Additional comments (3)
bin/test-runner/w3f/preimages.ts (1)

126-126: LGTM: Consistent use of internal deepEqual utility.

The change from Node's assert.deepEqual to the internal deepEqual utility is appropriate and maintains consistency with the existing usage on line 122. This removes the external dependency and provides more comprehensive comparison capabilities for complex objects.

packages/core/bytes/bytes.ts (1)

67-71: Verify graypaper link points to current version.

We touched this file; coding guideline asks to ensure graypaper links are current when code changes are made. Please confirm the URL remains valid and version-correct.

As per coding guidelines

packages/core/collections/index.ts (1)

2-2: Public export looks good.

Re-exporting blob-dictionary from collections index is correct and consistent.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
benchmarks/collections/hash-dict-vs-blob-dict_delete.ts (1)

15-21: Compile-time fix: use BytesBlob.blobFromString (and import it).

Same issue as in set.ts; update import and call site.

Apply:

-import { Bytes } from "@typeberry/bytes";
+import { Bytes, BytesBlob } from "@typeberry/bytes";
@@
 function longCollisionKey(n: number) {
-  const key = Bytes.blobFromString(`${n}`);
+  const key = BytesBlob.blobFromString(`${n}`);
   const ret = Bytes.zero(HASH_SIZE);
   ret.raw.set(key.raw, 0);
   ret.raw.reverse();
   return ret;
 }

Scan for remaining usages:

#!/bin/bash
rg -nP --type=ts '\bBytes\.blobFromString\(' -C2
🧹 Nitpick comments (2)
benchmarks/collections/hash-dict-vs-blob-dict_set.ts (1)

39-47: Benchmark integrity: reset state each iteration to measure consistent inserts.

Maps are created once in the factory; subsequent iterations measure overwrites into a hot map. Create the map inside the measured fn.

Apply:

       add(`BlobDictionary(${threshold})`, () => {
-        const map = BlobDictionary.new<OpaqueHash, number>(threshold);
-
-        return () => {
-          for (let k = 0; k < NO_OF_KEYS; k += 1) {
-            map.set(LONG_COLLISION_KEYS[k], k);
-          }
-        };
+        return () => {
+          const map = BlobDictionary.new<OpaqueHash, number>(threshold);
+          for (let k = 0; k < NO_OF_KEYS; k += 1) {
+            map.set(LONG_COLLISION_KEYS[k], k);
+          }
+        };
       }),
@@
     add("StringHashDictionary", () => {
-      const map = StringHashDictionary.new<OpaqueHash, number>();
-
-      return () => {
-        for (let k = 0; k < NO_OF_KEYS; k += 1) {
-          map.set(LONG_COLLISION_KEYS[k], k);
-        }
-      };
+      return () => {
+        const map = StringHashDictionary.new<OpaqueHash, number>();
+        for (let k = 0; k < NO_OF_KEYS; k += 1) {
+          map.set(LONG_COLLISION_KEYS[k], k);
+        }
+      };
     }),
@@
       add(`BlobDictionary(${threshold})`, () => {
-        const map = BlobDictionary.new<OpaqueHash, number>(threshold);
-
-        return () => {
-          for (let k = 0; k < NO_OF_KEYS; k += 1) {
-            map.set(HASH_KEYS[k], k);
-          }
-        };
+        return () => {
+          const map = BlobDictionary.new<OpaqueHash, number>(threshold);
+          for (let k = 0; k < NO_OF_KEYS; k += 1) {
+            map.set(HASH_KEYS[k], k);
+          }
+        };
       }),
@@
     add("StringHashDictionary", () => {
-      const map = StringHashDictionary.new<OpaqueHash, number>();
-
-      return () => {
-        for (let k = 0; k < NO_OF_KEYS; k += 1) {
-          map.set(HASH_KEYS[k], k);
-        }
-      };
+      return () => {
+        const map = StringHashDictionary.new<OpaqueHash, number>();
+        for (let k = 0; k < NO_OF_KEYS; k += 1) {
+          map.set(HASH_KEYS[k], k);
+        }
+      };
     }),

Confirm benchmark harness calls the returned fn multiple times per test; if it supports per-iteration setup hooks, prefer those instead.

Also applies to: 54-61, 75-83, 90-98

benchmarks/collections/hash-dict-vs-blob-dict_delete.ts (1)

39-49: Benchmark integrity: keep the dataset non-empty across iterations.

Maps are prefilled once in the factory; after the first measured run, deletes empty the map and later iterations delete non-existent keys. Recreate/populate inside the measured fn.

Apply:

       add(`BlobDictionary(${threshold})`, () => {
-        const map = BlobDictionary.new<OpaqueHash, number>(threshold);
-        for (let k = 0; k < NO_OF_KEYS; k += 1) {
-          map.set(LONG_COLLISION_KEYS[k], k);
-        }
-        return () => {
-          for (let k = 0; k < NO_OF_KEYS; k += 1) {
-            map.delete(LONG_COLLISION_KEYS[k]);
-          }
-        };
+        return () => {
+          const map = BlobDictionary.new<OpaqueHash, number>(threshold);
+          for (let k = 0; k < NO_OF_KEYS; k += 1) {
+            map.set(LONG_COLLISION_KEYS[k], k);
+          }
+          for (let k = 0; k < NO_OF_KEYS; k += 1) {
+            map.delete(LONG_COLLISION_KEYS[k]);
+          }
+        };
       }),
@@
     add("StringHashDictionary", () => {
-      const map = StringHashDictionary.new<OpaqueHash, number>();
-      for (let k = 0; k < NO_OF_KEYS; k += 1) {
-        map.set(LONG_COLLISION_KEYS[k], k);
-      }
-      return () => {
-        for (let k = 0; k < NO_OF_KEYS; k += 1) {
-          map.delete(LONG_COLLISION_KEYS[k]);
-        }
-      };
+      return () => {
+        const map = StringHashDictionary.new<OpaqueHash, number>();
+        for (let k = 0; k < NO_OF_KEYS; k += 1) {
+          map.set(LONG_COLLISION_KEYS[k], k);
+        }
+        for (let k = 0; k < NO_OF_KEYS; k += 1) {
+          map.delete(LONG_COLLISION_KEYS[k]);
+        }
+      };
     }),
@@
       add(`BlobDictionary(${threshold})`, () => {
-        const map = BlobDictionary.new<OpaqueHash, number>(threshold);
-        for (let k = 0; k < NO_OF_KEYS; k += 1) {
-          map.set(HASH_KEYS[k], k);
-        }
-        return () => {
-          for (let k = 0; k < NO_OF_KEYS; k += 1) {
-            map.delete(HASH_KEYS[k]);
-          }
-        };
+        return () => {
+          const map = BlobDictionary.new<OpaqueHash, number>(threshold);
+          for (let k = 0; k < NO_OF_KEYS; k += 1) {
+            map.set(HASH_KEYS[k], k);
+          }
+          for (let k = 0; k < NO_OF_KEYS; k += 1) {
+            map.delete(HASH_KEYS[k]);
+          }
+        };
       }),
@@
     add("StringHashDictionary", () => {
-      const map = StringHashDictionary.new<OpaqueHash, number>();
-      for (let k = 0; k < NO_OF_KEYS; k += 1) {
-        map.set(HASH_KEYS[k], k);
-      }
-      return () => {
-        for (let k = 0; k < NO_OF_KEYS; k += 1) {
-          map.delete(HASH_KEYS[k]);
-        }
-      };
+      return () => {
+        const map = StringHashDictionary.new<OpaqueHash, number>();
+        for (let k = 0; k < NO_OF_KEYS; k += 1) {
+          map.set(HASH_KEYS[k], k);
+        }
+        for (let k = 0; k < NO_OF_KEYS; k += 1) {
+          map.delete(HASH_KEYS[k]);
+        }
+      };
     }),

If the harness supports beforeEach/afterEach hooks, use them instead to avoid measuring setup costs while ensuring consistent starting state.

Also applies to: 55-65, 79-89, 96-106

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c575c11 and 8c9af5a.

📒 Files selected for processing (3)
  • benchmarks/collections/hash-dict-vs-blob-dict_delete.ts (1 hunks)
  • benchmarks/collections/hash-dict-vs-blob-dict_get.ts (1 hunks)
  • benchmarks/collections/hash-dict-vs-blob-dict_set.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • benchmarks/collections/hash-dict-vs-blob-dict_get.ts
🧰 Additional context used
📓 Path-based instructions (1)
**/*.ts

⚙️ CodeRabbit configuration file

**/*.ts: rules from ./CODESTYLE.md should be adhered to.

**/*.ts: Any function whose documentation mention it must not be used in production code,
can be safely used in *.test.ts files. Other usage should be carefuly reviewed
and the comment must explain why it's safe to use there.

**/*.ts: as conversions must not be used. Suggest using tryAs conversion methods.

**/*.ts: Classes with static Codec field must have private constructor and static create method.

**/*.ts: Casting a bigint (or U64) using Number(x) must have an explanation comment why
it is safe.

**/*.ts: When making changes to code with comments containing links (in classes, constants, methods, etc.)
to graypaper.fluffylabs.dev, ensure those links point to the current version for this update.

Files:

  • benchmarks/collections/hash-dict-vs-blob-dict_delete.ts
  • benchmarks/collections/hash-dict-vs-blob-dict_set.ts
🧬 Code graph analysis (2)
benchmarks/collections/hash-dict-vs-blob-dict_delete.ts (5)
packages/core/bytes/bytes.ts (1)
  • Bytes (176-242)
packages/misc/benchmark/setup.ts (2)
  • configure (12-15)
  • save (17-36)
packages/core/collections/blob-dictionary.ts (1)
  • BlobDictionary (6-335)
packages/core/hash/hash.ts (1)
  • OpaqueHash (18-18)
packages/core/collections/hash-dictionary.ts (1)
  • StringHashDictionary (27-105)
benchmarks/collections/hash-dict-vs-blob-dict_set.ts (5)
packages/core/bytes/bytes.ts (1)
  • Bytes (176-242)
packages/misc/benchmark/setup.ts (3)
  • add (1-1)
  • configure (12-15)
  • save (17-36)
packages/core/collections/blob-dictionary.ts (1)
  • BlobDictionary (6-335)
packages/core/hash/hash.ts (1)
  • OpaqueHash (18-18)
packages/core/collections/hash-dictionary.ts (1)
  • StringHashDictionary (27-105)
⏰ Context from checks skipped due to timeout of 120000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: run (22.x)
  • GitHub Check: run (22.x)
  • GitHub Check: benchmarks (22.x)
  • GitHub Check: e2e (22.x)
  • GitHub Check: run (22.x)
  • GitHub Check: test (22.x)

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
benchmarks/collections/hash-dict-vs-blob-dict_get.ts (1)

3-3: Critical: Import BytesBlob and use BytesBlob.blobFromString.

This issue was already flagged in a previous review. Line 3 must import BytesBlob from @typeberry/bytes, and line 16 must use BytesBlob.blobFromString instead of Bytes.blobFromString.

Apply this diff:

-import { Bytes } from "@typeberry/bytes";
+import { Bytes, BytesBlob } from "@typeberry/bytes";
 function longCollisionKey(n: number) {
-  const key = Bytes.blobFromString(`${n}`);
+  const key = BytesBlob.blobFromString(`${n}`);
   const ret = Bytes.zero(HASH_SIZE);
   ret.raw.set(key.raw, 0);
   ret.raw.reverse();
   return ret;
 }

Also applies to: 16-16

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8c9af5a and cb1d6c0.

📒 Files selected for processing (3)
  • benchmarks/collections/hash-dict-vs-blob-dict_delete.ts (1 hunks)
  • benchmarks/collections/hash-dict-vs-blob-dict_get.ts (1 hunks)
  • benchmarks/collections/hash-dict-vs-blob-dict_set.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • benchmarks/collections/hash-dict-vs-blob-dict_delete.ts
  • benchmarks/collections/hash-dict-vs-blob-dict_set.ts
🧰 Additional context used
📓 Path-based instructions (1)
**/*.ts

⚙️ CodeRabbit configuration file

**/*.ts: rules from ./CODESTYLE.md should be adhered to.

**/*.ts: Any function whose documentation mention it must not be used in production code,
can be safely used in *.test.ts files. Other usage should be carefuly reviewed
and the comment must explain why it's safe to use there.

**/*.ts: as conversions must not be used. Suggest using tryAs conversion methods.

**/*.ts: Classes with static Codec field must have private constructor and static create method.

**/*.ts: Casting a bigint (or U64) using Number(x) must have an explanation comment why
it is safe.

**/*.ts: When making changes to code with comments containing links (in classes, constants, methods, etc.)
to graypaper.fluffylabs.dev, ensure those links point to the current version for this update.

Files:

  • benchmarks/collections/hash-dict-vs-blob-dict_get.ts
🧬 Code graph analysis (1)
benchmarks/collections/hash-dict-vs-blob-dict_get.ts (4)
packages/core/bytes/bytes.ts (1)
  • Bytes (176-242)
packages/core/collections/blob-dictionary.ts (1)
  • BlobDictionary (6-335)
packages/core/hash/hash.ts (1)
  • OpaqueHash (18-18)
packages/core/collections/hash-dictionary.ts (1)
  • StringHashDictionary (27-105)
⏰ Context from checks skipped due to timeout of 120000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: run (22.x)
  • GitHub Check: run (22.x)
  • GitHub Check: benchmarks (22.x)
  • GitHub Check: run (22.x)

@mateuszsikora mateuszsikora self-assigned this Oct 28, 2025
@tomusdrw
Copy link
Contributor

@mateuszsikora can you address the two remaining comments?

@mateuszsikora
Copy link
Contributor Author

@mateuszsikora can you address the two remaining comments?

done

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
packages/core/collections/truncated-hash-dictionary.ts (1)

11-15: Consider importing the threshold constant if it exists elsewhere.

If BLOB_DICTIONARY_THRESHOLD is also defined in hash-dictionary.ts, importing it here would eliminate duplication and ensure both dictionaries use the same threshold.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b9ae614 and 493f501.

📒 Files selected for processing (3)
  • benchmarks/collections/hash-dict-vs-blob-dict_delete.ts (1 hunks)
  • benchmarks/collections/hash-dict-vs-blob-dict_get.ts (1 hunks)
  • packages/core/collections/truncated-hash-dictionary.ts (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • benchmarks/collections/hash-dict-vs-blob-dict_delete.ts
  • benchmarks/collections/hash-dict-vs-blob-dict_get.ts
🧰 Additional context used
📓 Path-based instructions (2)
packages/core/**/*.ts

⚙️ CodeRabbit configuration file

packages/core/**/*.ts: Core packages must not import any JAM-related packages
(i.e. packages defined under packages/jam/**)

Files:

  • packages/core/collections/truncated-hash-dictionary.ts
**/*.ts

⚙️ CodeRabbit configuration file

**/*.ts: rules from ./CODESTYLE.md should be adhered to.

**/*.ts: Any function whose documentation mention it must not be used in production code,
can be safely used in *.test.ts files. Other usage should be carefuly reviewed
and the comment must explain why it's safe to use there.

**/*.ts: as conversions must not be used. Suggest using tryAs conversion methods.

**/*.ts: Classes with static Codec field must have private constructor and static create method.

**/*.ts: Casting a bigint (or U64) using Number(x) must have an explanation comment why
it is safe.

**/*.ts: When making changes to code with comments containing links (in classes, constants, methods, etc.)
to graypaper.fluffylabs.dev, ensure those links point to the current version for this update.

Files:

  • packages/core/collections/truncated-hash-dictionary.ts
🧠 Learnings (2)
📓 Common learnings
Learnt from: tomusdrw
Repo: FluffyLabs/typeberry PR: 351
File: packages/jam/state-merkleization/index.ts:19-37
Timestamp: 2025-04-24T19:48:31.051Z
Learning: In the typeberry codebase, `Bytes` inherits from `BytesBlob`, so a `Bytes` instance can be directly used where a `BytesBlob` is expected without needing conversion.
📚 Learning: 2025-06-10T12:20:17.513Z
Learnt from: tomusdrw
Repo: FluffyLabs/typeberry PR: 419
File: packages/jam/state/state-inmemory.ts:141-149
Timestamp: 2025-06-10T12:20:17.513Z
Learning: In the `InMemoryService.copyFrom` function in `packages/jam/state/state-inmemory.ts`, duplicate checking for `(hash, length)` pairs in the lookup history is not necessary because the function operates under the assumption that the input `ServiceEntries` comes from an existing well-formed state, which already maintains the invariant of unique lookup history entries per hash and length combination.

Applied to files:

  • packages/core/collections/truncated-hash-dictionary.ts
🧬 Code graph analysis (1)
packages/core/collections/truncated-hash-dictionary.ts (3)
packages/core/hash/hash.ts (4)
  • OpaqueHash (18-18)
  • TruncatedHash (27-27)
  • TRUNCATED_HASH_SIZE (14-14)
  • TRUNCATED_HASH_SIZE (15-15)
packages/core/bytes/bytes.ts (1)
  • Bytes (177-243)
packages/core/collections/blob-dictionary.ts (3)
  • BlobDictionary (6-335)
  • entries (295-299)
  • TEST_COMPARE_USING (54-58)
⏰ Context from checks skipped due to timeout of 120000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: run (22.x)
  • GitHub Check: state_transition (22.x)
  • GitHub Check: run (22.x)
  • GitHub Check: run (22.x)
  • GitHub Check: e2e (22.x)
  • GitHub Check: test (22.x)
🔇 Additional comments (4)
packages/core/collections/truncated-hash-dictionary.ts (4)

6-9: Clean implementation of key truncation.

The helper correctly ensures all keys are exactly TRUNCATED_HASH_SIZE bytes. Using subarray creates a view without copying data, and the approach is consistent with the past review suggestions.


27-38: Excellent type consistency and implementation.

The refactor correctly:

  • Uses TruncatedHash as the internal dictionary key type
  • Applies getTruncatedKey consistently in fromEntries
  • Removes the unnecessary HashWithZeroedBit type alias
  • Maintains the public API with T extends OpaqueHash for caller flexibility

This addresses all concerns from the previous review.


50-71: Consistent and correct method implementations.

All dictionary operations (get, has, set, delete) follow the same clean pattern: truncate the key via getTruncatedKey, then delegate to the underlying BlobDictionary. This ensures type safety and eliminates the redundant conversions that were previously flagged.


79-81: Perfect fix for the redundant conversion.

The simplified yield* this.dict.entries() correctly removes the unnecessary byte conversion that was flagged in the previous review. Since the underlying dictionary already stores TruncatedHash keys, direct delegation is both correct and more efficient.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/core/collections/truncated-hash-dictionary.ts (1)

6-13: Good optimization for already-truncated keys.

The early return at lines 8-10 correctly avoids allocating new Bytes objects when the key is already TRUNCATED_HASH_SIZE, addressing the performance concern from previous reviews. The logic is sound and the comment is clear.

Consider adding an explicit return type annotation for clarity:

-function getTruncatedKey(key: OpaqueHash | TruncatedHash) {
+function getTruncatedKey(key: OpaqueHash | TruncatedHash): TruncatedHash {
   // Always return exactly TRUNCATED_HASH_SIZE bytes.
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 493f501 and 1401f96.

📒 Files selected for processing (1)
  • packages/core/collections/truncated-hash-dictionary.ts (4 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
packages/core/**/*.ts

⚙️ CodeRabbit configuration file

packages/core/**/*.ts: Core packages must not import any JAM-related packages
(i.e. packages defined under packages/jam/**)

Files:

  • packages/core/collections/truncated-hash-dictionary.ts
**/*.ts

⚙️ CodeRabbit configuration file

**/*.ts: rules from ./CODESTYLE.md should be adhered to.

**/*.ts: Any function whose documentation mention it must not be used in production code,
can be safely used in *.test.ts files. Other usage should be carefuly reviewed
and the comment must explain why it's safe to use there.

**/*.ts: as conversions must not be used. Suggest using tryAs conversion methods.

**/*.ts: Classes with static Codec field must have private constructor and static create method.

**/*.ts: Casting a bigint (or U64) using Number(x) must have an explanation comment why
it is safe.

**/*.ts: When making changes to code with comments containing links (in classes, constants, methods, etc.)
to graypaper.fluffylabs.dev, ensure those links point to the current version for this update.

Files:

  • packages/core/collections/truncated-hash-dictionary.ts
🧠 Learnings (1)
📚 Learning: 2025-06-10T12:20:17.513Z
Learnt from: tomusdrw
Repo: FluffyLabs/typeberry PR: 419
File: packages/jam/state/state-inmemory.ts:141-149
Timestamp: 2025-06-10T12:20:17.513Z
Learning: In the `InMemoryService.copyFrom` function in `packages/jam/state/state-inmemory.ts`, duplicate checking for `(hash, length)` pairs in the lookup history is not necessary because the function operates under the assumption that the input `ServiceEntries` comes from an existing well-formed state, which already maintains the invariant of unique lookup history entries per hash and length combination.

Applied to files:

  • packages/core/collections/truncated-hash-dictionary.ts
🧬 Code graph analysis (1)
packages/core/collections/truncated-hash-dictionary.ts (3)
packages/core/hash/hash.ts (4)
  • OpaqueHash (18-18)
  • TruncatedHash (27-27)
  • TRUNCATED_HASH_SIZE (14-14)
  • TRUNCATED_HASH_SIZE (15-15)
packages/core/bytes/bytes.ts (1)
  • Bytes (177-243)
packages/core/collections/blob-dictionary.ts (3)
  • BlobDictionary (6-335)
  • entries (295-299)
  • TEST_COMPARE_USING (54-58)
⏰ Context from checks skipped due to timeout of 120000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: run (22.x)
  • GitHub Check: run (22.x)
  • GitHub Check: run (22.x)
  • GitHub Check: test (22.x)
  • GitHub Check: e2e (22.x)
🔇 Additional comments (1)
packages/core/collections/truncated-hash-dictionary.ts (1)

34-39: Clean refactoring to BlobDictionary backing store.

The migration from HashDictionary to BlobDictionary is well-executed:

  • fromEntries correctly maps entries through getTruncatedKey before delegating to BlobDictionary.fromEntries
  • Constructor properly typed with BlobDictionary<TruncatedHash, V>
  • All CRUD methods (get, has, set, delete) consistently use the getTruncatedKey helper
  • entries() cleanly delegates with yield*
  • Type safety preserved: public API accepts T extends OpaqueHash while internal storage uses TruncatedHash

This addresses previous concerns about unnecessary asOpaque conversions and maintains clear separation between the public API contract and internal implementation.

Also applies to: 42-42, 45-45, 54-75, 84-84

@tomusdrw
Copy link
Contributor

@fluffylabs-bot benchmark

@fluffylabs-bot
Copy link

fluffylabs-bot bot commented Nov 18, 2025

Benchmark workflow triggered successfully! 🎉
Requested by: @tomusdrw
PR number: #732
Target repository: FluffyLabs/typeberry-testing

🔗 Check the Actions tab for workflow progress.

@github-actions
Copy link

View all
File Benchmark Ops
bytes/hex-from.ts[0] parse hex using Number with NaN checking 117606.96 ±1.48% 85.64% slower
bytes/hex-from.ts[1] parse hex from char codes 819072.9 ±1.17% fastest ✅
bytes/hex-from.ts[2] parse hex from string nibbles 502147.25 ±1.63% 38.69% slower
codec/encoding.ts[0] manual encode 2571796.55 ±4.73% 23.77% slower
codec/encoding.ts[1] int32array encode 3373822.6 ±4.19% fastest ✅
codec/encoding.ts[2] dataview encode 3132633.15 ±6.32% 7.15% slower
codec/bigint.compare.ts[0] compare custom 228558990.21 ±5.67% 1.96% slower
codec/bigint.compare.ts[1] compare bigint 233131125.41 ±5.91% fastest ✅
codec/bigint.decode.ts[0] decode custom 153892200.09 ±3.38% fastest ✅
codec/bigint.decode.ts[1] decode bigint 93443852.5 ±2.93% 39.28% slower
codec/decoding.ts[0] manual decode 19881633.48 ±2.77% 87.74% slower
codec/decoding.ts[1] int32array decode 148713570.37 ±3.61% 8.31% slower
codec/decoding.ts[2] dataview decode 162190324.73 ±3.06% fastest ✅
collections/map-set.ts[0] 2 gets + conditional set 115993.64 ±0.38% fastest ✅
collections/map-set.ts[1] 1 get 1 set 58844.07 ±0.35% 49.27% slower
logger/index.ts[0] console.log with string concat 8282839.79 ±36.92% fastest ✅
logger/index.ts[1] console.log with args 515721.5 ±118.81% 93.77% slower
math/switch.ts[0] switch 221742977.17 ±21.74% 14.39% slower
math/switch.ts[1] if 259002971.14 ±4.95% fastest ✅
math/mul_overflow.ts[0] multiply and bring back to u32 222396321.21 ±6.07% 6.36% slower
math/mul_overflow.ts[1] multiply and take modulus 237502722.17 ±5.44% fastest ✅
math/count-bits-u32.ts[0] standard method 75696431.51 ±1.67% 67.92% slower
math/count-bits-u32.ts[1] magic 235967839.63 ±5.34% fastest ✅
math/count-bits-u64.ts[0] standard method 3739296.31 ±2.06% 95.67% slower
math/count-bits-u64.ts[1] magic 86288026.18 ±1.2% fastest ✅
math/add_one_overflow.ts[0] add and take modulus 238988737.87 ±6.73% fastest ✅
math/add_one_overflow.ts[1] condition before calculation 235912081.46 ±6.95% 1.29% slower
hash/index.ts[0] hash with numeric representation 167.44 ±2.24% 35.97% slower
hash/index.ts[1] hash with string representation 102.63 ±2.54% 60.75% slower
hash/index.ts[2] hash with symbol representation 182.25 ±0.69% 30.3% slower
hash/index.ts[3] hash with uint8 representation 160.05 ±0.77% 38.79% slower
hash/index.ts[4] hash with packed representation 261.49 ±0.51% fastest ✅
hash/index.ts[5] hash with bigint representation 184.78 ±0.31% 29.34% slower
hash/index.ts[6] hash with uint32 representation 202.06 ±0.46% 22.73% slower
bytes/hex-to.ts[0] number toString + padding 216297.49 ±0.91% fastest ✅
bytes/hex-to.ts[1] manual 12612.13 ±0.57% 94.17% slower
collections/hash-dict-vs-blob-dict_set.ts[0] StringHashDictionary 2925.88 ±1.72% 0.5% slower
collections/hash-dict-vs-blob-dict_set.ts[1] BlobDictionary(1) 2940.5 ±1.51% fastest ✅
collections/hash-dict-vs-blob-dict_set.ts[2] BlobDictionary(2) 2892.18 ±1.45% 1.64% slower
collections/hash-dict-vs-blob-dict_set.ts[3] BlobDictionary(3) 2636.26 ±2.27% 10.35% slower
collections/hash-dict-vs-blob-dict_set.ts[4] BlobDictionary(4) 2572.63 ±1.53% 12.51% slower
collections/hash-dict-vs-blob-dict_set.ts[5] BlobDictionary(5) 2547.56 ±1.3% 13.36% slower
collections/hash-dict-vs-blob-dict_get.ts[0] StringHashDictionary 2528.01 ±1.79% fastest ✅
collections/hash-dict-vs-blob-dict_get.ts[1] BlobDictionary(1) 2514.83 ±1.46% 0.52% slower
collections/hash-dict-vs-blob-dict_get.ts[2] BlobDictionary(2) 2481.23 ±1.98% 1.85% slower
collections/hash-dict-vs-blob-dict_get.ts[3] BlobDictionary(3) 2470.85 ±1.83% 2.26% slower
collections/hash-dict-vs-blob-dict_get.ts[4] BlobDictionary(4) 2443.92 ±1.42% 3.33% slower
collections/hash-dict-vs-blob-dict_get.ts[5] BlobDictionary(5) 2475.43 ±1.8% 2.08% slower
collections/hash-dict-vs-blob-dict_delete.ts[0] StringHashDictionary 2790.22 ±1.92% 5.46% slower
collections/hash-dict-vs-blob-dict_delete.ts[1] BlobDictionary(1) 2749.53 ±1.71% 6.84% slower
collections/hash-dict-vs-blob-dict_delete.ts[2] BlobDictionary(2) 2844.72 ±2.26% 3.61% slower
collections/hash-dict-vs-blob-dict_delete.ts[3] BlobDictionary(3) 2862.1 ±2.1% 3.02% slower
collections/hash-dict-vs-blob-dict_delete.ts[4] BlobDictionary(4) 2833.15 ±1.94% 4% slower
collections/hash-dict-vs-blob-dict_delete.ts[5] BlobDictionary(5) 2951.32 ±1.98% fastest ✅
collections/map_vs_sorted.ts[0] Map 284353.64 ±1% fastest ✅
collections/map_vs_sorted.ts[1] Map-array 94477.31 ±1.17% 66.77% slower
collections/map_vs_sorted.ts[2] Array 35230.45 ±5.54% 87.61% slower
collections/map_vs_sorted.ts[3] SortedArray 195881.93 ±0.46% 31.11% slower
bytes/bytes-to-number.ts[0] Conversion with bitops 6329.74 ±8.48% 10.91% slower
bytes/bytes-to-number.ts[1] Conversion without bitops 7104.75 ±4.91% fastest ✅
bytes/compare.ts[0] Comparing Uint32 bytes 16626.16 ±3.34% 9.48% slower
bytes/compare.ts[1] Comparing raw bytes 18366.98 ±1.97% fastest ✅
codec/view_vs_object.ts[0] Get the first field from Decoded 41620.57 ±75.24% 89.95% slower
codec/view_vs_object.ts[1] Get the first field from View 83224.8 ±1.03% 79.9% slower
codec/view_vs_object.ts[2] Get the first field as view from View 85953.86 ±0.82% 79.24% slower
codec/view_vs_object.ts[3] Get two fields from Decoded 414083.64 ±0.84% fastest ✅
codec/view_vs_object.ts[4] Get two fields from View 67460.38 ±0.98% 83.71% slower
codec/view_vs_object.ts[5] Get two fields from materialized from View 137087.55 ±0.94% 66.89% slower
codec/view_vs_object.ts[6] Get two fields as views from View 68631.43 ±0.88% 83.43% slower
codec/view_vs_object.ts[7] Get only third field from Decoded 412934.65 ±0.95% 0.28% slower
codec/view_vs_object.ts[8] Get only third field from View 82780.98 ±1.05% 80.01% slower
codec/view_vs_object.ts[9] Get only third field as view from View 82639.71 ±1.22% 80.04% slower
codec/view_vs_collection.ts[0] Get first element from Decoded 24680.74 ±1.01% 52.3% slower
codec/view_vs_collection.ts[1] Get first element from View 51739.82 ±1.03% fastest ✅
codec/view_vs_collection.ts[2] Get 50th element from Decoded 23865.19 ±1.57% 53.87% slower
codec/view_vs_collection.ts[3] Get 50th element from View 27455.27 ±1.08% 46.94% slower
codec/view_vs_collection.ts[4] Get last element from Decoded 24782.36 ±1.11% 52.1% slower
codec/view_vs_collection.ts[5] Get last element from View 17891.48 ±2.04% 65.42% slower
hash/blake2b.ts[0] our hasher 2.16 ±1.38% fastest ✅
hash/blake2b.ts[1] blake2b js 0.05 ±2.33% 97.69% slower
crypto/ed25519.ts[0] native crypto 8.25 ±0.51% 73.08% slower
crypto/ed25519.ts[1] wasm lib 10.89 ±0.02% 64.47% slower
crypto/ed25519.ts[2] wasm lib batch 30.65 ±0.47% fastest ✅

Benchmarks summary: 83/83 OK ✅

@fluffylabs-bot
Copy link

Picofuzz Benchmark Results

fallback

Metric @typeberry/[email protected] Current Difference
min 13.49ms 12.68ms ≈ -0.81ms (-5.98%)
mean 16.44ms 15.41ms ≈ -1.03ms (-6.27%)
p90 18.71ms 18.19ms ≈ -0.52ms (-2.81%)
p99 22.68ms 22.53ms ≈ -0.15ms (-0.66%)

safrole

Metric @typeberry/[email protected] Current Difference
min 13.81ms 13.07ms ≈ -0.75ms (-5.39%)
mean 65.12ms 64.13ms ≈ -0.99ms (-1.52%)
p90 143.29ms 141.96ms ≈ -1.34ms (-0.93%)
p99 147.97ms 149.62ms ≈ +1.65ms (+1.12%)

storage

Metric @typeberry/[email protected] Current Difference
min 14.11ms 13.73ms ≈ -0.38ms (-2.67%)
mean 47.59ms 46.46ms ≈ -1.13ms (-2.38%)
p90 91.48ms 89.90ms ≈ -1.58ms (-1.73%)
p99 122.47ms 119.39ms ≈ -3.08ms (-2.52%)

storage_light

Metric @typeberry/[email protected] Current Difference
min 14.58ms 13.74ms ≈ -0.84ms (-5.76%)
mean 32.67ms 31.24ms ≈ -1.43ms (-4.39%)
p90 48.04ms 45.92ms ≈ -2.13ms (-4.43%)
p99 70.72ms 68.34ms ≈ -2.39ms (-3.38%)

🤖 Automated benchmark

@mateuszsikora mateuszsikora merged commit 0a3acb2 into main Nov 18, 2025
16 checks passed
@mateuszsikora mateuszsikora deleted the ms-blob-dict branch November 18, 2025 19:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants