From 553dbb6539e401120da17ad3f677a224da199b92 Mon Sep 17 00:00:00 2001 From: Sergey Penkovsky Date: Wed, 6 Aug 2025 14:44:12 +0300 Subject: [PATCH] refactor(benchmarks): introduce DIAdapter abstraction, migrate all scenarios to use DIAdapter --- benchmark_cherrypick/bin/main.dart | 99 ++++++++++++++++--- .../lib/async_chain_benchmark.dart | 14 +-- .../lib/cherrypick_benchmark.dart | 20 ++-- .../lib/complex_bindings_benchmark.dart | 40 ++++---- benchmark_cherrypick/lib/di_adapter.dart | 78 +++++++++++++++ .../lib/scope_override_benchmark.dart | 25 ++--- 6 files changed, 213 insertions(+), 63 deletions(-) create mode 100644 benchmark_cherrypick/lib/di_adapter.dart diff --git a/benchmark_cherrypick/bin/main.dart b/benchmark_cherrypick/bin/main.dart index 026b0c1..12c5185 100644 --- a/benchmark_cherrypick/bin/main.dart +++ b/benchmark_cherrypick/bin/main.dart @@ -1,14 +1,19 @@ import 'package:benchmark_cherrypick/cherrypick_benchmark.dart'; import 'package:benchmark_cherrypick/complex_bindings_benchmark.dart'; import 'package:benchmark_cherrypick/async_chain_benchmark.dart'; +import 'package:benchmark_cherrypick/di_adapter.dart'; import 'package:benchmark_cherrypick/scope_override_benchmark.dart'; import 'package:args/args.dart'; +import 'dart:io'; +import 'dart:math'; Future main(List args) async { final parser = ArgParser() ..addOption('benchmark', abbr: 'b', help: 'Benchmark name (register, chain_singleton, chain_factory, named, override, async_chain, all)', defaultsTo: 'all') ..addOption('chainCount', abbr: 'c', help: 'Comma-separated chainCounts (используется в chain_singleton/factory)', defaultsTo: '100') ..addOption('nestingDepth', abbr: 'd', help: 'Comma-separated depths (используется в chain_singleton/factory)', defaultsTo: '100') + ..addOption('repeat', abbr: 'r', help: 'Repeats for each run (statistical run, >=2)', defaultsTo: '5') + ..addOption('warmup', abbr: 'w', help: 'Warmup runs before timing', defaultsTo: '2') ..addOption('format', abbr: 'f', help: 'Output format (pretty, csv, json)', defaultsTo: 'pretty') ..addFlag('help', abbr: 'h', negatable: false, help: 'Show help'); @@ -17,66 +22,116 @@ Future main(List args) async { if (result['help'] == true) { print('Dart DI benchmarks'); print(parser.usage); + print('\nExamples:\n' + ' dart run bin/main.dart --benchmark=chain_singleton --chainCount=10,100 --nestingDepth=5,10 --format=csv\n' + ' dart run bin/main.dart --benchmark=named\n' + 'Extra: --repeat=7 --warmup=3\n'); return; } + final di = CherrypickDIAdapter(); + final benchmark = result['benchmark'] as String; final format = result['format'] as String; final chainCounts = _parseIntList(result['chainCount'] as String); final nestDepths = _parseIntList(result['nestingDepth'] as String); + final repeats = int.tryParse(result['repeat'] as String? ?? "") ?? 5; + final warmups = int.tryParse(result['warmup'] as String? ?? "") ?? 2; final results = >[]; - void addResult(String name, int? chainCount, int? nestingDepth, num elapsed) { + void addResult( + String name, + int? chainCount, + int? nestingDepth, + List timings, + int? memoryDiffKb, + int? deltaPeakKb, + int? peakRssKb, + ) { + timings.sort(); + + var mean = timings.reduce((a, b) => a + b) / timings.length; + var median = timings[timings.length ~/ 2]; + var minVal = timings.first; + var maxVal = timings.last; + var stddev = sqrt(timings.map((x) => pow(x - mean, 2)).reduce((a, b) => a + b) / timings.length); results.add({ 'benchmark': name, 'chainCount': chainCount, 'nestingDepth': nestingDepth, - 'elapsed_us': elapsed.round() + 'mean_us': mean.round(), + 'median_us': median.round(), + 'stddev_us': stddev.round(), + 'min_us': minVal.round(), + 'max_us': maxVal.round(), + 'trials': timings.length, + 'timings_us': timings.map((t) => t.round()).toList(), + 'memory_diff_kb': memoryDiffKb, + 'delta_peak_kb': deltaPeakKb, + 'peak_rss_kb': peakRssKb, }); } - Future runAndCollect(String name, Future Function() fn, {int? chainCount, int? nestingDepth}) async { - final elapsed = await fn(); - addResult(name, chainCount, nestingDepth, elapsed); + Future runAndCollect( + String name, + Future Function() fn, { + int? chainCount, + int? nestingDepth, + }) async { + for (int i = 0; i < warmups; i++) { + await fn(); + } + final timings = []; + final rssValues = []; + final memBefore = ProcessInfo.currentRss; + for (int i = 0; i < repeats; i++) { + timings.add(await fn()); + rssValues.add(ProcessInfo.currentRss); + } + final memAfter = ProcessInfo.currentRss; + final memDiffKB = ((memAfter - memBefore) / 1024).round(); + final peakRss = [...rssValues, memBefore].reduce(max); + final deltaPeakKb = ((peakRss - memBefore) / 1024).round(); + addResult(name, chainCount, nestingDepth, timings, memDiffKB, deltaPeakKb, (peakRss/1024).round()); } if (benchmark == 'all' || benchmark == 'register') { await runAndCollect('RegisterAndResolve', () async { - return _captureReport(RegisterAndResolveBenchmark().report); + return _captureReport(RegisterAndResolveBenchmark(di).report); }); } if (benchmark == 'all' || benchmark == 'chain_singleton') { - for (final c in chainCounts) { + for (final c in chainCounts) { for (final d in nestDepths) { await runAndCollect('ChainSingleton', () async { - return _captureReport(() => ChainSingletonBenchmark(chainCount: c, nestingDepth: d).report()); + return _captureReport(() => ChainSingletonBenchmark(di,chainCount: c, nestingDepth: d).report()); }, chainCount: c, nestingDepth: d); } } } if (benchmark == 'all' || benchmark == 'chain_factory') { - for (final c in chainCounts) { + for (final c in chainCounts) { for (final d in nestDepths) { await runAndCollect('ChainFactory', () async { - return _captureReport(() => ChainFactoryBenchmark(chainCount: c, nestingDepth: d).report()); + return _captureReport(() => ChainFactoryBenchmark(di, chainCount: c, nestingDepth: d).report()); }, chainCount: c, nestingDepth: d); } } } if (benchmark == 'all' || benchmark == 'named') { await runAndCollect('NamedResolve', () async { - return _captureReport(NamedResolveBenchmark().report); + return _captureReport(NamedResolveBenchmark(di).report); }); } if (benchmark == 'all' || benchmark == 'override') { await runAndCollect('ScopeOverride', () async { - return _captureReport(ScopeOverrideBenchmark().report); + return _captureReport(ScopeOverrideBenchmark(di).report); }); } if (benchmark == 'all' || benchmark == 'async_chain') { await runAndCollect('AsyncChain', () async { - return _captureReportAsync(AsyncChainBenchmark().report); + return _captureReportAsync(AsyncChainBenchmark(di).report); }); } @@ -106,16 +161,28 @@ Future _captureReportAsync(Future Function() fn) async { } String _toPretty(List> rows) { - final keys = ['benchmark','chainCount','nestingDepth','elapsed_us']; + final keys = [ + 'benchmark','chainCount','nestingDepth','mean_us','median_us','stddev_us', + 'min_us','max_us','trials','memory_diff_kb','delta_peak_kb','peak_rss_kb' + ]; final header = keys.join('\t'); final lines = rows.map((r) => keys.map((k) => (r[k] ?? '').toString()).join('\t')).toList(); return ([header] + lines).join('\n'); } String _toCsv(List> rows) { - final keys = ['benchmark','chainCount','nestingDepth','elapsed_us']; + final keys = [ + 'benchmark','chainCount','nestingDepth','mean_us','median_us','stddev_us', + 'min_us','max_us','trials','timings_us','memory_diff_kb','delta_peak_kb','peak_rss_kb' + ]; final header = keys.join(','); - final lines = rows.map((r) => keys.map((k) => (r[k] ?? '').toString()).join(',')).toList(); + final lines = rows.map((r) => + keys.map((k) { + final v = r[k]; + if (v is List) return '"${v.join(';')}"'; + return (v ?? '').toString(); + }).join(',') + ).toList(); return ([header] + lines).join('\n'); } diff --git a/benchmark_cherrypick/lib/async_chain_benchmark.dart b/benchmark_cherrypick/lib/async_chain_benchmark.dart index 7b27184..636f8ba 100644 --- a/benchmark_cherrypick/lib/async_chain_benchmark.dart +++ b/benchmark_cherrypick/lib/async_chain_benchmark.dart @@ -1,7 +1,8 @@ // ignore: depend_on_referenced_packages +import 'package:benchmark_cherrypick/di_adapter.dart'; +// ignore: depend_on_referenced_packages import 'package:benchmark_harness/benchmark_harness.dart'; import 'package:cherrypick/cherrypick.dart'; -import 'benchmark_utils.dart'; class AsyncA {} @@ -30,21 +31,22 @@ class AsyncChainModule extends Module { } } -class AsyncChainBenchmark extends AsyncBenchmarkBase with BenchmarkWithScope { - AsyncChainBenchmark() : super('AsyncChain (A->B->C, async)'); +class AsyncChainBenchmark extends AsyncBenchmarkBase { + final DIAdapter di; + AsyncChainBenchmark(this.di) : super('AsyncChain (A->B->C, async)'); @override Future setup() async { - setupScope([AsyncChainModule()]); + di.setupModules([AsyncChainModule()]); } @override Future teardown() async { - teardownScope(); + di.teardown(); } @override Future run() async { - await scope.resolveAsync(); + await di.resolveAsync(); } } diff --git a/benchmark_cherrypick/lib/cherrypick_benchmark.dart b/benchmark_cherrypick/lib/cherrypick_benchmark.dart index 87c53f0..caeb56d 100644 --- a/benchmark_cherrypick/lib/cherrypick_benchmark.dart +++ b/benchmark_cherrypick/lib/cherrypick_benchmark.dart @@ -1,4 +1,6 @@ // ignore: depend_on_referenced_packages +import 'package:benchmark_cherrypick/di_adapter.dart'; +// ignore: depend_on_referenced_packages import 'package:benchmark_harness/benchmark_harness.dart'; import 'package:cherrypick/cherrypick.dart'; @@ -13,26 +15,20 @@ class AppModule extends Module { class FooService {} class RegisterAndResolveBenchmark extends BenchmarkBase { - RegisterAndResolveBenchmark() : super('RegisterAndResolve'); - late final Scope scope; + final DIAdapter di; + + RegisterAndResolveBenchmark(this.di) : super('RegisterAndResolve'); @override void setup() { - CherryPick.disableGlobalCycleDetection(); - CherryPick.disableGlobalCrossScopeCycleDetection(); - scope = CherryPick.openRootScope(); - scope.installModules([AppModule()]); + di.setupModules([AppModule()]); } @override void run() { - scope.resolve(); + di.resolve(); } @override - void teardown() => CherryPick.closeRootScope(); -} - -void main() { - RegisterAndResolveBenchmark().report(); + void teardown() => di.teardown(); } diff --git a/benchmark_cherrypick/lib/complex_bindings_benchmark.dart b/benchmark_cherrypick/lib/complex_bindings_benchmark.dart index b2754e8..51a4f3f 100644 --- a/benchmark_cherrypick/lib/complex_bindings_benchmark.dart +++ b/benchmark_cherrypick/lib/complex_bindings_benchmark.dart @@ -1,7 +1,8 @@ // ignore: depend_on_referenced_packages +import 'package:benchmark_cherrypick/di_adapter.dart'; +// ignore: depend_on_referenced_packages import 'package:benchmark_harness/benchmark_harness.dart'; import 'package:cherrypick/cherrypick.dart'; -import 'benchmark_utils.dart'; // === DI graph: A -> B -> C (singleton) === abstract class Service { @@ -59,11 +60,13 @@ class ChainSingletonModule extends Module { } } -class ChainSingletonBenchmark extends BenchmarkBase with BenchmarkWithScope { +class ChainSingletonBenchmark extends BenchmarkBase { + final DIAdapter di; final int chainCount; final int nestingDepth; - ChainSingletonBenchmark({ + ChainSingletonBenchmark( + this.di, { this.chainCount = 1, this.nestingDepth = 3, }) : super( @@ -73,7 +76,7 @@ class ChainSingletonBenchmark extends BenchmarkBase with BenchmarkWithScope { @override void setup() { - setupScope([ + di.setupModules([ ChainSingletonModule( chainCount: chainCount, nestingDepth: nestingDepth, @@ -82,12 +85,12 @@ class ChainSingletonBenchmark extends BenchmarkBase with BenchmarkWithScope { } @override - void teardown() => teardownScope(); + void teardown() => di.teardown(); @override void run() { final serviceName = '${chainCount.toString()}_${nestingDepth.toString()}'; - scope.resolve(named: serviceName); + di.resolve(named: serviceName); } } @@ -129,11 +132,13 @@ class ChainFactoryModule extends Module { } } -class ChainFactoryBenchmark extends BenchmarkBase with BenchmarkWithScope { +class ChainFactoryBenchmark extends BenchmarkBase { + final DIAdapter di; final int chainCount; final int nestingDepth; - ChainFactoryBenchmark({ + ChainFactoryBenchmark( + this.di, { this.chainCount = 1, this.nestingDepth = 3, }) : super( @@ -143,7 +148,7 @@ class ChainFactoryBenchmark extends BenchmarkBase with BenchmarkWithScope { @override void setup() { - setupScope([ + di.setupModules([ ChainFactoryModule( chainCount: chainCount, nestingDepth: nestingDepth, @@ -152,12 +157,12 @@ class ChainFactoryBenchmark extends BenchmarkBase with BenchmarkWithScope { } @override - void teardown() => teardownScope(); + void teardown() => di.teardown(); @override void run() { final serviceName = '${chainCount.toString()}_${nestingDepth.toString()}'; - scope.resolve(named: serviceName); + di.resolve(named: serviceName); } } @@ -174,20 +179,21 @@ class NamedModule extends Module { } } -class NamedResolveBenchmark extends BenchmarkBase with BenchmarkWithScope { - NamedResolveBenchmark() : super('NamedResolve (by name)'); +class NamedResolveBenchmark extends BenchmarkBase { + final DIAdapter di; + + NamedResolveBenchmark(this.di) : super('NamedResolve (by name)'); @override void setup() { - setupScope([NamedModule()]); + di.setupModules([NamedModule()]); } @override - void teardown() => teardownScope(); + void teardown() => di.teardown(); @override void run() { - // Switch name for comparison - scope.resolve(named: 'impl2'); + di.resolve(named: 'impl2'); } } diff --git a/benchmark_cherrypick/lib/di_adapter.dart b/benchmark_cherrypick/lib/di_adapter.dart new file mode 100644 index 0000000..aed5ecc --- /dev/null +++ b/benchmark_cherrypick/lib/di_adapter.dart @@ -0,0 +1,78 @@ +import 'package:cherrypick/cherrypick.dart'; + +abstract class DIAdapter { + void setupModules(List modules); + T resolve({String? named}); + Future resolveAsync({String? named}); + void teardown(); + DIAdapter openSubScope(String name); +} + +class CherrypickDIAdapter implements DIAdapter { + Scope? _scope; + + @override + void setupModules(List modules) { + _scope = CherryPick.openRootScope(); + _scope!.installModules(modules); + } + + @override + T resolve({String? named}) { + return named == null + ? _scope!.resolve() + : _scope!.resolve(named: named); + } + + @override + Future resolveAsync({String? named}) async { + return named == null + ? await _scope!.resolveAsync() + : await _scope!.resolveAsync(named: named); + } + + @override + void teardown() { + CherryPick.closeRootScope(); + _scope = null; + } + + @override + CherrypickDIAdapter openSubScope(String name) { + final sub = _scope!.openSubScope(name); + return _CherrypickSubScopeAdapter(sub); + } +} + +class _CherrypickSubScopeAdapter extends CherrypickDIAdapter { + final Scope _subScope; + _CherrypickSubScopeAdapter(this._subScope); + @override + void setupModules(List modules) { + _subScope.installModules(modules); + } + + @override + T resolve({String? named}) { + return named == null + ? _subScope.resolve() + : _subScope.resolve(named: named); + } + + @override + Future resolveAsync({String? named}) async { + return named == null + ? await _subScope.resolveAsync() + : await _subScope.resolveAsync(named: named); + } + + @override + void teardown() { + // subScope teardown убирать отдельно не требуется + } + + @override + CherrypickDIAdapter openSubScope(String name) { + return _CherrypickSubScopeAdapter(_subScope.openSubScope(name)); + } +} diff --git a/benchmark_cherrypick/lib/scope_override_benchmark.dart b/benchmark_cherrypick/lib/scope_override_benchmark.dart index 868ee69..e976c1d 100644 --- a/benchmark_cherrypick/lib/scope_override_benchmark.dart +++ b/benchmark_cherrypick/lib/scope_override_benchmark.dart @@ -1,7 +1,8 @@ // ignore: depend_on_referenced_packages +import 'package:benchmark_cherrypick/di_adapter.dart'; +// ignore: depend_on_referenced_packages import 'package:benchmark_harness/benchmark_harness.dart'; import 'package:cherrypick/cherrypick.dart'; -import 'benchmark_utils.dart'; class Shared {} @@ -23,26 +24,26 @@ class ChildOverrideModule extends Module { } } -class ScopeOverrideBenchmark extends BenchmarkBase with BenchmarkWithScope { - ScopeOverrideBenchmark() : super('ScopeOverride (child overrides parent)'); - late Scope child; +class ScopeOverrideBenchmark extends BenchmarkBase { + final DIAdapter di; + late DIAdapter childDi; + + ScopeOverrideBenchmark(this.di) : super('ScopeOverride (child overrides parent)'); @override void setup() { - setupScope([ParentModule()]); - child = scope.openSubScope('child'); - child.installModules([ChildOverrideModule()]); + di.setupModules([ParentModule()]); + childDi = di.openSubScope('child'); + childDi.setupModules([ChildOverrideModule()]); } @override - void teardown() { - teardownScope(); - } + void teardown() => di.teardown(); @override void run() { - // Должен возвращать ChildImpl, а не ParentImpl - final resolved = child.resolve(); + // Should return ChildImpl, not ParentImpl + final resolved = childDi.resolve(); assert(resolved is ChildImpl); } }