Добавленны бенчмарки производительности и подтвержден прирост native crypto/blurhash

This commit is contained in:
2026-03-28 09:37:04 +05:00
parent 66369ec0b9
commit 8314318a8a
7 changed files with 671 additions and 36 deletions

View File

@@ -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

View File

@@ -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).

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View 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

View File

@@ -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