From 8624ac20fa5f99e4d5d64166d2682c0f51267445 Mon Sep 17 00:00:00 2001 From: pa Date: Sun, 15 Mar 2026 21:04:02 +0900 Subject: [PATCH] refactor --- src/plugins/piniaActionTrail.js | 135 ------------------ src/plugins/rendererMemoryReport.js | 50 +++++++ src/plugins/sentry.js | 2 +- src/stores/favorite.js | 12 +- src/stores/index.js | 9 -- src/stores/vrcx.js | 19 +-- .../Sidebar/components/FriendsSidebar.vue | 22 ++- 7 files changed, 68 insertions(+), 181 deletions(-) delete mode 100644 src/plugins/piniaActionTrail.js create mode 100644 src/plugins/rendererMemoryReport.js diff --git a/src/plugins/piniaActionTrail.js b/src/plugins/piniaActionTrail.js deleted file mode 100644 index 5434569d..00000000 --- a/src/plugins/piniaActionTrail.js +++ /dev/null @@ -1,135 +0,0 @@ -import dayjs from 'dayjs'; - -const STORAGE_KEY = 'vrcx:sentry:piniaActions'; -const DEFAULT_MAX_ENTRIES = 200; - -function getStorage() { - try { - return localStorage; - } catch { - return null; - } -} - -function safeJsonParse(value) { - if (!value) return null; - try { - return JSON.parse(value); - } catch { - return null; - } -} - -function safeJsonStringify(value, fallback = '[]') { - try { - return JSON.stringify(value); - } catch { - return fallback; - } -} - -export function getPiniaActionTrail() { - const storage = getStorage(); - if (!storage) return []; - - const data = safeJsonParse(storage.getItem(STORAGE_KEY)); - return Array.isArray(data) ? data : []; -} - -export function clearPiniaActionTrail() { - const storage = getStorage(); - if (!storage) return; - storage.removeItem(STORAGE_KEY); -} - -export function appendPiniaActionTrail(entry, options) { - const storage = getStorage(); - if (!storage) return; - - const maxEntries = options?.maxEntries ?? DEFAULT_MAX_ENTRIES; - - const existing = getPiniaActionTrail(); - existing.push(entry); - - if (existing.length > maxEntries) { - existing.splice(0, existing.length - maxEntries); - } - - try { - storage.setItem(STORAGE_KEY, safeJsonStringify(existing)); - } catch { - // ignore - } -} - -export function createPiniaActionTrailPlugin(options) { - const maxEntries = options?.maxEntries ?? DEFAULT_MAX_ENTRIES; - return ({ store }) => { - store.$onAction(({ name }) => { - appendPiniaActionTrail( - { - t: dayjs().format('HH:mm:ss'), - a: name - }, - { maxEntries } - ); - }); - }; -} - -function readPerformanceMemory() { - // @ts-ignore - const memory = window?.performance?.memory; - if (!memory) return null; - const { usedJSHeapSize, jsHeapSizeLimit } = memory; - if ( - typeof usedJSHeapSize !== 'number' || - typeof jsHeapSizeLimit !== 'number' - ) { - return null; - } - return { usedJSHeapSize, jsHeapSizeLimit }; -} - -export function startRendererMemoryThresholdReport( - Sentry, - { intervalMs = 10_000, thresholdRatio = 0.8, cooldownMs = 5 * 60_000 } = {} -) { - const initial = readPerformanceMemory(); - if (!initial) return null; - - if (!Sentry?.withScope) return null; - - let lastSent = 0; - - return setInterval(() => { - const m = readPerformanceMemory(); - if (!m) return; - - const ratio = m.usedJSHeapSize / m.jsHeapSizeLimit; - if (!Number.isFinite(ratio) || ratio < thresholdRatio) return; - - const now = Date.now(); - if (now - lastSent < cooldownMs) return; - lastSent = now; - - const trail = getPiniaActionTrail(); - const trailText = JSON.stringify(trail); - Sentry.withScope((scope) => { - scope.setLevel('warning'); - scope.setTag('reason', 'high-js-heap'); - scope.setContext('memory', { - usedMB: m.usedJSHeapSize / 1024 / 1024, - limitMB: m.jsHeapSizeLimit / 1024 / 1024, - ratio - }); - scope.setContext('pinia_actions', { - trailText, - count: trail.length - }); - Sentry.captureMessage( - 'Memory usage critical: nearing JS heap limit' - ); - }); - }, intervalMs); -} diff --git a/src/plugins/rendererMemoryReport.js b/src/plugins/rendererMemoryReport.js new file mode 100644 index 00000000..0e15966f --- /dev/null +++ b/src/plugins/rendererMemoryReport.js @@ -0,0 +1,50 @@ +function readPerformanceMemory() { + // @ts-ignore + const memory = window?.performance?.memory; + if (!memory) return null; + const { usedJSHeapSize, jsHeapSizeLimit } = memory; + if ( + typeof usedJSHeapSize !== 'number' || + typeof jsHeapSizeLimit !== 'number' + ) { + return null; + } + return { usedJSHeapSize, jsHeapSizeLimit }; +} + +export function startRendererMemoryThresholdReport( + Sentry, + { intervalMs = 10_000, thresholdRatio = 0.8, cooldownMs = 5 * 60_000 } = {} +) { + const initial = readPerformanceMemory(); + if (!initial) return null; + + if (!Sentry?.withScope) return null; + + let lastSent = 0; + + return setInterval(() => { + const m = readPerformanceMemory(); + if (!m) return; + + const ratio = m.usedJSHeapSize / m.jsHeapSizeLimit; + if (!Number.isFinite(ratio) || ratio < thresholdRatio) return; + + const now = Date.now(); + if (now - lastSent < cooldownMs) return; + lastSent = now; + + Sentry.withScope((scope) => { + scope.setLevel('warning'); + scope.setTag('reason', 'high-js-heap'); + scope.setContext('memory', { + usedMB: m.usedJSHeapSize / 1024 / 1024, + limitMB: m.jsHeapSizeLimit / 1024 / 1024, + ratio + }); + Sentry.captureMessage( + 'Memory usage critical: nearing JS heap limit' + ); + }); + }, intervalMs); +} diff --git a/src/plugins/sentry.js b/src/plugins/sentry.js index 0326ac90..f18b4afd 100644 --- a/src/plugins/sentry.js +++ b/src/plugins/sentry.js @@ -1,5 +1,5 @@ import { router } from './router'; -import { startRendererMemoryThresholdReport } from './piniaActionTrail'; +import { startRendererMemoryThresholdReport } from './rendererMemoryReport'; import configRepository from '../services/config'; diff --git a/src/stores/favorite.js b/src/stores/favorite.js index b0bfb47f..8d839fe9 100644 --- a/src/stores/favorite.js +++ b/src/stores/favorite.js @@ -90,23 +90,23 @@ export const useFavoriteStore = defineStore('Favorite', () => { const favoriteFriends = computed(() => { if (appearanceSettingsStore.sortFavorites) { - return state.favoriteFriends_.sort(compareByFavoriteSortOrder); + return state.favoriteFriends_.toSorted(compareByFavoriteSortOrder); } - return state.favoriteFriends_.sort(compareByName); + return state.favoriteFriends_.toSorted(compareByName); }); const favoriteWorlds = computed(() => { if (appearanceSettingsStore.sortFavorites) { - return state.favoriteWorlds_.sort(compareByFavoriteSortOrder); + return state.favoriteWorlds_.toSorted(compareByFavoriteSortOrder); } - return state.favoriteWorlds_.sort(compareByName); + return state.favoriteWorlds_.toSorted(compareByName); }); const favoriteAvatars = computed(() => { if (appearanceSettingsStore.sortFavorites) { - return state.favoriteAvatars_.sort(compareByFavoriteSortOrder); + return state.favoriteAvatars_.toSorted(compareByFavoriteSortOrder); } - return state.favoriteAvatars_.sort(compareByName); + return state.favoriteAvatars_.toSorted(compareByName); }); watch( diff --git a/src/stores/index.js b/src/stores/index.js index 6228f608..89e5541f 100644 --- a/src/stores/index.js +++ b/src/stores/index.js @@ -1,7 +1,6 @@ import { createPinia } from 'pinia'; import { getSentry, isSentryOptedIn } from '../plugins'; -import { createPiniaActionTrailPlugin } from '../plugins/piniaActionTrail'; import { useAdvancedSettingsStore } from './settings/advanced'; import { useAppearanceSettingsStore } from './settings/appearance'; import { useAuthStore } from './auth'; @@ -43,11 +42,6 @@ import { useWristOverlaySettingsStore } from './settings/wristOverlay'; export const pinia = createPinia(); -function registerPiniaActionTrailPlugin() { - if (!NIGHTLY) return; - pinia.use(createPiniaActionTrailPlugin({ maxEntries: 200 })); -} - async function registerSentryPiniaPlugin() { if (!NIGHTLY) return; if (!(await isSentryOptedIn())) return; @@ -125,9 +119,6 @@ async function registerSentryPiniaPlugin() { export async function initPiniaPlugins() { await registerSentryPiniaPlugin(); - setTimeout(() => { - registerPiniaActionTrailPlugin(); - }, 60000); } export function createGlobalStores() { diff --git a/src/stores/vrcx.js b/src/stores/vrcx.js index f4526759..79e7ab7c 100644 --- a/src/stores/vrcx.js +++ b/src/stores/vrcx.js @@ -10,10 +10,6 @@ import { SEARCH_LIMIT_MIN } from '../shared/constants'; import { avatarRequest, queryRequest } from '../api'; -import { - clearPiniaActionTrail, - getPiniaActionTrail -} from '../plugins/piniaActionTrail'; import { debounce, parseLocation } from '../shared/utils'; import { AppDebug } from '../services/appConfig'; import { database } from '../services/database'; @@ -556,29 +552,16 @@ export const useVrcxStore = defineStore('Vrcx', () => { if (advancedSettingsStore.sentryErrorReporting) { try { import('@sentry/vue').then((Sentry) => { - const trail = getPiniaActionTrail() - .filter((entry) => { - if (!entry) return false; - return ( - typeof entry.t === 'string' && - typeof entry.a === 'string' - ); - }) - .reverse(); - const trailText = JSON.stringify(trail); Sentry.withScope((scope) => { scope.setLevel('fatal'); scope.setTag('reason', 'crash-recovery'); - scope.setContext('pinia_actions', { - trailText, + scope.setContext('session', { sessionTime: performance.now() / 1000 / 60 }); Sentry.captureMessage( `crash message: ${crashMessage}` ); }); - - clearPiniaActionTrail(); }); } catch (error) { console.error('Error setting up Sentry feedback:', error); diff --git a/src/views/Sidebar/components/FriendsSidebar.vue b/src/views/Sidebar/components/FriendsSidebar.vue index e59a1e94..ed6ed6de 100644 --- a/src/views/Sidebar/components/FriendsSidebar.vue +++ b/src/views/Sidebar/components/FriendsSidebar.vue @@ -278,9 +278,11 @@ const shouldHideSameInstance = computed(() => isSidebarGroupByInstance.value && isHideFriendsInSameInstance.value); + const selectedFavoriteGroupKeys = computed(() => new Set(sidebarFavoriteGroups.value)); + const selectedFavoriteGroupIds = computed(() => { - const selectedGroups = sidebarFavoriteGroups.value; - const hasFilter = selectedGroups.length > 0; + const selectedGroups = selectedFavoriteGroupKeys.value; + const hasFilter = selectedGroups.size > 0; if (!hasFilter) { return allFavoriteFriendIds.value; } @@ -337,22 +339,18 @@ ); }); - const vipFriendsByGroupStatus = computed(() => { - return visibleFavoriteOnlineFriends.value; - }); - // VIP friends divide by group const vipFriendsDivideByGroup = computed(() => { const remoteFriendsByGroup = groupedByGroupKeyFavoriteFriends.value; - const selectedGroups = sidebarFavoriteGroups.value; - const hasFilter = selectedGroups.length > 0; + const selectedGroups = selectedFavoriteGroupKeys.value; + const hasFilter = selectedGroups.size > 0; // Build a normalized list of { key, groupName, memberIds } const groups = []; for (const key in remoteFriendsByGroup) { if (Object.hasOwn(remoteFriendsByGroup, key)) { - if (hasFilter && !selectedGroups.includes(key)) continue; + if (hasFilter && !selectedGroups.has(key)) continue; const groupName = favoriteFriendGroups.value.find((g) => g.key === key)?.displayName || ''; const memberIds = new Set(remoteFriendsByGroup[key].map((f) => f.id)); groups.push({ key, groupName, memberIds }); @@ -361,7 +359,7 @@ for (const groupName in localFriendFavorites.value) { const selectedKey = `local:${groupName}`; - if (hasFilter && !selectedGroups.includes(selectedKey)) continue; + if (hasFilter && !selectedGroups.has(selectedKey)) continue; const userIds = localFriendFavorites.value[groupName]; if (userIds?.length) { groups.push({ key: selectedKey, groupName, memberIds: new Set(userIds) }); @@ -407,7 +405,7 @@ const vipFriendCount = isSidebarDivideByFriendGroup.value ? vipFriendsDivideByGroup.value.reduce((sum, group) => sum + group.length, 0) - : vipFriendsByGroupStatus.value.length; + : visibleFavoriteOnlineFriends.value.length; if (vipFriendCount) { rows.push( @@ -456,7 +454,7 @@ } }); } else { - vipFriendsByGroupStatus.value.forEach((friend, idx) => { + visibleFavoriteOnlineFriends.value.forEach((friend, idx) => { rows.push(buildFriendRow(friend, `vip:${friend?.id ?? idx}`)); }); }