perf: performance improvements 1
This commit is contained in:
+23
-1
@@ -10,7 +10,11 @@ import { RealtimeSessionFacade } from '../../../../core/realtime';
|
||||
import { selectCurrentUserId } from '../../../../store/users/users.selectors';
|
||||
import { DatabaseService } from '../../../../infrastructure/persistence';
|
||||
import { yieldToAttachmentHydrationLoop } from '../../domain/logic/attachment-blob.rules';
|
||||
import { buildAttachmentDisplayPinKey, shouldRevokeDisplayBlobForAttachment } from '../../domain/logic/attachment-blob-eviction.rules';
|
||||
import {
|
||||
buildAttachmentDisplayPinKey,
|
||||
collectMessageIdsForInactiveRoomBlobRelease,
|
||||
shouldRevokeDisplayBlobForAttachment
|
||||
} from '../../domain/logic/attachment-blob-eviction.rules';
|
||||
import {
|
||||
getWatchedAttachmentRoomIdFromUrl,
|
||||
isDirectMessageAttachmentRoomId,
|
||||
@@ -68,8 +72,14 @@ export class AttachmentManagerService {
|
||||
return;
|
||||
}
|
||||
|
||||
const previousRoomId = this.watchedRoomId;
|
||||
|
||||
this.watchedRoomId = this.extractWatchedRoomId(event.urlAfterRedirects || event.url);
|
||||
|
||||
if (this.watchedRoomId !== previousRoomId) {
|
||||
this.releaseDisplayBlobsForInactiveRooms(this.watchedRoomId);
|
||||
}
|
||||
|
||||
if (this.watchedRoomId) {
|
||||
void this.restoreLocalAttachmentsForRoom(this.watchedRoomId);
|
||||
void this.requestAutoDownloadsForRoom(this.watchedRoomId);
|
||||
@@ -211,6 +221,18 @@ export class AttachmentManagerService {
|
||||
}
|
||||
}
|
||||
|
||||
releaseDisplayBlobsForInactiveRooms(activeRoomId: string | null): void {
|
||||
const messageIds = collectMessageIdsForInactiveRoomBlobRelease(
|
||||
Array.from(this.runtimeStore.getAttachmentEntries(), ([messageId]) => messageId),
|
||||
(messageId) => this.runtimeStore.getMessageRoomId(messageId) ?? null,
|
||||
activeRoomId
|
||||
);
|
||||
|
||||
for (const messageId of messageIds) {
|
||||
this.revokeOffscreenDisplayBlobsForMessage(messageId);
|
||||
}
|
||||
}
|
||||
|
||||
requestFile(messageId: string, attachment: Attachment): Promise<void> {
|
||||
return this.transfer.requestFile(messageId, attachment);
|
||||
}
|
||||
|
||||
+91
@@ -134,6 +134,35 @@ describe('AttachmentPersistenceService', () => {
|
||||
expect(attachmentStorage.readFile).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not duplicate disk-hydrated bytes into the original-file cache', async () => {
|
||||
const injector = Injector.create({
|
||||
providers: [
|
||||
AttachmentPersistenceService,
|
||||
AttachmentRuntimeStore,
|
||||
{ provide: DatabaseService, useValue: database },
|
||||
{ provide: AttachmentStorageService, useValue: attachmentStorage },
|
||||
{ provide: Store, useValue: { select: () => of('room-1') } }
|
||||
]
|
||||
});
|
||||
const service = runInInjectionContext(injector, () => injector.get(AttachmentPersistenceService));
|
||||
const runtimeStore = injector.get(AttachmentRuntimeStore);
|
||||
const attachment = {
|
||||
id: 'att-1',
|
||||
messageId: 'msg-1',
|
||||
filename: 'photo.png',
|
||||
size: 3,
|
||||
mime: 'image/png',
|
||||
isImage: true,
|
||||
savedPath: '/appdata/photo.png',
|
||||
available: false
|
||||
};
|
||||
|
||||
await expect(service.ensureInlineDisplayObjectUrl(attachment)).resolves.toBe(true);
|
||||
|
||||
expect(attachment.objectUrl).toMatch(/^blob:/);
|
||||
expect(runtimeStore.getOriginalFile('msg-1:att-1')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('restores a blob from a whole-file read when the store cannot read chunks (browser store)', async () => {
|
||||
attachmentStorage.canReadFileChunks.mockReturnValue(false);
|
||||
|
||||
@@ -263,4 +292,66 @@ describe('AttachmentPersistenceService', () => {
|
||||
|
||||
revokeSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('releases the cached original file when revoking a disk-backed display blob', () => {
|
||||
const injector = Injector.create({
|
||||
providers: [
|
||||
AttachmentPersistenceService,
|
||||
AttachmentRuntimeStore,
|
||||
{ provide: DatabaseService, useValue: database },
|
||||
{ provide: AttachmentStorageService, useValue: attachmentStorage },
|
||||
{ provide: Store, useValue: { select: () => of('room-1') } }
|
||||
]
|
||||
});
|
||||
const service = runInInjectionContext(injector, () => injector.get(AttachmentPersistenceService));
|
||||
const runtimeStore = injector.get(AttachmentRuntimeStore);
|
||||
const attachment = {
|
||||
id: 'att-1',
|
||||
messageId: 'msg-1',
|
||||
filename: 'photo.png',
|
||||
size: 3,
|
||||
mime: 'image/png',
|
||||
isImage: true,
|
||||
savedPath: '/appdata/photo.png',
|
||||
available: true,
|
||||
objectUrl: 'blob:http://localhost/abc'
|
||||
};
|
||||
const revokeSpy = vi.spyOn(URL, 'revokeObjectURL').mockImplementation(() => undefined);
|
||||
|
||||
runtimeStore.setOriginalFile('msg-1:att-1', new File(['abc'], 'photo.png', { type: 'image/png' }));
|
||||
|
||||
expect(service.revokeAttachmentDisplayBlob(attachment)).toBe(true);
|
||||
expect(runtimeStore.getOriginalFile('msg-1:att-1')).toBeUndefined();
|
||||
|
||||
revokeSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('keeps the cached original file when the attachment is not persisted to disk yet', () => {
|
||||
const injector = Injector.create({
|
||||
providers: [
|
||||
AttachmentPersistenceService,
|
||||
AttachmentRuntimeStore,
|
||||
{ provide: DatabaseService, useValue: database },
|
||||
{ provide: AttachmentStorageService, useValue: attachmentStorage },
|
||||
{ provide: Store, useValue: { select: () => of('room-1') } }
|
||||
]
|
||||
});
|
||||
const service = runInInjectionContext(injector, () => injector.get(AttachmentPersistenceService));
|
||||
const runtimeStore = injector.get(AttachmentRuntimeStore);
|
||||
const attachment = {
|
||||
id: 'att-2',
|
||||
messageId: 'msg-1',
|
||||
filename: 'clip.mp4',
|
||||
size: 3,
|
||||
mime: 'video/mp4',
|
||||
isImage: false,
|
||||
available: true,
|
||||
objectUrl: 'blob:http://localhost/def'
|
||||
};
|
||||
|
||||
runtimeStore.setOriginalFile('msg-1:att-2', new File(['abc'], 'clip.mp4', { type: 'video/mp4' }));
|
||||
|
||||
expect(service.revokeAttachmentDisplayBlob(attachment)).toBe(false);
|
||||
expect(runtimeStore.getOriginalFile('msg-1:att-2')).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
+10
-6
@@ -141,6 +141,13 @@ export class AttachmentPersistenceService {
|
||||
this.revokeAttachmentObjectUrl(attachment);
|
||||
attachment.objectUrl = undefined;
|
||||
|
||||
// Once the bytes live on disk, the cached File duplicate is redundant:
|
||||
// peer requests are served from the disk path and re-display rehydrates
|
||||
// from disk, so keeping it would double the attachment's memory cost.
|
||||
if (attachment.savedPath?.trim()) {
|
||||
this.runtimeStore.deleteOriginalFile(`${attachment.messageId}:${attachment.id}`);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -388,15 +395,12 @@ export class AttachmentPersistenceService {
|
||||
return true;
|
||||
}
|
||||
|
||||
// The blob always comes from a disk path here, so peers are served from
|
||||
// disk and no original-file copy is cached; caching one would keep a second
|
||||
// full copy of the bytes alive for the whole session.
|
||||
private applyAttachmentBlob(attachment: Attachment, blob: Blob): void {
|
||||
attachment.objectUrl = URL.createObjectURL(blob);
|
||||
attachment.available = true;
|
||||
|
||||
this.runtimeStore.setOriginalFile(
|
||||
`${attachment.messageId}:${attachment.id}`,
|
||||
new File([blob], attachment.filename, { type: attachment.mime })
|
||||
);
|
||||
|
||||
this.runtimeStore.touch();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user