forked from BasedHardware/omi
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathMemoryAtlasSnapshotCache.swift
More file actions
134 lines (121 loc) · 4.57 KB
/
Copy pathMemoryAtlasSnapshotCache.swift
File metadata and controls
134 lines (121 loc) · 4.57 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
import Foundation
/// Memoizes the atlas layout across view reconstructions.
///
/// `CanonicalMemoryAtlasSurface` builds its snapshot in `init`, which SwiftUI
/// re-runs whenever the parent re-renders. That was affordable when placement
/// was a closed-form function of type and rank; relaxing a thousand-entity
/// graph is not, and recomputing it would also make the map visibly reshuffle
/// for no reason the user caused.
///
/// The key is the graph's content, so a snapshot is reused exactly when it
/// would have been recomputed identically, and dropped the moment the graph
/// actually changes.
final class MemoryAtlasSnapshotCache: @unchecked Sendable {
/// Deliberately tiny. In practice one entry serves the whole session; the
/// spares cover a rebuild landing while the previous graph is still on
/// screen, and the compact/full surfaces briefly disagreeing.
private static let capacity = 3
static let shared = MemoryAtlasSnapshotCache()
struct Key: Hashable {
let userName: String?
let nodeCount: Int
let edgeCount: Int
let digest: Int
}
/// Guards every stored property below. The class is `@unchecked Sendable`
/// because this lock — not the compiler — is what makes it safe.
private let lock = NSLock()
private var entries: [(key: Key, snapshot: MemoryAtlasSnapshot)] = []
private var computes = 0
private var reuses = 0
/// Counted rather than timed, so the performance harness asserts the same
/// thing on every Mac.
var computeCount: Int {
lock.lock()
defer { lock.unlock() }
return computes
}
var reuseCount: Int {
lock.lock()
defer { lock.unlock() }
return reuses
}
func snapshot(for graph: KnowledgeGraphResponse, userName: String?) -> MemoryAtlasSnapshot {
let key = Self.makeKey(for: graph, userName: userName)
lock.lock()
if let index = entries.firstIndex(where: { $0.key == key }) {
let hit = entries.remove(at: index)
entries.append(hit)
reuses += 1
lock.unlock()
return hit.snapshot
}
computes += 1
lock.unlock()
// Deliberately computed outside the lock: this is the expensive call, and
// holding a lock across it would serialize unrelated surfaces. A duplicate
// computation under a race is wasteful but not wrong — the function is pure.
let snapshot = MemoryAtlasLayoutEngine.makeSnapshot(graph: graph, userName: userName)
lock.lock()
entries.removeAll { $0.key == key }
entries.append((key, snapshot))
if entries.count > Self.capacity { entries.removeFirst(entries.count - Self.capacity) }
lock.unlock()
return snapshot
}
/// Content digest over everything the snapshot reads.
///
/// The memory ids are hashed individually rather than counted. Co-occurrence
/// is derived from *which* memories two entities share, so a node swapping
/// one memory for another changes the whole layout while leaving every count
/// identical — a digest over counts would serve the previous map back and
/// look like the rebuild had silently done nothing.
///
/// Hashing a few thousand short strings costs well under a millisecond,
/// against tens of milliseconds for the relaxation it protects.
static func makeKey(for graph: KnowledgeGraphResponse, userName: String?) -> Key {
var hasher = Hasher()
for node in graph.nodes {
hasher.combine(node.id)
hasher.combine(node.label)
hasher.combine(node.nodeType)
hasher.combine(node.aliases)
hasher.combine(node.memoryIds)
// The replay timeline is built from these, so they are part of the
// snapshot even though they never move a node.
hasher.combine(node.createdAt)
hasher.combine(node.updatedAt)
}
for node in graph.catalogNodes ?? [] {
hasher.combine(node.id)
hasher.combine(node.label)
hasher.combine(node.nodeType)
hasher.combine(node.aliases)
hasher.combine(node.memoryIds)
hasher.combine(node.createdAt)
hasher.combine(node.updatedAt)
}
for edge in graph.edges {
hasher.combine(edge.id)
hasher.combine(edge.sourceId)
hasher.combine(edge.targetId)
hasher.combine(edge.label)
hasher.combine(edge.memoryIds)
hasher.combine(edge.createdAt)
}
return Key(
userName: userName,
nodeCount: graph.nodes.count,
edgeCount: graph.edges.count,
digest: hasher.finalize())
}
/// Tests share a process, so a stale entry from one case would otherwise
/// satisfy the next.
func resetForTesting() {
lock.lock()
entries.removeAll()
computes = 0
reuses = 0
lock.unlock()
}
}