From 9005b9f4301fbb3985b507aa7d7b43c79bd94c81 Mon Sep 17 00:00:00 2001 From: sed-i Date: Tue, 6 Jan 2026 03:04:35 +0800 Subject: [PATCH] feat(stage-*): performance overlay & markdown stress test (#838) --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Neko --- apps/stage-tamagotchi/src/main/index.ts | 4 +- .../src/main/windows/devtools/index.ts | 67 +++ .../src/main/windows/settings/index.ts | 9 +- .../windows/settings/rpc/index.electron.ts | 13 +- apps/stage-tamagotchi/src/renderer/App.vue | 2 + .../src/renderer/layouts/settings.vue | 4 + .../pages/devtools/lag-visualizer.vue | 24 + .../pages/settings/system/developer.vue | 21 +- apps/stage-tamagotchi/src/shared/eventa.ts | 1 + apps/stage-web/package.json | 1 + apps/stage-web/src/App.vue | 4 + .../Devtools/PerformanceOverlay.vue | 127 +++++ .../composables/perf/register-lag-sampler.ts | 128 +++++ .../pages/devtools/performance-visualizer.vue | 116 +++++ .../src/pages/settings/system/developer.vue | 12 + apps/stage-web/src/stores/devtools-lag.ts | 273 ++++++++++ packages/i18n/src/locales/en/settings.yaml | 6 + .../src/locales/en/tamagotchi/settings.yaml | 8 + .../stage-layouts/src/layouts/settings.vue | 8 + .../src/pages/devtools/markdown-stress.vue | 159 ++++++ packages/stage-shared/src/export-csv.ts | 28 ++ packages/stage-shared/src/index.ts | 2 + packages/stage-shared/src/perf/tracer.ts | 154 ++++++ packages/stage-ui/src/composables/markdown.ts | 90 +++- packages/stage-ui/src/stores/chat.ts | 45 +- .../stage-ui/src/stores/markdown-stress.ts | 467 ++++++++++++++++++ .../src/stores/mods/api/context-bridge.ts | 36 +- .../stage-ui/src/stores/perf-tracer-bridge.ts | 142 ++++++ pnpm-lock.yaml | 3 + 29 files changed, 1918 insertions(+), 36 deletions(-) create mode 100644 apps/stage-tamagotchi/src/main/windows/devtools/index.ts create mode 100644 apps/stage-tamagotchi/src/renderer/pages/devtools/lag-visualizer.vue create mode 100644 apps/stage-web/src/components/Devtools/PerformanceOverlay.vue create mode 100644 apps/stage-web/src/composables/perf/register-lag-sampler.ts create mode 100644 apps/stage-web/src/pages/devtools/performance-visualizer.vue create mode 100644 apps/stage-web/src/stores/devtools-lag.ts create mode 100644 packages/stage-pages/src/pages/devtools/markdown-stress.vue create mode 100644 packages/stage-shared/src/export-csv.ts create mode 100644 packages/stage-shared/src/perf/tracer.ts create mode 100644 packages/stage-ui/src/stores/markdown-stress.ts create mode 100644 packages/stage-ui/src/stores/perf-tracer-bridge.ts diff --git a/apps/stage-tamagotchi/src/main/index.ts b/apps/stage-tamagotchi/src/main/index.ts index 33f503cd5..d5d387edd 100644 --- a/apps/stage-tamagotchi/src/main/index.ts +++ b/apps/stage-tamagotchi/src/main/index.ts @@ -20,6 +20,7 @@ import { setupAboutWindowReusable } from './windows/about' import { setupBeatSync } from './windows/beat-sync' import { setupCaptionWindowManager } from './windows/caption' import { setupChatWindowReusableFunc } from './windows/chat' +import { setupDevtoolsWindow } from './windows/devtools' import { setupMainWindow } from './windows/main' import { setupNoticeWindowManager } from './windows/notice' import { setupSettingsWindowReusableFunc } from './windows/settings' @@ -82,6 +83,7 @@ app.whenReady().then(async () => { // BeatSync will create a background window to capture and process audio. const beatSync = injeca.provide('windows:beat-sync', () => setupBeatSync()) + const devtoolsMarkdownStressWindow = injeca.provide('windows:devtools:markdown-stress', () => setupDevtoolsWindow()) const chatWindow = injeca.provide('windows:chat', { dependsOn: { widgetsManager }, @@ -89,7 +91,7 @@ app.whenReady().then(async () => { }) const settingsWindow = injeca.provide('windows:settings', { - dependsOn: { widgetsManager, beatSync, autoUpdater }, + dependsOn: { widgetsManager, beatSync, autoUpdater, devtoolsMarkdownStressWindow }, build: async ({ dependsOn }) => setupSettingsWindowReusableFunc(dependsOn), }) diff --git a/apps/stage-tamagotchi/src/main/windows/devtools/index.ts b/apps/stage-tamagotchi/src/main/windows/devtools/index.ts new file mode 100644 index 000000000..c08d0255d --- /dev/null +++ b/apps/stage-tamagotchi/src/main/windows/devtools/index.ts @@ -0,0 +1,67 @@ +import { join, resolve } from 'node:path' + +import { BrowserWindow, shell } from 'electron' + +import icon from '../../../../resources/icon.png?asset' + +import { baseUrl, getElectronMainDirname, load, withHashRoute } from '../../libs/electron/location' +import { createReusableWindow } from '../../libs/electron/window-manager' + +export interface DevtoolsWindowManager { + openWindow: (route?: string) => Promise + getWindow: () => Promise +} + +export function setupDevtoolsWindow(): DevtoolsWindowManager { + const rendererBase = baseUrl(resolve(getElectronMainDirname(), '..', 'renderer')) + const defaultRoute = '/devtools/markdown-stress' + let currentRoute = defaultRoute + + const reusable = createReusableWindow(async () => { + const window = new BrowserWindow({ + title: 'Devtools', + width: 1020, + height: 720, + minWidth: 640, + minHeight: 480, + show: false, + icon, + webPreferences: { + preload: join(__dirname, '../preload/index.mjs'), + // Preload exposes Electron APIs and needs Node access. + sandbox: false, + }, + }) + + window.on('ready-to-show', () => window.show()) + window.webContents.setWindowOpenHandler((details) => { + shell.openExternal(details.url) + return { action: 'deny' } + }) + + await load(window, withHashRoute(rendererBase, currentRoute)) + return window + }) + + async function openWindow(route?: string) { + if (route) + currentRoute = route + + const window = await reusable.getWindow() + const targetRoute = route ?? currentRoute + const url = withHashRoute(rendererBase, targetRoute) + + // If the route changes while the window is open, reload to that route. + const currentUrl = window.webContents.getURL() + if (!currentUrl.includes(`#${targetRoute}`)) { + await load(window, url) + } + + return window + } + + return { + openWindow, + getWindow: reusable.getWindow, + } +} diff --git a/apps/stage-tamagotchi/src/main/windows/settings/index.ts b/apps/stage-tamagotchi/src/main/windows/settings/index.ts index 37da29805..485cdd256 100644 --- a/apps/stage-tamagotchi/src/main/windows/settings/index.ts +++ b/apps/stage-tamagotchi/src/main/windows/settings/index.ts @@ -1,4 +1,5 @@ import type { AutoUpdater } from '../../services/electron/auto-updater' +import type { DevtoolsWindowManager } from '../devtools' import type { WidgetsWindowManager } from '../widgets' import { join, resolve } from 'node:path' @@ -14,6 +15,7 @@ import { setupSettingsWindowInvokes } from './rpc/index.electron' export function setupSettingsWindowReusableFunc(params: { widgetsManager: WidgetsWindowManager autoUpdater: AutoUpdater + devtoolsMarkdownStressWindow: DevtoolsWindowManager onWindowCreated?: (window: BrowserWindow) => void }) { return createReusableWindow(async () => { @@ -40,7 +42,12 @@ export function setupSettingsWindowReusableFunc(params: { }) await load(window, withHashRoute(baseUrl(resolve(getElectronMainDirname(), '..', 'renderer')), '/settings')) - await setupSettingsWindowInvokes({ settingsWindow: window, widgetsManager: params.widgetsManager, autoUpdater: params.autoUpdater }) + await setupSettingsWindowInvokes({ + settingsWindow: window, + widgetsManager: params.widgetsManager, + autoUpdater: params.autoUpdater, + devtoolsMarkdownStressWindow: params.devtoolsMarkdownStressWindow, + }) return window }).getWindow diff --git a/apps/stage-tamagotchi/src/main/windows/settings/rpc/index.electron.ts b/apps/stage-tamagotchi/src/main/windows/settings/rpc/index.electron.ts index 5c087d7ee..5040e18a4 100644 --- a/apps/stage-tamagotchi/src/main/windows/settings/rpc/index.electron.ts +++ b/apps/stage-tamagotchi/src/main/windows/settings/rpc/index.electron.ts @@ -1,17 +1,23 @@ import type { BrowserWindow } from 'electron' import type { AutoUpdater } from '../../../services/electron/auto-updater' +import type { DevtoolsWindowManager } from '../../devtools' import type { WidgetsWindowManager } from '../../widgets' import { defineInvokeHandler } from '@moeru/eventa' import { createContext } from '@moeru/eventa/adapters/electron/main' import { ipcMain } from 'electron' -import { electronOpenSettingsDevtools } from '../../../../shared/eventa' +import { electronOpenDevtoolsWindow, electronOpenSettingsDevtools } from '../../../../shared/eventa' import { createWidgetsService } from '../../../services/airi/widgets' import { createAutoUpdaterService, createScreenService, createWindowService } from '../../../services/electron' -export async function setupSettingsWindowInvokes(params: { settingsWindow: BrowserWindow, widgetsManager: WidgetsWindowManager, autoUpdater: AutoUpdater }) { +export async function setupSettingsWindowInvokes(params: { + settingsWindow: BrowserWindow + widgetsManager: WidgetsWindowManager + autoUpdater: AutoUpdater + devtoolsMarkdownStressWindow: DevtoolsWindowManager +}) { // TODO: once we refactored eventa to support window-namespaced contexts, // we can remove the setMaxListeners call below since eventa will be able to dispatch and // manage events within eventa's context system. @@ -25,4 +31,7 @@ export async function setupSettingsWindowInvokes(params: { settingsWindow: Brows createAutoUpdaterService({ context, window: params.settingsWindow, service: params.autoUpdater }) defineInvokeHandler(context, electronOpenSettingsDevtools, async () => params.settingsWindow.webContents.openDevTools({ mode: 'detach' })) + defineInvokeHandler(context, electronOpenDevtoolsWindow, async (payload) => { + await params.devtoolsMarkdownStressWindow.openWindow(payload?.route) + }) } diff --git a/apps/stage-tamagotchi/src/renderer/App.vue b/apps/stage-tamagotchi/src/renderer/App.vue index 0d26d7ac1..8afbe66d6 100644 --- a/apps/stage-tamagotchi/src/renderer/App.vue +++ b/apps/stage-tamagotchi/src/renderer/App.vue @@ -8,6 +8,7 @@ import { useModsServerChannelStore } from '@proj-airi/stage-ui/stores/mods/api/c import { useContextBridgeStore } from '@proj-airi/stage-ui/stores/mods/api/context-bridge' import { useAiriCardStore } from '@proj-airi/stage-ui/stores/modules/airi-card' import { useOnboardingStore } from '@proj-airi/stage-ui/stores/onboarding' +import { usePerfTracerBridgeStore } from '@proj-airi/stage-ui/stores/perf-tracer-bridge' import { useSettings } from '@proj-airi/stage-ui/stores/settings' import { useTheme } from '@proj-airi/ui' import { storeToRefs } from 'pinia' @@ -31,6 +32,7 @@ const cardStore = useAiriCardStore() const serverChannelStore = useModsServerChannelStore() const characterOrchestratorStore = useCharacterOrchestratorStore() const analyticsStore = useSharedAnalyticsStore() +usePerfTracerBridgeStore() watch(language, () => { i18n.locale.value = language.value diff --git a/apps/stage-tamagotchi/src/renderer/layouts/settings.vue b/apps/stage-tamagotchi/src/renderer/layouts/settings.vue index 81d49b1a2..60889ae05 100644 --- a/apps/stage-tamagotchi/src/renderer/layouts/settings.vue +++ b/apps/stage-tamagotchi/src/renderer/layouts/settings.vue @@ -120,6 +120,10 @@ const routeHeaderMetadataMap = computed(() => { subtitle: t('tamagotchi.settings.devtools.title'), title: t('tamagotchi.settings.devtools.pages.widgets-calling.title'), }, + '/devtools/markdown-stress': { + subtitle: t('tamagotchi.settings.devtools.title'), + title: t('tamagotchi.settings.devtools.pages.markdown-stress.title'), + }, '/devtools/context-flow': { subtitle: t('tamagotchi.settings.devtools.title'), title: t('tamagotchi.settings.devtools.pages.context-flow.title'), diff --git a/apps/stage-tamagotchi/src/renderer/pages/devtools/lag-visualizer.vue b/apps/stage-tamagotchi/src/renderer/pages/devtools/lag-visualizer.vue new file mode 100644 index 000000000..284507e54 --- /dev/null +++ b/apps/stage-tamagotchi/src/renderer/pages/devtools/lag-visualizer.vue @@ -0,0 +1,24 @@ + + + + + +meta: + layout: settings + diff --git a/apps/stage-tamagotchi/src/renderer/pages/settings/system/developer.vue b/apps/stage-tamagotchi/src/renderer/pages/settings/system/developer.vue index 7ad82a6ec..207ad0f2f 100644 --- a/apps/stage-tamagotchi/src/renderer/pages/settings/system/developer.vue +++ b/apps/stage-tamagotchi/src/renderer/pages/settings/system/developer.vue @@ -4,7 +4,7 @@ import { useSettings } from '@proj-airi/stage-ui/stores/settings' import { computed } from 'vue' import { useI18n } from 'vue-i18n' -import { electronOpenMainDevtools } from '../../../../shared/eventa' +import { electronOpenDevtoolsWindow, electronOpenMainDevtools } from '../../../../shared/eventa' import { useElectronEventaInvoke } from '../../../composables/electron-vueuse' const { t } = useI18n() @@ -62,6 +62,7 @@ const menu = computed(() => [ ]) const openDevTools = useElectronEventaInvoke(electronOpenMainDevtools) +const openMarkdownStressWindow = useElectronEventaInvoke(electronOpenDevtoolsWindow)