# 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+(?\d+)/.freeze PSS_PATTERN = /^Pss:\s+(?\d+)/.freeze RSS_TOTAL_PATTERN = /^VmRSS:\s+(?\d+)/.freeze RSS_ANON_PATTERN = /^RssAnon:\s+(?\d+)/.freeze RSS_FILE_PATTERN = /^RssFile:\s+(?\d+)/.freeze MAX_OPEN_FILES_PATTERN = /Max open files\s*(?\d+)/.freeze MEM_TOTAL_PATTERN = /^MemTotal:\s+(?\d+) (.+)/.freeze def summary proportional_mem = memory_usage_uss_pss { version: RUBY_DESCRIPTION, gc_stat: GC.stat, memory_rss: memory_usage_rss[:total], 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') results = { total: 0, anon: 0, file: 0 } safe_yield_procfile(PROC_STATUS_PATH % pid) do |io| io.each_line do |line| if (value = parse_metric_value(line, RSS_TOTAL_PATTERN)) > 0 results[:total] = value.kilobytes elsif (value = parse_metric_value(line, RSS_ANON_PATTERN)) > 0 results[:anon] = value.kilobytes elsif (value = parse_metric_value(line, RSS_FILE_PATTERN)) > 0 results[:file] = value.kilobytes end end end results 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| results[metric] += parse_metric_value(line, pattern) end end end results end def parse_metric_value(line, pattern) match = line.match(pattern) return 0 unless match match.named_captures.fetch('value', 0).to_i 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