server_metrics 0.0.6 → 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/example_processes.rb CHANGED
@@ -1,9 +1,11 @@
1
1
  require "rubygems"
2
- require File.dirname(__FILE__)+ "/lib/server_metrics"
2
+
3
+ $LOAD_PATH.unshift(File.expand_path(__FILE__), "lib") # set the loadpath for convenience during development
4
+ require "server_metrics"
5
+
3
6
  require "pry"
4
7
  require "awesome_print"
5
8
 
6
-
7
9
  p = ServerMetrics::Processes.new(1)
8
10
  puts "stating ..."
9
11
  p.run
@@ -1,5 +1,3 @@
1
- $LOAD_PATH << File.join(File.dirname(__FILE__))
2
-
3
1
  require 'server_metrics/version'
4
2
  require 'server_metrics/collector'
5
3
  require 'server_metrics/multi_collector'
@@ -1,13 +1,14 @@
1
1
  require 'sys/proctable'
2
+ require 'server_metrics/system_info'
2
3
 
3
4
  # Collects information on processes. Groups processes running under the same command, and sums up their CPU & memory usage.
4
- # CPU is calculated **since the last run**
5
- #
6
-
5
+ # CPU is calculated **since the last run**, and is a pecentage of overall CPU usage during the timeframe
6
+ # http://www.linuxquestions.org/questions/linux-general-1/per-process-cpu-utilization-557577/
7
7
  class ServerMetrics::Processes
8
8
 
9
9
  def initialize(options={})
10
10
  @last_run
11
+ @last_jiffies
11
12
  @last_process_list
12
13
  end
13
14
 
@@ -51,42 +52,58 @@ class ServerMetrics::Processes
51
52
  # and calculates CPU time for each process. Since CPU time has to be calculated relative to the last sample,
52
53
  # the collector has to be run twice to get CPU data.
53
54
  def calculate_processes
54
- ## 1. get a list of all processes grouped by command
55
- processes = Sys::ProcTable.ps
56
- grouped = Hash.new
55
+
56
+ ## 1. get a list of all processes
57
+ processes = Sys::ProcTable.ps.map{|p| ServerMetrics::Processes::Process.new(p) } # our Process object adds a method and adds some behavior
58
+
59
+ ## 2. loop through each process and calculate the CPU time.
60
+ # The CPU values returned by ProcTable are cumulative for the life of the process, which is not what we want.
61
+ # So, we rely on @last_process_list to make this calculation. If a process wasn't around last time, we use it's cumulative CPU time so far, which will be accurate enough.
62
+ now = Time.now
63
+ current_jiffies = get_jiffies
64
+ if @last_run && @last_jiffies && @last_process_list
65
+ elapsed_time = now - @last_run # in seconds
66
+ elapsed_jiffies = current_jiffies - @last_jiffies
67
+ if elapsed_time >= 1
68
+ processes.each do |p|
69
+ if last_cpu = @last_process_list[p.pid]
70
+ p.recent_cpu = p.combined_cpu - last_cpu
71
+ else
72
+ p.recent_cpu = p.combined_cpu # this process wasn't around last time, so just use the cumulative CPU time for its existence so far
73
+ end
74
+ # a) p.recent_cpu / elapsed_jiffies = the amount of CPU time this process has taken divided by the total "time slots" the CPU has available
75
+ # b) * 100 ... this turns it into a percentage
76
+ # b) / num_processors ... this normalizes for the the number of processors in the system, so it reflects the amount of CPU power avaiable as a whole
77
+ p.recent_cpu_percentage = ((p.recent_cpu.to_f / elapsed_jiffies.to_f ) * 100.0) / ServerMetrics::SystemInfo.num_processors.to_f
78
+ end
79
+ end
80
+ end
81
+
82
+ ## 3. group by command and aggregate the CPU
83
+ grouped = {}
57
84
  processes.each do |proc|
58
85
  grouped[proc.comm] ||= {
59
- :count => 0,
60
- :raw_cpu => 0,
61
86
  :cpu => 0,
62
87
  :memory => 0,
63
- :uid => 0,
88
+ :count => 0,
64
89
  :cmdlines => []
65
90
  }
66
- grouped[proc.comm][:count] += 1
67
- grouped[proc.comm][:raw_cpu] += proc.cutime + proc.cstime
68
- grouped[proc.comm][:memory] += proc.rss.to_f / 1024.0
69
- grouped[proc.comm][:uid] = proc.uid
91
+ grouped[proc.comm][:count] += 1
92
+ grouped[proc.comm][:cpu] += proc.recent_cpu_percentage || 0
93
+ grouped[proc.comm][:memory] += proc.rss.to_f / 1024.0
70
94
  grouped[proc.comm][:cmdlines] << proc.cmdline if !grouped[proc.comm][:cmdlines].include?(proc.cmdline)
71
95
  end # processes.each
72
96
 
73
- ## 2. loop through each and calculate the CPU time. To do this, you need to compare the current values against the last run
74
- now = Time.now
75
- if @last_run and @last_process_list
76
- elapsed_time = now - @last_run # in seconds
77
- if elapsed_time >= 1
78
- grouped.each do |name, values|
79
- if last_values = @last_process_list[name]
80
- cpu_since_last_sample = values[:raw_cpu] - last_values[:raw_cpu]
81
- grouped[name][:cpu] = (cpu_since_last_sample/(elapsed_time * ServerMetrics::SystemInfo.num_processors))*100
82
- else
83
- grouped.reject!(name) # no data from last run. don't report anything.
84
- end
85
- end
86
- end
97
+ # {pid => cpu_snapshot, pid2 => cpu_snapshot ...}
98
+ processes_to_store = processes.inject(Hash.new) do |hash, proc|
99
+ hash[proc.pid] = proc.combined_cpu
100
+ hash
87
101
  end
88
- @last_process_list = grouped
102
+
103
+ @last_process_list = processes_to_store
104
+ @last_jiffies = current_jiffies
89
105
  @last_run = now
106
+
90
107
  grouped
91
108
  end
92
109
 
@@ -97,10 +114,17 @@ class ServerMetrics::Processes
97
114
  @processes.map { |key, hash| {:cmd => key}.merge(hash) }.sort { |a, b| a[order_by] <=> b[order_by] }.reverse[0...num]
98
115
  end
99
116
 
117
+ # Relies on the /proc directory (/proc/timer_list). We need this because the process CPU utilization is measured in jiffies.
118
+ # In order to calculate the process' % usage of total CPU resources, we need to know how many jiffies have passed.
119
+ # Unfortunately, jiffies isn't a fixed value (it can vary between 100 and 250 per second), so we need to calculate it ourselves.
120
+ def get_jiffies
121
+ `cat /proc/timer_list`.match(/^jiffies: (\d+)$/)[1].to_i
122
+ end
123
+
100
124
  # for persisting to a file -- conforms to same basic API as the Collectors do.
101
125
  # why not just use marshall? This is a lot more manageable written to the Scout agent's history file.
102
126
  def to_hash
103
- {:last_run=>@last_run, :last_process_list=>@last_process_list}
127
+ {:last_run=>@last_run, :last_jiffies=>@last_jiffies, :last_process_list=>@last_process_list}
104
128
  end
105
129
 
106
130
  # for reinstantiating from a hash
@@ -108,8 +132,27 @@ class ServerMetrics::Processes
108
132
  def self.from_hash(hash)
109
133
  p=new(hash[:options])
110
134
  p.instance_variable_set('@last_run', hash[:last_run])
135
+ p.instance_variable_set('@last_jiffies', hash[:last_jiffies])
111
136
  p.instance_variable_set('@last_process_list', hash[:last_process_list])
112
137
  p
113
138
  end
114
139
 
140
+ # a thin wrapper around Sys:ProcTable's ProcTableStruct. We're using it to add some fields and behavior.
141
+ # Beyond what we're adding, it just passes through to its instance of ProcTableStruct
142
+ class Process
143
+ attr_accessor :recent_cpu, :recent_cpu_percentage # used to store the calculation of CPU since last sample
144
+ def initialize(proctable_struct)
145
+ @pts=proctable_struct
146
+ @recent_cpu = 0
147
+ end
148
+ def combined_cpu
149
+ # best thread I've seen on cutime vs utime & cstime vs stime: https://www.ruby-forum.com/topic/93176
150
+ # trying the metric that doesn't include the consumption of child processes
151
+ utime + stime
152
+ end
153
+ # delegate everything else to ProcTable::Struct
154
+ def method_missing(sym, *args, &block)
155
+ @pts.send sym, *args, &block
156
+ end
157
+ end
115
158
  end
@@ -1,3 +1,3 @@
1
1
  module ServerMetrics
2
- VERSION = "0.0.6"
2
+ VERSION = "0.0.8"
3
3
  end
data/test/test_helper.rb CHANGED
@@ -1,12 +1,12 @@
1
- $LOAD_PATH << File.expand_path( File.dirname(__FILE__) + '/..' )
2
- require 'test/unit'
3
- require 'rubygems'
1
+ $LOAD_PATH << File.expand_path( File.dirname(__FILE__) + '/../lib' ) # needed because when running tests, Rubygems manage $LOAD_PATH like it does in runtime
2
+ require 'rubygems' # so the development-only dependencies below can be loaded
4
3
  require 'pry'
5
4
  require 'awesome_print'
6
5
  require 'timecop'
6
+ require 'test/unit'
7
7
  require 'mocha/setup'
8
+ require 'server_metrics'
8
9
 
9
- require 'lib/server_metrics'
10
10
  AwesomePrint.defaults = {
11
11
  :indent => -2,
12
12
  :sort_keys =>true
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: server_metrics
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.6
4
+ version: 0.0.8
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-10-30 00:00:00.000000000 Z
12
+ date: 2013-11-06 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: sys-proctable
@@ -171,7 +171,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
171
171
  version: '0'
172
172
  segments:
173
173
  - 0
174
- hash: 2520847385531363153
174
+ hash: -865855317494333286
175
175
  required_rubygems_version: !ruby/object:Gem::Requirement
176
176
  none: false
177
177
  requirements:
@@ -180,7 +180,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
180
180
  version: '0'
181
181
  segments:
182
182
  - 0
183
- hash: 2520847385531363153
183
+ hash: -865855317494333286
184
184
  requirements: []
185
185
  rubyforge_project:
186
186
  rubygems_version: 1.8.25