Skip to content

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=pdb in 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 @AssemblyDir in the configuration) or via a network share.

Installation

  1. Download the latest release of the profiler.
  2. Extract the profiler into a directory readable and writable by the process being profiled, e.g. C:\Users\Public\Coverage Profiler. C:\Users\Public is writable by all Windows user accounts by default.
  3. 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. targetdirCOR_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):

SelectorDescription
executableNameCase-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.
executablePathRegexC++ 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\.
loadedAssemblyPathRegexMatched 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 keyDefaultDescription
enabledtrueEnable or disable profiling for processes matching this section
targetdirC:\Users\PublicDirectory where trace files are written
light_modetrueUltra-light mode — disables re-jitting of assemblies. Disable only if you use the Native Image Cache.
eagerness0When > 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_daemonfalseEnables 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 directorypdbDirectory specifies where PDB files are located. @AssemblyDir resolves to the directory of each loaded assembly (requires assembly_paths: true, the default):

yaml
uploader:
  pdbDirectory: '@AssemblyDir'

Revision filerevisionFile 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:

yaml
uploader:
  revisionFile: '@AssemblyDir\revision.txt'

The revision file contains a single line in one of these formats:

FormatExampleDescription
revision: HASHrevision: abc123def456VCS revision (Git SHA1 or TFS changeset ID)
timestamp: BRANCH:MStimestamp: master:1672531200000Unix timestamp in milliseconds with branch name
timestamp: MStimestamp: 1672531200000Unix 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:

yaml
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:

cmd
UploadDaemon.exe --config path\to\profiler.yml

See the Configuration Reference for all Upload Daemon options.

Example

This example configuration uploads coverage data directy to Teamscale.

yaml
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:

yaml
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 variableValueDescription
COR_ENABLE_PROFILING1Enables the profiler
COR_PROFILER{DD0A1BB6-11CE-11DD-8EE8-3F9E55D89593}GUID identifying the profiler DLL
COR_PROFILER_PATH_32Path to Profiler 32bit DLLe.g. C:\Users\Public\Coverage Profiler\Profiler32.dll
COR_PROFILER_PATH_64Path to Profiler 64bit DLLe.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 variableValueDescription
CORECLR_ENABLE_PROFILING1Enables the profiler
CORECLR_PROFILER{DD0A1BB6-11CE-11DD-8EE8-3F9E55D89593}GUID identifying the profiler DLL
CORECLR_PROFILER_PATH_32Path to Profiler 32bit DLLe.g. C:\Users\Public\Coverage Profiler\Profiler32.dll
CORECLR_PROFILER_PATH_64Path to Profiler 64bit DLLe.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\Environment or ...\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

  1. 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\.
  2. In Azure, go to Application Settings and add the registration environment variables and COR_PROFILER_CONFIG pointing to your Profiler.yml.
  3. Configure targetdir in Profiler.yml to D:\home\LogFiles or 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
<?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.