diff options
author | vladkoronnov <vladkoronnov@yandex-team.com> | 2023-09-05 20:36:23 +0300 |
---|---|---|
committer | vladkoronnov <vladkoronnov@yandex-team.com> | 2023-09-05 21:25:18 +0300 |
commit | 914b0463af553af307edd9cd9951142a83b6423c (patch) | |
tree | 87762ea617774977a4812b900295f859a2e8bf28 | |
parent | 74da3b1a704f1ce547e0120614a2655b3c2bd249 (diff) | |
download | ydb-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
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"><<</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"><</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">></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">>></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; }; |