// Copyright Epic Games, Inc. All Rights Reserved. #include "trace_viewer_service.h" #include "timeline_query.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #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 }; #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 inline void AppendUintFast(StringBuilderBase& Sb, T Value) { static_assert(std::is_unsigned_v && std::is_integral_v); // 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::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 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(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 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 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 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, "":{"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{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& 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