From bb91e6db2b09e743ea7271f961cb94af756062d2 Mon Sep 17 00:00:00 2001 From: Chris Wolfgang <210299580+Chris-Wolfgang@users.noreply.github.com> Date: Mon, 22 Jun 2026 22:31:26 -0400 Subject: [PATCH] perf: add BenchmarkDotNet baseline project (#163, #185) Stands up benchmarks/Wolfgang.Etl.Abstractions.Benchmarks measuring the abstraction overhead the library adds over bare in-memory sequences: - ExtractorBenchmarks: ExtractorBase extraction loop, no-progress vs with-progress (Interlocked counter + timer/final-report path). - PipelineBenchmarks: fluent Pipeline builder vs hand-wired IAsyncEnumerable composition vs base-class composition (the builder is documented as zero-extra-allocation sugar; the benchmark verifies it). #185: benchmarks/.editorconfig relaxes the analyzer rules that don't fit a benchmark harness (AsyncFixer01, MA0004, S1215, VSTHRD200), mirroring the ETL-FixedWidth reference. Benchmark components are internal; build clean under Release/TreatWarningsAsErrors. The gh-pages publish workflow (#164) follows in a separate protected-file PR. Co-Authored-By: Claude Opus 4.8 --- ETL-Abstractions.sln | 163 ++++++++++++++++++ benchmarks/.editorconfig | 16 ++ .../BenchmarkComponents.cs | 161 +++++++++++++++++ .../ExtractorBenchmarks.cs | 63 +++++++ .../PipelineBenchmarks.cs | 59 +++++++ .../Program.cs | 5 + ...olfgang.Etl.Abstractions.Benchmarks.csproj | 20 +++ 7 files changed, 487 insertions(+) create mode 100644 benchmarks/.editorconfig create mode 100644 benchmarks/Wolfgang.Etl.Abstractions.Benchmarks/BenchmarkComponents.cs create mode 100644 benchmarks/Wolfgang.Etl.Abstractions.Benchmarks/ExtractorBenchmarks.cs create mode 100644 benchmarks/Wolfgang.Etl.Abstractions.Benchmarks/PipelineBenchmarks.cs create mode 100644 benchmarks/Wolfgang.Etl.Abstractions.Benchmarks/Program.cs create mode 100644 benchmarks/Wolfgang.Etl.Abstractions.Benchmarks/Wolfgang.Etl.Abstractions.Benchmarks.csproj diff --git a/ETL-Abstractions.sln b/ETL-Abstractions.sln index a80b8315..ab191738 100644 --- a/ETL-Abstractions.sln +++ b/ETL-Abstractions.sln @@ -81,84 +81,246 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "benchmarks", "benchmarks", "{AE206253-B766-4B6A-8C08-9E70605A2B27}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wolfgang.Etl.Abstractions.Benchmarks", "benchmarks\Wolfgang.Etl.Abstractions.Benchmarks\Wolfgang.Etl.Abstractions.Benchmarks.csproj", "{7855D034-57F4-4CF0-8EB4-5677EF80EC57}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {C4987BAD-4513-955F-B3C1-7563D0C1A7A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C4987BAD-4513-955F-B3C1-7563D0C1A7A3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C4987BAD-4513-955F-B3C1-7563D0C1A7A3}.Debug|x64.ActiveCfg = Debug|Any CPU + {C4987BAD-4513-955F-B3C1-7563D0C1A7A3}.Debug|x64.Build.0 = Debug|Any CPU + {C4987BAD-4513-955F-B3C1-7563D0C1A7A3}.Debug|x86.ActiveCfg = Debug|Any CPU + {C4987BAD-4513-955F-B3C1-7563D0C1A7A3}.Debug|x86.Build.0 = Debug|Any CPU {C4987BAD-4513-955F-B3C1-7563D0C1A7A3}.Release|Any CPU.ActiveCfg = Release|Any CPU {C4987BAD-4513-955F-B3C1-7563D0C1A7A3}.Release|Any CPU.Build.0 = Release|Any CPU + {C4987BAD-4513-955F-B3C1-7563D0C1A7A3}.Release|x64.ActiveCfg = Release|Any CPU + {C4987BAD-4513-955F-B3C1-7563D0C1A7A3}.Release|x64.Build.0 = Release|Any CPU + {C4987BAD-4513-955F-B3C1-7563D0C1A7A3}.Release|x86.ActiveCfg = Release|Any CPU + {C4987BAD-4513-955F-B3C1-7563D0C1A7A3}.Release|x86.Build.0 = Release|Any CPU {B8558C7F-934B-3DC1-AAED-D668CF964C8E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B8558C7F-934B-3DC1-AAED-D668CF964C8E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B8558C7F-934B-3DC1-AAED-D668CF964C8E}.Debug|x64.ActiveCfg = Debug|Any CPU + {B8558C7F-934B-3DC1-AAED-D668CF964C8E}.Debug|x64.Build.0 = Debug|Any CPU + {B8558C7F-934B-3DC1-AAED-D668CF964C8E}.Debug|x86.ActiveCfg = Debug|Any CPU + {B8558C7F-934B-3DC1-AAED-D668CF964C8E}.Debug|x86.Build.0 = Debug|Any CPU {B8558C7F-934B-3DC1-AAED-D668CF964C8E}.Release|Any CPU.ActiveCfg = Release|Any CPU {B8558C7F-934B-3DC1-AAED-D668CF964C8E}.Release|Any CPU.Build.0 = Release|Any CPU + {B8558C7F-934B-3DC1-AAED-D668CF964C8E}.Release|x64.ActiveCfg = Release|Any CPU + {B8558C7F-934B-3DC1-AAED-D668CF964C8E}.Release|x64.Build.0 = Release|Any CPU + {B8558C7F-934B-3DC1-AAED-D668CF964C8E}.Release|x86.ActiveCfg = Release|Any CPU + {B8558C7F-934B-3DC1-AAED-D668CF964C8E}.Release|x86.Build.0 = Release|Any CPU {F1BA1AF5-9A71-421B-963C-E277610FBD40}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F1BA1AF5-9A71-421B-963C-E277610FBD40}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F1BA1AF5-9A71-421B-963C-E277610FBD40}.Debug|x64.ActiveCfg = Debug|Any CPU + {F1BA1AF5-9A71-421B-963C-E277610FBD40}.Debug|x64.Build.0 = Debug|Any CPU + {F1BA1AF5-9A71-421B-963C-E277610FBD40}.Debug|x86.ActiveCfg = Debug|Any CPU + {F1BA1AF5-9A71-421B-963C-E277610FBD40}.Debug|x86.Build.0 = Debug|Any CPU {F1BA1AF5-9A71-421B-963C-E277610FBD40}.Release|Any CPU.ActiveCfg = Release|Any CPU {F1BA1AF5-9A71-421B-963C-E277610FBD40}.Release|Any CPU.Build.0 = Release|Any CPU + {F1BA1AF5-9A71-421B-963C-E277610FBD40}.Release|x64.ActiveCfg = Release|Any CPU + {F1BA1AF5-9A71-421B-963C-E277610FBD40}.Release|x64.Build.0 = Release|Any CPU + {F1BA1AF5-9A71-421B-963C-E277610FBD40}.Release|x86.ActiveCfg = Release|Any CPU + {F1BA1AF5-9A71-421B-963C-E277610FBD40}.Release|x86.Build.0 = Release|Any CPU {B715D0C5-3F1A-485C-92D1-FFB87A801AC3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B715D0C5-3F1A-485C-92D1-FFB87A801AC3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B715D0C5-3F1A-485C-92D1-FFB87A801AC3}.Debug|x64.ActiveCfg = Debug|Any CPU + {B715D0C5-3F1A-485C-92D1-FFB87A801AC3}.Debug|x64.Build.0 = Debug|Any CPU + {B715D0C5-3F1A-485C-92D1-FFB87A801AC3}.Debug|x86.ActiveCfg = Debug|Any CPU + {B715D0C5-3F1A-485C-92D1-FFB87A801AC3}.Debug|x86.Build.0 = Debug|Any CPU {B715D0C5-3F1A-485C-92D1-FFB87A801AC3}.Release|Any CPU.ActiveCfg = Release|Any CPU {B715D0C5-3F1A-485C-92D1-FFB87A801AC3}.Release|Any CPU.Build.0 = Release|Any CPU + {B715D0C5-3F1A-485C-92D1-FFB87A801AC3}.Release|x64.ActiveCfg = Release|Any CPU + {B715D0C5-3F1A-485C-92D1-FFB87A801AC3}.Release|x64.Build.0 = Release|Any CPU + {B715D0C5-3F1A-485C-92D1-FFB87A801AC3}.Release|x86.ActiveCfg = Release|Any CPU + {B715D0C5-3F1A-485C-92D1-FFB87A801AC3}.Release|x86.Build.0 = Release|Any CPU {F4A2F47C-9687-0ABE-FEAE-D07873E0133A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F4A2F47C-9687-0ABE-FEAE-D07873E0133A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F4A2F47C-9687-0ABE-FEAE-D07873E0133A}.Debug|x64.ActiveCfg = Debug|Any CPU + {F4A2F47C-9687-0ABE-FEAE-D07873E0133A}.Debug|x64.Build.0 = Debug|Any CPU + {F4A2F47C-9687-0ABE-FEAE-D07873E0133A}.Debug|x86.ActiveCfg = Debug|Any CPU + {F4A2F47C-9687-0ABE-FEAE-D07873E0133A}.Debug|x86.Build.0 = Debug|Any CPU {F4A2F47C-9687-0ABE-FEAE-D07873E0133A}.Release|Any CPU.ActiveCfg = Release|Any CPU {F4A2F47C-9687-0ABE-FEAE-D07873E0133A}.Release|Any CPU.Build.0 = Release|Any CPU + {F4A2F47C-9687-0ABE-FEAE-D07873E0133A}.Release|x64.ActiveCfg = Release|Any CPU + {F4A2F47C-9687-0ABE-FEAE-D07873E0133A}.Release|x64.Build.0 = Release|Any CPU + {F4A2F47C-9687-0ABE-FEAE-D07873E0133A}.Release|x86.ActiveCfg = Release|Any CPU + {F4A2F47C-9687-0ABE-FEAE-D07873E0133A}.Release|x86.Build.0 = Release|Any CPU {733AD8E6-D170-789A-6F61-13C041011037}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {733AD8E6-D170-789A-6F61-13C041011037}.Debug|Any CPU.Build.0 = Debug|Any CPU + {733AD8E6-D170-789A-6F61-13C041011037}.Debug|x64.ActiveCfg = Debug|Any CPU + {733AD8E6-D170-789A-6F61-13C041011037}.Debug|x64.Build.0 = Debug|Any CPU + {733AD8E6-D170-789A-6F61-13C041011037}.Debug|x86.ActiveCfg = Debug|Any CPU + {733AD8E6-D170-789A-6F61-13C041011037}.Debug|x86.Build.0 = Debug|Any CPU {733AD8E6-D170-789A-6F61-13C041011037}.Release|Any CPU.ActiveCfg = Release|Any CPU {733AD8E6-D170-789A-6F61-13C041011037}.Release|Any CPU.Build.0 = Release|Any CPU + {733AD8E6-D170-789A-6F61-13C041011037}.Release|x64.ActiveCfg = Release|Any CPU + {733AD8E6-D170-789A-6F61-13C041011037}.Release|x64.Build.0 = Release|Any CPU + {733AD8E6-D170-789A-6F61-13C041011037}.Release|x86.ActiveCfg = Release|Any CPU + {733AD8E6-D170-789A-6F61-13C041011037}.Release|x86.Build.0 = Release|Any CPU {2DC16382-F59F-4024-B560-D400E09EF91F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2DC16382-F59F-4024-B560-D400E09EF91F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2DC16382-F59F-4024-B560-D400E09EF91F}.Debug|x64.ActiveCfg = Debug|Any CPU + {2DC16382-F59F-4024-B560-D400E09EF91F}.Debug|x64.Build.0 = Debug|Any CPU + {2DC16382-F59F-4024-B560-D400E09EF91F}.Debug|x86.ActiveCfg = Debug|Any CPU + {2DC16382-F59F-4024-B560-D400E09EF91F}.Debug|x86.Build.0 = Debug|Any CPU {2DC16382-F59F-4024-B560-D400E09EF91F}.Release|Any CPU.ActiveCfg = Release|Any CPU {2DC16382-F59F-4024-B560-D400E09EF91F}.Release|Any CPU.Build.0 = Release|Any CPU + {2DC16382-F59F-4024-B560-D400E09EF91F}.Release|x64.ActiveCfg = Release|Any CPU + {2DC16382-F59F-4024-B560-D400E09EF91F}.Release|x64.Build.0 = Release|Any CPU + {2DC16382-F59F-4024-B560-D400E09EF91F}.Release|x86.ActiveCfg = Release|Any CPU + {2DC16382-F59F-4024-B560-D400E09EF91F}.Release|x86.Build.0 = Release|Any CPU {5A7CFA9D-DCE7-43FD-AF89-7418C36AAE14}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5A7CFA9D-DCE7-43FD-AF89-7418C36AAE14}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5A7CFA9D-DCE7-43FD-AF89-7418C36AAE14}.Debug|x64.ActiveCfg = Debug|Any CPU + {5A7CFA9D-DCE7-43FD-AF89-7418C36AAE14}.Debug|x64.Build.0 = Debug|Any CPU + {5A7CFA9D-DCE7-43FD-AF89-7418C36AAE14}.Debug|x86.ActiveCfg = Debug|Any CPU + {5A7CFA9D-DCE7-43FD-AF89-7418C36AAE14}.Debug|x86.Build.0 = Debug|Any CPU {5A7CFA9D-DCE7-43FD-AF89-7418C36AAE14}.Release|Any CPU.ActiveCfg = Release|Any CPU {5A7CFA9D-DCE7-43FD-AF89-7418C36AAE14}.Release|Any CPU.Build.0 = Release|Any CPU + {5A7CFA9D-DCE7-43FD-AF89-7418C36AAE14}.Release|x64.ActiveCfg = Release|Any CPU + {5A7CFA9D-DCE7-43FD-AF89-7418C36AAE14}.Release|x64.Build.0 = Release|Any CPU + {5A7CFA9D-DCE7-43FD-AF89-7418C36AAE14}.Release|x86.ActiveCfg = Release|Any CPU + {5A7CFA9D-DCE7-43FD-AF89-7418C36AAE14}.Release|x86.Build.0 = Release|Any CPU {483AE567-071E-4797-B13B-84B142D4ED44}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {483AE567-071E-4797-B13B-84B142D4ED44}.Debug|Any CPU.Build.0 = Debug|Any CPU + {483AE567-071E-4797-B13B-84B142D4ED44}.Debug|x64.ActiveCfg = Debug|Any CPU + {483AE567-071E-4797-B13B-84B142D4ED44}.Debug|x64.Build.0 = Debug|Any CPU + {483AE567-071E-4797-B13B-84B142D4ED44}.Debug|x86.ActiveCfg = Debug|Any CPU + {483AE567-071E-4797-B13B-84B142D4ED44}.Debug|x86.Build.0 = Debug|Any CPU {483AE567-071E-4797-B13B-84B142D4ED44}.Release|Any CPU.ActiveCfg = Release|Any CPU {483AE567-071E-4797-B13B-84B142D4ED44}.Release|Any CPU.Build.0 = Release|Any CPU + {483AE567-071E-4797-B13B-84B142D4ED44}.Release|x64.ActiveCfg = Release|Any CPU + {483AE567-071E-4797-B13B-84B142D4ED44}.Release|x64.Build.0 = Release|Any CPU + {483AE567-071E-4797-B13B-84B142D4ED44}.Release|x86.ActiveCfg = Release|Any CPU + {483AE567-071E-4797-B13B-84B142D4ED44}.Release|x86.Build.0 = Release|Any CPU {6C19204D-7CC8-48D2-A475-49CD98931105}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6C19204D-7CC8-48D2-A475-49CD98931105}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6C19204D-7CC8-48D2-A475-49CD98931105}.Debug|x64.ActiveCfg = Debug|Any CPU + {6C19204D-7CC8-48D2-A475-49CD98931105}.Debug|x64.Build.0 = Debug|Any CPU + {6C19204D-7CC8-48D2-A475-49CD98931105}.Debug|x86.ActiveCfg = Debug|Any CPU + {6C19204D-7CC8-48D2-A475-49CD98931105}.Debug|x86.Build.0 = Debug|Any CPU {6C19204D-7CC8-48D2-A475-49CD98931105}.Release|Any CPU.ActiveCfg = Release|Any CPU {6C19204D-7CC8-48D2-A475-49CD98931105}.Release|Any CPU.Build.0 = Release|Any CPU + {6C19204D-7CC8-48D2-A475-49CD98931105}.Release|x64.ActiveCfg = Release|Any CPU + {6C19204D-7CC8-48D2-A475-49CD98931105}.Release|x64.Build.0 = Release|Any CPU + {6C19204D-7CC8-48D2-A475-49CD98931105}.Release|x86.ActiveCfg = Release|Any CPU + {6C19204D-7CC8-48D2-A475-49CD98931105}.Release|x86.Build.0 = Release|Any CPU {E858F22E-01D7-4BE9-B8DE-0E4AFBA5B33A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E858F22E-01D7-4BE9-B8DE-0E4AFBA5B33A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E858F22E-01D7-4BE9-B8DE-0E4AFBA5B33A}.Debug|x64.ActiveCfg = Debug|Any CPU + {E858F22E-01D7-4BE9-B8DE-0E4AFBA5B33A}.Debug|x64.Build.0 = Debug|Any CPU + {E858F22E-01D7-4BE9-B8DE-0E4AFBA5B33A}.Debug|x86.ActiveCfg = Debug|Any CPU + {E858F22E-01D7-4BE9-B8DE-0E4AFBA5B33A}.Debug|x86.Build.0 = Debug|Any CPU {E858F22E-01D7-4BE9-B8DE-0E4AFBA5B33A}.Release|Any CPU.ActiveCfg = Release|Any CPU {E858F22E-01D7-4BE9-B8DE-0E4AFBA5B33A}.Release|Any CPU.Build.0 = Release|Any CPU + {E858F22E-01D7-4BE9-B8DE-0E4AFBA5B33A}.Release|x64.ActiveCfg = Release|Any CPU + {E858F22E-01D7-4BE9-B8DE-0E4AFBA5B33A}.Release|x64.Build.0 = Release|Any CPU + {E858F22E-01D7-4BE9-B8DE-0E4AFBA5B33A}.Release|x86.ActiveCfg = Release|Any CPU + {E858F22E-01D7-4BE9-B8DE-0E4AFBA5B33A}.Release|x86.Build.0 = Release|Any CPU {8068B622-46A9-4ABA-BDA3-6AB259D1682C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8068B622-46A9-4ABA-BDA3-6AB259D1682C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8068B622-46A9-4ABA-BDA3-6AB259D1682C}.Debug|x64.ActiveCfg = Debug|Any CPU + {8068B622-46A9-4ABA-BDA3-6AB259D1682C}.Debug|x64.Build.0 = Debug|Any CPU + {8068B622-46A9-4ABA-BDA3-6AB259D1682C}.Debug|x86.ActiveCfg = Debug|Any CPU + {8068B622-46A9-4ABA-BDA3-6AB259D1682C}.Debug|x86.Build.0 = Debug|Any CPU {8068B622-46A9-4ABA-BDA3-6AB259D1682C}.Release|Any CPU.ActiveCfg = Release|Any CPU {8068B622-46A9-4ABA-BDA3-6AB259D1682C}.Release|Any CPU.Build.0 = Release|Any CPU + {8068B622-46A9-4ABA-BDA3-6AB259D1682C}.Release|x64.ActiveCfg = Release|Any CPU + {8068B622-46A9-4ABA-BDA3-6AB259D1682C}.Release|x64.Build.0 = Release|Any CPU + {8068B622-46A9-4ABA-BDA3-6AB259D1682C}.Release|x86.ActiveCfg = Release|Any CPU + {8068B622-46A9-4ABA-BDA3-6AB259D1682C}.Release|x86.Build.0 = Release|Any CPU {6DA63E99-692F-461C-983A-270E5CBE8D45}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6DA63E99-692F-461C-983A-270E5CBE8D45}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6DA63E99-692F-461C-983A-270E5CBE8D45}.Debug|x64.ActiveCfg = Debug|Any CPU + {6DA63E99-692F-461C-983A-270E5CBE8D45}.Debug|x64.Build.0 = Debug|Any CPU + {6DA63E99-692F-461C-983A-270E5CBE8D45}.Debug|x86.ActiveCfg = Debug|Any CPU + {6DA63E99-692F-461C-983A-270E5CBE8D45}.Debug|x86.Build.0 = Debug|Any CPU {6DA63E99-692F-461C-983A-270E5CBE8D45}.Release|Any CPU.ActiveCfg = Release|Any CPU {6DA63E99-692F-461C-983A-270E5CBE8D45}.Release|Any CPU.Build.0 = Release|Any CPU + {6DA63E99-692F-461C-983A-270E5CBE8D45}.Release|x64.ActiveCfg = Release|Any CPU + {6DA63E99-692F-461C-983A-270E5CBE8D45}.Release|x64.Build.0 = Release|Any CPU + {6DA63E99-692F-461C-983A-270E5CBE8D45}.Release|x86.ActiveCfg = Release|Any CPU + {6DA63E99-692F-461C-983A-270E5CBE8D45}.Release|x86.Build.0 = Release|Any CPU {6BD7828E-55B4-4213-8DF9-7BFBA891B8EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6BD7828E-55B4-4213-8DF9-7BFBA891B8EE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6BD7828E-55B4-4213-8DF9-7BFBA891B8EE}.Debug|x64.ActiveCfg = Debug|Any CPU + {6BD7828E-55B4-4213-8DF9-7BFBA891B8EE}.Debug|x64.Build.0 = Debug|Any CPU + {6BD7828E-55B4-4213-8DF9-7BFBA891B8EE}.Debug|x86.ActiveCfg = Debug|Any CPU + {6BD7828E-55B4-4213-8DF9-7BFBA891B8EE}.Debug|x86.Build.0 = Debug|Any CPU {6BD7828E-55B4-4213-8DF9-7BFBA891B8EE}.Release|Any CPU.ActiveCfg = Release|Any CPU {6BD7828E-55B4-4213-8DF9-7BFBA891B8EE}.Release|Any CPU.Build.0 = Release|Any CPU + {6BD7828E-55B4-4213-8DF9-7BFBA891B8EE}.Release|x64.ActiveCfg = Release|Any CPU + {6BD7828E-55B4-4213-8DF9-7BFBA891B8EE}.Release|x64.Build.0 = Release|Any CPU + {6BD7828E-55B4-4213-8DF9-7BFBA891B8EE}.Release|x86.ActiveCfg = Release|Any CPU + {6BD7828E-55B4-4213-8DF9-7BFBA891B8EE}.Release|x86.Build.0 = Release|Any CPU {2D14E222-4E44-40AB-82EF-1E8ABCED0476}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2D14E222-4E44-40AB-82EF-1E8ABCED0476}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2D14E222-4E44-40AB-82EF-1E8ABCED0476}.Debug|x64.ActiveCfg = Debug|Any CPU + {2D14E222-4E44-40AB-82EF-1E8ABCED0476}.Debug|x64.Build.0 = Debug|Any CPU + {2D14E222-4E44-40AB-82EF-1E8ABCED0476}.Debug|x86.ActiveCfg = Debug|Any CPU + {2D14E222-4E44-40AB-82EF-1E8ABCED0476}.Debug|x86.Build.0 = Debug|Any CPU {2D14E222-4E44-40AB-82EF-1E8ABCED0476}.Release|Any CPU.ActiveCfg = Release|Any CPU {2D14E222-4E44-40AB-82EF-1E8ABCED0476}.Release|Any CPU.Build.0 = Release|Any CPU + {2D14E222-4E44-40AB-82EF-1E8ABCED0476}.Release|x64.ActiveCfg = Release|Any CPU + {2D14E222-4E44-40AB-82EF-1E8ABCED0476}.Release|x64.Build.0 = Release|Any CPU + {2D14E222-4E44-40AB-82EF-1E8ABCED0476}.Release|x86.ActiveCfg = Release|Any CPU + {2D14E222-4E44-40AB-82EF-1E8ABCED0476}.Release|x86.Build.0 = Release|Any CPU {861EA36D-970E-4CFE-9E72-D3D12F0BBB60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {861EA36D-970E-4CFE-9E72-D3D12F0BBB60}.Debug|Any CPU.Build.0 = Debug|Any CPU + {861EA36D-970E-4CFE-9E72-D3D12F0BBB60}.Debug|x64.ActiveCfg = Debug|Any CPU + {861EA36D-970E-4CFE-9E72-D3D12F0BBB60}.Debug|x64.Build.0 = Debug|Any CPU + {861EA36D-970E-4CFE-9E72-D3D12F0BBB60}.Debug|x86.ActiveCfg = Debug|Any CPU + {861EA36D-970E-4CFE-9E72-D3D12F0BBB60}.Debug|x86.Build.0 = Debug|Any CPU {861EA36D-970E-4CFE-9E72-D3D12F0BBB60}.Release|Any CPU.ActiveCfg = Release|Any CPU {861EA36D-970E-4CFE-9E72-D3D12F0BBB60}.Release|Any CPU.Build.0 = Release|Any CPU + {861EA36D-970E-4CFE-9E72-D3D12F0BBB60}.Release|x64.ActiveCfg = Release|Any CPU + {861EA36D-970E-4CFE-9E72-D3D12F0BBB60}.Release|x64.Build.0 = Release|Any CPU + {861EA36D-970E-4CFE-9E72-D3D12F0BBB60}.Release|x86.ActiveCfg = Release|Any CPU + {861EA36D-970E-4CFE-9E72-D3D12F0BBB60}.Release|x86.Build.0 = Release|Any CPU {80E49C71-1073-4208-B48F-E0F399946B3B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {80E49C71-1073-4208-B48F-E0F399946B3B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {80E49C71-1073-4208-B48F-E0F399946B3B}.Debug|x64.ActiveCfg = Debug|Any CPU + {80E49C71-1073-4208-B48F-E0F399946B3B}.Debug|x64.Build.0 = Debug|Any CPU + {80E49C71-1073-4208-B48F-E0F399946B3B}.Debug|x86.ActiveCfg = Debug|Any CPU + {80E49C71-1073-4208-B48F-E0F399946B3B}.Debug|x86.Build.0 = Debug|Any CPU {80E49C71-1073-4208-B48F-E0F399946B3B}.Release|Any CPU.ActiveCfg = Release|Any CPU {80E49C71-1073-4208-B48F-E0F399946B3B}.Release|Any CPU.Build.0 = Release|Any CPU + {80E49C71-1073-4208-B48F-E0F399946B3B}.Release|x64.ActiveCfg = Release|Any CPU + {80E49C71-1073-4208-B48F-E0F399946B3B}.Release|x64.Build.0 = Release|Any CPU + {80E49C71-1073-4208-B48F-E0F399946B3B}.Release|x86.ActiveCfg = Release|Any CPU + {80E49C71-1073-4208-B48F-E0F399946B3B}.Release|x86.Build.0 = Release|Any CPU {85A15A15-D528-4542-A546-63BE0EAED986}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {85A15A15-D528-4542-A546-63BE0EAED986}.Debug|Any CPU.Build.0 = Debug|Any CPU + {85A15A15-D528-4542-A546-63BE0EAED986}.Debug|x64.ActiveCfg = Debug|Any CPU + {85A15A15-D528-4542-A546-63BE0EAED986}.Debug|x64.Build.0 = Debug|Any CPU + {85A15A15-D528-4542-A546-63BE0EAED986}.Debug|x86.ActiveCfg = Debug|Any CPU + {85A15A15-D528-4542-A546-63BE0EAED986}.Debug|x86.Build.0 = Debug|Any CPU {85A15A15-D528-4542-A546-63BE0EAED986}.Release|Any CPU.ActiveCfg = Release|Any CPU {85A15A15-D528-4542-A546-63BE0EAED986}.Release|Any CPU.Build.0 = Release|Any CPU + {85A15A15-D528-4542-A546-63BE0EAED986}.Release|x64.ActiveCfg = Release|Any CPU + {85A15A15-D528-4542-A546-63BE0EAED986}.Release|x64.Build.0 = Release|Any CPU + {85A15A15-D528-4542-A546-63BE0EAED986}.Release|x86.ActiveCfg = Release|Any CPU + {85A15A15-D528-4542-A546-63BE0EAED986}.Release|x86.Build.0 = Release|Any CPU + {7855D034-57F4-4CF0-8EB4-5677EF80EC57}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7855D034-57F4-4CF0-8EB4-5677EF80EC57}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7855D034-57F4-4CF0-8EB4-5677EF80EC57}.Debug|x64.ActiveCfg = Debug|Any CPU + {7855D034-57F4-4CF0-8EB4-5677EF80EC57}.Debug|x64.Build.0 = Debug|Any CPU + {7855D034-57F4-4CF0-8EB4-5677EF80EC57}.Debug|x86.ActiveCfg = Debug|Any CPU + {7855D034-57F4-4CF0-8EB4-5677EF80EC57}.Debug|x86.Build.0 = Debug|Any CPU + {7855D034-57F4-4CF0-8EB4-5677EF80EC57}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7855D034-57F4-4CF0-8EB4-5677EF80EC57}.Release|Any CPU.Build.0 = Release|Any CPU + {7855D034-57F4-4CF0-8EB4-5677EF80EC57}.Release|x64.ActiveCfg = Release|Any CPU + {7855D034-57F4-4CF0-8EB4-5677EF80EC57}.Release|x64.Build.0 = Release|Any CPU + {7855D034-57F4-4CF0-8EB4-5677EF80EC57}.Release|x86.ActiveCfg = Release|Any CPU + {7855D034-57F4-4CF0-8EB4-5677EF80EC57}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -186,6 +348,7 @@ Global {85A15A15-D528-4542-A546-63BE0EAED986} = {336D72A1-8E5E-49DE-83D9-DF6BE458BA24} {AF971B90-A335-49AF-8AB6-F387CAED12E4} = {9B9A162C-C5B8-495C-A6D0-8C3135E283B9} {2D19706F-4199-46BD-B047-C4ED3AEDD90A} = {9B9A162C-C5B8-495C-A6D0-8C3135E283B9} + {7855D034-57F4-4CF0-8EB4-5677EF80EC57} = {AE206253-B766-4B6A-8C08-9E70605A2B27} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {F673635D-58CE-48A5-9AE4-31F4484BED9E} diff --git a/benchmarks/.editorconfig b/benchmarks/.editorconfig new file mode 100644 index 00000000..0352d900 --- /dev/null +++ b/benchmarks/.editorconfig @@ -0,0 +1,16 @@ +# Analyzer rules relaxed for benchmark projects + +[*.cs] + +# AsyncFixer01: Remove async/await — benchmark components use async-iterator +# conventions and intentionally minimal awaits +dotnet_diagnostic.AsyncFixer01.severity = none + +# MA0004: Use ConfigureAwait(false) — not needed in the benchmark harness +dotnet_diagnostic.MA0004.severity = none + +# S1215: Remove use of GC.GetTotalMemory — required for memory benchmarks +dotnet_diagnostic.S1215.severity = none + +# VSTHRD200: Use Async suffix — benchmark methods follow BenchmarkDotNet naming conventions +dotnet_diagnostic.VSTHRD200.severity = none diff --git a/benchmarks/Wolfgang.Etl.Abstractions.Benchmarks/BenchmarkComponents.cs b/benchmarks/Wolfgang.Etl.Abstractions.Benchmarks/BenchmarkComponents.cs new file mode 100644 index 00000000..208fa400 --- /dev/null +++ b/benchmarks/Wolfgang.Etl.Abstractions.Benchmarks/BenchmarkComponents.cs @@ -0,0 +1,161 @@ +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Wolfgang.Etl.Abstractions; + +namespace Wolfgang.Etl.Abstractions.Benchmarks; + +// ---------------------------------------------------------------------- +// Base-class-derived components — exercise the ExtractorBase / TransformerBase +// / LoaderBase machinery (Interlocked counters, progress plumbing, the async +// iterator wrapper) over a purely in-memory sequence so the benchmark measures +// the abstraction overhead, not any I/O. +// ---------------------------------------------------------------------- + +/// An extractor that yields 0..count-1 from memory. +internal sealed class SequenceExtractor : ExtractorBase +{ + private readonly int _count; + + + + public SequenceExtractor(int count) + { + _count = count; + } + + + + protected override async IAsyncEnumerable ExtractWorkerAsync + ( + [EnumeratorCancellation] CancellationToken token + ) + { + for (var i = 0; i < _count; i++) + { + IncrementCurrentItemCount(); + yield return i; + } + + await Task.CompletedTask; + } + + + + protected override Report CreateProgressReport() + { + return new Report(CurrentItemCount); + } +} + + + +/// A pass-through transformer that yields each source item unchanged. +internal sealed class PassThroughTransformer : TransformerBase +{ + protected override async IAsyncEnumerable TransformWorkerAsync + ( + IAsyncEnumerable items, + [EnumeratorCancellation] CancellationToken token + ) + { + await foreach (var item in items.WithCancellation(token)) + { + IncrementCurrentItemCount(); + yield return item; + } + } + + + + protected override Report CreateProgressReport() + { + return new Report(CurrentItemCount); + } +} + + + +/// A loader that drains the sequence, counting items. +internal sealed class CountingLoader : LoaderBase +{ + protected override async Task LoadWorkerAsync + ( + IAsyncEnumerable items, + CancellationToken token + ) + { + await foreach (var _ in items.WithCancellation(token)) + { + IncrementCurrentItemCount(); + } + } + + + + protected override Report CreateProgressReport() + { + return new Report(CurrentItemCount); + } +} + + + +// ---------------------------------------------------------------------- +// Interface-only components — implement just the simplest (no-progress, +// no-cancellation) ETL interfaces so the fluent Pipeline composition resolves +// to its leanest path, isolating the cost of the Pipeline plumbing itself. +// ---------------------------------------------------------------------- + +/// A minimal over an in-memory range. +internal sealed class RangeExtractor : IExtractAsync +{ + private readonly int _count; + + + + public RangeExtractor(int count) + { + _count = count; + } + + + + public async IAsyncEnumerable ExtractAsync() + { + for (var i = 0; i < _count; i++) + { + yield return i; + } + + await Task.CompletedTask; + } +} + + + +/// A minimal pass-through . +internal sealed class IdentityTransformer : ITransformAsync +{ + public async IAsyncEnumerable TransformAsync(IAsyncEnumerable items) + { + await foreach (var item in items) + { + yield return item; + } + } +} + + + +/// A minimal that drains the sequence. +internal sealed class SinkLoader : ILoadAsync +{ + public async Task LoadAsync(IAsyncEnumerable items) + { + await foreach (var _ in items) + { + } + } +} diff --git a/benchmarks/Wolfgang.Etl.Abstractions.Benchmarks/ExtractorBenchmarks.cs b/benchmarks/Wolfgang.Etl.Abstractions.Benchmarks/ExtractorBenchmarks.cs new file mode 100644 index 00000000..8e915d9c --- /dev/null +++ b/benchmarks/Wolfgang.Etl.Abstractions.Benchmarks/ExtractorBenchmarks.cs @@ -0,0 +1,63 @@ +using System; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; +using Wolfgang.Etl.Abstractions; + +namespace Wolfgang.Etl.Abstractions.Benchmarks; + +/// +/// Measures the per-item overhead the +/// machinery adds on top of a bare in-memory sequence — the async-iterator +/// wrapper, the Interlocked-based item counter, and the progress path +/// (timer creation + a final progress report). +/// +[MemoryDiagnoser] +public class ExtractorBenchmarks +{ + [Params(1_000, 100_000)] + public int RecordCount { get; set; } + + + + [Benchmark(Baseline = true)] + public async Task Extract_NoProgress() + { + var extractor = new SequenceExtractor(RecordCount); + + var count = 0; + await foreach (var _ in extractor.ExtractAsync()) + { + count++; + } + + return count; + } + + + + [Benchmark] + public async Task Extract_WithProgress() + { + var extractor = new SequenceExtractor(RecordCount); + IProgress progress = new Sink(); + + var count = 0; + await foreach (var _ in extractor.ExtractAsync(progress)) + { + count++; + } + + return count; + } + + + + // Synchronous no-op progress sink — avoids the SynchronizationContext + // posting that System.Progress would add as benchmark noise. + private sealed class Sink : IProgress + { + public void Report(Report value) + { + } + } +} diff --git a/benchmarks/Wolfgang.Etl.Abstractions.Benchmarks/PipelineBenchmarks.cs b/benchmarks/Wolfgang.Etl.Abstractions.Benchmarks/PipelineBenchmarks.cs new file mode 100644 index 00000000..ee8c914a --- /dev/null +++ b/benchmarks/Wolfgang.Etl.Abstractions.Benchmarks/PipelineBenchmarks.cs @@ -0,0 +1,59 @@ +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; +using Wolfgang.Etl.Abstractions; + +namespace Wolfgang.Etl.Abstractions.Benchmarks; + +/// +/// Measures the cost of composing and running a full Extract → Transform → Load +/// pipeline. Compares the fluent builder against hand-wired +/// composition over the same in-memory stages, +/// so the delta is the builder's plumbing overhead (it should be negligible — the +/// builder is documented as zero-extra-allocation sugar over the same composition). +/// +[MemoryDiagnoser] +public class PipelineBenchmarks +{ + [Params(1_000, 100_000)] + public int RecordCount { get; set; } + + + + [Benchmark(Baseline = true)] + public async Task FluentPipeline() + { + await Pipeline + .Extract(new RangeExtractor(RecordCount)) + .Transform(new IdentityTransformer()) + .Load(new SinkLoader()) + .RunAsync(); + } + + + + [Benchmark] + public async Task ManualComposition() + { + var extractor = new RangeExtractor(RecordCount); + var transformer = new IdentityTransformer(); + var loader = new SinkLoader(); + + await loader.LoadAsync(transformer.TransformAsync(extractor.ExtractAsync())); + } + + + + // Same Extract -> Transform -> Load shape, but wired with the base-class + // components (ExtractorBase / TransformerBase / LoaderBase) so the result + // captures the full abstraction overhead — Interlocked item counting and + // the async-iterator wrappers on every stage. + [Benchmark] + public async Task BaseClassComposition() + { + var extractor = new SequenceExtractor(RecordCount); + var transformer = new PassThroughTransformer(); + var loader = new CountingLoader(); + + await loader.LoadAsync(transformer.TransformAsync(extractor.ExtractAsync())); + } +} diff --git a/benchmarks/Wolfgang.Etl.Abstractions.Benchmarks/Program.cs b/benchmarks/Wolfgang.Etl.Abstractions.Benchmarks/Program.cs new file mode 100644 index 00000000..87c286f8 --- /dev/null +++ b/benchmarks/Wolfgang.Etl.Abstractions.Benchmarks/Program.cs @@ -0,0 +1,5 @@ +using BenchmarkDotNet.Running; + +BenchmarkSwitcher + .FromAssembly(typeof(Program).Assembly) + .Run(args); diff --git a/benchmarks/Wolfgang.Etl.Abstractions.Benchmarks/Wolfgang.Etl.Abstractions.Benchmarks.csproj b/benchmarks/Wolfgang.Etl.Abstractions.Benchmarks/Wolfgang.Etl.Abstractions.Benchmarks.csproj new file mode 100644 index 00000000..a1ef3e46 --- /dev/null +++ b/benchmarks/Wolfgang.Etl.Abstractions.Benchmarks/Wolfgang.Etl.Abstractions.Benchmarks.csproj @@ -0,0 +1,20 @@ + + + + Exe + net10.0 + latest + disable + + + + + + + + + + + + +