Skip to content

Commit 17d93dc

Browse files
authored
Warm up PowerShell Worker before receiving function load request (#535)
1 parent d980663 commit 17d93dc

File tree

3 files changed

+62
-26
lines changed

3 files changed

+62
-26
lines changed

src/RequestProcessor.cs

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,12 @@
1919
namespace Microsoft.Azure.Functions.PowerShellWorker
2020
{
2121
using System.Diagnostics;
22-
using System.IO;
2322
using LogLevel = Microsoft.Azure.WebJobs.Script.Grpc.Messages.RpcLog.Types.Level;
2423

2524
internal class RequestProcessor
2625
{
2726
private readonly MessagingStream _msgStream;
27+
private readonly System.Management.Automation.PowerShell _firstPwshInstance;
2828
private readonly PowerShellManagerPool _powershellPool;
2929
private DependencyManager _dependencyManager;
3030

@@ -37,9 +37,10 @@ internal class RequestProcessor
3737
private Dictionary<StreamingMessage.ContentOneofCase, Func<StreamingMessage, StreamingMessage>> _requestHandlers =
3838
new Dictionary<StreamingMessage.ContentOneofCase, Func<StreamingMessage, StreamingMessage>>();
3939

40-
internal RequestProcessor(MessagingStream msgStream)
40+
internal RequestProcessor(MessagingStream msgStream, System.Management.Automation.PowerShell firstPwshInstance)
4141
{
4242
_msgStream = msgStream;
43+
_firstPwshInstance = firstPwshInstance;
4344
_powershellPool = new PowerShellManagerPool(() => new RpcLogger(msgStream));
4445

4546
// Host sends capabilities/init data to worker
@@ -194,18 +195,12 @@ internal StreamingMessage ProcessFunctionLoadRequest(StreamingMessage request)
194195
_dependencyManager = new DependencyManager(request.FunctionLoadRequest.Metadata.Directory, logger: rpcLogger);
195196
var managedDependenciesPath = _dependencyManager.Initialize(request, rpcLogger);
196197

197-
// Setup the FunctionApp root path and module path.
198-
FunctionLoader.SetupWellKnownPaths(functionLoadRequest, managedDependenciesPath);
198+
SetupAppRootPathAndModulePath(functionLoadRequest, managedDependenciesPath);
199199

200-
// Create the very first Runspace so the debugger has the target to attach to.
201-
// This PowerShell instance is shared by the first PowerShellManager instance created in the pool,
202-
// and the dependency manager (used to download dependent modules if needed).
203-
var pwsh = Utils.NewPwshInstance();
204-
LogPowerShellVersion(rpcLogger, pwsh);
205-
_powershellPool.Initialize(pwsh);
200+
_powershellPool.Initialize(_firstPwshInstance);
206201

207202
// Start the download asynchronously if needed.
208-
_dependencyManager.StartDependencyInstallationIfNeeded(request, pwsh, rpcLogger);
203+
_dependencyManager.StartDependencyInstallationIfNeeded(request, _firstPwshInstance, rpcLogger);
209204

210205
rpcLogger.Log(isUserOnlyLog: false, LogLevel.Trace, string.Format(PowerShellWorkerStrings.FirstFunctionLoadCompleted, stopwatch.ElapsedMilliseconds));
211206
}
@@ -493,10 +488,19 @@ private static void BindOutputFromResult(InvocationResponse response, AzFunction
493488
}
494489
}
495490

496-
private static void LogPowerShellVersion(RpcLogger rpcLogger, System.Management.Automation.PowerShell pwsh)
491+
private void SetupAppRootPathAndModulePath(FunctionLoadRequest functionLoadRequest, string managedDependenciesPath)
497492
{
498-
var message = string.Format(PowerShellWorkerStrings.PowerShellVersion, Utils.GetPowerShellVersion(pwsh));
499-
rpcLogger.Log(isUserOnlyLog: false, LogLevel.Information, message);
493+
FunctionLoader.SetupWellKnownPaths(functionLoadRequest, managedDependenciesPath);
494+
495+
if (FunctionLoader.FunctionAppRootPath == null)
496+
{
497+
throw new InvalidOperationException(PowerShellWorkerStrings.FunctionAppRootNotResolved);
498+
}
499+
500+
_firstPwshInstance.AddCommand("Microsoft.PowerShell.Management\\Set-Content")
501+
.AddParameter("Path", "env:PSModulePath")
502+
.AddParameter("Value", FunctionLoader.FunctionModulePath)
503+
.InvokeAndClearCommands();
500504
}
501505

502506
#endregion

src/Utility/Utils.cs

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,6 @@ internal static PowerShell NewPwshInstance()
3737
{
3838
if (s_iss == null)
3939
{
40-
if (FunctionLoader.FunctionAppRootPath == null)
41-
{
42-
throw new InvalidOperationException(PowerShellWorkerStrings.FunctionAppRootNotResolved);
43-
}
44-
4540
s_iss = InitialSessionState.CreateDefault();
4641

4742
if (!AreDurableFunctionsEnabled())
@@ -51,11 +46,14 @@ internal static PowerShell NewPwshInstance()
5146
s_iss.ThreadOptions = PSThreadOptions.UseCurrentThread;
5247
}
5348

54-
s_iss.EnvironmentVariables.Add(
55-
new SessionStateVariableEntry(
56-
"PSModulePath",
57-
FunctionLoader.FunctionModulePath,
58-
description: null));
49+
if (FunctionLoader.FunctionAppRootPath != null)
50+
{
51+
s_iss.EnvironmentVariables.Add(
52+
new SessionStateVariableEntry(
53+
"PSModulePath",
54+
FunctionLoader.FunctionModulePath,
55+
description: null));
56+
}
5957

6058
// Setting the execution policy on macOS and Linux throws an exception so only update it on Windows
6159
if(Platform.IsWindows)

src/Worker.cs

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44
//
55

66
using System;
7+
using System.Management.Automation;
78
using System.Threading.Tasks;
89

910
using CommandLine;
1011
using Microsoft.Azure.Functions.PowerShellWorker.Messaging;
12+
using Microsoft.Azure.Functions.PowerShellWorker.PowerShell;
1113
using Microsoft.Azure.Functions.PowerShellWorker.Utility;
1214
using Microsoft.Azure.WebJobs.Script.Grpc.Messages;
1315

@@ -34,18 +36,50 @@ public async static Task Main(string[] args)
3436
.WithParsed(ops => arguments = ops)
3537
.WithNotParsed(err => Environment.Exit(1));
3638

39+
// Create the very first Runspace so the debugger has the target to attach to.
40+
// This PowerShell instance is shared by the first PowerShellManager instance created in the pool,
41+
// and the dependency manager (used to download dependent modules if needed).
42+
var firstPowerShellInstance = Utils.NewPwshInstance();
43+
LogPowerShellVersion(firstPowerShellInstance);
44+
WarmUpPowerShell(firstPowerShellInstance);
45+
3746
var msgStream = new MessagingStream(arguments.Host, arguments.Port);
38-
var requestProcessor = new RequestProcessor(msgStream);
47+
var requestProcessor = new RequestProcessor(msgStream, firstPowerShellInstance);
3948

4049
// Send StartStream message
41-
var startedMessage = new StreamingMessage() {
50+
var startedMessage = new StreamingMessage()
51+
{
4252
RequestId = arguments.RequestId,
4353
StartStream = new StartStream() { WorkerId = arguments.WorkerId }
4454
};
4555

4656
msgStream.Write(startedMessage);
4757
await requestProcessor.ProcessRequestLoop();
4858
}
59+
60+
// Warm up the PowerShell instance so that the subsequent function load and invocation requests are faster
61+
private static void WarmUpPowerShell(System.Management.Automation.PowerShell firstPowerShellInstance)
62+
{
63+
// It turns out that creating/removing a function warms up the runspace enough.
64+
// We just need this name to be unique, so that it does not coincide with an actual function.
65+
const string DummyFunctionName = "DummyFunction-71b09c92-6bce-42d0-aba1-7b985b8c3563";
66+
67+
firstPowerShellInstance.AddCommand("Microsoft.PowerShell.Management\\New-Item")
68+
.AddParameter("Path", "Function:")
69+
.AddParameter("Name", DummyFunctionName)
70+
.AddParameter("Value", ScriptBlock.Create(string.Empty))
71+
.InvokeAndClearCommands();
72+
73+
firstPowerShellInstance.AddCommand("Microsoft.PowerShell.Management\\Remove-Item")
74+
.AddParameter("Path", $"Function:{DummyFunctionName}")
75+
.InvokeAndClearCommands();
76+
}
77+
78+
private static void LogPowerShellVersion(System.Management.Automation.PowerShell pwsh)
79+
{
80+
var message = string.Format(PowerShellWorkerStrings.PowerShellVersion, Utils.GetPowerShellVersion(pwsh));
81+
RpcLogger.WriteSystemLog(LogLevel.Information, message);
82+
}
4983
}
5084

5185
internal class WorkerArguments

0 commit comments

Comments
 (0)