1/**************************************************************************/
2/* editor_network_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_network_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
38void EditorNetworkProfiler::_bind_methods() {
39 ADD_SIGNAL(MethodInfo("enable_profiling", PropertyInfo(Variant::BOOL, "enable")));
40 ADD_SIGNAL(MethodInfo("open_request", PropertyInfo(Variant::STRING, "path")));
41}
42
43void EditorNetworkProfiler::_notification(int p_what) {
44 switch (p_what) {
45 case NOTIFICATION_THEME_CHANGED: {
46 if (activate->is_pressed()) {
47 activate->set_icon(theme_cache.stop_icon);
48 } else {
49 activate->set_icon(theme_cache.play_icon);
50 }
51 clear_button->set_icon(theme_cache.clear_icon);
52
53 incoming_bandwidth_text->set_right_icon(theme_cache.incoming_bandwidth_icon);
54 outgoing_bandwidth_text->set_right_icon(theme_cache.outgoing_bandwidth_icon);
55
56 // This needs to be done here to set the faded color when the profiler is first opened
57 incoming_bandwidth_text->add_theme_color_override("font_uneditable_color", theme_cache.incoming_bandwidth_color * Color(1, 1, 1, 0.5));
58 outgoing_bandwidth_text->add_theme_color_override("font_uneditable_color", theme_cache.outgoing_bandwidth_color * Color(1, 1, 1, 0.5));
59 } break;
60 }
61}
62
63void EditorNetworkProfiler::_update_theme_item_cache() {
64 VBoxContainer::_update_theme_item_cache();
65
66 theme_cache.node_icon = get_theme_icon(SNAME("Node"), EditorStringName(EditorIcons));
67 theme_cache.stop_icon = get_theme_icon(SNAME("Stop"), EditorStringName(EditorIcons));
68 theme_cache.play_icon = get_theme_icon(SNAME("Play"), EditorStringName(EditorIcons));
69 theme_cache.clear_icon = get_theme_icon(SNAME("Clear"), EditorStringName(EditorIcons));
70
71 theme_cache.multiplayer_synchronizer_icon = get_theme_icon("MultiplayerSynchronizer", EditorStringName(EditorIcons));
72 theme_cache.instance_options_icon = get_theme_icon(SNAME("InstanceOptions"), EditorStringName(EditorIcons));
73
74 theme_cache.incoming_bandwidth_icon = get_theme_icon(SNAME("ArrowDown"), EditorStringName(EditorIcons));
75 theme_cache.outgoing_bandwidth_icon = get_theme_icon(SNAME("ArrowUp"), EditorStringName(EditorIcons));
76
77 theme_cache.incoming_bandwidth_color = get_theme_color(SNAME("font_color"), EditorStringName(Editor));
78 theme_cache.outgoing_bandwidth_color = get_theme_color(SNAME("font_color"), EditorStringName(Editor));
79}
80
81void EditorNetworkProfiler::_refresh() {
82 if (!dirty) {
83 return;
84 }
85 dirty = false;
86 refresh_rpc_data();
87 refresh_replication_data();
88}
89
90void EditorNetworkProfiler::refresh_rpc_data() {
91 counters_display->clear();
92
93 TreeItem *root = counters_display->create_item();
94 int cols = counters_display->get_columns();
95
96 for (const KeyValue<ObjectID, RPCNodeInfo> &E : rpc_data) {
97 TreeItem *node = counters_display->create_item(root);
98
99 for (int j = 0; j < cols; ++j) {
100 node->set_text_alignment(j, j > 0 ? HORIZONTAL_ALIGNMENT_RIGHT : HORIZONTAL_ALIGNMENT_LEFT);
101 }
102
103 node->set_text(0, E.value.node_path);
104 node->set_text(1, E.value.incoming_rpc == 0 ? "-" : vformat(TTR("%d (%s)"), E.value.incoming_rpc, String::humanize_size(E.value.incoming_size)));
105 node->set_text(2, E.value.outgoing_rpc == 0 ? "-" : vformat(TTR("%d (%s)"), E.value.outgoing_rpc, String::humanize_size(E.value.outgoing_size)));
106 }
107}
108
109void EditorNetworkProfiler::refresh_replication_data() {
110 replication_display->clear();
111
112 TreeItem *root = replication_display->create_item();
113
114 for (const KeyValue<ObjectID, SyncInfo> &E : sync_data) {
115 // Ensure the nodes have at least a temporary cache.
116 ObjectID ids[3] = { E.value.synchronizer, E.value.config, E.value.root_node };
117 for (uint32_t i = 0; i < 3; i++) {
118 const ObjectID &id = ids[i];
119 if (!node_data.has(id)) {
120 missing_node_data.insert(id);
121 node_data[id] = NodeInfo(id);
122 }
123 }
124
125 TreeItem *node = replication_display->create_item(root);
126
127 const NodeInfo &root_info = node_data[E.value.root_node];
128 const NodeInfo &sync_info = node_data[E.value.synchronizer];
129 const NodeInfo &cfg_info = node_data[E.value.config];
130
131 node->set_text(0, root_info.path.get_file());
132 node->set_icon(0, has_theme_icon(root_info.type, EditorStringName(EditorIcons)) ? get_theme_icon(root_info.type, EditorStringName(EditorIcons)) : theme_cache.node_icon);
133 node->set_tooltip_text(0, root_info.path);
134
135 node->set_text(1, sync_info.path.get_file());
136 node->set_icon(1, theme_cache.multiplayer_synchronizer_icon);
137 node->set_tooltip_text(1, sync_info.path);
138
139 int cfg_idx = cfg_info.path.find("::");
140 if (cfg_info.path.begins_with("res://") && ResourceLoader::exists(cfg_info.path) && cfg_idx > 0) {
141 String res_idstr = cfg_info.path.substr(cfg_idx + 2).replace("SceneReplicationConfig_", "");
142 String scene_path = cfg_info.path.substr(0, cfg_idx);
143 node->set_text(2, vformat("%s (%s)", res_idstr, scene_path.get_file()));
144 node->add_button(2, theme_cache.instance_options_icon);
145 node->set_tooltip_text(2, cfg_info.path);
146 node->set_metadata(2, scene_path);
147 } else {
148 node->set_text(2, cfg_info.path);
149 node->set_metadata(2, "");
150 }
151
152 node->set_text(3, vformat("%d - %d", E.value.incoming_syncs, E.value.outgoing_syncs));
153 node->set_text(4, vformat("%d - %d", E.value.incoming_size, E.value.outgoing_size));
154 }
155}
156
157Array EditorNetworkProfiler::pop_missing_node_data() {
158 Array out;
159 for (const ObjectID &id : missing_node_data) {
160 out.push_back(id);
161 }
162 missing_node_data.clear();
163 return out;
164}
165
166void EditorNetworkProfiler::add_node_data(const NodeInfo &p_info) {
167 ERR_FAIL_COND(!node_data.has(p_info.id));
168 node_data[p_info.id] = p_info;
169 dirty = true;
170}
171
172void EditorNetworkProfiler::_activate_pressed() {
173 if (activate->is_pressed()) {
174 refresh_timer->start();
175 activate->set_icon(theme_cache.stop_icon);
176 activate->set_text(TTR("Stop"));
177 } else {
178 refresh_timer->stop();
179 activate->set_icon(theme_cache.play_icon);
180 activate->set_text(TTR("Start"));
181 }
182 emit_signal(SNAME("enable_profiling"), activate->is_pressed());
183}
184
185void EditorNetworkProfiler::_clear_pressed() {
186 rpc_data.clear();
187 sync_data.clear();
188 node_data.clear();
189 missing_node_data.clear();
190 set_bandwidth(0, 0);
191 refresh_rpc_data();
192 refresh_replication_data();
193}
194
195void EditorNetworkProfiler::_replication_button_clicked(TreeItem *p_item, int p_column, int p_idx, MouseButton p_button) {
196 if (!p_item) {
197 return;
198 }
199 String meta = p_item->get_metadata(p_column);
200 if (meta.size() && ResourceLoader::exists(meta)) {
201 emit_signal("open_request", meta);
202 }
203}
204
205void EditorNetworkProfiler::add_rpc_frame_data(const RPCNodeInfo &p_frame) {
206 dirty = true;
207 if (!rpc_data.has(p_frame.node)) {
208 rpc_data.insert(p_frame.node, p_frame);
209 } else {
210 rpc_data[p_frame.node].incoming_rpc += p_frame.incoming_rpc;
211 rpc_data[p_frame.node].outgoing_rpc += p_frame.outgoing_rpc;
212 }
213 if (p_frame.incoming_rpc) {
214 rpc_data[p_frame.node].incoming_size = p_frame.incoming_size / p_frame.incoming_rpc;
215 }
216 if (p_frame.outgoing_rpc) {
217 rpc_data[p_frame.node].outgoing_size = p_frame.outgoing_size / p_frame.outgoing_rpc;
218 }
219}
220
221void EditorNetworkProfiler::add_sync_frame_data(const SyncInfo &p_frame) {
222 dirty = true;
223 if (!sync_data.has(p_frame.synchronizer)) {
224 sync_data[p_frame.synchronizer] = p_frame;
225 } else {
226 sync_data[p_frame.synchronizer].incoming_syncs += p_frame.incoming_syncs;
227 sync_data[p_frame.synchronizer].outgoing_syncs += p_frame.outgoing_syncs;
228 }
229 SyncInfo &info = sync_data[p_frame.synchronizer];
230 if (info.incoming_syncs) {
231 info.incoming_size = p_frame.incoming_size / p_frame.incoming_syncs;
232 }
233 if (info.outgoing_syncs) {
234 info.outgoing_size = p_frame.outgoing_size / p_frame.outgoing_syncs;
235 }
236}
237
238void EditorNetworkProfiler::set_bandwidth(int p_incoming, int p_outgoing) {
239 incoming_bandwidth_text->set_text(vformat(TTR("%s/s"), String::humanize_size(p_incoming)));
240 outgoing_bandwidth_text->set_text(vformat(TTR("%s/s"), String::humanize_size(p_outgoing)));
241
242 // Make labels more prominent when the bandwidth is greater than 0 to attract user attention
243 incoming_bandwidth_text->add_theme_color_override(
244 "font_uneditable_color",
245 theme_cache.incoming_bandwidth_color * Color(1, 1, 1, p_incoming > 0 ? 1 : 0.5));
246 outgoing_bandwidth_text->add_theme_color_override(
247 "font_uneditable_color",
248 theme_cache.outgoing_bandwidth_color * Color(1, 1, 1, p_outgoing > 0 ? 1 : 0.5));
249}
250
251bool EditorNetworkProfiler::is_profiling() {
252 return activate->is_pressed();
253}
254
255EditorNetworkProfiler::EditorNetworkProfiler() {
256 HBoxContainer *hb = memnew(HBoxContainer);
257 hb->add_theme_constant_override("separation", 8 * EDSCALE);
258 add_child(hb);
259
260 activate = memnew(Button);
261 activate->set_toggle_mode(true);
262 activate->set_text(TTR("Start"));
263 activate->connect("pressed", callable_mp(this, &EditorNetworkProfiler::_activate_pressed));
264 hb->add_child(activate);
265
266 clear_button = memnew(Button);
267 clear_button->set_text(TTR("Clear"));
268 clear_button->connect("pressed", callable_mp(this, &EditorNetworkProfiler::_clear_pressed));
269 hb->add_child(clear_button);
270
271 hb->add_spacer();
272
273 Label *lb = memnew(Label);
274 // TRANSLATORS: This is the label for the network profiler's incoming bandwidth.
275 lb->set_text(TTR("Down", "Network"));
276 hb->add_child(lb);
277
278 incoming_bandwidth_text = memnew(LineEdit);
279 incoming_bandwidth_text->set_editable(false);
280 incoming_bandwidth_text->set_custom_minimum_size(Size2(120, 0) * EDSCALE);
281 incoming_bandwidth_text->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_RIGHT);
282 hb->add_child(incoming_bandwidth_text);
283
284 Control *down_up_spacer = memnew(Control);
285 down_up_spacer->set_custom_minimum_size(Size2(30, 0) * EDSCALE);
286 hb->add_child(down_up_spacer);
287
288 lb = memnew(Label);
289 // TRANSLATORS: This is the label for the network profiler's outgoing bandwidth.
290 lb->set_text(TTR("Up", "Network"));
291 hb->add_child(lb);
292
293 outgoing_bandwidth_text = memnew(LineEdit);
294 outgoing_bandwidth_text->set_editable(false);
295 outgoing_bandwidth_text->set_custom_minimum_size(Size2(120, 0) * EDSCALE);
296 outgoing_bandwidth_text->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_RIGHT);
297 hb->add_child(outgoing_bandwidth_text);
298
299 // Set initial texts in the incoming/outgoing bandwidth labels
300 set_bandwidth(0, 0);
301
302 HSplitContainer *sc = memnew(HSplitContainer);
303 add_child(sc);
304 sc->set_v_size_flags(SIZE_EXPAND_FILL);
305 sc->set_h_size_flags(SIZE_EXPAND_FILL);
306 sc->set_split_offset(100 * EDSCALE);
307
308 // RPC
309 counters_display = memnew(Tree);
310 counters_display->set_custom_minimum_size(Size2(320, 0) * EDSCALE);
311 counters_display->set_v_size_flags(SIZE_EXPAND_FILL);
312 counters_display->set_h_size_flags(SIZE_EXPAND_FILL);
313 counters_display->set_hide_folding(true);
314 counters_display->set_hide_root(true);
315 counters_display->set_columns(3);
316 counters_display->set_column_titles_visible(true);
317 counters_display->set_column_title(0, TTR("Node"));
318 counters_display->set_column_expand(0, true);
319 counters_display->set_column_clip_content(0, true);
320 counters_display->set_column_custom_minimum_width(0, 60 * EDSCALE);
321 counters_display->set_column_title(1, TTR("Incoming RPC"));
322 counters_display->set_column_expand(1, false);
323 counters_display->set_column_clip_content(1, true);
324 counters_display->set_column_custom_minimum_width(1, 120 * EDSCALE);
325 counters_display->set_column_title(2, TTR("Outgoing RPC"));
326 counters_display->set_column_expand(2, false);
327 counters_display->set_column_clip_content(2, true);
328 counters_display->set_column_custom_minimum_width(2, 120 * EDSCALE);
329 sc->add_child(counters_display);
330
331 // Replication
332 replication_display = memnew(Tree);
333 replication_display->set_custom_minimum_size(Size2(320, 0) * EDSCALE);
334 replication_display->set_v_size_flags(SIZE_EXPAND_FILL);
335 replication_display->set_h_size_flags(SIZE_EXPAND_FILL);
336 replication_display->set_hide_folding(true);
337 replication_display->set_hide_root(true);
338 replication_display->set_columns(5);
339 replication_display->set_column_titles_visible(true);
340 replication_display->set_column_title(0, TTR("Root"));
341 replication_display->set_column_expand(0, true);
342 replication_display->set_column_clip_content(0, true);
343 replication_display->set_column_custom_minimum_width(0, 80 * EDSCALE);
344 replication_display->set_column_title(1, TTR("Synchronizer"));
345 replication_display->set_column_expand(1, true);
346 replication_display->set_column_clip_content(1, true);
347 replication_display->set_column_custom_minimum_width(1, 80 * EDSCALE);
348 replication_display->set_column_title(2, TTR("Config"));
349 replication_display->set_column_expand(2, true);
350 replication_display->set_column_clip_content(2, true);
351 replication_display->set_column_custom_minimum_width(2, 80 * EDSCALE);
352 replication_display->set_column_title(3, TTR("Count"));
353 replication_display->set_column_expand(3, false);
354 replication_display->set_column_clip_content(3, true);
355 replication_display->set_column_custom_minimum_width(3, 80 * EDSCALE);
356 replication_display->set_column_title(4, TTR("Size"));
357 replication_display->set_column_expand(4, false);
358 replication_display->set_column_clip_content(4, true);
359 replication_display->set_column_custom_minimum_width(4, 80 * EDSCALE);
360 replication_display->connect("button_clicked", callable_mp(this, &EditorNetworkProfiler::_replication_button_clicked));
361 sc->add_child(replication_display);
362
363 refresh_timer = memnew(Timer);
364 refresh_timer->set_wait_time(0.5);
365 refresh_timer->connect("timeout", callable_mp(this, &EditorNetworkProfiler::_refresh));
366 add_child(refresh_timer);
367}
368