diff --git a/src/Server/Controllers/SearchdomainController.cs b/src/Server/Controllers/SearchdomainController.cs index 30e0124..2eca1c2 100644 --- a/src/Server/Controllers/SearchdomainController.cs +++ b/src/Server/Controllers/SearchdomainController.cs @@ -247,17 +247,13 @@ public class SearchdomainController : ControllerBase [HttpGet("QueryCache/Size")] public ActionResult GetSearchCacheSize([Required]string searchdomain) { + if (!SearchdomainHelper.IsSearchdomainLoaded(_domainManager, searchdomain)) + { + return Ok(new SearchdomainSearchCacheSizeResults() { QueryCacheSizeBytes = 0, Success = true }); + } (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}); - 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() { QueryCacheSizeBytes = sizeInBytes, Success = true }); + return Ok(new SearchdomainSearchCacheSizeResults() { QueryCacheSizeBytes = searchdomain_.GetSearchCacheSize(), Success = true }); } /// @@ -284,5 +280,5 @@ public class SearchdomainController : ControllerBase if (searchdomain_ is null || httpStatusCode is not null) return StatusCode(httpStatusCode ?? 500, new SearchdomainUpdateResults(){Success = false, Message = message}); long sizeInBytes = DatabaseHelper.GetSearchdomainDatabaseSize(searchdomain_.helper, searchdomain); return Ok(new SearchdomainGetDatabaseSizeResult() { SearchdomainDatabaseSizeBytes = sizeInBytes, Success = true }); - } + } } diff --git a/src/Server/Controllers/ServerController.cs b/src/Server/Controllers/ServerController.cs index c442838..7fd23a0 100644 --- a/src/Server/Controllers/ServerController.cs +++ b/src/Server/Controllers/ServerController.cs @@ -5,8 +5,10 @@ using System.Text.Json; using AdaptiveExpressions; using ElmahCore; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; using Server.Exceptions; using Server.Helper; +using Server.Models; using Shared.Models; [ApiController] @@ -17,13 +19,15 @@ public class ServerController : ControllerBase private readonly IConfiguration _config; private AIProvider _aIProvider; private readonly SearchdomainManager _searchdomainManager; + private readonly IOptions _options; - public ServerController(ILogger logger, IConfiguration config, AIProvider aIProvider, SearchdomainManager searchdomainManager) + public ServerController(ILogger logger, IConfiguration config, AIProvider aIProvider, SearchdomainManager searchdomainManager, IOptions options) { _logger = logger; _config = config; _aIProvider = aIProvider; _searchdomainManager = searchdomainManager; + _options = options; } /// @@ -47,31 +51,51 @@ public class ServerController : ControllerBase } /// - /// Gets the total memory size of the embedding cache + /// Gets numeric info regarding the searchdomains /// - [HttpGet("EmbeddingCache/Size")] - public ActionResult GetEmbeddingCacheSize() + [HttpGet("Stats")] + public async Task> Stats() { - long size = 0; - long elementCount = 0; - long embeddingsCount = 0; - LRUCache> embeddingCache = _searchdomainManager.embeddingCache; - var cacheListField = embeddingCache.GetType() - .GetField("_cacheList", BindingFlags.Instance | BindingFlags.NonPublic) ?? throw new InvalidOperationException("_cacheList field not found"); // TODO Remove this unsafe reflection atrocity - LinkedList cacheListOriginal = (LinkedList)cacheListField.GetValue(embeddingCache)!; - LinkedList cacheList = new(cacheListOriginal); - - foreach (string key in cacheList) + try { - if (!embeddingCache.TryGet(key, out var entry)) - continue; + long size = 0; + long elementCount = 0; + long embeddingsCount = 0; + LRUCache> embeddingCache = _searchdomainManager.embeddingCache; + var cacheListField = embeddingCache.GetType() + .GetField("_cacheList", BindingFlags.Instance | BindingFlags.NonPublic) ?? throw new InvalidOperationException("_cacheList field not found"); // TODO Remove this unsafe reflection atrocity + LinkedList cacheListOriginal = (LinkedList)cacheListField.GetValue(embeddingCache)!; + LinkedList cacheList = new(cacheListOriginal); - // estimate size - size += EstimateEntrySize(key, entry); - elementCount++; - embeddingsCount += entry.Keys.Count; + foreach (string key in cacheList) + { + if (!embeddingCache.TryGet(key, out var entry)) + continue; + + // estimate size + size += EstimateEntrySize(key, entry); + elementCount++; + embeddingsCount += entry.Keys.Count; + } + var sqlHelper = DatabaseHelper.GetSQLHelper(_options.Value); + Task entityCountTask = DatabaseHelper.CountEntities(sqlHelper); + long queryCacheUtilization = 0; + foreach (string searchdomain in _searchdomainManager.ListSearchdomains()) + { + if (SearchdomainHelper.IsSearchdomainLoaded(_searchdomainManager, searchdomain)) + { + (Searchdomain? searchdomain_, int? httpStatusCode, string? message) = SearchdomainHelper.TryGetSearchdomain(_searchdomainManager, searchdomain, _logger); + if (searchdomain_ is null || httpStatusCode is not null) return StatusCode(httpStatusCode ?? 500, new ServerGetStatsResult(){Success = false, Message = message}); + queryCacheUtilization += searchdomain_.GetSearchCacheSize(); + } + }; + long entityCount = await entityCountTask; + return new ServerGetStatsResult() { Success = true, EntityCount = entityCount, QueryCacheUtilization = queryCacheUtilization, SizeInBytes = size, MaxElementCount = _searchdomainManager.EmbeddingCacheMaxCount, ElementCount = elementCount, EmbeddingsCount = embeddingsCount}; + } catch (Exception ex) + { + ElmahExtensions.RaiseError(ex); + return StatusCode(500, new ServerGetStatsResult(){Success = false, Message = ex.Message}); } - return new ServerGetEmbeddingCacheSizeResult() { Success = true, SizeInBytes = size, MaxElementCount = _searchdomainManager.EmbeddingCacheMaxCount, ElementCount = elementCount, EmbeddingsCount = embeddingsCount}; } private static long EstimateEntrySize(string key, Dictionary value) diff --git a/src/Server/Helper/DatabaseHelper.cs b/src/Server/Helper/DatabaseHelper.cs index 2904659..c4336c8 100644 --- a/src/Server/Helper/DatabaseHelper.cs +++ b/src/Server/Helper/DatabaseHelper.cs @@ -1,6 +1,9 @@ +using System.Configuration; using System.Data.Common; using System.Text; +using MySql.Data.MySqlClient; using Server.Exceptions; +using Server.Models; using Shared.Models; namespace Server.Helper; @@ -9,6 +12,14 @@ public class DatabaseHelper(ILogger logger) { private readonly ILogger _logger = logger; + public static SQLHelper GetSQLHelper(EmbeddingSearchOptions embeddingSearchOptions) + { + string connectionString = embeddingSearchOptions.ConnectionStrings.SQL; + MySqlConnection connection = new(connectionString); + connection.Open(); + return new SQLHelper(connection, connectionString); + } + public static void DatabaseInsertEmbeddingBulk(SQLHelper helper, int id_datapoint, List<(string model, byte[] embedding)> data) { Dictionary parameters = []; @@ -210,5 +221,27 @@ public class DatabaseHelper(ILogger logger) attributeSumReader.Close(); return result; - } + } + + public static async Task CountEntities(SQLHelper helper) + { + DbDataReader searchdomainSumReader = helper.ExecuteSQLCommand("SELECT COUNT(*) FROM entity;", []); + bool success = searchdomainSumReader.Read(); + long result = success && !searchdomainSumReader.IsDBNull(0) ? searchdomainSumReader.GetInt64(0) : 0; + searchdomainSumReader.Close(); + return result; + } + + public static long CountEntitiesForSearchdomain(SQLHelper helper, string searchdomain) + { + Dictionary parameters = new() + { + { "searchdomain", searchdomain} + }; + DbDataReader searchdomainSumReader = helper.ExecuteSQLCommand("SELECT COUNT(*) FROM entity e JOIN searchdomain s on e.id_searchdomain = s.id WHERE e.id_searchdomain = s.id AND s.name = @searchdomain;", parameters); + bool success = searchdomainSumReader.Read(); + long result = success && !searchdomainSumReader.IsDBNull(0) ? searchdomainSumReader.GetInt64(0) : 0; + searchdomainSumReader.Close(); + return result; + } } \ No newline at end of file diff --git a/src/Server/Helper/SearchdomainHelper.cs b/src/Server/Helper/SearchdomainHelper.cs index 69b271f..5b4d256 100644 --- a/src/Server/Helper/SearchdomainHelper.cs +++ b/src/Server/Helper/SearchdomainHelper.cs @@ -299,4 +299,9 @@ public class SearchdomainHelper(ILogger logger, DatabaseHelp return (null, 404, $"Unable to update searchdomain {searchdomain}"); } } + + public static bool IsSearchdomainLoaded(SearchdomainManager searchdomainManager, string name) + { + return searchdomainManager.IsSearchdomainLoaded(name); + } } \ No newline at end of file diff --git a/src/Server/Models/ConfigModels.cs b/src/Server/Models/ConfigModels.cs index af55494..321c642 100644 --- a/src/Server/Models/ConfigModels.cs +++ b/src/Server/Models/ConfigModels.cs @@ -6,7 +6,7 @@ namespace Server.Models; public class EmbeddingSearchOptions : ApiKeyOptions { - public required ConnectionStringsSection ConnectionStrings { get; set; } + public required ConnectionStringsOptions ConnectionStrings { get; set; } public ElmahOptions? Elmah { get; set; } public required long EmbeddingCacheMaxCount { get; set; } public required Dictionary AiProviders { get; set; } @@ -34,3 +34,8 @@ public class SimpleUser public string Password { get; set; } = ""; public string[] Roles { get; set; } = []; } + +public class ConnectionStringsOptions +{ + public required string SQL { get; set; } +} \ No newline at end of file diff --git a/src/Server/Searchdomain.cs b/src/Server/Searchdomain.cs index c764203..c5a1971 100644 --- a/src/Server/Searchdomain.cs +++ b/src/Server/Searchdomain.cs @@ -339,4 +339,16 @@ public class Searchdomain { searchCache = []; } + + public long GetSearchCacheSize() + { + 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 sizeInBytes; + } } diff --git a/src/Server/SearchdomainManager.cs b/src/Server/SearchdomainManager.cs index 8a00b4c..dbc45af 100644 --- a/src/Server/SearchdomainManager.cs +++ b/src/Server/SearchdomainManager.cs @@ -6,6 +6,8 @@ using Server.Exceptions; using AdaptiveExpressions; using Shared.Models; using System.Text.Json; +using Microsoft.Extensions.Options; +using Server.Models; namespace Server; @@ -13,24 +15,24 @@ public class SearchdomainManager { private Dictionary searchdomains = []; private readonly ILogger _logger; - private readonly IConfiguration _config; + private readonly EmbeddingSearchOptions _options; public readonly AIProvider aIProvider; private readonly DatabaseHelper _databaseHelper; private readonly string connectionString; private MySqlConnection connection; public SQLHelper helper; public LRUCache> embeddingCache; - public int EmbeddingCacheMaxCount; + public long EmbeddingCacheMaxCount; - public SearchdomainManager(ILogger logger, IConfiguration config, AIProvider aIProvider, DatabaseHelper databaseHelper) + public SearchdomainManager(ILogger logger, IOptions options, AIProvider aIProvider, DatabaseHelper databaseHelper) { _logger = logger; - _config = config; + _options = options.Value; this.aIProvider = aIProvider; _databaseHelper = databaseHelper; - EmbeddingCacheMaxCount = config.GetValue("Embeddingsearch:EmbeddingCacheMaxCount") ?? 1000000; - embeddingCache = new(EmbeddingCacheMaxCount); - connectionString = _config.GetSection("Embeddingsearch").GetConnectionString("SQL") ?? ""; + EmbeddingCacheMaxCount = _options.EmbeddingCacheMaxCount; + embeddingCache = new((int)EmbeddingCacheMaxCount); + connectionString = _options.ConnectionStrings.SQL; connection = new MySqlConnection(connectionString); connection.Open(); helper = new SQLHelper(connection, connectionString); @@ -122,4 +124,9 @@ public class SearchdomainManager searchdomains[name] = searchdomain; return searchdomain; } + + public bool IsSearchdomainLoaded(string name) + { + return searchdomains.ContainsKey(name); + } } diff --git a/src/Server/Views/Home/Index.cshtml b/src/Server/Views/Home/Index.cshtml index 750036f..12f5004 100644 --- a/src/Server/Views/Home/Index.cshtml +++ b/src/Server/Views/Home/Index.cshtml @@ -5,7 +5,6 @@ @using Server @inject LocalizationService T -@inject AIProvider AIProvider @model HomeIndexViewModel @{ ViewData["Title"] = "Home Page"; @@ -144,39 +143,14 @@ searchdomains = result.Searchdomains; hideThrobber(searchdomainCount); searchdomainCount.textContent = searchdomains.length; - - const perDomainPromises = searchdomains.map(async domain => { - const [entityListResult, querycacheUtilizationResult] = await Promise.all([ - listEntities(domain), - getQuerycacheUtilization(domain) - ]); - - return { - entityCount: entityListResult.Results.length, - utilization: querycacheUtilizationResult.QueryCacheSizeBytes - }; - }); - - const results = await Promise.all(perDomainPromises); - - let entityCount = 0; - let totalUtilization = 0; - - for (const r of results) { - entityCount += r.entityCount; - totalUtilization += r.utilization; - } - - hideThrobber(searchdomainEntityCount); - hideThrobber(totalQuerycacheUtilization); - searchdomainEntityCount.textContent = entityCount; - totalQuerycacheUtilization.textContent = NumberOfBytesAsHumanReadable(totalUtilization); }); - getEmbeddingcacheUtilization().then(result => { + getServerStats().then(result => { let utilization = result.SizeInBytes; let maxElementCount = result.MaxElementCount; let elementCount = result.ElementCount; let embeddingCount = result.EmbeddingsCount; + let entityCount = result.EntityCount; + let queryCacheUtilization = result.QueryCacheUtilization; hideThrobber(embeddingcacheSize); embeddingcacheSize.textContent = NumberOfBytesAsHumanReadable(utilization); hideThrobber(embeddingcacheElementCount); @@ -184,6 +158,10 @@ hideThrobber(embeddingcacheEmbeddingCount); embeddingcacheEmbeddingCount.textContent = embeddingCount; embeddingcacheElementCountProgressBar.style.width = `${elementCount / maxElementCount * 100}%`; + hideThrobber(searchdomainEntityCount); + searchdomainEntityCount.textContent = entityCount; + hideThrobber(totalQuerycacheUtilization); + totalQuerycacheUtilization.textContent = NumberOfBytesAsHumanReadable(queryCacheUtilization); }); getHealthCheckStatusAndApply(healthchecksServer, "/healthz/Database"); getHealthCheckStatusAndApply(healthchecksAiProvider, "/healthz/AIProvider"); @@ -206,8 +184,8 @@ .then(r => r.json()); } - async function getEmbeddingcacheUtilization() { - return await fetch(`/Server/EmbeddingCache/Size`) + async function getServerStats() { + return await fetch(`/Server/Stats`) .then(r => r.json()); } diff --git a/src/Shared/Models/ServerModels.cs b/src/Shared/Models/ServerModels.cs index e761c31..2ad0eae 100644 --- a/src/Shared/Models/ServerModels.cs +++ b/src/Shared/Models/ServerModels.cs @@ -8,14 +8,18 @@ public class ServerGetModelsResult : SuccesMessageBaseModel public string[]? Models { get; set; } } -public class ServerGetEmbeddingCacheSizeResult : SuccesMessageBaseModel +public class ServerGetStatsResult : SuccesMessageBaseModel { [JsonPropertyName("SizeInBytes")] - public required long? SizeInBytes { get; set; } + public long? SizeInBytes { get; set; } [JsonPropertyName("MaxElementCount")] - public required long? MaxElementCount { get; set; } + public long? MaxElementCount { get; set; } [JsonPropertyName("ElementCount")] - public required long? ElementCount { get; set; } + public long? ElementCount { get; set; } [JsonPropertyName("EmbeddingsCount")] - public required long? EmbeddingsCount { get; set; } + public long? EmbeddingsCount { get; set; } + [JsonPropertyName("EntityCount")] + public long? EntityCount { get; set; } + [JsonPropertyName("QueryCacheUtilization")] + public long? QueryCacheUtilization { get; set; } } \ No newline at end of file