Created
April 15, 2016 21:09
-
-
Save YoukaiCat/dd45d4cd3c6ed9854c123c177668e4e5 to your computer and use it in GitHub Desktop.
ItrackerTest.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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