Compare commits

...

3 Commits

Author SHA1 Message Date
Yu Leng (from Dev Box)
7a2d8d7851 Enhance CmdPal AOT Trimming Report script with detailed analysis process and output descriptions 2025-08-01 17:41:48 +08:00
Yu Leng (from Dev Box)
f882bc3370 [TrimmingAnalyzer] Update report generation and add new analysis scripts for AOT comparison 2025-08-01 16:48:48 +08:00
Yu Leng
d872e844e7 init 2025-07-31 17:29:45 +08:00
8 changed files with 571 additions and 0 deletions

1
deps/cxxopts vendored Submodule

Submodule deps/cxxopts added at 12e496da3d

View File

@@ -0,0 +1,51 @@
# PowerToys CmdPal AOT 分析工具
这个工具包用于分析 PowerToys CmdPal 在启用 AOT (Ahead-of-Time) 编译时被移除的类型。
## 核心文件
### 分析工具 (`tools/TrimmingAnalyzer/`)
- `TrimmingAnalyzer.csproj` - 项目文件
- `Program.cs` - 主程序入口
- `TypeAnalyzer.cs` - 程序集类型分析引擎
- `ReportGenerator.cs` - 报告生成器 (Markdown/XML/JSON)
### 脚本文件 (`tools/build/`)
- `Generate-CmdPalTrimmingReport.ps1` - 主要分析脚本(包含完整说明和自动化流程)
## 使用方法
### 前提条件
- Visual Studio 2022 with C++ workload
- Windows SDK
- 使用 Developer Command Prompt for VS 2022
### 运行分析
```powershell
cd C:\Users\yuleng\PowerToys
.\tools\build\Generate-CmdPalTrimmingReport.ps1
```
### 输出报告
- `TrimmedTypes.md` - 人类可读的 Markdown 报告
- `TrimmedTypes.rd.xml` - 运行时指令以保留类型
- JSON 格式的分析数据
## 分析原理
1. **Debug 构建** - 不启用 AOT保留所有类型
2. **Release 构建** - 启用 AOT移除未使用的类型
3. **程序集比较** - 识别被 AOT 优化移除的类型
4. **报告生成** - 生成详细的优化效果报告
## 价值
- 显示 AOT 优化的有效性
- 识别被消除的未使用代码
- 帮助理解二进制大小减少
- 协助排查运行时反射问题
- 为性能优化决策提供数据
---
*工具状态: 已完成,等待 Visual Studio C++ 构建环境配置后即可使用*

View File

@@ -0,0 +1,20 @@
using System.Collections.Generic;
namespace TrimmingAnalyzer.Models
{
public class TypeInfo
{
public string FullName { get; set; } = string.Empty;
public string Namespace { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public bool IsPublic { get; set; }
public bool IsSealed { get; set; }
public bool IsAbstract { get; set; }
public bool IsInterface { get; set; }
public bool IsEnum { get; set; }
public bool IsDelegate { get; set; }
public string? BaseType { get; set; }
public List<string> Interfaces { get; set; } = new();
public int MemberCount { get; set; }
}
}

View File

@@ -0,0 +1,59 @@
using System;
using System.IO;
namespace TrimmingAnalyzer
{
class Program
{
static void Main(string[] args)
{
if (args.Length < 3)
{
Console.WriteLine("Usage: TrimmingAnalyzer <untrimmed.dll> <trimmed.dll> <output-dir> [formats]");
Console.WriteLine("Formats: rdxml,markdown (default: rdxml,markdown)");
return;
}
var untrimmedPath = Path.GetFullPath(args[0]);
var trimmedPath = Path.GetFullPath(args[1]);
var outputDir = Path.GetFullPath(args[2]);
var formats = args.Length > 3 ? args[3].Split(',') : new[] { "rdxml", "markdown" };
try
{
Console.WriteLine("Analyzing assemblies...");
var analyzer = new TypeAnalyzer();
var removedTypes = analyzer.GetRemovedTypes(untrimmedPath, trimmedPath);
Console.WriteLine($"Found {removedTypes.Count} trimmed types");
var generator = new ReportGenerator();
foreach (var format in formats)
{
switch (format.Trim().ToLower())
{
case "rdxml":
var rdxmlPath = Path.Combine(outputDir, "TrimmedTypes.rd.xml");
generator.GenerateRdXml(removedTypes, rdxmlPath);
Console.WriteLine($"Generated: {rdxmlPath}");
break;
case "markdown":
var markdownPath = Path.Combine(outputDir, "TrimmedTypes.md");
generator.GenerateMarkdown(removedTypes, markdownPath);
Console.WriteLine($"Generated: {markdownPath}");
break;
default:
Console.WriteLine($"Unknown format: {format}");
break;
}
}
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
Environment.Exit(1);
}
}
}
}

View File

@@ -0,0 +1,167 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Xml.Linq;
using TrimmingAnalyzer.Models;
namespace TrimmingAnalyzer
{
public class ReportGenerator
{
public void GenerateRdXml(List<TypeInfo> removedTypes, string outputPath)
{
var typesByNamespace = removedTypes.GroupBy(t => t.Namespace);
// Define the namespace
XNamespace ns = "http://schemas.microsoft.com/netfx/2013/01/metadata";
var doc = new XDocument(
new XElement(ns + "Directives",
new XElement(ns + "Application",
new XComment($"CmdPal Trimming Report - Generated on {DateTime.Now:yyyy-MM-dd HH:mm:ss}"),
new XComment($"Total types trimmed: {removedTypes.Count}"),
new XComment("TrimMode: partial (as configured in Microsoft.CmdPal.UI.csproj)"),
new XElement(ns + "Assembly",
new XAttribute("Name", "Microsoft.CmdPal.UI"),
new XAttribute("Dynamic", "Required All"),
typesByNamespace.Select(g =>
new XElement(ns + "Namespace",
new XAttribute("Name", g.Key),
new XAttribute("Preserve", "All"),
new XAttribute("Dynamic", "Required All"),
g.Select(type =>
new XElement(ns + "Type",
new XAttribute("Name", type.Name),
new XAttribute("Dynamic", "Required All"),
new XAttribute("Serialize", "All"),
new XAttribute("DataContractSerializer", "All"),
new XAttribute("DataContractJsonSerializer", "All"),
new XAttribute("XmlSerializer", "All"),
new XAttribute("MarshalObject", "All"),
new XAttribute("MarshalDelegate", "All")
)
)
)
)
)
)
)
);
Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? ".");
doc.Save(outputPath);
}
public void GenerateMarkdown(List<TypeInfo> removedTypes, string outputPath)
{
GenerateMarkdown(removedTypes, outputPath, null);
}
public void GenerateMarkdown(List<TypeInfo> removedTypes, string outputPath, List<string>? assemblyNames)
{
var sb = new StringBuilder();
sb.AppendLine("# CmdPal Debug vs AOT Release Comparison Report");
sb.AppendLine();
sb.AppendLine($"**Generated:** {DateTime.Now:yyyy-MM-dd HH:mm:ss}");
sb.AppendLine();
sb.AppendLine($"**Comparison:** Debug Build (no AOT) vs Release Build (with AOT)");
sb.AppendLine($"**Purpose:** Show types removed when enabling AOT compilation in Release mode");
sb.AppendLine();
if (assemblyNames != null && assemblyNames.Count > 0)
{
sb.AppendLine($"**Analyzed assemblies:** {string.Join(", ", assemblyNames.Distinct().OrderBy(x => x))}");
sb.AppendLine();
}
sb.AppendLine($"**Total types removed by AOT:** {removedTypes.Count}");
sb.AppendLine();
// Summary by namespace
sb.AppendLine("## Summary by Namespace");
sb.AppendLine();
sb.AppendLine("| Namespace | Types Trimmed |");
sb.AppendLine("|-----------|---------------|");
foreach (var group in removedTypes.GroupBy(t => t.Namespace).OrderBy(g => g.Key))
{
sb.AppendLine($"| `{group.Key}` | {group.Count()} |");
}
sb.AppendLine();
sb.AppendLine("## Detailed Type List");
sb.AppendLine();
foreach (var group in removedTypes.GroupBy(t => t.Namespace).OrderBy(g => g.Key))
{
sb.AppendLine($"### {group.Key}");
sb.AppendLine();
sb.AppendLine("| Type | Kind | Visibility | Base Type | Interfaces | Members |");
sb.AppendLine("|------|------|------------|-----------|------------|---------|");
foreach (var type in group.OrderBy(t => t.Name))
{
var kind = GetTypeKind(type);
var visibility = type.IsPublic ? "Public" : "Internal";
var baseType = string.IsNullOrEmpty(type.BaseType) ? "-" : $"`{type.BaseType.Split('.').Last()}`";
var interfaces = type.Interfaces.Any()
? string.Join(", ", type.Interfaces.Take(3).Select(i => $"`{i.Split('.').Last()}`")) +
(type.Interfaces.Count > 3 ? "..." : "")
: "-";
sb.AppendLine($"| `{type.Name}` | {kind} | {visibility} | {baseType} | {interfaces} | {type.MemberCount} |");
}
sb.AppendLine();
}
// Add usage instructions
sb.AppendLine("## How to Use This Report");
sb.AppendLine();
sb.AppendLine("If you need to preserve any of these types from trimming:");
sb.AppendLine();
sb.AppendLine("1. Copy the relevant entries from `TrimmedTypes.rd.xml` to your project's `rd.xml` file");
sb.AppendLine("2. Or use `[DynamicallyAccessedMembers]` attributes in your code");
sb.AppendLine("3. Or use `[DynamicDependency]` attributes to preserve specific members");
sb.AppendLine();
sb.AppendLine("Note: This report shows types that are present in Debug builds but removed in AOT Release builds.");
sb.AppendLine("AOT compilation removes unused types and members to reduce binary size and improve startup performance.");
Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? ".");
File.WriteAllText(outputPath, sb.ToString());
}
public void GenerateJson(List<TypeInfo> removedTypes, string outputPath, string assemblyName)
{
var analysisResult = new
{
AssemblyName = assemblyName,
GeneratedAt = DateTime.Now,
TotalTypes = removedTypes.Count,
RemovedTypes = removedTypes.OrderBy(t => t.FullName).ToList()
};
var options = new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
var json = JsonSerializer.Serialize(analysisResult, options);
Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? ".");
File.WriteAllText(outputPath, json);
}
private string GetTypeKind(TypeInfo type)
{
if (type.IsInterface) return "Interface";
if (type.IsEnum) return "Enum";
if (type.IsDelegate) return "Delegate";
if (type.IsAbstract) return "Abstract Class";
if (type.IsSealed) return "Sealed Class";
return "Class";
}
}
}

View File

@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<EnableDefaultItems>false</EnableDefaultItems>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
<WarningsAsErrors />
<WarningsNotAsErrors />
<NoWarn>SA1633;SA1400;SA1518;SA1516;SA1503;SA1111;SA1116;SA1122;SA1028;SA1413;SA1513;CA1311;CA1304;CA1305;CA1860;CA1852</NoWarn>
</PropertyGroup>
<ItemGroup>
<Compile Include="Program.cs" />
<Compile Include="TypeAnalyzer.cs" />
<Compile Include="ReportGenerator.cs" />
<Compile Include="Models\TypeInfo.cs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="System.Text.Json" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,79 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
using TrimmingAnalyzer.Models;
namespace TrimmingAnalyzer
{
public class TypeAnalyzer
{
public List<Models.TypeInfo> GetRemovedTypes(string untrimmedPath, string trimmedPath)
{
if (!File.Exists(untrimmedPath))
{
throw new FileNotFoundException($"Untrimmed assembly not found: {untrimmedPath}");
}
if (!File.Exists(trimmedPath))
{
throw new FileNotFoundException($"Trimmed assembly not found: {trimmedPath}");
}
var removedTypes = new List<Models.TypeInfo>();
var untrimmedContext = new AssemblyLoadContext("Untrimmed", true);
var trimmedContext = new AssemblyLoadContext("Trimmed", true);
try
{
var untrimmedAssembly = untrimmedContext.LoadFromAssemblyPath(untrimmedPath);
var trimmedAssembly = trimmedContext.LoadFromAssemblyPath(trimmedPath);
var untrimmedTypes = untrimmedAssembly.GetTypes().Where(t => t.FullName != null).ToDictionary(t => t.FullName!);
var trimmedTypeNames = trimmedAssembly.GetTypes().Where(t => t.FullName != null).Select(t => t.FullName!).ToHashSet();
foreach (var kvp in untrimmedTypes)
{
if (!trimmedTypeNames.Contains(kvp.Key))
{
var type = kvp.Value;
var typeInfo = new Models.TypeInfo
{
FullName = type.FullName ?? string.Empty,
Namespace = type.Namespace ?? "Global",
Name = type.Name,
IsPublic = type.IsPublic,
IsSealed = type.IsSealed,
IsAbstract = type.IsAbstract,
IsInterface = type.IsInterface,
IsEnum = type.IsEnum,
IsDelegate = type.IsSubclassOf(typeof(Delegate)),
BaseType = type.BaseType?.FullName,
Interfaces = type.GetInterfaces().Select(i => i.FullName ?? string.Empty).ToList(),
MemberCount = type.GetMembers(
BindingFlags.Public | BindingFlags.NonPublic |
BindingFlags.Instance | BindingFlags.Static |
BindingFlags.DeclaredOnly).Length,
};
removedTypes.Add(typeInfo);
}
}
}
catch (Exception ex)
{
throw new InvalidOperationException($"Error analyzing assemblies: {ex.Message}", ex);
}
finally
{
untrimmedContext.Unload();
trimmedContext.Unload();
}
return removedTypes.OrderBy(t => t.Namespace).ThenBy(t => t.Name).ToList();
}
}
}

View File

@@ -0,0 +1,168 @@
<#
.SYNOPSIS
PowerToys CmdPal AOT Trimming Analysis - Generates assembly comparison reports
.DESCRIPTION
This script builds CmdPal UI with and without AOT optimization, then uses TrimmingAnalyzer
to analyze the differences and generate reports showing which types are removed by AOT.
ANALYSIS PROCESS:
1. Build Debug version (no AOT optimization)
2. Build Release version (with AOT optimization)
3. Compare assemblies to identify removed types
4. Generate reports: TrimmedTypes.md, TrimmedTypes.rd.xml
REQUIREMENTS:
• Visual Studio 2022 with C++ workload
• Windows SDK
• Use Developer Command Prompt for VS 2022
OUTPUT REPORTS:
• TrimmedTypes.md - Human-readable Markdown report
• TrimmedTypes.rd.xml - Runtime directives to preserve types
• Analysis JSON data for further processing
.PARAMETER Configuration
Build configuration (Debug/Release). Defaults to Release
.PARAMETER EnableAOT
Whether to enable AOT compilation. Defaults to true
.EXAMPLE
.\Generate-CmdPalTrimmingReport.ps1
Runs the complete AOT trimming analysis with default settings
.NOTES
Author: PowerToys CmdPal AOT Analysis Tool
Purpose: Show types removed when enabling AOT compilation
#>
param(
[string]$Configuration = "Release",
[bool]$EnableAOT = $true
)
$ErrorActionPreference = "Stop"
Write-Host "======================================" -ForegroundColor Cyan
Write-Host "PowerToys CmdPal AOT Trimming Analysis" -ForegroundColor Cyan
Write-Host "======================================" -ForegroundColor Cyan
Write-Host ""
Write-Host "PURPOSE: Generate reports showing types removed by AOT optimization" -ForegroundColor Yellow
Write-Host "OUTPUT: TrimmedTypes.md, TrimmedTypes.rd.xml, analysis data" -ForegroundColor Yellow
Write-Host ""
# Get paths
$rootDir = Resolve-Path (Join-Path $PSScriptRoot "..\..")
$cmdPalProject = Join-Path $rootDir "src\modules\cmdpal\Microsoft.CmdPal.UI\Microsoft.CmdPal.UI.csproj"
$cmdPalDir = Split-Path -Parent $cmdPalProject
$analyzerProject = Join-Path $rootDir "tools\TrimmingAnalyzer\TrimmingAnalyzer.csproj"
# Build paths
$tempDir = Join-Path $env:TEMP "CmdPalTrimAnalysis_$(Get-Date -Format 'yyyyMMdd_HHmmss')"
$untrimmedDir = Join-Path $tempDir "untrimmed"
$trimmedDir = Join-Path $tempDir "trimmed"
# Ensure all NuGet packages are restored
Write-Host "Restoring NuGet packages..." -ForegroundColor Yellow
& dotnet restore $cmdPalProject
if ($LASTEXITCODE -ne 0) {
Write-Warning "Package restore had some issues, but continuing..."
}
try {
# Create directories
New-Item -ItemType Directory -Path $untrimmedDir -Force | Out-Null
New-Item -ItemType Directory -Path $trimmedDir -Force | Out-Null
# Build TrimmingAnalyzer
Write-Host "Building TrimmingAnalyzer tool..." -ForegroundColor Yellow
& dotnet build $analyzerProject -c Release
if ($LASTEXITCODE -ne 0) {
throw "Failed to build TrimmingAnalyzer"
}
# Build Debug mode without AOT (baseline for comparison)
Write-Host "Building CmdPal in Debug mode without AOT (baseline)..." -ForegroundColor Yellow
& dotnet publish $cmdPalProject `
--configuration Debug `
--runtime win-x64 `
--self-contained true `
--property:PublishTrimmed=false `
--property:EnableCmdPalAOT=false `
--property:PublishAot=false `
--verbosity minimal
if ($LASTEXITCODE -ne 0) {
throw "Failed to build Debug baseline version"
}
# Copy baseline (Debug without AOT) output to analysis directory
$baselineOutput = Join-Path $cmdPalDir "bin\Debug\net9.0-windows10.0.26100.0\win-x64\publish"
Copy-Item "$baselineOutput\Microsoft.CmdPal.UI.dll" $untrimmedDir -Force
# Copy all dependencies to help with assembly resolution
Get-ChildItem "$baselineOutput\*.dll" | ForEach-Object {
if ($_.Name -ne "Microsoft.CmdPal.UI.dll") {
Copy-Item $_.FullName $untrimmedDir -Force -ErrorAction SilentlyContinue
}
}
Write-Host "Copied Debug baseline DLLs from: $baselineOutput"
# Build Release mode with AOT enabled
Write-Host "Building CmdPal in Release mode with AOT enabled..." -ForegroundColor Yellow
& dotnet publish $cmdPalProject `
--configuration Release `
--runtime win-x64 `
--self-contained true `
--property:PublishTrimmed=false `
--property:EnableCmdPalAOT=true `
--property:PublishAot=true `
--verbosity minimal
if ($LASTEXITCODE -ne 0) {
throw "Failed to build AOT+trimmed version"
}
# Copy AOT+trimmed output to analysis directory
$trimmedOutput = Join-Path $cmdPalDir "bin\$Configuration\net9.0-windows10.0.26100.0\win-x64\publish"
Copy-Item "$trimmedOutput\Microsoft.CmdPal.UI.dll" $trimmedDir -Force
# Copy all dependencies to help with assembly resolution
Get-ChildItem "$trimmedOutput\*.dll" | ForEach-Object {
if ($_.Name -ne "Microsoft.CmdPal.UI.dll") {
Copy-Item $_.FullName $trimmedDir -Force -ErrorAction SilentlyContinue
}
}
Write-Host "Copied Release AOT DLLs from: $trimmedOutput"
# Use new directory comparison method to compare all types
Write-Host "Analyzing differences (Debug baseline vs Release AOT)..." -ForegroundColor Yellow
Write-Host "Using advanced directory comparison to detect AOT-optimized types..." -ForegroundColor Cyan
# Use the new directory comparison feature
& $analyzerPath --compare-directories "$untrimmedDir" "$trimmedDir" "$cmdPalDir" "rdxml,markdown,json" "TrimmedTypes"
if ($LASTEXITCODE -eq 0) {
Write-Host "Analysis completed successfully!" -ForegroundColor Green
} else {
throw "Analysis failed with exit code $LASTEXITCODE"
}
Write-Host "`n===== Analysis Complete =====" -ForegroundColor Cyan
Write-Host "Reports generated in: $cmdPalDir" -ForegroundColor Green
# List the main combined reports
$mainReports = @("TrimmedTypes.md", "TrimmedTypes.rd.xml")
foreach ($report in $mainReports) {
$reportPath = Join-Path $cmdPalDir $report
if (Test-Path $reportPath) {
Write-Host " - $report" -ForegroundColor Green
}
}
} finally {
# Cleanup
if (Test-Path $tempDir) {
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
}
}