diff --git a/SConstruct b/SConstruct index 75eb5cb2656..8ee270250f7 100644 --- a/SConstruct +++ b/SConstruct @@ -202,7 +202,12 @@ 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( + EnumVariable( + "profiler", "Specify the profiler to use", "none", ["none", "tracy", "perfetto", "instruments"], ignorecase=2 + ) +) +opts.Add(("profiler_path", "Path to the Profiler framework.", "")) opts.Add( BoolVariable( "profiler_sample_callstack", diff --git a/core/profiling/SCsub b/core/profiling/SCsub index 80fa6e9a78f..7f55873ea7b 100644 --- a/core/profiling/SCsub +++ b/core/profiling/SCsub @@ -12,35 +12,47 @@ Import("env") env.add_source_files(env.core_sources, "*.cpp") -def get_profiler_and_path_from_path(path: pathlib.Path) -> tuple[str, pathlib.Path]: +def find_perfetto_path(path: pathlib.Path) -> 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" + return path / "sdk" if (path / "perfetto.cc").is_file(): # perfetto sdk directory. - return "perfetto", path + return 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.") + print("Invalid profiler_path. Unable to find perfetto.cc.") 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 +def find_tracy_path(path: pathlib.Path) -> pathlib.Path: + if not path.is_dir(): + print("profiler_path must point to a directory.") + Exit(255) - if profiler_name == "tracy": + if (path / "public" / "TracyClient.cpp").is_file(): + # tracy root directory + return path / "public" + if (path / "TracyClient.cpp").is_file(): + # tracy public directory + return path + + print("Invalid profiler_path. Unable to find TracyClient.cpp.") + Exit(255) + + +if env["profiler"]: + if env["profiler"] == "instruments": + # Nothing else to do for Instruments. + pass + elif env["profiler"] == "tracy": + if not env["profiler_path"]: + print("profiler_path must be set when using the tracy profiler. Aborting.") + Exit(255) + profiler_path = find_tracy_path(pathlib.Path(env["profiler_path"])) env.Prepend(CPPPATH=[str(profiler_path.absolute())]) env_tracy = env.Clone() @@ -55,7 +67,11 @@ if env["profiler_path"]: 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": + elif env["profiler"] == "perfetto": + if not env["profiler_path"]: + print("profiler_path must be set when using the perfetto profiler. Aborting.") + Exit(255) + profiler_path = find_perfetto_path(pathlib.Path(env["profiler_path"])) env.Prepend(CPPPATH=[str(profiler_path.absolute())]) env_perfetto = env.Clone() @@ -65,6 +81,8 @@ if env["profiler_path"]: 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())) - +elif env["profiler_path"]: + print("profiler is required if profiler_path is set. Aborting.") + Exit(255) 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 index 4b16d13b5be..625a79e89b1 100644 --- a/core/profiling/profiling.cpp +++ b/core/profiling/profiling.cpp @@ -210,6 +210,32 @@ void godot_cleanup_profiler() { // Stub } +#elif defined(GODOT_USE_INSTRUMENTS) + +namespace apple::instruments { + +os_log_t LOG; +os_log_t LOG_TRACING; + +} // namespace apple::instruments + +void godot_init_profiler() { + static bool initialized = false; + if (initialized) { + return; + } + initialized = true; + apple::instruments::LOG = os_log_create("org.godotengine.godot", OS_LOG_CATEGORY_POINTS_OF_INTEREST); +#ifdef INSTRUMENTS_SAMPLE_CALLSTACKS + apple::instruments::LOG_TRACING = os_log_create("org.godotengine.godot", OS_LOG_CATEGORY_DYNAMIC_STACK_TRACING); +#else + apple::instruments::LOG_TRACING = os_log_create("org.godotengine.godot", "tracing"); +#endif +} + +void godot_cleanup_profiler() { +} + #else void godot_init_profiler() { // Stub diff --git a/core/profiling/profiling.h b/core/profiling/profiling.h index 5ede472c07e..43d50c2bb09 100644 --- a/core/profiling/profiling.h +++ b/core/profiling/profiling.h @@ -119,6 +119,65 @@ struct PerfettoGroupedEventEnder { void godot_init_profiler(); void godot_cleanup_profiler(); +#elif defined(GODOT_USE_INSTRUMENTS) + +#include +#include + +namespace apple::instruments { + +extern os_log_t LOG; +extern os_log_t LOG_TRACING; + +typedef void (*DeferFunc)(); + +class Defer { +public: + explicit Defer(DeferFunc p_fn) : + _fn(p_fn) {} + ~Defer() { + _fn(); + } + +private: + DeferFunc _fn; +}; + +} // namespace apple::instruments + +#define GodotProfileFrameMark \ + os_signpost_event_emit(apple::instruments::LOG, OS_SIGNPOST_ID_EXCLUSIVE, "Frame"); + +#define GodotProfileZoneGroupedFirst(m_group_name, m_zone_name) \ + os_signpost_interval_begin(apple::instruments::LOG_TRACING, OS_SIGNPOST_ID_EXCLUSIVE, m_zone_name); \ + apple::instruments::DeferFunc _GD_VARNAME_CONCAT_(defer__fn, _, m_group_name) = []() { \ + os_signpost_interval_end(apple::instruments::LOG_TRACING, OS_SIGNPOST_ID_EXCLUSIVE, m_zone_name); \ + }; \ + apple::instruments::Defer _GD_VARNAME_CONCAT_(__instruments_defer_zone_end__, _, m_group_name)(_GD_VARNAME_CONCAT_(defer__fn, _, m_group_name)); + +#define GodotProfileZoneGroupedEndEarly(m_group_name, m_zone_name) \ + _GD_VARNAME_CONCAT_(__instruments_defer_zone_end__, _, m_group_name).~Defer(); + +#define GodotProfileZoneGrouped(m_group_name, m_zone_name) \ + GodotProfileZoneGroupedEndEarly(m_group_name, m_zone_name); \ + os_signpost_interval_begin(apple::instruments::LOG_TRACING, OS_SIGNPOST_ID_EXCLUSIVE, m_zone_name); \ + _GD_VARNAME_CONCAT_(defer__fn, _, m_group_name) = []() { \ + os_signpost_interval_end(apple::instruments::LOG_TRACING, OS_SIGNPOST_ID_EXCLUSIVE, m_zone_name); \ + }; \ + new (&_GD_VARNAME_CONCAT_(__instruments_defer_zone_end__, _, m_group_name)) apple::instruments::Defer(_GD_VARNAME_CONCAT_(defer__fn, _, m_group_name)); + +#define GodotProfileZone(m_zone_name) \ + GodotProfileZoneGroupedFirst(__COUNTER__, m_zone_name) + +#define GodotProfileZoneGroupedFirstScript(m_varname, m_ptr, m_file, m_function, m_line) + +// Instruments has its own memory profiling, so these are no-ops. +#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. diff --git a/core/profiling/profiling_builders.py b/core/profiling/profiling_builders.py index 9a2122d8b2b..63820846245 100644 --- a/core/profiling/profiling_builders.py +++ b/core/profiling/profiling_builders.py @@ -11,3 +11,7 @@ def profiler_gen_builder(target, source, env): file.write("#define TRACY_CALLSTACK 62\n") if env["profiler"] == "perfetto": file.write("#define GODOT_USE_PERFETTO\n") + if env["profiler"] == "instruments": + file.write("#define GODOT_USE_INSTRUMENTS\n") + if env["profiler_sample_callstack"]: + file.write("#define INSTRUMENTS_SAMPLE_CALLSTACKS\n")