summaryrefslogtreecommitdiff
path: root/lib/gitlab/memory/instrumentation.rb
blob: 8f9f6d19ce8aebb6be2a49a846a8d392ecc583b2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# frozen_string_literal: true

# This class uses a custom Ruby patch to allow
# a per-thread memory allocation tracking in a efficient manner
#
# This concept is currently tried to be upstreamed here:
# - https://github.com/ruby/ruby/pull/3978
module Gitlab
  module Memory
    class Instrumentation
      KEY_MAPPING = {
        total_allocated_objects: :mem_objects,
        total_malloc_bytes: :mem_bytes,
        total_mallocs: :mem_mallocs
      }.freeze

      MUTEX = Mutex.new

      def self.available?
        Thread.respond_to?(:trace_memory_allocations=) &&
          Thread.current.respond_to?(:memory_allocations)
      end

      # This method changes a global state
      def self.ensure_feature_flag!
        return unless available?

        enabled = Feature.enabled?(:trace_memory_allocations, default_enabled: true)
        return if enabled == Thread.trace_memory_allocations

        MUTEX.synchronize do
          # This enables or disables feature dynamically
          # based on a feature flag
          Thread.trace_memory_allocations = enabled
        end
      end

      def self.start_thread_memory_allocations
        return unless available?

        ensure_feature_flag!

        # it will return `nil` if disabled
        Thread.current.memory_allocations
      end

      # This method returns a hash with the following keys:
      # - mem_objects: a number of allocated heap slots (as reflected by GC)
      # - mem_mallocs: a number of malloc calls
      # - mem_bytes: a number of bytes allocated with a mallocs tied to heap slots
      def self.measure_thread_memory_allocations(previous)
        return unless available?
        return unless previous

        current = Thread.current.memory_allocations
        return unless current

        # calculate difference in a memory allocations
        previous.to_h do |key, value|
          [KEY_MAPPING.fetch(key), current[key].to_i - value]
        end
      end

      def self.with_memory_allocations
        previous = self.start_thread_memory_allocations
        yield
        self.measure_thread_memory_allocations(previous)
      end
    end
  end
end