Share via


Perfmon Log Translator (PLT)

Go to English version

L’utilisation de PAL a déjà été couverte dans un article d’Arnaud Lheureux qui est disponible ici (L’article est en français. Pour toute autre langue je vous invite à consulter Performance Analysis of Logs (PAL) Tool)

Le principal problème dans l’utilisation de PAL vient de systèmes d’exploitation non installés en anglais (en-us). En effet l’outil se base sur les noms anglais des compteurs. Ce qui rend l’analyse de données (*.blg) issus d’un serveur non anglophone impossible. Perfmon Log Translator (PLT) est là pour pallier à ce problème. Il assure la traduction de compteurs en anglais (grâce à une correspondance des noms de compteurs inclus dans des fichiers XML - un fichier par langue). Malheureusement beaucoup de compteurs sont absents des fichiers XML fournis. J’ai donc décidé de pallier, en partie, à ce problème. Quand je dis en partie, je me suis limité à l’OS, IIS et ASP.Net. Mais comme vous le verrez par la suite, il est très simple d’étendre la traduction à d’autres rôles ou fonctionnalités du système d’exploitation et à d’autres langues. Si vous souhaitez juste récupérer mes fichiers XML de traduction c’est ici que cela se passe (Pour les cultures suivantes : et-EE, hr-HR, lt-LT, sv-SE, tr-TR, en-GB, lv-LV, sr-Latn-RS, es-MX, fr-CA, ro-RO, sk-SK, sl-SI, pt-PT, es-ES, fi-FI, fr-FR, cs-CZ, da-DK, de-DE, hu-HU, pl-PL, pt-BR, it-IT, nb-NO, nl-NL) . Dans tous les cas je vous invite quand même à lire la suite de l’article.

Ces fichiers ont été générés grâce au script suivant et dont le code est également affiché en bas de page. Le principe est simple. Je liste les compteurs (et leur ID) sur un OS installé en anglais (avec IIS et ASP.Net) que je mémorise dans un fichier CSV(*). Ensuite grâce au pack de langues (notamment dispo sur https://my.visualstudio.com et https://www.microsoft.com/Licensing/servicecenter) je bascule en français et je recommence. L’ID ne changeant pas, je fais la correspondance via cet ID. Attention toutefois, un même compteur n’a pas le même ID en fonction de la version de Windows Server (je ne connais pas la raison). Il est donc nécessaire d’exécuter le script sur tous les OS qui vous intéressent. Si vous souhaitez étendre à d’autres rôles/fonctionnalités, vous les installez sur votre OS (via des machines virtuelles ou Azure) et faites tourner le script dans chaque langue qui vous intéresse. A la fin vous obtiendrez un fichier XML par langue avec un suffixe correspondant à la version de l’OS (par exemple fr-FR_PFL_10.0.xml). Le suffixe sert à identifier l’OS source (10.0 étant la version du noyau de Windows Server 2016) et à ne pas écraser un fichier d’un autre OS. Ensuite vous copiez votre fichier dans le répertoire de PLT en supprimant le suffixe. En effet dans PAL le nom est simplement fr-FR_PFL.xml (pour la correspondance Français ==> Anglais).

(*)Le fichier CSV joue un rôle important car :

  • S’il n’existe pas il sera créé à la première utilisation (sur un OS anglais normalement , mais pas forcément). Autrement les compteurs qu’il contient (quelquesoit la langue) seront importés en mémoire
  • Il est propre à chaque OS (il utilise le même suffixe que les fichiers XML pour identifier l’OS source)
  • Il est unique (pour un même OS) quelquesoit la ou les langues utilisées
  • il est donc de ce fait incrémental (vous pouvez rajouter d’autres packs de langues pour un OS) et refaire tourner le script directement dans cette nouvelle langue (il faut toutefois qu’il ait tourné au moins une fois sur un OS anglais)

Aller à la version française

The PAL usage has already been covered in an article of Arnaud Lheureux which is available here (The article is written in the french language. For any other language, I invite you to read  Performance Analysis of Logs (PAL) Tool)

The main problem with the PAL usage comes from non-english (en-us) operating systems. Indeed, this tool uses only english performance counters. That makes the analysis of data (* .blg) from a non-English server impossible. Perfmon Log Translator (PLT) is there to overcome this problem. It ensures the translation english counters (via the matching of the counter names of included in XML files - one file per language). Unfortunately many counters are missing from the supplied XML files. So I decided to partially address this problem. When I say in partially, I focus only to the OS, IIS and ASP.Net. But as you will see later, it is very simple to extend the translation to other roles or functionalities of the operating system and to other languages. If you just want to retrieve my translation XML files this is here (For the following cultures : et-EE, hr-HR, lt-LT, sv-SE, tr-TR, en-GB, lv-LV, sr-Latn-RS, es-MX, fr-CA, ro-RO, sk-SK, sl-SI, pt-PT, es-ES, fi-FI, fr-FR, cs-CZ, da-DK, de-DE, hu-HU, pl-PL, pt-BR, it-IT, nb-NO, nl-NL) . Nevertheless, I invite you to read the rest of the article.

These files were generated by the following script and whose code is also displayed at the bottom of the page. The principle is pretty simple. I list the counters (and their ID) on an english OS (with IIS and ASP.Net) that I store in a CSV file(*). Then I install the relevant language pack (especially available on https://my.visualstudio.com and https://www.microsoft.com/Licensing/servicecenter) I switch to French and I start again. The ID does not change, I do the correspondence via this ID. Be careful though, the same counter does not have the same ID depending on the version of Windows Server (I do not know the reason). It is therefore necessary to run the script on all the OSes you are interested in. If you want to extend to other roles / features, install them on your OS (via virtual machines or Azure) and run the script in each language you are interested in. At the end you will get an XML file per language with a suffix corresponding to the version of the OS (for example fr-FR_PFL_10.0.xml). The suffix is ​​used to identify the source OS (10.0 being the kernel version of Windows Server 2016) and not to overwrite a file from another OS. Then you copy your file to the PLT directory by deleting the suffix. Indeed in PAL the name is simply en-FR_PFL.xml (for the French correspondence ==> English).

(*)The CSV file plays an important role because:

  • If it does not exist it will be created at the first use (on an English OS normally, but not necessarily). Otherwise the counters it contains (regardless language) will be imported into memory
  • It is unique for each OS (it uses the same suffix as the XML files to identify the source OS)
  • It is unique (for the same OS) regardless of the language(s) used
  • It is therefore incremental (you can add other language packs for an OS) and redo the script directly with this new language (it must however have run at least once on an English OS)  
 #requires -version 3
function Compare-PLTFile
{
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory = $True, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
        [ValidateScript({
                    Test-Path -Path $_ -PathType Container
        })]
        #The directory where are stored the generated XML File
        [Alias('Directory', 'Path')]
        [String]$FullName
    )

    $PLTFiles=Get-ChildItem -Filter *_PFL.xml -File -Path $FullName -Recurse | Group-Object -Property Name -AsHashTable -AsString

    $Differences = New-Object -TypeName 'System.Collections.ArrayList'
    foreach ($XMLFile in $PLTFiles.Keys)
    {
        Write-Verbose -Message "Processing $XMLFile ..."
        $HT=@{}
        foreach ($CurrentXMLFile in $PLTFiles[$XMLFile])
        {
            Write-Verbose -Message "Processing $($CurrentXMLFile.FullName) ..."
            $OS = $($CurrentXMLFile.Directory.Name)
            Write-Verbose -Message "Operation System : $OS"
            Write-Verbose -Message "`$CurrentXMLFile : $($CurrentXMLFile.FullName)"
            [xml] $XMLContent = Get-Content -Path $($CurrentXMLFile.FullName)
            $LanguageNodes=$XMLContent.SelectNodes("/Counters/Counter")
            foreach ($CurrentLanguageNode in $LanguageNodes)
            {
                if ($HT.ContainsKey($CurrentLanguageNode.en))
                {
                    $Data=$HT[$CurrentLanguageNode.en]
                    if ($Data.Value -ne $CurrentLanguageNode.org)
                    {
                        Write-Verbose -Message "Difference found for [$($CurrentLanguageNode.en)] in $XMLFile ..."
                        Write-Verbose -Message "[$($CurrentLanguageNode.en)]: [$OS]$($CurrentLanguageNode.org) vs. [$($Data.OS)]$($Data.Value)"
                        $CurrentDifference = New-Object -TypeName PSObject -Property @{ EN = $CurrentLanguageNode.en; Locale = $($XMLFile -replace "_PFL.xml", ""); OS1 = $OS; Value1 = $CurrentLanguageNode.org; OS2 = $Data.OS; Value2 = $Data.Value}
                        $null = $Differences.Add($CurrentDifference)
                    }
                }
                else
                {
                    $Data = New-Object -TypeName PSObject -Property @{ OS = $OS; Value = $CurrentLanguageNode.org }
                    $HT.Add($CurrentLanguageNode.en, $Data)
                }
            }
        }   
    }
    return $Differences
}

function New-PLTLangFile
{
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory = $True, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
        [ValidateScript({
                    Test-Path -Path $_ -PathType Container
        })]
        #The directory where are stored the generated XML File
        [Alias('Directory', 'Path')]
        [String]$FullName
    )
    $PLTFiles=Get-ChildItem -Filter *_PFL.xml -File -Path $FullName -Recurse | Group-Object -Property Directory -AsHashTable

    foreach ($Directory in $PLTFiles.Keys)
    {
        $XMLFile = Join-Path -Path $($Directory.FullName) -ChildPath "PLT-lang.xml"
        Write-Verbose -Message "Generating $XMLFile ..."
        $XMLWriter = New-Object -TypeName System.XMl.XmlTextWriter -ArgumentList ($XMLFile, [System.Text.Encoding]::UTF8)
        $XMLWriter.Formatting = [ System.Xml.Formatting]::Indented
        $XMLWriter.Indentation = 1
        $XMLWriter.IndentChar = "`t"
        $XMLWriter.WriteStartDocument()
        #Adding a comment to have the date of the generation
        $XMLWriter.WriteComment("Generated (UTC): $(Get-Date -Format 'U')")
        $XMLWriter.WriteStartElement('languages')
        foreach ($CurrentPLTFile in $PLTFiles[$Directory])
        {
            Write-Verbose -Message "Adding locale : $($CurrentCulture.DisplayName)"
            $CurrentCulture=[cultureinfo]::GetCultureInfo($CurrentPLTFile.BaseName  -replace "_PFL", "")
            $XMLWriter.WriteStartElement('language')
            $XMLWriter.WriteElementString('displayName',$CurrentCulture.DisplayName)
            $XMLWriter.WriteElementString('fileName', $CurrentCulture.Name+"_PFL.xml")
            $XMLWriter.WriteEndElement()
        }
        $XMLWriter.WriteEndElement()
        $XMLWriter.WriteEndDocument()
        $XMLWriter.Flush()
        $XMLWriter.Close()
    }
}


function Get-ProcessedLanguage
{
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory = $True, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
        [ValidateScript({
                    Test-Path -Path $_ -PathType Container
        })]
        #The directory where are stored the generated XML File
        [Alias('Directory', 'Path')]
        [String]$FullName
    )
    $ProcessedLanguages = (Get-ChildItem -Path $FullName -Filter "*-*.xml" -File).BaseName -replace "_.*$", ""
    return $ProcessedLanguages
}

#Getting Performance counter data from the local registry
function Get-PerformanceCounter
{
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory = $True, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
        [ValidateScript({
                    Test-Path -Path $_ -PathType Container
        })]
        #The directory where are stored the generated XML File
        [Alias('Directory', 'Path')]
        [String]$FullName,
        #Switch to force the performance data collection event if this language has already been processed
        [Switch]$Force
    )

    #Getting the current UI culture
    $CurrentUICulture = ([cultureinfo]::CurrentUICulture).Name
    $ProcessedLanguages = Get-ProcessedLanguage -Directory $FullName
    if (($CurrentUICulture -notin $ProcessedLanguages) -or ($Force))
    {

        if ($CurrentUICulture -notin $ProcessedLanguages)
        {
            Write-Verbose -Message "[New] $CurrentUICulture will be processed"
        }
        elseif ($Force)
        {
            Write-Verbose -Message "[Force] $CurrentUICulture will be processed"
        }
        #Hashtable for storing the performance counter data. The Index is the key
        $PerformanceCounters = @{}
        #Getting all performance counters
        $RegistryPerformanceCounters = (Get-ItemProperty -Path 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Perflib\CurrentLanguage' -Name Counter).Counter
        $PerformanceCountersNumber = $RegistryPerformanceCounters.Count

        for($i = 2; $i -lt $PerformanceCountersNumber; $i += 2)    
        {
            #Getting the performance counter index
            $CurrentIndex = $RegistryPerformanceCounters[$i]
            #Getting the performance counter name
            $CurrentPerformanceCounterName = $RegistryPerformanceCounters[$i+1]
            $PercentComplete = ($i/$PerformanceCountersNumber)
            Write-Progress -Activity "[$i/$PerformanceCountersNumber] Retrieving $CurrentUICulture - $('{0:D4}' -f [int]$CurrentIndex) : $CurrentPerformanceCounterName" -Status ('Processing {0:p0}' -f $PercentComplete) -PercentComplete ($PercentComplete*100)
            #Only counter with a valid name
            if (($CurrentPerformanceCounterName) -and ($CurrentPerformanceCounterName.Length -gt 0))
            {
                Write-Verbose -Message "Getting Performance Counter with Index $('{0:D4}' -f [int]$CurrentIndex) : [$CurrentPerformanceCounterName]"
                $CounterObject = New-Object -TypeName PSObject -Property @{
                    #the index of the counter
                    Index = $CurrentIndex
                    #The name of the counter in the current locale, culture. Ie : en-US, fr-FR ...
                    $CurrentUICulture = $CurrentPerformanceCounterName
                }
                $PerformanceCounters.Add($CurrentIndex,$CounterObject)
            }
        }
        Write-Progress -Completed -Activity 'Performance Counters Collection Complete !'
        Write-Host -Object 'Performance Counters Collection Complete !'
    }
    else
    {
        $PerformanceCounters = $null
        Write-Verbose -Message "[Skip] $CurrentUICulture has already been processed"
    }
    return $PerformanceCounters
}

#Importing performance counter data from a specified CSV file
function Import-PerformanceCounter
{
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory = $True, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)][ValidateScript({
                    Test-Path -Path $_ -PathType Leaf
        })]
        #CSV file full name
        [String]$FullName
    )
    #Hashtable for storing the performance counter data. The Index is the key
    $PerformanceCounters = @{}
    Write-Verbose -Message "Importing the Performance Counters from $FullName"
    #Importing performance counter data with a valid Index
    $ImportedPerformanceCounters = Import-Csv -Path $FullName -Encoding 'UTF8'  | Where-Object -FilterScript {
        $_.Index
    }
    $Counter = 0
    #Going through the imported performance counter data
    foreach ($CurrentImportedPerformanceCounter in $ImportedPerformanceCounters)
    {
        $Counter++
        $PercentComplete = ($Counter/$ImportedPerformanceCounters.Count) 
        Write-Progress -Activity "[$Counter/$($ImportedPerformanceCounters.Count)] Importing Performance Counter with Index $('{0:D4}' -f [int]$CurrentImportedPerformanceCounter.Index)" -Status ('Processing {0:p0}' -f $PercentComplete) -PercentComplete ($PercentComplete*100)
        #If the counter with the current index has not been alreay imported
        if (!$PerformanceCounters.ContainsKey($CurrentImportedPerformanceCounter.Index))
        {
            #We add it to the Hashtable
            $PerformanceCounters.Add($CurrentImportedPerformanceCounter.Index, $CurrentImportedPerformanceCounter)
            Write-Verbose -Message "Importing Performance Counter with Index $('{0:D4}' -f [int]$($CurrentImportedPerformanceCounter.Index)) : $CurrentImportedPerformanceCounter"
        }
        else
        {
            #else we raise a non-terminating error
            Write-Error -Message "$($CurrentImportedPerformanceCounter.Index) was already imported"
        }
    }
    Write-Progress -Completed -Activity 'Performance Counters Import Complete !'
    Write-Host -Object 'Performance Counters Import Complete !'
    return $PerformanceCounters
}

#Exporting performance counter data into a specified CSV file
function Export-PerformanceCounter
{
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory = $True, ValueFromPipeline = $False, ValueFromPipelineByPropertyName = $False)][ValidateNotNullOrEmpty()][ValidateScript({
                    Test-Path -Path (Split-Path -Path $_ -Parent) -PathType Container
        })]
        #CSV file full name
        [Alias('FilePath')]
        [String]$Path,
        [Parameter(Mandatory = $True, ValueFromPipeline = $False, ValueFromPipelineByPropertyName = $False)][ValidateNotNullOrEmpty()]
        [hashtable]$PerformanceCounters
    )
    $PerformanceCounters.Values |
    Sort-Object -Property Index |
    Select-Object -Property Index, * -ErrorAction Ignore |
    Export-Csv -Path  $Path -Force -NoTypeInformation -Encoding 'UTF8'
    Write-Host -Object "The Performance Counters are exported to $Path"
}

#Merging the imported and the local performance counter 
function Merge-PerformanceCounter
{
    [CmdletBinding()]
    Param(
        #The imported performance counter
        [Parameter(Mandatory = $True, ValueFromPipeline = $False, ValueFromPipelineByPropertyName = $False)][ValidateNotNullOrEmpty()]
        [hashtable]$ImportedPerformanceCounters,
        #The local performance counter
        [Parameter(Mandatory = $True, ValueFromPipeline = $False, ValueFromPipelineByPropertyName = $False)][ValidateNotNullOrEmpty()]
        [Alias('LocalPerformanceCounters')]
        [hashtable]$CurrentCulturePerformanceCounters,
        [Parameter(Mandatory = $True, ValueFromPipeline = $False, ValueFromPipelineByPropertyName = $False)][ValidateNotNullOrEmpty()]
        #The current UI culture
        [String]$CurrentUICulture,
        #Switch to force the performance data if a counter with the same index already exists
        [Switch]$Force
    )
    #We work from the imported data
    $MergedPerformanceCounters = $ImportedPerformanceCounters
    #Going through the current localized performance counter
    foreach ($CurrentCulturePerformanceCounterValue in $CurrentCulturePerformanceCounters.Values)
    {
        Write-Verbose -Message "[Merge] Processing Performance Counter with Index $('{0:D4}' -f [int]$($CurrentCulturePerformanceCounterValue.Index)) : $CurrentCulturePerformanceCounterValue"
        #If the index of the current localized counter already exists in the imported counter: we can match the counter in different locales
        if ($MergedPerformanceCounters[$CurrentCulturePerformanceCounterValue.Index])
        {
            #If the counter name for the current culture is unknow, we add it to the hastable (the index is the key)
            if (-not ($MergedPerformanceCounters[$CurrentCulturePerformanceCounterValue.Index].PSObject.Properties[$CurrentUICulture]))
            {
                Write-Verbose -Message "Updating the Performance Counter with Index $('{0:D4}' -f [int]$($CurrentCulturePerformanceCounterValue.Index)) with $CurrentUICulture=$($CurrentCulturePerformanceCounterValue.$CurrentUICulture)"
                $MergedPerformanceCounters[$CurrentCulturePerformanceCounterValue.Index] | Add-Member -MemberType NoteProperty -Name "$CurrentUICulture" -Value $CurrentCulturePerformanceCounterValue.$CurrentUICulture
                # $MergedPerformanceCounters[$CurrentCulturePerformanceCounterValue.Index].PSObject.Properties[$CurrentUICulture]=$CurrentCulturePerformanceCounterValue.$CurrentUICulture
            }
            #If the counter name for the current culture is empty or null, we upate it in the hastable (the index is the key)
            elseif (($MergedPerformanceCounters[$CurrentCulturePerformanceCounterValue.Index].PSObject.Properties[$CurrentUICulture] -eq $null) -or ($MergedPerformanceCounters[$CurrentCulturePerformanceCounterValue.Index].PSObject.Properties[$CurrentUICulture].Length -le 0))
            {
                Write-Verbose -Message "Updating $CurrentUICulture for the Performance Counter with Index $('{0:D4}' -f [int]$($CurrentCulturePerformanceCounterValue.Index)) because the previous value was null or empty"
                #$ImportedPerformanceCounters[$CurrentCulturePerformanceCounterValue.Index].$CurrentUICulture=$CurrentCulturePerformanceCounterValue.$CurrentUICulture
                $MergedPerformanceCounters[$CurrentCulturePerformanceCounterValue.Index].$CurrentUICulture = $CurrentCulturePerformanceCounterValue.$CurrentUICulture
            }
            #else (valid counter name) we update only if -force is specified
            elseif ($Force)
            {
                Write-Verbose -Message "Updating $CurrentUICulture for the Performance Counter with Index $('{0:D4}' -f [int]$($CurrentCulturePerformanceCounterValue.Index)) because -Force was explicit specified"
                $MergedPerformanceCounters[$CurrentCulturePerformanceCounterValue.Index].$CurrentUICulture = $CurrentCulturePerformanceCounterValue.$CurrentUICulture
            }
        }
        #else if it is a new counter
        else
        {
            Write-Verbose -Message "Setting the Performance Counter with Index $('{0:D4}' -f [int]$($CurrentCulturePerformanceCounterValue.Index)) with $CurrentUICulture=$($CurrentCulturePerformanceCounterValue.$CurrentUICulture)"
            $MergedPerformanceCounters[$CurrentCulturePerformanceCounterValue.Index] = $CurrentCulturePerformanceCounterValue
        }
    }
    Write-Host -Object 'PLT Files Merge Complete !'
    return $MergedPerformanceCounters
}

#creting a PLT file for all merged data
function New-PLTFile
{
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory = $True, ValueFromPipeline = $False, ValueFromPipelineByPropertyName = $False)][ValidateNotNullOrEmpty()]
        [hashtable]$PerformanceCounters,
        #Output directory : where to store the generated PLT file (XML format)
        [Parameter(Mandatory = $True, ValueFromPipeline = $False, ValueFromPipelineByPropertyName = $False)][ValidateScript({
                    Test-Path -Path (Split-Path -Path $_ -Parent) -PathType Container
        })]
        [Alias('Path')]
        [String]$OutputDir,

        #Switch to force XML file generation event if the file already exists
        [Switch]$Force
    )
    #Getting the source OS version 
    $OSVersion = [Environment]::OSVersion.Version.Major.ToString()+'.'+[Environment]::OSVersion.Version.Minor.ToString()
    #Hashtable for XMLWriter where the locale is the key and the value if the XML content
    $XMLWriters = @{}
    $Languages = ($PerformanceCounters.Values |
        Get-Member -MemberType NoteProperty |
        Where-Object -FilterScript {
            @('Index', 'en-us') -notcontains $_.Name
    }).Name
    if (-not($Force))
    {
        $ProcessedLanguages = Get-ProcessedLanguage -Directory $OutputDir
        $Languages = $Languages  | Where-Object -FilterScript { $ProcessedLanguages -notcontains $_ }
    }
    #for each non en-us languages
    if ($Languages)
    {
        #For each performance counter data 
        $PerformanceCounters.Values | ForEach-Object `
        -Begin {
            #Before processing the first item in the collection we build an XMLWriter for the current language and store it in the Hashtable
            $Counter = 0
            $Languages | ForEach-Object -Process { 
                #Adding a prefix to identify the source OS
                $XMLWriter = New-Object -TypeName System.XMl.XmlTextWriter -ArgumentList ($(Join-Path -Path $OutputDir -ChildPath "$($_)_PFL_$($OSVersion).xml"), [System.Text.Encoding]::UTF8)
                $XMLWriter.Formatting = [ System.Xml.Formatting]::Indented
                $XMLWriter.Indentation = 1
                $XMLWriter.IndentChar = "`t"
                $XMLWriter.WriteStartDocument()
                #Adding a comment to have the date of the generation
                $XMLWriter.WriteComment("Generated (UTC): $(Get-Date -Format 'U')")
                #Adding a comment to have the OS Version because a same performance counter index may vary with the OS
                $XMLWriter.WriteComment("OS Version : $((Get-WmiObject -Class win32_operatingsystem).caption)")
                $XMLWriter.WriteStartElement('Counters')
                $XMLWriters.Add($_, $XMLWriter)
            }
        } `
        -Process { 
            #For every performance data
            $Counter++
            $PercentComplete = ($Counter/$PerformanceCounters.Count) 
            $CurrentPerformanceCounter = $_
            $Languages | ForEach-Object -Process {
                Write-Progress -Activity "[$Counter/$($PerformanceCounters.Count)] Generating PLT File for $_" -Status ('Processing {0:p0}' -f $PercentComplete) -PercentComplete ($PercentComplete*100)
                #If the counter doesn't exist in the current locale
                if (-not ($CurrentPerformanceCounter.$_))
                {
                    Write-Warning -Message "The Performance counter with index $('{0:D4}' -f [int]$($CurrentPerformanceCounter.Index)) has no value for $($_). It will be skipped"
                }
                #If the counter doesn't exist in the en-us locale
                elseif (-not ($CurrentPerformanceCounter.'en-US'))
                {
                    Write-Warning -Message "The Performance counter with index $('{0:D4}' -f [int]$($CurrentPerformanceCounter.Index)) has no value for en-US. It will be skipped"
                }
                #We generated the XML content for the counter : en-us and current locale matching
                else
                {
                    if ($CurrentPerformanceCounter.$_ -ne $CurrentPerformanceCounter.'en-US')
                    {
                        $XMLWriter = $XMLWriters[$_]
                        $XMLWriter.WriteComment("Counter Id: $($CurrentPerformanceCounter.Index)")
                        $XMLWriter.WriteStartElement('Counter')
                        $XMLWriter.WriteElementString('org',$CurrentPerformanceCounter.$_)
                        $XMLWriter.WriteElementString('en',$CurrentPerformanceCounter.'en-US')
                        $XMLWriter.WriteEndElement()
                    }
                    else
                    {
                        Write-Verbose -Message "[$($CurrentPerformanceCounter.$_)] is an english Perfomance Counter name. It will be skipped"
                    }
                }
            }
        } `
        -End { 
            #After collection processing : We generate the XML files
            $Languages | ForEach-Object -Process {
                #Adding a prefix to identify the source OS
                $CurrentPLTFile = Join-Path -Path $OutputDir -ChildPath "$($_)_PFL_$($OSVersion).xml"
                $XMLWriter = $XMLWriters[$_]
                $XMLWriter.WriteEndElement()
                $XMLWriter.WriteEndDocument()
                $XMLWriter.Flush()
                $XMLWriter.Close()
                Write-Host -Object "The $CurrentPLTFile has been generated"
            }
        }
    }
    else
    {
        if ($Force)
        {
            Write-Verbose -Message "No non-english language found. The PLT file(s) won't be generated"
        }
        else
        {
            Write-Verbose -Message "No new non-english language found. The PLT file(s) won't be generated"
        }
    }
    Write-Progress -Completed -Activity 'PLT Files Generation Complete !'
    Write-Host -Object 'PLT Files Generation Complete !'
}

#CAUTION : Performance Counter Index are not neccessary the same accross the OS. So work with only one OS version at a time
Clear-Host
# Getting the this script path
$CurrentScript = $MyInvocation.MyCommand.Path
# Getting the directory of this script
$CurrentDir = Split-Path -Path $CurrentScript -Parent

#Getting the source OS version 
$OSVersion = [Environment]::OSVersion.Version.Major.ToString()+'.'+[Environment]::OSVersion.Version.Minor.ToString()
#Adding a prefix to identify the source OS
$ExportedCSVFile = $CurrentScript.replace((Get-Item -Path $CurrentScript).Extension, "_$($OSVersion).csv")
#Getting local performance counters
$CurrentCulturePerformanceCounters = Get-PerformanceCounter -FullName $CurrentDir -Verbose #Force
#Getting the current UI culture (ie. : en-US, fr-FR ...)
$CurrentUICulture = ([cultureinfo]::CurrentUICulture).Name
$DifferencesCSVFile = Join-Path -Path $CurrentDir -ChildPath "Differences.csv"


if ($CurrentCulturePerformanceCounters)
{

    #Hashtable for potential performance counters to import
    $ImportedPerformanceCounters = @{}
    #If we have a file for importing performance counters
    if (Test-Path -Path $ExportedCSVFile -PathType Leaf)
    {
        $ImportedPerformanceCounters = Import-PerformanceCounter -FullName $ExportedCSVFile -Verbose
    }

    #If we have no imported performance counters
    if ($ImportedPerformanceCounters.Count -le 0)
    {
        Write-Verbose -Message 'No Imported Performance Counters'
        #Export the performance counters to a CSV file
        Export-PerformanceCounter -Path $ExportedCSVFile -PerformanceCounter $CurrentCulturePerformanceCounters -Verbose
        #Generating PLT File
        New-PLTFile -PerformanceCounter $CurrentCulturePerformanceCounters -OutputDir $CurrentDir -Verbose
    }
    else
    {
        #Merging local and imported performance counter data
        $MergedPerformanceCounters = Merge-PerformanceCounter -ImportedPerformanceCounters $ImportedPerformanceCounters -CurrentCulturePerformanceCounters $CurrentCulturePerformanceCounters -CurrentUICulture $CurrentUICulture -Verbose #-Force 
        #We export the performance counters to a CSV file
        Export-PerformanceCounter -Path $ExportedCSVFile -PerformanceCounter $MergedPerformanceCounters -Verbose
        #Generating PLT File
        New-PLTFile -PerformanceCounter $MergedPerformanceCounters -OutputDir $CurrentDir -Verbose
    }
}
else
{
    Write-Verbose -Message "$CurrentUICulture has already been processed ..."
}

New-PLTLangFile -FullName $CurrentDir -Verbose
$Differences=Compare-PLTFile -FullName $CurrentDir -Verbose | Sort-Object -Property EN | Select-Object -Property * -Unique
$Differences | Select-Object -Property EN, Locale, OS1, Value1, OS2 , Value2 | Export-Csv -Path $DifferencesCSVFile -NoTypeInformation

Laurent.