Skip to content

Instantly share code, notes, and snippets.

@tombruijn
Last active May 2, 2019 13:49
Show Gist options
  • Save tombruijn/5f5e0c34af40cfb3967eca81ad8b5317 to your computer and use it in GitHub Desktop.
Save tombruijn/5f5e0c34af40cfb3967eca81ad8b5317 to your computer and use it in GitHub Desktop.
# appsignal_stats.rb
#
# Script to test CPU usage calculations from inside a container. Compare these
# values against the output of:
#
# - `docker stats`
# - `ctop`
# - `ctop -scale-cpu`
#
# Usage:
# $ ruby appsignal_stats.rb
#
# Exit by pressing `Ctrl + C`.
## Configuration
# The time interval for which we should measure the CPU metrics and print new calculations.
# The value 1 will measure the CPU every second.
# The value 60 will measure the CPU every minute.
# Changing this value will change the update time in the terminal, but should
# not affect the calculated and printed values.
measurement_interval_in_s = 2
###############################################################################
### Do not change below this line ###
###############################################################################
# Store this measurements in the `previous_` prefixed variable so we can
# Calculate the deltas using the next measurement in the next loop.
previous_measurement = {}
previous_time_in_ns = nil
# The number of logical CPU cores the host exposes to the container.
# Format of file: `<cpu time> <cpu time> <cpu time> <cpu time> [<cpu time> element per core]`
cpu_count = File.read("/sys/fs/cgroup/cpuacct/cpuacct.usage_percpu").split(" ").count
## Helper methods
# Calculates the difference (delta) between two values.
# @param first_value [Number] Should be higher than the second_value.
# @param second_value [Number] Should be lower than the first_value.
# @return [Number] The delta of two values
def delta(first_value, second_value)
if second_value > first_value
raise "Second value cannot be larger than the first value."
end
first_value - second_value
end
# Calculates the percentage of a value based on the total value.
# @param value [Number] the number of which to calculate the percentage.
# @param total [Number] the number which is the total of which percentage of
# the value pararmeter is calculated. It is the 100% of the value.
# @return [Float] Percentage of the value based on its total.
def percentage(value, total)
(value.to_f / total.to_f) * 100.0
end
# Returns the current time in nanoseconds.
# Source: https://stackoverflow.com/questions/46557704/convert-a-time-to-nanoseconds-in-ruby#comment80077621_46558082
# @return [Float]
def time_now_in_ns
time = Time.now.utc
(time.to_i * (10 ** 9) + time.nsec).to_f
end
# @return [Hash] Hash containing the container's CPU metrics.
def fetch_cpu_measurement
measurement = {}
# The container's total time spend using the CPU expressed in nanoseconds.
measurement[:total_usage] = File.read("/sys/fs/cgroup/cpuacct/cpuacct.usage").to_i
# The container's time spend using the CPU broken into the `user` and
# `system` groups.
parts = File.read("/sys/fs/cgroup/cpuacct/cpuacct.stat")
parts.split("\n").each do |line|
# File format, every line: `<group> <value in ms>`
part, value = line.split(" ")
# Convert the time to nanoseconds.
measurement[part.to_sym] = value.to_f * 10_000_000.0
end
# The time the container's CPU usage was throttled.
parts = File.read("/sys/fs/cgroup/cpu/cpu.stat")
parts.split("\n").each do |line|
part, value = line.split(" ")
next unless part == "throttled_time"
measurement[part.to_sym] = value.to_f
end
measurement
end
# Round float value to 2 number behind the decimal and display as a percentage.
def format_percentage(value)
"%1.2f%%" % value
end
# Round float value to 2 number behind the decimal.
def format_value(value)
"%1.2f" % value
end
loop do
measurement = fetch_cpu_measurement
# Only calculate deltas when we have the first measurement. This is why the
# first measurement is not printed to the terminal.
unless previous_measurement.empty?
print `clear` # Clear screen on every measurement so it's at the top of the terminal pane.
# This is used to calculate the total CPU time. This is the 100% against
# which we calculate the total usage, user and system groups for CPU usage.
time_difference_ns = delta(time_now_in_ns, previous_time_in_ns)
# Calculate deltas for every measurement and then calculate the percentages
# for those deltas based on the total time.
percentages = {
:total_usage => percentage(delta(measurement[:total_usage], previous_measurement[:total_usage]), time_difference_ns),
:user => percentage(delta(measurement[:user], previous_measurement[:user]), time_difference_ns),
:system => percentage(delta(measurement[:system], previous_measurement[:system]), time_difference_ns),
:throttled_time => percentage(delta(measurement[:throttled_time], previous_measurement[:throttled_time]), time_difference_ns)
}
# Printed values array used to format it human readable
values = [
[
"",
"CPU %",
"Raw value" # Measurement values expressed in nanoseconds
],
[
"Total time passed:",
format_percentage(percentage(time_difference_ns, time_difference_ns)),
format_value(time_difference_ns)
],
[
"Total CPU usage:",
format_percentage(percentages[:total_usage]),
format_value(measurement[:total_usage])
],
[
"Total CPU usage / # CPU cores:",
# Same output of `ctop -scale-cpu`.
# It will "show cpu as % of system total".
format_percentage(percentages[:total_usage] / cpu_count),
format_value(measurement[:total_usage])
],
[
"User group usage:",
format_percentage(percentages[:user]),
format_value(measurement[:user])
],
[
"System group usage:",
format_percentage(percentages[:system]),
format_value(measurement[:system])
],
[
"CPU throttled:",
format_percentage(percentages[:throttled_time]),
format_value(measurement[:throttled_time])
]
]
# Calculate the maximum width for every column so everything is aligned in
# a readable way.
max_column_widths = []
values.each do |value_pairs|
value_pairs.each_with_index do |value, index|
max_column_widths[index] ||= []
max_column_widths[index] << value.length
end
end
max_column_widths.map!(&:max)
puts "AppSignal container stats"
puts "- Detected container CPUs: #{format_value(cpu_count)}"
puts
# Print columns containing values
values.each do |value_pairs|
value_pairs.each_with_index do |value, index|
print "#{value.rjust(max_column_widths[index])} | "
end
print "\n"
end
end
# Store this measurements in the `previous_` prefixed variable so we can
# Calculate the deltas using the next measurement in the next loop.
previous_measurement = measurement
previous_time_in_ns = time_now_in_ns
# Sleep until the next measurement
sleep measurement_interval_in_s
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment