diff --git a/CCPMemory.cpp b/CCPMemory.cpp index ab7ec90..9107277 100644 --- a/CCPMemory.cpp +++ b/CCPMemory.cpp @@ -3,6 +3,7 @@ #include "include/CCPMemory.h" #include "include/CCPMemoryTracker.h" #include "include/CCPAssert.h" +#include "include/CcpSecureCrt.h" #include "include/CcpTelemetry.h" #include "CcpMemoryTrackerMutex.h" diff --git a/CMakeLists.txt b/CMakeLists.txt index c0ec58a..f199862 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,6 +20,7 @@ set(SRC_FILES CcpCore.cpp CCPAssert.cpp CCPCallstack.cpp + CcpColorConstants.cpp CCPDefines.cpp CcpFileUtils.cpp CCPHash.cpp @@ -177,6 +178,16 @@ if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME) set(SPHINX_SOURCE ${CMAKE_CURRENT_SOURCE_DIR}/doc/source) set(SPHINX_BUILD ${CMAKE_CURRENT_BINARY_DIR}/doc/build) + get_target_property(CORE_COMPILE_DEFINITIONS CcpCore COMPILE_DEFINITIONS) + list(TRANSFORM CORE_COMPILE_DEFINITIONS GENEX_STRIP) + list(APPEND CORE_COMPILE_DEFINITIONS + CARBON_CORE_API= + CARBON_CORE_DEPRECATED_EXPORT= + NOINLINE= + CCPLOG_PRINTF_FORMAT= + ) + list(JOIN CORE_COMPILE_DEFINITIONS " \\\n" CORE_COMPILE_DEFINITIONS) + create_carbon_docs_sphinx_target( PYTHON_EXE ${Python3_EXECUTABLE} VENV_NAME docs_generation_venv @@ -187,6 +198,7 @@ if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME) SPHINX_TARGET_NAME Sphinx DOXYGEN_TARGET_NAME Doxygen INSTALL_DESTINATION documentation/carbon-core + EXTRA_COMPILE_DEFINITIONS ${CORE_COMPILE_DEFINITIONS} ) endif() diff --git a/CcpColorConstants.cpp b/CcpColorConstants.cpp new file mode 100644 index 0000000..56a41dc --- /dev/null +++ b/CcpColorConstants.cpp @@ -0,0 +1,152 @@ +// Copyright © 2026 CCP ehf. + +#include "include/CcpColorConstants.h" + +std::string_view CcpColorToString( CcpColor color ) +{ + // For colors sharing the same RGB value (Aqua/Cyan, Fuchsia/Magenta, and the + // various *Gray/*Grey pairs), all names sharing that value are returned. + switch( color ) + { + case CcpColor::AliceBlue: return "AliceBlue"; + case CcpColor::AntiqueWhite: return "AntiqueWhite"; + case CcpColor::Aqua: return "Aqua / Cyan"; + case CcpColor::Aquamarine: return "Aquamarine"; + case CcpColor::Azure: return "Azure"; + case CcpColor::Beige: return "Beige"; + case CcpColor::Bisque: return "Bisque"; + case CcpColor::Black: return "Black"; + case CcpColor::BlanchedAlmond: return "BlanchedAlmond"; + case CcpColor::Blue: return "Blue"; + case CcpColor::BlueViolet: return "BlueViolet"; + case CcpColor::Brown: return "Brown"; + case CcpColor::BurlyWood: return "BurlyWood"; + case CcpColor::CadetBlue: return "CadetBlue"; + case CcpColor::Chartreuse: return "Chartreuse"; + case CcpColor::Chocolate: return "Chocolate"; + case CcpColor::Coral: return "Coral"; + case CcpColor::CornflowerBlue: return "CornflowerBlue"; + case CcpColor::Cornsilk: return "Cornsilk"; + case CcpColor::Crimson: return "Crimson"; + case CcpColor::DarkBlue: return "DarkBlue"; + case CcpColor::DarkCyan: return "DarkCyan"; + case CcpColor::DarkGoldenrod: return "DarkGoldenrod"; + case CcpColor::DarkGray: return "DarkGray / DarkGrey"; + case CcpColor::DarkGreen: return "DarkGreen"; + case CcpColor::DarkKhaki: return "DarkKhaki"; + case CcpColor::DarkMagenta: return "DarkMagenta"; + case CcpColor::DarkOliveGreen: return "DarkOliveGreen"; + case CcpColor::DarkOrange: return "DarkOrange"; + case CcpColor::DarkOrchid: return "DarkOrchid"; + case CcpColor::DarkRed: return "DarkRed"; + case CcpColor::DarkSalmon: return "DarkSalmon"; + case CcpColor::DarkSeaGreen: return "DarkSeaGreen"; + case CcpColor::DarkSlateBlue: return "DarkSlateBlue"; + case CcpColor::DarkSlateGray: return "DarkSlateGray / DarkSlateGrey"; + case CcpColor::DarkTurquoise: return "DarkTurquoise"; + case CcpColor::DarkViolet: return "DarkViolet"; + case CcpColor::DeepPink: return "DeepPink"; + case CcpColor::DeepSkyBlue: return "DeepSkyBlue"; + case CcpColor::DimGray: return "DimGray / DimGrey"; + case CcpColor::DodgerBlue: return "DodgerBlue"; + case CcpColor::FireBrick: return "FireBrick"; + case CcpColor::FloralWhite: return "FloralWhite"; + case CcpColor::ForestGreen: return "ForestGreen"; + case CcpColor::Fuchsia: return "Fuchsia / Magenta"; + case CcpColor::Gainsboro: return "Gainsboro"; + case CcpColor::GhostWhite: return "GhostWhite"; + case CcpColor::Gold: return "Gold"; + case CcpColor::Goldenrod: return "Goldenrod"; + case CcpColor::Gray: return "Gray / Grey"; + case CcpColor::Green: return "Green"; + case CcpColor::GreenYellow: return "GreenYellow"; + case CcpColor::Honeydew: return "Honeydew"; + case CcpColor::HotPink: return "HotPink"; + case CcpColor::IndianRed: return "IndianRed"; + case CcpColor::Indigo: return "Indigo"; + case CcpColor::Ivory: return "Ivory"; + case CcpColor::Khaki: return "Khaki"; + case CcpColor::Lavender: return "Lavender"; + case CcpColor::LavenderBlush: return "LavenderBlush"; + case CcpColor::LawnGreen: return "LawnGreen"; + case CcpColor::LemonChiffon: return "LemonChiffon"; + case CcpColor::LightBlue: return "LightBlue"; + case CcpColor::LightCoral: return "LightCoral"; + case CcpColor::LightCyan: return "LightCyan"; + case CcpColor::LightGoldenrodYellow: return "LightGoldenrodYellow"; + case CcpColor::LightGray: return "LightGray / LightGrey"; + case CcpColor::LightGreen: return "LightGreen"; + case CcpColor::LightPink: return "LightPink"; + case CcpColor::LightSalmon: return "LightSalmon"; + case CcpColor::LightSeaGreen: return "LightSeaGreen"; + case CcpColor::LightSkyBlue: return "LightSkyBlue"; + case CcpColor::LightSlateGray: return "LightSlateGray / LightSlateGrey"; + case CcpColor::LightSteelBlue: return "LightSteelBlue"; + case CcpColor::LightYellow: return "LightYellow"; + case CcpColor::Lime: return "Lime"; + case CcpColor::LimeGreen: return "LimeGreen"; + case CcpColor::Linen: return "Linen"; + case CcpColor::Maroon: return "Maroon"; + case CcpColor::MediumAquamarine: return "MediumAquamarine"; + case CcpColor::MediumBlue: return "MediumBlue"; + case CcpColor::MediumOrchid: return "MediumOrchid"; + case CcpColor::MediumPurple: return "MediumPurple"; + case CcpColor::MediumSeaGreen: return "MediumSeaGreen"; + case CcpColor::MediumSlateBlue: return "MediumSlateBlue"; + case CcpColor::MediumSpringGreen: return "MediumSpringGreen"; + case CcpColor::MediumTurquoise: return "MediumTurquoise"; + case CcpColor::MediumVioletRed: return "MediumVioletRed"; + case CcpColor::MidnightBlue: return "MidnightBlue"; + case CcpColor::MintCream: return "MintCream"; + case CcpColor::MistyRose: return "MistyRose"; + case CcpColor::Moccasin: return "Moccasin"; + case CcpColor::NavajoWhite: return "NavajoWhite"; + case CcpColor::Navy: return "Navy"; + case CcpColor::OldLace: return "OldLace"; + case CcpColor::Olive: return "Olive"; + case CcpColor::OliveDrab: return "OliveDrab"; + case CcpColor::Orange: return "Orange"; + case CcpColor::OrangeRed: return "OrangeRed"; + case CcpColor::Orchid: return "Orchid"; + case CcpColor::PaleGoldenrod: return "PaleGoldenrod"; + case CcpColor::PaleGreen: return "PaleGreen"; + case CcpColor::PaleTurquoise: return "PaleTurquoise"; + case CcpColor::PaleVioletRed: return "PaleVioletRed"; + case CcpColor::PapayaWhip: return "PapayaWhip"; + case CcpColor::PeachPuff: return "PeachPuff"; + case CcpColor::Peru: return "Peru"; + case CcpColor::Pink: return "Pink"; + case CcpColor::Plum: return "Plum"; + case CcpColor::PowderBlue: return "PowderBlue"; + case CcpColor::Purple: return "Purple"; + case CcpColor::RebeccaPurple: return "RebeccaPurple"; + case CcpColor::Red: return "Red"; + case CcpColor::RosyBrown: return "RosyBrown"; + case CcpColor::RoyalBlue: return "RoyalBlue"; + case CcpColor::SaddleBrown: return "SaddleBrown"; + case CcpColor::Salmon: return "Salmon"; + case CcpColor::SandyBrown: return "SandyBrown"; + case CcpColor::SeaGreen: return "SeaGreen"; + case CcpColor::SeaShell: return "SeaShell"; + case CcpColor::Sienna: return "Sienna"; + case CcpColor::Silver: return "Silver"; + case CcpColor::SkyBlue: return "SkyBlue"; + case CcpColor::SlateBlue: return "SlateBlue"; + case CcpColor::SlateGray: return "SlateGray / SlateGrey"; + case CcpColor::Snow: return "Snow"; + case CcpColor::SpringGreen: return "SpringGreen"; + case CcpColor::SteelBlue: return "SteelBlue"; + case CcpColor::Tan: return "Tan"; + case CcpColor::Teal: return "Teal"; + case CcpColor::Thistle: return "Thistle"; + case CcpColor::Tomato: return "Tomato"; + case CcpColor::Turquoise: return "Turquoise"; + case CcpColor::Violet: return "Violet"; + case CcpColor::Wheat: return "Wheat"; + case CcpColor::White: return "White"; + case CcpColor::WhiteSmoke: return "WhiteSmoke"; + case CcpColor::Yellow: return "Yellow"; + case CcpColor::YellowGreen: return "YellowGreen"; + default: return "ColorNotFound"; + } +} diff --git a/CcpMutex.cpp b/CcpMutex.cpp index d6b8c52..4832f74 100644 --- a/CcpMutex.cpp +++ b/CcpMutex.cpp @@ -5,7 +5,10 @@ #include "include/CcpAtomic.h" #include "include/CcpMutex.h" #include "include/CcpThread.h" + +#if CCP_TELEMETRY_ENABLED #include "tracy/TracyC.h" +#endif namespace diff --git a/CcpSemaphore.cpp b/CcpSemaphore.cpp index 112836e..6a920b6 100644 --- a/CcpSemaphore.cpp +++ b/CcpSemaphore.cpp @@ -2,7 +2,9 @@ #include "include/CcpSemaphore.h" +#if CCP_TELEMETRY_ENABLED #include "tracy/TracyC.h" +#endif // OS specific includes: #ifdef _WIN32 @@ -16,6 +18,8 @@ using NativeHandle = sem_t; #endif +#include "include/CcpSecureCrt.h" + struct CcpSemaphore::Private { #if CCP_TELEMETRY_ENABLED @@ -60,6 +64,7 @@ CcpSemaphore::CcpSemaphore( const char* semaphoreName, uint32_t initialCount, ui namespace { +#if CCP_TELEMETRY_ENABLED void AnnounceSemaphoreToTelemetry( TracyCLockCtx& ctx, const char* name ) { // Lazy initialization pattern because there are many instances which are created statically, @@ -72,6 +77,7 @@ namespace } } } +#endif } // Preferred constructor, with default value overloads (see header file for details) diff --git a/CcpStatistics.cpp b/CcpStatistics.cpp index faa9dcd..900b18a 100644 --- a/CcpStatistics.cpp +++ b/CcpStatistics.cpp @@ -2,10 +2,12 @@ #include "include/CcpStatistics.h" -#include - #include "CcpTelemetry.h" +#if CCP_TELEMETRY_ENABLED +#include +#endif + static double SCALING_FACTOR_FOR_TIME = 1000000.0; CcpStatistics::EntryArray& CcpStatistics::GetEntryArray() diff --git a/CcpTelemetry.cpp b/CcpTelemetry.cpp index 00c0ae4..6818d85 100644 --- a/CcpTelemetry.cpp +++ b/CcpTelemetry.cpp @@ -1,9 +1,14 @@ // Copyright © 2013 CCP ehf. +#include +#include +#include +#include #include #include #include "include/CCPAssert.h" +#include "include/CcpMutex.h" #include "include/CcpTelemetry.h" #include "include/CcpTime.h" @@ -17,12 +22,61 @@ static CcpLogChannel_t s_ch = CCP_LOG_DEFINE_CHANNEL( "Telemetry" ); #pragma warning(pop) #include +/*! + \def TMCM_GENERAL + \brief Legacy index into the registered profiler category array, denoting the default "general" category. + + This used to be a bitmask value back when categories were selected via a bitmask rather than an index into + `CcpTelemetryGetRegisteredCategories()`. It is kept around for source compatibility with older call sites + that still pass it to the deprecated `TelemetryZone( uint32_t, ... )` constructor. + + \see TMCM_CPP + \see CcpTelemetryCategoryRegister +*/ + +/*! + \def TMCM_CPP + \brief Legacy index into the registered profiler category array, denoting the built-in "cpp" category. + + \see TMCM_GENERAL +*/ + typedef std::set FiberNameStore; struct TelemetryZone::Private { - std::optional telemetryContext; - FiberNameStore::const_iterator fiber; + std::optional telemetryContext; //!< If active, this contains the context object required for the Tracy integration. + FiberNameStore::const_iterator fiber; //!< The fiber the zone belongs to. +}; + +/*! + \struct CcpTelemetryCategory + \brief Opaque, registered category that zones can be tagged with. + + A `CcpTelemetryCategory` groups related `TelemetryZone` instances (e.g. "rendering", "physics", "scripting") + so that they can be shown in a distinct color and selectively captured at runtime via + `CcpTelemetrySetActiveCategories()`. Instances are only ever handed out by reference from + `CcpTelemetryCategoryRegister()` and `CcpTelemetryGetRegisteredCategories()` - the type itself is opaque to + consumers of the header, which only ever see a forward declaration. + + \par Example + \code + auto [category, ok] = CcpTelemetryCategoryRegister( "physics", CcpColor::Orange ); + if( ok ) + { + TelemetryZone zone( category, "StepSimulation", __FILE__, __LINE__ ); + // ... simulate ... + } + \endcode + + \see CcpTelemetryCategoryRegister + \see CcpTelemetrySetActiveCategories +*/ +struct CcpTelemetryCategory +{ + std::string name; + CcpColor color{CcpColor::White}; + uint64_t captureBit{0}; }; enum ProfilerState { @@ -54,7 +108,7 @@ thread_local TaskletZoneStore::iterator t_activeTaskletZoneStore{ t_taskletZoneS thread_local std::set t_manuallyTrackedZones; // Keep track of zones created through `CcpTelemetryEnterZone` to ensure that we only pop off the zone store's stack when leaving a manually created zone constexpr std::chrono::milliseconds s_cleanupDelay{5000}; -std::queue> s_fiberEraseMap; // Map of fibers scheduled for erasure +std::map s_fiberEraseMap; // Map of fibers scheduled for erasure typedef TrackableStdMap> MutexNameMap_t; typedef TrackableStdMap ThreadNameMap_t; @@ -83,55 +137,428 @@ namespace static EventHandlerVector_t s_eventHandlers( "CcpTelemetry/s_eventHandlers" ); return s_eventHandlers; } + + // ------------------------------- + // ProfilerCategory specifics: + // ------------------------------- + constexpr size_t CCP_TELEMETRY_CATEGORIES_MAX{64}; + + CcpMutex s_profilerCategoryRegistryLock( "CcpTelemetry", "ProfilerCategoryRegistry" ); + + // Fixed size array of registered ProfilerCategories. + std::array, CCP_TELEMETRY_CATEGORIES_MAX> s_registeredProfilerCategories{ + CcpTelemetryCategory{ "general", CcpColor::SteelBlue, TMCM_GENERAL }, // legacy definition from TMCM_GENERAL, used to be a bitmask, but can now be treated as index into this array + CcpTelemetryCategory{ "cpp", CcpColor::Yellow, TMCM_CPP }, // legacy value from TMCM_CPP, used to be a bitmask, but can now be treated as index into this array + CcpTelemetryCategory{ "core", CcpColor::LightGreen, 1<<2 } + }; + + uint64_t s_profilerCategoryCaptureMask{0}; + + bool IsProfilerCategoryActive( uint64_t captureBit ) + { + return ( s_profilerCategoryCaptureMask & captureBit ) != 0; + } +} + +/*! + \brief Compares two `CcpTelemetryCategory` instances for equality. + + Categories are considered equal purely based on their registered name; the color and internal capture bit + are not taken into account. + + \param lhs First category to compare. + \param rhs Second category to compare. + \return `true` if both categories share the same name, `false` otherwise. +*/ +bool operator==( const CcpTelemetryCategory& lhs, const CcpTelemetryCategory& rhs ) +{ + // Profiler Categories need to be unique by name only + return lhs.name == rhs.name; +} + +/*! + \brief Returns the display name a `CcpTelemetryCategory` was registered with. + + \param category Category to query, as obtained from `CcpTelemetryCategoryRegister()` or + `CcpTelemetryGetRegisteredCategories()`. + \return The category's name. + + \par Example + \code + auto categories = CcpTelemetryGetRegisteredCategories(); + for( const CcpTelemetryCategory& category : categories ) + { + printf( "%s\n", CcpTelemetryCategoryGetName( category ).c_str() ); + } + \endcode +*/ +const std::string& CcpTelemetryCategoryGetName( const CcpTelemetryCategory& category ) +{ + return category.name; +} + +/*! + \brief Returns the color a `CcpTelemetryCategory` should be rendered with in the profiler UI. + + \param category Category to query, as obtained from `CcpTelemetryCategoryRegister()` or + `CcpTelemetryGetRegisteredCategories()`. + \return The category's display color. +*/ +CcpColor CcpTelemetryCategoryGetColor( const CcpTelemetryCategory& category ) +{ + return category.color; +} + +/*! + \brief Registers a new telemetry category, or returns the existing one if `name` is already registered. + + Categories are used to group `TelemetryZone` instances (e.g. by subsystem) and to let consumers selectively + enable capture for only a subset of zones via `CcpTelemetrySetActiveCategories()`. Up to + `PROFILER_CATEGORIES_MAX` (64) categories can be registered for the lifetime of the process; three of them + ("general", "cpp", "core") are pre-registered by default. + + \param name Unique, human-readable name for the category. Must not be empty. + \param color Color the category's zones should be rendered with. Defaults to `CcpColor::SteelBlue`. + \return A pair of a reference to the (newly or previously) registered category, and a `bool` that is `true` + on success. On failure - an empty `name`, or all 64 slots already taken - the returned reference + refers to a static, empty placeholder category and the `bool` is `false`. + + \par Example + \code + auto [category, ok] = CcpTelemetryCategoryRegister( "rendering", CcpColor::Green ); + if( ok ) + { + TelemetryZone zone( category, "DrawFrame", __FILE__, __LINE__ ); + } + + // Registering the same name again simply returns the existing category. + auto [same, stillOk] = CcpTelemetryCategoryRegister( "rendering" ); + \endcode + + \see CcpTelemetryGetRegisteredCategories + \see CcpTelemetrySetActiveCategories +*/ +std::pair CcpTelemetryCategoryRegister( const std::string& name, CcpColor color ) +{ + static const CcpTelemetryCategory empty; + CcpAutoMutex lock( s_profilerCategoryRegistryLock ); + + if( name.empty() ) + { + CCP_LOGERR_CH( s_ch, "Cannot register a Profiler Category without a name" ); + return { empty, false }; + } + + for( uint64_t i = 0; i < s_registeredProfilerCategories.size(); ++i ) + { + auto& entry = s_registeredProfilerCategories[i]; + + if (!entry) + { + entry = { name, color, 1ULL << i }; + CCP_LOG_CH( s_ch, "Registered a new Profiler Category for '%s'", entry->name.c_str() ); + return { *entry, true }; + } + + if ( entry->name == name ) + { + CCP_LOGERR_CH( s_ch, "A Profiler Category with the name %s already exists, returning existing entry.", entry->name.c_str() ); + return { *entry, true }; + } + } + + return { empty, false }; +} + +/*! + \brief Returns all telemetry categories registered so far, in registration order. + + The result always includes the built-in "general", "cpp" and "core" categories, followed by any categories + registered via `CcpTelemetryCategoryRegister()`. + + \return A vector of references to the registered categories. The references remain valid for the lifetime + of the process, since categories are never unregistered. + + \par Example + \code + for( const CcpTelemetryCategory& category : CcpTelemetryGetRegisteredCategories() ) + { + printf( "Registered category: %s\n", CcpTelemetryCategoryGetName( category ).c_str() ); + } + \endcode +*/ +CcpTelemetryCategories CcpTelemetryGetRegisteredCategories() +{ + CcpAutoMutex lock( s_profilerCategoryRegistryLock ); + + CcpTelemetryCategories result; + result.reserve( s_registeredProfilerCategories.size() ); + for( const auto& registeredEntry : s_registeredProfilerCategories ) + { + if( ! registeredEntry ) + { + break; + } + + result.emplace_back( *registeredEntry ); + } + return result; } +/*! + \brief Replaces the set of telemetry categories that are actively captured. + + Only zones tagged with an active category are actually recorded by the connected profiler; zones tagged + with an inactive (or unregistered) category are cheap no-ops. Pass an empty vector to stop capturing any + category. + + \param categories Names of the categories to activate, as previously passed to `CcpTelemetryCategoryRegister()`. + Unknown names are silently ignored. Must not contain more than `PROFILER_CATEGORIES_MAX` + (64) entries. + \return `true` on success, `false` if more than 64 names were passed in - in which case the active category + set is left unchanged. + + \par Example + \code + CcpTelemetrySetActiveCategories( { "cpp", "rendering" } ); + // ... zones tagged "cpp" or "rendering" are now captured ... + CcpTelemetrySetActiveCategories( {} ); // stop capturing any category + \endcode + + \see CcpTelemetryGetActiveCategories +*/ +bool CcpTelemetrySetActiveCategories( const CcpTelemetryCategories& categories ) +{ + // Guard access to all ProfilerCategories members + CcpAutoMutex lock( s_profilerCategoryRegistryLock ); + + if ( categories.size() > s_registeredProfilerCategories.size() ) + { + CCP_LOGERR_CH( s_ch, "Failed setting active Profiler Category because more %lu maskNames were passed in, but only %lu are allowed", categories.size(), s_registeredProfilerCategories.size() ); + return false; + } + + uint64_t newActiveProfilerCategory = 0; + for( const auto& category : categories ) + { + newActiveProfilerCategory |= category.get().captureBit; + } + + s_profilerCategoryCaptureMask = newActiveProfilerCategory; + + return true; +} + +/*! + \brief Returns the telemetry categories that are currently active for capture. + + \return A vector of references to the categories most recently activated via `CcpTelemetrySetActiveCategories()`. + Empty if no category is currently active. + + \see CcpTelemetrySetActiveCategories +*/ +CcpTelemetryCategories CcpTelemetryGetActiveCategories() +{ + CcpTelemetryCategories result; + size_t index{0}; + for ( const auto& registeredEntry : s_registeredProfilerCategories ) + { + uint64_t currentMaskBit = 1ULL << index; + + if ( registeredEntry && ( s_profilerCategoryCaptureMask & currentMaskBit ) != 0 ) + { + result.emplace_back( *registeredEntry ); + } + + ++index; + } + return result; +} + +/*! + \brief Checks whether the telemetry integration is both started and connected to a profiler client. + + This is the state in which zones and locks are actually captured. Compare with `CcpTelemetryIsStarted()`, + which only reflects the internal state machine and can be `true` while a connection is still pending. + + \return `true` if telemetry is started and a profiler client is connected, `false` otherwise. +*/ bool CcpTelemetryIsConnected() { return TracyIsStarted && TracyIsConnected && s_profilerState.load( std::memory_order_acquire ) == ProfilerState::Started; } +/*! + \brief Checks whether telemetry has been requested to start but is still waiting for a profiler client to connect. + + \return `true` if the telemetry server is listening but no client has connected yet, `false` otherwise. +*/ bool CcpTelemetryIsConnectionRequested() { return TracyIsStarted && !TracyIsConnected && s_profilerState.load( std::memory_order_acquire ) == ProfilerState::StartRequested; } +/*! + \brief Checks whether the telemetry integration has fully started (server running and profiler client connected). + + \return `true` if telemetry is in the `Started` state, `false` otherwise. + + \see CcpTelemetryIsConnected + \see CcpTelemetryIsStopped +*/ bool CcpTelemetryIsStarted() { return s_profilerState.load( std::memory_order_acquire ) == ProfilerState::Started; } +/*! + \brief Checks whether the telemetry integration is fully stopped. + + \return `true` if telemetry is in the `Stopped` state, `false` otherwise. + + \see CcpTelemetryIsStarted + \see CcpStopTelemetry +*/ bool CcpTelemetryIsStopped() { return s_profilerState.load( std::memory_order_acquire ) == ProfilerState::Stopped; } +/*! + \brief Checks whether the current telemetry session was configured to track memory allocations. + + \return The value of `CcpTelemetryConfig::trackMemoryAllocations` that was passed to `CcpStartTelemetry()`. + + \see CcpTelemetryTrackAllocation + \see CcpTelemetryTrackDeallocation +*/ bool CcpTelemetryMemoryTrackingIsEnabled() { return s_config.trackMemoryAllocations; } +/*! + \brief Checks whether the current telemetry session was configured to track lock contention. + + \return The value of `CcpTelemetryConfig::trackLocks` that was passed to `CcpStartTelemetry()`. +*/ bool CcpTelemetryLockTrackingIsEnabled() { return s_config.trackLocks; } +/*! + \brief Announces a `CcpMutex` to the telemetry integration so that lock contention can be tracked under its + given owner and name. + + This is called automatically by `CcpMutex`'s constructor - callers do not need to invoke this directly. + + \param m Mutex being announced. + \param owner Name of the subsystem or class that owns the mutex. + \param name Human-readable name for the mutex. +*/ void CcpRegisterMutex( class CcpMutex& m, const char* owner, const char* name ) { MutexNameMap_t& mutexNames = GetMutexNameMap(); mutexNames[&m] = std::make_pair( owner, name ); } +/*! + \brief Assigns a human-readable name to a thread, shown by the connected profiler. + + \param threadId Identifier of the thread being named, as returned by the platform thread APIs. + \param name Human-readable name to associate with the thread. + + \par Example + \code + CcpRegisterThread( CcpThreadGetCurrentId(), "RenderThread" ); + \endcode +*/ void CcpRegisterThread( CcpThreadId_t threadId, const char* name ) { ThreadNameMap_t& threadNames = GetThreadNameMap(); threadNames[threadId] = name; } +/*! + \brief Starts the telemetry integration for the given server or dump path. + + \deprecated Use `CcpStartTelemetry( const CcpTelemetryConfig& config )` instead. + + \param serverOrDumpPath Kept for source compatibility; forwarded as the application name of a default-constructed + `CcpTelemetryConfig`. + \param connectionType Unused, kept for source compatibility. + \param maxThreadCount Unused, kept for source compatibility. + \return See `CcpStartTelemetry( const CcpTelemetryConfig& )`. +*/ bool CcpStartTelemetry( const char* serverOrDumpPath, int connectionType, uint32_t maxThreadCount ) { return CcpStartTelemetry( { serverOrDumpPath } ); } +/*! + \struct CcpTelemetryConfig + \brief Configuration passed to `CcpStartTelemetry( const CcpTelemetryConfig& )`. + + \var CcpTelemetryConfig::applicationName + Name of the application, shown in the connected profiler. + + \var CcpTelemetryConfig::captureDuration + If non-zero, telemetry automatically stops itself once this much time has passed since it started (see + `CcpTelemetryRemainingCaptureDuration()`). A value of zero (the default) means "capture indefinitely, until + `CcpStopTelemetry()` is called explicitly". + + \var CcpTelemetryConfig::trackMemoryAllocations + Whether allocations reported via `CcpTelemetryTrackAllocation()` / `CcpTelemetryTrackDeallocation()` should + actually be forwarded to the profiler (see `CcpTelemetryMemoryTrackingIsEnabled()`). + + \var CcpTelemetryConfig::trackLocks + Whether `CcpMutex` contention should be reported to the profiler (see `CcpTelemetryLockTrackingIsEnabled()`). + + \par Example + \code + CcpTelemetryConfig config; + config.applicationName = "MyGame"; + config.captureDuration = std::chrono::seconds( 30 ); // stop automatically after 30 seconds + config.trackMemoryAllocations = true; + config.trackLocks = true; + CcpStartTelemetry( config ); + \endcode + + \see CcpStartTelemetry +*/ + +/*! + \brief Requests that the telemetry integration start listening for a profiler connection. + + Starting is asynchronous: this function only records the request and applies `config`. The actual + transition to `CcpTelemetryIsStarted()` happens on a subsequent call to `CcpTelemetryTick()`, once the + telemetry server is listening and a profiler client has connected. + + \param config Application name, optional timed-capture duration, and memory/lock tracking flags to use for + this session. See `CcpTelemetryConfig`. + \return `true` if the request was accepted, `false` if telemetry is already started or a start is already + in progress. + + \par Example + \code + CcpTelemetryConfig config; + config.applicationName = "MyGame"; + config.trackMemoryAllocations = true; + config.trackLocks = true; + CcpStartTelemetry( config ); + + // Somewhere in the main loop: + while( isRunning ) + { + CcpTelemetryTick(); + } + \endcode + + \see CcpTelemetryConfig + \see CcpStopTelemetry + \see CcpTelemetryTick +*/ bool CcpStartTelemetry( const CcpTelemetryConfig& config ) { if( s_profilerState.load( std::memory_order_acquire ) == ProfilerState::Started || s_profilerState.load( std::memory_order_acquire ) == ProfilerState::StartRequested ) @@ -148,6 +575,15 @@ bool CcpStartTelemetry( const CcpTelemetryConfig& config ) return true; } +/*! + \brief Requests that the telemetry integration stop and disconnect from the profiler client. + + Stopping is asynchronous, just like starting: this only records the request. The actual transition to + `CcpTelemetryIsStopped()` happens on a subsequent call to `CcpTelemetryTick()`. + + \see CcpStartTelemetry + \see CcpTelemetryIsStopped +*/ void CcpStopTelemetry() { if( s_profilerState.load( std::memory_order_acquire ) == ProfilerState::Stopped || s_profilerState.load( std::memory_order_acquire ) == ProfilerState::StopRequested ) @@ -159,6 +595,28 @@ void CcpStopTelemetry() s_profilerState.store( ProfilerState::StopRequested, std::memory_order_release ); } +/*! + \brief Advances the telemetry integration's internal state machine by one tick. + + This must be called regularly (typically once per frame, or in a dedicated polling loop) for + `CcpStartTelemetry()` and `CcpStopTelemetry()` requests to actually take effect, for the connected profiler + to receive a frame mark, for pending fiber-name cleanups to run, and for a configured + `CcpTelemetryConfig::captureDuration` to be enforced. + + \par Example + \code + CcpStartTelemetry( config ); + while( isRunning ) + { + CcpTelemetryTick(); + // ... rest of the frame ... + } + \endcode + + \see CcpStartTelemetry + \see CcpStopTelemetry + \see CcpTelemetryGetTickCount +*/ void CcpTelemetryTick() { switch ( s_profilerState.load(std::memory_order_acquire) ) @@ -198,23 +656,24 @@ void CcpTelemetryTick() FrameMark; ++s_telemetryTick; - // Give the profiler a few seconds to receive information from the fiber name store before deallocating - // the underlying string - if ( !s_fiberEraseMap.empty() ) + auto now = std::chrono::steady_clock::now(); + + // Check if there are any pending fiber name erases + for (auto it = s_fiberEraseMap.begin(); it != s_fiberEraseMap.end(); ) { - auto now = std::chrono::steady_clock::now(); - auto elem = s_fiberEraseMap.front(); - while ( !s_fiberEraseMap.empty() && elem.second >= now ) + if ( now >= it->second ) { - s_fiberNameStore.erase( elem.first ); - s_fiberEraseMap.pop(); - elem = s_fiberEraseMap.front(); + s_fiberNameStore.erase( it->first ); + it = s_fiberEraseMap.erase( it ); + } else + { + ++it; } } if( s_config.captureDuration != std::chrono::milliseconds::zero() ) // Check if we have passed our timed sample time { - auto timeSinceStart = std::chrono::steady_clock::now() - s_profilerStartTime; + auto timeSinceStart = now - s_profilerStartTime; if( timeSinceStart >= s_config.captureDuration ) { CCP_LOG_CH( s_ch, "Finalizing timed profiler run" ); @@ -251,11 +710,33 @@ void CcpTelemetryTick() } } +/*! + \brief Returns how much longer the current timed capture will run. + + Only meaningful when `CcpTelemetryConfig::captureDuration` was set to a non-zero value in the config passed + to `CcpStartTelemetry()`; otherwise always returns zero. + + \return The remaining capture duration, clamped to zero once it has elapsed. + + \see CcpTelemetryConfig +*/ std::chrono::milliseconds CcpTelemetryRemainingCaptureDuration() { return std::max( std::chrono::milliseconds( 0 ), s_config.captureDuration - std::chrono::duration_cast( std::chrono::steady_clock::now() - s_profilerStartTime ) ); } +/*! + \brief Reports a memory allocation to the connected profiler. + + Intended to be called from custom allocators (see `CCPMemory.cpp`) rather than directly by application + code. A no-op unless both `CcpTelemetryMemoryTrackingIsEnabled()` and `CcpTelemetryIsConnected()` are `true`. + + \param p Address of the allocated memory block. + \param size Size, in bytes, of the allocated memory block. + + \see CcpTelemetryTrackDeallocation + \see CcpTelemetryMemoryTrackingIsEnabled +*/ void CcpTelemetryTrackAllocation( void* p, size_t size ) { if ( CcpTelemetryMemoryTrackingIsEnabled() && CcpTelemetryIsConnected() ) { @@ -263,6 +744,17 @@ void CcpTelemetryTrackAllocation( void* p, size_t size ) } } +/*! + \brief Reports a memory deallocation to the connected profiler. + + Intended to be called from custom allocators (see `CCPMemory.cpp`) rather than directly by application + code. A no-op if `p` is `nullptr`, or unless both `CcpTelemetryMemoryTrackingIsEnabled()` and + `CcpTelemetryIsConnected()` are `true`. + + \param p Address of the memory block being freed. + + \see CcpTelemetryTrackAllocation +*/ void CcpTelemetryTrackDeallocation( void* p ) { if ( p && CcpTelemetryMemoryTrackingIsEnabled() && CcpTelemetryIsConnected() ) @@ -271,11 +763,66 @@ void CcpTelemetryTrackDeallocation( void* p ) } } +/*! + \brief Returns a monotonically increasing counter of `CcpTelemetryTick()` calls since the last + `CcpStartTelemetry()`. + + \return The current tick count. Reset to `1` every time `CcpStartTelemetry()` succeeds. + + \see CcpTelemetryTick +*/ uint32_t CcpTelemetryGetTickCount() { return s_telemetryTick; } +/*! + \enum CcpTelemetryEvent + \brief Events reported to handlers registered via `CcpRegisterTelemetryEventHandler()`. + + \var CCP_TELEMETRY_STARTED + Telemetry has started and connected to a profiler client. + + \var CCP_TELEMETRY_STOPPED + Telemetry has stopped and disconnected from the profiler client. + + \see CcpRegisterTelemetryEventHandler +*/ + +/*! + \typedef CcpOnTelemetryEventHandler + \brief Callback signature for handlers registered via `CcpRegisterTelemetryEventHandler()`. + + \see CcpRegisterTelemetryEventHandler + \see CcpTelemetryEvent +*/ + +/*! + \brief Registers a callback to be invoked whenever telemetry starts or stops. + + If telemetry is already connected at the time of registration, `handler` is invoked immediately with + `CCP_TELEMETRY_STARTED`. + + \param handler Callback to invoke on telemetry start/stop events. + \param userData Opaque pointer passed back to `handler` unchanged; use it to disambiguate multiple + registrations of the same function pointer. + + \par Example + \code + void OnTelemetryEvent( CcpTelemetryEvent event, void* userData ) + { + if( event == CCP_TELEMETRY_STARTED ) + { + printf( "Telemetry connected\n" ); + } + } + + CcpRegisterTelemetryEventHandler( &OnTelemetryEvent, nullptr ); + \endcode + + \see CcpUnregisterTelemetryEventHandler + \see CcpTelemetryEvent +*/ void CcpRegisterTelemetryEventHandler( CcpOnTelemetryEventHandler handler, void* userData ) { GetEventHandlers().push_back( std::make_pair( handler, userData ) ); @@ -285,6 +832,14 @@ void CcpRegisterTelemetryEventHandler( CcpOnTelemetryEventHandler handler, void* } } +/*! + \brief Removes a callback previously registered with `CcpRegisterTelemetryEventHandler()`. + + Both `handler` and `userData` must match the values passed at registration time for the entry to be found. + + \param handler Callback that was registered. + \param userData Opaque pointer that was registered alongside `handler`. +*/ void CcpUnregisterTelemetryEventHandler( CcpOnTelemetryEventHandler handler, void* userData ) { auto& handlers = GetEventHandlers(); @@ -295,6 +850,8 @@ void CcpUnregisterTelemetryEventHandler( CcpOnTelemetryEventHandler handler, voi } } +// Internal helper: switches the active fiber by string-store iterator. Used by the public +// `CcpTelemetrySetActiveFiber( const std::string& )` overload below, once the name has been interned. void CcpTelemetrySetActiveFiber( FiberNameStore::const_iterator elem ) { if ( elem == t_activeFiber ) @@ -329,9 +886,29 @@ void CcpTelemetrySetActiveFiber( FiberNameStore::const_iterator elem ) // CCP_LOG_CH( s_ch, "[Fiber %p] [Store %p] Setting active tasklet zone store", t_activeFiber, t_activeTaskletZoneStore ); } +/*! + \brief Marks `name` as the calling thread's active fiber for zone bookkeeping purposes. + + Zones entered while a fiber is active (e.g. via `TelemetryZone` or the deprecated `CcpTelemetryEnterZone()`) + are tracked per-fiber rather than per-thread, so that fibers which get resumed on different OS threads still + show a coherent call stack in the profiler. Pass an empty string to indicate "no fiber" (the root context). + + \param name Name of the fiber to activate. An empty string deactivates fiber tracking for the calling thread. + + \par Example + \code + CcpTelemetrySetActiveFiber( "WorkerFiber1" ); + // ... work performed on behalf of the fiber ... + CcpTelemetrySetActiveFiber( "" ); // back to the root context + \endcode + + \see CcpTelemetryGetActiveFiber + \see CcpTelemetryRemoveFiber +*/ void CcpTelemetrySetActiveFiber( const std::string& name ) { auto elem = s_fiberNameStore.insert( name ); + s_fiberEraseMap.erase( elem.first ); // cancel any pending deletion of this name // if ( elem.second ) // { // CCP_LOG_CH( s_ch, "Registered new [Fiber %p]", elem.first->c_str() ); @@ -339,6 +916,18 @@ void CcpTelemetrySetActiveFiber( const std::string& name ) CcpTelemetrySetActiveFiber( elem.first ); } +/*! + \brief Schedules a previously named fiber for removal from the fiber name store. + + If `name` is the calling thread's currently active fiber, the active fiber is first reset to "no fiber" + (equivalent to calling `CcpTelemetrySetActiveFiber( "" )`). The name itself is only erased from the + internal store after a short grace period, to avoid invalidating references still in flight. + + \param name Name of the fiber to remove. Empty names are ignored, since the "no fiber" root context is + never removed. + + \see CcpTelemetrySetActiveFiber +*/ void CcpTelemetryRemoveFiber( const std::string& name ) { // Cannot remove nameless fibers @@ -360,12 +949,58 @@ void CcpTelemetryRemoveFiber( const std::string& name ) } } +/*! + \brief Returns the name of the calling thread's currently active fiber. + + \return The name most recently passed to `CcpTelemetrySetActiveFiber()`, or an empty string if no fiber is + active. + + \see CcpTelemetrySetActiveFiber +*/ const std::string& CcpTelemetryGetActiveFiber() { return *t_activeFiber; } -TelemetryZone::TelemetryZone( uint32_t ctx, const char* name, const char* filename, uint32_t lineno, CcpColor color ) : m_impl(std::make_unique()) +/*! + \class TelemetryZone + \brief RAII scope marker that reports a named, timed span of work to the connected profiler. + + Construct a `TelemetryZone` at the start of the scope you want to measure; it automatically ends the zone + when it goes out of scope. Zones are cheap no-ops while telemetry is not started, and are only actually + recorded while their `CcpTelemetryCategory` is active (see `CcpTelemetrySetActiveCategories()`). + + `TelemetryZone` is move-only: it cannot be copied, and can only be moved into e.g. an `std::optional` or a + container, since a zone must have a single, well-defined owner responsible for ending it. + + \par Example + \code + auto [category, ok] = CcpTelemetryCategoryRegister( "physics" ); + + void StepSimulation() + { + TelemetryZone zone( category, "StepSimulation", __FILE__, __LINE__ ); + zone.text( "10 bodies" ); + // ... do work; the zone ends automatically when `zone` goes out of scope ... + } + \endcode + + \see CcpTelemetryCategoryRegister +*/ + +/*! + \brief Constructs a zone using a legacy, bitmask-style category handle. + + \deprecated Use the `TelemetryZone( const CcpTelemetryCategory&, const char*, const char*, uint32_t )` + constructor instead. + + \param handle Legacy category handle, e.g. `TMCM_CPP` or a bit index into the registered category array. + \param name Name of the zone, as shown in the profiler. + \param filename Source file the zone originates from; pass `__FILE__`. + \param lineno Source line the zone originates from; pass `__LINE__`. + \param color Color to render the zone with. Defaults to `CcpColor::SteelBlue`. +*/ +TelemetryZone::TelemetryZone( uint32_t handle, const char* name, const char* filename, uint32_t lineno, CcpColor color ) : m_impl(std::make_unique()) { if( s_profilerState.load( std::memory_order_acquire ) != ProfilerState::Started ) { @@ -374,12 +1009,52 @@ TelemetryZone::TelemetryZone( uint32_t ctx, const char* name, const char* filena CCP_ASSERT( filename != nullptr ); CCP_ASSERT( name != nullptr ); + + const int active = IsProfilerCategoryActive( handle ); auto data = ___tracy_alloc_srcloc( lineno, filename, strlen( filename ), name, strlen( name ), static_cast( color ) ); // CCP_LOG_CH( s_ch, "[Fiber %p] Creating zone %s (%p)", t_activeFiber->c_str(), ret.first->c_str(), this ); m_impl->fiber = t_activeFiber; - m_impl->telemetryContext.emplace( ___tracy_emit_zone_begin_alloc( data, ctx & TMCM_CPP ) ); + m_impl->telemetryContext.emplace( ___tracy_emit_zone_begin_alloc( data, active ) ); +} + +/*! + \brief Constructs a zone tagged with a registered `CcpTelemetryCategory`. + + The zone is only actually recorded by the connected profiler if telemetry is started and `category` is + currently active (see `CcpTelemetrySetActiveCategories()`); otherwise this constructor is a cheap no-op. + + \param category Category to tag the zone with, as obtained from `CcpTelemetryCategoryRegister()`. + \param name Name of the zone, as shown in the profiler. + \param filename Source file the zone originates from; pass `__FILE__`. + \param lineno Source line the zone originates from; pass `__LINE__`. + + \par Example + \code + auto [category, ok] = CcpTelemetryCategoryRegister( "rendering" ); + TelemetryZone zone( category, "DrawFrame", __FILE__, __LINE__ ); + \endcode +*/ +TelemetryZone::TelemetryZone( const CcpTelemetryCategory& category, const char* name, const char* filename, uint32_t lineno ) : m_impl( std::make_unique() ) +{ + if( s_profilerState.load( std::memory_order_acquire ) != ProfilerState::Started ) + { + return; + } + + const int active = IsProfilerCategoryActive( category.captureBit ); + auto data = ___tracy_alloc_srcloc( lineno, filename, strlen( filename ), name, strlen( name ), static_cast( category.color ) ); + // CCP_LOG_CH( s_ch, "[Fiber %p] Creating zone %s (%p)", t_activeFiber->c_str(), ret.first->c_str(), this ); + m_impl->fiber = t_activeFiber; + m_impl->telemetryContext.emplace( ___tracy_emit_zone_begin_alloc( data, active ) ); } +/*! + \brief Transfers ownership of an in-flight zone from `other` to the newly constructed instance. + + After the move, `other` no longer ends any zone on destruction. + + \param other Zone to move from. +*/ TelemetryZone::TelemetryZone( TelemetryZone&& other ) noexcept : m_impl( std::make_unique() ) { m_impl->fiber = other.m_impl->fiber; @@ -389,6 +1064,12 @@ TelemetryZone::TelemetryZone( TelemetryZone&& other ) noexcept : m_impl( std::ma // CCP_LOG_CH( s_ch, "[Fiber %p] Moving zone %p (fiber=%s) to new zone %p (fiber=%s)", t_activeFiber->c_str(), &other, other.m_impl->fiber->c_str(), this, m_impl->fiber->c_str() ); } +/*! + \brief Ends the zone, if one is still active on this instance. + + Ending happens on whichever fiber the zone was originally started on, temporarily switching the active + fiber back if the calling thread has since moved on to a different one. +*/ TelemetryZone::~TelemetryZone() { // Notify Tracy of all zones ended with a valid context, regardless of profiler state @@ -405,6 +1086,19 @@ TelemetryZone::~TelemetryZone() CcpTelemetrySetActiveFiber( previous ); } +/*! + \brief Attaches a free-form text annotation to the zone, visible alongside it in the profiler. + + A no-op unless the zone is currently active (telemetry started and its category active). + + \param text Text to attach to the zone. Must not be `nullptr`. + + \par Example + \code + TelemetryZone zone( category, "LoadAsset", __FILE__, __LINE__ ); + zone.text( assetPath.c_str() ); + \endcode +*/ void TelemetryZone::text( const char* text ) const { if( s_profilerState.load( std::memory_order_acquire ) == ProfilerState::Started && m_impl->telemetryContext ) @@ -414,16 +1108,39 @@ void TelemetryZone::text( const char* text ) const } } +/*! + \brief Manually enters a zone identified by an opaque key. + + \deprecated Use a `TelemetryZone` instead, which ends its zone automatically via RAII instead of requiring a + matching `CcpTelemetryLeaveZone()` call. + + \param key Opaque identifier used to match this call with a later `CcpTelemetryLeaveZone( key )`. Zones + sharing the same `key` nest, forming a stack. + \param name Name of the zone, as shown in the profiler. + \param filename Source file the zone originates from; pass `__FILE__`. + \param lineno Source line the zone originates from; pass `__LINE__`. + + \see CcpTelemetryLeaveZone + \see TelemetryZone +*/ void CcpTelemetryEnterZone( void* key, const char* name, const char* filename, uint32_t lineno ) { if( s_profilerState.load( std::memory_order_acquire ) == ProfilerState::Started ) { t_manuallyTrackedZones.emplace( key ); - t_activeTaskletZoneStore->second.emplace( TMCM_CPP, name, filename, lineno, CcpColor::Yellow ); -// CCP_LOG_CH( s_ch, "[Fiber %p] [Store %p] [Zone %p] Enter", t_activeFiber, t_activeTaskletZoneStore, &t_activeTaskletZoneStore->second.top() ); + t_activeTaskletZoneStore->second.emplace( TMCM_CPP, name, filename, lineno ); } } +/*! + \brief Manually leaves the zone most recently entered under `key`. + + \deprecated Use a `TelemetryZone` instead. + + \param key Opaque identifier previously passed to `CcpTelemetryEnterZone()`. + + \see CcpTelemetryEnterZone +*/ void CcpTelemetryLeaveZone( void* key ) { if ( t_manuallyTrackedZones.find( key ) != t_manuallyTrackedZones.end() ) @@ -439,6 +1156,17 @@ void CcpTelemetryLeaveZone( void* key ) } } +/*! + \brief Attaches a free-form text annotation to the zone most recently entered under `key`. + + \deprecated Use `TelemetryZone::text()` instead. + + \param key Opaque identifier previously passed to `CcpTelemetryEnterZone()`. + \param text Text to attach to the zone. If `nullptr`, the call is ignored. + + \see CcpTelemetryEnterZone + \see TelemetryZone::text +*/ void CcpTelemetryZoneAddText( void* key, const char* text ) { if ( text != nullptr ) @@ -452,6 +1180,40 @@ void CcpTelemetryZoneAddText( void* key, const char* text ) #else +// With telemetry compiled out, every entry point below is a no-op, and every query reports that +// nothing is registered, nothing is active and nothing is being captured. The declarations in +// `CcpTelemetry.h` remain visible in this configuration, so call sites need no guarding. + +struct CcpTelemetryCategory +{ + std::string name; + CcpColor color{CcpColor::White}; +}; + +struct TelemetryZone::Private +{ +}; + +namespace +{ + const CcpTelemetryCategory s_emptyCategory; +} + +bool operator==( const CcpTelemetryCategory& lhs, const CcpTelemetryCategory& rhs ) +{ + return lhs.name == rhs.name; +} + +const std::string& CcpTelemetryCategoryGetName( const CcpTelemetryCategory& category ) +{ + return category.name; +} + +CcpColor CcpTelemetryCategoryGetColor( const CcpTelemetryCategory& category ) +{ + return category.color; +} + bool CcpTelemetryIsConnectionRequested() { return false; @@ -467,6 +1229,16 @@ bool CcpTelemetryIsStarted() return false; } +bool CcpTelemetryIsStopped() +{ + return true; +} + +std::chrono::milliseconds CcpTelemetryRemainingCaptureDuration() +{ + return std::chrono::milliseconds::zero(); +} + bool CcpTelemetryMemoryTrackingIsEnabled() { return false; @@ -481,6 +1253,26 @@ void CcpRegisterThread( CcpThreadId_t threadId, const char* name ) { } +std::pair CcpTelemetryCategoryRegister( const std::string&, CcpColor ) +{ + return { s_emptyCategory, false }; +} + +CcpTelemetryCategories CcpTelemetryGetRegisteredCategories() +{ + return {}; +} + +bool CcpTelemetrySetActiveCategories( const CcpTelemetryCategories& ) +{ + return false; +} + +CcpTelemetryCategories CcpTelemetryGetActiveCategories() +{ + return {}; +} + bool CcpStartTelemetry( const char* server, int connectionType, uint32_t maxThreadCount ) { return false; @@ -518,13 +1310,30 @@ void CcpTelemetrySetActiveFiber( const std::string& ) const std::string& CcpTelemetryGetActiveFiber() { - return ""; + static const std::string s_noFiber; + return s_noFiber; } void CcpTelemetryRemoveFiber( const std::string& ) { } +TelemetryZone::TelemetryZone( uint32_t, const char*, const char*, uint32_t, CcpColor ) +{ +} + +TelemetryZone::TelemetryZone( const CcpTelemetryCategory&, const char*, const char*, uint32_t ) +{ +} + +TelemetryZone::TelemetryZone( TelemetryZone&& ) noexcept = default; + +TelemetryZone::~TelemetryZone() = default; + +void TelemetryZone::text( const char* ) const +{ +} + void CcpTelemetryEnterZone( void* key, const char* name, const char* filename, uint32_t lineno ) { } @@ -537,4 +1346,12 @@ void CcpTelemetryZoneAddText( void* key, const char* text ) { } +void CcpTelemetryTrackAllocation( void*, size_t ) +{ +} + +void CcpTelemetryTrackDeallocation( void* ) +{ +} + #endif // CCP_TELEMETRY_ENABLED diff --git a/cmake/CcpDocsGenerator.cmake b/cmake/CcpDocsGenerator.cmake index aea1e97..9433daf 100644 --- a/cmake/CcpDocsGenerator.cmake +++ b/cmake/CcpDocsGenerator.cmake @@ -27,7 +27,7 @@ endfunction() function(create_carbon_docs_sphinx_target) set(options "") set(single_value_keywords "SPHINX_SOURCE;SPHINX_BUILD;SPHINX_TARGET_NAME;INSTALL_DESTINATION;DOXYGEN_TARGET_NAME;PYTHON_EXE;VENV_NAME") - set(multi_value_keywords "DOXYGEN_SRC_FILES;PYTHONPATH_ENV") + set(multi_value_keywords "DOXYGEN_SRC_FILES;PYTHONPATH_ENV;EXTRA_COMPILE_DEFINITIONS") cmake_parse_arguments(PARSE_ARGV 0 "arg" "${options}" "${single_value_keywords}" "${multi_value_keywords}" ) @@ -45,12 +45,15 @@ function(create_carbon_docs_sphinx_target) # Evaluate config file for Doxygen to input project values set(DOXYFILE_IN ${CMAKE_CURRENT_SOURCE_DIR}/doc/Doxyfile.in) set(DOXYFILE_OUT ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile) - set(DOXYGEN_INDEX_FILE ${CMAKE_CURRENT_BINARY_DIR}/xml/index.xml) + set(DOXYGEN_INDEX_FILE ${CMAKE_CURRENT_BINARY_DIR}/doc/xml/index.xml) + set(DOCUMENTATION_OUT_DIR ${CMAKE_CURRENT_BINARY_DIR}/doc) + set(EXTRA_COMPILE_DEFINITIONS ${arg_EXTRA_COMPILE_DEFINITIONS}) configure_file(${DOXYFILE_IN} ${DOXYFILE_OUT} @ONLY) # Regenerate with source changes add_custom_command(OUTPUT ${DOXYGEN_INDEX_FILE} DEPENDS ${arg_DOXYGEN_SRC_FILES} + COMMAND ${CMAKE_COMMAND} -E make_directory ${DOCUMENTATION_OUT_DIR} COMMAND ${DOXYGEN_EXECUTABLE} ${DOXYFILE_OUT} MAIN_DEPENDENCY ${DOXYFILE_OUT} ${DOXYFILE_IN} COMMENT "Running Doxygen" @@ -59,9 +62,9 @@ function(create_carbon_docs_sphinx_target) add_custom_target(${arg_DOXYGEN_TARGET_NAME} ALL DEPENDS ${DOXYGEN_INDEX_FILE}) if (WIN32) - set(SPHINX_COMMAND Scripts/sphinx-build -E -b html -D breathe_projects.doxygen=${CMAKE_CURRENT_BINARY_DIR}/docs/xml -c ${arg_SPHINX_SOURCE} ${arg_SPHINX_SOURCE} ${arg_SPHINX_BUILD}) + set(SPHINX_COMMAND Scripts/sphinx-build -E -b html -D breathe_projects.doxygen=${CMAKE_CURRENT_BINARY_DIR}/doc/xml -c ${arg_SPHINX_SOURCE} ${arg_SPHINX_SOURCE} ${arg_SPHINX_BUILD}) elseif(APPLE) - set(SPHINX_COMMAND bin/sphinx-build -E -b html -D breathe_projects.doxygen=${CMAKE_CURRENT_BINARY_DIR}/docs/xml -c ${arg_SPHINX_SOURCE} ${arg_SPHINX_SOURCE} ${arg_SPHINX_BUILD}) + set(SPHINX_COMMAND bin/sphinx-build -E -b html -D breathe_projects.doxygen=${CMAKE_CURRENT_BINARY_DIR}/doc/xml -c ${arg_SPHINX_SOURCE} ${arg_SPHINX_SOURCE} ${arg_SPHINX_BUILD}) endif() add_custom_target(${arg_SPHINX_TARGET_NAME} ALL @@ -72,4 +75,4 @@ function(create_carbon_docs_sphinx_target) # Install rule for documentation install(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/doc/build/ DESTINATION ${arg_INSTALL_DESTINATION}) -endfunction() \ No newline at end of file +endfunction() diff --git a/doc/Doxyfile.in b/doc/Doxyfile.in index d96725f..9388c05 100644 --- a/doc/Doxyfile.in +++ b/doc/Doxyfile.in @@ -396,7 +396,7 @@ AUTOLINK_SUPPORT = YES # diagrams that involve STL classes more complete and accurate. # The default value is: NO. -BUILTIN_STL_SUPPORT = NO +BUILTIN_STL_SUPPORT = YES # If you use Microsoft's C++/CLI language, you should set this option to YES to # enable parsing support. @@ -524,7 +524,7 @@ TIMESTAMP = NO # normally produced when WARNINGS is set to YES. # The default value is: NO. -EXTRACT_ALL = YES +EXTRACT_ALL = NO # If the EXTRACT_PRIVATE tag is set to YES, all private members of a class will # be included in the documentation. @@ -949,7 +949,7 @@ WARN_LOGFILE = # spaces. See also FILE_PATTERNS and EXTENSION_MAPPING # Note: If this tag is empty the current directory is searched. -INPUT = @CMAKE_CURRENT_SOURCE_DIR@/README.md @CMAKE_CURRENT_SOURCE_DIR@/src @CMAKE_CURRENT_SOURCE_DIR@/include @CMAKE_CURRENT_SOURCE_DIR@/python +INPUT = @CMAKE_CURRENT_SOURCE_DIR@ @CMAKE_CURRENT_SOURCE_DIR@/include # This tag can be used to specify the character encoding of the source files # that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses @@ -1078,13 +1078,13 @@ EXCLUDE_PATTERNS = # wildcard * is used, a substring. Examples: ANamespace, AClass, # ANamespace::AClass, ANamespace::*Test -EXCLUDE_SYMBOLS = +EXCLUDE_SYMBOLS = *_h # The EXAMPLE_PATH tag can be used to specify one or more files or directories # that contain example code fragments that are included (see the \include # command). -EXAMPLE_PATH = @CMAKE_CURRENT_SOURCE_DIR@/src/ +EXAMPLE_PATH = @CMAKE_CURRENT_SOURCE_DIR@/ # If the value of the EXAMPLE_PATH tag contains directories, you can use the # EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp and @@ -1263,46 +1263,6 @@ USE_HTAGS = NO VERBATIM_HEADERS = YES -# If the CLANG_ASSISTED_PARSING tag is set to YES then doxygen will use the -# clang parser (see: -# http://clang.llvm.org/) for more accurate parsing at the cost of reduced -# performance. This can be particularly helpful with template rich C++ code for -# which doxygen's built-in parser lacks the necessary type information. -# Note: The availability of this option depends on whether or not doxygen was -# generated with the -Duse_libclang=ON option for CMake. -# The default value is: NO. - -CLANG_ASSISTED_PARSING = NO - -# If the CLANG_ASSISTED_PARSING tag is set to YES and the CLANG_ADD_INC_PATHS -# tag is set to YES then doxygen will add the directory of each input to the -# include path. -# The default value is: YES. -# This tag requires that the tag CLANG_ASSISTED_PARSING is set to YES. - -CLANG_ADD_INC_PATHS = YES - -# If clang assisted parsing is enabled you can provide the compiler with command -# line options that you would normally use when invoking the compiler. Note that -# the include paths will already be set by doxygen for the files and directories -# specified with INPUT and INCLUDE_PATH. -# This tag requires that the tag CLANG_ASSISTED_PARSING is set to YES. - -CLANG_OPTIONS = - -# If clang assisted parsing is enabled you can provide the clang parser with the -# path to the directory containing a file called compile_commands.json. This -# file is the compilation database (see: -# http://clang.llvm.org/docs/HowToSetupToolingForLLVM.html) containing the -# options used when the source files were built. This is equivalent to -# specifying the -p option to a clang tool, such as clang-check. These options -# will then be passed to the parser. Any options specified with CLANG_OPTIONS -# will be added as well. -# Note: The availability of this option depends on whether or not doxygen was -# generated with the -Duse_libclang=ON option for CMake. - -CLANG_DATABASE_PATH = - #--------------------------------------------------------------------------- # Configuration options related to the alphabetical class index #--------------------------------------------------------------------------- @@ -2417,7 +2377,7 @@ ENABLE_PREPROCESSING = YES # The default value is: NO. # This tag requires that the tag ENABLE_PREPROCESSING is set to YES. -MACRO_EXPANSION = NO +MACRO_EXPANSION = YES # If the EXPAND_ONLY_PREDEF and MACRO_EXPANSION tags are both set to YES then # the macro expansion is limited to the macros specified with the PREDEFINED and @@ -2425,7 +2385,7 @@ MACRO_EXPANSION = NO # The default value is: NO. # This tag requires that the tag ENABLE_PREPROCESSING is set to YES. -EXPAND_ONLY_PREDEF = NO +EXPAND_ONLY_PREDEF = YES # If the SEARCH_INCLUDES tag is set to YES, the include files in the # INCLUDE_PATH will be searched if a #include is found. @@ -2458,7 +2418,7 @@ INCLUDE_FILE_PATTERNS = # recursively expanded use the := operator instead of the = operator. # This tag requires that the tag ENABLE_PREPROCESSING is set to YES. -PREDEFINED = +PREDEFINED = @EXTRA_COMPILE_DEFINITIONS@ # If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then this # tag can be used to specify a list of macro names that should be expanded. The @@ -2849,15 +2809,6 @@ DOT_GRAPH_MAX_NODES = 50 MAX_DOT_GRAPH_DEPTH = 0 -# Set the DOT_MULTI_TARGETS tag to YES to allow dot to generate multiple output -# files in one run (i.e. multiple -o and -T options on the command line). This -# makes dot run faster, but since only newer versions of dot (>1.8.10) support -# this, this feature is disabled by default. -# The default value is: NO. -# This tag requires that the tag HAVE_DOT is set to YES. - -DOT_MULTI_TARGETS = NO - # If the GENERATE_LEGEND tag is set to YES doxygen will generate a legend page # explaining the meaning of the various boxes and arrows in the dot generated # graphs. diff --git a/doc/source/api.rst b/doc/source/api.rst new file mode 100644 index 0000000..5e9a2e5 --- /dev/null +++ b/doc/source/api.rst @@ -0,0 +1,4 @@ +API Reference +============== + +.. doxygenfile:: CcpTelemetry.h diff --git a/doc/source/conf.py b/doc/source/conf.py index dcc0b5e..0fe8306 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -27,7 +27,7 @@ } breathe_default_project = "doxygen" -breathe_implementation_filename_extensions = ['.c', '.cc'] +breathe_implementation_filename_extensions = ['.c', '.cc', '.cpp'] templates_path = ['_templates'] exclude_patterns = [] diff --git a/doc/source/index.rst b/doc/source/index.rst index df75b2c..f81bedb 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -1,7 +1,7 @@ carbon-core Documentation ========================== -*carbon-core* is generated from `carbon-template`. +*carbon-core* provides generic low-level functionality and cross-platform abstractions for system calls. .. Top-level table of contents @@ -11,18 +11,5 @@ carbon-core Documentation :maxdepth: 1 :caption: Contents -.. hint:: - This project supports documentation authoring using *reStructuredText* and the *MyST* markdown language. - - Python docstrings are pulled in automatically using *autodoc* - -.. seealso:: - - `reST usages`_ - For authoring documentation using *reStructuredText*. - - `MyST with Sphinx`_ - For authoring documentation using *MyST* flavored markdown. - -.. _reST usages: https://www.sphinx-doc.org/en/master/usage/restructuredtext -.. _MyST with Sphinx: https://myst-parser.readthedocs.io/en/v0.17.1/sphinx/index.html \ No newline at end of file + telemetry.md + api.rst diff --git a/doc/source/telemetry.md b/doc/source/telemetry.md new file mode 100644 index 0000000..8d469d2 --- /dev/null +++ b/doc/source/telemetry.md @@ -0,0 +1,483 @@ +# Telemetry + +The `CcpTelemetry.h` header provides an instrumented profiling solution. Instrumentation is expressed +directly in the source code — you mark the scopes you care about, tag them with a *category*, and the +data is streamed to a connected [Tracy](https://github.com/wolfpld/tracy) profiler while a capture is +running. + +Everything the header exposes falls into one of four groups: + +| Group | Purpose | Key entry points | +| --- | --- | --- | +| Session control | Start, stop and pump a capture | `CcpStartTelemetry`, `CcpStopTelemetry`, `CcpTelemetryTick` | +| Instrumentation | Describe what the program is doing | `TelemetryZone`, `CcpTelemetrySetActiveFiber` | +| Categories | Group zones by subsystem | `CcpTelemetryCategoryRegister`, `CcpTelemetryGetRegisteredCategories` | +| Capture masks | Choose which categories are recorded | `CcpTelemetrySetActiveCategories`, `CcpTelemetryGetActiveCategories` | + +## Build-time configuration + +Telemetry is a compile-time feature, controlled by the `WITH_TELEMETRY` CMake option (`ON` by default). +The option defines `CCP_TELEMETRY_ENABLED` as a `PUBLIC` compile definition on the `CcpCore` target, so +consumers that link against `CcpCore` automatically agree with the library on whether telemetry is +compiled in: + +```shell +cmake --preset -DWITH_TELEMETRY=OFF +``` + +```{important} +Instrumentation code should not be wrapped in `#if CCP_TELEMETRY_ENABLED` guards by callers. The +declarations in `CcpTelemetry.h` are always visible, and `CcpRegisterMutex` degrades into an empty +macro when telemetry is disabled. +``` + +## On-demand profiling + +Nothing is captured until two things are true at the same time: + +1. The application has requested a capture with `CcpStartTelemetry()`, and +2. a Tracy profiler client has connected to the process. + +Because the Tracy client is built with `manual-lifetime`, the profiler server inside the process is +only spun up once a capture is requested, and it is `CcpTelemetryTick()` that drives the transition. +The session therefore behaves like a small state machine: + +| State | Reached by | `CcpTelemetryIsConnectionRequested()` | `CcpTelemetryIsStarted()` | `CcpTelemetryIsStopped()` | +| --- | --- | --- | --- | --- | +| Stopped | initial state, or a tick after `CcpStopTelemetry()` | `false` | `false` | `true` | +| Start requested | `CcpStartTelemetry()` | `true` | `false` | `false` | +| Started | a tick after a profiler client connected | `false` | `true` | `false` | +| Stop requested | `CcpStopTelemetry()`, a lost connection, or an elapsed `captureDuration` | `false` | `false` | `false` | + +`CcpTelemetryIsConnected()` is the one to check before doing work purely for the profiler's benefit: it +is `true` only while the session is started *and* a client is still attached. + +### Starting a session + +A session is configured with `CcpTelemetryConfig`: + +| Field | Meaning | +| --- | --- | +| `applicationName` | Name shown in the profiler UI. | +| `captureDuration` | If non-zero, the capture stops itself this long after the profiler connected. Zero (the default) captures until `CcpStopTelemetry()` is called. | +| `trackMemoryAllocations` | Forward allocations reported by `CCPMemory` to the profiler. | +| `trackLocks` | Report `CcpMutex`, `CcpSpinLock` and `CcpSemaphore` contention to the profiler. | + +```cpp +#include + +CcpTelemetryConfig config; +config.applicationName = "MyGame"; +config.captureDuration = std::chrono::seconds( 30 ); // omit for an open-ended capture +config.trackMemoryAllocations = true; +config.trackLocks = true; + +CcpStartTelemetry( config ); +``` + +`CcpStartTelemetry()` only records the request; it returns `false` if a capture is already started or +already pending. The actual work happens in `CcpTelemetryTick()`, which must be called regularly — +typically once per frame from the main loop: + +```cpp +while( isRunning ) +{ + CcpTelemetryTick(); + + // ... rest of the frame ... +} +``` + +```{warning} +Without a regular `CcpTelemetryTick()`, a requested capture never becomes active, no frame marks are +produced, and timed captures never expire. +``` + +### Reacting to session changes + +Register an event handler to hook subsystems into the session lifecycle. This is the natural place to +reset per-capture bookkeeping. + +```cpp +void OnTelemetryEvent( CcpTelemetryEvent event, void* userData ) +{ + switch( event ) + { + case CCP_TELEMETRY_STARTED: + // your code, f.e. toggle some UI state + break; + case CCP_TELEMETRY_STOPPED: + // your code, f.e. toggle some UI state + break; + } +} + +CcpRegisterTelemetryEventHandler( &OnTelemetryEvent, nullptr ); +``` + +If a profiler is already connected when the handler is registered, it is invoked immediately with +`CCP_TELEMETRY_STARTED`. `CcpUnregisterTelemetryEventHandler()` matches on both the function pointer +*and* the `userData` value, so pass back exactly what was registered. + +For a timed capture, `CcpTelemetryRemainingCaptureDuration()` reports how much of `captureDuration` is +left, clamped to zero. It returns zero for open-ended captures. + +## Instrumenting code + +### Zones + +A *zone* is a named, timed span of work. Zones are created with `TelemetryZone`, which reports the span +using RAII: the zone begins where the object is constructed and ends when it goes out of scope. + +```cpp +#include + +namespace +{ + // Register once, reuse everywhere. The returned reference is valid for the lifetime of the + // process, because categories are never unregistered. + const CcpTelemetryCategory& PhysicsCategory() + { + static const CcpTelemetryCategory& category = + CcpTelemetryCategoryRegister( "physics", CcpColor::Orange ).first; + return category; + } +} + +void StepSimulation( const World& world, float deltaTime ) +{ + TelemetryZone zone( PhysicsCategory(), "StepSimulation", __FILE__, __LINE__ ); + + // ... simulate; the zone ends automatically here ... +} +``` + +The constructor takes the category, the name to display, and the source location of the zone: + +| Parameter | Value to pass | +| --- | --- | +| `category` | A category obtained from `CcpTelemetryCategoryRegister()`. | +| `name` | Display name of the zone. Must not be `nullptr`. | +| `filename` | `__FILE__`. | +| `lineno` | `__LINE__`. | + +Zones nest naturally with the call stack. A zone created inside another zone's scope is shown as a +child of it in the profiler. The zone's color comes from its category, so all zones belonging to one +subsystem are visually grouped. + +Because the source location is part of every zone, it is common to hide the boilerplate behind a +per-subsystem macro. `CCP_ANONYMOUS_VARIABLE` from `CcpMacros.h` keeps the variable name unique: + +```cpp +#include + +#define PHYSICS_ZONE( zoneName ) \ + TelemetryZone CCP_ANONYMOUS_VARIABLE( physicsZone_ )( PhysicsCategory(), zoneName, __FILE__, __LINE__ ) + +void StepSimulation( const World& world, float deltaTime ) +{ + PHYSICS_ZONE( "StepSimulation" ); + // ... +} +``` + +### Annotating zones + +`TelemetryZone::text()` attaches a free-form string to a zone, which shows up next to it in the +profiler. Use it for the data that makes a particular sample interesting — an asset path, an entity +count, a request id: + +```cpp +void LoadAsset( const std::string& path ) +{ + TelemetryZone zone( AssetCategory(), "LoadAsset", __FILE__, __LINE__ ); + zone.text( path.c_str() ); + + // ... +} +``` + +### Zone lifetime and ownership + +`TelemetryZone` has no default constructor and cannot be copied — a zone must have exactly one owner +that is responsible for ending it. It *can* be moved, which is what makes it possible to keep a zone +alive beyond the scope that created it: + +```cpp +std::optional pending; + +void BeginStreaming() +{ + pending.emplace( StreamingCategory(), "StreamChunk", __FILE__, __LINE__ ); +} + +void EndStreaming() +{ + pending.reset(); // the zone ends here +} +``` + +```{note} +Whether a zone is recorded is decided once, when the `TelemetryZone` is constructed: a capture must be +running and the zone's category must be active in the capture mask. A zone constructed outside a +capture stays a no-op for its whole lifetime, even if a capture starts before it goes out of scope. +Keep zones short-lived, and prefer creating them inside the scope being measured. +``` + +### Fibers + +Code that runs on fibers (for example Python tasklets) can be resumed on a different OS thread than the one it +started on, which would otherwise produce interleaved, nonsensical call stacks. Telling telemetry which +fiber is currently active makes zone bookkeeping per-fiber instead of per-thread: + +```cpp +CcpTelemetrySetActiveFiber( "WorkerFiber1" ); +{ + TelemetryZone zone( ScriptCategory(), "RunTasklet", __FILE__, __LINE__ ); + // ... work performed on behalf of the fiber ... +} +CcpTelemetrySetActiveFiber( "" ); // back to the root, "no fiber" context +``` + +- The empty string is the root context, meaning "not on a fiber". +- `CcpTelemetryGetActiveFiber()` returns the calling thread's current fiber name. +- `CcpTelemetryRemoveFiber()` retires a fiber name once the fiber is gone. If the removed fiber is the + calling thread's active one, the thread falls back to the root context. The name itself is only + released after a short grace period, on a later `CcpTelemetryTick()`. + +A zone always ends on the fiber it started on, even if the owning `TelemetryZone` is destroyed after +the thread has switched to another fiber. + +### Locks and memory + +Lock and allocation tracking need no instrumentation in application code — they are wired into the +primitives themselves and gated on the session configuration: + +- **Locks.** `CcpMutex`, `CcpSpinLock` and `CcpSemaphore` announce themselves to the profiler and report + their wait/obtain/release events while `trackLocks` is enabled and a profiler is connected. A + `CcpMutex` appears under `-` from its constructor arguments, so pass something + meaningful there. `CcpRegisterMutex()` is called by `CcpMutex` itself; there is no need to call it + directly. +- **Memory.** The `CCPMemory` allocators call `CcpTelemetryTrackAllocation()` and + `CcpTelemetryTrackDeallocation()` for you while `trackMemoryAllocations` is enabled. Call them + directly only from a custom allocator. + +Use `CcpTelemetryLockTrackingIsEnabled()` and `CcpTelemetryMemoryTrackingIsEnabled()` to query what the +running session was configured with. + +## Telemetry categories + +A `CcpTelemetryCategory` groups related zones — usually one category per subsystem. Categories give +zones their color in the profiler, and, more importantly, they are the unit of selection for capture +masks. + +The type is opaque: `CcpTelemetry.h` only forward-declares it. Instances are only ever handed out by +reference from the registry, and are inspected through free functions: + +```cpp +auto [category, ok] = CcpTelemetryCategoryRegister( "rendering", CcpColor::Green ); + +const std::string& name = CcpTelemetryCategoryGetName( category ); +CcpColor color = CcpTelemetryCategoryGetColor( category ); +``` + +To hold several of them, use `CcpTelemetryCategories` which is also the type the registry and mask +functions speak. + +### Registering a category + +`CcpTelemetryCategoryRegister()` is idempotent by name: registering a name that already exists returns +the existing category rather than creating a second one. This makes it safe to call from several +translation units, and means a subsystem never has to publish its category handle to its callers. + +```cpp +auto [category, ok] = CcpTelemetryCategoryRegister( "rendering", CcpColor::Green ); + +// Later, elsewhere — same category, the color argument is ignored for an existing name. +auto [same, stillOk] = CcpTelemetryCategoryRegister( "rendering" ); +``` + +```{note} +Category names are case-sensitive. +``` + +The `bool` is `false` when a category cannot be registered. This can happen, for example, when the name was empty, or +all category slots are taken. On failure the returned reference refers to a shared, empty placeholder category. + +```{warning} +Always check the `bool`. Zones tagged with the placeholder category returned on failure can never be +captured, because the placeholder owns no capture flag. +``` + +### Built-in categories + +Three categories are registered before any application code runs, and always occupy the first three +slots: + +| Name | Color | Notes | +| --- | --- | --- | +| `general` | `CcpColor::SteelBlue` | Default category; corresponds to the legacy `TMCM_GENERAL`. | +| `cpp` | `CcpColor::Yellow` | Used by the deprecated `CcpTelemetryEnterZone()` path; corresponds to the legacy `TMCM_CPP`. | +| `core` | `CcpColor::LightGreen` | Zones inside *carbon-core* itself. | + +`CcpTelemetryGetRegisteredCategories()` returns everything registered so far, in registration order, +starting with those three: + +```cpp +for( const CcpTelemetryCategory& category : CcpTelemetryGetRegisteredCategories() ) +{ + printf( "%s\n", CcpTelemetryCategoryGetName( category ).c_str() ); +} +``` + +## Capture masks + +A capture mask is the set of categories that are actually recorded. When a `TelemetryZone` is constructed, +its category is tested against the set of active categories. If a zone's category is not part of that set, +said zone will not show up in the profiler. + +This is what makes instrumentation affordable to leave in shipping code: you can instrument +generously, then decide at runtime which subsystems a given capture should actually pay for. + +```{important} +The active mask starts out **empty**, and `CcpStartTelemetry()` does not change it. Until +`CcpTelemetrySetActiveCategories()` is called, a connected profiler receives frame marks, lock and +memory events — but no zones. +``` + +### Setting the mask + +`CcpTelemetrySetActiveCategories()` **replaces** the active set; it is not additive: + +```cpp +auto [rendering, renderingOk] = CcpTelemetryCategoryRegister( "rendering" ); +auto [physics, physicsOk] = CcpTelemetryCategoryRegister( "physics" ); + +// Capture rendering and physics zones, and nothing else. +CcpTelemetrySetActiveCategories( { rendering, physics } ); +``` + +Passing an empty list clears the mask and stops all zone capture: + +```cpp +CcpTelemetrySetActiveCategories( {} ); +``` + +Capturing everything is a matter of feeding the registry back in: + +```cpp +CcpTelemetrySetActiveCategories( CcpTelemetryGetRegisteredCategories() ); +``` + +The call returns `false` — leaving the mask unchanged — if too many entries are passed in. +Duplicate entries are harmless, as are references to categories that never registered successfully; +they are simply ignored. + +### Reading the mask back + +`CcpTelemetryGetActiveCategories()` returns the currently active categories, in registry order. Pairing +it with `CcpTelemetryGetRegisteredCategories()` is all that is needed to drive a debug UI, a console +command, or a config file: + +```cpp +void DumpCategoryState() +{ + const auto active = CcpTelemetryGetActiveCategories(); + + for( const CcpTelemetryCategory& category : CcpTelemetryGetRegisteredCategories() ) + { + const bool isActive = std::find_if( active.begin(), active.end(), + [&category]( const CcpTelemetryCategory& candidate ) { return candidate == category; } ) != active.end(); + + printf( "[%c] %s\n", isActive ? 'x' : ' ', CcpTelemetryCategoryGetName( category ).c_str() ); + } +} +``` + +Since the setter replaces the whole set, toggling a single category is a read-modify-write of the +active list: + +```cpp +void SetCategoryActive( const CcpTelemetryCategory& category, bool active ) +{ + auto categories = CcpTelemetryGetActiveCategories(); + + auto it = std::find_if( categories.begin(), categories.end(), + [&category]( const CcpTelemetryCategory& candidate ) { return candidate == category; } ); + + if( active && it == categories.end() ) + { + categories.emplace_back( category ); + } + else if( !active && it != categories.end() ) + { + categories.erase( it ); + } + + CcpTelemetrySetActiveCategories( categories ); +} +``` + +### When mask changes take effect + +The mask is consulted when a zone is *constructed*, so a change applies to zones created afterwards. +Zones that are already open keep the state they were created with and are reported normally when they +end. In practice this means a mask change lands within a frame, and never produces a truncated or +orphaned zone. + +```{note} +Registering categories and setting the mask are serialized against each other, and are meant to be +driven from a single control path — a debug UI, a console command, or a telemetry event handler. Zone +construction reads the mask without synchronization, so a mask change may be observed a moment later by +threads that are already running. +``` + +### A worked example + +Putting the pieces together — a 10 second capture that only records the rendering subsystem: + +```cpp +void CaptureRenderingProfile() +{ + auto [rendering, ok] = CcpTelemetryCategoryRegister( "rendering", CcpColor::Green ); + if( !ok ) + { + return; + } + + CcpTelemetryConfig config; + config.applicationName = "MyGame"; + config.captureDuration = std::chrono::seconds( 10 ); + + if( CcpStartTelemetry( config ) ) + { + CcpTelemetrySetActiveCategories( { rendering } ); + } +} +``` + +The capture becomes active as soon as a Tracy profiler connects, records only zones tagged +`rendering`, and stops itself 10 seconds later — provided `CcpTelemetryTick()` keeps being called. + +## Legacy API + +Older call sites use an earlier, bitmask-oriented API that is still supported but deprecated. New code +should not use any of it: + +| Deprecated | Replacement | +| --- | --- | +| `TelemetryZone( uint32_t handle, ... )` | `TelemetryZone( const CcpTelemetryCategory&, ... )` | +| `CcpTelemetryEnterZone()` / `CcpTelemetryLeaveZone()` | A scoped `TelemetryZone` | +| `CcpTelemetryZoneAddText()` | `TelemetryZone::text()` | +| `CcpStartTelemetry( const char*, int, uint32_t )` | `CcpStartTelemetry( const CcpTelemetryConfig& )` | + +`TMCM_GENERAL` and `TMCM_CPP` are the surviving legacy category constants. They date from when +categories were selected with a hand-written bitmask, and they happen to line up with the capture bits +of the built-in `general` and `cpp` categories, which is why the deprecated zone constructor still +works. The manual `CcpTelemetryEnterZone()` / `CcpTelemetryLeaveZone()` pair keys zones on an opaque +pointer and requires the calls to be balanced by hand; `TelemetryZone` does the same job with RAII. + +## API reference + +Signatures and per-function details for everything described here are generated from the sources — see +the {doc}`api` section. diff --git a/include/CcpColorConstants.h b/include/CcpColorConstants.h index 8696294..15a190c 100644 --- a/include/CcpColorConstants.h +++ b/include/CcpColorConstants.h @@ -1,6 +1,12 @@ +// Copyright © 2026 CCP ehf. + +#pragma once #ifndef CCP_COLOR_CONSTANTS_H #define CCP_COLOR_CONSTANTS_H +#include +#include + // Useful RGB color constants // Initially based on the type of CSS standard, see https://www.w3.org/TR/css-color-4/#named-colors. enum class CcpColor : uint32_t @@ -155,4 +161,6 @@ enum class CcpColor : uint32_t YellowGreen = 0x9acd32, }; +CARBON_CORE_API std::string_view CcpColorToString( CcpColor color ); + #endif diff --git a/include/CcpTelemetry.h b/include/CcpTelemetry.h index 93beb7a..067ff72 100644 --- a/include/CcpTelemetry.h +++ b/include/CcpTelemetry.h @@ -4,7 +4,10 @@ #ifndef CcpTelemetry_h #define CcpTelemetry_h +#include +#include #include +#include #include "CcpColorConstants.h" #include "CcpThread.h" @@ -35,6 +38,19 @@ CARBON_CORE_API void CcpRegisterThread( CcpThreadId_t threadId, const char* name ); +struct CcpTelemetryCategory; +using CcpTelemetryCategories = std::vector>; +CARBON_CORE_API bool operator==( const CcpTelemetryCategory& lhs, const CcpTelemetryCategory& rhs ); +CARBON_CORE_API const std::string& CcpTelemetryCategoryGetName( const CcpTelemetryCategory& category ); +CARBON_CORE_API CcpColor CcpTelemetryCategoryGetColor( const CcpTelemetryCategory& category ); + +CARBON_CORE_API std::pair CcpTelemetryCategoryRegister( const std::string& name, CcpColor color = CcpColor::SteelBlue ); +CARBON_CORE_API CcpTelemetryCategories CcpTelemetryGetRegisteredCategories(); + +CARBON_CORE_API bool CcpTelemetrySetActiveCategories( const CcpTelemetryCategories& categories ); +CARBON_CORE_API CcpTelemetryCategories CcpTelemetryGetActiveCategories(); + + struct CcpTelemetryConfig { std::string applicationName; @@ -76,8 +92,13 @@ class TelemetryZone { public: TelemetryZone() = delete; - CARBON_CORE_API TelemetryZone( uint32_t ctx, const char* name, const char* filename, uint32_t lineno, CcpColor color = CcpColor::SteelBlue ); + + [[deprecated("Use the `TelemetryZone( const CcpTelemetryCategory&, const char*, const char*, uint32_t, CcpColor )` constructor instead.")]] + CARBON_CORE_API TelemetryZone( uint32_t handle, const char* name, const char* filename, uint32_t lineno, CcpColor color = CcpColor::SteelBlue ); + + CARBON_CORE_API TelemetryZone( const CcpTelemetryCategory& category, const char* name, const char* filename, uint32_t lineno ); CARBON_CORE_API ~TelemetryZone(); + TelemetryZone( TelemetryZone&& other ) noexcept; TelemetryZone( const TelemetryZone& ) = delete; TelemetryZone& operator=( TelemetryZone&& ) = delete; @@ -90,8 +111,11 @@ class TelemetryZone std::unique_ptr m_impl; }; +[[deprecated("Use a `TelemetryZone` instead.")]] CARBON_CORE_API void CcpTelemetryEnterZone( void* key, const char* name, const char* filename, uint32_t lineno ); +[[deprecated("Use a `TelemetryZone` instead.")]] CARBON_CORE_API void CcpTelemetryLeaveZone( void* key ); +[[deprecated("Use a `TelemetryZone` instead.")]] CARBON_CORE_API void CcpTelemetryZoneAddText( void* key, const char* text ); void CcpTelemetryTrackAllocation( void*, size_t ); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 0c8077f..bc506aa 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -3,7 +3,6 @@ enable_testing() find_package(GTest CONFIG REQUIRED) -find_package(lz4 CONFIG REQUIRED) add_executable(CcpCoreTest CCPCallstack.cpp CcpCoreTest.cpp @@ -14,15 +13,22 @@ add_executable(CcpCoreTest CCPMemoryTracker.cpp CcpSecureCrt.cpp CcpStatistics.cpp - CcpTelemetry.cpp CcpThread.cpp CcpTime.cpp StringConversions.cpp TempFile.cpp CCPLog.cpp - TracyTestClient.cpp ) -target_link_libraries(CcpCoreTest PRIVATE CcpCore GTest::gtest GTest::gtest_main lz4::lz4 Tracy::TracyClient) +target_link_libraries(CcpCoreTest PRIVATE CcpCore GTest::gtest GTest::gtest_main) + +if(WITH_TELEMETRY) + find_package(lz4 CONFIG REQUIRED) + target_sources(CcpCoreTest PRIVATE + CcpTelemetry.cpp + TracyTestClient.cpp + ) + target_link_libraries(CcpCoreTest PRIVATE lz4::lz4 Tracy::TracyClient) +endif() if(WIN32) target_link_libraries(CcpCoreTest PRIVATE ws2_32) endif() diff --git a/tests/CcpTelemetry.cpp b/tests/CcpTelemetry.cpp index c3448db..426e376 100644 --- a/tests/CcpTelemetry.cpp +++ b/tests/CcpTelemetry.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -14,6 +15,8 @@ #include +#include "SilenceDeprecationWarnings.h" + // How can we test telemetry-related functionality to ensure our bookkeeping // there is sane? // The problem is that such tests need `ProfilerState::Started` in order to @@ -37,6 +40,16 @@ // includes the AI-written, but human-reviewed test client. #include "TracyTestClient.h" +// Helper for find a ProfilerCategory by name from the list of already registered ProfilerCategories +bool TryGetProfilerCategoryNamed( const std::string& name, CcpTelemetryCategories::const_iterator& out ) +{ + auto categories = CcpTelemetryGetRegisteredCategories(); + out = std::find_if( categories.begin(), categories.end(), [name](const CcpTelemetryCategory& cat) { + return CcpTelemetryCategoryGetName( cat ) == name; + } ); + return out != categories.end(); +} + class CcpTelemetryTest : public ::testing::Test { protected: @@ -57,13 +70,13 @@ class CcpTelemetryTest : public ::testing::Test ::testing::Test::TearDown(); } - void TickTelemetry( std::function predicate = nullptr, std::chrono::milliseconds timeout = std::chrono::milliseconds( 500 ) ) + void TickTelemetry( std::function predicate = nullptr, std::chrono::milliseconds timeout = std::chrono::milliseconds( 100 ) ) { const auto deadline = std::chrono::steady_clock::now() + timeout; while( std::chrono::steady_clock::now() < deadline && !( predicate && predicate() ) ) { CcpTelemetryTick(); - std::this_thread::sleep_for( std::chrono::milliseconds( 5 ) ); + std::this_thread::sleep_for( std::chrono::milliseconds( 1 ) ); } } @@ -163,6 +176,7 @@ class CcpTelemetryTest : public ::testing::Test return m_tracyClient.TryGetLock( lockId, outLock ); } + const std::string expectedNoFiber; const std::string expectedFiberName1{ "TestFiber1" }; const std::string expectedFiberName2{ "TestFiber2" }; @@ -196,6 +210,24 @@ TEST_F( CcpTelemetryTest, RemovingActiveFiberClearsIt ) EXPECT_EQ( CcpTelemetryGetActiveFiber(), expectedNoFiber ); } +TEST_F( CcpTelemetryTest, RemovingElementsFromFiberNameStoreDoesNotCrash ) +{ + // There was a crash when the same fiber was queued multiple times for + // deletion. Additionally, some silent data corruption could occur when + // inserting a fiber name again that was about to be deleted. + // This isn't the best test to properly validate both scenarios, but it + // is at least good enough to reproduce the observed crash. + CcpTelemetrySetActiveFiber( expectedFiberName1 ); + CcpTelemetryRemoveFiber( expectedFiberName1 ); + CcpTelemetrySetActiveFiber( expectedFiberName1 ); + CcpTelemetryRemoveFiber( expectedFiberName1 ); + TickTelemetry( nullptr ); + CcpTelemetrySetActiveFiber( expectedFiberName1 ); + CcpTelemetryRemoveFiber( expectedFiberName1 ); + CcpTelemetrySetActiveFiber( expectedFiberName1 ); + TickTelemetry( nullptr ); +} + TEST_F( CcpTelemetryTest, RemainingCaptureDuration ) { // The test fixture starts telemetry without a specific capture duration. @@ -221,19 +253,31 @@ TEST_F( CcpTelemetryTest, RemainingCaptureDuration ) EXPECT_TRUE( CcpTelemetryIsStopped() ); } +// The following tests cover deprecated functionality. We don't care about the noisy warning here, since the tests will break when the functionality is removed. +CCP_DISABLE_DEPRECATED_BEGIN + TEST_F( CcpTelemetryTest, SimpleZoneTest ) { EXPECT_TRUE( CcpTelemetryIsConnected() ); static int key = 4711; const std::string zoneName{ "TestZone" }; - CcpTelemetryEnterZone( &key, zoneName.c_str(), __FILE__, __LINE__ ); + + auto [cat, ok] = CcpTelemetryCategoryRegister( "cpp" ); + EXPECT_TRUE( ok ); + CcpTelemetrySetActiveCategories( {cat} ); + + CcpTelemetryEnterZone( &key, zoneName.c_str(), __FILE__, __LINE__ ); // Original deprecated version // Tracy's worker sleeps up to 10 ms between queue flushes, so give it // time to process and send the zone event before asserting. TickTelemetry( [this] { return m_tracyClient.GetZoneBeginCount() == 1; } ); EXPECT_EQ( 1, m_tracyClient.GetZoneBeginCount() ); EXPECT_TRUE( ZoneExists( zoneName ) ); + const auto zones = m_tracyClient.GetZones(); + ASSERT_EQ( 1, zones.size() ); + EXPECT_EQ( static_cast( CcpColor::SteelBlue ), zones.front().color ) << CcpColorToString( CcpColor(zones.front().color) ).data(); + CcpTelemetryLeaveZone( &key ); TickTelemetry( [this] { return m_tracyClient.GetZoneEndCount() == 1; } ); EXPECT_EQ( 1, m_tracyClient.GetZoneEndCount() ); @@ -244,6 +288,9 @@ TEST_F( CcpTelemetryTest, StackedZones ) { // A stacked zone is a zone that has the same key as a previously created zone. static int key = 4711; + auto cppCategory = CcpTelemetryCategoryRegister( "cpp" ); + EXPECT_TRUE( cppCategory.second ); + CcpTelemetrySetActiveCategories( {cppCategory.first} ); CcpTelemetryEnterZone( &key, "TestZone", __FILE__, __LINE__ ); CcpTelemetryEnterZone( &key, "TestZone2", __FILE__, __LINE__ ); TickTelemetry( [this] { return m_tracyClient.GetZones().size() == 2; } ); @@ -260,12 +307,17 @@ TEST_F( CcpTelemetryTest, StackedZones ) CcpTelemetryLeaveZone( &key ); TickTelemetry( [this] { return m_tracyClient.GetZones().empty(); } ); EXPECT_TRUE( m_tracyClient.GetZones().empty() ); + CcpTelemetrySetActiveCategories( {} ); } TEST_F( CcpTelemetryTest, StartStopStartTelemetryWhileClientIsRunning ) { static int key1 = 1001; const std::string zoneName1{ "FirstZone" }; + + auto cppCategory = CcpTelemetryCategoryRegister( "cpp" ); + EXPECT_TRUE( cppCategory.second ); + CcpTelemetrySetActiveCategories( {cppCategory.first} ); CcpTelemetryEnterZone( &key1, zoneName1.c_str(), __FILE__, __LINE__ ); TickTelemetry( [this, zoneName1] { return ZoneExists( zoneName1 ); } ); EXPECT_TRUE( ZoneExists( zoneName1 ) ); @@ -294,7 +346,10 @@ TEST_F( CcpTelemetryTest, StartStopStartTelemetryWhileClientIsRunning ) TickTelemetry( [this, zoneName2] { return ZoneExists( zoneName2 ); } ); EXPECT_TRUE( ZoneExists( zoneName2 ) ); EXPECT_FALSE( ZoneExists( zoneName1 ) ) << "FirstZone should not exist"; - EXPECT_EQ( 1, m_tracyClient.GetZones().size() ); + + const auto zones = m_tracyClient.GetZones(); + ASSERT_EQ( 1, zones.size() ); + EXPECT_EQ( static_cast( CcpColor::SteelBlue ), zones.front().color ) << "Default color for ProfilerCategory TMCM_GENERAL should be CcpColor::SteelBlue"; EXPECT_EQ( 2, m_tracyClient.GetZoneBeginCount() ); EXPECT_EQ( 1, m_tracyClient.GetZoneEndCount() ); @@ -302,8 +357,47 @@ TEST_F( CcpTelemetryTest, StartStopStartTelemetryWhileClientIsRunning ) TickTelemetry(); EXPECT_TRUE( m_tracyClient.GetZones().empty() ); EXPECT_EQ( 2, m_tracyClient.GetZoneEndCount() ); + CcpTelemetrySetActiveCategories( {} ); +} + +TEST_F( CcpTelemetryTest, TelemetryZoneConstructor ) +{ + // Test where ProfilerCategory is in the Active list + auto [ cppCategory, cppOK ] = CcpTelemetryCategoryRegister( "cpp" ); + auto [ generalCategory, generalOK ] = CcpTelemetryCategoryRegister( "general" ); + EXPECT_TRUE( cppOK ); + EXPECT_TRUE( generalOK ); + CcpTelemetrySetActiveCategories( { cppCategory, generalCategory } ); + { + TelemetryZone activeZone( TMCM_CPP, "ZoneIsInActiveList", __FILE__, __LINE__, CcpColor::Yellow ); + TickTelemetry( [this] { return m_tracyClient.GetZoneBeginCount() == 1; } ); + EXPECT_EQ( 1, m_tracyClient.GetZoneBeginCount() ); + const auto zones = m_tracyClient.GetZones(); + ASSERT_EQ( 1, zones.size() ); + EXPECT_EQ( "ZoneIsInActiveList", zones.front().function ); + EXPECT_EQ( static_cast( CcpColor::Yellow ), zones.front().color ); + EXPECT_EQ( 0, m_tracyClient.GetZoneEndCount() ); + + TelemetryZone anotherActiveZone( cppCategory, "New constructor test", __FILE__, __LINE__ ); + } + // Now the activeZone has gone out of scope, so the zone should have ended + TickTelemetry( [this] { return m_tracyClient.GetZoneEndCount() == 2; } ); + EXPECT_EQ( 2, m_tracyClient.GetZoneEndCount() ); + EXPECT_TRUE( m_tracyClient.GetZones().empty() ); + + // Test where ProfilerCategory is NOT in the Active list + CcpTelemetrySetActiveCategories( {} ); + { + TelemetryZone inactiveZone( TMCM_CPP, "ZoneIsNotInActiveList", __FILE__, __LINE__, CcpColor::Blue ); + TickTelemetry(); + EXPECT_EQ( 2, m_tracyClient.GetZoneBeginCount() ) << "Inactive zone must not emit ZoneBegin, count should stay at 1"; + EXPECT_EQ( 2, m_tracyClient.GetZoneEndCount() ) << "Inactive zone must not emit ZoneEnd, count should stay at 1"; + EXPECT_TRUE( m_tracyClient.GetZones().empty() ); + } } +CCP_DISABLE_DEPRECATED_END // Nothing deprecated is used after this point + // --------------------------------------------------------------------------- // CcpMutex / CcpAutoMutex @@ -323,7 +417,7 @@ TEST_F( CcpTelemetryTest, CcpMutexAnnounceAndTerminate ) // The custom name arrives almost immediately, but the source location // resolves through extra server-query round trips; wait for both. TickTelemetry( [&] { return TryGetActiveLockNamed( lockName, lockInfo ) && !lockInfo.source.empty(); } ); - ASSERT_TRUE( TryGetActiveLockNamed( lockName, lockInfo ) ); + EXPECT_TRUE( TryGetActiveLockNamed( lockName, lockInfo ) ); EXPECT_FALSE( lockInfo.terminated ); // The owner and name passed to CcpMutex arrive combined as the custom lock name. EXPECT_EQ( lockName, lockInfo.name ); @@ -348,7 +442,7 @@ TEST_F( CcpTelemetryTest, CcpMutexAcquireAndRelease ) TracyTestClient::LockInfo lockInfo; mutex.Acquire(); TickTelemetry( [&] { return TryGetActiveLockNamed( lockName, lockInfo ) && lockInfo.obtainCount == 1; } ); - ASSERT_TRUE( TryGetActiveLockNamed( lockName, lockInfo ) ); + EXPECT_TRUE( TryGetActiveLockNamed( lockName, lockInfo ) ); EXPECT_EQ( 1, lockInfo.waitCount ); EXPECT_EQ( 1, lockInfo.obtainCount ); EXPECT_EQ( 0, lockInfo.releaseCount ); @@ -433,8 +527,8 @@ TEST_F( CcpTelemetryTest, MultipleCcpMutexesAnnounceDistinctLocks ) return TryGetActiveLockNamed( firstLockName, firstLock ) && TryGetActiveLockNamed( secondLockName, secondLock ); } ); - ASSERT_TRUE( TryGetActiveLockNamed( firstLockName, firstLock ) ); - ASSERT_TRUE( TryGetActiveLockNamed( secondLockName, secondLock ) ); + EXPECT_TRUE( TryGetActiveLockNamed( firstLockName, firstLock ) ); + EXPECT_TRUE( TryGetActiveLockNamed( secondLockName, secondLock ) ); EXPECT_NE( firstLock.id, secondLock.id ); secondMutex.Release(); @@ -455,7 +549,7 @@ TEST_F( CcpTelemetryTest, CcpSpinLockAnnounceAndTerminate ) CcpAutoSpinLock autoSpinLock( spinLock ); TickTelemetry( [&] { return TryGetActiveLockNamed( lockName, lockInfo ); } ); - ASSERT_TRUE( TryGetActiveLockNamed( lockName, lockInfo ) ); + EXPECT_TRUE( TryGetActiveLockNamed( lockName, lockInfo ) ); EXPECT_FALSE( lockInfo.terminated ); EXPECT_EQ( lockName, lockInfo.name ); EXPECT_TRUE( lockInfo.waitingThreads.empty() ); @@ -479,7 +573,7 @@ TEST_F( CcpTelemetryTest, CcpSpinLockAcquireAndRelease ) spinLock.Acquire(); TickTelemetry( [&] { return TryGetActiveLockNamed( lockName, lockInfo ); } ); - ASSERT_TRUE( TryGetActiveLockNamed( lockName, lockInfo ) ); + EXPECT_TRUE( TryGetActiveLockNamed( lockName, lockInfo ) ); const uint32_t lockId = lockInfo.id; EXPECT_EQ( 1, lockInfo.waitCount ); EXPECT_EQ( 1, lockInfo.obtainCount ); @@ -504,7 +598,7 @@ TEST_F( CcpTelemetryTest, CcpSemaphoreAnnounceAndTerminate ) std::thread waiter( [&semaphore] { semaphore.Wait(); } ); TickTelemetry( [&] { return TryGetActiveLockNamed( lockName, lockInfo ) && lockInfo.name == lockName; } ); - ASSERT_TRUE( TryGetActiveLockNamed( lockName, lockInfo ) ) << "lockName: " << lockName << " lockInfo.name: " << lockInfo.name; + EXPECT_TRUE( TryGetActiveLockNamed( lockName, lockInfo ) ) << "lockName: " << lockName << " lockInfo.name: " << lockInfo.name; EXPECT_FALSE( lockInfo.terminated ); EXPECT_EQ( lockName, lockInfo.name ); EXPECT_EQ( 1, lockInfo.waitCount ); @@ -534,8 +628,71 @@ TEST_F( CcpTelemetryTest, CcpSemaphoreTimedWaitTimesOut ) // No signal beforehand — TimedWait should time out and report a wait without an obtain. EXPECT_FALSE( semaphore.TimedWait( 10 ) ); TickTelemetry( [&] { return TryGetActiveLockNamed( lockName, lockInfo ) && lockInfo.waitCount == 1 && lockInfo.obtainCount == 1; } ); - ASSERT_TRUE( TryGetActiveLockNamed( lockName, lockInfo ) ) << "lockName: " << lockName << " lockInfo.name: " << lockInfo.name; + EXPECT_TRUE( TryGetActiveLockNamed( lockName, lockInfo ) ) << "lockName: " << lockName << " lockInfo.name: " << lockInfo.name; EXPECT_EQ( 1, lockInfo.waitCount ); EXPECT_EQ( 1, lockInfo.obtainCount ); EXPECT_EQ( 0, lockInfo.releaseCount ); } + +// --------------------------------------------------------------------------- +// ProfilerCategory tests: +// --------------------------------------------------------------------------- +class CcpTelemetryProfilerCategoryTest : public ::testing::Test +{ + public: + void TearDown() + { + // Clear any active Profiler Category to let subsequent tests just work + CcpTelemetrySetActiveCategories({}); + ::testing::Test::TearDown(); + } +}; + +TEST_F( CcpTelemetryProfilerCategoryTest, EmptyActiveProfilerCategory ) +{ + EXPECT_EQ( CcpTelemetryCategories{}, CcpTelemetryGetActiveCategories() ); +} + +TEST_F( CcpTelemetryTest, ProfilerCategoryRegisterRejectEmpty ) +{ + EXPECT_FALSE( CcpTelemetryCategoryRegister( "" ).second ); +} + +TEST_F( CcpTelemetryProfilerCategoryTest, RegistrationReturnsExisting ) +{ + auto ret = CcpTelemetryCategoryRegister( { "general" } ); + auto& keepAlive = ret.first; + EXPECT_TRUE( ret.second ); + EXPECT_EQ( CcpTelemetryCategoryGetName( keepAlive ), "general" ); +} + +TEST_F( CcpTelemetryProfilerCategoryTest, SetEmptyProfilerCategoriesClearsCategory ) +{ + auto [cpp, ok] = CcpTelemetryCategoryRegister( "cpp" ); + EXPECT_TRUE( ok ); + EXPECT_TRUE( CcpTelemetrySetActiveCategories( {cpp} ) ); + EXPECT_NE( CcpTelemetryCategories{}, CcpTelemetryGetActiveCategories() ); + EXPECT_TRUE( CcpTelemetrySetActiveCategories( {} ) ); + EXPECT_EQ( CcpTelemetryCategories{}, CcpTelemetryGetActiveCategories() ); +} + +TEST_F( CcpTelemetryProfilerCategoryTest, SetProfilerCategoryRejectsTooManyCategories ) +{ + auto [cat, ok] = CcpTelemetryCategoryRegister( "cpp" ); + EXPECT_TRUE( ok ); + CcpTelemetryCategories cats; + for ( int i = 0; i < 10000; ++i ) + { + cats.emplace_back( cat ); + } + EXPECT_FALSE( CcpTelemetrySetActiveCategories( cats ) ) << "Should have failed because more than the allowed number of categories was requested"; +} + +TEST_F( CcpTelemetryProfilerCategoryTest, ProfilerCategoryDefaultsAreRegistered ) +{ + CcpTelemetryCategories::const_iterator unused; + // The default ProfilerCategories must be available from the start. + EXPECT_TRUE( TryGetProfilerCategoryNamed( "core", unused ) ); + EXPECT_TRUE( TryGetProfilerCategoryNamed( "general", unused ) ); + EXPECT_TRUE( TryGetProfilerCategoryNamed( "cpp", unused ) ); +} diff --git a/tests/SilenceDeprecationWarnings.h b/tests/SilenceDeprecationWarnings.h new file mode 100644 index 0000000..a210805 --- /dev/null +++ b/tests/SilenceDeprecationWarnings.h @@ -0,0 +1,31 @@ +// Copyright © 2026 CCP ehf. + +// Helper utility for tests that need to cover deprecated functionality +#pragma once +#ifndef SilenceDeprecationWarnings_H +#define SilenceDeprecationWarnings_H + +#if defined(__clang__) + #define CCP_DISABLE_DEPRECATED_BEGIN \ + _Pragma("clang diagnostic push") \ + _Pragma("clang diagnostic ignored \"-Wdeprecated-declarations\"") + #define CCP_DISABLE_DEPRECATED_END \ + _Pragma("clang diagnostic pop") +#elif defined(__GNUC__) + #define CCP_DISABLE_DEPRECATED_BEGIN \ + _Pragma("GCC diagnostic push") \ + _Pragma("GCC diagnostic ignored \"-Wdeprecated-declarations\"") + #define CCP_DISABLE_DEPRECATED_END \ + _Pragma("GCC diagnostic pop") +#elif defined(_MSC_VER) + #define CCP_DISABLE_DEPRECATED_BEGIN \ + __pragma(warning(push)) \ + __pragma(warning(disable: 4996)) + #define CCP_DISABLE_DEPRECATED_END \ + __pragma(warning(pop)) +#else + #define CCP_DISABLE_DEPRECATED_BEGIN + #define CCP_DISABLE_DEPRECATED_END +#endif + +#endif diff --git a/tests/TracyTestClient.cpp b/tests/TracyTestClient.cpp index a2941b6..30959bc 100644 --- a/tests/TracyTestClient.cpp +++ b/tests/TracyTestClient.cpp @@ -425,7 +425,9 @@ void TracyTestClient::ProcessDecompressedData( const char* data, int sz ) if( strSz >= 9 ) { const char* p = ptr; - p += 4; // skip color + uint32_t color = 0; + std::memcpy( &color, p, 4 ); + p += 4; uint32_t line = 0; std::memcpy( &line, p, 4 ); p += 4; @@ -440,6 +442,7 @@ void TracyTestClient::ProcessDecompressedData( const char* data, int sz ) m_pendingZone.function = function; m_pendingZone.source = source; m_pendingZone.line = line; + m_pendingZone.color = color; if( nameLen > 0 ) m_pendingZone.name = std::string( p, nameLen ); m_hasPendingZone = true; diff --git a/tests/TracyTestClient.h b/tests/TracyTestClient.h index 99d95fb..7c9d2e6 100644 --- a/tests/TracyTestClient.h +++ b/tests/TracyTestClient.h @@ -23,6 +23,7 @@ class TracyTestClient std::string function; std::string source; uint32_t line = 0; + uint32_t color = 0; // Defaults to Black = 0x000000 }; using ZoneStack = std::vector;