tigerbeetle-node 0.9.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 (83) hide show
  1. package/README.md +3 -2
  2. package/dist/index.d.ts +66 -61
  3. package/dist/index.js +66 -61
  4. package/dist/index.js.map +1 -1
  5. package/package.json +1 -1
  6. package/src/index.ts +5 -0
  7. package/src/node.zig +17 -18
  8. package/src/tigerbeetle/scripts/benchmark.bat +4 -3
  9. package/src/tigerbeetle/scripts/benchmark.sh +25 -10
  10. package/src/tigerbeetle/scripts/install.sh +2 -1
  11. package/src/tigerbeetle/scripts/install_zig.sh +14 -18
  12. package/src/tigerbeetle/scripts/upgrade_ubuntu_kernel.sh +12 -3
  13. package/src/tigerbeetle/scripts/vopr.sh +5 -5
  14. package/src/tigerbeetle/src/benchmark.zig +17 -9
  15. package/src/tigerbeetle/src/benchmark_array_search.zig +317 -0
  16. package/src/tigerbeetle/src/benchmarks/perf.zig +299 -0
  17. package/src/tigerbeetle/src/c/tb_client/context.zig +103 -0
  18. package/src/tigerbeetle/src/c/tb_client/packet.zig +80 -0
  19. package/src/tigerbeetle/src/c/tb_client/signal.zig +288 -0
  20. package/src/tigerbeetle/src/c/tb_client/thread.zig +329 -0
  21. package/src/tigerbeetle/src/c/tb_client.h +201 -0
  22. package/src/tigerbeetle/src/c/tb_client.zig +101 -0
  23. package/src/tigerbeetle/src/c/test.zig +1 -0
  24. package/src/tigerbeetle/src/cli.zig +142 -83
  25. package/src/tigerbeetle/src/config.zig +119 -10
  26. package/src/tigerbeetle/src/demo.zig +12 -8
  27. package/src/tigerbeetle/src/demo_05_post_pending_transfers.zig +2 -2
  28. package/src/tigerbeetle/src/ewah.zig +318 -0
  29. package/src/tigerbeetle/src/ewah_benchmark.zig +121 -0
  30. package/src/tigerbeetle/src/eytzinger_benchmark.zig +317 -0
  31. package/src/tigerbeetle/src/fifo.zig +17 -1
  32. package/src/tigerbeetle/src/io/darwin.zig +12 -10
  33. package/src/tigerbeetle/src/io/linux.zig +25 -9
  34. package/src/tigerbeetle/src/io/windows.zig +13 -9
  35. package/src/tigerbeetle/src/iops.zig +101 -0
  36. package/src/tigerbeetle/src/lsm/binary_search.zig +214 -0
  37. package/src/tigerbeetle/src/lsm/bloom_filter.zig +82 -0
  38. package/src/tigerbeetle/src/lsm/compaction.zig +603 -0
  39. package/src/tigerbeetle/src/lsm/composite_key.zig +75 -0
  40. package/src/tigerbeetle/src/lsm/direction.zig +11 -0
  41. package/src/tigerbeetle/src/lsm/eytzinger.zig +587 -0
  42. package/src/tigerbeetle/src/lsm/forest.zig +630 -0
  43. package/src/tigerbeetle/src/lsm/grid.zig +473 -0
  44. package/src/tigerbeetle/src/lsm/groove.zig +939 -0
  45. package/src/tigerbeetle/src/lsm/k_way_merge.zig +452 -0
  46. package/src/tigerbeetle/src/lsm/level_iterator.zig +296 -0
  47. package/src/tigerbeetle/src/lsm/manifest.zig +680 -0
  48. package/src/tigerbeetle/src/lsm/manifest_level.zig +1169 -0
  49. package/src/tigerbeetle/src/lsm/manifest_log.zig +904 -0
  50. package/src/tigerbeetle/src/lsm/node_pool.zig +231 -0
  51. package/src/tigerbeetle/src/lsm/posted_groove.zig +399 -0
  52. package/src/tigerbeetle/src/lsm/segmented_array.zig +998 -0
  53. package/src/tigerbeetle/src/lsm/set_associative_cache.zig +844 -0
  54. package/src/tigerbeetle/src/lsm/table.zig +932 -0
  55. package/src/tigerbeetle/src/lsm/table_immutable.zig +196 -0
  56. package/src/tigerbeetle/src/lsm/table_iterator.zig +295 -0
  57. package/src/tigerbeetle/src/lsm/table_mutable.zig +123 -0
  58. package/src/tigerbeetle/src/lsm/test.zig +429 -0
  59. package/src/tigerbeetle/src/lsm/tree.zig +1085 -0
  60. package/src/tigerbeetle/src/main.zig +119 -109
  61. package/src/tigerbeetle/src/message_bus.zig +49 -48
  62. package/src/tigerbeetle/src/message_pool.zig +15 -2
  63. package/src/tigerbeetle/src/ring_buffer.zig +126 -30
  64. package/src/tigerbeetle/src/simulator.zig +76 -44
  65. package/src/tigerbeetle/src/state_machine.zig +1022 -585
  66. package/src/tigerbeetle/src/storage.zig +46 -16
  67. package/src/tigerbeetle/src/test/cluster.zig +109 -63
  68. package/src/tigerbeetle/src/test/message_bus.zig +15 -24
  69. package/src/tigerbeetle/src/test/network.zig +26 -17
  70. package/src/tigerbeetle/src/test/state_checker.zig +7 -5
  71. package/src/tigerbeetle/src/test/state_machine.zig +159 -69
  72. package/src/tigerbeetle/src/test/storage.zig +57 -28
  73. package/src/tigerbeetle/src/tigerbeetle.zig +5 -0
  74. package/src/tigerbeetle/src/unit_tests.zig +8 -0
  75. package/src/tigerbeetle/src/util.zig +51 -0
  76. package/src/tigerbeetle/src/vsr/client.zig +21 -7
  77. package/src/tigerbeetle/src/vsr/journal.zig +154 -167
  78. package/src/tigerbeetle/src/vsr/replica.zig +744 -226
  79. package/src/tigerbeetle/src/vsr/superblock.zig +1743 -0
  80. package/src/tigerbeetle/src/vsr/superblock_client_table.zig +258 -0
  81. package/src/tigerbeetle/src/vsr/superblock_free_set.zig +644 -0
  82. package/src/tigerbeetle/src/vsr/superblock_manifest.zig +546 -0
  83. package/src/tigerbeetle/src/vsr.zig +43 -115
@@ -0,0 +1,121 @@
1
+ const std = @import("std");
2
+ const assert = std.debug.assert;
3
+ const ewah = @import("ewah.zig").ewah(usize);
4
+
5
+ const BitSetConfig = struct {
6
+ words: usize,
7
+ run_length_e: usize,
8
+ literals_length_e: usize,
9
+ };
10
+
11
+ const samples = 100;
12
+ const repeats: usize = 100_000;
13
+
14
+ // Explanation of fields:
15
+ // - "n": Number of randomly generate bitsets to test.
16
+ // - "words": The length of the decoded bitset, in u64s.
17
+ // - "run_length_e": The expected length of a run, ignoring truncation due to reaching the end of
18
+ // the bitset.
19
+ // - "literals_length_e": Expected length of a sequence of literals.
20
+ const configs = [_]BitSetConfig{
21
+ // primarily runs
22
+ .{ .words = 640, .run_length_e = 10, .literals_length_e = 10 },
23
+ .{ .words = 640, .run_length_e = 100, .literals_length_e = 10 },
24
+ .{ .words = 640, .run_length_e = 200, .literals_length_e = 10 },
25
+ // primarily literals
26
+ .{ .words = 640, .run_length_e = 1, .literals_length_e = 100 },
27
+ };
28
+
29
+ var prng = std.rand.DefaultPrng.init(42);
30
+
31
+ pub fn main() !void {
32
+ const stdout = std.io.getStdOut().writer();
33
+
34
+ for (configs) |config| {
35
+ var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
36
+ defer arena.deinit();
37
+
38
+ var i: usize = 0;
39
+ var bitsets: [samples][]usize = undefined;
40
+ var bitsets_encoded: [samples][]align(@alignOf(usize)) u8 = undefined;
41
+ var bitsets_decoded: [samples][]usize = undefined;
42
+ var bitset_lengths: [samples]usize = undefined;
43
+ while (i < samples) : (i += 1) {
44
+ bitsets[i] = try make_bitset(&arena.allocator, config);
45
+ bitsets_encoded[i] = try arena.allocator.alignedAlloc(u8, @alignOf(usize), ewah.encode_size_max(bitsets[0].len));
46
+ bitsets_decoded[i] = try arena.allocator.alloc(usize, config.words);
47
+ }
48
+
49
+ // Benchmark encoding.
50
+ const encode_timer = try std.time.Timer.start();
51
+ i = 0;
52
+ while (i < samples) : (i += 1) {
53
+ var j: usize = 0;
54
+ var size: usize = undefined;
55
+ while (j < repeats) : (j += 1) {
56
+ size = ewah.encode(bitsets[i], bitsets_encoded[i]);
57
+ }
58
+ bitset_lengths[i] = size;
59
+ }
60
+ const encode_time = encode_timer.read() / samples / repeats;
61
+
62
+ const decode_timer = try std.time.Timer.start();
63
+ // Benchmark decoding.
64
+ i = 0;
65
+ while (i < samples) : (i += 1) {
66
+ const bitset_encoded = bitsets_encoded[i][0..bitset_lengths[i]];
67
+ var j: usize = 0;
68
+ while (j < repeats) : (j += 1) {
69
+ _ = ewah.decode(bitset_encoded, bitsets_decoded[i]);
70
+ }
71
+ }
72
+ const decode_time = decode_timer.read() / samples / repeats;
73
+
74
+ i = 0;
75
+ while (i < samples) : (i += 1) {
76
+ assert(std.mem.eql(usize, bitsets[i], bitsets_decoded[i]));
77
+ }
78
+
79
+ // Compute compression ratio.
80
+ var total_uncompressed: f64 = 0.0;
81
+ var total_compressed: f64 = 0.0;
82
+ i = 0;
83
+ while (i < samples) : (i += 1) {
84
+ total_uncompressed += @intToFloat(f64, bitsets[i].len * @sizeOf(usize));
85
+ total_compressed += @intToFloat(f64, bitset_lengths[i]);
86
+ }
87
+
88
+ try stdout.print("Words={:_>3} E(Run)={:_>3} E(Literal)={:_>3} EncTime={:_>6}ns DecTime={:_>6}ns Ratio={d:_>6.2}\n", .{
89
+ config.words,
90
+ config.run_length_e,
91
+ config.literals_length_e,
92
+ encode_time,
93
+ decode_time,
94
+ total_uncompressed / total_compressed,
95
+ });
96
+ }
97
+ }
98
+
99
+ fn make_bitset(allocator: *std.mem.Allocator, config: BitSetConfig) ![]usize {
100
+ var words = try allocator.alloc(usize, config.words);
101
+ var w: usize = 0;
102
+ var run: bool = true;
103
+ var literal: usize = 1;
104
+ while (w < words.len) : (w += 1) {
105
+ const start = w;
106
+ const run_length = prng.random.uintLessThan(usize, 2 * config.run_length_e);
107
+ const literals_length = prng.random.uintLessThan(usize, 2 * config.literals_length_e);
108
+ const run_bit = prng.random.boolean();
109
+
110
+ const run_end = std.math.min(w + run_length, words.len);
111
+ while (w < run_end) : (w += 1) {
112
+ words[w] = if (run_bit) std.math.maxInt(usize) else 0;
113
+ }
114
+ const literals_end = std.math.min(w + literals_length, words.len);
115
+ while (w < literals_end) : (w += 1) {
116
+ words[w] = literal;
117
+ literal += 1;
118
+ }
119
+ }
120
+ return words;
121
+ }
@@ -0,0 +1,317 @@
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
+ }
@@ -34,6 +34,10 @@ pub fn FIFO(comptime T: type) type {
34
34
  return self.out;
35
35
  }
36
36
 
37
+ pub fn empty(self: Self) bool {
38
+ return self.peek() == null;
39
+ }
40
+
37
41
  /// Remove an element from the FIFO. Asserts that the element is
38
42
  /// in the FIFO. This operation is O(N), if this is done often you
39
43
  /// probably want a different data structure.
@@ -55,7 +59,7 @@ pub fn FIFO(comptime T: type) type {
55
59
  };
56
60
  }
57
61
 
58
- test "push/pop/peek/remove" {
62
+ test "push/pop/peek/remove/empty" {
59
63
  const testing = @import("std").testing;
60
64
 
61
65
  const Foo = struct { next: ?*@This() = null };
@@ -65,34 +69,45 @@ test "push/pop/peek/remove" {
65
69
  var three: Foo = .{};
66
70
 
67
71
  var fifo: FIFO(Foo) = .{};
72
+ try testing.expect(fifo.empty());
68
73
 
69
74
  fifo.push(&one);
75
+ try testing.expect(!fifo.empty());
70
76
  try testing.expectEqual(@as(?*Foo, &one), fifo.peek());
71
77
 
72
78
  fifo.push(&two);
73
79
  fifo.push(&three);
80
+ try testing.expect(!fifo.empty());
74
81
  try testing.expectEqual(@as(?*Foo, &one), fifo.peek());
75
82
 
76
83
  fifo.remove(&one);
84
+ try testing.expect(!fifo.empty());
77
85
  try testing.expectEqual(@as(?*Foo, &two), fifo.pop());
78
86
  try testing.expectEqual(@as(?*Foo, &three), fifo.pop());
79
87
  try testing.expectEqual(@as(?*Foo, null), fifo.pop());
88
+ try testing.expect(fifo.empty());
80
89
 
81
90
  fifo.push(&one);
82
91
  fifo.push(&two);
83
92
  fifo.push(&three);
84
93
  fifo.remove(&two);
94
+ try testing.expect(!fifo.empty());
85
95
  try testing.expectEqual(@as(?*Foo, &one), fifo.pop());
86
96
  try testing.expectEqual(@as(?*Foo, &three), fifo.pop());
87
97
  try testing.expectEqual(@as(?*Foo, null), fifo.pop());
98
+ try testing.expect(fifo.empty());
88
99
 
89
100
  fifo.push(&one);
90
101
  fifo.push(&two);
91
102
  fifo.push(&three);
92
103
  fifo.remove(&three);
104
+ try testing.expect(!fifo.empty());
93
105
  try testing.expectEqual(@as(?*Foo, &one), fifo.pop());
106
+ try testing.expect(!fifo.empty());
94
107
  try testing.expectEqual(@as(?*Foo, &two), fifo.pop());
108
+ try testing.expect(fifo.empty());
95
109
  try testing.expectEqual(@as(?*Foo, null), fifo.pop());
110
+ try testing.expect(fifo.empty());
96
111
 
97
112
  fifo.push(&one);
98
113
  fifo.push(&two);
@@ -101,4 +116,5 @@ test "push/pop/peek/remove" {
101
116
  try testing.expectEqual(@as(?*Foo, &one), fifo.pop());
102
117
  try testing.expectEqual(@as(?*Foo, &three), fifo.pop());
103
118
  try testing.expectEqual(@as(?*Foo, null), fifo.pop());
119
+ try testing.expect(fifo.empty());
104
120
  }
@@ -83,7 +83,7 @@ pub const IO = struct {
83
83
  const change_events = self.flush_io(&events, &io_pending);
84
84
 
85
85
  // Only call kevent() if we need to submit io events or if we need to wait for completions.
86
- if (change_events > 0 or self.completed.peek() == null) {
86
+ if (change_events > 0 or self.completed.empty()) {
87
87
  // Zero timeouts for kevent() implies a non-blocking poll
88
88
  var ts = std.mem.zeroes(os.timespec);
89
89
 
@@ -91,7 +91,7 @@ pub const IO = struct {
91
91
  // We should never wait indefinitely (timeout_ptr = null for kevent) given:
92
92
  // - tick() is non-blocking (wait_for_completions = false)
93
93
  // - run_for_ns() always submits a timeout
94
- if (change_events == 0 and self.completed.peek() == null) {
94
+ if (change_events == 0 and self.completed.empty()) {
95
95
  if (wait_for_completions) {
96
96
  const timeout_ns = next_timeout orelse @panic("kevent() blocking forever");
97
97
  ts.tv_nsec = @intCast(@TypeOf(ts.tv_nsec), timeout_ns % std.time.ns_per_s);
@@ -430,6 +430,7 @@ pub const IO = struct {
430
430
  IsDir,
431
431
  SystemResources,
432
432
  Unseekable,
433
+ ConnectionTimedOut,
433
434
  } || os.UnexpectedError;
434
435
 
435
436
  pub fn read(
@@ -481,6 +482,7 @@ pub const IO = struct {
481
482
  .NXIO => error.Unseekable,
482
483
  .OVERFLOW => error.Unseekable,
483
484
  .SPIPE => error.Unseekable,
485
+ .TIMEDOUT => error.ConnectionTimedOut,
484
486
  else => |err| os.unexpectedErrno(err),
485
487
  };
486
488
  }
@@ -635,10 +637,12 @@ pub const IO = struct {
635
637
  }
636
638
 
637
639
  /// Opens a directory with read only access.
638
- pub fn open_dir(dir_path: [:0]const u8) !os.fd_t {
639
- return os.openZ(dir_path, os.O.CLOEXEC | os.O.RDONLY, 0);
640
+ pub fn open_dir(dir_path: []const u8) !os.fd_t {
641
+ return os.open(dir_path, os.O.CLOEXEC | os.O.RDONLY, 0);
640
642
  }
641
643
 
644
+ pub const INVALID_FILE: os.fd_t = -1;
645
+
642
646
  /// Opens or creates a journal file:
643
647
  /// - For reading and writing.
644
648
  /// - For Direct I/O (required on darwin).
@@ -648,14 +652,11 @@ pub const IO = struct {
648
652
  /// The caller is responsible for ensuring that the parent directory inode is durable.
649
653
  /// - Verifies that the file size matches the expected file size before returning.
650
654
  pub fn open_file(
651
- self: *IO,
652
655
  dir_fd: os.fd_t,
653
- relative_path: [:0]const u8,
656
+ relative_path: []const u8,
654
657
  size: u64,
655
658
  must_create: bool,
656
659
  ) !os.fd_t {
657
- _ = self;
658
-
659
660
  assert(relative_path.len > 0);
660
661
  assert(size >= config.sector_size);
661
662
  assert(size % config.sector_size == 0);
@@ -685,7 +686,7 @@ pub const IO = struct {
685
686
 
686
687
  // Be careful with openat(2): "If pathname is absolute, then dirfd is ignored." (man page)
687
688
  assert(!std.fs.path.isAbsolute(relative_path));
688
- const fd = try os.openatZ(dir_fd, relative_path, flags, mode);
689
+ const fd = try os.openat(dir_fd, relative_path, flags, mode);
689
690
  // TODO Return a proper error message when the path exists or does not exist (init/start).
690
691
  errdefer os.close(fd);
691
692
 
@@ -720,8 +721,9 @@ pub const IO = struct {
720
721
  // We always do this when opening because we don't know if this was done before crashing.
721
722
  try fs_sync(dir_fd);
722
723
 
724
+ // TODO Document that `size` is now `data_file_size_min` from `main.zig`.
723
725
  const stat = try os.fstat(fd);
724
- if (stat.size != size) @panic("data file inode size was truncated or corrupted");
726
+ if (stat.size < size) @panic("data file inode size was truncated or corrupted");
725
727
 
726
728
  return fd;
727
729
  }