Browse Source

Add support for profiling GDScript with tracy.
This adds macro `GodotProfileZoneGroupedFirstScript`, and uses interning for speedy lookups.

Co-authored-by: Samuel Nicholas <[email protected]>

Lukas Tenbrink 2 weeks ago
parent
commit
acefbbbbcd

+ 167 - 0
core/profiling/profiling.cpp

@@ -31,11 +31,169 @@
 #include "profiling.h"
 
 #if defined(GODOT_USE_TRACY)
+// Use the tracy profiler.
+
+#include "core/os/mutex.h"
+#include "core/templates/paged_allocator.h"
+
+namespace TracyInternal {
+static bool configured = false;
+
+// Implementation similar to StringName.
+struct StringInternData {
+	StringName name;
+	CharString name_utf8;
+
+	uint32_t hash = 0;
+	StringInternData *prev = nullptr;
+	StringInternData *next = nullptr;
+
+	StringInternData() {}
+};
+
+struct SourceLocationInternData {
+	const StringInternData *file;
+	const StringInternData *function;
+
+	tracy::SourceLocationData source_location_data;
+
+	uint32_t function_ptr_hash = 0;
+	SourceLocationInternData *prev = nullptr;
+	SourceLocationInternData *next = nullptr;
+
+	SourceLocationInternData() {}
+};
+
+struct TracyInternTable {
+	constexpr static uint32_t TABLE_BITS = 16;
+	constexpr static uint32_t TABLE_LEN = 1 << TABLE_BITS;
+	constexpr static uint32_t TABLE_MASK = TABLE_LEN - 1;
+
+	static inline BinaryMutex mutex;
+
+	static inline SourceLocationInternData *source_location_table[TABLE_LEN];
+	static inline PagedAllocator<SourceLocationInternData> source_location_allocator;
+
+	static inline StringInternData *string_table[TABLE_LEN];
+	static inline PagedAllocator<StringInternData> string_allocator;
+};
+
+const StringInternData *_intern_name(const StringName &p_name) {
+	CRASH_COND(!configured);
+
+	const uint32_t hash = p_name.hash();
+	const uint32_t idx = hash & TracyInternTable::TABLE_MASK;
+
+	StringInternData *_data = TracyInternTable::string_table[idx];
+
+	while (_data) {
+		if (_data->hash == hash) {
+			return _data;
+		}
+		_data = _data->next;
+	}
+
+	_data = TracyInternTable::string_allocator.alloc();
+	_data->name = p_name;
+	_data->name_utf8 = p_name.operator String().utf8();
+
+	_data->next = TracyInternTable::string_table[idx];
+	_data->prev = nullptr;
+
+	if (TracyInternTable::string_table[idx]) {
+		TracyInternTable::string_table[idx]->prev = _data;
+	}
+	TracyInternTable::string_table[idx] = _data;
+
+	return _data;
+}
+
+const char *intern_name(const StringName &p_name) {
+	MutexLock lock(TracyInternTable::mutex);
+	return _intern_name(p_name)->name_utf8.get_data();
+}
+
+const tracy::SourceLocationData *intern_source_location(const void *p_function_ptr, const StringName &p_file, const StringName &p_function, uint32_t p_line) {
+	CRASH_COND(!configured);
+
+	const uint32_t hash = HashMapHasherDefault::hash(p_function_ptr);
+	const uint32_t idx = hash & TracyInternTable::TABLE_MASK;
+
+	MutexLock lock(TracyInternTable::mutex);
+	SourceLocationInternData *_data = TracyInternTable::source_location_table[idx];
+
+	while (_data) {
+		if (_data->function_ptr_hash == hash && _data->source_location_data.line == p_line && _data->file->name == p_file && _data->function->name == p_function) {
+			return &_data->source_location_data;
+		}
+		_data = _data->next;
+	}
+
+	_data = TracyInternTable::source_location_allocator.alloc();
+
+	_data->function_ptr_hash = hash;
+	_data->file = _intern_name(p_file);
+	_data->function = _intern_name(p_function);
+
+	_data->source_location_data.file = _data->file->name_utf8.get_data();
+	_data->source_location_data.function = _data->function->name_utf8.get_data();
+	_data->source_location_data.name = _data->source_location_data.function;
+
+	_data->source_location_data.line = p_line;
+	_data->source_location_data.color = 0x478cbf; // godot_logo_blue
+
+	_data->next = TracyInternTable::source_location_table[idx];
+	_data->prev = nullptr;
+
+	if (TracyInternTable::source_location_table[idx]) {
+		TracyInternTable::source_location_table[idx]->prev = _data;
+	}
+	TracyInternTable::source_location_table[idx] = _data;
+
+	return &_data->source_location_data;
+}
+} // namespace TracyInternal
+
 void godot_init_profiler() {
+	MutexLock lock(TracyInternal::TracyInternTable::mutex);
+	ERR_FAIL_COND(TracyInternal::configured);
+
+	for (uint32_t i = 0; i < TracyInternal::TracyInternTable::TABLE_LEN; i++) {
+		TracyInternal::TracyInternTable::source_location_table[i] = nullptr;
+	}
+	for (uint32_t i = 0; i < TracyInternal::TracyInternTable::TABLE_LEN; i++) {
+		TracyInternal::TracyInternTable::string_table[i] = nullptr;
+	}
+
+	TracyInternal::configured = true;
+
 	// Send our first event to tracy; otherwise it doesn't start collecting data.
 	// FrameMark is kind of fitting because it communicates "this is where we started tracing".
 	FrameMark;
 }
+
+void godot_cleanup_profiler() {
+	MutexLock lock(TracyInternal::TracyInternTable::mutex);
+	ERR_FAIL_COND(!TracyInternal::configured);
+
+	for (uint32_t i = 0; i < TracyInternal::TracyInternTable::TABLE_LEN; i++) {
+		while (TracyInternal::TracyInternTable::source_location_table[i]) {
+			TracyInternal::SourceLocationInternData *d = TracyInternal::TracyInternTable::source_location_table[i];
+			TracyInternal::TracyInternTable::source_location_table[i] = TracyInternal::TracyInternTable::source_location_table[i]->next;
+			TracyInternal::TracyInternTable::source_location_allocator.free(d);
+		}
+	}
+	for (uint32_t i = 0; i < TracyInternal::TracyInternTable::TABLE_LEN; i++) {
+		while (TracyInternal::TracyInternTable::string_table[i]) {
+			TracyInternal::StringInternData *d = TracyInternal::TracyInternTable::string_table[i];
+			TracyInternal::TracyInternTable::string_table[i] = TracyInternal::TracyInternTable::string_table[i]->next;
+			TracyInternal::TracyInternTable::string_allocator.free(d);
+		}
+	}
+
+	TracyInternal::configured = false;
+}
+
 #elif defined(GODOT_USE_PERFETTO)
 PERFETTO_TRACK_EVENT_STATIC_STORAGE();
 
@@ -47,8 +205,17 @@ void godot_init_profiler() {
 	perfetto::Tracing::Initialize(args);
 	perfetto::TrackEvent::Register();
 }
+
+void godot_cleanup_profiler() {
+	// Stub
+}
+
 #else
 void godot_init_profiler() {
 	// Stub
 }
+
+void godot_cleanup_profiler() {
+	// Stub
+}
 #endif

+ 22 - 1
core/profiling/profiling.h

@@ -30,7 +30,6 @@
 
 #pragma once
 
-#include "core/typedefs.h"
 #include "profiling.gen.h"
 
 // This header provides profiling primitives (implemented as macros) for various backends.
@@ -46,9 +45,17 @@
 #if defined(GODOT_USE_TRACY)
 // Use the tracy profiler.
 
+#include "core/string/string_name.h"
+
 #define TRACY_ENABLE
+
 #include <tracy/Tracy.hpp>
 
+namespace TracyInternal {
+const char *intern_name(const StringName &p_name);
+const tracy::SourceLocationData *intern_source_location(const void *p_function_ptr, const StringName &p_file, const StringName &p_function, uint32_t p_line);
+} //namespace TracyInternal
+
 // Define tracing macros.
 #define GodotProfileFrameMark FrameMark
 #define GodotProfileZone(m_zone_name) ZoneNamedN(GD_UNIQUE_NAME(__godot_tracy_szone_), m_zone_name, true)
@@ -65,12 +72,15 @@
 	static constexpr tracy::SourceLocationData TracyConcat(__tracy_source_location, TracyLine){ m_zone_name, TracyFunction, TracyFile, (uint32_t)TracyLine, 0 }; \
 	new (&__godot_tracy_zone_##m_group_name) tracy::ScopedZone(&TracyConcat(__tracy_source_location, TracyLine), TRACY_CALLSTACK, true)
 #endif
+#define GodotProfileZoneGroupedFirstScript(m_varname, m_ptr, m_file, m_function, m_line) \
+	tracy::ScopedZone __godot_tracy_zone_##m_group_name(TracyInternal::intern_source_location(m_ptr, m_file, m_function, m_line))
 
 // Memory allocation
 #define GodotProfileAlloc(m_ptr, m_size) TracyAlloc(m_ptr, m_size)
 #define GodotProfileFree(m_ptr) TracyFree(m_ptr)
 
 void godot_init_profiler();
+void godot_cleanup_profiler();
 
 #elif defined(GODOT_USE_PERFETTO)
 // Use the perfetto profiler.
@@ -101,15 +111,19 @@ struct PerfettoGroupedEventEnder {
 #define GodotProfileZoneGrouped(m_group_name, m_zone_name) \
 	__godot_perfetto_zone_##m_group_name._end_now();       \
 	TRACE_EVENT_BEGIN("godot", m_zone_name);
+#define GodotProfileZoneGroupedFirstScript(m_varname, m_ptr, m_file, m_function, m_line) \\ TODO
 
 #define GodotProfileAlloc(m_ptr, m_size)
 #define GodotProfileFree(m_ptr)
+
 void godot_init_profiler();
+void godot_cleanup_profiler();
 
 #else
 // No profiling; all macros are stubs.
 
 void godot_init_profiler();
+void godot_cleanup_profiler();
 
 // Tell the profiling backend that a new frame has started.
 #define GodotProfileFrameMark
@@ -128,4 +142,11 @@ void godot_init_profiler();
 // Tell the profiling backend that an allocation was freed.
 // There must be a one to one correspondence of GodotProfileAlloc and GodotProfileFree calls.
 #define GodotProfileFree(m_ptr)
+
+// Define a zone with custom source information (for scripting)
+// m_varname is equivalent to GodotProfileZoneGrouped varnames.
+// m_ptr is a pointer to the function instance, which will be used for the lookup.
+// m_file, m_function are StringNames, m_line is a uint32_t, all used for the source location.
+#define GodotProfileZoneGroupedFirstScript(m_varname, m_ptr, m_file, m_function, m_line)
+
 #endif

+ 3 - 0
modules/gdscript/gdscript_vm.cpp

@@ -33,6 +33,7 @@
 #include "gdscript_lambda_callable.h"
 
 #include "core/os/os.h"
+#include "core/profiling/profiling.h"
 
 #ifdef DEBUG_ENABLED
 
@@ -495,6 +496,8 @@ void (*type_init_function_table[])(Variant *) = {
 #define METHOD_CALL_ON_FREED_INSTANCE_ERROR(method_pointer) "Cannot call method '" + (method_pointer)->get_name() + "' on a previously freed instance."
 
 Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_args, int p_argcount, Callable::CallError &r_err, CallState *p_state) {
+	GodotProfileZoneGroupedFirstScript(zone, this, source, name, _initial_line);
+
 	OPCODES_TABLE;
 
 	if (!_code_ptr) {

+ 1 - 0
platform/android/java_godot_lib_jni.cpp

@@ -129,6 +129,7 @@ static void _terminate(JNIEnv *env, bool p_restart = false) {
 	NetSocketAndroid::terminate();
 
 	cleanup_android_class_loader();
+	godot_cleanup_profiler();
 
 	if (godot_java) {
 		godot_java->on_godot_terminating(env);

+ 1 - 0
platform/ios/main_ios.mm

@@ -74,5 +74,6 @@ int apple_embedded_main(int argc, char **argv) {
 
 void apple_embedded_finish() {
 	Main::cleanup();
+	godot_cleanup_profiler();
 	delete os;
 }

+ 1 - 0
platform/linuxbsd/godot_linuxbsd.cpp

@@ -131,5 +131,6 @@ int main(int argc, char *argv[]) {
 	}
 	free(cwd);
 
+	godot_cleanup_profiler();
 	return os.get_exit_code();
 }

+ 1 - 0
platform/macos/godot_main_macos.mm

@@ -151,5 +151,6 @@ int main(int argc, char **argv) {
 
 	memdelete(os);
 
+	godot_cleanup_profiler();
 	return exit_code;
 }

+ 2 - 0
platform/macos/os_macos.mm

@@ -1147,6 +1147,8 @@ void OS_MacOS_NSApp::start_main() {
 }
 
 void OS_MacOS_NSApp::terminate() {
+	godot_cleanup_profiler();
+
 	if (pre_wait_observer) {
 		CFRunLoopRemoveObserver(CFRunLoopGetCurrent(), pre_wait_observer, kCFRunLoopCommonModes);
 		CFRelease(pre_wait_observer);

+ 1 - 0
platform/visionos/main_visionos.mm

@@ -69,5 +69,6 @@ int apple_embedded_main(int argc, char **argv) {
 
 void apple_embedded_finish() {
 	Main::cleanup();
+	godot_cleanup_profiler();
 	delete os;
 }

+ 1 - 0
platform/web/web_main.cpp

@@ -66,6 +66,7 @@ void exit_callback() {
 	int exit_code = OS_Web::get_singleton()->get_exit_code();
 	memdelete(os);
 	os = nullptr;
+	godot_cleanup_profiler();
 	emscripten_force_exit(exit_code); // Exit runtime.
 }
 

+ 1 - 0
platform/windows/godot_windows.cpp

@@ -107,6 +107,7 @@ int widechar_main(int argc, wchar_t **argv) {
 	}
 	delete[] argv_utf8;
 
+	godot_cleanup_profiler();
 	return os.get_exit_code();
 }