Exceptions ASP.Net et Performances / ASP.Net Exceptions and Performance

Go to the English version

Lors de mes visites chez mes clients il m'arrive souvent de vérifier la configuration ASP.Net et de jeter un coup d'oeil rapide aux performances de leurs applications. Je laisse l'expertise approfondie à mes collègues PFE Développement. Hormis le mode debug activé sur au moins une application de production chez 50% des clients (pensez à bien vérifiez chez vous et à revoir vos processus opérationnels en conséquence), je collecte généralement des données de performances (idéalement sur 24 heures) via Performance Monitor (aka PerfMon). Je vous livre même ci-dessous mon script pour lancer le tout depuis une ligne de commande :

 REM To create a Data Collector Set with the appropriate Performance counters for an IIS server (every 15 seconds during 24 hours)
logman create counter %COMPUTERNAME%-IIS -v mmddhhmm -o %SYSTEMDRIVE%\PerfLogs\Admin\%COMPUTERNAME%-IISPerformanceCounters -f bin -rf 24:00:00 -si 00:00:15 -c "\.NET CLR Data\*" "\.NET CLR Exceptions(*)\*" "\.NET CLR Interop(*)\*" "\.NET CLR Jit(*)\*" "\.NET CLR Loading(*)\*" "\.NET CLR LocksAndThreads(*)\*" "\.NET CLR Memory(*)\*" "\.NET CLR Networking(*)\*" "\.NET CLR Remoting(*)\*" "\.NET CLR Security(*)\*" "\.NET Data Provider for SqlServer(*)\*" "\Active Server Pages\*" "\APP_POOL_WAS(*)\*" "\ASP.NET Applications(*)\*" "\ASP.NET Apps v1.1.4322(*)\*" "\ASP.NET Apps v2.0.50727(*)\*" "\ASP.NET Apps v4.0.30319(*)\*" "\ASP.NET v1.1.4322\*" "\ASP.NET v2.0.50727\*" "\ASP.NET v4.0.30319\*" "\ASP.NET\*" "\Cache\*" "\Distributed Transaction Coordinator\*" "\HTTP Service Request Queues(*)\*" "\HTTP Service Url Groups(*)\*" "\HTTP Service\*" "\Internet Information Services Global\*" "\LogicalDisk(*)\*" "\Memory\*" "\Network Inspection System\*" "\Network Interface(*)\*" "\Paging File(*)\*" "\PhysicalDisk(*)\*" "\Process(*)\*" "\Processor Information(*)\*" "\Processor(*)\*" "\Server\*" "\System\*" "\TCP\*" "\TCPv4\*"  "\TCPv6\*" "\W3SVC_W3WP\*" "\WAS_W3WP(*)\*" "\Web Service Cache\*" "\Web Service(*)\*"
 
REM To start the Data Collector Set 
logman start %COMPUTERNAME%-IIS

je laisse ici l'emplacement par défaut (%SYSTEMDRIVE%\PerfLogs\Admin\%COMPUTERNAME%-IIS) pour le fichier généré (*.blg) que vous pouvez bien entendu changer.

Ensuite une macro analyse avec PAL (si vous ne connaissez pas PAL je vous conseille très vivement de lire cet article) vous donnera un aperçu rapide de la performance du ou des serveurs. Mais là n'est pas le but de cet article.

J'aimerai ici me focaliser sur les exceptions .Net et leur impact sur la performance de vos applications Web/ASP.Net. Comme expliqué sur https://msdn.microsoft.com/en-us/library/ff647791.aspx (cf. Exceptions). Les exceptions CLR .NET sont observées à travers le compteur de performances “ .NET CLR Exceptions, # of Excepts Thrown” (Ce compteur indique le nombre total d'exceptions générées par seconde dans le code managé). La recommandation est de rester proche de 0. Une application qui génère des exceptions peut avoir des bugs ou utiliser le mécanisme d'exceptions comme partie intégrante de la logique métier. Cette dernière utilisation est déconseillée car les exceptions sont très coûteuses et peuvent dégrader sérieusement les performances de votre application. Vous devez analyser votre code pour voir si celui-ci utilise des exceptions dans son algorithme. Response.Redirect, Server.Transfer et Response.End provoquent une exception de type ThreadAbortException dans les applications ASP.NET. Un serveur Web qui fonctionne bien ne doit pas générer d'erreurs. Examiner et corriger vos bugs avant de faire des tests de performance et surtout avant de passer en production. De plus, moins de 5 % de vos requêtes ASP.NET / sec doivent avoir des exceptions .NET (compteur de performances “ .NET CLR Exceptions, # of Excepts Thrown / sec”). Si vous voyez plus de 1 requête sur 20 lancer une exception, vous devez investiguer. Et c'est précisément sur ce dernier point que je vous propose un script PowerShell qui analysera vos fichiers de performances (*.BLG) et vous indiquera quand le seuil de 5% d'exceptions ASP.Net est dépassé (toutes applications confondues). Vous n'êtes pas obligés d'utiliser le jeu de compteurs que j'ai spécifié plus haut mais il faut au moins que les compteurs suivants aient été collectés :

  • .NET CLR Exceptions(w3wp*)\# of Exceps Thrown / sec ou .NET CLR Exceptions(*)\# of Exceps Thrown / sec
  • ASP.NET Applications(*_LM_W3SVC_*)\Requests/Sec ou ASP.NET Applications(*)\Requests/Sec

 

Le code source est disponible ici.

 #requires -version 2

#region function definitions
Function Get-NetExceptionsRate
{
    <#
            .SYNOPSIS
            Return performance counters data by extracting only the .NET CLR Exceptions/(*w3wp*)\# of Exceps Thrown / sec and ASP.NET Applications\(*_LM_W3SVC_*)\Requests/Sec when the 5 percent threshold is exceeded.

            .DESCRIPTION
            Return performance counters data by extracting only the .NET CLR Exceptions/(*w3wp*)\# of Exceps Thrown / sec and ASP.NET Applications\(*_LM_W3SVC_*)\Requests/Sec when the 5 percent threshold is exceeded.

            .PARAMETER FullName
            The BLG File to analyze specified by its full name

            .PARAMETER Full
            A switch to specify if we return all the data (threshold exceeded or not)

            .EXAMPLE
            Get-ChildItem "*.blg" | Get-NetExceptionsRate -Verbose
    #>
    [CmdletBinding(DefaultParameterSetName = 'Global', SupportsShouldProcess = $true)]
    Param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Position = 0)]
        #The BLG File to convert : Checking if the file exists and has the .BLG extension
        [ValidateScript({
                    (Test-Path -Path $_ -PathType Leaf) -and ($_ -match '\.blg$')
        })]
        [alias('Source', 'BLG')]
        [String[]]$FullName,

        #To list all timestamp even if the 5 percent is not exceeded"
        [parameter(Mandatory = $false)]
        [switch]$Full,
        
        # For ASP.Net 2.0
        [Parameter(ParameterSetName = 'ASPNetV2')]
        [switch]$v2,        

        # For ASP.Net 4.0
        [Parameter(ParameterSetName = 'ASPNetV4')]
        [switch]$v4        
    )
    begin
    {
        #Array for storing the results
        $Data = @()
    }    
    process
    {
        #For all files passed as argument outside a pipeline context
        ForEach ($CurrentFullName in $FullName)
        {
            $CurrentFullName = Get-Item -Path $CurrentFullName
            $SourceBLGFullName = $CurrentFullName.FullName
            Write-Host -Object "Processing $SourceBLGFullName ..."

            # Keeping only '.NET CLR Exceptions', 'ASP.NET Applications', 'ASP.NET Apps v2.0.50727' and 'ASP.NET Apps v4.0.30319' performance counter names
            Write-Verbose -Message 'Extracting .Net Performance Data ...'
            $CounterNames = Import-Counter -Path $SourceBLGFullName  -Listset '.NET CLR Exceptions', 'ASP.NET Applications', 'ASP.NET Apps v2.0.50727', 'ASP.NET Apps v4.0.30319' -ErrorAction SilentlyContinue |
            Select-Object -ExpandProperty PathsWithInstances |
            Where-Object -FilterScript {
                ($_ -like '*w3wp*)\# of Exceps Thrown / sec') -or ($_ -like '*_LM_W3SVC_*)\Requests/Sec')
            }

            #Importing only the required performance counters"
            $Counters = $(Import-Counter -Path $SourceBLGFullName -Counter $CounterNames -ErrorAction SilentlyContinue)
            
            #Getting the computer list (if data have been collected remotely for multiple servers)
            $ServerNames = $Counters.countersamples |
            ForEach-Object -Process {
                $_.path.split('\')[2] 
            } |
            Select-Object -Unique

            # Adding the computername as a member of the performance counters
            $Counters.countersamples | ForEach-Object -Process {
                $_ | Add-Member -MemberType NoteProperty -Name 'ServerName' -Value $($_.path.split('\')[2])
            }

            #Processing loop for each server
            ForEach ($CurrentServerName in $ServerNames) 
            {
                Write-Verbose -Message "Processing performance counter for $($CurrentServerName) ..."

                #Getting the performance couter list for the processed server
                $CurrentCounters  = $($CounterNames | Where-Object -FilterScript {
                        $_ -like "*$CurrentServerName*"
                }) -join ', '
                Write-Verbose -Message "Processing the following counters : $CurrentCounters"
                
                $Counters | ForEach-Object -Process {
                    $Timestamp = $_.Timestamp
                    #If we work only for ASP.Net v2
                    if ($v2)
                    {
                        #Getting the ASP.Net Request/Sec performance counters for every web applications
                        $ASPNetAppsV2RequestPerSecCounters = ($_.CounterSamples | Where-Object -FilterScript {
                                ($_.Path -like '*ASP.NET Apps v2.0.50727*') -and ($_.InstanceName -like '*_LM_W3SVC_*') -and ($_.ServerName -eq $CurrentServerName ) 
                        })
                        $ASPNetAppsV2RequestPerSec = ($ASPNetAppsV2RequestPerSecCounters | Measure-Object -Property CookedValue -Sum).Sum

                        # If at least one ASP.NET Apps v2.0.50727 request
                        if ($ASPNetAppsV2RequestPerSec -gt 0)
                        {
                            #Getting the .NET CLR Exceptions Exceptions Thrown/Sec performance counters for every worker process
                            $NetClrExceptionsThrownPerSecCounters = $_.CounterSamples | Where-Object -FilterScript {
                                ($_.InstanceName -like '*w3wp*') -and ($_.ServerName -eq $CurrentServerName ) 
                            }
                            $NetClrExceptionsThrownPerSec = ($NetClrExceptionsThrownPerSecCounters | Measure-Object -Property CookedValue -Sum).Sum
                            #We calculate the rate
                            $Percent = $NetClrExceptionsThrownPerSec/$ASPNetAppsV2RequestPerSec*100
                            #If this rate exceeds the 5% percent threshold
                            if ($Percent -ge 5)
                            {
                                Write-Verbose -Message $("[$($_.TimeStamp)] More than 5 percent of ASP.NET Apps v2.0.50727\Requests/sec are .NET exceptions. The Rate is {0:N2}% ({1:N2}/{2:N2})" -f $Percent, $NetClrExceptionsThrownPerSec, $ASPNetAppsV2RequestPerSec)
                                #Building an object to store the data
                                $CurrentData = New-Object -TypeName PSObject -Property @{
                                    SourceBLGFullName               = $SourceBLGFullName
                                    ServerName                      = $CurrentServerName
                                    Timestamp                       = $Timestamp
                                    NetClrExceptionsThrownPerSec    = $NetClrExceptionsThrownPerSec
                                    ASPNetApplicationsRequestPerSec = $ASPNetAppsV2RequestPerSec
                                    Percent                         = $Percent
                                    IsThreSholdExceeded             = $true
                                }
                                #Adding this data to the data array
                                $Data += $CurrentData
                            }
                            #If the -full switch has been specified we even store the data
                            elseif ($Full)
                            {
                                Write-Verbose -Message $("[$($_.TimeStamp)] Less than 5 percent of ASP.NET Apps v2.0.50727\Requests/sec are .NET exceptions. The Rate is {0:N2}% ({1:N2}/{2:N2})" -f $Percent, $NetClrExceptionsThrownPerSec, $ASPNetAppsV2RequestPerSec)
                                $CurrentData = New-Object -TypeName PSObject -Property @{
                                    SourceBLGFullName               = $SourceBLGFullName
                                    ServerName                      = $CurrentServerName
                                    Timestamp                       = $Timestamp
                                    NetClrExceptionsThrownPerSec    = $NetClrExceptionsThrownPerSec
                                    ASPNetApplicationsRequestPerSec = $ASPNetAppsV2RequestPerSec
                                    Percent                         = $Percent
                                    IsThreSholdExceeded             = $false
                                }
                                $Data += $CurrentData
                            }
                        }
                        #IF no ASP.Net v2 request but -full switch has been specified we write a verbose message
                        elseif ($Full)
                        {
                            Write-Verbose -Message "[$($_.TimeStamp)] No ASP.NET Apps v2.0.50727 Request "
                        }
                    }
                    #If we work only for ASP.Net v4
                    elseif ($v4)
                    {
                        #Getting the ASP.Net Request/Sec performance counters for every web applications
                        $ASPNetAppsV4RequestPerSecCounters = ($_.CounterSamples | Where-Object -FilterScript {
                                ($_.Path -like '*ASP.NET Apps v4.0.30319*') -and ($_.InstanceName -like '*_LM_W3SVC_*') -and ($_.ServerName -eq $CurrentServerName ) 
                        })
                        $ASPNetAppsV4RequestPerSec = ($ASPNetAppsV4RequestPerSecCounters | Measure-Object -Property CookedValue -Sum).Sum
                        # If at least one ASP.NET Apps v4.0.30319 request
                        if ($ASPNetAppsV4RequestPerSec -gt 0)
                        {
                            #Getting the .NET CLR Exceptions Exceptions Thrown/Sec performance counters for every worker process
                            $NetClrExceptionsThrownPerSecCounters = $_.CounterSamples | Where-Object -FilterScript {
                                ($_.InstanceName -like '*w3wp*') -and ($_.ServerName -eq $CurrentServerName ) 
                            }
                            $NetClrExceptionsThrownPerSec = ($NetClrExceptionsThrownPerSecCounters | Measure-Object -Property CookedValue -Sum).Sum
                            #We calculate the rate
                            $Percent = $NetClrExceptionsThrownPerSec/$ASPNetAppsV4RequestPerSec*100
                            #If this rate exceeds the 5% percent threshold
                            if ($Percent -ge 5)
                            {
                                Write-Verbose -Message $("[$($_.TimeStamp)] More than 5 percent of ASP.NET Apps v4.0.30319\Requests/sec are .NET exceptions. The Rate is {0:N2}% ({1:N2}/{2:N2})" -f $Percent, $NetClrExceptionsThrownPerSec, $ASPNetAppsV4RequestPerSec)
                                #Building an object to store the data
                                $CurrentData = New-Object -TypeName PSObject -Property @{
                                    SourceBLGFullName               = $SourceBLGFullName
                                    ServerName                      = $CurrentServerName
                                    Timestamp                       = $Timestamp
                                    NetClrExceptionsThrownPerSec    = $NetClrExceptionsThrownPerSec
                                    ASPNetApplicationsRequestPerSec = $ASPNetAppsV4RequestPerSec
                                    Percent                         = $Percent
                                    IsThreSholdExceeded             = $true
                                }
                                #Adding this data to the data array
                                $Data += $CurrentData
                            }
                            #If the -full switch has been specified we even store the data
                            elseif ($Full)
                            {
                                Write-Verbose -Message $("[$($_.TimeStamp)] Less than 5 percent of ASP.NET Apps v4.0.30319\Requests/sec are .NET exceptions. The Rate is {0:N2}% ({1:N2}/{2:N2})" -f $Percent, $NetClrExceptionsThrownPerSec, $ASPNetAppsV4RequestPerSec)
                                $CurrentData = New-Object -TypeName PSObject -Property @{
                                    SourceBLGFullName               = $SourceBLGFullName
                                    ServerName                      = $CurrentServerName
                                    Timestamp                       = $Timestamp
                                    NetClrExceptionsThrownPerSec    = $NetClrExceptionsThrownPerSec
                                    ASPNetApplicationsRequestPerSec = $ASPNetAppsV4RequestPerSec
                                    Percent                         = $Percent
                                    IsThreSholdExceeded             = $false
                                }
                                $Data += $CurrentData
                            }
                        }
                        #IF no ASP.Net v4 request but -full switch has been specified we write a verbose message
                        elseif ($Full)
                        {
                            Write-Verbose -Message "[$($_.TimeStamp)] No ASP.NET Apps v4.0.30319 Request "
                        }
                    }
                    #Regarless the ASP.Net version
                    else
                    {
                        #Getting the ASP.Net Request/Sec performance counters for every web applications
                        $ASPNetApplicationsRequestPerSecCounters = ($_.CounterSamples | Where-Object -FilterScript {
                                ($_.Path -like '*ASP.NET Applications*') -and ($_.InstanceName -like '*_LM_W3SVC_*') -and ($_.ServerName -eq $CurrentServerName ) 
                        })
                        $ASPNetApplicationsRequestPerSec = ($ASPNetApplicationsRequestPerSecCounters | Measure-Object -Property CookedValue -Sum).Sum
                        # If at least one ASP.Net Applications request
                        if ($ASPNetApplicationsRequestPerSec -gt 0)
                        {
                            #Getting the .NET CLR Exceptions Exceptions Thrown/Sec performance counters for every worker process
                            $NetClrExceptionsThrownPerSecCounters = $_.CounterSamples | Where-Object -FilterScript {
                                ($_.InstanceName -like '*w3wp*') -and ($_.ServerName -eq $CurrentServerName ) 
                            }
                            $NetClrExceptionsThrownPerSec = ($NetClrExceptionsThrownPerSecCounters | Measure-Object -Property CookedValue -Sum).Sum
                            #We calculate the rate
                            $Percent = $NetClrExceptionsThrownPerSec/$ASPNetApplicationsRequestPerSec*100
                            #If this rate exceeds the 5% percent threshold
                            if ($Percent -ge 5)
                            {
                                Write-Verbose -Message $("[$($_.TimeStamp)] More than 5 percent of ASP.NET Applications\Requests/sec are .NET exceptions. The Rate is {0:N2}% ({1:N2}/{2:N2})" -f $Percent, $NetClrExceptionsThrownPerSec, $ASPNetApplicationsRequestPerSec)
                                #Building an object to store the data
                                $CurrentData = New-Object -TypeName PSObject -Property @{
                                    SourceBLGFullName               = $SourceBLGFullName
                                    ServerName                      = $CurrentServerName
                                    Timestamp                       = $Timestamp
                                    NetClrExceptionsThrownPerSec    = $NetClrExceptionsThrownPerSec
                                    ASPNetApplicationsRequestPerSec = $ASPNetApplicationsRequestPerSec
                                    Percent                         = $Percent
                                    IsThreSholdExceeded             = $true
                                }
                                #Adding this data to the data array
                                $Data += $CurrentData
                            }
                            #If the -full switch has been specified we even store the data
                            elseif ($Full)
                            {
                                Write-Verbose -Message $("[$($_.TimeStamp)] Less than 5 percent of ASP.NET Applications\Requests/sec are .NET exceptions. The Rate is {0:N2}% ({1:N2}/{2:N2})" -f $Percent, $NetClrExceptionsThrownPerSec, $ASPNetApplicationsRequestPerSec)
                                $CurrentData = New-Object -TypeName PSObject -Property @{
                                    SourceBLGFullName               = $SourceBLGFullName
                                    ServerName                      = $CurrentServerName
                                    Timestamp                       = $Timestamp
                                    NetClrExceptionsThrownPerSec    = $NetClrExceptionsThrownPerSec
                                    ASPNetApplicationsRequestPerSec = $ASPNetApplicationsRequestPerSec
                                    Percent                         = $Percent
                                    IsThreSholdExceeded             = $false
                                }
                                $Data += $CurrentData
                            }
                        }
                        #IF no ASP.Net v4 request but -full switch has been specified we write a verbose message
                        elseif ($Full)
                        {
                            Write-Verbose -Message "[$($_.TimeStamp)] No ASP.Net Applications Request "
                        }
                    }            
                }
            }
        }
    }
    end
    {
        #returning the data array
        return $Data
    }
}
#endregion


Clear-Host
# Getting the this script path
$CurrentScript = $MyInvocation.MyCommand.Path
# Getting the directory of this script
$CurrentDir = Split-Path -Path $CurrentScript -Parent

# Building a timestamped CSV file with the same base name that with script
$CSVFile = $CurrentScript.replace((Get-Item -Path $CurrentScript).Extension, '_'+$(Get-Date  -Format 'yyyyMMddTHHmmss')+'.csv')

# Analyzing .Net v2 performance for every BLG file in the current directory
#$Data = Get-ChildItem -Path $CurrentDir -Filter '*.blg' | Get-NetExceptionsRate -V2 -Verbose -Full

# Analyzing .Net v4 performance for every BLG file in the current directory
#$Data = Get-ChildItem -Path $CurrentDir -Filter '*.blg' | Get-NetExceptionsRate -V4 -Verbose -Full

# Analyzing .Net performance (regardless the version) for every BLG file in the current directory
$Data = Get-ChildItem -Path $CurrentDir -Filter '*.blg' | Get-NetExceptionsRate -Verbose -Full

#Exporting the data in the CSV file 
$Data | Export-Csv -Path $CSVFile -Force -NoTypeInformation
Write-Host -Object "Results are available in '$CSVFile'"

Le résultat sera un fichier CSV comme celui-ci :

Get-NetExceptionsRate

Ensuite vous avez la possibilité de trier sur la colonne D (IsThreSholdExceeded) pour ne garder que les intervalles de temps où le seuil est dépassé (valeur True).

Get-NetExceptionsRate_Filtered


Aller à la Version française

During my customer visits I often check the ASP.Net configuration and take a quick look at the performance of their applications. I let the deep expertise to my colleagues PFE Development. Apart debug mode enabled on at least one production application in 50% of customers (remember to check your applications and review your business processes accordingly), I usually collect performance data (ideally 24 hours) via Performance Monitor (aka PerfMon). Below I give you my script to start all from a command line:

 REM To create a Data Collector Set with the appropriate Performance counters for an IIS server (every 15 seconds during 24 hours)
logman create counter %COMPUTERNAME%-IIS -v mmddhhmm -o %SYSTEMDRIVE%\PerfLogs\Admin\%COMPUTERNAME%-IISPerformanceCounters -f bin -rf 24:00:00 -si 00:00:15 -c "\.NET CLR Data\*" "\.NET CLR Exceptions(*)\*" "\.NET CLR Interop(*)\*" "\.NET CLR Jit(*)\*" "\.NET CLR Loading(*)\*" "\.NET CLR LocksAndThreads(*)\*" "\.NET CLR Memory(*)\*" "\.NET CLR Networking(*)\*" "\.NET CLR Remoting(*)\*" "\.NET CLR Security(*)\*" "\.NET Data Provider for SqlServer(*)\*" "\Active Server Pages\*" "\APP_POOL_WAS(*)\*" "\ASP.NET Applications(*)\*" "\ASP.NET Apps v1.1.4322(*)\*" "\ASP.NET Apps v2.0.50727(*)\*" "\ASP.NET Apps v4.0.30319(*)\*" "\ASP.NET v1.1.4322\*" "\ASP.NET v2.0.50727\*" "\ASP.NET v4.0.30319\*" "\ASP.NET\*" "\Cache\*" "\Distributed Transaction Coordinator\*" "\HTTP Service Request Queues(*)\*" "\HTTP Service Url Groups(*)\*" "\HTTP Service\*" "\Internet Information Services Global\*" "\LogicalDisk(*)\*" "\Memory\*" "\Network Inspection System\*" "\Network Interface(*)\*" "\Paging File(*)\*" "\PhysicalDisk(*)\*" "\Process(*)\*" "\Processor Information(*)\*" "\Processor(*)\*" "\Server\*" "\System\*" "\TCP\*" "\TCPv4\*"  "\TCPv6\*" "\W3SVC_W3WP\*" "\WAS_W3WP(*)\*" "\Web Service Cache\*" "\Web Service(*)\*"
 
REM To start the Data Collector Set 
logman start %COMPUTERNAME%-IIS

 

I keep here the default location (%SYSTEMDRIVE%\PerfLogs\Admin\%COMPUTERNAME%-IIS) for the generated file (* .blg) you can of course change.

Then a macro analysis with PAL(if you do not know PAL I advise you very strongly to read this article) give you a quick overview of the performance of the server. But that is not the purpose of this article.

I would like here to focus on .Net exceptions and their impact on the performance of your Web / ASP.Net applications. As explained on https://msdn.microsoft.com/en-us/library/ff647791.aspx (cf. Exceptions).

The .NET CLR Exceptions are observed through the “ .NET CLR Exceptions, # of Excepts Thrown” performance counter (This counter indicates the total number of exceptions generated per second in managed code). The recommendation is to stay close to 0. An application which generates exceptions can have bugs or use the exception mechanism to manage application. This last use is not recommended at all because exceptions are very costly and can severely degrade your application performance. You should investigate your code for application logic that uses exceptions for normal processing behavior. Response.Redirect, “ .NET CLR Exceptions, # of Excepts Thrown” and Response.End all cause a ThreadAbortException in ASP.NET applications. A well-functioning Web server should not generate errors.If errors occur in your ASP.NET Web application, they may skew any throughput results because of very different code paths for error recovery. Investigate and fix any bugs in your application before performance testing. In addition, no more than 5 percent of Request/sec for the ASP.NET application has to be .NET exceptions (counter “.NET CLR Exceptions, # of Excepts Thrown / sec” ). If you see more than 1 request in 20 throw an exception, you should pay closer attention to it. And it is precisely on this last point that I propose a PowerShell script that will analyze your performance files (* .BLG) and tell you when the threshold of 5% of ASP.Net exceptions is exceeded (for all applications). You are not obliged to use the set of counters that I specified above, but you need at least to collect the following counters:

  • .NET CLR Exceptions(w3wp*)\# of Exceps Thrown / sec or .NET CLR Exceptions(*)\# of Exceps Thrown / sec
  • ASP.NET Applications(*_LM_W3SVC_*)\Requests/Sec or ASP.NET Applications(*)\Requests/Sec

The source code is available here.

 #requires -version 2

#region function definitions
Function Get-NetExceptionsRate
{
    <#
            .SYNOPSIS
            Return performance counters data by extracting only the .NET CLR Exceptions/(*w3wp*)\# of Exceps Thrown / sec and ASP.NET Applications\(*_LM_W3SVC_*)\Requests/Sec when the 5 percent threshold is exceeded.

            .DESCRIPTION
            Return performance counters data by extracting only the .NET CLR Exceptions/(*w3wp*)\# of Exceps Thrown / sec and ASP.NET Applications\(*_LM_W3SVC_*)\Requests/Sec when the 5 percent threshold is exceeded.

            .PARAMETER FullName
            The BLG File to analyze specified by its full name

            .PARAMETER Full
            A switch to specify if we return all the data (threshold exceeded or not)

            .EXAMPLE
            Get-ChildItem "*.blg" | Get-NetExceptionsRate -Verbose
    #>
    [CmdletBinding(DefaultParameterSetName = 'Global', SupportsShouldProcess = $true)]
    Param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Position = 0)]
        #The BLG File to convert : Checking if the file exists and has the .BLG extension
        [ValidateScript({
                    (Test-Path -Path $_ -PathType Leaf) -and ($_ -match '\.blg$')
        })]
        [alias('Source', 'BLG')]
        [String[]]$FullName,

        #To list all timestamp even if the 5 percent is not exceeded"
        [parameter(Mandatory = $false)]
        [switch]$Full,
        
        # For ASP.Net 2.0
        [Parameter(ParameterSetName = 'ASPNetV2')]
        [switch]$v2,        

        # For ASP.Net 4.0
        [Parameter(ParameterSetName = 'ASPNetV4')]
        [switch]$v4        
    )
    begin
    {
        #Array for storing the results
        $Data = @()
    }    
    process
    {
        #For all files passed as argument outside a pipeline context
        ForEach ($CurrentFullName in $FullName)
        {
            $CurrentFullName = Get-Item -Path $CurrentFullName
            $SourceBLGFullName = $CurrentFullName.FullName
            Write-Host -Object "Processing $SourceBLGFullName ..."

            # Keeping only '.NET CLR Exceptions', 'ASP.NET Applications', 'ASP.NET Apps v2.0.50727' and 'ASP.NET Apps v4.0.30319' performance counter names
            Write-Verbose -Message 'Extracting .Net Performance Data ...'
            $CounterNames = Import-Counter -Path $SourceBLGFullName  -Listset '.NET CLR Exceptions', 'ASP.NET Applications', 'ASP.NET Apps v2.0.50727', 'ASP.NET Apps v4.0.30319' -ErrorAction SilentlyContinue |
            Select-Object -ExpandProperty PathsWithInstances |
            Where-Object -FilterScript {
                ($_ -like '*w3wp*)\# of Exceps Thrown / sec') -or ($_ -like '*_LM_W3SVC_*)\Requests/Sec')
            }

            #Importing only the required performance counters"
            $Counters = $(Import-Counter -Path $SourceBLGFullName -Counter $CounterNames -ErrorAction SilentlyContinue)
            
            #Getting the computer list (if data have been collected remotely for multiple servers)
            $ServerNames = $Counters.countersamples |
            ForEach-Object -Process {
                $_.path.split('\')[2] 
            } |
            Select-Object -Unique

            # Adding the computername as a member of the performance counters
            $Counters.countersamples | ForEach-Object -Process {
                $_ | Add-Member -MemberType NoteProperty -Name 'ServerName' -Value $($_.path.split('\')[2])
            }

            #Processing loop for each server
            ForEach ($CurrentServerName in $ServerNames) 
            {
                Write-Verbose -Message "Processing performance counter for $($CurrentServerName) ..."

                #Getting the performance couter list for the processed server
                $CurrentCounters  = $($CounterNames | Where-Object -FilterScript {
                        $_ -like "*$CurrentServerName*"
                }) -join ', '
                Write-Verbose -Message "Processing the following counters : $CurrentCounters"
                
                $Counters | ForEach-Object -Process {
                    $Timestamp = $_.Timestamp
                    #If we work only for ASP.Net v2
                    if ($v2)
                    {
                        #Getting the ASP.Net Request/Sec performance counters for every web applications
                        $ASPNetAppsV2RequestPerSecCounters = ($_.CounterSamples | Where-Object -FilterScript {
                                ($_.Path -like '*ASP.NET Apps v2.0.50727*') -and ($_.InstanceName -like '*_LM_W3SVC_*') -and ($_.ServerName -eq $CurrentServerName ) 
                        })
                        $ASPNetAppsV2RequestPerSec = ($ASPNetAppsV2RequestPerSecCounters | Measure-Object -Property CookedValue -Sum).Sum

                        # If at least one ASP.NET Apps v2.0.50727 request
                        if ($ASPNetAppsV2RequestPerSec -gt 0)
                        {
                            #Getting the .NET CLR Exceptions Exceptions Thrown/Sec performance counters for every worker process
                            $NetClrExceptionsThrownPerSecCounters = $_.CounterSamples | Where-Object -FilterScript {
                                ($_.InstanceName -like '*w3wp*') -and ($_.ServerName -eq $CurrentServerName ) 
                            }
                            $NetClrExceptionsThrownPerSec = ($NetClrExceptionsThrownPerSecCounters | Measure-Object -Property CookedValue -Sum).Sum
                            #We calculate the rate
                            $Percent = $NetClrExceptionsThrownPerSec/$ASPNetAppsV2RequestPerSec*100
                            #If this rate exceeds the 5% percent threshold
                            if ($Percent -ge 5)
                            {
                                Write-Verbose -Message $("[$($_.TimeStamp)] More than 5 percent of ASP.NET Apps v2.0.50727\Requests/sec are .NET exceptions. The Rate is {0:N2}% ({1:N2}/{2:N2})" -f $Percent, $NetClrExceptionsThrownPerSec, $ASPNetAppsV2RequestPerSec)
                                #Building an object to store the data
                                $CurrentData = New-Object -TypeName PSObject -Property @{
                                    SourceBLGFullName               = $SourceBLGFullName
                                    ServerName                      = $CurrentServerName
                                    Timestamp                       = $Timestamp
                                    NetClrExceptionsThrownPerSec    = $NetClrExceptionsThrownPerSec
                                    ASPNetApplicationsRequestPerSec = $ASPNetAppsV2RequestPerSec
                                    Percent                         = $Percent
                                    IsThreSholdExceeded             = $true
                                }
                                #Adding this data to the data array
                                $Data += $CurrentData
                            }
                            #If the -full switch has been specified we even store the data
                            elseif ($Full)
                            {
                                Write-Verbose -Message $("[$($_.TimeStamp)] Less than 5 percent of ASP.NET Apps v2.0.50727\Requests/sec are .NET exceptions. The Rate is {0:N2}% ({1:N2}/{2:N2})" -f $Percent, $NetClrExceptionsThrownPerSec, $ASPNetAppsV2RequestPerSec)
                                $CurrentData = New-Object -TypeName PSObject -Property @{
                                    SourceBLGFullName               = $SourceBLGFullName
                                    ServerName                      = $CurrentServerName
                                    Timestamp                       = $Timestamp
                                    NetClrExceptionsThrownPerSec    = $NetClrExceptionsThrownPerSec
                                    ASPNetApplicationsRequestPerSec = $ASPNetAppsV2RequestPerSec
                                    Percent                         = $Percent
                                    IsThreSholdExceeded             = $false
                                }
                                $Data += $CurrentData
                            }
                        }
                        #IF no ASP.Net v2 request but -full switch has been specified we write a verbose message
                        elseif ($Full)
                        {
                            Write-Verbose -Message "[$($_.TimeStamp)] No ASP.NET Apps v2.0.50727 Request "
                        }
                    }
                    #If we work only for ASP.Net v4
                    elseif ($v4)
                    {
                        #Getting the ASP.Net Request/Sec performance counters for every web applications
                        $ASPNetAppsV4RequestPerSecCounters = ($_.CounterSamples | Where-Object -FilterScript {
                                ($_.Path -like '*ASP.NET Apps v4.0.30319*') -and ($_.InstanceName -like '*_LM_W3SVC_*') -and ($_.ServerName -eq $CurrentServerName ) 
                        })
                        $ASPNetAppsV4RequestPerSec = ($ASPNetAppsV4RequestPerSecCounters | Measure-Object -Property CookedValue -Sum).Sum
                        # If at least one ASP.NET Apps v4.0.30319 request
                        if ($ASPNetAppsV4RequestPerSec -gt 0)
                        {
                            #Getting the .NET CLR Exceptions Exceptions Thrown/Sec performance counters for every worker process
                            $NetClrExceptionsThrownPerSecCounters = $_.CounterSamples | Where-Object -FilterScript {
                                ($_.InstanceName -like '*w3wp*') -and ($_.ServerName -eq $CurrentServerName ) 
                            }
                            $NetClrExceptionsThrownPerSec = ($NetClrExceptionsThrownPerSecCounters | Measure-Object -Property CookedValue -Sum).Sum
                            #We calculate the rate
                            $Percent = $NetClrExceptionsThrownPerSec/$ASPNetAppsV4RequestPerSec*100
                            #If this rate exceeds the 5% percent threshold
                            if ($Percent -ge 5)
                            {
                                Write-Verbose -Message $("[$($_.TimeStamp)] More than 5 percent of ASP.NET Apps v4.0.30319\Requests/sec are .NET exceptions. The Rate is {0:N2}% ({1:N2}/{2:N2})" -f $Percent, $NetClrExceptionsThrownPerSec, $ASPNetAppsV4RequestPerSec)
                                #Building an object to store the data
                                $CurrentData = New-Object -TypeName PSObject -Property @{
                                    SourceBLGFullName               = $SourceBLGFullName
                                    ServerName                      = $CurrentServerName
                                    Timestamp                       = $Timestamp
                                    NetClrExceptionsThrownPerSec    = $NetClrExceptionsThrownPerSec
                                    ASPNetApplicationsRequestPerSec = $ASPNetAppsV4RequestPerSec
                                    Percent                         = $Percent
                                    IsThreSholdExceeded             = $true
                                }
                                #Adding this data to the data array
                                $Data += $CurrentData
                            }
                            #If the -full switch has been specified we even store the data
                            elseif ($Full)
                            {
                                Write-Verbose -Message $("[$($_.TimeStamp)] Less than 5 percent of ASP.NET Apps v4.0.30319\Requests/sec are .NET exceptions. The Rate is {0:N2}% ({1:N2}/{2:N2})" -f $Percent, $NetClrExceptionsThrownPerSec, $ASPNetAppsV4RequestPerSec)
                                $CurrentData = New-Object -TypeName PSObject -Property @{
                                    SourceBLGFullName               = $SourceBLGFullName
                                    ServerName                      = $CurrentServerName
                                    Timestamp                       = $Timestamp
                                    NetClrExceptionsThrownPerSec    = $NetClrExceptionsThrownPerSec
                                    ASPNetApplicationsRequestPerSec = $ASPNetAppsV4RequestPerSec
                                    Percent                         = $Percent
                                    IsThreSholdExceeded             = $false
                                }
                                $Data += $CurrentData
                            }
                        }
                        #IF no ASP.Net v4 request but -full switch has been specified we write a verbose message
                        elseif ($Full)
                        {
                            Write-Verbose -Message "[$($_.TimeStamp)] No ASP.NET Apps v4.0.30319 Request "
                        }
                    }
                    #Regarless the ASP.Net version
                    else
                    {
                        #Getting the ASP.Net Request/Sec performance counters for every web applications
                        $ASPNetApplicationsRequestPerSecCounters = ($_.CounterSamples | Where-Object -FilterScript {
                                ($_.Path -like '*ASP.NET Applications*') -and ($_.InstanceName -like '*_LM_W3SVC_*') -and ($_.ServerName -eq $CurrentServerName ) 
                        })
                        $ASPNetApplicationsRequestPerSec = ($ASPNetApplicationsRequestPerSecCounters | Measure-Object -Property CookedValue -Sum).Sum
                        # If at least one ASP.Net Applications request
                        if ($ASPNetApplicationsRequestPerSec -gt 0)
                        {
                            #Getting the .NET CLR Exceptions Exceptions Thrown/Sec performance counters for every worker process
                            $NetClrExceptionsThrownPerSecCounters = $_.CounterSamples | Where-Object -FilterScript {
                                ($_.InstanceName -like '*w3wp*') -and ($_.ServerName -eq $CurrentServerName ) 
                            }
                            $NetClrExceptionsThrownPerSec = ($NetClrExceptionsThrownPerSecCounters | Measure-Object -Property CookedValue -Sum).Sum
                            #We calculate the rate
                            $Percent = $NetClrExceptionsThrownPerSec/$ASPNetApplicationsRequestPerSec*100
                            #If this rate exceeds the 5% percent threshold
                            if ($Percent -ge 5)
                            {
                                Write-Verbose -Message $("[$($_.TimeStamp)] More than 5 percent of ASP.NET Applications\Requests/sec are .NET exceptions. The Rate is {0:N2}% ({1:N2}/{2:N2})" -f $Percent, $NetClrExceptionsThrownPerSec, $ASPNetApplicationsRequestPerSec)
                                #Building an object to store the data
                                $CurrentData = New-Object -TypeName PSObject -Property @{
                                    SourceBLGFullName               = $SourceBLGFullName
                                    ServerName                      = $CurrentServerName
                                    Timestamp                       = $Timestamp
                                    NetClrExceptionsThrownPerSec    = $NetClrExceptionsThrownPerSec
                                    ASPNetApplicationsRequestPerSec = $ASPNetApplicationsRequestPerSec
                                    Percent                         = $Percent
                                    IsThreSholdExceeded             = $true
                                }
                                #Adding this data to the data array
                                $Data += $CurrentData
                            }
                            #If the -full switch has been specified we even store the data
                            elseif ($Full)
                            {
                                Write-Verbose -Message $("[$($_.TimeStamp)] Less than 5 percent of ASP.NET Applications\Requests/sec are .NET exceptions. The Rate is {0:N2}% ({1:N2}/{2:N2})" -f $Percent, $NetClrExceptionsThrownPerSec, $ASPNetApplicationsRequestPerSec)
                                $CurrentData = New-Object -TypeName PSObject -Property @{
                                    SourceBLGFullName               = $SourceBLGFullName
                                    ServerName                      = $CurrentServerName
                                    Timestamp                       = $Timestamp
                                    NetClrExceptionsThrownPerSec    = $NetClrExceptionsThrownPerSec
                                    ASPNetApplicationsRequestPerSec = $ASPNetApplicationsRequestPerSec
                                    Percent                         = $Percent
                                    IsThreSholdExceeded             = $false
                                }
                                $Data += $CurrentData
                            }
                        }
                        #IF no ASP.Net v4 request but -full switch has been specified we write a verbose message
                        elseif ($Full)
                        {
                            Write-Verbose -Message "[$($_.TimeStamp)] No ASP.Net Applications Request "
                        }
                    }            
                }
            }
        }
    }
    end
    {
        #returning the data array
        return $Data
    }
}
#endregion


Clear-Host
# Getting the this script path
$CurrentScript = $MyInvocation.MyCommand.Path
# Getting the directory of this script
$CurrentDir = Split-Path -Path $CurrentScript -Parent

# Building a timestamped CSV file with the same base name that with script
$CSVFile = $CurrentScript.replace((Get-Item -Path $CurrentScript).Extension, '_'+$(Get-Date  -Format 'yyyyMMddTHHmmss')+'.csv')

# Analyzing .Net v2 performance for every BLG file in the current directory
#$Data = Get-ChildItem -Path $CurrentDir -Filter '*.blg' | Get-NetExceptionsRate -V2 -Verbose -Full

# Analyzing .Net v4 performance for every BLG file in the current directory
#$Data = Get-ChildItem -Path $CurrentDir -Filter '*.blg' | Get-NetExceptionsRate -V4 -Verbose -Full

# Analyzing .Net performance (regardless the version) for every BLG file in the current directory
$Data = Get-ChildItem -Path $CurrentDir -Filter '*.blg' | Get-NetExceptionsRate -Verbose -Full

#Exporting the data in the CSV file 
$Data | Export-Csv -Path $CSVFile -Force -NoTypeInformation
Write-Host -Object "Results are available in '$CSVFile'"

The result will be a CSV file as shown below:

Get-NetExceptionsRate

Then you have the ability to sort the column D (IsThreSholdExceeded) to keep only the time intervals in which the threshold is exceeded (value True).
Get-NetExceptionsRate_Filtered

 

Laurent.