also looking at this
feat(video): add bounded packet history, delayed FIFO and owned snapshots
#916
3 files
+229
-0
| @@ -0,0 +1,191 @@ | ||
| 1 | //! Owned encoded-packet history. One caller serializes all access. Fixed byte | |
| 2 | //! arena and entry array; push never allocates, blocks, or keeps borrowed input. | |
| 3 | //! Eviction drops dependent packets until the oldest retained packet is a key. | |
| 4 | const std = ; | |
| 5 | pub const Entry = struct { offset: usize, length: usize, timestamp: u64, key: bool }; | |
| 6 | pub const Ring = struct { | |
| 7 | allocator: std.mem.Allocator, | |
| 8 | bytes: []u8, | |
| 9 | entries: []Entry, | |
| 10 | duration: u64, | |
| 11 | head: usize = 0, | |
| 12 | count: usize = 0, | |
| 13 | used: usize = 0, | |
| 14 | write_pos: usize = 0, | |
| 15 | last: ?u64 = null, | |
| 16 | evicted: u64 = 0, | |
| 17 | skipped: u64 = 0, | |
| 18 | pub fn init(allocator: std.mem.Allocator, byte_capacity: usize, frame_capacity: usize, duration: u64) !Ring { | |
| 19 | if (byte_capacity == 0 or frame_capacity == 0 or frame_capacity > std.math.maxInt(usize) / 2 or duration == 0) return error.InvalidConfiguration; | |
| 20 | const bytes = try allocator.alloc(u8, byte_capacity); | |
| 21 | errdefer allocator.free(bytes); | |
| 22 | const entries = try allocator.alloc(Entry, frame_capacity); | |
| 23 | return .{ .allocator = allocator, .bytes = bytes, .entries = entries, .duration = duration }; | |
| 24 | } | |
| 25 | pub fn deinit(s: *Ring) void { | |
| 26 | s.allocator.free(s.bytes); | |
| 27 | s.allocator.free(s.entries); | |
| 28 | } | |
| 29 | fn drop(s: *Ring) void { | |
| 30 | s.used -= s.entries[s.head].length; | |
| 31 | s.head = (s.head + 1) % s.entries.len; | |
| 32 | s.count -= 1; | |
| 33 | s.evicted += 1; | |
| 34 | } | |
| 35 | pub fn entry(s: *const Ring, index: usize) ?Entry { | |
| 36 | if (index >= s.count) return null; | |
| 37 | return s.entries[(s.head + index) % s.entries.len]; | |
| 38 | } | |
| 39 | /// False means waiting for a key after eviction. Limits are upper bounds; | |
| 40 | /// history can be empty when a GOP is larger than the retention limits. | |
| 41 | /// Oversized/invalid input is rejected before modifying retained history. | |
| 42 | pub fn push(s: *Ring, packet: []const u8, timestamp: u64, key: bool) !bool { | |
| 43 | if (packet.len == 0 or packet.len > s.bytes.len) return error.PacketTooLarge; | |
| 44 | if (s.last) |last| if (timestamp <= last) return error.InvalidTimestamp; | |
| 45 | while (s.count > 0 and (s.count == s.entries.len or packet.len > s.bytes.len - s.used or timestamp - s.entry(0).?.timestamp > s.duration)) s.drop(); | |
| 46 | while (s.count > 0 and !s.entry(0).?.key) s.drop(); | |
| 47 | s.last = timestamp; | |
| 48 | if (s.count == 0 and !key) { | |
| 49 | s.skipped += 1; | |
| 50 | return false; | |
| 51 | } | |
| 52 | s.store(packet, timestamp, key); | |
| 53 | return true; | |
| 54 | } | |
| 55 | fn store(s: *Ring, packet: []const u8, timestamp: u64, key: bool) void { | |
| 56 | s.last = timestamp; | |
| 57 | const first = @min(packet.len, s.bytes.len - s.write_pos); | |
| 58 | @memcpy(s.bytes[s.write_pos..][0..first], packet[0..first]); | |
| 59 | @memcpy(s.bytes[0 .. packet.len - first], packet[first..]); | |
| 60 | s.entries[(s.head + s.count) % s.entries.len] = .{ .offset = s.write_pos, .length = packet.len, .timestamp = timestamp, .key = key }; | |
| 61 | s.write_pos = if (packet.len - first != 0) packet.len - first else if (s.write_pos + first == s.bytes.len) 0 else s.write_pos + first; | |
| 62 | s.count += 1; | |
| 63 | s.used += packet.len; | |
| 64 | } | |
| 65 | /// FIFO mode: no implicit eviction or GOP filtering. A consumer must pop | |
| 66 | /// mature entries before enqueueing; overflow leaves all state unchanged. | |
| 67 | pub fn pushQueued(s: *Ring, packet: []const u8, timestamp: u64, key: bool) !void { | |
| 68 | if (packet.len == 0 or packet.len > s.bytes.len) return error.PacketTooLarge; | |
| 69 | if (s.last) |last| if (timestamp <= last) return error.InvalidTimestamp; | |
| 70 | if (s.count == s.entries.len or packet.len > s.bytes.len - s.used) return error.BufferFull; | |
| 71 | s.store(packet, timestamp, key); | |
| 72 | } | |
| 73 | pub fn pop(s: *Ring) void { | |
| 74 | if (s.count != 0) s.drop(); | |
| 75 | } | |
| 76 | /// Null clock flushes EOF; subtraction avoids overflowing timestamp+delay. | |
| 77 | pub fn ready(s: *const Ring, now: ?u64, delay: u64) bool { | |
| 78 | const e = s.entry(0) orelse return false; | |
| 79 | const clock = now orelse return true; | |
| 80 | return clock >= e.timestamp and clock - e.timestamp >= delay; | |
| 81 | } | |
| 82 | /// Own only the selected suffix. Borrowed source data never escapes. | |
| 83 | pub fn cloneFrom(s: *const Ring, a: std.mem.Allocator, start: usize) !Ring { | |
| 84 | if (start >= s.count) return error.InvalidIndex; | |
| 85 | var bytes: usize = 0; | |
| 86 | for (start..s.count) |i| bytes = try std.math.add(usize, bytes, s.entry(i).?.length); | |
| 87 | var result = try Ring.init(a, bytes, s.count - start, s.duration); | |
| 88 | errdefer result.deinit(); | |
| 89 | // Destination is contiguous in this clone; copy straight into its arena. | |
| 90 | for (start..s.count) |i| { | |
| 91 | const e = s.entry(i).?; | |
| 92 | _ = try s.copy(i, result.bytes[result.used..]); | |
| 93 | result.entries[result.count] = .{ .offset = result.used, .length = e.length, .timestamp = e.timestamp, .key = e.key }; | |
| 94 | result.count += 1; | |
| 95 | result.used += e.length; | |
| 96 | result.last = e.timestamp; | |
| 97 | } | |
| 98 | result.write_pos = 0; | |
| 99 | return result; | |
| 100 | } | |
| 101 | /// Explicit owned snapshot copy; data and metadata views otherwise expire | |
| 102 | /// on the next mutation. Returns the active destination slice. | |
| 103 | pub fn copy(s: *const Ring, index: usize, out: []u8) ![]const u8 { | |
| 104 | const e = s.entry(index) orelse return error.InvalidIndex; | |
| 105 | if (out.len < e.length) return error.BufferTooSmall; | |
| 106 | const first = @min(e.length, s.bytes.len - e.offset); | |
| 107 | @memcpy(out[0..first], s.bytes[e.offset..][0..first]); | |
| 108 | @memcpy(out[first..e.length], s.bytes[0 .. e.length - first]); | |
| 109 | return out[0..e.length]; | |
| 110 | } | |
| 111 | }; | |
| 112 | test "byte wrap and GOP eviction keep only independently decodable history" { | |
| 113 | var ring = try Ring.init(std.testing.allocator, 10, 4, 100); | |
| 114 | defer ring.deinit(); | |
| 115 | try std.testing.expect(try ring.push("aaaa", 0, true)); | |
| 116 | try std.testing.expect(try ring.push("bb", 1, false)); | |
| 117 | try std.testing.expect(try ring.push("ccc", 2, true)); | |
| 118 | try std.testing.expect(try ring.push("dddd", 3, false)); | |
| 119 | try std.testing.expectEqual(@as(usize, 2), ring.count); | |
| 120 | try std.testing.expectEqual(@as(usize, 7), ring.used); | |
| 121 | var out: [10]u8 = undefined; | |
| 122 | try std.testing.expectEqualStrings("ccc", try ring.copy(0, &out)); | |
| 123 | try std.testing.expectEqualStrings("dddd", try ring.copy(1, &out)); | |
| 124 | try std.testing.expect(!try ring.push("12345678", 4, false)); | |
| 125 | try std.testing.expectEqual(@as(usize, 0), ring.count); | |
| 126 | try std.testing.expect(try ring.push("key", 5, true)); | |
| 127 | try std.testing.expectEqualStrings("key", try ring.copy(0, &out)); | |
| 128 | } | |
| 129 | test "frame and time limits; rejected inputs preserve existing payload" { | |
| 130 | var ring = try Ring.init(std.testing.allocator, 10, 2, 3); | |
| 131 | defer ring.deinit(); | |
| 132 | try std.testing.expect(!try ring.push("p", 0, false)); | |
| 133 | try std.testing.expect(try ring.push("a", 1, true)); | |
| 134 | try std.testing.expect(try ring.push("b", 2, false)); | |
| 135 | try std.testing.expect(!try ring.push("c", 3, false)); | |
| 136 | try std.testing.expect(try ring.push("d", 4, true)); | |
| 137 | try std.testing.expectError(error.PacketTooLarge, ring.push("01234567890", 5, true)); | |
| 138 | try std.testing.expectError(error.InvalidTimestamp, ring.push("x", 4, true)); | |
| 139 | var out: [1]u8 = undefined; | |
| 140 | try std.testing.expectEqualStrings("d", try ring.copy(0, &out)); | |
| 141 | try std.testing.expect(!try ring.push("e", 8, false)); | |
| 142 | try std.testing.expectEqual(@as(usize, 0), ring.count); | |
| 143 | try std.testing.expectError(error.InvalidIndex, ring.copy(0, &out)); | |
| 144 | } | |
| 145 | test "initialization rejects zero limits and frees partial allocations" { | |
| 146 | try std.testing.expectError(error.InvalidConfiguration, Ring.init(std.testing.allocator, 0, 1, 1)); | |
| 147 | const Check = struct { | |
| 148 | fn run(a: std.mem.Allocator) !void { | |
| 149 | var r = try Ring.init(a, 10, 2, 3); | |
| 150 | defer r.deinit(); | |
| 151 | } | |
| 152 | }; | |
| 153 | try std.testing.checkAllAllocationFailures(std.testing.allocator, Check.run, .{}); | |
| 154 | } | |
| 155 | ||
| 156 | test "FIFO delay preserves dependencies and rejects overflow atomically" { | |
| 157 | var r = try Ring.init(std.testing.allocator, 8, 3, 1); | |
| 158 | defer r.deinit(); | |
| 159 | try r.pushQueued("key", 10, true); | |
| 160 | try r.pushQueued("p", 20, false); | |
| 161 | try std.testing.expect(!r.ready(19, 10)); | |
| 162 | try std.testing.expect(r.ready(20, 10)); | |
| 163 | r.pop(); | |
| 164 | try std.testing.expect(!r.entry(0).?.key); | |
| 165 | try r.pushQueued("next", 30, false); | |
| 166 | try std.testing.expectError(error.BufferFull, r.pushQueued("full", 40, true)); | |
| 167 | try std.testing.expectEqual(@as(u64, 30), r.last.?); | |
| 168 | try std.testing.expectEqual(@as(usize, 2), r.count); | |
| 169 | try std.testing.expect(r.ready(null, std.math.maxInt(u64))); | |
| 170 | try std.testing.expect(!r.ready(0, 0)); | |
| 171 | var clone = try r.cloneFrom(std.testing.allocator, 0); | |
| 172 | defer clone.deinit(); | |
| 173 | r.pop(); | |
| 174 | r.pop(); | |
| 175 | var out: [8]u8 = undefined; | |
| 176 | try std.testing.expectEqualStrings("next", try clone.copy(1, &out)); | |
| 177 | } | |
| 178 | test "suffix cloning frees partial allocations and preserves source" { | |
| 179 | const Check = struct { | |
| 180 | fn run(a: std.mem.Allocator) !void { | |
| 181 | var r = try Ring.init(a, 9, 3, 10); | |
| 182 | defer r.deinit(); | |
| 183 | _ = try r.push("first", 0, true); | |
| 184 | _ = try r.push("next", 1, true); | |
| 185 | var frozen = try r.cloneFrom(a, 1); | |
| 186 | defer frozen.deinit(); | |
| 187 | try std.testing.expectEqual(@as(usize, 2), r.count); | |
| 188 | } | |
| 189 | }; | |
| 190 | try std.testing.checkAllAllocationFailures(std.testing.allocator, Check.run, .{}); | |
| 191 | } | |