Skip to content

Instantly share code, notes, and snippets.

@YoukaiCat
Created April 15, 2016 21:09
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save YoukaiCat/dd45d4cd3c6ed9854c123c177668e4e5 to your computer and use it in GitHub Desktop.
Save YoukaiCat/dd45d4cd3c6ed9854c123c177668e4e5 to your computer and use it in GitHub Desktop.
ItrackerTest.rb
#!/usr/bin/env ruby
# coding: utf-8
# gem install gnuplot
# Jmeter config: ${__P(threads,1)} and ${__P(rampup,0)}
module OS
require 'rbconfig'
def self.os
RbConfig::CONFIG['host_os']
end
def self.fix_path path
if os =~ /linux/
path
else
path.gsub('/', '\\')
end
end
def self.delim
if os =~ /linux/
'/'
else
'\\'
end
end
end
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Логгеры
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~
module Logger
def log str
end
end
class ConsoleLogger
include Logger
def log str
puts str
end
end
class FileLogger
include Logger
def initialize filename
@filename = filename
end
def log str
file = File.open @filename, 'a'
file.puts str
file.close
end
end
# Не работает
class GuiLogger
include Logger
def initialize tktext
@tktext = tktext
end
def log str
@tktext.insert 'end', "#{str}\n"
end
end
class ProxyLogger
include Logger
def initialize loggers
@loggers = loggers
end
def log str
@loggers.each { |logger| logger.log str }
end
end
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Отчёты
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~
module Reporter
def write_first_line min_rps, max_rps, step
end
def report results, records_count
end
end
class GnuplotReporter
include Reporter
attr_reader :filename
def initialize filename
@filename = filename
end
def write_first_line min_rps, max_rps, step
file = File.open @filename, 'a'
file.print ([0] + (min_rps..max_rps).step(step).to_a).join(' ')
file.print "\n"
file.close
end
def report results, records_count
file = File.open @filename, 'a'
file.print records_count
file.print ' '
file.print results.join(' ')
file.print "\n"
file.close
end
end
class ProxyReporter
include Reporter
def initialize reporters
@reporters = reporters
end
def write_first_line min_rps, max_rps, step
@reporters.each { |reporter| reporter.write_first_line min_rps, max_rps, step }
end
def report results, records_count
@reporters.each { |reporter| reporter.report results, records_count }
end
end
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Класс, модифицирующий конфиг генератора данных
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~
class DataGeneratorConfig
def initialize dgp_file
@dgp_file = dgp_file
end
def change_records_count records_count
xml = File.read @dgp_file
xml = xml.gsub(/<EP_ROWSTOGENERATE>\d+<\/EP_ROWSTOGENERATE>/, "<EP_ROWSTOGENERATE>#{records_count}</EP_ROWSTOGENERATE>")
File.write @dgp_file, xml
end
end
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Обёртка над исполяемым файлом генератора данных
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~
class DataGen
def initialize gen_path, logger
@gen_path = gen_path
@logger = logger
end
def run config_path
launcher = OS.os =~ /linux/ ? 'wineconsole' : ''
cmd = %[#{launcher} "#{@gen_path}" -file "#{config_path}" -ot DB]
pid = spawn cmd
Process.wait pid
end
end
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Обёртка над исполняемым файлом Jmeter
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~
class Tomcat
def initialize startup, shutdown, logger
@startup = startup
@shutdown = shutdown
@logger = logger
end
def restart
stop
start
end
def start
cmd = %["#{@startup}"]
@logger.log "Запуск tomcat #{cmd}"
output = `#{cmd}`
@logger.log output
end
def stop
cmd = %["#{@shutdown}"] #%[pkill tomcat]
@logger.log "Остановка tomcat #{cmd}"
output = `#{cmd}`
@logger.log output
end
end
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Обёртка над исполняемым файлом Jmeter
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~
class Jmeter
def initialize jmeter_path, jmx_path, logger
@jmeter = jmeter_path
@jmx = jmx_path
@logger = logger
end
def run threads_count, rampup_period
cmd = %["#{@jmeter}" -n -t "#{@jmx}" -Jthreads=#{threads_count} -Jrampup=#{rampup_period}]
@logger.log cmd
jmeter_output = `#{cmd}` #"Generate Summary Results = 40 in 30s = 1.3/s Avg: 8611 Min: 2450 Max: 15114 Err: 0 (0.00%)"
#@logger.log jmeter_output
output = parse_output jmeter_output
@logger.log "RPS: #{output[:rps]}, AVG: #{output[:avg]}, ERR: #{output[:err]} \n"
get_avg output
end
def parse_output str
report = str[/Generate Summary Results = .*/]
arr = report.split
{ :rps => arr[8], :avg => arr[10].to_i, :err => arr[17][/(\d+.\d+)/].to_i }
end
def get_avg output
avg = output[:avg]
errors = output[:err]
(avg < 30_000 && errors < 30) ? avg : 0
end
end
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Класс, последовательно запускающий jmeter на увеличивающимся RPS
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~
class LoadTester
def initialize jmeter, ramp_up_period
@jmeter = jmeter
@ramp_up_period = ramp_up_period
end
def run min_rps, max_rps, step
results = []
last = 1
(min_rps..max_rps).step(step) do |rps|
if last > 0
threads_count = calculate_number_of_threads @ramp_up_period, rps
avg = @jmeter.run threads_count, @ramp_up_period
results << avg
else
results << 0
end
last = results.last
end
results
end
def calculate_number_of_threads ramp_up_period, rps
ramp_up_period * rps
end
end
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Генератор 3D диаграммы
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~
class PlotGenerator
require 'gnuplot'
def initialize gnuplot_reporter, png_path
@gnuplot_data_path = gnuplot_reporter.filename
@png_path = png_path
end
def run
Gnuplot.open do |gp|
Gnuplot::Plot.new( gp ) do |plot|
plot.title 'Itracker'
plot.xlabel 'Запросы в секунду'
plot.ylabel 'Размер БД (записи)'
plot.zlabel 'Отклик (мс)'
plot.arbitrary_lines << "splot '#{@gnuplot_data_path}' matrix nonuniform with pm3d"
plot.output @png_path
plot.terminal 'png size 1280,1024'
end
end
end
end
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Класс, последовательно запускающий генератор данных, Jmeter и генератор диаграмм
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~
class ItrackerTest
def initialize settings
@settings = settings
end
def run
validate_settings
fix_paths
results_path = unique_dir
logger = ProxyLogger.new [ConsoleLogger.new]
generator_config = DataGeneratorConfig.new @settings[:dgp_file]
generator = DataGen.new @settings[:gen_path], logger
jmeter = Jmeter.new @settings[:jmeter_path], @settings[:jmx_path], logger
ramp_up_period = @settings[:ramp_up_period].to_i
tester = LoadTester.new jmeter, ramp_up_period
volovichreporter = GnuplotReporter.new results_path + OS.delim + 'report.txt'
reporter = ProxyReporter.new [volovichreporter]
min_records = @settings[:min_records].to_i
max_records = @settings[:max_records].to_i
step_records = @settings[:step_records].to_i
min_rps = @settings[:min_rps].to_i
max_rps = @settings[:max_rps].to_i
step_rps = @settings[:step_rps].to_i
reporter.write_first_line min_rps, max_rps, step_rps
logger.log "Поехали!"
(min_records..max_records).step(step_records) do |records|
logger.log "\n~~~~~~~~~~~~ Generate #{records} records ~~~~~~~~~~~~\n\n"
generator_config.change_records_count records
generator.run @settings[:dgp_file]
logger.log "\n~~~~~~~~~~~~ Run on #{records} records ~~~~~~~~~~~~\n\n"
results = tester.run min_rps, max_rps, step_rps
reporter.report results, records
if results.first == 0
logger.log "Время отклика при первом запуске на предыдущем значении количества записей превышает допустимые пределы. Оптимизирую."
break
end
end
logger.log "Генерация диаграмм..."
PlotGenerator.new(volovichreporter, results_path.gsub('\\', '/') + '/plot.png').run
logger.log "Завершено."
end
def validate_settings
if OS.os =~ /linux/ #Потому что wine
fail "Неверно задан исполняемый файл генератора" unless File.exists? @settings[:gen_path]
else
fail "Неверно задан исполняемый файл генератора" unless File.executable? @settings[:gen_path]
end
fail "Неверно задан файл конфигурации генератора" unless File.exists? @settings[:dgp_file]
fail "Неверно задан исполняемый файл jmeter" unless File.executable? @settings[:jmeter_path]
fail "Неверно задан файл конфигурации jmeter" unless File.exists? @settings[:jmx_path]
end
def fix_paths
@settings[:gen_path] = OS.fix_path @settings[:gen_path]
@settings[:dgp_file] = OS.fix_path @settings[:dgp_file]
@settings[:jmeter_path] = OS.fix_path @settings[:jmeter_path]
@settings[:jmx_path] = OS.fix_path @settings[:jmx_path]
@settings[:results_path] = OS.fix_path @settings[:results_path]
end
def unique_dir
results_path = @settings[:results_path] + OS.delim + Time.now.getutc.to_i.to_s
Dir.mkdir results_path
results_path
end
end
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~
settings = {
gen_path: "C:/dg6.exe",
dgp_file: "C:/datagen.dgp",
min_records: '1',
max_records: '5',
step_records: '1',
jmeter_path: "C:/Jmeter.bat",
jmx_path: "C:/itracker.jmx",
ramp_up_period: '5',
min_rps: '1',
max_rps: '5',
step_rps: '1',
results_path: Dir.pwd
}
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# GUI
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~
require 'tk'
require 'json'
$gen_path_entry = nil
$dgp_file_entry = nil
$min_records_entry = nil
$max_records_entry = nil
$step_records_entry = nil
$jmeter_path_entry = nil
$jmx_path_entry = nil
$ramp_up_period_entry = nil
$min_rps_entry = nil
$max_rps_entry = nil
$step_rps_entry = nil
$results_path_entry = nil
def save_to_settings settings
settings[:min_records] = $min_records_entry.textvariable.value
settings[:max_records] = $max_records_entry.textvariable.value
settings[:step_records] = $step_records_entry.textvariable.value
settings[:ramp_up_period] = $ramp_up_period_entry.textvariable.value
settings[:min_rps] = $min_rps_entry.textvariable.value
settings[:max_rps] = $max_rps_entry.textvariable.value
settings[:step_rps] = $step_rps_entry.textvariable.value
settings[:gen_path] = $gen_path_entry.textvariable.value
settings[:dgp_file] = $dgp_file_entry.textvariable.value
settings[:jmeter_path] = $jmeter_path_entry.textvariable.value
settings[:jmx_path] = $jmx_path_entry.textvariable.value
settings[:results_path] = $results_path_entry.textvariable.value
end
def load_from_settings settings
$min_records_entry.textvariable.value = settings[:min_records]
$max_records_entry.textvariable.value = settings[:max_records]
$step_records_entry.textvariable.value = settings[:step_records]
$ramp_up_period_entry.textvariable.value = settings[:ramp_up_period]
$min_rps_entry.textvariable.value = settings[:min_rps]
$max_rps_entry.textvariable.value = settings[:max_rps]
$step_rps_entry.textvariable.value = settings[:step_rps]
$gen_path_entry.textvariable.value = settings[:gen_path]
$dgp_file_entry.textvariable.value = settings[:dgp_file]
$jmeter_path_entry.textvariable.value = settings[:jmeter_path]
$jmx_path_entry.textvariable.value = settings[:jmx_path]
$results_path_entry.textvariable.value = settings[:results_path]
end
TkRoot.new do |root|
title 'ItrackerTest'
Tk::Tile::Notebook.new(root) do |notebook|
frame1 = TkFrame.new(notebook) do |frame1|
TkFrame.new(frame1) do |f|
TkLabel.new(f) do
text = TkVariable.new
text.value = 'Исполняемый файл консольного генератора:'
textvariable text
pack side: :left
end
$gen_path_entry = TkEntry.new(f) do
text = TkVariable.new
text.value = settings[:gen_path]
textvariable text
state :disabled
pack side: :left
end
TkButton.new(f) do
comman Proc.new {
filename = Tk.getOpenFile filetypes: [['Datagen console executable', '*.exe']]
if File.exists? filename
$gen_path_entry.textvariable.value = filename
settings[:gen_path] = filename
end
}
text 'Выбрать'
pack side: :left
end
pack fill: :y
end
TkFrame.new(frame1) do |f|
TkLabel.new(f) do
text = TkVariable.new
text.value = 'Путь до dgp файла:'
textvariable text
pack side: :left
end
$dgp_file_entry = TkEntry.new(f) do
text = TkVariable.new
text.value = settings[:dgp_file]
textvariable text
state :disabled
pack side: :left
end
TkButton.new(f) do
comman Proc.new {
filename = Tk.getOpenFile filetypes: [['Datagen config', '*.dgp']]
if File.exists? filename
$dgp_file_entry.textvariable.value = filename
settings[:dgp_file] = filename
end
}
text 'Выбрать'
pack side: :left
end
pack fill: :y
end
TkFrame.new(frame1) do |f|
TkLabel.new(f) do
text = TkVariable.new
text.value = 'Минимально записей:'
textvariable text
pack side: :left
end
$min_records_entry = TkEntry.new(f) do
text = TkVariable.new
text.value = settings[:min_records]
textvariable text
pack side: :left
end
pack fill: :y
end
TkFrame.new(frame1) do |f|
TkLabel.new(f) do
text = TkVariable.new
text.value = 'Максимально записей:'
textvariable text
pack side: :left
end
$max_records_entry = TkEntry.new(f) do
text = TkVariable.new
text.value = settings[:max_records]
textvariable text
pack side: :left
end
pack fill: :y
end
TkFrame.new(frame1) do |f|
TkLabel.new(f) do
text = TkVariable.new
text.value = 'Шаг увеличения записей:'
textvariable text
pack side: :left
end
$step_records_entry = TkEntry.new(f) do
text = TkVariable.new
text.value = settings[:step_records]
textvariable text
pack side: :left
end
pack fill: :y
end
end
frame2 = TkFrame.new(notebook) do |frame2|
TkFrame.new(frame2) do |f|
TkLabel.new(f) do
text = TkVariable.new
text.value = 'Исполняемый файл Jmeter:'
textvariable text
pack side: :left
end
$jmeter_path_entry = TkEntry.new(f) do
text = TkVariable.new
text.value = settings[:jmeter_path]
textvariable text
state :disabled
pack side: :left
end
TkButton.new(f) do
comman Proc.new {
filename = Tk.getOpenFile filetypes: [['Executable', '*.bat *.exe *.sh']]
if File.exists? filename
$jmeter_path_entry.textvariable.value = filename
settings[:jmeter_path] = filename
end
}
text 'Выбрать'
pack side: :left
end
pack fill: :y
end
TkFrame.new(frame2) do |f|
TkLabel.new(f) do
text = TkVariable.new
text.value = 'Путь до файла конфигурации Jmeter (jmx):'
textvariable text
pack side: :left
end
$jmx_path_entry = TkEntry.new(f) do
text = TkVariable.new
text.value = settings[:jmx_path]
textvariable text
state :disabled
pack side: :left
end
TkButton.new(f) do
comman Proc.new {
filename = Tk.getOpenFile filetypes: [['Jmeter config', '*.jmx']]
if File.exists? filename
$jmx_path_entry.textvariable.value = filename
settings[:jmx_path] = filename
end
}
text 'Выбрать'
pack side: :left
end
pack fill: :y
end
TkFrame.new(frame2) do |f|
TkLabel.new(f) do
text = TkVariable.new
text.value = 'Ramp up period (sec):'
textvariable text
pack side: :left
end
$ramp_up_period_entry = TkEntry.new(f) do
text = TkVariable.new
text.value = settings[:ramp_up_period]
textvariable text
pack side: :left
end
pack fill: :y
end
TkFrame.new(frame2) do |f|
TkLabel.new(f) do
text = TkVariable.new
text.value = 'Минимальный RPS:'
textvariable text
pack side: :left
end
$min_rps_entry = TkEntry.new(f) do
text = TkVariable.new
text.value = settings[:min_rps]
textvariable text
pack side: :left
end
pack fill: :y
end
TkFrame.new(frame2) do |f|
TkLabel.new(f) do
text = TkVariable.new
text.value = 'Максимальный RPS:'
textvariable text
pack side: :left
end
$max_rps_entry = TkEntry.new(f) do
text = TkVariable.new
text.value = settings[:max_rps]
textvariable text
pack side: :left
end
pack fill: :y
end
TkFrame.new(frame2) do |f|
TkLabel.new(f) do
text = TkVariable.new
text.value = 'Шаг RPS:'
textvariable text
pack side: :left
end
$step_rps_entry = TkEntry.new(f) do
text = TkVariable.new
text.value = settings[:step_rps]
textvariable text
pack side: :left
end
pack fill: :y
end
end
frame3 = TkFrame.new(notebook) do |frame3|
TkFrame.new(frame3) do |f|
TkLabel.new(f) do
text = TkVariable.new
text.value = 'Сохранять логи, отчёты и графики в:'
textvariable text
pack side: :left
end
$results_path_entry = TkEntry.new(f) do
text = TkVariable.new
text.value = settings[:results_path]
textvariable text
state :disabled
pack side: :left
end
TkButton.new(f) do
comman Proc.new {
dir = Tk.chooseDirectory
if Dir.exists? dir
$results_path_entry.textvariable.value = dir
settings[:results_path] = dir
end
}
text 'Выбрать'
pack side: :left
end
pack fill: :y
end
end
add frame1, text: 'Генераторация записей'
add frame2, text: 'Нагрузочное тестирование'
add frame3, text: 'Результаты работы'
pack
end
TkButton.new(root) do
comman Proc.new {
save_to_settings settings
ItrackerTest.new(settings).run
}
text 'Запустить'
pack side: :left
end
TkButton.new(root) do
comman Proc.new {
filename = Tk.getSaveFile initialfile: 'settings.json', filetypes: [['Settings files', '*.json']]
unless filename.empty?
save_to_settings settings
File.open(filename, 'w') { |f| f.write(JSON.pretty_generate(settings)) }
end
}
text 'Сохранить настройки'
pack side: :left
end
TkButton.new(root) do
comman Proc.new {
filename = Tk.getOpenFile initialfile: 'settings.json', filetypes: [['Settings files', '*.json']]
if File.exists? filename
json = File.new(filename).readlines.join
settings = JSON.parse json, symbolize_names: true
load_from_settings settings
end
}
text 'Загрузить настройки'
pack side: :left
end
end
# Comment first line and uncomment second to run without gui
Tk.mainloop
# ItrackerTest.new(settings).run
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment