├── .gitignore ├── screenshot.png ├── register_types.h ├── config.py ├── register_types.cpp ├── client ├── zprofiling_graph_view.h ├── zprofiling_tree_view.h ├── zprofiling_timeline_view.h ├── zprofiling_receive_buffer.h ├── zprofiling_client.h ├── zprofiling_graph_view.cpp ├── zprofiling_tree_view.cpp ├── zprofiling_timeline_view.cpp └── zprofiling_client.cpp ├── SCsub ├── LICENSE.md ├── server ├── zprofiling_send_buffer.h ├── zprofiling_server.h ├── zprofiler.h ├── zprofiler.cpp └── zprofiling_server.cpp └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.obj 2 | *.pyc 3 | *.o 4 | *.autosave 5 | *.bc 6 | *.d 7 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zylann/zprofiler/HEAD/screenshot.png -------------------------------------------------------------------------------- /register_types.h: -------------------------------------------------------------------------------- 1 | void register_zprofiler_types(); 2 | void unregister_zprofiler_types(); 3 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | 2 | def can_build(env, platform): 3 | return True 4 | 5 | 6 | def configure(env): 7 | pass 8 | 9 | 10 | def get_doc_classes(): 11 | return [] 12 | 13 | 14 | def get_doc_path(): 15 | return "doc/classes" 16 | -------------------------------------------------------------------------------- /register_types.cpp: -------------------------------------------------------------------------------- 1 | #include "register_types.h" 2 | #ifdef TOOLS_ENABLED 3 | #include "client/zprofiling_client.h" 4 | #include "server/zprofiling_server.h" 5 | #endif 6 | 7 | void register_zprofiler_types() { 8 | #ifdef TOOLS_ENABLED 9 | ClassDB::register_class(); 10 | ZProfiler::get_thread_profiler().set_thread_name("Main"); 11 | ZProfilingServer::create_singleton(); 12 | #endif 13 | } 14 | 15 | void unregister_zprofiler_types() { 16 | #ifdef TOOLS_ENABLED 17 | ZProfiler::get_thread_profiler().finalize(); 18 | ZProfilingServer::destroy_singleton(); 19 | ZProfiler::terminate(); 20 | #endif 21 | } 22 | -------------------------------------------------------------------------------- /client/zprofiling_graph_view.h: -------------------------------------------------------------------------------- 1 | #ifndef ZPROFILING_GRAPH_VIEW_H 2 | #define ZPROFILING_GRAPH_VIEW_H 3 | 4 | #include 5 | 6 | class ZProfilingClient; 7 | 8 | class ZProfilingGraphView : public Control { 9 | GDCLASS(ZProfilingGraphView, Control) 10 | public: 11 | static const char *SIGNAL_FRAME_CLICKED; 12 | static const char *SIGNAL_MOUSE_WHEEL_MOVED; 13 | 14 | ZProfilingGraphView(); 15 | 16 | void set_client(ZProfilingClient *client); 17 | 18 | private: 19 | void _notification(int p_what); 20 | void _gui_input(Ref p_event); 21 | void _draw(); 22 | 23 | int get_frame_index_from_pixel(int x) const; 24 | 25 | static void _bind_methods(); 26 | 27 | const ZProfilingClient *_client = nullptr; 28 | int _max_frame = 0; 29 | int _hovered_frame = -1; 30 | }; 31 | 32 | #endif // ZPROFILING_GRAPH_VIEW_H 33 | -------------------------------------------------------------------------------- /client/zprofiling_tree_view.h: -------------------------------------------------------------------------------- 1 | #ifndef ZPROFILING_TREE_VIEW_H 2 | #define ZPROFILING_TREE_VIEW_H 3 | 4 | #include 5 | 6 | class Tree; 7 | class ZProfilingClient; 8 | 9 | class ZProfilingTreeView : public Control { 10 | GDCLASS(ZProfilingTreeView, Control) 11 | public: 12 | ZProfilingTreeView(); 13 | 14 | void set_client(const ZProfilingClient *client); 15 | void set_frame_index(int frame_index); 16 | void set_thread_index(int thread_index); 17 | 18 | enum Column { 19 | COLUMN_NAME = 0, 20 | COLUMN_HIT_COUNT, 21 | COLUMN_TOTAL_TIME, 22 | COLUMN_COUNT 23 | }; 24 | 25 | private: 26 | static void _bind_methods(); 27 | 28 | void update_tree(); 29 | void sort_tree(); 30 | 31 | const ZProfilingClient *_client; 32 | Tree *_tree = nullptr; 33 | int _thread_index; 34 | int _frame_index; 35 | }; 36 | 37 | #endif // ZPROFILING_TREE_VIEW_H 38 | -------------------------------------------------------------------------------- /SCsub: -------------------------------------------------------------------------------- 1 | Import('env') 2 | Import('env_modules') 3 | 4 | env_zprofiler = env_modules.Clone() 5 | 6 | files = [ 7 | "*.cpp", 8 | "client/*.cpp", 9 | "server/*.cpp" 10 | ] 11 | 12 | for f in files: 13 | env_zprofiler.add_source_files(env.modules_sources, f) 14 | 15 | # Ignored clang warnings because Godot's codebase is old and isn't using override yet 16 | if env['platform'] == 'osx' or env['platform'] == 'android': 17 | env_zprofiler.Append(CXXFLAGS=['-Wno-inconsistent-missing-override']) 18 | 19 | # Doesn't work, because reasons 20 | #if env.msvc: 21 | # env_zprofiler.Append(CXXFLAGS=['/std:c++17']) 22 | #else: 23 | # env_zprofiler.Append(CXXFLAGS=['-std=c++17']) 24 | 25 | # This also doesn't work, since the rest of Godot doesn't use this, linking fails. 26 | # No safe STL boundary checks for you. 27 | #if env['target'] == 'debug': 28 | # if env.msvc: 29 | # # Enable STL bound checks, Godot's master environment doesn't do it 30 | # env_zprofiler.Append(CXXFLAGS=['/D_DEBUG']) 31 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Profiler for Godot Engine 2 | -------------------------------- 3 | 4 | Copyright (c) 2019 Marc Gilleron 5 | 6 | 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: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | 10 | 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. 11 | 12 | -------------------------------------------------------------------------------- /client/zprofiling_timeline_view.h: -------------------------------------------------------------------------------- 1 | #ifndef ZPROFILING_TIMELINE_VIEW_H 2 | #define ZPROFILING_TIMELINE_VIEW_H 3 | 4 | #include 5 | 6 | class ZProfilingClient; 7 | class InputEvent; 8 | 9 | // Displays profiling scopes in a timeline 10 | class ZProfilingTimelineView : public Control { 11 | GDCLASS(ZProfilingTimelineView, Control) 12 | public: 13 | ZProfilingTimelineView(); 14 | 15 | void set_client(const ZProfilingClient *client); 16 | void set_thread(int thread_index); 17 | void set_frame_index(int frame_index); 18 | 19 | private: 20 | void _notification(int p_what); 21 | void _draw(); 22 | void _gui_input(Ref p_event); 23 | 24 | void add_zoom(float factor, float mouse_x); 25 | void set_view_range(float min_time_us, float max_time_us); 26 | bool try_get_item_at(Vector2 pixel_pos, int &out_lane_index, int &out_item_index) const; 27 | void try_select_item_at(Vector2 pixel_pos); 28 | 29 | static void _bind_methods(); 30 | 31 | const ZProfilingClient *_client = nullptr; 32 | 33 | int _thread_index = 0; 34 | int _frame_index = 0; 35 | double _view_min_time_us = 0; 36 | double _view_max_time_us = 1; 37 | int _selected_item_lane = -1; 38 | int _selected_item_index = -1; 39 | int _selected_item_hit_count = 0; 40 | uint64_t _selected_item_total_us = 0; 41 | }; 42 | 43 | #endif // ZPROFILING_TIMELINE_VIEW_H 44 | -------------------------------------------------------------------------------- /client/zprofiling_receive_buffer.h: -------------------------------------------------------------------------------- 1 | #ifndef ZPROFILING_RECEIVE_BUFFER_H 2 | #define ZPROFILING_RECEIVE_BUFFER_H 3 | 4 | #include 5 | #include 6 | 7 | // Deserialization helper with re-usable memory 8 | class ZProfilingReceiveBuffer { 9 | public: 10 | inline void resize(uint32_t size) { 11 | _data.resize(size); 12 | } 13 | 14 | inline uint8_t *data() { 15 | return _data.data(); 16 | } 17 | 18 | inline uint8_t get_u8() { 19 | CRASH_COND(_position >= _data.size()); 20 | return _data[_position++]; 21 | } 22 | 23 | template 24 | inline T get_t() { 25 | const size_t pos = _position; 26 | _position += sizeof(T); 27 | CRASH_COND(_position > _data.size()); 28 | return *(T *)&_data[pos]; 29 | } 30 | 31 | inline uint16_t get_u16() { 32 | return get_t(); 33 | } 34 | 35 | inline uint32_t get_u32() { 36 | return get_t(); 37 | } 38 | 39 | inline uint64_t get_u64() { 40 | return get_t(); 41 | } 42 | 43 | inline void get_data(uint8_t *dst, uint32_t size) { 44 | const size_t pos = _position; 45 | _position += size; 46 | CRASH_COND(_position > _data.size()); 47 | memcpy(dst, _data.data() + pos, size); 48 | } 49 | 50 | inline void clear() { 51 | _data.clear(); 52 | _position = 0; 53 | } 54 | 55 | inline bool is_end() const { 56 | return _position >= _data.size(); 57 | } 58 | 59 | inline size_t size() const { 60 | return _data.size(); 61 | } 62 | 63 | private: 64 | // Capacity of this vector matters a lot for performance. 65 | std::vector _data; 66 | size_t _position = 0; 67 | }; 68 | 69 | #endif // ZPROFILING_RECEIVE_BUFFER_H 70 | -------------------------------------------------------------------------------- /server/zprofiling_send_buffer.h: -------------------------------------------------------------------------------- 1 | #ifndef ZPROFILING_SEND_BUFFER_H 2 | #define ZPROFILING_SEND_BUFFER_H 3 | 4 | #include 5 | #include 6 | 7 | // Serialization helper with re-usable memory 8 | class ZProfilingSendBuffer { 9 | public: 10 | inline void put_u8(uint8_t v) { 11 | _data.push_back(v); 12 | } 13 | 14 | template 15 | inline void put_t(T v) { 16 | const size_t a = _data.size(); 17 | _data.resize(_data.size() + sizeof(T)); 18 | *(T *)(&_data[a]) = v; 19 | } 20 | 21 | inline void set_u32(uint32_t pos, uint32_t v) { 22 | CRASH_COND(pos + sizeof(uint32_t) > _data.size()); 23 | *(uint32_t *)(&_data[pos]) = v; 24 | } 25 | 26 | inline void put_u16(uint16_t v) { 27 | put_t(v); 28 | } 29 | 30 | inline void put_u32(uint32_t v) { 31 | put_t(v); 32 | } 33 | 34 | inline void put_u64(uint64_t v) { 35 | put_t(v); 36 | } 37 | 38 | inline void put_data(const uint8_t *bytes, uint32_t size) { 39 | const size_t a = _data.size(); 40 | _data.resize(_data.size() + size); 41 | memcpy(&_data[a], bytes, size); 42 | } 43 | 44 | inline void put_utf8_string(const String str) { 45 | const CharString cs = str.utf8(); 46 | put_u32(cs.length()); 47 | put_data((const uint8_t *)cs.get_data(), cs.length()); 48 | } 49 | 50 | inline size_t size() const { 51 | return _data.size(); 52 | } 53 | 54 | inline const uint8_t *data() const { 55 | return _data.data(); 56 | } 57 | 58 | inline void clear() { 59 | _data.clear(); 60 | } 61 | 62 | private: 63 | // Capacity of this vector matters a lot for performance. 64 | std::vector _data; 65 | }; 66 | 67 | #endif // ZPROFILING_SEND_BUFFER_H 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Profiler for Godot 2 | ========================= 3 | 4 | A CPU time instrumenting profiler for Godot Engine. 5 | 6 | ![Client GUI embedded in a game](screenshot.png) 7 | 8 | This allows to add profiling scopes to any part of the C++ codebase in Godot, so that much better insight about what's happening can be provided to users. Besides, scripts can also make use of it thanks to the recent addition of a `StringName`-based API. 9 | 10 | It was primarily used in Voxel Tools C++ codebase as a simple profiling solution, but evolved to become independent. It's still relatively work in progress, but mostly usable. 11 | Old Git history was lost in the migration, but can be found in the `profiler` branch of Voxel Tools. 12 | 13 | 14 | Features 15 | --------------------------- 16 | 17 | - Function and scope sampling in C++ by adding a single macro 18 | - API supports either string literal (fastest) or `StringName` for script-friendly usage 19 | - Microsecond accuracy using Godot's time functions 20 | - Low overhead: effort is made for sampling to be very fast. Goals being: no dictionary lookup, no string copies, no allocations, no mutex locking. Only getting time and blitting the info into an array by index. 21 | - Multithreaded: each thread can be recorded independently 22 | - Client/server architecture: the server records, the client displays. 23 | - Client GUI made with Godot nodes, can be embedded in game or in editor 24 | - Timeline with pan & zoom showing all the measured calls 25 | - Tree view showing call hierarchy and time taken 26 | - Sample categories (so we can distinguish easily when an item is in C++ or a script) 27 | - Starts recording when a connection from the client is made 28 | 29 | 30 | How To Install And Use 31 | ------------------------- 32 | 33 | ZProfiler is a custom C++ module for Godot 3.1+. It must be compiled into the engine to work. 34 | 35 | As is, it doesn't do much apart from recording data if the server gets initialized. To display profiling information, the client must be instanciated and connected to the server. Integrating an instance to the editor is one of the things left to do. 36 | 37 | As a module, it holds a particular place because in theory, any place of the engine should be able to use it, while modules are supposed to be optional independent parts, so I'm not sure if a module is the right place to put this system. Including `modules/zprofiler/server/zprofiler.h` and starting to use `ZPROFILER_SCOPE()` should works anyways, though. 38 | 39 | 40 | Roadmap 41 | --------- 42 | 43 | These are some ideas that may or may not be implemented in the future: 44 | 45 | * Resolving function names in C++ (the preprocessor doesn't know them so they aren't literals) 46 | * Fix issues related to `ThreadLocal` resource management (potential leaks) 47 | * Script API exposition (although it could record automatically by function in GDScript's runtime) 48 | * Save data to files for later inspection (was originally working this way but got removed temporarily) 49 | * True integration with Godot's official profiler, through a PR to the engine 50 | -------------------------------------------------------------------------------- /server/zprofiling_server.h: -------------------------------------------------------------------------------- 1 | #ifndef ZPROFILING_SERVER_H 2 | #define ZPROFILING_SERVER_H 3 | 4 | #include "zprofiler.h" 5 | #include "zprofiling_send_buffer.h" 6 | 7 | #include 8 | #include 9 | // Used for now because Godot 3.x doesn't have a worthy vector for frequently re-used memory 10 | #include 11 | 12 | class Thread; 13 | class TCP_Server; 14 | class StreamPeerTCP; 15 | 16 | // Gathers data logged by thread profilers and sends it to network peers. 17 | class ZProfilingServer { 18 | public: 19 | static const char *DEFAULT_HOST_ADDRESS; 20 | static const uint16_t DEFAULT_HOST_PORT = 13118; 21 | static const uint64_t LOOP_PERIOD_USEC = 20000; 22 | 23 | // Input commands 24 | enum CommandType { 25 | CMD_ENABLE = 0, // Turn on profiling 26 | CMD_DISABLE // Turn off profiling 27 | }; 28 | 29 | // Output messages 30 | enum EventType { 31 | EVENT_FRAME, // Following data is about a whole frame 32 | EVENT_STRING_DEF, // New string ID definition 33 | // TODO Do we need a THREAD_END message? 34 | EVENT_INCOMING_DATA_SIZE // Header for incoming data block 35 | }; 36 | 37 | static void create_singleton(); 38 | static void destroy_singleton(); 39 | 40 | // TODO This was meant to be private. Had to make it public for `memdelete` to work... 41 | ~ZProfilingServer(); 42 | 43 | private: 44 | ZProfilingServer(); 45 | 46 | static void c_thread_func(void *userdata); 47 | 48 | void thread_func(); 49 | void update_server(); 50 | void harvest(); 51 | void serialize_and_send_messages(); 52 | void serialize_and_send_messages(StreamPeerTCP &peer, bool send_all_strings); 53 | void recycle_data(); 54 | void clear(); 55 | uint16_t get_or_create_c_string_id(const char *cs); 56 | uint16_t get_or_create_string_id(String s); 57 | 58 | struct Item { 59 | // POD 60 | // This layout must match what the client expects 61 | uint32_t begin; 62 | uint32_t end; 63 | uint16_t description_id; 64 | uint8_t category; 65 | }; 66 | 67 | struct Frame { 68 | int current_lane = -1; 69 | // Vectors contain re-usable memory which is frequently pushed to 70 | std::array, ZProfiler::MAX_STACK> lanes; 71 | 72 | inline void reset() { 73 | for (size_t i = 0; i < lanes.size(); ++i) { 74 | if (lanes[i].size() == 0) { 75 | break; 76 | } 77 | lanes[i].clear(); 78 | } 79 | current_lane = -1; 80 | } 81 | }; 82 | 83 | Ref _server; 84 | Ref _peer; 85 | bool _peer_just_connected = false; 86 | Thread *_thread = nullptr; 87 | bool _running = false; 88 | Vector _buffers_to_send; 89 | ZProfiler::Buffer *_recycled_buffers = nullptr; 90 | ZProfilingSendBuffer _message; 91 | 92 | // Re-used buffers to pre-process raw events before sending them 93 | HashMap _frame_buffers; 94 | 95 | // Strings are separated in two categories because one incurs higher performance cost 96 | HashMap _static_strings; 97 | HashMap _dynamic_strings; 98 | uint16_t _next_string_id = 0; 99 | }; 100 | 101 | #endif // ZPROFILING_SERVER_H 102 | -------------------------------------------------------------------------------- /client/zprofiling_client.h: -------------------------------------------------------------------------------- 1 | #ifndef ZPROFILING_CLIENT_H 2 | #define ZPROFILING_CLIENT_H 3 | 4 | #include "zprofiling_receive_buffer.h" 5 | #include 6 | #include 7 | #include 8 | 9 | class StreamPeerTCP; 10 | class Button; 11 | class Label; 12 | class SpinBox; 13 | class OptionButton; 14 | class VSplitContainer; 15 | class ZProfilingTimelineView; 16 | class ZProfilingGraphView; 17 | class ZProfilingTreeView; 18 | 19 | class ZProfilingClient : public Control { 20 | GDCLASS(ZProfilingClient, Control) 21 | public: 22 | struct Item { 23 | // POD 24 | // This layout must match what the server sends 25 | uint32_t begin_time_relative = 0; 26 | uint32_t end_time_relative = 0; 27 | uint16_t description_id = 0; 28 | uint8_t category = 0; 29 | }; 30 | 31 | struct Lane { 32 | Vector items; 33 | }; 34 | 35 | struct Frame { 36 | Vector lanes; 37 | uint64_t begin_time = 0; 38 | uint64_t end_time = 0; 39 | }; 40 | 41 | struct ThreadData { 42 | uint16_t id = 0; 43 | // TODO Drop/dump frames that go beyond a fixed time 44 | Vector frames; 45 | int selected_frame = -1; 46 | }; 47 | 48 | ZProfilingClient(); 49 | ~ZProfilingClient(); 50 | 51 | void connect_to_host(); 52 | void disconnect_from_host(); 53 | int get_thread_count() const; 54 | const ThreadData &get_thread_data(int thread_id) const; 55 | const ZProfilingClient::Frame *get_frame(int thread_index, int frame_index) const; 56 | const String &get_indexed_name(uint16_t i) const; 57 | void set_selected_frame(int frame_index); 58 | void set_selected_thread(int thread_index); 59 | int get_selected_thread() const; 60 | 61 | private: 62 | void _notification(int p_what); 63 | void _process(); 64 | 65 | bool process_incoming_data(); 66 | bool process_event_string_def(uint16_t i, String str); 67 | 68 | void _on_connect_button_pressed(); 69 | void _on_frame_spinbox_value_changed(float value); 70 | void _on_thread_selector_item_selected(int idx); 71 | void _on_graph_view_frame_clicked(int frame_index); 72 | void _on_graph_view_mouse_wheel_moved(int delta); 73 | 74 | void reset_connect_button(); 75 | bool try_auto_select_main_thread(); 76 | void disconnect_on_error(String message); 77 | void update_thread_list(); 78 | int get_thread_index_from_id(uint16_t thread_id) const; 79 | void clear_profiling_data(); 80 | void clear_network_states(); 81 | bool has_indexed_name(uint16_t i) const; 82 | 83 | static void _bind_methods(); 84 | 85 | // GUI 86 | Button *_connect_button = nullptr; 87 | Label *_status_label = nullptr; 88 | ZProfilingGraphView *_graph_view = nullptr; 89 | ZProfilingTimelineView *_timeline_view = nullptr; 90 | ZProfilingTreeView *_tree_view = nullptr; 91 | SpinBox *_frame_spinbox = nullptr; 92 | bool _frame_spinbox_ignore_changes = false; 93 | OptionButton *_thread_selector = nullptr; 94 | bool _auto_select_main = true; 95 | 96 | // Data 97 | Vector _threads; 98 | Vector _names; // They are zero-based and not that many, so a vector works 99 | int _selected_thread_index = -1; 100 | 101 | // Network 102 | Ref _peer; 103 | ZProfilingReceiveBuffer _received_data; 104 | int _previous_peer_status = -1; 105 | int _last_received_block_size = -1; 106 | }; 107 | 108 | #endif // ZPROFILING_CLIENT_H 109 | -------------------------------------------------------------------------------- /server/zprofiler.h: -------------------------------------------------------------------------------- 1 | #ifndef ZPROFILER_H 2 | #define ZPROFILER_H 3 | 4 | #define ZPROFILER_ENABLED 5 | 6 | #ifdef ZPROFILER_ENABLED 7 | 8 | #include 9 | #include 10 | 11 | // Helpers 12 | // Macros can be tested with gcc and -E option at https://godbolt.org/ 13 | #define ZPROFILER_STRINGIFY_(a) #a 14 | #define ZPROFILER_STRINGIFY(a) ZPROFILER_STRINGIFY_(a) 15 | #define ZPROFILER_LINE_STR ZPROFILER_STRINGIFY(__LINE__) 16 | #define ZPROFILER_FILE_LINE_STR __FILE__ ": " ZPROFILER_LINE_STR 17 | #define ZPROFILER_COMBINE_NAME_(a, b) a##b 18 | #define ZPROFILER_COMBINE_NAME(a, b) ZPROFILER_COMBINE_NAME_(a, b) 19 | 20 | // C++ usage macros (not required, just convenient). 21 | #define ZPROFILER_BEGIN_NAMED(_key) ZProfiler::get_thread_profiler().begin(_key) 22 | #define ZPROFILER_BEGIN() ZPROFILER_BEGIN_NAMED(ZPROFILER_FILE_LINE_STR) 23 | #define ZPROFILER_END() ZProfiler::get_thread_profiler().end() 24 | #define ZPROFILER_SCOPE_NAMED(_key) ZProfilerScope ZPROFILER_COMBINE_NAME(profiler_scope, __LINE__)(_key) 25 | #define ZPROFILER_SCOPE() ZPROFILER_SCOPE_NAMED(ZPROFILER_FILE_LINE_STR) 26 | 27 | // Profiler for one thread. Main API to record profiling data. 28 | class ZProfiler { 29 | public: 30 | static const uint32_t MAX_STACK = 64; 31 | 32 | enum EventType { 33 | EVENT_PUSH = 0, 34 | EVENT_PUSH_SN, 35 | EVENT_POP, 36 | EVENT_FRAME 37 | }; 38 | 39 | // Having string categories is possible, 40 | // but I figured this is enough for now 41 | enum Category { 42 | CATEGORY_ENGINE = 0, // Default, since all code starts from engine 43 | CATEGORY_SCRIPT, 44 | CATEGORY_COUNT 45 | }; 46 | 47 | // 16 bytes POD, there can be a LOT of these 48 | struct Event { 49 | union { 50 | const char *description; 51 | uint8_t description_sn[sizeof(StringName)]; 52 | uint64_t time; 53 | }; 54 | // Time for events inside a frame is stored as 32-bit relative to frame start, 55 | // because it saves space and should be enough for the durations of a frame. 56 | // Absolute 64-bit time is only used at FRAME events. 57 | uint32_t relative_time; 58 | uint8_t type; 59 | uint8_t category; 60 | }; 61 | 62 | static_assert(sizeof(Event) <= 16, "Event is expected to be no more than 16 bytes"); 63 | 64 | // Events are written into a fixed-size buffer, which has lower overhead. 65 | // When full, or when the frame ends, buffers are posted to a shared location (single, short sync point), 66 | // where they can be picked up by the reader for processing. 67 | struct Buffer { 68 | std::array events; 69 | unsigned int write_index = 0; 70 | String thread_name; 71 | Buffer *prev = nullptr; 72 | 73 | ~Buffer() { 74 | reset(); 75 | } 76 | 77 | inline void reset_no_string_names() { 78 | write_index = 0; 79 | prev = nullptr; 80 | } 81 | 82 | inline void reset() { 83 | // We have to do this JUST because of StringName... 84 | for (int i = 0; i < write_index; ++i) { 85 | if (events[i].type == EVENT_PUSH_SN) { 86 | StringName *sn = (StringName *)events[i].description_sn; 87 | sn->~StringName(); 88 | } 89 | } 90 | 91 | write_index = 0; 92 | prev = nullptr; 93 | } 94 | 95 | inline Buffer *find_last() { 96 | Buffer *last = this; 97 | Buffer *p = prev; 98 | while (p != nullptr) { 99 | last = p; 100 | p = p->prev; 101 | } 102 | return last; 103 | } 104 | 105 | static void delete_recursively(ZProfiler::Buffer *b) { 106 | while (b != nullptr) { 107 | CRASH_COND(b == b->prev); 108 | Buffer *prev = b->prev; 109 | memdelete(b); 110 | b = prev; 111 | } 112 | } 113 | }; 114 | 115 | ZProfiler(); 116 | ~ZProfiler(); 117 | 118 | void set_thread_name(String name); 119 | void begin_sn(StringName description); // For scripts, which can't provide a const char* 120 | void begin_sn(StringName description, uint8_t category); 121 | void begin(const char *description); // For C++, where litterals just work 122 | void begin_category(uint8_t category); 123 | void end_category(); 124 | void end(); 125 | void mark_frame(); 126 | 127 | // Cleanups the profiler of the current thread, it won't record anything ever again. 128 | // Due to thread_local destructors not always getting called (??), 129 | // it is recommended to call this at the end of a thread. 130 | void finalize(); 131 | 132 | // Gets profiler for the current executing thread so events can be logged 133 | static ZProfiler &get_thread_profiler(); 134 | 135 | // Used from a special thread, from ProfilingServer only 136 | static Buffer *harvest(Buffer *recycled_buffers); 137 | static void set_enabled(bool enabled); 138 | 139 | // Declares that all profiling is to be stopped forever. 140 | // This will cleanup global state and any profiler still active will stop posting results. 141 | static void terminate(); 142 | 143 | private: 144 | void push_event(Event e); 145 | void flush(bool acquire_new_buffer); 146 | 147 | Buffer *_buffer = nullptr; 148 | String _profiler_name; 149 | bool _enabled = false; 150 | bool _finalized = false; 151 | uint64_t _frame_begin_time = 0; 152 | std::array _category_stack; 153 | uint32_t _category_stack_pos = 0; 154 | }; 155 | 156 | struct ZProfilerScope { 157 | ZProfilerScope(const char *description) { 158 | ZProfiler::get_thread_profiler().begin(description); 159 | } 160 | ~ZProfilerScope() { 161 | ZProfiler::get_thread_profiler().end(); 162 | } 163 | }; 164 | 165 | struct ZProfilerScopeSN { 166 | ZProfilerScopeSN(StringName description) { 167 | ZProfiler::get_thread_profiler().begin_sn(description); 168 | } 169 | ZProfilerScopeSN(StringName description, uint8_t category) { 170 | ZProfiler::get_thread_profiler().begin_sn(description, category); 171 | } 172 | ~ZProfilerScopeSN() { 173 | ZProfiler::get_thread_profiler().end(); 174 | } 175 | }; 176 | 177 | struct ZProfilerCategoryScope { 178 | ZProfilerCategoryScope(uint8_t category) { 179 | ZProfiler::get_thread_profiler().begin_category(category); 180 | } 181 | ~ZProfilerCategoryScope() { 182 | ZProfiler::get_thread_profiler().end_category(); 183 | } 184 | }; 185 | 186 | #else 187 | 188 | // Empty definitions 189 | #define ZPROFILER_BEGIN_NAMED(_key) // 190 | #define ZPROFILER_BEGIN() // 191 | #define ZPROFILER_END() // 192 | #define ZPROFILER_SCOPE() // 193 | #define ZPROFILER_SCOPE_NAMED(_name) // 194 | 195 | #endif 196 | 197 | #endif // ZPROFILER_H 198 | -------------------------------------------------------------------------------- /client/zprofiling_graph_view.cpp: -------------------------------------------------------------------------------- 1 | #include "zprofiling_graph_view.h" 2 | #include "../server/zprofiler.h" 3 | #include "zprofiling_client.h" 4 | 5 | const int FRAME_WIDTH_PX = 4; 6 | const char *ZProfilingGraphView::SIGNAL_FRAME_CLICKED = "frame_clicked"; 7 | const char *ZProfilingGraphView::SIGNAL_MOUSE_WHEEL_MOVED = "mouse_wheel_moved"; 8 | 9 | ZProfilingGraphView::ZProfilingGraphView() { 10 | } 11 | 12 | void ZProfilingGraphView::set_client(ZProfilingClient *client) { 13 | _client = client; 14 | } 15 | 16 | void ZProfilingGraphView::_notification(int p_what) { 17 | switch (p_what) { 18 | case NOTIFICATION_DRAW: 19 | _draw(); 20 | break; 21 | 22 | case NOTIFICATION_MOUSE_EXIT: 23 | _hovered_frame = -1; 24 | update(); 25 | break; 26 | 27 | default: 28 | break; 29 | } 30 | } 31 | 32 | void ZProfilingGraphView::_gui_input(Ref p_event) { 33 | Ref mb = p_event; 34 | Ref mm = p_event; 35 | 36 | if (mb.is_valid()) { 37 | if (mb->is_pressed()) { 38 | switch (mb->get_button_index()) { 39 | case BUTTON_LEFT: { 40 | const int frame_index = get_frame_index_from_pixel(mb->get_position().x); 41 | emit_signal(SIGNAL_FRAME_CLICKED, frame_index); 42 | } break; 43 | 44 | case BUTTON_WHEEL_DOWN: 45 | emit_signal(SIGNAL_MOUSE_WHEEL_MOVED, -1); 46 | break; 47 | 48 | case BUTTON_WHEEL_UP: 49 | emit_signal(SIGNAL_MOUSE_WHEEL_MOVED, 1); 50 | break; 51 | } 52 | } 53 | } 54 | 55 | if (mm.is_valid()) { 56 | const int hovered_frame = get_frame_index_from_pixel(mm->get_position().x); 57 | if (hovered_frame != _hovered_frame) { 58 | _hovered_frame = hovered_frame; 59 | update(); 60 | } 61 | } 62 | } 63 | 64 | int ZProfilingGraphView::get_frame_index_from_pixel(int x) const { 65 | return _max_frame - (get_rect().size.x - x) / FRAME_WIDTH_PX; 66 | } 67 | 68 | static bool try_get_last_completed_frame_index(const ZProfilingClient::ThreadData &thread_data, int &out_frame_index) { 69 | if (thread_data.frames.size() == 0) { 70 | return false; 71 | } 72 | int i = thread_data.frames.size() - 1; 73 | while (i >= 0 && thread_data.frames[i].end_time == -1) { 74 | --i; 75 | } 76 | if (i < 0) { 77 | return false; 78 | } 79 | out_frame_index = i; 80 | return true; 81 | } 82 | 83 | // TODO Bake this once instead of computing it every time 84 | static int get_frame_time(const ZProfilingClient::Frame &frame) { 85 | if (frame.lanes.size() == 0) { 86 | return 0; 87 | } 88 | int sum = 0; 89 | const ZProfilingClient::Lane &lane = frame.lanes[0]; 90 | for (int i = 0; i < lane.items.size(); ++i) { 91 | const ZProfilingClient::Item &item = lane.items[i]; 92 | sum += item.end_time_relative - item.begin_time_relative; 93 | } 94 | return sum; 95 | } 96 | 97 | void ZProfilingGraphView::_draw() { 98 | ZPROFILER_SCOPE_NAMED(FUNCTION_STR); 99 | 100 | const Color bg_color(0.f, 0.f, 0.f, 0.7f); 101 | const Color engine_curve_color(0.4f, 0.4f, 1.0f); 102 | const Color script_curve_color(0.3f, 0.8f, 0.3f); 103 | const Color text_fg_color(1.f, 1.f, 1.f); 104 | const Color text_bg_color(0.f, 0.f, 0.f); 105 | const Color frame_graduation_color(1.f, 1.f, 1.f, 0.2f); 106 | const Color selected_frame_bg(1.f, 0.f, 0.f, 0.5f); 107 | const Color selected_frame_fg(1.f, 1.f, 1.f); 108 | const Color hovered_frame_fg(1.f, 1.f, 1.f, 0.2f); 109 | 110 | // Background 111 | const Rect2 control_rect = get_rect(); 112 | draw_rect(Rect2(0, 0, control_rect.size.x, control_rect.size.y), bg_color); 113 | 114 | ERR_FAIL_COND(_client == nullptr); 115 | int thread_index = _client->get_selected_thread(); 116 | if (thread_index == -1) { 117 | return; 118 | } 119 | 120 | const ZProfilingClient::ThreadData &thread_data = _client->get_thread_data(thread_index); 121 | 122 | if (!try_get_last_completed_frame_index(thread_data, _max_frame)) { 123 | return; 124 | } 125 | 126 | const int max_visible_frame_count = control_rect.size.x / FRAME_WIDTH_PX; 127 | int min_frame = _max_frame - max_visible_frame_count; 128 | if (min_frame < 0) { 129 | min_frame = 0; 130 | } 131 | 132 | const int min_x = control_rect.size.x - FRAME_WIDTH_PX * (_max_frame - min_frame); 133 | 134 | // Get normalized factor 135 | int max_frame_time = 1; // microseconds 136 | for (int frame_index = min_frame; frame_index < _max_frame; ++frame_index) { 137 | const ZProfilingClient::Frame &frame = thread_data.frames[frame_index]; 138 | const int frame_time = get_frame_time(frame); 139 | if (frame_time > max_frame_time) { 140 | max_frame_time = frame_time; 141 | } 142 | } 143 | 144 | // Add some headroom 145 | //max_frame_time = 10 * max_frame_time / 9; 146 | 147 | // Curve 148 | Rect2 item_rect(min_x, 0, FRAME_WIDTH_PX, 0); 149 | for (int frame_index = min_frame; frame_index < _max_frame; ++frame_index) { 150 | const ZProfilingClient::Frame &frame = thread_data.frames[frame_index]; 151 | const int frame_time = get_frame_time(frame); 152 | item_rect.size.y = control_rect.size.y * (static_cast(frame_time) / max_frame_time); 153 | item_rect.position.y = control_rect.size.y - item_rect.size.y; 154 | 155 | // TODO Show script part 156 | if (frame_index == thread_data.selected_frame) { 157 | draw_rect(Rect2(item_rect.position.x, 0, item_rect.size.x, control_rect.size.y - item_rect.size.y), selected_frame_bg); 158 | draw_rect(item_rect, selected_frame_fg); 159 | } else { 160 | draw_rect(item_rect, engine_curve_color); 161 | if (frame_index == _hovered_frame) { 162 | draw_rect(Rect2(item_rect.position.x, 0, item_rect.size.x, control_rect.size.y), hovered_frame_fg); 163 | } 164 | } 165 | 166 | item_rect.position.x += FRAME_WIDTH_PX; 167 | } 168 | 169 | Ref font = get_font("font"); 170 | ERR_FAIL_COND(font.is_null()); 171 | 172 | // Graduations 173 | 174 | // 16 ms limit 175 | int frame_graduation_y = control_rect.size.y - control_rect.size.y * (static_cast(16000) / max_frame_time); 176 | if (frame_graduation_y > 0) { 177 | draw_line(Vector2(0, frame_graduation_y), Vector2(control_rect.size.x, frame_graduation_y), frame_graduation_color); 178 | } 179 | 180 | // Max ms 181 | const float max_frame_time_ms = static_cast(max_frame_time) / 1000.f; 182 | String max_time_text = String("{0} ms").format(varray(max_frame_time_ms)); 183 | draw_string(font, Vector2(5, 5 + font->get_ascent()), max_time_text, text_bg_color); 184 | draw_string(font, Vector2(4, 4 + font->get_ascent()), max_time_text, text_fg_color); 185 | } 186 | 187 | void ZProfilingGraphView::_bind_methods() { 188 | ClassDB::bind_method("_gui_input", &ZProfilingGraphView::_gui_input); 189 | 190 | ADD_SIGNAL(MethodInfo(SIGNAL_FRAME_CLICKED)); 191 | ADD_SIGNAL(MethodInfo(SIGNAL_MOUSE_WHEEL_MOVED, PropertyInfo(Variant::INT, "delta"))); 192 | } 193 | -------------------------------------------------------------------------------- /client/zprofiling_tree_view.cpp: -------------------------------------------------------------------------------- 1 | #include "zprofiling_tree_view.h" 2 | #include "../server/zprofiling_server.h" 3 | #include "zprofiling_client.h" 4 | 5 | #include 6 | 7 | ZProfilingTreeView::ZProfilingTreeView() { 8 | _tree = memnew(Tree); 9 | _tree->set_anchors_and_margins_preset(PRESET_WIDE); 10 | _tree->set_hide_root(true); 11 | _tree->set_columns(COLUMN_COUNT); 12 | _tree->set_column_titles_visible(true); 13 | _tree->set_column_title(COLUMN_NAME, "Location"); 14 | _tree->set_column_title(COLUMN_HIT_COUNT, "Hits"); 15 | _tree->set_column_title(COLUMN_TOTAL_TIME, "Total ms"); 16 | _tree->set_column_expand(COLUMN_HIT_COUNT, false); 17 | _tree->set_column_expand(COLUMN_TOTAL_TIME, false); 18 | _tree->set_column_min_width(COLUMN_HIT_COUNT, 50); 19 | _tree->set_column_min_width(COLUMN_TOTAL_TIME, 75); 20 | add_child(_tree); 21 | } 22 | 23 | void ZProfilingTreeView::set_client(const ZProfilingClient *client) { 24 | _client = client; 25 | } 26 | 27 | void ZProfilingTreeView::set_frame_index(int frame_index) { 28 | if (_frame_index != frame_index) { 29 | _frame_index = frame_index; 30 | update_tree(); 31 | } 32 | } 33 | 34 | void ZProfilingTreeView::set_thread_index(int thread_index) { 35 | if (_thread_index != thread_index) { 36 | _thread_index = thread_index; 37 | update_tree(); 38 | } 39 | } 40 | 41 | static void process_item( 42 | const ZProfilingClient::Item &item, 43 | const ZProfilingClient::Frame *frame, int lane_index, 44 | const ZProfilingClient *client, Tree *tree, TreeItem *parent_tree_item, 45 | std::array &lane_tails) { 46 | 47 | // Search for existing item 48 | TreeItem *tree_item = parent_tree_item->get_children(); 49 | while (tree_item != nullptr) { 50 | const uint16_t string_id = tree_item->get_metadata(ZProfilingTreeView::COLUMN_NAME); 51 | if (string_id == item.description_id) { 52 | break; 53 | } 54 | tree_item = tree_item->get_next(); 55 | } 56 | 57 | if (tree_item != nullptr) { 58 | // First occurrence 59 | int hit_count = tree_item->get_metadata(ZProfilingTreeView::COLUMN_HIT_COUNT); 60 | int total_time = tree_item->get_metadata(ZProfilingTreeView::COLUMN_TOTAL_TIME); 61 | 62 | ++hit_count; 63 | total_time += item.end_time_relative - item.begin_time_relative; 64 | 65 | tree_item->set_metadata(ZProfilingTreeView::COLUMN_HIT_COUNT, hit_count); 66 | tree_item->set_metadata(ZProfilingTreeView::COLUMN_TOTAL_TIME, total_time); 67 | 68 | } else { 69 | // More occurrences 70 | tree_item = tree->create_item(parent_tree_item); 71 | 72 | String text = client->get_indexed_name(item.description_id); 73 | if (text.length() > 32) { 74 | tree_item->set_tooltip(ZProfilingTreeView::COLUMN_NAME, text); 75 | text = String("...") + text.right(text.length() - 32); 76 | } 77 | tree_item->set_text(ZProfilingTreeView::COLUMN_NAME, text); 78 | 79 | tree_item->set_metadata(ZProfilingTreeView::COLUMN_NAME, item.description_id); 80 | tree_item->set_metadata(ZProfilingTreeView::COLUMN_HIT_COUNT, 1); 81 | tree_item->set_metadata(ZProfilingTreeView::COLUMN_TOTAL_TIME, item.end_time_relative - item.begin_time_relative); 82 | } 83 | 84 | // Process children in the same time range as the parent 85 | int next_lane_index = lane_index + 1; 86 | if (next_lane_index < frame->lanes.size()) { 87 | int child_item_index = lane_tails[next_lane_index]; 88 | const ZProfilingClient::Lane &next_lane = frame->lanes[next_lane_index]; 89 | 90 | while (child_item_index < next_lane.items.size()) { 91 | const ZProfilingClient::Item &child_item = next_lane.items[child_item_index]; 92 | CRASH_COND(child_item.begin_time_relative < item.begin_time_relative); 93 | if (child_item.begin_time_relative > item.end_time_relative) { 94 | break; 95 | } 96 | process_item(child_item, frame, next_lane_index, client, tree, tree_item, lane_tails); 97 | ++child_item_index; 98 | } 99 | 100 | // Remember last child index so we'll start from there when processing the next item in our lane 101 | lane_tails[next_lane_index] = child_item_index; 102 | } 103 | } 104 | 105 | static void update_tree_text(TreeItem *tree_item) { 106 | const int hit_count = tree_item->get_metadata(ZProfilingTreeView::COLUMN_HIT_COUNT); 107 | const int total_time = tree_item->get_metadata(ZProfilingTreeView::COLUMN_TOTAL_TIME); 108 | 109 | tree_item->set_text(ZProfilingTreeView::COLUMN_HIT_COUNT, String::num_int64(hit_count)); 110 | tree_item->set_text(ZProfilingTreeView::COLUMN_TOTAL_TIME, String::num_real((float)total_time / 1000.0)); 111 | 112 | TreeItem *child = tree_item->get_children(); 113 | while (child != nullptr) { 114 | update_tree_text(child); 115 | child = child->get_next(); 116 | } 117 | } 118 | 119 | template 120 | static void sort_tree(TreeItem *parent_tree_item) { 121 | Vector items; 122 | 123 | TreeItem *item = parent_tree_item->get_children(); 124 | while (item != nullptr) { 125 | // Sort recursively 126 | sort_tree(item); 127 | 128 | items.push_back(item); 129 | item = item->get_next(); 130 | } 131 | 132 | items.sort_custom(); 133 | 134 | for (int i = items.size() - 1; i >= 0; --i) { 135 | // Moving to top is less expensive 136 | items[i]->move_to_top(); 137 | } 138 | } 139 | 140 | void ZProfilingTreeView::update_tree() { 141 | ERR_FAIL_COND(_client == nullptr); 142 | const ZProfilingClient::Frame *frame = _client->get_frame(_thread_index, _frame_index); 143 | if (frame == nullptr) { 144 | return; 145 | } 146 | 147 | _tree->clear(); 148 | TreeItem *tree_root = _tree->create_item(); 149 | 150 | if (frame->lanes.size() == 0) { 151 | return; 152 | } 153 | 154 | std::array lane_tails; 155 | for (int i = 0; i < lane_tails.size(); ++i) { 156 | lane_tails[i] = 0; 157 | } 158 | 159 | int lane_index = 0; 160 | const ZProfilingClient::Lane &lane = frame->lanes[lane_index]; 161 | 162 | for (int item_index = 0; item_index < lane.items.size(); ++item_index) { 163 | const ZProfilingClient::Item &item = lane.items[item_index]; 164 | process_item(item, frame, lane_index, _client, _tree, tree_root, lane_tails); 165 | } 166 | 167 | TreeItem *child = tree_root->get_children(); 168 | while (child != nullptr) { 169 | update_tree_text(child); 170 | child = child->get_next(); 171 | } 172 | 173 | sort_tree(); 174 | } 175 | 176 | void ZProfilingTreeView::sort_tree() { 177 | struct TimeComparator { 178 | bool operator()(const TreeItem *a, const TreeItem *b) const { 179 | const int a_total = a->get_metadata(ZProfilingTreeView::COLUMN_TOTAL_TIME); 180 | const int b_total = b->get_metadata(ZProfilingTreeView::COLUMN_TOTAL_TIME); 181 | return a_total > b_total; 182 | } 183 | }; 184 | 185 | // TODO Different sorters 186 | ::sort_tree(_tree->get_root()); 187 | } 188 | 189 | void ZProfilingTreeView::_bind_methods() { 190 | } 191 | -------------------------------------------------------------------------------- /server/zprofiler.cpp: -------------------------------------------------------------------------------- 1 | #include "zprofiler.h" 2 | 3 | #ifdef ZPROFILER_ENABLED 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | namespace { 11 | 12 | thread_local ZProfiler g_profiler; 13 | 14 | ZProfiler::Buffer *g_shared_buffer_pool = nullptr; 15 | ZProfiler::Buffer *g_shared_output = nullptr; 16 | std::mutex g_shared_mutex; // TODO Locking is extremely short. Could be a spinlock? 17 | bool g_enabled = false; // TODO Use atomic? 18 | bool g_finalized = false; 19 | 20 | } // namespace 21 | 22 | void ZProfiler::terminate() { 23 | g_shared_mutex.lock(); 24 | 25 | g_finalized = true; 26 | 27 | printf("Freeing global profiling data\n"); 28 | 29 | if (g_shared_buffer_pool != nullptr) { 30 | Buffer::delete_recursively(g_shared_buffer_pool); 31 | g_shared_buffer_pool = nullptr; 32 | } 33 | 34 | if (g_shared_output != nullptr) { 35 | Buffer::delete_recursively(g_shared_output); 36 | g_shared_output = nullptr; 37 | } 38 | 39 | g_shared_mutex.unlock(); 40 | 41 | g_enabled = false; 42 | // No profiling operations must ever run after this 43 | } 44 | 45 | ZProfiler::Buffer *ZProfiler::harvest(Buffer *recycled_buffers) { 46 | CRASH_COND(g_finalized); // No collection should happen when engine is quitting 47 | ZProfiler::Buffer *out; 48 | 49 | g_shared_mutex.lock(); 50 | 51 | if (g_shared_output != nullptr) { 52 | CRASH_COND(g_shared_output == g_shared_buffer_pool); 53 | } 54 | 55 | out = g_shared_output; 56 | g_shared_output = nullptr; 57 | 58 | if (recycled_buffers != nullptr) { 59 | CRASH_COND(g_shared_buffer_pool == recycled_buffers); 60 | // A <-- B <-- C <-- [pool] <<< E <-- F <-- G <-- [recycled] 61 | recycled_buffers->find_last()->prev = g_shared_buffer_pool; 62 | g_shared_buffer_pool = recycled_buffers; 63 | } 64 | 65 | g_shared_mutex.unlock(); 66 | 67 | return out; 68 | } 69 | 70 | void ZProfiler::set_enabled(bool enabled) { 71 | CRASH_COND(enabled && g_finalized); 72 | g_enabled = enabled; 73 | } 74 | 75 | ZProfiler &ZProfiler::get_thread_profiler() { 76 | return g_profiler; 77 | } 78 | 79 | inline uint64_t get_time() { 80 | return OS::get_singleton()->get_ticks_usec(); 81 | } 82 | 83 | ZProfiler::ZProfiler() { 84 | _enabled = false; 85 | _profiler_name = String::num_uint64(Thread::get_caller_id()); 86 | _category_stack[0] = CATEGORY_ENGINE; 87 | } 88 | 89 | ZProfiler::~ZProfiler() { 90 | // Thread has stopped 91 | //flush(false); 92 | 93 | if (_buffer != nullptr) { 94 | memdelete(_buffer); 95 | _buffer = nullptr; 96 | } 97 | 98 | // On Windows, I noticed destructors of a thread_local object were not always called. 99 | // Like, about 14 instances get created, but only 1 destructor was called. 100 | // And it might be intentional? I didn't get answers yet :( 101 | // https://developercommunity.visualstudio.com/content/problem/798234/thread-local.html 102 | // This is very annoying because each profiler buffers events in their own 103 | // data storage before they publish it, to reduce overhead. 104 | // That would have been fine, if there was no StringNames involved. 105 | // So I try not to rely on destructor to free it, or global data. 106 | // Instead, it must be freed explicitely when a profiled thread ends, or when the engine quits. 107 | // Otherwise, it is possible to leak StringNames because this destructor 108 | // doesn't get called. 109 | } 110 | 111 | void ZProfiler::finalize() { 112 | printf("Finalizing profiler\n"); 113 | CRASH_COND(_finalized); // Don't call twice 114 | _finalized = true; 115 | _enabled = false; 116 | if (_buffer != nullptr) { 117 | memdelete(_buffer); 118 | _buffer = nullptr; 119 | } 120 | } 121 | 122 | void ZProfiler::set_thread_name(String name) { 123 | _profiler_name = String::num_uint64(Thread::get_caller_id()); 124 | _profiler_name += "_"; 125 | _profiler_name += name; 126 | 127 | if (_buffer != nullptr) { 128 | _buffer->thread_name = _profiler_name; 129 | } 130 | } 131 | 132 | void ZProfiler::begin_sn(StringName description) { 133 | begin_sn(description, _category_stack[_category_stack_pos]); 134 | } 135 | 136 | void ZProfiler::begin_sn(StringName description, uint8_t category) { 137 | if (!_enabled) { 138 | return; 139 | } 140 | Event e; 141 | e.type = EVENT_PUSH_SN; 142 | // StringName does ref-counting with global mutex locking, 143 | // which adds overhead and is incompatible with the POD nature of event buffers, which uses a union. 144 | // So we have to freeze it inside a byte array. 145 | memnew_placement((StringName *)e.description_sn, StringName); 146 | *(StringName *)e.description_sn = description; 147 | e.category = category; 148 | e.relative_time = get_time() - _frame_begin_time; 149 | push_event(e); 150 | } 151 | 152 | void ZProfiler::begin(const char *description) { 153 | if (!_enabled) { 154 | return; 155 | } 156 | Event e; 157 | e.type = EVENT_PUSH; 158 | e.description = description; 159 | e.category = _category_stack[_category_stack_pos]; 160 | e.relative_time = get_time() - _frame_begin_time; 161 | push_event(e); 162 | } 163 | 164 | void ZProfiler::end() { 165 | if (!_enabled) { 166 | return; 167 | } 168 | Event e; 169 | e.type = EVENT_POP; 170 | e.description = nullptr; 171 | e.relative_time = get_time() - _frame_begin_time; 172 | push_event(e); 173 | } 174 | 175 | // Every samples taken inside this scope will have the specified category, 176 | // unless overriden with another category call 177 | void ZProfiler::begin_category(uint8_t category) { 178 | if (!_enabled) { 179 | return; 180 | } 181 | ERR_FAIL_COND(_category_stack_pos + 1 == _category_stack.size()); 182 | ++_category_stack_pos; 183 | _category_stack[_category_stack_pos] = category; 184 | } 185 | 186 | void ZProfiler::end_category() { 187 | if (!_enabled) { 188 | return; 189 | } 190 | ERR_FAIL_COND(_category_stack_pos == 0); 191 | --_category_stack_pos; 192 | } 193 | 194 | void ZProfiler::mark_frame() { 195 | if (_finalized) { 196 | return; 197 | } 198 | 199 | // A thread profiler only changes its enabled state at the frame mark 200 | if (_enabled != g_enabled) { 201 | _enabled = !_enabled; 202 | 203 | if (_enabled == false) { 204 | // Got disabled 205 | flush(false); 206 | return; 207 | 208 | } else { 209 | // Got enabled 210 | if (_buffer != nullptr) { 211 | CRASH_COND(_buffer->write_index > 0); 212 | } else { 213 | _buffer = memnew(Buffer); 214 | _buffer->thread_name = _profiler_name; 215 | } 216 | } 217 | } else { 218 | if (!_enabled) { 219 | return; 220 | } 221 | } 222 | 223 | // We need to flush periodically here, 224 | // otherwise we would have to wait until the buffer is full 225 | flush(true); 226 | 227 | // TODO Enforce all preceding events were popped 228 | Event e; 229 | e.description = nullptr; 230 | e.type = EVENT_FRAME; 231 | e.time = get_time(); 232 | _frame_begin_time = e.time; 233 | push_event(e); 234 | } 235 | 236 | void ZProfiler::push_event(Event e) { 237 | CRASH_COND(_buffer == nullptr); 238 | CRASH_COND(_buffer->write_index >= _buffer->events.size()); 239 | 240 | _buffer->events[_buffer->write_index] = e; 241 | ++_buffer->write_index; 242 | 243 | if (_buffer->write_index == _buffer->events.size()) { 244 | //printf("ZProfiler end of capacity\n"); 245 | flush(true); 246 | } 247 | } 248 | 249 | void ZProfiler::flush(bool acquire_new_buffer) { 250 | if (_buffer == nullptr || _buffer->write_index == 0) { 251 | // Nothing to flush 252 | return; 253 | } 254 | 255 | if (g_finalized) { 256 | // Engine is quitting, don't do anything 257 | return; 258 | } 259 | 260 | g_shared_mutex.lock(); 261 | 262 | // Post buffer 263 | if (_buffer != nullptr) { 264 | CRASH_COND(g_shared_output == _buffer); 265 | _buffer->prev = g_shared_output; 266 | g_shared_output = _buffer; 267 | _buffer = nullptr; 268 | } 269 | 270 | if (acquire_new_buffer) { 271 | // Get new buffer 272 | 273 | if (g_shared_buffer_pool != nullptr) { 274 | _buffer = g_shared_buffer_pool; 275 | CRASH_COND(g_shared_buffer_pool == g_shared_buffer_pool->prev); 276 | g_shared_buffer_pool = g_shared_buffer_pool->prev; 277 | 278 | g_shared_mutex.unlock(); 279 | 280 | } else { 281 | g_shared_mutex.unlock(); 282 | 283 | _buffer = memnew(Buffer); 284 | } 285 | 286 | _buffer->reset(); 287 | _buffer->thread_name = _profiler_name; 288 | 289 | } else { 290 | g_shared_mutex.unlock(); 291 | 292 | _buffer = nullptr; 293 | } 294 | } 295 | 296 | #endif // ZPROFILER_ENABLED 297 | -------------------------------------------------------------------------------- /server/zprofiling_server.cpp: -------------------------------------------------------------------------------- 1 | #include "zprofiling_server.h" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | const char *ZProfilingServer::DEFAULT_HOST_ADDRESS = "127.0.0.1"; 10 | 11 | namespace { 12 | ZProfilingServer *g_profiling_server = nullptr; 13 | } 14 | 15 | void ZProfilingServer::create_singleton() { 16 | CRASH_COND(g_profiling_server != nullptr); 17 | g_profiling_server = memnew(ZProfilingServer); 18 | } 19 | 20 | void ZProfilingServer::destroy_singleton() { 21 | CRASH_COND(g_profiling_server == nullptr); 22 | memdelete(g_profiling_server); 23 | } 24 | 25 | ZProfilingServer::ZProfilingServer() { 26 | //printf("Creating profiling server singleton\n"); 27 | _running = true; 28 | _thread = Thread::create(c_thread_func, this); 29 | } 30 | 31 | ZProfilingServer::~ZProfilingServer() { 32 | //printf("Destroying profiling server singleton\n"); 33 | _running = false; 34 | Thread::wait_to_finish(_thread); 35 | memdelete(_thread); 36 | //printf("Destroyed profiling server singleton\n"); 37 | } 38 | 39 | void ZProfilingServer::c_thread_func(void *userdata) { 40 | ZProfilingServer *h = (ZProfilingServer *)userdata; 41 | h->thread_func(); 42 | } 43 | 44 | void ZProfilingServer::thread_func() { 45 | ZProfiler::get_thread_profiler().set_thread_name("ProfilingServer"); 46 | 47 | //printf("Profiling server thread started\n"); 48 | 49 | _server.instance(); 50 | // Only listen to localhost 51 | const Error err = _server->listen(DEFAULT_HOST_PORT, IP_Address(DEFAULT_HOST_ADDRESS)); 52 | CRASH_COND(err != OK); 53 | 54 | while (_running) { 55 | OS::get_singleton()->delay_usec(LOOP_PERIOD_USEC); 56 | 57 | ZProfiler::get_thread_profiler().mark_frame(); 58 | ZPROFILER_SCOPE_NAMED(FUNCTION_STR); 59 | 60 | // TODO The data should be dropped past some amount, otherwise it will saturate memory and bandwidth 61 | 62 | update_server(); 63 | harvest(); 64 | serialize_and_send_messages(); 65 | recycle_data(); 66 | } 67 | 68 | clear(); 69 | 70 | _peer.unref(); 71 | _server->stop(); 72 | _server.unref(); 73 | 74 | //printf("Profiling server stopped\n"); 75 | ZProfiler::get_thread_profiler().finalize(); 76 | } 77 | 78 | void ZProfilingServer::update_server() { 79 | ZPROFILER_SCOPE_NAMED(FUNCTION_STR); 80 | 81 | if (_peer.is_null() && _server->is_connection_available()) { 82 | _peer = _server->take_connection(); 83 | _peer_just_connected = true; 84 | printf("Peer connected to profiler\n"); 85 | } 86 | 87 | if (_peer.is_null()) { 88 | return; 89 | } 90 | 91 | StreamPeerTCP &peer = **_peer; 92 | const StreamPeerTCP::Status peer_status = peer.get_status(); 93 | 94 | switch (peer_status) { 95 | case StreamPeerTCP::STATUS_ERROR: 96 | case StreamPeerTCP::STATUS_NONE: 97 | printf("Peer disconnected from profiler\n"); 98 | _peer.unref(); 99 | printf("Last peer disconnected, disabling profiler\n"); 100 | ZProfiler::set_enabled(false); 101 | break; 102 | 103 | case StreamPeerTCP::STATUS_CONNECTING: 104 | break; 105 | 106 | case StreamPeerTCP::STATUS_CONNECTED: { 107 | const size_t available_bytes = peer.get_available_bytes(); 108 | 109 | if (available_bytes == 1) { 110 | const uint8_t command = peer.get_8(); 111 | 112 | switch (command) { 113 | case CMD_ENABLE: 114 | printf("Enabling profiler\n"); 115 | ZProfiler::set_enabled(true); 116 | break; 117 | 118 | case CMD_DISABLE: 119 | printf("Disabling profiler\n"); 120 | ZProfiler::set_enabled(false); 121 | break; 122 | 123 | default: 124 | CRASH_NOW_MSG("Unhandled command ID"); 125 | break; 126 | } 127 | } 128 | } break; 129 | 130 | default: 131 | CRASH_NOW_MSG("Unhandled status"); 132 | break; 133 | } 134 | } 135 | 136 | void ZProfilingServer::harvest() { 137 | // Gather last recorded data. 138 | // This function does as few work as possible to minimize synchronization overhead, 139 | // so we'll have to process the data a little bit before sending it. 140 | ZProfiler::Buffer *harvested_buffers = ZProfiler::harvest(_recycled_buffers); 141 | _recycled_buffers = nullptr; 142 | 143 | ZProfiler::Buffer *buffer = harvested_buffers; 144 | while (buffer != nullptr) { 145 | _buffers_to_send.push_back(buffer); 146 | CRASH_COND(buffer->prev == buffer); 147 | ZProfiler::Buffer *prev = buffer->prev; 148 | buffer->prev = nullptr; 149 | buffer = prev; 150 | } 151 | } 152 | 153 | void ZProfilingServer::serialize_and_send_messages() { 154 | if (_peer.is_valid()) { 155 | serialize_and_send_messages(**_peer, _peer_just_connected); 156 | _peer_just_connected = false; 157 | } 158 | } 159 | 160 | template 161 | inline void serialize_string_def(Peer_T &peer, uint16_t id, String str) { 162 | //print_line(String("Sending string def {0}: {1}").format(varray(id, str))); 163 | peer.put_u8(ZProfilingServer::EVENT_STRING_DEF); 164 | peer.put_u16(id); 165 | peer.put_utf8_string(str); 166 | } 167 | 168 | uint16_t ZProfilingServer::get_or_create_c_string_id(const char *cs) { 169 | const uint16_t *id_ptr = _static_strings.getptr(cs); 170 | if (id_ptr != nullptr) { 171 | return *id_ptr; 172 | 173 | } else { 174 | const uint16_t id = _next_string_id++; 175 | _static_strings.set(cs, id); 176 | serialize_string_def(_message, id, cs); 177 | return id; 178 | } 179 | } 180 | 181 | uint16_t ZProfilingServer::get_or_create_string_id(String s) { 182 | const uint16_t *id_ptr = _dynamic_strings.getptr(s); 183 | if (id_ptr != nullptr) { 184 | return *id_ptr; 185 | 186 | } else { 187 | const uint16_t id = _next_string_id++; 188 | _dynamic_strings.set(s, id); 189 | serialize_string_def(_message, id, s); 190 | return id; 191 | } 192 | } 193 | 194 | void ZProfilingServer::serialize_and_send_messages(StreamPeerTCP &peer, bool send_all_strings) { 195 | ZPROFILER_SCOPE_NAMED(FUNCTION_STR); 196 | 197 | // An intermediary buffer is used instead of `StreamPeerTCP` directly, 198 | // because using the latter slowed it down horribly. 199 | // Turning on Nagle's algorithm didn't help... 200 | _message.clear(); 201 | 202 | _message.put_u8(EVENT_INCOMING_DATA_SIZE); 203 | const size_t incoming_data_size_index = _message.size(); 204 | _message.put_u32(0); 205 | const size_t begin_index = _message.size(); 206 | 207 | if (send_all_strings) { 208 | // New clients need to get all strings they missed 209 | { 210 | const String *key = nullptr; 211 | while ((key = _dynamic_strings.next(key))) { 212 | serialize_string_def(_message, _dynamic_strings.get(*key), *key); 213 | } 214 | } 215 | { 216 | const char *const *key = nullptr; 217 | while ((key = _static_strings.next(key))) { 218 | serialize_string_def(_message, _static_strings.get(*key), *key); 219 | } 220 | } 221 | } 222 | 223 | // Simple event stream. 224 | // Iterate buffers in reverse because that's how buffers got harvested 225 | for (int i = _buffers_to_send.size() - 1; i >= 0; --i) { 226 | ZProfiler::Buffer *buffer = _buffers_to_send[i]; 227 | const uint16_t thread_name_id = get_or_create_string_id(buffer->thread_name); 228 | 229 | // Get frame buffer corresponding to the thread 230 | Frame *frame_buffer; 231 | Frame **frame_buffer_ptr = _frame_buffers.getptr(thread_name_id); 232 | if (frame_buffer_ptr == nullptr) { 233 | frame_buffer = memnew(Frame); 234 | _frame_buffers.set(thread_name_id, frame_buffer); 235 | } else { 236 | frame_buffer = *frame_buffer_ptr; 237 | } 238 | 239 | for (size_t j = 0; j < buffer->write_index; ++j) { 240 | const ZProfiler::Event &event = buffer->events[j]; 241 | 242 | switch (event.type) { 243 | case ZProfiler::EVENT_PUSH: { 244 | const uint16_t name_id = get_or_create_c_string_id(event.description); 245 | ERR_FAIL_COND_MSG(frame_buffer->current_lane + 1 >= frame_buffer->lanes.size(), "Stack too deep"); 246 | ++frame_buffer->current_lane; 247 | frame_buffer->lanes[frame_buffer->current_lane].push_back(Item{ event.relative_time, 0, name_id, event.category }); 248 | } break; 249 | 250 | case ZProfiler::EVENT_PUSH_SN: { 251 | StringName &sn = *(StringName *)event.description_sn; 252 | const uint16_t name_id = get_or_create_string_id(sn); 253 | ERR_FAIL_COND_MSG(frame_buffer->current_lane + 1 >= frame_buffer->lanes.size(), "Stack too deep"); 254 | ++frame_buffer->current_lane; 255 | frame_buffer->lanes[frame_buffer->current_lane].push_back(Item{ event.relative_time, 0, name_id, event.category }); 256 | sn.~StringName(); 257 | } break; 258 | 259 | case ZProfiler::EVENT_POP: 260 | ERR_FAIL_COND_MSG(frame_buffer->current_lane == -1, "Profiler was popped too many times"); 261 | frame_buffer->lanes[frame_buffer->current_lane].back().end = event.relative_time; 262 | --frame_buffer->current_lane; 263 | break; 264 | 265 | case ZProfiler::EVENT_FRAME: 266 | // Last frame is now finalized, serialize it 267 | 268 | _message.put_u8(EVENT_FRAME); 269 | _message.put_u16(thread_name_id); 270 | _message.put_u64(event.time); // This is the *end* time of the frame (and beginning of next one) 271 | 272 | for (size_t i = 0; i < frame_buffer->lanes.size(); ++i) { 273 | std::vector &items = frame_buffer->lanes[i]; 274 | _message.put_u32(items.size()); 275 | if (items.size() == 0) { 276 | break; 277 | } 278 | _message.put_data((const uint8_t *)items.data(), items.size() * sizeof(Item)); 279 | } 280 | 281 | frame_buffer->reset(); 282 | break; 283 | 284 | default: 285 | CRASH_NOW_MSG("Unhandled event type"); 286 | break; 287 | } 288 | } 289 | 290 | // Optimized way to reset since we took care of freeing StringNames 291 | buffer->reset_no_string_names(); 292 | } 293 | 294 | // Write block data size back at the beginning 295 | _message.set_u32(incoming_data_size_index, _message.size() - begin_index); 296 | 297 | { 298 | // Send in one block 299 | ZPROFILER_SCOPE_NAMED("ZProfilingServer::serialize_and_send_messages put_data"); 300 | //print_line(String("Sending {0} bytes").format(varray(_message.size()))); 301 | Error err = peer.put_data(_message.data(), _message.size()); 302 | if (err != OK) { 303 | // The peer can have disconnected in the meantime 304 | ERR_PRINT("Failed to send profiling data"); 305 | } 306 | } 307 | 308 | _message.clear(); 309 | } 310 | 311 | void ZProfilingServer::recycle_data() { 312 | // Recycle buffers 313 | for (size_t i = 0; i < _buffers_to_send.size(); ++i) { 314 | ZProfiler::Buffer *buffer = _buffers_to_send[i]; 315 | CRASH_COND(buffer == _recycled_buffers); 316 | buffer->prev = _recycled_buffers; 317 | _recycled_buffers = buffer; 318 | } 319 | 320 | _buffers_to_send.clear(); 321 | } 322 | 323 | void ZProfilingServer::clear() { 324 | printf("Clearing data from ZProfilingServer\n"); 325 | // Clear all data we were about to send 326 | for (size_t i = 0; i < _buffers_to_send.size(); ++i) { 327 | ZProfiler::Buffer *buffer = _buffers_to_send[i]; 328 | CRASH_COND(buffer->prev != nullptr); // These buffers aren't linked 329 | memdelete(buffer); 330 | } 331 | 332 | // Clear all data we were about to recycle 333 | if (_recycled_buffers != nullptr) { 334 | ZProfiler::Buffer::delete_recursively(_recycled_buffers); 335 | _recycled_buffers = nullptr; 336 | } 337 | 338 | // Clear frame buffers 339 | const uint16_t *key = nullptr; 340 | while ((key = _frame_buffers.next(key))) { 341 | Frame *f = _frame_buffers.get(*key); 342 | memdelete(f); 343 | } 344 | _frame_buffers.clear(); 345 | 346 | _dynamic_strings.clear(); 347 | _static_strings.clear(); 348 | _buffers_to_send.clear(); 349 | _next_string_id = 0; 350 | } 351 | -------------------------------------------------------------------------------- /client/zprofiling_timeline_view.cpp: -------------------------------------------------------------------------------- 1 | #include "zprofiling_timeline_view.h" 2 | #include "../server/zprofiler.h" 3 | #include "zprofiling_client.h" 4 | 5 | const double ZOOM_FACTOR = 1.25; 6 | const double MINIMUM_TIME_WIDTH_US = 1.0; 7 | const int LANE_HEIGHT = 20; 8 | const int LANE_SEPARATION = 1; 9 | 10 | static const ZProfilingClient::Frame *get_frame(const ZProfilingClient *client, int thread_index, int frame_index) { 11 | ERR_FAIL_COND_V(client == nullptr, nullptr); 12 | return client->get_frame(thread_index, frame_index); 13 | } 14 | 15 | ZProfilingTimelineView::ZProfilingTimelineView() { 16 | set_clip_contents(true); 17 | } 18 | 19 | void ZProfilingTimelineView::set_client(const ZProfilingClient *client) { 20 | _client = client; 21 | } 22 | 23 | void ZProfilingTimelineView::set_thread(int thread_index) { 24 | if (_thread_index != thread_index) { 25 | _thread_index = thread_index; 26 | const ZProfilingClient::Frame *frame = get_frame(_client, _thread_index, _frame_index); 27 | if (frame != nullptr) { 28 | set_view_range(0, frame->end_time - frame->begin_time); 29 | } 30 | update(); 31 | } 32 | } 33 | 34 | void ZProfilingTimelineView::set_frame_index(int frame_index) { 35 | if (_frame_index != frame_index) { 36 | _frame_index = frame_index; 37 | const ZProfilingClient::Frame *frame = get_frame(_client, _thread_index, _frame_index); 38 | if (frame != nullptr) { 39 | set_view_range(0, frame->end_time - frame->begin_time); 40 | } 41 | update(); 42 | } 43 | } 44 | 45 | void ZProfilingTimelineView::_notification(int p_what) { 46 | switch (p_what) { 47 | case NOTIFICATION_DRAW: 48 | _draw(); 49 | break; 50 | 51 | default: 52 | break; 53 | } 54 | } 55 | 56 | void ZProfilingTimelineView::_gui_input(Ref p_event) { 57 | const ZProfilingClient::Frame *frame = get_frame(_client, _thread_index, _frame_index); 58 | if (frame == nullptr) { 59 | return; 60 | } 61 | 62 | Ref mm = p_event; 63 | Ref mb = p_event; 64 | 65 | if (mb.is_valid()) { 66 | if (mb->is_pressed()) { 67 | switch (mb->get_button_index()) { 68 | case BUTTON_WHEEL_UP: 69 | add_zoom(1.f / ZOOM_FACTOR, mb->get_position().x); 70 | break; 71 | 72 | case BUTTON_WHEEL_DOWN: 73 | add_zoom(ZOOM_FACTOR, mb->get_position().x); 74 | break; 75 | 76 | case BUTTON_LEFT: 77 | try_select_item_at(mb->get_position()); 78 | break; 79 | 80 | default: 81 | break; 82 | } 83 | } 84 | } 85 | 86 | if (mm.is_valid()) { 87 | if ((mm->get_button_mask() & BUTTON_MASK_MIDDLE) != 0) { 88 | const int time_width = _view_max_time_us - _view_min_time_us; 89 | const float microseconds_per_pixel = time_width / get_rect().size.x; 90 | double offset = -mm->get_relative().x * microseconds_per_pixel; 91 | 92 | const uint64_t frame_duration = frame->end_time - frame->begin_time; 93 | 94 | if (offset > 0) { 95 | if (_view_max_time_us + offset > frame_duration) { 96 | offset = frame_duration - _view_max_time_us; 97 | } 98 | } else { 99 | if (_view_min_time_us + offset < 0) { 100 | offset = -_view_min_time_us; 101 | } 102 | } 103 | 104 | set_view_range(_view_min_time_us + offset, _view_max_time_us + offset); 105 | update(); 106 | } 107 | } 108 | } 109 | 110 | void ZProfilingTimelineView::add_zoom(float factor, float mouse_x) { 111 | double time_offset = _view_min_time_us; 112 | double time_width = _view_max_time_us - _view_min_time_us; 113 | 114 | const double d = mouse_x / get_rect().size.x; 115 | const double prev_width = time_width; 116 | time_width = time_width * factor; 117 | time_offset -= d * (time_width - prev_width); 118 | 119 | if (time_width < MINIMUM_TIME_WIDTH_US) { 120 | time_width = MINIMUM_TIME_WIDTH_US; 121 | } 122 | 123 | set_view_range(time_offset, time_offset + time_width); 124 | } 125 | 126 | void ZProfilingTimelineView::set_view_range(float min_time_us, float max_time_us) { 127 | const ZProfilingClient::Frame *frame = get_frame(_client, _thread_index, _frame_index); 128 | ERR_FAIL_COND(frame == nullptr); 129 | const uint64_t frame_duration_us = frame->end_time - frame->begin_time; 130 | 131 | // Clamp 132 | if (min_time_us < 0) { 133 | min_time_us = 0; 134 | } 135 | if (max_time_us > frame_duration_us) { 136 | max_time_us = frame_duration_us; 137 | } 138 | 139 | if (min_time_us == _view_min_time_us && max_time_us == _view_max_time_us) { 140 | return; 141 | } 142 | 143 | _view_min_time_us = min_time_us; 144 | _view_max_time_us = max_time_us; 145 | 146 | update(); 147 | } 148 | 149 | static int get_hit_count(const ZProfilingClient::Frame &frame, uint16_t item_name_id, uint64_t &out_us_total) { 150 | int count = 0; 151 | uint64_t us_total = 0; 152 | 153 | for (int i = 0; i < frame.lanes.size(); ++i) { 154 | const ZProfilingClient::Lane &lane = frame.lanes[i]; 155 | 156 | for (int j = 0; j < lane.items.size(); ++j) { 157 | const ZProfilingClient::Item &item = lane.items[j]; 158 | 159 | if (item.description_id == item_name_id) { 160 | ++count; 161 | us_total += item.end_time_relative - item.begin_time_relative; 162 | } 163 | } 164 | } 165 | 166 | out_us_total = us_total; 167 | return count; 168 | } 169 | 170 | void ZProfilingTimelineView::try_select_item_at(Vector2 pixel_pos) { 171 | int lane_index = -1; 172 | int item_index = -1; 173 | 174 | try_get_item_at(pixel_pos, lane_index, item_index); 175 | 176 | if (lane_index != _selected_item_lane || item_index != _selected_item_index) { 177 | _selected_item_lane = lane_index; 178 | _selected_item_index = item_index; 179 | 180 | if (_selected_item_index != -1) { 181 | const ZProfilingClient::Frame *frame = get_frame(_client, _thread_index, _frame_index); 182 | ERR_FAIL_COND(frame == nullptr); 183 | 184 | const ZProfilingClient::Lane &lane = frame->lanes[_selected_item_lane]; 185 | const ZProfilingClient::Item &item = lane.items[_selected_item_index]; 186 | 187 | _selected_item_hit_count = get_hit_count(*frame, item.description_id, _selected_item_total_us); 188 | } 189 | 190 | update(); 191 | } 192 | } 193 | 194 | bool ZProfilingTimelineView::try_get_item_at(Vector2 pixel_pos, int &out_lane_index, int &out_item_index) const { 195 | const ZProfilingClient::Frame *frame = get_frame(_client, _thread_index, _frame_index); 196 | ERR_FAIL_COND_V(frame == nullptr, false); 197 | 198 | int lane_index = pixel_pos.y / (LANE_HEIGHT + LANE_SEPARATION); 199 | if (lane_index < 0 || lane_index >= frame->lanes.size()) { 200 | return false; 201 | } 202 | 203 | const Rect2 control_rect = get_rect(); 204 | const float frame_view_duration_us = _view_max_time_us - _view_min_time_us; 205 | const float frame_view_duration_us_inv = 1.f / frame_view_duration_us; 206 | const int view_begin = (int)Math::floor(_view_min_time_us); 207 | const int view_end = (int)Math::ceil(_view_max_time_us); 208 | 209 | const ZProfilingClient::Lane &lane = frame->lanes[lane_index]; 210 | 211 | for (int i = 0; i < lane.items.size(); ++i) { 212 | const ZProfilingClient::Item &item = lane.items[i]; 213 | 214 | if (item.end_time_relative < view_begin) { 215 | continue; 216 | } 217 | if (item.begin_time_relative > view_end) { 218 | break; 219 | } 220 | 221 | const float item_x_begin = control_rect.size.x * 222 | (static_cast(item.begin_time_relative) - _view_min_time_us) * 223 | frame_view_duration_us_inv; 224 | 225 | const float item_x_end = control_rect.size.x * 226 | (static_cast(item.end_time_relative) - _view_min_time_us) * 227 | frame_view_duration_us_inv; 228 | 229 | if (pixel_pos.x >= item_x_begin && pixel_pos.x < item_x_end) { 230 | out_lane_index = lane_index; 231 | out_item_index = i; 232 | return true; 233 | } 234 | } 235 | 236 | return false; 237 | } 238 | 239 | static void draw_shaded_text(CanvasItem *ci, Ref font, Vector2 pos, String text, Color fg, Color bg) { 240 | ci->draw_string(font, pos + Vector2(1, 1), text, bg); 241 | ci->draw_string(font, pos, text, fg); 242 | } 243 | 244 | static String get_left_ellipsed_text(String text, int max_width, Ref font, Vector2 &out_string_size) { 245 | if (text.length() == 0) { 246 | out_string_size = Vector2(0, font->get_height()); 247 | return text; 248 | } 249 | 250 | float w = 0; 251 | const String ellipsis = "..."; 252 | const float ellipsis_width = font->get_string_size(ellipsis).x; 253 | 254 | // Iterate characters from right to left 255 | int i = text.size() - 1; 256 | for (; i >= 0; --i) { 257 | w += font->get_char_size(text[i], text[i + 1]).width; 258 | 259 | if (w > max_width) { 260 | break; 261 | } 262 | } 263 | 264 | if (i >= 0) { 265 | // Ellipsis is needed 266 | w += ellipsis_width; 267 | 268 | // Iterate forward until it fits with the ellipsis 269 | for (; i < text.size(); ++i) { 270 | w -= font->get_char_size(text[i], text[i + 1]).width; 271 | 272 | if (w <= max_width) { 273 | break; 274 | } 275 | } 276 | 277 | text = ellipsis + text.right(i); 278 | } 279 | 280 | out_string_size = Vector2(w, font->get_height()); 281 | return text; 282 | } 283 | 284 | // Non-shitty version 285 | static void draw_rect_outline(CanvasItem *ci, Rect2 rect, Color color, int thickness) { 286 | // Horizontal 287 | ci->draw_rect(Rect2(rect.position.x, rect.position.y, /* */ rect.size.x, thickness), color); 288 | ci->draw_rect(Rect2(rect.position.x, rect.position.y + rect.size.y - thickness, rect.size.x, thickness), color); 289 | // Vertical 290 | ci->draw_rect(Rect2(rect.position.x, /* */ rect.position.y + thickness, thickness, rect.size.y - 2 * thickness), color); 291 | ci->draw_rect(Rect2(rect.position.x + rect.size.x - thickness, rect.position.y + thickness, thickness, rect.size.y - 2 * thickness), color); 292 | } 293 | 294 | void ZProfilingTimelineView::_draw() { 295 | ZPROFILER_SCOPE(); 296 | 297 | const Color engine_item_color(0.5f, 0.5f, 1.0f); 298 | const Color script_item_color(0.4f, 0.7f, 0.4f); 299 | const Color item_selected_outline_color(1, 1, 1, 0.5); 300 | const Color bg_color(0.f, 0.f, 0.f, 0.7f); 301 | const Color item_text_color(0.f, 0.f, 0.f); 302 | const Color text_fg_color(1.f, 1.f, 1.f); 303 | const Color text_bg_color(0.f, 0.f, 0.f); 304 | const Color tooltip_bg_color(0, 0, 0, 0.8); 305 | const Color tooltip_text_color(1, 1, 1); 306 | const Vector2 text_margin(4, 4); 307 | const int lane_height = LANE_HEIGHT; 308 | const int lane_separation = LANE_SEPARATION; 309 | 310 | // Background 311 | const Rect2 control_rect = get_rect(); 312 | draw_rect(Rect2(0, 0, control_rect.size.x, control_rect.size.y), bg_color); 313 | 314 | Ref font = get_font("font"); 315 | ERR_FAIL_COND(font.is_null()); 316 | 317 | const ZProfilingClient::Frame *frame = get_frame(_client, _thread_index, _frame_index); 318 | if (frame == nullptr) { 319 | return; 320 | } 321 | 322 | Rect2 item_rect; 323 | item_rect.position.y = lane_separation; 324 | item_rect.size.y = lane_height; 325 | 326 | const float frame_view_duration_us = _view_max_time_us - _view_min_time_us; 327 | const float frame_view_duration_us_inv = 1.f / frame_view_duration_us; 328 | 329 | Rect2 selected_item_rect; 330 | bool show_selected_item = false; 331 | 332 | // Lanes and items 333 | for (int lane_index = 0; lane_index < frame->lanes.size(); ++lane_index) { 334 | const ZProfilingClient::Lane &lane = frame->lanes[lane_index]; 335 | 336 | for (int item_index = 0; item_index < lane.items.size(); ++item_index) { 337 | const ZProfilingClient::Item &item = lane.items[item_index]; 338 | 339 | item_rect.position.x = control_rect.size.x * 340 | (static_cast(item.begin_time_relative) - _view_min_time_us) * 341 | frame_view_duration_us_inv; 342 | 343 | item_rect.size.x = control_rect.size.x * 344 | (static_cast(item.end_time_relative) - item.begin_time_relative) * 345 | frame_view_duration_us_inv; 346 | 347 | if (item_rect.position.x + item_rect.size.x < 0) { 348 | // Out of view 349 | continue; 350 | } 351 | if (item_rect.position.x > control_rect.size.x) { 352 | // Out of view, next items also will 353 | break; 354 | } 355 | 356 | if (item_rect.size.x < 1.01f) { 357 | // Clamp item size so it doesn't disappear 358 | item_rect.size.x = 1.01f; 359 | 360 | } else if (item_rect.size.x > 1.f) { 361 | // Give a small gap to see glued items distinctly 362 | item_rect.size.x -= 1.f; 363 | } 364 | 365 | switch (item.category) { 366 | case ZProfiler::CATEGORY_ENGINE: 367 | draw_rect(item_rect, engine_item_color); 368 | break; 369 | case ZProfiler::CATEGORY_SCRIPT: 370 | draw_rect(item_rect, script_item_color); 371 | break; 372 | default: 373 | ERR_FAIL_MSG("Unknown category, memory corrupted?"); 374 | break; 375 | } 376 | 377 | if (_selected_item_lane == lane_index && _selected_item_index == item_index) { 378 | selected_item_rect = item_rect; 379 | show_selected_item = true; 380 | } 381 | 382 | if (item_rect.size.x > 100) { 383 | String text = _client->get_indexed_name(item.description_id); 384 | 385 | int clamped_item_width = item_rect.size.x; 386 | if (item_rect.position.x < 0) { 387 | clamped_item_width += item_rect.position.x; 388 | } 389 | 390 | const int text_area_width = clamped_item_width - 2 * text_margin.x; 391 | Vector2 text_size; 392 | text = get_left_ellipsed_text(text, text_area_width, font, text_size); 393 | 394 | Vector2 text_pos( 395 | item_rect.position.x + text_margin.x, 396 | item_rect.position.y + text_margin.y + font->get_ascent()); 397 | 398 | // If the item starts off-screen, clamp it to the left 399 | if (text_pos.x < text_margin.x) { 400 | text_pos.x = text_margin.x; 401 | } 402 | 403 | draw_string(font, text_pos.floor(), text, item_text_color, item_rect.size.x); 404 | } 405 | } 406 | 407 | if (show_selected_item) { 408 | draw_rect_outline(this, selected_item_rect, item_selected_outline_color, 2); 409 | 410 | const ZProfilingClient::Lane &lane = frame->lanes[_selected_item_lane]; 411 | const ZProfilingClient::Item &item = lane.items[_selected_item_index]; 412 | 413 | const float self_ms = (item.end_time_relative - item.begin_time_relative) / 1000.0; 414 | const float total_ms = _selected_item_total_us / 1000.0; 415 | 416 | String text1 = _client->get_indexed_name(item.description_id); 417 | String text2 = String::num_real(self_ms); 418 | text2 += " ms | "; 419 | text2 += String::num_int64(_selected_item_hit_count); 420 | if (_selected_item_hit_count == 1) { 421 | text2 += " hit"; 422 | } else { 423 | text2 += " hits | Total: "; 424 | text2 += String::num_real(total_ms); 425 | text2 += " ms"; 426 | } 427 | 428 | const Vector2 text1_size = font->get_string_size(text1); 429 | const Vector2 text2_size = font->get_string_size(text2); 430 | const Vector2 text_size(MAX(text1_size.x, text2_size.x), 2 * font->get_height()); 431 | 432 | const Vector2 bg_size = text_size + text_margin * 2; 433 | const Vector2 bg_pos( 434 | selected_item_rect.position.x + (selected_item_rect.size.x - bg_size.x) * 0.5f, 435 | selected_item_rect.position.y + selected_item_rect.size.y); 436 | 437 | const Vector2 text_pos = (bg_pos + text_margin + Vector2(0, font->get_ascent())).floor(); 438 | 439 | draw_rect(Rect2(bg_pos, bg_size), tooltip_bg_color); 440 | 441 | draw_string(font, text_pos, text1, tooltip_text_color); 442 | draw_string(font, text_pos + Vector2(0, font->get_height()), text2, tooltip_text_color); 443 | } 444 | 445 | item_rect.position.y += lane_height + lane_separation; 446 | } 447 | 448 | // Time graduations 449 | 450 | const float min_time_ms = _view_min_time_us / 1000.0; 451 | const float max_time_ms = _view_max_time_us / 1000.0; 452 | 453 | const String begin_time_text = String("{0} ms").format(varray(min_time_ms)); 454 | const String end_time_text = String("{0} ms").format(varray(max_time_ms)); 455 | 456 | const Vector2 min_text_pos(4, control_rect.size.y - font->get_height() + font->get_ascent()); 457 | const Vector2 max_text_size = font->get_string_size(end_time_text); 458 | const Vector2 max_text_pos(control_rect.size.x - 4 - max_text_size.x, control_rect.size.y - font->get_height() + font->get_ascent()); 459 | 460 | draw_shaded_text(this, font, min_text_pos, begin_time_text, text_fg_color, text_bg_color); 461 | draw_shaded_text(this, font, max_text_pos, end_time_text, text_fg_color, text_bg_color); 462 | } 463 | 464 | void ZProfilingTimelineView::_bind_methods() { 465 | ClassDB::bind_method("_gui_input", &ZProfilingTimelineView::_gui_input); 466 | } 467 | -------------------------------------------------------------------------------- /client/zprofiling_client.cpp: -------------------------------------------------------------------------------- 1 | #include "zprofiling_client.h" 2 | #include "../server/zprofiling_server.h" 3 | #include "zprofiling_graph_view.h" 4 | #include "zprofiling_timeline_view.h" 5 | #include "zprofiling_tree_view.h" 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | const uint32_t MAX_LANES = 256; 16 | const uint32_t MAX_STRING_SIZE = 1000; 17 | const int MAX_TIME_READING_EVENTS_MSEC = 32; 18 | 19 | inline void set_margins(Control *c, int left, int top, int right, int bottom) { 20 | c->set_margin(MARGIN_LEFT, left); 21 | c->set_margin(MARGIN_RIGHT, right); 22 | c->set_margin(MARGIN_TOP, top); 23 | c->set_margin(MARGIN_BOTTOM, bottom); 24 | } 25 | 26 | ZProfilingClient::ZProfilingClient() { 27 | VBoxContainer *main_vb = memnew(VBoxContainer); 28 | main_vb->set_anchors_and_margins_preset(Control::PRESET_WIDE); 29 | ::set_margins(main_vb, 4, 4, -4, -4); 30 | add_child(main_vb); 31 | 32 | HBoxContainer *header_hb = memnew(HBoxContainer); 33 | main_vb->add_child(header_hb); 34 | 35 | _connect_button = memnew(Button); 36 | _connect_button->set_text(TTR("Connect")); 37 | _connect_button->connect("pressed", this, "_on_connect_button_pressed"); 38 | header_hb->add_child(_connect_button); 39 | 40 | Control *spacer = memnew(Control); 41 | spacer->set_h_size_flags(Control::SIZE_EXPAND_FILL); 42 | header_hb->add_child(spacer); 43 | 44 | Label *frame_label = memnew(Label); 45 | frame_label->set_text(TTR("Frame: ")); 46 | header_hb->add_child(frame_label); 47 | 48 | _frame_spinbox = memnew(SpinBox); 49 | _frame_spinbox->set_min(0); 50 | _frame_spinbox->set_step(1); 51 | _frame_spinbox->connect("value_changed", this, "_on_frame_spinbox_value_changed"); 52 | header_hb->add_child(_frame_spinbox); 53 | 54 | Label *thread_label = memnew(Label); 55 | thread_label->set_text(TTR("Thread: ")); 56 | header_hb->add_child(thread_label); 57 | 58 | _thread_selector = memnew(OptionButton); 59 | _thread_selector->connect("item_selected", this, "_on_thread_selector_item_selected"); 60 | header_hb->add_child(_thread_selector); 61 | 62 | HSplitContainer *h_split_container = memnew(HSplitContainer); 63 | h_split_container->set_v_size_flags(Control::SIZE_EXPAND_FILL); 64 | h_split_container->set_split_offset(100); 65 | main_vb->add_child(h_split_container); 66 | 67 | _tree_view = memnew(ZProfilingTreeView); 68 | _tree_view->set_client(this); 69 | h_split_container->add_child(_tree_view); 70 | 71 | VSplitContainer *v_split_container = memnew(VSplitContainer); 72 | v_split_container->set_split_offset(30); 73 | h_split_container->add_child(v_split_container); 74 | 75 | _graph_view = memnew(ZProfilingGraphView); 76 | _graph_view->set_client(this); 77 | _graph_view->connect(ZProfilingGraphView::SIGNAL_FRAME_CLICKED, this, "_on_graph_view_frame_clicked"); 78 | _graph_view->connect(ZProfilingGraphView::SIGNAL_MOUSE_WHEEL_MOVED, this, "_on_graph_view_mouse_wheel_moved"); 79 | v_split_container->add_child(_graph_view); 80 | 81 | _timeline_view = memnew(ZProfilingTimelineView); 82 | _timeline_view->set_client(this); 83 | v_split_container->add_child(_timeline_view); 84 | 85 | HBoxContainer *footer_hb = memnew(HBoxContainer); 86 | main_vb->add_child(footer_hb); 87 | 88 | _status_label = memnew(Label); 89 | _status_label->set_text("---"); 90 | footer_hb->add_child(_status_label); 91 | 92 | _peer.instance(); 93 | 94 | set_process(true); 95 | } 96 | 97 | ZProfilingClient::~ZProfilingClient() { 98 | } 99 | 100 | void ZProfilingClient::_notification(int p_what) { 101 | switch (p_what) { 102 | case NOTIFICATION_PROCESS: 103 | _process(); 104 | break; 105 | 106 | default: 107 | break; 108 | } 109 | } 110 | 111 | void ZProfilingClient::_process() { 112 | ZPROFILER_SCOPE_NAMED(FUNCTION_STR); 113 | 114 | if (_peer.is_null()) { 115 | return; 116 | } 117 | 118 | const StreamPeerTCP::Status peer_status = _peer->get_status(); 119 | 120 | if (peer_status != _previous_peer_status) { 121 | _previous_peer_status = peer_status; 122 | 123 | switch (peer_status) { 124 | case StreamPeerTCP::STATUS_CONNECTED: 125 | _status_label->set_text(TTR("Status: connected")); 126 | _connect_button->set_disabled(false); 127 | _connect_button->set_text("Disconnect"); 128 | _peer->put_8(ZProfilingServer::CMD_ENABLE); 129 | break; 130 | 131 | case StreamPeerTCP::STATUS_CONNECTING: 132 | _status_label->set_text(TTR("Status: connecting...")); 133 | break; 134 | 135 | case StreamPeerTCP::STATUS_ERROR: 136 | _status_label->set_text(TTR("Status: error")); 137 | reset_connect_button(); 138 | _auto_select_main = true; 139 | break; 140 | 141 | case StreamPeerTCP::STATUS_NONE: 142 | _status_label->set_text(TTR("Status: disconnected")); 143 | reset_connect_button(); 144 | _auto_select_main = true; 145 | break; 146 | 147 | default: 148 | CRASH_NOW_MSG("Unhandled status"); 149 | break; 150 | } 151 | } 152 | 153 | if (peer_status == StreamPeerTCP::STATUS_CONNECTED) { 154 | uint64_t time_before = OS::get_singleton()->get_ticks_msec(); 155 | while (process_incoming_data()) { 156 | if (OS::get_singleton()->get_ticks_msec() - time_before > MAX_TIME_READING_EVENTS_MSEC) { 157 | print_line("Spent too long processing incoming data"); 158 | break; 159 | } 160 | } 161 | //print_line(String("Remaining data: {0}").format(varray(_peer->get_available_bytes()))); 162 | } 163 | } 164 | 165 | // Returns true as long as it should be called again 166 | bool ZProfilingClient::process_incoming_data() { 167 | ZPROFILER_SCOPE_NAMED(FUNCTION_STR); 168 | CRASH_COND(_peer.is_null()); 169 | StreamPeerTCP &peer = **_peer; 170 | 171 | if (_last_received_block_size == -1) { 172 | // Waiting for next block header 173 | const int available_bytes = peer.get_available_bytes(); 174 | 175 | if (available_bytes < 5) { 176 | // Data not arrived yet 177 | return false; 178 | } else { 179 | const uint8_t event_type = peer.get_u8(); 180 | if (event_type != ZProfilingServer::EVENT_INCOMING_DATA_SIZE) { 181 | disconnect_on_error(String("Expected incoming data block header, got {0}") 182 | .format(varray(event_type))); 183 | return false; 184 | } 185 | _last_received_block_size = peer.get_u32(); 186 | return true; 187 | } 188 | } 189 | 190 | if (_received_data.size() < _last_received_block_size) { 191 | // Block is incomplete 192 | 193 | const int remaining_bytes = _last_received_block_size - _received_data.size(); 194 | const int available_bytes = peer.get_available_bytes(); 195 | const int available_bytes_for_block = MIN(available_bytes, remaining_bytes); 196 | 197 | if (available_bytes_for_block == 0) { 198 | // Data not arrived yet 199 | return false; 200 | } else { 201 | size_t start_index = _received_data.size(); 202 | _received_data.resize(_received_data.size() + available_bytes_for_block); 203 | const Error err = peer.get_data(_received_data.data() + start_index, available_bytes_for_block); 204 | if (err != OK) { 205 | disconnect_on_error(String("Could not get data size {0} for block, error {1}") 206 | .format(varray(_last_received_block_size, err))); 207 | return false; 208 | } 209 | // We should continue only when the received data is complete 210 | return _received_data.size() == _last_received_block_size; 211 | } 212 | } 213 | 214 | // We have a complete block of data, read it 215 | while (!_received_data.is_end()) { 216 | const uint8_t event_type = _received_data.get_u8(); 217 | 218 | switch (event_type) { 219 | case ZProfilingServer::EVENT_FRAME: { 220 | const uint16_t thread_name_id = _received_data.get_u16(); 221 | const uint64_t frame_end_time = _received_data.get_u64(); 222 | 223 | if (!has_indexed_name(thread_name_id)) { 224 | disconnect_on_error(String("Received Thread event with non-registered name {0}") 225 | .format(varray(thread_name_id))); 226 | return false; 227 | } 228 | 229 | // Get thread 230 | const int existing_thread_index = get_thread_index_from_id(thread_name_id); 231 | int thread_index = -1; 232 | 233 | if (existing_thread_index == -1) { 234 | ThreadData thread_data; 235 | thread_data.id = thread_name_id; 236 | thread_index = _threads.size(); 237 | _threads.push_back(thread_data); 238 | update_thread_list(); 239 | 240 | if (_auto_select_main) { 241 | _auto_select_main = !try_auto_select_main_thread(); 242 | } 243 | 244 | } else { 245 | thread_index = existing_thread_index; 246 | } 247 | 248 | ThreadData &thread_data = _threads.write[thread_index]; 249 | 250 | if (thread_data.frames.size() == 0) { 251 | // Initial frame, normally with no events inside, 252 | // as frame markers must be at the beginning 253 | thread_data.frames.push_back(Frame()); 254 | Frame &initial_frame = thread_data.frames.write[0]; 255 | initial_frame.end_time = frame_end_time; 256 | } 257 | 258 | _frame_spinbox->set_max(thread_data.frames.size() - 1); 259 | _graph_view->update(); 260 | 261 | // Finalize frame 262 | Frame &frame = thread_data.frames.write[thread_data.frames.size() - 1]; 263 | frame.end_time = frame_end_time; 264 | 265 | for (size_t i = 0; i < ZProfiler::MAX_STACK; ++i) { 266 | const uint32_t item_count = _received_data.get_u32(); 267 | if (item_count == 0) { 268 | break; 269 | } 270 | Lane lane; 271 | lane.items.resize(item_count); 272 | _received_data.get_data((uint8_t *)lane.items.ptrw(), item_count * sizeof(Item)); 273 | frame.lanes.push_back(lane); 274 | } 275 | 276 | // Start next frame 277 | Frame next_frame; 278 | next_frame.begin_time = frame_end_time; 279 | thread_data.frames.push_back(next_frame); 280 | } break; 281 | 282 | case ZProfilingServer::EVENT_STRING_DEF: { 283 | const uint16_t string_id = _received_data.get_u16(); 284 | const uint16_t string_size = _received_data.get_u32(); 285 | 286 | Vector data; 287 | data.resize(string_size); 288 | _received_data.get_data(data.ptrw(), data.size()); 289 | 290 | String str; 291 | if (str.parse_utf8((const char *)data.ptr(), data.size())) { 292 | disconnect_on_error("Error when parsing UTF8"); 293 | return false; 294 | } 295 | 296 | if (!process_event_string_def(string_id, str)) { 297 | return false; 298 | } 299 | } break; 300 | 301 | case ZProfilingServer::EVENT_INCOMING_DATA_SIZE: 302 | disconnect_on_error("Received unexpected incoming data size header"); 303 | return false; 304 | break; 305 | 306 | default: 307 | disconnect_on_error(String("Received unknown event {0}").format(varray(event_type))); 308 | break; 309 | } 310 | } 311 | 312 | // Done reading that block 313 | _received_data.clear(); 314 | _last_received_block_size = -1; 315 | return true; 316 | } 317 | 318 | bool ZProfilingClient::process_event_string_def(uint16_t i, String str) { 319 | // New string definition 320 | 321 | if (has_indexed_name(i)) { 322 | print_line(String("WARNING: already received string {0}: previous was `{1}`, newly received is `{2}`") 323 | .format(varray(i, _names[i], str))); 324 | } else { 325 | print_line(String("Registering string {0}: {1}").format(varray(i, str))); 326 | if (i >= _names.size()) { 327 | _names.resize(i + 1); 328 | } 329 | } 330 | 331 | _names.write[i] = str; 332 | return true; 333 | } 334 | 335 | int ZProfilingClient::get_thread_index_from_id(uint16_t thread_id) const { 336 | for (int i = 0; i < _threads.size(); ++i) { 337 | const ThreadData &tdata = _threads[i]; 338 | if (tdata.id == thread_id) { 339 | return i; 340 | } 341 | } 342 | return -1; 343 | } 344 | 345 | int ZProfilingClient::get_thread_count() const { 346 | return _threads.size(); 347 | } 348 | 349 | const ZProfilingClient::ThreadData &ZProfilingClient::get_thread_data(int thread_id) const { 350 | return _threads[thread_id]; 351 | } 352 | 353 | const ZProfilingClient::Frame *ZProfilingClient::get_frame(int thread_index, int frame_index) const { 354 | if (thread_index >= get_thread_count()) { 355 | return nullptr; 356 | } 357 | const ZProfilingClient::ThreadData &thread_data = get_thread_data(thread_index); 358 | if (frame_index < 0 || frame_index >= thread_data.frames.size()) { 359 | return nullptr; 360 | } 361 | const ZProfilingClient::Frame &frame = thread_data.frames[frame_index]; 362 | if (frame.end_time == -1) { 363 | // Frame is not finalized 364 | return nullptr; 365 | } 366 | return &frame; 367 | } 368 | 369 | const String &ZProfilingClient::get_indexed_name(uint16_t i) const { 370 | return _names[i]; 371 | } 372 | 373 | bool ZProfilingClient::has_indexed_name(uint16_t i) const { 374 | return i < _names.size() && _names[i] != ""; 375 | } 376 | 377 | void ZProfilingClient::connect_to_host() { 378 | clear_profiling_data(); 379 | 380 | _peer->connect_to_host(IP_Address(ZProfilingServer::DEFAULT_HOST_ADDRESS), ZProfilingServer::DEFAULT_HOST_PORT); 381 | _connect_button->set_disabled(true); 382 | } 383 | 384 | void ZProfilingClient::disconnect_from_host() { 385 | _peer->disconnect_from_host(); 386 | reset_connect_button(); 387 | _auto_select_main = false; 388 | clear_network_states(); 389 | } 390 | 391 | void ZProfilingClient::clear_network_states() { 392 | _previous_peer_status = -1; 393 | _last_received_block_size = -1; 394 | _received_data.clear(); 395 | } 396 | 397 | void ZProfilingClient::clear_profiling_data() { 398 | _threads.clear(); 399 | 400 | _names.clear(); 401 | _selected_thread_index = -1; 402 | 403 | update_thread_list(); 404 | _timeline_view->update(); 405 | 406 | _frame_spinbox_ignore_changes = true; 407 | _frame_spinbox->set_value(0); 408 | _frame_spinbox->set_max(0); 409 | _frame_spinbox_ignore_changes = false; 410 | } 411 | 412 | void ZProfilingClient::_on_connect_button_pressed() { 413 | if (_peer->get_status() == StreamPeerTCP::STATUS_CONNECTED) { 414 | disconnect_from_host(); 415 | } else { 416 | connect_to_host(); 417 | } 418 | } 419 | 420 | void ZProfilingClient::_on_frame_spinbox_value_changed(float value) { 421 | if (_frame_spinbox_ignore_changes) { 422 | return; 423 | } 424 | set_selected_frame((int)value); 425 | } 426 | 427 | void ZProfilingClient::_on_thread_selector_item_selected(int idx) { 428 | set_selected_thread(idx); 429 | } 430 | 431 | void ZProfilingClient::_on_graph_view_frame_clicked(int frame_index) { 432 | if (frame_index < 0) { 433 | return; 434 | } 435 | if (get_selected_thread() == -1) { 436 | return; 437 | } 438 | const ThreadData &thread_data = get_thread_data(get_selected_thread()); 439 | if (frame_index >= thread_data.frames.size()) { 440 | return; 441 | } 442 | set_selected_frame(frame_index); 443 | } 444 | 445 | void ZProfilingClient::_on_graph_view_mouse_wheel_moved(int delta) { 446 | if (get_selected_thread() == -1) { 447 | return; 448 | } 449 | const ThreadData &thread_data = get_thread_data(get_selected_thread()); 450 | int new_frame_index = thread_data.selected_frame + delta; 451 | if (new_frame_index < 0) { 452 | new_frame_index = 0; 453 | } 454 | if (new_frame_index >= thread_data.frames.size()) { 455 | new_frame_index = thread_data.frames.size() - 1; 456 | } 457 | set_selected_frame(new_frame_index); 458 | } 459 | 460 | void ZProfilingClient::disconnect_on_error(String message) { 461 | ERR_PRINT(String("ERROR: ") + message); 462 | disconnect_from_host(); 463 | } 464 | 465 | void ZProfilingClient::reset_connect_button() { 466 | _connect_button->set_disabled(false); 467 | _connect_button->set_text(TTR("Connect")); 468 | } 469 | 470 | bool ZProfilingClient::try_auto_select_main_thread() { 471 | for (size_t i = 0; i < _threads.size(); ++i) { 472 | const ThreadData &t = _threads[i]; 473 | const String &iname = _names[t.id]; 474 | // TODO Have a more "official" way to say which thread is main? 475 | if (iname.findn("main") != -1) { 476 | set_selected_thread(i); 477 | return true; 478 | } 479 | } 480 | return false; 481 | } 482 | 483 | void ZProfilingClient::set_selected_thread(int thread_index) { 484 | if (thread_index == _selected_thread_index) { 485 | return; 486 | } 487 | 488 | ERR_FAIL_COND(thread_index >= _threads.size()); 489 | _selected_thread_index = thread_index; 490 | _timeline_view->set_thread(_selected_thread_index); 491 | _tree_view->set_thread_index(_selected_thread_index); 492 | 493 | _graph_view->update(); 494 | 495 | print_line(String("Selected thread {0}").format(varray(_selected_thread_index))); 496 | _thread_selector->select(thread_index); 497 | 498 | const ThreadData &thread_data = _threads[_selected_thread_index]; 499 | _frame_spinbox_ignore_changes = true; 500 | _frame_spinbox->set_max(thread_data.frames.size() - 1); 501 | _frame_spinbox_ignore_changes = false; 502 | 503 | if (thread_data.frames.size() > 0) { 504 | set_selected_frame(thread_data.frames.size() - 1); 505 | } 506 | } 507 | 508 | int ZProfilingClient::get_selected_thread() const { 509 | return _selected_thread_index; 510 | } 511 | 512 | void ZProfilingClient::set_selected_frame(int frame_index) { 513 | ERR_FAIL_COND(_selected_thread_index == -1); 514 | ThreadData &thread_data = _threads.write[_selected_thread_index]; 515 | 516 | ERR_FAIL_COND(thread_data.frames.size() == 0); 517 | ERR_FAIL_COND(frame_index >= thread_data.frames.size()); 518 | 519 | // Clamp to last completed frame 520 | while (frame_index >= 0 && thread_data.frames[frame_index].end_time == -1) { 521 | --frame_index; 522 | } 523 | if (frame_index < 0) { 524 | // No complete frame available yet 525 | // TODO Support viewing partial frames? 526 | return; 527 | } 528 | 529 | if (thread_data.selected_frame == frame_index) { 530 | return; 531 | } 532 | 533 | thread_data.selected_frame = frame_index; 534 | _timeline_view->set_frame_index(frame_index); 535 | _tree_view->set_frame_index(frame_index); 536 | _graph_view->update(); 537 | 538 | // This can emit again and cycle back to our method... 539 | // hence why checking if it changed is important 540 | _frame_spinbox->set_value(thread_data.selected_frame); 541 | } 542 | 543 | void ZProfilingClient::update_thread_list() { 544 | for (size_t i = 0; i < _threads.size(); ++i) { 545 | const ThreadData &thread_data = _threads[i]; 546 | const String &iname = _names[thread_data.id]; 547 | 548 | if (i < _thread_selector->get_item_count()) { 549 | _thread_selector->set_item_text(i, iname); 550 | } else { 551 | _thread_selector->add_item(iname); 552 | } 553 | } 554 | 555 | while (_thread_selector->get_item_count() > _threads.size()) { 556 | _thread_selector->remove_item(_thread_selector->get_item_count() - 1); 557 | } 558 | } 559 | 560 | void ZProfilingClient::_bind_methods() { 561 | // Internal 562 | ClassDB::bind_method(D_METHOD("_on_connect_button_pressed"), &ZProfilingClient::_on_connect_button_pressed); 563 | ClassDB::bind_method(D_METHOD("_on_frame_spinbox_value_changed"), &ZProfilingClient::_on_frame_spinbox_value_changed); 564 | ClassDB::bind_method(D_METHOD("_on_thread_selector_item_selected"), &ZProfilingClient::_on_thread_selector_item_selected); 565 | ClassDB::bind_method(D_METHOD("_on_graph_view_frame_clicked"), &ZProfilingClient::_on_graph_view_frame_clicked); 566 | ClassDB::bind_method(D_METHOD("_on_graph_view_mouse_wheel_moved"), &ZProfilingClient::_on_graph_view_mouse_wheel_moved); 567 | 568 | ClassDB::bind_method(D_METHOD("connect_to_host"), &ZProfilingClient::connect_to_host); 569 | ClassDB::bind_method(D_METHOD("disconnect_from_host"), &ZProfilingClient::disconnect_from_host); 570 | } 571 | --------------------------------------------------------------------------------