Periodically optimize FTS table

This commit is contained in:
Fedor Indutny 2023-01-24 11:13:00 -08:00 committed by GitHub
parent e8ed8b3400
commit 5dfdde998b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 131 additions and 0 deletions

View file

@ -55,6 +55,7 @@ import { senderCertificateService } from './services/senderCertificate';
import { GROUP_CREDENTIALS_KEY } from './services/groupCredentialFetcher'; import { GROUP_CREDENTIALS_KEY } from './services/groupCredentialFetcher';
import * as KeyboardLayout from './services/keyboardLayout'; import * as KeyboardLayout from './services/keyboardLayout';
import * as StorageService from './services/storage'; import * as StorageService from './services/storage';
import { optimizeFTS } from './services/ftsOptimizer';
import { RoutineProfileRefresher } from './routineProfileRefresh'; import { RoutineProfileRefresher } from './routineProfileRefresh';
import { isOlderThan, toDayMillis } from './util/timestamp'; import { isOlderThan, toDayMillis } from './util/timestamp';
import { isValidReactionEmoji } from './reactions/isValidReactionEmoji'; import { isValidReactionEmoji } from './reactions/isValidReactionEmoji';
@ -862,6 +863,8 @@ export async function startApp(): Promise<void> {
if (newVersion) { if (newVersion) {
await window.Signal.Data.cleanupOrphanedAttachments(); await window.Signal.Data.cleanupOrphanedAttachments();
optimizeFTS();
// Don't block on the following operation // Don't block on the following operation
void window.Signal.Data.ensureFilePermissions(); void window.Signal.Data.ensureFilePermissions();
} }

View file

@ -90,6 +90,7 @@ import { isNotNil } from '../util/isNotNil';
import { dropNull } from '../util/dropNull'; import { dropNull } from '../util/dropNull';
import { notificationService } from '../services/notifications'; import { notificationService } from '../services/notifications';
import { storageServiceUploadJob } from '../services/storage'; import { storageServiceUploadJob } from '../services/storage';
import { scheduleOptimizeFTS } from '../services/ftsOptimizer';
import { getSendOptions } from '../util/getSendOptions'; import { getSendOptions } from '../util/getSendOptions';
import { isConversationAccepted } from '../util/isConversationAccepted'; import { isConversationAccepted } from '../util/isConversationAccepted';
import { import {
@ -5039,6 +5040,8 @@ export class ConversationModel extends window.Backbone
await window.Signal.Data.removeAllMessagesInConversation(this.id, { await window.Signal.Data.removeAllMessagesInConversation(this.id, {
logId: this.idForLogging(), logId: this.idForLogging(),
}); });
scheduleOptimizeFTS();
} }
getTitle(options?: { isShort?: boolean }): string { getTitle(options?: { isShort?: boolean }): string {

View file

@ -79,6 +79,7 @@ import { migrateLegacyReadStatus } from '../messages/migrateLegacyReadStatus';
import { migrateLegacySendAttributes } from '../messages/migrateLegacySendAttributes'; import { migrateLegacySendAttributes } from '../messages/migrateLegacySendAttributes';
import { getOwn } from '../util/getOwn'; import { getOwn } from '../util/getOwn';
import { markRead, markViewed } from '../services/MessageUpdater'; import { markRead, markViewed } from '../services/MessageUpdater';
import { scheduleOptimizeFTS } from '../services/ftsOptimizer';
import { isMessageUnread } from '../util/isMessageUnread'; import { isMessageUnread } from '../util/isMessageUnread';
import { import {
isDirectConversation, isDirectConversation,
@ -1184,6 +1185,8 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
} }
await window.Signal.Data.deleteSentProtoByMessageId(this.id); await window.Signal.Data.deleteSentProtoByMessageId(this.id);
scheduleOptimizeFTS();
} }
override isEmpty(): boolean { override isEmpty(): boolean {

View file

@ -8,6 +8,7 @@ import { clearTimeoutIfNecessary } from '../util/clearTimeoutIfNecessary';
import { sleep } from '../util/sleep'; import { sleep } from '../util/sleep';
import { SECOND } from '../util/durations'; import { SECOND } from '../util/durations';
import * as Errors from '../types/errors'; import * as Errors from '../types/errors';
import { scheduleOptimizeFTS } from './ftsOptimizer';
class ExpiringMessagesDeletionService { class ExpiringMessagesDeletionService {
public update: typeof this.checkExpiringMessages; public update: typeof this.checkExpiringMessages;
@ -64,6 +65,10 @@ class ExpiringMessagesDeletionService {
conversation.decrementMessageCount(); conversation.decrementMessageCount();
} }
}); });
if (messages.length > 0) {
scheduleOptimizeFTS();
}
} catch (error) { } catch (error) {
window.SignalContext.log.error( window.SignalContext.log.error(
'destroyExpiredMessages: Error deleting expired messages', 'destroyExpiredMessages: Error deleting expired messages',

View file

@ -0,0 +1,63 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { debounce } from 'lodash';
import { SECOND } from '../util/durations';
import { sleep } from '../util/sleep';
import { drop } from '../util/drop';
import dataInterface from '../sql/Client';
import type { FTSOptimizationStateType } from '../sql/Interface';
import * as log from '../logging/log';
const INTERACTIVITY_DELAY_MS = 50;
class FTSOptimizer {
private isRunning = false;
public async run(): Promise<void> {
if (this.isRunning) {
return;
}
this.isRunning = true;
log.info('ftsOptimizer: starting');
let state: FTSOptimizationStateType | undefined;
const start = Date.now();
try {
do {
if (state !== undefined) {
// eslint-disable-next-line no-await-in-loop
await sleep(INTERACTIVITY_DELAY_MS);
}
// eslint-disable-next-line no-await-in-loop
state = await dataInterface.optimizeFTS(state);
} while (!state?.done);
} finally {
this.isRunning = false;
}
const duration = Date.now() - start;
if (!state) {
log.warn('ftsOptimizer: no final state');
return;
}
log.info(`ftsOptimizer: took ${duration}ms and ${state.steps} steps`);
}
}
const optimizer = new FTSOptimizer();
export const optimizeFTS = (): void => {
drop(optimizer.run());
};
export const scheduleOptimizeFTS = debounce(optimizeFTS, SECOND, {
maxWait: 5 * SECOND,
});

View file

@ -360,6 +360,12 @@ export type GetAllStoriesResultType = ReadonlyArray<
} }
>; >;
export type FTSOptimizationStateType = Readonly<{
changes: number;
steps: number;
done?: boolean;
}>;
export type DataInterface = { export type DataInterface = {
close: () => Promise<void>; close: () => Promise<void>;
removeDB: () => Promise<void>; removeDB: () => Promise<void>;
@ -704,6 +710,10 @@ export type DataInterface = {
getMaxMessageCounter(): Promise<number | undefined>; getMaxMessageCounter(): Promise<number | undefined>;
getStatisticsForLogging(): Promise<Record<string, string>>; getStatisticsForLogging(): Promise<Record<string, string>>;
optimizeFTS: (
state?: FTSOptimizationStateType
) => Promise<FTSOptimizationStateType | undefined>;
}; };
export type ServerInterface = DataInterface & { export type ServerInterface = DataInterface & {

View file

@ -78,6 +78,7 @@ import type {
DeleteSentProtoRecipientOptionsType, DeleteSentProtoRecipientOptionsType,
DeleteSentProtoRecipientResultType, DeleteSentProtoRecipientResultType,
EmojiType, EmojiType,
FTSOptimizationStateType,
GetAllStoriesResultType, GetAllStoriesResultType,
GetConversationRangeCenteredOnMessageResultType, GetConversationRangeCenteredOnMessageResultType,
GetKnownMessageAttachmentsResultType, GetKnownMessageAttachmentsResultType,
@ -341,6 +342,8 @@ const dataInterface: ServerInterface = {
getStatisticsForLogging, getStatisticsForLogging,
optimizeFTS,
// Server-only // Server-only
initialize, initialize,
@ -5405,6 +5408,47 @@ async function removeKnownDraftAttachments(
return Object.keys(lookup); return Object.keys(lookup);
} }
// Default value of 'automerge'.
// See: https://www.sqlite.org/fts5.html#the_automerge_configuration_option
const OPTIMIZE_FTS_PAGE_COUNT = 4;
// This query is incremental. It gets the `state` from the return value of
// previous `optimizeFTS` call. When `state.done` is `true` - optimization is
// complete.
async function optimizeFTS(
state?: FTSOptimizationStateType
): Promise<FTSOptimizationStateType | undefined> {
// See https://www.sqlite.org/fts5.html#the_merge_command
let pageCount = OPTIMIZE_FTS_PAGE_COUNT;
if (state === undefined) {
pageCount = -pageCount;
}
const db = getInstance();
const { changes } = prepare(
db,
`
INSERT INTO messages_fts(messages_fts, rank) VALUES ('merge', $pageCount);
`
).run({ pageCount });
if (state === undefined) {
return {
changes,
steps: 1,
};
}
const { changes: prevChanges, steps } = state;
if (Math.abs(changes - prevChanges) < 2) {
return { changes, steps, done: true };
}
// More work is needed.
return { changes, steps: steps + 1 };
}
async function getJobsInQueue(queueType: string): Promise<Array<StoredJob>> { async function getJobsInQueue(queueType: string): Promise<Array<StoredJob>> {
const db = getInstance(); const db = getInstance();
return getJobsInQueueSync(db, queueType); return getJobsInQueueSync(db, queueType);