diff --git a/SConstruct b/SConstruct index 8b12900cc20..08fbdefdd28 100644 --- a/SConstruct +++ b/SConstruct @@ -202,6 +202,14 @@ opts.Add(BoolVariable("use_volk", "Use the volk library to load the Vulkan loade opts.Add(BoolVariable("accesskit", "Use AccessKit C SDK", True)) opts.Add(("accesskit_sdk_path", "Path to the AccessKit C SDK", "")) opts.Add(BoolVariable("sdl", "Enable the SDL3 input driver", True)) +opts.Add(("profiler_path", "Path to the Profiler framework. Only tracy and perfetto are supported at the moment.", "")) +opts.Add( + BoolVariable( + "profiler_sample_callstack", + "Profile random samples application-wide using a callstack based sampler.", + False, + ) +) # Advanced options opts.Add( diff --git a/core/SCsub b/core/SCsub index 60ea202ac96..3f1b92055a4 100644 --- a/core/SCsub +++ b/core/SCsub @@ -217,6 +217,7 @@ env.CommandNoCache( ) # Chain load SCsubs +SConscript("profiling/SCsub") SConscript("os/SCsub") SConscript("math/SCsub") SConscript("crypto/SCsub") diff --git a/core/profiling/SCsub b/core/profiling/SCsub new file mode 100644 index 00000000000..1ddc0c05659 --- /dev/null +++ b/core/profiling/SCsub @@ -0,0 +1,69 @@ +#!/usr/bin/env python +from misc.utility.scons_hints import * + +import pathlib +from typing import Tuple + +import profiling_builders + +Import("env") + +env.add_source_files(env.core_sources, "*.cpp") + + +def get_profiler_and_path_from_path(path: pathlib.Path) -> Tuple[str, pathlib.Path]: + if not path.is_dir(): + print("profiler_path must be empty or point to a directory.") + Exit(255) + + if (path / "sdk" / "perfetto.cc").is_file(): + # perfetto root directory. + return "perfetto", path / "sdk" + if (path / "perfetto.cc").is_file(): + # perfetto sdk directory. + return "perfetto", path + + if (path / "public" / "TracyClient.cpp").is_file(): + # tracy root directory + return "tracy", path / "public" + if (path / "TracyClient.cpp").is_file(): + # tracy public directory + return "tracy", path + + print("Unrecognized profiler_path option. Please set a path to either tracy or perfetto.") + Exit(255) + + +env["profiler"] = None +if env["profiler_path"]: + profiler_name, profiler_path = get_profiler_and_path_from_path(pathlib.Path(env["profiler_path"])) + env["profiler"] = profiler_name + + if profiler_name == "tracy": + env.Prepend(CPPPATH=[str(profiler_path.absolute())]) + + env_tracy = env.Clone() + env_tracy.Append(CPPDEFINES=["TRACY_ENABLE"]) + if env["profiler_sample_callstack"]: + if env["platform"] not in ("windows", "linux", "android"): + # Reference the feature matrix in the tracy documentation. + print("Tracy does not support call stack sampling on this platform. Aborting.") + Exit(255) + + # 62 is the maximum supported callstack depth reported by the tracy docs. + env_tracy.Append(CPPDEFINES=[("TRACY_CALLSTACK", 62)]) + env_tracy.disable_warnings() + env_tracy.add_source_files(env.core_sources, str((profiler_path / "TracyClient.cpp").absolute())) + elif profiler_name == "perfetto": + env.Prepend(CPPPATH=[str(profiler_path.absolute())]) + + env_perfetto = env.Clone() + if env["profiler_sample_callstack"]: + print("Perfetto does not support call stack sampling. Aborting.") + Exit(255) + env_perfetto.disable_warnings() + env_perfetto.Prepend(CPPPATH=[str(profiler_path.absolute())]) + env_perfetto.add_source_files(env.core_sources, str((profiler_path / "perfetto.cc").absolute())) + + +env.CommandNoCache("profiling.gen.h", [env.Value(env["profiler"])], env.Run(profiling_builders.profiler_gen_builder)) diff --git a/core/profiling/profiling.cpp b/core/profiling/profiling.cpp new file mode 100644 index 00000000000..a63db864484 --- /dev/null +++ b/core/profiling/profiling.cpp @@ -0,0 +1,45 @@ +/**************************************************************************/ +/* profiling.cpp */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#include "profiling.h" + +#ifdef GODOT_USE_PERFETTO +PERFETTO_TRACK_EVENT_STATIC_STORAGE(); + +void godot_init_profiler() { + perfetto::TracingInitArgs args; + + args.backends |= perfetto::kSystemBackend; + + perfetto::Tracing::Initialize(args); + perfetto::TrackEvent::Register(); +} + +#endif diff --git a/core/profiling/profiling.h b/core/profiling/profiling.h new file mode 100644 index 00000000000..c6d1b265897 --- /dev/null +++ b/core/profiling/profiling.h @@ -0,0 +1,106 @@ +/**************************************************************************/ +/* profiling.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#pragma once + +#include "core/typedefs.h" +#include "profiling.gen.h" + +#if defined(GODOT_USE_TRACY) +// Use the tracy profiler. + +#define TRACY_ENABLE +#include + +#ifndef TRACY_CALLSTACK +#define TRACY_CALLSTACK 0 +#endif + +// Define tracing macros. +#define GodotProfileFrameMark FrameMark +#define GodotProfileZone(m_zone_name) ZoneScopedN(m_zone_name) +#define GodotProfileZoneGroupedFirst(m_group_name, m_zone_name) ZoneNamedN(__godot_tracy_zone_##m_group_name, m_zone_name, true) +#define GodotProfileZoneGroupedEndEarly(m_group_name, m_zone_name) __godot_tracy_zone_##m_group_name.~ScopedZone(); +#define GodotProfileZoneGrouped(m_group_name, m_zone_name) \ + GodotProfileZoneGroupedEndEarly(m_group_name, m_zone_name); \ + 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) + +static void godot_init_profiler() { + // 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; +} + +#elif defined(GODOT_USE_PERFETTO) +// Use the perfetto profiler. + +#include + +PERFETTO_DEFINE_CATEGORIES( + perfetto::Category("godot") + .SetDescription("All Godot Events"), ); + +// See PERFETTO_INTERNAL_SCOPED_EVENT_FINALIZER +struct PerfettoGroupedEventEnder { + _FORCE_INLINE_ void _end_now() { + TRACE_EVENT_END("godot"); + } + + _FORCE_INLINE_ ~PerfettoGroupedEventEnder() { + _end_now(); + } +}; + +#define GodotProfileFrameMark // TODO +#define GodotProfileZone(m_zone_name) TRACE_EVENT("godot", m_zone_name); +#define GodotProfileZoneGroupedFirst(m_group_name, m_zone_name) \ + TRACE_EVENT_BEGIN("godot", m_zone_name); \ + PerfettoGroupedEventEnder __godot_perfetto_zone_##m_group_name +#define GodotProfileZoneGroupedEndEarly(m_group_name, m_zone_name) __godot_perfetto_zone_##m_group_name.~PerfettoGroupedEventEnder() +#define GodotProfileZoneGrouped(m_group_name, m_zone_name) \ + __godot_perfetto_zone_##m_group_name._end_now(); \ + TRACE_EVENT_BEGIN("godot", m_zone_name); + +void godot_init_profiler(); + +#else +// No profiling; all macros are stubs. + +static void godot_init_profiler() { +} + +#define GodotProfileFrameMark +#define GodotProfileZone(m_zone_name) +#define GodotProfileZoneGroupedFirst(m_group_name, m_zone_name) +#define GodotProfileZoneGroupedEndEarly(m_group_name, m_zone_name) +#define GodotProfileZoneGrouped(m_group_name, m_zone_name) + +#endif diff --git a/core/profiling/profiling_builders.py b/core/profiling/profiling_builders.py new file mode 100644 index 00000000000..9a2122d8b2b --- /dev/null +++ b/core/profiling/profiling_builders.py @@ -0,0 +1,13 @@ +"""Functions used to generate source files during build time""" + +import methods + + +def profiler_gen_builder(target, source, env): + with methods.generated_wrapper(str(target[0])) as file: + if env["profiler"] == "tracy": + file.write("#define GODOT_USE_TRACY\n") + if env["profiler_sample_callstack"]: + file.write("#define TRACY_CALLSTACK 62\n") + if env["profiler"] == "perfetto": + file.write("#define GODOT_USE_PERFETTO\n") diff --git a/platform/android/java_godot_lib_jni.cpp b/platform/android/java_godot_lib_jni.cpp index fd19541f7cd..8b3035301bb 100644 --- a/platform/android/java_godot_lib_jni.cpp +++ b/platform/android/java_godot_lib_jni.cpp @@ -49,6 +49,7 @@ #include "core/config/project_settings.h" #include "core/input/input.h" #include "core/os/main_loop.h" +#include "core/profiling/profiling.h" #include "main/main.h" #include "servers/rendering/rendering_server.h" @@ -151,6 +152,8 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_setVirtualKeyboardHei } JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_initialize(JNIEnv *env, jclass clazz, jobject p_godot_instance, jobject p_asset_manager, jobject p_godot_io, jobject p_net_utils, jobject p_directory_access_handler, jobject p_file_access_handler, jboolean p_use_apk_expansion) { + godot_init_profiler(); + JavaVM *jvm; env->GetJavaVM(&jvm); diff --git a/platform/ios/main_ios.mm b/platform/ios/main_ios.mm index fe7e2022ca7..7d9c054950e 100644 --- a/platform/ios/main_ios.mm +++ b/platform/ios/main_ios.mm @@ -30,6 +30,7 @@ #import "os_ios.h" +#include "core/profiling/profiling.h" #import "drivers/apple_embedded/godot_app_delegate.h" #import "drivers/apple_embedded/main_utilities.h" #include "main/main.h" @@ -55,6 +56,8 @@ int apple_embedded_main(int argc, char **argv) { char *fargv[64]; argc = process_args(argc, argv, fargv); + godot_init_profiler(); + Error err = Main::setup(fargv[0], argc - 1, &fargv[1], false); if (err != OK) { diff --git a/platform/linuxbsd/godot_linuxbsd.cpp b/platform/linuxbsd/godot_linuxbsd.cpp index 8bda6d932c1..f2c7d8eccf1 100644 --- a/platform/linuxbsd/godot_linuxbsd.cpp +++ b/platform/linuxbsd/godot_linuxbsd.cpp @@ -30,6 +30,7 @@ #include "os_linuxbsd.h" +#include "core/profiling/profiling.h" #include "main/main.h" #include @@ -93,6 +94,8 @@ int main(int argc, char *argv[]) { setrlimit(RLIMIT_STACK, &stack_lim); #endif + godot_init_profiler(); + OS_LinuxBSD os; setlocale(LC_CTYPE, ""); diff --git a/platform/macos/godot_main_macos.mm b/platform/macos/godot_main_macos.mm index 3807464fcf2..4eefa7df571 100644 --- a/platform/macos/godot_main_macos.mm +++ b/platform/macos/godot_main_macos.mm @@ -32,6 +32,7 @@ #import "godot_application.h" +#include "core/profiling/profiling.h" #include "main/main.h" #if defined(SANITIZERS_ENABLED) @@ -39,6 +40,8 @@ #endif int main(int argc, char **argv) { + godot_init_profiler(); + #if defined(VULKAN_ENABLED) setenv("MVK_CONFIG_FULL_IMAGE_VIEW_SWIZZLE", "1", 1); // MoltenVK - enable full component swizzling support. setenv("MVK_CONFIG_SWAPCHAIN_MIN_MAG_FILTER_USE_NEAREST", "0", 1); // MoltenVK - use linear surface scaling. TODO: remove when full DPI scaling is implemented. diff --git a/platform/macos/os_macos.mm b/platform/macos/os_macos.mm index 512489734cc..5354b5d4a94 100644 --- a/platform/macos/os_macos.mm +++ b/platform/macos/os_macos.mm @@ -41,6 +41,7 @@ #include "core/crypto/crypto_core.h" #include "core/io/file_access.h" #include "core/os/main_loop.h" +#include "core/profiling/profiling.h" #include "core/version_generated.gen.h" #include "drivers/apple/os_log_logger.h" #include "main/main.h" @@ -1080,6 +1081,8 @@ static void handle_interrupt(int sig) { } void OS_MacOS_NSApp::start_main() { + godot_init_profiler(); + Error err; @autoreleasepool { err = Main::setup(execpath, argc, argv); diff --git a/platform/visionos/main_visionos.mm b/platform/visionos/main_visionos.mm index 3e392307c86..c0d1ba3b70a 100644 --- a/platform/visionos/main_visionos.mm +++ b/platform/visionos/main_visionos.mm @@ -30,6 +30,7 @@ #import "os_visionos.h" +#include "core/profiling/profiling.h" #import "drivers/apple_embedded/godot_app_delegate.h" #import "drivers/apple_embedded/main_utilities.h" #include "main/main.h" @@ -50,6 +51,8 @@ int apple_embedded_main(int argc, char **argv) { char *fargv[64]; argc = process_args(argc, argv, fargv); + godot_init_profiler(); + Error err = Main::setup(fargv[0], argc - 1, &fargv[1], false); if (err != OK) { diff --git a/platform/web/web_main.cpp b/platform/web/web_main.cpp index 2378dd32ce8..3fb3f2bcce8 100644 --- a/platform/web/web_main.cpp +++ b/platform/web/web_main.cpp @@ -35,6 +35,7 @@ #include "core/config/engine.h" #include "core/io/file_access.h" #include "core/io/resource_loader.h" +#include "core/profiling/profiling.h" #include "main/main.h" #include "scene/main/scene_tree.h" #include "scene/main/window.h" // SceneTree only forward declares it. @@ -126,6 +127,8 @@ void print_web_header() { /// When calling main, it is assumed FS is setup and synced. extern EMSCRIPTEN_KEEPALIVE int godot_web_main(int argc, char *argv[]) { + godot_init_profiler(); + os = new OS_Web(); #ifdef TOOLS_ENABLED diff --git a/platform/windows/godot_windows.cpp b/platform/windows/godot_windows.cpp index 3d6f973b331..7355a9f925a 100644 --- a/platform/windows/godot_windows.cpp +++ b/platform/windows/godot_windows.cpp @@ -30,6 +30,7 @@ #include "os_windows.h" +#include "core/profiling/profiling.h" #include "main/main.h" #include @@ -66,6 +67,8 @@ char *wc_to_utf8(const wchar_t *wc) { } int widechar_main(int argc, wchar_t **argv) { + godot_init_profiler(); + OS_Windows os(nullptr); setlocale(LC_CTYPE, "");