-- Caching system for performance optimization local M = {} -- Cache storage local cache_storage = { memory = {}, lru = {}, timed = {} } -- Cache configuration local cache_config = { memory = { max_size = 1000, enabled = true }, lru = { max_size = 500, enabled = true }, timed = { default_ttl = 300, -- 5 minutes cleanup_interval = 60, -- 1 minute enabled = true } } -- Performance metrics local cache_metrics = { hits = 0, misses = 0, sets = 0, evictions = 0, cleanups = 0 } -- Simple memory cache local MemoryCache = {} MemoryCache.__index = MemoryCache function MemoryCache.new(max_size) local self = setmetatable({}, MemoryCache) self.data = {} self.max_size = max_size or 1000 self.current_size = 0 return self end function MemoryCache:set(key, value) if self.data[key] == nil then self.current_size = self.current_size + 1 end self.data[key] = value -- Evict if over size limit if self.current_size > self.max_size then self:evict() end cache_metrics.sets = cache_metrics.sets + 1 end function MemoryCache:get(key) local value = self.data[key] if value ~= nil then cache_metrics.hits = cache_metrics.hits + 1 return value else cache_metrics.misses = cache_metrics.misses + 1 return nil end end function MemoryCache:evict() -- Simple eviction: remove first item local first_key = next(self.data) if first_key then self.data[first_key] = nil self.current_size = self.current_size - 1 cache_metrics.evictions = cache_metrics.evictions + 1 end end function MemoryCache:clear() self.data = {} self.current_size = 0 end function MemoryCache:size() return self.current_size end -- LRU (Least Recently Used) cache local LRUCache = {} LRUCache.__index = LRUCache function LRUCache.new(max_size) local self = setmetatable({}, LRUCache) self.data = {} self.access_order = {} self.max_size = max_size or 500 return self end function LRUCache:set(key, value) if self.data[key] then -- Update existing item self.data[key] = value self:update_access(key) else -- Add new item self.data[key] = value table.insert(self.access_order, key) -- Evict if over size limit if #self.access_order > self.max_size then self:evict() end end cache_metrics.sets = cache_metrics.sets + 1 end function LRUCache:get(key) local value = self.data[key] if value ~= nil then self:update_access(key) cache_metrics.hits = cache_metrics.hits + 1 return value else cache_metrics.misses = cache_metrics.misses + 1 return nil end end function LRUCache:update_access(key) -- Remove key from current position for i, k in ipairs(self.access_order) do if k == key then table.remove(self.access_order, i) break end end -- Add to end (most recently used) table.insert(self.access_order, key) end function LRUCache:evict() if #self.access_order > 0 then local lru_key = table.remove(self.access_order, 1) self.data[lru_key] = nil cache_metrics.evictions = cache_metrics.evictions + 1 end end function LRUCache:clear() self.data = {} self.access_order = {} end function LRUCache:size() return #self.access_order end -- Timed cache with TTL local TimedCache = {} TimedCache.__index = TimedCache function TimedCache.new(default_ttl) local self = setmetatable({}, TimedCache) self.data = {} self.default_ttl = default_ttl or 300 self.cleanup_timer = nil self:start_cleanup_timer() return self end function TimedCache:set(key, value, ttl) ttl = ttl or self.default_ttl local expire_time = os.time() + ttl self.data[key] = { value = value, expire_time = expire_time } cache_metrics.sets = cache_metrics.sets + 1 end function TimedCache:get(key) local item = self.data[key] if item then if os.time() < item.expire_time then cache_metrics.hits = cache_metrics.hits + 1 return item.value else -- Expired, remove it self.data[key] = nil end end cache_metrics.misses = cache_metrics.misses + 1 return nil end function TimedCache:cleanup() local current_time = os.time() local cleaned = 0 for key, item in pairs(self.data) do if current_time >= item.expire_time then self.data[key] = nil cleaned = cleaned + 1 end end cache_metrics.cleanups = cache_metrics.cleanups + 1 return cleaned end function TimedCache:start_cleanup_timer() if self.cleanup_timer then return end self.cleanup_timer = vim.loop.new_timer() if self.cleanup_timer then self.cleanup_timer:start( cache_config.timed.cleanup_interval * 1000, cache_config.timed.cleanup_interval * 1000, vim.schedule_wrap(function() self:cleanup() end) ) end end function TimedCache:stop_cleanup_timer() if self.cleanup_timer then self.cleanup_timer:close() self.cleanup_timer = nil end end function TimedCache:clear() self.data = {} end function TimedCache:size() local count = 0 for _ in pairs(self.data) do count = count + 1 end return count end -- Initialize caches function M.init(config) config = config or {} cache_config = vim.tbl_deep_extend("force", cache_config, config) -- Initialize cache instances if cache_config.memory.enabled then cache_storage.memory = MemoryCache.new(cache_config.memory.max_size) end if cache_config.lru.enabled then cache_storage.lru = LRUCache.new(cache_config.lru.max_size) end if cache_config.timed.enabled then cache_storage.timed = TimedCache.new(cache_config.timed.default_ttl) end M.info("Cache system initialized", cache_config) end -- Memory cache operations function M.memory_set(key, value) if not cache_storage.memory then return false, "Memory cache disabled" end cache_storage.memory:set(key, value) return true end function M.memory_get(key) if not cache_storage.memory then return nil end return cache_storage.memory:get(key) end -- LRU cache operations function M.lru_set(key, value) if not cache_storage.lru then return false, "LRU cache disabled" end cache_storage.lru:set(key, value) return true end function M.lru_get(key) if not cache_storage.lru then return nil end return cache_storage.lru:get(key) end -- Timed cache operations function M.timed_set(key, value, ttl) if not cache_storage.timed then return false, "Timed cache disabled" end cache_storage.timed:set(key, value, ttl) return true end function M.timed_get(key) if not cache_storage.timed then return nil end return cache_storage.timed:get(key) end -- Generic cache operations with automatic cache selection function M.set(key, value, cache_type, ttl) cache_type = cache_type or "memory" if cache_type == "memory" then return M.memory_set(key, value) elseif cache_type == "lru" then return M.lru_set(key, value) elseif cache_type == "timed" then return M.timed_set(key, value, ttl) else return false, "Unknown cache type: " .. cache_type end end function M.get(key, cache_type) cache_type = cache_type or "memory" if cache_type == "memory" then return M.memory_get(key) elseif cache_type == "lru" then return M.lru_get(key) elseif cache_type == "timed" then return M.timed_get(key) else return nil, "Unknown cache type: " .. cache_type end end -- Get or set pattern (compute if not cached) function M.get_or_set(key, compute_func, cache_type, ttl) local value = M.get(key, cache_type) if value ~= nil then return value end -- Compute value local success, result = pcall(compute_func) if success then M.set(key, result, cache_type, ttl) return result else error("Failed to compute cached value: " .. result) end end -- Cache with multiple backends (try each in order) function M.multi_get(key, cache_types) cache_types = cache_types or {"memory", "lru", "timed"} for _, cache_type in ipairs(cache_types) do local value = M.get(key, cache_type) if value ~= nil then return value, cache_type end end return nil end -- Invalidate cache entries function M.invalidate(key, cache_type) if cache_type then -- Invalidate specific cache type if cache_type == "memory" and cache_storage.memory then cache_storage.memory.data[key] = nil elseif cache_type == "lru" and cache_storage.lru then cache_storage.lru.data[key] = nil for i, k in ipairs(cache_storage.lru.access_order) do if k == key then table.remove(cache_storage.lru.access_order, i) break end end elseif cache_type == "timed" and cache_storage.timed then cache_storage.timed.data[key] = nil end else -- Invalidate from all caches M.invalidate(key, "memory") M.invalidate(key, "lru") M.invalidate(key, "timed") end end -- Clear all caches function M.clear_all() if cache_storage.memory then cache_storage.memory:clear() end if cache_storage.lru then cache_storage.lru:clear() end if cache_storage.timed then cache_storage.timed:clear() end -- Reset metrics cache_metrics.hits = 0 cache_metrics.misses = 0 cache_metrics.sets = 0 cache_metrics.evictions = 0 cache_metrics.cleanups = 0 M.info("All caches cleared") end -- Get cache statistics function M.get_stats() local stats = { metrics = vim.deepcopy(cache_metrics), sizes = {}, config = vim.deepcopy(cache_config) } -- Calculate hit ratio local total_requests = cache_metrics.hits + cache_metrics.misses stats.metrics.hit_ratio = total_requests > 0 and (cache_metrics.hits / total_requests) or 0 -- Get cache sizes if cache_storage.memory then stats.sizes.memory = cache_storage.memory:size() end if cache_storage.lru then stats.sizes.lru = cache_storage.lru:size() end if cache_storage.timed then stats.sizes.timed = cache_storage.timed:size() end return stats end -- Cache warming functions function M.warm_query_cache(queries) if not cache_storage.lru then return false, "LRU cache not available" end local query_engine = require('notex.query') local warmed = 0 for _, query in ipairs(queries) do local key = "query:" .. query local cached = M.lru_get(key) if not cached then -- Execute query and cache result local result = query_engine.execute_query(query) if result.success then M.lru_set(key, result) warmed = warmed + 1 end end end M.info("Warmed query cache", {queries_warmed = warmed}) return true end function M.warm_document_cache(document_paths) if not cache_storage.memory then return false, "Memory cache not available" end local indexer = require('notex.index') local warmed = 0 for _, path in ipairs(document_paths) do local key = "document:" .. path local cached = M.memory_get(key) if not cached then -- Get document details and cache local details, err = indexer.get_document_details_by_path(path) if details then M.memory_set(key, details) warmed = warmed + 1 end end end M.info("Warmed document cache", {documents_warmed = warmed}) return true end -- Cleanup function function M.cleanup() if cache_storage.timed then cache_storage.timed:stop_cleanup_timer() end M.clear_all() M.info("Cache system cleaned up") end -- Export cache metrics for monitoring M.metrics = cache_metrics M.config = cache_config -- Forward logging functions (circular dependency resolution) function M.info(message, context) local ok, logging = pcall(require, 'notex.utils.logging') if ok then logging.info(message, context) else vim.notify("Cache: " .. message, vim.log.levels.INFO) end end return M