Private/ConvertTo-VMPerformanceRow.ps1

function ConvertTo-VMPerformanceRow {
    <#
    .SYNOPSIS
        Aggregates raw per-interval datapoints for one VM into monthly
        VMPerformance-compatible rows (one per VM x month x CounterName).

    .DESCRIPTION
        Buckets datapoints by calendar month (UTC) and computes
        Min / Median / Percentile95 / Percentile99 / Max / Average / SampleCount per
        bucket, emitting the three counters consumed by VMPerformance():

            % Processor Time <- Percentage CPU (avg per interval)
            Available MBytes <- Available Memory Bytes / 1MB (exact)
            % Committed Bytes In Use <- 100 - (Available / TotalRAM * 100) (PROXY)

        The committed-memory counter is a PROXY derived from host-available memory and
        the SKU's total RAM; it is not the guest's true committed bytes. It is only
        emitted when TotalRamMB is known.

    .PARAMETER VMName
        The VM name to stamp on every row.

    .PARAMETER CpuPoint
        Datapoints for 'Percentage CPU' (objects with TimeStamp + Average).

    .PARAMETER MemPoint
        Datapoints for 'Available Memory Bytes' (objects with TimeStamp + Average).

    .PARAMETER TotalRamMB
        VM total RAM in MB (from Resolve-VMSkuRam). When $null, the committed-memory
        proxy counter is skipped.

    .OUTPUTS
        [pscustomobject[]] rows: vmName, Date, CounterName, Min, Median, Percentile95,
        Percentile99, Max, Average, SampleCount.
    #>

    [CmdletBinding()]
    [OutputType([pscustomobject[]])]
    param(
        [Parameter(Mandatory)] [string] $VMName,
        [Parameter(Mandatory)] [AllowEmptyCollection()] [object[]] $CpuPoint,
        [Parameter(Mandatory)] [AllowEmptyCollection()] [object[]] $MemPoint,
        [double] $TotalRamMB
    )

    $rows = [System.Collections.Generic.List[pscustomobject]]::new()

    # Local helper: emit one row per month for a {monthKey -> double[]} map.
    $emit = {
        param($counterName, $byMonth)
        foreach ($monthKey in ($byMonth.Keys | Sort-Object)) {
            $vals = [double[]]$byMonth[$monthKey]
            if ($vals.Count -eq 0) { continue }
            $rows.Add([pscustomobject]@{
                vmName       = $VMName
                Date         = $monthKey
                CounterName  = $counterName
                Min          = [math]::Round(($vals | Measure-Object -Minimum).Minimum, 2)
                Median       = [math]::Round((Measure-Percentile -Value $vals -Percentile 50), 2)
                Percentile95 = [math]::Round((Measure-Percentile -Value $vals -Percentile 95), 2)
                Percentile99 = [math]::Round((Measure-Percentile -Value $vals -Percentile 99), 2)
                Max          = [math]::Round(($vals | Measure-Object -Maximum).Maximum, 2)
                Average      = [math]::Round(($vals | Measure-Object -Average).Average, 2)
                SampleCount  = $vals.Count
            })
        }
    }

    $monthOf = { param($ts) ([datetime]$ts).ToUniversalTime().ToString('yyyy-MM-01') }

    # --- CPU: % Processor Time ---
    $cpuByMonth = @{}
    foreach ($p in $CpuPoint) {
        if ($null -eq $p.Average) { continue }
        $mk = & $monthOf $p.TimeStamp
        if (-not $cpuByMonth.ContainsKey($mk)) { $cpuByMonth[$mk] = [System.Collections.Generic.List[double]]::new() }
        $cpuByMonth[$mk].Add([double]$p.Average)
    }
    & $emit '% Processor Time' $cpuByMonth

    # --- Memory: Available MBytes (exact) + % Committed Bytes In Use (proxy) ---
    $availByMonth = @{}
    $commByMonth  = @{}
    $totalBytes = if ($PSBoundParameters.ContainsKey('TotalRamMB') -and $TotalRamMB -gt 0) { $TotalRamMB * 1MB } else { $null }
    foreach ($p in $MemPoint) {
        if ($null -eq $p.Average) { continue }
        $mk = & $monthOf $p.TimeStamp
        $availMB = [double]$p.Average / 1MB
        if (-not $availByMonth.ContainsKey($mk)) { $availByMonth[$mk] = [System.Collections.Generic.List[double]]::new() }
        $availByMonth[$mk].Add($availMB)

        if ($null -ne $totalBytes) {
            $committedPct = 100.0 - ([double]$p.Average / $totalBytes * 100.0)
            if ($committedPct -lt 0) { $committedPct = 0 }
            if ($committedPct -gt 100) { $committedPct = 100 }
            if (-not $commByMonth.ContainsKey($mk)) { $commByMonth[$mk] = [System.Collections.Generic.List[double]]::new() }
            $commByMonth[$mk].Add($committedPct)
        }
    }
    & $emit 'Available MBytes' $availByMonth
    if ($null -ne $totalBytes) { & $emit '% Committed Bytes In Use' $commByMonth }

    return $rows.ToArray()
}