diff --git a/eng/ci/host.metrics-monitor.yml b/eng/ci/host.metrics-monitor.yml new file mode 100644 index 0000000000..c9619fe966 --- /dev/null +++ b/eng/ci/host.metrics-monitor.yml @@ -0,0 +1,56 @@ +# No triggers for code push to any branch. +trigger: none + +# No PR triggers. +pr: none + +schedules: + - cron: "0 6,18 * * *" + displayName: Daily Schedule (6 AM & 6 PM UTC) + branches: + include: + - dev + always: true + +resources: + repositories: + - repository: 1es + type: git + name: 1ESPipelineTemplates/1ESPipelineTemplates + ref: refs/tags/release + - repository: eng + type: git + name: engineering + ref: refs/tags/release + +variables: + - template: /ci/variables/cfs.yml@eng + +extends: + template: v1/1ES.Unofficial.PipelineTemplate.yml@1es + parameters: + pool: + name: 1es-pool-azfunc-benchmarking + image: 1es-windows-2022-benchmark-runner-vanilla + os: windows + + stages: + - stage: RunWindows + displayName: Collect Windows metrics + jobs: + - template: /eng/ci/templates/official/jobs/run-metrics-monitor.yml@self + parameters: + description: .NET9 Timer Application + functionAppName: TimerAppNet9 + azureMonitorConnectionString: $(WINDOWS_AZURE_MONITOR_CONNECTION) + + - stage: RunLinux + dependsOn: [] + displayName: Collect Linux metrics + jobs: + - template: /eng/ci/templates/official/jobs/run-metrics-monitor.yml@self + parameters: + os: Linux + description: .NET9 Timer Application + functionAppName: TimerAppNet9 + azureMonitorConnectionString: $(LINUX_AZURE_MONITOR_CONNECTION) diff --git a/eng/ci/templates/official/jobs/run-metrics-monitor.yml b/eng/ci/templates/official/jobs/run-metrics-monitor.yml new file mode 100644 index 0000000000..8426e21e9d --- /dev/null +++ b/eng/ci/templates/official/jobs/run-metrics-monitor.yml @@ -0,0 +1,167 @@ +parameters: +- name: description + type: string +- name: functionAppName + type: string +- name: os + type: string + default: Windows + values: + - Windows + - Linux +- name: azureMonitorConnectionString + type: string + +jobs: +- job: ${{ parameters.functionAppName }}_${{ parameters.os }} + displayName: ${{ parameters.os }} ${{ parameters.description }} + pool: + name: 1es-pool-azfunc-benchmarking + ${{ if eq(parameters.os, 'Linux') }}: + image: 1es-ubuntu-22.04-benchmark-runner-vanilla + os: linux + ${{ else }}: + image: 1es-windows-2022-benchmark-runner-vanilla + os: windows + + variables: + functionAppOutputPath: $(Build.BinariesDirectory)/Published/${{ parameters.functionAppName }} + hostOutputPath: $(Build.BinariesDirectory)/Published/HostRuntime + hostAssemblyPath: $(hostOutputPath)/Microsoft.Azure.WebJobs.Script.WebHost.dll + logsDirectory: $(Build.ArtifactStagingDirectory)/Logs + stdOutLogsFilePath: $(logsDirectory)/std_out_logs.txt + stdErrorLogsFilePath: $(logsDirectory)/std_error_logs.txt + FUNCTIONS_WORKER_RUNTIME: 'dotnet-isolated' + FUNCTIONS_WORKER_RUNTIME_VERSION: '9.0' + AzureFunctionsWebHost__hostid: '${{ parameters.functionAppName }}_${{ parameters.os }}' + AzureWebJobsScriptRoot: '$(functionAppOutputPath)' + ${{ if eq(parameters.os, 'Linux') }}: + publishRid: linux-x64 + ${{ if eq(parameters.os, 'Windows') }}: + publishRid: win-x64 + + steps: + - template: /eng/ci/templates/install-dotnet.yml@self + + - task: CopyFiles@2 + displayName: Copy benchmark apps to temp location + inputs: + SourceFolder: '$(Build.SourcesDirectory)/test/Performance/Apps' + Contents: '**/*' + TargetFolder: '$(Build.ArtifactStagingDirectory)/PerformanceTestApps' + CleanTargetFolder: true + + - task: DotNetCoreCLI@2 + displayName: Publish function app + inputs: + command: publish + publishWebProjects: false + zipAfterPublish: false + modifyOutputPath: false + projects: '$(Build.ArtifactStagingDirectory)/PerformanceTestApps/${{ parameters.functionAppName }}/App.csproj' + arguments: -c Release -o $(functionAppOutputPath) -f net9.0 -r $(publishRid) + workingDirectory: $(Build.ArtifactStagingDirectory)/PerformanceTestApps/${{ parameters.functionAppName }} + + - task: DotNetCoreCLI@2 + displayName: Publish host + inputs: + command: publish + publishWebProjects: false + zipAfterPublish: false + modifyOutputPath: false + projects: '$(Build.SourcesDirectory)/src/WebJobs.Script.WebHost/WebJobs.Script.WebHost.csproj' + arguments: -c Release -o $(hostOutputPath) -r $(publishRid) -f net8.0 -p:PlaceholderSimulation=true + workingDirectory: $(Build.SourcesDirectory)/src/WebJobs.Script.WebHost + + - pwsh: | + Write-Host "Creating log directory: $(logsDirectory)" + New-Item -ItemType Directory -Path $(logsDirectory) + displayName: Create log directories + + - ${{ if eq(parameters.os, 'Windows') }}: + - pwsh: | + $env:APPLICATIONINSIGHTS_CONNECTION_STRING = "${env:AZ_MON_CONNECTION_STRING}" + $errorLogFilePath = "$(Build.ArtifactStagingDirectory)/Logs/std_err_logs.txt" + $process = Start-Process -FilePath "$(hostOutputPath)/Microsoft.Azure.WebJobs.Script.WebHost.exe" -RedirectStandardOutput $(stdOutLogsFilePath) -RedirectStandardError $(stdErrorLogsFilePath) -PassThru + Write-Host "Started process ID: $($process.Id)" + echo $process.Id > $(Build.ArtifactStagingDirectory)/hostProcessId.txt + displayName: Start functions host + env: + AZ_MON_CONNECTION_STRING: ${{ parameters.azureMonitorConnectionString }} + - ${{ else }}: + - script: | + # In Azure DevOps, when variables convert into environment variables, variable names become uppercase, and periods turn into underscores. + # This works for windows when getting the env variable value, but fails on linux. So we need to pass the variable value using correct case. + # https://learn.microsoft.com/en-us/azure/devops/pipelines/process/variables?view=azure-devops&tabs=yaml%2Cbatch#environment-variables + export AzureWebJobsScriptRoot="$(functionAppOutputPath)" + export APPLICATIONINSIGHTS_CONNECTION_STRING="$AZ_MON_CONNECTION_STRING" + nohup dotnet $(hostAssemblyPath) > $(stdOutLogsFilePath) 2> $(stdErrorLogsFilePath) & + echo $! > $(Build.ArtifactStagingDirectory)/hostProcessId.txt + displayName: Start functions host + env: + AZ_MON_CONNECTION_STRING: ${{ parameters.azureMonitorConnectionString }} + + - pwsh: | + $url = "http://localhost:5000/api/warmup" + Write-Host "Checking if host is ready at $url..." + $maxAttempts = 5 + + for ($attempt = 0; $attempt -lt $maxAttempts; $attempt++) { + try { + $response = Invoke-RestMethod -Uri $url -Method Get -TimeoutSec 5 -ErrorAction Stop + Write-Host "Response: $response" + break + } catch { + Write-Host "Attempt $($attempt+1) failed: $_.Exception.Message. Retrying in 10 seconds..." + Start-Sleep -Seconds 10 + } + } + displayName: Wait until host is ready + + - pwsh: | + $helloUrl = "http://localhost:5000?forcespecialization=1" + Write-Host "Calling $helloUrl" + Invoke-WebRequest -Uri $helloUrl -Method Get -ErrorAction Stop + displayName: Specialize + + - pwsh: | + $appRunDurationInSeconds = $env:RUN_DURATION_IN_SECONDS + Start-Sleep -Seconds $appRunDurationInSeconds + displayName: Run for $(APP_RUN_DURATION_IN_SECONDS) seconds + env: + RUN_DURATION_IN_SECONDS: $(APP_RUN_DURATION_IN_SECONDS) + + - ${{ if eq(parameters.os, 'Windows') }}: + - pwsh: | + $processIdFile = "$(Build.ArtifactStagingDirectory)/hostProcessId.txt" + if (Test-Path $processIdFile) { + $processId = Get-Content $processIdFile + Write-Host "Stop functions host process with process ID: $processId" + Stop-Process -Id $processId -Force + } else { + Write-Host "Process ID file not found." + } + displayName: Stop host process + condition: always() + + - ${{ if eq(parameters.os, 'Linux') }}: + - script: | + processIdFile="$(Build.ArtifactStagingDirectory)/hostProcessId.txt" + if [ -f "$processIdFile" ]; then + processId=$(cat $processIdFile) + echo "Sending SIGTERM to functions host process process ID: $processId" + kill -SIGTERM $processId + else + echo "Process ID file not found." + fi + displayName: Stop host process + condition: always() + + - pwsh: | + Write-Host "Logs:" + Get-Content $(stdOutLogsFilePath) + Write-Host "----" + Write-Host "Error logs:" + Get-Content $(stdErrorLogsFilePath) + displayName: Print logs + condition: always() diff --git a/test/Performance/Apps/TimerAppNet9/App.csproj b/test/Performance/Apps/TimerAppNet9/App.csproj new file mode 100644 index 0000000000..8b0798793c --- /dev/null +++ b/test/Performance/Apps/TimerAppNet9/App.csproj @@ -0,0 +1,16 @@ + + + net9.0 + v4 + Exe + enable + enable + + + + + + + + + diff --git a/test/Performance/Apps/TimerAppNet9/Program.cs b/test/Performance/Apps/TimerAppNet9/Program.cs new file mode 100644 index 0000000000..bf1061cf85 --- /dev/null +++ b/test/Performance/Apps/TimerAppNet9/Program.cs @@ -0,0 +1,7 @@ +using Microsoft.Azure.Functions.Worker.Builder; +using Microsoft.Extensions.Hosting; + +var builder = FunctionsApplication.CreateBuilder(args); +builder.ConfigureFunctionsWebApplication(); + +await builder.Build().RunAsync(); diff --git a/test/Performance/Apps/TimerAppNet9/TimerFunctions.cs b/test/Performance/Apps/TimerAppNet9/TimerFunctions.cs new file mode 100644 index 0000000000..ba501c0911 --- /dev/null +++ b/test/Performance/Apps/TimerAppNet9/TimerFunctions.cs @@ -0,0 +1,14 @@ +using Microsoft.Azure.Functions.Worker; +using Microsoft.Extensions.Logging; + +namespace App +{ + public sealed class TimerFunctions(ILoggerFactory loggerFactory) + { + private readonly ILogger _logger = loggerFactory.CreateLogger(); + + [Function("TimerFunction1")] + public void Run([TimerTrigger("%TIMER_RUN_SCHEDULE_CRON_EXPRESSION%")] TimerInfo timer) + => _logger.LogInformation($"C# Timer trigger executed at:{DateTime.Now}. IsPastDue:{timer.IsPastDue}"); + } +} diff --git a/test/Performance/Apps/TimerAppNet9/host.json b/test/Performance/Apps/TimerAppNet9/host.json new file mode 100644 index 0000000000..26ecc434cb --- /dev/null +++ b/test/Performance/Apps/TimerAppNet9/host.json @@ -0,0 +1,4 @@ +{ + "version": "2.0", + "telemetryMode": "OpenTelemetry" +}