Skip to content

Commit 2f718b0

Browse files
committed
improve memory performance, disable sql logger in prod
1 parent 9862400 commit 2f718b0

8 files changed

Lines changed: 68 additions & 113 deletions

File tree

electron-src/data/base-database.ts

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -73,25 +73,27 @@ export class BaseDatabase<T> {
7373
const dialect = new SqliteDialect({ database: sqliteDb });
7474
const options: KyselyConfig = {
7575
dialect,
76-
log(event): void {
77-
const isError = event.level === "error";
76+
log: !debugLoggingEnabled
77+
? ["error"]
78+
: (event) => {
79+
const isError = event.level === "error";
7880

79-
if (isError || debugLoggingEnabled) {
80-
const { sql, parameters } = event.query;
81+
if (isError || debugLoggingEnabled) {
82+
const { sql, parameters } = event.query;
8183

82-
const { queryDurationMillis } = event;
83-
const duration = queryDurationMillis.toFixed(2);
84-
const params = (parameters as string[]) || [];
85-
const formattedSql = format(sql, { params: params.map((l) => String(l)), language: "sqlite" });
86-
if (event.level === "query") {
87-
logger.debug(`[Query - ${duration}ms]:\n${formattedSql}\n`);
88-
}
84+
const { queryDurationMillis } = event;
85+
const duration = queryDurationMillis.toFixed(2);
86+
const params = (parameters as string[]) || [];
87+
const formattedSql = format(sql, { params: params.map((l) => String(l)), language: "sqlite" });
88+
if (event.level === "query") {
89+
logger.debug(`[Query - ${duration}ms]:\n${formattedSql}\n`);
90+
}
8991

90-
if (isError) {
91-
logger.error(`[SQL Error - ${duration}ms]: ${event.error}\n\n${formattedSql}\n`);
92-
}
93-
}
94-
},
92+
if (isError) {
93+
logger.error(`[SQL Error - ${duration}ms]: ${event.error}\n\n${formattedSql}\n`);
94+
}
95+
}
96+
},
9597
};
9698

9799
const db = new Kysely<T>(options);

electron-src/data/database.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -301,14 +301,14 @@ export class SQLDatabase extends BaseDatabase<MesssagesDatabase> {
301301
};
302302

303303
getAllMessageTexts = async (limit?: number, offset?: number) => {
304-
let query = this.baseAllMessageQuery().select(["text", "guid"]);
304+
let query = this.baseAllMessageQuery().select("text").distinct();
305305
if (limit) {
306306
query = query.limit(limit);
307307
}
308308
if (offset) {
309309
query = query.offset(offset);
310310
}
311-
return await query.execute();
311+
return (await query.execute()).map((r) => r.text!);
312312
};
313313
countAllMessageTexts = async (distinct = false): Promise<number> => {
314314
const query = this.baseAllMessageQuery().select((e) => {

electron-src/data/embeddings-database.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,14 +73,19 @@ export class EmbeddingsDatabase extends BaseDatabase<EmbeddingsDb> {
7373
const result = await this.db.selectFrom("embeddings").select("text").execute();
7474
return result.map((l) => l.text!);
7575
};
76+
getExistingText = async (text: string[]): Promise<string[]> => {
77+
await this.initialize();
78+
const result = await this.db.selectFrom("embeddings").select("text").where("text", "in", text).execute();
79+
return result.map((l) => l.text!);
80+
};
7681

77-
insertEmbeddings = async (embeddings: { text: string; embedding: number[] }[]) => {
82+
insertEmbeddings = async (embeddings: { input: string; values: number[] }[]) => {
7883
await this.initialize();
7984
const values = embeddings.map((e) => {
80-
const typedBuffer = new Float32Array(e.embedding);
85+
const typedBuffer = new Float32Array(e.values);
8186
const buffer = Buffer.from(typedBuffer.buffer);
8287
return {
83-
text: e.text,
88+
text: e.input,
8489
embedding: buffer,
8590
};
8691
});

electron-src/semantic-search/batch-utils.ts

Lines changed: 8 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,20 @@
11
import logger from "../utils/logger";
22
import type { OpenAIApi } from "openai";
3-
import type { Chunk, SemanticSearchMetadata, SemanticSearchVector } from "./semantic-search";
3+
import type { SemanticSearchVector } from "./semantic-search";
44
import { isRateLimitExceeded } from "./semantic-search";
55
import { pRateLimit } from "p-ratelimit";
66

77
export class BatchOpenAi {
88
private openai: OpenAIApi;
9-
private batch: PendingVector[] = [];
10-
private batchSize = 500; // create 500 embeddings at a time with the openai api
9+
private batch: string[] = [];
10+
private batchSize = 500;
1111

1212
constructor(openai: OpenAIApi) {
1313
this.openai = openai;
1414
}
1515

16-
async addPendingVectors(chunks: Chunk[], id: string) {
17-
const pendingVectors = chunks.map(({ text, start, end }, index) => {
18-
return {
19-
id: `${id}:${index}`,
20-
input: text,
21-
metadata: {
22-
index,
23-
id,
24-
text,
25-
end,
26-
start,
27-
},
28-
};
29-
});
30-
this.batch.push(...pendingVectors);
16+
async addPendingVectors(chunks: string[]) {
17+
this.batch.push(...chunks);
3118

3219
if (this.batch.length >= this.batchSize) {
3320
return await this.flush();
@@ -48,7 +35,6 @@ export class BatchOpenAi {
4835
interface PendingVector {
4936
id: string;
5037
input: string;
51-
metadata: SemanticSearchMetadata;
5238
}
5339

5440
export const OPENAI_EMBEDDING_MODEL = "text-embedding-ada-002";
@@ -60,16 +46,15 @@ const rateLimit = pRateLimit({
6046
rate: 3500, // 3500 calls per minute
6147
concurrency: 60, // no more than 60 running at once
6248
});
63-
const embeddingsFromPendingVectors = async (pendingVectors: PendingVector[], openai: OpenAIApi) => {
49+
const embeddingsFromPendingVectors = async (pendingVectors: string[], openai: OpenAIApi) => {
6450
const vectors: SemanticSearchVector[] = [];
6551

6652
let timeout = 10_000;
6753
while (pendingVectors.length) {
6854
try {
69-
const input = pendingVectors.map((l) => l.input);
7055
const { data: embed } = await rateLimit(() =>
7156
openai.createEmbedding({
72-
input,
57+
input: pendingVectors,
7358
model: OPENAI_EMBEDDING_MODEL,
7459
}),
7560
);
@@ -78,9 +63,8 @@ const embeddingsFromPendingVectors = async (pendingVectors: PendingVector[], ope
7863
const embedding = embeddings[i].embedding;
7964
if (embedding) {
8065
const vector: SemanticSearchVector = {
81-
id: pendingVectors[i].id,
82-
metadata: pendingVectors[i].metadata,
8366
values: embedding || [],
67+
input: pendingVectors[i],
8468
};
8569
vectors.push(vector);
8670
}

electron-src/semantic-search/semantic-search-stats.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@ import { GPT4Tokenizer } from "gpt4-tokenizer";
22

33
const tokenizer = new GPT4Tokenizer({ type: "gpt3" });
44

5-
export const getStatsForText = (text: { text: string | null }[]) => {
5+
export const getStatsForText = (text: string[]) => {
66
let totalTokens = 0;
77
const uniqueText = new Set<string>();
88
for (const line of text) {
9-
if (line.text && !uniqueText.has(line.text)) {
10-
uniqueText.add(line.text);
11-
const tokens = tokenizer.estimateTokenCount(line.text);
9+
if (line && !uniqueText.has(line)) {
10+
uniqueText.add(line);
11+
const tokens = tokenizer.estimateTokenCount(line);
1212
totalTokens += tokens;
1313
}
1414
}

electron-src/semantic-search/semantic-search.ts

Lines changed: 25 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -5,35 +5,16 @@ import { handleIpc } from "../ipc/ipc";
55
import logger from "../utils/logger";
66
import { BatchOpenAi, OPENAI_EMBEDDING_MODEL } from "./batch-utils";
77
import pMap from "p-map";
8-
import { uniqBy } from "lodash-es";
9-
10-
export interface SemanticSearchMetadata {
11-
id: string;
12-
text: string;
13-
start: number;
14-
end: number;
15-
[key: string]: any;
16-
}
178

189
export interface SemanticSearchVector {
19-
id: string;
10+
input: string;
2011
values: number[];
21-
metadata: SemanticSearchMetadata;
22-
}
23-
24-
export interface PostContent {
25-
chunks: Chunk[];
2612
}
2713

28-
export interface Chunk {
29-
text: string;
30-
start: number;
31-
end: number;
32-
}
3314
const tokenizer = new GPT4Tokenizer({ type: "gpt3" });
3415
const debugLoggingEnabled = process.env.DEBUG_LOGGING === "true";
3516

36-
export const MAX_INPUT_TOKENS = 1000;
17+
export const MAX_INPUT_TOKENS = 7000;
3718

3819
export function isRateLimitExceeded(err: unknown): boolean {
3920
return (
@@ -50,26 +31,20 @@ export function isRateLimitExceeded(err: unknown): boolean {
5031
let numCompleted = 0;
5132

5233
const splitIntoChunks = (content: string, maxInputTokens = MAX_INPUT_TOKENS) => {
53-
const chunks: Chunk[] = [];
54-
55-
let start = 0;
56-
57-
const chunked = tokenizer.chunkText(content, maxInputTokens);
58-
59-
for (const chunk of chunked) {
60-
chunks.push({
61-
start,
62-
end: start + chunk.text.length,
63-
text: chunk.text,
64-
});
65-
66-
start += chunk.text.length + 1;
34+
if (content.length < 2000) {
35+
return [content];
6736
}
37+
const chunks: string[] = [];
6838

39+
const encoded = tokenizer.encode(content);
40+
for (let i = 0; i < encoded.length; i += maxInputTokens) {
41+
const chunk = encoded.slice(i, i + maxInputTokens);
42+
chunks.push(tokenizer.decode(chunk));
43+
}
6944
return chunks;
7045
};
7146

72-
const PAGE_SIZE = 100_000;
47+
const PAGE_SIZE = 30_000;
7348

7449
export const createEmbeddings = async ({ openAiKey }: { openAiKey: string }) => {
7550
logger.info("Creating embeddings");
@@ -78,8 +53,6 @@ export const createEmbeddings = async ({ openAiKey }: { openAiKey: string }) =>
7853
const messageCount = await dbWorker.worker.countAllMessageTexts();
7954

8055
const pages = Math.ceil(messageCount / PAGE_SIZE);
81-
const existingText = await dbWorker.embeddingsWorker.getAllText();
82-
const set = new Set(existingText);
8356

8457
const configuration = new Configuration({
8558
apiKey: openAiKey,
@@ -88,21 +61,17 @@ export const createEmbeddings = async ({ openAiKey }: { openAiKey: string }) =>
8861
const openai = new OpenAIApi(configuration);
8962

9063
const batchOpenai = new BatchOpenAi(openai);
91-
const processMessage = async (message: Awaited<ReturnType<typeof dbWorker.worker.getAllMessageTexts>>[number]) => {
64+
const processMessage = async (message: string) => {
9265
try {
93-
if (!message.text) {
66+
if (!message) {
9467
return;
9568
}
9669

97-
const chunks = splitIntoChunks(message.text);
98-
const itemEmbeddings = await batchOpenai.addPendingVectors(chunks, message.guid);
99-
70+
const chunks = splitIntoChunks(message);
71+
const itemEmbeddings = await batchOpenai.addPendingVectors(chunks);
10072
if (itemEmbeddings.length) {
10173
try {
102-
logger.info(`Inserting ${itemEmbeddings.length} vectors`);
103-
const embeddings = itemEmbeddings.map((l) => ({ embedding: l.values, text: l.metadata.text }));
104-
await dbWorker.embeddingsWorker.insertEmbeddings(embeddings);
105-
logger.info(`Inserted ${itemEmbeddings.length} vectors`);
74+
await dbWorker.embeddingsWorker.insertEmbeddings(itemEmbeddings);
10675
numCompleted += itemEmbeddings.length;
10776
} catch (e) {
10877
logger.error(e);
@@ -117,17 +86,14 @@ export const createEmbeddings = async ({ openAiKey }: { openAiKey: string }) =>
11786
for (let i = 0; i < pages; i++) {
11887
const messages = await dbWorker.worker.getAllMessageTexts(PAGE_SIZE, i * PAGE_SIZE);
11988
logger.info(`Got ${messages.length} messages - ${i + 1} of ${pages}`);
120-
121-
numCompleted = existingText.length;
122-
const notParsed = messages.filter((m) => m.text && !set.has(m.text));
123-
124-
const uniqueMessages = uniqBy(notParsed, "text");
125-
126-
await pMap(uniqueMessages, processMessage, { concurrency: 100 });
127-
128-
if (debugLoggingEnabled) {
129-
logger.info(`Completed ${numCompleted} of ${messageCount} (${Math.round((numCompleted / messageCount) * 100)}%)`);
130-
}
89+
const now = performance.now();
90+
const existingText = await dbWorker.embeddingsWorker.getExistingText(messages);
91+
logger.info(`Got existing text in ${performance.now() - now}ms`);
92+
const set = new Set(existingText);
93+
numCompleted += existingText.length;
94+
const notParsed = messages.filter((m) => !set.has(m));
95+
await pMap(notParsed, processMessage, { concurrency: 50 });
96+
logger.info(`Completed ${numCompleted} of ${messageCount} (${Math.round((numCompleted / messageCount) * 100)}%)`);
13197
}
13298
logger.info("Done creating embeddings");
13399
};
@@ -159,7 +125,7 @@ export async function semanticQuery({ queryText, openAiKey }: SemanticQueryOpts)
159125
return [];
160126
}
161127
// save embedding
162-
await dbWorker.embeddingsWorker.insertEmbeddings([{ embedding, text: queryText }]);
128+
await dbWorker.embeddingsWorker.insertEmbeddings([{ values: embedding, input: queryText }]);
163129
floatEmbedding = new Float32Array(embedding);
164130
}
165131

electron-src/utils/flags.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
import type { App } from "electron";
22
export const addFlags = (app: App) => {
3-
process.env.UV_THREADPOOL_SIZE = "128";
3+
process.env.NODE_OPTIONS = "--max-old-space-size=32678";
44
app.commandLine.appendSwitch(
55
"enable-features",
66
"HardwareMediaKeyHandling,MediaSessionService,WebGPU,WebGPUDeveloperFeatures,WebGPUImportTexture,CSSVideoDynamicRangeMediaQueries,ExtraWebGLVideoTextureMetadata",
77
);
88
app.commandLine.appendSwitch("ignore-connections-limit", "localhost");
99
app.commandLine.appendArgument("--enable-experimental-web-platform-features");
10-
app.commandLine.appendSwitch(
11-
'--js-flags="--max-old-space-size=32678 --max-semi-space-size=32678 --use-largepages=silent"',
12-
);
10+
app.commandLine.appendSwitch('--js-flags="--max-old-space-size=32678');
1311
app.commandLine.appendSwitch("--remote-allow-origins=*");
1412
};

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"author": "JonLuca DeCaro <mimessage@jonlu.ca>",
44
"main": "build/electron-src/index.js",
55
"name": "MiMessage",
6-
"version": "1.1.0",
6+
"version": "1.1.1",
77
"productName": "Mimessage",
88
"description": "Apple Messages UI alternative, with export, search, and more.",
99
"scripts": {

0 commit comments

Comments
 (0)