Browse Source

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
vladkoronnov 1 year ago
parent
commit
914b0463af

+ 1 - 0
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
 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_builder.cpp
+  ${CMAKE_SOURCE_DIR}/ydb/public/lib/stat_visualization/flame_graph_entry.cpp
 )
 )

+ 1 - 0
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
 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_builder.cpp
+  ${CMAKE_SOURCE_DIR}/ydb/public/lib/stat_visualization/flame_graph_entry.cpp
 )
 )

+ 1 - 0
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
 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_builder.cpp
+  ${CMAKE_SOURCE_DIR}/ydb/public/lib/stat_visualization/flame_graph_entry.cpp
 )
 )

+ 1 - 0
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
 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_builder.cpp
+  ${CMAKE_SOURCE_DIR}/ydb/public/lib/stat_visualization/flame_graph_entry.cpp
 )
 )

+ 66 - 204
ydb/public/lib/stat_visualization/flame_graph_builder.cpp

@@ -1,4 +1,5 @@
 #include "flame_graph_builder.h"
 #include "flame_graph_builder.h"
+#include "flame_graph_entry.h"
 #include "svg_script.h"
 #include "svg_script.h"
 #include "stat_visalization_error.h"
 #include "stat_visalization_error.h"
 
 
@@ -12,197 +13,6 @@
 
 
 namespace NKikimr::NVisual {
 namespace NKikimr::NVisual {
 using namespace NJson;
 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 {
 class TFlameGraphBuilder {
 public:
 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 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
         // 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
         resultStream
-                << Sprintf(FG_SVG_HEADER.data(), viewPortWidth, viewPortHeight, viewPortWidth, viewPortHeight)
+                << Sprintf(FG_SVG_HEADER.data(), VIEWPORT_WIDTH, viewPortHeight, VIEWPORT_WIDTH, viewPortHeight)
                 << FG_SVG_SCRIPT
                 << 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
                 << FG_SVG_RESET_ZOOM
                 << Sprintf(FG_SVG_SEARCH.data(), searchPos);
                 << Sprintf(FG_SVG_SEARCH.data(), searchPos);
         (void) type;
         (void) type;
         float i = 1;
         float i = 1;
-        for (const auto &it: TypeName) {
+        for (const auto &it: TPlanGraphEntry::PlanGraphTypeName()) {
             resultStream << Sprintf(FG_SVG_TITLE.data(),
             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());
                                     it.second.c_str());
+            auto typedVertOffset =
+                    viewPortHeight * i / static_cast<double>(TPlanGraphEntry::PlanGraphTypeName().size());
             planGraph->SerializeToSvg(resultStream,
             planGraph->SerializeToSvg(resultStream,
-                                      viewPortHeight * i / static_cast<float>(TypeName.size()),
+                                      tmpTaskStream,
+                                      typedVertOffset,
                                       it.first);
                                       it.first);
+            resultStream << Sprintf(FG_SVG_INFO_BAR.data(), it.second.c_str(), typedVertOffset - detailsElementOffset);
             i += 1;
             i += 1;
         }
         }
 
 
+        resultStream << FG_SVG_TASK_PROFILE_BACKGROUND;
+        resultStream << tmpTaskElements;
         resultStream << FG_SVG_FOOTER;
         resultStream << FG_SVG_FOOTER;
     }
     }
 
 
@@ -349,16 +166,22 @@ private:
             }
             }
             stageDescription += "]";
             stageDescription += "]";
         }
         }
+
+        auto stageId = plan->GetValueByPath("PlanNodeId", '/');
         auto cpuUsage = plan->GetValueByPath("Stats/TotalCpuTimeUs", '/');
         auto cpuUsage = plan->GetValueByPath("Stats/TotalCpuTimeUs", '/');
         auto outBytes = plan->GetValueByPath("Stats/TotalOutputBytes", '/');
         auto outBytes = plan->GetValueByPath("Stats/TotalOutputBytes", '/');
         auto ms = plan->GetValueByPath("Stats/TotalDurationMs", '/');
         auto ms = plan->GetValueByPath("Stats/TotalDurationMs", '/');
         auto tasks = plan->GetValueByPath("Stats/TotalTasks", '/');
         auto tasks = plan->GetValueByPath("Stats/TotalTasks", '/');
 
 
+        auto taskProfile = parseTasksProfile(plan->GetValueByPath("Stats", '/'));
+
         auto planEntry = MakeHolder<TPlanGraphEntry>(stageDescription,
         auto planEntry = MakeHolder<TPlanGraphEntry>(stageDescription,
+                                                     stageId ? stageId->GetUIntegerSafe() : 0,
                                                      cpuUsage ? cpuUsage->GetUIntegerSafe() : 0,
                                                      cpuUsage ? cpuUsage->GetUIntegerSafe() : 0,
                                                      outBytes ? outBytes->GetUIntegerSafe() : 0,
                                                      outBytes ? outBytes->GetUIntegerSafe() : 0,
                                                      ms ? ms->GetUIntegerSafe() : 0,
                                                      ms ? ms->GetUIntegerSafe() : 0,
-                                                     tasks ? ms->GetUIntegerSafe() : 0
+                                                     tasks ? tasks->GetUIntegerSafe() : 0,
+                                                     std::move(taskProfile)
         );
         );
 
 
         TJsonValue children;
         TJsonValue children;
@@ -370,6 +193,45 @@ private:
         return planEntry;
         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:
 private:
     TString ResultFile;
     TString ResultFile;
 
 

+ 218 - 0
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));
+}
+}
+

+ 153 - 0
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;
+};
+
+
+}

+ 312 - 43
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">
 <!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">
 <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%%"/>
 <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";
 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";
         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(
 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(
 const std::string_view FG_SVG_RESET_ZOOM = R"scr(
 <text
 <text
         id="resetZoom" onclick="resetZoom()" style="opacity:0.0;cursor:pointer" text-anchor="left" x="10.00" y="24.00"
         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";
         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 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"
         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"
         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";
         font-family="Verdana" fill="rgb(0, 0, 0)"> </text> )scr";
 
 
 const std::string_view FG_SVG_GRAPH_ELEMENT = R"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>
     <text text-anchor="left" x="%.2f" y="%.2f" font-size="12" font-family="Verdana" fill="rgb(0, 0, 0)">%s</text>
 </g>)scr";
 </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 fontSize = 12;
 var fontWidth = 0.59;
 var fontWidth = 0.59;
 var xpad = 10;
 var xpad = 10;
+var tasksPerPage = 50;
+var taskListOffset = 0;
+var activeTaskStageId = -1;
+var activeTaskDataType = "";
 ]]><![CDATA[var infoBar, searchButton, foundText, svg;
 ]]><![CDATA[var infoBar, searchButton, foundText, svg;
 function init(evt) {
 function init(evt) {
-    infoBar = document.getElementById("infoBar").firstChild;
     searchButton = document.getElementById("search");
     searchButton = document.getElementById("search");
     foundText = document.getElementById("matched");
     foundText = document.getElementById("matched");
     svg = document.getElementsByTagName("svg")[0];
     svg = document.getElementsByTagName("svg")[0];
@@ -52,9 +93,16 @@ function init(evt) {
 // Show element title in bottom bar
 // Show element title in bottom bar
 function onGraphMouseOver(node) {		// show
 function onGraphMouseOver(node) {		// show
     info = getNodeTitle(node);
     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 = ' ';
     infoBar.nodeValue = ' ';
 }
 }
 // ctrl-F for search
 // ctrl-F for search
@@ -64,38 +112,52 @@ window.addEventListener("keydown",function (e) {
             startSearch();
             startSearch();
         }
         }
 })
 })
-// functions
+// helper functions
 function findElement(parent, name, attr) {
 function findElement(parent, name, attr) {
     var children = parent.childNodes;
     var children = parent.childNodes;
     for (var i=0; i<children.length;i++) {
     for (var i=0; i<children.length;i++) {
+
         if (children[i].tagName == name)
         if (children[i].tagName == name)
             return (attr != undefined) ? children[i].attributes[attr].value : children[i];
             return (attr != undefined) ? children[i].attributes[attr].value : children[i];
     }
     }
     return;
     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 (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) {
 function getNodeTitle(element) {
     var text = findElement(element, "title").firstChild.nodeValue;
     var text = findElement(element, "title").firstChild.nodeValue;
     return (text)
     return (text)
 }
 }
+function getNodeDataType(element) {
+    var text = findElement(element, "rect", "data-type");
+    return text
+}
 
 
 function adjustText(element) {
 function adjustText(element) {
+    if(findElement(element, "title") == undefined) {
+        return;
+    }
+
     var textElement = findElement(element, "text");
     var textElement = findElement(element, "text");
     var rect = findElement(element, "rect");
     var rect = findElement(element, "rect");
     var width = parseFloat(rect.attributes["width"].value) - 3;
     var width = parseFloat(rect.attributes["width"].value) - 3;
     var title = findElement(element, "title").textContent.replace(/\\([^(]*\\)\$/,"");
     var title = findElement(element, "title").textContent.replace(/\\([^(]*\\)\$/,"");
     textElement.attributes["x"].value = parseFloat(rect.attributes["x"].value) + 3;
     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) {
     if (2*fontSize*fontWidth > width) {
         textElement.textContent = "";
         textElement.textContent = "";
         return;
         return;
@@ -115,17 +177,15 @@ function adjustText(element) {
     textElement.textContent = "";
     textElement.textContent = "";
 }
 }
 
 
-
+// Zoom processing
 function zoomChild(element, x, ratio) {
 function zoomChild(element, x, ratio) {
     if (element.attributes != undefined) {
     if (element.attributes != undefined) {
         if (element.attributes["x"] != 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.tagName == "text") element.attributes["x"].value = findElement(element.parentNode, "rect", "x") + 3;
         }
         }
         if (element.attributes["width"] != undefined) {
         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;
     if (element.childNodes == undefined) return;
@@ -136,12 +196,10 @@ function zoomChild(element, x, ratio) {
 function zoomParent(element) {
 function zoomParent(element) {
     if (element.attributes) {
     if (element.attributes) {
         if (element.attributes["x"] != undefined) {
         if (element.attributes["x"] != undefined) {
-            backupAttribute(element, "x");
-            element.attributes["x"].value = xpad;
+            backupAttribute(element, "x", "zoom", xpad);
         }
         }
         if (element.attributes["width"] != undefined) {
         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;
     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;
     var rect = findElement(element, "rect").attributes;
 
 
     if(rect["data-type"].value != type) {
     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 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 (parseFloat(rect["y"].value) > ymin) {
-        if (currentX <= xmin && (currentX+currentWidtn+comparisionOffset) >= xmax) {
+        if (currentX <= xmin && (currentX+currentWidth+comparisonOffset) >= xmax) {
             element.style["opacity"] = "0.5";
             element.style["opacity"] = "0.5";
             zoomParent(element);
             zoomParent(element);
-            element.onclick = function(element){resetZoom(); zoom(this);};
+            if(overrideOnClick && element.onclick) {
+                element.onclick = function(element){resetZoom(); zoom(this);};
+            }
             adjustText(element);
             adjustText(element);
         }
         }
         else {
         else {
@@ -173,17 +233,19 @@ function zoomElement(element, type, xmin, xmax, ymin, ratio) {
         }
         }
     }
     }
     else {
     else {
-        if (currentX < xmin || currentX + comparisionOffset >= xmax) {
+        if (currentX < xmin || currentX + comparisonOffset >= xmax) {
             element.style["display"] = "none";
             element.style["display"] = "none";
         }
         }
         else {
         else {
             zoomChild(element, xmin, ratio);
             zoomChild(element, xmin, ratio);
-            element.onclick = function(element){zoom(this);};
+            if(overrideOnClick && element.onclick) {
+                element.onclick = function(element){zoom(this);};
+            }
             adjustText(element);
             adjustText(element);
         }
         }
     }
     }
-
 }
 }
+
 function zoom(node) {
 function zoom(node) {
     var attr = findElement(node, "rect").attributes;
     var attr = findElement(node, "rect").attributes;
     var type = attr["data-type"].value
     var type = attr["data-type"].value
@@ -194,16 +256,26 @@ function zoom(node) {
     var ratio = (svg.width.baseVal.value - 2*xpad) / width;
     var ratio = (svg.width.baseVal.value - 2*xpad) / width;
     var resetZoomBtn = document.getElementById("resetZoom");
     var resetZoomBtn = document.getElementById("resetZoom");
     resetZoomBtn.style["opacity"] = "1.0";
     resetZoomBtn.style["opacity"] = "1.0";
-    var el = document.getElementsByTagName("g");
+    var el = document.getElementsByClassName("graphElement");
     for(var i=0;i<el.length;i++){
     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) {
 function resetElementZoom(element) {
     if (element.attributes != undefined) {
     if (element.attributes != undefined) {
-        restoreAttribute(element, "x");
-        restoreAttribute(element, "width");
+        restoreAttribute(element, "x", "zoom");
+        restoreAttribute(element, "width", "zoom");
     }
     }
 
 
     if (element.childNodes == undefined) return;
     if (element.childNodes == undefined) return;
@@ -224,13 +296,209 @@ function resetZoom() {
         adjustText(element[i]);
         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
 // search
 function dropSearch() {
 function dropSearch() {
     var el = document.getElementsByTagName("rect");
     var el = document.getElementsByTagName("rect");
     for (var i=0; i < el.length; i++) {
     for (var i=0; i < el.length; i++) {
-        restoreAttribute(el[i], "fill")
+        restoreAttribute(el[i], "fill", "search")
     }
     }
 }
 }
+
 function startSearch() {
 function startSearch() {
     if (!searching) {
     if (!searching) {
         var pattern = prompt("Enter a string to search (regexp allowed)", "");
         var pattern = prompt("Enter a string to search (regexp allowed)", "");
@@ -261,8 +529,7 @@ function search(pattern) {
             continue;
             continue;
 
 
         if (titleFunc.match(regex)) {
         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;
             searching = 1;
         }
         }
     }
     }
@@ -272,9 +539,11 @@ function search(pattern) {
         searchButton.firstChild.nodeValue = "Reset Search"
         searchButton.firstChild.nodeValue = "Reset Search"
     }
     }
 }
 }
+
 function onSearchHover() {
 function onSearchHover() {
     searchButton.style["opacity"] = "1.0";
     searchButton.style["opacity"] = "1.0";
 }
 }
+
 function onSearchOut() {
 function onSearchOut() {
     if (searching) {
     if (searching) {
         searchButton.style["opacity"] = "1.0";
         searchButton.style["opacity"] = "1.0";

+ 1 - 0
ydb/public/lib/stat_visualization/ya.make

@@ -2,6 +2,7 @@ LIBRARY()
 
 
 SRCS(
 SRCS(
     flame_graph_builder.cpp
     flame_graph_builder.cpp
+    flame_graph_entry.cpp
 )
 )
 
 
 PEERDIR(
 PEERDIR(

+ 9 - 6
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]")
     config.Opts->AddLongOption("stats", "Collect statistics mode [none, basic, full]")
         .RequiredArgument("[String]").StoreResult(&CollectStatsMode);
         .RequiredArgument("[String]").StoreResult(&CollectStatsMode);
     config.Opts->AddLongOption("flame-graph", "Path for statistics flame graph image, works only with full stats")
     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('s', "script", "Text of script to execute").RequiredArgument("[String]").StoreResult(&Script);
     config.Opts->AddLongOption('f', "file", "Script file").RequiredArgument("PATH").StoreResult(&ScriptFile);
     config.Opts->AddLongOption('f', "file", "Script file").RequiredArgument("PATH").StoreResult(&ScriptFile);
 
 
@@ -77,6 +77,10 @@ void TCommandYql::Parse(TConfig& config) {
     if (ScriptFile) {
     if (ScriptFile) {
         Script = ReadFromFile(ScriptFile, "script");
         Script = ReadFromFile(ScriptFile, "script");
     }
     }
+    if(FlameGraphPath && FlameGraphPath->Empty())
+    {
+        throw TMisuseException() << "FlameGraph path can not be empty.";
+    }
     ParseParameters(config);
     ParseParameters(config);
 }
 }
 
 
@@ -91,7 +95,7 @@ int TCommandYql::RunCommand(TConfig& config, const TString& script) {
     NScripting::TExecuteYqlRequestSettings settings;
     NScripting::TExecuteYqlRequestSettings settings;
     settings.CollectQueryStats(ParseQueryStatsMode(CollectStatsMode, NTable::ECollectQueryStatsMode::None));
     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)) {
                            && settings.CollectQueryStats_ != NTable::ECollectQueryStatsMode::Profile)) {
         throw TMisuseException() << "Flame graph is available for full or profile stats. Current: "
         throw TMisuseException() << "Flame graph is available for full or profile stats. Current: "
                                     + (CollectStatsMode.Empty() ? "none" : CollectStatsMode) + '.';
                                     + (CollectStatsMode.Empty() ? "none" : CollectStatsMode) + '.';
@@ -180,11 +184,11 @@ bool TCommandYql::PrintResponse(NScripting::TYqlResultPartIterator& result) {
         TQueryPlanPrinter queryPlanPrinter(OutputFormat, /* analyzeMode */ true);
         TQueryPlanPrinter queryPlanPrinter(OutputFormat, /* analyzeMode */ true);
         queryPlanPrinter.Print(*fullStats);
         queryPlanPrinter.Print(*fullStats);
 
 
-        if (FlameGraphFile) {
+        if (FlameGraphPath) {
             try {
             try {
-                NKikimr::NVisual::GenerateFlameGraphSvg(FlameGraphFile, *fullStats,
+                NKikimr::NVisual::GenerateFlameGraphSvg(*FlameGraphPath, *fullStats,
                                                         NKikimr::NVisual::EFlameGraphType::CPU);
                                                         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) {
             catch (const yexception& ex) {
                 Cout << "Can't save resource usage flame graph, error: " << ex.what() << Endl;
                 Cout << "Can't save resource usage flame graph, error: " << ex.what() << Endl;
@@ -201,4 +205,3 @@ bool TCommandYql::PrintResponse(NScripting::TYqlResultPartIterator& result) {
 
 
 }
 }
 }
 }
-

Some files were not shown because too many files changed in this diff