2026-09-09 01:19:31 +03:00 | | | //+------------------------------------------------------------------+
|
2026-09-10 16:43:28 +03:00 | | | //| ReportBuilder.mqh |
|
2026-09-16 15:37:55 +03:00 | | | //| Copyright 2026, MetaQuotes Ltd. |
|
2026-09-10 16:43:28 +03:00 | | | //| www.mql5.com |
|
2026-09-09 01:19:31 +03:00 | | | //+------------------------------------------------------------------+
|
| | | #ifndef REQUEST_LATENCY_LAB_REPORT_BUILDER_MQH
|
| | | #define REQUEST_LATENCY_LAB_REPORT_BUILDER_MQH
|
| | |
|
| | | #include "..\..\Include\RequestLatencyLab\Models.mqh"
|
| | | #include "..\..\Include\RequestLatencyLab\CsvStorage.mqh"
|
| | | #include "..\..\Include\RequestLatencyLab\Statistics.mqh"
|
| | |
|
| | | struct RebuiltSample
|
| | | {
|
2026-09-12 22:14:24 +03:00 | | | ulong sequence;
|
| | | string condition_id;
|
| | | string experiment_id;
|
| | | string series_id;
|
| | | string outcome;
|
| | | uint present_mask;
|
| | | ulong t0, t1, t2, t3, t4, t5, t6;
|
| | | bool recovered;
|
| | | bool conflict;
|
| | | bool interrupted;
|
| | | bool late;
|
| | | bool deadline_exceeded; // R6-B3/B2: sticky-флаг превышения срока
|
| | | bool completed;
|
| | | bool confirmation_invalidated; // R6-B2: полное восстановление флагов metadata
|
| | | bool is_warmup; // R6-B2: WARMUP исключается как в онлайн
|
2026-09-10 22:54:26 +03:00 | | | //--- R4-B8: роль/операция/режим из samples.csv (исключение CLEANUP,
|
| | | //--- семантика applicability — как онлайн EvaluateMetric)
|
2026-09-12 22:14:24 +03:00 | | | string role;
|
| | | string operation;
|
| | | string mode;
|
| | | bool is_cleanup;
|
| | | string session_id;
|
| | | string symbol; // R5-B7: совместимость серий
|
| | | string configuration_id; // R7-B6: сверка строк серии с manifest
|
| | | string market_window_id; // R8-S2: связь sample->окно E3
|
| | | ulong market_window_seq; // R8-S2
|
| | | ulong observation_deadline_us; // R5-B3: срок метрики (колонка 49)
|
| | | bool callback_coverage_complete; // R5-B4: полнота локальных callback
|
2026-09-09 01:19:31 +03:00 | | | };
|
| | |
|
| | | class CReportBuilder
|
| | | {
|
| | | public:
|
| | | //--- чтение samples.csv и восстановление записей
|
2026-09-16 15:37:55 +03:00 | | | static int LoadSamples(const string rel_path, RebuiltSample &samples[],
|
| | | const int capacity, LabError &err);
|
| | | //--- R6-S1: числовое поле: пустое = отсутствие метки (допустимо),
|
| | | //--- любой нецифровой токен = повреждённый файл (отказ).
|
| | | static bool ParseCsvUlong(const string txt, ulong &out);
|
| | | static int CsvNum(const string txt, ulong &out, LabError &err, const string what,
|
| | | const int row);
|
| | | //--- R7-B4: деление контента на ЛОГИЧЕСКИЕ записи CSV. Перевод строки
|
| | | //--- ВНУТРИ кавычек — данные, а не разделитель; хвостовой \r терминатора
|
| | | //--- записи снимается один раз (внутренний \r данных сохраняется).
|
| | | //--- Возвращает число сохранённых записей, total_count — полное число
|
| | | //--- (позволяет отвергнуть переполнение ёмкости до молчаливого обрыва).
|
| | | static int SplitCsvRecords(const string content, string &recs[],
|
| | | const int capacity, int &total_count);
|
| | | //--- R7-B4: консистентность present_mask и числовых ячеек: бит наличия
|
| | | //--- должен соответствовать непустой ячейке (иначе данные несогласованы,
|
| | | //--- читатель не заменяет их нулём и не пропускает молча).
|
| | | static bool MaskFieldConsistent(const uint pm, const uint mask, const string txt);
|
| | | //--- R6-B2: ЕДИНАЯ функция определения роли/применимости/валидности/срока
|
| | | //--- для всех путей (online, rebuild, histogram, comparisons, E4): запись
|
| | | //--- восстанавливается в RequestMetadata и оценивается тем же static
|
| | | //--- CRequestTracker::EvaluateMetric, что и в online-экспорте.
|
| | | static bool EvaluateRebuilt(const RebuiltSample &r, const ENUM_LAB_METRIC metric,
|
| | | MetricObservation &out, bool &evaluated);
|
| | | //--- R4-B8: семантика применимости, зеркало онлайн CRequestTracker::EvaluateMetric.
|
| | | //--- Возвращает true, если метрика применима и значение может считаться.
|
| | | static bool RebuiltApplicable(const RebuiltSample &r, const ENUM_LAB_METRIC metric);
|
| | | //--- расчёт интервалов из восстановленных записей
|
| | | static bool Interval(const RebuiltSample &r, const ENUM_LAB_METRIC metric,
|
| | | long &value);
|
| | | //--- R5-B3: конец метрики для оценки своевременности по сроку
|
| | | static ulong MetricEndUs(const RebuiltSample &r, const ENUM_LAB_METRIC metric);
|
| | | //--- summary по одной группе (R4-B8: outcome_group; R6-B2: та же
|
| | | //--- семантика роли/валидности/срока и те же deadline-счётчики, что
|
| | | //--- онлайн BuildSummary — единая EvaluateMetric без второй реализации)
|
| | | static bool SummarizeGroup(const RebuiltSample &samples[], const int count,
|
| | | const ENUM_LAB_METRIC metric, const string condition,
|
| | | const string outcome_group, StatSummaryRow &row);
|
| | | //--- R4-B8: знаковые категории T2-T4 для offset_signs_rebuilt.csv
|
| | | static bool OffsetSigns(const RebuiltSample &samples[], const int count,
|
| | | const string condition, long &neg, long &zero, long &pos);
|
| | | //--- полный пересчёт из каталога набора данных
|
| | | static bool RebuildReport(const string dataset_relative_path,
|
| | | const string output_relative_path, LabError &err);
|
| | | //--- R5-B7: чтение значения ключа в manifest.csv исходного набора
|
| | | static string ReadManifestValue(const string dir, const string key);
|
| | | //--- R4-B6: E4 — report-only workflow из ровно пяти завершённых E1 ASYNC
|
| | | //--- datasets. Исходные samples НЕ копируются: строки ссылаются на сессии
|
| | | //--- оригинала; статистика считается по объединённым первичным записям
|
| | | //--- (500 ASYNC наблюдений), а не усреднением пяти медиан/p99.
|
| | | static bool RebuildE4(const string &dirs[], const int ndirs, const string dataset_id,
|
| | | const string output_relative_path, LabError &err,
|
| | | const int expected_main_per_series = 100,
|
| | | const string allowed_origin = "DEMO");
|
| | | };
|
| | | //+------------------------------------------------------------------+
|
| | | //| Внешние определения методов. |
|
| | | //+------------------------------------------------------------------+
|
| | | //--- чтение samples.csv и восстановление записей
|
| | | int CReportBuilder::LoadSamples(const string rel_path, RebuiltSample &samples[],
|
| | | const int capacity, LabError &err)
|
| | | {
|
| | | err.Reset();
|
| | | err.component = LAB_COMP_REPORT;
|
| | | string content;
|
| | | if(!CCsv::LoadUtf8(rel_path, content, err))
|
| | | return(-1);
|
| | | //--- R7-B4: деление на ЛОГИЧЕСКИЕ записи CSV (кавычки/встроенные
|
| | | //--- переводы строк/CRLF): перевод строки внутри кавычек — данные,
|
| | | //--- а не разделитель записи; терминатор записи снимается один раз.
|
| | | string recs[];
|
| | | if(ArrayResize(recs, capacity + 16) != (capacity + 16))
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | err.code = 44;
|
| | | err.severity = LAB_SEV_BLOCKER;
|
| | | err.message = "samples.csv record buffer allocation failed";
|
| | | return(-1);
|
| | | }
|
| | | int total_records = 0;
|
| | | //--- R8-S1: capacity — число НАБЛЮДЕНИЙ; заголовок НЕ расходует её
|
| | | //--- (допустимо header + capacity строк данных = capacity+1 записей).
|
| | | const int n = SplitCsvRecords(content, recs, capacity + 1, total_records);
|
| | | if(total_records > capacity + 1)
|
| | | {
|
| | | err.code = 31;
|
| | | err.severity = LAB_SEV_BLOCKER;
|
| | | err.message = "samples.csv exceeds capacity " + IntegerToString(capacity);
|
| | | return(-1);
|
| | | }
|
| | | if(n < 2)
|
| | | {
|
| | | err.code = 32;
|
| | | err.severity = LAB_SEV_BLOCKER;
|
| | | err.message = "samples.csv has no data rows";
|
| | | return(-1);
|
| | | }
|
| | | //--- R5-B5/R6-S1: полный контроль заголовка по CSV_SCHEMA (66 имён);
|
| | | //--- перестановка колонок между индексами обнаруживается.
|
| | | string hline = recs[0];
|
| | | string hcols[];
|
| | | int hcount = 0;
|
| | | CCsv::ParseRow(hline, hcols, hcount);
|
| | | bool hdr_ok = (hcount >= 68);
|
| | | const string hdr_expect[68] =
|
| | | {
|
| | | "request_id", "sequence", "send_start_us", "send_return_us", "request_event_us",
|
| | | "order_event_us", "first_deal_us", "final_event_us", "retcode", "completed",
|
| | | "campaign_id", "experiment_id", "series_id", "session_id", "configuration_id",
|
| | | "condition_id", "slot_id", "parent_sequence", "cleanup_attempt", "role",
|
| | | "is_warmup", "operation", "mode", "logging_mode", "symbol", "side",
|
| | | "correlation_status", "correlation_method", "order_ticket", "position_ticket",
|
| | | "position_identifier", "last_deal_us", "deal_count", "executed_volume",
|
| | | "callback_volume", "history_volume", "deal_coverage_complete", "present_mask",
|
| | | "recovered", "conflict", "measurement_interrupted", "scenario_deviation",
|
| | | "outcome", "outcome_at_final", "final_source", "trading_state_known",
|
| | | "confirmation_invalidated", "confirmation_record_id",
|
| | | "current_confirmation_record_id", "observation_deadline_us",
|
| | | "collection_deadline_us", "collection_close_us", "collection_closed",
|
| | | "deadline_exceeded", "market_regime", "calibration_id",
|
| | | "bid_at_send", "ask_at_send", "spread_at_send", "tick_time_msc",
|
| | | "actual_type", "actual_filling", "deviation_points", "comment",
|
| | | "request_price", "callback_coverage_complete", "market_window_id",
|
| | | "market_window_seq"
|
| | | };
|
| | | //--- R9-S1: проверяются ВСЕ объявленные имена (68), включая две
|
| | | //--- новые колонки связи sample->окно (market_window_id/seq).
|
| | | for(int hi = 0; hi < 68 && hdr_ok; hi++)
|
| | | if(hcount <= hi || hcols[hi] != hdr_expect[hi])
|
| | | hdr_ok = false;
|
| | | if(!hdr_ok)
|
| | | {
|
| | | err.code = 32;
|
| | | err.severity = LAB_SEV_BLOCKER;
|
| | | err.message = "samples.csv header mismatch (cols=" + IntegerToString(hcount) + ")";
|
| | | return(-1);
|
| | | }
|
| | | int count = 0;
|
| | | for(int i = 1; i < n; i++) // запись 0 - заголовок
|
| | | {
|
| | | string s = recs[i];
|
| | | if(StringLen(s) == 0)
|
| | | continue;
|
| | | string cols[];
|
| | | int ccount = 0;
|
| | | if(!CCsv::ParseRow(s, cols, ccount))
|
2026-09-11 15:17:38 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | err.code = 33;
|
2026-09-12 22:14:24 +03:00 | | | err.severity = LAB_SEV_BLOCKER;
|
2026-09-16 15:37:55 +03:00 | | | err.message = "samples.csv row " + IntegerToString(i) + " parse failed";
|
2026-09-11 15:17:38 +03:00 | | | return(-1);
|
| | | }
|
2026-09-16 15:37:55 +03:00 | | | //--- R5-B5: повреждённые строки не исключаются молча
|
| | | if(ccount < 68)
|
2026-09-11 15:17:38 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | err.code = 30;
|
2026-09-12 22:14:24 +03:00 | | | err.severity = LAB_SEV_BLOCKER;
|
2026-09-16 15:37:55 +03:00 | | | err.message = "samples.csv row " + IntegerToString(i) + " short: " +
|
| | | IntegerToString(ccount) + " cols (expected 68)";
|
2026-09-11 15:17:38 +03:00 | | | return(-1);
|
| | | }
|
2026-09-16 15:37:55 +03:00 | | | if(count >= capacity)
|
2026-09-11 04:09:32 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | err.code = 31;
|
2026-09-12 22:14:24 +03:00 | | | err.severity = LAB_SEV_BLOCKER;
|
2026-09-16 15:37:55 +03:00 | | | err.message = "samples.csv exceeds capacity " + IntegerToString(capacity);
|
2026-09-11 04:09:32 +03:00 | | | return(-1);
|
| | | }
|
2026-09-16 15:37:55 +03:00 | | | RebuiltSample r;
|
| | | ulong u = 0;
|
| | | if(CsvNum(cols[1], u, err, "sequence", i) < 0)
|
| | | return(-1);
|
| | | r.sequence = u;
|
| | | r.condition_id = cols[15];
|
| | | r.experiment_id = cols[11];
|
| | | r.series_id = cols[12];
|
| | | r.outcome = cols[42];
|
| | | if(CsvNum(cols[37], u, err, "present_mask", i) < 0)
|
| | | return(-1);
|
| | | r.present_mask = (uint)u;
|
| | | if(CsvNum(cols[2], u, err, "t0", i) < 0)
|
| | | return(-1);
|
| | | r.t0 = u;
|
| | | if(CsvNum(cols[3], u, err, "t1", i) < 0)
|
| | | return(-1);
|
| | | r.t1 = u;
|
| | | if(CsvNum(cols[4], u, err, "t2", i) < 0)
|
| | | return(-1);
|
| | | r.t2 = u;
|
| | | if(CsvNum(cols[5], u, err, "t3", i) < 0)
|
| | | return(-1);
|
| | | r.t3 = u;
|
| | | if(CsvNum(cols[6], u, err, "t4", i) < 0)
|
| | | return(-1);
|
| | | r.t4 = u;
|
| | | if(CsvNum(cols[31], u, err, "t5", i) < 0)
|
| | | return(-1);
|
| | | r.t5 = u;
|
| | | if(CsvNum(cols[7], u, err, "t6", i) < 0)
|
| | | return(-1);
|
| | | r.t6 = u;
|
| | | if(CsvNum(cols[49], u, err, "observation_deadline_us", i) < 0)
|
| | | return(-1);
|
| | | r.observation_deadline_us = u;
|
| | | r.recovered = cols[38] == "1";
|
| | | r.conflict = cols[39] == "1";
|
| | | r.interrupted = cols[40] == "1";
|
| | | r.late = cols[53] == "1";
|
| | | r.deadline_exceeded = cols[53] == "1"; // R6-B3
|
| | | r.completed = cols[9] == "1";
|
| | | r.confirmation_invalidated = cols[46] == "1"; // R6-B2
|
| | | r.is_warmup = (ccount > 20 && cols[20] == "1"); // R6-B2
|
| | | //--- R4-B8: роль, операция, режим (колонки 19/21/22)
|
| | | r.role = (ccount > 19 ? cols[19] : "");
|
| | | r.operation = (ccount > 21 ? cols[21] : "");
|
| | | r.mode = (ccount > 22 ? cols[22] : "");
|
| | | r.is_cleanup = (r.role == "CLEANUP");
|
| | | r.session_id = (ccount > 13 ? cols[13] : "");
|
| | | r.symbol = (ccount > 24 ? cols[24] : ""); // R5-B7
|
| | | r.configuration_id = (ccount > 14 ? cols[14] : ""); // R7-B6
|
| | | r.market_window_id = (ccount > 66 ? cols[66] : ""); // R8-S2
|
| | | if(ccount > 67)
|
2026-09-12 22:14:24 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | if(CsvNum(cols[67], u, err, "market_window_seq", i) < 0)
|
| | | return(-1);
|
| | | r.market_window_seq = u;
|
| | | }
|
| | | r.callback_coverage_complete = (ccount > 65 && cols[65] == "1"); // R5-B4
|
| | | //--- R7-B4: пустые числовые поля сверяются с present_mask: бит
|
| | | //--- наличия ↔ непустая ячейка. Изменение непустой T1 на пустую при
|
| | | //--- сохранённом бите (или наоборот) — несогласованный файл, отказ.
|
| | | if(!MaskFieldConsistent(r.present_mask, (uint)LAB_MASK_T0, cols[2]) ||
|
| | | !MaskFieldConsistent(r.present_mask, (uint)LAB_MASK_T1, cols[3]) ||
|
| | | !MaskFieldConsistent(r.present_mask, (uint)LAB_MASK_T2, cols[4]) ||
|
| | | !MaskFieldConsistent(r.present_mask, (uint)LAB_MASK_T3, cols[5]) ||
|
| | | !MaskFieldConsistent(r.present_mask, (uint)LAB_MASK_T4, cols[6]) ||
|
| | | !MaskFieldConsistent(r.present_mask, (uint)LAB_MASK_T5, cols[31]) ||
|
| | | !MaskFieldConsistent(r.present_mask, (uint)LAB_MASK_T6, cols[7]))
|
2026-09-11 04:09:32 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | err.code = 40;
|
2026-09-12 22:14:24 +03:00 | | | err.severity = LAB_SEV_BLOCKER;
|
2026-09-16 15:37:55 +03:00 | | | err.message = "samples.csv row " + IntegerToString(i) +
|
| | | " timestamp/mask mismatch";
|
2026-09-11 04:09:32 +03:00 | | | return(-1);
|
| | | }
|
2026-09-16 15:37:55 +03:00 | | | //--- R6-S1: ключи (сессия/sequence) уникальны — повтор = повреждение
|
| | | for(int dj = 0; dj < count; dj++)
|
| | | if(samples[dj].sequence == r.sequence)
|
2026-09-11 04:09:32 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | err.code = 35;
|
2026-09-12 22:14:24 +03:00 | | | err.severity = LAB_SEV_BLOCKER;
|
2026-09-16 15:37:55 +03:00 | | | err.message = "samples.csv duplicate sequence " + IntegerToString(r.sequence);
|
2026-09-11 04:09:32 +03:00 | | | return(-1);
|
| | | }
|
2026-09-16 15:37:55 +03:00 | | | samples[count] = r;
|
| | | count++;
|
2026-09-09 01:19:31 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | return(count);
|
| | | }
|
| | | bool CReportBuilder::ParseCsvUlong(const string txt, ulong &out)
|
| | | {
|
| | | out = 0;
|
| | | if(StringLen(txt) == 0)
|
| | | return(true);
|
| | | const int len = StringLen(txt);
|
| | | //--- R8-S1: диапазон ulong — не более 20 десятичных знаков
|
| | | if(len > 20)
|
| | | return(false);
|
| | | for(int k = 0; k < len; k++)
|
2026-09-11 09:06:39 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | const ushort ch = StringGetCharacter(txt, k);
|
| | | if(ch < '0' || ch > '9')
|
2026-09-11 18:16:32 +03:00 | | | return(false);
|
2026-09-16 15:37:55 +03:00 | | | }
|
| | | if(len == 20)
|
| | | {
|
| | | //--- строго <= ULONG_MAX (18446744073709551615)
|
| | | const string max_ulong = "18446744073709551615";
|
| | | for(int k = 0; k < 20; k++)
|
2026-09-11 09:06:39 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | const ushort a = StringGetCharacter(txt, k);
|
| | | const ushort b = StringGetCharacter(max_ulong, k);
|
| | | if(a > b)
|
2026-09-11 09:06:39 +03:00 | | | return(false);
|
2026-09-16 15:37:55 +03:00 | | | if(a < b)
|
| | | break;
|
2026-09-11 09:06:39 +03:00 | | | }
|
| | | }
|
2026-09-16 15:37:55 +03:00 | | | out = (ulong)StringToInteger(txt);
|
| | | return(true);
|
| | | }
|
| | | int CReportBuilder::CsvNum(const string txt, ulong &out, LabError &err, const string what,
|
| | | const int row)
|
| | | {
|
| | | if(ParseCsvUlong(txt, out))
|
| | | return(0);
|
| | | err.Reset();
|
| | | err.component = LAB_COMP_REPORT;
|
| | | err.code = 34;
|
| | | err.severity = LAB_SEV_BLOCKER;
|
| | | err.message = "samples.csv " + what + " row " + IntegerToString(row) + " not numeric";
|
| | | return(-1);
|
| | | }
|
| | | int CReportBuilder::SplitCsvRecords(const string content, string &recs[],
|
| | | const int capacity, int &total_count)
|
| | | {
|
| | | total_count = 0;
|
| | | int count = 0;
|
| | | int len = StringLen(content);
|
| | | bool in_quotes = false;
|
| | | string cur = "";
|
| | | for(int i = 0; i < len; i++)
|
2026-09-11 15:17:38 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | const ushort ch = StringGetCharacter(content, i);
|
| | | if(ch == '"')
|
2026-09-11 15:17:38 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | if(in_quotes && i + 1 < len && StringGetCharacter(content, i + 1) == '"')
|
2026-09-11 15:17:38 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | cur += "\"\"";
|
| | | i++;
|
2026-09-11 15:17:38 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | else
|
2026-09-11 15:17:38 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | cur += "\"";
|
| | | in_quotes = !in_quotes;
|
2026-09-11 15:17:38 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | continue;
|
2026-09-11 15:17:38 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | if(!in_quotes && ch == '\n')
|
2026-09-11 15:17:38 +03:00 | | | {
|
| | | total_count++;
|
2026-09-12 22:14:24 +03:00 | | | if(StringLen(cur) > 0 && StringGetCharacter(cur, StringLen(cur) - 1) == '\r')
|
| | | cur = StringSubstr(cur, 0, StringLen(cur) - 1);
|
| | | if(count < capacity)
|
2026-09-11 15:17:38 +03:00 | | | {
|
2026-09-12 22:14:24 +03:00 | | | recs[count] = cur;
|
2026-09-11 15:17:38 +03:00 | | | count++;
|
| | | }
|
2026-09-16 15:37:55 +03:00 | | | cur = "";
|
2026-09-11 15:17:38 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | else
|
| | | cur += ShortToString(ch);
|
2026-09-11 15:17:38 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | if(StringLen(cur) > 0 || total_count == 0)
|
2026-09-11 15:17:38 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | total_count++;
|
| | | if(StringLen(cur) > 0 && StringGetCharacter(cur, StringLen(cur) - 1) == '\r')
|
| | | cur = StringSubstr(cur, 0, StringLen(cur) - 1);
|
| | | if(count < capacity)
|
| | | {
|
| | | recs[count] = cur;
|
| | | count++;
|
| | | }
|
2026-09-11 15:17:38 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | return(count);
|
| | | }
|
| | | bool CReportBuilder::MaskFieldConsistent(const uint pm, const uint mask, const string txt)
|
| | | {
|
| | | const bool bit = ((pm & mask) != 0);
|
| | | const bool empty = (StringLen(txt) == 0);
|
| | | return(bit != empty);
|
| | | }
|
| | | bool CReportBuilder::EvaluateRebuilt(const RebuiltSample &r, const ENUM_LAB_METRIC metric,
|
| | | MetricObservation &out, bool &evaluated)
|
| | | {
|
| | | evaluated = false;
|
| | | out.Zero();
|
| | | //--- роль/операция/исход строкой -> enum (схема BuildSamples round-trip)
|
| | | ENUM_LAB_OPERATION op = LAB_OP_NONE;
|
| | | if(r.operation == "MARKET_OPEN")
|
| | | op = LAB_OP_MARKET_OPEN;
|
| | | else
|
| | | if(r.operation == "PENDING_CREATE")
|
| | | op = LAB_OP_PENDING_CREATE;
|
2026-09-12 22:14:24 +03:00 | | | else
|
2026-09-16 15:37:55 +03:00 | | | if(r.operation == "PENDING_DELETE")
|
| | | op = LAB_OP_PENDING_DELETE;
|
2026-09-12 22:14:24 +03:00 | | | else
|
2026-09-16 15:37:55 +03:00 | | | if(r.operation == "POSITION_CLOSE")
|
| | | op = LAB_OP_POSITION_CLOSE;
|
| | | ENUM_LAB_OUTCOME oc = LAB_OUT_UNKNOWN;
|
| | | if(r.outcome == "FILLED")
|
| | | oc = LAB_OUT_FILLED;
|
| | | else
|
| | | if(r.outcome == "PARTIAL_CANCELED")
|
| | | oc = LAB_OUT_PARTIAL_CANCELED;
|
2026-09-12 22:14:24 +03:00 | | | else
|
2026-09-16 15:37:55 +03:00 | | | if(r.outcome == "PARTIAL_EXPIRED")
|
| | | oc = LAB_OUT_PARTIAL_EXPIRED;
|
2026-09-12 22:14:24 +03:00 | | | else
|
2026-09-16 15:37:55 +03:00 | | | if(r.outcome == "CANCELED_NO_FILL")
|
| | | oc = LAB_OUT_CANCELED_NO_FILL;
|
2026-09-12 22:14:24 +03:00 | | | else
|
2026-09-16 15:37:55 +03:00 | | | if(r.outcome == "EXPIRED_NO_FILL")
|
| | | oc = LAB_OUT_EXPIRED_NO_FILL;
|
2026-09-12 22:14:24 +03:00 | | | else
|
2026-09-16 15:37:55 +03:00 | | | if(r.outcome == "REJECTED")
|
| | | oc = LAB_OUT_REJECTED;
|
2026-09-12 22:14:24 +03:00 | | | else
|
2026-09-16 15:37:55 +03:00 | | | if(r.outcome == "PENDING_PLACED")
|
| | | oc = LAB_OUT_PENDING_PLACED;
|
2026-09-12 22:14:24 +03:00 | | | else
|
2026-09-16 15:37:55 +03:00 | | | if(r.outcome == "PENDING_DELETED")
|
| | | oc = LAB_OUT_PENDING_DELETED;
|
2026-09-12 22:14:24 +03:00 | | | else
|
2026-09-16 15:37:55 +03:00 | | | if(r.outcome == "POSITION_CLOSED")
|
| | | oc = LAB_OUT_POSITION_CLOSED;
|
2026-09-12 22:14:24 +03:00 | | | else
|
2026-09-16 15:37:55 +03:00 | | | if(r.outcome == "UNEXPECTED_ACTIVATION")
|
| | | oc = LAB_OUT_UNEXPECTED_ACTIVATION;
|
| | | RequestPlan plan;
|
| | | plan.Zero();
|
| | | plan.operation = op;
|
| | | plan.symbol = r.symbol;
|
| | | plan.role = (r.role == "WARMUP" ? LAB_ROLE_WARMUP :
|
| | | (r.role == "CLEANUP" ? LAB_ROLE_CLEANUP :
|
| | | (r.role == "PILOT" ? LAB_ROLE_PILOT : LAB_ROLE_MAIN)));
|
| | | plan.sequence = r.sequence;
|
| | | RequestMetadata md;
|
| | | md.Reset(plan);
|
| | | md.present_mask = r.present_mask;
|
| | | md.outcome = oc;
|
| | | md.sample.send_start_us = r.t0;
|
| | | md.sample.send_return_us = r.t1;
|
| | | md.sample.request_event_us = r.t2;
|
| | | md.sample.order_event_us = r.t3;
|
| | | md.sample.first_deal_us = r.t4;
|
| | | md.last_deal_us = r.t5;
|
| | | md.sample.final_event_us = r.t6;
|
| | | md.callback_coverage_complete = r.callback_coverage_complete;
|
| | | md.conflict = r.conflict;
|
| | | md.measurement_interrupted = r.interrupted;
|
| | | md.confirmation_invalidated = r.confirmation_invalidated;
|
| | | md.observation_deadline_us = r.observation_deadline_us;
|
| | | return(CRequestTracker::EvaluateMetric(md, metric, out, evaluated));
|
| | | }
|
| | | bool CReportBuilder::RebuiltApplicable(const RebuiltSample &r, const ENUM_LAB_METRIC metric)
|
| | | {
|
| | | const bool deal_expected = (r.operation == "MARKET_OPEN" ||
|
| | | r.operation == "POSITION_CLOSE");
|
| | | const bool rejected = (r.outcome == "REJECTED");
|
| | | const bool unknown_outcome = (r.outcome == "UNKNOWN");
|
| | | switch(metric)
|
| | | {
|
| | | case LAB_METRIC_CALL_DURATION:
|
| | | case LAB_METRIC_REQUEST_DELAY:
|
| | | case LAB_METRIC_FINAL_STATE_DELAY:
|
| | | case LAB_METRIC_REMAINING_AFTER_RETURN:
|
| | | return(true);
|
| | | case LAB_METRIC_ORDER_DELAY:
|
| | | if(r.operation == "PENDING_DELETE" || rejected || unknown_outcome)
|
| | | return(false);
|
| | | return(true);
|
| | | case LAB_METRIC_FIRST_DEAL_DELAY:
|
| | | case LAB_METRIC_LAST_DEAL_DELAY:
|
| | | if(r.operation == "PENDING_CREATE" && r.outcome != "UNEXPECTED_ACTIVATION")
|
| | | return(false);
|
| | | if(rejected)
|
| | | return(false);
|
| | | if(!deal_expected && r.outcome != "UNEXPECTED_ACTIVATION")
|
| | | return(false);
|
| | | return(true);
|
| | | case LAB_METRIC_REQUEST_FIRST_DEAL_OFFSET:
|
| | | if(!deal_expected && r.outcome != "UNEXPECTED_ACTIVATION")
|
| | | return(false);
|
| | | if(rejected)
|
| | | return(false);
|
| | | return(true);
|
2026-09-11 09:06:39 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | return(false);
|
| | | }
|
| | | bool CReportBuilder::Interval(const RebuiltSample &r, const ENUM_LAB_METRIC metric,
|
| | | long &value)
|
| | | {
|
| | | const bool has_t0 = (r.present_mask & (uint)LAB_MASK_T0) != 0;
|
| | | const bool has_t1 = (r.present_mask & (uint)LAB_MASK_T1) != 0;
|
| | | const bool has_t2 = (r.present_mask & (uint)LAB_MASK_T2) != 0;
|
| | | const bool has_t3 = (r.present_mask & (uint)LAB_MASK_T3) != 0;
|
| | | const bool has_t4 = (r.present_mask & (uint)LAB_MASK_T4) != 0;
|
| | | const bool has_t5 = (r.present_mask & (uint)LAB_MASK_T5) != 0;
|
| | | const bool has_t6 = (r.present_mask & (uint)LAB_MASK_T6) != 0;
|
| | | ulong start = 0, end = 0;
|
| | | switch(metric)
|
2026-09-10 22:54:26 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | case LAB_METRIC_CALL_DURATION:
|
| | | start = r.t0;
|
| | | end = r.t1;
|
| | | break;
|
| | | case LAB_METRIC_REQUEST_DELAY:
|
| | | start = r.t0;
|
| | | end = r.t2;
|
| | | break;
|
| | | case LAB_METRIC_ORDER_DELAY:
|
| | | start = r.t0;
|
| | | end = r.t3;
|
| | | break;
|
| | | case LAB_METRIC_FIRST_DEAL_DELAY:
|
| | | start = r.t0;
|
| | | end = r.t4;
|
| | | break;
|
| | | case LAB_METRIC_LAST_DEAL_DELAY:
|
| | | start = r.t0;
|
| | | end = r.t5;
|
| | | break;
|
| | | case LAB_METRIC_FINAL_STATE_DELAY:
|
| | | start = r.t0;
|
| | | end = r.t6;
|
| | | break;
|
| | | case LAB_METRIC_REMAINING_AFTER_RETURN:
|
| | | start = r.t1;
|
| | | end = r.t6;
|
| | | break;
|
| | | case LAB_METRIC_REQUEST_FIRST_DEAL_OFFSET:
|
| | | start = r.t2;
|
| | | end = r.t4;
|
| | | break;
|
| | | }
|
| | | const bool sp = ((metric == LAB_METRIC_CALL_DURATION || metric == LAB_METRIC_REQUEST_DELAY ||
|
| | | metric == LAB_METRIC_ORDER_DELAY || metric == LAB_METRIC_FIRST_DEAL_DELAY ||
|
| | | metric == LAB_METRIC_LAST_DEAL_DELAY || metric == LAB_METRIC_FINAL_STATE_DELAY) && has_t0) ||
|
| | | (metric == LAB_METRIC_REMAINING_AFTER_RETURN && has_t1) ||
|
| | | (metric == LAB_METRIC_REQUEST_FIRST_DEAL_OFFSET && has_t2);
|
| | | const bool ep = ((metric == LAB_METRIC_CALL_DURATION && has_t1) ||
|
| | | (metric == LAB_METRIC_REQUEST_DELAY && has_t2) ||
|
| | | (metric == LAB_METRIC_ORDER_DELAY && has_t3) ||
|
| | | (metric == LAB_METRIC_FIRST_DEAL_DELAY && has_t4) ||
|
| | | (metric == LAB_METRIC_LAST_DEAL_DELAY && has_t5) ||
|
| | | ((metric == LAB_METRIC_FINAL_STATE_DELAY || metric == LAB_METRIC_REMAINING_AFTER_RETURN) && has_t6) ||
|
| | | (metric == LAB_METRIC_REQUEST_FIRST_DEAL_OFFSET && has_t4));
|
| | | if(!sp || !ep)
|
2026-09-10 22:54:26 +03:00 | | | return(false);
|
2026-09-16 15:37:55 +03:00 | | | if(metric == LAB_METRIC_REQUEST_FIRST_DEAL_OFFSET)
|
| | | value = SignedDelta(start, end);
|
| | | else
|
| | | value = (long)(end - start);
|
| | | return(true);
|
| | | }
|
| | | ulong CReportBuilder::MetricEndUs(const RebuiltSample &r, const ENUM_LAB_METRIC metric)
|
| | | {
|
| | | switch(metric)
|
| | | {
|
| | | case LAB_METRIC_CALL_DURATION:
|
| | | return(r.t1);
|
| | | case LAB_METRIC_REQUEST_DELAY:
|
| | | return(r.t2);
|
| | | case LAB_METRIC_ORDER_DELAY:
|
| | | return(r.t3);
|
| | | case LAB_METRIC_FIRST_DEAL_DELAY:
|
| | | return(r.t4);
|
| | | case LAB_METRIC_LAST_DEAL_DELAY:
|
| | | return(r.t5);
|
| | | case LAB_METRIC_FINAL_STATE_DELAY:
|
| | | return(r.t6);
|
| | | case LAB_METRIC_REMAINING_AFTER_RETURN:
|
| | | return(r.t6);
|
| | | case LAB_METRIC_REQUEST_FIRST_DEAL_OFFSET:
|
| | | return(r.t4);
|
2026-09-10 22:54:26 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | return(0);
|
| | | }
|
| | | bool CReportBuilder::SummarizeGroup(const RebuiltSample &samples[], const int count,
|
| | | const ENUM_LAB_METRIC metric, const string condition,
|
| | | const string outcome_group, StatSummaryRow &row)
|
| | | {
|
| | | row.Zero();
|
| | | row.condition_id = condition;
|
| | | row.metric_id = LabMetricName(metric);
|
| | | row.outcome_group = outcome_group;
|
| | | double values[];
|
| | | if(ArrayResize(values, count) != count)
|
| | | return(false);
|
| | | int used = 0;
|
| | | ulong n_deadline = 0, n_deadline_denom = 0, n_t6missing = 0;
|
| | | for(int i = 0; i < count; i++)
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | const RebuiltSample r = samples[i];
|
| | | //--- R6-B2: исключение прогрева точно как онлайн (is_warmup + CLEANUP)
|
| | | if(r.is_cleanup || r.is_warmup)
|
| | | continue;
|
| | | if(StringLen(condition) > 0 && r.condition_id != condition)
|
| | | continue;
|
| | | if(outcome_group == "REJECTED" && r.outcome != "REJECTED")
|
| | | continue;
|
| | | if(outcome_group == "SUCCESS" &&
|
| | | (r.outcome == "REJECTED" || r.outcome == "UNKNOWN"))
|
| | | continue;
|
| | | row.n_attempted++;
|
| | | //--- R6-B2: deadline-счётчики идентичны онлайн-семантике
|
| | | if(r.observation_deadline_us > 0)
|
| | | n_deadline_denom++;
|
| | | if(r.deadline_exceeded)
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | n_deadline++;
|
| | | if((r.present_mask & (uint)LAB_MASK_T6) == 0)
|
| | | n_t6missing++;
|
2026-09-09 01:19:31 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | MetricObservation mo;
|
| | | bool ev = false;
|
| | | if(!EvaluateRebuilt(r, metric, mo, ev))
|
2026-09-11 04:09:32 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | row.n_applicability_unknown++;
|
| | | continue;
|
2026-09-11 04:09:32 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | if(mo.applicability == LAB_APP_NOT_APPLICABLE)
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | row.n_not_applicable++;
|
| | | continue;
|
| | | }
|
| | | if(mo.applicability == LAB_APP_UNKNOWN)
|
| | | {
|
| | | row.n_applicability_unknown++;
|
| | | continue;
|
| | | }
|
| | | row.n_applicable++;
|
| | | if(!mo.has_value)
|
| | | {
|
| | | row.n_missing++;
|
| | | continue;
|
| | | }
|
| | | const ENUM_LAB_FITNESS cat = mo.category;
|
| | | if(cat == LAB_FIT_VALID)
|
| | | {
|
| | | if(used >= ArraySize(values))
|
2026-09-11 04:09:32 +03:00 | | | continue;
|
2026-09-16 15:37:55 +03:00 | | | values[used] = (double)mo.value_us;
|
| | | used++;
|
| | | row.n_valid++;
|
| | | }
|
| | | else
|
| | | if(cat == LAB_FIT_LATE)
|
| | | row.n_late++;
|
2026-09-11 09:06:39 +03:00 | | | else
|
2026-09-16 15:37:55 +03:00 | | | if(cat == LAB_FIT_MISSING)
|
| | | row.n_missing++;
|
2026-09-12 22:14:24 +03:00 | | | else
|
2026-09-16 15:37:55 +03:00 | | | if(cat == LAB_FIT_CONFLICT)
|
| | | row.n_conflict++;
|
2026-09-12 22:14:24 +03:00 | | | else
|
2026-09-16 15:37:55 +03:00 | | | if(cat == LAB_FIT_INTERRUPTED)
|
| | | row.n_interrupted++;
|
2026-09-12 22:14:24 +03:00 | | | else
|
| | | row.n_not_applicable++;
|
2026-09-09 01:19:31 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | if(used >= 1) // R5-S5: n=1 допустим
|
| | | CStatistics::Compute(values, used, row);
|
| | | row.n_deadline_exceeded = n_deadline;
|
| | | row.deadline_denominator = n_deadline_denom;
|
| | | row.deadline_rate = (n_deadline_denom > 0 ? (double)n_deadline / n_deadline_denom : 0.0);
|
| | | row.n_timestamp_missing_at_deadline = n_t6missing;
|
| | | row.missing_timestamp_rate = (n_deadline_denom > 0 ? (double)n_t6missing / n_deadline_denom : 0.0);
|
| | | return(true);
|
| | | }
|
| | | bool CReportBuilder::OffsetSigns(const RebuiltSample &samples[], const int count,
|
| | | const string condition, long &neg, long &zero, long &pos)
|
| | | {
|
| | | neg = 0;
|
| | | zero = 0;
|
| | | pos = 0;
|
| | | for(int i = 0; i < count; i++)
|
2026-09-10 22:54:26 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | const RebuiltSample r = samples[i];
|
| | | if(r.is_cleanup)
|
| | | continue;
|
| | | if(StringLen(condition) > 0 && r.condition_id != condition)
|
| | | continue;
|
| | | if(r.is_warmup)
|
| | | continue;
|
| | | MetricObservation mo;
|
| | | bool ev = false;
|
| | | if(!EvaluateRebuilt(r, LAB_METRIC_REQUEST_FIRST_DEAL_OFFSET, mo, ev))
|
| | | continue;
|
| | | if(!ev || mo.applicability != LAB_APP_APPLICABLE || !mo.has_value ||
|
| | | mo.category != LAB_FIT_VALID)
|
| | | continue;
|
| | | const long v = mo.value_us;
|
| | | if(v < 0)
|
| | | neg++;
|
| | | else
|
| | | if(v == 0)
|
| | | zero++;
|
2026-09-10 22:54:26 +03:00 | | | else
|
2026-09-16 15:37:55 +03:00 | | | pos++;
|
2026-09-10 22:54:26 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | return(true);
|
| | | }
|
| | | bool CReportBuilder::RebuildReport(const string dataset_relative_path,
|
| | | const string output_relative_path, LabError &err)
|
| | | {
|
| | | err.Reset();
|
| | | err.component = LAB_COMP_REPORT;
|
| | | RebuiltSample samples[];
|
| | | if(ArrayResize(samples, 4096) != 4096)
|
| | | return(false);
|
| | | const int n = LoadSamples(dataset_relative_path, samples, 4096, err);
|
| | | if(n < 0)
|
| | | return(false);
|
| | | //--- пересчёт summary (R5-B5): ЕДИНАЯ схема колонок с онлайн-export
|
| | | //--- (41 колонка, CSV_SCHEMA.md) и одна функция построения популяции.
|
| | | string out = "";
|
| | | const string header[] =
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | "experiment_id", "condition_id", "series_id", "metric_id", "outcome_group",
|
| | | "population", "observation_unit", "segment_key", "source_set_id", "unit",
|
| | | "n_attempted", "n_applicable", "n_not_applicable", "n_applicability_unknown",
|
| | | "n_valid", "n_missing", "n_late", "n_conflict", "n_interrupted", "n_used",
|
| | | "minimum", "mean", "median", "p90", "p95", "p99", "p999", "maximum", "stddev",
|
| | | "percentile_method", "method_fingerprint",
|
| | | "p95_tail_expected_n", "p99_tail_expected_n", "p999_tail_expected_n",
|
| | | "tail_warning_codes", "n_deadline_exceeded", "deadline_denominator",
|
| | | "deadline_rate", "n_timestamp_missing_at_deadline", "missing_timestamp_rate",
|
| | | "conditional_distribution"
|
| | | };
|
| | | out += CCsv::Header(header);
|
| | | string cols[41];
|
| | | ulong rows = 0; // фактическое число строк данных
|
| | | string exp_id = "";
|
| | | string ser_id = "";
|
| | | string src_session = ""; // R6-B2: источник (session)
|
| | | string conds[16];
|
| | | int cond_count = 0;
|
| | | for(int i = 0; i < n; i++)
|
| | | {
|
| | | const RebuiltSample r = samples[i];
|
| | | if(StringLen(exp_id) == 0 && StringLen(r.experiment_id) > 0)
|
| | | exp_id = r.experiment_id;
|
| | | if(StringLen(ser_id) == 0 && StringLen(r.series_id) > 0)
|
| | | ser_id = r.series_id;
|
| | | if(StringLen(src_session) == 0 && StringLen(r.session_id) > 0)
|
| | | src_session = r.session_id;
|
| | | if(r.is_cleanup || r.is_warmup) // R6-B2: прогрев не сегмент
|
| | | continue;
|
| | | bool known = false;
|
| | | for(int c = 0; c < cond_count; c++)
|
| | | if(conds[c] == r.condition_id)
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | known = true;
|
| | | break;
|
2026-09-09 01:19:31 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | if(!known && cond_count < 16)
|
2026-09-11 15:17:38 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | conds[cond_count] = r.condition_id;
|
| | | cond_count++;
|
2026-09-11 15:17:38 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | }
|
| | | //--- R5-S5: три группы исходов (ALL, SUCCESS, REJECTED); последний
|
| | | //--- проход (ccond==cond_count) — combined A+B; ключи уникальны.
|
| | | const string groups[3] = {"ALL", "SUCCESS", "REJECTED"};
|
| | | for(int g = 0; g < 3; g++)
|
| | | {
|
| | | for(int ccond = 0; ccond < cond_count + 1; ccond++)
|
2026-09-10 22:54:26 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | const string cond = (ccond < cond_count ? conds[ccond] : "");
|
| | | for(int m = 0; m < 8; m++)
|
2026-09-10 22:54:26 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | StatSummaryRow row;
|
| | | if(!SummarizeGroup(samples, n, (ENUM_LAB_METRIC)m, cond, groups[g], row))
|
2026-09-12 22:14:24 +03:00 | | | continue;
|
2026-09-16 15:37:55 +03:00 | | | cols[0] = (StringLen(row.experiment_id) > 0 ? row.experiment_id : exp_id);
|
| | | cols[1] = row.condition_id;
|
| | | cols[2] = (StringLen(row.series_id) > 0 ? row.series_id : ser_id);
|
| | | cols[3] = row.metric_id;
|
| | | cols[4] = row.outcome_group;
|
| | | cols[5] = row.population;
|
| | | cols[6] = row.observation_unit;
|
| | | cols[7] = "{}";
|
| | | cols[8] = (StringLen(row.source_set_id) > 0 ? row.source_set_id : src_session);
|
| | | cols[9] = "us";
|
| | | cols[10] = IntegerToString(row.n_attempted);
|
| | | cols[11] = IntegerToString(row.n_applicable);
|
| | | cols[12] = IntegerToString(row.n_not_applicable);
|
| | | cols[13] = IntegerToString(row.n_applicability_unknown);
|
| | | cols[14] = IntegerToString(row.n_valid);
|
| | | cols[15] = IntegerToString(row.n_missing);
|
| | | cols[16] = IntegerToString(row.n_late);
|
| | | cols[17] = IntegerToString(row.n_conflict);
|
| | | cols[18] = IntegerToString(row.n_interrupted);
|
| | | cols[19] = IntegerToString(row.n_used);
|
| | | cols[20] = (row.has_stats ? DoubleToString(row.minimum, 10) : "");
|
| | | cols[21] = (row.has_stats ? DoubleToString(row.mean, 10) : "");
|
| | | cols[22] = (row.has_stats ? DoubleToString(row.median, 10) : "");
|
| | | cols[23] = (row.has_stats ? DoubleToString(row.p90, 10) : "");
|
| | | cols[24] = (row.has_stats ? DoubleToString(row.p95, 10) : "");
|
| | | cols[25] = (row.has_stats ? DoubleToString(row.p99, 10) : "");
|
| | | cols[26] = (row.has_stats ? DoubleToString(row.p999, 10) : "");
|
| | | cols[27] = (row.has_stats ? DoubleToString(row.maximum, 10) : "");
|
| | | cols[28] = (row.has_stats && row.n_used >= 2 ? DoubleToString(row.stddev, 10) : "");
|
| | | cols[29] = row.percentile_method;
|
| | | cols[30] = row.percentile_method + "_V1";
|
| | | cols[31] = DoubleToString(row.p95_tail_expected_n, 10);
|
| | | cols[32] = DoubleToString(row.p99_tail_expected_n, 10);
|
| | | cols[33] = DoubleToString(row.p999_tail_expected_n, 10);
|
| | | cols[34] = row.tail_warning_codes;
|
| | | cols[35] = IntegerToString(row.n_deadline_exceeded);
|
| | | cols[36] = IntegerToString(row.deadline_denominator);
|
| | | cols[37] = DoubleToString(row.deadline_rate, 12);
|
| | | cols[38] = IntegerToString(row.n_timestamp_missing_at_deadline);
|
| | | cols[39] = DoubleToString(row.missing_timestamp_rate, 12);
|
| | | cols[40] = (row.conditional_distribution ? "1" : "0");
|
| | | out += CCsv::BuildRow(cols, 41);
|
| | | rows++;
|
2026-09-10 22:54:26 +03:00 | | | }
|
| | | }
|
2026-09-16 15:37:55 +03:00 | | | }
|
| | | const string summary_path = output_relative_path + "summary_rebuilt.csv";
|
| | | if(!CCsv::SaveUtf8(summary_path, out, err))
|
| | | return(false);
|
| | | //--- R5-B5: контрольное чтение — от РЕАЛЬНО созданной таблицы
|
| | | if(!CCsv::ReadBackVerifyFile(summary_path, (int)rows + 1, err))
|
| | | return(false);
|
| | | //--- histogram_rebuilt.csv (без знаковой метрики T2-T4)
|
| | | string hist = "";
|
| | | const string hh[] = {"experiment_id", "condition_id", "series_id", "metric_id",
|
| | | "outcome_group", "population", "segment_key", "bin_index",
|
| | | "lower_us", "upper_us", "count", "denominator", "share"
|
| | | };
|
| | | hist += CCsv::Header(hh);
|
| | | string hcol[13];
|
| | | const ulong lo[10] = {0, 1000, 2000, 4000, 8000, 16000, 32000, 64000, 128000, 256000};
|
| | | const ulong up[10] = {1000, 2000, 4000, 8000, 16000, 32000, 64000, 128000, 256000, 0};
|
| | | string hist_conds[16];
|
| | | int hist_cond_count = 0;
|
| | | for(int i = 0; i < n; i++)
|
| | | {
|
| | | if(samples[i].is_cleanup || samples[i].is_warmup)
|
| | | continue; // R6-B2
|
| | | bool known = false;
|
| | | for(int c2 = 0; c2 < hist_cond_count; c2++)
|
| | | if(hist_conds[c2] == samples[i].condition_id)
|
2026-09-10 22:54:26 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | known = true;
|
| | | break;
|
2026-09-10 22:54:26 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | if(!known && hist_cond_count < 16)
|
| | | { hist_conds[hist_cond_count] = samples[i].condition_id; hist_cond_count++; }
|
| | | }
|
| | | for(int m = 0; m < 8; m++)
|
| | | {
|
| | | if(m == (int)LAB_METRIC_REQUEST_FIRST_DEAL_OFFSET)
|
| | | continue; // знаковая — отдельно
|
| | | //--- R7-B4: группы условий ИЗ ДАННЫХ + combined (как online BuildHistogram);
|
| | | //--- одинаковые исходные данные дают одинаковую схему таблиц.
|
| | | for(int c = 0; c < hist_cond_count + 1; c++)
|
2026-09-10 22:54:26 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | const string cond = (c < hist_cond_count ? hist_conds[c] : "");
|
| | | long vals[];
|
| | | if(ArrayResize(vals, n) != n)
|
2026-09-10 22:54:26 +03:00 | | | continue;
|
2026-09-16 15:37:55 +03:00 | | | int vc = 0;
|
2026-09-12 22:14:24 +03:00 | | | for(int i = 0; i < n; i++)
|
2026-09-10 22:54:26 +03:00 | | | {
|
2026-09-12 22:14:24 +03:00 | | | if(samples[i].is_cleanup || samples[i].is_warmup)
|
| | | continue; // R6-B2
|
2026-09-16 15:37:55 +03:00 | | | if(StringLen(cond) > 0 && samples[i].condition_id != cond)
|
| | | continue;
|
2026-09-12 22:14:24 +03:00 | | | MetricObservation mo;
|
| | | bool ev = false;
|
| | | EvaluateRebuilt(samples[i], (ENUM_LAB_METRIC)m, mo, ev);
|
| | | if(!ev || mo.applicability != LAB_APP_APPLICABLE || !mo.has_value ||
|
| | | mo.category != LAB_FIT_VALID)
|
| | | continue;
|
2026-09-16 15:37:55 +03:00 | | | vals[vc] = (long)(mo.value_us);
|
| | | vc++;
|
| | | }
|
| | | ulong bins[10];
|
| | | ArrayInitialize(bins, 0);
|
| | | if(vc > 0)
|
| | | HistogramCount(vals, vc, bins);
|
| | | for(int b = 0; b < 10; b++)
|
| | | {
|
| | | hcol[0] = exp_id;
|
| | | hcol[1] = cond;
|
| | | hcol[2] = ser_id;
|
| | | hcol[3] = LabMetricName((ENUM_LAB_METRIC)m);
|
| | | hcol[4] = "ALL";
|
| | | hcol[5] = "PRIMARY";
|
| | | hcol[6] = "{}";
|
| | | hcol[7] = IntegerToString(b);
|
| | | hcol[8] = IntegerToString(lo[b]);
|
| | | hcol[9] = (up[b] == 0 ? "" : IntegerToString(up[b]));
|
| | | hcol[10] = IntegerToString(bins[b]);
|
| | | hcol[11] = IntegerToString(vc);
|
| | | hcol[12] = (vc > 0 ? DoubleToString((double)bins[b] / vc, 12) : "0");
|
| | | hist += CCsv::BuildRow(hcol, 13);
|
2026-09-10 22:54:26 +03:00 | | | }
|
| | | }
|
| | | }
|
2026-09-16 15:37:55 +03:00 | | | const string hist_path = output_relative_path + "histogram_rebuilt.csv";
|
| | | if(!CCsv::SaveUtf8(hist_path, hist, err))
|
| | | return(false);
|
| | | //--- offset_signs_rebuilt.csv (знаковая метрика). R7-B4: схема РОВНО
|
| | | //--- как онлайн BuildOffsetSigns (10 колонок с low_us/high_us), группы
|
| | | //--- условий ИЗ ДАННЫХ + combined — одинаковые данные, одинаковая схема.
|
| | | string os = "";
|
| | | const string oh[] = {"experiment_id", "condition_id", "series_id", "metric_id",
|
| | | "sign", "low_us", "high_us", "count", "denominator", "share"
|
| | | };
|
| | | os += CCsv::Header(oh);
|
| | | string ocol[10];
|
| | | const string signs[3] = {"NEGATIVE", "ZERO", "POSITIVE"};
|
| | | string os_conds[16];
|
| | | int os_cond_count = 0;
|
| | | for(int i = 0; i < n; i++)
|
| | | {
|
| | | if(samples[i].is_cleanup || samples[i].is_warmup)
|
| | | continue; // R6-B2
|
| | | bool known = false;
|
| | | for(int c2 = 0; c2 < os_cond_count; c2++)
|
| | | if(os_conds[c2] == samples[i].condition_id)
|
| | | {
|
| | | known = true;
|
| | | break;
|
| | | }
|
| | | if(!known && os_cond_count < 16)
|
| | | { os_conds[os_cond_count] = samples[i].condition_id; os_cond_count++; }
|
| | | }
|
| | | for(int c = 0; c < os_cond_count + 1; c++)
|
| | | {
|
| | | const string cond = (c < os_cond_count ? os_conds[c] : "");
|
| | | long neg = 0, zero = 0, pos = 0;
|
| | | OffsetSigns(samples, n, cond, neg, zero, pos);
|
| | | const long totals[3] = {neg, zero, pos};
|
| | | const long total = neg + zero + pos;
|
| | | for(int k = 0; k < 3; k++)
|
| | | {
|
| | | ocol[0] = exp_id;
|
| | | ocol[1] = cond;
|
| | | ocol[2] = ser_id;
|
| | | ocol[3] = LabMetricName(LAB_METRIC_REQUEST_FIRST_DEAL_OFFSET);
|
| | | ocol[4] = signs[k];
|
| | | ocol[5] = (k == 0 ? "" : (k == 1 ? "0" : "1"));
|
| | | ocol[6] = (k == 0 ? "-1" : (k == 1 ? "0" : ""));
|
| | | ocol[7] = IntegerToString(totals[k]);
|
| | | ocol[8] = IntegerToString(total);
|
| | | ocol[9] = (total > 0 ? DoubleToString((double)totals[k] / total, 12) : "0");
|
| | | os += CCsv::BuildRow(ocol, 10);
|
| | | }
|
| | | }
|
| | | const string os_path = output_relative_path + "offset_signs_rebuilt.csv";
|
| | | if(!CCsv::SaveUtf8(os_path, os, err))
|
| | | return(false);
|
| | | //--- comparisons_rebuilt.csv (A vs B). R7-S2: ТЗ §7 — delta = B−A,
|
| | | //--- mean/median/p95 и относительное изменение (B−A)/A; схема с онлайн.
|
| | | string cmp = "";
|
| | | const string ch[] = {"experiment_id", "condition_pair", "metric_id", "unit",
|
| | | "outcome_group", "a_n", "a_mean", "a_median", "a_p95",
|
| | | "b_n", "b_mean", "b_median", "b_p95",
|
| | | "delta_mean", "delta_median", "delta_p95",
|
| | | "rel_mean", "rel_median", "rel_p95"
|
| | | };
|
| | | cmp += CCsv::Header(ch);
|
| | | string ccol[19];
|
| | | for(int m = 0; m < 8; m++)
|
2026-09-11 04:09:32 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | double va[], vb[];
|
| | | int na = 0, nb = 0;
|
| | | if(ArrayResize(va, n) != n || ArrayResize(vb, n) != n)
|
| | | continue;
|
2026-09-12 22:14:24 +03:00 | | | for(int i = 0; i < n; i++)
|
2026-09-11 04:09:32 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | if(samples[i].is_cleanup || samples[i].is_warmup)
|
| | | continue; // R6-B2
|
| | | const string rc = samples[i].condition_id;
|
| | | MetricObservation mo;
|
| | | bool ev = false;
|
| | | EvaluateRebuilt(samples[i], (ENUM_LAB_METRIC)m, mo, ev);
|
| | | if(!ev || mo.applicability != LAB_APP_APPLICABLE || !mo.has_value ||
|
| | | mo.category != LAB_FIT_VALID)
|
2026-09-11 04:09:32 +03:00 | | | continue;
|
2026-09-16 15:37:55 +03:00 | | | if(rc == "A")
|
| | | {
|
| | | va[na] = (double)(mo.value_us);
|
| | | na++;
|
| | | }
|
| | | else
|
| | | if(rc == "B")
|
| | | {
|
| | | vb[nb] = (double)(mo.value_us);
|
| | | nb++;
|
| | | }
|
2026-09-11 04:09:32 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | double ma = 0, pa = 0, mb = 0, pb = 0, ea = 0, eb = 0;
|
| | | const bool ha = (na >= 1 && LabAvgValue(va, na, ea) &&
|
| | | LabPercentile(va, na, 50.0, ma) && LabPercentile(va, na, 95.0, pa));
|
| | | const bool hb = (nb >= 1 && LabAvgValue(vb, nb, eb) &&
|
| | | LabPercentile(vb, nb, 50.0, mb) && LabPercentile(vb, nb, 95.0, pb));
|
| | | ccol[0] = exp_id;
|
| | | ccol[1] = "A_vs_B";
|
| | | ccol[2] = LabMetricName((ENUM_LAB_METRIC)m);
|
| | | ccol[3] = "us";
|
| | | ccol[4] = "ALL";
|
| | | ccol[5] = IntegerToString(na);
|
| | | ccol[6] = (ha ? DoubleToString(ea, 10) : "");
|
| | | ccol[7] = (ha ? DoubleToString(ma, 10) : "");
|
| | | ccol[8] = (ha ? DoubleToString(pa, 10) : "");
|
| | | ccol[9] = IntegerToString(nb);
|
| | | ccol[10] = (hb ? DoubleToString(eb, 10) : "");
|
| | | ccol[11] = (hb ? DoubleToString(mb, 10) : "");
|
| | | ccol[12] = (hb ? DoubleToString(pb, 10) : "");
|
| | | //--- delta = B−A; rel = (B−A)/A (ТЗ §7, стр. 419)
|
| | | ccol[13] = (ha && hb ? DoubleToString(eb - ea, 10) : "");
|
| | | ccol[14] = (ha && hb ? DoubleToString(mb - ma, 10) : "");
|
| | | ccol[15] = (ha && hb ? DoubleToString(pb - pa, 10) : "");
|
| | | ccol[16] = (ha && hb && MathAbs(ea) > 1e-12 ? DoubleToString((eb - ea) / ea, 10) : "");
|
| | | ccol[17] = (ha && hb && MathAbs(ma) > 1e-12 ? DoubleToString((mb - ma) / ma, 10) : "");
|
| | | ccol[18] = (ha && hb && MathAbs(pa) > 1e-12 ? DoubleToString((pb - pa) / pa, 10) : "");
|
| | | cmp += CCsv::BuildRow(ccol, 19);
|
| | | }
|
| | | const string cmp_path = output_relative_path + "comparisons_rebuilt.csv";
|
| | | if(!CCsv::SaveUtf8(cmp_path, cmp, err))
|
| | | return(false);
|
| | | return(true);
|
| | | }
|
| | | string CReportBuilder::ReadManifestValue(const string dir, const string key)
|
| | | {
|
| | | LabError e;
|
| | | string content;
|
| | | if(!CCsv::LoadUtf8(dir + "\\manifest.csv", content, e))
|
2026-09-11 04:09:32 +03:00 | | | return("");
|
2026-09-16 15:37:55 +03:00 | | | string lines[];
|
| | | const int n = StringSplit(content, '\n', lines);
|
| | | for(int i = 0; i < n; i++)
|
| | | {
|
| | | string s = lines[i];
|
| | | StringReplace(s, "\r", "");
|
| | | if(StringLen(s) == 0)
|
| | | continue;
|
| | | string cols[];
|
| | | int c = 0;
|
| | | if(!CCsv::ParseRow(s, cols, c) || c < 2)
|
| | | continue;
|
| | | if(cols[0] == key)
|
| | | return(cols[1]);
|
2026-09-11 04:09:32 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | return("");
|
| | | }
|
| | | bool CReportBuilder::RebuildE4(const string &dirs[], const int ndirs, const string dataset_id,
|
| | | const string output_relative_path, LabError &err,
|
| | | const int expected_main_per_series, const string allowed_origin)
|
| | | {
|
| | | err.Reset();
|
| | | err.component = LAB_COMP_REPORT;
|
| | | if(ndirs != 5)
|
2026-09-10 22:54:26 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | err.code = 20;
|
| | | err.message = "E4 requires exactly 5 E1 ASYNC datasets (got " +
|
| | | IntegerToString(ndirs) + ")";
|
| | | return(false);
|
| | | }
|
| | | //--- R5-B7: пять РАЗНЫХ каталогов (повтор одного набора под разными
|
| | | //--- именами не даёт 500 независимых наблюдений)
|
| | | for(int a = 0; a < ndirs; a++)
|
| | | for(int b = a + 1; b < ndirs; b++)
|
2026-09-10 22:54:26 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | if(dirs[a] == dirs[b])
|
2026-09-12 22:14:24 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | err.code = 23;
|
| | | err.message = "E4 duplicate dataset path: " + dirs[a];
|
| | | return(false);
|
2026-09-12 22:14:24 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | }
|
| | | RebuiltSample merged[];
|
| | | if(ArrayResize(merged, 4096) != 4096)
|
| | | return(false);
|
| | | int total = 0;
|
| | | int per_series[5];
|
| | | string series_session[5];
|
| | | for(int d = 0; d < 5; d++)
|
| | | per_series[d] = 0;
|
| | | //--- R6-B7: уникальные sequence внутри каждого файла серии
|
| | | ulong seen_seq[5][4096];
|
| | | int seen_count[5];
|
| | | for(int d = 0; d < 5; d++)
|
| | | seen_count[d] = 0; // заполняется до чтения
|
| | | string series_symbol[5];
|
| | | string series_cfg[5];
|
| | | for(int d = 0; d < ndirs; d++)
|
| | | {
|
| | | //--- R6-B7: manifest обязателен (отсутствие = недостающее
|
| | | //--- доказательство завершения), допускается только FINISHED.
|
| | | const string rs = ReadManifestValue(dirs[d], "run_status");
|
| | | if(rs == "")
|
| | | {
|
| | | err.code = 36;
|
| | | err.message = "E4 dataset " + IntegerToString(d + 1) + " manifest missing (no evidence of finish)";
|
2026-09-10 22:54:26 +03:00 | | | return(false);
|
2026-09-16 15:37:55 +03:00 | | | }
|
| | | if(rs != "FINISHED")
|
2026-09-10 22:54:26 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | err.code = 26;
|
| | | err.message = "E4 dataset " + IntegerToString(d + 1) + " run_status=" + rs + " (expected FINISHED)";
|
| | | return(false);
|
| | | }
|
| | | const string nm = ReadManifestValue(dirs[d], "n_main_dispatched");
|
| | | if((int)StringToInteger(nm) < expected_main_per_series)
|
| | | {
|
| | | err.code = 37;
|
| | | err.message = "E4 dataset " + IntegerToString(d + 1) + " n_main_dispatched=" + nm +
|
| | | " < expected " + IntegerToString(expected_main_per_series);
|
| | | return(false);
|
| | | }
|
| | | series_cfg[d] = ReadManifestValue(dirs[d], "configuration_id");
|
| | | if(series_cfg[d] == "")
|
| | | {
|
| | | err.code = 31;
|
| | | err.message = "E4 dataset " + IntegerToString(d + 1) + " configuration_id missing";
|
| | | return(false);
|
| | | }
|
| | | //--- R7-B6: обычный E4 принимает ТОЛЬКО допустимое экспериментальное
|
| | | //--- происхождение (DEMO); LOCAL_ONLY формирует SYNTHETIC MAIN/WARMUP,
|
| | | //--- и включение таких наборов в материал статьи невозможно без
|
| | | //--- явного перехода в отдельный синтетический режим проверки.
|
| | | const string org = ReadManifestValue(dirs[d], "data_origin");
|
| | | if(org != allowed_origin)
|
| | | {
|
| | | err.code = 41;
|
| | | err.message = "E4 dataset " + IntegerToString(d + 1) + " data_origin=" +
|
| | | (StringLen(org) == 0 ? "<missing>" : org) +
|
| | | " (requires " + allowed_origin + ")";
|
| | | return(false);
|
| | | }
|
| | | RebuiltSample buf[4096];
|
| | | const int nb = LoadSamples(dirs[d] + "\\samples.csv", buf, 4096, err);
|
| | | if(nb < 0)
|
| | | {
|
| | | err.message = "E4 dataset " + IntegerToString(d + 1) + " unreadable: " + err.message;
|
| | | return(false);
|
| | | }
|
| | | for(int i = 0; i < nb; i++)
|
| | | {
|
| | | const RebuiltSample r = buf[i];
|
| | | //--- совместимость: только E1, только ASYNC (ветвь B), без CLEANUP
|
| | | if(r.is_cleanup)
|
| | | continue;
|
| | | if(r.experiment_id != "E1")
|
2026-09-11 15:17:38 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | err.code = 21;
|
| | | err.message = "E4 dataset " + IntegerToString(d + 1) + " is not E1";
|
2026-09-11 15:17:38 +03:00 | | | return(false);
|
| | | }
|
2026-09-16 15:37:55 +03:00 | | | //--- R5-B7: прогрев не входит в pooled; PILOT/SYNTHETIC отвергаются
|
| | | if(r.role == "WARMUP")
|
| | | continue;
|
| | | if(r.role != "MAIN")
|
2026-09-10 22:54:26 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | err.code = 24;
|
| | | err.message = "E4 dataset " + IntegerToString(d + 1) + " contains role " + r.role;
|
2026-09-10 22:54:26 +03:00 | | | return(false);
|
| | | }
|
2026-09-16 15:37:55 +03:00 | | | if(r.condition_id != "B" || r.mode != "ASYNC")
|
| | | continue;
|
| | | if(StringLen(r.series_id) == 0)
|
| | | continue;
|
| | | //--- R5-B7: символ серий должен совпадать
|
| | | if(StringLen(r.symbol) > 0)
|
2026-09-10 22:54:26 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | if(StringLen(series_symbol[d]) == 0)
|
| | | series_symbol[d] = r.symbol;
|
2026-09-12 22:14:24 +03:00 | | | else
|
2026-09-16 15:37:55 +03:00 | | | if(r.symbol != series_symbol[d])
|
2026-09-11 04:09:32 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | err.code = 28;
|
| | | err.message = "E4 dataset " + IntegerToString(d + 1) + " symbol mismatch";
|
2026-09-11 04:09:32 +03:00 | | | return(false);
|
| | | }
|
2026-09-16 15:37:55 +03:00 | | | }
|
| | | //--- R6-B7: все строки файла принадлежат ОДНОЙ сессии серии
|
| | | if(StringLen(series_session[d]) == 0)
|
| | | series_session[d] = r.session_id;
|
| | | else
|
| | | if(r.session_id != series_session[d])
|
2026-09-11 15:17:38 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | err.code = 38;
|
| | | err.message = "E4 dataset " + IntegerToString(d + 1) + " mixed sessions";
|
2026-09-11 15:17:38 +03:00 | | | return(false);
|
| | | }
|
2026-09-16 15:37:55 +03:00 | | | //--- R7-B6: конфигурация СТРОК сверяется с manifest серии
|
| | | //--- (не только равенство двух деклараций configuration_id).
|
| | | if(StringLen(r.configuration_id) > 0 &&
|
| | | r.configuration_id != series_cfg[d])
|
2026-09-11 04:09:32 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | err.code = 42;
|
2026-09-12 22:14:24 +03:00 | | | err.message = "E4 dataset " + IntegerToString(d + 1) +
|
2026-09-16 15:37:55 +03:00 | | | " row configuration_id mismatch (manifest vs samples)";
|
2026-09-11 04:09:32 +03:00 | | | return(false);
|
| | | }
|
2026-09-16 15:37:55 +03:00 | | | //--- R6-B7: ключ измерения уникален внутри серии (дубликат — фальсификация)
|
| | | for(int sk = 0; sk < seen_count[d]; sk++)
|
| | | if(seen_seq[d][sk] == r.sequence)
|
2026-09-12 22:14:24 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | err.code = 39;
|
| | | err.message = "E4 dataset " + IntegerToString(d + 1) + " duplicate sequence " +
|
| | | IntegerToString(r.sequence);
|
| | | return(false);
|
2026-09-12 22:14:24 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | seen_seq[d][seen_count[d]++] = r.sequence;
|
| | | if(total >= 4096)
|
| | | break;
|
| | | merged[total] = r;
|
| | | per_series[d]++;
|
| | | total++;
|
| | | }
|
| | | }
|
| | | //--- R5-B7: точные квоты MAIN на каждую серию
|
| | | for(int d = 0; d < 5; d++)
|
| | | {
|
| | | if(per_series[d] != expected_main_per_series)
|
2026-09-11 04:09:32 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | err.code = 25;
|
| | | err.message = "E4 dataset " + IntegerToString(d + 1) +
|
| | | " MAIN B-ASYNC count " + IntegerToString(per_series[d]) +
|
| | | " != expected " + IntegerToString(expected_main_per_series);
|
2026-09-11 04:09:32 +03:00 | | | return(false);
|
| | | }
|
2026-09-16 15:37:55 +03:00 | | | }
|
| | | //--- R5-B7: пять РАЗНЫХ исходных сессий
|
| | | bool sessions_unique = true;
|
| | | for(int a = 0; a < 5 && sessions_unique; a++)
|
| | | for(int b = a + 1; b < 5; b++)
|
| | | {
|
| | | if(StringLen(series_session[a]) == 0 || StringLen(series_session[b]) == 0 ||
|
| | | series_session[a] == series_session[b])
|
2026-09-12 22:14:24 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | sessions_unique = false;
|
| | | break;
|
2026-09-12 22:14:24 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | }
|
| | | if(!sessions_unique)
|
| | | {
|
| | | err.code = 27;
|
| | | err.message = "E4 datasets are not 5 distinct finished series";
|
| | | return(false);
|
| | | }
|
| | | //--- R6-B7: совместимость источников — символ и нормализованная
|
| | | //--- конфигурация равны МЕЖДУ всеми пятью сериями (не только внутри).
|
| | | for(int a = 0; a < 5; a++)
|
| | | for(int b = a + 1; b < 5; b++)
|
2026-09-10 22:54:26 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | if(StringLen(series_symbol[a]) == 0 || StringLen(series_symbol[b]) == 0 ||
|
| | | series_symbol[a] != series_symbol[b])
|
| | | {
|
| | | err.code = 34;
|
| | | err.message = "E4 symbol mismatch across series: " +
|
| | | series_symbol[a] + " vs " + series_symbol[b];
|
| | | return(false);
|
| | | }
|
| | | if(series_cfg[a] != series_cfg[b])
|
| | | {
|
| | | err.code = 32;
|
| | | err.message = "E4 configuration mismatch: " +
|
| | | series_cfg[a] + " vs " + series_cfg[b];
|
| | | return(false);
|
| | | }
|
2026-09-10 22:54:26 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | //--- pooled-порог: ровно 5*expected MAIN B-ASYNC наблюдений
|
| | | if(total < expected_main_per_series * 5)
|
| | | {
|
| | | err.code = 22;
|
| | | err.message = "E4 pooled ASYNC count insufficient: " + IntegerToString(total);
|
| | | return(false);
|
| | | }
|
| | | //--- summary_rebuilt_e4.csv: per-series + pooled (только объединённые
|
| | | //--- первичные записи; усреднение серийных статистик не производится)
|
| | | string out = "";
|
| | | //--- R7-B6: происхождение переносится во ВСЕ производные файлы;
|
| | | //--- добавлены показатели полноты по серии и pooled (n_attempted,
|
| | | //--- отказы/пропуски/deadline — числа нельзя выводить из одного n).
|
| | | const string header[] =
|
| | | {
|
| | | "experiment_id", "series_id", "source_session", "data_origin", "metric_id", "n",
|
| | | "n_attempted", "n_applicable", "n_not_applicable", "n_applicability_unknown",
|
| | | "n_missing", "n_late", "n_conflict", "n_interrupted",
|
| | | "n_deadline_exceeded", "deadline_denominator", "deadline_rate",
|
| | | "median", "p90", "p95", "p99", "minimum", "maximum", "mean", "stddev"
|
| | | };
|
| | | out += CCsv::Header(header);
|
| | | string cols[25];
|
| | | const int metrics[7] = {LAB_METRIC_CALL_DURATION, LAB_METRIC_REQUEST_DELAY,
|
| | | LAB_METRIC_ORDER_DELAY, LAB_METRIC_FIRST_DEAL_DELAY,
|
| | | LAB_METRIC_LAST_DEAL_DELAY, LAB_METRIC_FINAL_STATE_DELAY,
|
| | | LAB_METRIC_REMAINING_AFTER_RETURN
|
| | | };
|
| | | //--- pooled из 500 первичных записей
|
| | | for(int mi = 0; mi < 7; mi++)
|
| | | {
|
| | | const ENUM_LAB_METRIC metric = (ENUM_LAB_METRIC)metrics[mi];
|
| | | double vals[];
|
| | | int vc = 0;
|
| | | if(ArrayResize(vals, 512) != 512)
|
| | | continue;
|
| | | ulong na = 0, napp = 0, nnapp = 0, nunk = 0, nmis = 0, nlate = 0, nconf = 0, nint = 0,
|
| | | nddead = 0, ndden = 0, nt6m = 0;
|
| | | for(int i = 0; i < total; i++)
|
2026-09-12 22:14:24 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | const RebuiltSample rr = merged[i];
|
| | | MetricObservation mo;
|
| | | bool ev = false;
|
| | | na++;
|
| | | if(rr.observation_deadline_us > 0)
|
| | | ndden++;
|
| | | if(rr.deadline_exceeded)
|
| | | {
|
| | | nddead++;
|
| | | if((rr.present_mask & (uint)LAB_MASK_T6) == 0)
|
| | | nt6m++;
|
| | | }
|
| | | if(!EvaluateRebuilt(rr, metric, mo, ev) || !ev)
|
| | | { nunk++; continue; }
|
| | | if(mo.applicability == LAB_APP_NOT_APPLICABLE)
|
| | | { nnapp++;
|
| | | continue; }
|
| | | if(mo.applicability == LAB_APP_UNKNOWN)
|
| | | { nunk++; continue; }
|
| | | napp++;
|
| | | //--- R7-S1: CONFLICT/INTERRUPTED учитываются ДО проверки has_value
|
| | | if(mo.category == LAB_FIT_CONFLICT)
|
| | | { nconf++; continue; }
|
| | | if(mo.category == LAB_FIT_INTERRUPTED)
|
| | | { nint++; continue; }
|
| | | if(!mo.has_value)
|
| | | { nmis++; continue; }
|
| | | if(mo.category == LAB_FIT_LATE)
|
| | | { nlate++; continue; }
|
| | | if(mo.category != LAB_FIT_VALID)
|
| | | { nmis++; continue; }
|
| | | if(vc >= ArraySize(vals) && ArrayResize(vals, vc + 256) != vc + 256)
|
| | | break;
|
| | | vals[vc] = (double)(mo.value_us);
|
| | | vc++;
|
| | | }
|
| | | StatSummaryRow row;
|
| | | row.Zero();
|
| | | if(vc >= 2)
|
| | | CStatistics::Compute(vals, vc, row);
|
| | | cols[0] = "E4";
|
| | | cols[1] = "POOLED";
|
| | | cols[2] = "(5 series)";
|
| | | cols[3] = allowed_origin;
|
| | | cols[4] = LabMetricName(metric);
|
| | | cols[5] = IntegerToString(vc);
|
| | | cols[6] = IntegerToString(na);
|
| | | cols[7] = IntegerToString(napp);
|
| | | cols[8] = IntegerToString(nnapp);
|
| | | cols[9] = IntegerToString(nunk);
|
| | | cols[10] = IntegerToString(nmis);
|
| | | cols[11] = IntegerToString(nlate);
|
| | | cols[12] = IntegerToString(nconf);
|
| | | cols[13] = IntegerToString(nint);
|
| | | cols[14] = IntegerToString(nddead);
|
| | | cols[15] = IntegerToString(ndden);
|
| | | cols[16] = (ndden > 0 ? DoubleToString((double)nddead / ndden, 12) : "0");
|
| | | cols[17] = (row.has_stats ? DoubleToString(row.median, 10) : "");
|
| | | cols[18] = (row.has_stats ? DoubleToString(row.p90, 10) : "");
|
| | | cols[19] = (row.has_stats ? DoubleToString(row.p95, 10) : "");
|
| | | cols[20] = (row.has_stats ? DoubleToString(row.p99, 10) : "");
|
| | | cols[21] = (row.has_stats ? DoubleToString(row.minimum, 10) : "");
|
| | | cols[22] = (row.has_stats ? DoubleToString(row.maximum, 10) : "");
|
| | | cols[23] = (row.has_stats ? DoubleToString(row.mean, 10) : "");
|
| | | cols[24] = (row.has_stats ? DoubleToString(row.stddev, 10) : "");
|
| | | out += CCsv::BuildRow(cols, 25);
|
| | | }
|
| | | //--- per-series variability (реальные серии E1, ссылки на исходные сессии)
|
| | | //--- (5 серий берутся из первых 5 исходных файлов по порядку)
|
| | | int offset = 0;
|
| | | for(int d = 0; d < 5; d++)
|
| | | {
|
| | | //--- per-series блок: пересчитываем статистику по merged срезом
|
| | | int from = offset;
|
| | | int span = per_series[d];
|
2026-09-12 22:14:24 +03:00 | | | for(int mi = 0; mi < 7; mi++)
|
2026-09-10 22:54:26 +03:00 | | | {
|
2026-09-12 22:14:24 +03:00 | | | const ENUM_LAB_METRIC metric = (ENUM_LAB_METRIC)metrics[mi];
|
| | | double vals[];
|
| | | int vc = 0;
|
2026-09-16 15:37:55 +03:00 | | | if(ArrayResize(vals, 256) != 256)
|
2026-09-10 22:54:26 +03:00 | | | continue;
|
2026-09-12 22:14:24 +03:00 | | | ulong na = 0, napp = 0, nnapp = 0, nunk = 0, nmis = 0, nlate = 0, nconf = 0, nint = 0,
|
| | | nddead = 0, ndden = 0, nt6m = 0;
|
2026-09-16 15:37:55 +03:00 | | | for(int i = from; i < from + span && i < total; i++)
|
2026-09-10 22:54:26 +03:00 | | | {
|
2026-09-12 22:14:24 +03:00 | | | const RebuiltSample rr = merged[i];
|
2026-09-11 09:06:39 +03:00 | | | MetricObservation mo;
|
2026-09-12 22:14:24 +03:00 | | | bool ev = false;
|
2026-09-11 15:17:38 +03:00 | | | na++;
|
2026-09-12 22:14:24 +03:00 | | | if(rr.observation_deadline_us > 0)
|
2026-09-11 15:17:38 +03:00 | | | ndden++;
|
| | | if(rr.deadline_exceeded)
|
| | | {
|
| | | nddead++;
|
2026-09-12 22:14:24 +03:00 | | | if((rr.present_mask & (uint)LAB_MASK_T6) == 0)
|
2026-09-11 15:17:38 +03:00 | | | nt6m++;
|
| | | }
|
2026-09-12 22:14:24 +03:00 | | | if(!EvaluateRebuilt(rr, metric, mo, ev) || !ev)
|
2026-09-11 15:17:38 +03:00 | | | { nunk++; continue; }
|
2026-09-12 22:14:24 +03:00 | | | if(mo.applicability == LAB_APP_NOT_APPLICABLE)
|
2026-09-11 15:17:38 +03:00 | | | { nnapp++; continue; }
|
2026-09-12 22:14:24 +03:00 | | | if(mo.applicability == LAB_APP_UNKNOWN)
|
2026-09-11 15:17:38 +03:00 | | | { nunk++; continue; }
|
| | | napp++;
|
2026-09-12 22:14:24 +03:00 | | | if(mo.category == LAB_FIT_CONFLICT)
|
2026-09-11 15:17:38 +03:00 | | | { nconf++; continue; }
|
2026-09-12 22:14:24 +03:00 | | | if(mo.category == LAB_FIT_INTERRUPTED)
|
2026-09-11 15:17:38 +03:00 | | | { nint++; continue; }
|
| | | if(!mo.has_value)
|
| | | { nmis++; continue; }
|
2026-09-12 22:14:24 +03:00 | | | if(mo.category == LAB_FIT_LATE)
|
2026-09-11 15:17:38 +03:00 | | | { nlate++; continue; }
|
2026-09-12 22:14:24 +03:00 | | | if(mo.category != LAB_FIT_VALID)
|
2026-09-11 15:17:38 +03:00 | | | { nmis++; continue; }
|
2026-09-12 22:14:24 +03:00 | | | if(vc >= ArraySize(vals) && ArrayResize(vals, vc + 256) != vc + 256)
|
2026-09-10 22:54:26 +03:00 | | | break;
|
2026-09-12 22:14:24 +03:00 | | | vals[vc] = (double)(mo.value_us);
|
2026-09-10 22:54:26 +03:00 | | | vc++;
|
| | | }
|
| | | StatSummaryRow row;
|
| | | row.Zero();
|
2026-09-12 22:14:24 +03:00 | | | if(vc >= 2)
|
| | | CStatistics::Compute(vals, vc, row);
|
| | | cols[0] = "E4";
|
2026-09-16 15:37:55 +03:00 | | | cols[1] = IntegerToString(d + 1);
|
| | | cols[2] = series_session[d];
|
2026-09-12 22:14:24 +03:00 | | | cols[3] = allowed_origin;
|
| | | cols[4] = LabMetricName(metric);
|
| | | cols[5] = IntegerToString(vc);
|
| | | cols[6] = IntegerToString(na);
|
| | | cols[7] = IntegerToString(napp);
|
| | | cols[8] = IntegerToString(nnapp);
|
| | | cols[9] = IntegerToString(nunk);
|
| | | cols[10] = IntegerToString(nmis);
|
| | | cols[11] = IntegerToString(nlate);
|
| | | cols[12] = IntegerToString(nconf);
|
| | | cols[13] = IntegerToString(nint);
|
| | | cols[14] = IntegerToString(nddead);
|
| | | cols[15] = IntegerToString(ndden);
|
| | | cols[16] = (ndden > 0 ? DoubleToString((double)nddead / ndden, 12) : "0");
|
| | | cols[17] = (row.has_stats ? DoubleToString(row.median, 10) : "");
|
| | | cols[18] = (row.has_stats ? DoubleToString(row.p90, 10) : "");
|
| | | cols[19] = (row.has_stats ? DoubleToString(row.p95, 10) : "");
|
| | | cols[20] = (row.has_stats ? DoubleToString(row.p99, 10) : "");
|
| | | cols[21] = (row.has_stats ? DoubleToString(row.minimum, 10) : "");
|
| | | cols[22] = (row.has_stats ? DoubleToString(row.maximum, 10) : "");
|
| | | cols[23] = (row.has_stats ? DoubleToString(row.mean, 10) : "");
|
| | | cols[24] = (row.has_stats ? DoubleToString(row.stddev, 10) : "");
|
| | | out += CCsv::BuildRow(cols, 25);
|
2026-09-10 22:54:26 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | offset += span;
|
2026-09-09 01:19:31 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | const string path = output_relative_path + "summary_rebuilt_e4.csv";
|
| | | if(!CCsv::SaveUtf8(path, out, err))
|
| | | return(false);
|
| | | err.message = "";
|
| | | return(true);
|
| | | }
|
2026-09-10 22:54:26 +03:00 | | |
|
2026-09-12 22:14:24 +03:00 | | | #endif // REQUEST_LATENCY_LAB_REPORT_BUILDER_MQH
|
| | | //+------------------------------------------------------------------+
|