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