diff options
author | Fabio Alessandrelli <fabio.alessandrelli@gmail.com> | 2020-02-27 03:30:20 +0100 |
---|---|---|
committer | Fabio Alessandrelli <fabio.alessandrelli@gmail.com> | 2020-03-08 12:36:39 +0100 |
commit | b8ddaf9c33107e01928e04ed462aa08d4017247b (patch) | |
tree | 6ab71cdcbfd372e8390baa6ac30a2974fe43d119 /core/debugger | |
parent | d0009636df6544dd26ab7c568a0244af6a20634a (diff) |
Refactor ScriptDebugger.
EngineDebugger is the new interface to access the debugger.
It tries to be as agnostic as possible on the data that various
subsystems can expose.
It allows 2 types of interactions:
- Profilers:
A subsystem can register a profiler, assigning it a unique name.
That name can be used to activate the profiler or add data to it.
The registered profiler can be composed of up to 3 functions:
- Toggle: called when the profiler is activated/deactivated.
- Add: called whenever data is added to the debugger
(via `EngineDebugger::profiler_add_frame_data`)
- Tick: called every frame (during idle), receives frame times.
- Captures: (Only relevant in remote debugger for now)
A subsystem can register a capture, assigning it a unique name.
When receiving a message, the remote debugger will check if it starts
with `[prefix]:` and call the associated capture with name `prefix`.
Port MultiplayerAPI, Servers, Scripts, Visual, Performance to the new
profiler system.
Port SceneDebugger and RemoteDebugger to the new capture system.
The LocalDebugger also uses the new profiler system for scripts
profiling.
Diffstat (limited to 'core/debugger')
-rw-r--r-- | core/debugger/SCsub | 5 | ||||
-rw-r--r-- | core/debugger/debugger_marshalls.cpp | 329 | ||||
-rw-r--r-- | core/debugger/debugger_marshalls.h | 175 | ||||
-rw-r--r-- | core/debugger/engine_debugger.cpp | 186 | ||||
-rw-r--r-- | core/debugger/engine_debugger.h | 130 | ||||
-rw-r--r-- | core/debugger/local_debugger.cpp | 416 | ||||
-rw-r--r-- | core/debugger/local_debugger.h | 60 | ||||
-rw-r--r-- | core/debugger/remote_debugger.cpp | 935 | ||||
-rw-r--r-- | core/debugger/remote_debugger.h | 114 | ||||
-rw-r--r-- | core/debugger/remote_debugger_peer.cpp | 242 | ||||
-rw-r--r-- | core/debugger/remote_debugger_peer.h | 94 | ||||
-rw-r--r-- | core/debugger/script_debugger.cpp | 123 | ||||
-rw-r--r-- | core/debugger/script_debugger.h | 80 |
13 files changed, 2889 insertions, 0 deletions
diff --git a/core/debugger/SCsub b/core/debugger/SCsub new file mode 100644 index 0000000000..1c5f954470 --- /dev/null +++ b/core/debugger/SCsub @@ -0,0 +1,5 @@ +#!/usr/bin/env python + +Import('env') + +env.add_source_files(env.core_sources, "*.cpp") diff --git a/core/debugger/debugger_marshalls.cpp b/core/debugger/debugger_marshalls.cpp new file mode 100644 index 0000000000..4bccf0805f --- /dev/null +++ b/core/debugger/debugger_marshalls.cpp @@ -0,0 +1,329 @@ +/*************************************************************************/ +/* debugger_marshalls.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "debugger_marshalls.h" + +#include "core/io/marshalls.h" + +#define CHECK_SIZE(arr, expected, what) ERR_FAIL_COND_V_MSG((uint32_t)arr.size() < (uint32_t)(expected), false, String("Malformed ") + what + " message from script debugger, message too short. Exptected size: " + itos(expected) + ", actual size: " + itos(arr.size())) +#define CHECK_END(arr, expected, what) ERR_FAIL_COND_V_MSG((uint32_t)arr.size() > (uint32_t)expected, false, String("Malformed ") + what + " message from script debugger, message too long. Exptected size: " + itos(expected) + ", actual size: " + itos(arr.size())) + +Array DebuggerMarshalls::ResourceUsage::serialize() { + infos.sort(); + + Array arr; + arr.push_back(infos.size() * 4); + for (List<ResourceInfo>::Element *E = infos.front(); E; E = E->next()) { + arr.push_back(E->get().path); + arr.push_back(E->get().format); + arr.push_back(E->get().type); + arr.push_back(E->get().vram); + } + return arr; +} + +bool DebuggerMarshalls::ResourceUsage::deserialize(const Array &p_arr) { + CHECK_SIZE(p_arr, 1, "ResourceUsage"); + uint32_t size = p_arr[0]; + CHECK_SIZE(p_arr, size, "ResourceUsage"); + int idx = 1; + for (uint32_t i = 0; i < size / 4; i++) { + ResourceInfo info; + info.path = p_arr[idx]; + info.format = p_arr[idx + 1]; + info.type = p_arr[idx + 2]; + info.vram = p_arr[idx + 3]; + infos.push_back(info); + } + CHECK_END(p_arr, idx, "ResourceUsage"); + return true; +} + +Array DebuggerMarshalls::ScriptFunctionSignature::serialize() { + Array arr; + arr.push_back(name); + arr.push_back(id); + return arr; +} + +bool DebuggerMarshalls::ScriptFunctionSignature::deserialize(const Array &p_arr) { + CHECK_SIZE(p_arr, 2, "ScriptFunctionSignature"); + name = p_arr[0]; + id = p_arr[1]; + CHECK_END(p_arr, 2, "ScriptFunctionSignature"); + return true; +} + +Array DebuggerMarshalls::NetworkProfilerFrame::serialize() { + Array arr; + arr.push_back(infos.size() * 6); + for (int i = 0; i < infos.size(); ++i) { + arr.push_back(uint64_t(infos[i].node)); + arr.push_back(infos[i].node_path); + arr.push_back(infos[i].incoming_rpc); + arr.push_back(infos[i].incoming_rset); + arr.push_back(infos[i].outgoing_rpc); + arr.push_back(infos[i].outgoing_rset); + } + return arr; +} + +bool DebuggerMarshalls::NetworkProfilerFrame::deserialize(const Array &p_arr) { + CHECK_SIZE(p_arr, 1, "NetworkProfilerFrame"); + uint32_t size = p_arr[0]; + CHECK_SIZE(p_arr, size, "NetworkProfilerFrame"); + infos.resize(size); + int idx = 1; + for (uint32_t i = 0; i < size / 6; ++i) { + infos.write[i].node = uint64_t(p_arr[idx]); + infos.write[i].node_path = p_arr[idx + 1]; + infos.write[i].incoming_rpc = p_arr[idx + 2]; + infos.write[i].incoming_rset = p_arr[idx + 3]; + infos.write[i].outgoing_rpc = p_arr[idx + 4]; + infos.write[i].outgoing_rset = p_arr[idx + 5]; + } + CHECK_END(p_arr, idx, "NetworkProfilerFrame"); + return true; +} + +Array DebuggerMarshalls::ServersProfilerFrame::serialize() { + Array arr; + arr.push_back(frame_number); + arr.push_back(frame_time); + arr.push_back(idle_time); + arr.push_back(physics_time); + arr.push_back(physics_frame_time); + arr.push_back(script_time); + + arr.push_back(servers.size()); + for (int i = 0; i < servers.size(); i++) { + ServerInfo &s = servers[i]; + arr.push_back(s.name); + arr.push_back(s.functions.size() * 2); + for (int j = 0; j < s.functions.size(); j++) { + ServerFunctionInfo &f = s.functions[j]; + arr.push_back(f.name); + arr.push_back(f.time); + } + } + + arr.push_back(script_functions.size() * 4); + for (int i = 0; i < script_functions.size(); i++) { + arr.push_back(script_functions[i].sig_id); + arr.push_back(script_functions[i].call_count); + arr.push_back(script_functions[i].self_time); + arr.push_back(script_functions[i].total_time); + } + return arr; +} + +bool DebuggerMarshalls::ServersProfilerFrame::deserialize(const Array &p_arr) { + CHECK_SIZE(p_arr, 7, "ServersProfilerFrame"); + frame_number = p_arr[0]; + frame_time = p_arr[1]; + idle_time = p_arr[2]; + physics_time = p_arr[3]; + physics_frame_time = p_arr[4]; + script_time = p_arr[5]; + int servers_size = p_arr[6]; + int idx = 7; + while (servers_size) { + CHECK_SIZE(p_arr, idx + 2, "ServersProfilerFrame"); + servers_size--; + ServerInfo si; + si.name = p_arr[idx]; + int sub_data_size = p_arr[idx + 1]; + idx += 2; + CHECK_SIZE(p_arr, idx + sub_data_size, "ServersProfilerFrame"); + for (int j = 0; j < sub_data_size / 2; j++) { + ServerFunctionInfo sf; + sf.name = p_arr[idx]; + sf.time = p_arr[idx + 1]; + idx += 2; + si.functions.push_back(sf); + } + servers.push_back(si); + } + CHECK_SIZE(p_arr, idx + 3, "ServersProfilerFrame"); + int func_size = p_arr[idx]; + idx += 1; + CHECK_SIZE(p_arr, idx + func_size, "ServersProfilerFrame"); + for (int i = 0; i < func_size / 4; i++) { + ScriptFunctionInfo fi; + fi.sig_id = p_arr[idx]; + fi.call_count = p_arr[idx + 1]; + fi.self_time = p_arr[idx + 2]; + fi.total_time = p_arr[idx + 3]; + script_functions.push_back(fi); + idx += 4; + } + CHECK_END(p_arr, idx, "ServersProfilerFrame"); + return true; +} + +Array DebuggerMarshalls::ScriptStackDump::serialize() { + Array arr; + arr.push_back(frames.size() * 3); + for (int i = 0; i < frames.size(); i++) { + arr.push_back(frames[i].file); + arr.push_back(frames[i].line); + arr.push_back(frames[i].func); + } + return arr; +} + +bool DebuggerMarshalls::ScriptStackDump::deserialize(const Array &p_arr) { + CHECK_SIZE(p_arr, 1, "ScriptStackDump"); + uint32_t size = p_arr[0]; + CHECK_SIZE(p_arr, size, "ScriptStackDump"); + int idx = 1; + for (uint32_t i = 0; i < size / 3; i++) { + ScriptLanguage::StackInfo sf; + sf.file = p_arr[idx]; + sf.line = p_arr[idx + 1]; + sf.func = p_arr[idx + 2]; + frames.push_back(sf); + idx += 3; + } + CHECK_END(p_arr, idx, "ScriptStackDump"); + return true; +} + +Array DebuggerMarshalls::ScriptStackVariable::serialize(int max_size) { + Array arr; + arr.push_back(name); + arr.push_back(type); + + Variant var = value; + if (value.get_type() == Variant::OBJECT && value.get_validated_object() == nullptr) { + var = Variant(); + } + + int len = 0; + Error err = encode_variant(var, NULL, len, true); + if (err != OK) + ERR_PRINT("Failed to encode variant."); + + if (len > max_size) { + arr.push_back(Variant()); + } else { + arr.push_back(var); + } + return arr; +} + +bool DebuggerMarshalls::ScriptStackVariable::deserialize(const Array &p_arr) { + CHECK_SIZE(p_arr, 3, "ScriptStackVariable"); + name = p_arr[0]; + type = p_arr[1]; + value = p_arr[2]; + CHECK_END(p_arr, 3, "ScriptStackVariable"); + return true; +} + +Array DebuggerMarshalls::OutputError::serialize() { + Array arr; + arr.push_back(hr); + arr.push_back(min); + arr.push_back(sec); + arr.push_back(msec); + arr.push_back(source_file); + arr.push_back(source_func); + arr.push_back(source_line); + arr.push_back(error); + arr.push_back(error_descr); + arr.push_back(warning); + unsigned int size = callstack.size(); + const ScriptLanguage::StackInfo *r = callstack.ptr(); + arr.push_back(size * 3); + for (int i = 0; i < callstack.size(); i++) { + arr.push_back(r[i].file); + arr.push_back(r[i].func); + arr.push_back(r[i].line); + } + return arr; +} + +bool DebuggerMarshalls::OutputError::deserialize(const Array &p_arr) { + CHECK_SIZE(p_arr, 11, "OutputError"); + hr = p_arr[0]; + min = p_arr[1]; + sec = p_arr[2]; + msec = p_arr[3]; + source_file = p_arr[4]; + source_func = p_arr[5]; + source_line = p_arr[6]; + error = p_arr[7]; + error_descr = p_arr[8]; + warning = p_arr[9]; + unsigned int stack_size = p_arr[10]; + CHECK_SIZE(p_arr, stack_size, "OutputError"); + int idx = 11; + callstack.resize(stack_size / 3); + ScriptLanguage::StackInfo *w = callstack.ptrw(); + for (unsigned int i = 0; i < stack_size / 3; i++) { + w[i].file = p_arr[idx]; + w[i].func = p_arr[idx + 1]; + w[i].line = p_arr[idx + 2]; + idx += 3; + } + CHECK_END(p_arr, idx, "OutputError"); + return true; +} + +Array DebuggerMarshalls::VisualProfilerFrame::serialize() { + Array arr; + arr.push_back(frame_number); + arr.push_back(areas.size() * 3); + for (int i = 0; i < areas.size(); i++) { + arr.push_back(areas[i].name); + arr.push_back(areas[i].cpu_msec); + arr.push_back(areas[i].gpu_msec); + } + return arr; +} + +bool DebuggerMarshalls::VisualProfilerFrame::deserialize(const Array &p_arr) { + CHECK_SIZE(p_arr, 2, "VisualProfilerFrame"); + frame_number = p_arr[0]; + int size = p_arr[1]; + CHECK_SIZE(p_arr, size, "VisualProfilerFrame"); + int idx = 2; + areas.resize(size / 3); + VS::FrameProfileArea *w = areas.ptrw(); + for (int i = 0; i < size / 3; i++) { + w[i].name = p_arr[idx]; + w[i].cpu_msec = p_arr[idx + 1]; + w[i].gpu_msec = p_arr[idx + 2]; + idx += 3; + } + CHECK_END(p_arr, idx, "VisualProfilerFrame"); + return true; +} diff --git a/core/debugger/debugger_marshalls.h b/core/debugger/debugger_marshalls.h new file mode 100644 index 0000000000..4c15adc555 --- /dev/null +++ b/core/debugger/debugger_marshalls.h @@ -0,0 +1,175 @@ +/*************************************************************************/ +/* debugger_marshalls.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef DEBUGGER_MARSHARLLS_H +#define DEBUGGER_MARSHARLLS_H + +#include "core/script_language.h" +#include "servers/visual_server.h" + +struct DebuggerMarshalls { + + // Memory usage + struct ResourceInfo { + String path; + String format; + String type; + RID id; + int vram; + bool operator<(const ResourceInfo &p_img) const { return vram == p_img.vram ? id < p_img.id : vram > p_img.vram; } + ResourceInfo() { + vram = 0; + } + }; + + struct ResourceUsage { + List<ResourceInfo> infos; + + Array serialize(); + bool deserialize(const Array &p_arr); + }; + + // Network profiler + struct MultiplayerNodeInfo { + ObjectID node; + String node_path; + int incoming_rpc = 0; + int incoming_rset = 0; + int outgoing_rpc = 0; + int outgoing_rset = 0; + }; + + struct NetworkProfilerFrame { + Vector<MultiplayerNodeInfo> infos; + + Array serialize(); + bool deserialize(const Array &p_arr); + }; + + // Script Profiler + class ScriptFunctionSignature { + public: + StringName name; + int id = -1; + + Array serialize(); + bool deserialize(const Array &p_arr); + }; + + struct ScriptFunctionInfo { + StringName name; + int sig_id = -1; + int call_count = 0; + float self_time = 0; + float total_time = 0; + }; + + // Servers profiler + struct ServerFunctionInfo { + StringName name; + float time = 0; + }; + + struct ServerInfo { + StringName name; + List<ServerFunctionInfo> functions; + }; + + struct ServersProfilerFrame { + int frame_number = 0; + float frame_time = 0; + float idle_time = 0; + float physics_time = 0; + float physics_frame_time = 0; + float script_time = 0; + List<ServerInfo> servers; + Vector<ScriptFunctionInfo> script_functions; + + Array serialize(); + bool deserialize(const Array &p_arr); + }; + + struct ScriptStackVariable { + String name; + Variant value; + int type; + ScriptStackVariable() { + type = -1; + } + + Array serialize(int max_size = 1 << 20); // 1 MiB default. + bool deserialize(const Array &p_arr); + }; + + struct ScriptStackDump { + List<ScriptLanguage::StackInfo> frames; + ScriptStackDump() {} + + Array serialize(); + bool deserialize(const Array &p_arr); + }; + + struct OutputError { + int hr; + int min; + int sec; + int msec; + String source_file; + String source_func; + int source_line; + String error; + String error_descr; + bool warning; + Vector<ScriptLanguage::StackInfo> callstack; + + OutputError() { + hr = -1; + min = -1; + sec = -1; + msec = -1; + source_line = -1; + warning = false; + } + + Array serialize(); + bool deserialize(const Array &p_arr); + }; + + // Visual Profiler + struct VisualProfilerFrame { + uint64_t frame_number; + Vector<VS::FrameProfileArea> areas; + + Array serialize(); + bool deserialize(const Array &p_arr); + }; +}; + +#endif // DEBUGGER_MARSHARLLS_H diff --git a/core/debugger/engine_debugger.cpp b/core/debugger/engine_debugger.cpp new file mode 100644 index 0000000000..305802e5f2 --- /dev/null +++ b/core/debugger/engine_debugger.cpp @@ -0,0 +1,186 @@ +/*************************************************************************/ +/* engine_debugger.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "engine_debugger.h" + +#include "core/debugger/local_debugger.h" +#include "core/debugger/remote_debugger.h" +#include "core/debugger/script_debugger.h" +#include "core/os/os.h" + +EngineDebugger *EngineDebugger::singleton = NULL; +ScriptDebugger *EngineDebugger::script_debugger = NULL; + +Map<StringName, EngineDebugger::Profiler> EngineDebugger::profilers; +Map<StringName, EngineDebugger::Capture> EngineDebugger::captures; + +void EngineDebugger::register_profiler(const StringName &p_name, const Profiler &p_func) { + ERR_FAIL_COND_MSG(profilers.has(p_name), "Profiler already registered: " + p_name); + profilers.insert(p_name, p_func); +} + +void EngineDebugger::unregister_profiler(const StringName &p_name) { + ERR_FAIL_COND_MSG(!profilers.has(p_name), "Profiler not registered: " + p_name); + Profiler &p = profilers[p_name]; + if (p.active && p.toggle) { + p.toggle(p.data, false, Array()); + p.active = false; + } + profilers.erase(p_name); +} + +void EngineDebugger::register_message_capture(const StringName &p_name, Capture p_func) { + ERR_FAIL_COND_MSG(captures.has(p_name), "Capture already registered: " + p_name); + captures.insert(p_name, p_func); +} + +void EngineDebugger::unregister_message_capture(const StringName &p_name) { + ERR_FAIL_COND_MSG(!captures.has(p_name), "Capture not registered: " + p_name); + captures.erase(p_name); +} + +void EngineDebugger::profiler_enable(const StringName &p_name, bool p_enabled, const Array &p_opts) { + ERR_FAIL_COND_MSG(!profilers.has(p_name), "Can't change profiler state, no profiler: " + p_name); + Profiler &p = profilers[p_name]; + if (p.toggle) { + p.toggle(p.data, p_enabled, p_opts); + } + p.active = p_enabled; +} + +void EngineDebugger::profiler_add_frame_data(const StringName &p_name, const Array &p_data) { + ERR_FAIL_COND_MSG(!profilers.has(p_name), "Can't add frame data, no profiler: " + p_name); + Profiler &p = profilers[p_name]; + if (p.add) { + p.add(p.data, p_data); + } +} + +bool EngineDebugger::is_profiling(const StringName &p_name) { + return profilers.has(p_name) && profilers[p_name].active; +} + +bool EngineDebugger::has_profiler(const StringName &p_name) { + return profilers.has(p_name); +} + +bool EngineDebugger::has_capture(const StringName &p_name) { + return captures.has(p_name); +} + +Error EngineDebugger::capture_parse(const StringName &p_name, const String &p_msg, const Array &p_args, bool &r_captured) { + r_captured = false; + ERR_FAIL_COND_V_MSG(!captures.has(p_name), ERR_UNCONFIGURED, "Capture not registered: " + p_name); + const Capture &cap = captures[p_name]; + return cap.capture(cap.data, p_msg, p_args, r_captured); +} + +void EngineDebugger::line_poll() { + // The purpose of this is just processing events every now and then when the script might get too busy otherwise bugs like infinite loops can't be caught + if (poll_every % 2048 == 0) + poll_events(false); + poll_every++; +} + +void EngineDebugger::iteration(uint64_t p_frame_ticks, uint64_t p_idle_ticks, uint64_t p_physics_ticks, float p_physics_frame_time) { + frame_time = USEC_TO_SEC(p_frame_ticks); + idle_time = USEC_TO_SEC(p_idle_ticks); + physics_time = USEC_TO_SEC(p_physics_ticks); + physics_frame_time = p_physics_frame_time; + // Notify tick to running profilers + for (Map<StringName, Profiler>::Element *E = profilers.front(); E; E = E->next()) { + Profiler &p = E->get(); + if (!p.active || !p.tick) + continue; + p.tick(p.data, frame_time, idle_time, physics_time, physics_frame_time); + } + singleton->poll_events(true); +} + +void EngineDebugger::initialize(const String &p_uri, bool p_skip_breakpoints, Vector<String> p_breakpoints) { + if (p_uri.empty()) + return; + if (p_uri == "local://") { + singleton = memnew(LocalDebugger); + script_debugger = memnew(ScriptDebugger); + // Tell the OS that we want to handle termination signals. + OS::get_singleton()->initialize_debugging(); + } else { + singleton = RemoteDebugger::create_for_uri(p_uri); + if (!singleton) + return; + script_debugger = memnew(ScriptDebugger); + // Notify editor of our pid (to allow focus stealing). + Array msg; + msg.push_back(OS::get_singleton()->get_process_id()); + singleton->send_message("set_pid", msg); + } + if (!singleton) + return; + + // There is a debugger, parse breakpoints. + ScriptDebugger *script_debugger = singleton->get_script_debugger(); + script_debugger->set_skip_breakpoints(p_skip_breakpoints); + + for (int i = 0; i < p_breakpoints.size(); i++) { + + String bp = p_breakpoints[i]; + int sp = bp.find_last(":"); + ERR_CONTINUE_MSG(sp == -1, "Invalid breakpoint: '" + bp + "', expected file:line format."); + + script_debugger->insert_breakpoint(bp.substr(sp + 1, bp.length()).to_int(), bp.substr(0, sp)); + } +} + +void EngineDebugger::deinitialize() { + if (!singleton) + return; + + // Stop all profilers + for (Map<StringName, Profiler>::Element *E = profilers.front(); E; E = E->next()) { + if (E->get().active) + singleton->profiler_enable(E->key(), false); + } + + // Flush any remaining message + singleton->poll_events(false); + + memdelete(singleton); + singleton = NULL; + profilers.clear(); + captures.clear(); +} + +EngineDebugger::~EngineDebugger() { + if (script_debugger) + memdelete(script_debugger); + script_debugger = NULL; + singleton = NULL; +} diff --git a/core/debugger/engine_debugger.h b/core/debugger/engine_debugger.h new file mode 100644 index 0000000000..9e01aeba18 --- /dev/null +++ b/core/debugger/engine_debugger.h @@ -0,0 +1,130 @@ +/*************************************************************************/ +/* engine_debugger.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef ENGINE_DEBUGGER_H +#define ENGINE_DEBUGGER_H + +#include "core/array.h" +#include "core/map.h" +#include "core/string_name.h" +#include "core/ustring.h" +#include "core/variant.h" +#include "core/vector.h" + +class ScriptDebugger; + +class EngineDebugger { +public: + typedef void (*ProfilingToggle)(void *p_user, bool p_enable, const Array &p_opts); + typedef void (*ProfilingTick)(void *p_user, float p_frame_time, float p_idle_time, float p_physics_time, float p_physics_frame_time); + typedef void (*ProfilingAdd)(void *p_user, const Array &p_arr); + typedef Error (*CaptureFunc)(void *p_user, const String &p_msg, const Array &p_args, bool &r_captured); + + class Profiler { + friend class EngineDebugger; + + ProfilingToggle toggle = NULL; + ProfilingAdd add = NULL; + ProfilingTick tick = NULL; + void *data = NULL; + bool active = false; + + public: + Profiler() {} + Profiler(void *p_data, ProfilingToggle p_toggle, ProfilingAdd p_add, ProfilingTick p_tick) { + data = p_data; + toggle = p_toggle; + add = p_add; + tick = p_tick; + } + }; + + class Capture { + friend class EngineDebugger; + + CaptureFunc capture = NULL; + void *data = NULL; + + public: + Capture() {} + Capture(void *p_data, CaptureFunc p_capture) { + data = p_data; + capture = p_capture; + } + }; + +private: + float frame_time = 0.0; + float idle_time = 0.0; + float physics_time = 0.0; + float physics_frame_time = 0.0; + + uint32_t poll_every = 0; + +protected: + static EngineDebugger *singleton; + static ScriptDebugger *script_debugger; + + static Map<StringName, Profiler> profilers; + static Map<StringName, Capture> captures; + +public: + _FORCE_INLINE_ static EngineDebugger *get_singleton() { return singleton; } + _FORCE_INLINE_ static bool is_active() { return singleton != NULL && script_debugger != NULL; } + + _FORCE_INLINE_ static ScriptDebugger *get_script_debugger() { return script_debugger; }; + + static void initialize(const String &p_uri, bool p_skip_breakpoints, Vector<String> p_breakpoints); + static void deinitialize(); + static void register_profiler(const StringName &p_name, const Profiler &p_profiler); + static void unregister_profiler(const StringName &p_name); + static bool is_profiling(const StringName &p_name); + static bool has_profiler(const StringName &p_name); + static void profiler_add_frame_data(const StringName &p_name, const Array &p_data); + + static void register_message_capture(const StringName &p_name, Capture p_func); + static void unregister_message_capture(const StringName &p_name); + static bool has_capture(const StringName &p_name); + + void iteration(uint64_t p_frame_ticks, uint64_t p_idle_ticks, uint64_t p_physics_ticks, float p_physics_frame_time); + void profiler_enable(const StringName &p_name, bool p_enabled, const Array &p_opts = Array()); + Error capture_parse(const StringName &p_name, const String &p_msg, const Array &p_args, bool &r_captured); + + void line_poll(); + + virtual void poll_events(bool p_is_idle) {} + virtual void send_message(const String &p_msg, const Array &p_data) = 0; + virtual void send_error(const String &p_func, const String &p_file, int p_line, const String &p_err, const String &p_descr, ErrorHandlerType p_type) = 0; + virtual void debug(bool p_can_continue = true, bool p_is_error_breakpoint = false) = 0; + + virtual ~EngineDebugger(); +}; + +#endif // ENGINE_DEBUGGER_H diff --git a/core/debugger/local_debugger.cpp b/core/debugger/local_debugger.cpp new file mode 100644 index 0000000000..913d3fc031 --- /dev/null +++ b/core/debugger/local_debugger.cpp @@ -0,0 +1,416 @@ +/*************************************************************************/ +/* local_debugger.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "local_debugger.h" + +#include "core/debugger/script_debugger.h" +#include "core/os/os.h" +#include "scene/main/scene_tree.h" + +struct LocalDebugger::ScriptsProfiler { + struct ProfileInfoSort { + + bool operator()(const ScriptLanguage::ProfilingInfo &A, const ScriptLanguage::ProfilingInfo &B) const { + return A.total_time > B.total_time; + } + }; + + float frame_time = 0; + uint64_t idle_accum = 0; + Vector<ScriptLanguage::ProfilingInfo> pinfo; + + void toggle(bool p_enable, const Array &p_opts) { + if (p_enable) { + for (int i = 0; i < ScriptServer::get_language_count(); i++) { + ScriptServer::get_language(i)->profiling_start(); + } + + print_line("BEGIN PROFILING"); + pinfo.resize(32768); + } else { + _print_frame_data(true); + for (int i = 0; i < ScriptServer::get_language_count(); i++) { + ScriptServer::get_language(i)->profiling_stop(); + } + } + } + + void tick(float p_frame_time, float p_idle_time, float p_physics_time, float p_physics_frame_time) { + frame_time = p_frame_time; + _print_frame_data(false); + } + + void _print_frame_data(bool p_accumulated) { + uint64_t diff = OS::get_singleton()->get_ticks_usec() - idle_accum; + + if (!p_accumulated && diff < 1000000) //show every one second + return; + + idle_accum = OS::get_singleton()->get_ticks_usec(); + + int ofs = 0; + for (int i = 0; i < ScriptServer::get_language_count(); i++) { + if (p_accumulated) + ofs += ScriptServer::get_language(i)->profiling_get_accumulated_data(&pinfo.write[ofs], pinfo.size() - ofs); + else + ofs += ScriptServer::get_language(i)->profiling_get_frame_data(&pinfo.write[ofs], pinfo.size() - ofs); + } + + SortArray<ScriptLanguage::ProfilingInfo, ProfileInfoSort> sort; + sort.sort(pinfo.ptrw(), ofs); + + // compute total script frame time + uint64_t script_time_us = 0; + for (int i = 0; i < ofs; i++) { + + script_time_us += pinfo[i].self_time; + } + float script_time = USEC_TO_SEC(script_time_us); + float total_time = p_accumulated ? script_time : frame_time; + + if (!p_accumulated) { + print_line("FRAME: total: " + rtos(total_time) + " script: " + rtos(script_time) + "/" + itos(script_time * 100 / total_time) + " %"); + } else { + print_line("ACCUMULATED: total: " + rtos(total_time)); + } + + for (int i = 0; i < ofs; i++) { + + print_line(itos(i) + ":" + pinfo[i].signature); + float tt = USEC_TO_SEC(pinfo[i].total_time); + float st = USEC_TO_SEC(pinfo[i].self_time); + print_line("\ttotal: " + rtos(tt) + "/" + itos(tt * 100 / total_time) + " % \tself: " + rtos(st) + "/" + itos(st * 100 / total_time) + " % tcalls: " + itos(pinfo[i].call_count)); + } + } + + ScriptsProfiler() { + idle_accum = OS::get_singleton()->get_ticks_usec(); + } +}; + +void LocalDebugger::debug(bool p_can_continue, bool p_is_error_breakpoint) { + + ScriptLanguage *script_lang = script_debugger->get_break_language(); + + if (!target_function.empty()) { + String current_function = script_lang->debug_get_stack_level_function(0); + if (current_function != target_function) { + script_debugger->set_depth(0); + script_debugger->set_lines_left(1); + return; + } + target_function = ""; + } + + print_line("\nDebugger Break, Reason: '" + script_lang->debug_get_error() + "'"); + 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) + "'"); + print_line("Enter \"help\" for assistance."); + int current_frame = 0; + int total_frames = script_lang->debug_get_stack_level_count(); + while (true) { + + OS::get_singleton()->print("debug> "); + String line = OS::get_singleton()->get_stdin_string().strip_edges(); + + // Cache options + String variable_prefix = options["variable_prefix"]; + + if (line == "") { + print_line("\nDebugger Break, Reason: '" + script_lang->debug_get_error() + "'"); + 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) + "'"); + print_line("Enter \"help\" for assistance."); + } else if (line == "c" || line == "continue") + break; + else if (line == "bt" || line == "breakpoint") { + + for (int i = 0; i < total_frames; i++) { + + String cfi = (current_frame == i) ? "*" : " "; //current frame indicator + 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) + "'"); + } + + } else if (line.begins_with("fr") || line.begins_with("frame")) { + + if (line.get_slice_count(" ") == 1) { + 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) + "'"); + } else { + int frame = line.get_slicec(' ', 1).to_int(); + if (frame < 0 || frame >= total_frames) { + print_line("Error: Invalid frame."); + } else { + current_frame = frame; + 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) + "'"); + } + } + + } else if (line.begins_with("set")) { + + if (line.get_slice_count(" ") == 1) { + + for (Map<String, String>::Element *E = options.front(); E; E = E->next()) { + print_line("\t" + E->key() + "=" + E->value()); + } + + } else { + String key_value = line.get_slicec(' ', 1); + int value_pos = key_value.find("="); + + if (value_pos < 0) { + print_line("Error: Invalid set format. Use: set key=value"); + } else { + + String key = key_value.left(value_pos); + + if (!options.has(key)) { + print_line("Error: Unknown option " + key); + } else { + + // Allow explicit tab character + String value = key_value.right(value_pos + 1).replace("\\t", "\t"); + + options[key] = value; + } + } + } + + } else if (line == "lv" || line == "locals") { + + List<String> locals; + List<Variant> values; + script_lang->debug_get_stack_level_locals(current_frame, &locals, &values); + print_variables(locals, values, variable_prefix); + + } else if (line == "gv" || line == "globals") { + + List<String> globals; + List<Variant> values; + script_lang->debug_get_globals(&globals, &values); + print_variables(globals, values, variable_prefix); + + } else if (line == "mv" || line == "members") { + + List<String> members; + List<Variant> values; + script_lang->debug_get_stack_level_members(current_frame, &members, &values); + print_variables(members, values, variable_prefix); + + } else if (line.begins_with("p") || line.begins_with("print")) { + + if (line.get_slice_count(" ") <= 1) { + print_line("Usage: print <expre>"); + } else { + + String expr = line.get_slicec(' ', 2); + String res = script_lang->debug_parse_stack_level_expression(current_frame, expr); + print_line(res); + } + + } else if (line == "s" || line == "step") { + + script_debugger->set_depth(-1); + script_debugger->set_lines_left(1); + break; + } else if (line == "n" || line == "next") { + + script_debugger->set_depth(0); + script_debugger->set_lines_left(1); + break; + } else if (line == "fin" || line == "finish") { + + String current_function = script_lang->debug_get_stack_level_function(0); + + for (int i = 0; i < total_frames; i++) { + target_function = script_lang->debug_get_stack_level_function(i); + if (target_function != current_function) { + script_debugger->set_depth(0); + script_debugger->set_lines_left(1); + return; + } + } + + print_line("Error: Reached last frame."); + target_function = ""; + + } else if (line.begins_with("br") || line.begins_with("break")) { + + if (line.get_slice_count(" ") <= 1) { + + const Map<int, Set<StringName> > &breakpoints = script_debugger->get_breakpoints(); + if (breakpoints.size() == 0) { + print_line("No Breakpoints."); + continue; + } + + print_line("Breakpoint(s): " + itos(breakpoints.size())); + for (Map<int, Set<StringName> >::Element *E = breakpoints.front(); E; E = E->next()) { + print_line("\t" + String(E->value().front()->get()) + ":" + itos(E->key())); + } + + } else { + + Pair<String, int> breakpoint = to_breakpoint(line); + + String source = breakpoint.first; + int linenr = breakpoint.second; + + if (source.empty()) + continue; + + script_debugger->insert_breakpoint(linenr, source); + + print_line("Added breakpoint at " + source + ":" + itos(linenr)); + } + + } else if (line == "q" || line == "quit") { + + // Do not stop again on quit + script_debugger->clear_breakpoints(); + script_debugger->set_depth(-1); + script_debugger->set_lines_left(-1); + + SceneTree::get_singleton()->quit(); + break; + } else if (line.begins_with("delete")) { + + if (line.get_slice_count(" ") <= 1) { + script_debugger->clear_breakpoints(); + } else { + + Pair<String, int> breakpoint = to_breakpoint(line); + + String source = breakpoint.first; + int linenr = breakpoint.second; + + if (source.empty()) + continue; + + script_debugger->remove_breakpoint(linenr, source); + + print_line("Removed breakpoint at " + source + ":" + itos(linenr)); + } + + } else if (line == "h" || line == "help") { + + print_line("Built-In Debugger command list:\n"); + print_line("\tc,continue\t\t Continue execution."); + print_line("\tbt,backtrace\t\t Show stack trace (frames)."); + print_line("\tfr,frame <frame>:\t Change current frame."); + print_line("\tlv,locals\t\t Show local variables for current frame."); + print_line("\tmv,members\t\t Show member variables for \"this\" in frame."); + print_line("\tgv,globals\t\t Show global variables."); + print_line("\tp,print <expr>\t\t Execute and print variable in expression."); + print_line("\ts,step\t\t\t Step to next line."); + print_line("\tn,next\t\t\t Next line."); + print_line("\tfin,finish\t\t Step out of current frame."); + print_line("\tbr,break [source:line]\t List all breakpoints or place a breakpoint."); + print_line("\tdelete [source:line]:\t Delete one/all breakpoints."); + print_line("\tset [key=value]:\t List all options, or set one."); + print_line("\tq,quit\t\t\t Quit application."); + } else { + print_line("Error: Invalid command, enter \"help\" for assistance."); + } + } +} + +void LocalDebugger::print_variables(const List<String> &names, const List<Variant> &values, const String &variable_prefix) { + + String value; + Vector<String> value_lines; + const List<Variant>::Element *V = values.front(); + for (const List<String>::Element *E = names.front(); E; E = E->next()) { + + value = String(V->get()); + + if (variable_prefix.empty()) { + print_line(E->get() + ": " + String(V->get())); + } else { + + print_line(E->get() + ":"); + value_lines = value.split("\n"); + for (int i = 0; i < value_lines.size(); ++i) { + print_line(variable_prefix + value_lines[i]); + } + } + + V = V->next(); + } +} + +Pair<String, int> LocalDebugger::to_breakpoint(const String &p_line) { + + String breakpoint_part = p_line.get_slicec(' ', 1); + Pair<String, int> breakpoint; + + int last_colon = breakpoint_part.rfind(":"); + if (last_colon < 0) { + print_line("Error: Invalid breakpoint format. Expected [source:line]"); + return breakpoint; + } + + breakpoint.first = script_debugger->breakpoint_find_source(breakpoint_part.left(last_colon).strip_edges()); + breakpoint.second = breakpoint_part.right(last_colon).strip_edges().to_int(); + + return breakpoint; +} + +void LocalDebugger::send_message(const String &p_message, const Array &p_args) { + + // This needs to be cleaned up entirely. + // print_line("MESSAGE: '" + p_message + "' - " + String(Variant(p_args))); +} + +void LocalDebugger::send_error(const String &p_func, const String &p_file, int p_line, const String &p_err, const String &p_descr, ErrorHandlerType p_type) { + + print_line("ERROR: '" + (p_descr.empty() ? p_err : p_descr) + "'"); +} + +LocalDebugger::LocalDebugger() { + + options["variable_prefix"] = ""; + + // Bind scripts profiler. + scripts_profiler = memnew(ScriptsProfiler); + Profiler scr_prof( + scripts_profiler, + [](void *p_user, bool p_enable, const Array &p_opts) { + ((ScriptsProfiler *)p_user)->toggle(p_enable, p_opts); + }, + NULL, + [](void *p_user, float p_frame_time, float p_idle_time, float p_physics_time, float p_physics_frame_time) { + ((ScriptsProfiler *)p_user)->tick(p_frame_time, p_idle_time, p_physics_time, p_physics_frame_time); + }); + register_profiler("scripts", scr_prof); +} + +LocalDebugger::~LocalDebugger() { + unregister_profiler("scripts"); + if (scripts_profiler) + memdelete(scripts_profiler); +} diff --git a/core/debugger/local_debugger.h b/core/debugger/local_debugger.h new file mode 100644 index 0000000000..e299df0546 --- /dev/null +++ b/core/debugger/local_debugger.h @@ -0,0 +1,60 @@ +/*************************************************************************/ +/* local_debugger.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef LOCAL_DEBUGGER_H +#define LOCAL_DEBUGGER_H + +#include "core/debugger/engine_debugger.h" +#include "core/list.h" +#include "core/script_language.h" + +class LocalDebugger : public EngineDebugger { + +private: + struct ScriptsProfiler; + + ScriptsProfiler *scripts_profiler = NULL; + + String target_function; + Map<String, String> options; + + Pair<String, int> to_breakpoint(const String &p_line); + void print_variables(const List<String> &names, const List<Variant> &values, const String &variable_prefix); + +public: + void debug(bool p_can_continue, bool p_is_error_breakpoint); + void send_message(const String &p_message, const Array &p_args); + void send_error(const String &p_func, const String &p_file, int p_line, const String &p_err, const String &p_descr, ErrorHandlerType p_type); + + LocalDebugger(); + ~LocalDebugger(); +}; + +#endif // LOCAL_DEBUGGER_H diff --git a/core/debugger/remote_debugger.cpp b/core/debugger/remote_debugger.cpp new file mode 100644 index 0000000000..7952391a27 --- /dev/null +++ b/core/debugger/remote_debugger.cpp @@ -0,0 +1,935 @@ +/*************************************************************************/ +/* remote_debugger.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "remote_debugger.h" + +#include "core/debugger/debugger_marshalls.h" +#include "core/debugger/engine_debugger.h" +#include "core/debugger/script_debugger.h" +#include "core/os/input.h" +#include "core/os/os.h" +#include "core/project_settings.h" +#include "core/script_language.h" +#include "scene/main/node.h" + +template <typename T> +void RemoteDebugger::_bind_profiler(const String &p_name, T *p_prof) { + EngineDebugger::Profiler prof( + p_prof, + [](void *p_user, bool p_enable, const Array &p_opts) { + ((T *)p_user)->toggle(p_enable, p_opts); + }, + [](void *p_user, const Array &p_data) { + ((T *)p_user)->add(p_data); + }, + [](void *p_user, float p_frame_time, float p_idle_time, float p_physics_time, float p_physics_frame_time) { + ((T *)p_user)->tick(p_frame_time, p_idle_time, p_physics_time, p_physics_frame_time); + }); + EngineDebugger::register_profiler(p_name, prof); +} + +struct RemoteDebugger::NetworkProfiler { + +public: + typedef DebuggerMarshalls::MultiplayerNodeInfo NodeInfo; + struct BandwidthFrame { + uint32_t timestamp; + int packet_size; + }; + + int bandwidth_in_ptr = 0; + Vector<BandwidthFrame> bandwidth_in; + int bandwidth_out_ptr = 0; + Vector<BandwidthFrame> bandwidth_out; + uint64_t last_bandwidth_time = 0; + + Map<ObjectID, NodeInfo> multiplayer_node_data; + uint64_t last_profile_time = 0; + + NetworkProfiler() {} + + int bandwidth_usage(const Vector<BandwidthFrame> &p_buffer, int p_pointer) { + int total_bandwidth = 0; + + uint32_t timestamp = OS::get_singleton()->get_ticks_msec(); + uint32_t final_timestamp = timestamp - 1000; + + int i = (p_pointer + p_buffer.size() - 1) % p_buffer.size(); + + while (i != p_pointer && p_buffer[i].packet_size > 0) { + if (p_buffer[i].timestamp < final_timestamp) { + return total_bandwidth; + } + total_bandwidth += p_buffer[i].packet_size; + i = (i + p_buffer.size() - 1) % p_buffer.size(); + } + + ERR_FAIL_COND_V_MSG(i == p_pointer, total_bandwidth, "Reached the end of the bandwidth profiler buffer, values might be inaccurate."); + return total_bandwidth; + } + + void init_node(const ObjectID p_node) { + if (multiplayer_node_data.has(p_node)) + return; + multiplayer_node_data.insert(p_node, DebuggerMarshalls::MultiplayerNodeInfo()); + multiplayer_node_data[p_node].node = p_node; + multiplayer_node_data[p_node].node_path = Object::cast_to<Node>(ObjectDB::get_instance(p_node))->get_path(); + multiplayer_node_data[p_node].incoming_rpc = 0; + multiplayer_node_data[p_node].incoming_rset = 0; + multiplayer_node_data[p_node].outgoing_rpc = 0; + multiplayer_node_data[p_node].outgoing_rset = 0; + } + + void toggle(bool p_enable, const Array &p_opts) { + multiplayer_node_data.clear(); + + if (!p_enable) { + bandwidth_in.clear(); + bandwidth_out.clear(); + } else { + bandwidth_in_ptr = 0; + bandwidth_in.resize(16384); // ~128kB + for (int i = 0; i < bandwidth_in.size(); ++i) { + bandwidth_in.write[i].packet_size = -1; + } + bandwidth_out_ptr = 0; + bandwidth_out.resize(16384); // ~128kB + for (int i = 0; i < bandwidth_out.size(); ++i) { + bandwidth_out.write[i].packet_size = -1; + } + } + } + + void add(const Array &p_data) { + ERR_FAIL_COND(p_data.size() < 1); + const String type = p_data[0]; + if (type == "node") { + ERR_FAIL_COND(p_data.size() < 3); + const ObjectID id = p_data[1]; + const String what = p_data[2]; + init_node(id); + NodeInfo &info = multiplayer_node_data[id]; + if (what == "rpc_in") { + info.incoming_rpc++; + } else if (what == "rpc_out") { + info.outgoing_rpc++; + } else if (what == "rset_in") { + info.incoming_rset = 0; + } else if (what == "rset_out") { + info.outgoing_rset++; + } + } else if (type == "bandwidth") { + ERR_FAIL_COND(p_data.size() < 4); + const String inout = p_data[1]; + int time = p_data[2]; + int size = p_data[3]; + if (inout == "in") { + bandwidth_in.write[bandwidth_in_ptr].timestamp = time; + bandwidth_in.write[bandwidth_in_ptr].packet_size = size; + bandwidth_in_ptr = (bandwidth_in_ptr + 1) % bandwidth_in.size(); + } else if (inout == "out") { + bandwidth_out.write[bandwidth_out_ptr].timestamp = time; + bandwidth_out.write[bandwidth_out_ptr].packet_size = size; + bandwidth_out_ptr = (bandwidth_out_ptr + 1) % bandwidth_out.size(); + } + } + } + + void tick(float p_frame_time, float p_idle_time, float p_physics_time, float p_physics_frame_time) { + uint64_t pt = OS::get_singleton()->get_ticks_msec(); + if (pt - last_bandwidth_time > 200) { + last_bandwidth_time = pt; + int incoming_bandwidth = bandwidth_usage(bandwidth_in, bandwidth_in_ptr); + int outgoing_bandwidth = bandwidth_usage(bandwidth_out, bandwidth_out_ptr); + + Array arr; + arr.push_back(incoming_bandwidth); + arr.push_back(outgoing_bandwidth); + EngineDebugger::get_singleton()->send_message("network:bandwidth", arr); + } + if (pt - last_profile_time > 100) { + last_profile_time = pt; + DebuggerMarshalls::NetworkProfilerFrame frame; + for (Map<ObjectID, NodeInfo>::Element *E = multiplayer_node_data.front(); E; E = E->next()) { + frame.infos.push_back(E->get()); + } + multiplayer_node_data.clear(); + EngineDebugger::get_singleton()->send_message("network:profile_frame", frame.serialize()); + } + } +}; + +struct RemoteDebugger::ScriptsProfiler { + typedef DebuggerMarshalls::ScriptFunctionSignature FunctionSignature; + typedef DebuggerMarshalls::ScriptFunctionInfo FunctionInfo; + struct ProfileInfoSort { + + bool operator()(ScriptLanguage::ProfilingInfo *A, ScriptLanguage::ProfilingInfo *B) const { + return A->total_time < B->total_time; + } + }; + Vector<ScriptLanguage::ProfilingInfo> info; + Vector<ScriptLanguage::ProfilingInfo *> ptrs; + Map<StringName, int> sig_map; + int max_frame_functions = 16; + + void toggle(bool p_enable, const Array &p_opts) { + if (p_enable) { + sig_map.clear(); + for (int i = 0; i < ScriptServer::get_language_count(); i++) { + ScriptServer::get_language(i)->profiling_start(); + } + if (p_opts.size() == 1 && p_opts[0].get_type() == Variant::INT) { + max_frame_functions = MAX(0, int(p_opts[0])); + } + } else { + for (int i = 0; i < ScriptServer::get_language_count(); i++) { + ScriptServer::get_language(i)->profiling_stop(); + } + } + } + + void write_frame_data(Vector<FunctionInfo> &r_funcs, uint64_t &r_total, bool p_accumulated) { + int ofs = 0; + for (int i = 0; i < ScriptServer::get_language_count(); i++) { + if (p_accumulated) + ofs += ScriptServer::get_language(i)->profiling_get_accumulated_data(&info.write[ofs], info.size() - ofs); + else + ofs += ScriptServer::get_language(i)->profiling_get_frame_data(&info.write[ofs], info.size() - ofs); + } + + for (int i = 0; i < ofs; i++) { + ptrs.write[i] = &info.write[i]; + } + + SortArray<ScriptLanguage::ProfilingInfo *, ProfileInfoSort> sa; + sa.sort(ptrs.ptrw(), ofs); + + int to_send = MIN(ofs, max_frame_functions); + + // Check signatures first, and compute total time. + r_total = 0; + for (int i = 0; i < to_send; i++) { + if (!sig_map.has(ptrs[i]->signature)) { + int idx = sig_map.size(); + FunctionSignature sig; + sig.name = ptrs[i]->signature; + sig.id = idx; + EngineDebugger::get_singleton()->send_message("servers:function_signature", sig.serialize()); + sig_map[ptrs[i]->signature] = idx; + } + r_total += ptrs[i]->self_time; + } + + // Send frame, script time, functions information then + r_funcs.resize(to_send); + + FunctionInfo *w = r_funcs.ptrw(); + for (int i = 0; i < to_send; i++) { + if (sig_map.has(ptrs[i]->signature)) { + w[i].sig_id = sig_map[ptrs[i]->signature]; + } + w[i].call_count = ptrs[i]->call_count; + w[i].total_time = ptrs[i]->total_time / 1000000.0; + w[i].self_time = ptrs[i]->self_time / 1000000.0; + } + } + + ScriptsProfiler() { + info.resize(GLOBAL_GET("debug/settings/profiler/max_functions")); + ptrs.resize(info.size()); + } +}; + +struct RemoteDebugger::ServersProfiler { + + bool skip_profile_frame = false; + typedef DebuggerMarshalls::ServerInfo ServerInfo; + typedef DebuggerMarshalls::ServerFunctionInfo ServerFunctionInfo; + + Map<StringName, ServerInfo> server_data; + ScriptsProfiler scripts_profiler; + + float frame_time = 0; + float idle_time = 0; + float physics_time = 0; + float physics_frame_time = 0; + + void toggle(bool p_enable, const Array &p_opts) { + skip_profile_frame = false; + if (p_enable) { + server_data.clear(); // Clear old profiling data. + } else { + _send_frame_data(true); // Send final frame. + } + scripts_profiler.toggle(p_enable, p_opts); + } + + void add(const Array &p_data) { + String name = p_data[0]; + if (!server_data.has(name)) { + ServerInfo info; + info.name = name; + server_data[name] = info; + } + ServerInfo &srv = server_data[name]; + + ServerFunctionInfo fi; + fi.name = p_data[1]; + fi.time = p_data[2]; + srv.functions.push_back(fi); + } + + void tick(float p_frame_time, float p_idle_time, float p_physics_time, float p_physics_frame_time) { + frame_time = p_frame_time; + idle_time = p_idle_time; + physics_time = p_physics_time; + physics_frame_time = p_physics_frame_time; + _send_frame_data(false); + } + + void _send_frame_data(bool p_final) { + DebuggerMarshalls::ServersProfilerFrame frame; + frame.frame_number = Engine::get_singleton()->get_frames_drawn(); + frame.frame_time = frame_time; + frame.idle_time = idle_time; + frame.physics_time = physics_time; + frame.physics_frame_time = physics_frame_time; + Map<StringName, ServerInfo>::Element *E = server_data.front(); + while (E) { + if (!p_final) { + frame.servers.push_back(E->get()); + } + E->get().functions.clear(); + E = E->next(); + } + uint64_t time = 0; + scripts_profiler.write_frame_data(frame.script_functions, time, p_final); + frame.script_time = USEC_TO_SEC(time); + if (skip_profile_frame) { + skip_profile_frame = false; + return; + } + if (p_final) { + EngineDebugger::get_singleton()->send_message("servers:profile_total", frame.serialize()); + } else { + EngineDebugger::get_singleton()->send_message("servers:profile_frame", frame.serialize()); + } + } +}; + +struct RemoteDebugger::VisualProfiler { + + typedef DebuggerMarshalls::ServerInfo ServerInfo; + typedef DebuggerMarshalls::ServerFunctionInfo ServerFunctionInfo; + + Map<StringName, ServerInfo> server_data; + + void toggle(bool p_enable, const Array &p_opts) { + VS::get_singleton()->set_frame_profiling_enabled(p_enable); + } + + void add(const Array &p_data) {} + + void tick(float p_frame_time, float p_idle_time, float p_physics_time, float p_physics_frame_time) { + Vector<VS::FrameProfileArea> profile_areas = VS::get_singleton()->get_frame_profile(); + DebuggerMarshalls::VisualProfilerFrame frame; + if (!profile_areas.size()) + return; + + frame.frame_number = VS::get_singleton()->get_frame_profile_frame(); + frame.areas.append_array(profile_areas); + EngineDebugger::get_singleton()->send_message("visual:profile_frame", frame.serialize()); + } +}; + +struct RemoteDebugger::PerformanceProfiler { + + Object *performance = NULL; + int last_perf_time = 0; + + void toggle(bool p_enable, const Array &p_opts) {} + void add(const Array &p_data) {} + void tick(float p_frame_time, float p_idle_time, float p_physics_time, float p_physics_frame_time) { + if (!performance) + return; + + uint64_t pt = OS::get_singleton()->get_ticks_msec(); + if (pt - last_perf_time < 1000) + return; + last_perf_time = pt; + int max = performance->get("MONITOR_MAX"); + Array arr; + arr.resize(max); + for (int i = 0; i < max; i++) { + arr[i] = performance->call("get_monitor", i); + } + EngineDebugger::get_singleton()->send_message("performance:profile_frame", arr); + } + + PerformanceProfiler(Object *p_performance) { + performance = p_performance; + } +}; + +void RemoteDebugger::_send_resource_usage() { + + DebuggerMarshalls::ResourceUsage usage; + + List<VS::TextureInfo> tinfo; + VS::get_singleton()->texture_debug_usage(&tinfo); + + for (List<VS::TextureInfo>::Element *E = tinfo.front(); E; E = E->next()) { + + DebuggerMarshalls::ResourceInfo info; + info.path = E->get().path; + info.vram = E->get().bytes; + info.id = E->get().texture; + info.type = "Texture"; + if (E->get().depth == 0) { + info.format = itos(E->get().width) + "x" + itos(E->get().height) + " " + Image::get_format_name(E->get().format); + } else { + info.format = itos(E->get().width) + "x" + itos(E->get().height) + "x" + itos(E->get().depth) + " " + Image::get_format_name(E->get().format); + } + usage.infos.push_back(info); + } + + EngineDebugger::get_singleton()->send_message("memory:usage", usage.serialize()); +} + +Error RemoteDebugger::_put_msg(String p_message, Array p_data) { + Array msg; + msg.push_back(p_message); + msg.push_back(p_data); + Error err = peer->put_message(msg); + if (err != OK) + n_messages_dropped++; + return err; +} + +void RemoteDebugger::_err_handler(void *p_this, const char *p_func, const char *p_file, int p_line, const char *p_err, const char *p_descr, ErrorHandlerType p_type) { + + if (p_type == ERR_HANDLER_SCRIPT) + return; //ignore script errors, those go through debugger + + RemoteDebugger *rd = (RemoteDebugger *)p_this; + if (rd->flushing && Thread::get_caller_id() == rd->flush_thread) // Can't handle recursive errors during flush. + return; + + Vector<ScriptLanguage::StackInfo> si; + + for (int i = 0; i < ScriptServer::get_language_count(); i++) { + si = ScriptServer::get_language(i)->debug_get_current_stack_info(); + if (si.size()) + break; + } + + // send_error will lock internally. + rd->script_debugger->send_error(p_func, p_file, p_line, p_err, p_descr, p_type, si); +} + +void RemoteDebugger::_print_handler(void *p_this, const String &p_string, bool p_error) { + + RemoteDebugger *rd = (RemoteDebugger *)p_this; + + if (rd->flushing && Thread::get_caller_id() == rd->flush_thread) // Can't handle recursive prints during flush. + return; + + String s = p_string; + int allowed_chars = MIN(MAX(rd->max_chars_per_second - rd->char_count, 0), s.length()); + + if (allowed_chars == 0) + return; + + if (allowed_chars < s.length()) { + s = s.substr(0, allowed_chars); + } + + MutexLock lock(rd->mutex); + + rd->char_count += allowed_chars; + bool overflowed = rd->char_count >= rd->max_chars_per_second; + if (rd->is_peer_connected()) { + if (overflowed) + s += "[...]"; + rd->output_strings.push_back(s); + + if (overflowed) { + rd->output_strings.push_back("[output overflow, print less text!]"); + } + } +} + +RemoteDebugger::ErrorMessage RemoteDebugger::_create_overflow_error(const String &p_what, const String &p_descr) { + ErrorMessage oe; + oe.error = p_what; + oe.error_descr = p_descr; + oe.warning = false; + uint64_t time = OS::get_singleton()->get_ticks_msec(); + oe.hr = time / 3600000; + oe.min = (time / 60000) % 60; + oe.sec = (time / 1000) % 60; + oe.msec = time % 1000; + return oe; +} + +void RemoteDebugger::flush_output() { + flush_thread = Thread::get_caller_id(); + flushing = true; + MutexLock lock(mutex); + if (!is_peer_connected()) + return; + + if (n_messages_dropped > 0) { + ErrorMessage err_msg = _create_overflow_error("TOO_MANY_MESSAGES", "Too many messages! " + String::num_int64(n_messages_dropped) + " messages were dropped. Profiling might misbheave, try raising 'network/limits/debugger/max_queued_messages' in project setting."); + if (_put_msg("error", err_msg.serialize()) == OK) + n_messages_dropped = 0; + } + + if (output_strings.size()) { + + // Join output strings so we generate less messages. + Vector<String> strings; + strings.resize(output_strings.size()); + String *w = strings.ptrw(); + for (int i = 0; i < output_strings.size(); i++) { + w[i] = output_strings[i]; + } + + Array arr; + arr.push_back(strings); + _put_msg("output", arr); + output_strings.clear(); + } + + while (errors.size()) { + ErrorMessage oe = errors.front()->get(); + _put_msg("error", oe.serialize()); + errors.pop_front(); + } + + // Update limits + uint64_t ticks = OS::get_singleton()->get_ticks_usec() / 1000; + + if (ticks - last_reset > 1000) { + last_reset = ticks; + char_count = 0; + err_count = 0; + n_errors_dropped = 0; + warn_count = 0; + n_warnings_dropped = 0; + } + flushing = false; +} + +void RemoteDebugger::send_message(const String &p_message, const Array &p_args) { + + MutexLock lock(mutex); + if (is_peer_connected()) { + _put_msg(p_message, p_args); + } +} + +void RemoteDebugger::send_error(const String &p_func, const String &p_file, int p_line, const String &p_err, const String &p_descr, ErrorHandlerType p_type) { + + ErrorMessage oe; + oe.error = p_err; + oe.error_descr = p_descr; + oe.source_file = p_file; + oe.source_line = p_line; + oe.source_func = p_func; + oe.warning = p_type == ERR_HANDLER_WARNING; + uint64_t time = OS::get_singleton()->get_ticks_msec(); + oe.hr = time / 3600000; + oe.min = (time / 60000) % 60; + oe.sec = (time / 1000) % 60; + oe.msec = time % 1000; + oe.callstack.append_array(script_debugger->get_error_stack_info()); + + if (flushing && Thread::get_caller_id() == flush_thread) // Can't handle recursive errors during flush. + return; + + MutexLock lock(mutex); + + if (oe.warning) { + warn_count++; + } else { + err_count++; + } + + if (is_peer_connected()) { + + if (oe.warning) { + if (warn_count > max_warnings_per_second) { + n_warnings_dropped++; + if (n_warnings_dropped == 1) { + // Only print one message about dropping per second + ErrorMessage overflow = _create_overflow_error("TOO_MANY_WARNINGS", "Too many warnings! Ignoring warnings for up to 1 second."); + errors.push_back(overflow); + } + } else { + errors.push_back(oe); + } + } else { + if (err_count > max_errors_per_second) { + n_errors_dropped++; + if (n_errors_dropped == 1) { + // Only print one message about dropping per second + ErrorMessage overflow = _create_overflow_error("TOO_MANY_ERRORS", "Too many errors! Ignoring errors for up to 1 second."); + errors.push_back(overflow); + } + } else { + errors.push_back(oe); + } + } + } +} + +void RemoteDebugger::_send_stack_vars(List<String> &p_names, List<Variant> &p_vals, int p_type) { + DebuggerMarshalls::ScriptStackVariable stvar; + List<String>::Element *E = p_names.front(); + List<Variant>::Element *F = p_vals.front(); + while (E) { + stvar.name = E->get(); + stvar.value = F->get(); + stvar.type = p_type; + send_message("stack_frame_var", stvar.serialize()); + E = E->next(); + F = F->next(); + } +} + +Error RemoteDebugger::_try_capture(const String &p_msg, const Array &p_data, bool &r_captured) { + const int idx = p_msg.find(":"); + r_captured = false; + if (idx < 0) { // No prefix, unknown message. + return OK; + } + const String cap = p_msg.substr(0, idx); + if (!has_capture(cap)) + return ERR_UNAVAILABLE; // Unknown message... + const String msg = p_msg.substr(idx + 1); + return capture_parse(cap, msg, p_data, r_captured); +} + +void RemoteDebugger::debug(bool p_can_continue, bool p_is_error_breakpoint) { + + //this function is called when there is a debugger break (bug on script) + //or when execution is paused from editor + + if (script_debugger->is_skipping_breakpoints() && !p_is_error_breakpoint) + return; + + ERR_FAIL_COND_MSG(!is_peer_connected(), "Script Debugger failed to connect, but being used anyway."); + + ScriptLanguage *script_lang = script_debugger->get_break_language(); + const String error_str = script_lang ? script_lang->debug_get_error() : ""; + Array msg; + msg.push_back(p_can_continue); + msg.push_back(error_str); + send_message("debug_enter", msg); + + servers_profiler->skip_profile_frame = true; // Avoid frame time spike in debug. + + Input::MouseMode mouse_mode = Input::get_singleton()->get_mouse_mode(); + if (mouse_mode != Input::MOUSE_MODE_VISIBLE) + Input::get_singleton()->set_mouse_mode(Input::MOUSE_MODE_VISIBLE); + + uint64_t loop_begin_usec = 0; + uint64_t loop_time_sec = 0; + while (is_peer_connected()) { + loop_begin_usec = OS::get_singleton()->get_ticks_usec(); + + flush_output(); + peer->poll(); + + if (peer->has_message()) { + + Array cmd = peer->get_message(); + + ERR_CONTINUE(cmd.size() != 2); + ERR_CONTINUE(cmd[0].get_type() != Variant::STRING); + ERR_CONTINUE(cmd[1].get_type() != Variant::ARRAY); + + String command = cmd[0]; + Array data = cmd[1]; + + if (command == "step") { + script_debugger->set_depth(-1); + script_debugger->set_lines_left(1); + break; + + } else if (command == "next") { + script_debugger->set_depth(0); + script_debugger->set_lines_left(1); + break; + + } else if (command == "continue") { + script_debugger->set_depth(-1); + script_debugger->set_lines_left(-1); + OS::get_singleton()->move_window_to_foreground(); + break; + + } else if (command == "break") { + ERR_PRINT("Got break when already broke!"); + break; + + } else if (command == "get_stack_dump") { + ERR_FAIL_COND(!script_lang); + DebuggerMarshalls::ScriptStackDump dump; + int slc = script_lang->debug_get_stack_level_count(); + for (int i = 0; i < slc; i++) { + ScriptLanguage::StackInfo frame; + frame.file = script_lang->debug_get_stack_level_source(i); + frame.line = script_lang->debug_get_stack_level_line(i); + frame.func = script_lang->debug_get_stack_level_function(i); + dump.frames.push_back(frame); + } + send_message("stack_dump", dump.serialize()); + + } else if (command == "get_stack_frame_vars") { + ERR_FAIL_COND(data.size() != 1); + ERR_FAIL_COND(!script_lang); + int lv = data[0]; + + List<String> members; + List<Variant> member_vals; + if (ScriptInstance *inst = script_lang->debug_get_stack_level_instance(lv)) { + members.push_back("self"); + member_vals.push_back(inst->get_owner()); + } + script_lang->debug_get_stack_level_members(lv, &members, &member_vals); + ERR_FAIL_COND(members.size() != member_vals.size()); + + List<String> locals; + List<Variant> local_vals; + script_lang->debug_get_stack_level_locals(lv, &locals, &local_vals); + ERR_FAIL_COND(locals.size() != local_vals.size()); + + List<String> globals; + List<Variant> globals_vals; + script_lang->debug_get_globals(&globals, &globals_vals); + ERR_FAIL_COND(globals.size() != globals_vals.size()); + + send_message("stack_frame_vars", Array()); + _send_stack_vars(locals, local_vals, 0); + _send_stack_vars(members, member_vals, 1); + _send_stack_vars(globals, globals_vals, 2); + + } else if (command == "reload_scripts") { + reload_all_scripts = true; + + } else if (command == "breakpoint") { + ERR_FAIL_COND(data.size() < 3); + bool set = data[2]; + if (set) + script_debugger->insert_breakpoint(data[1], data[0]); + else + script_debugger->remove_breakpoint(data[1], data[0]); + + } else if (command == "set_skip_breakpoints") { + ERR_FAIL_COND(data.size() < 1); + script_debugger->set_skip_breakpoints(data[0]); + } else { + bool captured = false; + ERR_CONTINUE(_try_capture(command, data, captured) != OK); + if (!captured) + WARN_PRINT("Unknown message received from debugger: " + command); + } + } else { + OS::get_singleton()->delay_usec(10000); + OS::get_singleton()->process_and_drop_events(); + } + + // This is for the camera override to stay live even when the game is paused from the editor + loop_time_sec = (OS::get_singleton()->get_ticks_usec() - loop_begin_usec) / 1000000.0f; + VisualServer::get_singleton()->sync(); + if (VisualServer::get_singleton()->has_changed()) { + VisualServer::get_singleton()->draw(true, loop_time_sec * Engine::get_singleton()->get_time_scale()); + } + } + + send_message("debug_exit", Array()); + + if (mouse_mode != Input::MOUSE_MODE_VISIBLE) + Input::get_singleton()->set_mouse_mode(mouse_mode); +} + +void RemoteDebugger::poll_events(bool p_is_idle) { + if (peer.is_null()) + return; + + flush_output(); + peer->poll(); + while (peer->has_message()) { + + Array arr = peer->get_message(); + + ERR_CONTINUE(arr.size() != 2); + ERR_CONTINUE(arr[0].get_type() != Variant::STRING); + ERR_CONTINUE(arr[1].get_type() != Variant::ARRAY); + + const String cmd = arr[0]; + const int idx = cmd.find(":"); + bool parsed = false; + if (idx < 0) { // Not prefix, use scripts capture. + capture_parse("core", cmd, arr[1], parsed); + continue; + } + + const String cap = cmd.substr(0, idx); + if (!has_capture(cap)) + continue; // Unknown message... + + const String msg = cmd.substr(idx + 1); + capture_parse(cap, msg, arr[1], parsed); + } + + // Reload scripts during idle poll only. + if (p_is_idle && reload_all_scripts) { + for (int i = 0; i < ScriptServer::get_language_count(); i++) { + ScriptServer::get_language(i)->reload_all_scripts(); + } + reload_all_scripts = false; + } +} + +Error RemoteDebugger::_core_capture(const String &p_cmd, const Array &p_data, bool &r_captured) { + r_captured = true; + if (p_cmd == "reload_scripts") { + reload_all_scripts = true; + + } else if (p_cmd == "breakpoint") { + ERR_FAIL_COND_V(p_data.size() < 3, ERR_INVALID_DATA); + bool set = p_data[2]; + if (set) + script_debugger->insert_breakpoint(p_data[1], p_data[0]); + else + script_debugger->remove_breakpoint(p_data[1], p_data[0]); + + } else if (p_cmd == "set_skip_breakpoints") { + ERR_FAIL_COND_V(p_data.size() < 1, ERR_INVALID_DATA); + script_debugger->set_skip_breakpoints(p_data[0]); + } else if (p_cmd == "memory") { + _send_resource_usage(); + } else if (p_cmd == "break") { + script_debugger->debug(script_debugger->get_break_language()); + } else { + r_captured = false; + } + return OK; +} + +Error RemoteDebugger::_profiler_capture(const String &p_cmd, const Array &p_data, bool &r_captured) { + r_captured = false; + ERR_FAIL_COND_V(p_data.size() < 1, ERR_INVALID_DATA); + ERR_FAIL_COND_V(p_data[0].get_type() != Variant::BOOL, ERR_INVALID_DATA); + ERR_FAIL_COND_V(!has_profiler(p_cmd), ERR_UNAVAILABLE); + Array opts; + if (p_data.size() > 1) { // Optional profiler parameters. + ERR_FAIL_COND_V(p_data[1].get_type() != Variant::ARRAY, ERR_INVALID_DATA); + opts = p_data[1]; + } + r_captured = true; + profiler_enable(p_cmd, p_data[0], opts); + return OK; +} + +RemoteDebugger *RemoteDebugger::create_for_uri(const String &p_uri) { + Ref<RemoteDebuggerPeer> peer = RemoteDebuggerPeer::create_from_uri(p_uri); + if (peer.is_valid()) + return memnew(RemoteDebugger(peer)); + return NULL; +} + +RemoteDebugger::RemoteDebugger(Ref<RemoteDebuggerPeer> p_peer) { + peer = p_peer; + max_chars_per_second = GLOBAL_GET("network/limits/debugger/max_chars_per_second"); + max_errors_per_second = GLOBAL_GET("network/limits/debugger/max_errors_per_second"); + max_warnings_per_second = GLOBAL_GET("network/limits/debugger/max_warnings_per_second"); + + // Network Profiler + network_profiler = memnew(NetworkProfiler); + _bind_profiler("network", network_profiler); + + // Servers Profiler (audio/physics/...) + servers_profiler = memnew(ServersProfiler); + _bind_profiler("servers", servers_profiler); + + // Visual Profiler (cpu/gpu times) + visual_profiler = memnew(VisualProfiler); + _bind_profiler("visual", visual_profiler); + + // Perfromance Profiler + Object *perf = Engine::get_singleton()->get_singleton_object("Performance"); + if (perf) { + performance_profiler = memnew(PerformanceProfiler(perf)); + _bind_profiler("performance", performance_profiler); + profiler_enable("performance", true); + } + + // Core and profiler captures. + Capture core_cap(this, + [](void *p_user, const String &p_cmd, const Array &p_data, bool &r_captured) { + return ((RemoteDebugger *)p_user)->_core_capture(p_cmd, p_data, r_captured); + }); + register_message_capture("core", core_cap); + Capture profiler_cap(this, + [](void *p_user, const String &p_cmd, const Array &p_data, bool &r_captured) { + return ((RemoteDebugger *)p_user)->_profiler_capture(p_cmd, p_data, r_captured); + }); + register_message_capture("profiler", profiler_cap); + + // Error handlers + phl.printfunc = _print_handler; + phl.userdata = this; + add_print_handler(&phl); + + eh.errfunc = _err_handler; + eh.userdata = this; + add_error_handler(&eh); +} + +RemoteDebugger::~RemoteDebugger() { + remove_print_handler(&phl); + remove_error_handler(&eh); + + EngineDebugger::get_singleton()->unregister_profiler("servers"); + EngineDebugger::get_singleton()->unregister_profiler("network"); + EngineDebugger::get_singleton()->unregister_profiler("visual"); + if (EngineDebugger::has_profiler("performance")) { + EngineDebugger::get_singleton()->unregister_profiler("performance"); + } + memdelete(servers_profiler); + memdelete(network_profiler); + memdelete(visual_profiler); + if (performance_profiler) + memdelete(performance_profiler); +} diff --git a/core/debugger/remote_debugger.h b/core/debugger/remote_debugger.h new file mode 100644 index 0000000000..83789c67f9 --- /dev/null +++ b/core/debugger/remote_debugger.h @@ -0,0 +1,114 @@ +/*************************************************************************/ +/* remote_debugger.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef REMOTE_DEBUGGER_H +#define REMOTE_DEBUGGER_H + +#include "core/array.h" +#include "core/debugger/debugger_marshalls.h" +#include "core/debugger/engine_debugger.h" +#include "core/debugger/remote_debugger_peer.h" +#include "core/object.h" +#include "core/string_name.h" +#include "core/ustring.h" + +class RemoteDebugger : public EngineDebugger { + +private: + typedef DebuggerMarshalls::OutputError ErrorMessage; + + struct NetworkProfiler; + struct ServersProfiler; + struct ScriptsProfiler; + struct VisualProfiler; + struct PerformanceProfiler; + + NetworkProfiler *network_profiler = NULL; + ServersProfiler *servers_profiler = NULL; + VisualProfiler *visual_profiler = NULL; + PerformanceProfiler *performance_profiler = NULL; + + Ref<RemoteDebuggerPeer> peer; + + List<String> output_strings; + List<ErrorMessage> errors; + + int n_messages_dropped = 0; + int max_errors_per_second = 0; + int max_chars_per_second = 0; + int max_warnings_per_second = 0; + int n_errors_dropped = 0; + int n_warnings_dropped = 0; + int char_count = 0; + int err_count = 0; + int warn_count = 0; + int last_reset = 0; + bool reload_all_scripts = false; + + // Make handlers and send_message thread safe. + Mutex mutex; + bool flushing = false; + Thread::ID flush_thread = 0; + + PrintHandlerList phl; + static void _print_handler(void *p_this, const String &p_string, bool p_error); + ErrorHandlerList eh; + static void _err_handler(void *p_this, const char *p_func, const char *p_file, int p_line, const char *p_err, const char *p_descr, ErrorHandlerType p_type); + + ErrorMessage _create_overflow_error(const String &p_what, const String &p_descr); + Error _put_msg(String p_message, Array p_data); + + bool is_peer_connected() { return peer->is_peer_connected(); } + void flush_output(); + + void _send_resource_usage(); + void _send_stack_vars(List<String> &p_names, List<Variant> &p_vals, int p_type); + + Error _profiler_capture(const String &p_cmd, const Array &p_data, bool &r_captured); + Error _core_capture(const String &p_cmd, const Array &p_data, bool &r_captured); + + template <typename T> + void _bind_profiler(const String &p_name, T *p_prof); + Error _try_capture(const String &p_name, const Array &p_data, bool &r_captured); + +public: + static RemoteDebugger *create_for_uri(const String &p_uri); + + // Overrides + void poll_events(bool p_is_idle); + void send_message(const String &p_message, const Array &p_args); + void send_error(const String &p_func, const String &p_file, int p_line, const String &p_err, const String &p_descr, ErrorHandlerType p_type); + void debug(bool p_can_continue = true, bool p_is_error_breakpoint = false); + + RemoteDebugger(Ref<RemoteDebuggerPeer> p_peer); + ~RemoteDebugger(); +}; + +#endif // REMOTE_DEBUGGER_H diff --git a/core/debugger/remote_debugger_peer.cpp b/core/debugger/remote_debugger_peer.cpp new file mode 100644 index 0000000000..42c2c8e309 --- /dev/null +++ b/core/debugger/remote_debugger_peer.cpp @@ -0,0 +1,242 @@ +/*************************************************************************/ +/* remote_debugger_peer.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "remote_debugger_peer.h" + +#include "core/io/marshalls.h" +#include "core/os/os.h" +#include "core/project_settings.h" + +bool RemoteDebuggerPeerTCP::is_peer_connected() { + return connected; +} + +bool RemoteDebuggerPeerTCP::has_message() { + return in_queue.size() > 0; +} + +Array RemoteDebuggerPeerTCP::get_message() { + MutexLock lock(mutex); + ERR_FAIL_COND_V(!has_message(), Array()); + Array out = in_queue[0]; + in_queue.pop_front(); + return out; +} + +Error RemoteDebuggerPeerTCP::put_message(const Array &p_arr) { + MutexLock lock(mutex); + if (out_queue.size() >= max_queued_messages) + return ERR_OUT_OF_MEMORY; + + out_queue.push_back(p_arr); + return OK; +} + +int RemoteDebuggerPeerTCP::get_max_message_size() const { + return 8 << 20; // 8 MiB +} + +void RemoteDebuggerPeerTCP::close() { + if (thread) { + running = false; + Thread::wait_to_finish(thread); + memdelete(thread); + thread = NULL; + } + tcp_client->disconnect_from_host(); + out_buf.resize(0); + in_buf.resize(0); +} + +RemoteDebuggerPeerTCP::RemoteDebuggerPeerTCP(Ref<StreamPeerTCP> p_tcp) { + // This means remote debugger takes 16 MiB just because it exists... + in_buf.resize((8 << 20) + 4); // 8 MiB should be way more than enough (need 4 extra bytes for encoding packet size). + out_buf.resize(8 << 20); // 8 MiB should be way more than enough + tcp_client = p_tcp; + if (tcp_client.is_valid()) { // Attaching to an already connected stream. + connected = true; +#ifndef NO_THREADS + running = true; + thread = Thread::create(_thread_func, this); +#endif + } else { + tcp_client.instance(); + } +} + +RemoteDebuggerPeerTCP::~RemoteDebuggerPeerTCP() { + close(); +} + +void RemoteDebuggerPeerTCP::_write_out() { + while (tcp_client->poll(NetSocket::POLL_TYPE_OUT) == OK) { + uint8_t *buf = out_buf.ptrw(); + if (out_left <= 0) { + if (out_queue.size() == 0) + break; // Nothing left to send + mutex.lock(); + Variant var = out_queue[0]; + out_queue.pop_front(); + mutex.unlock(); + int size = 0; + Error err = encode_variant(var, NULL, size); + ERR_CONTINUE(err != OK || size > out_buf.size() - 4); // 4 bytes separator. + encode_uint32(size, buf); + encode_variant(var, buf + 4, size); + out_left = size + 4; + out_pos = 0; + } + int sent = 0; + tcp_client->put_partial_data(buf + out_pos, out_left, sent); + out_left -= sent; + out_pos += sent; + } +} + +void RemoteDebuggerPeerTCP::_read_in() { + while (tcp_client->poll(NetSocket::POLL_TYPE_IN) == OK) { + uint8_t *buf = in_buf.ptrw(); + if (in_left <= 0) { + if (in_queue.size() > max_queued_messages) { + break; // Too many messages already in queue. + } + if (tcp_client->get_available_bytes() < 4) { + break; // Need 4 more bytes. + } + uint32_t size = 0; + int read = 0; + Error err = tcp_client->get_partial_data((uint8_t *)&size, 4, read); + ERR_CONTINUE(read != 4 || err != OK || size > (uint32_t)in_buf.size()); + in_left = size; + in_pos = 0; + } + int read = 0; + tcp_client->get_partial_data(buf + in_pos, in_left, read); + in_left -= read; + in_pos += read; + if (in_left == 0) { + Variant var; + Error err = decode_variant(var, buf, in_pos, &read); + ERR_CONTINUE(read != in_pos || err != OK); + ERR_CONTINUE_MSG(var.get_type() != Variant::ARRAY, "Malformed packet received, not an Array."); + mutex.lock(); + in_queue.push_back(var); + mutex.unlock(); + } + } +} + +Error RemoteDebuggerPeerTCP::connect_to_host(const String &p_host, uint16_t p_port) { + + IP_Address ip; + if (p_host.is_valid_ip_address()) + ip = p_host; + else + ip = IP::get_singleton()->resolve_hostname(p_host); + + int port = p_port; + + const int tries = 6; + int waits[tries] = { 1, 10, 100, 1000, 1000, 1000 }; + + tcp_client->connect_to_host(ip, port); + + for (int i = 0; i < tries; i++) { + + if (tcp_client->get_status() == StreamPeerTCP::STATUS_CONNECTED) { + print_verbose("Remote Debugger: Connected!"); + break; + } else { + + const int ms = waits[i]; + OS::get_singleton()->delay_usec(ms * 1000); + print_verbose("Remote Debugger: Connection failed with status: '" + String::num(tcp_client->get_status()) + "', retrying in " + String::num(ms) + " msec."); + }; + }; + + if (tcp_client->get_status() != StreamPeerTCP::STATUS_CONNECTED) { + + ERR_PRINT("Remote Debugger: Unable to connect. Status: " + String::num(tcp_client->get_status()) + "."); + return FAILED; + }; + connected = true; +#ifndef NO_THREADS + running = true; + thread = Thread::create(_thread_func, this); +#endif + return OK; +} + +void RemoteDebuggerPeerTCP::_thread_func(void *p_ud) { + RemoteDebuggerPeerTCP *peer = (RemoteDebuggerPeerTCP *)p_ud; + while (peer->running && peer->is_peer_connected()) { + peer->_poll(); + if (!peer->is_peer_connected()) + break; + peer->tcp_client->poll(NetSocket::POLL_TYPE_IN_OUT, 1); + } +} + +void RemoteDebuggerPeerTCP::poll() { +#ifdef NO_THREADS + _poll(); +#endif +} + +void RemoteDebuggerPeerTCP::_poll() { + if (connected) { + _write_out(); + _read_in(); + connected = tcp_client->get_status() == StreamPeerTCP::STATUS_CONNECTED; + } +} + +Ref<RemoteDebuggerPeer> RemoteDebuggerPeer::create_from_uri(const String p_uri) { + if (!p_uri.begins_with("tcp://")) + return Ref<RemoteDebuggerPeer>(); // Only TCP supported for now, more to come. + + String debug_host = p_uri.replace("tcp://", ""); + uint16_t debug_port = 6007; + + if (debug_host.find(":") != -1) { + int sep_pos = debug_host.find_last(":"); + debug_port = debug_host.substr(sep_pos + 1).to_int(); + debug_host = debug_host.substr(0, sep_pos); + } + Ref<RemoteDebuggerPeerTCP> peer = Ref<RemoteDebuggerPeer>(memnew(RemoteDebuggerPeerTCP)); + Error err = peer->connect_to_host(debug_host, debug_port); + if (err != OK) + return Ref<RemoteDebuggerPeer>(); + return peer; +} + +RemoteDebuggerPeer::RemoteDebuggerPeer() { + max_queued_messages = (int)GLOBAL_GET("network/limits/debugger/max_queued_messages"); +} diff --git a/core/debugger/remote_debugger_peer.h b/core/debugger/remote_debugger_peer.h new file mode 100644 index 0000000000..6fc3214a51 --- /dev/null +++ b/core/debugger/remote_debugger_peer.h @@ -0,0 +1,94 @@ +/*************************************************************************/ +/* remote_debugger_peer.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef REMOTE_DEBUGGER_PEER_H +#define REMOTE_DEBUGGER_PEER_H + +#include "core/io/stream_peer_tcp.h" +#include "core/os/mutex.h" +#include "core/os/thread.h" +#include "core/reference.h" +#include "core/ustring.h" + +class RemoteDebuggerPeer : public Reference { +protected: + int max_queued_messages = 4096; + +public: + static Ref<RemoteDebuggerPeer> create_from_uri(const String p_uri); + virtual bool is_peer_connected() = 0; + virtual bool has_message() = 0; + virtual Error put_message(const Array &p_arr) = 0; + virtual Array get_message() = 0; + virtual void close() = 0; + virtual void poll() = 0; + virtual int get_max_message_size() const = 0; + + RemoteDebuggerPeer(); +}; + +class RemoteDebuggerPeerTCP : public RemoteDebuggerPeer { +private: + Ref<StreamPeerTCP> tcp_client; + Mutex mutex; + Thread *thread = NULL; + List<Array> in_queue; + List<Array> out_queue; + int out_left = 0; + int out_pos = 0; + Vector<uint8_t> out_buf; + int in_left = 0; + int in_pos = 0; + Vector<uint8_t> in_buf; + bool connected = false; + bool running = false; + + static void _thread_func(void *p_ud); + + void _poll(); + void _write_out(); + void _read_in(); + +public: + Error connect_to_host(const String &p_host, uint16_t p_port); + + void poll(); + bool is_peer_connected(); + bool has_message(); + Array get_message(); + Error put_message(const Array &p_arr); + int get_max_message_size() const; + void close(); + + RemoteDebuggerPeerTCP(Ref<StreamPeerTCP> p_stream = Ref<StreamPeerTCP>()); + ~RemoteDebuggerPeerTCP(); +}; + +#endif // REMOTE_DEBUGGER_PEER_H diff --git a/core/debugger/script_debugger.cpp b/core/debugger/script_debugger.cpp new file mode 100644 index 0000000000..935ad01d80 --- /dev/null +++ b/core/debugger/script_debugger.cpp @@ -0,0 +1,123 @@ +/*************************************************************************/ +/* script_debugger.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "script_debugger.h" + +#include "core/debugger/engine_debugger.h" + +void ScriptDebugger::set_lines_left(int p_left) { + + lines_left = p_left; +} + +int ScriptDebugger::get_lines_left() const { + + return lines_left; +} + +void ScriptDebugger::set_depth(int p_depth) { + + depth = p_depth; +} + +int ScriptDebugger::get_depth() const { + + return depth; +} + +void ScriptDebugger::insert_breakpoint(int p_line, const StringName &p_source) { + + if (!breakpoints.has(p_line)) + breakpoints[p_line] = Set<StringName>(); + breakpoints[p_line].insert(p_source); +} + +void ScriptDebugger::remove_breakpoint(int p_line, const StringName &p_source) { + + if (!breakpoints.has(p_line)) + return; + + breakpoints[p_line].erase(p_source); + if (breakpoints[p_line].size() == 0) + breakpoints.erase(p_line); +} +bool ScriptDebugger::is_breakpoint(int p_line, const StringName &p_source) const { + + if (!breakpoints.has(p_line)) + return false; + return breakpoints[p_line].has(p_source); +} +bool ScriptDebugger::is_breakpoint_line(int p_line) const { + + return breakpoints.has(p_line); +} + +String ScriptDebugger::breakpoint_find_source(const String &p_source) const { + + return p_source; +} + +void ScriptDebugger::clear_breakpoints() { + + breakpoints.clear(); +} + +void ScriptDebugger::set_skip_breakpoints(bool p_skip_breakpoints) { + + skip_breakpoints = p_skip_breakpoints; +} + +bool ScriptDebugger::is_skipping_breakpoints() { + + return skip_breakpoints; +} + +void ScriptDebugger::debug(ScriptLanguage *p_lang, bool p_can_continue, bool p_is_error_breakpoint) { + ScriptLanguage *prev = break_lang; + break_lang = p_lang; + EngineDebugger::get_singleton()->debug(p_can_continue, p_is_error_breakpoint); + break_lang = prev; +} + +void ScriptDebugger::send_error(const String &p_func, const String &p_file, int p_line, const String &p_err, const String &p_descr, ErrorHandlerType p_type, const Vector<StackInfo> &p_stack_info) { + // Store stack info, this is ugly, but allows us to separate EngineDebugger and ScriptDebugger. There might be a better way. + error_stack_info.append_array(p_stack_info); + EngineDebugger::get_singleton()->send_error(p_func, p_file, p_line, p_err, p_descr, p_type); + error_stack_info.resize(0); +} + +Vector<ScriptLanguage::StackInfo> ScriptDebugger::get_error_stack_info() const { + return error_stack_info; +} + +ScriptLanguage *ScriptDebugger::get_break_language() const { + + return break_lang; +} diff --git a/core/debugger/script_debugger.h b/core/debugger/script_debugger.h new file mode 100644 index 0000000000..d8ddf353bf --- /dev/null +++ b/core/debugger/script_debugger.h @@ -0,0 +1,80 @@ +/*************************************************************************/ +/* script_debugger.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef SCRIPT_DEBUGGER_H +#define SCRIPT_DEBUGGER_H + +#include "core/map.h" +#include "core/script_language.h" +#include "core/set.h" +#include "core/string_name.h" +#include "core/vector.h" + +class ScriptDebugger { + + typedef ScriptLanguage::StackInfo StackInfo; + + int lines_left = -1; + int depth = -1; + bool skip_breakpoints = false; + + Map<int, Set<StringName> > breakpoints; + + ScriptLanguage *break_lang = NULL; + Vector<StackInfo> error_stack_info; + +public: + void set_lines_left(int p_left); + int get_lines_left() const; + + void set_depth(int p_depth); + int get_depth() const; + + String breakpoint_find_source(const String &p_source) const; + void set_break_language(ScriptLanguage *p_lang) { break_lang = p_lang; } + ScriptLanguage *get_break_language() { return break_lang; } + void set_skip_breakpoints(bool p_skip_breakpoints); + bool is_skipping_breakpoints(); + void insert_breakpoint(int p_line, const StringName &p_source); + void remove_breakpoint(int p_line, const StringName &p_source); + bool is_breakpoint(int p_line, const StringName &p_source) const; + bool is_breakpoint_line(int p_line) const; + void clear_breakpoints(); + const Map<int, Set<StringName> > &get_breakpoints() const { return breakpoints; } + + void debug(ScriptLanguage *p_lang, bool p_can_continue = true, bool p_is_error_breakpoint = false); + ScriptLanguage *get_break_language() const; + + void send_error(const String &p_func, const String &p_file, int p_line, const String &p_err, const String &p_descr, ErrorHandlerType p_type, const Vector<StackInfo> &p_stack_info); + Vector<StackInfo> get_error_stack_info() const; + ScriptDebugger() {} +}; + +#endif |