Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
public override async global::System.Threading.Tasks.Task WriteToAsync(global::System.IO.Stream stream, global::System.Threading.CancellationToken cancellationToken = ((global::System.Threading.CancellationToken)default))
public override async global::System.Threading.Tasks.Task WriteToAsync(global::System.IO.Stream stream, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken))
{
bool first = true;
await foreach (T value in _values.WithCancellation(cancellationToken))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
BenchmarkDotNet.Artifacts/
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# AppService production SDK hotspot profiling

> Historical pre-main-merge investigation. Measurements and legacy/shadow phase names below do not describe the current generator. See [README.md](README.md) for the updated workloads and measurement caveats.

Date: 2026-10-22.

## Status: blocked before SDK generation

**No AppService performance measurements have been collected.** This file records the setup attempt and the tooling blocker; it must not be read as a completed production-service profile. The separate [fixture profiling report](HotspotProfilingResults.md) is unchanged and does not contain AppService results.

The user approved generation and profiling of **only `Azure.ResourceManager.AppService`**, using the sibling local Azure SDK for .NET repository. Replacing Roslyn `Simplifier.ReduceAsync` is already the agreed direction; the production profile will characterize replacement requirements and additional CPU/allocation hotspots, not reopen that decision.

## Recorded inputs

| Item | Value |
| --- | --- |
| SDK repository | `/workspaces/azure-sdk-for-net` |
| SDK revision | `97b9032786d85215b83d87ea8f2859f44bd10662` |
| Service | `sdk/websites/Azure.ResourceManager.AppService` |
| Service version | `1.6.0-beta.1`; `ApiCompatVersion` `1.5.0` |
| Pinned specs | `Azure/azure-rest-api-specs` at `a61a447dd6ecb9063b7ec7da5b9cd74c4d4b4e6a` |
| Spec directory | `specification/web/resource-manager/Microsoft.Web/AppService` |
| Actual environment | Codespaces/container Linux, not WSL |
| Required SDK | `.NET SDK 10.0.401`, from the SDK repo's `global.json`; `rollForward: feature` |
| Installed SDKs | `10.0.103`, `10.0.400` — neither satisfies that pin |

The management emitter's existing lockfile was restored successfully using `npm ci --no-audit --no-fund` (488 packages, approximately 21 seconds reported by npm). No dependency versions or lockfile contents were changed. This replaces stale installed September packages with the existing October lockfile inputs; actual loaded generator assembly versions will be recorded after a successful build, not inferred from package names.

## Attempted official scoped workflow

From `eng/packages/http-client-csharp-mgmt`:

```sh
npm ci --no-audit --no-fund
pwsh -NoProfile -File ./eng/scripts/RegenSdkLocal.ps1 \
-Services Azure.ResourceManager.AppService -Parallel 1 -SaveInputs
```

The emitter build succeeded. The management-generator .NET build then failed because the pinned SDK could not be resolved:

```text
Requested SDK version: 10.0.401
Installed SDKs: 10.0.103, 10.0.400
global.json file: /workspaces/azure-sdk-for-net/global.json
A compatible .NET SDK was not found.
```

The wrapper failed during build step 1, before service generation. No TypeSpec service compilation, new saved-input generation, generator replay, or performance trace was completed. A stale pre-existing generator binary was not substituted, and `global.json` was not relaxed or edited.

## Required manual tooling action

Install **.NET SDK 10.0.401**, preferably alongside the existing SDKs, and verify that it is visible to `dotnet --list-sdks`. A user-local installation is also usable; provide its path so the generation command can explicitly select it without changing repository files.

For example, using the official [dotnet-install script](https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-install-script):

```sh
# After obtaining dotnet-install.sh from the official .NET distribution:
bash dotnet-install.sh --version 10.0.401 --install-dir "$HOME/.dotnet-sdk-10.0.401"
"$HOME/.dotnet-sdk-10.0.401/dotnet" --list-sdks
```

No host SDK or kernel settings were installed/changed by this investigation. The installed `powershell-yaml` 0.4.12 satisfies the script's minimum 0.4.7 requirement.

For the later native/on-CPU profile, Linux `perf` is still missing and `perf_event_paranoid=4` remains restrictive. Kernel-matched perf tools and appropriate WSL/host permissions are a **separate** manual requirement. Managed EventPipe collection can proceed once the .NET SDK build blocker is removed. `dotnet-gcdump` remains optional for retained-heap/root attribution; allocation traces alone do not establish retained memory or leaks.

## Safety checks and local evidence

Before running the workflow, all **2,677 original service files / 31,407,964 bytes** were hashed and archived. After the failure, the complete service file manifest matched the original exactly. Tracked package/lock/global/spec inputs also remained unchanged, and SDK `git status --short` still showed only its pre-existing local `.pi/` directory.

Raw setup evidence and prepared replay/analyzer scripts are at:

```text
/tmp/mtg-appservice-20261022/
```

Important files include:

- `original-service.tar.gz`, `original-service-manifest.json`
- `dependency-input-manifest.json`, `blocked-status.json`
- `npm-ci.log`, `regeneration.log`, regeneration start/end timestamps
- `capture_and_restore.py`, `profile_appservice.py`, `analyze/`

A stable local copy of the small evidence/scripts and original archive is saved under:

```text
/workspaces/typespec/.pi/benchmarks/appservice-20261022/
```

Both locations are local artifacts, not published attachments; `/tmp` is ephemeral.

## Resume after SDK installation

1. Revalidate the SDK/service state and reuse the existing original snapshot; do not overwrite it.
2. Rerun the exact official scoped regeneration command, selecting the installed SDK. Keep `-Services Azure.ResourceManager.AppService`; omitting the filter would regenerate all management SDKs and is outside scope.
3. Preserve the complete fresh result, saved inputs, custom code, and ancestor NuGet/ApiCompat context. Restore only investigation-created changes to the original service after preserving evidence.
4. Build the local management generator in Release and record actual core/plugin dependency versions and binary hashes. Its pinned MTG dependency is not automatically the sibling TypeSpec source used by the fixture benchmark.
5. Validate generator-only replay output against the freshly generated C# inventory before trusting measurements. Use the actual `ManagementClientGenerator` export and exclude TypeSpec compilation/build activity from profiling.
6. Run separate repeated CLI timing/process-memory measurements, EventPipe sampled-thread collection, and GC/allocation-stack collection. New CLI processes are not warm in-process generation. Do not interpret blocked/safepoint samples as CPU percentages or allocation traffic as retained heap.
7. Replace this blocked-status report with the completed AppService findings, keeping the fixture report separate. No full regeneration pipeline, generator implementation changes, commits, or pushes are authorized by this profiling task.
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text.Json.Nodes;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;

namespace Microsoft.TypeSpec.Generator.Perf
{
public class FullGenerationBenchmark
{
private const string ProfileEnvironmentVariable = "POSTPROCESSING_BENCHMARK_PROFILE_STEPS";
private const string ProfileOutputDirectoryEnvironmentVariable = "POSTPROCESSING_BENCHMARK_PROFILE_DIR";

private bool _profileSteps;

[Params(false, true)]
public bool DisableRoslynReduce { get; set; }

[GlobalSetup]
public void GlobalSetup()
{
_profileSteps = string.Equals(
Environment.GetEnvironmentVariable(ProfileEnvironmentVariable),
"true",
StringComparison.OrdinalIgnoreCase);
}

[Benchmark]
public async Task<int> GenerateSampleTypeSpecProject()
{
var postProcessingProfile = _profileSteps ? new GeneratedCodeWorkspacePostProcessingProfile() : null;
var generationProfile = _profileSteps ? new GeneratedCodeWorkspacePostProcessingProfile() : null;
GeneratedCodeWorkspace.PostProcessingProfile = postProcessingProfile;
CSharpGen.GenerationProfile = generationProfile;

var benchmarkDirectory = CreateBenchmarkInputDirectory();
var stopwatch = Stopwatch.StartNew();
try
{
CodeModelGenerator.Instance = new BenchmarkCodeModelGenerator(benchmarkDirectory, DisableRoslynReduce);
CodeModelGenerator.Instance.Configure();

var csharpGen = new CSharpGen();
await csharpGen.ExecuteAsync();

return Directory.GetFiles(CodeModelGenerator.Instance.Configuration.ProjectGeneratedDirectory, "*.cs", SearchOption.AllDirectories)
.Sum(static path => (int)new FileInfo(path).Length);
}
finally
{
stopwatch.Stop();
if (generationProfile != null)
{
WriteProfile(
generationProfile,
$"full-generation-profile-reduce-{!DisableRoslynReduce}-{DateTime.UtcNow:yyyyMMddHHmmssfff}.csv",
$"Full generation invocation elapsed ms: {stopwatch.Elapsed.TotalMilliseconds:F3}{Environment.NewLine}" +
$"Input directory: {benchmarkDirectory}{Environment.NewLine}");
}

if (postProcessingProfile != null)
{
WriteProfile(
postProcessingProfile,
$"full-generation-post-processing-profile-reduce-{!DisableRoslynReduce}-{DateTime.UtcNow:yyyyMMddHHmmssfff}.csv",
$"Full generation post-processing profile{Environment.NewLine}" +
$"Input directory: {benchmarkDirectory}{Environment.NewLine}");
}

CSharpGen.GenerationProfile = null;
GeneratedCodeWorkspace.PostProcessingProfile = null;
TryDeleteDirectory(benchmarkDirectory);
}
}

private static void WriteProfile(GeneratedCodeWorkspacePostProcessingProfile profile, string fileName, string header)
{
var profileDirectory = GetProfileOutputDirectory();
Directory.CreateDirectory(profileDirectory);
File.WriteAllText(Path.Combine(profileDirectory, fileName), header + profile.GetSummary());
}

private static string CreateBenchmarkInputDirectory()
{
var sourceDirectory = FindFullGenerationInputDirectory();
var benchmarkDirectory = Path.Combine(Path.GetTempPath(), "typespec-full-generation-benchmark", Guid.NewGuid().ToString("N"));
CopyDirectory(sourceDirectory, benchmarkDirectory);
return benchmarkDirectory;
}

private static string FindFullGenerationInputDirectory()
{
const string relativePath = "packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec";

var directory = new DirectoryInfo(AppContext.BaseDirectory);
while (directory != null)
{
var inputDirectory = Path.Combine(directory.FullName, relativePath);
if (File.Exists(Path.Combine(inputDirectory, "tspCodeModel.json")) &&
File.Exists(Path.Combine(inputDirectory, "Configuration.json")))
{
return inputDirectory;
}

directory = directory.Parent;
}

throw new DirectoryNotFoundException($"Could not find '{relativePath}' from '{AppContext.BaseDirectory}'.");
}

private static void CopyDirectory(string sourceDirectory, string destinationDirectory)
{
Directory.CreateDirectory(destinationDirectory);
foreach (var sourceFile in Directory.GetFiles(sourceDirectory, "*", SearchOption.AllDirectories))
{
var relativePath = Path.GetRelativePath(sourceDirectory, sourceFile);
// Build artifacts are not generator inputs and can vary between runs.
if (relativePath.Split(Path.DirectorySeparatorChar).Any(static part => part is "bin" or "obj"))
{
continue;
}

var destinationFile = Path.Combine(destinationDirectory, relativePath);
Directory.CreateDirectory(Path.GetDirectoryName(destinationFile)!);
File.Copy(sourceFile, destinationFile, overwrite: true);
}
}

private static void TryDeleteDirectory(string directory)
{
try
{
if (Directory.Exists(directory))
{
Directory.Delete(directory, recursive: true);
}
}
catch
{
// Best-effort cleanup for benchmark temp output.
}
}

private static string GetProfileOutputDirectory()
{
var configuredPath = Environment.GetEnvironmentVariable(ProfileOutputDirectoryEnvironmentVariable);
return string.IsNullOrWhiteSpace(configuredPath)
? Path.Combine(Path.GetTempPath(), "typespec-post-processing-profiles")
: Path.GetFullPath(configuredPath);
}

private sealed class BenchmarkCodeModelGenerator : CodeModelGenerator
{
public BenchmarkCodeModelGenerator(string outputPath, bool disableRoslynReduce)
: base(new GeneratorContext(LoadConfiguration(outputPath, disableRoslynReduce)))
{
}

private static Configuration LoadConfiguration(string outputPath, bool disableRoslynReduce)
{
var configuration = JsonNode.Parse(File.ReadAllText(Path.Combine(outputPath, "Configuration.json")))!;
configuration["disable-roslyn-reduce"] = disableRoslynReduce;
return Configuration.Load(outputPath, configuration.ToJsonString());
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.IO;
using System.Reflection;
using Microsoft.TypeSpec.Generator.SourceInput;

namespace Microsoft.TypeSpec.Generator.Perf
{
internal static class GeneratorInitializer
{
public static void Initialize()
{
GeneratorHandler generatorHandler = new GeneratorHandler();
generatorHandler.LoadGenerator(new CommandLineOptions
{
OutputDirectory = Path.Combine(Directory.GetParent(Assembly.GetExecutingAssembly().Location)!.FullName, "Projects", "Model"),
GeneratorName = "CodeModelGenerator"
});
// Microbenchmarks need the base generator, not plugin discovery across
// every assembly in the benchmark/test output directory.
var outputPath = Path.Combine(AppContext.BaseDirectory, "Projects", "Model");
CodeModelGenerator.Instance = new BenchmarkCodeModelGenerator(outputPath);
CodeModelGenerator.Instance.Configure();
CodeModelGenerator.Instance.SourceInputModel = new SourceInputModel(null, null);
}

private sealed class BenchmarkCodeModelGenerator(string outputPath)
: CodeModelGenerator(new GeneratorContext(Configuration.Load(outputPath)))
{
}
}
}
Loading
Loading