mirror of
https://github.com/pese-git/cherrypick.git
synced 2026-05-16 10:10:43 +00:00
Compare commits
7 Commits
3c550db8cd
...
fix/BR-imp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c9500a6a1c | ||
|
|
fb0e77a87f | ||
|
|
c413dfed20 | ||
|
|
4077ed8469 | ||
|
|
59234b44c2 | ||
|
|
3c4fc67166 | ||
|
|
3331a3ee9c |
137
benchmark_di/REPORT_BENCHMARK_COMPARISON.md
Normal file
137
benchmark_di/REPORT_BENCHMARK_COMPARISON.md
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
# 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 ~5–7 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 2–18× 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).
|
||||||
148
benchmark_di/REPORT_v2.md
Normal file
148
benchmark_di/REPORT_v2.md
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
# 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.
|
||||||
148
benchmark_di/REPORT_v2.ru.md
Normal file
148
benchmark_di/REPORT_v2.ru.md
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
# Сравнительный отчет 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.
|
||||||
@@ -25,7 +25,11 @@ class UniversalChainAsyncBenchmark<TContainer> extends AsyncBenchmarkBase {
|
|||||||
bindingMode: mode,
|
bindingMode: mode,
|
||||||
scenario: UniversalScenario.asyncChain,
|
scenario: UniversalScenario.asyncChain,
|
||||||
));
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> prewarm() async {
|
||||||
await di.waitForAsyncReady();
|
await di.waitForAsyncReady();
|
||||||
|
await run();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -50,7 +50,14 @@ class UniversalChainBenchmark<TContainer> extends BenchmarkBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void teardown() => _di.teardown();
|
void teardown() {
|
||||||
|
_childDi?.teardown();
|
||||||
|
_di.teardown();
|
||||||
|
}
|
||||||
|
|
||||||
|
void prewarm() {
|
||||||
|
run();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void run() {
|
void run() {
|
||||||
@@ -59,11 +66,7 @@ class UniversalChainBenchmark<TContainer> extends BenchmarkBase {
|
|||||||
_di.resolve<UniversalService>();
|
_di.resolve<UniversalService>();
|
||||||
break;
|
break;
|
||||||
case UniversalScenario.named:
|
case UniversalScenario.named:
|
||||||
if (_di.runtimeType.toString().contains('GetItAdapter')) {
|
|
||||||
_di.resolve<UniversalService>(named: 'impl2');
|
_di.resolve<UniversalService>(named: 'impl2');
|
||||||
} else {
|
|
||||||
_di.resolve<UniversalService>(named: 'impl2');
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case UniversalScenario.chain:
|
case UniversalScenario.chain:
|
||||||
final serviceName = '${chainCount}_$nestingDepth';
|
final serviceName = '${chainCount}_$nestingDepth';
|
||||||
|
|||||||
@@ -31,9 +31,16 @@ 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
|
||||||
|
const asyncUnsupported = {'kiwi', 'yx_scope'};
|
||||||
|
for (final phase in config.phases) {
|
||||||
for (final bench in config.benchesToRun) {
|
for (final bench in config.benchesToRun) {
|
||||||
final scenario = toScenario(bench);
|
final scenario = toScenario(bench);
|
||||||
final mode = toMode(bench);
|
final mode = toMode(bench);
|
||||||
|
if (asyncUnsupported.contains(config.di) &&
|
||||||
|
scenario == UniversalScenario.asyncChain) {
|
||||||
|
continue; // Skip async benchmarks for DI that does not support them
|
||||||
|
}
|
||||||
for (final c in config.chainCounts) {
|
for (final c in config.chainCounts) {
|
||||||
for (final d in config.nestDepths) {
|
for (final d in config.nestDepths) {
|
||||||
BenchmarkResult benchResult;
|
BenchmarkResult benchResult;
|
||||||
@@ -50,6 +57,7 @@ class BenchmarkCliRunner {
|
|||||||
benchmark: benchAsync,
|
benchmark: benchAsync,
|
||||||
warmups: config.warmups,
|
warmups: config.warmups,
|
||||||
repeats: config.repeats,
|
repeats: config.repeats,
|
||||||
|
phase: phase,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
final benchSync = UniversalChainBenchmark<GetIt>(
|
final benchSync = UniversalChainBenchmark<GetIt>(
|
||||||
@@ -63,12 +71,12 @@ class BenchmarkCliRunner {
|
|||||||
benchmark: benchSync,
|
benchmark: benchSync,
|
||||||
warmups: config.warmups,
|
warmups: config.warmups,
|
||||||
repeats: config.repeats,
|
repeats: config.repeats,
|
||||||
|
phase: phase,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (config.di == 'kiwi') {
|
} else if (config.di == 'kiwi') {
|
||||||
final di = KiwiAdapter();
|
final di = KiwiAdapter();
|
||||||
if (scenario == UniversalScenario.asyncChain) {
|
if (scenario == UniversalScenario.asyncChain) {
|
||||||
// UnsupportedError будет выброшен адаптером, но если дойдёт — вызывать async benchmark
|
|
||||||
final benchAsync = UniversalChainAsyncBenchmark<KiwiContainer>(
|
final benchAsync = UniversalChainAsyncBenchmark<KiwiContainer>(
|
||||||
di,
|
di,
|
||||||
chainCount: c,
|
chainCount: c,
|
||||||
@@ -79,6 +87,7 @@ class BenchmarkCliRunner {
|
|||||||
benchmark: benchAsync,
|
benchmark: benchAsync,
|
||||||
warmups: config.warmups,
|
warmups: config.warmups,
|
||||||
repeats: config.repeats,
|
repeats: config.repeats,
|
||||||
|
phase: phase,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
final benchSync = UniversalChainBenchmark<KiwiContainer>(
|
final benchSync = UniversalChainBenchmark<KiwiContainer>(
|
||||||
@@ -92,6 +101,7 @@ class BenchmarkCliRunner {
|
|||||||
benchmark: benchSync,
|
benchmark: benchSync,
|
||||||
warmups: config.warmups,
|
warmups: config.warmups,
|
||||||
repeats: config.repeats,
|
repeats: config.repeats,
|
||||||
|
phase: phase,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (config.di == 'riverpod') {
|
} else if (config.di == 'riverpod') {
|
||||||
@@ -108,6 +118,7 @@ class BenchmarkCliRunner {
|
|||||||
benchmark: benchAsync,
|
benchmark: benchAsync,
|
||||||
warmups: config.warmups,
|
warmups: config.warmups,
|
||||||
repeats: config.repeats,
|
repeats: config.repeats,
|
||||||
|
phase: phase,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
final benchSync = UniversalChainBenchmark<
|
final benchSync = UniversalChainBenchmark<
|
||||||
@@ -122,6 +133,7 @@ class BenchmarkCliRunner {
|
|||||||
benchmark: benchSync,
|
benchmark: benchSync,
|
||||||
warmups: config.warmups,
|
warmups: config.warmups,
|
||||||
repeats: config.repeats,
|
repeats: config.repeats,
|
||||||
|
phase: phase,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (config.di == 'yx_scope') {
|
} else if (config.di == 'yx_scope') {
|
||||||
@@ -138,6 +150,7 @@ class BenchmarkCliRunner {
|
|||||||
benchmark: benchAsync,
|
benchmark: benchAsync,
|
||||||
warmups: config.warmups,
|
warmups: config.warmups,
|
||||||
repeats: config.repeats,
|
repeats: config.repeats,
|
||||||
|
phase: phase,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
final benchSync =
|
final benchSync =
|
||||||
@@ -152,6 +165,7 @@ class BenchmarkCliRunner {
|
|||||||
benchmark: benchSync,
|
benchmark: benchSync,
|
||||||
warmups: config.warmups,
|
warmups: config.warmups,
|
||||||
repeats: config.repeats,
|
repeats: config.repeats,
|
||||||
|
phase: phase,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -167,6 +181,7 @@ class BenchmarkCliRunner {
|
|||||||
benchmark: benchAsync,
|
benchmark: benchAsync,
|
||||||
warmups: config.warmups,
|
warmups: config.warmups,
|
||||||
repeats: config.repeats,
|
repeats: config.repeats,
|
||||||
|
phase: phase,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
final benchSync = UniversalChainBenchmark<Scope>(
|
final benchSync = UniversalChainBenchmark<Scope>(
|
||||||
@@ -180,22 +195,27 @@ class BenchmarkCliRunner {
|
|||||||
benchmark: benchSync,
|
benchmark: benchSync,
|
||||||
warmups: config.warmups,
|
warmups: config.warmups,
|
||||||
repeats: config.repeats,
|
repeats: config.repeats,
|
||||||
|
phase: phase,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
final timings = benchResult.timings;
|
final timings = benchResult.timings;
|
||||||
|
if (timings.isEmpty) continue; // skip failed scenarios
|
||||||
timings.sort();
|
timings.sort();
|
||||||
var mean = timings.reduce((a, b) => a + b) / timings.length;
|
final count = timings.length;
|
||||||
var median = timings[timings.length ~/ 2];
|
final mean = timings.reduce((a, b) => a + b) / count;
|
||||||
var minVal = timings.first;
|
final median = count.isOdd
|
||||||
var maxVal = timings.last;
|
? timings[count ~/ 2]
|
||||||
var stddev = timings.isEmpty
|
: (timings[count ~/ 2 - 1] + timings[count ~/ 2]) / 2;
|
||||||
? 0
|
final minVal = timings.first;
|
||||||
: sqrt(
|
final maxVal = timings.last;
|
||||||
timings.map((x) => pow(x - mean, 2)).reduce((a, b) => a + b) /
|
final stddev = sqrt(timings
|
||||||
timings.length);
|
.map((x) => pow(x - mean, 2))
|
||||||
|
.reduce((a, b) => a + b) /
|
||||||
|
count);
|
||||||
results.add({
|
results.add({
|
||||||
'benchmark': 'Universal_$bench',
|
'benchmark': 'Universal_$bench',
|
||||||
|
'phase': phase.name,
|
||||||
'chainCount': c,
|
'chainCount': c,
|
||||||
'nestingDepth': d,
|
'nestingDepth': d,
|
||||||
'mean_us': mean.toStringAsFixed(2),
|
'mean_us': mean.toStringAsFixed(2),
|
||||||
@@ -212,6 +232,7 @@ class BenchmarkCliRunner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
final reportGenerators = {
|
final reportGenerators = {
|
||||||
'pretty': PrettyReport(),
|
'pretty': PrettyReport(),
|
||||||
'csv': CsvReport(),
|
'csv': CsvReport(),
|
||||||
|
|||||||
@@ -6,12 +6,18 @@ import 'package:benchmark_di/scenarios/universal_scenario.dart';
|
|||||||
|
|
||||||
/// Enum describing all supported Universal DI benchmark types.
|
/// Enum describing all supported Universal DI benchmark types.
|
||||||
enum UniversalBenchmark {
|
enum UniversalBenchmark {
|
||||||
/// Simple singleton registration benchmark
|
/// Simple singleton registration benchmark (eager, where supported)
|
||||||
registerSingleton,
|
registerSingleton,
|
||||||
|
|
||||||
/// Chain of singleton dependencies
|
/// Simple lazy singleton registration benchmark
|
||||||
|
registerLazySingleton,
|
||||||
|
|
||||||
|
/// Chain of eager singleton dependencies
|
||||||
chainSingleton,
|
chainSingleton,
|
||||||
|
|
||||||
|
/// Chain of lazy singleton dependencies
|
||||||
|
chainLazySingleton,
|
||||||
|
|
||||||
/// Chain using factories
|
/// Chain using factories
|
||||||
chainFactory,
|
chainFactory,
|
||||||
|
|
||||||
@@ -25,12 +31,19 @@ enum UniversalBenchmark {
|
|||||||
override,
|
override,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum ResolvePhase {
|
||||||
|
firstResolve,
|
||||||
|
steadyStateResolve,
|
||||||
|
}
|
||||||
|
|
||||||
/// Maps [UniversalBenchmark] to the scenario enum for DI chains.
|
/// Maps [UniversalBenchmark] to the scenario enum for DI chains.
|
||||||
UniversalScenario toScenario(UniversalBenchmark b) {
|
UniversalScenario toScenario(UniversalBenchmark b) {
|
||||||
switch (b) {
|
switch (b) {
|
||||||
case UniversalBenchmark.registerSingleton:
|
case UniversalBenchmark.registerSingleton:
|
||||||
|
case UniversalBenchmark.registerLazySingleton:
|
||||||
return UniversalScenario.register;
|
return UniversalScenario.register;
|
||||||
case UniversalBenchmark.chainSingleton:
|
case UniversalBenchmark.chainSingleton:
|
||||||
|
case UniversalBenchmark.chainLazySingleton:
|
||||||
return UniversalScenario.chain;
|
return UniversalScenario.chain;
|
||||||
case UniversalBenchmark.chainFactory:
|
case UniversalBenchmark.chainFactory:
|
||||||
return UniversalScenario.chain;
|
return UniversalScenario.chain;
|
||||||
@@ -43,21 +56,21 @@ UniversalScenario toScenario(UniversalBenchmark b) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Maps benchmark to registration mode (singleton/factory/async).
|
/// Maps benchmark to registration mode (singleton/lazySingleton/factory/async).
|
||||||
UniversalBindingMode toMode(UniversalBenchmark b) {
|
UniversalBindingMode toMode(UniversalBenchmark b) {
|
||||||
switch (b) {
|
switch (b) {
|
||||||
case UniversalBenchmark.registerSingleton:
|
case UniversalBenchmark.registerSingleton:
|
||||||
return UniversalBindingMode.singletonStrategy;
|
|
||||||
case UniversalBenchmark.chainSingleton:
|
case UniversalBenchmark.chainSingleton:
|
||||||
|
case UniversalBenchmark.named:
|
||||||
|
case UniversalBenchmark.override:
|
||||||
return UniversalBindingMode.singletonStrategy;
|
return UniversalBindingMode.singletonStrategy;
|
||||||
|
case UniversalBenchmark.registerLazySingleton:
|
||||||
|
case UniversalBenchmark.chainLazySingleton:
|
||||||
|
return UniversalBindingMode.lazySingletonStrategy;
|
||||||
case UniversalBenchmark.chainFactory:
|
case UniversalBenchmark.chainFactory:
|
||||||
return UniversalBindingMode.factoryStrategy;
|
return UniversalBindingMode.factoryStrategy;
|
||||||
case UniversalBenchmark.chainAsync:
|
case UniversalBenchmark.chainAsync:
|
||||||
return UniversalBindingMode.asyncStrategy;
|
return UniversalBindingMode.asyncStrategy;
|
||||||
case UniversalBenchmark.named:
|
|
||||||
return UniversalBindingMode.singletonStrategy;
|
|
||||||
case UniversalBenchmark.override:
|
|
||||||
return UniversalBindingMode.singletonStrategy;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,6 +111,10 @@ class BenchmarkCliConfig {
|
|||||||
|
|
||||||
/// Name of DI implementation ("cherrypick" or "getit")
|
/// Name of DI implementation ("cherrypick" or "getit")
|
||||||
final String di;
|
final String di;
|
||||||
|
|
||||||
|
/// Which resolve phase(s) to measure.
|
||||||
|
final List<ResolvePhase> phases;
|
||||||
|
|
||||||
BenchmarkCliConfig({
|
BenchmarkCliConfig({
|
||||||
required this.benchesToRun,
|
required this.benchesToRun,
|
||||||
required this.chainCounts,
|
required this.chainCounts,
|
||||||
@@ -106,6 +123,7 @@ class BenchmarkCliConfig {
|
|||||||
required this.warmups,
|
required this.warmups,
|
||||||
required this.format,
|
required this.format,
|
||||||
required this.di,
|
required this.di,
|
||||||
|
required this.phases,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,6 +137,8 @@ BenchmarkCliConfig parseBenchmarkCli(List<String> args) {
|
|||||||
..addOption('repeat', abbr: 'r', defaultsTo: '2')
|
..addOption('repeat', abbr: 'r', defaultsTo: '2')
|
||||||
..addOption('warmup', abbr: 'w', defaultsTo: '1')
|
..addOption('warmup', abbr: 'w', defaultsTo: '1')
|
||||||
..addOption('format', abbr: 'f', defaultsTo: 'pretty')
|
..addOption('format', abbr: 'f', defaultsTo: 'pretty')
|
||||||
|
..addOption('resolvePhase',
|
||||||
|
defaultsTo: 'all', help: 'Resolve phase: first, steady, or all')
|
||||||
..addOption('di',
|
..addOption('di',
|
||||||
defaultsTo: 'cherrypick',
|
defaultsTo: 'cherrypick',
|
||||||
help: 'DI implementation: cherrypick, getit or riverpod')
|
help: 'DI implementation: cherrypick, getit or riverpod')
|
||||||
@@ -128,12 +148,39 @@ BenchmarkCliConfig parseBenchmarkCli(List<String> args) {
|
|||||||
print(parser.usage);
|
print(parser.usage);
|
||||||
exit(0);
|
exit(0);
|
||||||
}
|
}
|
||||||
final benchName = result['benchmark'] as String;
|
final benchNameInput = result['benchmark'] as String;
|
||||||
final isAll = benchName == 'all';
|
final isAll = benchNameInput.trim() == 'all';
|
||||||
final allBenches = UniversalBenchmark.values;
|
final allBenches = UniversalBenchmark.values;
|
||||||
|
|
||||||
|
String normalizeBenchName(String name) {
|
||||||
|
final n = name.trim().toLowerCase();
|
||||||
|
return switch (n) {
|
||||||
|
'register' || 'registersingleton' || 'registereager' => 'registerSingleton',
|
||||||
|
'registerlazy' || 'registerlazysingleton' || 'registerlazysingle' => 'registerLazySingleton',
|
||||||
|
'chain' || 'chainsingleton' || 'chaineager' => 'chainSingleton',
|
||||||
|
'chainlazy' || 'chainlazysingleton' || 'lazysingleton' => 'chainLazySingleton',
|
||||||
|
'chainfactory' || 'factory' => 'chainFactory',
|
||||||
|
'async' || 'asyncchain' || 'chainasync' => 'chainAsync',
|
||||||
|
'named' => 'named',
|
||||||
|
'override' => 'override',
|
||||||
|
_ => n,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
final benchesToRun = isAll
|
final benchesToRun = isAll
|
||||||
? allBenches
|
? allBenches
|
||||||
: [parseEnum(benchName, allBenches, UniversalBenchmark.chainSingleton)];
|
: benchNameInput
|
||||||
|
.split(',')
|
||||||
|
.map((n) => parseEnum(normalizeBenchName(n), allBenches,
|
||||||
|
UniversalBenchmark.chainSingleton))
|
||||||
|
.toSet()
|
||||||
|
.toList();
|
||||||
|
final phaseName = (result['resolvePhase'] as String).toLowerCase();
|
||||||
|
final phases = switch (phaseName) {
|
||||||
|
'first' => [ResolvePhase.firstResolve],
|
||||||
|
'steady' => [ResolvePhase.steadyStateResolve],
|
||||||
|
_ => ResolvePhase.values,
|
||||||
|
};
|
||||||
return BenchmarkCliConfig(
|
return BenchmarkCliConfig(
|
||||||
benchesToRun: benchesToRun,
|
benchesToRun: benchesToRun,
|
||||||
chainCounts: parseIntList(result['chainCount'] as String),
|
chainCounts: parseIntList(result['chainCount'] as String),
|
||||||
@@ -142,5 +189,6 @@ BenchmarkCliConfig parseBenchmarkCli(List<String> args) {
|
|||||||
warmups: int.tryParse(result['warmup'] as String? ?? "") ?? 1,
|
warmups: int.tryParse(result['warmup'] as String? ?? "") ?? 1,
|
||||||
format: result['format'] as String,
|
format: result['format'] as String,
|
||||||
di: result['di'] as String? ?? 'cherrypick',
|
di: result['di'] as String? ?? 'cherrypick',
|
||||||
|
phases: phases,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ class CsvReport extends ReportGenerator {
|
|||||||
@override
|
@override
|
||||||
final List<String> keys = [
|
final List<String> keys = [
|
||||||
'benchmark',
|
'benchmark',
|
||||||
|
'phase',
|
||||||
'chainCount',
|
'chainCount',
|
||||||
'nestingDepth',
|
'nestingDepth',
|
||||||
'mean_us',
|
'mean_us',
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ class MarkdownReport extends ReportGenerator {
|
|||||||
@override
|
@override
|
||||||
final List<String> keys = [
|
final List<String> keys = [
|
||||||
'benchmark',
|
'benchmark',
|
||||||
|
'phase',
|
||||||
'chainCount',
|
'chainCount',
|
||||||
'nestingDepth',
|
'nestingDepth',
|
||||||
'mean_us',
|
'mean_us',
|
||||||
@@ -24,7 +25,9 @@ class MarkdownReport extends ReportGenerator {
|
|||||||
/// Friendly display names for each benchmark type.
|
/// Friendly display names for each benchmark type.
|
||||||
static const nameMap = {
|
static const nameMap = {
|
||||||
'Universal_UniversalBenchmark.registerSingleton': 'RegisterSingleton',
|
'Universal_UniversalBenchmark.registerSingleton': 'RegisterSingleton',
|
||||||
|
'Universal_UniversalBenchmark.registerLazySingleton': 'RegisterLazySingleton',
|
||||||
'Universal_UniversalBenchmark.chainSingleton': 'ChainSingleton',
|
'Universal_UniversalBenchmark.chainSingleton': 'ChainSingleton',
|
||||||
|
'Universal_UniversalBenchmark.chainLazySingleton': 'ChainLazySingleton',
|
||||||
'Universal_UniversalBenchmark.chainFactory': 'ChainFactory',
|
'Universal_UniversalBenchmark.chainFactory': 'ChainFactory',
|
||||||
'Universal_UniversalBenchmark.chainAsync': 'AsyncChain',
|
'Universal_UniversalBenchmark.chainAsync': 'AsyncChain',
|
||||||
'Universal_UniversalBenchmark.named': 'Named',
|
'Universal_UniversalBenchmark.named': 'Named',
|
||||||
@@ -36,6 +39,7 @@ class MarkdownReport extends ReportGenerator {
|
|||||||
String render(List<Map<String, dynamic>> rows) {
|
String render(List<Map<String, dynamic>> rows) {
|
||||||
final headers = [
|
final headers = [
|
||||||
'Benchmark',
|
'Benchmark',
|
||||||
|
'Phase',
|
||||||
'Chain Count',
|
'Chain Count',
|
||||||
'Depth',
|
'Depth',
|
||||||
'Mean (us)',
|
'Mean (us)',
|
||||||
@@ -52,6 +56,7 @@ class MarkdownReport extends ReportGenerator {
|
|||||||
final readableName = nameMap[r['benchmark']] ?? r['benchmark'];
|
final readableName = nameMap[r['benchmark']] ?? r['benchmark'];
|
||||||
return [
|
return [
|
||||||
readableName,
|
readableName,
|
||||||
|
r['phase'],
|
||||||
r['chainCount'],
|
r['chainCount'],
|
||||||
r['nestingDepth'],
|
r['nestingDepth'],
|
||||||
r['mean_us'],
|
r['mean_us'],
|
||||||
@@ -82,6 +87,7 @@ class MarkdownReport extends ReportGenerator {
|
|||||||
final legend = '''
|
final legend = '''
|
||||||
> **Legend:**
|
> **Legend:**
|
||||||
> `Benchmark` – Test name
|
> `Benchmark` – Test name
|
||||||
|
> `Phase` – `firstResolve` or `steadyStateResolve`
|
||||||
> `Chain Count` – Number of independent chains
|
> `Chain Count` – Number of independent chains
|
||||||
> `Depth` – Depth of each chain
|
> `Depth` – Depth of each chain
|
||||||
> `Mean (us)` – Average time per run (microseconds)
|
> `Mean (us)` – Average time per run (microseconds)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ class PrettyReport extends ReportGenerator {
|
|||||||
@override
|
@override
|
||||||
final List<String> keys = [
|
final List<String> keys = [
|
||||||
'benchmark',
|
'benchmark',
|
||||||
|
'phase',
|
||||||
'chainCount',
|
'chainCount',
|
||||||
'nestingDepth',
|
'nestingDepth',
|
||||||
'mean_us',
|
'mean_us',
|
||||||
@@ -24,7 +25,9 @@ class PrettyReport extends ReportGenerator {
|
|||||||
/// Mappings from internal benchmark IDs to display names.
|
/// Mappings from internal benchmark IDs to display names.
|
||||||
static const nameMap = {
|
static const nameMap = {
|
||||||
'Universal_UniversalBenchmark.registerSingleton': 'RegisterSingleton',
|
'Universal_UniversalBenchmark.registerSingleton': 'RegisterSingleton',
|
||||||
|
'Universal_UniversalBenchmark.registerLazySingleton': 'RegisterLazySingleton',
|
||||||
'Universal_UniversalBenchmark.chainSingleton': 'ChainSingleton',
|
'Universal_UniversalBenchmark.chainSingleton': 'ChainSingleton',
|
||||||
|
'Universal_UniversalBenchmark.chainLazySingleton': 'ChainLazySingleton',
|
||||||
'Universal_UniversalBenchmark.chainFactory': 'ChainFactory',
|
'Universal_UniversalBenchmark.chainFactory': 'ChainFactory',
|
||||||
'Universal_UniversalBenchmark.chainAsync': 'AsyncChain',
|
'Universal_UniversalBenchmark.chainAsync': 'AsyncChain',
|
||||||
'Universal_UniversalBenchmark.named': 'Named',
|
'Universal_UniversalBenchmark.named': 'Named',
|
||||||
@@ -36,6 +39,7 @@ class PrettyReport extends ReportGenerator {
|
|||||||
String render(List<Map<String, dynamic>> rows) {
|
String render(List<Map<String, dynamic>> rows) {
|
||||||
final headers = [
|
final headers = [
|
||||||
'Benchmark',
|
'Benchmark',
|
||||||
|
'Phase',
|
||||||
'Chain Count',
|
'Chain Count',
|
||||||
'Depth',
|
'Depth',
|
||||||
'Mean (us)',
|
'Mean (us)',
|
||||||
@@ -53,6 +57,7 @@ class PrettyReport extends ReportGenerator {
|
|||||||
final readableName = nameMap[r['benchmark']] ?? r['benchmark'];
|
final readableName = nameMap[r['benchmark']] ?? r['benchmark'];
|
||||||
return [
|
return [
|
||||||
readableName,
|
readableName,
|
||||||
|
r['phase'],
|
||||||
r['chainCount'],
|
r['chainCount'],
|
||||||
r['nestingDepth'],
|
r['nestingDepth'],
|
||||||
r['mean_us'],
|
r['mean_us'],
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'dart:io';
|
|||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
import 'package:benchmark_di/benchmarks/universal_chain_benchmark.dart';
|
import 'package:benchmark_di/benchmarks/universal_chain_benchmark.dart';
|
||||||
import 'package:benchmark_di/benchmarks/universal_chain_async_benchmark.dart';
|
import 'package:benchmark_di/benchmarks/universal_chain_async_benchmark.dart';
|
||||||
|
import 'package:benchmark_di/cli/parser.dart';
|
||||||
|
|
||||||
/// Holds the results for a single benchmark execution.
|
/// Holds the results for a single benchmark execution.
|
||||||
class BenchmarkResult {
|
class BenchmarkResult {
|
||||||
@@ -50,17 +51,24 @@ class BenchmarkRunner {
|
|||||||
required UniversalChainBenchmark benchmark,
|
required UniversalChainBenchmark benchmark,
|
||||||
required int warmups,
|
required int warmups,
|
||||||
required int repeats,
|
required int repeats,
|
||||||
|
required ResolvePhase phase,
|
||||||
}) async {
|
}) async {
|
||||||
final timings = <num>[];
|
final timings = <num>[];
|
||||||
final rssValues = <int>[];
|
final rssValues = <int>[];
|
||||||
for (int i = 0; i < warmups; i++) {
|
for (int i = 0; i < warmups; i++) {
|
||||||
benchmark.setup();
|
benchmark.setup();
|
||||||
|
if (phase == ResolvePhase.steadyStateResolve) {
|
||||||
|
benchmark.prewarm();
|
||||||
|
}
|
||||||
benchmark.run();
|
benchmark.run();
|
||||||
benchmark.teardown();
|
benchmark.teardown();
|
||||||
}
|
}
|
||||||
final memBefore = ProcessInfo.currentRss;
|
final memBefore = ProcessInfo.currentRss;
|
||||||
for (int i = 0; i < repeats; i++) {
|
for (int i = 0; i < repeats; i++) {
|
||||||
benchmark.setup();
|
benchmark.setup();
|
||||||
|
if (phase == ResolvePhase.steadyStateResolve) {
|
||||||
|
benchmark.prewarm();
|
||||||
|
}
|
||||||
final sw = Stopwatch()..start();
|
final sw = Stopwatch()..start();
|
||||||
benchmark.run();
|
benchmark.run();
|
||||||
sw.stop();
|
sw.stop();
|
||||||
@@ -78,17 +86,24 @@ class BenchmarkRunner {
|
|||||||
required UniversalChainAsyncBenchmark benchmark,
|
required UniversalChainAsyncBenchmark benchmark,
|
||||||
required int warmups,
|
required int warmups,
|
||||||
required int repeats,
|
required int repeats,
|
||||||
|
required ResolvePhase phase,
|
||||||
}) async {
|
}) async {
|
||||||
final timings = <num>[];
|
final timings = <num>[];
|
||||||
final rssValues = <int>[];
|
final rssValues = <int>[];
|
||||||
for (int i = 0; i < warmups; i++) {
|
for (int i = 0; i < warmups; i++) {
|
||||||
await benchmark.setup();
|
await benchmark.setup();
|
||||||
|
if (phase == ResolvePhase.steadyStateResolve) {
|
||||||
|
await benchmark.prewarm();
|
||||||
|
}
|
||||||
await benchmark.run();
|
await benchmark.run();
|
||||||
await benchmark.teardown();
|
await benchmark.teardown();
|
||||||
}
|
}
|
||||||
final memBefore = ProcessInfo.currentRss;
|
final memBefore = ProcessInfo.currentRss;
|
||||||
for (int i = 0; i < repeats; i++) {
|
for (int i = 0; i < repeats; i++) {
|
||||||
await benchmark.setup();
|
await benchmark.setup();
|
||||||
|
if (phase == ResolvePhase.steadyStateResolve) {
|
||||||
|
await benchmark.prewarm();
|
||||||
|
}
|
||||||
final sw = Stopwatch()..start();
|
final sw = Stopwatch()..start();
|
||||||
await benchmark.run();
|
await benchmark.run();
|
||||||
sw.stop();
|
sw.stop();
|
||||||
|
|||||||
@@ -59,11 +59,16 @@ class UniversalChainModule extends Module {
|
|||||||
|
|
||||||
switch (scenario) {
|
switch (scenario) {
|
||||||
case UniversalScenario.register:
|
case UniversalScenario.register:
|
||||||
// Simple singleton registration.
|
if (bindingMode == UniversalBindingMode.lazySingletonStrategy) {
|
||||||
bind<UniversalService>()
|
bind<UniversalService>()
|
||||||
.toProvide(
|
.toProvide(
|
||||||
() => UniversalServiceImpl(value: 'reg', dependency: null))
|
() => UniversalServiceImpl(value: 'reg', dependency: null))
|
||||||
.singleton();
|
.singleton();
|
||||||
|
} else {
|
||||||
|
bind<UniversalService>()
|
||||||
|
.toInstance(
|
||||||
|
UniversalServiceImpl(value: 'reg', dependency: null));
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case UniversalScenario.named:
|
case UniversalScenario.named:
|
||||||
// Named factory registration for two distinct objects.
|
// Named factory registration for two distinct objects.
|
||||||
@@ -76,6 +81,8 @@ class UniversalChainModule extends Module {
|
|||||||
break;
|
break;
|
||||||
case UniversalScenario.chain:
|
case UniversalScenario.chain:
|
||||||
// Chain of nested services, with dependency on previous level by name.
|
// Chain of nested services, with dependency on previous level by name.
|
||||||
|
UniversalService? lastEagerInstance;
|
||||||
|
final Map<String, UniversalService> eagerInstances = {};
|
||||||
for (var chainIndex = 0; chainIndex < chainCount; chainIndex++) {
|
for (var chainIndex = 0; chainIndex < chainCount; chainIndex++) {
|
||||||
for (var levelIndex = 0; levelIndex < nestingDepth; levelIndex++) {
|
for (var levelIndex = 0; levelIndex < nestingDepth; levelIndex++) {
|
||||||
final chain = chainIndex + 1;
|
final chain = chainIndex + 1;
|
||||||
@@ -84,6 +91,17 @@ class UniversalChainModule extends Module {
|
|||||||
final depName = '${chain}_$level';
|
final depName = '${chain}_$level';
|
||||||
switch (bindingMode) {
|
switch (bindingMode) {
|
||||||
case UniversalBindingMode.singletonStrategy:
|
case UniversalBindingMode.singletonStrategy:
|
||||||
|
final instance = UniversalServiceImpl(
|
||||||
|
value: depName,
|
||||||
|
dependency: eagerInstances[prevDepName],
|
||||||
|
);
|
||||||
|
bind<UniversalService>()
|
||||||
|
.toInstance(instance)
|
||||||
|
.withName(depName);
|
||||||
|
eagerInstances[depName] = instance;
|
||||||
|
lastEagerInstance = instance;
|
||||||
|
break;
|
||||||
|
case UniversalBindingMode.lazySingletonStrategy:
|
||||||
bind<UniversalService>()
|
bind<UniversalService>()
|
||||||
.toProvide(() => UniversalServiceImpl(
|
.toProvide(() => UniversalServiceImpl(
|
||||||
value: depName,
|
value: depName,
|
||||||
@@ -116,15 +134,19 @@ class UniversalChainModule extends Module {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Регистрация алиаса без имени (на последний элемент цепочки)
|
// Register unnamed alias for the last chain element.
|
||||||
|
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, но алиас нужен прямо в этом scope!
|
// Handled at benchmark level, but alias is needed directly in this scope.
|
||||||
final depName = '${chainCount}_$nestingDepth';
|
final depName = '${chainCount}_$nestingDepth';
|
||||||
bind<UniversalService>()
|
bind<UniversalService>()
|
||||||
.toProvide(
|
.toProvide(
|
||||||
@@ -189,7 +211,7 @@ class CherrypickDIAdapter extends DIAdapter<Scope> {
|
|||||||
await CherryPick.closeRootScope();
|
await CherryPick.closeRootScope();
|
||||||
_scope = null;
|
_scope = null;
|
||||||
}
|
}
|
||||||
// SubScope teardown не требуется
|
// SubScope teardown is handled by the parent scope.
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -4,14 +4,14 @@ import 'package:benchmark_di/scenarios/universal_service.dart';
|
|||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
import 'di_adapter.dart';
|
import 'di_adapter.dart';
|
||||||
|
|
||||||
/// Универсальный DIAdapter для GetIt c поддержкой scopes и строгой типизацией.
|
/// DIAdapter for GetIt with scope support and strict typing.
|
||||||
class GetItAdapter extends DIAdapter<GetIt> {
|
class GetItAdapter extends DIAdapter<GetIt> {
|
||||||
late GetIt _getIt;
|
late GetIt _getIt;
|
||||||
final String? _scopeName;
|
final String? _scopeName;
|
||||||
final bool _isSubScope;
|
final bool _isSubScope;
|
||||||
bool _scopePushed = false;
|
bool _scopePushed = false;
|
||||||
|
|
||||||
/// Основной (root) и subScope-конструкторы.
|
/// Root and subScope constructors.
|
||||||
GetItAdapter({GetIt? instance, String? scopeName, bool isSubScope = false})
|
GetItAdapter({GetIt? instance, String? scopeName, bool isSubScope = false})
|
||||||
: _scopeName = scopeName,
|
: _scopeName = scopeName,
|
||||||
_isSubScope = isSubScope {
|
_isSubScope = isSubScope {
|
||||||
@@ -23,7 +23,7 @@ class GetItAdapter extends DIAdapter<GetIt> {
|
|||||||
@override
|
@override
|
||||||
void setupDependencies(void Function(GetIt container) registration) {
|
void setupDependencies(void Function(GetIt container) registration) {
|
||||||
if (_isSubScope) {
|
if (_isSubScope) {
|
||||||
// Создаём scope через pushNewScope с init
|
// Create scope via pushNewScope with init
|
||||||
_getIt.pushNewScope(
|
_getIt.pushNewScope(
|
||||||
scopeName: _scopeName,
|
scopeName: _scopeName,
|
||||||
init: (getIt) => registration(getIt),
|
init: (getIt) => registration(getIt),
|
||||||
@@ -41,7 +41,7 @@ class GetItAdapter extends DIAdapter<GetIt> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<T> resolveAsync<T extends Object>({String? named}) async =>
|
Future<T> resolveAsync<T extends Object>({String? named}) async =>
|
||||||
_getIt<T>(instanceName: named);
|
_getIt.getAsync<T>(instanceName: named);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void teardown() {
|
void teardown() {
|
||||||
@@ -92,8 +92,13 @@ class GetItAdapter extends DIAdapter<GetIt> {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case UniversalScenario.register:
|
case UniversalScenario.register:
|
||||||
|
if (bindingMode == UniversalBindingMode.lazySingletonStrategy) {
|
||||||
|
getIt.registerLazySingleton<UniversalService>(
|
||||||
|
() => UniversalServiceImpl(value: 'reg', dependency: null));
|
||||||
|
} else {
|
||||||
getIt.registerSingleton<UniversalService>(
|
getIt.registerSingleton<UniversalService>(
|
||||||
UniversalServiceImpl(value: 'reg', dependency: null));
|
UniversalServiceImpl(value: 'reg', dependency: null));
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case UniversalScenario.named:
|
case UniversalScenario.named:
|
||||||
getIt.registerFactory<UniversalService>(
|
getIt.registerFactory<UniversalService>(
|
||||||
@@ -120,6 +125,17 @@ class GetItAdapter extends DIAdapter<GetIt> {
|
|||||||
instanceName: depName,
|
instanceName: depName,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
case UniversalBindingMode.lazySingletonStrategy:
|
||||||
|
getIt.registerLazySingleton<UniversalService>(
|
||||||
|
() => UniversalServiceImpl(
|
||||||
|
value: depName,
|
||||||
|
dependency: level > 1
|
||||||
|
? getIt<UniversalService>(instanceName: prevDepName)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
instanceName: depName,
|
||||||
|
);
|
||||||
|
break;
|
||||||
case UniversalBindingMode.factoryStrategy:
|
case UniversalBindingMode.factoryStrategy:
|
||||||
getIt.registerFactory<UniversalService>(
|
getIt.registerFactory<UniversalService>(
|
||||||
() => UniversalServiceImpl(
|
() => UniversalServiceImpl(
|
||||||
@@ -154,10 +170,16 @@ 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.registerLazySingleton<UniversalService>(
|
||||||
|
() => getIt<UniversalService>(instanceName: depName),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
getIt.registerSingleton<UniversalService>(
|
getIt.registerSingleton<UniversalService>(
|
||||||
getIt<UniversalService>(instanceName: depName),
|
getIt<UniversalService>(instanceName: depName),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
throw UnsupportedError('Scenario $scenario not supported by GetItAdapter');
|
throw UnsupportedError('Scenario $scenario not supported by GetItAdapter');
|
||||||
|
|||||||
@@ -4,14 +4,11 @@ import 'package:benchmark_di/scenarios/universal_service.dart';
|
|||||||
import 'package:kiwi/kiwi.dart';
|
import 'package:kiwi/kiwi.dart';
|
||||||
import 'di_adapter.dart';
|
import 'di_adapter.dart';
|
||||||
|
|
||||||
/// DIAdapter-для KiwiContainer с поддержкой universal benchmark сценариев.
|
/// DIAdapter for KiwiContainer with universal benchmark scenario support.
|
||||||
class KiwiAdapter extends DIAdapter<KiwiContainer> {
|
class KiwiAdapter extends DIAdapter<KiwiContainer> {
|
||||||
late KiwiContainer _container;
|
late KiwiContainer _container;
|
||||||
// ignore: unused_field
|
|
||||||
final bool _isSubScope;
|
|
||||||
|
|
||||||
KiwiAdapter({KiwiContainer? container, bool isSubScope = false})
|
KiwiAdapter({KiwiContainer? container, bool isSubScope = false}) {
|
||||||
: _isSubScope = isSubScope {
|
|
||||||
_container = container ?? KiwiContainer();
|
_container = container ?? KiwiContainer();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,6 +54,8 @@ class KiwiAdapter extends DIAdapter<KiwiContainer> {
|
|||||||
final depName = '${chain}_$level';
|
final depName = '${chain}_$level';
|
||||||
switch (bindingMode) {
|
switch (bindingMode) {
|
||||||
case UniversalBindingMode.singletonStrategy:
|
case UniversalBindingMode.singletonStrategy:
|
||||||
|
case UniversalBindingMode.lazySingletonStrategy:
|
||||||
|
// Kiwi's registerSingleton is lazy (factory-based, cached on first resolve)
|
||||||
container.registerSingleton<UniversalService>(
|
container.registerSingleton<UniversalService>(
|
||||||
(c) => UniversalServiceImpl(
|
(c) => UniversalServiceImpl(
|
||||||
value: depName,
|
value: depName,
|
||||||
@@ -75,7 +74,7 @@ class KiwiAdapter extends DIAdapter<KiwiContainer> {
|
|||||||
name: depName);
|
name: depName);
|
||||||
break;
|
break;
|
||||||
case UniversalBindingMode.asyncStrategy:
|
case UniversalBindingMode.asyncStrategy:
|
||||||
// Не поддерживается
|
// Not supported
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -97,23 +96,12 @@ class KiwiAdapter extends DIAdapter<KiwiContainer> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
T resolve<T extends Object>({String? named}) {
|
T resolve<T extends Object>({String? named}) {
|
||||||
// Для asyncChain нужен resolve<Future<T>>
|
|
||||||
if (T.toString().startsWith('Future<')) {
|
|
||||||
return _container.resolve<T>(named);
|
return _container.resolve<T>(named);
|
||||||
} else {
|
|
||||||
return _container.resolve<T>(named);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<T> resolveAsync<T extends Object>({String? named}) async {
|
Future<T> resolveAsync<T extends Object>({String? named}) {
|
||||||
if (T.toString().startsWith('Future<')) {
|
|
||||||
// resolve<Future<T>>, unwrap result
|
|
||||||
return Future.value(_container.resolve<T>(named));
|
return Future.value(_container.resolve<T>(named));
|
||||||
} else {
|
|
||||||
// Для совместимости с chain/override
|
|
||||||
return Future.value(_container.resolve<T>(named));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -123,7 +111,7 @@ class KiwiAdapter extends DIAdapter<KiwiContainer> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
KiwiAdapter openSubScope(String name) {
|
KiwiAdapter openSubScope(String name) {
|
||||||
// Возвращаем новый scoped контейнер (отдельный). Наследование не реализовано.
|
// Returns a new scoped container. Inheritance not implemented.
|
||||||
return KiwiAdapter(container: KiwiContainer.scoped(), isSubScope: true);
|
return KiwiAdapter(container: KiwiContainer.scoped(), isSubScope: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,8 +29,7 @@ class YxScopeAdapter extends DIAdapter<UniversalYxScopeContainer> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void teardown() {
|
void teardown() {
|
||||||
// У yx_scope нет явного dispose на ScopeContainer, но можно добавить очистку Map/Deps если потребуется
|
_scope = UniversalYxScopeContainer();
|
||||||
// Ничего не делаем
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -54,26 +53,13 @@ class YxScopeAdapter extends DIAdapter<UniversalYxScopeContainer> {
|
|||||||
required UniversalBindingMode bindingMode,
|
required UniversalBindingMode bindingMode,
|
||||||
}) {
|
}) {
|
||||||
if (scenario is UniversalScenario) {
|
if (scenario is UniversalScenario) {
|
||||||
|
if (scenario == UniversalScenario.asyncChain ||
|
||||||
|
bindingMode == UniversalBindingMode.asyncStrategy) {
|
||||||
|
throw UnsupportedError(
|
||||||
|
'YxScope does not support async dependencies or async binding scenarios.');
|
||||||
|
}
|
||||||
return (scope) {
|
return (scope) {
|
||||||
switch (scenario) {
|
switch (scenario) {
|
||||||
case UniversalScenario.asyncChain:
|
|
||||||
for (int chain = 1; chain <= chainCount; chain++) {
|
|
||||||
for (int level = 1; level <= nestingDepth; level++) {
|
|
||||||
final prevDepName = '${chain}_${level - 1}';
|
|
||||||
final depName = '${chain}_$level';
|
|
||||||
final dep = scope.dep<UniversalService>(
|
|
||||||
() => UniversalServiceImpl(
|
|
||||||
value: depName,
|
|
||||||
dependency: level > 1
|
|
||||||
? scope.depFor<UniversalService>(name: prevDepName).get
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
name: depName,
|
|
||||||
);
|
|
||||||
scope.register<UniversalService>(dep, name: depName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case UniversalScenario.register:
|
case UniversalScenario.register:
|
||||||
final dep = scope.dep<UniversalService>(
|
final dep = scope.dep<UniversalService>(
|
||||||
() => UniversalServiceImpl(value: 'reg', dependency: null),
|
() => UniversalServiceImpl(value: 'reg', dependency: null),
|
||||||
@@ -113,6 +99,8 @@ class YxScopeAdapter extends DIAdapter<UniversalYxScopeContainer> {
|
|||||||
case UniversalScenario.override:
|
case UniversalScenario.override:
|
||||||
// handled at benchmark level
|
// handled at benchmark level
|
||||||
break;
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
if (scenario == UniversalScenario.chain ||
|
if (scenario == UniversalScenario.chain ||
|
||||||
scenario == UniversalScenario.override) {
|
scenario == UniversalScenario.override) {
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
/// Enum to represent the DI registration/binding mode.
|
/// Enum to represent the DI registration/binding mode.
|
||||||
enum UniversalBindingMode {
|
enum UniversalBindingMode {
|
||||||
/// Singleton/provider binding.
|
/// Eager singleton — instance created at registration time.
|
||||||
singletonStrategy,
|
singletonStrategy,
|
||||||
|
|
||||||
/// Factory-based binding.
|
/// Lazy singleton — instance created on first resolve, then cached.
|
||||||
|
lazySingletonStrategy,
|
||||||
|
|
||||||
|
/// Factory-based binding — new instance every time.
|
||||||
factoryStrategy,
|
factoryStrategy,
|
||||||
|
|
||||||
/// Async-based binding.
|
/// Async-based binding.
|
||||||
|
|||||||
@@ -11,6 +11,8 @@
|
|||||||
// 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}
|
||||||
@@ -69,6 +71,8 @@ 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;
|
||||||
@@ -191,10 +195,9 @@ 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(Instance<T> value) {
|
Binding<T> toInstance(FutureOr<T> value) {
|
||||||
_resolver = InstanceResolver<T>(value);
|
_resolver = InstanceResolver.create<T>(value);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,8 +208,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(Provider<T> value) {
|
Binding<T> toProvide(FutureOr<T> Function() value) {
|
||||||
_resolver = ProviderResolver<T>((_) => value.call(), withParams: false);
|
_resolver = ProviderResolver.create<T>(value);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,24 +219,32 @@ class Binding<T> {
|
|||||||
/// ```dart
|
/// ```dart
|
||||||
/// bind<User>().toProvideWithParams((params) => User(name: params["name"]));
|
/// bind<User>().toProvideWithParams((params) => User(name: params["name"]));
|
||||||
/// ```
|
/// ```
|
||||||
Binding<T> toProvideWithParams(ProviderWithParams<T> value) {
|
Binding<T> toProvideWithParams(FutureOr<T> Function(dynamic) value) {
|
||||||
_resolver = ProviderResolver<T>(value, withParams: true);
|
_resolver = ProviderResolver.createWithParams<T>(value);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Deprecated('Use toInstance instead of toInstanceAsync')
|
@Deprecated('Use toInstance instead of toInstanceAsync')
|
||||||
Binding<T> toInstanceAsync(Instance<T> value) {
|
Binding<T> toInstanceAsync(FutureOr<T> value) {
|
||||||
return toInstance(value);
|
return toInstance(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Deprecated('Use toProvide instead of toProvideAsync')
|
/// Asynchronous variant of [toProvide] for providers that return [Future<T>].
|
||||||
Binding<T> toProvideAsync(Provider<T> value) {
|
///
|
||||||
return toProvide(value);
|
/// Prefer this over [toProvide] when the provider is async, so the resolver
|
||||||
|
/// is type-safe and avoids runtime detection overhead.
|
||||||
|
Binding<T> toProvideAsync(AsyncProviderFactory<T> value) {
|
||||||
|
_resolver = ProviderResolver.async<T>(value);
|
||||||
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Deprecated('Use toProvideWithParams instead of toProvideAsyncWithParams')
|
/// Asynchronous variant of [toProvideWithParams] for providers that return [Future<T>].
|
||||||
Binding<T> toProvideAsyncWithParams(ProviderWithParams<T> value) {
|
///
|
||||||
return toProvideWithParams(value);
|
/// Prefer this over [toProvideWithParams] when the provider is async, so the resolver
|
||||||
|
/// 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).
|
||||||
@@ -281,6 +292,10 @@ 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()}',
|
||||||
@@ -313,6 +328,10 @@ 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(
|
||||||
|
|||||||
@@ -13,86 +13,37 @@
|
|||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
/// Represents a direct instance or an async instance ([T] or [Future<T>]).
|
/// Synchronous factory: `T Function()`.
|
||||||
/// Used for both direct and async bindings.
|
typedef ProviderFactory<T> = T Function();
|
||||||
///
|
|
||||||
/// Example:
|
|
||||||
/// ```dart
|
|
||||||
/// Instance<String> sync = "hello";
|
|
||||||
/// Instance<MyApi> async = Future.value(MyApi());
|
|
||||||
/// ```
|
|
||||||
typedef Instance<T> = FutureOr<T>;
|
|
||||||
|
|
||||||
/// Provider function type for synchronous or asynchronous, parameterless creation of [T].
|
/// Parameterized synchronous factory: `T Function(dynamic)`.
|
||||||
/// Can return [T] or [Future<T>].
|
typedef ProviderFactoryWithParams<T> = T Function(dynamic);
|
||||||
///
|
|
||||||
/// Example:
|
|
||||||
/// ```dart
|
|
||||||
/// Provider<MyService> provider = () => MyService();
|
|
||||||
/// Provider<Api> asyncProvider = () async => await Api.connect();
|
|
||||||
/// ```
|
|
||||||
typedef Provider<T> = FutureOr<T> Function();
|
|
||||||
|
|
||||||
/// Provider function type that accepts a dynamic parameter, for factory/parametrized injection.
|
/// Asynchronous factory: `Future<T> Function()`.
|
||||||
/// Returns [T] or [Future<T>].
|
typedef AsyncProviderFactory<T> = Future<T> Function();
|
||||||
///
|
|
||||||
/// Example:
|
|
||||||
/// ```dart
|
|
||||||
/// ProviderWithParams<User> provider = (params) => User(params["name"]);
|
|
||||||
/// ```
|
|
||||||
typedef ProviderWithParams<T> = FutureOr<T> Function(dynamic);
|
|
||||||
|
|
||||||
/// Abstract interface for dependency resolvers used by [Binding].
|
/// Parameterized asynchronous factory: `Future<T> Function(dynamic)`.
|
||||||
/// Defines how to resolve instances of type [T].
|
typedef AsyncProviderFactoryWithParams<T> = Future<T> Function(dynamic);
|
||||||
///
|
|
||||||
/// You usually don't use this directly; it's used internally for advanced/low-level DI.
|
/// Internal interface for resolvers managed by [Binding].
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Concrete resolver for direct instance ([T] or [Future<T>]). No provider is called.
|
/// Resolver for a pre-built synchronous instance.
|
||||||
///
|
class SyncInstanceResolver<T> implements BindingResolver<T> {
|
||||||
/// Used for [Binding.toInstance].
|
final T _instance;
|
||||||
/// 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;
|
|
||||||
|
|
||||||
/// Wraps the given instance (sync or async) in a resolver.
|
SyncInstanceResolver(this._instance);
|
||||||
InstanceResolver(this._instance);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
T resolveSync([_]) {
|
T resolveSync([_]) => _instance;
|
||||||
if (_instance is T) return _instance;
|
|
||||||
throw StateError(
|
|
||||||
'Instance $_instance is Future; '
|
|
||||||
'use resolveAsync() instead',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<T> resolveAsync([_]) {
|
Future<T> resolveAsync([_]) => Future<T>.value(_instance);
|
||||||
if (_instance is Future<T>) return _instance;
|
|
||||||
return Future.value(_instance);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void toSingleton() {}
|
void toSingleton() {}
|
||||||
@@ -101,63 +52,31 @@ class InstanceResolver<T> implements BindingResolver<T> {
|
|||||||
bool get isSingleton => true;
|
bool get isSingleton => true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolver for provider functions (sync/async/factory), with optional singleton caching.
|
/// Resolver for a pre-built async instance ([Future]).
|
||||||
/// Used for [Binding.toProvide], [Binding.toProvideWithParams], [Binding.singleton].
|
class AsyncInstanceResolver<T> implements BindingResolver<T> {
|
||||||
///
|
final Future<T> _instance;
|
||||||
/// 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;
|
AsyncInstanceResolver(this._instance);
|
||||||
|
|
||||||
|
@override
|
||||||
|
T resolveSync([_]) {
|
||||||
|
throw StateError('Instance is a Future; use resolveAsync() instead');
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<T> resolveAsync([_]) => _instance;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void toSingleton() {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get isSingleton => true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Base class for provider-based resolvers with singleton flag support.
|
||||||
|
abstract class _BaseProviderResolver<T> implements BindingResolver<T> {
|
||||||
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;
|
||||||
@@ -165,13 +84,282 @@ class ProviderResolver<T> implements BindingResolver<T> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool get isSingleton => _singleton;
|
bool get isSingleton => _singleton;
|
||||||
|
}
|
||||||
|
|
||||||
/// Throws if params required but not supplied.
|
/// Resolves [Binding.toProvide] with a sync `T Function()` provider.
|
||||||
void _checkParams(dynamic params) {
|
class SyncProviderResolver<T> extends _BaseProviderResolver<T> {
|
||||||
if (_withParams && params == null) {
|
final ProviderFactory<T> _provider;
|
||||||
|
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(
|
throw StateError(
|
||||||
'[$T] Params is null. Maybe you forgot to pass it?',
|
'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(
|
||||||
|
'Provider returns Future<$T>. Use resolveAsync() instead.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ 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.
|
||||||
@@ -60,6 +59,13 @@ 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();
|
||||||
|
|
||||||
@@ -71,6 +77,7 @@ 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'}',
|
||||||
@@ -82,10 +89,11 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final Set<Module> _modulesList = HashSet();
|
final Set<Module> _modulesList = HashSet();
|
||||||
|
|
||||||
// индекс для мгновенного поиска binding’ов
|
// index for fast binding lookup
|
||||||
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.
|
||||||
@@ -119,6 +127,7 @@ 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: {
|
||||||
@@ -130,6 +139,7 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return _scopeMap[name]!;
|
return _scopeMap[name]!;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,6 +159,7 @@ 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',
|
||||||
@@ -161,6 +172,7 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
_scopeMap.remove(name);
|
_scopeMap.remove(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,13 +187,14 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
|||||||
/// ```
|
/// ```
|
||||||
Scope installModules(List<Module> modules) {
|
Scope installModules(List<Module> modules) {
|
||||||
_modulesList.addAll(modules);
|
_modulesList.addAll(modules);
|
||||||
if (modules.isNotEmpty) {
|
if (!_isSilentObserver && 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: {
|
||||||
@@ -191,14 +204,17 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
|||||||
'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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_rebuildResolversIndex();
|
_addModuleToIndex(module);
|
||||||
|
}
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,12 +228,13 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
|||||||
/// testScope.dropModules();
|
/// testScope.dropModules();
|
||||||
/// ```
|
/// ```
|
||||||
Scope dropModules() {
|
Scope dropModules() {
|
||||||
if (_modulesList.isNotEmpty) {
|
if (!_isSilentObserver && _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: {
|
||||||
@@ -226,6 +243,7 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
|||||||
'description': 'modules dropped',
|
'description': 'modules dropped',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
}
|
||||||
_modulesList.clear();
|
_modulesList.clear();
|
||||||
_rebuildResolversIndex();
|
_rebuildResolversIndex();
|
||||||
return this;
|
return this;
|
||||||
@@ -242,6 +260,16 @@ 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) {
|
||||||
@@ -315,6 +343,12 @@ 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, () {
|
||||||
@@ -358,6 +392,21 @@ 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 {
|
||||||
@@ -413,6 +462,17 @@ 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 {
|
||||||
@@ -441,12 +501,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>(
|
Future<T?> _tryResolveAsyncInternal<T>({String? named, dynamic params}) {
|
||||||
{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.
|
||||||
@@ -454,23 +514,27 @@ 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>?;
|
||||||
|
|
||||||
/// Rebuilds the internal index of all [BindingResolver]s from installed modules.
|
void _addModuleToIndex(Module module) {
|
||||||
/// Called after [installModules] and [dropModules]. Internal use only.
|
|
||||||
void _rebuildResolversIndex() {
|
|
||||||
_bindingResolvers.clear();
|
|
||||||
for (var module in _modulesList) {
|
|
||||||
for (var binding in module.bindingSet) {
|
for (var binding in module.bindingSet) {
|
||||||
_bindingResolvers.putIfAbsent(binding.key, () => {});
|
_bindingResolvers.putIfAbsent(binding.key, () => {});
|
||||||
final nameKey = binding.isNamed ? binding.name : null;
|
final nameKey = binding.isNamed ? binding.name : null;
|
||||||
_bindingResolvers[binding.key]![nameKey] = binding.resolver!;
|
_bindingResolvers[binding.key]![nameKey] = binding.resolver!;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Rebuilds the internal index of all [BindingResolver]s from installed modules.
|
||||||
|
/// Called after [dropModules]. Internal use only.
|
||||||
|
void _rebuildResolversIndex() {
|
||||||
|
_bindingResolvers.clear();
|
||||||
|
for (var module in _modulesList) {
|
||||||
|
_addModuleToIndex(module);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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 && !_disposables.contains(obj)) {
|
if (obj is Disposable) {
|
||||||
_disposables.add(obj);
|
_disposables.add(obj);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -487,14 +551,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 scopesCopy = Map<String, Scope>.from(_scopeMap);
|
final scopes = _scopeMap.values.toList();
|
||||||
for (final subScope in scopesCopy.values) {
|
for (final subScope in scopes) {
|
||||||
await subScope.dispose();
|
await subScope.dispose();
|
||||||
}
|
}
|
||||||
_scopeMap.clear();
|
_scopeMap.clear();
|
||||||
|
|
||||||
final disposablesCopy = Set<Disposable>.from(_disposables);
|
final disposables = _disposables.toList();
|
||||||
for (final d in disposablesCopy) {
|
for (final d in disposables) {
|
||||||
await d.dispose();
|
await d.dispose();
|
||||||
}
|
}
|
||||||
_disposables.clear();
|
_disposables.clear();
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:cherrypick/cherrypick.dart';
|
import 'package:cherrypick/cherrypick.dart';
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
@@ -12,7 +14,8 @@ 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<InstanceResolver<int>>());
|
expect(binding.resolver, isA<BindingResolver<int>>());
|
||||||
|
expect(binding.resolver, isA<SyncInstanceResolver<int>>());
|
||||||
});
|
});
|
||||||
|
|
||||||
test('isSingleton is true', () {
|
test('isSingleton is true', () {
|
||||||
@@ -34,7 +37,8 @@ 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<InstanceResolver<int>>());
|
expect(binding.resolver, isA<BindingResolver<int>>());
|
||||||
|
expect(binding.resolver, isA<SyncInstanceResolver<int>>());
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Sets key', () {
|
test('Sets key', () {
|
||||||
@@ -73,7 +77,8 @@ 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<InstanceResolver<int>>());
|
expect(binding.resolver, isA<BindingResolver<int>>());
|
||||||
|
expect(binding.resolver, isA<AsyncInstanceResolver<int>>());
|
||||||
});
|
});
|
||||||
|
|
||||||
test('isSingleton is true after toInstanceAsync', () {
|
test('isSingleton is true after toInstanceAsync', () {
|
||||||
@@ -107,7 +112,8 @@ 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<ProviderResolver<int>>());
|
expect(binding.resolver, isA<BindingResolver<int>>());
|
||||||
|
expect(binding.resolveSync(), 5);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('isSingleton is false by default', () {
|
test('isSingleton is false by default', () {
|
||||||
@@ -129,7 +135,8 @@ 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<ProviderResolver<int>>());
|
expect(binding.resolver, isA<BindingResolver<int>>());
|
||||||
|
expect(binding.resolveSync(), 5);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Sets key', () {
|
test('Sets key', () {
|
||||||
@@ -166,6 +173,43 @@ 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 ---
|
||||||
@@ -232,6 +276,103 @@ 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', () {
|
||||||
|
|||||||
153
cherrypick/test/src/scope_fast_path_test.dart
Normal file
153
cherrypick/test/src/scope_fast_path_test.dart
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -64,10 +64,14 @@
|
|||||||
- **WHEN** метод помечен одновременно `@instance` и `@provide`
|
- **WHEN** метод помечен одновременно `@instance` и `@provide`
|
||||||
- **THEN** генератор завершает сборку с ошибкой валидации
|
- **THEN** генератор завершает сборку с ошибкой валидации
|
||||||
|
|
||||||
#### Scenario: Требования к @named
|
#### Scenario: Требования к @named на provider-методе
|
||||||
- **WHEN** `@named` использует пустую строку или некорректный идентификатор
|
- **WHEN** `@named` на provider-методе использует пустую строку или некорректный идентификатор
|
||||||
- **THEN** генератор завершает сборку с ошибкой валидации
|
- **THEN** генератор завершает сборку с ошибкой валидации
|
||||||
|
|
||||||
|
#### Scenario: Пустой @named на inject-поле
|
||||||
|
- **WHEN** `@named('')` указан на поле с `@inject`
|
||||||
|
- **THEN** генератор трактует поле как безымянный резолв (без параметра `named`)
|
||||||
|
|
||||||
#### Scenario: Валидность @module
|
#### Scenario: Валидность @module
|
||||||
- **WHEN** класс с `@module` не имеет публичных методов
|
- **WHEN** класс с `@module` не имеет публичных методов
|
||||||
- **THEN** генератор завершает сборку с ошибкой валидации
|
- **THEN** генератор завершает сборку с ошибкой валидации
|
||||||
|
|||||||
@@ -23,11 +23,11 @@
|
|||||||
- **THEN** subscope удаляется из дерева, а связанные ресурсы освобождаются
|
- **THEN** subscope удаляется из дерева, а связанные ресурсы освобождаются
|
||||||
|
|
||||||
#### Scenario: Путь scope и разделитель
|
#### Scenario: Путь scope и разделитель
|
||||||
- **WHEN** scope открывается по иерархическому пути с разделителем
|
- **WHEN** вызывается `CherryPick.openScope(scopeName: ..., separator: ...)` с иерархическим путем
|
||||||
- **THEN** создается цепочка subscopes по каждому сегменту пути
|
- **THEN** создается цепочка subscopes по каждому сегменту пути
|
||||||
|
|
||||||
#### Scenario: Пустой scopeName
|
#### Scenario: Пустой scopeName
|
||||||
- **WHEN** scope открывается с пустым именем
|
- **WHEN** вызывается `CherryPick.openScope(scopeName: '')`
|
||||||
- **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: ResolveSync для async‑инстанса
|
#### Scenario: Синхронный резолв для async‑binding
|
||||||
- **WHEN** binding зарегистрирован как async‑инстанс или async‑provider, а вызывается `resolveSync`
|
- **WHEN** binding зарегистрирован как async‑инстанс или async‑provider, а вызывается `resolve<T>()` или `tryResolve<T>()`
|
||||||
- **THEN** выбрасывается ошибка с указанием использовать async‑резолв
|
- **THEN** выбрасывается ошибка с указанием использовать async‑резолв
|
||||||
|
|
||||||
### Requirement: Управление Disposable
|
### Requirement: Управление Disposable
|
||||||
@@ -111,7 +111,7 @@
|
|||||||
- **THEN** observer получает соответствующие уведомления
|
- **THEN** observer получает соответствующие уведомления
|
||||||
|
|
||||||
### Requirement: Ошибки и сообщения об ошибках
|
### Requirement: Ошибки и сообщения об ошибках
|
||||||
При критических сбоях резолва ядро MUST выбрасывать ошибку с понятным сообщением, а для tryResolve MUST не бросать исключения.
|
При критических сбоях резолва ядро MUST выбрасывать ошибку с понятным сообщением. Для отсутствующей зависимости `tryResolve`/`tryResolveAsync` MUST возвращать `null` без исключения; ошибки выполнения резолва (например, цикл, sync/async mismatch, отсутствие обязательных params) MAY быть проброшены.
|
||||||
|
|
||||||
#### Scenario: Ошибка отсутствующей зависимости
|
#### Scenario: Ошибка отсутствующей зависимости
|
||||||
- **WHEN** вызывается `resolve<T>()` для незарегистрированной зависимости
|
- **WHEN** вызывается `resolve<T>()` для незарегистрированной зависимости
|
||||||
|
|||||||
@@ -41,14 +41,14 @@ Flutter‑интеграция MUST предоставлять `CherryPickProvid
|
|||||||
|
|
||||||
#### Scenario: Ошибка lookup
|
#### Scenario: Ошибка lookup
|
||||||
- **WHEN** вызов происходит вне поддерева провайдера
|
- **WHEN** вызов происходит вне поддерева провайдера
|
||||||
- **THEN** происходит assertion‑ошибка
|
- **THEN** в debug‑режиме происходит assertion‑ошибка
|
||||||
|
|
||||||
### Requirement: Ошибки и сообщения
|
### Requirement: Ошибки и сообщения
|
||||||
При отсутствии провайдера в дереве MUST быть диагностируемая ошибка.
|
При отсутствии провайдера в дереве MUST быть диагностируемая ошибка.
|
||||||
|
|
||||||
#### Scenario: Диагностика отсутствия провайдера
|
#### Scenario: Диагностика отсутствия провайдера
|
||||||
- **WHEN** `CherryPickProvider.of(context)` не находит провайдер
|
- **WHEN** `CherryPickProvider.of(context)` не находит провайдер
|
||||||
- **THEN** сообщение об ошибке указывает на отсутствие провайдера
|
- **THEN** в debug‑режиме сообщение assertion указывает на отсутствие провайдера
|
||||||
|
|
||||||
### Requirement: Точки расширения
|
### Requirement: Точки расширения
|
||||||
Flutter‑интеграция MUST позволять использовать собственные DI‑scope стратегии поверх `CherryPickProvider`.
|
Flutter‑интеграция MUST позволять использовать собственные DI‑scope стратегии поверх `CherryPickProvider`.
|
||||||
|
|||||||
Reference in New Issue
Block a user