Skip to content

Instantly share code, notes, and snippets.

@BastianBlokland
Last active June 5, 2019 08:10
Show Gist options
  • Save BastianBlokland/9503bea5ced35bf0e6c299c745558848 to your computer and use it in GitHub Desktop.
Save BastianBlokland/9503bea5ced35bf0e6c299c745558848 to your computer and use it in GitHub Desktop.
Profile dotnet application using 'dotnet-trace'

Starting from dotnet core 2.1 the dotnet runtime emits profiling events through the EventPipe api. And there is now a convient and cross platform way to consume those: dotnet-trace.

This little script automates the process of installing dotnet-sdk dependencies, installing a sdk and installing the dotnet-trace tool. At the moment this works for debian based distributions but can be expanded to different distributions.

Show help:

curl -sSL -o ./pd.sh https://bastian.tech/scripts/profile-dotnet.sh && chmod +x ./pd.sh && ./pd.sh -h

List processes that can be traced:

curl -sSL -o ./pd.sh https://bastian.tech/scripts/profile-dotnet.sh && chmod +x ./pd.sh && ./pd.sh -l

Start tracing a process:

curl -sSL -o ./pd.sh https://bastian.tech/scripts/profile-dotnet.sh && chmod +x ./pd.sh && ./pd.sh -t [PROCESS_ID]

Monitor a process:

curl -sSL -o ./pd.sh https://bastian.tech/scripts/profile-dotnet.sh && chmod +x ./pd.sh && ./pd.sh -m [PROCESS_ID]

Traces can be explored with: speedscope

Script:

#!/bin/bash
set -e
trap 'cleanup' EXIT SIGINT

info ()
{
    echo -e "\033[0;96mINFO: $1\033[0m"
}

trace ()
{
    echo -e "\033[0;32mTRACE: $1\033[0m"
}

error ()
{
    echo -e "\033[0;31mERROR: $1\033[0m" >&2
}

hasCommand ()
{
    if [ -x "$(command -v $1)" ]
    then
        return 0
    fi
    return 1
}

missingCommand ()
{
    if ! [ -x "$(command -v $1)" ]
    then
        return 0
    fi
    return 1
}

failIfMissingCommand ()
{
    local command="$1"
    if missingCommand "$command"
    then
        error "Command '$command' is missing, unable to continue."
        exit 1
    fi
}

cleanup ()
{
    kill 0
}

installPackages ()
{
    local packages="$1"
    info "Installing packages: '$packages' through apt-get"
    failIfMissingCommand apt-get
    apt-get update
    apt-get install -y $packages
}

installDotnetSdk ()
{
    # Make sure we use the 'dotnet' cli from the path we want.
    export PATH="$dotnetInstallDir:$dotnetInstallDir/tools:$PATH"
    export DOTNET_ROOT="$dotnetInstallDir"

    # Check if desired 'dotnet' sdk is already installed.
    if hasCommand "$dotnetInstallDir/dotnet"
    then
        trace "Found 'dotnet' cli tooling, checking for sdks"
        # Check if desired sdk is already installed.
        while read -r sdk; do
            if [[ "$sdk" == "$dotnetSdkChannel"* ]]
            then
                trace "Found sdk matching channel: '$sdk'"
                return
            else
                trace "Non matching sdk: '$sdk'"
            fi
        done <<< "$($dotnetInstallDir/dotnet --list-sdks)"
    fi

    # Install dependencies.
    installPackages "curl openssl libunwind8"

    # Download install script.
    info "Downloading dotnet install script"
    local instalFilePath="temp_dotnet-install.sh"
    failIfMissingCommand curl
    curl -sSL -o "$instalFilePath" https://dot.net/v1/dotnet-install.sh
    chmod +x "$instalFilePath"

    # Installing sdk.
    info "Installing dotnet sdk: $dotnetSdkChannel"
    "./$instalFilePath" -Channel "$dotnetSdkChannel" -InstallDir "$dotnetInstallDir"

    # Remove install script.
    rm -f "$instalFilePath"
}

installDotnetTool ()
{
    # Install sdk.
    installDotnetSdk
    failIfMissingCommand "$dotnetInstallDir/dotnet"

    # Install tool.
    local tool="$1"
    local version="$2"
    if missingCommand "$dotnetInstallDir/tools/$tool"
    then
        info "Install dotnet tool: '$tool' version: '$version'"
        "$dotnetInstallDir/dotnet" tool install --global "$tool" --version "$version"
    else
        trace "Tool '$tool' is already installed"
    fi
}

runTrace ()
{
    local processId="$1"
    local outputPath="$2"

    installDotnetTool "dotnet-trace" "$dotnetTraceVersion"
    failIfMissingCommand "$dotnetInstallDir/tools/dotnet-trace"

    info "Starting trace, processId: '$processId', outputPath: '$outputPath'"
    "$dotnetInstallDir/tools/dotnet-trace" collect -p "$processId" -o "$outputPath" --format Speedscope
}

runMonitor ()
{
    local processId="$1"

    installDotnetTool "dotnet-counters" "$dotnetCountersVersion"
    failIfMissingCommand "$dotnetInstallDir/tools/dotnet-counters"

    info "Start monitoring, processId: '$processId'"
    "$dotnetInstallDir/tools/dotnet-counters" monitor --process-id "$processId" --refresh-interval "$monitorInterval"
}

runList ()
{
    installDotnetTool "dotnet-trace" "$dotnetTraceVersion"
    failIfMissingCommand "$dotnetInstallDir/tools/dotnet-trace"

    info "Traceable processes:"
    "$dotnetInstallDir/tools/dotnet-trace" list-processes
}

help ()
{
    echo
    echo "Utility script to aid in profiling on docker containers"
    echo
    echo "Usage: $0"
    echo
    echo " -t, --trace      Install dependencies and start collecting a trace using 'dotnet-trace'"
    echo "                      [ProcessId] [OutputPath]"
    echo " -m, --monitor    Install dependencies and start monitoring using 'dotnet-counters'"
    echo "                      [ProcessId]"
    echo " -l, --list       List trace-able processes using 'dotnet-trace'"
    echo
    echo " -h, --help       Display help"
    echo
    echo "Environment vars:"
    echo
    echo " dotnetSdkChannel         (Current: '$dotnetSdkChannel') 'dotnet' sdk version to install"
    echo " dotnetInstallDir         (Current: '$dotnetInstallDir') Directory where to install 'dotnet' sdk to"
    echo " dotnetTraceVersion       (Current: '$dotnetTraceVersion') 'dotnet-trace' version to install"
    echo " dotnetCountersVersion    (Current: '$dotnetCountersVersion') 'dotnet-counters' version to install"
    echo " monitorInterval          (Current: '$monitorInterval') Interval in seconds for refreshing the monitoring info"
    echo
}

# Environment arg config.
dotnetSdkChannel="${dotnetSdkChannel:-"3.0"}"
dotnetInstallDir="${dotnetInstallDir:-"$HOME/.dotnet"}"
dotnetTraceVersion="${dotnetTraceVersion:-"1.0.3-preview5.19251.2"}"
dotnetCountersVersion="${dotnetCountersVersion:-"1.0.3-preview5.19251.2"}"
monitorInterval="${monitorInterval:-"2"}"

# Input parsing.
arg="$1"
if [ -z $arg ]
then
    error "Expected argument to be provided"
    help
    exit 1
else
    case $arg in
        -t|--trace)
            runTrace "${2:-"1"}" "${3:-"trace.speedscope"}"
            ;;
        -m|--monitor)
            runMonitor "${2:-"1"}"
            ;;
        -l|--list)
            runList
            ;;
        -h|--help)
            help
            ;;
        *)
            error "Unknown argument: '$arg'"
            exit 1
            ;;
    esac
fi

exit 0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment