aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorvladkoronnov <vladkoronnov@yandex-team.com>2023-09-05 20:36:23 +0300
committervladkoronnov <vladkoronnov@yandex-team.com>2023-09-05 21:25:18 +0300
commit914b0463af553af307edd9cd9951142a83b6423c (patch)
tree87762ea617774977a4812b900295f859a2e8bf28
parent74da3b1a704f1ce547e0120614a2655b3c2bd249 (diff)
downloadydb-914b0463af553af307edd9cd9951142a83b6423c.tar.gz
KIKIMR-18802 Added modal window with task profile
generated flame graph svg can show modal window with detailed info about tasks if stats were collected with profile mode
-rw-r--r--ydb/public/lib/stat_visualization/CMakeLists.darwin-x86_64.txt1
-rw-r--r--ydb/public/lib/stat_visualization/CMakeLists.linux-aarch64.txt1
-rw-r--r--ydb/public/lib/stat_visualization/CMakeLists.linux-x86_64.txt1
-rw-r--r--ydb/public/lib/stat_visualization/CMakeLists.windows-x86_64.txt1
-rw-r--r--ydb/public/lib/stat_visualization/flame_graph_builder.cpp270
-rw-r--r--ydb/public/lib/stat_visualization/flame_graph_entry.cpp218
-rw-r--r--ydb/public/lib/stat_visualization/flame_graph_entry.h153
-rw-r--r--ydb/public/lib/stat_visualization/svg_script.h355
-rw-r--r--ydb/public/lib/stat_visualization/ya.make1
-rw-r--r--ydb/public/lib/ydb_cli/commands/ydb_yql.cpp15
-rw-r--r--ydb/public/lib/ydb_cli/commands/ydb_yql.h2
11 files changed, 764 insertions, 254 deletions
diff --git a/ydb/public/lib/stat_visualization/CMakeLists.darwin-x86_64.txt b/ydb/public/lib/stat_visualization/CMakeLists.darwin-x86_64.txt
index 9bb3d80687..45ab1794d1 100644
--- a/ydb/public/lib/stat_visualization/CMakeLists.darwin-x86_64.txt
+++ b/ydb/public/lib/stat_visualization/CMakeLists.darwin-x86_64.txt
@@ -14,4 +14,5 @@ target_link_libraries(public-lib-stat_visualization PUBLIC
)
target_sources(public-lib-stat_visualization PRIVATE
${CMAKE_SOURCE_DIR}/ydb/public/lib/stat_visualization/flame_graph_builder.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/public/lib/stat_visualization/flame_graph_entry.cpp
)
diff --git a/ydb/public/lib/stat_visualization/CMakeLists.linux-aarch64.txt b/ydb/public/lib/stat_visualization/CMakeLists.linux-aarch64.txt
index 8e87d2fde8..43dd0d292a 100644
--- a/ydb/public/lib/stat_visualization/CMakeLists.linux-aarch64.txt
+++ b/ydb/public/lib/stat_visualization/CMakeLists.linux-aarch64.txt
@@ -15,4 +15,5 @@ target_link_libraries(public-lib-stat_visualization PUBLIC
)
target_sources(public-lib-stat_visualization PRIVATE
${CMAKE_SOURCE_DIR}/ydb/public/lib/stat_visualization/flame_graph_builder.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/public/lib/stat_visualization/flame_graph_entry.cpp
)
diff --git a/ydb/public/lib/stat_visualization/CMakeLists.linux-x86_64.txt b/ydb/public/lib/stat_visualization/CMakeLists.linux-x86_64.txt
index 8e87d2fde8..43dd0d292a 100644
--- a/ydb/public/lib/stat_visualization/CMakeLists.linux-x86_64.txt
+++ b/ydb/public/lib/stat_visualization/CMakeLists.linux-x86_64.txt
@@ -15,4 +15,5 @@ target_link_libraries(public-lib-stat_visualization PUBLIC
)
target_sources(public-lib-stat_visualization PRIVATE
${CMAKE_SOURCE_DIR}/ydb/public/lib/stat_visualization/flame_graph_builder.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/public/lib/stat_visualization/flame_graph_entry.cpp
)
diff --git a/ydb/public/lib/stat_visualization/CMakeLists.windows-x86_64.txt b/ydb/public/lib/stat_visualization/CMakeLists.windows-x86_64.txt
index 9bb3d80687..45ab1794d1 100644
--- a/ydb/public/lib/stat_visualization/CMakeLists.windows-x86_64.txt
+++ b/ydb/public/lib/stat_visualization/CMakeLists.windows-x86_64.txt
@@ -14,4 +14,5 @@ target_link_libraries(public-lib-stat_visualization PUBLIC
)
target_sources(public-lib-stat_visualization PRIVATE
${CMAKE_SOURCE_DIR}/ydb/public/lib/stat_visualization/flame_graph_builder.cpp
+ ${CMAKE_SOURCE_DIR}/ydb/public/lib/stat_visualization/flame_graph_entry.cpp
)
diff --git a/ydb/public/lib/stat_visualization/flame_graph_builder.cpp b/ydb/public/lib/stat_visualization/flame_graph_builder.cpp
index 584d7d5abd..07a0dd15ba 100644
--- a/ydb/public/lib/stat_visualization/flame_graph_builder.cpp
+++ b/ydb/public/lib/stat_visualization/flame_graph_builder.cpp
@@ -1,4 +1,5 @@
#include "flame_graph_builder.h"
+#include "flame_graph_entry.h"
#include "svg_script.h"
#include "stat_visalization_error.h"
@@ -12,197 +13,6 @@
namespace NKikimr::NVisual {
using namespace NJson;
-namespace {
-constexpr float rectHeight = 15;
-constexpr float minElementWidth = 1;
-
-constexpr float interElementOffset = 3;
-
-// Offsets from image limits
-constexpr float horOffset = 10;
-constexpr float vertOffset = 30;
-
-constexpr float textSideOffset = 3;
-constexpr float textTopOffset = 10.5;
-
-constexpr float viewPortWidth = 1200;
-
-TMap<EFlameGraphType, TString> TypeName = {
- {CPU, "CPU"},
- {TIME, "TIME_MS"},
- {BYTES_OUTPUT, "OUT_B"},
- {TASKS, "TASKS"}
-};
-
-struct TWeight {
- explicit TWeight(ui64 self)
- : Self(self) {};
- ui64 Self;
- ui64 Total = 0;
-};
-
-struct TCombinedWeights {
- TCombinedWeights()
- : Cpu(0), Bytes(0), Ms(0), Tasks(0) {}
-
- TCombinedWeights(ui64 cpu, ui64 bytes, ui64 ms, ui64 tasks)
- : Cpu(cpu), Bytes(bytes), Ms(ms), Tasks(tasks) {}
-
- void AddSelfToTotal() {
- Cpu.Self = (Cpu.Total + Cpu.Self) ? Cpu.Self : 1;
- Bytes.Self = (Bytes.Total + Bytes.Self) ? Bytes.Self : 1;
- Ms.Self = (Ms.Total + Ms.Self) ? Ms.Self : 1;
- Tasks.Self = (Tasks.Total + Tasks.Self) ? Tasks.Self : 1;
-
- Cpu.Total += Cpu.Self;
- Bytes.Total += Bytes.Self;
- Ms.Total += Ms.Self;
- Tasks.Total += Tasks.Self;
- }
-
- TCombinedWeights operator+(const TCombinedWeights &rhs) {
- Cpu.Total += rhs.Cpu.Total;
- Bytes.Total += rhs.Bytes.Total;
- Ms.Total += rhs.Ms.Total;
- Tasks.Total += rhs.Tasks.Total;
- return *this;
- }
-
- TWeight operator[](EFlameGraphType type) const {
- switch (type) {
- case CPU:
- return Cpu;
- case TIME:
- return Ms;
- case BYTES_OUTPUT:
- return Bytes;
- case TASKS:
- return Tasks;
- case ALL:
- throw yexception() << "Unsupported value for EFlameGraphType";
- }
- }
-
- // Cpu usage from this step stats
- TWeight Cpu;
- // Output bytes of step
- TWeight Bytes;
- // Time spent
- TWeight Ms;
- // Number of tasks used
- TWeight Tasks;
-
-};
-}
-
-class TPlanGraphEntry {
-public:
- TPlanGraphEntry(const TString &name, ui64 weightCpu, ui64 weightBytes, ui64 weightMs, ui64 weightTasks)
- : Name(name)//
- , Weights(weightCpu, weightBytes, weightMs, weightTasks) {};
-
- void AddChild(THolder<TPlanGraphEntry> &&child) {
- Children.emplace_back(std::move(child));
- }
-
-
- /// Builds svg for graph, starting from this node
- void SerializeToSvg(TOFStream &stream, float viewportHeight, EFlameGraphType type) const {
- auto baseParentWeight = Weights[type];
- SerializeToSvgImpl(stream,
- horOffset, viewportHeight - vertOffset - rectHeight,
- baseParentWeight.Total, type, viewPortWidth - 2 * horOffset);
- }
-
- /// Returns current depth of graph
- ui64 CalculateDepth(ui32 curDepth) {
- ui64 maxDepth = curDepth + 1;
- for (auto &child: Children) {
- maxDepth = Max(child->CalculateDepth(curDepth + 1), maxDepth);
- }
-
- return maxDepth;
- }
-
- /// After graph is build, we can recalculate weights considering children
- ///
- /// Not all plan entries has own statistics, for such entries we recalculate the weight as
- /// weight of all children
- TCombinedWeights CalculateWeight() {
- TCombinedWeights childrenWeights;
- for (auto &child: Children) {
- childrenWeights = childrenWeights + child->CalculateWeight();
- }
-
- Weights = Weights + childrenWeights;
- Weights.AddSelfToTotal();
-
- return Weights;
- }
-
- /// Returns Svg element corresponding to current graph and calls itself recursively for children
- float SerializeToSvgImpl(TOFStream &stream,
- float xOffset,
- float yOffset,
- ui64 parentWeight,
- EFlameGraphType type,
- float parentWidth) const {
- float width = parentWidth * (static_cast<float>(Weights[type].Total) / static_cast<float>(parentWeight));
- auto weight = Weights[type];
-
- float xChildOffset = xOffset;
- for (const auto &child: Children) {
- xChildOffset += child->SerializeToSvgImpl(stream, xChildOffset, yOffset - rectHeight - interElementOffset,
- weight.Total, type, width);
- }
-
- // Full description of step
- TString stepInfo = Sprintf("%s %s(self: %lu, total: %lu)",
- Name.c_str(), TypeName[type].c_str(), weight.Self, weight.Total);
-
- // Step name(we have to manually cut it, according to available space
- // 7 is found empirically
- auto symbolsAvailable = std::lround((width - 2 * textSideOffset) / 7);
- TString stepName;
- if (symbolsAvailable <= 2) {
- stepName = "";
- } else if (static_cast<ui64>(symbolsAvailable) < Name.length()) {
- stepName = Name.substr(0, symbolsAvailable - 2) + "..";
- } else {
- stepName = Name;
- }
-
- // Color falls more to red, if step takes more cpu, than it's children
- float selfToChildren = 0;
- if (weight.Total > weight.Self) {
- selfToChildren =
- 1 -
- Min(static_cast<float>(weight.Self) / static_cast<float>(weight.Total - weight.Self),
- 1.f);
- }
- TString color = Sprintf("rgb(255, %ld, 0)", std::lround(selfToChildren * 255));
-
- stream << Sprintf(FG_SVG_GRAPH_ELEMENT.data(),
- stepInfo.c_str(), // full text
- TypeName[type].c_str(),
- xOffset, yOffset, // position
- Max(width - interElementOffset, minElementWidth), rectHeight, // width and height
- color.c_str(), // element background color
- xOffset + textSideOffset, yOffset + textTopOffset, // Text position
- stepName.c_str() // short text
- );
- return width;
- }
-
-
-public:
- TString Name;
-
-
- TCombinedWeights Weights;
- TVector<THolder<TPlanGraphEntry>> Children;
-};
-
class TFlameGraphBuilder {
public:
@@ -252,36 +62,43 @@ public:
}
}
- void GenerateSvg(EFlameGraphType type, const THolder<TPlanGraphEntry> &planGraph, TOFStream &resultStream) {
+ static void GenerateSvg(EFlameGraphType type, const THolder<TPlanGraphEntry> &planGraph, TOFStream &resultStream) {
auto depth = planGraph->CalculateDepth(0);
- auto viewPortHeight = (2 * vertOffset) + (static_cast<float>(depth) * (rectHeight + 2 * interElementOffset));
- viewPortHeight *= static_cast<float>(TypeName.size());
+ auto viewPortHeight = (2 * VERTICAL_OFFSET) + (static_cast<double>(depth) * (RECT_HEIGHT + 2 * INTER_ELEMENT_OFFSET));
+ viewPortHeight *= static_cast<double>(TPlanGraphEntry::PlanGraphTypeName().size());
// offsets for static elements
- float detailsElementPos = viewPortHeight - 17;
- constexpr float searchPos = viewPortWidth - 110;
-
+ constexpr float detailsElementOffset = 17;
+ constexpr float searchPos = VIEWPORT_WIDTH - 110;
+ TString tmpTaskElements;
+ TStringOutput tmpTaskStream(tmpTaskElements);
resultStream
- << Sprintf(FG_SVG_HEADER.data(), viewPortWidth, viewPortHeight, viewPortWidth, viewPortHeight)
+ << Sprintf(FG_SVG_HEADER.data(), VIEWPORT_WIDTH, viewPortHeight, VIEWPORT_WIDTH, viewPortHeight)
<< FG_SVG_SCRIPT
- << Sprintf(FG_SVG_BACKGROUND.data(), viewPortWidth, viewPortHeight)
- << Sprintf(FG_SVG_INFO_BAR.data(), detailsElementPos)
+ << Sprintf(FG_SVG_BACKGROUND.data(), VIEWPORT_WIDTH, viewPortHeight)
<< FG_SVG_RESET_ZOOM
<< Sprintf(FG_SVG_SEARCH.data(), searchPos);
(void) type;
float i = 1;
- for (const auto &it: TypeName) {
+ for (const auto &it: TPlanGraphEntry::PlanGraphTypeName()) {
resultStream << Sprintf(FG_SVG_TITLE.data(),
- vertOffset + (viewPortHeight * (i - 1) / static_cast<float>(TypeName.size())),
+ VERTICAL_OFFSET + (viewPortHeight * (i - 1) /
+ static_cast<double>(TPlanGraphEntry::PlanGraphTypeName().size())),
it.second.c_str());
+ auto typedVertOffset =
+ viewPortHeight * i / static_cast<double>(TPlanGraphEntry::PlanGraphTypeName().size());
planGraph->SerializeToSvg(resultStream,
- viewPortHeight * i / static_cast<float>(TypeName.size()),
+ tmpTaskStream,
+ typedVertOffset,
it.first);
+ resultStream << Sprintf(FG_SVG_INFO_BAR.data(), it.second.c_str(), typedVertOffset - detailsElementOffset);
i += 1;
}
+ resultStream << FG_SVG_TASK_PROFILE_BACKGROUND;
+ resultStream << tmpTaskElements;
resultStream << FG_SVG_FOOTER;
}
@@ -349,16 +166,22 @@ private:
}
stageDescription += "]";
}
+
+ auto stageId = plan->GetValueByPath("PlanNodeId", '/');
auto cpuUsage = plan->GetValueByPath("Stats/TotalCpuTimeUs", '/');
auto outBytes = plan->GetValueByPath("Stats/TotalOutputBytes", '/');
auto ms = plan->GetValueByPath("Stats/TotalDurationMs", '/');
auto tasks = plan->GetValueByPath("Stats/TotalTasks", '/');
+ auto taskProfile = parseTasksProfile(plan->GetValueByPath("Stats", '/'));
+
auto planEntry = MakeHolder<TPlanGraphEntry>(stageDescription,
+ stageId ? stageId->GetUIntegerSafe() : 0,
cpuUsage ? cpuUsage->GetUIntegerSafe() : 0,
outBytes ? outBytes->GetUIntegerSafe() : 0,
ms ? ms->GetUIntegerSafe() : 0,
- tasks ? ms->GetUIntegerSafe() : 0
+ tasks ? tasks->GetUIntegerSafe() : 0,
+ std::move(taskProfile)
);
TJsonValue children;
@@ -370,6 +193,45 @@ private:
return planEntry;
}
+
+ static TVector<TTaskInfo> parseTasksProfile(TJsonValue *stats) {
+ TJsonValue computeNodes;
+ if (!stats || !stats->GetValue("ComputeNodes", &computeNodes)) {
+ return {};
+ }
+ TVector<TTaskInfo> taskInfo;
+ for (auto &node: computeNodes.GetArray()) {
+ TJsonValue tasks;
+ if (!node.GetValue("Tasks", &tasks)) {
+ continue;
+ }
+ for (auto &task: tasks.GetArray()) {
+ auto taskId = task.GetValueByPath("TaskId");
+ if (!taskId) {
+ continue;
+ }
+ auto cpu = task.GetValueByPath("ComputeTimeUs");
+ auto bytes = task.GetValueByPath("OutputBytes");
+
+ auto startMs = task.GetValueByPath("FirstRowTimeMs");
+ auto endMs = task.GetValueByPath("FinishTimeMs");
+ ui64 duration = 0;
+ if (startMs && endMs) {
+ duration = endMs->GetIntegerSafe() - startMs->GetUIntegerSafe();
+ }
+ TMap<EFlameGraphType, double> taskStats = {
+ {EFlameGraphType::CPU, cpu ? cpu->GetDoubleRobust() : 0},
+ {EFlameGraphType::TIME, duration},
+ {EFlameGraphType::BYTES_OUTPUT, bytes ? bytes->GetDoubleRobust() : 0}
+ };
+
+
+ taskInfo.push_back({.TaskId = taskId->GetUIntegerSafe(), .TaskStats = taskStats});
+ }
+ }
+ return taskInfo;
+ }
+
private:
TString ResultFile;
diff --git a/ydb/public/lib/stat_visualization/flame_graph_entry.cpp b/ydb/public/lib/stat_visualization/flame_graph_entry.cpp
new file mode 100644
index 0000000000..3ba1de0681
--- /dev/null
+++ b/ydb/public/lib/stat_visualization/flame_graph_entry.cpp
@@ -0,0 +1,218 @@
+#include "flame_graph_entry.h"
+#include "svg_script.h"
+
+#include <ydb/public/lib/ydb_cli/common/common.h>
+#include <library/cpp/json/json_reader.h>
+#include <util/folder/path.h>
+#include <util/generic/fwd.h>
+#include <util/generic/utility.h>
+#include <util/generic/strbuf.h>
+#include <util/string/printf.h>
+
+namespace NKikimr::NVisual {
+namespace {
+TMap<EFlameGraphType, TString> TypeName = {
+ {CPU, "CPU"},
+ {TIME, "TIME_MS"},
+ {BYTES_OUTPUT, "OUT_B"},
+ {TASKS, "TASKS"}
+};
+}
+
+void TPlanGraphEntry::SerializeToSvg(TOFStream &stageStream,TStringOutput &taskStream, double viewportHeight, EFlameGraphType type) const {
+ auto baseParentWeight = Weights[type];
+
+ SerializeToSvgImpl(stageStream, taskStream,
+ HORIZONTAL_OFFSET, viewportHeight - VERTICAL_OFFSET - RECT_HEIGHT,
+ static_cast<double>(baseParentWeight.Total), static_cast<double>(baseParentWeight.Total),
+ type, VIEWPORT_WIDTH - 2 * HORIZONTAL_OFFSET);
+}
+
+ui32 TPlanGraphEntry::CalculateDepth(ui32 curDepth) {
+ ui32 depthStep = Tasks.empty() ? 1 : 2;
+
+ ui32 maxDepth = curDepth + depthStep;
+ for (auto &child: Children) {
+ maxDepth = Max(child->CalculateDepth(curDepth + depthStep), maxDepth);
+ }
+
+ return maxDepth;
+}
+
+TCombinedWeights TPlanGraphEntry::CalculateWeight() {
+ TCombinedWeights childrenWeights;
+ for (auto &child: Children) {
+ childrenWeights = childrenWeights + child->CalculateWeight();
+ }
+
+ Weights = Weights + childrenWeights;
+ Weights.AddSelfToTotal();
+
+ return Weights;
+}
+
+double
+TPlanGraphEntry::SerializeToSvgImpl(TOFStream &stageStream, TStringOutput &taskStream, double xOffset, double yOffset,
+ double parentWeight, double visibleWeight,
+ EFlameGraphType type, double parentWidth) const {
+ double width = parentWidth * (visibleWeight / parentWeight);
+ auto weight = Weights[type];
+
+ bool shouldShowTaskProfile = !Tasks.empty() && type != TASKS;
+ double thisRectHeight = shouldShowTaskProfile ? 2 * RECT_HEIGHT : RECT_HEIGHT;
+
+ double xChildOffset = xOffset;
+
+ auto parentVisibleWeight = static_cast<double>(weight.Total);
+ for (const auto &child: Children) {
+ if (static_cast<double>(child->Weights[type].Total) / static_cast<double>(weight.Total) < 0.05) {
+ parentVisibleWeight += static_cast<double>(weight.Total) * 0.05f;
+ }
+ }
+
+ for (const auto &child: Children) {
+ xChildOffset += child->SerializeToSvgImpl(stageStream, taskStream, xChildOffset,
+ yOffset - thisRectHeight - INTER_ELEMENT_OFFSET,
+ parentVisibleWeight, Max(static_cast<double>(weight.Total) * 0.05f,
+ static_cast<double>(child->Weights[type].Total)),
+ type, width);
+ }
+
+
+ if (shouldShowTaskProfile) {
+ SerializeTaskProfile(taskStream,
+ xOffset, yOffset - RECT_HEIGHT,
+ type, width);
+ }
+ SerializeStage(stageStream,
+ xOffset, yOffset,
+ type, weight, width);
+
+
+ return width;
+}
+
+void TPlanGraphEntry::SerializeTaskProfile(TStringOutput &stream, double xOffset, double yOffset, EFlameGraphType type,
+ double parentWidth) const {
+ Y_ENSURE(type == CPU || type == BYTES_OUTPUT || type == TIME, "Unsupported task profile type");
+ if (Tasks.empty()) {
+ return;
+ }
+ double total = 0;
+ TVector<double> widthOfElements;
+ for (const auto &task: Tasks) {
+ total += task.TaskStats.Value(type, 0);
+ }
+
+ const ui8 startColorOffset = 100;
+ const ui8 endColorOffset = 160;
+
+ int i = 0;
+ ui8 colorOffset = startColorOffset;
+ auto TasksByStat = Tasks;
+ std::sort(TasksByStat.begin(), TasksByStat.end(), [&type](const TTaskInfo& a, const TTaskInfo& b)
+ {
+ return a.TaskStats.Value(type, 0) > b.TaskStats.Value(type, 0);
+ });
+
+ const double minVisibleWidth = 30.0;
+ double additionalWidth = 0.0;
+ for (const auto &task: TasksByStat) {
+ double width;
+ if (total == 0) {
+ // Corner case, when metrics for all tasks are 0(can happen for MS metrics for example)
+ width = parentWidth / TasksByStat.size();
+ } else {
+ width = (task.TaskStats.Value(type, 0) / total) * parentWidth;
+ }
+ // After zooming, object should be at least minVisibleWidth pixes wide, to be clickable
+ auto minWidth = minVisibleWidth / VIEWPORT_WIDTH * parentWidth;
+ if (width < minWidth) {
+ additionalWidth += minWidth - width;
+ width = minWidth;
+ }
+ widthOfElements.emplace_back(width);
+ }
+
+ for (auto &width: widthOfElements) {
+ width *= parentWidth / (parentWidth + additionalWidth);
+ }
+
+ for (const auto &task: TasksByStat) {
+ auto stepName = Sprintf("TaskId: %lu", task.TaskId);
+ auto stepDescription = Sprintf("%s (%s %.0f)", stepName.c_str(),
+ TypeName.Value(type, "").c_str(),
+ task.TaskStats.Value(type, 0));
+ stepName = CutTextForAvailableWidth(stepName, widthOfElements[i]);
+
+ stream << Sprintf(FG_SVG_TASK_PROFILE_ELEMENT.data(),
+ TypeName.Value(type, "").c_str(),
+ StageId,
+ stepDescription.c_str(), // full text
+ TypeName.Value(type, "").c_str(),
+ task.TaskStats.Value(type, 0.0),
+ xOffset, yOffset, // position
+ widthOfElements[i], RECT_HEIGHT, // width and height
+ colorOffset,
+ xOffset + TEXT_SIDE_OFFSET, yOffset + TEXT_TOP_OFFSET, // Text position
+ stepName.c_str() // short text
+ );
+ xOffset += widthOfElements[i];
+ i++;
+ colorOffset = colorOffset == endColorOffset ? startColorOffset : endColorOffset;
+ }
+}
+
+void TPlanGraphEntry::SerializeStage(TOFStream &stream, double xOffset, double yOffset, EFlameGraphType type,
+ const TWeight &weight, double width) const {
+
+ // Full description of step
+ TString stepInfo = Sprintf("%s %s(self: %lu, total: %lu)",
+ Name.c_str(), TypeName.Value(type, "").c_str(), weight.Self, weight.Total);
+
+ auto stepName = CutTextForAvailableWidth(Name, width);
+
+ // Color falls more to red, if step takes more cpu, than it's children
+ double selfToChildren = 0;
+ if (weight.Total > weight.Self) {
+ selfToChildren =
+ 1 -
+ Min(static_cast<double>(weight.Self) / static_cast<double>(weight.Total - weight.Self),
+ 1.0);
+ }
+ TString color = Sprintf("rgb(255, %ld, 0)", std::lround(selfToChildren * 255));
+
+ stream << Sprintf(FG_SVG_GRAPH_ELEMENT.data(),
+ stepInfo.c_str(), // full text
+ TypeName.Value(type, "").c_str(),
+ StageId,
+ xOffset, yOffset, // position
+ width, RECT_HEIGHT, // width and height
+ color.c_str(), // element background color
+ xOffset + TEXT_SIDE_OFFSET, yOffset + TEXT_TOP_OFFSET, // Text position
+ stepName.c_str() // short text
+ );
+}
+
+TMap<EFlameGraphType, TString> &TPlanGraphEntry::PlanGraphTypeName() {
+ return TypeName;
+}
+
+TString TPlanGraphEntry::CutTextForAvailableWidth(const TString &text, double width) {
+ // Step name(we have to manually cut it, according to available space
+ // 7 is found empirically
+ auto symbolsAvailable = std::lround((width - 2 * TEXT_SIDE_OFFSET) / 7);
+ if (symbolsAvailable <= 2) {
+ return "";
+ } else if (static_cast<ui64>(symbolsAvailable) < text.length()) {
+ return text.substr(0, symbolsAvailable - 2) + "..";
+ } else {
+ return text;
+ }
+}
+
+void TPlanGraphEntry::AddChild(THolder<TPlanGraphEntry> &&child) {
+ Children.emplace_back(std::move(child));
+}
+}
+
diff --git a/ydb/public/lib/stat_visualization/flame_graph_entry.h b/ydb/public/lib/stat_visualization/flame_graph_entry.h
new file mode 100644
index 0000000000..06d18bc167
--- /dev/null
+++ b/ydb/public/lib/stat_visualization/flame_graph_entry.h
@@ -0,0 +1,153 @@
+#pragma once
+
+#include "flame_graph_builder.h"
+#include "svg_script.h"
+#include "stat_visalization_error.h"
+
+#include <util/generic/map.h>
+#include <util/generic/vector.h>
+
+namespace NKikimr::NVisual {
+
+constexpr double RECT_HEIGHT = 15;
+constexpr double INTER_ELEMENT_OFFSET = 3;
+
+// Offsets from image limits
+constexpr double HORIZONTAL_OFFSET = 10;
+constexpr double VERTICAL_OFFSET = 30;
+
+constexpr double TEXT_SIDE_OFFSET = 3;
+constexpr double TEXT_TOP_OFFSET = 10.5;
+
+constexpr double VIEWPORT_WIDTH = 1200;
+
+struct TWeight {
+ explicit TWeight(ui64 self)
+ : Self(self) {};
+ ui64 Self;
+ ui64 Total = 0;
+};
+
+struct TCombinedWeights {
+ // Cpu usage from this step stats
+ TWeight Cpu;
+ // Output bytes of step
+ TWeight Bytes;
+ // Time spent
+ TWeight Ms;
+ // Number of tasks used
+ TWeight Tasks;
+
+ TCombinedWeights()
+ : Cpu(0), Bytes(0), Ms(0), Tasks(0) {}
+
+ TCombinedWeights(ui64 cpu, ui64 bytes, ui64 ms, ui64 tasks)
+ : Cpu(cpu), Bytes(bytes), Ms(ms), Tasks(tasks) {}
+
+ void AddSelfToTotal() {
+ Cpu.Self = (Cpu.Total + Cpu.Self) ? Cpu.Self : 1;
+ Bytes.Self = (Bytes.Total + Bytes.Self) ? Bytes.Self : 1;
+ Ms.Self = (Ms.Total + Ms.Self) ? Ms.Self : 1;
+ Tasks.Self = (Tasks.Total + Tasks.Self) ? Tasks.Self : 1;
+
+ Cpu.Total += Cpu.Self;
+ Bytes.Total += Bytes.Self;
+ Ms.Total += Ms.Self;
+ Tasks.Total += Tasks.Self;
+ }
+
+ TCombinedWeights operator+(const TCombinedWeights &rhs) {
+ Cpu.Total += rhs.Cpu.Total;
+ Bytes.Total += rhs.Bytes.Total;
+ Ms.Total += rhs.Ms.Total;
+ Tasks.Total += rhs.Tasks.Total;
+ return *this;
+ }
+
+ TWeight operator[](EFlameGraphType type) const {
+ switch (type) {
+ case CPU:
+ return Cpu;
+ case TIME:
+ return Ms;
+ case BYTES_OUTPUT:
+ return Bytes;
+ case TASKS:
+ return Tasks;
+ case ALL:
+ throw yexception() << "Unsupported value for FlameGraphType";
+ }
+ }
+};
+
+struct TTaskInfo {
+ ui64 TaskId = 0;
+ TMap<EFlameGraphType, double> TaskStats;
+};
+
+class TPlanGraphEntry {
+public:
+ TPlanGraphEntry(const TString &name, ui32 stageId,
+ ui64 weightCpu, ui64 weightBytes, ui64 weightMs, ui64 weightTasks,
+ TVector<TTaskInfo> &&taskInfo)
+ : Name(name)
+ , StageId(stageId)
+ , Weights(weightCpu, weightBytes, weightMs, weightTasks)
+ , Tasks(std::move(taskInfo)) {};
+
+ void AddChild(THolder<TPlanGraphEntry> &&child);
+
+
+ /// Builds svg for graph, starting from this node
+ void SerializeToSvg(TOFStream &stream, TStringOutput &taskStream, double viewportHeight, EFlameGraphType type) const;
+
+ /// Returns current depth of graph
+ ui32 CalculateDepth(ui32 curDepth);
+
+ /// After graph is build, we can recalculate weights considering children
+ ///
+ /// Not all plan entries has own statistics, for such entries we recalculate the weight as
+ /// weight of all children
+ TCombinedWeights CalculateWeight();
+
+ /// Returns Svg element corresponding to current graph and calls itself recursively for children
+ /// Writes stage and task elements to different streams, as we have to sort it later.
+ /// Task streams should be placed in the end of svg file, to give us correct Z axis alignment
+ double SerializeToSvgImpl(TOFStream &stageStream,
+ TStringOutput &taskStream,
+ double xOffset,
+ double yOffset,
+ double parentWeight,
+ double visibleWeight,
+ EFlameGraphType type,
+ double parentWidth) const;
+
+ static TMap<EFlameGraphType, TString> &PlanGraphTypeName();
+
+private:
+ void SerializeTaskProfile(TStringOutput &taskStream,
+ double xOffset,
+ double yOffset,
+ EFlameGraphType type,
+ double parentWidth) const;
+
+ void SerializeStage(TOFStream &stream,
+ double xOffset,
+ double yOffset,
+ EFlameGraphType type,
+ const TWeight &weight,
+ double width) const;
+
+ static TString CutTextForAvailableWidth(const TString &text, double width);
+
+public:
+ TString Name;
+ ui32 StageId;
+ TCombinedWeights Weights;
+ TVector<TTaskInfo> Tasks;
+
+ TVector<THolder<TPlanGraphEntry>> Children;
+};
+
+
+}
diff --git a/ydb/public/lib/stat_visualization/svg_script.h b/ydb/public/lib/stat_visualization/svg_script.h
index 1ae4ddd0bf..be90f0c65d 100644
--- a/ydb/public/lib/stat_visualization/svg_script.h
+++ b/ydb/public/lib/stat_visualization/svg_script.h
@@ -8,7 +8,11 @@ const std::string_view FG_SVG_HEADER = R"scr(<?xml version="1.0" standalone="no"
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" width="%.2f" height="%.2f" onload="init(evt)" viewBox="0 0 %.2f %.2f" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs><linearGradient id="background" y1="0" y2="1" x1="0" x2="0"><stop stop-color="#eeeeee" offset="5%%"/><stop stop-color="#eeeeb0" offset="95%%"/>
-</linearGradient></defs><style type="text/css">.graphElement:hover { stroke:black; stroke-width:0.5; cursor:pointer; }</style>" )scr";
+</linearGradient></defs>
+<style type="text/css">
+.graphElement:hover { stroke:black; stroke-width:0.5; cursor:pointer; }
+.TaskNavigationButton:hover {cursor:pointer; }
+</style>" )scr";
const std::string_view FG_SVG_FOOTER = R"scr(</svg>)scr";
@@ -18,32 +22,69 @@ const std::string_view FG_SVG_TITLE = R"scr(<text text-anchor="middle" x="600.00
y="%.2f" font-size="17" font-family="Verdana" fill="rgb(0, 0, 0)">%s</text>)scr";
const std::string_view FG_SVG_INFO_BAR = R"scr(
-<text id="infoBar" text-anchor="left" x="10.00" y="%.2f" font-size="12" font-family="Verdana" fill="rgb(0, 0, 0)"> </text>)scr";
+<text id="infoBar_%s" text-anchor="left" x="10.00" y="%.2f" font-size="12" font-family="Verdana" fill="rgb(0, 0, 0)"> </text>)scr";
const std::string_view FG_SVG_RESET_ZOOM = R"scr(
<text
id="resetZoom" onclick="resetZoom()" style="opacity:0.0;cursor:pointer" text-anchor="left" x="10.00" y="24.00"
font-size="12" font-family="Verdana" fill="rgb(0, 0, 0)">Reset Zoom</text>)scr";
-const std::string_view FG_SVG_SEARCH =R"scr(
+const std::string_view FG_SVG_SEARCH = R"scr(
<text id="search" onmouseover="onSearchHover()" onmouseout="onSearchOut()" onclick="startSearch()" style="opacity:0.1;cursor:pointer"
text-anchor="left" x="%.2f" y="24.00" font-size="12" font-family="Verdana"
fill="rgb(0, 0, 0)">Search</text><text id="matched" text-anchor="left" x="1090.00" y="1637.00" font-size="12"
font-family="Verdana" fill="rgb(0, 0, 0)"> </text> )scr";
const std::string_view FG_SVG_GRAPH_ELEMENT = R"scr(
-<g class="graphElement" onmouseover="onGraphMouseOver(this)" onmouseout="onGraphMouseOut()" onclick="zoom(this)">
- <title>%s</title><rect data-type="%s" x="%.2f" y="%.2f" width="%.2f" height="%.2f" fill="%s" />
+<g class="graphElement" onmouseover="onGraphMouseOver(this)" onmouseout="onGraphMouseOut(this)" onclick="zoom(this)">
+ <title>%s</title><rect data-type="%s" stage-id="%u" x="%.2f" y="%.2f" width="%.2f" height="%.2f" fill="%s" />
<text text-anchor="left" x="%.2f" y="%.2f" font-size="12" font-family="Verdana" fill="rgb(0, 0, 0)">%s</text>
</g>)scr";
-const std::string_view FG_SVG_SCRIPT = R"scr(<script type="text/ecmascript"><![CDATA[var nametype = 'Function:';
+const std::string_view FG_SVG_TASK_PROFILE_ELEMENT = R"scr(
+<g class="taskProfile-%s-%u" onmouseover="onGraphMouseOver(this)" onmouseout="onGraphMouseOut(this)" onclick="showTasks(this)">
+ <title>%s</title><rect data-type="%s" data-weight="%f" x="%.2f" y="%.2f" width="%.2f" height="%.2f" fill="rgb(139, 174, %d)" />
+ <text text-anchor="left" x="%.2f" y="%.2f" font-size="12" font-family="Verdana" fill="rgb(0, 0, 0)">%s</text>
+</g>)scr";
+
+const std::string_view FG_SVG_TASK_PROFILE_BACKGROUND = R"scr(
+<g class="taskBackground" style="display:none">
+ <rect x="0" y="0" width="0" height="0" fill="url(#background)" />
+ <rect x="0.00" y="0" width="0" height="0" fill="#cee7e9" />
+ <text id="TaskTotal" text-anchor="left" x="10.00" y="0" font-size="12" font-family="Verdana" fill="rgb(0, 0, 0)"></text>
+ <g class="TaskNavigationButton" onclick="goToFirstTask()" >
+ <rect x="10.00" y="0" width="36.00" height="15" fill="rgb(0, 200, 200)" style="display:none"/>
+ <text text-anchor="middle" x="28.00" y="810" font-size="12" font-family="Verdana" fill="rgb(0, 0, 0)" style="display:none">&lt;&lt;</text>
+ </g>
+ <g class="TaskNavigationButton" onclick="goToPre≤vTask()" >
+ <rect x="50.00" y="0" width="36.00" height="15" fill="rgb(0, 200, 200)" style="display:none"/>
+ <text text-anchor="middle" x="68.00" y="810" font-size="12" font-family="Verdana" fill="rgb(0, 0, 0)" style="display:none">&lt;</text>
+ </g>
+ <g class="TaskNavigationButton" onclick="goToNextTask()" >
+ <rect x="90.00" y="0" width="36.00" height="15" fill="rgb(0, 200, 200)" style="display:none"/>
+ <text text-anchor="middle" x="108.00" y="810" font-size="12" font-family="Verdana" fill="rgb(0, 0, 0)" style="display:none">&gt;</text>
+ </g>
+ <g class="TaskNavigationButton" onclick="goToLastTask()" >
+ <rect x="130.00" y="0" width="36.00" height="15" fill="rgb(0, 200, 200)" style="display:none"/>
+ <text text-anchor="middle" x="148.00" y="810" font-size="12" font-family="Verdana" fill="rgb(0, 0, 0)" style="display:none">&gt;&gt;</text>
+ </g>
+ <g class="TaskNavigationButton" onclick="hideTasks()" >
+ <rect x="210.00" y="0" width="60.00" height="15" fill="rgb(0, 200, 200)" style="display:none"/>
+ <text text-anchor="middle" x="240.00" y="810" font-size="12" font-family="Verdana" fill="rgb(0, 0, 0)" style="display:none">Close</text>
+ </g>
+</g>)scr";
+
+const std::string_view FG_SVG_SCRIPT = R"scr(
+<script type="text/ecmascript"><![CDATA[var nametype = 'Function:';
var fontSize = 12;
var fontWidth = 0.59;
var xpad = 10;
+var tasksPerPage = 50;
+var taskListOffset = 0;
+var activeTaskStageId = -1;
+var activeTaskDataType = "";
]]><![CDATA[var infoBar, searchButton, foundText, svg;
function init(evt) {
- infoBar = document.getElementById("infoBar").firstChild;
searchButton = document.getElementById("search");
foundText = document.getElementById("matched");
svg = document.getElementsByTagName("svg")[0];
@@ -52,9 +93,16 @@ function init(evt) {
// Show element title in bottom bar
function onGraphMouseOver(node) { // show
info = getNodeTitle(node);
- infoBar.nodeValue = nametype + " " + info;
+ dataType = getNodeDataType(node)
+
+ var infoBar = document.getElementById("infoBar_" + dataType).firstChild;
+ infoBar.nodeValue = info;
}
-function onGraphMouseOut() { // clear
+
+function onGraphMouseOut(node) { // clear
+ dataType = getNodeDataType(node)
+
+ var infoBar = document.getElementById("infoBar_" + dataType).firstChild;
infoBar.nodeValue = ' ';
}
// ctrl-F for search
@@ -64,38 +112,52 @@ window.addEventListener("keydown",function (e) {
startSearch();
}
})
-// functions
+// helper functions
function findElement(parent, name, attr) {
var children = parent.childNodes;
for (var i=0; i<children.length;i++) {
+
if (children[i].tagName == name)
return (attr != undefined) ? children[i].attributes[attr].value : children[i];
}
return;
}
-function backupAttribute(element, attr, value) {
- if (element.attributes[attr + ".bk"] != undefined) return;
+
+function backupAttribute(element, attr, mark, value) {
if (element.attributes[attr] == undefined) return;
- if (value == undefined) value = element.attributes[attr].value;
- element.setAttribute(attr + ".bk", value);
+ if (element.attributes[attr + ".bk" + mark] == undefined){
+ oldValue = element.attributes[attr].value;
+ element.setAttribute(attr + ".bk" + mark, oldValue);
+ }
+ if (value != undefined) {
+ element.setAttribute(attr, value);
+ }
}
-function restoreAttribute(element, attr) {
- if (element.attributes[attr + ".bk"] == undefined) return;
- element.attributes[attr].value = element.attributes[attr + ".bk"].value;
- element.removeAttribute(attr + ".bk");
+function restoreAttribute(element, attr, mark) {
+ if (element.attributes[attr + ".bk" + mark] == undefined) return;
+ element.attributes[attr].value = element.attributes[attr + ".bk" + mark].value;
+ element.removeAttribute(attr + ".bk" + mark);
}
function getNodeTitle(element) {
var text = findElement(element, "title").firstChild.nodeValue;
return (text)
}
+function getNodeDataType(element) {
+ var text = findElement(element, "rect", "data-type");
+ return text
+}
function adjustText(element) {
+ if(findElement(element, "title") == undefined) {
+ return;
+ }
+
var textElement = findElement(element, "text");
var rect = findElement(element, "rect");
var width = parseFloat(rect.attributes["width"].value) - 3;
var title = findElement(element, "title").textContent.replace(/\\([^(]*\\)\$/,"");
textElement.attributes["x"].value = parseFloat(rect.attributes["x"].value) + 3;
- // Not enought space for any text
+ // Not enough space for any text
if (2*fontSize*fontWidth > width) {
textElement.textContent = "";
return;
@@ -115,17 +177,15 @@ function adjustText(element) {
textElement.textContent = "";
}
-
+// Zoom processing
function zoomChild(element, x, ratio) {
if (element.attributes != undefined) {
if (element.attributes["x"] != undefined) {
- backupAttribute(element, "x");
- element.attributes["x"].value = (parseFloat(element.attributes["x"].value) - x - xpad) * ratio + xpad;
+ backupAttribute(element, "x", "zoom", (parseFloat(element.attributes["x"].value) - x - xpad) * ratio + xpad);
if(element.tagName == "text") element.attributes["x"].value = findElement(element.parentNode, "rect", "x") + 3;
}
if (element.attributes["width"] != undefined) {
- backupAttribute(element, "width");
- element.attributes["width"].value = parseFloat(element.attributes["width"].value) * ratio;
+ backupAttribute(element, "width", "zoom", parseFloat(element.attributes["width"].value) * ratio);
}
}
if (element.childNodes == undefined) return;
@@ -136,12 +196,10 @@ function zoomChild(element, x, ratio) {
function zoomParent(element) {
if (element.attributes) {
if (element.attributes["x"] != undefined) {
- backupAttribute(element, "x");
- element.attributes["x"].value = xpad;
+ backupAttribute(element, "x", "zoom", xpad);
}
if (element.attributes["width"] != undefined) {
- backupAttribute(element, "width");
- element.attributes["width"].value = parseInt(svg.width.baseVal.value) - (xpad*2);
+ backupAttribute(element, "width", "zoom", parseInt(svg.width.baseVal.value) - (xpad*2));
}
}
if (element.childNodes == undefined) return;
@@ -150,7 +208,7 @@ function zoomParent(element) {
}
}
-function zoomElement(element, type, xmin, xmax, ymin, ratio) {
+function zoomElement(element, type, xmin, xmax, ymin, ratio, overrideOnClick) {
var rect = findElement(element, "rect").attributes;
if(rect["data-type"].value != type) {
@@ -158,14 +216,16 @@ function zoomElement(element, type, xmin, xmax, ymin, ratio) {
}
var currentX = parseFloat(rect["x"].value);
- var currentWidtn = parseFloat(rect["width"].value);
- var comparisionOffset = 0.0001;
+ var currentWidth = parseFloat(rect["width"].value);
+ var comparisonOffset = 0.0001;
if (parseFloat(rect["y"].value) > ymin) {
- if (currentX <= xmin && (currentX+currentWidtn+comparisionOffset) >= xmax) {
+ if (currentX <= xmin && (currentX+currentWidth+comparisonOffset) >= xmax) {
element.style["opacity"] = "0.5";
zoomParent(element);
- element.onclick = function(element){resetZoom(); zoom(this);};
+ if(overrideOnClick && element.onclick) {
+ element.onclick = function(element){resetZoom(); zoom(this);};
+ }
adjustText(element);
}
else {
@@ -173,17 +233,19 @@ function zoomElement(element, type, xmin, xmax, ymin, ratio) {
}
}
else {
- if (currentX < xmin || currentX + comparisionOffset >= xmax) {
+ if (currentX < xmin || currentX + comparisonOffset >= xmax) {
element.style["display"] = "none";
}
else {
zoomChild(element, xmin, ratio);
- element.onclick = function(element){zoom(this);};
+ if(overrideOnClick && element.onclick) {
+ element.onclick = function(element){zoom(this);};
+ }
adjustText(element);
}
}
-
}
+
function zoom(node) {
var attr = findElement(node, "rect").attributes;
var type = attr["data-type"].value
@@ -194,16 +256,26 @@ function zoom(node) {
var ratio = (svg.width.baseVal.value - 2*xpad) / width;
var resetZoomBtn = document.getElementById("resetZoom");
resetZoomBtn.style["opacity"] = "1.0";
- var el = document.getElementsByTagName("g");
+ var el = document.getElementsByClassName("graphElement");
for(var i=0;i<el.length;i++){
- zoomElement(el[i], type, xmin, xmax, ymin, ratio)
+ zoomElement(el[i], type, xmin, xmax, ymin, ratio, true)
+ }
+
+ el = document.getElementsByTagName("g");
+ for (var i=0; i < el.length; i++) {
+ className = el[i].attributes["class"].value;
+ if(!className.startsWith("taskProfile"))
+ {
+ continue;
+ }
+ zoomElement(el[i], type, xmin, xmax, ymin, ratio, false)
}
}
function resetElementZoom(element) {
if (element.attributes != undefined) {
- restoreAttribute(element, "x");
- restoreAttribute(element, "width");
+ restoreAttribute(element, "x", "zoom");
+ restoreAttribute(element, "width", "zoom");
}
if (element.childNodes == undefined) return;
@@ -224,13 +296,209 @@ function resetZoom() {
adjustText(element[i]);
}
}
+
+// Floating task view
+
+function getElementByStageId(stageId) {
+ var el = document.getElementsByClassName("graphElement");
+
+ for (var i=0; i < el.length; i++) {
+ rect = findElement(el[i], "rect")
+ stageIdAttr = rect.attributes['stage-id']
+ if( stageIdAttr != undefined && stageIdAttr.value == stageId)
+ {
+ return el[i]
+ }
+ }
+}
+
+function showNavigationButtons(tasksZoneBottom) {
+ buttons = document.getElementsByClassName("TaskNavigationButton");
+ for( var i=0; i< buttons.length; i++)
+ {
+ rect = findElement(buttons[i], "rect")
+ rect.attributes["y"].value = tasksZoneBottom - 15
+ rect.style["display"] = "block"
+
+ text = findElement(buttons[i], "text")
+ text.attributes["y"].value = tasksZoneBottom - 5
+ text.style["display"] = "block"
+ }
+}
+
+function hideNavigationButtons(tasksZoneBottom) {
+ buttons = document.getElementsByClassName("TaskNavigationButton");
+ for( var i=0; i< buttons.length; i++)
+ {
+ findElement(buttons[i], "rect").style["display"] = "none"
+ findElement(buttons[i], "text").style["display"] = "none"
+ }
+}
+
+function showTasks(node){
+ tasks = document.getElementsByTagName("g");
+ for(var i = 0; i < tasks.length; i++ )
+ {
+ className = tasks[i].attributes["class"].value;
+ if(className.startsWith("taskProfile"))
+ {
+ tasks[i].style["display"] = "none";
+ }
+ }
+
+ className = node.attributes["class"].value.split('-', 3)
+ stageId = className[2];
+ dataType = className[1];
+ activeTaskStageId = stageId;
+ activeTaskDataType = dataType;
+
+ showTasksForStage()
+}
+
+function showTaskBackground(taskZoneY, taskZoneWidth, taskZoneHeight) {
+ taskBackground = document.getElementsByClassName("taskBackground")[0];
+ taskBackground.style["display"] = "block"
+
+ tbRect = taskBackground.children[0]
+ tbRect.style["display"] = "block"
+ tbRect.attributes["width"].value = svg.width.baseVal.value;
+ tbRect.attributes["height"].value = svg.height.baseVal.value;
+
+ tbRect = taskBackground.children[1]
+ tbRect.style["display"] = "block"
+ tbRect.attributes["y"].value = taskZoneY;
+ tbRect.attributes["width"].value = taskZoneWidth;
+ tbRect.attributes["height"].value = taskZoneHeight;
+ showNavigationButtons(taskZoneHeight + taskZoneY);
+}
+
+function hideTaskBackground() {
+ taskBackground = document.getElementsByClassName("taskBackground")[0];
+ taskBackground.style["display"] = "none"
+
+ tbRect = taskBackground.children[0]
+ tbRect.style["display"] = "none"
+
+ tbRect = taskBackground.children[1]
+ tbRect.style["display"] = "none"
+ hideNavigationButtons();
+}
+
+function showTasksForStage(){
+ stageId = activeTaskStageId
+ stageRect = getElementByStageId(stageId)
+ zoom(stageRect)
+
+ taskZoneY = 60;
+ taskZoneX = 10
+ taskFieldHeight = 15;
+ taskZoneWidth = svg.width.baseVal.value;
+
+ tasks = document.getElementsByClassName("taskProfile-" + activeTaskDataType + "-" + stageId);
+ maxWeight = parseFloat(findElement(tasks[0], "rect").attributes["data-weight"].value);
+ maxWeight = maxWeight == 0 ? 1 : maxWeight;
+
+ tasksNum=(tasksPerPage < tasks.length) ? tasksPerPage : tasks.length;
+ taskZoneHeight = (4 + tasksNum) * taskFieldHeight;
+
+ showTaskBackground(taskZoneY, taskZoneWidth, taskZoneHeight);
+
+ if (tasksPerPage >= tasks.length) {
+ taskListOffset = 0;
+ }
+ else if(taskListOffset + tasksPerPage > tasks.length ) {
+ taskListOffset = tasks.length - tasksPerPage;
+ }
+
+ for (var i=0; i < tasks.length; i++) {
+ rect = findElement(tasks[i], "rect");
+ text = findElement(tasks[i], "text");
+
+ if(i < taskListOffset || i >= taskListOffset + tasksPerPage) {
+ tasks[i].style["display"] = "none"
+ continue;
+ }
+
+ tasks[i].style["display"] = "block"
+
+ weight = rect.attributes["data-weight"].value;
+ heightOffset = i - taskListOffset;
+ backupAttribute(rect, "y", "tasks", taskZoneY + (heightOffset + 1) * taskFieldHeight);
+ backupAttribute(rect, "x", "tasks", taskZoneX);
+ backupAttribute(rect, "width", "tasks", (taskZoneWidth - 10) * weight / maxWeight);
+
+ backupAttribute(text, "y", "tasks", taskZoneY + (heightOffset + 2) * taskFieldHeight - 2);
+ backupAttribute(text, "x", "tasks", taskZoneX + 2);
+ text.textContent = getNodeTitle(tasks[i]);
+ }
+
+ total = document.getElementById("TaskTotal")
+ total.style["display"] = "block"
+ total.setAttribute("y", taskZoneY + taskZoneHeight - 24);
+
+ lastTask = taskListOffset + tasksPerPage > (tasks.length -1) ? tasks.length : taskListOffset + tasksPerPage + 1;
+ total.textContent = "Showing tasks: " + (taskListOffset + 1) + "-" + lastTask + " from " + tasks.length + " from stage " + stageId;
+}
+
+function goToFirstTask() {
+ taskListOffset = 0;
+ showTasksForStage();
+}
+function goToPrevTask() {
+ taskListOffset = taskListOffset < tasksPerPage ? 0 : taskListOffset - tasksPerPage;
+ showTasksForStage();
+}
+function goToNextTask() {
+ taskListOffset += tasksPerPage;
+ showTasksForStage();
+}
+function goToLastTask() {
+ taskListOffset = Number.MAX_SAFE_INTEGER;
+ showTasksForStage();
+}
+
+function hideTasks() {
+ tasks = document.getElementsByTagName("g");
+ for (var i=0; i < tasks.length; i++) {
+ className = tasks[i].attributes["class"].value;
+ if(!className.startsWith("taskProfile"))
+ {
+ continue;
+ }
+ tasks[i].style["display"] = "block"
+
+ rect = findElement(tasks[i], "rect");
+ restoreAttribute(rect, "y", "tasks");
+ restoreAttribute(rect, "x", "tasks");
+ restoreAttribute(rect, "width", "tasks");
+
+ text = findElement(tasks[i], "text");
+ restoreAttribute(text, "y", "tasks");
+ restoreAttribute(text, "x", "tasks");
+
+ adjustText(tasks[i])
+ }
+ hideTaskBackground();
+
+ total = document.getElementById("TaskTotal")
+ total.style["display"] = "none"
+
+ stageRect = getElementByStageId(activeTaskStageId)
+ resetZoom()
+ zoom(stageRect)
+
+ activeTaskStageId = -1;
+ taskListOffset=0;
+}
+
// search
function dropSearch() {
var el = document.getElementsByTagName("rect");
for (var i=0; i < el.length; i++) {
- restoreAttribute(el[i], "fill")
+ restoreAttribute(el[i], "fill", "search")
}
}
+
function startSearch() {
if (!searching) {
var pattern = prompt("Enter a string to search (regexp allowed)", "");
@@ -261,8 +529,7 @@ function search(pattern) {
continue;
if (titleFunc.match(regex)) {
- backupAttribute(rect, "fill");
- rect.attributes["fill"].value = 'rgb(120,80,230)';
+ backupAttribute(rect, "fill", "search", 'rgb(120,80,230)');
searching = 1;
}
}
@@ -272,9 +539,11 @@ function search(pattern) {
searchButton.firstChild.nodeValue = "Reset Search"
}
}
+
function onSearchHover() {
searchButton.style["opacity"] = "1.0";
}
+
function onSearchOut() {
if (searching) {
searchButton.style["opacity"] = "1.0";
diff --git a/ydb/public/lib/stat_visualization/ya.make b/ydb/public/lib/stat_visualization/ya.make
index f7834b99cc..f47dbcb41a 100644
--- a/ydb/public/lib/stat_visualization/ya.make
+++ b/ydb/public/lib/stat_visualization/ya.make
@@ -2,6 +2,7 @@ LIBRARY()
SRCS(
flame_graph_builder.cpp
+ flame_graph_entry.cpp
)
PEERDIR(
diff --git a/ydb/public/lib/ydb_cli/commands/ydb_yql.cpp b/ydb/public/lib/ydb_cli/commands/ydb_yql.cpp
index 4f352b0cf1..af31259241 100644
--- a/ydb/public/lib/ydb_cli/commands/ydb_yql.cpp
+++ b/ydb/public/lib/ydb_cli/commands/ydb_yql.cpp
@@ -28,7 +28,7 @@ void TCommandYql::Config(TConfig& config) {
config.Opts->AddLongOption("stats", "Collect statistics mode [none, basic, full]")
.RequiredArgument("[String]").StoreResult(&CollectStatsMode);
config.Opts->AddLongOption("flame-graph", "Path for statistics flame graph image, works only with full stats")
- .RequiredArgument("[Path]").StoreResult(&FlameGraphFile);
+ .RequiredArgument("[Path]").StoreResult(&FlameGraphPath);
config.Opts->AddLongOption('s', "script", "Text of script to execute").RequiredArgument("[String]").StoreResult(&Script);
config.Opts->AddLongOption('f', "file", "Script file").RequiredArgument("PATH").StoreResult(&ScriptFile);
@@ -77,6 +77,10 @@ void TCommandYql::Parse(TConfig& config) {
if (ScriptFile) {
Script = ReadFromFile(ScriptFile, "script");
}
+ if(FlameGraphPath && FlameGraphPath->Empty())
+ {
+ throw TMisuseException() << "FlameGraph path can not be empty.";
+ }
ParseParameters(config);
}
@@ -91,7 +95,7 @@ int TCommandYql::RunCommand(TConfig& config, const TString& script) {
NScripting::TExecuteYqlRequestSettings settings;
settings.CollectQueryStats(ParseQueryStatsMode(CollectStatsMode, NTable::ECollectQueryStatsMode::None));
- if (FlameGraphFile && (settings.CollectQueryStats_ != NTable::ECollectQueryStatsMode::Full
+ if (FlameGraphPath && (settings.CollectQueryStats_ != NTable::ECollectQueryStatsMode::Full
&& settings.CollectQueryStats_ != NTable::ECollectQueryStatsMode::Profile)) {
throw TMisuseException() << "Flame graph is available for full or profile stats. Current: "
+ (CollectStatsMode.Empty() ? "none" : CollectStatsMode) + '.';
@@ -180,11 +184,11 @@ bool TCommandYql::PrintResponse(NScripting::TYqlResultPartIterator& result) {
TQueryPlanPrinter queryPlanPrinter(OutputFormat, /* analyzeMode */ true);
queryPlanPrinter.Print(*fullStats);
- if (FlameGraphFile) {
+ if (FlameGraphPath) {
try {
- NKikimr::NVisual::GenerateFlameGraphSvg(FlameGraphFile, *fullStats,
+ NKikimr::NVisual::GenerateFlameGraphSvg(*FlameGraphPath, *fullStats,
NKikimr::NVisual::EFlameGraphType::CPU);
- Cout << "Resource usage flame graph is successfully saved to " << FlameGraphFile << Endl;
+ Cout << "Resource usage flame graph is successfully saved to " << *FlameGraphPath << Endl;
}
catch (const yexception& ex) {
Cout << "Can't save resource usage flame graph, error: " << ex.what() << Endl;
@@ -201,4 +205,3 @@ bool TCommandYql::PrintResponse(NScripting::TYqlResultPartIterator& result) {
}
}
-
diff --git a/ydb/public/lib/ydb_cli/commands/ydb_yql.h b/ydb/public/lib/ydb_cli/commands/ydb_yql.h
index 3d790e1139..43b895c5b1 100644
--- a/ydb/public/lib/ydb_cli/commands/ydb_yql.h
+++ b/ydb/public/lib/ydb_cli/commands/ydb_yql.h
@@ -26,7 +26,7 @@ private:
bool PrintResponse(NScripting::TYqlResultPartIterator& result);
TString CollectStatsMode;
- TString FlameGraphFile;
+ TMaybe<TString> FlameGraphPath;
TString Script;
TString ScriptFile;
};