Repository navigation
fix: prevent repeated session lineage resolution - #3
Conversation
…out deep copies Cold-scan batch publishes re-resolved lineage over the full corpus every 250ms, pinning cores for the whole 'Preparing session index' phase: key() re-standardized every file URL four-plus times per pass, heuristic pools copied and sorted whole SessionSummary values, and the roots sort hashed long string keys inside its comparator (then deep-copied materialized subtrees). Sessions are now keyed exactly once per resolve, pools and sorts carry small value tuples, and the snapshot store memoizes the forest per rebuild generation so keystrokes and same-source refreshes never resolve again. Live corpus: startup burst settles to 0% idle instead of oscillating 20-80% indefinitely.
📝 WalkthroughWalkthroughLineage resolution now returns stable-key indexes with cancellation support. Session and evidence revisions drive navigation snapshot cache invalidation and shared resolution tasks, while projections derive session filtering and display state from lineage references. ChangesLineage and navigation snapshot updates
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant SessionStore
participant AppServices
participant NavigationSnapshotStore
participant SessionLineage
SessionStore->>AppServices: publish session and evidence revisions
AppServices->>NavigationSnapshotStore: rebuild with revision inputs
NavigationSnapshotStore->>SessionLineage: start or share cancellable resolution
SessionLineage-->>NavigationSnapshotStore: return indexed resolution
NavigationSnapshotStore-->>AppServices: project updated navigation snapshot
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
Sources/TrifolaKit/SessionLineage.swift (1)
710-745: 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick winSibling materialization still hashes long keys per comparator call — this is the PR's own flagged unmet criterion.
childKeys.sortedcallsnodes[$0]!andnodes[$1]!inside the comparator (Lines 715-716), i.e. two dictionary probes (each hashing the full standardized-path stable key) per comparison, for every parent's sibling list. TheRootOrderpattern introduced just below (Lines 750-767) solves exactly this by precomputing(key, activity, stable)once per node before sorting — the same fix should be applied here.♻️ Apply the RootOrder pattern to child materialization
func materialize(_ nodeKey: String, depth: Int) throws -> LineageNode { try checkForCancellation() let value = nodes[nodeKey]! let candidate = value.candidate - let childKeys = (childrenByParent[nodeKey] ?? []).sorted { - let lhs = nodes[$0]!.session - let rhs = nodes[$1]!.session - let ld = lhs.startedAt ?? lhs.lastActivity ?? .distantPast - let rd = rhs.startedAt ?? rhs.lastActivity ?? .distantPast - return ld == rd ? lhs.stableKey < rhs.stableKey : ld < rd - } + let childOrder = (childrenByParent[nodeKey] ?? []).map { childKey -> ChildOrder in + let session = nodes[childKey]!.session + return ChildOrder( + key: childKey, + activity: session.startedAt ?? session.lastActivity ?? .distantPast, + stable: session.stableKey) + }.sorted { + $0.activity == $1.activity ? $0.stable < $1.stable : $0.activity < $1.activity + } var children: [LineageNode] = [] - children.reserveCapacity(childKeys.count) - for childKey in childKeys { - children.append(try materialize(childKey, depth: depth + 1)) + children.reserveCapacity(childOrder.count) + for child in childOrder { + children.append(try materialize(child.key, depth: depth + 1)) }Add a small
ChildOrderstruct (key/activity/stable) alongsideRootOrder, mirroring its shape.This confirms the exact gap called out in the PR description itself.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Sources/TrifolaKit/SessionLineage.swift` around lines 710 - 745, Optimize sibling ordering in materialize by introducing a ChildOrder value containing each child’s key, activity date, and stable key, populated before sorting. Sort the precomputed ChildOrder collection without repeated nodes lookups, then iterate its keys when recursively materializing children; define ChildOrder alongside RootOrder and preserve the existing ordering behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@Sources/TrifolaKit/SessionLineage.swift`:
- Around line 710-745: Optimize sibling ordering in materialize by introducing a
ChildOrder value containing each child’s key, activity date, and stable key,
populated before sorting. Sort the precomputed ChildOrder collection without
repeated nodes lookups, then iterate its keys when recursively materializing
children; define ChildOrder alongside RootOrder and preserve the existing
ordering behavior.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 243a60f4-0b90-490e-b10c-efb3334e5007
📒 Files selected for processing (6)
Sources/Trifola/AppServices.swiftSources/Trifola/NavigationSnapshotStore.swiftSources/TrifolaKit/SessionLineage.swiftSources/TrifolaKit/Stores.swiftTests/TrifolaKitTests/NavigationSnapshotShapeTests.swiftTests/TrifolaKitTests/SessionLineageTests.swift
Intent
Fix the session-lineage startup performance fire: the app sat in 'Preparing session index' for 30+ seconds because lineage resolution re-ran over the entire corpus on every incremental scan publish and every keystroke; resolution must run once per corpus rebuild (memoized), key each session exactly once, and sort without deep-copying subtrees or hashing long keys per comparison — with no behavior change to the resolved forest
What Changed
Risk Assessment
🚨 High: The prior three blockers are fixed, but one explicit performance acceptance criterion remains unmet, so the change should not merge without correction or explicit human approval.
Testing
Diff inspection, focused lineage and memo-contract tests, and the production release build all passed; a seven-run 10,000-session comparison improved median resolution from 377.941 ms to 206.355 ms (45.4%) while producing byte-identical forests, and the production snapshot-store trace showed exactly one resolution for nine same-source publishes plus rapid typing, one key per session, memo reuse for later keystrokes, and exactly one new resolution after a real corpus revision. No screenshot was captured because this is a transient performance-only change with no visual or copy change; the readiness timing and resolution-count trace directly demonstrate removal of the visible stall.
Evidence: Complete lineage validation report
Evidence: Base canonical 10,000-node forest
/var/folders/nc/j5wdfhws2q56jzdcj_jkdvww0000gn/T/no-mistakes-evidence/01KXYHH6CTBV43ZATGCMSVB5S7/target-forest.tsv)Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
Sources/Trifola/NavigationSnapshotStore.swift:392- Required intent says “resolution must run once per corpus rebuild (memoized),” but the newif let memo, memo.source == sourcecache usessourceGeneration, which increments on everyrebuild(). Search-index progress callsrebuild()every 250 ms, and a normal refresh calls it twice, so unchanged lineage inputs repeatedly miss the memo and rerun the full resolver. Key the memo by a lineage-specific corpus/evidence revision.Sources/Trifola/NavigationSnapshotStore.swift:378- The same required once-per-rebuild invariant is violated while resolution is in flight. Each filter update captures a nil completed memo, cancels only the outer task, and launches anotherTask.detached; canceled tasks cannot publish their completed forest into the memo. Startup keystrokes can therefore create overlapping full-corpus resolves. Memoize and share an in-flight lineage task, canceling it only when lineage inputs change.Sources/Trifola/NavigationSnapshotStore.swift:394- Required intent says “key each session exactly once,” but even the new memo-hit path immediately callsprojectLineage, which rebuildssummaryByKeyusingSessionLineage.keyfor every summary at line 621. Thus every keystroke and same-corpus projection still re-standardizes all session paths. Carry the keyed lookup alongside the memoized forest instead of recreating it per projection.🔧 Fix: Fix lineage memoization and keyed projection reuse
1 error still open:
Sources/TrifolaKit/SessionLineage.swift:714- Required intent says sorting must occur “without … hashing long keys per comparison,” but the changed child materialization still sorts sibling keys usingnodes[$0]andnodes[$1]. Those dictionary probes hash full stable-key strings for every comparison. Precompute lightweight(key, date, stable)child-order records once, sort those records, then recurse by key.✅ **Test** - passed
✅ No issues found.
git diff --find-renames --find-copies --unified=80 1d1c0bac2bf8252641a908fb96d6613afcafd189..a5abb62c68ce1801d01b21f5015023298ae8af80swift test --filter SessionLineageTestsswift test --filter NavigationSnapshotShapeTestsswift build -c release --product TrifolaRelease-build and run the sameLineageProbe 10000 <canonical-path>harness seven times against base and targetcmp -s <base-forest.tsv> <target-forest.tsv>andshasum -a 256 <base-forest.tsv> <target-forest.tsv>Instrumented production-store check:CFFIXED_USER_HOME=<evidence-home> .build/release/Trifola --evidence-lineage-store --evidence-search-db <evidence-db>git status --shortafter removing transient probe sources, builds, databases, and sandbox data✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.
Summary by CodeRabbit
Bug Fixes
Tests