This commit is contained in:
pa
2026-03-15 21:04:02 +09:00
parent 1d7e41a4a1
commit 8624ac20fa
7 changed files with 68 additions and 181 deletions

View File

@@ -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);
}

View 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);
}

View File

@@ -1,5 +1,5 @@
import { router } from './router';
import { startRendererMemoryThresholdReport } from './piniaActionTrail';
import { startRendererMemoryThresholdReport } from './rendererMemoryReport';
import configRepository from '../services/config';

View File

@@ -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(

View File

@@ -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() {

View File

@@ -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);

View File

@@ -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}`));
});
}