mirror of
https://github.com/basicswap/basicswap.git
synced 2025-11-05 10:28:10 +01:00
583 lines
16 KiB
JavaScript
583 lines
16 KiB
JavaScript
const MemoryManager = (function() {
|
|
const config = {
|
|
tooltipCleanupInterval: 300000,
|
|
diagnosticsInterval: 600000,
|
|
elementVerificationInterval: 300000,
|
|
maxTooltipsThreshold: 100,
|
|
maxTooltips: 300,
|
|
cleanupThreshold: 1.5,
|
|
minTimeBetweenCleanups: 180000,
|
|
memoryGrowthThresholdMB: 100,
|
|
debug: false,
|
|
protectedWebSockets: ['wsPort', 'ws_port'],
|
|
interactiveSelectors: [
|
|
'tr:hover',
|
|
'[data-tippy-root]:hover',
|
|
'.tooltip:hover',
|
|
'[data-tooltip-trigger-id]:hover',
|
|
'[data-tooltip-target]:hover'
|
|
],
|
|
protectedContainers: [
|
|
'#sent-tbody',
|
|
'#received-tbody',
|
|
'#offers-body'
|
|
]
|
|
};
|
|
|
|
const state = {
|
|
pendingAnimationFrames: new Set(),
|
|
pendingTimeouts: new Set(),
|
|
cleanupInterval: null,
|
|
diagnosticsInterval: null,
|
|
elementVerificationInterval: null,
|
|
mutationObserver: null,
|
|
lastCleanupTime: Date.now(),
|
|
startTime: Date.now(),
|
|
isCleanupRunning: false,
|
|
metrics: {
|
|
tooltipsRemoved: 0,
|
|
cleanupRuns: 0,
|
|
lastMemoryUsage: null,
|
|
lastCleanupDetails: {},
|
|
history: []
|
|
},
|
|
originalTooltipFunctions: {}
|
|
};
|
|
|
|
function log(message, ...args) {
|
|
if (config.debug) {
|
|
console.log(`[MemoryManager] ${message}`, ...args);
|
|
}
|
|
}
|
|
|
|
function preserveTooltipFunctions() {
|
|
if (window.TooltipManager && !state.originalTooltipFunctions.destroy) {
|
|
state.originalTooltipFunctions = {
|
|
destroy: window.TooltipManager.destroy,
|
|
cleanup: window.TooltipManager.cleanup,
|
|
create: window.TooltipManager.create
|
|
};
|
|
}
|
|
}
|
|
|
|
function isInProtectedContainer(element) {
|
|
if (!element) return false;
|
|
|
|
for (const selector of config.protectedContainers) {
|
|
if (element.closest && element.closest(selector)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function shouldSkipCleanup() {
|
|
if (state.isCleanupRunning) return true;
|
|
|
|
const selector = config.interactiveSelectors.join(', ');
|
|
const hoveredElements = document.querySelectorAll(selector);
|
|
|
|
return hoveredElements.length > 0;
|
|
}
|
|
|
|
function performCleanup(force = false) {
|
|
if (shouldSkipCleanup() && !force) {
|
|
return false;
|
|
}
|
|
|
|
if (state.isCleanupRunning) {
|
|
return false;
|
|
}
|
|
|
|
const now = Date.now();
|
|
if (!force && now - state.lastCleanupTime < config.minTimeBetweenCleanups) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
state.isCleanupRunning = true;
|
|
state.lastCleanupTime = now;
|
|
state.metrics.cleanupRuns++;
|
|
|
|
const startTime = performance.now();
|
|
const startMemory = checkMemoryUsage();
|
|
|
|
state.pendingAnimationFrames.forEach(id => {
|
|
cancelAnimationFrame(id);
|
|
});
|
|
state.pendingAnimationFrames.clear();
|
|
|
|
state.pendingTimeouts.forEach(id => {
|
|
clearTimeout(id);
|
|
});
|
|
state.pendingTimeouts.clear();
|
|
|
|
const tooltipsResult = removeOrphanedTooltips();
|
|
state.metrics.tooltipsRemoved += tooltipsResult;
|
|
|
|
const disconnectedResult = checkForDisconnectedElements();
|
|
|
|
tryRunGarbageCollection(false);
|
|
|
|
const endTime = performance.now();
|
|
const endMemory = checkMemoryUsage();
|
|
|
|
const runStats = {
|
|
timestamp: new Date().toISOString(),
|
|
duration: endTime - startTime,
|
|
tooltipsRemoved: tooltipsResult,
|
|
disconnectedRemoved: disconnectedResult,
|
|
memoryBefore: startMemory ? startMemory.usedMB : null,
|
|
memoryAfter: endMemory ? endMemory.usedMB : null,
|
|
memorySaved: startMemory && endMemory ?
|
|
(startMemory.usedMB - endMemory.usedMB).toFixed(2) : null
|
|
};
|
|
|
|
state.metrics.history.unshift(runStats);
|
|
if (state.metrics.history.length > 10) {
|
|
state.metrics.history.pop();
|
|
}
|
|
|
|
state.metrics.lastCleanupDetails = runStats;
|
|
|
|
if (config.debug) {
|
|
log(`Cleanup completed in ${runStats.duration.toFixed(2)}ms, removed ${tooltipsResult} tooltips`);
|
|
}
|
|
|
|
return true;
|
|
} catch (error) {
|
|
console.error("Error during cleanup:", error);
|
|
return false;
|
|
} finally {
|
|
state.isCleanupRunning = false;
|
|
}
|
|
}
|
|
|
|
function removeOrphanedTooltips() {
|
|
try {
|
|
|
|
const tippyRoots = document.querySelectorAll('[data-tippy-root]:not(:hover)');
|
|
let removed = 0;
|
|
|
|
tippyRoots.forEach(root => {
|
|
const tooltipId = root.getAttribute('data-for-tooltip-id');
|
|
const trigger = tooltipId ?
|
|
document.querySelector(`[data-tooltip-trigger-id="${tooltipId}"]`) : null;
|
|
|
|
if (!trigger || !document.body.contains(trigger)) {
|
|
if (root.parentNode) {
|
|
root.parentNode.removeChild(root);
|
|
removed++;
|
|
}
|
|
}
|
|
});
|
|
|
|
return removed;
|
|
} catch (error) {
|
|
console.error("Error removing orphaned tooltips:", error);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
function checkForDisconnectedElements() {
|
|
try {
|
|
|
|
const tooltipTriggers = document.querySelectorAll('[data-tooltip-trigger-id]:not(:hover)');
|
|
const disconnectedElements = new Set();
|
|
|
|
tooltipTriggers.forEach(el => {
|
|
if (!document.body.contains(el)) {
|
|
const tooltipId = el.getAttribute('data-tooltip-trigger-id');
|
|
disconnectedElements.add(tooltipId);
|
|
}
|
|
});
|
|
|
|
const tooltipRoots = document.querySelectorAll('[data-for-tooltip-id]');
|
|
let removed = 0;
|
|
|
|
disconnectedElements.forEach(id => {
|
|
for (const root of tooltipRoots) {
|
|
if (root.getAttribute('data-for-tooltip-id') === id && root.parentNode) {
|
|
root.parentNode.removeChild(root);
|
|
removed++;
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
|
|
return disconnectedElements.size;
|
|
} catch (error) {
|
|
console.error("Error checking for disconnected elements:", error);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
function tryRunGarbageCollection(aggressive = false) {
|
|
setTimeout(() => {
|
|
|
|
const cache = {};
|
|
for (let i = 0; i < 100; i++) {
|
|
cache[`key${i}`] = {};
|
|
}
|
|
|
|
for (const key in cache) {
|
|
delete cache[key];
|
|
}
|
|
}, 100);
|
|
|
|
return true;
|
|
}
|
|
|
|
function checkMemoryUsage() {
|
|
const result = {
|
|
usedJSHeapSize: 0,
|
|
totalJSHeapSize: 0,
|
|
jsHeapSizeLimit: 0,
|
|
percentUsed: "0",
|
|
usedMB: "0",
|
|
totalMB: "0",
|
|
limitMB: "0"
|
|
};
|
|
|
|
if (window.performance && window.performance.memory) {
|
|
result.usedJSHeapSize = window.performance.memory.usedJSHeapSize;
|
|
result.totalJSHeapSize = window.performance.memory.totalJSHeapSize;
|
|
result.jsHeapSizeLimit = window.performance.memory.jsHeapSizeLimit;
|
|
result.percentUsed = (result.usedJSHeapSize / result.jsHeapSizeLimit * 100).toFixed(2);
|
|
result.usedMB = (result.usedJSHeapSize / (1024 * 1024)).toFixed(2);
|
|
result.totalMB = (result.totalJSHeapSize / (1024 * 1024)).toFixed(2);
|
|
result.limitMB = (result.jsHeapSizeLimit / (1024 * 1024)).toFixed(2);
|
|
} else {
|
|
result.usedMB = "Unknown";
|
|
result.totalMB = "Unknown";
|
|
result.limitMB = "Unknown";
|
|
result.percentUsed = "Unknown";
|
|
}
|
|
|
|
state.metrics.lastMemoryUsage = result;
|
|
return result;
|
|
}
|
|
|
|
function handleVisibilityChange() {
|
|
if (document.hidden) {
|
|
removeOrphanedTooltips();
|
|
checkForDisconnectedElements();
|
|
}
|
|
}
|
|
|
|
function setupMutationObserver() {
|
|
if (state.mutationObserver) {
|
|
state.mutationObserver.disconnect();
|
|
state.mutationObserver = null;
|
|
}
|
|
|
|
let processingScheduled = false;
|
|
let lastProcessTime = 0;
|
|
const MIN_PROCESS_INTERVAL = 10000;
|
|
|
|
const processMutations = (mutations) => {
|
|
const now = Date.now();
|
|
|
|
if (now - lastProcessTime < MIN_PROCESS_INTERVAL || processingScheduled) {
|
|
return;
|
|
}
|
|
|
|
processingScheduled = true;
|
|
|
|
setTimeout(() => {
|
|
processingScheduled = false;
|
|
lastProcessTime = Date.now();
|
|
|
|
if (state.isCleanupRunning) {
|
|
return;
|
|
}
|
|
|
|
const tooltipSelectors = ['[data-tippy-root]', '[data-tooltip-trigger-id]', '.tooltip'];
|
|
let tooltipCount = 0;
|
|
|
|
tooltipCount = document.querySelectorAll(tooltipSelectors.join(', ')).length;
|
|
|
|
if (tooltipCount > config.maxTooltipsThreshold &&
|
|
(Date.now() - state.lastCleanupTime > config.minTimeBetweenCleanups)) {
|
|
|
|
removeOrphanedTooltips();
|
|
checkForDisconnectedElements();
|
|
state.lastCleanupTime = Date.now();
|
|
}
|
|
}, 5000);
|
|
};
|
|
|
|
state.mutationObserver = new MutationObserver(processMutations);
|
|
|
|
state.mutationObserver.observe(document.body, {
|
|
childList: true,
|
|
subtree: true,
|
|
attributes: false,
|
|
characterData: false
|
|
});
|
|
|
|
return state.mutationObserver;
|
|
}
|
|
|
|
function enhanceTooltipManager() {
|
|
if (!window.TooltipManager || window.TooltipManager._memoryManagerEnhanced) return false;
|
|
|
|
preserveTooltipFunctions();
|
|
|
|
const originalDestroy = window.TooltipManager.destroy;
|
|
const originalCleanup = window.TooltipManager.cleanup;
|
|
|
|
window.TooltipManager.destroy = function(element) {
|
|
if (!element) return;
|
|
|
|
try {
|
|
const tooltipId = element.getAttribute('data-tooltip-trigger-id');
|
|
|
|
if (isInProtectedContainer(element)) {
|
|
if (originalDestroy) {
|
|
return originalDestroy.call(window.TooltipManager, element);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (tooltipId) {
|
|
if (originalDestroy) {
|
|
originalDestroy.call(window.TooltipManager, element);
|
|
}
|
|
|
|
const tooltipRoot = document.querySelector(`[data-for-tooltip-id="${tooltipId}"]`);
|
|
if (tooltipRoot && tooltipRoot.parentNode) {
|
|
tooltipRoot.parentNode.removeChild(tooltipRoot);
|
|
}
|
|
|
|
element.removeAttribute('data-tooltip-trigger-id');
|
|
element.removeAttribute('aria-describedby');
|
|
|
|
if (element._tippy) {
|
|
try {
|
|
element._tippy.destroy();
|
|
element._tippy = null;
|
|
} catch (e) {}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Error in enhanced tooltip destroy:', error);
|
|
|
|
if (originalDestroy) {
|
|
originalDestroy.call(window.TooltipManager, element);
|
|
}
|
|
}
|
|
};
|
|
|
|
window.TooltipManager.cleanup = function() {
|
|
try {
|
|
if (originalCleanup) {
|
|
originalCleanup.call(window.TooltipManager);
|
|
}
|
|
|
|
removeOrphanedTooltips();
|
|
} catch (error) {
|
|
console.error('Error in enhanced tooltip cleanup:', error);
|
|
|
|
if (originalCleanup) {
|
|
originalCleanup.call(window.TooltipManager);
|
|
}
|
|
}
|
|
};
|
|
|
|
window.TooltipManager._memoryManagerEnhanced = true;
|
|
window.TooltipManager._originalDestroy = originalDestroy;
|
|
window.TooltipManager._originalCleanup = originalCleanup;
|
|
|
|
return true;
|
|
}
|
|
|
|
function initializeScheduledCleanups() {
|
|
if (state.cleanupInterval) {
|
|
clearInterval(state.cleanupInterval);
|
|
state.cleanupInterval = null;
|
|
}
|
|
|
|
if (state.diagnosticsInterval) {
|
|
clearInterval(state.diagnosticsInterval);
|
|
state.diagnosticsInterval = null;
|
|
}
|
|
|
|
if (state.elementVerificationInterval) {
|
|
clearInterval(state.elementVerificationInterval);
|
|
state.elementVerificationInterval = null;
|
|
}
|
|
|
|
state.cleanupInterval = setInterval(() => {
|
|
removeOrphanedTooltips();
|
|
checkForDisconnectedElements();
|
|
}, config.tooltipCleanupInterval);
|
|
|
|
state.diagnosticsInterval = setInterval(() => {
|
|
checkMemoryUsage();
|
|
}, config.diagnosticsInterval);
|
|
|
|
state.elementVerificationInterval = setInterval(() => {
|
|
checkForDisconnectedElements();
|
|
}, config.elementVerificationInterval);
|
|
|
|
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
|
document.addEventListener('visibilitychange', handleVisibilityChange);
|
|
|
|
setupMutationObserver();
|
|
|
|
return true;
|
|
}
|
|
|
|
function initialize(options = {}) {
|
|
preserveTooltipFunctions();
|
|
|
|
if (options) {
|
|
Object.assign(config, options);
|
|
}
|
|
|
|
enhanceTooltipManager();
|
|
|
|
if (window.WebSocketManager && !window.WebSocketManager.cleanupOrphanedSockets) {
|
|
window.WebSocketManager.cleanupOrphanedSockets = function() {
|
|
return 0;
|
|
};
|
|
}
|
|
|
|
const manager = window.ApiManager || window.Api;
|
|
if (manager && !manager.abortPendingRequests) {
|
|
manager.abortPendingRequests = function() {
|
|
return 0;
|
|
};
|
|
}
|
|
|
|
initializeScheduledCleanups();
|
|
|
|
setTimeout(() => {
|
|
removeOrphanedTooltips();
|
|
checkForDisconnectedElements();
|
|
}, 5000);
|
|
|
|
return this;
|
|
}
|
|
|
|
function dispose() {
|
|
if (state.cleanupInterval) {
|
|
clearInterval(state.cleanupInterval);
|
|
state.cleanupInterval = null;
|
|
}
|
|
|
|
if (state.diagnosticsInterval) {
|
|
clearInterval(state.diagnosticsInterval);
|
|
state.diagnosticsInterval = null;
|
|
}
|
|
|
|
if (state.elementVerificationInterval) {
|
|
clearInterval(state.elementVerificationInterval);
|
|
state.elementVerificationInterval = null;
|
|
}
|
|
|
|
if (state.mutationObserver) {
|
|
state.mutationObserver.disconnect();
|
|
state.mutationObserver = null;
|
|
}
|
|
|
|
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
|
|
|
return true;
|
|
}
|
|
|
|
function displayStats() {
|
|
const stats = getDetailedStats();
|
|
|
|
console.group('Memory Manager Stats');
|
|
console.log('Memory Usage:', stats.memory ?
|
|
`${stats.memory.usedMB}MB / ${stats.memory.limitMB}MB (${stats.memory.percentUsed}%)` :
|
|
'Not available');
|
|
console.log('Total Cleanups:', stats.metrics.cleanupRuns);
|
|
console.log('Total Tooltips Removed:', stats.metrics.tooltipsRemoved);
|
|
console.log('Current Tooltips:', stats.tooltips.total);
|
|
console.log('Last Cleanup:', stats.metrics.lastCleanupDetails);
|
|
console.log('Cleanup History:', stats.metrics.history);
|
|
console.groupEnd();
|
|
|
|
return stats;
|
|
}
|
|
|
|
function getDetailedStats() {
|
|
|
|
const allTooltipElements = document.querySelectorAll('[data-tippy-root], [data-tooltip-trigger-id], .tooltip');
|
|
|
|
const tooltips = {
|
|
roots: document.querySelectorAll('[data-tippy-root]').length,
|
|
triggers: document.querySelectorAll('[data-tooltip-trigger-id]').length,
|
|
tooltipElements: document.querySelectorAll('.tooltip').length,
|
|
total: allTooltipElements.length,
|
|
protectedContainers: {}
|
|
};
|
|
|
|
config.protectedContainers.forEach(selector => {
|
|
const container = document.querySelector(selector);
|
|
if (container) {
|
|
tooltips.protectedContainers[selector] = {
|
|
tooltips: container.querySelectorAll('.tooltip').length,
|
|
triggers: container.querySelectorAll('[data-tooltip-trigger-id]').length,
|
|
roots: document.querySelectorAll(`[data-tippy-root][data-for-tooltip-id]`).length
|
|
};
|
|
}
|
|
});
|
|
|
|
return {
|
|
memory: checkMemoryUsage(),
|
|
metrics: { ...state.metrics },
|
|
tooltips,
|
|
config: { ...config }
|
|
};
|
|
}
|
|
|
|
return {
|
|
initialize,
|
|
cleanup: performCleanup,
|
|
forceCleanup: function() {
|
|
return performCleanup(true);
|
|
},
|
|
fullCleanup: function() {
|
|
return performCleanup(true);
|
|
},
|
|
getStats: getDetailedStats,
|
|
displayStats,
|
|
setDebugMode: function(enabled) {
|
|
config.debug = Boolean(enabled);
|
|
return config.debug;
|
|
},
|
|
addProtectedContainer: function(selector) {
|
|
if (!config.protectedContainers.includes(selector)) {
|
|
config.protectedContainers.push(selector);
|
|
}
|
|
return config.protectedContainers;
|
|
},
|
|
removeProtectedContainer: function(selector) {
|
|
const index = config.protectedContainers.indexOf(selector);
|
|
if (index !== -1) {
|
|
config.protectedContainers.splice(index, 1);
|
|
}
|
|
return config.protectedContainers;
|
|
},
|
|
dispose
|
|
};
|
|
})();
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const isDevMode = window.location.hostname === 'localhost' ||
|
|
window.location.hostname === '127.0.0.1';
|
|
|
|
MemoryManager.initialize({
|
|
debug: isDevMode
|
|
});
|
|
|
|
console.log('Memory Manager initialized');
|
|
});
|
|
|
|
window.MemoryManager = MemoryManager;
|