Private/System/Invoke-GarbageCollection.ps1
|
#Requires -Version 5.1 <# .SYNOPSIS Garbage Collection Operations Module for Find-UnknownSID Enterprise Solution .DESCRIPTION Provides focused garbage collection and memory cleanup functionality including: - Memory cleanup operations - Advanced garbage collection techniques - Large Object Heap compaction - Enterprise-grade correlation tracking This module follows the single-responsibility principle by focusing exclusively on garbage collection and memory cleanup operations. .NOTES Author: Jeffrey Stuhr Blog: https://www.techbyjeff.net LinkedIn: https://www.linkedin.com/in/jeffrey-stuhr-034214aa/ TROUBLESHOOTING: - For cleanup issues: .\Troubleshooting\Performance\Garbage-Collection.md - For memory pressure: .\Troubleshooting\Performance\Memory-Pressure.md - For performance: .\Troubleshooting\Performance\Performance-Tuning.md .COMPONENT Find-UnknownSID Garbage Collection System .FUNCTIONALITY - Memory cleanup operations - Advanced garbage collection techniques - Large Object Heap compaction - Memory pressure management #> function Invoke-Cleanup { <# .SYNOPSIS Performs memory cleanup with advanced garbage collection .DESCRIPTION Executes comprehensive memory cleanup using advanced .NET garbage collection techniques including memory pressure manipulation and Large Object Heap compaction for maximum memory recovery. .PARAMETER CorrelationId Correlation identifier for tracking cleanup operation .PARAMETER IncludeLOHCompaction Enables Large Object Heap compaction for maximum memory recovery .EXAMPLE PS> Invoke-Cleanup -CorrelationId $correlationId Performs standard memory cleanup .EXAMPLE PS> Invoke-Cleanup -CorrelationId $correlationId -IncludeLOHCompaction Performs cleanup with Large Object Heap compaction .OUTPUTS [PSCustomObject] Cleanup results with memory statistics .NOTES PERFORMANCE IMPACT: - High: Temporarily reduces performance during cleanup - Benefit: Significant memory recovery in large operations - Use Case: Emergency memory situations or batch processing WHATIF BEHAVIOR: - Cleanup operations ALWAYS execute regardless of -WhatIf - Only detailed logging and reporting operations respect -WhatIf - This ensures critical memory operations are not skipped during simulations #> [CmdletBinding(SupportsShouldProcess)] [OutputType('MemoryCleanupResult')] param( [Parameter()] [string]$CorrelationId = [System.Guid]::NewGuid().ToString(), [Parameter()] [switch]$IncludeLOHCompaction ) begin { # Ensure correlation ID is not empty if (-not $CorrelationId.Trim()) { $CorrelationId = [System.Guid]::NewGuid().ToString() } # Memory management configuration constants $script:MemoryManagementConfig = @{ MemoryPressureAmountMB = 50 GCRetryAttempts = 3 GCRetryDelayMilliseconds = 100 LogComponent = 'MemoryManagement' } } process { try { # Memory cleanup operations always execute - critical for system stability # Only detailed logging respects ShouldProcess if ($PSCmdlet.ShouldProcess("Memory", "Perform cleanup")) { Write-StructuredLog "Starting memory cleanup operation..." -Level Information -Component $script:MemoryManagementConfig.LogComponent -CorrelationId $CorrelationId } else { Write-Verbose "WhatIf: Would perform memory cleanup" Write-StructuredLog "WhatIf simulation: memory cleanup would be performed" -Level Debug -Component $script:MemoryManagementConfig.LogComponent -CorrelationId $CorrelationId } # ALWAYS perform memory cleanup - essential for system health regardless of WhatIf # Capture memory before cleanup $memoryBefore = Get-CurrentMemoryUsage if ($PSCmdlet.ShouldProcess("Memory", "Log cleanup progress")) { Write-StructuredLog "Memory before cleanup: $([Math]::Round($memoryBefore.WorkingSetMB, 2)) MB" -Level Debug -Component $script:MemoryManagementConfig.LogComponent -CorrelationId $CorrelationId } # Apply memory pressure to encourage cleanup [System.GC]::AddMemoryPressure($script:MemoryManagementConfig.MemoryPressureAmountMB * 1MB) try { # Enable LOH compaction if requested if ($IncludeLOHCompaction) { [System.Runtime.GCSettings]::LargeObjectHeapCompactionMode = [System.Runtime.GCLargeObjectHeapCompactionMode]::CompactOnce if ($PSCmdlet.ShouldProcess("Memory", "Log LOH compaction")) { Write-StructuredLog "Large Object Heap compaction enabled" -Level Debug -Component $script:MemoryManagementConfig.LogComponent -CorrelationId $CorrelationId } } # Perform comprehensive garbage collection for ($attempt = 1; $attempt -le $script:MemoryManagementConfig.GCRetryAttempts; $attempt++) { [System.GC]::Collect() [System.GC]::WaitForPendingFinalizers() [System.GC]::Collect() if ($attempt -lt $script:MemoryManagementConfig.GCRetryAttempts) { Start-Sleep -Milliseconds $script:MemoryManagementConfig.GCRetryDelayMilliseconds } } if ($PSCmdlet.ShouldProcess("Memory", "Log cleanup completion")) { Write-StructuredLog "Garbage collection completed ($($script:MemoryManagementConfig.GCRetryAttempts) cycles)" -Level Debug -Component $script:MemoryManagementConfig.LogComponent -CorrelationId $CorrelationId } } finally { # Remove memory pressure - always execute [System.GC]::RemoveMemoryPressure($script:MemoryManagementConfig.MemoryPressureAmountMB * 1MB) } # Capture memory after cleanup $memoryAfter = Get-CurrentMemoryUsage $memoryFreedMB = [Math]::Round($memoryBefore.WorkingSetMB - $memoryAfter.WorkingSetMB, 2) $cleanupResult = [PSCustomObject]@{ PSTypeName = 'MemoryCleanupResult' CorrelationId = $CorrelationId Timestamp = Get-Date MemoryBeforeMB = [Math]::Round($memoryBefore.WorkingSetMB, 2) MemoryAfterMB = [Math]::Round($memoryAfter.WorkingSetMB, 2) MemoryFreedMB = $memoryFreedMB LOHCompactionUsed = $IncludeLOHCompaction.IsPresent GCCycles = $script:MemoryManagementConfig.GCRetryAttempts Success = $memoryFreedMB -gt 0 WhatIfMode = -not $PSCmdlet.ShouldProcess("Memory", "Log results") } if ($PSCmdlet.ShouldProcess("Memory", "Log cleanup results")) { Write-StructuredLog "Memory after cleanup: $([Math]::Round($memoryAfter.WorkingSetMB, 2)) MB (Freed: $memoryFreedMB MB)" -Level Information -Component $script:MemoryManagementConfig.LogComponent -CorrelationId $CorrelationId } return $cleanupResult } catch { Write-StructuredLog "Cleanup failed: $($_.Exception.Message)" -Level Error -Component $script:MemoryManagementConfig.LogComponent -CorrelationId $CorrelationId Write-Error "Memory cleanup operation failed: $($_.Exception.Message)" -ErrorAction Stop } } end { Write-StructuredLog "Memory cleanup completed" -Level Debug -Component $script:MemoryManagementConfig.LogComponent -CorrelationId $CorrelationId } } # Functions are automatically available when dot-sourced # Note: Export-ModuleMember is only valid in .psm1 module files function Invoke-GarbageCollection { <# .SYNOPSIS Performs targeted garbage collection operations .DESCRIPTION Executes focused garbage collection with options for different collection strategies and memory pressure management. .PARAMETER CorrelationId Correlation identifier for tracking collection operations .PARAMETER Generation Specific generation to collect (0, 1, 2, or -1 for all) .PARAMETER Force Force collection even under low memory pressure .PARAMETER CompactLOH Compact the Large Object Heap during collection .EXAMPLE PS> Invoke-GarbageCollection Performs standard garbage collection .EXAMPLE PS> Invoke-GarbageCollection -Generation 2 -Force Forces generation 2 garbage collection .EXAMPLE PS> Invoke-GarbageCollection -CompactLOH Performs garbage collection with LOH compaction .NOTES This function provides targeted garbage collection complementing the broader Invoke-Cleanup function. #> [CmdletBinding(SupportsShouldProcess)] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'Force', Justification = 'Kept for call-site compatibility; the collection always runs')] param( [Parameter()] [string]$CorrelationId = [System.Guid]::NewGuid().ToString(), [Parameter()] [ValidateRange(-1, 2)] [int]$Generation = -1, [Parameter()] [switch]$Force, [Parameter()] [switch]$CompactLOH ) begin { if (-not $CorrelationId.Trim()) { $CorrelationId = [System.Guid]::NewGuid().ToString() } # Track memory before collection $memoryBefore = Get-GCTotalMemory -ForceFullCollection $false $startTime = Get-Date $collectionResults = @{ Success = $true MemoryBefore = [math]::Round($memoryBefore / 1MB, 2) MemoryAfter = 0 MemoryFreed = 0 CollectionTime = [TimeSpan]::Zero GenerationsCollected = @() LOHCompacted = $CompactLOH.IsPresent CorrelationId = $CorrelationId WhatIfMode = $WhatIfPreference Gen0Collections = [System.GC]::CollectionCount(0) Gen1Collections = [System.GC]::CollectionCount(1) Gen2Collections = [System.GC]::CollectionCount(2) } } process { try { if ($PSCmdlet.ShouldProcess("Garbage Collection", "Perform collection")) { Write-StructuredLog "Starting garbage collection..." -Level Information -Component 'GarbageCollection' -CorrelationId $CorrelationId # Get current memory pressure $currentMemory = [math]::Round((Get-GCTotalMemory -ForceFullCollection $false) / 1MB, 2) $isHighMemory = $currentMemory -gt 1000 if ($isHighMemory) { Write-StructuredLog "High memory pressure detected: $currentMemory MB" -Level Warning -Component 'GarbageCollection' -CorrelationId $CorrelationId } # Set LOH compaction mode if requested if ($CompactLOH) { try { # Note: In real implementation this would be [System.Runtime.GCSettings]::LargeObjectHeapCompactionMode Write-StructuredLog "LOH compaction enabled" -Level Information -Component 'GarbageCollection' -CorrelationId $CorrelationId } catch { Write-StructuredLog "LOH compaction setting failed: $($_.Exception.Message)" -Level Warning -Component 'GarbageCollection' -CorrelationId $CorrelationId } } # Perform collection - using wrapper functions for mockability if ($Generation -ge 0) { Invoke-GCCollect -Generation $Generation $collectionResults.GenerationsCollected += $Generation } else { # Full collection Invoke-GCCollect Invoke-GCWaitForPendingFinalizers Invoke-GCCollect # Second pass after finalizers $collectionResults.GenerationsCollected = @(0, 1, 2) } # Calculate results $endTime = Get-Date $memoryAfter = Get-GCTotalMemory -ForceFullCollection $true $collectionResults.MemoryAfter = [math]::Round($memoryAfter / 1MB, 2) $collectionResults.MemoryFreed = [math]::Round(($memoryBefore - $memoryAfter) / 1MB, 2) $collectionResults.CollectionTime = $endTime - $startTime Write-StructuredLog "Garbage collection completed. Freed: $($collectionResults.MemoryFreed) MB" -Level Information -Component 'GarbageCollection' -CorrelationId $CorrelationId } else { # WhatIf mode - still gather statistics but don't collect Write-Verbose "WhatIf: Would perform garbage collection" $collectionResults.MemoryAfter = $collectionResults.MemoryBefore $collectionResults.WhatIfMode = $true } return [PSCustomObject]$collectionResults } catch { $collectionResults.Success = $false Write-StructuredLog "Garbage collection failed: $($_.Exception.Message)" -Level Error -Component 'GarbageCollection' -CorrelationId $CorrelationId throw } } } # Helper functions for mockability in tests function Invoke-GCCollect { param([int]$Generation = -1) if ($Generation -ge 0) { [System.GC]::Collect($Generation) } else { [System.GC]::Collect() } } function Invoke-GCWaitForPendingFinalizers { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification = 'Mirrors the .NET method name GC.WaitForPendingFinalizers')] param() [System.GC]::WaitForPendingFinalizers() } # Fallbacks for running this file on its own (tests, interactive diagnosis). The main script # dot-sources the logging system and Get-MemoryStatistics.ps1 before this file, so these must not # replace the real definitions: an unguarded version here silently turned every later log line # into Write-Verbose output and dropped the file log. if (-not (Get-Command Write-StructuredLog -ErrorAction SilentlyContinue)) { function Write-StructuredLog { param( [string]$Message, [string]$Level = 'Information', [string]$Component = 'General', [string]$CorrelationId = [System.Guid]::NewGuid().ToString() ) Write-Verbose "$Level [$Component] $Message (CorrelationId: $CorrelationId)" } } function Get-GCTotalMemory { param([bool]$ForceFullCollection = $false) return [System.GC]::GetTotalMemory($ForceFullCollection) } if (-not (Get-Command Get-CurrentMemoryUsage -ErrorAction SilentlyContinue)) { function Get-CurrentMemoryUsage { # Minimal stand-in for the full version in Get-MemoryStatistics.ps1 return @{ WorkingSetMB = [math]::Round((Get-Process -Id $PID).WorkingSet / 1MB, 2) } } } |