286 lines
10 KiB
C#
Executable File
286 lines
10 KiB
C#
Executable File
using Microsoft.CodeAnalysis;
|
|
using Microsoft.CodeAnalysis.CSharp;
|
|
using Microsoft.Extensions.Caching.Memory;
|
|
using Microsoft.Extensions.Logging;
|
|
using System;
|
|
using System.Collections.Concurrent;
|
|
using System.IO;
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace MarketAlly.AIPlugin.Refactoring.Caching
|
|
{
|
|
public interface ISyntaxTreeCache
|
|
{
|
|
Task<SyntaxTree> GetOrCreateAsync(string filePath, CancellationToken cancellationToken = default);
|
|
Task<SyntaxTree> GetOrCreateAsync(string filePath, string content, CancellationToken cancellationToken = default);
|
|
void Invalidate(string filePath);
|
|
void Clear();
|
|
CacheStatistics GetStatistics();
|
|
}
|
|
|
|
public record CacheStatistics(
|
|
int TotalEntries,
|
|
long TotalHits,
|
|
long TotalMisses,
|
|
double HitRatio,
|
|
long TotalMemoryBytes);
|
|
|
|
public class SyntaxTreeCache : ISyntaxTreeCache, IDisposable
|
|
{
|
|
private readonly IMemoryCache _cache;
|
|
private readonly ILogger<SyntaxTreeCache>? _logger;
|
|
private readonly ConcurrentDictionary<string, FileSystemWatcher> _watchers = new();
|
|
private readonly ConcurrentDictionary<string, DateTime> _lastModified = new();
|
|
private readonly object _statsLock = new();
|
|
|
|
private long _hits = 0;
|
|
private long _misses = 0;
|
|
private bool _disposed = false;
|
|
|
|
public SyntaxTreeCache(IMemoryCache? cache = null, ILogger<SyntaxTreeCache>? logger = null)
|
|
{
|
|
_cache = cache ?? new MemoryCache(new MemoryCacheOptions
|
|
{
|
|
SizeLimit = 1000, // Max 1000 entries
|
|
CompactionPercentage = 0.1 // Remove 10% when full
|
|
});
|
|
_logger = logger;
|
|
}
|
|
|
|
public async Task<SyntaxTree> GetOrCreateAsync(string filePath, CancellationToken cancellationToken = default)
|
|
{
|
|
if (string.IsNullOrEmpty(filePath))
|
|
throw new ArgumentNullException(nameof(filePath));
|
|
|
|
var normalizedPath = Path.GetFullPath(filePath);
|
|
var fileInfo = new FileInfo(normalizedPath);
|
|
|
|
if (!fileInfo.Exists)
|
|
throw new FileNotFoundException($"File not found: {filePath}");
|
|
|
|
var cacheKey = GenerateCacheKey(normalizedPath, fileInfo.LastWriteTimeUtc);
|
|
|
|
if (_cache.TryGetValue(cacheKey, out SyntaxTree cachedTree))
|
|
{
|
|
Interlocked.Increment(ref _hits);
|
|
_logger?.LogDebug("Cache hit for {FilePath}", normalizedPath);
|
|
return cachedTree;
|
|
}
|
|
|
|
Interlocked.Increment(ref _misses);
|
|
_logger?.LogDebug("Cache miss for {FilePath}", normalizedPath);
|
|
|
|
// Parse the file
|
|
var content = await File.ReadAllTextAsync(normalizedPath, cancellationToken);
|
|
var syntaxTree = await ParseFileAsync(normalizedPath, content, cancellationToken);
|
|
|
|
// Cache the result
|
|
var cacheOptions = new MemoryCacheEntryOptions
|
|
{
|
|
Size = EstimateTreeSize(syntaxTree),
|
|
SlidingExpiration = TimeSpan.FromMinutes(30),
|
|
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(2),
|
|
Priority = CacheItemPriority.Normal
|
|
};
|
|
|
|
_cache.Set(cacheKey, syntaxTree, cacheOptions);
|
|
|
|
// Set up file watching for cache invalidation
|
|
EnsureFileWatcher(normalizedPath);
|
|
_lastModified[normalizedPath] = fileInfo.LastWriteTimeUtc;
|
|
|
|
return syntaxTree;
|
|
}
|
|
|
|
public async Task<SyntaxTree> GetOrCreateAsync(string filePath, string content, CancellationToken cancellationToken = default)
|
|
{
|
|
if (string.IsNullOrEmpty(filePath))
|
|
throw new ArgumentNullException(nameof(filePath));
|
|
|
|
if (content == null)
|
|
throw new ArgumentNullException(nameof(content));
|
|
|
|
var normalizedPath = Path.GetFullPath(filePath);
|
|
var contentHash = ComputeContentHash(content);
|
|
var cacheKey = $"{normalizedPath}:{contentHash}";
|
|
|
|
if (_cache.TryGetValue(cacheKey, out SyntaxTree cachedTree))
|
|
{
|
|
Interlocked.Increment(ref _hits);
|
|
_logger?.LogDebug("Cache hit for content-based key {FilePath}", normalizedPath);
|
|
return cachedTree;
|
|
}
|
|
|
|
Interlocked.Increment(ref _misses);
|
|
_logger?.LogDebug("Cache miss for content-based key {FilePath}", normalizedPath);
|
|
|
|
var syntaxTree = await ParseFileAsync(normalizedPath, content, cancellationToken);
|
|
|
|
var cacheOptions = new MemoryCacheEntryOptions
|
|
{
|
|
Size = EstimateTreeSize(syntaxTree),
|
|
SlidingExpiration = TimeSpan.FromMinutes(30),
|
|
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(2),
|
|
Priority = CacheItemPriority.Normal
|
|
};
|
|
|
|
_cache.Set(cacheKey, syntaxTree, cacheOptions);
|
|
|
|
return syntaxTree;
|
|
}
|
|
|
|
public void Invalidate(string filePath)
|
|
{
|
|
var normalizedPath = Path.GetFullPath(filePath);
|
|
|
|
// Remove from last modified tracking
|
|
_lastModified.TryRemove(normalizedPath, out _);
|
|
|
|
// We can't easily remove specific entries from MemoryCache without knowing the exact key
|
|
// In a production system, you might want to use a more sophisticated cache implementation
|
|
// For now, we'll rely on the file watcher to handle invalidation on the next access
|
|
|
|
_logger?.LogDebug("Invalidated cache entries for {FilePath}", normalizedPath);
|
|
}
|
|
|
|
public void Clear()
|
|
{
|
|
if (_cache is MemoryCache memoryCache)
|
|
{
|
|
memoryCache.Compact(1.0); // Remove all entries
|
|
}
|
|
|
|
_lastModified.Clear();
|
|
|
|
// Reset statistics
|
|
lock (_statsLock)
|
|
{
|
|
_hits = 0;
|
|
_misses = 0;
|
|
}
|
|
|
|
_logger?.LogInformation("Cache cleared");
|
|
}
|
|
|
|
public CacheStatistics GetStatistics()
|
|
{
|
|
lock (_statsLock)
|
|
{
|
|
var totalRequests = _hits + _misses;
|
|
var hitRatio = totalRequests > 0 ? (double)_hits / totalRequests : 0.0;
|
|
|
|
// Estimate memory usage (rough approximation)
|
|
var memoryUsage = _lastModified.Count * 1024L; // Rough estimate
|
|
|
|
return new CacheStatistics(
|
|
TotalEntries: _lastModified.Count,
|
|
TotalHits: _hits,
|
|
TotalMisses: _misses,
|
|
HitRatio: hitRatio,
|
|
TotalMemoryBytes: memoryUsage
|
|
);
|
|
}
|
|
}
|
|
|
|
private async Task<SyntaxTree> ParseFileAsync(string filePath, string content, CancellationToken cancellationToken)
|
|
{
|
|
return await Task.Run(() =>
|
|
CSharpSyntaxTree.ParseText(content, path: filePath, cancellationToken: cancellationToken),
|
|
cancellationToken);
|
|
}
|
|
|
|
private string GenerateCacheKey(string filePath, DateTime lastModified)
|
|
{
|
|
return $"{filePath}:{lastModified.Ticks}";
|
|
}
|
|
|
|
private string ComputeContentHash(string content)
|
|
{
|
|
using var sha256 = SHA256.Create();
|
|
var hashBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(content));
|
|
return Convert.ToBase64String(hashBytes);
|
|
}
|
|
|
|
private int EstimateTreeSize(SyntaxTree syntaxTree)
|
|
{
|
|
// Rough estimation of memory size
|
|
// In practice, you might want a more accurate calculation
|
|
var text = syntaxTree.GetText();
|
|
return text.Length * 2; // Rough estimate: 2 bytes per character
|
|
}
|
|
|
|
private void EnsureFileWatcher(string filePath)
|
|
{
|
|
var directory = Path.GetDirectoryName(filePath);
|
|
var fileName = Path.GetFileName(filePath);
|
|
|
|
if (string.IsNullOrEmpty(directory) || _watchers.ContainsKey(filePath))
|
|
return;
|
|
|
|
try
|
|
{
|
|
var watcher = new FileSystemWatcher(directory, fileName)
|
|
{
|
|
NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.Size,
|
|
EnableRaisingEvents = true
|
|
};
|
|
|
|
watcher.Changed += (sender, e) => OnFileChanged(e.FullPath);
|
|
watcher.Deleted += (sender, e) => OnFileChanged(e.FullPath);
|
|
|
|
_watchers[filePath] = watcher;
|
|
|
|
_logger?.LogDebug("File watcher set up for {FilePath}", filePath);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger?.LogWarning(ex, "Failed to set up file watcher for {FilePath}", filePath);
|
|
}
|
|
}
|
|
|
|
private void OnFileChanged(string filePath)
|
|
{
|
|
_logger?.LogDebug("File changed: {FilePath}", filePath);
|
|
Invalidate(filePath);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (_disposed)
|
|
return;
|
|
|
|
foreach (var watcher in _watchers.Values)
|
|
{
|
|
try
|
|
{
|
|
watcher.Dispose();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger?.LogError(ex, "Error disposing file watcher");
|
|
}
|
|
}
|
|
|
|
_watchers.Clear();
|
|
_cache?.Dispose();
|
|
_disposed = true;
|
|
}
|
|
}
|
|
|
|
// Static factory for easy access
|
|
public static class SyntaxTreeCacheFactory
|
|
{
|
|
private static readonly Lazy<ISyntaxTreeCache> _defaultInstance =
|
|
new(() => new SyntaxTreeCache());
|
|
|
|
public static ISyntaxTreeCache Default => _defaultInstance.Value;
|
|
|
|
public static ISyntaxTreeCache Create(IMemoryCache? cache = null, ILogger<SyntaxTreeCache>? logger = null)
|
|
{
|
|
return new SyntaxTreeCache(cache, logger);
|
|
}
|
|
}
|
|
} |