Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Performance

- **Coherent Git object session reuse** - successful loose object and tree
writes preserve typed `cat-file` and `mktree` processes. Scoped bulk writes
preserve `cat-file` but still retire `mktree` after checkpoint because Git's
quick tree-entry validation does not refresh packs created after the
session's object-database snapshot.

## [6.5.2] — 2026-07-19

### Added
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
---
title: 'PERF-0053 - Git Object Session Coherence'
cycle: '0053'
task_id: 'git-object-session-coherence'
legend: 'PERF'
release_home: 'v6.5.3'
issue: 'https://github.com/git-stunts/git-cas/issues/94'
goalpost_issue: 'https://github.com/git-stunts/git-cas/issues/94'
tracker_source: 'github'
status: 'active'
base_commit: 'fb3d3c2b620bae11d52e8ecc7a78a7ea07f27e24'
owners:
- '@git-stunts'
sponsors:
human: 'James'
agent: 'Codex'
blocking_issues: []
supersedes: []
superseded_by: null
created: '2026-07-19'
updated: '2026-07-19'
---

# PERF-0053 - Git Object Session Coherence

## Linked Tracker

- Issue: [#94 - Preserve Git object sessions across coherent writes](https://github.com/git-stunts/git-cas/issues/94)
- Milestone: [`v6.5.3`](https://github.com/git-stunts/git-cas/milestone/13)
- Parent design: [PERF-0052](../0052-persistent-git-object-sessions/persistent-git-object-sessions.md)

## Design Type

This design is primarily:

- [x] Runtime/API
- [x] Storage/substrate
- [x] Migration/release
- [ ] CLI/operator
- [ ] Docs/public guidance
- [ ] TUI/visual surface
- [x] Test/tooling

## Decision Summary

`GitPersistenceAdapter` will preserve a live `cat-file --batch-command`
session across every successful immutable object write. It will preserve a live
`mktree --batch -z` session across one-shot loose blob writes and loose tree
writes, but retire `mktree` after a scoped bulk `fast-import` write because
that operation may create a pack that an already prepared `mktree` process
cannot discover. The scoped `fast-import` process still checkpoints and closes
before any OID escapes. No public API, object identity, retention policy, or
lifecycle contract changes.

## Hill

By the end of this cycle, repeated git-warp materialization writes through one
CAS adapter reuse coherent Git reader and tree-writer processes instead of
restarting them after every loose object. Real-Git tests prove both sides of the
boundary: safe reuse after loose writes and mandatory `mktree` retirement
after a pack-producing bulk write.

## Current Truth

- A one-shot blob write retires both persistent object protocols after
`hash-object -w` succeeds.
[cite: `src/infrastructure/adapters/GitPersistenceAdapter.js#93-103@fb3d3c2b620bae11d52e8ecc7a78a7ea07f27e24`]
- A bulk blob write retires both persistent object protocols after checkpoint,
then separately retires the scoped `fast-import` process.
[cite: `src/infrastructure/adapters/GitPersistenceAdapter.js#114-151@fb3d3c2b620bae11d52e8ecc7a78a7ea07f27e24`]
- Both typed and fallback tree writes retire the persistent reader.
[cite: `src/infrastructure/adapters/GitPersistenceAdapter.js#160-178@fb3d3c2b620bae11d52e8ecc7a78a7ea07f27e24`]
- The session pool already supports independent protocol retirement,
invalidation, generation barriers, idle bounds, and explicit close.
[cite: `src/infrastructure/adapters/GitObjectSessionPool.js#39-117@fb3d3c2b620bae11d52e8ecc7a78a7ea07f27e24`]
- PERF-0052 correctly keeps individual blobs on one-shot `hash-object` and
scoped batches on checkpointed `fast-import`.
[cite: `docs/design/0052-persistent-git-object-sessions/persistent-git-object-sessions.md#220-240@fb3d3c2b620bae11d52e8ecc7a78a7ea07f27e24`]
- The existing real-Git gate proves scoped bulk writes reduce process count and
that one-shot writes recreate objects after external pruning.
[cite: `test/integration/bundle-reference-performance.test.js#193-247@fb3d3c2b620bae11d52e8ecc7a78a7ea07f27e24`]

## Problem

The implementation treats every successful object-database mutation as if it
invalidated every typed Git process. That is stricter than the protocols'
actual lookup behavior and collapses persistent sessions back into
process-per-object execution on alternating blob, tree, and read workloads.

A 128-node retained git-warp materialization on `@git-stunts/git-cas@6.5.2`
opened 558 Git children: 140 `hash-object`, 82 `mktree`, and 80
`cat-file`. A temporary audited candidate opened 401 children: the same 140
one-shot blob writers, one `mktree`, and four `cat-file` processes. The
candidate therefore removes 157 unnecessary process launches without changing
the one-shot blob correctness boundary.

## Audited Git Coherence Matrix

The raw protocol matrix was exercised with Git 2.50.1 and Git 2.39.5 in both
SHA-1 and SHA-256 repositories.

| Existing session | Subsequent write | Preserve? | Reason |
| -------------------------- | ------------------------------- | --------- | ------------------------------------------------- |
| `cat-file --batch-command` | loose `hash-object -w` | Yes | Direct lookup observes the new loose object. |
| `cat-file --batch-command` | checkpointed `fast-import` pack | Yes | Missing lookup refreshes packed object state. |
| `cat-file --batch-command` | loose `mktree` output | Yes | Direct lookup observes the new loose tree. |
| `mktree --batch -z` | loose `hash-object -w` | Yes | Loose lookup follows the prepared pack lookup. |
| `mktree --batch -z` | checkpointed `fast-import` pack | No | Quick lookup does not refresh prepared pack data. |

The behavior follows upstream Git's lookup contract. `cat-file` uses
`odb_read_object_info_extended()` without `OBJECT_INFO_QUICK`, allowing a
second read to refresh source state. `mktree` explicitly requests
`OBJECT_INFO_QUICK`, which suppresses that second read.

- [Upstream object lookup retry](https://github.com/git/git/blob/41365c2a9ba347870b80881c0d67454edd22fd49/odb.c#L550-L610)
- [Upstream packed-source refresh](https://github.com/git/git/blob/41365c2a9ba347870b80881c0d67454edd22fd49/odb/source-packed.c#L35-L58)
- [Upstream mktree quick lookup](https://github.com/git/git/blob/41365c2a9ba347870b80881c0d67454edd22fd49/builtin/mktree.c#L118-L143)

## Scope

This cycle includes exactly one runtime correction:

- remove `cat-file` retirement after successful blob, bulk blob, and tree writes
- remove `mktree` retirement after successful one-shot loose blob writes
- retain `mktree` retirement after successful scoped bulk writes
- retain scoped `fast-import` checkpoint and retirement
- replace obsolete blanket-retirement tests with the exact coherence matrix
- add real-Git regression coverage and measured witness evidence

## Non-Goals

This cycle does not include:

- changing the public API or TypeScript declarations
- reusing `fast-import` across individual writes or separate batches
- changing individual `writeBlob()` from one-shot `hash-object`
- adding workspace, RootSet, retention, TTL, LRU, or cache policy
- coalescing staging-workspace generations
- changing Git ref, commit, or history operations
- changing session idle timeout, retry, invalidation, or explicit close behavior
- claiming that every remaining git-warp Git process is necessary

## Runtime Contract

The externally observable contract is unchanged. Internally, successful writes
apply this retirement policy:

| Operation | `cat-file` | `mktree` | `fast-import` |
| ---------------------------- | ---------- | ---------- | ------------- |
| One-shot loose blob write | Preserve | Preserve | Not opened |
| Scoped bulk blob write | Preserve | Retire | Retire |
| Typed or fallback tree write | Preserve | Preserve | Not opened |
| Failure in that protocol | Invalidate | Invalidate | Invalidate |
| Idle timeout | Retire | Retire | Retire |
| Adapter close | Close | Close | Close |

## Determinism, Retention, and Lifecycle

Session preservation changes process topology only. Content bytes, tree entry
ordering, Git OIDs, CAS handles, refs, retention witnesses, and publication
state remain unchanged. Existing protocol failures still invalidate their own
generation. Idle retirement still bounds abandoned reusable processes.
`ContentAddressableStore.close()` remains the deterministic resource boundary.

## Risks

- Removing `mktree` retirement after bulk writes would be a correctness bug
when `fast-import` leaves a pack above `fastimport.unpackLimit`.
- A timing-only benchmark could hide a process-topology regression in scheduler
noise. Tests must assert protocol opening counts directly.
- Small bulk batches may unpack into loose objects, so the real-Git regression
must exceed `fastimport.unpackLimit` to prove the unsafe packed case.
- External repository mutation can still affect a long-lived process. This
cycle governs writes performed through this adapter; it does not promise a
transactional snapshot over arbitrary concurrent Git maintenance.

## Alternatives Considered

### Preserve every session after every write

Rejected. A pack-primed `mktree --batch -z` process cannot validate an object
introduced in a later pack because its quick lookup does not refresh prepared
pack state.

### Retire every session after every write

Rejected. This is the current behavior and needlessly discards coherent
`cat-file` sessions and `mktree` sessions after loose writes.

### Raise the idle timeout

Rejected. A 30-second temporary timeout produced the same 558-process
git-warp trace because explicit retirement, not idle expiry, caused churn.

### Add a workspace batch API

Rejected for this cycle. Trie branches depend on child handles and workspace
retention has separate safety obligations. Combining that change would violate
the one-change release boundary.

## Tests To Write First

1. A one-shot blob write preserves already opened `cat-file` and `mktree`.
2. Typed and fallback tree writes preserve already opened `cat-file`.
3. A scoped bulk write preserves `cat-file`, retires `mktree`, and retires
`fast-import`.
4. A real-Git session reads a new loose blob and tree without reopening.
5. A real-Git reader discovers a later pack without reopening.
6. A real-Git pack-primed `mktree` is replaced after a pack-producing batch
and validates the new object through the replacement.
7. Existing prune/rewrite, failure, idle, close, SHA-1, and SHA-256 gates pass.

## Acceptance Criteria

- [x] Every checkbox in issue #94 is proven.
- [x] No public export or declaration changes.
- [x] The unit retirement matrix is explicit and complete.
- [x] Real-Git tests exceed `fastimport.unpackLimit`.
- [x] Full unit, integration, platform, lint, and release gates pass.
- [x] Witness evidence distinguishes deterministic process counts from local timing.
- [x] The complete diff passes self-review against `origin/main`.

## Implementation Slices

1. Commit design, failing matrix tests, and raw coherence evidence.
2. Apply the narrow adapter retirement correction.
3. Run full gates and record the witness.
4. Review, publish one PR, and release `v6.5.3`.

## Follow-On Debt

Git-warp still performs one-shot blob writes and frequent workspace RootSet,
ref, and commit updates. Their necessity and any safe coalescing contract remain
separate work. This cycle neither solves nor obscures that remaining cost.
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
{
"schema": "git-cas.git-object-session-coherence-measurement/v1",
"generatedAt": "2026-07-19T16:16:43Z",
"implementationCommit": "9208871802a596cbc508be725353532110d40198",
"environment": {
"node": "v26.0.0",
"git": "git version 2.50.1 (Apple Git-155)",
"platform": "darwin",
"architecture": "arm64"
},
"protocolMatrix": {
"gitVersions": ["2.50.1", "2.39.5"],
"objectFormats": ["sha1", "sha256"],
"results": {
"catFileAfterLooseBlob": "preserved-and-readable",
"catFileAfterLooseTree": "preserved-and-readable",
"catFileAfterPackedBulk": "preserved-and-readable",
"mktreeAfterLooseBlob": "preserved-and-writable",
"mktreeAfterPackedBulk": "must-retire"
}
},
"consumerExploratory": {
"repository": "git-stunts/git-warp",
"commit": "0d0cefa9f8ade44e58c0a83b65f85ce557d3e9dd",
"fixture": "temporary 128-node retained materialization diagnostic",
"samples": 3,
"baseline": {
"gitCas": "6.5.2",
"gitChildProcesses": 558,
"majorProcessCounts": {
"hash-object": 140,
"rev-parse": 87,
"mktree": 82,
"cat-file": 80,
"update-ref": 43,
"symbolic-ref": 43,
"commit-tree": 39,
"rev-list": 37
},
"medianMaterializationSeconds": 14.24,
"medianProcessCpuSeconds": 15.74,
"medianWallSeconds": 17.39
},
"candidate": {
"implementationCommit": "9208871802a596cbc508be725353532110d40198",
"gitChildProcesses": 401,
"majorProcessCounts": {
"hash-object": 140,
"rev-parse": 87,
"mktree": 1,
"cat-file": 4,
"update-ref": 43,
"symbolic-ref": 43,
"commit-tree": 39,
"rev-list": 37
},
"medianMaterializationSeconds": 12.72,
"medianProcessCpuSeconds": 13.83,
"medianWallSeconds": 16.93
},
"deltaPercent": {
"gitChildProcesses": -28.1,
"cat-file": -95.0,
"mktree": -98.8,
"materializationTime": -10.7,
"processCpu": -12.1,
"wall": -2.6
},
"interpretation": {
"processCounts": "deterministic for this fixture",
"timings": "local three-run medians; not a cross-platform guarantee",
"reproducibility": "exploratory consumer fixture was temporary; repository integration counts are the durable gate"
}
},
"verification": {
"releaseVerify": {
"stepsPassed": 14,
"stepsTotal": 14,
"observedTests": 6826
},
"nodeIntegrationAfterFinalAssertionPass": {
"filesPassed": 13,
"testsPassed": 198
},
"platformBats": {
"parallelCommand": "pnpm test:platforms",
"parallelResult": "host-harness-failure-before-tests",
"parallelCause": "Homebrew moreutils parallel is not GNU parallel",
"serialCommand": "bats test/platform/runtimes.bats",
"serialTestsPassed": 3,
"serialTestsTotal": 3
},
"publicApiSemverImpact": "none"
}
}
Loading
Loading