diff options
author | Oleg Doronin <dorooleg@yandex.ru> | 2025-04-09 09:30:04 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-04-09 09:30:04 +0300 |
commit | 34a1855aaa5fb3d26817b2bbedbebc08a57f6a11 (patch) | |
tree | 7d74c42f886d1f61e403d3e4a0093f083f8aa1c4 | |
parent | 56d21546eaa29f332c0ac39e83742ed4614c9309 (diff) | |
download | ydb-34a1855aaa5fb3d26817b2bbedbebc08a57f6a11.tar.gz |
DDR scheduler (#16950)
98 files changed, 15751 insertions, 0 deletions
diff --git a/ydb/library/analytics/all.h b/ydb/library/analytics/all.h new file mode 100644 index 00000000000..c3defbba4be --- /dev/null +++ b/ydb/library/analytics/all.h @@ -0,0 +1,10 @@ +#pragma once + +#include "asciiart_output.h" +#include "csv_output.h" +#include "data.h" +#include "html_output.h" +#include "json_output.h" +#include "protobuf.h" +#include "transform.h" +#include "util.h" diff --git a/ydb/library/analytics/analytics.cpp b/ydb/library/analytics/analytics.cpp new file mode 100644 index 00000000000..1b25263386b --- /dev/null +++ b/ydb/library/analytics/analytics.cpp @@ -0,0 +1,5 @@ +#include "all.h" + +namespace NAnalytics { + +} diff --git a/ydb/library/analytics/asciiart_output.h b/ydb/library/analytics/asciiart_output.h new file mode 100644 index 00000000000..5b7398700ad --- /dev/null +++ b/ydb/library/analytics/asciiart_output.h @@ -0,0 +1,256 @@ +#pragma once + +#include <cmath> +#include <util/string/printf.h> +#include <util/stream/str.h> +#include <util/generic/set.h> +#include <util/generic/map.h> +#include "data.h" +#include "util.h" + +namespace NAnalytics { + +struct TAxis { + TString Name; + double From; + double To; + char Symbol; + bool Ticks; + + TAxis(const TString& name, double from, double to, char symbol = '+', bool ticks = true) + : Name(name) + , From(from) + , To(to) + , Symbol(symbol) + , Ticks(ticks) + {} + + double Place(double value) const + { + return (value - From) / (To - From); + } + + bool Get(const TRow& row, double& value) const + { + return row.Get(Name, value); + } +}; + +struct TChart { + int Width; + int Height; + bool Frame; + + TChart(int width, int height, bool frame = true) + : Width(width) + , Height(height) + , Frame(frame) + { + if (Width < 2) + Width = 2; + if (Height < 2) + Height = 2; + } +}; + +struct TPoint { + int X; + int Y; + char* Pixel; +}; + +class TScreen { +public: + TChart Chart; + TAxis Ox; + TAxis Oy; + TVector<char> Screen; +public: + TScreen(const TChart& chart, const TAxis& ox, const TAxis& oy) + : Chart(chart) + , Ox(ox) + , Oy(oy) + , Screen(Chart.Width * Chart.Height, ' ') + {} + + int X(double x) const + { + return llrint(Ox.Place(x) * (Chart.Width - 1)); + } + + int Y(double y) const + { + return Chart.Height - 1 - llrint(Oy.Place(y) * (Chart.Height - 1)); + } + + TPoint At(double x, double y) + { + TPoint pt{X(x), Y(y), nullptr}; + if (Fits(pt)) { + pt.Pixel = &Screen[pt.Y * Chart.Width + pt.X]; + } + return pt; + } + + bool Fits(TPoint pt) const + { + return pt.X >= 0 && pt.X < Chart.Width + && pt.Y >= 0 && pt.Y < Chart.Height; + } + + TString Str() const + { + TStringStream ss; + size_t i = 0; + TString lmargin; + TString x1label; + TString x2label; + TString xtick = "-"; + TString y1label; + TString y2label; + TString ytick = "|"; + if (Ox.Ticks) { + x1label = Sprintf("%-7.2le", Ox.From); + x2label = Sprintf("%-7.2le", Ox.To); + xtick = "+"; + } + if (Oy.Ticks) { + y1label = Sprintf("%-7.2le ", Oy.From); + y2label = Sprintf("%-7.2le ", Oy.To); + int sz = Max(y1label.size(), y2label.size()); + y1label = TString(sz - y1label.size(), ' ') + y1label; + y2label = TString(sz - y2label.size(), ' ') + y2label; + lmargin = TString(sz, ' '); + ytick = "+"; + } + if (Chart.Frame) { + ss << lmargin << "." << TString(Chart.Width, '-') << ".\n"; + } + for (int iy = 0; iy < Chart.Height; iy++) { + if (iy == 0) { + ss << y2label; + if (Chart.Frame) + ss << ytick; + } else if (iy == Chart.Height - 1) { + ss << y1label; + if (Chart.Frame) + ss << ytick; + } else { + ss << lmargin; + if (Chart.Frame) + ss << "|"; + } + for (int ix = 0; ix < Chart.Width; ix++) + ss << Screen[i++]; + if (Chart.Frame) + ss << "|"; + ss << "\n"; + } + if (Chart.Frame) { + ss << lmargin << "'" << xtick + << TString(Chart.Width - 2, '-') + << xtick << "'\n"; + } + if (Ox.Ticks) { + ss << lmargin << " " << x1label + << TString(Max(Chart.Width - int(x1label.size()) - int(x2label.size()), 1), ' ') + << x2label << "\n"; + } + return ss.Str(); + } +}; + +class TLegend { +public: + TMap<TString, char> Symbols; + char DefSymbol = '+'; +public: + void Register(const TString& name) + { + if (name) + Symbols[name] = DefSymbol; + } + + void Build() + { + char c = 'A'; + for (auto& kv : Symbols) { + if (!kv.first) + continue; + kv.second = c; + if (c == '9') + c = 'a'; + else if (c == 'z') + c = 'A'; + else if (c == 'Z') + c = '1'; + else + c++; + } + } + + char Get(const TString& name) const + { + auto iter = Symbols.find(name); + return iter != Symbols.end()? iter->second: DefSymbol; + } + + TString Str(size_t columns) const + { + if (columns == 0) + columns = 1; + size_t height = (Symbols.size() + columns - 1) / columns; + TVector<TString> all; + TVector<size_t> widths; + size_t width = 0; + size_t count = 0; + for (auto kv : Symbols) { + TString s = Sprintf("(%ld) %c = %s", count + 1, kv.second, kv.first.data()); + width = Max(width, s.size()); + all.push_back(s); + if (++count % height == 0) { + widths.push_back(width); + width = 0; + } + } + widths.push_back(width); + + TStringStream ss; + for (size_t row = 0; row < height; ++row) { + bool first = true; + for (size_t col = 0; col < widths.size(); col++) { + size_t idx = col * height + row; + if (idx < all.size()) { + ss << (first? "": " ") << Sprintf("%-*s", (int)widths[col], all[idx].data()); + first = false; + } + } + ss << Endl; + } + return ss.Str(); + } +}; + +inline TString PlotAsAsciiArt(const TTable& in, const TChart& chart, const TAxis& ox, const TAxis& oy, bool showLegend = true, size_t columns = 4) +{ + TLegend legend; + legend.DefSymbol = oy.Symbol; + for (const TRow& row : in) { + legend.Register(row.Name); + } + legend.Build(); + + TScreen screen(chart, ox, oy); + for (const TRow& row : in) { + double x, y; + if (ox.Get(row, x) && oy.Get(row, y)) { + TPoint pt = screen.At(Finitize(x), Finitize(y)); + if (pt.Pixel) { + *pt.Pixel = legend.Get(row.Name); + } + } + } + return screen.Str() + (showLegend? TString("\n") + legend.Str(columns): TString()); +} + +} diff --git a/ydb/library/analytics/csv_output.h b/ydb/library/analytics/csv_output.h new file mode 100644 index 00000000000..3b767b454c5 --- /dev/null +++ b/ydb/library/analytics/csv_output.h @@ -0,0 +1,52 @@ +#pragma once + +#include <util/string/printf.h> +#include <util/stream/str.h> +#include <util/generic/set.h> +#include "data.h" + +namespace NAnalytics { + +inline TString ToCsv(const TTable& in, TString sep = TString("\t"), bool head = true) +{ + TSet<TString> cols; + bool hasName = false; + for (const TRow& row : in) { + hasName = hasName || !row.Name.empty(); + for (const auto& kv : row) { + cols.insert(kv.first); + } + } + + TStringStream ss; + if (head) { + bool first = true; + if (hasName) { + ss << (first? TString(): sep) << "Name"; + first = false; + } + for (const TString& c : cols) { + ss << (first? TString(): sep) << c; + first = false; + } + ss << Endl; + } + + for (const TRow& row : in) { + bool first = true; + if (hasName) { + ss << (first? TString(): sep) << row.Name; + first = false; + } + for (const TString& c : cols) { + ss << (first? TString(): sep); + first = false; + double value; + ss << (row.Get(c, value)? Sprintf("%le", value) : TString("-")); + } + ss << Endl; + } + return ss.Str(); +} + +} diff --git a/ydb/library/analytics/data.h b/ydb/library/analytics/data.h new file mode 100644 index 00000000000..fe544c72734 --- /dev/null +++ b/ydb/library/analytics/data.h @@ -0,0 +1,76 @@ +#pragma once + +#include <util/generic/string.h> +#include <util/generic/hash.h> +#include <util/generic/vector.h> + +namespace NAnalytics { + +struct TRow : public THashMap<TString, double> { + TString Name; + + bool Get(const TString& name, double& value) const + { + if (name == "_count") { // Special values + value = 1.0; + return true; + } + auto iter = find(name); + if (iter != end()) { + value = iter->second; + return true; + } else { + return false; + } + } +}; + +using TAttributes = THashMap<TString, TString>; + +struct TTable : public TVector<TRow> { + TAttributes Attributes; +}; + +struct TMatrix : public TVector<double> { + size_t Rows; + size_t Cols; + + explicit TMatrix(size_t rows = 0, size_t cols = 0) + : TVector<double>(rows * cols) + , Rows(rows) + , Cols(cols) + {} + + void Reset(size_t rows, size_t cols) + { + Rows = rows; + Cols = cols; + clear(); + resize(rows * cols); + } + + double& Cell(size_t row, size_t col) + { + Y_ABORT_UNLESS(row < Rows); + Y_ABORT_UNLESS(col < Cols); + return operator[](row * Cols + col); + } + + double Cell(size_t row, size_t col) const + { + Y_ABORT_UNLESS(row < Rows); + Y_ABORT_UNLESS(col < Cols); + return operator[](row * Cols + col); + } + + double CellSum() const + { + double sum = 0.0; + for (double x : *this) { + sum += x; + } + return sum; + } +}; + +} diff --git a/ydb/library/analytics/html_output.h b/ydb/library/analytics/html_output.h new file mode 100644 index 00000000000..bdc6213a314 --- /dev/null +++ b/ydb/library/analytics/html_output.h @@ -0,0 +1,86 @@ +#pragma once + +#include <util/string/printf.h> +#include <util/stream/str.h> +#include <util/generic/set.h> +#include "data.h" + +namespace NAnalytics { + +inline TString ToHtml(const TTable& in) +{ + TSet<TString> cols; + bool hasName = false; + for (const TRow& row : in) { + hasName = hasName || !row.Name.empty(); + for (const auto& kv : row) { + cols.insert(kv.first); + } + } + + TStringStream ss; + ss << "<table>"; + ss << "<thead><tr>"; + if (hasName) { + ss << "<th>Name</th>"; + } + for (const TString& c : cols) { + ss << "<th>" << c << "</th>"; + } + ss << "</tr></thead><tbody>"; + + for (const TRow& row : in) { + ss << "<tr>"; + if (hasName) { + ss << "<th>" << row.Name << "</th>"; + } + for (const TString& c : cols) { + double value; + ss << "<td>" << (row.Get(c, value)? Sprintf("%lf", value) : TString("-")) << "</td>"; + } + ss << "</tr>"; + } + ss << "</tbody></table>"; + + return ss.Str(); +} + +inline TString ToTransposedHtml(const TTable& in) +{ + TSet<TString> cols; + bool hasName = false; + for (const TRow& row : in) { + hasName = hasName || !row.Name.empty(); + for (const auto& kv : row) { + cols.insert(kv.first); + } + } + + TStringStream ss; + ss << "<table><thead>"; + if (hasName) { + ss << "<tr>"; + ss << "<th>Name</th>"; + for (const TRow& row : in) { + ss << "<th>" << row.Name << "</th>"; + } + ss << "</tr>"; + } + + ss << "</thead><tbody>"; + + for (const TString& c : cols) { + ss << "<tr>"; + ss << "<th>" << c << "</th>"; + for (const TRow& row : in) { + double value; + ss << "<td>" << (row.Get(c, value)? Sprintf("%lf", value) : TString("-")) << "</td>"; + } + ss << "</tr>"; + } + ss << "</tbody></table>"; + + return ss.Str(); +} + +} diff --git a/ydb/library/analytics/json_output.h b/ydb/library/analytics/json_output.h new file mode 100644 index 00000000000..189f9802d3c --- /dev/null +++ b/ydb/library/analytics/json_output.h @@ -0,0 +1,98 @@ +#pragma once + +#include <util/string/printf.h> +#include <util/stream/str.h> +#include <util/string/vector.h> +#include <util/generic/set.h> +#include <util/generic/hash_set.h> +#include "data.h" +#include "util.h" + +namespace NAnalytics { + +inline TString ToJsonFlot(const TTable& in, const TString& xno, const TVector<TString>& ynos, const TString& opts = TString()) +{ + TStringStream ss; + ss << "[ "; + bool first = true; + + TString xn; + THashSet<TString> xopts; + ParseNameAndOpts(xno, xn, xopts); + bool xstack = xopts.contains("stack"); + + for (const TString& yno : ynos) { + TString yn; + THashSet<TString> yopts; + ParseNameAndOpts(yno, yn, yopts); + bool ystackOpt = yopts.contains("stack"); + + ss << (first? "": ",\n ") << "{ " << opts << (opts? ", ": "") << "\"label\": \"" << yn << "\", \"data\": [ "; + bool first2 = true; + using TPt = std::tuple<double, double, TString>; + std::vector<TPt> pts; + for (const TRow& row : in) { + double x, y; + if (row.Get(xn, x) && row.Get(yn, y)) { + pts.emplace_back(x, y, row.Name); + } + } + + if (xstack) { + std::sort(pts.begin(), pts.end(), [] (const TPt& a, const TPt& b) { + // At first sort by Name, then by x, then by y + return std::make_tuple(std::get<2>(a), std::get<0>(a), std::get<1>(a)) < + std::make_tuple(std::get<2>(b), std::get<0>(b), std::get<1>(b)); + }); + } else { + std::sort(pts.begin(), pts.end()); + } + + double x = 0.0, xsum = 0.0; + double y = 0.0, ysum = 0.0; + for (auto& pt : pts) { + if (xstack) { + x = xsum; + xsum += std::get<0>(pt); + } else { + x = std::get<0>(pt); + } + + if (ystackOpt) { + y = ysum; + ysum += std::get<1>(pt); + } else { + y = std::get<1>(pt); + } + + ss << (first2? "": ", ") << "[" + << Sprintf("%.6lf", Finitize(x)) << ", " // x coordinate + << Sprintf("%.6lf", Finitize(y)) << ", " // y coordinate + << "\"" << std::get<2>(pt) << "\", " // label + << Sprintf("%.6lf", std::get<0>(pt)) << ", " // x label (real value) + << Sprintf("%.6lf", std::get<1>(pt)) // y label (real value) + << "]"; + first2 = false; + } + // Add final point + if (!first2 && (xstack || ystackOpt)) { + if (xstack) + x = xsum; + if (ystackOpt) + y = ysum; + ss << (first2? "": ", ") << "[" + << Sprintf("%.6lf", Finitize(x)) << ", " // x coordinate + << Sprintf("%.6lf", Finitize(y)) << ", " // y coordinate + << "\"\", " + << Sprintf("%.6lf", x) << ", " // x label (real value) + << Sprintf("%.6lf", y) // y label (real value) + << "]"; + } + ss << " ] }"; + first = false; + } + ss << "\n]"; + return ss.Str(); +} + +} diff --git a/ydb/library/analytics/protobuf.h b/ydb/library/analytics/protobuf.h new file mode 100644 index 00000000000..202bd54c717 --- /dev/null +++ b/ydb/library/analytics/protobuf.h @@ -0,0 +1,49 @@ +#pragma once + +#include "data.h" +#include <ydb/library/analytics/protos/data.pb.h> + +namespace NAnalytics { + +inline void ToProtoBuf(const TTable& in, TTableData* tableData) +{ + for (const TRow& row : in) { + TRowData* rowData = tableData->AddRows(); + if (row.Name) { + rowData->SetName(row.Name); + } + for (const auto& kv : row) { + TFieldData* fieldData = rowData->AddFields(); + fieldData->SetKey(kv.first); + fieldData->SetValue(kv.second); + } + } + for (const auto& av : in.Attributes) { + TAttributeData* attrData = tableData->AddAttributes(); + attrData->SetAttribute(av.first); + attrData->SetValue(av.second); + } +} + +inline TTable FromProtoBuf(const TTableData& tableData) +{ + TTable table; + for (int i = 0; i < tableData.GetAttributes().size(); i++) { + const TAttributeData& attrData = tableData.GetAttributes(i); + table.Attributes[attrData.GetAttribute()] = attrData.GetValue(); + } + + for (int i = 0; i < tableData.GetRows().size(); i++) { + const TRowData& rowData = tableData.GetRows(i); + table.push_back(TRow()); + TRow& row = table.back(); + row.Name = rowData.GetName(); + for (int j = 0; j < rowData.GetFields().size(); j++) { + const TFieldData& fieldData = rowData.GetFields(j); + row[fieldData.GetKey()] = fieldData.GetValue(); + } + } + return table; +} + +} diff --git a/ydb/library/analytics/protos/data.proto b/ydb/library/analytics/protos/data.proto new file mode 100644 index 00000000000..9e500c9b026 --- /dev/null +++ b/ydb/library/analytics/protos/data.proto @@ -0,0 +1,21 @@ +package NAnalytics; + +message TFieldData { + optional string Key = 1; + optional double Value = 2; +} + +message TRowData { + optional string Name = 1; + repeated TFieldData Fields = 2; +} + +message TAttributeData { + optional string Attribute = 1; + optional string Value = 2; +} + +message TTableData { + repeated TRowData Rows = 1; + repeated TAttributeData Attributes = 2; +} diff --git a/ydb/library/analytics/protos/ya.make b/ydb/library/analytics/protos/ya.make new file mode 100644 index 00000000000..588646b21b5 --- /dev/null +++ b/ydb/library/analytics/protos/ya.make @@ -0,0 +1,11 @@ +PROTO_LIBRARY() + +SUBSCRIBER(g:kikimr) + +SRCS( + data.proto +) + +EXCLUDE_TAGS(GO_PROTO) + +END() diff --git a/ydb/library/analytics/transform.h b/ydb/library/analytics/transform.h new file mode 100644 index 00000000000..29dfba7b744 --- /dev/null +++ b/ydb/library/analytics/transform.h @@ -0,0 +1,192 @@ +#pragma once + +#include "data.h" + +namespace NAnalytics { + +template <class TSkip, class TX, class TY> +inline TTable Histogram(const TTable& in, TSkip skip, + const TString& xn_out, TX x_in, + const TString& yn_out, TY y_in, + double x1, double x2, double dx) +{ + long buckets = (x2 - x1) / dx; + TTable out; + TString yn_sum = yn_out + "_sum"; + TString yn_share = yn_out + "_share"; + double ysum = 0.0; + out.resize(buckets); + for (size_t i = 0; i < out.size(); i++) { + double lb = x1 + dx*i; + double ub = lb + dx; + out[i].Name = "[" + ToString(lb) + ";" + ToString(ub) + (ub==x2? "]": ")"); + out[i][xn_out] = (lb + ub) / 2; + out[i][yn_sum] = 0.0; + } + for (const auto& row : in) { + if (skip(row)) { + continue; + } + double x = x_in(row); + long i = (x - x1) / dx; + if (x == x2) { // Special hack to include right edge + i--; + } + double y = y_in(row); + ysum += y; + if (i >= 0 && i < buckets) { + out[i][yn_sum] += y; + } + } + for (TRow& row : out) { + if (ysum != 0.0) { + row[yn_share] = row[yn_sum] / ysum; + } + } + return out; +} + +inline TTable HistogramAll(const TTable& in, const TString& xn, double x1, double x2, double dx) +{ + long buckets = (dx == 0.0? 1: (x2 - x1) / dx); + TTable out; + THashMap<TString, double> colSum; + out.resize(buckets); + + TSet<TString> cols; + for (auto& row : in) { + for (auto& kv : row) { + cols.insert(kv.first); + } + } + cols.insert("_count"); + cols.erase(xn); + + for (const TString& col : cols) { + colSum[col] = 0.0; + } + + for (size_t i = 0; i < out.size(); i++) { + double lb = x1 + dx*i; + double ub = lb + dx; + TRow& row = out[i]; + row.Name = "[" + ToString(lb) + ";" + ToString(ub) + (ub==x2? "]": ")"); + row[xn] = (lb + ub) / 2; + for (const TString& col : cols) { + row[col + "_sum"] = 0.0; + } + } + for (const TRow& row_in : in) { + double x; + if (!row_in.Get(xn, x)) { + continue; + } + long i = (dx == 0.0? 0: (x - x1) / dx); + if (x == x2 && dx > 0.0) { // Special hack to include right edge + i--; + } + for (const auto& kv : row_in) { + const TString& yn = kv.first; + if (yn == xn) { + continue; + } + double y = kv.second; + colSum[yn] += y; + if (i >= 0 && i < buckets) { + out[i][yn + "_cnt"]++; + out[i][yn + "_sum"] += y; + if (out[i].contains(yn + "_min")) { + out[i][yn + "_min"] = Min(y, out[i][yn + "_min"]); + } else { + out[i][yn + "_min"] = y; + } + if (out[i].contains(yn + "_max")) { + out[i][yn + "_max"] = Max(y, out[i][yn + "_max"]); + } else { + out[i][yn + "_max"] = y; + } + } + } + colSum["_count"]++; + if (i >= 0 && i < buckets) { + out[i]["_count_sum"]++; + } + } + for (TRow& row : out) { + for (const TString& col : cols) { + double ysum = colSum[col]; + if (col != "_count") { + if (row[col + "_cnt"] != 0.0) { + row[col + "_avg"] = row[col + "_sum"] / row[col + "_cnt"]; + } + } + if (ysum != 0.0) { + row[col + "_share"] = row[col + "_sum"] / ysum; + } + } + } + return out; +} + +inline TMatrix CovarianceMatrix(const TTable& in) +{ + TSet<TString> cols; + for (auto& row : in) { + for (auto& kv : row) { + cols.insert(kv.first); + } + } + + struct TAggregate { + size_t Idx = 0; + double Sum = 0; + size_t Count = 0; + double Mean = 0; + }; + + THashMap<TString, TAggregate> colAggr; + + size_t colCount = 0; + for (const TString& col : cols) { + TAggregate& aggr = colAggr[col]; + aggr.Idx = colCount++; + } + + for (const TRow& row : in) { + for (const auto& kv : row) { + const TString& xn = kv.first; + double x = kv.second; + TAggregate& aggr = colAggr[xn]; + aggr.Sum += x; + aggr.Count++; + } + } + + for (auto& kv : colAggr) { + TAggregate& aggr = kv.second; + aggr.Mean = aggr.Sum / aggr.Count; + } + + TMatrix covCount(cols.size(), cols.size()); + TMatrix cov(cols.size(), cols.size()); + for (const TRow& row : in) { + for (const auto& kv1 : row) { + double x = kv1.second; + TAggregate& xaggr = colAggr[kv1.first]; + for (const auto& kv2 : row) { + double y = kv2.second; + TAggregate& yaggr = colAggr[kv2.first]; + covCount.Cell(xaggr.Idx, yaggr.Idx)++; + cov.Cell(xaggr.Idx, yaggr.Idx) += (x - xaggr.Mean) * (y - yaggr.Mean); + } + } + } + + for (size_t idx = 0; idx < cov.size(); idx++) { + cov[idx] /= covCount[idx]; + } + + return cov; +} + +} diff --git a/ydb/library/analytics/util.h b/ydb/library/analytics/util.h new file mode 100644 index 00000000000..e07d06cc43f --- /dev/null +++ b/ydb/library/analytics/util.h @@ -0,0 +1,122 @@ +#pragma once + +#include "data.h" +#include <util/generic/algorithm.h> +#include <util/generic/hash_set.h> +#include <util/string/vector.h> + +namespace NAnalytics { + +// Get rid of NaNs and INFs +inline double Finitize(double x, double notFiniteValue = 0.0) +{ + return isfinite(x)? x: notFiniteValue; +} + +inline void ParseNameAndOpts(const TString& nameAndOpts, TString& name, THashSet<TString>& opts) +{ + name.clear(); + opts.clear(); + bool first = true; + auto vs = SplitString(nameAndOpts, "-"); + for (const auto& s : vs) { + if (first) { + name = s; + first = false; + } else { + opts.insert(s); + } + } +} + +inline TString ParseName(const TString& nameAndOpts) +{ + auto vs = SplitString(nameAndOpts, "-"); + if (vs.empty()) { + return TString(); + } else { + return vs[0]; + } +} + +template <class R, class T> +inline R AccumulateIfExist(const TString& name, const TTable& table, R r, T t) +{ + ForEach(table.begin(), table.end(), [=,&r] (const TRow& row) { + double value; + if (row.Get(name, value)) { + r = t(r, value); + } + }); + return r; +} + +inline double MinValue(const TString& nameAndOpts, const TTable& table) +{ + TString name; + THashSet<TString> opts; + ParseNameAndOpts(nameAndOpts, name, opts); + bool stack = opts.contains("stack"); + if (stack) { + return 0.0; + } else { + auto zero = 0.0; + + return AccumulateIfExist(name, table, 1.0 / zero /*+inf*/, [] (double x, double y) { + return Min(x, y); + }); + } +} + +inline double MaxValue(const TString& nameAndOpts, const TTable& table) +{ + TString name; + THashSet<TString> opts; + ParseNameAndOpts(nameAndOpts, name, opts); + bool stack = opts.contains("stack"); + if (stack) { + return AccumulateIfExist(name, table, 0.0, [] (double x, double y) { + return x + y; + }); + } else { + auto zero = 0.0; + + return AccumulateIfExist(name, table, -1.0 / zero /*-inf*/, [] (double x, double y) { + return Max(x, y); + }); + } +} + +template <class T> +inline void Map(TTable& table, const TString& rname, T t) +{ + ForEach(table.begin(), table.end(), [=] (TRow& row) { + row[rname] = t(row); + }); +} + +inline std::function<bool(const TRow&)> HasNoValueFor(TString name) +{ + return [=] (const TRow& row) -> bool { + double value; + return !row.Get(name, value); + }; +} + + +inline std::function<double(const TRow&)> GetValueFor(TString name, double defVal = 0.0) +{ + return [=] (const TRow& row) -> double { + double value; + return row.Get(name, value)? value: defVal; + }; +} + +inline std::function<double(const TRow&)> Const(double defVal = 0.0) +{ + return [=] (const TRow&) { + return defVal; + }; +} + +} diff --git a/ydb/library/analytics/ya.make b/ydb/library/analytics/ya.make new file mode 100644 index 00000000000..5c20a3f42c2 --- /dev/null +++ b/ydb/library/analytics/ya.make @@ -0,0 +1,15 @@ +LIBRARY() + +PEERDIR( + ydb/library/analytics/protos +) + +SRCS( + analytics.cpp +) + +END() + +RECURSE( + protos +) diff --git a/ydb/library/drr/drr.cpp b/ydb/library/drr/drr.cpp new file mode 100644 index 00000000000..7f948daba88 --- /dev/null +++ b/ydb/library/drr/drr.cpp @@ -0,0 +1,5 @@ +#include "drr.h" + +namespace NScheduling { + +} diff --git a/ydb/library/drr/drr.h b/ydb/library/drr/drr.h new file mode 100644 index 00000000000..601eda29ae8 --- /dev/null +++ b/ydb/library/drr/drr.h @@ -0,0 +1,363 @@ +#pragma once + +#include <util/system/yassert.h> +#include <util/system/types.h> +#include <util/generic/ptr.h> +#include <util/generic/hash.h> +#include <util/generic/typetraits.h> +#include <util/generic/string.h> +#include <ydb/library/planner/base/defs.h> +#include "probes.h" + +namespace NScheduling { + +class TDRRSchedulerBase; +template <class TQueueType, class TId = TString> class TDRRScheduler; + +class TDRRQueue { +private: + TDRRSchedulerBase* Scheduler = nullptr; + TWeight Weight; + TUCost DeficitCounter; + TUCost QuantumFraction = 0; + TUCost MaxBurst; + // Active list (cyclic double-linked list of active queues for round-robin to cycle through) + TDRRQueue* ActNext = nullptr; + TDRRQueue* ActPrev = nullptr; + TString Name; +public: + TDRRQueue(TWeight w = 1, TUCost maxBurst = 0, const TString& name = TString()) + : Weight(w) + , DeficitCounter(maxBurst) + , MaxBurst(maxBurst) + , Name(name) + {} + + TDRRSchedulerBase* GetScheduler() { return Scheduler; } + TWeight GetWeight() const { return Weight; } + TUCost GetDeficitCounter() const { return DeficitCounter; } + TUCost GetQuantumFraction() const { return QuantumFraction; } + const TString& GetName() const { return Name; } + bool IsActive() const { return ActNext != nullptr; } +protected: + void OnSchedulerAttach() {} // To be overriden in derived class + void OnSchedulerDetach() {} // To be overriden in derived class +private: // For TDRRScheduler + friend class TDRRSchedulerBase; + template <class Q, class I> friend class TDRRScheduler; + + void SetScheduler(TDRRSchedulerBase* scheduler) { Scheduler = scheduler; } + void SetWeight(TWeight w) { Y_ABORT_UNLESS(w != 0, "zero weight in queue '%s'", Name.data()); Weight = w; } + void SetDeficitCounter(TUCost c) { DeficitCounter = c; } + void SetQuantumFraction(TUCost c) { QuantumFraction = c; } + + TDRRQueue* GetActNext() { return ActNext; } + void SetActNext(TDRRQueue* v) { ActNext = v; } + TDRRQueue* GetActPrev() { return ActPrev; } + void SetActPrev(TDRRQueue* v) { ActPrev = v; } + + void AddCredit(TUCost c) + { + DeficitCounter += c; + } + + void UseCredit(TUCost c) + { + if (DeficitCounter > c) { + DeficitCounter -= c; + } else { + DeficitCounter = 0; + } + } + + bool CreditOverflow() const + { + return DeficitCounter >= MaxBurst; + } + + void FixCreditOverflow() + { + DeficitCounter = Min(MaxBurst, DeficitCounter); + } +}; + +class TDRRSchedulerBase: public TDRRQueue { +protected: + TUCost Quantum; + TWeight AllWeight = 0; + TWeight ActWeight = 0; + TDRRQueue* CurQueue = nullptr; + TDRRQueue* LastQueue = nullptr; + void* Peek = nullptr; +public: + TDRRSchedulerBase(TUCost quantum, TWeight w = 1, TUCost maxBurst = 0, const TString& name = TString()) + : TDRRQueue(w, maxBurst, name) + , Quantum(quantum) + {} + + // Must be called when queue becomes backlogged + void ActivateQueue(TDRRQueue* queue) + { + if (queue->IsActive()) { + return; + } + DRR_PROBE(ActivateQueue, GetName(), queue->GetName()); + ActWeight += queue->GetWeight(); + if (CurQueue == nullptr) { + // First active queue + queue->SetActNext(queue); + queue->SetActPrev(queue); + CurQueue = queue; + if (GetScheduler()) { + GetScheduler()->ActivateQueue(this); + } + } else { + // Add queue to the tail of active list to avoid unfairness due to + // otherwise possible multiple reorderings in a single round + TDRRQueue* after = CurQueue->GetActPrev(); + queue->SetActNext(CurQueue); + queue->SetActPrev(after); + CurQueue->SetActPrev(queue); + after->SetActNext(queue); + } + } + + // Must be called after each push to queue front + void DropCache(TDRRQueue* queue) + { + DRR_PROBE(DropCache, GetName(), queue->GetName()); + if (CurQueue == queue && Peek) { + DRR_PROBE(CacheDropped, GetName(), queue->GetName()); + Peek = nullptr; + if (GetScheduler()) { + GetScheduler()->DropCache(this); + } + } + } +}; + +// Deficit Weighted Round Robin scheduling algorithm +// http://en.wikipedia.org/wiki/Deficit_round_robin +// TODO(serxa): add TSharingPolicy (as template parameter that is inherited) to control Quantum splitting +// TODO(serxa): add support for non-zero DeficitCounter while idle +template <class TQueueType, class TId> +class TDRRScheduler: public TDRRSchedulerBase { + static_assert(std::is_base_of<TDRRQueue, TQueueType>::value, "TQueueType must inherit TDRRQueue"); +private: + typedef std::shared_ptr<TQueueType> TQueuePtr; + typedef THashMap<TId, TQueuePtr> TAllQueues; + TAllQueues AllQueues; +public: + typedef typename TQueueType::TTask TTask; + explicit TDRRScheduler(TUCost quantum, TWeight w = 1, TUCost maxBurst = 0, const TString& name = TString()) + : TDRRSchedulerBase(quantum, w, maxBurst, name) + {} + + template <class Func> + void ForEachQueue(Func func) const { + for (auto kv : AllQueues) { + func(kv.first, kv.second.Get()); + } + } + + // Add an empty queue + // NOTE: if queue is not empty ActivateQueue() must be called after Add() + bool AddQueue(typename TTypeTraits<TId>::TFuncParam id, TQueuePtr queue) + { + if (AllQueues.contains(id)) { + return false; + } + queue->SetScheduler(this); + AllQueues.insert(std::make_pair(id, queue)); + AllWeight += queue->GetWeight(); + UpdateQuantums(); + queue->OnSchedulerAttach(); + return true; + } + + // Delete queue by Id + void DeleteQueue(typename TTypeTraits<TId>::TFuncParam id) + { + typename TAllQueues::iterator i = AllQueues.find(id); + if (i != AllQueues.end()) { + TQueueType* queue = i->second.get(); + queue->OnSchedulerDetach(); + if (queue->IsActive()) { + DeactivateQueue(queue); + } + queue->SetScheduler(nullptr); + AllWeight -= queue->GetWeight(); + AllQueues.erase(id); + } + } + + // Get queue by Id + TQueuePtr GetQueue(typename TTypeTraits<TId>::TFuncParam id) + { + typename TAllQueues::iterator i = AllQueues.find(id); + if (i != AllQueues.end()) { + return i->second; + } else { + return TQueuePtr(); + } + } + + // Get queue by Id + TQueueType* GetQueuePtr(typename TTypeTraits<TId>::TFuncParam id) + { + typename TAllQueues::iterator i = AllQueues.find(id); + if (i != AllQueues.end()) { + return i->second.Get(); + } else { + return nullptr; + } + } + + // Update queue weight + void UpdateQueueWeight(TQueueType* queue, TWeight w) + { + if (w) { + AllWeight -= queue->GetWeight(); + if (queue->IsActive()) { + ActWeight -= queue->GetWeight(); + } + queue->SetWeight(w); + if (queue->IsActive()) { + ActWeight += w; + } + AllWeight += w; + UpdateQuantums(); + } + } + + TQueueType* GetCurQueue() + { + return static_cast<TQueueType*>(CurQueue); + } + + TTask* GetPeek() + { + return static_cast<TTask*>(Peek); + } + + // Runs scheduling algorithm and returns task to be served next or nullptr if empty + TTask* PeekTask() + { + if (!Peek) { + if (!CurQueue) { + LastQueue = nullptr; + return nullptr; + } + + if (!LastQueue) { + GiveCredit(); // For first round at start of backlogged period + } + while (GetCurQueue()->Empty() || GetCurQueue()->PeekTask()->GetCost() > CurQueue->GetDeficitCounter()) { + if (!NextNonEmptyQueue()) { + // If all queue was deactivated due to credit overflow + LastQueue = nullptr; + return nullptr; + } + } + Peek = GetCurQueue()->PeekTask(); + } + return GetPeek(); + } + + // Pops peek task from queue + void PopTask() + { + if (!PeekTask()) { + return; // No more tasks + } + + // Pop peek task from current queue and use credit + CurQueue->UseCredit(GetPeek()->GetCost()); + Peek = nullptr; +// Cerr << "pop\t" << (ui64)CurQueue << "\tDC:" << CurQueue->DeficitCounter << Endl; + GetCurQueue()->PopTask(); + + LastQueue = CurQueue; + } + + bool Empty() + { + PeekTask(); + return !CurQueue; + } + +private: + void GiveCredit() + { + // Maintain given Quantum to be split to exactly one round of active queues + CurQueue->AddCredit(CurQueue->GetQuantumFraction()*AllWeight/ActWeight); +// Cerr << "credit\t" << (ui64)CurQueue << "\tplus:" << CurQueue->GetQuantumFraction()*AllWeight/ActWeight +// << "\tDC:" << CurQueue->DeficitCounter << Endl; + } + + bool NextNonEmptyQueue(TDRRQueue* next = nullptr) + { + while (true) { + CurQueue = next? next: CurQueue->GetActNext(); + next = nullptr; + GiveCredit(); + if (GetCurQueue()->Empty()) { + if (CurQueue->CreditOverflow()) { + next = DeactivateQueueImpl(CurQueue); + if (!CurQueue) { + return false; // Last empty queue was deactivated due to credit overflow + } + } + continue; + } + break; + } + return true; + } + + void DeactivateQueue(TDRRQueue* queue) + { + if (TDRRQueue* next = DeactivateQueueImpl(queue)) { + NextNonEmptyQueue(next); + } + } + + TDRRQueue* DeactivateQueueImpl(TDRRQueue* queue) + { +// Cerr << "deactivate\t" << (ui64)queue << Endl; + TDRRQueue* ret = nullptr; + ActWeight -= queue->GetWeight(); + Y_ASSERT(queue->IsActive()); + TDRRQueue* before = queue->GetActPrev(); + TDRRQueue* after = queue->GetActNext(); + bool lastActive = (before == queue); + if (lastActive) { + CurQueue = nullptr; + } else { + if (queue == CurQueue) { + ret = CurQueue->GetActNext(); + } + before->SetActNext(after); + after->SetActPrev(before); + } + queue->SetActNext(nullptr); + queue->SetActPrev(nullptr); + + // Keep deficit counter of non-backlogged queues limited by MaxBurst + // to exclude accumulation of resource while being idle + queue->FixCreditOverflow(); + return ret; + } + + void UpdateQuantums() + { + TUCost qsum = Quantum; + TWeight wsum = AllWeight; + for (typename TAllQueues::iterator i = AllQueues.begin(), e = AllQueues.end(); i != e; ++i) { + TQueueType* qi = i->second.get(); + qi->SetQuantumFraction(WCut(qi->GetWeight(), wsum, qsum)); + } + } +}; + +} diff --git a/ydb/library/drr/probes.cpp b/ydb/library/drr/probes.cpp new file mode 100644 index 00000000000..4433d18d0d2 --- /dev/null +++ b/ydb/library/drr/probes.cpp @@ -0,0 +1,3 @@ +#include "probes.h" + +LWTRACE_DEFINE_PROVIDER(SCHEDULING_DRR_PROVIDER) diff --git a/ydb/library/drr/probes.h b/ydb/library/drr/probes.h new file mode 100644 index 00000000000..29861e8ecb2 --- /dev/null +++ b/ydb/library/drr/probes.h @@ -0,0 +1,22 @@ +#pragma once + +#include <library/cpp/lwtrace/all.h> + +#define DRR_PROBE(name, ...) GLOBAL_LWPROBE(SCHEDULING_DRR_PROVIDER, name, ## __VA_ARGS__) + +#define SCHEDULING_DRR_PROVIDER(PROBE, EVENT, GROUPS, TYPES, NAMES) \ + PROBE(ActivateQueue, GROUPS("Scheduling", "Drr"), \ + TYPES(TString,TString), \ + NAMES("parent","queue")) \ + PROBE(DropCache, GROUPS("Scheduling", "Drr"), \ + TYPES(TString,TString), \ + NAMES("parent","queue")) \ + PROBE(CacheDropped, GROUPS("Scheduling", "Drr"), \ + TYPES(TString,TString), \ + NAMES("parent","queue")) \ + PROBE(Info, GROUPS("Scheduling", "DrrDetails"), \ + TYPES(TString,TString), \ + NAMES("drr","info")) \ + /**/ + +LWTRACE_DECLARE_PROVIDER(SCHEDULING_DRR_PROVIDER) diff --git a/ydb/library/drr/ut/drr_ut.cpp b/ydb/library/drr/ut/drr_ut.cpp new file mode 100644 index 00000000000..ae8324539b0 --- /dev/null +++ b/ydb/library/drr/ut/drr_ut.cpp @@ -0,0 +1,581 @@ +#include <ydb/library/drr/drr.h> +#include <library/cpp/threading/future/legacy_future.h> + +#include <util/random/random.h> +#include <util/generic/list.h> +#include <util/string/vector.h> +#include <library/cpp/testing/unittest/registar.h> + +Y_UNIT_TEST_SUITE(SchedulingDRR) { + using namespace NScheduling; + + class TMyQueue; + + class TMyTask { + public: + TUCost Cost; + TMyQueue* Queue; // Needed only for test + public: + TMyTask(TUCost cost) + : Cost(cost) + , Queue(nullptr) + {} + + TUCost GetCost() const + { + return Cost; + } + }; + + class TMyQueue: public TDRRQueue { + public: + typedef TMyTask TTask; + public: + typedef TList<TMyTask*> TTasks; + TString Name; + TTasks Tasks; + public: // Interface for clients + TMyQueue(const TString& name, TWeight w = 1, TUCost maxBurst = 0) + : TDRRQueue(w, maxBurst) + , Name(name) + {} + + ~TMyQueue() + { + for (TTasks::iterator i = Tasks.begin(), e = Tasks.end(); i != e; ++i) { + delete *i; + } + } + + void PushTask(TMyTask* task) + { + task->Queue = this; + if (Tasks.empty()) { + // Scheduler must be notified on first task in queue + if (GetScheduler()) { + GetScheduler()->ActivateQueue(this); + } + } + Tasks.push_back(task); + } + + void PushTaskFront(TMyTask* task) + { + task->Queue = this; + if (Tasks.empty()) { + // Scheduler must be notified on first task in queue + if (GetScheduler()) { + GetScheduler()->ActivateQueue(this); + } + } + Tasks.push_front(task); + if (GetScheduler()) { + GetScheduler()->DropCache(this); + } + } + public: // Interface for scheduler + void OnSchedulerAttach() + { + Y_ASSERT(GetScheduler() != nullptr); + if (!Tasks.empty()) { + GetScheduler()->ActivateQueue(this); + } + } + + TTask* PeekTask() + { + UNIT_ASSERT(!Tasks.empty()); + return Tasks.front(); + } + + void PopTask() + { + UNIT_ASSERT(!Tasks.empty()); + Tasks.pop_front(); + } + + bool Empty() const + { + return Tasks.empty(); + } + }; + + void Generate(TMyQueue* queue, const TString& tasks) + { + TVector<TString> v = SplitString(tasks, " "); + for (size_t i = 0; i < v.size(); i++) { + queue->PushTask(new TMyTask(FromString<TUCost>(v[i]))); + } + } + + void GenerateFront(TMyQueue* queue, const TString& tasks) + { + TVector<TString> v = SplitString(tasks, " "); + for (size_t i = 0; i < v.size(); i++) { + queue->PushTaskFront(new TMyTask(FromString<TUCost>(v[i]))); + } + } + + template <class TDRR> + TString Schedule(TDRR& drr, size_t count = size_t(-1), bool printcost = false) { + TStringStream ss; + while (count--) { + TMyTask* task = drr.PeekTask(); + if (!task) + break; + drr.PopTask(); + ss << task->Queue->Name; + if (printcost) { + ss << task->Cost; + } + delete task; + } + if (count != size_t(-1)) { + UNIT_ASSERT(drr.PeekTask() == nullptr); + } + return ss.Str(); + } + + typedef std::shared_ptr<TMyQueue> TQueuePtr; + + Y_UNIT_TEST(SimpleDRR) { + TDRRScheduler<TMyQueue> drr(100); + + TMyQueue* A; + TMyQueue* B; + drr.AddQueue("A", TQueuePtr(A = new TMyQueue("A"))); + drr.AddQueue("B", TQueuePtr(B = new TMyQueue("B"))); + + Generate(A, "100 100 100"); // 50 + Generate(B, "100 100 100"); // 50 + UNIT_ASSERT_STRINGS_EQUAL(Schedule(drr), "ABABAB"); + + Generate(A, "50 50 50 50 50"); // 50 + Generate(B, "50 50 50 50 50"); // 50 + UNIT_ASSERT_STRINGS_EQUAL(Schedule(drr), "ABABABABAB"); + + Generate(A, "20 20 20 20 20 20 20"); // 50 + Generate(B, "20 20 20 20 20 20 20"); // 50 + UNIT_ASSERT_STRINGS_EQUAL(Schedule(drr), "AABBAAABBBAABB"); + + Generate(A, "20 20 20 20 20 20 20"); // 50 + Generate(B, "50 50 50" ); // 50 + UNIT_ASSERT_STRINGS_EQUAL(Schedule(drr), "AABAAABAAB"); + + Generate(A, " 100 100"); // 50 + Generate(B, "20 20 20 20 20 20 20 "); // 50 + UNIT_ASSERT_STRINGS_EQUAL(Schedule(drr), "BBABBBBBA"); + + TMyQueue* C; + drr.AddQueue("C", TQueuePtr(C = new TMyQueue("C", 2))); + + Generate(A, "20 20 20 20 20 20"); // 25 + Generate(B, "20 20 20 20 20 20"); // 25 + Generate(C, "20 20 20 20 20 20 20 20 20 20 20"); // 50 + UNIT_ASSERT_STRINGS_EQUAL(Schedule(drr), "ABCCABCCCABCCAABBCCCABC"); + + drr.GetQueue("B"); // just to instantiate GetQueue() function from template + + drr.DeleteQueue("A"); + // DRR.DeleteQueue("B"); // scheduler must delete queue be itself + } + + Y_UNIT_TEST(SimpleDRRLag) { + TDRRScheduler<TMyQueue> drr(100); + + TMyQueue* A; + TMyQueue* B; + TMyQueue* C; + TMyQueue* D; + TMyQueue* E; + drr.AddQueue("A", TQueuePtr(A = new TMyQueue("A"))); + drr.AddQueue("B", TQueuePtr(B = new TMyQueue("B"))); + drr.AddQueue("C", TQueuePtr(C = new TMyQueue("C"))); + drr.AddQueue("D", TQueuePtr(D = new TMyQueue("D"))); + drr.AddQueue("E", TQueuePtr(E = new TMyQueue("E"))); + + Generate(A, "500 500 500"); // 25 + Generate(B, "500 500 500"); // 25 + Generate(C, "500 500 500"); // 25 + Generate(D, "500 500 500"); // 25 + UNIT_ASSERT_STRINGS_EQUAL(Schedule(drr, 8), "ABCDABCD"); + Generate(E, "500"); // 100 + UNIT_ASSERT_STRINGS_EQUAL(Schedule(drr), "ABCED"); + } + + Y_UNIT_TEST(SimpleDRRWithOneBursty) { + TDRRScheduler<TMyQueue> drr(100); + + TMyQueue* A; + TMyQueue* B; + TMyQueue* C; + TMyQueue* D; + TMyQueue* E; + drr.AddQueue("A", TQueuePtr(A = new TMyQueue("A"))); + drr.AddQueue("B", TQueuePtr(B = new TMyQueue("B"))); + drr.AddQueue("C", TQueuePtr(C = new TMyQueue("C"))); + drr.AddQueue("D", TQueuePtr(D = new TMyQueue("D"))); + drr.AddQueue("E", TQueuePtr(E = new TMyQueue("E", 1, 500))); + + Generate(A, "500 500 500"); // 25 + Generate(B, "500 500 500"); // 25 + Generate(C, "500 500 500"); // 25 + Generate(D, "500 500 500"); // 25 + UNIT_ASSERT_STRINGS_EQUAL(Schedule(drr, 8), "ABCDABCD"); + Generate(E, "500"); // 100 + UNIT_ASSERT_STRINGS_EQUAL(Schedule(drr), "EABCD"); + + // The same test to avoid effect of first message after add + Generate(A, "500 500 500"); // 25 + Generate(B, "500 500 500"); // 25 + Generate(C, "500 500 500"); // 25 + Generate(D, "500 500 500"); // 25 + UNIT_ASSERT_STRINGS_EQUAL(Schedule(drr, 8), "ABCDABCD"); + Generate(E, "500"); // 20 + UNIT_ASSERT_STRINGS_EQUAL(Schedule(drr), "EABCD"); + } + + Y_UNIT_TEST(SimpleDRRWithAllBursty) { + TDRRScheduler<TMyQueue> drr(100); + + TMyQueue* A; + TMyQueue* B; + TMyQueue* C; + TMyQueue* D; + TMyQueue* E; + drr.AddQueue("A", TQueuePtr(A = new TMyQueue("A", 1, 500))); + drr.AddQueue("B", TQueuePtr(B = new TMyQueue("B", 1, 500))); + drr.AddQueue("C", TQueuePtr(C = new TMyQueue("C", 1, 500))); + drr.AddQueue("D", TQueuePtr(D = new TMyQueue("D", 1, 500))); + drr.AddQueue("E", TQueuePtr(E = new TMyQueue("E", 1, 500))); + + Generate(A, "500 500 500"); // 25 + Generate(B, "500 500 500"); // 25 + Generate(C, "500 500 500"); // 25 + Generate(D, "500 500 500"); // 25 + UNIT_ASSERT_STRINGS_EQUAL(Schedule(drr, 8), "ABCDABCD"); + Generate(E, "500"); // 20 + UNIT_ASSERT_STRINGS_EQUAL(Schedule(drr), "EABCD"); + } + + Y_UNIT_TEST(DoubleDRR) { + typedef TDRRScheduler<TMyQueue> TInnerDRR; + typedef std::shared_ptr<TInnerDRR> TInnerDRRPtr; + typedef TDRRScheduler<TInnerDRR> TOuterDRR; + TOuterDRR drr(200); + + TInnerDRR* G; + drr.AddQueue("G", TInnerDRRPtr(G = new TInnerDRR(200))); + TInnerDRR* H; + drr.AddQueue("H", TInnerDRRPtr(H = new TInnerDRR(200))); + + TMyQueue* A; + TMyQueue* B; + TMyQueue* C; + TMyQueue* D; + TMyQueue* E; + TMyQueue* X; + G->AddQueue("A", TQueuePtr(A = new TMyQueue("A"))); + G->AddQueue("B", TQueuePtr(B = new TMyQueue("B"))); + G->AddQueue("C", TQueuePtr(C = new TMyQueue("C"))); + G->AddQueue("D", TQueuePtr(D = new TMyQueue("D"))); + G->AddQueue("E", TQueuePtr(E = new TMyQueue("E"))); + H->AddQueue("X", TQueuePtr(X = new TMyQueue("X"))); + + Generate(A, "100 100 100"); + Generate(B, "100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(drr), "ABABAB"); + + Generate(A, "100 100 100"); + Generate(X, "100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(drr), "AXAXAX"); + + Generate(A, "100 100 100"); + Generate(B, "100 100 100"); + Generate(C, "100 100 100"); + Generate(D, "100 100 100"); + Generate(E, "100 100 100"); + Generate(X, "100 100 100 100 100 100 100 100 100 100 100 100 100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(drr), "AXBXCXDXEXAXBXCXDXEXAXBXCXDXEX"); + } + + Y_UNIT_TEST(DoubleDRRWithWeights) { + typedef TDRRScheduler<TMyQueue> TInnerDRR; + typedef std::shared_ptr<TInnerDRR> TInnerDRRPtr; + typedef TDRRScheduler<TInnerDRR> TOuterDRR; + TOuterDRR drr(200); + + TInnerDRR* G; + drr.AddQueue("G", TInnerDRRPtr(G = new TInnerDRR(200, 1))); + TInnerDRR* H; + drr.AddQueue("H", TInnerDRRPtr(H = new TInnerDRR(200, 3))); + + TMyQueue* A; + TMyQueue* B; + TMyQueue* C; + TMyQueue* D; + G->AddQueue("A", TQueuePtr(A = new TMyQueue("A"))); + G->AddQueue("B", TQueuePtr(B = new TMyQueue("B"))); + H->AddQueue("C", TQueuePtr(C = new TMyQueue("C"))); + H->AddQueue("D", TQueuePtr(D = new TMyQueue("D"))); + + Generate(A, "100 100 100"); // 100 + Generate(B, "100 100 100"); // 100 + UNIT_ASSERT_STRINGS_EQUAL(Schedule(drr), "ABABAB"); + + Generate(C, "100 100 100"); // 100 + Generate(D, "100 100 100"); // 100 + UNIT_ASSERT_STRINGS_EQUAL(Schedule(drr), "CDCDCD"); + + Generate(A, "100 100 100 100 100 100 100 100"); // 50 + Generate(C, "100 100 100 100 100 100 100 100"); // 150 + UNIT_ASSERT_STRINGS_EQUAL(Schedule(drr), "CACCCACCCACAAAAA"); + + Generate(B, "100 100 100 100 100 100 100 100"); // 50 + Generate(D, "100 100 100 100 100 100 100 100"); // 150 + UNIT_ASSERT_STRINGS_EQUAL(Schedule(drr), "DBDDDBDDDBDBBBBB"); + + Generate(A, "200 200 200 200 200 200 200 200 200 200 200"); // 25 + Generate(B, "200 200 200 200 200 200 200 200 200 200 200"); // 25 + Generate(C, "200 200 200 200 200 200 200 200 200 200 200"); // 75 + Generate(D, "200 200 200 200 200 200 200 200 200 200 200"); // 75 + UNIT_ASSERT_STRINGS_EQUAL(Schedule(drr), "CDACDCBDCDACDCBDCDACDCBDCDACDBABABABABABABAB"); + + Generate(A, "100 100 100 100 100 100 100 100 100 100 100"); // 25 + Generate(B, "100 100 100 100 100 100 100 100 100 100 100"); // 25 + Generate(C, "100 100 100 100 100 100 100 100 100 100 100"); // 75 + Generate(D, "100 100 100 100 100 100 100 100 100 100 100"); // 75 + UNIT_ASSERT_STRINGS_EQUAL(Schedule(drr), "CADCDBCDCADCDBCDCADCDBCDCADCDBABABABABABABAB"); + + Generate(A, "50 50 50 50 50 50 50 50 50 50 50"); // 25 + Generate(B, "50 50 50 50 50 50 50 50 50 50 50"); // 25 + Generate(C, "50 50 50 50 50 50 50 50 50 50 50"); // 75 + Generate(D, "50 50 50 50 50 50 50 50 50 50 50"); // 75 + UNIT_ASSERT_STRINGS_EQUAL(Schedule(drr), "ACCDADCCBDDCBCDDACCDADCCBDDCBDAABBAABBAABBAB"); + + Generate(A, "25 25 25 25 25 25 25 25 25 25 25"); // 25 + Generate(B, "25 25 25 25 25 25 25 25 25 25 25"); // 25 + Generate(C, "25 25 25 25 25 25 25 25 25 25 25"); // 75 + Generate(D, "25 25 25 25 25 25 25 25 25 25 25"); // 75 + UNIT_ASSERT_STRINGS_EQUAL(Schedule(drr), "AACCCCDDAADDCCCCBBDDDDCCBBCDDDAAAABBBBAAABBB"); + } + + Y_UNIT_TEST(OneQueueDRRFrontPush) { + TDRRScheduler<TMyQueue> drr(100); + + TMyQueue* A; + drr.AddQueue("A", TQueuePtr(A = new TMyQueue("A"))); + + Generate(A, "10 20 30"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(drr, 1, true), "A10"); + + GenerateFront(A, "40"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(drr, size_t(-1), true), "A40A20A30"); + } + + Y_UNIT_TEST(SimpleDRRFrontPush) { + TDRRScheduler<TMyQueue> drr(10); + + TMyQueue* A; + TMyQueue* B; + drr.AddQueue("A", TQueuePtr(A = new TMyQueue("A"))); + drr.AddQueue("B", TQueuePtr(B = new TMyQueue("B"))); + + Generate(A, "10 30 40"); + Generate(B, "10 20 30"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(drr, 2, true), "A10B10"); + + GenerateFront(A, "20"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(drr, size_t(-1), true), "A20B20A30B30A40"); + } + + Y_UNIT_TEST(SimpleDRRFrontPushAll) { + TDRRScheduler<TMyQueue> drr(10); + + TMyQueue* A; + TMyQueue* B; + drr.AddQueue("A", TQueuePtr(A = new TMyQueue("A"))); + drr.AddQueue("B", TQueuePtr(B = new TMyQueue("B"))); + + Generate(A, "10 30"); + Generate(B, "10 30"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(drr, 2, true), "A10B10"); + + GenerateFront(A, "20"); + GenerateFront(B, "20"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(drr, size_t(-1), true), "A20B20A30B30"); + } + + Y_UNIT_TEST(DoubleDRRFrontPush) { + typedef TDRRScheduler<TMyQueue> TInnerDRR; + typedef std::shared_ptr<TInnerDRR> TInnerDRRPtr; + typedef TDRRScheduler<TInnerDRR> TOuterDRR; + TOuterDRR drr(10); + + TInnerDRR* G; + drr.AddQueue("G", TInnerDRRPtr(G = new TInnerDRR(10))); + TInnerDRR* H; + drr.AddQueue("H", TInnerDRRPtr(H = new TInnerDRR(10))); + + TMyQueue* A; + TMyQueue* B; + TMyQueue* C; + TMyQueue* D; + G->AddQueue("A", TQueuePtr(A = new TMyQueue("A"))); + G->AddQueue("B", TQueuePtr(B = new TMyQueue("B"))); + H->AddQueue("C", TQueuePtr(C = new TMyQueue("C"))); + H->AddQueue("D", TQueuePtr(D = new TMyQueue("D"))); + + Generate(A, "10 20"); + Generate(B, "10 30"); + Generate(C, "10 20"); + Generate(D, "10 20"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(drr, 4, true), "A10C10B10D10"); + + GenerateFront(B, "20"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(drr, size_t(-1), true), "A20C20B20D20B30"); + } + + Y_UNIT_TEST(DoubleDRRFrontPushAll) { + typedef TDRRScheduler<TMyQueue> TInnerDRR; + typedef std::shared_ptr<TInnerDRR> TInnerDRRPtr; + typedef TDRRScheduler<TInnerDRR> TOuterDRR; + TOuterDRR drr(10); + + TInnerDRR* G; + drr.AddQueue("G", TInnerDRRPtr(G = new TInnerDRR(10))); + TInnerDRR* H; + drr.AddQueue("H", TInnerDRRPtr(H = new TInnerDRR(10))); + + TMyQueue* A; + TMyQueue* B; + TMyQueue* C; + TMyQueue* D; + G->AddQueue("A", TQueuePtr(A = new TMyQueue("A"))); + G->AddQueue("B", TQueuePtr(B = new TMyQueue("B"))); + H->AddQueue("C", TQueuePtr(C = new TMyQueue("C"))); + H->AddQueue("D", TQueuePtr(D = new TMyQueue("D"))); + + Generate(A, "10 30"); + Generate(B, "10 30"); + Generate(C, "10 30"); + Generate(D, "10 30"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(drr, 4, true), "A10C10B10D10"); + + GenerateFront(A, "20"); + GenerateFront(B, "20"); + GenerateFront(C, "20"); + GenerateFront(D, "20"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(drr, size_t(-1), true), "A20C20B20D20A30C30B30D30"); + } + + Y_UNIT_TEST(DoubleDRRMultiplePush) { + typedef TDRRScheduler<TMyQueue> TInnerDRR; + typedef std::shared_ptr<TInnerDRR> TInnerDRRPtr; + typedef TDRRScheduler<TInnerDRR> TOuterDRR; + TOuterDRR drr(40); + + TInnerDRR* G; + drr.AddQueue("G", TInnerDRRPtr(G = new TInnerDRR(20))); + TInnerDRR* H; + drr.AddQueue("H", TInnerDRRPtr(H = new TInnerDRR(20))); + + TMyQueue* A; + TMyQueue* B; + TMyQueue* C; + TMyQueue* D; + G->AddQueue("A", TQueuePtr(A = new TMyQueue("A"))); + G->AddQueue("B", TQueuePtr(B = new TMyQueue("B"))); + H->AddQueue("C", TQueuePtr(C = new TMyQueue("C"))); + H->AddQueue("D", TQueuePtr(D = new TMyQueue("D"))); + + Generate(A, "5 5 5 5 5"); + Generate(B, "10 10"); + Generate(C, "10 10"); + Generate(D, "10 10"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(drr, size_t(-1), true), "A5A5B10C10D10A5A5B10C10D10A5"); + } + + Y_UNIT_TEST(DoubleDRRFrontMultiplePush) { + typedef TDRRScheduler<TMyQueue> TInnerDRR; + typedef std::shared_ptr<TInnerDRR> TInnerDRRPtr; + typedef TDRRScheduler<TInnerDRR> TOuterDRR; + TOuterDRR drr(40); + + TInnerDRR* G; + drr.AddQueue("G", TInnerDRRPtr(G = new TInnerDRR(20))); + TInnerDRR* H; + drr.AddQueue("H", TInnerDRRPtr(H = new TInnerDRR(20))); + + TMyQueue* A; + TMyQueue* B; + TMyQueue* C; + TMyQueue* D; + G->AddQueue("A", TQueuePtr(A = new TMyQueue("A"))); + G->AddQueue("B", TQueuePtr(B = new TMyQueue("B"))); + H->AddQueue("C", TQueuePtr(C = new TMyQueue("C"))); + H->AddQueue("D", TQueuePtr(D = new TMyQueue("D"))); +// Cerr << "A\t" << (ui64)A << Endl; +// Cerr << "B\t" << (ui64)B << Endl; +// Cerr << "C\t" << (ui64)C << Endl; +// Cerr << "D\t" << (ui64)D << Endl; +// Cerr << "G\t" << (ui64)G << Endl; +// Cerr << "H\t" << (ui64)H << Endl; + + + Generate(A, "5"); + Generate(B, "10 10"); + Generate(C, "10 10"); + Generate(D, "10 10"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(drr, 1, true), "A5"); + + GenerateFront(A, "5"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(drr, 1, true), "A5"); + + GenerateFront(A, "5"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(drr, 4, true), "B10C10D10A5"); + + GenerateFront(A, "5"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(drr, 1, true), "A5"); + + GenerateFront(A, "5"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(drr, size_t(-1), true), "B10C10D10A5"); + } + + Y_UNIT_TEST(DoubleDRRCheckEmpty) { + typedef TDRRScheduler<TMyQueue> TInnerDRR; + typedef std::shared_ptr<TInnerDRR> TInnerDRRPtr; + typedef TDRRScheduler<TInnerDRR> TOuterDRR; + TOuterDRR drr(40); + + TInnerDRR* G; + drr.AddQueue("G", TInnerDRRPtr(G = new TInnerDRR(20))); + TInnerDRR* H; + drr.AddQueue("H", TInnerDRRPtr(H = new TInnerDRR(20))); + + TMyQueue* A; + TMyQueue* B; + TMyQueue* C; + TMyQueue* D; + G->AddQueue("A", TQueuePtr(A = new TMyQueue("A"))); + G->AddQueue("B", TQueuePtr(B = new TMyQueue("B"))); + H->AddQueue("C", TQueuePtr(C = new TMyQueue("C"))); + H->AddQueue("D", TQueuePtr(D = new TMyQueue("D"))); + + Generate(A, "5"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(drr, size_t(1), true), "A5"); + + GenerateFront(A, "5"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(drr, size_t(1), true), "A5"); + + GenerateFront(A, "5"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(drr, size_t(1), true), "A5"); + + GenerateFront(A, "5"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(drr, size_t(1), true), "A5"); + + GenerateFront(A, "5"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(drr, size_t(-1), true), "A5"); + } + // TODO(serxa): add test for weight update +} diff --git a/ydb/library/drr/ut/ya.make b/ydb/library/drr/ut/ya.make new file mode 100644 index 00000000000..3ee62e9ec6d --- /dev/null +++ b/ydb/library/drr/ut/ya.make @@ -0,0 +1,12 @@ +UNITTEST() + +PEERDIR( + library/cpp/threading/future + ydb/library/drr +) + +SRCS( + drr_ut.cpp +) + +END() diff --git a/ydb/library/drr/ya.make b/ydb/library/drr/ya.make new file mode 100644 index 00000000000..9051bc10c69 --- /dev/null +++ b/ydb/library/drr/ya.make @@ -0,0 +1,17 @@ +LIBRARY() + +PEERDIR( + library/cpp/lwtrace + library/cpp/monlib/encode/legacy_protobuf/protos +) + +SRCS( + drr.cpp + probes.cpp +) + +END() + +RECURSE( + ut +) diff --git a/ydb/library/planner/base/defs.h b/ydb/library/planner/base/defs.h new file mode 100644 index 00000000000..fb669fa638c --- /dev/null +++ b/ydb/library/planner/base/defs.h @@ -0,0 +1,60 @@ +#pragma once + +#include <util/system/types.h> +#include <util/system/yassert.h> +#include <util/generic/utility.h> + +#define SHAREPLANNER_CHECK_EACH_ACTION + +namespace NScheduling { + +typedef ui64 TUCost; // [res*sec] + +// Different parrots for resource counting +typedef double TLength; // [metre] +typedef double TForce; // [newton] +typedef double TEnergy; // [joule] +typedef double TDimless; // [1] + +typedef ui64 TWeight; +typedef double FWeight; + +static const TForce gs = 1; + +template <class W, class X> +X WCut(W w, W& wsum, X& xsum) +{ + Y_ASSERT(w > 0); + Y_ASSERT(wsum > 0); + Y_ASSERT(xsum > 0); + X x = Max<X>(0, xsum * w / wsum); + Y_ASSERT(x >= 0); + wsum -= w; + if (wsum < 0) + wsum = 0; + xsum -= x; + if (xsum < 0) + xsum = 0; + return x; +} + +template <class W, class X> +X WMaxCut(W w, X xmax, W& wsum, X& xsum) +{ + Y_ASSERT(w > 0); + Y_ASSERT(wsum > 0); + Y_ASSERT(xsum > 0); + X x = Max<X>(0, Min<X>(xmax, xsum * w / wsum)); + Y_ASSERT(x >= 0); + wsum -= w; + if (wsum < 0) + wsum = 0; + xsum -= x; + if (xsum < 0) + xsum = 0; + Y_ASSERT(wsum >= 0); + Y_ASSERT(xsum >= 0); + return x; +} + +} diff --git a/ydb/library/planner/base/visitor.h b/ydb/library/planner/base/visitor.h new file mode 100644 index 00000000000..981aa47a497 --- /dev/null +++ b/ydb/library/planner/base/visitor.h @@ -0,0 +1,55 @@ +#pragma once + +#include <util/generic/cast.h> +#include <util/system/type_name.h> + +namespace NScheduling { + +class IVisitable; + +class IVisitorBase { +public: + virtual ~IVisitorBase() {} + virtual void VisitUnkown(IVisitable* o) { VisitFailed(o); } + virtual void VisitUnkown(const IVisitable* o) { VisitFailed(o); } +private: + inline void VisitFailed(const IVisitable* o); +}; + +template <class T> +class IVisitor { +public: + virtual void Visit(T* node) = 0; +}; + +class IVisitable { +public: + virtual ~IVisitable() {} + virtual void Accept(IVisitorBase* v) { v->VisitUnkown(this); } + virtual void Accept(IVisitorBase* v) const { v->VisitUnkown(this); } +protected: + template <class TDerived> + static bool AcceptImpl(TDerived* d, IVisitorBase* v) + { + if (auto* p = dynamic_cast<IVisitor<TDerived>*>(v)) { + p->Visit(d); + return true; + } else { + return false; + } + } +private: +}; + +#define SCHEDULING_DEFINE_VISITABLE(TBase) \ + void Accept(::NScheduling::IVisitorBase* v) override { if (!AcceptImpl(this, v)) { TBase::Accept(v); } } \ + void Accept(::NScheduling::IVisitorBase* v) const override { if (!AcceptImpl(this, v)) { TBase::Accept(v); } } \ + /**/ + + +void IVisitorBase::VisitFailed(const IVisitable* o) +{ + Y_ABORT("visitor of type '%s' cannot visit class of type '%s'", TypeName(*this).c_str(), TypeName(*o).c_str()); +} + +} diff --git a/ydb/library/planner/share/account.cpp b/ydb/library/planner/share/account.cpp new file mode 100644 index 00000000000..1481943fe52 --- /dev/null +++ b/ydb/library/planner/share/account.cpp @@ -0,0 +1,5 @@ +#include "account.h" + +namespace NScheduling { + +} diff --git a/ydb/library/planner/share/account.h b/ydb/library/planner/share/account.h new file mode 100644 index 00000000000..e2754bfe686 --- /dev/null +++ b/ydb/library/planner/share/account.h @@ -0,0 +1,24 @@ +#pragma once + +#include "node.h" + +namespace NScheduling { + +class TShareAccount: public TShareNode { +public: + SCHEDULING_DEFINE_VISITABLE(TShareNode); +public: // Configuration + TShareAccount(TAutoPtr<TConfig> cfg) + : TShareNode(cfg.Release()) + {} + TShareAccount(const TString& name, FWeight w, FWeight wmax, TEnergy v) + : TShareAccount(new TConfig(v, wmax, w, name)) + {} + void Configure(const TString& name, FWeight w, FWeight wmax, TEnergy v) + { + SetConfig(new TConfig(v, wmax, w, name)); + } + const TConfig& Cfg() const { return *static_cast<const TConfig*>(Config.Get()); } +}; + +} diff --git a/ydb/library/planner/share/analytics.h b/ydb/library/planner/share/analytics.h new file mode 100644 index 00000000000..6d75a08e416 --- /dev/null +++ b/ydb/library/planner/share/analytics.h @@ -0,0 +1,99 @@ +#pragma once + +#include <ydb/library/analytics/data.h> +#include "apply.h" +#include <ydb/library/planner/share/models/density.h> + +#define FOREACH_NODE_ACCESSOR(XX, YY) \ + XX(s0) \ + XX(w0) \ + XX(wmax) \ + XX(w) \ + XX(h) \ + XX(D) \ + XX(S) \ + XX(dD) \ + XX(dS) \ + XX(pdD) \ + XX(pdS) \ + XX(E) \ + XX(V) \ + YY(L) \ + YY(O) \ + YY(A) \ + YY(P) \ + XX(dd) \ + XX(ds) \ + XX(pdd) \ + XX(pds) \ + XX(e) \ + XX(v) \ + YY(l) \ + YY(o) \ + YY(a) \ + YY(p) \ + /**/ + +#define FILL_ROW(n) row[#n] = c->n +#define FILL_ROW_N(n, v) row[n] = c->v + +namespace NScheduling { namespace NAnalytics { + +using namespace ::NAnalytics; + +inline void AddFromCtx(TRow& row, const IContext* ctx) +{ + if (const TDeContext* c = dynamic_cast<const TDeContext*>(ctx)) { + FILL_ROW(x); + FILL_ROW(ix); + FILL_ROW(s); + FILL_ROW(u); + FILL_ROW(iu); + FILL_ROW(wl); + FILL_ROW(iwl); + FILL_ROW_N("pa", p); // pa = point of attachment + FILL_ROW(pr); + FILL_ROW(pl); + FILL_ROW(pf); + FILL_ROW(lambda); + FILL_ROW_N("hf", h); // h-function + FILL_ROW(sigma); + FILL_ROW(isigma); + FILL_ROW(p0); + FILL_ROW(ip0); + FILL_ROW_N("stretch", Pull.stretch); + FILL_ROW_N("retardness", Pull.retardness); + FILL_ROW_N("s0R", Pull.s0R); + } +} + +inline TRow FromNode(const TShareNode* node, TEnergy Eg) +{ + TRow row; + row.Name = node->GetName(); +#define XX_MACRO(n) row[#n] = node->n(); +#define YY_MACRO(n) row[#n] = node->n(Eg); + FOREACH_NODE_ACCESSOR(XX_MACRO, YY_MACRO); +#undef XX_MACRO +#undef YY_MACRO + AddFromCtx(row, node->Ctx()); + return row; +} + +inline TTable FromGroup(const TShareGroup* group) +{ + TTable out; + TEnergy Eg = 0; + ApplyTo<const TShareNode>(group, [&Eg] (const TShareNode* node) { + Eg += node->E(); + }); + ApplyTo<const TShareNode>(group, [&out, Eg] (const TShareNode* node) { + out.push_back(FromNode(node, Eg)); + }); + return out; +} + +}} + +#undef FILL_ROW +#undef FOREACH_NODE_FIELD diff --git a/ydb/library/planner/share/apply.h b/ydb/library/planner/share/apply.h new file mode 100644 index 00000000000..b692d2cada6 --- /dev/null +++ b/ydb/library/planner/share/apply.h @@ -0,0 +1,104 @@ +#pragma once + +#include "node_visitor.h" +#include "group.h" +#include <functional> + +namespace NScheduling { + +template <class TAcc, class TGrp> +class TApplyVisitor + : public IVisitorBase + , public IVisitor<TAcc> + , public IVisitor<TGrp> +{ +private: + std::function<void (TAcc*)> AccFunc; + std::function<void (TGrp*)> GrpFunc; +public: + template <class AF, class GF> + TApplyVisitor(AF af, GF gf) + : AccFunc(af) + , GrpFunc(gf) + {} + + void Visit(TAcc* node) override + { + AccFunc(node); + } + + void Visit(TGrp* node) override + { + GrpFunc(node); + } +}; + +template <class TNode> +class TApplySimpleVisitor + : public IVisitorBase + , public IVisitor<TNode> +{ +private: + std::function<void (TNode*)> NodeFunc; +public: + template <class NF> + TApplySimpleVisitor(NF nf) + : NodeFunc(nf) + {} + + void Visit(TNode* node) override + { + NodeFunc(node); + } +}; + +template <class TAcc, class TGrp> +class TRecursiveApplyVisitor + : public IVisitorBase + , public IVisitor<TAcc> + , public IVisitor<TGrp> +{ +private: + std::function<void (TAcc*)> AccFunc; + std::function<void (TGrp*)> GrpFunc; +public: + template <class AF, class GF> + TRecursiveApplyVisitor(AF af, GF gf) + : AccFunc(af) + , GrpFunc(gf) + {} + + void Visit(TAcc* node) override + { + AccFunc(node); + } + + void Visit(TGrp* node) override + { + GrpFunc(node); + node->AcceptInChildren(this); + } +}; + +template <class TAcc, class TGrp, class T, class AF, class GF> +void ApplyTo(T* group, AF af, GF gf) +{ + TApplyVisitor<TAcc, TGrp> v(af, gf); + group->AcceptInChildren(&v); +} + +template <class TNode, class T, class NF> +void ApplyTo(T* group, NF nf) +{ + TApplySimpleVisitor<TNode> v(nf); + group->AcceptInChildren(&v); +} + +template <class TAcc, class TGrp, class T, class AF, class GF> +void RecursiveApplyTo(T* group, AF af, GF gf) +{ + TRecursiveApplyVisitor<TAcc, TGrp> v(af, gf); + group->Accept(&v); +} + +} diff --git a/ydb/library/planner/share/billing.h b/ydb/library/planner/share/billing.h new file mode 100644 index 00000000000..affcd8fc5ca --- /dev/null +++ b/ydb/library/planner/share/billing.h @@ -0,0 +1,78 @@ +#pragma once + +#include <util/generic/algorithm.h> +#include "apply.h" +#include "node_visitor.h" +#include "shareplanner.h" +#include <ydb/library/planner/share/models/density.h> + +namespace NScheduling { + +// Just apply static tariff +class TStaticBilling : public INodeVisitor +{ +private: + TDimless Tariff; +public: + TStaticBilling(TDimless tariff = 1.0) + : Tariff(tariff) + {} + + void Visit(TShareAccount* node) override + { + Handle(node); + } + + void Visit(TShareGroup* node) override + { + node->AcceptInChildren(this); + Handle(node); + } +protected: + void Handle(TShareNode* node) + { + node->SetTariff(Tariff); + } +}; + +// It sets current tariff, which in turn is just sigma value +// for parent group. Sigma represents how many nodes of a group is present at the moment +// each multiplied by it's static share +// For example: +// 1) if every user is working, then sigma equals one +// 2) if there is the only working user with static weight s0, then sigma equals s0 +// Note that 0 < sigma <= 1 +class TPresentShareBilling : public INodeVisitor +{ +private: + bool Memoryless = false; +public: + TPresentShareBilling(bool memoryless) + : Memoryless(memoryless) + {} + + void Visit(TShareAccount* node) override + { + Handle(node); + } + + void Visit(TShareGroup* node) override + { + node->AcceptInChildren(this); + Handle(node); + } +protected: + void Handle(TShareNode* node) + { + if (TShareGroup* group = node->GetParent()) { + if (TDeContext* pctx = group->CtxAs<TDeContext>()) { + TDimless tariff = Memoryless? pctx->isigma: pctx->sigma; + if (tariff == 0) // There is nobody working on cluster + tariff = 1.0; // Just to avoid stalling (otherwise, every dDi will be zero always) + node->SetTariff(tariff); + } + } + } +}; + +} diff --git a/ydb/library/planner/share/group.cpp b/ydb/library/planner/share/group.cpp new file mode 100644 index 00000000000..df1245c4b7f --- /dev/null +++ b/ydb/library/planner/share/group.cpp @@ -0,0 +1,100 @@ +#include "group.h" +#include "apply.h" + +namespace NScheduling { + +TShareNode* TShareGroup::FindByName(const TString& name) +{ + auto i = Children.find(name); + if (i == Children.end()) { + return nullptr; + } else { + return i->second; + } +} + +void TShareGroup::ResetShare() +{ + TForce s0 = gs; + FWeight wsum = TotalWeight; + for (auto ch : Children) { + TShareNode* node = ch.second; + node->SetShare(WCut(node->w0(), wsum, s0)); + } +} + +void TShareGroup::Add(TShareNode* node) +{ + Y_ABORT_UNLESS(!Children.contains(node->GetName()), "duplicate child name '%s' in group '%s'", node->GetName().data(), GetName().data()); + Children[node->GetName()] = node; + TotalWeight += node->w0(); + TotalVolume += node->V(); + ResetShare(); +} + +void TShareGroup::Remove(TShareNode* node) +{ + Y_ABORT_UNLESS(Children.contains(node->GetName()), "trying to delete unknown child name '%s' in group '%s'", node->GetName().data(), GetName().data()); + Children.erase(node->GetName()); + TotalWeight -= node->w0(); + TotalVolume -= node->V(); + ResetShare(); +} + +void TShareGroup::Clear() +{ + ApplyTo<TShareAccount, TShareGroup>(this, [] (TShareAccount* acc) { + acc->DetachNoRemove(); + }, [] (TShareGroup* grp) { + grp->Clear(); + grp->DetachNoRemove(); + }); + Children.clear(); + TotalWeight = 0; + TotalVolume = 0; +} + +TEnergy TShareGroup::ComputeEc() const +{ + TEnergy Ec = 0; + ApplyTo<const TShareNode>(this, [&Ec] (const TShareNode* node) { + Ec += node->E(); + }); + return Ec; +} + +TEnergy TShareGroup::GetTotalCredit() const +{ + TEnergy Eg = 0; + for (auto ch : Children) { + TShareNode* node = ch.second; + Eg += node->E(); + } + TEnergy credit = 0; + for (auto ch : Children) { + TShareNode* node = ch.second; + TEnergy P = node->P(Eg); + if (P > 0) { + credit += P; + } + } + return credit; +} + +void TShareGroup::AcceptInChildren(IVisitorBase* v) +{ + for (auto ch : Children) { + TShareNode* node = ch.second; + node->Accept(v); + } +} + +void TShareGroup::AcceptInChildren(IVisitorBase* v) const +{ + for (auto ch : Children) { + const TShareNode* node = ch.second; + node->Accept(v); + } +} + +} diff --git a/ydb/library/planner/share/group.h b/ydb/library/planner/share/group.h new file mode 100644 index 00000000000..305a8519c8b --- /dev/null +++ b/ydb/library/planner/share/group.h @@ -0,0 +1,63 @@ +#pragma once + +#include <util/generic/map.h> +#include <util/system/type_name.h> +#include "account.h" + +namespace NScheduling { + +class TShareGroup: public TShareNode { +public: + SCHEDULING_DEFINE_VISITABLE(TShareNode); +protected: + TMap<TString, TShareNode*> Children; + FWeight TotalWeight = 0; // Total weight of children + TEnergy TotalVolume = 0; // Total volume of children +public: // Configuration + TShareGroup(TAutoPtr<TConfig> cfg) + : TShareNode(cfg.Release()) + {} + TShareGroup(const TString& name, FWeight w, FWeight wmax, TEnergy v) + : TShareGroup(new TConfig(v, wmax, w, name)) + {} + void Configure(const TString& name, FWeight w, FWeight wmax, TEnergy v) + { + SetConfig(new TConfig(v, wmax, w, name)); + } + const TConfig& Cfg() const { return *static_cast<const TConfig*>(Config.Get()); } +public: + bool Empty() const { return Children.empty(); } + void AcceptInChildren(IVisitorBase* v); + void AcceptInChildren(IVisitorBase* v) const; +public: + TShareNode* FindByName(const TString& name); + inline FWeight GetTotalWeight() const { return TotalWeight; } + inline TEnergy GetTotalVolume() const { return TotalVolume; } + void Add(TShareNode* node); + void Remove(TShareNode* node); + void Clear(); + TEnergy ComputeEc() const; +public: // Monitoring + TEnergy GetTotalCredit() const; +protected: + void Insert(TShareAccount* node); + void Insert(TShareGroup* node); + void Erase(TShareAccount* node); + void Erase(TShareGroup* node); + void ResetShare(); +}; + +#define CUSTOMSHAREPLANNER_FOR(acctype, grptype, node, expr) \ + do { \ + for (auto _ch_ : Children) { \ + auto* _node_ = _ch_.second; \ + if (auto* node = dynamic_cast<acctype*>(_node_)) { expr; } \ + else if (auto* node = dynamic_cast<grptype*>(_node_)) { expr; } \ + else { Y_ABORT("node is niether " #acctype " nor " #grptype ", it is %s", TypeName(*node).c_str()); } \ + } \ + } while (false) \ + /**/ + +#define SHAREPLANNER_FOR(node, expr) CUSTOMSHAREPLANNER_FOR(TShareAccount, TShareGroup, node, expr) + +} diff --git a/ydb/library/planner/share/history.cpp b/ydb/library/planner/share/history.cpp new file mode 100644 index 00000000000..41aeb015137 --- /dev/null +++ b/ydb/library/planner/share/history.cpp @@ -0,0 +1,4 @@ +#include "history.h" + +namespace NScheduling { +} diff --git a/ydb/library/planner/share/history.h b/ydb/library/planner/share/history.h new file mode 100644 index 00000000000..7e64b4ab2f6 --- /dev/null +++ b/ydb/library/planner/share/history.h @@ -0,0 +1,114 @@ +#pragma once + +#include "shareplanner.h" +#include <ydb/library/planner/share/protos/shareplanner_history.pb.h> +#include "node_visitor.h" +#include "apply.h" +#include "probes.h" + +namespace NScheduling { + +class THistorySaver : public IConstNodeVisitor +{ +protected: + TSharePlannerHistory History; + TEnergy Eg = 0; +public: + static void Save(const TSharePlanner* planner, IOutputStream& os) + { + PLANNER_PROBE(SerializeHistory, planner->GetName()); + THistorySaver v; + planner->GetRoot()->Accept(&v); + v.History.SerializeToArcadiaStream(&os); + } +protected: + void Visit(const TShareAccount* account) override + { + SaveNode(account); + } + + void Visit(const TShareGroup* group) override + { + SaveNode(group); + // Recurse into children + TEnergy Eg_stack = Eg; + Eg = 0; + ApplyTo<const TShareNode>(group, [=] (const TShareNode* node) { + Eg += node->E(); + }); + group->AcceptInChildren(this); + Eg = Eg_stack; + } + + void SaveNode(const TShareNode* node) + { + TShareNodeHistory* nh = History.AddNodes(); + nh->SetName(node->GetName()); + nh->SetProficit(node->P(Eg)); + } +}; + +class THistoryLoader: public INodeVisitor +{ +protected: + THashMap<TString, const TShareNodeHistory*> NHMap; + TSharePlannerHistory History; + TSharePlanner* Planner; +public: + static bool Load(TSharePlanner* planner, IInputStream& is) + { + THistoryLoader v(planner); + return v.LoadFrom(is); + } +protected: + explicit THistoryLoader(TSharePlanner* planner) + : Planner(planner) + {} + + bool LoadFrom(IInputStream& is) + { + PLANNER_PROBE(DeserializeHistoryBegin, Planner->GetName()); + bool success = false; + if (History.ParseFromArcadiaStream(&is)) { + for (size_t i = 0; i < History.NodesSize(); i++) { + const TShareNodeHistory& nh = History.GetNodes(i); + NHMap[nh.GetName()] = &nh; + } + + if (OnHistoryDeserialized()) { + //UtilMeter->Load(History.GetUtilization()); // Load utilization history + Planner->GetRoot()->Accept(this); + success = true; + } + } + PLANNER_PROBE(DeserializeHistoryEnd, Planner->GetName(), success); + return success; + } + + void Visit(TShareAccount* account) override + { + LoadNode(account); + } + + void Visit(TShareGroup* group) override + { + LoadNode(group); + group->AcceptInChildren(this); + } + + void LoadNode(TShareNode* node) + { + auto i = NHMap.find(node->GetName()); + TEnergy Dnew = 0; + if (i != NHMap.end()) { + const TShareNodeHistory& nh = *i->second; + Dnew = nh.GetProficit(); + } + node->SetS(0); + node->SetD(Dnew); + } + + virtual bool OnHistoryDeserialized() { return true; } +}; + +} diff --git a/ydb/library/planner/share/models/density.h b/ydb/library/planner/share/models/density.h new file mode 100644 index 00000000000..bada470ee38 --- /dev/null +++ b/ydb/library/planner/share/models/density.h @@ -0,0 +1,385 @@ +#pragma once + +#include <cmath> +#include <ydb/library/planner/share/apply.h> +#include "recursive.h" + +namespace NScheduling { + +enum class TDepType { Left = 0, Top, AtNode, Bottom, Right, Other }; + +template <class TCtx> +struct TDePoint { + TCtx* Ctx; + double p; + TDepType Type; + + void Move(double& h, double& lambda, const TDePoint* prev) const + { + if (prev) { + h += lambda * (prev->p - p); // Integrate density + } + switch (Type) { + case TDepType::Right: lambda += Ctx->lambda; break; + case TDepType::Left: lambda -= Ctx->lambda; break; + case TDepType::Bottom: break; + case TDepType::Top: h += Ctx->x; break; + default: break; // other types must be processed separately + } + } + + bool operator<(const TDePoint& o) const + { + if (p == o.p) { + return Type > o.Type; + } else { + return p > o.p; + } + } +}; + +class TDeContext : public IContext { +public: + SCHEDULING_DEFINE_VISITABLE(IContext); + + ui64 ModelCycle = 0; // Number of times that Run() passed with this context + + // Context for child role + TShareNode* Node; + FWeight w; // weight + TDimless ix = 0; // instant utilization + TDimless x = 0; // average utilization + TDimless u = 0; // usage + TDimless iu = 0; // instant usage + TDimless wl = 0; // x/w (water-level) + TDimless iwl = 0; // ix/w (instant water-level) + TLength p = 0; // normalized proficit + TLength pr = 0; // dense interval rightmost point + TLength pl = 0; // dense interval leftmost point + TLength pf = 0; // normalized proficit relative to floating origin + double lambda = 0; // density [1/metre] + TDimless h = 0; // h-function value + TForce s = 0; // dynamic share + + // Context for parent role + TEnergy Ec = 0; // sum of E() of children + TEnergy dDc = 0; // sum of dD() of children + TForce sigma = 0; // used fraction of band (0; 1] + TForce isigma = 0; // instant used fraction of band (0; 1] + TLength p0 = 0; // floating origin + TLength ip0 = 0; // instant floating origin + + // Context for insensitive puller + struct { + ui64 Cycle = ui64(-1); + TEnergy Dlast = 0; // D was on last pull + TDimless stretch = 0; + TDimless retardness = 0; + TForce s0R = 0; + } Pull; + + explicit TDeContext(IModel* model, TShareNode* node) + : IContext(model) + , Node(node) + , w(node? node->w0(): 1.0) + {} + + void Update(TDeContext gctx, double avgLength) + { + // Update utilization + ix = Node->dD() / gctx.dDc; + if (avgLength > 0) { + TLength ddg = gctx.dDc / gs; + double alpha = std::pow(2.0, -ddg / avgLength); + x = alpha * x + (1-alpha) * ix; + } else { + x = ix; + } + + // Update normalized proficit + p = Node->p(gctx.Ec); + } + + void Normalize(double Xsum, double iXsum) + { + if (Xsum > 0) { + // Moving averaging formulas should always give Xsum == 1.0 exactly + // Reasons for normalize are: + // 1) fix floating-point arithmetic errors + // 2) on new group start actual sum of x of all children is zero + // and this should be fixed somehow + // 3) when account leaves group Xsum would be not equal to 1.0 + x /= Xsum; + wl = x / w; + } + if (iXsum > 0) { + ix /= iXsum; + iwl = ix / w; + } + } + + void ComputeUsage(double& xsum, double& wsum, double& ucur, double& wlcur, bool instant) + { + // EXPLANATION FOR FORMULA + // If we assume: + // (1) all "vectors" below are sorted by water-level wl[k] = x[k] / w[k] + // (2) u[k] >= u[k-1] -- u should monotonically increase with k + // (3) u[k] must be bounded by 1 (when xp[k] == 0) + // (4) u[k] must be a linear function of wl[k] + // + // One can proove that: + // wl[k] - wl[k-1] + // u[k] = u[k-1] + ----------------- (1 - u[k-1]) + // WL[k] - wl[k-1] + // + // , where WL[k] = sum(i=k..n, x[i]) / sum(i=k..n, w[i]) + // WL[k] is max possible value for wl[k] (due to sort by wl[k]) + // + // Or equivalently (adapted and used below for computation): + // xm[k] + // (*) u[k] = u[k-1] + --------------- (1 - u[k-1]) + // xp[k] + xm[k] + // + // , where xp[k] = x[k] - wl[k-1] * w[k] -- water in k-th bucket lacking to become max possible WL[k] + // xm[k] = WL[k] * w[k] - x[k] -- water in k-th bucket above min possible wl[k-1] + // + + Y_ABORT_UNLESS(wsum > 0); + double xx = (instant? ix: x); + double xn = xx - wlcur * w; + double xp = w * (xsum/wsum) - xx; + if (xn + xp > 0) { + ucur += xn / (xn+xp) * (1-ucur); + } + wlcur = (instant? iwl: wl); + xsum -= xx; + wsum -= w; + (instant? iu: u) = ucur; + } + + template <class TCtx> + static void PushPointsImpl(TCtx* t, TVector<TDePoint<TCtx>>& points) + { + if (t->pr > t->pl) { + points.push_back(TDePoint<TCtx>{t, t->pr, TDepType::Right}); + points.push_back(TDePoint<TCtx>{t, t->p, TDepType::AtNode}); + points.push_back(TDePoint<TCtx>{t, t->pl, TDepType::Left}); + } else { + points.push_back(TDePoint<TCtx>{t, t->p, TDepType::Bottom}); + points.push_back(TDePoint<TCtx>{t, t->p, TDepType::AtNode}); + points.push_back(TDePoint<TCtx>{t, t->p, TDepType::Top}); + } + } + + void PushPoints(TVector<TDePoint<TDeContext>>& points) + { + PushPointsImpl(this, points); + } + + void PushPoints(TVector<TDePoint<const TDeContext>>& points) const + { + PushPointsImpl(this, points); + } + + FWeight GetWeight() const override + { + return w; + } + + double CalcGlobalRealShare() const + { + if (TShareGroup* group = Node->GetParent()) + if (TDeContext* pctx = group->CtxAs<TDeContext>()) + return ix * pctx->CalcGlobalRealShare(); + return 1.0; + } + + double CalcGlobalAvgRealShare() const + { + if (TShareGroup* group = Node->GetParent()) + if (TDeContext* pctx = group->CtxAs<TDeContext>()) + return x * pctx->CalcGlobalAvgRealShare(); + return 1.0; + } + + double CalcGlobalDynamicShare() const + { + if (TShareGroup* group = Node->GetParent()) + if (TDeContext* pctx = group->CtxAs<TDeContext>()) + return s * pctx->CalcGlobalDynamicShare(); + return 1.0; + } + + void FillSensors(TShareNodeSensors& sensors) const override + { + sensors.SetFloatingLag(pf); + sensors.SetRealShare(CalcGlobalRealShare()); + sensors.SetAvgRealShare(CalcGlobalAvgRealShare()); + sensors.SetDynamicShare(CalcGlobalDynamicShare()); + sensors.SetGrpRealShare(ix); + sensors.SetGrpAvgRealShare(x); + sensors.SetGrpDynamicShare(s); + sensors.SetUsage(u); + sensors.SetInstantUsage(iu); + sensors.SetTariff(sigma); + sensors.SetInstantTariff(isigma); + sensors.SetFloatingOrigin(p0); + sensors.SetInstantOrigin(ip0); + sensors.SetBoost(h); + sensors.SetPullStretch(Pull.stretch); + sensors.SetRetardness(Pull.retardness); + sensors.SetRetardShare(Pull.s0R); + } +}; + +class TDensityModel : public TRecursiveModel<TDeContext> { +private: + TLength DenseLength; + TLength AveragingLength; + TVector<TDeContext*> Ctxs; + TVector<TDePoint<TCtx>> Points; + double Xsum = 0; + double iXsum = 0; + double Wsum = 0; + double Wsum_new = 0; +public: + explicit TDensityModel(TSharePlanner* planner) + : TRecursiveModel(planner) + , DenseLength(planner->Cfg().GetDenseLength()) + , AveragingLength(planner->Cfg().GetAveragingLength()) + {} + void OnAttach(TShareNode*) override {} + void OnDetach(TShareNode*) override {} + void OnAccount(TShareAccount* account, TCtx& ctx, TCtx& pctx) override + { + pctx.Ec += account->E(); + pctx.dDc += account->dD(); + ctx.ModelCycle++; + } + void OnDescend(TShareGroup* group, TCtx& gctx, TCtx& pctx) override + { + pctx.Ec += group->E(); + pctx.dDc += group->dD(); + gctx.Ec = gctx.dDc = 0; + gctx.ModelCycle++; + } + void OnAscend(TShareGroup* group, TCtx& gctx, TCtx&) override + { + if (ProcessNodes(group, gctx)) { + ProcessUtilization(); + ProcessUsage(gctx); + ProcessLag(gctx); + ProcessWeight(); + ProcessSensors(gctx); + } + } +private: + bool ProcessNodes(TShareGroup* group, TCtx& gctx) + { + if (gctx.dDc == 0) + // There was no work done since last model run, so dynamic weights + // must remain the same + return false; + Ctxs.clear(); + Xsum = 0; + iXsum = 0; + Wsum = 0; + ApplyTo<TShareNode>(group, [=] (TShareNode* node) { + TCtx& ctx = node->Ctx<TCtx>(); + ctx.Update(gctx, AveragingLength); + Xsum += ctx.x; + iXsum += ctx.ix; + Wsum += ctx.w; + Ctxs.push_back(&ctx); + }); + return Xsum > 0 && iXsum > 0; + } + + void ProcessUtilization() + { + // Renormalize utilization + double xsum = 0; + double ixsum = 0; + for (TDeContext* ctx : Ctxs) { + ctx->Normalize(Xsum, iXsum); + xsum += ctx->x; + ixsum += ctx->ix; + } + Xsum = xsum; + iXsum = ixsum; + } + + void ProcessUsageImpl(TForce& sigma, TLength& p0, bool instant) + { + // Ctx are assumed to be sorted by water-level (instant or average) + double ucur = 0; + double wlcur = 0; + double xsum = (instant? iXsum: Xsum); + double wsum = Wsum; + sigma = 0; + TEnergy p0sigma = 0; + for (TDeContext* ctx : Ctxs) { + ctx->ComputeUsage(xsum, wsum, ucur, wlcur, instant); + TForce sigma_i = ucur * ctx->Node->s0(); + sigma += sigma_i; + p0sigma += sigma_i * ctx->p; + } + p0 = p0sigma / sigma; + } + + void ProcessUsage(TDeContext& gctx) + { + Sort(Ctxs, [] (const TDeContext* x, const TDeContext* y) { return x->iwl < y->iwl; }); + ProcessUsageImpl(gctx.isigma, gctx.ip0, true); + Sort(Ctxs, [] (const TDeContext* x, const TDeContext* y) { return x->wl < y->wl; }); + ProcessUsageImpl(gctx.sigma, gctx.p0, false); + } + + void ProcessLag(TDeContext& gctx) + { + Points.clear(); + // We should not multiply by sigma iff sigma is used as tariff, but let's not comlicate and + // not multiply by sigma any ways, so you'd better use tarification by sigma, otherwise + // density model is NOT insensitive to absent users + //double dp = DenseLength * gs / gctx.sigma; // TODO[serxa]: also multiply by efficiency + double dp = DenseLength; + for (TDeContext* ctx : Ctxs) { + ctx->pr = ctx->p + Max(0.0, Min(dp, gctx.p0 - ctx->p)); + ctx->pl = ctx->pr - dp; + ctx->lambda = dp > 0.0? ctx->x / dp: 0.0; + ctx->PushPoints(Points); + } + Sort(Points); + double h = 0.0; + double lambda = 0.0; + TDePoint<TDeContext>* prev = nullptr; + for (TDePoint<TDeContext>& cur : Points) { + cur.Move(h, lambda, prev); + if (cur.Type == TDepType::AtNode) { + cur.Ctx->h = h; + } + prev = &cur; + } + } + + void ProcessWeight() + { + Wsum_new = 0.0; + for (TDeContext* ctx : Ctxs) { + TShareNode& node = *ctx->Node; + double w = node.w0() + (node.wmax() - node.w0()) * ctx->h; + ctx->w = Max(node.w0(), Min(node.wmax(), w)); + Wsum_new +=ctx->w; + } + } + + void ProcessSensors(TDeContext& gctx) + { + for (TDeContext* ctx : Ctxs) { + ctx->s = ctx->w / Wsum_new; + ctx->pf = ctx->p - gctx.p0; + } + } +}; + +} diff --git a/ydb/library/planner/share/models/max.h b/ydb/library/planner/share/models/max.h new file mode 100644 index 00000000000..bd8f596e101 --- /dev/null +++ b/ydb/library/planner/share/models/max.h @@ -0,0 +1,35 @@ +#pragma once + +#include "recursive.h" + +namespace NScheduling { + +class TMaxContext : public IContext { +private: + TShareNode* Node; +public: + TMaxContext(IModel* model, TShareNode* node) + : IContext(model) + , Node(node) + {} + + FWeight GetWeight() const override + { + return Node->wmax(); + } +}; + +class TMaxModel : public TRecursiveModel<TMaxContext> { +public: + explicit TMaxModel(TSharePlanner* planner) + : TRecursiveModel(planner) + {} + + void OnAccount(TShareAccount*, TCtx&, TCtx&) override {} + void OnDescend(TShareGroup*, TCtx&, TCtx&) override {} + void OnAscend(TShareGroup*, TCtx&, TCtx&) override {} + void OnAttach(TShareNode*) override {} + void OnDetach(TShareNode*) override {} +}; + +} diff --git a/ydb/library/planner/share/models/model.h b/ydb/library/planner/share/models/model.h new file mode 100644 index 00000000000..ca424328a98 --- /dev/null +++ b/ydb/library/planner/share/models/model.h @@ -0,0 +1,47 @@ +#pragma once + +#include <ydb/library/planner/share/node_visitor.h> +#include <ydb/library/planner/share/protos/shareplanner_sensors.pb.h> + +namespace NScheduling { + +class IContext; +class IModel; +class TShareNode; +class TSharePlanner; + +class IContext: public IVisitable { +public: + SCHEDULING_DEFINE_VISITABLE(IVisitable); +protected: + IModel* Model; +public: + explicit IContext(IModel* model) + : Model(model) + {} + + virtual ~IContext() {} + virtual FWeight GetWeight() const = 0; + virtual void FillSensors(TShareNodeSensors&) const { } + IModel* GetModel() { return Model; } + const IModel* GetModel() const { return Model; } +}; + +class IModel: public INodeVisitor +{ +protected: + TSharePlanner* Planner; +public: + explicit IModel(TSharePlanner* planner) + : Planner(planner) + {} + + virtual void Run(TShareGroup* root) + { + Visit(root); + } + virtual void OnAttach(TShareNode* node) = 0; + virtual void OnDetach(TShareNode* node) = 0; +}; + +} diff --git a/ydb/library/planner/share/models/recursive.h b/ydb/library/planner/share/models/recursive.h new file mode 100644 index 00000000000..9acd6e4656c --- /dev/null +++ b/ydb/library/planner/share/models/recursive.h @@ -0,0 +1,80 @@ +#pragma once + +#include "model.h" +#include <ydb/library/planner/share/shareplanner.h> +#include <functional> + +namespace NScheduling { + +template <class Context> +class TRecursiveModel : public IModel +{ +public: + typedef Context TCtx; +private: + THolder<TCtx> GlobalCtx; // Special parent context for root +public: + explicit TRecursiveModel(TSharePlanner* planner) + : IModel(planner) + {} + + void Visit(TShareAccount* node) final + { + TCtx& ctx = GetContext(node); + TCtx& pctx = GetParentContext(node); + OnAccount(node, ctx, pctx); + } + + void Visit(TShareGroup* node) final + { + TCtx& ctx = GetContext(node); + TCtx& pctx = GetParentContext(node); + OnDescend(node, ctx, pctx); + node->AcceptInChildren(this); + OnAscend(node, ctx, pctx); + } + + virtual TCtx* CreateContext(TShareNode* node = nullptr) + { + return new TCtx(this, node); + } +protected: + virtual void OnAccount(TShareAccount* account, TCtx& ctx, TCtx& pctx) { Y_UNUSED(account); Y_UNUSED(ctx); Y_UNUSED(pctx); } + virtual void OnDescend(TShareGroup* group, TCtx& ctx, TCtx& pctx) { Y_UNUSED(group); Y_UNUSED(ctx); Y_UNUSED(pctx); } + virtual void OnAscend(TShareGroup* group, TCtx& ctx, TCtx& pctx) { Y_UNUSED(group); Y_UNUSED(ctx); Y_UNUSED(pctx); } +protected: + TCtx& GetGlobalCtx() + { + Y_ASSERT(GlobalCtx); + return *GlobalCtx; + } + + TCtx& GetContext(TShareNode* node) + { + if (!node->Ctx() || node->Ctx()->GetModel() != this) { + node->ResetCtx(CreateContext(node)); + } + return node->Ctx<TCtx>(); + } + + TCtx& GetParentContext(TShareNode* node) + { + TShareNode* parent = node->GetParent(); + if (parent) { + Y_ASSERT(parent->Ctx()->GetModel() == this); + return parent->Ctx<TCtx>(); + } else { + if (!GlobalCtx) { + GlobalCtx.Reset(CreateContext()); + } + return *GlobalCtx; + } + } + + void DestroyGlobalContext() + { + GlobalCtx.Destroy(); + } +}; + +} diff --git a/ydb/library/planner/share/models/static.h b/ydb/library/planner/share/models/static.h new file mode 100644 index 00000000000..bcafe76b7c5 --- /dev/null +++ b/ydb/library/planner/share/models/static.h @@ -0,0 +1,35 @@ +#pragma once + +#include "recursive.h" + +namespace NScheduling { + +class TStaticContext : public IContext { +private: + TShareNode* Node; +public: + TStaticContext(IModel* model, TShareNode* node) + : IContext(model) + , Node(node) + {} + + FWeight GetWeight() const override + { + return Node->w0(); + } +}; + +class TStaticModel : public TRecursiveModel<TStaticContext> { +public: + explicit TStaticModel(TSharePlanner* planner) + : TRecursiveModel(planner) + {} + + void OnAccount(TShareAccount*, TCtx&, TCtx&) override {} + void OnDescend(TShareGroup*, TCtx&, TCtx&) override {} + void OnAscend(TShareGroup*, TCtx&, TCtx&) override {} + void OnAttach(TShareNode*) override {} + void OnDetach(TShareNode*) override {} +}; + +} diff --git a/ydb/library/planner/share/monitoring.h b/ydb/library/planner/share/monitoring.h new file mode 100644 index 00000000000..ba63e7949c7 --- /dev/null +++ b/ydb/library/planner/share/monitoring.h @@ -0,0 +1,542 @@ +#pragma once + +#include <cmath> +#include <util/system/type_name.h> +#include "shareplanner.h" +#include <ydb/library/planner/share/protos/shareplanner_sensors.pb.h> +#include <ydb/library/planner/share/models/density.h> + +namespace NScheduling { + +class TNameWidthEvaluator : public IConstNodeVisitor { +private: + size_t Ident; + size_t TreeWidth; + size_t Depth = 0; + + explicit TNameWidthEvaluator(size_t ident, size_t minWidth) + : Ident(ident) + , TreeWidth(minWidth) + {} + + void Visit(const TShareAccount* node) override + { + Update(node->GetName().size()); + } + + void Visit(const TShareGroup* node) override + { + Update(node->GetName().size()); + Depth++; + node->AcceptInChildren(this); + Depth--; + } + + void Update(size_t width) + { + TreeWidth = Max(TreeWidth, Depth * Ident + width); + } +public: + static size_t Get(const TShareGroup* root, size_t ident = 1, size_t minWidth = 4 /* = len("Name")*/) + { + TNameWidthEvaluator v(ident, minWidth); + v.Visit(root); + return v.TreeWidth; + } +}; + +#define SP_NAME(x) Sprintf("%-*s ", (int)treeWidth, ((depth>0? TString(depth - 1, '|') + "+": "") + ToString(x)).data()) << +#define SP_FOREACH_STATUS(XX) \ + XX("V", Node->V()) \ + XX("w0", Node->w0()) \ + XX("wmax", Node->wmax()) \ + XX("s0", Node->s0()) \ + XX("v", Node->v()) \ + XX("w", Node->w()) \ + XX("D", Node->D()) \ + XX("S", Node->S()) \ + XX("l", Node->l(Eg())) \ + XX("e", Node->e()) \ + XX("a", Node->a(Eg())) \ + XX("Ec", Dc + Sc) \ + /**/ +#define SP_HEAD(h, e) Sprintf(" %11s", h) << +#define SP_ELEM(h, e) Sprintf(" %11.2le", double(e)) << +#define SP_STR(s) SP_HEAD(s, not_used) +#define SP_CTX(n) SP_STR(Sprintf("%s:%.2le", #n, ctx->n).data()) + +class TCtxStatusPrinter + : public IVisitorBase + , public IVisitor<const IContext> + , public IVisitor<const TDeContext> +{ +private: + TStringStream Ss; +public: + void Visit(const IContext*) override + { + Ss << SP_STR("unknown") ""; + } + + void Visit(const TDeContext* ctx) override + { + Ss << SP_STR("density") + SP_CTX(x) + SP_CTX(s) + SP_CTX(u) + SP_CTX(wl) + SP_CTX(p) + SP_CTX(pr) + SP_CTX(pl) + SP_CTX(pf) + SP_CTX(lambda) + SP_CTX(h) + SP_CTX(sigma) + SP_CTX(p0) + SP_CTX(isigma) + SP_CTX(ip0) + ""; + } + + TString Str() const + { + return Ss.Str(); + } + + static TString Get(const TShareNode* node) + { + TCtxStatusPrinter v; + if (const IContext* ctx = node->Ctx()) { + ctx->Accept(&v); + } else { + v.Ss << SP_STR("-") ""; + } + return v.Str(); + } +}; + +class TStatusPrinter : public IConstNodeVisitor { +private: + class TSums : public IConstSimpleNodeVisitor { + public: + const TShareNode* Node; + TSums* PSums; + TEnergy Dc = 0, Sc = 0; // Sums over alll children + + TSums(const TShareAccount* node, TSums* psums) + : Node(node) + , PSums(psums) + {} + + + TSums(const TShareGroup* node, TSums* psums) + : Node(node) + , PSums(psums) + { + node->AcceptInChildren(this); + } + + void Visit(const TShareNode* node) override + { + Dc += node->D(); Sc += node->S(); + } + + void Print(IOutputStream& os, size_t depth, size_t treeWidth) const + { + os << SP_NAME(Node->GetName()) SP_FOREACH_STATUS(SP_ELEM) TCtxStatusPrinter::Get(Node) << "\n"; + } + + TEnergy Eg() const + { + return PSums? PSums->Dc + PSums->Sc: 0; + } + }; + + TStringStream Ss; + size_t TreeWidth; + size_t Depth = 0; + TSums* PSums = nullptr; + + explicit TStatusPrinter(size_t treeWidth) + : TreeWidth(treeWidth) + {} + + void Visit(const TShareAccount* node) override + { + TSums sums(node, PSums); + sums.Print(Ss, Depth, TreeWidth); + } + + void Visit(const TShareGroup* node) override + { + // Stack + TSums* StackedSums = PSums; + + // Recurse + TSums sums(node, PSums); + PSums = &sums; + Depth++; + node->AcceptInChildren(this); + Depth--; + PSums = StackedSums; + + // Print + sums.Print(Ss, Depth, TreeWidth); + } + + TString Str() const + { + return Ss.Str(); + } + +public: + static TString Print(const TSharePlanner* planner) + { + TStringStream ss; + size_t treeWidth = TNameWidthEvaluator::Get(planner->GetRoot()); + size_t depth = 0; // just for macro to work + ss << SP_NAME("Name") SP_FOREACH_STATUS(SP_HEAD) SP_STR("model") "\n"; + TStatusPrinter v(treeWidth); + v.Visit(planner->GetRoot()); + ss << v.Str() << "\n"; + return ss.Str(); + } +}; + +struct TAsciiArt { + size_t TreeWidth; + size_t Depth; + const TShareGroup* Group; + TArtParams Art; + + int Height = 32; + int Width = 36; + int Length = 4 * Width; + double ScaleFactor = 1.0; + double Sigma = 1.0; + + TAsciiArt(size_t treeWidth, size_t depth, const TShareGroup* parent, const TArtParams& art) + : TreeWidth(treeWidth) + , Depth(depth) + , Group(parent) + , Art(art) + { + if (const TDeContext* gctx = dynamic_cast<const TDeContext*>(Group->Ctx())) { + Sigma = gctx->sigma; + } + ScaleFactor = (Art.SigmaStretch? Sigma: 1.0) / Art.Scale; + } + + TLength Xinv(int x) const + { + return double(x) / ScaleFactor / Width * gs * Group->GetTotalVolume(); + } + + int Xcut(TLength x) const + { + return Max<i64>(0, Min<i64>(Length, X(x))); + } + + int X(TLength x) const + { + return llrint(x * ScaleFactor * Width * gs / Group->GetTotalVolume()); + } + + TForce Yinv(int y) const + { + return double(y) / Height * gs; + } + + int Ycut(TForce s) const + { + return Max<i64>(0, Min<i64>(Height, Y(s))); + } + + int Y(TForce s) const + { + return llrint(s * Height / gs); + } +}; + +class TBandPrinter : public IConstSimpleNodeVisitor, public TAsciiArt { +private: + TStringStream Ss; + TString Prefix; + TVector<const TShareNode*> Nodes; + TEnergy Eg = 0; + FWeight wg = 0; +public: + TBandPrinter(size_t treeWidth, size_t depth, const TShareGroup* group, const TArtParams& art) + : TAsciiArt(treeWidth, depth, group, art) + , Prefix(TString(2 * Depth, ' ') + "|" + TString(TreeWidth + 1, ' ')) + { + Height = 0; // Manual height + } + + void Visit(const TShareNode* node) override + { + Nodes.push_back(node); + Eg += node->E(); + wg += node->w(); + Height += 5; + } + + TString Str() + { + TForce x_offset = (Eg - 2 * Group->GetTotalVolume()) / gs; + for (const TShareNode* node : Nodes) { + TString status = node->GetStatus(); + Ss << Sprintf("%-*s ", (int)TreeWidth, (TString(2 * Depth, ' ') + "+-" + ToString(node->GetName())).data()) + << TString(9, '*') + << Sprintf(" Ac:%-9ld De:%-9ld Re:%-9ld Ot:%-9ld *** Done:%-9ld w0:%-7.2le w:%-7.2le ", + node->GetStats().Activations, node->GetStats().Deactivations, + node->GetStats().Retardations, node->GetStats().Overtakes, + node->GetStats().NDone, node->w0(), node->w()) + << TString(Length - 109 - status.size(), '*') + << Sprintf(" %s ***", status.data()) + << Endl; + // Ensure li <= oi <= ei to be able to draw even corrupted or transitional state + int li = Xcut(node->l(Eg) - x_offset); + int oi = Max(li, Xcut(node->o(Eg) - x_offset)); + int e = Max(oi, Xcut(Eg/gs - x_offset)); + int ei = Xcut(node->e() - x_offset); + // ei + // | (4 possible cases) + // .-----------+-----+-----+-----------. + // | | | | + // ei_l li ei_lo oi ei_oe e ei_e + // -----+-----+-----+-----+-----+-----+-----+------ + // ..... ===== ::::: >>>>> OOOOO ----- XXXXX + // ..... ===== ::::: >>>>> OOOOO ----- XXXXX + // -----+-----+-----+-----+-----+-----+-----+-----> x + // + int ei_l = Min(ei, li); + int ei_lo = Max(li, Min(ei, oi)); + int ei_oe = Max(oi, Min(ei, e)); + int ei_e = Max(ei, e); + int h = Max(1, Ycut(node->s0())); + for (int i = 0; i < h; i++) { + Ss << Prefix + << TString(ei_l , '.') + << TString(li - ei_l , '=') + << TString(ei_lo - li , ':') + << TString(oi - ei_lo, '>') + << TString(ei_oe - oi , 'O') + << TString(e - ei_oe, '-') + << TString(ei_e - e , 'X') + << Endl; + } + } + Ss << Sprintf("%-*s ", (int)TreeWidth, (TString(2 * Depth, ' ') + ToString(Group->GetName())).data()) + << TString(61, '*') + << Sprintf(" gw0:%-7.2le gw:%-7.2le ", Group->GetTotalWeight(), wg) + << TString(Length - 87, '*') + << Endl; + return Ss.Str(); + } + + static TString Print(size_t treeWidth, size_t depth, const TShareGroup* group, const TArtParams& art) + { + TBandPrinter v(treeWidth, depth, group, art); + group->AcceptInChildren(&v); + return v.Str(); + } +}; + +class TDensityPlotter + : public IConstSimpleNodeVisitor + , public IVisitor<const TDeContext> + , public IVisitor<const IContext> + , public TAsciiArt { +private: + TStringStream Ss; + TStringStream Err; + TString Prefix; + TVector<TDePoint<const TDeContext>> Points; + TVector<char> Pixels; + TLength Xoffset = 0; +public: + TDensityPlotter(size_t treeWidth, size_t depth, const TShareGroup* group, const TArtParams& art) + : TAsciiArt(treeWidth, depth, group, art) + , Prefix(TString(2 * Depth + TreeWidth + 1, ' ')) + {} + + void Visit(const IContext* ctx) override + { + Err << "unknown context type: " << TypeName(*ctx) << "\n"; + } + + void Visit(const TDeContext* ctx) override + { + ctx->PushPoints(Points); + } + + void Visit(const TShareNode* node) override + { + if (const IContext* ctx = node->Ctx()) { + ctx->Accept(this); + } + } + + TString Str() + { + if (Points.empty()) + Err << "no points"; + if (!Err.Empty()) + return "TDensityPlotter: error: " + Err.Str() + "\n"; + Xoffset = (- 2 * Group->GetTotalVolume()) / gs; + AddLine(); + MakePlot(); + PrintPlot(); + return Ss.Str(); + } + + void AddLine() + { + for (int x = 0; x <= Length; x++) { + double p = Xinv(x) + Xoffset; + Points.push_back(TDePoint<const TDeContext>{nullptr, p, TDepType::Other}); + } + } + + void MakePlot() + { + static const char Signs[] = "+^1.+-"; + Pixels.resize((Length+1) * (Height+1), ' '); + double h = 0.0; + double lambda = 0.0; + TDePoint<const TDeContext>* prev = nullptr; + Sort(Points); + for (TDePoint<const TDeContext>& cur : Points) { + cur.Move(h, lambda, prev); + prev = &cur; + int x = X(cur.p - Xoffset); + int y = Y((1-h) * gs); + char pc = GetPixel(x, y); + char c = Signs[int(cur.Type)]; + if (pc == ' ' || pc == '-' || c == '1') { + if (c == '1') { + if (pc >= '1' && pc <= '8') { + c = pc + 1; + } + if (pc == '9' || pc == '*') { + c = '*'; + } + } + SetPixel(x, y, c); + } + } + TLength p0 = 0; + if (const TDeContext* gctx = dynamic_cast<const TDeContext*>(Group->Ctx())) { + p0 = gctx->p0; + } + int xp0 = X(p0 - Xoffset); + for (int y = 0; y <= Height; y++) { + char pc = GetPixel(xp0, y); + if (pc == ' ') { + SetPixel(xp0, y, ':'); + } + } + } + + char SigmaPixel(int y) + { + return (1.0 - Yinv(y)) > Sigma? ' ': 'X'; + } + + void PrintPlot() + { + for (int y = 0; y <= Height; y++) { + Ss << Prefix + << SigmaPixel(y) + << TString(Pixels.begin() + y*(Length+1), Pixels.begin() + (y+1)*(Length+1)) << Endl; + } + Ss << Sprintf("%-*s ", (int)TreeWidth, (TString(2 * Depth, ' ') + ToString(Group->GetName())).data()) + << 's' << TString(Length + 1, '*') << Endl; + } + + void SetPixel(int x, int y, char c) + { + if (x >= 0 && y >= 0 && x <= Length && y <= Height) { + Pixels[y * (Length+1) + x] = c; + } + } + + char GetPixel(int x, int y) + { + if (x >= 0 && y >= 0 && x <= Length && y <= Height) { + return Pixels[y * (Length+1) + x]; + } else { + return 0; + } + } + + static TString Print(size_t treeWidth, size_t depth, const TShareGroup* group, const TArtParams& art) + { + TDensityPlotter v(treeWidth, depth, group, art); + group->AcceptInChildren(&v); + return v.Str(); + } +}; + +class TTreePrinter : public IConstNodeVisitor { +private: + const TSharePlanner* Planner; + TStringStream Ss; + size_t TreeWidth; + size_t Depth = 0; + bool Band; + bool Model; + TArtParams Art; + + explicit TTreePrinter(const TSharePlanner* planner, size_t treeWidth, bool band, bool model, const TArtParams& art) + : Planner(planner) + , TreeWidth(treeWidth) + , Band(band) + , Model(model) + , Art(art) + {} + + void Visit(const TShareAccount*) override + { + // Do nothing for accounts + } + + void Visit(const TShareGroup* node) override + { + Depth++; + node->AcceptInChildren(this); + Depth--; + if (Band) { + Ss << TBandPrinter::Print(TreeWidth, Depth, node, Art); + } + if (Model) { + switch (Planner->Cfg().GetModel()) { + case PM_STATIC: + break; + case PM_MAX: + break; + case PM_DENSITY: + Ss << TDensityPlotter::Print(TreeWidth, Depth, node, Art); + break; + default: break; + } + } + Ss << Endl; + } + + TString Str() const + { + return Ss.Str(); + } + +public: + static TString Print(const TSharePlanner* planner, bool band, bool model, const TArtParams& art) + { + TTreePrinter v(planner, TNameWidthEvaluator::Get(planner->GetRoot(), 2), band, model, art); + v.Visit(planner->GetRoot()); + return v.Str(); + } +}; + +} diff --git a/ydb/library/planner/share/node.cpp b/ydb/library/planner/share/node.cpp new file mode 100644 index 00000000000..4cee89d9e36 --- /dev/null +++ b/ydb/library/planner/share/node.cpp @@ -0,0 +1,114 @@ +#include "node.h" +#include "group.h" +#include "apply.h" + +namespace NScheduling { + +void TShareNode::SetConfig(TAutoPtr<TShareNode::TConfig> cfg) +{ + Y_ABORT_UNLESS(!Planner, "configure of attached share planner nodes is not allowed"); + Config.Reset(cfg.Release()); +} + +TEnergy TShareNode::ComputeEg() const +{ + if (Parent) { + return Parent->ComputeEc(); + } else { + return E(); + } +} + +TForce TShareNode::CalcGlobalShare() const +{ + return Share * (Parent? Parent->CalcGlobalShare(): 1.0); +} + +void TShareNode::Attach(TSharePlanner* planner, TShareGroup* parent) +{ + Planner = planner; + Parent = parent; + if (Parent) { + Parent->Add(this); + } else { + Share = gs; + } +} + +void TShareNode::Detach() +{ + if (Parent) { + Parent->Remove(this); + } + DetachNoRemove(); +} + +void TShareNode::DetachNoRemove() +{ + Planner = nullptr; + Parent = nullptr; +} + +void TShareNode::Done(TEnergy cost) +{ + Y_ABORT_UNLESS(cost >= 0, "negative work in node '%s' dD=%lf", GetName().data(), cost); + + // Apply current tariff + TEnergy bill = cost * Tariff; + D_ += bill; + dD_ += bill; + Stats.Done(cost); + Stats.Pay(bill); + + if (Parent) { + Parent->Done(cost); // Work is transmitted to parent + } +} + +void TShareNode::Spoil(TEnergy cost) +{ + Y_ABORT_UNLESS(cost >= 0, "negative spoil in node '%s' dS=%lf", GetName().data(), cost); + S_ += cost; + dS_ += cost; + Stats.Spoil(cost); + // Spoil is NOT transmitted to parent +} + +void TShareNode::SetTariff(TDimless tariff) +{ + Tariff = tariff; +} + +void TShareNode::Step() +{ + bool wasActive = IsActive(); + bool wasRetarded = IsRetard(); + pdS_ = dS_; + pdD_ = dD_; + dS_ = 0; + dD_ = 0; + if (!wasActive && IsActive()) + Stats.Activations++; + if (wasActive && !IsActive()) + Stats.Deactivations++; + if (!wasRetarded && IsRetard()) + Stats.Retardations++; + if (wasRetarded && !IsRetard()) + Stats.Overtakes++; +} + +TString TShareNode::GetStatus() const +{ + TString status; + if (pdD() > 0) { + status += "ACTIVE"; + } else { + status += "IDLE"; + } + if (pdS() > 0) { + status += " RETARD"; + } + return status; +} + +} diff --git a/ydb/library/planner/share/node.h b/ydb/library/planner/share/node.h new file mode 100644 index 00000000000..e8197311633 --- /dev/null +++ b/ydb/library/planner/share/node.h @@ -0,0 +1,136 @@ +#pragma once + +#include <util/system/types.h> +#include <util/system/yassert.h> +#include <util/generic/vector.h> +#include <util/generic/list.h> +#include <util/generic/hash_set.h> +#include <util/generic/hash.h> +#include <util/generic/algorithm.h> +#include <util/generic/ptr.h> +#include <ydb/library/planner/base/defs.h> +#include <ydb/library/planner/share/models/model.h> +#include "stats.h" + +namespace NScheduling { + +class IContext; +class IModel; +class TShareAccount; +class TShareGroup; +class TSharePlanner; + +class TShareNode : public IVisitable { +public: + SCHEDULING_DEFINE_VISITABLE(IVisitable); +public: + struct TConfig { + virtual ~TConfig() {} + TString Name; + FWeight Weight; // Weight in parent group + FWeight MaxWeight; + TEnergy Volume; + + TConfig(TEnergy v, FWeight wmax, FWeight w, const TString& name) + : Name(name) + , Weight(w) + , MaxWeight(wmax) + , Volume(v) + { + Y_ABORT_UNLESS(Weight > 0, "non-positive (%lf) weight in planner node '%s'", + Weight, Name.data()); + Y_ABORT_UNLESS(Weight <= MaxWeight, "max weight (%lf) must be greater or equal to normal weight (%lf) in planner node '%s'", + MaxWeight, Weight, Name.data()); + Y_ABORT_UNLESS(Volume >= 0, "negative (%lf) volume in planner node '%s'", + Volume, Name.data()); + } + }; +protected: + THolder<IContext> Context; + THolder<TConfig> Config; + TSharePlanner* Planner = nullptr; + TShareGroup* Parent = nullptr; + TForce Share = 0; // s0[i] = w0[i] / sum(w0[i] for i in parent group) -- default share in parent group + TEnergy D_ = 0; // Work done + TEnergy S_ = 0; // Spoiled energy + TEnergy dD_ = 0; + TEnergy dS_ = 0; + TEnergy pdD_ = 0; + TEnergy pdS_ = 0; + TDimless Tariff = 1.0; + TNodeStats Stats; +public: + explicit TShareNode(TAutoPtr<TConfig> cfg) + : Config(cfg.Release()) + {} + virtual ~TShareNode() {} + const TConfig& Cfg() const { return *Config.Get(); } + void SetConfig(TAutoPtr<TConfig> cfg); +public: // Accessors + const TString& GetName() const { return Cfg().Name; } + TShareGroup* GetParent() { return Parent; } + const TShareGroup* GetParent() const { return Parent; } + TSharePlanner* GetPlanner() { return Planner; } + const TSharePlanner* GetPlanner() const { return Planner; } + TForce GetShare() const { return Share; } + void SetShare(TForce value) { Share = value; } +public: // Computations + TLength Normalize(TEnergy cost) const { return cost / s0(); } + TEnergy ComputeEg() const; + TForce CalcGlobalShare() const; +public: // Short accessors for formulas + TForce s0() const { return Share; } + FWeight w0() const { return Cfg().Weight; } + FWeight wmax() const { return Cfg().MaxWeight; } + FWeight w() const { return Context? Ctx()->GetWeight(): w0(); } + TDimless h() const { return (w() - w0()) / (wmax() - w0()); } + TEnergy D() const { return D_; } + TEnergy S() const { return S_; } + TEnergy dD() const { return dD_; } + TEnergy dS() const { return dS_; } + TEnergy pdD() const { return pdD_; } + TEnergy pdS() const { return pdS_; } + TEnergy E() const { return D_ + S_; } + TEnergy V() const { return Cfg().Volume; } + TEnergy L(TEnergy Eg) const { return Eg*Share/gs - V(); } + TEnergy O(TEnergy Eg) const { return L(Eg); } + TEnergy A(TEnergy Eg) const { return E() - L(Eg); } + TEnergy P(TEnergy Eg) const { return E() - Eg*Share/gs; } + TLength dd() const { return dD() / Share; } + TLength ds() const { return dS() / Share; } + TLength pdd() const { return pdD() / Share; } + TLength pds() const { return pdS() / Share; } + TLength e() const { return E() / Share; } + TLength v() const { return V() / Share; } + TLength l(TEnergy Eg) const { return L(Eg) / Share; } + TLength o(TEnergy Eg) const { return O(Eg) / Share; } + TLength a(TEnergy Eg) const { return A(Eg) / Share; } + TLength p(TEnergy Eg) const { return e() - Eg/gs; } +public: + void SetS(TEnergy value) { S_ = value; } + void SetD(TEnergy value) { D_ = value; } +public: // Context for models + IContext* Ctx() { return Context.Get(); } + const IContext* Ctx() const { return Context.Get(); } + void ResetCtx(IContext* ctx) { Context.Reset(ctx); } + template <class TCtx> + TCtx& Ctx() { return *static_cast<TCtx*>(Context.Get()); } + template <class TCtx> + TCtx* CtxAs() { return dynamic_cast<TCtx*>(Context.Get()); } +public: // Tree modifications + void Attach(TSharePlanner* planner, TShareGroup* parent); + void Detach(); + void DetachNoRemove(); +public: // Evolution + void Done(TEnergy cost); + void Spoil(TEnergy cost); + void SetTariff(TDimless tariff); + void Step(); +public: // Monitoring + bool IsActive() const { return pdD() > 0; } + bool IsRetard() const { return pdS() > 0; } + const TNodeStats& GetStats() const { return Stats; } + virtual TString GetStatus() const; +}; + +} diff --git a/ydb/library/planner/share/node_visitor.h b/ydb/library/planner/share/node_visitor.h new file mode 100644 index 00000000000..3f7d6e1a299 --- /dev/null +++ b/ydb/library/planner/share/node_visitor.h @@ -0,0 +1,42 @@ +#pragma once + +#include <ydb/library/planner/base/defs.h> +#include <ydb/library/planner/base/visitor.h> + +namespace NScheduling { + +class TShareNode; +class TShareAccount; +class TShareGroup; + +class INodeVisitor + : public IVisitorBase + , public IVisitor<TShareAccount> + , public IVisitor<TShareGroup> +{ +public: + using IVisitor<TShareAccount>::Visit; + using IVisitor<TShareGroup>::Visit; +}; + +class IConstNodeVisitor + : public IVisitorBase + , public IVisitor<const TShareAccount> + , public IVisitor<const TShareGroup> +{ +public: + using IVisitor<const TShareAccount>::Visit; + using IVisitor<const TShareGroup>::Visit; +}; + +class ISimpleNodeVisitor + : public IVisitorBase + , public IVisitor<TShareNode> +{}; + +class IConstSimpleNodeVisitor + : public IVisitorBase + , public IVisitor<const TShareNode> +{}; + +} diff --git a/ydb/library/planner/share/probes.cpp b/ydb/library/planner/share/probes.cpp new file mode 100644 index 00000000000..478de8a4b13 --- /dev/null +++ b/ydb/library/planner/share/probes.cpp @@ -0,0 +1,3 @@ +#include "probes.h" + +LWTRACE_DEFINE_PROVIDER(SCHEDULING_SHAREPLANNER_PROVIDER) diff --git a/ydb/library/planner/share/probes.h b/ydb/library/planner/share/probes.h new file mode 100644 index 00000000000..2b39253ec21 --- /dev/null +++ b/ydb/library/planner/share/probes.h @@ -0,0 +1,123 @@ +#pragma once + +#include <ydb/library/planner/base/defs.h> +#include <ydb/library/planner/share/protos/shareplanner.pb.h> +#include <library/cpp/lwtrace/all.h> + +namespace NScheduling { + +// Helper class for printing cost in percents of total planner capacity +struct TModelField { + typedef int TStoreType; + static void ToString(int value, TString* out) { + *out = Sprintf("%d(%s)", value, EPlanningModel_Name((EPlanningModel)value).c_str()); + } +}; + +} + +#define PLANNER_PROBE(name, ...) GLOBAL_LWPROBE(SCHEDULING_SHAREPLANNER_PROVIDER, name, ## __VA_ARGS__) + +#define SCHEDULING_SHAREPLANNER_PROVIDER(PROBE, EVENT, GROUPS, TYPES, NAMES) \ + PROBE(Done, GROUPS("Scheduling", "SharePlannerInterface", "SharePlannerAccount"), \ + TYPES(TString,TString,double,double), \ + NAMES("planner","account","cost","time")) \ + PROBE(Waste, GROUPS("Scheduling", "SharePlannerInterface"), \ + TYPES(TString,double), \ + NAMES("planner","cost")) \ + PROBE(Run, GROUPS("Scheduling", "SharePlannerInterface"), \ + TYPES(TString), \ + NAMES("planner")) \ + PROBE(Configure, GROUPS("Scheduling", "SharePlannerInterface"), \ + TYPES(TString, TString), \ + NAMES("planner", "cfg")) \ + PROBE(SerializeHistory, GROUPS("Scheduling", "SharePlannerInterface"), \ + TYPES(TString), \ + NAMES("planner")) \ + PROBE(DeserializeHistoryBegin, GROUPS("Scheduling", "SharePlannerInterface"), \ + TYPES(TString), \ + NAMES("planner")) \ + PROBE(DeserializeHistoryEnd, GROUPS("Scheduling", "SharePlannerInterface"), \ + TYPES(TString, bool), \ + NAMES("planner", "success")) \ + PROBE(Add, GROUPS("Scheduling", "SharePlannerInterface"), \ + TYPES(TString,TString,NScheduling::TWeight,TString), \ + NAMES("planner","parent","weight","node")) \ + PROBE(Delete, GROUPS("Scheduling", "SharePlannerInterface"), \ + TYPES(TString,TString), \ + NAMES("planner","node")) \ + \ + PROBE(FirstActivation, GROUPS("Scheduling", "SharePlannerDetails"), \ + TYPES(TString), \ + NAMES("planner")) \ + PROBE(LastDeactivation, GROUPS("Scheduling", "SharePlannerDetails"), \ + TYPES(TString), \ + NAMES("planner")) \ + PROBE(CommitInfo, GROUPS("Scheduling", "SharePlannerDetails"), \ + TYPES(TString, bool,bool,TString), \ + NAMES("planner","model","run","info")) \ + PROBE(Advance, GROUPS("Scheduling", "SharePlannerDetails"), \ + TYPES(TString,double,double), \ + NAMES("planner","cost","wcost")) \ + PROBE(TryDeactivate, GROUPS("Scheduling", "SharePlannerDetails"), \ + TYPES(TString), \ + NAMES("planner")) \ + PROBE(Spoil, GROUPS("Scheduling", "SharePlannerDetails", "SharePlannerAccount"), \ + TYPES(TString,TString,double), \ + NAMES("planner","account","cost")) \ + PROBE(SetRate, GROUPS("Scheduling", "SharePlannerDetails", "SharePlannerAccount"), \ + TYPES(TString,TString,double,double), \ + NAMES("planner","account","oldrate","newrate")) \ + PROBE(SetAssuredRate, GROUPS("Scheduling", "SharePlannerDetails", "SharePlannerAccount"), \ + TYPES(TString,TString,double,double), \ + NAMES("planner","account","oldrate","newrate")) \ + PROBE(ResetRate, GROUPS("Scheduling", "SharePlannerDetails"), \ + TYPES(TString,TString,double), \ + NAMES("planner","group","rate")) \ + PROBE(ResetAssuredRate, GROUPS("Scheduling", "SharePlannerDetails"), \ + TYPES(TString,TString,double), \ + NAMES("planner","group","rate")) \ + PROBE(SetTL, GROUPS("Scheduling", "SharePlannerDetails"), \ + TYPES(TString,double,double), \ + NAMES("planner","old","new")) \ + PROBE(CalcFixedHyperbolicWeight, GROUPS("Scheduling", "SharePlannerDetails", "SharePlannerAccount", "SharePlannerWeight"), \ + TYPES(TString,TString,double,double,double,double,double), \ + NAMES("planner","account","l","x","X","Xxi","result")) \ + PROBE(CalcFixedHyperbolicWeight_I, GROUPS("Scheduling", "SharePlannerDetails", "SharePlannerAccount", "SharePlannerWeight"), \ + TYPES(TString,TString,double,double,double,double,double), \ + NAMES("planner","account","D_R","W","WMax","xi_min","RepaymentPeriod")) \ + PROBE(FloatingLinearModel, GROUPS("Scheduling", "SharePlannerDetails", "SharePlannerAccount", "SharePlannerWeight"), \ + TYPES(TString,TString,double,double,double,double,double, double, double), \ + NAMES("planner","account","w_prev","rho","h","u","dD","xsum","wsum")) \ + PROBE(FloatingLinearModel_I, GROUPS("Scheduling", "SharePlannerDetails", "SharePlannerAccount", "SharePlannerWeight"), \ + TYPES(TString,TString,double,double,double,double,double,double,double,double), \ + NAMES("planner","account","w0","wm","RepaymentPeriod","a0","amin","a","w","result")) \ + \ + PROBE(ActivePoolPush, GROUPS("Scheduling", "SharePlannerDetails", "SharePlannerHeaps", "SharePlannerAccount"), \ + TYPES(TString,TString,double,size_t), \ + NAMES("planner","account","until","newsize")) \ + PROBE(ActivePoolPop, GROUPS("Scheduling", "SharePlannerDetails", "SharePlannerHeaps", "SharePlannerAccount"), \ + TYPES(TString,TString,size_t), \ + NAMES("planner","account","newsize")) \ + PROBE(ActivePoolPeek, GROUPS("Scheduling", "SharePlannerDetails", "SharePlannerHeaps", "SharePlannerAccount"), \ + TYPES(TString,TString,double,size_t), \ + NAMES("planner","account","until","size")) \ + PROBE(ActivePoolRebuild, GROUPS("Scheduling", "SharePlannerDetails", "SharePlannerHeaps"), \ + TYPES(TString,size_t,size_t), \ + NAMES("planner","badnonidle","size")) \ + PROBE(ActivePoolIncBad, GROUPS("Scheduling", "SharePlannerDetails", "SharePlannerHeaps", "SharePlannerAccount"), \ + TYPES(TString,TString,size_t), \ + NAMES("planner","account","new")) \ + PROBE(ActivePoolDecBad, GROUPS("Scheduling", "SharePlannerDetails", "SharePlannerHeaps", "SharePlannerAccount"), \ + TYPES(TString,TString,size_t), \ + NAMES("planner","account","new")) \ + \ + PROBE(Deactivate, GROUPS("Scheduling", "SharePlannerDetails", "SharePlannerAccountTransitions", "SharePlannerAccount"), \ + TYPES(TString,TString), \ + NAMES("planner","account")) \ + PROBE(Activate, GROUPS("Scheduling", "SharePlannerDetails", "SharePlannerAccountTransitions", "SharePlannerAccount"), \ + TYPES(TString,TString), \ + NAMES("planner","account")) \ + /**/ + +LWTRACE_DECLARE_PROVIDER(SCHEDULING_SHAREPLANNER_PROVIDER) diff --git a/ydb/library/planner/share/protos/shareplanner.proto b/ydb/library/planner/share/protos/shareplanner.proto new file mode 100644 index 00000000000..8b746b1f5b0 --- /dev/null +++ b/ydb/library/planner/share/protos/shareplanner.proto @@ -0,0 +1,38 @@ +package NScheduling; + +option java_package = "ru.yandex.scheduling.proto"; + +enum EPlanningModel { + PM_STATIC = 0; + PM_MAX = 1; + PM_DENSITY = 2; +} + +enum EPullType { + PT_STRICT = 0; + PT_INSENSITIVE = 1; + PT_NONE = 2; +} + +enum EBillingType { + BT_STATIC = 0; + BT_PRESENT_SHARE = 1; +} + +message TSharePlannerConfig { + optional string Name = 1 [ default = "shareplanner" ]; + + // Puller params + optional EPullType Pull = 2 [ default = PT_STRICT ]; + optional double PullLength = 8 [ default = 100 ]; + + // Model params + optional EPlanningModel Model = 3 [ default = PM_DENSITY ]; + optional double DenseLength = 4 [ default = 1.0 ]; + optional double AveragingLength = 5 [ default = 0.1 ]; + + // Billing params + optional EBillingType Billing = 6 [ default = BT_STATIC ]; + optional double StaticTariff = 7 [ default = 1.0 ]; // Used only if Billing=BT_STATIC + optional bool BillingIsMemoryless = 9; // Used only if Billing=BT_PRESENT_SHARE +} diff --git a/ydb/library/planner/share/protos/shareplanner_history.proto b/ydb/library/planner/share/protos/shareplanner_history.proto new file mode 100644 index 00000000000..cb9fe0e4aca --- /dev/null +++ b/ydb/library/planner/share/protos/shareplanner_history.proto @@ -0,0 +1,22 @@ +package NScheduling; + +option java_package = "ru.yandex.scheduling.proto"; + +message TShareNodeHistory { + optional string Name = 1; + optional double Proficit = 2; +} + +// +//message TUtilizationHistory { +// repeated int64 DoneInSlot = 1; +// optional int64 SlotVolume = 2; +// optional uint64 CurrentSlot = 3; +//} +// + +message TSharePlannerHistory { + repeated TShareNodeHistory Nodes = 1; +// optional TUtilizationHistory Utilization = 3; +} + diff --git a/ydb/library/planner/share/protos/shareplanner_sensors.proto b/ydb/library/planner/share/protos/shareplanner_sensors.proto new file mode 100644 index 00000000000..a7a00b66007 --- /dev/null +++ b/ydb/library/planner/share/protos/shareplanner_sensors.proto @@ -0,0 +1,88 @@ +import "library/cpp/monlib/encode/legacy_protobuf/protos/metric_meta.proto"; + +package NScheduling; + +option java_package = "ru.yandex.scheduling.proto"; + +message TShareNodeSensors { + optional string Name = 1; + optional string ParentName = 2; + + // Planner outcome stats for requests + optional uint64 NDone = 11 [ (NMonProto.Metric).Type = RATE ]; + optional uint64 NSpoiled = 12 [ (NMonProto.Metric).Type = RATE ]; + + // Resource usage [cluster-power * sec] + optional double ResDone = 13 [ (NMonProto.Metric).Type = RATE ]; + optional double ResSpoiled = 14 [ (NMonProto.Metric).Type = RATE ]; + optional double MoneySpent = 29 [ (NMonProto.Metric).Type = RATE ]; + optional double Proficit = 15 [ (NMonProto.Metric).Type = GAUGE ]; + //optional uint64 ResDone = 16 [ (NMonProto.Metric).Type = RATE ]; DEPRECATED + //optional int64 Deficit = 17 [ (NMonProto.Metric).Type = GAUGE ]; DEPRECATED + + // Lag is proficit normalized for account's parent group + optional double Lag = 18 [ (NMonProto.Metric).Type = GAUGE ]; + optional double FloatingLag = 19 [ (NMonProto.Metric).Type = GAUGE ]; + + // Consumer status count (0 - is not current state; 1 - is current state) + optional uint64 Idle = 20 [ (NMonProto.Metric).Type = GAUGE ]; + optional uint64 Active = 21 [ (NMonProto.Metric).Type = GAUGE ]; + optional uint64 IdleRetard = 22 [ (NMonProto.Metric).Type = GAUGE ]; + optional uint64 ActiveRetard = 23 [ (NMonProto.Metric).Type = GAUGE ]; + + // Consumer event stats + optional uint64 Activations = 30 [ (NMonProto.Metric).Type = RATE ]; + optional uint64 Deactivations = 31 [ (NMonProto.Metric).Type = RATE ]; + optional uint64 Retardations = 32 [ (NMonProto.Metric).Type = RATE ]; + optional uint64 Overtakes = 33 [ (NMonProto.Metric).Type = RATE ]; + + // Global shares [between 0 and 1] + optional double DefShare = 40 [ (NMonProto.Metric).Type = GAUGE ]; + optional double RealShare = 41 [ (NMonProto.Metric).Type = GAUGE ]; + optional double AvgRealShare = 42 [ (NMonProto.Metric).Type = GAUGE ]; + optional double DynamicShare = 43 [ (NMonProto.Metric).Type = GAUGE ]; + + // Group shares [between 0 and 1] + optional double GrpDefShare = 60 [ (NMonProto.Metric).Type = GAUGE ]; + optional double GrpRealShare = 61 [ (NMonProto.Metric).Type = GAUGE ]; + optional double GrpAvgRealShare = 62 [ (NMonProto.Metric).Type = GAUGE ]; + optional double GrpDynamicShare = 63 [ (NMonProto.Metric).Type = GAUGE ]; + + // Weights information + optional double DefWeight = 51 [ (NMonProto.Metric).Type = GAUGE ]; + optional double MaxWeight = 52 [ (NMonProto.Metric).Type = GAUGE ]; + optional double MinWeight = 53 [ (NMonProto.Metric).Type = GAUGE ]; + optional double DynamicWeight = 54 [ (NMonProto.Metric).Type = GAUGE ]; + + // Density model specific + optional double Usage = 70 [ (NMonProto.Metric).Type = GAUGE ]; + optional double InstantUsage = 74 [ (NMonProto.Metric).Type = GAUGE ]; + optional double Tariff = 71 [ (NMonProto.Metric).Type = GAUGE ]; + optional double InstantTariff = 76 [ (NMonProto.Metric).Type = GAUGE ]; + optional double Boost = 72 [ (NMonProto.Metric).Type = GAUGE ]; + optional double FloatingOrigin = 73 [ (NMonProto.Metric).Type = GAUGE ]; + optional double InstantOrigin = 75 [ (NMonProto.Metric).Type = GAUGE ]; + optional double PullStretch = 77 [ (NMonProto.Metric).Type = GAUGE ]; + optional double Retardness = 78 [ (NMonProto.Metric).Type = GAUGE ]; + optional double RetardShare = 79 [ (NMonProto.Metric).Type = GAUGE ]; + + // Parent-specific sensors (set only for groups) + optional double TotalCredit = 100 [ (NMonProto.Metric).Type = GAUGE ]; +} + +message TSharePlannerSensors { + repeated TShareNodeSensors Accounts = 1 [ (NMonProto.Metric).Path = false, (NMonProto.Metric).Keys = "account:Name" ]; + repeated TShareNodeSensors Groups = 2 [ (NMonProto.Metric).Path = false, (NMonProto.Metric).Keys = "group:Name" ]; + repeated TShareNodeSensors Nodes = 4 [ (NMonProto.Metric).Path = false, (NMonProto.Metric).Keys = "node:Name parent:ParentName" ]; + optional TShareNodeSensors Total = 3 [ (NMonProto.Metric).Path = true ]; // All accounts w/o groups + + // Resource totals + optional double ResUsed = 10 [ (NMonProto.Metric).Type = RATE ]; + optional double ResWasted = 11 [ (NMonProto.Metric).Type = RATE ]; + + // Utilization measuments + optional double ResUsedInWindow = 20 [ (NMonProto.Metric).Type = GAUGE ]; + optional double ResWastedInWindow = 21 [ (NMonProto.Metric).Type = GAUGE ]; + optional double Utilization = 22 [ (NMonProto.Metric).Type = GAUGE ]; +} + diff --git a/ydb/library/planner/share/protos/ya.make b/ydb/library/planner/share/protos/ya.make new file mode 100644 index 00000000000..cdccb9b0be7 --- /dev/null +++ b/ydb/library/planner/share/protos/ya.make @@ -0,0 +1,15 @@ +PROTO_LIBRARY() + +PEERDIR( + library/cpp/monlib/encode/legacy_protobuf/protos +) + +SRCS( + shareplanner_sensors.proto + shareplanner.proto + shareplanner_history.proto +) + +EXCLUDE_TAGS(GO_PROTO) + +END() diff --git a/ydb/library/planner/share/pull.h b/ydb/library/planner/share/pull.h new file mode 100644 index 00000000000..a335a6bf4d9 --- /dev/null +++ b/ydb/library/planner/share/pull.h @@ -0,0 +1,208 @@ +#pragma once + +#include <util/generic/algorithm.h> +#include "apply.h" +#include "node_visitor.h" +#include "shareplanner.h" +#include <ydb/library/planner/share/models/density.h> + +namespace NScheduling { + +// It pulls retarded users in group up to their left edge immediately +class TStrictPuller : public ISimpleNodeVisitor { +private: + struct TNodeInfo { + TShareNode* Node; + TLength Key; + + explicit TNodeInfo(TShareNode& n) + : Node(&n) + , Key(double(n.D() + n.S() + n.V()) / n.s0()) // e[i] + v[i] + {} + + bool operator<(const TNodeInfo& o) const + { + return Key > o.Key; // Greater is used to build min-heap + } + }; + + TShareGroup* Group = nullptr; + TVector<TNodeInfo> Infos; + TForce s0R = 0; // sum(s0[i] for i in retarded nodes) + TEnergy E_ = 0; // sum(E[i] for i in Group) + TEnergy dS = 0; +public: + void Pull(TShareGroup* group) + { + StartGroup(group); + group->AcceptInChildren(this); + FinishGroup(); + } +private: + void StartGroup(TShareGroup* group) + { + Group = group; + Infos.clear(); + s0R = 0; + E_ = 0; + dS = 0; + } + + void Visit(TShareNode* node) override + { + Infos.push_back(TNodeInfo(*node)); + E_ += node->E(); + } + + TEnergy E() const { return E_ + dS; } + TLength e() const { return E() / gs; } + + void FinishGroup() + { + if (Infos.size() <= 1) { + return; // If there is less than 2 nodes, then pull is not needed + } + // Iterate over all nodes in Group sorted by e[i] + v[i] + MakeHeap(Infos.begin(), Infos.end()); + auto last = Infos.end(); + auto second = Infos.begin() + 1; // avoid pulling first node which otherwise lead to dS=inf (s0R - gs == 0.0) + for (; last != second; ) { + TLength de = e() - Infos.front().Key; + if (de <= 0) { + break; // Stop if no more retarded nodes + } + + PopHeap(Infos.begin(), last); + TNodeInfo& ni = *--last; + TShareNode& n = *ni.Node; + s0R += n.s0(); + dS += de * gs * n.s0() / (gs - s0R); + } + + // Iterate backwards over retarded nodes in Group + for (; last != Infos.end(); ++last) { + TNodeInfo& ni = *last; + TShareNode& n = *ni.Node; + TEnergy dSi = e() * n.s0() - n.V() - n.E(); + if (dSi > 0) { // Negative value may appear only due to floating-point arithmetic inaccuracy + n.Spoil(dSi); + } + } + } +}; + +// It pulls all retarded users in group for the same amount of normalized proficit +// It also calculates left edge relative to the floating origin (which in turn is +// insensitive to absent users) +class TInsensitivePuller { +private: + TLength Length; +public: + TInsensitivePuller(TLength length) + : Length(length) + {} + + void Pull(TShareGroup* group) + { + // This puller is assumed to be used only with density model + TDeContext* gctx = group->CtxAs<TDeContext>(); + if (!gctx) + return; // Most probably new group + + // (1) Compute p0 based on u computed in last model run + // (Note that gctx->p0 is updated only on model runs, so we cannot use it directly + // And it is assumed that iu is not changing very fast, so ctx->iu is good enough) + // (2) And also compute dD since last pull in the same traverse + TEnergy dD = 0; + TForce sigma = 0; + TEnergy p0sigma = 0; + TEnergy Ec = group->ComputeEc(); + ApplyTo<TShareNode>(group, [=, &dD, &sigma, &p0sigma] (TShareNode* node) { + if (TDeContext* ctx = node->CtxAs<TDeContext>()) { + // Avoid pulling with big dD just after turning ON + if (ctx->Pull.Cycle == ctx->ModelCycle || ctx->Pull.Cycle == ctx->ModelCycle + 1) { + TEnergy dDi = node->D() - ctx->Pull.Dlast; + dD += dDi; + } + ctx->Pull.Cycle = ctx->ModelCycle + 1; + ctx->Pull.Dlast = node->D(); + + TForce sigma_i = ctx->iu * node->s0(); + sigma += sigma_i; + p0sigma += sigma_i * node->p(Ec); + } + }); + TLength p0 = p0sigma / sigma; + + // Take movement into account + p0 -= dD / gs; + TForce s0R_prev = gctx->Pull.s0R; // We do not want solve system of equations, so just use previous value of s0R + if (s0R_prev > 0.0 && s0R_prev < 1.0) { + TEnergy dS_estimate = s0R_prev / (1 - s0R_prev) * dD; // See formula explanation below + p0 -= dS_estimate / gs; + } + + // Find retards and compute their total share + TForce s0R = 0; + ApplyTo<TShareNode>(group, [=, &s0R] (TShareNode* node) { + if (TDeContext* ctx = node->CtxAs<TDeContext>()) { + ctx->Pull.stretch = Min(2.0, Max(0.0, p0 - node->p(Ec) - node->v() + Length) / Length); + ctx->Pull.retardness = Min(1.0, ctx->Pull.stretch); // To avoid s0R jumps + if (ctx->Pull.retardness > 0.0) { // Retard found + s0R += node->s0() * ctx->Pull.retardness; + } + } + }); + gctx->Pull.s0R = s0R; // Just for analytics + + if (s0R > 0.0 && s0R < 1.0) { + // EXPLANATION FOR FORMULA + // If we assume: + // (1) dsi = dd # we pull accounts for the same distance [metre] as non-retarded accounts + // (2) dSi = s0i/s0R * dS # we pull each retarded account for the same distance + // We can conclude that: + // (1) => dSi / s0i = dD / (1 - s0R) + // (2) => dS / s0R = dD / (1 - s0R) + // Therefore: dS = s0R / (1 - s0R) * dD + TEnergy dS = s0R / (1 - s0R) * dD; + + // Pull retards + ApplyTo<TShareNode>(group, [=] (TShareNode* node) { + if (TDeContext* ctx = node->CtxAs<TDeContext>()) { + if (ctx->Pull.retardness > 0.0) { + TEnergy dSi = node->s0() * ctx->Pull.stretch / s0R * dS; + if (dSi > 0) { // Just to avoid floating-point arithmetic errors + node->Spoil(dSi); + } + } + } + }); + } + } +}; + +// It recursively runs given visitor in each group in tree +template <class TPuller> +class TRecursivePuller : public INodeVisitor +{ +private: + TPuller Puller; +public: + template <class... TArgs> + explicit TRecursivePuller(TArgs... args) + : Puller(args...) + {} + + void Visit(TShareAccount*) override + { + // Do nothing for accounts + } + + void Visit(TShareGroup* node) override + { + node->AcceptInChildren(this); + Puller.Pull(node); + } +}; + +} diff --git a/ydb/library/planner/share/shareplanner.cpp b/ydb/library/planner/share/shareplanner.cpp new file mode 100644 index 00000000000..736b096328d --- /dev/null +++ b/ydb/library/planner/share/shareplanner.cpp @@ -0,0 +1,186 @@ +#include "shareplanner.h" +#include <util/stream/str.h> +#include <util/string/hex.h> +#include <util/string/cast.h> +#include <util/string/printf.h> +#include <ydb/library/planner/share/protos/shareplanner.pb.h> +#include "pull.h" +#include "billing.h" +#include "step.h" +#include <ydb/library/planner/share/models/static.h> +#include <ydb/library/planner/share/models/max.h> +#include <ydb/library/planner/share/models/density.h> +#include "history.h" +#include "monitoring.h" + +namespace NScheduling { + +TSharePlanner::TSharePlanner(const TSharePlannerConfig& cfg) + : Root(nullptr) + //, UtilMeter(new TUtilizationMeter()) + , Stepper(new TStepper()) +{ + Configure(cfg); +} + +void TSharePlanner::Configure(const TSharePlannerConfig& cfg) +{ + PLANNER_PROBE(Configure, GetName(), cfg.DebugString()); + Config = cfg; + EPullType pullType = Config.GetPull(); + EBillingType billingType = Config.GetBilling(); + if (pullType == PT_INSENSITIVE && Config.GetModel() != PM_DENSITY) { + // Insensitive puller can be used only with density model + // because it uses some data (p0) from its context. + // Otherwise fallback to strict puller + pullType = PT_STRICT; + } + + TDimless staticTariff = Config.GetStaticTariff(); + if ((billingType == BT_PRESENT_SHARE) + && Config.GetModel() != PM_DENSITY) { + // Present share billing can be used only with density model + // because they use some data (sigma/isigma) from its context. + // Otherwise do not use billing at all + billingType = BT_STATIC; + staticTariff = 1.0; + } + switch (pullType) { + default: // for forward compatibility + case PT_STRICT: Puller.Reset(new TRecursivePuller<TStrictPuller>()); break; + case PT_INSENSITIVE: Puller.Reset(new TRecursivePuller<TInsensitivePuller>(Config.GetPullLength())); break; + case PT_NONE: Puller.Destroy(); break; + } + switch (Config.GetModel()) { + case PM_STATIC: Model.Reset(new TStaticModel(this)); break; + case PM_MAX: Model.Reset(new TMaxModel(this)); break; + default: // for forward compatibility + case PM_DENSITY: Model.Reset(new TDensityModel(this)); break; + } + switch (billingType) { + default: // for forward compatibility + case BT_STATIC: Billing.Reset(new TStaticBilling(staticTariff)); break; + case BT_PRESENT_SHARE: Billing.Reset(new TPresentShareBilling(Config.GetBillingIsMemoryless())); break; + } +} + +void TSharePlanner::Advance(TEnergy cost, bool wasted) +{ + PLANNER_PROBE(Advance, GetName(), cost, wasted); + Y_ASSERT(cost >= 0); + + // Adjust totals + if (!wasted) { + D_ += cost; + dD_ += cost; + Stats.ResUsed += cost; + } else { + W_ += cost; + dW_ += cost; + Stats.ResWasted += cost; + } + + //UtilMeter->Add(cost, wasted); // Adjust utilization history +} + +void TSharePlanner::Done(TShareAccount* acc, TEnergy cost) +{ + Y_ASSERT(cost >= 0); + acc->Done(cost); + PLANNER_PROBE(Done, GetName(), acc->GetName(), cost, acc->Normalize(cost)); + Advance(cost, false); +} + +void TSharePlanner::Waste(TEnergy cost) +{ + PLANNER_PROBE(Waste, GetName(), cost); + Advance(cost, true); +} + +void TSharePlanner::Commit(bool model) +{ + if (!Puller && !Model && !Billing) { + return; // We are not configured yet + } + if (Puller) { + Root->Accept(Puller.Get()); + } + bool run = false; + if (model && (dD_ > 0 || dW_ > 0)) { + PLANNER_PROBE(Run, GetName()); + if (Model) { + Model->Run(Root); + if (Billing) { + Root->Accept(Billing.Get()); + } + } + Root->Accept(Stepper.Get()); + pdD_ = dD_; + pdW_ = dW_; + dD_ = dW_ = 0; + run = true; + } + PLANNER_PROBE(CommitInfo, GetName(), model, run, PrintInfo()); +} + +void TSharePlanner::OnDetach(TShareNode* node) +{ + if (Model) { + Model->OnDetach(node); + } + node->Detach(); +} + +void TSharePlanner::OnAttach(TShareNode* node, TShareGroup* parent) +{ + node->Attach(this, parent); + if (Model) { + Model->OnAttach(node); + } +} + +TString TSharePlanner::PrintStatus() const +{ + return TStatusPrinter::Print(this); +} + +TString TSharePlanner::PrintTree(bool band, bool model, const TArtParams& art) const +{ + return TTreePrinter::Print(this, band, model, art); +} + +TString TSharePlanner::PrintStats() const +{ + TStringStream ss; + ss << " === PLANNER ===\n" + << " D:" << D_ << "\n" + << " W:" << W_ << "\n" + << " dD:" << dD_ << "\n" + << " dW:" << dW_ << "\n" +// << " Used:" << UtilMeter->Used() << "\n" +// << " Wasted:" << UtilMeter->Wasted() << "\n" +// << " Utilization:" << UtilMeter->Utilization() << "\n" + << "\n" + ; + return ss.Str(); +} + +TString TSharePlanner::PrintInfo(bool status, bool band, bool model, bool stats, const TArtParams& art) const +{ + TStringStream ss; + ss << Endl; + if (status) + ss << PrintStatus(); + if (band || model) + ss << PrintTree(band, model, art); + if (stats) + ss << PrintStats(); + return ss.Str(); +} + +void TSharePlanner::GetStats(TSharePlannerStats& stats) const +{ + stats = Stats; +} + +} diff --git a/ydb/library/planner/share/shareplanner.h b/ydb/library/planner/share/shareplanner.h new file mode 100644 index 00000000000..71b8f8bd1c8 --- /dev/null +++ b/ydb/library/planner/share/shareplanner.h @@ -0,0 +1,332 @@ +#pragma once + +#include <cmath> +#include <util/system/types.h> +#include <util/system/yassert.h> +#include <util/generic/vector.h> +#include <util/generic/list.h> +#include <util/generic/hash_set.h> +#include <util/generic/hash.h> +#include <util/generic/algorithm.h> +#include <util/generic/ptr.h> +#include <ydb/library/planner/share/protos/shareplanner.pb.h> +#include <ydb/library/planner/share/models/model.h> +#include "probes.h" +#include "node.h" +#include "account.h" +#include "group.h" +//#include <ydb/library/planner/share/utilization.h> + +namespace NScheduling { + +struct TArtParams { + bool SigmaStretch; + double Scale; + + TArtParams(bool sigmaStretch = true, double scale = 1.0) + : SigmaStretch(sigmaStretch) + , Scale(scale) + {} +}; + +struct TSharePlannerStats { + double ResUsed = 0; + double ResWasted = 0; +}; + +class TSharePlanner { +protected: + // Configuration + TSharePlannerConfig Config; + + TShareGroup* Root = nullptr; // Root must be set by derived class (e.g. TCustomSharePlanner) + TEnergy D_ = 0; // Total work done in system + TEnergy W_ = 0; // Total work wasted in system + TEnergy dD_ = 0; + TEnergy dW_ = 0; + TEnergy pdD_ = 0; + TEnergy pdW_ = 0; + //THolder<TUtilizationMeter> UtilMeter; + THolder<INodeVisitor> Billing; + THolder<IModel> Model; + THolder<INodeVisitor> Puller; + THolder<INodeVisitor> Stepper; + + // Statistics and monitoring + TSharePlannerStats Stats; +public: // Configuration + explicit TSharePlanner(const TSharePlannerConfig& cfg = TSharePlannerConfig()); + virtual ~TSharePlanner() {} + const TSharePlannerConfig& Cfg() const { return Config; } + void Configure(const TSharePlannerConfig& cfg); +public: // Accessors + TString GetName() const { return Config.GetName(); } + TShareGroup* GetRoot() { return Root; } + const TShareGroup* GetRoot() const { return Root; } + TEnergy D() const { return D_; } + TEnergy W() const { return W_; } + TEnergy dD() const { return dD_; } + TEnergy dW() const { return dW_; } + TEnergy pdD() const { return dD_; } + TEnergy pdW() const { return dW_; } + //double Utilization() const { return UtilMeter->Utilization(); } +public: // Evolution + void Done(TShareAccount* acc, TEnergy cost); + void Waste(TEnergy cost); + void Commit(bool model = true); +public: // Monitoring + virtual TString PrintStatus() const; + virtual TString PrintTree(bool band, bool model, const TArtParams& art) const; + virtual TString PrintStats() const; + TString PrintInfo(bool status = true, bool band = true, bool model = true, bool stats = true, const TArtParams& art = TArtParams()) const; + void GetStats(TSharePlannerStats& stats) const; +protected: // Implementation + void Advance(TEnergy cost, bool wasted); + virtual void OnDetach(TShareNode* node); + virtual void OnAttach(TShareNode* node, TShareGroup* parent); +}; + +///////////////////////////////// +/// /// +/// Custom planner /// +/// /// +///////////////////////////////// + +// Helpers +template <class TAcc, class TGrp, class TAccPtr, class TGrpPtr, class Node> struct TNodeTraits {}; // See specialization below +template <class TAcc, class TGrp, class TAccPtr, class TGrpPtr> +struct TNodeTraits<TAcc, TGrp, TAccPtr, TGrpPtr, TAcc> { typedef TAccPtr TPtr; }; +template <class TAcc, class TGrp, class TAccPtr, class TGrpPtr> +struct TNodeTraits<TAcc, TGrp, TAccPtr, TGrpPtr, TGrp> { typedef TGrpPtr TPtr; }; + +template <class TAcc, class TGrp, class TAccPtr = std::shared_ptr<TAcc>, class TGrpPtr = std::shared_ptr<TGrp>> +class TCustomSharePlanner: public TSharePlanner { +public: + typedef THashMap<TString, TAccPtr> TAccs; + typedef THashMap<TString, TGrpPtr> TGrps; + static_assert(std::is_base_of<TShareGroup, TGrp>::value, "TGrp must inherit TShareGroup"); + static_assert(std::is_base_of<TShareAccount, TAcc>::value, "TAcc must inherit TShareAccount"); + template <class TNode> using TTraits = TNodeTraits<TAcc, TGrp, TAccPtr, TGrpPtr, TNode>; +protected: + TAccs Accs; // All accounts storage + TGrps Grps; // All groups storage +protected: // To support shared pointers on base class of TAcc/TGrp (not directly of TAcc/TGrp) + static TAcc* Cast(const TAccPtr& node) { return static_cast<TAcc*>(&*node); } + static TGrp* Cast(const TGrpPtr& node) { return static_cast<TGrp*>(&*node); } + static const TAcc* ConstCast(const TAccPtr& node) { return static_cast<const TAcc*>(&*node); } + static const TGrp* ConstCast(const TGrpPtr& node) { return static_cast<const TGrp*>(&*node); } + static TShareAccount* BaseCast(const TAccPtr& node) { return (TShareAccount*)Cast(node); } + static TShareGroup* BaseCast(const TGrpPtr& node) { return (TShareGroup*)Cast(node); } + static TShareAccount* Base(TAcc* node) { return (TShareAccount*)node; } + static TShareGroup* Base(TGrp* node) { return (TShareGroup*)node; } +public: // Configuration + explicit TCustomSharePlanner(const TSharePlannerConfig& cfg = TSharePlannerConfig()) + : TSharePlanner(cfg) + { + Root = Cast(Add<TGrp>(nullptr, "/", 1, 1, 1)); + } + + TGrp* GetRoot() + { + return static_cast<TGrp*>(Root); + } + + const TGrp* GetRoot() const + { + return static_cast<const TGrp*>(Root); + } + + template <typename TNode, typename... Args> + typename TTraits<TNode>::TPtr Add(TGrp* parent, const Args&... args) + { + typename TTraits<TNode>::TPtr node(new TNode(args...)); + Add<TNode>(node, parent); + return node; + } + + template <class TNode> + void Add(typename TTraits<TNode>::TPtr node, TGrp* parent) + { + PLANNER_PROBE(Add, GetName(), parent? parent->GetName(): "NULL", Cast(node)->w0(), Cast(node)->GetName()); + AddImpl(node); + OnAttach(Cast(node), parent); + } + + template <class TNode> + void Delete(TNode* node) + { + PLANNER_PROBE(Delete, GetName(), node->GetName()); + OnDetach(node); + DeleteImpl(node); + } + + void Clear() + { + Root->Clear(); + Accs.clear(); + // Clear Grps, but save Root + for (auto i = Grps.begin(), e = Grps.end(); i != e;) { + if (Cast(i->second) != Root) { + Grps.erase(i++); + } else { + ++i; + } + } + } + + TAccPtr FindAccountByName(const TString& name) + { + auto i = Accs.find(name); + if (i == Accs.end()) { + return TAccPtr(); + } + return i->second; + } + + TGrpPtr FindGroupByName(const TString& name) + { + auto i = Grps.find(name); + if (i == Grps.end()) { + return TGrpPtr(); + } + return i->second; + } + + size_t AccountsCount() const + { + return Accs.size(); + } + + size_t GroupsCount() const + { + return Grps.size(); + } + + const TAccs& Accounts() const + { + return Accs; + } + + const TGrps& Groups() const + { + return Grps; + } + + void GetSensors(TSharePlannerSensors& sensors, bool total, bool accounts, bool groups) const + { + if (accounts) { + for (typename TAccs::const_iterator i = Accs.begin(), e = Accs.end(); i != e; ++i) { + const TShareAccount& node = *Cast(i->second); + TShareNodeSensors* pb = sensors.AddAccounts(); + pb->SetName(node.GetName()); + pb->SetParentName(node.GetParent()? node.GetParent()->GetName(): ""); + node.GetStats().FillSensorsPb(pb); + FillNodeSensors(pb, node); + } + } + if (groups) { + for (typename TGrps::const_iterator i = Grps.begin(), e = Grps.end(); i != e; ++i) { + const TShareGroup& node = *Cast(i->second); + TShareNodeSensors* pb = sensors.AddGroups(); + pb->SetName(node.GetName()); + pb->SetParentName(node.GetParent()? node.GetParent()->GetName(): ""); + FillNodeSensors(pb, node); + } + } + if (accounts && groups) { + sensors.MutableNodes()->MergeFrom(sensors.GetAccounts()); + sensors.MutableNodes()->MergeFrom(sensors.GetGroups()); + } + if (total) { + TShareNodeSensors* pb = sensors.MutableTotal(); + Root->GetStats().FillSensorsPb(pb); + + size_t idleCount = 0; + size_t activeCount = 0; + size_t idleRetardCount = 0; + size_t activeRetardCount = 0; + size_t ac = 0, de = 0, re = 0, ot = 0; + for (typename TAccs::const_iterator i = Accs.begin(), e = Accs.end(); i != e; ++i) { + const TShareAccount& node = *Cast(i->second); + bool active = node.IsActive(); + bool retard = node.IsRetard(); + idleCount += !active && !retard; + activeCount += active && !retard; + idleRetardCount += !active && retard; + activeRetardCount += active && retard; + ac += node.GetStats().Activations; + de += node.GetStats().Deactivations; + re += node.GetStats().Retardations; + ot += node.GetStats().Overtakes; + } + pb->SetIdle(idleCount); + pb->SetActive(activeCount); + pb->SetIdleRetard(idleRetardCount); + pb->SetActiveRetard(activeRetardCount); + pb->SetActivations(ac); + pb->SetDeactivations(de); + pb->SetRetardations(re); + pb->SetOvertakes(ot); + sensors.SetResUsed(Stats.ResUsed); + sensors.SetResWasted(Stats.ResWasted); + //UtilMeter->GetSensors(sensors); + } + } +private: + void AddImpl(TAccPtr node) + { + bool inserted = Accs.insert(typename TAccs::value_type(Cast(node)->GetName(), node)).second; + Y_ABORT_UNLESS(inserted, "duplicate account name '%s'", Cast(node)->GetName().data()); + } + + void AddImpl(TGrpPtr node) + { + bool inserted = Grps.insert(typename TGrps::value_type(Cast(node)->GetName(), node)).second; + Y_ABORT_UNLESS(inserted, "duplicate group name '%s'", Cast(node)->GetName().data()); + } + + void DeleteImpl(TAcc* node) + { + typename TAccs::iterator i = Accs.find(node->GetName()); + Y_ASSERT(i != Accs.end() && "trying to delete unknown/detached account"); + Accs.erase(i); + } + + void DeleteImpl(TGrp* node) + { + typename TGrps::iterator i = Grps.find(node->GetName()); + Y_ASSERT(i != Grps.end() && "trying to delete unknown/detached group"); + Grps.erase(i); + } + + template <class T> + void FillNodeSensors(TShareNodeSensors* pb, const T& node) const + { + bool active = node.IsActive(); + bool retard = node.IsRetard(); + pb->SetIdle(!active && !retard); + pb->SetActive(active && !retard); + pb->SetIdleRetard(!active && retard); + pb->SetActiveRetard(active && retard); + pb->SetActivations(node.GetStats().Activations); + pb->SetDeactivations(node.GetStats().Deactivations); + pb->SetRetardations(node.GetStats().Retardations); + pb->SetOvertakes(node.GetStats().Overtakes); + TEnergy Eg = node.ComputeEg(); + pb->SetProficit(node.P(Eg)); + pb->SetLag(node.p(Eg)); + pb->SetDefShare(node.CalcGlobalShare()); + pb->SetGrpDefShare(node.s0()); + pb->SetDefWeight(node.w0()); + pb->SetMaxWeight(node.wmax()); + pb->SetDynamicWeight(node.w()); + if (const IContext* ctx = node.Ctx()) { + ctx->FillSensors(*pb); + } + if (const TShareGroup* grp = dynamic_cast<const TShareGroup*>(&node)) { + pb->SetTotalCredit(grp->GetTotalCredit()); + } + } +}; + +} diff --git a/ydb/library/planner/share/stats.h b/ydb/library/planner/share/stats.h new file mode 100644 index 00000000000..cef1878979c --- /dev/null +++ b/ydb/library/planner/share/stats.h @@ -0,0 +1,48 @@ +#pragma once + +#include <ydb/library/planner/base/defs.h> +#include <ydb/library/planner/share/protos/shareplanner_sensors.pb.h> + +namespace NScheduling { + +struct TNodeStats { + ui64 NDone = 0; + ui64 NSpoiled = 0; + TEnergy ResDone = 0; + TEnergy ResSpoiled = 0; + TEnergy MoneySpent = 0; + ui64 Activations = 0; + ui64 Deactivations = 0; + ui64 Retardations = 0; + ui64 Overtakes = 0; + + void Done(TEnergy cost, size_t queries = 1) + { + NDone += queries; + ResDone += cost; + } + + void Spoil(TEnergy cost, size_t queries = 1) + { + NSpoiled += queries; + ResSpoiled += cost; + } + + void Pay(TEnergy bill) + { + MoneySpent += bill; + } + + void FillSensorsPb(TShareNodeSensors* sensors) const + { + sensors->SetNDone(NDone); + sensors->SetNSpoiled(NSpoiled); + sensors->SetResDone(ResDone); + sensors->SetResSpoiled(ResSpoiled); + sensors->SetMoneySpent(MoneySpent); + sensors->SetActivations(Activations); + sensors->SetDeactivations(Deactivations); + } +}; + +} diff --git a/ydb/library/planner/share/step.h b/ydb/library/planner/share/step.h new file mode 100644 index 00000000000..629c0ff7400 --- /dev/null +++ b/ydb/library/planner/share/step.h @@ -0,0 +1,23 @@ +#pragma once + +#include "node_visitor.h" +#include "shareplanner.h" + +namespace NScheduling { + +class TStepper : public INodeVisitor +{ +public: + void Visit(TShareAccount* node) override + { + node->Step(); + } + + void Visit(TShareGroup* node) override + { + node->AcceptInChildren(this); + node->Step(); + } +}; + +} diff --git a/ydb/library/planner/share/ut/all.lwt b/ydb/library/planner/share/ut/all.lwt new file mode 100644 index 00000000000..1f63dd63453 --- /dev/null +++ b/ydb/library/planner/share/ut/all.lwt @@ -0,0 +1,14 @@ +Blocks { + ProbeDesc { Group: "SCHEDULING_SHAREPLANNER_PROVIDER" } + Action { + LogAction { LogTimestamp: true } + PrintToStderrAction {} + } +} +Blocks { + ProbeDesc { Group: "SHAREPLANNER_UT_PROVIDER" } + Action { + LogAction { LogTimestamp: true } + PrintToStderrAction {} + } +} diff --git a/ydb/library/planner/share/ut/shareplanner_ut.cpp b/ydb/library/planner/share/ut/shareplanner_ut.cpp new file mode 100644 index 00000000000..6d918c0bf58 --- /dev/null +++ b/ydb/library/planner/share/ut/shareplanner_ut.cpp @@ -0,0 +1,541 @@ +#include <ydb/library/planner/share/shareplanner.h> +#include <ydb/library/planner/share/history.h> +#include <library/cpp/threading/future/legacy_future.h> + +#include <util/random/random.h> +#include <util/generic/list.h> +#include <util/string/vector.h> +#include <library/cpp/testing/unittest/registar.h> + +#include <library/cpp/lwtrace/all.h> +#define SHAREPLANNER_UT_PROVIDER(PROBE, EVENT, GROUPS, TYPES, NAMES) \ + PROBE(TracePrint, GROUPS(), \ + TYPES(TString), \ + NAMES("message")) \ + /**/ + +LWTRACE_DECLARE_PROVIDER(SHAREPLANNER_UT_PROVIDER) +LWTRACE_DEFINE_PROVIDER(SHAREPLANNER_UT_PROVIDER) +LWTRACE_USING(SHAREPLANNER_UT_PROVIDER) + +Y_UNIT_TEST_SUITE(SchedulingSharePlanner) { + using namespace NScheduling; + + class TMyAccount; + class TMyGroup; + class TMyPlanner; + + //////////////////////////// + /// /// + /// Planner for test /// + /// /// + //////////////////////////// + + class TMyAccount: public TShareAccount { + private: + double DemandShare = 1.0; // Max share that account can consume (1.0 - is whole cluster) + public: + TMyAccount(const TString& name, FWeight w, FWeight wmax, TEnergy v = 1) + : TShareAccount(name, w, wmax, v) + {} + TMyPlanner* GetPlanner(); + TMyGroup* GetParent(); + const TMyGroup* GetParent() const; + void Distribute(TEnergy cost); + double GatherDemands(); + double P(); + double C(); + double GetDemandShare() const { return DemandShare; } + void SetDemandShare(double value) { DemandShare = value; } + }; + + class TMyGroup: public TShareGroup { + private: + double DemandShare = 1.0; // Max share that account can consume (1.0 - is whole cluster) + public: + TMyGroup(const TString& name, FWeight w, FWeight wmax, TEnergy v = 1) + : TShareGroup(name, w, wmax, v) + {} + TMyPlanner* GetPlanner(); + TMyGroup* GetParent(); + const TMyGroup* GetParent() const; + void Distribute(TEnergy cost); + double GatherDemands(); + double GetDemandShare() const { return DemandShare; } + void SetDemandShare(double value) { DemandShare = value; } + TEnergy Esum() const + { + TEnergy result = 0; + CUSTOMSHAREPLANNER_FOR(TMyAccount, TMyGroup, node, + result += node->E(); + ); + return result; + } + }; + + class TMyPlanner: public TCustomSharePlanner<TMyAccount, TMyGroup> { + public: + double GetRepaymentRate() const; + double GetMaxRepaymentSpeed() const; + void CheckRepayment(double debt1, double debt2, double dx, double maxErr, const TString& desc = TString()) const; + double GetX() const; + friend class TMyGroup; + friend class TMyAccount; + }; + + /////////////////////////////// + /// /// + /// Accessors /// + /// /// + /////////////////////////////// + + TMyPlanner* TMyAccount::GetPlanner() { return static_cast<TMyPlanner*>(Planner); } + TMyGroup* TMyAccount::GetParent() { return static_cast<TMyGroup*>(Parent); } + const TMyGroup* TMyAccount::GetParent() const { return static_cast<const TMyGroup*>(Parent); } + + TMyPlanner* TMyGroup::GetPlanner() { return static_cast<TMyPlanner*>(Planner); } + TMyGroup* TMyGroup::GetParent() { return static_cast<TMyGroup*>(Parent); } + const TMyGroup* TMyGroup::GetParent() const { return static_cast<const TMyGroup*>(Parent); } + + //////////////////////////////// + /// /// + /// Main class for tests /// + /// /// + //////////////////////////////// + + struct TTester { + TMyPlanner Planner; + void Evolve(TEnergy cost, size_t steps); + }; + + /////////////////////////////// + /// /// + /// Scheduler emulation /// + /// /// + /////////////////////////////// + + void TMyAccount::Distribute(TEnergy cost) + { + Planner->Done(this, cost); + } + + struct TDistrItem { + TMyAccount* Account = nullptr; + TMyGroup* Group = nullptr; + double Nordem; // Demands normalized by weight + + TDistrItem(TMyAccount* node) + : Account(node) + , Nordem(node->GetDemandShare() / node->Ctx()->GetWeight()) + {} + + TDistrItem(TMyGroup* node) + : Group(node) + , Nordem(node->GetDemandShare() / node->Ctx()->GetWeight()) + {} + + bool operator<(const TDistrItem& o) const + { + return Nordem < o.Nordem; + } + }; + +#define CUSTOMSHAREPLANNER_FOR_DST(items, node, expr) \ + do { \ + for (auto& item : (items)) { \ + if (auto* node = item.Account) { expr; } \ + else if (auto* node = item.Group) { expr; } \ + } \ + } while (false) \ + /**/ + + void TMyGroup::Distribute(TEnergy cost) + { + TEnergy slot = cost; + FWeight wsum = 0; + TVector<TDistrItem> Items; + CUSTOMSHAREPLANNER_FOR(TMyAccount, TMyGroup, node, + Items.push_back(TDistrItem(node)); + wsum += node->Ctx()->GetWeight()); + + Sort(Items); + CUSTOMSHAREPLANNER_FOR_DST(Items, node, + node->Distribute(WMaxCut(node->Ctx()->GetWeight(), TEnergy(node->GetDemandShare() * slot), wsum, cost))); + GetPlanner()->Waste(cost); + } + + double TMyAccount::GatherDemands() + { + return DemandShare; + } + + double TMyGroup::GatherDemands() + { + DemandShare = 0.0; + CUSTOMSHAREPLANNER_FOR(TMyAccount, TMyGroup, node, + DemandShare += node->GatherDemands()); + return DemandShare; + } + + void TTester::Evolve(TEnergy cost, size_t steps = 1) + { + Planner.GetRoot()->GatherDemands(); + for (; steps > 0; steps--) { + TEnergy c = cost / steps; + cost -= c; + Planner.GetRoot()->Distribute(c); + Planner.Commit(); + } + } + + /////////////////////////////// + /// /// + /// Measurements /// + /// /// + /////////////////////////////// + + double TMyAccount::P() + { + return E() - double(GetParent()->Esum()) / gs * s0(); + } + + double TMyAccount::C() + { + return fabs(P()); + } + + double TMyPlanner::GetRepaymentRate() const + { + return Config.GetDenseLength() > 0.0 ? 1.0 / Config.GetDenseLength() : 0.0; + } + + double TMyPlanner::GetMaxRepaymentSpeed() const + { + double gR = 128*81*125*7*11*13; // WTF? + return gR / 2; + } + + void TMyPlanner::CheckRepayment(double debt1, double debt2, double dx, double maxErr, const TString& desc) const + { + UNIT_ASSERT(debt1 >= 0 && debt2 >= 0); + double minDebt = 10000; + double debt2_hi_threshold = debt1 * exp(-(GetRepaymentRate()/maxErr) * dx); + double debt2_lo_threshold = debt1 * exp(-(GetRepaymentRate()*maxErr) * dx); + double debt2_target = debt1 * exp(-GetRepaymentRate() * dx); + Y_UNUSED(debt2_target); + bool tooLoDebts = debt2 < minDebt && debt2 < minDebt; + double repaySpeed = debt1 * GetRepaymentRate(); + bool tooHiSpeed = repaySpeed > 0.3 * GetMaxRepaymentSpeed(); + auto tracegen = [=]() { + return Sprintf("CheckRepayment: %s DenseLength=%g speed=%g maxspeed=%g debt1=%g debt2=%g dx=%g -%s%s-> %g < %g < %g", + desc.data(), Config.GetDenseLength(), repaySpeed, GetMaxRepaymentSpeed(), + debt1, debt2, dx, tooLoDebts? "L": "-", tooHiSpeed? "H": "-", + debt2_lo_threshold / debt2_target, debt2 / debt2_target, + debt2_hi_threshold / debt2_target); + }; + if (!tooLoDebts && !tooHiSpeed) { + UNIT_ASSERT_C(debt2 < debt2_hi_threshold && debt2_lo_threshold < debt2, + tracegen()); + } + LWPROBE(TracePrint, tracegen()); + } + + double TMyPlanner::GetX() const + { + return GetRoot()->Esum()/gs; + } + + //////////////////////////// + /// /// + /// Unit tests /// + /// /// + //////////////////////////// + + Y_UNIT_TEST(StartLwtrace) { + NLWTrace::StartLwtraceFromEnv(); + } + + Y_UNIT_TEST(Smoke) { + TTester t; + + TMyAccount* A = t.Planner.Add<TMyAccount>(t.Planner.GetRoot(), "A", 1000, 2000).get(); + TMyAccount* B = t.Planner.Add<TMyAccount>(t.Planner.GetRoot(), "B", 1000, 2000).get(); + float N = 100; + TEnergy c = 1.0 / N; + for (int i = 0; i<50; i++) { + t.Planner.Done(A, c); + } + t.Planner.Done(B, 50*c); + t.Planner.Commit(); + } + + Y_UNIT_TEST(Pull) { + TTester t; + + TMyAccount* A = t.Planner.Add<TMyAccount>(t.Planner.GetRoot(), "A", 2000, 100000, 3).get(); + TMyAccount* B = t.Planner.Add<TMyAccount>(t.Planner.GetRoot(), "B", 1000, 100000, 2).get(); + TMyAccount* C = t.Planner.Add<TMyAccount>(t.Planner.GetRoot(), "C", 1000, 100000, 1).get(); + float N = 100; + TEnergy c = 6.0 / N; + t.Planner.Done(A, 10*c); + t.Planner.Done(B, 20*c); + t.Planner.Done(C, 30*c); + t.Planner.Commit(); + for (int i = 0; i<10; i++) { + t.Planner.Done(C, 10*c); + t.Planner.Commit(); + } + for (int i = 0; i<20; i++) { + t.Planner.Done(B, 10*c); + t.Planner.Commit(); + } + for (int i = 0; i<50; i++) { + t.Planner.Done(A, 10*c); + t.Planner.Commit(); + } + } + + Y_UNIT_TEST(RelativeHistorySmoke) { + TStringStream ss; + TTester t; + + TMyAccount* A = t.Planner.Add<TMyAccount>(t.Planner.GetRoot(), "A", 1000, 100000).get(); + TMyAccount* B = t.Planner.Add<TMyAccount>(t.Planner.GetRoot(), "B", 2000, 200000).get(); + TMyAccount* C = t.Planner.Add<TMyAccount>(t.Planner.GetRoot(), "C", 3000, 300000).get(); + + // Create some history + float N = 100; + TEnergy c = 3.0 / N; + t.Planner.Done(A, 10*c); + t.Planner.Done(B, 20*c); + t.Planner.Done(C, 30*c); + t.Planner.Commit(); + THistorySaver::Save(&t.Planner, ss); + + // Change state + t.Planner.Done(A, 30*c); + t.Planner.Done(B, 20*c); + t.Planner.Done(C, 10*c); + t.Planner.Commit(); + + // Load and check history + bool ok = THistoryLoader::Load(&t.Planner, ss); + UNIT_ASSERT(ok); + t.Planner.Commit(); + UNIT_ASSERT(fabs(A->e() - B->e()) < 1e-5); + UNIT_ASSERT(fabs(B->e() - C->e()) < 1e-5); + + // Create another history + t.Planner.Done(A, 25*c); + t.Planner.Done(B, 36*c); + t.Planner.Done(C, 49*c); + t.Planner.Commit(); + ss.Clear(); + THistorySaver::Save(&t.Planner, ss); + double eAB = A->e() - B->e(); + double eBC = B->e() - C->e(); + + // Change state + t.Planner.Done(A, 44*c); + t.Planner.Done(B, 33*c); + t.Planner.Done(C, 22*c); + t.Planner.Commit(); + + // Load and check history + ok = THistoryLoader::Load(&t.Planner, ss); + UNIT_ASSERT(ok); + t.Planner.Commit(); + UNIT_ASSERT(fabs(eAB - (A->e() - B->e())) < 1e-5); + UNIT_ASSERT(fabs(eBC - (B->e() - C->e())) < 1e-5); + } + + Y_UNIT_TEST(RelativeHistoryWithDelete) { + TStringStream ss; + TTester t; + + TMyAccount* A = t.Planner.Add<TMyAccount>(t.Planner.GetRoot(), "A", 1000, 100000).get(); + TMyAccount* B = t.Planner.Add<TMyAccount>(t.Planner.GetRoot(), "B", 2000, 200000).get(); + TMyAccount* C = t.Planner.Add<TMyAccount>(t.Planner.GetRoot(), "C", 3000, 300000).get(); + + // Create some history + float N = 100; + TEnergy c = 3.0 / N; + t.Planner.Done(A, 10*c); + t.Planner.Done(B, 20*c); + t.Planner.Done(C, 30*c); + t.Planner.Commit(); + THistorySaver::Save(&t.Planner, ss); + + // Change state + t.Planner.Done(A, 30*c); + t.Planner.Done(B, 20*c); + t.Planner.Commit(); + + // Delete one account + t.Planner.Delete(C); + t.Planner.Commit(); + + // Load and check history + bool ok = THistoryLoader::Load(&t.Planner, ss); + UNIT_ASSERT(ok); + t.Planner.Commit(); + UNIT_ASSERT(fabs(A->e() - B->e()) < 1e-5); + } + + double denseLengths[] = {1.0, 0.2, 0.4, 0.5, 0.6, 0.9, 0.99, 1.01, 1.1, 1.5, 2.0, 4.0, 8.0, 10.0, 0.0}; + int checks[] = {1, 5, 10, 20, 40, 50, 80}; + int steps = 1000; + + Y_UNIT_TEST(RepaymentConvergence) { + for (const auto& denseLength : denseLengths) { + TTester t; + + TMyAccount* A = t.Planner.Add<TMyAccount>(t.Planner.GetRoot(), "A", 1000, 100000).get(); + TMyAccount* B = t.Planner.Add<TMyAccount>(t.Planner.GetRoot(), "B", 1000, 100000).get(); + + TSharePlannerConfig cfg; + cfg.SetDenseLength(denseLength); + t.Planner.Configure(cfg); + LWPROBE(TracePrint, Sprintf("SetDenseLength: %g", denseLength)); + + // Create some history + float N = 100; + TEnergy c = 2.0 / N; + t.Planner.Done(A, 40*c); + t.Planner.Done(B, 10*c); + t.Planner.Commit(); + + // Emulate dynamics + TVector<std::pair<double, double>> state; // <debt, x> pairs + for (int i = 0; i < steps; ++i) { + double debt = (A->C() + B->C()) / 2; + double x = t.Planner.GetX(); + state.push_back(std::make_pair(debt, x)); + LWPROBE(TracePrint, Sprintf("i=%d x=%g debt=%g", i, x, debt)); + t.Evolve(c); + } + + // Check convergence + for (const auto& check : checks) { + for (int i = check; i < steps; ++i) { + const auto& s1 = state[i - check]; + const auto& s2 = state[i]; + t.Planner.CheckRepayment(s1.first, s2.first, s2.second - s1.second, 2.0, Sprintf("i1=%d i2=%d", i-check, i)); + } + } + } + } + + Y_UNIT_TEST(RepaymentConvergence4Users) { + for (const auto& denseLength : denseLengths) { + TTester t; + + TMyAccount* A = t.Planner.Add<TMyAccount>(t.Planner.GetRoot(), "A", 1000, 100000).get(); + TMyAccount* B = t.Planner.Add<TMyAccount>(t.Planner.GetRoot(), "B", 2000, 200000).get(); + TMyAccount* C = t.Planner.Add<TMyAccount>(t.Planner.GetRoot(), "C", 3000, 300000).get(); + TMyAccount* D = t.Planner.Add<TMyAccount>(t.Planner.GetRoot(), "D", 4000, 400000).get(); + + TSharePlannerConfig cfg; + cfg.SetDenseLength(denseLength); + t.Planner.Configure(cfg); + LWPROBE(TracePrint, Sprintf("SetDenseLength: %g", denseLength)); + + // Create some history + float N = 100; + TEnergy c = 4.0 / N; + t.Planner.Done(A, 20*c); + t.Planner.Done(B, 10*c); + t.Planner.Done(C, 40*c); + t.Planner.Done(D, 30*c); + t.Planner.Commit(); + + // Emulate dynamics + TVector<std::pair<double, double>> state; // <debt, x> pairs + for (int i = 0; i < steps; ++i) { + double debt = (A->C() + B->C() + C->C() + D->C()) / 2; + double x = t.Planner.GetX(); + state.push_back(std::make_pair(debt, x)); + LWPROBE(TracePrint, Sprintf("i=%d x=%g debt=%g", i, x, debt)); + t.Evolve(c); + } + + // Check convergence + for (const auto& check : checks) { + for (int i = check; i < steps; ++i) { + const auto& s1 = state[i - check]; + const auto& s2 = state[i]; + t.Planner.CheckRepayment(s1.first, s2.first, s2.second - s1.second, 2.0, Sprintf("i1=%d i2=%d", i-check, i)); + } + } + + /* + TMyAccount* accounts[] = {A, B, C, D}; + // Emulate dynamics + typedef THashMap<TMyAccount*, double> TAccs; + TAccs prev; + double prevx; + for (int i = 0; i < steps; ++i) { + // Check + double x = t.Planner.GetX(); + if (!prev.empty()) { + for (auto acc : accounts) { + //t.Planner.CheckRepayment(prev[acc], acc->P(), x - prevx, 2.0e10, Sprintf("i=%d", i)); + Y_UNUSED(acc); Y_UNUSED(prevx); + } + } + + // Save prev state + for (auto acc : accounts) { + prev[acc] = acc->P(); + } + prevx = x; + + // Emulate + t.Evolve(c); + } + */ + } + } + + Y_UNIT_TEST(OuterGroupInsensitivity) { + for (const auto& denseLength : denseLengths) { + if (denseLength == 0.0) + continue; // This test should not work for discontinuous h-function + TTester t; + + TMyAccount* A = t.Planner.Add<TMyAccount>(t.Planner.GetRoot(), "A", 1000, 2000).get(); + TMyGroup* G = t.Planner.Add<TMyGroup>(t.Planner.GetRoot(), "G", 1000, 2000).get(); + TMyAccount* B = t.Planner.Add<TMyAccount>(G, "B", 1000, 100000).get(); + TMyAccount* C = t.Planner.Add<TMyAccount>(G, "C", 1000, 100000).get(); + + B->SetDemandShare(0.0); + + TSharePlannerConfig cfg; + cfg.SetDenseLength(denseLength); + t.Planner.Configure(cfg); + LWPROBE(TracePrint, Sprintf("SetDenseLength: %g", denseLength)); + + // Create some history + float N = 100; + TEnergy c = 3.0 / N; + t.Planner.Done(A, 50*c); + t.Planner.Done(B, 25*c); + t.Planner.Done(C, 25*c); + t.Planner.Commit(); + + // Emulate dynamics + int idleSteps = 100; + for (int i = 0; i < steps; ++i) { + UNIT_ASSERT(fabs(A->w() - 1000.0) < 50.0); + UNIT_ASSERT(fabs(G->w() - 1000.0) < 50.0); + LWPROBE(TracePrint, Sprintf("wA=%g wG=%g", A->w(), G->w())); + t.Evolve(c); + if (B->pdS() > 0) { + if (--idleSteps == 0) { + B->SetDemandShare(1.0); + } + } + } + } + } +} diff --git a/ydb/library/planner/share/ut/ya.make b/ydb/library/planner/share/ut/ya.make new file mode 100644 index 00000000000..d3fca68ec02 --- /dev/null +++ b/ydb/library/planner/share/ut/ya.make @@ -0,0 +1,12 @@ +UNITTEST() + +PEERDIR( + library/cpp/threading/future + ydb/library/planner/share +) + +SRCS( + shareplanner_ut.cpp +) + +END() diff --git a/ydb/library/planner/share/utilization.cpp b/ydb/library/planner/share/utilization.cpp new file mode 100644 index 00000000000..9762a5ab65a --- /dev/null +++ b/ydb/library/planner/share/utilization.cpp @@ -0,0 +1,71 @@ +#include "utilization.h" + +namespace NScheduling { + +TUtilizationMeter::TUtilizationMeter() +{ + for (unsigned i = 0; i < Slots; i++) { + DoneInSlot[i] = 0; + } + DoneInWindow = 0; + SlotVolume = 0; + CurrentSlot = 0; +} + +void TUtilizationMeter::AddToSlot(TEnergy cost, bool wasted) +{ + SlotVolume += cost; + Y_ASSERT(SlotVolume <= SlotCapacity && "slot overflow"); + if (!wasted) { + DoneInSlot[CurrentSlot] += cost; + DoneInWindow += cost; + } +} + +void TUtilizationMeter::NextSlot() +{ + Y_ASSERT(SlotVolume == SlotCapacity && "previous slot must be filled before moving to the next slot"); + CurrentSlot = (CurrentSlot + 1)%Slots; + DoneInWindow -= DoneInSlot[CurrentSlot]; + DoneInSlot[CurrentSlot] = 0; + SlotVolume = 0; +} + +void TUtilizationMeter::Add(TEnergy cost, bool wasted) +{ + while (SlotVolume + cost > SlotCapacity) { + TEnergy added = SlotCapacity - SlotVolume; + AddToSlot(added, wasted); + cost -= added; + NextSlot(); + } + AddToSlot(cost, wasted); +} + +void TUtilizationMeter::Save(TUtilizationHistory* history) const +{ + for (unsigned i = 0; i < Slots; i++) { + history->AddDoneInSlot(DoneInSlot[i]); + } + history->SetSlotVolume(SlotVolume); + history->SetCurrentSlot(CurrentSlot); +} + +void TUtilizationMeter::Load(const TUtilizationHistory& history) +{ + DoneInWindow = 0; + for (unsigned i = 0; i < Slots; i++) { + DoneInWindow += (DoneInSlot[i] = history.GetDoneInSlot(i)); + } + SlotVolume = history.GetSlotVolume(); + CurrentSlot = history.GetCurrentSlot(); +} + +void TUtilizationMeter::GetSensors(TSharePlannerSensors& sensors) const +{ + sensors.SetResUsedInWindow(double(Used())*1000/gV); + sensors.SetResWastedInWindow(double(Wasted())*1000/gV); + sensors.SetUtilization(Utilization()); +} + +} diff --git a/ydb/library/planner/share/utilization.h b/ydb/library/planner/share/utilization.h new file mode 100644 index 00000000000..3326a5e34a7 --- /dev/null +++ b/ydb/library/planner/share/utilization.h @@ -0,0 +1,33 @@ +#pragma once + +#include <ydb/library/planner/base/defs.h> +#include <ydb/library/planner/share/protos/shareplanner.pb.h> +#include <ydb/library/planner/share/protos/shareplanner_sensors.pb.h> + +namespace NScheduling { + +class TUtilizationMeter { +private: + static const unsigned Slots = 2*gX; // Number of slots in averaging window + static const TEnergy SlotCapacity = gV/Slots; // Size of a slot in res*sec + TEnergy DoneInSlot[Slots]; + TEnergy DoneInWindow; // Sum of values DoneInSlot[i] for all i + TEnergy SlotVolume; // Done in slot plus wasted in slot + unsigned CurrentSlot; +public: + TUtilizationMeter(); + void Add(TEnergy cost, bool wasted); + void Save(TUtilizationHistory* history) const; + void Load(const TUtilizationHistory& history); +public: // Accessors + inline double Utilization() const { return double(DoneInWindow)/(gV-SlotCapacity+SlotVolume); } + inline TEnergy Used() const { return DoneInWindow; } + inline TEnergy Wasted() const { return (gV-SlotCapacity+SlotVolume) - DoneInWindow; } +public: // Monitoring and sensors + void GetSensors(TSharePlannerSensors& sensors) const; +private: + void AddToSlot(TEnergy cost, bool wasted); + void NextSlot(); +}; + +} diff --git a/ydb/library/planner/share/ya.make b/ydb/library/planner/share/ya.make new file mode 100644 index 00000000000..ea6a5b3c9c6 --- /dev/null +++ b/ydb/library/planner/share/ya.make @@ -0,0 +1,25 @@ +LIBRARY() + +PEERDIR( + ydb/library/planner/share/protos + ydb/library/analytics + library/cpp/lwtrace + library/cpp/monlib/encode/legacy_protobuf/protos +) + +SRCS( + account.cpp + group.cpp + history.cpp + node.cpp + probes.cpp + shareplanner.cpp + # utilization.cpp +) + +END() + +RECURSE( + protos + ut +) diff --git a/ydb/library/planner/ya.make b/ydb/library/planner/ya.make new file mode 100644 index 00000000000..5d9f9198651 --- /dev/null +++ b/ydb/library/planner/ya.make @@ -0,0 +1,3 @@ +RECURSE( + share +) diff --git a/ydb/library/shop/counters.h b/ydb/library/shop/counters.h new file mode 100644 index 00000000000..07e26234eee --- /dev/null +++ b/ydb/library/shop/counters.h @@ -0,0 +1,107 @@ +#pragma once + +#include "resource.h" + +#include <library/cpp/deprecated/atomic/atomic.h> +#include <util/generic/vector.h> + +namespace NShop { + +/////////////////////////////////////////////////////////////////////////////// + +struct TConsumerCounters { + TAtomic Consumed = 0; // in TCost units (deriv) + TAtomic Borrowed = 0; // in TCost units (deriv) + TAtomic Donated = 0; // in TCost units (deriv) + TAtomic Usage = 0; // in promiles (as_is) +}; + +/////////////////////////////////////////////////////////////////////////////// + +template <class TCons, class TTag> +class TCountersAggregator { +private: + struct TData { + TConsumerCounters* Counters; + TWeight Weight; + TTag Consumed; + + explicit TData(TCons* consumer) + : Counters(consumer->GetCounters()) + , Weight(consumer->GetWeight()) + , Consumed(consumer->ResetConsumed()) + {} + }; + + TVector<TData> Consumers; + TTag ConsumedRest = TTag(); + TTag UsedTotal = TTag(); + TWeight WeightTotal = 0; + +public: + void Add(TCons* consumer) + { + Consumers.emplace_back(consumer); + TData& d = Consumers.back(); + ConsumedRest += d.Consumed; + UsedTotal += d.Consumed * d.Weight; + WeightTotal += d.Weight; + } + + void Apply() + { + if (WeightTotal == 0) { + return; + } + + Sort(Consumers, [] (const TData& lhs, const TData& rhs) { + return lhs.Consumed < rhs.Consumed; + }); + + TTag usagePrev = TTag(); + TTag consumedPrev = TTag(); + size_t consumersRest = Consumers.size(); + TTag fairShare = UsedTotal / TTag(WeightTotal); + for (TData& d : Consumers) { + TTag borrowed = (d.Consumed - fairShare) * d.Weight; + AtomicAdd(d.Counters->Consumed, TAtomicBase(d.Consumed * d.Weight)); + if (borrowed > 0) { + AtomicAdd(d.Counters->Borrowed, TAtomicBase(borrowed)); + } else { + AtomicAdd(d.Counters->Donated, TAtomicBase(-borrowed)); + } + + // EXPLANATION FOR USAGE FORMULA + // If we assume: + // (1) all "vectors" below are sorted by consumption c[k] + // (2) u[k] >= u[k-1] -- u should monotonically increase with k + // (3) u[k] must be bounded by 1 + // (4) the last u[n] must be equal to 1 + // (5) u[k] must be a continuous function of every c[k] + // + // One can proove that the following formula satisfies these requirements: + // c[k] - c[k-1] + // u[k] = u[k-1] + ----------------- (1 - u[k-1]) + // L[k] - c[k-1] + // + // , where L[k] = sum(i=k..n, c[i]) / (n - k + 1) + // L[k] is max possible value for c[k] (due to sort by c[k]) + + if (ConsumedRest > 1e-6) { + TTag avgConsumed = ConsumedRest / consumersRest; + if (avgConsumed > d.Consumed) { + usagePrev += (d.Consumed - consumedPrev) / (avgConsumed - d.Consumed) * (1.0 - usagePrev); + } + } + if (usagePrev > 1.0) { + usagePrev = 1.0; // just to account for fp-errors + } + consumedPrev = d.Consumed; + ConsumedRest -= d.Consumed; + consumersRest--; + AtomicSet(d.Counters->Usage, TAtomicBase(usagePrev * 1000.0)); + } + } +}; + +} diff --git a/ydb/library/shop/estimator.h b/ydb/library/shop/estimator.h new file mode 100644 index 00000000000..3490e2693de --- /dev/null +++ b/ydb/library/shop/estimator.h @@ -0,0 +1,192 @@ +#pragma once + +#include "probes.h" +#include <util/string/builder.h> + +namespace NShop { + +template <int ReactionNom = 1, int ReactionDenom = 100> +class TMovingAverageEstimator { + static constexpr double NewFactor = double(ReactionNom) / double(ReactionDenom); + static constexpr double OldFactor = 1 - NewFactor; +protected: + double GoalMean; +public: + explicit TMovingAverageEstimator(double average) + : GoalMean(average) + {} + + double GetAverage() const + { + return GoalMean; + } + + void Update(double goal) + { + GoalMean = OldFactor * GoalMean + NewFactor * goal; + } + + TString ToString() const + { + return TStringBuilder() << "{ GoalMean:" << GoalMean << "}"; + } +}; + +template <int ReactionNom = 1, int ReactionDenom = 100> +class TMovingSlrEstimator : public TMovingAverageEstimator<ReactionNom, ReactionDenom> { +protected: + static constexpr double NewFactor = double(ReactionNom) / double(ReactionDenom); + static constexpr double OldFactor = 1 - NewFactor; + double FeatureMean; + double FeatureSqMean; + double GoalFeatureMean; +public: + TMovingSlrEstimator(double goal1, double feature1, double goal2, double feature2) + : TMovingAverageEstimator<ReactionNom, ReactionDenom>(0.5 * (goal1 + goal2)) + , FeatureMean(0.5 * (feature1 + feature2)) + , FeatureSqMean(0.5 * (feature1 * feature1 + feature2 * feature2)) + , GoalFeatureMean(0.5 * (goal1 * feature1 + goal2 * feature2)) + {} + + double GetEstimation(double feature) const + { + return this->GoalMean + GetSlope() * (feature - FeatureMean); + } + + double GetEstimationAndSlope(double feature, double& slope) const + { + slope = GetSlope(); + return this->GoalMean + slope * (feature - FeatureMean); + } + + double GetSlope() const + { + double disp = FeatureSqMean - FeatureMean * FeatureMean; + if (disp > 1e-10) { + return (GoalFeatureMean - this->GoalMean * FeatureMean) / disp; + } else { + return this->GetAverage(); + } + } + + using TMovingAverageEstimator<ReactionNom, ReactionDenom>::Update; + + void Update(double goal, double feature) + { + Update(goal); + FeatureMean = OldFactor * FeatureMean + NewFactor * feature; + FeatureSqMean = OldFactor * FeatureSqMean + NewFactor * feature * feature; + GoalFeatureMean = OldFactor * GoalFeatureMean + NewFactor * goal * feature; + } + + TString ToString() const + { + return TStringBuilder() + << "{ GoalMean:" << this->GoalMean + << ", FeatureMean:" << FeatureMean + << ", FeatureSqMean:" << FeatureSqMean + << ", GoalFeatureMean:" << GoalFeatureMean + << " }"; + } +}; + +struct TAnomalyFilter { + // New value is classified as anomaly if it is `MaxRelativeError' times larger or smaller + float MinRelativeError; + float MaxRelativeError; + + // Token bucket for anomaly filter (1 token = 1 anomaly allowed) + float MaxAnomaliesBurst; // Total bucket capacity + float MaxAnomaliesRate; // Bucket fill rate (number of tokens generated per one Update() calls) + + void Disable() + { + MaxAnomaliesBurst = 0.0f; + } + + TAnomalyFilter(float minRelativeError, float maxRelativeError, float maxAnomaliesBurst, float maxAnomaliesRate) + : MinRelativeError(minRelativeError) + , MaxRelativeError(maxRelativeError) + , MaxAnomaliesBurst(maxAnomaliesBurst) + , MaxAnomaliesRate(maxAnomaliesRate) + {} +}; + +template <int ReactionNom = 1, int ReactionDenom = 100> +class TFilteredMovingSlrEstimator { +protected: + TMovingSlrEstimator<ReactionNom, ReactionDenom> Estimator; + float AnomaliesAllowed; // Current token count +public: + TFilteredMovingSlrEstimator(double goal1, double feature1, double goal2, double feature2, ui64 anomaliesAllowed = 0) + : Estimator(goal1, feature1, goal2, feature2) + , AnomaliesAllowed(anomaliesAllowed) + {} + + double GetAverage() const + { + return Estimator.GetAverage(); + } + + double GetEstimation(double feature) const + { + return Estimator.GetEstimation(feature); + } + + double GetEstimationAndSlope(double feature, double& slope) const + { + return Estimator.GetEstimationAndSlope(feature, slope); + } + + double GetSlope() const + { + return Estimator.GetSlope(); + } + + void Update(double goal, double estimation, const TAnomalyFilter& filter) + { + if (Filter(goal, estimation, filter)) { + Estimator.Update(goal); + } + } + + void Update(double goal, double estimation, double feature, const TAnomalyFilter& filter) + { + if (Filter(goal, estimation, filter)) { + Estimator.Update(goal, feature); + } + } + + TString ToString() const + { + return TStringBuilder() + << "{ Estimator:" << Estimator.ToString() + << ", AnomaliesAllowed:" << AnomaliesAllowed + << " }"; + } +private: + bool Filter(double goal, double estimation, const TAnomalyFilter& filter) + { + // Shortcut for turned off mode + if (filter.MaxAnomaliesBurst <= 0.0f || estimation < 1e-3) { + return true; + } + + // Fill bucket + AnomaliesAllowed += filter.MaxAnomaliesRate; + if (AnomaliesAllowed > filter.MaxAnomaliesBurst) { + AnomaliesAllowed = filter.MaxAnomaliesBurst; + } + + // Apply filter + float error = goal / estimation; + if ((error < filter.MinRelativeError || filter.MaxRelativeError < error) && AnomaliesAllowed >= 1.0f) { + AnomaliesAllowed--; // Use 1 token on anomaly that we ignore + return false; + } else { + return true; + } + } +}; + +} diff --git a/ydb/library/shop/flowctl.cpp b/ydb/library/shop/flowctl.cpp new file mode 100644 index 00000000000..e015b9952fb --- /dev/null +++ b/ydb/library/shop/flowctl.cpp @@ -0,0 +1,680 @@ +#include "flowctl.h" +#include "probes.h" + +#include <util/generic/utility.h> +#include <util/system/yassert.h> + +#include <cmath> + +namespace NShop { + +LWTRACE_USING(SHOP_PROVIDER); + +TFlowCtlCounters TFlowCtl::FakeCounters; // instantiate static object + +TFlowCtl::TFlowCtl(ui64 initialWindow) + : Window(initialWindow) + , State(Window) + , Counters(&FakeCounters) +{ + UpdateThreshold(); +} + +TFlowCtl::TFlowCtl(const TFlowCtlConfig& cfg, double now, ui64 initialWindow) + : TFlowCtl(initialWindow) +{ + Configure(cfg, now); +} + +void TFlowCtl::Validate(const TFlowCtlConfig& cfg) +{ +#define TLFC_VERIFY(cond, ...) \ + do { \ + if (!(cond)) ythrow yexception() << Sprintf(__VA_ARGS__); \ + } while (false) \ + /**/ + + TLFC_VERIFY(cfg.GetPeriodDuration() > 0, + "PeriodDuration must be positive, got %lf", + (double)cfg.GetPeriodDuration()); + TLFC_VERIFY(cfg.GetMeasurementError() > 0 && cfg.GetMeasurementError() < 1, + "MeasurementError must be in (0, 1) range, got %lf", + (double)cfg.GetMeasurementError()); + TLFC_VERIFY(cfg.GetSteadyLimit() > 0 && cfg.GetSteadyLimit() < 1, + "SteadyLimit must be in (0, 1) range, got %lf", + (double)cfg.GetSteadyLimit()); + TLFC_VERIFY(cfg.GetHistorySize() > 1, + "HistorySize must be at least 2, got %ld", + (long)cfg.GetHistorySize()); + TLFC_VERIFY(cfg.GetMinCountInFly() > 0, + "MinCountInFly must be at least 1, got %ld", + (long)cfg.GetMinCountInFly()); + TLFC_VERIFY(cfg.GetMinCostInFly() > 0, + "MinCostInFly must be at least 1, got %ld", + (long)cfg.GetMinCostInFly()); + TLFC_VERIFY(cfg.GetMultiplierLimit() > 0 && cfg.GetMultiplierLimit() < 1, + "MultiplierLimit must be in (0, 1) range, got %lf", + (double)cfg.GetMultiplierLimit()); + +#undef TLFC_VERIFY +} + +void TFlowCtl::Configure(const TFlowCtlConfig& cfg, double now) +{ + if (cfg.GetFixedWindow()) { + Window = cfg.GetFixedWindow(); + } + Window = Max(Window, cfg.GetMinCostInFly()); + State = Max(State, (double)cfg.GetMinCostInFly()); + UpdateThreshold(); + if (Period.empty()) { // if brand new TFlowCtl + Cfg = cfg; + Period.resize(Cfg.GetHistorySize()); + ConfigureTime = now; + SlowStart = Cfg.GetSlowStartLimit(); + AdvanceTime(now, false); + } else { // if reconfiguration + AdvanceTime(now, false); + + // Pretend that current period was started with new config + Cfg = cfg; + TPeriod* cp = CurPeriod(); + cp->PeriodDuration = Cfg.GetPeriodDuration(); + ConfigureTime = cp->StartTime; + ConfigurePeriodId = CurPeriodId; + + // Create new history + TVector<TPeriod> oldPeriod; + oldPeriod.swap(Period); + Period.resize(Cfg.GetHistorySize()); + + // Rearrange old history into new one using new modulo + // and fill old periods with zeros if history was enlarged + ui64 id = CurPeriodId; + for (ui64 j = Min(Period.size(), oldPeriod.size()); j > 0 && id != ui64(-1); j--, id--) { + *GetPeriod(id) = oldPeriod[id % oldPeriod.size()]; + } + for (ui64 j = oldPeriod.size(); j < Period.size() && id != ui64(-1); j++, id--) { + InitPeriod(id, 0); + } + } + SetWindow(Window); // update CostCap + LWPROBE(Configure, GetName(), Now, cfg.ShortDebugString()); +} + +bool TFlowCtl::IsOpen() const +{ + return Cfg.GetDisabled() || CountInFly < Cfg.GetMinCountInFly() || CostInFly < Window; +} + +bool TFlowCtl::IsOpenForMonitoring() const +{ + return Cfg.GetDisabled() || CostInFly < WindowCloseThreshold; +} + +enum class EMode { + Zero = 0, + Restart = 1, + Unused = 2, + SlowStart = 3, + Optimum = 4, + Unsteady = 5, + Undispersive = 6, + WindowInc = 7, + WindowDec = 8, + Overshoot = 9, + CatchUp = 10, + FixedWindow = 11, + FixedLatency = 12 +}; + +void TFlowCtl::Arrive(TFcOp& op, ui64 estcost, double now) +{ + TPeriod* p = AdvanceTime(now, true); + p->AccumulateClosedTime(now, IsOpen()); + + op.EstCost = estcost; + op.UsedCost = Min(estcost, CostCap); + op.ArriveTime = Now; + op.PeriodId = CurPeriodId; + op.OpId = ++LastOpId; + + p->CountInFly++; + p->CostInFly += op.EstCost; + + LWPROBE(Arrive, GetName(), op.OpId, op.PeriodId, Now, + op.EstCost, op.UsedCost, CountInFly, CostInFly, Window); + + CountInFly++; + CostInFly += op.UsedCost; + if (KleinrockControlEnabled() && CountInFly <= Cfg.GetMinCountInFly() && State < CostInFly) { + ui64 minCost = RealCostMA * Cfg.GetMinCountInFly(); + if (State < minCost) { + SetWindow(minCost); + State = Window; + UpdateThreshold(); + } + } + + AtomicIncrement(Counters->CountArrived); + AtomicAdd(Counters->CostArrived, op.EstCost); +} + +void TFlowCtl::Depart(TFcOp& op, ui64 realcost, double now) +{ + Y_ABORT_UNLESS(Now >= op.ArriveTime); + Y_ABORT_UNLESS(CurPeriodId >= op.PeriodId); + + TPeriod* p = AdvanceTime(now, true); + p->AccumulateClosedTime(now, IsOpen()); + + CountInFly--; + CostInFly -= op.UsedCost; + + // Weight is coef of significance of given op in period performance metrics + // we use 1, 1/2, 1/3... weight if ops was execute during 1, 2, 3... periods + double weight = 1.0 / (1 + CurPeriodId - op.PeriodId); + + double latency = Now - op.ArriveTime; + LatencyMA = latency * Cfg.GetLatencyMACoef() + + LatencyMA * (1 - Cfg.GetLatencyMACoef()); + RealCostMA = realcost * Cfg.GetRealCostMACoef() + + RealCostMA * (1 - Cfg.GetRealCostMACoef()); + + LWPROBE(Depart, GetName(), op.OpId, op.PeriodId, Now, + op.EstCost, op.UsedCost, CountInFly, CostInFly, realcost, + LatencyMA * 1000, weight, Window); + + LWPROBE(DepartLatency, GetName(), op.OpId, op.PeriodId, Now, + latency * 1000, LatencyMA * 1000, RealCostMA, op.ArriveTime); + + // Iterate over all periods that intersect execution of operation + if (op.PeriodId + Cfg.GetHistorySize() > CurPeriodId) { + p = GetPeriod(op.PeriodId); + p->CountInFly--; + p->CostInFly -= op.EstCost; + } else { + p = LastPeriod(); + AtomicIncrement(Counters->CountDepartedLate); + AtomicAdd(Counters->CostDepartedLate, realcost); + } + TPeriod* curPeriod = CurPeriod(); + while (true) { + p->WeightSum += weight; + p->RealCostSum += weight * realcost; + p->EstCostSum += weight * op.EstCost; + p->LatencySum += weight * LatencyMA; + p->LatencySqSum += weight * LatencyMA * LatencyMA; + if (p == curPeriod) { + break; + } + p = NextPeriod(p); + } + + AtomicIncrement(Counters->CountDeparted); + AtomicAdd(Counters->CostDeparted, realcost); +} + +void TFlowCtl::Abort(TFcOp& op) +{ + CountInFly--; + CostInFly -= op.UsedCost; + + // Abort op in period it arrived (if it is not forgotten yet) + if (op.PeriodId + Cfg.GetHistorySize() > CurPeriodId) { + TPeriod* p = GetPeriod(op.PeriodId); + p->CountInFly--; + p->CostInFly -= op.EstCost; + } + + AtomicIncrement(Counters->CountAborted); + AtomicAdd(Counters->CostAborted, op.EstCost); + + LWPROBE(Abort, GetName(), op.OpId, op.PeriodId, Now, + op.EstCost, op.UsedCost, CountInFly, CostInFly); +} + +TFlowCtl::EStateTransition TFlowCtl::ConfigureST(const TFlowCtlConfig& cfg, double now) +{ + bool wasOpen = IsOpen(); + Configure(cfg, now); + return CreateST(wasOpen, IsOpen()); +} + +TFlowCtl::EStateTransition TFlowCtl::ArriveST(TFcOp& op, ui64 estcost, double now) +{ + bool wasOpen = IsOpen(); + Arrive(op, estcost, now); + return CreateST(wasOpen, IsOpen()); +} + +TFlowCtl::EStateTransition TFlowCtl::DepartST(TFcOp& op, ui64 realcost, double now) +{ + bool wasOpen = IsOpen(); + Depart(op, realcost, now); + return CreateST(wasOpen, IsOpen()); +} + +TFlowCtl::EStateTransition TFlowCtl::AbortST(TFcOp& op) +{ + bool wasOpen = IsOpen(); + Abort(op); + return CreateST(wasOpen, IsOpen()); +} + +void TFlowCtl::UpdateCounters() +{ + AdvanceMonitoring(); + AtomicSet(Counters->CostInFly, CostInFly); + AtomicSet(Counters->CountInFly, CountInFly); + AtomicSet(Counters->Window, Window); +} + +void TFlowCtl::SetWindow(ui64 window) +{ + Window = window; + CostCap = Max<ui64>(Cfg.GetMinCostInFly(), ui64(ceil(double(window) * Cfg.GetCostCapToWindow()))); +} + +TFlowCtl::EStateTransition TFlowCtl::CreateST(bool wasOpen, bool isOpen) +{ + if (wasOpen) { + if (!isOpen) { + LWPROBE(TransitionClosed, GetName(), Now); + return Closed; + } + } else { + if (isOpen) { + LWPROBE(TransitionOpened, GetName(), Now); + return Opened; + } + } + return None; +} + +TFlowCtl::TPeriod* TFlowCtl::AdvanceTime(double now, bool control) +{ + AdvanceMonitoring(); + + LWPROBE(AdvanceTime, GetName(), Now, now); + + if (Now == 0) { // Initialization on first call + Now = now; + InitPeriod(); + } + + if (Now < now) { + Now = now; + } + + TPeriod* cp = CurPeriod(); + double end = cp->StartTime + cp->PeriodDuration; + if (end < Now) { + // Take into account zero periods (skipped; no arrival/departure) + // TODO: You should test this!! look in debugger!! better write UT + size_t skipPeriods = (Now - end) / Cfg.GetPeriodDuration(); + size_t addPeriods = Min(skipPeriods, Period.size()); + CurPeriodId += skipPeriods - addPeriods; + while (addPeriods-- > 0) { + CurPeriodId++; + cp = InitPeriod(); + } + + // Adjust window before starting new period, because we want have as + // much historic information as possible, including oldest period + if (control) { + Control(); + } + + // Start period with new value of Window + CurPeriodId++; + cp = InitPeriod(); + } + + return cp; +} + +TFlowCtl::TPeriod* TFlowCtl::GetPeriod(ui64 periodId) +{ + Y_ABORT_UNLESS(!Period.empty(), "TFlowCtl must be configured"); + return &Period[periodId % Cfg.GetHistorySize()]; +} + +TFlowCtl::TPeriod* TFlowCtl::CurPeriod() +{ + return GetPeriod(CurPeriodId); +} + +TFlowCtl::TPeriod* TFlowCtl::LastPeriod() +{ + if (CurPeriodId < Cfg.GetHistorySize()) { + return &Period[0]; + } else { + return GetPeriod(CurPeriodId + 1); + } +} + +TFlowCtl::TPeriod* TFlowCtl::NextPeriod(TFlowCtl::TPeriod* p) +{ + p++; + if (p == &Period[0] + Cfg.GetHistorySize()) { + p = &Period[0]; + } + return p; +} + +TFlowCtl::TPeriod* TFlowCtl::InitPeriod(ui64 periodId, ui64 window) +{ + TPeriod* cp = GetPeriod(periodId); + *cp = TPeriod(); + cp->PeriodId = periodId; + cp->StartTime = ConfigureTime + + (periodId - ConfigurePeriodId) * Cfg.GetPeriodDuration(); + cp->PeriodDuration = Cfg.GetPeriodDuration(); + cp->Window = window; + return cp; +} + +TFlowCtl::TPeriod* TFlowCtl::InitPeriod() +{ + TPeriod* cp = InitPeriod(CurPeriodId, Window); + LWPROBE(InitPeriod, GetName(), CurPeriodId, Now, Window); + return cp; +} + +bool TFlowCtl::KleinrockControlEnabled() +{ + return !Cfg.GetFixedWindow() && Cfg.GetFixedLatencyMs() <= 0.0; +} + +void TFlowCtl::Control() +{ + // Iterate over completed periods + TPeriod* p = LastPeriod(); + TPeriod* curPeriod = CurPeriod(); + double costSumError = 0; + double weightSumError = 0; + double latencySumError = 0; + double latencySqSumError = 0; + ui64 goodPeriods = 0; + ui64 badPeriods = 0; + ui64 zeroPeriods = 0; + double rthroughputSum = 0; + double ethroughputSum = 0; + double latencyAvgSum = 0; + double rthroughputSqSum = 0; + double ethroughputSqSum = 0; + double latencyAvgSqSum = 0; + double rthroughputMin = -1.0; + double rthroughputMax = -1.0; + double ethroughputMin = -1.0; + double ethroughputMax = -1.0; + double latencyAvgMin = -1.0; + double latencyAvgMax = -1.0; + // Note that curPeriod is not included, because it can have or not have + // higher error, and could lead to `goodPeriods' blinking + for (; p != curPeriod; p = NextPeriod(p)) { + // It is a good approximation to think that op will continue it's + // execution for as long as it already executes, so it's weight will be + // about twice less than if op will complete in current period + ui64 periodsToComplete = 2*(CurPeriodId - p->PeriodId); + double weightEst = 1.0 / periodsToComplete; + double latencyEst = p->PeriodDuration * periodsToComplete; + + // Error accumulates over uncompleted ops of periods before `p' + costSumError += p->CostInFly * weightEst; + weightSumError += p->CountInFly * weightEst; + latencySumError += p->CountInFly * weightEst * latencyEst; + latencySqSumError += p->CountInFly * weightEst * latencyEst * latencyEst; + + // Take in-fly operations into account to increase accuracy + double rcostSum = p->RealCostSum + costSumError; + double ecostSum = p->EstCostSum + costSumError; + double weightSum = p->WeightSum + weightSumError; + double latencySum = p->LatencySum + latencySumError; + double latencySqSum = p->LatencySqSum + latencySqSumError; + + LWPROBE(PeriodStats, GetName(), CurPeriodId, Now, p->PeriodId, + rcostSum, costSumError, + weightSum, weightSumError, + latencySum, latencySumError); + if (rcostSum == 0 || weightSum == 0) { + zeroPeriods++; + } else if (costSumError / rcostSum < Cfg.GetMeasurementError() && // cost + weightSumError / weightSum < Cfg.GetMeasurementError() // count + ) { + // Period performace metrics at best we can estimate for now + double latencyAvg = latencySum / weightSum; + double latencyErr = sqrt(latencySqSum / weightSum - latencyAvg * latencyAvg); + double rthroughput = rcostSum / p->PeriodDuration; + double ethroughput = ecostSum / p->PeriodDuration; + + LWPROBE(PeriodGood, GetName(), CurPeriodId, Now, p->PeriodId, + goodPeriods, rthroughput, ethroughput, latencyAvg, latencyErr); + + // Aggregate performance metrics over all "good" periods + goodPeriods++; + latencyAvgSum += latencyAvg; + rthroughputSum += rthroughput; + ethroughputSum += ethroughput; + latencyAvgSqSum += latencyAvg * latencyAvg; + rthroughputSqSum += rthroughput * rthroughput; + ethroughputSqSum += ethroughput * ethroughput; + if (rthroughputMax < 0) { // For first pass + rthroughputMin = rthroughputMax = rthroughput; + ethroughputMin = ethroughputMax = ethroughput; + latencyAvgMin = latencyAvgMax = latencyAvg; + } else { + rthroughputMin = Min(rthroughput, rthroughputMin); + rthroughputMax = Max(rthroughput, rthroughputMax); + ethroughputMin = Min(ethroughput, ethroughputMin); + ethroughputMax = Max(ethroughput, ethroughputMax); + latencyAvgMin = Min(latencyAvg, latencyAvgMin); + latencyAvgMax = Max(latencyAvg, latencyAvgMax); + } + } else { // Not enough measurement accuracy + badPeriods++; + } + } + + AtomicAdd(Counters->BadPeriods, badPeriods); + AtomicAdd(Counters->GoodPeriods, goodPeriods); + AtomicAdd(Counters->ZeroPeriods, zeroPeriods); + LWPROBE(Measurement, GetName(), CurPeriodId, Now, + badPeriods, goodPeriods, zeroPeriods); + + EMode mode = EMode::Zero; // Just for monitoring + if (KleinrockControlEnabled() && goodPeriods == 0) { + if (zeroPeriods == 0) { + mode = EMode::Restart; // Probably we have huge window, restart + SetWindow(Cfg.GetMinCostInFly()); + State = Window; + SlowStart = Cfg.GetSlowStartLimit(); + // NOTE: PeriodDuration maybe lower than latency. + // NOTE: If this is the case try to manually choose PeriodDuration + // NOTE: to be much higher (x20) than average latency + } else { + mode = EMode::Unused; // Looks like utilization is zero mainly, wait + } + } else { + double L = latencyAvgSum / goodPeriods; + double T = rthroughputSum / goodPeriods; + double ET = ethroughputSum / goodPeriods; + double dT = sqrt(rthroughputSqSum / goodPeriods - T*T); + double dET = sqrt(ethroughputSqSum / goodPeriods - ET*ET); + double dL = sqrt(latencyAvgSqSum / goodPeriods - L*L); + + LWPROBE(Throughput, GetName(), CurPeriodId, Now, + T, rthroughputMin, rthroughputMax, T-dT, T+dT, dT/T); + LWPROBE(EstThroughput, GetName(), CurPeriodId, Now, + ET, ethroughputMin, ethroughputMax, ET-dET, ET+dET, dET/ET); + LWPROBE(Latency, GetName(), CurPeriodId, Now, + L*1000.0, latencyAvgMin*1000.0, latencyAvgMax*1000.0, (L-dL)*1000.0, (L+dL)*1000.0, dL/L); + + // Process variable = abs latency error over abs throughput error + double pv; + if (dT/T < 1e-6) { + pv = 1e6; + } else { + pv = (dL/L) / (dT/T); // We want pv -> 1 + } + + // Error. Maps pv=0 -> e=1, pv=1 -> e=0 and pv=+inf -> e=-1 + // Intention is to make throughput and latency symetric for controller + double err = (pv < 1.0? 1.0 - pv: 1.0/pv - 1.0); // We want e -> 0 + + if (!Cfg.GetFixedWindow() && Cfg.GetFixedLatencyMs() <= 0.0 && SlowStart) { + mode = EMode::SlowStart; // In SlowStart mode we always multiply window by max allowed coef + State *= 1.0 + Cfg.GetMultiplierLimit(); + SetWindow(State); + SlowStart--; + } else { + // Exclude NaNs and zeros + if (dL >= 0 && L > 0 && dT >= 0 && T > 0 && pv >= 0) { + AtomicSet(Counters->Throughput, T * 1e6); + AtomicSet(Counters->Latency, L * 1e6); + AtomicSet(Counters->ThroughputDispAbs, dT/T * 1e6); + AtomicSet(Counters->LatencyDispAbs, dL/L * 1e6); + LWPROBE(Slowdown, GetName(), CurPeriodId, Now, Slowdown); + if (Slowdown) { + mode = EMode::Optimum; // Just wait if slowdown enabled + Slowdown--; + } else { + if (dL/L > Cfg.GetSteadyLimit() || + dT/T > Cfg.GetSteadyLimit()) + { + mode = EMode::Unsteady; // Too unsteady, wait + } else { + if (dL/L < Cfg.GetMeasurementError() && + dT/T < Cfg.GetMeasurementError()) + { // Dispersions are lower than measurement accuracy + mode = EMode::Undispersive; + } else { + // Slowdown if we are close to setpoint (optimum) + if (Cfg.GetSlowdownLimit() && + 0.5 / Cfg.GetSlowdownLimit() < fabs(err) && + fabs(err) < 0.5) + { + Slowdown = Min( + Cfg.GetSlowdownLimit(), + ui64(1.0/fabs(err))); + } + + // Make controlling decision + // e-values closer to 0 lead to finer control + // due to e-square term + double ratio = 1.0 + Cfg.GetMultiplierLimit() * + err * err * (pv < 1? 1.0: -1.0); + double target = T * L * ratio; + + // System reaction has lag. Real position is T*L. + // In steady state with full window, Little's law: + // T*L = Window + // So we have two exceptions: + // - not full window due to underutilization + // - not steady state due to reaction lag + if (target > 0.5 * State) { + mode = ratio > 1.0? EMode::WindowInc: EMode::WindowDec; + State *= ratio; + } else if (target > Cfg.GetMinCostInFly() + && ratio < 1.0) + { + mode = EMode::Overshoot; // Too large window + State *= 1.0 - Cfg.GetMultiplierLimit(); + } else { + mode = EMode::CatchUp; // Catching up, wait + } + + // Apply boundary conditions + if (State < Cfg.GetMinCostInFly()) { + State = Cfg.GetMinCostInFly(); + } + + LWPROBE(Control, GetName(), CurPeriodId, Now, + pv, err, ratio, target); + } + } + } + } + // Set window to osscilate near current state + double disp = Cfg.GetMeasurementError() * 2.0; + double mult = 1.0 - disp + + ((CurPeriodId * 2 / Cfg.GetHistorySize() / 3) % 2) * disp; + SetWindow(mult * State); + if (Window < Cfg.GetMinCostInFly()) { + SetWindow(Cfg.GetMinCostInFly()); + } + if (Cfg.GetFixedWindow()) { + mode = EMode::FixedWindow; // Fixed window mode + SetWindow(Cfg.GetFixedWindow()); + State = Window; + } else if (Cfg.GetFixedLatencyMs() > 0.0) { + mode = EMode::FixedLatency; // Fixed latency mode + SetWindow(Max<ui64>( + Cfg.GetMinCostInFly(), + Cfg.GetFixedLatencyMs() * ethroughputMax / 1000.0)); + State = Window; + } + LWPROBE(Window, GetName(), CurPeriodId, Now, Window); + } + UpdateThreshold(); + } + + switch (mode) { +#define TLFC_MODE(name) \ + case EMode::name: AtomicIncrement(Counters->Mode ## name); break + TLFC_MODE(Zero); + TLFC_MODE(Restart); + TLFC_MODE(Unused); + TLFC_MODE(SlowStart); + TLFC_MODE(Optimum); + TLFC_MODE(Unsteady); + TLFC_MODE(Undispersive); + TLFC_MODE(WindowInc); + TLFC_MODE(WindowDec); + TLFC_MODE(Overshoot); + TLFC_MODE(CatchUp); + TLFC_MODE(FixedWindow); + TLFC_MODE(FixedLatency); + } +#undef TLFC_MODE + LWPROBE(State, GetName(), CurPeriodId, Now, (ui64)mode, State); +} + +void TFlowCtl::AdvanceMonitoring() +{ + if (LastEvent == 0) { + LastEvent = GetCycleCount(); + } + ui64 now = GetCycleCount(); + ui64 elapsed = 0; + if (LastEvent < now) { + elapsed = now - LastEvent; + LastEvent = now; + } + ui64 elapsedUs = CyclesToDuration(elapsed).MicroSeconds(); + bool isIdle = CountInFly == 0; + bool isOpen = IsOpenForMonitoring(); + if (!LastIdle && isIdle) { + AtomicIncrement(Counters->IdleCount); + } + if (LastOpen && !isOpen) { + AtomicIncrement(Counters->CloseCount); + } + if (isIdle) { + AtomicAdd(Counters->IdleUs, elapsedUs); + } else if (isOpen) { + AtomicAdd(Counters->OpenUs, elapsedUs); + } else { + AtomicAdd(Counters->ClosedUs, elapsedUs); + } + LastIdle = isIdle; + LastOpen = isOpen; +} + +void TFlowCtl::UpdateThreshold() +{ + WindowCloseThreshold = Window * UtilizationThreshold / 100; +} + +} diff --git a/ydb/library/shop/flowctl.h b/ydb/library/shop/flowctl.h new file mode 100644 index 00000000000..76c1888e77d --- /dev/null +++ b/ydb/library/shop/flowctl.h @@ -0,0 +1,213 @@ +#pragma once + +#include <ydb/library/shop/protos/shop.pb.h> + +#include <library/cpp/deprecated/atomic/atomic.h> + +#include <util/generic/string.h> +#include <util/generic/vector.h> +#include <util/system/types.h> + +namespace NShop { + +struct TFlowCtlCounters { + TAtomic CostArrived = 0; // Total estimated cost arrived + TAtomic CountArrived = 0; // Total ops arrived + TAtomic CostDeparted = 0; // Total real cost departed + TAtomic CountDeparted = 0; // Total ops departed + TAtomic CostDepartedLate = 0; // Total real cost departed after history forget it + TAtomic CountDepartedLate = 0; // Total ops departed after history forget it + TAtomic CostAborted = 0; // Total estimated cost aborted + TAtomic CountAborted = 0; // Total ops aborted + TAtomic CostInFly = 0; // Currently estimated cost in flight + TAtomic CountInFly = 0; // Currently ops in flight + + TAtomic Window = 0; // Current window size in estimated cost units + TAtomic IdleCount = 0; // Total switches to idle state (zero in-flight) + TAtomic CloseCount = 0; // Total switches to closed state (ecost in-flight >= 80% Window) + TAtomic IdleUs = 0; // Total microseconds in idle state + TAtomic OpenUs = 0; // Total microseconds in open state + TAtomic ClosedUs = 0; // Total microseconds in closed state + + // On every period boundary we analyze history up to `Cfg.HistorySize' periods into past + // and based on amount and cost of still-in-fly requests we can estimate error + // of throughtput and latency measurements, and than control it based on `Cfg.MeasurementError' + // NOTE: one period is counted multiple times (Cfg.HistorySize) and can be classified differently + TAtomic BadPeriods = 0; // Total periods with too high measurement error + TAtomic GoodPeriods = 0; // Total periods with acceptable measurment error + TAtomic ZeroPeriods = 0; // Total periods w/o requests (not crossed by any request) + + // Performance metrics + // Measured based on good period (up to `Cfg.HistorySize') + TAtomic Throughput = 0; // Measured throughput (micro-realcost per second) + TAtomic Latency = 0; // Measured latency (microseconds) + TAtomic ThroughputDispAbs = 0; // Measured throughput variability (ppm) + TAtomic LatencyDispAbs = 0; // Measured latency variability (ppm) + + // On every period flow control operates in one on the following mode: + TAtomic ModeZero = 0; // Undefined + TAtomic ModeRestart = 0; // Restart with minimal window due to every period being bad + TAtomic ModeUnused = 0; // Only zero and/or bad periods (just wait, probably idle) + TAtomic ModeSlowStart = 0; // Window exponential growth after Restart + TAtomic ModeOptimum = 0; // Optimal operating point (Kleinrock point) + TAtomic ModeUnsteady = 0; // Too high variability of latency and/or throughput + TAtomic ModeUndispersive = 0; // Too low variability of latency and throughput + TAtomic ModeWindowInc = 0; // Increase window + TAtomic ModeWindowDec = 0; // Decrease window + TAtomic ModeOvershoot = 0; // Too large window, decrease as fast as allowed + TAtomic ModeCatchUp = 0; // Catching up, wait + + // Do not apply smart flowctl, use `Cfg.FixedWindow' + TAtomic ModeFixedWindow = 0; + + // Use max measured estimated cost throughput ETmax (estcost per second) + // to compute `Window = ETmax * Cfg.FixedLatencyMs' + TAtomic ModeFixedLatency = 0; +}; + + +struct TFcOp { + ui64 EstCost = 0; // Estimation of cost; should be known on arrive + ui64 UsedCost = 0; // Min(EstCost, Window * Cfg.CostCapToWindow) + double ArriveTime = 0; + ui64 PeriodId = 0; + ui64 OpId = 0; + + bool HasArrived() const + { + return OpId != 0; + } + + void Reset() + { + OpId = 0; + } +}; + +class TFlowCtl : public TAtomicRefCount<TFlowCtl> { +public: + enum EStateTransition { + None = 0, + Closed = 1, + Opened = 2 + }; +private: + TFlowCtlConfig Cfg; + TString Name; + + // Flow state + ui64 Window; + ui64 CostCap; + ui64 CostInFly = 0; + ui64 CountInFly = 0; + ui64 CurPeriodId = 0; + ui64 LastOpId = 0; + + // Performance measurements + struct TPeriod { + // Constants during period + ui64 PeriodId = 0; + double StartTime = 0; + double PeriodDuration = 0; + ui64 Window = 0; // Window used for period + + // Intermediate and accumulated values + ui64 CountInFly = 0; // Number of in fly ops arrived during period + ui64 CostInFly = 0; // In-fly-cost of arrived during period operations + double WeightSum = 0; // Total weight of executed operations + double RealCostSum = 0; // Weighted sum of executed operations' real cost + double EstCostSum = 0; // Weighted sum of executed operations' estimated cost + double LatencySum = 0; // Weighted sum of executed operations' latencies + double LatencySqSum = 0; // Weighted sum of executed operations' latency squares + + // Window utilization + double LastEventTime = 0; + double TotalClosedTime = 0; + void AccumulateClosedTime(double now, bool wasOpen) { + if (!LastEventTime) { + LastEventTime = StartTime; + } + double end = StartTime + PeriodDuration; + if (now > end) { + now = end; + } + if (!wasOpen) { + TotalClosedTime += now - LastEventTime; + } + LastEventTime = now; + } + }; + TVector<TPeriod> Period; // Cyclic buffer for last periods + double Now = 0; + double ConfigureTime = 0; + double ConfigurePeriodId = 0; + double LatencyMA = 0; + double RealCostMA = 0; + + // Controller + double State; + ui64 Slowdown = 0; + ui64 SlowStart = 0; + + // Monitoring + TFlowCtlCounters* Counters; + static TFlowCtlCounters FakeCounters; + ui64 LastEvent = 0; + bool LastOpen = true; + bool LastIdle = true; + + // If `UtilizationThreshold' share of Window is used (in-fly) than + // for monitoring we count window as closed. This is done to provide meaningful + // `ClosedUs' counter. Note that if we count really !IsOpen() time + // we'd get metric that depend on reaction time (just opened -> closed again) + static const ui64 UtilizationThreshold = 80; // percent + ui64 WindowCloseThreshold; + +public: + // Configuration + explicit TFlowCtl(ui64 initialWindow = 1); + TFlowCtl(const TFlowCtlConfig& cfg, double now, ui64 initialWindow = 1); + // You'd better validate config before Configure() to avoid troubles + static void Validate(const TFlowCtlConfig& cfg); + void Configure(const TFlowCtlConfig& cfg, double now); + + // Processing + bool IsOpen() const; + bool IsOpenForMonitoring() const; + void Arrive(TFcOp& op, ui64 estcost, double now); + void Depart(TFcOp& op, ui64 realcost, double now); + void Abort(TFcOp& op); + + // Adapters with state transitions (just sugar) + EStateTransition ConfigureST(const TFlowCtlConfig& cfg, double now); + EStateTransition ArriveST(TFcOp& op, ui64 estcost, double now); + EStateTransition DepartST(TFcOp& op, ui64 realcost, double now); + EStateTransition AbortST(TFcOp& op); + + // Acessors + const TFlowCtlConfig& GetConfig() { return Cfg; } + const TString& GetName() const { return Name; } + void SetName(const TString& name) { Name = name; } + + // Monitoring + void UpdateCounters(); + void SetCounters(TFlowCtlCounters* counters) { Counters = counters; } +private: + void SetWindow(ui64 window); + EStateTransition CreateST(bool wasOpen, bool isOpen); + TPeriod* AdvanceTime(double now, bool control); + TPeriod* GetPeriod(ui64 periodId); + TPeriod* CurPeriod(); // Returns current period + TPeriod* LastPeriod(); // Returns last remembered period + TPeriod* NextPeriod(TPeriod* p); // Iterate periods + TPeriod* InitPeriod(ui64 periodId, ui64 window); + TPeriod* InitPeriod(); + bool KleinrockControlEnabled(); + void Control(); + void AdvanceMonitoring(); + void UpdateThreshold(); +}; + +using TFlowCtlPtr = TIntrusivePtr<TFlowCtl>; + +} diff --git a/ydb/library/shop/lazy_scheduler.h b/ydb/library/shop/lazy_scheduler.h new file mode 100644 index 00000000000..dc2357a0140 --- /dev/null +++ b/ydb/library/shop/lazy_scheduler.h @@ -0,0 +1,788 @@ +#pragma once + +#include "counters.h" +#include "probes.h" +#include "resource.h" +#include "schedulable.h" +#include "shop_state.h" + +#include <util/generic/ptr.h> +#include <util/generic/string.h> +#include <util/generic/vector.h> +#include <library/cpp/containers/stack_vector/stack_vec.h> + +/// +/// Lazy scheduler is SFQ-scheduler with almost zero freezing cost (in term of CPU cycles). +/// Note that it is true even for hierarchical scheduling. Laziness means that hierarchy, +/// schedulers and consumers are not modified to freeze/unfreeze a machine. There is a global +/// shop state that reflects current state of machines, and every consumer has set of machines +/// from which at least one must be warm (not frozen) for consumer to be scheduled. +/// +/// In terms of resulting schedule (and fairness) there is no difference between shop/scheduler.h +/// and lazy scheduler, but there are the following issues: +/// - lazy scheduler has no layer of TFreezable objects between scheduler and consumers, but +/// freeze control is done through global (shared between schedulers) shop state; +/// - lazy scheduler has no heap to find consumer with minimal tag, but it just uses vector and +/// scans it on every scheduling event (it's okay if there are few consumers per node); +/// - it is more compute-intensive but less memory-intensive, because frozen key are pulled +/// on every scheduling event, but data required for it is dense; +/// - lazy scheduler uses small stack-vectors to remove one memory hop. +/// +/// Note that there is no thread-safety precautions (except for atomic monitoring counters). +/// + +namespace NShop { +namespace NLazy { + +template <class TRes = TSingleResourceDense> class TConsumer; +template <class TRes = TSingleResourceDense> class TScheduler; +template <class TRes = TSingleResourceDense> class TRootScheduler; + +using TConsumerIdx = ui16; + +constexpr TMachineIdx DoNotActivate = TMachineIdx(-1); +constexpr TMachineIdx DoNotDeactivate = TMachineIdx(-1); + +/////////////////////////////////////////////////////////////////////////////// + +constexpr size_t ResOptCount = 2; // optimal resource count (for stack vec) +constexpr size_t ConOptCount = 6; // optimal consumers count (for stack vec) + +/////////////////////////////////////////////////////////////////////////////// + +struct TCtx { + // Warm machines on current subtree (include overrides on local states) + TMachineMask Warm; + + // Create root context using shop state + explicit TCtx(const TShopState& shopState) + : Warm(shopState.Warm) + {} + + // Create subcontext on subtree with override and localstate + // NOTE: overrides bit marked in `override` using values from `state` + explicit TCtx(const TCtx& ctx, TMachineMask override, TMachineMask state) + : Warm((ctx.Warm & ~override) | (state & override)) + {} + + bool IsRunnable(TMachineMask active, TMachineMask allowed) const + { + return Warm & active & allowed; + } +}; + +/////////////////////////////////////////////////////////////////////////////// + +template <class TRes> +class TConsumer: public virtual TThrRefBase { +private: + using TCost = typename TRes::TCost; + using TTag = typename TRes::TTag; + using TSch = TScheduler<TRes>; + +private: + // Configuration + TString Name; + TWeight Weight = 1; + TSch* Scheduler = nullptr; + + // Scheduling state + TCost Underestimation = TCost(); // sum over all errors (realCost - estCost) to be fixed + TConsumerIdx Idx = TConsumerIdx(-1); // Index of consumer in parent scheduler + ui64 BusyPeriod = ui64(-1); + + // Monitoring + TTag Consumed = TTag(); + static TConsumerCounters FakeCounters; + TConsumerCounters* Counters = &FakeCounters; + +public: + virtual ~TConsumer() + { + Detach(); + } + + void SetName(const TString& name) { Name = name; } + const TString& GetName() const { return Name; } + + void SetWeight(TWeight weight) { Y_ABORT_UNLESS(weight > 0); Weight = weight; } + TWeight GetWeight() const { return Weight; } + + TSch* GetScheduler() const { return Scheduler; } + + void SetCounters(TConsumerCounters* counters) { Counters = counters; } + TConsumerCounters* GetCounters() { return Counters; } + + // Returns the next thing to schedule or nullptr (denial) + // WARNING: in case of denial, this scheduler will NOT be called with the + // same set (or its subset) of warm machines, till either + // - Allow(mi) is called on this or CHILD scheduler with machine from the set + // - DropDenials() is called on any PARENT scheduler (including root one) + virtual TSchedulable<typename TRes::TCost>* PopSchedulable(const TCtx& ctx) = 0; + + // Recursive counters update (nothing to do on leafs) + virtual void UpdateCounters() {} + + // Activates consumer to compete for resources on given machine + void Activate(TMachineIdx mi); + + // Removes consumer from competition for resources on given machine + // NOTE: Scheduler keeps track of resource consumption + // NOTE: till busy period is over (in case deactivated consumer will return) + void Deactivate(TMachineIdx mi); + + // Notify about unfreeze + virtual void Allow(TMachineIdx mi); + + // Clear every cached denial + virtual void DropDenials() {} + + // Removes consumer from competition for resources on every machine + virtual void DeactivateAll() = 0; + + // Unlinks scheduler and consumer + void Detach(); + + // Adds estimation error (real - est) cost to be fixed on this consumer + void AddUnderestimation(TCost cost) { + Underestimation += cost; + } + + // Monitoring + TTag ResetConsumed(); + virtual void DebugPrint(IOutputStream& out, const TString& indent = {}) const; + TString DebugString() const; + + friend class TScheduler<TRes>; +}; + +template <class TRes> +TConsumerCounters TConsumer<TRes>::FakeCounters; + +template <class TRes> +class TScheduler: public TConsumer<TRes> { +protected: + using TCost = typename TRes::TCost; + using TTag = typename TRes::TTag; + using TKey = typename TRes::TKey; + using TCon = TConsumer<TRes>; + +protected: + // Configuration + TShopState* ShopState = nullptr; + TMachineMask* Override = nullptr; + TMachineMask* LocalState = nullptr; + TConsumerIdx Active = 0; // Total number of consumers currently active on at least on machine + ui64 BusyPeriod = 0; + TTag VirtualTime = TTag(); + + // Scheduling state + TStackVec<TCon*, ConOptCount> Consumers; // consumerIdx -> consumer-object + TStackVec<TMachineMask, ConOptCount> Masks; // consumerIdx -> machineIdx -> (1=active | 0=empty) + TStackVec<TMachineMask, ConOptCount> Allowed; // consumerIdx -> machineIdx -> (1=allowed | 0=frozen) + TStackVec<TKey, ConOptCount> Keys; // consumerIdx -> TKey (e.g. amount of consumed resource so far) + TStackVec<TConsumerIdx, ConOptCount> FrozenTmp; // temporary array for consumers we have to pull + TStackVec<TConsumerIdx, ResOptCount> ActivePerMachine; // machineIdx -> number of active consumers +public: + // NOTE: Shop state should be set before scheduling begins + // NOTE: Shop state can be shared by multiple schedulers + void SetShopState(TShopState* value); + TShopState* GetShopState() const { return ShopState; } + + // Do not use `ShopState` bits marked by `override` mask, instead use corresponding bits from `local` mask + void OverrideState(TMachineMask* override, TMachineMask* local) { Override = override; LocalState = local; } + TMachineMask* GetOverride() const { return Override; } + TMachineMask* GetLocalState() const { return LocalState; } + + // Attaches new consumer to scheduler + void Attach(TCon* consumer); + + // Returns the next thing to schedule or nullptr (if empty and/or frozen) + TSchedulable<typename TRes::TCost>* PopSchedulable(); + TSchedulable<typename TRes::TCost>* PopSchedulable(const TCtx& ctx) override; + + // Return true iff there is no active consumers for warm machines + bool Empty() const; // DEPRECATED: use TRootScheduler::IsRunnable() + + // Activates every consumer for which `machineIdx(consumer)' + // call returns idx different from `DoNotActivate'. + // Returns number of activatied consumers + template <class TFunc> + size_t ActivateConsumers(TFunc&& machineIdx); + + // Deactivates every consumer for which `machineIdx(consumer)' + // call returns idx different from `DoNotActivate'. + // Returns number of activatied consumers + template <class TFunc> + size_t DeactivateConsumers(TFunc&& machineIdx); + + // Recursively DeactivateAll() every active consumer + void DeactivateAll() override; + + // Clears every cached denial + void DropDenials() override; + + // Detaches all consumers + void Clear(); + + // Monitoring + void UpdateCounters() override; + void DebugPrint(IOutputStream& out, const TString& indent = {}) const override; +private: + TSchedulable<typename TRes::TCost>* PopSchedulableImpl(const TCtx& ctx); + bool IsNotFrozen(TConsumerIdx ci) const; + void Swap(TConsumerIdx ci1, TConsumerIdx ci2); + void Activate(TConsumerIdx ci, TMachineIdx mi); + TConsumerIdx Deactivate(TConsumerIdx ci, TMachineIdx mi); + void Deny(TConsumerIdx ci, TMachineMask machines); + void AllowChild(TConsumerIdx ci, TMachineIdx mi); + void Detach(TConsumerIdx ci); + + friend class TConsumer<TRes>; +}; + +/////////////////////////////////////////////////////////////////////////////// + +template <class TRes> +class TRootScheduler: public TScheduler<TRes> { +protected: + TMachineMask RootAllowed; +public: + bool IsRunnable() const; + TSchedulable<typename TRes::TCost>* PopSchedulable(const TCtx& ctx) override; + void Allow(TMachineIdx mi) override; +}; + +/////////////////////////////////////////////////////////////////////////////// + +template <class TRes> +inline +void TConsumer<TRes>::Activate(TMachineIdx mi) +{ + Scheduler->Activate(Idx, mi); +} + +template <class TRes> +inline +void TConsumer<TRes>::Deactivate(TMachineIdx mi) +{ + Scheduler->Deactivate(Idx, mi); +} + +template <class TRes> +void TConsumer<TRes>::Allow(TMachineIdx mi) +{ + if (Scheduler) { + Scheduler->AllowChild(Idx, mi); + } +} + +template <class TRes> +inline +void TConsumer<TRes>::Detach() +{ + if (Scheduler) { + Scheduler->Detach(Idx); + } +} + +template <class TRes> +inline +typename TRes::TTag TConsumer<TRes>::ResetConsumed() +{ + TTag result = Consumed; + Consumed = 0; + return result; +} + +template <class TRes> +inline +void TConsumer<TRes>::DebugPrint(IOutputStream& out, const TString& indent) const +{ + out << indent << "Name: " << Name << Endl + << indent << "Weight: " << Weight << Endl + << indent << "BusyPeriod: " << BusyPeriod << Endl + << indent << "Borrowed: " << Counters->Borrowed << Endl + << indent << "Donated: " << Counters->Donated << Endl + << indent << "Usage: " << Counters->Usage << Endl + << indent << "Idx: " << Idx << Endl; +} + +template <class TRes> +inline +TString TConsumer<TRes>::DebugString() const +{ + TStringStream ss; + DebugPrint(ss); + return ss.Str(); +} + +/////////////////////////////////////////////////////////////////////////////// + +template <class TRes> +inline +void TScheduler<TRes>::SetShopState(TShopState* value) +{ + ShopState = value; + // NOTE: machines can be added and removed on flight, but cannot change its machineIds + ActivePerMachine.resize(ShopState->MachineCount, 0); +} + +template <class TRes> +inline +TSchedulable<typename TRes::TCost>* TScheduler<TRes>::PopSchedulable() +{ + return PopSchedulable(TCtx(*ShopState)); +} + + +template <class TRes> +inline +TSchedulable<typename TRes::TCost>* TScheduler<TRes>::PopSchedulable(const TCtx& ctx) +{ + if (Override) { + Y_ASSERT(LocalState); + return PopSchedulableImpl(TCtx(ctx, *Override, *LocalState)); + } else { + return PopSchedulableImpl(ctx); + } +} + +template <class TRes> +inline +TSchedulable<typename TRes::TCost>* TScheduler<TRes>::PopSchedulableImpl(const TCtx& ctx) +{ + while (true) { + // Select next warm consumer to be scheduled in a fair way + // And collect frozen consumer keys (to pull later) + TKey minKey = TRes::MaxKey(); + TConsumerIdx ci = TConsumerIdx(-1); + FrozenTmp.clear(); + for (TConsumerIdx cj = 0; cj < Active; cj++) { + if (ctx.IsRunnable(Masks[cj], Allowed[cj])) { + // If consumer is active on at least one warm machine -- it can be scheduled + TKey& key = Keys[cj]; + if (key < minKey) { + minKey = key; + ci = cj; + } + } else { + // Cannot be scheduled, but is active on at least one frozen machine + FrozenTmp.emplace_back(cj); + } + } + + if (ci == TConsumerIdx(-1)) { // If all warm and active consumers turned to be inactive + GLOBAL_LWPROBE(SHOP_PROVIDER, LazyIdlePeriod, + this->Name, VirtualTime, BusyPeriod); + + // Start new idle period (and new busy period that will follow it) + BusyPeriod++; + for (TConsumerIdx cj : FrozenTmp) { // Frozen consumers should NOT have idle periods + Consumers[cj]->BusyPeriod = BusyPeriod; + TKey& frozenKey = Keys[cj]; + frozenKey = TRes::OffsetKey(frozenKey, -VirtualTime); + } + VirtualTime = TTag(); + + if (!this->GetScheduler()) { + GLOBAL_LWPROBE(SHOP_PROVIDER, LazyWasted, + this->Name, VirtualTime, BusyPeriod, + this->DebugString()); + } + + return nullptr; + } + + // Schedule consumer + TCon* consumer = Consumers[ci]; + ui64 busyPeriod = BusyPeriod; + + if (TSchedulable<typename TRes::TCost>* schedulable = consumer->PopSchedulable(ctx)) { + // Update idx in case consumer was deactivated in PopSchedulable() + // NOTE: FrozenKeys cannot invalidate even if consumer was deactivated + ci = consumer->Idx; + + // Propagate virtual time (if idle period has not been started) + if (busyPeriod == BusyPeriod) { + TTag vtime0 = VirtualTime; + VirtualTime = TRes::GetTag(minKey); + TTag vtimeDelta = VirtualTime - vtime0; + + // Pull all frozen consumers + for (TConsumerIdx cj : FrozenTmp) { + TKey& frozenKey = Keys[cj]; + frozenKey = TRes::OffsetKey(frozenKey, vtimeDelta); + } + } + + // Try to immediatly fix any discrepancy between real and estimated costs + // (as long as it doesn't lead to negative cost) + TCost cost = schedulable->Cost + consumer->Underestimation; + if (cost >= 0) { + consumer->Underestimation = 0; + } else { + // Lower consumer overestimation by schedulable cost, and allow "free" usage + consumer->Underestimation = cost; + cost = 0; + } + + // Update consumer key + TTag duration = cost / consumer->Weight; + TKey& key = Keys[ci]; + key = TRes::OffsetKey(key, duration); + consumer->Consumed += duration; + + GLOBAL_LWPROBE(SHOP_PROVIDER, LazyPopSchedulable, + this->Name, consumer->Name, + VirtualTime, BusyPeriod, + TRes::GetTag(key), schedulable->Cost, + cost - schedulable->Cost, consumer->Weight, duration); + + return schedulable; + } else { + // Consumer refused to return schedulable + // Deny entrance into it with currently warm machines to avoid hang up + Deny(ci, ctx.Warm); + } + } +} + +template <class TRes> +inline +bool TScheduler<TRes>::Empty() const +{ + for (TConsumerIdx cj = 0; cj < Active; cj++) { + if (IsNotFrozen(cj)) { + return false; // There is an active consumer on at least one warm machine + } + } + return true; // There is no active consumers for warm machines +} + +template <class TRes> +inline +void TScheduler<TRes>::Clear() +{ + auto consumers = Consumers; + for (TCon* c : consumers) { + c->Detach(); + } + Y_ASSERT(Masks.empty()); + Y_ASSERT(Allowed.empty()); + Y_ASSERT(Keys.empty()); + Y_ASSERT(Consumers.empty()); +} + +template <class TRes> +inline +void TScheduler<TRes>::UpdateCounters() +{ + TCountersAggregator<TCon, TTag> aggr; + for (TCon* consumer : Consumers) { + aggr.Add(consumer); + consumer->UpdateCounters(); // Recurse into children + } + aggr.Apply(); +} + +template <class TRes> +inline +void TScheduler<TRes>::DebugPrint(IOutputStream& out, const TString& indent) const +{ + out << indent << "VirtualTime: " << VirtualTime << Endl + << indent << "BusyPeriod: " << BusyPeriod << Endl; + if (this->GetScheduler()) { + // Print consumer-related part of scheduler, if it is not root scheudler + TCon::DebugPrint(out, indent); + } + for (size_t ci = 0; ci < Consumers.size(); ci++) { + const TCon& c = *Consumers[ci]; + TString indent2 = indent + " "; + out << indent << "Consumer {" << Endl + << indent2 << (ci < Active? "[A]": "[I]") << "Mask: " << Masks[ci].ToString(ShopState->MachineCount) << Endl + << indent2 << "Allowed: " << Allowed[ci].ToString(ShopState->MachineCount) << Endl + << indent2 << "Key: " << Keys[ci] << Endl; + c.DebugPrint(out, indent2); + out << indent << "}" << Endl; + } +} + +template <class TRes> +inline +void TScheduler<TRes>::Attach(TCon* consumer) +{ + // Reset state in case consumer was used in another scheduler + consumer->BusyPeriod = size_t(-1); + consumer->Underestimation = TCost(); + consumer->Idx = Consumers.size(); + consumer->Scheduler = this; + + // Attach consumer + Consumers.emplace_back(consumer); + Masks.emplace_back(0); + Allowed.emplace_back(0); + Keys.emplace_back(TRes::ZeroKey(consumer)); + + Y_ABORT_UNLESS(Consumers.size() < (1ull << (8 * sizeof(TConsumerIdx))), "too many consumers"); +} + +template <class TRes> +inline +bool TScheduler<TRes>::IsNotFrozen(TConsumerIdx ci) const +{ + if (Override && LocalState) { + TMachineMask override = *Override; + TMachineMask state = *LocalState; + // Override bit marked in `Override` using values from `LocalState` + // and then check if there is at least one warm and active machine + return ((ShopState->Warm & ~override) + | (state & override)) & Masks[ci]; + } else { + // Just check if there is at least one warm and active machine + return ShopState->Warm & Masks[ci]; + } +} + +template <class TRes> +inline +void TScheduler<TRes>::Swap(TConsumerIdx ci1, TConsumerIdx ci2) +{ + DoSwap(Consumers[ci1], Consumers[ci2]); + DoSwap(Masks[ci1], Masks[ci2]); + DoSwap(Allowed[ci1], Allowed[ci2]); + DoSwap(Keys[ci1], Keys[ci2]); + Consumers[ci1]->Idx = ci1; + Consumers[ci2]->Idx = ci2; +} + +template <class TRes> +inline +void TScheduler<TRes>::Activate(TConsumerIdx ci, TMachineIdx mi) +{ + AllowChild(ci, mi); // Activation is worthless without allowment + + TMachineMask& mask = Masks[ci]; + if (mask.Get(mi)) { + return; // Avoid double activation + } + + // Recursively activate machine in parent schedulers (if required) + if (this->GetScheduler() && ActivePerMachine[mi] == 0) { + TCon::Activate(mi); + } + + // Activate machine for consumer + mask.Set(mi); + ActivePerMachine[mi]++; + + // Update consumer's key + TCon* consumer = Consumers[ci]; + TKey& key = Keys[ci]; + if (consumer->BusyPeriod != BusyPeriod) { + consumer->BusyPeriod = BusyPeriod; + // Memoryless property: consumption history must be reset on new busy period + key = TRes::ZeroKey(consumer); + // Estimation errors of pervious busy period does not matter any more + consumer->Underestimation = TCost(); + } + TRes::ActivateKey(key, VirtualTime); // Do not reclaim unused resource from past + + GLOBAL_LWPROBE(SHOP_PROVIDER, LazyActivate, + this->Name, consumer->Name, + VirtualTime, BusyPeriod, + TRes::GetTag(Keys[ci]), mask.ToString(ShopState->MachineCount), + mi, ActivePerMachine[mi]); + + // Rearrange consumers to have completely deactivated (on every machine) in separate range + if (ci >= Active) { + if (ci != Active) { + Swap(ci, Active); + } + Active++; + } +} + +template <class TRes> +template <class TFunc> +inline +size_t TScheduler<TRes>::ActivateConsumers(TFunc&& machineIdx) +{ + size_t prevActiveCount = Active; + auto i = Consumers.begin() + Active; + auto e = Consumers.end(); + for (TConsumerIdx ci = Active; i != e; ++i, ci++) { + TCon* consumer = *i; + TMachineIdx mi = machineIdx(consumer); + if (mi != DoNotActivate) { + Activate(ci, mi); + } + } + return Active - prevActiveCount; +} + +template <class TRes> +template <class TFunc> +inline +size_t TScheduler<TRes>::DeactivateConsumers(TFunc&& machineIdx) +{ + size_t prevActiveCount = Active; + auto i = Consumers.rend() - Active; + auto e = Consumers.rend(); + for (TConsumerIdx ci = Active - 1; i != e; ++i, ci--) { + TCon* consumer = *i; + TMachineIdx mi = machineIdx(consumer); + if (mi != DoNotDeactivate) { + Deactivate(ci, mi); + } + } + return prevActiveCount - Active; +} + +template <class TRes> +inline +void TScheduler<TRes>::DeactivateAll() +{ + while (Active > 0) { + TCon* consumer = Consumers[Active - 1]; + consumer->DeactivateAll(); + Y_ABORT_UNLESS(Masks[consumer->Idx].IsZero(), + "unable to deactivate consumer '%s' of scheduler '%s'", + consumer->GetName().c_str(), this->GetName().c_str()); + } +} + +template <class TRes> +void TScheduler<TRes>::DropDenials() +{ + // every active consumed is allowed recursively + for (TConsumerIdx ci = 0; ci < Active; ci++) { + Allowed[ci] = Masks[ci]; + Consumers[ci]->DropDenials(); + } +} + +template <class TRes> +inline +TConsumerIdx TScheduler<TRes>::Deactivate(TConsumerIdx ci, TMachineIdx mi) +{ + TMachineMask& mask = Masks[ci]; + if (!mask.Get(mi)) { + return ci; // Avoid double deactivation + } + + // Deactivate machine for consumer + mask.Reset(mi); + // NOTE: Deny is not required, because only (Allowed & Masks) matters and mask is reset + ActivePerMachine[mi]--; + + GLOBAL_LWPROBE(SHOP_PROVIDER, LazyDeactivate, + this->Name, Consumers[ci]->Name, + VirtualTime, BusyPeriod, + TRes::GetTag(Keys[ci]), mask.ToString(ShopState->MachineCount), + mi, ActivePerMachine[mi]); + + // Recursively deactivate machine in parent schedulers (if required) + if (this->GetScheduler() && ActivePerMachine[mi] == 0) { + TCon::Deactivate(mi); + } + + // Rearrange consumers to have completely deactivated (on every machine) in separate range + // (to be able to quickly iterate through them) + if (ci < Active && mask.IsZero()) { + Active--; + Swap(ci, Active); + ci = Active; + } + + return ci; +} + +template <class TRes> +void TScheduler<TRes>::AllowChild(TConsumerIdx ci, TMachineIdx mi) +{ + TMachineMask& allowed = Allowed[ci]; + if (allowed.Get(mi)) { + return; // Avoid double allow + } + + // Recursively allow machine in parent schedulers + this->Allow(mi); + + GLOBAL_LWPROBE(SHOP_PROVIDER, LazyAllow, + this->GetName(), Consumers[ci]->GetName(), + VirtualTime, BusyPeriod, + mi); + + allowed.Set(mi); +} + +template <class TRes> +void TScheduler<TRes>::Deny(TConsumerIdx ci, TMachineMask machines) +{ + GLOBAL_LWPROBE(SHOP_PROVIDER, LazyDeny, + this->GetName(), Consumers[ci]->GetName(), + VirtualTime, BusyPeriod, + machines.ToString(ShopState->MachineCount)); + + Allowed[ci].ResetAll(machines); +} + +template <class TRes> +inline +void TScheduler<TRes>::Detach(TConsumerIdx ci) +{ + // Completely deactivate consumer on every machine + for (TMachineIdx mi = 0; mi < ShopState->MachineCount; mi++) { + ci = Deactivate(ci, mi); + } + Y_ASSERT(ci >= Active); + + // Unlink consumer and scheduler + if (ci != Consumers.size() - 1) { // Consumer should be the last one to be detached + Swap(ci, Consumers.size() - 1); + } + Consumers.back()->Scheduler = nullptr; + Consumers.pop_back(); + Masks.pop_back(); + Allowed.pop_back(); + Keys.pop_back(); +} + + +/////////////////////////////////////////////////////////////////////////////// + +template <class TRes> +bool TRootScheduler<TRes>::IsRunnable() const +{ + return RootAllowed & this->GetShopState()->Warm; +} + +template <class TRes> +TSchedulable<typename TRes::TCost>* TRootScheduler<TRes>::PopSchedulable(const TCtx& ctx) +{ + if (TSchedulable<typename TRes::TCost>* schedulable = TScheduler<TRes>::PopSchedulable(ctx)) { + return schedulable; + } else { // Deny + GLOBAL_LWPROBE(SHOP_PROVIDER, LazyRootDeny, + this->GetName(), ctx.Warm.ToString(this->ShopState->MachineCount)); + RootAllowed.ResetAll(ctx.Warm); + return nullptr; + } +} + +template <class TRes> +void TRootScheduler<TRes>::Allow(TMachineIdx mi) +{ + if (RootAllowed.Get(mi)) { + return; // Avoid double allow + } + + GLOBAL_LWPROBE(SHOP_PROVIDER, LazyRootAllow, + this->GetName(), mi); + + RootAllowed.Set(mi); +} + +} +} diff --git a/ydb/library/shop/probes.cpp b/ydb/library/shop/probes.cpp new file mode 100644 index 00000000000..86fc2d5bd3e --- /dev/null +++ b/ydb/library/shop/probes.cpp @@ -0,0 +1,3 @@ +#include "probes.h" + +LWTRACE_DEFINE_PROVIDER(SHOP_PROVIDER) diff --git a/ydb/library/shop/probes.h b/ydb/library/shop/probes.h new file mode 100644 index 00000000000..3c6a1c31119 --- /dev/null +++ b/ydb/library/shop/probes.h @@ -0,0 +1,157 @@ +#pragma once + +#include <library/cpp/lwtrace/all.h> +#include <util/system/hp_timer.h> + +inline ui64 Duration(ui64 ts1, ui64 ts2) +{ + return ts2 > ts1? ts2 - ts1: 0; +} + +inline double CyclesToMs(ui64 cycles) +{ + return double(cycles) * 1e3 / NHPTimer::GetCyclesPerSecond(); +} + +#define SHOP_PROVIDER(PROBE, EVENT, GROUPS, TYPES, NAMES) \ + PROBE(Arrive, GROUPS("ShopFlowCtlOp"), \ + TYPES(TString, ui64, ui64, double, ui64, ui64, ui64, ui64, ui64), \ + NAMES("flowctl", "opId", "periodId", "now", "estCost", "usedCost", "countInFly", "costInFly", "window")) \ + PROBE(Depart, GROUPS("ShopFlowCtlOp", "ShopFlowCtlOpDepart"), \ + TYPES(TString, ui64, ui64, double, ui64, ui64, ui64, ui64, ui64, double, double, ui64), \ + NAMES("flowctl", "opId", "periodId", "now", "estCost", "usedCost", "countInFly", "costInFly", "realCost", "latencyMs", "weight", "window")) \ + PROBE(DepartLatency, GROUPS("ShopFlowCtlOpDepart"), \ + TYPES(TString, ui64, ui64, double, double, double, ui64, double), \ + NAMES("flowctl", "opId", "periodId", "now", "realLatencyMs", "latencyMAMs", "realCostMA", "arriveTime")) \ + PROBE(AdvanceTime, GROUPS(), \ + TYPES(TString, double, double), \ + NAMES("flowctl", "oldNow", "now")) \ + PROBE(Abort, GROUPS("ShopFlowCtlOp"), \ + TYPES(TString, ui64, ui64, double, ui64, ui64, ui64, ui64), \ + NAMES("flowctl", "opId", "periodId", "now", "estCost", "usedCost", "countInFly", "costInFly")) \ + PROBE(InitPeriod, GROUPS("ShopFlowCtlPeriod"), \ + TYPES(TString, ui64, double, ui64), \ + NAMES("flowctl", "periodId", "now", "window")) \ + PROBE(PeriodStats, GROUPS(), \ + TYPES(TString, ui64, double, ui64, double, double, double, double, double, double), \ + NAMES("flowctl", "periodId", "now", "statsPeriodId", "costSum", "costSumError", "weightSum", "weightSumError", "latencySum", "latencySumError")) \ + PROBE(PeriodGood, GROUPS(), \ + TYPES(TString, ui64, double, ui64, ui64, double, double, double, double), \ + NAMES("flowctl", "periodId", "now", "statsPeriodId", "goodPeriodIdx", "periodThroughput", "periodEThroughput", "periodLatencyAvgMs", "periodLatencyErrMs")) \ + PROBE(Measurement, GROUPS("ShopFlowCtlPeriod"), \ + TYPES(TString, ui64, double, ui64, ui64, ui64), \ + NAMES("flowctl", "periodId", "now", "badPeriods", "goodPeriods", "zeroPeriods")) \ + PROBE(Throughput, GROUPS("ShopFlowCtlPeriod"), \ + TYPES(TString, ui64, double, double, double, double, double, double, double), \ + NAMES("flowctl", "periodId", "now", "throughput", "throughputMin", "throughputMax", "throughputLo", "throughputHi", "dT_T")) \ + PROBE(EstThroughput, GROUPS("ShopFlowCtlPeriod"), \ + TYPES(TString, ui64, double, double, double, double, double, double, double), \ + NAMES("flowctl", "periodId", "now", "ethroughput", "ethroughputMin", "ethroughputMax", "ethroughputLo", "ethroughputHi", "dET_ET")) \ + PROBE(Latency, GROUPS("ShopFlowCtlPeriod"), \ + TYPES(TString, ui64, double, double, double, double, double, double, double), \ + NAMES("flowctl", "periodId", "now", "latencyAvgMs", "latencyAvgMinMs", "latencyAvgMaxMs", "latencyAvgLoMs", "latencyAvgHiMs", "dL_L")) \ + PROBE(Slowdown, GROUPS("ShopFlowCtlPeriod"), \ + TYPES(TString, ui64, double, ui64), \ + NAMES("flowctl", "periodId", "now", "waitSteady")) \ + PROBE(Control, GROUPS("ShopFlowCtlPeriod"), \ + TYPES(TString, ui64, double, double, double, double, double), \ + NAMES("flowctl", "periodId", "now", "pv", "error", "ratio", "target")) \ + PROBE(Window, GROUPS("ShopFlowCtlPeriod"), \ + TYPES(TString, ui64, double, ui64), \ + NAMES("flowctl", "periodId", "now", "window")) \ + PROBE(State, GROUPS("ShopFlowCtlPeriod"), \ + TYPES(TString, ui64, double, ui64, double), \ + NAMES("flowctl", "periodId", "now", "mode", "state")) \ + PROBE(TransitionClosed, GROUPS("FlowCtlTransition"), \ + TYPES(TString, double), \ + NAMES("flowctl", "now")) \ + PROBE(TransitionOpened, GROUPS("FlowCtlTransition"), \ + TYPES(TString, double), \ + NAMES("flowctl", "now")) \ + PROBE(Configure, GROUPS(), \ + TYPES(TString, double, TString), \ + NAMES("flowctl", "now", "config")) \ + \ + \ + \ + PROBE(StartJob, GROUPS("ShopJob"), \ + TYPES(TString, TString, ui64), \ + NAMES("shop", "flow", "job")) \ + PROBE(CancelJob, GROUPS("ShopJob"), \ + TYPES(TString, TString, ui64), \ + NAMES("shop", "flow", "job")) \ + PROBE(JobFinished, GROUPS("ShopJob"), \ + TYPES(TString, TString, ui64, double), \ + NAMES("shop", "flow", "job", "jobTimeMs")) \ + PROBE(StartOperation, GROUPS("ShopJob", "ShopOp"), \ + TYPES(TString, TString, ui64, ui64, ui64, ui64), \ + NAMES("shop", "flow", "job", "sid", "machineid", "estcost")) \ + PROBE(SkipOperation, GROUPS("ShopJob", "ShopOp"), \ + TYPES(TString, TString, ui64, ui64, ui64), \ + NAMES("shop", "flow", "job", "sid", "machineid")) \ + PROBE(OperationFinished, GROUPS("ShopJob", "ShopOp"), \ + TYPES(TString, TString, ui64, ui64, ui64, ui64, bool, double), \ + NAMES("shop", "flow", "job", "sid", "machineid", "realcost", "success", "procTimeMs")) \ + PROBE(NoMachine, GROUPS("ShopJob", "ShopOp"), \ + TYPES(TString, TString, ui64, ui64, ui64), \ + NAMES("shop", "flow", "job", "sid", "machineid")) \ + \ + \ + \ + PROBE(GlobalBusyPeriod, GROUPS("ShopScheduler"), \ + TYPES(TString, double, ui64, ui64, ui64, double, double, double), \ + NAMES("scheduler", "vtime", "busyPeriod", "busyPeriodPops", "busyPeriodCost", "idlePeriodDurationMs", "busyPeriodDurationMs", "utilization")) \ + PROBE(LocalBusyPeriod, GROUPS("ShopScheduler"), \ + TYPES(TString, double, ui64, ui64, ui64, double, double, double), \ + NAMES("scheduler", "vtime", "busyPeriod", "busyPeriodPops", "busyPeriodCost", "idlePeriodDurationMs", "busyPeriodDurationMs", "utilization")) \ + PROBE(PopSchedulable, GROUPS("ShopScheduler", "ShopFreezable", "ShopConsumer"), \ + TYPES(TString, TString, TString, double, ui64, double, ui64, i64, double, double), \ + NAMES("scheduler", "freezable", "consumer", "vtime", "busyPeriod", "finish", "cost", "underestimation", "weight", "vduration")) \ + PROBE(Activate, GROUPS("ShopScheduler", "ShopFreezable", "ShopConsumer"), \ + TYPES(TString, TString, TString, double, ui64, double, bool), \ + NAMES("scheduler", "freezable", "consumer", "vtime", "busyPeriod", "start", "frozen")) \ + PROBE(Deactivate, GROUPS("ShopScheduler", "ShopFreezable", "ShopConsumer"), \ + TYPES(TString, TString, TString, double, ui64, double, bool), \ + NAMES("scheduler", "freezable", "consumer", "vtime", "busyPeriod", "start", "frozen")) \ + PROBE(DeactivateImplicit, GROUPS("ShopScheduler", "ShopFreezable", "ShopConsumer"), \ + TYPES(TString, TString, TString, double, ui64, double, bool), \ + NAMES("scheduler", "freezable", "consumer", "vtime", "busyPeriod", "start", "frozen")) \ + PROBE(Freeze, GROUPS("ShopScheduler", "ShopFreezable"), \ + TYPES(TString, TString, double, ui64, ui64, ui64, double), \ + NAMES("scheduler", "freezable", "vtime", "busyPeriod", "freezableBusyPeriod", "frozenCount", "offset")) \ + PROBE(Unfreeze, GROUPS("ShopScheduler", "ShopFreezable"), \ + TYPES(TString, TString, double, ui64, ui64, ui64, double), \ + NAMES("scheduler", "freezable", "vtime", "busyPeriod", "freezableBusyPeriod", "frozenCount", "offset")) \ + \ + \ + \ + PROBE(LazyIdlePeriod, GROUPS("ShopLazyScheduler"), \ + TYPES(TString, double, ui64), \ + NAMES("scheduler", "vtime", "busyPeriod")) \ + PROBE(LazyWasted, GROUPS("ShopLazyScheduler"), \ + TYPES(TString, double, ui64, TString), \ + NAMES("scheduler", "vtime", "busyPeriod", "debugString")) \ + PROBE(LazyPopSchedulable, GROUPS("ShopLazyScheduler", "ShopLazyConsumer"), \ + TYPES(TString, TString, double, ui64, double, ui64, i64, double, double), \ + NAMES("scheduler", "consumer", "vtime", "busyPeriod", "finish", "cost", "underestimation", "weight", "vduration")) \ + PROBE(LazyActivate, GROUPS("ShopLazyScheduler", "ShopLazyConsumer"), \ + TYPES(TString, TString, double, ui64, double, TString, ui64, ui64), \ + NAMES("scheduler", "consumer", "vtime", "busyPeriod", "start", "mask", "machineIdx", "activePerMachine")) \ + PROBE(LazyDeactivate, GROUPS("ShopLazyScheduler", "ShopLazyConsumer"), \ + TYPES(TString, TString, double, ui64, double, TString, ui64, ui64), \ + NAMES("scheduler", "consumer", "vtime", "busyPeriod", "start", "mask", "machineIdx", "activePerMachine")) \ + PROBE(LazyDeny, GROUPS("ShopLazyScheduler", "ShopLazyConsumer"), \ + TYPES(TString, TString, double, ui64, TString), \ + NAMES("scheduler", "consumer", "vtime", "busyPeriod", "machines")) \ + PROBE(LazyAllow, GROUPS("ShopLazyScheduler", "ShopLazyConsumer"), \ + TYPES(TString, TString, double, ui64, ui64), \ + NAMES("scheduler", "consumer", "vtime", "busyPeriod", "machineIdx")) \ + PROBE(LazyRootDeny, GROUPS("ShopLazyScheduler"), \ + TYPES(TString, TString), \ + NAMES("scheduler", "machines")) \ + PROBE(LazyRootAllow, GROUPS("ShopLazyScheduler"), \ + TYPES(TString, ui64), \ + NAMES("scheduler", "machineIdx")) \ +/**/ + +LWTRACE_DECLARE_PROVIDER(SHOP_PROVIDER) diff --git a/ydb/library/shop/protos/library-shop-protos-sources.jar b/ydb/library/shop/protos/library-shop-protos-sources.jar new file mode 120000 index 00000000000..8263a11083f --- /dev/null +++ b/ydb/library/shop/protos/library-shop-protos-sources.jar @@ -0,0 +1 @@ +/home/hcpp/.ya/build/symres/5714ce2c5d23ea3b02c8b92a30c7a99c/library-shop-protos-sources.jar
\ No newline at end of file diff --git a/ydb/library/shop/protos/library-shop-protos.jar b/ydb/library/shop/protos/library-shop-protos.jar new file mode 120000 index 00000000000..1ce433596d4 --- /dev/null +++ b/ydb/library/shop/protos/library-shop-protos.jar @@ -0,0 +1 @@ +/home/hcpp/.ya/build/symres/129ac0f5aaf3e03e818dafe3713a8744/library-shop-protos.jar
\ No newline at end of file diff --git a/ydb/library/shop/protos/library-shop-protos.protosrc b/ydb/library/shop/protos/library-shop-protos.protosrc new file mode 120000 index 00000000000..2c1acf2d1f0 --- /dev/null +++ b/ydb/library/shop/protos/library-shop-protos.protosrc @@ -0,0 +1 @@ +/home/hcpp/.ya/build/symres/40a3e923e38f7d8fad119a1d90c0a787/library-shop-protos.protosrc
\ No newline at end of file diff --git a/ydb/library/shop/protos/shop.proto b/ydb/library/shop/protos/shop.proto new file mode 100644 index 00000000000..de26c548d64 --- /dev/null +++ b/ydb/library/shop/protos/shop.proto @@ -0,0 +1,47 @@ +package NShop; + +option java_package = "ru.yandex.shop.proto"; + +message TFlowCtlConfig { + // Time-related stuff + optional double PeriodDuration = 100 [default = 1.0]; // in seconds + optional uint64 HistorySize = 101 [default = 9]; // in periods + + // Measurements + optional double MeasurementError = 200 [default = 0.01]; // ratio (not percents) + optional double SteadyLimit = 201 [default = 0.5]; // ratio + optional double LatencyMACoef = 202 [default = 0.01]; // moving average coef (less=smooth) + optional double RealCostMACoef = 203 [default = 0.01]; // moving average coef (less=smooth) + + // Controller + optional bool Disabled = 299 [default = false]; // Has always open window if disabled + optional uint64 FixedWindow = 300 [default = 0]; // Just use const window iff>0 + optional double FixedLatencyMs = 301 [default = 0]; // Adjust window to contain queue of given length in milliseconds iff>0 + optional uint64 MinCountInFly = 302 [default = 10]; + optional uint64 MinCostInFly = 303 [default = 1]; + optional double MultiplierLimit = 304 [default = 0.05]; // Max window multiplier per period + optional uint64 SlowdownLimit = 305 [default = 5]; // Max periods in slowest controlling mode + optional uint64 SlowStartLimit = 306 [default = 30]; // Max periods in SlowStart controlling mode + optional double CostCapToWindow = 307 [default = 0.5]; // Cap for request cost devided by window +} + +message TProcCounters { + // Op or job result count + optional uint64 Done = 5; + optional uint64 Failed = 6; + optional uint64 Aborted = 7; // Op skip or job cancel + + // Op or job processing time + optional uint64 Time1ms = 20; + optional uint64 Time3ms = 21; + optional uint64 Time10ms = 22; + optional uint64 Time30ms = 23; + optional uint64 Time100ms = 24; + optional uint64 Time300ms = 25; + optional uint64 Time1000ms = 26; + optional uint64 Time3000ms = 27; + optional uint64 Time10000ms = 28; + optional uint64 Time30000ms = 29; + optional uint64 Time100000ms = 30; + optional uint64 TimeGt100000ms = 31; +} diff --git a/ydb/library/shop/protos/ya.make b/ydb/library/shop/protos/ya.make new file mode 100644 index 00000000000..ec7290c7814 --- /dev/null +++ b/ydb/library/shop/protos/ya.make @@ -0,0 +1,9 @@ +PROTO_LIBRARY() + +SRCS( + shop.proto +) + +EXCLUDE_TAGS(GO_PROTO) + +END() diff --git a/ydb/library/shop/resource.h b/ydb/library/shop/resource.h new file mode 100644 index 00000000000..199d2261f0a --- /dev/null +++ b/ydb/library/shop/resource.h @@ -0,0 +1,98 @@ +#pragma once + +#include <util/generic/utility.h> +#include <util/system/types.h> + +#include <limits> +#include <utility> + +namespace NShop { + +using TWeight = double; + +// Single resource max-min fairness allocation policy +template <class TFloat> +struct TSingleResourceTempl { + using TCost = i64; + using TTag = TFloat; + using TKey = TFloat; + + inline static TKey OffsetKey(TKey key, TTag offset) + { + return key + offset; + } + + template <class ConsumerT> + inline static void SetKey(TKey& key, ConsumerT* consumer) + { + key = consumer->GetTag(); + } + + inline static TTag GetTag(const TKey& key) + { + return key; + } + + inline static TKey MaxKey() + { + return std::numeric_limits<TKey>::max(); + } + + template <class ConsumerT> + inline static TKey ZeroKey(ConsumerT* consumer) + { + Y_UNUSED(consumer); + return 0; + } + + inline static void ActivateKey(TKey& key, TTag vtime) + { + key = Max(key, vtime); + } +}; + +using TSingleResource = TSingleResourceTempl<double>; +using TSingleResourceDense = TSingleResourceTempl<float>; // for lazy scheduler + +// Dominant resource fairness queueing (DRFQ) allocation policy +// with two resources and perfect dovetailing +struct TPairDrf { + using TCost = std::pair<i64, i64>; + using TTag = std::pair<double, double>; + using TKey = double; // dominant resource + + inline static TKey OffsetKey(TKey key, TTag offset) + { + return key + Dominant(offset); + } + + template <class ConsumerT> + inline static void SetKey(TKey& key, ConsumerT* consumer) + { + key = Dominant(consumer->GetTag()); + } + + inline static TKey Dominant(TTag tag) + { + return Max(tag.first, tag.second); + } + + inline static TKey MaxKey() + { + return std::numeric_limits<TKey>::max(); + } + + template <class ConsumerT> + inline static TKey ZeroKey(ConsumerT* consumer) + { + Y_UNUSED(consumer); + return 0; + } + + inline static void ActivateKey(TKey& key, TTag vtime) + { + key = Max(key, Dominant(vtime)); + } +}; + +} diff --git a/ydb/library/shop/schedulable.h b/ydb/library/shop/schedulable.h new file mode 100644 index 00000000000..b09058a4819 --- /dev/null +++ b/ydb/library/shop/schedulable.h @@ -0,0 +1,17 @@ +#pragma once + +#include "resource.h" + +#include <util/generic/ptr.h> + +namespace NShop { + +/////////////////////////////////////////////////////////////////////////////// + +template <class TCost> +struct TSchedulable: public virtual TThrRefBase { +public: + TCost Cost; +}; + +} diff --git a/ydb/library/shop/scheduler.h b/ydb/library/shop/scheduler.h new file mode 100644 index 00000000000..3ba0052b98d --- /dev/null +++ b/ydb/library/shop/scheduler.h @@ -0,0 +1,732 @@ +#pragma once + +#include "counters.h" +#include "probes.h" +#include "resource.h" +#include "schedulable.h" + +#include <util/generic/hash.h> +#include <util/generic/ptr.h> +#include <util/generic/set.h> +#include <util/generic/string.h> +#include <util/generic/utility.h> +#include <util/generic/vector.h> +#include <util/stream/output.h> +#include <util/system/types.h> +#include <util/system/yassert.h> + +#include <algorithm> + +namespace NShop { + +static constexpr i64 IsActive = -1; +static constexpr i64 IsDetached = -2; + +struct TConsumerCounters; +template <class TRes = TSingleResource> class TConsumer; +template <class TRes = TSingleResource> class TFreezable; +template <class TRes = TSingleResource> class TScheduler; + +/////////////////////////////////////////////////////////////////////////////// + +template <class TRes> +class TConsumer { +private: + using TCost = typename TRes::TCost; + using TTag = typename TRes::TTag; + using FreezableT = TFreezable<TRes>; + using SchedulerT = TScheduler<TRes>; + + // Configuration + TString Name; + TWeight Weight = 1; + SchedulerT* Scheduler = nullptr; + FreezableT* Freezable = nullptr; + + // Scheduling state + TTag HolTag = TTag(); // Tag (aka virtual time) of head-of-line schedulable + ui64 GlobalBusyPeriod = 0; + i64 DeactIdx = IsDetached; // Index of deactivated consumer in parent freezable (or special values) + TCost Underestimation = TCost(); // sum over all errors (realCost - estCost) to be fixed + + // Monitoring + TTag Consumed = TTag(); + static TConsumerCounters FakeCounters; + TConsumerCounters* Counters = &FakeCounters; + +public: + virtual ~TConsumer() + { + Detach(); + } + + void SetName(const TString& name) { Name = name; } + const TString& GetName() const { return Name; } + + void SetWeight(TWeight weight) { Y_ABORT_UNLESS(weight > 0); Weight = weight; } + TWeight GetWeight() const { return Weight; } + + void SetScheduler(SchedulerT* scheduler) { Scheduler = scheduler; } + SchedulerT* GetScheduler() const { return Scheduler; } + + void SetFreezable(FreezableT* freezable) { Freezable = freezable; } + FreezableT* GetFreezable() const { return Freezable; } + + void SetCounters(TConsumerCounters* counters) { Counters = counters; } + TConsumerCounters* GetCounters() { return Counters; } + + TTag GetTag() const { return HolTag; } + + virtual TSchedulable<typename TRes::TCost>* PopSchedulable() = 0; + virtual bool Empty() const = 0; + virtual void UpdateCounters() {} + + void Activate(); // NOTE: it also attaches consumer if it hasn't been done yet + void Deactivate(); + void Detach(); + void AddUnderestimation(TCost cost) { + Underestimation += cost; + } + + TTag ResetConsumed(); + void Print(IOutputStream& out) const; + + friend class TFreezable<TRes>; + friend class TScheduler<TRes>; +}; + +template <class TRes> +TConsumerCounters TConsumer<TRes>::FakeCounters; + +template <class TRes> +class TFreezable { +private: + using TCost = typename TRes::TCost; + using TTag = typename TRes::TTag; + using SchedulerT = TScheduler<TRes>; + using ConsumerT = TConsumer<TRes>; + +private: + struct THeapItem { + typename TRes::TKey Key; + ConsumerT* Consumer; + + explicit THeapItem(ConsumerT* consumer) + : Consumer(consumer) + { + UpdateKey(); + } + + void UpdateKey() + { + TRes::SetKey(Key, Consumer); + } + + bool operator<(const THeapItem& rhs) const + { + return rhs.Key < Key; // swapped for min-heap + } + }; + +private: + // Configuration + TString Name; + SchedulerT* Scheduler = nullptr; + + // Scheduling state + TVector<THeapItem> Heap; + i64 HeapEndIdx = 0; + bool Frozen = false; + TTag LastFreeze = TTag(); + TTag Offset = TTag(); + ui64 GlobalBusyPeriod = 0; +public: + void SetName(const TString& name) { Name = name; } + TString GetName() { return Name; } + + TSchedulable<typename TRes::TCost>* PopSchedulable(); + bool Empty() const; + + void SetScheduler(SchedulerT* scheduler) { Scheduler = scheduler; } + SchedulerT* GetScheduler() { return Scheduler; } + + // Activates consumers for which `predicate' holds true + template <class TPredicate> + size_t ActivateConsumers(TPredicate&& predicate); + + // Should be called before delete or just for detaching freezable + void Deactivate(); + + bool IsFrozen() const { return Frozen; } + void Freeze(); + void Unfreeze(); + + // Defines scheduling order of freezables + bool operator<(const TFreezable& o) const + { + Y_ASSERT(HeapEndIdx > 0); + Y_ASSERT(o.HeapEndIdx > 0); + Y_ASSERT(!Heap.empty()); + Y_ASSERT(!o.Heap.empty()); + return TRes::OffsetKey(Heap.front().Key, Offset) + < TRes::OffsetKey(o.Heap.front().Key, o.Offset); + } +private: + void Activate(ConsumerT* consumer); + void Insert(ConsumerT* consumer); + void Deactivate(ConsumerT* consumer); + void Detach(ConsumerT* consumer); + + friend class TScheduler<TRes>; + friend class TConsumer<TRes>; +}; + +template <class TRes> +class TScheduler: public TConsumer<TRes> { +private: + using TCost = typename TRes::TCost; + using TTag = typename TRes::TTag; + using FreezableT = TFreezable<TRes>; + using ConsumerT = TConsumer<TRes>; + +private: + struct TCmp { + bool operator()(FreezableT* lhs, FreezableT* rhs) const + { + return *lhs < *rhs; + } + }; + TVector<FreezableT*> Freezables; + ui64 FrozenCount = 0; + TTag VirtualTime = TTag(); + TTag LatestFinish = TTag(); + ui64 GlobalBusyPeriod = 0; + TCost GlobalBusyPeriodCost = TCost(); + ui64 GlobalBusyPeriodPops = 0; + ui64 GlobalPeriodTs = 0; // Idle or busy period start time (in cycles) + ui64 GlobalIdlePeriodDuration = ui64(-1); + ui64 LocalBusyPeriod = 0; + TCost LocalBusyPeriodCost = TCost(); + ui64 LocalBusyPeriodPops = 0; + ui64 LocalPeriodTs = 0; // Idle or busy period start time (in cycles) + ui64 LocalIdlePeriodDuration = ui64(-1); +public: + TSchedulable<typename TRes::TCost>* PopSchedulable() override; + bool Empty() const override; + void Clear(); + void UpdateCounters() override; + void Print(IOutputStream& out) const; + void DebugPrint(IOutputStream& out) const; +private: + void Activate(FreezableT* freezable); + void Deactivate(FreezableT* freezable); + void StartGlobalBusyPeriod(); + void StartGlobalIdlePeriod(); + void StartLocalBusyPeriod(); + void StartLocalIdlePeriod(); + + friend class TFreezable<TRes>; + friend class TConsumer<TRes>; +}; + +/////////////////////////////////////////////////////////////////////////////// + +template <class TRes> +inline +void TConsumer<TRes>::Activate() +{ + if (DeactIdx != IsActive) { // Avoid double activation + Freezable->Activate(this); + } +} + +template <class TRes> +inline +void TConsumer<TRes>::Deactivate() +{ + if (DeactIdx == IsActive) { + Freezable->Deactivate(this); + } +} + +template<class TRes> +void TConsumer<TRes>::Detach() +{ + if (DeactIdx == IsActive) { + Freezable->Deactivate(this); + Freezable->Detach(this); + } else if (DeactIdx != IsDetached) { + Freezable->Detach(this); + } + DeactIdx = IsDetached; +} + +template <class TRes> +inline +typename TRes::TTag TConsumer<TRes>::ResetConsumed() +{ + TTag result = Consumed; + Consumed = 0; + return result; +} + +template <class TRes> +inline +void TConsumer<TRes>::Print(IOutputStream& out) const +{ + out << "Weight = " << Weight << Endl + << "HolTag = " << HolTag << Endl + << "GlobalBusyPeriod = " << GlobalBusyPeriod << Endl + << "Borrowed = " << Counters->Borrowed << Endl + << "Donated = " << Counters->Donated << Endl + << "Usage = " << Counters->Usage << Endl + << "DeactIdx = " << DeactIdx << Endl; +} + +template <class TRes> +inline +TSchedulable<typename TRes::TCost>* TFreezable<TRes>::PopSchedulable() +{ + if (!Empty() && !Frozen) { + PopHeap(Heap.begin(), Heap.begin() + HeapEndIdx); + HeapEndIdx--; + THeapItem& item = Heap[HeapEndIdx]; + ConsumerT* consumer = item.Consumer; + + if (TSchedulable<typename TRes::TCost>* schedulable = consumer->PopSchedulable()) { + Scheduler->VirtualTime = consumer->HolTag + Offset; + + // Try to immediatly fix any discrepancy between real and estimated costs + // (as long as it doesn't lead to negative cost) + TCost cost = schedulable->Cost + consumer->Underestimation; + if (cost >= 0) { + consumer->Underestimation = 0; + } else { + // lower consumer overestimation by schedulable cost, and allow "free" usage + consumer->Underestimation = cost; + cost = 0; + } + TTag duration = cost / consumer->Weight; + consumer->HolTag += duration; + consumer->Consumed += duration; + Scheduler->LatestFinish = Max(Scheduler->LatestFinish, consumer->HolTag); + + if (consumer->Empty()) { + consumer->DeactIdx = HeapEndIdx; + } else { + item.UpdateKey(); + HeapEndIdx++; + PushHeap(Heap.begin(), Heap.begin() + HeapEndIdx); + } + + GLOBAL_LWPROBE(SHOP_PROVIDER, PopSchedulable, + Scheduler->GetName(), Name, consumer->Name, + Scheduler->VirtualTime, Scheduler->GlobalBusyPeriod, + consumer->HolTag + Offset, schedulable->Cost, + cost - schedulable->Cost, consumer->Weight, duration); + + return schedulable; + } else { + // Consumer turns to be inactive -- just deactivate it and repeat + GLOBAL_LWPROBE(SHOP_PROVIDER, DeactivateImplicit, + Scheduler->GetName(), Name, consumer->Name, + Scheduler->VirtualTime, Scheduler->GlobalBusyPeriod, + consumer->HolTag, Frozen); + consumer->DeactIdx = HeapEndIdx; + return nullptr; + } + } else { + return nullptr; + } +} + +template <class TRes> +inline +bool TFreezable<TRes>::Empty() const +{ + return HeapEndIdx == 0; +} + +template <class TRes> +template <class TPredicate> +inline +size_t TFreezable<TRes>::ActivateConsumers(TPredicate&& predicate) +{ + size_t prevHeapEndIdx = HeapEndIdx; + for (auto i = Heap.begin() + HeapEndIdx, e = Heap.end(); i != e; ++i) { + ConsumerT* consumer = i->Consumer; + if (predicate(consumer)) { // Consumer should be activated + Y_ASSERT(consumer->DeactIdx >= 0); + Insert(consumer); + } + } + size_t inserted = HeapEndIdx - prevHeapEndIdx; + if (prevHeapEndIdx == 0 && inserted > 0 && !Frozen) { + Scheduler->Activate(this); + } + return inserted; +} + +template <class TRes> +inline +void TFreezable<TRes>::Deactivate() +{ + if (Frozen) { + Unfreeze(); + } + if (!Empty()) { + Scheduler->Deactivate(this); + } +} + +template <class TRes> +inline +void TFreezable<TRes>::Freeze() +{ + Y_ASSERT(!Frozen); + Scheduler->FrozenCount++; + if (!Empty()) { + Scheduler->Deactivate(this); + } + Frozen = true; + LastFreeze = Scheduler->VirtualTime; + GLOBAL_LWPROBE(SHOP_PROVIDER, Freeze, + Scheduler->GetName(), Name, + Scheduler->VirtualTime, Scheduler->GlobalBusyPeriod, GlobalBusyPeriod, + Scheduler->FrozenCount, Offset); +} + +template <class TRes> +inline +void TFreezable<TRes>::Unfreeze() +{ + Y_ASSERT(Frozen); + Frozen = false; + Offset = Offset + Scheduler->VirtualTime - LastFreeze; + GLOBAL_LWPROBE(SHOP_PROVIDER, Unfreeze, + Scheduler->GetName(), Name, + Scheduler->VirtualTime, Scheduler->GlobalBusyPeriod, GlobalBusyPeriod, + Scheduler->FrozenCount, Offset); + if (!Empty()) { + Scheduler->Activate(this); + Scheduler->FrozenCount--; + } else { + Scheduler->FrozenCount--; + if (Scheduler->Empty()) { + Scheduler->StartGlobalIdlePeriod(); + } + } +} + +template <class TRes> +inline +void TFreezable<TRes>::Activate(ConsumerT* consumer) +{ + bool wasEmpty = Empty(); + Insert(consumer); + if (!Frozen && wasEmpty) { + Scheduler->Activate(this); + } +} + +template <class TRes> +inline +void TFreezable<TRes>::Insert(ConsumerT* consumer) +{ + // Update consumer's tag + if (consumer->GlobalBusyPeriod != Scheduler->GlobalBusyPeriod) { + consumer->GlobalBusyPeriod = Scheduler->GlobalBusyPeriod; + consumer->HolTag = TTag(); + // Estimation errors of pervious busy period does not matter any more + consumer->Underestimation = TCost(); + if (GlobalBusyPeriod != Scheduler->GlobalBusyPeriod) { + GlobalBusyPeriod = Scheduler->GlobalBusyPeriod; + Offset = TTag(); + } + } + TTag vtime = (Frozen? LastFreeze: Scheduler->VirtualTime); + TTag selfvtime = vtime - Offset; + consumer->HolTag = Max(consumer->HolTag, selfvtime); + + // Insert consumer into attached vector (if detached) + Y_ASSERT(consumer->DeactIdx != IsActive); + if (consumer->DeactIdx == IsDetached) { + consumer->DeactIdx = Heap.size(); + Heap.emplace_back(consumer); + } + + // Swap consumer in place to push into heap + i64 deactIdx = consumer->DeactIdx; + THeapItem& place = Heap[HeapEndIdx]; + if (deactIdx != HeapEndIdx) { + THeapItem& oldItem = Heap[deactIdx]; + DoSwap(place, oldItem); + oldItem.Consumer->DeactIdx = deactIdx; + } + place.UpdateKey(); + + // Push into active consumers heap + HeapEndIdx++; + PushHeap(Heap.begin(), Heap.begin() + HeapEndIdx); + consumer->DeactIdx = IsActive; + GLOBAL_LWPROBE(SHOP_PROVIDER, Activate, + Scheduler->GetName(), Name, consumer->Name, + Scheduler->VirtualTime, Scheduler->GlobalBusyPeriod, + consumer->HolTag, Frozen); +} + +template <class TRes> +inline +void TFreezable<TRes>::Deactivate(ConsumerT* consumer) +{ + GLOBAL_LWPROBE(SHOP_PROVIDER, Deactivate, + Scheduler->GetName(), Name, consumer->Name, + Scheduler->VirtualTime, Scheduler->GlobalBusyPeriod, + consumer->HolTag, Frozen); + + Y_ASSERT(consumer->DeactIdx == IsActive); + for (auto i = Heap.begin(), e = Heap.begin() + HeapEndIdx; i != e; ++i) { + if (i->Consumer == consumer) { + HeapEndIdx--; + i64 idx = i - Heap.begin(); + if (HeapEndIdx != idx) { + DoSwap(Heap[HeapEndIdx], Heap[idx]); + } + consumer->DeactIdx = HeapEndIdx; + if (!Empty()) { + MakeHeap(Heap.begin(), Heap.begin() + HeapEndIdx); + } else { + Scheduler->Deactivate(this); + } + return; + } + } + Y_ABORT_UNLESS("trying to deactivate unknown consumer"); +} + +template <class TRes> +inline +void TFreezable<TRes>::Detach(ConsumerT* consumer) +{ + Y_ASSERT(consumer->DeactIdx >= 0); + i64 oldIdx = consumer->DeactIdx; + i64 newIdx = Heap.size() - 1; + if (oldIdx != newIdx) { + DoSwap(Heap[oldIdx], Heap[newIdx]); + Heap[oldIdx].Consumer->DeactIdx = oldIdx; + } + Heap.pop_back(); +} + +template <class TRes> +inline +TSchedulable<typename TRes::TCost>* TScheduler<TRes>::PopSchedulable() +{ + while (!Empty()) { + auto iter = std::min_element(Freezables.begin(), Freezables.end(), TCmp()); + + FreezableT* freezable = *iter; + + TSchedulable<typename TRes::TCost>* schedulable = freezable->PopSchedulable(); + if (schedulable) { + GlobalBusyPeriodCost = GlobalBusyPeriodCost + schedulable->Cost; + GlobalBusyPeriodPops++; + LocalBusyPeriodCost = LocalBusyPeriodCost + schedulable->Cost; + LocalBusyPeriodPops++; + } + + if (freezable->Empty()) { + if (Freezables.size() > 1) { + DoSwap(*iter, Freezables.back()); + Freezables.pop_back(); + } else { + Freezables.pop_back(); + StartGlobalIdlePeriod(); + StartLocalIdlePeriod(); + if (this->GetFreezable()) { // Deactivate parent if it is not root scheduler + TConsumer<TRes>::Deactivate(); + } + } + } + + if (schedulable) { + return schedulable; + } + } + return nullptr; +} + +template <class TRes> +inline +bool TScheduler<TRes>::Empty() const +{ + return Freezables.empty(); +} + +template <class TRes> +inline +void TScheduler<TRes>::Clear() +{ + TVector<ConsumerT*> consumers; + for (FreezableT* freezable : Freezables) { + for (auto item : freezable->Heap) { + // Delay modification of Freezables (we are iterating over it) + consumers.push_back(item.Consumer); + } + } + for (ConsumerT* consumer : consumers) { + consumer->Detach(); + } + Y_ASSERT(Freezables.empty()); +} + +template <class TRes> +inline +void TScheduler<TRes>::UpdateCounters() +{ + TCountersAggregator<TConsumer<TRes>, TTag> aggr; + for (FreezableT* freezable : Freezables) { + for (auto item : freezable->Heap) { + aggr.Add(item.Consumer); + item.Consumer->UpdateCounters(); // Recurse into children + } + } + aggr.Apply(); +} + +template <class TRes> +inline +void TScheduler<TRes>::Print(IOutputStream& out) const +{ + out << "FrozenCount = " << FrozenCount << Endl + << "VirtualTime = " << VirtualTime << Endl + << "LatestFinish = " << LatestFinish << Endl + << "GlobalBusyPeriod = " << GlobalBusyPeriod << Endl + << "GlobalBusyPeriodCost = " << GlobalBusyPeriodCost << Endl + << "GlobalBusyPeriodPops = " << GlobalBusyPeriodPops << Endl + << "GlobalPeriodTs = " << GlobalPeriodTs << Endl + << "GlobalIdlePeriodDuration = " << GlobalIdlePeriodDuration << Endl + << "LocalBusyPeriod = " << LocalBusyPeriod << Endl + << "LocalBusyPeriodCost = " << LocalBusyPeriodCost << Endl + << "LocalBusyPeriodPops = " << LocalBusyPeriodPops << Endl + << "LocalPeriodTs = " << LocalPeriodTs << Endl + << "LocalIdlePeriodDuration = " << LocalIdlePeriodDuration << Endl; + if (this->GetFreezable()) { + TConsumer<TRes>::Print(out); + } +} + +template <class TRes> +void TScheduler<TRes>::DebugPrint(IOutputStream& out) const +{ + for (const auto& freezable : Freezables) { + i64 idx = 0; + for (const auto& item: freezable->Heap) { + out << (idx < freezable->HeapEndIdx? "* ": " ") + << "key:" << item.Key + << " tag:" << item.Consumer->HolTag + << " didx:" << item.Consumer->DeactIdx << Endl; + idx++; + } + } + out << Endl; +} + +template <class TRes> +inline +void TScheduler<TRes>::Activate(FreezableT* freezable) +{ + Y_ASSERT(!freezable->Frozen); + bool wasEmpty = Empty(); + Freezables.push_back(freezable); + if (wasEmpty) { + StartGlobalBusyPeriod(); + StartLocalBusyPeriod(); + if (this->GetFreezable()) { // Activate parent if it is not root scheduler + TConsumer<TRes>::Activate(); + } + } +} + +template <class TRes> +inline +void TScheduler<TRes>::Deactivate(FreezableT* freezable) +{ + Y_ASSERT(!freezable->Frozen); + Freezables.erase(std::remove(Freezables.begin(), Freezables.end(), freezable), Freezables.end()); + if (Empty()) { + StartGlobalIdlePeriod(); + StartLocalIdlePeriod(); + if (this->GetFreezable()) { // Deactivate parent if it is not root scheduler + TConsumer<TRes>::Deactivate(); + } + } +} + +template <class TRes> +inline +void TScheduler<TRes>::StartGlobalBusyPeriod() +{ + if (FrozenCount == 0) { + Y_ASSERT(GlobalIdlePeriodDuration == ui64(-1)); + ui64 now = GetCycleCount(); + GlobalIdlePeriodDuration = GlobalPeriodTs? Duration(GlobalPeriodTs, now): 0; + GlobalPeriodTs = now; + } +} + +template <class TRes> +inline +void TScheduler<TRes>::StartGlobalIdlePeriod() +{ + if (FrozenCount == 0 && GlobalIdlePeriodDuration != ui64(-1)) { + ui64 now = GetCycleCount(); + ui64 busyPeriodDuration = Duration(GlobalPeriodTs, now); + GlobalPeriodTs = now; + GLOBAL_LWPROBE(SHOP_PROVIDER, GlobalBusyPeriod, this->GetName(), + VirtualTime, GlobalBusyPeriod, + GlobalBusyPeriodPops, GlobalBusyPeriodCost, + CyclesToMs(GlobalIdlePeriodDuration), CyclesToMs(busyPeriodDuration), + double(busyPeriodDuration) / (GlobalIdlePeriodDuration + busyPeriodDuration)); + GlobalBusyPeriod++; + GlobalBusyPeriodCost = TCost(); + GlobalBusyPeriodPops = 0; + GlobalIdlePeriodDuration = ui64(-1); + + VirtualTime = TTag(); + LatestFinish = TTag(); + } +} + +template <class TRes> +inline +void TScheduler<TRes>::StartLocalBusyPeriod() +{ + Y_ASSERT(LocalIdlePeriodDuration == ui64(-1)); + ui64 now = GetCycleCount(); + LocalIdlePeriodDuration = LocalPeriodTs? Duration(LocalPeriodTs, now): 0; + LocalPeriodTs = now; +} + +template <class TRes> +inline +void TScheduler<TRes>::StartLocalIdlePeriod() +{ + Y_ASSERT(LocalIdlePeriodDuration != ui64(-1)); + ui64 now = GetCycleCount(); + ui64 busyPeriodDuration = Duration(LocalPeriodTs, now); + LocalPeriodTs = now; + GLOBAL_LWPROBE(SHOP_PROVIDER, LocalBusyPeriod, this->GetName(), + VirtualTime, LocalBusyPeriod, + LocalBusyPeriodPops, LocalBusyPeriodCost, + CyclesToMs(LocalIdlePeriodDuration), CyclesToMs(busyPeriodDuration), + double(busyPeriodDuration) / (LocalIdlePeriodDuration + busyPeriodDuration)); + LocalBusyPeriod++; + LocalBusyPeriodCost = TCost(); + LocalBusyPeriodPops = 0; + LocalIdlePeriodDuration = ui64(-1); + + VirtualTime = LatestFinish; +} + +} diff --git a/ydb/library/shop/shop.cpp b/ydb/library/shop/shop.cpp new file mode 100644 index 00000000000..528e713ad1d --- /dev/null +++ b/ydb/library/shop/shop.cpp @@ -0,0 +1,363 @@ +#include "shop.h" +#include "probes.h" + +#include <util/generic/utility.h> +#include <util/system/yassert.h> + +namespace NShop { + +LWTRACE_USING(SHOP_PROVIDER); + +TOp::TOp(const TStage* stage) + : DepsLeft(stage->Depends.size()) +{} + +const char* TOp::StateName() const +{ + switch (State) { + case EState::Wait: return "Wait"; + case EState::Proc: return "Proc"; + case EState::Done: return "Done"; + case EState::Fail: return "Fail"; + case EState::Skip: return "Skip"; + } + Y_ABORT("Unexpected"); +} + +size_t TFlow::AddStage(std::initializer_list<size_t> depends) +{ + Stage.emplace_back(); + size_t sid = Stage.size() - 1; + TStage& desc = Stage.back(); + desc.MachineId = sid; + desc.Depends.reserve(depends.size()); + for (size_t idx : depends) { + desc.Depends.push_back(idx); + Y_ABORT_UNLESS(idx < Stage.size()); + Stage[idx].Blocks.push_back(sid); + } + return sid; +} + +size_t TFlow::AddStage(ui64 machineId, std::initializer_list<size_t> depends) +{ + ui64 sid = AddStage(depends); + Stage[sid].MachineId = machineId; + return sid; +} + +TString TFlow::DebugDump() const +{ + TStringStream ss; + ss << "=== TFlow ===" << Endl; + ss << "Name:" << Name << Endl; + size_t sid = 0; + for (const TStage& stage : Stage) { + ss << sid++ << ") Name:" << stage.Name + << " MachineId:" << stage.MachineId + << " Blocks:["; + for (auto x : stage.Blocks) { + ss << " " << x; + } + ss << " ] Depends:["; + for (auto x : stage.Depends) { + ss << " " << x; + } + ss << " ]" << Endl; + } + return ss.Str(); +} + +void TJob::AddOp(ui64 estcost) +{ + Y_ABORT_UNLESS(Op.size() < Flow->StageCount()); + Op.emplace_back(Flow->GetStage(Op.size())); + TOp& op = Op.back(); + op.EstCost = estcost; +} + +TString TJob::DebugDump() const +{ + TStringStream ss; + if (Flow) { + ss << Flow->DebugDump() << Endl; + } else { + ss << "Flow:nullptr" << Endl; + } + ss << "=== TJob ===" << Endl; + ss << "JobId:" << JobId << Endl; + ss << "OpsLeft:" << OpsLeft << Endl; + ss << "OpsInFly:" << OpsInFly << Endl; + ss << "Failed:" << Failed << Endl; + ss << "StartTs:" << StartTs << Endl; + size_t sid = 0; + for (const TOp& op : Op) { + ss << sid++ << ")" + << " DepsLeft:" << op.DepsLeft + << " StartTs:" << op.StartTs + << " State:" << op.StateName() + << Endl; + } + return ss.Str(); +} + +void TShop::StartJob(TJob* job, double now) +{ + Y_ABORT_UNLESS(job->Flow); + Y_ABORT_UNLESS(job->Op.size() == job->Flow->StageCount(), "opSize:%lu != flowSize:%lu", + job->Op.size(), job->Flow->StageCount()); + + job->JobId = ++LastJobId; + job->OpsLeft = job->Op.size(); + job->StartTs = GetCycleCount(); + LWPROBE(StartJob, Name, job->Flow->Name, job->JobId); + + RunOperation(job, 0, now); +} + +void TShop::CancelJob(TJob* job) +{ + job->Failed = true; + job->Canceled = true; + LWPROBE(CancelJob, Name, job->Flow->Name, job->JobId); +} + +void TShop::JobFinished(TJob* job) +{ + job->FinishTs = GetCycleCount(); + job->Duration = Duration(job->StartTs, job->FinishTs); + LWPROBE(JobFinished, Name, job->Flow->Name, job->JobId, CyclesToMs(job->Duration)); +} + +void TShop::SkipOperation(TJob* job, size_t sid) +{ + // Mark operation to be skipped + job->GetOp(sid)->State = TOp::EState::Skip; + LWPROBE(SkipOperation, Name, job->Flow->Name, job->JobId, sid, + job->Flow->GetStage(sid)->MachineId); + OnOperationAbort(job, sid); +} + +// Returns true if job has been finished +bool TShop::OperationFinished(TJob* job, size_t sid, ui64 realcost, bool success, double now) +{ + // Monitoring + TOp* op = job->GetOp(sid); + op->FinishTs = GetCycleCount(); + op->Duration = Duration(op->StartTs, op->FinishTs); + + // Change state + job->OpsInFly--; + job->OpsLeft--; + LWPROBE(OperationFinished, Name, job->Flow->Name, job->JobId, sid, + job->Flow->GetStage(sid)->MachineId, + realcost, success, CyclesToMs(op->Duration)); + if (op->State != TOp::EState::Skip) { + Y_ABORT_UNLESS(op->State == TOp::EState::Proc); + op->State = success? TOp::EState::Done : TOp::EState::Fail; + op->Machine->Count(op); + if (!success) { + job->Failed = true; + } + OnOperationFinished(job, sid, realcost, now); + } else { + op->Machine->Count(op); + } + + if (!job->Failed) { + if (job->OpsLeft == 0) { + JobFinished(job); + return true; + } else { + // Run dependent operations + for (size_t i : job->Flow->GetStage(sid)->Blocks) { + if (--job->GetOp(i)->DepsLeft == 0) { + if (RunOperation(job, i, now)) { + return true; + } + } + } + } + } else { + // Finish failed/canceled job immediately if all in fly ops are finished + // and don't start any other operations for this job + if (job->OpsInFly == 0) { + for (size_t op2Idx = 0; op2Idx < job->Op.size(); op2Idx++) { + if (job->GetOp(op2Idx)->State == TOp::EState::Wait) { + OnOperationAbort(job, op2Idx); + } + } + JobFinished(job); + return true; + } else { + return false; // In fly ops remain -- wait for them to finish + } + } + Y_ABORT_UNLESS(job->OpsInFly > 0, "no more ops in flight\n%s", job->DebugDump().data()); + return false; +} + +// Returns true if job has been finished (3 reasons: skip/cancel/done) +bool TShop::RunOperation(TJob* job, size_t sid, double now) +{ + job->OpsInFly++; + + Y_ABORT_UNLESS(job->Flow); + TOp::EState state = job->GetOp(sid)->State; + if (state != TOp::EState::Skip) { + Y_ABORT_UNLESS(job->GetOp(sid)->State == TOp::EState::Wait); + job->GetOp(sid)->State = TOp::EState::Proc; + if (TMachine* machine = GetMachine(job, sid)) { + return machine->StartOperation(job, sid); + } else { + // Required machine is not available; job failed + LWPROBE(NoMachine, Name, job->Flow->Name, job->JobId, sid, + job->Flow->GetStage(sid)->MachineId); + return OperationFinished(job, sid, 0, false, now); + } + } else { + return OperationFinished(job, sid, 0, true, now); + } +} + +TMachine::TMachine(TShop* shop) + : Shop(shop) +{} + +bool TMachine::StartOperation(TJob* job, size_t sid) +{ + LWPROBE(StartOperation, Shop->GetName(), job->Flow->GetName(), job->JobId, sid, + job->Flow->GetStage(sid)->MachineId, + job->GetOp(sid)->EstCost); + TOp* op = job->GetOp(sid); + op->StartTs = GetCycleCount(); + op->Machine = this; + return false; +} + +void TMachine::Count(const TOp* op) +{ + if (Counters) { + Counters->Count(op); + } +} + +void TProcMonCounters::Count(const TOp* op) +{ +#define INC_COUNTER(name) Counters.Set##name(Counters.Get##name() + 1) + if (op->State == TOp::EState::Done) { + INC_COUNTER(Done); + } else if (op->State == TOp::EState::Fail) { + INC_COUNTER(Failed); + } else if (op->State == TOp::EState::Skip) { + INC_COUNTER(Aborted); + } + double timeMs = CyclesToMs(op->Duration); + if (timeMs <= 30.0) { + if (timeMs <= 3.0) { + if (timeMs <= 1.0) { + INC_COUNTER(Time1ms); + } else { + INC_COUNTER(Time3ms); + } + } else { + if (timeMs <= 10.0) { + INC_COUNTER(Time10ms); + } else { + INC_COUNTER(Time30ms); + } + } + } else if (timeMs <= 3000.0) { + if (timeMs <= 300.0) { + if (timeMs <= 100.0) { + INC_COUNTER(Time100ms); + } else { + INC_COUNTER(Time300ms); + } + } else { + if (timeMs <= 1000.0) { + INC_COUNTER(Time1000ms); + } else { + INC_COUNTER(Time3000ms); + } + } + } else { + if (timeMs <= 30000.0) { + if (timeMs <= 10000.0) { + INC_COUNTER(Time10000ms); + } else { + INC_COUNTER(Time30000ms); + } + } else { + if (timeMs <= 100000.0) { + INC_COUNTER(Time100000ms); + } else { + INC_COUNTER(TimeGt100000ms); + } + } + } +#undef INC_COUNTER +} + +void TMachineCtl::Transition(TFlowCtl::EStateTransition st) +{ + switch (st) { + case TFlowCtl::None: break; + case TFlowCtl::Closed: Freeze(); break; + case TFlowCtl::Opened: Unfreeze(); break; + } +} + +void TShopCtl::Configure(TMachineCtl* ctl, const TFlowCtlConfig& cfg, double now) +{ + ctl->Transition(ctl->GetFlowCtl()->ConfigureST(cfg, now)); +} + +void TShopCtl::ArriveJob(TJob* job, double now) +{ + // Flow control arrive for all operations + for (size_t sid = 0; sid < job->Op.size(); sid++) { + ArriveJobStage(job, sid, now); + } +} + +void TShopCtl::ArriveJobStage(TJob* job, size_t sid, double now) +{ + TOp* op = job->GetOp(sid); + if (TMachineCtl* ctl = GetMachineCtl(job, sid)) { + ctl->Transition(ctl->GetFlowCtl()->ArriveST(*op, op->EstCost, now)); + } else { + // OpId == 0 is marker for disabled flow control + Y_ABORT_UNLESS(op->OpId == 0, "operation not marked with OpId=0"); + } +} + +void TShopCtl::DepartJobStage(TJob* job, size_t sid, ui64 realcost, double now) +{ + if (TMachineCtl* ctl = GetMachineCtl(job, sid)) { + Depart(ctl, job->GetOp(sid), realcost, now); + } +} + +void TShopCtl::Depart(TMachineCtl* ctl, TFcOp* op, ui64 realcost, double now) +{ + if (op->HasArrived()) { + ctl->Transition(ctl->GetFlowCtl()->DepartST(*op, realcost, now)); + } +} + +void TShopCtl::AbortJobStage(TJob* job, size_t sid) +{ + if (TMachineCtl* ctl = GetMachineCtl(job, sid)) { + Abort(ctl, job->GetOp(sid)); + } +} + +void TShopCtl::Abort(TMachineCtl* ctl, TFcOp* op) +{ + if (op->HasArrived()) { + ctl->Transition(ctl->GetFlowCtl()->AbortST(*op)); + } +} + +} diff --git a/ydb/library/shop/shop.h b/ydb/library/shop/shop.h new file mode 100644 index 00000000000..151e7b863f4 --- /dev/null +++ b/ydb/library/shop/shop.h @@ -0,0 +1,195 @@ +#pragma once + +#include "flowctl.h" + +#include <util/generic/string.h> +#include <util/generic/vector.h> +#include <util/generic/ptr.h> +#include <util/system/types.h> + +#include <library/cpp/containers/stack_vector/stack_vec.h> + +namespace NShop { + +struct TStage; +class TFlow; +struct TOp; +struct TJob; +class TMachine; +class TShop; + +struct TStage { + ui64 MachineId; + TString Name; + TStackVec<size_t, 16> Depends; // Idx of stages it depends on + TStackVec<size_t, 16> Blocks; // Idx of stages dependent on it +}; + +class TFlow { +private: + TStackVec<TStage, 16> Stage; // Topologicaly sorted DAG of stages + TString Name; +public: + virtual ~TFlow() {} + + size_t AddStage(std::initializer_list<size_t> depends); + size_t AddStage(ui64 machineId, std::initializer_list<size_t> depends); + + // Accessors + void SetName(const TString& name) { Name = name; } + TString GetName() const { return Name; } + const TStage* GetStage(size_t idx) const { return &Stage[idx]; } + size_t StageCount() const { return Stage.size(); } + + TString DebugDump() const; + + friend class TShop; +}; + +struct TOp: public TFcOp { + enum class EState : ui8 { + Wait = 0, // Awaits it's dependencies to be done + Proc = 1, // Is processing on machine + Done = 2, // Already processed + Fail = 3, // Already processed, but unsuccessfully + Skip = 4, // Not going to be processed + }; + + TMachine* Machine = nullptr; // Machine assigned for processing + + EState State = EState::Wait; + ui64 DepsLeft = 0; // Count of blocker operation left to be done + + ui64 StartTs = 0; // in cycles + ui64 FinishTs = 0; // in cycles + ui64 Duration = 0; // in cycles + + TOp() {} + explicit TOp(const TStage* stage); + + const char* StateName() const; +}; + +struct TJob { + TFlow* Flow = nullptr; + TStackVec<TOp, 16> Op; + ui64 JobId = 0; + ui64 OpsLeft = 0; + ui64 OpsInFly = 0; + bool Failed = false; + bool Canceled = false; + + ui64 StartTs = 0; // in cycles + ui64 FinishTs = 0; // in cycles + ui64 Duration = 0; // in cycles + + void AddOp(ui64 estcost); + TOp* GetOp(size_t idx) { return &Op[idx]; } + const TOp* GetOp(size_t idx) const { return &Op[idx]; } + + TString DebugDump() const; +}; + +class IMonCounters { +public: + virtual ~IMonCounters() {} + virtual void Count(const TOp* op) = 0; +}; + +class TProcMonCounters : public IMonCounters { +private: + TProcCounters Counters; +public: + void Count(const TOp* op) override; + const TProcCounters& GetCounters() const { return Counters; } +}; + +class TMachine { +private: + TShop* Shop; + TString Name; + THolder<IMonCounters> Counters; +public: + explicit TMachine(TShop* shop); + virtual ~TMachine() {} + + TShop* GetShop() { return Shop; } + void SetName(const TString& name) { Name = name; } + TString GetName() const { return Name; } + void SetCounters(IMonCounters* counters) { Counters.Reset(counters); } + + // Execute sync or start async operation processing. + // TShop::OperationFinished() must be called on finish + // Returns: + // - result of sync-called OperationFinished() + // - false in case of async operation + virtual bool StartOperation(TJob* job, size_t sid); + + // Monitoring + void Count(const TOp* op); +}; + +class TShop { +private: + ui64 LastJobId = 0; + TString Name; +public: + virtual ~TShop() {} + + void SetName(const TString& name) { Name = name; } + TString GetName() const { return Name; } + + // Machines + virtual TMachine* GetMachine(const TJob* job, size_t sid) = 0; + + // Jobs + void StartJob(TJob* job, double now); + void CancelJob(TJob* job); + virtual void JobFinished(TJob* job); + + // Operations + void SkipOperation(TJob* job, size_t sid); + bool OperationFinished(TJob* job, size_t sid, ui64 realcost, bool success, double now); + +protected: + virtual void OnOperationFinished(TJob* job, size_t sid, ui64 realcost, double now) = 0; + virtual void OnOperationAbort(TJob* job, size_t sid) = 0; + +private: + bool RunOperation(TJob* job, size_t sid, double now); +}; + +class TMachineCtl { +private: + TFlowCtlPtr FlowCtl; +public: + explicit TMachineCtl(TFlowCtl* flowCtl) + : FlowCtl(flowCtl) + {} + + TFlowCtl* GetFlowCtl() { return FlowCtl.Get(); } + + void Transition(TFlowCtl::EStateTransition st); + + // Flow control callbacks (e.g. stop/start incoming job flows in scheduler) + virtual void Freeze() = 0; + virtual void Unfreeze() = 0; +}; + +class TShopCtl { +public: + void Configure(TMachineCtl* ctl, const TFlowCtlConfig& cfg, double now); + + virtual TMachineCtl* GetMachineCtl(const TJob* job, size_t sid) = 0; + + void ArriveJob(TJob* job, double now); + void DepartJobStage(TJob* job, size_t sid, ui64 realcost, double now); + void Depart(TMachineCtl* ctl, TFcOp* op, ui64 realcost, double now); + void AbortJobStage(TJob* job, size_t sid); + void Abort(TMachineCtl* ctl, TFcOp* op); + +private: + void ArriveJobStage(TJob* job, size_t sid, double now); +}; + +} diff --git a/ydb/library/shop/shop_state.h b/ydb/library/shop/shop_state.h new file mode 100644 index 00000000000..66807a173a1 --- /dev/null +++ b/ydb/library/shop/shop_state.h @@ -0,0 +1,122 @@ +#pragma once + +#include <util/system/types.h> +#include <util/system/compiler.h> + +namespace NShop { + +using TMachineIdx = ui8; + +/////////////////////////////////////////////////////////////////////////////// + +struct TMachineMask { + using TIdx = TMachineIdx; + using TMask = ui64; + constexpr static TMask One = TMask(1); + + TMask Mask; + + TMachineMask(TMask mask = 0) + : Mask(mask) + {} + + operator TMask() const + { + return Mask; + } + + void Set(TIdx idx) + { + Mask = Mask | (One << idx); + } + + void SetAll(TMask mask) + { + Mask = Mask | mask; + } + + void Reset(TIdx idx) + { + Mask = Mask & ~(One << idx) ; + } + + void ResetAll(TMask mask) + { + Mask = Mask & ~mask; + } + + bool Get(TIdx idx) const + { + return Mask & (One << idx); + } + + bool IsZero() const + { + return Mask == 0; + } + + TString ToString(TIdx size) const + { + TStringStream ss; + for (TIdx idx = 0; idx < size; idx++) { + if (idx % 8 == 0 && idx) { + ss << '-'; // bytes separator + } + ss << (Get(idx)? '1': '0'); + } + return ss.Str(); + } +}; + +/////////////////////////////////////////////////////////////////////////////// + +struct TShopState { + TMachineMask Warm; // machineIdx -> (1=warm | 0=frozen) + TMachineIdx MachineCount; + static constexpr TMachineIdx MaxMachines = 64; // we have only 64 bits, it must be enough for now + + TShopState(TMachineIdx machineCount = 1) + : Warm((TMachineMask::One << machineCount) - 1) // all machines are warm + , MachineCount(machineCount) + {} + + void Freeze(TMachineIdx idx) + { + Y_ASSERT(idx < MachineCount); + Warm.Reset(idx); + } + + void Unfreeze(TMachineIdx idx) + { + Y_ASSERT(idx < MachineCount); + Warm.Set(idx); + } + + void AddMachine() + { + Warm.Set(MachineCount); // New machine is warm at start + MachineCount++; + } + + bool AllFrozen() const + { + return Warm.Mask == 0; + } + + bool HasFrozen() const + { + if (Y_LIKELY(MachineCount < 8*sizeof(TMachineMask::TMask))) { + return Warm.Mask != ((TMachineMask::One << MachineCount) - 1); + } else { + return Warm.Mask != TMachineMask::TMask(-1); + } + } + + void Print(IOutputStream& out) const + { + out << "MachineCount = " << int(MachineCount) << Endl + << "Warm = [" << Warm.ToString(MachineCount) << "]" << Endl; + } +}; + +} diff --git a/ydb/library/shop/sim_estimator/estimator.css b/ydb/library/shop/sim_estimator/estimator.css new file mode 100644 index 00000000000..fa1d99f535c --- /dev/null +++ b/ydb/library/shop/sim_estimator/estimator.css @@ -0,0 +1,32 @@ +html,body,#wrapper { + width: 100%; + height: 100%; + margin: 0px; +} + +body { + font: 14px Helvetica Neue; + text-rendering: optimizeLegibility; + margin-top: 1em; + overflow-y: scroll; +} + +.data-table { + border: 2px; + border-color: black; +} + +.data-table-head { + +} + +.chart { + font-family: Arial, sans-serif; + font-size: 12px; +} + +.axis path,.axis line { + fill: none; + stroke: #000; + shape-rendering: crispEdges; +} diff --git a/ydb/library/shop/sim_estimator/estimator.html b/ydb/library/shop/sim_estimator/estimator.html new file mode 100644 index 00000000000..ce3d405b0b3 --- /dev/null +++ b/ydb/library/shop/sim_estimator/estimator.html @@ -0,0 +1,131 @@ +<!DOCTYPE html> +<html> + +<head> + <meta charset="utf-8" /> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <title>Estimators</title> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <link rel="stylesheet" type="text/css" media="screen" href="estimator.css" /> + <link rel="stylesheet" type="text/css" href="https://yastatic.net/bootstrap/3.3.6/css/bootstrap.min.css" /> + <script src="https://yastatic.net/jquery/2.2.4/jquery.min.js"></script> + <script src="https://yastatic.net/bootstrap/3.3.6/js/bootstrap.min.js"></script> + <script src="https://d3js.org/d3.v4.min.js"></script> +</head> + +<body> + <div class="container"> + <h1>Moving Simple Linear Regression</h1> + <p>This page allow you to enter any data and check how it is interpreted by online moving simple linear regression (MSLR). + MSLR is algorithm that trains regression model online using incoming data and tries to minimize exponentially + weighted sum of squared errors, i.e. more recent points get more weight based on reaction factor (0 to 1) + </p> + <h3>Data</h3> + <div class="row"> + <div class="col-md-12"> + <div id="data-table"></div> + </div> + </div> + <div class="row"> + <div class="col-md-12"> + <button class="btn btn-default btn-block" type="button" onclick="scharm.addRow()">Add Row</button> + </div> + </div> + <div class="row-fluid"> + <div class="form-group"> + <div class="pull-left"> + <div class="btn-group"> + <button class="btn btn-primary" type="button" onclick="estimator.applyData()">Apply</button> + </div> + <div class="btn-group"> + <button id="btnReserved" class="btn btn-primary" type="button" onclick="estimator.reserved()">Reserved</button> + </div> + </div> + </div> + </div> + <br/> + <br/> + <h3>Initial State</h3> + <div class="row"> + <div class="col-md-4"> + <div class="form-group"> + <label class="sr-only" for="goalMean">Initial Goal Mean</label> + <div class="input-group"> + <div class="input-group-addon">Goal</div> + <input type="text" class="form-control" id="goalMean" style="text-align:right;" value="1"> + <div class="input-group-addon">Y</div> + </div> + </div> + </div> + </div> + <div class="row"> + <div class="col-md-4"> + <div class="form-group"> + <label class="sr-only" for="featureMean">Initial Feature Mean</label> + <div class="input-group"> + <div class="input-group-addon">Feature</div> + <input type="text" class="form-control" id="featureMean" style="text-align:right;" value="1"> + <div class="input-group-addon">X</div> + </div> + </div> + </div> + </div> + <div class="row"> + <div class="col-md-4"> + <div class="form-group"> + <label class="sr-only" for="featureSqMean">Initial Feature Square Mean</label> + <div class="input-group"> + <div class="input-group-addon">Feature Square</div> + <input type="text" class="form-control" id="featureSqMean" style="text-align:right;" value="1"> + <div class="input-group-addon">X*X</div> + </div> + </div> + </div> + </div> + <div class="row"> + <div class="col-md-4"> + <div class="form-group"> + <label class="sr-only" for="goalFeatureMean">Initial Goal Feature Product Mean</label> + <div class="input-group"> + <div class="input-group-addon">Product</div> + <input type="text" class="form-control" id="goalFeatureMean" style="text-align:right;" value="1"> + <div class="input-group-addon">X*Y</div> + </div> + </div> + </div> + </div> + <h3>Model</h3> + <div class="row"> + <div class="col-md-12"> + <div id="model-chart"></div> + </div> + </div> + </div> +</body> + +</html> + +<script src="estimator.js"></script> + +<script type="text/javascript"> +var data = [{ + "goal": 11, + "feature": 10 +},{ + "goal": 23, + "feature": 20 +},{ + "goal": 31, + "feature": 30 +},{ + "goal": 43, + "feature": 40 +}]; + +var estimator = d3.estimator(data) + //.height(350) + //.width(800) + ; + +</script> + diff --git a/ydb/library/shop/sim_estimator/estimator.js b/ydb/library/shop/sim_estimator/estimator.js new file mode 100644 index 00000000000..29ca1854752 --- /dev/null +++ b/ydb/library/shop/sim_estimator/estimator.js @@ -0,0 +1,124 @@ +d3.estimator = function (tableData) { + function estimator() { } + + function createAll() { + // create empty table + table.append("table") + .attr("class", "table data-table") + ; + + table.append("thead").append("tr").attr("class", "data-table-head") + .selectAll("td") + .data(attributes) + .enter().append("th") + .text(function (d) { return d.name; }) + ; + + table.append("tbody"); + + // initialize initial state editor + //d3.select("input#throughput").on('keypress', onKeyPress); + } + + function createTableData(tableData) { + var data = []; + for (var i = 0; i < tableData.length; i++) { + var row = []; + for (var a = 0; a < attributes.length; a++) { + if (attributes[a].type) { + row.push(tableData[i][attributes[a].name]); + } else { + row.push(i); // save row index for remove operation + } + } + data.push(row); + } + + var tr = table.select("tbody").selectAll("tr.data-table-row") + .data(data) + ; + tr.exit().remove(); + var trEnterUpd = tr.enter().append("tr") + .attr("class", "data-table-row") + .merge(tr) // enter + update + ; + + var td = trEnterUpd.selectAll("td") + .data(function (d) { return d; }) + ; + + td.enter().append("td") + .merge(td) // enter + update + .attr("contenteditable", function (d, i) { return attributes[i].type ? "true" : null; }) + .text(function (d, i) { return attributes[i].type ? d : null; }) + //.on('keypress', onKeyPress) + .attr("class", function (d, i) { + return "data-cell data-cell-" + + (attributes[i].type ? attributes[i].type : attributes[i].class) + ; + }) + ; + } + + estimator.GetAverage = function () { + return GoalMean; + } + + estimator.GetEstimation = function (feature) { + return GoalMean + GetSlope() * (feature - FeatureMean); + } + + estimator.GetSlope = function () { + var disp = FeatureSqMean - FeatureMean * FeatureMean; + if (disp > 1e-10) { + return (GoalFeatureMean - GoalMean * FeatureMean) / disp; + } else { + return GetAverage(); + } + } + + estimator.Update = function (goal, feature) { + GoalMean = OldFactor * GoalMean + NewFactor * goal; + FeatureMean = OldFactor * FeatureMean + NewFactor * feature; + FeatureSqMean = OldFactor * FeatureSqMean + NewFactor * feature * feature; + GoalFeatureMean = OldFactor * GoalFeatureMean + NewFactor * goal * feature; + } + + var GoalMean = 1, + FeatureMean = 1, + FeatureSqMean = 1, + GoalFeatureMean = 1, + NewFactor = 0.3, + OldFactor = 1 - NewFactor + ; + + var attributes = [ + { name: "feature", type: "integer" }, + { name: "goal", type: "integer" }, + //{ name: "estimate", type: "integer" }, + //{ name: "error", type: "integer" }, + + // Remove button + { name: "", class: "btn-remove" } + ]; + + var margin = { + top: 20, + right: 40, + bottom: 20, + left: 80, + footer: 100, + }; + + var emptyRow = { + goal: 1, + feature: 1 + }; + + var table = d3.select("#data-table"); + + createAll(); + createTableData(tableData); + + return estimator; +}
\ No newline at end of file diff --git a/ydb/library/shop/sim_flowctl/flowctlmain.cpp b/ydb/library/shop/sim_flowctl/flowctlmain.cpp new file mode 100644 index 00000000000..19b2a5ef1b1 --- /dev/null +++ b/ydb/library/shop/sim_flowctl/flowctlmain.cpp @@ -0,0 +1,588 @@ +#include <ydb/library/shop/probes.h> +#include <ydb/library/shop/flowctl.h> + +#include <ydb/library/drr/drr.h> +#include <library/cpp/lwtrace/mon/mon_lwtrace.h> + +#include <library/cpp/getopt/last_getopt.h> +#include <library/cpp/lwtrace/all.h> +#include <library/cpp/monlib/service/monservice.h> +#include <library/cpp/monlib/service/pages/templates.h> + +#include <util/random/normal.h> +#include <util/random/random.h> +#include <util/stream/file.h> +#include <util/system/condvar.h> +#include <util/system/hp_timer.h> + +#include <google/protobuf/text_format.h> + +#include <cmath> + +#define SIMFC_PROVIDER(PROBE, EVENT, GROUPS, TYPES, NAMES) \ + PROBE(IncomingRequest, GROUPS("FcSimEvents"), \ + TYPES(ui64, TString, double, double), \ + NAMES("requestId","type","costSec","startTime")) \ + PROBE(ScheduleRequest, GROUPS("FcSimEvents"), \ + TYPES(ui64, TString, double), \ + NAMES("requestId","type","schedulerTimeSec")) \ + PROBE(DequeueRequest, GROUPS("FcSimEvents"), \ + TYPES(ui64, TString, double), \ + NAMES("requestId","type","queueTimeSec")) \ + PROBE(ExecuteRequest, GROUPS("FcSimEvents"), \ + TYPES(ui64, TString, double), \ + NAMES("requestId","type","execTimeSec")) \ + PROBE(WorkerStats, GROUPS("FcSimWorker"), \ + TYPES(double, double, double), \ + NAMES("idleTimeSec", "activeTimeSec", "totalTimeSec")) \ + PROBE(SystemStats, GROUPS("FcSimSystem"), \ + TYPES(double), \ + NAMES("utilization")) \ + PROBE(CompleteRequest, GROUPS("FcSimEvents"), \ + TYPES(ui64, TString, double, double, double, double), \ + NAMES("requestId", "type", "waitTimeSec", "totalTimeSec", "procTimeSec", "costSec")) \ +/**/ + +LWTRACE_DECLARE_PROVIDER(SIMFC_PROVIDER) +LWTRACE_DEFINE_PROVIDER(SIMFC_PROVIDER); + +namespace NFcSim { + +inline double Now() +{ + return CyclesToDuration(GetCycleCount()).SecondsFloat(); +} + +LWTRACE_USING(SIMFC_PROVIDER); + +using namespace NScheduling; +class TMyTask; +class TMyQueue; + +struct TRMeta { + double m; + double d; + double p; +}; + +// Config +int g_MonPort = 8080; +double g_AvgPeriodSec = 0.002; +double g_MaxPeriodSec = 0.001; +///TRMeta g_CostSec[] = {{0.5, 0.05, 0.05}, {0.02, 0.01, 1.0}}; // double peek +TRMeta g_CostSec[] = {{0.02, 0.01, 1.0}}; // small reqs +///TRMeta g_CostSec[] = {{0.02, 0.0, 1.0}}; // fixed cost +///TRMeta g_CostSec[] = {{0.2, 0.1, 1.0}}; // large reqs +TRMeta g_WaitSec[] = {{0.05, 0.005, 1.0}}; // 50ms wait +///TRMeta g_WaitSec[] = {{0.05, 0.000, 1.0}}; // fixed 50ms wait +///TRMeta g_WaitSec[] = {{0.005, 0.0005, 1.0}}; // 5ms wait +TDuration g_CompletePeriod = TDuration::MicroSeconds(100); +ui64 g_QuantumNs = 100 * 1000ull; +NShop::TFlowCtlConfig g_FcCfg; + +// Returns exponentially distributed random number (cuts if upperLimit exceeded) +double ExpRandom(double lambda, double upperLimit) +{ + return Min(upperLimit, -(1.0/lambda) * log(1 - RandomNumber<double>())); +} + +// Returns ranged Gaussian distribution +double GaussRandom(double m, double d) +{ + while (true) { + double x = NormalRandom(m, d); + if (x >= m/3 && x <= m*3) { + return x; + } + } +} + +// Returns random value distributed according to sum of gaussian distributions +double GaussSumRandom(TRMeta* meta, size_t size) +{ + double p = RandomNumber<double>(); + for (size_t i = 0; i < size - 1; i++) { + if (p < meta[i].p) { + return GaussRandom(meta[i].m, meta[i].d); + } + } + return GaussRandom(meta[size - 1].m, meta[size - 1].d); +} + +class TMyTask { +public: + TUCost Cost; // in microseconds + TUCost RealCost; // in microseconds + size_t Type; + TMyQueue* Queue = nullptr; + ui64 Id; + NHPTimer::STime Timer; + double SchedTime = 0.0; + double TotalTime = 0.0; + NShop::TFcOp FcOp; + TInstant CompleteDeadline; +public: + TMyTask(TUCost cost, TUCost realcost, size_t type, ui64 id) + : Cost(cost) + , RealCost(realcost) + , Type(type) + , Id(id) + { + NHPTimer::GetTime(&Timer); + } + + TUCost GetCost() const + { + return Cost; + } +}; + +class TMyQueue: public TDRRQueue { +public: + typedef TMyTask TTask; +public: + typedef TList<TMyTask*> TTasks; + TString Name; + TTasks Tasks; +public: // Interface for clients + TMyQueue(const TString& name, TWeight w = 1, TUCost maxBurst = 0) + : TDRRQueue(w, maxBurst) + , Name(name) + {} + + ~TMyQueue() + { + for (TTasks::iterator i = Tasks.begin(), e = Tasks.end(); i != e; ++i) { + delete *i; + } + } + + void PushTask(TMyTask* task) + { + task->Queue = this; + if (Tasks.empty()) { + // Scheduler must be notified on first task in queue + if (GetScheduler()) { + GetScheduler()->ActivateQueue(this); + } + } + Tasks.push_back(task); + LWPROBE(IncomingRequest, task->Id, task->Queue->Name, + double(task->Cost) / 1000000.0, + NHPTimer::GetSeconds(task->Timer)); + } +public: // Interface for scheduler + void OnSchedulerAttach() + { + Y_ABORT_UNLESS(GetScheduler() != nullptr); + if (!Tasks.empty()) { + GetScheduler()->ActivateQueue(this); + } + } + + TTask* PeekTask() + { + Y_ABORT_UNLESS(!Tasks.empty()); + return Tasks.front(); + } + + void PopTask() + { + Y_ABORT_UNLESS(!Tasks.empty()); + Tasks.pop_front(); + } + + bool Empty() const + { + return Tasks.empty(); + } +}; + +typedef std::shared_ptr<TMyQueue> TQueuePtr; + +// State +NLWTrace::TProbeRegistry* g_Probes; +NLWTrace::TManager* g_TraceMngr; +volatile bool g_Running = true; +ui64 g_LastRequestId = 0; + +TMutex g_CompleteLock; +TCondVar g_CompleteCondVar; +TVector<TMyTask*> g_Complete; + +TMutex g_ScheduledLock; +TCondVar g_ScheduledCondVar; +TDeque<TMyTask*> g_Scheduled; + +TMutex g_Lock; +TCondVar g_CondVar; +TDeque<TMyTask*> g_Incoming; +TVector<TQueuePtr> g_SchedulerQs; +THolder<NShop::TFlowCtl> g_Fc; +THolder<TDRRScheduler<TMyQueue>> g_Drr; + +TAtomic g_IdleTime = 0; // in microsec +TAtomic g_ActiveTime = 0; // in microsec + +void Arrive(TMyTask* task) +{ + TGuard<TMutex> g(g_Lock); + bool wasOpen = g_Fc->IsOpen(); + g_Fc->Arrive(task->FcOp, task->Cost, Now()); + if (!wasOpen && g_Fc->IsOpen()) { + g_CondVar.BroadCast(); + } +} + +void Depart(TMyTask* task) +{ + TGuard<TMutex> g(g_Lock); + bool wasOpen = g_Fc->IsOpen(); + g_Fc->Depart(task->FcOp, task->RealCost, Now()); + if (!wasOpen && g_Fc->IsOpen()) { + g_CondVar.BroadCast(); + } +} + +void Enqueue(TMyTask* task) +{ + TGuard<TMutex> g(g_ScheduledLock); + g_ScheduledCondVar.Signal(); + g_Scheduled.push_back(task); +} + +TMyTask* Dequeue() +{ + TGuard<TMutex> g(g_ScheduledLock); + while (g_Scheduled.empty()) { + if (!g_ScheduledCondVar.WaitT(g_ScheduledLock, TDuration::Seconds(1))) { + if (!g_Running) { + return nullptr; + } + } + } + TMyTask* task = g_Scheduled.front(); + g_Scheduled.pop_front(); + return task; +} + +struct TCompleteCmp { + bool operator()(const TMyTask* lhs, const TMyTask* rhs) { + return lhs->CompleteDeadline > rhs->CompleteDeadline; + } +}; + +void PushToWaitHeap(TMyTask* task) +{ + TGuard<TMutex> g(g_CompleteLock); + if (g_Complete.empty()) { + g_CompleteCondVar.BroadCast(); + } + g_Complete.push_back(task); + PushHeap(g_Complete.begin(), g_Complete.end(), TCompleteCmp()); +} + +TMyTask* PopFromWaitHeap() +{ + TInstant deadline = TInstant::Zero(); + while (g_Running) { + if (deadline != TInstant::Zero()) { + TDuration waitTime = TInstant::Now() - deadline; + Sleep(Min(waitTime, g_CompletePeriod)); + } + + TGuard<TMutex> g(g_CompleteLock); + while (g_Complete.empty()) { + if (!g_CompleteCondVar.WaitT(g_CompleteLock, TDuration::Seconds(1))) { + if (!g_Running) { + return nullptr; + } + } + } + + TMyTask* peek = g_Complete.front(); + TInstant now = TInstant::Now(); + if (peek->CompleteDeadline < now) { + PopHeap(g_Complete.begin(), g_Complete.end(), TCompleteCmp()); + TMyTask* task = g_Complete.back(); + g_Complete.pop_back(); + return task; + } else { + deadline = peek->CompleteDeadline; + } + } + return nullptr; +} + +void Execute(TMyTask* task) +{ + // Emulate execution for real cost nanosaconds + NHPTimer::STime timer; + NHPTimer::GetTime(&timer); + if (task->RealCost >= 2) { + Sleep(TDuration::MicroSeconds((task->RealCost - 1))); + } + double passed = 0.0; + while (passed * 1000000ull < task->RealCost) { + passed += NHPTimer::GetTimePassed(&timer); + } + + // Time measurements + double execTime = NHPTimer::GetTimePassed(&task->Timer); + task->TotalTime += execTime; + LWPROBE(ExecuteRequest, task->Id, task->Queue->Name, execTime); + + // Complete request + double wait = GaussSumRandom(g_WaitSec, Y_ARRAY_SIZE(g_WaitSec)); + task->CompleteDeadline = TInstant::Now() + + TDuration::MicroSeconds(ui64(wait * 1000000.0)); + PushToWaitHeap(task); +} + +void* CompleteThread(void*) +{ + double lastMonSec = Now(); + ui64 lastIdleTime = 0; + ui64 lastActiveTime = 0; + while (TMyTask* task = PopFromWaitHeap()) { + double waitTime = NHPTimer::GetTimePassed(&task->Timer); + task->TotalTime += waitTime; + LWPROBE(CompleteRequest, task->Id, task->Queue->Name, waitTime, + task->TotalTime, task->TotalTime - task->SchedTime, + double(task->Cost)/1000000.0); + Depart(task); + delete task; + + // Utilization monitoring + double now = Now(); + if (lastMonSec + 1.0 < now) { + ui64 idleTime = AtomicGet(g_IdleTime); + ui64 activeTime = AtomicGet(g_ActiveTime); + ui64 idleDelta = idleTime - lastIdleTime; + ui64 activeDelta = activeTime - lastActiveTime; + ui64 elapsed = idleDelta + activeDelta; + double utilization = (elapsed == 0? 0: double(activeDelta) / elapsed); + lastIdleTime = idleTime; + lastActiveTime = activeTime; + LWPROBE(SystemStats, utilization); + lastMonSec = now; + } + } + return nullptr; +} + +void* WorkerThread(void*) +{ + NHPTimer::STime workerTimer; + NHPTimer::GetTime(&workerTimer); + while (TMyTask* task = Dequeue()) { + double workerIdleTime = NHPTimer::GetTimePassed(&workerTimer); + double queueTime = NHPTimer::GetTimePassed(&task->Timer); + task->TotalTime += queueTime; + LWPROBE(DequeueRequest, task->Id, task->Queue->Name, queueTime); + Execute(task); + double workerActiveTime = NHPTimer::GetTimePassed(&workerTimer); + LWPROBE(WorkerStats, workerIdleTime, workerActiveTime, + workerIdleTime + workerActiveTime); + AtomicAdd(g_IdleTime, workerIdleTime * 1000000); + AtomicAdd(g_ActiveTime, workerActiveTime * 1000000); + } + return nullptr; +} + +void* SchedulerThread(void*) +{ + while (g_Running) { + TMyTask* task = g_Drr->PeekTask(); + if (task) { + g_Drr->PopTask(); + task->SchedTime = NHPTimer::GetTimePassed(&task->Timer); + task->TotalTime += task->SchedTime; + LWPROBE(ScheduleRequest, task->Id, task->Queue->Name, + task->SchedTime); + Arrive(task); + Enqueue(task); + } + + TGuard<TMutex> g(g_Lock); + while (!g_Fc->IsOpen() || (!g_Drr->PeekTask() && g_Incoming.empty())) { + g_CondVar.WaitI(g_Lock); + } + + for (TMyTask* task : g_Incoming) { + TMyQueue* q = g_SchedulerQs[task->Type].get(); + q->PushTask(task); + } + g_Incoming.clear(); + } + return nullptr; +} + +void* GenerateThread(void*) +{ + while (g_Running) { + // Wait some random time + Sleep(TDuration::Seconds( + ExpRandom(1.0/g_AvgPeriodSec, g_MaxPeriodSec))); + + // Generate some random task + size_t type = RandomNumber<size_t>(g_SchedulerQs.size()); + TUCost cost = 1000000ull * GaussSumRandom(g_CostSec, Y_ARRAY_SIZE(g_CostSec)); + TUCost realcost = cost * (0.5 + 1.5 * RandomNumber<double>()); // cost; + TMyTask* task = new TMyTask(cost, realcost, type, ++g_LastRequestId); + + // Push task into incoming queue + TGuard<TMutex> g(g_Lock); + if (g_Incoming.empty()) { + g_CondVar.BroadCast(); + } + g_Incoming.push_back(task); + } + return nullptr; +} + +} // namespace NFcSim + +class TMachineMonPage : public NMonitoring::IMonPage { +public: + TMachineMonPage() + : IMonPage("dashboard", "Dashboard") + {} + virtual void Output(NMonitoring::IMonHttpRequest& request) { + const char* urls[] = { + "/trace?fullscreen=y&aggr=hist&autoscale=y&refresh=3000&bn=queueTimeSec&id=.SIMFC_PROVIDER.DequeueRequest.d1s&linesfill=y&mode=analytics&out=flot&pointsshow=n&xn=queueTimeSec&y1=0&x1=0&yns=_count_share", + "/trace?fullscreen=y&aggr=hist&autoscale=y&refresh=3000&bn=execTimeSec&id=.SIMFC_PROVIDER.ExecuteRequest.d1s&linesfill=y&mode=analytics&out=flot&pointsshow=n&xn=execTimeSec&y1=0&x1=0&yns=_count_share", + "/trace?fullscreen=y&aggr=hist&autoscale=y&refresh=3000&bn=waitTimeSec&id=.SIMFC_PROVIDER.CompleteRequest.d1s&linesfill=y&mode=analytics&out=flot&pointsshow=n&xn=waitTimeSec&y1=0&x1=0&yns=_count_share", + + "/trace?fullscreen=y&id=.SHOP_PROVIDER.Arrive.d200ms&mode=analytics&out=flot&xn=_thrRTime&y1=0&yns=costInFly", + "/trace?fullscreen=y&aggr=hist&autoscale=y&refresh=3000&bn=procTimeSec&id=.SIMFC_PROVIDER.CompleteRequest.d1s&linesfill=y&mode=analytics&out=flot&pointsshow=n&xn=procTimeSec&y1=0&x1=0&yns=_count_share", + "/trace?fullscreen=y&aggr=hist&autoscale=y&refresh=3000&bn=schedulerTimeSec&id=.SIMFC_PROVIDER.ScheduleRequest.d1s&linesfill=y&mode=analytics&out=flot&pointsshow=n&xn=schedulerTimeSec&y1=0&x1=0&yns=_count_share", + + "/trace?fullscreen=y&id=.SHOP_PROVIDER.Arrive.d200ms&mode=analytics&out=flot&xn=_thrRTime&y1=0&yns=countInFly", + "/trace?fullscreen=y&id=.SIMFC_PROVIDER.DequeueRequest.d200ms&mode=analytics&out=flot&xn=_thrRTime&yns=queueTimeSec&y0=0&pointsshow=n&cutts=y", + "/trace?fullscreen=y&id=.Group.ShopFlowCtlPeriod.d10m&mode=analytics&out=flot&pointsshow=n&xn=periodId&yns=badPeriods:goodPeriods:zeroPeriods", + + "/trace?fullscreen=y&g=periodId&id=.Group.ShopFlowCtlPeriod.d10m&mode=analytics&out=flot&pointsshow=n&xn=periodId&y1=0&yns=dT_T:dL_L", + "/trace?fullscreen=y&g=periodId&id=.Group.ShopFlowCtlPeriod.d10m&mode=analytics&out=flot&pointsshow=n&xn=periodId&y1=0&yns=pv", + "/trace?fullscreen=y&g=periodId&id=.Group.ShopFlowCtlPeriod.d10m&linesfill=y&mode=analytics&out=flot&xn=periodId&y1=0&yns=state:window&pointsshow=n", + + "/trace?fullscreen=y&g=periodId&id=.Group.ShopFlowCtlPeriod.d10m&mode=analytics&out=flot&xn=periodId&y1=-1&y2=1&yns=error", + "/trace?fullscreen=y&g=periodId&id=.Group.ShopFlowCtlPeriod.d10m&linesshow=n&mode=analytics&out=flot&x1=0&xn=window&y1=0&yns=throughput&cutts=y", + "/trace?fullscreen=y&g=periodId&id=.Group.ShopFlowCtlPeriod.d10m&mode=analytics&out=flot&xn=periodId&y1=0&yns=throughput:throughputMin:throughputMax:throughputLo:throughputHi&pointsshow=n&legendshow=n", + + "/trace?fullscreen=y&g=periodId&id=.Group.ShopFlowCtlPeriod.d10m&mode=analytics&out=flot&xn=periodId&yns=mode", + "/trace?fullscreen=y&g=periodId&id=.Group.ShopFlowCtlPeriod.d10m&linesshow=n&mode=analytics&out=flot&x1=0&xn=window&y1=0&yns=latencyAvgMs&cutts=y", + "/trace?fullscreen=y&g=periodId&id=.Group.ShopFlowCtlPeriod.d10m&mode=analytics&out=flot&pointsshow=n&xn=periodId&y1=0&yns=latencyAvgMs:latencyAvgMinMs:latencyAvgMaxMs:latencyAvgLoMs:latencyAvgHiMs&legendshow=n", + + "/trace?fullscreen=y&id=.SIMFC_PROVIDER.SystemStats.d10m&mode=analytics&out=flot&xn=_thrRTime&yns=utilization&linesfill=y&y1=0&y2=1&pointsshow=n", + "/trace?fullscreen=y&autoscale=y&id=.SIMFC_PROVIDER.WorkerStats.d1s&mode=analytics&out=flot&xn=_thrRTime&yns=activeTimeSec-stack:totalTimeSec-stack&pointsshow=n&linesfill=y&cutts=y&legendshow=n", + "/trace?fullscreen=y&id=.SHOP_PROVIDER.Depart.d1s&mode=analytics&out=flot&xn=realCost&yns=estCost&linesshow=n" + }; + + TStringStream out; + out << NMonitoring::HTTPOKHTML; + HTML(out) { + out << "<!DOCTYPE html>" << Endl; + HTML_TAG() { + //HEAD() + BODY() { + out << "<table border=\"0\" width=\"100%\"><tr>"; + for (size_t i = 0; i < Y_ARRAY_SIZE(urls); i++) { + if (i > 0 && i % 3 == 0) { + out << "</tr><tr>"; + } + out << "<td><iframe style=\"border:none;width:100%;height:100%\" src=\"" + << urls[i] + << "\"></iframe></td>"; + } + out << "</tr></table>"; + } + } + } + request.Output() << out.Str(); + } +}; + +int main(int argc, char** argv) +{ + using namespace NFcSim; + try { + NLWTrace::StartLwtraceFromEnv(); +#ifdef _unix_ + signal(SIGPIPE, SIG_IGN); +#endif + +#ifdef _win32_ + WSADATA dummy; + WSAStartup(MAKEWORD(2,2), &dummy); +#endif + + // Configure + using TMonSrvc = NMonitoring::TMonService2; + THolder<TMonSrvc> MonSrvc; + NLastGetopt::TOpts opts = NLastGetopt::TOpts::Default(); + opts.AddLongOption(0, "mon-port", "port of monitoring service") + .RequiredArgument("port") + .StoreResult(&g_MonPort, g_MonPort); + NLastGetopt::TOptsParseResult res(&opts, argc, argv); + + // Init monservice + MonSrvc.Reset(new TMonSrvc(g_MonPort)); + MonSrvc->Register(new TMachineMonPage()); + NLwTraceMonPage::RegisterPages(MonSrvc->GetRoot()); + NLwTraceMonPage::ProbeRegistry().AddProbesList( + LWTRACE_GET_PROBES(SIMFC_PROVIDER)); + NLwTraceMonPage::ProbeRegistry().AddProbesList( + LWTRACE_GET_PROBES(SHOP_PROVIDER)); + g_Probes = &NLwTraceMonPage::ProbeRegistry(); + g_TraceMngr = &NLwTraceMonPage::TraceManager(); + + // Start monservice + MonSrvc->Start(); + + // Initialization + g_SchedulerQs.push_back(TQueuePtr(new TMyQueue("A:rdc"))); + g_SchedulerQs.push_back(TQueuePtr(new TMyQueue("B:map"))); + g_Fc.Reset(new NShop::TFlowCtl(g_FcCfg, NFcSim::Now())); + g_Drr.Reset(new TDRRScheduler<TMyQueue>(g_QuantumNs)); + for (TQueuePtr q : g_SchedulerQs) { + g_Drr->AddQueue(q->Name, q); + } + + + // Start all threads + TThread completeThread(&CompleteThread, nullptr); + TThread generateThread(&GenerateThread, nullptr); + TThread workerThread1(&WorkerThread, nullptr); + TThread workerThread2(&WorkerThread, nullptr); + TThread workerThread3(&WorkerThread, nullptr); + TThread workerThread4(&WorkerThread, nullptr); + TThread workerThread5(&WorkerThread, nullptr); + TThread workerThread6(&WorkerThread, nullptr); + TThread workerThread7(&WorkerThread, nullptr); + TThread workerThread8(&WorkerThread, nullptr); + TThread workerThread9(&WorkerThread, nullptr); + TThread workerThread0(&WorkerThread, nullptr); + completeThread.Start(); + generateThread.Start(); + workerThread1.Start(); + workerThread2.Start(); + workerThread3.Start(); + workerThread4.Start(); + workerThread5.Start(); + workerThread6.Start(); + workerThread7.Start(); + workerThread8.Start(); + workerThread9.Start(); + workerThread0.Start(); + SchedulerThread(nullptr); + + // Finish + g_Running = false; + Cout << "bye" << Endl; + return 0; + } catch (...) { + Cerr << "failure: " << CurrentExceptionMessage() << Endl; + return 1; + } +} diff --git a/ydb/library/shop/sim_flowctl/one.pb.txt b/ydb/library/shop/sim_flowctl/one.pb.txt new file mode 100644 index 00000000000..5a32986f89d --- /dev/null +++ b/ydb/library/shop/sim_flowctl/one.pb.txt @@ -0,0 +1,18 @@ +Machine { + Name: "srv" + Scheduler { FIFO { Name: "fifo" } } + WorkerCount: 10 + Wait { Distr { Name: "wait" Gauss { Mean: 0.05 Disp: 0.005 } MinRelValue: 0.1 MaxRelValue: 10 } } + FlowCtl { Name: "srv" } +} + +Source { + Name: "src" + InterArrival { Distr { Name: "ia" Exp { Period: 0.0002 } MaxRelValue: 10 } } + Operation { + Name: "exec" + Machine: "srv" + EstCost { Distr { Gauss { Mean: 0.02 Disp: 0.01 } MinRelValue: 0.1 MaxRelValue: 10 } } + EstCostOverRealCost { Distr { Const: 1.0 } } + } +} diff --git a/ydb/library/shop/sim_flowctl/ya.make b/ydb/library/shop/sim_flowctl/ya.make new file mode 100644 index 00000000000..fa5a8e34b5c --- /dev/null +++ b/ydb/library/shop/sim_flowctl/ya.make @@ -0,0 +1,16 @@ +PROGRAM() + +SRCS( + flowctlmain.cpp +) + +PEERDIR( + ydb/library/drr + library/cpp/lwtrace/mon + ydb/library/shop + library/cpp/getopt + library/cpp/lwtrace + library/cpp/monlib/dynamic_counters +) + +END() diff --git a/ydb/library/shop/sim_shop/config.proto b/ydb/library/shop/sim_shop/config.proto new file mode 100644 index 00000000000..30bee490c5a --- /dev/null +++ b/ydb/library/shop/sim_shop/config.proto @@ -0,0 +1,114 @@ +import "ydb/library/shop/protos/shop.proto"; + +package NShopSim; + +option java_package = "ru.yandex.shopsim.proto"; + +message TGaussDistrPb { + optional double Mean = 1; + optional double Disp = 2; +} + +message TExpDistrPb { + // Either only should be used + optional double Lambda = 1; + optional double Period = 2; // 1/Lambda +} + +message TUniformDistrPb { + optional double From = 11; + optional double To = 12; +} + +message TDistrPb { + optional string Name = 1; + optional double Weight = 2 [default = 1.0]; + + // The only one of types should be set + optional TGaussDistrPb Gauss = 11; + optional TExpDistrPb Exp = 12; + optional TUniformDistrPb Uniform = 13; + optional double Const = 14; // not random, just constant value + + optional double MinAbsValue = 101; + optional double MaxAbsValue = 102; + optional double MinRelValue = 103; // relative to Mean + optional double MaxRelValue = 104; // relative to Mean +} + +message TRandomPb { + repeated TDistrPb Distr = 2; +} + +message TOperationPb { + optional string Name = 1; + optional string Queue = 2; // for machine schedulers (overrides TSourcePb.Queue) + optional string Machine = 3; + + // cost estimation (any pair have to be set) + optional TRandomPb EstCost = 11; + optional TRandomPb RealCost = 12; + optional TRandomPb EstCostOverRealCost = 13; + optional TRandomPb EstCostMinusRealCost = 14; +} + +message TSourcePb { + optional string Name = 1; + optional string Queue = 2; // for main scheduler + optional TRandomPb InterArrival = 3; + repeated TOperationPb Operation = 4; + // TODO: make it possible to correlate costs in different ops +} + +// TODO: for common worker pool at different machines (aka common resource) +//message TWorkersPb { +// optional string Name = 1; +// optional uint64 Count = 2; +// optional double Speed = 3; +// // TODO: worker scheduling procedure? +//} + +message TFIFOPb { + optional string Name = 1; +} + +message TDRRQueuePb { + optional string Name = 1; + optional uint64 Weight = 2; + optional uint64 MaxBurst = 3; +} + +message TDRRPb { + optional string Name = 1; + optional uint64 Quantum = 2; + + repeated TDRRQueuePb Queue = 3; +}; + +message TSchedulerPb { + // Only one qdisc should be set + optional TFIFOPb FIFO = 1; + optional TDRRPb DRR = 2; +} + +message TMachinePb { + optional string Name = 1; + + // At first all incoming jobs are pushed to queueing discipline + optional TSchedulerPb Scheduler = 101; + + // After job scheduling, operation of a job is processed by worker + optional uint64 WorkerCount = 201 [default = 1]; + optional double Speed = 202 [default = 1.0]; + + // After operation processing job waits for random time + optional TRandomPb Wait = 301; + + // Machine operates under flow controller + optional NShop.TFlowCtlConfig FlowCtl = 401; +} + +message TConfigPb { + repeated TMachinePb Machine = 1; + repeated TSourcePb Source = 2; +} diff --git a/ydb/library/shop/sim_shop/myshop.cpp b/ydb/library/shop/sim_shop/myshop.cpp new file mode 100644 index 00000000000..8892dcc7a54 --- /dev/null +++ b/ydb/library/shop/sim_shop/myshop.cpp @@ -0,0 +1,807 @@ +#include "myshop.h" + +#include <library/cpp/protobuf/util/is_equal.h> + +#include <util/random/normal.h> +#include <util/random/random.h> + +#include <util/generic/hash_set.h> + +LWTRACE_DEFINE_PROVIDER(SIMSHOP_PROVIDER); + +namespace NShopSim { + +LWTRACE_USING(SIMSHOP_PROVIDER); + +// Return random value according to distribution described by protobuf +double Random(const TDistrPb& pb) +{ + double x; + double mean; + if (pb.HasConst()) { + mean = x = pb.GetConst(); + } else if (pb.HasGauss()) { + mean = pb.GetGauss().GetMean(); + x = NormalRandom(mean, pb.GetGauss().GetDisp()); + } else if (pb.HasExp()) { + mean = pb.GetExp().HasLambda()? + pb.GetExp().GetLambda() : 1.0 / pb.GetExp().GetPeriod(); + x = -(1.0 / mean) * log(1 - RandomNumber<double>()); + } else if (pb.HasUniform()) { + double from = pb.GetUniform().GetFrom(); + double to = pb.GetUniform().GetTo(); + mean = (from + to) / 2.0; + x = from + (to - from) * RandomNumber<double>(); + } + if (pb.HasMinAbsValue()) { + x = Max(x, pb.GetMinAbsValue()); + } + if (pb.HasMaxAbsValue()) { + x = Min(x, pb.GetMaxAbsValue()); + } + if (pb.HasMinRelValue()) { + x = Max(x, mean * pb.GetMinRelValue()); + } + if (pb.HasMaxAbsValue()) { + x = Min(x, mean * pb.GetMaxRelValue()); + } + return x; +} + +// Return random value according to distribution described by protobuf +double Random(const TRandomPb& pb) +{ + double totalWeight = 0; + for (int i = 0; i < pb.GetDistr().size(); i++) { + double weight = pb.GetDistr(i).GetWeight(); + Y_ABORT_UNLESS(weight >= 0); + totalWeight += weight; + } + + double weightSum = 0; + double p = RandomNumber<double>(); + for (int i = 0; i < pb.GetDistr().size(); i++) { + TDistrPb distr = pb.GetDistr(i); + double weight = distr.GetWeight(); + weightSum += weight; + if (p < weightSum/totalWeight) { + return Random(distr); + } + } + Y_ABORT("bad weights configuration"); +} + +TMyShop::TMyShop() + : SourceThread(&SourceThreadFunc, this) + , SchedulerThread(&SchedulerThreadFunc, this) +{ + SourceThread.Start(); + SchedulerThread.Start(); +} + +TMyShop::~TMyShop() +{ + AtomicSet(Running, 0); + // Explicit join to avoid troubles + SourceThread.Join(); + SchedulerThread.Join(); +} + +void TMyShop::Configure(const TConfigPb& newCfg) +{ + TGuard<TMutex> g(Mutex); + Cfg = newCfg; + + // Create map of current machines by name + THashMap<TString, TMyMachine*> machines; + for (const auto& kv : Machines) { + TMyMachine* machine = kv.second.Machine.Get(); + machines[machine->GetName()] = machine; + } + + // Create/update machines + THashSet<TString> names; + for (int i = 0; i < Cfg.GetMachine().size(); i++) { + const TMachinePb& pb = Cfg.GetMachine(i); + bool inserted = names.insert(pb.GetName()).second; + Y_ABORT_UNLESS(inserted, "duplicate machine name '%s'", pb.GetName().data()); + + TMachineItem* item; + auto mIter = machines.find(pb.GetName()); + if (mIter != machines.end()) { + item = &Machines.find(mIter->second->GetId())->second; + } else { + // Create new machine + ui64 id = ++LastMachineId; + item = &Machines[id]; + item->Machine.Reset(new TMyMachine(this, id)); + machines[pb.GetName()] = item->Machine.Get(); + } + + // Machine's flows must be cleared to destroy old flows + item->Machine->ClearFlows(); + + // Configure/reconfigure machine and it's flow control + item->Machine->Configure(pb); + auto st = item->Machine->GetFlowCtl()->ConfigureST(pb.GetFlowCtl(), Now()); + switch (st) { + case TFlowCtl::None: break; + case TFlowCtl::Closed: item->Machine->Freeze(); break; + case TFlowCtl::Opened: item->Machine->Unfreeze(); break; + } + } + + // Remove machines that are no longer in config + TVector<TMyMachine*> toDelete; // to avoid deadlocks + for (const auto& kv : machines) { + if (names.find(kv.first) == names.end()) { + TMyMachine* machine = kv.second; + auto mIter = Machines.find(machine->GetId()); + Y_ABORT_UNLESS(mIter != Machines.end()); + TMachineItem* item = &mIter->second; + toDelete.push_back(item->Machine.Release()); + Machines.erase(mIter); + } + } + + // Create map of current flows by name + THashMap<TString, TMyFlowPtr> flows; + for (const THeapItem& item : SourceHeap) { + const TMyFlowPtr& flow = item.Flow; + flows[flow->Source.GetName()] = flow; + } + + // All flows will be actually deleted when all their jobs are done + SourceHeap.clear(); + Sched.Clear(); + + // Create/update flows + double now = Now(); + for (int i = 0; i < newCfg.GetSource().size(); i++) { + TMyFlowPtr flow; + const TSourcePb& source = newCfg.GetSource(i); + double ia = Random(source.GetInterArrival()); + auto fIter = flows.find(source.GetName()); + if (fIter != flows.end() && NProtoBuf::IsEqual(source, fIter->second->Source)) { + // Use already existing flow + flow = fIter->second; + } else { + // Create new flow + flow.Reset(new TMyFlow(source, &Sched)); + + // Create ops with simplest linear dependencies graph + // (i.e. sequential processing) + size_t prevsid; + for (int i = 0; i < source.GetOperation().size(); i++) { + const TOperationPb& pb = source.GetOperation(i); + auto mIter = machines.find(pb.GetMachine()); + if (mIter != machines.end()) { + TMyMachine* machine = mIter->second; + ui64 machineId = machine->GetId(); + if (i == 0) { + prevsid = flow->AddStage(machineId, {}); + } else { + prevsid = flow->AddStage(machineId, {prevsid}); + } + + // Freeze control + machine->AddFlow(flow); + if (machine->IsFrozen()) { + flow->IncFrozenMachine(); + } + } else { + Y_ABORT("unknown machine %s", pb.GetMachine().data()); + } + } + } + PushSource(now + ia, flow); + if (!flow->Empty()) { + flow->Activate(); + } + } + + // Delete machines after lock release to avoid deadlock w/ wait-thread + g.Release(); + for (TMyMachine* machine : toDelete) { + delete machine; // this will join thread that can lock `Mutex' + } +} + +void TMyShop::OperationFinished(TMyJob* job, size_t sid, ui64 realcost, bool success) +{ + TGuard<TMutex> g(Mutex); + TShop::OperationFinished(job, sid, realcost, success, Now()); +} + +TMyMachine* TMyShop::GetMachine(ui64 machineId) +{ + TGuard<TMutex> g(Mutex); + auto iter = Machines.find(machineId); + if (iter != Machines.end()) { + return iter->second.Machine.Get(); + } else { + return nullptr; + } +} + +TMachine* TMyShop::GetMachine(const TJob* job, size_t sid) +{ + return GetMachine(job->Flow->GetStage(sid)->MachineId); +} + +TMachineCtl* TMyShop::GetMachineCtl(const TJob* job, size_t sid) +{ + return GetMachine(job->Flow->GetStage(sid)->MachineId); +} + +void TMyShop::StartJob(TMyJob* job) +{ + TGuard<TMutex> g(Mutex); + double now = Now(); + TShopCtl::ArriveJob(job, now); + TShop::StartJob(job, now); +} + +void TMyShop::JobFinished(TJob* job) +{ + TGuard<TMutex> g(Mutex); // for TMyFlow dtor to work under lock + TShop::JobFinished(job); + delete static_cast<TMyJob*>(job); +} + +void TMyShop::OnOperationFinished(TJob* job, size_t sid, ui64 realcost, double now) +{ + DepartJobStage(job, sid, realcost, now); +} + +void TMyShop::OnOperationAbort(TJob* job, size_t sid) +{ + AbortJobStage(job, sid); +} + +void* TMyShop::SourceThreadFunc(void* this_) +{ + ::NShopSim::SetCurrentThreadName("source"); + reinterpret_cast<TMyShop*>(this_)->Source(); + return nullptr; +} + +void* TMyShop::SchedulerThreadFunc(void* this_) +{ + ::NShopSim::SetCurrentThreadName("sched"); + reinterpret_cast<TMyShop*>(this_)->Scheduler(); + return nullptr; +} + +inline ui64 IntegerCost(double cost) +{ + return Max<ui64>(1, ceilf(cost * 1e6)); +} + +void TMyShop::Generate(double time, const TMyFlowPtr& flow) +{ + TMyJob* job = new TMyJob(); + job->MyFlow = flow; + job->GenerateTime = time; + job->Flow = flow.Get(); + job->Cost = 0; + for (int i = 0; i < flow->Source.GetOperation().size(); i++) { + const TOperationPb& op = flow->Source.GetOperation(i); + double estcost = NAN; + double realcost = NAN; + if (op.HasEstCost()) { + estcost = Random(op.GetEstCost()); + if (op.HasRealCost()) { + realcost = Random(op.GetRealCost()); + } else if (op.HasEstCostMinusRealCost()) { + double diff = Random(op.GetEstCostMinusRealCost()); + realcost = estcost - diff; + } else if (op.HasEstCostOverRealCost()) { + double ratio = Random(op.GetEstCostOverRealCost()); + realcost = estcost / ratio; + } else { + Y_ABORT("wrong flow config"); + } + } else if (op.HasRealCost()) { + realcost = Random(op.GetRealCost()); + if (op.HasEstCostMinusRealCost()) { + double diff = Random(op.GetEstCostMinusRealCost()); + estcost = realcost + diff; + } else if (op.HasEstCostOverRealCost()) { + double ratio = Random(op.GetEstCostOverRealCost()); + estcost = realcost * ratio; + } else { + Y_ABORT("wrong flow config"); + } + } else { + Y_ABORT("wrong flow config"); + } + ui64 intrealcost = IntegerCost(realcost); + job->Cost += intrealcost; + job->RealCost.push_back(intrealcost); + job->AddOp(IntegerCost(estcost)); + } + Enqueue(job); +} + +void TMyShop::PushSource(double time, const TMyFlowPtr& flow) +{ + SourceHeap.emplace_back(time, flow); + PushHeap(SourceHeap.begin(), SourceHeap.end()); +} + +void TMyShop::PopSource() +{ + PopHeap(SourceHeap.begin(), SourceHeap.end()); + SourceHeap.pop_back(); +} + +void TMyShop::Source() +{ + double deadline = 0.0; + while (AtomicGet(Running)) { + if (deadline != 0.0) { + double waitTime = Now() - deadline; + if (waitTime > 0.0) { + NanoSleep(Min(waitTime, 1.0) * 1e9); + } + } + + while (AtomicGet(Running)) { + TGuard<TMutex> g(Mutex); + double now = Now(); + if (SourceHeap.empty()) { + deadline = now + 1.0; // Wait for config with sources + break; + } + + THeapItem peek = SourceHeap.front(); + if (peek.Time < now) { + PopSource(); + Generate(peek.Time, peek.Flow); + double ia = Random(peek.Flow->Source.GetInterArrival()); + PushSource(peek.Time + ia, peek.Flow); + } else { + deadline = peek.Time; + break; + } + } + } +} + +bool TMyShop::IsFlowFrozen(TMyFlow* flow) +{ + for (size_t sid = 0; sid < flow->StageCount(); sid++) { + if (TMachine* machine = GetMachine(flow->GetStage(sid)->MachineId)) { + if (static_cast<TMyMachine*>(machine)->IsFrozen()) { + return true; + } + } else { + // Flow is considered to be frozen if any of machines is unavailable + return true; + } + } + // Flow is not frozen only if every machine is not frozen + return false; +} + +void TMyShop::Enqueue(TMyJob* job) +{ + TGuard<TMutex> g(Mutex); + CondVar.Signal(); + job->MyFlow->Enqueue(job); +} + +TMyJob* TMyShop::Dequeue(TInstant deadline) +{ + TGuard<TMutex> g(Mutex); + // Wait for jobs + while (Sched.Empty()) { + if (!CondVar.WaitD(Mutex, deadline)) { + return nullptr; + } + } + return static_cast<TMyJob*>(Sched.PopSchedulable()); +} + +void TMyShop::Scheduler() +{ + while (AtomicGet(Running)) { + if (TMyJob* job = Dequeue(TInstant::Now() + TDuration::Seconds(1))) { + StartJob(job); + } + } +} + +class TFifo : public IScheduler { +private: + TString Name; + TMutex Mutex; + TCondVar CondVar; + struct TItem { + TMyJob* Job; + size_t Sid; + ui64 Ts; + TItem() {} + TItem(TMyJob* job, size_t sid) : Job(job), Sid(sid), Ts(GetCycleCount()) {} + }; + TDeque<TItem> Queue; + ui64 EstCostInQueue = 0; + TMyShop* Shop; +public: + explicit TFifo(TMyShop* shop) + : Shop(shop) + {} + + void SetName(const TString& name) { Name = name; } + + ~TFifo() { + Y_ABORT_UNLESS(Queue.empty(), "queue must be empty on destruction"); + } + + void Enqueue(TMyJob* job, size_t sid) override { + TGuard<TMutex> g(Mutex); + LWPROBE(FifoEnqueue, Shop->GetName(), job->Flow->GetName(), job->JobId, sid, + job->Flow->GetStage(sid)->MachineId, + job->GetOp(sid)->EstCost, Queue.size(), EstCostInQueue); + CondVar.Signal(); + Queue.emplace_back(job, sid); + EstCostInQueue += job->GetOp(sid)->EstCost; + } + + TMyJob* Dequeue(size_t* sid) override { + TGuard<TMutex> g(Mutex); + if (!Queue.empty()) { + TItem item = Queue.front(); + Queue.pop_front(); + EstCostInQueue -= item.Job->GetOp(item.Sid)->EstCost; + if (sid) { + *sid = item.Sid; + } + LWPROBE(FifoDequeue, Shop->GetName(), item.Job->Flow->GetName(), item.Job->JobId, item.Sid, + item.Job->Flow->GetStage(item.Sid)->MachineId, + item.Job->GetOp(item.Sid)->EstCost, Queue.size(), EstCostInQueue, + CyclesToMs(Duration(item.Ts, GetCycleCount()))); + return item.Job; + } + return nullptr; + } + + TMyJob* DequeueWait(size_t* sid, TInstant deadline) override { + TGuard<TMutex> g(Mutex); + while (Queue.empty()) { + if (!CondVar.WaitD(Mutex, deadline)) { + return nullptr; + } + } + return TFifo::Dequeue(sid); + } +}; + +void* TMyMachine::TWorkerThread::ThreadProc() +{ + ::NShopSim::SetCurrentThreadName(ToString(WorkerIdx) + Machine->GetName()); + Machine->Worker(WorkerIdx); + return nullptr; +} + +TMyMachine::TMyMachine(TMyShop* shop, ui64 machineId) + : TMachine(shop) + , TMachineCtl(new TFlowCtl()) + , MyShop(shop) + , MachineId(machineId) + , WaitThread(&WaitThreadFunc, this) +{ + WaitThread.Start(); +} + +void TMyMachine::FailAllJobs() +{ + TReadSpinLockGuard g(ConfigureLock); + size_t sid; + while (TMyJob* job = Scheduler->Dequeue(&sid)) { + MyShop->OperationFinished(job, sid, 0, false); + } +} + +TMyMachine::~TMyMachine() +{ + AtomicSet(Running, 0); + + // Fail all enqueued jobs + FailAllJobs(); + + // Join all threads (complete and workers) explicitly + // to avoid joining on half-destructed TMachine object + WorkerThread.clear(); + WaitThread.Join(); +} + +void TMyMachine::Configure(const TMachinePb& cfg) +{ + TWriteSpinLockGuard g(ConfigureLock); + TSchedulerPb oldSchedulerCfg = GetSchedulerCfg(); + SetConfig(cfg); + SetName(cfg.GetName()); + + // Update scheduler + if (!NProtoBuf::IsEqual(oldSchedulerCfg, cfg.GetScheduler())) { + THolder<IScheduler> oldScheduler(Scheduler.Release()); + if (cfg.GetScheduler().HasFIFO()) { + TFifo* fifo = new TFifo(MyShop); + fifo->SetName(cfg.GetScheduler().GetFIFO().GetName()); + Scheduler.Reset(fifo); + } else { + Y_ABORT_UNLESS("only FIFO queueing disciplice is supported for now"); + } + + if (oldScheduler) { + // Requeue jobs into new scheduler + size_t sid; + while (TMyJob* job = oldScheduler->Dequeue(&sid)) { + Scheduler->Enqueue(job, sid); + } + oldScheduler.Destroy(); // Just to be explicit + } + } + + // Adjust workers count + while (WorkerThread.size() < cfg.GetWorkerCount()) { + auto worker = new TWorkerThread(this, WorkerThread.size()); + WorkerThread.emplace_back(worker); + worker->Start(); + } + WorkerThread.resize(cfg.GetWorkerCount()); // Shrink only, join threads +} + +bool TMyMachine::StartOperation(TJob* job, size_t sid) +{ + TReadSpinLockGuard g(ConfigureLock); + TMachine::StartOperation(job, sid); + Scheduler->Enqueue(static_cast<TMyJob*>(job), sid); + return false; +} + +void TMyMachine::Freeze() +{ + Frozen = true; + for (const TMyFlowPtr& p : Flows) { + p->IncFrozenMachine(); + } +} + +void TMyMachine::Unfreeze() +{ + Frozen = false; + for (const TMyFlowPtr& p : Flows) { + p->DecFrozenMachine(); + } +} + +void TMyMachine::ClearFlows() +{ + if (Frozen) { + for (const TMyFlowPtr& flow : Flows) { + flow->DecFrozenMachine(); + } + } + Flows.clear(); +} + +void TMyMachine::AddFlow(const TMyFlowPtr& flow) +{ + Flows.push_back(flow); +} + +void TMyMachine::SetConfig(const TMachinePb& cfg) +{ + TGuard<TSpinLock> g(CfgLock); + Cfg = cfg; +} + +TSchedulerPb TMyMachine::GetSchedulerCfg() +{ + TGuard<TSpinLock> g(CfgLock); + return Cfg.GetScheduler(); +} + +double TMyMachine::RandomWaitTime() +{ + TGuard<TSpinLock> g(CfgLock); + return Random(Cfg.GetWait()); +} + +ui64 TMyMachine::GetWorkerCount() +{ + TGuard<TSpinLock> g(CfgLock); + return Cfg.GetWorkerCount(); +} + +double TMyMachine::GetSpeed() +{ + TGuard<TSpinLock> g(CfgLock); + return Cfg.GetSpeed(); +} + +TMyJob* TMyMachine::DequeueJob(size_t* sid) +{ + TReadSpinLockGuard g(ConfigureLock); + return Scheduler->DequeueWait(sid, TInstant::Now() + TDuration::Seconds(1)); +} + +void TMyMachine::Worker(size_t workerIdx) +{ + NHPTimer::STime workerTimer; + NHPTimer::GetTime(&workerTimer); + + while (AtomicGet(Running) && workerIdx < GetWorkerCount()) { + size_t jobLimitPerCycle = 10; + size_t sid; + while (TMyJob* job = DequeueJob(&sid)) { + double workerIdleTime = NHPTimer::GetTimePassed(&workerTimer) * 1000; + Execute(job, sid); + double workerActiveTime = NHPTimer::GetTimePassed(&workerTimer) * 1000; + LWPROBE(MachineWorkerStats, MyShop->GetName(), MachineId, + workerIdleTime, workerActiveTime, workerIdleTime + workerActiveTime); + AtomicAdd(IdleTime, workerIdleTime * 1000); + AtomicAdd(ActiveTime, workerActiveTime * 1000); + if (!--jobLimitPerCycle) { + break; + } + } + } +} + +void TMyMachine::Execute(TMyJob* job, size_t sid) +{ + // Emulate execution for real cost nanosaconds + ui64 ts = GetCycleCount(); + double speed = GetSpeed(); + ui64 realcost = job->RealCost[sid] / speed; + if (realcost >= 2) { + Sleep(TDuration::MicroSeconds((realcost - 1))); + } + while (CyclesToMs(Duration(ts, GetCycleCount())) * 1000.0 < realcost) ; + + // Time measurements + ui64 execTs = GetCycleCount(); + double execTimeMs = CyclesToMs(Duration(ts, execTs)); + LWPROBE(MachineExecute, MyShop->GetName(), job->Flow->GetName(), job->JobId, sid, + MachineId, + job->GetOp(sid)->EstCost, job->RealCost[sid], speed, execTimeMs); + + double wait = RandomWaitTime(); + TInstant deadline = TInstant::Now() + TDuration::MicroSeconds(ui64(wait * 1000000.0)); + PushToWaitHeap(deadline, job, sid, execTs); +} + +void TMyMachine::PushToWaitHeap(TInstant deadline, TMyJob* job, size_t sid, ui64 execTs) +{ + TGuard<TMutex> g(WaitMutex); + WaitCondVar.Signal(); + WaitHeap.emplace_back(deadline, job, sid, execTs); + PushHeap(WaitHeap.begin(), WaitHeap.end(), TWaitCmp()); +} + +TMyJob* TMyMachine::PopFromWaitHeap(size_t* sid) +{ + while (AtomicGet(Running)) { + TGuard<TMutex> g(WaitMutex); + while (WaitHeap.empty()) { + if (!WaitCondVar.WaitT(WaitMutex, TDuration::Seconds(1))) { + if (!AtomicGet(Running)) { + return nullptr; + } + } + } + + TWaitItem peek = WaitHeap.front(); + TInstant now = TInstant::Now(); + if (peek.Deadline < now) { + PopHeap(WaitHeap.begin(), WaitHeap.end(), TWaitCmp()); + WaitHeap.pop_back(); + if (sid) { + *sid = peek.Sid; + } + LWPROBE(MachineWait, MyShop->GetName(), peek.Job->Flow->GetName(), peek.Job->JobId, peek.Sid, + MachineId, + CyclesToMs(Duration(peek.Ts, GetCycleCount()))); + return peek.Job; + } else { + TInstant deadline = peek.Deadline; + WaitCondVar.WaitD(WaitMutex, Min(deadline, now + TDuration::Seconds(1))); + } + } + return nullptr; +} + +void* TMyMachine::WaitThreadFunc(void* this_) +{ + reinterpret_cast<TMyMachine*>(this_)->Wait(); + return nullptr; +} + +void TMyMachine::Wait() +{ + ::NShopSim::SetCurrentThreadName(GetName()); + + double lastMonSec = Now(); + ui64 lastIdleTime = 0; + ui64 lastActiveTime = 0; + size_t sid; + while (TMyJob* job = PopFromWaitHeap(&sid)) { + ui64 realcost = job->RealCost[sid]; + MyShop->OperationFinished(job, sid, realcost, true); + + // Utilization monitoring + double now = Now(); + if (lastMonSec + 1.0 < now) { + ui64 idleTime = AtomicGet(IdleTime); + ui64 activeTime = AtomicGet(ActiveTime); + ui64 idleDelta = idleTime - lastIdleTime; + ui64 activeDelta = activeTime - lastActiveTime; + ui64 elapsed = idleDelta + activeDelta; + double utilization = (elapsed == 0? 0: double(activeDelta) / elapsed); + lastIdleTime = idleTime; + lastActiveTime = activeTime; + LWPROBE(MachineStats, MyShop->GetName(), MachineId, utilization); + lastMonSec = now; + } + } +} + +TMyFlow::TMyFlow(const TSourcePb& source, TScheduler<TSingleResource>* sched) + : Source(source) +{ + TFlow::SetName(source.GetName()); + Freezable.SetName(source.GetName()); + Freezable.SetScheduler(sched); + TConsumer<TSingleResource>::SetName(source.GetName()); + SetScheduler(sched); + SetFreezable(&Freezable); +} + +TMyFlow::~TMyFlow() +{ + Deactivate(); +} + +void TMyFlow::Enqueue(TMyJob* job) +{ + if (Queue.empty()) { + Activate(); + } + Queue.push_back(job); +} + +TSchedulable<TSingleResource::TCost>* TMyFlow::PopSchedulable() +{ + Y_ABORT_UNLESS(!Queue.empty()); + TMyJob* job = Queue.front(); + Queue.pop_front(); + return job; +} + +bool TMyFlow::Empty() const +{ + return Queue.empty(); +} + +void TMyFlow::IncFrozenMachine() +{ + if (FrozenMachines == 0) { + Freezable.Freeze(); + } + FrozenMachines++; +} + +void TMyFlow::DecFrozenMachine() +{ + Y_ASSERT(FrozenMachines > 0); + FrozenMachines--; + if (FrozenMachines == 0) { + Freezable.Unfreeze(); + } +} + +} diff --git a/ydb/library/shop/sim_shop/myshop.h b/ydb/library/shop/sim_shop/myshop.h new file mode 100644 index 00000000000..7bac571c892 --- /dev/null +++ b/ydb/library/shop/sim_shop/myshop.h @@ -0,0 +1,286 @@ +#pragma once + +#include <ydb/library/shop/sim_shop/config.pb.h> + +#include <ydb/library/shop/flowctl.h> +#include <ydb/library/shop/shop.h> +#include <ydb/library/shop/scheduler.h> + +#include <ydb/library/drr/drr.h> + +#include <library/cpp/lwtrace/all.h> + +#include <util/system/condvar.h> +#include <util/system/execpath.h> +#include <util/system/hp_timer.h> + +#include <util/generic/ptr.h> +#include <util/generic/list.h> + +#define SIMSHOP_PROVIDER(PROBE, EVENT, GROUPS, TYPES, NAMES) \ + PROBE(FifoEnqueue, GROUPS("SimShopJob", "SimShopOp"), \ + TYPES(TString, TString, ui64, ui64, ui64, ui64, ui64, ui64), \ + NAMES("shop", "flow", "job", "sid", "machineid", "estcost", "queueLength", "queueCost")) \ + PROBE(FifoDequeue, GROUPS("SimShopJob", "SimShopOp"), \ + TYPES(TString, TString, ui64, ui64, ui64, ui64, ui64, ui64, double), \ + NAMES("shop", "flow", "job", "sid", "machineid", "estcost", "queueLength", "queueCost", "queueTimeMs")) \ + PROBE(MachineExecute, GROUPS("SimShopJob", "SimShopOp"), \ + TYPES(TString, TString, ui64, ui64, ui64, ui64, ui64, double, double), \ + NAMES("shop", "flow", "job", "sid", "machineid", "estcost", "realcost", "speed", "execTimeMs")) \ + PROBE(MachineWait, GROUPS("SimShopJob", "SimShopOp"), \ + TYPES(TString, TString, ui64, ui64, ui64, double), \ + NAMES("shop", "flow", "job", "sid", "machineid", "waitTimeMs")) \ + PROBE(MachineWorkerStats, GROUPS("SimShopMachine"), \ + TYPES(TString, ui64, double, double, double), \ + NAMES("shop", "machineid", "idleTimeMs", "activeTimeMs", "totalTimeMs")) \ + PROBE(MachineStats, GROUPS("SimShopMachine"), \ + TYPES(TString, ui64, double), \ + NAMES("shop", "machineid", "utilization")) \ +/**/ + +LWTRACE_DECLARE_PROVIDER(SIMSHOP_PROVIDER) + +namespace NShopSim { + +inline void SetCurrentThreadName(const TString& name, + const ui32 maxCharsFromProcessName = 8) +{ +#if defined(_linux_) + // linux limits threadname by 15 + \0 + + TStringBuf procName(GetExecPath()); + procName = procName.RNextTok('/'); + procName = procName.SubStr(0, maxCharsFromProcessName); + + TStringStream linuxName; + linuxName << procName << "." << name; + TThread::SetCurrentThreadName(linuxName.Str().data()); +#else + Y_UNUSED(maxCharsFromProcessName); + TThread::SetCurrentThreadName(name.c_str()); +#endif +} + +inline double Now() +{ + return CyclesToDuration(GetCycleCount()).SecondsFloat(); +} + +using namespace NShop; + +class TMyQueue; +class TMyFlow; +struct TMyJob; +class TMyShop; +class TMyMachine; + +class TMyFlow + : public TFlow + , public TConsumer<TSingleResource> + , public TAtomicRefCount<TMyFlow> +{ +public: + const TSourcePb Source; + + // Should be accessed under Shop::Mutex lock + TDeque<TMyJob*> Queue; + TFreezable<TSingleResource> Freezable; + ui64 FrozenMachines = 0; + + TMyFlow(const TSourcePb& source, TScheduler<TSingleResource>* sched); + ~TMyFlow(); + void Enqueue(TMyJob* job); + TSchedulable<TSingleResource::TCost>* PopSchedulable() override; + bool Empty() const override; + void IncFrozenMachine(); + void DecFrozenMachine(); +}; + +using TMyFlowPtr = TIntrusivePtr<TMyFlow>; + +struct TMyJob + : public TJob + , public TSchedulable<TSingleResource::TCost> +{ + TVector<ui64> RealCost; // us + double GenerateTime = 0.0; + TMyFlowPtr MyFlow; +}; + +class IScheduler { +public: + virtual ~IScheduler() {} + virtual void Enqueue(TMyJob* job, size_t sid) = 0; + virtual TMyJob* Dequeue(size_t* sid) = 0; + virtual TMyJob* DequeueWait(size_t* sid, TInstant deadline) = 0; +}; + +class TMyMachine : public TMachine, public TMachineCtl { +private: + class TWorkerThread + : public ISimpleThread + , public TSimpleRefCount<TWorkerThread> + { + private: + TMyMachine* Machine; + size_t WorkerIdx; + public: + TWorkerThread(TMyMachine* machine, size_t workerIdx) + : Machine(machine) + , WorkerIdx(workerIdx) + {} + void* ThreadProc() override; + }; + using TWorkerThreadPtr = TIntrusivePtr<TWorkerThread>; + +private: + TAtomic Running = 1; + TMyShop* MyShop; + const ui64 MachineId; + + bool Frozen = false; + TVector<TMyFlowPtr> Flows; + + TSpinLock CfgLock; + TMachinePb Cfg; + + TRWSpinLock ConfigureLock; + TFlowCtl MyFlowCtl; + THolder<IScheduler> Scheduler; + TList<TWorkerThreadPtr> WorkerThread; + + TMutex WaitMutex; + TCondVar WaitCondVar; + struct TWaitItem { + TInstant Deadline; + TMyJob* Job; + size_t Sid; + ui64 Ts; + TWaitItem(TInstant d, TMyJob* j, size_t s, ui64 t) + : Deadline(d), Job(j), Sid(s), Ts(t) + {} + }; + struct TWaitCmp { + bool operator()(const TWaitItem& lhs, const TWaitItem& rhs) const { + return lhs.Deadline > rhs.Deadline; + } + }; + TVector<TWaitItem> WaitHeap; + TThread WaitThread; + + // Monitoring + TAtomic IdleTime; + TAtomic ActiveTime; + +public: + TMyMachine(TMyShop* shop, ui64 machineId); + ~TMyMachine(); + + ui64 GetId() const { return MachineId; } + + void Configure(const TMachinePb& cfg); + bool StartOperation(TJob* job, size_t sid) override; + void Freeze() override; + void Unfreeze() override; + bool IsFrozen() { return Frozen; } + + void ClearFlows(); + void AddFlow(const TMyFlowPtr& flow); + + +private: + void SetConfig(const TMachinePb& cfg); + TSchedulerPb GetSchedulerCfg(); + double RandomWaitTime(); + ui64 GetWorkerCount(); + double GetSpeed(); + TMyJob* DequeueJob(size_t* sid); + void Worker(size_t workerIdx); + void Execute(TMyJob* job, size_t sid); + void PushToWaitHeap(TInstant deadline, TMyJob* job, size_t sid, ui64 execTs); + TMyJob* PopFromWaitHeap(size_t* sid); + static void* WaitThreadFunc(void* this_); + void Wait(); + void FailAllJobs(); + friend class TWorkerThread; +}; + +class TMyShop : public TShop, public TShopCtl { +private: + TAtomic Running = 1; + + TMutex Mutex; + TCondVar CondVar; + TConfigPb Cfg; + + // Flows and sources + TThread SourceThread; + struct THeapItem { + double Time; + TMyFlowPtr Flow; + THeapItem(double time, const TMyFlowPtr flow) + : Time(time) + , Flow(flow) + {} + bool operator<(const THeapItem& rhs) const { + return Time > rhs.Time; + } + }; + TVector<THeapItem> SourceHeap; + + // Scheduling and congestion control + TThread SchedulerThread; + TScheduler<TSingleResource> Sched; + + // Machines + struct TMachineItem { + THolder<TMyMachine> Machine; + }; + THashMap<ui64, TMachineItem> Machines; // machineId -> machine/flowctl + ui64 LastMachineId = 0; +public: + TMyShop(); + ~TMyShop(); + + void Configure(const TConfigPb& newCfg); + void OperationFinished(TMyJob* job, size_t sid, ui64 realcost, bool success); + + TMyMachine* GetMachine(ui64 machineId); + TMachine* GetMachine(const TJob* job, size_t sid) override; + TMachineCtl* GetMachineCtl(const TJob* job, size_t sid) override; + + void StartJob(TMyJob* job); + void JobFinished(TJob* job) override; + + template <class TFunc> + void ForEachMachine(TFunc func) + { + TGuard<TMutex> g(Mutex); + for (const auto& kv : Machines) { + ui64 machineId = kv.first; + TMyMachine* machine = kv.second.Machine.Get(); + func(machineId, machine, machine->GetFlowCtl()); + } + } + + TConfigPb GetConfig() const { TGuard<TMutex> g(Mutex); return Cfg; } + +protected: + void OnOperationFinished(TJob* job, size_t sid, ui64 realcost, double now) override; + void OnOperationAbort(TJob* job, size_t sid) override; + +private: + static void* SourceThreadFunc(void* this_); + static void* SchedulerThreadFunc(void* this_); + void Generate(double time, const TMyFlowPtr& flow); + void PushSource(double time, const TMyFlowPtr& flow); + void PopSource(); + void Source(); + + bool IsFlowFrozen(TMyFlow* flow); + void Enqueue(TMyJob* job); + TMyJob* Dequeue(TInstant deadline); + void Scheduler(); +}; + +} diff --git a/ydb/library/shop/sim_shop/myshopmain.cpp b/ydb/library/shop/sim_shop/myshopmain.cpp new file mode 100644 index 00000000000..c78a7c64d43 --- /dev/null +++ b/ydb/library/shop/sim_shop/myshopmain.cpp @@ -0,0 +1,319 @@ +#include "myshop.h" + +#include <ydb/library/shop/sim_shop/config.pb.h> + +#include <ydb/library/shop/probes.h> + +#include <library/cpp/lwtrace/mon/mon_lwtrace.h> + +#include <library/cpp/getopt/last_getopt.h> +#include <library/cpp/monlib/service/monservice.h> +#include <library/cpp/monlib/service/pages/templates.h> +#include <library/cpp/resource/resource.h> + +#include <util/stream/file.h> +#include <util/string/subst.h> +#include <util/system/hp_timer.h> + +#include <google/protobuf/text_format.h> + +using namespace NShopSim; + +#define WWW_CHECK(cond, ...) \ + do { \ + if (!(cond)) { \ + ythrow yexception() << Sprintf(__VA_ARGS__); \ + } \ + } while (false) \ + /**/ + +#define WWW_HTML_INNER(out, body) HTML(out) { \ + out << "<!DOCTYPE html>" << Endl; \ + HTML_TAG() { \ + HEAD() { OutputCommonHeader(out); } \ + BODY() { \ + body; \ + } \ + } \ +} +#define WWW_HTML(out, body) out << NMonitoring::HTTPOKHTML; \ + WWW_HTML_INNER(out, body) + +void OutputCommonHeader(IOutputStream& out) +{ + out << "<link rel=\"stylesheet\" href=\"trace-static/css/bootstrap.min.css\">"; + out << "<script language=\"javascript\" type=\"text/javascript\" src=\"trace-static/js/bootstrap.min.js\"></script>"; + out << "<script language=\"javascript\" type=\"text/javascript\" src=\"trace-static/jquery.min.js\"></script>"; +} + +class TMachineMonPage : public NMonitoring::IMonPage { +public: + TMachineMonPage() + : IMonPage("machine") + {} + + virtual void Output(NMonitoring::IMonHttpRequest& request) { + + const char* urls[] = { + "/trace?fullscreen=y&id=.SIMSHOP_PROVIDER.FifoDequeue.pmachineid={{machineid}}.d1s&aggr=hist&autoscale=y&refresh=3000&bn=queueTimeMs&linesfill=y&mode=analytics&out=flot&pointsshow=n&xn=queueTimeMs&y1=0&x1=0&yns=_count_share", + "/trace?fullscreen=y&id=.SIMSHOP_PROVIDER.MachineExecute.pmachineid={{machineid}}.d1s&aggr=hist&autoscale=y&refresh=3000&bn=execTimeMs&linesfill=y&mode=analytics&out=flot&pointsshow=n&xn=execTimeMs&y1=0&x1=0&yns=_count_share", + "/trace?fullscreen=y&id=.SIMSHOP_PROVIDER.MachineWait.pmachineid={{machineid}}.d1s&aggr=hist&autoscale=y&refresh=3000&bn=waitTimeMs&linesfill=y&mode=analytics&out=flot&pointsshow=n&xn=waitTimeMs&y1=0&x1=0&yns=_count_share", + + "/trace?fullscreen=y&id=.SHOP_PROVIDER.Arrive.pflowctl={{flowctl}}.d200ms:.SHOP_PROVIDER.Depart.pflowctl={{flowctl}}.d200ms:.SIMSHOP_PROVIDER.FifoEnqueue.pmachineid={{machineid}}.d200ms:.SIMSHOP_PROVIDER.FifoDequeue.pmachineid={{machineid}}.d200ms&mode=analytics&out=flot&xn=_thrRTime&y1=0&yns=costInFly:queueCost&cutts=y", + "/trace?fullscreen=y&id=.SIMSHOP_PROVIDER.FifoEnqueue.pmachineid={{machineid}}.d1s&aggr=hist&autoscale=y&refresh=3000&bn=queueCost&linesfill=y&mode=analytics&out=flot&pointsshow=n&xn=queueCost&y1=0&x1=0&yns=_count_share-stack", + "/trace?fullscreen=y&id=.SIMSHOP_PROVIDER.FifoEnqueue.pmachineid={{machineid}}.d1s&aggr=hist&autoscale=y&refresh=3000&bn=queueCost&linesfill=y&mode=analytics&out=flot&pointsshow=n&xn=queueCost&y1=0&x1=0&yns=_count_share", + + "/trace?fullscreen=y&id=.SHOP_PROVIDER.Arrive.pflowctl={{flowctl}}.d200ms:.SHOP_PROVIDER.Depart.pflowctl={{flowctl}}.d200ms:.SIMSHOP_PROVIDER.FifoEnqueue.pmachineid={{machineid}}.d200ms:.SIMSHOP_PROVIDER.FifoDequeue.pmachineid={{machineid}}.d200ms&mode=analytics&out=flot&xn=_thrRTime&y1=0&yns=countInFly:queueLength&cutts=y", + "/trace?fullscreen=y&id=.SIMSHOP_PROVIDER.FifoDequeue.pmachineid={{machineid}}.d200ms&mode=analytics&out=flot&xn=_thrRTime&yns=queueTimeMs&y0=0&pointsshow=n&cutts=y", + "/trace?fullscreen=y&id=.Group.ShopFlowCtlPeriod.pflowctl={{flowctl}}.d10m&mode=analytics&out=flot&pointsshow=n&xn=periodId&yns=badPeriods:goodPeriods:zeroPeriods", + + "/trace?fullscreen=y&id=.Group.ShopFlowCtlPeriod.pflowctl={{flowctl}}.d10m&g=periodId&mode=analytics&out=flot&pointsshow=n&xn=periodId&y1=0&yns=dT_T:dL_L", + "/trace?fullscreen=y&id=.Group.ShopFlowCtlPeriod.pflowctl={{flowctl}}.d10m&g=periodId&mode=analytics&out=flot&pointsshow=n&xn=periodId&y1=0&yns=pv", + "/trace?fullscreen=y&id=.Group.ShopFlowCtlPeriod.pflowctl={{flowctl}}.d10m&g=periodId&linesfill=y&mode=analytics&out=flot&xn=periodId&y1=0&yns=state:window&pointsshow=n", + + "/trace?fullscreen=y&id=.Group.ShopFlowCtlPeriod.pflowctl={{flowctl}}.d10m&g=periodId&mode=analytics&out=flot&xn=periodId&y1=-1&y2=1&yns=error", + "/trace?fullscreen=y&id=.Group.ShopFlowCtlPeriod.pflowctl={{flowctl}}.d10m&g=periodId&linesshow=n&mode=analytics&out=flot&x1=0&xn=window&y1=0&yns=throughput&cutts=y", + "/trace?fullscreen=y&id=.Group.ShopFlowCtlPeriod.pflowctl={{flowctl}}.d10m&g=periodId&mode=analytics&out=flot&xn=periodId&y1=0&yns=throughput:throughputMin:throughputMax:throughputLo:throughputHi&pointsshow=n&legendshow=n", + + "/trace?fullscreen=y&id=.Group.ShopFlowCtlPeriod.pflowctl={{flowctl}}.d10m&g=periodId&mode=analytics&out=flot&xn=periodId&yns=mode", + "/trace?fullscreen=y&id=.Group.ShopFlowCtlPeriod.pflowctl={{flowctl}}.d10m&g=periodId&linesshow=n&mode=analytics&out=flot&x1=0&xn=window&y1=0&yns=latencyAvgMs&cutts=y", + "/trace?fullscreen=y&id=.Group.ShopFlowCtlPeriod.pflowctl={{flowctl}}.d10m&g=periodId&mode=analytics&out=flot&pointsshow=n&xn=periodId&y1=0&yns=latencyAvgMs:latencyAvgMinMs:latencyAvgMaxMs:latencyAvgLoMs:latencyAvgHiMs&legendshow=n", + + "/trace?fullscreen=y&id=.SIMSHOP_PROVIDER.MachineStats.pmachineid={{machineid}}.d10m&mode=analytics&out=flot&xn=_thrRTime&yns=utilization&linesfill=y&y1=0&y2=1&pointsshow=n", + "/trace?fullscreen=y&id=.SIMSHOP_PROVIDER.MachineWorkerStats.pmachineid={{machineid}}.d1s&autoscale=y&mode=analytics&out=flot&xn=_thrRTime&yns=activeTimeMs-stack:totalTimeMs-stack&pointsshow=n&linesfill=y&cutts=y&legendshow=n", + "/trace?fullscreen=y&id=.SHOP_PROVIDER.Depart.pflowctl={{flowctl}}.d1s&mode=analytics&out=flot&xn=realCost&yns=estCost&linesshow=n" + }; + + THashMap<TString, TString> dict; + for (const auto& kv : request.GetParams()) { + dict[kv.first] = kv.second; + } + + TStringStream out; + out << NMonitoring::HTTPOKHTML; + HTML(out) { + out << "<!DOCTYPE html>" << Endl; + HTML_TAG() { + BODY() { + out << "<table border=\"0\" width=\"100%\"><tr>"; + for (size_t i = 0; i < Y_ARRAY_SIZE(urls); i++) { + if (i > 0 && i % 3 == 0) { + out << "</tr><tr>"; + } + out << "<td><iframe style=\"border:none;width:100%;height:100%\" src=\"" + << Subst(urls[i], dict) + << "\"></iframe></td>"; + } + out << "</tr></table>"; + } + } + } + request.Output() << out.Str(); + } +private: + template <class TDict> + TString Subst(const TString& str, const TDict& dict) + { + TString res = str; + for (const auto& kv: dict) { + SubstGlobal(res, "{{" + kv.first + "}}", kv.second); + } + return res; + } +}; + +class TShopMonPage : public NMonitoring::IMonPage { +private: + TMyShop* Shop; + TVector<TString> Templates; +public: + explicit TShopMonPage(TMyShop* shop) + : IMonPage("shop", "Shop") + , Shop(shop) + , Templates({"one", "two"}) + {} + + virtual void Output(NMonitoring::IMonHttpRequest& request) { + + TStringStream out; + try { + if (request.GetParams().Get("mode") == "") { + OutputMain(request, out); + } else if (request.GetParams().Get("mode") == "configure") { + PostConfigure(request, out); + } else { + ythrow yexception() << "Bad request"; + } + } catch (...) { + out.Clear(); + WWW_HTML(out, + out << "<h2>Error</h2><pre>" + << CurrentExceptionMessage() + << Endl; + ) + } + + request.Output() << out.Str(); + } + + void OutputMain(const NMonitoring::IMonHttpRequest& request, IOutputStream& out) + { + out << NMonitoring::HTTPOKHTML; + HTML(out) { + out << "<!DOCTYPE html>" << Endl; + HTML_TAG() { + OutputHead(out); + BODY() { + out << "<h1>Machines</h1>"; + Shop->ForEachMachine([&out] (ui64 machineId, TMyMachine* machine, TFlowCtl* flowctl) { + out << "<span class=\"glyphicon glyphicon-home\"></span> "; + out << "<a href=\"machine?machineid=" << machineId + << "&flowctl=" << flowctl->GetName() + << "\">" << (machine->GetName()? machine->GetName(): ToString(machineId)) << "</a><br>"; + }); + out << "<h1>Configure</h1>"; + OutputConfigure(request, out); + } + } + } + } + + void PostConfigure(const NMonitoring::IMonHttpRequest& request, IOutputStream& out) + { + TConfigPb shopCfg; + bool ok = NProtoBuf::TextFormat::ParseFromString( + request.GetPostParams().Get("config"), + &shopCfg + ); + WWW_CHECK(ok, "config parse failed"); + Shop->Configure(shopCfg); + WWW_HTML(out, + out << + "<div class=\"jumbotron alert-success\">" + "<h2>Configured successfully</h2>" + "</div>" + "<script type=\"text/javascript\">\n" + "$(function() {\n" + " setTimeout(function() {" + " window.location.replace('?');" + " }, 1000);" + "});\n" + "</script>\n"; + ) + } +private: + void OutputHead(IOutputStream& out) + { + out << "<head>\n"; + out << "<title>" << Title << "</title>\n"; + OutputCommonHeader(out); + out << "<script language=\"javascript\" type=\"text/javascript\">"; + out << "var cfg_templates = {"; + bool first = true; + for (auto templ : Templates) { + TString pbtxt = NResource::Find(templ); + SubstGlobal(pbtxt, "\\", "\\\\"); + SubstGlobal(pbtxt, "\n", "\\n"); + SubstGlobal(pbtxt, "'", "\\'"); + out << (first? "": ",") << templ << ":'" << pbtxt << "'"; + first = false; + } + out << "};" + << "function use_cfg_template(templ) {" + << " $('#textareaCfg').text(cfg_templates[templ]);" + << "}" + << "</script>"; + out << "</head>\n"; + } + + void OutputConfigure(const NMonitoring::IMonHttpRequest& request, IOutputStream& out) + { + Y_UNUSED(request); + HTML(out) { + out << "<form class=\"form-horizontal\" action=\"?mode=configure\" method=\"POST\">"; + DIV_CLASS("form-group") { + LABEL_CLASS_FOR("col-sm-1 control-label", "textareaQuery") { out << "Templates"; } + DIV_CLASS("col-sm-11") { + for (auto name : Templates) { + out << "<button type=\"button\" class=\"btn btn-default\" onClick=\"use_cfg_template('" << name << "');\">" << name << "</button> "; + } + } + } + DIV_CLASS("form-group") { + LABEL_CLASS_FOR("col-sm-1 control-label", "textareaQuery") { out << "Config"; } + DIV_CLASS("col-sm-11") { + out << "<textarea class=\"form-control\" id=\"textareaCfg\" name=\"config\" rows=\"20\">" + << Shop->GetConfig().DebugString() + << "</textarea>"; + } + } + DIV_CLASS("form-group") { + DIV_CLASS("col-sm-offset-1 col-sm-11") { + out << "<button type=\"submit\" class=\"btn btn-default\">Apply</button>"; + } + } + out << "</form>"; + } + } +}; + +int main(int argc, char** argv) +{ + try { + NLWTrace::StartLwtraceFromEnv(); +#ifdef _unix_ + signal(SIGPIPE, SIG_IGN); +#endif + +#ifdef _win32_ + WSADATA dummy; + WSAStartup(MAKEWORD(2,2), &dummy); +#endif + + // Configure + int monPort = 8080; + using TMonSrvc = NMonitoring::TMonService2; + THolder<TMonSrvc> MonSrvc; + NLastGetopt::TOpts opts = NLastGetopt::TOpts::Default(); + opts.AddLongOption(0, "mon-port", "port of monitoring service") + .RequiredArgument("port") + .StoreResult(&monPort, monPort); + NLastGetopt::TOptsParseResult res(&opts, argc, argv); + + // Init monservice + MonSrvc.Reset(new TMonSrvc(monPort)); + MonSrvc->Register(new TMachineMonPage()); + NLwTraceMonPage::RegisterPages(MonSrvc->GetRoot()); + NLwTraceMonPage::ProbeRegistry().AddProbesList( + LWTRACE_GET_PROBES(SHOP_PROVIDER)); + NLwTraceMonPage::ProbeRegistry().AddProbesList( + LWTRACE_GET_PROBES(SIMSHOP_PROVIDER)); + + // Start monservice + MonSrvc->Start(); + + // Initial shop config + TConfigPb shopCfg; + bool ok = NProtoBuf::TextFormat::ParseFromString( + NResource::Find("one"), + &shopCfg + ); + Y_ABORT_UNLESS(ok, "config parse failed"); + + // Start shop + TMyShop shop; + MonSrvc->Register(new TShopMonPage(&shop)); + shop.Configure(shopCfg); + + while (true) { + Sleep(TDuration::Seconds(1)); + } + + // Finish + Cout << "bye" << Endl; + return 0; + } catch (...) { + Cerr << "failure: " << CurrentExceptionMessage() << Endl; + return 1; + } +} diff --git a/ydb/library/shop/sim_shop/one.pb.txt b/ydb/library/shop/sim_shop/one.pb.txt new file mode 100644 index 00000000000..5a32986f89d --- /dev/null +++ b/ydb/library/shop/sim_shop/one.pb.txt @@ -0,0 +1,18 @@ +Machine { + Name: "srv" + Scheduler { FIFO { Name: "fifo" } } + WorkerCount: 10 + Wait { Distr { Name: "wait" Gauss { Mean: 0.05 Disp: 0.005 } MinRelValue: 0.1 MaxRelValue: 10 } } + FlowCtl { Name: "srv" } +} + +Source { + Name: "src" + InterArrival { Distr { Name: "ia" Exp { Period: 0.0002 } MaxRelValue: 10 } } + Operation { + Name: "exec" + Machine: "srv" + EstCost { Distr { Gauss { Mean: 0.02 Disp: 0.01 } MinRelValue: 0.1 MaxRelValue: 10 } } + EstCostOverRealCost { Distr { Const: 1.0 } } + } +} diff --git a/ydb/library/shop/sim_shop/two.pb.txt b/ydb/library/shop/sim_shop/two.pb.txt new file mode 100644 index 00000000000..cccf13a8557 --- /dev/null +++ b/ydb/library/shop/sim_shop/two.pb.txt @@ -0,0 +1,26 @@ +Machine { + Name: "srv1" + Scheduler { FIFO { Name: "fifo1" } } + WorkerCount: 10 + Wait { Distr { Name: "wait1" Gauss { Mean: 0.05 Disp: 0.005 } MinRelValue: 0.1 MaxRelValue: 10 } } + FlowCtl { Name: "srv1" } +} + +Machine { + Name: "srv2" + Scheduler { FIFO { Name: "fifo2" } } + WorkerCount: 10 + Wait { Distr { Name: "wait2" Gauss { Mean: 0.05 Disp: 0.005 } MinRelValue: 0.1 MaxRelValue: 10 } } + FlowCtl { Name: "srv2" } +} + +Source { + Name: "src" + InterArrival { Distr { Name: "ia" Exp { Period: 0.0002 } MaxRelValue: 10 } } + Operation { + Name: "exec" + Machine: "srv" + EstCost { Distr { Gauss { Mean: 0.02 Disp: 0.01 } MinRelValue: 0.1 MaxRelValue: 10 } } + EstCostOverRealCost { Distr { Const: 1.0 } } + } +} diff --git a/ydb/library/shop/sim_shop/ya.make b/ydb/library/shop/sim_shop/ya.make new file mode 100644 index 00000000000..68e3add530e --- /dev/null +++ b/ydb/library/shop/sim_shop/ya.make @@ -0,0 +1,25 @@ +PROGRAM() + +RESOURCE( + one.pb.txt one + two.pb.txt two +) + +SRCS( + myshopmain.cpp + myshop.cpp + config.proto +) + +PEERDIR( + ydb/library/drr + library/cpp/lwtrace/mon + ydb/library/shop + library/cpp/getopt + library/cpp/lwtrace + library/cpp/monlib/dynamic_counters + library/cpp/resource + library/cpp/protobuf/util +) + +END() diff --git a/ydb/library/shop/ut/estimator_ut.cpp b/ydb/library/shop/ut/estimator_ut.cpp new file mode 100644 index 00000000000..6aa74e9e559 --- /dev/null +++ b/ydb/library/shop/ut/estimator_ut.cpp @@ -0,0 +1,58 @@ +#include <ydb/library/shop/estimator.h> + +#include <library/cpp/testing/unittest/registar.h> + +using namespace NShop; + +Y_UNIT_TEST_SUITE(ShopEstimator) { + Y_UNIT_TEST(StartLwtrace) { + NLWTrace::StartLwtraceFromEnv(); + } + + Y_UNIT_TEST(MovingAverage) { + TMovingAverageEstimator<1, 2> est(1.0); + UNIT_ASSERT_EQUAL(est.GetAverage(), 1.0); + est.Update(2.0); + UNIT_ASSERT(fabs(est.GetAverage() - 1.5) < 1e-5); + for (int i = 1; i < 100; i++) { + est.Update(2.0); + } + UNIT_ASSERT(fabs(est.GetAverage() - 2.0) < 1e-5); + } + + Y_UNIT_TEST(MovingSlr) { + TMovingSlrEstimator<1, 2> est(1.0, 1.0, 2.0, 2.0); + + // check that initial 2-point guess is a straight line + UNIT_ASSERT(fabs(est.GetEstimation(3.0) - 3.0) < 1e-5); + UNIT_ASSERT(fabs(est.GetEstimation(4.0) - 4.0) < 1e-5); + UNIT_ASSERT(fabs(est.GetEstimation(0.0) - 0.0) < 1e-5); + + // check that update has right 1/2 weight + est.Update(2.5, 2.5); + UNIT_ASSERT(fabs(est.GetEstimation(3.0) - 3.0) < 1e-5); + UNIT_ASSERT(fabs(est.GetEstimation(4.0) - 4.0) < 1e-5); + UNIT_ASSERT(fabs(est.GetEstimation(0.0) - 0.0) < 1e-5); + UNIT_ASSERT(fabs(est.GetAverage() - 2.0) < 1e-5); + + // check history wipe out + for (int i = 1; i < 100; i++) { + est.Update(10.0, 1.0); + est.Update(11.0, 5.0); + } + UNIT_ASSERT(fabs(est.GetEstimation(1.0) - 10.0) < 1e-5); + UNIT_ASSERT(fabs(est.GetEstimation(5.0) - 11.0) < 1e-5); + UNIT_ASSERT(fabs(est.GetEstimation(9.0) - 12.0) < 1e-5); + UNIT_ASSERT(fabs(est.GetEstimation(3.0) - 10.5) < 1e-5); + + // another check history wipe out + for (int i = 1; i < 100; i++) { + est.Update(10.0, 5.0); + est.Update(11.0, 1.0); + } + UNIT_ASSERT(fabs(est.GetEstimation(5.0) - 10.0) < 1e-5); + UNIT_ASSERT(fabs(est.GetEstimation(1.0) - 11.0) < 1e-5); + UNIT_ASSERT(fabs(est.GetEstimation(9.0) - 9.0) < 1e-5); + UNIT_ASSERT(fabs(est.GetEstimation(3.0) - 10.5) < 1e-5); + } +} diff --git a/ydb/library/shop/ut/flowctl_ut.cpp b/ydb/library/shop/ut/flowctl_ut.cpp new file mode 100644 index 00000000000..89d9264442e --- /dev/null +++ b/ydb/library/shop/ut/flowctl_ut.cpp @@ -0,0 +1,523 @@ +#include <ydb/library/shop/flowctl.h> + +#include <library/cpp/testing/unittest/registar.h> +#include <library/cpp/lwtrace/all.h> + +#include <util/generic/cast.h> +#include <util/generic/deque.h> +#include <util/generic/vector.h> +#include <util/string/vector.h> +#include <util/system/env.h> + +#include <functional> + +namespace NFcTest { + +//////////////////////////////////////////////////////////////////////////////// +/// Event-driven simulator core +/// +using namespace NShop; + +class INode; +class TSimulator; + +struct TMyEvent { + double Time = 0; // Time at which event should be executed + bool Quit = false; + INode* Sender = nullptr; + INode* Receiver = nullptr; + + virtual ~TMyEvent() {} + + virtual void Print(IOutputStream& os); +}; + +class INode { +protected: + TSimulator* Sim; + TString Name; +public: + INode(TSimulator* sim) + : Sim(sim) + {} + virtual ~INode() {} + virtual void Receive(TMyEvent* ev) = 0; + void SetName(const TString& name) { Name = name; } + const TString& GetName() const { return Name; } +protected: + void Send(TMyEvent* ev, INode* receiver); + void SendAt(TMyEvent* ev, INode* receiver, double time); + void SendAfter(TMyEvent* ev, INode* receiver, double delay); +}; + +class TSimulator { +public: + double CurrentTime = 0; + struct TEventsCmp { + bool operator()(const TMyEvent* l, const TMyEvent* r) const { + return l->Time > r->Time; + } + }; + TVector<TMyEvent*> Events; + bool SimLog; +public: + TSimulator() + { + TString path = GetEnv("SIMLOG"); + SimLog = path && TString(path) == "y"; + } + + ~TSimulator() + { + for (TMyEvent* ev : Events) { + delete ev; + } + } + + void Schedule(TMyEvent* ev, INode* sender, INode* receiver, double time) + { + if (receiver == nullptr && !ev->Quit) { + delete ev; + } else { + ev->Time = time; + ev->Sender = sender; + ev->Receiver = receiver; + Events.push_back(ev); + PushHeap(Events.begin(), Events.end(), TEventsCmp()); + } + } + + void Run() + { + while (!Events.empty()) { + PopHeap(Events.begin(), Events.end(), TEventsCmp()); + TMyEvent* ev = Events.back(); + Events.pop_back(); + CurrentTime = ev->Time; + if (SimLog) { + ev->Print(Cerr); + Cerr << Endl; + } + if (ev->Quit) { + delete ev; + return; + } else { + ev->Receiver->Receive(ev); + } + } + } + + + void QuitAt(double time) + { + TMyEvent* ev = new TMyEvent(); + ev->Quit = true; + Schedule(ev, nullptr, nullptr, time); + } + + double Now() const { return CurrentTime; } +}; + +void INode::Send(TMyEvent* ev, INode* receiver) +{ + Sim->Schedule(ev, this, receiver, Sim->Now()); +} + +void INode::SendAt(TMyEvent* ev, INode* receiver, double time) +{ + Sim->Schedule(ev, this, receiver, time); +} + +void INode::SendAfter(TMyEvent* ev, INode* receiver, double delay) +{ + Sim->Schedule(ev, this, receiver, Sim->Now() + delay); +} + +//////////////////////////////////////////////////////////////////////////////// +/// Flow control related stuff +/// + +struct TMyRequest : public TMyEvent { + bool Arrived = false; + double ArriveTime = 0; + double DepartTime = 0; + ui64 Cost = 0; + TFcOp FcOp; + + explicit TMyRequest(ui64 cost = 0) + : Cost(cost) + {} + + double ResponseTime() const { return DepartTime - ArriveTime; } + + virtual void Print(IOutputStream& os); +}; + +struct TMyTransition : public TMyEvent { + TFlowCtl::EStateTransition Transition; + bool IsOpen; + bool Arrive; // true = arrive, false = depart + + TMyTransition(TFlowCtl::EStateTransition transition, bool isOpen, bool arrive) + : Transition(transition) + , IsOpen(isOpen) + , Arrive(arrive) + {} + + virtual void Print(IOutputStream& os); +}; + +class TFcSystem : public INode { +protected: + TFlowCtl Fc; + INode* Source = nullptr; +public: + TFcSystem(TSimulator* sim) + : INode(sim) + {} + + void Configure(const TFlowCtlConfig& cfg) + { + Fc.Configure(cfg, Sim->Now()); + } + + void Receive(TMyEvent* ev) override + { + TMyRequest* req = CheckedCast<TMyRequest*>(ev); + if (!req->Arrived) { + req->ArriveTime = Sim->Now(); + req->Arrived = true; + TFlowCtl::EStateTransition st = Fc.ArriveST(req->FcOp, req->Cost, req->ArriveTime); + if (Source) { + Send(new TMyTransition(st, Fc.IsOpen(), true), Source); + } + Enter(req); + } else { + req->DepartTime = Sim->Now(); + TFlowCtl::EStateTransition st = Fc.DepartST(req->FcOp, req->Cost, req->DepartTime); + if (Source) { + Send(new TMyTransition(st, Fc.IsOpen(), false), Source); + } + Exit(req); + } + } + + bool IsOpen() const { return Fc.IsOpen(); } + + void SetSource(INode* source) { Source = source; } +protected: + virtual void Enter(TMyRequest* req) = 0; + virtual void Exit(TMyRequest* req) = 0; +}; + +TFlowCtlConfig DefaultFlowCtlConfig() +{ + TFlowCtlConfig cfg; + return cfg; +} + +class TOneServerWithFifo : public TFcSystem { +public: + double Throughput; // cost/sec + TDeque<TMyRequest*> Queue; + INode* Target = nullptr; + bool Busy = false; + + TOneServerWithFifo(TSimulator* sim, double throughput) + : TFcSystem(sim) + , Throughput(throughput) + { + Y_ABORT_UNLESS(Throughput > 0); + } + + ~TOneServerWithFifo() + { + for (TMyRequest* req : Queue) { + delete req; + } + } + + void Enter(TMyRequest* req) override + { + if (!Busy) { + Execute(req); + Busy = true; + } else { + Queue.push_back(req); + } + } + + void Exit(TMyRequest* req) override + { + Send(req, Target); + if (Queue.empty()) { + Busy = false; + } else { + TMyRequest* nextReq = Queue.back(); + Queue.pop_back(); + Execute(nextReq); + } + } + + void SetTarget(INode* target) { Target = target; } +private: + void Execute(TMyRequest* req) + { + SendAfter(req, this, req->Cost / Throughput); + } +}; + +class TParallelServersWithFifo : public TFcSystem { +public: + double Throughput; // cost/sec per server + ui64 ServersCount; + TDeque<TMyRequest*> Queue; + INode* Target = nullptr; + ui64 BusyCount = 0; + + TParallelServersWithFifo(TSimulator* sim, double throughput, ui64 serversCount) + : TFcSystem(sim) + , Throughput(throughput) + , ServersCount(serversCount) + { + Y_ABORT_UNLESS(Throughput > 0); + } + + ~TParallelServersWithFifo() + { + for (TMyRequest* req : Queue) { + delete req; + } + } + + void Enter(TMyRequest* req) override + { + if (BusyCount < ServersCount) { + Execute(req); + BusyCount++; + } else { + Queue.push_back(req); + } + } + + void Exit(TMyRequest* req) override + { + Send(req, Target); + if (Queue.empty()) { + BusyCount--; + } else { + TMyRequest* nextReq = Queue.back(); + Queue.pop_back(); + Execute(nextReq); + } + } + + void SetTarget(INode* target) { Target = target; } +private: + void Execute(TMyRequest* req) + { + SendAfter(req, this, req->Cost / Throughput); + } +}; + +class TUnlimSource : public INode { +private: + TFcSystem* Target = nullptr; + ui64 Cost; + bool InFly = false; +public: + TUnlimSource(TSimulator* sim, ui64 cost) + : INode(sim) + , Cost(cost) + {} + + void Receive(TMyEvent* ev) override + { + TMyTransition* tr = CheckedCast<TMyTransition*>(ev); + if (tr->Arrive) { + InFly = false; + } + if (tr->IsOpen && !InFly) { + Generate(); + } + delete tr; + } + + void Generate() + { + Y_ABORT_UNLESS(!InFly); + Send(new TMyRequest(Cost), Target); + InFly = true; + } + + void SetTarget(TFcSystem* target) + { + Y_ABORT_UNLESS(!Target); + Target = target; + Generate(); + } +}; + +class TChecker : public INode { +private: + using TCheckFunc = std::function<void(TMyRequest*)>; + TCheckFunc CheckFunc; + INode* Target = nullptr; +public: + TChecker(TSimulator* sim, TCheckFunc f) + : INode(sim) + , CheckFunc(f) + {} + + void Receive(TMyEvent* ev) override + { + TMyRequest* req = CheckedCast<TMyRequest*>(ev); + CheckFunc(req); + Send(req, Target); + } + + void SetTarget(INode* target) { Target = target; } +}; + + +//////////////////////////////////////////////////////////////////////////////// +/// Auxilary stuff +/// + +struct TAuxEvent : public TMyEvent { + std::function<void()> Func; + + explicit TAuxEvent(std::function<void()>&& f) + : Func(std::move(f)) + {} +}; + +class TAuxNode : public INode { +public: + explicit TAuxNode(TSimulator* sim) + : INode(sim) + {} + + void ExecuteAt(double time, std::function<void()>&& f) + { + SendAt(new TAuxEvent(std::move(f)), this, time); + } + + void Receive(TMyEvent* ev) override + { + TAuxEvent* aux = CheckedCast<TAuxEvent*>(ev); + aux->Func(); + delete aux; + } +}; + +void TMyEvent::Print(IOutputStream& os) +{ + if (Quit) { + os << Sprintf("[%010.3lf] QUIT", Time); + } else { + os << Sprintf("[%010.3lf] %s->%s", + Time, + Sender? Sender->GetName().data(): "nullptr", + Receiver? Receiver->GetName().data(): "nullptr" + ); + } +} + +void TMyRequest::Print(IOutputStream& os) +{ + TMyEvent::Print(os); + os << " Arrived=" << Arrived + << " Cost=" << Cost + << " ArriveTime=" << ArriveTime + << " DepartTime=" << DepartTime + ; +} + +void TMyTransition::Print(IOutputStream& os) +{ + TMyEvent::Print(os); + os << " Transition=" << int(Transition) + << " IsOpen=" << IsOpen + << " Arrive=" << Arrive + ; +} + +} // namespace NFcTest + +Y_UNIT_TEST_SUITE(ShopFlowCtl) { + + using namespace NFcTest; + using namespace NShop; + + Y_UNIT_TEST(StartLwtrace) { + NLWTrace::StartLwtraceFromEnv(); + } + + Y_UNIT_TEST(OneOp) { + { + TSimulator sim; + TOneServerWithFifo srv(&sim, 1.0); + srv.Configure(DefaultFlowCtlConfig()); + sim.Schedule(new TMyRequest(3), nullptr, &srv, 5.0); + sim.QuitAt(10.0); + sim.Run(); + UNIT_ASSERT(sim.Now() == 10.0); + } + + { + TSimulator sim; + TOneServerWithFifo srv(&sim, 1.0); + srv.Configure(DefaultFlowCtlConfig()); + sim.Schedule(new TMyRequest(3), nullptr, &srv, 5.0); + sim.Run(); + UNIT_ASSERT(sim.Now() == 8.0); + } + } + + Y_UNIT_TEST(Unlim_D_1_FIFO) { + TSimulator sim; + TUnlimSource src(&sim, 1); + TOneServerWithFifo srv(&sim, 100.0); + srv.Configure(DefaultFlowCtlConfig()); + TChecker chk(&sim, [&sim] (TMyRequest* req) { + if (sim.Now() > 500.0) { + UNIT_ASSERT(req->ResponseTime() < 5.0); + } + }); + + src.SetTarget(&srv); + srv.SetSource(&src); + srv.SetTarget(&chk); + + src.SetName("src"); + srv.SetName("srv"); + chk.SetName("chk"); + + sim.QuitAt(2000.0); + sim.Run(); + } + + Y_UNIT_TEST(Unlim_D_k_FIFO) { + TSimulator sim; + TUnlimSource src(&sim, 1); + TParallelServersWithFifo srv(&sim, 10.0, 10); + srv.Configure(DefaultFlowCtlConfig()); + TChecker chk(&sim, [&sim] (TMyRequest* req) { + if (sim.Now() > 500.0) { + UNIT_ASSERT(req->ResponseTime() < 5.0); + } + }); + + src.SetTarget(&srv); + srv.SetSource(&src); + srv.SetTarget(&chk); + + src.SetName("src"); + srv.SetName("srv"); + chk.SetName("chk"); + + sim.QuitAt(2000.0); + sim.Run(); + } +} diff --git a/ydb/library/shop/ut/lazy_scheduler_ut.cpp b/ydb/library/shop/ut/lazy_scheduler_ut.cpp new file mode 100644 index 00000000000..0027e5c10b5 --- /dev/null +++ b/ydb/library/shop/ut/lazy_scheduler_ut.cpp @@ -0,0 +1,1537 @@ +#include <ydb/library/shop/lazy_scheduler.h> + +#include <util/generic/deque.h> +#include <util/random/random.h> +#include <util/string/vector.h> +#include <util/system/type_name.h> +#include <library/cpp/testing/unittest/registar.h> + +#define SHOP_LAZY_SCHEDULER_UT_PROVIDER(PROBE, EVENT, GROUPS, TYPES, NAMES) \ + PROBE(LazyUtScheduleState, GROUPS(), \ + TYPES(TString), \ + NAMES("state")) \ + PROBE(LazyUtScheduleTask, GROUPS(), \ + TYPES(TString, ui64), \ + NAMES("queue", "cost")) \ + /**/ + +LWTRACE_DECLARE_PROVIDER(SHOP_LAZY_SCHEDULER_UT_PROVIDER) +LWTRACE_DEFINE_PROVIDER(SHOP_LAZY_SCHEDULER_UT_PROVIDER) + +Y_UNIT_TEST_SUITE(ShopLazyScheduler) { + LWTRACE_USING(SHOP_LAZY_SCHEDULER_UT_PROVIDER); + using namespace NShop; + + template <class TRes> + struct TTest { + // We need Priority in tests to break ties in deterministic fashion + struct TMyRes { + using TCost = typename TRes::TCost; + using TTag = typename TRes::TTag; + + struct TKey { + typename TRes::TKey Key; + int Priority; + + bool operator<(const TKey& rhs) const + { + if (Key < rhs.Key) { + return true; + } else if (Key > rhs.Key) { + return false; + } else { + return Priority < rhs.Priority; + } + } + }; + + inline static TKey OffsetKey(TKey key, typename TRes::TTag offset); + template <class ConsumerT> + inline static void SetKey(TKey& key, ConsumerT* consumer); + inline static TTag GetTag(const TKey& key); + inline static TKey MaxKey(); + template <class ConsumerT> + inline static TKey ZeroKey(ConsumerT* consumer); + inline static void ActivateKey(TKey& key, TTag vtime); + }; + + class TMyQueue; + using TMySchedulable = TSchedulable<typename TMyRes::TCost>; + using TMyConsumer = NLazy::TConsumer<TMyRes>; + using TCtx = NLazy::TCtx; + + class TMyTask : public TMySchedulable { + public: + TMyQueue* Queue = nullptr; + public: + explicit TMyTask(typename TMyRes::TCost cost) + { + this->Cost = cost; + } + }; + + class TMyQueue: public TMyConsumer { + public: + TDeque<TMyTask*> Tasks; + int Priority; + TMachineIdx MachineIdx = 0; + public: // Interface for clients + TMyQueue(TWeight w = 1, int priority = 0) + : Priority(priority) + { + this->SetWeight(w); + } + + ~TMyQueue() + { + for (auto i = Tasks.begin(), e = Tasks.end(); i != e; ++i) { + delete *i; + } + } + + void SetMachineIdx(TMachineIdx midx) + { + MachineIdx = midx; + } + + void PushTask(TMyTask* task) + { + if (Empty()) { // Scheduler must be notified on first task in queue + this->Activate(MachineIdx); + } + task->Queue = this; + Tasks.push_back(task); + } + + void PushTaskFront(TMyTask* task) + { + if (Empty()) { // Scheduler must be notified on first task in queue + this->Activate(MachineIdx); + } + task->Queue = this; + Tasks.push_front(task); + } + + using TMyConsumer::Activate; + using TMyConsumer::Deactivate; + + void Activate() + { + this->Activate(MachineIdx); + } + + void Deactivate() + { + this->Deactivate(MachineIdx); + } + + bool Empty() const + { + return Tasks.empty(); + } + + public: // Interface for scheduler + TMySchedulable* PopSchedulable(const TCtx&) override + { + if (Tasks.empty()) { + return nullptr; // denial + } + UNIT_ASSERT(!Tasks.empty()); + TMyTask* task = Tasks.front(); + Tasks.pop_front(); + if (Tasks.empty()) { + this->Deactivate(MachineIdx); + } + return task; + } + + void DeactivateAll() override + { + Deactivate(); + } + }; + + + /////////////////////////////////////////////////////////////////////////////// + + class TMyScheduler; + + struct TMyShopState : public TShopState { + explicit TMyShopState(size_t size = 1) + : TShopState(size) + {} + + void Freeze(TMachineIdx idx); + void Unfreeze(TMachineIdx idx); + void AddScheduler(TMyScheduler* scheduler); + }; + + /////////////////////////////////////////////////////////////////////////////// + + using TQueuePtr = std::shared_ptr<TMyQueue>; + + class TMyScheduler: public NLazy::TScheduler<TMyRes> { + public: + THashMap<TString, TQueuePtr> Queues; + int Priority; + public: + explicit TMyScheduler(TMyShopState& ss, TWeight w = 1, int priority = 0) + : Priority(priority) + { + ss.AddScheduler(this); + this->SetWeight(w); + } + + ~TMyScheduler() + { + this->UpdateCounters(); + } + + void AddQueue(const TString& name, TMachineIdx midx, const TQueuePtr& queue) + { + queue->SetName(name); + queue->SetMachineIdx(midx); + Queues.emplace(name, queue); + this->Attach(queue.get()); + } + + void AddSubScheduler(const TString& name, TMyScheduler* scheduler) + { + scheduler->SetName(name); + this->Attach(scheduler); + } + + void DeleteQueue(const TString& name) + { + auto iter = Queues.find(name); + if (iter != Queues.end()) { + TMyConsumer* consumer = iter->second.get(); + consumer->Detach(); + Queues.erase(iter); + } + } + + void DeleteSubScheduler(TMyScheduler* scheduler) + { + scheduler->Detach(); + } + }; + + /////////////////////////////////////////////////////////////////////////////// + + template <class ConsumerT> + static void SetPriority(int& priority, ConsumerT const* consumer) + { + if (auto queue = dynamic_cast<TMyQueue const*>(consumer)) { + priority = queue->Priority; + } else if (auto sched = dynamic_cast<TMyScheduler const*>(consumer)) { + priority = sched->Priority; + } else { + UNIT_FAIL("unable get priority of object of type: " << TypeName(consumer)); + } + } + + static void Generate(TMyQueue* queue, const TString& tasks) + { + TVector<TString> v = SplitString(tasks, " "); + for (size_t i = 0; i < v.size(); i++) { + queue->PushTask(new TMyTask(FromString<typename TMyRes::TCost>(v[i]))); + } + } + + static void GenerateFront(TMyQueue* queue, const TString& tasks) + { + TVector<TString> v = SplitString(tasks, " "); + for (size_t i = 0; i < v.size(); i++) { + queue->PushTaskFront(new TMyTask(FromString<typename TMyRes::TCost>(v[i]))); + } + } + + static TString Schedule(TMyScheduler& sched, size_t count = size_t(-1), bool printcost = false) + { + TStringStream ss; + while (count--) { + LWPROBE(LazyUtScheduleState, sched.DebugString()); + TMyTask* task = static_cast<TMyTask*>(sched.PopSchedulable()); + if (!task) { + break; + } + LWPROBE(LazyUtScheduleTask, task->Queue->GetName(), task->Cost); + ss << task->Queue->GetName(); + if (printcost) { + ss << task->Cost; + } + delete task; + } + return ss.Str(); + } + }; + + /////////////////////////////////////////////////////////////////////////////// + + template <class TRes> + void TTest<TRes>::TMyShopState::Freeze(TMachineIdx idx) + { + TShopState::Freeze(idx); + } + + template <class TRes> + void TTest<TRes>::TMyShopState::Unfreeze(TMachineIdx idx) + { + TShopState::Unfreeze(idx); + } + + template <class TRes> + void TTest<TRes>::TMyShopState::AddScheduler(TMyScheduler* scheduler) + { + scheduler->SetShopState(this); + } + + /////////////////////////////////////////////////////////////////////////////// + + template <class TRes> + template <class ConsumerT> + inline void TTest<TRes>::TMyRes::SetKey( + typename TTest<TRes>::TMyRes::TKey& key, + ConsumerT* consumer) + { + TRes::SetKey(key.Key, consumer); + TTest<TRes>::SetPriority(key.Priority, consumer); + } + + template <class TRes> + inline typename TRes::TTag TTest<TRes>::TMyRes::GetTag( + const typename TTest<TRes>::TMyRes::TKey& key) + { + return TRes::GetTag(key.Key); + } + + template <class TRes> + inline typename TTest<TRes>::TMyRes::TKey TTest<TRes>::TMyRes::OffsetKey( + typename TTest<TRes>::TMyRes::TKey key, + typename TRes::TTag offset) + { + return { TRes::OffsetKey(key.Key, offset), key.Priority }; + } + + template <class TRes> + inline typename TTest<TRes>::TMyRes::TKey TTest<TRes>::TMyRes::MaxKey() + { + return { TRes::MaxKey(), std::numeric_limits<int>::max() }; + } + + template <class TRes> + template <class ConsumerT> + inline typename TTest<TRes>::TMyRes::TKey TTest<TRes>::TMyRes::ZeroKey( + ConsumerT* consumer) + { + TKey key; + key.Key = TRes::ZeroKey(consumer); + TTest<TRes>::SetPriority(key.Priority, consumer); + return key; + } + + template <class TRes> + inline void TTest<TRes>::TMyRes::ActivateKey( + typename TTest<TRes>::TMyRes::TKey& key, + typename TRes::TTag vtime) + { + TRes::ActivateKey(key.Key, vtime); + // Keep key.Priority unchanged + } + + namespace NSingleResource { + using TRes = NShop::TSingleResource; + using TMyConsumer = TTest<TRes>::TMyConsumer; + using TMyShopState = TTest<TRes>::TMyShopState; + using TMyScheduler = TTest<TRes>::TMyScheduler; + using TMyQueue = TTest<TRes>::TMyQueue; + using TQueuePtr = TTest<TRes>::TQueuePtr; + + template <class... TArgs> + void Generate(TArgs&&... args) + { + TTest<TRes>::Generate(args...); + } + + template <class... TArgs> + void GenerateFront(TArgs&&... args) + { + TTest<TRes>::GenerateFront(args...); + } + + template <class... TArgs> + auto Schedule(TArgs&&... args) + { + return TTest<TRes>::Schedule(args...); + } + } + + Y_UNIT_TEST(StartLwtrace) { + NLWTrace::StartLwtraceFromEnv(); + } + + Y_UNIT_TEST(Simple) { + using namespace NSingleResource; + + TMyShopState state; + TMyScheduler sched(state); + + TMyQueue* A; + TMyQueue* B; + sched.AddQueue("A", 0, TQueuePtr(A = new TMyQueue(1, 0))); + sched.AddQueue("B", 0, TQueuePtr(B = new TMyQueue(1, 1))); + + Generate(A, "100 100 100"); + Generate(B, "100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "ABABAB"); + + Generate(A, "50 50 50 50 50"); + Generate(B, "50 50 50 50 50"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "ABABABABAB"); + + Generate(A, "20 20 20 20 20 20 20"); + Generate(B, "20 20 20 20 20 20 20"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "ABABABABABABAB"); + + Generate(A, "20 20 20 20 20 20 20"); + Generate(B, "50 50 50" ); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "ABAABAAABA"); + + Generate(A, " 100 100"); + Generate(B, "20 20 20 20 20 20 20 "); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "ABBBBBABB"); + + TMyQueue* C; + sched.AddQueue("C", 0, TQueuePtr(C = new TMyQueue(2, 2))); + + Generate(A, "20 20 20 20 20 20"); + Generate(B, "20 20 20 20 20 20"); + Generate(C, "20 20 20 20 20 20 20 20 20 20"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "ABCCABCCABCCABCCABCCAB"); + + sched.DeleteQueue("A"); + // sched.DeleteQueue("B"); // scheduler must delete queue be itself + } + + Y_UNIT_TEST(DoubleActivation) { + using namespace NSingleResource; + + TMyShopState state; + TMyScheduler sched(state); + + TMyQueue* A; + TMyQueue* B; + sched.AddQueue("A", 0, TQueuePtr(A = new TMyQueue(1, 0))); + sched.AddQueue("B", 0, TQueuePtr(B = new TMyQueue(1, 1))); + + Generate(A, "100 100 100"); + Generate(B, "100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 2), "AB"); + + A->Activate(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 2), "AB"); + + A->Activate(); + B->Activate(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "AB"); + } + + Y_UNIT_TEST(SimpleLag) { + using namespace NSingleResource; + + TMyShopState state; + TMyScheduler sched(state); + + TMyQueue* A; + TMyQueue* B; + TMyQueue* C; + TMyQueue* D; + TMyQueue* E; + sched.AddQueue("A", 0, TQueuePtr(A = new TMyQueue(1, 0))); + sched.AddQueue("B", 0, TQueuePtr(B = new TMyQueue(1, 1))); + sched.AddQueue("C", 0, TQueuePtr(C = new TMyQueue(1, 2))); + sched.AddQueue("D", 0, TQueuePtr(D = new TMyQueue(1, 3))); + sched.AddQueue("E", 0, TQueuePtr(E = new TMyQueue(1, 4))); + + Generate(A, "500 500 500"); // 25 + Generate(B, "500 500 500"); // 25 + Generate(C, "500 500 500"); // 25 + Generate(D, "500 500 500"); // 25 + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 8), "ABCDABCD"); + Generate(E, "500"); // 100 + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "EABCD"); + } + + Y_UNIT_TEST(Complex) { + using namespace NSingleResource; + + TMyShopState state(2); + TMyScheduler sched(state); + + TMyQueue* A; + TMyQueue* B; + TMyQueue* C; + TMyQueue* D; + TMyQueue* E; + TMyQueue* X; + sched.AddQueue("A", 0, TQueuePtr(A = new TMyQueue(1, 0))); + sched.AddQueue("B", 0, TQueuePtr(B = new TMyQueue(1, 1))); + sched.AddQueue("C", 0, TQueuePtr(C = new TMyQueue(1, 2))); + sched.AddQueue("D", 0, TQueuePtr(D = new TMyQueue(1, 3))); + sched.AddQueue("E", 0, TQueuePtr(E = new TMyQueue(1, 4))); + sched.AddQueue("X", 1, TQueuePtr(X = new TMyQueue(1, 5))); + + Generate(A, "100 100 100"); + Generate(B, "100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "ABABAB"); + + Generate(A, "100 100 100"); + Generate(X, "100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "AXAXAX"); + + Generate(A, "100 100 100"); + Generate(B, "100 100 100"); + Generate(C, "100 100 100"); + Generate(D, "100 100 100"); + Generate(E, "100 100 100"); + Generate(X, "100 100 100 100 100 100 100 100 100 100 100 100 100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "ABCDEXABCDEXABCDEXXXXXXXXXXXXX"); + } + + Y_UNIT_TEST(ComplexWithWeights) { + using namespace NSingleResource; + + TMyShopState state(2); + TMyScheduler sched(state); + + TMyQueue* A; + TMyQueue* B; + TMyQueue* C; + TMyQueue* D; + sched.AddQueue("A", 0, TQueuePtr(A = new TMyQueue(1, 0))); + sched.AddQueue("B", 0, TQueuePtr(B = new TMyQueue(1, 1))); + sched.AddQueue("C", 1, TQueuePtr(C = new TMyQueue(3, 2))); + sched.AddQueue("D", 1, TQueuePtr(D = new TMyQueue(3, 3))); + + Generate(A, "100 100 100"); // 100 + Generate(B, "100 100 100"); // 100 + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "ABABAB"); + + Generate(C, "100 100 100"); // 100 + Generate(D, "100 100 100"); // 100 + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "CDCDCD"); + + Generate(A, "100 100 100 100 100 100 100 100"); + Generate(C, "100 100 100 100 100 100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "ACCCACCCACCAAAAA"); + + Generate(B, "100 100 100 100 100 100 100 100"); + Generate(D, "100 100 100 100 100 100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "BDDDBDDDBDDBBBBB"); + + Generate(A, "200 200 200 200 200 200 200 200 200 200 200"); + Generate(B, "200 200 200 200 200 200 200 200 200 200 200"); + Generate(C, "200 200 200 200 200 200 200 200 200 200 200"); + Generate(D, "200 200 200 200 200 200 200 200 200 200 200"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "ABCDCDCDABCDCDCDABCDCDCDABCDCDABABABABABABAB"); + + Generate(A, "100 100 100 100 100 100 100 100 100 100 100"); + Generate(B, "100 100 100 100 100 100 100 100 100 100 100"); + Generate(C, "100 100 100 100 100 100 100 100 100 100 100"); + Generate(D, "100 100 100 100 100 100 100 100 100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "ABCDCDCDABCDCDCDABCDCDCDABCDCDABABABABABABAB"); + + Generate(A, "50 50 50 50 50 50 50 50 50 50 50"); + Generate(B, "50 50 50 50 50 50 50 50 50 50 50"); + Generate(C, "50 50 50 50 50 50 50 50 50 50 50"); + Generate(D, "50 50 50 50 50 50 50 50 50 50 50"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "ABCDCDCDABCDCDCDABCDCDCDABCDCDABABABABABABAB"); + } + + Y_UNIT_TEST(OneQueueFrontPush) { + using namespace NSingleResource; + + TMyShopState state(1); + TMyScheduler sched(state); + + TMyQueue* A; + sched.AddQueue("A", 0, TQueuePtr(A = new TMyQueue())); + + Generate(A, "10 20 30"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 1, true), "A10"); + GenerateFront(A, "40"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, size_t(-1), true), "A40A20A30"); + } + + Y_UNIT_TEST(SimpleFrontPush) { + using namespace NSingleResource; + + TMyShopState state(1); + TMyScheduler sched(state); + + TMyQueue* A; + TMyQueue* B; + sched.AddQueue("A", 0, TQueuePtr(A = new TMyQueue(1, 0))); + sched.AddQueue("B", 0, TQueuePtr(B = new TMyQueue(1, 1))); + + Generate(A, "10 30 40"); + Generate(B, "10 20 30"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 2, true), "A10B10"); + GenerateFront(A, "20"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, size_t(-1), true), "A20B20A30B30A40"); + } + + Y_UNIT_TEST(SimpleFrontPushAll) { + using namespace NSingleResource; + + TMyShopState state(1); + TMyScheduler sched(state); + + TMyQueue* A; + TMyQueue* B; + sched.AddQueue("A", 0, TQueuePtr(A = new TMyQueue(1, 0))); + sched.AddQueue("B", 0, TQueuePtr(B = new TMyQueue(1, 1))); + + Generate(A, "10 30 40"); + Generate(B, " 5 30 40"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 2, true), "A10B5"); + GenerateFront(A, "20"); + GenerateFront(B, "20"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 4, true), "B20A20B30A30"); + GenerateFront(A, "10 10"); + GenerateFront(B, "10 8"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, size_t(-1), true), "B8A10B10A10B40A40"); + } + + Y_UNIT_TEST(ComplexFrontPush) { + using namespace NSingleResource; + + TMyShopState state(2); + TMyScheduler sched(state); + + TMyQueue* A; + TMyQueue* B; + TMyQueue* C; + TMyQueue* D; + sched.AddQueue("A", 0, TQueuePtr(A = new TMyQueue(1, 0))); + sched.AddQueue("B", 0, TQueuePtr(B = new TMyQueue(1, 1))); + sched.AddQueue("C", 1, TQueuePtr(C = new TMyQueue(1, 2))); + sched.AddQueue("D", 1, TQueuePtr(D = new TMyQueue(1, 3))); + + Generate(A, "10 20"); + Generate(B, "10 30"); + Generate(C, "10 20"); + Generate(D, "10 20"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 4, true), "A10B10C10D10"); + GenerateFront(B, "20"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, size_t(-1), true), "A20B20C20D20B30"); + } + + Y_UNIT_TEST(ComplexFrontPushAll) { + using namespace NSingleResource; + + TMyShopState state(2); + TMyScheduler sched(state); + + TMyQueue* A; + TMyQueue* B; + TMyQueue* C; + TMyQueue* D; + sched.AddQueue("A", 0, TQueuePtr(A = new TMyQueue(1, 0))); + sched.AddQueue("B", 0, TQueuePtr(B = new TMyQueue(1, 1))); + sched.AddQueue("C", 1, TQueuePtr(C = new TMyQueue(1, 2))); + sched.AddQueue("D", 1, TQueuePtr(D = new TMyQueue(1, 3))); + + Generate(A, "10 36"); + Generate(B, "10 34"); + Generate(C, "10 32"); + Generate(D, "10 30"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 4, true), "A10B10C10D10"); + GenerateFront(A, "20"); + GenerateFront(B, "21"); + GenerateFront(C, "22"); + GenerateFront(D, "23"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, size_t(-1), true), "A20B21C22D23A36B34C32D30"); + } + + Y_UNIT_TEST(ComplexMultiplePush) { + using namespace NSingleResource; + + TMyShopState state(2); + TMyScheduler sched(state); + + TMyQueue* A; + TMyQueue* B; + TMyQueue* C; + TMyQueue* D; + sched.AddQueue("A", 0, TQueuePtr(A = new TMyQueue(1, 0))); + sched.AddQueue("B", 0, TQueuePtr(B = new TMyQueue(1, 1))); + sched.AddQueue("C", 1, TQueuePtr(C = new TMyQueue(1, 2))); + sched.AddQueue("D", 1, TQueuePtr(D = new TMyQueue(1, 3))); + + Generate(A, "5 5 5 5 5"); + Generate(B, "10 10"); + Generate(C, "10 10"); + Generate(D, "10 10"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "ABCDAABCDAA"); + } + + Y_UNIT_TEST(ComplexFrontMultiplePush) { + using namespace NSingleResource; + + TMyShopState state(2); + TMyScheduler sched(state); + + TMyQueue* A; + TMyQueue* B; + TMyQueue* C; + TMyQueue* D; + sched.AddQueue("A", 0, TQueuePtr(A = new TMyQueue(1, 0))); + sched.AddQueue("B", 0, TQueuePtr(B = new TMyQueue(1, 1))); + sched.AddQueue("C", 1, TQueuePtr(C = new TMyQueue(1, 2))); + sched.AddQueue("D", 1, TQueuePtr(D = new TMyQueue(1, 3))); + + Generate(A, "5"); + Generate(B, "10 10"); + Generate(C, "10 10"); + Generate(D, "10 10"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 1, true), "A5"); + GenerateFront(A, "5"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 1, true), "B10"); + GenerateFront(A, "5"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 4, true), "C10D10A5A5"); + GenerateFront(A, "5"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 1, true), "B10"); + GenerateFront(A, "5"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, size_t(-1), true), "C10D10A5A5"); + } + + Y_UNIT_TEST(ComplexCheckEmpty) { + using namespace NSingleResource; + + TMyShopState state(2); + TMyScheduler sched(state); + + TMyQueue* A; + TMyQueue* B; + TMyQueue* C; + TMyQueue* D; + sched.AddQueue("A", 0, TQueuePtr(A = new TMyQueue(1, 0))); + sched.AddQueue("B", 0, TQueuePtr(B = new TMyQueue(1, 1))); + sched.AddQueue("C", 1, TQueuePtr(C = new TMyQueue(1, 2))); + sched.AddQueue("D", 1, TQueuePtr(D = new TMyQueue(1, 3))); + + Generate(A, "5"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, size_t(1), true), "A5"); + + GenerateFront(A, "5"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, size_t(1), true), "A5"); + + GenerateFront(A, "5"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, size_t(1), true), "A5"); + + GenerateFront(A, "5"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, size_t(1), true), "A5"); + + GenerateFront(A, "5"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, size_t(-1), true), "A5"); + } + + Y_UNIT_TEST(SimpleFreeze) { + using namespace NSingleResource; + + TMyShopState state(1); + TMyScheduler sched(state); + + TMyQueue* A; + TMyQueue* B; + sched.AddQueue("A", 0, TQueuePtr(A = new TMyQueue(1, 0))); + sched.AddQueue("B", 0, TQueuePtr(B = new TMyQueue(1, 1))); + + Generate(A, "100 100 100 100"); + Generate(B, "100 100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 4), "ABAB"); + state.Freeze(0); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), ""); + state.Unfreeze(0); + + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "ABAB"); + + Generate(A, "100 100 100 100"); + Generate(B, "100 100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 4), "ABAB"); + state.Freeze(0); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), ""); + state.Unfreeze(0); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 1), "A"); + state.Freeze(0); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), ""); + state.Unfreeze(0); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 1), "B"); + state.Freeze(0); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), ""); + state.Unfreeze(0); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 1), "A"); + state.Freeze(0); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), ""); + state.Unfreeze(0); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 1), "B"); + } + + Y_UNIT_TEST(FreezeSaveTagDiff) { + using namespace NSingleResource; + + TMyShopState state(2); + TMyScheduler sched(state); + + TMyQueue* A; + TMyQueue* B; + TMyQueue* C; + sched.AddQueue("A", 0, TQueuePtr(A = new TMyQueue(1, 0))); + sched.AddQueue("B", 0, TQueuePtr(B = new TMyQueue(1, 1))); + sched.AddQueue("C", 1, TQueuePtr(C = new TMyQueue(1, 2))); + + Generate(A, "100 100 100 100"); + Generate(B, "50 25 25 100 100 100"); + Generate(C, "100 100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 3), "ABC"); + state.Freeze(0); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 2), "CC"); + state.Unfreeze(0); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "BBABCABAB"); + } + + Y_UNIT_TEST(FreezeSaveTagDiffOnIdlePeriod) { + using namespace NSingleResource; + + TMyShopState state(2); + TMyScheduler sched(state); + + TMyQueue* A; + TMyQueue* B; + TMyQueue* C; + sched.AddQueue("A", 0, TQueuePtr(A = new TMyQueue(1, 0))); + sched.AddQueue("B", 0, TQueuePtr(B = new TMyQueue(1, 1))); + sched.AddQueue("C", 1, TQueuePtr(C = new TMyQueue(1, 2))); + + Generate(A, "100 100 100 100"); + Generate(B, "50 25 25 100 100 100"); + Generate(C, "100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 3), "ABC"); + state.Freeze(0); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "CC"); + state.Unfreeze(0); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "BBABABAB"); + } + + Y_UNIT_TEST(FreezeSaveTagDiffOnSeveralIdlePeriods) { + using namespace NSingleResource; + + TMyShopState state(2); + TMyScheduler sched(state); + + TMyQueue* A; + TMyQueue* B; + TMyQueue* C; + sched.AddQueue("A", 0, TQueuePtr(A = new TMyQueue(1, 0))); + sched.AddQueue("B", 0, TQueuePtr(B = new TMyQueue(1, 1))); + sched.AddQueue("C", 1, TQueuePtr(C = new TMyQueue(1, 2))); + + Generate(A, "100 100 100 100"); + Generate(B, "50 25 25 100 100 100"); + Generate(C, "100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 3), "ABC"); + state.Freeze(0); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "CC"); + Generate(C, "100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "CCC"); + Generate(C, "100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "CCC"); + state.Unfreeze(0); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "BBABABAB"); + } + + Y_UNIT_TEST(ComplexFreeze) { + using namespace NSingleResource; + + TMyShopState state(2); + TMyScheduler sched(state); + + TMyQueue* A; + TMyQueue* B; + TMyQueue* C; + TMyQueue* D; + sched.AddQueue("A", 0, TQueuePtr(A = new TMyQueue(1, 0))); + sched.AddQueue("B", 0, TQueuePtr(B = new TMyQueue(1, 1))); + sched.AddQueue("C", 1, TQueuePtr(C = new TMyQueue(1, 2))); + sched.AddQueue("D", 1, TQueuePtr(D = new TMyQueue(1, 3))); + + Generate(A, "100 100 100 100"); + Generate(B, "100 100 100 100"); + Generate(C, "100 100 100 100"); + Generate(D, "100 100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 4), "ABCD"); + state.Freeze(0); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 4), "CDCD"); + state.Unfreeze(0); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "ABCDABAB"); + + Generate(A, "100 100 100 100"); + Generate(B, "100 100 100 100"); + Generate(C, "100 100 100 100"); + Generate(D, "100 100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 4), "ABCD"); + state.Freeze(0); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 2), "CD"); + state.Freeze(1); + state.Unfreeze(1); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 2), "CD"); + state.Unfreeze(0); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "ABCDABAB"); + + Generate(A, "100 100 100 100"); + Generate(B, "100 100 100 100"); + Generate(C, "100 100 100 100"); + Generate(D, "100 100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 4), "ABCD"); + state.Freeze(0); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 2), "CD"); + state.Freeze(1); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), ""); + state.Unfreeze(1); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 2), "CD"); + state.Unfreeze(0); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "ABCDABAB"); + + Generate(A, "100 100 100 100"); + Generate(B, "100 100 100 100"); + Generate(C, "100 100 100 100"); + Generate(D, "100 100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 4), "ABCD"); + state.Freeze(0); + state.Freeze(1); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), ""); + state.Unfreeze(0); + state.Unfreeze(1); + state.Freeze(0); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 4), "CDCD"); + state.Unfreeze(0); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "ABCDABAB"); + + Generate(A, "100 100 100 100"); + Generate(B, "100 100 100 100"); + Generate(C, "100 100 100 100"); + Generate(D, "100 100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 4), "ABCD"); + state.Freeze(0); + state.Freeze(1); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), ""); + state.Unfreeze(1); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 1), "C"); + state.Freeze(1); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), ""); + state.Unfreeze(1); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 1), "D"); + state.Freeze(1); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), ""); + state.Unfreeze(1); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 1), "C"); + state.Freeze(1); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), ""); + state.Unfreeze(1); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 1), "D"); + state.Unfreeze(0); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "ABCDABAB"); + } + + Y_UNIT_TEST(ComplexLocalBusyPeriods) { + using namespace NSingleResource; + + TMyShopState state(2); + TMyScheduler sched(state); + + TMyQueue* A; + TMyQueue* B; + TMyQueue* C; + TMyQueue* D; + sched.AddQueue("A", 0, TQueuePtr(A = new TMyQueue(1, 0))); + sched.AddQueue("B", 0, TQueuePtr(B = new TMyQueue(1, 1))); + sched.AddQueue("C", 1, TQueuePtr(C = new TMyQueue(1, 2))); + sched.AddQueue("D", 1, TQueuePtr(D = new TMyQueue(1, 3))); + + Generate(A, "100 100"); + Generate(B, "100 100"); + Generate(C, "100 100 100"); + Generate(D, "100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 4), "ABCD"); + state.Freeze(0); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 4), "CDCD"); + for (int i = 0; i < 10; i++) { + Generate(C, "50 50 50 40"); + Generate(D, "100 100 "); + UNIT_ASSERT_STRINGS_EQUAL_C(Schedule(sched), "CDCCDC", i); + } + state.Unfreeze(0); + Generate(C, "50 50 50"); + Generate(D, "50 50 50"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "CDCDABCD"); + } + + Y_UNIT_TEST(ComplexGlobalBusyPeriods) { + using namespace NSingleResource; + + TMyShopState state(2); + TMyScheduler sched(state); + + TMyQueue* A; + TMyQueue* B; + TMyQueue* C; + TMyQueue* D; + sched.AddQueue("A", 0, TQueuePtr(A = new TMyQueue(1, 0))); + sched.AddQueue("B", 0, TQueuePtr(B = new TMyQueue(1, 1))); + sched.AddQueue("C", 1, TQueuePtr(C = new TMyQueue(1, 2))); + sched.AddQueue("D", 1, TQueuePtr(D = new TMyQueue(1, 3))); + + for (int j = 0; j < 5; j++) { + Generate(A, "100 100"); + Generate(B, "100 100"); + Generate(C, "100 100 100"); + Generate(D, "100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 4), "ABCD"); + state.Freeze(0); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 4), "CDCD"); + for (int i = 0; i < 5; i++) { + Generate(C, "50 50 50 40"); + Generate(D, "100 100 "); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "CDCCDC"); + } + state.Unfreeze(0); + Generate(C, "50 50 25"); + Generate(D, "50 50 50"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "CDCDABCD"); + } + } + + Y_UNIT_TEST(VeryComplexFreeze) { + using namespace NSingleResource; + + TMyShopState state(3); + TMyScheduler sched(state); + + TMyQueue* A; + TMyQueue* B; + TMyQueue* C; + TMyQueue* D; + TMyQueue* E; + TMyQueue* F; + sched.AddQueue("A", 0, TQueuePtr(A = new TMyQueue(1, 0))); + sched.AddQueue("B", 0, TQueuePtr(B = new TMyQueue(1, 1))); + sched.AddQueue("C", 1, TQueuePtr(C = new TMyQueue(1, 2))); + sched.AddQueue("D", 1, TQueuePtr(D = new TMyQueue(1, 3))); + sched.AddQueue("E", 2, TQueuePtr(E = new TMyQueue(1, 4))); + sched.AddQueue("F", 2, TQueuePtr(F = new TMyQueue(1, 5))); + + for (int j = 0; j < 5; j++) { + Generate(A, "100 100"); + Generate(B, "100 100"); + Generate(C, "100 100 100"); + Generate(D, "100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 4), "ABCD"); + state.Freeze(0); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 4), "CDCD"); + for (int i = 0; i < 5; i++) { + Generate(C, "50 50 50 40"); + Generate(D, "100 100 "); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "CDCCDC"); + Generate(E, "50 50 50 40"); + Generate(F, "100 100 "); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "EFEEFE"); + } + for (int i = 0; i < 5; i++) { + Generate(C, "50 50 50 50"); + Generate(D, "100 100 "); + Generate(E, "50 50 50 50 50 50"); + Generate(F, "100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 6), "CDEFCE"); + state.Freeze(1); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 3), "EFE"); + state.Unfreeze(1); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "CDEFCE"); + } + state.Unfreeze(0); + Generate(C, "50 50 55"); + Generate(D, "50 50 40"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "CDCDABCD"); + } + } + + Y_UNIT_TEST(SimplestClear) { + using namespace NSingleResource; + + TMyShopState state(1); + TMyScheduler sched(state); + + TMyQueue* A; + sched.AddQueue("A", 0, TQueuePtr(A = new TMyQueue(1, 0))); + + Generate(A, "100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "AAA"); + + Generate(A, "100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "AAA"); + + sched.Clear(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), ""); + } + + Y_UNIT_TEST(SimpleClear) { + using namespace NSingleResource; + + TMyShopState state(1); + TMyScheduler sched(state); + + TMyQueue* A; + TMyQueue* B; + sched.AddQueue("A", 0, TQueuePtr(A = new TMyQueue(1, 0))); + sched.AddQueue("B", 0, TQueuePtr(B = new TMyQueue(1, 1))); + + Generate(A, "100 100 100"); + Generate(B, "100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 4), "ABAB"); + + sched.Clear(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), ""); + + sched.Attach(A); + A->Activate(); + sched.Attach(B); + B->Activate(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "AB"); + } + + Y_UNIT_TEST(ComplexClear) { + using namespace NSingleResource; + + TMyShopState state(2); + TMyScheduler sched(state); + + TMyQueue* A; + TMyQueue* B; + sched.AddQueue("A", 0, TQueuePtr(A = new TMyQueue(1, 0))); + sched.AddQueue("B", 1, TQueuePtr(B = new TMyQueue(1, 1))); + + Generate(A, "100 100 100"); + Generate(B, "100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 4), "ABAB"); + + sched.Clear(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), ""); + + sched.Attach(A); + A->Activate(); + sched.Attach(B); + B->Activate(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "AB"); + } + + Y_UNIT_TEST(SimpleFreezeClear) { + using namespace NSingleResource; + + TMyShopState state(1); + TMyScheduler sched(state); + + TMyQueue* A; + sched.AddQueue("A", 0, TQueuePtr(A = new TMyQueue(1, 0))); + + Generate(A, "100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 1), "A"); + + state.Freeze(0); + sched.Clear(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), ""); + + sched.Attach(A); + A->Activate(); + state.Unfreeze(0); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "AA"); + } + + Y_UNIT_TEST(EmptyFreezeClear) { + using namespace NSingleResource; + + TMyShopState state(1); + TMyScheduler sched(state); + + TMyQueue* A; + sched.AddQueue("A", 0, TQueuePtr(A = new TMyQueue(1, 0))); + + Generate(A, "100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "AAA"); + + state.Freeze(0); + sched.Clear(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), ""); + + state.Unfreeze(0); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), ""); + } + + Y_UNIT_TEST(ComplexFreezeClear) { + using namespace NSingleResource; + + TMyShopState state(2); + TMyScheduler sched(state); + + TMyQueue* A; + TMyQueue* B; + sched.AddQueue("A", 0, TQueuePtr(A = new TMyQueue(1, 0))); + sched.AddQueue("B", 1, TQueuePtr(B = new TMyQueue(1, 1))); + + Generate(A, "100 100 100"); + Generate(B, "100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 4), "ABAB"); + + state.Freeze(0); + sched.Clear(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), ""); + + sched.Attach(A); + sched.Attach(B); + A->Activate(); // double activation should be ignored gracefully + B->Activate(); + state.Unfreeze(0); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "AB"); + } + + Y_UNIT_TEST(DeleteQueue) { + using namespace NSingleResource; + + TMyShopState state(1); + TMyScheduler sched(state); + + TMyQueue* A; + TMyQueue* B; + sched.AddQueue("A", 0, TQueuePtr(A = new TMyQueue(1, 0))); + sched.AddQueue("B", 0, TQueuePtr(B = new TMyQueue(1, 1))); + + Generate(A, "100 100 100 100 100"); + Generate(B, "100 100 100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 2), "AB"); + + TMyQueue* C; + sched.AddQueue("C", 0, TQueuePtr(C = new TMyQueue(1, 2))); + Generate(C, "100 100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 4), "CABC"); + + sched.DeleteQueue("C"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 4), "ABAB"); + + sched.AddQueue("C", 0, TQueuePtr(C = new TMyQueue(1, 2))); + Generate(C, "100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "CABC"); + } + + Y_UNIT_TEST(SimpleActDeact) { + using namespace NSingleResource; + + TMyShopState state(1); + TMyScheduler sched(state); + + TMyQueue* A; + TMyQueue* B; + sched.AddQueue("A", 0, TQueuePtr(A = new TMyQueue(1, 0))); + sched.AddQueue("B", 0, TQueuePtr(B = new TMyQueue(1, 1))); + + Generate(A, "100 100 100"); + Generate(B, "100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 4), "ABAB"); + + A->Deactivate(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "B"); + + A->Activate(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "A"); + } + + Y_UNIT_TEST(TotalActDeact) { + using namespace NSingleResource; + + TMyShopState state(1); + TMyScheduler sched(state); + + TMyQueue* A; + TMyQueue* B; + sched.AddQueue("A", 0, TQueuePtr(A = new TMyQueue(1, 0))); + sched.AddQueue("B", 0, TQueuePtr(B = new TMyQueue(1, 1))); + + Generate(A, "100 100 100"); + Generate(B, "100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 4), "ABAB"); + + A->Deactivate(); + B->Deactivate(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), ""); + + B->Activate(); + A->Activate(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "AB"); + } + + Y_UNIT_TEST(TotalActDeactReordered) { + using namespace NSingleResource; + + TMyShopState state(1); + TMyScheduler sched(state); + + TMyQueue* A; + TMyQueue* B; + sched.AddQueue("A", 0, TQueuePtr(A = new TMyQueue(1, 0))); + sched.AddQueue("B", 0, TQueuePtr(B = new TMyQueue(1, 1))); + + Generate(A, "100 100 100"); + Generate(B, "100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 4), "ABAB"); + + B->Deactivate(); + A->Deactivate(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), ""); + + A->Activate(); + B->Activate(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "AB"); + } + + Y_UNIT_TEST(ComplexActDeact) { + using namespace NSingleResource; + + TMyShopState state(1); + TMyScheduler sched(state); + + TMyQueue* A; + TMyQueue* B; + sched.AddQueue("A", 0, TQueuePtr(A = new TMyQueue(1, 0))); + sched.AddQueue("B", 0, TQueuePtr(B = new TMyQueue(1, 1))); + + Generate(A, "100 100 100"); + Generate(B, "100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 4), "ABAB"); + + B->Deactivate(); + A->Deactivate(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), ""); + + sched.ActivateConsumers([=] (TMyConsumer* c) { return c == A? 0: NLazy::DoNotActivate; }); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "A"); + + sched.ActivateConsumers([=] (TMyConsumer* c) { return c == B? 0: NLazy::DoNotActivate; }); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "B"); + } + + Y_UNIT_TEST(ComplexDeactDoubleAct) { + using namespace NSingleResource; + + TMyShopState state(1); + TMyScheduler sched(state); + + TMyQueue* A; + TMyQueue* B; + TMyQueue* C; + TMyQueue* D; + sched.AddQueue("A", 0, TQueuePtr(A = new TMyQueue(1, 0))); + sched.AddQueue("B", 0, TQueuePtr(B = new TMyQueue(1, 1))); + sched.AddQueue("C", 0, TQueuePtr(C = new TMyQueue(1, 2))); + sched.AddQueue("D", 0, TQueuePtr(D = new TMyQueue(1, 3))); + + Generate(A, "100 100 100"); + Generate(B, "100 100 100"); + Generate(C, "100 100 100"); + Generate(D, "100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 6), "ABCDAB"); + + A->Deactivate(); + B->Deactivate(); + C->Deactivate(); + D->Deactivate(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), ""); + + sched.ActivateConsumers([=] (TMyConsumer* c) { return c == A || c == D? 0: NLazy::DoNotActivate; }); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "ADD"); + + sched.ActivateConsumers([=] (TMyConsumer* c) { return c == B || c == C? 0: NLazy::DoNotActivate; }); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "BCC"); + } + + + Y_UNIT_TEST(AttachIntoFreezableAfterDeact) { + using namespace NSingleResource; + + TMyShopState state(1); + TMyScheduler sched(state); + + TMyQueue* A; + TMyQueue* B; + TMyQueue* C; + TMyQueue* D; + sched.AddQueue("A", 0, TQueuePtr(A = new TMyQueue(1, 0))); + sched.AddQueue("B", 0, TQueuePtr(B = new TMyQueue(1, 1))); + sched.AddQueue("C", 0, TQueuePtr(C = new TMyQueue(1, 2))); + + Generate(A, "100 100 100"); + Generate(B, "100 100 100"); + Generate(C, "100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 6), "ABCABC"); + + C->Deactivate(); + + sched.AddQueue("D", 0, TQueuePtr(D = new TMyQueue(1, 3))); + Generate(D, "100"); + + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "DAB"); + + C->Activate(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "C"); + } + + Y_UNIT_TEST(Random) { + using namespace NSingleResource; + + for (int cycle = 0; cycle < 50; cycle++) { + TMyShopState state(1); + TMyScheduler sched(state); + + TVector<TQueuePtr> queues; + for (int i = 0; i < 50; i++) { + TQueuePtr queue(new TMyQueue(1, 0)); + queues.push_back(queue); + sched.AddQueue(Sprintf("Q%03d", i), 0, queue); + } + + for (int i = 0; i < 10000; i++) { + if (RandomNumber<size_t>(10)) { + size_t i = RandomNumber<size_t>(queues.size()); + TMyQueue* queue = queues[i].get(); + switch (RandomNumber<size_t>(3)) { + case 0: + Generate(queue, "100"); + queue->Activate(); + break; + case 2: + queue->Deactivate(); + break; + } + } else { + Schedule(sched, 3); + } + } + } + } + + Y_UNIT_TEST(SimpleHierarchy) { + using namespace NSingleResource; + + TMyShopState state(1); + TMyScheduler schedR(state); + + TMyScheduler schedG(state, 1, 0); + TMyQueue* A; + TMyQueue* B; + schedG.AddQueue("A", 0, TQueuePtr(A = new TMyQueue(1, 0))); + schedG.AddQueue("B", 0, TQueuePtr(B = new TMyQueue(1, 1))); + + TMyScheduler schedH(state, 1, 0); + TMyQueue* C; + TMyQueue* D; + schedH.AddQueue("C", 0, TQueuePtr(C = new TMyQueue(1, 0))); + schedH.AddQueue("D", 0, TQueuePtr(D = new TMyQueue(1, 1))); + + schedR.AddSubScheduler("G", &schedG); + schedR.AddSubScheduler("H", &schedH); + + Generate(A, "100 100 100"); + Generate(B, "100 100 100"); + Generate(C, "100 100 100"); + Generate(D, "100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(schedR), "ACBDACBDACBD"); + } + + Y_UNIT_TEST(SimpleHierarchyFreezeSubTree) { + using namespace NSingleResource; + + TMyShopState state(2); + TMyScheduler schedR(state); + + TMyScheduler schedG(state, 1, 0); + TMyQueue* A; + TMyQueue* B; + schedG.AddQueue("A", 0, TQueuePtr(A = new TMyQueue(1, 0))); + schedG.AddQueue("B", 0, TQueuePtr(B = new TMyQueue(1, 1))); + + TMyScheduler schedH(state, 1, 0); + TMyQueue* C; + TMyQueue* D; + schedH.AddQueue("C", 1, TQueuePtr(C = new TMyQueue(1, 0))); + schedH.AddQueue("D", 1, TQueuePtr(D = new TMyQueue(1, 1))); + + schedR.AddSubScheduler("G", &schedG); + schedR.AddSubScheduler("H", &schedH); + + Generate(A, "100 100 100"); + Generate(B, "100 100 100"); + Generate(C, "100 100 100"); + Generate(D, "100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(schedR, 4), "ACBD"); + state.Freeze(0); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(schedR), "CDCD"); + state.Unfreeze(0); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(schedR), "ABAB"); + } + + Y_UNIT_TEST(SimpleHierarchyFreezeLeafsOnly) { + using namespace NSingleResource; + + TMyShopState state(2); + TMyScheduler schedR(state); + + TMyScheduler schedG(state, 1, 0); + TMyQueue* A; + TMyQueue* B; + schedG.AddQueue("A", 0, TQueuePtr(A = new TMyQueue(1, 0))); + schedG.AddQueue("B", 1, TQueuePtr(B = new TMyQueue(1, 1))); + + TMyScheduler schedH(state, 1, 0); + TMyQueue* C; + TMyQueue* D; + schedH.AddQueue("C", 0, TQueuePtr(C = new TMyQueue(1, 0))); + schedH.AddQueue("D", 1, TQueuePtr(D = new TMyQueue(1, 1))); + + schedR.AddSubScheduler("G", &schedG); + schedR.AddSubScheduler("H", &schedH); + + Generate(A, "100 100 100"); + Generate(B, "100 100 100"); + Generate(C, "100 100 100"); + Generate(D, "100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(schedR, 4), "ACBD"); + state.Freeze(0); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(schedR), "BDBD"); + state.Unfreeze(0); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(schedR), "ACAC"); + } +} + +template<> +void Out<NTestSuiteShopLazyScheduler::TTest<NShop::TSingleResource>::TMyRes::TKey>( + IOutputStream& out, + const NTestSuiteShopLazyScheduler::TTest<NShop::TSingleResource>::TMyRes::TKey& key) +{ + out << "<" << key.Key << "|" << key.Priority << ">"; +} + +template<> +void Out<NTestSuiteShopLazyScheduler::TTest<NShop::TPairDrf>::TMyRes::TKey>( + IOutputStream& out, + const NTestSuiteShopLazyScheduler::TTest<NShop::TPairDrf>::TMyRes::TKey& key) +{ + out << "<" << key.Key << "|" << key.Priority << ">"; +} diff --git a/ydb/library/shop/ut/scheduler_ut.cpp b/ydb/library/shop/ut/scheduler_ut.cpp new file mode 100644 index 00000000000..619fde5b31f --- /dev/null +++ b/ydb/library/shop/ut/scheduler_ut.cpp @@ -0,0 +1,1403 @@ +#include <ydb/library/shop/scheduler.h> + +#include <util/generic/deque.h> +#include <util/random/random.h> +#include <util/string/vector.h> +#include <library/cpp/testing/unittest/registar.h> + +Y_UNIT_TEST_SUITE(ShopScheduler) { + using namespace NShop; + + template <class TRes> + struct TTest { + // We need Priority in tests to break ties in deterministic fashion + struct TMyRes { + using TCost = typename TRes::TCost; + using TTag = typename TRes::TTag; + + struct TKey { + typename TRes::TKey Key; + int Priority; + + bool operator<(const TKey& rhs) const + { + if (Key < rhs.Key) { + return true; + } else if (Key > rhs.Key) { + return false; + } else { + return Priority < rhs.Priority; + } + } + }; + + inline static TKey OffsetKey(TKey key, typename TRes::TTag offset); + template <class ConsumerT> + inline static void SetKey(TKey& key, ConsumerT* consumer); + }; + + class TMyQueue; + using TMySchedulable = TSchedulable<typename TMyRes::TCost>; + using TMyConsumer = TConsumer<TMyRes>; + using TMyFreezable = TFreezable<TMyRes>; + + class TMyTask : public TMySchedulable { + public: + TMyQueue* Queue = nullptr; + public: + explicit TMyTask(typename TMyRes::TCost cost) + { + this->Cost = cost; + } + }; + + class TMyQueue: public TMyConsumer { + public: + TDeque<TMyTask*> Tasks; + int Priority; + bool AllowEmpty = false; + public: // Interface for clients + TMyQueue(TWeight w = 1, int priority = 0) + : Priority(priority) + { + this->SetWeight(w); + } + + ~TMyQueue() + { + for (auto i = Tasks.begin(), e = Tasks.end(); i != e; ++i) { + delete *i; + } + } + + void PushTask(TMyTask* task) + { + if (Empty()) { // Scheduler must be notified on first task in queue + this->Activate(); + } + task->Queue = this; + Tasks.push_back(task); + } + + void PushTaskFront(TMyTask* task) + { + if (Empty()) { // Scheduler must be notified on first task in queue + this->Activate(); + } + task->Queue = this; + Tasks.push_front(task); + } + public: // Interface for scheduler + TMySchedulable* PopSchedulable() override + { + if (AllowEmpty && Tasks.empty()) { + return nullptr; + } + UNIT_ASSERT(!Tasks.empty()); + TMyTask* task = Tasks.front(); + Tasks.pop_front(); + return task; + } + + bool Empty() const override + { + return Tasks.empty(); + } + }; + + /////////////////////////////////////////////////////////////////////////////// + + using TQueuePtr = std::shared_ptr<TMyQueue>; + using TFreezablePtr = std::shared_ptr<TMyFreezable>; + + class TMyScheduler: public TScheduler<TMyRes> { + public: + THashMap<TString, TFreezablePtr> Freezables; + THashMap<TString, TQueuePtr> Queues; + bool AllowEmpty; + int Priority; + public: + explicit TMyScheduler(bool allowEmpty, TWeight w = 1, int priority = 0) + : AllowEmpty(allowEmpty) + , Priority(priority) + { + this->SetWeight(w); + } + + explicit TMyScheduler(TWeight w = 1, int priority = 0) + : TMyScheduler(false, w, priority) + {} + + ~TMyScheduler() + { + this->UpdateCounters(); + } + + void AddFreezable(const TString& name, const TFreezablePtr& freezable) + { + freezable->SetName(name); + freezable->SetScheduler(this); + Freezables.emplace(name, freezable); + } + + void DeleteFreezable(const TString& name) + { + auto iter = Freezables.find(name); + if (iter != Freezables.end()) { + TMyFreezable* freezable = iter->second.Get(); + freezable->Deactivate(); + Freezables.erase(iter); + } + } + + void AddQueue(const TString& name, TMyFreezable* freezable, const TQueuePtr& queue) + { + queue->SetName(name); + queue->SetScheduler(this); + queue->SetFreezable(freezable); + queue->AllowEmpty = AllowEmpty; + Queues.emplace(name, queue); + } + + void AddSubScheduler(const TString& name, TMyFreezable* freezable, TMyScheduler* scheduler) + { + scheduler->SetName(name); + scheduler->SetScheduler(this); + scheduler->SetFreezable(freezable); + scheduler->AllowEmpty = AllowEmpty; + } + + void DeleteQueue(const TString& name) + { + auto iter = Queues.find(name); + if (iter != Queues.end()) { + TMyConsumer* consumer = iter->second.get(); + consumer->Detach(); + Queues.erase(iter); + } + } + + void DeleteSubScheduler(TMyScheduler* scheduler) + { + scheduler->Detach(); + } + }; + + /////////////////////////////////////////////////////////////////////////////// + + static void Generate(TMyQueue* queue, const TString& tasks) + { + TVector<TString> v = SplitString(tasks, " "); + for (size_t i = 0; i < v.size(); i++) { + queue->PushTask(new TMyTask(FromString<typename TMyRes::TCost>(v[i]))); + } + } + + static void GenerateFront(TMyQueue* queue, const TString& tasks) + { + TVector<TString> v = SplitString(tasks, " "); + for (size_t i = 0; i < v.size(); i++) { + queue->PushTaskFront(new TMyTask(FromString<typename TMyRes::TCost>(v[i]))); + } + } + + static TString Schedule(TMyScheduler& sched, size_t count = size_t(-1), bool printcost = false) + { + TStringStream ss; + while (count-- && !sched.Empty()) { + TMyTask* task = static_cast<TMyTask*>(sched.PopSchedulable()); + if (sched.AllowEmpty && !task) { + break; + } + ss << task->Queue->GetName(); + if (printcost) { + ss << task->Cost; + } + delete task; + } + if (count != size_t(-1)) { + UNIT_ASSERT(sched.Empty()); + } + return ss.Str(); + } + }; + + /////////////////////////////////////////////////////////////////////////////// + + template <class TRes> + template <class ConsumerT> + inline void TTest<TRes>::TMyRes::SetKey( + typename TTest<TRes>::TMyRes::TKey& key, + ConsumerT* consumer) + { + TRes::SetKey(key.Key, consumer); + if (auto queue = dynamic_cast<TTest<TRes>::TMyQueue*>(consumer)) { + key.Priority = queue->Priority; + } else if (auto sched = dynamic_cast<TTest<TRes>::TMyScheduler*>(consumer)) { + key.Priority = sched->Priority; + } + } + + template <class TRes> + inline typename TTest<TRes>::TMyRes::TKey TTest<TRes>::TMyRes::OffsetKey( + typename TTest<TRes>::TMyRes::TKey key, + typename TRes::TTag offset) + { + return { TRes::OffsetKey(key.Key, offset), key.Priority }; + } + + namespace NSingleResource { + using TRes = NShop::TSingleResource; + using TMyConsumer = TTest<TRes>::TMyConsumer; + using TMyFreezable = TTest<TRes>::TMyFreezable; + using TMyScheduler = TTest<TRes>::TMyScheduler; + using TMyQueue = TTest<TRes>::TMyQueue; + using TQueuePtr = TTest<TRes>::TQueuePtr; + using TFreezablePtr = TTest<TRes>::TFreezablePtr; + + template <class... TArgs> + void Generate(TArgs&&... args) + { + TTest<TRes>::Generate(args...); + } + + template <class... TArgs> + void GenerateFront(TArgs&&... args) + { + TTest<TRes>::GenerateFront(args...); + } + + template <class... TArgs> + auto Schedule(TArgs&&... args) + { + return TTest<TRes>::Schedule(args...); + } + } + + namespace NPairDrf { + using TRes = NShop::TPairDrf; + using TMyConsumer = TTest<TRes>::TMyConsumer; + using TMyFreezable = TTest<TRes>::TMyFreezable; + using TMyScheduler = TTest<TRes>::TMyScheduler; + using TMyQueue = TTest<TRes>::TMyQueue; + using TQueuePtr = TTest<TRes>::TQueuePtr; + using TFreezablePtr = TTest<TRes>::TFreezablePtr; + + template <class... TArgs> + void Generate(TArgs&&... args) + { + TTest<TRes>::Generate(args...); + } + + template <class... TArgs> + void GenerateFront(TArgs&&... args) + { + TTest<TRes>::GenerateFront(args...); + } + + template <class... TArgs> + auto Schedule(TArgs&&... args) + { + return TTest<TRes>::Schedule(args...); + } + } + + Y_UNIT_TEST(StartLwtrace) { + NLWTrace::StartLwtraceFromEnv(); + } + + Y_UNIT_TEST(Simple) { + using namespace NSingleResource; + + TMyScheduler sched; + TMyFreezable* G; + sched.AddFreezable("G", TFreezablePtr(G = new TMyFreezable())); + + TMyQueue* A; + TMyQueue* B; + sched.AddQueue("A", G, TQueuePtr(A = new TMyQueue(1, 0))); + sched.AddQueue("B", G, TQueuePtr(B = new TMyQueue(1, 1))); + + Generate(A, "100 100 100"); + Generate(B, "100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "ABABAB"); + + Generate(A, "50 50 50 50 50"); + Generate(B, "50 50 50 50 50"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "ABABABABAB"); + + Generate(A, "20 20 20 20 20 20 20"); + Generate(B, "20 20 20 20 20 20 20"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "ABABABABABABAB"); + + Generate(A, "20 20 20 20 20 20 20"); + Generate(B, "50 50 50" ); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "ABAABAAABA"); + + Generate(A, " 100 100"); + Generate(B, "20 20 20 20 20 20 20 "); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "ABBBBBABB"); + + TMyQueue* C; + sched.AddQueue("C", G, TQueuePtr(C = new TMyQueue(2, 2))); + + Generate(A, "20 20 20 20 20 20"); + Generate(B, "20 20 20 20 20 20"); + Generate(C, "20 20 20 20 20 20 20 20 20 20"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "ABCCABCCABCCABCCABCCAB"); + + sched.DeleteQueue("A"); + // sched.DeleteQueue("B"); // scheduler must delete queue be itself + } + +// Y_UNIT_TEST(CompileDrf) { +// using namespace NPairDrf; + +// TMyScheduler sched; +// TMyFreezable* G; +// sched.AddFreezable("G", TFreezablePtr(G = new TMyFreezable())); +// TMyQueue* A; +// TMyQueue* B; +// sched.AddQueue("A", G, TQueuePtr(A = new TMyQueue(1, 0))); +// sched.AddQueue("B", G, TQueuePtr(B = new TMyQueue(1, 1))); +// } + + Y_UNIT_TEST(DoubleActivation) { + using namespace NSingleResource; + + TMyScheduler sched; + TMyFreezable* G; + sched.AddFreezable("G", TFreezablePtr(G = new TMyFreezable())); + + TMyQueue* A; + TMyQueue* B; + sched.AddQueue("A", G, TQueuePtr(A = new TMyQueue(1, 0))); + sched.AddQueue("B", G, TQueuePtr(B = new TMyQueue(1, 1))); + + Generate(A, "100 100 100"); + Generate(B, "100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 2), "AB"); + + A->Activate(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 2), "AB"); + + A->Activate(); + B->Activate(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "AB"); + } + + Y_UNIT_TEST(SimpleLag) { + using namespace NSingleResource; + + TMyScheduler sched; + TMyFreezable* G; + sched.AddFreezable("G", TFreezablePtr(G = new TMyFreezable())); + + TMyQueue* A; + TMyQueue* B; + TMyQueue* C; + TMyQueue* D; + TMyQueue* E; + sched.AddQueue("A", G, TQueuePtr(A = new TMyQueue(1, 0))); + sched.AddQueue("B", G, TQueuePtr(B = new TMyQueue(1, 1))); + sched.AddQueue("C", G, TQueuePtr(C = new TMyQueue(1, 2))); + sched.AddQueue("D", G, TQueuePtr(D = new TMyQueue(1, 3))); + sched.AddQueue("E", G, TQueuePtr(E = new TMyQueue(1, 4))); + + Generate(A, "500 500 500"); // 25 + Generate(B, "500 500 500"); // 25 + Generate(C, "500 500 500"); // 25 + Generate(D, "500 500 500"); // 25 + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 8), "ABCDABCD"); + Generate(E, "500"); // 100 + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "EABCD"); + } + + Y_UNIT_TEST(Complex) { + using namespace NSingleResource; + + TMyScheduler sched; + TMyFreezable* G; + sched.AddFreezable("G", TFreezablePtr(G = new TMyFreezable())); + TMyFreezable* H; + sched.AddFreezable("H", TFreezablePtr(H = new TMyFreezable())); + + TMyQueue* A; + TMyQueue* B; + TMyQueue* C; + TMyQueue* D; + TMyQueue* E; + TMyQueue* X; + sched.AddQueue("A", G, TQueuePtr(A = new TMyQueue(1, 0))); + sched.AddQueue("B", G, TQueuePtr(B = new TMyQueue(1, 1))); + sched.AddQueue("C", G, TQueuePtr(C = new TMyQueue(1, 2))); + sched.AddQueue("D", G, TQueuePtr(D = new TMyQueue(1, 3))); + sched.AddQueue("E", G, TQueuePtr(E = new TMyQueue(1, 4))); + sched.AddQueue("X", H, TQueuePtr(X = new TMyQueue(1, 5))); + + Generate(A, "100 100 100"); + Generate(B, "100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "ABABAB"); + + Generate(A, "100 100 100"); + Generate(X, "100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "AXAXAX"); + + Generate(A, "100 100 100"); + Generate(B, "100 100 100"); + Generate(C, "100 100 100"); + Generate(D, "100 100 100"); + Generate(E, "100 100 100"); + Generate(X, "100 100 100 100 100 100 100 100 100 100 100 100 100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "ABCDEXABCDEXABCDEXXXXXXXXXXXXX"); + } + + Y_UNIT_TEST(ComplexWithWeights) { + using namespace NSingleResource; + + TMyScheduler sched; + TMyFreezable* G; + sched.AddFreezable("G", TFreezablePtr(G = new TMyFreezable())); + TMyFreezable* H; + sched.AddFreezable("H", TFreezablePtr(H = new TMyFreezable())); + + TMyQueue* A; + TMyQueue* B; + TMyQueue* C; + TMyQueue* D; + sched.AddQueue("A", G, TQueuePtr(A = new TMyQueue(1, 0))); + sched.AddQueue("B", G, TQueuePtr(B = new TMyQueue(1, 1))); + sched.AddQueue("C", H, TQueuePtr(C = new TMyQueue(3, 2))); + sched.AddQueue("D", H, TQueuePtr(D = new TMyQueue(3, 3))); + + Generate(A, "100 100 100"); // 100 + Generate(B, "100 100 100"); // 100 + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "ABABAB"); + + Generate(C, "100 100 100"); // 100 + Generate(D, "100 100 100"); // 100 + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "CDCDCD"); + + Generate(A, "100 100 100 100 100 100 100 100"); + Generate(C, "100 100 100 100 100 100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "ACCCACCCACCAAAAA"); + + Generate(B, "100 100 100 100 100 100 100 100"); + Generate(D, "100 100 100 100 100 100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "BDDDBDDDBDDBBBBB"); + + Generate(A, "200 200 200 200 200 200 200 200 200 200 200"); + Generate(B, "200 200 200 200 200 200 200 200 200 200 200"); + Generate(C, "200 200 200 200 200 200 200 200 200 200 200"); + Generate(D, "200 200 200 200 200 200 200 200 200 200 200"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "ABCDCDCDABCDCDCDABCDCDCDABCDCDABABABABABABAB"); + + Generate(A, "100 100 100 100 100 100 100 100 100 100 100"); + Generate(B, "100 100 100 100 100 100 100 100 100 100 100"); + Generate(C, "100 100 100 100 100 100 100 100 100 100 100"); + Generate(D, "100 100 100 100 100 100 100 100 100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "ABCDCDCDABCDCDCDABCDCDCDABCDCDABABABABABABAB"); + + Generate(A, "50 50 50 50 50 50 50 50 50 50 50"); + Generate(B, "50 50 50 50 50 50 50 50 50 50 50"); + Generate(C, "50 50 50 50 50 50 50 50 50 50 50"); + Generate(D, "50 50 50 50 50 50 50 50 50 50 50"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "ABCDCDCDABCDCDCDABCDCDCDABCDCDABABABABABABAB"); + } + + Y_UNIT_TEST(OneQueueFrontPush) { + using namespace NSingleResource; + + TMyScheduler sched; + TMyFreezable* G; + sched.AddFreezable("G", TFreezablePtr(G = new TMyFreezable())); + + TMyQueue* A; + sched.AddQueue("A", G, TQueuePtr(A = new TMyQueue())); + + Generate(A, "10 20 30"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 1, true), "A10"); + GenerateFront(A, "40"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, size_t(-1), true), "A40A20A30"); + } + + Y_UNIT_TEST(SimpleFrontPush) { + using namespace NSingleResource; + + TMyScheduler sched; + TMyFreezable* G; + sched.AddFreezable("G", TFreezablePtr(G = new TMyFreezable())); + + TMyQueue* A; + TMyQueue* B; + sched.AddQueue("A", G, TQueuePtr(A = new TMyQueue(1, 0))); + sched.AddQueue("B", G, TQueuePtr(B = new TMyQueue(1, 1))); + + Generate(A, "10 30 40"); + Generate(B, "10 20 30"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 2, true), "A10B10"); + GenerateFront(A, "20"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, size_t(-1), true), "A20B20A30B30A40"); + } + + Y_UNIT_TEST(SimpleFrontPushAll) { + using namespace NSingleResource; + + TMyScheduler sched; + TMyFreezable* G; + sched.AddFreezable("G", TFreezablePtr(G = new TMyFreezable())); + + TMyQueue* A; + TMyQueue* B; + sched.AddQueue("A", G, TQueuePtr(A = new TMyQueue(1, 0))); + sched.AddQueue("B", G, TQueuePtr(B = new TMyQueue(1, 1))); + + Generate(A, "10 30 40"); + Generate(B, " 5 30 40"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 2, true), "A10B5"); + GenerateFront(A, "20"); + GenerateFront(B, "20"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 4, true), "B20A20B30A30"); + GenerateFront(A, "10 10"); + GenerateFront(B, "10 8"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, size_t(-1), true), "B8A10B10A10B40A40"); + } + + Y_UNIT_TEST(ComplexFrontPush) { + using namespace NSingleResource; + + TMyScheduler sched; + TMyFreezable* G; + sched.AddFreezable("G", TFreezablePtr(G = new TMyFreezable())); + TMyFreezable* H; + sched.AddFreezable("H", TFreezablePtr(H = new TMyFreezable())); + + TMyQueue* A; + TMyQueue* B; + TMyQueue* C; + TMyQueue* D; + sched.AddQueue("A", G, TQueuePtr(A = new TMyQueue(1, 0))); + sched.AddQueue("B", G, TQueuePtr(B = new TMyQueue(1, 1))); + sched.AddQueue("C", H, TQueuePtr(C = new TMyQueue(1, 2))); + sched.AddQueue("D", H, TQueuePtr(D = new TMyQueue(1, 3))); + + Generate(A, "10 20"); + Generate(B, "10 30"); + Generate(C, "10 20"); + Generate(D, "10 20"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 4, true), "A10B10C10D10"); + GenerateFront(B, "20"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, size_t(-1), true), "A20B20C20D20B30"); + } + + Y_UNIT_TEST(ComplexFrontPushAll) { + using namespace NSingleResource; + + TMyScheduler sched; + TMyFreezable* G; + sched.AddFreezable("G", TFreezablePtr(G = new TMyFreezable())); + TMyFreezable* H; + sched.AddFreezable("H", TFreezablePtr(H = new TMyFreezable())); + + TMyQueue* A; + TMyQueue* B; + TMyQueue* C; + TMyQueue* D; + sched.AddQueue("A", G, TQueuePtr(A = new TMyQueue(1, 0))); + sched.AddQueue("B", G, TQueuePtr(B = new TMyQueue(1, 1))); + sched.AddQueue("C", H, TQueuePtr(C = new TMyQueue(1, 2))); + sched.AddQueue("D", H, TQueuePtr(D = new TMyQueue(1, 3))); + + Generate(A, "10 36"); + Generate(B, "10 34"); + Generate(C, "10 32"); + Generate(D, "10 30"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 4, true), "A10B10C10D10"); + GenerateFront(A, "20"); + GenerateFront(B, "21"); + GenerateFront(C, "22"); + GenerateFront(D, "23"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, size_t(-1), true), "A20B21C22D23A36B34C32D30"); + } + + Y_UNIT_TEST(ComplexMultiplePush) { + using namespace NSingleResource; + + TMyScheduler sched; + TMyFreezable* G; + sched.AddFreezable("G", TFreezablePtr(G = new TMyFreezable())); + TMyFreezable* H; + sched.AddFreezable("H", TFreezablePtr(H = new TMyFreezable())); + + TMyQueue* A; + TMyQueue* B; + TMyQueue* C; + TMyQueue* D; + sched.AddQueue("A", G, TQueuePtr(A = new TMyQueue(1, 0))); + sched.AddQueue("B", G, TQueuePtr(B = new TMyQueue(1, 1))); + sched.AddQueue("C", H, TQueuePtr(C = new TMyQueue(1, 2))); + sched.AddQueue("D", H, TQueuePtr(D = new TMyQueue(1, 3))); + + Generate(A, "5 5 5 5 5"); + Generate(B, "10 10"); + Generate(C, "10 10"); + Generate(D, "10 10"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "ABCDAABCDAA"); + } + + Y_UNIT_TEST(ComplexFrontMultiplePush) { + using namespace NSingleResource; + + TMyScheduler sched; + TMyFreezable* G; + sched.AddFreezable("G", TFreezablePtr(G = new TMyFreezable())); + TMyFreezable* H; + sched.AddFreezable("H", TFreezablePtr(H = new TMyFreezable())); + + TMyQueue* A; + TMyQueue* B; + TMyQueue* C; + TMyQueue* D; + sched.AddQueue("A", G, TQueuePtr(A = new TMyQueue(1, 0))); + sched.AddQueue("B", G, TQueuePtr(B = new TMyQueue(1, 1))); + sched.AddQueue("C", H, TQueuePtr(C = new TMyQueue(1, 2))); + sched.AddQueue("D", H, TQueuePtr(D = new TMyQueue(1, 3))); + + Generate(A, "5"); + Generate(B, "10 10"); + Generate(C, "10 10"); + Generate(D, "10 10"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 1, true), "A5"); + GenerateFront(A, "5"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 1, true), "B10"); + GenerateFront(A, "5"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 4, true), "C10D10A5A5"); + GenerateFront(A, "5"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 1, true), "B10"); + GenerateFront(A, "5"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, size_t(-1), true), "C10D10A5A5"); + } + + Y_UNIT_TEST(ComplexCheckEmpty) { + using namespace NSingleResource; + + TMyScheduler sched; + TMyFreezable* G; + sched.AddFreezable("G", TFreezablePtr(G = new TMyFreezable())); + TMyFreezable* H; + sched.AddFreezable("H", TFreezablePtr(H = new TMyFreezable())); + + TMyQueue* A; + TMyQueue* B; + TMyQueue* C; + TMyQueue* D; + sched.AddQueue("A", G, TQueuePtr(A = new TMyQueue(1, 0))); + sched.AddQueue("B", G, TQueuePtr(B = new TMyQueue(1, 1))); + sched.AddQueue("C", H, TQueuePtr(C = new TMyQueue(1, 2))); + sched.AddQueue("D", H, TQueuePtr(D = new TMyQueue(1, 3))); + + Generate(A, "5"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, size_t(1), true), "A5"); + + GenerateFront(A, "5"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, size_t(1), true), "A5"); + + GenerateFront(A, "5"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, size_t(1), true), "A5"); + + GenerateFront(A, "5"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, size_t(1), true), "A5"); + + GenerateFront(A, "5"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, size_t(-1), true), "A5"); + } + + Y_UNIT_TEST(SimpleFreeze) { + using namespace NSingleResource; + + TMyScheduler sched; + TMyFreezable* G; + sched.AddFreezable("G", TFreezablePtr(G = new TMyFreezable())); + + TMyQueue* A; + TMyQueue* B; + sched.AddQueue("A", G, TQueuePtr(A = new TMyQueue(1, 0))); + sched.AddQueue("B", G, TQueuePtr(B = new TMyQueue(1, 1))); + + Generate(A, "100 100 100 100"); + Generate(B, "100 100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 4), "ABAB"); + G->Freeze(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), ""); + G->Unfreeze(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "ABAB"); + + Generate(A, "100 100 100 100"); + Generate(B, "100 100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 4), "ABAB"); + G->Freeze(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), ""); + G->Unfreeze(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 1), "A"); + G->Freeze(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), ""); + G->Unfreeze(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 1), "B"); + G->Freeze(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), ""); + G->Unfreeze(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 1), "A"); + G->Freeze(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), ""); + G->Unfreeze(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 1), "B"); + } + + Y_UNIT_TEST(ComplexFreeze) { + using namespace NSingleResource; + + TMyScheduler sched; + TMyFreezable* G; + sched.AddFreezable("G", TFreezablePtr(G = new TMyFreezable())); + TMyFreezable* H; + sched.AddFreezable("H", TFreezablePtr(H = new TMyFreezable())); + + TMyQueue* A; + TMyQueue* B; + TMyQueue* C; + TMyQueue* D; + sched.AddQueue("A", G, TQueuePtr(A = new TMyQueue(1, 0))); + sched.AddQueue("B", G, TQueuePtr(B = new TMyQueue(1, 1))); + sched.AddQueue("C", H, TQueuePtr(C = new TMyQueue(1, 2))); + sched.AddQueue("D", H, TQueuePtr(D = new TMyQueue(1, 3))); + + Generate(A, "100 100 100 100"); + Generate(B, "100 100 100 100"); + Generate(C, "100 100 100 100"); + Generate(D, "100 100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 4), "ABCD"); + G->Freeze(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 4), "CDCD"); + G->Unfreeze(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "ABCDABAB"); + + Generate(A, "100 100 100 100"); + Generate(B, "100 100 100 100"); + Generate(C, "100 100 100 100"); + Generate(D, "100 100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 4), "ABCD"); + G->Freeze(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 2), "CD"); + H->Freeze(); + H->Unfreeze(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 2), "CD"); + G->Unfreeze(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "ABCDABAB"); + + Generate(A, "100 100 100 100"); + Generate(B, "100 100 100 100"); + Generate(C, "100 100 100 100"); + Generate(D, "100 100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 4), "ABCD"); + G->Freeze(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 2), "CD"); + H->Freeze(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), ""); + H->Unfreeze(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 2), "CD"); + G->Unfreeze(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "ABCDABAB"); + + Generate(A, "100 100 100 100"); + Generate(B, "100 100 100 100"); + Generate(C, "100 100 100 100"); + Generate(D, "100 100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 4), "ABCD"); + G->Freeze(); + H->Freeze(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), ""); + G->Unfreeze(); + H->Unfreeze(); + G->Freeze(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 4), "CDCD"); + G->Unfreeze(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "ABCDABAB"); + + Generate(A, "100 100 100 100"); + Generate(B, "100 100 100 100"); + Generate(C, "100 100 100 100"); + Generate(D, "100 100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 4), "ABCD"); + G->Freeze(); + H->Freeze(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), ""); + H->Unfreeze(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 1), "C"); + H->Freeze(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), ""); + H->Unfreeze(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 1), "D"); + H->Freeze(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), ""); + H->Unfreeze(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 1), "C"); + H->Freeze(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), ""); + H->Unfreeze(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 1), "D"); + G->Unfreeze(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "ABCDABAB"); + } + + Y_UNIT_TEST(ComplexLocalBusyPeriods) { + using namespace NSingleResource; + + TMyScheduler sched; + TMyFreezable* G; + sched.AddFreezable("G", TFreezablePtr(G = new TMyFreezable())); + TMyFreezable* H; + sched.AddFreezable("H", TFreezablePtr(H = new TMyFreezable())); + + TMyQueue* A; + TMyQueue* B; + TMyQueue* C; + TMyQueue* D; + sched.AddQueue("A", G, TQueuePtr(A = new TMyQueue(1, 0))); + sched.AddQueue("B", G, TQueuePtr(B = new TMyQueue(1, 1))); + sched.AddQueue("C", H, TQueuePtr(C = new TMyQueue(1, 2))); + sched.AddQueue("D", H, TQueuePtr(D = new TMyQueue(1, 3))); + + Generate(A, "100 100"); + Generate(B, "100 100"); + Generate(C, "100 100 100"); + Generate(D, "100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 4), "ABCD"); + G->Freeze(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 4), "CDCD"); + for (int i = 0; i < 10; i++) { + Generate(C, "50 50 50 40"); + Generate(D, "100 100 "); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "CDCCDC"); + } + G->Unfreeze(); + Generate(C, "50 50 50"); + Generate(D, "50 50 50"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "CDCDABCD"); + } + + Y_UNIT_TEST(ComplexGlobalBusyPeriods) { + using namespace NSingleResource; + + TMyScheduler sched; + TMyFreezable* G; + sched.AddFreezable("G", TFreezablePtr(G = new TMyFreezable())); + TMyFreezable* H; + sched.AddFreezable("H", TFreezablePtr(H = new TMyFreezable())); + + TMyQueue* A; + TMyQueue* B; + TMyQueue* C; + TMyQueue* D; + sched.AddQueue("A", G, TQueuePtr(A = new TMyQueue(1, 0))); + sched.AddQueue("B", G, TQueuePtr(B = new TMyQueue(1, 1))); + sched.AddQueue("C", H, TQueuePtr(C = new TMyQueue(1, 2))); + sched.AddQueue("D", H, TQueuePtr(D = new TMyQueue(1, 3))); + + for (int j = 0; j < 5; j++) { + Generate(A, "100 100"); + Generate(B, "100 100"); + Generate(C, "100 100 100"); + Generate(D, "100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 4), "ABCD"); + G->Freeze(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 4), "CDCD"); + for (int i = 0; i < 5; i++) { + Generate(C, "50 50 50 40"); + Generate(D, "100 100 "); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "CDCCDC"); + } + G->Unfreeze(); + Generate(C, "50 50 25"); + Generate(D, "50 50 50"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "CDCDABCD"); + } + } + + Y_UNIT_TEST(VeryComplexFreeze) { + using namespace NSingleResource; + + TMyScheduler sched; + TMyFreezable* G; + sched.AddFreezable("G", TFreezablePtr(G = new TMyFreezable())); + TMyFreezable* H; + sched.AddFreezable("H", TFreezablePtr(H = new TMyFreezable())); + TMyFreezable* K; + sched.AddFreezable("K", TFreezablePtr(K = new TMyFreezable())); + + TMyQueue* A; + TMyQueue* B; + TMyQueue* C; + TMyQueue* D; + TMyQueue* E; + TMyQueue* F; + sched.AddQueue("A", G, TQueuePtr(A = new TMyQueue(1, 0))); + sched.AddQueue("B", G, TQueuePtr(B = new TMyQueue(1, 1))); + sched.AddQueue("C", H, TQueuePtr(C = new TMyQueue(1, 2))); + sched.AddQueue("D", H, TQueuePtr(D = new TMyQueue(1, 3))); + sched.AddQueue("E", K, TQueuePtr(E = new TMyQueue(1, 4))); + sched.AddQueue("F", K, TQueuePtr(F = new TMyQueue(1, 5))); + + for (int j = 0; j < 5; j++) { + Generate(A, "100 100"); + Generate(B, "100 100"); + Generate(C, "100 100 100"); + Generate(D, "100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 4), "ABCD"); + G->Freeze(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 4), "CDCD"); + for (int i = 0; i < 5; i++) { + Generate(C, "50 50 50 40"); + Generate(D, "100 100 "); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "CDCCDC"); + Generate(E, "50 50 50 40"); + Generate(F, "100 100 "); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "EFEEFE"); + } + for (int i = 0; i < 5; i++) { + Generate(C, "50 50 50 50"); + Generate(D, "100 100 "); + Generate(E, "50 50 50 50 50 50"); + Generate(F, "100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 6), "CDEFCE"); + H->Freeze(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 3), "EFE"); + H->Unfreeze(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "CDEFCE"); + } + G->Unfreeze(); + Generate(C, "50 50 55"); + Generate(D, "50 50 40"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "CDCDABCD"); + } + } + + Y_UNIT_TEST(SimplestClear) { + using namespace NSingleResource; + + TMyScheduler sched; + TMyFreezable* G; + sched.AddFreezable("G", TFreezablePtr(G = new TMyFreezable())); + + TMyQueue* A; + sched.AddQueue("A", G, TQueuePtr(A = new TMyQueue(1, 0))); + + Generate(A, "100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "AAA"); + + Generate(A, "100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "AAA"); + + sched.Clear(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), ""); + } + + Y_UNIT_TEST(SimpleClear) { + using namespace NSingleResource; + + TMyScheduler sched; + TMyFreezable* G; + sched.AddFreezable("G", TFreezablePtr(G = new TMyFreezable())); + + TMyQueue* A; + TMyQueue* B; + sched.AddQueue("A", G, TQueuePtr(A = new TMyQueue(1, 0))); + sched.AddQueue("B", G, TQueuePtr(B = new TMyQueue(1, 1))); + + Generate(A, "100 100 100"); + Generate(B, "100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 4), "ABAB"); + + sched.Clear(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), ""); + + A->Activate(); + B->Activate(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "AB"); + } + + Y_UNIT_TEST(ComplexClear) { + using namespace NSingleResource; + + TMyScheduler sched; + TMyFreezable* G; + sched.AddFreezable("G", TFreezablePtr(G = new TMyFreezable())); + TMyFreezable* H; + sched.AddFreezable("H", TFreezablePtr(H = new TMyFreezable())); + + TMyQueue* A; + TMyQueue* B; + sched.AddQueue("A", G, TQueuePtr(A = new TMyQueue(1, 0))); + sched.AddQueue("B", H, TQueuePtr(B = new TMyQueue(1, 1))); + + Generate(A, "100 100 100"); + Generate(B, "100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 4), "ABAB"); + + sched.Clear(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), ""); + + A->Activate(); + B->Activate(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "AB"); + } + + Y_UNIT_TEST(SimpleFreezeClear) { + using namespace NSingleResource; + + TMyScheduler sched; + TMyFreezable* G; + sched.AddFreezable("G", TFreezablePtr(G = new TMyFreezable())); + + TMyQueue* A; + sched.AddQueue("A", G, TQueuePtr(A = new TMyQueue(1, 0))); + + Generate(A, "100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 1), "A"); + + G->Freeze(); + sched.Clear(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), ""); + + G->Unfreeze(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "AA"); + } + + Y_UNIT_TEST(EmptyFreezeClear) { + using namespace NSingleResource; + + TMyScheduler sched; + TMyFreezable* G; + sched.AddFreezable("G", TFreezablePtr(G = new TMyFreezable())); + + TMyQueue* A; + sched.AddQueue("A", G, TQueuePtr(A = new TMyQueue(1, 0))); + + Generate(A, "100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "AAA"); + + G->Freeze(); + sched.Clear(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), ""); + + G->Unfreeze(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), ""); + } + + Y_UNIT_TEST(ComplexFreezeClear) { + using namespace NSingleResource; + + TMyScheduler sched; + TMyFreezable* G; + sched.AddFreezable("G", TFreezablePtr(G = new TMyFreezable())); + TMyFreezable* H; + sched.AddFreezable("H", TFreezablePtr(H = new TMyFreezable())); + + TMyQueue* A; + TMyQueue* B; + sched.AddQueue("A", G, TQueuePtr(A = new TMyQueue(1, 0))); + sched.AddQueue("B", H, TQueuePtr(B = new TMyQueue(1, 1))); + + Generate(A, "100 100 100"); + Generate(B, "100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 4), "ABAB"); + + G->Freeze(); + sched.Clear(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), ""); + + A->Activate(); // double activation should be ignored gracefully + B->Activate(); + G->Unfreeze(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "BA"); + } + + Y_UNIT_TEST(DeleteQueue) { + using namespace NSingleResource; + + TMyScheduler sched; + TMyFreezable* G; + sched.AddFreezable("G", TFreezablePtr(G = new TMyFreezable())); + + TMyQueue* A; + TMyQueue* B; + sched.AddQueue("A", G, TQueuePtr(A = new TMyQueue(1, 0))); + sched.AddQueue("B", G, TQueuePtr(B = new TMyQueue(1, 1))); + + Generate(A, "100 100 100 100 100"); + Generate(B, "100 100 100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 2), "AB"); + + TMyQueue* C; + sched.AddQueue("C", G, TQueuePtr(C = new TMyQueue(1, 2))); + Generate(C, "100 100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 4), "CABC"); + + sched.DeleteQueue("C"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 4), "ABAB"); + + sched.AddQueue("C", G, TQueuePtr(C = new TMyQueue(1, 2))); + Generate(C, "100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "CABC"); + } + + Y_UNIT_TEST(SimpleActDeact) { + using namespace NSingleResource; + + TMyScheduler sched; + TMyFreezable* G; + sched.AddFreezable("G", TFreezablePtr(G = new TMyFreezable())); + + TMyQueue* A; + TMyQueue* B; + sched.AddQueue("A", G, TQueuePtr(A = new TMyQueue(1, 0))); + sched.AddQueue("B", G, TQueuePtr(B = new TMyQueue(1, 1))); + + Generate(A, "100 100 100"); + Generate(B, "100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 4), "ABAB"); + + A->Deactivate(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "B"); + + A->Activate(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "A"); + } + + Y_UNIT_TEST(TotalActDeact) { + using namespace NSingleResource; + + TMyScheduler sched; + TMyFreezable* G; + sched.AddFreezable("G", TFreezablePtr(G = new TMyFreezable())); + + TMyQueue* A; + TMyQueue* B; + sched.AddQueue("A", G, TQueuePtr(A = new TMyQueue(1, 0))); + sched.AddQueue("B", G, TQueuePtr(B = new TMyQueue(1, 1))); + + Generate(A, "100 100 100"); + Generate(B, "100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 4), "ABAB"); + + A->Deactivate(); + B->Deactivate(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), ""); + + B->Activate(); + A->Activate(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "AB"); + } + + Y_UNIT_TEST(TotalActDeactReordered) { + using namespace NSingleResource; + + TMyScheduler sched; + TMyFreezable* G; + sched.AddFreezable("G", TFreezablePtr(G = new TMyFreezable())); + + TMyQueue* A; + TMyQueue* B; + sched.AddQueue("A", G, TQueuePtr(A = new TMyQueue(1, 0))); + sched.AddQueue("B", G, TQueuePtr(B = new TMyQueue(1, 1))); + + Generate(A, "100 100 100"); + Generate(B, "100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 4), "ABAB"); + + B->Deactivate(); + A->Deactivate(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), ""); + + A->Activate(); + B->Activate(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "AB"); + } + + Y_UNIT_TEST(ComplexActDeact) { + using namespace NSingleResource; + + TMyScheduler sched; + TMyFreezable* G; + sched.AddFreezable("G", TFreezablePtr(G = new TMyFreezable())); + + TMyQueue* A; + TMyQueue* B; + sched.AddQueue("A", G, TQueuePtr(A = new TMyQueue(1, 0))); + sched.AddQueue("B", G, TQueuePtr(B = new TMyQueue(1, 1))); + + Generate(A, "100 100 100"); + Generate(B, "100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 4), "ABAB"); + + B->Deactivate(); + A->Deactivate(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), ""); + + G->ActivateConsumers([=] (TMyConsumer* c) { return c == A; }); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "A"); + + G->ActivateConsumers([=] (TMyConsumer* c) { return c == B; }); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "B"); + } + + Y_UNIT_TEST(ComplexDeactDoubleAct) { + using namespace NSingleResource; + + TMyScheduler sched; + TMyFreezable* G; + sched.AddFreezable("G", TFreezablePtr(G = new TMyFreezable())); + + TMyQueue* A; + TMyQueue* B; + TMyQueue* C; + TMyQueue* D; + sched.AddQueue("A", G, TQueuePtr(A = new TMyQueue(1, 0))); + sched.AddQueue("B", G, TQueuePtr(B = new TMyQueue(1, 1))); + sched.AddQueue("C", G, TQueuePtr(C = new TMyQueue(1, 2))); + sched.AddQueue("D", G, TQueuePtr(D = new TMyQueue(1, 3))); + + Generate(A, "100 100 100"); + Generate(B, "100 100 100"); + Generate(C, "100 100 100"); + Generate(D, "100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 6), "ABCDAB"); + + A->Deactivate(); + B->Deactivate(); + C->Deactivate(); + D->Deactivate(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), ""); + + G->ActivateConsumers([=] (TMyConsumer* c) { return c == A || c == D; }); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "ADD"); + + G->ActivateConsumers([=] (TMyConsumer* c) { return c == B || c == C; }); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "BCC"); + } + + + Y_UNIT_TEST(AttachIntoFreezableAfterDeact) { + using namespace NSingleResource; + + TMyScheduler sched; + TMyFreezable* G; + sched.AddFreezable("G", TFreezablePtr(G = new TMyFreezable())); + + TMyQueue* A; + TMyQueue* B; + TMyQueue* C; + TMyQueue* D; + sched.AddQueue("A", G, TQueuePtr(A = new TMyQueue(1, 0))); + sched.AddQueue("B", G, TQueuePtr(B = new TMyQueue(1, 1))); + sched.AddQueue("C", G, TQueuePtr(C = new TMyQueue(1, 2))); + + Generate(A, "100 100 100"); + Generate(B, "100 100 100"); + Generate(C, "100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched, 6), "ABCABC"); + + C->Deactivate(); + + sched.AddQueue("D", G, TQueuePtr(D = new TMyQueue(1, 3))); + Generate(D, "100"); + + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "DAB"); + + C->Activate(); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(sched), "C"); + } + + Y_UNIT_TEST(Random) { + using namespace NSingleResource; + + for (int cycle = 0; cycle < 50; cycle++) { + TMyScheduler sched(true); + TMyFreezable* G; + sched.AddFreezable("G", TFreezablePtr(G = new TMyFreezable())); + + TVector<TQueuePtr> queues; + for (int i = 0; i < 50; i++) { + TQueuePtr queue(new TMyQueue(1, 0)); + queues.push_back(queue); + sched.AddQueue(Sprintf("Q%03d", i), G, queue); + } + + for (int i = 0; i < 10000; i++) { + if (RandomNumber<size_t>(10)) { + size_t i = RandomNumber<size_t>(queues.size()); + TMyQueue* queue = queues[i].get(); + switch (RandomNumber<size_t>(3)) { + case 0: + Generate(queue, "100"); + queue->Activate(); + break; + case 2: + queue->Deactivate(); + break; + } + } else { + Schedule(sched, 3); + } + } + } + } + + Y_UNIT_TEST(SimpleHierarchy) { + using namespace NSingleResource; + + TMyScheduler schedR; // must be destructed last + + TMyScheduler schedG(1, 0); + TMyFreezable* G; + schedG.AddFreezable("G", TFreezablePtr(G = new TMyFreezable())); + TMyQueue* A; + TMyQueue* B; + schedG.AddQueue("A", G, TQueuePtr(A = new TMyQueue(1, 0))); + schedG.AddQueue("B", G, TQueuePtr(B = new TMyQueue(1, 1))); + + TMyScheduler schedH(1, 0); + TMyFreezable* H; + schedH.AddFreezable("H", TFreezablePtr(H = new TMyFreezable())); + TMyQueue* C; + TMyQueue* D; + schedH.AddQueue("C", H, TQueuePtr(C = new TMyQueue(1, 0))); + schedH.AddQueue("D", H, TQueuePtr(D = new TMyQueue(1, 1))); + + TMyFreezable* R; + schedR.AddFreezable("R", TFreezablePtr(R = new TMyFreezable())); + schedR.AddSubScheduler("G", R, &schedG); + schedR.AddSubScheduler("H", R, &schedH); + + Generate(A, "100 100 100"); + Generate(B, "100 100 100"); + Generate(C, "100 100 100"); + Generate(D, "100 100 100"); + UNIT_ASSERT_STRINGS_EQUAL(Schedule(schedR), "ACBDACBDACBD"); + } +} + +template<> +void Out<NTestSuiteShopScheduler::TTest<NShop::TSingleResource>::TMyRes::TKey>( + IOutputStream& out, + const NTestSuiteShopScheduler::TTest<NShop::TSingleResource>::TMyRes::TKey& key) +{ + out << "<" << key.Key << "|" << key.Priority << ">"; +} + +template<> +void Out<NTestSuiteShopScheduler::TTest<NShop::TPairDrf>::TMyRes::TKey>( + IOutputStream& out, + const NTestSuiteShopScheduler::TTest<NShop::TPairDrf>::TMyRes::TKey& key) +{ + out << "<" << key.Key << "|" << key.Priority << ">"; +} diff --git a/ydb/library/shop/ut/tr.lwt b/ydb/library/shop/ut/tr.lwt new file mode 100644 index 00000000000..3c9c55a310a --- /dev/null +++ b/ydb/library/shop/ut/tr.lwt @@ -0,0 +1,4 @@ +Blocks { + ProbeDesc { Group: "SHOP_PROVIDER" } + Action { PrintToStderrAction {} } +} diff --git a/ydb/library/shop/ut/ya.make b/ydb/library/shop/ut/ya.make new file mode 100644 index 00000000000..81ffb3398fd --- /dev/null +++ b/ydb/library/shop/ut/ya.make @@ -0,0 +1,15 @@ +UNITTEST() + +PEERDIR( + library/cpp/threading/future + ydb/library/shop +) + +SRCS( + estimator_ut.cpp + flowctl_ut.cpp + scheduler_ut.cpp + lazy_scheduler_ut.cpp +) + +END() diff --git a/ydb/library/shop/valve.h b/ydb/library/shop/valve.h new file mode 100644 index 00000000000..40eec6b3050 --- /dev/null +++ b/ydb/library/shop/valve.h @@ -0,0 +1,51 @@ +#pragma once + +#include "resource.h" +#include <util/system/spinlock.h> + +namespace NShop { + +class TValve { +public: + using TCost = i64; + +private: + TSpinLock Lock; + + TCost Consumed = 0; + TCost Supply = 0; + TCost Demand = 0; + +public: + bool CanConsume() const + { + auto guard = Guard(Lock); + return Consumed <= Supply; + } + + bool CanSupply() const + { + auto guard = Guard(Lock); + return Supply < Demand; + } + + void AddConsumed(TCost cost) + { + auto guard = Guard(Lock); + Consumed += cost; + } + + void AddSupply(TCost cost) + { + auto guard = Guard(Lock); + Supply += cost; + } + + void AddDemand(TCost cost) + { + auto guard = Guard(Lock); + Demand += cost; + } +}; + +} diff --git a/ydb/library/shop/ya.make b/ydb/library/shop/ya.make new file mode 100644 index 00000000000..81b5922b16a --- /dev/null +++ b/ydb/library/shop/ya.make @@ -0,0 +1,23 @@ +LIBRARY() + +SRCS( + probes.cpp + shop.cpp + flowctl.cpp +) + +PEERDIR( + library/cpp/containers/stack_vector + library/cpp/lwtrace + ydb/library/shop/protos + library/cpp/deprecated/atomic +) + +END() + +RECURSE( + protos + sim_flowctl + sim_shop + ut +) diff --git a/ydb/library/ya.make b/ydb/library/ya.make index 4e35a1d1fcd..72d27498af0 100644 --- a/ydb/library/ya.make +++ b/ydb/library/ya.make @@ -2,6 +2,7 @@ RECURSE( accessor aclib actors + analytics arrow_clickhouse arrow_kernels arrow_parquet @@ -9,6 +10,7 @@ RECURSE( benchmarks breakpad chunks_limiter + drr folder_service formats fyamlcpp @@ -23,11 +25,13 @@ RECURSE( ncloud pdisk_io persqueue + planner pretty_types_print protobuf_printer query_actor schlab security + shop signal_backtrace table_creator testlib |