1 | /**************************************************************************/ |
2 | /* local_debugger.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 "local_debugger.h" |
32 | |
33 | #include "core/debugger/script_debugger.h" |
34 | #include "core/os/os.h" |
35 | |
36 | struct LocalDebugger::ScriptsProfiler { |
37 | struct ProfileInfoSort { |
38 | bool operator()(const ScriptLanguage::ProfilingInfo &A, const ScriptLanguage::ProfilingInfo &B) const { |
39 | return A.total_time > B.total_time; |
40 | } |
41 | }; |
42 | |
43 | double frame_time = 0; |
44 | uint64_t idle_accum = 0; |
45 | Vector<ScriptLanguage::ProfilingInfo> pinfo; |
46 | |
47 | void toggle(bool p_enable, const Array &p_opts) { |
48 | if (p_enable) { |
49 | for (int i = 0; i < ScriptServer::get_language_count(); i++) { |
50 | ScriptServer::get_language(i)->profiling_start(); |
51 | } |
52 | |
53 | print_line("BEGIN PROFILING" ); |
54 | pinfo.resize(32768); |
55 | } else { |
56 | _print_frame_data(true); |
57 | for (int i = 0; i < ScriptServer::get_language_count(); i++) { |
58 | ScriptServer::get_language(i)->profiling_stop(); |
59 | } |
60 | } |
61 | } |
62 | |
63 | void tick(double p_frame_time, double p_process_time, double p_physics_time, double p_physics_frame_time) { |
64 | frame_time = p_frame_time; |
65 | _print_frame_data(false); |
66 | } |
67 | |
68 | void _print_frame_data(bool p_accumulated) { |
69 | uint64_t diff = OS::get_singleton()->get_ticks_usec() - idle_accum; |
70 | |
71 | if (!p_accumulated && diff < 1000000) { //show every one second |
72 | return; |
73 | } |
74 | |
75 | idle_accum = OS::get_singleton()->get_ticks_usec(); |
76 | |
77 | int ofs = 0; |
78 | for (int i = 0; i < ScriptServer::get_language_count(); i++) { |
79 | if (p_accumulated) { |
80 | ofs += ScriptServer::get_language(i)->profiling_get_accumulated_data(&pinfo.write[ofs], pinfo.size() - ofs); |
81 | } else { |
82 | ofs += ScriptServer::get_language(i)->profiling_get_frame_data(&pinfo.write[ofs], pinfo.size() - ofs); |
83 | } |
84 | } |
85 | |
86 | SortArray<ScriptLanguage::ProfilingInfo, ProfileInfoSort> sort; |
87 | sort.sort(pinfo.ptrw(), ofs); |
88 | |
89 | // compute total script frame time |
90 | uint64_t script_time_us = 0; |
91 | for (int i = 0; i < ofs; i++) { |
92 | script_time_us += pinfo[i].self_time; |
93 | } |
94 | double script_time = USEC_TO_SEC(script_time_us); |
95 | double total_time = p_accumulated ? script_time : frame_time; |
96 | |
97 | if (!p_accumulated) { |
98 | print_line("FRAME: total: " + rtos(total_time) + " script: " + rtos(script_time) + "/" + itos(script_time * 100 / total_time) + " %" ); |
99 | } else { |
100 | print_line("ACCUMULATED: total: " + rtos(total_time)); |
101 | } |
102 | |
103 | for (int i = 0; i < ofs; i++) { |
104 | print_line(itos(i) + ":" + pinfo[i].signature); |
105 | double tt = USEC_TO_SEC(pinfo[i].total_time); |
106 | double st = USEC_TO_SEC(pinfo[i].self_time); |
107 | print_line("\ttotal: " + rtos(tt) + "/" + itos(tt * 100 / total_time) + " % \tself: " + rtos(st) + "/" + itos(st * 100 / total_time) + " % tcalls: " + itos(pinfo[i].call_count)); |
108 | } |
109 | } |
110 | |
111 | ScriptsProfiler() { |
112 | idle_accum = OS::get_singleton()->get_ticks_usec(); |
113 | } |
114 | }; |
115 | |
116 | void LocalDebugger::debug(bool p_can_continue, bool p_is_error_breakpoint) { |
117 | ScriptLanguage *script_lang = script_debugger->get_break_language(); |
118 | |
119 | if (!target_function.is_empty()) { |
120 | String current_function = script_lang->debug_get_stack_level_function(0); |
121 | if (current_function != target_function) { |
122 | script_debugger->set_depth(0); |
123 | script_debugger->set_lines_left(1); |
124 | return; |
125 | } |
126 | target_function = "" ; |
127 | } |
128 | |
129 | print_line("\nDebugger Break, Reason: '" + script_lang->debug_get_error() + "'" ); |
130 | print_line("*Frame " + itos(0) + " - " + script_lang->debug_get_stack_level_source(0) + ":" + itos(script_lang->debug_get_stack_level_line(0)) + " in function '" + script_lang->debug_get_stack_level_function(0) + "'" ); |
131 | print_line("Enter \"help\" for assistance." ); |
132 | int current_frame = 0; |
133 | int total_frames = script_lang->debug_get_stack_level_count(); |
134 | while (true) { |
135 | OS::get_singleton()->print("debug> " ); |
136 | String line = OS::get_singleton()->get_stdin_string().strip_edges(); |
137 | |
138 | // Cache options |
139 | String variable_prefix = options["variable_prefix" ]; |
140 | |
141 | if (line.is_empty() && !feof(stdin)) { |
142 | print_line("\nDebugger Break, Reason: '" + script_lang->debug_get_error() + "'" ); |
143 | print_line("*Frame " + itos(current_frame) + " - " + script_lang->debug_get_stack_level_source(current_frame) + ":" + itos(script_lang->debug_get_stack_level_line(current_frame)) + " in function '" + script_lang->debug_get_stack_level_function(current_frame) + "'" ); |
144 | print_line("Enter \"help\" for assistance." ); |
145 | } else if (line == "c" || line == "continue" ) { |
146 | break; |
147 | } else if (line == "bt" || line == "breakpoint" ) { |
148 | for (int i = 0; i < total_frames; i++) { |
149 | String cfi = (current_frame == i) ? "*" : " " ; //current frame indicator |
150 | print_line(cfi + "Frame " + itos(i) + " - " + script_lang->debug_get_stack_level_source(i) + ":" + itos(script_lang->debug_get_stack_level_line(i)) + " in function '" + script_lang->debug_get_stack_level_function(i) + "'" ); |
151 | } |
152 | |
153 | } else if (line.begins_with("fr" ) || line.begins_with("frame" )) { |
154 | if (line.get_slice_count(" " ) == 1) { |
155 | print_line("*Frame " + itos(current_frame) + " - " + script_lang->debug_get_stack_level_source(current_frame) + ":" + itos(script_lang->debug_get_stack_level_line(current_frame)) + " in function '" + script_lang->debug_get_stack_level_function(current_frame) + "'" ); |
156 | } else { |
157 | int frame = line.get_slicec(' ', 1).to_int(); |
158 | if (frame < 0 || frame >= total_frames) { |
159 | print_line("Error: Invalid frame." ); |
160 | } else { |
161 | current_frame = frame; |
162 | print_line("*Frame " + itos(frame) + " - " + script_lang->debug_get_stack_level_source(frame) + ":" + itos(script_lang->debug_get_stack_level_line(frame)) + " in function '" + script_lang->debug_get_stack_level_function(frame) + "'" ); |
163 | } |
164 | } |
165 | |
166 | } else if (line.begins_with("set" )) { |
167 | if (line.get_slice_count(" " ) == 1) { |
168 | for (const KeyValue<String, String> &E : options) { |
169 | print_line("\t" + E.key + "=" + E.value); |
170 | } |
171 | |
172 | } else { |
173 | String key_value = line.get_slicec(' ', 1); |
174 | int value_pos = key_value.find("=" ); |
175 | |
176 | if (value_pos < 0) { |
177 | print_line("Error: Invalid set format. Use: set key=value" ); |
178 | } else { |
179 | String key = key_value.left(value_pos); |
180 | |
181 | if (!options.has(key)) { |
182 | print_line("Error: Unknown option " + key); |
183 | } else { |
184 | // Allow explicit tab character |
185 | String value = key_value.substr(value_pos + 1).replace("\\t" , "\t" ); |
186 | |
187 | options[key] = value; |
188 | } |
189 | } |
190 | } |
191 | |
192 | } else if (line == "lv" || line == "locals" ) { |
193 | List<String> locals; |
194 | List<Variant> values; |
195 | script_lang->debug_get_stack_level_locals(current_frame, &locals, &values); |
196 | print_variables(locals, values, variable_prefix); |
197 | |
198 | } else if (line == "gv" || line == "globals" ) { |
199 | List<String> globals; |
200 | List<Variant> values; |
201 | script_lang->debug_get_globals(&globals, &values); |
202 | print_variables(globals, values, variable_prefix); |
203 | |
204 | } else if (line == "mv" || line == "members" ) { |
205 | List<String> members; |
206 | List<Variant> values; |
207 | script_lang->debug_get_stack_level_members(current_frame, &members, &values); |
208 | print_variables(members, values, variable_prefix); |
209 | |
210 | } else if (line.begins_with("p" ) || line.begins_with("print" )) { |
211 | if (line.get_slice_count(" " ) <= 1) { |
212 | print_line("Usage: print <expre>" ); |
213 | } else { |
214 | String expr = line.get_slicec(' ', 2); |
215 | String res = script_lang->debug_parse_stack_level_expression(current_frame, expr); |
216 | print_line(res); |
217 | } |
218 | |
219 | } else if (line == "s" || line == "step" ) { |
220 | script_debugger->set_depth(-1); |
221 | script_debugger->set_lines_left(1); |
222 | break; |
223 | } else if (line == "n" || line == "next" ) { |
224 | script_debugger->set_depth(0); |
225 | script_debugger->set_lines_left(1); |
226 | break; |
227 | } else if (line == "fin" || line == "finish" ) { |
228 | String current_function = script_lang->debug_get_stack_level_function(0); |
229 | |
230 | for (int i = 0; i < total_frames; i++) { |
231 | target_function = script_lang->debug_get_stack_level_function(i); |
232 | if (target_function != current_function) { |
233 | script_debugger->set_depth(0); |
234 | script_debugger->set_lines_left(1); |
235 | return; |
236 | } |
237 | } |
238 | |
239 | print_line("Error: Reached last frame." ); |
240 | target_function = "" ; |
241 | |
242 | } else if (line.begins_with("br" ) || line.begins_with("break" )) { |
243 | if (line.get_slice_count(" " ) <= 1) { |
244 | const HashMap<int, HashSet<StringName>> &breakpoints = script_debugger->get_breakpoints(); |
245 | if (breakpoints.size() == 0) { |
246 | print_line("No Breakpoints." ); |
247 | continue; |
248 | } |
249 | |
250 | print_line("Breakpoint(s): " + itos(breakpoints.size())); |
251 | for (const KeyValue<int, HashSet<StringName>> &E : breakpoints) { |
252 | print_line("\t" + String(*E.value.begin()) + ":" + itos(E.key)); |
253 | } |
254 | |
255 | } else { |
256 | Pair<String, int> breakpoint = to_breakpoint(line); |
257 | |
258 | String source = breakpoint.first; |
259 | int linenr = breakpoint.second; |
260 | |
261 | if (source.is_empty()) { |
262 | continue; |
263 | } |
264 | |
265 | script_debugger->insert_breakpoint(linenr, source); |
266 | |
267 | print_line("Added breakpoint at " + source + ":" + itos(linenr)); |
268 | } |
269 | |
270 | } else if (line == "q" || line == "quit" || |
271 | (line.is_empty() && feof(stdin))) { |
272 | // Do not stop again on quit |
273 | script_debugger->clear_breakpoints(); |
274 | script_debugger->set_depth(-1); |
275 | script_debugger->set_lines_left(-1); |
276 | |
277 | MainLoop *main_loop = OS::get_singleton()->get_main_loop(); |
278 | if (main_loop->get_class() == "SceneTree" ) { |
279 | main_loop->call("quit" ); |
280 | } |
281 | break; |
282 | } else if (line.begins_with("delete" )) { |
283 | if (line.get_slice_count(" " ) <= 1) { |
284 | script_debugger->clear_breakpoints(); |
285 | } else { |
286 | Pair<String, int> breakpoint = to_breakpoint(line); |
287 | |
288 | String source = breakpoint.first; |
289 | int linenr = breakpoint.second; |
290 | |
291 | if (source.is_empty()) { |
292 | continue; |
293 | } |
294 | |
295 | script_debugger->remove_breakpoint(linenr, source); |
296 | |
297 | print_line("Removed breakpoint at " + source + ":" + itos(linenr)); |
298 | } |
299 | |
300 | } else if (line == "h" || line == "help" ) { |
301 | print_line("Built-In Debugger command list:\n" ); |
302 | print_line("\tc,continue\t\t Continue execution." ); |
303 | print_line("\tbt,backtrace\t\t Show stack trace (frames)." ); |
304 | print_line("\tfr,frame <frame>:\t Change current frame." ); |
305 | print_line("\tlv,locals\t\t Show local variables for current frame." ); |
306 | print_line("\tmv,members\t\t Show member variables for \"this\" in frame." ); |
307 | print_line("\tgv,globals\t\t Show global variables." ); |
308 | print_line("\tp,print <expr>\t\t Execute and print variable in expression." ); |
309 | print_line("\ts,step\t\t\t Step to next line." ); |
310 | print_line("\tn,next\t\t\t Next line." ); |
311 | print_line("\tfin,finish\t\t Step out of current frame." ); |
312 | print_line("\tbr,break [source:line]\t List all breakpoints or place a breakpoint." ); |
313 | print_line("\tdelete [source:line]:\t Delete one/all breakpoints." ); |
314 | print_line("\tset [key=value]:\t List all options, or set one." ); |
315 | print_line("\tq,quit\t\t\t Quit application." ); |
316 | } else { |
317 | print_line("Error: Invalid command, enter \"help\" for assistance." ); |
318 | } |
319 | } |
320 | } |
321 | |
322 | void LocalDebugger::print_variables(const List<String> &names, const List<Variant> &values, const String &variable_prefix) { |
323 | String value; |
324 | Vector<String> value_lines; |
325 | const List<Variant>::Element *V = values.front(); |
326 | for (const String &E : names) { |
327 | value = String(V->get()); |
328 | |
329 | if (variable_prefix.is_empty()) { |
330 | print_line(E + ": " + String(V->get())); |
331 | } else { |
332 | print_line(E + ":" ); |
333 | value_lines = value.split("\n" ); |
334 | for (int i = 0; i < value_lines.size(); ++i) { |
335 | print_line(variable_prefix + value_lines[i]); |
336 | } |
337 | } |
338 | |
339 | V = V->next(); |
340 | } |
341 | } |
342 | |
343 | Pair<String, int> LocalDebugger::to_breakpoint(const String &p_line) { |
344 | String breakpoint_part = p_line.get_slicec(' ', 1); |
345 | Pair<String, int> breakpoint; |
346 | |
347 | int last_colon = breakpoint_part.rfind(":" ); |
348 | if (last_colon < 0) { |
349 | print_line("Error: Invalid breakpoint format. Expected [source:line]" ); |
350 | return breakpoint; |
351 | } |
352 | |
353 | breakpoint.first = script_debugger->breakpoint_find_source(breakpoint_part.left(last_colon).strip_edges()); |
354 | breakpoint.second = breakpoint_part.substr(last_colon).strip_edges().to_int(); |
355 | |
356 | return breakpoint; |
357 | } |
358 | |
359 | void LocalDebugger::send_message(const String &p_message, const Array &p_args) { |
360 | // This needs to be cleaned up entirely. |
361 | // print_line("MESSAGE: '" + p_message + "' - " + String(Variant(p_args))); |
362 | } |
363 | |
364 | void LocalDebugger::send_error(const String &p_func, const String &p_file, int p_line, const String &p_err, const String &p_descr, bool p_editor_notify, ErrorHandlerType p_type) { |
365 | print_line("ERROR: '" + (p_descr.is_empty() ? p_err : p_descr) + "'" ); |
366 | } |
367 | |
368 | LocalDebugger::LocalDebugger() { |
369 | options["variable_prefix" ] = "" ; |
370 | |
371 | // Bind scripts profiler. |
372 | scripts_profiler = memnew(ScriptsProfiler); |
373 | Profiler scr_prof( |
374 | scripts_profiler, |
375 | [](void *p_user, bool p_enable, const Array &p_opts) { |
376 | static_cast<ScriptsProfiler *>(p_user)->toggle(p_enable, p_opts); |
377 | }, |
378 | nullptr, |
379 | [](void *p_user, double p_frame_time, double p_process_time, double p_physics_time, double p_physics_frame_time) { |
380 | static_cast<ScriptsProfiler *>(p_user)->tick(p_frame_time, p_process_time, p_physics_time, p_physics_frame_time); |
381 | }); |
382 | register_profiler("scripts" , scr_prof); |
383 | } |
384 | |
385 | LocalDebugger::~LocalDebugger() { |
386 | unregister_profiler("scripts" ); |
387 | if (scripts_profiler) { |
388 | memdelete(scripts_profiler); |
389 | } |
390 | } |
391 | |