Skip to content

Instantly share code, notes, and snippets.

@mutaguchi
Last active August 1, 2023 03:27
Show Gist options
  • Save mutaguchi/93438ba53aa769cb4f024e2030d28c69 to your computer and use it in GitHub Desktop.
Save mutaguchi/93438ba53aa769cb4f024e2030d28c69 to your computer and use it in GitHub Desktop.

Mitigating the Impact of PSAMSIMethodInvocationLogging on PowerShell Performance: An Exploration of the Invoke-Method Cmdlet

Method execution in PowerShell 7.3 and later has been slowed down due to PSAMSIMethodInvocationLogging. This feature executes the AMSI's Logging method by specifying the method information and arguments to be executed just before method execution. Originally, this was an experimental feature, but it has been promoted to an official feature.

The problem of slow operation has been partially solved, but still, with Windows Defender's real-time protection enabled, especially when the argument size is large, the execution of methods within a loop is extremely slow. PowerShell/PowerShell#19431

(Measure-Command{$a='x';$b='a'*1000;foreach($i in (1..1000000)){$y=$a.Contains($b)}}).TotalSeconds

This code takes 35 seconds to execute in the author's environment, but completes in 1.17 seconds when real-time protection is turned off. Meanwhile, it completes in 0.72 seconds in Windows PowerShell. Furthermore, if the string length of the argument is increased tenfold, it takes 287 seconds in PowerShell 7, while the time does not change in Windows PowerShell (0.72 seconds).

To avoid this issue, you could turn off real-time protection or revert to Windows PowerShell, but I devised a way to dynamically generate a cmdlet that executes methods via reflection. That is the Invoke-Method cmdlet shown below.

The execution results in PowerShell 7.3.6 are as follows:

Real-time Protection Method Type Argument Length Execution Time (sec)
Off Method Invoke Short 0.8060254
Off Method Invoke Long 1.1832432
Off Cmdlet Invoke Short 6.0721477
Off Cmdlet Invoke Long 6.012772
On Method Invoke Short 10.0729004
On Method Invoke Long 35.6239528
On Cmdlet Invoke Short 6.2883229
On Cmdlet Invoke Long 6.155602

Cmdlet execution is about an order of magnitude slower than method execution, but it might still be better, especially when the argument size is large.

$code = @"
using System;
using System.Management.Automation;
using System.Linq;
using System.Reflection;
namespace Winscript
{
[Cmdlet(VerbsLifecycle.Invoke, "Method")]
public class InvokeMethodCommand : Cmdlet
{
[Parameter(Position = 0, Mandatory = true, ValueFromPipeline = true)]
public PSObject InputObject { get; set; }
[Parameter(Position = 1, Mandatory = true)]
public string Name { get; set; }
[Parameter(Position = 2, Mandatory = false)]
public object[] Arguments { get; set; }
protected override void ProcessRecord()
{
// Get the base object if it's a PSObject
var baseObject = InputObject.BaseObject;
if (Arguments is null)
{
Arguments = new object[0];
}
var method = baseObject.GetType().GetMethod(Name,
Arguments.Select(arg => arg.GetType()).ToArray());
if (method == null)
{
WriteError(new ErrorRecord(
new Exception("Method not found"),
"MethodNotFound",
ErrorCategory.InvalidOperation,
baseObject));
return;
}
try
{
// Invoke the method
var result = method.Invoke(baseObject, Arguments);
// Write the result to the pipeline
WriteObject(result);
}
catch (Exception e)
{
WriteError(new ErrorRecord(
e,
"MethodInvocationFailed",
ErrorCategory.NotSpecified,
baseObject));
}
}
}
}
"@
Import-Module -Assembly (Add-Type -TypeDefinition $code -PassThru).Assembly
$obj = 'x'
$short_arg = 'a' * 100
$long_arg = 'a' * 1000
$range = 1..1000000
Write-Host "method invoke (with short_arg): $((Measure-Command {foreach ($i in $range) {$y=$obj.Contains($short_arg)}}).TotalSeconds) sec"
Write-Host "method invoke (with long_arg): $((Measure-Command {foreach ($i in $range) {$y=$obj.Contains($long_arg)}}).TotalSeconds) sec"
Write-Host "cmdlet invoke (with short_arg): $((Measure-Command {foreach ($i in $range) {$y=Invoke-Method -InputObject $obj -Name Contains -Arguments $short_arg}}).TotalSeconds) sec"
Write-Host "cmdlet invoke (with long_arg): $((Measure-Command {foreach ($i in $range) {$y=Invoke-Method -InputObject $obj -Name Contains -Arguments $long_arg}}).TotalSeconds) sec"
Read-Host Press any key.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment