fix: universal benchmarks and DI registration; proper named binding; robust override support for cherrypick and get_it; improved CLI args

This commit is contained in:
Sergey Penkovsky
2025-08-07 09:15:26 +03:00
parent d523a5f261
commit 64f33b20a7
11 changed files with 233 additions and 52 deletions

View File

@@ -2,6 +2,7 @@ import 'package:benchmark_harness/benchmark_harness.dart';
import 'package:benchmark_cherrypick/di_adapters/di_adapter.dart';
import 'package:benchmark_cherrypick/scenarios/universal_chain_module.dart';
import 'package:benchmark_cherrypick/scenarios/universal_service.dart';
import 'package:benchmark_cherrypick/scenarios/di_universal_registration.dart';
class UniversalChainAsyncBenchmark extends AsyncBenchmarkBase {
final DIAdapter di;
@@ -18,16 +19,14 @@ class UniversalChainAsyncBenchmark extends AsyncBenchmarkBase {
@override
Future<void> setup() async {
di.setupDependencies((scope) {
scope.installModules([
UniversalChainModule(
chainCount: chainCount,
nestingDepth: nestingDepth,
bindingMode: mode,
scenario: UniversalScenario.asyncChain,
),
]);
});
di.setupDependencies(getUniversalRegistration(
di,
chainCount: chainCount,
nestingDepth: nestingDepth,
bindingMode: mode,
scenario: UniversalScenario.asyncChain,
));
await di.waitForAsyncReady();
}
@override

View File

@@ -2,6 +2,7 @@ import 'package:benchmark_harness/benchmark_harness.dart';
import 'package:benchmark_cherrypick/di_adapters/di_adapter.dart';
import 'package:benchmark_cherrypick/scenarios/universal_chain_module.dart';
import 'package:benchmark_cherrypick/scenarios/universal_service.dart';
import 'package:benchmark_cherrypick/scenarios/di_universal_registration.dart';
class UniversalChainBenchmark extends BenchmarkBase {
final DIAdapter _di;
@@ -23,39 +24,30 @@ class UniversalChainBenchmark extends BenchmarkBase {
void setup() {
switch (scenario) {
case UniversalScenario.override:
_di.setupDependencies((scope) {
scope.installModules([
UniversalChainModule(
chainCount: chainCount,
nestingDepth: nestingDepth,
bindingMode: UniversalBindingMode.singletonStrategy,
scenario: UniversalScenario.register,
),
]);
});
_di.setupDependencies(getUniversalRegistration(
_di,
chainCount: chainCount,
nestingDepth: nestingDepth,
bindingMode: UniversalBindingMode.singletonStrategy,
scenario: UniversalScenario.chain,
));
_childDi = _di.openSubScope('child');
_childDi!.setupDependencies((scope) {
scope.installModules([
UniversalChainModule(
chainCount: chainCount,
nestingDepth: nestingDepth,
bindingMode: UniversalBindingMode.singletonStrategy,
scenario: UniversalScenario.register,
),
]);
});
_childDi!.setupDependencies(getUniversalRegistration(
_childDi!,
chainCount: chainCount,
nestingDepth: nestingDepth,
bindingMode: UniversalBindingMode.singletonStrategy,
scenario: UniversalScenario.chain, // критично: цепочку, а не просто alias!
));
break;
default:
_di.setupDependencies((scope) {
scope.installModules([
UniversalChainModule(
chainCount: chainCount,
nestingDepth: nestingDepth,
bindingMode: mode,
scenario: scenario,
),
]);
});
_di.setupDependencies(getUniversalRegistration(
_di,
chainCount: chainCount,
nestingDepth: nestingDepth,
bindingMode: mode,
scenario: scenario,
));
break;
}
}
@@ -70,7 +62,11 @@ class UniversalChainBenchmark extends BenchmarkBase {
_di.resolve<UniversalService>();
break;
case UniversalScenario.named:
_di.resolve<Object>(named: 'impl2');
if (_di.runtimeType.toString().contains('GetItAdapter')) {
_di.resolve<UniversalService>(named: 'impl2');
} else {
_di.resolve<UniversalService>(named: 'impl2');
}
break;
case UniversalScenario.chain:
final serviceName = '${chainCount}_$nestingDepth';

View File

@@ -11,6 +11,7 @@ import 'runner.dart';
import 'package:benchmark_cherrypick/benchmarks/universal_chain_benchmark.dart';
import 'package:benchmark_cherrypick/benchmarks/universal_chain_async_benchmark.dart';
import 'package:benchmark_cherrypick/di_adapters/cherrypick_adapter.dart';
import 'package:benchmark_cherrypick/di_adapters/get_it_adapter.dart';
/// Command-line interface (CLI) runner for benchmarks.
///
@@ -28,8 +29,8 @@ class BenchmarkCliRunner {
for (final c in config.chainCounts) {
for (final d in config.nestDepths) {
BenchmarkResult benchResult;
final di = config.di == 'getit' ? GetItAdapter() : CherrypickDIAdapter();
if (scenario == UniversalScenario.asyncChain) {
final di = CherrypickDIAdapter();
final benchAsync = UniversalChainAsyncBenchmark(di,
chainCount: c, nestingDepth: d, mode: mode,
);
@@ -39,7 +40,6 @@ class BenchmarkCliRunner {
repeats: config.repeats,
);
} else {
final di = CherrypickDIAdapter();
final benchSync = UniversalChainBenchmark(di,
chainCount: c, nestingDepth: d, mode: mode, scenario: scenario,
);

View File

@@ -81,6 +81,8 @@ class BenchmarkCliConfig {
final int warmups;
/// Output report format.
final String format;
/// Name of DI implementation ("cherrypick" or "getit")
final String di;
BenchmarkCliConfig({
required this.benchesToRun,
required this.chainCounts,
@@ -88,6 +90,7 @@ class BenchmarkCliConfig {
required this.repeats,
required this.warmups,
required this.format,
required this.di,
});
}
@@ -101,6 +104,7 @@ BenchmarkCliConfig parseBenchmarkCli(List<String> args) {
..addOption('repeat', abbr: 'r', defaultsTo: '2')
..addOption('warmup', abbr: 'w', defaultsTo: '1')
..addOption('format', abbr: 'f', defaultsTo: 'pretty')
..addOption('di', defaultsTo: 'cherrypick', help: 'DI implementation: cherrypick or getit')
..addFlag('help', abbr: 'h', negatable: false, help: 'Show help');
final result = parser.parse(args);
if (result['help'] == true) {
@@ -120,5 +124,6 @@ BenchmarkCliConfig parseBenchmarkCli(List<String> args) {
repeats: int.tryParse(result['repeat'] as String? ?? "") ?? 2,
warmups: int.tryParse(result['warmup'] as String? ?? "") ?? 1,
format: result['format'] as String,
di: result['di'] as String? ?? 'cherrypick',
);
}

View File

@@ -12,11 +12,11 @@ class CherrypickDIAdapter implements DIAdapter {
}
@override
T resolve<T>({String? named}) =>
T resolve<T extends Object>({String? named}) =>
named == null ? _scope!.resolve<T>() : _scope!.resolve<T>(named: named);
@override
Future<T> resolveAsync<T>({String? named}) async =>
Future<T> resolveAsync<T extends Object>({String? named}) async =>
named == null ? await _scope!.resolveAsync<T>() : await _scope!.resolveAsync<T>(named: named);
@override
@@ -30,6 +30,9 @@ class CherrypickDIAdapter implements DIAdapter {
final sub = _scope!.openSubScope(name);
return _CherrypickSubScopeAdapter(sub);
}
@override
Future<void> waitForAsyncReady() async {}
}
/// Internal adapter for a CherryPick sub-scope (callbacks based).
@@ -43,11 +46,11 @@ class _CherrypickSubScopeAdapter extends CherrypickDIAdapter {
}
@override
T resolve<T>({String? named}) =>
T resolve<T extends Object>({String? named}) =>
named == null ? _subScope.resolve<T>() : _subScope.resolve<T>(named: named);
@override
Future<T> resolveAsync<T>({String? named}) async =>
Future<T> resolveAsync<T extends Object>({String? named}) async =>
named == null ? await _subScope.resolveAsync<T>() : await _subScope.resolveAsync<T>(named: named);
@override

View File

@@ -8,14 +8,17 @@ abstract class DIAdapter {
void setupDependencies(void Function(dynamic container) registration);
/// Резолвит (возвращает) экземпляр типа [T] (по имени, если требуется).
T resolve<T>({String? named});
T resolve<T extends Object>({String? named});
/// Асинхронно резолвит экземпляр типа [T].
Future<T> resolveAsync<T>({String? named});
Future<T> resolveAsync<T extends Object>({String? named});
/// Уничтожает/отчищает DI-контейнер.
void teardown();
/// Открывает дочерний под-scope (если применимо).
DIAdapter openSubScope(String name);
/// Ожидание готовности DI контейнера (нужно для async DI, например get_it)
Future<void> waitForAsyncReady();
}

View File

@@ -0,0 +1,32 @@
import 'package:get_it/get_it.dart';
import 'di_adapter.dart';
class GetItAdapter implements DIAdapter {
late GetIt _getIt;
@override
void setupDependencies(void Function(dynamic container) registration) {
_getIt = GetIt.asNewInstance();
registration(_getIt);
}
@override
T resolve<T extends Object>({String? named}) => _getIt<T>(instanceName: named);
@override
Future<T> resolveAsync<T extends Object>({String? named}) async => _getIt<T>(instanceName: named);
@override
void teardown() => _getIt.reset();
@override
DIAdapter openSubScope(String name) {
// get_it не поддерживает scope, возвращаем новый инстанс
return GetItAdapter();
}
@override
Future<void> waitForAsyncReady() async {
await _getIt.allReady();
}
}

View File

@@ -0,0 +1,109 @@
import 'package:benchmark_cherrypick/scenarios/universal_service.dart';
import '../di_adapters/di_adapter.dart';
import '../di_adapters/cherrypick_adapter.dart';
import '../di_adapters/get_it_adapter.dart';
import 'universal_chain_module.dart';
import 'universal_service.dart';
import 'package:get_it/get_it.dart';
import 'package:cherrypick/cherrypick.dart';
/// Возвращает универсальную функцию регистрации зависимостей,
/// подходящую под выбранный DI-адаптер.
void Function(dynamic) getUniversalRegistration(
DIAdapter adapter, {
required int chainCount,
required int nestingDepth,
required UniversalBindingMode bindingMode,
required UniversalScenario scenario,
}) {
if (adapter is CherrypickDIAdapter) {
return (scope) {
scope.installModules([
UniversalChainModule(
chainCount: chainCount,
nestingDepth: nestingDepth,
bindingMode: bindingMode,
scenario: scenario,
),
]);
};
} else if (adapter is GetItAdapter) {
return (getIt) {
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';
getIt.registerSingletonAsync<UniversalService>(
() async {
final prev = level > 1
? await getIt.getAsync<UniversalService>(instanceName: prevDepName)
: null;
return UniversalServiceImpl(value: depName, dependency: prev);
},
instanceName: depName,
);
}
}
break;
case UniversalScenario.register:
getIt.registerSingleton<UniversalService>(UniversalServiceImpl(value: 'reg', dependency: null));
break;
case UniversalScenario.named:
getIt.registerFactory<UniversalService>(() => UniversalServiceImpl(value: 'impl1'), instanceName: 'impl1');
getIt.registerFactory<UniversalService>(() => UniversalServiceImpl(value: 'impl2'), instanceName: 'impl2');
break;
case UniversalScenario.chain:
for (int chain = 1; chain <= chainCount; chain++) {
for (int level = 1; level <= nestingDepth; level++) {
final prevDepName = '${chain}_${level - 1}';
final depName = '${chain}_$level';
switch (bindingMode) {
case UniversalBindingMode.singletonStrategy:
getIt.registerSingleton<UniversalService>(
UniversalServiceImpl(
value: depName,
dependency: level > 1
? getIt<UniversalService>(instanceName: prevDepName)
: null,
),
instanceName: depName,
);
break;
case UniversalBindingMode.factoryStrategy:
getIt.registerFactory<UniversalService>(
() => UniversalServiceImpl(
value: depName,
dependency: level > 1
? getIt<UniversalService>(instanceName: prevDepName)
: null,
),
instanceName: depName,
);
break;
case UniversalBindingMode.asyncStrategy:
// getIt не поддерживает асинх. factory напрямую, но можно так:
getIt.registerSingletonAsync<UniversalService>(
() async => UniversalServiceImpl(
value: depName,
dependency: level > 1
? await getIt.getAsync<UniversalService>(instanceName: prevDepName)
: null,
),
instanceName: depName,
);
break;
}
}
}
break;
case UniversalScenario.override:
// handled at benchmark level
break;
}
};
}
throw UnsupportedError('Unknown DIAdapter type: ${adapter.runtimeType}');
}

View File

@@ -85,8 +85,8 @@ class UniversalChainModule extends Module {
break;
case UniversalScenario.named:
// Named factory registration for two distinct objects.
bind<Object>().toProvide(() => UniversalServiceImpl(value: 'impl1')).withName('impl1');
bind<Object>().toProvide(() => UniversalServiceImpl(value: 'impl2')).withName('impl2');
bind<UniversalService>().toProvide(() => UniversalServiceImpl(value: 'impl1')).withName('impl1');
bind<UniversalService>().toProvide(() => UniversalServiceImpl(value: 'impl2')).withName('impl2');
break;
case UniversalScenario.chain:
// Chain of nested services, with dependency on previous level by name.
@@ -126,9 +126,18 @@ class UniversalChainModule extends Module {
}
}
}
// Регистрация алиаса без имени (на последний элемент цепочки)
final depName = '${chainCount}_${nestingDepth}';
bind<UniversalService>()
.toProvide(() => currentScope.resolve<UniversalService>(named: depName))
.singleton();
break;
case UniversalScenario.override:
// handled at benchmark level
// handled at benchmark level, но алиас нужен прямо в этом scope!
final depName = '${chainCount}_${nestingDepth}';
bind<UniversalService>()
.toProvide(() => currentScope.resolve<UniversalService>(named: depName))
.singleton();
break;
case UniversalScenario.asyncChain:
// already handled above

View File

@@ -17,6 +17,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.7.0"
async:
dependency: transitive
description:
name: async
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
url: "https://pub.dev"
source: hosted
version: "2.13.0"
benchmark_harness:
dependency: "direct dev"
description:
@@ -40,6 +48,14 @@ packages:
relative: true
source: path
version: "3.0.0-dev.2"
collection:
dependency: transitive
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
source: hosted
version: "1.19.1"
exception_templates:
dependency: transitive
description:
@@ -48,6 +64,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.3.1"
get_it:
dependency: "direct main"
description:
name: get_it
sha256: a4292e7cf67193f8e7c1258203104eb2a51ec8b3a04baa14695f4064c144297b
url: "https://pub.dev"
source: hosted
version: "8.2.0"
lazy_memo:
dependency: transitive
description:

View File

@@ -10,6 +10,7 @@ dependencies:
cherrypick:
path: ../cherrypick
args: ^2.7.0
get_it: ^8.2.0
dev_dependencies:
lints: ^5.0.0