Replaced GetEmbeddingCacheSize with GetStats, fixed long loading times for front-end stats retrieval
This commit is contained in:
@@ -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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user