Implemented query cache size limit in front-end and in logic, Reworked LRUCache for performance, Fixed updating entities from front-end not working

This commit is contained in:
2026-01-05 01:04:26 +01:00
parent 88d1b27394
commit 3dfcaa19e6
4 changed files with 185 additions and 130 deletions

View File

@@ -55,6 +55,10 @@ public class SearchdomainController : ControllerBase
{ {
try try
{ {
if (settings.QueryCacheSize <= 0)
{
settings.QueryCacheSize = 1_000_000; // TODO get rid of this magic number
}
int id = _domainManager.CreateSearchdomain(searchdomain, settings); int id = _domainManager.CreateSearchdomain(searchdomain, settings);
return Ok(new SearchdomainCreateResults(){Id = id, Success = true}); return Ok(new SearchdomainCreateResults(){Id = id, Success = true});
} catch (Exception) } catch (Exception)
@@ -255,7 +259,7 @@ public class SearchdomainController : ControllerBase
} }
(Searchdomain? searchdomain_, int? httpStatusCode, string? message) = SearchdomainHelper.TryGetSearchdomain(_domainManager, searchdomain, _logger); (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}); 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; int ElementMaxCount = searchdomain_.settings.QueryCacheSize;
return Ok(new SearchdomainQueryCacheSizeResults() { SizeBytes = searchdomain_.GetSearchCacheSize(), ElementCount = elementCount, ElementMaxCount = ElementMaxCount, Success = true }); return Ok(new SearchdomainQueryCacheSizeResults() { SizeBytes = searchdomain_.GetSearchCacheSize(), ElementCount = elementCount, ElementMaxCount = ElementMaxCount, Success = true });
} }

View File

@@ -91,4 +91,10 @@ public static class DatabaseMigrations
helper.ExecuteSQLNonQuery("ALTER TABLE datapoint ADD COLUMN similaritymethod VARCHAR(512) NULL DEFAULT 'Cosine' AFTER probmethod_embedding", []); helper.ExecuteSQLNonQuery("ALTER TABLE datapoint ADD COLUMN similaritymethod VARCHAR(512) NULL DEFAULT 'Cosine' AFTER probmethod_embedding", []);
return 4; 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;
}
} }

View File

@@ -887,12 +887,12 @@
var data = [{ var data = [{
"name": name, "name": name,
"probmethod": probMethod, "probmethod": probMethod,
"searchdomain": encodeURIComponent(domains[getSelectedDomainKey()]), "searchdomain": domains[getSelectedDomainKey()],
"attributes": attributes, "attributes": attributes,
"datapoints": datapoints "datapoints": datapoints
}]; }];
showToast("@T["Updating entity"]", "primary"); showToast("@T["Updating entity"]", "primary");
fetch(`/Entity`, { fetch(`/Entities`, {
method: 'PUT', method: 'PUT',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
@@ -1073,6 +1073,8 @@
let configElementCachereconciliation = document.getElementById('searchdomainConfigCacheReconciliation'); let configElementCachereconciliation = document.getElementById('searchdomainConfigCacheReconciliation');
let configElementCacheSize = document.getElementById('searchdomainConfigQueryCacheSize'); let configElementCacheSize = document.getElementById('searchdomainConfigQueryCacheSize');
showThrobber(document.querySelector('#searchdomainConfigQueryCacheSize'), true);
showThrobber(document.querySelector('#searchdomainConfigCacheReconciliation'), true);
let cacheUtilizationPromise = getSearchdomainCacheUtilization(getSelectedDomainKey()); let cacheUtilizationPromise = getSearchdomainCacheUtilization(getSelectedDomainKey());
let databaseUtilizationPromise = getSearchdomainDatabaseUtilization(getSelectedDomainKey()); let databaseUtilizationPromise = getSearchdomainDatabaseUtilization(getSelectedDomainKey());
@@ -1114,6 +1116,7 @@
}); });
searchdomainConfigPromise.then(searchdomainConfig => { searchdomainConfigPromise.then(searchdomainConfig => {
hideThrobber(document.querySelector('#searchdomainConfigCacheReconciliation'), true);
if (searchdomainConfig != null && searchdomainConfig.Settings != null) if (searchdomainConfig != null && searchdomainConfig.Settings != null)
{ {
configElementCacheSize.value = searchdomainConfig.Settings.QueryCacheSize; configElementCacheSize.value = searchdomainConfig.Settings.QueryCacheSize;
@@ -1126,6 +1129,7 @@
} }
}); });
cacheUtilizationPromise.then(cacheUtilization => { cacheUtilizationPromise.then(cacheUtilization => {
hideThrobber(document.querySelector('#searchdomainConfigQueryCacheSize'), true);
if (cacheUtilization != null && cacheUtilization.SizeBytes != null) if (cacheUtilization != null && cacheUtilization.SizeBytes != null)
{ {
document.querySelector('#cacheUtilization').innerText = document.querySelector('#cacheUtilization').innerText =
@@ -1309,14 +1313,30 @@
domainItem.classList.add('list-group-item-danger'); domainItem.classList.add('list-group-item-danger');
} }
function showThrobber(element = null) { function showThrobber(element = null, direct = false) {
if (element == null) element = document; 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; 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) { function showEntityDetails(entity) {

View File

@@ -1,215 +1,240 @@
namespace Shared; namespace Shared;
public sealed class EnumerableLruCache<TKey, TValue> where TKey : notnull
public class EnumerableLruCache<TKey, TValue> where TKey : notnull
{ {
private readonly Dictionary<TKey, TValue> _cache; private sealed record CacheItem(TKey Key, TValue Value);
private readonly LinkedList<TKey> _keys = new();
private readonly Dictionary<TKey, LinkedListNode<CacheItem>> _map;
private readonly LinkedList<CacheItem> _lruList;
private readonly ReaderWriterLockSlim _lock = new();
private int _capacity; private int _capacity;
private ReaderWriterLockSlim _readerWriterLock;
public EnumerableLruCache(int capacity)
{
if (capacity <= 0)
throw new ArgumentOutOfRangeException(nameof(capacity));
_capacity = capacity;
_map = new Dictionary<TKey, LinkedListNode<CacheItem>>(capacity);
_lruList = new LinkedList<CacheItem>();
}
public int Capacity public int Capacity
{ {
get get
{ {
_readerWriterLock.EnterReadLock(); _lock.EnterReadLock();
try try
{ {
return _capacity; return _capacity;
} }
finally finally
{ {
_readerWriterLock.ExitReadLock(); _lock.ExitReadLock();
} }
} }
set set
{ {
_readerWriterLock.EnterWriteLock(); ArgumentOutOfRangeException.ThrowIfNegativeOrZero(value);
_lock.EnterWriteLock();
try try
{ {
_capacity = value; _capacity = value;
TrimIfNeeded();
// Trim cache if new capacity is smaller than current size
while (_keys.Count > _capacity)
{
TKey last = _keys.Last!.Value;
_keys.RemoveLast();
_cache.Remove(last);
}
} }
finally finally
{ {
_readerWriterLock.ExitWriteLock(); _lock.ExitWriteLock();
} }
} }
} }
public EnumerableLruCache(int capacity) public int Count
{ {
_readerWriterLock = new(); get
_capacity = capacity; {
_cache = []; _lock.EnterReadLock();
try
{
return _map.Count;
}
finally
{
_lock.ExitReadLock();
}
}
} }
public TValue this[TKey key] public TValue this[TKey key]
{ {
get 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 try
{ {
return _cache[key]; _lruList.Remove(node);
_lruList.AddFirst(node);
} }
finally 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) return true;
{
TKey last = _keys.Last!.Value;
_keys.RemoveLast();
_cache.Remove(last);
}
}
finally
{
_readerWriterLock.ExitWriteLock();
}
} }
} finally
public bool ContainsKey(TKey key)
{
_readerWriterLock.EnterReadLock();
try
{ {
return _cache.ContainsKey(key); _lock.ExitUpgradeableReadLock();
} finally
{
_readerWriterLock.ExitReadLock();
}
}
public int Count()
{
_readerWriterLock.EnterReadLock();
try
{
return _cache.Count;
} finally
{
_readerWriterLock.ExitReadLock();
} }
} }
public void Set(TKey key, TValue value) public void Set(TKey key, TValue value)
{ {
_readerWriterLock.EnterWriteLock(); _lock.EnterWriteLock();
try 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) var item = new CacheItem(key, value);
{ var node = new LinkedListNode<CacheItem>(item);
TKey? last = _keys.Last();
_keys.RemoveLast(); _lruList.AddFirst(node);
_cache.Remove(last); _map[key] = node;
}
} finally TrimIfNeeded();
}
finally
{ {
_readerWriterLock.ExitWriteLock(); _lock.ExitWriteLock();
} }
} }
public void Remove(TKey key) public bool Remove(TKey key)
{ {
_readerWriterLock.EnterWriteLock(); _lock.EnterWriteLock();
try try
{ {
_keys.Remove(key); if (!_map.TryGetValue(key, out var node))
_cache.Remove(key);
} finally
{
_readerWriterLock.ExitWriteLock();
}
}
public bool TryGetValue(TKey key, out TValue? value)
{
_readerWriterLock.EnterUpgradeableReadLock();
try
{
if (_cache.TryGetValue(key, out value))
{
return false; return false;
}
_readerWriterLock.EnterWriteLock(); _lruList.Remove(node);
try _map.Remove(key);
{
_keys.Remove(key);
_keys.AddFirst(key);
} finally
{
_readerWriterLock.ExitWriteLock();
}
return true; 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<TKey, TValue> AsDictionary() public Dictionary<TKey, TValue> AsDictionary()
{ {
_readerWriterLock.EnterReadLock(); _lock.EnterReadLock();
try try
{ {
return new Dictionary<TKey, TValue>(_cache); return _map.Values.ToDictionary(
n => n.Value.Key,
n => n.Value.Value
);
} }
finally finally
{ {
_readerWriterLock.ExitReadLock(); _lock.ExitReadLock();
} }
} }
public IEnumerable<KeyValuePair<TKey, TValue>> Items() public IEnumerable<KeyValuePair<TKey, TValue>> Items()
{ {
_readerWriterLock.EnterReadLock(); _lock.EnterReadLock();
try try
{ {
foreach (var key in _keys) foreach (var item in _lruList)
{ {
if (_cache.TryGetValue(key, out var value)) yield return new KeyValuePair<TKey, TValue>(item.Key, item.Value);
{
yield return new KeyValuePair<TKey, TValue>(key, value);
}
} }
} finally }
finally
{ {
_readerWriterLock.ExitReadLock(); _lock.ExitReadLock();
} }
} }
public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator() public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator()
{ {
return Items().GetEnumerator(); List<KeyValuePair<TKey, TValue>> snapshot;
_lock.EnterReadLock();
try
{
snapshot = new List<KeyValuePair<TKey, TValue>>(_map.Count);
foreach (var item in _lruList)
{
snapshot.Add(new KeyValuePair<TKey, TValue>(
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);
}
} }
} }