284 lines
8.7 KiB
C#
Executable File
284 lines
8.7 KiB
C#
Executable File
using Microsoft.Extensions.Caching.Memory;
|
|
using Microsoft.Extensions.Logging;
|
|
using MarketAlly.AIPlugin.Context.Configuration;
|
|
using System.Collections.Concurrent;
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
|
|
namespace MarketAlly.AIPlugin.Context.Performance
|
|
{
|
|
/// <summary>
|
|
/// Manages caching for search results and frequently accessed context data
|
|
/// </summary>
|
|
public class CacheManager : IDisposable
|
|
{
|
|
private readonly MemoryCache _searchCache;
|
|
private readonly MemoryCache _contextCache;
|
|
private readonly ContextConfiguration _configuration;
|
|
private readonly ILogger<CacheManager> _logger;
|
|
private readonly ConcurrentDictionary<string, SemaphoreSlim> _cacheLocks;
|
|
private readonly Timer _cleanupTimer;
|
|
|
|
public CacheManager(ContextConfiguration configuration, ILogger<CacheManager> logger)
|
|
{
|
|
_configuration = configuration;
|
|
_logger = logger;
|
|
_cacheLocks = new ConcurrentDictionary<string, SemaphoreSlim>();
|
|
|
|
var cacheOptions = new MemoryCacheOptions
|
|
{
|
|
SizeLimit = _configuration.Performance.CacheSizeLimit,
|
|
CompactionPercentage = _configuration.Performance.CacheCompactionPercentage
|
|
};
|
|
|
|
_searchCache = new MemoryCache(cacheOptions);
|
|
_contextCache = new MemoryCache(cacheOptions);
|
|
|
|
// Setup cleanup timer to run every 10 minutes
|
|
_cleanupTimer = new Timer(CleanupExpiredEntries, null, TimeSpan.FromMinutes(10), TimeSpan.FromMinutes(10));
|
|
|
|
_logger.LogDebug("Cache manager initialized with size limit: {SizeLimit}", cacheOptions.SizeLimit);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets cached search results for a query
|
|
/// </summary>
|
|
public async Task<SearchResults?> GetCachedSearchResultsAsync(string query, string projectPath, SearchParameters parameters)
|
|
{
|
|
if (!_configuration.Search.EnableCaching)
|
|
return null;
|
|
|
|
var cacheKey = GenerateSearchCacheKey(query, projectPath, parameters);
|
|
|
|
if (_searchCache.TryGetValue(cacheKey, out SearchResults? results))
|
|
{
|
|
_logger.LogDebug("Cache hit for search query: {Query}", query);
|
|
return results;
|
|
}
|
|
|
|
_logger.LogDebug("Cache miss for search query: {Query}", query);
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Caches search results for future use
|
|
/// </summary>
|
|
public async Task CacheSearchResultsAsync(string query, string projectPath, SearchParameters parameters, SearchResults results)
|
|
{
|
|
if (!_configuration.Search.EnableCaching)
|
|
return;
|
|
|
|
var cacheKey = GenerateSearchCacheKey(query, projectPath, parameters);
|
|
var lockKey = $"search_{cacheKey}";
|
|
|
|
var lockObject = _cacheLocks.GetOrAdd(lockKey, _ => new SemaphoreSlim(1, 1));
|
|
|
|
await lockObject.WaitAsync();
|
|
try
|
|
{
|
|
var cacheEntryOptions = new MemoryCacheEntryOptions
|
|
{
|
|
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(_configuration.Search.CacheExpirationMinutes),
|
|
Size = EstimateSearchResultsSize(results),
|
|
Priority = CacheItemPriority.Normal
|
|
};
|
|
|
|
_searchCache.Set(cacheKey, results, cacheEntryOptions);
|
|
_logger.LogDebug("Cached search results for query: {Query}, expires in {Minutes} minutes",
|
|
query, _configuration.Search.CacheExpirationMinutes);
|
|
}
|
|
finally
|
|
{
|
|
lockObject.Release();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets cached context entries for a file
|
|
/// </summary>
|
|
public async Task<List<StoredContextEntry>?> GetCachedContextEntriesAsync(string filePath)
|
|
{
|
|
var cacheKey = GenerateFileCacheKey(filePath);
|
|
|
|
if (_contextCache.TryGetValue(cacheKey, out List<StoredContextEntry>? entries))
|
|
{
|
|
var fileInfo = new System.IO.FileInfo(filePath);
|
|
if (_contextCache.TryGetValue($"{cacheKey}_timestamp", out DateTime cachedTimestamp) &&
|
|
cachedTimestamp >= fileInfo.LastWriteTime)
|
|
{
|
|
_logger.LogDebug("Cache hit for context file: {FilePath}", filePath);
|
|
return entries;
|
|
}
|
|
else
|
|
{
|
|
// File has been modified, remove from cache
|
|
_contextCache.Remove(cacheKey);
|
|
_contextCache.Remove($"{cacheKey}_timestamp");
|
|
}
|
|
}
|
|
|
|
_logger.LogDebug("Cache miss for context file: {FilePath}", filePath);
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Caches context entries for a file
|
|
/// </summary>
|
|
public async Task CacheContextEntriesAsync(string filePath, List<StoredContextEntry> entries)
|
|
{
|
|
var cacheKey = GenerateFileCacheKey(filePath);
|
|
var lockKey = $"context_{cacheKey}";
|
|
|
|
var lockObject = _cacheLocks.GetOrAdd(lockKey, _ => new SemaphoreSlim(1, 1));
|
|
|
|
await lockObject.WaitAsync();
|
|
try
|
|
{
|
|
var cacheEntryOptions = new MemoryCacheEntryOptions
|
|
{
|
|
SlidingExpiration = TimeSpan.FromMinutes(30), // Keep in cache if accessed within 30 minutes
|
|
Size = EstimateContextEntriesSize(entries),
|
|
Priority = CacheItemPriority.High // Context entries are high priority
|
|
};
|
|
|
|
_contextCache.Set(cacheKey, entries, cacheEntryOptions);
|
|
_contextCache.Set($"{cacheKey}_timestamp", File.GetLastWriteTime(filePath), cacheEntryOptions);
|
|
|
|
_logger.LogDebug("Cached {Count} context entries for file: {FilePath}", entries.Count, filePath);
|
|
}
|
|
finally
|
|
{
|
|
lockObject.Release();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Invalidates cache entries for a specific file
|
|
/// </summary>
|
|
public void InvalidateFileCache(string filePath)
|
|
{
|
|
var cacheKey = GenerateFileCacheKey(filePath);
|
|
_contextCache.Remove(cacheKey);
|
|
_contextCache.Remove($"{cacheKey}_timestamp");
|
|
_logger.LogDebug("Invalidated cache for file: {FilePath}", filePath);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Invalidates all search cache entries for a project
|
|
/// </summary>
|
|
public void InvalidateProjectSearchCache(string projectPath)
|
|
{
|
|
// Unfortunately, MemoryCache doesn't support wildcard removal
|
|
// In a production system, you might want to use a more sophisticated cache like Redis
|
|
_searchCache.Clear();
|
|
_logger.LogInformation("Cleared search cache for project changes");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets cache statistics
|
|
/// </summary>
|
|
public CacheStatistics GetStatistics()
|
|
{
|
|
// Note: MemoryCache doesn't expose detailed statistics
|
|
// In production, you might want to track these manually
|
|
return new CacheStatistics
|
|
{
|
|
SearchCacheEnabled = _configuration.Search.EnableCaching,
|
|
ContextCacheEnabled = true,
|
|
CacheSizeLimit = _configuration.Performance.CacheSizeLimit,
|
|
CompactionPercentage = _configuration.Performance.CacheCompactionPercentage
|
|
};
|
|
}
|
|
|
|
private string GenerateSearchCacheKey(string query, string projectPath, SearchParameters parameters)
|
|
{
|
|
var keyBuilder = new StringBuilder();
|
|
keyBuilder.Append($"search_{query}_{projectPath}");
|
|
keyBuilder.Append($"_{parameters.ContextType}_{parameters.Priority}");
|
|
keyBuilder.Append($"_{parameters.DaysBack}_{parameters.MaxResults}");
|
|
keyBuilder.Append($"_{parameters.IncludeContent}");
|
|
|
|
if (!string.IsNullOrEmpty(parameters.Tags))
|
|
{
|
|
keyBuilder.Append($"_{parameters.Tags}");
|
|
}
|
|
|
|
return GenerateHashKey(keyBuilder.ToString());
|
|
}
|
|
|
|
private string GenerateFileCacheKey(string filePath)
|
|
{
|
|
return GenerateHashKey($"file_{filePath}");
|
|
}
|
|
|
|
private string GenerateHashKey(string input)
|
|
{
|
|
using var sha256 = SHA256.Create();
|
|
var hashBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(input));
|
|
return Convert.ToBase64String(hashBytes).Replace("/", "_").Replace("+", "-").TrimEnd('=');
|
|
}
|
|
|
|
private long EstimateSearchResultsSize(SearchResults results)
|
|
{
|
|
// Rough estimation: each result ~1KB
|
|
return results.Results.Count * 1024;
|
|
}
|
|
|
|
private long EstimateContextEntriesSize(List<StoredContextEntry> entries)
|
|
{
|
|
// Rough estimation based on content length
|
|
return entries.Sum(e => e.Content.Length + e.Summary.Length + 500); // 500 bytes overhead per entry
|
|
}
|
|
|
|
private void CleanupExpiredEntries(object? state)
|
|
{
|
|
try
|
|
{
|
|
_searchCache.Compact(1.0); // Force cleanup of expired entries
|
|
_contextCache.Compact(1.0);
|
|
_logger.LogDebug("Completed cache cleanup cycle");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error during cache cleanup");
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_cleanupTimer?.Dispose();
|
|
_searchCache?.Dispose();
|
|
_contextCache?.Dispose();
|
|
|
|
foreach (var lockObject in _cacheLocks.Values)
|
|
{
|
|
lockObject.Dispose();
|
|
}
|
|
_cacheLocks.Clear();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parameters for search operations used in cache key generation
|
|
/// </summary>
|
|
public class SearchParameters
|
|
{
|
|
public string ContextType { get; set; } = "all";
|
|
public string Priority { get; set; } = "all";
|
|
public int DaysBack { get; set; } = 0;
|
|
public int MaxResults { get; set; } = 10;
|
|
public bool IncludeContent { get; set; } = true;
|
|
public string? Tags { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Cache performance and configuration statistics
|
|
/// </summary>
|
|
public class CacheStatistics
|
|
{
|
|
public bool SearchCacheEnabled { get; set; }
|
|
public bool ContextCacheEnabled { get; set; }
|
|
public int CacheSizeLimit { get; set; }
|
|
public double CompactionPercentage { get; set; }
|
|
}
|
|
} |