MarketAlly.AIPlugin.Extensions/MarketAlly.AIPlugin.Refacto.../FileCache.cs

244 lines
6.4 KiB
C#
Executable File

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using System;
using System.Collections.Concurrent;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace MarketAlly.AIPlugin.Refactoring.Plugins
{
public interface IFileCache
{
Task<SyntaxTree> GetSyntaxTreeAsync(string filePath);
Task<string> GetFileContentAsync(string filePath);
void InvalidateCache(string filePath);
void InvalidateAll();
long GetCacheSize();
void SetMaxCacheSize(long maxSizeBytes);
}
public class FileCache : IFileCache
{
private readonly ConcurrentDictionary<string, CacheEntry> _syntaxTreeCache = new();
private readonly ConcurrentDictionary<string, CacheEntry> _contentCache = new();
private readonly ReaderWriterLockSlim _cacheLock = new();
private long _maxCacheSize = 100 * 1024 * 1024; // 100MB default
private long _currentCacheSize = 0;
private class CacheEntry
{
public object Content { get; set; }
public DateTime LastAccessed { get; set; }
public DateTime FileLastModified { get; set; }
public long Size { get; set; }
public CacheEntry(object content, DateTime fileLastModified, long size)
{
Content = content;
LastAccessed = DateTime.UtcNow;
FileLastModified = fileLastModified;
Size = size;
}
}
public async Task<SyntaxTree> GetSyntaxTreeAsync(string filePath)
{
if (string.IsNullOrEmpty(filePath) || !File.Exists(filePath))
throw new FileNotFoundException($"File not found: {filePath}");
var fileInfo = new FileInfo(filePath);
var normalizedPath = Path.GetFullPath(filePath);
// Check cache first
if (_syntaxTreeCache.TryGetValue(normalizedPath, out var cachedEntry))
{
// Validate cache entry is still fresh
if (cachedEntry.FileLastModified >= fileInfo.LastWriteTime)
{
cachedEntry.LastAccessed = DateTime.UtcNow;
return (SyntaxTree)cachedEntry.Content;
}
else
{
// File has been modified, remove stale entry
_syntaxTreeCache.TryRemove(normalizedPath, out _);
Interlocked.Add(ref _currentCacheSize, -cachedEntry.Size);
}
}
// Load and parse file
var content = await File.ReadAllTextAsync(filePath);
var syntaxTree = CSharpSyntaxTree.ParseText(content, path: filePath);
// Estimate memory usage (rough approximation)
var size = content.Length * 2 + 1024; // Text + overhead
// Check if we need to evict entries before adding new one
await EnsureCacheSpace(size);
// Add to cache
var newEntry = new CacheEntry(syntaxTree, fileInfo.LastWriteTime, size);
_syntaxTreeCache.TryAdd(normalizedPath, newEntry);
Interlocked.Add(ref _currentCacheSize, size);
return syntaxTree;
}
public async Task<string> GetFileContentAsync(string filePath)
{
if (string.IsNullOrEmpty(filePath) || !File.Exists(filePath))
throw new FileNotFoundException($"File not found: {filePath}");
var fileInfo = new FileInfo(filePath);
var normalizedPath = Path.GetFullPath(filePath);
// Check cache first
if (_contentCache.TryGetValue(normalizedPath, out var cachedEntry))
{
// Validate cache entry is still fresh
if (cachedEntry.FileLastModified >= fileInfo.LastWriteTime)
{
cachedEntry.LastAccessed = DateTime.UtcNow;
return (string)cachedEntry.Content;
}
else
{
// File has been modified, remove stale entry
_contentCache.TryRemove(normalizedPath, out _);
Interlocked.Add(ref _currentCacheSize, -cachedEntry.Size);
}
}
// Load file content
var content = await File.ReadAllTextAsync(filePath);
var size = content.Length * 2; // Rough string memory usage
// Check if we need to evict entries before adding new one
await EnsureCacheSpace(size);
// Add to cache
var newEntry = new CacheEntry(content, fileInfo.LastWriteTime, size);
_contentCache.TryAdd(normalizedPath, newEntry);
Interlocked.Add(ref _currentCacheSize, size);
return content;
}
public void InvalidateCache(string filePath)
{
if (string.IsNullOrEmpty(filePath))
return;
var normalizedPath = Path.GetFullPath(filePath);
if (_syntaxTreeCache.TryRemove(normalizedPath, out var syntaxEntry))
{
Interlocked.Add(ref _currentCacheSize, -syntaxEntry.Size);
}
if (_contentCache.TryRemove(normalizedPath, out var contentEntry))
{
Interlocked.Add(ref _currentCacheSize, -contentEntry.Size);
}
}
public void InvalidateAll()
{
_cacheLock.EnterWriteLock();
try
{
_syntaxTreeCache.Clear();
_contentCache.Clear();
_currentCacheSize = 0;
}
finally
{
_cacheLock.ExitWriteLock();
}
}
public long GetCacheSize()
{
return _currentCacheSize;
}
public void SetMaxCacheSize(long maxSizeBytes)
{
_maxCacheSize = maxSizeBytes;
// Trigger cleanup if current size exceeds new limit
_ = Task.Run(() => EnsureCacheSpace(0));
}
private async Task EnsureCacheSpace(long requiredSpace)
{
if (_currentCacheSize + requiredSpace <= _maxCacheSize)
return;
_cacheLock.EnterWriteLock();
try
{
// Calculate how much space we need to free
var targetSize = _maxCacheSize - requiredSpace;
var toRemove = _currentCacheSize - targetSize;
if (toRemove <= 0)
return;
// Collect all entries with their access times
var allEntries = new List<(string key, CacheEntry entry, bool isSyntaxTree)>();
foreach (var kvp in _syntaxTreeCache)
{
allEntries.Add((kvp.Key, kvp.Value, true));
}
foreach (var kvp in _contentCache)
{
allEntries.Add((kvp.Key, kvp.Value, false));
}
// Sort by last accessed time (LRU)
allEntries.Sort((a, b) => a.entry.LastAccessed.CompareTo(b.entry.LastAccessed));
// Remove oldest entries until we have enough space
long freedSpace = 0;
foreach (var (key, entry, isSyntaxTree) in allEntries)
{
if (freedSpace >= toRemove)
break;
if (isSyntaxTree)
{
_syntaxTreeCache.TryRemove(key, out _);
}
else
{
_contentCache.TryRemove(key, out _);
}
freedSpace += entry.Size;
Interlocked.Add(ref _currentCacheSize, -entry.Size);
}
}
finally
{
_cacheLock.ExitWriteLock();
}
}
public void Dispose()
{
_cacheLock?.Dispose();
}
}
// Singleton instance for global use across plugins
public static class GlobalFileCache
{
private static readonly Lazy<IFileCache> _instance = new Lazy<IFileCache>(() => new FileCache());
public static IFileCache Instance => _instance.Value;
}
}