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