From 868b2475c130446498019c0c22b5c6ed27da0aed Mon Sep 17 00:00:00 2001 From: gerlofvanek Date: Thu, 8 May 2025 21:01:02 +0200 Subject: [PATCH] Fix: Better memory/tooltip/clean-up managers, various fixes. --- basicswap/static/js/bids_sentreceived.js | 10 - .../static/js/modules/cleanup-manager.js | 262 +++- basicswap/static/js/modules/memory-manager.js | 1053 ++++++++--------- basicswap/static/js/modules/price-manager.js | 4 +- .../static/js/modules/tooltips-manager.js | 732 ++++++++---- basicswap/static/js/offers.js | 52 +- basicswap/static/js/pricechart.js | 9 +- 7 files changed, 1266 insertions(+), 856 deletions(-) diff --git a/basicswap/static/js/bids_sentreceived.js b/basicswap/static/js/bids_sentreceived.js index decbc6f..52b91e5 100644 --- a/basicswap/static/js/bids_sentreceived.js +++ b/basicswap/static/js/bids_sentreceived.js @@ -311,7 +311,6 @@ CleanupManager.addListener(document, 'visibilitychange', () => { window.TooltipManager.cleanup(); } - // Run memory optimization if (window.MemoryManager) { MemoryManager.forceCleanup(); } @@ -1883,7 +1882,6 @@ function setupMemoryMonitoring() { }, { once: true }); } -// Init function initialize() { const filterElements = { stateSelect: document.getElementById('state'), @@ -1901,8 +1899,6 @@ function initialize() { if (filterElements.coinFrom) filterElements.coinFrom.value = 'any'; if (filterElements.coinTo) filterElements.coinTo.value = 'any'; - setupMemoryMonitoring(); - setTimeout(() => { WebSocketManager.initialize(); setupEventListeners(); @@ -1921,12 +1917,6 @@ function initialize() { updateBidsTable(); }, 100); - setInterval(() => { - if ((state.data.sent.length + state.data.received.length) > 1000) { - optimizeMemoryUsage(); - } - }, 5 * 60 * 1000); // Check every 5 minutes - window.cleanupBidsTable = cleanup; } diff --git a/basicswap/static/js/modules/cleanup-manager.js b/basicswap/static/js/modules/cleanup-manager.js index d179ee9..7fcb32a 100644 --- a/basicswap/static/js/modules/cleanup-manager.js +++ b/basicswap/static/js/modules/cleanup-manager.js @@ -1,12 +1,12 @@ const CleanupManager = (function() { - const state = { eventListeners: [], timeouts: [], intervals: [], animationFrames: [], resources: new Map(), - debug: false + debug: false, + memoryOptimizationInterval: null }; function log(message, ...args) { @@ -232,6 +232,229 @@ const CleanupManager = (function() { }; }, + setupMemoryOptimization: function(options = {}) { + const memoryCheckInterval = options.interval || 2 * 60 * 1000; // Default: 2 minutes + const maxCacheSize = options.maxCacheSize || 100; + const maxDataSize = options.maxDataSize || 1000; + + if (state.memoryOptimizationInterval) { + this.clearInterval(state.memoryOptimizationInterval); + } + + this.addListener(document, 'visibilitychange', () => { + if (document.hidden) { + log('Tab hidden - running memory optimization'); + this.optimizeMemory({ + maxCacheSize: maxCacheSize, + maxDataSize: maxDataSize + }); + } else if (window.TooltipManager) { + window.TooltipManager.cleanup(); + } + }); + + state.memoryOptimizationInterval = this.setInterval(() => { + if (document.hidden) { + log('Periodic memory optimization'); + this.optimizeMemory({ + maxCacheSize: maxCacheSize, + maxDataSize: maxDataSize + }); + } + }, memoryCheckInterval); + + log('Memory optimization setup complete'); + return state.memoryOptimizationInterval; + }, + + optimizeMemory: function(options = {}) { + log('Running memory optimization'); + + if (window.TooltipManager && typeof window.TooltipManager.cleanup === 'function') { + window.TooltipManager.cleanup(); + } + + if (window.IdentityManager && typeof window.IdentityManager.limitCacheSize === 'function') { + window.IdentityManager.limitCacheSize(options.maxCacheSize || 100); + } + + this.cleanupOrphanedResources(); + + if (window.gc) { + try { + window.gc(); + log('Forced garbage collection'); + } catch (e) { + } + } + + document.dispatchEvent(new CustomEvent('memoryOptimized', { + detail: { + timestamp: Date.now(), + maxDataSize: options.maxDataSize || 1000 + } + })); + + log('Memory optimization complete'); + }, + + cleanupOrphanedResources: function() { + let removedListeners = 0; + const validListeners = []; + + for (let i = 0; i < state.eventListeners.length; i++) { + const listener = state.eventListeners[i]; + if (!listener.element) { + removedListeners++; + continue; + } + + try { + + const isDetached = !(listener.element instanceof Node) || + !document.body.contains(listener.element) || + (listener.element.classList && listener.element.classList.contains('hidden')) || + (listener.element.style && listener.element.style.display === 'none'); + + if (isDetached) { + try { + if (listener.element instanceof Node) { + listener.element.removeEventListener(listener.type, listener.handler, listener.options); + } + removedListeners++; + } catch (e) { + + } + } else { + validListeners.push(listener); + } + } catch (e) { + + log(`Error checking listener (removing): ${e.message}`); + removedListeners++; + } + } + + if (removedListeners > 0) { + state.eventListeners = validListeners; + log(`Removed ${removedListeners} event listeners for detached/hidden elements`); + } + + let removedResources = 0; + const resourcesForRemoval = []; + + state.resources.forEach((info, id) => { + const resource = info.resource; + + try { + + if (resource instanceof Element && !document.body.contains(resource)) { + resourcesForRemoval.push(id); + } + + if (resource && resource.element) { + + if (resource.element instanceof Node && !document.body.contains(resource.element)) { + resourcesForRemoval.push(id); + } + } + } catch (e) { + log(`Error checking resource ${id}: ${e.message}`); + } + }); + + resourcesForRemoval.forEach(id => { + this.unregisterResource(id); + removedResources++; + }); + + if (removedResources > 0) { + log(`Removed ${removedResources} orphaned resources`); + } + + if (window.TooltipManager) { + if (typeof window.TooltipManager.cleanupOrphanedTooltips === 'function') { + try { + window.TooltipManager.cleanupOrphanedTooltips(); + } catch (e) { + + if (typeof window.TooltipManager.cleanup === 'function') { + try { + window.TooltipManager.cleanup(); + } catch (err) { + log(`Error cleaning up tooltips: ${err.message}`); + } + } + } + } else if (typeof window.TooltipManager.cleanup === 'function') { + try { + window.TooltipManager.cleanup(); + } catch (e) { + log(`Error cleaning up tooltips: ${e.message}`); + } + } + } + + try { + this.cleanupTooltipDOM(); + } catch (e) { + log(`Error in cleanupTooltipDOM: ${e.message}`); + } + }, + + cleanupTooltipDOM: function() { + let removedElements = 0; + + try { + + const tooltipSelectors = [ + '[role="tooltip"]', + '[id^="tooltip-"]', + '.tippy-box', + '[data-tippy-root]' + ]; + + tooltipSelectors.forEach(selector => { + try { + const elements = document.querySelectorAll(selector); + + elements.forEach(element => { + try { + + if (!(element instanceof Element)) return; + + const isDetached = !element.parentElement || + !document.body.contains(element.parentElement) || + element.classList.contains('hidden') || + element.style.display === 'none' || + element.style.visibility === 'hidden'; + + if (isDetached) { + try { + element.remove(); + removedElements++; + } catch (e) { + + } + } + } catch (err) { + + } + }); + } catch (err) { + + log(`Error querying for ${selector}: ${err.message}`); + } + }); + } catch (e) { + log(`Error in tooltip DOM cleanup: ${e.message}`); + } + + if (removedElements > 0) { + log(`Removed ${removedElements} detached tooltip elements`); + } + }, + setDebugMode: function(enabled) { state.debug = Boolean(enabled); log(`Debug mode ${state.debug ? 'enabled' : 'disabled'}`); @@ -247,6 +470,17 @@ const CleanupManager = (function() { if (options.debug !== undefined) { this.setDebugMode(options.debug); } + + if (typeof window !== 'undefined' && !options.noAutoCleanup) { + this.addListener(window, 'beforeunload', () => { + this.clearAll(); + }); + } + + if (typeof window !== 'undefined' && !options.noMemoryOptimization) { + this.setupMemoryOptimization(options.memoryOptions || {}); + } + log('CleanupManager initialized'); return this; } @@ -255,16 +489,20 @@ const CleanupManager = (function() { return publicAPI; })(); +if (typeof module !== 'undefined' && module.exports) { + module.exports = CleanupManager; +} -window.CleanupManager = CleanupManager; +if (typeof window !== 'undefined') { + window.CleanupManager = CleanupManager; +} - -document.addEventListener('DOMContentLoaded', function() { - if (!window.cleanupManagerInitialized) { - CleanupManager.initialize(); - window.cleanupManagerInitialized = true; +if (typeof window !== 'undefined' && typeof document !== 'undefined') { + if (document.readyState === 'complete' || document.readyState === 'interactive') { + CleanupManager.initialize({ debug: false }); + } else { + document.addEventListener('DOMContentLoaded', () => { + CleanupManager.initialize({ debug: false }); + }, { once: true }); } -}); - -//console.log('CleanupManager initialized with methods:', Object.keys(CleanupManager)); -console.log('CleanupManager initialized'); +} diff --git a/basicswap/static/js/modules/memory-manager.js b/basicswap/static/js/modules/memory-manager.js index 8a96323..4c7687f 100644 --- a/basicswap/static/js/modules/memory-manager.js +++ b/basicswap/static/js/modules/memory-manager.js @@ -1,132 +1,169 @@ const MemoryManager = (function() { const config = { - tooltipCleanupInterval: 60000, + tooltipCleanupInterval: 300000, + diagnosticsInterval: 600000, + elementVerificationInterval: 300000, maxTooltipsThreshold: 100, - diagnosticsInterval: 300000, - tooltipLifespan: 240000, + maxTooltips: 300, + cleanupThreshold: 1.5, + minTimeBetweenCleanups: 180000, + memoryGrowthThresholdMB: 100, debug: false, - autoCleanup: true, - elementVerificationInterval: 50000, - tooltipSelectors: [ - '[data-tippy-root]', - '[data-tooltip-trigger-id]', - '.tooltip', - '.tippy-box', - '.tippy-content' + 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' ] }; - let mutationObserver = null; - - const safeGet = (obj, path, defaultValue = null) => { - if (!obj) return defaultValue; - const pathParts = path.split('.'); - let result = obj; - for (const part of pathParts) { - if (result === null || result === undefined) return defaultValue; - result = result[part]; - } - return result !== undefined ? result : defaultValue; - }; - const state = { - intervals: new Map(), - trackedTooltips: new Map(), - trackedElements: new WeakMap(), - startTime: Date.now(), + pendingAnimationFrames: new Set(), + pendingTimeouts: new Set(), + cleanupInterval: null, + diagnosticsInterval: null, + elementVerificationInterval: null, + mutationObserver: null, lastCleanupTime: Date.now(), + startTime: Date.now(), + isCleanupRunning: false, metrics: { - tooltipsCreated: 0, - tooltipsDestroyed: 0, - orphanedTooltipsRemoved: 0, - elementsProcessed: 0, + tooltipsRemoved: 0, cleanupRuns: 0, - manualCleanupRuns: 0, - lastMemoryUsage: null + lastMemoryUsage: null, + lastCleanupDetails: {}, + history: [] + }, + originalTooltipFunctions: {} + }; + + function log(message, ...args) { + if (config.debug) { + console.log(`[MemoryManager] ${message}`, ...args); } - }; + } - const log = (message, ...args) => { - if (!config.debug) return; - const now = new Date().toISOString(); - console.log(`[MemoryManager ${now}]`, message, ...args); - }; + function preserveTooltipFunctions() { + if (window.TooltipManager && !state.originalTooltipFunctions.destroy) { + state.originalTooltipFunctions = { + destroy: window.TooltipManager.destroy, + cleanup: window.TooltipManager.cleanup, + create: window.TooltipManager.create + }; + } + } - const logError = (message, error) => { - console.error(`[MemoryManager] ${message}`, error); - }; + 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; + } - const trackTooltip = (element, tooltipInstance) => { try { - if (!element || !tooltipInstance) return; + state.isCleanupRunning = true; + state.lastCleanupTime = now; + state.metrics.cleanupRuns++; - const timestamp = Date.now(); - const tooltipId = element.getAttribute('data-tooltip-trigger-id') || `tooltip_${timestamp}_${Math.random().toString(36).substring(2, 9)}`; + const startTime = performance.now(); + const startMemory = checkMemoryUsage(); - state.trackedTooltips.set(tooltipId, { - timestamp, - element, - instance: tooltipInstance, - processed: false + state.pendingAnimationFrames.forEach(id => { + cancelAnimationFrame(id); }); + state.pendingAnimationFrames.clear(); - state.metrics.tooltipsCreated++; + state.pendingTimeouts.forEach(id => { + clearTimeout(id); + }); + state.pendingTimeouts.clear(); - setTimeout(() => { - if (state.trackedTooltips.has(tooltipId)) { - destroyTooltip(tooltipId); - } - }, config.tooltipLifespan); + const tooltipsResult = removeOrphanedTooltips(); + state.metrics.tooltipsRemoved += tooltipsResult; - return tooltipId; - } catch (error) { - logError('Error tracking tooltip:', error); - return null; - } - }; + const disconnectedResult = checkForDisconnectedElements(); - const destroyTooltip = (tooltipId) => { - try { - const tooltipInfo = state.trackedTooltips.get(tooltipId); - if (!tooltipInfo) return false; + tryRunGarbageCollection(false); - const { element, instance } = tooltipInfo; + const endTime = performance.now(); + const endMemory = checkMemoryUsage(); - if (instance && typeof instance.destroy === 'function') { - instance.destroy(); + 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(); } - if (element && element.removeAttribute) { - element.removeAttribute('data-tooltip-trigger-id'); - element.removeAttribute('aria-describedby'); - } + state.metrics.lastCleanupDetails = runStats; - const tippyRoot = document.querySelector(`[data-for-tooltip-id="${tooltipId}"]`); - if (tippyRoot && tippyRoot.parentNode) { - tippyRoot.parentNode.removeChild(tippyRoot); + if (config.debug) { + log(`Cleanup completed in ${runStats.duration.toFixed(2)}ms, removed ${tooltipsResult} tooltips`); } - state.trackedTooltips.delete(tooltipId); - state.metrics.tooltipsDestroyed++; - return true; } catch (error) { - logError(`Error destroying tooltip ${tooltipId}:`, error); + console.error("Error during cleanup:", error); return false; + } finally { + state.isCleanupRunning = false; } - }; + } - const removeOrphanedTooltips = () => { + function removeOrphanedTooltips() { try { - const tippyRoots = document.querySelectorAll('[data-tippy-root]'); + + 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; + document.querySelector(`[data-tooltip-trigger-id="${tooltipId}"]`) : null; if (!trigger || !document.body.contains(trigger)) { if (root.parentNode) { @@ -136,528 +173,410 @@ const MemoryManager = (function() { } }); - document.querySelectorAll('[data-tooltip-trigger-id]').forEach(trigger => { - const tooltipId = trigger.getAttribute('data-tooltip-trigger-id'); - const root = document.querySelector(`[data-for-tooltip-id="${tooltipId}"]`); - - if (!root) { - trigger.removeAttribute('data-tooltip-trigger-id'); - trigger.removeAttribute('aria-describedby'); - removed++; - } - }); - - state.metrics.orphanedTooltipsRemoved += removed; return removed; } catch (error) { - logError('Error removing orphaned tooltips:', error); + console.error("Error removing orphaned tooltips:", error); return 0; } - }; + } - const checkMemoryUsage = () => { - if (window.performance && window.performance.memory) { - const memoryUsage = { - usedJSHeapSize: window.performance.memory.usedJSHeapSize, - totalJSHeapSize: window.performance.memory.totalJSHeapSize, - jsHeapSizeLimit: window.performance.memory.jsHeapSizeLimit, - percentUsed: (window.performance.memory.usedJSHeapSize / window.performance.memory.jsHeapSizeLimit * 100).toFixed(2) - }; - - state.metrics.lastMemoryUsage = memoryUsage; - return memoryUsage; - } - - return null; - }; - - const checkForDisconnectedElements = () => { + function checkForDisconnectedElements() { try { + + const tooltipTriggers = document.querySelectorAll('[data-tooltip-trigger-id]:not(:hover)'); const disconnectedElements = new Set(); - state.trackedTooltips.forEach((info, id) => { - const { element } = info; - if (element && !document.body.contains(element)) { - disconnectedElements.add(id); + 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 => { - destroyTooltip(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) { - logError('Error checking for disconnected elements:', error); + console.error("Error checking for disconnected elements:", error); return 0; } - }; + } - const setupMutationObserver = () => { - if (mutationObserver) { - mutationObserver.disconnect(); + 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"; } - mutationObserver = new MutationObserver(mutations => { - let needsCleanup = false; + state.metrics.lastMemoryUsage = result; + return result; + } - mutations.forEach(mutation => { - if (mutation.removedNodes.length) { - Array.from(mutation.removedNodes).forEach(node => { - if (node.nodeType === 1) { - if (node.hasAttribute && node.hasAttribute('data-tooltip-trigger-id')) { - const tooltipId = node.getAttribute('data-tooltip-trigger-id'); - destroyTooltip(tooltipId); - needsCleanup = true; - } - - if (node.querySelectorAll) { - const tooltipTriggers = node.querySelectorAll('[data-tooltip-trigger-id]'); - if (tooltipTriggers.length > 0) { - tooltipTriggers.forEach(el => { - const tooltipId = el.getAttribute('data-tooltip-trigger-id'); - destroyTooltip(tooltipId); - }); - needsCleanup = true; - } - } - } - }); - } - }); - - if (needsCleanup) { - removeOrphanedTooltips(); - } - }); - - mutationObserver.observe(document.body, { - childList: true, - subtree: true - }); - - return mutationObserver; - }; - - const performCleanup = (force = false) => { - try { - log('Starting tooltip cleanup' + (force ? ' (forced)' : '')); - - state.lastCleanupTime = Date.now(); - state.metrics.cleanupRuns++; - - if (force) { - state.metrics.manualCleanupRuns++; - } - - document.querySelectorAll('[data-tippy-root]').forEach(root => { - const instance = safeGet(root, '_tippy'); - if (instance && instance._animationFrame) { - cancelAnimationFrame(instance._animationFrame); - instance._animationFrame = null; - } - }); - - const orphanedRemoved = removeOrphanedTooltips(); - - const disconnectedRemoved = checkForDisconnectedElements(); - - const tooltipCount = document.querySelectorAll('[data-tippy-root]').length; - const triggerCount = document.querySelectorAll('[data-tooltip-trigger-id]').length; - - if (force || tooltipCount > config.maxTooltipsThreshold) { - if (tooltipCount > config.maxTooltipsThreshold) { - document.querySelectorAll('[data-tooltip-trigger-id]').forEach(trigger => { - const tooltipId = trigger.getAttribute('data-tooltip-trigger-id'); - destroyTooltip(tooltipId); - }); - - document.querySelectorAll('[data-tippy-root]').forEach(root => { - if (root.parentNode) { - root.parentNode.removeChild(root); - } - }); - } - - document.querySelectorAll('[data-tooltip-trigger-id], [aria-describedby]').forEach(el => { - if (window.CleanupManager && window.CleanupManager.removeListenersByElement) { - window.CleanupManager.removeListenersByElement(el); - } else { - if (el.parentNode) { - const clone = el.cloneNode(true); - el.parentNode.replaceChild(clone, el); - } - } - }); - } - - if (window.gc) { - window.gc(); - } else if (force) { - const arr = new Array(1000); - for (let i = 0; i < 1000; i++) { - arr[i] = new Array(10000).join('x'); - } - } - - checkMemoryUsage(); - - const result = { - orphanedRemoved, - disconnectedRemoved, - tooltipCount: document.querySelectorAll('[data-tippy-root]').length, - triggerCount: document.querySelectorAll('[data-tooltip-trigger-id]').length, - memoryUsage: state.metrics.lastMemoryUsage - }; - - log('Cleanup completed', result); - return result; - } catch (error) { - logError('Error during cleanup:', error); - return { error: error.message }; + function handleVisibilityChange() { + if (document.hidden) { + removeOrphanedTooltips(); + checkForDisconnectedElements(); } - }; + } - const runDiagnostics = () => { - try { - log('Running memory diagnostics'); - - const memoryUsage = checkMemoryUsage(); - const tooltipCount = document.querySelectorAll('[data-tippy-root]').length; - const triggerCount = document.querySelectorAll('[data-tooltip-trigger-id]').length; - - const diagnostics = { - time: new Date().toISOString(), - uptime: Date.now() - state.startTime, - memoryUsage, - elementsCount: { - tippyRoots: tooltipCount, - tooltipTriggers: triggerCount, - orphanedTriggers: triggerCount - tooltipCount > 0 ? triggerCount - tooltipCount : 0, - orphanedTooltips: tooltipCount - triggerCount > 0 ? tooltipCount - triggerCount : 0 - }, - metrics: { ...state.metrics }, - issues: [] - }; - - if (tooltipCount > config.maxTooltipsThreshold) { - diagnostics.issues.push({ - severity: 'high', - message: `Excessive tooltip count: ${tooltipCount} (threshold: ${config.maxTooltipsThreshold})`, - recommendation: 'Run cleanup and check for tooltip creation loops' - }); - } - - if (Math.abs(tooltipCount - triggerCount) > 10) { - diagnostics.issues.push({ - severity: 'medium', - message: `Mismatch between tooltips (${tooltipCount}) and triggers (${triggerCount})`, - recommendation: 'Remove orphaned tooltips and tooltip triggers' - }); - } - - if (memoryUsage && memoryUsage.percentUsed > 80) { - diagnostics.issues.push({ - severity: 'high', - message: `High memory usage: ${memoryUsage.percentUsed}%`, - recommendation: 'Force garbage collection and check for memory leaks' - }); - } - - if (config.autoCleanup && diagnostics.issues.some(issue => issue.severity === 'high')) { - log('Critical issues detected, triggering automatic cleanup'); - performCleanup(true); - } - - return diagnostics; - } catch (error) { - logError('Error running diagnostics:', error); - return { error: error.message }; + function setupMutationObserver() { + if (state.mutationObserver) { + state.mutationObserver.disconnect(); + state.mutationObserver = null; } - }; - const patchTooltipManager = () => { - try { - if (!window.TooltipManager) { - log('TooltipManager not found'); - return false; + 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; } - log('Patching TooltipManager'); - - const originalCreate = window.TooltipManager.create; - const originalDestroy = window.TooltipManager.destroy; - const originalCleanup = window.TooltipManager.cleanup; - - window.TooltipManager.create = function(element, content, options = {}) { - if (!element) return null; - - try { - const result = originalCreate.call(this, element, content, options); - const tooltipId = element.getAttribute('data-tooltip-trigger-id'); - - if (tooltipId) { - const tippyInstance = safeGet(element, '_tippy') || null; - trackTooltip(element, tippyInstance); - } - - return result; - } catch (error) { - logError('Error in patched create:', error); - return originalCreate.call(this, element, content, options); - } - }; - - window.TooltipManager.destroy = function(element) { - if (!element) return; - - try { - const tooltipId = element.getAttribute('data-tooltip-trigger-id'); - - originalDestroy.call(this, element); - - if (tooltipId) { - state.trackedTooltips.delete(tooltipId); - state.metrics.tooltipsDestroyed++; - } - } catch (error) { - logError('Error in patched destroy:', error); - originalDestroy.call(this, element); - } - }; - - window.TooltipManager.cleanup = function() { - try { - originalCleanup.call(this); - removeOrphanedTooltips(); - } catch (error) { - logError('Error in patched cleanup:', error); - originalCleanup.call(this); - } - }; - - return true; - } catch (error) { - logError('Error patching TooltipManager:', error); - return false; - } - }; - - const patchTippy = () => { - try { - if (typeof tippy !== 'function') { - log('tippy.js not found globally'); - return false; - } - - log('Patching global tippy'); - - const originalTippy = window.tippy; - - window.tippy = function(...args) { - const result = originalTippy.apply(this, args); - - if (Array.isArray(result)) { - result.forEach(instance => { - const reference = instance.reference; - - if (reference) { - const originalShow = instance.show; - const originalHide = instance.hide; - const originalDestroy = instance.destroy; - - instance.show = function(...showArgs) { - return originalShow.apply(this, showArgs); - }; - - instance.hide = function(...hideArgs) { - return originalHide.apply(this, hideArgs); - }; - - instance.destroy = function(...destroyArgs) { - return originalDestroy.apply(this, destroyArgs); - }; - } - }); - } - - return result; - }; - - Object.assign(window.tippy, originalTippy); - - return true; - } catch (error) { - logError('Error patching tippy:', error); - return false; - } - }; - - const startMonitoring = () => { - try { - stopMonitoring(); - - state.intervals.set('cleanup', setInterval(() => { - performCleanup(); - }, config.tooltipCleanupInterval)); - - state.intervals.set('diagnostics', setInterval(() => { - runDiagnostics(); - }, config.diagnosticsInterval)); - - state.intervals.set('elementVerification', setInterval(() => { - checkForDisconnectedElements(); - }, config.elementVerificationInterval)); - - setupMutationObserver(); - - log('Monitoring started'); - return true; - } catch (error) { - logError('Error starting monitoring:', error); - return false; - } - }; - - const stopMonitoring = () => { - try { - state.intervals.forEach((interval, key) => { - clearInterval(interval); - }); - - state.intervals.clear(); - - if (mutationObserver) { - mutationObserver.disconnect(); - mutationObserver = null; - } - - log('Monitoring stopped'); - return true; - } catch (error) { - logError('Error stopping monitoring:', error); - return false; - } - }; - - const autoFix = () => { - try { - log('Running auto-fix'); - - performCleanup(true); - - document.querySelectorAll('[data-tooltip-trigger-id]').forEach(element => { - const tooltipId = element.getAttribute('data-tooltip-trigger-id'); - const duplicates = document.querySelectorAll(`[data-tooltip-trigger-id="${tooltipId}"]`); - - if (duplicates.length > 1) { - for (let i = 1; i < duplicates.length; i++) { - duplicates[i].removeAttribute('data-tooltip-trigger-id'); - duplicates[i].removeAttribute('aria-describedby'); - } - } - }); - - const tippyRoots = document.querySelectorAll('[data-tippy-root]'); - tippyRoots.forEach(root => { - if (!document.body.contains(root) && root.parentNode) { - root.parentNode.removeChild(root); - } - }); - - if (window.TooltipManager && window.TooltipManager.getInstance) { - const manager = window.TooltipManager.getInstance(); - if (manager && manager.chartRefs && manager.chartRefs.clear) { - manager.chartRefs.clear(); - } - - if (manager && manager.tooltipElementsMap && manager.tooltipElementsMap.clear) { - manager.tooltipElementsMap.clear(); - } - } - - patchTooltipManager(); - patchTippy(); - - return true; - } catch (error) { - logError('Error during auto-fix:', error); - return false; - } - }; - - const initialize = (options = {}) => { - try { - Object.assign(config, options); - - if (document.head) { - const metaCache = document.createElement('meta'); - metaCache.setAttribute('http-equiv', 'Cache-Control'); - metaCache.setAttribute('content', 'no-store, max-age=0'); - document.head.appendChild(metaCache); - } - - patchTooltipManager(); - patchTippy(); - - startMonitoring(); - - if (window.CleanupManager && window.CleanupManager.registerResource) { - window.CleanupManager.registerResource('memorymanager', MemoryManager, (optimizer) => { - optimizer.dispose(); - }); - } - - log('Memory Optimizer initialized', config); + processingScheduled = true; setTimeout(() => { - runDiagnostics(); + 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); + }; - return MemoryManager; - } catch (error) { - logError('Error initializing Memory Optimizer:', error); - return null; + 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; } - }; - const dispose = () => { - try { - log('Disposing Memory Optimizer'); - - performCleanup(true); - - stopMonitoring(); - - state.trackedTooltips.clear(); - - return true; - } catch (error) { - logError('Error disposing Memory Optimizer:', error); - return false; + 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, - dispose, - performCleanup, - runDiagnostics, - autoFix, - getConfig: () => ({ ...config }), - getMetrics: () => ({ ...state.metrics }), - setDebugMode: (enabled) => { + 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 }; })(); -if (typeof document !== 'undefined') { - document.addEventListener('DOMContentLoaded', function() { - MemoryManager.initialize(); +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; -console.log('Memory Manager initialized'); diff --git a/basicswap/static/js/modules/price-manager.js b/basicswap/static/js/modules/price-manager.js index 17fe22a..b109bc0 100644 --- a/basicswap/static/js/modules/price-manager.js +++ b/basicswap/static/js/modules/price-manager.js @@ -59,7 +59,7 @@ const PriceManager = (function() { return fetchPromise; } - console.log('PriceManager: Fetching latest prices.'); + //console.log('PriceManager: Fetching latest prices.'); lastFetchTime = Date.now(); fetchPromise = this.fetchPrices() .then(prices => { @@ -89,7 +89,7 @@ const PriceManager = (function() { ? window.config.coins.map(c => c.symbol).filter(symbol => symbol && symbol.trim() !== '') : ['BTC', 'XMR', 'PART', 'BCH', 'PIVX', 'FIRO', 'DASH', 'LTC', 'DOGE', 'DCR', 'NMC', 'WOW']); - console.log('PriceManager: lookupFiatRates ' + coinSymbols.join(', ')); + //console.log('PriceManager: lookupFiatRates ' + coinSymbols.join(', ')); if (!coinSymbols.length) { throw new Error('No valid coins configured'); diff --git a/basicswap/static/js/modules/tooltips-manager.js b/basicswap/static/js/modules/tooltips-manager.js index 69219f0..59f70b2 100644 --- a/basicswap/static/js/modules/tooltips-manager.js +++ b/basicswap/static/js/modules/tooltips-manager.js @@ -1,6 +1,5 @@ const TooltipManager = (function() { let instance = null; - const tooltipInstanceMap = new WeakMap(); class TooltipManagerImpl { @@ -8,20 +7,22 @@ const TooltipManager = (function() { if (instance) { return instance; } - this.pendingAnimationFrames = new Set(); - this.pendingTimeouts = new Set(); + this.tooltipIdCounter = 0; this.maxTooltips = 200; this.cleanupThreshold = 1.2; - this.disconnectedCheckInterval = null; - this.cleanupInterval = null; - this.mutationObserver = null; this.debug = false; this.tooltipData = new WeakMap(); - this.setupStyles(); - this.setupMutationObserver(); - this.startPeriodicCleanup(); - this.startDisconnectedElementsCheck(); + this.resources = {}; + + if (window.CleanupManager) { + CleanupManager.registerResource( + 'tooltipManager', + this, + (manager) => manager.dispose() + ); + } + instance = this; } @@ -33,7 +34,7 @@ const TooltipManager = (function() { create(element, content, options = {}) { if (!element || !document.body.contains(element)) return null; - + if (!document.contains(element)) { this.log('Tried to create tooltip for detached element'); return null; @@ -47,9 +48,7 @@ const TooltipManager = (function() { this.performPeriodicCleanup(true); } - const rafId = requestAnimationFrame(() => { - this.pendingAnimationFrames.delete(rafId); - + const createTooltip = () => { if (!document.body.contains(element)) return; const rect = element.getBoundingClientRect(); @@ -58,7 +57,7 @@ const TooltipManager = (function() { } else { let retryCount = 0; const maxRetries = 3; - + const retryCreate = () => { const newRect = element.getBoundingClientRect(); if ((newRect.width > 0 && newRect.height > 0) || retryCount >= maxRetries) { @@ -67,30 +66,29 @@ const TooltipManager = (function() { } } else { retryCount++; - const timeoutId = setTimeout(() => { - this.pendingTimeouts.delete(timeoutId); - const newRafId = requestAnimationFrame(retryCreate); - this.pendingAnimationFrames.add(newRafId); + CleanupManager.setTimeout(() => { + CleanupManager.requestAnimationFrame(retryCreate); }, 100); - this.pendingTimeouts.add(timeoutId); } }; - const initialTimeoutId = setTimeout(() => { - this.pendingTimeouts.delete(initialTimeoutId); - const retryRafId = requestAnimationFrame(retryCreate); - this.pendingAnimationFrames.add(retryRafId); + CleanupManager.setTimeout(() => { + CleanupManager.requestAnimationFrame(retryCreate); }, 100); - this.pendingTimeouts.add(initialTimeoutId); } - }); + }; - this.pendingAnimationFrames.add(rafId); + CleanupManager.requestAnimationFrame(createTooltip); return null; } - + createTooltipInstance(element, content, options = {}) { - if (!element || !document.body.contains(element) || !window.tippy) { + if (!element || !document.body.contains(element)) { + return null; + } + + if (typeof window.tippy !== 'function') { + console.error('Tippy.js is not available.'); return null; } @@ -130,7 +128,7 @@ const TooltipManager = (function() { }, onHidden(instance) { if (!document.body.contains(element)) { - setTimeout(() => { + CleanupManager.setTimeout(() => { if (instance && instance.destroy) { instance.destroy(); } @@ -168,9 +166,26 @@ const TooltipManager = (function() { }); element.setAttribute('data-tooltip-trigger-id', tooltipId); - tooltipInstanceMap.set(element, tippyInstance[0]); + const resourceId = CleanupManager.registerResource( + 'tooltip', + { element, instance: tippyInstance[0] }, + (resource) => { + try { + if (resource.instance && resource.instance.destroy) { + resource.instance.destroy(); + } + if (resource.element) { + resource.element.removeAttribute('data-tooltip-trigger-id'); + resource.element.removeAttribute('aria-describedby'); + } + } catch (e) { + console.warn('Error destroying tooltip during cleanup:', e); + } + } + ); + return tippyInstance[0]; } @@ -214,181 +229,292 @@ const TooltipManager = (function() { } } + getActiveTooltipInstances() { + const result = []; + try { + document.querySelectorAll('[data-tooltip-trigger-id]').forEach(element => { + const instance = element._tippy ? [element._tippy] : null; + if (instance) { + result.push([element, instance]); + } + }); + } catch (error) { + console.error('Error getting active tooltip instances:', error); + } + return result; + } + cleanup() { this.log('Running tooltip cleanup'); - this.pendingAnimationFrames.forEach(id => { - cancelAnimationFrame(id); - }); - this.pendingAnimationFrames.clear(); - - this.pendingTimeouts.forEach(id => { - clearTimeout(id); - }); - this.pendingTimeouts.clear(); - - const elements = document.querySelectorAll('[data-tooltip-trigger-id]'); - const batchSize = 20; - - const processElementsBatch = (startIdx) => { - const endIdx = Math.min(startIdx + batchSize, elements.length); - - for (let i = startIdx; i < endIdx; i++) { - this.destroy(elements[i]); + try { + if ((window.location.pathname.includes('/offers') || window.location.pathname.includes('/bids')) && + (document.querySelector('[data-tippy-root]:hover') || document.querySelector('[data-tooltip-trigger-id]:hover'))) { + console.log('Skipping tooltip cleanup - tooltip is being hovered'); + return; } - if (endIdx < elements.length) { - const rafId = requestAnimationFrame(() => { - this.pendingAnimationFrames.delete(rafId); - processElementsBatch(endIdx); - }); - this.pendingAnimationFrames.add(rafId); + const elements = document.querySelectorAll('[data-tooltip-trigger-id]:not(:hover)'); + const batchSize = 20; + + const processElementsBatch = (startIdx) => { + const endIdx = Math.min(startIdx + batchSize, elements.length); + + for (let i = startIdx; i < endIdx; i++) { + this.destroy(elements[i]); + } + + if (endIdx < elements.length) { + CleanupManager.requestAnimationFrame(() => { + processElementsBatch(endIdx); + }); + } else { + this.cleanupOrphanedTooltips(); + } + }; + + if (elements.length > 0) { + processElementsBatch(0); } else { this.cleanupOrphanedTooltips(); } - }; + } catch (error) { + console.error('Error during cleanup:', error); + } + } - if (elements.length > 0) { - processElementsBatch(0); - } else { - this.cleanupOrphanedTooltips(); + thoroughCleanup() { + this.log('Running thorough tooltip cleanup'); + + try { + this.cleanup(); + this.cleanupAllTooltips(); + this.log('Thorough tooltip cleanup completed'); + } catch (error) { + console.error('Error in thorough tooltip cleanup:', error); + } + } + + cleanupAllTooltips() { + this.log('Cleaning up all tooltips'); + + try { + if ((window.location.pathname.includes('/offers') || window.location.pathname.includes('/bids')) && + document.querySelector('#offers-body tr:hover')) { + this.log('Skipping all tooltips cleanup on offers/bids page with row hover'); + return; + } + + const tooltipRoots = document.querySelectorAll('[data-tippy-root]'); + const tooltipTriggers = document.querySelectorAll('[data-tooltip-trigger-id]'); + const tooltipElements = document.querySelectorAll('.tooltip'); + + const isHovered = element => { + try { + return element.matches && element.matches(':hover'); + } catch (e) { + + return false; + } + }; + + tooltipRoots.forEach(root => { + if (!isHovered(root) && root.parentNode) { + root.parentNode.removeChild(root); + } + }); + + tooltipTriggers.forEach(trigger => { + if (!isHovered(trigger)) { + trigger.removeAttribute('data-tooltip-trigger-id'); + trigger.removeAttribute('aria-describedby'); + + if (trigger._tippy) { + try { + trigger._tippy.destroy(); + trigger._tippy = null; + } catch (e) {} + } + } + }); + + tooltipElements.forEach(tooltip => { + if (!isHovered(tooltip) && tooltip.parentNode) { + let closestHoveredRow = false; + + try { + if (tooltip.closest && tooltip.closest('tr') && isHovered(tooltip.closest('tr'))) { + closestHoveredRow = true; + } + } catch (e) {} + + if (!closestHoveredRow) { + const style = window.getComputedStyle(tooltip); + const isVisible = style.display !== 'none' && + style.visibility !== 'hidden' && + style.opacity !== '0'; + + if (!isVisible) { + tooltip.parentNode.removeChild(tooltip); + } + } + } + }); + } catch (error) { + console.error('Error cleaning up all tooltips:', error); } } cleanupOrphanedTooltips() { - const tippyElements = document.querySelectorAll('[data-tippy-root]'); - let removed = 0; + try { + const tippyElements = document.querySelectorAll('[data-tippy-root]'); + let removed = 0; - tippyElements.forEach(element => { - const tooltipId = element.getAttribute('data-for-tooltip-id'); - const trigger = tooltipId ? - document.querySelector(`[data-tooltip-trigger-id="${tooltipId}"]`) : - null; + tippyElements.forEach(element => { + const tooltipId = element.getAttribute('data-for-tooltip-id'); + const trigger = tooltipId ? + document.querySelector(`[data-tooltip-trigger-id="${tooltipId}"]`) : + null; - if (!trigger || !document.body.contains(trigger)) { - if (element.parentNode) { - element.parentNode.removeChild(element); - removed++; + if (!trigger || !document.body.contains(trigger)) { + if (element.parentNode) { + element.parentNode.removeChild(element); + removed++; + } } + }); + + if (removed > 0) { + this.log(`Removed ${removed} orphaned tooltip elements`); } - }); - if (removed > 0) { - this.log(`Removed ${removed} orphaned tooltip elements`); + return removed; + } catch (error) { + console.error('Error cleaning up orphaned tooltips:', error); + return 0; } - - return removed; } setupMutationObserver() { - if (this.mutationObserver) { - this.mutationObserver.disconnect(); - } + try { + const mutationObserver = new MutationObserver(mutations => { + let needsCleanup = false; - this.mutationObserver = new MutationObserver(mutations => { - let needsCleanup = false; - - mutations.forEach(mutation => { - if (mutation.removedNodes.length) { - Array.from(mutation.removedNodes).forEach(node => { - if (node.nodeType === Node.ELEMENT_NODE) { - if (node.hasAttribute && node.hasAttribute('data-tooltip-trigger-id')) { - this.destroy(node); - needsCleanup = true; - } - - if (node.querySelectorAll) { - const tooltipTriggers = node.querySelectorAll('[data-tooltip-trigger-id]'); - if (tooltipTriggers.length > 0) { - tooltipTriggers.forEach(trigger => { - this.destroy(trigger); - }); + mutations.forEach(mutation => { + if (mutation.removedNodes.length) { + Array.from(mutation.removedNodes).forEach(node => { + if (node.nodeType === Node.ELEMENT_NODE) { + if (node.hasAttribute && node.hasAttribute('data-tooltip-trigger-id')) { + this.destroy(node); needsCleanup = true; } + + if (node.querySelectorAll) { + const tooltipTriggers = node.querySelectorAll('[data-tooltip-trigger-id]'); + if (tooltipTriggers.length > 0) { + tooltipTriggers.forEach(trigger => { + this.destroy(trigger); + }); + needsCleanup = true; + } + } } - } - }); + }); + } + }); + + if (needsCleanup) { + this.cleanupOrphanedTooltips(); } }); - - if (needsCleanup) { - this.cleanupOrphanedTooltips(); - } - }); - this.mutationObserver.observe(document.body, { - childList: true, - subtree: true - }); + mutationObserver.observe(document.body, { + childList: true, + subtree: true + }); - return this.mutationObserver; + this.resources.mutationObserver = CleanupManager.registerResource( + 'mutationObserver', + mutationObserver, + (observer) => observer.disconnect() + ); + + return mutationObserver; + } catch (error) { + console.error('Error setting up mutation observer:', error); + return null; + } } startDisconnectedElementsCheck() { - if (this.disconnectedCheckInterval) { - clearInterval(this.disconnectedCheckInterval); + try { + this.resources.disconnectedCheckInterval = CleanupManager.setInterval(() => { + this.checkForDisconnectedElements(); + }, 60000); + } catch (error) { + console.error('Error starting disconnected elements check:', error); } - - this.disconnectedCheckInterval = setInterval(() => { - this.checkForDisconnectedElements(); - }, 60000); } checkForDisconnectedElements() { - const elements = document.querySelectorAll('[data-tooltip-trigger-id]'); - let removedCount = 0; + try { + const elements = document.querySelectorAll('[data-tooltip-trigger-id]'); + let removedCount = 0; - elements.forEach(element => { - if (!document.body.contains(element)) { - this.destroy(element); - removedCount++; + elements.forEach(element => { + if (!document.body.contains(element)) { + this.destroy(element); + removedCount++; + } + }); + + if (removedCount > 0) { + this.log(`Removed ${removedCount} tooltips for disconnected elements`); + this.cleanupOrphanedTooltips(); } - }); - - if (removedCount > 0) { - this.log(`Removed ${removedCount} tooltips for disconnected elements`); - this.cleanupOrphanedTooltips(); + } catch (error) { + console.error('Error checking for disconnected elements:', error); } } startPeriodicCleanup() { - if (this.cleanupInterval) { - clearInterval(this.cleanupInterval); + try { + this.resources.cleanupInterval = CleanupManager.setInterval(() => { + this.performPeriodicCleanup(); + }, 120000); + } catch (error) { + console.error('Error starting periodic cleanup:', error); } - - this.cleanupInterval = setInterval(() => { - this.performPeriodicCleanup(); - }, 120000); } performPeriodicCleanup(force = false) { - this.cleanupOrphanedTooltips(); - - this.checkForDisconnectedElements(); - - const tooltipCount = document.querySelectorAll('[data-tippy-root]').length; - - if (force || tooltipCount > this.maxTooltips) { - this.log(`Performing aggressive cleanup (${tooltipCount} tooltips)`); - - this.cleanup(); - - if (window.gc) { - window.gc(); - } else { - const arr = new Array(1000); - for (let i = 0; i < 1000; i++) { - arr[i] = new Array(10000).join('x'); - } + try { + if ((window.location.pathname.includes('/offers') || window.location.pathname.includes('/bids')) && + !force) { + return; } + + this.cleanupOrphanedTooltips(); + this.checkForDisconnectedElements(); + + const tooltipCount = document.querySelectorAll('[data-tippy-root]').length; + + if (force || tooltipCount > this.maxTooltips) { + this.log(`Performing aggressive cleanup (${tooltipCount} tooltips)`); + this.cleanup(); + } + } catch (error) { + console.error('Error performing periodic cleanup:', error); } } setupStyles() { if (document.getElementById('tooltip-styles')) return; - document.head.insertAdjacentHTML('beforeend', ` - - `); + `; + document.head.appendChild(style); + + this.resources.tooltipStyles = CleanupManager.registerResource( + 'tooltipStyles', + style, + (styleElement) => { + if (styleElement && styleElement.parentNode) { + styleElement.parentNode.removeChild(styleElement); + } + } + ); + } catch (error) { + console.error('Error setting up styles:', error); + try { + document.head.insertAdjacentHTML('beforeend', ` + + `); + + const styleElement = document.getElementById('tooltip-styles'); + if (styleElement) { + this.resources.tooltipStyles = CleanupManager.registerResource( + 'tooltipStyles', + styleElement, + (elem) => { + if (elem && elem.parentNode) { + elem.parentNode.removeChild(elem); + } + } + ); + } + } catch (e) { + console.error('Failed to add tooltip styles:', e); + } + } } initializeTooltips(selector = '[data-tooltip-target]') { - document.querySelectorAll(selector).forEach(element => { - const targetId = element.getAttribute('data-tooltip-target'); - if (!targetId) return; + try { + document.querySelectorAll(selector).forEach(element => { + const targetId = element.getAttribute('data-tooltip-target'); + if (!targetId) return; - const tooltipContent = document.getElementById(targetId); + const tooltipContent = document.getElementById(targetId); - if (tooltipContent) { - this.create(element, tooltipContent.innerHTML, { - placement: element.getAttribute('data-tooltip-placement') || 'top' - }); - } - }); + if (tooltipContent) { + this.create(element, tooltipContent.innerHTML, { + placement: element.getAttribute('data-tooltip-placement') || 'top' + }); + } + }); + } catch (error) { + console.error('Error initializing tooltips:', error); + } } dispose() { this.log('Disposing TooltipManager'); - - this.cleanup(); - this.pendingAnimationFrames.forEach(id => { - cancelAnimationFrame(id); - }); - this.pendingAnimationFrames.clear(); + try { + this.cleanup(); - this.pendingTimeouts.forEach(id => { - clearTimeout(id); - }); - this.pendingTimeouts.clear(); + Object.values(this.resources).forEach(resourceId => { + if (resourceId) { + CleanupManager.unregisterResource(resourceId); + } + }); - if (this.disconnectedCheckInterval) { - clearInterval(this.disconnectedCheckInterval); - this.disconnectedCheckInterval = null; + this.resources = {}; + + instance = null; + return true; + } catch (error) { + console.error('Error disposing TooltipManager:', error); + return false; } - - if (this.cleanupInterval) { - clearInterval(this.cleanupInterval); - this.cleanupInterval = null; - } - - if (this.mutationObserver) { - this.mutationObserver.disconnect(); - this.mutationObserver = null; - } - - const styleElement = document.getElementById('tooltip-styles'); - if (styleElement && styleElement.parentNode) { - styleElement.parentNode.removeChild(styleElement); - } - - instance = null; - return true; } setDebugMode(enabled) { @@ -536,16 +766,26 @@ const TooltipManager = (function() { } initialize(options = {}) { - if (options.maxTooltips) { - this.maxTooltips = options.maxTooltips; - } + try { + if (options.maxTooltips) { + this.maxTooltips = options.maxTooltips; + } - if (options.debug !== undefined) { - this.setDebugMode(options.debug); - } + if (options.debug !== undefined) { + this.setDebugMode(options.debug); + } - this.log('TooltipManager initialized'); - return this; + this.setupStyles(); + this.setupMutationObserver(); + this.startPeriodicCleanup(); + this.startDisconnectedElementsCheck(); + + this.log('TooltipManager initialized'); + return this; + } catch (error) { + console.error('Error initializing TooltipManager:', error); + return this; + } } } @@ -580,6 +820,11 @@ const TooltipManager = (function() { return manager.cleanup(...args); }, + thoroughCleanup: function() { + const manager = this.getInstance(); + return manager.thoroughCleanup(); + }, + initializeTooltips: function(...args) { const manager = this.getInstance(); return manager.initializeTooltips(...args); @@ -590,6 +835,11 @@ const TooltipManager = (function() { return manager.setDebugMode(enabled); }, + getActiveTooltipInstances: function() { + const manager = this.getInstance(); + return manager.getActiveTooltipInstances(); + }, + dispose: function(...args) { const manager = this.getInstance(); return manager.dispose(...args); @@ -597,37 +847,53 @@ const TooltipManager = (function() { }; })(); -function installTooltipManager() { - const originalTooltipManager = window.TooltipManager; - - window.TooltipManager = TooltipManager; - - window.TooltipManager.initialize({ - maxTooltips: 200, - debug: false - }); - - document.addEventListener('DOMContentLoaded', function() { - if (!window.tooltipManagerInitialized) { - window.TooltipManager.initializeTooltips(); - window.tooltipManagerInitialized = true; - } - }); - - return originalTooltipManager; -} - -if (typeof document !== 'undefined') { - if (document.readyState === 'complete' || document.readyState === 'interactive') { - installTooltipManager(); - } else { - document.addEventListener('DOMContentLoaded', installTooltipManager); - } -} - if (typeof module !== 'undefined' && module.exports) { module.exports = TooltipManager; } -window.TooltipManager = TooltipManager; -console.log('TooltipManager initialized'); +if (typeof window !== 'undefined') { + window.TooltipManager = TooltipManager; +} + +if (typeof window !== 'undefined' && typeof document !== 'undefined') { + function initializeTooltipManager() { + if (!window.tooltipManagerInitialized) { + + if (!window.CleanupManager) { + console.warn('CleanupManager not found. TooltipManager will run with limited functionality.'); + + window.CleanupManager = window.CleanupManager || { + registerResource: (type, resource, cleanup) => { + return Math.random().toString(36).substring(2, 9); + }, + unregisterResource: () => {}, + setTimeout: (callback, delay) => setTimeout(callback, delay), + setInterval: (callback, delay) => setInterval(callback, delay), + requestAnimationFrame: (callback) => requestAnimationFrame(callback), + addListener: (element, type, handler, options) => { + element.addEventListener(type, handler, options); + return handler; + } + }; + } + + window.TooltipManager.initialize({ + maxTooltips: 200, + debug: false + }); + + window.TooltipManager.initializeTooltips(); + window.tooltipManagerInitialized = true; + } + } + + if (document.readyState === 'complete' || document.readyState === 'interactive') { + initializeTooltipManager(); + } else { + document.addEventListener('DOMContentLoaded', initializeTooltipManager, { once: true }); + } +} + +if (typeof window !== 'undefined' && typeof console !== 'undefined') { + console.log('TooltipManager initialized'); +} diff --git a/basicswap/static/js/offers.js b/basicswap/static/js/offers.js index 8cf7f71..18ba205 100644 --- a/basicswap/static/js/offers.js +++ b/basicswap/static/js/offers.js @@ -990,7 +990,7 @@ function createTableRow(offer, identity = null) { } let coinFromDisplay, coinToDisplay; - + if (window.CoinManager) { coinFromDisplay = window.CoinManager.getDisplayName(coinFrom) || coinFrom; coinToDisplay = window.CoinManager.getDisplayName(coinTo) || coinTo; @@ -1000,7 +1000,7 @@ function createTableRow(offer, identity = null) { if (coinFromDisplay.toLowerCase() === 'zcoin') coinFromDisplay = 'Firo'; if (coinToDisplay.toLowerCase() === 'zcoin') coinToDisplay = 'Firo'; } - + const postedTime = formatTime(createdAt, true); const expiresIn = formatTime(expireAt); const currentTime = Math.floor(Date.now() / 1000); @@ -1370,7 +1370,6 @@ function createRecipientTooltip(uniqueId, identityInfo, identity, successRate, t return 'text-red-600'; }; - const truncateText = (text, maxLength) => { if (text.length <= maxLength) return text; return text.substring(0, maxLength) + '...'; @@ -1454,7 +1453,7 @@ function createTooltipContent(isSentOffers, coinFrom, coinTo, fromAmount, toAmou const getPriceKey = (coin) => { if (!coin) return null; - + const lowerCoin = coin.toLowerCase(); if (lowerCoin === 'zcoin') return 'firo'; @@ -2167,6 +2166,18 @@ document.addEventListener('DOMContentLoaded', async function() { tableRateModule.init(); } + document.addEventListener('memoryOptimized', (e) => { + if (jsonData && jsonData.length > e.detail.maxDataSize) { + console.log(`Trimming offers data from ${jsonData.length} to ${e.detail.maxDataSize} items`); + jsonData = jsonData.slice(0, e.detail.maxDataSize); + } + + if (originalJsonData && originalJsonData.length > e.detail.maxDataSize) { + console.log(`Trimming original offers data from ${originalJsonData.length} to ${e.detail.maxDataSize} items`); + originalJsonData = originalJsonData.slice(0, e.detail.maxDataSize); + } + }); + await initializeTableAndData(); if (window.PriceManager) { @@ -2179,7 +2190,7 @@ document.addEventListener('DOMContentLoaded', async function() { if (window.WebSocketManager) { WebSocketManager.addMessageHandler('message', async (data) => { if (data.event === 'new_offer' || data.event === 'offer_revoked') { - console.log('WebSocket event received:', data.event); + //console.log('WebSocket event received:', data.event); try { const previousPrices = latestPrices; @@ -2188,7 +2199,7 @@ document.addEventListener('DOMContentLoaded', async function() { if (!offersResponse.ok) { throw new Error(`HTTP error! status: ${offersResponse.status}`); } - + const newData = await offersResponse.json(); const processedNewData = Array.isArray(newData) ? newData : Object.values(newData); jsonData = formatInitialData(processedNewData); @@ -2200,7 +2211,7 @@ document.addEventListener('DOMContentLoaded', async function() { } else { priceData = await fetchLatestPrices(); } - + if (priceData) { latestPrices = priceData; CacheManager.set('prices_coingecko', priceData, 'prices'); @@ -2230,7 +2241,7 @@ document.addEventListener('DOMContentLoaded', async function() { updatePaginationInfo(); - console.log('WebSocket-triggered refresh completed successfully'); + //console.log('WebSocket-triggered refresh completed successfully'); } catch (error) { console.error('Error during WebSocket-triggered refresh:', error); NetworkManager.handleNetworkError(error); @@ -2357,28 +2368,10 @@ function cleanup() { if (window.CleanupManager) { window.CleanupManager.removeListenersByElement(row); } - - while (row.attributes && row.attributes.length > 0) { - row.removeAttribute(row.attributes[0].name); - } - - while (row.firstChild) { - row.removeChild(row.firstChild); - } }); - offersBody.innerHTML = ''; } - const filterForm = document.getElementById('filterForm'); - if (filterForm) { - CleanupManager.removeListenersByElement(filterForm); - - filterForm.querySelectorAll('select').forEach(select => { - CleanupManager.removeListenersByElement(select); - }); - } - jsonData = null; originalJsonData = null; latestPrices = null; @@ -2388,13 +2381,12 @@ function cleanup() { } if (window.MemoryManager) { - if (window.MemoryManager.cleanupTooltips) { - window.MemoryManager.cleanupTooltips(true); + if (window.MemoryManager.forceCleanup) { + window.MemoryManager.forceCleanup(); } - window.MemoryManager.forceCleanup(); } - console.log('Offers.js cleanup completed'); + //console.log('Offers.js cleanup completed'); } catch (error) { console.error('Error during cleanup:', error); } diff --git a/basicswap/static/js/pricechart.js b/basicswap/static/js/pricechart.js index 8b56ba4..1508ba1 100644 --- a/basicswap/static/js/pricechart.js +++ b/basicswap/static/js/pricechart.js @@ -578,7 +578,7 @@ const chartModule = { this.chartRefs.set(element, chart); }, - destroyChart: function() { +destroyChart: function() { if (chartModule.chart) { try { const chartInstance = chartModule.chart; @@ -592,12 +592,17 @@ const chartModule = { if (canvas) { chartModule.chartRefs.delete(canvas); + + const ctx = canvas.getContext('2d'); + if (ctx) { + ctx.clearRect(0, 0, canvas.width, canvas.height); + } } } catch (e) { console.error('Error destroying chart:', e); } } - }, +}, initChart: function() { this.destroyChart();