diff --git a/tools/Crank/Agent/Linux/Docker/Dockerfile b/tools/Crank/Agent/Linux/Docker/Dockerfile
new file mode 100644
index 0000000000..12a6a716d0
--- /dev/null
+++ b/tools/Crank/Agent/Linux/Docker/Dockerfile
@@ -0,0 +1,3 @@
+FROM crank-agent
+
+ENTRYPOINT [ "/app/crank-agent", "--url", "http://*:5010" ]
diff --git a/tools/Crank/Agent/Linux/Docker/build.sh b/tools/Crank/Agent/Linux/Docker/build.sh
new file mode 100644
index 0000000000..8f8e0c345e
--- /dev/null
+++ b/tools/Crank/Agent/Linux/Docker/build.sh
@@ -0,0 +1,3 @@
+#!/usr/bin/env bash
+
+docker build -t functions-crank-agent -f Dockerfile ../../
diff --git a/tools/Crank/Agent/Linux/Docker/run.sh b/tools/Crank/Agent/Linux/Docker/run.sh
new file mode 100644
index 0000000000..eeebbc52d3
--- /dev/null
+++ b/tools/Crank/Agent/Linux/Docker/run.sh
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+
+docker run -it --name functions-crank-agent -d --network host --restart always --privileged \
+--mount type=bind,source=/home/Functions/FunctionApps/HelloApp,target=/home/Functions/FunctionApps/HelloApp \
+-v /var/run/docker.sock:/var/run/docker.sock functions-crank-agent "$@"
\ No newline at end of file
diff --git a/tools/Crank/Agent/Linux/Docker/stop.sh b/tools/Crank/Agent/Linux/Docker/stop.sh
new file mode 100644
index 0000000000..89f6e9136b
--- /dev/null
+++ b/tools/Crank/Agent/Linux/Docker/stop.sh
@@ -0,0 +1,4 @@
+#!/usr/bin/env bash
+
+docker stop functions-crank-agent
+docker rm functions-crank-agent
diff --git a/tools/Crank/Agent/Linux/bootstrap.sh b/tools/Crank/Agent/Linux/bootstrap.sh
new file mode 100755
index 0000000000..02b6967250
--- /dev/null
+++ b/tools/Crank/Agent/Linux/bootstrap.sh
@@ -0,0 +1,15 @@
+#!/bin/bash
+
+mkdir ~/github
+cd ~/github
+git clone https://github.com/Azure/azure-functions-host.git
+cd azure-functions-host
+git checkout dev
+
+cd tools/Crank/Agent
+sudo find . -name "*.sh" -exec sudo chmod +xr {} \;
+sudo find . -name "*.ps1" -exec sudo chmod +xr {} \;
+
+Linux/install-powershell.sh
+
+./setup-crank-agent-raw.ps1 $1 -Verbose
diff --git a/tools/Crank/Agent/Linux/install-docker.sh b/tools/Crank/Agent/Linux/install-docker.sh
new file mode 100644
index 0000000000..3248ebab45
--- /dev/null
+++ b/tools/Crank/Agent/Linux/install-docker.sh
@@ -0,0 +1,30 @@
+#!/bin/bash
+
+# Following instructions at https://docs.docker.com/engine/install/ubuntu
+
+sudo apt-get update
+
+sudo apt-get install -y \
+ apt-transport-https \
+ ca-certificates \
+ curl \
+ gnupg-agent \
+ software-properties-common
+
+curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
+
+sudo add-apt-repository \
+ "deb [arch=amd64] https://download.docker.com/linux/ubuntu \
+ $(lsb_release -cs) \
+ stable"
+
+sudo apt-get update
+sudo apt-get install -y docker-ce docker-ce-cli containerd.io
+
+sudo groupadd docker
+sudo usermod -aG docker Functions
+newgrp docker
+
+sudo setfacl --modify user:Functions:rw /var/run/docker.sock
+
+sudo systemctl enable docker
diff --git a/tools/Crank/Agent/Linux/install-powershell.sh b/tools/Crank/Agent/Linux/install-powershell.sh
new file mode 100755
index 0000000000..4775d96df2
--- /dev/null
+++ b/tools/Crank/Agent/Linux/install-powershell.sh
@@ -0,0 +1,18 @@
+#!/bin/bash
+
+# From https://docs.microsoft.com/powershell/scripting/install/installing-powershell-core-on-linux?view=powershell-7#ubuntu-1804
+
+# Download the Microsoft repository GPG keys
+wget -q https://packages.microsoft.com/config/ubuntu/18.04/packages-microsoft-prod.deb
+
+# Register the Microsoft repository GPG keys
+sudo dpkg -i packages-microsoft-prod.deb
+
+# Update the list of products
+sudo apt-get update
+
+# Enable the "universe" repositories
+sudo add-apt-repository universe
+
+# Install PowerShell
+sudo apt-get install -y powershell
diff --git a/tools/Crank/Agent/VmDeployment/create-resources.bicep b/tools/Crank/Agent/VmDeployment/create-resources.bicep
new file mode 100644
index 0000000000..52b1508921
--- /dev/null
+++ b/tools/Crank/Agent/VmDeployment/create-resources.bicep
@@ -0,0 +1,271 @@
+@description('The location of the resources')
+param location string = resourceGroup().location
+
+@description('The names of the virtual machines')
+param vmNames array
+
+@description('The admin username for the virtual machines')
+param adminUsername string
+
+@description('The admin password for the virtual machines')
+@secure()
+param adminPassword string
+
+@description('Base64 encoded JSON parameters')
+param parametersJsonBase64 string
+
+@description('Windows local admin username')
+param windowsLocalAdminUserName string
+
+@description('Base64 encoded Windows local admin password')
+param windowsLocalAdminPasswordBase64 string
+
+@description('The operating system type')
+param osType string = 'Windows'
+
+@description('The name of the virtual network')
+param virtualNetworkName string = 'shared-vnet'
+
+@description('The name of the subnet')
+param subnetName string = 'default'
+
+@description('The name of the network security group')
+param networkSecurityGroupName string = 'shared-nsg'
+
+@description('The name of the private DNS zone')
+param privateDnsZoneName string = 'shared.private'
+
+@description('The size of the virtual machines')
+param vmSize string
+
+@description('The type of the OS disk')
+param osDiskType string
+
+resource virtualNetwork 'Microsoft.Network/virtualNetworks@2021-02-01' = {
+ name: virtualNetworkName
+ location: location
+ properties: {
+ addressSpace: {
+ addressPrefixes: [
+ '10.0.0.0/16'
+ ]
+ }
+ subnets: [
+ {
+ name: subnetName
+ properties: {
+ addressPrefix: '10.0.0.0/24'
+ }
+ }
+ ]
+ }
+}
+
+resource networkSecurityGroup 'Microsoft.Network/networkSecurityGroups@2021-02-01' = {
+ name: networkSecurityGroupName
+ location: location
+ properties: {
+ securityRules: [
+ {
+ name: 'AllowInternetOutbound'
+ properties: {
+ priority: 100
+ protocol: '*'
+ access: 'Allow'
+ direction: 'Outbound'
+ sourceAddressPrefix: '*'
+ sourcePortRange: '*'
+ destinationAddressPrefix: 'Internet'
+ destinationPortRange: '*'
+ }
+ }
+ {
+ name: 'SSH'
+ properties: {
+ priority: 1000
+ protocol: 'TCP'
+ access: 'Allow'
+ direction: 'Inbound'
+ sourceAddressPrefix: '*'
+ sourcePortRange: '*'
+ destinationAddressPrefix: '*'
+ destinationPortRange: '22'
+ }
+ }
+ {
+ name: 'RDP'
+ properties: {
+ priority: 1001
+ protocol: 'TCP'
+ access: 'Allow'
+ direction: 'Inbound'
+ sourceAddressPrefix: '*'
+ sourcePortRange: '*'
+ destinationAddressPrefix: '*'
+ destinationPortRange: '3389'
+ }
+ }
+ {
+ name: 'DotNet-Crank'
+ properties: {
+ priority: 1011
+ protocol: '*'
+ access: 'Allow'
+ direction: 'Inbound'
+ sourceAddressPrefix: '*'
+ sourcePortRange: '*'
+ destinationAddressPrefix: '*'
+ destinationPortRange: '5010'
+ }
+ }
+ {
+ name: 'Benchmark-App'
+ properties: {
+ priority: 1012
+ protocol: '*'
+ access: 'Allow'
+ direction: 'Inbound'
+ sourceAddressPrefix: '*'
+ sourcePortRange: '*'
+ destinationAddressPrefix: '*'
+ destinationPortRange: '5000'
+ }
+ }
+ ]
+ }
+}
+
+resource publicIPAddresses 'Microsoft.Network/publicIPAddresses@2021-02-01' = [
+ for (vmName, index) in vmNames: {
+ name: '${vmName}-pip'
+ location: location
+ properties: {
+ publicIPAllocationMethod: 'Dynamic'
+ dnsSettings: {
+ domainNameLabel: vmName
+ }
+ }
+ }
+]
+
+resource networkInterfaces 'Microsoft.Network/networkInterfaces@2021-02-01' = [
+ for (vmName, index) in vmNames: {
+ name: '${vmName}-nic'
+ location: location
+ properties: {
+ ipConfigurations: [
+ {
+ name: 'ipconfig1'
+ properties: {
+ subnet: {
+ id: virtualNetwork.properties.subnets[0].id
+ }
+ privateIPAllocationMethod: 'Dynamic'
+ publicIPAddress: {
+ id: publicIPAddresses[index].id
+ }
+ }
+ }
+ ]
+ networkSecurityGroup: {
+ id: networkSecurityGroup.id
+ }
+ }
+ }
+]
+
+resource virtualMachines 'Microsoft.Compute/virtualMachines@2021-07-01' = [
+ for (vmName, index) in vmNames: {
+ name: vmName
+ location: location
+ properties: {
+ hardwareProfile: {
+ vmSize: vmSize
+ }
+ osProfile: {
+ computerName: vmName
+ adminUsername: adminUsername
+ adminPassword: adminPassword
+ }
+ storageProfile: {
+ imageReference: {
+ publisher: 'MicrosoftWindowsServer'
+ offer: 'WindowsServer'
+ sku: '2022-Datacenter'
+ version: 'latest'
+ }
+ osDisk: {
+ createOption: 'FromImage'
+ managedDisk: {
+ storageAccountType: osDiskType
+ }
+ }
+ }
+ networkProfile: {
+ networkInterfaces: [
+ {
+ id: networkInterfaces[index].id
+ }
+ ]
+ }
+ }
+ }
+]
+
+resource customScriptExtensions 'Microsoft.Compute/virtualMachines/extensions@2021-03-01' = [
+ for (vmName, index) in vmNames: if (osType == 'Windows') {
+ name: '${vmName}-bootstrap'
+ parent: virtualMachines[index]
+ location: location
+ properties: {
+ publisher: 'Microsoft.Compute'
+ type: 'CustomScriptExtension'
+ typeHandlerVersion: '1.10'
+ autoUpgradeMinorVersion: true
+ settings: {
+ fileUris: [
+ 'https://raw.githubusercontent.com/Azure/azure-functions-host/refs/heads/dev/tools/Crank/Agent/Windows/bootstrap.ps1'
+ ]
+ commandToExecute: 'powershell.exe -ExecutionPolicy Unrestricted -NoProfile -NonInteractive -File .\\bootstrap.ps1 -ParametersJsonBase64 ${parametersJsonBase64} -WindowsLocalAdminUserName ${windowsLocalAdminUserName} -WindowsLocalAdminPasswordBase64 ${windowsLocalAdminPasswordBase64}'
+ }
+ }
+ }
+]
+
+resource privateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = {
+ name: privateDnsZoneName
+ location: 'global'
+}
+
+resource virtualNetworkLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = {
+ name: '${privateDnsZoneName}-vnet-link'
+ parent: privateDnsZone
+ location: 'global'
+ properties: {
+ virtualNetwork: {
+ id: virtualNetwork.id
+ }
+ registrationEnabled: true
+ }
+}
+
+resource aRecords 'Microsoft.Network/privateDnsZones/A@2020-06-01' = [
+ for (vmName, index) in vmNames: {
+ name: '${vmName}.${privateDnsZoneName}'
+ parent: privateDnsZone
+ properties: {
+ ttl: 3600
+ aRecords: [
+ {
+ ipv4Address: networkInterfaces[index].properties.ipConfigurations[0].properties.privateIPAddress
+ }
+ ]
+ }
+ }
+]
+
+output adminUsername string = adminUsername
+output hostnames array = [for (vmName, index) in vmNames: publicIPAddresses[index].properties.dnsSettings.fqdn]
+output sshCommands array = [
+ for (vmName, index) in vmNames: 'ssh ${adminUsername}@${publicIPAddresses[index].properties.dnsSettings.fqdn}'
+]
diff --git a/tools/Crank/Agent/VmDeployment/deploy-vm.ps1 b/tools/Crank/Agent/VmDeployment/deploy-vm.ps1
new file mode 100644
index 0000000000..47aa900e45
--- /dev/null
+++ b/tools/Crank/Agent/VmDeployment/deploy-vm.ps1
@@ -0,0 +1,142 @@
+#!/usr/bin/env pwsh
+
+[CmdletBinding()]
+param (
+ [Parameter(Mandatory = $true)]
+ [string]
+ $SubscriptionName,
+
+ [Parameter(Mandatory = $true)]
+ [string]
+ $BaseName,
+
+ [string[]]
+ $NamePostfixes = @('-app','load'),
+
+ [Parameter(Mandatory = $true)]
+ [ValidateSet('Linux', 'Windows')]
+ $OsType,
+
+ [switch]
+ $Docker,
+
+ [string]
+ $VmSize = 'Standard_E2s_v3',
+
+ [string]
+ $OsDiskType = 'Premium_LRS',
+
+ [string]
+ $Location = 'West Central US',
+
+ [string]
+ $UserName = 'Functions',
+
+ [string]
+ $KeyVaultName = 'functions-perf-crank-kv',
+
+ [string]
+ $VaultResourceGroupName = 'FunctionsCrank',
+
+ [string]
+ $VaultSubscriptionName = 'Functions Build Infra'
+)
+
+$ErrorActionPreference = 'Stop'
+
+# Retrieve the Key Vault secret
+# Determine the secret name based on the OS type
+if ($OsType -eq 'Windows') {
+ $secretName = 'CrankAgentVMAdminPassword'
+} else {
+ $secretName = 'LinuxCrankAgentVmSshKey-Public'
+}
+$vaultSubscriptionId = (Get-AzSubscription -SubscriptionName $VaultSubscriptionName).Id
+$keyVault = Get-AzKeyVault -SubscriptionId $vaultSubscriptionId -ResourceGroupName $VaultResourceGroupName -VaultName $KeyVaultName
+
+if (-not $keyVault) {
+ throw "Key Vault '$KeyVaultName' not found in resource group '$VaultResourceGroupName' and SubscriptionId '$vaultSubscriptionId'"
+}
+
+$secret = Get-AzKeyVaultSecret -VaultName $KeyVaultName -Name $secretName -AsPlainText
+
+if (-not $secret) {
+ throw "Secret '$secretName' not found in Key Vault '$KeyVaultName'."
+}
+
+$resourceGroupName = "FunctionsCrank-$OsType-$BaseName-rg"
+
+# loop through $NamePostfixes array and create a new array called VMNamesArray.
+$VMNamesArray = [System.Collections.ArrayList]@()
+foreach ($postfix in $NamePostfixes) {
+ $agentVmName= "crank-$BaseName$postfix".ToLower()
+ # VM name must be less than 16 characters. If greater than 15, take the last 15 characters
+ if ($agentVmName.Length -gt 15) {
+ Write-Warning "VM name '$agentVmName' is greater than 15 characters. Truncating to 15 characters."
+ $agentVmName = $agentVmName.Substring($agentVmName.Length - 15)
+ }
+ $VMNamesArray.Add($agentVmName) | Out-Null
+}
+
+Set-AzContext -Subscription $SubscriptionName | Out-Null
+
+New-AzResourceGroup -Name $resourceGroupName -Location $Location | Out-Null
+
+$adminPassword = $secret
+$adminPasswordBase64 = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($adminPassword))
+
+$customScriptParameters = @{
+ CrankBranch = 'main'
+ Docker = $Docker.IsPresent
+}
+
+# Convert custom script parameters to JSON and then to base64
+$parametersJson = $customScriptParameters | ConvertTo-Json -Compress
+$parametersJsonBase64 = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($parametersJson))
+
+$parameters = @{
+ location = $Location
+ vmNames = $VMNamesArray
+ vmSize = $VmSize
+ osDiskType = $OsDiskType
+ osType = $OsType
+ adminUsername = $UserName
+ adminPassword = $adminPassword
+ windowsLocalAdminUserName = $UserName
+ windowsLocalAdminPasswordBase64 = $adminPasswordBase64
+ parametersJsonBase64 = $parametersJsonBase64
+}
+
+# Retry logic for deployment
+$maxRetries = 1
+$retryCount = 0
+$retryDelay = 30
+
+while ($retryCount -lt $maxRetries) {
+ try {
+ # Deploy the resources using the Bicep template
+ New-AzResourceGroupDeployment `
+ -ResourceGroupName $resourceGroupName `
+ -TemplateFile "$PSScriptRoot\create-resources.bicep" `
+ -TemplateParameterObject $parameters `
+ -Verbose
+ break
+ } catch {
+ Write-Error "Deployment failed: $_"
+ $retryCount++
+ if ($retryCount -lt $maxRetries) {
+ Write-Verbose "Retrying deployment in $retryDelay seconds... (Attempt $retryCount of $maxRetries)"
+ Start-Sleep -Seconds $retryDelay
+ } else {
+ throw "Deployment failed after $maxRetries attempts."
+ }
+ }
+}
+
+Write-Host "Restarting $($VMNamesArray.Count) virtual machine(s)"
+# Restart all VMs in $VMNamesArray
+foreach ($vmName in $VMNamesArray) {
+ Restart-AzVM -ResourceGroupName $resourceGroupName -Name $vmName | Out-Null
+ Write-Host "Restart request initiated for $vmName. Please wait a few minutes for the VM to complete the restart process."
+ Start-Sleep -Seconds 30
+}
\ No newline at end of file
diff --git a/tools/Crank/Agent/VmDeployment/deploy.ps1 b/tools/Crank/Agent/VmDeployment/deploy.ps1
new file mode 100644
index 0000000000..72aa725645
--- /dev/null
+++ b/tools/Crank/Agent/VmDeployment/deploy.ps1
@@ -0,0 +1,51 @@
+#!/usr/bin/env pwsh
+
+[CmdletBinding()]
+param (
+ [Parameter(Mandatory = $true)]
+ [string]
+ $SubscriptionName,
+
+ [Parameter(Mandatory = $true)]
+ [string]
+ $BaseName,
+
+ [string[]]
+ $NamePostfixes = @('-app', '-load'),
+
+ [Parameter(Mandatory = $true)]
+ [ValidateSet('Linux', 'Windows')]
+ $OsType,
+
+ [switch]
+ $Docker,
+
+ [string]
+ $VmSize = 'Standard_E2s_v3',
+
+ [string]
+ $OsDiskType = 'Premium_LRS',
+
+ [string]
+ $Location = 'West Central US',
+
+ [string]
+ $UserName = 'Functions'
+)
+
+$ErrorActionPreference = 'Stop'
+
+& "$PSScriptRoot/deploy-vm.ps1" `
+ -SubscriptionName $SubscriptionName `
+ -BaseName $BaseName `
+ -NamePostfixes $NamePostfixes `
+ -OsType $OsType `
+ -VmSize $VmSize `
+ -OsDiskType $OsDiskType `
+ -Location $Location `
+ -UserName $UserName `
+ -Verbose:$VerbosePreference
+
+# TODO: remove this warning when app deployment is automated
+$appPath = if ($OsType -eq 'Linux') { "/home/$UserName/FunctionApps" } else { 'C:\FunctionApps' }
+Write-Warning "Remember to deploy the Function apps to $appPath"
diff --git a/tools/Crank/Agent/Windows/bootstrap.ps1 b/tools/Crank/Agent/Windows/bootstrap.ps1
new file mode 100644
index 0000000000..31f178b2c8
--- /dev/null
+++ b/tools/Crank/Agent/Windows/bootstrap.ps1
@@ -0,0 +1,135 @@
+param(
+ [string]$ParametersJsonBase64,
+ [string]$WindowsLocalAdminUserName,
+ [string]$WindowsLocalAdminPasswordBase64)
+
+$ErrorActionPreference = 'Stop'
+
+# The user should have "Log on as a service" right to run psexec".
+
+$tempFilePath = "C:\temp\secpol.cfg"
+
+if (!(Test-Path -Path "C:\temp")) { New-Item -Path "C:\temp" -ItemType Directory | Out-Null }
+
+try {
+ # Export current security policy
+ secedit /export /cfg $tempFilePath
+ if (!(Test-Path -Path $tempFilePath)) { throw "Failed to export security policy." }
+
+ # Read and update 'SeServiceLogonRight'
+ $content = Get-Content $tempFilePath
+ $entry = $content | Where-Object { $_ -match "^SeServiceLogonRight\s*=" }
+ $updatedContent = if ($entry) {
+ $values = ($entry -split "=")[1].Trim()
+ if ($values -notmatch "\b$WindowsLocalAdminUserName\b") { $content -replace "^SeServiceLogonRight\s*=.*", "SeServiceLogonRight = $values,$WindowsLocalAdminUserName" } else { return }
+ }
+ else { $content + "`r`nSeServiceLogonRight = $WindowsLocalAdminUserName" }
+
+ # Apply updated security policy
+ $updatedContent | Set-Content $tempFilePath
+ secedit /configure /db secedit.sdb /cfg $tempFilePath /areas USER_RIGHTS
+ Write-Host "Successfully added 'Log on as a service' right for user '$WindowsLocalAdminUserName'." -ForegroundColor Green
+}
+catch {
+ Write-Host "An error occurred: $_" -ForegroundColor Red
+}
+finally {
+ if (Test-Path -Path $tempFilePath) { Remove-Item -Path $tempFilePath -Force }
+}
+
+# Install chocolatey
+Set-ExecutionPolicy Bypass -Scope Process -Force
+[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072
+Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))
+$chocoCmd = "$env:ProgramData\chocolatey\bin\choco.exe"
+
+& $chocoCmd install -y git
+$env:PATH += ";$env:ProgramFiles\Git\cmd"
+
+& $chocoCmd install -y powershell-core
+$env:PATH += ";$env:ProgramFiles\PowerShell\7"
+
+& $chocoCmd install -y sysinternals --version 2024.12.16
+
+& $chocoCmd install -y dotnet-sdk --version="8.0.404"
+& $chocoCmd install -y dotnet-sdk --version="9.0.101"
+
+# Clone azure-functions-host repo
+$githubPath = 'C:\github'
+New-Item -Path $githubPath -ItemType Directory
+Set-Location -Path $githubPath
+& git clone https://github.com/Azure/azure-functions-host.git
+Set-Location -Path azure-functions-host
+
+$originalLocation = Get-Location
+# Publish dotnet function apps.
+$benchmarkAppsPath = "$githubPath\azure-functions-host\tools\Crank\BenchmarkApps\Dotnet";
+$tempDirectory = 'C:\temp\BenchmarkApps'
+$publishOutputRootDirectory = 'C:\FunctionApps'
+New-Item -Path $publishOutputRootDirectory -ItemType Directory -Force
+
+# copy the apps to temp directory for publishing so that the host global.json .NET SDK version doesn't interfere with the publish.
+Copy-Item -Path $benchmarkAppsPath -Destination $tempDirectory -Recurse
+
+$directories = Get-ChildItem -Path $tempDirectory -Directory
+
+# Define the log file path
+$logFilePath = "$publishOutputRootDirectory\publish.log"
+
+# Function to write log messages to a file
+function Write-Log {
+ param (
+ [string]$message
+ )
+ $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
+ $logMessage = "$timestamp - $message"
+ Add-Content -Path $logFilePath -Value $logMessage
+}
+
+Write-Log "Child directory count inside $benchmarkAppsPath : $($directories.Count)"
+
+# Loop through each directory and publish the app
+foreach ($dir in $directories) {
+ $appName = $dir.Name
+ Write-Log "Processing $appName"
+
+ try {
+ # Find the .csproj or .sln file within the directory
+ $projectFile = Get-ChildItem -Path $dir.FullName -Filter *.csproj -Recurse -File | Select-Object -First 1
+ if (-not $projectFile) {
+ Write-Log "No project file (.csproj) found in $appName"
+ Write-Host "No project file (.csproj) found in $appName"
+ continue
+ }
+
+ $publishOutputDir = Join-Path -Path $publishOutputRootDirectory -ChildPath "$appName/site/wwwroot"
+ Write-Host "Publishing $($projectFile.FullName) to $publishOutputDir"
+ Write-Log "Publishing $($projectFile.FullName) to $publishOutputDir"
+
+ Set-Location -Path (Split-Path -Path $projectFile.FullName -Parent)
+ # Publish the app
+ dotnet publish -c Release -o $publishOutputDir
+
+ Write-Log "Successfully published $appName"
+ }
+ catch {
+ Write-Log "Failed to publish $appName. Error: $_"
+ Write-Host "Failed to publish $appName. Error: $_"
+ }
+}
+Set-Location -Path $originalLocation
+
+# Setup Crank agent
+$plaintextPassword = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($WindowsLocalAdminPasswordBase64))
+
+psexec -accepteula -h -u $WindowsLocalAdminUserName -p $plaintextPassword `
+ pwsh.exe -NoProfile -NonInteractive `
+ -File "$githubPath\azure-functions-host\tools\Crank\Agent\setup-crank-agent-raw.ps1" `
+ -ParametersJsonBase64 $ParametersJsonBase64 `
+ -WindowsLocalAdminUserName $WindowsLocalAdminUserName `
+ -WindowsLocalAdminPasswordBase64 $WindowsLocalAdminPasswordBase64 `
+ -Verbose
+
+if (-not $?) {
+ throw "psexec exit code: $LASTEXITCODE"
+}
diff --git a/tools/Crank/Agent/run-crank-agent.ps1 b/tools/Crank/Agent/run-crank-agent.ps1
new file mode 100755
index 0000000000..414b57aeb1
--- /dev/null
+++ b/tools/Crank/Agent/run-crank-agent.ps1
@@ -0,0 +1,26 @@
+#!/usr/bin/env pwsh
+
+param(
+ [string]
+ $UserName = 'Functions'
+)
+
+$ErrorActionPreference = 'Stop'
+
+$logsDir = $IsWindows ? 'C:\crank-agent-logs' : "/home/$UserName/crank-agent-logs"
+if (-not (Test-Path $logsDir -PathType Container)) {
+ New-Item -Path $logsDir -ItemType Container > $null
+}
+
+$logFileName = Join-Path -Path $logsDir -ChildPath "$(Get-Date -Format 'yyyy-MM-dd_HH-mm-ss').log"
+New-Item -Path $logFileName -ItemType File > $null
+
+$stableLogFileName = Join-Path -Path $logsDir -ChildPath 'current.log'
+if (Test-Path $stableLogFileName) {
+ Remove-Item $stableLogFileName
+}
+New-Item -ItemType SymbolicLink -Path $stableLogFileName -Target $logFileName > $null
+
+$invokeCrankAgentCommand = $IsWindows ? 'C:\dotnet-tools\crank-agent.exe' : "/home/$UserName/.dotnet/tools/crank-agent";
+
+& $invokeCrankAgentCommand 2>&1 >> $logFileName
diff --git a/tools/Crank/Agent/setup-crank-agent-raw.ps1 b/tools/Crank/Agent/setup-crank-agent-raw.ps1
new file mode 100644
index 0000000000..6070075cd1
--- /dev/null
+++ b/tools/Crank/Agent/setup-crank-agent-raw.ps1
@@ -0,0 +1,27 @@
+#!/usr/bin/env pwsh
+
+[CmdletBinding()]
+param (
+ [string]$ParametersJsonBase64,
+ [string]$WindowsLocalAdminUserName,
+ [string]$WindowsLocalAdminPasswordBase64 # not a SecureString because we'll need to pass it via pwsh command line args
+)
+
+function GetWindowsLocalAdminCredential {
+ $plaintextPassword = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($WindowsLocalAdminPasswordBase64))
+ $securePassword = ConvertTo-SecureString -String $plaintextPassword -AsPlainText -Force
+ [pscredential]::new($WindowsLocalAdminUserName, $securePassword)
+}
+
+$ErrorActionPreference = 'Stop'
+
+$parametersJson = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($ParametersJsonBase64))
+
+Write-Verbose "setup-crank-agent-raw.ps1: `$parametersJson: '$parametersJson' $WindowsLocalAdminUserName ***" -Verbose
+
+$parameters = @{}
+($parametersJson | ConvertFrom-Json).PSObject.Properties | ForEach-Object { $parameters[$_.Name] = $_.Value }
+
+$credential = $IsWindows ? @{ WindowsLocalAdmin = GetWindowsLocalAdminCredential } : @{ }
+
+& "$PSScriptRoot/setup-crank-agent.ps1" @parameters @credential -Verbose
diff --git a/tools/Crank/Agent/setup-crank-agent.ps1 b/tools/Crank/Agent/setup-crank-agent.ps1
new file mode 100755
index 0000000000..ab057413da
--- /dev/null
+++ b/tools/Crank/Agent/setup-crank-agent.ps1
@@ -0,0 +1,237 @@
+#!/usr/bin/env pwsh
+
+[CmdletBinding()]
+param (
+ [bool]$InstallDotNet = $false,
+ [bool]$InstallCrankAgent = $true,
+ [string]$CrankBranch,
+ [bool]$Docker = $false,
+ [pscredential]$WindowsLocalAdmin
+)
+
+$ErrorActionPreference = 'Stop'
+
+#region Utilities
+
+function BuildCrankAgent($CrankRepoPath) {
+ Push-Location $CrankRepoPath
+ try {
+ $logFileName = 'build.log'
+ Write-Verbose "Building crank (see $(Join-Path -Path $PWD -ChildPath $logFileName))..."
+ $buildCommand = $IsWindows ? '.\build.cmd' : './build.sh'
+ & $buildCommand -configuration Release -pack > $logFileName
+ if (-not $?) {
+ throw "Crank build failed, exit code: $LASTEXITCODE"
+ }
+
+ Join-Path -Path $PWD -ChildPath "artifacts/packages/Release/Shipping"
+ } finally {
+ Pop-Location
+ }
+}
+
+function GetDotNetToolsLocationArgs {
+ $IsWindows ? ('--tool-path', 'c:\dotnet-tools') : '-g'
+}
+
+function InstallCrankAgentTool($LocalPackageSource) {
+
+ Write-Verbose 'Installing crank-agent tool...'
+
+ Write-Verbose 'Stopping crank-agent...'
+
+ $crankAgentProcessName = 'crank-agent'
+ if (Get-Process -Name $crankAgentProcessName -ErrorAction SilentlyContinue) {
+ Stop-Process -Name $crankAgentProcessName -Force
+ }
+
+ Write-Verbose 'Uninstalling crank-agent...'
+
+ $uninstallArgs = 'tool', 'uninstall', 'Microsoft.Crank.Agent'
+ $uninstallArgs += GetDotNetToolsLocationArgs
+ & dotnet $uninstallArgs
+
+ Write-Verbose 'Installing crank-agent...'
+
+ $installArgs =
+ 'tool', 'install', 'Microsoft.Crank.Agent',
+ '--version', '0.2.0-*'
+
+ $installArgs += GetDotNetToolsLocationArgs
+
+ if ($LocalPackageSource) {
+ $installArgs += '--add-source', $LocalPackageSource
+ }
+
+ Write-Verbose "Invoking dotnet with arguments: $installArgs"
+ & dotnet $installArgs
+}
+
+function EnsureDirectoryExists($Path) {
+ if (-not (Test-Path -PathType Container -Path $Path)) {
+ New-Item -ItemType Directory -Path $Path | Out-Null
+ }
+}
+
+function CloneCrankRepo {
+ Write-Verbose "Cloning crank repo..."
+ $githubParent = $IsLinux ? '~' : 'C:\'
+ $githubPath = Join-Path -Path $githubParent -ChildPath 'github'
+ EnsureDirectoryExists $githubPath
+ Push-Location -Path $githubPath
+ try {
+ git clone https://github.com/dotnet/crank.git | Out-Null
+ Set-Location crank
+ if ($CrankBranch) {
+ git checkout $CrankBranch | Out-Null
+ }
+ $PWD.Path
+ } finally {
+ Pop-Location
+ }
+}
+
+function InstallCrankAgent {
+ if ($Docker) {
+ $crankRepoPath = CloneCrankRepo
+ Push-Location $crankRepoPath/docker/agent
+ try {
+ # Build the docker-agent image
+ ./build.sh
+
+ # Build the functions-docker-agent image
+ Set-Location $PSScriptRoot/Linux/Docker
+ ./build.sh
+ } finally {
+ Pop-Location
+ }
+ } else {
+ InstallCrankAgentTool
+ }
+
+ if ($IsWindows) {
+ New-NetFirewallRule -DisplayName 'Crank Agent' -Group 'Crank' -LocalPort 5010 -Protocol TCP -Direction Inbound -Action Allow | Out-Null
+ New-NetFirewallRule -DisplayName 'Crank App & Load (inbound)' -Group 'Crank' -LocalPort 5000 -Protocol TCP -Direction Inbound -Action Allow | Out-Null
+ New-NetFirewallRule -DisplayName 'Crank App & Load (outbound)' -Group 'Crank' -LocalPort 5000 -Protocol TCP -Direction Outbound -Action Allow | Out-Null
+ }
+}
+
+function ScheduleCrankAgentAsWindowsService {
+ param (
+ [string]$ServiceName = "CrankAgentService"
+ )
+
+ $logPath = "C:\crank-agent-logs"
+ $binPath = "`"C:\dotnet-tools\crank-agent.exe`" --service --log-path=$logPath"
+
+ try {
+ # Create the log directory if it doesn't exist
+ if (-not (Test-Path -Path $logPath)) {
+ New-Item -Path $logPath -ItemType Directory
+ }
+
+ # Disable real-time monitoring and exclude crank-agent.exe, functions host from Defender scans
+ Set-MpPreference -DisableRealtimeMonitoring $true
+ Add-MpPreference -ExclusionProcess 'crank-agent.exe'
+ Add-MpPreference -ExclusionProcess 'Microsoft.Azure.WebJobs.Script.WebHost.exe'
+
+ # Create the service
+ sc.exe create $ServiceName binpath= $binPath
+ sc.exe config $ServiceName start= auto
+
+ # Verify the service creation
+ sc.exe qc $ServiceName
+
+ # Start the service
+ sc.exe start $ServiceName
+ Write-Host "Service '$ServiceName' started successfully."
+ } catch {
+ Write-Error "An error occurred while creating, querying, or starting the service: $_"
+ }
+}
+
+function ScheduleCrankAgentStartWindows($RunScriptPath, [pscredential]$Credential) {
+ $taskName = 'CrankAgent'
+
+ if (Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue) {
+ Write-Warning "Task '$taskName' already exists, no changes performed"
+ } else {
+ $action = New-ScheduledTaskAction -Execute 'pwsh.exe' `
+ -Argument "-NoProfile -WindowStyle Hidden -File $RunScriptPath"
+
+ $trigger = New-ScheduledTaskTrigger -AtStartup
+
+ $auth =
+ if ($Credential) {
+ @{
+ User = $Credential.UserName
+ Password = $Credential.GetNetworkCredential().Password
+ }
+ } else {
+ @{
+ Principal = New-ScheduledTaskPrincipal -UserID "NT AUTHORITY\NETWORKSERVICE" `
+ -LogonType ServiceAccount -RunLevel Highest
+ }
+ }
+
+ $null = Register-ScheduledTask `
+ -TaskName $taskName -Description "Start crank-agent" `
+ -Action $action -Trigger $trigger `
+ @auth
+ }
+}
+
+function ScheduleCrankAgentStartLinux($RunScriptPath) {
+ $currentCrontabContent = (crontab -l) ?? $null
+ if ($currentCrontabContent -match '\bcrank-agent\b') {
+ Write-Warning "crank-agent reference is found in crontab, no changes performed"
+ } else {
+ $currentCrontabContent, "@reboot $RunScriptPath" | crontab -
+ }
+}
+
+function ScheduleCrankAgentStart {
+ if ($Docker) {
+ Write-Verbose 'Starting crank-agent...'
+
+ $functionAppsPath = Join-Path -Path '~' -ChildPath 'FunctionApps'
+ EnsureDirectoryExists -Path $functionAppsPath
+ $helloAppPath = Join-Path -Path $functionAppsPath -ChildPath 'HelloApp'
+ EnsureDirectoryExists -Path $helloAppPath
+
+ & "$PSScriptRoot/Linux/Docker/run.sh"
+ } else {
+ Write-Verbose 'Scheduling crank-agent start...'
+
+ $scriptPath = Join-Path -Path (Split-Path $PSCommandPath -Parent) -ChildPath 'run-crank-agent.ps1'
+
+ if ($IsWindows) {
+ ScheduleCrankAgentAsWindowsService
+ } else {
+ ScheduleCrankAgentStartLinux -RunScriptPath $scriptPath
+ }
+
+ Write-Warning 'Please reboot to start crank-agent'
+ }
+}
+
+function InstallDocker {
+ Write-Verbose 'Installing Docker...'
+ if ($IsWindows) {
+ throw 'Using Docker on Windows is not supported yet'
+ } else {
+ & "$PSScriptRoot/Linux/install-docker.sh"
+ }
+}
+
+#endregion
+
+#region Main
+
+Write-Verbose "WindowsLocalAdmin: '$($WindowsLocalAdmin.UserName)'"
+
+if ($Docker) { InstallDocker }
+if ($InstallCrankAgent) { InstallCrankAgent }
+ScheduleCrankAgentStart
+
+#endregion
diff --git a/tools/Crank/BenchmarkApps/AspNetBenchmark/AspNetBenchmark.sln b/tools/Crank/BenchmarkApps/AspNetBenchmark/AspNetBenchmark.sln
new file mode 100644
index 0000000000..fcfc5de3fe
--- /dev/null
+++ b/tools/Crank/BenchmarkApps/AspNetBenchmark/AspNetBenchmark.sln
@@ -0,0 +1,22 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.12.35527.113 d17.12
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNetBenchmark", "AspNetBenchmark\AspNetBenchmark.csproj", "{DE9DA551-1E9C-415C-B000-0C5C3D767A65}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {DE9DA551-1E9C-415C-B000-0C5C3D767A65}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {DE9DA551-1E9C-415C-B000-0C5C3D767A65}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {DE9DA551-1E9C-415C-B000-0C5C3D767A65}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {DE9DA551-1E9C-415C-B000-0C5C3D767A65}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+EndGlobal
diff --git a/tools/Crank/BenchmarkApps/AspNetBenchmark/AspNetBenchmark/AspNetBenchmark.csproj b/tools/Crank/BenchmarkApps/AspNetBenchmark/AspNetBenchmark/AspNetBenchmark.csproj
new file mode 100644
index 0000000000..10faf6ba00
--- /dev/null
+++ b/tools/Crank/BenchmarkApps/AspNetBenchmark/AspNetBenchmark/AspNetBenchmark.csproj
@@ -0,0 +1,17 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tools/Crank/BenchmarkApps/AspNetBenchmark/AspNetBenchmark/Controllers/HelloController.cs b/tools/Crank/BenchmarkApps/AspNetBenchmark/AspNetBenchmark/Controllers/HelloController.cs
new file mode 100644
index 0000000000..331fa55951
--- /dev/null
+++ b/tools/Crank/BenchmarkApps/AspNetBenchmark/AspNetBenchmark/Controllers/HelloController.cs
@@ -0,0 +1,15 @@
+using Microsoft.AspNetCore.Mvc;
+
+namespace AspNetBenchmark.Controllers
+{
+ [Route("api/[controller]")]
+ [ApiController]
+ public class HelloController : ControllerBase
+ {
+ [HttpGet]
+ public IActionResult Hello()
+ {
+ return new OkObjectResult("Hello from ASP.NET!");
+ }
+ }
+}
diff --git a/tools/Crank/BenchmarkApps/AspNetBenchmark/AspNetBenchmark/Program.cs b/tools/Crank/BenchmarkApps/AspNetBenchmark/AspNetBenchmark/Program.cs
new file mode 100644
index 0000000000..54453a90aa
--- /dev/null
+++ b/tools/Crank/BenchmarkApps/AspNetBenchmark/AspNetBenchmark/Program.cs
@@ -0,0 +1,18 @@
+namespace AspNetBenchmark
+{
+ public class Program
+ {
+ public static void Main(string[] args)
+ {
+ var builder = WebApplication.CreateBuilder(args);
+
+ builder.Services.AddControllers();
+
+ var app = builder.Build();
+
+ app.MapControllers();
+
+ app.Run();
+ }
+ }
+}
diff --git a/tools/Crank/BenchmarkApps/DotNet/HelloHttpNet8/Hello.cs b/tools/Crank/BenchmarkApps/DotNet/HelloHttpNet8/Hello.cs
new file mode 100644
index 0000000000..434ac79fde
--- /dev/null
+++ b/tools/Crank/BenchmarkApps/DotNet/HelloHttpNet8/Hello.cs
@@ -0,0 +1,24 @@
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Azure.Functions.Worker;
+using Microsoft.Extensions.Logging;
+
+namespace HelloHttp
+{
+ public sealed class Hello
+ {
+ private readonly ILogger _logger;
+
+ public Hello(ILogger logger)
+ {
+ _logger = logger;
+ }
+
+ [Function("Hello")]
+ public IActionResult Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequest req)
+ {
+ _logger.LogInformation("C# HTTP trigger function processed a request.");
+ return new OkObjectResult("Welcome to Azure Functions!");
+ }
+ }
+}
diff --git a/tools/Crank/BenchmarkApps/DotNet/HelloHttpNet8/HelloHttp.csproj b/tools/Crank/BenchmarkApps/DotNet/HelloHttpNet8/HelloHttp.csproj
new file mode 100644
index 0000000000..f49a6861ab
--- /dev/null
+++ b/tools/Crank/BenchmarkApps/DotNet/HelloHttpNet8/HelloHttp.csproj
@@ -0,0 +1,31 @@
+
+
+ net8.0
+ v4
+ Exe
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+ Never
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tools/Crank/BenchmarkApps/DotNet/HelloHttpNet8/Program.cs b/tools/Crank/BenchmarkApps/DotNet/HelloHttpNet8/Program.cs
new file mode 100644
index 0000000000..770df503bd
--- /dev/null
+++ b/tools/Crank/BenchmarkApps/DotNet/HelloHttpNet8/Program.cs
@@ -0,0 +1,13 @@
+using Microsoft.Azure.Functions.Worker.Builder;
+using Microsoft.Extensions.Hosting;
+
+var builder = FunctionsApplication.CreateBuilder(args);
+
+builder.ConfigureFunctionsWebApplication();
+
+// Application Insights isn't enabled by default. See https://aka.ms/AAt8mw4.
+// builder.Services
+// .AddApplicationInsightsTelemetryWorkerService()
+// .ConfigureFunctionsApplicationInsights();
+
+builder.Build().Run();
diff --git a/tools/Crank/BenchmarkApps/DotNet/HelloHttpNet8/global.json b/tools/Crank/BenchmarkApps/DotNet/HelloHttpNet8/global.json
new file mode 100644
index 0000000000..869f895289
--- /dev/null
+++ b/tools/Crank/BenchmarkApps/DotNet/HelloHttpNet8/global.json
@@ -0,0 +1,6 @@
+{
+ "sdk": {
+ "rollForward": "latestFeature",
+ "version": "8.0.100"
+ }
+}
\ No newline at end of file
diff --git a/tools/Crank/BenchmarkApps/DotNet/HelloHttpNet8/host.json b/tools/Crank/BenchmarkApps/DotNet/HelloHttpNet8/host.json
new file mode 100644
index 0000000000..ee5cf5f83f
--- /dev/null
+++ b/tools/Crank/BenchmarkApps/DotNet/HelloHttpNet8/host.json
@@ -0,0 +1,12 @@
+{
+ "version": "2.0",
+ "logging": {
+ "applicationInsights": {
+ "samplingSettings": {
+ "isEnabled": true,
+ "excludedTypes": "Request"
+ },
+ "enableLiveMetricsFilters": true
+ }
+ }
+}
\ No newline at end of file
diff --git a/tools/Crank/BenchmarkApps/DotNet/HelloHttpNet9/Hello.cs b/tools/Crank/BenchmarkApps/DotNet/HelloHttpNet9/Hello.cs
new file mode 100644
index 0000000000..434ac79fde
--- /dev/null
+++ b/tools/Crank/BenchmarkApps/DotNet/HelloHttpNet9/Hello.cs
@@ -0,0 +1,24 @@
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Azure.Functions.Worker;
+using Microsoft.Extensions.Logging;
+
+namespace HelloHttp
+{
+ public sealed class Hello
+ {
+ private readonly ILogger _logger;
+
+ public Hello(ILogger logger)
+ {
+ _logger = logger;
+ }
+
+ [Function("Hello")]
+ public IActionResult Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequest req)
+ {
+ _logger.LogInformation("C# HTTP trigger function processed a request.");
+ return new OkObjectResult("Welcome to Azure Functions!");
+ }
+ }
+}
diff --git a/tools/Crank/BenchmarkApps/DotNet/HelloHttpNet9/HelloHttp.csproj b/tools/Crank/BenchmarkApps/DotNet/HelloHttpNet9/HelloHttp.csproj
new file mode 100644
index 0000000000..441c720a57
--- /dev/null
+++ b/tools/Crank/BenchmarkApps/DotNet/HelloHttpNet9/HelloHttp.csproj
@@ -0,0 +1,31 @@
+
+
+ net9.0
+ v4
+ Exe
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+ Never
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tools/Crank/BenchmarkApps/DotNet/HelloHttpNet9/Program.cs b/tools/Crank/BenchmarkApps/DotNet/HelloHttpNet9/Program.cs
new file mode 100644
index 0000000000..770df503bd
--- /dev/null
+++ b/tools/Crank/BenchmarkApps/DotNet/HelloHttpNet9/Program.cs
@@ -0,0 +1,13 @@
+using Microsoft.Azure.Functions.Worker.Builder;
+using Microsoft.Extensions.Hosting;
+
+var builder = FunctionsApplication.CreateBuilder(args);
+
+builder.ConfigureFunctionsWebApplication();
+
+// Application Insights isn't enabled by default. See https://aka.ms/AAt8mw4.
+// builder.Services
+// .AddApplicationInsightsTelemetryWorkerService()
+// .ConfigureFunctionsApplicationInsights();
+
+builder.Build().Run();
diff --git a/tools/Crank/BenchmarkApps/DotNet/HelloHttpNet9/global.json b/tools/Crank/BenchmarkApps/DotNet/HelloHttpNet9/global.json
new file mode 100644
index 0000000000..9ef7f87523
--- /dev/null
+++ b/tools/Crank/BenchmarkApps/DotNet/HelloHttpNet9/global.json
@@ -0,0 +1,6 @@
+{
+ "sdk": {
+ "rollForward": "latestFeature",
+ "version": "9.0.100"
+ }
+}
\ No newline at end of file
diff --git a/tools/Crank/BenchmarkApps/DotNet/HelloHttpNet9/host.json b/tools/Crank/BenchmarkApps/DotNet/HelloHttpNet9/host.json
new file mode 100644
index 0000000000..ee5cf5f83f
--- /dev/null
+++ b/tools/Crank/BenchmarkApps/DotNet/HelloHttpNet9/host.json
@@ -0,0 +1,12 @@
+{
+ "version": "2.0",
+ "logging": {
+ "applicationInsights": {
+ "samplingSettings": {
+ "isEnabled": true,
+ "excludedTypes": "Request"
+ },
+ "enableLiveMetricsFilters": true
+ }
+ }
+}
\ No newline at end of file
diff --git a/tools/Crank/BenchmarkApps/DotNet/HelloHttpNet9NoProxy/Hello.cs b/tools/Crank/BenchmarkApps/DotNet/HelloHttpNet9NoProxy/Hello.cs
new file mode 100644
index 0000000000..8fcee28345
--- /dev/null
+++ b/tools/Crank/BenchmarkApps/DotNet/HelloHttpNet9NoProxy/Hello.cs
@@ -0,0 +1,30 @@
+using System.Net;
+using Microsoft.Azure.Functions.Worker;
+using Microsoft.Azure.Functions.Worker.Http;
+using Microsoft.Extensions.Logging;
+
+namespace Company.Function
+{
+ public class HelloHttp
+ {
+ private readonly ILogger _logger;
+
+ public HelloHttp(ILoggerFactory loggerFactory)
+ {
+ _logger = loggerFactory.CreateLogger();
+ }
+
+ [Function("HelloHttp")]
+ public HttpResponseData Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestData req)
+ {
+ _logger.LogInformation("C# HTTP trigger function processed a request.");
+
+ var response = req.CreateResponse(HttpStatusCode.OK);
+ response.Headers.Add("Content-Type", "text/plain; charset=utf-8");
+
+ response.WriteString("Welcome to Azure Functions!");
+
+ return response;
+ }
+ }
+}
\ No newline at end of file
diff --git a/tools/Crank/BenchmarkApps/DotNet/HelloHttpNet9NoProxy/HelloHttp.csproj b/tools/Crank/BenchmarkApps/DotNet/HelloHttpNet9NoProxy/HelloHttp.csproj
new file mode 100644
index 0000000000..e54a899449
--- /dev/null
+++ b/tools/Crank/BenchmarkApps/DotNet/HelloHttpNet9NoProxy/HelloHttp.csproj
@@ -0,0 +1,27 @@
+
+
+ net9.0
+ v4
+ Exe
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+ Never
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tools/Crank/BenchmarkApps/DotNet/HelloHttpNet9NoProxy/Program.cs b/tools/Crank/BenchmarkApps/DotNet/HelloHttpNet9NoProxy/Program.cs
new file mode 100644
index 0000000000..51336f3d1b
--- /dev/null
+++ b/tools/Crank/BenchmarkApps/DotNet/HelloHttpNet9NoProxy/Program.cs
@@ -0,0 +1,7 @@
+using Microsoft.Extensions.Hosting;
+
+var host = new HostBuilder()
+ .ConfigureFunctionsWorkerDefaults()
+ .Build();
+
+host.Run();
diff --git a/tools/Crank/BenchmarkApps/DotNet/HelloHttpNet9NoProxy/global.json b/tools/Crank/BenchmarkApps/DotNet/HelloHttpNet9NoProxy/global.json
new file mode 100644
index 0000000000..9ef7f87523
--- /dev/null
+++ b/tools/Crank/BenchmarkApps/DotNet/HelloHttpNet9NoProxy/global.json
@@ -0,0 +1,6 @@
+{
+ "sdk": {
+ "rollForward": "latestFeature",
+ "version": "9.0.100"
+ }
+}
\ No newline at end of file
diff --git a/tools/Crank/BenchmarkApps/DotNet/HelloHttpNet9NoProxy/host.json b/tools/Crank/BenchmarkApps/DotNet/HelloHttpNet9NoProxy/host.json
new file mode 100644
index 0000000000..ee5cf5f83f
--- /dev/null
+++ b/tools/Crank/BenchmarkApps/DotNet/HelloHttpNet9NoProxy/host.json
@@ -0,0 +1,12 @@
+{
+ "version": "2.0",
+ "logging": {
+ "applicationInsights": {
+ "samplingSettings": {
+ "isEnabled": true,
+ "excludedTypes": "Request"
+ },
+ "enableLiveMetricsFilters": true
+ }
+ }
+}
\ No newline at end of file
diff --git a/tools/Crank/benchmarks.yml b/tools/Crank/benchmarks.yml
new file mode 100644
index 0000000000..e7d91c3462
--- /dev/null
+++ b/tools/Crank/benchmarks.yml
@@ -0,0 +1,78 @@
+imports:
+ - https://raw.githubusercontent.com/dotnet/crank/master/src/Microsoft.Crank.Jobs.Bombardier/bombardier.yml
+
+jobs:
+ functionsServer:
+ source:
+ repository: https://github.com/Azure/azure-functions-host
+ branchOrCommit: "{{ BranchOrCommit }}"
+ project: src/WebJobs.Script.WebHost/WebJobs.Script.WebHost.csproj
+ readyStateText: Application started.
+ aspnetServer:
+ source:
+ repository: https://github.com/Azure/azure-functions-host
+ branchOrCommit: "{{ BranchOrCommit }}"
+ project: tools/Crank/BenchmarkApps/AspNetBenchmark/AspNetBenchmark/AspNetBenchmark.csproj
+ readyStateText: Application started.
+
+scenarios:
+ http:
+ application:
+ job: functionsServer
+ environmentVariables:
+ FUNCTIONS_WORKER_RUNTIME: "{{ FunctionsWorkerRuntime }}"
+ AzureWebJobsScriptRoot: "{{ FunctionAppPath }}"
+ ASPNETCORE_URLS: "{{ AspNetUrls }}"
+ load:
+ job: bombardier
+ variables:
+ path: /api/Hello
+ http-appsvc:
+ application:
+ job: functionsServer
+ environmentVariables:
+ HOME: "{{ HomePath }}"
+ WEBSITE_SITE_NAME: "Test"
+ WEBSITE_INSTANCE_ID: "8399B720-AB73-46D6-94DE-5A27871B3155"
+ WEBSITE_OWNER_NAME: "A5F47496-A284-4788-A127-E79454330567+westuswebspace"
+ ASPNETCORE_URLS: "{{ AspNetUrls }}"
+ load:
+ job: bombardier
+ variables:
+ path: /api/Hello
+ http-linux-appsvc:
+ application:
+ job: functionsServer
+ environmentVariables:
+ HOME: "{{ HomePath }}"
+ WEBSITE_SITE_NAME: "Test"
+ WEBSITE_INSTANCE_ID: "8399B720-AB73-46D6-94DE-5A27871B3155"
+ WEBSITE_OWNER_NAME: "A5F47496-A284-4788-A127-E79454330567+westuswebspace"
+ FUNCTIONS_LOGS_MOUNT_PATH: "{{ TempLogPath }}"
+ ASPNETCORE_URLS: "{{ AspNetUrls }}"
+ load:
+ job: bombardier
+ variables:
+ path: /api/Hello
+ aspnet-hello:
+ application:
+ job: aspnetServer
+ environmentVariables:
+ ASPNETCORE_URLS: "{{ AspNetUrls }}"
+ load:
+ job: bombardier
+ variables:
+ path: /api/Hello
+
+profiles:
+ default:
+ variables:
+ serverUri: http://{{ CrankAgentAppVm }}
+ serverPort: 5000
+ jobs:
+ application:
+ endpoints:
+ - "http://{{ CrankAgentAppVm }}:5010"
+ load:
+ endpoints:
+ - "http://{{ CrankAgentLoadVm }}:5010"
diff --git a/tools/Crank/run-benchmarks.ps1 b/tools/Crank/run-benchmarks.ps1
new file mode 100755
index 0000000000..19e93f7501
--- /dev/null
+++ b/tools/Crank/run-benchmarks.ps1
@@ -0,0 +1,154 @@
+param(
+ [Parameter(Mandatory = $true)]
+ [string]
+ $CrankAgentAppVm,
+
+ [Parameter(Mandatory = $true)]
+ [string]
+ $CrankAgentLoadVm,
+
+ [string]
+ $BranchOrCommit = 'dev',
+
+ [string]
+ $Scenario = 'http',
+
+ [string]
+ $FunctionApp = 'HelloHttpNet9',
+
+ [string]
+ $FunctionsWorkerRuntime = 'dotnet-isolated',
+
+ [string]
+ $InvokeCrankCommand,
+
+ [switch]
+ $WriteResultsToDatabase,
+
+ [switch]
+ $RefreshCrankContoller,
+
+ [string]
+ $UserName = 'Functions',
+
+ [bool]
+ $Trace = $false,
+
+ [int]
+ $Duration = 30,
+
+ [int]
+ $Warmup = 15,
+
+ [int]
+ $Iterations = 1
+)
+
+$ErrorActionPreference = 'Stop'
+
+#region Utilities
+
+function InstallCrankController {
+ dotnet tool install -g Microsoft.Crank.Controller --version "0.2.0-*"
+}
+
+function UninstallCrankController {
+ dotnet tool uninstall -g microsoft.crank.controller
+}
+
+#endregion
+
+#region Main
+
+if (-not $InvokeCrankCommand) {
+ if (Get-Command crank -ErrorAction SilentlyContinue) {
+ if ($RefreshCrankContoller) {
+ Write-Warning 'Reinstalling crank controller...'
+ UninstallCrankController
+ InstallCrankController
+ }
+ } else {
+ Write-Warning 'Crank controller is not found, installing...'
+ InstallCrankController
+ }
+ $InvokeCrankCommand = 'crank'
+}
+
+$crankConfigPath = Join-Path `
+ -Path (Split-Path $PSCommandPath -Parent) `
+ -ChildPath 'benchmarks.yml'
+
+$isLinuxApp = $CrankAgentAppVm -match '\blinux\b'
+
+$homePath = if ($isLinuxApp) { "/home/$UserName/FunctionApps/$FunctionApp" } else { "C:\FunctionApps\$FunctionApp" }
+$functionAppPath = if ($isLinuxApp) { "/home/$UserName/FunctionApps/$FunctionApp/site/wwwroot" } else { "C:\FunctionApps\$FunctionApp\site\wwwroot" }
+$tmpLogPath = if ($isLinuxApp) { "/tmp/functions/log" } else { 'C:\Temp\Functions\Log' }
+
+$crankAgentAppPrivateDnsName = $CrankAgentAppVm.Split('.')[0]
+
+$aspNetUrls = "http://$($crankAgentAppPrivateDnsName):5000"
+$profileName = "default"
+
+$patchedConfigFile = New-TemporaryFile |
+ ForEach-Object { Rename-Item -Path $_.FullName -NewName ($_.Name + '.yml') -PassThru }
+
+try {
+ # This is a temporary hack to work around a Crank issue: variables are not expanded in some contexts.
+ # So, we patch the config file with the required data.
+ Get-Content -Path $crankConfigPath |
+ ForEach-Object { $_ -replace 'serverUri: http://{{ CrankAgentAppVm }}', "serverUri: http://$crankAgentAppPrivateDnsName" } |
+ Out-File -FilePath $patchedConfigFile.FullName
+
+ $crankArgs =
+ '--config', $patchedConfigFile.FullName,
+ '--scenario', $Scenario,
+ '--profile', $profileName,
+ '--chart',
+ '--chart-type hex',
+ '--application.collectCounters', $true,
+ '--variable', "CrankAgentAppVm=$CrankAgentAppVm",
+ '--variable', "CrankAgentLoadVm=$CrankAgentLoadVm",
+ '--variable', "FunctionAppPath=`"$functionAppPath`"",
+ '--variable', "FunctionsWorkerRuntime=`"$FunctionsWorkerRuntime`"",
+ '--variable', "HomePath=`"$homePath`"",
+ '--variable', "TempLogPath=`"$tmpLogPath`"",
+ '--variable', "BranchOrCommit=$BranchOrCommit",
+ '--variable', "duration=$Duration",
+ '--variable', "warmup=$Warmup",
+ '--variable', "AspNetUrls=$aspNetUrls"
+
+ if ($Trace) {
+ $crankArgs += '--application.collect', $true
+ }
+
+ if ($WriteResultsToDatabase) {
+ Set-AzContext -Subscription 'Functions Build Infra' > $null
+ $sqlPassword = (Get-AzKeyVaultSecret -vaultName 'functions-crank-kv' -name 'SqlAdminPassword').SecretValueText
+
+ $sqlConnectionString = "Server=tcp:functions-crank-sql.database.windows.net,1433;Initial Catalog=functions-crank-db;Persist Security Info=False;User ID=Functions;Password=$sqlPassword;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;"
+
+ $crankArgs += '--sql', $sqlConnectionString
+ $crankArgs += '--table', 'FunctionsPerf'
+ }
+
+ if ($Iterations -gt 1) {
+ $crankArgs += '--iterations', $Iterations
+ $crankArgs += '--display-iterations'
+ }
+
+ # Print the command being executed
+ Write-Host "Executing command: $InvokeCrankCommand $($crankArgs -join ' ')"
+
+ & $InvokeCrankCommand $crankArgs 2>&1 | Tee-Object -Variable crankOutput
+} finally {
+ Remove-Item -Path $patchedConfigFile.FullName
+}
+
+$badResponses = $crankOutput | Where-Object { $_ -match '\bBad responses\b\s*\|\s*(\S*)\s' } | ForEach-Object { $Matches[1] }
+if ($null -eq $badResponses) {
+ Write-Warning "Could not detect the number of bad responses. The performance results may be unreliable."
+} elseif ($badResponses -ne 0) {
+ Write-Warning "Detected $badResponses bad response(s). The performance results may be unreliable."
+}
+
+#endregion