#!/usr/bin/env ruby require 'tins' include Tins::GO require 'term/ansicolor' include Term::ANSIColor class String include Term::ANSIColor end require 'hackmac' include Hackmac require 'amatch' require 'search_ui' include SearchUI $opts = go 'c:g:n:jlh', defaults: { ?s => true, ?n => 10 } def usage puts <<~end Usage: #{File.basename($0)} [OPTIONS] OPTIONS are -h this help -l output GPU performance data as list -j output GPU performance data as json -n SECONDS measure every SECONDS -g METRIC output graph for performance METRIC -c COLOR output graph in this terminal COLOR (between 0 - 255) end 0 end def ps `ioreg -l`.lines.find { |l| if l =~ /"PerformanceStatistics" = {([^}]+)}/ break Hash[ $1.split(?,).map { |a| a = a.split(?=) [ a[0][1..-2], (Integer(a[1]) rescue a[1]) ] }.sort_by { |name, _| name.downcase } ] end } end def format_bytes(n, v) if n =~ /bytes/i Tins::Unit.format(v, format: '%.3f%U', unit: '') else v.to_s end end def list(ps) max = ps.keys.max_by(&:size)&.size&.to_i puts ps.map { |n, v| ("%-#{max}s" % n) + " " + ("%s" % format_bytes(n, v)).bold } end def choose_metric(ps) case metric = $opts[?g] when nil ps_names = ps.keys metric = Search.new( match: -> answer { matcher = Amatch::PairDistance.new(answer.downcase) matches = ps_names.map { |n| [ n, -matcher.similar(n.downcase) ] }. select { |_, s| s < 0 }.sort_by(&:last).map(&:first) matches.empty? and matches = ps_names matches.first(Tins::Terminal.lines - 1) }, query: -> _answer, matches, selector { matches.each_with_index. map { |m, i| i == selector ? m.on_blue : m } * ?\n }, found: -> _answer, matches, selector { matches[selector] }, output: STDOUT ).start and return metric else ps.key?(metric) and return metric end fail "Metric #{metric} not known" end def display_graph if metric = choose_metric(ps) sleep_duration = [ 1, ($opts[?n] || 10).to_i ].max data = [] print hide_cursor at_exit do print reset, clear_screen, move_home, show_cursor end trap(:SIGWINCH) { lines, cols = Tins::Terminal.winsize print clear_screen, move_to(lines / 2, (cols - "Zzz…".size) / 2) { "Zzz…".italic } } loop do print clear_screen lines, cols = Tins::Terminal.winsize as_title = -> t { move_to(lines, (cols - t.size) / 2) { t.bold } } case value = ps[metric] when Float data << value else data << value.to_i end data = data.last(cols) y_width = (data.max - data.min).to_f if y_width == 0 print( as_title.("#{metric} / #{sleep_duration}s"), move_to(lines / 2, (cols - "no data".size) / 2) { "no data".italic } ) sleep sleep_duration print clear_screen next end unless c = $opts[?c] cs = (21..226).select { |d| Term::ANSIColor::Attribute[d].to_rgb_triple.to_hsl_triple. lightness < 40 } c = cs[ metric.bytes.reduce(0, :+) % cs.size ] end color = Term::ANSIColor::Attribute[c] color_light = color.to_rgb_triple.to_hsl_triple.lighten(15) rescue color data.each_with_index do |value, x| x = x + cols - data.size + 1 y = lines - (((value - data.min) * lines / y_width)).round + 1 y.upto(lines) do |iy| print move_to(iy, x) { ' '.on_color(y == iy ? color : color_light) } end end print( as_title.("#{metric} #{format_bytes(metric, data.last)} / #{sleep_duration}s"), move_home { format_bytes(metric, data.max).bold }, move_to_line(lines) { format_bytes(metric, data.min).bold } ) sleep sleep_duration end end rescue Interrupt end case when $opts[?h] exit usage when $opts[?l] list ps when $opts[?j] jj ps else display_graph end