From 7e37d8c3f40d082fee4a7aaf7edbeb35ce865890 Mon Sep 17 00:00:00 2001 From: Anders Lyager Kaae Date: Wed, 5 Aug 2026 19:28:17 +0200 Subject: [PATCH] fix: use Clear(key) instead of ClearByKey(key) for exact-key cache removal SyncEntityCache.AddName/GetEntity and ContentTypeBaseSerializer's alias cache all called DictionaryAppCache.ClearByKey(exactKey), which does a full LINQ scan of every entry in the cache looking for a prefix match, even though the key passed is always an exact id/guid string with no other entries sharing that prefix. DictionaryAppCache already exposes Clear(key), a direct ConcurrentDictionary.TryRemove - O(1), no scan. nameCache in particular is shared across an entire Export/Report run and grows by one entry per resolved ancestor name, so ClearByKey's O(n) scan compounded into genuine O(n^2) cost as the cache grew - confirmed via CPU profiling (~91% of wall time in the LINQ Where/ToArray + ConcurrentDictionary enumeration) and an isolated benchmark (60k items: 27.4s vs 27ms, ~1000x). Left 5 other ClearByKey call sites unchanged (uSyncService_Handlers, SyncHandlerRoot x2 incl. PrepCaches/CleanCaches) - those rely on genuine multi-key prefix matching, one explicitly documented in a comment as "a starts with call" for clearing a related folder cache. Changing those to Clear() would silently break that behavior. Local fix for LegalDesk V2's uSync migration validation work. Not yet upstreamed to Jumoo - see LegalDesk-V2 task history for the diagnosis. --- uSync.Core/Cache/SyncEntityCache.cs | 12 +++++++++--- .../Serializers/ContentTypeBaseSerializer.cs | 8 ++++++-- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/uSync.Core/Cache/SyncEntityCache.cs b/uSync.Core/Cache/SyncEntityCache.cs index c712560fe..2bcbcbb7e 100644 --- a/uSync.Core/Cache/SyncEntityCache.cs +++ b/uSync.Core/Cache/SyncEntityCache.cs @@ -47,7 +47,11 @@ public CachedName GetName(int id) public void AddName(int id, Guid guid, string name) { - nameCache.ClearByKey(id.ToString()); + // was ClearByKey(id.ToString()) - a full LINQ scan of every entry in nameCache + // looking for a prefix match, on every single call. id.ToString() is an exact + // key here (no other key is ever a variant/suffix of it in this cache), so a + // direct single-key removal is correct and avoids the O(n) scan entirely. + nameCache.Clear(id.ToString()); nameCache.GetCacheItem(id.ToString(), () => { return new CachedName(guid, name); @@ -103,7 +107,8 @@ public IEntitySlim GetEntity(Guid id) } else { - keyCache.ClearByKey(id.ToString()); + // was ClearByKey (O(n) scan) - id.ToString() is an exact key here too. + keyCache.Clear(id.ToString()); return null; } } @@ -134,7 +139,8 @@ public IEntitySlim GetEntity(Guid id, UmbracoObjectTypes objectType) } else { - keyCache.ClearByKey(id.ToString()); + // was ClearByKey (O(n) scan) - id.ToString() is an exact key here too. + keyCache.Clear(id.ToString()); return null; } } diff --git a/uSync.Core/Serialization/Serializers/ContentTypeBaseSerializer.cs b/uSync.Core/Serialization/Serializers/ContentTypeBaseSerializer.cs index 509e113a8..7b2dcf793 100644 --- a/uSync.Core/Serialization/Serializers/ContentTypeBaseSerializer.cs +++ b/uSync.Core/Serialization/Serializers/ContentTypeBaseSerializer.cs @@ -659,7 +659,9 @@ private void EnsureAliasCache() protected void ClearAliases() { aliasCache = null; - _appCache.ClearByKey($"usync_{this.Id}"); + // was ClearByKey (O(n) scan of the whole shared RuntimeCache) - the key here is + // exact (this.Id is a single content type), so a direct removal is correct. + _appCache.Clear($"usync_{this.Id}"); } protected void RemoveAlias(string alias) @@ -675,7 +677,9 @@ protected void RemoveAlias(string alias) private void RefreshAliasCache() { - _appCache.ClearByKey($"usync_{this.Id}"); + // was ClearByKey (O(n) scan of the whole shared RuntimeCache) - the key here is + // exact (this.Id is a single content type), so a direct removal is correct. + _appCache.Clear($"usync_{this.Id}"); _appCache.GetCacheItem($"usync_{this.Id}", () => { return aliasCache; }); }