Skip to content

Conversation

@PaulHigin
Copy link
Contributor

@PaulHigin PaulHigin commented Oct 15, 2018

PR Summary

Implicit remoting is a feature in PowerShell where commands intended to be run on remote machines via Invoke-Command, are wrapped in proxy script commands. That way the remote command can be run by simply typing it on the command line, and the proxy script takes care of invoking it on the remote target.
The Import-PSSession cmdlet creates proxy command scripts for commands or module commands to be run on remote targets.

This is very convenient, but there can be performance problems with proxy commands. If the output of one proxy command is piped to another proxy command, then the data makes an unnecessary round trip to the client and back to the target machine. For example:

# Perf issue.  All data from Get-ProcessProxy is returned to the client and then immediately sent back to remote machine for the Select-ObjectProxy cmdlet.
PS > Get-ProcessProxy | Select-ObjectProxy -Property Name,Id

If there is a lot of data involved, or the data is complex (such as a Process object), then the round tripping of data can take up a significant amount of time.

This experimental feature examines a command typed in the shell, and if all the commands are implicit remoting proxy commands that form a simple pipeline, then the commands are batched together and invoked as a single remote pipeline.

Example:

# Create remote session and import TestIMod module
$s = nsn -host remoteMachine
icm $s { ipmo 'C:\Users\user\Documents\WindowsPowerShell\Modules\TestIMod\TestIMod.psd1' }
Import-PSSession $s -mod testimod

$maxProcs = 1000
$filter = 'pwsh','powershell*','wmi*'

# Without batching, this pipeline takes approximately 12 seconds to run
Measure-Command { Get-AllProcesses -MaxCount $maxProcs | Select-Custom $filter | Group-Stuff $filter }
Days              : 0
Hours             : 0
Minutes           : 0
Seconds           : 12
Milliseconds      : 463

# But with the batching experimental feature enabled, it takes approximately 0.20 seconds
Measure-Command { Get-AllProcesses -MaxCount $maxProcs | Select-Custom $filter | Group-Stuff $filter }
Days              : 0
Hours             : 0
Minutes           : 0
Seconds           : 0
Milliseconds      : 209

As seen above, with the batching experimental feature enabled, all three implicit remoting proxy commands, Get-AllProcesses, Select-Custom, Group-Stuff, run in the remote session and the only data returned to the client is the result from the pipeline. This greatly decreases the amount of data sent back and forth between client and remote session, and also greatly reduces the amount of object serialization and de-serialization.

To enable this feature, create this file as experimental.json

            {
                "ExperimentalFeatures": [
		  "PSImplicitRemotingBatching"
                ]
            }

Then start PowerShell with: pwsh -settingsfile ./experimental.json

PR Checklist

@PaulHigin PaulHigin added the WG-Remoting PSRP issues with any transport layer label Oct 15, 2018
Copy link
Member

@daxian-dbw daxian-dbw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two quick comments from a glance. Will look more closely later.


// Experimental:
// Check for implicit remoting commands that can be batched, and execute as batched if able.
if (ExperimentalFeature.EnabledExperimentalFeatureNames.TryGetValue("PSImplicitRemotingBatching", out string actualValue))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use ExperimentalFeature.IsEnabled("PSImplicitRemotingBatching")

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we collect all constants like "PSImplicitRemotingBatching" for all code base in one place?

new ExperimentalFeature(name: "PSImplicitRemotingBatching",
description: "Batch implicit remoting proxy commands to improve performance",
source: "EngineSource",
isEnabled: true)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use isEnabled: false here. When the feature is enabled in powershell.config.json, the feature will be shown as Enabled.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do.

return AstVisitAction.Continue;
}

/*
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove commented block.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is experimental, I want to leave it in for now. We may need some kind of script block restriction.

foreach (var cmdInfo in cmdInfoList)
{
// Check for allowed command
var aliasInfo = cmdInfo as AliasInfo;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about the following:

string cmdName = (cmdInfo is AliasInfo aliasInfo) ? aliasInfo.ReferencedCommand.Name : cmdInfo.Name;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Thanks.

continue;
}

if (cmdInfo.Module == null || string.IsNullOrEmpty(cmdInfo.ModuleName))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

== null and string.IsNullOrEmpty check seems redundant.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually Module and ModuleName properties seem redundant to me. I am checking both properties to be safe.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, misread it as ModuleName twice.

break;
}

var privateData = cmdInfo.Module.PrivateData as System.Collections.Hashtable;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar pattern as above:

if (cmdInfo.Module.PrivateData is System.Collections.Hashtable privateData)
{
    success = false;
    break;
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed.

if (psSession == null || ps.Streams.Error.Count > 0)
{
// Cannot continue
throw new PSInvalidOperationException();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need a better exception message here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will add.

@adityapatwardhan
Copy link
Member

@PaulHigin Can we add some tests here as well?

@PaulHigin
Copy link
Contributor Author

Since this is a perf improvement for special command lines, I don't see a good way to add reliable tests.

@PaulHigin
Copy link
Contributor Author

Actually, offline we decided that adding a test hook to test the TryRunAsImplicitBatch() method should be sufficient to test the code path. I'll work on adding that.

@mattpwhite
Copy link

Does this avoid the intermediate serialization step entirely? If so, then it’s more than a perf improvement, it also enables things that are currently problematic. In many cases a dead serialized property bag isn’t valid pipeline input, but a live object is. So Get | Set scenarios that didn’t work with implicit remoting might start to work.

Copy link
Collaborator

@iSazonov iSazonov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will the feature work if remote point is Windows PowerShell or previous PowerShell Core version?

{
psSessionId = new Guid(privateData["ImplicitSessionId"] as string);
}
else if (!psSessionId.ToString().Equals(privateData["ImplicitSessionId"] as string, StringComparison.OrdinalIgnoreCase))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we replace the string constant with nameof()?

// If this is just a single command, there is no point in batching it
if (checker.Commands.Count < 2)
{
throw new ImplicitRemotingBatchingNotSupportedException(ConsoleHostStrings.ImplicitBatchSingleCommand);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need the exception? Can we return false?

More general question: why we throw exceptions in "Try" method (TryRunAsImplicitBatch)? Why we have exceptionThrown out parameter?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At one point I was thinking of emitting compatibility information through tracing, and do that with exceptions. But since this is such a rare condition it doesn't seem worth it. I'll remove them as needed.

success = false;
break;
}
if (!privateData.ContainsKey("ImplicitSessionId"))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could use TryGet().

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually hash table doesn't support TryGet().

}
if (psSessionId == Guid.Empty)
{
psSessionId = new Guid(privateData["ImplicitSessionId"] as string);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why ImplicitSessionId is not Guid type?

@PaulHigin
Copy link
Contributor Author

@iSazonov Yes, batching will work for any remote target that supports implicit remoting.

@PaulHigin PaulHigin requested a review from TravisEz13 as a code owner October 16, 2018 22:28
private const int MaxModuleNestingDepth = 10;

/// <summary>
/// True when an implicit remoting module is loaded
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment should start with "Gets and sets" and has final dot.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do.

}

/// <summary>
/// TestImplicitRemotingBatching
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add more useful summary and final dot.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do.

@PaulHigin
Copy link
Contributor Author

PaulHigin commented Oct 17, 2018

@mattpwhite , Whoops missed this comment. Yes, batching the pipeline means "live" objects are passed and implicit commands that didn't work with de-serialized objects will likely work as batched.

@TravisEz13
Copy link
Member

@adityapatwardhan Can you update your review?

@SteveL-MSFT SteveL-MSFT added the Experimental Experimental Feature label Oct 24, 2018
*/
new ExperimentalFeature(name: "PSImplicitRemotingBatching",
description: "Batch implicit remoting proxy commands to improve performance",
source: "EngineSource",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be EngineSource without quotes as it's a const string defined on line 22 of this file

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do.

},
@{
Name = 'Two implicit commands with Where-Object should be successfully batched'
CommandLine = 'Get-Process | Write-Output | Where-Object { $_.Name -like "*pwsh*" }'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about aliases like ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know what you mean. If you have a scenario then let me know. But I don't intend to cover all scenarios on this initial check in. This is an experimental feature that will evolve over time.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe you have code above to see if the command is an alias and convert it to the cmdlet. So you should just add a test variation where:

CommandLine = 'Get-Process | Write-Output | ? { $_.Name -like "*pwsh*" }'

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we'll use the alias we should add a comment to exclude a unneeded change later.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I understand now. Sure, I can add that test case.

@PaulHigin
Copy link
Contributor Author

@adityapatwardhan Can you please merge these approved changes? We have users who would like to try out the experimental feature.

@adityapatwardhan adityapatwardhan merged commit 20f3a6a into PowerShell:master Oct 30, 2018
@PaulHigin PaulHigin deleted the implicit_remoting_batching branch October 30, 2018 17:50
@PaulHigin
Copy link
Contributor Author

@adityapatwardhan Thanks!

@iSazonov
Copy link
Collaborator

It will be nice to share config file where the feature is turned on.

@SteveL-MSFT
Copy link
Member

@iSazonov I created new issue to improve experimental features experience #8155

@iSazonov
Copy link
Collaborator

@SteveL-MSFT Thanks!

I meant that attaching a sample file to the PR will help users.

@SteveL-MSFT
Copy link
Member

@iSazonov added example to the PR description

@jpsnover
Copy link
Contributor

jpsnover commented Oct 31, 2018

Something is wrong here.

PS C:\temp\PowerShell-6.2.0-preview.859-win-x64> Get-ExperimentalFeature

Name                                Enabled Source                              Description
----                                ------- ------                              -----------
PSImplicitRemotingBatching             True PSEngine                            Batch implicit remoting proxy comman...


PS C:\temp\PowerShell-6.2.0-preview.859-win-x64> Import-PSSession $s -CommandName get-Process -Prefix r -verbose -AllowClobber

ModuleType Version    Name                                ExportedCommands
---------- -------    ----                                ----------------
Script     1.0        tmp_a4ox23yj.min                    Get-rProcess


PS C:\temp\PowerShell-6.2.0-preview.859-win-x64> get-RProcess |where Name -like *ss
PS C:\temp\PowerShell-6.2.0-preview.859-win-x64> $test = get-RProcess
PS C:\temp\PowerShell-6.2.0-preview.859-win-x64> $test |where Name -like *ss

 NPM(K)    PM(M)      WS(M)     CPU(s)      Id  SI ProcessName
 ------    -----      -----     ------      --  -- -----------
     25     1.90       1.86       7.20     752   0 csrss
     32    11.32       5.30      72.31     868   1 csrss
     85    19.81      24.61     935.81     104   0 lsass
      3     0.57       0.62       0.27     416   0 smss


PS C:\temp\PowerShell-6.2.0-preview.859-win-x64>

@PaulHigin
Copy link
Contributor Author

You need to enable the feature via the powershell.config.json file.

PS > Get-ExperimentalFeature

Name                                Enabled Source                              Description
----                                ------- ------                              -----------
PSImplicitRemotingBatching             True PSEngine                            Batch implicit remoting proxy comman...

I am not sure about applying a prefix to the proxy command, it seems like that should be Ok. But the other two involving assignment and piping from a variable fail the "simple pipeline" checker used to determine if batching is allowed.

I am adding a verbose output that reports the checker errors so it is easier to determine if the command line is batched or not, and make it easier to address these scenarios.

@PaulHigin
Copy link
Contributor Author

Whoops, left out the powershell.config.json example:

{
    "Microsoft.PowerShell:ExecutionPolicy":"RemoteSigned",
    "ExperimentalFeatures": [
      "PSImplicitRemotingBatching"
    ]
}

@jpsnover
Copy link
Contributor

jpsnover commented Nov 1, 2018

Sorry that the formatting got screwed up but if you look at my post, the very first command confirms that the feature is enabled.

@SteveL-MSFT
Copy link
Member

SteveL-MSFT commented Nov 1, 2018

@jpsnover I reformatted your comment by wrapping in:

    ```powershell
    <powershell stuff here>
    ```

Looks like get-RProcess |where Name -like *ss failed to return anything. I just build master which has @PaulHigin's commit and works for me:

PS /Users/steve/repos/PowerShell> Get-rProcess | where name -like "*pwsh*"  

 NPM(K)    PM(M)      WS(M)     CPU(s)      Id  SI ProcessName
 ------    -----      -----     ------      --  -- -----------
     50    55.58      84.09      32.27    3728   0 pwsh
     63    49.50      96.32       2.48   10152   2 pwsh
     61    46.90      92.68       2.36   14056   2 pwsh

@PaulHigin
Copy link
Contributor Author

Turning on verbose output helps to see what is going on:

$VerbosePreference = "Continue"
Get-rProcess | where Name -like *ss
VERBOSE: Implicit remoting command pipeline has been batched for execution on remote target.

This was batched successfully, however, Get-rProcess proxy command is not on the target machine. I forgot to merge error stream so this error is not showing. It is easy to fix the error stream merge, but I have to think about how to convert Get-rProcess proxy wrapper command to the name of the actual wrapped command.

$test = Get-RProcess
$test | where Name -like *ss

Neither of these are pipelines that round trip data and don't need to be batched.

However,

Import-PSSession $s -CommandName Get-Process -AllowClobber
$VerbosePreference = "Continue"

Get-Process | where Name -like *ss
VERBOSE: Implicit remoting command pipeline has been batched for execution on remote target.

$test = Get-Process | where Name -like *ss
VERBOSE: Command is not a simple pipeline and cannot be batched.

100 | Get-Process | where Name -like *ss
VERBOSE: Command pipeline not supported for implicit remoting batching. : PipelineStartingWithExpressionNotSupported

The last two lines are not being batched because they do not pass the simple pipeline check. They are more complex and require parsing the AST and re-building complex commands, but I think they should be supported.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Experimental Experimental Feature WG-Remoting PSRP issues with any transport layer

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants