Skip to content

Reuse one XmlSerializer for profile load and save (~100 ms → ~1 ms per profile switch) - #134

Merged
hbashton merged 1 commit into
hbashton:mainfrom
petemess95:fast-profile-switch
Oct 9, 2026
Merged

hbashton merged 1 commit into
hbashton:mainfrom
petemess95:fast-profile-switch

Conversation

@petemess95

Copy link
Copy Markdown
Contributor

PreparedProfileLoad.TryPrepare and SaveProfileNew each built a new XmlSerializer(typeof(ProfileDTO), ProfileDTO.GetAttributeOverrides()). .NET only caches serializers built from (Type) or (Type, string). With XmlAttributeOverrides, every construction generates and loads a new dynamic assembly, which never unloads. The constructor plus the first Deserialize on each new instance cost about 100 ms.

Temporary profile switches (for example a hold-to-switch special action with automatic untrigger) run TryPrepare on every press and every release. Each press therefore cost about 100 ms of CPU and leaked an assembly, which can cause stutter in games and delays the switch on quick taps.

Change: one shared, lazily built instance, ProfileDTO.Serializer (Lazy<XmlSerializer>, ExecutionAndPublication), used by both call sites. Serialize and Deserialize are thread-safe, and no call site attaches Unknown* event handlers, so sharing one instance is safe. No other production code builds a serializer with overrides.

Results (a local timing harness, not included in this PR; ~17.8 KB profile, 5 warm-up calls and 40 timed calls, before and after measured in the same session on the same machine):

Before After
Warm TryPrepare wall / CPU 101.1 / 100.0 ms 0.885 / 1.2 ms
Loaded assemblies over 45 calls 186 → 231 (+1 per call) 182 → 182

The first profile load or save in a process still pays the one-time ~100 ms.

On hardware (DualSense Edge, hold-L2-to-switch profile action): DS4Windows CPU during rapid tapping dropped from 3–5% to ~1.5%. The log still shows one profile line per press.

Tests:

  • New ProfileSerializerCacheTests: the same instance is returned each time, and 20 repeated TryPrepare calls leave the assembly count unchanged.
  • Full suite: 7132 passed, 0 failed, 12 skipped (the same opt-in and live-hardware skips as on main).

XmlSerializer built with XmlAttributeOverrides is not cached by .NET. Every
profile load (TryPrepare) and save (SaveProfileNew) built a new one, which
generated and loaded a new dynamic assembly (~100 ms with its first use)
that never unloads. Temporary profile switches pay this on every press and
every release.

Share one lazily built, thread-safe instance (ProfileDTO.Serializer).
Warm TryPrepare: ~101 ms -> ~0.9 ms; loaded assemblies +1 per call -> +0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

2 participants