Добавленны бенчмарки производительности и подтвержден прирост native crypto/blurhash
This commit is contained in:
@@ -7,6 +7,7 @@ import Foundation
|
||||
enum XChaCha20Engine {
|
||||
|
||||
nonisolated static let poly1305TagSize = 16
|
||||
nonisolated private static let isNativeCryptoDisabled = ProcessInfo.processInfo.environment["ROSETTA_DISABLE_NATIVE_CRYPTO"] == "1"
|
||||
|
||||
// MARK: - XChaCha20-Poly1305 Decrypt
|
||||
|
||||
@@ -19,12 +20,14 @@ enum XChaCha20Engine {
|
||||
throw CryptoError.invalidData("Key must be 32 bytes, nonce must be 24 bytes")
|
||||
}
|
||||
|
||||
if let native = NativeCryptoBridge.xChaCha20Poly1305Decrypt(
|
||||
ciphertextWithTag,
|
||||
key: key,
|
||||
nonce: nonce
|
||||
) {
|
||||
return native
|
||||
if !isNativeCryptoDisabled {
|
||||
if let native = NativeCryptoBridge.xChaCha20Poly1305Decrypt(
|
||||
ciphertextWithTag,
|
||||
key: key,
|
||||
nonce: nonce
|
||||
) {
|
||||
return native
|
||||
}
|
||||
}
|
||||
|
||||
let ciphertext = ciphertextWithTag[0..<(ciphertextWithTag.count - poly1305TagSize)]
|
||||
@@ -61,12 +64,14 @@ enum XChaCha20Engine {
|
||||
throw CryptoError.invalidData("Key must be 32 bytes, nonce must be 24 bytes")
|
||||
}
|
||||
|
||||
if let native = NativeCryptoBridge.xChaCha20Poly1305Encrypt(
|
||||
plaintext,
|
||||
key: key,
|
||||
nonce: nonce
|
||||
) {
|
||||
return native
|
||||
if !isNativeCryptoDisabled {
|
||||
if let native = NativeCryptoBridge.xChaCha20Poly1305Encrypt(
|
||||
plaintext,
|
||||
key: key,
|
||||
nonce: nonce
|
||||
) {
|
||||
return native
|
||||
}
|
||||
}
|
||||
|
||||
// Step 1: HChaCha20 — derive subkey
|
||||
|
||||
@@ -54,12 +54,22 @@ final class DatabaseManager {
|
||||
t.column("account", .text).notNull()
|
||||
t.column("from_public_key", .text).notNull()
|
||||
t.column("to_public_key", .text).notNull()
|
||||
// Desktop/Android parity: encrypted payload as received from network.
|
||||
t.column("content", .text).notNull().defaults(to: "")
|
||||
t.column("chacha_key", .text).notNull().defaults(to: "")
|
||||
t.column("text", .text).notNull().defaults(to: "")
|
||||
// Android/Desktop naming parity (alias of `text` in iOS pipeline).
|
||||
t.column("plain_message", .text).notNull().defaults(to: "")
|
||||
t.column("timestamp", .integer).notNull()
|
||||
t.column("is_read", .integer).notNull().defaults(to: 0)
|
||||
// Android/Desktop naming parity (alias of `is_read`).
|
||||
t.column("read", .integer).notNull().defaults(to: 0)
|
||||
t.column("from_me", .integer).notNull().defaults(to: 0)
|
||||
t.column("delivery_status", .integer).notNull().defaults(to: 0)
|
||||
// Android/Desktop naming parity (alias of `delivery_status`).
|
||||
t.column("delivered", .integer).notNull().defaults(to: 0)
|
||||
t.column("message_id", .text).notNull()
|
||||
t.column("reply_to_message_id", .text)
|
||||
t.column("dialog_key", .text).notNull()
|
||||
t.column("attachments", .text).notNull().defaults(to: "[]")
|
||||
t.column("attachment_password", .text)
|
||||
@@ -131,7 +141,418 @@ final class DatabaseManager {
|
||||
try db.execute(sql: "UPDATE messages SET delivery_status = 1 WHERE delivery_status = 3")
|
||||
|
||||
// Step 3: Add last_message_read to dialogs (Android: lastMessageRead column).
|
||||
try db.execute(sql: "ALTER TABLE dialogs ADD COLUMN last_message_read INTEGER NOT NULL DEFAULT 0")
|
||||
let dialogColumns = Set(try db.columns(in: "dialogs").map(\.name))
|
||||
if !dialogColumns.contains("last_message_read") {
|
||||
try db.execute(sql: "ALTER TABLE dialogs ADD COLUMN last_message_read INTEGER NOT NULL DEFAULT 0")
|
||||
}
|
||||
}
|
||||
|
||||
// v4: align critical schema gaps with Android/Desktop without breaking existing iOS code.
|
||||
migrator.registerMigration("v4_schema_parity_android_desktop") { db in
|
||||
// Android parity index used by participant-pair queries and migration tooling.
|
||||
try db.execute(
|
||||
sql: """
|
||||
CREATE INDEX IF NOT EXISTS idx_messages_account_from_to_timestamp
|
||||
ON messages(account, from_public_key, to_public_key, timestamp)
|
||||
"""
|
||||
)
|
||||
|
||||
// Android parity: dialogs cache attachments of the latest message.
|
||||
let dialogColumns = Set(try db.columns(in: "dialogs").map(\.name))
|
||||
if !dialogColumns.contains("last_message_attachments") {
|
||||
try db.execute(
|
||||
sql: "ALTER TABLE dialogs ADD COLUMN last_message_attachments TEXT NOT NULL DEFAULT '[]'"
|
||||
)
|
||||
}
|
||||
|
||||
// Backfill attachment cache for existing dialogs from current messages.
|
||||
try db.execute(
|
||||
sql: """
|
||||
UPDATE dialogs
|
||||
SET last_message_attachments = COALESCE((
|
||||
SELECT m.attachments
|
||||
FROM messages m
|
||||
WHERE m.account = dialogs.account
|
||||
AND m.dialog_key = CASE
|
||||
WHEN dialogs.account = dialogs.opponent_key THEN dialogs.account
|
||||
WHEN dialogs.account < dialogs.opponent_key THEN dialogs.account || ':' || dialogs.opponent_key
|
||||
ELSE dialogs.opponent_key || ':' || dialogs.account
|
||||
END
|
||||
ORDER BY m.timestamp DESC, m.message_id DESC
|
||||
LIMIT 1
|
||||
), '[]')
|
||||
WHERE last_message_attachments = '[]'
|
||||
"""
|
||||
)
|
||||
|
||||
// Android/Desktop naming parity for sync cursor storage.
|
||||
try db.execute(
|
||||
sql: """
|
||||
CREATE TABLE IF NOT EXISTS accounts_sync_times (
|
||||
account TEXT NOT NULL PRIMARY KEY,
|
||||
last_sync INTEGER NOT NULL
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
if try db.tableExists("sync_cursors") {
|
||||
try db.execute(
|
||||
sql: """
|
||||
INSERT INTO accounts_sync_times (account, last_sync)
|
||||
SELECT account, timestamp FROM sync_cursors
|
||||
ON CONFLICT(account) DO UPDATE SET last_sync = excluded.last_sync
|
||||
"""
|
||||
)
|
||||
}
|
||||
|
||||
// Android parity tables (prepared for shared features).
|
||||
try db.execute(
|
||||
sql: """
|
||||
CREATE TABLE IF NOT EXISTS pinned_messages (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
account TEXT NOT NULL,
|
||||
dialog_key TEXT NOT NULL,
|
||||
message_id TEXT NOT NULL,
|
||||
pinned_at INTEGER NOT NULL
|
||||
)
|
||||
"""
|
||||
)
|
||||
try db.execute(
|
||||
sql: """
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS index_pinned_messages_account_dialog_key_message_id
|
||||
ON pinned_messages(account, dialog_key, message_id)
|
||||
"""
|
||||
)
|
||||
try db.execute(
|
||||
sql: """
|
||||
CREATE INDEX IF NOT EXISTS index_pinned_messages_account_dialog_key_pinned_at
|
||||
ON pinned_messages(account, dialog_key, pinned_at)
|
||||
"""
|
||||
)
|
||||
|
||||
try db.execute(
|
||||
sql: """
|
||||
CREATE TABLE IF NOT EXISTS groups (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
account TEXT NOT NULL,
|
||||
group_id TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
"key" TEXT NOT NULL
|
||||
)
|
||||
"""
|
||||
)
|
||||
try db.execute(
|
||||
sql: """
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS index_groups_account_group_id
|
||||
ON groups(account, group_id)
|
||||
"""
|
||||
)
|
||||
|
||||
try db.execute(
|
||||
sql: """
|
||||
CREATE TABLE IF NOT EXISTS avatar_cache (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
public_key TEXT NOT NULL,
|
||||
avatar TEXT NOT NULL,
|
||||
timestamp INTEGER NOT NULL
|
||||
)
|
||||
"""
|
||||
)
|
||||
try db.execute(
|
||||
sql: """
|
||||
CREATE INDEX IF NOT EXISTS index_avatar_cache_public_key_timestamp
|
||||
ON avatar_cache(public_key, timestamp)
|
||||
"""
|
||||
)
|
||||
|
||||
try db.execute(
|
||||
sql: """
|
||||
CREATE TABLE IF NOT EXISTS blacklist (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
public_key TEXT NOT NULL,
|
||||
account TEXT NOT NULL
|
||||
)
|
||||
"""
|
||||
)
|
||||
try db.execute(
|
||||
sql: """
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS index_blacklist_account_public_key
|
||||
ON blacklist(account, public_key)
|
||||
"""
|
||||
)
|
||||
}
|
||||
|
||||
// v5: full schema superset parity with Android + Desktop, preserving iOS contracts.
|
||||
migrator.registerMigration("v5_full_schema_superset_parity") { db in
|
||||
func hasColumn(_ table: String, _ column: String) throws -> Bool {
|
||||
try db.columns(in: table).contains { $0.name == column }
|
||||
}
|
||||
|
||||
// MARK: - messages parity columns
|
||||
|
||||
if try !hasColumn("messages", "content") {
|
||||
try db.execute(sql: "ALTER TABLE messages ADD COLUMN content TEXT NOT NULL DEFAULT ''")
|
||||
}
|
||||
if try !hasColumn("messages", "chacha_key") {
|
||||
try db.execute(sql: "ALTER TABLE messages ADD COLUMN chacha_key TEXT NOT NULL DEFAULT ''")
|
||||
}
|
||||
if try !hasColumn("messages", "plain_message") {
|
||||
try db.execute(sql: "ALTER TABLE messages ADD COLUMN plain_message TEXT NOT NULL DEFAULT ''")
|
||||
}
|
||||
if try !hasColumn("messages", "read") {
|
||||
try db.execute(sql: "ALTER TABLE messages ADD COLUMN read INTEGER NOT NULL DEFAULT 0")
|
||||
}
|
||||
if try !hasColumn("messages", "delivered") {
|
||||
try db.execute(sql: "ALTER TABLE messages ADD COLUMN delivered INTEGER NOT NULL DEFAULT 0")
|
||||
}
|
||||
if try !hasColumn("messages", "reply_to_message_id") {
|
||||
try db.execute(sql: "ALTER TABLE messages ADD COLUMN reply_to_message_id TEXT")
|
||||
}
|
||||
|
||||
try db.execute(
|
||||
sql: """
|
||||
UPDATE messages
|
||||
SET read = is_read,
|
||||
delivered = delivery_status,
|
||||
plain_message = CASE
|
||||
WHEN plain_message = '' THEN text
|
||||
ELSE plain_message
|
||||
END
|
||||
"""
|
||||
)
|
||||
|
||||
// Keep iOS-native and Android/Desktop alias columns synchronized.
|
||||
try db.execute(sql: "DROP TRIGGER IF EXISTS trg_messages_ai_sync_aliases")
|
||||
try db.execute(sql: "DROP TRIGGER IF EXISTS trg_messages_au_sync_aliases")
|
||||
try db.execute(
|
||||
sql: """
|
||||
CREATE TRIGGER trg_messages_ai_sync_aliases
|
||||
AFTER INSERT ON messages
|
||||
BEGIN
|
||||
UPDATE messages
|
||||
SET read = NEW.is_read,
|
||||
delivered = NEW.delivery_status,
|
||||
plain_message = CASE
|
||||
WHEN NEW.plain_message = '' THEN NEW.text
|
||||
ELSE NEW.plain_message
|
||||
END
|
||||
WHERE id = NEW.id;
|
||||
END
|
||||
"""
|
||||
)
|
||||
try db.execute(
|
||||
sql: """
|
||||
CREATE TRIGGER trg_messages_au_sync_aliases
|
||||
AFTER UPDATE OF is_read, delivery_status, text ON messages
|
||||
BEGIN
|
||||
UPDATE messages
|
||||
SET read = NEW.is_read,
|
||||
delivered = NEW.delivery_status,
|
||||
plain_message = NEW.text
|
||||
WHERE id = NEW.id;
|
||||
END
|
||||
"""
|
||||
)
|
||||
|
||||
// MARK: - dialogs parity alias columns
|
||||
|
||||
if try !hasColumn("dialogs", "dialog_id") {
|
||||
try db.execute(sql: "ALTER TABLE dialogs ADD COLUMN dialog_id TEXT NOT NULL DEFAULT ''")
|
||||
}
|
||||
if try !hasColumn("dialogs", "last_message_id") {
|
||||
try db.execute(sql: "ALTER TABLE dialogs ADD COLUMN last_message_id TEXT NOT NULL DEFAULT ''")
|
||||
}
|
||||
if try !hasColumn("dialogs", "last_timestamp") {
|
||||
try db.execute(sql: "ALTER TABLE dialogs ADD COLUMN last_timestamp INTEGER NOT NULL DEFAULT 0")
|
||||
}
|
||||
if try !hasColumn("dialogs", "is_request") {
|
||||
try db.execute(sql: "ALTER TABLE dialogs ADD COLUMN is_request INTEGER NOT NULL DEFAULT 0")
|
||||
}
|
||||
|
||||
try db.execute(
|
||||
sql: """
|
||||
UPDATE dialogs
|
||||
SET dialog_id = CASE
|
||||
WHEN dialog_id = '' THEN opponent_key
|
||||
ELSE dialog_id
|
||||
END,
|
||||
last_timestamp = last_message_timestamp,
|
||||
is_request = CASE WHEN i_have_sent = 1 THEN 0 ELSE 1 END
|
||||
"""
|
||||
)
|
||||
|
||||
try db.execute(
|
||||
sql: """
|
||||
UPDATE dialogs
|
||||
SET last_message_id = COALESCE((
|
||||
SELECT m.message_id
|
||||
FROM messages m
|
||||
WHERE m.account = dialogs.account
|
||||
AND m.dialog_key = CASE
|
||||
WHEN dialogs.account = dialogs.opponent_key THEN dialogs.account
|
||||
WHEN LOWER(TRIM(dialogs.opponent_key)) LIKE '#group:%' THEN dialogs.opponent_key
|
||||
WHEN LOWER(TRIM(dialogs.opponent_key)) LIKE 'group:%' THEN dialogs.opponent_key
|
||||
WHEN dialogs.account < dialogs.opponent_key THEN dialogs.account || ':' || dialogs.opponent_key
|
||||
ELSE dialogs.opponent_key || ':' || dialogs.account
|
||||
END
|
||||
ORDER BY m.timestamp DESC, m.message_id DESC
|
||||
LIMIT 1
|
||||
), last_message_id)
|
||||
"""
|
||||
)
|
||||
|
||||
try db.execute(sql: "DROP TRIGGER IF EXISTS trg_dialogs_ai_sync_aliases")
|
||||
try db.execute(sql: "DROP TRIGGER IF EXISTS trg_dialogs_au_sync_aliases")
|
||||
try db.execute(
|
||||
sql: """
|
||||
CREATE TRIGGER trg_dialogs_ai_sync_aliases
|
||||
AFTER INSERT ON dialogs
|
||||
BEGIN
|
||||
UPDATE dialogs
|
||||
SET dialog_id = NEW.opponent_key,
|
||||
last_timestamp = NEW.last_message_timestamp,
|
||||
is_request = CASE WHEN NEW.i_have_sent = 1 THEN 0 ELSE 1 END,
|
||||
last_message_id = COALESCE((
|
||||
SELECT m.message_id
|
||||
FROM messages m
|
||||
WHERE m.account = NEW.account
|
||||
AND m.dialog_key = CASE
|
||||
WHEN NEW.account = NEW.opponent_key THEN NEW.account
|
||||
WHEN LOWER(TRIM(NEW.opponent_key)) LIKE '#group:%' THEN NEW.opponent_key
|
||||
WHEN LOWER(TRIM(NEW.opponent_key)) LIKE 'group:%' THEN NEW.opponent_key
|
||||
WHEN NEW.account < NEW.opponent_key THEN NEW.account || ':' || NEW.opponent_key
|
||||
ELSE NEW.opponent_key || ':' || NEW.account
|
||||
END
|
||||
ORDER BY m.timestamp DESC, m.message_id DESC
|
||||
LIMIT 1
|
||||
), last_message_id),
|
||||
last_message_attachments = COALESCE((
|
||||
SELECT m.attachments
|
||||
FROM messages m
|
||||
WHERE m.account = NEW.account
|
||||
AND m.dialog_key = CASE
|
||||
WHEN NEW.account = NEW.opponent_key THEN NEW.account
|
||||
WHEN LOWER(TRIM(NEW.opponent_key)) LIKE '#group:%' THEN NEW.opponent_key
|
||||
WHEN LOWER(TRIM(NEW.opponent_key)) LIKE 'group:%' THEN NEW.opponent_key
|
||||
WHEN NEW.account < NEW.opponent_key THEN NEW.account || ':' || NEW.opponent_key
|
||||
ELSE NEW.opponent_key || ':' || NEW.account
|
||||
END
|
||||
ORDER BY m.timestamp DESC, m.message_id DESC
|
||||
LIMIT 1
|
||||
), '[]')
|
||||
WHERE id = NEW.id;
|
||||
END
|
||||
"""
|
||||
)
|
||||
try db.execute(
|
||||
sql: """
|
||||
CREATE TRIGGER trg_dialogs_au_sync_aliases
|
||||
AFTER UPDATE OF opponent_key, last_message_timestamp, i_have_sent, account ON dialogs
|
||||
BEGIN
|
||||
UPDATE dialogs
|
||||
SET dialog_id = NEW.opponent_key,
|
||||
last_timestamp = NEW.last_message_timestamp,
|
||||
is_request = CASE WHEN NEW.i_have_sent = 1 THEN 0 ELSE 1 END,
|
||||
last_message_id = COALESCE((
|
||||
SELECT m.message_id
|
||||
FROM messages m
|
||||
WHERE m.account = NEW.account
|
||||
AND m.dialog_key = CASE
|
||||
WHEN NEW.account = NEW.opponent_key THEN NEW.account
|
||||
WHEN LOWER(TRIM(NEW.opponent_key)) LIKE '#group:%' THEN NEW.opponent_key
|
||||
WHEN LOWER(TRIM(NEW.opponent_key)) LIKE 'group:%' THEN NEW.opponent_key
|
||||
WHEN NEW.account < NEW.opponent_key THEN NEW.account || ':' || NEW.opponent_key
|
||||
ELSE NEW.opponent_key || ':' || NEW.account
|
||||
END
|
||||
ORDER BY m.timestamp DESC, m.message_id DESC
|
||||
LIMIT 1
|
||||
), last_message_id),
|
||||
last_message_attachments = COALESCE((
|
||||
SELECT m.attachments
|
||||
FROM messages m
|
||||
WHERE m.account = NEW.account
|
||||
AND m.dialog_key = CASE
|
||||
WHEN NEW.account = NEW.opponent_key THEN NEW.account
|
||||
WHEN LOWER(TRIM(NEW.opponent_key)) LIKE '#group:%' THEN NEW.opponent_key
|
||||
WHEN LOWER(TRIM(NEW.opponent_key)) LIKE 'group:%' THEN NEW.opponent_key
|
||||
WHEN NEW.account < NEW.opponent_key THEN NEW.account || ':' || NEW.opponent_key
|
||||
ELSE NEW.opponent_key || ':' || NEW.account
|
||||
END
|
||||
ORDER BY m.timestamp DESC, m.message_id DESC
|
||||
LIMIT 1
|
||||
), '[]')
|
||||
WHERE id = NEW.id;
|
||||
END
|
||||
"""
|
||||
)
|
||||
|
||||
// MARK: - Desktop-only tables (for full schema parity surface)
|
||||
|
||||
try db.execute(
|
||||
sql: """
|
||||
CREATE TABLE IF NOT EXISTS accounts (
|
||||
public_key TEXT PRIMARY KEY,
|
||||
private_key TEXT NOT NULL,
|
||||
sfen TEXT NOT NULL
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
try db.execute(
|
||||
sql: """
|
||||
CREATE TABLE IF NOT EXISTS avatar_delivery (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
public_key TEXT NOT NULL,
|
||||
account TEXT NOT NULL
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
try db.execute(
|
||||
sql: """
|
||||
CREATE TABLE IF NOT EXISTS cached_users (
|
||||
public_key TEXT PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
username TEXT NOT NULL,
|
||||
verified INTEGER NOT NULL DEFAULT 0
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
// Android accounts table parity (iOS still uses Keychain as source of truth).
|
||||
try db.execute(
|
||||
sql: """
|
||||
CREATE TABLE IF NOT EXISTS encrypted_accounts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
public_key TEXT NOT NULL,
|
||||
private_key_encrypted TEXT NOT NULL,
|
||||
seed_phrase_encrypted TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
last_used TEXT,
|
||||
is_active INTEGER NOT NULL DEFAULT 1,
|
||||
username TEXT
|
||||
)
|
||||
"""
|
||||
)
|
||||
try db.execute(
|
||||
sql: """
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS index_encrypted_accounts_public_key
|
||||
ON encrypted_accounts(public_key)
|
||||
"""
|
||||
)
|
||||
|
||||
// Desktop shape compatibility (optional id while account remains canonical key).
|
||||
if try !hasColumn("accounts_sync_times", "id") {
|
||||
try db.execute(sql: "ALTER TABLE accounts_sync_times ADD COLUMN id INTEGER")
|
||||
try db.execute(
|
||||
sql: """
|
||||
UPDATE accounts_sync_times
|
||||
SET id = rowid
|
||||
WHERE id IS NULL OR id = 0
|
||||
"""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
try migrator.migrate(pool)
|
||||
@@ -216,19 +637,29 @@ final class DatabaseManager {
|
||||
func loadSyncCursor(account: String) -> Int64 {
|
||||
do {
|
||||
let stored = try read { db in
|
||||
try Int64.fetchOne(db,
|
||||
sql: "SELECT timestamp FROM sync_cursors WHERE account = ?",
|
||||
if let lastSync = try Int64.fetchOne(
|
||||
db,
|
||||
sql: "SELECT last_sync FROM accounts_sync_times WHERE account = ?",
|
||||
arguments: [account]
|
||||
) ?? 0
|
||||
) {
|
||||
return lastSync
|
||||
}
|
||||
|
||||
if try db.tableExists("sync_cursors") {
|
||||
return try Int64.fetchOne(
|
||||
db,
|
||||
sql: "SELECT timestamp FROM sync_cursors WHERE account = ?",
|
||||
arguments: [account]
|
||||
) ?? 0
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
// Android parity: normalize seconds → milliseconds
|
||||
if stored > 0, stored < 1_000_000_000_000 {
|
||||
let corrected = stored * 1000
|
||||
try? writeSync { db in
|
||||
try db.execute(
|
||||
sql: "INSERT INTO sync_cursors (account, timestamp) VALUES (?, ?) ON CONFLICT(account) DO UPDATE SET timestamp = excluded.timestamp",
|
||||
arguments: [account, corrected]
|
||||
)
|
||||
try upsertSyncCursor(db: db, account: account, timestamp: corrected)
|
||||
}
|
||||
return corrected
|
||||
}
|
||||
@@ -243,14 +674,31 @@ final class DatabaseManager {
|
||||
guard timestamp > existing else { return }
|
||||
do {
|
||||
try writeSync { db in
|
||||
try db.execute(
|
||||
sql: "INSERT INTO sync_cursors (account, timestamp) VALUES (?, ?) ON CONFLICT(account) DO UPDATE SET timestamp = excluded.timestamp",
|
||||
arguments: [account, timestamp]
|
||||
)
|
||||
try upsertSyncCursor(db: db, account: account, timestamp: timestamp)
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
private func upsertSyncCursor(db: Database, account: String, timestamp: Int64) throws {
|
||||
try db.execute(
|
||||
sql: """
|
||||
INSERT INTO accounts_sync_times (account, last_sync) VALUES (?, ?)
|
||||
ON CONFLICT(account) DO UPDATE SET last_sync = excluded.last_sync
|
||||
""",
|
||||
arguments: [account, timestamp]
|
||||
)
|
||||
|
||||
if try db.tableExists("sync_cursors") {
|
||||
try db.execute(
|
||||
sql: """
|
||||
INSERT INTO sync_cursors (account, timestamp) VALUES (?, ?)
|
||||
ON CONFLICT(account) DO UPDATE SET timestamp = excluded.timestamp
|
||||
""",
|
||||
arguments: [account, timestamp]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Delete
|
||||
|
||||
/// Delete all data for the given account (used on account delete).
|
||||
|
||||
@@ -10,12 +10,18 @@ struct MessageRecord: Codable, FetchableRecord, MutablePersistableRecord, Sendab
|
||||
var account: String
|
||||
var fromPublicKey: String
|
||||
var toPublicKey: String
|
||||
var content: String
|
||||
var chachaKey: String
|
||||
var text: String
|
||||
var plainMessage: String
|
||||
var timestamp: Int64
|
||||
var isRead: Int
|
||||
var readAlias: Int
|
||||
var fromMe: Int
|
||||
var deliveryStatus: Int
|
||||
var deliveredAlias: Int
|
||||
var messageId: String
|
||||
var replyToMessageId: String?
|
||||
var dialogKey: String
|
||||
var attachments: String
|
||||
var attachmentPassword: String?
|
||||
@@ -26,11 +32,17 @@ struct MessageRecord: Codable, FetchableRecord, MutablePersistableRecord, Sendab
|
||||
case id, account
|
||||
case fromPublicKey = "from_public_key"
|
||||
case toPublicKey = "to_public_key"
|
||||
case content
|
||||
case chachaKey = "chacha_key"
|
||||
case text, timestamp
|
||||
case plainMessage = "plain_message"
|
||||
case isRead = "is_read"
|
||||
case readAlias = "read"
|
||||
case fromMe = "from_me"
|
||||
case deliveryStatus = "delivery_status"
|
||||
case deliveredAlias = "delivered"
|
||||
case messageId = "message_id"
|
||||
case replyToMessageId = "reply_to_message_id"
|
||||
case dialogKey = "dialog_key"
|
||||
case attachments
|
||||
case attachmentPassword = "attachment_password"
|
||||
@@ -40,11 +52,17 @@ struct MessageRecord: Codable, FetchableRecord, MutablePersistableRecord, Sendab
|
||||
case id, account
|
||||
case fromPublicKey = "from_public_key"
|
||||
case toPublicKey = "to_public_key"
|
||||
case content
|
||||
case chachaKey = "chacha_key"
|
||||
case text, timestamp
|
||||
case plainMessage = "plain_message"
|
||||
case isRead = "is_read"
|
||||
case readAlias = "read"
|
||||
case fromMe = "from_me"
|
||||
case deliveryStatus = "delivery_status"
|
||||
case deliveredAlias = "delivered"
|
||||
case messageId = "message_id"
|
||||
case replyToMessageId = "reply_to_message_id"
|
||||
case dialogKey = "dialog_key"
|
||||
case attachments
|
||||
case attachmentPassword = "attachment_password"
|
||||
@@ -72,10 +90,10 @@ struct MessageRecord: Codable, FetchableRecord, MutablePersistableRecord, Sendab
|
||||
id: messageId,
|
||||
fromPublicKey: fromPublicKey,
|
||||
toPublicKey: toPublicKey,
|
||||
text: overrideText ?? text,
|
||||
text: overrideText ?? (plainMessage.isEmpty ? text : plainMessage),
|
||||
timestamp: timestamp,
|
||||
deliveryStatus: DeliveryStatus(rawValue: deliveryStatus) ?? .waiting,
|
||||
isRead: isRead != 0,
|
||||
isRead: (isRead != 0) || (readAlias != 0),
|
||||
attachments: parsedAttachments,
|
||||
attachmentPassword: attachmentPassword
|
||||
)
|
||||
@@ -97,12 +115,18 @@ struct MessageRecord: Codable, FetchableRecord, MutablePersistableRecord, Sendab
|
||||
account: account,
|
||||
fromPublicKey: message.fromPublicKey,
|
||||
toPublicKey: message.toPublicKey,
|
||||
content: "",
|
||||
chachaKey: "",
|
||||
text: message.text,
|
||||
plainMessage: message.text,
|
||||
timestamp: message.timestamp,
|
||||
isRead: message.isRead ? 1 : 0,
|
||||
readAlias: message.isRead ? 1 : 0,
|
||||
fromMe: fromMe ? 1 : 0,
|
||||
deliveryStatus: message.deliveryStatus.rawValue,
|
||||
deliveredAlias: message.deliveryStatus.rawValue,
|
||||
messageId: message.id,
|
||||
replyToMessageId: nil,
|
||||
dialogKey: DatabaseManager.dialogKey(account: account, opponentKey: fromMe ? message.toPublicKey : message.fromPublicKey),
|
||||
attachments: attachmentsJSON,
|
||||
attachmentPassword: message.attachmentPassword
|
||||
|
||||
@@ -360,7 +360,9 @@ final class MessageRepository: ObservableObject {
|
||||
// Update existing message (store encrypted text)
|
||||
try db.execute(
|
||||
sql: """
|
||||
UPDATE messages SET text = ?, timestamp = ?, attachments = ?,
|
||||
UPDATE messages SET content = COALESCE(NULLIF(?, ''), content),
|
||||
chacha_key = COALESCE(NULLIF(?, ''), chacha_key),
|
||||
text = ?, plain_message = ?, timestamp = ?, attachments = ?,
|
||||
attachment_password = COALESCE(?, attachment_password),
|
||||
delivery_status = CASE
|
||||
WHEN from_me = 1 AND delivery_status = 2 THEN ?
|
||||
@@ -370,7 +372,8 @@ final class MessageRepository: ObservableObject {
|
||||
WHERE account = ? AND message_id = ?
|
||||
""",
|
||||
arguments: [
|
||||
storedText, timestamp, finalAttachments,
|
||||
packet.content, packet.chachaKey,
|
||||
storedText, storedText, timestamp, finalAttachments,
|
||||
attachmentPassword,
|
||||
fromSync ? DeliveryStatus.delivered.rawValue : DeliveryStatus.waiting.rawValue,
|
||||
incomingRead ? 1 : 0,
|
||||
@@ -387,12 +390,18 @@ final class MessageRepository: ObservableObject {
|
||||
account: myPublicKey,
|
||||
fromPublicKey: packet.fromPublicKey,
|
||||
toPublicKey: packet.toPublicKey,
|
||||
content: packet.content,
|
||||
chachaKey: packet.chachaKey,
|
||||
text: storedText,
|
||||
plainMessage: storedText,
|
||||
timestamp: timestamp,
|
||||
isRead: isRead ? 1 : 0,
|
||||
readAlias: isRead ? 1 : 0,
|
||||
fromMe: fromMe ? 1 : 0,
|
||||
deliveryStatus: outgoingStatus.rawValue,
|
||||
deliveredAlias: outgoingStatus.rawValue,
|
||||
messageId: messageId,
|
||||
replyToMessageId: nil,
|
||||
dialogKey: dialogKey,
|
||||
attachments: attachmentsJSON,
|
||||
attachmentPassword: attachmentPassword
|
||||
|
||||
@@ -189,6 +189,7 @@ enum BlurHashEncoder {
|
||||
/// - Buffer: CFDataCreateMutable (canonical)
|
||||
/// - Punch parameter for color intensity control (Android parity)
|
||||
enum BlurHashDecoder {
|
||||
nonisolated private static let isNativeBlurHashDisabled = ProcessInfo.processInfo.environment["ROSETTA_DISABLE_NATIVE_BLURHASH"] == "1"
|
||||
|
||||
/// Decodes a BlurHash string into a UIImage.
|
||||
///
|
||||
@@ -201,14 +202,16 @@ enum BlurHashDecoder {
|
||||
nonisolated static func decode(blurHash: String, width: Int = 32, height: Int = 32, punch: Float = 1) -> UIImage? {
|
||||
guard blurHash.count >= 6 else { return nil }
|
||||
|
||||
if let nativeRGB = NativeBlurHashBridge.decodeBlurHash(
|
||||
blurHash,
|
||||
width: width,
|
||||
height: height,
|
||||
punch: punch
|
||||
),
|
||||
let nativeImage = makeImageFromRGBData(nativeRGB, width: width, height: height) {
|
||||
return nativeImage
|
||||
if !isNativeBlurHashDisabled {
|
||||
if let nativeRGB = NativeBlurHashBridge.decodeBlurHash(
|
||||
blurHash,
|
||||
width: width,
|
||||
height: height,
|
||||
punch: punch
|
||||
),
|
||||
let nativeImage = makeImageFromRGBData(nativeRGB, width: width, height: height) {
|
||||
return nativeImage
|
||||
}
|
||||
}
|
||||
|
||||
let sizeFlag = decodeBase83(blurHash, from: 0, length: 1)
|
||||
|
||||
145
Rosetta/Core/Utils/DebugPerformanceBenchmarks.swift
Normal file
145
Rosetta/Core/Utils/DebugPerformanceBenchmarks.swift
Normal file
@@ -0,0 +1,145 @@
|
||||
import Foundation
|
||||
import os
|
||||
import UIKit
|
||||
|
||||
#if DEBUG
|
||||
/// Lightweight startup benchmarks for local before/after performance checks.
|
||||
/// Run with launch env:
|
||||
/// `ROSETTA_RUN_BENCHMARKS=1` and optionally `ROSETTA_BENCHMARK_EXIT=1`.
|
||||
enum DebugPerformanceBenchmarks {
|
||||
nonisolated private static let logger = Logger(subsystem: "com.rosetta.messenger", category: "Bench")
|
||||
|
||||
nonisolated static func runIfRequested() {
|
||||
guard ProcessInfo.processInfo.environment["ROSETTA_RUN_BENCHMARKS"] == "1" else { return }
|
||||
|
||||
Task.detached(priority: .userInitiated) {
|
||||
let nativeCryptoDisabled = ProcessInfo.processInfo.environment["ROSETTA_DISABLE_NATIVE_CRYPTO"] == "1"
|
||||
let nativeBlurHashDisabled = ProcessInfo.processInfo.environment["ROSETTA_DISABLE_NATIVE_BLURHASH"] == "1"
|
||||
|
||||
let beginLine = "BENCH_BEGIN native_crypto_disabled=\(nativeCryptoDisabled) native_blurhash_disabled=\(nativeBlurHashDisabled)"
|
||||
print(beginLine)
|
||||
logger.info("\(beginLine)")
|
||||
|
||||
do {
|
||||
let cryptoResult = try benchmarkXChaCha20()
|
||||
let blurResult = benchmarkBlurHash()
|
||||
let streamResult = await MainActor.run {
|
||||
benchmarkStream()
|
||||
}
|
||||
|
||||
let cryptoLine =
|
||||
"BENCH_CRYPTO encrypt_ms_total=\(fmt(cryptoResult.encryptTotalMs)) encrypt_ms_per_op=\(fmt(cryptoResult.encryptMsPerOp)) decrypt_ms_total=\(fmt(cryptoResult.decryptTotalMs)) decrypt_ms_per_op=\(fmt(cryptoResult.decryptMsPerOp))"
|
||||
let blurLine =
|
||||
"BENCH_BLURHASH decode_ms_total=\(fmt(blurResult.totalMs)) decode_ms_per_op=\(fmt(blurResult.msPerOp))"
|
||||
let streamLine =
|
||||
"BENCH_STREAM write_ms_total=\(fmt(streamResult.writeTotalMs)) write_ms_per_op=\(fmt(streamResult.writeMsPerOp)) read_ms_total=\(fmt(streamResult.readTotalMs)) read_ms_per_op=\(fmt(streamResult.readMsPerOp))"
|
||||
print(cryptoLine)
|
||||
print(blurLine)
|
||||
print(streamLine)
|
||||
print("BENCH_END")
|
||||
logger.info("\(cryptoLine)")
|
||||
logger.info("\(blurLine)")
|
||||
logger.info("\(streamLine)")
|
||||
logger.info("BENCH_END")
|
||||
} catch {
|
||||
let errorLine = "BENCH_ERROR \(error)"
|
||||
print(errorLine)
|
||||
logger.error("\(errorLine)")
|
||||
}
|
||||
|
||||
if ProcessInfo.processInfo.environment["ROSETTA_BENCHMARK_EXIT"] == "1" {
|
||||
exit(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nonisolated private static func benchmarkXChaCha20() throws -> (
|
||||
encryptTotalMs: Double,
|
||||
encryptMsPerOp: Double,
|
||||
decryptTotalMs: Double,
|
||||
decryptMsPerOp: Double
|
||||
) {
|
||||
let payload = Data((0..<1024).map { UInt8($0 & 0xFF) })
|
||||
let key = Data((0..<32).map { UInt8(($0 + 1) & 0xFF) })
|
||||
let nonce = Data((0..<24).map { UInt8(($0 + 2) & 0xFF) })
|
||||
let iterations = 1000
|
||||
|
||||
let encryptTotalMs = try measureMs(iterations: iterations) {
|
||||
_ = try XChaCha20Engine.encrypt(plaintext: payload, key: key, nonce: nonce)
|
||||
}
|
||||
|
||||
let ciphertext = try XChaCha20Engine.encrypt(plaintext: payload, key: key, nonce: nonce)
|
||||
let decryptTotalMs = try measureMs(iterations: iterations) {
|
||||
_ = try XChaCha20Engine.decrypt(ciphertextWithTag: ciphertext, key: key, nonce: nonce)
|
||||
}
|
||||
|
||||
return (
|
||||
encryptTotalMs: encryptTotalMs,
|
||||
encryptMsPerOp: encryptTotalMs / Double(iterations),
|
||||
decryptTotalMs: decryptTotalMs,
|
||||
decryptMsPerOp: decryptTotalMs / Double(iterations)
|
||||
)
|
||||
}
|
||||
|
||||
nonisolated private static func benchmarkBlurHash() -> (totalMs: Double, msPerOp: Double) {
|
||||
let blurHash = "LEHV6nWB2yk8pyo0adR*.7kCMdnj"
|
||||
let iterations = 800
|
||||
|
||||
let totalMs = measureMs(iterations: iterations) {
|
||||
_ = UIImage.fromBlurHash(blurHash, width: 32, height: 32, punch: 1)
|
||||
}
|
||||
return (totalMs: totalMs, msPerOp: totalMs / Double(iterations))
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private static func benchmarkStream() -> (
|
||||
writeTotalMs: Double,
|
||||
writeMsPerOp: Double,
|
||||
readTotalMs: Double,
|
||||
readMsPerOp: Double
|
||||
) {
|
||||
let iterations = 20_000
|
||||
var packet = Data()
|
||||
|
||||
let writeTotalMs = measureMs(iterations: iterations) {
|
||||
let stream = Stream()
|
||||
stream.writeInt16(0x19)
|
||||
stream.writeInt64(1_741_129_600_123)
|
||||
stream.writeString("bench_message")
|
||||
stream.writeBoolean(true)
|
||||
stream.writeBytes(Data([1, 2, 3, 4, 5, 6, 7, 8]))
|
||||
packet = stream.toData()
|
||||
}
|
||||
|
||||
let readTotalMs = measureMs(iterations: iterations) {
|
||||
let stream = Stream(data: packet)
|
||||
_ = stream.readInt16()
|
||||
_ = stream.readInt64()
|
||||
_ = stream.readString()
|
||||
_ = stream.readBoolean()
|
||||
_ = stream.readBytes()
|
||||
}
|
||||
|
||||
return (
|
||||
writeTotalMs: writeTotalMs,
|
||||
writeMsPerOp: writeTotalMs / Double(iterations),
|
||||
readTotalMs: readTotalMs,
|
||||
readMsPerOp: readTotalMs / Double(iterations)
|
||||
)
|
||||
}
|
||||
|
||||
nonisolated private static func measureMs(iterations: Int, block: () throws -> Void) rethrows -> Double {
|
||||
let start = DispatchTime.now().uptimeNanoseconds
|
||||
for _ in 0..<iterations {
|
||||
try block()
|
||||
}
|
||||
let end = DispatchTime.now().uptimeNanoseconds
|
||||
let elapsedNs = end - start
|
||||
return Double(elapsedNs) / 1_000_000.0
|
||||
}
|
||||
|
||||
nonisolated private static func fmt(_ value: Double) -> String {
|
||||
String(format: "%.3f", value)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -305,6 +305,7 @@ struct RosettaApp: App {
|
||||
}
|
||||
|
||||
// Avoid heavy startup work on MainActor; Lottie assets load lazily on first use.
|
||||
DebugPerformanceBenchmarks.runIfRequested()
|
||||
}
|
||||
|
||||
@AppStorage("hasCompletedOnboarding") private var hasCompletedOnboarding = false
|
||||
|
||||
Reference in New Issue
Block a user