Artifact 3915583b18358853fad592b3ee1fbbfc994836b02704874d870c2ec86f1694f3:
- File
src/log_tree_store.zig
— part of check-in
[598d040de5]
at
2026-08-29 12:44:50
on branch trunk
— logstore: putArtifactPreferring takes a known base before searching (zit#169)
The converter's hints are the source's own chains and each manifest's parent, so one delta usually settles it; the best-of-N window is the fallback. Conversion is now faster than the old convert-then-pack. (user: dmestas, size: 6176) [annotate] [blame] [check-ins using]
//! A `TreeStore` over a `LogStore`: the seam through which the sub-manifest //! editor reads and writes tree nodes in an append-only log. Shared by the //! Git importer and the fossil converter (#167) — both build every check-in's //! tree incrementally from its parent's, and both want the same write-through //! plaintext memo over the nodes that consecutive commits share. const std = @import("std"); const artifact = @import("artifact.zig"); const logstore = @import("logstore.zig"); const editor = @import("submanifest_editor.zig"); pub const LogTreeStore = struct { gpa: std.mem.Allocator, log: *logstore.LogStore, /// #63/L1 — plaintext memo over the staging log's tree nodes. Every /// commit's tree build AND flatten re-load the same content-addressed, /// immutable nodes the previous commit just loaded; without a memo each /// load is a frame read, an inflate, and a delta-chain assemble (the /// profile put those, plus their worker-thread readv time, at over half /// the import). `memoize_undeltaed` because staging nodes are routinely /// raw/zlib top-level frames, exactly the class the delta-base-only /// default declines to retain. cache: logstore.PlaintextCache = .{ .memoize_undeltaed = true }, pub fn deinitCache(self: *LogTreeStore) void { self.cache.deinit(self.gpa); } pub fn treeStore(self: *LogTreeStore) editor.TreeStore { return .{ .ctx = self, .load_fn = load, .put_fn = put, .put_batch_fn = putBatch }; } fn load(context: *anyopaque, allocator: std.mem.Allocator, name: []const u8) editor.StoreError!editor.LoadedBytes { const self: *LogTreeStore = @ptrCast(@alignCast(context)); var digest: [32]u8 = undefined; const raw = rawHashName(name, &digest) orelse return error.MissingNode; const rid = self.log.lookup(raw) orelse return error.MissingNode; // Through the memo, on the STORE's allocator: the cache's resident // copies must outlive any per-call arena the editor hands in, and // `LoadedBytes` frees by its recorded allocator, so returning // `self.gpa`-owned bytes keeps the contract whichever allocator the // caller offered. _ = allocator; const bytes = self.log.contentCached(self.gpa, rid, &self.cache) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, else => return error.StorageFailure, }; return .{ .bytes = bytes orelse return error.MissingNode, .allocator = self.gpa }; } fn put(context: *anyopaque, name: []const u8, bytes: []const u8) editor.StoreError!void { const self: *LogTreeStore = @ptrCast(@alignCast(context)); // Algorithm-agnostic: `verify` picks the digest from the name width. if (!artifact.verify(name, bytes)) return error.StorageFailure; var digest: [32]u8 = undefined; const raw = rawHashName(name, &digest) orelse return error.StorageFailure; const rid = self.log.putArtifactPreferring(raw, bytes, parentHint(self, bytes)) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, else => return error.StorageFailure, }; // #65 — write-through: the very next flatten reads this node straight // back; priming turns that read-after-write miss (a delta-chain // reassembly) into a memcpy. self.cache.put(self.gpa, self.log.offsetOf(rid), bytes) catch {}; } fn putBatch(context: *anyopaque, nodes: []const editor.StoreBatchNode) editor.StoreError!void { const self: *LogTreeStore = @ptrCast(@alignCast(context)); var digests: std.ArrayList([32]u8) = .empty; defer digests.deinit(self.gpa); var items: std.ArrayList(logstore.LogStore.ArtifactBatchItem) = .empty; defer items.deinit(self.gpa); digests.ensureTotalCapacity(self.gpa, nodes.len) catch return error.OutOfMemory; items.ensureTotalCapacity(self.gpa, nodes.len) catch return error.OutOfMemory; for (nodes) |node| { // Algorithm-agnostic: `verify` picks the digest from the name width. if (!artifact.verify(node.name, node.bytes)) return error.StorageFailure; var digest: [32]u8 = undefined; const raw = rawHashName(node.name, &digest) orelse return error.StorageFailure; digests.appendAssumeCapacity(digest); items.appendAssumeCapacity(.{ .hash = digests.items[digests.items.len - 1][0..raw.len], .bytes = node.bytes, .base = parentHint(self, node.bytes) }); } self.log.putArtifactBatch(items.items) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, else => return error.StorageFailure, }; // #65 — write-through for the batch path too; see `put`. for (nodes) |node| { var digest: [32]u8 = undefined; const raw = rawHashName(node.name, &digest) orelse continue; const rid = self.log.lookup(raw) orelse continue; self.cache.put(self.gpa, self.log.offsetOf(rid), node.bytes) catch {}; } } }; /// zit#169: a manifest's primary parent, already in the log, is the delta base /// that pays best — the same hint `zit commit` gives. Anything without a P /// card (a sub-manifest, a blob) falls back to the recent window. fn parentHint(self: *LogTreeStore, bytes: []const u8) ?logstore.Rid { var lines = std.mem.splitScalar(u8, bytes, '\n'); while (lines.next()) |line| { if (line.len < 2 or line[0] != 'P' or line[1] != ' ') continue; var toks = std.mem.tokenizeScalar(u8, line[2..], ' '); const first = toks.next() orelse return null; var digest: [32]u8 = undefined; const raw = rawHashName(first, &digest) orelse return null; return self.log.lookup(raw); } return null; } pub fn rawHashName(name: []const u8, buffer: *[32]u8) ?[]const u8 { if (name.len != 40 and name.len != 64) return null; const raw = buffer[0 .. name.len / 2]; _ = std.fmt.hexToBytes(raw, name) catch return null; return raw; }