summaryrefslogtreecommitdiff
path: root/lib/gitlab/metrics/system.rb
blob: affadc4274c3060bdd294ed5e0a94254dd75cce1 (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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# frozen_string_literal: true

module Gitlab
  module Metrics
    # Module for gathering system/process statistics such as the memory usage.
    #
    # This module relies on the /proc filesystem being available. If /proc is
    # not available the methods of this module will be stubbed.
    module System
      extend self

      PROC_STAT_PATH = '/proc/self/stat'
      PROC_STATUS_PATH = '/proc/%s/status'
      PROC_SMAPS_ROLLUP_PATH = '/proc/%s/smaps_rollup'
      PROC_LIMITS_PATH = '/proc/self/limits'
      PROC_FD_GLOB = '/proc/self/fd/*'
      PROC_MEM_INFO = '/proc/meminfo'

      PRIVATE_PAGES_PATTERN = /^(Private_Clean|Private_Dirty|Private_Hugetlb):\s+(?<value>\d+)/.freeze
      PSS_PATTERN = /^Pss:\s+(?<value>\d+)/.freeze
      RSS_PATTERN = /VmRSS:\s+(?<value>\d+)/.freeze
      MAX_OPEN_FILES_PATTERN = /Max open files\s*(?<value>\d+)/.freeze
      MEM_TOTAL_PATTERN = /^MemTotal:\s+(?<value>\d+) (.+)/.freeze

      def summary
        proportional_mem = memory_usage_uss_pss
        {
          version: RUBY_DESCRIPTION,
          gc_stat: GC.stat,
          memory_rss: memory_usage_rss,
          memory_uss: proportional_mem[:uss],
          memory_pss: proportional_mem[:pss],
          time_cputime: cpu_time,
          time_realtime: real_time,
          time_monotonic: monotonic_time
        }
      end

      # Returns the given process' RSS (resident set size) in bytes.
      def memory_usage_rss(pid: 'self')
        sum_matches(PROC_STATUS_PATH % pid, rss: RSS_PATTERN)[:rss].kilobytes
      end

      # Returns the given process' USS/PSS (unique/proportional set size) in bytes.
      def memory_usage_uss_pss(pid: 'self')
        sum_matches(PROC_SMAPS_ROLLUP_PATH % pid, uss: PRIVATE_PAGES_PATTERN, pss: PSS_PATTERN)
          .transform_values(&:kilobytes)
      end

      def memory_total
        sum_matches(PROC_MEM_INFO, memory_total: MEM_TOTAL_PATTERN)[:memory_total].kilobytes
      end

      def file_descriptor_count
        Dir.glob(PROC_FD_GLOB).length
      end

      def max_open_file_descriptors
        sum_matches(PROC_LIMITS_PATH, max_fds: MAX_OPEN_FILES_PATTERN)[:max_fds]
      end

      def cpu_time
        Process.clock_gettime(Process::CLOCK_PROCESS_CPUTIME_ID, :float_second)
      end

      # Returns the current real time in a given precision.
      #
      # Returns the time as a Float for precision = :float_second.
      def real_time(precision = :float_second)
        Process.clock_gettime(Process::CLOCK_REALTIME, precision)
      end

      # Returns the current monotonic clock time as seconds with microseconds precision.
      #
      # Returns the time as a Float.
      def monotonic_time
        Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_second)
      end

      def thread_cpu_time
        # Not all OS kernels are supporting `Process::CLOCK_THREAD_CPUTIME_ID`
        # Refer: https://gitlab.com/gitlab-org/gitlab/issues/30567#note_221765627
        return unless defined?(Process::CLOCK_THREAD_CPUTIME_ID)

        Process.clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID, :float_second)
      end

      def thread_cpu_duration(start_time)
        end_time = thread_cpu_time
        return unless start_time && end_time

        end_time - start_time
      end

      # Returns the total time the current process has been running in seconds.
      def process_runtime_elapsed_seconds
        # Entry 22 (1-indexed) contains the process `starttime`, see:
        # https://man7.org/linux/man-pages/man5/proc.5.html
        #
        # This value is a fixed timestamp in clock ticks.
        # To obtain an elapsed time in seconds, we divide by the number
        # of ticks per second and subtract from the system uptime.
        start_time_ticks = proc_stat_entries[21].to_f
        clock_ticks_per_second = Etc.sysconf(Etc::SC_CLK_TCK)
        uptime - (start_time_ticks / clock_ticks_per_second)
      end

      private

      # Given a path to a file in /proc and a hash of (metric, pattern) pairs,
      # sums up all values found for those patterns under the respective metric.
      def sum_matches(proc_file, **patterns)
        results = patterns.transform_values { 0 }

        safe_yield_procfile(proc_file) do |io|
          io.each_line do |line|
            patterns.each do |metric, pattern|
              match = line.match(pattern)
              value = match&.named_captures&.fetch('value', 0)
              results[metric] += value.to_i
            end
          end
        end

        results
      end

      def proc_stat_entries
        safe_yield_procfile(PROC_STAT_PATH) do |io|
          io.read.split(' ')
        end || []
      end

      def safe_yield_procfile(path, &block)
        File.open(path, &block)
      rescue Errno::ENOENT
        # This means the procfile we're reading from did not exist;
        # most likely we're on Darwin.
      end

      # Equivalent to reading /proc/uptime on Linux 2.6+.
      #
      # Returns 0 if not supported, e.g. on Darwin.
      def uptime
        Process.clock_gettime(Process::CLOCK_BOOTTIME)
      rescue NameError
        0
      end
    end
  end
end