Keep user manual chunks in a separate object.

This makes the user manual available outside of the LLM context.

The code is also more readable, as splitting the manual into sections and
splitting section content into chunks fit for embeddings is now separated.
A bug has been fixed, where the above splits were mixed up for the last
manual section, producing invalid data.

The unembedded manual contents are no longer held in the memory. The only
use case for this was to calculate the manual contents hash. The hash is
now precalculated and cached.
This commit is contained in:
Bartosz Taudul
2025-11-29 20:16:00 +01:00
parent b74cac005d
commit 154712bc81
10 changed files with 184 additions and 137 deletions

View File

@@ -12,7 +12,7 @@ The user manual
**Bartosz Taudul** [\<wolf@nereid.pl\>](mailto:wolf@nereid.pl)
2025-11-11 <https://github.com/wolfpld/tracy>
2025-11-29 <https://github.com/wolfpld/tracy>
:::
# Quick overview {#quick-overview .unnumbered}

View File

@@ -66,6 +66,7 @@ set(SERVER_FILES
TracyFileselector.cpp
TracyFilesystem.cpp
TracyImGui.cpp
TracyManualData.cpp
TracyMarkdown.cpp
TracyMicroArchitecture.cpp
TracyMouse.cpp

View File

@@ -24,7 +24,7 @@ extern double s_time;
constexpr size_t InputBufferSize = 1024;
TracyLlm::TracyLlm( Worker& worker )
TracyLlm::TracyLlm( Worker& worker, const TracyManualData& manual )
: m_exit( false )
, m_input( nullptr )
{
@@ -47,7 +47,7 @@ TracyLlm::TracyLlm( Worker& worker )
m_api = std::make_unique<TracyLlmApi>();
m_chatUi = std::make_unique<TracyLlmChat>();
m_tools = std::make_unique<TracyLlmTools>( worker );
m_tools = std::make_unique<TracyLlmTools>( worker, manual );
m_busy = true;
QueueConnect();

View File

@@ -19,6 +19,7 @@ namespace tracy
class TracyLlmApi;
class TracyLlmChat;
class TracyLlmTools;
class TracyManualData;
class Worker;
class TracyLlm
@@ -40,7 +41,7 @@ class TracyLlm
};
public:
TracyLlm( Worker& worker );
TracyLlm( Worker& worker, const TracyManualData& manual );
~TracyLlm();
[[nodiscard]] bool IsBusy() const { std::lock_guard lock( m_lock ); return m_busy; }

View File

@@ -9,15 +9,12 @@
#include <time.h>
#include "TracyConfig.hpp"
#include "TracyEmbed.hpp"
#include "TracyLlmApi.hpp"
#include "TracyLlmTools.hpp"
#include "TracyManualData.hpp"
#include "TracyStorage.hpp"
#include "TracyUtility.hpp"
#include "TracyWorker.hpp"
#include "tracy_xxhash.h"
#include "data/Manual.hpp"
constexpr const char* NoNetworkAccess = "Internet access is disabled by the user. You may inform the user that he can enable it in the settings, so that you can use the tools to gather information.";
@@ -81,122 +78,29 @@ static std::unique_ptr<pugi::xml_document> ParseHtml( const std::string& html )
return doc;
}
TracyLlmTools::TracyLlmTools( Worker& worker )
: m_manual( Unembed( Manual ) )
, m_worker( worker )
TracyLlmTools::TracyLlmTools( Worker& worker, const TracyManualData& manual )
: m_worker( worker )
, m_manual( manual )
{
std::string_view manual( m_manual->data(), m_manual->size() );
const auto sz = (int)m_manual->size();
std::vector<int> levels = { 0 };
std::vector<std::string> chapterNames = { "Title Page" };
int manualChunkPos = 0;
int pos = 0;
while( pos < sz )
int idx = 0;
for( auto& chunk : m_manual.GetChunks() )
{
std::string::size_type next = pos;
for(;;)
std::string hdr;
if( !chunk.section.empty() ) hdr += "Section " + chunk.section;
if( !chunk.title.empty() )
{
next = manual.find( '\n', next );
if( next == std::string_view::npos )
{
next = sz;
break;
}
if( next+1 >= sz || manual[next+1] == '\n' ) break;
next++;
if( !chunk.section.empty() ) hdr += ": ";
hdr += chunk.title;
}
if( next != pos )
hdr += '\n';
for( auto& line : SplitLines( chunk.text.c_str(), chunk.text.size() ) )
{
std::string_view line( manual.data() + pos, next - pos );
if( line != "---" && line != ":::" && line != "::: bclogo" )
{
if( line[0] == '#' )
{
if( manualChunkPos != pos )
{
auto start = manualChunkPos;
auto end = pos;
manualChunkPos = pos;
while( manual[start] != '\n' ) start++;
while( manual[start] == '\n' ) start++;
while( manual[end-1] == '\n' ) end--;
if( end > start )
{
std::string text, section, title, parents;
text = std::string( manual.data() + start, end - start );
if( levels[0] != 0 )
{
section = std::to_string( levels[0] );
for( size_t i=1; i<levels.size(); i++ ) section += "." + std::to_string( levels[i] );
}
if( levels.size() == 1 )
{
title = chapterNames[0];
}
else
{
title = chapterNames[levels.size()-1];
parents = chapterNames[0];
for( size_t i=1; i<levels.size() - 1; i++ ) parents += " > " + chapterNames[i];
}
m_manualChunks.emplace_back( ManualChunk {
.text = std::move( text ),
.section = std::move( section ),
.title = std::move( title ),
.parents = std::move( parents )
} );
}
}
int level = 1;
if( line.find( ".unnumbered}" ) == std::string_view::npos )
{
while( level < line.size() && line[level] == '#' ) level++;
if( level != levels.size() )
{
levels.resize( level, 0 );
chapterNames.resize( level );
}
levels[level - 1]++;
chapterNames[level - 1] = line.substr( level + 1 );
}
}
else
{
std::string chunk;
if( levels[0] != 0 )
{
chunk += "Section " + std::to_string( levels[0] );
for( size_t i=1; i<levels.size(); i++ ) chunk += "." + std::to_string( levels[i] );
chunk += "\n";
}
chunk += chapterNames[levels.size()-1] + "\n\n";
chunk += std::string( line );
m_chunkData.emplace_back( std::move( chunk ), m_manualChunks.size() );
}
}
if( line.empty() ) continue;
if( line == "---" || line == ":::" || line == "::: bclogo" ) continue;
m_chunkData.emplace_back( hdr + line, idx );
}
pos = next + 1;
while( pos < sz && manual[pos] == '\n' ) pos++;
}
if( manualChunkPos != pos )
{
std::string manualChunk;
if( levels[0] != 0 )
{
manualChunk += "Section " + std::to_string( levels[0] );
for( size_t i=1; i<levels.size(); i++ ) manualChunk += "." + std::to_string( levels[i] );
manualChunk += "\n";
}
manualChunk += "Navigation: " + chapterNames[0];
for( size_t i=1; i<levels.size(); i++ ) manualChunk += " > " + chapterNames[i];
manualChunk += "\n\n";
manualChunk += std::string( manual.data() + manualChunkPos, pos - manualChunkPos );
m_manualChunks.emplace_back( ManualChunk{std::move( manualChunk ), "", "", ""} );
idx++;
}
}
@@ -288,12 +192,11 @@ void TracyLlmTools::SelectManualEmbeddings( const std::string& model )
assert( !m_manualEmbeddingState.inProgress );
if( m_manualEmbeddingState.done && m_manualEmbeddingState.model == model ) return;
const uint64_t hash = XXH3_64bits( m_manual->data(), m_manual->size() );
auto cache = GetCachePath( model.c_str() );
try
{
m_manualEmbeddings = std::make_unique<TracyLlmEmbeddings>( cache, hash );
m_manualEmbeddings = std::make_unique<TracyLlmEmbeddings>( cache, m_manual.GetHash() );
m_manualEmbeddingState = { .model = model, .done = true };
}
catch( std::exception& ) {}
@@ -315,7 +218,6 @@ void TracyLlmTools::BuildManualEmbeddings( const std::string& model, TracyLlmApi
void TracyLlmTools::ManualEmbeddingsWorker( TracyLlmApi& api )
{
const uint64_t hash = XXH3_64bits( m_manual->data(), m_manual->size() );
auto cache = GetCachePath( m_manualEmbeddingState.model.c_str() );
std::unique_lock lock( m_lock );
@@ -395,7 +297,7 @@ void TracyLlmTools::ManualEmbeddingsWorker( TracyLlmApi& api )
i += bsz;
}
m_manualEmbeddings->Save( cache, hash );
m_manualEmbeddings->Save( cache, m_manual.GetHash() );
lock.lock();
m_manualEmbeddingState.inProgress = false;
@@ -825,12 +727,13 @@ std::string TracyLlmTools::SearchManual( const std::string& query, TracyLlmApi&
}
if( chunks.size() > MaxOutputChunks ) chunks.resize( MaxOutputChunks );
auto& manualChunks = m_manual.GetChunks();
const auto maxSize = CalcMaxSize();
int totalSize = 0;
int idx;
for( idx = 0; idx < chunks.size(); idx++ )
{
totalSize += m_manualChunks[chunks[idx].first].text.size();
totalSize += manualChunks[chunks[idx].first].text.size();
if( totalSize >= maxSize ) break;
}
if( idx < chunks.size() ) chunks.resize( idx );
@@ -838,7 +741,7 @@ std::string TracyLlmTools::SearchManual( const std::string& query, TracyLlmApi&
nlohmann::json json;
for( auto& chunk : chunks )
{
auto& m = m_manualChunks[chunk.first];
auto& m = manualChunks[chunk.first];
nlohmann::json r;
r["distance"] = chunk.second;
r["content"] = m.text;

View File

@@ -17,6 +17,7 @@ namespace tracy
{
class TracyLlmApi;
class TracyManualData;
class Worker;
class TracyLlmTools
@@ -36,15 +37,7 @@ public:
float progress = 0;
};
struct ManualChunk
{
std::string text;
std::string section;
std::string title;
std::string parents;
};
TracyLlmTools( Worker& worker );
TracyLlmTools( Worker& worker, const TracyManualData& manual );
~TracyLlmTools();
ToolReply HandleToolCalls( const nlohmann::json& json, TracyLlmApi& api, int contextSize, bool hasEmbeddingsModel );
@@ -82,11 +75,10 @@ private:
EmbeddingState m_manualEmbeddingState;
std::unique_ptr<TracyLlmEmbeddings> m_manualEmbeddings;
std::shared_ptr<EmbedData> m_manual;
std::vector<ManualChunk> m_manualChunks;
std::vector<std::pair<std::string, uint32_t>> m_chunkData;
Worker& m_worker;
const TracyManualData& m_manual;
};
}

View File

@@ -0,0 +1,107 @@
#include "TracyEmbed.hpp"
#include "TracyManualData.hpp"
#define XXH_INLINE_ALL
#include "tracy_xxhash.h"
#include "data/Manual.hpp"
namespace tracy
{
TracyManualData::TracyManualData()
{
auto data = Unembed( Manual );
m_hash = XXH3_64bits( data->data(), data->size() );
std::string_view manual( data->data(), data->size() );
const auto sz = (int)data->size();
std::vector<int> levels = { 0 };
std::vector<std::string> chapterNames = { "Title Page" };
int manualChunkPos = 0;
int pos = 0;
while( pos < sz )
{
std::string::size_type next = pos;
for(;;)
{
next = manual.find( '\n', next );
if( next == std::string_view::npos )
{
next = sz;
break;
}
if( next+1 >= sz || manual[next+1] == '\n' ) break;
next++;
}
if( next != pos )
{
std::string_view line( manual.data() + pos, next - pos );
if( line[0] == '#' )
{
if( manualChunkPos != pos )
{
AddManualChunk( manual, manualChunkPos, pos, levels, chapterNames );
manualChunkPos = pos;
}
int level = 1;
if( line.find( ".unnumbered}" ) == std::string_view::npos )
{
while( level < line.size() && line[level] == '#' ) level++;
if( level != levels.size() )
{
levels.resize( level, 0 );
chapterNames.resize( level );
}
levels[level - 1]++;
chapterNames[level - 1] = line.substr( level + 1 );
}
}
}
pos = next + 1;
while( pos < sz && manual[pos] == '\n' ) pos++;
}
if( manualChunkPos != pos )
{
AddManualChunk( manual, manualChunkPos, pos, levels, chapterNames );
}
}
void TracyManualData::AddManualChunk( const std::string_view& manual, int start, int end, const std::vector<int>& levels, const std::vector<std::string>& chapterNames )
{
while( manual[start] != '\n' ) start++;
while( manual[start] == '\n' ) start++;
while( manual[end-1] == '\n' ) end--;
if( end > start )
{
std::string text, section, title, parents;
text = std::string( manual.data() + start, end - start );
if( levels[0] != 0 )
{
section = std::to_string( levels[0] );
for( size_t i=1; i<levels.size(); i++ ) section += "." + std::to_string( levels[i] );
}
if( levels.size() == 1 )
{
title = chapterNames[0];
}
else
{
title = chapterNames[levels.size()-1];
parents = chapterNames[0];
for( size_t i=1; i<levels.size() - 1; i++ ) parents += " > " + chapterNames[i];
}
m_manualChunks.emplace_back( ManualChunk {
.text = std::move( text ),
.section = std::move( section ),
.title = std::move( title ),
.parents = std::move( parents )
} );
}
}
}

View File

@@ -0,0 +1,37 @@
#ifndef __TRACYMANUALDATA_HPP__
#define __TRACYMANUALDATA_HPP__
#include <stdint.h>
#include <string>
#include <string_view>
#include <vector>
namespace tracy
{
class TracyManualData
{
public:
struct ManualChunk
{
std::string text;
std::string section;
std::string title;
std::string parents;
};
TracyManualData();
[[nodiscard]] const std::vector<ManualChunk>& GetChunks() const { return m_manualChunks; }
[[nodiscard]] uint64_t GetHash() const { return m_hash; }
private:
void AddManualChunk( const std::string_view& manual, int manualChunkPos, int pos, const std::vector<int>& levels, const std::vector<std::string>& chapterNames );
std::vector<ManualChunk> m_manualChunks;
uint64_t m_hash;
};
}
#endif

View File

@@ -17,6 +17,7 @@
#include "TracyFileRead.hpp"
#include "TracyFilesystem.hpp"
#include "TracyImGui.hpp"
#include "TracyManualData.hpp"
#include "TracyPrint.hpp"
#include "TracySourceView.hpp"
#include "TracyTexture.hpp"
@@ -57,11 +58,12 @@ View::View( void(*cbMainThread)(const std::function<void()>&, bool), const char*
, m_achievements( s_config.achievements )
, m_horizontalScrollMultiplier( s_config.horizontalScrollMultiplier )
, m_verticalScrollMultiplier( s_config.verticalScrollMultiplier )
, m_manualData( std::make_shared<TracyManualData>() )
#ifdef __EMSCRIPTEN__
, m_td( 2, "ViewMt" )
#else
, m_td( std::thread::hardware_concurrency(), "ViewMt" )
, m_llm( m_worker )
, m_llm( m_worker, *m_manualData )
#endif
{
InitTextEditor();
@@ -86,11 +88,12 @@ View::View( void(*cbMainThread)(const std::function<void()>&, bool), FileRead& f
, m_achievements( s_config.achievements )
, m_horizontalScrollMultiplier( s_config.horizontalScrollMultiplier )
, m_verticalScrollMultiplier( s_config.verticalScrollMultiplier )
, m_manualData( std::make_shared<TracyManualData>() )
#ifdef __EMSCRIPTEN__
, m_td( 2, "ViewMt" )
#else
, m_td( std::thread::hardware_concurrency(), "ViewMt" )
, m_llm( m_worker )
, m_llm( m_worker, *m_manualData )
#endif
{
m_notificationTime = 4;

View File

@@ -63,6 +63,7 @@ struct CpuCtxDraw;
struct LockDraw;
struct PlotDraw;
struct FlameGraphContext;
class TracyManualData;
class View
@@ -929,6 +930,8 @@ private:
double m_horizontalScrollMultiplier = 1.0;
double m_verticalScrollMultiplier = 1.0;
std::shared_ptr<TracyManualData> m_manualData;
TaskDispatch m_td;
std::vector<FlameGraphItem> m_flameGraphData;
struct