Powershell : Tracking VXHD Growth over time (with charts)

If you would like to track on growth of dynamic expanding virtual disk files, remembering that these are not limited to virtual hard drive files as they can be used for user profiles as well.

Then this particular script will scan the folder specified for VHDX files, it will then make two files one is the current state of the drive or directory and the other file is the comparison (the first time you run the script, it won’t be very helpful)

The comparison will compare the size of the file on the last scan with the size on the file of the latest scan and give you percentage differences both in size and growth obviously, if you want to make a turkey chart, it exports it in a CSV file So you can go crazy in Excel or your other spreadsheet, editor of choice.

Note : Update the <smb_share> with the name of the actual valid share

Script : InfrequentScan.ps1

Note : This scan is more for infrequent scans of your server VXHD files as the data will be split across two files as the first file is used as a baseline.

# Define share path and other configuration variables
$SharePath = "<smb_share>"
$LogFile = "VHDXGrowthLog.csv"
$HistoryFile = "VHDXHistory.csv"
$LogPath = "VHDXTracker.log"

# Function to get file size in GB
function Get-FileSizeInGB {
    param($FileSize)
    return [math]::Round($FileSize/1GB, 2)
}

# Function to write to log file
function Write-Log {
    param($Message)
    $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    "$timestamp - $Message" | Add-Content -Path $LogPath
    Write-Host "$timestamp - $Message"
}
try {
    Write-Log "Starting VHDX growth tracking script"
    Write-Log "Scanning path: $SharePath"

    # Verify share path exists
    if (-not (Test-Path $SharePath)) {
        throw "Share path does not exist: $SharePath"
    }

    # Get current VHDX files
    $currentFiles = Get-ChildItem -Path $SharePath -Recurse -Include *.vhdx, *.vhd -ErrorAction Stop | Select-Object `
        FullName,
        @{Name='SizeGB';Expression={Get-FileSizeInGB $_.Length}},
        LastWriteTime

    # Create current snapshot
    $currentSnapshot = @()
    foreach ($file in $currentFiles) {
        $currentSnapshot += [PSCustomObject]@{
            Path = $file.FullName
            SizeGB = $file.SizeGB
            LastModified = $file.LastWriteTime
            TimeStamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
        }
    }

    # Load previous snapshot if exists
    $previousSnapshot = @()
    if (Test-Path $HistoryFile) {
        $previousSnapshot = Import-Csv -Path $HistoryFile
    }

    # Compare and generate growth report
    $growthReport = @()
    foreach ($current in $currentSnapshot) {
        $previous = $previousSnapshot | Where-Object { $_.Path -eq $current.Path } | Select-Object -First 1
        if ($previous) {
            $growth = $current.SizeGB - [double]$previous.SizeGB
            $growthReport += [PSCustomObject]@{
                Path = $current.Path
                CurrentSizeGB = $current.SizeGB
                PreviousSizeGB = [double]$previous.SizeGB
                GrowthGB = [math]::Round($growth, 2)
                GrowthPercent = if ([double]$previous.SizeGB -gt 0) { 
                    [math]::Round(($growth / [double]$previous.SizeGB) * 100, 2) 
                } else { 0 }
                LastModified = $current.LastModified
                TimeStamp = $current.TimeStamp
            }
        } else {
            # New file
            $growthReport += [PSCustomObject]@{
                Path = $current.Path
                CurrentSizeGB = $current.SizeGB
                PreviousSizeGB = 0
                GrowthGB = $current.SizeGB
                GrowthPercent = 100
                LastModified = $current.LastModified
                TimeStamp = $current.TimeStamp
            }
        }
    }

    # Save current snapshot as history
    $currentSnapshot | Export-Csv -Path $HistoryFile -NoTypeInformation -Force

    # Export growth report
    $growthReport | Export-Csv -Path $LogFile -NoTypeInformation -Force

    # Display summary
    $totalGrowth = ($growthReport | Measure-Object -Property GrowthGB -Sum).Sum
    $significantGrowth = $growthReport | Where-Object { $_.GrowthGB -gt 0 }
    Write-Log "Scan completed successfully"
    Write-Log "Total files scanned: $($currentFiles.Count)"
    Write-Log "Files with growth: $($significantGrowth.Count)"
    Write-Log "Total growth: $([math]::Round($totalGrowth, 2)) GB"
    Write-Log "Detailed report saved to: $LogFile"   

    # Output files with significant growth
    if ($significantGrowth) {
        Write-Log "Files with significant growth:"
        $significantGrowth | Sort-Object GrowthGB -Descending | ForEach-Object {
            Write-Log "Path: $($_.Path)"
            Write-Log "Growth: $($_.GrowthGB) GB ($($_.GrowthPercent)%)"
            Write-Log "---"
        }
    }
}
catch {
    Write-Log "Error: $_"
    throw $_
}

This script will produce the following files as below:


VHDXTracker.log - This is the log file of what the script is doing
VXHDGrowthLog.csv - This will analyse the previous scan and the latest scan to give you infrequent data extraction of % growth and % changes (this uses VHDXHistory.csv as a baseline)
VHDXHistory.csv - This will give you the data from the current scan you are running (live data)

Optimise the Scripting (for more frequent scans)

That script is good for infrequent checks, but can become a little messy if you need to run the script for a more detailed analysis over a longer period of time, however if you want to run this scan more frequent then I have added to the option to add the new scan data to the same files with the date of that scan as the header value.

Script : AnalyseData.ps1

# Define share path and other configuration variables
$SharePath = "\\smbshare.bear.local\FileTest"
$HistoryFile = "VHDXHistory.csv"
$LogPath = "VHDXTracker.log"

# Function to get file size in GB with more precision
function Get-FileSizeInGB {
    param($FileSize)
    # Convert to GB with 3 decimal places for sub-GB precision
    return [math]::Round($FileSize/1GB, 3)
}

# Function to write to log file
function Write-Log {
    param($Message)
    $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    "$timestamp - $Message" | Add-Content -Path $LogPath
    Write-Host "$timestamp - $Message"
}
try {
    Write-Log "Starting VHDX tracking script"
    Write-Log "Scanning path: $SharePath"

    # Verify share path exists
    if (-not (Test-Path $SharePath)) {
        throw "Share path does not exist: $SharePath"
    }

    # Get current timestamp for column header (including seconds)
    $currentTimestamp = Get-Date -Format "yyyy-MM-dd_HH-mm-ss"

    # Get current VHDX files
    $currentFiles = Get-ChildItem -Path $SharePath -Recurse -Include *.vhdx, *.vhd -ErrorAction Stop | ForEach-Object {
        $sizeGB = Get-FileSizeInGB $_.Length
        [PSCustomObject]@{
            FullName = $_.FullName
            SizeGB = $sizeGB
            LastWriteTime = $_.LastWriteTime
            SizeDisplay = if ($sizeGB -ge 1) {
                "{0:N2} GB" -f $sizeGB
            } else {
                "{0:N0} MB" -f ($_.Length/1MB)
            }
        }
    }

    # Display summary of files found
    Write-Log "Total files found: $($currentFiles.Count)"   

    # Debug output for file sizes
    foreach ($file in $currentFiles) {
        Write-Log "Found file: $($file.FullName) - Size: $($file.SizeDisplay)"
    }
    if ($currentFiles.Count -eq 0) {
        Write-Log "No VHDX files found in the specified path"
        exit 0
    }

    # Load existing history file or create new if doesn't exist
    $history = @{}
    if (Test-Path $HistoryFile) {
        $existingData = Import-Csv -Path $HistoryFile
        foreach ($row in $existingData) {
            $history[$row.Path] = $row
        }
    }

    # Create or update entries
    foreach ($file in $currentFiles) {
        if ($history.ContainsKey($file.FullName)) {
            # Add new size column to existing entry
            $history[$file.FullName] | Add-Member -NotePropertyName $currentTimestamp -NotePropertyValue $file.SizeGB -Force
        } else {
            # Create new entry
            $newEntry = [PSCustomObject]@{
                Path = $file.FullName
                LastModified = $file.LastWriteTime
            }
            $newEntry | Add-Member -NotePropertyName $currentTimestamp -NotePropertyValue $file.SizeGB -Force
            $history[$file.FullName] = $newEntry
        }
    }

    # Convert history hashtable to array and export
    $historyArray = $history.Values | Sort-Object Path
    if ($historyArray) {
        $historyArray | Export-Csv -Path $HistoryFile -NoTypeInformation -Force
        Write-Log "History saved to: $HistoryFile"
    }

       # Calculate and display changes since last scan
   if ($historyArray -and $historyArray.Count -gt 0) {
        $scanColumns = $historyArray[0].PSObject.Properties.Name | 
            Where-Object { $_ -match '^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}$' } | 
            Sort-Object -Descending
        if ($scanColumns.Count -ge 2) {
            $currentScan = $scanColumns[0]
            $previousScan = $scanColumns[1]           
            $totalGrowth = 0
            $changedFiles = @()
            foreach ($file in $historyArray) {
                $currentSize = [double]($file.$currentScan)
                $previousSize = [double]($file.$previousScan)
                $growth = [math]::Round($currentSize - $previousSize, 3)
                $growthPercent = if ($previousSize -gt 0) {
                    [math]::Round(($growth / $previousSize) * 100, 2)
                } else {
                    100
                }
                if ($growth -ne 0) {
 # Format the size display for current and previous sizes
                    $currentSizeDisplay = if ($currentSize -ge 1) {
                        "{0:N2} GB" -f $currentSize
                    } else {
                        "{0:N0} MB" -f ($currentSize * 1024)
                    }                 
                    $previousSizeDisplay = if ($previousSize -ge 1) {
                        "{0:N2} GB" -f $previousSize
                    } else {
                        "{0:N0} MB" -f ($previousSize * 1024)
                    }
                $growthDisplay = if ([Math]::Abs($growth) -ge 1) {
                        "{0:N2} GB" -f $growth
                    } else {
                        "{0:N0} MB" -f ($growth * 1024)
                    }
                   $changedFiles += [PSCustomObject]@{
                        Path = $file.Path
                        CurrentSize = $currentSize
                        PreviousSize = $previousSize
                        Growth = $growth
                        GrowthPercent = $growthPercent
                        CurrentSizeDisplay = $currentSizeDisplay
                        PreviousSizeDisplay = $previousSizeDisplay
                        GrowthDisplay = $growthDisplay
                    }
                    $totalGrowth += $growth
                }
            }
            Write-Log "Analysis between scans: $previousScan -> $currentScan"

            Write-Log "Total growth: $(if ([Math]::Abs($totalGrowth) -ge 1) { "{0:N2} GB" -f $totalGrowth } else { "{0:N0} MB" -f ($totalGrowth * 1024) })"
            Write-Log "Files with changes: $($changedFiles.Count)"
            if ($changedFiles) {
                Write-Log "`nDetailed growth report:"
                $changedFiles | Sort-Object Growth -Descending | ForEach-Object {
                    Write-Log "Path: $($_.Path)"
                    Write-Log "Size: $($_.PreviousSizeDisplay) -> $($_.CurrentSizeDisplay)"
                    Write-Log "Growth: $($_.GrowthDisplay) ($($_.GrowthPercent)%)"
                    Write-Log "---"
                }
            }
        } else {
            Write-Log "First scan completed - growth calculations will be available after next scan"
        }
    }
}
catch {
    Write-Log "Error: $_"
    throw $_
}

This will then produce a file that contains the "size" over all the scans you complete in one files as below (ignore the headers like "Day 3" this will actually be the actual date and time of the scan)


Visual Output from Powershell

This will give you a visual output from Powershell so you can see what is going on in a grpah format

Script : DazzleMe.ps1

# Import required assemblies
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Windows.Forms.DataVisualization

# Create Chart
$chart = New-Object System.Windows.Forms.DataVisualization.Charting.Chart
$chart.Width = 1000
$chart.Height = 600
$chart.Titles.Add("File Size Growth Over Time")

# Create ChartArea
$chartArea = New-Object System.Windows.Forms.DataVisualization.Charting.ChartArea
$chart.ChartAreas.Add($chartArea)

# Import CSV data
$data = Import-Csv "VHDXHistory.csv"
$uniquePaths = $data | Select-Object -ExpandProperty Path -Unique

# Create a series for each path
foreach ($path in $uniquePaths) {
    $series = New-Object System.Windows.Forms.DataVisualization.Charting.Series
    $series.Name = [System.IO.Path]::GetFileName($path)  # Only show filename in legend
    $series.ChartType = [System.Windows.Forms.DataVisualization.Charting.SeriesChartType]::Line
    $chart.Series.Add($series)
    
    $rowData = $data | Where-Object { $_.Path -eq $path }
    
    # Get all columns except Path
    $timeColumns = $rowData.PSObject.Properties | 
                  Where-Object { $_.Name -ne "Path" } |
                  Select-Object -ExpandProperty Name
    
    # Add points for each timestamp
    $pointIndex = 0
    foreach ($column in $timeColumns) {
        $value = if ($rowData.$column) { 
            try { [double]($rowData.$column) } catch { 0 }
        } else { 0 }
        
        $series.Points.AddXY($pointIndex, $value)
        $pointIndex++
    }
}

# Configure axes
$chartArea.AxisX.Interval = 1
$timeColumns = $data | Get-Member -MemberType NoteProperty | 
               Where-Object { $_.Name -ne "Path" } | 
               Select-Object -ExpandProperty Name

$index = 0
foreach ($column in $timeColumns) {
    $chartArea.AxisX.CustomLabels.Add($index - 0.5, $index + 0.5, [System.IO.Path]::GetFileName($column))
    $index++
}

$chartArea.AxisX.Title = "Timestamp"
$chartArea.AxisX.LabelStyle.Angle = -45
$chartArea.AxisY.Title = "Size (bytes)"
$chartArea.AxisY.LabelStyle.Format = "N0"

# Add legend
$legend = New-Object System.Windows.Forms.DataVisualization.Charting.Legend
$chart.Legends.Add($legend)

# Create form
$form = New-Object Windows.Forms.Form
$form.Text = "File Size Growth"
$form.Width = 1200
$form.Height = 800
$form.controls.add($chart)

# Save and show
$chart.SaveImage("chart.png", "PNG")
$form.Add_Shown({$form.Activate()})
$form.ShowDialog()

This will give you a visual output of the data from all the previous scans like this, however this cannot be customised as its a static window:


Excel Export

If you have Excel installed on your device you can if you choose to output that chart to Excel and create that magical spreadsheet file, however I do not use Excel as my weapon of choice.

Script : DazzleMe-Excel.ps1

# Import CSV data
$data = Import-Csv "VHDXHistory.csv"

# Create Excel COM object
$excel = New-Object -ComObject Excel.Application
$excel.Visible = $true
$workbook = $excel.Workbooks.Add()
$worksheet = $workbook.Worksheets.Item(1)

# Write headers
$headers = $data | Get-Member -MemberType NoteProperty | Select-Object -ExpandProperty Name
for ($col = 0; $col -lt $headers.Count; $col++) {
    $worksheet.Cells.Item(1, $col + 1) = $headers[$col]
}

# Write data
$row = 2
foreach ($record in $data) {
    $col = 1
    foreach ($header in $headers) {
        $worksheet.Cells.Item($row, $col) = $record.$header
        $col++
    }
    $row++
}

# Create chart
$chartObject = $worksheet.Shapes.AddChart2(-1, [Microsoft.Office.Interop.Excel.XlChartType]::xlLine)
$chart = $chartObject.Chart

# Set chart properties
$chart.HasTitle = $true
$chart.ChartTitle.Text = "File Size Growth Over Time"
$chart.HasLegend = $true

# Set source data
$lastRow = $data.Count + 1
$lastCol = $headers.Count
$range = $worksheet.Range("A1:${lastCol}${lastRow}")
$chart.SetSourceData($range)

# Save workbook
$workbook.SaveAs("FileGrowthChart.xlsx")
Previous Post Next Post

نموذج الاتصال