add mixed load test, other fixes
diff --git a/TODO b/TODO
index 042c477..56a20a3 100644
--- a/TODO
+++ b/TODO
@@ -1,51 +1,37 @@
-посмотреть настройки qemu
+2.0:
+	* Перейти на анализ логов fio
+	* Занести интервал усреднения в конфиг
+	* Делать один больщой тест на несколько минут и мерять по нему все параметры
+	* починить SW & HW info, добавить настройки qemu и все такое
+	* Все тесты - в один поток
+	* Перед началом теста проверять наличие его результатов и скипать
+	* fio --client????
+	* собрать новый fio под основные платформы и положить в git
+	* продолжение работы при большинстве ошибок
+	* Починить процессор
+	* Починить боттлнеки
+	* Юнит-тесты
+	* Сравнения билдов - пока по папкам из CLI, тектовое
+	* Make python module
+	* putget/ssbench tests
+	* rbd с нод без виртуалок
+	* отдельный тенант на все и очистка полная
+	* Per-vm stats & between vm dev
+	* Логи визуальные
+	* psql, mssql, SPC-1
+	* Тестирование кешей
 
-рестарт fio при ошибке
-дамп промежуточных данных и воосстановление
-печатать fio параметры
+Мелочи:
+	* печатать fio параметры в лог
+	* Зарефакторить запуск/мониторинг/оставнов процесса по SSH, запуск в фоне с чеком - в отдельную ф-цию
+	* prefill запускать в фоне и чекать периодически
+	* починить все подвисания во всех потоках - дампить стеки при подвисании и таймаут
+	* При убивании - грохать все удаленные процессы. Хранить машины и пиды в конфиге и в файле
+	* fadvise_hint=0
+	* Изменить с репорте сенсоров все на %
+	* посмотреть что с сетевыми картами
+	* Intellectual granular sensors
 
-Зарефакторить запуск/мониторинг/оставнов процесса по SSH
-fio --client
-собрать новый fio под основные платформы и положить в git
-
-запуск в фоне с чеком - в отдельную ф-цию
-prefill запускать в фоне и чекать периодически
-починить все подвисания во всех потоках - дампить стеки при подвисании
-
-
-
-v2 - Однопоточная версия, обработка и продолжение работы при большинстве ошибок
-
-
-
-Finding bottlenecks (алена) - починить процессор
-fadvise_hint=0
-Изменить с репорте сенсоров все на %
-посмотреть что с сетевыми картами
-Resource consumption:
-	добавить процессор,
-	добавить время в IO,
-	генерировать репорты по всему
-
-Lab software report
-
-
-Интеграционные/функциональные тесты *
-Инфо по лабе * - заковырять в certification (Глеб)
-Сравнения билдов (пока по папкам из CLI)
-Make python module
-putget/ssbench tests (костя)
-тестирование (костя)
-отдельный тенант на все
-Per-vm stats & between vm dev
-+ собрать новый fio статиком
- 
-
-Intellectual granular sensors
-
-
-Отчеты
-Добавить к отчету экстраполированные скорости
 Стат-обработка:
 	расчет async
 	расчет количества измерений
diff --git a/scripts/hdd.fio b/scripts/hdd.fio
index c682380..33b05c0 100644
--- a/scripts/hdd.fio
+++ b/scripts/hdd.fio
@@ -5,13 +5,16 @@
 buffered=0
 iodepth=1
 softrandommap=1
-filename=/var/lib/nova/instances/b9fa1a9f-7d43-4cc6-b6cc-133c2a84ab41/xxx.bin
+filename=/media/data/xxx.bin
+# filename=/tmp/xxx.bin
 randrepeat=0
 size=10G
 ramp_time=5
-runtime=10
+runtime=15
 blocksize=4k
 rw=randwrite
 sync=1
-numjobs=1
+direct=1
+thread=1
+numjobs=50
 
diff --git a/wally/pretty_yaml.py b/wally/pretty_yaml.py
index 699af7e..2cb5607 100644
--- a/wally/pretty_yaml.py
+++ b/wally/pretty_yaml.py
@@ -9,8 +9,13 @@
         if isinstance(val, unicode):
             val = val.encode('utf8')
 
-        if len(bad_symbols & set(val)) != 0:
-            return repr(val)
+        try:
+            float(val)
+            val = repr(val)
+        except ValueError:
+            if len(bad_symbols & set(val)) != 0:
+                val = repr(val)
+
         return val
     elif val is True:
         return 'true'
diff --git a/wally/report.py b/wally/report.py
index 2aa5338..e893c25 100644
--- a/wally/report.py
+++ b/wally/report.py
@@ -128,14 +128,13 @@
             num_res += len(result.raw_result['jobs'])
             for job_info in result.raw_result['jobs']:
                 for k, v in job_info['latency_ms'].items():
-                    if isinstance(k, str):
-                        assert k[:2] == '>='
+                    if isinstance(k, basestring) and k.startswith('>='):
                         lat_mks[int(k[2:]) * 1000] += v
                     else:
-                        lat_mks[k * 1000] += v
+                        lat_mks[int(k) * 1000] += v
 
                 for k, v in job_info['latency_us'].items():
-                    lat_mks[k] += v
+                    lat_mks[int(k)] += v
 
         for k, v in lat_mks.items():
             lat_mks[k] = float(v) / num_res
@@ -675,6 +674,68 @@
     return render_all_html(comment, di, lab_info, images, "report_ceph.html")
 
 
+@report('mixed', 'mixed')
+def make_mixed_report(processed_results, lab_info, comment):
+    #
+    # IOPS(X% read) = 100 / ( X / IOPS_W + (100 - X) / IOPS_R )
+    #
+    is_ssd = True
+    mixed = collections.defaultdict(lambda: [])
+    for res in processed_results.values():
+        if res.name.startswith('mixed'):
+            if res.name.startswith('mixed-ssd'):
+                is_ssd = True
+            mixed[res.concurence].append((res.p.rwmixread,
+                                          res.lat.average / 1000.0,
+                                          res.lat.deviation / 1000.0,
+                                          res.iops.average,
+                                          res.iops.deviation))
+
+    if len(mixed) == 0:
+        raise ValueError("No mixed load found")
+
+    fig, p1 = plt.subplots()
+    p2 = p1.twinx()
+
+    colors = ['red', 'green', 'blue', 'orange', 'magenta', "teal"]
+    colors_it = iter(colors)
+    for conc, mix_lat_iops in sorted(mixed.items()):
+        mix_lat_iops = sorted(mix_lat_iops)
+        read_perc, lat, dev, iops, iops_dev = zip(*mix_lat_iops)
+        p1.errorbar(read_perc, iops, color=next(colors_it),
+                    yerr=iops_dev, label=str(conc) + " th")
+
+        p2.errorbar(read_perc, lat, color=next(colors_it),
+                    ls='--', yerr=dev, label=str(conc) + " th lat")
+
+    if is_ssd:
+        p1.set_yscale('log')
+        p2.set_yscale('log')
+
+    p1.set_xlim(-5, 105)
+
+    read_perc = set(read_perc)
+    read_perc.add(0)
+    read_perc.add(100)
+    read_perc = sorted(read_perc)
+
+    plt.xticks(read_perc, map(str, read_perc))
+
+    p1.grid(True)
+    p1.set_xlabel("% of reads")
+    p1.set_ylabel("Mixed IOPS")
+    p2.set_ylabel("Latency, ms")
+
+    handles1, labels1 = p1.get_legend_handles_labels()
+    handles2, labels2 = p2.get_legend_handles_labels()
+    plt.subplots_adjust(top=0.85)
+    plt.legend(handles1 + handles2, labels1 + labels2,
+               bbox_to_anchor=(0.5, 1.15),
+               loc='upper center',
+               prop={'size': 12}, ncol=3)
+    plt.show()
+
+
 def make_load_report(idx, results_dir, fname):
     dpath = os.path.join(results_dir, "io_" + str(idx))
     files = sorted(os.listdir(dpath))
@@ -751,13 +812,16 @@
                     logger.exception("Diring {0} report generation".format(name))
                     continue
 
-                try:
-                    with open(hpath, "w") as fd:
-                        fd.write(report)
-                except:
-                    logger.exception("Diring saving {0} report".format(name))
-                    continue
-                logger.info("Report {0} saved into {1}".format(name, hpath))
+                if report is not None:
+                    try:
+                        with open(hpath, "w") as fd:
+                            fd.write(report)
+                    except:
+                        logger.exception("Diring saving {0} report".format(name))
+                        continue
+                    logger.info("Report {0} saved into {1}".format(name, hpath))
+                else:
+                    logger.warning("No report produced by {0!r}".format(name))
 
         if not found:
             logger.warning("No report generator found for this load")
diff --git a/wally/run_test.py b/wally/run_test.py
index d4fb911..359d917 100755
--- a/wally/run_test.py
+++ b/wally/run_test.py
@@ -749,10 +749,11 @@
 
 
 def get_stage_name(func):
-    if func.__name__.endswith("stage"):
-        return func.__name__
+    nm = get_func_name(func)
+    if nm.endswith("stage"):
+        return nm
     else:
-        return func.__name__ + " stage"
+        return nm + " stage"
 
 
 def get_test_names(raw_res):
@@ -798,6 +799,31 @@
     print(tab.draw())
 
 
+def get_func_name(obj):
+    if hasattr(obj, '__name__'):
+        return obj.__name__
+    if hasattr(obj, 'func_name'):
+        return obj.func_name
+    return obj.func.func_name
+
+
+@contextlib.contextmanager
+def log_stage(func):
+    msg_templ = "Exception during {0}: {1!s}"
+    msg_templ_no_exc = "During {0}"
+
+    logger.info("Start " + get_stage_name(func))
+
+    try:
+        yield
+    except utils.StopTestError as exc:
+        logger.error(msg_templ.format(
+            get_func_name(func), exc))
+    except Exception:
+        logger.exception(msg_templ_no_exc.format(
+            get_func_name(func)))
+
+
 def main(argv):
     if faulthandler is not None:
         faulthandler.register(signal.SIGUSR1, all_threads=True)
@@ -876,42 +902,29 @@
     if cfg_dict.get('run_web_ui', False):
         start_web_ui(cfg_dict, ctx)
 
-    msg_templ = "Exception during {0.__name__}: {1!s}"
-    msg_templ_no_exc = "During {0.__name__}"
-
-    try:
-        for stage in stages:
+    for stage in stages:
+        ok = False
+        with log_stage(stage):
             logger.info("Start " + get_stage_name(stage))
             stage(cfg_dict, ctx)
-    except utils.StopTestError as exc:
-        logger.error(msg_templ.format(stage, exc))
-    except Exception:
-        logger.exception(msg_templ_no_exc.format(stage))
-    finally:
-        exc, cls, tb = sys.exc_info()
-        for stage in ctx.clear_calls_stack[::-1]:
-            try:
-                logger.info("Start " + get_stage_name(stage))
-                stage(cfg_dict, ctx)
-            except utils.StopTestError as cleanup_exc:
-                logger.error(msg_templ.format(stage, cleanup_exc))
-            except Exception:
-                logger.exception(msg_templ_no_exc.format(stage))
+            ok = True
+        if not ok:
+            break
 
-        logger.debug("Start utils.cleanup")
-        for clean_func, args, kwargs in utils.iter_clean_func():
-            try:
-                logger.info("Start " + get_stage_name(clean_func))
-                clean_func(*args, **kwargs)
-            except utils.StopTestError as cleanup_exc:
-                logger.error(msg_templ.format(clean_func, cleanup_exc))
-            except Exception:
-                logger.exception(msg_templ_no_exc.format(clean_func))
+    exc, cls, tb = sys.exc_info()
+    for stage in ctx.clear_calls_stack[::-1]:
+        with log_stage(stage):
+            stage(cfg_dict, ctx)
+
+    logger.debug("Start utils.cleanup")
+    for clean_func, args, kwargs in utils.iter_clean_func():
+        with log_stage(clean_func):
+            clean_func(*args, **kwargs)
 
     if exc is None:
         for report_stage in report_stages:
-            logger.info("Start " + get_stage_name(report_stage))
-            report_stage(cfg_dict, ctx)
+            with log_stage(report_stage):
+                report_stage(cfg_dict, ctx)
 
     logger.info("All info stored in {0} folder".format(cfg_dict['var_dir']))
 
diff --git a/wally/suits/io/defaults.cfg b/wally/suits/io/defaults.cfg
index 9aff22c..8c8644b 100644
--- a/wally/suits/io/defaults.cfg
+++ b/wally/suits/io/defaults.cfg
@@ -1,6 +1,7 @@
 buffered=0
 group_reporting=1
 iodepth=1
+unified_rw_reporting=1
 
 norandommap=1
 
diff --git a/wally/suits/io/fio_task_parser.py b/wally/suits/io/fio_task_parser.py
index aca0254..ade0028 100644
--- a/wally/suits/io/fio_task_parser.py
+++ b/wally/suits/io/fio_task_parser.py
@@ -270,7 +270,7 @@
         'x': 'sync direct'
     }
     off_mode = {'s': 'sequential', 'r': 'random'}
-    oper = {'r': 'read', 'w': 'write'}
+    oper = {'r': 'read', 'w': 'write', 'm': 'mixed'}
     return smode[name[2]] + " " + \
         off_mode[name[0]] + " " + oper[name[1]]
 
@@ -322,7 +322,10 @@
     rw = {"randread": "rr",
           "randwrite": "rw",
           "read": "sr",
-          "write": "sw"}[sec.vals["rw"]]
+          "write": "sw",
+          "randrw": "rm",
+          "rw": "sm",
+          "readwrite": "sm"}[sec.vals["rw"]]
 
     sync_mode = get_test_sync_mode(sec)
     th_count = sec.vals.get('numjobs')
diff --git a/wally/suits/io/mixed_hdd.cfg b/wally/suits/io/mixed_hdd.cfg
new file mode 100644
index 0000000..be8b1ab
--- /dev/null
+++ b/wally/suits/io/mixed_hdd.cfg
@@ -0,0 +1,12 @@
+[global]
+include defaults.cfg
+ramp_time=5
+runtime=30
+blocksize=4k
+rw=randrw
+sync=1
+direct=1
+
+[mixed-hdd-r{rwmixread}_{TEST_SUMM}]
+rwmixread={% 0,20,40,60,80,100 %}
+numjobs={% 1,8,16 %}
diff --git a/wally/suits/io/mixed_ssd.cfg b/wally/suits/io/mixed_ssd.cfg
new file mode 100644
index 0000000..2e9f04c
--- /dev/null
+++ b/wally/suits/io/mixed_ssd.cfg
@@ -0,0 +1,12 @@
+[global]
+include defaults.cfg
+ramp_time=5
+runtime=30
+blocksize=4k
+rw=randrw
+sync=1
+direct=1
+
+[mixed-ssd-r{rwmixread}_{TEST_SUMM}]
+rwmixread={% 0,20,40,60,80,85,90,95,100 %}
+numjobs={% 1,16,64,128 %}