feat(benchmark): build jobs from DXF manifests and run solves in parallel

Benchmark jobs could only come from .nest files. A JSON manifest now lists
DXF files with quantities (plus sheet sizes, spacing, edge spacing, quadrant
and per-part allowRotation), imported through CadImporter. DXF paths resolve
relative to the manifest; sheet sizes are required from the manifest or
--sheet-sizes and are read in the DXFs' own units. Folder scans pick up
*.nest and *.manifest.json, and invalid manifests fail loudly.

BenchmarkRunner now runs (job x engine) solves concurrently, capped by
--parallel N (CLI default 3; --parallel 1 is sequential). Results are written
by index so report order is unchanged. Concurrent solves compete for cores,
so Time(ms) is only clean at --parallel 1; the run prints a note when N > 1.

Also fixes --output for manifest jobs, which tried to read the manifest as a
.nest to copy metadata from.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
aj
2026-09-20 21:43:41 -04:00
co-authored by Claude Sonnet 5
parent e7cbd99db6
commit 5061b41a5d
9 changed files with 690 additions and 28 deletions
+48 -3
View File
@@ -38,7 +38,7 @@ static class BenchmarkConsole
if (jobs.Count == 0)
{
Console.Error.WriteLine(
"No benchmark jobs found (no .nest files with any drawing quantity > 0)."
"No benchmark jobs found (no .nest files with any drawing quantity > 0, or *.manifest.json files)."
);
return 1;
}
@@ -83,12 +83,23 @@ static class BenchmarkConsole
Console.WriteLine($"Engines: {string.Join(", ", engines.Select(e => e.Name))}");
var solves = jobs.Count * engines.Count;
if (options.Parallel > 1 && solves > 1)
{
Console.WriteLine(
$"Running up to {options.Parallel} solves at a time; Time(ms) is measured under that "
+ "concurrent load. Use --parallel 1 for strictly isolated timings."
);
}
var results = BenchmarkRunner.Run(
jobs,
engines,
options.SalvageRate,
options.MinimumSalvageDimension,
options.OutputDirectory
options.OutputDirectory,
options.Parallel
);
Report.PrintDetailed(results);
@@ -149,6 +160,15 @@ static class BenchmarkConsole
o.OutputDirectory = args[++i];
break;
case "--parallel" when i + 1 < args.Length:
if (int.TryParse(args[++i], out var parallel) && parallel >= 1)
o.Parallel = parallel;
else
Console.Error.WriteLine(
$"Warning: --parallel needs a whole number >= 1, using {o.Parallel}"
);
break;
case "--help":
PrintUsage();
return null;
@@ -215,7 +235,25 @@ static class BenchmarkConsole
);
Console.Error.WriteLine();
Console.Error.WriteLine("Usage:");
Console.Error.WriteLine(" OpenNest.Benchmark <file.nest | folder> [options]");
Console.Error.WriteLine(
" OpenNest.Benchmark <file.nest | manifest.json | folder> [options]"
);
Console.Error.WriteLine();
Console.Error.WriteLine(
"A manifest.json builds a job straight from DXF files (paths relative to the manifest):"
);
Console.Error.WriteLine(
" { \"sheetSizes\": [\"48x96\"], \"spacing\": 0.25, \"edgeSpacing\": 0.25, \"quadrant\": 1,"
);
Console.Error.WriteLine(
" \"parts\": [ { \"dxf\": \"a.dxf\", \"quantity\": 12 }, { \"dxf\": \"b.dxf\", \"quantity\": 4, \"allowRotation\": false } ] }"
);
Console.Error.WriteLine(
"Sheet sizes must use the same units as the DXFs. A folder is scanned for *.nest and"
);
Console.Error.WriteLine(
"*.manifest.json files. --sheet-sizes and --spacing override the manifest."
);
Console.Error.WriteLine();
Console.Error.WriteLine("Options:");
Console.Error.WriteLine(
@@ -242,6 +280,12 @@ static class BenchmarkConsole
Console.Error.WriteLine(
" --output <directory> Save valid layouts as .nest plus detailed JSON reports"
);
Console.Error.WriteLine(
" --parallel <n> Solves to run at once (default 3; 1 = strictly sequential,"
);
Console.Error.WriteLine(
" which gives the cleanest per-engine timings)"
);
Console.Error.WriteLine(" --help Show this message");
}
@@ -255,5 +299,6 @@ static class BenchmarkConsole
public string OutputDirectory;
public double SalvageRate;
public double MinimumSalvageDimension;
public int Parallel = 3;
}
}