diff options
| author | 2026-03-28 17:40:02 +0000 | |
|---|---|---|
| committer | 2026-03-28 17:40:02 +0000 | |
| commit | 06583274fa64ad6677773bf4ca54f69226388203 (patch) | |
| tree | 66354bf972d267bf7ee5135eb7e5b90c31f9fe4d | |
| parent | format/commitgraph/read: Lifetime (diff) | |
| signature | No signature | |
commitquery: Make a reusable engine thingy
| -rw-r--r-- | commitquery/ancestor.go | 4 | ||||
| -rw-r--r-- | commitquery/compare.go | 2 | ||||
| -rw-r--r-- | commitquery/context.go | 18 | ||||
| -rw-r--r-- | commitquery/generation.go | 4 | ||||
| -rw-r--r-- | commitquery/graph_pos.go | 6 | ||||
| -rw-r--r-- | commitquery/load.go | 2 | ||||
| -rw-r--r-- | commitquery/marks.go | 18 | ||||
| -rw-r--r-- | commitquery/merge_bases.go | 6 | ||||
| -rw-r--r-- | commitquery/node.go | 2 | ||||
| -rw-r--r-- | commitquery/oid.go | 10 | ||||
| -rw-r--r-- | commitquery/paint.go | 2 | ||||
| -rw-r--r-- | commitquery/parent.go | 4 | ||||
| -rw-r--r-- | commitquery/populate.go | 2 | ||||
| -rw-r--r-- | commitquery/priority_queue.go | 6 | ||||
| -rw-r--r-- | commitquery/queries.go | 23 | ||||
| -rw-r--r-- | commitquery/queries_acquire.go | 16 | ||||
| -rw-r--r-- | commitquery/queries_ancestor.go | 14 | ||||
| -rw-r--r-- | commitquery/queries_mergebase.go | 21 | ||||
| -rw-r--r-- | commitquery/queries_new.go | 25 | ||||
| -rw-r--r-- | commitquery/queries_release.go | 14 | ||||
| -rw-r--r-- | commitquery/reduce.go | 6 |
21 files changed, 161 insertions, 44 deletions
diff --git a/commitquery/ancestor.go b/commitquery/ancestor.go index 4f182a95..6a43babb 100644 --- a/commitquery/ancestor.go +++ b/commitquery/ancestor.go @@ -6,7 +6,7 @@ import objectid "codeberg.org/lindenii/furgit/object/id" // commit parent edges. // // Both inputs are peeled through annotated tags before commit traversal. -func (query *Query) IsAncestor(ancestor, descendant objectid.ObjectID) (bool, error) { +func (query *query) IsAncestor(ancestor, descendant objectid.ObjectID) (bool, error) { ancestorIdx, err := query.resolveCommitish(ancestor) if err != nil { return false, err @@ -20,7 +20,7 @@ func (query *Query) IsAncestor(ancestor, descendant objectid.ObjectID) (bool, er return query.isAncestor(ancestorIdx, descendantIdx) } -func (query *Query) isAncestor(ancestor, descendant nodeIndex) (bool, error) { +func (query *query) isAncestor(ancestor, descendant nodeIndex) (bool, error) { if ancestor == descendant { return true, nil } diff --git a/commitquery/compare.go b/commitquery/compare.go index 21dc5eb9..7ae984dc 100644 --- a/commitquery/compare.go +++ b/commitquery/compare.go @@ -3,7 +3,7 @@ package commitquery import objectid "codeberg.org/lindenii/furgit/object/id" // Compare compares two internal nodes using merge-base queue ordering. -func (query *Query) compare(left, right nodeIndex) int { +func (query *query) compare(left, right nodeIndex) int { leftGeneration := query.effectiveGeneration(left) rightGeneration := query.effectiveGeneration(right) diff --git a/commitquery/context.go b/commitquery/context.go index 3e9ceee8..92705182 100644 --- a/commitquery/context.go +++ b/commitquery/context.go @@ -7,9 +7,7 @@ import ( objectstore "codeberg.org/lindenii/furgit/object/store" ) -// Query owns the mutable node arena for commit-domain queries over one object -// store. -type Query struct { +type query struct { store objectstore.ReadingStore graph *commitgraphread.Reader @@ -22,13 +20,19 @@ type Query struct { touched []nodeIndex } -// New builds one reusable commit query arena over one object store and optional -// commit-graph reader. -func New(store objectstore.ReadingStore, graph *commitgraphread.Reader) *Query { - return &Query{ +func newQuery(store objectstore.ReadingStore, graph *commitgraphread.Reader) *query { + return &query{ store: store, graph: graph, byOID: make(map[objectid.ObjectID]nodeIndex), byGraphPos: make(map[commitgraphread.Position]nodeIndex), } } + +func (query *query) resetForReuse() { + for _, idx := range query.touched { + query.nodes[idx].marks = 0 + } + + query.touched = query.touched[:0] +} diff --git a/commitquery/generation.go b/commitquery/generation.go index 39ddd2e9..935da104 100644 --- a/commitquery/generation.go +++ b/commitquery/generation.go @@ -7,7 +7,7 @@ import ( ) // EffectiveGeneration returns one node's generation value. -func (query *Query) effectiveGeneration(idx nodeIndex) uint64 { +func (query *query) effectiveGeneration(idx nodeIndex) uint64 { if !query.nodes[idx].hasGeneration { return generationInfinity } @@ -19,7 +19,7 @@ const ( generationInfinity = uint64(math.MaxUint64) ) -func compareByGeneration(query *Query) func(nodeIndex, nodeIndex) int { +func compareByGeneration(query *query) func(nodeIndex, nodeIndex) int { return func(left, right nodeIndex) int { leftGeneration := query.effectiveGeneration(left) rightGeneration := query.effectiveGeneration(right) diff --git a/commitquery/graph_pos.go b/commitquery/graph_pos.go index b5bc02d8..6b5118b0 100644 --- a/commitquery/graph_pos.go +++ b/commitquery/graph_pos.go @@ -3,7 +3,7 @@ package commitquery import commitgraphread "codeberg.org/lindenii/furgit/format/commitgraph/read" // resolveGraphPos resolves one commit-graph position to one internal query node. -func (query *Query) resolveGraphPos(pos commitgraphread.Position) (nodeIndex, error) { +func (query *query) resolveGraphPos(pos commitgraphread.Position) (nodeIndex, error) { idx, ok := query.byGraphPos[pos] if ok { err := query.ensureLoaded(idx) @@ -40,13 +40,13 @@ func (query *Query) resolveGraphPos(pos commitgraphread.Position) (nodeIndex, er } // loadByGraphPos populates one node from a commit-graph position. -func (query *Query) loadByGraphPos(idx nodeIndex) error { +func (query *query) loadByGraphPos(idx nodeIndex) error { pos := query.nodes[idx].graphPos return query.loadCommitAtGraphPos(idx, pos) } -func (query *Query) loadCommitAtGraphPos(idx nodeIndex, pos commitgraphread.Position) error { +func (query *query) loadCommitAtGraphPos(idx nodeIndex, pos commitgraphread.Position) error { commit, err := query.graph.CommitAt(pos) if err != nil { return err diff --git a/commitquery/load.go b/commitquery/load.go index 58ed7bf3..be39c7d9 100644 --- a/commitquery/load.go +++ b/commitquery/load.go @@ -1,7 +1,7 @@ package commitquery // ensureLoaded completes one node's metadata load if it has not been loaded yet. -func (query *Query) ensureLoaded(idx nodeIndex) error { +func (query *query) ensureLoaded(idx nodeIndex) error { if query.nodes[idx].loaded { return nil } diff --git a/commitquery/marks.go b/commitquery/marks.go index 401acb15..43ca9f44 100644 --- a/commitquery/marks.go +++ b/commitquery/marks.go @@ -1,22 +1,22 @@ package commitquery // Marks returns the mark bits of one internal node. -func (query *Query) marks(idx nodeIndex) markBits { +func (query *query) marks(idx nodeIndex) markBits { return query.nodes[idx].marks } // HasAnyMarks reports whether one internal node has any requested bit. -func (query *Query) hasAnyMarks(idx nodeIndex, bits markBits) bool { +func (query *query) hasAnyMarks(idx nodeIndex, bits markBits) bool { return query.nodes[idx].marks&bits != 0 } // HasAllMarks reports whether one internal node already has all requested bits. -func (query *Query) hasAllMarks(idx nodeIndex, bits markBits) bool { +func (query *query) hasAllMarks(idx nodeIndex, bits markBits) bool { return query.nodes[idx].marks&bits == bits } // SetMarks ORs one set of mark bits into one internal node. -func (query *Query) setMarks(idx nodeIndex, bits markBits) { +func (query *query) setMarks(idx nodeIndex, bits markBits) { newBits := bits &^ query.nodes[idx].marks if newBits == 0 { return @@ -27,7 +27,7 @@ func (query *Query) setMarks(idx nodeIndex, bits markBits) { } // ClearMarks removes one set of mark bits from one internal node. -func (query *Query) clearMarks(idx nodeIndex, bits markBits) { +func (query *query) clearMarks(idx nodeIndex, bits markBits) { if query.nodes[idx].marks&bits == 0 { return } @@ -37,7 +37,7 @@ func (query *Query) clearMarks(idx nodeIndex, bits markBits) { } // BeginMarkPhase starts one tracked mark-mutation phase. -func (query *Query) beginMarkPhase() { +func (query *query) beginMarkPhase() { for _, idx := range query.touched { query.nodes[idx].marks = 0 } @@ -55,13 +55,13 @@ func (query *Query) beginMarkPhase() { // ClearTouchedMarks clears the provided bits from all nodes touched in the // current mark phase. -func (query *Query) clearTouchedMarks(bits markBits) { +func (query *query) clearTouchedMarks(bits markBits) { for _, idx := range query.touched { query.nodes[idx].marks &^= bits } } -func (query *Query) trackTouched(idx nodeIndex) { +func (query *query) trackTouched(idx nodeIndex) { if query.nodes[idx].touchedPhase == query.markPhase { return } @@ -70,7 +70,7 @@ func (query *Query) trackTouched(idx nodeIndex) { query.touched = append(query.touched, idx) } -func (query *Query) collectMarkedResults() []nodeIndex { +func (query *query) collectMarkedResults() []nodeIndex { out := make([]nodeIndex, 0, 4) for _, idx := range query.touched { diff --git a/commitquery/merge_bases.go b/commitquery/merge_bases.go index 0e410709..281906f3 100644 --- a/commitquery/merge_bases.go +++ b/commitquery/merge_bases.go @@ -9,7 +9,7 @@ import ( // MergeBases reports all merge bases in Git's merge-base --all order. // // Both inputs are peeled through annotated tags before commit traversal. -func (query *Query) MergeBases(left, right objectid.ObjectID) ([]objectid.ObjectID, error) { +func (query *query) MergeBases(left, right objectid.ObjectID) ([]objectid.ObjectID, error) { leftIdx, err := query.resolveCommitish(left) if err != nil { return nil, err @@ -45,7 +45,7 @@ func (query *Query) MergeBases(left, right objectid.ObjectID) ([]objectid.Object } // MergeBase reports one merge base between left and right, if any. -func (query *Query) MergeBase(left, right objectid.ObjectID) (objectid.ObjectID, bool, error) { +func (query *query) MergeBase(left, right objectid.ObjectID) (objectid.ObjectID, bool, error) { bases, err := query.MergeBases(left, right) if err != nil { return objectid.ObjectID{}, false, err @@ -58,7 +58,7 @@ func (query *Query) MergeBase(left, right objectid.ObjectID) (objectid.ObjectID, return bases[0], true, nil } -func (query *Query) mergeBases(left, right nodeIndex) ([]nodeIndex, error) { +func (query *query) mergeBases(left, right nodeIndex) ([]nodeIndex, error) { if left == right { return []nodeIndex{left}, nil } diff --git a/commitquery/node.go b/commitquery/node.go index c0f25c55..d5f1d3cd 100644 --- a/commitquery/node.go +++ b/commitquery/node.go @@ -28,7 +28,7 @@ type node struct { } // newNode allocates one empty internal node. -func (query *Query) newNode(id objectid.ObjectID) nodeIndex { +func (query *query) newNode(id objectid.ObjectID) nodeIndex { count := len(query.nodes) idx := nodeIndex(count) diff --git a/commitquery/oid.go b/commitquery/oid.go index 22d41a82..fd524dfb 100644 --- a/commitquery/oid.go +++ b/commitquery/oid.go @@ -12,15 +12,15 @@ import ( objecttype "codeberg.org/lindenii/furgit/object/type" ) -func (query *Query) id(idx nodeIndex) objectid.ObjectID { +func (query *query) id(idx nodeIndex) objectid.ObjectID { return query.nodes[idx].id } -func (query *Query) commitTime(idx nodeIndex) int64 { +func (query *query) commitTime(idx nodeIndex) int64 { return query.nodes[idx].commitTime } -func (query *Query) resolveOID(id objectid.ObjectID) (nodeIndex, error) { +func (query *query) resolveOID(id objectid.ObjectID) (nodeIndex, error) { idx, ok := query.byOID[id] if ok { err := query.ensureLoaded(idx) @@ -44,7 +44,7 @@ func (query *Query) resolveOID(id objectid.ObjectID) (nodeIndex, error) { return idx, nil } -func (query *Query) resolveCommitish(id objectid.ObjectID) (nodeIndex, error) { +func (query *query) resolveCommitish(id objectid.ObjectID) (nodeIndex, error) { commitID, err := peel.ToCommit(query.store, id) if err != nil { return 0, err @@ -54,7 +54,7 @@ func (query *Query) resolveCommitish(id objectid.ObjectID) (nodeIndex, error) { } // loadByOID populates one node from an object ID. -func (query *Query) loadByOID(idx nodeIndex) error { +func (query *query) loadByOID(idx nodeIndex) error { id := query.nodes[idx].id if query.graph != nil { diff --git a/commitquery/paint.go b/commitquery/paint.go index 90b38b10..6bb1f489 100644 --- a/commitquery/paint.go +++ b/commitquery/paint.go @@ -1,6 +1,6 @@ package commitquery -func (query *Query) paintDownToCommon(left nodeIndex, rights []nodeIndex, minGeneration uint64) error { +func (query *query) paintDownToCommon(left nodeIndex, rights []nodeIndex, minGeneration uint64) error { query.beginMarkPhase() query.setMarks(left, markLeft) diff --git a/commitquery/parent.go b/commitquery/parent.go index e7703f77..fe0f4fba 100644 --- a/commitquery/parent.go +++ b/commitquery/parent.go @@ -13,12 +13,12 @@ type parentRef struct { } // Parents returns resolved parent node indices for one internal node. -func (query *Query) parents(idx nodeIndex) []nodeIndex { +func (query *query) parents(idx nodeIndex) []nodeIndex { return query.nodes[idx].parents } // resolveParent resolves one parent descriptor to one internal node. -func (query *Query) resolveParent(parent parentRef) (nodeIndex, error) { +func (query *query) resolveParent(parent parentRef) (nodeIndex, error) { if parent.HasGraphPos { return query.resolveGraphPos(parent.GraphPos) } diff --git a/commitquery/populate.go b/commitquery/populate.go index cc71955f..26fb5629 100644 --- a/commitquery/populate.go +++ b/commitquery/populate.go @@ -3,7 +3,7 @@ package commitquery import "fmt" // populateNode fills one node's metadata and resolves its parents. -func (query *Query) populateNode(idx nodeIndex, commit commitData) error { +func (query *query) populateNode(idx nodeIndex, commit commitData) error { if query.nodes[idx].loaded { if query.nodes[idx].id != commit.ID { return fmt.Errorf("commitquery: node identity mismatch: have %s, got %s", query.nodes[idx].id, commit.ID) diff --git a/commitquery/priority_queue.go b/commitquery/priority_queue.go index 0d948c64..8651ea0b 100644 --- a/commitquery/priority_queue.go +++ b/commitquery/priority_queue.go @@ -4,12 +4,12 @@ import "container/heap" // priorityQueue orders internal nodes using one query context's comparator. type priorityQueue struct { - query *Query + query *query items []nodeIndex } // newPriorityQueue builds one empty priority queue over one query context. -func newPriorityQueue(query *Query) *priorityQueue { +func newPriorityQueue(query *query) *priorityQueue { queue := &priorityQueue{query: query} heap.Init(queue) @@ -67,7 +67,7 @@ func (queue *priorityQueue) PopNode() nodeIndex { return idx } -func (query *Query) queueHasNonStale(queue *priorityQueue) bool { +func (query *query) queueHasNonStale(queue *priorityQueue) bool { // TODO for _, idx := range queue.items { if !query.hasAnyMarks(idx, markStale) { diff --git a/commitquery/queries.go b/commitquery/queries.go new file mode 100644 index 00000000..dec8606f --- /dev/null +++ b/commitquery/queries.go @@ -0,0 +1,23 @@ +package commitquery + +import ( + "sync" + + commitgraphread "codeberg.org/lindenii/furgit/format/commitgraph/read" + objectstore "codeberg.org/lindenii/furgit/object/store" +) + +// Queries provides commit-domain queries over one object store +// and optional commit-graph reader. +// +// Queries reuses internal mutable query workers across operations. +// +// Labels: MT-Safe. +type Queries struct { + store objectstore.ReadingStore + graph *commitgraphread.Reader + + mu sync.Mutex + idle []*query + maxIdle int +} diff --git a/commitquery/queries_acquire.go b/commitquery/queries_acquire.go new file mode 100644 index 00000000..ac7b765c --- /dev/null +++ b/commitquery/queries_acquire.go @@ -0,0 +1,16 @@ +package commitquery + +func (queries *Queries) acquire() *query { + queries.mu.Lock() + defer queries.mu.Unlock() + + count := len(queries.idle) + if count == 0 { + return newQuery(queries.store, queries.graph) + } + + q := queries.idle[count-1] + queries.idle = queries.idle[:count-1] + + return q +} diff --git a/commitquery/queries_ancestor.go b/commitquery/queries_ancestor.go new file mode 100644 index 00000000..e2c955c6 --- /dev/null +++ b/commitquery/queries_ancestor.go @@ -0,0 +1,14 @@ +package commitquery + +import objectid "codeberg.org/lindenii/furgit/object/id" + +// IsAncestor reports whether ancestor is reachable from descendant through +// commit parent edges. +// +// Both inputs are peeled through annotated tags before commit traversal. +func (queries *Queries) IsAncestor(ancestor, descendant objectid.ObjectID) (bool, error) { + query := queries.acquire() + defer queries.release(query) + + return query.IsAncestor(ancestor, descendant) +} diff --git a/commitquery/queries_mergebase.go b/commitquery/queries_mergebase.go new file mode 100644 index 00000000..ff0aafb9 --- /dev/null +++ b/commitquery/queries_mergebase.go @@ -0,0 +1,21 @@ +package commitquery + +import objectid "codeberg.org/lindenii/furgit/object/id" + +// MergeBases reports all merge bases in Git's merge-base --all order. +// +// Both inputs are peeled through annotated tags before commit traversal. +func (queries *Queries) MergeBases(left, right objectid.ObjectID) ([]objectid.ObjectID, error) { + query := queries.acquire() + defer queries.release(query) + + return query.MergeBases(left, right) +} + +// MergeBase reports one merge base between left and right, if any. +func (queries *Queries) MergeBase(left, right objectid.ObjectID) (objectid.ObjectID, bool, error) { + query := queries.acquire() + defer queries.release(query) + + return query.MergeBase(left, right) +} diff --git a/commitquery/queries_new.go b/commitquery/queries_new.go new file mode 100644 index 00000000..b43084b4 --- /dev/null +++ b/commitquery/queries_new.go @@ -0,0 +1,25 @@ +package commitquery + +import ( + "runtime" + + commitgraphread "codeberg.org/lindenii/furgit/format/commitgraph/read" + objectstore "codeberg.org/lindenii/furgit/object/store" +) + +// New builds one concurrent-safe commit query service over one object store +// and optional commit-graph reader. +// +// Labels: Deps-Borrowed. +func New(store objectstore.ReadingStore, graph *commitgraphread.Reader) *Queries { + maxIdle := runtime.GOMAXPROCS(0) + if maxIdle < 1 { + maxIdle = 1 + } + + return &Queries{ + store: store, + graph: graph, + maxIdle: maxIdle, + } +} diff --git a/commitquery/queries_release.go b/commitquery/queries_release.go new file mode 100644 index 00000000..791b2f03 --- /dev/null +++ b/commitquery/queries_release.go @@ -0,0 +1,14 @@ +package commitquery + +func (queries *Queries) release(q *query) { + q.resetForReuse() + + queries.mu.Lock() + defer queries.mu.Unlock() + + if len(queries.idle) >= queries.maxIdle { + return + } + + queries.idle = append(queries.idle, q) +} diff --git a/commitquery/reduce.go b/commitquery/reduce.go index ac85d619..f8a86f69 100644 --- a/commitquery/reduce.go +++ b/commitquery/reduce.go @@ -5,7 +5,7 @@ import ( ) // removeRedundant removes redundant merge-base candidates. -func removeRedundant(query *Query, candidates []nodeIndex) ([]nodeIndex, error) { +func removeRedundant(query *query, candidates []nodeIndex) ([]nodeIndex, error) { for _, idx := range candidates { if query.effectiveGeneration(idx) != generationInfinity { return removeRedundantWithGen(query, candidates), nil @@ -15,7 +15,7 @@ func removeRedundant(query *Query, candidates []nodeIndex) ([]nodeIndex, error) return removeRedundantNoGen(query, candidates) } -func removeRedundantNoGen(query *Query, candidates []nodeIndex) ([]nodeIndex, error) { +func removeRedundantNoGen(query *query, candidates []nodeIndex) ([]nodeIndex, error) { redundant := make([]bool, len(candidates)) work := make([]nodeIndex, 0, len(candidates)-1) filledIndex := make([]int, 0, len(candidates)-1) @@ -72,7 +72,7 @@ func removeRedundantNoGen(query *Query, candidates []nodeIndex) ([]nodeIndex, er return out, nil } -func removeRedundantWithGen(query *Query, candidates []nodeIndex) []nodeIndex { +func removeRedundantWithGen(query *query, candidates []nodeIndex) []nodeIndex { sorted := append([]nodeIndex(nil), candidates...) slices.SortFunc(sorted, compareByGeneration(query)) |
