diff --git a/CMakeLists.txt b/CMakeLists.txt index c0ec58a..e29e75f 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,10 @@ 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(JOIN CORE_COMPILE_DEFINITIONS " \\\n" CORE_COMPILE_DEFINITIONS) + create_carbon_docs_sphinx_target( PYTHON_EXE ${Python3_EXECUTABLE} VENV_NAME docs_generation_venv @@ -187,6 +192,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/CcpTelemetry.cpp b/CcpTelemetry.cpp index 00c0ae4..ae7dd5f 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" @@ -25,6 +30,13 @@ struct TelemetryZone::Private FiberNameStore::const_iterator fiber; }; +struct CcpTelemetryCategory +{ + std::string name; + CcpColor color{CcpColor::White}; + uint64_t captureBit{0}; +}; + enum ProfilerState { Stopped, StartRequested, @@ -54,7 +66,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,6 +95,132 @@ 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; + } +} + +bool operator==( const CcpTelemetryCategory& lhs, const CcpTelemetryCategory& rhs ) +{ + // Profiler Categories need to be unique by name only + return lhs.name == rhs.name; +} +const std::string& CcpTelemetryCategoryGetName( const CcpTelemetryCategory& category ) +{ + return category.name; +} +CcpColor CcpTelemetryCategoryGetColor( const CcpTelemetryCategory& category ) +{ + return category.color; +} + +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 }; +} + +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; +} + +bool CcpTelemetrySetActiveCategories( const std::vector& maskNames ) +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; +} + +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; } bool CcpTelemetryIsConnected() @@ -198,23 +336,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( it->first ); + it = s_fiberEraseMap.erase( it ); + } else { - s_fiberNameStore.erase( elem.first ); - s_fiberEraseMap.pop(); - elem = s_fiberEraseMap.front(); + ++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" ); @@ -332,6 +471,7 @@ void CcpTelemetrySetActiveFiber( FiberNameStore::const_iterator elem ) 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() ); @@ -365,7 +505,7 @@ 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()) +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,10 +514,25 @@ 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 ) ); +} +TelemetryZone::TelemetryZone( const CcpTelemetryCategory& category, const char* name, const char* filename, uint32_t lineno ) +{ + 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 ) ); } TelemetryZone::TelemetryZone( TelemetryZone&& other ) noexcept : m_impl( std::make_unique() ) @@ -419,8 +574,7 @@ void CcpTelemetryEnterZone( void* key, const char* name, const char* filename, u 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 ); } } @@ -481,6 +635,25 @@ void CcpRegisterThread( CcpThreadId_t threadId, const char* name ) { } +CcpProfilerCategoryHandle CcpRegisterProfilerCategory( const std::string&, CcpColor ) +{ + return 0; +} + +std::vector CcpGetRegisteredProfilerCategories() +{ + return {}; +} + +bool CcpSetActiveProfilerCategory( const std::vector& ) +{ +} + +std::vector CcpGetActiveProfilerCategory() +{ + return {}; +} + bool CcpStartTelemetry( const char* server, int connectionType, uint32_t maxThreadCount ) { return false; 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..04a6281 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. @@ -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 @@ -1084,7 +1084,7 @@ EXCLUDE_SYMBOLS = # 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 #--------------------------------------------------------------------------- @@ -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/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..fcfbf0a 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,45 @@ 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 = CcpTelemetryCategoryRegister( "cpp" ); + auto generalCategory = CcpTelemetryCategoryRegister( "general" ); + EXPECT_TRUE( cppCategory.second ); + EXPECT_TRUE( generalCategory.second ); + CcpTelemetrySetActiveCategories( { cppCategory.first, generalCategory.first } ); + { + 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() ); + } + // Now the activeZone has gone out of scope, so the zone should have ended + TickTelemetry( [this] { return m_tracyClient.GetZoneEndCount() == 1; } ); + EXPECT_EQ( 1, 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( 1, m_tracyClient.GetZoneBeginCount() ) << "Inactive zone must not emit ZoneBegin, count should stay at 1"; + EXPECT_EQ( 1, 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 +415,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 +440,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 +525,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 +547,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 +571,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 +596,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 +626,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;