Add support for Tracy profiler (#15113)

This commit is contained in:
DS 2024-09-15 13:47:45 +02:00 committed by GitHub
parent 6f23de41fb
commit 4aec4fbe6f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 379 additions and 4 deletions

@ -56,6 +56,11 @@ if((WIN32 AND CMAKE_CXX_COMPILER_ID STREQUAL "GNU") OR APPLE)
endif()
set(ENABLE_LTO ${DEFAULT_ENABLE_LTO} CACHE BOOL "Use Link Time Optimization")
set(BUILD_WITH_TRACY FALSE CACHE BOOL
"Fetch and build with the Tracy profiler client")
set(FETCH_TRACY_GIT_TAG "master" CACHE STRING
"Git tag for fetching Tracy client. Match with your server (gui) version")
set(DEFAULT_RUN_IN_PLACE FALSE)
if(WIN32)
set(DEFAULT_RUN_IN_PLACE TRUE)
@ -370,3 +375,19 @@ if(BUILD_DOCUMENTATION)
)
endif()
endif()
# Fetch Tracy
if(BUILD_WITH_TRACY)
include(FetchContent)
message(STATUS "Fetching Tracy (${FETCH_TRACY_GIT_TAG})...")
FetchContent_Declare(
tracy
GIT_REPOSITORY https://github.com/wolfpld/tracy.git
GIT_TAG ${FETCH_TRACY_GIT_TAG}
GIT_SHALLOW TRUE
GIT_PROGRESS TRUE
)
FetchContent_MakeAvailable(tracy)
message(STATUS "Fetching Tracy - done")
endif()

@ -40,6 +40,8 @@ General options and their default values:
ENABLE_UPDATE_CHECKER=TRUE - Whether to enable update checks by default
INSTALL_DEVTEST=FALSE - Whether the Development Test game should be installed alongside Minetest
USE_GPROF=FALSE - Enable profiling using GProf
BUILD_WITH_TRACY=FALSE - Fetch and build with the Tracy profiler client
FETCH_TRACY_GIT_TAG=master - Git tag for fetching Tracy client. Match with your server (gui) version
VERSION_EXTRA= - Text to append to version (e.g. VERSION_EXTRA=foobar -> Minetest 0.4.9-foobar)
Library specific options:

@ -1,6 +1,6 @@
# Miscellaneous
## Profiling Minetest on Linux
## Profiling Minetest on Linux with perf
We will be using a tool called "perf", which you can get by installing `perf` or `linux-perf` or `linux-tools-common`.
@ -36,3 +36,54 @@ Give both files to the developer and also provide:
* commit the source was built from and/or modified source code (if applicable)
Hotspot will resolve symbols correctly when pointing the sysroot option at the collected libs.
## Profiling with Tracy
[Tracy](https://github.com/wolfpld/tracy) is
> A real time, nanosecond resolution, remote telemetry, hybrid frame and sampling
> profiler for games and other applications.
It allows one to annotate important functions and generate traces, where one can
see when each individual function call happened, and how long it took.
Tracy can also record when frames, e.g. server step, start and end, and inspect
frames that took longer than usual. Minetest already contains annotations for
its frames.
See also [Tracy's official documentation](https://github.com/wolfpld/tracy/releases/latest/download/tracy.pdf).
### Installing
Tracy consists of a client (Minetest) and a server (the gui).
Install the server, e.g. using your package manager.
### Building
Build Minetest with `-DDBUILD_WITH_TRACY=1`, this will fetch Tracy for building
the Tracy client. And use `FETCH_TRACY_GIT_TAG` to get a version matching your
Tracy server, e.g. `-DFETCH_TRACY_GIT_TAG=v0.11.0` if it's `0.11.0`.
To actually use Tracy, you also have to enable it with Tracy's build options:
```
-DTRACY_ENABLE=1 -DTRACY_ONLY_LOCALHOST=1
```
See Tracy's documentation for more build options.
### Using in C++
Start the Tracy server and Minetest. You should see Minetest in the menu.
To actually get useful traces, you have to annotate functions with `ZoneScoped`
macros and recompile. Please refer to Tracy's official documentation.
### Using in Lua
Tracy also supports Lua.
If built with Tracy, Minetest loads its API in the global `tracy` table.
See Tracy's official documentation for more information.
Note: The whole Tracy Lua API is accessible to all mods. And we don't check if it
is or becomes insecure. Run untrusted mods at your own risk.

@ -11394,6 +11394,16 @@ Functions: bit.tobit, bit.tohex, bit.bnot, bit.band, bit.bor, bit.bxor, bit.lshi
See http://bitop.luajit.org/ for advanced information.
Tracy Profiler
--------------
Minetest can be built with support for the Tracy profiler, which can also be
useful for profiling mods and is exposed to Lua as the global `tracy`.
See doc/developing/misc.md for details.
Note: This is a development feature and not covered by compatibility promises.
Error Handling
--------------

@ -468,6 +468,10 @@ foreach(object_lib
target_include_directories(${object_lib} PRIVATE ${link_includes})
# Add objects from object library to main library
target_sources(IrrlichtMt PRIVATE $<TARGET_OBJECTS:${object_lib}>)
if(BUILD_WITH_TRACY)
target_link_libraries(${object_lib} PRIVATE Tracy::TracyClient)
endif()
endforeach()
# Alias target provides add_submodule compatibility

@ -648,6 +648,9 @@ if(BUILD_CLIENT)
if(BUILD_UNITTESTS OR BUILD_BENCHMARKS)
target_link_libraries(${PROJECT_NAME} Catch2::Catch2)
endif()
if(BUILD_WITH_TRACY)
target_link_libraries(${PROJECT_NAME} Tracy::TracyClient)
endif()
if(PRECOMPILE_HEADERS)
target_precompile_headers(${PROJECT_NAME} PRIVATE ${PRECOMPILED_HEADERS_LIST})
@ -715,6 +718,9 @@ if(BUILD_SERVER)
if(BUILD_UNITTESTS OR BUILD_BENCHMARKS)
target_link_libraries(${PROJECT_NAME}server Catch2::Catch2)
endif()
if(BUILD_WITH_TRACY)
target_link_libraries(${PROJECT_NAME}server Tracy::TracyClient)
endif()
if(PRECOMPILE_HEADERS)
target_precompile_headers(${PROJECT_NAME}server PRIVATE ${PRECOMPILED_HEADERS_LIST})

@ -35,6 +35,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "version.h"
#include "renderingengine.h"
#include "network/networkexceptions.h"
#include "util/tracy_wrapper.h"
#include <IGUISpriteBank.h>
#include <ICameraSceneNode.h>
#include <unordered_map>
@ -544,15 +545,19 @@ void ClientLauncher::main_menu(MainMenuData *menudata)
video::IVideoDriver *driver = m_rendering_engine->get_video_driver();
infostream << "Waiting for other menus" << std::endl;
auto framemarker = FrameMarker("ClientLauncher::main_menu()-wait-frame").started();
while (m_rendering_engine->run() && !*kill) {
if (!isMenuActive())
break;
driver->beginScene(true, true, video::SColor(255, 128, 128, 128));
m_rendering_engine->get_gui_env()->drawAll();
driver->endScene();
framemarker.end();
// On some computers framerate doesn't seem to be automatically limited
sleep_ms(25);
framemarker.start();
}
framemarker.end();
infostream << "Waited for other menus" << std::endl;
auto *cur_control = m_rendering_engine->get_raw_device()->getCursorControl();

@ -31,6 +31,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "settings.h"
#include "camera.h" // CameraModes
#include "util/basic_macros.h"
#include "util/tracy_wrapper.h"
#include "client/renderingengine.h"
#include <queue>
@ -714,6 +715,8 @@ void ClientMap::touchMapBlocks()
void ClientMap::renderMap(video::IVideoDriver* driver, s32 pass)
{
ZoneScoped;
bool is_transparent_pass = pass == scene::ESNRP_TRANSPARENT;
std::string prefix;

@ -22,6 +22,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "util/basic_macros.h"
#include "util/numeric.h"
#include "util/directiontables.h"
#include "util/tracy_wrapper.h"
#include "mapblock_mesh.h"
#include "settings.h"
#include "nodedef.h"
@ -1750,6 +1751,8 @@ void MapblockMeshGenerator::drawNode()
void MapblockMeshGenerator::generate()
{
ZoneScoped;
for (cur_node.p.Z = 0; cur_node.p.Z < data->side_length; cur_node.p.Z++)
for (cur_node.p.Y = 0; cur_node.p.Y < data->side_length; cur_node.p.Y++)
for (cur_node.p.X = 0; cur_node.p.X < data->side_length; cur_node.p.X++) {

@ -79,6 +79,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "hud.h"
#include "clientdynamicinfo.h"
#include <IAnimatedMeshSceneNode.h>
#include "util/tracy_wrapper.h"
#if USE_SOUND
#include "client/sound/sound_openal.h"
@ -1140,6 +1141,8 @@ bool Game::startup(bool *kill,
void Game::run()
{
ZoneScoped;
ProfilerGraph graph;
RunStats stats = {};
CameraOrientation cam_view_target = {};
@ -1167,15 +1170,21 @@ void Game::run()
const bool initial_window_maximized = !g_settings->getBool("fullscreen") &&
g_settings->getBool("window_maximized");
auto framemarker = FrameMarker("Game::run()-frame").started();
while (m_rendering_engine->run()
&& !(*kill || g_gamecallback->shutdown_requested
|| (server && server->isShutdownRequested()))) {
framemarker.end();
// Calculate dtime =
// m_rendering_engine->run() from this iteration
// + Sleep time until the wanted FPS are reached
draw_times.limit(device, &dtime, g_menumgr.pausesGame());
framemarker.start();
const auto current_dynamic_info = ClientDynamicInfo::getCurrent();
if (!current_dynamic_info.equal(client_display_info)) {
client_display_info = current_dynamic_info;
@ -1232,6 +1241,8 @@ void Game::run()
}
}
framemarker.end();
RenderingEngine::autosaveScreensizeAndCo(initial_screen_size, initial_window_maximized);
}
@ -1671,9 +1682,13 @@ bool Game::connectToServer(const GameStartData &start_data,
fps_control.reset();
auto framemarker = FrameMarker("Game::connectToServer()-frame").started();
while (m_rendering_engine->run()) {
framemarker.end();
fps_control.limit(device, &dtime);
framemarker.start();
// Update client and server
step(dtime);
@ -1719,6 +1734,7 @@ bool Game::connectToServer(const GameStartData &start_data,
// Update status
showOverlayMessage(N_("Connecting to server..."), dtime, 20);
}
framemarker.end();
} catch (con::PeerNotFoundException &e) {
warningstream << "This should not happen. Please report a bug." << std::endl;
return false;
@ -1736,9 +1752,11 @@ bool Game::getServerContent(bool *aborted)
fps_control.reset();
auto framemarker = FrameMarker("Game::getServerContent()-frame").started();
while (m_rendering_engine->run()) {
framemarker.end();
fps_control.limit(device, &dtime);
framemarker.start();
// Update client and server
step(dtime);
@ -1804,6 +1822,7 @@ bool Game::getServerContent(bool *aborted)
texture_src, dtime, progress);
}
}
framemarker.end();
*aborted = true;
infostream << "Connect aborted [device]" << std::endl;
@ -2773,6 +2792,8 @@ void Game::updatePauseState()
inline void Game::step(f32 dtime)
{
ZoneScoped;
if (server) {
float fps_max = (!device->isWindowFocused() || g_menumgr.pausesGame()) ?
g_settings->getFloat("fps_max_unfocused") :
@ -4052,6 +4073,7 @@ void Game::handleDigging(const PointedThing &pointed, const v3s16 &nodepos,
void Game::updateFrame(ProfilerGraph *graph, RunStats *stats, f32 dtime,
const CameraOrientation &cam)
{
ZoneScoped;
TimeTaker tt_update("Game::updateFrame()");
LocalPlayer *player = client->getEnv().getLocalPlayer();
@ -4311,6 +4333,8 @@ void Game::updateShadows()
void Game::drawScene(ProfilerGraph *graph, RunStats *stats)
{
ZoneScoped;
const video::SColor fog_color = this->sky->getFogColor();
const video::SColor sky_color = this->sky->getSkyColor();

@ -28,6 +28,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "minimap.h"
#include "content_mapblock.h"
#include "util/directiontables.h"
#include "util/tracy_wrapper.h"
#include "client/meshgen/collector.h"
#include "client/renderingengine.h"
#include <array>
@ -611,6 +612,8 @@ MapBlockMesh::MapBlockMesh(Client *client, MeshMakeData *data, v3s16 camera_offs
m_last_crack(-1),
m_last_daynight_ratio((u32) -1)
{
ZoneScoped;
for (auto &m : m_mesh)
m = new scene::SMesh();
m_enable_shaders = data->m_use_shaders;

@ -26,6 +26,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "sound_singleton.h"
#include "util/numeric.h" // myrand()
#include "util/tracy_wrapper.h"
#include "filesys.h"
#include "porting.h"
@ -501,6 +502,8 @@ void *OpenALSoundManager::run()
u64 t_step_start = porting::getTimeMs();
while (true) {
auto framemarker = FrameMarker("OpenALSoundManager::run()-frame").started();
auto get_time_since_last_step = [&] {
return (f32)(porting::getTimeMs() - t_step_start);
};

@ -41,3 +41,4 @@
#cmakedefine01 BUILD_UNITTESTS
#cmakedefine01 BUILD_BENCHMARKS
#cmakedefine01 USE_SDL2
#cmakedefine01 BUILD_WITH_TRACY

@ -40,6 +40,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include <ICameraSceneNode.h>
#include <IGUIStaticText.h>
#include "client/imagefilters.h"
#include "util/tracy_wrapper.h"
#if USE_SOUND
#include "client/sound/sound_openal.h"
@ -329,9 +330,12 @@ void GUIEngine::run()
fps_control.reset();
while (m_rendering_engine->run() && !m_startgame && !m_kill) {
auto framemarker = FrameMarker("GUIEngine::run()-frame").started();
while (m_rendering_engine->run() && !m_startgame && !m_kill) {
framemarker.end();
fps_control.limit(device, &dtime);
framemarker.start();
if (device->isWindowVisible()) {
// check if we need to update the "upper left corner"-text
@ -371,6 +375,7 @@ void GUIEngine::run()
m_menu->getAndroidUIInput();
#endif
}
framemarker.end();
m_script->beforeClose();

@ -73,6 +73,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "filesys.h"
#include "log.h"
#include "util/string.h"
#include "util/tracy_wrapper.h"
#include <vector>
#include <cstdarg>
#include <cstdio>
@ -960,6 +961,8 @@ void TrackFreedMemory(size_t amount)
void TriggerMemoryTrim()
{
ZoneScoped;
constexpr auto MO = std::memory_order_relaxed;
if (memory_freed.load(MO) >= MEMORY_TRIM_THRESHOLD) {
// Synchronize call

@ -32,6 +32,9 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "client/client.h"
#endif
#if BUILD_WITH_TRACY
#include "tracy/TracyLua.hpp"
#endif
extern "C" {
#include "lualib.h"
@ -95,6 +98,11 @@ ScriptApiBase::ScriptApiBase(ScriptingType type):
lua_pushstring(m_luastack, LUA_BITLIBNAME);
lua_call(m_luastack, 1, 0);
#if BUILD_WITH_TRACY
// Load tracy lua bindings
tracy::LuaRegister(m_luastack);
#endif
// Make the ScriptApiBase* accessible to ModApiBase
#if INDIRECT_SCRIPTAPI_RIDX
*(void **)(lua_newuserdata(m_luastack, sizeof(void *))) = this;

@ -109,7 +109,12 @@ void ScriptApiSecurity::initializeSecurity()
"string",
"table",
"math",
"bit"
"bit",
// Not sure if completely safe. But if someone enables tracy, they'll
// know what they do.
#if BUILD_WITH_TRACY
"tracy",
#endif
};
static const char *io_whitelist[] = {
"close",
@ -303,6 +308,11 @@ void ScriptApiSecurity::initializeSecurityClient()
"table",
"math",
"bit",
// Not sure if completely safe. But if someone enables tracy, they'll
// know what they do.
#if BUILD_WITH_TRACY
"tracy",
#endif
};
static const char *os_whitelist[] = {
"clock",

@ -75,6 +75,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "gameparams.h"
#include "particles.h"
#include "gettext.h"
#include "util/tracy_wrapper.h"
class ClientNotFoundException : public BaseException
{
@ -101,6 +102,8 @@ private:
void *ServerThread::run()
{
ZoneScoped;
BEGIN_DEBUG_EXCEPTION_HANDLER
/*
@ -110,6 +113,7 @@ void *ServerThread::run()
* server-step frequency. Receive() is used for waiting between the steps.
*/
auto framemarker = FrameMarker("ServerThread::run()-frame").started();
try {
m_server->AsyncRunStep(0.0f, true);
} catch (con::ConnectionBindFailed &e) {
@ -119,10 +123,12 @@ void *ServerThread::run()
} catch (ModError &e) {
m_server->setAsyncFatalError(e.what());
}
framemarker.end();
float dtime = 0.0f;
while (!stopRequested()) {
framemarker.start();
ScopeProfiler spm(g_profiler, "Server::RunStep() (max)", SPT_MAX);
u64 t0 = porting::getTimeUs();
@ -149,6 +155,7 @@ void *ServerThread::run()
}
dtime = 1e-6f * (porting::getTimeUs() - t0);
framemarker.end();
}
END_DEBUG_EXCEPTION_HANDLER
@ -607,6 +614,9 @@ void Server::step()
void Server::AsyncRunStep(float dtime, bool initial_step)
{
ZoneScoped;
auto framemarker = FrameMarker("Server::AsyncRunStep()-frame").started();
{
// Send blocks to clients
SendBlocks(dtime);
@ -1055,6 +1065,9 @@ void Server::AsyncRunStep(float dtime, bool initial_step)
void Server::Receive(float timeout)
{
ZoneScoped;
auto framemarker = FrameMarker("Server::Receive()-frame").started();
const u64 t0 = porting::getTimeUs();
const float timeout_us = timeout * 1e6f;
auto remaining_time_us = [&]() -> float {

200
src/util/tracy_wrapper.h Normal file

@ -0,0 +1,200 @@
/*
Minetest
Copyright (C) 2024 DS
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 2.1 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
/*
* Wrapper for <tracy/Tracy.hpp>, so that we can use Tracy's macros without
* having it as mandatory dependency.
*
* For annotations that you don't intend to upstream, you can also include
* <tracy/Tracy.hpp> directly (which also works in irr/).
*/
#pragma once
#include "config.h"
#include "util/basic_macros.h"
#if BUILD_WITH_TRACY
#include <tracy/Tracy.hpp> // IWYU pragma: export
#else
// Copied from Tracy.hpp
#define TracyNoop
#define ZoneNamed(x,y)
#define ZoneNamedN(x,y,z)
#define ZoneNamedC(x,y,z)
#define ZoneNamedNC(x,y,z,w)
#define ZoneTransient(x,y)
#define ZoneTransientN(x,y,z)
#define ZoneScoped
#define ZoneScopedN(x)
#define ZoneScopedC(x)
#define ZoneScopedNC(x,y)
#define ZoneText(x,y)
#define ZoneTextV(x,y,z)
#define ZoneTextF(x,...)
#define ZoneTextVF(x,y,...)
#define ZoneName(x,y)
#define ZoneNameV(x,y,z)
#define ZoneNameF(x,...)
#define ZoneNameVF(x,y,...)
#define ZoneColor(x)
#define ZoneColorV(x,y)
#define ZoneValue(x)
#define ZoneValueV(x,y)
#define ZoneIsActive false
#define ZoneIsActiveV(x) false
#define FrameMark
#define FrameMarkNamed(x)
#define FrameMarkStart(x)
#define FrameMarkEnd(x)
#define FrameImage(x,y,z,w,a)
#define TracyLockable( type, varname ) type varname
#define TracyLockableN( type, varname, desc ) type varname
#define TracySharedLockable( type, varname ) type varname
#define TracySharedLockableN( type, varname, desc ) type varname
#define LockableBase( type ) type
#define SharedLockableBase( type ) type
#define LockMark(x) (void)x
#define LockableName(x,y,z)
#define TracyPlot(x,y)
#define TracyPlotConfig(x,y,z,w,a)
#define TracyMessage(x,y)
#define TracyMessageL(x)
#define TracyMessageC(x,y,z)
#define TracyMessageLC(x,y)
#define TracyAppInfo(x,y)
#define TracyAlloc(x,y)
#define TracyFree(x)
#define TracySecureAlloc(x,y)
#define TracySecureFree(x)
#define TracyAllocN(x,y,z)
#define TracyFreeN(x,y)
#define TracySecureAllocN(x,y,z)
#define TracySecureFreeN(x,y)
#define ZoneNamedS(x,y,z)
#define ZoneNamedNS(x,y,z,w)
#define ZoneNamedCS(x,y,z,w)
#define ZoneNamedNCS(x,y,z,w,a)
#define ZoneTransientS(x,y,z)
#define ZoneTransientNS(x,y,z,w)
#define ZoneScopedS(x)
#define ZoneScopedNS(x,y)
#define ZoneScopedCS(x,y)
#define ZoneScopedNCS(x,y,z)
#define TracyAllocS(x,y,z)
#define TracyFreeS(x,y)
#define TracySecureAllocS(x,y,z)
#define TracySecureFreeS(x,y)
#define TracyAllocNS(x,y,z,w)
#define TracyFreeNS(x,y,z)
#define TracySecureAllocNS(x,y,z,w)
#define TracySecureFreeNS(x,y,z)
#define TracyMessageS(x,y,z)
#define TracyMessageLS(x,y)
#define TracyMessageCS(x,y,z,w)
#define TracyMessageLCS(x,y,z)
#define TracySourceCallbackRegister(x,y)
#define TracyParameterRegister(x,y)
#define TracyParameterSetup(x,y,z,w)
#define TracyIsConnected false
#define TracyIsStarted false
#define TracySetProgramName(x)
#define TracyFiberEnter(x)
#define TracyFiberEnterHint(x,y)
#define TracyFiberLeave
#endif
// Helper for making sure frames end in all possible control flow path
class FrameMarker
{
const char *m_name;
bool m_started = false;
public:
FrameMarker(const char *name) : m_name(name) {}
~FrameMarker() { end(); }
DISABLE_CLASS_COPY(FrameMarker)
FrameMarker(FrameMarker &&other) noexcept :
m_name(other.m_name), m_started(other.m_started)
{
other.m_started = false;
}
FrameMarker &operator=(FrameMarker &&other) noexcept
{
if (&other != this) {
end();
m_name = other.m_name;
m_started = other.m_started;
other.m_started = false;
}
return *this;
}
FrameMarker &&started() &&
{
if (!m_started) {
FrameMarkStart(m_name);
m_started = true;
}
return std::move(*this);
}
void start()
{
// no move happens, because we drop the reference
(void)std::move(*this).started();
}
void end()
{
if (m_started) {
m_started = false;
FrameMarkEnd(m_name);
}
}
};