tigerbeetle-node 0.11.0 → 0.11.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/dist/.client.node.sha256 +1 -0
  2. package/package.json +5 -3
  3. package/src/tigerbeetle/scripts/fuzz_loop.sh +1 -1
  4. package/src/tigerbeetle/scripts/pre-commit.sh +2 -2
  5. package/src/tigerbeetle/scripts/validate_docs.sh +17 -0
  6. package/src/tigerbeetle/src/benchmark.zig +25 -11
  7. package/src/tigerbeetle/src/c/tb_client/context.zig +248 -47
  8. package/src/tigerbeetle/src/c/tb_client/echo_client.zig +108 -0
  9. package/src/tigerbeetle/src/c/tb_client/packet.zig +2 -2
  10. package/src/tigerbeetle/src/c/tb_client/signal.zig +2 -4
  11. package/src/tigerbeetle/src/c/tb_client/thread.zig +17 -256
  12. package/src/tigerbeetle/src/c/tb_client.h +18 -4
  13. package/src/tigerbeetle/src/c/tb_client.zig +88 -26
  14. package/src/tigerbeetle/src/c/tb_client_header_test.zig +135 -0
  15. package/src/tigerbeetle/src/c/test.zig +371 -1
  16. package/src/tigerbeetle/src/cli.zig +90 -18
  17. package/src/tigerbeetle/src/config.zig +12 -4
  18. package/src/tigerbeetle/src/demo.zig +2 -1
  19. package/src/tigerbeetle/src/demo_01_create_accounts.zig +1 -1
  20. package/src/tigerbeetle/src/demo_03_create_transfers.zig +13 -0
  21. package/src/tigerbeetle/src/ewah.zig +11 -33
  22. package/src/tigerbeetle/src/ewah_benchmark.zig +8 -9
  23. package/src/tigerbeetle/src/lsm/README.md +97 -3
  24. package/src/tigerbeetle/src/lsm/compaction.zig +32 -7
  25. package/src/tigerbeetle/src/{eytzinger_benchmark.zig → lsm/eytzinger_benchmark.zig} +34 -21
  26. package/src/tigerbeetle/src/lsm/forest_fuzz.zig +34 -32
  27. package/src/tigerbeetle/src/lsm/grid.zig +39 -21
  28. package/src/tigerbeetle/src/lsm/groove.zig +1 -0
  29. package/src/tigerbeetle/src/lsm/k_way_merge.zig +3 -3
  30. package/src/tigerbeetle/src/lsm/level_iterator.zig +1 -1
  31. package/src/tigerbeetle/src/lsm/manifest.zig +13 -0
  32. package/src/tigerbeetle/src/lsm/manifest_level.zig +0 -49
  33. package/src/tigerbeetle/src/lsm/manifest_log.zig +173 -335
  34. package/src/tigerbeetle/src/lsm/manifest_log_fuzz.zig +665 -0
  35. package/src/tigerbeetle/src/lsm/node_pool.zig +4 -0
  36. package/src/tigerbeetle/src/lsm/posted_groove.zig +1 -0
  37. package/src/tigerbeetle/src/lsm/segmented_array.zig +24 -15
  38. package/src/tigerbeetle/src/lsm/table.zig +32 -20
  39. package/src/tigerbeetle/src/lsm/table_immutable.zig +1 -1
  40. package/src/tigerbeetle/src/lsm/table_iterator.zig +4 -5
  41. package/src/tigerbeetle/src/lsm/test.zig +13 -2
  42. package/src/tigerbeetle/src/lsm/tree.zig +45 -7
  43. package/src/tigerbeetle/src/lsm/tree_fuzz.zig +36 -32
  44. package/src/tigerbeetle/src/main.zig +69 -13
  45. package/src/tigerbeetle/src/message_bus.zig +18 -7
  46. package/src/tigerbeetle/src/message_pool.zig +8 -2
  47. package/src/tigerbeetle/src/ring_buffer.zig +7 -3
  48. package/src/tigerbeetle/src/simulator.zig +38 -11
  49. package/src/tigerbeetle/src/state_machine.zig +48 -23
  50. package/src/tigerbeetle/src/test/accounting/workload.zig +9 -5
  51. package/src/tigerbeetle/src/test/cluster.zig +15 -33
  52. package/src/tigerbeetle/src/test/conductor.zig +2 -1
  53. package/src/tigerbeetle/src/test/network.zig +45 -19
  54. package/src/tigerbeetle/src/test/packet_simulator.zig +40 -29
  55. package/src/tigerbeetle/src/test/state_checker.zig +5 -7
  56. package/src/tigerbeetle/src/test/storage.zig +453 -110
  57. package/src/tigerbeetle/src/test/storage_checker.zig +204 -0
  58. package/src/tigerbeetle/src/tigerbeetle.zig +1 -0
  59. package/src/tigerbeetle/src/unit_tests.zig +7 -1
  60. package/src/tigerbeetle/src/util.zig +97 -11
  61. package/src/tigerbeetle/src/vopr.zig +2 -1
  62. package/src/tigerbeetle/src/vsr/client.zig +8 -3
  63. package/src/tigerbeetle/src/vsr/journal.zig +280 -202
  64. package/src/tigerbeetle/src/vsr/replica.zig +169 -31
  65. package/src/tigerbeetle/src/vsr/superblock.zig +356 -629
  66. package/src/tigerbeetle/src/vsr/superblock_client_table.zig +7 -6
  67. package/src/tigerbeetle/src/vsr/superblock_free_set.zig +414 -151
  68. package/src/tigerbeetle/src/vsr/superblock_free_set_fuzz.zig +332 -0
  69. package/src/tigerbeetle/src/vsr/superblock_fuzz.zig +349 -0
  70. package/src/tigerbeetle/src/vsr/superblock_manifest.zig +44 -9
  71. package/src/tigerbeetle/src/vsr/superblock_quorums.zig +394 -0
  72. package/src/tigerbeetle/src/vsr/superblock_quorums_fuzz.zig +312 -0
  73. package/src/tigerbeetle/src/vsr.zig +19 -5
  74. package/src/tigerbeetle/src/benchmark_array_search.zig +0 -317
  75. package/src/tigerbeetle/src/benchmarks/perf.zig +0 -299
  76. package/src/tigerbeetle/src/vopr_hub/README.md +0 -58
  77. package/src/tigerbeetle/src/vopr_hub/SETUP.md +0 -199
  78. package/src/tigerbeetle/src/vopr_hub/go.mod +0 -3
  79. package/src/tigerbeetle/src/vopr_hub/main.go +0 -1022
  80. package/src/tigerbeetle/src/vopr_hub/scheduler/go.mod +0 -3
  81. package/src/tigerbeetle/src/vopr_hub/scheduler/main.go +0 -403
@@ -0,0 +1,312 @@
1
+ const std = @import("std");
2
+ const assert = std.debug.assert;
3
+ const log = std.log.scoped(.fuzz_vsr_superblock_quorums);
4
+
5
+ const superblock = @import("./superblock.zig");
6
+ const SuperBlockSector = superblock.SuperBlockSector;
7
+ const SuperBlockVersion = superblock.SuperBlockVersion;
8
+
9
+ const fuzz = @import("../test/fuzz.zig");
10
+ const superblock_quorums = @import("superblock_quorums.zig");
11
+ const QuorumsType = superblock_quorums.QuorumsType;
12
+
13
+ pub fn main() !void {
14
+ const fuzz_args = try fuzz.parse_fuzz_args(std.testing.allocator);
15
+ var prng = std.rand.DefaultPrng.init(fuzz_args.seed);
16
+
17
+ // TODO When there is a top-level fuzz.zig main(), split these fuzzers into two different
18
+ // commands.
19
+ try fuzz_quorums_working(prng.random());
20
+
21
+ try fuzz_quorum_repairs(prng.random(), .{ .superblock_copies = 4 });
22
+ // TODO: Enable these once SuperBlockSector is generic over its Constants.
23
+ // try fuzz_quorum_repairs(prng.random(), .{ .superblock_copies = 6 });
24
+ // try fuzz_quorum_repairs(prng.random(), .{ .superblock_copies = 8 });
25
+ }
26
+
27
+ pub fn fuzz_quorums_working(random: std.rand.Random) !void {
28
+ const r = random;
29
+ const t = test_quorums_working;
30
+ const o = CopyTemplate.make_valid;
31
+ const x = CopyTemplate.make_invalid_broken;
32
+ const X = {}; // Ignored; just for text alignment + contrast.
33
+
34
+ // No faults:
35
+ try t(r, 2, &.{ o(3), o(3), o(3), o(3) }, 3);
36
+ try t(r, 3, &.{ o(3), o(3), o(3), o(3) }, 3);
37
+
38
+ // Single fault:
39
+ try t(r, 3, &.{ x(X), o(4), o(4), o(4) }, 4);
40
+ // Double fault, same quorum:
41
+ try t(r, 2, &.{ x(X), x(X), o(4), o(4) }, 4);
42
+ try t(r, 3, &.{ x(X), x(X), o(4), o(4) }, error.QuorumLost);
43
+ // Double fault, different quorums:
44
+ try t(r, 2, &.{ x(X), x(X), o(3), o(4) }, error.QuorumLost);
45
+ // Triple fault.
46
+ try t(r, 2, &.{ x(X), x(X), x(X), o(4) }, error.QuorumLost);
47
+
48
+ // Partial format (broken sequence=1):
49
+ try t(r, 2, &.{ x(X), o(1), o(1), o(1) }, 1);
50
+ try t(r, 3, &.{ x(X), o(1), o(1), o(1) }, 1);
51
+ try t(r, 2, &.{ x(X), x(X), o(1), o(1) }, 1);
52
+ try t(r, 3, &.{ x(X), x(X), o(1), o(1) }, error.QuorumLost);
53
+ try t(r, 2, &.{ x(X), x(X), x(X), o(1) }, error.QuorumLost);
54
+ try t(r, 2, &.{ x(X), x(X), x(X), x(X) }, error.NotFound);
55
+
56
+ // Partial checkpoint() to sequence=4 (2 quorums):
57
+ try t(r, 2, &.{ o(3), o(2), o(2), o(2) }, 2); // open after 1/4
58
+ try t(r, 2, &.{ o(3), o(3), o(2), o(2) }, 3); // open after 2/4
59
+ try t(r, 2, &.{ o(3), o(3), o(3), o(2) }, 3); // open after 3/4
60
+ // Partial checkpoint() to sequence=4 (3 quorums):
61
+ try t(r, 2, &.{ o(1), o(2), o(3), o(3) }, 3);
62
+ try t(r, 3, &.{ o(1), o(2), o(3), o(3) }, error.QuorumLost);
63
+
64
+ // Skipped sequence.
65
+ try t(r, 2, &.{ o(2), o(2), o(2), o(4) }, error.ParentSkipped); // open after 1/4
66
+ try t(r, 2, &.{ o(2), o(2), o(4), o(4) }, 4); // open after 2/4
67
+ try t(r, 2, &.{ o(2), o(2), o(4), o(4) }, 4); // open after 3/4
68
+
69
+ // Forked sequence: same sequence number, different checksum, both valid.
70
+ const f = CopyTemplate.make_invalid_fork;
71
+ try t(r, 2, &.{ o(3), o(3), o(3), f(3) }, error.Fork);
72
+ try t(r, 2, &.{ o(3), o(3), f(3), f(3) }, error.Fork);
73
+
74
+ // Parent has wrong cluster|replica.
75
+ const m = CopyTemplate.make_invalid_misdirect;
76
+ try t(r, 2, &.{ m(2), m(2), m(2), o(3) }, 2);
77
+ try t(r, 2, &.{ m(2), m(2), o(3), o(3) }, 3);
78
+ try t(r, 2, &.{ m(2), o(3), o(3), o(3) }, 3);
79
+ // Grandparent has wrong cluster|replica.
80
+ try t(r, 2, &.{ m(2), m(2), m(2), o(4) }, 2);
81
+ try t(r, 2, &.{ m(2), m(2), o(4), o(4) }, 4);
82
+ try t(r, 2, &.{ m(2), o(4), o(4), o(4) }, 4);
83
+
84
+ // Parent/child hash chain is broken.
85
+ const p = CopyTemplate.make_invalid_parent;
86
+ try t(r, 2, &.{ o(2), o(2), o(2), p(3) }, 2);
87
+ try t(r, 2, &.{ o(2), o(2), p(3), p(3) }, error.ParentNotConnected);
88
+ try t(r, 2, &.{ o(2), p(3), p(3), p(3) }, error.ParentNotConnected);
89
+ try t(r, 2, &.{ p(3), p(3), p(3), p(3) }, 3);
90
+
91
+ // Parent view is greater than child view.
92
+ const v = CopyTemplate.make_invalid_vsr_state;
93
+ try t(r, 2, &.{ v(2), v(2), o(3), o(3) }, error.VSRStateNotMonotonic);
94
+
95
+ // A member of the quorum has an "invalid" copy, but an otherwise valid checksum.
96
+ const h = CopyTemplate.make_valid_high_copy;
97
+ try t(r, 2, &.{ o(2), o(2), o(3), h(3) }, 3);
98
+ }
99
+
100
+ fn test_quorums_working(
101
+ random: std.rand.Random,
102
+ threshold_count: u8,
103
+ copies: *[4]CopyTemplate,
104
+ result: QuorumsType(.{ .superblock_copies = 4 }).Error!u64,
105
+ ) !void {
106
+ const Quorums = QuorumsType(.{ .superblock_copies = 4 });
107
+ const misdirect = random.boolean(); // true:cluster false:replica
108
+ var quorums: Quorums = undefined;
109
+ var sectors: [4]SuperBlockSector = undefined;
110
+ // TODO(Zig): Ideally this would be a [6]?u128 and the access would be
111
+ // "checksums[i] orelse random.int(u128)", but that currently causes the compiler to segfault
112
+ // during code generation.
113
+ var checksums: [6]u128 = undefined;
114
+ for (checksums) |*c| c.* = random.int(u128);
115
+
116
+ // Create sectors in ascending-sequence order to build the checksum/parent hash chain.
117
+ std.sort.sort(CopyTemplate, copies, {}, CopyTemplate.less_than);
118
+
119
+ for (sectors) |*sector, i| {
120
+ sector.* = std.mem.zeroInit(SuperBlockSector, .{
121
+ .copy = @intCast(u8, i),
122
+ .version = SuperBlockVersion,
123
+ .replica = 1,
124
+ .size_max = superblock.data_file_size_min,
125
+ .sequence = copies[i].sequence,
126
+ .parent = checksums[copies[i].sequence - 1],
127
+ });
128
+
129
+ var checksum: ?u128 = null;
130
+ switch (copies[i].variant) {
131
+ .valid => {},
132
+ .valid_high_copy => sector.copy = 4,
133
+ .invalid_broken => {
134
+ if (random.boolean() and i > 0) {
135
+ // Error: duplicate sector (if available).
136
+ sector.* = sectors[random.uintLessThanBiased(usize, i)];
137
+ checksum = random.int(u128);
138
+ } else {
139
+ // Error: invalid checksum.
140
+ checksum = random.int(u128);
141
+ }
142
+ },
143
+ .invalid_fork => sector.size_max += 1, // Ensure we have a different checksum.
144
+ .invalid_parent => sector.parent += 1,
145
+ .invalid_misdirect => {
146
+ if (misdirect) {
147
+ sector.cluster += 1;
148
+ } else {
149
+ sector.replica += 1;
150
+ }
151
+ },
152
+ .invalid_vsr_state => sector.vsr_state.view += 1,
153
+ }
154
+ sector.checksum = checksum orelse sector.calculate_checksum();
155
+
156
+ if (copies[i].variant == .valid or copies[i].variant == .invalid_vsr_state) {
157
+ checksums[sector.sequence] = sector.checksum;
158
+ }
159
+ }
160
+
161
+ for (copies) |template| {
162
+ if (template.variant == .valid_high_copy) break;
163
+ } else {
164
+ // Shuffling copies can only change the working quorum when we have a corrupt copy index,
165
+ // because we guess that the true index is the slot.
166
+ random.shuffle(SuperBlockSector, &sectors);
167
+ }
168
+
169
+ const threshold = switch (threshold_count) {
170
+ 2 => Quorums.Threshold.open,
171
+ 3 => Quorums.Threshold.verify,
172
+ else => unreachable,
173
+ };
174
+ assert(threshold.count() == threshold_count);
175
+
176
+ if (quorums.working(&sectors, threshold)) |working| {
177
+ try std.testing.expectEqual(result, working.sector.sequence);
178
+ } else |err| {
179
+ try std.testing.expectEqual(result, err);
180
+ }
181
+ }
182
+
183
+ pub const CopyTemplate = struct {
184
+ sequence: u64,
185
+ variant: Variant,
186
+
187
+ const Variant = enum {
188
+ valid,
189
+ valid_high_copy,
190
+ invalid_broken,
191
+ invalid_fork,
192
+ invalid_misdirect,
193
+ invalid_parent,
194
+ invalid_vsr_state,
195
+ };
196
+
197
+ pub fn make_valid(sequence: u64) CopyTemplate {
198
+ return .{ .sequence = sequence, .variant = .valid };
199
+ }
200
+
201
+ /// Construct a copy with a corrupt copy index (≥superblock_copies).
202
+ pub fn make_valid_high_copy(sequence: u64) CopyTemplate {
203
+ return .{ .sequence = sequence, .variant = .valid_high_copy };
204
+ }
205
+
206
+ /// Construct a corrupt (invalid checksum) or duplicate copy copy.
207
+ pub fn make_invalid_broken(_: void) CopyTemplate {
208
+ // Use a high sequence so that invalid copies are the last generated by
209
+ // test_quorums_working(), so that they can become duplicates of (earlier) valid copies.
210
+ return .{ .sequence = 6, .variant = .invalid_broken };
211
+ }
212
+
213
+ /// Construct a copy with a valid checksum — but which differs from the "canonical" version
214
+ /// of this sequence.
215
+ pub fn make_invalid_fork(sequence: u64) CopyTemplate {
216
+ return .{ .sequence = sequence, .variant = .invalid_fork };
217
+ }
218
+
219
+ /// Construct a copy with either an incorrect "cluster" or "replica".
220
+ pub fn make_invalid_misdirect(sequence: u64) CopyTemplate {
221
+ return .{ .sequence = sequence, .variant = .invalid_misdirect };
222
+ }
223
+
224
+ /// Construct a copy with an invalid "parent" checksum.
225
+ pub fn make_invalid_parent(sequence: u64) CopyTemplate {
226
+ return .{ .sequence = sequence, .variant = .invalid_parent };
227
+ }
228
+
229
+ /// Construct a copy with a newer `VSRState` than its parent.
230
+ pub fn make_invalid_vsr_state(sequence: u64) CopyTemplate {
231
+ return .{ .sequence = sequence, .variant = .invalid_vsr_state };
232
+ }
233
+
234
+ fn less_than(_: void, a: CopyTemplate, b: CopyTemplate) bool {
235
+ return a.sequence < b.sequence;
236
+ }
237
+ };
238
+
239
+ // Verify that a torn sector write during repair never compromises the existing quorum.
240
+ pub fn fuzz_quorum_repairs(
241
+ random: std.rand.Random,
242
+ comptime options: superblock_quorums.Options,
243
+ ) !void {
244
+ const superblock_copies = options.superblock_copies;
245
+ const Quorums = QuorumsType(options);
246
+
247
+ var q1: Quorums = undefined;
248
+ var q2: Quorums = undefined;
249
+
250
+ const sectors_valid = blk: {
251
+ var sectors: [superblock_copies]SuperBlockSector = undefined;
252
+ for (&sectors) |*sector, i| {
253
+ sector.* = std.mem.zeroInit(SuperBlockSector, .{
254
+ .copy = @intCast(u8, i),
255
+ .version = SuperBlockVersion,
256
+ .replica = 1,
257
+ .size_max = superblock.data_file_size_min,
258
+ .sequence = 123,
259
+ });
260
+ sector.set_checksum();
261
+ }
262
+ break :blk sectors;
263
+ };
264
+
265
+ const sector_invalid = blk: {
266
+ var sector = sectors_valid[0];
267
+ sector.checksum = 456;
268
+ break :blk sector;
269
+ };
270
+
271
+ // Generate a random valid 2/4 quorum.
272
+ // 1 bits indicate valid sectors.
273
+ // 0 bits indicate invalid sectors.
274
+ var valid = std.bit_set.IntegerBitSet(superblock_copies).initEmpty();
275
+ while (valid.count() < Quorums.Threshold.open.count() or random.boolean()) {
276
+ valid.set(random.uintLessThan(usize, superblock_copies));
277
+ }
278
+
279
+ var working_sectors: [superblock_copies]SuperBlockSector = undefined;
280
+ for (&working_sectors) |*sector, i| {
281
+ sector.* = if (valid.isSet(i)) sectors_valid[i] else sector_invalid;
282
+ }
283
+ random.shuffle(SuperBlockSector, &working_sectors);
284
+ var repair_sectors = working_sectors;
285
+
286
+ const working_quorum = q1.working(&working_sectors, .open) catch unreachable;
287
+ var quorum_repairs = working_quorum.repairs();
288
+ while (quorum_repairs.next()) |repair_copy| {
289
+ {
290
+ // Simulate a torn sector write, crash, recover sequence.
291
+ var damaged_sectors = repair_sectors;
292
+ damaged_sectors[repair_copy] = sector_invalid;
293
+ const damaged_quorum = q2.working(&damaged_sectors, .open) catch unreachable;
294
+ assert(damaged_quorum.sector.checksum == working_quorum.sector.checksum);
295
+ }
296
+
297
+ // "Finish" the write so that we can test the next repair.
298
+ repair_sectors[repair_copy] = sectors_valid[repair_copy];
299
+
300
+ const quorum_repaired = q2.working(&repair_sectors, .open) catch unreachable;
301
+ assert(quorum_repaired.sector.checksum == working_quorum.sector.checksum);
302
+ }
303
+
304
+ // At the end of all repairs, we expect to have every copy of the superblock.
305
+ // They do not need to be in their home slot.
306
+ var copies = Quorums.QuorumCount.initEmpty();
307
+ for (repair_sectors) |repair_sector| {
308
+ assert(repair_sector.checksum == working_quorum.sector.checksum);
309
+ copies.set(repair_sector.copy);
310
+ }
311
+ assert(repair_sectors.len == copies.count());
312
+ }
@@ -24,11 +24,23 @@ pub const ProcessType = enum { replica, client };
24
24
 
25
25
  pub const Zone = enum {
26
26
  superblock,
27
- wal,
27
+ wal_headers,
28
+ wal_prepares,
28
29
  grid,
29
30
 
30
31
  const size_superblock = @import("vsr/superblock.zig").superblock_zone_size;
31
- const size_wal = config.journal_size_max;
32
+ const size_wal_headers = config.journal_size_headers;
33
+ const size_wal_prepares = config.journal_size_prepares;
34
+
35
+ comptime {
36
+ for (.{
37
+ size_superblock,
38
+ size_wal_headers,
39
+ size_wal_prepares,
40
+ }) |zone_size| {
41
+ assert(zone_size % config.sector_size == 0);
42
+ }
43
+ }
32
44
 
33
45
  pub fn offset(zone: Zone, offset_logical: u64) u64 {
34
46
  if (zone.size()) |zone_size| {
@@ -37,15 +49,17 @@ pub const Zone = enum {
37
49
 
38
50
  return offset_logical + switch (zone) {
39
51
  .superblock => 0,
40
- .wal => size_superblock,
41
- .grid => size_superblock + size_wal,
52
+ .wal_headers => size_superblock,
53
+ .wal_prepares => size_superblock + size_wal_headers,
54
+ .grid => size_superblock + size_wal_headers + size_wal_prepares,
42
55
  };
43
56
  }
44
57
 
45
58
  pub fn size(zone: Zone) ?u64 {
46
59
  return switch (zone) {
47
60
  .superblock => size_superblock,
48
- .wal => size_wal,
61
+ .wal_headers => size_wal_headers,
62
+ .wal_prepares => size_wal_prepares,
49
63
  .grid => null,
50
64
  };
51
65
  }
@@ -1,317 +0,0 @@
1
- const std = @import("std");
2
- const assert = std.debug.assert;
3
- const math = std.math;
4
-
5
- const binary_search = @import("./binary_search.zig").binary_search;
6
- const eytzinger = @import("./eytzinger.zig").eytzinger;
7
- const perf = @import("./benchmarks/perf.zig");
8
-
9
- const GiB = 1 << 30;
10
- const searches = 500_000;
11
-
12
- const kv_types = .{
13
- .{ .key_size = 8, .value_size = 128 },
14
- .{ .key_size = 8, .value_size = 64 },
15
- .{ .key_size = 16, .value_size = 16 },
16
- .{ .key_size = 32, .value_size = 32 },
17
- };
18
-
19
- // keys_per_summary = values_per_page / summary_fraction
20
- const summary_fractions = .{ 4, 8, 16, 32 };
21
- const values_per_page = .{ 128, 256, 512, 1024, 2048, 4096, 8192 };
22
- const body_fmt = "{:_>2}B/{:_>3}B {:_>4}/{:_>4} {s}{s}: WT={:_>6}ns UT={:_>6}ns" ++
23
- " CY={:_>6} IN={:_>6} CR={:_>5} CM={:_>5} BM={}\n";
24
-
25
- const summary_sizes = blk: {
26
- var sizes: [values_per_page.len][summary_fractions.len]usize = undefined;
27
- for (values_per_page) |values_count, v| {
28
- for (summary_fractions) |fraction, k| {
29
- // Set in reverse order so that the summary sizes ascend.
30
- sizes[v][summary_fractions.len - k - 1] = values_count / fraction;
31
- }
32
- }
33
- break :blk sizes;
34
- };
35
-
36
- pub fn main() !void {
37
- std.log.info("Samples: {}", .{searches});
38
- std.log.info("WT: Wall time/search", .{});
39
- std.log.info("UT: utime time/search", .{});
40
- std.log.info("CY: CPU cycles/search", .{});
41
- std.log.info("IN: instructions/search", .{});
42
- std.log.info("CR: cache references/search", .{});
43
- std.log.info("CM: cache misses/search", .{});
44
- std.log.info("BM: branch misses/search", .{});
45
-
46
- var seed: u64 = undefined;
47
- try std.os.getrandom(std.mem.asBytes(&seed));
48
- var prng = std.rand.DefaultPrng.init(seed);
49
-
50
- // Allocate on the heap just once.
51
- // All page allocations reuse this buffer to speed up the run time.
52
- var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
53
- defer arena.deinit();
54
-
55
- const blob_size = GiB;
56
- var blob = try arena.allocator.alloc(u8, blob_size);
57
-
58
- inline for (kv_types) |kv| {
59
- inline for (values_per_page) |values_count, v| {
60
- inline for (summary_sizes[v]) |keys_count| {
61
- try run_benchmark(.{
62
- .blob_size = blob_size,
63
- .key_size = kv.key_size,
64
- .value_size = kv.value_size,
65
- .keys_count = keys_count,
66
- .values_count = values_count,
67
- .searches = searches,
68
- }, blob, &prng.random);
69
- }
70
- }
71
- }
72
- }
73
-
74
- fn run_benchmark(comptime layout: Layout, blob: []u8, random: *std.rand.Random) !void {
75
- assert(blob.len == layout.blob_size);
76
- const Eytzinger = eytzinger(layout.keys_count - 1, layout.values_count);
77
- const V = Value(layout);
78
- const K = V.Key;
79
- const Page = struct {
80
- keys: [layout.keys_count]K,
81
- values: [layout.values_count]V,
82
- };
83
- const page_count = layout.blob_size / @sizeOf(Page);
84
-
85
- // Search pages and keys in random order.
86
- var page_picker = shuffled_index(page_count, random);
87
- var value_picker = shuffled_index(layout.values_count, random);
88
-
89
- // Generate 1GiB worth of 24KiB pages.
90
- var blob_alloc = std.heap.FixedBufferAllocator.init(blob);
91
- var pages = try blob_alloc.allocator.alloc(Page, page_count);
92
- random.bytes(std.mem.sliceAsBytes(pages));
93
- for (pages) |*page| {
94
- for (page.values) |*value, i| value.key = i;
95
- Eytzinger.layout_from_keys_or_values(K, V, V.key_from_value, V.max_key, &page.values, &page.keys);
96
- }
97
-
98
- const stdout = std.io.getStdOut().writer();
99
- {
100
- var benchmark = try Benchmark.begin();
101
- var i: usize = 0;
102
- var v: usize = 0;
103
- while (i < layout.searches) : (i += 1) {
104
- const page_index = page_picker[i % page_picker.len];
105
- const target = value_picker[v % value_picker.len];
106
- const page = &pages[page_index];
107
- const bounds = Eytzinger.search_values(K, V, V.key_compare, &page.keys, &page.values, target);
108
- const hit = bounds[binary_search(K, V, V.key_from_value, V.key_compare, bounds, target, .{})];
109
-
110
- assert(hit.key == target);
111
- if (i % pages.len == 0) v += 1;
112
- }
113
-
114
- const result = try benchmark.end(layout.searches);
115
- try stdout.print(body_fmt, .{
116
- layout.key_size,
117
- layout.value_size,
118
- layout.keys_count,
119
- layout.values_count,
120
- "E",
121
- "B",
122
- result.wall_time,
123
- result.utime,
124
- result.cpu_cycles,
125
- result.instructions,
126
- result.cache_references,
127
- result.cache_misses,
128
- result.branch_misses,
129
- });
130
- }
131
-
132
- {
133
- var benchmark = try Benchmark.begin();
134
- var i: usize = 0;
135
- var v: usize = 0;
136
- while (i < layout.searches) : (i += 1) {
137
- const target = value_picker[v % value_picker.len];
138
- const page = &pages[page_picker[i % page_picker.len]];
139
- const hit = page.values[binary_search(K, V, V.key_from_value, V.key_compare, page.values[0..], target, .{})];
140
-
141
- assert(hit.key == target);
142
- if (i % pages.len == 0) v += 1;
143
- }
144
- const result = try benchmark.end(layout.searches);
145
- try stdout.print(body_fmt, .{
146
- layout.key_size,
147
- layout.value_size,
148
- layout.keys_count,
149
- layout.values_count,
150
- "_",
151
- "B",
152
- result.wall_time,
153
- result.utime,
154
- result.cpu_cycles,
155
- result.instructions,
156
- result.cache_references,
157
- result.cache_misses,
158
- result.branch_misses,
159
- });
160
- }
161
- }
162
-
163
- const Layout = struct {
164
- blob_size: usize, // bytes allocated for all pages
165
- key_size: usize, // bytes per key
166
- value_size: usize, // bytes per value
167
- keys_count: usize, // keys per page (in the summary)
168
- values_count: usize, // values per page
169
- searches: usize,
170
- };
171
-
172
- fn Value(comptime layout: Layout) type {
173
- return struct {
174
- pub const max_key = 1 << (8 * layout.key_size) - 1;
175
- pub const Key = math.IntFittingRange(0, max_key);
176
- const Self = @This();
177
- key: Key,
178
- body: [layout.value_size - layout.key_size]u8,
179
-
180
- comptime {
181
- assert(@sizeOf(Key) == layout.key_size);
182
- assert(@sizeOf(Self) == layout.value_size);
183
- }
184
-
185
- inline fn key_from_value(self: Self) Key {
186
- return self.key;
187
- }
188
-
189
- inline fn key_from_key(x: Key) Key {
190
- return x;
191
- }
192
-
193
- inline fn key_compare(a: Key, b: Key) math.Order {
194
- return math.order(a, b);
195
- }
196
- };
197
- }
198
-
199
- const BenchmarkResult = struct {
200
- wall_time: u64, // nanoseconds
201
- utime: u64, // nanoseconds
202
- cpu_cycles: usize,
203
- instructions: usize,
204
- cache_references: usize,
205
- cache_misses: usize,
206
- branch_misses: usize,
207
- };
208
-
209
- const PERF = perf.PERF;
210
- const perf_event_attr = perf.perf_event_attr;
211
- const perf_event_open = perf.perf_event_open;
212
- const perf_counters = [_]PERF.COUNT.HW{
213
- PERF.COUNT.HW.CPU_CYCLES,
214
- PERF.COUNT.HW.INSTRUCTIONS,
215
- PERF.COUNT.HW.CACHE_REFERENCES,
216
- PERF.COUNT.HW.CACHE_MISSES,
217
- PERF.COUNT.HW.BRANCH_MISSES,
218
- };
219
-
220
- const Benchmark = struct {
221
- timer: std.time.Timer,
222
- rusage: std.os.rusage,
223
- perf_fds: [perf_counters.len]std.os.fd_t,
224
-
225
- fn begin() !Benchmark {
226
- const flags = PERF.FLAG.FD_NO_GROUP;
227
- var perf_fds = [1]std.os.fd_t{-1} ** perf_counters.len;
228
- for (perf_counters) |counter, i| {
229
- var attr: perf_event_attr = .{
230
- .type = PERF.TYPE.HARDWARE,
231
- .config = @enumToInt(counter),
232
- .flags = .{
233
- .disabled = true,
234
- .exclude_kernel = true,
235
- .exclude_hv = true,
236
- },
237
- };
238
- perf_fds[i] = try perf_event_open(&attr, 0, -1, perf_fds[0], PERF.FLAG.FD_CLOEXEC);
239
- }
240
- const err = std.os.linux.ioctl(perf_fds[0], PERF.EVENT_IOC.ENABLE, PERF.IOC_FLAG_GROUP);
241
- if (err == -1) return error.Unexpected;
242
-
243
- // Start the wall clock after perf, since setup is slow.
244
- const timer = try std.time.Timer.start();
245
- return Benchmark{
246
- .timer = timer,
247
- // TODO pass std.os.linux.rusage.SELF once Zig is upgraded
248
- .rusage = std.os.getrusage(0),
249
- .perf_fds = perf_fds,
250
- };
251
- }
252
-
253
- fn end(self: *Benchmark, samples: usize) !BenchmarkResult {
254
- defer {
255
- for (perf_counters) |_, i| {
256
- std.os.close(self.perf_fds[i]);
257
- self.perf_fds[i] = -1;
258
- }
259
- }
260
-
261
- const rusage = std.os.getrusage(0);
262
- const err = std.os.linux.ioctl(self.perf_fds[0], PERF.EVENT_IOC.DISABLE, PERF.IOC_FLAG_GROUP);
263
- if (err == -1) return error.Unexpected;
264
- return BenchmarkResult{
265
- .wall_time = self.timer.read() / samples,
266
- .utime = (timeval_to_ns(rusage.utime) - timeval_to_ns(self.rusage.utime)) / samples,
267
- .cpu_cycles = (try readPerfFd(self.perf_fds[0])) / samples,
268
- .instructions = (try readPerfFd(self.perf_fds[1])) / samples,
269
- .cache_references = (try readPerfFd(self.perf_fds[2])) / samples,
270
- .cache_misses = (try readPerfFd(self.perf_fds[3])) / samples,
271
- .branch_misses = (try readPerfFd(self.perf_fds[4])) / samples,
272
- };
273
- }
274
- };
275
-
276
- // shuffle([0,1,…,n-1])
277
- fn shuffled_index(comptime n: usize, rand: *std.rand.Random) [n]usize {
278
- var indices: [n]usize = undefined;
279
- for (indices) |*i, j| i.* = j;
280
- rand.shuffle(usize, indices[0..]);
281
- return indices;
282
- }
283
-
284
- fn timeval_to_ns(tv: std.os.timeval) u64 {
285
- const ns_per_us = std.time.ns_per_s / std.time.us_per_s;
286
- return @bitCast(u64, tv.tv_sec) * std.time.ns_per_s +
287
- @bitCast(u64, tv.tv_usec) * ns_per_us;
288
- }
289
-
290
- fn readPerfFd(fd: std.os.fd_t) !usize {
291
- var result: usize = 0;
292
- const n = try std.os.read(fd, std.mem.asBytes(&result));
293
- assert(n == @sizeOf(usize));
294
-
295
- return result;
296
- }
297
-
298
- fn binary_search_keys(
299
- comptime layout: Layout,
300
- comptime Key: type,
301
- comptime V: type,
302
- comptime compare_keys: fn (Key, Key) math.Order,
303
- keys: []const Key,
304
- values: []const V,
305
- key: Key,
306
- ) []const V {
307
- assert(keys.len == layout.keys_count);
308
- assert(values.len == layout.values_count);
309
-
310
- const key_index = binary_search(Key, Key, V.key_from_key, compare_keys, keys, key, .{});
311
- const key_stride = layout.values_count / layout.keys_count;
312
- const high = key_index * key_stride;
313
- if (key_index < keys.len and keys[key_index] == key) {
314
- return if (high == 0) values[0..1] else values[high - 1 .. high];
315
- }
316
- return values[high - key_stride .. high];
317
- }