mirror of
				https://github.com/dgtlmoon/changedetection.io.git
				synced 2025-11-04 00:27:48 +00:00 
			
		
		
		
	
		
			Some checks failed
		
		
	
	Build and push containers / metadata (push) Has been cancelled
				
			Build and push containers / build-push-containers (push) Has been cancelled
				
			Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Has been cancelled
				
			Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Test the built 📦 package works basically. (push) Has been cancelled
				
			Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been cancelled
				
			ChangeDetection.io App Test / lint-code (push) Has been cancelled
				
			ChangeDetection.io App Test / test-application-3-10 (push) Has been cancelled
				
			ChangeDetection.io App Test / test-application-3-11 (push) Has been cancelled
				
			ChangeDetection.io App Test / test-application-3-12 (push) Has been cancelled
				
			ChangeDetection.io App Test / test-application-3-13 (push) Has been cancelled
				
			ChangeDetection.io Container Build Test / test-container-build (push) Has been cancelled
				
			
		
			
				
	
	
		
			162 lines
		
	
	
		
			6.7 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			162 lines
		
	
	
		
			6.7 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
#!/usr/bin/env python3
 | 
						|
 | 
						|
import ctypes
 | 
						|
import gc
 | 
						|
import re
 | 
						|
import psutil
 | 
						|
import sys
 | 
						|
import threading
 | 
						|
import importlib
 | 
						|
from loguru import logger
 | 
						|
 | 
						|
def memory_cleanup(app=None):
 | 
						|
    """
 | 
						|
    Perform comprehensive memory cleanup operations and log memory usage
 | 
						|
    at each step with nicely formatted numbers.
 | 
						|
    
 | 
						|
    Args:
 | 
						|
        app: Optional Flask app instance for clearing Flask-specific caches
 | 
						|
        
 | 
						|
    Returns:
 | 
						|
        str: Status message
 | 
						|
    """
 | 
						|
    # Get current process
 | 
						|
    process = psutil.Process()
 | 
						|
    
 | 
						|
    # Log initial memory usage with nicely formatted numbers
 | 
						|
    current_memory = process.memory_info().rss / 1024 / 1024
 | 
						|
    logger.debug(f"Memory cleanup started - Current memory usage: {current_memory:,.2f} MB")
 | 
						|
 | 
						|
    # 1. Standard garbage collection - force full collection on all generations
 | 
						|
    gc.collect(0)  # Collect youngest generation
 | 
						|
    gc.collect(1)  # Collect middle generation
 | 
						|
    gc.collect(2)  # Collect oldest generation
 | 
						|
 | 
						|
    # Run full collection again to ensure maximum cleanup
 | 
						|
    gc.collect()
 | 
						|
    current_memory = process.memory_info().rss / 1024 / 1024
 | 
						|
    logger.debug(f"After full gc.collect() - Memory usage: {current_memory:,.2f} MB")
 | 
						|
    
 | 
						|
 | 
						|
    # 3. Call libc's malloc_trim to release memory back to the OS
 | 
						|
    libc = ctypes.CDLL("libc.so.6")
 | 
						|
    libc.malloc_trim(0)
 | 
						|
    current_memory = process.memory_info().rss / 1024 / 1024
 | 
						|
    logger.debug(f"After malloc_trim(0) - Memory usage: {current_memory:,.2f} MB")
 | 
						|
    
 | 
						|
    # 4. Clear Python's regex cache
 | 
						|
    re.purge()
 | 
						|
    current_memory = process.memory_info().rss / 1024 / 1024
 | 
						|
    logger.debug(f"After re.purge() - Memory usage: {current_memory:,.2f} MB")
 | 
						|
 | 
						|
    # 5. Reset thread-local storage
 | 
						|
    # Create a new thread local object to encourage cleanup of old ones
 | 
						|
    threading.local()
 | 
						|
    current_memory = process.memory_info().rss / 1024 / 1024
 | 
						|
    logger.debug(f"After threading.local() - Memory usage: {current_memory:,.2f} MB")
 | 
						|
 | 
						|
    # 6. Clear sys.intern cache if Python version supports it
 | 
						|
    try:
 | 
						|
        sys.intern.clear()
 | 
						|
        current_memory = process.memory_info().rss / 1024 / 1024
 | 
						|
        logger.debug(f"After sys.intern.clear() - Memory usage: {current_memory:,.2f} MB")
 | 
						|
    except (AttributeError, TypeError):
 | 
						|
        logger.debug("sys.intern.clear() not supported in this Python version")
 | 
						|
    
 | 
						|
    # 7. Clear XML/lxml caches if available
 | 
						|
    try:
 | 
						|
        # Check if lxml.etree is in use
 | 
						|
        lxml_etree = sys.modules.get('lxml.etree')
 | 
						|
        if lxml_etree:
 | 
						|
            # Clear module-level caches
 | 
						|
            if hasattr(lxml_etree, 'clear_error_log'):
 | 
						|
                lxml_etree.clear_error_log()
 | 
						|
            
 | 
						|
            # Check for _ErrorLog and _RotatingErrorLog objects and clear them
 | 
						|
            for obj in gc.get_objects():
 | 
						|
                if hasattr(obj, '__class__') and hasattr(obj.__class__, '__name__'):
 | 
						|
                    class_name = obj.__class__.__name__
 | 
						|
                    if class_name in ('_ErrorLog', '_RotatingErrorLog', '_DomainErrorLog') and hasattr(obj, 'clear'):
 | 
						|
                        try:
 | 
						|
                            obj.clear()
 | 
						|
                        except (AttributeError, TypeError):
 | 
						|
                            pass
 | 
						|
                    
 | 
						|
                    # Clear Element objects which can hold references to documents
 | 
						|
                    elif class_name in ('_Element', 'ElementBase') and hasattr(obj, 'clear'):
 | 
						|
                        try:
 | 
						|
                            obj.clear()
 | 
						|
                        except (AttributeError, TypeError):
 | 
						|
                            pass
 | 
						|
            
 | 
						|
            current_memory = process.memory_info().rss / 1024 / 1024
 | 
						|
            logger.debug(f"After lxml.etree cleanup - Memory usage: {current_memory:,.2f} MB")
 | 
						|
 | 
						|
        # Check if lxml.html is in use
 | 
						|
        lxml_html = sys.modules.get('lxml.html')
 | 
						|
        if lxml_html:
 | 
						|
            # Clear HTML-specific element types
 | 
						|
            for obj in gc.get_objects():
 | 
						|
                if hasattr(obj, '__class__') and hasattr(obj.__class__, '__name__'):
 | 
						|
                    class_name = obj.__class__.__name__
 | 
						|
                    if class_name in ('HtmlElement', 'FormElement', 'InputElement',
 | 
						|
                                    'SelectElement', 'TextareaElement', 'CheckboxGroup',
 | 
						|
                                    'RadioGroup', 'MultipleSelectOptions', 'FieldsDict') and hasattr(obj, 'clear'):
 | 
						|
                        try:
 | 
						|
                            obj.clear()
 | 
						|
                        except (AttributeError, TypeError):
 | 
						|
                            pass
 | 
						|
 | 
						|
            current_memory = process.memory_info().rss / 1024 / 1024
 | 
						|
            logger.debug(f"After lxml.html cleanup - Memory usage: {current_memory:,.2f} MB")
 | 
						|
    except (ImportError, AttributeError):
 | 
						|
        logger.debug("lxml cleanup not applicable")
 | 
						|
    
 | 
						|
    # 8. Clear JSON parser caches if applicable
 | 
						|
    try:
 | 
						|
        # Check if json module is being used and try to clear its cache
 | 
						|
        json_module = sys.modules.get('json')
 | 
						|
        if json_module and hasattr(json_module, '_default_encoder'):
 | 
						|
            json_module._default_encoder.markers.clear()
 | 
						|
            current_memory = process.memory_info().rss / 1024 / 1024
 | 
						|
            logger.debug(f"After JSON parser cleanup - Memory usage: {current_memory:,.2f} MB")
 | 
						|
    except (AttributeError, KeyError):
 | 
						|
        logger.debug("JSON cleanup not applicable")
 | 
						|
    
 | 
						|
    # 9. Force Python's memory allocator to release unused memory
 | 
						|
    try:
 | 
						|
        if hasattr(sys, 'pypy_version_info'):
 | 
						|
            # PyPy has different memory management
 | 
						|
            gc.collect()
 | 
						|
        else:
 | 
						|
            # CPython - try to release unused memory
 | 
						|
            ctypes.pythonapi.PyGC_Collect()
 | 
						|
            current_memory = process.memory_info().rss / 1024 / 1024
 | 
						|
            logger.debug(f"After PyGC_Collect - Memory usage: {current_memory:,.2f} MB")
 | 
						|
    except (AttributeError, TypeError):
 | 
						|
        logger.debug("PyGC_Collect not supported")
 | 
						|
    
 | 
						|
    # 10. Clear Flask-specific caches if applicable
 | 
						|
    if app:
 | 
						|
        try:
 | 
						|
            # Clear Flask caches if they exist
 | 
						|
            for key in list(app.config.get('_cache', {}).keys()):
 | 
						|
                app.config['_cache'].pop(key, None)
 | 
						|
            
 | 
						|
            # Clear Jinja2 template cache if available
 | 
						|
            if hasattr(app, 'jinja_env') and hasattr(app.jinja_env, 'cache'):
 | 
						|
                app.jinja_env.cache.clear()
 | 
						|
            
 | 
						|
            current_memory = process.memory_info().rss / 1024 / 1024
 | 
						|
            logger.debug(f"After Flask cache clear - Memory usage: {current_memory:,.2f} MB")
 | 
						|
        except (AttributeError, KeyError):
 | 
						|
            logger.debug("No Flask cache to clear")
 | 
						|
    
 | 
						|
    # Final garbage collection pass
 | 
						|
    gc.collect()
 | 
						|
    libc.malloc_trim(0)
 | 
						|
    
 | 
						|
    # Log final memory usage
 | 
						|
    final_memory = process.memory_info().rss / 1024 / 1024
 | 
						|
    logger.info(f"Memory cleanup completed - Final memory usage: {final_memory:,.2f} MB")
 | 
						|
    return "cleaned" |