mirror of
https://github.com/Boof2015/prism.git
synced 2026-08-12 13:20:53 +02:00
logging
This commit is contained in:
@@ -22,6 +22,7 @@ import type {
|
||||
ThemeLibrarySnapshot,
|
||||
} from '../types/theme'
|
||||
import { RESIZE_DIRECTIONS, type ResizeDirection } from '../types/windowResize'
|
||||
import type { PerformanceMemorySnapshot } from '../types/performance'
|
||||
import { normalizeProfile } from '../shared/profileState'
|
||||
import { calculateResizedWindowBounds } from '../shared/windowResize'
|
||||
import { FileBackedProfileLibrary } from './profileLibrary'
|
||||
@@ -70,6 +71,25 @@ const POPOUT_DEFAULTS = {
|
||||
minHeight: 160,
|
||||
}
|
||||
|
||||
function kilobytesToMegabytes(value: number | undefined): number {
|
||||
return Math.round((((value ?? 0) / 1024) * 10)) / 10
|
||||
}
|
||||
|
||||
function workingSetForMetric(metric: Electron.ProcessMetric | undefined): number {
|
||||
return kilobytesToMegabytes(metric?.memory.workingSetSize)
|
||||
}
|
||||
|
||||
function sumWorkingSet(metrics: Electron.ProcessMetric[], predicate: (metric: Electron.ProcessMetric) => boolean): number {
|
||||
let totalKilobytes = 0
|
||||
for (const metric of metrics) {
|
||||
if (!predicate(metric)) {
|
||||
continue
|
||||
}
|
||||
totalKilobytes += metric.memory.workingSetSize
|
||||
}
|
||||
return kilobytesToMegabytes(totalKilobytes)
|
||||
}
|
||||
|
||||
function getProfileLibrary(): FileBackedProfileLibrary {
|
||||
if (!profileLibrary) {
|
||||
profileLibrary = new FileBackedProfileLibrary(
|
||||
@@ -903,6 +923,25 @@ function setupIPC(): void {
|
||||
return getWindowFromSender(event.sender)?.isAlwaysOnTop() ?? true
|
||||
})
|
||||
|
||||
ipcMain.handle('performance:get-memory-snapshot', async (event): Promise<PerformanceMemorySnapshot> => {
|
||||
const metrics = app.getAppMetrics()
|
||||
const senderPid = event.sender.getOSProcessId()
|
||||
const browserMetric = metrics.find((metric) => metric.type === 'Browser')
|
||||
const rendererMetric = metrics.find((metric) => metric.pid === senderPid)
|
||||
|
||||
return {
|
||||
capturedAt: Date.now(),
|
||||
appMb: sumWorkingSet(metrics, () => true),
|
||||
mainMb: workingSetForMetric(browserMetric),
|
||||
rendererMb: workingSetForMetric(rendererMetric),
|
||||
rendererPrivateMb: null,
|
||||
gpuMb: sumWorkingSet(metrics, (metric) => metric.type === 'GPU'),
|
||||
utilityMb: sumWorkingSet(metrics, (metric) => metric.type === 'Utility'),
|
||||
jsHeapUsedMb: null,
|
||||
jsHeapLimitMb: null,
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('audio:get-desktop-sources', async () => {
|
||||
const sources = await desktopCapturer.getSources({ types: ['screen'] })
|
||||
return sources.map((s) => ({ id: s.id, name: s.name }))
|
||||
|
||||
@@ -28,9 +28,14 @@ import type {
|
||||
} from '../types/theme'
|
||||
import type { ResizeDirection } from '../types/windowResize'
|
||||
import type { VisualizerDSP } from '../renderer/audio/native/visualizer-dsp'
|
||||
import type { PerformanceMemorySnapshot } from '../types/performance'
|
||||
|
||||
type NativeAddonModule = VisualizerDSP & NativeCaptureAPI
|
||||
|
||||
function kilobytesToMegabytes(value: number | undefined): number {
|
||||
return Math.round((((value ?? 0) / 1024) * 10)) / 10
|
||||
}
|
||||
|
||||
// Expose Electron API to renderer
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
platform: process.platform,
|
||||
@@ -45,6 +50,18 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
repositionWindow: (position: 'top' | 'bottom') => ipcRenderer.send('window:reposition', position),
|
||||
toggleAlwaysOnTop: () => ipcRenderer.send('window:toggle-always-on-top'),
|
||||
isAlwaysOnTop: () => ipcRenderer.invoke('window:is-always-on-top'),
|
||||
getPerformanceMemorySnapshot: async () => {
|
||||
const snapshot = await ipcRenderer.invoke('performance:get-memory-snapshot') as PerformanceMemorySnapshot
|
||||
try {
|
||||
const rendererMemory = await process.getProcessMemoryInfo()
|
||||
return {
|
||||
...snapshot,
|
||||
rendererPrivateMb: kilobytesToMegabytes(rendererMemory.private),
|
||||
} satisfies PerformanceMemorySnapshot
|
||||
} catch {
|
||||
return snapshot
|
||||
}
|
||||
},
|
||||
getDesktopSources: () => ipcRenderer.invoke('audio:get-desktop-sources') as Promise<{ id: string; name: string }[]>,
|
||||
getCaptureBackendSupport: async () => {
|
||||
const support = await ipcRenderer.invoke('capture:get-backend-support') as CaptureBackendSupport
|
||||
|
||||
@@ -8,6 +8,7 @@ import WindowResizeOverlay from './components/WindowResizeOverlay'
|
||||
import { useSettingsStore } from './stores/settingsStore'
|
||||
import { useAstraStore } from './stores/astraStore'
|
||||
import { useAudioStore } from './stores/audioStore'
|
||||
import { usePerformanceStore } from './stores/performanceStore'
|
||||
import { useThemeStore } from './stores/themeStore'
|
||||
import { SCOPE_KINDS } from '../types/scope'
|
||||
|
||||
@@ -27,6 +28,8 @@ export default function App(): JSX.Element {
|
||||
const guardProfileTransition = useSettingsStore((s) => s.guardProfileTransition)
|
||||
const importProfileFromPath = useSettingsStore((s) => s.importProfileFromPath)
|
||||
const updateMainWindowBounds = useSettingsStore((s) => s.updateMainWindowBounds)
|
||||
const startMemoryMonitoring = usePerformanceStore((s) => s.startMemoryMonitoring)
|
||||
const stopMemoryMonitoring = usePerformanceStore((s) => s.stopMemoryMonitoring)
|
||||
const initializeThemes = useThemeStore((s) => s.initializeThemes)
|
||||
const applyExternalThemeSnapshot = useThemeStore((s) => s.applyExternalThemeSnapshot)
|
||||
const initializeAstra = useAstraStore((s) => s.initialize)
|
||||
@@ -102,6 +105,13 @@ export default function App(): JSX.Element {
|
||||
updateMainWindowBounds,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
startMemoryMonitoring()
|
||||
return () => {
|
||||
stopMemoryMonitoring()
|
||||
}
|
||||
}, [startMemoryMonitoring, stopMemoryMonitoring])
|
||||
|
||||
const measuredSettingsHeight = settingsPanelHeight > 0 && bottomBarHeight > 0
|
||||
? settingsPanelHeight + bottomBarHeight
|
||||
: DEFAULT_SETTINGS_HEIGHT
|
||||
|
||||
@@ -46,6 +46,9 @@ export default function BottomBar({ onClose, onHeightChange }: BottomBarProps):
|
||||
const toggleScope = useSettingsStore((s) => s.toggleScope)
|
||||
const frameTarget = usePerformanceStore((s) => s.frameTarget)
|
||||
const dockedRenderFps = usePerformanceStore((s) => s.dockedRenderFps)
|
||||
const memorySample = usePerformanceStore((s) => s.memorySample)
|
||||
const rendererMemoryDeltaMb = usePerformanceStore((s) => s.rendererMemoryDeltaMb)
|
||||
const appMemoryDeltaMb = usePerformanceStore((s) => s.appMemoryDeltaMb)
|
||||
const setFrameTarget = usePerformanceStore((s) => s.setFrameTarget)
|
||||
const themeId = useSettingsStore((s) => s.themeId)
|
||||
const setThemeId = useSettingsStore((s) => s.setThemeId)
|
||||
@@ -155,6 +158,9 @@ export default function BottomBar({ onClose, onHeightChange }: BottomBarProps):
|
||||
const trimPercent = Math.min(100, Math.max(0, ((inputGainDb + 12) / 24) * 100))
|
||||
const roundedDockedRenderFps = Math.max(0, Math.round(dockedRenderFps))
|
||||
const themeEntries = Object.entries(themes)
|
||||
const rendererMemoryMb = memorySample?.rendererPrivateMb ?? memorySample?.rendererMb ?? 0
|
||||
const jsHeapUsedMb = memorySample?.jsHeapUsedMb ?? null
|
||||
const jsHeapLimitMb = memorySample?.jsHeapLimitMb ?? null
|
||||
|
||||
const handleThemeChange = async (value: string): Promise<void> => {
|
||||
await loadTheme(value)
|
||||
@@ -435,6 +441,28 @@ export default function BottomBar({ onClose, onHeightChange }: BottomBarProps):
|
||||
<span>{roundedDockedRenderFps} FPS</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{memorySample ? (
|
||||
<div className="bottom-bar__inline bottom-bar__inline--memory">
|
||||
<div className="settings-status-pill bottom-bar__memory-pill" title="Whole-app working set from Electron process metrics">
|
||||
<span>App {memorySample.appMb.toFixed(1)} MB</span>
|
||||
</div>
|
||||
<div className="settings-status-pill bottom-bar__memory-pill" title="Current renderer private memory when available, otherwise working set">
|
||||
<span>Renderer {rendererMemoryMb.toFixed(1)} MB</span>
|
||||
</div>
|
||||
<div className="settings-status-pill bottom-bar__memory-pill" title="Growth since the first sample in this session">
|
||||
<span>ΔR {rendererMemoryDeltaMb >= 0 ? '+' : ''}{rendererMemoryDeltaMb.toFixed(1)} MB</span>
|
||||
</div>
|
||||
<div className="settings-status-pill bottom-bar__memory-pill" title="Whole-app growth since the first sample in this session">
|
||||
<span>ΔA {appMemoryDeltaMb >= 0 ? '+' : ''}{appMemoryDeltaMb.toFixed(1)} MB</span>
|
||||
</div>
|
||||
{jsHeapUsedMb !== null ? (
|
||||
<div className="settings-status-pill bottom-bar__memory-pill" title={jsHeapLimitMb !== null ? `Renderer JS heap (${jsHeapUsedMb.toFixed(1)} / ${jsHeapLimitMb.toFixed(1)} MB)` : 'Renderer JS heap'}>
|
||||
<span>JS {jsHeapUsedMb.toFixed(1)} MB</span>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
Vendored
+2
@@ -29,6 +29,7 @@ import type {
|
||||
ThemeLibrarySnapshot,
|
||||
} from '../types/theme'
|
||||
import type { ResizeDirection } from '../types/windowResize'
|
||||
import type { PerformanceMemorySnapshot } from '../types/performance'
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
@@ -47,6 +48,7 @@ declare global {
|
||||
repositionWindow: (position: 'top' | 'bottom') => void
|
||||
toggleAlwaysOnTop: () => void
|
||||
isAlwaysOnTop: () => Promise<boolean>
|
||||
getPerformanceMemorySnapshot: () => Promise<PerformanceMemorySnapshot>
|
||||
getDesktopSources: () => Promise<{ id: string; name: string }[]>
|
||||
getCaptureBackendSupport: () => Promise<CaptureBackendSupport>
|
||||
getAstraConfig: () => Promise<AstraIntegrationConfig>
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { create } from 'zustand'
|
||||
import { isVisualizerFrameTarget, type VisualizerFrameTarget } from '../../types/performance'
|
||||
import {
|
||||
isVisualizerFrameTarget,
|
||||
type PerformanceMemorySnapshot,
|
||||
type VisualizerFrameTarget,
|
||||
} from '../../types/performance'
|
||||
|
||||
const STORAGE_KEY = 'prism:performance'
|
||||
const SYNC_CHANNEL_NAME = 'prism:performance'
|
||||
@@ -11,8 +15,14 @@ interface PersistedPerformanceState {
|
||||
interface PerformanceState {
|
||||
frameTarget: VisualizerFrameTarget
|
||||
dockedRenderFps: number
|
||||
memorySample: PerformanceMemorySnapshot | null
|
||||
memoryHistory: PerformanceMemorySnapshot[]
|
||||
rendererMemoryDeltaMb: number
|
||||
appMemoryDeltaMb: number
|
||||
setFrameTarget: (target: VisualizerFrameTarget) => void
|
||||
setDockedRenderFps: (fps: number) => void
|
||||
startMemoryMonitoring: () => void
|
||||
stopMemoryMonitoring: () => void
|
||||
}
|
||||
|
||||
interface StorageLike {
|
||||
@@ -66,10 +76,110 @@ function persistPerformancePreferences(target: VisualizerFrameTarget, storage =
|
||||
}
|
||||
|
||||
const storedPreferences = loadPerformancePreferences()
|
||||
const MEMORY_SAMPLE_INTERVAL_MS = 5000
|
||||
const MEMORY_HISTORY_LIMIT = 720
|
||||
|
||||
interface ChromiumPerformanceMemory {
|
||||
jsHeapSizeLimit: number
|
||||
totalJSHeapSize: number
|
||||
usedJSHeapSize: number
|
||||
}
|
||||
|
||||
let memoryMonitorTimer: ReturnType<typeof setInterval> | null = null
|
||||
let memoryMonitorRefCount = 0
|
||||
let memoryMonitorInFlight = false
|
||||
|
||||
function isMainWindowContext(): boolean {
|
||||
if (typeof window === 'undefined') {
|
||||
return false
|
||||
}
|
||||
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
return params.get('window') !== 'scope-popout'
|
||||
}
|
||||
|
||||
function bytesToMegabytes(value: number | undefined): number {
|
||||
return Math.round((((value ?? 0) / (1024 * 1024)) * 10)) / 10
|
||||
}
|
||||
|
||||
async function collectMemorySnapshot(): Promise<PerformanceMemorySnapshot | null> {
|
||||
if (typeof window === 'undefined' || typeof window.electronAPI?.getPerformanceMemorySnapshot !== 'function') {
|
||||
return null
|
||||
}
|
||||
|
||||
const snapshot = await window.electronAPI.getPerformanceMemorySnapshot()
|
||||
const performanceWithMemory = performance as Performance & { memory?: ChromiumPerformanceMemory }
|
||||
const heap = performanceWithMemory.memory
|
||||
return {
|
||||
...snapshot,
|
||||
jsHeapUsedMb: heap ? bytesToMegabytes(heap.usedJSHeapSize) : null,
|
||||
jsHeapLimitMb: heap ? bytesToMegabytes(heap.jsHeapSizeLimit) : null,
|
||||
}
|
||||
}
|
||||
|
||||
async function sampleMemory(): Promise<void> {
|
||||
if (memoryMonitorInFlight) {
|
||||
return
|
||||
}
|
||||
|
||||
memoryMonitorInFlight = true
|
||||
try {
|
||||
const snapshot = await collectMemorySnapshot()
|
||||
if (!snapshot) {
|
||||
return
|
||||
}
|
||||
|
||||
usePerformanceStore.setState((state) => {
|
||||
const baseline = state.memoryHistory[0] ?? snapshot
|
||||
const nextHistory = [...state.memoryHistory, snapshot]
|
||||
if (nextHistory.length > MEMORY_HISTORY_LIMIT) {
|
||||
nextHistory.splice(0, nextHistory.length - MEMORY_HISTORY_LIMIT)
|
||||
}
|
||||
|
||||
const baselineRendererMb = baseline.rendererPrivateMb ?? baseline.rendererMb
|
||||
const currentRendererMb = snapshot.rendererPrivateMb ?? snapshot.rendererMb
|
||||
|
||||
return {
|
||||
memorySample: snapshot,
|
||||
memoryHistory: nextHistory,
|
||||
rendererMemoryDeltaMb: Math.round((currentRendererMb - baselineRendererMb) * 10) / 10,
|
||||
appMemoryDeltaMb: Math.round((snapshot.appMb - baseline.appMb) * 10) / 10,
|
||||
}
|
||||
})
|
||||
} finally {
|
||||
memoryMonitorInFlight = false
|
||||
}
|
||||
}
|
||||
|
||||
function ensureMemoryMonitor(): void {
|
||||
if (memoryMonitorTimer || !isMainWindowContext()) {
|
||||
return
|
||||
}
|
||||
|
||||
void sampleMemory()
|
||||
memoryMonitorTimer = setInterval(() => {
|
||||
void sampleMemory()
|
||||
}, MEMORY_SAMPLE_INTERVAL_MS)
|
||||
}
|
||||
|
||||
function releaseMemoryMonitor(): void {
|
||||
if (memoryMonitorRefCount > 0) {
|
||||
return
|
||||
}
|
||||
|
||||
if (memoryMonitorTimer) {
|
||||
clearInterval(memoryMonitorTimer)
|
||||
memoryMonitorTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
export const usePerformanceStore = create<PerformanceState>((set) => ({
|
||||
frameTarget: storedPreferences.frameTarget,
|
||||
dockedRenderFps: 0,
|
||||
memorySample: null,
|
||||
memoryHistory: [],
|
||||
rendererMemoryDeltaMb: 0,
|
||||
appMemoryDeltaMb: 0,
|
||||
|
||||
setFrameTarget: (target: VisualizerFrameTarget) => {
|
||||
persistPerformancePreferences(target)
|
||||
@@ -87,6 +197,16 @@ export const usePerformanceStore = create<PerformanceState>((set) => ({
|
||||
return { ...state, dockedRenderFps: nextFps }
|
||||
})
|
||||
},
|
||||
|
||||
startMemoryMonitoring: () => {
|
||||
memoryMonitorRefCount += 1
|
||||
ensureMemoryMonitor()
|
||||
},
|
||||
|
||||
stopMemoryMonitoring: () => {
|
||||
memoryMonitorRefCount = Math.max(0, memoryMonitorRefCount - 1)
|
||||
releaseMemoryMonitor()
|
||||
},
|
||||
}))
|
||||
|
||||
let syncChannel: BroadcastChannel | null = null
|
||||
|
||||
@@ -1414,6 +1414,12 @@ select {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.bottom-bar__inline--memory {
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.bottom-bar__divider {
|
||||
width: 1px;
|
||||
align-self: stretch;
|
||||
@@ -1483,6 +1489,13 @@ select {
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
.bottom-bar__memory-pill {
|
||||
justify-content: center;
|
||||
color: var(--text-primary);
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.bottom-bar__error-text {
|
||||
margin-top: 0;
|
||||
max-width: 360px;
|
||||
|
||||
@@ -2,6 +2,18 @@ export const VISUALIZER_FRAME_TARGETS = [10, 30, 60, 120, 144, 'display-sync'] a
|
||||
|
||||
export type VisualizerFrameTarget = typeof VISUALIZER_FRAME_TARGETS[number]
|
||||
|
||||
export interface PerformanceMemorySnapshot {
|
||||
capturedAt: number
|
||||
appMb: number
|
||||
mainMb: number
|
||||
rendererMb: number
|
||||
rendererPrivateMb: number | null
|
||||
gpuMb: number
|
||||
utilityMb: number
|
||||
jsHeapUsedMb: number | null
|
||||
jsHeapLimitMb: number | null
|
||||
}
|
||||
|
||||
export function isVisualizerFrameTarget(value: unknown): value is VisualizerFrameTarget {
|
||||
return VISUALIZER_FRAME_TARGETS.includes(value as VisualizerFrameTarget)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user