Кеширование скриптов
Система кеширования 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
- Размер кеша: Настройте размер в зависимости от доступной памяти
- Хеширование: Используйте криптографические хеши для уникальности
- Стратегия: Выбирайте LRU для общего случая, LFU для известных паттернов
- TTL: Используйте для динамических скриптов
- Прогрев: Загружайте часто используемые скрипты при старте
- Мониторинг: Отслеживайте hit rate и настраивайте соответственно
- Очистка: Периодически очищайте кеш в долгоживущих приложениях