require 'optparse' require 'json' require 'tempfile' require 'tmpdir' module PerfMonger module Command class PlotCommand < BaseCommand register_command 'plot', "Plot system performance graphs collected by 'record'" def initialize @parser = OptionParser.new @parser.banner = </dev/null 2>&1') puts("ERROR: convert(1) not found.") puts("ERROR: ImageMagick is required for #{typ}") puts(@parser.help) exit(false) end @output_type = typ end @parser.on('-p', '--prefix PREFIX', 'Output file name prefix.') do |prefix| if ! (prefix =~ /-\Z/) prefix += '-' end @output_prefix = prefix end @parser.on('-s', '--save', 'Save GNUPLOT and data files.') do @save_gpfiles = true end @parser.on('--disk-only REGEX', "Select disk devices that matches REGEX") do |regex| @disk_only = regex @disk_only_regex = Regexp.compile(regex) end @parser.on('--plot-read-only', "Plot only READ performance for disks") do @disk_plot_read = true @disk_plot_write = false end @parser.on('--plot-write-only', "Plot only WRITE performance for disks") do @disk_plot_read = false @disk_plot_write = true end @parser.on('--plot-read-write', "Plot READ and WRITE performance for disks") do @disk_plot_read = true @disk_plot_write = true end @parser.on('--plot-numkey-threshold NUM', "Legends of per-disk plots are turned off if the number of disks is larger than this value.") do |num| @disk_numkey_threshold = num.to_i end @parser.on('--plot-iops-max IOPS', "Maximum of IOPS plot range (default: auto)") do |iops| @plot_iops_max = iops.to_f end @parser.parse!(argv) if argv.size == 0 puts("ERROR: PerfMonger log file is required") puts(@parser.help) exit(false) end @data_file = File.expand_path(argv.shift) end def run(argv) unless system('which gnuplot >/dev/null 2>&1') puts("ERROR: gnuplot not found") puts(@parser.help) exit(false) end parse_args(argv) unless system('gnuplot -e "set terminal" < /dev/null 2>&1 | grep pdfcairo >/dev/null 2>&1') puts("ERROR: pdfcairo is not supported by installed gnuplot") puts("ERROR: PerfMonger requires pdfcairo-supported gnuplot") puts(@parser.help) exit(false) end formatter_bin = ::PerfMonger::Command::CoreFinder.plot_formatter() @tmpdir = Dir.mktmpdir @disk_dat = File.expand_path("disk.dat", @tmpdir) @cpu_dat = File.expand_path("cpu.dat", @tmpdir) @mem_dat = File.expand_path("mem.dat", @tmpdir) meta_json = nil cmd = [formatter_bin, "-perfmonger", @data_file, "-cpufile", @cpu_dat, "-diskfile", @disk_dat, "-memfile", @mem_dat] if @disk_only_regex cmd << "-disk-only" cmd << @disk_only end IO.popen(cmd, "r") do |io| meta_json = io.read end if $?.exitstatus != 0 puts("ERROR: failed to run perfmonger-plot-formatter") exit(false) end meta = JSON.parse(meta_json) plot_disk(meta) plot_cpu(meta) plot_mem(meta) FileUtils.rm_rf(@tmpdir) true end private def plot_disk(meta) iops_pdf_filename = @output_prefix + 'disk-iops.pdf' transfer_pdf_filename = @output_prefix + 'disk-transfer.pdf' total_iops_pdf_filename = @output_prefix + 'disk-total-iops.pdf' total_transfer_pdf_filename = @output_prefix + 'disk-total-transfer.pdf' gp_filename = @output_prefix + 'disk.gp' dat_filename = @output_prefix + 'disk.dat' if @output_type != 'pdf' iops_img_filename = @output_prefix + 'disk-iops.' + @output_type transfer_img_filename = @output_prefix + 'disk-transfer.' + @output_type total_iops_img_filename = @output_prefix + 'disk-total-iops.' + @output_type total_transfer_img_filename = @output_prefix + 'disk-total-transfer.' + @output_type else iops_img_filename = nil transfer_img_filename = nil total_iops_img_filename = nil total_transfer_img_filename = nil end start_time = meta["start_time"] end_time = meta["end_time"] Dir.chdir(@tmpdir) do gpfile = File.new(gp_filename, 'w') total_iops_plot_stmt_list = [] iops_plot_stmt_list = meta["disk"]["devices"].map do |dev_entry| devname = dev_entry["name"] idx = dev_entry["idx"] if devname == "total" if @disk_plot_read total_iops_plot_stmt_list.push("\"disk.dat\" ind #{idx} usi 1:2 with lines lw 2 title \"#{devname} read\"") end if @disk_plot_write total_iops_plot_stmt_list.push("\"disk.dat\" ind #{idx} usi 1:3 with lines lw 2 title \"#{devname} write\"") end [] elsif @disk_only_regex && !(devname =~ @disk_only_regex) [] else plot_stmt = [] if @disk_plot_read plot_stmt.push("\"disk.dat\" ind #{idx} usi 1:2 with lines lw 2 title \"#{devname} read\"") end if @disk_plot_write plot_stmt.push("\"disk.dat\" ind #{idx} usi 1:3 with lines lw 2 title \"#{devname} write\"") end plot_stmt end end.flatten total_transfer_plot_stmt_list = [] transfer_plot_stmt_list = meta["disk"]["devices"].map do |dev_entry| devname = dev_entry["name"] idx = dev_entry["idx"] if devname == "total" if @disk_plot_read total_transfer_plot_stmt_list.push("\"disk.dat\" ind #{idx} usi 1:4 with lines lw 2 title \"#{devname} read\"") end if @disk_plot_write total_transfer_plot_stmt_list.push("\"disk.dat\" ind #{idx} usi 1:5 with lines lw 2 title \"#{devname} write\"") end [] elsif @disk_only_regex && !(devname =~ @disk_only_regex) [] else plot_stmt = [] if @disk_plot_read plot_stmt.push("\"disk.dat\" ind #{idx} usi 1:4 with lines lw 2 title \"#{devname} read\"") end if @disk_plot_write plot_stmt.push("\"disk.dat\" ind #{idx} usi 1:5 with lines lw 2 title \"#{devname} write\"") end plot_stmt end end.flatten if iops_plot_stmt_list.size == 0 puts("No plot target disk devices.") return end num_dev = meta["disk"]["devices"].select do |dev_entry| dev_entry["name"] != "total" end.size if num_dev > @disk_numkey_threshold set_key_stmt = "unset key" else set_key_stmt = "set key below center" end iops_yrange = "set yrange [0:*]" if @plot_iops_max iops_yrange = "set yrange [0:#{@plot_iops_max}]" end gpfile.puts < GB # "elapsed_time", // 1 # "mem_total", // 2 # "mem_used", // 3 # "mem_free", // 4 # "buffers", // 5 # "cached", // 6 # "swap_cached", // 7 # "active", // 8 # "inactive", // 9 # "swap_total", // 10 # "swap_free", // 11 # "dirty", // 12 # "writeback", // 13 # "anon_pages", // 14 # "mapped", // 15 # "shmem", // 16 # "slab", // 17 # "s_reclaimable", // 18 # "s_unreclaim", // 19 # "kernel_stack", // 20 # "page_tables", // 21 # "nfs_unstable", // 22 # "bounce", // 23 # "commit_limit", // 24 # "committed_as", // 25 # "anon_huge_pages", // 26 # "huge_pages_total", // 27 # "huge_pages_free", // 28 # "huge_pages_rsvd", // 29 # "huge_pages_surp"}, // 30 # "hugepagesize"}, // 31 Dir.chdir(@tmpdir) do total = `tail -n+2 #{dat_filename}|head -n1`.split[1].to_f if total == 0.0 raise RuntimeError.new("Failed to get MemTotal value from mem.dat file: #{dat_filename}") end gpfile = File.open(gp_filename, 'w') pdf_file = File.join(@output_dir, "mem.pdf") gpfile.puts <