Skip to content

PR-M3: Persist viewer panel layout in ViewerSettings#129

Merged
philliphoff merged 5 commits into
mainfrom
philliphoff/pr-m3-layout-persistence
May 28, 2026
Merged

PR-M3: Persist viewer panel layout in ViewerSettings#129
philliphoff merged 5 commits into
mainfrom
philliphoff/pr-m3-layout-persistence

Conversation

@philliphoff
Copy link
Copy Markdown
Owner

Persists viewer panel layout across restarts: per-dock visibility (left/right/bottom), selected tab per dock (right + bottom; left dock keeps existing LastSelectedActivity semantics with Settings-tab exemption), and panel sizes (three outer docks + Datasets/Catalog inner splitters).

Highlights

  • ViewerSettings: new IsLeftDockOpen (default true), IsRightDockOpen/IsBottomDockOpen (default false), LastSelectedRightTab, LastSelectedBottomTab, Panels (new PanelSizes class with nullable size fields). LastSelectedActivity kept for back-compat (XML doc clarifies it is the left dock).
  • MainViewModel: hydrates dock visibility + selected tab + panel sizes in constructor before WireAutoOpenSubscriptions; stale persisted ids fall back to the first registered tab of that dock. Stores _settingsInitialized flag so setters only persist after hydration. Adds OnShutdown() that flushes the size-saver.
  • CollapsibleColumn / CollapsibleRow: new SavedWidth / SavedHeight attached properties (two-way). Lazy PropertyChanged subscription pushes user-driven changes back to the view model.
  • SplitterPersistence: new attached SavedFraction (0–1) on inner GridSplitters; rewrites the two adjacent star definitions preserving their total star factor.
  • DebouncedSettingsSaver: 500 ms System.Threading.Timer, RequestSave / Flush / Dispose. UI dispatch optional (omitted in tests). Save action wrapped in try/catch so I/O failures don't crash the viewer. MainWindow.Closed calls OnShutdownFlush so the last drag is never lost.
  • Restore-immediately precedence: persisted IsRightDockOpen=true opens the dock at startup without waiting for the next content signal; PR-M4 auto-open events still fire but are no-ops when the dock is already open.

Tests

Viewer test count: 346 → 368 (+22).

  • DebouncedSettingsSaverTests (6): single fire, coalesce, Flush, no-op Flush, Dispose flushes, swallows exceptions.
  • LayoutPersistenceTests (16): default visibility (3), restore from settings (3), toggle writes back, tab restore + stale-id fallback (3), user selection writes back, restore-immediately precedence vs. auto-open, panel-size hydration, inner-split persistence (2), OnShutdown doesn't throw.

Decisions / notes

  • Kept LastSelectedActivity name (no rename) — avoids back-compat read+write code; new XML doc clarifies it's the left dock.
  • Timer mechanism: System.Threading.Timer (testable, no UI-thread coupling). UI dispatcher (Dispatcher.UIThread.Post) is passed in by MainViewModel.
  • Inner splitter persistence model: single double fraction [0, 1] of the previous row/column relative to the sum of two adjacent star factors; preserves the original star total when rewriting.
  • CollapsibleColumn / CollapsibleRow already maintained RememberedWidth / RememberedHeight for collapse/restore. The new SavedWidth / SavedHeight are independent two-way bindings; they observe Width/Height changes via PropertyChanged and only write back when the value is absolute and meaningfully changed (> 0.5 px).
  • Unreachable-code warnings in SplitterPersistence.FindRows/FindColumns are benign (loop body always breaks); left as-is to keep the loop shape readable.

Out of scope (defer markers in code)

  • TODO PR-M-future: persist dataset-panel inner tab selection (validation vs. properties).
  • TODO PR-M-future: persist activity-bar collapsed state.

Acceptance

  • dotnet build EncDotNet.S100.slnx -c Release — clean (2 pre-existing warnings, none new in changed projects).
  • dotnet test tests/EncDotNet.S100.Viewer.Tests/ -c Release — 368 passed, 0 failed.

Closes the PR-M3 TODOs left at MainViewModel.cs (PR-M4 markers on the three IsXDockOpen setters).

- Add IsLeftDockOpen/IsRightDockOpen/IsBottomDockOpen,
  LastSelectedRightTab, LastSelectedBottomTab, and PanelSizes
  to ViewerSettings.
- Restore dock visibility, selected tabs, and panel sizes on
  startup; user-driven tab selection writes back to settings.
- Add CollapsibleColumn.SavedWidth / CollapsibleRow.SavedHeight
  attached properties that two-way bind dock widths/height to
  the view model.
- Add SplitterPersistence.SavedFraction attached property for
  inner GridSplitter star ratios (Datasets / Catalog tabs).
- Add DebouncedSettingsSaver (500 ms, System.Threading.Timer)
  to coalesce rapid splitter drags into a single disk write;
  MainWindow.Closed flushes pending writes on shutdown.
- Restore-immediately precedence: persisted IsRightDockOpen=true
  opens the dock at startup without waiting for content signals.
- Settings tab exemption preserved (Left dock only).
- 22 new tests (12 layout persistence + 6 debouncer + earlier
  baseline). Viewer test count 346 → 368.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 28, 2026

Performance Gate

PASSED — no regressions.

Threshold: 10.0%, MAD multiplier (k): 3.0, retry-zone mult: 2.0×

Scenario summary

Scenario Status Δ median (%) z (Δ/MAD) Base median (ms) Samples (b/c)
exchange-set-open ✅ pass +5.5 +0.68 0.49 20/20
s101-portray-cold ✅ pass +5.1 +1.08 399.12 20/20
s101-portray-warm ✅ pass +1.4 +0.59 250.12 20/20
s101-render-warm ✅ pass +3.9 +1.10 236.97 20/20
s102-coverage ✅ pass -0.9 -0.38 1.05 20/20
s102-coverage-open ✅ pass +1.2 +0.37 1.71 20/20
s102-coverage-render-large ✅ pass +1.1 +1.17 114.16 20/20
s124-vector ✅ pass -17.1 -6.22 0.35 20/20
s201-vector ✅ pass -8.2 -1.42 0.37 20/20

exchange-set-open

Iteration statistics

Stat Baseline Candidate
Samples 20 20
Median (ms) 0.49 0.52
Baseline MAD (ms) 0.04
Δ median +5.5%
z (Δ/MAD) +0.68

Spans (sum of all iterations)

Span Baseline (ms) Candidate (ms) Delta Status
s100.asset.read 10.25 10.83 +5.7%
s100.exchangeset.parse 38.54 38.63 +0.3% ▫️

Metrics

Metric Baseline Candidate Delta Status
s100.asset.read.duration 17.62 17.89 +1.5% ▫️

s101-portray-cold

Iteration statistics

Stat Baseline Candidate
Samples 20 20
Median (ms) 399.12 419.53
Baseline MAD (ms) 18.87
Δ median +5.1%
z (Δ/MAD) +1.08

Spans (sum of all iterations)

Span Baseline (ms) Candidate (ms) Delta Status
s100.lua.execute 10145.08 10483.28 +3.3% ▫️
s100.lua.rule.invoke 8939.27 9265.19 +3.6% ▫️
s100.pipeline.vector.process 10384.51 10725.40 +3.3% ▫️
s100.pipeline.vector.stage.assemble 0.30 0.27 -11.9%
s100.pipeline.vector.stage.feature_xml 178.88 176.76 -1.2% ▫️
s100.pipeline.vector.stage.lua 10147.62 10485.88 +3.3% ▫️
s100.pipeline.vector.stage.rule_select 8.06 8.24 +2.1% ▫️
s100.pipeline.vector.stage.sort 13.18 14.00 +6.2%
s100.pipeline.vector.stage.viewing_groups 15.42 18.45 +19.6%
s100.pipeline.vector.stage.xslt 0.33 0.34 +5.1%
s100.render.frame 2509.48 2520.65 +0.4% ▫️

Metrics

Metric Baseline Candidate Delta Status
s100.catalogue.match.count 7.00 7.00 +0.0% ▫️
s100.featurecatalogue.cache.hit.count 6.00 6.00 +0.0% ▫️
s100.featurecatalogue.cache.miss.count 1.00 1.00 +0.0% ▫️
s100.lua.execute.duration 2474.70 2402.37 -2.9% ▫️
s100.lua.features.count 2478.00 2478.00 +0.0% ▫️
s100.lua.instructions.emitted.count 4004.00 4004.00 +0.0% ▫️
s100.lua.rule.invoke.count 7.00 7.00 +0.0% ▫️
s100.lua.rule.invoke.count 77.00 77.00 +0.0% ▫️
s100.lua.rule.invoke.duration 2090.78 2035.70 -2.6% ▫️
s100.lua.rule.invoke.duration 4.16 4.23 +1.8% ▫️
s100.lua.source.cache.hit.count 552.00 552.00 +0.0% ▫️
s100.lua.source.cache.miss.count 43.00 43.00 +0.0% ▫️
s100.pattern.cache.hit.count 210.00 210.00 +0.0% ▫️
s100.pattern.cache.miss.count 14.00 14.00 +0.0% ▫️
s100.pipeline.drawinginstructions.out 4004.00 4004.00 +0.0% ▫️
s100.pipeline.duration 2537.83 2463.98 -2.9% ▫️
s100.pipeline.features.in 217.00 217.00 +0.0% ▫️
s100.pipeline.stage.duration 0.25 0.29 +15.5%
s100.pipeline.stage.duration 48.41 47.06 -2.8% ▫️
s100.pipeline.stage.duration 2476.14 2403.88 -2.9% ▫️
s100.pipeline.stage.duration 4.64 4.64 +0.1% ▫️
s100.pipeline.stage.duration 4.49 4.15 -7.5% ▫️
s100.pipeline.stage.duration 1.03 1.03 -0.2% ▫️
s100.pipeline.stage.duration 0.86 0.85 -1.2% ▫️
s100.pipeline.stage.instructions.count 0.00 0.00 N/A ▫️
s100.pipeline.stage.instructions.count 4004.00 4004.00 +0.0% ▫️
s100.pipeline.stage.instructions.count 4004.00 4004.00 +0.0% ▫️
s100.pipeline.stage.instructions.count 4004.00 4004.00 +0.0% ▫️
s100.portrayal.cache.hit.count 12.00 12.00 +0.0% ▫️
s100.portrayal.cache.hit.count 552.00 552.00 +0.0% ▫️
s100.portrayal.cache.hit.count 7.00 7.00 +0.0% ▫️
s100.portrayal.cache.hit.count 96.00 96.00 +0.0% ▫️
s100.portrayal.cache.miss.count 2.00 2.00 +0.0% ▫️
s100.portrayal.cache.miss.count 43.00 43.00 +0.0% ▫️
s100.portrayal.cache.miss.count 16.00 16.00 +0.0% ▫️
s100.render.frame.duration 761.54 759.50 -0.3% ▫️
s100.render.instructions.processed.count 4004.00 4004.00 +0.0% ▫️
s100.render.styles.applied.count 4256.00 4256.00 +0.0% ▫️
s100.symbol.cache.hit.count 364.00 364.00 +0.0% ▫️
s100.symbol.cache.miss.count 98.00 98.00 +0.0% ▫️
s100.symbol.resolve.duration 0.37 0.33 -11.7%
s100.symbol.resolve.duration 17.54 16.50 -5.9% ▫️

s101-portray-warm

Iteration statistics

Stat Baseline Candidate
Samples 20 20
Median (ms) 250.12 253.65
Baseline MAD (ms) 5.97
Δ median +1.4%
z (Δ/MAD) +0.59

Spans (sum of all iterations)

Span Baseline (ms) Candidate (ms) Delta Status
s100.lua.execute 7193.51 7396.72 +2.8% ▫️
s100.lua.rule.invoke 6351.61 6614.48 +4.1% ▫️
s100.pipeline.vector.process 7545.23 7732.88 +2.5% ▫️
s100.pipeline.vector.stage.assemble 0.22 0.22 +0.4% ▫️
s100.pipeline.vector.stage.feature_xml 319.24 305.55 -4.3% ▫️
s100.pipeline.vector.stage.lua 7195.32 7398.53 +2.8% ▫️
s100.pipeline.vector.stage.rule_select 4.87 4.83 -0.8% ▫️
s100.pipeline.vector.stage.sort 19.02 17.36 -8.7% ▫️
s100.pipeline.vector.stage.viewing_groups 21.25 19.53 -8.1% ▫️
s100.pipeline.vector.stage.xslt 0.26 0.27 +4.5% ▫️
s100.render.frame 297.61 289.06 -2.9% ▫️

Metrics

Metric Baseline Candidate Delta Status
s100.catalogue.match.count 1.00 1.00 +0.0% ▫️
s100.featurecatalogue.cache.hit.count 7.00 7.00 +0.0% ▫️
s100.lua.execute.duration 1690.84 1736.13 +2.7% ▫️
s100.lua.features.count 2478.00 2478.00 +0.0% ▫️
s100.lua.instructions.emitted.count 4004.00 4004.00 +0.0% ▫️
s100.lua.rule.invoke.count 7.00 7.00 +0.0% ▫️
s100.lua.rule.invoke.count 77.00 77.00 +0.0% ▫️
s100.lua.rule.invoke.duration 1485.34 1528.76 +2.9% ▫️
s100.lua.rule.invoke.duration 2.18 2.41 +10.5%
s100.lua.source.cache.hit.count 595.00 595.00 +0.0% ▫️
s100.pattern.cache.hit.count 222.00 222.00 +0.0% ▫️
s100.pattern.cache.miss.count 2.00 2.00 +0.0% ▫️
s100.pipeline.drawinginstructions.out 4004.00 4004.00 +0.0% ▫️
s100.pipeline.duration 1771.83 1814.94 +2.4% ▫️
s100.pipeline.features.in 217.00 217.00 +0.0% ▫️
s100.pipeline.stage.duration 0.02 0.02 +18.7%
s100.pipeline.stage.duration 71.57 70.60 -1.4% ▫️
s100.pipeline.stage.duration 1691.30 1736.58 +2.7% ▫️
s100.pipeline.stage.duration 1.38 1.67 +21.5%
s100.pipeline.stage.duration 4.93 4.31 -12.6%
s100.pipeline.stage.duration 0.34 0.31 -9.0% ▫️
s100.pipeline.stage.duration 0.04 0.04 +2.6% ▫️
s100.pipeline.stage.instructions.count 0.00 0.00 N/A ▫️
s100.pipeline.stage.instructions.count 4004.00 4004.00 +0.0% ▫️
s100.pipeline.stage.instructions.count 4004.00 4004.00 +0.0% ▫️
s100.pipeline.stage.instructions.count 4004.00 4004.00 +0.0% ▫️
s100.portrayal.cache.hit.count 2.00 2.00 +0.0% ▫️
s100.portrayal.cache.hit.count 595.00 595.00 +0.0% ▫️
s100.portrayal.cache.hit.count 7.00 7.00 +0.0% ▫️
s100.portrayal.cache.hit.count 16.00 16.00 +0.0% ▫️
s100.render.frame.duration 138.25 134.42 -2.8% ▫️
s100.render.instructions.processed.count 4004.00 4004.00 +0.0% ▫️
s100.render.styles.applied.count 4256.00 4256.00 +0.0% ▫️
s100.symbol.cache.hit.count 448.00 448.00 +0.0% ▫️
s100.symbol.cache.miss.count 14.00 14.00 +0.0% ▫️
s100.symbol.resolve.duration 0.43 0.39 -9.4% ▫️
s100.symbol.resolve.duration 2.06 2.03 -1.4% ▫️

s101-render-warm

Iteration statistics

Stat Baseline Candidate
Samples 20 20
Median (ms) 236.97 246.23
Baseline MAD (ms) 8.39
Δ median +3.9%
z (Δ/MAD) +1.10

Spans (sum of all iterations)

Span Baseline (ms) Candidate (ms) Delta Status
s100.lua.execute 6908.58 7373.67 +6.7%
s100.lua.rule.invoke 6294.11 6739.15 +7.1%
s100.pipeline.vector.process 7085.15 7531.55 +6.3%
s100.pipeline.vector.stage.assemble 0.18 0.19 +9.8%
s100.pipeline.vector.stage.feature_xml 156.73 129.23 -17.5%
s100.pipeline.vector.stage.lua 6910.03 7375.17 +6.7%
s100.pipeline.vector.stage.rule_select 2.95 2.84 -3.8% ▫️
s100.pipeline.vector.stage.sort 10.44 10.16 -2.6% ▫️
s100.pipeline.vector.stage.viewing_groups 12.27 11.88 -3.1% ▫️
s100.pipeline.vector.stage.xslt 0.21 0.21 +1.2% ▫️
s100.render.frame 160.46 166.92 +4.0% ▫️

Metrics

Metric Baseline Candidate Delta Status
s100.catalogue.match.count 1.00 1.00 +0.0% ▫️
s100.featurecatalogue.cache.hit.count 7.00 7.00 +0.0% ▫️
s100.lua.execute.duration 1619.20 1656.28 +2.3% ▫️
s100.lua.features.count 2478.00 2478.00 +0.0% ▫️
s100.lua.instructions.emitted.count 4004.00 4004.00 +0.0% ▫️
s100.lua.rule.invoke.count 7.00 7.00 +0.0% ▫️
s100.lua.rule.invoke.count 77.00 77.00 +0.0% ▫️
s100.lua.rule.invoke.duration 1449.35 1497.89 +3.3% ▫️
s100.lua.rule.invoke.duration 1.85 2.03 +10.0%
s100.lua.source.cache.hit.count 595.00 595.00 +0.0% ▫️
s100.pattern.cache.hit.count 222.00 222.00 +0.0% ▫️
s100.pattern.cache.miss.count 2.00 2.00 +0.0% ▫️
s100.pipeline.drawinginstructions.out 4004.00 4004.00 +0.0% ▫️
s100.pipeline.duration 1665.36 1701.67 +2.2% ▫️
s100.pipeline.features.in 217.00 217.00 +0.0% ▫️
s100.pipeline.stage.duration 0.02 0.02 +12.0%
s100.pipeline.stage.duration 41.17 40.65 -1.3% ▫️
s100.pipeline.stage.duration 1619.54 1656.59 +2.3% ▫️
s100.pipeline.stage.duration 0.89 0.77 -14.1%
s100.pipeline.stage.duration 2.40 2.43 +1.2% ▫️
s100.pipeline.stage.duration 0.33 0.30 -9.2% ▫️
s100.pipeline.stage.duration 0.04 0.04 -0.5% ▫️
s100.pipeline.stage.instructions.count 0.00 0.00 N/A ▫️
s100.pipeline.stage.instructions.count 4004.00 4004.00 +0.0% ▫️
s100.pipeline.stage.instructions.count 4004.00 4004.00 +0.0% ▫️
s100.pipeline.stage.instructions.count 4004.00 4004.00 +0.0% ▫️
s100.portrayal.cache.hit.count 2.00 2.00 +0.0% ▫️
s100.portrayal.cache.hit.count 595.00 595.00 +0.0% ▫️
s100.portrayal.cache.hit.count 7.00 7.00 +0.0% ▫️
s100.portrayal.cache.hit.count 16.00 16.00 +0.0% ▫️
s100.render.frame.duration 114.05 113.84 -0.2% ▫️
s100.render.instructions.processed.count 4004.00 4004.00 +0.0% ▫️
s100.render.styles.applied.count 4256.00 4256.00 +0.0% ▫️
s100.symbol.cache.hit.count 448.00 448.00 +0.0% ▫️
s100.symbol.cache.miss.count 14.00 14.00 +0.0% ▫️
s100.symbol.resolve.duration 0.21 0.24 +10.7%
s100.symbol.resolve.duration 2.14 2.14 +0.1% ▫️

s102-coverage

Iteration statistics

Stat Baseline Candidate
Samples 20 20
Median (ms) 1.05 1.04
Baseline MAD (ms) 0.03
Δ median -0.9%
z (Δ/MAD) -0.38

Spans (sum of all iterations)

Span Baseline (ms) Candidate (ms) Delta Status
s100.pipeline.coverage.process 42.11 43.55 +3.4% ▫️
s100.pipeline.coverage.stage.read 4.48 4.74 +5.7%
s100.pipeline.coverage.stage.resolve 34.73 35.62 +2.6% ▫️
s100.render.coverage.build 76.66 76.92 +0.3% ▫️

Metrics

Metric Baseline Candidate Delta Status
s100.catalogue.match.count 1.00 1.00 +0.0% ▫️
s100.coverage.cells 4557.00 4557.00 +0.0% ▫️
s100.hdf5.read.bytes 5208.00 5208.00 +0.0% ▫️
s100.hdf5.read.duration 20.30 20.72 +2.1% ▫️
s100.hdf5.read.duration 28.43 28.54 +0.4% ▫️
s100.hdf5.read.duration 6.92 7.09 +2.5% ▫️
s100.pipeline.duration 8.69 9.07 +4.5% ▫️
s100.pipeline.stage.duration 0.89 0.99 +11.3%
s100.pipeline.stage.duration 7.31 7.51 +2.7% ▫️
s100.portrayal.cache.hit.count 7.00 7.00 +0.0% ▫️

s102-coverage-open

Iteration statistics

Stat Baseline Candidate
Samples 20 20
Median (ms) 1.71 1.73
Baseline MAD (ms) 0.06
Δ median +1.2%
z (Δ/MAD) +0.37

Spans (sum of all iterations)

Span Baseline (ms) Candidate (ms) Delta Status
s100.dataset.open 50.24 50.67 +0.8% ▫️
s100.hdf5.dataset.read 9.12 9.31 +2.0% ▫️
s100.hdf5.file.open 11.11 11.88 +6.9%
s100.hdf5.open 11.08 11.23 +1.4% ▫️

Metrics

Metric Baseline Candidate Delta Status
s100.hdf5.read.bytes 36456.00 36456.00 +0.0% ▫️
s100.hdf5.read.duration 4.07 4.14 +1.9% ▫️
s100.hdf5.read.duration 1.73 1.79 +4.0% ▫️
s100.hdf5.read.duration 2.63 2.73 +3.7% ▫️

s102-coverage-render-large

Iteration statistics

Stat Baseline Candidate
Samples 20 20
Median (ms) 114.16 115.40
Baseline MAD (ms) 1.05
Δ median +1.1%
z (Δ/MAD) +1.17

Spans (sum of all iterations)

Span Baseline (ms) Candidate (ms) Delta Status
s100.pipeline.coverage.process 182.03 202.83 +11.4%
s100.pipeline.coverage.stage.read 134.23 151.43 +12.8%
s100.pipeline.coverage.stage.resolve 44.54 48.04 +7.9%
s100.render.coverage.build 3975.61 4270.79 +7.4%

Metrics

Metric Baseline Candidate Delta Status
s100.catalogue.match.count 1.00 1.00 +0.0% ▫️
s100.coverage.cells 7000000.00 7000000.00 +0.0% ▫️
s100.hdf5.read.bytes 8000000.00 8000000.00 +0.0% ▫️
s100.hdf5.read.duration 0.07 0.08 +6.5%
s100.hdf5.read.duration 3.50 3.37 -3.5% ▫️
s100.hdf5.read.duration 1.00 0.94 -5.9% ▫️
s100.pipeline.duration 33.67 40.70 +20.9%
s100.pipeline.stage.duration 24.52 29.19 +19.0%
s100.pipeline.stage.duration 8.54 10.85 +27.0%
s100.portrayal.cache.hit.count 7.00 7.00 +0.0% ▫️

s124-vector

Iteration statistics

Stat Baseline Candidate
Samples 20 20
Median (ms) 0.35 0.29
Baseline MAD (ms) 0.01
Δ median -17.1%
z (Δ/MAD) -6.22

Spans (sum of all iterations)

Span Baseline (ms) Candidate (ms) Delta Status
s100.pipeline.vector.process 7.35 13.28 +80.7%
s100.pipeline.vector.stage.assemble 0.24 0.25 +2.9% ▫️
s100.pipeline.vector.stage.feature_xml 0.92 4.20 +358.9%
s100.pipeline.vector.stage.rule_select 0.27 0.25 -7.1% ▫️
s100.pipeline.vector.stage.sort 0.20 0.19 -4.6% ▫️
s100.pipeline.vector.stage.viewing_groups 0.45 0.40 -11.7%
s100.pipeline.vector.stage.xslt 3.82 6.75 +76.5%
s100.render.frame 0.97 0.78 -19.6%
s100.xslt.transform 1.55 4.62 +197.2%

Metrics

Metric Baseline Candidate Delta Status
s100.catalogue.match.count 1.00 1.00 +0.0% ▫️
s100.featurecatalogue.cache.miss.count 1.00 1.00 +0.0% ▫️
s100.pipeline.drawinginstructions.out 14.00 14.00 +0.0% ▫️
s100.pipeline.duration 51.09 51.49 +0.8% ▫️
s100.pipeline.features.in 7.00 7.00 +0.0% ▫️
s100.pipeline.stage.duration 1.09 0.98 -9.8% ▫️
s100.pipeline.stage.duration 3.00 3.01 +0.4% ▫️
s100.pipeline.stage.duration 1.09 1.03 -5.2% ▫️
s100.pipeline.stage.duration 0.06 0.05 -20.9%
s100.pipeline.stage.duration 0.10 0.08 -16.1%
s100.pipeline.stage.duration 45.21 45.93 +1.6% ▫️
s100.pipeline.stage.instructions.count 14.00 14.00 +0.0% ▫️
s100.pipeline.stage.instructions.count 14.00 14.00 +0.0% ▫️
s100.pipeline.stage.instructions.count 14.00 14.00 +0.0% ▫️
s100.portrayal.cache.hit.count 13.00 13.00 +0.0% ▫️
s100.portrayal.cache.hit.count 7.00 7.00 +0.0% ▫️
s100.portrayal.cache.hit.count 6.00 6.00 +0.0% ▫️
s100.portrayal.cache.miss.count 1.00 1.00 +0.0% ▫️
s100.portrayal.cache.miss.count 1.00 1.00 +0.0% ▫️
s100.render.frame.duration 2.32 5.25 +126.2%
s100.render.instructions.processed.count 14.00 14.00 +0.0% ▫️
s100.render.styles.applied.count 14.00 14.00 +0.0% ▫️
s100.xslt.transform.duration 9.74 9.64 -1.0% ▫️

s201-vector

Iteration statistics

Stat Baseline Candidate
Samples 20 20
Median (ms) 0.37 0.34
Baseline MAD (ms) 0.02
Δ median -8.2%
z (Δ/MAD) -1.42

Spans (sum of all iterations)

Span Baseline (ms) Candidate (ms) Delta Status
s100.pipeline.vector.process 8.02 7.49 -6.6% ▫️
s100.pipeline.vector.stage.assemble 0.18 0.17 -5.8% ▫️
s100.pipeline.vector.stage.feature_xml 0.94 0.92 -2.2% ▫️
s100.pipeline.vector.stage.rule_select 0.30 0.28 -5.9% ▫️
s100.pipeline.vector.stage.sort 0.11 0.13 +15.2%
s100.pipeline.vector.stage.viewing_groups 0.40 0.39 -1.9% ▫️
s100.pipeline.vector.stage.xslt 3.41 3.19 -6.3% ▫️
s100.render.frame 0.91 0.85 -6.7% ▫️
s100.xslt.transform 2.29 2.18 -4.6% ▫️

Metrics

Metric Baseline Candidate Delta Status
s100.catalogue.match.count 1.00 1.00 +0.0% ▫️
s100.featurecatalogue.cache.miss.count 1.00 1.00 +0.0% ▫️
s100.pipeline.drawinginstructions.out 7.00 7.00 +0.0% ▫️
s100.pipeline.duration 138.18 132.17 -4.3% ▫️
s100.pipeline.features.in 21.00 21.00 +0.0% ▫️
s100.pipeline.stage.duration 0.61 0.55 -10.0% ▫️
s100.pipeline.stage.duration 7.34 7.49 +2.1% ▫️
s100.pipeline.stage.duration 0.14 0.12 -16.6%
s100.pipeline.stage.duration 0.02 0.02 -20.8%
s100.pipeline.stage.duration 0.05 0.04 -27.0%
s100.pipeline.stage.duration 129.56 123.54 -4.6% ▫️
s100.pipeline.stage.instructions.count 7.00 7.00 +0.0% ▫️
s100.pipeline.stage.instructions.count 7.00 7.00 +0.0% ▫️
s100.pipeline.stage.instructions.count 7.00 7.00 +0.0% ▫️
s100.portrayal.cache.hit.count 7.00 7.00 +0.0% ▫️
s100.portrayal.cache.hit.count 6.00 6.00 +0.0% ▫️
s100.portrayal.cache.miss.count 1.00 1.00 +0.0% ▫️
s100.portrayal.cache.miss.count 1.00 1.00 +0.0% ▫️
s100.render.frame.duration 1.16 1.17 +0.5% ▫️
s100.render.instructions.processed.count 7.00 7.00 +0.0% ▫️
s100.render.styles.applied.count 14.00 14.00 +0.0% ▫️
s100.symbol.cache.hit.count 6.00 6.00 +0.0% ▫️
s100.symbol.cache.miss.count 1.00 1.00 +0.0% ▫️
s100.symbol.resolve.duration 0.02 0.03 +83.5%
s100.symbol.resolve.duration 0.83 0.80 -3.7% ▫️
s100.xslt.transform.duration 29.14 28.53 -2.1% ▫️

Generated by EncDotNet.S100.PerfReport gate command

Phillip Hoff and others added 4 commits May 27, 2026 18:03
The OnSavedWidthChanged / OnSavedHeightChanged class handlers only
fire when the property's value actually changes. On first run the
bound view-model value is null, which matches the attached
property's default of null — so no Changed event fired and the
WidthProperty / HeightProperty observer was never installed.
Result: splitter drags never reached SavedWidth / SavedHeight and
nothing was persisted.

Fix: register a global ColumnDefinition.WidthProperty.Changed /
RowDefinition.HeightProperty.Changed class handler in the static
ctor. Gate inside on column.IsSet(SavedWidthProperty) /
row.IsSet(SavedHeightProperty) — IsSet returns true once a
binding has been assigned, even when the bound value equals the
default. Verified manually: resize the left dock, restart,
restored width is correct.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sets Mapsui Map.BackColor to RGB(170, 211, 223) so the map area
visually blends with the chart's water when zoomed beyond the OSM
tile layer's extent. Previously the empty area outside basemap
coverage rendered as white, making it hard to distinguish the map
control from side panels at low zoom.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…out-persistence

# Conflicts:
#	src/EncDotNet.S100.Viewer/MainWindow.axaml.cs
The 'fire after delay' and 'coalesce' tests asserted on a save
counter after a fixed Thread.Sleep window. On slower CI runners
(Linux build, windows-11-arm) the 50 ms / 100 ms debounce timer
plus dispatch overhead occasionally exceeded the 200 ms / 300 ms
post-sleep window, producing intermittent failures.

Replace the post-sleep assertion with a ManualResetEventSlim that
the save action sets, then Wait(5s) for it. The coalesce test
still keeps a final Thread.Sleep after the first fire to let any
incorrectly-scheduled extra fires land before we assert
coalescence (the bug it's guarding against would *cause* extra
fires, not fewer, so we still need a passive window for it).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@philliphoff philliphoff merged commit 8dc5327 into main May 28, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant