Add image processing benchmarks and UI folder open buttons

- Added MaddoShared.Benchmarks project with BenchmarkDotNet for comprehensive image processing performance tests (parallel, chunk, size, stress).
- Included helper for generating test images and custom configs to ensure InProcess toolchain for .NET Windows compatibility.
- Added cross-platform scripts to run benchmarks easily.
- Updated .gitignore for benchmark artifacts and temp files.
- Exposed GetFilesToProcessPublic in ImageCreationStuff for testability.
- Added file name sanitization in ImageCreatorSharp to prevent IO errors.
- Enhanced WinForms UI: added "Open" buttons for source/destination folders, handled folder opening in Explorer, and improved user messaging and layout.
- Updated solution file to include new benchmark project.
This commit is contained in:
MaddoScientisto 2026-02-14 19:20:25 +01:00
commit c2fd4bf780
17 changed files with 1608 additions and 301 deletions

View file

@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 18
VisualStudioVersion = 18.2.11415.280 d18.0
VisualStudioVersion = 18.2.11415.280
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImageCatalog 2", "imagecatalog\ImageCatalog 2.csproj", "{3F1E23DB-435E-0590-1EF5-735E898DBA3C}"
EndProject
@ -13,6 +13,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{5F0BEF23
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MaddoShared.Tests", "MaddoShared.Tests\MaddoShared.Tests.csproj", "{59952BE8-20B4-4BF2-9367-705F41395265}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MaddoShared.Benchmarks", "MaddoShared.Benchmarks\MaddoShared.Benchmarks.csproj", "{07499348-8C15-4DCC-8316-4AD121A43C38}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -59,6 +61,18 @@ Global
{59952BE8-20B4-4BF2-9367-705F41395265}.Release|x64.Build.0 = Release|Any CPU
{59952BE8-20B4-4BF2-9367-705F41395265}.Release|x86.ActiveCfg = Release|Any CPU
{59952BE8-20B4-4BF2-9367-705F41395265}.Release|x86.Build.0 = Release|Any CPU
{07499348-8C15-4DCC-8316-4AD121A43C38}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{07499348-8C15-4DCC-8316-4AD121A43C38}.Debug|Any CPU.Build.0 = Debug|Any CPU
{07499348-8C15-4DCC-8316-4AD121A43C38}.Debug|x64.ActiveCfg = Debug|Any CPU
{07499348-8C15-4DCC-8316-4AD121A43C38}.Debug|x64.Build.0 = Debug|Any CPU
{07499348-8C15-4DCC-8316-4AD121A43C38}.Debug|x86.ActiveCfg = Debug|Any CPU
{07499348-8C15-4DCC-8316-4AD121A43C38}.Debug|x86.Build.0 = Debug|Any CPU
{07499348-8C15-4DCC-8316-4AD121A43C38}.Release|Any CPU.ActiveCfg = Release|Any CPU
{07499348-8C15-4DCC-8316-4AD121A43C38}.Release|Any CPU.Build.0 = Release|Any CPU
{07499348-8C15-4DCC-8316-4AD121A43C38}.Release|x64.ActiveCfg = Release|Any CPU
{07499348-8C15-4DCC-8316-4AD121A43C38}.Release|x64.Build.0 = Release|Any CPU
{07499348-8C15-4DCC-8316-4AD121A43C38}.Release|x86.ActiveCfg = Release|Any CPU
{07499348-8C15-4DCC-8316-4AD121A43C38}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

25
MaddoShared.Benchmarks/.gitignore vendored Normal file
View file

@ -0,0 +1,25 @@
# BenchmarkDotNet artifacts
BenchmarkDotNet.Artifacts/
# Test images generated during benchmarks
TestImages/
# Build outputs
bin/
obj/
# User-specific files
*.suo
*.user
*.userosscache
*.sln.docstates
# Results and logs
*.log
*.html
*.csv
results/
# Temporary files
*.tmp
*.temp

View file

@ -0,0 +1,91 @@
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Diagnosers;
using BenchmarkDotNet.Engines;
using BenchmarkDotNet.Exporters;
using BenchmarkDotNet.Exporters.Csv;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Loggers;
using BenchmarkDotNet.Toolchains.InProcess.Emit;
namespace MaddoShared.Benchmarks;
/// <summary>
/// InProcess configuration for benchmarks requiring Windows-specific APIs
/// This avoids the net10.0-windows vs net10.0 compatibility issue
/// </summary>
public class InProcessConfig : ManualConfig
{
public InProcessConfig()
{
AddLogger(ConsoleLogger.Default);
AddExporter(HtmlExporter.Default);
AddExporter(MarkdownExporter.GitHub);
AddExporter(CsvExporter.Default);
AddDiagnoser(MemoryDiagnoser.Default);
// Add job with InProcess toolchain
AddJob(Job.Default
.WithToolchain(InProcessEmitToolchain.Instance)
.WithWarmupCount(1)
.WithIterationCount(3));
// Configuration options
WithOptions(ConfigOptions.DisableOptimizationsValidator);
WithOptions(ConfigOptions.KeepBenchmarkFiles);
}
}
/// <summary>
/// Custom configuration for image processing benchmarks
/// Uses InProcess toolchain to avoid net10.0-windows compatibility issues
/// </summary>
public class BenchmarkConfig : ManualConfig
{
public BenchmarkConfig()
{
// Add console logger
AddLogger(ConsoleLogger.Default);
// Add exporters for different formats
AddExporter(HtmlExporter.Default);
AddExporter(MarkdownExporter.GitHub);
AddExporter(CsvExporter.Default);
AddExporter(RPlotExporter.Default);
// Add diagnosers
AddDiagnoser(MemoryDiagnoser.Default);
AddDiagnoser(ThreadingDiagnoser.Default);
// Add columns
AddColumn(StatisticColumn.Mean);
AddColumn(StatisticColumn.StdDev);
AddColumn(StatisticColumn.Error);
AddColumn(StatisticColumn.Min);
AddColumn(StatisticColumn.Max);
AddColumn(StatisticColumn.Median);
AddColumn(BaselineRatioColumn.RatioMean);
// Customize jobs with InProcess toolchain for Windows compatibility
AddJob(Job.Default
.WithToolchain(InProcessEmitToolchain.Instance)
.WithWarmupCount(1)
.WithIterationCount(3)
.WithId("Quick"));
AddJob(Job.Default
.WithToolchain(InProcessEmitToolchain.Instance)
.WithWarmupCount(2)
.WithIterationCount(5)
.WithId("Standard"));
}
/// <summary>
/// Fast configuration for development and quick tests
/// </summary>
public static IConfig Fast => new ManualConfig()
.AddLogger(ConsoleLogger.Default)
.AddExporter(MarkdownExporter.GitHub)
.AddDiagnoser(MemoryDiagnoser.Default)
.AddJob(Job.Dry.WithToolchain(InProcessEmitToolchain.Instance)); // Very fast, but less accurate
}

View file

@ -0,0 +1,130 @@
using System;
using System.Collections.Concurrent;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Engines;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Toolchains.InProcess.Emit;
using MaddoShared.Benchmarks.Helpers;
using Microsoft.Extensions.Logging;
namespace MaddoShared.Benchmarks;
/// <summary>
/// Benchmarks focused on different chunk sizes for parallel processing
/// </summary>
[MemoryDiagnoser]
[Config(typeof(InProcessConfig))]
public class ChunkSizeBenchmarks
{
private string _sourceDirectory;
private string _destinationDirectory;
private ImageCreationStuff _imageCreationStuff;
private PicSettings _picSettings;
[Params(100)]
public int ImageCount { get; set; }
[Params(0, 5, 10, 20, 50)]
public int ChunkSize { get; set; }
[GlobalSetup]
public void Setup()
{
var tempBase = Path.Combine(Path.GetTempPath(), "ChunkBenchmarks", Guid.NewGuid().ToString());
_sourceDirectory = Path.Combine(tempBase, "Source");
_destinationDirectory = Path.Combine(tempBase, "Destination");
Directory.CreateDirectory(_sourceDirectory);
Directory.CreateDirectory(_destinationDirectory);
Console.WriteLine($"Generating {ImageCount} test images for chunk size testing...");
TestImageGenerator.GenerateTestImages(_sourceDirectory, ImageCount, width: 2000, height: 1500);
var loggerFactory = LoggerFactory.Create(builder =>
{
builder.SetMinimumLevel(LogLevel.Warning);
});
var logger = loggerFactory.CreateLogger<ImageCreationStuff>();
var imageCreatorLogger = loggerFactory.CreateLogger<ImageCreatorSharp>();
_picSettings = new PicSettings
{
DirectorySorgente = _sourceDirectory,
DirectoryDestinazione = _destinationDirectory,
DimStandard = 800,
DimStandardMiniatura = 200,
LarghezzaBig = 1024,
AltezzaBig = 768,
LarghezzaSmall = 200,
AltezzaSmall = 150,
CreaMiniature = true,
AggiungiScritteMiniature = false,
UsaForzaJpg = true,
UsaRotazioneAutomatica = true,
LogoAggiungi = false,
FotoGrandeDimOrigina = false,
TestoNome = false,
NomeData = false,
Suffisso = "_small",
Margine = 10,
Trasparenza = 100
};
var imageCreatorService = new ImageCreatorSharp(_picSettings, imageCreatorLogger);
_imageCreationStuff = new ImageCreationStuff(logger, _picSettings, imageCreatorService);
}
[GlobalCleanup]
public void Cleanup()
{
try
{
var tempBase = Path.GetDirectoryName(_sourceDirectory);
if (Directory.Exists(tempBase))
{
Directory.Delete(tempBase, recursive: true);
}
}
catch (Exception ex)
{
Console.WriteLine($"Cleanup error: {ex.Message}");
}
}
[IterationSetup]
public void IterationSetup()
{
if (Directory.Exists(_destinationDirectory))
{
Directory.Delete(_destinationDirectory, recursive: true);
}
Directory.CreateDirectory(_destinationDirectory);
}
[Benchmark]
public async Task ProcessWithVariableChunkSize()
{
var options = new ImageCreationStuff.Options
{
SourcePath = _sourceDirectory,
DestinationPath = _destinationDirectory,
MaxThreads = Environment.ProcessorCount,
ChunksSize = ChunkSize,
LinearExecution = false,
AggiornaSottodirectory = false,
CreaSottocartelle = false,
FilePerCartella = 100,
SuffissoCartelle = "",
CifreContatore = 4,
NumerazioneType = NumerazioneType.Progressiva
};
var results = new ConcurrentBag<string>();
await _imageCreationStuff.ProcessImagesParallel(options, results, null, CancellationToken.None);
}
}

View file

@ -0,0 +1,107 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
namespace MaddoShared.Benchmarks.Helpers;
/// <summary>
/// Helper class to generate test images for benchmarking
/// </summary>
[SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")]
public static class TestImageGenerator
{
/// <summary>
/// Generates a set of test JPEG images in the specified directory
/// </summary>
/// <param name="outputDirectory">Directory where images will be created</param>
/// <param name="imageCount">Number of images to generate</param>
/// <param name="width">Width of each image</param>
/// <param name="height">Height of each image</param>
/// <param name="includeSubfolders">Whether to create images in subfolders</param>
public static void GenerateTestImages(
string outputDirectory,
int imageCount,
int width = 4000,
int height = 3000,
bool includeSubfolders = false)
{
Directory.CreateDirectory(outputDirectory);
var random = new Random(42); // Fixed seed for reproducibility
for (int i = 0; i < imageCount; i++)
{
var targetDir = outputDirectory;
if (includeSubfolders && i % 10 == 0)
{
targetDir = Path.Combine(outputDirectory, $"Subfolder_{i / 10}");
Directory.CreateDirectory(targetDir);
}
var filePath = Path.Combine(targetDir, $"test_image_{i:D5}.jpg");
// Skip if already exists
if (File.Exists(filePath))
continue;
using var bitmap = new Bitmap(width, height);
using var graphics = Graphics.FromImage(bitmap);
// Fill with a random color background
var bgColor = Color.FromArgb(random.Next(256), random.Next(256), random.Next(256));
graphics.Clear(bgColor);
// Draw some random shapes to make it more realistic
for (int j = 0; j < 20; j++)
{
var color = Color.FromArgb(random.Next(256), random.Next(256), random.Next(256));
var brush = new SolidBrush(color);
var x = random.Next(width);
var y = random.Next(height);
var w = random.Next(200, 800);
var h = random.Next(200, 800);
graphics.FillEllipse(brush, x, y, w, h);
}
// Add some text
using var font = new Font("Arial", 48, FontStyle.Bold);
var text = $"Test Image {i}";
var textBrush = new SolidBrush(Color.White);
graphics.DrawString(text, font, textBrush, new PointF(100, 100));
// Save as JPEG with standard quality
var encoder = GetEncoder(ImageFormat.Jpeg);
var encoderParameters = new EncoderParameters(1);
encoderParameters.Param[0] = new EncoderParameter(Encoder.Quality, 85L);
bitmap.Save(filePath, encoder, encoderParameters);
}
}
/// <summary>
/// Cleans up generated test images
/// </summary>
public static void CleanupTestImages(string directory)
{
if (Directory.Exists(directory))
{
Directory.Delete(directory, recursive: true);
}
}
private static ImageCodecInfo GetEncoder(ImageFormat format)
{
var codecs = ImageCodecInfo.GetImageEncoders();
foreach (var codec in codecs)
{
if (codec.FormatID == format.Guid)
{
return codec;
}
}
return null;
}
}

View file

@ -0,0 +1,182 @@
using System;
using System.Collections.Concurrent;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Engines;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Toolchains.InProcess.Emit;
using MaddoShared.Benchmarks.Helpers;
using Microsoft.Extensions.Logging;
namespace MaddoShared.Benchmarks;
/// <summary>
/// Benchmarks for image processing with various configurations
/// </summary>
[MemoryDiagnoser]
[Config(typeof(InProcessConfig))]
public class ImageProcessingBenchmarks
{
private string _sourceDirectory;
private string _destinationDirectory;
private ImageCreationStuff _imageCreationStuff;
private PicSettings _picSettings;
private ILogger<ImageCreationStuff> _logger;
private ILogger<ImageCreatorSharp> _imageCreatorLogger;
[Params(10, 50, 100)]
public int ImageCount { get; set; }
[Params(1, 2, 4, 8)]
public int MaxThreads { get; set; }
[GlobalSetup]
public void Setup()
{
// Create temp directories
var tempBase = Path.Combine(Path.GetTempPath(), "ImageBenchmarks", Guid.NewGuid().ToString());
_sourceDirectory = Path.Combine(tempBase, "Source");
_destinationDirectory = Path.Combine(tempBase, "Destination");
Directory.CreateDirectory(_sourceDirectory);
Directory.CreateDirectory(_destinationDirectory);
// Generate test images
Console.WriteLine($"Generating {ImageCount} test images...");
TestImageGenerator.GenerateTestImages(_sourceDirectory, ImageCount, width: 2000, height: 1500);
// Setup logging
var loggerFactory = LoggerFactory.Create(builder =>
{
builder.SetMinimumLevel(LogLevel.Warning); // Reduce noise during benchmarks
});
_logger = loggerFactory.CreateLogger<ImageCreationStuff>();
_imageCreatorLogger = loggerFactory.CreateLogger<ImageCreatorSharp>();
// Setup PicSettings with default values
_picSettings = new PicSettings
{
DirectorySorgente = _sourceDirectory,
DirectoryDestinazione = _destinationDirectory,
DimStandard = 800,
DimStandardMiniatura = 200,
LarghezzaBig = 1024,
AltezzaBig = 768,
LarghezzaSmall = 200,
AltezzaSmall = 150,
CreaMiniature = true,
AggiungiScritteMiniature = false,
UsaForzaJpg = true,
UsaRotazioneAutomatica = true,
LogoAggiungi = false,
FotoGrandeDimOrigina = false,
TestoNome = false,
NomeData = false,
Suffisso = "_small",
Margine = 10,
Trasparenza = 100
};
var imageCreatorService = new ImageCreatorSharp(_picSettings, _imageCreatorLogger);
_imageCreationStuff = new ImageCreationStuff(_logger, _picSettings, imageCreatorService);
}
[GlobalCleanup]
public void Cleanup()
{
// Clean up temp directories
try
{
var tempBase = Path.GetDirectoryName(_sourceDirectory);
if (Directory.Exists(tempBase))
{
Directory.Delete(tempBase, recursive: true);
}
}
catch (Exception ex)
{
Console.WriteLine($"Cleanup error: {ex.Message}");
}
}
[IterationSetup]
public void IterationSetup()
{
// Clean destination directory before each iteration
if (Directory.Exists(_destinationDirectory))
{
Directory.Delete(_destinationDirectory, recursive: true);
}
Directory.CreateDirectory(_destinationDirectory);
}
[Benchmark(Description = "Process images in parallel with chunking")]
public async Task ProcessImagesParallelWithChunks()
{
var options = new ImageCreationStuff.Options
{
SourcePath = _sourceDirectory,
DestinationPath = _destinationDirectory,
MaxThreads = MaxThreads,
ChunksSize = 10,
LinearExecution = false,
AggiornaSottodirectory = false,
CreaSottocartelle = false,
FilePerCartella = 100,
SuffissoCartelle = "",
CifreContatore = 4,
NumerazioneType = NumerazioneType.Progressiva
};
var results = new ConcurrentBag<string>();
await _imageCreationStuff.ProcessImagesParallel(options, results, null, CancellationToken.None);
}
[Benchmark(Description = "Process images in parallel without chunking")]
public async Task ProcessImagesParallelWithoutChunks()
{
var options = new ImageCreationStuff.Options
{
SourcePath = _sourceDirectory,
DestinationPath = _destinationDirectory,
MaxThreads = MaxThreads,
ChunksSize = 0, // No chunking
LinearExecution = false,
AggiornaSottodirectory = false,
CreaSottocartelle = false,
FilePerCartella = 100,
SuffissoCartelle = "",
CifreContatore = 4,
NumerazioneType = NumerazioneType.Progressiva
};
var results = new ConcurrentBag<string>();
await _imageCreationStuff.ProcessImagesParallel(options, results, null, CancellationToken.None);
}
[Benchmark(Description = "Process images linearly")]
public async Task ProcessImagesLinear()
{
var options = new ImageCreationStuff.Options
{
SourcePath = _sourceDirectory,
DestinationPath = _destinationDirectory,
MaxThreads = MaxThreads,
ChunksSize = 0,
LinearExecution = true,
AggiornaSottodirectory = false,
CreaSottocartelle = false,
FilePerCartella = 100,
SuffissoCartelle = "",
CifreContatore = 4,
NumerazioneType = NumerazioneType.Progressiva
};
var results = new ConcurrentBag<string>();
await _imageCreationStuff.ProcessImagesParallel(options, results, null, CancellationToken.None);
}
}

View file

@ -0,0 +1,151 @@
using System;
using System.Collections.Concurrent;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Engines;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Toolchains.InProcess.Emit;
using MaddoShared.Benchmarks.Helpers;
using Microsoft.Extensions.Logging;
namespace MaddoShared.Benchmarks;
/// <summary>
/// Benchmarks for comparing performance with different image sizes
/// </summary>
[MemoryDiagnoser]
[Config(typeof(InProcessConfig))]
public class ImageSizeBenchmarks
{
private string _sourceDirectory;
private string _destinationDirectory;
private ImageCreationStuff _imageCreationStuff;
private PicSettings _picSettings;
[Params(50)]
public int ImageCount { get; set; }
public enum ImageSize
{
Small, // 1280x960
Medium, // 2560x1920
Large, // 4000x3000
ExtraLarge // 6000x4000
}
[ParamsAllValues]
public ImageSize Size { get; set; }
[GlobalSetup]
public void Setup()
{
var tempBase = Path.Combine(Path.GetTempPath(), "SizeBenchmarks", Guid.NewGuid().ToString());
_sourceDirectory = Path.Combine(tempBase, "Source");
_destinationDirectory = Path.Combine(tempBase, "Destination");
Directory.CreateDirectory(_sourceDirectory);
Directory.CreateDirectory(_destinationDirectory);
var (width, height) = GetDimensions(Size);
Console.WriteLine($"Generating {ImageCount} test images at {width}x{height}...");
TestImageGenerator.GenerateTestImages(_sourceDirectory, ImageCount, width, height);
var loggerFactory = LoggerFactory.Create(builder =>
{
builder.SetMinimumLevel(LogLevel.Warning);
});
var logger = loggerFactory.CreateLogger<ImageCreationStuff>();
var imageCreatorLogger = loggerFactory.CreateLogger<ImageCreatorSharp>();
_picSettings = new PicSettings
{
DirectorySorgente = _sourceDirectory,
DirectoryDestinazione = _destinationDirectory,
DimStandard = 800,
DimStandardMiniatura = 200,
LarghezzaBig = 1024,
AltezzaBig = 768,
LarghezzaSmall = 200,
AltezzaSmall = 150,
CreaMiniature = true,
AggiungiScritteMiniature = false,
UsaForzaJpg = true,
UsaRotazioneAutomatica = true,
LogoAggiungi = false,
FotoGrandeDimOrigina = false,
TestoNome = false,
NomeData = false,
Suffisso = "_small",
Margine = 10,
Trasparenza = 100
};
var imageCreatorService = new ImageCreatorSharp(_picSettings, imageCreatorLogger);
_imageCreationStuff = new ImageCreationStuff(logger, _picSettings, imageCreatorService);
}
private static (int width, int height) GetDimensions(ImageSize size)
{
return size switch
{
ImageSize.Small => (1280, 960),
ImageSize.Medium => (2560, 1920),
ImageSize.Large => (4000, 3000),
ImageSize.ExtraLarge => (6000, 4000),
_ => throw new ArgumentException($"Unknown size: {size}")
};
}
[GlobalCleanup]
public void Cleanup()
{
try
{
var tempBase = Path.GetDirectoryName(_sourceDirectory);
if (Directory.Exists(tempBase))
{
Directory.Delete(tempBase, recursive: true);
}
}
catch (Exception ex)
{
Console.WriteLine($"Cleanup error: {ex.Message}");
}
}
[IterationSetup]
public void IterationSetup()
{
if (Directory.Exists(_destinationDirectory))
{
Directory.Delete(_destinationDirectory, recursive: true);
}
Directory.CreateDirectory(_destinationDirectory);
}
[Benchmark]
public async Task ProcessDifferentImageSizes()
{
var options = new ImageCreationStuff.Options
{
SourcePath = _sourceDirectory,
DestinationPath = _destinationDirectory,
MaxThreads = Environment.ProcessorCount,
ChunksSize = 10,
LinearExecution = false,
AggiornaSottodirectory = false,
CreaSottocartelle = false,
FilePerCartella = 100,
SuffissoCartelle = "",
CifreContatore = 4,
NumerazioneType = NumerazioneType.Progressiva
};
var results = new ConcurrentBag<string>();
await _imageCreationStuff.ProcessImagesParallel(options, results, null, CancellationToken.None);
}
}

View file

@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0-windows</TargetFramework>
<PlatformTarget>x64</PlatformTarget>
<UseWindowsForms>true</UseWindowsForms>
<ImportWindowsDesktopTargets>true</ImportWindowsDesktopTargets>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.14.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.2" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="10.0.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MaddoShared\MaddoShared.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,35 @@
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Running;
using BenchmarkDotNet.Toolchains.InProcess.Emit;
using System;
using System.Linq;
namespace MaddoShared.Benchmarks;
internal class Program
{
static void Main(string[] args)
{
// Check if --job argument is provided
bool hasJobArg = args.Any(a => a.Contains("--job"));
if (hasJobArg)
{
Console.WriteLine("Note: Overriding --job argument to use InProcess toolchain");
Console.WriteLine("This is required to avoid net10.0 vs net10.0-windows compatibility issues.");
Console.WriteLine();
// Remove --job arguments and add our own InProcess config
args = args.Where(a => !a.StartsWith("--job") && a != "dry" && a != "short").ToArray();
}
// Create configuration that always uses InProcess toolchain
var config = DefaultConfig.Instance
.WithOptions(ConfigOptions.DisableOptimizationsValidator)
.WithOptions(ConfigOptions.KeepBenchmarkFiles);
// Run benchmarks - each class has [Config(typeof(InProcessConfig))] which provides InProcess toolchain
BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args, config);
}
}

View file

@ -0,0 +1,172 @@
using System;
using System.Collections.Concurrent;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Toolchains.InProcess.Emit;
using MaddoShared.Benchmarks.Helpers;
using Microsoft.Extensions.Logging;
namespace MaddoShared.Benchmarks;
/// <summary>
/// Stress test benchmark for large-scale image processing
/// WARNING: This will generate a large number of images and may take significant time and disk space
/// </summary>
[MemoryDiagnoser]
[Config(typeof(InProcessConfig))]
public class StressTestBenchmark
{
private string _sourceDirectory;
private string _destinationDirectory;
private ImageCreationStuff _imageCreationStuff;
private PicSettings _picSettings;
[Params(500, 1000)]
public int ImageCount { get; set; }
[GlobalSetup]
public void Setup()
{
var tempBase = Path.Combine(Path.GetTempPath(), "StressTestBenchmarks", Guid.NewGuid().ToString());
_sourceDirectory = Path.Combine(tempBase, "Source");
_destinationDirectory = Path.Combine(tempBase, "Destination");
Directory.CreateDirectory(_sourceDirectory);
Directory.CreateDirectory(_destinationDirectory);
Console.WriteLine($"[STRESS TEST] Generating {ImageCount} test images...");
Console.WriteLine("This may take several minutes depending on your hardware.");
// Use smaller images for stress test to save space and time
TestImageGenerator.GenerateTestImages(_sourceDirectory, ImageCount, width: 1920, height: 1080);
var loggerFactory = LoggerFactory.Create(builder =>
{
builder.SetMinimumLevel(LogLevel.Warning);
});
var logger = loggerFactory.CreateLogger<ImageCreationStuff>();
var imageCreatorLogger = loggerFactory.CreateLogger<ImageCreatorSharp>();
_picSettings = new PicSettings
{
DirectorySorgente = _sourceDirectory,
DirectoryDestinazione = _destinationDirectory,
DimStandard = 800,
DimStandardMiniatura = 200,
LarghezzaBig = 1024,
AltezzaBig = 768,
LarghezzaSmall = 200,
AltezzaSmall = 150,
CreaMiniature = true,
AggiungiScritteMiniature = false,
UsaForzaJpg = true,
UsaRotazioneAutomatica = true,
LogoAggiungi = false,
FotoGrandeDimOrigina = false,
TestoNome = false,
NomeData = false,
Suffisso = "_small",
Margine = 10,
Trasparenza = 100
};
var imageCreatorService = new ImageCreatorSharp(_picSettings, imageCreatorLogger);
_imageCreationStuff = new ImageCreationStuff(logger, _picSettings, imageCreatorService);
Console.WriteLine($"[STRESS TEST] Setup complete. Ready to process {ImageCount} images.");
}
[GlobalCleanup]
public void Cleanup()
{
Console.WriteLine("[STRESS TEST] Cleaning up test data...");
try
{
var tempBase = Path.GetDirectoryName(_sourceDirectory);
if (Directory.Exists(tempBase))
{
Directory.Delete(tempBase, recursive: true);
}
}
catch (Exception ex)
{
Console.WriteLine($"Cleanup error: {ex.Message}");
}
}
[IterationSetup]
public void IterationSetup()
{
if (Directory.Exists(_destinationDirectory))
{
Directory.Delete(_destinationDirectory, recursive: true);
}
Directory.CreateDirectory(_destinationDirectory);
GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive, blocking: true, compacting: true);
}
[Benchmark(Description = "Stress test with optimal settings")]
public async Task StressTestOptimalSettings()
{
var options = new ImageCreationStuff.Options
{
SourcePath = _sourceDirectory,
DestinationPath = _destinationDirectory,
MaxThreads = Environment.ProcessorCount,
ChunksSize = 25, // Process in chunks to manage memory
LinearExecution = false,
AggiornaSottodirectory = false,
CreaSottocartelle = false,
FilePerCartella = 100,
SuffissoCartelle = "",
CifreContatore = 4,
NumerazioneType = NumerazioneType.Progressiva
};
var results = new ConcurrentBag<string>();
var startTime = DateTime.Now;
await _imageCreationStuff.ProcessImagesParallel(options, results, null, CancellationToken.None);
var duration = DateTime.Now - startTime;
var throughput = ImageCount / duration.TotalSeconds;
Console.WriteLine($"[STRESS TEST] Processed {results.Count}/{ImageCount} images in {duration.TotalSeconds:F2}s");
Console.WriteLine($"[STRESS TEST] Throughput: {throughput:F2} images/second");
}
[Benchmark(Description = "Stress test with aggressive memory management")]
public async Task StressTestAggressiveMemoryManagement()
{
var options = new ImageCreationStuff.Options
{
SourcePath = _sourceDirectory,
DestinationPath = _destinationDirectory,
MaxThreads = Environment.ProcessorCount / 2, // Reduce threads to save memory
ChunksSize = 10, // Smaller chunks for more frequent GC
LinearExecution = false,
AggiornaSottodirectory = false,
CreaSottocartelle = false,
FilePerCartella = 100,
SuffissoCartelle = "",
CifreContatore = 4,
NumerazioneType = NumerazioneType.Progressiva
};
var results = new ConcurrentBag<string>();
var startTime = DateTime.Now;
await _imageCreationStuff.ProcessImagesParallel(options, results, null, CancellationToken.None);
var duration = DateTime.Now - startTime;
var throughput = ImageCount / duration.TotalSeconds;
Console.WriteLine($"[STRESS TEST] Processed {results.Count}/{ImageCount} images in {duration.TotalSeconds:F2}s");
Console.WriteLine($"[STRESS TEST] Throughput: {throughput:F2} images/second");
}
}

View file

@ -52,6 +52,15 @@ namespace MaddoShared
$"{stopwatch.Elapsed.Hours}h {stopwatch.Elapsed.Minutes}m {stopwatch.Elapsed.Seconds}s ({stopwatch.Elapsed.TotalSeconds}s)";
}
/// <summary>
/// Gets the list of files that will be processed based on the provided options.
/// Useful for benchmarking and testing to understand the scope of work.
/// </summary>
public List<FileData> GetFilesToProcessPublic(Options options)
{
return GetFilesToProcess(options);
}
public async Task ProcessImagesParallel(
Options options,
ConcurrentBag<string> results,

View file

@ -194,6 +194,21 @@ public class ImageCreatorSharp(PicSettings picSettings, ILogger<ImageCreatorShar
// nomeFileBig = NomeFileChild
imgState.NomeFileSmall = picSettings.Suffisso + imgState.WorkFile.Name;
imgState.NomeFileBig = imgState.WorkFile.Name;
// Sanitize file names to avoid invalid characters causing IO errors
imgState.NomeFileSmall = SanitizeFileName(imgState.NomeFileSmall);
imgState.NomeFileBig = SanitizeFileName(imgState.NomeFileBig);
}
private static string SanitizeFileName(string fileName)
{
if (string.IsNullOrEmpty(fileName)) return fileName;
var invalid = Path.GetInvalidFileNameChars();
var sb = new System.Text.StringBuilder(fileName.Length);
foreach (var ch in fileName)
{
sb.Append(Array.IndexOf(invalid, ch) >= 0 ? '_' : ch);
}
return sb.ToString();
}
private void PrepareThumbnailSize(Image g, ImageState imgState)

View file

@ -1010,6 +1010,8 @@ namespace ImageCatalog_2
public event EventHandler<string> SaveSettingsRequested;
public event EventHandler<string> LoadSettingsRequested;
public event EventHandler SelectColorRequested;
// Request that the View shows a message to the user (message, caption, icon)
public event EventHandler<Tuple<string, string, MessageBoxIcon>> ShowMessageRequested;
private void SelectSourceFolder(object parameter)
{

File diff suppressed because it is too large Load diff

View file

@ -7,6 +7,7 @@ using System.Drawing.Text;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
@ -47,6 +48,10 @@ public partial class MainForm
BindControls();
// Wire up 'Open folder in Explorer' buttons
btnOpenSourceFolder.Click += BtnOpenSourceFolder_Click;
btnOpenDestFolder.Click += BtnOpenDestFolder_Click;
var version = Assembly.GetExecutingAssembly().GetName().Version;
_Label27.Text = $"Version: {version.Major}.{version.Minor}.{version.Build}.{version.Revision}";
}
@ -70,6 +75,21 @@ public partial class MainForm
Model.SaveSettingsRequested += OnSaveSettingsRequested;
Model.LoadSettingsRequested += OnLoadSettingsRequested;
Model.SelectColorRequested += OnSelectColorRequested;
// Show message requests (from ViewModel validation)
Model.ShowMessageRequested += OnShowMessageRequested;
}
private void OnShowMessageRequested(object? sender, Tuple<string, string, MessageBoxIcon> args)
{
if (args is null) return;
// Ensure call on UI thread
if (InvokeRequired)
{
Invoke(new Action(() => OnShowMessageRequested(sender, args)));
return;
}
MessageBox.Show(this, args.Item1, args.Item2, MessageBoxButtons.OK, args.Item3);
}
private void SetDefaults()
@ -181,6 +201,57 @@ public partial class MainForm
}
}
private void BtnOpenSourceFolder_Click(object? sender, EventArgs e)
{
// Prefer the model value but fall back to the textbox if needed
var path = string.IsNullOrWhiteSpace(Model.SourcePath) ? txtSorgente.Text : Model.SourcePath;
OpenFolder(path);
}
private void BtnOpenDestFolder_Click(object? sender, EventArgs e)
{
var path = string.IsNullOrWhiteSpace(Model.DestinationPath) ? txtDestinazione.Text : Model.DestinationPath;
OpenFolder(path);
}
private void OpenFolder(string? path)
{
if (string.IsNullOrWhiteSpace(path))
{
MessageBox.Show(this, "Folder path is empty.", "Open Folder", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
path = path.Trim().Trim('"');
try
{
if (File.Exists(path))
{
// If a file was provided, open its folder and select it
Process.Start("explorer.exe", $"/select,\"{path}\"");
return;
}
if (Directory.Exists(path))
{
Process.Start(new ProcessStartInfo
{
FileName = path,
UseShellExecute = true
});
return;
}
MessageBox.Show(this, $"Folder does not exist: {path}", "Open Folder", MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
catch (Exception ex)
{
_logger?.LogError(ex, "Failed to open folder {Path}", path);
MessageBox.Show(this, $"Failed to open folder: {ex.Message}", "Open Folder", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
private void OnSelectDestinationFolderRequested(object sender, EventArgs e)
{
var dialogResult = SelectFolder(Model.DestinationPath);

124
run-benchmarks.ps1 Normal file
View file

@ -0,0 +1,124 @@
# Image Processing Benchmark Runner
# This script provides easy access to common benchmark scenarios
param(
[Parameter(Position=0)]
[ValidateSet("all", "quick", "parallel", "chunks", "sizes", "stress", "help")]
[string]$Scenario = "help",
[switch]$Fast,
[switch]$DetailedOutput
)
$projectPath = "MaddoShared.Benchmarks"
function Show-Help {
Write-Host @"
Image Processing Benchmark Runner
==================================
Usage: .\run-benchmarks.ps1 [scenario] [-Fast] [-DetailedOutput]
Scenarios:
all - Run all benchmarks (60-120 minutes)
quick - Fast test run for development (5-10 minutes)
parallel - Test parallel processing strategies (20-30 minutes)
chunks - Optimize chunk size (20-30 minutes)
sizes - Test different image sizes (45-90 minutes)
stress - Large-scale stress test (2-4 hours)
help - Show this help message
Flags:
-Fast - Use dry run for faster but less accurate results
-DetailedOutput - Show detailed output
Examples:
.\run-benchmarks.ps1 quick
.\run-benchmarks.ps1 parallel -Fast
.\run-benchmarks.ps1 stress -DetailedOutput
Results will be saved to: MaddoShared.Benchmarks\BenchmarkDotNet.Artifacts\results\
"@
}
function Run-Benchmark {
param(
[string]$Filter,
[string]$Description
)
Write-Host "`n========================================" -ForegroundColor Cyan
Write-Host " $Description" -ForegroundColor Cyan
Write-Host "========================================`n" -ForegroundColor Cyan
$args = @("-c", "Release")
if ($Filter) {
$args += @("--", "--filter", $Filter)
}
# Note: We don't pass --job arguments anymore because they override
# the InProcess toolchain configuration needed for Windows compatibility
if ($Fast) {
Write-Host "Note: Fast mode (-Fast) requested but not using --job dry" -ForegroundColor Yellow
Write-Host "Reason: --job arguments override InProcess toolchain config" -ForegroundColor Yellow
Write-Host "The benchmark will run with the default InProcessConfig settings.`n" -ForegroundColor Yellow
}
if ($DetailedOutput) {
$args += "--verbosity", "detailed"
}
Push-Location $projectPath
try {
dotnet run @args
}
finally {
Pop-Location
}
}
# Main execution
switch ($Scenario) {
"help" {
Show-Help
}
"all" {
Write-Host "Running ALL benchmarks..." -ForegroundColor Yellow
Write-Host "This will take 60-120 minutes. Press Ctrl+C to cancel.`n" -ForegroundColor Yellow
Start-Sleep -Seconds 3
Run-Benchmark "" "All Benchmarks"
}
"quick" {
Write-Host "Running QUICK test..." -ForegroundColor Green
Run-Benchmark "*ImageProcessingBenchmarks*" "Quick Development Test"
}
"parallel" {
Write-Host "Running PARALLEL processing benchmarks..." -ForegroundColor Green
Run-Benchmark "*ImageProcessingBenchmarks*" "Parallel Processing Strategies"
}
"chunks" {
Write-Host "Running CHUNK SIZE optimization..." -ForegroundColor Green
Run-Benchmark "*ChunkSizeBenchmarks*" "Chunk Size Optimization"
}
"sizes" {
Write-Host "Running IMAGE SIZE comparison..." -ForegroundColor Green
Run-Benchmark "*ImageSizeBenchmarks*" "Image Size Impact Analysis"
}
"stress" {
Write-Host "Running STRESS TEST..." -ForegroundColor Red
Write-Host "WARNING: This will take 2-4 hours and use significant disk space!" -ForegroundColor Red
Write-Host "Press Ctrl+C within 5 seconds to cancel...`n" -ForegroundColor Red
Start-Sleep -Seconds 5
Run-Benchmark "*StressTestBenchmark*" "Large-Scale Stress Test"
}
}
Write-Host "`nBenchmark execution complete!" -ForegroundColor Green
Write-Host "Results saved to: $projectPath\BenchmarkDotNet.Artifacts\results\" -ForegroundColor Green

134
run-benchmarks.sh Normal file
View file

@ -0,0 +1,134 @@
#!/bin/bash
# Image Processing Benchmark Runner (Linux/Mac)
# This script provides easy access to common benchmark scenarios
show_help() {
cat << EOF
Image Processing Benchmark Runner
==================================
Usage: ./run-benchmarks.sh [scenario] [--fast] [--verbose]
Scenarios:
all - Run all benchmarks (60-120 minutes)
quick - Fast test run for development (5-10 minutes)
parallel - Test parallel processing strategies (20-30 minutes)
chunks - Optimize chunk size (20-30 minutes)
sizes - Test different image sizes (45-90 minutes)
stress - Large-scale stress test (2-4 hours)
help - Show this help message
Flags:
--fast - Use dry run for faster but less accurate results
--verbose - Show detailed output
Examples:
./run-benchmarks.sh quick
./run-benchmarks.sh parallel --fast
./run-benchmarks.sh stress --verbose
Results will be saved to: MaddoShared.Benchmarks/BenchmarkDotNet.Artifacts/results/
EOF
}
run_benchmark() {
local filter=$1
local description=$2
echo ""
echo "========================================"
echo " $description"
echo "========================================"
echo ""
local args="-c Release"
if [ -n "$filter" ]; then
args="$args -- --filter \"$filter\""
fi
if [ "$fast_mode" = true ]; then
args="$args --job dry"
fi
if [ "$verbose_mode" = true ]; then
args="$args --verbose"
fi
cd MaddoShared.Benchmarks
eval "dotnet run $args"
cd ..
}
# Parse arguments
scenario="${1:-help}"
fast_mode=false
verbose_mode=false
shift
while [ $# -gt 0 ]; do
case "$1" in
--fast)
fast_mode=true
;;
--verbose)
verbose_mode=true
;;
esac
shift
done
# Main execution
case "$scenario" in
help)
show_help
;;
all)
echo "Running ALL benchmarks..."
echo "This will take 60-120 minutes. Press Ctrl+C to cancel."
echo ""
sleep 3
run_benchmark "" "All Benchmarks"
;;
quick)
echo "Running QUICK test..."
run_benchmark "*ImageProcessingBenchmarks*" "Quick Development Test"
;;
parallel)
echo "Running PARALLEL processing benchmarks..."
run_benchmark "*ImageProcessingBenchmarks*" "Parallel Processing Strategies"
;;
chunks)
echo "Running CHUNK SIZE optimization..."
run_benchmark "*ChunkSizeBenchmarks*" "Chunk Size Optimization"
;;
sizes)
echo "Running IMAGE SIZE comparison..."
run_benchmark "*ImageSizeBenchmarks*" "Image Size Impact Analysis"
;;
stress)
echo "Running STRESS TEST..."
echo "WARNING: This will take 2-4 hours and use significant disk space!"
echo "Press Ctrl+C within 5 seconds to cancel..."
echo ""
sleep 5
run_benchmark "*StressTestBenchmark*" "Large-Scale Stress Test"
;;
*)
echo "Unknown scenario: $scenario"
echo "Run './run-benchmarks.sh help' for usage information"
exit 1
;;
esac
echo ""
echo "Benchmark execution complete!"
echo "Results saved to: MaddoShared.Benchmarks/BenchmarkDotNet.Artifacts/results/"