Teamscale .NET Profiler
The Teamscale .NET Profiler records method coverage of .NET applications for Test Gap Analysis. It consists of two components:
- Profiler: Registers with the .NET runtime via environment variables and writes trace files to disk as the application runs. Works with any .NET application on .NET Framework and .NET Core.
- Upload Daemon: Runs in the background, picks up finished trace files, converts them to method coverage using your application's PDB files, and uploads the result to Teamscale or to intermediate storage (Artifactory, Azure File Storage, network share).
Method coverage
The profiler records method coverage, not line coverage. Coverage maps to methods rather than specific line numbers. This is sufficient for Test Gap Analysis, which operates at method granularity. If you need line coverage, AltCover is an alternative with higher performance impact.
Trace files are created when the first .NET method executes, but remain mostly empty until the process ends gracefully — all recorded data is flushed on shutdown. For long-running processes (such as IIS applications), you must either schedule regular recycling or set eagerness in the configuration file.
If you want to set up Test Gap Analysis (TGA), have a look at our TGA tutorial and how-to page.
Prerequisites
You must modify your application's build to provide information necessary for the profiler to function. The Upload Daemon converts trace files to method coverage using PDB files. These must be generated during your build and made accessible:
- Set
/p:DebugType=pdbin your MSBuild arguments to generate PDB files. Set this at the solution level rather than per.csproj. - Ensure PDB files are not deleted during packaging. Copy them to a separate location if needed.
- Make PDB files accessible to the Upload Daemon — either by placing each PDB file in the same directory as its assembly (use
@AssemblyDirin the configuration) or via a network share.
Installation
- Download the latest release of the profiler.
- Extract the profiler into a directory readable and writable by the process being profiled, e.g.
C:\Users\Public\Coverage Profiler.C:\Users\Publicis writable by all Windows user accounts by default. - Create an output directory for trace files, e.g.
C:\Users\Public\Traces. Must also be readable and writable by the process being profiled.
Configuration
Both the profiler and the Upload Daemon are configured via the Profiler.yml, located in the same directory as the profiler DLLs. If you want to read a YAML file from a different path, set COR_PROFILER_CONFIG to that path.
YAML string quoting
Use single quotes for all string values in Profiler.yml. In single-quoted strings, backslashes are literal and require no extra escaping, e.g. Windows paths. The only character that needs escaping inside single-quoted strings is the single quote itself, which must be doubled: ''.
Environment variables as overrides
Most options from the configuration file can also be overridden with an environment variable: COR_PROFILER_ + the option name uppercased (e.g. targetdir → COR_PROFILER_TARGETDIR). Environment variables take precedence over config file values. See the Configuration Reference for the complete list.
The config file uses match sections to apply settings to specific processes. Sections are applied in order — later sections override earlier ones when both match. If no selectors are given, a section applies to all processes.
Selectors (all must match if multiple are specified in one section):
| Selector | Description |
|---|---|
executableName | Case-insensitive suffix match against the executable filename. Use when the application has a unique name. Example: myapp.exe matches any process whose executable path ends with myapp.exe. |
executablePathRegex | C++ ECMAScript regex matched against the full executable path. Must match the entire path. Use when two applications share the same filename in different directories, or to match all executables under a path. Example: 'C:\\inetpub\\myapp\\[^\\]+\.exe' matches any exe directly under C:\inetpub\myapp\. |
loadedAssemblyPathRegex | Matched against the paths of assemblies loaded into the process. Use for host processes that run multiple applications simultaneously (IIS, COM+ services, plugin hosts) — routes coverage from different assemblies to different Teamscale projects. Requires the Upload Daemon. See multiple applications in a single process. |
Profiler configuration
Options go under the profiler: key in each match section. The most important ones:
| YAML key | Default | Description |
|---|---|---|
enabled | true | Enable or disable profiling for processes matching this section |
targetdir | C:\Users\Public | Directory where trace files are written |
light_mode | true | Ultra-light mode — disables re-jitting of assemblies. Disable only if you use the Native Image Cache. |
eagerness | 0 | When > 0, writes trace data to disk after every N method calls instead of waiting for shutdown. Use in environments where the runtime is killed rather than shut down gracefully. |
upload_daemon | false | Enables the Upload Daemon for this process |
See the Configuration Reference for all profiler options.
Upload Daemon configuration
The Upload Daemon picks up trace files, converts them to method coverage using PDB files, and uploads the result. Enable it by setting upload_daemon: true under profiler:. Configure it under the uploader: key in the same match section.
PDB directory — pdbDirectory specifies where PDB files are located. @AssemblyDir resolves to the directory of each loaded assembly (requires assembly_paths: true, the default):
uploader:
pdbDirectory: '@AssemblyDir'Revision file — revisionFile points to a file containing the code revision of the deployed build. The @AssemblyDir token scans assembly directories in load order for the first match:
uploader:
revisionFile: '@AssemblyDir\revision.txt'The revision file contains a single line in one of these formats:
| Format | Example | Description |
|---|---|---|
revision: HASH | revision: abc123def456 | VCS revision (Git SHA1 or TFS changeset ID) |
timestamp: BRANCH:MS | timestamp: master:1672531200000 | Unix timestamp in milliseconds with branch name |
timestamp: MS | timestamp: 1672531200000 | Unix timestamp only — uploads to the default branch. Not recommended. |
Assembly patterns — restrict coverage to your application's assemblies to avoid errors about missing PDB files for third-party libraries:
uploader:
assemblyPatterns:
include: ['MyCompany.*']
exclude: ['*DoNotProfileThis*']Upload destination — configure exactly one destination under uploader:. Options: teamscale, artifactory, directory (network share), or azureFileStorage. See the Configuration Reference for all fields.
Daemon log — the daemon writes UploadDaemon.log to the directory containing UploadDaemon.exe. Check this after your first configuration to verify uploads are working. The daemon can also be installed as a Windows service or invoked manually:
UploadDaemon.exe --config path\to\profiler.ymlSee the Configuration Reference for all Upload Daemon options.
Example
This example configuration uploads coverage data directy to Teamscale.
match:
- # Disable profiling globally
profiler:
enabled: false
- executableName: 'myapp.exe'
profiler:
enabled: true
targetdir: 'C:\Users\Public\Traces'
upload_daemon: true
uploader:
pdbDirectory: '@AssemblyDir'
revisionFile: '@AssemblyDir\revision.txt'
assemblyPatterns:
include: ['MyCompany.*']
teamscale:
url: 'http://teamscale.example.com'
username: 'build'
accessKey: 'your-access-key'
project: 'my-project'
partition: 'Manual Tests'Multiple applications in a single process
Some architectures (IIS, COM+ services, plugin hosts) run multiple applications inside a single host process. If these are set up as separate Teamscale projects, you cannot identify where to send which trace file by the process name alone. For these cases, we need to identify which trace files should be uploaded to which Teamscale project by the assemblies that were loaded. Use loadedAssemblyPathRegex to route coverage to different Teamscale projects:
match:
- profiler:
targetdir: 'C:\profiler\traces'
enabled: true
- loadedAssemblyPathRegex: 'C:\\webapp-1\\.*'
profiler:
upload_daemon: true
uploader:
pdbDirectory: '@AssemblyDir'
revisionFile: '@AssemblyDir\revision.txt'
teamscale:
url: 'http://teamscale.example.com'
username: 'build'
accessKey: 'your-access-key'
project: 'webapp-1'
partition: 'Unit Tests'
- loadedAssemblyPathRegex: 'C:\\webapp-2\\.*'
profiler:
upload_daemon: true
uploader:
pdbDirectory: '@AssemblyDir'
revisionFile: '@AssemblyDir\revision.txt'
teamscale:
url: 'http://teamscale.example.com'
username: 'build'
accessKey: 'your-access-key'
project: 'webapp-2'
partition: 'Unit Tests'Assemblies that do not match any loadedAssemblyPathRegex pattern are still profiled. You only need to match a single key assembly per trace file so the correct Teamscale project can be determined.
Enabling the Profiler for Your Application
To enable the profiler, you must set different environment variables for a .NET framework application than for a .NET core application.
.NET Framework
Register the profiler by setting these environment variables system-wide or in the application's start script:
| Environment variable | Value | Description |
|---|---|---|
COR_ENABLE_PROFILING | 1 | Enables the profiler |
COR_PROFILER | {DD0A1BB6-11CE-11DD-8EE8-3F9E55D89593} | GUID identifying the profiler DLL |
COR_PROFILER_PATH_32 | Path to Profiler 32bit DLL | e.g. C:\Users\Public\Coverage Profiler\Profiler32.dll |
COR_PROFILER_PATH_64 | Path to Profiler 64bit DLL | e.g. C:\Users\Public\Coverage Profiler\Profiler32.dll |
TIP
We recommend enabling the profiler globally via system-wide environment variables, then using the configuration file to filter which processes are profiled with executableName or executablePathRegex.
.NET Core
Register the profiler by setting these environment variables system-wide or in the application's start script:
| Environment variable | Value | Description |
|---|---|---|
CORECLR_ENABLE_PROFILING | 1 | Enables the profiler |
CORECLR_PROFILER | {DD0A1BB6-11CE-11DD-8EE8-3F9E55D89593} | GUID identifying the profiler DLL |
CORECLR_PROFILER_PATH_32 | Path to Profiler 32bit DLL | e.g. C:\Users\Public\Coverage Profiler\Profiler32.dll |
CORECLR_PROFILER_PATH_64 | Path to Profiler 64bit DLL | e.g. C:\Users\Public\Coverage Profiler\Profiler64.dll |
WARNING
All profiler configuration options (e.g. targetdir) are the same for .NET Framework and .NET Core, regardless of which registration variables you used.
IIS / Web Applications
For applications running in IIS (w3wp.exe):
Set environment variables for the account running the relevant application pool (preferred), or system-wide (which profiles all pools):
- Per application pool account: Enable "Load User Profile" in the app pool's Advanced Settings (IIS Manager → Application Pools → [Pool] → Advanced Settings → Process Model), then set variables in the Registry as described in this guide.
- System-wide: Set variables in the Registry at
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\W3SVC\Environmentor...\IISADMIN\Environment.
Restart IIS (or recycle the application pool) for the variables to take effect.
Since IIS processes are long-running, configure the application pool to recycle nightly so that trace files are written regularly. Alternatively, set eagerness in the configuration file. Trace files include IIS AppPool: [Your App Pool ID] to distinguish traces from different pools sharing the same output directory.
Azure App Service
- Download the latest profiler release. Enable the Azure FTP account and upload the profiler (including
Profiler.yml) to a location that won't be overwritten by deployments, e.g.D:\home\site\repository\profiler\. - In Azure, go to Application Settings and add the registration environment variables and
COR_PROFILER_CONFIGpointing to yourProfiler.yml. - Configure
targetdirinProfiler.ymltoD:\home\LogFilesor a dedicated location (create it first).
By default, Azure Apps are shut down at least every 29 hours. To set a custom recycling schedule, configure D:\home\site\applicationHost.xdt. E.g. to recycle every night at 3am:
<?xml version="1.0"?>
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
<system.applicationHost>
<applicationPools>
<add name="yourappname" xdt:Locator="Match(name)">
<recycling xdt:Transform="Insert">
<periodicRestart>
<schedule>
<clear />
<add value="03:00:00" />
</schedule>
</periodicRestart>
</recycling>
</add>
</applicationPools>
</system.applicationHost>
</configuration>Troubleshooting
See the Troubleshooting page if trace files are not created or coverage is not uploaded.
