mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-19 14:53:50 +02:00
refactor
This commit is contained in:
@@ -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);
|
|
||||||
}
|
|
||||||
50
src/plugins/rendererMemoryReport.js
Normal file
50
src/plugins/rendererMemoryReport.js
Normal file
@@ -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);
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { router } from './router';
|
import { router } from './router';
|
||||||
import { startRendererMemoryThresholdReport } from './piniaActionTrail';
|
import { startRendererMemoryThresholdReport } from './rendererMemoryReport';
|
||||||
|
|
||||||
import configRepository from '../services/config';
|
import configRepository from '../services/config';
|
||||||
|
|
||||||
|
|||||||
@@ -90,23 +90,23 @@ export const useFavoriteStore = defineStore('Favorite', () => {
|
|||||||
|
|
||||||
const favoriteFriends = computed(() => {
|
const favoriteFriends = computed(() => {
|
||||||
if (appearanceSettingsStore.sortFavorites) {
|
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(() => {
|
const favoriteWorlds = computed(() => {
|
||||||
if (appearanceSettingsStore.sortFavorites) {
|
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(() => {
|
const favoriteAvatars = computed(() => {
|
||||||
if (appearanceSettingsStore.sortFavorites) {
|
if (appearanceSettingsStore.sortFavorites) {
|
||||||
return state.favoriteAvatars_.sort(compareByFavoriteSortOrder);
|
return state.favoriteAvatars_.toSorted(compareByFavoriteSortOrder);
|
||||||
}
|
}
|
||||||
return state.favoriteAvatars_.sort(compareByName);
|
return state.favoriteAvatars_.toSorted(compareByName);
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { createPinia } from 'pinia';
|
import { createPinia } from 'pinia';
|
||||||
|
|
||||||
import { getSentry, isSentryOptedIn } from '../plugins';
|
import { getSentry, isSentryOptedIn } from '../plugins';
|
||||||
import { createPiniaActionTrailPlugin } from '../plugins/piniaActionTrail';
|
|
||||||
import { useAdvancedSettingsStore } from './settings/advanced';
|
import { useAdvancedSettingsStore } from './settings/advanced';
|
||||||
import { useAppearanceSettingsStore } from './settings/appearance';
|
import { useAppearanceSettingsStore } from './settings/appearance';
|
||||||
import { useAuthStore } from './auth';
|
import { useAuthStore } from './auth';
|
||||||
@@ -43,11 +42,6 @@ import { useWristOverlaySettingsStore } from './settings/wristOverlay';
|
|||||||
|
|
||||||
export const pinia = createPinia();
|
export const pinia = createPinia();
|
||||||
|
|
||||||
function registerPiniaActionTrailPlugin() {
|
|
||||||
if (!NIGHTLY) return;
|
|
||||||
pinia.use(createPiniaActionTrailPlugin({ maxEntries: 200 }));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function registerSentryPiniaPlugin() {
|
async function registerSentryPiniaPlugin() {
|
||||||
if (!NIGHTLY) return;
|
if (!NIGHTLY) return;
|
||||||
if (!(await isSentryOptedIn())) return;
|
if (!(await isSentryOptedIn())) return;
|
||||||
@@ -125,9 +119,6 @@ async function registerSentryPiniaPlugin() {
|
|||||||
|
|
||||||
export async function initPiniaPlugins() {
|
export async function initPiniaPlugins() {
|
||||||
await registerSentryPiniaPlugin();
|
await registerSentryPiniaPlugin();
|
||||||
setTimeout(() => {
|
|
||||||
registerPiniaActionTrailPlugin();
|
|
||||||
}, 60000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createGlobalStores() {
|
export function createGlobalStores() {
|
||||||
|
|||||||
@@ -10,10 +10,6 @@ import {
|
|||||||
SEARCH_LIMIT_MIN
|
SEARCH_LIMIT_MIN
|
||||||
} from '../shared/constants';
|
} from '../shared/constants';
|
||||||
import { avatarRequest, queryRequest } from '../api';
|
import { avatarRequest, queryRequest } from '../api';
|
||||||
import {
|
|
||||||
clearPiniaActionTrail,
|
|
||||||
getPiniaActionTrail
|
|
||||||
} from '../plugins/piniaActionTrail';
|
|
||||||
import { debounce, parseLocation } from '../shared/utils';
|
import { debounce, parseLocation } from '../shared/utils';
|
||||||
import { AppDebug } from '../services/appConfig';
|
import { AppDebug } from '../services/appConfig';
|
||||||
import { database } from '../services/database';
|
import { database } from '../services/database';
|
||||||
@@ -556,29 +552,16 @@ export const useVrcxStore = defineStore('Vrcx', () => {
|
|||||||
if (advancedSettingsStore.sentryErrorReporting) {
|
if (advancedSettingsStore.sentryErrorReporting) {
|
||||||
try {
|
try {
|
||||||
import('@sentry/vue').then((Sentry) => {
|
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) => {
|
Sentry.withScope((scope) => {
|
||||||
scope.setLevel('fatal');
|
scope.setLevel('fatal');
|
||||||
scope.setTag('reason', 'crash-recovery');
|
scope.setTag('reason', 'crash-recovery');
|
||||||
scope.setContext('pinia_actions', {
|
scope.setContext('session', {
|
||||||
trailText,
|
|
||||||
sessionTime: performance.now() / 1000 / 60
|
sessionTime: performance.now() / 1000 / 60
|
||||||
});
|
});
|
||||||
Sentry.captureMessage(
|
Sentry.captureMessage(
|
||||||
`crash message: ${crashMessage}`
|
`crash message: ${crashMessage}`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
clearPiniaActionTrail();
|
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error setting up Sentry feedback:', error);
|
console.error('Error setting up Sentry feedback:', error);
|
||||||
|
|||||||
@@ -278,9 +278,11 @@
|
|||||||
|
|
||||||
const shouldHideSameInstance = computed(() => isSidebarGroupByInstance.value && isHideFriendsInSameInstance.value);
|
const shouldHideSameInstance = computed(() => isSidebarGroupByInstance.value && isHideFriendsInSameInstance.value);
|
||||||
|
|
||||||
|
const selectedFavoriteGroupKeys = computed(() => new Set(sidebarFavoriteGroups.value));
|
||||||
|
|
||||||
const selectedFavoriteGroupIds = computed(() => {
|
const selectedFavoriteGroupIds = computed(() => {
|
||||||
const selectedGroups = sidebarFavoriteGroups.value;
|
const selectedGroups = selectedFavoriteGroupKeys.value;
|
||||||
const hasFilter = selectedGroups.length > 0;
|
const hasFilter = selectedGroups.size > 0;
|
||||||
if (!hasFilter) {
|
if (!hasFilter) {
|
||||||
return allFavoriteFriendIds.value;
|
return allFavoriteFriendIds.value;
|
||||||
}
|
}
|
||||||
@@ -337,22 +339,18 @@
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const vipFriendsByGroupStatus = computed(() => {
|
|
||||||
return visibleFavoriteOnlineFriends.value;
|
|
||||||
});
|
|
||||||
|
|
||||||
// VIP friends divide by group
|
// VIP friends divide by group
|
||||||
const vipFriendsDivideByGroup = computed(() => {
|
const vipFriendsDivideByGroup = computed(() => {
|
||||||
const remoteFriendsByGroup = groupedByGroupKeyFavoriteFriends.value;
|
const remoteFriendsByGroup = groupedByGroupKeyFavoriteFriends.value;
|
||||||
const selectedGroups = sidebarFavoriteGroups.value;
|
const selectedGroups = selectedFavoriteGroupKeys.value;
|
||||||
const hasFilter = selectedGroups.length > 0;
|
const hasFilter = selectedGroups.size > 0;
|
||||||
|
|
||||||
// Build a normalized list of { key, groupName, memberIds }
|
// Build a normalized list of { key, groupName, memberIds }
|
||||||
const groups = [];
|
const groups = [];
|
||||||
|
|
||||||
for (const key in remoteFriendsByGroup) {
|
for (const key in remoteFriendsByGroup) {
|
||||||
if (Object.hasOwn(remoteFriendsByGroup, key)) {
|
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 groupName = favoriteFriendGroups.value.find((g) => g.key === key)?.displayName || '';
|
||||||
const memberIds = new Set(remoteFriendsByGroup[key].map((f) => f.id));
|
const memberIds = new Set(remoteFriendsByGroup[key].map((f) => f.id));
|
||||||
groups.push({ key, groupName, memberIds });
|
groups.push({ key, groupName, memberIds });
|
||||||
@@ -361,7 +359,7 @@
|
|||||||
|
|
||||||
for (const groupName in localFriendFavorites.value) {
|
for (const groupName in localFriendFavorites.value) {
|
||||||
const selectedKey = `local:${groupName}`;
|
const selectedKey = `local:${groupName}`;
|
||||||
if (hasFilter && !selectedGroups.includes(selectedKey)) continue;
|
if (hasFilter && !selectedGroups.has(selectedKey)) continue;
|
||||||
const userIds = localFriendFavorites.value[groupName];
|
const userIds = localFriendFavorites.value[groupName];
|
||||||
if (userIds?.length) {
|
if (userIds?.length) {
|
||||||
groups.push({ key: selectedKey, groupName, memberIds: new Set(userIds) });
|
groups.push({ key: selectedKey, groupName, memberIds: new Set(userIds) });
|
||||||
@@ -407,7 +405,7 @@
|
|||||||
|
|
||||||
const vipFriendCount = isSidebarDivideByFriendGroup.value
|
const vipFriendCount = isSidebarDivideByFriendGroup.value
|
||||||
? vipFriendsDivideByGroup.value.reduce((sum, group) => sum + group.length, 0)
|
? vipFriendsDivideByGroup.value.reduce((sum, group) => sum + group.length, 0)
|
||||||
: vipFriendsByGroupStatus.value.length;
|
: visibleFavoriteOnlineFriends.value.length;
|
||||||
|
|
||||||
if (vipFriendCount) {
|
if (vipFriendCount) {
|
||||||
rows.push(
|
rows.push(
|
||||||
@@ -456,7 +454,7 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
vipFriendsByGroupStatus.value.forEach((friend, idx) => {
|
visibleFavoriteOnlineFriends.value.forEach((friend, idx) => {
|
||||||
rows.push(buildFriendRow(friend, `vip:${friend?.id ?? idx}`));
|
rows.push(buildFriendRow(friend, `vip:${friend?.id ?? idx}`));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user