perf(shared): index git history merge parents lazily (#17469)

This commit is contained in:
Neil
2026-08-30 23:05:16 -07:00
committed by GitHub
parent 435a5e2490
commit 490a7de5fa
2 changed files with 80 additions and 1 deletions
+67
View File
@@ -29,6 +29,19 @@ function item(
}
}
function trackedItem(id: string, parentIds: string[], reads: { count: number }): GitHistoryItem {
const result = item(id, parentIds)
Object.defineProperty(result, 'id', {
configurable: true,
enumerable: true,
get: () => {
reads.count += 1
return id
}
})
return result
}
function branch(name: string, revision: string): GitHistoryItemRef {
return {
id: `refs/heads/${name}`,
@@ -80,6 +93,60 @@ describe('git history graph model', () => {
expect(getGitHistoryMergeParentLaneIndex(viewModels[0]!, 'B')).toBe(1)
})
it('keeps the first matching parent when history contains duplicate ids', () => {
const headRef = branch('head', 'merge')
const firstParentRef = branch('first', 'duplicate')
const laterParentRef = remote('later', 'duplicate')
const colorMap = buildDefaultGitHistoryColorMap({
currentRef: headRef,
remoteRef: firstParentRef,
baseRef: laterParentRef
})
const viewModels = buildGitHistoryViewModels(
[
item('merge', ['base', 'duplicate'], [headRef]),
item('base', []),
item('duplicate', [], [firstParentRef]),
item('duplicate', [], [laterParentRef])
],
colorMap,
headRef
)
expect(viewModels[0]!.outputSwimlanes[1]!.color).toBe(GIT_HISTORY_REMOTE_REF_COLOR)
})
it('avoids building a parent index for linear history', () => {
const count = 64
const reads = { count: 0 }
const historyItems = Array.from({ length: count }, (_, index) =>
trackedItem(`commit-${index}`, index + 1 < count ? [`commit-${index + 1}`] : [], reads)
)
buildGitHistoryViewModels(historyItems)
// A linear graph reads each item while projecting; an eager index would add one read per row.
expect(reads.count).toBeLessThanOrEqual(count * 5)
})
it('indexes merge parents once instead of rescanning a large history', () => {
const mergeCount = 64
const targetId = `target-${mergeCount}`
const reads = { count: 0 }
const historyItems: GitHistoryItem[] = []
for (let index = 0; index < mergeCount; index += 1) {
historyItems.push(trackedItem(`merge-${index}`, [`missing-${index}`, targetId], reads))
// Reset swimlanes between merges so this measures parent lookup, not lane growth.
historyItems.push(trackedItem(`leaf-${index}`, [], reads))
}
historyItems.push(trackedItem(targetId, [], reads))
buildGitHistoryViewModels(historyItems)
// Repeated Array#find scans grow with rows; one index build stays within a linear read budget.
expect(reads.count).toBeLessThan(historyItems.length * 8)
})
it('inserts incoming and outgoing boundary rows at the merge base', () => {
const currentRef = branch('feature', 'A')
const remoteRef = remote('origin/feature', 'R')
+13 -1
View File
@@ -103,6 +103,7 @@ export function buildGitHistoryViewModels(
): GitHistoryItemViewModel[] {
let colorIndex = -1
const viewModels: GitHistoryItemViewModel[] = []
let historyItemsById: Map<string, GitHistoryItem> | undefined
for (const historyItem of historyItems) {
const kind = historyItem.id === currentRef?.revision ? 'HEAD' : 'node'
@@ -131,7 +132,18 @@ export function buildGitHistoryViewModels(
if (index === 0) {
colorIdentifier = getLabelColorIdentifier(historyItem, colorMap)
} else {
const parent = historyItems.find((item) => item.id === historyItem.parentIds[index])
// Side-parent colors need a lookup; defer indexing so linear histories pay no map cost.
if (!historyItemsById) {
historyItemsById = new Map()
for (const candidate of historyItems) {
// Array#find returns the first duplicate, so retain first-wins ordering here.
const candidateId = candidate.id
if (!historyItemsById.has(candidateId)) {
historyItemsById.set(candidateId, candidate)
}
}
}
const parent = historyItemsById.get(historyItem.parentIds[index]!)
colorIdentifier = parent ? getLabelColorIdentifier(parent, colorMap) : undefined
}