feat(benchmark_di): add resolve phases, eager/lazy scenarios, and adapter parity

This commit is contained in:
Klim
2026-04-22 20:33:07 +03:00
parent 59234b44c2
commit 4077ed8469
13 changed files with 383 additions and 257 deletions

View File

@@ -25,7 +25,11 @@ class UniversalChainAsyncBenchmark<TContainer> extends AsyncBenchmarkBase {
bindingMode: mode, bindingMode: mode,
scenario: UniversalScenario.asyncChain, scenario: UniversalScenario.asyncChain,
)); ));
}
Future<void> prewarm() async {
await di.waitForAsyncReady(); await di.waitForAsyncReady();
await run();
} }
@override @override

View File

@@ -50,7 +50,14 @@ class UniversalChainBenchmark<TContainer> extends BenchmarkBase {
} }
@override @override
void teardown() => _di.teardown(); void teardown() {
_childDi?.teardown();
_di.teardown();
}
void prewarm() {
run();
}
@override @override
void run() { void run() {
@@ -59,11 +66,7 @@ class UniversalChainBenchmark<TContainer> extends BenchmarkBase {
_di.resolve<UniversalService>(); _di.resolve<UniversalService>();
break; break;
case UniversalScenario.named: case UniversalScenario.named:
if (_di.runtimeType.toString().contains('GetItAdapter')) { _di.resolve<UniversalService>(named: 'impl2');
_di.resolve<UniversalService>(named: 'impl2');
} else {
_di.resolve<UniversalService>(named: 'impl2');
}
break; break;
case UniversalScenario.chain: case UniversalScenario.chain:
final serviceName = '${chainCount}_$nestingDepth'; final serviceName = '${chainCount}_$nestingDepth';

View File

@@ -31,184 +31,205 @@ class BenchmarkCliRunner {
Future<void> run(List<String> args) async { Future<void> run(List<String> args) async {
final config = parseBenchmarkCli(args); final config = parseBenchmarkCli(args);
final results = <Map<String, dynamic>>[]; final results = <Map<String, dynamic>>[];
for (final bench in config.benchesToRun) { // DI implementations that do not support async scenarios
final scenario = toScenario(bench); const asyncUnsupported = {'kiwi', 'yx_scope'};
final mode = toMode(bench); for (final phase in config.phases) {
for (final c in config.chainCounts) { for (final bench in config.benchesToRun) {
for (final d in config.nestDepths) { final scenario = toScenario(bench);
BenchmarkResult benchResult; final mode = toMode(bench);
if (config.di == 'getit') { if (asyncUnsupported.contains(config.di) &&
final di = GetItAdapter(); scenario == UniversalScenario.asyncChain) {
if (scenario == UniversalScenario.asyncChain) { continue; // Skip async benchmarks for DI that does not support them
final benchAsync = UniversalChainAsyncBenchmark<GetIt>( }
di, for (final c in config.chainCounts) {
chainCount: c, for (final d in config.nestDepths) {
nestingDepth: d, BenchmarkResult benchResult;
mode: mode, if (config.di == 'getit') {
); final di = GetItAdapter();
benchResult = await BenchmarkRunner.runAsync( if (scenario == UniversalScenario.asyncChain) {
benchmark: benchAsync, final benchAsync = UniversalChainAsyncBenchmark<GetIt>(
warmups: config.warmups, di,
repeats: config.repeats, chainCount: c,
); nestingDepth: d,
mode: mode,
);
benchResult = await BenchmarkRunner.runAsync(
benchmark: benchAsync,
warmups: config.warmups,
repeats: config.repeats,
phase: phase,
);
} else {
final benchSync = UniversalChainBenchmark<GetIt>(
di,
chainCount: c,
nestingDepth: d,
mode: mode,
scenario: scenario,
);
benchResult = await BenchmarkRunner.runSync(
benchmark: benchSync,
warmups: config.warmups,
repeats: config.repeats,
phase: phase,
);
}
} else if (config.di == 'kiwi') {
final di = KiwiAdapter();
if (scenario == UniversalScenario.asyncChain) {
final benchAsync = UniversalChainAsyncBenchmark<KiwiContainer>(
di,
chainCount: c,
nestingDepth: d,
mode: mode,
);
benchResult = await BenchmarkRunner.runAsync(
benchmark: benchAsync,
warmups: config.warmups,
repeats: config.repeats,
phase: phase,
);
} else {
final benchSync = UniversalChainBenchmark<KiwiContainer>(
di,
chainCount: c,
nestingDepth: d,
mode: mode,
scenario: scenario,
);
benchResult = await BenchmarkRunner.runSync(
benchmark: benchSync,
warmups: config.warmups,
repeats: config.repeats,
phase: phase,
);
}
} else if (config.di == 'riverpod') {
final di = RiverpodAdapter();
if (scenario == UniversalScenario.asyncChain) {
final benchAsync = UniversalChainAsyncBenchmark<
Map<String, rp.ProviderBase<Object?>>>(
di,
chainCount: c,
nestingDepth: d,
mode: mode,
);
benchResult = await BenchmarkRunner.runAsync(
benchmark: benchAsync,
warmups: config.warmups,
repeats: config.repeats,
phase: phase,
);
} else {
final benchSync = UniversalChainBenchmark<
Map<String, rp.ProviderBase<Object?>>>(
di,
chainCount: c,
nestingDepth: d,
mode: mode,
scenario: scenario,
);
benchResult = await BenchmarkRunner.runSync(
benchmark: benchSync,
warmups: config.warmups,
repeats: config.repeats,
phase: phase,
);
}
} else if (config.di == 'yx_scope') {
final di = YxScopeAdapter();
if (scenario == UniversalScenario.asyncChain) {
final benchAsync =
UniversalChainAsyncBenchmark<UniversalYxScopeContainer>(
di,
chainCount: c,
nestingDepth: d,
mode: mode,
);
benchResult = await BenchmarkRunner.runAsync(
benchmark: benchAsync,
warmups: config.warmups,
repeats: config.repeats,
phase: phase,
);
} else {
final benchSync =
UniversalChainBenchmark<UniversalYxScopeContainer>(
di,
chainCount: c,
nestingDepth: d,
mode: mode,
scenario: scenario,
);
benchResult = await BenchmarkRunner.runSync(
benchmark: benchSync,
warmups: config.warmups,
repeats: config.repeats,
phase: phase,
);
}
} else { } else {
final benchSync = UniversalChainBenchmark<GetIt>( final di = CherrypickDIAdapter();
di, if (scenario == UniversalScenario.asyncChain) {
chainCount: c, final benchAsync = UniversalChainAsyncBenchmark<Scope>(
nestingDepth: d, di,
mode: mode, chainCount: c,
scenario: scenario, nestingDepth: d,
); mode: mode,
benchResult = await BenchmarkRunner.runSync( );
benchmark: benchSync, benchResult = await BenchmarkRunner.runAsync(
warmups: config.warmups, benchmark: benchAsync,
repeats: config.repeats, warmups: config.warmups,
); repeats: config.repeats,
} phase: phase,
} else if (config.di == 'kiwi') { );
final di = KiwiAdapter(); } else {
if (scenario == UniversalScenario.asyncChain) { final benchSync = UniversalChainBenchmark<Scope>(
// UnsupportedError будет выброшен адаптером, но если дойдёт — вызывать async benchmark di,
final benchAsync = UniversalChainAsyncBenchmark<KiwiContainer>( chainCount: c,
di, nestingDepth: d,
chainCount: c, mode: mode,
nestingDepth: d, scenario: scenario,
mode: mode, );
); benchResult = await BenchmarkRunner.runSync(
benchResult = await BenchmarkRunner.runAsync( benchmark: benchSync,
benchmark: benchAsync, warmups: config.warmups,
warmups: config.warmups, repeats: config.repeats,
repeats: config.repeats, phase: phase,
); );
} else { }
final benchSync = UniversalChainBenchmark<KiwiContainer>(
di,
chainCount: c,
nestingDepth: d,
mode: mode,
scenario: scenario,
);
benchResult = await BenchmarkRunner.runSync(
benchmark: benchSync,
warmups: config.warmups,
repeats: config.repeats,
);
}
} else if (config.di == 'riverpod') {
final di = RiverpodAdapter();
if (scenario == UniversalScenario.asyncChain) {
final benchAsync = UniversalChainAsyncBenchmark<
Map<String, rp.ProviderBase<Object?>>>(
di,
chainCount: c,
nestingDepth: d,
mode: mode,
);
benchResult = await BenchmarkRunner.runAsync(
benchmark: benchAsync,
warmups: config.warmups,
repeats: config.repeats,
);
} else {
final benchSync = UniversalChainBenchmark<
Map<String, rp.ProviderBase<Object?>>>(
di,
chainCount: c,
nestingDepth: d,
mode: mode,
scenario: scenario,
);
benchResult = await BenchmarkRunner.runSync(
benchmark: benchSync,
warmups: config.warmups,
repeats: config.repeats,
);
}
} else if (config.di == 'yx_scope') {
final di = YxScopeAdapter();
if (scenario == UniversalScenario.asyncChain) {
final benchAsync =
UniversalChainAsyncBenchmark<UniversalYxScopeContainer>(
di,
chainCount: c,
nestingDepth: d,
mode: mode,
);
benchResult = await BenchmarkRunner.runAsync(
benchmark: benchAsync,
warmups: config.warmups,
repeats: config.repeats,
);
} else {
final benchSync =
UniversalChainBenchmark<UniversalYxScopeContainer>(
di,
chainCount: c,
nestingDepth: d,
mode: mode,
scenario: scenario,
);
benchResult = await BenchmarkRunner.runSync(
benchmark: benchSync,
warmups: config.warmups,
repeats: config.repeats,
);
}
} else {
final di = CherrypickDIAdapter();
if (scenario == UniversalScenario.asyncChain) {
final benchAsync = UniversalChainAsyncBenchmark<Scope>(
di,
chainCount: c,
nestingDepth: d,
mode: mode,
);
benchResult = await BenchmarkRunner.runAsync(
benchmark: benchAsync,
warmups: config.warmups,
repeats: config.repeats,
);
} else {
final benchSync = UniversalChainBenchmark<Scope>(
di,
chainCount: c,
nestingDepth: d,
mode: mode,
scenario: scenario,
);
benchResult = await BenchmarkRunner.runSync(
benchmark: benchSync,
warmups: config.warmups,
repeats: config.repeats,
);
} }
final timings = benchResult.timings;
if (timings.isEmpty) continue; // skip failed scenarios
timings.sort();
final count = timings.length;
final mean = timings.reduce((a, b) => a + b) / count;
final median = count.isOdd
? timings[count ~/ 2]
: (timings[count ~/ 2 - 1] + timings[count ~/ 2]) / 2;
final minVal = timings.first;
final maxVal = timings.last;
final stddev = sqrt(timings
.map((x) => pow(x - mean, 2))
.reduce((a, b) => a + b) /
count);
results.add({
'benchmark': 'Universal_$bench',
'phase': phase.name,
'chainCount': c,
'nestingDepth': d,
'mean_us': mean.toStringAsFixed(2),
'median_us': median.toStringAsFixed(2),
'stddev_us': stddev.toStringAsFixed(2),
'min_us': minVal.toStringAsFixed(2),
'max_us': maxVal.toStringAsFixed(2),
'trials': timings.length,
'timings_us': timings.map((t) => t.toStringAsFixed(2)).toList(),
'memory_diff_kb': benchResult.memoryDiffKb,
'delta_peak_kb': benchResult.deltaPeakKb,
'peak_rss_kb': benchResult.peakRssKb,
});
} }
final timings = benchResult.timings;
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 = timings.isEmpty
? 0
: sqrt(
timings.map((x) => pow(x - mean, 2)).reduce((a, b) => a + b) /
timings.length);
results.add({
'benchmark': 'Universal_$bench',
'chainCount': c,
'nestingDepth': d,
'mean_us': mean.toStringAsFixed(2),
'median_us': median.toStringAsFixed(2),
'stddev_us': stddev.toStringAsFixed(2),
'min_us': minVal.toStringAsFixed(2),
'max_us': maxVal.toStringAsFixed(2),
'trials': timings.length,
'timings_us': timings.map((t) => t.toStringAsFixed(2)).toList(),
'memory_diff_kb': benchResult.memoryDiffKb,
'delta_peak_kb': benchResult.deltaPeakKb,
'peak_rss_kb': benchResult.peakRssKb,
});
} }
} }
} }

View File

@@ -6,12 +6,18 @@ import 'package:benchmark_di/scenarios/universal_scenario.dart';
/// Enum describing all supported Universal DI benchmark types. /// Enum describing all supported Universal DI benchmark types.
enum UniversalBenchmark { enum UniversalBenchmark {
/// Simple singleton registration benchmark /// Simple singleton registration benchmark (eager, where supported)
registerSingleton, registerSingleton,
/// Chain of singleton dependencies /// Simple lazy singleton registration benchmark
registerLazySingleton,
/// Chain of eager singleton dependencies
chainSingleton, chainSingleton,
/// Chain of lazy singleton dependencies
chainLazySingleton,
/// Chain using factories /// Chain using factories
chainFactory, chainFactory,
@@ -25,12 +31,19 @@ enum UniversalBenchmark {
override, override,
} }
enum ResolvePhase {
firstResolve,
steadyStateResolve,
}
/// Maps [UniversalBenchmark] to the scenario enum for DI chains. /// Maps [UniversalBenchmark] to the scenario enum for DI chains.
UniversalScenario toScenario(UniversalBenchmark b) { UniversalScenario toScenario(UniversalBenchmark b) {
switch (b) { switch (b) {
case UniversalBenchmark.registerSingleton: case UniversalBenchmark.registerSingleton:
case UniversalBenchmark.registerLazySingleton:
return UniversalScenario.register; return UniversalScenario.register;
case UniversalBenchmark.chainSingleton: case UniversalBenchmark.chainSingleton:
case UniversalBenchmark.chainLazySingleton:
return UniversalScenario.chain; return UniversalScenario.chain;
case UniversalBenchmark.chainFactory: case UniversalBenchmark.chainFactory:
return UniversalScenario.chain; return UniversalScenario.chain;
@@ -43,21 +56,21 @@ UniversalScenario toScenario(UniversalBenchmark b) {
} }
} }
/// Maps benchmark to registration mode (singleton/factory/async). /// Maps benchmark to registration mode (singleton/lazySingleton/factory/async).
UniversalBindingMode toMode(UniversalBenchmark b) { UniversalBindingMode toMode(UniversalBenchmark b) {
switch (b) { switch (b) {
case UniversalBenchmark.registerSingleton: case UniversalBenchmark.registerSingleton:
return UniversalBindingMode.singletonStrategy;
case UniversalBenchmark.chainSingleton: case UniversalBenchmark.chainSingleton:
case UniversalBenchmark.named:
case UniversalBenchmark.override:
return UniversalBindingMode.singletonStrategy; return UniversalBindingMode.singletonStrategy;
case UniversalBenchmark.registerLazySingleton:
case UniversalBenchmark.chainLazySingleton:
return UniversalBindingMode.lazySingletonStrategy;
case UniversalBenchmark.chainFactory: case UniversalBenchmark.chainFactory:
return UniversalBindingMode.factoryStrategy; return UniversalBindingMode.factoryStrategy;
case UniversalBenchmark.chainAsync: case UniversalBenchmark.chainAsync:
return UniversalBindingMode.asyncStrategy; return UniversalBindingMode.asyncStrategy;
case UniversalBenchmark.named:
return UniversalBindingMode.singletonStrategy;
case UniversalBenchmark.override:
return UniversalBindingMode.singletonStrategy;
} }
} }
@@ -98,6 +111,10 @@ class BenchmarkCliConfig {
/// Name of DI implementation ("cherrypick" or "getit") /// Name of DI implementation ("cherrypick" or "getit")
final String di; final String di;
/// Which resolve phase(s) to measure.
final List<ResolvePhase> phases;
BenchmarkCliConfig({ BenchmarkCliConfig({
required this.benchesToRun, required this.benchesToRun,
required this.chainCounts, required this.chainCounts,
@@ -106,6 +123,7 @@ class BenchmarkCliConfig {
required this.warmups, required this.warmups,
required this.format, required this.format,
required this.di, required this.di,
required this.phases,
}); });
} }
@@ -119,6 +137,8 @@ BenchmarkCliConfig parseBenchmarkCli(List<String> args) {
..addOption('repeat', abbr: 'r', defaultsTo: '2') ..addOption('repeat', abbr: 'r', defaultsTo: '2')
..addOption('warmup', abbr: 'w', defaultsTo: '1') ..addOption('warmup', abbr: 'w', defaultsTo: '1')
..addOption('format', abbr: 'f', defaultsTo: 'pretty') ..addOption('format', abbr: 'f', defaultsTo: 'pretty')
..addOption('resolvePhase',
defaultsTo: 'all', help: 'Resolve phase: first, steady, or all')
..addOption('di', ..addOption('di',
defaultsTo: 'cherrypick', defaultsTo: 'cherrypick',
help: 'DI implementation: cherrypick, getit or riverpod') help: 'DI implementation: cherrypick, getit or riverpod')
@@ -128,12 +148,39 @@ BenchmarkCliConfig parseBenchmarkCli(List<String> args) {
print(parser.usage); print(parser.usage);
exit(0); exit(0);
} }
final benchName = result['benchmark'] as String; final benchNameInput = result['benchmark'] as String;
final isAll = benchName == 'all'; final isAll = benchNameInput.trim() == 'all';
final allBenches = UniversalBenchmark.values; final allBenches = UniversalBenchmark.values;
String normalizeBenchName(String name) {
final n = name.trim().toLowerCase();
return switch (n) {
'register' || 'registersingleton' || 'registereager' => 'registerSingleton',
'registerlazy' || 'registerlazysingleton' || 'registerlazysingle' => 'registerLazySingleton',
'chain' || 'chainsingleton' || 'chaineager' => 'chainSingleton',
'chainlazy' || 'chainlazysingleton' || 'lazysingleton' => 'chainLazySingleton',
'chainfactory' || 'factory' => 'chainFactory',
'async' || 'asyncchain' || 'chainasync' => 'chainAsync',
'named' => 'named',
'override' => 'override',
_ => n,
};
}
final benchesToRun = isAll final benchesToRun = isAll
? allBenches ? allBenches
: [parseEnum(benchName, allBenches, UniversalBenchmark.chainSingleton)]; : benchNameInput
.split(',')
.map((n) => parseEnum(normalizeBenchName(n), allBenches,
UniversalBenchmark.chainSingleton))
.toSet()
.toList();
final phaseName = (result['resolvePhase'] as String).toLowerCase();
final phases = switch (phaseName) {
'first' => [ResolvePhase.firstResolve],
'steady' => [ResolvePhase.steadyStateResolve],
_ => ResolvePhase.values,
};
return BenchmarkCliConfig( return BenchmarkCliConfig(
benchesToRun: benchesToRun, benchesToRun: benchesToRun,
chainCounts: parseIntList(result['chainCount'] as String), chainCounts: parseIntList(result['chainCount'] as String),
@@ -142,5 +189,6 @@ BenchmarkCliConfig parseBenchmarkCli(List<String> args) {
warmups: int.tryParse(result['warmup'] as String? ?? "") ?? 1, warmups: int.tryParse(result['warmup'] as String? ?? "") ?? 1,
format: result['format'] as String, format: result['format'] as String,
di: result['di'] as String? ?? 'cherrypick', di: result['di'] as String? ?? 'cherrypick',
phases: phases,
); );
} }

View File

@@ -6,6 +6,7 @@ class CsvReport extends ReportGenerator {
@override @override
final List<String> keys = [ final List<String> keys = [
'benchmark', 'benchmark',
'phase',
'chainCount', 'chainCount',
'nestingDepth', 'nestingDepth',
'mean_us', 'mean_us',

View File

@@ -8,6 +8,7 @@ class MarkdownReport extends ReportGenerator {
@override @override
final List<String> keys = [ final List<String> keys = [
'benchmark', 'benchmark',
'phase',
'chainCount', 'chainCount',
'nestingDepth', 'nestingDepth',
'mean_us', 'mean_us',
@@ -24,7 +25,9 @@ class MarkdownReport extends ReportGenerator {
/// Friendly display names for each benchmark type. /// Friendly display names for each benchmark type.
static const nameMap = { static const nameMap = {
'Universal_UniversalBenchmark.registerSingleton': 'RegisterSingleton', 'Universal_UniversalBenchmark.registerSingleton': 'RegisterSingleton',
'Universal_UniversalBenchmark.registerLazySingleton': 'RegisterLazySingleton',
'Universal_UniversalBenchmark.chainSingleton': 'ChainSingleton', 'Universal_UniversalBenchmark.chainSingleton': 'ChainSingleton',
'Universal_UniversalBenchmark.chainLazySingleton': 'ChainLazySingleton',
'Universal_UniversalBenchmark.chainFactory': 'ChainFactory', 'Universal_UniversalBenchmark.chainFactory': 'ChainFactory',
'Universal_UniversalBenchmark.chainAsync': 'AsyncChain', 'Universal_UniversalBenchmark.chainAsync': 'AsyncChain',
'Universal_UniversalBenchmark.named': 'Named', 'Universal_UniversalBenchmark.named': 'Named',
@@ -36,6 +39,7 @@ class MarkdownReport extends ReportGenerator {
String render(List<Map<String, dynamic>> rows) { String render(List<Map<String, dynamic>> rows) {
final headers = [ final headers = [
'Benchmark', 'Benchmark',
'Phase',
'Chain Count', 'Chain Count',
'Depth', 'Depth',
'Mean (us)', 'Mean (us)',
@@ -52,6 +56,7 @@ class MarkdownReport extends ReportGenerator {
final readableName = nameMap[r['benchmark']] ?? r['benchmark']; final readableName = nameMap[r['benchmark']] ?? r['benchmark'];
return [ return [
readableName, readableName,
r['phase'],
r['chainCount'], r['chainCount'],
r['nestingDepth'], r['nestingDepth'],
r['mean_us'], r['mean_us'],
@@ -82,6 +87,7 @@ class MarkdownReport extends ReportGenerator {
final legend = ''' final legend = '''
> **Legend:** > **Legend:**
> `Benchmark` Test name > `Benchmark` Test name
> `Phase` `firstResolve` or `steadyStateResolve`
> `Chain Count` Number of independent chains > `Chain Count` Number of independent chains
> `Depth` Depth of each chain > `Depth` Depth of each chain
> `Mean (us)` Average time per run (microseconds) > `Mean (us)` Average time per run (microseconds)

View File

@@ -8,6 +8,7 @@ class PrettyReport extends ReportGenerator {
@override @override
final List<String> keys = [ final List<String> keys = [
'benchmark', 'benchmark',
'phase',
'chainCount', 'chainCount',
'nestingDepth', 'nestingDepth',
'mean_us', 'mean_us',
@@ -24,7 +25,9 @@ class PrettyReport extends ReportGenerator {
/// Mappings from internal benchmark IDs to display names. /// Mappings from internal benchmark IDs to display names.
static const nameMap = { static const nameMap = {
'Universal_UniversalBenchmark.registerSingleton': 'RegisterSingleton', 'Universal_UniversalBenchmark.registerSingleton': 'RegisterSingleton',
'Universal_UniversalBenchmark.registerLazySingleton': 'RegisterLazySingleton',
'Universal_UniversalBenchmark.chainSingleton': 'ChainSingleton', 'Universal_UniversalBenchmark.chainSingleton': 'ChainSingleton',
'Universal_UniversalBenchmark.chainLazySingleton': 'ChainLazySingleton',
'Universal_UniversalBenchmark.chainFactory': 'ChainFactory', 'Universal_UniversalBenchmark.chainFactory': 'ChainFactory',
'Universal_UniversalBenchmark.chainAsync': 'AsyncChain', 'Universal_UniversalBenchmark.chainAsync': 'AsyncChain',
'Universal_UniversalBenchmark.named': 'Named', 'Universal_UniversalBenchmark.named': 'Named',
@@ -36,6 +39,7 @@ class PrettyReport extends ReportGenerator {
String render(List<Map<String, dynamic>> rows) { String render(List<Map<String, dynamic>> rows) {
final headers = [ final headers = [
'Benchmark', 'Benchmark',
'Phase',
'Chain Count', 'Chain Count',
'Depth', 'Depth',
'Mean (us)', 'Mean (us)',
@@ -53,6 +57,7 @@ class PrettyReport extends ReportGenerator {
final readableName = nameMap[r['benchmark']] ?? r['benchmark']; final readableName = nameMap[r['benchmark']] ?? r['benchmark'];
return [ return [
readableName, readableName,
r['phase'],
r['chainCount'], r['chainCount'],
r['nestingDepth'], r['nestingDepth'],
r['mean_us'], r['mean_us'],

View File

@@ -2,6 +2,7 @@ import 'dart:io';
import 'dart:math'; import 'dart:math';
import 'package:benchmark_di/benchmarks/universal_chain_benchmark.dart'; import 'package:benchmark_di/benchmarks/universal_chain_benchmark.dart';
import 'package:benchmark_di/benchmarks/universal_chain_async_benchmark.dart'; import 'package:benchmark_di/benchmarks/universal_chain_async_benchmark.dart';
import 'package:benchmark_di/cli/parser.dart';
/// Holds the results for a single benchmark execution. /// Holds the results for a single benchmark execution.
class BenchmarkResult { class BenchmarkResult {
@@ -50,17 +51,24 @@ class BenchmarkRunner {
required UniversalChainBenchmark benchmark, required UniversalChainBenchmark benchmark,
required int warmups, required int warmups,
required int repeats, required int repeats,
required ResolvePhase phase,
}) async { }) async {
final timings = <num>[]; final timings = <num>[];
final rssValues = <int>[]; final rssValues = <int>[];
for (int i = 0; i < warmups; i++) { for (int i = 0; i < warmups; i++) {
benchmark.setup(); benchmark.setup();
if (phase == ResolvePhase.steadyStateResolve) {
benchmark.prewarm();
}
benchmark.run(); benchmark.run();
benchmark.teardown(); benchmark.teardown();
} }
final memBefore = ProcessInfo.currentRss; final memBefore = ProcessInfo.currentRss;
for (int i = 0; i < repeats; i++) { for (int i = 0; i < repeats; i++) {
benchmark.setup(); benchmark.setup();
if (phase == ResolvePhase.steadyStateResolve) {
benchmark.prewarm();
}
final sw = Stopwatch()..start(); final sw = Stopwatch()..start();
benchmark.run(); benchmark.run();
sw.stop(); sw.stop();
@@ -78,17 +86,24 @@ class BenchmarkRunner {
required UniversalChainAsyncBenchmark benchmark, required UniversalChainAsyncBenchmark benchmark,
required int warmups, required int warmups,
required int repeats, required int repeats,
required ResolvePhase phase,
}) async { }) async {
final timings = <num>[]; final timings = <num>[];
final rssValues = <int>[]; final rssValues = <int>[];
for (int i = 0; i < warmups; i++) { for (int i = 0; i < warmups; i++) {
await benchmark.setup(); await benchmark.setup();
if (phase == ResolvePhase.steadyStateResolve) {
await benchmark.prewarm();
}
await benchmark.run(); await benchmark.run();
await benchmark.teardown(); await benchmark.teardown();
} }
final memBefore = ProcessInfo.currentRss; final memBefore = ProcessInfo.currentRss;
for (int i = 0; i < repeats; i++) { for (int i = 0; i < repeats; i++) {
await benchmark.setup(); await benchmark.setup();
if (phase == ResolvePhase.steadyStateResolve) {
await benchmark.prewarm();
}
final sw = Stopwatch()..start(); final sw = Stopwatch()..start();
await benchmark.run(); await benchmark.run();
sw.stop(); sw.stop();

View File

@@ -59,11 +59,16 @@ class UniversalChainModule extends Module {
switch (scenario) { switch (scenario) {
case UniversalScenario.register: case UniversalScenario.register:
// Simple singleton registration. if (bindingMode == UniversalBindingMode.lazySingletonStrategy) {
bind<UniversalService>() bind<UniversalService>()
.toProvide( .toProvide(
() => UniversalServiceImpl(value: 'reg', dependency: null)) () => UniversalServiceImpl(value: 'reg', dependency: null))
.singleton(); .singleton();
} else {
bind<UniversalService>()
.toInstance(
UniversalServiceImpl(value: 'reg', dependency: null));
}
break; break;
case UniversalScenario.named: case UniversalScenario.named:
// Named factory registration for two distinct objects. // Named factory registration for two distinct objects.
@@ -76,6 +81,8 @@ class UniversalChainModule extends Module {
break; break;
case UniversalScenario.chain: case UniversalScenario.chain:
// Chain of nested services, with dependency on previous level by name. // Chain of nested services, with dependency on previous level by name.
UniversalService? lastEagerInstance;
final Map<String, UniversalService> eagerInstances = {};
for (var chainIndex = 0; chainIndex < chainCount; chainIndex++) { for (var chainIndex = 0; chainIndex < chainCount; chainIndex++) {
for (var levelIndex = 0; levelIndex < nestingDepth; levelIndex++) { for (var levelIndex = 0; levelIndex < nestingDepth; levelIndex++) {
final chain = chainIndex + 1; final chain = chainIndex + 1;
@@ -84,6 +91,17 @@ class UniversalChainModule extends Module {
final depName = '${chain}_$level'; final depName = '${chain}_$level';
switch (bindingMode) { switch (bindingMode) {
case UniversalBindingMode.singletonStrategy: case UniversalBindingMode.singletonStrategy:
final instance = UniversalServiceImpl(
value: depName,
dependency: eagerInstances[prevDepName],
);
bind<UniversalService>()
.toInstance(instance)
.withName(depName);
eagerInstances[depName] = instance;
lastEagerInstance = instance;
break;
case UniversalBindingMode.lazySingletonStrategy:
bind<UniversalService>() bind<UniversalService>()
.toProvide(() => UniversalServiceImpl( .toProvide(() => UniversalServiceImpl(
value: depName, value: depName,
@@ -116,15 +134,19 @@ class UniversalChainModule extends Module {
} }
} }
} }
// Регистрация алиаса без имени (на последний элемент цепочки) // Register unnamed alias for the last chain element.
final depName = '${chainCount}_$nestingDepth'; if (bindingMode == UniversalBindingMode.lazySingletonStrategy) {
bind<UniversalService>() final depName = '${chainCount}_$nestingDepth';
.toProvide( bind<UniversalService>()
() => currentScope.resolve<UniversalService>(named: depName)) .toProvide(
.singleton(); () => currentScope.resolve<UniversalService>(named: depName))
.singleton();
} else if (lastEagerInstance != null) {
bind<UniversalService>().toInstance(lastEagerInstance);
}
break; break;
case UniversalScenario.override: case UniversalScenario.override:
// handled at benchmark level, но алиас нужен прямо в этом scope! // Handled at benchmark level, but alias is needed directly in this scope.
final depName = '${chainCount}_$nestingDepth'; final depName = '${chainCount}_$nestingDepth';
bind<UniversalService>() bind<UniversalService>()
.toProvide( .toProvide(
@@ -189,7 +211,7 @@ class CherrypickDIAdapter extends DIAdapter<Scope> {
await CherryPick.closeRootScope(); await CherryPick.closeRootScope();
_scope = null; _scope = null;
} }
// SubScope teardown не требуется // SubScope teardown is handled by the parent scope.
} }
@override @override

View File

@@ -4,14 +4,14 @@ import 'package:benchmark_di/scenarios/universal_service.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import 'di_adapter.dart'; import 'di_adapter.dart';
/// Универсальный DIAdapter для GetIt c поддержкой scopes и строгой типизацией. /// DIAdapter for GetIt with scope support and strict typing.
class GetItAdapter extends DIAdapter<GetIt> { class GetItAdapter extends DIAdapter<GetIt> {
late GetIt _getIt; late GetIt _getIt;
final String? _scopeName; final String? _scopeName;
final bool _isSubScope; final bool _isSubScope;
bool _scopePushed = false; bool _scopePushed = false;
/// Основной (root) и subScope-конструкторы. /// Root and subScope constructors.
GetItAdapter({GetIt? instance, String? scopeName, bool isSubScope = false}) GetItAdapter({GetIt? instance, String? scopeName, bool isSubScope = false})
: _scopeName = scopeName, : _scopeName = scopeName,
_isSubScope = isSubScope { _isSubScope = isSubScope {
@@ -23,7 +23,7 @@ class GetItAdapter extends DIAdapter<GetIt> {
@override @override
void setupDependencies(void Function(GetIt container) registration) { void setupDependencies(void Function(GetIt container) registration) {
if (_isSubScope) { if (_isSubScope) {
// Создаём scope через pushNewScope с init // Create scope via pushNewScope with init
_getIt.pushNewScope( _getIt.pushNewScope(
scopeName: _scopeName, scopeName: _scopeName,
init: (getIt) => registration(getIt), init: (getIt) => registration(getIt),
@@ -41,7 +41,7 @@ class GetItAdapter extends DIAdapter<GetIt> {
@override @override
Future<T> resolveAsync<T extends Object>({String? named}) async => Future<T> resolveAsync<T extends Object>({String? named}) async =>
_getIt<T>(instanceName: named); _getIt.getAsync<T>(instanceName: named);
@override @override
void teardown() { void teardown() {
@@ -92,8 +92,13 @@ class GetItAdapter extends DIAdapter<GetIt> {
} }
break; break;
case UniversalScenario.register: case UniversalScenario.register:
getIt.registerSingleton<UniversalService>( if (bindingMode == UniversalBindingMode.lazySingletonStrategy) {
UniversalServiceImpl(value: 'reg', dependency: null)); getIt.registerLazySingleton<UniversalService>(
() => UniversalServiceImpl(value: 'reg', dependency: null));
} else {
getIt.registerSingleton<UniversalService>(
UniversalServiceImpl(value: 'reg', dependency: null));
}
break; break;
case UniversalScenario.named: case UniversalScenario.named:
getIt.registerFactory<UniversalService>( getIt.registerFactory<UniversalService>(
@@ -120,6 +125,17 @@ class GetItAdapter extends DIAdapter<GetIt> {
instanceName: depName, instanceName: depName,
); );
break; break;
case UniversalBindingMode.lazySingletonStrategy:
getIt.registerLazySingleton<UniversalService>(
() => UniversalServiceImpl(
value: depName,
dependency: level > 1
? getIt<UniversalService>(instanceName: prevDepName)
: null,
),
instanceName: depName,
);
break;
case UniversalBindingMode.factoryStrategy: case UniversalBindingMode.factoryStrategy:
getIt.registerFactory<UniversalService>( getIt.registerFactory<UniversalService>(
() => UniversalServiceImpl( () => UniversalServiceImpl(
@@ -154,9 +170,15 @@ class GetItAdapter extends DIAdapter<GetIt> {
if (scenario == UniversalScenario.chain || if (scenario == UniversalScenario.chain ||
scenario == UniversalScenario.override) { scenario == UniversalScenario.override) {
final depName = '${chainCount}_$nestingDepth'; final depName = '${chainCount}_$nestingDepth';
getIt.registerSingleton<UniversalService>( if (bindingMode == UniversalBindingMode.lazySingletonStrategy) {
getIt<UniversalService>(instanceName: depName), getIt.registerLazySingleton<UniversalService>(
); () => getIt<UniversalService>(instanceName: depName),
);
} else {
getIt.registerSingleton<UniversalService>(
getIt<UniversalService>(instanceName: depName),
);
}
} }
}; };
} }

View File

@@ -4,14 +4,11 @@ import 'package:benchmark_di/scenarios/universal_service.dart';
import 'package:kiwi/kiwi.dart'; import 'package:kiwi/kiwi.dart';
import 'di_adapter.dart'; import 'di_adapter.dart';
/// DIAdapter-для KiwiContainer с поддержкой universal benchmark сценариев. /// DIAdapter for KiwiContainer with universal benchmark scenario support.
class KiwiAdapter extends DIAdapter<KiwiContainer> { class KiwiAdapter extends DIAdapter<KiwiContainer> {
late KiwiContainer _container; late KiwiContainer _container;
// ignore: unused_field
final bool _isSubScope;
KiwiAdapter({KiwiContainer? container, bool isSubScope = false}) KiwiAdapter({KiwiContainer? container, bool isSubScope = false}) {
: _isSubScope = isSubScope {
_container = container ?? KiwiContainer(); _container = container ?? KiwiContainer();
} }
@@ -57,6 +54,8 @@ class KiwiAdapter extends DIAdapter<KiwiContainer> {
final depName = '${chain}_$level'; final depName = '${chain}_$level';
switch (bindingMode) { switch (bindingMode) {
case UniversalBindingMode.singletonStrategy: case UniversalBindingMode.singletonStrategy:
case UniversalBindingMode.lazySingletonStrategy:
// Kiwi's registerSingleton is lazy (factory-based, cached on first resolve)
container.registerSingleton<UniversalService>( container.registerSingleton<UniversalService>(
(c) => UniversalServiceImpl( (c) => UniversalServiceImpl(
value: depName, value: depName,
@@ -75,7 +74,7 @@ class KiwiAdapter extends DIAdapter<KiwiContainer> {
name: depName); name: depName);
break; break;
case UniversalBindingMode.asyncStrategy: case UniversalBindingMode.asyncStrategy:
// Не поддерживается // Not supported
break; break;
} }
} }
@@ -97,23 +96,12 @@ class KiwiAdapter extends DIAdapter<KiwiContainer> {
@override @override
T resolve<T extends Object>({String? named}) { T resolve<T extends Object>({String? named}) {
// Для asyncChain нужен resolve<Future<T>> return _container.resolve<T>(named);
if (T.toString().startsWith('Future<')) {
return _container.resolve<T>(named);
} else {
return _container.resolve<T>(named);
}
} }
@override @override
Future<T> resolveAsync<T extends Object>({String? named}) async { Future<T> resolveAsync<T extends Object>({String? named}) {
if (T.toString().startsWith('Future<')) { return Future.value(_container.resolve<T>(named));
// resolve<Future<T>>, unwrap result
return Future.value(_container.resolve<T>(named));
} else {
// Для совместимости с chain/override
return Future.value(_container.resolve<T>(named));
}
} }
@override @override
@@ -123,7 +111,7 @@ class KiwiAdapter extends DIAdapter<KiwiContainer> {
@override @override
KiwiAdapter openSubScope(String name) { KiwiAdapter openSubScope(String name) {
// Возвращаем новый scoped контейнер (отдельный). Наследование не реализовано. // Returns a new scoped container. Inheritance not implemented.
return KiwiAdapter(container: KiwiContainer.scoped(), isSubScope: true); return KiwiAdapter(container: KiwiContainer.scoped(), isSubScope: true);
} }

View File

@@ -29,8 +29,7 @@ class YxScopeAdapter extends DIAdapter<UniversalYxScopeContainer> {
@override @override
void teardown() { void teardown() {
// У yx_scope нет явного dispose на ScopeContainer, но можно добавить очистку Map/Deps если потребуется _scope = UniversalYxScopeContainer();
// Ничего не делаем
} }
@override @override
@@ -54,26 +53,13 @@ class YxScopeAdapter extends DIAdapter<UniversalYxScopeContainer> {
required UniversalBindingMode bindingMode, required UniversalBindingMode bindingMode,
}) { }) {
if (scenario is UniversalScenario) { if (scenario is UniversalScenario) {
if (scenario == UniversalScenario.asyncChain ||
bindingMode == UniversalBindingMode.asyncStrategy) {
throw UnsupportedError(
'YxScope does not support async dependencies or async binding scenarios.');
}
return (scope) { return (scope) {
switch (scenario) { switch (scenario) {
case UniversalScenario.asyncChain:
for (int chain = 1; chain <= chainCount; chain++) {
for (int level = 1; level <= nestingDepth; level++) {
final prevDepName = '${chain}_${level - 1}';
final depName = '${chain}_$level';
final dep = scope.dep<UniversalService>(
() => UniversalServiceImpl(
value: depName,
dependency: level > 1
? scope.depFor<UniversalService>(name: prevDepName).get
: null,
),
name: depName,
);
scope.register<UniversalService>(dep, name: depName);
}
}
break;
case UniversalScenario.register: case UniversalScenario.register:
final dep = scope.dep<UniversalService>( final dep = scope.dep<UniversalService>(
() => UniversalServiceImpl(value: 'reg', dependency: null), () => UniversalServiceImpl(value: 'reg', dependency: null),
@@ -113,6 +99,8 @@ class YxScopeAdapter extends DIAdapter<UniversalYxScopeContainer> {
case UniversalScenario.override: case UniversalScenario.override:
// handled at benchmark level // handled at benchmark level
break; break;
default:
break;
} }
if (scenario == UniversalScenario.chain || if (scenario == UniversalScenario.chain ||
scenario == UniversalScenario.override) { scenario == UniversalScenario.override) {

View File

@@ -1,9 +1,12 @@
/// Enum to represent the DI registration/binding mode. /// Enum to represent the DI registration/binding mode.
enum UniversalBindingMode { enum UniversalBindingMode {
/// Singleton/provider binding. /// Eager singleton — instance created at registration time.
singletonStrategy, singletonStrategy,
/// Factory-based binding. /// Lazy singleton — instance created on first resolve, then cached.
lazySingletonStrategy,
/// Factory-based binding — new instance every time.
factoryStrategy, factoryStrategy,
/// Async-based binding. /// Async-based binding.