Skip to content

Commit ebe3083

Browse files
NSHkrNSHkr
authored andcommitted
refactor memory manager
1 parent a3fb310 commit ebe3083

10 files changed

Lines changed: 2319 additions & 1267 deletions

File tree

lib/elixir_scope/ast_repository/memory_manager.ex

Lines changed: 106 additions & 720 deletions
Large diffs are not rendered by default.
Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
defmodule ElixirScope.ASTRepository.MemoryManager.CacheManager do
2+
@moduledoc """
3+
Multi-level caching system with LRU eviction for the AST Repository.
4+
5+
Manages query caches, analysis caches, and CPG caches with different
6+
TTLs and eviction policies.
7+
"""
8+
9+
use GenServer
10+
require Logger
11+
12+
# Cache configuration
13+
@query_cache_ttl 60_000 # 1 minute
14+
@analysis_cache_ttl 300_000 # 5 minutes
15+
@cpg_cache_ttl 600_000 # 10 minutes
16+
@max_cache_entries 1000
17+
18+
# ETS tables for caching
19+
@query_cache_table :ast_repo_query_cache
20+
@analysis_cache_table :ast_repo_analysis_cache
21+
@cpg_cache_table :ast_repo_cpg_cache
22+
23+
@type cache_stats :: %{
24+
query_cache_size: non_neg_integer(),
25+
analysis_cache_size: non_neg_integer(),
26+
cpg_cache_size: non_neg_integer(),
27+
total_cache_hits: non_neg_integer(),
28+
total_cache_misses: non_neg_integer(),
29+
cache_hit_ratio: float(),
30+
evictions: non_neg_integer()
31+
}
32+
33+
defstruct [
34+
:cache_hits,
35+
:cache_misses,
36+
:evictions
37+
]
38+
39+
def start_link(opts \\ []) do
40+
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
41+
end
42+
43+
def init(_opts) do
44+
init_cache_tables()
45+
46+
state = %__MODULE__{
47+
cache_hits: 0,
48+
cache_misses: 0,
49+
evictions: 0
50+
}
51+
52+
{:ok, state}
53+
end
54+
55+
@doc """
56+
Gets a value from the specified cache.
57+
"""
58+
@spec get(atom(), term()) :: {:ok, term()} | :miss
59+
def get(cache_type, key) do
60+
GenServer.call(__MODULE__, {:get, cache_type, key})
61+
end
62+
63+
@doc """
64+
Puts a value in the specified cache.
65+
"""
66+
@spec put(atom(), term(), term()) :: :ok
67+
def put(cache_type, key, value) do
68+
GenServer.call(__MODULE__, {:put, cache_type, key, value})
69+
end
70+
71+
@doc """
72+
Clears the specified cache.
73+
"""
74+
@spec clear(atom()) :: :ok
75+
def clear(cache_type) do
76+
GenServer.call(__MODULE__, {:clear, cache_type})
77+
end
78+
79+
@doc """
80+
Gets cache statistics.
81+
"""
82+
@spec get_stats() :: cache_stats()
83+
def get_stats() do
84+
GenServer.call(__MODULE__, :get_stats)
85+
end
86+
87+
@doc """
88+
Gets initial cache statistics structure.
89+
"""
90+
@spec get_initial_stats() :: cache_stats()
91+
def get_initial_stats() do
92+
%{
93+
query_cache_size: 0,
94+
analysis_cache_size: 0,
95+
cpg_cache_size: 0,
96+
total_cache_hits: 0,
97+
total_cache_misses: 0,
98+
cache_hit_ratio: 0.0,
99+
evictions: 0
100+
}
101+
end
102+
103+
@doc """
104+
Configures cache settings for a specific cache type.
105+
"""
106+
@spec configure_cache(atom(), keyword()) :: :ok | {:error, term()}
107+
def configure_cache(cache_type, opts) do
108+
GenServer.call(__MODULE__, {:configure_cache, cache_type, opts})
109+
end
110+
111+
# GenServer Callbacks
112+
113+
def handle_call({:get, cache_type, key}, _from, state) do
114+
case cache_get_internal(cache_type, key) do
115+
{:ok, value} ->
116+
new_state = %{state | cache_hits: state.cache_hits + 1}
117+
{:reply, {:ok, value}, new_state}
118+
:miss ->
119+
new_state = %{state | cache_misses: state.cache_misses + 1}
120+
{:reply, :miss, new_state}
121+
end
122+
end
123+
124+
def handle_call({:put, cache_type, key, value}, _from, state) do
125+
evictions = cache_put_internal(cache_type, key, value)
126+
new_state = %{state | evictions: state.evictions + evictions}
127+
{:reply, :ok, new_state}
128+
end
129+
130+
def handle_call({:clear, cache_type}, _from, state) do
131+
cache_clear_internal(cache_type)
132+
{:reply, :ok, state}
133+
end
134+
135+
def handle_call(:get_stats, _from, state) do
136+
stats = %{
137+
query_cache_size: get_cache_size(@query_cache_table),
138+
analysis_cache_size: get_cache_size(@analysis_cache_table),
139+
cpg_cache_size: get_cache_size(@cpg_cache_table),
140+
total_cache_hits: state.cache_hits,
141+
total_cache_misses: state.cache_misses,
142+
cache_hit_ratio: calculate_hit_ratio(state.cache_hits, state.cache_misses),
143+
evictions: state.evictions
144+
}
145+
{:reply, stats, state}
146+
end
147+
148+
def handle_call({:configure_cache, _cache_type, _opts}, _from, state) do
149+
# Cache configuration could be implemented here for runtime changes
150+
# For now, configuration is handled during initialization
151+
{:reply, :ok, state}
152+
end
153+
154+
# Private Implementation
155+
156+
defp init_cache_tables() do
157+
# Query cache: {key, value, timestamp, access_count}
158+
:ets.new(@query_cache_table, [:named_table, :public, :set, {:read_concurrency, true}])
159+
160+
# Analysis cache: {key, value, timestamp, access_count}
161+
:ets.new(@analysis_cache_table, [:named_table, :public, :set, {:read_concurrency, true}])
162+
163+
# CPG cache: {key, value, timestamp, access_count}
164+
:ets.new(@cpg_cache_table, [:named_table, :public, :set, {:read_concurrency, true}])
165+
end
166+
167+
defp cache_get_internal(cache_type, key) do
168+
try do
169+
table = cache_table_for_type(cache_type)
170+
case :ets.lookup(table, key) do
171+
[{^key, value, timestamp, _access_count}] ->
172+
ttl = cache_ttl_for_type(cache_type)
173+
if System.monotonic_time(:millisecond) - timestamp < ttl do
174+
# Update access count and timestamp
175+
:ets.update_counter(table, key, {4, 1})
176+
:ets.update_element(table, key, {3, System.monotonic_time(:millisecond)})
177+
{:ok, value}
178+
else
179+
# Expired entry
180+
:ets.delete(table, key)
181+
:miss
182+
end
183+
[] ->
184+
:miss
185+
end
186+
rescue
187+
_error ->
188+
:miss
189+
end
190+
end
191+
192+
defp cache_put_internal(cache_type, key, value) do
193+
try do
194+
table = cache_table_for_type(cache_type)
195+
timestamp = System.monotonic_time(:millisecond)
196+
197+
# Check cache size and evict if necessary
198+
cache_size = :ets.info(table, :size)
199+
evictions = if cache_size >= @max_cache_entries do
200+
evict_lru_entries(table, div(@max_cache_entries, 10)) # Evict 10%
201+
else
202+
0
203+
end
204+
205+
:ets.insert(table, {key, value, timestamp, 1})
206+
evictions
207+
rescue
208+
_error ->
209+
0 # Fail silently for cache operations
210+
end
211+
end
212+
213+
defp cache_clear_internal(cache_type) do
214+
try do
215+
table = cache_table_for_type(cache_type)
216+
:ets.delete_all_objects(table)
217+
:ok
218+
rescue
219+
_error ->
220+
:ok # Fail silently for cache operations
221+
end
222+
end
223+
224+
defp evict_lru_entries(table, count) do
225+
# Get entries sorted by access time (oldest first)
226+
entries = :ets.tab2list(table)
227+
|> Enum.sort_by(fn {_key, _value, timestamp, _access_count} -> timestamp end)
228+
|> Enum.take(count)
229+
230+
# Remove oldest entries
231+
Enum.each(entries, fn {key, _value, _timestamp, _access_count} ->
232+
:ets.delete(table, key)
233+
end)
234+
235+
length(entries)
236+
end
237+
238+
defp get_cache_size(table) do
239+
case :ets.info(table, :size) do
240+
:undefined -> 0
241+
size -> size
242+
end
243+
end
244+
245+
defp calculate_hit_ratio(hits, misses) do
246+
total = hits + misses
247+
if total > 0 do
248+
hits / total
249+
else
250+
0.0
251+
end
252+
end
253+
254+
defp cache_table_for_type(:query), do: @query_cache_table
255+
defp cache_table_for_type(:analysis), do: @analysis_cache_table
256+
defp cache_table_for_type(:cpg), do: @cpg_cache_table
257+
defp cache_table_for_type(_), do: @query_cache_table # Default fallback
258+
259+
defp cache_ttl_for_type(:query), do: @query_cache_ttl
260+
defp cache_ttl_for_type(:analysis), do: @analysis_cache_ttl
261+
defp cache_ttl_for_type(:cpg), do: @cpg_cache_ttl
262+
defp cache_ttl_for_type(_), do: @query_cache_ttl # Default fallback
263+
end

0 commit comments

Comments
 (0)