Кеширование скриптов

Система кеширования IgdrasilScripting позволяет избежать повторной компиляции часто используемых скриптов через интерфейс IScriptCache<TResult>.

Основы кеширования

Получение кеша

var engine = new LuaEngine();
var cache = engine.Cache;

if (cache != null)
{
    Console.WriteLine($"Кешировано скриптов: {cache.CachedCount}");
}

Работа с кешем

Проверка наличия

string scriptHash = ComputeHash("return 42");

if (cache.Contains(scriptHash))
{
    Console.WriteLine("Скрипт уже в кеше");
}

Получение из кеша

var cachedScript = cache.TryGetCached(scriptHash);
if (cachedScript != null)
{
    // Использовать закешированный скрипт
    var result = cachedScript.Evaluate();
}
else
{
    // Скомпилировать и кешировать
    var newScript = engine.Compile("return 42");
    cache.Cache(scriptHash, newScript);
}

Удаление из кеша

// Удалить конкретный скрипт
cache.Remove(scriptHash);

// Очистить весь кеш
cache.Clear();

Создание кеша

public class ScriptCache<TResult> : IScriptCache<TResult>
{
    private readonly Dictionary<string, CacheEntry<TResult>> _cache = new();
    private readonly int _maxCacheSize;
    
    public int CachedCount => _cache.Count;
    
    public ScriptCache(int maxCacheSize = 1000)
    {
        _maxCacheSize = maxCacheSize;
    }
    
    public IScript<TResult>? TryGetCached(string scriptHash)
    {
        if (_cache.TryGetValue(scriptHash, out var entry))
        {
            entry.LastAccessed = DateTime.UtcNow;
            entry.AccessCount++;
            return entry.Script;
        }
        return null;
    }
    
    public void Cache(string scriptHash, IScript<TResult> script)
    {
        // Проверка лимита
        if (_cache.Count >= _maxCacheSize)
        {
            EvictLeastRecentlyUsed();
        }
        
        _cache[scriptHash] = new CacheEntry<TResult>
        {
            Script = script,
            CachedAt = DateTime.UtcNow,
            LastAccessed = DateTime.UtcNow,
            AccessCount = 0
        };
    }
    
    public void Remove(string scriptHash)
    {
        _cache.Remove(scriptHash);
    }
    
    public bool Contains(string scriptHash)
    {
        return _cache.ContainsKey(scriptHash);
    }
    
    public void Clear()
    {
        _cache.Clear();
    }
    
    private void EvictLeastRecentlyUsed()
    {
        var lruEntry = _cache
            .OrderBy(kvp => kvp.Value.LastAccessed)
            .First();
        
        _cache.Remove(lruEntry.Key);
    }
    
    private class CacheEntry<T>
    {
        public IScript<T> Script { get; set; } = null!;
        public DateTime CachedAt { get; set; }
        public DateTime LastAccessed { get; set; }
        public int AccessCount { get; set; }
    }
}

Стратегии вытеснения

LRU (Least Recently Used)

public class LRUScriptCache<TResult> : IScriptCache<TResult>
{
    private readonly LinkedList<string> _lruList = new();
    private readonly Dictionary<string, LinkedListNode<string>> _lruNodes = new();
    private readonly Dictionary<string, IScript<TResult>> _cache = new();
    private readonly int _maxSize;
    
    public int CachedCount => _cache.Count;
    
    public LRUScriptCache(int maxSize = 100)
    {
        _maxSize = maxSize;
    }
    
    public IScript<TResult>? TryGetCached(string scriptHash)
    {
        if (_cache.TryGetValue(scriptHash, out var script))
        {
            // Переместить в начало (самый свежий)
            MoveToFront(scriptHash);
            return script;
        }
        return null;
    }
    
    public void Cache(string scriptHash, IScript<TResult> script)
    {
        if (_cache.ContainsKey(scriptHash))
        {
            _cache[scriptHash] = script;
            MoveToFront(scriptHash);
            return;
        }
        
        if (_cache.Count >= _maxSize)
        {
            // Удалить самый старый
            var oldest = _lruList.Last!.Value;
            Remove(oldest);
        }
        
        _cache[scriptHash] = script;
        var node = _lruList.AddFirst(scriptHash);
        _lruNodes[scriptHash] = node;
    }
    
    public void Remove(string scriptHash)
    {
        if (_lruNodes.TryGetValue(scriptHash, out var node))
        {
            _lruList.Remove(node);
            _lruNodes.Remove(scriptHash);
            _cache.Remove(scriptHash);
        }
    }
    
    public bool Contains(string scriptHash) => _cache.ContainsKey(scriptHash);
    
    public void Clear()
    {
        _cache.Clear();
        _lruList.Clear();
        _lruNodes.Clear();
    }
    
    private void MoveToFront(string scriptHash)
    {
        if (_lruNodes.TryGetValue(scriptHash, out var node))
        {
            _lruList.Remove(node);
            _lruNodes[scriptHash] = _lruList.AddFirst(scriptHash);
        }
    }
}

LFU (Least Frequently Used)

public class LFUScriptCache<TResult> : IScriptCache<TResult>
{
    private readonly Dictionary<string, CacheItem> _cache = new();
    private readonly int _maxSize;
    
    public int CachedCount => _cache.Count;
    
    public LFUScriptCache(int maxSize = 100)
    {
        _maxSize = maxSize;
    }
    
    public IScript<TResult>? TryGetCached(string scriptHash)
    {
        if (_cache.TryGetValue(scriptHash, out var item))
        {
            item.Frequency++;
            return item.Script;
        }
        return null;
    }
    
    public void Cache(string scriptHash, IScript<TResult> script)
    {
        if (_cache.ContainsKey(scriptHash))
        {
            _cache[scriptHash].Script = script;
            return;
        }
        
        if (_cache.Count >= _maxSize)
        {
            // Удалить наименее частый
            var lfuKey = _cache
                .OrderBy(kvp => kvp.Value.Frequency)
                .First()
                .Key;
            Remove(lfuKey);
        }
        
        _cache[scriptHash] = new CacheItem
        {
            Script = script,
            Frequency = 0
        };
    }
    
    public void Remove(string scriptHash) => _cache.Remove(scriptHash);
    public bool Contains(string scriptHash) => _cache.ContainsKey(scriptHash);
    public void Clear() => _cache.Clear();
    
    private class CacheItem
    {
        public IScript<TResult> Script { get; set; } = null!;
        public int Frequency { get; set; }
    }
}

TTL (Time To Live)

public class TTLScriptCache<TResult> : IScriptCache<TResult>
{
    private readonly Dictionary<string, CacheItem> _cache = new();
    private readonly TimeSpan _ttl;
    private readonly Timer _cleanupTimer;
    
    public int CachedCount => _cache.Count;
    
    public TTLScriptCache(TimeSpan ttl, TimeSpan? cleanupInterval = null)
    {
        _ttl = ttl;
        var interval = cleanupInterval ?? TimeSpan.FromMinutes(1);
        _cleanupTimer = new Timer(_ => RemoveExpired(), null, interval, interval);
    }
    
    public IScript<TResult>? TryGetCached(string scriptHash)
    {
        if (_cache.TryGetValue(scriptHash, out var item))
        {
            if (DateTime.UtcNow - item.CachedAt < _ttl)
            {
                return item.Script;
            }
            else
            {
                Remove(scriptHash);
            }
        }
        return null;
    }
    
    public void Cache(string scriptHash, IScript<TResult> script)
    {
        _cache[scriptHash] = new CacheItem
        {
            Script = script,
            CachedAt = DateTime.UtcNow
        };
    }
    
    public void Remove(string scriptHash) => _cache.Remove(scriptHash);
    public bool Contains(string scriptHash) => _cache.ContainsKey(scriptHash);
    public void Clear() => _cache.Clear();
    
    private void RemoveExpired()
    {
        var now = DateTime.UtcNow;
        var expired = _cache
            .Where(kvp => now - kvp.Value.CachedAt >= _ttl)
            .Select(kvp => kvp.Key)
            .ToList();
        
        foreach (var key in expired)
        {
            Remove(key);
        }
    }
    
    private class CacheItem
    {
        public IScript<TResult> Script { get; set; } = null!;
        public DateTime CachedAt { get; set; }
    }
}

Интеграция с движком

public class CachedLuaEngine : LuaEngine
{
    private readonly LRUScriptCache<object[]> _cache;
    
    public override IScriptCache<object[]>? Cache => _cache;
    
    public CachedLuaEngine(int cacheSize = 100)
    {
        _cache = new LRUScriptCache<object[]>(cacheSize);
    }
    
    public override IScript<object[]> Compile(string script)
    {
        var hash = ComputeHash(script);
        
        // Попытка получить из кеша
        var cached = _cache.TryGetCached(hash);
        if (cached != null)
        {
            return cached;
        }
        
        // Компиляция и кеширование
        var compiled = base.Compile(script);
        _cache.Cache(hash, compiled);
        
        return compiled;
    }
    
    private static string ComputeHash(string script)
    {
        using var sha256 = SHA256.Create();
        var bytes = Encoding.UTF8.GetBytes(script);
        var hash = sha256.ComputeHash(bytes);
        return Convert.ToBase64String(hash);
    }
}

Статистика кеша

public class CacheStatistics
{
    public int Hits { get; set; }
    public int Misses { get; set; }
    public int Evictions { get; set; }
    
    public double HitRate => Hits + Misses > 0 
        ? Hits / (double)(Hits + Misses) 
        : 0;
}

public class StatisticalScriptCache<TResult> : IScriptCache<TResult>
{
    private readonly IScriptCache<TResult> _innerCache;
    private readonly CacheStatistics _stats = new();
    
    public int CachedCount => _innerCache.CachedCount;
    public CacheStatistics Statistics => _stats;
    
    public StatisticalScriptCache(IScriptCache<TResult> innerCache)
    {
        _innerCache = innerCache;
    }
    
    public IScript<TResult>? TryGetCached(string scriptHash)
    {
        var result = _innerCache.TryGetCached(scriptHash);
        
        if (result != null)
            _stats.Hits++;
        else
            _stats.Misses++;
        
        return result;
    }
    
    public void Cache(string scriptHash, IScript<TResult> script)
    {
        var wasCached = _innerCache.Contains(scriptHash);
        _innerCache.Cache(scriptHash, script);
        
        if (!wasCached && _innerCache.CachedCount == _innerCache.CachedCount)
        {
            _stats.Evictions++;
        }
    }
    
    public void Remove(string scriptHash) => _innerCache.Remove(scriptHash);
    public bool Contains(string scriptHash) => _innerCache.Contains(scriptHash);
    public void Clear() => _innerCache.Clear();
}

Предварительный прогрев кеша

public class CacheWarmer
{
    private readonly IScriptEngine<object[], LuaModuleInterface> _engine;
    private readonly IScriptCache<object[]> _cache;
    
    public CacheWarmer(
        IScriptEngine<object[], LuaModuleInterface> engine,
        IScriptCache<object[]> cache)
    {
        _engine = engine;
        _cache = cache;
    }
    
    public async Task WarmUpAsync(IEnumerable<string> scripts, CancellationToken cancellationToken = default)
    {
        var tasks = scripts.Select(script => WarmUpScriptAsync(script, cancellationToken));
        await Task.WhenAll(tasks);
    }
    
    private async Task WarmUpScriptAsync(string script, CancellationToken cancellationToken)
    {
        try
        {
            var hash = ComputeHash(script);
            
            if (!_cache.Contains(hash))
            {
                var compiled = await _engine.CompileAsync(script, cancellationToken);
                _cache.Cache(hash, compiled);
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Ошибка прогрева кеша: {ex.Message}");
        }
    }
    
    private static string ComputeHash(string script)
    {
        using var sha256 = SHA256.Create();
        var bytes = Encoding.UTF8.GetBytes(script);
        var hash = sha256.ComputeHash(bytes);
        return Convert.ToBase64String(hash);
    }
}

// Использование
var scripts = new[]
{
    "return 2 + 2",
    "return math.sqrt(144)",
    "return string.upper('hello')"
};

var warmer = new CacheWarmer(engine, cache);
await warmer.WarmUpAsync(scripts);

Мониторинг кеша

public class CacheMonitor
{
    private readonly IScriptCache<object[]> _cache;
    private readonly Timer _timer;
    
    public CacheMonitor(IScriptCache<object[]> cache, TimeSpan interval)
    {
        _cache = cache;
        _timer = new Timer(_ => PrintStats(), null, interval, interval);
    }
    
    private void PrintStats()
    {
        Console.WriteLine("=== Cache Statistics ===");
        Console.WriteLine($"Cached Scripts: {_cache.CachedCount}");
        
        if (_cache is StatisticalScriptCache<object[]> statsCache)
        {
            var stats = statsCache.Statistics;
            Console.WriteLine($"Hits: {stats.Hits}");
            Console.WriteLine($"Misses: {stats.Misses}");
            Console.WriteLine($"Hit Rate: {stats.HitRate:P2}");
            Console.WriteLine($"Evictions: {stats.Evictions}");
        }
    }
}

Best Practices

  1. Размер кеша: Настройте размер в зависимости от доступной памяти
  2. Хеширование: Используйте криптографические хеши для уникальности
  3. Стратегия: Выбирайте LRU для общего случая, LFU для известных паттернов
  4. TTL: Используйте для динамических скриптов
  5. Прогрев: Загружайте часто используемые скрипты при старте
  6. Мониторинг: Отслеживайте hit rate и настраивайте соответственно
  7. Очистка: Периодически очищайте кеш в долгоживущих приложениях