Добавленны бенчмарки производительности и подтвержден прирост native crypto/blurhash
This commit is contained in:
@@ -7,6 +7,7 @@ import Foundation
|
|||||||
enum XChaCha20Engine {
|
enum XChaCha20Engine {
|
||||||
|
|
||||||
nonisolated static let poly1305TagSize = 16
|
nonisolated static let poly1305TagSize = 16
|
||||||
|
nonisolated private static let isNativeCryptoDisabled = ProcessInfo.processInfo.environment["ROSETTA_DISABLE_NATIVE_CRYPTO"] == "1"
|
||||||
|
|
||||||
// MARK: - XChaCha20-Poly1305 Decrypt
|
// MARK: - XChaCha20-Poly1305 Decrypt
|
||||||
|
|
||||||
@@ -19,6 +20,7 @@ enum XChaCha20Engine {
|
|||||||
throw CryptoError.invalidData("Key must be 32 bytes, nonce must be 24 bytes")
|
throw CryptoError.invalidData("Key must be 32 bytes, nonce must be 24 bytes")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !isNativeCryptoDisabled {
|
||||||
if let native = NativeCryptoBridge.xChaCha20Poly1305Decrypt(
|
if let native = NativeCryptoBridge.xChaCha20Poly1305Decrypt(
|
||||||
ciphertextWithTag,
|
ciphertextWithTag,
|
||||||
key: key,
|
key: key,
|
||||||
@@ -26,6 +28,7 @@ enum XChaCha20Engine {
|
|||||||
) {
|
) {
|
||||||
return native
|
return native
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let ciphertext = ciphertextWithTag[0..<(ciphertextWithTag.count - poly1305TagSize)]
|
let ciphertext = ciphertextWithTag[0..<(ciphertextWithTag.count - poly1305TagSize)]
|
||||||
let tag = ciphertextWithTag[(ciphertextWithTag.count - poly1305TagSize)...]
|
let tag = ciphertextWithTag[(ciphertextWithTag.count - poly1305TagSize)...]
|
||||||
@@ -61,6 +64,7 @@ enum XChaCha20Engine {
|
|||||||
throw CryptoError.invalidData("Key must be 32 bytes, nonce must be 24 bytes")
|
throw CryptoError.invalidData("Key must be 32 bytes, nonce must be 24 bytes")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !isNativeCryptoDisabled {
|
||||||
if let native = NativeCryptoBridge.xChaCha20Poly1305Encrypt(
|
if let native = NativeCryptoBridge.xChaCha20Poly1305Encrypt(
|
||||||
plaintext,
|
plaintext,
|
||||||
key: key,
|
key: key,
|
||||||
@@ -68,6 +72,7 @@ enum XChaCha20Engine {
|
|||||||
) {
|
) {
|
||||||
return native
|
return native
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Step 1: HChaCha20 — derive subkey
|
// Step 1: HChaCha20 — derive subkey
|
||||||
let subkey = hchacha20(key: key, nonce: Data(nonce[0..<16]))
|
let subkey = hchacha20(key: key, nonce: Data(nonce[0..<16]))
|
||||||
|
|||||||
@@ -54,12 +54,22 @@ final class DatabaseManager {
|
|||||||
t.column("account", .text).notNull()
|
t.column("account", .text).notNull()
|
||||||
t.column("from_public_key", .text).notNull()
|
t.column("from_public_key", .text).notNull()
|
||||||
t.column("to_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: "")
|
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("timestamp", .integer).notNull()
|
||||||
t.column("is_read", .integer).notNull().defaults(to: 0)
|
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("from_me", .integer).notNull().defaults(to: 0)
|
||||||
t.column("delivery_status", .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("message_id", .text).notNull()
|
||||||
|
t.column("reply_to_message_id", .text)
|
||||||
t.column("dialog_key", .text).notNull()
|
t.column("dialog_key", .text).notNull()
|
||||||
t.column("attachments", .text).notNull().defaults(to: "[]")
|
t.column("attachments", .text).notNull().defaults(to: "[]")
|
||||||
t.column("attachment_password", .text)
|
t.column("attachment_password", .text)
|
||||||
@@ -131,8 +141,419 @@ final class DatabaseManager {
|
|||||||
try db.execute(sql: "UPDATE messages SET delivery_status = 1 WHERE delivery_status = 3")
|
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).
|
// Step 3: Add last_message_read to dialogs (Android: lastMessageRead column).
|
||||||
|
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")
|
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)
|
try migrator.migrate(pool)
|
||||||
dbPool = pool
|
dbPool = pool
|
||||||
@@ -216,19 +637,29 @@ final class DatabaseManager {
|
|||||||
func loadSyncCursor(account: String) -> Int64 {
|
func loadSyncCursor(account: String) -> Int64 {
|
||||||
do {
|
do {
|
||||||
let stored = try read { db in
|
let stored = try read { db in
|
||||||
try Int64.fetchOne(db,
|
if let lastSync = try Int64.fetchOne(
|
||||||
|
db,
|
||||||
|
sql: "SELECT last_sync FROM accounts_sync_times WHERE account = ?",
|
||||||
|
arguments: [account]
|
||||||
|
) {
|
||||||
|
return lastSync
|
||||||
|
}
|
||||||
|
|
||||||
|
if try db.tableExists("sync_cursors") {
|
||||||
|
return try Int64.fetchOne(
|
||||||
|
db,
|
||||||
sql: "SELECT timestamp FROM sync_cursors WHERE account = ?",
|
sql: "SELECT timestamp FROM sync_cursors WHERE account = ?",
|
||||||
arguments: [account]
|
arguments: [account]
|
||||||
) ?? 0
|
) ?? 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
// Android parity: normalize seconds → milliseconds
|
// Android parity: normalize seconds → milliseconds
|
||||||
if stored > 0, stored < 1_000_000_000_000 {
|
if stored > 0, stored < 1_000_000_000_000 {
|
||||||
let corrected = stored * 1000
|
let corrected = stored * 1000
|
||||||
try? writeSync { db in
|
try? writeSync { db in
|
||||||
try db.execute(
|
try upsertSyncCursor(db: db, account: account, timestamp: corrected)
|
||||||
sql: "INSERT INTO sync_cursors (account, timestamp) VALUES (?, ?) ON CONFLICT(account) DO UPDATE SET timestamp = excluded.timestamp",
|
|
||||||
arguments: [account, corrected]
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
return corrected
|
return corrected
|
||||||
}
|
}
|
||||||
@@ -243,12 +674,29 @@ final class DatabaseManager {
|
|||||||
guard timestamp > existing else { return }
|
guard timestamp > existing else { return }
|
||||||
do {
|
do {
|
||||||
try writeSync { db in
|
try writeSync { db in
|
||||||
|
try upsertSyncCursor(db: db, account: account, timestamp: timestamp)
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func upsertSyncCursor(db: Database, account: String, timestamp: Int64) throws {
|
||||||
try db.execute(
|
try db.execute(
|
||||||
sql: "INSERT INTO sync_cursors (account, timestamp) VALUES (?, ?) ON CONFLICT(account) DO UPDATE SET timestamp = excluded.timestamp",
|
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]
|
arguments: [account, timestamp]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Delete
|
// MARK: - Delete
|
||||||
|
|||||||
@@ -10,12 +10,18 @@ struct MessageRecord: Codable, FetchableRecord, MutablePersistableRecord, Sendab
|
|||||||
var account: String
|
var account: String
|
||||||
var fromPublicKey: String
|
var fromPublicKey: String
|
||||||
var toPublicKey: String
|
var toPublicKey: String
|
||||||
|
var content: String
|
||||||
|
var chachaKey: String
|
||||||
var text: String
|
var text: String
|
||||||
|
var plainMessage: String
|
||||||
var timestamp: Int64
|
var timestamp: Int64
|
||||||
var isRead: Int
|
var isRead: Int
|
||||||
|
var readAlias: Int
|
||||||
var fromMe: Int
|
var fromMe: Int
|
||||||
var deliveryStatus: Int
|
var deliveryStatus: Int
|
||||||
|
var deliveredAlias: Int
|
||||||
var messageId: String
|
var messageId: String
|
||||||
|
var replyToMessageId: String?
|
||||||
var dialogKey: String
|
var dialogKey: String
|
||||||
var attachments: String
|
var attachments: String
|
||||||
var attachmentPassword: String?
|
var attachmentPassword: String?
|
||||||
@@ -26,11 +32,17 @@ struct MessageRecord: Codable, FetchableRecord, MutablePersistableRecord, Sendab
|
|||||||
case id, account
|
case id, account
|
||||||
case fromPublicKey = "from_public_key"
|
case fromPublicKey = "from_public_key"
|
||||||
case toPublicKey = "to_public_key"
|
case toPublicKey = "to_public_key"
|
||||||
|
case content
|
||||||
|
case chachaKey = "chacha_key"
|
||||||
case text, timestamp
|
case text, timestamp
|
||||||
|
case plainMessage = "plain_message"
|
||||||
case isRead = "is_read"
|
case isRead = "is_read"
|
||||||
|
case readAlias = "read"
|
||||||
case fromMe = "from_me"
|
case fromMe = "from_me"
|
||||||
case deliveryStatus = "delivery_status"
|
case deliveryStatus = "delivery_status"
|
||||||
|
case deliveredAlias = "delivered"
|
||||||
case messageId = "message_id"
|
case messageId = "message_id"
|
||||||
|
case replyToMessageId = "reply_to_message_id"
|
||||||
case dialogKey = "dialog_key"
|
case dialogKey = "dialog_key"
|
||||||
case attachments
|
case attachments
|
||||||
case attachmentPassword = "attachment_password"
|
case attachmentPassword = "attachment_password"
|
||||||
@@ -40,11 +52,17 @@ struct MessageRecord: Codable, FetchableRecord, MutablePersistableRecord, Sendab
|
|||||||
case id, account
|
case id, account
|
||||||
case fromPublicKey = "from_public_key"
|
case fromPublicKey = "from_public_key"
|
||||||
case toPublicKey = "to_public_key"
|
case toPublicKey = "to_public_key"
|
||||||
|
case content
|
||||||
|
case chachaKey = "chacha_key"
|
||||||
case text, timestamp
|
case text, timestamp
|
||||||
|
case plainMessage = "plain_message"
|
||||||
case isRead = "is_read"
|
case isRead = "is_read"
|
||||||
|
case readAlias = "read"
|
||||||
case fromMe = "from_me"
|
case fromMe = "from_me"
|
||||||
case deliveryStatus = "delivery_status"
|
case deliveryStatus = "delivery_status"
|
||||||
|
case deliveredAlias = "delivered"
|
||||||
case messageId = "message_id"
|
case messageId = "message_id"
|
||||||
|
case replyToMessageId = "reply_to_message_id"
|
||||||
case dialogKey = "dialog_key"
|
case dialogKey = "dialog_key"
|
||||||
case attachments
|
case attachments
|
||||||
case attachmentPassword = "attachment_password"
|
case attachmentPassword = "attachment_password"
|
||||||
@@ -72,10 +90,10 @@ struct MessageRecord: Codable, FetchableRecord, MutablePersistableRecord, Sendab
|
|||||||
id: messageId,
|
id: messageId,
|
||||||
fromPublicKey: fromPublicKey,
|
fromPublicKey: fromPublicKey,
|
||||||
toPublicKey: toPublicKey,
|
toPublicKey: toPublicKey,
|
||||||
text: overrideText ?? text,
|
text: overrideText ?? (plainMessage.isEmpty ? text : plainMessage),
|
||||||
timestamp: timestamp,
|
timestamp: timestamp,
|
||||||
deliveryStatus: DeliveryStatus(rawValue: deliveryStatus) ?? .waiting,
|
deliveryStatus: DeliveryStatus(rawValue: deliveryStatus) ?? .waiting,
|
||||||
isRead: isRead != 0,
|
isRead: (isRead != 0) || (readAlias != 0),
|
||||||
attachments: parsedAttachments,
|
attachments: parsedAttachments,
|
||||||
attachmentPassword: attachmentPassword
|
attachmentPassword: attachmentPassword
|
||||||
)
|
)
|
||||||
@@ -97,12 +115,18 @@ struct MessageRecord: Codable, FetchableRecord, MutablePersistableRecord, Sendab
|
|||||||
account: account,
|
account: account,
|
||||||
fromPublicKey: message.fromPublicKey,
|
fromPublicKey: message.fromPublicKey,
|
||||||
toPublicKey: message.toPublicKey,
|
toPublicKey: message.toPublicKey,
|
||||||
|
content: "",
|
||||||
|
chachaKey: "",
|
||||||
text: message.text,
|
text: message.text,
|
||||||
|
plainMessage: message.text,
|
||||||
timestamp: message.timestamp,
|
timestamp: message.timestamp,
|
||||||
isRead: message.isRead ? 1 : 0,
|
isRead: message.isRead ? 1 : 0,
|
||||||
|
readAlias: message.isRead ? 1 : 0,
|
||||||
fromMe: fromMe ? 1 : 0,
|
fromMe: fromMe ? 1 : 0,
|
||||||
deliveryStatus: message.deliveryStatus.rawValue,
|
deliveryStatus: message.deliveryStatus.rawValue,
|
||||||
|
deliveredAlias: message.deliveryStatus.rawValue,
|
||||||
messageId: message.id,
|
messageId: message.id,
|
||||||
|
replyToMessageId: nil,
|
||||||
dialogKey: DatabaseManager.dialogKey(account: account, opponentKey: fromMe ? message.toPublicKey : message.fromPublicKey),
|
dialogKey: DatabaseManager.dialogKey(account: account, opponentKey: fromMe ? message.toPublicKey : message.fromPublicKey),
|
||||||
attachments: attachmentsJSON,
|
attachments: attachmentsJSON,
|
||||||
attachmentPassword: message.attachmentPassword
|
attachmentPassword: message.attachmentPassword
|
||||||
|
|||||||
@@ -360,7 +360,9 @@ final class MessageRepository: ObservableObject {
|
|||||||
// Update existing message (store encrypted text)
|
// Update existing message (store encrypted text)
|
||||||
try db.execute(
|
try db.execute(
|
||||||
sql: """
|
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),
|
attachment_password = COALESCE(?, attachment_password),
|
||||||
delivery_status = CASE
|
delivery_status = CASE
|
||||||
WHEN from_me = 1 AND delivery_status = 2 THEN ?
|
WHEN from_me = 1 AND delivery_status = 2 THEN ?
|
||||||
@@ -370,7 +372,8 @@ final class MessageRepository: ObservableObject {
|
|||||||
WHERE account = ? AND message_id = ?
|
WHERE account = ? AND message_id = ?
|
||||||
""",
|
""",
|
||||||
arguments: [
|
arguments: [
|
||||||
storedText, timestamp, finalAttachments,
|
packet.content, packet.chachaKey,
|
||||||
|
storedText, storedText, timestamp, finalAttachments,
|
||||||
attachmentPassword,
|
attachmentPassword,
|
||||||
fromSync ? DeliveryStatus.delivered.rawValue : DeliveryStatus.waiting.rawValue,
|
fromSync ? DeliveryStatus.delivered.rawValue : DeliveryStatus.waiting.rawValue,
|
||||||
incomingRead ? 1 : 0,
|
incomingRead ? 1 : 0,
|
||||||
@@ -387,12 +390,18 @@ final class MessageRepository: ObservableObject {
|
|||||||
account: myPublicKey,
|
account: myPublicKey,
|
||||||
fromPublicKey: packet.fromPublicKey,
|
fromPublicKey: packet.fromPublicKey,
|
||||||
toPublicKey: packet.toPublicKey,
|
toPublicKey: packet.toPublicKey,
|
||||||
|
content: packet.content,
|
||||||
|
chachaKey: packet.chachaKey,
|
||||||
text: storedText,
|
text: storedText,
|
||||||
|
plainMessage: storedText,
|
||||||
timestamp: timestamp,
|
timestamp: timestamp,
|
||||||
isRead: isRead ? 1 : 0,
|
isRead: isRead ? 1 : 0,
|
||||||
|
readAlias: isRead ? 1 : 0,
|
||||||
fromMe: fromMe ? 1 : 0,
|
fromMe: fromMe ? 1 : 0,
|
||||||
deliveryStatus: outgoingStatus.rawValue,
|
deliveryStatus: outgoingStatus.rawValue,
|
||||||
|
deliveredAlias: outgoingStatus.rawValue,
|
||||||
messageId: messageId,
|
messageId: messageId,
|
||||||
|
replyToMessageId: nil,
|
||||||
dialogKey: dialogKey,
|
dialogKey: dialogKey,
|
||||||
attachments: attachmentsJSON,
|
attachments: attachmentsJSON,
|
||||||
attachmentPassword: attachmentPassword
|
attachmentPassword: attachmentPassword
|
||||||
|
|||||||
@@ -189,6 +189,7 @@ enum BlurHashEncoder {
|
|||||||
/// - Buffer: CFDataCreateMutable (canonical)
|
/// - Buffer: CFDataCreateMutable (canonical)
|
||||||
/// - Punch parameter for color intensity control (Android parity)
|
/// - Punch parameter for color intensity control (Android parity)
|
||||||
enum BlurHashDecoder {
|
enum BlurHashDecoder {
|
||||||
|
nonisolated private static let isNativeBlurHashDisabled = ProcessInfo.processInfo.environment["ROSETTA_DISABLE_NATIVE_BLURHASH"] == "1"
|
||||||
|
|
||||||
/// Decodes a BlurHash string into a UIImage.
|
/// Decodes a BlurHash string into a UIImage.
|
||||||
///
|
///
|
||||||
@@ -201,6 +202,7 @@ enum BlurHashDecoder {
|
|||||||
nonisolated static func decode(blurHash: String, width: Int = 32, height: Int = 32, punch: Float = 1) -> UIImage? {
|
nonisolated static func decode(blurHash: String, width: Int = 32, height: Int = 32, punch: Float = 1) -> UIImage? {
|
||||||
guard blurHash.count >= 6 else { return nil }
|
guard blurHash.count >= 6 else { return nil }
|
||||||
|
|
||||||
|
if !isNativeBlurHashDisabled {
|
||||||
if let nativeRGB = NativeBlurHashBridge.decodeBlurHash(
|
if let nativeRGB = NativeBlurHashBridge.decodeBlurHash(
|
||||||
blurHash,
|
blurHash,
|
||||||
width: width,
|
width: width,
|
||||||
@@ -210,6 +212,7 @@ enum BlurHashDecoder {
|
|||||||
let nativeImage = makeImageFromRGBData(nativeRGB, width: width, height: height) {
|
let nativeImage = makeImageFromRGBData(nativeRGB, width: width, height: height) {
|
||||||
return nativeImage
|
return nativeImage
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let sizeFlag = decodeBase83(blurHash, from: 0, length: 1)
|
let sizeFlag = decodeBase83(blurHash, from: 0, length: 1)
|
||||||
let numY = (sizeFlag / 9) + 1
|
let numY = (sizeFlag / 9) + 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.
|
// Avoid heavy startup work on MainActor; Lottie assets load lazily on first use.
|
||||||
|
DebugPerformanceBenchmarks.runIfRequested()
|
||||||
}
|
}
|
||||||
|
|
||||||
@AppStorage("hasCompletedOnboarding") private var hasCompletedOnboarding = false
|
@AppStorage("hasCompletedOnboarding") private var hasCompletedOnboarding = false
|
||||||
|
|||||||
Reference in New Issue
Block a user