Tests/GraphShell.Performance.Tests.ps1

#Requires -Modules Pester

<#
    Performance/benchmark checks for the GraphShell module.
    These are informative Pester tests: they assert loose upper bounds and print
    real measurements (memory delta, first/second call latency) with Write-Host,
    visible with -Output Detailed.
 
    Run in a fresh PowerShell process so the "before load" measurement is meaningful:
        pwsh -NoProfile -Command "Invoke-Pester -Path .\powershell\GraphShell\Tests\GraphShell.Performance.Tests.ps1 -Output Detailed"
#>


BeforeAll {
    $script:ModuleRoot = Split-Path -Parent $PSScriptRoot
    $script:ManifestPath = Join-Path $ModuleRoot 'GraphShell.psd1'
    Remove-Module GraphShell -ErrorAction SilentlyContinue
    Import-Module $ManifestPath -Force -ErrorAction Stop
}

AfterAll {
    Remove-Module GraphShell -ErrorAction SilentlyContinue
}

Describe 'GraphShell catalog load performance' {
    It 'answers -Cmdlet lookups without loading the full command index' {
        [System.GC]::Collect()
        [System.GC]::WaitForPendingFinalizers()
        $before = [System.GC]::GetTotalMemory($true)

        $sw = [Diagnostics.Stopwatch]::StartNew()
        Get-GraphMapping -Cmdlet 'Get-MgUser' | Out-Null
        $sw.Stop()
        $firstCallMs = $sw.ElapsedMilliseconds

        $after = [System.GC]::GetTotalMemory($false)
        $deltaMb = [math]::Round(($after - $before) / 1MB, 1)

        Write-Host "Managed memory before -Cmdlet lookup: $([math]::Round($before/1MB,1)) MB"
        Write-Host "Managed memory after -Cmdlet lookup: $([math]::Round($after/1MB,1)) MB"
        Write-Host "Approximate -Cmdlet memory delta: $deltaMb MB"
        Write-Host "First Get-GraphMapping -Cmdlet call: $firstCallMs ms"

        $sw.Restart()
        Get-GraphMapping -Cmdlet 'Get-MgUser' | Out-Null
        $sw.Stop()
        Write-Host "Second (cached) call: $($sw.ElapsedMilliseconds) ms"

        # -Cmdlet only loads its one resource shard (data/graph-command-by-cmdlet/*.json), not
        # the full ~11 MB index, so the memory delta should be a small fraction of the ~200-250
        # MB the full catalog used to cost every single lookup.
        $deltaMb | Should -BeLessThan 60
        $firstCallMs | Should -BeLessThan 5000
    }

    It 'reports the memory cost of loading the full command index for -Query' {
        [System.GC]::Collect()
        [System.GC]::WaitForPendingFinalizers()
        $before = [System.GC]::GetTotalMemory($true)

        $sw = [Diagnostics.Stopwatch]::StartNew()
        Get-GraphMapping -Query 'PIM eligible' | Out-Null
        $sw.Stop()
        $firstCallMs = $sw.ElapsedMilliseconds

        $after = [System.GC]::GetTotalMemory($false)
        $deltaMb = [math]::Round(($after - $before) / 1MB, 1)

        Write-Host "Managed memory before -Query catalog load: $([math]::Round($before/1MB,1)) MB"
        Write-Host "Managed memory after -Query catalog load: $([math]::Round($after/1MB,1)) MB"
        Write-Host "Approximate -Query catalog memory delta: $deltaMb MB"
        Write-Host "First (cold) Get-GraphMapping -Query call: $firstCallMs ms"

        $sw.Restart()
        Get-GraphMapping -Query 'PIM eligible' | Out-Null
        $sw.Stop()
        Write-Host "Second (warm, cached inverted index) call: $($sw.ElapsedMilliseconds) ms"

        # -Query is the one parameter set intentionally allowed to pay the full-catalog cost
        # (~200-250 MB) because it also builds and caches per-session inverted word indexes.
        $deltaMb | Should -BeLessThan 350
        # First call includes a one-time inverted-index build across the whole catalog.
        $firstCallMs | Should -BeLessThan 20000
    }

    It 'answers subsequent cmdlet lookups fast once cached' {
        Get-GraphMapping -Cmdlet 'Get-MgUser' | Out-Null # warm cache
        $sw = [Diagnostics.Stopwatch]::StartNew()
        1..20 | ForEach-Object { Get-GraphMapping -Cmdlet 'Get-MgUser' | Out-Null }
        $sw.Stop()
        $avgMs = [math]::Round($sw.ElapsedMilliseconds / 20, 2)
        Write-Host "Average cached cmdlet lookup: $avgMs ms"
        $avgMs | Should -BeLessThan 100
    }

    It 'answers permission lookups fast using the reverse index' {
        $sw = [Diagnostics.Stopwatch]::StartNew()
        $result = Get-GraphMapping -Permission 'User.Read.All'
        $sw.Stop()
        Write-Host "Get-GraphMapping -Permission 'User.Read.All': $($sw.ElapsedMilliseconds) ms, $($result.Count) results"
        $sw.ElapsedMilliseconds | Should -BeLessThan 2000
    }

    It 'answers endpoint lookups fast' {
        $sw = [Diagnostics.Stopwatch]::StartNew()
        Get-GraphMapping -Endpoint '/users' | Out-Null
        $sw.Stop()
        Write-Host "Get-GraphMapping -Endpoint '/users': $($sw.ElapsedMilliseconds) ms"
        $sw.ElapsedMilliseconds | Should -BeLessThan 2000
    }

    It 'answers a warm free-text query search fast once the inverted index is cached' {
        Get-GraphMapping -Query 'PIM eligible' | Out-Null # warm the inverted index
        $sw = [Diagnostics.Stopwatch]::StartNew()
        Get-GraphMapping -Query 'PIM eligible' | Out-Null
        $sw.Stop()
        Write-Host "Get-GraphMapping -Query 'PIM eligible' (warm): $($sw.ElapsedMilliseconds) ms"
        $sw.ElapsedMilliseconds | Should -BeLessThan 1500
    }

    It 'loads only one detail shard for Get-GraphDetail, not the whole detail set' {
        InModuleScope GraphShell {
            $script:GraphDetailShardCache = $null
            Get-GraphDetail -Name 'Get-MgUser' | Out-Null
            $script:GraphDetailShardCache.Keys.Count | Should -Be 1
            $script:GraphDetailShardCache.Keys | Should -Contain 'g'
        }
    }
}