diff --git a/profiler/src/llm/tools.json b/profiler/src/llm/tools.json index 3e2d2ee0..ea72200f 100644 --- a/profiler/src/llm/tools.json +++ b/profiler/src/llm/tools.json @@ -221,5 +221,26 @@ "required": ["address"] } } +}, +{ + "type": "function", + "function": { + "name": "sampling_stats", + "description": "Get sampling statistics for functions, sorted by time spent in each.", + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "ECMAScript regex to search for." + }, + "limit": { + "type": "integer", + "description": "Maximum number of entries to return. Default: 30." + } + }, + "required": [] + } + } } ] diff --git a/profiler/src/profiler/TracyLlmChat.cpp b/profiler/src/profiler/TracyLlmChat.cpp index 0ed74bb6..06c38d69 100644 --- a/profiler/src/profiler/TracyLlmChat.cpp +++ b/profiler/src/profiler/TracyLlmChat.cpp @@ -159,6 +159,13 @@ std::string TracyLlmChat::ToolCallDescription( const nlohmann::json& json ) cons if( args.contains( "limit" ) ) limit = ", limit: " + std::to_string( args["limit"].get() ); return "Symbol parents: " + std::string( m_worker.GetString( sym->name ) ) + limit; } + else if( name == "sampling_stats" ) + { + std::string query, limit; + if( args.contains( "query" ) ) query = ", query: " + args["query"].get_ref(); + if( args.contains( "limit" ) ) limit = ", limit: " + std::to_string( args["limit"].get() ); + return "Sampling stats" + query + limit; + } return ""; } diff --git a/profiler/src/profiler/TracyLlmTools.cpp b/profiler/src/profiler/TracyLlmTools.cpp index 587ca54a..24260f2b 100644 --- a/profiler/src/profiler/TracyLlmTools.cpp +++ b/profiler/src/profiler/TracyLlmTools.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -203,6 +204,11 @@ std::string TracyLlmTools::HandleToolCalls( const std::string& tool, const nlohm { return SymbolParents( Param( "address" ), ParamOptU32( "limit", 10 ) ); } + else if( tool == "sampling_stats" ) + { + std::string empty; + return SamplingStats( ParamOptString( "query", empty ), ParamOptU32( "limit", 30 ) ); + } #endif return "Unknown tool call: " + tool; } @@ -1106,6 +1112,112 @@ std::string TracyLlmTools::SymbolParents( const std::string& address, uint32_t l } return result.dump( -1, ' ', false, nlohmann::json::error_handler_t::replace ); } + +std::string TracyLlmTools::SamplingStats( const std::string& query, uint32_t limit ) const +{ + if( !m_worker.AreSymbolSamplesReady() ) return "Sampling data is not ready yet. Wait for background processing to complete."; + if( m_worker.GetCallstackSampleCount() == 0 ) return "No call stack samples in this trace."; + + std::regex rx; + if( !query.empty() ) + { + try + { + rx = std::regex( query ); + } + catch( const std::regex_error& e ) + { + return "Error: Invalid query regex: " + std::string( e.what() ); + } + } + + const auto& symMap = m_worker.GetSymbolMap(); + const auto& symStat = m_worker.GetSymbolStats(); + + struct SymEntry + { + uint64_t symAddr; + uint32_t excl; + }; + + std::vector data; + data.reserve( symStat.size() ); + for( auto& v : symStat ) + { + auto sit = symMap.find( v.first ); + if( sit == symMap.end() ) continue; + data.emplace_back( v.first, v.second.excl ); + } + if( data.empty() ) return "No symbol statistics available."; + + unordered_flat_map baseMap; + for( auto& v : data ) + { + auto sym = m_worker.GetSymbolData( v.symAddr ); + const auto symAddr = ( sym && sym->isInline ) ? m_worker.GetSymbolForAddress( v.symAddr ) : v.symAddr; + auto it = baseMap.find( symAddr ); + if( it == baseMap.end() ) + { + baseMap.emplace( symAddr, SymEntry { symAddr, v.excl } ); + } + else + { + assert( symAddr == it->second.symAddr ); + it->second.excl += v.excl; + } + } + + data.clear(); + for( auto& v : baseMap ) + { + auto sit = symMap.find( v.second.symAddr ); + if( sit == symMap.end() ) continue; + if( !query.empty() ) + { + const auto name = m_worker.GetString( sit->second.name ); + if( !std::regex_search( name, rx ) ) continue; + } + data.emplace_back( v.second ); + } + if( data.empty() ) return "No symbols match the query."; + + pdqsort_branchless( data.begin(), data.end(), []( const auto& l, const auto& r ) { return l.excl > r.excl; } ); + if( data.size() > limit ) data.resize( limit ); + + const auto period = m_worker.GetSamplingPeriod(); + const auto totalSamples = m_worker.GetCallstackSampleCount(); + + nlohmann::json result = { + { "total_time", TimeToString( totalSamples * period ) }, + { "entries", nlohmann::json::array() }, + { "hint", "Entries are sorted by exclusive time (child time not included), highest first." } + }; + auto& entries = result["entries"]; + + for( auto& v : data ) + { + auto sit = symMap.find( v.symAddr ); + assert( sit != symMap.end() ); + + char addr[32]; + snprintf( addr, sizeof( addr ), "0x%" PRIx64, v.symAddr ); + + const auto file = m_worker.GetString( sit->second.file ); + char loc[1024]; + snprintf( loc, sizeof( loc ), "%s:%u", file, sit->second.line ); + + entries.push_back( { + { "name", m_worker.GetString( sit->second.name ) }, + { "address", addr }, + { "location", loc }, + { "image", m_worker.GetString( sit->second.imageName ) }, + { "time", TimeToString( v.excl * period ) }, + { "code_size", MemSizeToString( sit->second.size.Val() ) } + } ); + } + + return result.dump( -1, ' ', false, nlohmann::json::error_handler_t::replace ); +} #endif } diff --git a/profiler/src/profiler/TracyLlmTools.hpp b/profiler/src/profiler/TracyLlmTools.hpp index 673c48b1..72dc905f 100644 --- a/profiler/src/profiler/TracyLlmTools.hpp +++ b/profiler/src/profiler/TracyLlmTools.hpp @@ -68,6 +68,7 @@ private: std::string SymbolDisasm( const std::string& address ) const; #ifndef TRACY_NO_STATISTICS std::string SymbolParents( const std::string& address, uint32_t limit ) const; + std::string SamplingStats( const std::string& query, uint32_t limit ) const; #endif void ManualEmbeddingsWorker( TracyLlmApi& api );