Merge pull request #80 from LD-Reborn/77-fix-long-loading-times-for-entity-count-and-query-cache-utilization

Replaced GetEmbeddingCacheSize with GetStats, fixed long loading time…
This commit is contained in:
LD50
2026-01-02 02:05:24 +01:00
committed by GitHub
9 changed files with 140 additions and 76 deletions

View File

@@ -247,17 +247,13 @@ public class SearchdomainController : ControllerBase
[HttpGet("QueryCache/Size")]
public ActionResult<SearchdomainSearchCacheSizeResults> 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<string, DateTimedSearchResult> 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 });
}
/// <summary>
@@ -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 });
}
}
}

View File

@@ -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<EmbeddingSearchOptions> _options;
public ServerController(ILogger<ServerController> logger, IConfiguration config, AIProvider aIProvider, SearchdomainManager searchdomainManager)
public ServerController(ILogger<ServerController> logger, IConfiguration config, AIProvider aIProvider, SearchdomainManager searchdomainManager, IOptions<EmbeddingSearchOptions> options)
{
_logger = logger;
_config = config;
_aIProvider = aIProvider;
_searchdomainManager = searchdomainManager;
_options = options;
}
/// <summary>
@@ -47,31 +51,51 @@ public class ServerController : ControllerBase
}
/// <summary>
/// Gets the total memory size of the embedding cache
/// Gets numeric info regarding the searchdomains
/// </summary>
[HttpGet("EmbeddingCache/Size")]
public ActionResult<ServerGetEmbeddingCacheSizeResult> GetEmbeddingCacheSize()
[HttpGet("Stats")]
public async Task<ActionResult<ServerGetStatsResult>> Stats()
{
long size = 0;
long elementCount = 0;
long embeddingsCount = 0;
LRUCache<string, Dictionary<string, float[]>> 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<string> cacheListOriginal = (LinkedList<string>)cacheListField.GetValue(embeddingCache)!;
LinkedList<string> 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<string, Dictionary<string, float[]>> 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<string> cacheListOriginal = (LinkedList<string>)cacheListField.GetValue(embeddingCache)!;
LinkedList<string> 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<long> 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<string, float[]> value)

View File

@@ -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<DatabaseHelper> logger)
{
private readonly ILogger<DatabaseHelper> _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<string, object> parameters = [];
@@ -210,5 +221,27 @@ public class DatabaseHelper(ILogger<DatabaseHelper> logger)
attributeSumReader.Close();
return result;
}
}
public static async Task<long> 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<string, dynamic> 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;
}
}

View File

@@ -299,4 +299,9 @@ public class SearchdomainHelper(ILogger<SearchdomainHelper> logger, DatabaseHelp
return (null, 404, $"Unable to update searchdomain {searchdomain}");
}
}
public static bool IsSearchdomainLoaded(SearchdomainManager searchdomainManager, string name)
{
return searchdomainManager.IsSearchdomainLoaded(name);
}
}

View File

@@ -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<string, AiProvider> 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; }
}

View File

@@ -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;
}
}

View File

@@ -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<string, Searchdomain> searchdomains = [];
private readonly ILogger<SearchdomainManager> _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<string, Dictionary<string, float[]>> embeddingCache;
public int EmbeddingCacheMaxCount;
public long EmbeddingCacheMaxCount;
public SearchdomainManager(ILogger<SearchdomainManager> logger, IConfiguration config, AIProvider aIProvider, DatabaseHelper databaseHelper)
public SearchdomainManager(ILogger<SearchdomainManager> logger, IOptions<EmbeddingSearchOptions> options, AIProvider aIProvider, DatabaseHelper databaseHelper)
{
_logger = logger;
_config = config;
_options = options.Value;
this.aIProvider = aIProvider;
_databaseHelper = databaseHelper;
EmbeddingCacheMaxCount = config.GetValue<int?>("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);
}
}

View File

@@ -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());
}

View File

@@ -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; }
}