aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorOleg Doronin <dorooleg@yandex.ru>2025-04-09 09:30:04 +0300
committerGitHub <noreply@github.com>2025-04-09 09:30:04 +0300
commit34a1855aaa5fb3d26817b2bbedbebc08a57f6a11 (patch)
tree7d74c42f886d1f61e403d3e4a0093f083f8aa1c4
parent56d21546eaa29f332c0ac39e83742ed4614c9309 (diff)
downloadydb-34a1855aaa5fb3d26817b2bbedbebc08a57f6a11.tar.gz
DDR scheduler (#16950)
-rw-r--r--ydb/library/analytics/all.h10
-rw-r--r--ydb/library/analytics/analytics.cpp5
-rw-r--r--ydb/library/analytics/asciiart_output.h256
-rw-r--r--ydb/library/analytics/csv_output.h52
-rw-r--r--ydb/library/analytics/data.h76
-rw-r--r--ydb/library/analytics/html_output.h86
-rw-r--r--ydb/library/analytics/json_output.h98
-rw-r--r--ydb/library/analytics/protobuf.h49
-rw-r--r--ydb/library/analytics/protos/data.proto21
-rw-r--r--ydb/library/analytics/protos/ya.make11
-rw-r--r--ydb/library/analytics/transform.h192
-rw-r--r--ydb/library/analytics/util.h122
-rw-r--r--ydb/library/analytics/ya.make15
-rw-r--r--ydb/library/drr/drr.cpp5
-rw-r--r--ydb/library/drr/drr.h363
-rw-r--r--ydb/library/drr/probes.cpp3
-rw-r--r--ydb/library/drr/probes.h22
-rw-r--r--ydb/library/drr/ut/drr_ut.cpp581
-rw-r--r--ydb/library/drr/ut/ya.make12
-rw-r--r--ydb/library/drr/ya.make17
-rw-r--r--ydb/library/planner/base/defs.h60
-rw-r--r--ydb/library/planner/base/visitor.h55
-rw-r--r--ydb/library/planner/share/account.cpp5
-rw-r--r--ydb/library/planner/share/account.h24
-rw-r--r--ydb/library/planner/share/analytics.h99
-rw-r--r--ydb/library/planner/share/apply.h104
-rw-r--r--ydb/library/planner/share/billing.h78
-rw-r--r--ydb/library/planner/share/group.cpp100
-rw-r--r--ydb/library/planner/share/group.h63
-rw-r--r--ydb/library/planner/share/history.cpp4
-rw-r--r--ydb/library/planner/share/history.h114
-rw-r--r--ydb/library/planner/share/models/density.h385
-rw-r--r--ydb/library/planner/share/models/max.h35
-rw-r--r--ydb/library/planner/share/models/model.h47
-rw-r--r--ydb/library/planner/share/models/recursive.h80
-rw-r--r--ydb/library/planner/share/models/static.h35
-rw-r--r--ydb/library/planner/share/monitoring.h542
-rw-r--r--ydb/library/planner/share/node.cpp114
-rw-r--r--ydb/library/planner/share/node.h136
-rw-r--r--ydb/library/planner/share/node_visitor.h42
-rw-r--r--ydb/library/planner/share/probes.cpp3
-rw-r--r--ydb/library/planner/share/probes.h123
-rw-r--r--ydb/library/planner/share/protos/shareplanner.proto38
-rw-r--r--ydb/library/planner/share/protos/shareplanner_history.proto22
-rw-r--r--ydb/library/planner/share/protos/shareplanner_sensors.proto88
-rw-r--r--ydb/library/planner/share/protos/ya.make15
-rw-r--r--ydb/library/planner/share/pull.h208
-rw-r--r--ydb/library/planner/share/shareplanner.cpp186
-rw-r--r--ydb/library/planner/share/shareplanner.h332
-rw-r--r--ydb/library/planner/share/stats.h48
-rw-r--r--ydb/library/planner/share/step.h23
-rw-r--r--ydb/library/planner/share/ut/all.lwt14
-rw-r--r--ydb/library/planner/share/ut/shareplanner_ut.cpp541
-rw-r--r--ydb/library/planner/share/ut/ya.make12
-rw-r--r--ydb/library/planner/share/utilization.cpp71
-rw-r--r--ydb/library/planner/share/utilization.h33
-rw-r--r--ydb/library/planner/share/ya.make25
-rw-r--r--ydb/library/planner/ya.make3
-rw-r--r--ydb/library/shop/counters.h107
-rw-r--r--ydb/library/shop/estimator.h192
-rw-r--r--ydb/library/shop/flowctl.cpp680
-rw-r--r--ydb/library/shop/flowctl.h213
-rw-r--r--ydb/library/shop/lazy_scheduler.h788
-rw-r--r--ydb/library/shop/probes.cpp3
-rw-r--r--ydb/library/shop/probes.h157
l---------ydb/library/shop/protos/library-shop-protos-sources.jar1
l---------ydb/library/shop/protos/library-shop-protos.jar1
l---------ydb/library/shop/protos/library-shop-protos.protosrc1
-rw-r--r--ydb/library/shop/protos/shop.proto47
-rw-r--r--ydb/library/shop/protos/ya.make9
-rw-r--r--ydb/library/shop/resource.h98
-rw-r--r--ydb/library/shop/schedulable.h17
-rw-r--r--ydb/library/shop/scheduler.h732
-rw-r--r--ydb/library/shop/shop.cpp363
-rw-r--r--ydb/library/shop/shop.h195
-rw-r--r--ydb/library/shop/shop_state.h122
-rw-r--r--ydb/library/shop/sim_estimator/estimator.css32
-rw-r--r--ydb/library/shop/sim_estimator/estimator.html131
-rw-r--r--ydb/library/shop/sim_estimator/estimator.js124
-rw-r--r--ydb/library/shop/sim_flowctl/flowctlmain.cpp588
-rw-r--r--ydb/library/shop/sim_flowctl/one.pb.txt18
-rw-r--r--ydb/library/shop/sim_flowctl/ya.make16
-rw-r--r--ydb/library/shop/sim_shop/config.proto114
-rw-r--r--ydb/library/shop/sim_shop/myshop.cpp807
-rw-r--r--ydb/library/shop/sim_shop/myshop.h286
-rw-r--r--ydb/library/shop/sim_shop/myshopmain.cpp319
-rw-r--r--ydb/library/shop/sim_shop/one.pb.txt18
-rw-r--r--ydb/library/shop/sim_shop/two.pb.txt26
-rw-r--r--ydb/library/shop/sim_shop/ya.make25
-rw-r--r--ydb/library/shop/ut/estimator_ut.cpp58
-rw-r--r--ydb/library/shop/ut/flowctl_ut.cpp523
-rw-r--r--ydb/library/shop/ut/lazy_scheduler_ut.cpp1537
-rw-r--r--ydb/library/shop/ut/scheduler_ut.cpp1403
-rw-r--r--ydb/library/shop/ut/tr.lwt4
-rw-r--r--ydb/library/shop/ut/ya.make15
-rw-r--r--ydb/library/shop/valve.h51
-rw-r--r--ydb/library/shop/ya.make23
-rw-r--r--ydb/library/ya.make4
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