From 4070ef6cafd5226084a7caaf196db8ddfa68d1c1 Mon Sep 17 00:00:00 2001 From: Myx Date: Fri, 5 Jun 2026 15:27:33 +0200 Subject: [PATCH] perf: Add ram metric --- electron/app-metrics.ts | 23 + electron/ipc/system.ts | 3 + electron/preload.ts | 9 + .../platform/electron/electron-api.models.ts | 12 + .../electron-app-metrics.rules.spec.ts | 67 +++ .../electron/electron-app-metrics.rules.ts | 46 ++ .../chat-message-composer.component.html | 4 +- .../chat-message-item.component.html | 502 +++++++++--------- .../servers-rail/servers-rail.component.html | 4 +- .../debugging-settings.component.html | 16 + .../debugging-settings.component.ts | 47 +- 11 files changed, 478 insertions(+), 255 deletions(-) create mode 100644 electron/app-metrics.ts create mode 100644 toju-app/src/app/core/platform/electron/electron-app-metrics.rules.spec.ts create mode 100644 toju-app/src/app/core/platform/electron/electron-app-metrics.rules.ts diff --git a/electron/app-metrics.ts b/electron/app-metrics.ts new file mode 100644 index 0000000..ef225fb --- /dev/null +++ b/electron/app-metrics.ts @@ -0,0 +1,23 @@ +import { app } from 'electron'; + +export interface AppMetricsProcessSnapshot { + pid: number; + type: string; + workingSetKb: number | null; +} + +export interface AppMetricsSnapshot { + collectedAt: number; + processes: AppMetricsProcessSnapshot[]; +} + +export function collectAppMetricsSnapshot(): AppMetricsSnapshot { + return { + collectedAt: Date.now(), + processes: app.getAppMetrics().map((metric) => ({ + pid: metric.pid, + type: metric.type, + workingSetKb: metric.memory?.workingSetSize ?? null + })) + }; +} diff --git a/electron/ipc/system.ts b/electron/ipc/system.ts index 170e3e3..a84dc95 100644 --- a/electron/ipc/system.ts +++ b/electron/ipc/system.ts @@ -60,6 +60,7 @@ import { } from '../data-management'; import { listRunningProcessNames } from '../process-list'; import { detectActiveGame } from '../game-detection'; +import { collectAppMetricsSnapshot } from '../app-metrics'; const DEFAULT_MIME_TYPE = 'application/octet-stream'; const MAX_ACTIVE_DESKTOP_NOTIFICATIONS = 20; @@ -362,6 +363,8 @@ export function setupSystemHandlers(): void { return await stopLinuxScreenShareMonitorCapture(captureId); }); + ipcMain.handle('get-app-metrics', () => collectAppMetricsSnapshot()); + ipcMain.handle('get-app-data-path', () => app.getPath('userData')); ipcMain.handle('open-current-data-folder', async () => await openCurrentDataFolder()); ipcMain.handle('export-user-data', async () => await exportUserData()); diff --git a/electron/preload.ts b/electron/preload.ts index f043a54..137a225 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -240,6 +240,14 @@ export interface ElectronAPI { stopLinuxScreenShareMonitorCapture: (captureId?: string) => Promise; onLinuxScreenShareMonitorAudioChunk: (listener: (payload: LinuxScreenShareMonitorAudioChunkPayload) => void) => () => void; onLinuxScreenShareMonitorAudioEnded: (listener: (payload: LinuxScreenShareMonitorAudioEndedPayload) => void) => () => void; + getAppMetrics: () => Promise<{ + collectedAt: number; + processes: { + pid: number; + type: string; + workingSetKb: number | null; + }[]; + }>; getAppDataPath: () => Promise; openCurrentDataFolder: () => Promise; exportUserData: () => Promise; @@ -374,6 +382,7 @@ const electronAPI: ElectronAPI = { ipcRenderer.removeListener(LINUX_SCREEN_SHARE_MONITOR_AUDIO_ENDED_CHANNEL, wrappedListener); }; }, + getAppMetrics: () => ipcRenderer.invoke('get-app-metrics'), getAppDataPath: () => ipcRenderer.invoke('get-app-data-path'), openCurrentDataFolder: () => ipcRenderer.invoke('open-current-data-folder'), exportUserData: () => ipcRenderer.invoke('export-user-data'), diff --git a/toju-app/src/app/core/platform/electron/electron-api.models.ts b/toju-app/src/app/core/platform/electron/electron-api.models.ts index c78f13c..3565888 100644 --- a/toju-app/src/app/core/platform/electron/electron-api.models.ts +++ b/toju-app/src/app/core/platform/electron/electron-api.models.ts @@ -233,6 +233,17 @@ export interface ActiveGameCandidateResult { fallbackProcessNames: string[]; } +export interface ElectronAppMetricsProcess { + pid: number; + type: string; + workingSetKb: number | null; +} + +export interface ElectronAppMetricsSnapshot { + collectedAt: number; + processes: ElectronAppMetricsProcess[]; +} + export interface ElectronApi { linuxDisplayServer: string; minimizeWindow: () => void; @@ -251,6 +262,7 @@ export interface ElectronApi { stopLinuxScreenShareMonitorCapture: (captureId?: string) => Promise; onLinuxScreenShareMonitorAudioChunk: (listener: (payload: LinuxScreenShareMonitorAudioChunkPayload) => void) => () => void; onLinuxScreenShareMonitorAudioEnded: (listener: (payload: LinuxScreenShareMonitorAudioEndedPayload) => void) => () => void; + getAppMetrics: () => Promise; getAppDataPath: () => Promise; openCurrentDataFolder: () => Promise; exportUserData: () => Promise; diff --git a/toju-app/src/app/core/platform/electron/electron-app-metrics.rules.spec.ts b/toju-app/src/app/core/platform/electron/electron-app-metrics.rules.spec.ts new file mode 100644 index 0000000..60a90c7 --- /dev/null +++ b/toju-app/src/app/core/platform/electron/electron-app-metrics.rules.spec.ts @@ -0,0 +1,67 @@ +import { + describe, + expect, + it +} from 'vitest'; + +import { + formatAppRamLabel, + formatKilobytesAsMegabytes, + sumWorkingSetKb +} from './electron-app-metrics.rules'; +import type { ElectronAppMetricsSnapshot } from './electron-app-metrics.rules'; + +function createSnapshot( + processes: ElectronAppMetricsSnapshot['processes'] +): ElectronAppMetricsSnapshot { + return { + collectedAt: 1, + processes + }; +} + +describe('sumWorkingSetKb', () => { + it('sums working set across processes that report memory', () => { + const total = sumWorkingSetKb([{ pid: 1, type: 'Browser', workingSetKb: 1024 }, { pid: 2, type: 'GPU', workingSetKb: 512 }]); + + expect(total).toBe(1536); + }); + + it('ignores processes without memory readings', () => { + const total = sumWorkingSetKb([{ pid: 1, type: 'Browser', workingSetKb: 2048 }, { pid: 2, type: 'Unknown', workingSetKb: null }]); + + expect(total).toBe(2048); + }); + + it('returns null when no process reports memory', () => { + expect(sumWorkingSetKb([{ pid: 1, type: 'Browser', workingSetKb: null }])).toBeNull(); + }); +}); + +describe('formatKilobytesAsMegabytes', () => { + it('rounds large values to whole megabytes', () => { + expect(formatKilobytesAsMegabytes(412 * 1024)).toBe('412 MB'); + }); + + it('keeps one decimal for medium values', () => { + expect(formatKilobytesAsMegabytes(15.4 * 1024)).toBe('15.4 MB'); + }); + + it('keeps two decimals for small values', () => { + expect(formatKilobytesAsMegabytes(1.25 * 1024)).toBe('1.25 MB'); + }); +}); + +describe('formatAppRamLabel', () => { + it('formats total working set as a compact RAM label', () => { + const browser = { pid: 1, type: 'Browser', workingSetKb: 300 * 1024 }; + const gpu = { pid: 2, type: 'GPU', workingSetKb: 112 * 1024 }; + const label = formatAppRamLabel(createSnapshot([browser, gpu])); + + expect(label).toBe('412 MB'); + }); + + it('returns null when metrics contain no memory readings', () => { + expect(formatAppRamLabel(createSnapshot([{ pid: 1, type: 'Browser', workingSetKb: null }]))).toBeNull(); + }); +}); diff --git a/toju-app/src/app/core/platform/electron/electron-app-metrics.rules.ts b/toju-app/src/app/core/platform/electron/electron-app-metrics.rules.ts new file mode 100644 index 0000000..ae050cf --- /dev/null +++ b/toju-app/src/app/core/platform/electron/electron-app-metrics.rules.ts @@ -0,0 +1,46 @@ +export interface ElectronAppMetricsProcess { + pid: number; + type: string; + workingSetKb: number | null; +} + +export interface ElectronAppMetricsSnapshot { + collectedAt: number; + processes: ElectronAppMetricsProcess[]; +} + +export function sumWorkingSetKb(processes: ElectronAppMetricsProcess[]): number | null { + let total = 0; + let hasAny = false; + + for (const process of processes) { + if (process.workingSetKb == null || process.workingSetKb < 0) + continue; + + total += process.workingSetKb; + hasAny = true; + } + + return hasAny ? total : null; +} + +export function formatKilobytesAsMegabytes(kilobytes: number): string { + const megabytes = kilobytes / 1024; + + if (megabytes >= 100) + return `${Math.round(megabytes)} MB`; + + if (megabytes >= 10) + return `${megabytes.toFixed(1)} MB`; + + return `${megabytes.toFixed(2)} MB`; +} + +export function formatAppRamLabel(snapshot: ElectronAppMetricsSnapshot): string | null { + const totalKb = sumWorkingSetKb(snapshot.processes); + + if (totalKb == null) + return null; + + return formatKilobytesAsMegabytes(totalKb); +} diff --git a/toju-app/src/app/domains/chat/feature/chat-messages/components/message-composer/chat-message-composer.component.html b/toju-app/src/app/domains/chat/feature/chat-messages/components/message-composer/chat-message-composer.component.html index d93341e..12fb11a 100644 --- a/toju-app/src/app/domains/chat/feature/chat-messages/components/message-composer/chat-message-composer.component.html +++ b/toju-app/src/app/domains/chat/feature/chat-messages/components/message-composer/chat-message-composer.component.html @@ -335,9 +335,7 @@ > @if (dragActive()) { -
+
Drop files to attach
} diff --git a/toju-app/src/app/domains/chat/feature/chat-messages/components/message-item/chat-message-item.component.html b/toju-app/src/app/domains/chat/feature/chat-messages/components/message-item/chat-message-item.component.html index 0dcc439..d14b05c 100644 --- a/toju-app/src/app/domains/chat/feature/chat-messages/components/message-item/chat-message-item.component.html +++ b/toju-app/src/app/domains/chat/feature/chat-messages/components/message-item/chat-message-item.component.html @@ -198,7 +198,9 @@ name="lucideImage" class="h-5 w-5 text-primary" /> - {{ ((gridImage.receivedBytes || 0) * 100) / gridImage.size | number: '1.0-0' }}% + {{ ((gridImage.receivedBytes || 0) * 100) / gridImage.size | number: '1.0-0' }}%
} @else {
@@ -231,216 +233,249 @@ } @for (att of attachmentsList; track att.id) { @if (shouldShowAttachmentInList(att)) { - @if (isImageLikeAttachment(att) && !imageGridLayout().useGrid) { - @if (att.available && att.objectUrl) { -
- -
-
- - -
-
- } @else if ((att.receivedBytes || 0) > 0) { -
-
-
- -
-
-
{{ att.filename }}
-
{{ formatBytes(att.receivedBytes || 0) }} / {{ formatBytes(att.size) }}
-
-
{{ ((att.receivedBytes || 0) * 100) / att.size | number: '1.0-0' }}%
-
-
-
-
-
- } @else { -
-
-
- -
-
-
{{ att.filename }}
-
{{ formatBytes(att.size) }}
-
- {{ att.requestError || 'Waiting for image source...' }} -
-
-
- -
- } - } @else if (att.isVideo || att.isAudio) { - @if (att.available && att.objectUrl) { - @if (att.isVideo) { - - } @else { - - } - } @else if ((att.receivedBytes || 0) > 0) { -
-
-
-
{{ att.filename }}
-
{{ formatBytes(att.receivedBytes || 0) }} / {{ formatBytes(att.size) }}
-
- -
-
-
-
-
- {{ att.progressPercent | number: '1.0-0' }}% - @if (att.speedBps) { - {{ formatSpeed(att.speedBps) }} - } -
-
- } @else { -
-
-
-
{{ att.filename }}
-
{{ formatBytes(att.size) }}
-
+
+
+ + +
+
+ } @else if ((att.receivedBytes || 0) > 0) { +
+
+
+ +
+
+
{{ att.filename }}
+
{{ formatBytes(att.receivedBytes || 0) }} / {{ formatBytes(att.size) }}
+
+
{{ ((att.receivedBytes || 0) * 100) / att.size | number: '1.0-0' }}%
+
+
+
+
+
+ } @else { +
+
+
+ +
+
+
{{ att.filename }}
+
{{ formatBytes(att.size) }}
+
+ {{ att.requestError || 'Waiting for image source...' }} +
-
- } - } @else { -
-
-
-
{{ att.filename }}
-
{{ formatBytes(att.size) }}
+ } + } @else if (att.isVideo || att.isAudio) { + @if (att.available && att.objectUrl) { + @if (att.isVideo) { + + } @else { + + } + } @else if ((att.receivedBytes || 0) > 0) { +
+
+
+
{{ att.filename }}
+
{{ formatBytes(att.receivedBytes || 0) }} / {{ formatBytes(att.size) }}
+
+ +
+
+
+
+
+ {{ att.progressPercent | number: '1.0-0' }}% + @if (att.speedBps) { + {{ formatSpeed(att.speedBps) }} + } +
-
- @if (!att.isUploader) { - @if (!att.available) { -
-
+ } @else { +
+
+
+
{{ att.filename }}
+
{{ formatBytes(att.size) }}
+
+ {{ att.mediaStatusText }}
-
- {{ att.progressPercent | number: '1.0-0' }}% - @if (att.speedBps) { - • {{ formatSpeed(att.speedBps) }} +
+ +
+
+ } + } @else { +
+
+
+
{{ att.filename }}
+
{{ formatBytes(att.size) }}
+
+
+ @if (!att.isUploader) { + @if (!att.available) { +
+
+
+
+ {{ att.progressPercent | number: '1.0-0' }}% + @if (att.speedBps) { + • {{ formatSpeed(att.speedBps) }} + } +
+ @if (!(att.receivedBytes || 0)) { + + } @else { + } -
- @if (!(att.receivedBytes || 0)) { - } @else { + @if (att.canOpenExternally) { + + } + @if (att.canUseExperimentalPlayer) { + + } } } @else { +
Shared from your device
@if (att.canOpenExternally) { } - } @else { -
Shared from your device
- @if (att.canOpenExternally) { - - } - @if (att.canUseExperimentalPlayer) { - - } - } +
+ @if (!att.available && att.requestError) { +
+ {{ att.requestError }} +
+ }
- @if (!att.available && att.requestError) { -
- {{ att.requestError }} -
- } -
- @if (att.experimentalPlayerActive && att.objectUrl) { - @defer { - - } @loading { -
- Loading experimental player... -
+ @if (att.experimentalPlayerActive && att.objectUrl) { + @defer { + + } @loading { +
+ Loading experimental player... +
+ } } } } - } }
} diff --git a/toju-app/src/app/features/servers/servers-rail/servers-rail.component.html b/toju-app/src/app/features/servers/servers-rail/servers-rail.component.html index 3943e8b..bbb91c1 100644 --- a/toju-app/src/app/features/servers/servers-rail/servers-rail.component.html +++ b/toju-app/src/app/features/servers/servers-rail/servers-rail.component.html @@ -1,4 +1,6 @@ -