-
Notifications
You must be signed in to change notification settings - Fork 58
feat(swift-sdk): add Core wallet balance diagnostics #4580
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: v4.2-dev
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,87 @@ import SwiftData | |
|
|
||
| /// Factory for creating SwiftData model containers for Dash Platform persistence | ||
| public enum DashModelContainer { | ||
| private struct StoreFileSizes { | ||
| let main: UInt64 | ||
| let wal: UInt64 | ||
| let shm: UInt64 | ||
|
|
||
| var total: UInt64 { | ||
| [main, wal, shm].reduce(0) { partial, value in | ||
| let (sum, overflow) = partial.addingReportingOverflow(value) | ||
| return overflow ? UInt64.max : sum | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// Builds the common payload for both sides of the container open. The | ||
| /// outcome deliberately describes only what SwiftData tells us: opening an | ||
| /// existing store may have included a migration, but this API does not | ||
| /// expose whether one actually ran. | ||
| private static func storeOpenFields( | ||
| succeeded: Bool, | ||
| existedBefore: Bool, | ||
| startedAt: CFAbsoluteTime, | ||
| sizeBefore: StoreFileSizes, | ||
| sizeAfter: StoreFileSizes | ||
| ) -> [String: SDKLogValue] { | ||
| let elapsed = max(0, (CFAbsoluteTimeGetCurrent() - startedAt) * 1_000) | ||
| let duration: UInt64 | ||
| if !elapsed.isFinite { | ||
| duration = 0 | ||
| } else if elapsed >= Double(UInt64.max) { | ||
| duration = UInt64.max | ||
| } else { | ||
| duration = UInt64(elapsed) | ||
| } | ||
| let openOutcome: String | ||
| switch (succeeded, existedBefore) { | ||
| case (true, true): | ||
| openOutcome = "existing_store_opened" | ||
| case (true, false): | ||
| openOutcome = "new_store_created" | ||
| case (false, true): | ||
| openOutcome = "existing_store_open_or_migration_failed" | ||
| case (false, false): | ||
| openOutcome = "new_store_creation_failed" | ||
| } | ||
|
|
||
| return [ | ||
| "container_result": .publicText(succeeded ? "opened" : "open_failed"), | ||
| "container_reused": .boolean(false), | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
🤖 AI-assisted review (Claude Code / Opus 5), relayed by @romchornyi. |
||
| "duration_ms": .unsignedInteger(duration), | ||
| "result": .publicText(succeeded ? "success" : "failure"), | ||
| "store_existed_before_open": .boolean(existedBefore), | ||
| "store_main_size_bytes_after": .unsignedInteger(sizeAfter.main), | ||
| "store_main_size_bytes_before": .unsignedInteger(sizeBefore.main), | ||
| "store_open_outcome": .publicText(openOutcome), | ||
| "store_shm_size_bytes_after": .unsignedInteger(sizeAfter.shm), | ||
| "store_shm_size_bytes_before": .unsignedInteger(sizeBefore.shm), | ||
| "store_size_bytes_after": .unsignedInteger(sizeAfter.total), | ||
| "store_size_bytes_before": .unsignedInteger(sizeBefore.total), | ||
| "store_wal_size_bytes_after": .unsignedInteger(sizeAfter.wal), | ||
| "store_wal_size_bytes_before": .unsignedInteger(sizeBefore.wal), | ||
| ] | ||
| } | ||
|
|
||
| /// SQLite's durable state can be mostly in the WAL immediately after an | ||
| /// app kill, so the main file alone is not a useful corruption signal. | ||
| /// Read only sizes and never include any component of the device path. | ||
| private static func storeFileSizes(at storeURL: URL) -> StoreFileSizes { | ||
| func fileSize(at url: URL) -> UInt64 { | ||
| guard let size = try? url.resourceValues(forKeys: [.fileSizeKey]).fileSize, | ||
| size >= 0 | ||
| else { return 0 } | ||
| return UInt64(size) | ||
| } | ||
|
|
||
| return StoreFileSizes( | ||
| main: fileSize(at: storeURL), | ||
| wal: fileSize(at: URL(fileURLWithPath: storeURL.path + "-wal")), | ||
| shm: fileSize(at: URL(fileURLWithPath: storeURL.path + "-shm")) | ||
| ) | ||
| } | ||
|
|
||
| /// Every registered schema version's model list, parameterised on the | ||
| /// one model whose shape differs between versions. | ||
| /// | ||
|
|
@@ -97,12 +178,49 @@ public enum DashModelContainer { | |
| ) | ||
|
|
||
| // Always wire the migration plan so stores created by an older SDK | ||
| // advance through the registered versioned schemas. | ||
| return try ModelContainer( | ||
| for: schema, | ||
| migrationPlan: DashMigrationPlan.self, | ||
| configurations: [modelConfiguration] | ||
| ) | ||
| // advance through the registered versioned schemas. Record only | ||
| // metadata about the store — never its device path. | ||
| let storeURL = modelConfiguration.url | ||
| let existedBefore = FileManager.default.fileExists(atPath: storeURL.path) | ||
| let sizeBefore = storeFileSizes(at: storeURL) | ||
| let started = CFAbsoluteTimeGetCurrent() | ||
| do { | ||
| let container = try ModelContainer( | ||
| for: schema, | ||
| migrationPlan: DashMigrationPlan.self, | ||
| configurations: [modelConfiguration] | ||
| ) | ||
| let sizeAfter = storeFileSizes(at: storeURL) | ||
| SDKLogger.event( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The event is mirrored to NSLog/print only. The support artifact this PR is built to produce contains no 🤖 AI-assisted review (Claude Code / Opus 5), relayed by @romchornyi. |
||
| "core_store_open_result", | ||
| category: .persistence, | ||
| fields: storeOpenFields( | ||
| succeeded: true, | ||
| existedBefore: existedBefore, | ||
| startedAt: started, | ||
| sizeBefore: sizeBefore, | ||
| sizeAfter: sizeAfter | ||
| ) | ||
| ) | ||
| return container | ||
| } catch { | ||
| let sizeAfter = storeFileSizes(at: storeURL) | ||
| SDKLogger.event( | ||
| "core_store_open_result", | ||
| category: .persistence, | ||
| severity: .error, | ||
| fields: storeOpenFields( | ||
| succeeded: false, | ||
| existedBefore: existedBefore, | ||
| startedAt: started, | ||
| sizeBefore: sizeBefore, | ||
| sizeAfter: sizeAfter | ||
| ), | ||
| error: error, | ||
| redacting: [storeURL.path] | ||
| ) | ||
| throw error | ||
| } | ||
| } | ||
|
|
||
| /// Create an in-memory model container for testing | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
StoreFileSizes.totalre-implementsdiagnosticSaturatingSum, which this same PR adds as an internal module-level function.The
reducewithaddingReportingOverflowand theUInt64.maxsentinel is byte-for-byte the body ofdiagnosticSaturatingSum(PlatformWalletManagerCoreDiagnostics.swift:110-116), which is internal to the sameSwiftDashSDKmodule and therefore callable here. Two copies of the same saturating-sum rule mean a future fix to one — e.g. switching to a sentinel other thanUInt64.max— leaves the store-size totals on the old semantics.🤖 AI-assisted review (Claude Code / Opus 5), relayed by @romchornyi.