Skip to content

Avoid invoking run.ps1 and port helper module to C# to improve throughput performance #214

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
May 22, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 0 additions & 34 deletions build.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,6 @@ if ($Bootstrap.IsPresent) {
Write-Log -Warning "Module 'PSDepend' is missing. Installing 'PSDepend' ..."
Install-Module -Name PSDepend -Scope CurrentUser -Force
}
if (-not (Get-Module -Name Pester -ListAvailable)) {
Write-Log -Warning "Module 'Pester' is missing. Installing 'Pester' ..."
Install-Module -Name Pester -Scope CurrentUser -Force
}
if (-not (Get-Module -Name platyPS -ListAvailable)) {
Write-Log -Warning "Module 'platyPS' is missing. Installing 'platyPS' ..."
Install-Module -Name platyPS -Scope CurrentUser -Force
Expand Down Expand Up @@ -93,36 +89,6 @@ if(!$NoBuild.IsPresent) {

# Test step
if($Test.IsPresent) {
if (-not (Get-Module -Name Pester -ListAvailable)) {
throw "Cannot find the 'Pester' module. Please specify '-Bootstrap' to install build dependencies."
}

dotnet test "$PSScriptRoot/test/Unit"
if ($LASTEXITCODE -ne 0) { throw "xunit tests failed." }

Invoke-Tests -Path "$PSScriptRoot/test/Unit/Modules" -OutputFile UnitTestsResults.xml

if (-not (Get-Module -Name platyPS -ListAvailable)) {
throw "Cannot find the 'platyPS' module. Please specify '-Bootstrap' to install build dependencies."
}
elseif (-not (Get-Command -Name git -CommandType Application)) {
throw "Cannot find 'git'. Please make sure it's in the 'PATH'."
}

# Cmdlet help docs should be up-to-date.
# PlatyPS needs the module to be imported.
Import-Module -Force (Join-Path $PSScriptRoot src Modules Microsoft.Azure.Functions.PowerShellWorker)
try {
# Update the help and diff the result.
$docsPath = Join-Path $PSScriptRoot docs cmdlets
$null = Update-MarkdownHelp -Path $docsPath
$diff = git diff $docsPath
if ($diff) {
throw "Cmdlet help docs are not up-to-date, run Update-MarkdownHelp.`n$diff`n"
}
Write-Host "Help is up-to-date."
} finally {
# Clean up.
Remove-Module Microsoft.Azure.Functions.PowerShellWorker -Force
}
}
8 changes: 4 additions & 4 deletions docs/cmdlets/Get-OutputBinding.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
external help file: Microsoft.Azure.Functions.PowerShellWorker-help.xml
external help file: Microsoft.Azure.Functions.PowerShellWorker.dll-Help.xml
Module Name: Microsoft.Azure.Functions.PowerShellWorker
online version:
schema: 2.0.0
Expand All @@ -13,7 +13,7 @@ Gets the hashtable of the output bindings set so far.
## SYNTAX

```
Get-OutputBinding [[-Name] <String[]>] [-Purge] [<CommonParameters>]
Get-OutputBinding [[-Name] <String>] [-Purge] [<CommonParameters>]
```

## DESCRIPTION
Expand Down Expand Up @@ -49,12 +49,12 @@ The name of the output binding you want to get.
Supports wildcards.

```yaml
Type: String[]
Type: String
Parameter Sets: (All)
Aliases:

Required: False
Position: 1
Position: 0
Default value: *
Accept pipeline input: True (ByPropertyName, ByValue)
Accept wildcard characters: True
Expand Down
36 changes: 18 additions & 18 deletions docs/cmdlets/Push-OutputBinding.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
external help file: Microsoft.Azure.Functions.PowerShellWorker-help.xml
external help file: Microsoft.Azure.Functions.PowerShellWorker.dll-Help.xml
Module Name: Microsoft.Azure.Functions.PowerShellWorker
online version:
schema: 2.0.0
Expand Down Expand Up @@ -92,6 +92,21 @@ The output binding of "outQueue" will now have a list with 4 items:

## PARAMETERS

### -Clobber
(Optional) If specified, will force the value to be set for a specified output binding.

```yaml
Type: SwitchParameter
Parameter Sets: (All)
Aliases:

Required: False
Position: Named
Default value: False
Accept pipeline input: False
Accept wildcard characters: False
```

### -Name
The name of the output binding you want to set.

Expand All @@ -101,7 +116,7 @@ Parameter Sets: (All)
Aliases:

Required: True
Position: 1
Position: 0
Default value: None
Accept pipeline input: False
Accept wildcard characters: False
Expand All @@ -116,27 +131,12 @@ Parameter Sets: (All)
Aliases:

Required: True
Position: 2
Position: 1
Default value: None
Accept pipeline input: True (ByValue)
Accept wildcard characters: False
```

### -Clobber
(Optional) If specified, will force the value to be set for a specified output binding.

```yaml
Type: SwitchParameter
Parameter Sets: (All)
Aliases:

Required: False
Position: Named
Default value: False
Accept pipeline input: False
Accept wildcard characters: False
```

### CommonParameters
This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216).

Expand Down
4 changes: 2 additions & 2 deletions docs/cmdlets/Trace-PipelineObject.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
external help file: Microsoft.Azure.Functions.PowerShellWorker-help.xml
external help file: Microsoft.Azure.Functions.PowerShellWorker.dll-Help.xml
Module Name: Microsoft.Azure.Functions.PowerShellWorker
online version:
schema: 2.0.0
Expand Down Expand Up @@ -40,7 +40,7 @@ Parameter Sets: (All)
Aliases:

Required: True
Position: 1
Position: 0
Default value: None
Accept pipeline input: True (ByValue)
Accept wildcard characters: False
Expand Down
4 changes: 2 additions & 2 deletions src/DependencyManagement/DependencyManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ internal void InstallFunctionAppDependencies(PowerShell pwsh, ILogger logger)
.AddParameter("Name", moduleName)
.AddParameter("RequiredVersion", latestVersion)
.AddParameter("Path", DependenciesPath)
.AddParameter("Force", true)
.AddParameter("Force", Utils.BoxedTrue)
.AddParameter("ErrorAction", "Stop")
.InvokeAndClearCommands();

Expand All @@ -228,7 +228,7 @@ internal void InstallFunctionAppDependencies(PowerShell pwsh, ILogger logger)
// Clean up
pwsh.AddCommand(Utils.RemoveModuleCmdletInfo)
.AddParameter("Name", "PackageManagement, PowerShellGet")
.AddParameter("Force", true)
.AddParameter("Force", Utils.BoxedTrue)
.AddParameter("ErrorAction", "SilentlyContinue")
.InvokeAndClearCommands();
}
Expand Down
25 changes: 19 additions & 6 deletions src/FunctionInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Management.Automation;
using System.Management.Automation.Language;
using System.Text;

Expand All @@ -30,7 +31,9 @@ internal class AzFunctionInfo
internal readonly string FuncName;
internal readonly string EntryPoint;
internal readonly string ScriptPath;
internal readonly string DeployedPSFuncName;
internal readonly AzFunctionType Type;
internal readonly ScriptBlock FuncScriptBlock;
internal readonly ReadOnlyDictionary<string, PSScriptParamInfo> FuncParameters;
internal readonly ReadOnlyDictionary<string, ReadOnlyBindingInfo> AllBindings;
internal readonly ReadOnlyDictionary<string, ReadOnlyBindingInfo> InputBindings;
Expand All @@ -50,7 +53,8 @@ internal AzFunctionInfo(RpcFunctionMetadata metadata)
// Support 'entryPoint' only if 'scriptFile' is a .psm1 file;
// Support .psm1 'scriptFile' only if 'entryPoint' is specified.
bool isScriptFilePsm1 = ScriptPath.EndsWith(".psm1", StringComparison.OrdinalIgnoreCase);
if (string.IsNullOrEmpty(EntryPoint))
bool entryPointNotDefined = string.IsNullOrEmpty(EntryPoint);
if (entryPointNotDefined)
{
if (isScriptFilePsm1)
{
Expand All @@ -63,7 +67,7 @@ internal AzFunctionInfo(RpcFunctionMetadata metadata)
}

// Get the parameter names of the script or function.
var psScriptParams = GetParameters(ScriptPath, EntryPoint);
var psScriptParams = GetParameters(ScriptPath, EntryPoint, out ScriptBlockAst scriptAst);
FuncParameters = new ReadOnlyDictionary<string, PSScriptParamInfo>(psScriptParams);

var parametersCopy = new Dictionary<string, PSScriptParamInfo>(psScriptParams, StringComparer.OrdinalIgnoreCase);
Expand Down Expand Up @@ -121,6 +125,15 @@ internal AzFunctionInfo(RpcFunctionMetadata metadata)
throw new InvalidOperationException(errorMsg);
}

if (entryPointNotDefined && scriptAst.ScriptRequirements == null)
{
// If the function script is a '.ps1' file that doesn't have '#requires' defined,
// then we get the script block and will deploy it as a PowerShell function in the
// global scope of each Runspace, so as to avoid hitting the disk every invocation.
FuncScriptBlock = scriptAst.GetScriptBlock();
DeployedPSFuncName = $"_{FuncName}_";
}

AllBindings = new ReadOnlyDictionary<string, ReadOnlyBindingInfo>(allBindings);
InputBindings = new ReadOnlyDictionary<string, ReadOnlyBindingInfo>(inputBindings);
OutputBindings = new ReadOnlyDictionary<string, ReadOnlyBindingInfo>(outputBindings);
Expand All @@ -140,9 +153,9 @@ private AzFunctionType GetAzFunctionType(ReadOnlyBindingInfo bindingInfo)
}
}

private Dictionary<string, PSScriptParamInfo> GetParameters(string scriptFile, string entryPoint)
private Dictionary<string, PSScriptParamInfo> GetParameters(string scriptFile, string entryPoint, out ScriptBlockAst scriptAst)
{
ScriptBlockAst sbAst = Parser.ParseFile(scriptFile, out _, out ParseError[] errors);
scriptAst = Parser.ParseFile(scriptFile, out _, out ParseError[] errors);
if (errors != null && errors.Length > 0)
{
var stringBuilder = new StringBuilder(15);
Expand All @@ -158,11 +171,11 @@ private Dictionary<string, PSScriptParamInfo> GetParameters(string scriptFile, s
ReadOnlyCollection<ParameterAst> paramAsts = null;
if (string.IsNullOrEmpty(entryPoint))
{
paramAsts = sbAst.ParamBlock?.Parameters;
paramAsts = scriptAst.ParamBlock?.Parameters;
}
else
{
var asts = sbAst.FindAll(
var asts = scriptAst.FindAll(
ast => ast is FunctionDefinitionAst func && entryPoint.Equals(func.Name, StringComparison.OrdinalIgnoreCase),
searchNestedScriptBlocks: false).ToList();

Expand Down
28 changes: 22 additions & 6 deletions src/FunctionLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ namespace Microsoft.Azure.Functions.PowerShellWorker
/// <summary>
/// FunctionLoader holds metadata of functions.
/// </summary>
internal class FunctionLoader
internal static class FunctionLoader
{
private readonly Dictionary<string, AzFunctionInfo> _loadedFunctions = new Dictionary<string, AzFunctionInfo>();
private static readonly Dictionary<string, AzFunctionInfo> LoadedFunctions = new Dictionary<string, AzFunctionInfo>();

internal static string FunctionAppRootPath { get; private set; }
internal static string FunctionAppProfilePath { get; private set; }
Expand All @@ -26,9 +26,9 @@ internal class FunctionLoader
/// <summary>
/// Query for function metadata can happen in parallel.
/// </summary>
internal AzFunctionInfo GetFunctionInfo(string functionId)
internal static AzFunctionInfo GetFunctionInfo(string functionId)
{
if (_loadedFunctions.TryGetValue(functionId, out AzFunctionInfo funcInfo))
if (LoadedFunctions.TryGetValue(functionId, out AzFunctionInfo funcInfo))
{
return funcInfo;
}
Expand All @@ -40,9 +40,25 @@ internal AzFunctionInfo GetFunctionInfo(string functionId)
/// This method runs once per 'FunctionLoadRequest' during the code start of the worker.
/// It will always run synchronously because we process 'FunctionLoadRequest' synchronously.
/// </summary>
internal void LoadFunction(FunctionLoadRequest request)
internal static void LoadFunction(FunctionLoadRequest request)
{
Copy link
Member

Choose a reason for hiding this comment

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

do not use static. In future, functions host will move to a model where language worker process does not restart with file changes, FunctionLoader needs to handle lifetime of loaded functions - unload when host sends FileChangeEventRequest

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That will be a non-trivial refactoring. Can you please open an issue describing what worker is supposed to do when receiving FileChangeEventRequest? For example, if there are on-going invocations, should the worker stop them or wait for them to finish before taking action on the FileChangeEventRequest? We need more information about this scenario to decide the appropriate refactoring work.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If the requirement is simply reset the worker, then we already have FunctionLoader.ClearLoadedFunctions to clean up the loaded functions.

_loadedFunctions.Add(request.FunctionId, new AzFunctionInfo(request.Metadata));
LoadedFunctions.Add(request.FunctionId, new AzFunctionInfo(request.Metadata));
}

/// <summary>
/// Get all loaded functions.
/// </summary>
internal static IEnumerable<AzFunctionInfo> GetLoadedFunctions()
{
return LoadedFunctions.Values;
}

/// <summary>
/// Clear all loaded functions.
/// </summary>
internal static void ClearLoadedFunctions()
{
LoadedFunctions.Clear();
}

/// <summary>
Expand Down
1 change: 0 additions & 1 deletion src/Microsoft.Azure.Functions.PowerShellWorker.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ Licensed under the MIT license. See LICENSE file in the project root for full li
<ItemGroup>
<PackageReference Include="Grpc.Core" Version="1.20.1" />
<PackageReference Include="Microsoft.PowerShell.SDK" Version="6.2.0" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.1" />
<PackageReference Include="CommandLineParser" Version="2.3.0" />
<PackageReference Include="Google.Protobuf" Version="3.7.0" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@
@{

# Script module or binary module file associated with this manifest.
RootModule = 'Microsoft.Azure.Functions.PowerShellWorker.psm1'
RootModule = 'Microsoft.Azure.Functions.PowerShellWorker.dll'

# Version number of this module.
ModuleVersion = '0.1.0'

# Supported PSEditions
CompatiblePSEditions = @('Desktop', 'Core')
CompatiblePSEditions = @('Core')

# ID used to uniquely identify this module
GUID = 'f0149ba6-bd6f-4dbd-afe5-2a95bd755d6c'
Expand All @@ -30,7 +30,7 @@ Copyright = '(c) Microsoft Corporation. All rights reserved.'
Description = 'The module used in an Azure Functions environment for setting and retrieving Output Bindings.'

# Minimum version of the PowerShell engine required by this module
PowerShellVersion = '5.1'
PowerShellVersion = '6.2'

# Modules that must be imported into the global environment prior to importing this module
RequiredModules = @()
Expand All @@ -51,10 +51,10 @@ FormatsToProcess = @()
NestedModules = @()

# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export.
FunctionsToExport = @('Push-OutputBinding', 'Get-OutputBinding', 'Trace-PipelineObject')
FunctionsToExport = @()

# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export.
CmdletsToExport = @()
CmdletsToExport = @('Push-OutputBinding', 'Get-OutputBinding', 'Trace-PipelineObject')

# Variables to export from this module
VariablesToExport = @()
Expand Down
Loading