├── .editorconfig ├── .github └── workflows │ ├── documentation-coverage.yaml │ ├── documentation.yaml │ ├── rubocop.yaml │ ├── test-coverage.yaml │ ├── test-external.yaml │ └── test.yaml ├── .gitignore ├── .rubocop.yml ├── bake └── process │ └── metrics.rb ├── bin └── process-metrics ├── command-line.png ├── config ├── external.yaml └── sus.rb ├── gems.rb ├── guides ├── getting-started │ └── readme.md └── links.yaml ├── lib └── process │ ├── metrics.rb │ └── metrics │ ├── command.rb │ ├── command │ ├── summary.rb │ └── top.rb │ ├── general.rb │ ├── memory.rb │ ├── memory │ ├── darwin.rb │ └── linux.rb │ └── version.rb ├── license.md ├── process-metrics.gemspec ├── readme.md ├── release.cert └── test └── process ├── general.rb ├── memory.rb └── metrics.rb /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | indent_size = 2 6 | 7 | [*.{yml,yaml}] 8 | indent_style = space 9 | indent_size = 2 10 | -------------------------------------------------------------------------------- /.github/workflows/documentation-coverage.yaml: -------------------------------------------------------------------------------- 1 | name: Documentation Coverage 2 | 3 | on: [push, pull_request] 4 | 5 | permissions: 6 | contents: read 7 | 8 | env: 9 | CONSOLE_OUTPUT: XTerm 10 | COVERAGE: PartialSummary 11 | 12 | jobs: 13 | validate: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: ruby/setup-ruby@v1 19 | with: 20 | ruby-version: "3.4" 21 | bundler-cache: true 22 | 23 | - name: Validate coverage 24 | timeout-minutes: 5 25 | run: bundle exec bake decode:index:coverage lib 26 | -------------------------------------------------------------------------------- /.github/workflows/documentation.yaml: -------------------------------------------------------------------------------- 1 | name: Documentation 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages: 9 | permissions: 10 | contents: read 11 | pages: write 12 | id-token: write 13 | 14 | # Allow one concurrent deployment: 15 | concurrency: 16 | group: "pages" 17 | cancel-in-progress: true 18 | 19 | env: 20 | CONSOLE_OUTPUT: XTerm 21 | BUNDLE_WITH: maintenance 22 | 23 | jobs: 24 | generate: 25 | runs-on: ubuntu-latest 26 | 27 | steps: 28 | - uses: actions/checkout@v4 29 | 30 | - uses: ruby/setup-ruby@v1 31 | with: 32 | ruby-version: "3.4" 33 | bundler-cache: true 34 | 35 | - name: Installing packages 36 | run: sudo apt-get install wget 37 | 38 | - name: Generate documentation 39 | timeout-minutes: 5 40 | run: bundle exec bake utopia:project:static --force no 41 | 42 | - name: Upload documentation artifact 43 | uses: actions/upload-pages-artifact@v3 44 | with: 45 | path: docs 46 | 47 | deploy: 48 | runs-on: ubuntu-latest 49 | 50 | environment: 51 | name: github-pages 52 | url: ${{steps.deployment.outputs.page_url}} 53 | 54 | needs: generate 55 | steps: 56 | - name: Deploy to GitHub Pages 57 | id: deployment 58 | uses: actions/deploy-pages@v4 59 | -------------------------------------------------------------------------------- /.github/workflows/rubocop.yaml: -------------------------------------------------------------------------------- 1 | name: RuboCop 2 | 3 | on: [push, pull_request] 4 | 5 | permissions: 6 | contents: read 7 | 8 | env: 9 | CONSOLE_OUTPUT: XTerm 10 | 11 | jobs: 12 | check: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: ruby/setup-ruby@v1 18 | with: 19 | ruby-version: ruby 20 | bundler-cache: true 21 | 22 | - name: Run RuboCop 23 | timeout-minutes: 10 24 | run: bundle exec rubocop 25 | -------------------------------------------------------------------------------- /.github/workflows/test-coverage.yaml: -------------------------------------------------------------------------------- 1 | name: Test Coverage 2 | 3 | on: [push, pull_request] 4 | 5 | permissions: 6 | contents: read 7 | 8 | env: 9 | CONSOLE_OUTPUT: XTerm 10 | COVERAGE: PartialSummary 11 | 12 | jobs: 13 | test: 14 | name: ${{matrix.ruby}} on ${{matrix.os}} 15 | runs-on: ${{matrix.os}}-latest 16 | 17 | strategy: 18 | matrix: 19 | os: 20 | - ubuntu 21 | - macos 22 | 23 | ruby: 24 | - "3.4" 25 | 26 | steps: 27 | - uses: actions/checkout@v4 28 | - uses: ruby/setup-ruby@v1 29 | with: 30 | ruby-version: ${{matrix.ruby}} 31 | bundler-cache: true 32 | 33 | - name: Run tests 34 | timeout-minutes: 5 35 | run: bundle exec bake test 36 | 37 | - uses: actions/upload-artifact@v4 38 | with: 39 | include-hidden-files: true 40 | if-no-files-found: error 41 | name: coverage-${{matrix.os}}-${{matrix.ruby}} 42 | path: .covered.db 43 | 44 | validate: 45 | needs: test 46 | runs-on: ubuntu-latest 47 | 48 | steps: 49 | - uses: actions/checkout@v4 50 | - uses: ruby/setup-ruby@v1 51 | with: 52 | ruby-version: "3.4" 53 | bundler-cache: true 54 | 55 | - uses: actions/download-artifact@v4 56 | 57 | - name: Validate coverage 58 | timeout-minutes: 5 59 | run: bundle exec bake covered:validate --paths */.covered.db \; 60 | -------------------------------------------------------------------------------- /.github/workflows/test-external.yaml: -------------------------------------------------------------------------------- 1 | name: Test External 2 | 3 | on: [push, pull_request] 4 | 5 | permissions: 6 | contents: read 7 | 8 | env: 9 | CONSOLE_OUTPUT: XTerm 10 | 11 | jobs: 12 | test: 13 | name: ${{matrix.ruby}} on ${{matrix.os}} 14 | runs-on: ${{matrix.os}}-latest 15 | 16 | strategy: 17 | matrix: 18 | os: 19 | - ubuntu 20 | - macos 21 | 22 | ruby: 23 | - "3.1" 24 | - "3.2" 25 | - "3.3" 26 | - "3.4" 27 | 28 | steps: 29 | - uses: actions/checkout@v4 30 | - uses: ruby/setup-ruby@v1 31 | with: 32 | ruby-version: ${{matrix.ruby}} 33 | bundler-cache: true 34 | 35 | - name: Run tests 36 | timeout-minutes: 10 37 | run: bundle exec bake test:external 38 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push, pull_request] 4 | 5 | permissions: 6 | contents: read 7 | 8 | env: 9 | CONSOLE_OUTPUT: XTerm 10 | 11 | jobs: 12 | test: 13 | name: ${{matrix.ruby}} on ${{matrix.os}} 14 | runs-on: ${{matrix.os}}-latest 15 | continue-on-error: ${{matrix.experimental}} 16 | 17 | strategy: 18 | matrix: 19 | os: 20 | - ubuntu 21 | - macos 22 | 23 | ruby: 24 | - "3.1" 25 | - "3.2" 26 | - "3.3" 27 | - "3.4" 28 | 29 | experimental: [false] 30 | 31 | include: 32 | - os: ubuntu 33 | ruby: truffleruby 34 | experimental: true 35 | - os: ubuntu 36 | ruby: jruby 37 | experimental: true 38 | - os: ubuntu 39 | ruby: head 40 | experimental: true 41 | 42 | steps: 43 | - uses: actions/checkout@v4 44 | - uses: ruby/setup-ruby@v1 45 | with: 46 | ruby-version: ${{matrix.ruby}} 47 | bundler-cache: true 48 | 49 | - name: Run tests 50 | timeout-minutes: 10 51 | run: bundle exec bake test 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /pkg/ 3 | /gems.locked 4 | /.covered.db 5 | /external 6 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | DisabledByDefault: true 3 | 4 | Layout/IndentationStyle: 5 | Enabled: true 6 | EnforcedStyle: tabs 7 | 8 | Layout/InitialIndentation: 9 | Enabled: true 10 | 11 | Layout/IndentationWidth: 12 | Enabled: true 13 | Width: 1 14 | 15 | Layout/IndentationConsistency: 16 | Enabled: true 17 | EnforcedStyle: normal 18 | 19 | Layout/BlockAlignment: 20 | Enabled: true 21 | 22 | Layout/EndAlignment: 23 | Enabled: true 24 | EnforcedStyleAlignWith: start_of_line 25 | 26 | Layout/BeginEndAlignment: 27 | Enabled: true 28 | EnforcedStyleAlignWith: start_of_line 29 | 30 | Layout/ElseAlignment: 31 | Enabled: true 32 | 33 | Layout/DefEndAlignment: 34 | Enabled: true 35 | 36 | Layout/CaseIndentation: 37 | Enabled: true 38 | 39 | Layout/CommentIndentation: 40 | Enabled: true 41 | 42 | Layout/EmptyLinesAroundClassBody: 43 | Enabled: true 44 | 45 | Layout/EmptyLinesAroundModuleBody: 46 | Enabled: true 47 | 48 | Style/FrozenStringLiteralComment: 49 | Enabled: true 50 | 51 | Style/StringLiterals: 52 | Enabled: true 53 | EnforcedStyle: double_quotes 54 | -------------------------------------------------------------------------------- /bake/process/metrics.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2025, by Samuel Williams. 5 | 6 | def capture(pid: nil, ppid: nil) 7 | require "process/metrics/general" 8 | 9 | Process::Metrics::General.capture(pid: pid, ppid: ppid) 10 | end 11 | 12 | -------------------------------------------------------------------------------- /bin/process-metrics: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # Copyright, 2020, by Samuel G. D. Williams. 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | # THE SOFTWARE. 23 | 24 | require_relative "../lib/process/metrics/command" 25 | 26 | begin 27 | Process::Metrics::Command.call 28 | rescue Interrupt 29 | end 30 | -------------------------------------------------------------------------------- /command-line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/socketry/process-metrics/61007e46ec29adae85a6fcae9c4cb56a457754a0/command-line.png -------------------------------------------------------------------------------- /config/external.yaml: -------------------------------------------------------------------------------- 1 | falcon: 2 | url: https://github.com/socketry/falcon 3 | 4 | -------------------------------------------------------------------------------- /config/sus.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2024-2025, by Samuel Williams. 5 | 6 | require "covered/sus" 7 | include Covered::Sus 8 | -------------------------------------------------------------------------------- /gems.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2019-2025, by Samuel Williams. 5 | 6 | source "https://rubygems.org" 7 | 8 | gemspec 9 | 10 | group :maintenance, optional: true do 11 | gem "bake-gem" 12 | gem "bake-modernize" 13 | 14 | gem "utopia-project" 15 | end 16 | 17 | group :test do 18 | gem "sus" 19 | gem "covered" 20 | gem "decode" 21 | gem "rubocop" 22 | 23 | gem "bake-test" 24 | gem "bake-test-external" 25 | end 26 | -------------------------------------------------------------------------------- /guides/getting-started/readme.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | This guide explains how to use the `process-metrics` gem to collect and analyze process metrics including processor and memory utilization. 4 | 5 | ## Installation 6 | 7 | Add the gem to your project: 8 | 9 | ``` bash 10 | $ bundle add process-metrics 11 | ``` 12 | 13 | Or, if you prefer, install it globally: 14 | 15 | ``` bash 16 | $ gem install process-metrics 17 | ``` 18 | 19 | ## Core Concepts 20 | 21 | The `process-metrics` gem provides a simple interface to collect and analyze process metrics. 22 | 23 | - {ruby Process::Metrics::General} is the main entry point for process metrics. Use {ruby Process::Metrics::General.capture} to collect metrics for one or more processes. 24 | - {ruby Process::Metrics::Memory} provides additional methods for collecting memory metrics when the host operating system provides the necessary information. 25 | 26 | ## Usage 27 | 28 | To collect process metrics, use the {ruby Process::Metrics::General.capture} method: 29 | 30 | ``` ruby 31 | Process::Metrics::General.capture(pid: Process.pid) 32 | # => 33 | # {3517456=> 34 | # #>} 58 | ``` 59 | 60 | If you want to capture a tree of processes, you can specify the `ppid:` option instead. 61 | 62 | ### Fields 63 | 64 | The {ruby Process::Metrics::General} struct contains the following fields: 65 | 66 | - `process_id` - Process ID, a unique identifier for the process. 67 | - `parent_process_id` - Parent Process ID, the process ID of the process that started this process. 68 | - `process_group_id` - Process Group ID, the process group ID of the process, which can be shared by multiple processes. 69 | - `processor_utilization` - Processor Utilization (%), the percentage of CPU time used by the process (over a system-specific duration). 70 | - `total_size` - Memory Size (KB), the total size of the process's memory space (usually over-estimated as it doesn't take into account shared memory). 71 | - `resident_size` - Resident (Set) Size (KB), the amount of physical memory used by the process. 72 | - `processor_time` - CPU Time (s), the amount of CPU time used by the process. 73 | - `elapsed_time` - Elapsed Time (s), the amount of time the process has been running. 74 | - `command` - Command Name, the name of the command that started the process. 75 | 76 | The {ruby Process::Metrics::Memory} struct contains the following fields: 77 | 78 | - `map_count` - Number of Memory Mappings, e.g. number of thread stacks, fiber stacks, shared libraries, memory mapped files, etc. 79 | - `resident_size` - Resident Memory Size (KB), the amount of physical memory used by the process. 80 | - `proportional_size` - Proportional Memory Size (KB), the amount of memory that the process is using, taking into account shared memory. 81 | - `shared_clean_size` - Shared Clean Memory Size (KB), the amount of shared memory that is clean (not modified). 82 | - `shared_dirty_size` - Shared Dirty Memory Size (KB), the amount of shared memory that is dirty (modified). 83 | - `private_clean_size` - Private Clean Memory Size (KB), the amount of private memory that is clean (not modified). 84 | - `private_dirty_size` - Private Dirty Memory Size (KB), the amount of private memory that is dirty (modified). 85 | - `referenced_size` - Referenced Memory Size (KB), active page-cache that isn't going to be reclaimed any time soon. 86 | - `anonymous_size` - Anonymous Memory Size (KB), mapped memory that isn't backed by a file. 87 | - `swap_size` - Swap Memory Size (KB), the amount of memory that has been swapped to disk. 88 | - `proportional_swap_size` - Proportional Swap Memory Size (KB), the amount of memory that has been swapped to disk, excluding shared memory. 89 | 90 | In general, the interpretation of these fields is operating system specific. At best, they provide a rough estimate of the process's memory usage, but you should consult the documentation for your operating system for more details on exactly what each field represents. 91 | -------------------------------------------------------------------------------- /guides/links.yaml: -------------------------------------------------------------------------------- 1 | getting-started: 2 | order: 1 3 | -------------------------------------------------------------------------------- /lib/process/metrics.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2019-2024, by Samuel Williams. 5 | 6 | require_relative "metrics/version" 7 | require_relative "metrics/general" 8 | -------------------------------------------------------------------------------- /lib/process/metrics/command.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2020-2025, by Samuel Williams. 5 | 6 | require_relative "command/top" 7 | 8 | module Process 9 | module Metrics 10 | module Command 11 | def self.call(*arguments) 12 | Top.call(*arguments) 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/process/metrics/command/summary.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2020-2025, by Samuel Williams. 5 | 6 | require "samovar" 7 | 8 | require_relative "../general" 9 | 10 | require "console/terminal" 11 | 12 | module Process 13 | module Metrics 14 | module Command 15 | module Bar 16 | BLOCK = [ 17 | " ", 18 | "▏", 19 | "▎", 20 | "▍", 21 | "▌", 22 | "▋", 23 | "▊", 24 | "▉", 25 | "█", 26 | ] 27 | 28 | def self.format(value, width) 29 | blocks = width * value 30 | full_blocks = blocks.floor 31 | partial_block = ((blocks - full_blocks) * BLOCK.size).floor 32 | 33 | if partial_block.zero? 34 | BLOCK.last * full_blocks 35 | else 36 | "#{BLOCK.last * full_blocks}#{BLOCK[partial_block]}" 37 | end.ljust(width) 38 | end 39 | end 40 | 41 | class Summary < Samovar::Command 42 | self.description = "Display a summary of memory usage statistics." 43 | 44 | options do 45 | option "--pid ", "Report on a single process id.", type: Integer, required: true 46 | option "-p/--ppid ", "Report on all children of this process id.", type: Integer, required: true 47 | 48 | option "--total-memory ", "Set the total memory relative to the usage (MiB).", type: Integer 49 | end 50 | 51 | def terminal 52 | terminal = Console::Terminal.for($stdout) 53 | 54 | # terminal[:pid] = terminal.style(:blue) 55 | terminal[:command] = terminal.style(nil, nil, :bold) 56 | terminal[:key] = terminal.style(:cyan) 57 | 58 | terminal[:low] = terminal.style(:green) 59 | terminal[:medium] = terminal.style(:yellow) 60 | terminal[:high] = terminal.style(:red) 61 | 62 | return terminal 63 | end 64 | 65 | def format_processor_utilization(value, terminal) 66 | if value > 80.0 67 | intensity = :high 68 | elsif value > 50.0 69 | intensity = :medium 70 | else 71 | intensity = :low 72 | end 73 | 74 | formatted = "%5.1f%% " % value 75 | 76 | terminal.print(formatted.rjust(10), intensity, "[", Bar.format(value / 100.0, 60), "]", :reset) 77 | end 78 | 79 | UNITS = ["KiB", "MiB", "GiB"] 80 | 81 | def format_size(value, units: UNITS) 82 | unit = 0 83 | 84 | while value > 1024.0 && unit < units.size 85 | value /= 1024.0 86 | unit += 1 87 | end 88 | 89 | return "#{value.round(unit)}#{units[unit]}" 90 | end 91 | 92 | def format_memory(value, total, terminal) 93 | if value > (total * 0.8) 94 | intensity = :high 95 | elsif value > (total * 0.5) 96 | intensity = :medium 97 | else 98 | intensity = :low 99 | end 100 | 101 | formatted = (format_size(value) + " ").rjust(10) 102 | 103 | terminal.print(formatted, intensity, "[", Bar.format(value / total.to_f, 60), "]", :reset) 104 | end 105 | 106 | def total_memory 107 | if total_memory = @options[:total_memory] 108 | return total_memory * 1024 109 | else 110 | return Process::Metrics::Memory.total_size 111 | end 112 | end 113 | 114 | def call 115 | terminal = self.terminal 116 | 117 | summary = Process::Metrics::General.capture(pid: @options[:pid], ppid: @options[:ppid]) 118 | 119 | format_memory = self.method(:format_memory).curry 120 | shared_memory = 0 121 | private_memory = 0 122 | total_memory = self.total_memory 123 | 124 | proportional = true 125 | 126 | summary.each do |pid, general| 127 | terminal.print_line(:pid, pid, :reset, " ", :command, general[:command]) 128 | 129 | terminal.print(:key, "Processor Usage: ".rjust(20), :reset) 130 | format_processor_utilization(general.processor_utilization, terminal) 131 | terminal.print_line 132 | 133 | if memory = general.memory 134 | shared_memory += memory.proportional_size 135 | private_memory += memory.unique_size 136 | 137 | terminal.print_line( 138 | :key, "Memory: ".rjust(20), :reset, 139 | format_memory[memory.proportional_size, total_memory] 140 | ) 141 | 142 | terminal.print_line( 143 | :key, "Private Memory: ".rjust(20), :reset, 144 | format_memory[memory.unique_size, total_memory] 145 | ) 146 | else 147 | shared_memory += general.resident_size 148 | proportional = false 149 | 150 | terminal.print_line( 151 | :key, "Memory: ".rjust(20), :reset, 152 | format_memory[general.resident_size, total_memory] 153 | ) 154 | end 155 | end 156 | 157 | terminal.print_line("Summary") 158 | 159 | if proportional 160 | terminal.print_line( 161 | :key, "Memory: ".rjust(20), :reset, 162 | format_memory[shared_memory, total_memory] 163 | ) 164 | 165 | terminal.print_line( 166 | :key, "Private Memory: ".rjust(20), :reset, 167 | format_memory[private_memory, total_memory] 168 | ) 169 | else 170 | terminal.print_line( 171 | :key, "Memory: ".rjust(20), :reset, 172 | format_memory[memory, total_memory] 173 | ) 174 | end 175 | 176 | terminal.print_line( 177 | :key, "Memory (Total): ".rjust(20), :reset, 178 | format_memory[shared_memory + private_memory, total_memory] 179 | ) 180 | end 181 | end 182 | end 183 | end 184 | end 185 | -------------------------------------------------------------------------------- /lib/process/metrics/command/top.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2020-2025, by Samuel Williams. 5 | 6 | require "samovar" 7 | 8 | require_relative "summary" 9 | require_relative "../version" 10 | 11 | module Process 12 | module Metrics 13 | module Command 14 | class Top < Samovar::Command 15 | self.description = "Collect memory usage statistics." 16 | 17 | options do 18 | option "-h/--help", "Print out help information." 19 | option "-v/--version", "Print out the application version." 20 | end 21 | 22 | nested :command, { 23 | "summary" => Summary, 24 | }, default: "summary" 25 | 26 | def call 27 | if @options[:version] 28 | puts "#{self.name} v#{VERSION}" 29 | elsif @options[:help] 30 | self.print_usage 31 | else 32 | @command.call 33 | end 34 | end 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/process/metrics/general.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2019-2025, by Samuel Williams. 5 | 6 | require_relative "memory" 7 | require "set" 8 | require "json" 9 | 10 | module Process 11 | module Metrics 12 | PS = "ps" 13 | 14 | DURATION = /\A 15 | (?:(?\d+)-)? # Optional days (e.g., '2-') 16 | (?:(?\d+):)? # Optional hours (e.g., '1:') 17 | (?\d{1,2}): # Minutes (always present, 1 or 2 digits) 18 | (?\d{2}) # Seconds (exactly 2 digits) 19 | (?:\.(?\d{1,2}))? # Optional fraction of a second (e.g., '.27') 20 | \z/x 21 | 22 | 23 | # Parse a duration string into seconds. 24 | # According to the linux manual page specifications. 25 | def self.duration(value) 26 | if match = DURATION.match(value) 27 | days = match[:days].to_i 28 | hours = match[:hours].to_i 29 | minutes = match[:minutes].to_i 30 | seconds = match[:seconds].to_i 31 | fraction = match[:fraction].to_i 32 | 33 | return days * 24 * 60 * 60 + hours * 60 * 60 + minutes * 60 + seconds + fraction / 100.0 34 | else 35 | return 0.0 36 | end 37 | end 38 | 39 | # The fields that will be extracted from the `ps` command. 40 | FIELDS = { 41 | pid: ->(value){value.to_i}, # Process ID 42 | ppid: ->(value){value.to_i}, # Parent Process ID 43 | pgid: ->(value){value.to_i}, # Process Group ID 44 | pcpu: ->(value){value.to_f}, # Percentage CPU 45 | vsz: ->(value){value.to_i}, # Virtual Size (KiB) 46 | rss: ->(value){value.to_i}, # Resident Size (KiB) 47 | time: self.method(:duration), # CPU Time (seconds) 48 | etime: self.method(:duration), # Elapsed Time (seconds) 49 | command: ->(value){value}, # Command (name of the process) 50 | } 51 | 52 | # General process information. 53 | class General < Struct.new(:process_id, :parent_process_id, :process_group_id, :processor_utilization, :virtual_size, :resident_size, :processor_time, :elapsed_time, :command, :memory) 54 | # Convert the object to a JSON serializable hash. 55 | def as_json 56 | { 57 | process_id: self.process_id, 58 | parent_process_id: self.parent_process_id, 59 | process_group_id: self.process_group_id, 60 | processor_utilization: self.processor_utilization, 61 | total_size: self.total_size, 62 | virtual_size: self.virtual_size, 63 | resident_size: self.resident_size, 64 | processor_time: self.processor_time, 65 | elapsed_time: self.elapsed_time, 66 | command: self.command, 67 | memory: self.memory&.as_json, 68 | } 69 | end 70 | 71 | # Convert the object to a JSON string. 72 | def to_json(*arguments) 73 | as_json.to_json(*arguments) 74 | end 75 | 76 | # The total size of the process in memory, in kilobytes. 77 | def total_size 78 | if memory = self.memory 79 | memory.proportional_size 80 | else 81 | self.resident_size 82 | end 83 | end 84 | 85 | alias memory_usage total_size 86 | 87 | def self.expand_children(children, hierarchy, pids) 88 | children.each do |pid| 89 | self.expand(pid, hierarchy, pids) 90 | end 91 | end 92 | 93 | def self.expand(pid, hierarchy, pids) 94 | unless pids.include?(pid) 95 | pids << pid 96 | 97 | if children = hierarchy.fetch(pid, nil) 98 | self.expand_children(children, hierarchy, pids) 99 | end 100 | end 101 | end 102 | 103 | def self.build_tree(processes) 104 | hierarchy = Hash.new{|h,k| h[k] = []} 105 | 106 | processes.each_value do |process| 107 | if parent_process_id = process.parent_process_id 108 | hierarchy[parent_process_id] << process.process_id 109 | end 110 | end 111 | 112 | return hierarchy 113 | end 114 | 115 | def self.capture_memory(processes) 116 | count = processes.size 117 | 118 | processes.each do |pid, process| 119 | process.memory = Memory.capture(pid, count: count) 120 | end 121 | end 122 | 123 | # Capture process information. If given a `pid`, it will capture the details of that process. If given a `ppid`, it will capture the details of all child processes. Specify both `pid` and `ppid` if you want to capture a process and all its children. 124 | # 125 | # @parameter pid [Integer] The process ID to capture. 126 | # @parameter ppid [Integer] The parent process ID to capture. 127 | def self.capture(pid: nil, ppid: nil, ps: PS, memory: Memory.supported?) 128 | input, output = IO.pipe 129 | 130 | arguments = [ps] 131 | 132 | if pid && ppid.nil? 133 | arguments.push("-p", Array(pid).join(",")) 134 | else 135 | arguments.push("ax") 136 | end 137 | 138 | arguments.push("-o", FIELDS.keys.join(",")) 139 | 140 | ps_pid = Process.spawn(*arguments, out: output, pgroup: true) 141 | 142 | output.close 143 | 144 | header, *lines = input.readlines.map(&:strip) 145 | 146 | processes = {} 147 | 148 | lines.map do |line| 149 | record = FIELDS. 150 | zip(line.split(/\s+/, FIELDS.size)). 151 | map{|(key, type), value| type.call(value)} 152 | instance = self.new(*record) 153 | 154 | processes[instance.process_id] = instance 155 | end 156 | 157 | if ppid 158 | pids = Set.new 159 | 160 | hierarchy = self.build_tree(processes) 161 | 162 | self.expand_children(Array(pid), hierarchy, pids) 163 | self.expand_children(Array(ppid), hierarchy, pids) 164 | 165 | processes.select! do |pid, process| 166 | if pid != ps_pid 167 | pids.include?(pid) 168 | end 169 | end 170 | end 171 | 172 | if memory 173 | self.capture_memory(processes) 174 | end 175 | 176 | return processes 177 | ensure 178 | Process.wait(ps_pid) if ps_pid 179 | end 180 | end 181 | end 182 | end 183 | -------------------------------------------------------------------------------- /lib/process/metrics/memory.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2019-2025, by Samuel Williams. 5 | 6 | require "json" 7 | 8 | module Process 9 | module Metrics 10 | # Represents memory usage for a process, sizes are in kilobytes. 11 | class Memory < Struct.new(:map_count, :resident_size, :proportional_size, :shared_clean_size, :shared_dirty_size, :private_clean_size, :private_dirty_size, :referenced_size, :anonymous_size, :swap_size, :proportional_swap_size) 12 | 13 | alias as_json to_h 14 | 15 | # Convert the object to a JSON string. 16 | def to_json(*arguments) 17 | as_json.to_json(*arguments) 18 | end 19 | 20 | # The total size of the process in memory. 21 | def total_size 22 | self.resident_size + self.swap_size 23 | end 24 | 25 | # The unique set size, the size of completely private (unshared) data. 26 | def unique_size 27 | self.private_clean_size + self.private_dirty_size 28 | end 29 | 30 | def self.zero 31 | self.new(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) 32 | end 33 | 34 | # Whether the memory usage can be captured on this system. 35 | def self.supported? 36 | false 37 | end 38 | 39 | # Capture memory usage for the given process IDs. 40 | def self.capture(pid, **options) 41 | return nil 42 | end 43 | end 44 | end 45 | end 46 | 47 | require_relative "memory/linux" 48 | require_relative "memory/darwin" 49 | -------------------------------------------------------------------------------- /lib/process/metrics/memory/darwin.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2025, by Samuel Williams. 5 | 6 | module Process 7 | module Metrics 8 | class Memory::Darwin 9 | VMMAP = "/usr/bin/vmmap" 10 | 11 | # Whether the memory usage can be captured on this system. 12 | def self.supported? 13 | File.executable?(VMMAP) 14 | end 15 | 16 | # @returns [Numeric] Total memory size in kilobytes. 17 | def self.total_size 18 | # sysctl hw.memsize 19 | IO.popen(["sysctl", "hw.memsize"], "r") do |io| 20 | io.each_line do |line| 21 | if line =~ /hw.memsize: (\d+)/ 22 | return $1.to_i / 1024 23 | end 24 | end 25 | end 26 | end 27 | 28 | # Parse a size string into kilobytes. 29 | def self.parse_size(string) 30 | return 0 unless string 31 | 32 | case string.strip 33 | when /([\d\.]+)K/i then ($1.to_f).round 34 | when /([\d\.]+)M/i then ($1.to_f * 1024).round 35 | when /([\d\.]+)G/i then ($1.to_f * 1024 * 1024).round 36 | else (string.to_f / 1024).ceil 37 | end 38 | end 39 | 40 | LINE = /\A 41 | \s* 42 | (?.+?)\s+ 43 | (?[0-9a-fA-F]+)-(?[0-9a-fA-F]+)\s+ 44 | \[\s*(?[\d\.]+[KMG]?)\s+(?[\d\.]+[KMG]?)\s+(?[\d\.]+[KMG]?)\s+(?[\d\.]+[KMG]?)\s*\]\s+ 45 | (?[rwx\-\/]+)\s+ 46 | SM=(?\w+) 47 | /x 48 | 49 | # Capture memory usage for the given process IDs. 50 | def self.capture(pid, count: 1, **options) 51 | usage = Memory.zero 52 | 53 | IO.popen(["vmmap", pid.to_s], "r") do |io| 54 | io.each_line do |line| 55 | if match = LINE.match(line) 56 | virtual_size = parse_size(match[:virtual_size]) 57 | resident_size = parse_size(match[:resident_size]) 58 | dirty_size = parse_size(match[:dirty_size]) 59 | swap_size = parse_size(match[:swap_size]) 60 | 61 | # Update counts 62 | usage.map_count += 1 63 | usage.resident_size += resident_size 64 | usage.swap_size += swap_size 65 | 66 | # Private vs. Shared memory 67 | # COW=copy_on_write PRV=private NUL=empty ALI=aliased 68 | # SHM=shared ZER=zero_filled S/A=shared_alias 69 | case match[:sharing_mode] 70 | when "PRV" 71 | usage.private_clean_size += resident_size - dirty_size 72 | usage.private_dirty_size += dirty_size 73 | when "COW", "SHM" 74 | usage.shared_clean_size += resident_size - dirty_size 75 | usage.shared_dirty_size += dirty_size 76 | end 77 | 78 | # Anonymous memory: no region detail path or special names 79 | if match[:region_name] =~ /MALLOC|VM_ALLOCATE|Stack|STACK|anonymous/ 80 | usage.anonymous_size += resident_size 81 | end 82 | end 83 | end 84 | end 85 | 86 | # Darwin does not expose proportional memory usage, so we guess based on the number of processes. Yes, this is a terrible hack, but it's the most reasonable thing to do given the constraints: 87 | usage.proportional_size = usage.resident_size / count 88 | usage.proportional_swap_size = usage.swap_size / count 89 | 90 | return usage 91 | end 92 | end 93 | 94 | if Memory::Darwin.supported? 95 | class << Memory 96 | def supported? 97 | return true 98 | end 99 | 100 | def total_size 101 | return Memory::Darwin.total_size 102 | end 103 | 104 | def capture(...) 105 | return Memory::Darwin.capture(...) 106 | end 107 | end 108 | end 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /lib/process/metrics/memory/linux.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2025, by Samuel Williams. 5 | 6 | module Process 7 | module Metrics 8 | class Memory::Linux 9 | # @returns [Numeric] Total memory size in kilobytes. 10 | def self.total_size 11 | File.read("/proc/meminfo").each_line do |line| 12 | if /MemTotal:\s+(?\d+) kB/ =~ line 13 | return total.to_i 14 | end 15 | end 16 | end 17 | 18 | # The fields that will be extracted from the `smaps` data. 19 | SMAP = { 20 | "Rss" => :resident_size, 21 | "Pss" => :proportional_size, 22 | "Shared_Clean" => :shared_clean_size, 23 | "Shared_Dirty" => :shared_dirty_size, 24 | "Private_Clean" => :private_clean_size, 25 | "Private_Dirty" => :private_dirty_size, 26 | "Referenced" => :referenced_size, 27 | "Anonymous" => :anonymous_size, 28 | "Swap" => :swap_size, 29 | "SwapPss" => :proportional_swap_size, 30 | } 31 | 32 | if File.readable?("/proc/self/smaps_rollup") 33 | # Whether the memory usage can be captured on this system. 34 | def self.supported? 35 | true 36 | end 37 | 38 | # Capture memory usage for the given process IDs. 39 | def self.capture(pid, **options) 40 | usage = Memory.zero 41 | 42 | begin 43 | File.foreach("/proc/#{pid}/smaps_rollup") do |line| 44 | if /(?.*?):\s+(?\d+) kB/ =~ line 45 | if key = SMAP[name] 46 | usage[key] += value.to_i 47 | end 48 | end 49 | end 50 | 51 | usage.map_count += File.readlines("/proc/#{pid}/maps").size 52 | rescue Errno::ENOENT => error 53 | # Ignore. 54 | end 55 | 56 | return usage 57 | end 58 | elsif File.readable?("/proc/self/smaps") 59 | # Whether the memory usage can be captured on this system. 60 | def self.supported? 61 | true 62 | end 63 | 64 | # Capture memory usage for the given process IDs. 65 | def self.capture(pid, **options) 66 | usage = Memory.zero 67 | 68 | begin 69 | File.foreach("/proc/#{pid}/smaps") do |line| 70 | # The format of this is fixed according to: 71 | # https://github.com/torvalds/linux/blob/351c8a09b00b5c51c8f58b016fffe51f87e2d820/fs/proc/task_mmu.c#L804-L814 72 | if /(?.*?):\s+(?\d+) kB/ =~ line 73 | if key = SMAP[name] 74 | usage[key] += value.to_i 75 | end 76 | elsif /VmFlags:\s+(?.*)/ =~ line 77 | # It should be possible to extract the number of fibers and each fiber's memory usage. 78 | # flags = flags.split(/\s+/) 79 | usage.map_count += 1 80 | end 81 | end 82 | rescue Errno::ENOENT => error 83 | # Ignore. 84 | end 85 | 86 | return usage 87 | end 88 | else 89 | def self.supported? 90 | false 91 | end 92 | end 93 | end 94 | 95 | if Memory::Linux.supported? 96 | class << Memory 97 | def supported? 98 | return true 99 | end 100 | 101 | def total_size 102 | return Memory::Linux.total_size 103 | end 104 | 105 | def capture(...) 106 | return Memory::Linux.capture(...) 107 | end 108 | end 109 | end 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /lib/process/metrics/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2019-2024, by Samuel Williams. 5 | 6 | module Process 7 | module Metrics 8 | VERSION = "0.5.1" 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright, 2019-2025, by Samuel Williams. 4 | Copyright, 2024, by Adam Daniels. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /process-metrics.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "lib/process/metrics/version" 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = "process-metrics" 7 | spec.version = Process::Metrics::VERSION 8 | 9 | spec.summary = "Provide detailed OS-specific process metrics." 10 | spec.authors = ["Samuel Williams", "Adam Daniels"] 11 | spec.license = "MIT" 12 | 13 | spec.cert_chain = ["release.cert"] 14 | spec.signing_key = File.expand_path("~/.gem/release.pem") 15 | 16 | spec.homepage = "https://github.com/socketry/process-metrics" 17 | 18 | spec.metadata = { 19 | "documentation_uri" => "https://socketry.github.io/process-metrics/", 20 | "funding_uri" => "https://github.com/sponsors/ioquatix", 21 | "source_code_uri" => "https://github.com/socketry/process-metrics.git", 22 | } 23 | 24 | spec.files = Dir.glob(["{bin,lib}/**/*", "*.md"], File::FNM_DOTMATCH, base: __dir__) 25 | 26 | spec.executables = ["process-metrics"] 27 | 28 | spec.required_ruby_version = ">= 3.1" 29 | 30 | spec.add_dependency "console", "~> 1.8" 31 | spec.add_dependency "json", "~> 2" 32 | spec.add_dependency "samovar", "~> 2.1" 33 | end 34 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Process::Metrics 2 | 3 | Extract performance and memory metrics from running processes. 4 | 5 | ![Command Line Example](command-line.png) 6 | 7 | [![Development Status](https://github.com/socketry/process-metrics/workflows/Test/badge.svg)](https://github.com/socketry/process-metrics/actions?workflow=Test) 8 | 9 | ## Usage 10 | 11 | Please see the [project documentation](https://socketry.github.io/process-metrics/) for more details. 12 | 13 | - [Getting Started](https://socketry.github.io/process-metrics/guides/getting-started/index) - This guide explains how to use the `process-metrics` gem to collect and analyze process metrics including processor and memory utilization. 14 | 15 | ## Contributing 16 | 17 | We welcome contributions to this project. 18 | 19 | 1. Fork it. 20 | 2. Create your feature branch (`git checkout -b my-new-feature`). 21 | 3. Commit your changes (`git commit -am 'Add some feature'`). 22 | 4. Push to the branch (`git push origin my-new-feature`). 23 | 5. Create new Pull Request. 24 | 25 | ### Developer Certificate of Origin 26 | 27 | In order to protect users of this project, we require all contributors to comply with the [Developer Certificate of Origin](https://developercertificate.org/). This ensures that all contributions are properly licensed and attributed. 28 | 29 | ### Community Guidelines 30 | 31 | This project is best served by a collaborative and respectful environment. Treat each other professionally, respect differing viewpoints, and engage constructively. Harassment, discrimination, or harmful behavior is not tolerated. Communicate clearly, listen actively, and support one another. If any issues arise, please inform the project maintainers. 32 | -------------------------------------------------------------------------------- /release.cert: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIE2DCCA0CgAwIBAgIBATANBgkqhkiG9w0BAQsFADBhMRgwFgYDVQQDDA9zYW11 3 | ZWwud2lsbGlhbXMxHTAbBgoJkiaJk/IsZAEZFg1vcmlvbnRyYW5zZmVyMRIwEAYK 4 | CZImiZPyLGQBGRYCY28xEjAQBgoJkiaJk/IsZAEZFgJuejAeFw0yMjA4MDYwNDUz 5 | MjRaFw0zMjA4MDMwNDUzMjRaMGExGDAWBgNVBAMMD3NhbXVlbC53aWxsaWFtczEd 6 | MBsGCgmSJomT8ixkARkWDW9yaW9udHJhbnNmZXIxEjAQBgoJkiaJk/IsZAEZFgJj 7 | bzESMBAGCgmSJomT8ixkARkWAm56MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIB 8 | igKCAYEAomvSopQXQ24+9DBB6I6jxRI2auu3VVb4nOjmmHq7XWM4u3HL+pni63X2 9 | 9qZdoq9xt7H+RPbwL28LDpDNflYQXoOhoVhQ37Pjn9YDjl8/4/9xa9+NUpl9XDIW 10 | sGkaOY0eqsQm1pEWkHJr3zn/fxoKPZPfaJOglovdxf7dgsHz67Xgd/ka+Wo1YqoE 11 | e5AUKRwUuvaUaumAKgPH+4E4oiLXI4T1Ff5Q7xxv6yXvHuYtlMHhYfgNn8iiW8WN 12 | XibYXPNP7NtieSQqwR/xM6IRSoyXKuS+ZNGDPUUGk8RoiV/xvVN4LrVm9upSc0ss 13 | RZ6qwOQmXCo/lLcDUxJAgG95cPw//sI00tZan75VgsGzSWAOdjQpFM0l4dxvKwHn 14 | tUeT3ZsAgt0JnGqNm2Bkz81kG4A2hSyFZTFA8vZGhp+hz+8Q573tAR89y9YJBdYM 15 | zp0FM4zwMNEUwgfRzv1tEVVUEXmoFCyhzonUUw4nE4CFu/sE3ffhjKcXcY//qiSW 16 | xm4erY3XAgMBAAGjgZowgZcwCQYDVR0TBAIwADALBgNVHQ8EBAMCBLAwHQYDVR0O 17 | BBYEFO9t7XWuFf2SKLmuijgqR4sGDlRsMC4GA1UdEQQnMCWBI3NhbXVlbC53aWxs 18 | aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MC4GA1UdEgQnMCWBI3NhbXVlbC53aWxs 19 | aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MA0GCSqGSIb3DQEBCwUAA4IBgQB5sxkE 20 | cBsSYwK6fYpM+hA5B5yZY2+L0Z+27jF1pWGgbhPH8/FjjBLVn+VFok3CDpRqwXCl 21 | xCO40JEkKdznNy2avOMra6PFiQyOE74kCtv7P+Fdc+FhgqI5lMon6tt9rNeXmnW/ 22 | c1NaMRdxy999hmRGzUSFjozcCwxpy/LwabxtdXwXgSay4mQ32EDjqR1TixS1+smp 23 | 8C/NCWgpIfzpHGJsjvmH2wAfKtTTqB9CVKLCWEnCHyCaRVuKkrKjqhYCdmMBqCws 24 | JkxfQWC+jBVeG9ZtPhQgZpfhvh+6hMhraUYRQ6XGyvBqEUe+yo6DKIT3MtGE2+CP 25 | eX9i9ZWBydWb8/rvmwmX2kkcBbX0hZS1rcR593hGc61JR6lvkGYQ2MYskBveyaxt 26 | Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8 27 | voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg= 28 | -----END CERTIFICATE----- 29 | -------------------------------------------------------------------------------- /test/process/general.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2024-2025, by Samuel Williams. 5 | 6 | require "process/metrics" 7 | 8 | describe Process::Metrics::General do 9 | with ".capture" do 10 | let(:pid) {Process.pid} 11 | let(:capture) {Process::Metrics::General.capture(pid: pid)} 12 | 13 | it "can get memory usage for current process" do 14 | expect(capture).to be(:include?, pid) 15 | end 16 | 17 | it "can generate hash value" do 18 | expect(capture[pid].to_h).to have_keys(:process_id, :virtual_size, :resident_size, :command) 19 | end 20 | 21 | it "can generate json value" do 22 | json_string = capture[pid].to_json 23 | json = JSON.parse(json_string) 24 | 25 | expect(json).to have_keys("process_id", "total_size", "virtual_size", "resident_size", "command") 26 | end 27 | 28 | it "can extract memory usage" do 29 | expect(capture[pid].memory_usage).to be > 0.0 30 | end 31 | end 32 | 33 | with ".capture with parent pid" do 34 | def before 35 | super 36 | 37 | @pid = Process.spawn("sleep 10") 38 | end 39 | 40 | def after(error = nil) 41 | super 42 | 43 | Process.kill(:TERM, @pid) 44 | Process.wait(@pid) 45 | end 46 | 47 | let(:capture) {Process::Metrics::General.capture(pid: @pid, ppid: @pid)} 48 | 49 | it "doesn't include ps command in own output" do 50 | command = capture.each_value.find{|process| process.command.include?("ps")} 51 | 52 | expect(command).to be_nil 53 | end 54 | 55 | it "can get memory usage for parent process" do 56 | expect(capture.size).to be >= 1 57 | 58 | command = capture.each_value.find{|process| process.command.include?("sleep")} 59 | expect(command).not.to be_nil 60 | 61 | expect(command[:elapsed_time]).to be >= 0.0 62 | expect(command[:processor_time]).to be >= 0.0 63 | expect(command[:processor_utilization]).to be >= 0.0 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /test/process/memory.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2024-2025, by Samuel Williams. 5 | 6 | require "process/metrics" 7 | 8 | describe Process::Metrics::Memory do 9 | with ".total_size" do 10 | it "can get the total available memory" do 11 | expect(Process::Metrics::Memory.total_size).to be > 0 12 | end 13 | end 14 | 15 | with ".capture" do 16 | let(:pid) {Process.pid} 17 | let(:capture) {Process::Metrics::General.capture(pid: pid)} 18 | 19 | it "can get memory usage for current process" do 20 | unless memory = capture[pid].memory 21 | skip "Detailed memory information is not available on this platform!" 22 | end 23 | 24 | expect(memory).to have_attributes( 25 | map_count: be > 0, 26 | total_size: be > 0, 27 | resident_size: be > 0, 28 | proportional_size: be > 0, 29 | shared_clean_size: be > 0, 30 | shared_dirty_size: be >= 0, 31 | private_clean_size: be >= 0, 32 | private_dirty_size: be >= 0, 33 | referenced_size: be >= 0, 34 | anonymous_size: be >= 0, 35 | swap_size: be >= 0, 36 | proportional_swap_size: be >= 0 37 | ) 38 | end 39 | 40 | it "can generate json value" do 41 | unless memory = capture[pid].memory 42 | skip "Detailed memory information is not available on this platform!" 43 | end 44 | 45 | json_string = memory.to_json 46 | json = JSON.parse(json_string) 47 | 48 | expect(json).to have_keys( 49 | "map_count", "resident_size", "proportional_size", "shared_clean_size", "shared_dirty_size", "private_clean_size", "private_dirty_size", "referenced_size", "anonymous_size", "swap_size", "proportional_swap_size" 50 | ) 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /test/process/metrics.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2019-2025, by Samuel Williams. 5 | 6 | require "process/metrics" 7 | 8 | describe Process::Metrics do 9 | it "has a version number" do 10 | expect(Process::Metrics::VERSION).to be =~ /\A\d+\.\d+\.\d+\Z/ 11 | end 12 | 13 | # This format is loosely defined by the manual page. 14 | with ".duration" do 15 | it "can parse minutes and seconds" do 16 | expect(Process::Metrics.duration("00:00")).to be == 0 17 | expect(Process::Metrics.duration("00:01")).to be == 1 18 | expect(Process::Metrics.duration("01:00")).to be == 60 19 | expect(Process::Metrics.duration("01:01")).to be == 61 20 | end 21 | 22 | it "can parse hours, minutes and seconds" do 23 | expect(Process::Metrics.duration("00:00:00")).to be == 0 24 | expect(Process::Metrics.duration("00:00:01")).to be == 1 25 | expect(Process::Metrics.duration("01:00:00")).to be == 3600 26 | expect(Process::Metrics.duration("01:00:01")).to be == 3601 27 | expect(Process::Metrics.duration("01:01:01")).to be == 3661 28 | end 29 | 30 | it "can parse days, hours, minutes and seconds" do 31 | expect(Process::Metrics.duration("00-00:00:00")).to be == 0 32 | expect(Process::Metrics.duration("00-00:00:01")).to be == 1 33 | expect(Process::Metrics.duration("01-00:00:00")).to be == 86400 34 | expect(Process::Metrics.duration("01-01:01:01")).to be == (86400 + 3661) 35 | end 36 | 37 | it "can parse days, minutes and seconds" do 38 | expect(Process::Metrics.duration("00-00:00")).to be == 0 39 | expect(Process::Metrics.duration("00-00:01")).to be == 1 40 | expect(Process::Metrics.duration("01-00:00")).to be == 86400 41 | expect(Process::Metrics.duration("01-01:01")).to be == (86400 + 61) 42 | end 43 | end 44 | end 45 | --------------------------------------------------------------------------------