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