Optimize markAllCallHistoryReadSync
This commit is contained in:
parent
133f12cfd1
commit
01dda86538
4 changed files with 215 additions and 37 deletions
102
ts/sql/Server.ts
102
ts/sql/Server.ts
|
@ -2253,7 +2253,7 @@ export function getAllSyncTasksSync(db: Database): Array<SyncTaskType> {
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveMessageSync(
|
export function saveMessageSync(
|
||||||
db: Database,
|
db: Database,
|
||||||
data: MessageType,
|
data: MessageType,
|
||||||
options: {
|
options: {
|
||||||
|
@ -3638,7 +3638,7 @@ async function clearCallHistory(
|
||||||
): Promise<ReadonlyArray<string>> {
|
): Promise<ReadonlyArray<string>> {
|
||||||
const db = await getWritableInstance();
|
const db = await getWritableInstance();
|
||||||
return db.transaction(() => {
|
return db.transaction(() => {
|
||||||
const timestamp = getTimestampForCallLogEventTarget(db, target);
|
const timestamp = getMessageTimestampForCallLogEventTarget(db, target);
|
||||||
|
|
||||||
const [selectCallIdsQuery, selectCallIdsParams] = sql`
|
const [selectCallIdsQuery, selectCallIdsParams] = sql`
|
||||||
SELECT callsHistory.callId
|
SELECT callsHistory.callId
|
||||||
|
@ -3807,48 +3807,76 @@ async function markCallHistoryRead(callId: string): Promise<void> {
|
||||||
db.prepare(query).run(params);
|
db.prepare(query).run(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTimestampForCallLogEventTarget(
|
function getMessageTimestampForCallLogEventTarget(
|
||||||
db: Database,
|
db: Database,
|
||||||
target: CallLogEventTarget
|
target: CallLogEventTarget
|
||||||
): number {
|
): number {
|
||||||
let { timestamp } = target;
|
let { callId, peerId } = target;
|
||||||
|
const { timestamp } = target;
|
||||||
|
|
||||||
if (target.peerId != null && target.callId != null) {
|
if (callId == null || peerId == null) {
|
||||||
|
const predicate =
|
||||||
|
peerId != null
|
||||||
|
? sqlFragment`callsHistory.peerId IS ${target.peerId}`
|
||||||
|
: sqlFragment`TRUE`;
|
||||||
|
|
||||||
|
// Get the most recent call history timestamp for the target.timestamp
|
||||||
const [selectQuery, selectParams] = sql`
|
const [selectQuery, selectParams] = sql`
|
||||||
SELECT callsHistory.timestamp
|
SELECT callsHistory.callId, callsHistory.peerId
|
||||||
FROM callsHistory
|
FROM callsHistory
|
||||||
WHERE callsHistory.callId IS ${target.callId}
|
WHERE ${predicate}
|
||||||
AND callsHistory.peerId IS ${target.peerId}
|
AND callsHistory.timestamp <= ${timestamp}
|
||||||
|
ORDER BY callsHistory.timestamp DESC
|
||||||
|
LIMIT 1
|
||||||
`;
|
`;
|
||||||
const value = db.prepare(selectQuery).pluck().get(selectParams);
|
|
||||||
|
|
||||||
if (value != null) {
|
const row = db.prepare(selectQuery).get(selectParams);
|
||||||
timestamp = value;
|
if (row == null) {
|
||||||
} else {
|
log.warn('getTimestampForCallLogEventTarget: Target call not found');
|
||||||
log.warn(
|
return timestamp;
|
||||||
'getTimestampForCallLogEventTarget: Target call not found',
|
|
||||||
target.callId
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
callId = row.callId as string;
|
||||||
|
peerId = row.peerId as AciString;
|
||||||
}
|
}
|
||||||
|
|
||||||
return timestamp;
|
const [selectQuery, selectParams] = sql`
|
||||||
|
SELECT messages.sent_at
|
||||||
|
FROM messages
|
||||||
|
WHERE messages.type IS 'call-history'
|
||||||
|
AND messages.conversationId IS ${peerId}
|
||||||
|
AND messages.callId IS ${callId}
|
||||||
|
LIMIT 1
|
||||||
|
`;
|
||||||
|
|
||||||
|
const messageTimestamp = db.prepare(selectQuery).pluck().get(selectParams);
|
||||||
|
|
||||||
|
if (messageTimestamp == null) {
|
||||||
|
log.warn(
|
||||||
|
'getTimestampForCallLogEventTarget: Target call message not found'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return messageTimestamp ?? target.timestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function markAllCallHistoryReadWithPredicate(
|
export function markAllCallHistoryReadSync(
|
||||||
|
db: Database,
|
||||||
target: CallLogEventTarget,
|
target: CallLogEventTarget,
|
||||||
inConversation: boolean
|
inConversation: boolean
|
||||||
) {
|
): void {
|
||||||
const db = await getWritableInstance();
|
if (inConversation) {
|
||||||
|
strictAssert(target.peerId, 'peerId is required');
|
||||||
|
}
|
||||||
|
|
||||||
db.transaction(() => {
|
db.transaction(() => {
|
||||||
const jsonPatch = JSON.stringify({
|
const jsonPatch = JSON.stringify({
|
||||||
seenStatus: SeenStatus.Seen,
|
seenStatus: SeenStatus.Seen,
|
||||||
});
|
});
|
||||||
|
|
||||||
const timestamp = getTimestampForCallLogEventTarget(db, target);
|
const timestamp = getMessageTimestampForCallLogEventTarget(db, target);
|
||||||
|
|
||||||
const predicate = inConversation
|
const predicate = inConversation
|
||||||
? sqlFragment`callsHistory.peerId IS ${target.peerId}`
|
? sqlFragment`messages.conversationId IS ${target.peerId}`
|
||||||
: sqlFragment`TRUE`;
|
: sqlFragment`TRUE`;
|
||||||
|
|
||||||
const [updateQuery, updateParams] = sql`
|
const [updateQuery, updateParams] = sql`
|
||||||
|
@ -3856,14 +3884,10 @@ async function markAllCallHistoryReadWithPredicate(
|
||||||
SET
|
SET
|
||||||
seenStatus = ${SEEN_STATUS_SEEN},
|
seenStatus = ${SEEN_STATUS_SEEN},
|
||||||
json = json_patch(json, ${jsonPatch})
|
json = json_patch(json, ${jsonPatch})
|
||||||
WHERE id IN (
|
WHERE messages.type IS 'call-history'
|
||||||
SELECT id FROM messages
|
AND ${predicate}
|
||||||
INNER JOIN callsHistory ON callsHistory.callId IS messages.callId
|
AND messages.seenStatus IS ${SEEN_STATUS_UNSEEN}
|
||||||
WHERE messages.type IS 'call-history'
|
AND messages.sent_at <= ${timestamp}
|
||||||
AND messages.seenStatus IS ${SEEN_STATUS_UNSEEN}
|
|
||||||
AND callsHistory.timestamp <= ${timestamp}
|
|
||||||
AND ${predicate}
|
|
||||||
)
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
db.prepare(updateQuery).run(updateParams);
|
db.prepare(updateQuery).run(updateParams);
|
||||||
|
@ -3873,14 +3897,16 @@ async function markAllCallHistoryReadWithPredicate(
|
||||||
async function markAllCallHistoryRead(
|
async function markAllCallHistoryRead(
|
||||||
target: CallLogEventTarget
|
target: CallLogEventTarget
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await markAllCallHistoryReadWithPredicate(target, false);
|
const db = await getWritableInstance();
|
||||||
|
markAllCallHistoryReadSync(db, target, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function markAllCallHistoryReadInConversation(
|
async function markAllCallHistoryReadInConversation(
|
||||||
target: CallLogEventTarget
|
target: CallLogEventTarget
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
strictAssert(target.peerId, 'peerId is required');
|
strictAssert(target.peerId, 'peerId is required');
|
||||||
await markAllCallHistoryReadWithPredicate(target, true);
|
const db = await getWritableInstance();
|
||||||
|
markAllCallHistoryReadSync(db, target, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCallHistoryGroupDataSync(
|
function getCallHistoryGroupDataSync(
|
||||||
|
@ -4173,9 +4199,10 @@ async function getCallHistoryGroups(
|
||||||
.reverse();
|
.reverse();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveCallHistory(callHistory: CallHistoryDetails): Promise<void> {
|
export function saveCallHistorySync(
|
||||||
const db = await getWritableInstance();
|
db: Database,
|
||||||
|
callHistory: CallHistoryDetails
|
||||||
|
): void {
|
||||||
const [insertQuery, insertParams] = sql`
|
const [insertQuery, insertParams] = sql`
|
||||||
INSERT OR REPLACE INTO callsHistory (
|
INSERT OR REPLACE INTO callsHistory (
|
||||||
callId,
|
callId,
|
||||||
|
@ -4201,6 +4228,11 @@ async function saveCallHistory(callHistory: CallHistoryDetails): Promise<void> {
|
||||||
db.prepare(insertQuery).run(insertParams);
|
db.prepare(insertQuery).run(insertParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function saveCallHistory(callHistory: CallHistoryDetails): Promise<void> {
|
||||||
|
const db = await getWritableInstance();
|
||||||
|
saveCallHistorySync(db, callHistory);
|
||||||
|
}
|
||||||
|
|
||||||
async function hasGroupCallHistoryMessage(
|
async function hasGroupCallHistoryMessage(
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
eraId: string
|
eraId: string
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
// Copyright 2024 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import type { Database } from '@signalapp/better-sqlite3';
|
||||||
|
import type { LoggerType } from '../../types/Logging';
|
||||||
|
import { sql } from '../util';
|
||||||
|
|
||||||
|
export const version = 1100;
|
||||||
|
|
||||||
|
export function updateToSchemaVersion1100(
|
||||||
|
currentVersion: number,
|
||||||
|
db: Database,
|
||||||
|
logger: LoggerType
|
||||||
|
): void {
|
||||||
|
if (currentVersion >= 1100) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
db.transaction(() => {
|
||||||
|
const [query] = sql`
|
||||||
|
-- Fix: Query went from readStatus to seenStatus but index wasn't updated
|
||||||
|
DROP INDEX IF EXISTS messages_callHistory_readStatus;
|
||||||
|
DROP INDEX IF EXISTS messages_callHistory_seenStatus;
|
||||||
|
CREATE INDEX messages_callHistory_seenStatus
|
||||||
|
ON messages (type, seenStatus)
|
||||||
|
WHERE type IS 'call-history';
|
||||||
|
|
||||||
|
-- Update to index created in 89: add sent_at to make it covering, and where clause to make it smaller
|
||||||
|
DROP INDEX IF EXISTS messages_call;
|
||||||
|
CREATE INDEX messages_call ON messages
|
||||||
|
(type, conversationId, callId, sent_at)
|
||||||
|
WHERE type IS 'call-history';
|
||||||
|
|
||||||
|
-- Update to index created in 89: add callId and peerId to make it covering
|
||||||
|
DROP INDEX IF EXISTS callsHistory_order;
|
||||||
|
CREATE INDEX callsHistory_order ON callsHistory
|
||||||
|
(timestamp DESC, callId, peerId);
|
||||||
|
|
||||||
|
-- Update to index created in 89: add timestamp for querying by order and callId to make it covering
|
||||||
|
DROP INDEX IF EXISTS callsHistory_byConversation;
|
||||||
|
DROP INDEX IF EXISTS callsHistory_byConversation_order;
|
||||||
|
CREATE INDEX callsHistory_byConversation_order ON callsHistory (peerId, timestamp DESC, callId);
|
||||||
|
|
||||||
|
-- Optimize markAllCallHistoryRead
|
||||||
|
DROP INDEX IF EXISTS messages_callHistory_markReadBefore;
|
||||||
|
CREATE INDEX messages_callHistory_markReadBefore
|
||||||
|
ON messages (type, seenStatus, sent_at DESC)
|
||||||
|
WHERE type IS 'call-history';
|
||||||
|
|
||||||
|
-- Optimize markAllCallHistoryReadInConversation
|
||||||
|
DROP INDEX IF EXISTS messages_callHistory_markReadByConversationBefore;
|
||||||
|
CREATE INDEX messages_callHistory_markReadByConversationBefore
|
||||||
|
ON messages (type, conversationId, seenStatus, sent_at DESC)
|
||||||
|
WHERE type IS 'call-history';
|
||||||
|
`;
|
||||||
|
|
||||||
|
db.exec(query);
|
||||||
|
})();
|
||||||
|
|
||||||
|
db.pragma('user_version = 1100');
|
||||||
|
|
||||||
|
logger.info('updateToSchemaVersion1100: success!');
|
||||||
|
}
|
|
@ -84,10 +84,11 @@ import { updateToSchemaVersion1050 } from './1050-group-send-endorsements';
|
||||||
import { updateToSchemaVersion1060 } from './1060-addressable-messages-and-sync-tasks';
|
import { updateToSchemaVersion1060 } from './1060-addressable-messages-and-sync-tasks';
|
||||||
import { updateToSchemaVersion1070 } from './1070-attachment-backup';
|
import { updateToSchemaVersion1070 } from './1070-attachment-backup';
|
||||||
import { updateToSchemaVersion1080 } from './1080-nondisappearing-addressable';
|
import { updateToSchemaVersion1080 } from './1080-nondisappearing-addressable';
|
||||||
|
import { updateToSchemaVersion1090 } from './1090-message-delete-indexes';
|
||||||
import {
|
import {
|
||||||
updateToSchemaVersion1090,
|
updateToSchemaVersion1100,
|
||||||
version as MAX_VERSION,
|
version as MAX_VERSION,
|
||||||
} from './1090-message-delete-indexes';
|
} from './1100-optimize-mark-call-history-read-in-conversation';
|
||||||
|
|
||||||
function updateToSchemaVersion1(
|
function updateToSchemaVersion1(
|
||||||
currentVersion: number,
|
currentVersion: number,
|
||||||
|
@ -2040,6 +2041,7 @@ export const SCHEMA_VERSIONS = [
|
||||||
updateToSchemaVersion1070,
|
updateToSchemaVersion1070,
|
||||||
updateToSchemaVersion1080,
|
updateToSchemaVersion1080,
|
||||||
updateToSchemaVersion1090,
|
updateToSchemaVersion1090,
|
||||||
|
updateToSchemaVersion1100,
|
||||||
];
|
];
|
||||||
|
|
||||||
export class DBVersionFromFutureError extends Error {
|
export class DBVersionFromFutureError extends Error {
|
||||||
|
|
81
ts/test-node/sql/migration_1100_test.ts
Normal file
81
ts/test-node/sql/migration_1100_test.ts
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
// Copyright 2024 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { assert } from 'chai';
|
||||||
|
import type { Database } from '@signalapp/better-sqlite3';
|
||||||
|
import SQL from '@signalapp/better-sqlite3';
|
||||||
|
import { findLast } from 'lodash';
|
||||||
|
import { insertData, updateToVersion } from './helpers';
|
||||||
|
import { markAllCallHistoryReadSync } from '../../sql/Server';
|
||||||
|
import { SeenStatus } from '../../MessageSeenStatus';
|
||||||
|
import { CallMode } from '../../types/Calling';
|
||||||
|
import {
|
||||||
|
CallDirection,
|
||||||
|
CallType,
|
||||||
|
DirectCallStatus,
|
||||||
|
} from '../../types/CallDisposition';
|
||||||
|
import { strictAssert } from '../../util/assert';
|
||||||
|
|
||||||
|
describe('SQL/updateToSchemaVersion1100', () => {
|
||||||
|
let db: Database;
|
||||||
|
beforeEach(() => {
|
||||||
|
db = new SQL(':memory:');
|
||||||
|
updateToVersion(db, 1100);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
db.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Optimize markAllCallHistoryReadInConversation', () => {
|
||||||
|
it('is fast', () => {
|
||||||
|
const COUNT = 10_000;
|
||||||
|
|
||||||
|
const messages = Array.from({ length: COUNT }, (_, index) => {
|
||||||
|
return {
|
||||||
|
id: `test-message-${index}`,
|
||||||
|
type: 'call-history',
|
||||||
|
seenStatus: SeenStatus.Unseen,
|
||||||
|
conversationId: `test-conversation-${index % 30}`,
|
||||||
|
sent_at: index,
|
||||||
|
json: {
|
||||||
|
callId: `test-call-${index}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const callsHistory = Array.from({ length: COUNT }, (_, index) => {
|
||||||
|
return {
|
||||||
|
callId: `test-call-${index}`,
|
||||||
|
peerId: `test-conversation-${index % 30}`,
|
||||||
|
timestamp: index,
|
||||||
|
ringerId: null,
|
||||||
|
mode: CallMode.Direct,
|
||||||
|
type: CallType.Video,
|
||||||
|
direction: CallDirection.Incoming,
|
||||||
|
status: DirectCallStatus.Missed,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
insertData(db, 'messages', messages);
|
||||||
|
insertData(db, 'callsHistory', callsHistory);
|
||||||
|
|
||||||
|
const latestCallInConversation = findLast(callsHistory, call => {
|
||||||
|
return call.peerId === 'test-conversation-0';
|
||||||
|
});
|
||||||
|
|
||||||
|
strictAssert(latestCallInConversation, 'missing latest call');
|
||||||
|
|
||||||
|
const target = {
|
||||||
|
timestamp: latestCallInConversation.timestamp,
|
||||||
|
callId: latestCallInConversation.callId,
|
||||||
|
peerId: latestCallInConversation.peerId,
|
||||||
|
};
|
||||||
|
|
||||||
|
const start = performance.now();
|
||||||
|
markAllCallHistoryReadSync(db, target, true);
|
||||||
|
const end = performance.now();
|
||||||
|
assert.isBelow(end - start, 50);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
Add table
Add a link
Reference in a new issue