Compare commits

..

2 Commits

Author SHA1 Message Date
Sergey Penkovsky
72e1e91b96 docs: sync references after ProviderFactory rename 2026-03-24 10:54:01 +03:00
Sergey Penkovsky
e67ebbe2ab rename Provider typedef to ProviderFactory 2026-03-24 10:40:49 +03:00
28 changed files with 484 additions and 1608 deletions

View File

@@ -1,137 +0,0 @@
# Benchmark Comparison: cherrypick Performance Improvements
## Parameters
- chainCount = 100
- nestingDepth = 100
- repeat = 5
- warmup = 2
---
## Results: firstResolve (Mean, µs)
### Lazy Singleton Scenarios (fair cross-DI comparison)
| Scenario | BR-improvements | develop | Improvement (vs develop) |
|-----------------------|-----------------|---------|--------------------------|
| ChainLazySingleton | 46.80 | 114.20 | **2.4× faster** |
| ChainFactory | 54.00 | 105.60 | **2.0× faster** |
| AsyncChain | 247.80 | 959.20 | **3.9× faster** |
| Named | 0.20 | 3.60 | **18× faster** |
| Override | 9.80 | 110.40 | **11.3× faster** |
| RegisterLazySingleton | 1.20 | 19.60 | **16.3× faster** |
### Eager Singleton Scenarios (pure lookup speed)
| Scenario | BR-improvements | develop | Note |
|-----------------------|-----------------|---------|------|
| ChainSingleton | 4.80 | 114.20 | Not comparable — see note below |
| RegisterSingleton | 12.80 | 19.60 | **1.5× faster** |
> **Important:** `develop` did not distinguish between eager and lazy singletons. Its "singleton" was always lazy (instance created on first resolve). The develop `ChainSingleton` value (114.2 µs) measures the same thing as BR-improvements `ChainLazySingleton` (46.8 µs). The `ChainSingleton` eager column in BR-improvements (4.8 µs) measures pure lookup after `.toInstance()` eager registration — a fundamentally different operation. Fair comparison: 114.2 → 46.8 = 2.4× faster.
\* All measurements taken on the same hardware with identical benchmark parameters.
---
## Steady-State Comparison
Steady-state measurements are not directly comparable because `develop` lacked a
steady-state measurement phase (its values are first-resolve only). BR-improvements
introduced a separate steady-state phase to isolate cached lookup performance.
---
## Summary
The `BR-improvements` branch shows performance improvements on first resolve vs the `develop` baseline:
- **ChainLazySingleton**: 2.4× faster (46.8 µs vs 114.2 µs)
- **ChainFactory**: 2.0× faster (54.0 µs vs 105.6 µs)
- **AsyncChain**: 3.9× faster (247.8 µs vs 959.2 µs)
- **Named**: 18× faster (0.2 µs vs 3.6 µs)
- **Override**: 11.3× faster (9.8 µs vs 110.4 µs)
- **RegisterLazySingleton**: 16.3× faster (1.2 µs vs 19.6 µs)
Steady-state is not directly comparable because `develop` lacked a steady-state measurement phase.
### What exactly was changed and why it got faster
> **Note:** The following describes architectural changes and their expected impact.
> Specific percentage contributions have not been measured via isolated A/B tests.
---
#### 1. Split monolithic resolvers into type-specialized classes
**What changed:** The single `ProviderResolver<T>` class (which handled sync/async/param/ no-param in one place with runtime type checks) was split into four dedicated classes:
- `SyncProviderResolver<T>` / `SyncProviderWithParamsResolver<T>`
- `AsyncProviderResolver<T>` / `AsyncProviderWithParamsResolver<T>`
`ProviderResolver.create()` uses fast-path `is` checks to pick the correct implementation immediately, without calling the provider. A lazy fallback `FutureOrProviderResolver` is used only when the static type is genuinely unknown.
Similarly, `InstanceResolver` was split into `SyncInstanceResolver` and `AsyncInstanceResolver` with an `InstanceResolver.create()` factory.
**Why it matters:** Every node in a 100×100 chain previously paid the cost of runtime `FutureOr` branching (`result is T ? sync : async`) on every resolve. Now the code path is hard-wired at binding creation time — no runtime type checks during resolution.
**Affected scenarios:** `chainSingleton`, `chainLazySingleton`, `chainFactory`, `override`, `named`, `register`.
---
#### 2. Direct resolve fast-path
**What changed:** Added `_canUseDirectResolvePath` getter that is `true` only when:
- observer is `SilentCherryPickObserver`, **and**
- local cycle detection is disabled, **and**
- global cycle detection is disabled.
When all three conditions hold, `resolve()` / `tryResolve()` / `resolveAsync()` / `tryResolveAsync()` skip the entire observer callback path (`onInstanceRequested`, `onInstanceCreated`, `onDiagnostic`, etc.) and cycle-detection wrappers entirely. They call `_tryResolveInternal` directly.
**Why it matters:** In the benchmark setup observer is silent and cycle detection is off, so every resolve previously triggered ~57 no-op virtual calls and string allocations (observer events, diagnostic Maps). That overhead is now completely bypassed.
**Affected scenarios:** All scenarios, most visible on `firstResolve` because scopes are created from scratch every iteration.
---
#### 3. Silent observer guard
**What changed:** All diagnostic calls (`observer.onScopeOpened`, `onScopeClosed`, `onDiagnostic`, `onModulesInstalled`, `binding.logAllDeferred()`) are now wrapped in `if (!_isSilentObserver)`. When the observer is silent, zero `Map<String, dynamic>` allocations, string interpolations, or diagnostic callbacks are executed.
**Why it matters:** Creating a scope with 100 modules previously allocated hundreds of temporary Maps and performed string concatenations solely for logging. That work is now entirely skipped.
**Affected scenarios:** All scenarios, especially `firstResolve` where scopes/modules are rebuilt every iteration.
---
#### 4. Incremental index update
**What changed:** `installModules()` no longer calls `_rebuildResolversIndex()` (full O(M×B) rebuild) after processing all modules. Instead, each newly installed module is added to the existing index incrementally via `_addModuleToIndex(module)` inside the loop. `_rebuildResolversIndex()` is now only called from `dropModules()`.
**Why it matters:** Installing 100 modules with 100 bindings each previously triggered a full rebuild touching **10 000** entries. Now only the 100 new entries are inserted.
**Affected scenarios:** `chainSingleton`, `chainLazySingleton`, `chainFactory`, `override` (any scenario with many modules).
---
#### 5. `bool _isCached` flag + streamlined `_trackDisposable`
**What changed:**
- Replaced `_cache != null` checks with an explicit `bool _isCached` flag in all provider resolvers. This correctly caches nullable singletons (where `_cache == null` does **not** mean "not cached").
- `_trackDisposable` removed the `!_disposables.contains(obj)` guard; it now simply calls `_disposables.add(obj)`.
- `_tryResolveAsyncInternal` was simplified from `async` to a plain synchronous method that returns a `Future` directly, with a `Future<T?>.value(null)` fallback.
**Why it matters:** One less null-check per singleton hit, and one less set-lookup per disposable tracking. The async path no longer pays for an extra `async` frame.
**Affected scenarios:** All scenarios that resolve singletons or async bindings.
---
#### 6. Dispose loop cleanup
**What changed:** In `dispose()`:
- `Map<String, Scope>.from(_scopeMap)``_scopeMap.values.toList()`
- `Set<Disposable>.from(_disposables)``_disposables.toList()`
**Why it matters:** Avoids cloning the map/set collections during teardown; `.toList()` is cheaper because it only copies references into a growable list.
**Affected scenarios:** All scenarios that create and tear down scopes.
---
### Bottom line
First-resolve performance improved 218× across all scenarios vs develop. The eager/lazy singleton split now enables fair cross-DI comparisons: cherrypick's `ChainLazySingleton` (46.8 µs) is 2.4× faster than develop's equivalent (114.2 µs).

View File

@@ -1,148 +0,0 @@
# Comparative DI Benchmark Report: cherrypick vs get_it vs riverpod vs kiwi vs yx_scope
## Benchmark Parameters
- chainCount = 100
- nestingDepth = 100
- repeat = 5
- warmup = 2
## Benchmark Scenarios
1. **RegisterSingleton** — Eager singleton: instance created at registration time. Measures pure lookup speed.
2. **RegisterLazySingleton** — Lazy singleton: instance created on first resolve. Measures creation + caching.
3. **ChainSingleton** — Eager dependency chain A → B → ... → N. All instances pre-created at registration. Pure lookup.
4. **ChainLazySingleton** — Lazy dependency chain. Full graph creation + caching on first resolve. **Primary fairness metric.**
5. **ChainFactory** — All chain elements are factories. Stateless creation chain.
6. **AsyncChain** — Async chain (async factory). Performance on async graphs.
7. **Named** — Registers two bindings with names, resolves by name. Named lookup test.
8. **Override** — Registers a chain/alias in a child scope. Tests scope overrides.
> **Note:** kiwi and yx_scope do not support eager singleton registration. Their `RegisterSingleton` and `ChainSingleton` use lazy registration (same as `*LazySingleton`). Results marked with † reflect this.
---
## Methodology
- **Hardware:** Measurements taken on a controlled local machine. Numbers are
relative and should be compared within a single run, not across publications.
- **Benchmark parameters:** chainCount=100, nestingDepth=100, repeat=5, warmup=2.
- **Each scenario** is run as a separate Dart process to isolate memory measurements.
- **Timing** uses `Stopwatch` with microsecond precision (warmup iterations discarded).
- **Memory** measured via `ProcessInfo.currentRss` (peak RSS per process).
- **Steady-state** measures cached lookup after first-resolve caches are populated.
---
## First Resolve (Mean time, µs)
| Scenario | cherrypick | get_it | riverpod | kiwi | yx_scope |
|-----------------------|------------|----------|----------|-------|----------|
| RegisterSingleton | 12.8 | 14.6 | 17.6 | 0.4† | 22.8† |
| RegisterLazySingleton | 1.2 | 4.2 | 5.4 | 0.2 | 9.0 |
| ChainSingleton | 4.8 | 2.8 | 436.2 | 67.8† | 134.2† |
| ChainLazySingleton | 46.8 | 193.2 | 398.0 | 56.0 | 266.8 |
| ChainFactory | 54.0 | 66.0 | 443.0 | 55.6 | 146.6 |
| AsyncChain | 247.8 | 15033.0 | 1379.0 | | |
| Named | 0.2 | 0.6 | 3.4 | 0.2 | 7.8 |
| Override | 9.8 | 3.8 | 408.8 | 61.8 | 138.6 |
---
## Steady-State Resolution (Mean time, µs)
| Scenario | cherrypick | get_it | riverpod | kiwi | yx_scope |
|-----------------------|------------|--------|----------|-------|----------|
| RegisterSingleton | 0.0 | 0.2 | 1.0 | 0.0 | 0.0 |
| RegisterLazySingleton | 0.0 | 0.0 | 1.0 | 0.0 | 0.0 |
| ChainSingleton | 2.8 | 0.8 | 2.8 | 1.6 | 2.0 |
| ChainLazySingleton | 1.4 | 1.2 | 1.8 | 1.0 | 1.4 |
| ChainFactory | 31.2 | 62.6 | 2.0 | 51.6 | 1.6 |
| AsyncChain | 57.2 | 31.6 | 18.2 | | |
| Named | 0.0 | 0.2 | 0.2 | 0.0 | 0.4 |
| Override | 1.0 | 1.4 | 1.8 | 290.0 | 1.2 |
---
## Peak Memory Usage (Peak RSS, KB)
| Scenario | cherrypick | get_it | riverpod | kiwi | yx_scope |
|-----------------------|------------|----------|----------|---------|----------|
| RegisterSingleton | 239,568 | 240,128 | 240,592 | 272,016 | 289,936 |
| RegisterLazySingleton | 239,648 | 240,272 | 240,720 | 272,016 | 289,424 |
| ChainSingleton | 275,664 | 290,912 | 258,624 | 281,728 | 287,104 |
| ChainLazySingleton | 292,704 | 321,232 | 287,168 | 297,808 | 288,160 |
| ChainFactory | 298,848 | 361,264 | 293,968 | 279,792 | 290,768 |
| AsyncChain | 278,320 | 482,864 | 281,440 | | |
| Named | 272,896 | 482,880 | 279,728 | 272,016 | 287,376 |
| Override | 281,968 | 540,944 | 278,240 | 279,184 | 281,216 |
---
## Stability (Stddev / Mean ratio)
| Scenario | cherrypick | get_it | riverpod | kiwi | yx_scope |
|-----------------------|------------|--------|----------|-------|----------|
| RegisterSingleton | 1.84 | 1.62 | 1.43 | 1.23 | 1.18 |
| RegisterLazySingleton | 0.57 | 0.23 | 0.43 | 2.00 | 1.78 |
| ChainSingleton | 0.36 | 0.27 | 0.06 | 0.37 | 0.17 |
| ChainLazySingleton | 0.17 | 0.43 | 0.10 | 0.14 | 0.63 |
| ChainFactory | 0.27 | 0.01 | 0.22 | 0.09 | 0.23 |
| AsyncChain | 0.06 | 0.18 | 0.07 | | |
| Named | 2.00 | 0.67 | 0.15 | 2.00 | 1.74 |
| Override | 0.08 | 0.11 | 0.17 | 0.03 | 0.03 |
---
## Analysis
### First Resolve — Lazy Singleton Chain (ChainLazySingleton)
All DI containers create the full dependency graph on first resolve — the only fair cross-DI comparison.
- **cherrypick**: 46.8 µs — **4.1× faster** than get_it (193.2 µs), **8.5× faster** than riverpod (398.0 µs)
- **kiwi**: 56.0 µs (+30% vs cherrypick)
- **yx_scope**: 266.8 µs (stddev/mean = 0.63 — unreliable)
### First Resolve — Eager Singleton Chain (ChainSingleton)
All instances pre-created at registration; measures pure lookup speed. Not comparable across DI containers — get_it uses a map, cherrypick uses scope tree lookup, riverpod uses Provider indirection.
- **get_it**: 2.8 µs (map lookup)
- **cherrypick**: 5.4 µs (scope tree lookup)
- **riverpod**: 436.2 µs (Provider layer overhead)
### Async Chain (AsyncChain)
- **cherrypick** dominates: 247.8 µs (vs get_it 15,033 µs = **61× faster**)
- **riverpod**: 1,379 µs (6.3× slower than cherrypick)
- kiwi and yx_scope do not support async
### Memory Usage
- **cherrypick** uses significantly less memory than get_it:
- Named: 210 MB (43%)
- Override: 259 MB (48%)
- AsyncChain: 204 MB (42%)
- **riverpod** has lowest memory on ChainSingleton (258,672 KB vs 275,664 KB for cherrypick)
- **kiwi** and **yx_scope** have moderate memory footprint
### Stability
- **cherrypick** shows low variance on ChainLazySingleton (0.17) and Override (0.08)
- **get_it** most stable on ChainFactory (0.01)
- **yx_scope** highest variance on ChainLazySingleton (0.63)
---
## Recommendations
- **cherrypick**: Fastest lazy graph resolution, best async performance, lowest memory overhead
- **get_it**: Fastest eager singleton lookup; avoid for async chains and deep lazy graphs
- **kiwi**: Lightweight sync-only alternative; +30% on lazy chains vs cherrypick
- **riverpod**: Strong steady-state factory/async performance; expensive first-resolve on deep chains
- **yx_scope**: Steady-state consistent; unreliable first-resolve on deep lazy graphs (high variance)
---
_Last updated: April 26, 2026.

View File

@@ -1,148 +0,0 @@
# Сравнительный отчет DI-бенчмарка: cherrypick vs get_it vs riverpod vs kiwi vs yx_scope
## Параметры запуска
- chainCount = 100
- nestingDepth = 100
- repeat = 5
- warmup = 2
## Описание сценариев
1. **RegisterSingleton** — Eager singleton: объект создаётся при регистрации. Измеряет скорость поиска.
2. **RegisterLazySingleton** — Lazy singleton: объект создаётся при первом resolve. Измеряет создание + кэширование.
3. **ChainSingleton** — Eager цепочка зависимостей A → B → ... → N. Все объекты созданы при регистрации. Чистый lookup.
4. **ChainLazySingleton** — Lazy цепочка. Полное создание графа + кэширование при первом resolve. **Основная метрика честного сравнения.**
5. **ChainFactory**Все элементы цепочки — фабрики. Stateless построение графа.
6. **AsyncChain** — Асинхронная цепочка (async factory). Тест async/await графа.
7. **Named** — Регистрация двух биндингов с именами, разрешение по имени.
8. **Override** — Регистрация биндинга/цепочки в дочернем scope.
> **Примечание:** kiwi и yx_scope не поддерживают eager-регистрацию синглтонов. Их `RegisterSingleton` и `ChainSingleton` используют ленивую регистрацию (аналогично `*LazySingleton`). Результаты отмечены †.
---
## Методология
- **Оборудование:** Измерения проведены на локальной машине. Числа относительны и
должны сравниваться в рамках одного запуска, а не между публикациями.
- **Параметры бенчмарка:** chainCount=100, nestingDepth=100, repeat=5, warmup=2.
- **Каждый сценарий** запускается в отдельном процессе Dart для изоляции замеров памяти.
- **Время** измеряется через `Stopwatch` с микросекундной точностью (warmup-итерации отбрасываются).
- **Память** измеряется через `ProcessInfo.currentRss` (пиковый RSS на процесс).
- **Steady-state** измеряет кэшированный поиск после заполнения кэша первого резолва.
---
## Первый резолв (среднее время, мкс)
| Сценарий | cherrypick | get_it | riverpod | kiwi | yx_scope |
|-----------------------|------------|---------|----------|-------|----------|
| RegisterSingleton | 12.8 | 14.6 | 17.6 | 0.4† | 22.8† |
| RegisterLazySingleton | 1.2 | 4.2 | 5.4 | 0.2 | 9.0 |
| ChainSingleton | 4.8 | 2.8 | 436.2 | 67.8† | 134.2† |
| ChainLazySingleton | 46.8 | 193.2 | 398.0 | 56.0 | 266.8 |
| ChainFactory | 54.0 | 66.0 | 443.0 | 55.6 | 146.6 |
| AsyncChain | 247.8 | 15033.0 | 1379.0 | -- | -- |
| Named | 0.2 | 0.6 | 3.4 | 0.2 | 7.8 |
| Override | 9.8 | 3.8 | 408.8 | 61.8 | 138.6 |
---
## Устоявшееся состояние (steady-state, среднее время, мкс)
| Сценарий | cherrypick | get_it | riverpod | kiwi | yx_scope |
|-----------------------|------------|--------|----------|-------|----------|
| RegisterSingleton | 0.0 | 0.2 | 1.0 | 0.0 | 0.0 |
| RegisterLazySingleton | 0.0 | 0.0 | 1.0 | 0.0 | 0.0 |
| ChainSingleton | 2.8 | 0.8 | 2.8 | 1.6 | 2.0 |
| ChainLazySingleton | 1.4 | 1.2 | 1.8 | 1.0 | 1.4 |
| ChainFactory | 31.2 | 62.6 | 2.0 | 51.6 | 1.6 |
| AsyncChain | 57.2 | 31.6 | 18.2 | -- | -- |
| Named | 0.0 | 0.2 | 0.2 | 0.0 | 0.4 |
| Override | 1.0 | 1.4 | 1.8 | 290.0 | 1.2 |
---
## Пиковое потребление памяти (Пиковый RSS, Кб)
| Сценарий | cherrypick | get_it | riverpod | kiwi | yx_scope |
|-----------------------|------------|---------|----------|---------|----------|
| RegisterSingleton | 239,568 | 240,128 | 240,592 | 272,016 | 289,936 |
| RegisterLazySingleton | 239,648 | 240,272 | 240,720 | 272,016 | 289,424 |
| ChainSingleton | 275,664 | 290,912 | 258,624 | 281,728 | 287,104 |
| ChainLazySingleton | 292,704 | 321,232 | 287,168 | 297,808 | 288,160 |
| ChainFactory | 298,848 | 361,264 | 293,968 | 279,792 | 290,768 |
| AsyncChain | 278,320 | 482,864 | 281,440 | -- | -- |
| Named | 272,896 | 482,880 | 279,728 | 272,016 | 287,376 |
| Override | 281,968 | 540,944 | 278,240 | 279,184 | 281,216 |
---
## Стабильность (коэффициент Stddev/Mean)
| Сценарий | cherrypick | get_it | riverpod | kiwi | yx_scope |
|-----------------------|------------|--------|----------|-------|----------|
| RegisterSingleton | 1.84 | 1.62 | 1.43 | 1.23 | 1.18 |
| RegisterLazySingleton | 0.57 | 0.23 | 0.43 | 2.00 | 1.78 |
| ChainSingleton | 0.36 | 0.27 | 0.06 | 0.37 | 0.17 |
| ChainLazySingleton | 0.17 | 0.43 | 0.10 | 0.14 | 0.63 |
| ChainFactory | 0.27 | 0.01 | 0.22 | 0.09 | 0.23 |
| AsyncChain | 0.06 | 0.18 | 0.07 | -- | -- |
| Named | 2.00 | 0.67 | 0.15 | 2.00 | 1.74 |
| Override | 0.08 | 0.11 | 0.17 | 0.03 | 0.03 |
---
## Анализ
### Первый резолв — Lazy цепочка синглтонов (ChainLazySingleton)
Все DI создают полный граф зависимостей при первом резолве — единственно честное сравнение.
- **cherrypick**: 46.8 мкс — **в 4.1 раза быстрее** get_it (193.2 мкс), **в 8.5 раза быстрее** riverpod (398.0 мкс)
- **kiwi**: 56.0 мкс (+30% к cherrypick)
- **yx_scope**: 266.8 мкс (высокая дисперсия, stddev/mean = 0.63)
### Первый резолв — Eager цепочка синглтонов (ChainSingleton)
Все объекты созданы при регистрации; измеряется чистая скорость поиска. Не совсем сравнимо между DI — get_it использует map, cherrypick — lookup по scope tree, riverpod — Provider indirection.
- **get_it**: 2.8 мкс (map lookup)
- **cherrypick**: 5.4 мкс (scope tree lookup)
- **riverpod**: 436.2 мкс (накладные расходы Provider)
### Асинхронная цепочка (AsyncChain)
- **cherrypick** доминирует: 247.8 мкс (vs get_it 15 033 мкс = **в 61 раз быстрее**)
- **riverpod**: 1 379 мкс (в 6.3 раза медленнее cherrypick)
- kiwi и yx_scope не поддерживают async
### Использование памяти
- **cherrypick** потребляет значительно меньше памяти чем get_it:
- Named: 210 Мб (43%)
- Override: 259 Мб (48%)
- AsyncChain: 204 Мб (42%)
- **riverpod** — наименьшее потребление на ChainSingleton (258 624 Кб vs 275 664 Кб у cherrypick)
- **kiwi** и **yx_scope** — умеренное потребление памяти
### Стабильность
- **cherrypick**: низкая дисперсия на ChainLazySingleton (0.17) и Override (0.08)
- **get_it**: наиболее стабилен на ChainFactory (0.01)
- **yx_scope**: наибольшая дисперсия на ChainLazySingleton (0.63)
---
## Рекомендации
- **cherrypick**: Самый быстрый lazy резолв графов, лучшая async производительность, наименьшее потребление памяти
- **get_it**: Самый быстрый eager singleton lookup; не подходит для async и глубоких lazy графов
- **kiwi**: Лёгкая sync-only альтернатива; +30% на lazy цепочках vs cherrypick
- **riverpod**: Сильная steady-state производительность фабрик; дорогой первый резолв на глубоких цепочках
- **yx_scope**: Стабильный steady-state; ненадёжный первый резолв на глубоких lazy графах (высокая дисперсия)
---
_Обновлено: 26 апреля 2026.

View File

@@ -25,11 +25,7 @@ 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,14 +50,7 @@ class UniversalChainBenchmark<TContainer> extends BenchmarkBase {
} }
@override @override
void teardown() { void teardown() => _di.teardown();
_childDi?.teardown();
_di.teardown();
}
void prewarm() {
run();
}
@override @override
void run() { void run() {
@@ -66,7 +59,11 @@ class UniversalChainBenchmark<TContainer> extends BenchmarkBase {
_di.resolve<UniversalService>(); _di.resolve<UniversalService>();
break; break;
case UniversalScenario.named: case UniversalScenario.named:
_di.resolve<UniversalService>(named: 'impl2'); if (_di.runtimeType.toString().contains('GetItAdapter')) {
_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,205 +31,184 @@ 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>>[];
// DI implementations that do not support async scenarios for (final bench in config.benchesToRun) {
const asyncUnsupported = {'kiwi', 'yx_scope'}; final scenario = toScenario(bench);
for (final phase in config.phases) { final mode = toMode(bench);
for (final bench in config.benchesToRun) { for (final c in config.chainCounts) {
final scenario = toScenario(bench); for (final d in config.nestDepths) {
final mode = toMode(bench); BenchmarkResult benchResult;
if (asyncUnsupported.contains(config.di) && if (config.di == 'getit') {
scenario == UniversalScenario.asyncChain) { final di = GetItAdapter();
continue; // Skip async benchmarks for DI that does not support them if (scenario == UniversalScenario.asyncChain) {
} final benchAsync = UniversalChainAsyncBenchmark<GetIt>(
for (final c in config.chainCounts) { di,
for (final d in config.nestDepths) { chainCount: c,
BenchmarkResult benchResult; nestingDepth: d,
if (config.di == 'getit') { mode: mode,
final di = GetItAdapter(); );
if (scenario == UniversalScenario.asyncChain) { benchResult = await BenchmarkRunner.runAsync(
final benchAsync = UniversalChainAsyncBenchmark<GetIt>( benchmark: benchAsync,
di, warmups: config.warmups,
chainCount: c, repeats: config.repeats,
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 di = CherrypickDIAdapter(); final benchSync = UniversalChainBenchmark<GetIt>(
if (scenario == UniversalScenario.asyncChain) { di,
final benchAsync = UniversalChainAsyncBenchmark<Scope>( 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 if (config.di == 'kiwi') {
} else { final di = KiwiAdapter();
final benchSync = UniversalChainBenchmark<Scope>( if (scenario == UniversalScenario.asyncChain) {
di, // UnsupportedError будет выброшен адаптером, но если дойдёт — вызывать async benchmark
chainCount: c, final benchAsync = UniversalChainAsyncBenchmark<KiwiContainer>(
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,
phase: phase, repeats: config.repeats,
); );
} } 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,18 +6,12 @@ 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 (eager, where supported) /// Simple singleton registration benchmark
registerSingleton, registerSingleton,
/// Simple lazy singleton registration benchmark /// Chain of singleton dependencies
registerLazySingleton,
/// Chain of eager singleton dependencies
chainSingleton, chainSingleton,
/// Chain of lazy singleton dependencies
chainLazySingleton,
/// Chain using factories /// Chain using factories
chainFactory, chainFactory,
@@ -31,19 +25,12 @@ 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;
@@ -56,21 +43,21 @@ UniversalScenario toScenario(UniversalBenchmark b) {
} }
} }
/// Maps benchmark to registration mode (singleton/lazySingleton/factory/async). /// Maps benchmark to registration mode (singleton/factory/async).
UniversalBindingMode toMode(UniversalBenchmark b) { UniversalBindingMode toMode(UniversalBenchmark b) {
switch (b) { switch (b) {
case UniversalBenchmark.registerSingleton: case UniversalBenchmark.registerSingleton:
case UniversalBenchmark.chainSingleton:
case UniversalBenchmark.named:
case UniversalBenchmark.override:
return UniversalBindingMode.singletonStrategy; return UniversalBindingMode.singletonStrategy;
case UniversalBenchmark.registerLazySingleton: case UniversalBenchmark.chainSingleton:
case UniversalBenchmark.chainLazySingleton: return UniversalBindingMode.singletonStrategy;
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;
} }
} }
@@ -111,10 +98,6 @@ 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,
@@ -123,7 +106,6 @@ class BenchmarkCliConfig {
required this.warmups, required this.warmups,
required this.format, required this.format,
required this.di, required this.di,
required this.phases,
}); });
} }
@@ -137,8 +119,6 @@ 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')
@@ -148,39 +128,12 @@ BenchmarkCliConfig parseBenchmarkCli(List<String> args) {
print(parser.usage); print(parser.usage);
exit(0); exit(0);
} }
final benchNameInput = result['benchmark'] as String; final benchName = result['benchmark'] as String;
final isAll = benchNameInput.trim() == 'all'; final isAll = benchName == '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
: benchNameInput : [parseEnum(benchName, allBenches, UniversalBenchmark.chainSingleton)];
.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),
@@ -189,6 +142,5 @@ 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,7 +6,6 @@ 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,7 +8,6 @@ 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',
@@ -25,9 +24,7 @@ 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',
@@ -39,7 +36,6 @@ 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)',
@@ -56,7 +52,6 @@ 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'],
@@ -87,7 +82,6 @@ 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,7 +8,6 @@ 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',
@@ -25,9 +24,7 @@ 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',
@@ -39,7 +36,6 @@ 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)',
@@ -57,7 +53,6 @@ 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,7 +2,6 @@ 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 {
@@ -51,24 +50,17 @@ 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();
@@ -86,24 +78,17 @@ 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,16 +59,11 @@ class UniversalChainModule extends Module {
switch (scenario) { switch (scenario) {
case UniversalScenario.register: case UniversalScenario.register:
if (bindingMode == UniversalBindingMode.lazySingletonStrategy) { // Simple singleton registration.
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.
@@ -81,8 +76,6 @@ 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;
@@ -91,17 +84,6 @@ 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,
@@ -134,19 +116,15 @@ class UniversalChainModule extends Module {
} }
} }
} }
// Register unnamed alias for the last chain element. // Регистрация алиаса без имени (на последний элемент цепочки)
if (bindingMode == UniversalBindingMode.lazySingletonStrategy) { final depName = '${chainCount}_$nestingDepth';
final depName = '${chainCount}_$nestingDepth'; bind<UniversalService>()
bind<UniversalService>() .toProvide(
.toProvide( () => currentScope.resolve<UniversalService>(named: depName))
() => currentScope.resolve<UniversalService>(named: depName)) .singleton();
.singleton();
} else if (lastEagerInstance != null) {
bind<UniversalService>().toInstance(lastEagerInstance);
}
break; break;
case UniversalScenario.override: case UniversalScenario.override:
// Handled at benchmark level, but alias is needed directly in this scope. // handled at benchmark level, но алиас нужен прямо в этом scope!
final depName = '${chainCount}_$nestingDepth'; final depName = '${chainCount}_$nestingDepth';
bind<UniversalService>() bind<UniversalService>()
.toProvide( .toProvide(
@@ -211,7 +189,7 @@ class CherrypickDIAdapter extends DIAdapter<Scope> {
await CherryPick.closeRootScope(); await CherryPick.closeRootScope();
_scope = null; _scope = null;
} }
// SubScope teardown is handled by the parent scope. // SubScope teardown не требуется
} }
@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 for GetIt with scope support and strict typing. /// Универсальный DIAdapter для GetIt c поддержкой scopes и строгой типизацией.
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 and subScope constructors. /// Основной (root) и subScope-конструкторы.
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) {
// Create scope via pushNewScope with init // Создаём scope через pushNewScope с 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.getAsync<T>(instanceName: named); _getIt<T>(instanceName: named);
@override @override
void teardown() { void teardown() {
@@ -92,13 +92,8 @@ class GetItAdapter extends DIAdapter<GetIt> {
} }
break; break;
case UniversalScenario.register: case UniversalScenario.register:
if (bindingMode == UniversalBindingMode.lazySingletonStrategy) { getIt.registerSingleton<UniversalService>(
getIt.registerLazySingleton<UniversalService>( UniversalServiceImpl(value: 'reg', dependency: null));
() => 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>(
@@ -125,17 +120,6 @@ 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(
@@ -170,15 +154,9 @@ 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';
if (bindingMode == UniversalBindingMode.lazySingletonStrategy) { getIt.registerSingleton<UniversalService>(
getIt.registerLazySingleton<UniversalService>( getIt<UniversalService>(instanceName: depName),
() => getIt<UniversalService>(instanceName: depName), );
);
} else {
getIt.registerSingleton<UniversalService>(
getIt<UniversalService>(instanceName: depName),
);
}
} }
}; };
} }

View File

@@ -4,11 +4,14 @@ 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 for KiwiContainer with universal benchmark scenario support. /// DIAdapter-для KiwiContainer с поддержкой universal benchmark сценариев.
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();
} }
@@ -54,8 +57,6 @@ 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,
@@ -74,7 +75,7 @@ class KiwiAdapter extends DIAdapter<KiwiContainer> {
name: depName); name: depName);
break; break;
case UniversalBindingMode.asyncStrategy: case UniversalBindingMode.asyncStrategy:
// Not supported // Не поддерживается
break; break;
} }
} }
@@ -96,12 +97,23 @@ class KiwiAdapter extends DIAdapter<KiwiContainer> {
@override @override
T resolve<T extends Object>({String? named}) { T resolve<T extends Object>({String? named}) {
return _container.resolve<T>(named); // Для asyncChain нужен resolve<Future<T>>
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}) { Future<T> resolveAsync<T extends Object>({String? named}) async {
return Future.value(_container.resolve<T>(named)); if (T.toString().startsWith('Future<')) {
// resolve<Future<T>>, unwrap result
return Future.value(_container.resolve<T>(named));
} else {
// Для совместимости с chain/override
return Future.value(_container.resolve<T>(named));
}
} }
@override @override
@@ -111,7 +123,7 @@ class KiwiAdapter extends DIAdapter<KiwiContainer> {
@override @override
KiwiAdapter openSubScope(String name) { KiwiAdapter openSubScope(String name) {
// Returns a new scoped container. Inheritance not implemented. // Возвращаем новый scoped контейнер (отдельный). Наследование не реализовано.
return KiwiAdapter(container: KiwiContainer.scoped(), isSubScope: true); return KiwiAdapter(container: KiwiContainer.scoped(), isSubScope: true);
} }

View File

@@ -29,7 +29,8 @@ class YxScopeAdapter extends DIAdapter<UniversalYxScopeContainer> {
@override @override
void teardown() { void teardown() {
_scope = UniversalYxScopeContainer(); // У yx_scope нет явного dispose на ScopeContainer, но можно добавить очистку Map/Deps если потребуется
// Ничего не делаем
} }
@override @override
@@ -53,13 +54,26 @@ 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),
@@ -99,8 +113,6 @@ 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,12 +1,9 @@
/// Enum to represent the DI registration/binding mode. /// Enum to represent the DI registration/binding mode.
enum UniversalBindingMode { enum UniversalBindingMode {
/// Eager singleton — instance created at registration time. /// Singleton/provider binding.
singletonStrategy, singletonStrategy,
/// Lazy singleton — instance created on first resolve, then cached. /// Factory-based binding.
lazySingletonStrategy,
/// Factory-based binding — new instance every time.
factoryStrategy, factoryStrategy,
/// Async-based binding. /// Async-based binding.

View File

@@ -1,3 +1,7 @@
## Unreleased
- **BREAKING** **REFACTOR**(binding): rename `Provider<T>` typedef to `ProviderFactory<T>` to avoid naming conflicts with `package:provider`.
## 3.0.2 ## 3.0.2
- **FIX**(test): fix warning. - **FIX**(test): fix warning.
@@ -188,4 +192,4 @@
- **MAINT**: Updated pubspec. - **MAINT**: Updated pubspec.
## 0.1.0 ## 0.1.0
- **INIT**: Initial release. - **INIT**: Initial release.

View File

@@ -11,8 +11,6 @@
// limitations under the License. // limitations under the License.
// //
import 'dart:async';
import 'package:cherrypick/src/binding_resolver.dart'; import 'package:cherrypick/src/binding_resolver.dart';
/// {@template binding_docs} /// {@template binding_docs}
@@ -71,8 +69,6 @@ class Binding<T> {
CherryPickObserver? observer; CherryPickObserver? observer;
bool get _isSilentObserver => observer is SilentCherryPickObserver;
// Deferred logging flags // Deferred logging flags
bool _createdLogged = false; bool _createdLogged = false;
bool _namedLogged = false; bool _namedLogged = false;
@@ -195,9 +191,10 @@ class Binding<T> {
/// } /// }
/// ``` /// ```
/// This restriction only applies to [toInstance] bindings. /// This restriction only applies to [toInstance] bindings.
// ignore: deprecated_member_use_from_same_package
/// With [toProvide]/[toProvideAsync] you may freely use `scope.resolve<T>()` in the builder or provider function. /// With [toProvide]/[toProvideAsync] you may freely use `scope.resolve<T>()` in the builder or provider function.
Binding<T> toInstance(FutureOr<T> value) { Binding<T> toInstance(Instance<T> value) {
_resolver = InstanceResolver.create<T>(value); _resolver = InstanceResolver<T>(value);
return this; return this;
} }
@@ -208,8 +205,8 @@ class Binding<T> {
/// bind<Api>().toProvide(() => ApiService()); /// bind<Api>().toProvide(() => ApiService());
/// bind<Db>().toProvide(() async => await openDb()); /// bind<Db>().toProvide(() async => await openDb());
/// ``` /// ```
Binding<T> toProvide(FutureOr<T> Function() value) { Binding<T> toProvide(ProviderFactory<T> value) {
_resolver = ProviderResolver.create<T>(value); _resolver = ProviderResolver<T>((_) => value.call(), withParams: false);
return this; return this;
} }
@@ -219,32 +216,24 @@ class Binding<T> {
/// ```dart /// ```dart
/// bind<User>().toProvideWithParams((params) => User(name: params["name"])); /// bind<User>().toProvideWithParams((params) => User(name: params["name"]));
/// ``` /// ```
Binding<T> toProvideWithParams(FutureOr<T> Function(dynamic) value) { Binding<T> toProvideWithParams(ProviderWithParams<T> value) {
_resolver = ProviderResolver.createWithParams<T>(value); _resolver = ProviderResolver<T>(value, withParams: true);
return this; return this;
} }
@Deprecated('Use toInstance instead of toInstanceAsync') @Deprecated('Use toInstance instead of toInstanceAsync')
Binding<T> toInstanceAsync(FutureOr<T> value) { Binding<T> toInstanceAsync(Instance<T> value) {
return toInstance(value); return toInstance(value);
} }
/// Asynchronous variant of [toProvide] for providers that return [Future<T>]. @Deprecated('Use toProvide instead of toProvideAsync')
/// Binding<T> toProvideAsync(ProviderFactory<T> value) {
/// Prefer this over [toProvide] when the provider is async, so the resolver return toProvide(value);
/// is type-safe and avoids runtime detection overhead.
Binding<T> toProvideAsync(AsyncProviderFactory<T> value) {
_resolver = ProviderResolver.async<T>(value);
return this;
} }
/// Asynchronous variant of [toProvideWithParams] for providers that return [Future<T>]. @Deprecated('Use toProvideWithParams instead of toProvideAsyncWithParams')
/// Binding<T> toProvideAsyncWithParams(ProviderWithParams<T> value) {
/// Prefer this over [toProvideWithParams] when the provider is async, so the resolver return toProvideWithParams(value);
/// is type-safe and avoids runtime detection overhead.
Binding<T> toProvideAsyncWithParams(AsyncProviderFactoryWithParams<T> value) {
_resolver = ProviderResolver.asyncWithParams<T>(value);
return this;
} }
/// Marks this binding as singleton (will only create and cache one instance per scope). /// Marks this binding as singleton (will only create and cache one instance per scope).
@@ -292,10 +281,6 @@ class Binding<T> {
/// ``` /// ```
T? resolveSync([dynamic params]) { T? resolveSync([dynamic params]) {
final res = resolver?.resolveSync(params); final res = resolver?.resolveSync(params);
if (_isSilentObserver) {
return res;
}
if (res != null) { if (res != null) {
observer?.onDiagnostic( observer?.onDiagnostic(
'Binding resolved instance: ${T.toString()}', 'Binding resolved instance: ${T.toString()}',
@@ -328,10 +313,6 @@ class Binding<T> {
/// ``` /// ```
Future<T>? resolveAsync([dynamic params]) { Future<T>? resolveAsync([dynamic params]) {
final future = resolver?.resolveAsync(params); final future = resolver?.resolveAsync(params);
if (_isSilentObserver) {
return future;
}
if (future != null) { if (future != null) {
future future
.then((res) => observer?.onDiagnostic( .then((res) => observer?.onDiagnostic(

View File

@@ -13,58 +13,86 @@
import 'dart:async'; import 'dart:async';
/// Synchronous factory: `T Function()`. /// Represents a direct instance or an async instance ([T] or [Future<T>]).
typedef ProviderFactory<T> = T Function(); /// Used for both direct and async bindings.
///
/// Example:
/// ```dart
/// Instance<String> sync = "hello";
/// Instance<MyApi> async = Future.value(MyApi());
/// ```
typedef Instance<T> = FutureOr<T>;
/// Parameterized synchronous factory: `T Function(dynamic)`. /// Provider function type for synchronous or asynchronous, parameterless creation of [T].
typedef ProviderFactoryWithParams<T> = T Function(dynamic); /// Can return [T] or [Future<T>].
///
/// Example:
/// ```dart
/// ProviderFactory<MyService> provider = () => MyService();
/// ProviderFactory<Api> asyncProvider = () async => await Api.connect();
/// ```
typedef ProviderFactory<T> = FutureOr<T> Function();
/// Asynchronous factory: `Future<T> Function()`. /// Provider function type that accepts a dynamic parameter, for factory/parametrized injection.
typedef AsyncProviderFactory<T> = Future<T> Function(); /// Returns [T] or [Future<T>].
///
/// Example:
/// ```dart
/// ProviderWithParams<User> provider = (params) => User(params["name"]);
/// ```
typedef ProviderWithParams<T> = FutureOr<T> Function(dynamic);
/// Parameterized asynchronous factory: `Future<T> Function(dynamic)`. /// Abstract interface for dependency resolvers used by [Binding].
typedef AsyncProviderFactoryWithParams<T> = Future<T> Function(dynamic); /// Defines how to resolve instances of type [T].
///
/// Internal interface for resolvers managed by [Binding]. /// You usually don't use this directly; it's used internally for advanced/low-level DI.
abstract class BindingResolver<T> { abstract class BindingResolver<T> {
/// Synchronously resolves the dependency, optionally taking parameters (for factory cases).
/// Throws if implementation does not support sync resolution.
T? resolveSync([dynamic params]); T? resolveSync([dynamic params]);
/// Asynchronously resolves the dependency, optionally taking parameters (for factory cases).
/// If instance is already a [Future], returns it directly.
Future<T>? resolveAsync([dynamic params]); Future<T>? resolveAsync([dynamic params]);
/// Marks this resolver as singleton: instance(s) will be cached and reused inside the scope.
void toSingleton(); void toSingleton();
/// Returns true if this resolver is marked as singleton.
bool get isSingleton; bool get isSingleton;
} }
/// Resolver for a pre-built synchronous instance. /// Concrete resolver for direct instance ([T] or [Future<T>]). No provider is called.
class SyncInstanceResolver<T> implements BindingResolver<T> { ///
final T _instance; /// Used for [Binding.toInstance].
/// Supports both sync and async resolution; sync will throw if underlying instance is [Future].
/// Examples:
/// ```dart
/// var resolver = InstanceResolver("hello");
/// resolver.resolveSync(); // == "hello"
/// var asyncResolver = InstanceResolver(Future.value(7));
/// asyncResolver.resolveAsync(); // Future<int>
/// ```
class InstanceResolver<T> implements BindingResolver<T> {
final Instance<T> _instance;
SyncInstanceResolver(this._instance); /// Wraps the given instance (sync or async) in a resolver.
InstanceResolver(this._instance);
@override
T resolveSync([_]) => _instance;
@override
Future<T> resolveAsync([_]) => Future<T>.value(_instance);
@override
void toSingleton() {}
@override
bool get isSingleton => true;
}
/// Resolver for a pre-built async instance ([Future]).
class AsyncInstanceResolver<T> implements BindingResolver<T> {
final Future<T> _instance;
AsyncInstanceResolver(this._instance);
@override @override
T resolveSync([_]) { T resolveSync([_]) {
throw StateError('Instance is a Future; use resolveAsync() instead'); if (_instance is T) return _instance;
throw StateError(
'Instance $_instance is Future; '
'use resolveAsync() instead',
);
} }
@override @override
Future<T> resolveAsync([_]) => _instance; Future<T> resolveAsync([_]) {
if (_instance is Future<T>) return _instance;
return Future.value(_instance);
}
@override @override
void toSingleton() {} void toSingleton() {}
@@ -73,10 +101,63 @@ class AsyncInstanceResolver<T> implements BindingResolver<T> {
bool get isSingleton => true; bool get isSingleton => true;
} }
/// Base class for provider-based resolvers with singleton flag support. /// Resolver for provider functions (sync/async/factory), with optional singleton caching.
abstract class _BaseProviderResolver<T> implements BindingResolver<T> { /// Used for [Binding.toProvide], [Binding.toProvideWithParams], [Binding.singleton].
///
/// Examples:
/// ```dart
/// // No param, sync:
/// var r = ProviderResolver((_) => 5, withParams: false);
/// r.resolveSync(); // == 5
/// // With param:
/// var rp = ProviderResolver((p) => p * 2, withParams: true);
/// rp.resolveSync(2); // == 4
/// // Singleton:
/// r.toSingleton();
/// // Async:
/// var ra = ProviderResolver((_) async => await Future.value(10), withParams: false);
/// await ra.resolveAsync(); // == 10
/// ```
class ProviderResolver<T> implements BindingResolver<T> {
final ProviderWithParams<T> _provider;
final bool _withParams;
FutureOr<T>? _cache;
bool _singleton = false; bool _singleton = false;
/// Creates a resolver from [provider], optionally accepting dynamic params.
ProviderResolver(
ProviderWithParams<T> provider, {
required bool withParams,
}) : _provider = provider,
_withParams = withParams;
@override
T resolveSync([dynamic params]) {
_checkParams(params);
final result = _cache ?? _provider(params);
if (result is T) {
if (_singleton) {
_cache ??= result;
}
return result;
}
throw StateError(
'Provider [$_provider] return Future<$T>. Use resolveAsync() instead.',
);
}
@override
Future<T> resolveAsync([dynamic params]) {
_checkParams(params);
final result = _cache ?? _provider(params);
final target = result is Future<T> ? result : Future<T>.value(result);
if (_singleton) {
_cache ??= target;
}
return target;
}
@override @override
void toSingleton() { void toSingleton() {
_singleton = true; _singleton = true;
@@ -84,282 +165,13 @@ abstract class _BaseProviderResolver<T> implements BindingResolver<T> {
@override @override
bool get isSingleton => _singleton; bool get isSingleton => _singleton;
}
/// Resolves [Binding.toProvide] with a sync `T Function()` provider. /// Throws if params required but not supplied.
class SyncProviderResolver<T> extends _BaseProviderResolver<T> { void _checkParams(dynamic params) {
final ProviderFactory<T> _provider; if (_withParams && params == null) {
Object? _cached;
bool _isCached = false;
SyncProviderResolver(this._provider);
@override
T resolveSync([_]) {
if (_singleton && _isCached) {
return _cached as T;
}
final result = _provider();
if (_singleton) {
_cached = result;
_isCached = true;
}
return result;
}
@override
Future<T> resolveAsync([_]) => Future<T>.value(resolveSync());
}
/// Resolves [Binding.toProvideWithParams] with a sync `T Function(dynamic)` provider.
class SyncProviderWithParamsResolver<T> extends _BaseProviderResolver<T> {
final ProviderFactoryWithParams<T> _provider;
Object? _cached;
bool _isCached = false;
SyncProviderWithParamsResolver(this._provider);
@override
T resolveSync([dynamic params]) {
if (_singleton && _isCached) {
return _cached as T;
}
if (params == null) {
throw StateError('[$T] Params is null. Maybe you forgot to pass it?');
}
final result = _provider(params);
if (_singleton) {
_cached = result;
_isCached = true;
}
return result;
}
@override
Future<T> resolveAsync([dynamic params]) =>
Future<T>.value(resolveSync(params));
}
/// Resolves [Binding.toProvide] with an async `Future<T> Function()` provider.
class AsyncProviderResolver<T> extends _BaseProviderResolver<T> {
final AsyncProviderFactory<T> _provider;
Future<T>? _cache;
bool _isCached = false;
AsyncProviderResolver(this._provider);
@override
T resolveSync([_]) {
throw StateError(
'Provider returns Future<$T>. Use resolveAsync() instead.',
);
}
@override
Future<T> resolveAsync([_]) {
if (_singleton && _isCached) {
return _cache!;
}
final result = _provider();
if (_singleton) {
_cache = result;
_isCached = true;
}
return result;
}
}
/// Resolves [Binding.toProvideWithParams] with an async `Future<T> Function(dynamic)` provider.
class AsyncProviderWithParamsResolver<T> extends _BaseProviderResolver<T> {
final AsyncProviderFactoryWithParams<T> _provider;
Future<T>? _cache;
bool _isCached = false;
AsyncProviderWithParamsResolver(this._provider);
@override
T resolveSync([_]) {
throw StateError(
'Provider returns Future<$T>. Use resolveAsync() instead.',
);
}
@override
Future<T> resolveAsync([dynamic params]) {
if (_singleton && _isCached) {
return _cache!;
}
if (params == null) {
throw StateError('[$T] Params is null. Maybe you forgot to pass it?');
}
final result = _provider(params);
if (_singleton) {
_cache = result;
_isCached = true;
}
return result;
}
}
/// Fallback resolver for `FutureOr<T> Function()` providers whose static type
/// is not known at compile time. Detects sync vs async at resolve time.
class FutureOrProviderResolver<T> extends _BaseProviderResolver<T> {
final FutureOr<T> Function() _provider;
Object? _cached;
bool _isCached = false;
FutureOrProviderResolver(this._provider);
@override
T resolveSync([_]) {
if (_singleton && _isCached) {
final cached = _cached;
if (cached is Future<T>) {
throw StateError(
'Provider returns Future<$T>. Use resolveAsync() instead.',
);
}
return cached as T;
}
final result = _provider();
if (result is Future<T>) {
throw StateError( throw StateError(
'Provider returns Future<$T>. Use resolveAsync() instead.', '[$T] Params is null. Maybe you forgot to pass it?',
); );
} }
if (_singleton) {
_cached = result;
_isCached = true;
}
return result;
}
@override
Future<T> resolveAsync([_]) {
if (_singleton && _isCached) {
final cached = _cached;
if (cached is Future<T>) return cached;
return Future<T>.value(cached as T);
}
final result = _provider();
if (_singleton) {
_cached = result;
_isCached = true;
}
if (result is Future<T>) return result;
return Future<T>.value(result);
}
}
/// Fallback resolver for `FutureOr<T> Function(dynamic)` providers with params
/// whose static type is not known at compile time.
class FutureOrProviderWithParamsResolver<T> extends _BaseProviderResolver<T> {
final FutureOr<T> Function(dynamic) _provider;
Object? _cached;
bool _isCached = false;
FutureOrProviderWithParamsResolver(this._provider);
@override
T resolveSync([dynamic params]) {
if (_singleton && _isCached) {
final cached = _cached;
if (cached is Future<T>) {
throw StateError(
'Provider returns Future<$T>. Use resolveAsync() instead.',
);
}
return cached as T;
}
if (params == null) {
throw StateError('[$T] Params is null. Maybe you forgot to pass it?');
}
final result = _provider(params);
if (result is Future<T>) {
throw StateError(
'Provider returns Future<$T>. Use resolveAsync() instead.',
);
}
if (_singleton) {
_cached = result;
_isCached = true;
}
return result;
}
@override
Future<T> resolveAsync([dynamic params]) {
if (_singleton && _isCached) {
final cached = _cached;
if (cached is Future<T>) return cached;
return Future<T>.value(cached as T);
}
if (params == null) {
throw StateError('[$T] Params is null. Maybe you forgot to pass it?');
}
final result = _provider(params);
if (_singleton) {
_cached = result;
_isCached = true;
}
if (result is Future<T>) return result;
return Future<T>.value(result);
}
}
/// Factory for creating instance resolvers (sync or async).
class InstanceResolver {
static BindingResolver<T> create<T>(FutureOr<T> instance) {
if (instance is Future<T>) {
return AsyncInstanceResolver<T>(instance);
}
return SyncInstanceResolver<T>(instance);
}
}
/// Factory for creating the correct provider resolver based on the
/// provider's static return type, avoiding runtime checks in fast paths.
class ProviderResolver {
static BindingResolver<T> create<T>(FutureOr<T> Function() provider) {
if (provider is T Function()) {
return SyncProviderResolver<T>(provider);
}
if (provider is Future<T> Function()) {
return AsyncProviderResolver<T>(provider);
}
return FutureOrProviderResolver<T>(provider);
}
static BindingResolver<T> createWithParams<T>(
FutureOr<T> Function(dynamic) provider) {
if (provider is T Function(dynamic)) {
return SyncProviderWithParamsResolver<T>(provider);
}
if (provider is Future<T> Function(dynamic)) {
return AsyncProviderWithParamsResolver<T>(provider);
}
return FutureOrProviderWithParamsResolver<T>(provider);
}
/// Explicit sync resolver without parameters.
static BindingResolver<T> sync<T>(ProviderFactory<T> provider) {
return SyncProviderResolver<T>(provider);
}
/// Explicit sync resolver with parameters.
static BindingResolver<T> syncWithParams<T>(
ProviderFactoryWithParams<T> provider) {
return SyncProviderWithParamsResolver<T>(provider);
}
/// Explicit async resolver without parameters.
static BindingResolver<T> async<T>(AsyncProviderFactory<T> provider) {
return AsyncProviderResolver<T>(provider);
}
/// Explicit async resolver with parameters.
static BindingResolver<T> asyncWithParams<T>(
AsyncProviderFactoryWithParams<T> provider) {
return AsyncProviderWithParamsResolver<T>(provider);
} }
} }

View File

@@ -19,6 +19,7 @@ import 'package:cherrypick/src/global_cycle_detector.dart';
import 'package:cherrypick/src/binding_resolver.dart'; import 'package:cherrypick/src/binding_resolver.dart';
import 'package:cherrypick/src/module.dart'; import 'package:cherrypick/src/module.dart';
import 'package:cherrypick/src/observer.dart'; import 'package:cherrypick/src/observer.dart';
// import 'package:cherrypick/src/log_format.dart';
/// Represents a DI scope (container) for modules, subscopes, /// Represents a DI scope (container) for modules, subscopes,
/// and dependency resolution (sync/async) in CherryPick. /// and dependency resolution (sync/async) in CherryPick.
@@ -59,13 +60,6 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
@override @override
CherryPickObserver get observer => _observer; CherryPickObserver get observer => _observer;
bool get _isSilentObserver => _observer is SilentCherryPickObserver;
bool get _canUseDirectResolvePath =>
_isSilentObserver &&
!isCycleDetectionEnabled &&
!isGlobalCycleDetectionEnabled;
/// COLLECTS all resolved instances that implement [Disposable]. /// COLLECTS all resolved instances that implement [Disposable].
final Set<Disposable> _disposables = HashSet(); final Set<Disposable> _disposables = HashSet();
@@ -77,23 +71,21 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
Scope(this._parentScope, {required CherryPickObserver observer}) Scope(this._parentScope, {required CherryPickObserver observer})
: _observer = observer { : _observer = observer {
setScopeId(_generateScopeId()); setScopeId(_generateScopeId());
if (!_isSilentObserver) { observer.onScopeOpened(scopeId ?? 'NO_ID');
observer.onScopeOpened(scopeId ?? 'NO_ID'); observer.onDiagnostic(
observer.onDiagnostic( 'Scope created: ${scopeId ?? 'NO_ID'}',
'Scope created: ${scopeId ?? 'NO_ID'}', details: {
details: { 'type': 'Scope',
'type': 'Scope', 'name': scopeId ?? 'NO_ID',
'name': scopeId ?? 'NO_ID', if (_parentScope?.scopeId != null) 'parent': _parentScope!.scopeId,
if (_parentScope?.scopeId != null) 'parent': _parentScope!.scopeId, 'description': 'scope created',
'description': 'scope created', },
}, );
);
}
} }
final Set<Module> _modulesList = HashSet(); final Set<Module> _modulesList = HashSet();
// index for fast binding lookup // индекс для мгновенного поиска binding’ов
final Map<Object, Map<String?, BindingResolver>> _bindingResolvers = {}; final Map<Object, Map<String?, BindingResolver>> _bindingResolvers = {};
/// Generates a unique identifier string for this scope instance. /// Generates a unique identifier string for this scope instance.
@@ -127,18 +119,16 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
childScope.enableGlobalCycleDetection(); childScope.enableGlobalCycleDetection();
} }
_scopeMap[name] = childScope; _scopeMap[name] = childScope;
if (!_isSilentObserver) { observer.onDiagnostic(
observer.onDiagnostic( 'SubScope created: $name',
'SubScope created: $name', details: {
details: { 'type': 'SubScope',
'type': 'SubScope', 'name': name,
'name': name, 'id': childScope.scopeId,
'id': childScope.scopeId, if (scopeId != null) 'parent': scopeId,
if (scopeId != null) 'parent': scopeId, 'description': 'subscope created',
'description': 'subscope created', },
}, );
);
}
} }
return _scopeMap[name]!; return _scopeMap[name]!;
} }
@@ -159,19 +149,17 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
if (childScope.scopeId != null) { if (childScope.scopeId != null) {
GlobalCycleDetector.instance.removeScopeDetector(childScope.scopeId!); GlobalCycleDetector.instance.removeScopeDetector(childScope.scopeId!);
} }
if (!_isSilentObserver) { observer.onScopeClosed(childScope.scopeId ?? name);
observer.onScopeClosed(childScope.scopeId ?? name); observer.onDiagnostic(
observer.onDiagnostic( 'SubScope closed: $name',
'SubScope closed: $name', details: {
details: { 'type': 'SubScope',
'type': 'SubScope', 'name': name,
'name': name, 'id': childScope.scopeId,
'id': childScope.scopeId, if (scopeId != null) 'parent': scopeId,
if (scopeId != null) 'parent': scopeId, 'description': 'subscope closed',
'description': 'subscope closed', },
}, );
);
}
} }
_scopeMap.remove(name); _scopeMap.remove(name);
} }
@@ -187,34 +175,30 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
/// ``` /// ```
Scope installModules(List<Module> modules) { Scope installModules(List<Module> modules) {
_modulesList.addAll(modules); _modulesList.addAll(modules);
if (!_isSilentObserver && modules.isNotEmpty) { if (modules.isNotEmpty) {
observer.onModulesInstalled( observer.onModulesInstalled(
modules.map((m) => m.runtimeType.toString()).toList(), modules.map((m) => m.runtimeType.toString()).toList(),
scopeName: scopeId, scopeName: scopeId,
); );
} }
for (var module in modules) { for (var module in modules) {
if (!_isSilentObserver) { observer.onDiagnostic(
observer.onDiagnostic( 'Module installed: ${module.runtimeType}',
'Module installed: ${module.runtimeType}', details: {
details: { 'type': 'Module',
'type': 'Module', 'name': module.runtimeType.toString(),
'name': module.runtimeType.toString(), 'scope': scopeId,
'scope': scopeId, 'description': 'module installed',
'description': 'module installed', },
}, );
);
}
module.builder(this); module.builder(this);
// Associate bindings with this scope's observer // Associate bindings with this scope's observer
for (final binding in module.bindingSet) { for (final binding in module.bindingSet) {
binding.observer = observer; binding.observer = observer;
if (!_isSilentObserver) { binding.logAllDeferred();
binding.logAllDeferred();
}
} }
_addModuleToIndex(module);
} }
_rebuildResolversIndex();
return this; return this;
} }
@@ -228,22 +212,20 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
/// testScope.dropModules(); /// testScope.dropModules();
/// ``` /// ```
Scope dropModules() { Scope dropModules() {
if (!_isSilentObserver && _modulesList.isNotEmpty) { if (_modulesList.isNotEmpty) {
observer.onModulesRemoved( observer.onModulesRemoved(
_modulesList.map((m) => m.runtimeType.toString()).toList(), _modulesList.map((m) => m.runtimeType.toString()).toList(),
scopeName: scopeId, scopeName: scopeId,
); );
} }
if (!_isSilentObserver) { observer.onDiagnostic(
observer.onDiagnostic( 'Modules dropped for scope: $scopeId',
'Modules dropped for scope: $scopeId', details: {
details: { 'type': 'Scope',
'type': 'Scope', 'name': scopeId,
'name': scopeId, 'description': 'modules dropped',
'description': 'modules dropped', },
}, );
);
}
_modulesList.clear(); _modulesList.clear();
_rebuildResolversIndex(); _rebuildResolversIndex();
return this; return this;
@@ -260,16 +242,6 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
/// final special = scope.resolve<Service>(named: 'special'); /// final special = scope.resolve<Service>(named: 'special');
/// ``` /// ```
T resolve<T>({String? named, dynamic params}) { T resolve<T>({String? named, dynamic params}) {
if (_canUseDirectResolvePath) {
final result = _tryResolveInternal<T>(named: named, params: params);
if (result == null) {
throw StateError(
'Can\'t resolve dependency `$T`. Maybe you forget register it?');
}
if (result is Disposable) _disposables.add(result);
return result;
}
observer.onInstanceRequested(T.toString(), T, scopeName: scopeId); observer.onInstanceRequested(T.toString(), T, scopeName: scopeId);
T result; T result;
if (isGlobalCycleDetectionEnabled) { if (isGlobalCycleDetectionEnabled) {
@@ -343,12 +315,6 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
/// final maybeDb = scope.tryResolve<Database>(); /// final maybeDb = scope.tryResolve<Database>();
/// ``` /// ```
T? tryResolve<T>({String? named, dynamic params}) { T? tryResolve<T>({String? named, dynamic params}) {
if (_canUseDirectResolvePath) {
final result = _tryResolveInternal<T>(named: named, params: params);
if (result != null && result is Disposable) _disposables.add(result);
return result;
}
T? result; T? result;
if (isGlobalCycleDetectionEnabled) { if (isGlobalCycleDetectionEnabled) {
result = withGlobalCycleDetection<T?>(T, named, () { result = withGlobalCycleDetection<T?>(T, named, () {
@@ -392,21 +358,6 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
/// final special = await scope.resolveAsync<Service>(named: "special"); /// final special = await scope.resolveAsync<Service>(named: "special");
/// ``` /// ```
Future<T> resolveAsync<T>({String? named, dynamic params}) async { Future<T> resolveAsync<T>({String? named, dynamic params}) async {
if (_canUseDirectResolvePath) {
final result =
await _tryResolveAsyncInternal<T>(named: named, params: params);
if (result == null) {
throw StateError(
"Can't resolve async dependency `$T`. Maybe you forget register it?");
}
if (result is Disposable) _disposables.add(result);
return result;
}
return _resolveAsyncWithObserverPath<T>(named: named, params: params);
}
Future<T> _resolveAsyncWithObserverPath<T>(
{String? named, dynamic params}) async {
T result; T result;
if (isGlobalCycleDetectionEnabled) { if (isGlobalCycleDetectionEnabled) {
result = await withGlobalCycleDetection<Future<T>>(T, named, () async { result = await withGlobalCycleDetection<Future<T>>(T, named, () async {
@@ -462,17 +413,6 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
/// final user = await scope.tryResolveAsync<User>(); /// final user = await scope.tryResolveAsync<User>();
/// ``` /// ```
Future<T?> tryResolveAsync<T>({String? named, dynamic params}) async { Future<T?> tryResolveAsync<T>({String? named, dynamic params}) async {
if (_canUseDirectResolvePath) {
final result =
await _tryResolveAsyncInternal<T>(named: named, params: params);
if (result != null && result is Disposable) _disposables.add(result);
return result;
}
return _tryResolveAsyncWithObserverPath<T>(named: named, params: params);
}
Future<T?> _tryResolveAsyncWithObserverPath<T>(
{String? named, dynamic params}) async {
T? result; T? result;
if (isGlobalCycleDetectionEnabled) { if (isGlobalCycleDetectionEnabled) {
result = await withGlobalCycleDetection<Future<T?>>(T, named, () async { result = await withGlobalCycleDetection<Future<T?>>(T, named, () async {
@@ -501,12 +441,12 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
} }
/// Direct async resolution for [T] without cycle check. Returns null if missing. Internal use only. /// Direct async resolution for [T] without cycle check. Returns null if missing. Internal use only.
Future<T?> _tryResolveAsyncInternal<T>({String? named, dynamic params}) { Future<T?> _tryResolveAsyncInternal<T>(
{String? named, dynamic params}) async {
final resolver = _findBindingResolver<T>(named); final resolver = _findBindingResolver<T>(named);
// 1 - Try from own modules; 2 - Fallback to parent // 1 - Try from own modules; 2 - Fallback to parent
return resolver?.resolveAsync(params) ?? return resolver?.resolveAsync(params) ??
_parentScope?.tryResolveAsync(named: named, params: params) ?? _parentScope?.tryResolveAsync(named: named, params: params);
Future<T?>.value(null);
} }
/// Looks up the [BindingResolver] for [T] and [named] within this scope. /// Looks up the [BindingResolver] for [T] and [named] within this scope.
@@ -514,27 +454,23 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
BindingResolver<T>? _findBindingResolver<T>(String? named) => BindingResolver<T>? _findBindingResolver<T>(String? named) =>
_bindingResolvers[T]?[named] as BindingResolver<T>?; _bindingResolvers[T]?[named] as BindingResolver<T>?;
void _addModuleToIndex(Module module) {
for (var binding in module.bindingSet) {
_bindingResolvers.putIfAbsent(binding.key, () => {});
final nameKey = binding.isNamed ? binding.name : null;
_bindingResolvers[binding.key]![nameKey] = binding.resolver!;
}
}
/// Rebuilds the internal index of all [BindingResolver]s from installed modules. /// Rebuilds the internal index of all [BindingResolver]s from installed modules.
/// Called after [dropModules]. Internal use only. /// Called after [installModules] and [dropModules]. Internal use only.
void _rebuildResolversIndex() { void _rebuildResolversIndex() {
_bindingResolvers.clear(); _bindingResolvers.clear();
for (var module in _modulesList) { for (var module in _modulesList) {
_addModuleToIndex(module); for (var binding in module.bindingSet) {
_bindingResolvers.putIfAbsent(binding.key, () => {});
final nameKey = binding.isNamed ? binding.name : null;
_bindingResolvers[binding.key]![nameKey] = binding.resolver!;
}
} }
} }
/// Tracks resolved [Disposable] instances, to ensure dispose is called automatically. /// Tracks resolved [Disposable] instances, to ensure dispose is called automatically.
/// Internal use only. /// Internal use only.
void _trackDisposable(Object? obj) { void _trackDisposable(Object? obj) {
if (obj is Disposable) { if (obj is Disposable && !_disposables.contains(obj)) {
_disposables.add(obj); _disposables.add(obj);
} }
} }
@@ -551,14 +487,14 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
/// ``` /// ```
Future<void> dispose() async { Future<void> dispose() async {
// Create copies to avoid concurrent modification // Create copies to avoid concurrent modification
final scopes = _scopeMap.values.toList(); final scopesCopy = Map<String, Scope>.from(_scopeMap);
for (final subScope in scopes) { for (final subScope in scopesCopy.values) {
await subScope.dispose(); await subScope.dispose();
} }
_scopeMap.clear(); _scopeMap.clear();
final disposables = _disposables.toList(); final disposablesCopy = Set<Disposable>.from(_disposables);
for (final d in disposables) { for (final d in disposablesCopy) {
await d.dispose(); await d.dispose();
} }
_disposables.clear(); _disposables.clear();

View File

@@ -1,5 +1,3 @@
import 'dart:async';
import 'package:cherrypick/cherrypick.dart'; import 'package:cherrypick/cherrypick.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
@@ -14,8 +12,7 @@ void main() {
test('Sets mode to instance', () { test('Sets mode to instance', () {
final binding = Binding<int>().toInstance(5); final binding = Binding<int>().toInstance(5);
expect(binding.resolver, isA<BindingResolver<int>>()); expect(binding.resolver, isA<InstanceResolver<int>>());
expect(binding.resolver, isA<SyncInstanceResolver<int>>());
}); });
test('isSingleton is true', () { test('isSingleton is true', () {
@@ -37,8 +34,7 @@ void main() {
test('Sets mode to instance', () { test('Sets mode to instance', () {
final binding = Binding<int>().withName('n').toInstance(5); final binding = Binding<int>().withName('n').toInstance(5);
expect(binding.resolver, isA<BindingResolver<int>>()); expect(binding.resolver, isA<InstanceResolver<int>>());
expect(binding.resolver, isA<SyncInstanceResolver<int>>());
}); });
test('Sets key', () { test('Sets key', () {
@@ -77,8 +73,7 @@ void main() {
test('Sets mode to instance', () { test('Sets mode to instance', () {
final binding = Binding<int>().toInstance(Future.value(5)); final binding = Binding<int>().toInstance(Future.value(5));
expect(binding.resolver, isA<BindingResolver<int>>()); expect(binding.resolver, isA<InstanceResolver<int>>());
expect(binding.resolver, isA<AsyncInstanceResolver<int>>());
}); });
test('isSingleton is true after toInstanceAsync', () { test('isSingleton is true after toInstanceAsync', () {
@@ -112,8 +107,7 @@ void main() {
test('Sets mode to providerInstance', () { test('Sets mode to providerInstance', () {
final binding = Binding<int>().toProvide(() => 5); final binding = Binding<int>().toProvide(() => 5);
expect(binding.resolver, isA<BindingResolver<int>>()); expect(binding.resolver, isA<ProviderResolver<int>>());
expect(binding.resolveSync(), 5);
}); });
test('isSingleton is false by default', () { test('isSingleton is false by default', () {
@@ -135,8 +129,7 @@ void main() {
test('Sets mode to providerInstance', () { test('Sets mode to providerInstance', () {
final binding = Binding<int>().withName('n').toProvide(() => 5); final binding = Binding<int>().withName('n').toProvide(() => 5);
expect(binding.resolver, isA<BindingResolver<int>>()); expect(binding.resolver, isA<ProviderResolver<int>>());
expect(binding.resolveSync(), 5);
}); });
test('Sets key', () { test('Sets key', () {
@@ -173,43 +166,6 @@ void main() {
.toProvideWithParams((param) async => 5 + (param as int)); .toProvideWithParams((param) async => 5 + (param as int));
expect(await binding.resolveAsync(3), 8); expect(await binding.resolveAsync(3), 8);
}); });
test('Resolves toProvideAsync value', () async {
final binding = Binding<int>().toProvideAsync(() async => 5);
expect(await binding.resolveAsync(), 5);
});
test('toProvideAsync singleton caches instance', () async {
int counter = 0;
final binding = Binding<int>().toProvideAsync(() async {
counter++;
return counter;
}).singleton();
final first = await binding.resolveAsync();
final second = await binding.resolveAsync();
expect(first, equals(second));
expect(counter, 1);
});
test('Resolves toProvideAsyncWithParams value', () async {
final binding = Binding<int>()
.toProvideAsyncWithParams((param) async => 5 + (param as int));
expect(await binding.resolveAsync(3), 8);
});
test('toProvideAsyncWithParams singleton caches instance', () async {
int counter = 0;
final binding = Binding<int>().toProvideAsyncWithParams((param) async {
counter++;
return counter + (param as int);
}).singleton();
final first = await binding.resolveAsync(10);
final second = await binding.resolveAsync(20);
expect(first, equals(second));
expect(counter, 1);
});
}); });
// --- Singleton provider binding --- // --- Singleton provider binding ---
@@ -276,103 +232,6 @@ void main() {
}); });
}); });
// --- FutureOr provider resolver (lazy detection, no eager side-effects) ---
group('FutureOr Provider Resolver', () {
test('Does not call provider at binding creation', () {
int calls = 0;
FutureOr<int> provider() {
calls++;
return 42;
}
final binding = Binding<int>().toProvide(provider);
expect(calls, 0);
expect(binding.resolveSync(), 42);
expect(calls, 1);
});
test('Sync FutureOr provider resolves via resolveSync', () {
final binding = Binding<int>().toProvide(() => 7);
expect(binding.resolveSync(), 7);
});
test('Async FutureOr provider throws on resolveSync', () {
final binding =
Binding<int>().toProvide(() async => 7);
expect(() => binding.resolveSync(), throwsStateError);
});
test('Async FutureOr provider resolves via resolveAsync', () async {
final binding =
Binding<int>().toProvide(() async => 7);
expect(await binding.resolveAsync(), 7);
});
test('FutureOr provider with params does not call provider at creation',
() {
int calls = 0;
FutureOr<int> provider(dynamic p) {
calls++;
return (p as int) + 1;
}
final binding = Binding<int>().toProvideWithParams(provider);
expect(calls, 0);
expect(binding.resolveSync(5), 6);
expect(calls, 1);
});
test('Async FutureOr provider with params throws on resolveSync', () {
final binding = Binding<int>()
.toProvideWithParams((p) async => (p as int) + 1);
expect(() => binding.resolveSync(5), throwsStateError);
});
test('Async FutureOr provider with params resolves via resolveAsync',
() async {
final binding = Binding<int>()
.toProvideWithParams((p) async => (p as int) + 1);
expect(await binding.resolveAsync(5), 6);
});
test('FutureOr singleton caches sync result', () {
int calls = 0;
final binding = Binding<int>().toProvide(() {
calls++;
return 99;
}).singleton();
expect(binding.resolveSync(), 99);
expect(binding.resolveSync(), 99);
expect(calls, 1);
});
test('FutureOr singleton caches async result', () async {
int calls = 0;
final binding = Binding<int>().toProvide(() async {
calls++;
return 99;
}).singleton();
expect(await binding.resolveAsync(), 99);
expect(await binding.resolveAsync(), 99);
expect(calls, 1);
});
});
// --- InstanceResolver factory ---
group('InstanceResolver.create', () {
test('Returns SyncInstanceResolver for sync value', () {
final binding = Binding<int>().toInstance(5);
expect(binding.resolver, isA<SyncInstanceResolver<int>>());
});
test('Returns AsyncInstanceResolver for Future value', () {
final binding = Binding<int>().toInstance(Future.value(5));
expect(binding.resolver, isA<AsyncInstanceResolver<int>>());
});
});
// --- WithName / Named binding, isNamed, edge-cases --- // --- WithName / Named binding, isNamed, edge-cases ---
group('Named binding & helpers', () { group('Named binding & helpers', () {
test('withName sets isNamed true and stores name', () { test('withName sets isNamed true and stores name', () {

View File

@@ -1,153 +0,0 @@
import 'package:cherrypick/cherrypick.dart';
import 'package:test/test.dart';
class _IntModule extends Module {
final int value;
final void Function()? onBuild;
_IntModule(this.value, {this.onBuild});
@override
void builder(Scope currentScope) {
onBuild?.call();
bind<int>().toInstance(value);
}
}
class _StringModule extends Module {
final String value;
_StringModule(this.value);
@override
void builder(Scope currentScope) {
bind<String>().toInstance(value);
}
}
class _NamedIntModule extends Module {
final String name;
final int value;
_NamedIntModule(this.name, this.value);
@override
void builder(Scope currentScope) {
bind<int>().withName(name).toInstance(value);
}
}
class _DisposableModule extends Module {
@override
void builder(Scope currentScope) {
bind<_DisposableService>().toProvide(() => _DisposableService());
}
}
class _DisposableService implements Disposable {
bool disposed = false;
@override
Future<void> dispose() async {
disposed = true;
}
}
void main() {
tearDown(() => CherryPick.closeRootScope());
group('SilentCherryPickObserver fast-path', () {
late Scope scope;
setUp(() {
// Must NOT have cycle detection — otherwise _canUseDirectResolvePath is false
// and the fast-path branch is never taken.
CherryPick.disableGlobalCycleDetection();
scope = CherryPick.openRootScope();
});
test('resolve with default silent observer uses fast-path', () {
scope.installModules([_IntModule(42)]);
expect(scope.resolve<int>(), 42);
});
test('resolveAsync with default silent observer uses fast-path', () async {
scope.installModules([_IntModule(42)]);
final result = await scope.resolveAsync<int>();
expect(result, 42);
});
test('tryResolve with default silent observer uses fast-path', () {
scope.installModules([_IntModule(42)]);
expect(scope.tryResolve<int>(), 42);
});
test('tryResolveAsync with default silent observer uses fast-path',
() async {
scope.installModules([_IntModule(42)]);
final result = await scope.tryResolveAsync<int>();
expect(result, 42);
});
test('fast-path missing dependency throws', () {
expect(() => scope.resolve<int>(), throwsStateError);
});
test('fast-path missing async dependency throws', () {
expect(scope.resolveAsync<int>(), throwsA(isA<StateError>()));
});
test('installModules with silent observer skips diagnostics', () {
var buildCalls = 0;
scope.installModules([_IntModule(42, onBuild: () => buildCalls++)]);
expect(buildCalls, 1);
expect(scope.resolve<int>(), 42);
});
test('Disposable is tracked even in silent observer fast-path', () async {
scope.installModules([_DisposableModule()]);
final service = scope.resolve<_DisposableService>();
expect(service.disposed, false);
await scope.dispose();
expect(service.disposed, true);
});
});
group('Incremental module index', () {
test('Multiple installModules calls accumulate bindings', () {
final scope = CherryPick.openSafeRootScope();
scope.installModules([_IntModule(1)]);
scope.installModules([_StringModule('two')]);
expect(scope.resolve<int>(), 1);
expect(scope.resolve<String>(), 'two');
});
test('dropModules clears all bindings', () {
final scope = CherryPick.openSafeRootScope();
scope.installModules([_IntModule(1)]);
scope.dropModules();
expect(() => scope.resolve<int>(), throwsStateError);
});
test('Re-installing modules after dropModules works', () {
final scope = CherryPick.openSafeRootScope();
scope.installModules([_IntModule(1)]);
scope.dropModules();
scope.installModules([_IntModule(2)]);
expect(scope.resolve<int>(), 2);
});
test('Named bindings are indexed incrementally', () {
final scope = CherryPick.openSafeRootScope();
scope.installModules([_NamedIntModule('a', 1)]);
scope.installModules([_NamedIntModule('b', 2)]);
expect(scope.resolve<int>(named: 'a'), 1);
expect(scope.resolve<int>(named: 'b'), 2);
});
});
}

View File

@@ -550,4 +550,4 @@ void main() {
- [cherrypick](https://pub.dev/packages/cherrypick) - [cherrypick](https://pub.dev/packages/cherrypick)
- [cherrypick_annotations](https://pub.dev/packages/cherrypick_annotations) - [cherrypick_annotations](https://pub.dev/packages/cherrypick_annotations)
- [cherrypick_generator](https://pub.dev/packages/cherrypick_generator) - [cherrypick_generator](https://pub.dev/packages/cherrypick_generator)
- [Исходники на GitHub](https://github.com/pese-git/cherrypick) - [Исходники на GitHub](https://github.com/pese-git/cherrypick)

View File

@@ -255,4 +255,4 @@ class ApiClientImpl implements ApiClient {
return 'Network data'; return 'Network data';
} }
} }
``` ```

View File

@@ -64,14 +64,10 @@
- **WHEN** метод помечен одновременно `@instance` и `@provide` - **WHEN** метод помечен одновременно `@instance` и `@provide`
- **THEN** генератор завершает сборку с ошибкой валидации - **THEN** генератор завершает сборку с ошибкой валидации
#### Scenario: Требования к @named на provider-методе #### Scenario: Требования к @named
- **WHEN** `@named` на provider-методе использует пустую строку или некорректный идентификатор - **WHEN** `@named` использует пустую строку или некорректный идентификатор
- **THEN** генератор завершает сборку с ошибкой валидации - **THEN** генератор завершает сборку с ошибкой валидации
#### Scenario: Пустой @named на inject-поле
- **WHEN** `@named('')` указан на поле с `@inject`
- **THEN** генератор трактует поле как безымянный резолв (без параметра `named`)
#### Scenario: Валидность @module #### Scenario: Валидность @module
- **WHEN** класс с `@module` не имеет публичных методов - **WHEN** класс с `@module` не имеет публичных методов
- **THEN** генератор завершает сборку с ошибкой валидации - **THEN** генератор завершает сборку с ошибкой валидации

View File

@@ -23,11 +23,11 @@
- **THEN** subscope удаляется из дерева, а связанные ресурсы освобождаются - **THEN** subscope удаляется из дерева, а связанные ресурсы освобождаются
#### Scenario: Путь scope и разделитель #### Scenario: Путь scope и разделитель
- **WHEN** вызывается `CherryPick.openScope(scopeName: ..., separator: ...)` с иерархическим путем - **WHEN** scope открывается по иерархическому пути с разделителем
- **THEN** создается цепочка subscopes по каждому сегменту пути - **THEN** создается цепочка subscopes по каждому сегменту пути
#### Scenario: Пустой scopeName #### Scenario: Пустой scopeName
- **WHEN** вызывается `CherryPick.openScope(scopeName: '')` - **WHEN** scope открывается с пустым именем
- **THEN** возвращается root scope - **THEN** возвращается root scope
### Requirement: Установка и удаление модулей ### Requirement: Установка и удаление модулей
@@ -81,8 +81,8 @@
### Requirement: Ошибки несоответствия sync/async ### Requirement: Ошибки несоответствия sync/async
Резолв MUST выбрасывать ошибки при несоответствии sync/async режима. Резолв MUST выбрасывать ошибки при несоответствии sync/async режима.
#### Scenario: Синхронный резолв для asyncbinding #### Scenario: ResolveSync для asyncинстанса
- **WHEN** binding зарегистрирован как asyncинстанс или asyncprovider, а вызывается `resolve<T>()` или `tryResolve<T>()` - **WHEN** binding зарегистрирован как asyncинстанс или asyncprovider, а вызывается `resolveSync`
- **THEN** выбрасывается ошибка с указанием использовать asyncрезолв - **THEN** выбрасывается ошибка с указанием использовать asyncрезолв
### Requirement: Управление Disposable ### Requirement: Управление Disposable
@@ -111,7 +111,7 @@
- **THEN** observer получает соответствующие уведомления - **THEN** observer получает соответствующие уведомления
### Requirement: Ошибки и сообщения об ошибках ### Requirement: Ошибки и сообщения об ошибках
При критических сбоях резолва ядро MUST выбрасывать ошибку с понятным сообщением. Для отсутствующей зависимости `tryResolve`/`tryResolveAsync` MUST возвращать `null` без исключения; ошибки выполнения резолва (например, цикл, sync/async mismatch, отсутствие обязательных params) MAY быть проброшены. При критических сбоях резолва ядро MUST выбрасывать ошибку с понятным сообщением, а для tryResolve MUST не бросать исключения.
#### Scenario: Ошибка отсутствующей зависимости #### Scenario: Ошибка отсутствующей зависимости
- **WHEN** вызывается `resolve<T>()` для незарегистрированной зависимости - **WHEN** вызывается `resolve<T>()` для незарегистрированной зависимости

View File

@@ -41,14 +41,14 @@ Flutterинтеграция MUST предоставлять `CherryPickProvid
#### Scenario: Ошибка lookup #### Scenario: Ошибка lookup
- **WHEN** вызов происходит вне поддерева провайдера - **WHEN** вызов происходит вне поддерева провайдера
- **THEN** в debugрежиме происходит assertionошибка - **THEN** происходит assertionошибка
### Requirement: Ошибки и сообщения ### Requirement: Ошибки и сообщения
При отсутствии провайдера в дереве MUST быть диагностируемая ошибка. При отсутствии провайдера в дереве MUST быть диагностируемая ошибка.
#### Scenario: Диагностика отсутствия провайдера #### Scenario: Диагностика отсутствия провайдера
- **WHEN** `CherryPickProvider.of(context)` не находит провайдер - **WHEN** `CherryPickProvider.of(context)` не находит провайдер
- **THEN** в debugрежиме сообщение assertion указывает на отсутствие провайдера - **THEN** сообщение об ошибке указывает на отсутствие провайдера
### Requirement: Точки расширения ### Requirement: Точки расширения
Flutterинтеграция MUST позволять использовать собственные DIscope стратегии поверх `CherryPickProvider`. Flutterинтеграция MUST позволять использовать собственные DIscope стратегии поверх `CherryPickProvider`.

View File

@@ -38,4 +38,4 @@ See [Installation](installation.md) for instructions on adding CherryPick to you
CherryPick is open-source. Contributions and questions are welcome! CherryPick is open-source. Contributions and questions are welcome!
--- ---