Artifact 056fe33c0a91d56fc50f61da5de1b05e8d6bd38efcbbbddd8ba7a6070fc22b95:
- File
src/log_tree_store.zig
— part of check-in
[5e0155427e]
at
2026-08-28 01:56:16
on branch trunk
— import-fossil --submanifest: build each check-in's tree from its parent's with the sub-manifest editor
Every check-in went through artifact.buildTree from scratch, so a wide tree cost its whole file set per commit against a few changed slices actually written. The converter now applies each check-in's change set over its primary parent's root with TreeEditor — the commit verb's builder — keeps a parent cache released as children consume it, interns the retained cards, runs the pair proof through the run-scoped flatten memo, and parses each source manifest once. The Git importer's LogTreeStore moves to a shared module. Output is unchanged: every check-in hash matches the from-scratch build on libfossil and on a 30k-file synthetic history. Refs zit#167. (user: dmestas, size: 5366) [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.putArtifact(raw, bytes, null) 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 }); } 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 {}; } } }; 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; }