244 lines
6.4 KiB
C#
Executable File
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;
|
|
}
|
|
} |