Trace-ScriptInternal.ps1

function Trace-ScriptInternal { 
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [ScriptBlock] $ScriptBlock,
        [uint32] $Preheat = 0,
        [Switch] $DisableWarning,
        [Hashtable] $Flag,
        [Switch] $Before,
        [Switch] $UseNativePowerShell7Profiler,
        # Putting the collection into object will make it modified when returned for some reason
        # outputting time through this
        $Out
    )

    $ErrorView = "Normal"
    $ErrorActionPreference = "Stop"

    Assert-PowerShellVersion

    Write-Host -ForegroundColor Magenta "Running in PowerShell $($PSVersionTable.PSVersion)."

    if (0 -ge $Preheat) {
        $Preheat = 0
    }

    if ($Flag) { 
        Write-Host -ForegroundColor Magenta "Flags for $(if (-not $Before) { "After" } else { "Before"}) run:"
        foreach ($p in $Flag.GetEnumerator()) {
            $v = if ($Before) { $false } else { $p.Value }
            Write-Host -ForegroundColor Magenta " $($p.Key) = $($v)"
            Set-Variable -Scope Global -Name $p.Key -Value $v
        }
    }

    if (0 -lt $Preheat) { 
        foreach ($i in 1..$Preheat) {
            Write-Host -Foreground Magenta  "Warm up $i"

            if ($UseNativePowerShell7Profiler) {
                $null = Measure-Script $ScriptBlock
            }
            else {
                $result = Measure-ScriptHarmony $ScriptBlock
                if ($null -ne $result.Error) { 
                    Write-Host -ForegroundColor Red "Warm up failed with $($result.Error)."
                }
            }
            
        }
    }

    if (0 -lt $Flag.Count) { 
        foreach ($p in $Flag.GetEnumerator()) {
            $v = if ($Before) { $false } else { $p.Value }
            Set-Variable -Scope Global -Name $p.Key -Value $v
        }
    }

    Write-Host -Foreground Magenta  "Starting trace."
    Write-Host -Foreground Magenta  "Stopwatch $(if([Diagnostics.Stopwatch]::IsHighResolution) { "is" } else { "is not" }) high resolution, max resolution of timestamps is $([int] (1e9/[Diagnostics.Stopwatch]::Frequency))ns."

    if ($UseNativePowerShell7Profiler) {
        $result = Measure-Script $ScriptBlock
    }
    else {
        $result = Measure-ScriptHarmony $ScriptBlock
    }
    if ($null -eq $result.Error) {
        Write-Host -Foreground Magenta  "Run$(if (1 -lt $sides.Count) { " - $side" }) finished after $($result.Stopwatch)."
    }
    else {
        Write-Host -ForegroundColor Red "Run$(if (1 -lt $sides.Count) { " - $side" }) failed after $($result.Stopwatch) with the following error:`n$($result.Error)."
    }

    $out.Stopwatch = $result.Stopwatch
    $out.ScriptBlocks = $result.ScriptBlocks
    $trace = $result.Trace
    Write-Host -Foreground Magenta "Tracing done. Got $($trace.Count) trace events."
    if ($UseNativePowerShell7Profiler) {
        $normalizedTrace = [Collections.Generic.List[Profiler.Hit]]::new($trace.Count)
        Write-Host "Used native tracer from PowerShell 7. Normalizing trace."
        foreach ($t in $trace) { 
            $r = [Profiler.Hit]::new()
            $e = [Profiler.ScriptExtent]::new()
            $e.File = $t.Extent.File
            $e.StartLineNumber = $t.Extent.StartLineNumber
            $e.StartColumnNumber = $t.Extent.StartColumnNumber
            $e.EndLineNumber = $t.Extent.EndLineNumber
            $e.EndColumnNumber = $t.Extent.EndColumnNumber
            $e.StartOffset = $t.Extent.StartOffset
            $e.EndOffset = $t.Extent.EndOffset
            $r.Extent = $e
            $r.StartTime = $t.StartTime
            $r.SelfDuration = $t.SelfDuration
            $r.Index = $index

            $index++

            $normalizedTrace.Add($r)
        }
        
        $trace = $null
        $normalizedTrace
    }
    else { 
        $trace
    }
}

function Measure-ScriptHarmony ($ScriptBlock) {
    
    $tracer = [Profiler.ProfilerTracer]::new()

    if (-not [Profiler.Tracer]::IsEnabled) {
        # use this as a marker of position, scriptblocks are aware of the current line, so we can use it to
        # make this code not rely on exact line numbers
        $here = {}
        # add a dummy breakpoint and disable it, otherwise when someone calls Remove-PSBreakpoint and there are
        # no breakpoints left the debugger will disable itself. This could also be solved by adding global function
        # that is generated as a proxy for Remove-PSBreakpoint and re-enables Set-PSDebug -Trace 1. That would
        # be more resilient to users who can remove all breakpoints including ours. But we would have to generate it
        # and modify the code, and it would look weird in their code output. On the other hand we would not show up extra
        # breakpoint in VSCode (or other editor)
        $bp = Set-PSBreakpoint -Script $PSCommandPath -Line $here.StartPosition.StartLine -Action {}
        $null = $bp | Disable-PSBreakpoint
    }

    $sw = [System.Diagnostics.Stopwatch]::new()
    try {
        try {
            if (-not [Profiler.Tracer]::IsEnabled) {
                $null = [Profiler.Tracer]::Patch($PSVersionTable.PSVersion.Major, $ExecutionContext, $host.UI, $tracer)
            }
            else { 
                # just add second tracer to the existing setup
                $null = [Profiler.Tracer]::Register($tracer)
            }

            $sw.Start();
            $null = Set-PSDebug -Trace 1
            $null = & $ScriptBlock
        }
        finally { 
            # disable tracing in any case because we don't want too many internal
            # details to leak into the log, otherwise any change in this code will
            # reflect into the log and we need to change counts in Profiler
            $null = Set-PSDebug -Trace 0
            $sw.Stop()
            if ([Profiler.Tracer]::HasTracer2) {
                $null = [Profiler.Tracer]::Unregister()
                # re-enable tracing because tracer 1 still needs to continue tracing
                $null = Set-PSDebug -Trace 1
            }
            else { 
                $null = [Profiler.Tracer]::Unpatch()

                if ($bp) { 
                    $null = $bp | Remove-PSBreakPoint
                }
            }
        }
    } 
    catch {
        $err = $_
    }



    $result = @{
        Trace        = $tracer.Hits
        ScriptBlocks = $tracer.ScriptBlocks
        Error        = $err
        Stopwatch    = $sw.Elapsed
    }

    if ($null -eq $result.Trace -or 0 -eq @($result.Trace).Count) { 
        throw "Trace is null or empty."
    }

    $firstLine = $result.Trace[0]
    $enableCommand = '$null = [Profiler.Tracer]::Patch($PSVersionTable.PSVersion.Major, $ExecutionContext, $host.UI, $tracer)'
    $registerCommand = '$null = [Profiler.Tracer]::Register($tracer)'
    if (-not ($PSCommandPath -eq $firstLine.Path -and ($enableCommand -eq $firstLine.Text -or $registerCommand -eq $firstLine.Text))) { 
        Write-Warning "Event list is incomplete, it should start with '$enableCommand' or '$registerCommand' from within Profiler module, but instead starts with entry '$($firstLine.Text)', from '$($firstLine.Path)'. Are you running profiler in the code you are profiling?"
    }

    $lastLine = $result.Trace[-1]
    $disableCommand = '$null = [Profiler.Tracer]::Unpatch()'
    $unregisterCommand = '$null = [Profiler.Tracer]::Unregister()'
    if (-not ($PSCommandPath -eq $lastLine.Path -and ($disableCommand -eq $lastLine.Text -or $unregisterCommand -eq $lastLine.Text))) { 
        Write-Warning "Event list is incomplete, it should end with '$disableCommand' or '$unregisterCommand' from within Profiler module, but instead ends with entry '$($lastLine.Text)', from '$($lastLine.Path)'. Are you disabling trace mode in your code using Set-PSDebug -Trace 0 or using Remove-PSBreakpoint to remove all breakpoints?"
    }

    $result
}