diff --git a/ILSpy/Commands/DecompileAllCommand.cs b/ILSpy/Commands/DecompileAllCommand.cs new file mode 100644 index 000000000..3f5dcddea --- /dev/null +++ b/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 +{ + /// + /// DEBUG-only File-menu stress test: decompile every loaded assembly to + /// c:\temp\decompiled\<ShortName>.cs 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. + /// + [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(); + 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) { } + } + } + + /// + /// 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. + /// + [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().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 diff --git a/ILSpy/Commands/FileCommands.cs b/ILSpy/Commands/FileCommands.cs index bd8abf25e..445d865ac 100644 --- a/ILSpy/Commands/FileCommands.cs +++ b/ILSpy/Commands/FileCommands.cs @@ -107,19 +107,8 @@ namespace ILSpy.Commands public override void Execute(object? parameter) => assemblyTreeModel.Refresh(); } - [ExportMainMenuCommand(ParentMenuID = nameof(Resources._File), Header = nameof(Resources.DEBUGDecompile), MenuCategory = nameof(Resources.Open), MenuOrder = 2.5)] - [Shared] - 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); - } + // DEBUG-only DecompileAllCommand + Decompile100TimesCommand live in DecompileAllCommand.cs; + // the stubs that lived here were replaced with the real implementations in ``. [ExportMainMenuCommand(ParentMenuID = nameof(Resources._File), Header = nameof(Resources.DEBUGDisassemble), MenuCategory = nameof(Resources.Open), MenuOrder = 2.5)] [Shared]