| 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 | |