PowerShell İşlem Bellek Aracı Oluşturma


Bu hafta, Firefox'un yerine geçebilecek yeni bir tarayıcı olan Brave'i test ediyorum. Chrome'dan Firefox'a geçmemin nedenlerinden biri performans ve daha iyi kaynak kullanımıydı. Cesur artık daha iyi bir seçim olabilir ancak bu makalenin konusu bu değil. Kaynak kullanımını değerlendirmek için Get-Process PowerShell cmdlet'ine başvurdum. Bazı çok spesifik sonuçlar elde etmem gerektiğini fark ettiğim için süreci basitleştirmek amacıyla bir PowerShell işlevi yazmaya karar verdim. Bu nedenle sizin iş ihtiyaçlarınızı çözmek için kendi araçlarınızı oluşturmak üzere PowerShell'i öğrenmeli ve kullanmalısınız. İşte gelişim sürecimle ilgili bazı hikayeler.

Çalışma kümesi özelliğini kullanarak işlem bilgilerini görüntülemek için ilk olarak temel bir PowerShell ifadesiyle başladım.

Get-Process brave | Measure-Object Workingset -sum -average | 
Select-object Count,Sum,Average

Benzer sonuçları görmek için krom gibi istediğiniz herhangi bir işlemi değiştirin. Bu basit ifade, tüm cesur süreçleri alır ve bunları çalışma seti özelliğinin toplamını ve ortalamasını hesaplayan Measure-Object'e yönlendirir. Özellikle Firefox ile karşılaştırma yapmayı düşündüğüm için çıktıda işlem adını görmek faydalı olacaktır. Bunu yaparsam:

Get-Process brave,firefox | Measure-Object Workingset -sum -average | 
Select-object Count,Sum,Average

Her iki tarayıcının toplamını alıyorum. Hayır. Her işlem için Get-Process ve Measure-Object'i çalıştırmam gerekir. Bunun bir yolu ForEach-Object'i kullanmak olacaktır.

"brave","firefox" | foreach-object {
Get-Process -name $_ -PipelineVariable pv | 
Measure-Object Workingset -sum -average | 
Select-object @{Name="Name";Expression = {$pv.name}},
Count,Sum,Average
}

Bu daha iyi. İşlem adını almak için ortak PipelineVariable parametresini kullanıyorum. Measure-Object'in çıktısı Get-Process'teki işlem adını içermiyor, bu nedenle Select-Object'in çalışması için boru hattının daha önceki bir kısmına erişmenin bir yoluna ihtiyacım var. PipelineVariable'ın başardığı şey budur.

Seni bilmem ama ben tüm bu baytları kolayca daha anlamlı bir şeye dönüştüremem. Ancak PowerShell bunu yapabilir.

"brave","firefox" | foreach-object {
Get-Process -name $_ -PipelineVariable pv | 
Measure-Object Workingset -sum -average | 
Select-object @{Name="Name";Expression = {$pv.name}},
Count,
@{Name="SumMB";Expression = {[math]::round($_.Sum/1MB,2)}},
@{Name="AvgMB";Expression = {[math]::round($_.Average/1MB,2)}}
}

Artık ihtiyacım olan dostane sonucu üreten kodum var.

Ancak bunu esnek ve yeniden kullanılabilir hale getirmem gerekiyor. İşte bu noktada işlevler devreye giriyor. Bunu hızla bir fonksiyona dönüştürebilirim.

Function Get-MyProcess {
[cmdletbinding()]
Param([string[]]$Name)

$Name | foreach-object {
Get-Process -name $_ -PipelineVariable pv | 
Measure-Object Workingset -sum -average | 
Select-object @{Name="Name";Expression = {$pv.name}},
Count,
@{Name="SumMB";Expression = {[math]::round($_.Sum/1MB,2)}},
@{Name="AvgMB";Expression = {[math]::round($_.Average/1MB,2)}}
}

}

Ve çalışıyor.

Bu noktada bunun tamamlanmış olduğunu düşünebilirim. Ancak PowerShell işlevlerini yazdığımda, özellikle de bir şey değer veya veri alırken, genellikle bunun uzak bilgisayarlarla çalışmasını sağlamak isterim. İlk düşüncem, Get-Process'in -Computername parametresine sahip olması nedeniyle tek yapmam gereken onu fonksiyonuma eklemek ve kodda iletmekti. Bu yaklaşımın potansiyel dezavantajı, uzak bir bilgisayara bu şekilde bağlandığınızda eski uzaktan bağlantı bağlantıları üzerinden bağlanıyor olmanızdır. Mümkün olduğunca eski bağlantıları kullanmaktan kaçınmaya çalışıyorum, bu da PowerShell uzaktan iletişiminden yararlanmam gerektiği anlamına geliyor. Çözümlerden biri Get-Process komutumu bir Invoke-Command ifadesinin içine sarmak olabilir.

Function Get-MyProcess {
[cmdletbinding()]
Param([string[]]$Name,[string]$Computername = $env:computername)

Invoke-Command -ScriptBlock {
$using:Name | foreach-object {
Get-Process -name $_ -PipelineVariable pv | 
Measure-Object Workingset -sum -average | 
Select-object @{Name="Name";Expression = {$pv.name}},
Count,
@{Name="SumMB";Expression = {[math]::round($_.Sum/1MB,2)}},
@{Name="AvgMB";Expression = {[math]::round($_.Average/1MB,2)}}
}
} -ComputerName $computername
}

Bu, olgun bir PowerShell işlevine yaklaşıyor.

Gördüğünüz gibi bu fonksiyon herhangi bir hata işlemeyi içermiyor. RunspaceID özelliğini hiçbir zaman görmem gerekmediği için, işlevimi kaldırmak için her zaman başka bir PowerShell ifadesine sarmam gerekecekti. Fonksiyonun boru hattına sadece görmek istediğim değerleri içeren bir nesne yazması daha iyi olurdu. Diğer olası dezavantaj ise toplam ve ortalama değerlerimin MB cinsinden biçimlendirilmiş olmasıdır. Bu fonksiyonun esnekliğini sınırlayan bir varsayımda bulundum. Başlangıçta tembeldim. Daha iyi yaklaşım, ardışık düzene ham, biçimlendirilmemiş verilerle bir nesne yazmaktır.

Fonksiyonu revize ettikçe bu, scriptblock'un özü haline geldi:

foreach ($item in $ProcessName) {
    Get-Process -Name $item -PipelineVariable pv -OutVariable ov |
        Measure-Object -Property WorkingSet -Sum -Average |
        Select-Object -Property @{Name = "Name"; Expression = {$pv.name}},
    Count, 
    @{Name = "Threads"; Expression = {$ov.threads.count}},
    Average, Sum,
    @{Name = "Computername"; Expression = {$env:computername}} 
}

Çıkış değerleri artık baytlara geri döndü. Ama biliyorsunuz, bunların varsayılan olarak MB olarak biçimlendirilmiş olmasını gerçekten isterim. PowerShell bunu yapabilir ve eğer gerçekte bunu her zaman yapıyorsa. Get-Process'i çalıştırdığınızda varsayılan ekran sizin için biçimlendirilir. Gördüğünüz şey, temeldeki süreç nesnelerinin ham değeri değildir. PowerShell bir format yönergesi kullanıyor. Sen de yapabilirsin.

Öncelikle çıktı nesnenize bir tür adı vermeniz gerekir. PowerShell sınıfını kullanarak bir işlev oluşturabilirdim. Ancak bir tür adı eklemek de aynı derecede kolaydır.

Invoke-Command @PSBoundParameters | Select-Object -Property * -ExcludeProperty RunspaceID,PS* |
ForEach-Object {
    #insert a custom type name for the format directive
    $_.psobject.typenames.insert(0, "myProcessMemory") | Out-Null
    $_
}

Şimdi işin zor kısmı. Biçim yönergeleri, .ps1xml dosyasıyla kaydedilen özel XML dosyalarıdır. Yapabileceğim şeylerden biri özel bir başlık ve değer tanımlamaktır.

<TableColumnItem>
    <ScriptBlock>[math]::Round($_.average/1MB,4)</ScriptBlock>
</TableColumnItem>

Varsayılan çıktı, biçimlendirme yönergelerini kullanır ancak ben yine de temeldeki gerçek özellik adlarını kullanabilirim.

Bu arada, Firefox'un çok sayıda uzantısı ve Brave'in de çok sayıda açık sekmesi olduğundan, bu sonuçları çok fazla okumayın.

Ayrı bir .ps1xml dosyası oluşturmak yerine, fonksiyonumla birlikte içeriği betik dosyama bir burada dizesi olarak ekledim ve daha sonra Update-FormatData kullanılarak içe aktarılan geçici bir dosya oluşturdum. Dosyanın tamamı Github'da.

Get-ProcessMemory.ps1:

Function Get-ProcessMemory {
<#
.SYNOPSIS
Get a snapshot of a process' memory usage.
.DESCRIPTION
Get a snapshot of a process' memory usage based on its workingset value. You can get the same information using Get-Process or by querying the Win32_Process WMI class with Get-CimInstance. This command uses Invoke-Command to gather the information remotely. Many of the parameters are from that cmdlet.

.EXAMPLE
PS C:\> get-processmemory code,powershell*

Name                   Count Threads      AvgMB      SumMB      Computername
----                   ----- -------      -----      -----      ------------
Code                       9     154   112.8199  1015.3789         BOVINE320
powershell                 3      77   179.1367   537.4102         BOVINE320
powershell_ise             1      21   242.0586   242.0586         BOVINE320


The default output displays the process name, the total process count, the total number of threads, the average workingset value per process in MB and the total workingset size of all processes also formatted in MB.
.EXAMPLE
PS C:\> get-processmemory -computername srv1,srv2,dom1 -Name lsass -cred company\artd

Name                   Count Threads      AvgMB      SumMB     Computername
----                   ----- -------      -----      -----     ------------
lsass                      1      30    60.1719    60.1719             DOM1
lsass                      1       7    10.8594    10.8594             SRV1
lsass                      1       7     9.6953     9.6953             SRV2
.EXAMPLE
PS C:\> get-processmemory *ActiveDirectory* -Computername dom1 | select-object *

Name         : Microsoft.ActiveDirectory.WebServices
Count        : 1
Threads      : 10
Average      : 46940160
Sum          : 46940160
Computername : DOM1

This example uses a wildcard for the process name because the domain controller only has one related process. The output shows the raw values.
.EXAMPLE
PS C:\> get-processmemory *edge*,firefox,chrome | sort sum -Descending

Name                   Count Threads      AvgMB      SumMB     Computername
----                   ----- -------      -----      -----     ------------
firefox                    6     189   180.4134  1082.4805        BOVINE320
chrome                     8     124    67.1377   537.1016        BOVINE320
MicrosoftEdgeCP            4     171    66.1846   264.7383        BOVINE320
MicrosoftEdge              1      37    70.1367    70.1367        BOVINE320
MicrosoftEdgeSH            1      10     8.2734     8.2734        BOVINE320

Get browser processes and sort on the underlying SUM property in descending order.
.NOTES
    Learn more about PowerShell: http://jdhitsolutions.com/blog/essential-powershell-resources/
.LINK
Get-Process
Invoke-Command
.INPUTS
[string[]]
#>

    [CmdletBinding()]
    [OutputType("myProcessMemory")]

    Param
    (
        [Parameter(Mandatory, ValueFromPipeline, Position = 0)]
        [string[]]$Name,
        [ValidateNotNullOrEmpty()]
        [string[]]$Computername = $env:COMPUTERNAME,
        [PSCredential]$Credential,
        [switch]$UseSSL,
        [Int32]$ThrottleLimit,
        [ValidateSet('Default', 'Basic', 'Credssp', 'Digest', 'Kerberos', 'Negotiate', 'NegotiateWithImplicitCredential')]
        [ValidateNotNullorEmpty()]
        [string]$Authentication = "default"
    )

    Begin {

        Write-Verbose "Starting $($MyInvocation.Mycommand)"

        $sb = {
            Param([string[]]$ProcessName)

            # a process might have multiple instances so get each one by name
            #group the processes to accomodate the use of wildcards
            $data = Get-Process -Name $ProcessName | Group-Object -Property Name
            foreach ($item in $data) {
                $item.group | Measure-Object -Property WorkingSet -Sum -Average |
                    Select-Object -Property @{Name = "Name"; Expression = {$item.name}},
                Count,
                @{Name = "Threads"; Expression = {$item.group.threads.count}},
                Average, Sum,
                @{Name = "Computername"; Expression = {$env:computername}}
            }
        } #close ScriptBlock

        #update PSBoundparamters so it can be splatted to Invoke-Command
        $PSBoundParameters.Add("ScriptBlock", $sb) | Out-Null
        $PSBoundParameters.add("HideComputername", $True) | Out-Null

    } #begin

    Process {

        $PSBoundParameters.Remove("Name") | Out-Null
        Write-Verbose "Querying processes $($name -join ',') on $($Computername -join ',')"

        #need to make sure argument is treated as an array
        $PSBoundParameters.ArgumentList = , @($Name)
        if (-Not $PSBoundParameters.ContainsKey("Computername")) {
            #add the default value if nothing was specified
            $PSBoundParameters.Add("Computername", $Computername) | Out-Null
        }
        $PSBoundParameters | Out-String | Write-Verbose

        Invoke-Command @PSBoundParameters | Select-Object -Property * -ExcludeProperty RunspaceID, PS* |
            ForEach-Object {
            #insert a custom type name for the format directive
            $_.psobject.typenames.insert(0, "myProcessMemory") | Out-Null
            $_
        }

    } #process

    End {

        Write-Verbose "Ending $($MyInvocation.Mycommand)"

    } #end

} #close function

#region formatting directives for the custom object
[xml]$format = @'


    
        
            default
            
                myProcessMemory
            
            
            
                
                    
                        
                        20
                        left
                    
                    
                        
                        7
                        right
                    
                     
                        
                        7
                        right
                    
                    
                        
                        10
                        right
                    
                    
                        
                        10
                        right
                    
                    
                        
                        16
                        right
                    
                 
                
                    
                        
                            
                                Name
                             TableColumnIt
                            
                                Count
                             TableColumnIt
                            
                                Threads
                             TableColumnIt
                            
                                [math]::Round($_.average/1MB,4)
                             TableColumnIt
                            
                                [math]::Round($_.sum/1MB,4)
                             TableColumnIt
                            
                                Computername
                             TableColumnIt
                        
                    
                
            
        
    

'@

$format.Save("$env:temp\myProcessMemory.format.ps1xml")

Update-FormatData -AppendPath $env:temp\myProcessMemory.format.ps1xml

#endregion

Evet, bunun geliştirilmesi biraz zaman aldı ancak artık yeniden kullanılabilir ve esnek bir PowerShell işlevim var. ve gelecekteki işlevler için kullanabileceğim bir model. Nihai ürün kadar geliştirme sürecini de görmenin faydalı olduğunu düşünüyorum. Geri bildiriminiz veya sorularınız varsa, lütfen yorum bırakmaktan çekinmeyin. Eğlence!

Güncelleme 2 Ocak 2019

Joker karakterlerin kullanımını daha iyi ele almak için Github özetindeki kodu güncelledim. Ayrıca biçimlendirme yönergelerini de değiştirdim.

İlgili makaleler