| 1 | /**************************************************************************/ |
| 2 | /* editor_profiler.cpp */ |
| 3 | /**************************************************************************/ |
| 4 | /* This file is part of: */ |
| 5 | /* GODOT ENGINE */ |
| 6 | /* https://godotengine.org */ |
| 7 | /**************************************************************************/ |
| 8 | /* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ |
| 9 | /* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ |
| 10 | /* */ |
| 11 | /* Permission is hereby granted, free of charge, to any person obtaining */ |
| 12 | /* a copy of this software and associated documentation files (the */ |
| 13 | /* "Software"), to deal in the Software without restriction, including */ |
| 14 | /* without limitation the rights to use, copy, modify, merge, publish, */ |
| 15 | /* distribute, sublicense, and/or sell copies of the Software, and to */ |
| 16 | /* permit persons to whom the Software is furnished to do so, subject to */ |
| 17 | /* the following conditions: */ |
| 18 | /* */ |
| 19 | /* The above copyright notice and this permission notice shall be */ |
| 20 | /* included in all copies or substantial portions of the Software. */ |
| 21 | /* */ |
| 22 | /* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ |
| 23 | /* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ |
| 24 | /* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ |
| 25 | /* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ |
| 26 | /* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ |
| 27 | /* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ |
| 28 | /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ |
| 29 | /**************************************************************************/ |
| 30 | |
| 31 | #include "editor_profiler.h" |
| 32 | |
| 33 | #include "core/os/os.h" |
| 34 | #include "editor/editor_scale.h" |
| 35 | #include "editor/editor_settings.h" |
| 36 | #include "editor/editor_string_names.h" |
| 37 | #include "scene/resources/image_texture.h" |
| 38 | |
| 39 | void EditorProfiler::_make_metric_ptrs(Metric &m) { |
| 40 | for (int i = 0; i < m.categories.size(); i++) { |
| 41 | m.category_ptrs[m.categories[i].signature] = &m.categories.write[i]; |
| 42 | for (int j = 0; j < m.categories[i].items.size(); j++) { |
| 43 | m.item_ptrs[m.categories[i].items[j].signature] = &m.categories.write[i].items.write[j]; |
| 44 | } |
| 45 | } |
| 46 | } |
| 47 | |
| 48 | EditorProfiler::Metric EditorProfiler::_get_frame_metric(int index) { |
| 49 | return frame_metrics[(frame_metrics.size() + last_metric - (total_metrics - 1) + index) % frame_metrics.size()]; |
| 50 | } |
| 51 | |
| 52 | void EditorProfiler::add_frame_metric(const Metric &p_metric, bool p_final) { |
| 53 | ++last_metric; |
| 54 | if (last_metric >= frame_metrics.size()) { |
| 55 | last_metric = 0; |
| 56 | } |
| 57 | |
| 58 | total_metrics++; |
| 59 | if (total_metrics > frame_metrics.size()) { |
| 60 | total_metrics = frame_metrics.size(); |
| 61 | } |
| 62 | |
| 63 | frame_metrics.write[last_metric] = p_metric; |
| 64 | _make_metric_ptrs(frame_metrics.write[last_metric]); |
| 65 | |
| 66 | updating_frame = true; |
| 67 | clear_button->set_disabled(false); |
| 68 | cursor_metric_edit->set_editable(true); |
| 69 | cursor_metric_edit->set_max(p_metric.frame_number); |
| 70 | cursor_metric_edit->set_min(_get_frame_metric(0).frame_number); |
| 71 | |
| 72 | if (!seeking) { |
| 73 | cursor_metric_edit->set_value(p_metric.frame_number); |
| 74 | } |
| 75 | |
| 76 | updating_frame = false; |
| 77 | |
| 78 | if (frame_delay->is_stopped()) { |
| 79 | frame_delay->set_wait_time(p_final ? 0.1 : 1); |
| 80 | frame_delay->start(); |
| 81 | } |
| 82 | |
| 83 | if (plot_delay->is_stopped()) { |
| 84 | plot_delay->set_wait_time(0.1); |
| 85 | plot_delay->start(); |
| 86 | } |
| 87 | } |
| 88 | |
| 89 | void EditorProfiler::clear() { |
| 90 | int metric_size = EDITOR_GET("debugger/profiler_frame_history_size" ); |
| 91 | metric_size = CLAMP(metric_size, 60, 10000); |
| 92 | frame_metrics.clear(); |
| 93 | frame_metrics.resize(metric_size); |
| 94 | total_metrics = 0; |
| 95 | last_metric = -1; |
| 96 | variables->clear(); |
| 97 | plot_sigs.clear(); |
| 98 | plot_sigs.insert("physics_frame_time" ); |
| 99 | plot_sigs.insert("category_frame_time" ); |
| 100 | |
| 101 | updating_frame = true; |
| 102 | cursor_metric_edit->set_min(0); |
| 103 | cursor_metric_edit->set_max(100); // Doesn't make much sense, but we can't have min == max. Doesn't hurt. |
| 104 | cursor_metric_edit->set_value(0); |
| 105 | cursor_metric_edit->set_editable(false); |
| 106 | updating_frame = false; |
| 107 | hover_metric = -1; |
| 108 | seeking = false; |
| 109 | |
| 110 | // Ensure button text (start, stop) is correct |
| 111 | _update_button_text(); |
| 112 | emit_signal(SNAME("enable_profiling" ), activate->is_pressed()); |
| 113 | } |
| 114 | |
| 115 | static String _get_percent_txt(float p_value, float p_total) { |
| 116 | if (p_total == 0) { |
| 117 | p_total = 0.00001; |
| 118 | } |
| 119 | |
| 120 | return TS->format_number(String::num((p_value / p_total) * 100, 1)) + TS->percent_sign(); |
| 121 | } |
| 122 | |
| 123 | String EditorProfiler::_get_time_as_text(const Metric &m, float p_time, int p_calls) { |
| 124 | const int dmode = display_mode->get_selected(); |
| 125 | |
| 126 | if (dmode == DISPLAY_FRAME_TIME) { |
| 127 | return TS->format_number(rtos(p_time * 1000).pad_decimals(2)) + " " + RTR("ms" ); |
| 128 | } else if (dmode == DISPLAY_AVERAGE_TIME) { |
| 129 | if (p_calls == 0) { |
| 130 | return TS->format_number("0.00" ) + " " + RTR("ms" ); |
| 131 | } else { |
| 132 | return TS->format_number(rtos((p_time / p_calls) * 1000).pad_decimals(2)) + " " + RTR("ms" ); |
| 133 | } |
| 134 | } else if (dmode == DISPLAY_FRAME_PERCENT) { |
| 135 | return _get_percent_txt(p_time, m.frame_time); |
| 136 | } else if (dmode == DISPLAY_PHYSICS_FRAME_PERCENT) { |
| 137 | return _get_percent_txt(p_time, m.physics_frame_time); |
| 138 | } |
| 139 | |
| 140 | return "err" ; |
| 141 | } |
| 142 | |
| 143 | Color EditorProfiler::_get_color_from_signature(const StringName &p_signature) const { |
| 144 | Color bc = get_theme_color(SNAME("error_color" ), EditorStringName(Editor)); |
| 145 | double rot = ABS(double(p_signature.hash()) / double(0x7FFFFFFF)); |
| 146 | Color c; |
| 147 | c.set_hsv(rot, bc.get_s(), bc.get_v()); |
| 148 | return c.lerp(get_theme_color(SNAME("base_color" ), EditorStringName(Editor)), 0.07); |
| 149 | } |
| 150 | |
| 151 | void EditorProfiler::_item_edited() { |
| 152 | if (updating_frame) { |
| 153 | return; |
| 154 | } |
| 155 | |
| 156 | TreeItem *item = variables->get_edited(); |
| 157 | if (!item) { |
| 158 | return; |
| 159 | } |
| 160 | StringName signature = item->get_metadata(0); |
| 161 | bool checked = item->is_checked(0); |
| 162 | |
| 163 | if (checked) { |
| 164 | plot_sigs.insert(signature); |
| 165 | } else { |
| 166 | plot_sigs.erase(signature); |
| 167 | } |
| 168 | |
| 169 | if (!frame_delay->is_processing()) { |
| 170 | frame_delay->set_wait_time(0.1); |
| 171 | frame_delay->start(); |
| 172 | } |
| 173 | |
| 174 | _update_plot(); |
| 175 | } |
| 176 | |
| 177 | void EditorProfiler::_update_plot() { |
| 178 | const int w = graph->get_size().width; |
| 179 | const int h = graph->get_size().height; |
| 180 | bool reset_texture = false; |
| 181 | const int desired_len = w * h * 4; |
| 182 | |
| 183 | if (graph_image.size() != desired_len) { |
| 184 | reset_texture = true; |
| 185 | graph_image.resize(desired_len); |
| 186 | } |
| 187 | |
| 188 | uint8_t *wr = graph_image.ptrw(); |
| 189 | const Color background_color = get_theme_color(SNAME("dark_color_2" ), EditorStringName(Editor)); |
| 190 | |
| 191 | // Clear the previous frame and set the background color. |
| 192 | for (int i = 0; i < desired_len; i += 4) { |
| 193 | wr[i + 0] = Math::fast_ftoi(background_color.r * 255); |
| 194 | wr[i + 1] = Math::fast_ftoi(background_color.g * 255); |
| 195 | wr[i + 2] = Math::fast_ftoi(background_color.b * 255); |
| 196 | wr[i + 3] = 255; |
| 197 | } |
| 198 | |
| 199 | //find highest value |
| 200 | |
| 201 | const bool use_self = display_time->get_selected() == DISPLAY_SELF_TIME; |
| 202 | float highest = 0; |
| 203 | |
| 204 | for (int i = 0; i < total_metrics; i++) { |
| 205 | const Metric &m = _get_frame_metric(i); |
| 206 | |
| 207 | for (const StringName &E : plot_sigs) { |
| 208 | HashMap<StringName, Metric::Category *>::ConstIterator F = m.category_ptrs.find(E); |
| 209 | if (F) { |
| 210 | highest = MAX(F->value->total_time, highest); |
| 211 | } |
| 212 | |
| 213 | HashMap<StringName, Metric::Category::Item *>::ConstIterator G = m.item_ptrs.find(E); |
| 214 | if (G) { |
| 215 | if (use_self) { |
| 216 | highest = MAX(G->value->self, highest); |
| 217 | } else { |
| 218 | highest = MAX(G->value->total, highest); |
| 219 | } |
| 220 | } |
| 221 | } |
| 222 | } |
| 223 | |
| 224 | if (highest > 0) { |
| 225 | //means some data exists.. |
| 226 | highest *= 1.2; //leave some upper room |
| 227 | graph_height = highest; |
| 228 | |
| 229 | Vector<int> columnv; |
| 230 | columnv.resize(h * 4); |
| 231 | |
| 232 | int *column = columnv.ptrw(); |
| 233 | |
| 234 | HashMap<StringName, int> prev_plots; |
| 235 | |
| 236 | for (int i = 0; i < total_metrics * w / frame_metrics.size() - 1; i++) { |
| 237 | for (int j = 0; j < h * 4; j++) { |
| 238 | column[j] = 0; |
| 239 | } |
| 240 | |
| 241 | int current = i * frame_metrics.size() / w; |
| 242 | |
| 243 | for (const StringName &E : plot_sigs) { |
| 244 | const Metric &m = _get_frame_metric(current); |
| 245 | |
| 246 | float value = 0; |
| 247 | |
| 248 | HashMap<StringName, Metric::Category *>::ConstIterator F = m.category_ptrs.find(E); |
| 249 | if (F) { |
| 250 | value = F->value->total_time; |
| 251 | } |
| 252 | |
| 253 | HashMap<StringName, Metric::Category::Item *>::ConstIterator G = m.item_ptrs.find(E); |
| 254 | if (G) { |
| 255 | if (use_self) { |
| 256 | value = G->value->self; |
| 257 | } else { |
| 258 | value = G->value->total; |
| 259 | } |
| 260 | } |
| 261 | |
| 262 | int plot_pos = CLAMP(int(value * h / highest), 0, h - 1); |
| 263 | |
| 264 | int prev_plot = plot_pos; |
| 265 | HashMap<StringName, int>::Iterator H = prev_plots.find(E); |
| 266 | if (H) { |
| 267 | prev_plot = H->value; |
| 268 | H->value = plot_pos; |
| 269 | } else { |
| 270 | prev_plots[E] = plot_pos; |
| 271 | } |
| 272 | |
| 273 | plot_pos = h - plot_pos - 1; |
| 274 | prev_plot = h - prev_plot - 1; |
| 275 | |
| 276 | if (prev_plot > plot_pos) { |
| 277 | SWAP(prev_plot, plot_pos); |
| 278 | } |
| 279 | |
| 280 | Color col = _get_color_from_signature(E); |
| 281 | |
| 282 | for (int j = prev_plot; j <= plot_pos; j++) { |
| 283 | column[j * 4 + 0] += Math::fast_ftoi(CLAMP(col.r * 255, 0, 255)); |
| 284 | column[j * 4 + 1] += Math::fast_ftoi(CLAMP(col.g * 255, 0, 255)); |
| 285 | column[j * 4 + 2] += Math::fast_ftoi(CLAMP(col.b * 255, 0, 255)); |
| 286 | column[j * 4 + 3] += 1; |
| 287 | } |
| 288 | } |
| 289 | |
| 290 | for (int j = 0; j < h * 4; j += 4) { |
| 291 | const int a = column[j + 3]; |
| 292 | if (a > 0) { |
| 293 | column[j + 0] /= a; |
| 294 | column[j + 1] /= a; |
| 295 | column[j + 2] /= a; |
| 296 | } |
| 297 | |
| 298 | const uint8_t red = uint8_t(column[j + 0]); |
| 299 | const uint8_t green = uint8_t(column[j + 1]); |
| 300 | const uint8_t blue = uint8_t(column[j + 2]); |
| 301 | const bool is_filled = red >= 1 || green >= 1 || blue >= 1; |
| 302 | const int widx = ((j >> 2) * w + i) * 4; |
| 303 | |
| 304 | // If the pixel isn't filled by any profiler line, apply the background color instead. |
| 305 | wr[widx + 0] = is_filled ? red : Math::fast_ftoi(background_color.r * 255); |
| 306 | wr[widx + 1] = is_filled ? green : Math::fast_ftoi(background_color.g * 255); |
| 307 | wr[widx + 2] = is_filled ? blue : Math::fast_ftoi(background_color.b * 255); |
| 308 | wr[widx + 3] = 255; |
| 309 | } |
| 310 | } |
| 311 | } |
| 312 | |
| 313 | Ref<Image> img = Image::create_from_data(w, h, false, Image::FORMAT_RGBA8, graph_image); |
| 314 | |
| 315 | if (reset_texture) { |
| 316 | if (graph_texture.is_null()) { |
| 317 | graph_texture.instantiate(); |
| 318 | } |
| 319 | graph_texture->set_image(img); |
| 320 | } |
| 321 | |
| 322 | graph_texture->update(img); |
| 323 | |
| 324 | graph->set_texture(graph_texture); |
| 325 | graph->queue_redraw(); |
| 326 | } |
| 327 | |
| 328 | void EditorProfiler::_update_frame() { |
| 329 | int cursor_metric = cursor_metric_edit->get_value() - _get_frame_metric(0).frame_number; |
| 330 | |
| 331 | updating_frame = true; |
| 332 | variables->clear(); |
| 333 | |
| 334 | TreeItem *root = variables->create_item(); |
| 335 | const Metric &m = _get_frame_metric(cursor_metric); |
| 336 | |
| 337 | int dtime = display_time->get_selected(); |
| 338 | |
| 339 | for (int i = 0; i < m.categories.size(); i++) { |
| 340 | TreeItem *category = variables->create_item(root); |
| 341 | category->set_cell_mode(0, TreeItem::CELL_MODE_CHECK); |
| 342 | category->set_editable(0, true); |
| 343 | category->set_metadata(0, m.categories[i].signature); |
| 344 | category->set_text(0, String(m.categories[i].name)); |
| 345 | category->set_text(1, _get_time_as_text(m, m.categories[i].total_time, 1)); |
| 346 | |
| 347 | if (plot_sigs.has(m.categories[i].signature)) { |
| 348 | category->set_checked(0, true); |
| 349 | category->set_custom_color(0, _get_color_from_signature(m.categories[i].signature)); |
| 350 | } |
| 351 | |
| 352 | for (int j = m.categories[i].items.size() - 1; j >= 0; j--) { |
| 353 | const Metric::Category::Item &it = m.categories[i].items[j]; |
| 354 | |
| 355 | TreeItem *item = variables->create_item(category); |
| 356 | item->set_cell_mode(0, TreeItem::CELL_MODE_CHECK); |
| 357 | item->set_editable(0, true); |
| 358 | item->set_text(0, it.name); |
| 359 | item->set_metadata(0, it.signature); |
| 360 | item->set_metadata(1, it.script); |
| 361 | item->set_metadata(2, it.line); |
| 362 | item->set_text_alignment(2, HORIZONTAL_ALIGNMENT_RIGHT); |
| 363 | item->set_tooltip_text(0, it.name + "\n" + it.script + ":" + itos(it.line)); |
| 364 | |
| 365 | float time = dtime == DISPLAY_SELF_TIME ? it.self : it.total; |
| 366 | |
| 367 | item->set_text(1, _get_time_as_text(m, time, it.calls)); |
| 368 | |
| 369 | item->set_text(2, itos(it.calls)); |
| 370 | |
| 371 | if (plot_sigs.has(it.signature)) { |
| 372 | item->set_checked(0, true); |
| 373 | item->set_custom_color(0, _get_color_from_signature(it.signature)); |
| 374 | } |
| 375 | } |
| 376 | } |
| 377 | |
| 378 | updating_frame = false; |
| 379 | } |
| 380 | |
| 381 | void EditorProfiler::_update_button_text() { |
| 382 | if (activate->is_pressed()) { |
| 383 | activate->set_icon(get_editor_theme_icon(SNAME("Stop" ))); |
| 384 | activate->set_text(TTR("Stop" )); |
| 385 | } else { |
| 386 | activate->set_icon(get_editor_theme_icon(SNAME("Play" ))); |
| 387 | activate->set_text(TTR("Start" )); |
| 388 | } |
| 389 | } |
| 390 | |
| 391 | void EditorProfiler::_activate_pressed() { |
| 392 | _update_button_text(); |
| 393 | |
| 394 | if (activate->is_pressed()) { |
| 395 | _clear_pressed(); |
| 396 | } |
| 397 | |
| 398 | emit_signal(SNAME("enable_profiling" ), activate->is_pressed()); |
| 399 | } |
| 400 | |
| 401 | void EditorProfiler::_clear_pressed() { |
| 402 | clear_button->set_disabled(true); |
| 403 | clear(); |
| 404 | _update_plot(); |
| 405 | } |
| 406 | |
| 407 | void EditorProfiler::_notification(int p_what) { |
| 408 | switch (p_what) { |
| 409 | case NOTIFICATION_ENTER_TREE: |
| 410 | case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: |
| 411 | case NOTIFICATION_THEME_CHANGED: |
| 412 | case NOTIFICATION_TRANSLATION_CHANGED: { |
| 413 | activate->set_icon(get_editor_theme_icon(SNAME("Play" ))); |
| 414 | clear_button->set_icon(get_editor_theme_icon(SNAME("Clear" ))); |
| 415 | } break; |
| 416 | } |
| 417 | } |
| 418 | |
| 419 | void EditorProfiler::_graph_tex_draw() { |
| 420 | if (total_metrics == 0) { |
| 421 | return; |
| 422 | } |
| 423 | if (seeking) { |
| 424 | int frame = cursor_metric_edit->get_value() - _get_frame_metric(0).frame_number; |
| 425 | int cur_x = (2 * frame + 1) * graph->get_size().x / (2 * frame_metrics.size()) + 1; |
| 426 | graph->draw_line(Vector2(cur_x, 0), Vector2(cur_x, graph->get_size().y), Color(1, 1, 1, 0.8)); |
| 427 | } |
| 428 | if (hover_metric > -1 && hover_metric < total_metrics) { |
| 429 | int cur_x = (2 * hover_metric + 1) * graph->get_size().x / (2 * frame_metrics.size()) + 1; |
| 430 | graph->draw_line(Vector2(cur_x, 0), Vector2(cur_x, graph->get_size().y), Color(1, 1, 1, 0.4)); |
| 431 | } |
| 432 | } |
| 433 | |
| 434 | void EditorProfiler::_graph_tex_mouse_exit() { |
| 435 | hover_metric = -1; |
| 436 | graph->queue_redraw(); |
| 437 | } |
| 438 | |
| 439 | void EditorProfiler::_cursor_metric_changed(double) { |
| 440 | if (updating_frame) { |
| 441 | return; |
| 442 | } |
| 443 | |
| 444 | graph->queue_redraw(); |
| 445 | _update_frame(); |
| 446 | } |
| 447 | |
| 448 | void EditorProfiler::_graph_tex_input(const Ref<InputEvent> &p_ev) { |
| 449 | if (last_metric < 0) { |
| 450 | return; |
| 451 | } |
| 452 | |
| 453 | Ref<InputEventMouse> me = p_ev; |
| 454 | Ref<InputEventMouseButton> mb = p_ev; |
| 455 | Ref<InputEventMouseMotion> mm = p_ev; |
| 456 | |
| 457 | if ( |
| 458 | (mb.is_valid() && mb->get_button_index() == MouseButton::LEFT && mb->is_pressed()) || |
| 459 | (mm.is_valid())) { |
| 460 | int x = me->get_position().x - 1; |
| 461 | x = x * frame_metrics.size() / graph->get_size().width; |
| 462 | |
| 463 | hover_metric = x; |
| 464 | |
| 465 | if (x < 0) { |
| 466 | x = 0; |
| 467 | } |
| 468 | |
| 469 | if (x >= frame_metrics.size()) { |
| 470 | x = frame_metrics.size() - 1; |
| 471 | } |
| 472 | |
| 473 | if (mb.is_valid() || (mm->get_button_mask().has_flag(MouseButtonMask::LEFT))) { |
| 474 | updating_frame = true; |
| 475 | |
| 476 | if (x < total_metrics) { |
| 477 | cursor_metric_edit->set_value(_get_frame_metric(x).frame_number); |
| 478 | } |
| 479 | updating_frame = false; |
| 480 | |
| 481 | if (activate->is_pressed()) { |
| 482 | if (!seeking) { |
| 483 | emit_signal(SNAME("break_request" )); |
| 484 | } |
| 485 | } |
| 486 | |
| 487 | seeking = true; |
| 488 | |
| 489 | if (!frame_delay->is_processing()) { |
| 490 | frame_delay->set_wait_time(0.1); |
| 491 | frame_delay->start(); |
| 492 | } |
| 493 | } |
| 494 | |
| 495 | graph->queue_redraw(); |
| 496 | } |
| 497 | } |
| 498 | |
| 499 | void EditorProfiler::disable_seeking() { |
| 500 | seeking = false; |
| 501 | graph->queue_redraw(); |
| 502 | } |
| 503 | |
| 504 | void EditorProfiler::_combo_changed(int) { |
| 505 | _update_frame(); |
| 506 | _update_plot(); |
| 507 | } |
| 508 | |
| 509 | void EditorProfiler::_bind_methods() { |
| 510 | ADD_SIGNAL(MethodInfo("enable_profiling" , PropertyInfo(Variant::BOOL, "enable" ))); |
| 511 | ADD_SIGNAL(MethodInfo("break_request" )); |
| 512 | } |
| 513 | |
| 514 | void EditorProfiler::set_enabled(bool p_enable, bool p_clear) { |
| 515 | activate->set_disabled(!p_enable); |
| 516 | if (p_clear) { |
| 517 | clear(); |
| 518 | } |
| 519 | } |
| 520 | |
| 521 | void EditorProfiler::set_pressed(bool p_pressed) { |
| 522 | activate->set_pressed(p_pressed); |
| 523 | _update_button_text(); |
| 524 | } |
| 525 | |
| 526 | bool EditorProfiler::is_profiling() { |
| 527 | return activate->is_pressed(); |
| 528 | } |
| 529 | |
| 530 | Vector<Vector<String>> EditorProfiler::get_data_as_csv() const { |
| 531 | Vector<Vector<String>> res; |
| 532 | |
| 533 | if (frame_metrics.is_empty()) { |
| 534 | return res; |
| 535 | } |
| 536 | |
| 537 | // Different metrics may contain different number of categories. |
| 538 | HashSet<StringName> possible_signatures; |
| 539 | for (int i = 0; i < frame_metrics.size(); i++) { |
| 540 | const Metric &m = frame_metrics[i]; |
| 541 | if (!m.valid) { |
| 542 | continue; |
| 543 | } |
| 544 | for (const KeyValue<StringName, Metric::Category *> &E : m.category_ptrs) { |
| 545 | possible_signatures.insert(E.key); |
| 546 | } |
| 547 | for (const KeyValue<StringName, Metric::Category::Item *> &E : m.item_ptrs) { |
| 548 | possible_signatures.insert(E.key); |
| 549 | } |
| 550 | } |
| 551 | |
| 552 | // Generate CSV header and cache indices. |
| 553 | HashMap<StringName, int> sig_map; |
| 554 | Vector<String> signatures; |
| 555 | signatures.resize(possible_signatures.size()); |
| 556 | int sig_index = 0; |
| 557 | for (const StringName &E : possible_signatures) { |
| 558 | signatures.write[sig_index] = E; |
| 559 | sig_map[E] = sig_index; |
| 560 | sig_index++; |
| 561 | } |
| 562 | res.push_back(signatures); |
| 563 | |
| 564 | // values |
| 565 | Vector<String> values; |
| 566 | |
| 567 | int index = last_metric; |
| 568 | |
| 569 | for (int i = 0; i < frame_metrics.size(); i++) { |
| 570 | ++index; |
| 571 | |
| 572 | if (index >= frame_metrics.size()) { |
| 573 | index = 0; |
| 574 | } |
| 575 | |
| 576 | const Metric &m = frame_metrics[index]; |
| 577 | |
| 578 | if (!m.valid) { |
| 579 | continue; |
| 580 | } |
| 581 | |
| 582 | // Don't keep old values since there may be empty cells. |
| 583 | values.clear(); |
| 584 | values.resize(possible_signatures.size()); |
| 585 | |
| 586 | for (const KeyValue<StringName, Metric::Category *> &E : m.category_ptrs) { |
| 587 | values.write[sig_map[E.key]] = String::num_real(E.value->total_time); |
| 588 | } |
| 589 | for (const KeyValue<StringName, Metric::Category::Item *> &E : m.item_ptrs) { |
| 590 | values.write[sig_map[E.key]] = String::num_real(E.value->total); |
| 591 | } |
| 592 | |
| 593 | res.push_back(values); |
| 594 | } |
| 595 | |
| 596 | return res; |
| 597 | } |
| 598 | |
| 599 | EditorProfiler::EditorProfiler() { |
| 600 | HBoxContainer *hb = memnew(HBoxContainer); |
| 601 | add_child(hb); |
| 602 | activate = memnew(Button); |
| 603 | activate->set_toggle_mode(true); |
| 604 | activate->set_disabled(true); |
| 605 | activate->set_text(TTR("Start" )); |
| 606 | activate->connect("pressed" , callable_mp(this, &EditorProfiler::_activate_pressed)); |
| 607 | hb->add_child(activate); |
| 608 | |
| 609 | clear_button = memnew(Button); |
| 610 | clear_button->set_text(TTR("Clear" )); |
| 611 | clear_button->connect("pressed" , callable_mp(this, &EditorProfiler::_clear_pressed)); |
| 612 | clear_button->set_disabled(true); |
| 613 | hb->add_child(clear_button); |
| 614 | |
| 615 | hb->add_child(memnew(Label(TTR("Measure:" )))); |
| 616 | |
| 617 | display_mode = memnew(OptionButton); |
| 618 | display_mode->add_item(TTR("Frame Time (ms)" )); |
| 619 | display_mode->add_item(TTR("Average Time (ms)" )); |
| 620 | display_mode->add_item(TTR("Frame %" )); |
| 621 | display_mode->add_item(TTR("Physics Frame %" )); |
| 622 | display_mode->connect("item_selected" , callable_mp(this, &EditorProfiler::_combo_changed)); |
| 623 | |
| 624 | hb->add_child(display_mode); |
| 625 | |
| 626 | hb->add_child(memnew(Label(TTR("Time:" )))); |
| 627 | |
| 628 | display_time = memnew(OptionButton); |
| 629 | // TRANSLATORS: This is an option in the profiler to display the time spent in a function, including the time spent in other functions called by that function. |
| 630 | display_time->add_item(TTR("Inclusive" )); |
| 631 | // TRANSLATORS: This is an option in the profiler to display the time spent in a function, exincluding the time spent in other functions called by that function. |
| 632 | display_time->add_item(TTR("Self" )); |
| 633 | display_time->set_tooltip_text(TTR("Inclusive: Includes time from other functions called by this function.\nUse this to spot bottlenecks.\n\nSelf: Only count the time spent in the function itself, not in other functions called by that function.\nUse this to find individual functions to optimize." )); |
| 634 | display_time->connect("item_selected" , callable_mp(this, &EditorProfiler::_combo_changed)); |
| 635 | |
| 636 | hb->add_child(display_time); |
| 637 | |
| 638 | hb->add_spacer(); |
| 639 | |
| 640 | hb->add_child(memnew(Label(TTR("Frame #:" )))); |
| 641 | |
| 642 | cursor_metric_edit = memnew(SpinBox); |
| 643 | cursor_metric_edit->set_h_size_flags(SIZE_FILL); |
| 644 | cursor_metric_edit->set_value(0); |
| 645 | cursor_metric_edit->set_editable(false); |
| 646 | hb->add_child(cursor_metric_edit); |
| 647 | cursor_metric_edit->connect("value_changed" , callable_mp(this, &EditorProfiler::_cursor_metric_changed)); |
| 648 | |
| 649 | hb->add_theme_constant_override("separation" , 8 * EDSCALE); |
| 650 | |
| 651 | h_split = memnew(HSplitContainer); |
| 652 | add_child(h_split); |
| 653 | h_split->set_v_size_flags(SIZE_EXPAND_FILL); |
| 654 | |
| 655 | variables = memnew(Tree); |
| 656 | variables->set_custom_minimum_size(Size2(320, 0) * EDSCALE); |
| 657 | variables->set_hide_folding(true); |
| 658 | h_split->add_child(variables); |
| 659 | variables->set_hide_root(true); |
| 660 | variables->set_columns(3); |
| 661 | variables->set_column_titles_visible(true); |
| 662 | variables->set_column_title(0, TTR("Name" )); |
| 663 | variables->set_column_expand(0, true); |
| 664 | variables->set_column_clip_content(0, true); |
| 665 | variables->set_column_custom_minimum_width(0, 60); |
| 666 | variables->set_column_title(1, TTR("Time" )); |
| 667 | variables->set_column_expand(1, false); |
| 668 | variables->set_column_clip_content(1, true); |
| 669 | variables->set_column_custom_minimum_width(1, 75 * EDSCALE); |
| 670 | variables->set_column_title(2, TTR("Calls" )); |
| 671 | variables->set_column_expand(2, false); |
| 672 | variables->set_column_clip_content(2, true); |
| 673 | variables->set_column_custom_minimum_width(2, 50 * EDSCALE); |
| 674 | variables->connect("item_edited" , callable_mp(this, &EditorProfiler::_item_edited)); |
| 675 | |
| 676 | graph = memnew(TextureRect); |
| 677 | graph->set_expand_mode(TextureRect::EXPAND_IGNORE_SIZE); |
| 678 | graph->set_mouse_filter(MOUSE_FILTER_STOP); |
| 679 | graph->connect("draw" , callable_mp(this, &EditorProfiler::_graph_tex_draw)); |
| 680 | graph->connect("gui_input" , callable_mp(this, &EditorProfiler::_graph_tex_input)); |
| 681 | graph->connect("mouse_exited" , callable_mp(this, &EditorProfiler::_graph_tex_mouse_exit)); |
| 682 | |
| 683 | h_split->add_child(graph); |
| 684 | graph->set_h_size_flags(SIZE_EXPAND_FILL); |
| 685 | |
| 686 | int metric_size = CLAMP(int(EDITOR_GET("debugger/profiler_frame_history_size" )), 60, 10000); |
| 687 | frame_metrics.resize(metric_size); |
| 688 | |
| 689 | frame_delay = memnew(Timer); |
| 690 | frame_delay->set_wait_time(0.1); |
| 691 | frame_delay->set_one_shot(true); |
| 692 | add_child(frame_delay); |
| 693 | frame_delay->connect("timeout" , callable_mp(this, &EditorProfiler::_update_frame)); |
| 694 | |
| 695 | plot_delay = memnew(Timer); |
| 696 | plot_delay->set_wait_time(0.1); |
| 697 | plot_delay->set_one_shot(true); |
| 698 | add_child(plot_delay); |
| 699 | plot_delay->connect("timeout" , callable_mp(this, &EditorProfiler::_update_plot)); |
| 700 | |
| 701 | plot_sigs.insert("physics_frame_time" ); |
| 702 | plot_sigs.insert("category_frame_time" ); |
| 703 | } |
| 704 | |