-
-
Save tombruijn/5f5e0c34af40cfb3967eca81ad8b5317 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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