diff options
Diffstat (limited to 'src/zen/trace/trace_viewer_service.cpp')
| -rw-r--r-- | src/zen/trace/trace_viewer_service.cpp | 1225 |
1 files changed, 1225 insertions, 0 deletions
diff --git a/src/zen/trace/trace_viewer_service.cpp b/src/zen/trace/trace_viewer_service.cpp new file mode 100644 index 000000000..7d8301ae2 --- /dev/null +++ b/src/zen/trace/trace_viewer_service.cpp @@ -0,0 +1,1225 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "trace_viewer_service.h" + +#include "timeline_query.h" + +#include <zencore/compactbinarybuilder.h> +#include <zencore/filesystem.h> +#include <zencore/fmtutils.h> +#include <zencore/iobuffer.h> +#include <zencore/logging.h> +#include <zencore/string.h> +#include <zenhttp/httpcommon.h> + +#include <algorithm> +#include <charconv> +#include <cstdio> +#include <cstdlib> +#include <string> +#include <type_traits> + +#if !defined(ZEN_EMBED_ZEN_HTML_ZIP) +# define ZEN_EMBED_ZEN_HTML_ZIP 0 +#endif + +#if ZEN_EMBED_ZEN_HTML_ZIP +static unsigned char gZenHtmlZipData[] = { +# include <zen-html.zip.h> +}; +#endif + +namespace zen { + +namespace { + + // Parse a uint32 query parameter; returns the fallback on error / absent. + // The entire string must be a valid base-10 unsigned integer. + uint32_t ParseUintParam(std::string_view Value, uint32_t Fallback) + { + if (Value.empty()) + { + return Fallback; + } + + uint32_t Number = 0; + auto [Ptr, Ec] = std::from_chars(Value.data(), Value.data() + Value.size(), Number, 10); + if (Ec != std::errc() || Ptr != Value.data() + Value.size()) + { + return Fallback; + } + + return Number; + } + + void WriteNotFound(HttpServerRequest& Request, std::string_view Message = "Not found") + { + Request.WriteResponse(HttpResponseCode::NotFound, HttpContentType::kText, Message); + } + + struct CallstackSummaryInfo + { + std::string Summary; + std::string TopFrame; + std::string SecondaryFrame; + std::string GroupKey; + uint32_t HiddenPrefixCount = 0; + bool IncludedThirdPartyBoundary = false; + }; + + CallstackSummaryInfo BuildCallstackSummary(const trace_detail::FilteredCallstackView& View) + { + CallstackSummaryInfo Result; + Result.HiddenPrefixCount = View.HiddenPrefixCount; + Result.IncludedThirdPartyBoundary = View.IncludedThirdPartyBoundary; + if (View.Frames.empty()) + { + Result.Summary = "No frames"; + Result.GroupKey = "No frames"; + return Result; + } + + Result.TopFrame = View.Frames[0].Display; + Result.GroupKey = View.Frames[0].Display; + if (View.Frames.size() > 1) + { + Result.SecondaryFrame = View.Frames[1].Display; + Result.Summary = fmt::format("{} \xE2\x86\x90 {}", Result.TopFrame, Result.SecondaryFrame); + Result.GroupKey = fmt::format("{} | {}", Result.TopFrame, Result.SecondaryFrame); + } + else + { + Result.Summary = Result.TopFrame; + } + return Result; + } + + // Append a base-10 unsigned integer to a string builder via std::to_chars. + // Reserves the worst-case digit count up front, writes directly into the + // builder's buffer, then trims the unused suffix. About 5–10× faster than + // going through StringBuilder::operator<<(uint32_t), which routes integer + // formatting through snprintf via IntNum. + template<typename T> + inline void AppendUintFast(StringBuilderBase& Sb, T Value) + { + static_assert(std::is_unsigned_v<T> && std::is_integral_v<T>); + // digits10 is the largest K such that 10^K fits — the longest + // printable representation is digits10 + 1 digits. +1 more for safety. + constexpr size_t MaxDigits = std::numeric_limits<T>::digits10 + 2; + + const size_t Off = Sb.AddUninitialized(MaxDigits); + char* const Begin = Sb.Data() + Off; + const auto Result = std::to_chars(Begin, Begin + MaxDigits, Value); + const size_t Written = size_t(Result.ptr - Begin); + Sb.RemoveSuffix(uint32_t(MaxDigits - Written)); + } + + // Render a span of TimelineScopeView records directly into a string + // builder using the wire format consumed by the trace viewer front-end: + // [[beginUs, durationUs, nameId, depth, mergeCount?], ...] + // The trailing mergeCount element is only emitted for LOD-merged scopes. + // Output is compact (no whitespace) — the viewer parses both forms but + // dropping the spaces shaves ~10% off the response size. + void AppendScopesJsonArray(StringBuilderBase& Sb, const trace_detail::TimelineScopeView* Scopes, size_t Count) + { + Sb << '['; + for (size_t I = 0; I < Count; ++I) + { + const trace_detail::TimelineScopeView& S = Scopes[I]; + if (I > 0) + { + Sb << ','; + } + Sb << '['; + AppendUintFast(Sb, S.BeginUs); + Sb << ','; + AppendUintFast(Sb, S.DurationUs); + Sb << ','; + AppendUintFast(Sb, S.NameId); + Sb << ','; + AppendUintFast(Sb, S.Depth); + if (S.MergeCount > 1) + { + Sb << ','; + AppendUintFast(Sb, S.MergeCount); + } + Sb << ']'; + } + Sb << ']'; + } + +} // namespace + +////////////////////////////////////////////////////////////////////////////// + +TraceViewerService::TraceViewerService(const trace_detail::TraceModel& Model, + std::unique_ptr<trace_detail::SymbolResolver> Symbols, + std::filesystem::path DevHtmlDir) +: m_Model(Model) +, m_DevHtmlDir(std::move(DevHtmlDir)) +, m_Symbols(std::move(Symbols)) +, m_CallstackFormatter(m_Model, m_Symbols.get()) +{ +#if ZEN_EMBED_ZEN_HTML_ZIP + IoBuffer ZipBuffer(IoBuffer::Wrap, gZenHtmlZipData, sizeof(gZenHtmlZipData) - 1); + m_ZipFs = std::make_unique<ZipFs>(std::move(ZipBuffer)); +#endif + + m_TimelineQuery = trace_detail::MakeInMemoryTimelineQuery(m_Model); + + if (m_DevHtmlDir.empty()) + { + // Probe for development layout: walk up from the running executable + // until we find a directory named xmake.lua, then look for the html + // tree under src/zen/frontend/html. + std::filesystem::path Path = GetRunningExecutablePath(); + std::error_code Ec; + while (Path.has_parent_path()) + { + std::filesystem::path Parent = Path.parent_path(); + if (Parent == Path) + { + break; + } + if (IsFile(Parent / "xmake.lua", Ec)) + { + std::filesystem::path Candidate = Parent / "src" / "zen" / "frontend" / "html"; + if (IsDir(Candidate, Ec)) + { + m_DevHtmlDir = Candidate; + } + break; + } + Path = Parent; + } + } + + if (m_ZipFs) + { + ZEN_INFO("trace viewer front-end is served from embedded zip"); + } + else if (!m_DevHtmlDir.empty()) + { + ZEN_INFO("trace viewer front-end is served from '{}'", m_DevHtmlDir); + } + else + { + ZEN_WARN("trace viewer front-end is NOT AVAILABLE — only /api/* endpoints will respond"); + } +} + +TraceViewerService::~TraceViewerService() = default; + +const char* +TraceViewerService::BaseUri() const +{ + // Mounted at a sub-path so we don't collide with the http.sys server's + // own root handler on Windows. + return "/trace/"; +} + +void +TraceViewerService::HandleRequest(HttpServerRequest& Request) +{ + using namespace std::literals; + + std::string_view Uri = Request.RelativeUriWithExtension(); + for (; !Uri.empty() && Uri[0] == '/'; Uri = Uri.substr(1)) + { + } + + if (Uri.starts_with("api/"sv)) + { + HandleApiRequest(Request, Uri.substr(4)); + return; + } + + HandleStaticAsset(Request, Uri); +} + +////////////////////////////////////////////////////////////////////////////// +// Static asset handling + +void +TraceViewerService::HandleStaticAsset(HttpServerRequest& Request, std::string_view Uri) +{ + using namespace std::literals; + + ExtendableStringBuilder<256> UriBuilder; + if (Uri.empty()) + { + Uri = "index.html"sv; + } + else if (Uri.back() == '/') + { + UriBuilder << Uri << "index.html"sv; + Uri = UriBuilder; + } + + // Path traversal guard: reject parent refs, Windows-style separators, and absolute + // paths. `std::filesystem::path::operator/=` replaces the base when the RHS is + // absolute, so without this check a URI like `C:/Windows/...` would escape m_DevHtmlDir. + if (Uri.find("..") != Uri.npos || Uri.find('\\') != Uri.npos || std::filesystem::path(Uri).is_absolute()) + { + Request.WriteResponse(HttpResponseCode::Forbidden); + return; + } + + HttpContentType ContentType = HttpContentType::kUnknownContentType; + if (const size_t DotIndex = Uri.rfind("."); DotIndex != Uri.npos) + { + const std::string_view DotExt = Uri.substr(DotIndex + 1); + ContentType = ParseContentType(DotExt); + if (ContentType == HttpContentType::kUnknownContentType) + { + if (DotExt == "txt"sv || DotExt == "md"sv) + { + ContentType = HttpContentType::kText; + } + } + } + + if (ContentType == HttpContentType::kUnknownContentType) + { + Request.WriteResponse(HttpResponseCode::Forbidden); + return; + } + + // Dev mode: serve from disk first so HTML/JS edits show up without a rebuild + if (!m_DevHtmlDir.empty()) + { + std::filesystem::path FullPath = m_DevHtmlDir / std::filesystem::path(Uri).make_preferred(); + FileContents File = ReadFile(FullPath); + if (!File.ErrorCode) + { + Request.WriteResponse(HttpResponseCode::OK, ContentType, File.Data[0]); + return; + } + } + + // Fallback: embedded zip + if (m_ZipFs) + { + if (IoBuffer File = m_ZipFs->GetFile(Uri)) + { + Request.WriteResponse(HttpResponseCode::OK, ContentType, File); + return; + } + } + + WriteNotFound(Request); +} + +////////////////////////////////////////////////////////////////////////////// +// REST endpoints + +void +TraceViewerService::HandleApiRequest(HttpServerRequest& Request, std::string_view Path) +{ + using namespace std::literals; + + if (Path == "session"sv) + { + HandleSessionApi(Request); + } + else if (Path == "threads"sv) + { + HandleThreadsApi(Request); + } + else if (Path == "channels"sv) + { + HandleChannelsApi(Request); + } + else if (Path == "scope-stats"sv) + { + HandleScopeStatsApi(Request); + } + else if (Path == "scope-names"sv) + { + HandleScopeNamesApi(Request); + } + else if (Path == "timeline"sv) + { + HandleTimelineApi(Request); + } + else if (Path == "timeline-batch"sv) + { + HandleTimelineBatchApi(Request); + } + else if (Path == "log-categories"sv) + { + HandleLogCategoriesApi(Request); + } + else if (Path == "logs"sv) + { + HandleLogsApi(Request); + } + else if (Path == "bookmarks"sv) + { + HandleBookmarksApi(Request); + } + else if (Path == "regions"sv) + { + HandleRegionsApi(Request); + } + else if (Path == "csv-categories"sv) + { + HandleCsvCategoriesApi(Request); + } + else if (Path == "csv-stats"sv) + { + HandleCsvStatsApi(Request); + } + else if (Path == "csv-series"sv) + { + HandleCsvSeriesApi(Request); + } + else if (Path == "csv-events"sv) + { + HandleCsvEventsApi(Request); + } + else if (Path == "csv-metadata"sv) + { + HandleCsvMetadataApi(Request); + } + else if (Path == "alloc-summary"sv) + { + HandleAllocSummaryApi(Request); + } + else if (Path == "heaps"sv) + { + HandleHeapsApi(Request); + } + else if (Path == "alloc-tags"sv) + { + HandleAllocTagsApi(Request); + } + else if (Path == "memory-timeline"sv) + { + HandleMemoryTimelineApi(Request); + } + else if (Path == "heap-stats"sv) + { + HandleHeapStatsApi(Request); + } + else if (Path == "callstacks"sv) + { + HandleCallstacksApi(Request); + } + else if (Path == "callstack-stats"sv) + { + HandleCallstackStatsApi(Request); + } + else if (Path == "churn-stats"sv) + { + HandleChurnStatsApi(Request); + } + else if (Path == "alloc-size-histogram"sv) + { + HandleAllocSizeHistogramApi(Request); + } + else + { + WriteNotFound(Request, "Unknown API endpoint"); + } +} + +void +TraceViewerService::HandleSessionApi(HttpServerRequest& Request) +{ + const trace_detail::SessionInfo& Session = m_Model.Session; + + CbObjectWriter Obj; + Obj << "file_path" << m_Model.FilePath.string(); + Obj << "file_size" << m_Model.FileSize; + Obj << "total_events" << m_Model.TotalEvents; + Obj << "parse_time_ms" << m_Model.ParseTimeMs; + Obj << "trace_start_us" << m_Model.TraceStartUs; + Obj << "trace_end_us" << m_Model.TraceEndUs; + Obj << "has_session" << Session.HasSession; + Obj << "platform" << Session.Platform; + Obj << "app_name" << Session.AppName; + Obj << "project_name" << Session.ProjectName; + Obj << "command_line" << Session.CommandLine; + Obj << "branch" << Session.Branch; + Obj << "build_version" << Session.BuildVersion; + Obj << "changelist" << Session.Changelist; + Obj << "has_memory_data" << m_Model.AllocSummary.HasMemoryData; + + Request.WriteResponse(HttpResponseCode::OK, Obj.Save()); +} + +void +TraceViewerService::HandleThreadsApi(HttpServerRequest& Request) +{ + CbWriter Writer; + Writer.BeginArray(); + for (const trace_detail::ThreadInfoEntry& Thread : m_Model.Threads) + { + Writer.BeginObject(); + Writer << "thread_id" << Thread.ThreadId; + Writer << "name" << Thread.Name; + Writer << "group" << Thread.GroupName; + Writer << "system_id" << Thread.SystemId; + Writer << "sort_hint" << Thread.SortHint; + // Lane threads use synthetic IDs starting at 2048 (see lane_trace.inl). + // SystemId==0 alone is insufficient — the main/trace threads also lack + // a system ID in some traces. + Writer << "is_lane" << (Thread.SystemId == 0 && Thread.ThreadId >= 2048); + + // Per-thread timeline summary: whether we captured scopes and their span. + auto It = std::find_if(m_Model.Timelines.begin(), + m_Model.Timelines.end(), + [Tid = Thread.ThreadId](const trace_detail::ThreadTimeline& T) { return T.ThreadId == Tid; }); + if (It != m_Model.Timelines.end()) + { + Writer << "scope_count" << uint64_t(It->Scopes.size()); + } + else + { + Writer << "scope_count" << uint64_t(0); + } + + Writer.EndObject(); + } + Writer.EndArray(); + + Request.WriteResponse(HttpResponseCode::OK, Writer.Save().AsArray()); +} + +void +TraceViewerService::HandleChannelsApi(HttpServerRequest& Request) +{ + CbWriter Writer; + Writer.BeginArray(); + for (const trace_detail::ChannelInfo& Channel : m_Model.Channels) + { + Writer.BeginObject(); + Writer << "name" << Channel.Name; + Writer << "enabled" << Channel.Enabled; + Writer << "readonly" << Channel.ReadOnly; + Writer.EndObject(); + } + Writer.EndArray(); + + Request.WriteResponse(HttpResponseCode::OK, Writer.Save().AsArray()); +} + +void +TraceViewerService::HandleScopeStatsApi(HttpServerRequest& Request) +{ + CbWriter Writer; + Writer.BeginArray(); + for (const trace_detail::CpuScopeStat& Stat : m_Model.ScopeStats) + { + Writer.BeginObject(); + Writer << "name" << Stat.Name; + Writer << "count" << Stat.Count; + Writer << "min_us" << Stat.MinUs; + Writer << "max_us" << Stat.MaxUs; + Writer.AddFloat("mean_us", Stat.MeanUs); + Writer.AddFloat("stdev_us", Stat.StdDevUs); + Writer.EndObject(); + } + Writer.EndArray(); + + Request.WriteResponse(HttpResponseCode::OK, Writer.Save().AsArray()); +} + +void +TraceViewerService::HandleScopeNamesApi(HttpServerRequest& Request) +{ + CbWriter Writer; + Writer.BeginArray(); + for (const std::string& Name : m_Model.ScopeNames) + { + Writer.AddString(Name); + } + Writer.EndArray(); + + Request.WriteResponse(HttpResponseCode::OK, Writer.Save().AsArray()); +} + +void +TraceViewerService::HandleTimelineApi(HttpServerRequest& Request) +{ + HttpServerRequest::QueryParams Params = Request.GetQueryParams(); + + std::string_view ThreadStr = Params.GetValue("thread"); + if (ThreadStr.empty()) + { + Request.WriteResponse(HttpResponseCode::BadRequest, HttpContentType::kText, "thread parameter required"); + return; + } + + uint32_t ThreadId = ParseUintParam(ThreadStr, ~uint32_t(0)); + + trace_detail::TimelineQueryRequest Req; + Req.StartUs = ParseUintParam(Params.GetValue("start"), 0u); + Req.EndUs = ParseUintParam(Params.GetValue("end"), ~uint32_t(0)); + Req.MinDurUs = ParseUintParam(Params.GetValue("mindur"), 0u); + Req.ResolutionUs = ParseUintParam(Params.GetValue("resolution"), 0u); + + std::vector<trace_detail::TimelineScopeView> Scopes; + m_TimelineQuery->QueryThread(ThreadId, Req, Scopes); + + // Direct string formatting for the timeline wire format — the compact + // [[beginUs,durationUs,nameId,depth,mergeCount?],...] arrays are faster + // to serialize directly than via CbWriter. The 64 KB inline buffer + // covers small viewport queries without heap traffic. + ExtendableStringBuilder<65536> Sb; + Sb << R"({"thread_id":)"; + AppendUintFast(Sb, ThreadId); + Sb << R"(,"scopes":)"; + AppendScopesJsonArray(Sb, Scopes.data(), Scopes.size()); + Sb << '}'; + Request.WriteResponse(HttpResponseCode::OK, HttpContentType::kJSON, Sb.ToView()); +} + +void +TraceViewerService::HandleTimelineBatchApi(HttpServerRequest& Request) +{ + HttpServerRequest::QueryParams Params = Request.GetQueryParams(); + + std::string_view ThreadsStr = Params.GetValue("threads"); + if (ThreadsStr.empty()) + { + Request.WriteResponse(HttpResponseCode::BadRequest, HttpContentType::kText, "threads parameter required"); + return; + } + + trace_detail::TimelineQueryRequest Req; + Req.StartUs = ParseUintParam(Params.GetValue("start"), 0u); + Req.EndUs = ParseUintParam(Params.GetValue("end"), ~uint32_t(0)); + Req.MinDurUs = ParseUintParam(Params.GetValue("mindur"), 0u); + Req.ResolutionUs = ParseUintParam(Params.GetValue("resolution"), 0u); + + // Parse comma-separated thread IDs. Tokens with invalid IDs (~0u) are + // skipped — same behaviour as the previous handler. + std::vector<uint32_t> ThreadIds; + { + std::string_view Remaining = ThreadsStr; + while (!Remaining.empty()) + { + size_t Comma = Remaining.find(','); + std::string_view Token = (Comma != std::string_view::npos) ? Remaining.substr(0, Comma) : Remaining; + Remaining = (Comma != std::string_view::npos) ? Remaining.substr(Comma + 1) : std::string_view{}; + + uint32_t ThreadId = ParseUintParam(Token, ~uint32_t(0)); + if (ThreadId == ~uint32_t(0)) + { + continue; + } + ThreadIds.push_back(ThreadId); + } + } + + trace_detail::TimelineQuery::BatchResult Batch; + m_TimelineQuery->QueryBatch(ThreadIds, Req, Batch); + + // Multi-chunk response: one IoBuffer per thread plus the surrounding + // "{" / "}" braces. Avoids materialising the entire JSON in a single + // contiguous allocation. The transport gathers the chunks at write time. + static constexpr char kOpenBrace[] = "{"; + static constexpr char kCloseBrace[] = "}"; + + std::vector<IoBuffer> Chunks; + Chunks.reserve(2 + ThreadIds.size()); + Chunks.emplace_back(IoBuffer::Wrap, kOpenBrace, 1); + + for (size_t I = 0; I < ThreadIds.size(); ++I) + { + const trace_detail::TimelineQuery::BatchResult::Range R = Batch.Ranges[I]; + + // Per-thread chunk: optional leading comma, "<threadId>":{"scopes":[...]} + // 32 KB inline covers most threads at typical viewport zoom levels. + ExtendableStringBuilder<32768> Sb; + if (I > 0) + { + Sb << ','; + } + Sb << '"'; + AppendUintFast(Sb, ThreadIds[I]); + Sb << R"(":{"scopes":)"; + AppendScopesJsonArray(Sb, Batch.Scopes.data() + R.Begin, R.End - R.Begin); + Sb << '}'; + + // Clone into an IoBuffer so the chunk owns its bytes — the builder + // dies at the end of this iteration. + const std::string_view View = Sb.ToView(); + Chunks.emplace_back(IoBuffer::Clone, View.data(), View.size()); + } + + Chunks.emplace_back(IoBuffer::Wrap, kCloseBrace, 1); + + Request.WriteResponse(HttpResponseCode::OK, HttpContentType::kJSON, std::span<IoBuffer>{Chunks}); +} + +void +TraceViewerService::HandleLogCategoriesApi(HttpServerRequest& Request) +{ + CbWriter Writer; + Writer.BeginArray(); + for (const trace_detail::LogCategoryInfo& Cat : m_Model.LogCategories) + { + Writer.BeginObject(); + Writer << "name" << Cat.Name; + Writer << "default_verbosity" << uint32_t(Cat.DefaultVerbosity); + Writer.EndObject(); + } + Writer.EndArray(); + + Request.WriteResponse(HttpResponseCode::OK, Writer.Save().AsArray()); +} + +void +TraceViewerService::HandleLogsApi(HttpServerRequest& Request) +{ + HttpServerRequest::QueryParams Params = Request.GetQueryParams(); + + uint32_t StartUs = ParseUintParam(Params.GetValue("start"), 0u); + uint32_t EndUs = ParseUintParam(Params.GetValue("end"), ~uint32_t(0)); + uint32_t MinVerb = ParseUintParam(Params.GetValue("min_verbosity"), 0u); + uint32_t CategoryId = ParseUintParam(Params.GetValue("category"), ~uint32_t(0)); + uint32_t Limit = ParseUintParam(Params.GetValue("limit"), 5000u); + + // Binary-search lower bound by TimeUs. + const eastl::vector<trace_detail::LogEntry>& Entries = m_Model.LogEntries; + auto FirstIt = + std::lower_bound(Entries.begin(), Entries.end(), StartUs, [](const trace_detail::LogEntry& E, uint32_t V) { return E.TimeUs < V; }); + + CbObjectWriter Obj; + Obj << "total" << uint64_t(Entries.size()); + + uint32_t Emitted = 0; + Obj.BeginArray("entries"); + for (auto It = FirstIt; It != Entries.end() && Emitted < Limit; ++It) + { + if (It->TimeUs > EndUs) + { + break; + } + if (MinVerb != 0 && It->Verbosity > MinVerb) + { + // Lower verbosity value = higher severity in UE's ELogVerbosity. + // Skip entries less severe than the requested floor. + continue; + } + if (CategoryId != ~uint32_t(0) && It->CategoryIndex != CategoryId) + { + continue; + } + + Obj.BeginObject(); + Obj << "time_us" << It->TimeUs; + Obj << "category_index" << It->CategoryIndex; + Obj << "verbosity" << uint32_t(It->Verbosity); + Obj << "line" << It->Line; + Obj << "file" << It->File; + Obj << "message" << It->Message; + Obj.EndObject(); + ++Emitted; + } + Obj.EndArray(); + + Obj << "returned" << Emitted; + + Request.WriteResponse(HttpResponseCode::OK, Obj.Save()); +} + +void +TraceViewerService::HandleBookmarksApi(HttpServerRequest& Request) +{ + CbWriter Writer; + Writer.BeginArray(); + for (const trace_detail::Bookmark& B : m_Model.Bookmarks) + { + Writer.BeginObject(); + Writer << "time_us" << B.TimeUs; + Writer << "line" << B.Line; + Writer << "file" << B.File; + Writer << "text" << B.Text; + Writer.EndObject(); + } + Writer.EndArray(); + + Request.WriteResponse(HttpResponseCode::OK, Writer.Save().AsArray()); +} + +void +TraceViewerService::HandleRegionsApi(HttpServerRequest& Request) +{ + CbObjectWriter Obj; + Obj.BeginArray("categories"); + for (const trace_detail::RegionCategory& Cat : m_Model.RegionCategories) + { + Obj.BeginObject(); + Obj << "name" << std::string_view(Cat.Name.empty() ? "Uncategorized" : Cat.Name); + Obj << "lane_count" << Cat.LaneCount; + Obj.BeginArray("regions"); + for (const trace_detail::RegionEntry& R : Cat.Regions) + { + Obj.BeginObject(); + Obj << "begin_us" << R.BeginUs; + Obj << "end_us" << R.EndUs; + Obj << "depth" << uint32_t(R.Depth); + Obj << "name" << R.Name; + Obj.EndObject(); + } + Obj.EndArray(); + Obj.EndObject(); + } + Obj.EndArray(); + + Request.WriteResponse(HttpResponseCode::OK, Obj.Save()); +} + +void +TraceViewerService::HandleCsvCategoriesApi(HttpServerRequest& Request) +{ + CbWriter Writer; + Writer.BeginArray(); + for (const auto& Cat : m_Model.CsvCategories) + { + Writer.BeginObject(); + Writer << "index" << Cat.Index; + Writer << "name" << Cat.Name; + Writer.EndObject(); + } + Writer.EndArray(); + + Request.WriteResponse(HttpResponseCode::OK, Writer.Save().AsArray()); +} + +void +TraceViewerService::HandleCsvStatsApi(HttpServerRequest& Request) +{ + CbWriter Writer; + Writer.BeginArray(); + for (const auto& Def : m_Model.CsvStatDefs) + { + Writer.BeginObject(); + Writer << "stat_id" << Def.StatId; + Writer << "category_index" << Def.CategoryIndex; + Writer << "name" << Def.Name; + Writer.EndObject(); + } + Writer.EndArray(); + + Request.WriteResponse(HttpResponseCode::OK, Writer.Save().AsArray()); +} + +void +TraceViewerService::HandleCsvSeriesApi(HttpServerRequest& Request) +{ + HttpServerRequest::QueryParams Params = Request.GetQueryParams(); + + // Accept either a single series index or iterate all for the requested stat+thread. + std::string_view StatStr = Params.GetValue("stat"); + std::string_view ThreadStr = Params.GetValue("thread"); + + uint64_t StatId = StatStr.empty() ? 0 : ParseUintParam(StatStr, 0); + uint32_t ThreadId = ThreadStr.empty() ? ~uint32_t(0) : ParseUintParam(ThreadStr, ~uint32_t(0)); + + CbWriter Writer; + Writer.BeginArray(); + for (const auto& S : m_Model.CsvTimeSeries) + { + if (StatId != 0 && S.StatId != StatId) + { + continue; + } + if (ThreadId != ~uint32_t(0) && S.ThreadId != ThreadId) + { + continue; + } + Writer.BeginObject(); + Writer << "stat_id" << S.StatId; + Writer << "thread_id" << S.ThreadId; + Writer.BeginArray("samples"); + for (const auto& Sample : S.Samples) + { + Writer.BeginArray(); + Writer.AddInteger(uint32_t(Sample.TimeUs)); + Writer.AddFloat(double(Sample.Value)); + Writer.EndArray(); + } + Writer.EndArray(); + Writer.EndObject(); + } + Writer.EndArray(); + + Request.WriteResponse(HttpResponseCode::OK, Writer.Save().AsArray()); +} + +void +TraceViewerService::HandleCsvEventsApi(HttpServerRequest& Request) +{ + CbWriter Writer; + Writer.BeginArray(); + for (const auto& E : m_Model.CsvEvents) + { + Writer.BeginObject(); + Writer << "time_us" << E.TimeUs; + Writer << "category_index" << E.CategoryIndex; + Writer << "text" << E.Text; + Writer.EndObject(); + } + Writer.EndArray(); + + Request.WriteResponse(HttpResponseCode::OK, Writer.Save().AsArray()); +} + +void +TraceViewerService::HandleCsvMetadataApi(HttpServerRequest& Request) +{ + CbWriter Writer; + Writer.BeginArray(); + for (const auto& M : m_Model.CsvMetadata) + { + Writer.BeginObject(); + Writer << "key" << M.Key; + Writer << "value" << M.Value; + Writer.EndObject(); + } + Writer.EndArray(); + + Request.WriteResponse(HttpResponseCode::OK, Writer.Save().AsArray()); +} + +////////////////////////////////////////////////////////////////////////////// +// Memory allocation endpoints + +void +TraceViewerService::HandleAllocSummaryApi(HttpServerRequest& Request) +{ + const trace_detail::AllocationSummary& S = m_Model.AllocSummary; + + CbObjectWriter Obj; + Obj << "has_memory_data" << S.HasMemoryData; + Obj << "total_allocs" << S.TotalAllocs; + Obj << "total_frees" << S.TotalFrees; + Obj << "total_realloc_allocs" << S.TotalReallocAllocs; + Obj << "total_realloc_frees" << S.TotalReallocFrees; + Obj << "peak_bytes" << S.PeakBytes; + Obj << "peak_time_us" << S.PeakTimeUs; + Obj << "end_bytes" << S.EndBytes; + Obj << "live_allocations" << S.LiveAllocations; + + Request.WriteResponse(HttpResponseCode::OK, Obj.Save()); +} + +void +TraceViewerService::HandleHeapsApi(HttpServerRequest& Request) +{ + CbWriter Writer; + Writer.BeginArray(); + for (const trace_detail::HeapInfo& H : m_Model.Heaps) + { + Writer.BeginObject(); + Writer << "id" << H.Id; + Writer << "parent_id" << H.ParentId; + Writer << "flags" << H.Flags; + Writer << "name" << H.Name; + Writer.EndObject(); + } + Writer.EndArray(); + + Request.WriteResponse(HttpResponseCode::OK, Writer.Save().AsArray()); +} + +void +TraceViewerService::HandleAllocTagsApi(HttpServerRequest& Request) +{ + CbWriter Writer; + Writer.BeginArray(); + for (const trace_detail::TagInfo& T : m_Model.Tags) + { + Writer.BeginObject(); + Writer << "tag" << T.Tag; + Writer << "parent" << T.Parent; + Writer << "display" << T.Display; + Writer.EndObject(); + } + Writer.EndArray(); + + Request.WriteResponse(HttpResponseCode::OK, Writer.Save().AsArray()); +} + +void +TraceViewerService::HandleMemoryTimelineApi(HttpServerRequest& Request) +{ + const auto& Timeline = m_Model.MemoryTimeline; + + // Parse optional query parameters for range filtering and downsampling. + HttpServerRequest::QueryParams Params = Request.GetQueryParams(); + uint32_t StartUs = ParseUintParam(Params.GetValue("start"), 0); + uint32_t EndUs = ParseUintParam(Params.GetValue("end"), ~uint32_t(0)); + uint32_t MaxSamples = ParseUintParam(Params.GetValue("max_samples"), 2000); + if (MaxSamples == 0) + { + MaxSamples = 2000; + } + + // Binary-search for the start offset. + size_t Begin = 0; + { + size_t Lo = 0; + size_t Hi = Timeline.size(); + while (Lo < Hi) + { + size_t Mid = Lo + (Hi - Lo) / 2; + if (Timeline[Mid].TimeUs < StartUs) + { + Lo = Mid + 1; + } + else + { + Hi = Mid; + } + } + Begin = Lo; + } + + // Find end offset. + size_t End = Timeline.size(); + { + size_t Lo = Begin; + size_t Hi = Timeline.size(); + while (Lo < Hi) + { + size_t Mid = Lo + (Hi - Lo) / 2; + if (Timeline[Mid].TimeUs <= EndUs) + { + Lo = Mid + 1; + } + else + { + Hi = Mid; + } + } + End = Lo; + } + + size_t Count = (End > Begin) ? (End - Begin) : 0; + size_t Stride = (Count > MaxSamples) ? (Count / MaxSamples) : 1; + + CbObjectWriter Obj; + + uint64_t SampleCount = 0; + Obj.BeginArray("samples"); + for (size_t I = Begin; I < End; I += Stride) + { + const trace_detail::MemoryTimelineSample& S = Timeline[I]; + Obj.BeginArray(); + Obj.AddInteger(S.TimeUs); + Obj.AddInteger(S.TotalAllocatedBytes); + Obj.AddInteger(S.SystemBytes); + Obj.AddInteger(S.VideoBytes); + Obj.EndArray(); + ++SampleCount; + } + Obj.EndArray(); + + Obj << "sample_count" << SampleCount; + + Request.WriteResponse(HttpResponseCode::OK, Obj.Save()); +} + +void +TraceViewerService::HandleHeapStatsApi(HttpServerRequest& Request) +{ + CbWriter Writer; + Writer.BeginArray(); + for (const trace_detail::HeapStat& S : m_Model.HeapStats) + { + Writer.BeginObject(); + Writer << "heap_id" << S.HeapId; + Writer << "current_bytes" << S.CurrentBytes; + Writer << "peak_bytes" << S.PeakBytes; + Writer << "alloc_count" << S.AllocCount; + Writer << "free_count" << S.FreeCount; + Writer.EndObject(); + } + Writer.EndArray(); + + Request.WriteResponse(HttpResponseCode::OK, Writer.Save().AsArray()); +} + +void +TraceViewerService::HandleCallstacksApi(HttpServerRequest& Request) +{ + HttpServerRequest::QueryParams Params = Request.GetQueryParams(); + uint32_t Id = ParseUintParam(Params.GetValue("id"), 0); + + if (Id == 0) + { + WriteNotFound(Request, "Missing or invalid 'id' parameter"); + return; + } + + const trace_detail::CallstackEntry* Entry = m_CallstackFormatter.FindCallstackEntry(Id); + if (Entry == nullptr) + { + WriteNotFound(Request, "Callstack not found"); + return; + } + + trace_detail::FilteredCallstackView Filtered = m_CallstackFormatter.BuildView(*Entry, m_CallstackFilterOptions); + + CbObjectWriter Obj; + CallstackSummaryInfo Summary = BuildCallstackSummary(Filtered); + Obj << "id" << Entry->Id; + Obj << "summary" << Summary.Summary; + Obj << "top_frame" << Summary.TopFrame; + Obj << "secondary_frame" << Summary.SecondaryFrame; + Obj << "group_key" << Summary.GroupKey; + Obj << "hidden_prefix_count" << Filtered.HiddenPrefixCount; + Obj << "included_third_party_boundary" << Filtered.IncludedThirdPartyBoundary; + Obj.BeginArray("frames"); + for (const trace_detail::FilteredCallstackFrame& FrameView : Filtered.Frames) + { + const trace_detail::ResolvedFrame& F = *FrameView.Frame; + Obj.BeginObject(); + Obj << "index" << uint64_t(FrameView.OriginalIndex); + Obj.AddString("address", fmt::format("0x{:X}", F.Address)); + Obj << "display" << FrameView.Display; + if (F.ModuleIndex != ~0u && F.ModuleIndex < m_Model.Modules.size()) + { + const trace_detail::ModuleInfo& Module = m_Model.Modules[F.ModuleIndex]; + Obj << "module" << std::string_view(Module.Name); + Obj << "module_path" << std::string_view(Module.FullPath); + Obj.AddString("offset", fmt::format("0x{:X}", F.Offset)); + } + Obj.EndObject(); + } + Obj.EndArray(); + + Request.WriteResponse(HttpResponseCode::OK, Obj.Save()); +} + +void +TraceViewerService::HandleCallstackStatsApi(HttpServerRequest& Request) +{ + HttpServerRequest::QueryParams Params = Request.GetQueryParams(); + uint32_t Limit = ParseUintParam(Params.GetValue("limit"), 100); + if (Limit == 0) + { + Limit = 100; + } + + size_t Count = std::min(size_t(Limit), m_Model.CallstackStats.size()); + + CbObjectWriter Obj; + Obj << "total_unique_callstacks" << uint64_t(m_Model.Callstacks.size()); + Obj.BeginArray("stats"); + for (size_t I = 0; I < Count; ++I) + { + const trace_detail::CallstackAllocStat& S = m_Model.CallstackStats[I]; + Obj.BeginObject(); + Obj << "callstack_id" << S.CallstackId; + Obj << "live_bytes" << S.LiveBytes; + Obj << "live_count" << S.LiveCount; + if (const trace_detail::CallstackEntry* Entry = m_CallstackFormatter.FindCallstackEntry(S.CallstackId)) + { + CallstackSummaryInfo Summary = BuildCallstackSummary(m_CallstackFormatter.BuildView(*Entry, m_CallstackFilterOptions)); + Obj << "summary" << Summary.Summary; + Obj << "top_frame" << Summary.TopFrame; + Obj << "secondary_frame" << Summary.SecondaryFrame; + Obj << "group_key" << Summary.GroupKey; + Obj << "hidden_prefix_count" << Summary.HiddenPrefixCount; + Obj << "included_third_party_boundary" << Summary.IncludedThirdPartyBoundary; + } + Obj.EndObject(); + } + Obj.EndArray(); + + Request.WriteResponse(HttpResponseCode::OK, Obj.Save()); +} + +void +TraceViewerService::HandleChurnStatsApi(HttpServerRequest& Request) +{ + HttpServerRequest::QueryParams Params = Request.GetQueryParams(); + uint32_t Limit = ParseUintParam(Params.GetValue("limit"), 100); + if (Limit == 0) + { + Limit = 100; + } + + size_t Count = std::min(size_t(Limit), m_Model.ChurnStats.size()); + + CbObjectWriter Obj; + Obj << "total_unique_callstacks" << uint64_t(m_Model.Callstacks.size()); + Obj.BeginArray("stats"); + for (size_t I = 0; I < Count; ++I) + { + const trace_detail::CallstackChurnStat& S = m_Model.ChurnStats[I]; + Obj.BeginObject(); + Obj << "callstack_id" << S.CallstackId; + Obj << "churn_allocs" << S.ChurnAllocs; + Obj << "churn_bytes" << S.ChurnBytes; + Obj << "total_allocs" << S.TotalAllocs; + Obj << "total_bytes" << S.TotalBytes; + Obj.AddFloat("mean_distance", S.MeanDistance); + if (const trace_detail::CallstackEntry* Entry = m_CallstackFormatter.FindCallstackEntry(S.CallstackId)) + { + CallstackSummaryInfo Summary = BuildCallstackSummary(m_CallstackFormatter.BuildView(*Entry, m_CallstackFilterOptions)); + Obj << "summary" << Summary.Summary; + Obj << "top_frame" << Summary.TopFrame; + Obj << "secondary_frame" << Summary.SecondaryFrame; + Obj << "group_key" << Summary.GroupKey; + Obj << "hidden_prefix_count" << Summary.HiddenPrefixCount; + Obj << "included_third_party_boundary" << Summary.IncludedThirdPartyBoundary; + } + Obj.EndObject(); + } + Obj.EndArray(); + + Request.WriteResponse(HttpResponseCode::OK, Obj.Save()); +} + +void +TraceViewerService::HandleAllocSizeHistogramApi(HttpServerRequest& Request) +{ + const auto& Buckets = m_Model.AllocSizeHistogram; + + uint64_t TotalCount = 0; + uint64_t TotalBytes = 0; + uint64_t MaxCount = 0; + uint64_t MaxBytes = 0; + for (const trace_detail::AllocSizeBucket& B : Buckets) + { + TotalCount += B.Count; + TotalBytes += B.Bytes; + if (B.Count > MaxCount) + { + MaxCount = B.Count; + } + if (B.Bytes > MaxBytes) + { + MaxBytes = B.Bytes; + } + } + + CbObjectWriter Obj; + Obj << "total_count" << TotalCount; + Obj << "total_bytes" << TotalBytes; + Obj << "max_count" << MaxCount; + Obj << "max_bytes" << MaxBytes; + Obj.BeginArray("buckets"); + for (const trace_detail::AllocSizeBucket& B : Buckets) + { + Obj.BeginObject(); + Obj << "min_size" << B.MinSize; + Obj << "max_size" << B.MaxSize; + Obj << "count" << B.Count; + Obj << "bytes" << B.Bytes; + Obj.EndObject(); + } + Obj.EndArray(); + + Request.WriteResponse(HttpResponseCode::OK, Obj.Save()); +} + +} // namespace zen |