Skip to content

Commit 8d338f3

Browse files
author
ComputelessComputer
committed
Reduce activity event memory usage
Use fixed-size content hashes, compact oversized legacy hashes on open, and stop redundant retained-history warmup during refresh.
1 parent cd04f1c commit 8d338f3

6 files changed

Lines changed: 185 additions & 16 deletions

File tree

Sources/OpenbirdApp/AppModel.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -489,7 +489,6 @@ final class AppModel: ObservableObject {
489489

490490
let dayRange = Calendar.current.dayRange(for: requestedDay)
491491
let day = OpenbirdDateFormatting.dayString(for: requestedDay)
492-
await store.prepareRecentActivityEventsInBackground(dayCount: max(settings.retentionDays, 1))
493492
dayLoadStatus = Self.makeDayLoadStatus(
494493
step: 2,
495494
totalSteps: 5,

Sources/OpenbirdKit/Capture/WindowSnapshot.swift

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,12 @@ public struct WindowSnapshot: Sendable {
2828
}
2929

3030
public var fingerprint: String {
31-
[bundleId, windowTitle, url ?? "", visibleText]
32-
.joined(separator: "|")
33-
.stableHash
31+
ActivityEventContentHash.make(
32+
bundleId: bundleId,
33+
windowTitle: windowTitle,
34+
url: url,
35+
visibleText: visibleText
36+
)
3437
}
3538

3639
public func asEvent(startedAt: Date, excluded: Bool) -> ActivityEvent {
@@ -48,9 +51,3 @@ public struct WindowSnapshot: Sendable {
4851
)
4952
}
5053
}
51-
52-
private extension String {
53-
var stableHash: String {
54-
Data(utf8).base64EncodedString()
55-
}
56-
}

Sources/OpenbirdKit/Storage/SQLiteDatabase.swift

Lines changed: 64 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ public final class SQLiteDatabase: @unchecked Sendable {
5151
try execute("PRAGMA journal_mode=WAL;")
5252
try execute("PRAGMA foreign_keys=ON;")
5353
try migrate()
54+
try compactLegacyActivityEventContentHashes()
5455
try seedDefaultsIfNeeded()
5556
}
5657

@@ -292,7 +293,8 @@ public final class SQLiteDatabase: @unchecked Sendable {
292293
}
293294

294295
public func saveActivityEvent(_ event: ActivityEvent) throws -> ActivityEvent {
295-
try withImmediateTransaction {
296+
let normalizedEvent = normalizedActivityEvent(event)
297+
return try withImmediateTransaction {
296298
let overlappingDuplicates = try query(
297299
"""
298300
SELECT * FROM activity_events
@@ -303,14 +305,14 @@ public final class SQLiteDatabase: @unchecked Sendable {
303305
ORDER BY started_at ASC, ended_at DESC;
304306
""",
305307
bindings: [
306-
.text(event.contentHash),
307-
.integer(event.isExcluded ? 1 : 0),
308-
.double(event.endedAt.timeIntervalSince1970),
309-
.double(event.startedAt.timeIntervalSince1970),
308+
.text(normalizedEvent.contentHash),
309+
.integer(normalizedEvent.isExcluded ? 1 : 0),
310+
.double(normalizedEvent.endedAt.timeIntervalSince1970),
311+
.double(normalizedEvent.startedAt.timeIntervalSince1970),
310312
]
311313
).map(ActivityEvent.init(row:))
312314

313-
let mergedEvent = mergeActivityEvent(event, with: overlappingDuplicates)
315+
let mergedEvent = mergeActivityEvent(normalizedEvent, with: overlappingDuplicates)
314316
for duplicate in overlappingDuplicates where duplicate.id != mergedEvent.id {
315317
try execute("DELETE FROM activity_events_fts WHERE id = ?;", bindings: [.text(duplicate.id)])
316318
try execute("DELETE FROM activity_events WHERE id = ?;", bindings: [.text(duplicate.id)])
@@ -723,6 +725,45 @@ public final class SQLiteDatabase: @unchecked Sendable {
723725
}
724726
}
725727

728+
private func compactLegacyActivityEventContentHashes(batchSize: Int = 500) throws {
729+
while true {
730+
let rows = try query(
731+
"""
732+
SELECT id, bundle_id, window_title, url, visible_text
733+
FROM activity_events
734+
WHERE LENGTH(content_hash) > ?
735+
LIMIT ?;
736+
""",
737+
bindings: [
738+
.integer(Int64(ActivityEventContentHash.oversizedLegacyThreshold)),
739+
.integer(Int64(batchSize)),
740+
]
741+
)
742+
743+
guard rows.isEmpty == false else {
744+
return
745+
}
746+
747+
try withImmediateTransaction {
748+
for row in rows {
749+
let compactHash = ActivityEventContentHash.make(
750+
bundleId: row.stringValue(for: "bundle_id"),
751+
windowTitle: row.stringValue(for: "window_title"),
752+
url: row.optionalStringValue(for: "url"),
753+
visibleText: row.stringValue(for: "visible_text")
754+
)
755+
try execute(
756+
"UPDATE activity_events SET content_hash = ? WHERE id = ?;",
757+
bindings: [
758+
.text(compactHash),
759+
.text(row.stringValue(for: "id")),
760+
]
761+
)
762+
}
763+
}
764+
}
765+
}
766+
726767
private func seedDefaultsIfNeeded() throws {
727768
let countRows = try query("SELECT COUNT(*) AS value FROM provider_configs;")
728769
let count = countRows.first?.intValue(for: "value") ?? 0
@@ -781,6 +822,23 @@ public final class SQLiteDatabase: @unchecked Sendable {
781822
return value
782823
}
783824

825+
private func normalizedActivityEvent(_ event: ActivityEvent) -> ActivityEvent {
826+
let compactHash = ActivityEventContentHash.compactIfNeeded(
827+
event.contentHash,
828+
bundleId: event.bundleId,
829+
windowTitle: event.windowTitle,
830+
url: event.url,
831+
visibleText: event.visibleText
832+
)
833+
guard compactHash != event.contentHash else {
834+
return event
835+
}
836+
837+
var normalized = event
838+
normalized.contentHash = compactHash
839+
return normalized
840+
}
841+
784842
private func mergeActivityEvent(_ event: ActivityEvent, with duplicates: [ActivityEvent]) -> ActivityEvent {
785843
guard let canonical = duplicates.first else {
786844
return event
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import CryptoKit
2+
import Foundation
3+
4+
enum ActivityEventContentHash {
5+
static let compactLength = 64
6+
static let oversizedLegacyThreshold = 128
7+
8+
static func make(
9+
bundleId: String,
10+
windowTitle: String,
11+
url: String?,
12+
visibleText: String
13+
) -> String {
14+
let payload = [bundleId, windowTitle, url ?? "", visibleText]
15+
.joined(separator: "|")
16+
let digest = SHA256.hash(data: Data(payload.utf8))
17+
return digest.map { String(format: "%02x", $0) }.joined()
18+
}
19+
20+
static func compactIfNeeded(
21+
_ hash: String,
22+
bundleId: String,
23+
windowTitle: String,
24+
url: String?,
25+
visibleText: String
26+
) -> String {
27+
guard hash.count > oversizedLegacyThreshold else {
28+
return hash
29+
}
30+
31+
return make(
32+
bundleId: bundleId,
33+
windowTitle: windowTitle,
34+
url: url,
35+
visibleText: visibleText
36+
)
37+
}
38+
}

Tests/OpenbirdKitTests/OpenbirdStoreTests.swift

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,62 @@ struct OpenbirdStoreTests {
425425
#expect(try preparedActivityUpdatedAt(at: databaseURL, day: previousDay) != nil)
426426
}
427427

428+
@Test func databaseCompactsOversizedLegacyContentHashesOnOpen() throws {
429+
let databaseURL = FileManager.default.temporaryDirectory
430+
.appendingPathComponent(UUID().uuidString)
431+
.appendingPathExtension("sqlite")
432+
let startedAt = Date(timeIntervalSince1970: 1_720_000_000)
433+
let endedAt = startedAt.addingTimeInterval(45)
434+
let bundleId = "com.apple.Safari"
435+
let windowTitle = "Openbird"
436+
let visibleText = String(repeating: "Reviewing large activity payloads. ", count: 80)
437+
let legacyHash = Data([bundleId, windowTitle, "", visibleText].joined(separator: "|").utf8).base64EncodedString()
438+
439+
do {
440+
let database = try SQLiteDatabase(url: databaseURL)
441+
try database.execute(
442+
"""
443+
INSERT INTO activity_events
444+
(id, started_at, ended_at, bundle_id, app_name, window_title, url, visible_text, source, content_hash, is_excluded)
445+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
446+
""",
447+
bindings: [
448+
.text("legacy-event"),
449+
.double(startedAt.timeIntervalSince1970),
450+
.double(endedAt.timeIntervalSince1970),
451+
.text(bundleId),
452+
.text("Safari"),
453+
.text(windowTitle),
454+
.null,
455+
.text(visibleText),
456+
.text("accessibility"),
457+
.text(legacyHash),
458+
.integer(0),
459+
]
460+
)
461+
}
462+
463+
let reopenedDatabase = try SQLiteDatabase(url: databaseURL)
464+
let compactedRows = try reopenedDatabase.query(
465+
"SELECT content_hash FROM activity_events WHERE id = ?;",
466+
bindings: [.text("legacy-event")]
467+
)
468+
let compactedHash: String?
469+
if case .text(let value)? = compactedRows.first?["content_hash"] {
470+
compactedHash = value
471+
} else {
472+
compactedHash = nil
473+
}
474+
475+
#expect(compactedHash == ActivityEventContentHash.make(
476+
bundleId: bundleId,
477+
windowTitle: windowTitle,
478+
url: nil,
479+
visibleText: visibleText
480+
))
481+
#expect(compactedHash?.count == ActivityEventContentHash.compactLength)
482+
}
483+
428484
@Test func collectorLeaseBlocksSecondOwnerUntilHeartbeatExpires() async throws {
429485
let databaseURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString).appendingPathExtension("sqlite")
430486
let store = try OpenbirdStore(databaseURL: databaseURL)
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import Foundation
2+
import Testing
3+
@testable import OpenbirdKit
4+
5+
struct WindowSnapshotTests {
6+
@Test func fingerprintUsesFixedSizeDigest() {
7+
let snapshot = WindowSnapshot(
8+
bundleId: "com.apple.Safari",
9+
appName: "Safari",
10+
windowTitle: "Openbird",
11+
url: "https://openbird.app",
12+
visibleText: String(repeating: "Working on memory compaction. ", count: 120)
13+
)
14+
15+
let fingerprint = snapshot.fingerprint
16+
17+
#expect(fingerprint.count == ActivityEventContentHash.compactLength)
18+
#expect(fingerprint != snapshot.visibleText)
19+
#expect(fingerprint == snapshot.fingerprint)
20+
}
21+
}

0 commit comments

Comments
 (0)