diff --git a/src/Server/Controllers/SearchdomainController.cs b/src/Server/Controllers/SearchdomainController.cs index e0caab6..2acf8dd 100644 --- a/src/Server/Controllers/SearchdomainController.cs +++ b/src/Server/Controllers/SearchdomainController.cs @@ -55,6 +55,10 @@ public class SearchdomainController : ControllerBase { try { + if (settings.QueryCacheSize <= 0) + { + settings.QueryCacheSize = 1_000_000; // TODO get rid of this magic number + } int id = _domainManager.CreateSearchdomain(searchdomain, settings); return Ok(new SearchdomainCreateResults(){Id = id, Success = true}); } catch (Exception) @@ -255,7 +259,7 @@ public class SearchdomainController : ControllerBase } (Searchdomain? searchdomain_, int? httpStatusCode, string? message) = SearchdomainHelper.TryGetSearchdomain(_domainManager, searchdomain, _logger); if (searchdomain_ is null || httpStatusCode is not null) return StatusCode(httpStatusCode ?? 500, new SearchdomainUpdateResults(){Success = false, Message = message}); - int elementCount = searchdomain_.queryCache.Count(); + int elementCount = searchdomain_.queryCache.Count; int ElementMaxCount = searchdomain_.settings.QueryCacheSize; return Ok(new SearchdomainQueryCacheSizeResults() { SizeBytes = searchdomain_.GetSearchCacheSize(), ElementCount = elementCount, ElementMaxCount = ElementMaxCount, Success = true }); } diff --git a/src/Server/Migrations/DatabaseMigrations.cs b/src/Server/Migrations/DatabaseMigrations.cs index 800396b..e20ada8 100644 --- a/src/Server/Migrations/DatabaseMigrations.cs +++ b/src/Server/Migrations/DatabaseMigrations.cs @@ -91,4 +91,10 @@ public static class DatabaseMigrations helper.ExecuteSQLNonQuery("ALTER TABLE datapoint ADD COLUMN similaritymethod VARCHAR(512) NULL DEFAULT 'Cosine' AFTER probmethod_embedding", []); return 4; } + + public static int UpdateFrom4(SQLHelper helper) + { + helper.ExecuteSQLNonQuery("UPDATE searchdomain SET settings = JSON_SET(settings, '$.QueryCacheSize', 1000000) WHERE JSON_EXTRACT(settings, '$.QueryCacheSize') is NULL;", []); // Set QueryCacheSize to a default of 1000000 + return 5; + } } \ No newline at end of file diff --git a/src/Server/Views/Home/Searchdomains.cshtml b/src/Server/Views/Home/Searchdomains.cshtml index f482afe..bbe933b 100644 --- a/src/Server/Views/Home/Searchdomains.cshtml +++ b/src/Server/Views/Home/Searchdomains.cshtml @@ -887,12 +887,12 @@ var data = [{ "name": name, "probmethod": probMethod, - "searchdomain": encodeURIComponent(domains[getSelectedDomainKey()]), + "searchdomain": domains[getSelectedDomainKey()], "attributes": attributes, "datapoints": datapoints }]; showToast("@T["Updating entity"]", "primary"); - fetch(`/Entity`, { + fetch(`/Entities`, { method: 'PUT', headers: { 'Content-Type': 'application/json' @@ -1073,6 +1073,8 @@ let configElementCachereconciliation = document.getElementById('searchdomainConfigCacheReconciliation'); let configElementCacheSize = document.getElementById('searchdomainConfigQueryCacheSize'); + showThrobber(document.querySelector('#searchdomainConfigQueryCacheSize'), true); + showThrobber(document.querySelector('#searchdomainConfigCacheReconciliation'), true); let cacheUtilizationPromise = getSearchdomainCacheUtilization(getSelectedDomainKey()); let databaseUtilizationPromise = getSearchdomainDatabaseUtilization(getSelectedDomainKey()); @@ -1114,6 +1116,7 @@ }); searchdomainConfigPromise.then(searchdomainConfig => { + hideThrobber(document.querySelector('#searchdomainConfigCacheReconciliation'), true); if (searchdomainConfig != null && searchdomainConfig.Settings != null) { configElementCacheSize.value = searchdomainConfig.Settings.QueryCacheSize; @@ -1126,6 +1129,7 @@ } }); cacheUtilizationPromise.then(cacheUtilization => { + hideThrobber(document.querySelector('#searchdomainConfigQueryCacheSize'), true); if (cacheUtilization != null && cacheUtilization.SizeBytes != null) { document.querySelector('#cacheUtilization').innerText = @@ -1309,14 +1313,30 @@ domainItem.classList.add('list-group-item-danger'); } - function showThrobber(element = null) { + function showThrobber(element = null, direct = false) { if (element == null) element = document; - element.querySelector('.spinner').classList.remove('d-none'); + if (direct) { + let spinner = document.createElement('div'); + spinner.classList.add('spinner'); + spinner.style.position = "absolute"; + spinner.style.marginTop = "0.5rem"; + spinner.style.marginLeft = "0.5rem"; + element.style.opacity = "0.25"; + element.parentElement.insertBefore(spinner, element); + } else { + element.querySelector('.spinner').classList.remove('d-none'); + } + } - function hideThrobber(element = null) { + function hideThrobber(element = null, direct = false) { if (element == null) element = document; - element.querySelector('.spinner').classList.add('d-none'); + if (direct) { + element.previousElementSibling.remove() + element.style.opacity = "1"; + } else { + element.querySelector('.spinner').classList.add('d-none'); + } } function showEntityDetails(entity) { diff --git a/src/Shared/LRUCache.cs b/src/Shared/LRUCache.cs index 056c5f0..00752be 100644 --- a/src/Shared/LRUCache.cs +++ b/src/Shared/LRUCache.cs @@ -1,215 +1,240 @@ namespace Shared; - -public class EnumerableLruCache where TKey : notnull +public sealed class EnumerableLruCache where TKey : notnull { - private readonly Dictionary _cache; - private readonly LinkedList _keys = new(); + private sealed record CacheItem(TKey Key, TValue Value); + + private readonly Dictionary> _map; + private readonly LinkedList _lruList; + private readonly ReaderWriterLockSlim _lock = new(); + private int _capacity; - private ReaderWriterLockSlim _readerWriterLock; + + public EnumerableLruCache(int capacity) + { + if (capacity <= 0) + throw new ArgumentOutOfRangeException(nameof(capacity)); + + _capacity = capacity; + _map = new Dictionary>(capacity); + _lruList = new LinkedList(); + } public int Capacity { get { - _readerWriterLock.EnterReadLock(); + _lock.EnterReadLock(); try { return _capacity; } finally { - _readerWriterLock.ExitReadLock(); + _lock.ExitReadLock(); } } set { - _readerWriterLock.EnterWriteLock(); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(value); + + _lock.EnterWriteLock(); try { _capacity = value; - - // Trim cache if new capacity is smaller than current size - while (_keys.Count > _capacity) - { - TKey last = _keys.Last!.Value; - _keys.RemoveLast(); - _cache.Remove(last); - } + TrimIfNeeded(); } finally { - _readerWriterLock.ExitWriteLock(); + _lock.ExitWriteLock(); } } } - public EnumerableLruCache(int capacity) + public int Count { - _readerWriterLock = new(); - _capacity = capacity; - _cache = []; + get + { + _lock.EnterReadLock(); + try + { + return _map.Count; + } + finally + { + _lock.ExitReadLock(); + } + } } public TValue this[TKey key] { get { - _readerWriterLock.EnterReadLock(); + if (!TryGetValue(key, out var value)) + throw new KeyNotFoundException(); + + return value!; + } + set => Set(key, value); + } + + public bool TryGetValue(TKey key, out TValue? value) + { + _lock.EnterUpgradeableReadLock(); + try + { + if (!_map.TryGetValue(key, out var node)) + { + value = default; + return false; + } + + value = node.Value.Value; + + // LRU aktualisieren + _lock.EnterWriteLock(); try { - return _cache[key]; + _lruList.Remove(node); + _lruList.AddFirst(node); } finally { - _readerWriterLock.ExitReadLock(); + _lock.ExitWriteLock(); } - } - set - { - _readerWriterLock.EnterWriteLock(); - try - { - if (_cache.TryGetValue(key, out _)) - { - _keys.Remove(key); - } - _keys.AddFirst(key); - _cache[key] = value; - if (_keys.Count > _capacity) - { - TKey last = _keys.Last!.Value; - _keys.RemoveLast(); - _cache.Remove(last); - } - } - finally - { - _readerWriterLock.ExitWriteLock(); - } + return true; } - } - - public bool ContainsKey(TKey key) - { - _readerWriterLock.EnterReadLock(); - try + finally { - return _cache.ContainsKey(key); - } finally - { - _readerWriterLock.ExitReadLock(); - } - } - - public int Count() - { - _readerWriterLock.EnterReadLock(); - try - { - return _cache.Count; - } finally - { - _readerWriterLock.ExitReadLock(); + _lock.ExitUpgradeableReadLock(); } } public void Set(TKey key, TValue value) { - _readerWriterLock.EnterWriteLock(); + _lock.EnterWriteLock(); try { - if (_cache.TryGetValue(key, out _)) + if (_map.TryGetValue(key, out var existing)) { - _keys.Remove(key); + // Update + nach vorne + existing.Value = existing.Value with { Value = value }; + _lruList.Remove(existing); + _lruList.AddFirst(existing); + return; } - _keys.AddFirst(key); - _cache[key] = value; - if (_keys.Count > _capacity) - { - TKey? last = _keys.Last(); - _keys.RemoveLast(); - _cache.Remove(last); - } - } finally + var item = new CacheItem(key, value); + var node = new LinkedListNode(item); + + _lruList.AddFirst(node); + _map[key] = node; + + TrimIfNeeded(); + } + finally { - _readerWriterLock.ExitWriteLock(); + _lock.ExitWriteLock(); } } - public void Remove(TKey key) + public bool Remove(TKey key) { - _readerWriterLock.EnterWriteLock(); + _lock.EnterWriteLock(); try { - _keys.Remove(key); - _cache.Remove(key); - } finally - { - _readerWriterLock.ExitWriteLock(); - } - } - - public bool TryGetValue(TKey key, out TValue? value) - { - _readerWriterLock.EnterUpgradeableReadLock(); - try - { - if (_cache.TryGetValue(key, out value)) - { + if (!_map.TryGetValue(key, out var node)) return false; - } - _readerWriterLock.EnterWriteLock(); - try - { - _keys.Remove(key); - _keys.AddFirst(key); - } finally - { - _readerWriterLock.ExitWriteLock(); - } + + _lruList.Remove(node); + _map.Remove(key); return true; - } finally + } + finally { - _readerWriterLock.ExitUpgradeableReadLock(); + _lock.ExitWriteLock(); + } + } + + public bool ContainsKey(TKey key) + { + _lock.EnterReadLock(); + try + { + return _map.ContainsKey(key); + } + finally + { + _lock.ExitReadLock(); } } public Dictionary AsDictionary() { - _readerWriterLock.EnterReadLock(); + _lock.EnterReadLock(); try { - return new Dictionary(_cache); + return _map.Values.ToDictionary( + n => n.Value.Key, + n => n.Value.Value + ); } finally { - _readerWriterLock.ExitReadLock(); + _lock.ExitReadLock(); } } public IEnumerable> Items() { - _readerWriterLock.EnterReadLock(); + _lock.EnterReadLock(); try { - foreach (var key in _keys) + foreach (var item in _lruList) { - if (_cache.TryGetValue(key, out var value)) - { - yield return new KeyValuePair(key, value); - } + yield return new KeyValuePair(item.Key, item.Value); } - } finally + } + finally { - _readerWriterLock.ExitReadLock(); + _lock.ExitReadLock(); } } public IEnumerator> GetEnumerator() { - return Items().GetEnumerator(); + List> snapshot; + + _lock.EnterReadLock(); + try + { + snapshot = new List>(_map.Count); + + foreach (var item in _lruList) + { + snapshot.Add(new KeyValuePair( + item.Key, + item.Value + )); + } + } + finally + { + _lock.ExitReadLock(); + } + + return snapshot.GetEnumerator(); + } + + private void TrimIfNeeded() + { + while (_map.Count > _capacity) + { + var lruNode = _lruList.Last!; + _lruList.RemoveLast(); + _map.Remove(lruNode.Value.Key); + } } }