Repository navigation
Closed
Labels
First-Time-IssueEasy issues first time contributors can work on to learn about this projectEasy issues first time contributors can work on to learn about this projectIssue-Questionideally support can be provided via other mechanisms, but sometimes folks do open an issue to get aideally support can be provided via other mechanisms, but sometimes folks do open an issue to get aResolution-FixedThe issue is fixed.The issue is fixed.Up-for-GrabsUp-for-grabs issues are not high priorities, and may be opportunities for external contributorsUp-for-grabs issues are not high priorities, and may be opportunities for external contributorsWG-Cmdletsgeneral cmdlet issuesgeneral cmdlet issuesWG-Engine-Performancecore PowerShell engine, interpreter, and runtime performancecore PowerShell engine, interpreter, and runtime performance
Description
I'm calling an API repeatedly (500,000 times) and I noticed it runs 4-5x faster on PowerShell 5. The code below is not the actual API, but a minimal example that demos the issue.
Steps to reproduce
I'm running this on 2 different "machines":
- PowerShell 5 and 6 on Windows 10 (physical)
- PowerShell 5 and 7 on Windows Server (VM)
The results are pretty consistent. I noticed PowerShell 6 or 7 for Mac was really slow -- that's why I started benchmarking.
You can adjust $max on your machine to get a more reasonable run duration for comparison.
| PowerShell Version | Duration (sec.) |
|---|---|
| 5 | 1 |
| 6 | 4 |
| 7 | 5 |
<#
Ver. Sec
---- ---
5 1
6 4
7 5
#>
$max = 10
$start = Get-Date -Format "HH:mm:ss"
foreach ($i in 1..$max) {
Invoke-RestMethod "https://postman-echo.com/get?i=$i"
}
$end = Get-Date -Format "HH:mm:ss"
write-host "started at" $start
write-host " ended at" $endReacted by Herr-Sepp, Markus Blaschke and on-the-ray
Metadata
Metadata
Assignees
Labels
First-Time-IssueEasy issues first time contributors can work on to learn about this projectEasy issues first time contributors can work on to learn about this projectIssue-Questionideally support can be provided via other mechanisms, but sometimes folks do open an issue to get aideally support can be provided via other mechanisms, but sometimes folks do open an issue to get aResolution-FixedThe issue is fixed.The issue is fixed.Up-for-GrabsUp-for-grabs issues are not high priorities, and may be opportunities for external contributorsUp-for-grabs issues are not high priorities, and may be opportunities for external contributorsWG-Cmdletsgeneral cmdlet issuesgeneral cmdlet issuesWG-Engine-Performancecore PowerShell engine, interpreter, and runtime performancecore PowerShell engine, interpreter, and runtime performance
Activity
NoMoreFood commentedon May 29, 2020
Unless somebody is aware of why this might be, I'm willing to take a look.
NoMoreFood commentedon May 30, 2020
PowerShell 6/7 seems to use the HttpClient class which takes about 5-7 seconds of my system under PowerShell 7.0.1 or Windows PowerShell 5:
Windows PowerShell uses System.Net.WebRequest; this request completes in about 2-3 seconds on my system under Windows PowerShell:
Interestingly, this latter code also runs much 2x-3x slower under PowerShell 6/7 compared to Windows PowerShell. This kind of surprised me.
So the question may actually be three-fold: 1) Why the switch to HttpClient? This is not an obvious problem at first glance... just a curiosity. 2) Why is performance so much different between HttpClient and WebRequest under Windows PowerShell, and 3) Why the slowdown in WebRequest from Windows PowerShell to PowerShell Core (may be an underlying DotNet Core question).
vexx32 commentedon May 30, 2020
WebClient was simply nonexistent in .NET Core during pretty much all of the development of 6.x, and we needed web cmdlets to work with. Even now, it's only really been brought back in for compatibility and isn't recommended for new code as I understand it.
Nor sure about much beyond that, though. 🙂
xtqqczze commentedon May 31, 2020
PowerShell 5 is using a single persistent TCP connection, PowerShell 7 is opening a new connection for each HTTP request.
If the
-DisableKeepAliveparameter is used withInvoke-RestMethodthere is no difference in performance.xtqqczze commentedon May 31, 2020
Bryan Berns (@NoMoreFood) If you set
$Request.KeepAlive = $falsein your example the performance is also the same.xtqqczze commentedon May 31, 2020
PowerShell 7 actually sends the
Connection: Keep-Aliveheader, but than opens a new connection for every request.xtqqczze commentedon May 31, 2020
gabrielsroka commentedon May 31, 2020
That's really good info! Thank you.
I did a little more digging and here's what I found. Node.js (using
node-fetch) and Python (2/3, usingrequests) are also very slow, but JS in Chrome is as fast as PowerShell 5. I also used Wireshark, and noticed that the slow ones seem to negotiate HTTPS each time, but the fast ones do it only once. If you try it using HTTP instead of HTTPS, it's much faster. Unfortunately, the code I have to run requires HTTPS. hth.Python without connection pooling -- see below for an example using connection pooling
xtqqczze commentedon Jun 1, 2020
It appears that
TCP KeepAliveHTTP persistent connections are supported in certain circumstances. If a singularHttpClientis declared and reused for each request, TCP connection reuse occurs and performance is much better, especially when using HTTPS:Edited to correct conflation between TCP KeepAlive and HTTP KeepAlive (persistent connections). (Thanks Cory Nelson (@scalablecory))
scalablecory commentedon Jun 4, 2020
The problem here is that the
HttpClientneeds to be reused between calls ofInvoke-RestMethod. This enables connection pooling.Note that this is not TCP Keepalive (a technology to detect broken idle connections), and the two issues linked above will not help to resolve this.
xtqqczze commentedon Jun 4, 2020
So the the difference in perfomance is due the fact Windows PowerShell 5 pools connections between seperate instances of
[System.Net.WebRequest], and PowerShell 7 does not.As far as I can see, the only way to ensure use of a persistent connection (in PowerShell 7) is to use a raw
HttpClientinstead of the cmdletsInvoke-RestMethod/Invoke-WebRequest. Perhaps we could open a new issue to discuss whether to implement this functionality.gabrielsroka commentedon Jul 5, 2020
I guess I was only partially correct above. Python
requestshas aSession()object which allows for connection pooling. This only takes about 1 second as opposed to about 4 seconds for the example in my earlier comment (which doesn't use connection pooling).iSazonov commentedon Jul 6, 2020
HttpClient creates SocketsHttpHandler where connection pool is implemented.
If we will share the HttpClient or SocketsHttpHandler per process how do we resolve config conflicts between runspaces?
It seems we can not utilize a global pool.
Perhaps we could add new parameter in web cmdlets to get shared SocketsHttpHandler.We could enhance
WebRequestSessionwithSocketsHttpHandler.43 remaining items
iSazonov commentedon Feb 5, 2023
Perhaps there are unexpected obstacles there, but I don't see them now. Most likely the changes will be minimal.
stevenebutler commentedon Feb 5, 2023
stevenebutler commentedon Feb 5, 2023
I have made changes to detect when the WebSession has changed between invocations on my branch here. Please have a look and let me know if this approach is likely to work.
master...stevenebutler:PowerShell:12764-support-persistent-http-connections-in-cmdlets
iSazonov commentedon Feb 6, 2023
Looking at this code I think it is too confusing. We need to do a refactor first. My main idea is based on the fact that both handler and HttpClient can't be changed after the start. That means there is no point in calling the GetHttpClient() method on the fly. In other words, all initializations should be in BeginProcessing() and the cmdlets could be IDisposable. I hope it possible.
Perhaps CarloToso knows the code better and could refactor it.
stevenebutler commentedon Feb 6, 2023
CarloToso commentedon Feb 12, 2023
stevenebutler I tested your code and the speedup was impressive, please keep working on it and open a PR.
It also passes all the WebCmdlets tests.
Test:
Ps 7.3.2 --> 5538 milliseconds
your code --> 468 milliseconds
God-damnit-all commentedon Feb 13, 2023
Don't give up! That's just one person's opinion. The speed improvement is seriously impressive, I would really like to see a PR of this too.
stevenebutler commentedon Feb 14, 2023
To keep working on it I'd need to have some idea of what needs to be done to make it acceptable to merge.
I can start that process through a PR if that is the recommended course of action or if there's some improvements that should be made first, then I can try that first. I would assume at minimum it would need to merge with changes in #19109
God-damnit-all commentedon Feb 14, 2023
Best way to find out is to submit what you have as-is and see what they say about it.
CarloToso commentedon Feb 14, 2023
stevenebutler You should merge with changes in #19109 and open a [W.I.P] PR, then continue working on it there.
stevenebutler commentedon Feb 19, 2023
@ImportTaste responding to your query on my repo that got lost when I force pushed some changes...
I wouldn't be able to make this an exteranl cmdlet without copying most of the existing code base into the third party module and I'm not really willing to do that.
The reason you'd have to do that is it depends on a lot of internal interfaces that an external module can't access. It also adds internal properties to the WebRequestSession you'd have to do externally as well.
If you're desperate for an interim solution you can write your own simplified PowerShell script that uses httpclienthandler/httpclient directly that supports only what you need. That is what I have been doing but it was relatively simple since I don't need 95% of the features on IWR.