tigerbeetle-node 0.8.0 → 0.10.0

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 (99) hide show
  1. package/README.md +47 -47
  2. package/dist/benchmark.js +15 -15
  3. package/dist/benchmark.js.map +1 -1
  4. package/dist/index.d.ts +66 -61
  5. package/dist/index.js +66 -61
  6. package/dist/index.js.map +1 -1
  7. package/dist/test.js +1 -1
  8. package/dist/test.js.map +1 -1
  9. package/package.json +14 -16
  10. package/scripts/download_node_headers.sh +3 -1
  11. package/src/index.ts +5 -0
  12. package/src/node.zig +18 -19
  13. package/src/tigerbeetle/scripts/benchmark.bat +47 -46
  14. package/src/tigerbeetle/scripts/benchmark.sh +25 -10
  15. package/src/tigerbeetle/scripts/install.sh +2 -1
  16. package/src/tigerbeetle/scripts/install_zig.bat +109 -109
  17. package/src/tigerbeetle/scripts/install_zig.sh +18 -18
  18. package/src/tigerbeetle/scripts/upgrade_ubuntu_kernel.sh +12 -3
  19. package/src/tigerbeetle/scripts/vopr.bat +47 -47
  20. package/src/tigerbeetle/scripts/vopr.sh +5 -5
  21. package/src/tigerbeetle/src/benchmark.zig +17 -9
  22. package/src/tigerbeetle/src/benchmark_array_search.zig +317 -0
  23. package/src/tigerbeetle/src/benchmarks/perf.zig +299 -0
  24. package/src/tigerbeetle/src/c/tb_client/context.zig +103 -0
  25. package/src/tigerbeetle/src/c/tb_client/packet.zig +80 -0
  26. package/src/tigerbeetle/src/c/tb_client/signal.zig +288 -0
  27. package/src/tigerbeetle/src/c/tb_client/thread.zig +329 -0
  28. package/src/tigerbeetle/src/c/tb_client.h +201 -0
  29. package/src/tigerbeetle/src/c/tb_client.zig +101 -0
  30. package/src/tigerbeetle/src/c/test.zig +1 -0
  31. package/src/tigerbeetle/src/cli.zig +142 -83
  32. package/src/tigerbeetle/src/config.zig +136 -23
  33. package/src/tigerbeetle/src/demo.zig +12 -8
  34. package/src/tigerbeetle/src/demo_03_create_transfers.zig +3 -3
  35. package/src/tigerbeetle/src/demo_04_create_pending_transfers.zig +10 -10
  36. package/src/tigerbeetle/src/demo_05_post_pending_transfers.zig +7 -7
  37. package/src/tigerbeetle/src/demo_06_void_pending_transfers.zig +3 -3
  38. package/src/tigerbeetle/src/demo_07_lookup_transfers.zig +1 -1
  39. package/src/tigerbeetle/src/ewah.zig +318 -0
  40. package/src/tigerbeetle/src/ewah_benchmark.zig +121 -0
  41. package/src/tigerbeetle/src/eytzinger_benchmark.zig +317 -0
  42. package/src/tigerbeetle/src/fifo.zig +17 -1
  43. package/src/tigerbeetle/src/io/darwin.zig +12 -10
  44. package/src/tigerbeetle/src/io/linux.zig +25 -9
  45. package/src/tigerbeetle/src/io/windows.zig +13 -9
  46. package/src/tigerbeetle/src/iops.zig +101 -0
  47. package/src/tigerbeetle/src/lsm/binary_search.zig +214 -0
  48. package/src/tigerbeetle/src/lsm/bloom_filter.zig +82 -0
  49. package/src/tigerbeetle/src/lsm/compaction.zig +603 -0
  50. package/src/tigerbeetle/src/lsm/composite_key.zig +75 -0
  51. package/src/tigerbeetle/src/lsm/direction.zig +11 -0
  52. package/src/tigerbeetle/src/lsm/eytzinger.zig +587 -0
  53. package/src/tigerbeetle/src/lsm/forest.zig +630 -0
  54. package/src/tigerbeetle/src/lsm/grid.zig +473 -0
  55. package/src/tigerbeetle/src/lsm/groove.zig +939 -0
  56. package/src/tigerbeetle/src/lsm/k_way_merge.zig +452 -0
  57. package/src/tigerbeetle/src/lsm/level_iterator.zig +296 -0
  58. package/src/tigerbeetle/src/lsm/manifest.zig +680 -0
  59. package/src/tigerbeetle/src/lsm/manifest_level.zig +1169 -0
  60. package/src/tigerbeetle/src/lsm/manifest_log.zig +904 -0
  61. package/src/tigerbeetle/src/lsm/node_pool.zig +231 -0
  62. package/src/tigerbeetle/src/lsm/posted_groove.zig +399 -0
  63. package/src/tigerbeetle/src/lsm/segmented_array.zig +998 -0
  64. package/src/tigerbeetle/src/lsm/set_associative_cache.zig +844 -0
  65. package/src/tigerbeetle/src/lsm/table.zig +932 -0
  66. package/src/tigerbeetle/src/lsm/table_immutable.zig +196 -0
  67. package/src/tigerbeetle/src/lsm/table_iterator.zig +295 -0
  68. package/src/tigerbeetle/src/lsm/table_mutable.zig +123 -0
  69. package/src/tigerbeetle/src/lsm/test.zig +429 -0
  70. package/src/tigerbeetle/src/lsm/tree.zig +1085 -0
  71. package/src/tigerbeetle/src/main.zig +121 -95
  72. package/src/tigerbeetle/src/message_bus.zig +49 -48
  73. package/src/tigerbeetle/src/message_pool.zig +19 -3
  74. package/src/tigerbeetle/src/ring_buffer.zig +172 -31
  75. package/src/tigerbeetle/src/simulator.zig +171 -43
  76. package/src/tigerbeetle/src/state_machine.zig +1026 -599
  77. package/src/tigerbeetle/src/storage.zig +46 -16
  78. package/src/tigerbeetle/src/test/cluster.zig +257 -78
  79. package/src/tigerbeetle/src/test/message_bus.zig +15 -24
  80. package/src/tigerbeetle/src/test/network.zig +26 -17
  81. package/src/tigerbeetle/src/test/packet_simulator.zig +14 -1
  82. package/src/tigerbeetle/src/test/state_checker.zig +10 -6
  83. package/src/tigerbeetle/src/test/state_machine.zig +159 -68
  84. package/src/tigerbeetle/src/test/storage.zig +137 -49
  85. package/src/tigerbeetle/src/tigerbeetle.zig +5 -0
  86. package/src/tigerbeetle/src/unit_tests.zig +8 -0
  87. package/src/tigerbeetle/src/util.zig +51 -0
  88. package/src/tigerbeetle/src/vsr/client.zig +21 -7
  89. package/src/tigerbeetle/src/vsr/journal.zig +1429 -514
  90. package/src/tigerbeetle/src/vsr/replica.zig +1855 -550
  91. package/src/tigerbeetle/src/vsr/superblock.zig +1743 -0
  92. package/src/tigerbeetle/src/vsr/superblock_client_table.zig +258 -0
  93. package/src/tigerbeetle/src/vsr/superblock_free_set.zig +644 -0
  94. package/src/tigerbeetle/src/vsr/superblock_manifest.zig +546 -0
  95. package/src/tigerbeetle/src/vsr.zig +134 -52
  96. package/.yarn/releases/yarn-berry.cjs +0 -55
  97. package/.yarnrc.yml +0 -1
  98. package/scripts/postinstall.sh +0 -6
  99. package/yarn.lock +0 -42
@@ -0,0 +1,844 @@
1
+ const std = @import("std");
2
+ const builtin = @import("builtin");
3
+
4
+ const assert = std.debug.assert;
5
+ const math = std.math;
6
+ const mem = std.mem;
7
+ const meta = std.meta;
8
+ const Vector = meta.Vector;
9
+
10
+ const config = @import("../config.zig");
11
+ const verify = config.verify;
12
+
13
+ pub const Layout = struct {
14
+ ways: u64 = 16,
15
+ tag_bits: u64 = 8,
16
+ clock_bits: u64 = 2,
17
+ cache_line_size: u64 = 64,
18
+ /// Set this to a non-null value to override the alignment of the stored values.
19
+ value_alignment: ?u29 = null,
20
+ };
21
+
22
+ pub fn SetAssociativeCache(
23
+ comptime Key: type,
24
+ comptime Value: type,
25
+ comptime key_from_value: fn (*const Value) callconv(.Inline) Key,
26
+ comptime hash: fn (Key) callconv(.Inline) u64,
27
+ comptime equal: fn (Key, Key) callconv(.Inline) bool,
28
+ comptime layout: Layout,
29
+ ) type {
30
+ assert(math.isPowerOfTwo(@sizeOf(Key)));
31
+ assert(math.isPowerOfTwo(@sizeOf(Value)));
32
+
33
+ switch (layout.ways) {
34
+ // An 8-way set-associative cache has the clock hand as a u3, which would introduce padding.
35
+ 2, 4, 16 => {},
36
+ else => @compileError("ways must be 2, 4 or 16 for optimal CLOCK hand size."),
37
+ }
38
+ switch (layout.tag_bits) {
39
+ 8, 16 => {},
40
+ else => @compileError("tag_bits must be 8 or 16."),
41
+ }
42
+ switch (layout.clock_bits) {
43
+ 1, 2, 4 => {},
44
+ else => @compileError("clock_bits must be 1, 2 or 4."),
45
+ }
46
+
47
+ if (layout.value_alignment) |alignment| {
48
+ assert(alignment >= @alignOf(Value));
49
+ assert(@sizeOf(Value) % alignment == 0);
50
+ }
51
+ const value_alignment = layout.value_alignment orelse @alignOf(Value);
52
+
53
+ assert(math.isPowerOfTwo(layout.ways));
54
+ assert(math.isPowerOfTwo(layout.tag_bits));
55
+ assert(math.isPowerOfTwo(layout.clock_bits));
56
+ assert(math.isPowerOfTwo(layout.cache_line_size));
57
+
58
+ assert(@sizeOf(Key) <= @sizeOf(Value));
59
+ assert(@sizeOf(Key) < layout.cache_line_size);
60
+ assert(layout.cache_line_size % @sizeOf(Key) == 0);
61
+
62
+ if (layout.cache_line_size > @sizeOf(Value)) {
63
+ assert(layout.cache_line_size % @sizeOf(Value) == 0);
64
+ } else {
65
+ assert(@sizeOf(Value) % layout.cache_line_size == 0);
66
+ }
67
+
68
+ const clock_hand_bits = math.log2_int(u64, layout.ways);
69
+ assert(math.isPowerOfTwo(clock_hand_bits));
70
+ assert((1 << clock_hand_bits) == layout.ways);
71
+
72
+ const tags_per_line = @divExact(layout.cache_line_size * 8, layout.ways * layout.tag_bits);
73
+ assert(tags_per_line > 0);
74
+
75
+ const clocks_per_line = @divExact(layout.cache_line_size * 8, layout.ways * layout.clock_bits);
76
+ assert(clocks_per_line > 0);
77
+
78
+ const clock_hands_per_line = @divExact(layout.cache_line_size * 8, clock_hand_bits);
79
+ assert(clock_hands_per_line > 0);
80
+
81
+ return struct {
82
+ const Self = @This();
83
+
84
+ const Tag = meta.Int(.unsigned, layout.tag_bits);
85
+ const Count = meta.Int(.unsigned, layout.clock_bits);
86
+ const Clock = meta.Int(.unsigned, clock_hand_bits);
87
+
88
+ sets: u64,
89
+ tags: []Tag,
90
+ values: []align(value_alignment) Value,
91
+ counts: PackedUnsignedIntegerArray(Count),
92
+ clocks: PackedUnsignedIntegerArray(Clock),
93
+
94
+ pub fn init(allocator: mem.Allocator, value_count_max: u64) !Self {
95
+ assert(math.isPowerOfTwo(value_count_max));
96
+ assert(value_count_max > 0);
97
+ assert(value_count_max >= layout.ways);
98
+ assert(value_count_max % layout.ways == 0);
99
+
100
+ const sets = @divExact(value_count_max, layout.ways);
101
+ assert(math.isPowerOfTwo(sets));
102
+
103
+ const values_size_max = value_count_max * @sizeOf(Value);
104
+ assert(values_size_max >= layout.cache_line_size);
105
+ assert(values_size_max % layout.cache_line_size == 0);
106
+
107
+ const counts_size = @divExact(value_count_max * layout.clock_bits, 8);
108
+ assert(counts_size >= layout.cache_line_size);
109
+ assert(counts_size % layout.cache_line_size == 0);
110
+
111
+ const clocks_size = @divExact(sets * clock_hand_bits, 8);
112
+ assert(clocks_size >= layout.cache_line_size);
113
+ assert(clocks_size % layout.cache_line_size == 0);
114
+
115
+ const tags = try allocator.alloc(Tag, value_count_max);
116
+ errdefer allocator.free(tags);
117
+
118
+ const values = try allocator.allocAdvanced(
119
+ Value,
120
+ value_alignment,
121
+ value_count_max,
122
+ .exact,
123
+ );
124
+ errdefer allocator.free(values);
125
+
126
+ const counts = try allocator.alloc(u64, @divExact(counts_size, @sizeOf(u64)));
127
+ errdefer allocator.free(counts);
128
+
129
+ const clocks = try allocator.alloc(u64, @divExact(clocks_size, @sizeOf(u64)));
130
+ errdefer allocator.free(clocks);
131
+
132
+ var self = Self{
133
+ .sets = sets,
134
+ .tags = tags,
135
+ .values = values,
136
+ .counts = .{ .words = counts },
137
+ .clocks = .{ .words = clocks },
138
+ };
139
+
140
+ self.reset();
141
+
142
+ return self;
143
+ }
144
+
145
+ pub fn deinit(self: *Self, allocator: mem.Allocator) void {
146
+ assert(self.sets > 0);
147
+ self.sets = 0;
148
+
149
+ allocator.free(self.tags);
150
+ allocator.free(self.values);
151
+ allocator.free(self.counts.words);
152
+ allocator.free(self.clocks.words);
153
+ }
154
+
155
+ pub fn reset(self: *Self) void {
156
+ mem.set(Tag, self.tags, 0);
157
+ mem.set(u64, self.counts.words, 0);
158
+ mem.set(u64, self.clocks.words, 0);
159
+ }
160
+
161
+ pub fn get(self: *Self, key: Key) ?*align(value_alignment) Value {
162
+ const set = self.associate(key);
163
+ const way = self.search(set, key) orelse return null;
164
+
165
+ const count = self.counts.get(set.offset + way);
166
+ self.counts.set(set.offset + way, count +| 1);
167
+
168
+ return @alignCast(value_alignment, &set.values[way]);
169
+ }
170
+
171
+ /// Remove a key from the set associative cache if present.
172
+ pub fn remove(self: *Self, key: Key) void {
173
+ const set = self.associate(key);
174
+ const way = self.search(set, key) orelse return;
175
+
176
+ self.counts.set(set.offset + way, 0);
177
+ }
178
+
179
+ /// If the key is present in the set, returns the way. Otherwise returns null.
180
+ inline fn search(self: *Self, set: Set, key: Key) ?usize {
181
+ const ways = search_tags(set.tags, set.tag);
182
+
183
+ var it = BitIterator(Ways){ .bits = ways };
184
+ while (it.next()) |way| {
185
+ const count = self.counts.get(set.offset + way);
186
+ if (count > 0 and equal(key_from_value(&set.values[way]), key)) {
187
+ return way;
188
+ }
189
+ }
190
+
191
+ return null;
192
+ }
193
+
194
+ /// Where each set bit represents the index of a way that has the same tag.
195
+ const Ways = meta.Int(.unsigned, layout.ways);
196
+
197
+ inline fn search_tags(tags: *[layout.ways]Tag, tag: Tag) Ways {
198
+ const x: Vector(layout.ways, Tag) = tags.*;
199
+ const y: Vector(layout.ways, Tag) = @splat(layout.ways, tag);
200
+
201
+ const result: Vector(layout.ways, bool) = x == y;
202
+ return @ptrCast(*const Ways, &result).*;
203
+ }
204
+
205
+ pub fn put_no_clobber(self: *Self, key: Key) *align(value_alignment) Value {
206
+ return self.put_no_clobber_preserve_locked(
207
+ void,
208
+ struct {
209
+ inline fn locked(_: void, _: *const Value) bool {
210
+ return false;
211
+ }
212
+ }.locked,
213
+ {},
214
+ key,
215
+ );
216
+ }
217
+
218
+ /// Add a key, evicting an older entry if needed, and return a pointer to the value.
219
+ /// The key must not already be in the cache.
220
+ /// Never evicts keys for which locked() returns true.
221
+ /// The caller must guarantee that locked() returns true for less than layout.ways keys.
222
+ pub fn put_no_clobber_preserve_locked(
223
+ self: *Self,
224
+ comptime Context: type,
225
+ comptime locked: fn (
226
+ Context,
227
+ *align(value_alignment) const Value,
228
+ ) callconv(.Inline) bool,
229
+ context: Context,
230
+ key: Key,
231
+ ) *align(value_alignment) Value {
232
+ const set = self.associate(key);
233
+
234
+ if (verify) assert(self.search(set, key) == null);
235
+
236
+ const clock_index = @divExact(set.offset, layout.ways);
237
+
238
+ var way = self.clocks.get(clock_index);
239
+ comptime assert(math.maxInt(@TypeOf(way)) == layout.ways - 1);
240
+ comptime assert(@as(@TypeOf(way), math.maxInt(@TypeOf(way))) +% 1 == 0);
241
+
242
+ // The maximum number of iterations happens when every slot in the set has the maximum
243
+ // count. In this case, the loop will iterate until all counts have been decremented
244
+ // to 1. Then in the next iteration it will decrement a count to 0 and break.
245
+ const clock_iterations_max = layout.ways * (math.maxInt(Count) - 1);
246
+
247
+ var safety_count: usize = 0;
248
+ while (safety_count <= clock_iterations_max) : ({
249
+ safety_count += 1;
250
+ way +%= 1;
251
+ }) {
252
+ // We pass a value pointer to the callback here so that a cache miss
253
+ // can be avoided if the caller is able to determine if the value is
254
+ // locked by comparing pointers directly.
255
+ if (locked(context, @alignCast(value_alignment, &set.values[way]))) continue;
256
+
257
+ var count = self.counts.get(set.offset + way);
258
+ if (count == 0) break; // Way is already free.
259
+
260
+ count -= 1;
261
+ self.counts.set(set.offset + way, count);
262
+ if (count == 0) break; // Way has become free.
263
+ } else {
264
+ unreachable;
265
+ }
266
+ assert(self.counts.get(set.offset + way) == 0);
267
+
268
+ set.tags[way] = set.tag;
269
+ self.counts.set(set.offset + way, 1);
270
+ self.clocks.set(clock_index, way +% 1);
271
+
272
+ return @alignCast(value_alignment, &set.values[way]);
273
+ }
274
+
275
+ const Set = struct {
276
+ tag: Tag,
277
+ offset: u64,
278
+ tags: *[layout.ways]Tag,
279
+ values: *[layout.ways]Value,
280
+
281
+ fn inspect(set: Set, sac: Self) void {
282
+ const clock_index = @divExact(set.offset, layout.ways);
283
+ std.debug.print(
284
+ \\{{
285
+ \\ tag={}
286
+ \\ offset={}
287
+ \\ clock_hand={}
288
+ , .{
289
+ set.tag,
290
+ set.offset,
291
+ sac.clocks.get(clock_index),
292
+ });
293
+
294
+ std.debug.print("\n tags={}", .{set.tags[0]});
295
+ for (set.tags[1..]) |tag| std.debug.print(", {}", .{tag});
296
+
297
+ std.debug.print("\n values={}", .{set.values[0]});
298
+ for (set.values[1..]) |value| std.debug.print(", {}", .{value});
299
+
300
+ std.debug.print("\n counts={}", .{sac.counts.get(set.offset)});
301
+ var i: usize = 1;
302
+ while (i < layout.ways) : (i += 1) {
303
+ std.debug.print(", {}", .{sac.counts.get(set.offset + i)});
304
+ }
305
+
306
+ std.debug.print("\n}}\n", .{});
307
+ }
308
+ };
309
+
310
+ inline fn associate(self: *Self, key: Key) Set {
311
+ const entropy = hash(key);
312
+
313
+ const tag = @truncate(Tag, entropy >> math.log2_int(u64, self.sets));
314
+ const index = entropy % self.sets;
315
+ const offset = index * layout.ways;
316
+
317
+ return .{
318
+ .tag = tag,
319
+ .offset = offset,
320
+ .tags = self.tags[offset..][0..layout.ways],
321
+ .values = self.values[offset..][0..layout.ways],
322
+ };
323
+ }
324
+
325
+ pub fn inspect() void {
326
+ std.debug.print("\nKey={} Value={} ways={} tag_bits={} clock_bits={} " ++
327
+ "clock_hand_bits={} tags_per_line={} clocks_per_line={} " ++
328
+ "clock_hands_per_line={}\n", .{
329
+ @bitSizeOf(Key),
330
+ @sizeOf(Value),
331
+ layout.ways,
332
+ layout.tag_bits,
333
+ layout.clock_bits,
334
+ clock_hand_bits,
335
+ tags_per_line,
336
+ clocks_per_line,
337
+ clock_hands_per_line,
338
+ });
339
+ }
340
+ };
341
+ }
342
+
343
+ fn set_associative_cache_test(
344
+ comptime Key: type,
345
+ comptime Value: type,
346
+ comptime context: type,
347
+ comptime layout: Layout,
348
+ ) type {
349
+ const testing = std.testing;
350
+ const expect = testing.expect;
351
+ const expectEqual = testing.expectEqual;
352
+
353
+ const log = false;
354
+
355
+ const SAC = SetAssociativeCache(
356
+ Key,
357
+ Value,
358
+ context.key_from_value,
359
+ context.hash,
360
+ context.equal,
361
+ layout,
362
+ );
363
+
364
+ return struct {
365
+ fn run() !void {
366
+ if (log) SAC.inspect();
367
+
368
+ // TODO Add a nice calculator method to help solve the minimum value_count_max required:
369
+ var sac = try SAC.init(testing.allocator, 16 * 16 * 8);
370
+ defer sac.deinit(testing.allocator);
371
+
372
+ for (sac.tags) |tag| try testing.expectEqual(@as(SAC.Tag, 0), tag);
373
+ for (sac.counts.words) |word| try testing.expectEqual(@as(u64, 0), word);
374
+ for (sac.clocks.words) |word| try testing.expectEqual(@as(u64, 0), word);
375
+
376
+ // Fill up the first set entirely.
377
+ {
378
+ var i: usize = 0;
379
+ while (i < layout.ways) : (i += 1) {
380
+ try expectEqual(i, sac.clocks.get(0));
381
+
382
+ const key = i * sac.sets;
383
+ sac.put_no_clobber(key).* = key;
384
+ try expect(sac.counts.get(i) == 1);
385
+ try expectEqual(key, sac.get(key).?.*);
386
+ try expect(sac.counts.get(i) == 2);
387
+ }
388
+ try expect(sac.clocks.get(0) == 0);
389
+ }
390
+
391
+ if (log) sac.associate(0).inspect(sac);
392
+
393
+ // Insert another element into the first set, causing key 0 to be evicted.
394
+ {
395
+ const key = layout.ways * sac.sets;
396
+ sac.put_no_clobber(key).* = key;
397
+ try expect(sac.counts.get(0) == 1);
398
+ try expectEqual(key, sac.get(key).?.*);
399
+ try expect(sac.counts.get(0) == 2);
400
+
401
+ try expectEqual(@as(?*Value, null), sac.get(0));
402
+
403
+ {
404
+ var i: usize = 1;
405
+ while (i < layout.ways) : (i += 1) {
406
+ try expect(sac.counts.get(i) == 1);
407
+ }
408
+ }
409
+ }
410
+
411
+ if (log) sac.associate(0).inspect(sac);
412
+
413
+ // Lock all other slots, causing key layout.ways * sac.sets to be evicted despite having the
414
+ // highest count.
415
+ {
416
+ {
417
+ assert(sac.counts.get(0) == 2);
418
+ var i: usize = 1;
419
+ while (i < layout.ways) : (i += 1) assert(sac.counts.get(i) == 1);
420
+ }
421
+
422
+ const key = (layout.ways + 1) * sac.sets;
423
+ const expect_evicted = layout.ways * sac.sets;
424
+
425
+ sac.put_no_clobber_preserve_locked(
426
+ u64,
427
+ struct {
428
+ inline fn locked(only_unlocked: u64, value: *const Value) bool {
429
+ return value.* != only_unlocked;
430
+ }
431
+ }.locked,
432
+ expect_evicted,
433
+ key,
434
+ ).* = key;
435
+
436
+ try expectEqual(@as(?*Value, null), sac.get(expect_evicted));
437
+ }
438
+
439
+ if (log) sac.associate(0).inspect(sac);
440
+
441
+ // Ensure removal works.
442
+ {
443
+ const key = 5 * sac.sets;
444
+ assert(sac.get(key).?.* == key);
445
+ try expect(sac.counts.get(5) == 2);
446
+
447
+ sac.remove(key);
448
+ try expectEqual(@as(?*Value, null), sac.get(key));
449
+ try expect(sac.counts.get(5) == 0);
450
+ }
451
+
452
+ sac.reset();
453
+
454
+ for (sac.tags) |tag| try testing.expectEqual(@as(SAC.Tag, 0), tag);
455
+ for (sac.counts.words) |word| try testing.expectEqual(@as(u64, 0), word);
456
+ for (sac.clocks.words) |word| try testing.expectEqual(@as(u64, 0), word);
457
+
458
+ // Fill up the first set entirely, maxing out the count for each slot.
459
+ {
460
+ var i: usize = 0;
461
+ while (i < layout.ways) : (i += 1) {
462
+ try expectEqual(i, sac.clocks.get(0));
463
+
464
+ const key = i * sac.sets;
465
+ sac.put_no_clobber(key).* = key;
466
+ try expect(sac.counts.get(i) == 1);
467
+ var j: usize = 2;
468
+ while (j <= math.maxInt(SAC.Count)) : (j += 1) {
469
+ try expectEqual(key, sac.get(key).?.*);
470
+ try expect(sac.counts.get(i) == j);
471
+ }
472
+ try expectEqual(key, sac.get(key).?.*);
473
+ try expect(sac.counts.get(i) == math.maxInt(SAC.Count));
474
+ }
475
+ try expect(sac.clocks.get(0) == 0);
476
+ }
477
+
478
+ if (log) sac.associate(0).inspect(sac);
479
+
480
+ // Insert another element into the first set, causing key 0 to be evicted.
481
+ {
482
+ const key = layout.ways * sac.sets;
483
+ sac.put_no_clobber(key).* = key;
484
+ try expect(sac.counts.get(0) == 1);
485
+ try expectEqual(key, sac.get(key).?.*);
486
+ try expect(sac.counts.get(0) == 2);
487
+
488
+ try expectEqual(@as(?*Value, null), sac.get(0));
489
+
490
+ {
491
+ var i: usize = 1;
492
+ while (i < layout.ways) : (i += 1) {
493
+ try expect(sac.counts.get(i) == 1);
494
+ }
495
+ }
496
+ }
497
+
498
+ if (log) sac.associate(0).inspect(sac);
499
+ }
500
+ };
501
+ }
502
+
503
+ test "SetAssociativeCache: eviction" {
504
+ const Key = u64;
505
+ const Value = u64;
506
+
507
+ const context = struct {
508
+ inline fn key_from_value(value: *const Value) Key {
509
+ return value.*;
510
+ }
511
+ inline fn hash(key: Key) u64 {
512
+ return key;
513
+ }
514
+ inline fn equal(a: Key, b: Key) bool {
515
+ return a == b;
516
+ }
517
+ };
518
+
519
+ try set_associative_cache_test(Key, Value, context, .{}).run();
520
+ }
521
+
522
+ test "SetAssociativeCache: hash collision" {
523
+ const Key = u64;
524
+ const Value = u64;
525
+
526
+ const context = struct {
527
+ inline fn key_from_value(value: *const Value) Key {
528
+ return value.*;
529
+ }
530
+ /// This hash function is intentionally broken to simulate hash collision.
531
+ inline fn hash(key: Key) u64 {
532
+ _ = key;
533
+ return 0;
534
+ }
535
+ inline fn equal(a: Key, b: Key) bool {
536
+ return a == b;
537
+ }
538
+ };
539
+
540
+ try set_associative_cache_test(Key, Value, context, .{}).run();
541
+ }
542
+
543
+ /// A little simpler than PackedIntArray in the std lib, restricted to little endian 64-bit words,
544
+ /// and using words exactly without padding.
545
+ fn PackedUnsignedIntegerArray(comptime UInt: type) type {
546
+ const Word = u64;
547
+
548
+ assert(builtin.target.cpu.arch.endian() == .Little);
549
+ assert(@typeInfo(UInt).Int.signedness == .unsigned);
550
+ assert(@typeInfo(UInt).Int.bits < meta.bitCount(u8));
551
+ assert(math.isPowerOfTwo(@typeInfo(UInt).Int.bits));
552
+
553
+ const word_bits = meta.bitCount(Word);
554
+ const uint_bits = meta.bitCount(UInt);
555
+ const uints_per_word = @divExact(word_bits, uint_bits);
556
+
557
+ // An index bounded by the number of unsigned integers that fit exactly into a word.
558
+ const WordIndex = meta.Int(.unsigned, math.log2_int(u64, uints_per_word));
559
+ assert(math.maxInt(WordIndex) == uints_per_word - 1);
560
+
561
+ // An index bounded by the number of bits (not unsigned integers) that fit exactly into a word.
562
+ const BitsIndex = math.Log2Int(Word);
563
+ assert(math.maxInt(BitsIndex) == meta.bitCount(Word) - 1);
564
+ assert(math.maxInt(BitsIndex) == word_bits - 1);
565
+ assert(math.maxInt(BitsIndex) == uint_bits * (math.maxInt(WordIndex) + 1) - 1);
566
+
567
+ return struct {
568
+ const Self = @This();
569
+
570
+ words: []Word,
571
+
572
+ /// Returns the unsigned integer at `index`.
573
+ pub inline fn get(self: Self, index: u64) UInt {
574
+ // This truncate is safe since we want to mask the right-shifted word by exactly a UInt:
575
+ return @truncate(UInt, self.word(index).* >> bits_index(index));
576
+ }
577
+
578
+ /// Sets the unsigned integer at `index` to `value`.
579
+ pub inline fn set(self: Self, index: u64, value: UInt) void {
580
+ const w = self.word(index);
581
+ w.* &= ~mask(index);
582
+ w.* |= @as(Word, value) << bits_index(index);
583
+ }
584
+
585
+ inline fn mask(index: u64) Word {
586
+ return @as(Word, math.maxInt(UInt)) << bits_index(index);
587
+ }
588
+
589
+ inline fn word(self: Self, index: u64) *Word {
590
+ return &self.words[@divFloor(index, uints_per_word)];
591
+ }
592
+
593
+ inline fn bits_index(index: u64) BitsIndex {
594
+ // If uint_bits=2, then it's normal for the maximum return value value to be 62, even
595
+ // where BitsIndex allows up to 63 (inclusive) for a 64-bit word. This is because 62 is
596
+ // the bit index of the highest 2-bit UInt (e.g. bit index + bit length == 64).
597
+ comptime assert(uint_bits * (math.maxInt(WordIndex) + 1) == math.maxInt(BitsIndex) + 1);
598
+
599
+ return @as(BitsIndex, uint_bits) * @truncate(WordIndex, index);
600
+ }
601
+ };
602
+ }
603
+
604
+ test "PackedUnsignedIntegerArray: unit" {
605
+ const expectEqual = std.testing.expectEqual;
606
+
607
+ var words = [8]u64{ 0, 0b10110010, 0, 0, 0, 0, 0, 0 };
608
+
609
+ var p: PackedUnsignedIntegerArray(u2) = .{
610
+ .words = &words,
611
+ };
612
+
613
+ try expectEqual(@as(u2, 0b10), p.get(32 + 0));
614
+ try expectEqual(@as(u2, 0b00), p.get(32 + 1));
615
+ try expectEqual(@as(u2, 0b11), p.get(32 + 2));
616
+ try expectEqual(@as(u2, 0b10), p.get(32 + 3));
617
+
618
+ p.set(0, 0b01);
619
+ try expectEqual(@as(u64, 0b00000001), words[0]);
620
+ try expectEqual(@as(u2, 0b01), p.get(0));
621
+ p.set(1, 0b10);
622
+ try expectEqual(@as(u64, 0b00001001), words[0]);
623
+ try expectEqual(@as(u2, 0b10), p.get(1));
624
+ p.set(2, 0b11);
625
+ try expectEqual(@as(u64, 0b00111001), words[0]);
626
+ try expectEqual(@as(u2, 0b11), p.get(2));
627
+ p.set(3, 0b11);
628
+ try expectEqual(@as(u64, 0b11111001), words[0]);
629
+ try expectEqual(@as(u2, 0b11), p.get(3));
630
+ p.set(3, 0b01);
631
+ try expectEqual(@as(u64, 0b01111001), words[0]);
632
+ try expectEqual(@as(u2, 0b01), p.get(3));
633
+ p.set(3, 0b00);
634
+ try expectEqual(@as(u64, 0b00111001), words[0]);
635
+ try expectEqual(@as(u2, 0b00), p.get(3));
636
+
637
+ p.set(4, 0b11);
638
+ try expectEqual(
639
+ @as(u64, 0b0000000000000000000000000000000000000000000000000000001100111001),
640
+ words[0],
641
+ );
642
+ p.set(31, 0b11);
643
+ try expectEqual(
644
+ @as(u64, 0b1100000000000000000000000000000000000000000000000000001100111001),
645
+ words[0],
646
+ );
647
+ }
648
+
649
+ fn PackedUnsignedIntegerArrayFuzzTest(comptime UInt: type) type {
650
+ const testing = std.testing;
651
+
652
+ return struct {
653
+ const Self = @This();
654
+
655
+ const Array = PackedUnsignedIntegerArray(UInt);
656
+ random: std.rand.Random,
657
+
658
+ array: Array,
659
+ reference: []UInt,
660
+
661
+ fn init(random: std.rand.Random, len: usize) !Self {
662
+ const words = try testing.allocator.alloc(u64, @divExact(len * @bitSizeOf(UInt), 64));
663
+ errdefer testing.allocator.free(words);
664
+
665
+ const reference = try testing.allocator.alloc(UInt, len);
666
+ errdefer testing.allocator.free(reference);
667
+
668
+ mem.set(u64, words, 0);
669
+ mem.set(UInt, reference, 0);
670
+
671
+ return Self{
672
+ .random = random,
673
+ .array = Array{ .words = words },
674
+ .reference = reference,
675
+ };
676
+ }
677
+
678
+ fn deinit(context: *Self) void {
679
+ testing.allocator.free(context.array.words);
680
+ testing.allocator.free(context.reference);
681
+ }
682
+
683
+ fn run(context: *Self) !void {
684
+ var iterations: usize = 0;
685
+ while (iterations < 10_000) : (iterations += 1) {
686
+ const index = context.random.uintLessThanBiased(usize, context.reference.len);
687
+ const value = context.random.int(UInt);
688
+
689
+ context.array.set(index, value);
690
+ context.reference[index] = value;
691
+
692
+ try context.verify();
693
+ }
694
+ }
695
+
696
+ fn verify(context: *Self) !void {
697
+ for (context.reference) |value, index| {
698
+ try testing.expectEqual(value, context.array.get(index));
699
+ }
700
+ }
701
+ };
702
+ }
703
+
704
+ test "PackedUnsignedIntegerArray: fuzz" {
705
+ const seed = 42;
706
+
707
+ var prng = std.rand.DefaultPrng.init(seed);
708
+ const random = prng.random();
709
+
710
+ inline for (.{ u1, u2, u4 }) |UInt| {
711
+ const Context = PackedUnsignedIntegerArrayFuzzTest(UInt);
712
+
713
+ var context = try Context.init(random, 1024);
714
+ defer context.deinit();
715
+
716
+ try context.run();
717
+ }
718
+ }
719
+
720
+ fn BitIterator(comptime Bits: type) type {
721
+ return struct {
722
+ const Self = @This();
723
+ const BitIndex = math.Log2Int(Bits);
724
+
725
+ bits: Bits,
726
+
727
+ /// Iterates over the bits, consuming them.
728
+ /// Returns the bit index of each set bit until there are no more set bits, then null.
729
+ inline fn next(it: *Self) ?BitIndex {
730
+ if (it.bits == 0) return null;
731
+ // This @intCast() is safe since we never pass 0 to @ctz().
732
+ const index = @intCast(BitIndex, @ctz(Bits, it.bits));
733
+ // Zero the lowest set bit.
734
+ it.bits &= it.bits - 1;
735
+ return index;
736
+ }
737
+ };
738
+ }
739
+
740
+ test "BitIterator" {
741
+ const expectEqual = @import("std").testing.expectEqual;
742
+
743
+ var it = BitIterator(u16){ .bits = 0b1000_0000_0100_0101 };
744
+
745
+ for ([_]u4{ 0, 2, 6, 15 }) |e| {
746
+ try expectEqual(@as(?u4, e), it.next());
747
+ }
748
+ try expectEqual(it.next(), null);
749
+ }
750
+
751
+ fn search_tags_test(comptime Key: type, comptime Value: type, comptime layout: Layout) type {
752
+ const testing = std.testing;
753
+
754
+ const log = false;
755
+
756
+ const context = struct {
757
+ inline fn key_from_value(value: *const Value) Key {
758
+ return value.*;
759
+ }
760
+ inline fn hash(key: Key) u64 {
761
+ return key;
762
+ }
763
+ inline fn equal(a: Key, b: Key) bool {
764
+ return a == b;
765
+ }
766
+ };
767
+
768
+ const SAC = SetAssociativeCache(
769
+ Key,
770
+ Value,
771
+ context.key_from_value,
772
+ context.hash,
773
+ context.equal,
774
+ layout,
775
+ );
776
+
777
+ const reference = struct {
778
+ inline fn search_tags(tags: *[layout.ways]SAC.Tag, tag: SAC.Tag) SAC.Ways {
779
+ var bits: SAC.Ways = 0;
780
+ var count: usize = 0;
781
+ for (tags) |t, i| {
782
+ if (t == tag) {
783
+ const bit = @intCast(math.Log2Int(SAC.Ways), i);
784
+ bits |= (@as(SAC.Ways, 1) << bit);
785
+ count += 1;
786
+ }
787
+ }
788
+ assert(@popCount(SAC.Ways, bits) == count);
789
+ return bits;
790
+ }
791
+ };
792
+
793
+ return struct {
794
+ fn run(random: std.rand.Random) !void {
795
+ if (log) SAC.inspect();
796
+
797
+ var iterations: usize = 0;
798
+ while (iterations < 10_000) : (iterations += 1) {
799
+ var tags: [layout.ways]SAC.Tag = undefined;
800
+ random.bytes(mem.asBytes(&tags));
801
+
802
+ const tag = random.int(SAC.Tag);
803
+
804
+ var indexes: [layout.ways]usize = undefined;
805
+ for (indexes) |*x, i| x.* = i;
806
+ random.shuffle(usize, &indexes);
807
+
808
+ const matches_count_min = random.uintAtMostBiased(u32, layout.ways);
809
+ for (indexes[0..matches_count_min]) |index| {
810
+ tags[index] = tag;
811
+ }
812
+
813
+ const expected = reference.search_tags(&tags, tag);
814
+ const actual = SAC.search_tags(&tags, tag);
815
+ if (log) std.debug.print("expected: {b:0>16}, actual: {b:0>16}\n", .{
816
+ expected,
817
+ actual,
818
+ });
819
+ try testing.expectEqual(expected, actual);
820
+ }
821
+ }
822
+ };
823
+ }
824
+
825
+ test "SetAssociativeCache: search_tags()" {
826
+ const seed = 42;
827
+
828
+ const Key = u64;
829
+ const Value = u64;
830
+
831
+ var prng = std.rand.DefaultPrng.init(seed);
832
+ const random = prng.random();
833
+
834
+ inline for ([_]u64{ 2, 4, 16 }) |ways| {
835
+ inline for ([_]u64{ 8, 16 }) |tag_bits| {
836
+ const case = search_tags_test(Key, Value, .{
837
+ .ways = ways,
838
+ .tag_bits = tag_bits,
839
+ });
840
+
841
+ try case.run(random);
842
+ }
843
+ }
844
+ }