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]