diff --git a/README.md b/README.md index 86f7ea3..699cd9c 100644 --- a/README.md +++ b/README.md @@ -38,9 +38,9 @@ app = Hardware::PID.new "firefox" # Take the first matching PID loop do sleep 1 - cpu.used.to_i #=> 17 - pid.cpu_used #=> 1.5 - app.cpu_used.to_i #=> 4 + cpu.usage.to_i #=> 17 + pid.cpu_usage #=> 1.5 + app.cpu_usage.to_i #=> 4 end ``` ## Development diff --git a/shard.yml b/shard.yml index cba1bdd..a8f0d14 100644 --- a/shard.yml +++ b/shard.yml @@ -1,5 +1,5 @@ name: hardware -version: 0.3.1 +version: 0.4.0 authors: - bararchy diff --git a/spec/cpu_spec.cr b/spec/cpu_spec.cr index 038a4ca..def2277 100644 --- a/spec/cpu_spec.cr +++ b/spec/cpu_spec.cr @@ -3,15 +3,18 @@ require "./spec_helper" describe Hardware::CPU do cpu = Hardware::CPU.new it "parses '/proc/stat'" do - cpu.info.should be_a NamedTuple(used: Int32, idle: Int32, total: Int32) + cpu.stat.should be_a Array(Int32) end it "checks the percentage used" do - Hardware::CPU.previous_info.should be_a NamedTuple(used: Int32, idle: Int32, total: Int32) + cpu.previous_used.should be_a Int32 + cpu.previous_idle_wait.should be_a Int32 end it "checks the percentage used" do sleep 0.1 - (0 <= cpu.used <= 100).should be_true + usage = cpu.usage + usage.should be >= 0 + usage.should be <= 100 end end diff --git a/spec/memory_spec.cr b/spec/memory_spec.cr index de5a964..53a0309 100644 --- a/spec/memory_spec.cr +++ b/spec/memory_spec.cr @@ -20,10 +20,14 @@ describe Hardware::Memory do end it "checks the percentage available" do - (1 < memory.percent(used: false) <= 100).should be_true + percent = memory.percent(used: false) + percent.should be > 1 + percent.should be <= 100 end it "checks the percentage used" do - (1 < memory.percent(used: true) <= 100).should be_true + percent = memory.percent(used: true) + percent.should be > 1 + percent.should be <= 100 end end diff --git a/spec/net_spec.cr b/spec/net_spec.cr index dfd6058..d55d90f 100644 --- a/spec/net_spec.cr +++ b/spec/net_spec.cr @@ -4,7 +4,7 @@ describe Hardware::Net do net = Hardware::Net.new it "checks some types" do - net.in_octets.should be_a Int64 - net.out_octets.should be_a Int64 + net.in_octets.should be > 0_i64 + net.out_octets.should be > 0_i64 end end diff --git a/spec/pid_spec.cr b/spec/pid_spec.cr index 3e4abfb..28bff24 100644 --- a/spec/pid_spec.cr +++ b/spec/pid_spec.cr @@ -3,7 +3,7 @@ require "./spec_helper" describe Hardware::PID do describe "class methods" do it "tests .all " do - Hardware::PID.all { |pid| pid.should be_a Hardware::PID } + Hardware::PID.all &.should be_a Hardware::PID end it "tests .get_pids of the current process" do @@ -17,7 +17,7 @@ describe Hardware::PID do describe "instance methods" do it "creates a Hardware::PID based on a name" do - Hardware::PID.new("crystal-run-spec.tmp", cpu_time: false, cpu_total: false).pid.should eq Process.pid + Hardware::PID.new("crystal-run-spec.tmp", cpu_total: false).pid.should eq Process.pid end pid = Hardware::PID.new @@ -29,7 +29,7 @@ describe Hardware::PID do end it "parses exe" do - File.basename(pid.exe.not_nil!).should eq "crystal-run-spec.tmp" + File.basename(pid.exe).should eq "crystal-run-spec.tmp" end it "parses command" do @@ -40,39 +40,40 @@ describe Hardware::PID do describe "cpu_time" do pid1 = Hardware::PID.new(pid: 1) it "without children" do - (1 < pid1.cpu_time).should be_true + pid1.cpu_time.should be > 1 end it "with children" do - (1 < pid1.cpu_time(children: true)).should be_true + pid1.cpu_time(children: true).should be > 1 end end - describe "cpu_used with updates" do + describe "cpu_usage" do pid1 = Hardware::PID.new(pid: 1) it "percentage" do # Simulate CPU use if no activity - pid1.cpu_time_previous = pid1.cpu_time - 9 sleep 0.1 - (1 < pid1.cpu_used <= 100).should be_true + usage = pid1.cpu_usage + usage.should be > 0_f32 + usage.should be <= 100_f32 end it "cpu_total_previous equal to cpu_total_current" do pid1.cpu_total_previous.should eq Hardware::PID.cpu_total_current end end - describe "cpu_used with no updates" do + describe "cpu_usage with no updates" do Hardware::PID.cpu_total_current = -1 - pid1 = Hardware::PID.new(pid: 1, cpu_time: false, cpu_total: false) + pid1 = Hardware::PID.new(pid: 1, cpu_total: false) pid1.cpu_time_previous = -1 it "type" do - pid1.cpu_used.should be_a Float32 + pid1.cpu_usage.should be_a Float32 end - it "cpu_time_previous not updated" do - pid1.cpu_time_previous.should eq -1 + it "cpu_time_previous" do + pid1.cpu_time_previous.should be > 1 end it "cpu_total_current not updated" do @@ -85,7 +86,7 @@ describe Hardware::PID do end end - it "returns memory used" { (1 < pid.memory).should be_true } + it "returns memory usage" { pid.memory.should be > 1 } it "parses name" { pid.name.should eq "crystal-run-spec.tmp" } diff --git a/src/hardware.cr b/src/hardware.cr index 8f1e264..799e453 100644 --- a/src/hardware.cr +++ b/src/hardware.cr @@ -1,5 +1,5 @@ require "./hardware/**" module Hardware - VERSION = "0.3.1" + VERSION = "0.4.0" end diff --git a/src/hardware/cpu.cr b/src/hardware/cpu.cr index 2c016d8..30dc33a 100644 --- a/src/hardware/cpu.cr +++ b/src/hardware/cpu.cr @@ -4,23 +4,37 @@ # cpu = Hardware::CPU.new # loop do # sleep 1 -# cpu.used.to_i # => 17 +# cpu.usage.to_i # => 17 # end # ``` struct Hardware::CPU - # Returns the previous used, idle and total CPU time. Used to store the previous CPU time informations to calculate the percentage in`.used`. - class_property previous_info : NamedTuple(used: Int32, idle: Int32, total: Int32) = {used: 0, idle: 0, total: 0} - @stat = Array(Int32).new + # Previous used CPU time + getter previous_used : Int32 = 0 + # Previous idle CPU time + getter previous_idle_wait : Int32 = 0 + # Returns a parsed `/proc/stat` + getter stat : Array(Int32) # Creates a new `Hardware::CPU` based on the current memory state. def initialize + @stat = update_stat end - # Returns a parsed `/proc/stat`. - def stat : Array(Int32) + # Update the stats stored in `#stat` + def update_stat : Array(Int32) @stat = File.read("/proc/stat").lines.first[5..-1].split(' ').map &.to_i end + # Returns the CPU time used, which includes `idle` and `iowait` + def idle_wait : Int32 + idle + iowait + end + + # Returns the used CPU time, which includes `user`, `nice`, `system`, `irq`, `softirq` and `steal` + def used : Int32 + user + nice + system + irq + softirq + steal + end + # Generate methods based on stat {% begin %}{% i = 0 %} {% for num in %w(user nice system idle iowait irq softirq steal guest guest_nice) %} @@ -31,26 +45,21 @@ struct Hardware::CPU {% i = i + 1 %} {% end %}{% end %} - # Returns the current used, idle and total CPU time. - def info : NamedTuple(used: Int32, idle: Int32, total: Int32) - # update stat - stat - # Array: user nice system idle iowait irq softirq steal guest guest_nice - { - used: used = user + nice + system + irq + softirq + steal, - idle: idle_cpu = idle + iowait, - total: used + idle_cpu, - } + # Returns the total CPU time, the sum of `#idle` and `#used` + def total : Int32 + idle_wait + used end # Returns the CPU used in percentage based on `.previous_info`. - def used(update = true) : Float32 - current_info = info + def usage(update = true) : Float32 + # Update stats + update_stat + current_used, current_idle_wait = used, idle_wait # 100 * Usage / Total - result = (current_info[:used] - @@previous_info[:used]).to_f32 / (current_info[:total] - @@previous_info[:total]) * 100 + result = (current_used - @previous_used).to_f32 / (current_used + current_idle_wait - @previous_used - @previous_idle_wait) * 100 - @@previous_info = current_info if update + @previous_used, @previous_idle_wait = current_used, current_idle_wait if update result end end diff --git a/src/hardware/pid.cr b/src/hardware/pid.cr index d0875a4..46536bd 100644 --- a/src/hardware/pid.cr +++ b/src/hardware/pid.cr @@ -6,8 +6,8 @@ # # loop do # sleep 1 -# pid.cpu_used # => 1.5 -# app.cpu_used.to_i # => 4 +# pid.cpu_usage # => 1.5 +# app.cpu_usage.to_i # => 4 # end # ``` struct Hardware::PID @@ -15,46 +15,45 @@ struct Hardware::PID getter pid : Int32 # Used to avoid duplicate operations when lots of `Hardware::PID` are created (like a top implementation) class_property cpu_total_current : Int32 = 0 - # Previous `CPU.new.info[:total]`. + # Previous `CPU.new.total`. property cpu_total_previous : Int32 = 0 # Previous `#cpu_time`. property cpu_time_previous : Int32 = 0 - @cpu_time : Bool @cpu_total : Bool @stat = Stat.new Array(String).new # Creates a new `Hardware::PID` # Set to false to avoid setting `#cpu_total_current` (useful if lots of `Hardware::PID` are used) - def initialize(@pid : Int32 = Process.pid, @cpu_time = true, @cpu_total = true) - @cpu_total_previous = @@cpu_total_current = CPU.new.info[:total] if @cpu_total - @cpu_time_previous = self.cpu_time if @cpu_time + def initialize(@pid : Int32 = Process.pid, @cpu_total = true) + raise "pid #{pid} doesn't exist" if !exists? + @@cpu_total_current = CPU.new.total if @cpu_total end # Creates a new `Hardware::PID` by finding the `executable`'s pid. - def initialize(executable : String, cpu_time = true, cpu_total = true) + def initialize(executable : String, cpu_total = true) raise "no pid for '#{name}' exists" unless pid = Hardware::PID.get_pids(executable).first? - initialize pid, cpu_time, cpu_total + initialize pid, cpu_total end private def read_proc(file : String) : String - File.read "/proc/#{@pid}/" + file + File.read "/proc/#{@pid}/#{file}" rescue ex raise "#{ex}\nVerify if a process that have a pid number of '#{pid}' exists" end # Yields a `Hardware::PID` for each PID present on the system. - def self.all(cpu_time = false, cpu_total = false) + def self.all(cpu_total = false) : Nil Dir.each_child "/proc" do |pid_dir| if pid = pid_dir.to_i? - yield Hardware::PID.new(pid: pid, cpu_time: cpu_time, cpu_total: cpu_total) + yield Hardware::PID.new(pid: pid, cpu_total: cpu_total) end end end # Return all pids corresponding of a given `executable` name. - def self.get_pids(executable : String) + def self.get_pids(executable : String) : Array(Int32) pids = Array(Int32).new - all(cpu_time: false, cpu_total: false) do |pid| + all(cpu_total: false) do |pid| pids << pid.pid if pid.name == executable end pids @@ -71,7 +70,7 @@ struct Hardware::PID end # Returns the CPU time without including ones from `children` processes. - def cpu_time(children = false) + def cpu_time(children = false) : Int32 # update stat stat @@ -87,96 +86,51 @@ struct Hardware::PID end # Returns the CPU used in percentage. - def cpu_used : Float32 + def cpu_usage : Float32 cpu_time_current = cpu_time - @@cpu_total_current = CPU.new.info[:total] if @cpu_total + @@cpu_total_current = CPU.new.total if @cpu_total # 100 * Usage / Total - result = 100 * ((cpu_time_current - @cpu_time_previous.to_f32) / (@@cpu_total_current - @cpu_total_previous)) + result = (cpu_time_current - @cpu_time_previous).to_f32 / (@@cpu_total_current - @cpu_total_previous) * 100 + + @cpu_time_previous = cpu_time_current - @cpu_time_previous = cpu_time_current if @cpu_time @cpu_total_previous = @@cpu_total_current result end # Returns `/proc/``#pid``/exe` if readable. - def exe : String? - if File.readable? path = "/proc/#{@pid}/exe" - File.real_path path - end - rescue - nil + def exe : String + File.real_path "/proc/#{@pid}/exe" + end + + def exists? : Bool + Dir.exists? "/proc/#{@pid}" end # Returns the actual memory used by the process. - def memory + def memory : Int32 # Assuming that PAGESIZE is 4096 kB statm.first * 4 end # Returns the PID name based on `#exe` or `#cmdline`. - def name - File.basename (cmd = exe) ? cmd : command + def name : String + File.basename exe + rescue + File.basename command end # Returns `Hardware::Net` for `#pid` - def net + def net : Net Net.new @pid end # Returns a parsed `/proc/``#pid``/stat`. - def stat + def stat : Stat @stat = Stat.new read_proc("stat").split ' ' end - # Parse stat initialized at `Hadware::PID#stat` - struct Stat - @stat = Array(String).new - - def initialize(@stat) - end - - # Returns the "comm" field of `#stat`. - def comm : String - @stat[1] - end - - # Returns the "state" field of `#stat`. - def state : String - @stat[2][1..-2] - end - - # Generate methods based on stat - {% begin %}{% i = 3 %} - {% for num in %w( - ppid - pgrp - session - tty_nr - tpgid - flags minflt - cminflt - majflt - cmajflt - utime - stime - cutime - cstime - priority - nice - numthreads - itrealvalue - starttime - vsize - rss) %} - # Returns the "{{num.id}}" field of `#stat`. - def {{num.id}} : Int32 - @stat[{{i}}].to_i - end - {% i = i + 1 %} - {% end %}{% end %} - end - # Returns a parsed `/proc/``#pid``/statm`. def statm : Array(Int32) read_proc("statm").split(' ').map &.to_i diff --git a/src/hardware/pid/stat.cr b/src/hardware/pid/stat.cr new file mode 100644 index 0000000..29b0686 --- /dev/null +++ b/src/hardware/pid/stat.cr @@ -0,0 +1,47 @@ +# Parse stat initialized at `Hadware::PID#stat` +struct Hardware::PID::Stat + @stat = Array(String).new + + def initialize(@stat) + end + + # Returns the "comm" field of `#stat`. + def comm : String + @stat[1] + end + + # Returns the "state" field of `#stat`. + def state : String + @stat[2][1..-2] + end + + # Generate methods based on stat + {% begin %}{% i = 3 %} + {% for num in %w( + ppid + pgrp + session + tty_nr + tpgid + flags minflt + cminflt + majflt + cmajflt + utime + stime + cutime + cstime + priority + nice + numthreads + itrealvalue + starttime + vsize + rss) %} + # Returns the "{{num.id}}" field of `#stat`. + def {{num.id}} : Int32 + @stat[{{i}}].to_i + end + {% i = i + 1 %} + {% end %}{% end %} +end