perf(app): stop unbounded WASM heap growth and idle-CPU churn (#1721)

This commit is contained in:
Hampus authored and GitHub committed 2026-08-18 18:31:28 +02:00
1 parent fa71eb8682
commit 9e89539f0a
8 files changed
+205 -62

No files matched your search

@@ -1,5 +1,6 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {trackedRecipientNameKey} from '@app/features/channel/state/ChannelDisplayNameTracking';
import {onLocaleChange} from '@app/features/i18n/utils/LocaleChangeListener';
import {deferUntilModulesLoaded} from '@app/features/platform/utils/DeferUntilModulesLoaded';
import {noteText} from '@app/features/theme/fonts/ScriptFontLoader';
@@ -48,12 +49,8 @@ class ChannelDisplayName {
deferUntilModulesLoaded(() => {
reaction(
() => {
if (!Users) return [];
return Users.usersList.map((user) => ({
id: user.id,
username: user.username,
globalName: user.globalName,
}));
if (!Users) return '';
return trackedRecipientNameKey(this.channelSnapshots, Users.users);
},
() => this.recomputeAll(),
);
@@ -0,0 +1,129 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {
type TrackedRecipientSnapshot,
type TrackedRecipientUser,
trackedRecipientNameKey,
} from '@app/features/channel/state/ChannelDisplayNameTracking';
import {autorun, observable, runInAction} from 'mobx';
import {describe, expect, it} from 'vitest';
const snapshots = (entries: Record<string, ReadonlyArray<string>>): ReadonlyMap<string, TrackedRecipientSnapshot> =>
new Map(Object.entries(entries).map(([id, recipientIds]) => [id, {recipientIds}]));
const userTable = (count: number): Record<string, TrackedRecipientUser> => {
const users: Record<string, TrackedRecipientUser> = {};
for (let i = 0; i < count; i++) {
users[`u${i}`] = {username: `user${i}`, globalName: null};
}
return users;
};
/** Wraps a user table so we can count exactly which keys get read. */
const countingUsers = (
users: Record<string, TrackedRecipientUser | undefined>,
): {proxy: Record<string, TrackedRecipientUser | undefined>; reads: Array<string>} => {
const reads: Array<string> = [];
const proxy = new Proxy(users, {
get(target, key, receiver) {
if (typeof key === 'string') reads.push(key);
return Reflect.get(target, key, receiver);
},
});
return {proxy, reads};
};
describe('trackedRecipientNameKey', () => {
it('reads only the recipients of tracked group DMs, not the whole user cache', () => {
const {proxy, reads} = countingUsers(userTable(10_000));
trackedRecipientNameKey(snapshots({c1: ['u1', 'u2'], c2: ['u3']}), proxy);
// The cost must be a function of tracked recipients, never of cache size.
expect(reads).toEqual(['u1', 'u2', 'u3']);
});
it('cost is independent of how large the user cache grows', () => {
const small = countingUsers(userTable(10));
const huge = countingUsers(userTable(100_000));
const tracked = snapshots({c1: ['u1', 'u2']});
trackedRecipientNameKey(tracked, small.proxy);
trackedRecipientNameKey(tracked, huge.proxy);
expect(small.reads.length).toBe(2);
expect(huge.reads.length).toBe(2);
});
it('changes when a tracked recipient is renamed', () => {
const users = userTable(3);
const tracked = snapshots({c1: ['u1']});
const before = trackedRecipientNameKey(tracked, users);
users.u1 = {username: 'renamed', globalName: null};
expect(trackedRecipientNameKey(tracked, users)).not.toBe(before);
});
it('does not change when an untracked user is renamed', () => {
const users = userTable(3);
const tracked = snapshots({c1: ['u1']});
const before = trackedRecipientNameKey(tracked, users);
users.u2 = {username: 'unrelated-change', globalName: 'also changed'};
expect(trackedRecipientNameKey(tracked, users)).toBe(before);
});
it('distinguishes a missing recipient from one that arrives later', () => {
const users: Record<string, TrackedRecipientUser | undefined> = {};
const tracked = snapshots({c1: ['u1']});
const before = trackedRecipientNameKey(tracked, users);
users.u1 = {username: 'arrived', globalName: null};
expect(trackedRecipientNameKey(tracked, users)).not.toBe(before);
});
it('is not confused by usernames containing the separator characters', () => {
const tracked = snapshots({c1: ['a', 'b']});
const collidingLeft: Record<string, TrackedRecipientUser> = {
a: {username: 'x", "y', globalName: null},
b: {username: '', globalName: null},
};
const collidingRight: Record<string, TrackedRecipientUser> = {
a: {username: '', globalName: null},
b: {username: 'x", "y', globalName: null},
};
expect(trackedRecipientNameKey(tracked, collidingLeft)).not.toBe(trackedRecipientNameKey(tracked, collidingRight));
});
it('drives a MobX reaction on tracked renames but stays quiet on untracked ones', () => {
const users = observable.object<Record<string, TrackedRecipientUser | undefined>>({
u1: {username: 'one', globalName: null},
u2: {username: 'two', globalName: null},
});
const tracked = snapshots({c1: ['u1']});
let runs = 0;
const dispose = autorun(() => {
trackedRecipientNameKey(tracked, users);
runs++;
});
expect(runs).toBe(1);
// An untracked user changing must not wake the reaction.
runInAction(() => {
users.u2 = {username: 'two-renamed', globalName: null};
});
expect(runs).toBe(1);
// A tracked user changing must.
runInAction(() => {
users.u1 = {username: 'one-renamed', globalName: null};
});
expect(runs).toBe(2);
dispose();
});
});
@@ -0,0 +1,35 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
export interface TrackedRecipientSnapshot {
readonly recipientIds: ReadonlyArray<string>;
}
export interface TrackedRecipientUser {
readonly username: string;
readonly globalName?: string | null;
}
/**
* Builds the MobX tracking key for group-DM display names.
*
* Only the recipients of tracked group DMs are read, so the observing reaction
* depends on those users alone rather than on the whole (unbounded) user cache.
* Reading a recipient that is not cached yet still registers a dependency on
* that key, so a user arriving later re-triggers the reaction.
*
* Kept as a standalone function rather than a class method because
* makeAutoObservable converts methods into actions, and actions run untracked.
*/
export const trackedRecipientNameKey = (
snapshots: ReadonlyMap<string, TrackedRecipientSnapshot>,
users: Record<string, TrackedRecipientUser | undefined>,
): string => {
const parts: Array<[string, string, string]> = [];
for (const snapshot of snapshots.values()) {
for (const recipientId of snapshot.recipientIds) {
const user = users[recipientId];
parts.push([recipientId, user?.username ?? '', user?.globalName ?? '']);
}
}
return JSON.stringify(parts);
};