| 1 | #include "duckdb/main/query_profiler.hpp" | 
|---|
| 2 |  | 
|---|
| 3 | #include "duckdb/common/fstream.hpp" | 
|---|
| 4 | #include "duckdb/common/printer.hpp" | 
|---|
| 5 | #include "duckdb/common/string_util.hpp" | 
|---|
| 6 | #include "duckdb/execution/physical_operator.hpp" | 
|---|
| 7 | #include "duckdb/parser/sql_statement.hpp" | 
|---|
| 8 |  | 
|---|
| 9 | #include <iostream> | 
|---|
| 10 | #include <utility> | 
|---|
| 11 |  | 
|---|
| 12 | using namespace duckdb; | 
|---|
| 13 | using namespace std; | 
|---|
| 14 |  | 
|---|
| 15 | constexpr idx_t TREE_RENDER_WIDTH = 20; | 
|---|
| 16 | constexpr idx_t REMAINING_RENDER_WIDTH = TREE_RENDER_WIDTH - 2; | 
|---|
| 17 | constexpr idx_t  = 10; | 
|---|
| 18 |  | 
|---|
| 19 | void QueryProfiler::StartQuery(string query, SQLStatement &statement) { | 
|---|
| 20 | if (!enabled) { | 
|---|
| 21 | return; | 
|---|
| 22 | } | 
|---|
| 23 | if (statement.type != StatementType::SELECT_STATEMENT && statement.type != StatementType::EXECUTE_STATEMENT) { | 
|---|
| 24 | return; | 
|---|
| 25 | } | 
|---|
| 26 | this->running = true; | 
|---|
| 27 | this->query = query; | 
|---|
| 28 | tree_map.clear(); | 
|---|
| 29 | execution_stack = stack<PhysicalOperator *>(); | 
|---|
| 30 | root = nullptr; | 
|---|
| 31 | phase_timings.clear(); | 
|---|
| 32 | phase_stack.clear(); | 
|---|
| 33 |  | 
|---|
| 34 | main_query.Start(); | 
|---|
| 35 | op.Start(); | 
|---|
| 36 | } | 
|---|
| 37 |  | 
|---|
| 38 | void QueryProfiler::EndQuery() { | 
|---|
| 39 | if (!enabled || !running) { | 
|---|
| 40 | return; | 
|---|
| 41 | } | 
|---|
| 42 |  | 
|---|
| 43 | main_query.End(); | 
|---|
| 44 | this->running = false; | 
|---|
| 45 | // print the query after termination, if this is enabled | 
|---|
| 46 | if (automatic_print_format != ProfilerPrintFormat::NONE) { | 
|---|
| 47 | string query_info; | 
|---|
| 48 | if (automatic_print_format == ProfilerPrintFormat::JSON) { | 
|---|
| 49 | query_info = ToJSON(); | 
|---|
| 50 | } else if (automatic_print_format == ProfilerPrintFormat::QUERY_TREE) { | 
|---|
| 51 | query_info = ToString(); | 
|---|
| 52 | } | 
|---|
| 53 |  | 
|---|
| 54 | if (save_location.empty()) { | 
|---|
| 55 | cout << query_info << "\n"; | 
|---|
| 56 | } else { | 
|---|
| 57 | WriteToFile(save_location.c_str(), query_info); | 
|---|
| 58 | } | 
|---|
| 59 | } | 
|---|
| 60 | } | 
|---|
| 61 |  | 
|---|
| 62 | void QueryProfiler::StartPhase(string new_phase) { | 
|---|
| 63 | if (!enabled || !running) { | 
|---|
| 64 | return; | 
|---|
| 65 | } | 
|---|
| 66 |  | 
|---|
| 67 | if (!phase_stack.empty()) { | 
|---|
| 68 | // there are active phases | 
|---|
| 69 | phase_profiler.End(); | 
|---|
| 70 | // add the timing to all phases prior to this one | 
|---|
| 71 | string prefix = ""; | 
|---|
| 72 | for (auto &phase : phase_stack) { | 
|---|
| 73 | phase_timings[phase] += phase_profiler.Elapsed(); | 
|---|
| 74 | prefix += phase + " > "; | 
|---|
| 75 | } | 
|---|
| 76 | // when there are previous phases, we prefix the current phase with those phases | 
|---|
| 77 | new_phase = prefix + new_phase; | 
|---|
| 78 | } | 
|---|
| 79 |  | 
|---|
| 80 | // start a new phase | 
|---|
| 81 | phase_stack.push_back(new_phase); | 
|---|
| 82 | // restart the timer | 
|---|
| 83 | phase_profiler.Start(); | 
|---|
| 84 | } | 
|---|
| 85 |  | 
|---|
| 86 | void QueryProfiler::EndPhase() { | 
|---|
| 87 | if (!enabled || !running) { | 
|---|
| 88 | return; | 
|---|
| 89 | } | 
|---|
| 90 | assert(phase_stack.size() > 0); | 
|---|
| 91 |  | 
|---|
| 92 | // end the timer | 
|---|
| 93 | phase_profiler.End(); | 
|---|
| 94 | // add the timing to all currently active phases | 
|---|
| 95 | for (auto &phase : phase_stack) { | 
|---|
| 96 | phase_timings[phase] += phase_profiler.Elapsed(); | 
|---|
| 97 | } | 
|---|
| 98 | // now remove the last added phase | 
|---|
| 99 | phase_stack.pop_back(); | 
|---|
| 100 |  | 
|---|
| 101 | if (phase_stack.size() > 0) { | 
|---|
| 102 | phase_profiler.Start(); | 
|---|
| 103 | } | 
|---|
| 104 | } | 
|---|
| 105 |  | 
|---|
| 106 | void QueryProfiler::StartOperator(PhysicalOperator *phys_op) { | 
|---|
| 107 | if (!enabled || !running) { | 
|---|
| 108 | return; | 
|---|
| 109 | } | 
|---|
| 110 |  | 
|---|
| 111 | if (!root) { | 
|---|
| 112 | // start of execution: create operator tree | 
|---|
| 113 | root = CreateTree(phys_op); | 
|---|
| 114 | } | 
|---|
| 115 | if (!execution_stack.empty()) { | 
|---|
| 116 | // add timing for the previous element | 
|---|
| 117 | op.End(); | 
|---|
| 118 | assert(tree_map.count(execution_stack.top()) > 0); | 
|---|
| 119 | auto &info = tree_map[execution_stack.top()]->info; | 
|---|
| 120 | info.time += op.Elapsed(); | 
|---|
| 121 | } | 
|---|
| 122 | if (tree_map.count(phys_op) == 0) { | 
|---|
| 123 | // element does not exist in the tree! this only happens with a subquery | 
|---|
| 124 | // create a new tree | 
|---|
| 125 | assert(execution_stack.size() > 0); | 
|---|
| 126 | auto node = tree_map[execution_stack.top()]; | 
|---|
| 127 | auto new_tree = CreateTree(phys_op, node->depth + 1); | 
|---|
| 128 | // add it to the current node | 
|---|
| 129 | node->children.push_back(move(new_tree)); | 
|---|
| 130 | } | 
|---|
| 131 | execution_stack.push(phys_op); | 
|---|
| 132 |  | 
|---|
| 133 | // start timing for current element | 
|---|
| 134 | op.Start(); | 
|---|
| 135 | } | 
|---|
| 136 |  | 
|---|
| 137 | void QueryProfiler::EndOperator(DataChunk &chunk) { | 
|---|
| 138 | if (!enabled || !running) { | 
|---|
| 139 | return; | 
|---|
| 140 | } | 
|---|
| 141 |  | 
|---|
| 142 | // finish timing for the current element | 
|---|
| 143 | op.End(); | 
|---|
| 144 | auto &info = tree_map[execution_stack.top()]->info; | 
|---|
| 145 | info.time += op.Elapsed(); | 
|---|
| 146 | info.elements += chunk.size(); | 
|---|
| 147 |  | 
|---|
| 148 | assert(!execution_stack.empty()); | 
|---|
| 149 | execution_stack.pop(); | 
|---|
| 150 |  | 
|---|
| 151 | // start timing again for the previous element, if any | 
|---|
| 152 | if (!execution_stack.empty()) { | 
|---|
| 153 | op.Start(); | 
|---|
| 154 | } | 
|---|
| 155 | } | 
|---|
| 156 |  | 
|---|
| 157 | string QueryProfiler::ToString() const { | 
|---|
| 158 | if (!enabled) { | 
|---|
| 159 | return "Query profiling is disabled. Call " | 
|---|
| 160 | "Connection::EnableProfiling() to enable profiling!"; | 
|---|
| 161 | } | 
|---|
| 162 |  | 
|---|
| 163 | if (query.empty()) { | 
|---|
| 164 | return "<<Empty Profiling Information>>"; | 
|---|
| 165 | } | 
|---|
| 166 | string result = "<<Query Profiling Information>>\n"; | 
|---|
| 167 | result += StringUtil::Replace(query, "\n", " ") + "\n"; | 
|---|
| 168 | result += "<<Timing>>\n"; | 
|---|
| 169 | result += "Total Time: "+ to_string(main_query.Elapsed()) + "s\n"; | 
|---|
| 170 | // print phase timings | 
|---|
| 171 | for (const auto &entry : GetOrderedPhaseTimings()) { | 
|---|
| 172 | result += entry.first + ": "+ to_string(entry.second) + "s\n"; | 
|---|
| 173 | } | 
|---|
| 174 | // render the main operator tree | 
|---|
| 175 | result += "<<Operator Tree>>\n"; | 
|---|
| 176 | if (!root) { | 
|---|
| 177 | result += "<<ERROR RENDERING ROOT>"; | 
|---|
| 178 | } else { | 
|---|
| 179 | result += RenderTree(*root); | 
|---|
| 180 | } | 
|---|
| 181 | return result; | 
|---|
| 182 | } | 
|---|
| 183 |  | 
|---|
| 184 | static string ToJSONRecursive(QueryProfiler::TreeNode &node) { | 
|---|
| 185 | string result = "{ \"name\": \""+ node.name + "\",\n"; | 
|---|
| 186 | result += "\"timing\":"+ StringUtil::Format( "%.2f", node.info.time) + ",\n"; | 
|---|
| 187 | result += "\"cardinality\":"+ to_string(node.info.elements) + ",\n"; | 
|---|
| 188 | result += "\"extra_info\": \""+ StringUtil::Replace(node.extra_info, "\n", "\\n") + "\",\n"; | 
|---|
| 189 | result += "\"children\": ["; | 
|---|
| 190 | result += | 
|---|
| 191 | StringUtil::Join(node.children, node.children.size(), ",\n", | 
|---|
| 192 | [](const unique_ptr<QueryProfiler::TreeNode> &child) { return ToJSONRecursive(*child); }); | 
|---|
| 193 | result += "]\n}\n"; | 
|---|
| 194 | return result; | 
|---|
| 195 | } | 
|---|
| 196 |  | 
|---|
| 197 | string QueryProfiler::ToJSON() const { | 
|---|
| 198 | if (!enabled) { | 
|---|
| 199 | return "{ \"result\": \"disabled\" }\n"; | 
|---|
| 200 | } | 
|---|
| 201 | if (query.empty()) { | 
|---|
| 202 | return "{ \"result\": \"empty\" }\n"; | 
|---|
| 203 | } | 
|---|
| 204 | if (!root) { | 
|---|
| 205 | return "{ \"result\": \"error\" }\n"; | 
|---|
| 206 | } | 
|---|
| 207 | string result = "{ \"result\": "+ to_string(main_query.Elapsed()) + ",\n"; | 
|---|
| 208 | // print the phase timings | 
|---|
| 209 | result += "\"timings\": {\n"; | 
|---|
| 210 | const auto &ordered_phase_timings = GetOrderedPhaseTimings(); | 
|---|
| 211 | result += | 
|---|
| 212 | StringUtil::Join(ordered_phase_timings, ordered_phase_timings.size(), ",\n", [](const PhaseTimingItem &entry) { | 
|---|
| 213 | return "\""+ entry.first + "\": "+ to_string(entry.second); | 
|---|
| 214 | }); | 
|---|
| 215 | result += "},\n"; | 
|---|
| 216 | // recursively print the physical operator tree | 
|---|
| 217 | result += "\"tree\": "; | 
|---|
| 218 | result += ToJSONRecursive(*root); | 
|---|
| 219 | return result + "}"; | 
|---|
| 220 | } | 
|---|
| 221 |  | 
|---|
| 222 | void QueryProfiler::WriteToFile(const char *path, string &info) const { | 
|---|
| 223 | ofstream out(path); | 
|---|
| 224 | out << info; | 
|---|
| 225 | out.close(); | 
|---|
| 226 | } | 
|---|
| 227 |  | 
|---|
| 228 | static bool is_non_split_char(char l) { | 
|---|
| 229 | return (l >= 65 && l <= 90) || (l >= 97 && l <= 122) || l == 95 || l == ']' || l == ')'; | 
|---|
| 230 | } | 
|---|
| 231 |  | 
|---|
| 232 | static bool is_padding(char l) { | 
|---|
| 233 | return l == ' ' || l == '(' || l == ')'; | 
|---|
| 234 | } | 
|---|
| 235 |  | 
|---|
| 236 | static string remove_padding(string l) { | 
|---|
| 237 | idx_t start = 0, end = l.size(); | 
|---|
| 238 | while (start < l.size() && is_padding(l[start])) { | 
|---|
| 239 | start++; | 
|---|
| 240 | } | 
|---|
| 241 | while (end > 0 && is_padding(l[end - 1])) { | 
|---|
| 242 | end--; | 
|---|
| 243 | } | 
|---|
| 244 | return l.substr(start, end - start); | 
|---|
| 245 | } | 
|---|
| 246 |  | 
|---|
| 247 | unique_ptr<QueryProfiler::TreeNode> QueryProfiler::CreateTree(PhysicalOperator *root, idx_t depth) { | 
|---|
| 248 | auto node = make_unique<QueryProfiler::TreeNode>(); | 
|---|
| 249 | node->name = PhysicalOperatorToString(root->type); | 
|---|
| 250 | node->extra_info = root->ExtraRenderInformation(); | 
|---|
| 251 | if (!node->extra_info.empty()) { | 
|---|
| 252 | auto splits = StringUtil::Split(node->extra_info, '\n'); | 
|---|
| 253 | for (auto &split : splits) { | 
|---|
| 254 | string str = remove_padding(split); | 
|---|
| 255 | constexpr idx_t max_segment_size = REMAINING_RENDER_WIDTH - 2; | 
|---|
| 256 | idx_t location = 0; | 
|---|
| 257 | while (location < str.size() && node->split_extra_info.size() < MAX_EXTRA_LINES) { | 
|---|
| 258 | bool has_to_split = (str.size() - location) > max_segment_size; | 
|---|
| 259 | if (has_to_split) { | 
|---|
| 260 | // look for a split character | 
|---|
| 261 | idx_t i; | 
|---|
| 262 | for (i = 8; i < max_segment_size; i++) { | 
|---|
| 263 | if (!is_non_split_char(str[location + i])) { | 
|---|
| 264 | // split here | 
|---|
| 265 | break; | 
|---|
| 266 | } | 
|---|
| 267 | } | 
|---|
| 268 | node->split_extra_info.push_back(str.substr(location, i)); | 
|---|
| 269 | location += i; | 
|---|
| 270 | } else { | 
|---|
| 271 | node->split_extra_info.push_back(str.substr(location)); | 
|---|
| 272 | break; | 
|---|
| 273 | } | 
|---|
| 274 | } | 
|---|
| 275 | } | 
|---|
| 276 | } | 
|---|
| 277 | node->depth = depth; | 
|---|
| 278 | tree_map[root] = node.get(); | 
|---|
| 279 | for (auto &child : root->children) { | 
|---|
| 280 | auto child_node = CreateTree(child.get(), depth + 1); | 
|---|
| 281 | node->children.push_back(move(child_node)); | 
|---|
| 282 | } | 
|---|
| 283 | return node; | 
|---|
| 284 | } | 
|---|
| 285 |  | 
|---|
| 286 | static string DrawPadded(string text, char padding_character = ' ') { | 
|---|
| 287 | auto remaining_width = REMAINING_RENDER_WIDTH; | 
|---|
| 288 | if (text.size() > remaining_width) { | 
|---|
| 289 | text = text.substr(0, remaining_width); | 
|---|
| 290 | } | 
|---|
| 291 | assert(text.size() <= (idx_t)numeric_limits<int32_t>::max()); | 
|---|
| 292 |  | 
|---|
| 293 | auto right_padding = (remaining_width - text.size()) / 2; | 
|---|
| 294 | auto left_padding = remaining_width - text.size() - right_padding; | 
|---|
| 295 | return "|"+ string(left_padding, padding_character) + text + string(right_padding, padding_character) + "|"; | 
|---|
| 296 | } | 
|---|
| 297 |  | 
|---|
| 298 | idx_t QueryProfiler::RenderTreeRecursive(QueryProfiler::TreeNode &node, vector<string> &render, | 
|---|
| 299 | vector<idx_t> &render_heights, idx_t base_render_x, idx_t start_depth, | 
|---|
| 300 | idx_t depth) { | 
|---|
| 301 | auto render_height = render_heights[depth]; | 
|---|
| 302 | auto width = base_render_x; | 
|---|
| 303 | // render this node | 
|---|
| 304 | // first add any padding to render at this location | 
|---|
| 305 | auto start_position = width * TREE_RENDER_WIDTH; | 
|---|
| 306 | for (idx_t i = 0; i < render_height; i++) { | 
|---|
| 307 | if (render[start_depth + i].size() > start_position) { | 
|---|
| 308 | // something has already been rendered here! | 
|---|
| 309 | throw Exception( "Tree rendering error, overlapping nodes!"); | 
|---|
| 310 | } else { | 
|---|
| 311 | // add the padding | 
|---|
| 312 | render[start_depth + i] += string(start_position - render[start_depth + i].size(), ' '); | 
|---|
| 313 | } | 
|---|
| 314 | } | 
|---|
| 315 |  | 
|---|
| 316 | // draw the boundaries of the box | 
|---|
| 317 | render[start_depth] += string(TREE_RENDER_WIDTH, '-'); | 
|---|
| 318 | render[start_depth + render_height - 1] += string(TREE_RENDER_WIDTH, '-'); | 
|---|
| 319 |  | 
|---|
| 320 | // draw the name | 
|---|
| 321 | string name = node.name; | 
|---|
| 322 | render[start_depth + 1] += DrawPadded(name); | 
|---|
| 323 | // draw extra information | 
|---|
| 324 | for (idx_t i = 2; i < render_height - 3; i++) { | 
|---|
| 325 | auto split_index = i - 2; | 
|---|
| 326 | string string = split_index < node.split_extra_info.size() ? node.split_extra_info[split_index] : ""; | 
|---|
| 327 | render[start_depth + i] += DrawPadded(string); | 
|---|
| 328 | } | 
|---|
| 329 | // draw the timing information | 
|---|
| 330 | string timing = StringUtil::Format( "%.2f", node.info.time); | 
|---|
| 331 | render[start_depth + render_height - 3] += DrawPadded( "("+ timing + "s)"); | 
|---|
| 332 | // draw the intermediate count | 
|---|
| 333 | render[start_depth + render_height - 2] += DrawPadded(to_string(node.info.elements)); | 
|---|
| 334 |  | 
|---|
| 335 | for (auto &child : node.children) { | 
|---|
| 336 | // render all the children | 
|---|
| 337 | width = | 
|---|
| 338 | RenderTreeRecursive(*child, render, render_heights, width, start_depth + render_heights[depth], depth + 1); | 
|---|
| 339 | width++; | 
|---|
| 340 | } | 
|---|
| 341 | if (node.children.size() > 0) { | 
|---|
| 342 | width--; | 
|---|
| 343 | } | 
|---|
| 344 | return width; | 
|---|
| 345 | } | 
|---|
| 346 |  | 
|---|
| 347 | idx_t QueryProfiler::GetDepth(QueryProfiler::TreeNode &node) { | 
|---|
| 348 | idx_t depth = 0; | 
|---|
| 349 | for (auto &child : node.children) { | 
|---|
| 350 | depth = max(depth, GetDepth(*child)); | 
|---|
| 351 | } | 
|---|
| 352 | return depth + 1; | 
|---|
| 353 | } | 
|---|
| 354 |  | 
|---|
| 355 | static void GetRenderHeight(QueryProfiler::TreeNode &node, vector<idx_t> &render_heights, int depth = 0) { | 
|---|
| 356 | render_heights[depth] = max(render_heights[depth], (5 + (idx_t)node.split_extra_info.size())); | 
|---|
| 357 | for (auto &child : node.children) { | 
|---|
| 358 | GetRenderHeight(*child, render_heights, depth + 1); | 
|---|
| 359 | } | 
|---|
| 360 | } | 
|---|
| 361 |  | 
|---|
| 362 | string QueryProfiler::RenderTree(QueryProfiler::TreeNode &node) { | 
|---|
| 363 | vector<idx_t> render_heights; | 
|---|
| 364 | // compute the height of each level | 
|---|
| 365 | auto depth = GetDepth(node); | 
|---|
| 366 |  | 
|---|
| 367 | // compute the render height | 
|---|
| 368 | render_heights.resize(depth); | 
|---|
| 369 | GetRenderHeight(node, render_heights); | 
|---|
| 370 | int32_t total_height = 0; | 
|---|
| 371 | for (auto height : render_heights) { | 
|---|
| 372 | total_height += height; | 
|---|
| 373 | } | 
|---|
| 374 |  | 
|---|
| 375 | // now actually render the tree | 
|---|
| 376 | vector<string> render; | 
|---|
| 377 | render.resize(total_height); | 
|---|
| 378 |  | 
|---|
| 379 | RenderTreeRecursive(node, render, render_heights); | 
|---|
| 380 | string text; | 
|---|
| 381 | for (auto &str : render) { | 
|---|
| 382 | text += str + "\n"; | 
|---|
| 383 | } | 
|---|
| 384 | return text; | 
|---|
| 385 | } | 
|---|
| 386 |  | 
|---|
| 387 | void QueryProfiler::Print() { | 
|---|
| 388 | Printer::Print(ToString()); | 
|---|
| 389 | } | 
|---|
| 390 |  | 
|---|
| 391 | vector<QueryProfiler::PhaseTimingItem> QueryProfiler::GetOrderedPhaseTimings() const { | 
|---|
| 392 | vector<PhaseTimingItem> result; | 
|---|
| 393 | // first sort the phases alphabetically | 
|---|
| 394 | vector<string> phases; | 
|---|
| 395 | for (auto &entry : phase_timings) { | 
|---|
| 396 | phases.push_back(entry.first); | 
|---|
| 397 | } | 
|---|
| 398 | std::sort(phases.begin(), phases.end()); | 
|---|
| 399 | for (const auto &phase : phases) { | 
|---|
| 400 | auto entry = phase_timings.find(phase); | 
|---|
| 401 | assert(entry != phase_timings.end()); | 
|---|
| 402 | result.emplace_back(entry->first, entry->second); | 
|---|
| 403 | } | 
|---|
| 404 | return result; | 
|---|
| 405 | } | 
|---|
| 406 |  | 
|---|