# encoding: ascii
# api: powershell
# title: Get-ProcessProfile
# description: Uses cdb.exe from the Debugging Tools for Windows to create a sample-based
# version: 8.0
# author: Lee Holmes
# license: CC0
# x-poshcode-id: 3591
# x-archived: 2016-11-30T18:52:09
# x-published: 2012-08-23T00:23:00
#
# profile of .NET or native applications.
#
##############################################################################
##
## Get-ProcessProfile
##
##############################################################################

<#

.SYNOPSIS

Uses cdb.exe from the Debugging Tools for Windows to create a sample-based
profile of .NET or native applications.

.EXAMPLE

$frames = C:\temp\Get-ProcessProfile.ps1 -ProcessId 11844
$frames | % { $_[0] } | group | sort Count | Select Count,Name | ft -auto

Runs a sampling profile on process ID 1184. Then, it extracts out the top
(current) stack entry from each call frame and groups it by the resulting
text.

This gives a report like the following, which was taken while PowerShell
version 2 was slowly enumerating a network share. The output below
demonstrates that PowerShell was spending the majority of its time invoking a
pipeline, and calling the .NET System.IO.FillAttributeInfo API:

Count Name
----- ----
    1 System.Collections.Specialized.HybridDictionary.set_Item(System.Object...
    1 System.Text.StringBuilder..ctor(System.String, Int32, Int32, Int32)
    1 System.Management.Automation.Provider.CmdletProvider.WrapOutputInPSObj...
    1 System.Management.Automation.Provider.NavigationCmdletProvider.GetPare...
    1 System.Management.Automation.Provider.CmdletProvider.get_Force()
    1 System.Management.Automation.Cmdlet.WriteObject(System.Object)
    1 System.String.AppendInPlace(Char[], Int32, Int32, Int32)
    1 Microsoft.PowerShell.ConsoleHostRawUserInterface.LengthInBufferCells(C...
    1 System.Security.Util.StringExpressionSet.CanonicalizePath(System.Strin...
    1 Microsoft.PowerShell.ConsoleControl.GetConsoleScreenBufferInfo(Microso...
    1 System.IO.DirectoryInfo..ctor(System.String, Boolean)
    1 System.Security.Permissions.FileIOPermission.AddPathList(System.Securi...
    2 System.IO.Path.InternalCombine(System.String, System.String)
    2 System.Runtime.InteropServices.SafeHandle.Dispose(Boolean)
   18 System.IO.Directory.InternalGetFileDirectoryNames(System.String, Syste...
   66 System.IO.File.FillAttributeInfo(System.String, WIN32_FILE_ATTRIBUTE_D...
  100 System.Management.Automation.Runspaces.PipelineBase.Invoke(System.Coll...

#>


param(
    ## The process ID to attach to
    [Parameter(Mandatory = $true)]
    $ProcessId,

    ## How many samples to retrieve
    $SampleCount = 100,

    ## Switch parameter to debug a native process
    [Switch] $UseNativeDebugging,

    ## Path to CDB. Will be detected if not supplied.
    $CdbPath
)

## If the user didn't specify a path to CDB, see if we can find it in the
## standard locations.
if(-not $CdbPath)
{
    $cdbPaths = "C:\Program Files (x86)\Windows Kits\8.0\Debuggers\x64\cdb.exe",
        "C:\Program Files (x86)\Windows Kits\8.0\Debuggers\x86\cdb.exe",
        "C:\Program Files\Debugging Tools for Windows (x64)\cdb.exe",
        "C:\Program Files\Debugging Tools for Windows (x86)\cdb.exe"
        
    foreach($path in $CdbPaths)
    {
        if(Test-Path $path)
        {
            ## If we found it, remember it and break.
            $CdbPath = $path
            break
        }
    }

    if(-not $CdbPath)
    {
        throw "Could not find cdb.exe from the Debugging Tools for Windows package"
    }
}

if(-not (Get-Process -Id $ProcessId))
{
    throw "Could not find process ID $ProcessId"
}

## Prepare the command we will send to cdb.exe. We use one command for
## managed applications, and another for native.
$debuggingCommand = ""
$managedDebuggingCommand = ".loadby sos mscorwks; .loadby sos clr; ~*e !CLRStack"
$nativeDebuggingCommand = "~*k"

if($UseNativeDebugging)
{
    $debuggingCommand = $nativeDebuggingCommand
}
else
{
    $debuggingCommand = $managedDebuggingCommand
}

## Create the info to start cdb.exe, redirecting its standard input and output
## so that we can automate it.
$startInfo = [System.Diagnostics.ProcessStartInfo] @{
    FileName = $CdbPath;
    Arguments = "-p $processId -noinh -c `"$debuggingCommand`"";
    UseShellExecute = $false;
    RedirectStandardInput = $true
    RedirectStandardOutput = $true
}

$frames = @()

## Start sampling the process by launching cdb.exe, taking the stack trace,
## and detaching.
1..$SampleCount | % {
    $process = [System.Diagnostics.Process]::Start($startInfo)
    $process.StandardInput.WriteLine("qd")
    $process.StandardInput.Close()
    $r = $process.StandardOutput.ReadToEnd() -split "`n"

    ## Go through the output data, extracting the actual stack trace text
    ## data.
    $frame = @()
    $capture = $false
    switch -regex ($r)
    {
        'Child SP|Child-SP' { $capture = $true; continue; }
        'OS Thread Id|^\s*$' { $capture = $false; if($frame) { $frames += ,$frame; $frame = @() } }
        { $capture -and ($_ -match '\)$|!') } { $frame += $_ -replace ".*? .*? ([^+]*).*",'$1' }
    }

    if($frame) { $frames += ,$frame }

    ## Sleep a little bit (with randomness) so that we get accurate
    ## samples
    Start-Sleep -m (100 + (Get-Random 100))
}

## Output the frames we retrieved.
,$frames
