Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions FIX-PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@

_Derived from the 2026-07-10 three-part review (architecture · tests · performance). Every finding below was verified against source with file:line. Work items are self-contained and ordered so they can be implemented one at a time._

## Progress (2026-07-12)
## Progress (2026-07-13)
- **Done & merged:** B1, B2, B4, B5, B9 + T1, T2 (PR #8) · BindingContext test (#10) · D3 namespace typo (#11) · T3 DI integration tests (#12) · solution rename (#13) · **A2 naming → ExistForAll (#15)** · **P0 benchmark harness (#16)** · **P1 provider cache + C3 decided/implemented (#17)** · **P2 memoize `ExtractTypeProperties` + `HashSet` dedup (#18)** · **docs tutorials refresh (#20)** · **Q1–Q4 perf quick wins + M1 collision fix + micro-benchmarks (#21)**.
- **Q1–Q4 proven** via isolated micro-benchmarks (macro `ScanBenchmark` can't resolve them): Q1 2.7× / 64 KB→88 B · Q3 2.65× / 152 B→0 · Q4 32× / 224 B→0. **Q5 was already resolved by B4.** **M1** (code-review finding): namespace-qualify the generated impl name in the generator only — `GetNormalizeInterfaceName` also backs the section name. Suite → **56 per TFM**.
- **Merged since:** **benchmark-tracking CI (#22)** — BDN on push/PR, gates PRs on **allocation** regressions (>10%) via github-action-benchmark on `gh-pages`; time informational. · **session-wrap docs (#23)**.
- **In flight:** **P3cached "settings plan"** (`SettingsPlan` per type: section name resolved once + lazily, per-property key/default/converter precomputed, `[SettingsProperty]` read **once** per property). Warm re-populate **−55–61%** allocations (50 props 15,681→6,816 B); gated `ScanBenchmark` **≈flat (−0.4%)** — after a code+perf review pass, consolidating the 3× attribute read more than paid for the plan objects. The emitted/compiled setter was built and measured but **reverted** — it regressed the gated cold scan **+25%** for **no** warm gain (net10 reflective `SetValue` is already allocation-free for the args). A new gated `PlanPopulateBenchmark` tracks the warm path.
- **Next:** P4 (de-reflect array/enumerable converters) → P5 (resolve config section once per type).
- **Merged since:** **benchmark-tracking CI (#22)** — BDN on push/PR, gates PRs on **allocation** regressions (>10%) via github-action-benchmark on `gh-pages`; time informational. · **session-wrap docs (#23)** · **P3 — cached "settings plan" (#24)** — `SettingsPlan` per type (section name once+lazy, key/default/converter precomputed, `[SettingsProperty]` read once). Warm re-populate **−55–61%** (50 props 15,681→6,816 B); gated `ScanBenchmark` **≈flat (−0.4%)**. Reviewed by code+perf agents; emitted/compiled setter reverted (regressed the gated cold scan for **no** warm gain — net10 `SetValue` is already alloc-free). New gated `PlanPopulateBenchmark` tracks the warm path.
- **In flight:** **P4de-reflect + DRY the array/enumerable converters** = **PR #25 open, code-reviewed clean** (branch `perf/p4-dereflect-converters`; awaiting CI + merge). New shared `CollectionTypeConverter` builds results via `Array.CreateInstance` + indexed fill and selects the element converter by walking the concrete `LinkedList` (struct enumerator) — no `List<T>`+reflected `Enumerable.ToArray`, no `First` closure. `TypeConverter.CreateNullResult` de-reflected too (`Array.CreateInstance(t,0)` for `Enumerable.Empty<T>()`). Proven via new gated `ConvertArrayBenchmark`: **1.33 KB→688 B (−49%), 1,247→219 ns (5.7×)**. Suite **68 per TFM** (+12 collection-converter parity tests). Branch also carries the pre-P4 doc refresh + post-P3 style tweaks to `TypeConverter.cs`/`TypeExtensions.cs`.
- **Next:** P5 (resolve config section once per type) → optional P3b (tiered/lazy compiled setter, only if set *time* shows in a profile).
- **C3 — DECIDED (option 2):** cache in the provider only; Core `SettingsBuilder.GetSettings` unchanged; no reload. See #17.
- **Held — do NOT delete (feature work coming):** D1 Validations (reconcile with the `validate-settings` branch) · D2 EqualityCompererCreator.
- Running status lives in `SESSION-HANDOFF.md`.
Expand Down Expand Up @@ -58,7 +58,7 @@ _Derived from the 2026-07-10 three-part review (architecture · tests · perform
- [x] P2 · Memoize `ExtractTypeProperties` + fix O(n²) dedup · Sev High · Eff S
- [x] P3 · Cached “settings plan” — hoist section (lazy) + keys, precompute/cache converters, plan per type. Reflective `SetValue` kept; compiled setter deferred (regressed the gated cold scan for no warm gain) · Sev High · Eff L
- [x] Q1–Q5 · Quick wins (GetEnumerator, OrdinalIgnoreCase, env-binder, type-cache; Q5 dead ctor checks already done by B4)
- [ ] P4 · De-reflect array/enumerable converters · Sev Med · Eff M
- [x] P4 · De-reflect + DRY array/enumerable converters (shared `CollectionTypeConverter`; `Array.CreateInstance` + manual converter walk; `CreateNullResult` de-reflected) — **1.33 KB→688 B, 5.7×**; branch `perf/p4-dereflect-converters` · Sev Med · Eff M
- [ ] P5 · Resolve config section once per type, not per property · Sev Med · Eff M

**Phase 6 — Architecture strategy**
Expand Down Expand Up @@ -220,8 +220,9 @@ The populate loop (`src/Core/ExistForAll.SimpleSettings/ValuesPopulator.cs:36-55
- **Q4** `SettingsClassGenerator.cs:37` string-keyed assembly `GetType(...Replace("+"…))` per generate → `Dictionary<Type, Type>` cache.
- **Q5** `BindingContext.cs:31-32` dead null-checks per allocation (also covered by B4).

### P4 · De-reflect array/enumerable converters — Sev Med · Eff M
`TypeConverter.cs:22-23` (empty enumerable via `Enumerable.Empty` `MakeGenericMethod().Invoke()`), `ArrayTypeConverter.cs:40,48-52` (`Activator.CreateInstance(List<>)` + reflected `ToArray`), `EnumerableTypeConverter.cs:40`. Use `Array.Empty<T>()` factories, `Array.CreateInstance(elementType, n)` + indexed assignment, and cache any unavoidable `MethodInfo`/generic instantiations per element type. (`ArrayTypeConverter`/`EnumerableTypeConverter` are near-duplicates — DRY them here.)
### P4 · De-reflect + DRY array/enumerable converters — Sev Med · Eff M · **DONE (branch `perf/p4-dereflect-converters`)**
Was: `TypeConverter.cs` (empty enumerable via `Enumerable.Empty` `MakeGenericMethod().Invoke()`), `ArrayTypeConverter` (`Activator.CreateInstance(List<>)` + reflected `Enumerable.ToArray` `Invoke`), `EnumerableTypeConverter` (`Activator.CreateInstance(List<>)`), both selecting the element converter with LINQ `First` (boxes the `LinkedList` enumerator + a closure).
Now: a shared `CollectionTypeConverter` base implements `Convert` once — normalize the value to an array (split delimited string / passthrough / wrap scalar), select the element converter by walking the concrete `LinkedList` (struct enumerator, no boxing/closure), then fill an `Array.CreateInstance(elementType, n)` by index. `ArrayTypeConverter`/`EnumerableTypeConverter` are now thin subclasses differing only in `CanConvert` + element-type extraction; both return `T[]` (safe — `IsEnumerable()` matches only `IEnumerable<T>`, which a `T[]` satisfies). `CreateNullResult` uses `Array.CreateInstance(t,0)` instead of the `Enumerable.Empty<T>()` reflection. **Proof (`ConvertArrayBenchmark`, gated): 1.33 KB→688 B (−49%), 1,247→219 ns (5.7×).** 12 parity tests in `Conversion/CollectionConversionTests.cs` (int/string/enum/DateTime/Uri elements, empty-entry removal, custom delimiter, default passthrough, unbound→empty `T[]`, `T[]`-not-`List` guard, bad-element negative). Residual 688 B = irreducible split-substrings + element boxing + result array (shared by old & new). **Code-reviewed clean** (dotnet code-reviewer verified all parity claims; the enum/DateTime/Uri + null-path + negative tests were its suggestions — partially closes T6).

### P5 · Resolve config section once per type — Sev Med · Eff M
`ConfigurationBinder.BindPropertySettings` (`ConfigurationBinder.cs:25-33`) calls `_configuration.GetSection(...)` **per property**; the section is constant per type. Resolve the `IConfigurationSection` once per (type, section) — pass section context via the plan (P3) or cache per section string. Touches the binder/context contract.
Expand Down
Loading
Loading