• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

nshkrdotcom / ElixirScope / 263b75bcaeda1a9017c5ab33429630d229751173

29 May 2025 05:17PM UTC coverage: 58.515% (+0.3%) from 58.208%
263b75bcaeda1a9017c5ab33429630d229751173

push

github

NSHkr
perf test and mem mgr 1087 tests, 3 failures, 76 excluded

13 of 26 new or added lines in 4 files covered. (50.0%)

5 existing lines in 3 files now uncovered.

6082 of 10394 relevant lines covered (58.51%)

3193.89 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

57.65
/lib/elixir_scope/ast_repository/memory_manager.ex
1
defmodule ElixirScope.ASTRepository.MemoryManager do
2
  @moduledoc """
3
  Main memory management coordinator for the Enhanced AST Repository.
4

5
  Orchestrates memory monitoring, cleanup, compression, and caching
6
  strategies to handle production-scale projects with 1000+ modules.
7

8
  ## Features
9

10
  - **Memory Monitoring**: Real-time tracking of repository memory usage
11
  - **Intelligent Cleanup**: Remove stale and unused AST data
12
  - **Data Compression**: Compress infrequently accessed analysis data
13
  - **LRU Caching**: Least Recently Used cache for query optimization
14
  - **Memory Pressure Handling**: Multi-level response to memory constraints
15

16
  ## Performance Targets
17

18
  - Memory usage: <500MB for 1000 modules
19
  - Query response: <100ms for 95th percentile
20
  - Cache hit ratio: >80% for repeated queries
21
  - Memory cleanup: <10ms per cleanup cycle
22

23
  ## Examples
24

25
      # Start memory monitoring
26
      {:ok, _pid} = MemoryManager.start_link()
27

28
      # Monitor memory usage
29
      {:ok, stats} = MemoryManager.monitor_memory_usage()
30

31
      # Cleanup unused data
32
      :ok = MemoryManager.cleanup_unused_data(max_age: 3600)
33
  """
34

35
  use GenServer
36
  require Logger
37

38
  alias ElixirScope.ASTRepository.MemoryManager.{
39
    Monitor,
40
    Cleaner,
41
    Compressor,
42
    CacheManager,
43
    PressureHandler
44
  }
45

46
  # Configuration
47
  @memory_check_interval 30_000      # 30 seconds
48
  @cleanup_interval 300_000          # 5 minutes
49
  @compression_interval 600_000      # 10 minutes
50

51
  defstruct [
52
    :memory_stats,
53
    :cache_stats,
54
    :cleanup_stats,
55
    :compression_stats,
56
    :pressure_level,
57
    :last_cleanup,
58
    :last_compression,
59
    :monitoring_enabled
60
  ]
61

62
  @type memory_stats :: Monitor.memory_stats()
63
  @type cache_stats :: CacheManager.cache_stats()
64
  @type cleanup_stats :: Cleaner.cleanup_stats()
65
  @type compression_stats :: Compressor.compression_stats()
66

67
  # GenServer API
68

69
  def start_link(opts \\ []) do
70
    GenServer.start_link(__MODULE__, opts, name: __MODULE__)
2✔
71
  end
72

73
  def init(opts) do
74
    # Schedule periodic tasks
75
    schedule_memory_check()
2✔
76
    schedule_cleanup()
2✔
77
    schedule_compression()
2✔
78

79
    state = %__MODULE__{
2✔
80
      memory_stats: %{},
81
      cache_stats: CacheManager.get_initial_stats(),
82
      cleanup_stats: Cleaner.get_initial_stats(),
83
      compression_stats: Compressor.get_initial_stats(),
84
      pressure_level: :normal,
85
      last_cleanup: System.monotonic_time(:millisecond),
86
      last_compression: System.monotonic_time(:millisecond),
87
      monitoring_enabled: Keyword.get(opts, :monitoring_enabled, true)
88
    }
89

90
    Logger.info("MemoryManager started with monitoring enabled: #{state.monitoring_enabled}")
2✔
91
    {:ok, state}
92
  end
93

94
  # Public API
95

96
  @doc """
97
  Monitors current memory usage of the AST Repository.
98
  """
99
  @spec monitor_memory_usage() :: {:ok, memory_stats()} | {:error, term()}
100
  def monitor_memory_usage() do
101
    GenServer.call(__MODULE__, :monitor_memory_usage)
17✔
102
  end
103

104
  @doc """
105
  Cleans up unused AST data based on access patterns and age.
106
  """
107
  @spec cleanup_unused_data(keyword()) :: :ok | {:error, term()}
108
  def cleanup_unused_data(opts \\ []) do
109
    GenServer.call(__MODULE__, {:cleanup_unused_data, opts}, 30_000)
6✔
110
  end
111

112
  @doc """
113
  Compresses infrequently accessed analysis data.
114
  """
115
  @spec compress_old_analysis(keyword()) :: {:ok, compression_stats()} | {:error, term()}
116
  def compress_old_analysis(opts \\ []) do
117
    GenServer.call(__MODULE__, {:compress_old_analysis, opts}, 30_000)
3✔
118
  end
119

120
  @doc """
121
  Implements LRU (Least Recently Used) cache for query optimization.
122
  """
123
  @spec implement_lru_cache(atom(), keyword()) :: :ok | {:error, term()}
124
  def implement_lru_cache(cache_type, opts \\ []) do
125
    GenServer.call(__MODULE__, {:implement_lru_cache, cache_type, opts})
×
126
  end
127

128
  @doc """
129
  Handles memory pressure situations with appropriate response levels.
130
  """
131
  @spec memory_pressure_handler(atom()) :: :ok | {:error, term()}
132
  def memory_pressure_handler(pressure_level) do
133
    GenServer.call(__MODULE__, {:memory_pressure_handler, pressure_level}, 60_000)
8✔
134
  end
135

136
  @doc """
137
  Gets comprehensive memory and performance statistics.
138
  """
139
  @spec get_stats() :: {:ok, map()}
140
  def get_stats() do
141
    GenServer.call(__MODULE__, :get_stats)
5✔
142
  end
143

144
  @doc """
145
  Enables or disables memory monitoring.
146
  """
147
  @spec set_monitoring(boolean()) :: :ok
148
  def set_monitoring(enabled) do
149
    GenServer.call(__MODULE__, {:set_monitoring, enabled})
×
150
  end
151

152
  @doc """
153
  Forces garbage collection and memory optimization.
154
  """
155
  @spec force_gc() :: :ok
156
  def force_gc() do
157
    GenServer.call(__MODULE__, :force_gc)
×
158
  end
159

160
  # Cache API (delegated to CacheManager)
161

162
  @doc """
163
  Gets a value from the specified cache.
164
  """
165
  @spec cache_get(atom(), term()) :: {:ok, term()} | :miss
166
  def cache_get(cache_type, key) do
167
    CacheManager.get(cache_type, key)
56✔
168
  end
169

170
  @doc """
171
  Puts a value in the specified cache.
172
  """
173
  @spec cache_put(atom(), term(), term()) :: :ok
174
  def cache_put(cache_type, key, value) do
175
    CacheManager.put(cache_type, key, value)
58✔
176
  end
177

178
  @doc """
179
  Clears the specified cache.
180
  """
181
  @spec cache_clear(atom()) :: :ok
182
  def cache_clear(cache_type) do
183
    CacheManager.clear(cache_type)
1✔
184
  end
185

186
  # GenServer Callbacks
187

188
  def handle_call(:monitor_memory_usage, _from, state) do
189
    case safe_call(Monitor, :collect_memory_stats) do
17✔
190
      {:ok, memory_stats} ->
191
        new_state = %{state | memory_stats: memory_stats}
17✔
192
        {:reply, {:ok, memory_stats}, new_state}
17✔
193
      {:error, reason} ->
194
        # Fallback to basic memory info if Monitor is not available
NEW
195
        basic_stats = %{
×
196
          total_memory: :erlang.memory(:total),
197
          repository_memory: 0,
198
          cache_memory: 0,
199
          ets_memory: :erlang.memory(:ets),
200
          process_memory: :erlang.memory(:processes),
201
          memory_usage_percent: 50.0,  # Default safe value
202
          available_memory: :erlang.memory(:total) * 2  # Estimate
203
        }
NEW
204
        new_state = %{state | memory_stats: basic_stats}
×
NEW
205
        {:reply, {:ok, basic_stats}, new_state}
×
206
    end
207
  end
208

209
  def handle_call({:cleanup_unused_data, opts}, _from, state) do
210
    start_time = System.monotonic_time(:millisecond)
6✔
211

212
    case Cleaner.perform_cleanup(opts) do
6✔
213
      {:ok, cleanup_result} ->
214
        end_time = System.monotonic_time(:millisecond)
6✔
215
        duration = end_time - start_time
6✔
216

217
        new_cleanup_stats = Cleaner.update_stats(state.cleanup_stats, cleanup_result, duration)
6✔
218
        new_state = %{state |
6✔
219
          cleanup_stats: new_cleanup_stats,
220
          last_cleanup: end_time
221
        }
222

223
        # Return the cleanup result, not just :ok
224
        {:reply, {:ok, cleanup_result}, new_state}
6✔
225

226
      error ->
227
        {:reply, error, state}
×
228
    end
229
  end
230

231
  def handle_call({:compress_old_analysis, opts}, _from, state) do
232
    start_time = System.monotonic_time(:millisecond)
3✔
233

234
    case Compressor.perform_compression(opts) do
3✔
235
      {:ok, compression_result} ->
236
        end_time = System.monotonic_time(:millisecond)
3✔
237
        duration = end_time - start_time
3✔
238

239
        new_compression_stats = Compressor.update_stats(state.compression_stats, compression_result, duration)
3✔
240
        new_state = %{state |
3✔
241
          compression_stats: new_compression_stats,
242
          last_compression: end_time
243
        }
244

245
        {:reply, {:ok, compression_result}, new_state}
3✔
246

247
      error ->
248
        {:reply, error, state}
×
249
    end
250
  end
251

252
  def handle_call({:implement_lru_cache, cache_type, opts}, _from, state) do
253
    case CacheManager.configure_cache(cache_type, opts) do
×
254
      :ok ->
255
        {:reply, :ok, state}
×
256
      error ->
257
        {:reply, error, state}
×
258
    end
259
  end
260

261
  def handle_call({:memory_pressure_handler, pressure_level}, _from, state) do
262
    case PressureHandler.handle_pressure(pressure_level) do
8✔
263
      :ok ->
264
        new_state = %{state | pressure_level: pressure_level}
8✔
265
        {:reply, :ok, new_state}
8✔
266
      error ->
267
        {:reply, error, state}
×
268
    end
269
  end
270

271
  def handle_call(:get_stats, _from, state) do
272
    cache_stats = CacheManager.get_stats()
5✔
273

274
    stats = %{
5✔
275
      memory: state.memory_stats,
5✔
276
      cache: cache_stats,
277
      cleanup: state.cleanup_stats,
5✔
278
      compression: state.compression_stats,
5✔
279
      pressure_level: state.pressure_level,
5✔
280
      monitoring_enabled: state.monitoring_enabled
5✔
281
    }
282
    {:reply, {:ok, stats}, state}
5✔
283
  end
284

285
  def handle_call({:set_monitoring, enabled}, _from, state) do
286
    new_state = %{state | monitoring_enabled: enabled}
×
287
    {:reply, :ok, new_state}
×
288
  end
289

290
  def handle_call(:force_gc, _from, state) do
291
    # Force garbage collection
292
    :erlang.garbage_collect()
×
293

294
    # Force GC on all processes
295
    for pid <- Process.list() do
×
296
      if Process.alive?(pid) do
×
297
        :erlang.garbage_collect(pid)
×
298
      end
299
    end
300

301
    {:reply, :ok, state}
×
302
  end
303

304
  def handle_info(:memory_check, state) do
305
    if state.monitoring_enabled do
×
306
      case Monitor.collect_memory_stats() do
×
307
        {:ok, memory_stats} ->
308
          # Check for memory pressure
309
          pressure_level = PressureHandler.determine_pressure_level(memory_stats.memory_usage_percent)
×
310

311
          if pressure_level != :normal and pressure_level != state.pressure_level do
×
312
            Logger.warning("Memory pressure detected: #{pressure_level} (#{memory_stats.memory_usage_percent}%)")
×
313
            PressureHandler.handle_pressure(pressure_level)
×
314
          end
315

316
          new_state = %{state |
×
317
            memory_stats: memory_stats,
318
            pressure_level: pressure_level
319
          }
320

321
          schedule_memory_check()
×
322
          {:noreply, new_state}
323

324
        {:error, reason} ->
325
          Logger.error("Memory monitoring failed: #{inspect(reason)}")
×
326
          schedule_memory_check()
×
327
          {:noreply, state}
328
      end
329
    else
330
      schedule_memory_check()
×
331
      {:noreply, state}
332
    end
333
  end
334

335
  def handle_info(:cleanup, state) do
336
    # Perform automatic cleanup
337
    Cleaner.perform_cleanup([max_age: 3600])
×
338

339
    schedule_cleanup()
×
340
    {:noreply, state}
341
  end
342

343
  def handle_info(:compression, state) do
344
    # Perform automatic compression
345
    Compressor.perform_compression([access_threshold: 5, age_threshold: 1800])
×
346

347
    schedule_compression()
×
348
    {:noreply, state}
349
  end
350

351
  # Private Implementation
352
  # Safe wrapper for GenServer calls that might fail
353
  defp safe_call(process, message, timeout \\ 5000) do
354
    case GenServer.whereis(process) do
17✔
NEW
355
      nil ->
×
356
        {:error, :process_not_found}
357
      pid when is_pid(pid) ->
358
        if Process.alive?(pid) do
17✔
359
          try do
17✔
360
            GenServer.call(pid, message, timeout)
17✔
361
          catch
NEW
362
            :exit, reason -> {:error, {:exit, reason}}
×
363
          end
364
        else
365
          {:error, :process_not_alive}
366
        end
367
    end
368
  end
369

370
  defp schedule_memory_check() do
371
    Process.send_after(self(), :memory_check, @memory_check_interval)
2✔
372
  end
373

374
  defp schedule_cleanup() do
375
    Process.send_after(self(), :cleanup, @cleanup_interval)
2✔
376
  end
377

378
  defp schedule_compression() do
379
    Process.send_after(self(), :compression, @compression_interval)
2✔
380
  end
381
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc