From ca24dc04ab1b646eb325b355a8667a79d683339e Mon Sep 17 00:00:00 2001 From: LD-Reborn Date: Fri, 19 Dec 2025 10:07:22 +0100 Subject: [PATCH] Added search cache estimation, added search cache clearing --- .../Controllers/SearchdomainController.cs | 48 +++++++++++ src/Server/Views/Home/Index.cshtml | 43 +++++++++- src/Shared/Models/SearchdomainModels.cs | 81 +++++++++++++++++++ src/Shared/Models/SearchdomainResults.cs | 21 +++++ 4 files changed, 191 insertions(+), 2 deletions(-) diff --git a/src/Server/Controllers/SearchdomainController.cs b/src/Server/Controllers/SearchdomainController.cs index f76ecf1..a6ad502 100644 --- a/src/Server/Controllers/SearchdomainController.cs +++ b/src/Server/Controllers/SearchdomainController.cs @@ -176,4 +176,52 @@ public class SearchdomainController : ControllerBase SearchdomainSettings settings = searchdomain_.settings; return Ok(new SearchdomainSettingsResults() { Settings = settings, Success = true }); } + + [HttpGet("GetSearchCacheSize")] + public ActionResult GetSearchCacheSize(string searchdomain) + { + Searchdomain searchdomain_; + try + { + searchdomain_ = _domainManager.GetSearchdomain(searchdomain); + } + catch (SearchdomainNotFoundException) + { + _logger.LogError("Unable to retrieve the searchdomain {searchdomain} - it likely does not exist yet", [searchdomain]); + return Ok(new SearchdomainSearchCacheSizeResults() { SearchCacheSizeBytes = null, Success = false, Message = "Searchdomain not found" }); + } + catch (Exception ex) + { + _logger.LogError("Unable to retrieve the searchdomain {searchdomain} - {ex.Message} - {ex.StackTrace}", [searchdomain, ex.Message, ex.StackTrace]); + return Ok(new SearchdomainSearchCacheSizeResults() { SearchCacheSizeBytes = null, Success = false, Message = ex.Message }); + } + Dictionary searchCache = searchdomain_.searchCache; + long sizeInBytes = 0; + foreach (var entry in searchCache) + { + sizeInBytes += sizeof(int); // string length prefix + sizeInBytes += entry.Key.Length * sizeof(char); // string characters + sizeInBytes += entry.Value.EstimateSize(); + } + return Ok(new SearchdomainSearchCacheSizeResults() { SearchCacheSizeBytes = sizeInBytes, Success = true }); + } + + [HttpGet("ClearSearchCache")] + public ActionResult InvalidateSearchCache(string searchdomain) + { + try + { + Searchdomain searchdomain_ = _domainManager.GetSearchdomain(searchdomain); + searchdomain_.InvalidateSearchCache(); + } catch (SearchdomainNotFoundException) + { + _logger.LogError("Unable to invalidate search cache for searchdomain {searchdomain} - not found", [searchdomain]); + return Ok(new SearchdomainInvalidateCacheResults() { Success = false, Message = $"Unable to invalidate search cache for searchdomain {searchdomain} - not found" }); + } catch (Exception ex) + { + _logger.LogError("Unable to invalidate search cache for searchdomain {searchdomain} - Exception: {ex.Message} - {ex.StackTrace}", [searchdomain, ex.Message, ex.StackTrace]); + return Ok(new SearchdomainInvalidateCacheResults() { Success = false, Message = $"Unable to invalidate search cache for searchdomain {searchdomain}" }); + } + return Ok(new SearchdomainInvalidateCacheResults(){Success = true}); + } } diff --git a/src/Server/Views/Home/Index.cshtml b/src/Server/Views/Home/Index.cshtml index 8eb6f4c..dc9e5a9 100644 --- a/src/Server/Views/Home/Index.cshtml +++ b/src/Server/Views/Home/Index.cshtml @@ -68,9 +68,9 @@
- Cache utilization: 2.47MiB + Cache utilization: 0.00MiB
- +
@@ -405,6 +405,27 @@ console.error('Error creating searchdomain:', error); }); }); + + document + .getElementById('cacheClear') + .addEventListener('click', () => { + const domainKey = getSelectedDomainKey(); + fetch(`/Searchdomain/ClearSearchCache?searchdomain=${encodeURIComponent(domains[domainKey])}`, { + method: 'GET' + }).then(response => { + if (response.ok) { + // TODO add toast + console.log('Searchdomain cache cleared successfully'); + // Update cache utilization display + document.querySelector('#cacheUtilization').innerText = '0.00MiB'; + } else { + // TODO add toast + console.error('Failed to clear searchdomain cache'); + } + }).catch(error => { + console.error('Error clearing searchdomain cache:', error); + }); + }); }); function deleteSearchdomain(domainKey) { @@ -480,6 +501,11 @@ .then(r => r.json()); } + function getSearchdomainCacheUtilization(domainKey) { + return fetch(`/Searchdomain/GetSearchCacheSize?searchdomain=${encodeURIComponent(domains[domainKey])}`) + .then(r => r.json()); + } + function selectDomain(domainKey) { document.querySelectorAll('.domain-item').forEach(item => { item.classList.remove('active'); @@ -494,6 +520,8 @@ let searchdomainConfigPromise = getSearchdomainConfig(getSelectedDomainKey()); let configElementCacheReconsiliation = document.getElementById('searchdomainConfigCacheReconciliation'); + let cacheUtilizationPromise = getSearchdomainCacheUtilization(getSelectedDomainKey()); + /* ---------- ENTITIES ---------- */ let entitiesUrl = `/Entity/List?searchdomain=${encodeURIComponent(domainName)}&returnEmbeddings=false`; let entitiesCard = document.querySelector("#entitiesTable").parentElement; @@ -544,6 +572,17 @@ console.error('Failed to fetch searchdomain config'); } }); + cacheUtilizationPromise.then(cacheUtilization => { + if (cacheUtilization != null && cacheUtilization.SearchCacheSizeBytes != null) + { + console.log(cacheUtilization); + document.querySelector('#cacheUtilization').innerText = + `${(cacheUtilization.SearchCacheSizeBytes / (1024 * 1024)).toFixed(2)}MiB`; + } else { + // TODO add toast + console.error('Failed to fetch searchdomain cache utilization'); + } + }); } function clearEntitiesTable() { diff --git a/src/Shared/Models/SearchdomainModels.cs b/src/Shared/Models/SearchdomainModels.cs index de9ceac..646d701 100644 --- a/src/Shared/Models/SearchdomainModels.cs +++ b/src/Shared/Models/SearchdomainModels.cs @@ -8,6 +8,25 @@ public readonly struct ResultItem(float score, string name) public readonly float Score { get; } = score; [JsonPropertyName("Name")] public readonly string Name { get; } = name; + + public static long EstimateSize(ResultItem item) + { + long size = 0; + + // string object + if (item.Name != null) + { + size += MemorySizes.ObjectHeader; + size += sizeof(int); // string length + size += item.Name.Length * sizeof(char); + size = Align(size); + } + + return size; + } + + private static long Align(long size) + => (size + 7) & ~7; // 8-byte alignment } public struct DateTimedSearchResult(DateTime dateTime, List results) @@ -16,6 +35,57 @@ public struct DateTimedSearchResult(DateTime dateTime, List results) public List AccessDateTimes { get; set; } = [dateTime]; [JsonPropertyName("Results")] public List Results { get; set; } = results; + + public long EstimateSize() + { + long size = 0; + + size += EstimateDateTimeList(AccessDateTimes); + size += EstimateResultItemList(Results); + + return size; + } + + private static long EstimateDateTimeList(List? list) + { + if (list == null) + return 0; + + long size = 0; + + // List object + size += MemorySizes.ObjectHeader; + size += MemorySizes.Reference; // reference to array + + // Internal array + size += MemorySizes.ArrayHeader; + size += list.Capacity * sizeof(long); // DateTime = 8 bytes + + return size; + } + + private static long EstimateResultItemList(List? list) + { + if (list == null) + return 0; + + long size = 0; + + // List object + size += MemorySizes.ObjectHeader; + size += MemorySizes.Reference; + + // Internal array of structs + size += MemorySizes.ArrayHeader; + int resultItemInlineSize = sizeof(float) + IntPtr.Size; // float + string reference + size += list.Capacity * resultItemInlineSize; + + // Heap allocations referenced by ResultItem + foreach (var item in list) + size += ResultItem.EstimateSize(item); + + return size; + } } public struct SearchdomainSettings(bool cacheReconciliation = false) @@ -23,3 +93,14 @@ public struct SearchdomainSettings(bool cacheReconciliation = false) [JsonPropertyName("CacheReconciliation")] public bool CacheReconciliation { get; set; } = cacheReconciliation; } + +internal static class MemorySizes +{ + public static readonly int PointerSize = IntPtr.Size; + public static readonly int ObjectHeader = PointerSize * 2; + public static readonly int Reference = PointerSize; + public static readonly int ArrayHeader = Align(ObjectHeader + sizeof(int)); + + public static int Align(int size) + => (size + PointerSize - 1) & ~(PointerSize - 1); +} \ No newline at end of file diff --git a/src/Shared/Models/SearchdomainResults.cs b/src/Shared/Models/SearchdomainResults.cs index f0d3ec4..2585d86 100644 --- a/src/Shared/Models/SearchdomainResults.cs +++ b/src/Shared/Models/SearchdomainResults.cs @@ -65,4 +65,25 @@ public class SearchdomainSettingsResults [JsonPropertyName("Settings")] public required SearchdomainSettings? Settings { get; set; } +} + +public class SearchdomainSearchCacheSizeResults +{ + [JsonPropertyName("Success")] + public required bool Success { get; set; } + + [JsonPropertyName("Message")] + public string? Message { get; set; } + + [JsonPropertyName("SearchCacheSizeBytes")] + public required long? SearchCacheSizeBytes { get; set; } +} + +public class SearchdomainInvalidateCacheResults +{ + [JsonPropertyName("Success")] + public required bool Success { get; set; } + + [JsonPropertyName("Message")] + public string? Message { get; set; } } \ No newline at end of file