aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--config.yaml51
-rwxr-xr-xgen.rb313
-rwxr-xr-xplot.py62
-rwxr-xr-xrun.sh25
5 files changed, 452 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..1fcb152
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+out
diff --git a/config.yaml b/config.yaml
new file mode 100644
index 0000000..7e5c778
--- /dev/null
+++ b/config.yaml
@@ -0,0 +1,51 @@
+---
+# log level
+log_level: "debug"
+
+# path to output directory
+out_dir: "out"
+
+# array of hosts to benchmark
+# (order determines output order)
+hosts:
+ - name: "pi0w-zero"
+ host: "zero"
+ text: "Raspberry Pi Zero W"
+ arch: "armv6l"
+ pi: true
+
+ - name: "pi3b-peach"
+ host: "peach"
+ text: "Raspberry Pi 3 Model B"
+ arch: "armv7l"
+ pi: true
+
+ - name: "pi3b+-pecan"
+ host: "pecan"
+ text: "Raspberry Pi 3 Model B+"
+ arch: "armv7l"
+ pi: true
+
+ - name: "pi4b-cherry"
+ host: "cherry"
+ text: "Raspberry Pi 4 Model B"
+ arch: "armv7l"
+ pi: true
+
+ - name: "i78650u-flex"
+ host: "flex"
+ text: "Intel i7-8650U"
+ arch: "amd64"
+ pi: false
+
+ - name: "linode"
+ host: "pmdn.org"
+ text: "Linode VM (1 Core)"
+ arch: "amd64"
+ pi: false
+
+ - name: "tr1950x-v4"
+ host: "v4.wg"
+ text: "AMD Ryzen Threadripper 1950X"
+ arch: "amd64"
+ pi: false
diff --git a/gen.rb b/gen.rb
new file mode 100755
index 0000000..67d7363
--- /dev/null
+++ b/gen.rb
@@ -0,0 +1,313 @@
+#!/usr/bin/env ruby
+
+require 'fileutils'
+require 'yaml'
+require 'csv'
+require 'logger'
+
+class AlgorithmTester
+ # block sizes
+ SIZES = %w{16 64 256 1024 8192 16384}
+
+ TESTS = [{
+ name: 'lscpu',
+ exec: %w{lscpu},
+ }, {
+ name: 'openssl',
+ exec: %w{openssl speed -mr -evp blake2b512 sha256 sha512 aes},
+ }]
+
+ CSV_COLS = {
+ all: %w{host algo size speed},
+ algo: %w{host size speed},
+ }
+
+ def self.run(app, args)
+ new(app, args).run
+ end
+
+ def initialize(app, args)
+ @log = ::Logger.new(STDERR)
+
+ # check command-line arguments
+ unless config_path = args.shift
+ raise "Usage: #{app} config.yaml"
+ end
+
+ # load config
+ @config = load_config(config_path)
+ log_level = (@config['log_level'] || 'info').upcase
+ @log.level = Logger.const_get((@config['log_level'] || 'info').upcase)
+ @log.debug { "log level = #{log_level}" }
+ end
+
+ def run
+ # create output directories
+ make_output_dirs
+
+ # connect to hosts in background, wait for all to complete
+ join(spawn_benchmarks)
+
+ # generate csvs and svgs, wait for all to complete
+ join(save(parse_data))
+ end
+
+ private
+
+ #
+ # Create output directories
+ #
+ def make_output_dirs
+ dirs = (%w{csvs svgs} + @config['hosts'].map { |row|
+ 'hosts/%s' % [row['name']]
+ }).map { |dir|
+ '%s/%s' % [out_dir, dir]
+ }
+
+ @log.debug { 'creating output dirs: %s' % [dirs.join(', ')] }
+ FileUtils.mkdir_p(dirs)
+ end
+
+ #
+ # Spawn benchmarks in background and return a list of PIDs.
+ #
+ def spawn_benchmarks
+ # connect to hosts in background
+ @config['hosts'].reduce([]) do |r, row|
+ TESTS.reduce(r) do |r, test|
+ # build absolute path to output file
+ out_path = '%s/hosts/%s/%s.txt' % [
+ out_dir,
+ row['name'],
+ test[:name],
+ ]
+
+ unless File.exists?(out_path)
+ # run command, append PID to results
+ r << bg(out_path, ssh(row['host'], test[:exec]))
+ end
+
+ r
+ end
+ end
+ end
+
+ #
+ # Parse openssl benchmark data into a map of algorithm => rows
+ #
+ def parse_data
+ @config['hosts'].reduce(Hash.new do |h, k|
+ h[k] = Hash.new do |h2, k2|
+ h2[k2] = { max: 0, rows: [] }
+ end
+ end) do |r, row|
+ # build absolute path to openssl data file
+ path = '%s/hosts/%s/openssl.txt' % [out_dir, row['name']]
+
+ # get arch
+ arch = row['pi'] ? 'arm' : 'intel'
+
+ lines = File.readlines(path).select { |line|
+ # match on result rows
+ line =~ /^\+F:/
+ }.each do |line|
+ # split to results
+ vals = line.strip.split(':')
+
+ # build algorithm name
+ algo = vals[2].gsub(/\s+/, '-')
+
+ # walk block sizes
+ SIZES.each_with_index do |size, i|
+ [{
+ algo: 'all',
+ arch: 'all',
+ }, {
+ algo: algo,
+ arch: 'all',
+ }, {
+ algo: 'all',
+ arch: arch,
+ }, {
+ algo: algo,
+ arch: arch,
+ }].each do |agg|
+ val = vals[i + 3].to_f
+ max = r[agg[:algo]][agg[:arch]][:max]
+ r[agg[:algo]][agg[:arch]][:max] = val if val > max
+
+ r[agg[:algo]][agg[:arch]][:rows] << if agg[:algo] == 'all'
+ # build row for all-*.csv
+ [row['name'], algo, size, val]
+ else
+ # row for algo-specific CSV
+ [row['name'], size, val]
+ end
+ end
+ end
+ end
+ r
+ end
+ end
+
+ #
+ # save results as CSV, generate SVGs in background, and
+ # return array of PIDs.
+ #
+ def save(data)
+ data.reduce([]) do |r, pair|
+ algo, arch_hash = pair
+
+ arch_hash.reduce(r) do |r, pair|
+ arch, arch_data = pair
+
+ # save csv
+ csv_path = save_csv(algo, arch, arch_data[:rows])
+
+ if algo != 'all'
+ max = get_max_value(data, algo, arch)
+ r << save_svg(algo, arch, max, csv_path)
+ end
+
+ # return list of pids
+ r
+ end
+ end
+ end
+
+ #
+ # save CSV of rows.
+ #
+ def save_csv(algo, arch, rows)
+ # build path to output csv
+ csv_path = '%s/csvs/%s-%s.csv' % [out_dir, algo, arch]
+
+ # write csv
+ CSV.open(csv_path, 'wb') do |csv|
+ # write column headers
+ csv << CSV_COLS[(algo == 'all') ? :all : :algo]
+
+ # write rows
+ rows.each do |row|
+ csv << row
+ end
+ end
+
+ # return csv path
+ csv_path
+ end
+
+ ARCH_TITLES = {
+ all: 'OpenSSL Speed: %s, All Systems',
+ arm: 'OpenSSL Speed: %s, Raspberry Pis Only',
+ intel: 'OpenSSL Speed: %s, Intel Only',
+ }
+
+ #
+ # Render CSV as SVG in background and return PID.
+ #
+ def save_svg(algo, arch, max, csv_path)
+ plot_path = '%s/plot.py' % [__dir__]
+ svg_path = '%s/svgs/%s-%s.svg' % [out_dir, algo, arch]
+
+ # make chart title
+ title = ARCH_TITLES[arch.intern] % [algo]
+
+ # calculate xlimit (round up to nearest 100)
+ # xlimit = ((algo =~ /^aes/) ? 400 : 2000).to_s
+ xlimit = (max / 104857600.0).ceil * 100
+
+ # build plot command
+ plot_cmd = [
+ '/usr/bin/python3',
+ plot_path,
+ csv_path,
+ svg_path,
+ title,
+ xlimit.to_s,
+ ]
+
+ # create svg in background
+ bg('/dev/null', plot_cmd)
+ end
+
+ #
+ # get maximum value depending for chart
+ #
+ def get_max_value(data, algo, arch)
+ # get aes algorithms
+ aes_algos = data.keys.select { |k| k =~ /^aes-/ }
+
+ # calculate maximum value
+ max = if arch == 'all'
+ data['all']['all'][:max]
+ elsif aes_algos.include?(algo)
+ aes_algos.map { |k|
+ data[k][arch][:max]
+ }.reduce(0) { |rm, v|
+ v > rm ? v : rm
+ }
+ else
+ (data.keys - aes_algos).map { |k|
+ data[k][arch][:max]
+ }.reduce(0) { |rm, v|
+ v > rm ? v : rm
+ }
+ end
+ end
+
+ #
+ # Load config file and check for required keys.
+ #
+ def load_config(path)
+ # read/check config
+ YAML.load_file(path).tap do |r|
+ # check for required config keys
+ missing = %w{out_dir hosts}.reject { |key| r.key?(key) }
+ raise "Missing required config keys: #{missing}" if missing.size > 0
+ end
+ end
+
+ #
+ # join set of PIDs together
+ #
+ def join(set_name, pids = [])
+ @log.debug('join') do
+ 'set = %s, pids = %s' % [set_name, pids.join(', ')]
+ end
+
+ pids.each do |pid|
+ Process.wait(pid)
+ raise "#{set_name} #{pid} failed" unless $?.success?
+ end
+ end
+
+ #
+ # Generate SSH command.
+ #
+ def ssh(host, cmd)
+ cmd = ['/usr/bin/ssh', host, *cmd]
+ cmd
+ end
+
+ #
+ # Spawn background task and return PID.
+ #
+ def bg(out_path, cmd)
+ @log.debug('bg') do
+ 'out_path = %s, cmd = %s' % [out_path, cmd.join(' ')]
+ end
+
+ spawn(*cmd, in: '/dev/null', out: out_path, err: '/dev/null')
+ end
+
+ #
+ # Get output directory.
+ #
+ def out_dir
+ @config['out_dir']
+ end
+end
+
+# allow cli invocation
+AlgorithmTester.run($0, ARGV) if __FILE__ == $0
diff --git a/plot.py b/plot.py
new file mode 100755
index 0000000..d5f6863
--- /dev/null
+++ b/plot.py
@@ -0,0 +1,62 @@
+#!/usr/bin/python3
+
+# generate chart.svg for data
+
+import csv
+import sys
+import os
+import re
+import numpy as np
+import matplotlib.pyplot as plt
+
+def read_csv(path):
+ with open(sys.argv[1], newline = '') as fh:
+ return list(reversed(list([row for row in csv.DictReader(fh)])))
+
+# check arguments
+if len(sys.argv) < 3:
+ print("Usage: {} input.csv output.svg title xlimit".format(sys.argv[0]))
+ exit(-1)
+
+# read csv
+rows = read_csv(sys.argv[1])
+
+# sort by range
+# rows.sort(key = lambda row: int(row['range']))
+
+# plot values
+plt.barh(
+ np.arange(len(rows)),
+ [float(row['speed']) / 1048576 for row in rows],
+ align = 'center',
+ alpha = 0.5,
+ tick_label = ['{} ({} bytes)'.format(
+ row['host'],
+ row['size']
+ ) for row in rows]
+)
+
+# # build title
+# algo = os.path.splitext(os.path.basename(sys.argv[1]))[0]
+# title = 'OpenSSL Speed Test: {}'.format(algo)
+
+# get title and xlimit
+title = sys.argv[3]
+limit = int(sys.argv[4])
+
+# # build xlimit
+# # if re.match('^aes', algo):
+# limit = 400
+# else:
+# limit = 2000
+
+# add label and title
+plt.yticks(fontsize = 5)
+plt.xlim(0, limit)
+# plt.xscale('log')
+plt.xlabel('Speed (MB/s)')
+plt.title(title, fontsize = 9)
+plt.tight_layout()
+
+# save image
+plt.savefig(sys.argv[2])
diff --git a/run.sh b/run.sh
new file mode 100755
index 0000000..4155efa
--- /dev/null
+++ b/run.sh
@@ -0,0 +1,25 @@
+#!/bin/sh
+
+for i in v4.wg flex cherry zero pecan peach pmdn.org; do
+ echo $i
+ mkdir -p $i
+ ssh $i lscpu > $i/lscpu.txt
+ ssh $i openssl speed -mr -evp blake2b512 sha256 sha512 aes > $i/openssl-speed.txt &
+done
+
+# join tasks
+fg
+
+# mask hostnames
+mv v4.wg v4
+mv pmdn.org linode
+
+# generate csvs in csvs/
+ruby ./gen-csvs.rb */openssl-speed.txt
+
+# generate charts in svgs/
+for i in csvs/*.csv; do
+ python3 ./plot.py "$i" "${i//csv/svg}"
+done
+
+echo done