server_metrics 0.0.6 → 0.0.8

Sign up to get free protection for your applications and to get access to all the features.
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