Browse Source

DEBUG-only Decompile All + Decompile 100× commands

Replaces the NotImplementedDialog stubs in FileCommands.cs with real
implementations. Both are DEBUG-only File-menu entries for decompiler dev:

Assisted-by: Claude:claude-opus-4-7:Claude Code
pull/3755/head
Siegfried Pammer 2 months ago
parent
commit
800bf43e5c
  1. 165
      ILSpy/Commands/DecompileAllCommand.cs
  2. 15
      ILSpy/Commands/FileCommands.cs

165
ILSpy/Commands/DecompileAllCommand.cs

@ -0,0 +1,165 @@
// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
#if DEBUG
using System;
using System.Collections.Concurrent;
using System.Composition;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using ICSharpCode.Decompiler;
using ICSharpCode.ILSpy.Properties;
using ICSharpCode.ILSpyX;
using ILSpy.AssemblyTree;
using ILSpy.Docking;
using ILSpy.Languages;
using ILSpy.TextView;
using ILSpy.TreeNodes;
namespace ILSpy.Commands
{
/// <summary>
/// DEBUG-only File-menu stress test: decompile every loaded assembly to
/// <c>c:\temp\decompiled\&lt;ShortName&gt;.cs</c> in parallel and report per-assembly
/// timing in a results tab. Hard-coded output path matches WPF — the command's
/// CanExecute gates on that directory existing so it's effectively opt-in.
/// </summary>
[ExportMainMenuCommand(ParentMenuID = nameof(Resources._File), Header = nameof(Resources.DEBUGDecompile), MenuCategory = nameof(Resources.Open), MenuOrder = 2.5)]
[Shared]
sealed class DecompileAllCommand : SimpleCommand
{
const string OutputDir = @"c:\temp\decompiled";
readonly AssemblyTreeModel assemblyTreeModel;
readonly DockWorkspace dockWorkspace;
[ImportingConstructor]
public DecompileAllCommand(AssemblyTreeModel assemblyTreeModel, DockWorkspace dockWorkspace)
{
this.assemblyTreeModel = assemblyTreeModel;
this.dockWorkspace = dockWorkspace;
}
public override bool CanExecute(object? parameter) => Directory.Exists(OutputDir);
public override void Execute(object? parameter) => _ = ExecuteAsync();
async Task ExecuteAsync()
{
try
{
var report = await dockWorkspace.RunWithCancellation(token => Task.Run(() => {
var output = new AvaloniaEditTextOutput { Title = "Decompile All" };
var bag = new ConcurrentBag<string>();
Parallel.ForEach(
Partitioner.Create(assemblyTreeModel.AssemblyList!.GetAssemblies(), loadBalance: true),
new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount, CancellationToken = token },
asm => {
if (asm.HasLoadError)
return;
var watch = Stopwatch.StartNew();
Exception? ex = null;
var path = Path.Combine(OutputDir, asm.ShortName + ".cs");
try
{
using var writer = new StreamWriter(path);
var options = new DecompilationOptions {
CancellationToken = token,
FullDecompilation = true,
};
new CSharpLanguage().DecompileAssembly(asm, new PlainTextOutput(writer), options);
}
catch (Exception caught)
{
ex = caught;
}
watch.Stop();
bag.Add(asm.ShortName + " - " + watch.Elapsed + (ex != null ? " - " + ex.GetType().Name : ""));
});
foreach (var line in bag.OrderBy(s => s, StringComparer.OrdinalIgnoreCase))
{
output.Write(line);
output.WriteLine();
}
return output;
}, token), "Decompiling all assemblies…");
dockWorkspace.ShowText(report);
}
catch (OperationCanceledException) { }
}
}
/// <summary>
/// DEBUG-only stress test: re-decompile the current selection 100 times in series
/// and report average wall-clock time. Used for tracking decompilation perf
/// regressions across changes.
/// </summary>
[ExportMainMenuCommand(ParentMenuID = nameof(Resources._File), Header = nameof(Resources.DEBUGDecompile100x), MenuCategory = nameof(Resources.Open), MenuOrder = 2.6)]
[Shared]
sealed class Decompile100TimesCommand : SimpleCommand
{
const int NumRuns = 100;
readonly AssemblyTreeModel assemblyTreeModel;
readonly LanguageService languageService;
readonly DockWorkspace dockWorkspace;
[ImportingConstructor]
public Decompile100TimesCommand(AssemblyTreeModel assemblyTreeModel, LanguageService languageService, DockWorkspace dockWorkspace)
{
this.assemblyTreeModel = assemblyTreeModel;
this.languageService = languageService;
this.dockWorkspace = dockWorkspace;
}
public override void Execute(object? parameter) => _ = ExecuteAsync();
async Task ExecuteAsync()
{
var language = languageService.CurrentLanguage;
var nodes = assemblyTreeModel.SelectedItems.OfType<ILSpyTreeNode>().ToArray();
if (nodes.Length == 0)
return;
try
{
var report = await dockWorkspace.RunWithCancellation(token => Task.Run(() => {
var watch = Stopwatch.StartNew();
var options = new DecompilationOptions { CancellationToken = token };
for (int i = 0; i < NumRuns; i++)
{
foreach (var node in nodes)
node.Decompile(language, new PlainTextOutput(), options);
}
watch.Stop();
var output = new AvaloniaEditTextOutput { Title = "Decompile 100×" };
var msPerRun = watch.Elapsed.TotalMilliseconds / NumRuns;
output.Write($"Average time: {msPerRun:f1}ms");
output.WriteLine();
return output;
}, token), "Decompiling 100×…");
dockWorkspace.ShowText(report);
}
catch (OperationCanceledException) { }
}
}
}
#endif

15
ILSpy/Commands/FileCommands.cs

@ -107,19 +107,8 @@ namespace ILSpy.Commands
public override void Execute(object? parameter) => assemblyTreeModel.Refresh(); public override void Execute(object? parameter) => assemblyTreeModel.Refresh();
} }
[ExportMainMenuCommand(ParentMenuID = nameof(Resources._File), Header = nameof(Resources.DEBUGDecompile), MenuCategory = nameof(Resources.Open), MenuOrder = 2.5)] // DEBUG-only DecompileAllCommand + Decompile100TimesCommand live in DecompileAllCommand.cs;
[Shared] // the stubs that lived here were replaced with the real implementations in `<commit>`.
sealed class DecompileAllCommand : SimpleCommand
{
public override void Execute(object? parameter) => NotImplementedDialog.Show(Resources.DEBUGDecompile);
}
[ExportMainMenuCommand(ParentMenuID = nameof(Resources._File), Header = nameof(Resources.DEBUGDecompile100x), MenuCategory = nameof(Resources.Open), MenuOrder = 2.6)]
[Shared]
sealed class Decompile100TimesCommand : SimpleCommand
{
public override void Execute(object? parameter) => NotImplementedDialog.Show(Resources.DEBUGDecompile100x);
}
[ExportMainMenuCommand(ParentMenuID = nameof(Resources._File), Header = nameof(Resources.DEBUGDisassemble), MenuCategory = nameof(Resources.Open), MenuOrder = 2.5)] [ExportMainMenuCommand(ParentMenuID = nameof(Resources._File), Header = nameof(Resources.DEBUGDisassemble), MenuCategory = nameof(Resources.Open), MenuOrder = 2.5)]
[Shared] [Shared]

Loading…
Cancel
Save