Skip to content
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

Prejit support on FunctionsNetHost #2711

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions eng/ci/host/official-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ extends:

jobs:
- template: /eng/ci/templates/official/jobs/build-host-prelaunch-artifacts.yml@self
- template: /eng/ci/templates/official/jobs/build-host-prejit-artifacts.yml@self
- template: /eng/ci/templates/official/jobs/build-host-artifacts-linux.yml@self
parameters:
PoolName: 1es-pool-azfunc
Expand Down
1 change: 1 addition & 0 deletions eng/ci/host/public-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ extends:

jobs:
- template: /eng/ci/templates/official/jobs/build-host-prelaunch-artifacts.yml@self
- template: /eng/ci/templates/official/jobs/build-host-prejit-artifacts.yml@self
- template: /eng/ci/templates/official/jobs/build-host-artifacts-linux.yml@self
parameters:
PoolName: 1es-pool-azfunc-public
Expand Down
30 changes: 30 additions & 0 deletions eng/ci/templates/official/jobs/build-host-prejit-artifacts.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
jobs:

- job: BuildPreJitApp
displayName: Build Pre-jit placeholder app artifacts

templateContext:
outputParentDirectory: $(Build.ArtifactStagingDirectory)
outputs:
- output: pipelineArtifact
displayName: Publish Pre-jit placeholder app artifacts
path: $(Build.ArtifactStagingDirectory)/_preJitAppPackages
Copy link
Contributor

Choose a reason for hiding this comment

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

consider making this a variable

artifact: _preJitAppPackages

variables:
dotnetVersions: 'net8.0,net9.0'

steps:
- template: /eng/ci/templates/steps/install-dotnet.yml@self

- ${{ each version in split(variables.dotnetVersions, ',') }}:
Copy link
Contributor

Choose a reason for hiding this comment

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

small nit: use framework terminology instead of version.

- task: DotNetCoreCLI@2
displayName: ${{ version }} publish of pre-jit app
inputs:
command: publish
publishWebProjects: false
zipAfterPublish: false
modifyOutputPath: false
arguments: -c Release -o $(Build.ArtifactStagingDirectory)/_preJitAppPackages/${{ replace(version, 'net', '') }} -f ${{ version }} -p:UseAppHost=false
projects: host/src/PlaceholderApp/FunctionsNetHost.PlaceholderApp.csproj
workingDirectory: host/src
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,5 @@ jobs:
publishWebProjects: false
zipAfterPublish: false
modifyOutputPath: false
arguments: -c Release -o $(Build.ArtifactStagingDirectory)/_preLaunchAppPackages/${{ version }} -f ${{ version }} -p:UseAppHost=false
arguments: -c Release -o $(Build.ArtifactStagingDirectory)/_preLaunchAppPackages/${{ replace(version, 'net', '') }} -f ${{ version }} -p:UseAppHost=false
Copy link
Member Author

Choose a reason for hiding this comment

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

Fixing an existing error in output path value, which was introduced during the pipeline migration.

projects: host/src/PrelaunchApp/App.csproj
8 changes: 7 additions & 1 deletion eng/ci/templates/official/jobs/pack-host-artifacts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,13 @@ jobs:
displayName: Download prelaunch artifacts
inputs:
artifactName: _preLaunchAppPackages
path: $(Build.SourcesDirectory)/host/dist/portable
path: $(Build.SourcesDirectory)/host/dist/portable/prelaunchapps

- task: DownloadPipelineArtifact@2
Copy link
Contributor

Choose a reason for hiding this comment

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

Are we supposed to use DownloadPipelineArtifact? 1ES has its own syntax for artifact inputs:

https://eng.ms/docs/cloud-ai-platform/devdiv/one-engineering-system-1es/1es-docs/1es-pipeline-templates/features/inputs/pipeline-artifact

displayName: Download pre-jit artifacts
inputs:
artifactName: _preJitAppPackages
path: $(Build.SourcesDirectory)/host/dist/portable/prejit-placeholder-app

- task: DownloadPipelineArtifact@2
displayName: Download host artifacts - linux
Expand Down
13 changes: 13 additions & 0 deletions host/src/FunctionsNetHost.Shared/Constants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

namespace FunctionsNetHost.Shared
{
public static class Constants
{
public const string LogCategory = "FunctionsNetHost";
public const string DefaultLogPrefix = "LanguageWorkerConsoleLog";
public const string NetHostWaitHandleName = "AzureFunctionsNetHostSpecializationWaitHandle";
public const string LogTimeStampFormat = "yyyy-MM-dd HH:mm:ss.fff";
}
}
18 changes: 18 additions & 0 deletions host/src/FunctionsNetHost.Shared/EnvironmentVariables.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

namespace FunctionsNetHost.Shared
{
public static class EnvironmentVariables
{
/// <summary>
/// The environment variable which is used to specify the path to the jittrace file which will be used for prejitting.
/// </summary>
public const string PreJitFilePath = "AZURE_FUNCTIONS_NETHOST_PREJIT_FILE_PATH";

/// <summary>
/// The .NET startup hooks environment variable.
/// </summary>
public const string DotnetStartupHooks = "DOTNET_STARTUP_HOOKS";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
</Project>
6 changes: 6 additions & 0 deletions host/src/FunctionsNetHost.sln
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ VisualStudioVersion = 17.5.33627.172
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FunctionsNetHost", "FunctionsNetHost\FunctionsNetHost.csproj", "{6C05D0AC-F6AC-45FB-8A73-A3F44DF131BC}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FunctionsNetHost.Shared", "FunctionsNetHost.Shared\FunctionsNetHost.Shared.csproj", "{91867A34-8B79-4E01-81BC-592F1CD1CCCE}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -15,6 +17,10 @@ Global
{6C05D0AC-F6AC-45FB-8A73-A3F44DF131BC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6C05D0AC-F6AC-45FB-8A73-A3F44DF131BC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6C05D0AC-F6AC-45FB-8A73-A3F44DF131BC}.Release|Any CPU.Build.0 = Release|Any CPU
{91867A34-8B79-4E01-81BC-592F1CD1CCCE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{91867A34-8B79-4E01-81BC-592F1CD1CCCE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{91867A34-8B79-4E01-81BC-592F1CD1CCCE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{91867A34-8B79-4E01-81BC-592F1CD1CCCE}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
4 changes: 1 addition & 3 deletions host/src/FunctionsNetHost/Configuration/Configuration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ namespace FunctionsNetHost
/// </summary>
internal static class Configuration
{
private const string DefaultLogPrefix = "LanguageWorkerConsoleLog";

static Configuration()
{
Reload();
Expand All @@ -22,7 +20,7 @@ internal static void Reload()
{
IsTraceLogEnabled = string.Equals(EnvironmentUtils.GetValue(EnvironmentVariables.EnableTraceLogs), "1");
var disableLogPrefix = string.Equals(EnvironmentUtils.GetValue(EnvironmentVariables.DisableLogPrefix), "1");
LogPrefix = disableLogPrefix ? string.Empty : DefaultLogPrefix;
LogPrefix = disableLogPrefix ? string.Empty : Shared.Constants.DefaultLogPrefix;
}

/// <summary>
Expand Down
10 changes: 9 additions & 1 deletion host/src/FunctionsNetHost/Environment/EnvironmentUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,22 @@ internal static class EnvironmentUtils
#if OS_LINUX
[System.Runtime.InteropServices.DllImport("libc")]
private static extern int setenv(string name, string value, int overwrite);

[System.Runtime.InteropServices.DllImport("libc")]
private static extern string getenv(string name);
#endif

/// <summary>
/// Gets the environment variable value.
/// </summary>
internal static string? GetValue(string environmentVariableName)
{
{
// Observed Environment.GetEnvironmentVariable not returning the value which was just set. So using native method directly here.
#if OS_LINUX
return getenv(environmentVariableName);
#else
return Environment.GetEnvironmentVariable(environmentVariableName);
#endif
}

/// <summary>
Expand Down
11 changes: 8 additions & 3 deletions host/src/FunctionsNetHost/Environment/EnvironmentVariables.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,25 @@ internal static class EnvironmentVariables
/// Set value to "1" will prevent the log entries to have the prefix "LanguageWorkerConsoleLog".
/// Set this to see logs when you are debugging FunctionsNetHost locally with WebHost.
/// </summary>
internal const string DisableLogPrefix = "AZURE_FUNCTIONS_FUNCTIONSNETHOST_DISABLE_LOGPREFIX";
internal const string DisableLogPrefix = "AZURE_FUNCTIONS_NETHOST_DISABLE_LOGPREFIX";

/// <summary>
/// Set value to "1" for enabling additional trace logs in FunctionsNetHost.
/// </summary>
internal const string EnableTraceLogs = "AZURE_FUNCTIONS_FUNCTIONSNETHOST_TRACE";
internal const string EnableTraceLogs = "AZURE_FUNCTIONS_NETHOST_TRACE";

/// <summary>
/// Application pool Id for the placeholder app. Only available in Windows(when running in IIS).
/// </summary>
internal const string AppPoolId = "APP_POOL_ID";
internal const string AppPoolId = "APP_POOL_ID";

/// <summary>
/// The worker runtime version. Example value: "8.0" (for a .NET8 placeholder)
/// </summary>
internal const string FunctionsWorkerRuntimeVersion = "FUNCTIONS_WORKER_RUNTIME_VERSION";

/// <summary>
/// The environment variable that disables prejit. If set to "1," prejit will be disabled.
/// </summary>
internal const string DisablePrejit = "AZURE_FUNCTIONS_NETHOST_DISABLE_PREJIT";
}
12 changes: 9 additions & 3 deletions host/src/FunctionsNetHost/FunctionsNetHost.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<EnablePreviewFeatures>True</EnablePreviewFeatures>
<LangVersion>preview</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
Expand All @@ -28,12 +30,16 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
<PackageReference Include="System.Text.Json" Version="8.0.5" />
<PackageReference Include="Microsoft.NETCore.DotNetAppHost" Version="8.0.0-preview.4.23259.5" />
<PackageReference Include="System.Text.Json" Version="9.0.0-preview.6.24327.7" />
<PackageReference Include="Microsoft.NETCore.DotNetAppHost" Version="9.0.0-preview.6.24327.7" />
</ItemGroup>

<ItemGroup>
<Protobuf Include="..\..\..\protos\azure-functions-language-worker-protobuf\**\*.proto" ProtoRoot="..\..\..\protos\azure-functions-language-worker-protobuf\src\proto" GrpcServices="Client" Access="internal" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\FunctionsNetHost.Shared\FunctionsNetHost.Shared.csproj" />
</ItemGroup>

</Project>
16 changes: 8 additions & 8 deletions host/src/FunctionsNetHost/Grpc/GrpcClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ internal sealed class GrpcClient
{
private readonly Channel<StreamingMessage> _outgoingMessageChannel;
private readonly IncomingGrpcMessageHandler _messageHandler;
private readonly GrpcWorkerStartupOptions _grpcWorkerStartupOptions;
private readonly NetHostRunOptions _netHostRunOptions;

internal GrpcClient(GrpcWorkerStartupOptions grpcWorkerStartupOptions, AppLoader appLoader)
internal GrpcClient(NetHostRunOptions netHostRunOptions, AppLoader appLoader)
{
_grpcWorkerStartupOptions = grpcWorkerStartupOptions;
_netHostRunOptions = netHostRunOptions;
var channelOptions = new UnboundedChannelOptions
{
SingleWriter = false,
Expand All @@ -28,12 +28,12 @@ internal GrpcClient(GrpcWorkerStartupOptions grpcWorkerStartupOptions, AppLoader

_outgoingMessageChannel = Channel.CreateUnbounded<StreamingMessage>(channelOptions);

_messageHandler = new IncomingGrpcMessageHandler(appLoader, _grpcWorkerStartupOptions);
_messageHandler = new IncomingGrpcMessageHandler(appLoader, _netHostRunOptions);
}

internal async Task InitAsync()
{
var endpoint = _grpcWorkerStartupOptions.ServerUri.AbsoluteUri;
var endpoint = _netHostRunOptions.WorkerStartupOptions.ServerUri.AbsoluteUri;
Logger.LogTrace($"Grpc service endpoint:{endpoint}");

var functionRpcClient = CreateFunctionRpcClient(endpoint);
Expand Down Expand Up @@ -69,7 +69,7 @@ private async Task SendStartStreamMessageAsync(IClientStreamWriter<StreamingMess
{
var startStreamMsg = new StartStream()
{
WorkerId = _grpcWorkerStartupOptions.WorkerId
WorkerId = _netHostRunOptions.WorkerStartupOptions.WorkerId
};

var startStream = new StreamingMessage()
Expand All @@ -89,8 +89,8 @@ private FunctionRpcClient CreateFunctionRpcClient(string endpoint)

var grpcChannel = GrpcChannel.ForAddress(grpcUri, new GrpcChannelOptions()
{
MaxReceiveMessageSize = _grpcWorkerStartupOptions.GrpcMaxMessageLength,
MaxSendMessageSize = _grpcWorkerStartupOptions.GrpcMaxMessageLength,
MaxReceiveMessageSize = _netHostRunOptions.WorkerStartupOptions.GrpcMaxMessageLength,
MaxSendMessageSize = _netHostRunOptions.WorkerStartupOptions.GrpcMaxMessageLength,
Credentials = ChannelCredentials.Insecure
});

Expand Down
55 changes: 39 additions & 16 deletions host/src/FunctionsNetHost/Grpc/IncomingGrpcMessageHandler.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

using System.IO.Pipes;
using FunctionsNetHost.Prelaunch;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Grpc.Messages;
Expand All @@ -11,12 +12,12 @@ internal sealed class IncomingGrpcMessageHandler
{
private bool _specializationDone;
private readonly AppLoader _appLoader;
private readonly GrpcWorkerStartupOptions _grpcWorkerStartupOptions;
private readonly NetHostRunOptions _netHostRunOptions;

internal IncomingGrpcMessageHandler(AppLoader appLoader, GrpcWorkerStartupOptions grpcWorkerStartupOptions)
internal IncomingGrpcMessageHandler(AppLoader appLoader, NetHostRunOptions netHostRunOptions)
{
_appLoader = appLoader;
_grpcWorkerStartupOptions = grpcWorkerStartupOptions;
_netHostRunOptions = netHostRunOptions;
}

internal Task ProcessMessageAsync(StreamingMessage message)
Expand Down Expand Up @@ -60,12 +61,15 @@ private async Task Process(StreamingMessage msg)
};
break;
}
case StreamingMessage.ContentOneofCase.FunctionEnvironmentReloadRequest:
case StreamingMessage.ContentOneofCase.FunctionEnvironmentReloadRequest:

Configuration.Reload();
Logger.LogTrace("Specialization request received.");

var envReloadRequest = msg.FunctionEnvironmentReloadRequest;
Logger.Log("Specialization request received.");
var envReloadRequest = msg.FunctionEnvironmentReloadRequest;
foreach (var kv in envReloadRequest.EnvironmentVariables)
{
EnvironmentUtils.SetValue(kv.Key, kv.Value);
}
Configuration.Reload();

var workerConfig = await WorkerConfigUtils.GetWorkerConfig(envReloadRequest.FunctionAppDirectory);

Expand All @@ -88,19 +92,19 @@ private async Task Process(StreamingMessage msg)
var applicationExePath = Path.Combine(envReloadRequest.FunctionAppDirectory, workerConfig.Description.DefaultWorkerPath!);
Logger.LogTrace($"application path {applicationExePath}");

foreach (var kv in envReloadRequest.EnvironmentVariables)
if (_netHostRunOptions.IsPreJitSupported)
{
EnvironmentUtils.SetValue(kv.Key, kv.Value);
// Signal so that startup hook load the payload assembly.
await NotifySpecializationOccured(applicationExePath);
}

else
{
#pragma warning disable CS4014
Task.Run(() =>
Task.Run(() => _appLoader.RunApplication(applicationExePath));
#pragma warning restore CS4014
{
_ = _appLoader.RunApplication(applicationExePath);
});
}

Logger.LogTrace($"Will wait for worker loaded signal.");
Logger.LogTrace("Will wait for worker loaded signal.");
WorkerLoadStatusSignalManager.Instance.Signal.WaitOne();

var logMessage = $"FunctionApp assembly loaded successfully. ProcessId:{Environment.ProcessId}";
Expand All @@ -119,6 +123,25 @@ private async Task Process(StreamingMessage msg)
{
await MessageChannel.Instance.SendOutboundAsync(responseMessage);
}
}

private static async Task NotifySpecializationOccured(string applicationExePath)
{
// Startup hook code has opened a named pipe server stream and waiting for a client to connect & send a message.
try
{
using var pipeClient = new NamedPipeClientStream(".", Shared.Constants.NetHostWaitHandleName, PipeDirection.Out);
await pipeClient.ConnectAsync();
using var writer = new StreamWriter(pipeClient);
writer.WriteLine(applicationExePath);
writer.Flush();
Logger.LogTrace("Sent application path to named pipe server stream.");
}
catch (Exception ex)
{
Logger.Log($"Error connecting to named pipe server. {ex}");
throw;
}
}

private static FunctionEnvironmentReloadResponse BuildFailedEnvironmentReloadResponse(Exception? exception = null)
Expand Down
Loading