aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorStefan Boberg <[email protected]>2026-04-20 21:50:41 +0200
committerGitHub Enterprise <[email protected]>2026-04-20 21:50:41 +0200
commit2dfb5da16b97a6c12e01977af5b5188522178a4e (patch)
tree428aa0aa8e6079c64438931e0fd4f828c613c94d
parentAdd CompactString utility type (#990) (diff)
downloadarchived-zen-2dfb5da16b97a6c12e01977af5b5188522178a4e.tar.xz
archived-zen-2dfb5da16b97a6c12e01977af5b5188522178a4e.zip
zen trace analysis support (#945)
Integrates the **tourist** trace analysis library and builds a full `zen trace` command suite for working with Unreal Engine `.utrace` files. ### Trace analysis library (`thirdparty/tourist/`) - Adds the tourist library as a third-party dependency with three modules: **foundation** (platform primitives, memory, scheduling), **trace** (UE Trace protocol decoding), and **analysis** (event dispatching and analyzer framework). - Cross-platform support for Windows, Linux, and macOS. ### `zen trace` CLI commands (`src/zen/cmds/`, `src/zen/trace/`) - **`zen trace analyze`** — Summarize a `.utrace` file: session metadata, thread inventory, command line + build configuration, CPU profiling scopes, timing, event rates, log messages, and (with symbols) memory allocation metrics including live-allocs dumps, callstack-keyed aggregation, and allocation churn. Optional HTML output for memory reports. - **`zen trace inspect`** — Dump the event schema (declared types, fields, sizes) from a trace file. - **`zen trace trim`** — Extract a time-window from a trace into a new `.utrace` file. - **`zen trace serve`** — Launch a local HTTP server hosting an interactive trace viewer; opens in the default browser. ### Symbolication (`src/zen/trace/symbol_resolver.*`, `thirdparty/raw_pdb/`) - Pluggable resolver with multiple backends: `pdb` (in-tree raw_pdb), `dbghelp` (Windows), `llvm-symbolizer` (all platforms), `atos` (macOS). An `auto` backend picks the best available tool per platform. - Microsoft Symbol Server support: downloads PDBs on demand using a redirect-aware HTTP client. - Local PDB cache keyed by image GUID preserves symbols across binary recompilation. - Callstack trimming heuristic strips UE internal noise from reports. - Binary analysis cache (`.ucache_z`) avoids re-resolving the same trace. ### Interactive trace viewer (`src/zen/frontend/html/`, `src/zen/trace/trace_viewer_service.*`) - Timeline: scope-level detail, horizontal zoom/pan, vertical scrolling, viewport-driven loading with pre-computed LOD for responsive navigation of large traces. - Thread grouping (collapsible sidebar sections) synthesized from name suffixes, natural sort order, visual distinction between lane threads and OS threads. - Bookmark and region annotations; region categories with per-category toggles; bookmark marker toggle in the toolbar. - Filterable Logs tab showing captured `UE_LOG` output. - Stats tab with per-scope aggregate statistics. - Memory tab with interactive allocation analysis and an allocation size histogram. - CsvProfiler event parsing and chart UI. ### Other in-branch supporting changes - **Cross-platform browser launcher** (`browser_launcher.{h,cpp}`) used by `trace serve`. - **`ReciprocalU64`** fast 64-bit integer division (zencore/intmath) for trace analyzers. - **`parallelsort`** cross-platform parallel sort helper (zenutil). - Frontend zip build rule so the viewer's HTML assets are bundled into `zen.exe`. - `/Zo` flag for better optimized debug info on Windows release builds. - `trace-tests.cpp` in the `zen-test` harness (harness itself landed on main via #985).
-rw-r--r--CHANGELOG.md8
-rw-r--r--CLAUDE.md1
-rw-r--r--src/zen-test/trace-tests.cpp107
-rw-r--r--src/zen/browser_launcher.cpp71
-rw-r--r--src/zen/browser_launcher.h14
-rw-r--r--src/zen/cmds/trace_cmd.cpp89
-rw-r--r--src/zen/cmds/trace_cmd.h29
-rw-r--r--src/zen/cmds/ui_cmd.cpp39
-rw-r--r--src/zen/frontend/html/api.js137
-rw-r--r--src/zen/frontend/html/csvstats.js383
-rw-r--r--src/zen/frontend/html/index.html95
-rw-r--r--src/zen/frontend/html/logs.js237
-rw-r--r--src/zen/frontend/html/memory.js790
-rw-r--r--src/zen/frontend/html/stats.js95
-rw-r--r--src/zen/frontend/html/timeline.js973
-rw-r--r--src/zen/frontend/html/trace.css1312
-rw-r--r--src/zen/frontend/html/trace.js577
-rw-r--r--src/zen/trace/callstack_formatter.cpp251
-rw-r--r--src/zen/trace/callstack_formatter.h55
-rw-r--r--src/zen/trace/symbol_resolver.cpp1631
-rw-r--r--src/zen/trace/symbol_resolver.h45
-rw-r--r--src/zen/trace/timeline_query.cpp123
-rw-r--r--src/zen/trace/timeline_query.h69
-rw-r--r--src/zen/trace/trace_analyze.cpp812
-rw-r--r--src/zen/trace/trace_analyze.h29
-rw-r--r--src/zen/trace/trace_cache.cpp1104
-rw-r--r--src/zen/trace/trace_cache.h253
-rw-r--r--src/zen/trace/trace_cmd.cpp416
-rw-r--r--src/zen/trace/trace_cmd.h123
-rw-r--r--src/zen/trace/trace_memory.cpp901
-rw-r--r--src/zen/trace/trace_memory.h301
-rw-r--r--src/zen/trace/trace_model.cpp3847
-rw-r--r--src/zen/trace/trace_model.h314
-rw-r--r--src/zen/trace/trace_viewer_service.cpp1225
-rw-r--r--src/zen/trace/trace_viewer_service.h71
-rw-r--r--src/zen/xmake.lua46
-rw-r--r--src/zen/zen.cpp68
-rw-r--r--src/zen/zen.h12
-rw-r--r--src/zencore/include/zencore/fmtutils.h2
-rw-r--r--src/zencore/include/zencore/intmath.h32
-rw-r--r--src/zencore/intmath.cpp94
-rw-r--r--src/zencore/string.cpp10
-rw-r--r--src/zenserver/xmake.lua1
-rw-r--r--src/zenstore/cache/cachedisklayer.cpp20
-rw-r--r--src/zenutil/include/zenutil/parallelsort.h119
-rw-r--r--src/zenutil/parallelsort.cpp148
-rw-r--r--src/zenutil/xmake.lua3
-rw-r--r--src/zenutil/zenutil.cpp2
-rw-r--r--thirdparty/VERSIONS.md1
-rw-r--r--thirdparty/raw_pdb/.gitignore431
-rw-r--r--thirdparty/raw_pdb/CMakeLists.txt9
-rw-r--r--thirdparty/raw_pdb/LICENSE25
-rw-r--r--thirdparty/raw_pdb/README.md190
-rw-r--r--thirdparty/raw_pdb/raw_pdb.natvis12
-rw-r--r--thirdparty/raw_pdb/src/CMakeLists.txt112
-rw-r--r--thirdparty/raw_pdb/src/Examples/CMakeLists.txt39
-rw-r--r--thirdparty/raw_pdb/src/Examples/ExampleContributions.cpp96
-rw-r--r--thirdparty/raw_pdb/src/Examples/ExampleFunctionSymbols.cpp262
-rw-r--r--thirdparty/raw_pdb/src/Examples/ExampleFunctionVariables.cpp382
-rw-r--r--thirdparty/raw_pdb/src/Examples/ExampleIPI.cpp198
-rw-r--r--thirdparty/raw_pdb/src/Examples/ExampleLines.cpp268
-rw-r--r--thirdparty/raw_pdb/src/Examples/ExampleMain.cpp200
-rw-r--r--thirdparty/raw_pdb/src/Examples/ExampleMemoryMappedFile.cpp100
-rw-r--r--thirdparty/raw_pdb/src/Examples/ExampleMemoryMappedFile.h29
-rw-r--r--thirdparty/raw_pdb/src/Examples/ExamplePDBSize.cpp124
-rw-r--r--thirdparty/raw_pdb/src/Examples/ExampleSymbols.cpp238
-rw-r--r--thirdparty/raw_pdb/src/Examples/ExampleTimedScope.cpp54
-rw-r--r--thirdparty/raw_pdb/src/Examples/ExampleTimedScope.h22
-rw-r--r--thirdparty/raw_pdb/src/Examples/ExampleTypeTable.cpp41
-rw-r--r--thirdparty/raw_pdb/src/Examples/ExampleTypeTable.h49
-rw-r--r--thirdparty/raw_pdb/src/Examples/ExampleTypes.cpp1418
-rw-r--r--thirdparty/raw_pdb/src/Examples/Examples_PCH.cpp4
-rw-r--r--thirdparty/raw_pdb/src/Examples/Examples_PCH.h53
-rw-r--r--thirdparty/raw_pdb/src/Foundation/PDB_ArrayView.h68
-rw-r--r--thirdparty/raw_pdb/src/Foundation/PDB_Assert.h27
-rw-r--r--thirdparty/raw_pdb/src/Foundation/PDB_BitOperators.h23
-rw-r--r--thirdparty/raw_pdb/src/Foundation/PDB_BitUtil.h73
-rw-r--r--thirdparty/raw_pdb/src/Foundation/PDB_CRT.h14
-rw-r--r--thirdparty/raw_pdb/src/Foundation/PDB_Forward.h9
-rw-r--r--thirdparty/raw_pdb/src/Foundation/PDB_Log.h15
-rw-r--r--thirdparty/raw_pdb/src/Foundation/PDB_Macros.h126
-rw-r--r--thirdparty/raw_pdb/src/Foundation/PDB_Memory.h11
-rw-r--r--thirdparty/raw_pdb/src/Foundation/PDB_Move.h11
-rw-r--r--thirdparty/raw_pdb/src/Foundation/PDB_Platform.h45
-rw-r--r--thirdparty/raw_pdb/src/Foundation/PDB_PointerUtil.h33
-rw-r--r--thirdparty/raw_pdb/src/Foundation/PDB_TypeTraits.h65
-rw-r--r--thirdparty/raw_pdb/src/Foundation/PDB_Warnings.h45
-rw-r--r--thirdparty/raw_pdb/src/PDB.cpp55
-rw-r--r--thirdparty/raw_pdb/src/PDB.h21
-rw-r--r--thirdparty/raw_pdb/src/PDB_CoalescedMSFStream.cpp169
-rw-r--r--thirdparty/raw_pdb/src/PDB_CoalescedMSFStream.h71
-rw-r--r--thirdparty/raw_pdb/src/PDB_DBIStream.cpp335
-rw-r--r--thirdparty/raw_pdb/src/PDB_DBIStream.h65
-rw-r--r--thirdparty/raw_pdb/src/PDB_DBITypes.cpp9
-rw-r--r--thirdparty/raw_pdb/src/PDB_DBITypes.h928
-rw-r--r--thirdparty/raw_pdb/src/PDB_DirectMSFStream.cpp115
-rw-r--r--thirdparty/raw_pdb/src/PDB_DirectMSFStream.h84
-rw-r--r--thirdparty/raw_pdb/src/PDB_ErrorCodes.h26
-rw-r--r--thirdparty/raw_pdb/src/PDB_GlobalSymbolStream.cpp43
-rw-r--r--thirdparty/raw_pdb/src/PDB_GlobalSymbolStream.h49
-rw-r--r--thirdparty/raw_pdb/src/PDB_IPIStream.cpp140
-rw-r--r--thirdparty/raw_pdb/src/PDB_IPIStream.h66
-rw-r--r--thirdparty/raw_pdb/src/PDB_IPITypes.h144
-rw-r--r--thirdparty/raw_pdb/src/PDB_ImageSectionStream.cpp47
-rw-r--r--thirdparty/raw_pdb/src/PDB_ImageSectionStream.h42
-rw-r--r--thirdparty/raw_pdb/src/PDB_InfoStream.cpp102
-rw-r--r--thirdparty/raw_pdb/src/PDB_InfoStream.h62
-rw-r--r--thirdparty/raw_pdb/src/PDB_ModuleInfoStream.cpp184
-rw-r--r--thirdparty/raw_pdb/src/PDB_ModuleInfoStream.h104
-rw-r--r--thirdparty/raw_pdb/src/PDB_ModuleLineStream.cpp31
-rw-r--r--thirdparty/raw_pdb/src/PDB_ModuleLineStream.h151
-rw-r--r--thirdparty/raw_pdb/src/PDB_ModuleSymbolStream.cpp61
-rw-r--r--thirdparty/raw_pdb/src/PDB_ModuleSymbolStream.h70
-rw-r--r--thirdparty/raw_pdb/src/PDB_NamesStream.cpp28
-rw-r--r--thirdparty/raw_pdb/src/PDB_NamesStream.h48
-rw-r--r--thirdparty/raw_pdb/src/PDB_PCH.cpp4
-rw-r--r--thirdparty/raw_pdb/src/PDB_PCH.h20
-rw-r--r--thirdparty/raw_pdb/src/PDB_PublicSymbolStream.cpp43
-rw-r--r--thirdparty/raw_pdb/src/PDB_PublicSymbolStream.h49
-rw-r--r--thirdparty/raw_pdb/src/PDB_RawFile.cpp147
-rw-r--r--thirdparty/raw_pdb/src/PDB_RawFile.h66
-rw-r--r--thirdparty/raw_pdb/src/PDB_SectionContributionStream.cpp25
-rw-r--r--thirdparty/raw_pdb/src/PDB_SectionContributionStream.h38
-rw-r--r--thirdparty/raw_pdb/src/PDB_SourceFileStream.cpp68
-rw-r--r--thirdparty/raw_pdb/src/PDB_SourceFileStream.h65
-rw-r--r--thirdparty/raw_pdb/src/PDB_TPIStream.cpp86
-rw-r--r--thirdparty/raw_pdb/src/PDB_TPIStream.h85
-rw-r--r--thirdparty/raw_pdb/src/PDB_TPITypes.h867
-rw-r--r--thirdparty/raw_pdb/src/PDB_Types.cpp12
-rw-r--r--thirdparty/raw_pdb/src/PDB_Types.h167
-rw-r--r--thirdparty/raw_pdb/src/PDB_Util.h56
-rw-r--r--thirdparty/raw_pdb/xmake.lua17
-rw-r--r--thirdparty/tourist/analysis/include/analysis/analyzer.h54
-rw-r--r--thirdparty/tourist/analysis/include/analysis/array.h65
-rw-r--r--thirdparty/tourist/analysis/include/analysis/dispatcher.h22
-rw-r--r--thirdparty/tourist/analysis/include/analysis/outline.h153
-rw-r--r--thirdparty/tourist/analysis/include/analysis/string.h35
-rw-r--r--thirdparty/tourist/analysis/src/dispatcher.cpp148
-rw-r--r--thirdparty/tourist/foundation/include/foundation/buffer.h148
-rw-r--r--thirdparty/tourist/foundation/include/foundation/hash.h44
-rw-r--r--thirdparty/tourist/foundation/include/foundation/malloc.h31
-rw-r--r--thirdparty/tourist/foundation/include/foundation/platform.h40
-rw-r--r--thirdparty/tourist/foundation/include/foundation/scheduler.h113
-rw-r--r--thirdparty/tourist/foundation/include/foundation/types.h115
-rw-r--r--thirdparty/tourist/foundation/src/allocator.cpp69
-rw-r--r--thirdparty/tourist/foundation/src/buffer.cpp117
-rw-r--r--thirdparty/tourist/foundation/src/malloc.cpp211
-rw-r--r--thirdparty/tourist/foundation/src/ref.cpp69
-rw-r--r--thirdparty/tourist/foundation/src/scheduler.cpp339
-rw-r--r--thirdparty/tourist/foundation/src/slab.h50
-rw-r--r--thirdparty/tourist/foundation/src/stream.cpp72
-rw-r--r--thirdparty/tourist/trace/include/trace/detail/data.h45
-rw-r--r--thirdparty/tourist/trace/include/trace/detail/exceptions.h23
-rw-r--r--thirdparty/tourist/trace/include/trace/detail/preamble.h23
-rw-r--r--thirdparty/tourist/trace/include/trace/detail/protocol.h64
-rw-r--r--thirdparty/tourist/trace/include/trace/detail/transport.h70
-rw-r--r--thirdparty/tourist/trace/include/trace/detail/type.h39
-rw-r--r--thirdparty/tourist/trace/include/trace/trace.h8
-rw-r--r--thirdparty/tourist/trace/src/constants.h43
-rw-r--r--thirdparty/tourist/trace/src/data.cpp166
-rw-r--r--thirdparty/tourist/trace/src/preamble.cpp55
-rw-r--r--thirdparty/tourist/trace/src/protocol.cpp850
-rw-r--r--thirdparty/tourist/trace/src/transport.cpp183
-rw-r--r--thirdparty/tourist/trace/src/types.cpp96
-rw-r--r--thirdparty/xmake.lua52
-rw-r--r--xmake.lua5
166 files changed, 30497 insertions, 191 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8b1631303..64debc044 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,9 @@
##
+- Feature: `zen trace` subcommand suite for Unreal Insights `.utrace` files (and emitted by zenserver/zen via --trace options)
+ - `zen trace analyze` — offline analysis with CPU scope, memory allocation, allocation churn, and callstack reports; symbolication via PDB, DbgHelp, and Microsoft Symbol Server backends; optional HTML output
+ - `zen trace serve` — embedded HTTP server hosting a browser-based trace viewer (timeline with threads/scopes/regions/bookmarks/log events, memory analysis, stats) for interactive analysis
+ - `zen trace inspect` — print event schemas from a trace file
+ - `zen trace trim` — trim a trace file to a time range
- Feature: `zen down` new targeting modes
- `--pid <pid>` shuts down a specific zen server process by process id
- `--executable <name-or-path>` shuts down all zen server processes whose executable matches; a bare filename matches by name only, a path compares in full
@@ -9,8 +14,11 @@
- `zen history --print` prints the reconstructed command line of the selected row instead of launching it
- `--enable-execution-history` global option on both binaries (default `true`) to opt out per invocation
- The history file is attached to Sentry crash reports (alongside the existing zenserver log)
+- Improvement: `tourist` trace-analysis library and `raw_pdb` vendored under `thirdparty/`; tourist made cross-platform (Windows/Linux/macOS)
+- Improvement: Trace viewer HTML/JS frontend bundled via `zen` frontend zip build rule; `ZipFs` moved from `zenserver` into `zenhttp` for reuse
- Improvement: Replaced `OperationLogOutput` with `ProgressBase` in `zenutil`; logging and progress reporting are now separate concerns. Operation classes receive a `LoggerRef` for logging and a `ProgressBase&` for progress bars
- Improvement: New `ZEN_SCOPED_LOG(Expr)` macro routes `ZEN_INFO`/`ZEN_WARN`/`ZEN_DEBUG` in the enclosing block through the given logger expression instead of the default
+- Improvement: Log formatting updated with elapsed timestamps, short level labels, and consistent field order
- Improvement: `BuildContainer`, `SaveOplog`, and `LoadOplogContext` now take a caller-provided `LoggerRef` so diagnostic messages route through the caller's logger
- Improvement: `zen` CLI progress bar pins to the bottom of the terminal so log output can scroll above it; `--verbose` no longer forces plain progress output
- Improvement: `zen down --data-dir` probes lock-file liveness instead of presence; stale lock files are cleaned up and the command reports "nothing to do" instead of failing
diff --git a/CLAUDE.md b/CLAUDE.md
index 7d9b215e9..f446e1266 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -88,6 +88,7 @@ All `TEST_CASE` blocks must be wrapped in a `TEST_SUITE_BEGIN`/`TEST_SUITE_END`
| `zenremotestore` | `chunkblock.cpp` | `"remotestore.chunkblock"` |
| `zenserver-test` | `cache-tests.cpp` | `"server.cache"` |
| `zen-test` | `artifactprovider-tests.cpp` | `"zen.artifactprovider"` |
+| `zen-test` | `trace-tests.cpp` | `"zen.trace"` |
Each test executable defaults to running only the suites for its own module (e.g. `zencore-test` defaults to `core.*`). This default is derived automatically from the executable name by `RunTestMain` and can be overridden on the command line:
diff --git a/src/zen-test/trace-tests.cpp b/src/zen-test/trace-tests.cpp
new file mode 100644
index 000000000..e57d30e6c
--- /dev/null
+++ b/src/zen-test/trace-tests.cpp
@@ -0,0 +1,107 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#include <zencore/zencore.h>
+
+#if ZEN_WITH_TESTS
+
+# include "zen-test.h"
+
+# include <zencore/filesystem.h>
+# include <zencore/testing.h>
+
+# include <system_error>
+
+namespace zen::tests {
+
+using namespace std::literals;
+
+TEST_SUITE_BEGIN("zen.trace");
+
+TEST_CASE("help.top_level")
+{
+ ZenCommandResult Result = RunZen("trace --help");
+ CHECK_EQ(Result.ExitCode, 0);
+ CHECK_MESSAGE(Result.Output.find("subcommands:") != std::string::npos, Result.Output);
+ CHECK_MESSAGE(Result.Output.find("analyze") != std::string::npos, Result.Output);
+ CHECK_MESSAGE(Result.Output.find("inspect") != std::string::npos, Result.Output);
+ CHECK_MESSAGE(Result.Output.find("serve") != std::string::npos, Result.Output);
+}
+
+TEST_CASE("help.inspect")
+{
+ ZenCommandResult Result = RunZen("trace inspect --help");
+ CHECK_EQ(Result.ExitCode, 0);
+ CHECK_MESSAGE(Result.Output.find("Inspect event schemas") != std::string::npos, Result.Output);
+}
+
+TEST_CASE("inspect.missing_file")
+{
+ ZenCommandResult Result = RunZen("trace inspect this-file-does-not-exist.utrace");
+ CHECK_NE(Result.ExitCode, 0);
+ CHECK_MESSAGE(Result.Output.find("File not found") != std::string::npos, Result.Output);
+}
+
+TEST_CASE("inspect.generated_tracefile")
+{
+ const std::filesystem::path DataDir = TestEnv.CreateNewTestDir();
+ const std::filesystem::path TraceFile = DataDir / "powercycle-trace.utrace";
+
+ ZenServerInstance Instance(TestEnv);
+ Instance.SetDataDir(DataDir);
+
+ ExtendableStringBuilder<512> ServerArgs;
+ ServerArgs << "--tracefile=\"";
+ PathToUtf8(TraceFile.c_str(), ServerArgs);
+ ServerArgs << "\" --powercycle";
+
+ Instance.SpawnServer(TestEnv.GetNewPortNumber(), ServerArgs.ToView());
+ CHECK_MESSAGE(Instance.WaitUntilReady(10000), Instance.GetLogOutput());
+
+ if (Instance.IsRunning())
+ {
+ std::error_code Ec;
+ CHECK_MESSAGE(Instance.WaitUntilExited(10000, Ec), Instance.GetLogOutput());
+ CHECK_MESSAGE(!Ec, Ec.message());
+ }
+
+ CHECK_EQ(Instance.Shutdown(), 0);
+ CHECK_MESSAGE(std::filesystem::exists(TraceFile), TraceFile.string());
+ CHECK_MESSAGE(std::filesystem::file_size(TraceFile) > 0, TraceFile.string());
+
+ ExtendableStringBuilder<512> InspectArgs;
+ InspectArgs << "trace inspect \"";
+ PathToUtf8(TraceFile.c_str(), InspectArgs);
+ InspectArgs << "\"";
+
+ ZenCommandResult InspectResult = RunZen(InspectArgs.ToView());
+ CHECK_EQ(InspectResult.ExitCode, 0);
+ CHECK_MESSAGE(InspectResult.Output.find("Trace:") != std::string::npos, InspectResult.Output);
+ CHECK_MESSAGE(InspectResult.Output.find("Events:") != std::string::npos, InspectResult.Output);
+ CHECK_MESSAGE(InspectResult.Output.find("Types:") != std::string::npos, InspectResult.Output);
+ CHECK_MESSAGE(InspectResult.Output.find("Event Schemas:") != std::string::npos, InspectResult.Output);
+ CHECK_MESSAGE(InspectResult.Output.find("Events: 0") == std::string::npos, InspectResult.Output);
+ CHECK_MESSAGE(InspectResult.Output.find("Types: 0") == std::string::npos, InspectResult.Output);
+ CHECK_MESSAGE(InspectResult.Output.find("(uid=") != std::string::npos, InspectResult.Output);
+ CHECK_MESSAGE(InspectResult.Output.find("events=") != std::string::npos, InspectResult.Output);
+
+ ExtendableStringBuilder<512> AnalyzeArgs;
+ AnalyzeArgs << "trace analyze --no-cache --symbols=off \"";
+ PathToUtf8(TraceFile.c_str(), AnalyzeArgs);
+ AnalyzeArgs << "\"";
+
+ ZenCommandResult AnalyzeResult = RunZen(AnalyzeArgs.ToView());
+ CHECK_EQ(AnalyzeResult.ExitCode, 0);
+ CHECK_MESSAGE(AnalyzeResult.Output.find("Trace:") != std::string::npos, AnalyzeResult.Output);
+ CHECK_MESSAGE(AnalyzeResult.Output.find("Size:") != std::string::npos, AnalyzeResult.Output);
+ CHECK_MESSAGE(AnalyzeResult.Output.find("Events:") != std::string::npos, AnalyzeResult.Output);
+ CHECK_MESSAGE(AnalyzeResult.Output.find("Duration:") != std::string::npos, AnalyzeResult.Output);
+ CHECK_MESSAGE(AnalyzeResult.Output.find("Threads:") != std::string::npos, AnalyzeResult.Output);
+ CHECK_MESSAGE(AnalyzeResult.Output.find("Modules:") != std::string::npos, AnalyzeResult.Output);
+ CHECK_MESSAGE(AnalyzeResult.Output.find("Parsed:") != std::string::npos, AnalyzeResult.Output);
+}
+
+TEST_SUITE_END();
+
+} // namespace zen::tests
+
+#endif
diff --git a/src/zen/browser_launcher.cpp b/src/zen/browser_launcher.cpp
new file mode 100644
index 000000000..a115bf46a
--- /dev/null
+++ b/src/zen/browser_launcher.cpp
@@ -0,0 +1,71 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#include "browser_launcher.h"
+
+#include <zenbase/zenbase.h>
+#include <zencore/except_fmt.h>
+#include <zencore/logging.h>
+
+#include <stdexcept>
+#include <string>
+
+#if ZEN_PLATFORM_WINDOWS
+# include <zencore/windows.h>
+# include <shellapi.h>
+#else
+# include <spawn.h>
+# include <sys/wait.h>
+extern char** environ;
+#endif
+
+namespace zen {
+
+void
+LaunchBrowser(std::string_view Url)
+{
+ if (Url.empty())
+ {
+ throw zen::runtime_error("Cannot launch browser with empty URL");
+ }
+
+ bool Success = false;
+
+#if ZEN_PLATFORM_WINDOWS
+ std::string UrlZ(Url);
+ HINSTANCE Result = ShellExecuteA(nullptr, "open", UrlZ.c_str(), nullptr, nullptr, SW_SHOWNORMAL);
+ Success = reinterpret_cast<intptr_t>(Result) > 32;
+#else
+# if ZEN_PLATFORM_MAC
+ const char* Program = "open";
+# elif ZEN_PLATFORM_LINUX
+ const char* Program = "xdg-open";
+# else
+ ZEN_NOT_IMPLEMENTED("Browser launching not implemented on this platform");
+ const char* Program = nullptr;
+# endif
+
+ // Spawn directly via posix_spawnp to avoid the shell entirely, so URL contents
+ // cannot be interpreted as shell syntax regardless of what characters they contain.
+ std::string Url_c(Url);
+ char* const Argv[] = {const_cast<char*>(Program), Url_c.data(), nullptr};
+ pid_t Pid = 0;
+ const int SpawnRc = posix_spawnp(&Pid, Program, nullptr, nullptr, Argv, environ);
+ if (SpawnRc == 0)
+ {
+ int Status = 0;
+ if (waitpid(Pid, &Status, 0) == Pid)
+ {
+ Success = WIFEXITED(Status) && WEXITSTATUS(Status) == 0;
+ }
+ }
+#endif
+
+ if (!Success)
+ {
+ throw zen::runtime_error("Failed to launch browser for '{}'", Url);
+ }
+
+ ZEN_CONSOLE("Web browser launched for '{}' successfully", Url);
+}
+
+} // namespace zen
diff --git a/src/zen/browser_launcher.h b/src/zen/browser_launcher.h
new file mode 100644
index 000000000..1b8efcd69
--- /dev/null
+++ b/src/zen/browser_launcher.h
@@ -0,0 +1,14 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#pragma once
+
+#include <string_view>
+
+namespace zen {
+
+// Opens the given URL in the user's default web browser.
+// Throws zen::runtime_error on failure. On POSIX platforms the URL is
+// screened for shell metacharacters to prevent command injection.
+void LaunchBrowser(std::string_view Url);
+
+} // namespace zen
diff --git a/src/zen/cmds/trace_cmd.cpp b/src/zen/cmds/trace_cmd.cpp
deleted file mode 100644
index 54c0f080d..000000000
--- a/src/zen/cmds/trace_cmd.cpp
+++ /dev/null
@@ -1,89 +0,0 @@
-// Copyright Epic Games, Inc. All Rights Reserved.
-
-#include "trace_cmd.h"
-#include <zencore/logging.h>
-#include <zenhttp/httpclient.h>
-#include <zenhttp/httpcommon.h>
-
-using namespace std::literals;
-
-namespace zen {
-
-TraceCommand::TraceCommand()
-{
- m_Options.add_options()("h,help", "Print help");
- m_Options.add_option("", "u", "hosturl", kHostUrlHelp, cxxopts::value(m_HostName)->default_value(""), "<hosturl>");
- m_Options.add_option("", "s", "stop", "Stop tracing", cxxopts::value(m_Stop)->default_value("false"), "<stop>");
- m_Options.add_option("", "", "host", "Start tracing to host", cxxopts::value(m_TraceHost), "<hostip>");
- m_Options.add_option("", "", "file", "Start tracing to file", cxxopts::value(m_TraceFile), "<filepath>");
-}
-
-TraceCommand::~TraceCommand() = default;
-
-void
-TraceCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv)
-{
- ZEN_UNUSED(GlobalOptions);
-
- if (!ParseOptions(argc, argv))
- {
- return;
- }
-
- m_HostName = ResolveTargetHostSpec(m_HostName);
-
- if (m_HostName.empty())
- {
- throw OptionParseException("Unable to resolve server specification", m_Options.help());
- }
-
- zen::HttpClient Http = CreateHttpClient(m_HostName);
-
- if (m_Stop)
- {
- if (zen::HttpClient::Response Response = Http.Post("/admin/trace/stop"sv))
- {
- ZEN_CONSOLE("OK: {}", Response.ToText());
- }
- else
- {
- Response.ThrowError("Trace stop failed");
- }
- return;
- }
-
- std::string StartArg;
- if (!m_TraceHost.empty())
- {
- StartArg = fmt::format("host={}", m_TraceHost);
- }
- else if (!m_TraceFile.empty())
- {
- StartArg = fmt::format("file={}", m_TraceFile);
- }
-
- if (!StartArg.empty())
- {
- if (zen::HttpClient::Response Response = Http.Post(fmt::format("/admin/trace/start?{}"sv, StartArg)))
- {
- ZEN_CONSOLE("OK: {}", Response.ToText());
- }
- else
- {
- Response.ThrowError("Trace start failed");
- }
- }
- else
- {
- if (zen::HttpClient::Response Response = Http.Get("/admin/trace"sv))
- {
- ZEN_CONSOLE("OK: {}", Response.ToText());
- }
- else
- {
- Response.ThrowError("Trace status failed");
- }
- }
-}
-
-} // namespace zen
diff --git a/src/zen/cmds/trace_cmd.h b/src/zen/cmds/trace_cmd.h
deleted file mode 100644
index 6eb0ba22b..000000000
--- a/src/zen/cmds/trace_cmd.h
+++ /dev/null
@@ -1,29 +0,0 @@
-// Copyright Epic Games, Inc. All Rights Reserved.
-
-#pragma once
-
-#include "../zen.h"
-
-namespace zen {
-
-class TraceCommand : public ZenCmdBase
-{
-public:
- static constexpr char Name[] = "trace";
- static constexpr char Description[] = "Control zen realtime tracing";
-
- TraceCommand();
- ~TraceCommand();
-
- virtual void Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override;
- virtual cxxopts::Options& Options() override { return m_Options; }
-
-private:
- cxxopts::Options m_Options{Name, Description};
- std::string m_HostName;
- bool m_Stop = false;
- std::string m_TraceHost;
- std::string m_TraceFile;
-};
-
-} // namespace zen
diff --git a/src/zen/cmds/ui_cmd.cpp b/src/zen/cmds/ui_cmd.cpp
index 28ab6c45c..3d3021857 100644
--- a/src/zen/cmds/ui_cmd.cpp
+++ b/src/zen/cmds/ui_cmd.cpp
@@ -2,6 +2,8 @@
#include "ui_cmd.h"
+#include "browser_launcher.h"
+
#include <zencore/except_fmt.h>
#include <zencore/fmtutils.h>
#include <zencore/logging.h>
@@ -9,11 +11,6 @@
#include <zenutil/consoletui.h>
#include <zenutil/zenserverprocess.h>
-#if ZEN_PLATFORM_WINDOWS
-# include <zencore/windows.h>
-# include <shellapi.h>
-#endif
-
namespace zen {
namespace {
@@ -83,40 +80,10 @@ UiCommand::OpenBrowser(std::string_view HostName)
}
}
- bool Success = false;
-
ExtendableStringBuilder<256> FullUrl;
FullUrl << HostName << m_DashboardPath;
-#if ZEN_PLATFORM_WINDOWS
- HINSTANCE Result = ShellExecuteA(nullptr, "open", FullUrl.c_str(), nullptr, nullptr, SW_SHOWNORMAL);
- Success = reinterpret_cast<intptr_t>(Result) > 32;
-#else
- // Validate URL doesn't contain shell metacharacters that could lead to command injection
- std::string_view FullUrlView = FullUrl;
- constexpr std::string_view DangerousChars = ";|&$`\\\"'<>(){}[]!#*?~\n\r";
- if (FullUrlView.find_first_of(DangerousChars) != std::string_view::npos)
- {
- throw OptionParseException(fmt::format("URL contains invalid characters: '{}'", FullUrl), m_Options.help());
- }
-
-# if ZEN_PLATFORM_MAC
- std::string Command = fmt::format("open \"{}\"", FullUrl);
-# elif ZEN_PLATFORM_LINUX
- std::string Command = fmt::format("xdg-open \"{}\"", FullUrl);
-# else
- ZEN_NOT_IMPLEMENTED("Browser launching not implemented on this platform");
-# endif
-
- Success = system(Command.c_str()) == 0;
-#endif
-
- if (!Success)
- {
- throw zen::runtime_error("Failed to launch browser for '{}'", FullUrl);
- }
-
- ZEN_CONSOLE("Web browser launched for '{}' successfully", FullUrl);
+ LaunchBrowser(std::string_view(FullUrl));
}
void
diff --git a/src/zen/frontend/html/api.js b/src/zen/frontend/html/api.js
new file mode 100644
index 000000000..fbe5304ca
--- /dev/null
+++ b/src/zen/frontend/html/api.js
@@ -0,0 +1,137 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+// Thin wrappers around the /api/* endpoints exposed by TraceViewerService.
+
+const API = "api/";
+
+const JSON_HEADERS = { Accept: "application/json" };
+
+async function getJson(path) {
+ const response = await fetch(API + path, { headers: JSON_HEADERS });
+ if (!response.ok) {
+ throw new Error(`${path}: HTTP ${response.status}`);
+ }
+ return response.json();
+}
+
+export function getSession() {
+ return getJson("session");
+}
+
+export function getThreads() {
+ return getJson("threads");
+}
+
+export function getChannels() {
+ return getJson("channels");
+}
+
+export function getScopeStats() {
+ return getJson("scope-stats");
+}
+
+export function getScopeNames() {
+ return getJson("scope-names");
+}
+
+export async function getTimeline(threadId, startUs, endUs, minDurUs = 0, resolution = 0, { signal } = {}) {
+ const params = new URLSearchParams({
+ thread: String(threadId),
+ start: String(startUs),
+ end: String(endUs),
+ });
+ if (minDurUs > 0) params.set("mindur", String(minDurUs));
+ if (resolution > 0) params.set("resolution", String(resolution));
+ const response = await fetch(API + "timeline?" + params.toString(), { signal, headers: JSON_HEADERS });
+ if (!response.ok) {
+ throw new Error(`timeline: HTTP ${response.status}`);
+ }
+ return response.json();
+}
+
+export async function getTimelineBatch(threadIds, startUs, endUs, minDurUs = 0, resolution = 0, { signal } = {}) {
+ let url = `${API}timeline-batch?threads=${threadIds.join(",")}&start=${startUs}&end=${endUs}`;
+ if (minDurUs > 0) url += `&mindur=${minDurUs}`;
+ if (resolution > 0) url += `&resolution=${resolution}`;
+ const response = await fetch(url, { signal, headers: JSON_HEADERS });
+ if (!response.ok) {
+ throw new Error(`timeline-batch: HTTP ${response.status}`);
+ }
+ return response.json();
+}
+
+export function getLogCategories() {
+ return getJson("log-categories");
+}
+
+export function getLogs({ startUs = 0, endUs = 0xffffffff, minVerbosity = 0, category = null, limit = 5000 } = {}) {
+ const params = new URLSearchParams({
+ start: String(startUs),
+ end: String(endUs),
+ min_verbosity: String(minVerbosity),
+ limit: String(limit),
+ });
+ if (category !== null && category !== undefined) {
+ params.set("category", String(category));
+ }
+ return getJson("logs?" + params.toString());
+}
+
+export function getBookmarks() {
+ return getJson("bookmarks");
+}
+
+export function getRegions() {
+ return getJson("regions");
+}
+
+export function getCsvCategories() {
+ return getJson("csv-categories");
+}
+
+export function getCsvStats() {
+ return getJson("csv-stats");
+}
+
+export function getCsvSeries(statId, threadId) {
+ let url = "csv-series?";
+ if (statId != null) url += `stat=${statId}&`;
+ if (threadId != null) url += `thread=${threadId}&`;
+ return getJson(url);
+}
+
+export function getCsvEvents() {
+ return getJson("csv-events");
+}
+
+export function getCsvMetadata() {
+ return getJson("csv-metadata");
+}
+
+export function getAllocSummary() {
+ return getJson("alloc-summary");
+}
+
+export function getMemoryTimeline({ startUs = 0, endUs = 0xffffffff, maxSamples = 2000 } = {}) {
+ const params = new URLSearchParams({
+ start: String(startUs),
+ end: String(endUs),
+ max_samples: String(maxSamples),
+ });
+ return getJson("memory-timeline?" + params.toString());
+}
+
+export function getCallstackStats(limit = 100) {
+ return getJson("callstack-stats?limit=" + encodeURIComponent(limit));
+}
+
+export function getChurnStats(limit = 100) {
+ return getJson("churn-stats?limit=" + encodeURIComponent(limit));
+}
+
+export function getCallstack(callstackId) {
+ return getJson("callstacks?id=" + encodeURIComponent(callstackId));
+}
+
+export function getAllocSizeHistogram() {
+ return getJson("alloc-size-histogram");
+}
diff --git a/src/zen/frontend/html/csvstats.js b/src/zen/frontend/html/csvstats.js
new file mode 100644
index 000000000..a50b2f068
--- /dev/null
+++ b/src/zen/frontend/html/csvstats.js
@@ -0,0 +1,383 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+// CSV Profiler stats viewer — category/stat tree with line-chart visualization.
+
+import { getCsvSeries } from "./api.js";
+
+function escapeHtml(s) {
+ return String(s).replace(/[&<>"']/g, (c) => ({"&":"&amp;","<":"&lt;",">":"&gt;","\"":"&quot;","'":"&#39;"}[c]));
+}
+
+function formatTime(us) {
+ if (us < 1000) return `${us} \u00b5s`;
+ if (us < 1_000_000) return `${(us / 1000).toFixed(2)} ms`;
+ return `${(us / 1_000_000).toFixed(2)} s`;
+}
+
+// Palette for chart lines — distinct hues.
+const LINE_COLORS = [
+ "#4fc3f7", "#81c784", "#ffb74d", "#e57373", "#ba68c8",
+ "#4db6ac", "#fff176", "#f06292", "#7986cb", "#a1887f",
+];
+
+export class CsvStatsView {
+ constructor(model, containerEl) {
+ this.model = model;
+ this.container = containerEl;
+ this.loaded = false;
+
+ this.categories = model.csvCategories || [];
+ this.statDefs = model.csvStats || [];
+
+ // Group stats by category index.
+ this.catMap = new Map();
+ for (const cat of this.categories) {
+ this.catMap.set(cat.index, cat.name);
+ }
+ this.statsByCategory = new Map();
+ for (const s of this.statDefs) {
+ const catIdx = s.category_index;
+ if (!this.statsByCategory.has(catIdx)) this.statsByCategory.set(catIdx, []);
+ this.statsByCategory.get(catIdx).push(s);
+ }
+
+ // Selected series: Set of stat_id values to chart.
+ this.selectedStats = new Set();
+ this.seriesData = new Map(); // stat_id → [{time_us, value}, ...]
+ this.colorIndex = 0;
+ this.statColors = new Map(); // stat_id → color
+
+ this.buildLayout();
+ }
+
+ buildLayout() {
+ this.container.innerHTML =
+ `<div class="csv-layout">` +
+ `<div class="csv-tree-panel">` +
+ `<div class="sidebar-label">Stats</div>` +
+ `<div class="csv-tree"></div>` +
+ `</div>` +
+ `<div class="csv-chart-panel">` +
+ `<canvas class="csv-chart-canvas"></canvas>` +
+ `<div class="csv-chart-tooltip" hidden></div>` +
+ `</div>` +
+ `</div>`;
+
+ this.treeEl = this.container.querySelector(".csv-tree");
+ this.canvas = this.container.querySelector(".csv-chart-canvas");
+ this.tooltipEl = this.container.querySelector(".csv-chart-tooltip");
+ this.ctx = this.canvas.getContext("2d");
+ this.dpr = Math.max(1, window.devicePixelRatio || 1);
+
+ this.renderTree();
+
+ // Chart interaction
+ this.resizeObserver = new ResizeObserver(() => this.drawChart());
+ this.resizeObserver.observe(this.canvas);
+ this.canvas.addEventListener("mousemove", (e) => this.onChartHover(e));
+ this.canvas.addEventListener("mouseleave", () => { this.tooltipEl.hidden = true; });
+
+ // Pan + zoom state
+ this.viewStartUs = 0;
+ this.viewEndUs = this.model.session.trace_end_us || 1;
+ this.panning = false;
+ this.canvas.addEventListener("mousedown", (e) => this.onPanStart(e));
+ window.addEventListener("mousemove", (e) => this.onPanMove(e));
+ window.addEventListener("mouseup", () => this.onPanEnd());
+ this.canvas.addEventListener("wheel", (e) => this.onWheel(e), { passive: false });
+ }
+
+ renderTree() {
+ const parts = [];
+ // Sort categories by index.
+ const catIndices = Array.from(this.statsByCategory.keys()).sort((a, b) => a - b);
+ for (const catIdx of catIndices) {
+ const catName = this.catMap.get(catIdx) || `Category ${catIdx}`;
+ const stats = this.statsByCategory.get(catIdx);
+ parts.push(`<div class="csv-cat-header">${escapeHtml(catName)}</div>`);
+ for (const s of stats) {
+ const id = s.stat_id;
+ parts.push(
+ `<label class="csv-stat-row">` +
+ `<input type="checkbox" data-stat-id="${id}">` +
+ `<span class="csv-stat-name"></span>` +
+ `</label>`
+ );
+ }
+ }
+ if (parts.length === 0) {
+ parts.push(`<div class="csv-empty">No CSV profiler data in this trace.</div>`);
+ }
+ this.treeEl.innerHTML = parts.join("");
+
+ // Set names via DOM (XSS safe).
+ let statIdx = 0;
+ for (const catIdx of catIndices) {
+ const stats = this.statsByCategory.get(catIdx);
+ for (const s of stats) {
+ const rows = this.treeEl.querySelectorAll(".csv-stat-row");
+ if (statIdx < rows.length) {
+ rows[statIdx].querySelector(".csv-stat-name").textContent = s.name;
+ }
+ statIdx++;
+ }
+ }
+
+ // Wire checkboxes.
+ for (const cb of this.treeEl.querySelectorAll("input[type=checkbox]")) {
+ cb.addEventListener("change", () => {
+ const statId = Number(cb.dataset.statId);
+ if (cb.checked) {
+ this.selectedStats.add(statId);
+ if (!this.statColors.has(statId)) {
+ this.statColors.set(statId, LINE_COLORS[this.colorIndex++ % LINE_COLORS.length]);
+ }
+ this.fetchSeries(statId);
+ } else {
+ this.selectedStats.delete(statId);
+ this.drawChart();
+ }
+ });
+ }
+ }
+
+ async ensureLoaded() {
+ // Tree is built in constructor; nothing extra to lazy-load.
+ this.loaded = true;
+ this.drawChart();
+ }
+
+ async fetchSeries(statId) {
+ if (this.seriesData.has(statId)) {
+ this.drawChart();
+ return;
+ }
+ try {
+ const result = await getCsvSeries(statId);
+ // Merge all threads' samples for this stat into one combined array for now.
+ const allSamples = [];
+ for (const series of result) {
+ for (const [timeUs, value] of series.samples) {
+ allSamples.push({ timeUs, value });
+ }
+ }
+ allSamples.sort((a, b) => a.timeUs - b.timeUs);
+ this.seriesData.set(statId, allSamples);
+ this.drawChart();
+ } catch (e) {
+ console.error(`Failed to fetch CSV series for stat ${statId}: ${e.message}`);
+ }
+ }
+
+ resizeCanvas() {
+ const rect = this.canvas.getBoundingClientRect();
+ this.width = Math.floor(rect.width);
+ this.height = Math.floor(rect.height);
+ const bw = Math.floor(rect.width * this.dpr);
+ const bh = Math.floor(rect.height * this.dpr);
+ if (this.canvas.width !== bw || this.canvas.height !== bh) {
+ this.canvas.width = bw;
+ this.canvas.height = bh;
+ }
+ this.ctx.setTransform(this.dpr, 0, 0, this.dpr, 0, 0);
+ }
+
+ drawChart() {
+ this.resizeCanvas();
+ const ctx = this.ctx;
+ const W = this.width;
+ const H = this.height;
+
+ const bg = getComputedStyle(document.body).getPropertyValue("--bg0") || "#0d1117";
+ const fg2 = getComputedStyle(document.body).getPropertyValue("--fg2") || "#8b949e";
+ const border = getComputedStyle(document.body).getPropertyValue("--border") || "#30363d";
+ ctx.fillStyle = bg;
+ ctx.fillRect(0, 0, W, H);
+
+ if (this.selectedStats.size === 0) {
+ ctx.fillStyle = fg2;
+ ctx.font = "12px -apple-system, Segoe UI, sans-serif";
+ ctx.textAlign = "center";
+ ctx.textBaseline = "middle";
+ ctx.fillText("Select stats from the tree to chart them", W / 2, H / 2);
+ return;
+ }
+
+ const PAD_L = 60, PAD_R = 12, PAD_T = 12, PAD_B = 28;
+ const chartW = W - PAD_L - PAD_R;
+ const chartH = H - PAD_T - PAD_B;
+ if (chartW <= 0 || chartH <= 0) return;
+
+ const startUs = this.viewStartUs;
+ const endUs = this.viewEndUs;
+ const rangeUs = Math.max(1, endUs - startUs);
+
+ // Compute value range across all visible selected series.
+ let minVal = Infinity, maxVal = -Infinity;
+ for (const statId of this.selectedStats) {
+ const samples = this.seriesData.get(statId);
+ if (!samples) continue;
+ for (const s of samples) {
+ if (s.timeUs < startUs || s.timeUs > endUs) continue;
+ if (s.value < minVal) minVal = s.value;
+ if (s.value > maxVal) maxVal = s.value;
+ }
+ }
+ if (!isFinite(minVal)) { minVal = 0; maxVal = 1; }
+ if (minVal === maxVal) { minVal -= 0.5; maxVal += 0.5; }
+ const valRange = maxVal - minVal;
+ const valPad = valRange * 0.05;
+ minVal -= valPad;
+ maxVal += valPad;
+
+ const xAt = (us) => PAD_L + (us - startUs) / rangeUs * chartW;
+ const yAt = (v) => PAD_T + (1 - (v - minVal) / (maxVal - minVal)) * chartH;
+
+ // Grid lines.
+ ctx.strokeStyle = border;
+ ctx.lineWidth = 0.5;
+ for (let i = 0; i <= 4; i++) {
+ const y = PAD_T + chartH * i / 4;
+ ctx.beginPath(); ctx.moveTo(PAD_L, y); ctx.lineTo(PAD_L + chartW, y); ctx.stroke();
+ }
+
+ // Y axis labels.
+ ctx.fillStyle = fg2;
+ ctx.font = "10px -apple-system, Segoe UI, sans-serif";
+ ctx.textAlign = "right";
+ ctx.textBaseline = "middle";
+ for (let i = 0; i <= 4; i++) {
+ const v = minVal + (maxVal - minVal) * (1 - i / 4);
+ const y = PAD_T + chartH * i / 4;
+ ctx.fillText(v.toFixed(2), PAD_L - 4, y);
+ }
+
+ // X axis labels.
+ ctx.textAlign = "center";
+ ctx.textBaseline = "top";
+ const tickCount = Math.max(2, Math.min(8, Math.floor(chartW / 80)));
+ for (let i = 0; i <= tickCount; i++) {
+ const us = startUs + rangeUs * i / tickCount;
+ const x = xAt(us);
+ ctx.fillText(formatTime(us), x, PAD_T + chartH + 4);
+ }
+
+ // Draw lines.
+ for (const statId of this.selectedStats) {
+ const samples = this.seriesData.get(statId);
+ if (!samples || samples.length === 0) continue;
+ const color = this.statColors.get(statId) || "#fff";
+
+ ctx.strokeStyle = color;
+ ctx.lineWidth = 1.5;
+ ctx.beginPath();
+ let started = false;
+ for (const s of samples) {
+ if (s.timeUs < startUs || s.timeUs > endUs) continue;
+ const x = xAt(s.timeUs);
+ const y = yAt(s.value);
+ if (!started) { ctx.moveTo(x, y); started = true; }
+ else { ctx.lineTo(x, y); }
+ }
+ ctx.stroke();
+ }
+
+ // Chart border.
+ ctx.strokeStyle = border;
+ ctx.lineWidth = 1;
+ ctx.strokeRect(PAD_L, PAD_T, chartW, chartH);
+
+ // Legend.
+ ctx.font = "10px -apple-system, Segoe UI, sans-serif";
+ ctx.textAlign = "left";
+ ctx.textBaseline = "top";
+ let legendX = PAD_L + 6;
+ for (const statId of this.selectedStats) {
+ const def = this.statDefs.find(d => d.stat_id === statId);
+ const name = def ? def.name : `stat ${statId}`;
+ const color = this.statColors.get(statId) || "#fff";
+ ctx.fillStyle = color;
+ ctx.fillRect(legendX, PAD_T + 4, 10, 10);
+ ctx.fillStyle = "#ccc";
+ ctx.fillText(name, legendX + 14, PAD_T + 4);
+ legendX += ctx.measureText(name).width + 24;
+ }
+
+ // Store layout for hover.
+ this._chartLayout = { PAD_L, PAD_R, PAD_T, PAD_B, chartW, chartH, startUs, endUs, rangeUs, minVal, maxVal, xAt, yAt };
+ }
+
+ onChartHover(e) {
+ if (!this._chartLayout || this.selectedStats.size === 0) {
+ this.tooltipEl.hidden = true;
+ return;
+ }
+ const rect = this.canvas.getBoundingClientRect();
+ const mx = e.clientX - rect.left;
+ const my = e.clientY - rect.top;
+ const { PAD_L, PAD_T, chartW, chartH, startUs, rangeUs } = this._chartLayout;
+
+ if (mx < PAD_L || mx > PAD_L + chartW || my < PAD_T || my > PAD_T + chartH) {
+ this.tooltipEl.hidden = true;
+ return;
+ }
+
+ const cursorUs = startUs + (mx - PAD_L) / chartW * rangeUs;
+ const lines = [];
+ for (const statId of this.selectedStats) {
+ const samples = this.seriesData.get(statId);
+ if (!samples || samples.length === 0) continue;
+ // Find nearest sample.
+ let best = null, bestDist = Infinity;
+ for (const s of samples) {
+ const d = Math.abs(s.timeUs - cursorUs);
+ if (d < bestDist) { bestDist = d; best = s; }
+ }
+ if (best) {
+ const def = this.statDefs.find(d => d.stat_id === statId);
+ const name = def ? def.name : `stat ${statId}`;
+ const color = this.statColors.get(statId) || "#fff";
+ lines.push(`<span style="color:${color}">${escapeHtml(name)}</span>: ${best.value.toFixed(3)}`);
+ }
+ }
+ if (lines.length === 0) { this.tooltipEl.hidden = true; return; }
+ this.tooltipEl.innerHTML = `<div style="margin-bottom:2px">${formatTime(cursorUs)}</div>` + lines.join("<br>");
+ this.tooltipEl.style.left = `${mx + 12}px`;
+ this.tooltipEl.style.top = `${my + 12}px`;
+ this.tooltipEl.hidden = false;
+ }
+
+ onPanStart(e) {
+ if (e.button !== 0) return;
+ this.panning = true;
+ this.panStartX = e.clientX;
+ this.panStartViewStart = this.viewStartUs;
+ this.panStartViewEnd = this.viewEndUs;
+ }
+
+ onPanMove(e) {
+ if (!this.panning || !this._chartLayout) return;
+ const dx = e.clientX - this.panStartX;
+ const usPerPx = (this.panStartViewEnd - this.panStartViewStart) / this._chartLayout.chartW;
+ const shift = -dx * usPerPx;
+ this.viewStartUs = this.panStartViewStart + shift;
+ this.viewEndUs = this.panStartViewEnd + shift;
+ this.drawChart();
+ }
+
+ onPanEnd() { this.panning = false; }
+
+ onWheel(e) {
+ e.preventDefault();
+ if (!this._chartLayout) return;
+ const rect = this.canvas.getBoundingClientRect();
+ const mx = e.clientX - rect.left;
+ const { PAD_L, chartW, startUs, rangeUs } = this._chartLayout;
+ const cursorUs = startUs + (mx - PAD_L) / chartW * rangeUs;
+ const factor = e.deltaY > 0 ? 1.25 : 0.8;
+ const newRange = Math.max(10, (this.viewEndUs - this.viewStartUs) * factor);
+ const ratio = (mx - PAD_L) / chartW;
+ this.viewStartUs = cursorUs - ratio * newRange;
+ this.viewEndUs = this.viewStartUs + newRange;
+ this.drawChart();
+ }
+}
diff --git a/src/zen/frontend/html/index.html b/src/zen/frontend/html/index.html
new file mode 100644
index 000000000..5853a80dc
--- /dev/null
+++ b/src/zen/frontend/html/index.html
@@ -0,0 +1,95 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>zen trace viewer</title>
+ <link rel="stylesheet" href="trace.css">
+</head>
+<body>
+ <noscript>This viewer requires JavaScript.</noscript>
+ <div class="header">
+ <div class="header-title">zen trace viewer</div>
+ <div class="header-file" id="hdr-file"></div>
+ <div class="header-stats" id="hdr-stats"></div>
+ <button id="theme-toggle" class="header-btn" type="button" title="Toggle dark/light mode">Theme</button>
+ </div>
+ <div class="layout">
+ <aside class="sidebar">
+ <nav class="tabs">
+ <button class="tab active" data-tab="timeline">Timeline</button>
+ <button class="tab" data-tab="stats">Stats</button>
+ <button class="tab" data-tab="memory">Memory</button>
+ <button class="tab" data-tab="logs">Logs</button>
+ <button class="tab" data-tab="csv">CSV</button>
+ <button class="tab" data-tab="session">Session</button>
+ </nav>
+ <div class="sidebar-section">
+ <div class="sidebar-label">Search scopes</div>
+ <input id="search-input" type="text" placeholder="filter scopes..." autocomplete="off" spellcheck="false">
+ <div id="search-results" class="search-results"></div>
+ </div>
+ <div class="sidebar-section" id="regions-panel" hidden>
+ <div class="sidebar-label">Regions <button id="regions-toggle-all" class="sidebar-action">deselect all</button></div>
+ <div id="regions-list" class="regions-list"></div>
+ </div>
+ <div class="sidebar-section" id="threads-panel">
+ <div class="sidebar-label">Threads <button id="threads-toggle-all" class="sidebar-action">deselect all</button></div>
+ <div id="threads-list" class="threads-list"></div>
+ </div>
+ </aside>
+ <main class="content">
+ <section class="view view-timeline" data-view="timeline">
+ <div class="timeline-toolbar">
+ <div id="viewport-info" class="viewport-info"></div>
+ <label class="toolbar-toggle" title="Show or hide bookmark markers">
+ <input type="checkbox" id="bookmarks-toggle" checked>
+ <span>Bookmarks</span>
+ </label>
+ <label class="toolbar-toggle" title="Disable LOD to always fetch full-resolution scopes (slower but useful for validating LOD correctness)">
+ <input type="checkbox" id="lod-toggle" checked>
+ <span>LOD</span>
+ </label>
+ <button id="zoom-reset" class="btn">Reset view</button>
+ </div>
+ <div class="timeline-frame">
+ <canvas id="timeline-canvas"></canvas>
+ <div id="tooltip" class="tooltip" hidden></div>
+ </div>
+ <div id="selection-panel" class="selection-panel">
+ <div class="selection-hint">Click a scope to see details. Drag to pan, wheel to zoom.</div>
+ </div>
+ </section>
+ <section class="view view-stats" data-view="stats" hidden>
+ <table class="stats-table">
+ <thead>
+ <tr>
+ <th data-sort="name">Scope</th>
+ <th data-sort="count" class="num">Count</th>
+ <th data-sort="min_us" class="num">Min (ms)</th>
+ <th data-sort="mean_us" class="num">Mean (ms)</th>
+ <th data-sort="max_us" class="num">Max (ms)</th>
+ <th data-sort="stdev_us" class="num">σ (ms)</th>
+ </tr>
+ </thead>
+ <tbody id="stats-tbody"></tbody>
+ </table>
+ </section>
+ <section class="view view-memory" data-view="memory" hidden>
+ <div id="memory-content"></div>
+ </section>
+ <section class="view view-logs" data-view="logs" hidden>
+ <div id="logs-content"></div>
+ </section>
+ <section class="view view-csv" data-view="csv" hidden>
+ <div id="csv-content"></div>
+ </section>
+ <section class="view view-session" data-view="session" hidden>
+ <div id="session-content" class="session-content"></div>
+ </section>
+ </main>
+ </div>
+ <div id="loading" class="loading">Loading trace…</div>
+ <script type="module" src="trace.js"></script>
+</body>
+</html>
diff --git a/src/zen/frontend/html/logs.js b/src/zen/frontend/html/logs.js
new file mode 100644
index 000000000..d9646ba39
--- /dev/null
+++ b/src/zen/frontend/html/logs.js
@@ -0,0 +1,237 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+// Log viewer: filterable list of captured Logging.LogMessage events.
+
+import { getLogs } from "./api.js";
+
+// UE ELogVerbosity::Type values — lower number = more severe.
+const VERBOSITY_LABELS = [
+ "NoLogging",
+ "Fatal",
+ "Error",
+ "Warning",
+ "Display",
+ "Log",
+ "Verbose",
+ "VeryVerbose",
+ "All",
+];
+
+function escapeHtml(s) {
+ return String(s).replace(/[&<>"']/g, (c) => ({
+ "&": "&amp;",
+ "<": "&lt;",
+ ">": "&gt;",
+ "\"": "&quot;",
+ "'": "&#39;",
+ }[c]));
+}
+
+function verbosityLabel(v) {
+ return VERBOSITY_LABELS[v] || `V${v}`;
+}
+
+function verbosityClass(v) {
+ switch (v) {
+ case 1: return "vb-fatal";
+ case 2: return "vb-error";
+ case 3: return "vb-warn";
+ case 4: return "vb-display";
+ case 5: return "vb-log";
+ case 6: case 7: return "vb-verbose";
+ default: return "vb-other";
+ }
+}
+
+function formatTime(us) {
+ const totalMs = Math.floor(us / 1000);
+ const ms = totalMs % 1000;
+ const totalS = Math.floor(totalMs / 1000);
+ const s = totalS % 60;
+ const totalM = Math.floor(totalS / 60);
+ const m = totalM % 60;
+ const h = Math.floor(totalM / 60);
+ if (h > 0) {
+ return `${h}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}.${String(ms).padStart(3, "0")}`;
+ }
+ return `${m}:${String(s).padStart(2, "0")}.${String(ms).padStart(3, "0")}`;
+}
+
+export class LogsView {
+ constructor(model, containerEl) {
+ this.model = model;
+ this.container = containerEl;
+ this.minVerbosity = 0;
+ this.category = "";
+ this.textFilter = "";
+ this.loaded = false;
+ this.rendering = null;
+
+ this.container.innerHTML =
+ `<div class="logs-toolbar">
+ <div class="logs-filter">
+ <span class="logs-filter-label">Verbosity</span>
+ <select id="log-verbosity">
+ <option value="0">All</option>
+ <option value="6">Verbose</option>
+ <option value="5">Log</option>
+ <option value="4">Display</option>
+ <option value="3">Warning</option>
+ <option value="2">Error</option>
+ <option value="1">Fatal</option>
+ </select>
+ </div>
+ <div class="logs-filter">
+ <span class="logs-filter-label">Category</span>
+ <select id="log-category">
+ <option value="">All</option>
+ </select>
+ </div>
+ <div class="logs-filter logs-filter-grow">
+ <span class="logs-filter-label">Search</span>
+ <input id="log-search" type="text" placeholder="filter messages..." autocomplete="off" spellcheck="false">
+ </div>
+ <div id="log-count" class="logs-count"></div>
+ </div>
+ <div class="logs-list-wrap">
+ <table class="logs-table">
+ <thead><tr>
+ <th class="col-time">Time</th>
+ <th class="col-verb">Verbosity</th>
+ <th class="col-cat">Category</th>
+ <th class="col-msg">Message</th>
+ <th class="col-loc">Source</th>
+ </tr></thead>
+ <tbody id="logs-tbody"></tbody>
+ </table>
+ </div>`;
+
+ const catSelect = this.container.querySelector("#log-category");
+ if ((this.model.bookmarks || []).length > 0) {
+ const bmOpt = document.createElement("option");
+ bmOpt.value = "bookmarks";
+ bmOpt.textContent = "(bookmarks only)";
+ catSelect.appendChild(bmOpt);
+ }
+ for (let i = 0; i < this.model.logCategories.length; i++) {
+ const c = this.model.logCategories[i];
+ const opt = document.createElement("option");
+ opt.value = String(i);
+ opt.textContent = c.name || `category ${i}`;
+ catSelect.appendChild(opt);
+ }
+
+ this.container.querySelector("#log-verbosity").addEventListener("change", (e) => {
+ this.minVerbosity = Number(e.target.value) || 0;
+ this.refresh();
+ });
+ this.container.querySelector("#log-category").addEventListener("change", (e) => {
+ this.category = e.target.value;
+ this.refresh();
+ });
+ this.container.querySelector("#log-search").addEventListener("input", (e) => {
+ this.textFilter = e.target.value.toLowerCase();
+ this.renderFiltered();
+ });
+ }
+
+ async ensureLoaded() {
+ if (this.loaded) return;
+ this.loaded = true;
+ await this.refresh();
+ }
+
+ async refresh() {
+ // "(bookmarks only)" is a synthetic category that displays just the
+ // bookmark rows without hitting the /api/logs endpoint.
+ if (this.category === "bookmarks") {
+ this.result = { entries: [], total: 0, returned: 0 };
+ this.renderFiltered();
+ return;
+ }
+
+ const opts = {
+ minVerbosity: this.minVerbosity,
+ limit: 5000,
+ };
+ if (this.category !== "") {
+ opts.category = Number(this.category);
+ }
+ try {
+ this.result = await getLogs(opts);
+ } catch (e) {
+ this.container.querySelector("#logs-tbody").innerHTML =
+ `<tr><td colspan="5" class="logs-error">Failed to load logs: ${escapeHtml(e.message)}</td></tr>`;
+ return;
+ }
+ this.renderFiltered();
+ }
+
+ renderFiltered() {
+ if (!this.result) return;
+ const entries = this.result.entries || [];
+ const filter = this.textFilter;
+ const tbody = this.container.querySelector("#logs-tbody");
+ const count = this.container.querySelector("#log-count");
+
+ // Bookmarks are interleaved into the display list when the category
+ // filter is "All" or "(bookmarks only)". Any other category is log-
+ // specific so bookmarks are hidden to avoid confusion.
+ const showBookmarks = (this.category === "" || this.category === "bookmarks");
+ const bookmarks = showBookmarks ? (this.model.bookmarks || []) : [];
+
+ // Build a combined, time-sorted row list. Each item keeps its kind
+ // so we can render log and bookmark rows differently.
+ const items = [];
+ for (const e of entries) {
+ items.push({ kind: "log", time: e.time_us, entry: e });
+ }
+ for (const b of bookmarks) {
+ items.push({ kind: "bookmark", time: b.time_us, entry: b });
+ }
+ items.sort((a, b) => a.time - b.time);
+
+ const rows = [];
+ let shown = 0;
+ for (const it of items) {
+ if (it.kind === "log") {
+ const e = it.entry;
+ if (filter && !e.message.toLowerCase().includes(filter)) continue;
+ const cat = this.model.logCategories[e.category_index] || { name: "(unknown)" };
+ const file = e.file ? String(e.file).split(/[\\/]/).pop() : "";
+ rows.push(
+ `<tr class="${verbosityClass(e.verbosity)}">` +
+ `<td class="col-time mono">${formatTime(e.time_us)}</td>` +
+ `<td class="col-verb">${escapeHtml(verbosityLabel(e.verbosity))}</td>` +
+ `<td class="col-cat">${escapeHtml(cat.name)}</td>` +
+ `<td class="col-msg">${escapeHtml(e.message)}</td>` +
+ `<td class="col-loc mono">${escapeHtml(file)}${e.line ? ":" + e.line : ""}</td>` +
+ `</tr>`,
+ );
+ } else {
+ const b = it.entry;
+ if (filter && !b.text.toLowerCase().includes(filter)) continue;
+ const file = b.file ? String(b.file).split(/[\\/]/).pop() : "";
+ rows.push(
+ `<tr class="bm-row">` +
+ `<td class="col-time mono">${formatTime(b.time_us)}</td>` +
+ `<td class="col-verb">BOOKMARK</td>` +
+ `<td class="col-cat">—</td>` +
+ `<td class="col-msg">${escapeHtml(b.text)}</td>` +
+ `<td class="col-loc mono">${escapeHtml(file)}${b.line ? ":" + b.line : ""}</td>` +
+ `</tr>`,
+ );
+ }
+ shown++;
+ }
+ tbody.innerHTML = rows.join("") ||
+ `<tr><td colspan="5" class="logs-empty">No entries match the current filter.</td></tr>`;
+
+ const total = (this.result.total || 0) + bookmarks.length;
+ const returned = (this.result.returned || 0) + bookmarks.length;
+ if (total > returned) {
+ count.textContent = `${shown.toLocaleString()} shown · ${returned.toLocaleString()} of ${total.toLocaleString()} loaded`;
+ } else {
+ count.textContent = `${shown.toLocaleString()} of ${total.toLocaleString()}`;
+ }
+ }
+}
diff --git a/src/zen/frontend/html/memory.js b/src/zen/frontend/html/memory.js
new file mode 100644
index 000000000..6b9760439
--- /dev/null
+++ b/src/zen/frontend/html/memory.js
@@ -0,0 +1,790 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+// Interactive memory analysis view: summary cards, memory timeline, leak/churn/hot callsite tables.
+
+import { getAllocSummary, getMemoryTimeline, getCallstackStats, getChurnStats, getCallstack, getAllocSizeHistogram } from "./api.js";
+
+function escapeHtml(s) {
+ return String(s).replace(/[&<>"']/g, (c) => ({
+ "&": "&amp;",
+ "<": "&lt;",
+ ">": "&gt;",
+ '"': "&quot;",
+ "'": "&#39;",
+ }[c]));
+}
+
+function formatNum(n) {
+ return Number(n || 0).toLocaleString();
+}
+
+function formatBytes(bytes) {
+ const sign = bytes < 0 ? "-" : "";
+ let value = Math.abs(Number(bytes || 0));
+ const units = ["B", "KB", "MB", "GB", "TB"];
+ let unit = 0;
+ while (value >= 1024 && unit < units.length - 1) {
+ value /= 1024;
+ unit++;
+ }
+ const decimals = unit === 0 ? 0 : value >= 100 ? 0 : value >= 10 ? 1 : 2;
+ return `${sign}${value.toFixed(decimals)} ${units[unit]}`;
+}
+
+function formatDistance(events) {
+ return `${Math.round(Number(events || 0)).toLocaleString()} ev`;
+}
+
+function formatTimeAxis(us) {
+ const value = Number(us || 0);
+ if (value < 1000) return `${Math.round(value)} µs`;
+ if (value < 1_000_000) return `${Math.round(value / 1000)} ms`;
+ return `${Math.round(value / 1_000_000)} s`;
+}
+
+function chooseNiceTimeStep(spanUs, targetTickCount) {
+ const rawStep = Math.max(1, spanUs / Math.max(1, targetTickCount));
+ const bases = [1, 2, 5];
+ const magnitude = Math.pow(10, Math.floor(Math.log10(rawStep)));
+ for (const scale of [1, 10]) {
+ for (const base of bases) {
+ const step = base * magnitude * scale;
+ if (step >= rawStep) {
+ return step;
+ }
+ }
+ }
+ return 10 * magnitude;
+}
+
+function buildNiceTimeTicks(startUs, endUs, targetTickCount) {
+ const spanUs = Math.max(1, endUs - startUs);
+ const stepUs = chooseNiceTimeStep(spanUs, targetTickCount);
+ const firstTickUs = Math.ceil(startUs / stepUs) * stepUs;
+ const ticks = [];
+ for (let tickUs = firstTickUs; tickUs <= endUs; tickUs += stepUs) {
+ ticks.push(tickUs);
+ }
+ if (ticks.length === 0) {
+ const roundedStart = Math.floor(startUs / stepUs) * stepUs;
+ const roundedEnd = Math.ceil(endUs / stepUs) * stepUs;
+ if (roundedStart >= startUs && roundedStart <= endUs) {
+ ticks.push(roundedStart);
+ }
+ if (roundedEnd >= startUs && roundedEnd <= endUs && roundedEnd !== roundedStart) {
+ ticks.push(roundedEnd);
+ }
+ }
+ return ticks;
+}
+
+function trimPath(path) {
+ if (!path) return "";
+ const parts = String(path).split(/[\\/]/);
+ return parts[parts.length - 1] || path;
+}
+
+function compareValues(a, b, desc) {
+ if (typeof a === "string" || typeof b === "string") {
+ const result = String(a || "").localeCompare(String(b || ""), undefined, { numeric: true, sensitivity: "base" });
+ return desc ? -result : result;
+ }
+ const result = Number(a || 0) - Number(b || 0);
+ return desc ? -result : result;
+}
+
+function escapeRegExp(s) {
+ return String(s).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
+}
+
+function highlightMatch(text, filterText) {
+ const source = String(text || "");
+ if (!filterText) {
+ return escapeHtml(source);
+ }
+ const regex = new RegExp(`(${escapeRegExp(filterText)})`, "ig");
+ return escapeHtml(source).replace(regex, '<mark class="memory-mark">$1</mark>');
+}
+
+function buildSummaryHtml(row, filterText = "") {
+ const top = highlightMatch(row.top_frame || row.summary || `Callstack ${row.callstack_id}`, filterText);
+ const second = row.secondary_frame ? `<div class="memory-summary-secondary">${highlightMatch(row.secondary_frame, filterText)}</div>` : "";
+ const badges = [];
+ if (row.hidden_prefix_count > 0) {
+ badges.push(`<span class="memory-badge">skip ${escapeHtml(formatNum(row.hidden_prefix_count))}</span>`);
+ }
+ if (row.included_third_party_boundary) {
+ badges.push(`<span class="memory-badge">3p boundary</span>`);
+ }
+ const badgeHtml = badges.length ? `<div class="memory-summary-badges">${badges.join("")}</div>` : "";
+ return `<div class="memory-summary"><div class="memory-summary-top-row"><div class="memory-summary-top">${top}</div>${badgeHtml}</div>${second}</div>`;
+}
+
+function describeBucket(bucket) {
+ const min = Number(bucket.min_size || 0);
+ const max = Number(bucket.max_size || 0);
+ if (min === 0 && max === 0) {
+ return "0 bytes";
+ }
+ if (min === max) {
+ return `${formatBytes(min)}`;
+ }
+ return `${formatBytes(min)} – ${formatBytes(max)}`;
+}
+
+function formatBucketEdge(bucket) {
+ const max = Number(bucket.max_size || 0);
+ if (max === 0) {
+ return "0";
+ }
+ return formatBytes(max);
+}
+
+function sparklinePath(samples, width, height, valueIndex) {
+ if (!samples || samples.length === 0) {
+ return "";
+ }
+ let minValue = Infinity;
+ let maxValue = -Infinity;
+ for (const sample of samples) {
+ const value = Number(sample[valueIndex] || 0);
+ minValue = Math.min(minValue, value);
+ maxValue = Math.max(maxValue, value);
+ }
+ if (!isFinite(minValue) || !isFinite(maxValue)) {
+ return "";
+ }
+ if (minValue === maxValue) {
+ minValue -= 1;
+ maxValue += 1;
+ }
+ const count = samples.length;
+ const points = [];
+ for (let i = 0; i < count; ++i) {
+ const x = count > 1 ? (i / (count - 1)) * width : width * 0.5;
+ const norm = (Number(samples[i][valueIndex] || 0) - minValue) / (maxValue - minValue);
+ const y = height - norm * height;
+ points.push(`${i === 0 ? "M" : "L"}${x.toFixed(1)} ${y.toFixed(1)}`);
+ }
+ return points.join(" ");
+}
+
+export class MemoryView {
+ constructor(model, containerEl) {
+ this.model = model;
+ this.container = containerEl;
+ this.loaded = false;
+ this.summary = null;
+ this.memoryTimeline = null;
+ this.leaks = [];
+ this.churn = [];
+ this.hot = [];
+ this.sizeHistogram = null;
+ this.histogramMetric = "count";
+ this.callstackCache = new Map();
+ this.selectedCallstackId = 0;
+ this.tableState = {
+ leaks: { sortKey: "live_bytes", desc: true, groupMode: "none", filterText: "" },
+ churn: { sortKey: "churn_allocs", desc: true, groupMode: "none", filterText: "" },
+ hot: { sortKey: "total_allocs", desc: true, groupMode: "none", filterText: "" },
+ };
+ this.loadStateFromUrl();
+ this.buildLayout();
+ }
+
+ buildLayout() {
+ this.container.innerHTML =
+ `<div class="memory-view">` +
+ `<div class="memory-cards" id="memory-cards"></div>` +
+ `<div class="memory-panel">` +
+ `<div class="memory-panel-header">` +
+ `<div class="memory-panel-title">Memory timeline</div>` +
+ `<div class="memory-panel-subtitle" id="memory-timeline-meta"></div>` +
+ `</div>` +
+ `<div class="memory-chart-wrap" id="memory-chart-wrap">` +
+ `<svg class="memory-chart" id="memory-chart"></svg>` +
+ `</div>` +
+ `</div>` +
+ `<div class="memory-panel">` +
+ `<div class="memory-panel-header memory-panel-header-wrap">` +
+ `<div>` +
+ `<div class="memory-panel-title">Allocation size distribution</div>` +
+ `<div class="memory-panel-subtitle" id="memory-histogram-meta"></div>` +
+ `</div>` +
+ `<div class="memory-controls">` +
+ `<label>Metric <select id="memory-histogram-metric">` +
+ `<option value="count">Alloc count</option>` +
+ `<option value="bytes">Total bytes</option>` +
+ `</select></label>` +
+ `</div>` +
+ `</div>` +
+ `<div class="memory-chart-wrap" id="memory-histogram-wrap">` +
+ `<svg class="memory-chart memory-histogram" id="memory-histogram"></svg>` +
+ `</div>` +
+ `</div>` +
+ `<div class="memory-grid">` +
+ this.buildPanelMarkup("leaks", "Leaky callsites", "Top live allocation stacks", [
+ ["live_bytes", "Live bytes"],
+ ["live_count", "Live allocs"],
+ ["summary", "Summary"],
+ ]) +
+ this.buildPanelMarkup("churn", "Churn", "Short-lived allocation sites", [
+ ["churn_allocs", "Short-lived allocs"],
+ ["churn_bytes", "Churn bytes"],
+ ["mean_distance", "Avg distance"],
+ ["summary", "Summary"],
+ ]) +
+ this.buildPanelMarkup("hot", "Hot callsites", "Highest total allocation activity", [
+ ["total_allocs", "Total allocs"],
+ ["total_bytes", "Total bytes"],
+ ["churn_allocs", "Churn allocs"],
+ ["summary", "Summary"],
+ ]) +
+ `<div class="memory-panel memory-callstack-panel">` +
+ `<div class="memory-panel-header"><div class="memory-panel-title">Callstack details</div><div class="memory-panel-subtitle" id="memory-callstack-meta">Select a row to inspect its frames</div></div>` +
+ `<div class="memory-callstack-body" id="memory-callstack-body"><div class="memory-empty">No callstack selected.</div></div>` +
+ `</div>` +
+ `</div>` +
+ `</div>`;
+
+ this.cardsEl = this.container.querySelector("#memory-cards");
+ this.chartWrapEl = this.container.querySelector("#memory-chart-wrap");
+ this.chartEl = this.container.querySelector("#memory-chart");
+ this.chartMetaEl = this.container.querySelector("#memory-timeline-meta");
+ this.histogramWrapEl = this.container.querySelector("#memory-histogram-wrap");
+ this.histogramEl = this.container.querySelector("#memory-histogram");
+ this.histogramMetaEl = this.container.querySelector("#memory-histogram-meta");
+ this.histogramMetricEl = this.container.querySelector("#memory-histogram-metric");
+ this.callstackMetaEl = this.container.querySelector("#memory-callstack-meta");
+ this.callstackBodyEl = this.container.querySelector("#memory-callstack-body");
+ this.panelRefs = {
+ leaks: {
+ tbody: this.container.querySelector("#memory-leaks-body"),
+ sort: this.container.querySelector("#memory-leaks-sort"),
+ filter: this.container.querySelector("#memory-leaks-filter"),
+ clear: this.container.querySelector("#memory-leaks-clear"),
+ direction: this.container.querySelector("#memory-leaks-direction"),
+ group: this.container.querySelector("#memory-leaks-group"),
+ },
+ churn: {
+ tbody: this.container.querySelector("#memory-churn-body"),
+ sort: this.container.querySelector("#memory-churn-sort"),
+ filter: this.container.querySelector("#memory-churn-filter"),
+ clear: this.container.querySelector("#memory-churn-clear"),
+ direction: this.container.querySelector("#memory-churn-direction"),
+ group: this.container.querySelector("#memory-churn-group"),
+ },
+ hot: {
+ tbody: this.container.querySelector("#memory-hot-body"),
+ sort: this.container.querySelector("#memory-hot-sort"),
+ filter: this.container.querySelector("#memory-hot-filter"),
+ clear: this.container.querySelector("#memory-hot-clear"),
+ direction: this.container.querySelector("#memory-hot-direction"),
+ group: this.container.querySelector("#memory-hot-group"),
+ },
+ };
+
+ this.resizeObserver = new ResizeObserver(() => {
+ if (this.loaded) {
+ this.renderTimeline();
+ this.renderSizeHistogram();
+ }
+ });
+ this.resizeObserver.observe(this.chartWrapEl);
+ this.resizeObserver.observe(this.histogramWrapEl);
+
+ this.histogramMetricEl.value = this.histogramMetric;
+ this.histogramMetricEl.addEventListener("change", () => {
+ this.histogramMetric = this.histogramMetricEl.value;
+ this.saveStateToUrl();
+ this.renderSizeHistogram();
+ });
+
+ for (const [name, refs] of Object.entries(this.panelRefs)) {
+ refs.sort.value = this.tableState[name].sortKey;
+ refs.group.value = this.tableState[name].groupMode;
+ refs.filter.value = this.tableState[name].filterText;
+ refs.sort.addEventListener("change", () => {
+ this.tableState[name].sortKey = refs.sort.value;
+ this.tableState[name].desc = refs.sort.value !== "mean_distance";
+ this.updateDirectionButton(name);
+ this.saveStateToUrl();
+ this.renderTableByName(name);
+ });
+ refs.direction.addEventListener("click", () => {
+ this.tableState[name].desc = !this.tableState[name].desc;
+ this.updateDirectionButton(name);
+ this.saveStateToUrl();
+ this.renderTableByName(name);
+ });
+ refs.filter.addEventListener("input", () => {
+ this.tableState[name].filterText = refs.filter.value;
+ this.updateFilterButton(name);
+ this.saveStateToUrl();
+ this.renderTableByName(name);
+ });
+ refs.clear.addEventListener("click", () => {
+ refs.filter.value = "";
+ this.tableState[name].filterText = "";
+ this.updateFilterButton(name);
+ this.saveStateToUrl();
+ this.renderTableByName(name);
+ refs.filter.focus();
+ });
+ refs.group.addEventListener("change", () => {
+ this.tableState[name].groupMode = refs.group.value;
+ this.saveStateToUrl();
+ this.renderTableByName(name);
+ });
+ this.updateDirectionButton(name);
+ this.updateFilterButton(name);
+ }
+
+ this.container.addEventListener("keydown", (e) => {
+ if (e.key !== "/" || e.defaultPrevented) {
+ return;
+ }
+ const target = e.target;
+ if (target && (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.tagName === "SELECT" || target.isContentEditable)) {
+ return;
+ }
+ e.preventDefault();
+ const activeView = this.container.closest(".view");
+ if (activeView && activeView.hidden) {
+ return;
+ }
+ const firstFilter = this.panelRefs.leaks.filter;
+ if (firstFilter) {
+ firstFilter.focus();
+ firstFilter.select();
+ }
+ });
+ this.container.tabIndex = -1;
+ this.container.dataset.memoryView = "true";
+ }
+
+ buildPanelMarkup(name, title, subtitle, sortOptions) {
+ const sortHtml = sortOptions.map(([value, label]) => `<option value="${value}">${escapeHtml(label)}</option>`).join("");
+ return `
+ <div class="memory-panel">
+ <div class="memory-panel-header memory-panel-header-wrap">
+ <div>
+ <div class="memory-panel-title">${escapeHtml(title)}</div>
+ <div class="memory-panel-subtitle">${escapeHtml(subtitle)}</div>
+ </div>
+ <div class="memory-controls">
+ <label>Filter <input type="text" id="memory-${name}-filter" class="memory-filter-input" placeholder="filter entries..."></label>
+ <button type="button" class="memory-clear-btn" id="memory-${name}-clear">Clear</button>
+ <label>Sort <select id="memory-${name}-sort">${sortHtml}</select></label>
+ <button type="button" class="memory-direction-btn" id="memory-${name}-direction"></button>
+ <label>Group <select id="memory-${name}-group">
+ <option value="none">None</option>
+ <option value="top_frame">Top frame</option>
+ <option value="prefix">Trimmed prefix</option>
+ </select></label>
+ </div>
+ </div>
+ <div class="memory-table-wrap"><table class="memory-table"><tbody id="memory-${name}-body"></tbody></table></div>
+ </div>`;
+ }
+
+ loadStateFromUrl() {
+ const params = new URLSearchParams(window.location.search);
+ const histogramMetric = params.get("mem_hist_metric");
+ if (histogramMetric === "count" || histogramMetric === "bytes") {
+ this.histogramMetric = histogramMetric;
+ }
+ for (const [name, state] of Object.entries(this.tableState)) {
+ const sortKey = params.get(`mem_${name}_sort`);
+ const groupMode = params.get(`mem_${name}_group`);
+ const dir = params.get(`mem_${name}_dir`);
+ const filterText = params.get(`mem_${name}_filter`);
+ if (sortKey) {
+ state.sortKey = sortKey;
+ }
+ if (groupMode) {
+ state.groupMode = groupMode;
+ }
+ if (filterText) {
+ state.filterText = filterText;
+ }
+ if (dir === "asc") {
+ state.desc = false;
+ }
+ else if (dir === "desc") {
+ state.desc = true;
+ }
+ }
+ }
+
+ saveStateToUrl() {
+ const url = new URL(window.location.href);
+ url.searchParams.set("mem_hist_metric", this.histogramMetric);
+ for (const [name, state] of Object.entries(this.tableState)) {
+ url.searchParams.set(`mem_${name}_sort`, state.sortKey);
+ url.searchParams.set(`mem_${name}_group`, state.groupMode);
+ url.searchParams.set(`mem_${name}_dir`, state.desc ? "desc" : "asc");
+ if (state.filterText) {
+ url.searchParams.set(`mem_${name}_filter`, state.filterText);
+ }
+ else {
+ url.searchParams.delete(`mem_${name}_filter`);
+ }
+ }
+ window.history.replaceState({}, "", url);
+ }
+
+ updateFilterButton(name) {
+ const refs = this.panelRefs[name];
+ const hasText = !!this.tableState[name].filterText;
+ refs.clear.disabled = !hasText;
+ refs.clear.title = hasText ? "Clear filter" : "Filter is empty";
+ }
+
+ updateDirectionButton(name) {
+ const refs = this.panelRefs[name];
+ const desc = this.tableState[name].desc;
+ refs.direction.textContent = desc ? "↓ Desc" : "↑ Asc";
+ refs.direction.setAttribute("aria-label", desc ? "Sort descending" : "Sort ascending");
+ refs.direction.title = desc ? "Sorting descending" : "Sorting ascending";
+ }
+
+ async ensureLoaded() {
+ if (this.loaded) return;
+ this.loaded = true;
+ await this.refresh();
+ }
+
+ async refresh() {
+ this.renderLoading();
+ try {
+ const [summary, memoryTimeline, leakResponse, churnResponse, sizeHistogram] = await Promise.all([
+ getAllocSummary(),
+ getMemoryTimeline({ maxSamples: 1200 }),
+ getCallstackStats(100),
+ getChurnStats(200),
+ getAllocSizeHistogram(),
+ ]);
+ this.summary = summary;
+ this.memoryTimeline = memoryTimeline;
+ this.leaks = (leakResponse && leakResponse.stats) || [];
+ this.churn = (churnResponse && churnResponse.stats) || [];
+ this.sizeHistogram = sizeHistogram;
+ this.hot = this.churn.slice().sort((a, b) => {
+ if (b.total_allocs !== a.total_allocs) return b.total_allocs - a.total_allocs;
+ return b.total_bytes - a.total_bytes;
+ }).slice(0, 100);
+ this.render();
+ } catch (e) {
+ this.cardsEl.innerHTML = "";
+ this.chartEl.innerHTML = "";
+ this.chartMetaEl.textContent = "";
+ this.histogramEl.innerHTML = "";
+ this.histogramMetaEl.textContent = "";
+ for (const refs of Object.values(this.panelRefs)) {
+ refs.tbody.innerHTML = `<tr><td class="memory-empty">Failed to load memory data: ${escapeHtml(e.message)}</td></tr>`;
+ }
+ this.callstackBodyEl.innerHTML = `<div class="memory-empty">Failed to load memory data.</div>`;
+ }
+ }
+
+ renderLoading() {
+ this.cardsEl.innerHTML = `<div class="memory-card"><div class="memory-card-label">Loading</div><div class="memory-card-value">Memory analysis…</div></div>`;
+ this.chartEl.innerHTML = "";
+ this.chartMetaEl.textContent = "";
+ this.histogramEl.innerHTML = "";
+ this.histogramMetaEl.textContent = "Loading…";
+ for (const refs of Object.values(this.panelRefs)) {
+ refs.tbody.innerHTML = `<tr><td class="memory-empty">Loading…</td></tr>`;
+ }
+ }
+
+ render() {
+ this.renderCards();
+ this.renderTimeline();
+ this.renderSizeHistogram();
+ this.renderTableByName("leaks");
+ this.renderTableByName("churn");
+ this.renderTableByName("hot");
+ }
+
+ renderCards() {
+ const s = this.summary || {};
+ this.cardsEl.innerHTML = [
+ this.formatCard("Peak memory", formatBytes(s.peak_bytes)),
+ this.formatCard("End memory", formatBytes(s.end_bytes)),
+ this.formatCard("Live allocations", formatNum(s.live_allocations)),
+ this.formatCard("Total allocs", formatNum(s.total_allocs)),
+ this.formatCard("Total frees", formatNum(s.total_frees)),
+ this.formatCard("Reallocs", formatNum((s.total_realloc_allocs || 0) + (s.total_realloc_frees || 0))),
+ ].join("");
+ }
+
+ formatCard(label, value) {
+ return `<div class="memory-card"><div class="memory-card-label">${escapeHtml(label)}</div><div class="memory-card-value">${escapeHtml(value)}</div></div>`;
+ }
+
+ renderTimeline() {
+ const samples = (this.memoryTimeline && this.memoryTimeline.samples) || [];
+ if (samples.length === 0) {
+ this.chartMetaEl.textContent = "No memory timeline samples";
+ this.chartEl.innerHTML = `<text x="500" y="110" text-anchor="middle" class="memory-chart-text">No memory timeline samples</text>`;
+ return;
+ }
+
+ const width = Math.max(320, Math.floor(this.chartWrapEl.getBoundingClientRect().width) - 24);
+ const height = 220;
+ const padLeft = 56;
+ const padRight = 12;
+ const padTop = 12;
+ const padBottom = 22;
+ const chartWidth = width - padLeft - padRight;
+ const chartHeight = height - padTop - padBottom;
+ let minValue = Infinity;
+ let maxValue = -Infinity;
+ for (const sample of samples) {
+ minValue = Math.min(minValue, Number(sample[1] || 0));
+ maxValue = Math.max(maxValue, Number(sample[1] || 0));
+ }
+ if (minValue === maxValue) {
+ minValue -= 1;
+ maxValue += 1;
+ }
+
+ const xAt = (index) => samples.length > 1 ? (padLeft + (index / (samples.length - 1)) * chartWidth) : (padLeft + chartWidth * 0.5);
+ const yAt = (value) => {
+ const norm = (Number(value || 0) - minValue) / Math.max(1, maxValue - minValue);
+ return padTop + chartHeight - norm * chartHeight;
+ };
+ const path = samples.map((sample, index) => `${index === 0 ? "M" : "L"}${xAt(index).toFixed(1)} ${yAt(sample[1]).toFixed(1)}`).join(" ");
+
+ const grid = [];
+ for (let i = 0; i <= 4; ++i) {
+ const y = padTop + chartHeight * i / 4;
+ const value = maxValue + (minValue - maxValue) * i / 4;
+ grid.push(`<line x1="${padLeft}" y1="${y}" x2="${padLeft + chartWidth}" y2="${y}" class="memory-chart-grid"/>`);
+ grid.push(`<text x="${padLeft - 6}" y="${y + 4}" text-anchor="end" class="memory-chart-axis">${escapeHtml(formatBytes(value))}</text>`);
+ }
+
+ const startUs = Number(samples[0][0] || 0);
+ const endUs = Number(samples[samples.length - 1][0] || 0);
+ const spanUs = Math.max(1, endUs - startUs);
+ const targetTickCount = Math.max(3, Math.min(8, Math.floor(chartWidth / 110)));
+ const ticks = buildNiceTimeTicks(startUs, endUs, targetTickCount);
+ for (const timeUs of ticks) {
+ const t = spanUs > 0 ? ((timeUs - startUs) / spanUs) : 0;
+ const x = padLeft + chartWidth * t;
+ grid.push(`<line x1="${x}" y1="${padTop}" x2="${x}" y2="${padTop + chartHeight}" class="memory-chart-grid memory-chart-grid-vert"/>`);
+ grid.push(`<text x="${x}" y="${height - 4}" text-anchor="middle" class="memory-chart-axis">${escapeHtml(formatTimeAxis(timeUs))}</text>`);
+ }
+
+ const durationUs = (this.model.session.trace_end_us || 0) - (this.model.session.trace_start_us || 0);
+ this.chartMetaEl.textContent = `${formatNum(samples.length)} samples across ${(durationUs / 1_000_000).toFixed(2)} s`;
+ this.chartEl.setAttribute("viewBox", `0 0 ${width} ${height}`);
+ this.chartEl.setAttribute("preserveAspectRatio", "xMinYMin meet");
+ this.chartEl.innerHTML =
+ `<rect x="0" y="0" width="${width}" height="${height}" class="memory-chart-bg"/>` +
+ grid.join("") +
+ `<path d="${path}" class="memory-chart-line"/>`;
+ }
+
+ renderSizeHistogram() {
+ const buckets = (this.sizeHistogram && this.sizeHistogram.buckets) || [];
+ if (buckets.length === 0) {
+ this.histogramMetaEl.textContent = "No allocations recorded";
+ this.histogramEl.innerHTML = `<text x="500" y="110" text-anchor="middle" class="memory-chart-text">No allocations recorded</text>`;
+ return;
+ }
+
+ const metric = this.histogramMetric === "bytes" ? "bytes" : "count";
+ const metricLabel = metric === "bytes" ? "Total bytes" : "Alloc count";
+ const valueFor = (b) => Number((metric === "bytes" ? b.bytes : b.count) || 0);
+ const formatValue = metric === "bytes" ? formatBytes : formatNum;
+
+ let maxValue = 0;
+ let totalValue = 0;
+ for (const bucket of buckets) {
+ const v = valueFor(bucket);
+ if (v > maxValue) maxValue = v;
+ totalValue += v;
+ }
+ if (maxValue === 0) {
+ maxValue = 1;
+ }
+
+ const width = Math.max(320, Math.floor(this.histogramWrapEl.getBoundingClientRect().width) - 24);
+ const height = 240;
+ const padLeft = 64;
+ const padRight = 12;
+ const padTop = 12;
+ const padBottom = 42;
+ const chartWidth = width - padLeft - padRight;
+ const chartHeight = height - padTop - padBottom;
+
+ const bucketCount = buckets.length;
+ const slotWidth = chartWidth / bucketCount;
+ const barGap = Math.max(1, Math.min(4, slotWidth * 0.15));
+ const barWidth = Math.max(1, slotWidth - barGap);
+
+ const parts = [];
+ parts.push(`<rect x="0" y="0" width="${width}" height="${height}" class="memory-chart-bg"/>`);
+
+ // Horizontal grid + y-axis labels at 0, 25, 50, 75, 100% of max.
+ for (let i = 0; i <= 4; ++i) {
+ const y = padTop + chartHeight * i / 4;
+ const value = maxValue * (1 - i / 4);
+ parts.push(`<line x1="${padLeft}" y1="${y}" x2="${padLeft + chartWidth}" y2="${y}" class="memory-chart-grid"/>`);
+ parts.push(`<text x="${padLeft - 6}" y="${y + 4}" text-anchor="end" class="memory-chart-axis">${escapeHtml(formatValue(value))}</text>`);
+ }
+
+ // Bars. X-axis labels are drawn for a subset of buckets to avoid overlap.
+ const labelStride = Math.max(1, Math.ceil(bucketCount / Math.max(3, Math.floor(chartWidth / 64))));
+ for (let i = 0; i < bucketCount; ++i) {
+ const bucket = buckets[i];
+ const value = valueFor(bucket);
+ const barHeight = (value / maxValue) * chartHeight;
+ const x = padLeft + i * slotWidth + barGap / 2;
+ const y = padTop + chartHeight - barHeight;
+ const label = describeBucket(bucket);
+ const tooltip = `${label}\n${metricLabel}: ${formatValue(value)}\nAlloc count: ${formatNum(bucket.count)}\nTotal bytes: ${formatBytes(bucket.bytes)}`;
+ parts.push(
+ `<rect x="${x.toFixed(1)}" y="${y.toFixed(1)}" width="${barWidth.toFixed(1)}" height="${Math.max(0, barHeight).toFixed(1)}" class="memory-histogram-bar"><title>${escapeHtml(tooltip)}</title></rect>`
+ );
+ if (i % labelStride === 0 || i === bucketCount - 1) {
+ const tickX = padLeft + i * slotWidth + slotWidth / 2;
+ parts.push(
+ `<text x="${tickX.toFixed(1)}" y="${(padTop + chartHeight + 14).toFixed(1)}" text-anchor="middle" class="memory-chart-axis">${escapeHtml(formatBucketEdge(bucket))}</text>`
+ );
+ }
+ }
+
+ // Axis title for x.
+ parts.push(
+ `<text x="${(padLeft + chartWidth / 2).toFixed(1)}" y="${(height - 4).toFixed(1)}" text-anchor="middle" class="memory-chart-axis">Allocation size (power-of-two buckets)</text>`
+ );
+
+ const summaryTotalCount = Number((this.sizeHistogram && this.sizeHistogram.total_count) || 0);
+ const summaryTotalBytes = Number((this.sizeHistogram && this.sizeHistogram.total_bytes) || 0);
+ this.histogramMetaEl.textContent = `${formatNum(summaryTotalCount)} allocations, ${formatBytes(summaryTotalBytes)} total across ${bucketCount} bucket${bucketCount === 1 ? "" : "s"}`;
+
+ this.histogramEl.setAttribute("viewBox", `0 0 ${width} ${height}`);
+ this.histogramEl.setAttribute("preserveAspectRatio", "xMinYMin meet");
+ this.histogramEl.innerHTML = parts.join("");
+ }
+
+ getRowsForTable(name) {
+ if (name === "leaks") return this.leaks.slice(0, 100);
+ if (name === "churn") return this.churn.slice(0, 100);
+ return this.hot.slice(0, 100);
+ }
+
+ renderTableByName(name) {
+ const refs = this.panelRefs[name];
+ const state = this.tableState[name];
+ let rows = this.getRowsForTable(name).slice();
+ const filterText = state.filterText.trim().toLowerCase();
+ if (filterText) {
+ rows = rows.filter((row) => {
+ const haystack = [
+ row.summary,
+ row.top_frame,
+ row.secondary_frame,
+ row.group_key,
+ `callstack ${row.callstack_id}`,
+ ].join("\n").toLowerCase();
+ return haystack.includes(filterText);
+ });
+ }
+ rows.sort((a, b) => compareValues(a[state.sortKey], b[state.sortKey], state.desc));
+
+ if (!rows.length) {
+ refs.tbody.innerHTML = `<tr><td class="memory-empty">No data available.</td></tr>`;
+ return;
+ }
+
+ const parts = [];
+ let currentGroup = null;
+ for (let index = 0; index < rows.length; ++index) {
+ const row = rows[index];
+ const groupKey = state.groupMode === "top_frame" ? (row.top_frame || "(unknown)") :
+ (state.groupMode === "prefix" ? (row.group_key || row.top_frame || "(unknown)") : null);
+ if (groupKey !== null && groupKey !== currentGroup) {
+ currentGroup = groupKey;
+ parts.push(`<tr class="memory-group-row"><td colspan="5">${escapeHtml(groupKey)}</td></tr>`);
+ }
+ parts.push(this.renderDataRow(name, row, index));
+ }
+ refs.tbody.innerHTML = parts.join("");
+ for (const tr of refs.tbody.querySelectorAll("tr[data-callstack-id]")) {
+ tr.addEventListener("click", () => {
+ const callstackId = Number(tr.dataset.callstackId);
+ this.selectCallstack(callstackId);
+ for (const rowEl of this.container.querySelectorAll("tr[data-callstack-id]")) {
+ rowEl.classList.toggle("selected", Number(rowEl.dataset.callstackId) === callstackId);
+ }
+ });
+ }
+ }
+
+ renderDataRow(name, row, index) {
+ if (name === "leaks") {
+ return `<tr data-callstack-id="${row.callstack_id}">`
+ + `<td class="num">${index + 1}</td>`
+ + `<td class="num">${escapeHtml(formatBytes(row.live_bytes))}</td>`
+ + `<td class="num">${escapeHtml(formatNum(row.live_count))}</td>`
+ + `<td>${buildSummaryHtml(row)}</td>`
+ + `</tr>`;
+ }
+ if (name === "churn") {
+ return `<tr data-callstack-id="${row.callstack_id}">`
+ + `<td class="num">${index + 1}</td>`
+ + `<td class="num">${escapeHtml(formatNum(row.churn_allocs))}</td>`
+ + `<td class="num">${escapeHtml(formatBytes(row.churn_bytes))}</td>`
+ + `<td class="num">${escapeHtml(formatDistance(row.mean_distance))}</td>`
+ + `<td>${buildSummaryHtml(row)}</td>`
+ + `</tr>`;
+ }
+ return `<tr data-callstack-id="${row.callstack_id}">`
+ + `<td class="num">${index + 1}</td>`
+ + `<td class="num">${escapeHtml(formatNum(row.total_allocs))}</td>`
+ + `<td class="num">${escapeHtml(formatBytes(row.total_bytes))}</td>`
+ + `<td class="num">${escapeHtml(formatNum(row.churn_allocs))}</td>`
+ + `<td>${buildSummaryHtml(row)}</td>`
+ + `</tr>`;
+ }
+
+ async selectCallstack(callstackId) {
+ this.selectedCallstackId = callstackId;
+ this.callstackMetaEl.textContent = `Callstack ${callstackId}`;
+ this.callstackBodyEl.innerHTML = `<div class="memory-empty">Loading callstack ${callstackId}…</div>`;
+ try {
+ let callstack = this.callstackCache.get(callstackId);
+ if (!callstack) {
+ callstack = await getCallstack(callstackId);
+ this.callstackCache.set(callstackId, callstack);
+ }
+ const frames = callstack.frames || [];
+ if (!frames.length) {
+ this.callstackBodyEl.innerHTML = `<div class="memory-empty">No frames recorded for this callstack.</div>`;
+ return;
+ }
+ const notes = [];
+ if (callstack.hidden_prefix_count > 0) {
+ let note = `Skipped ${formatNum(callstack.hidden_prefix_count)} leading frame(s)`;
+ if (callstack.included_third_party_boundary) {
+ note += "; kept boundary third-party callsite";
+ }
+ notes.push(`<div class="memory-empty">${escapeHtml(note)}.</div>`);
+ }
+ const items = [];
+ for (let i = 0; i < frames.length; ++i) {
+ const frame = frames[i];
+ const display = frame.display || frame.address || "(unknown frame)";
+ const extra = frame.module_path ? ` <span class="memory-frame-path">${escapeHtml(trimPath(frame.module_path))}</span>` : "";
+ items.push(`<li><span class="memory-frame-index">#${frame.index ?? i}</span> <span class="memory-frame-display">${escapeHtml(display)}</span>${extra}</li>`);
+ }
+ this.callstackBodyEl.innerHTML = `${notes.join("")}<ol class="memory-callstack-list">${items.join("")}</ol>`;
+ } catch (e) {
+ this.callstackBodyEl.innerHTML = `<div class="memory-empty">Failed to load callstack ${callstackId}: ${escapeHtml(e.message)}</div>`;
+ }
+ }
+}
diff --git a/src/zen/frontend/html/stats.js b/src/zen/frontend/html/stats.js
new file mode 100644
index 000000000..741ad7ef9
--- /dev/null
+++ b/src/zen/frontend/html/stats.js
@@ -0,0 +1,95 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+// Sortable stats table view.
+
+const US_PER_MS = 1000;
+
+function escapeHtml(s) {
+ return String(s).replace(/[&<>"']/g, (c) => ({
+ "&": "&amp;",
+ "<": "&lt;",
+ ">": "&gt;",
+ "\"": "&quot;",
+ "'": "&#39;",
+ }[c]));
+}
+
+export class StatsView {
+ constructor(tbody, headerRow, model, onSelect) {
+ this.tbody = tbody;
+ this.headerRow = headerRow;
+ this.stats = model.scopeStats.slice();
+ this.onSelect = onSelect;
+ this.sortKey = "count";
+ this.sortAsc = false;
+ this.selectedName = null;
+
+ for (const th of headerRow.querySelectorAll("th[data-sort]")) {
+ th.addEventListener("click", () => this.handleSort(th.dataset.sort));
+ }
+ this.render();
+ }
+
+ handleSort(key) {
+ if (this.sortKey === key) {
+ this.sortAsc = !this.sortAsc;
+ } else {
+ this.sortKey = key;
+ this.sortAsc = key === "name";
+ }
+ this.render();
+ }
+
+ selectByName(name) {
+ this.selectedName = name;
+ for (const tr of this.tbody.querySelectorAll("tr")) {
+ tr.classList.toggle("selected", tr.dataset.name === name);
+ if (tr.dataset.name === name) {
+ tr.scrollIntoView({ block: "nearest" });
+ }
+ }
+ }
+
+ render() {
+ const key = this.sortKey;
+ const asc = this.sortAsc;
+ this.stats.sort((a, b) => {
+ const av = a[key];
+ const bv = b[key];
+ if (typeof av === "string") {
+ return asc ? av.localeCompare(bv) : bv.localeCompare(av);
+ }
+ return asc ? av - bv : bv - av;
+ });
+
+ for (const th of this.headerRow.querySelectorAll("th[data-sort]")) {
+ th.classList.toggle("sorted", th.dataset.sort === key);
+ th.classList.toggle("asc", th.dataset.sort === key && asc);
+ }
+
+ const rows = [];
+ for (const stat of this.stats) {
+ const selected = stat.name === this.selectedName ? " class=\"selected\"" : "";
+ rows.push(
+ `<tr data-name="${escapeHtml(stat.name)}"${selected}>` +
+ `<td>${escapeHtml(stat.name)}</td>` +
+ `<td class="num">${stat.count.toLocaleString()}</td>` +
+ `<td class="num">${(stat.min_us / US_PER_MS).toFixed(3)}</td>` +
+ `<td class="num">${(stat.mean_us / US_PER_MS).toFixed(3)}</td>` +
+ `<td class="num">${(stat.max_us / US_PER_MS).toFixed(3)}</td>` +
+ `<td class="num">${(stat.stdev_us / US_PER_MS).toFixed(3)}</td>` +
+ `</tr>`,
+ );
+ }
+ this.tbody.innerHTML = rows.join("");
+
+ for (const tr of this.tbody.querySelectorAll("tr")) {
+ tr.addEventListener("click", () => {
+ const name = tr.dataset.name;
+ this.selectByName(name);
+ if (this.onSelect) {
+ this.onSelect(name);
+ }
+ });
+ }
+ }
+}
diff --git a/src/zen/frontend/html/timeline.js b/src/zen/frontend/html/timeline.js
new file mode 100644
index 000000000..f463a8418
--- /dev/null
+++ b/src/zen/frontend/html/timeline.js
@@ -0,0 +1,973 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+// Canvas-drawn flame graph with per-thread swimlanes, pan+zoom, hover
+// tooltip, click-to-select and scope-name highlighting.
+
+const HEADER_H = 18; // thread name row height
+const DEPTH_H = 16; // scope lane row height
+const MAX_DRAWN_DEPTH = 32;
+const MIN_RECT_W = 1.5; // don't draw narrower than this (px)
+const RULER_H = 20;
+const THREAD_GAP = 6;
+const PADDING_X = 0;
+const REGION_LANE_H = 18; // region band row height
+const REGION_HEADER_H = 16; // category header row height
+const REGIONS_GAP = 6; // gap between the region rack and the first thread
+
+// Scope colors: golden-angle hue rotation keyed on NameId so the same scope
+// always renders in the same color across zoom levels.
+function scopeFillColor(nameId) {
+ const hue = ((nameId * 137.508) % 360 + 360) % 360;
+ return `hsl(${hue.toFixed(0)}, 55%, 42%)`;
+}
+
+function scopeHighlightColor(nameId) {
+ const hue = ((nameId * 137.508) % 360 + 360) % 360;
+ return `hsl(${hue.toFixed(0)}, 80%, 60%)`;
+}
+
+function stringHash(s) {
+ let h = 0;
+ for (let i = 0; i < s.length; i++) {
+ h = ((h << 5) - h + s.charCodeAt(i)) | 0;
+ }
+ return h >>> 0;
+}
+
+// Desaturated palette for regions so they don't compete visually with the
+// colourful CPU scopes below them.
+function regionFillColor(name) {
+ const hue = (stringHash(name || "region") * 2.3) % 360;
+ return `hsla(${hue.toFixed(0)}, 35%, 55%, 0.55)`;
+}
+
+function formatTime(us) {
+ if (us < 1000) {
+ return `${us} µs`;
+ }
+ if (us < 1_000_000) {
+ return `${(us / 1000).toFixed(3)} ms`;
+ }
+ return `${(us / 1_000_000).toFixed(3)} s`;
+}
+
+function formatRange(startUs, endUs) {
+ return `${formatTime(startUs)} → ${formatTime(endUs)} (${formatTime(endUs - startUs)})`;
+}
+
+export class Timeline {
+ constructor(opts) {
+ this.canvas = opts.canvas;
+ this.tooltip = opts.tooltip;
+ this.selectionEl = opts.selectionEl;
+ this.viewportInfoEl = opts.viewportInfoEl;
+ this.zoomResetBtn = opts.zoomResetBtn;
+ this.model = opts.model;
+ this.onScopeSelect = opts.onScopeSelect || (() => {});
+
+ this.ctx = this.canvas.getContext("2d");
+ this.dpr = Math.max(1, window.devicePixelRatio || 1);
+
+ this.bookmarks = (this.model.bookmarks || []).slice().sort((a, b) => a.time_us - b.time_us);
+ this.bookmarksVisible = true;
+ this.regionCategories = (this.model.regionCategories || []).filter(c => c.lane_count > 0);
+ // All categories enabled by default; renderRegionCategories() calls
+ // setEnabledRegionCategories() shortly after construction.
+ this.enabledRegionCategories = new Set(this.regionCategories.map((_, i) => i));
+ this.recomputeRegionsBlockH();
+
+ // Per-thread timelines keyed by threadId; each entry is an object
+ // { scopes, perDepth } where scopes is an array of tuples
+ // [beginUs, durationUs, nameId, depth, mergeCount?].
+ this.timelines = new Map();
+ // Set of threadIds the user wants visible.
+ this.enabledThreads = new Set();
+
+ // Viewport-driven fetch state.
+ this.lodEnabled = true; // when false, always request LOD 0 (raw)
+ this.fetchThrottled = false;
+ this.fetchPending = false;
+ this.fetchThrottleTimer = null;
+ this.abortControllers = new Map(); // threadId → AbortController
+ this.fetchSeq = new Map(); // threadId → monotonic fetch sequence id
+ this.cachedRanges = new Map(); // threadId → { startUs, endUs, resolution }
+ // Lookup helpers.
+ this.threadMeta = new Map(); // threadId → { name, sortHint, scopeCount }
+ for (const t of this.model.threads) {
+ this.threadMeta.set(t.thread_id, t);
+ }
+
+ // Viewport state — time units throughout are microseconds from trace start.
+ this.traceStart = 0;
+ this.traceEnd = Math.max(1, this.model.session.trace_end_us || 0);
+ if (this.traceEnd <= this.traceStart) {
+ this.traceEnd = this.traceStart + 1000;
+ }
+ this.startUs = this.traceStart;
+ const maxInitialUs = 60_000_000; // cap initial view to 60 seconds
+ this.endUs = (this.traceEnd - this.traceStart > maxInitialUs)
+ ? this.traceStart + maxInitialUs
+ : this.traceEnd;
+
+ // Vertical scroll offset in canvas pixels (0 = first thread flush
+ // against the ruler). Updated by both drag-pan and shift-wheel.
+ this.scrollY = 0;
+ this.maxScrollY = 0;
+
+ // Hit-test rects computed during the last draw.
+ this.hits = [];
+ this.selectedId = null;
+ this.highlightName = null;
+
+ // Pan state
+ this.panStartX = 0;
+ this.panStartY = 0;
+ this.panStartUs = 0;
+ this.panStartScrollY = 0;
+ this.panning = false;
+ this.panMoved = false;
+
+ this.resizeObserver = new ResizeObserver(() => this.requestDraw());
+ this.resizeObserver.observe(this.canvas);
+
+ this.canvas.addEventListener("mousedown", (e) => this.onMouseDown(e));
+ window.addEventListener("mousemove", (e) => this.onMouseMove(e));
+ window.addEventListener("mouseup", (e) => this.onMouseUp(e));
+ this.canvas.addEventListener("wheel", (e) => this.onWheel(e), { passive: false });
+ this.canvas.addEventListener("mouseleave", () => this.hideTooltip());
+ this.zoomResetBtn.addEventListener("click", () => this.resetView());
+
+ this.drawPending = false;
+ }
+
+ setBookmarksVisible(visible) {
+ this.bookmarksVisible = visible;
+ this.requestDraw();
+ }
+
+ setEnabledRegionCategories(indices) {
+ this.enabledRegionCategories = indices instanceof Set ? indices : new Set(indices);
+ this.recomputeRegionsBlockH();
+ this.requestDraw();
+ }
+
+ recomputeRegionsBlockH() {
+ this.regionsBlockH = 0;
+ for (let i = 0; i < this.regionCategories.length; i++) {
+ if (!this.enabledRegionCategories || !this.enabledRegionCategories.has(i)) continue;
+ const cat = this.regionCategories[i];
+ this.regionsBlockH += REGION_HEADER_H + cat.lane_count * REGION_LANE_H;
+ }
+ if (this.regionsBlockH > 0) {
+ this.regionsBlockH += REGIONS_GAP;
+ }
+ }
+
+ setLodEnabled(enabled) {
+ this.lodEnabled = enabled;
+ // Invalidate all caches so the next fetch uses the new setting.
+ this.cachedRanges.clear();
+ this.scheduleFetch();
+ }
+
+ setEnabledThreads(ids) {
+ this.enabledThreads = new Set(ids);
+ this.scheduleFetch();
+ this.requestDraw();
+ }
+
+ setHighlightName(name) {
+ this.highlightName = name || null;
+ this.requestDraw();
+ }
+
+ jumpToScopeName(name) {
+ // Find the first scope with the given name and frame it.
+ this.setHighlightName(name);
+ for (const threadId of this.enabledThreads) {
+ const timeline = this.timelines.get(threadId);
+ if (!timeline) continue;
+ for (const s of timeline.scopes) {
+ const nameId = s[2];
+ if (this.model.scopeNames[nameId] !== name) continue;
+ const beginUs = s[0];
+ const durationUs = s[1];
+ const pad = Math.max(durationUs * 3, 500);
+ this.startUs = Math.max(0, beginUs - pad);
+ this.endUs = beginUs + durationUs + pad;
+ this.selectScope({ threadId, tuple: s });
+ this.scheduleFetch();
+ this.requestDraw();
+ return;
+ }
+ }
+ }
+
+ resetView() {
+ this.startUs = this.traceStart;
+ this.endUs = this.traceEnd;
+ this.scrollY = 0;
+ this.scheduleFetch();
+ this.requestDraw();
+ }
+
+ // ── Viewport-driven fetch engine ──────────────────────────────────
+
+ computeResolution() {
+ const w = this.width || this.canvas.getBoundingClientRect().width || 1;
+ // The resolution tells the server the minimum renderable scope duration.
+ // A scope must be at least MIN_RECT_W pixels wide to be drawn, so the
+ // threshold is usPerPixel * MIN_RECT_W, not just usPerPixel. This
+ // selects a coarser LOD that merges across gaps smaller than one
+ // renderable unit, preventing empty holes in the timeline.
+ return Math.ceil((this.endUs - this.startUs) / w * MIN_RECT_W);
+ }
+
+ computeFetchWindow() {
+ const range = this.endUs - this.startUs;
+ const margin = range * 0.5;
+ return {
+ startUs: Math.max(0, Math.floor(this.startUs - margin)),
+ endUs: Math.ceil(this.endUs + margin),
+ };
+ }
+
+ // Map a resolution to the LOD index the server would select.
+ // Mirrors the server's selection: finest LOD where ResolutionUs >= res.
+ // Returns -1 for LOD 0 (raw), 0–4 for LOD 1–5.
+ lodForResolution(res) {
+ if (!this.lodEnabled || res <= 0) return -1;
+ const levels = [100, 1000, 8000, 40000, 200000];
+ for (let i = 0; i < levels.length; i++) {
+ if (levels[i] >= res) return i;
+ }
+ return levels.length - 1; // coarsest
+ }
+
+ needsRefetch(threadId) {
+ const cached = this.cachedRanges.get(threadId);
+ if (!cached) return true;
+ const currentRes = this.computeResolution();
+ // Re-fetch when the LOD level would change — this catches the exact
+ // boundary crossing and prevents jarring LOD transitions during pan.
+ if (this.lodForResolution(cached.resolution) !== this.lodForResolution(currentRes)) return true;
+ // Re-fetch when the viewport nears the edge of the cached range.
+ const margin = (cached.endUs - cached.startUs) * 0.25;
+ if (this.startUs < cached.startUs + margin) return true;
+ if (this.endUs > cached.endUs - margin) return true;
+ return false;
+ }
+
+ checkViewportFetch() {
+ for (const id of this.enabledThreads) {
+ if (this.needsRefetch(id)) {
+ this.scheduleFetch();
+ return;
+ }
+ }
+ }
+
+ scheduleFetch() {
+ // Leading+trailing throttle: fires immediately on the first call,
+ // then suppresses further calls for 150ms. If any calls arrived
+ // during the suppression window, one trailing fetch fires at the end.
+ // This keeps data flowing during continuous pan/zoom without flooding.
+ this.fetchPending = true;
+ if (this.fetchThrottled) return;
+ this.fetchThrottled = true;
+ this.fetchPending = false;
+ this.fetchViewport();
+ this.fetchThrottleTimer = setTimeout(() => {
+ this.fetchThrottled = false;
+ if (this.fetchPending) {
+ this.fetchPending = false;
+ this.scheduleFetch();
+ }
+ }, 150);
+ }
+
+ async fetchViewport() {
+ const { startUs, endUs } = this.computeFetchWindow();
+ const currentRes = this.lodEnabled ? this.computeResolution() : 0;
+
+ const threadIds = [];
+ let resolution = currentRes;
+ for (const threadId of this.enabledThreads) {
+ if (!this.needsRefetch(threadId)) continue;
+
+ // If the LOD level hasn't changed, reuse the cached resolution so
+ // the server selects the same LOD. This prevents a pan-triggered
+ // refetch from accidentally switching LOD levels due to minor
+ // resolution drift within the same LOD band.
+ const cached = this.cachedRanges.get(threadId);
+ if (cached && currentRes > 0 &&
+ this.lodForResolution(cached.resolution) === this.lodForResolution(currentRes)) {
+ resolution = cached.resolution;
+ }
+
+ threadIds.push(threadId);
+ }
+ if (threadIds.length === 0) return;
+
+ // Cancel any in-flight batch request.
+ if (this.batchAbort) this.batchAbort.abort();
+ const controller = new AbortController();
+ this.batchAbort = controller;
+
+ const seq = (this.batchSeq || 0) + 1;
+ this.batchSeq = seq;
+
+ try {
+ const { getTimelineBatch } = await import("./api.js");
+ const result = await getTimelineBatch(threadIds, startUs, endUs, 0, resolution, { signal: controller.signal });
+ // Discard stale responses.
+ if (this.batchSeq !== seq) return;
+
+ for (const threadId of threadIds) {
+ const entry = result[String(threadId)];
+ const scopes = entry ? (entry.scopes || []) : [];
+
+ const perDepth = [];
+ for (let i = 0; i < scopes.length; i++) {
+ const d = scopes[i][3];
+ while (perDepth.length <= d) perDepth.push([]);
+ perDepth[d].push(i);
+ }
+
+ this.timelines.set(threadId, { scopes, perDepth });
+ this.cachedRanges.set(threadId, { startUs, endUs, resolution });
+ }
+
+ this.batchAbort = null;
+ this.requestDraw();
+ } catch (e) {
+ if (e.name === "AbortError") return;
+ console.error(`failed to load timeline batch: ${e.message}`);
+ this.batchAbort = null;
+ }
+ }
+
+ requestDraw() {
+ if (this.drawPending) return;
+ this.drawPending = true;
+ requestAnimationFrame(() => {
+ this.drawPending = false;
+ this.draw();
+ });
+ }
+
+ resizeBackingStore() {
+ const rect = this.canvas.getBoundingClientRect();
+ this.width = Math.floor(rect.width);
+ this.height = Math.floor(rect.height);
+ const bw = Math.floor(rect.width * this.dpr);
+ const bh = Math.floor(rect.height * this.dpr);
+ if (this.canvas.width !== bw || this.canvas.height !== bh) {
+ this.canvas.width = bw;
+ this.canvas.height = bh;
+ }
+ this.ctx.setTransform(this.dpr, 0, 0, this.dpr, 0, 0);
+ }
+
+ pxPerUs() {
+ return (this.width - PADDING_X * 2) / Math.max(1, this.endUs - this.startUs);
+ }
+
+ usAtX(x) {
+ return this.startUs + (x - PADDING_X) / this.pxPerUs();
+ }
+
+ xAtUs(us) {
+ return PADDING_X + (us - this.startUs) * this.pxPerUs();
+ }
+
+ layoutThreads() {
+ // Returns { rows: [{threadId, y, maxDepth}], totalH }. The y values
+ // are in canvas coordinates with the current scrollY already applied.
+ const rows = [];
+ let y = RULER_H + this.regionsBlockH - this.scrollY;
+ const sorted = Array.from(this.enabledThreads)
+ .filter((id) => this.threadMeta.has(id))
+ .sort((a, b) => {
+ const ma = this.threadMeta.get(a);
+ const mb = this.threadMeta.get(b);
+ // OS threads first, then lanes
+ if (ma.is_lane !== mb.is_lane) return ma.is_lane ? 1 : -1;
+ // Group by group name (ungrouped first)
+ const ga = ma.group || "";
+ const gb = mb.group || "";
+ if (ga !== gb) {
+ if (!ga) return -1;
+ if (!gb) return 1;
+ return ga.localeCompare(gb, undefined, { numeric: true });
+ }
+ // Match sidebar sort: sort_hint → scopes-first → thread_id → name
+ if (ma.sort_hint !== mb.sort_hint) return ma.sort_hint - mb.sort_hint;
+ if ((ma.scope_count > 0) !== (mb.scope_count > 0)) return mb.scope_count - ma.scope_count;
+ if (ma.thread_id !== mb.thread_id) return ma.thread_id - mb.thread_id;
+ return (ma.name || "").localeCompare(mb.name || "", undefined, { numeric: true });
+ });
+ for (const threadId of sorted) {
+ const timeline = this.timelines.get(threadId);
+ const scopes = timeline ? timeline.scopes : [];
+ let maxDepth = 0;
+ for (const s of scopes) {
+ if (s[3] > maxDepth) maxDepth = s[3];
+ }
+ if (maxDepth > MAX_DRAWN_DEPTH) maxDepth = MAX_DRAWN_DEPTH;
+ const rowH = HEADER_H + (maxDepth + 1) * DEPTH_H;
+ rows.push({ threadId, y, headerH: HEADER_H, maxDepth, height: rowH });
+ y += rowH + THREAD_GAP;
+ }
+ // y now points at the bottom of the last row in scrolled coords.
+ // Recover the unscrolled total content height for scroll clamping.
+ const totalContentH = y + this.scrollY;
+ return { rows, totalH: totalContentH };
+ }
+
+ draw() {
+ this.resizeBackingStore();
+ const ctx = this.ctx;
+ const W = this.width;
+ const H = this.height;
+
+ ctx.fillStyle = getComputedStyle(document.body).getPropertyValue("--bg0") || "#0d1117";
+ ctx.fillRect(0, 0, W, H);
+
+ this.hits = [];
+
+ // First pass: lay out threads to discover total content height and
+ // clamp scrollY. We may re-layout after clamping so coordinates
+ // are accurate for the real draw.
+ let layout = this.layoutThreads();
+ const visibleH = Math.max(0, H - RULER_H);
+ this.maxScrollY = Math.max(0, layout.totalH - RULER_H - visibleH);
+ if (this.scrollY > this.maxScrollY)
+ {
+ this.scrollY = this.maxScrollY;
+ layout = this.layoutThreads();
+ }
+ if (this.scrollY < 0)
+ {
+ this.scrollY = 0;
+ layout = this.layoutThreads();
+ }
+ const { rows } = layout;
+
+ // Clip thread rendering to below the ruler strip so scrolled-up
+ // content never bleeds over it. The ruler is drawn after restoring.
+ ctx.save();
+ ctx.beginPath();
+ ctx.rect(0, RULER_H, W, H - RULER_H);
+ ctx.clip();
+
+ this.drawRegions(ctx, W);
+
+ const pxPerUs = this.pxPerUs();
+ const bg1 = getComputedStyle(document.body).getPropertyValue("--bg1") || "#161b22";
+ const fg1 = getComputedStyle(document.body).getPropertyValue("--fg1") || "#c9d1d9";
+ const fg2 = getComputedStyle(document.body).getPropertyValue("--fg2") || "#8b949e";
+
+ for (const row of rows) {
+ if (row.y > H) break;
+ if (row.y + row.height < RULER_H) continue;
+
+ // Thread header strip
+ const meta = this.threadMeta.get(row.threadId);
+ const isLane = meta && meta.is_lane;
+ ctx.fillStyle = isLane ? "rgba(130, 80, 220, 0.12)" : bg1;
+ ctx.fillRect(0, row.y, W, row.headerH);
+ const prefix = isLane ? "⬦ " : "";
+ const label = `${prefix}${(meta && meta.name) || `tid ${row.threadId}`} · ${row.threadId}`;
+ ctx.fillStyle = isLane ? "rgba(180, 140, 255, 0.8)" : fg2;
+ ctx.font = "11px -apple-system, Segoe UI, sans-serif";
+ ctx.textBaseline = "middle";
+ ctx.fillText(label, 6, row.y + row.headerH / 2);
+
+ // Swimlane backgrounds
+ for (let d = 0; d <= row.maxDepth; d++) {
+ ctx.fillStyle = d % 2 === 0 ? "rgba(255,255,255,0.015)" : "rgba(255,255,255,0.00)";
+ ctx.fillRect(0, row.y + row.headerH + d * DEPTH_H, W, DEPTH_H);
+ }
+
+ const timeline = this.timelines.get(row.threadId);
+ if (timeline) {
+ this.drawScopes(ctx, timeline, row, pxPerUs, fg1);
+ }
+ }
+
+ this.drawSelectionOutline(ctx);
+
+ ctx.restore();
+
+ // Ruler is drawn last so it always overlays the thread region
+ // regardless of how far the content has scrolled.
+ this.drawRuler(ctx, W);
+
+ // Bookmark lines span the whole content area, drawn after the ruler
+ // so the little diamond markers sit inside the ruler strip.
+ this.drawBookmarks(ctx, W, H);
+
+ this.drawViewportInfo();
+ }
+
+ drawRuler(ctx, W) {
+ const bg1 = getComputedStyle(document.body).getPropertyValue("--bg1") || "#161b22";
+ const fg2 = getComputedStyle(document.body).getPropertyValue("--fg2") || "#8b949e";
+ const border = getComputedStyle(document.body).getPropertyValue("--border") || "#30363d";
+
+ ctx.fillStyle = bg1;
+ ctx.fillRect(0, 0, W, RULER_H);
+ ctx.strokeStyle = border;
+ ctx.lineWidth = 1;
+ ctx.beginPath();
+ ctx.moveTo(0, RULER_H - 0.5);
+ ctx.lineTo(W, RULER_H - 0.5);
+ ctx.stroke();
+
+ // Pick a tick interval that yields 6–12 ticks across the visible range.
+ const rangeUs = this.endUs - this.startUs;
+ const targetTicks = Math.max(4, Math.min(12, Math.floor(W / 100)));
+ const roughInterval = rangeUs / targetTicks;
+ const pow10 = Math.pow(10, Math.floor(Math.log10(roughInterval)));
+ let interval = pow10;
+ if (roughInterval / pow10 > 5) interval = 10 * pow10;
+ else if (roughInterval / pow10 > 2) interval = 5 * pow10;
+ else if (roughInterval / pow10 > 1) interval = 2 * pow10;
+
+ ctx.fillStyle = fg2;
+ ctx.font = "10px -apple-system, Segoe UI, sans-serif";
+ ctx.textBaseline = "middle";
+ ctx.textAlign = "left";
+
+ const firstTick = Math.ceil(this.startUs / interval) * interval;
+ for (let t = firstTick; t <= this.endUs; t += interval) {
+ const x = this.xAtUs(t);
+ ctx.strokeStyle = border;
+ ctx.beginPath();
+ ctx.moveTo(x + 0.5, 0);
+ ctx.lineTo(x + 0.5, RULER_H);
+ ctx.stroke();
+ ctx.fillText(formatTime(t), x + 4, RULER_H / 2);
+ }
+ }
+
+ drawRegions(ctx, W) {
+ if (this.regionCategories.length === 0) return;
+
+ const startUs = this.startUs;
+ const endUs = this.endUs;
+ const pxPerUs = this.pxPerUs();
+ const fg2 = getComputedStyle(document.body).getPropertyValue("--fg2") || "#8b949e";
+ const bg1 = getComputedStyle(document.body).getPropertyValue("--bg1") || "#161b22";
+ let catY = RULER_H - this.scrollY;
+
+ for (let ci = 0; ci < this.regionCategories.length; ci++) {
+ if (!this.enabledRegionCategories.has(ci)) continue;
+ const cat = this.regionCategories[ci];
+ // Category header
+ ctx.fillStyle = bg1;
+ ctx.fillRect(0, catY, W, REGION_HEADER_H);
+ ctx.fillStyle = fg2;
+ ctx.font = "10px -apple-system, Segoe UI, sans-serif";
+ ctx.textBaseline = "middle";
+ ctx.textAlign = "left";
+ ctx.fillText(`Timing Regions \u2013 ${cat.name}`, 6, catY + REGION_HEADER_H / 2);
+ catY += REGION_HEADER_H;
+
+ // Region bands for this category
+ for (const r of cat.regions) {
+ const beginUs = r.begin_us;
+ const endRegUs = r.end_us;
+ if (endRegUs < startUs) continue;
+ if (beginUs > endUs) continue;
+
+ const x = this.xAtUs(beginUs);
+ const w = Math.max(MIN_RECT_W, (endRegUs - beginUs) * pxPerUs);
+ if (w < MIN_RECT_W) continue;
+
+ const y = catY + r.depth * REGION_LANE_H;
+
+ ctx.fillStyle = regionFillColor(r.name);
+ ctx.fillRect(x, y + 1, w, REGION_LANE_H - 2);
+
+ ctx.strokeStyle = "rgba(255,255,255,0.2)";
+ ctx.lineWidth = 1;
+ ctx.strokeRect(x + 0.5, y + 1.5, w - 1, REGION_LANE_H - 3);
+
+ const visX = Math.max(x, 0);
+ const visRight = Math.min(x + w, this.width);
+ const visW = visRight - visX;
+ if (visW > 24 && r.name) {
+ ctx.fillStyle = "rgba(255,255,255,0.95)";
+ ctx.font = "11px -apple-system, Segoe UI, sans-serif";
+ ctx.textBaseline = "middle";
+ ctx.textAlign = "left";
+ ctx.save();
+ ctx.beginPath();
+ ctx.rect(visX + 3, y, visW - 6, REGION_LANE_H);
+ ctx.clip();
+ ctx.fillText(r.name, visX + 5, y + REGION_LANE_H / 2);
+ ctx.restore();
+ }
+
+ this.hits.push({ x, y, w, h: REGION_LANE_H - 2, region: r, regionCategory: cat.name });
+ }
+
+ catY += cat.lane_count * REGION_LANE_H;
+ }
+ }
+
+ drawBookmarks(ctx, W, H) {
+ if (!this.bookmarksVisible || !this.bookmarks || this.bookmarks.length === 0) return;
+
+ const startUs = this.startUs;
+ const endUs = this.endUs;
+
+ ctx.save();
+ ctx.strokeStyle = "rgba(227, 179, 65, 0.85)";
+ ctx.fillStyle = "rgba(227, 179, 65, 0.95)";
+ ctx.lineWidth = 1;
+
+ for (const b of this.bookmarks) {
+ if (b.time_us < startUs) continue;
+ if (b.time_us > endUs) break;
+
+ const x = this.xAtUs(b.time_us);
+ if (x < -2 || x > W + 2) continue;
+
+ // Dashed vertical line spanning the whole content area.
+ ctx.setLineDash([3, 3]);
+ ctx.beginPath();
+ ctx.moveTo(x + 0.5, RULER_H);
+ ctx.lineTo(x + 0.5, H);
+ ctx.stroke();
+ ctx.setLineDash([]);
+
+ // Diamond marker inside the ruler strip.
+ const cy = RULER_H - 6;
+ ctx.beginPath();
+ ctx.moveTo(x, cy - 4);
+ ctx.lineTo(x + 4, cy);
+ ctx.lineTo(x, cy + 4);
+ ctx.lineTo(x - 4, cy);
+ ctx.closePath();
+ ctx.fill();
+
+ this.hits.push({ x: x - 4, y: 0, w: 9, h: H, bookmark: b });
+ }
+ ctx.restore();
+ }
+
+ drawScopes(ctx, timeline, row, pxPerUs, textColor) {
+ const { scopes, perDepth } = timeline;
+ const startUs = this.startUs;
+ const endUs = this.endUs;
+
+ const highlightNameId = this.highlightName
+ ? this.model.scopeNameIds.get(this.highlightName)
+ : undefined;
+
+ ctx.textBaseline = "middle";
+ ctx.textAlign = "left";
+ ctx.font = "11px -apple-system, Segoe UI, sans-serif";
+
+ const rowTop = row.y + row.headerH;
+ const maxDepth = Math.min(row.maxDepth, perDepth.length - 1);
+
+ for (let depth = 0; depth <= maxDepth; depth++) {
+ const indices = perDepth[depth];
+ if (!indices || indices.length === 0) continue;
+
+ // Sibling scopes at the same depth never overlap, so their end
+ // times are monotonic in begin order — a standard lower_bound
+ // on (end >= startUs) correctly finds the first visible scope,
+ // including outer-depth scopes whose begin is far before
+ // the viewport start.
+ let lo = 0;
+ let hi = indices.length;
+ while (lo < hi) {
+ const mid = (lo + hi) >>> 1;
+ const s = scopes[indices[mid]];
+ if (s[0] + s[1] < startUs) {
+ lo = mid + 1;
+ } else {
+ hi = mid;
+ }
+ }
+
+ const y = rowTop + depth * DEPTH_H;
+ let rendered = 0;
+ for (let j = lo; j < indices.length; j++) {
+ const s = scopes[indices[j]];
+ if (s[0] > endUs) break;
+
+ const beginUs = s[0];
+ const durationUs = s[1];
+ const nameId = s[2];
+ const mergeCount = s[4] || 0;
+
+ ++rendered;
+ const x = this.xAtUs(beginUs);
+ const w = Math.max(MIN_RECT_W, durationUs * pxPerUs);
+
+ const hue = ((nameId * 137.508) % 360 + 360) % 360;
+ const isHighlighted = highlightNameId !== undefined && nameId === highlightNameId;
+
+ if (mergeCount > 1) {
+ // Merged scope — desaturated fill with dashed top indicator.
+ ctx.fillStyle = isHighlighted
+ ? `hsl(${hue.toFixed(0)}, 50%, 50%)`
+ : `hsl(${hue.toFixed(0)}, 30%, 35%)`;
+ ctx.fillRect(x, y + 1, w, DEPTH_H - 2);
+ ctx.strokeStyle = "rgba(255,255,255,0.25)";
+ ctx.lineWidth = 1;
+ ctx.setLineDash([2, 2]);
+ ctx.beginPath();
+ ctx.moveTo(x, y + 1.5);
+ ctx.lineTo(x + w, y + 1.5);
+ ctx.stroke();
+ ctx.setLineDash([]);
+ } else {
+ ctx.fillStyle = isHighlighted ? scopeHighlightColor(nameId) : scopeFillColor(nameId);
+ ctx.fillRect(x, y + 1, w, DEPTH_H - 2);
+ }
+
+ // Draw the label pinned to the visible portion of the rect
+ // so zooming into a long scope still shows its name.
+ const visX = Math.max(x, 0);
+ const visRight = Math.min(x + w, this.width);
+ const visW = visRight - visX;
+ if (visW > 30) {
+ const name = this.model.scopeNames[nameId] || "?";
+ const maxChars = Math.floor((visW - 6) / 6);
+ const shown = name.length > maxChars ? name.slice(0, Math.max(0, maxChars - 1)) + "…" : name;
+ ctx.fillStyle = "rgba(255,255,255,0.95)";
+ ctx.save();
+ ctx.beginPath();
+ ctx.rect(visX + 3, y, visW - 6, DEPTH_H);
+ ctx.clip();
+ ctx.fillText(shown, visX + 4, y + DEPTH_H / 2);
+ ctx.restore();
+ }
+
+ this.hits.push({ x, y, w, h: DEPTH_H - 2, threadId: row.threadId, tuple: s });
+ }
+ }
+ }
+
+ drawSelectionOutline(ctx) {
+ if (!this.selected || !this.selected.tuple) return;
+ const s = this.selected.tuple;
+ const beginUs = s[0];
+ const durationUs = s[1];
+ const depth = s[3];
+ const { rows } = this.layoutThreads();
+ const row = rows.find((r) => r.threadId === this.selected.threadId);
+ if (!row) return;
+ const x = this.xAtUs(beginUs);
+ const w = Math.max(MIN_RECT_W, durationUs * this.pxPerUs());
+ const y = row.y + row.headerH + depth * DEPTH_H;
+ ctx.strokeStyle = "#ffffff";
+ ctx.lineWidth = 1.5;
+ ctx.strokeRect(x - 0.5, y + 0.5, w + 1, DEPTH_H - 1);
+ }
+
+ drawViewportInfo() {
+ const text = `${formatRange(this.startUs, this.endUs)} · ${(this.pxPerUs() * 1000).toFixed(2)} px/ms`;
+ this.viewportInfoEl.textContent = text;
+ }
+
+ hitTest(clientX, clientY) {
+ const rect = this.canvas.getBoundingClientRect();
+ const x = clientX - rect.left;
+ const y = clientY - rect.top;
+ for (let i = this.hits.length - 1; i >= 0; i--) {
+ const h = this.hits[i];
+ if (x >= h.x && x < h.x + h.w && y >= h.y && y < h.y + h.h) {
+ return h;
+ }
+ }
+ return null;
+ }
+
+ onMouseDown(e) {
+ if (e.button !== 0) return;
+ this.panning = true;
+ this.panMoved = false;
+ this.panStartX = e.clientX;
+ this.panStartY = e.clientY;
+ this.panStartUs = this.startUs;
+ this.panStartScrollY = this.scrollY;
+ this.panRangeUs = this.endUs - this.startUs;
+ }
+
+ onMouseMove(e) {
+ if (this.panning) {
+ const dx = e.clientX - this.panStartX;
+ const dy = e.clientY - this.panStartY;
+ if (Math.abs(dx) > 2 || Math.abs(dy) > 2) this.panMoved = true;
+ const deltaUs = -dx / this.pxPerUs();
+ this.startUs = this.panStartUs + deltaUs;
+ this.endUs = this.startUs + this.panRangeUs;
+ this.scrollY = this.panStartScrollY - dy;
+ this.checkViewportFetch();
+ this.requestDraw();
+ this.hideTooltip();
+ return;
+ }
+ const hit = this.hitTest(e.clientX, e.clientY);
+ if (hit) {
+ this.showTooltip(hit, e.clientX, e.clientY);
+ } else {
+ this.hideTooltip();
+ }
+ }
+
+ onMouseUp(e) {
+ if (!this.panning) return;
+ this.panning = false;
+ if (!this.panMoved) {
+ const hit = this.hitTest(e.clientX, e.clientY);
+ if (hit) {
+ this.selectScope(hit);
+ }
+ }
+ }
+
+ onWheel(e) {
+ e.preventDefault();
+
+ // Shift+wheel (or horizontal wheel delta from a trackpad) scrolls
+ // vertically without changing the zoom.
+ if (e.shiftKey || Math.abs(e.deltaX) > Math.abs(e.deltaY)) {
+ const step = e.deltaX !== 0 ? e.deltaX : e.deltaY;
+ this.scrollY += step;
+ this.requestDraw();
+ return;
+ }
+
+ const rect = this.canvas.getBoundingClientRect();
+ const cursorX = e.clientX - rect.left;
+ const cursorUs = this.usAtX(cursorX);
+ const factor = e.deltaY > 0 ? 1.25 : 0.8;
+ const newRange = Math.max(10, (this.endUs - this.startUs) * factor);
+ this.startUs = cursorUs - (cursorX - PADDING_X) * (newRange / (this.width - PADDING_X * 2));
+ this.endUs = this.startUs + newRange;
+ this.checkViewportFetch();
+ this.requestDraw();
+ }
+
+ showTooltip(hit, clientX, clientY) {
+ let title = "";
+ let meta = "";
+
+ if (hit.bookmark) {
+ const b = hit.bookmark;
+ title = b.text || "(unnamed bookmark)";
+ const loc = b.file ? `${b.file.split(/[\\/]/).pop()}:${b.line}` : "";
+ meta = `bookmark · ${formatTime(b.time_us)}${loc ? " · " + loc : ""}`;
+ }
+ else if (hit.region) {
+ const r = hit.region;
+ const dur = r.end_us - r.begin_us;
+ title = r.name || "(unnamed region)";
+ meta = `region · ${formatTime(dur)} · start ${formatTime(r.begin_us)}${hit.regionCategory ? " · " + hit.regionCategory : ""}`;
+ }
+ else {
+ const s = hit.tuple;
+ title = this.model.scopeNames[s[2]] || "?";
+ const tm = this.threadMeta.get(hit.threadId);
+ const threadName = (tm && tm.name) || `tid ${hit.threadId}`;
+ meta = `${formatTime(s[1])} · depth ${s[3]} · ${threadName} · start ${formatTime(s[0])}`;
+ if (s[4] > 1) {
+ meta += ` · ${s[4]} merged`;
+ }
+ }
+
+ this.tooltip.innerHTML =
+ `<div class="tt-name"></div>` +
+ `<div class="tt-meta"></div>`;
+ this.tooltip.querySelector(".tt-name").textContent = title;
+ this.tooltip.querySelector(".tt-meta").textContent = meta;
+
+ const rect = this.canvas.getBoundingClientRect();
+ const tx = clientX - rect.left + 12;
+ const ty = clientY - rect.top + 12;
+ this.tooltip.style.left = `${tx}px`;
+ this.tooltip.style.top = `${ty}px`;
+ this.tooltip.hidden = false;
+ }
+
+ hideTooltip() {
+ this.tooltip.hidden = true;
+ }
+
+ selectScope(hit) {
+ this.selected = hit;
+
+ if (hit.bookmark) {
+ const b = hit.bookmark;
+ this.selectionEl.innerHTML =
+ `<div class="selection-title"></div>` +
+ `<div class="selection-meta">` +
+ `<div><span class="k">Kind:</span> <span class="v">bookmark</span></div>` +
+ `<div><span class="k">Time:</span> <span class="v" data-k="time"></span></div>` +
+ `<div><span class="k">Source:</span> <span class="v" data-k="src"></span></div>` +
+ `</div>`;
+ this.selectionEl.querySelector(".selection-title").textContent = b.text || "(unnamed bookmark)";
+ this.selectionEl.querySelector("[data-k=time]").textContent = formatTime(b.time_us);
+ this.selectionEl.querySelector("[data-k=src]").textContent = b.file ? `${b.file}:${b.line}` : "";
+ this.requestDraw();
+ return;
+ }
+
+ if (hit.region) {
+ const r = hit.region;
+ this.selectionEl.innerHTML =
+ `<div class="selection-title"></div>` +
+ `<div class="selection-meta">` +
+ `<div><span class="k">Kind:</span> <span class="v">region</span></div>` +
+ `<div><span class="k">Duration:</span> <span class="v" data-k="dur"></span></div>` +
+ `<div><span class="k">Begin:</span> <span class="v" data-k="begin"></span></div>` +
+ `<div><span class="k">End:</span> <span class="v" data-k="end"></span></div>` +
+ `<div><span class="k">Category:</span> <span class="v" data-k="cat"></span></div>` +
+ `</div>`;
+ this.selectionEl.querySelector(".selection-title").textContent = r.name || "(unnamed region)";
+ this.selectionEl.querySelector("[data-k=dur]").textContent = formatTime(r.end_us - r.begin_us);
+ this.selectionEl.querySelector("[data-k=begin]").textContent = formatTime(r.begin_us);
+ this.selectionEl.querySelector("[data-k=end]").textContent = formatTime(r.end_us);
+ this.selectionEl.querySelector("[data-k=cat]").textContent = hit.regionCategory || "\u2014";
+ this.requestDraw();
+ return;
+ }
+
+ const s = hit.tuple;
+ const name = this.model.scopeNames[s[2]] || "?";
+ const meta = this.threadMeta.get(hit.threadId);
+ const threadName = (meta && meta.name) || `tid ${hit.threadId}`;
+ const mergedRow = s[4] > 1
+ ? `<div><span class="k">Merged:</span> <span class="v" data-k="merged"></span></div>`
+ : "";
+ this.selectionEl.innerHTML =
+ `<div class="selection-title"></div>` +
+ `<div class="selection-meta">` +
+ `<div><span class="k">Duration:</span> <span class="v" data-k="dur"></span></div>` +
+ `<div><span class="k">Begin:</span> <span class="v" data-k="begin"></span></div>` +
+ `<div><span class="k">End:</span> <span class="v" data-k="end"></span></div>` +
+ `<div><span class="k">Depth:</span> <span class="v" data-k="depth"></span></div>` +
+ `<div><span class="k">Thread:</span> <span class="v" data-k="thread"></span></div>` +
+ mergedRow +
+ `</div>`;
+ this.selectionEl.querySelector(".selection-title").textContent = name;
+ this.selectionEl.querySelector("[data-k=dur]").textContent = formatTime(s[1]);
+ this.selectionEl.querySelector("[data-k=begin]").textContent = formatTime(s[0]);
+ this.selectionEl.querySelector("[data-k=end]").textContent = formatTime(s[0] + s[1]);
+ this.selectionEl.querySelector("[data-k=depth]").textContent = String(s[3]);
+ this.selectionEl.querySelector("[data-k=thread]").textContent = `${threadName} (${hit.threadId})`;
+ if (s[4] > 1) {
+ this.selectionEl.querySelector("[data-k=merged]").textContent = `${s[4]} scopes`;
+ }
+ this.requestDraw();
+ this.onScopeSelect(name);
+ }
+}
diff --git a/src/zen/frontend/html/trace.css b/src/zen/frontend/html/trace.css
new file mode 100644
index 000000000..2ff324019
--- /dev/null
+++ b/src/zen/frontend/html/trace.css
@@ -0,0 +1,1312 @@
+/* Copyright Epic Games, Inc. All Rights Reserved. */
+
+:root,
+:root[data-theme="dark"] {
+ --bg0: #0d1117;
+ --bg1: #161b22;
+ --bg2: #1c2128;
+ --bg3: #21262d;
+ --border: #30363d;
+ --border-soft: #21262d;
+ --fg0: #f0f6fc;
+ --fg1: #c9d1d9;
+ --fg2: #8b949e;
+ --accent: #58a6ff;
+ --accent-soft: #1c2128;
+ --warn: #d29922;
+ --ok: #3fb950;
+ --fail: #f85149;
+ --highlight: #e3b34166;
+}
+
+@media (prefers-color-scheme: light) {
+ :root:not([data-theme]),
+ :root[data-theme="system"] {
+ --bg0: #ffffff;
+ --bg1: #f6f8fa;
+ --bg2: #ffffff;
+ --bg3: #eaeef2;
+ --border: #d0d7de;
+ --border-soft: #d8dee4;
+ --fg0: #1f2328;
+ --fg1: #24292f;
+ --fg2: #656d76;
+ --accent: #0969da;
+ --accent-soft: #ddf4ff;
+ --warn: #9a6700;
+ --ok: #1a7f37;
+ --fail: #cf222e;
+ --highlight: #b8860b44;
+ }
+}
+
+:root[data-theme="light"] {
+ --bg0: #ffffff;
+ --bg1: #f6f8fa;
+ --bg2: #ffffff;
+ --bg3: #eaeef2;
+ --border: #d0d7de;
+ --border-soft: #d8dee4;
+ --fg0: #1f2328;
+ --fg1: #24292f;
+ --fg2: #656d76;
+ --accent: #0969da;
+ --accent-soft: #ddf4ff;
+ --warn: #9a6700;
+ --ok: #1a7f37;
+ --fail: #cf222e;
+ --highlight: #b8860b44;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+html, body {
+ margin: 0;
+ padding: 0;
+ height: 100%;
+ background: var(--bg0);
+ color: var(--fg1);
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
+ font-size: 13px;
+ overflow: hidden;
+}
+
+body {
+ display: flex;
+ flex-direction: column;
+}
+
+pre, code, .mono {
+ font-family: 'SF Mono', 'Cascadia Mono', Consolas, 'DejaVu Sans Mono', monospace;
+ font-size: 12px;
+}
+
+/* -- header ---------------------------------------------------------------- */
+
+.header {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ padding: 10px 16px;
+ background: var(--bg1);
+ border-bottom: 1px solid var(--border);
+ flex-shrink: 0;
+}
+
+.header-title {
+ font-weight: 600;
+ color: var(--fg0);
+ font-size: 14px;
+}
+
+.header-file {
+ color: var(--fg2);
+ font-family: 'SF Mono', 'Cascadia Mono', Consolas, monospace;
+ font-size: 12px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ flex: 1;
+ min-width: 0;
+}
+
+.header-stats {
+ color: var(--fg2);
+ font-size: 12px;
+ display: flex;
+ gap: 16px;
+}
+
+.header-stats .k {
+ color: var(--fg2);
+ margin-right: 4px;
+}
+
+.header-stats .v {
+ color: var(--fg0);
+ font-weight: 500;
+}
+
+.header-btn {
+ background: var(--bg2);
+ color: var(--fg1);
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ padding: 6px 10px;
+ font-size: 12px;
+ cursor: pointer;
+ flex-shrink: 0;
+}
+
+.header-btn:hover {
+ background: var(--bg3);
+ color: var(--fg0);
+}
+
+/* -- layout ---------------------------------------------------------------- */
+
+.layout {
+ display: flex;
+ flex: 1;
+ min-height: 0;
+}
+
+.sidebar {
+ width: 260px;
+ flex-shrink: 0;
+ background: var(--bg1);
+ border-right: 1px solid var(--border);
+ display: flex;
+ flex-direction: column;
+ min-height: 0;
+}
+
+.content {
+ flex: 1;
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ background: var(--bg0);
+}
+
+/* -- tabs ------------------------------------------------------------------ */
+
+.tabs {
+ display: flex;
+ border-bottom: 1px solid var(--border);
+ flex-shrink: 0;
+}
+
+.tab {
+ flex: 1;
+ padding: 10px 8px;
+ background: transparent;
+ border: none;
+ border-bottom: 2px solid transparent;
+ color: var(--fg2);
+ font-size: 12px;
+ font-weight: 500;
+ cursor: pointer;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+.tab:hover {
+ color: var(--fg0);
+ background: var(--bg2);
+}
+
+.tab.active {
+ color: var(--accent);
+ border-bottom-color: var(--accent);
+}
+
+/* -- sidebar sections ------------------------------------------------------ */
+
+.sidebar-section {
+ padding: 12px 12px;
+ border-bottom: 1px solid var(--border-soft);
+ flex-shrink: 0;
+ min-height: 0;
+ display: flex;
+ flex-direction: column;
+}
+
+.sidebar-section:last-child {
+ flex: 1;
+ overflow-y: auto;
+}
+
+.sidebar-label {
+ display: flex;
+ align-items: baseline;
+ gap: 6px;
+ font-size: 10px;
+ color: var(--fg2);
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ margin-bottom: 6px;
+ font-weight: 600;
+}
+
+.sidebar-action {
+ font-size: 9px;
+ color: var(--fg2);
+ background: none;
+ border: none;
+ cursor: pointer;
+ padding: 0;
+ text-transform: lowercase;
+ letter-spacing: 0;
+ font-weight: 400;
+ opacity: 0.7;
+}
+
+.sidebar-action:hover {
+ color: var(--fg0);
+ opacity: 1;
+}
+
+#search-input {
+ width: 100%;
+ background: var(--bg2);
+ border: 1px solid var(--border);
+ color: var(--fg0);
+ padding: 5px 8px;
+ border-radius: 4px;
+ font-size: 12px;
+}
+
+#search-input:focus {
+ outline: none;
+ border-color: var(--accent);
+}
+
+.search-results {
+ margin-top: 6px;
+ max-height: 180px;
+ overflow-y: auto;
+ font-size: 12px;
+}
+
+.search-results .hit {
+ padding: 3px 6px;
+ border-radius: 3px;
+ cursor: pointer;
+ color: var(--fg1);
+ display: flex;
+ justify-content: space-between;
+ align-items: baseline;
+ gap: 8px;
+}
+
+.search-results .hit:hover {
+ background: var(--accent-soft);
+ color: var(--fg0);
+}
+
+.search-results .hit-name {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.search-results .hit-count {
+ color: var(--fg2);
+ font-size: 11px;
+ flex-shrink: 0;
+}
+
+/* -- threads list ---------------------------------------------------------- */
+
+.threads-list, .regions-list {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ overflow-y: auto;
+}
+
+.thread-group-header {
+ font-size: 10px;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ color: var(--fg2);
+ padding: 6px 4px 2px;
+ user-select: none;
+}
+
+.thread-group-header[data-group] {
+ cursor: pointer;
+ border-radius: 3px;
+}
+
+.thread-group-header[data-group]:hover {
+ color: var(--fg1);
+ background: var(--bg2);
+}
+
+.thread-group-header:first-child {
+ padding-top: 0;
+}
+
+.group-checkbox {
+ margin: 0 2px 0 0;
+ accent-color: var(--accent);
+ cursor: pointer;
+}
+
+.group-chevron {
+ display: inline-block;
+ margin-right: 2px;
+ transition: transform 0.15s;
+}
+
+.thread-group-header.collapsed .group-chevron {
+ transform: rotate(-90deg);
+}
+
+.thread-row {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 3px 4px;
+ border-radius: 3px;
+ cursor: pointer;
+ font-size: 12px;
+ color: var(--fg1);
+}
+
+.thread-row.lane .thread-name {
+ font-style: italic;
+}
+
+.thread-row:hover {
+ background: var(--bg2);
+}
+
+.thread-row.empty {
+ color: var(--fg2);
+ opacity: 0.6;
+}
+
+.thread-row input[type=checkbox] {
+ margin: 0;
+ accent-color: var(--accent);
+}
+
+.thread-row .thread-name {
+ flex: 1;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.thread-row .thread-count {
+ color: var(--fg2);
+ font-size: 11px;
+ flex-shrink: 0;
+}
+
+/* -- views ----------------------------------------------------------------- */
+
+.view {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ min-height: 0;
+ min-width: 0;
+ overflow: hidden;
+}
+
+.view[hidden] {
+ display: none;
+}
+
+/* -- timeline -------------------------------------------------------------- */
+
+.view-timeline {
+ position: relative;
+}
+
+.timeline-toolbar {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 6px 12px;
+ border-bottom: 1px solid var(--border-soft);
+ background: var(--bg1);
+ flex-shrink: 0;
+}
+
+.viewport-info {
+ color: var(--fg2);
+ font-size: 11px;
+ flex: 1;
+ font-family: 'SF Mono', 'Cascadia Mono', Consolas, monospace;
+}
+
+.toolbar-toggle {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ font-size: 11px;
+ color: var(--fg2);
+ cursor: pointer;
+ user-select: none;
+}
+
+.toolbar-toggle input[type="checkbox"] {
+ margin: 0;
+}
+
+.btn {
+ background: var(--bg2);
+ border: 1px solid var(--border);
+ color: var(--fg1);
+ padding: 3px 10px;
+ border-radius: 4px;
+ font-size: 11px;
+ cursor: pointer;
+}
+
+.btn:hover {
+ background: var(--bg3);
+ color: var(--fg0);
+}
+
+.timeline-frame {
+ flex: 1;
+ position: relative;
+ min-height: 0;
+ overflow: hidden;
+}
+
+#timeline-canvas {
+ display: block;
+ width: 100%;
+ height: 100%;
+ cursor: grab;
+}
+
+#timeline-canvas:active {
+ cursor: grabbing;
+}
+
+.tooltip {
+ position: absolute;
+ background: var(--bg1);
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ padding: 6px 10px;
+ font-size: 11px;
+ color: var(--fg0);
+ pointer-events: none;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
+ max-width: 360px;
+ z-index: 10;
+}
+
+.tooltip .tt-name {
+ font-weight: 600;
+ margin-bottom: 2px;
+}
+
+.tooltip .tt-meta {
+ color: var(--fg2);
+ font-family: 'SF Mono', 'Cascadia Mono', Consolas, monospace;
+ font-size: 10px;
+}
+
+.selection-panel {
+ background: var(--bg1);
+ border-top: 1px solid var(--border-soft);
+ padding: 10px 14px;
+ flex-shrink: 0;
+ min-height: 56px;
+ max-height: 140px;
+ overflow-y: auto;
+}
+
+.selection-hint {
+ color: var(--fg2);
+ font-size: 11px;
+ font-style: italic;
+}
+
+.selection-title {
+ color: var(--fg0);
+ font-weight: 600;
+ font-size: 13px;
+ margin-bottom: 4px;
+ word-break: break-all;
+}
+
+.selection-meta {
+ color: var(--fg2);
+ font-size: 11px;
+ font-family: 'SF Mono', 'Cascadia Mono', Consolas, monospace;
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
+ gap: 4px 16px;
+}
+
+.selection-meta .k {
+ color: var(--fg2);
+}
+
+.selection-meta .v {
+ color: var(--fg1);
+}
+
+/* -- stats table ----------------------------------------------------------- */
+
+.view-stats {
+ overflow-y: auto;
+ padding: 12px;
+}
+
+.stats-table {
+ width: 100%;
+ border-collapse: collapse;
+ font-size: 12px;
+}
+
+.stats-table th {
+ text-align: left;
+ padding: 8px 10px;
+ background: var(--bg1);
+ color: var(--fg2);
+ font-weight: 600;
+ text-transform: uppercase;
+ font-size: 10px;
+ letter-spacing: 0.5px;
+ border-bottom: 1px solid var(--border);
+ cursor: pointer;
+ user-select: none;
+ position: sticky;
+ top: 0;
+}
+
+.stats-table th.num {
+ text-align: right;
+}
+
+.stats-table th:hover {
+ color: var(--fg0);
+}
+
+.stats-table th.sorted::after {
+ content: ' ▾';
+ color: var(--accent);
+}
+
+.stats-table th.sorted.asc::after {
+ content: ' ▴';
+}
+
+.stats-table td {
+ padding: 5px 10px;
+ border-bottom: 1px solid var(--border-soft);
+ color: var(--fg1);
+}
+
+.stats-table td.num {
+ text-align: right;
+ font-variant-numeric: tabular-nums;
+ color: var(--fg0);
+}
+
+.stats-table tbody tr {
+ cursor: pointer;
+}
+
+.stats-table tbody tr:hover {
+ background: var(--bg1);
+}
+
+.stats-table tbody tr.selected {
+ background: var(--accent-soft);
+}
+
+/* -- session view ---------------------------------------------------------- */
+
+.view-session {
+ overflow-y: auto;
+ padding: 20px 24px;
+}
+
+.session-content h2 {
+ font-size: 14px;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ color: var(--fg2);
+ margin: 24px 0 10px;
+ border-bottom: 1px solid var(--border-soft);
+ padding-bottom: 4px;
+}
+
+.session-content h2:first-child {
+ margin-top: 0;
+}
+
+.session-content dl {
+ display: grid;
+ grid-template-columns: 150px 1fr;
+ gap: 6px 16px;
+ margin: 0 0 12px;
+ font-size: 12px;
+}
+
+.session-content dt {
+ color: var(--fg2);
+}
+
+.session-content dd {
+ margin: 0;
+ color: var(--fg1);
+ font-family: 'SF Mono', 'Cascadia Mono', Consolas, monospace;
+ word-break: break-all;
+}
+
+.session-content table {
+ width: 100%;
+ border-collapse: collapse;
+ font-size: 12px;
+}
+
+.session-content table th {
+ text-align: left;
+ padding: 6px 10px;
+ color: var(--fg2);
+ font-weight: 600;
+ font-size: 10px;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ border-bottom: 1px solid var(--border-soft);
+}
+
+.session-content table th.num {
+ text-align: right;
+}
+
+.session-content table td {
+ padding: 4px 10px;
+ border-bottom: 1px solid var(--border-soft);
+ color: var(--fg1);
+}
+
+.session-content table td.num {
+ text-align: right;
+ font-variant-numeric: tabular-nums;
+}
+
+.chan-enabled {
+ color: var(--ok);
+}
+
+.chan-disabled {
+ color: var(--fg2);
+}
+
+.chan-readonly {
+ color: var(--warn);
+ font-size: 10px;
+ margin-left: 8px;
+}
+
+/* -- logs view ------------------------------------------------------------- */
+
+.view-logs {
+ display: flex;
+ flex-direction: column;
+ min-height: 0;
+}
+
+#logs-content {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ min-height: 0;
+}
+
+.logs-toolbar {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ padding: 8px 12px;
+ background: var(--bg1);
+ border-bottom: 1px solid var(--border-soft);
+ flex-shrink: 0;
+}
+
+.logs-filter {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.logs-filter-grow {
+ flex: 1;
+}
+
+.logs-filter-label {
+ color: var(--fg2);
+ font-size: 10px;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ font-weight: 600;
+}
+
+.logs-toolbar select,
+.logs-toolbar input {
+ background: var(--bg2);
+ border: 1px solid var(--border);
+ color: var(--fg0);
+ padding: 4px 8px;
+ border-radius: 4px;
+ font-size: 12px;
+}
+
+.logs-toolbar select:focus,
+.logs-toolbar input:focus {
+ outline: none;
+ border-color: var(--accent);
+}
+
+.logs-toolbar input {
+ width: 100%;
+ box-sizing: border-box;
+}
+
+.logs-count {
+ color: var(--fg2);
+ font-size: 11px;
+ font-family: 'SF Mono', 'Cascadia Mono', Consolas, monospace;
+ white-space: nowrap;
+}
+
+.logs-list-wrap {
+ flex: 1;
+ overflow: auto;
+ min-height: 0;
+}
+
+.logs-table {
+ width: 100%;
+ border-collapse: collapse;
+ font-size: 12px;
+}
+
+.logs-table th {
+ text-align: left;
+ padding: 6px 10px;
+ background: var(--bg1);
+ color: var(--fg2);
+ font-weight: 600;
+ text-transform: uppercase;
+ font-size: 10px;
+ letter-spacing: 0.5px;
+ border-bottom: 1px solid var(--border);
+ position: sticky;
+ top: 0;
+ z-index: 1;
+}
+
+.logs-table td {
+ padding: 4px 10px;
+ border-bottom: 1px solid var(--border-soft);
+ vertical-align: top;
+}
+
+.logs-table .col-time {
+ white-space: nowrap;
+ color: var(--fg2);
+ width: 1%;
+}
+
+.logs-table .col-verb {
+ white-space: nowrap;
+ width: 1%;
+ font-weight: 500;
+}
+
+.logs-table .col-cat {
+ white-space: nowrap;
+ width: 1%;
+ color: var(--fg1);
+}
+
+.logs-table .col-msg {
+ color: var(--fg0);
+ word-break: break-word;
+}
+
+.logs-table .col-loc {
+ white-space: nowrap;
+ color: var(--fg2);
+ width: 1%;
+ font-size: 11px;
+}
+
+.logs-table tr.vb-fatal td,
+.logs-table tr.vb-error td {
+ color: var(--fail);
+}
+
+.logs-table tr.vb-error .col-msg {
+ color: var(--fail);
+}
+
+.logs-table tr.vb-warn .col-verb,
+.logs-table tr.vb-warn .col-msg {
+ color: var(--warn);
+}
+
+.logs-table tr.vb-display .col-verb {
+ color: var(--accent);
+}
+
+.logs-table tr.vb-verbose .col-verb,
+.logs-table tr.vb-verbose .col-msg {
+ color: var(--fg2);
+}
+
+.logs-table tr.bm-row .col-verb {
+ color: #e3b341;
+ font-weight: 600;
+}
+
+.logs-table tr.bm-row .col-msg {
+ color: #f0d078;
+}
+
+.logs-table tr.bm-row .col-time {
+ color: #e3b341;
+}
+
+.logs-empty, .logs-error {
+ padding: 20px;
+ text-align: center;
+ color: var(--fg2);
+}
+
+.logs-error {
+ color: var(--fail);
+}
+
+/* -- loading overlay ------------------------------------------------------- */
+
+.loading {
+ position: fixed;
+ inset: 0;
+ background: var(--bg0);
+ color: var(--fg2);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 14px;
+ z-index: 100;
+}
+
+.loading.hidden {
+ display: none;
+}
+
+/* ── CSV Stats view ───────────────────────────────────────────────── */
+
+.view-csv {
+ display: flex;
+ flex-direction: column;
+ min-height: 0;
+}
+
+#csv-content {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ min-height: 0;
+}
+
+.csv-layout {
+ display: flex;
+ flex: 1;
+ min-height: 0;
+}
+
+.csv-tree-panel {
+ width: 240px;
+ flex-shrink: 0;
+ border-right: 1px solid var(--border);
+ display: flex;
+ flex-direction: column;
+ overflow-y: auto;
+ padding: 8px;
+}
+
+.csv-chart-panel {
+ flex: 1;
+ position: relative;
+ min-width: 0;
+}
+
+.csv-chart-canvas {
+ width: 100%;
+ height: 100%;
+ display: block;
+}
+
+.csv-chart-tooltip {
+ position: absolute;
+ background: var(--bg1);
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ padding: 6px 8px;
+ font-size: 11px;
+ color: var(--fg1);
+ pointer-events: none;
+ z-index: 10;
+ white-space: nowrap;
+}
+
+.csv-cat-header {
+ font-size: 10px;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ color: var(--fg2);
+ padding: 8px 4px 2px;
+}
+
+.csv-stat-row {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 2px 4px;
+ border-radius: 3px;
+ cursor: pointer;
+ font-size: 12px;
+ color: var(--fg1);
+}
+
+.csv-stat-row:hover {
+ background: var(--bg2);
+}
+
+.csv-stat-row input[type=checkbox] {
+ margin: 0;
+ accent-color: var(--accent);
+}
+
+.csv-stat-name {
+ flex: 1;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.csv-empty {
+ color: var(--fg2);
+ font-size: 12px;
+ padding: 12px 4px;
+}
+
+/* -- memory view ---------------------------------------------------------- */
+
+.view-memory {
+ overflow: auto;
+ padding: 16px;
+}
+
+.memory-view {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+}
+
+.memory-cards {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
+ gap: 12px;
+}
+
+.memory-card,
+.memory-panel {
+ background: var(--bg1);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+}
+
+.memory-card {
+ padding: 12px 14px;
+}
+
+.memory-card-label,
+.memory-panel-subtitle,
+.memory-empty,
+.memory-frame-path {
+ color: var(--fg2);
+}
+
+.memory-chart-axis,
+.memory-chart-text {
+ fill: var(--fg2);
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
+ font-size: 11px;
+}
+
+.memory-card-label {
+ font-size: 11px;
+ text-transform: uppercase;
+ letter-spacing: 0.4px;
+ margin-bottom: 6px;
+}
+
+.memory-card-value {
+ font-size: 20px;
+ font-weight: 600;
+ color: var(--fg0);
+}
+
+.memory-panel-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: baseline;
+ gap: 12px;
+ padding: 12px 14px;
+ border-bottom: 1px solid var(--border-soft);
+}
+
+.memory-panel-header-wrap {
+ flex-wrap: wrap;
+}
+
+.memory-panel-title {
+ font-weight: 600;
+ color: var(--fg0);
+}
+
+.memory-controls {
+ display: flex;
+ gap: 10px;
+ align-items: center;
+ flex-wrap: wrap;
+ color: var(--fg2);
+ font-size: 12px;
+}
+
+.memory-controls label {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.memory-filter-input,
+.memory-controls select {
+ background: var(--bg2);
+ color: var(--fg1);
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ padding: 4px 6px;
+ font-size: 12px;
+}
+
+.memory-filter-input {
+ min-width: 180px;
+}
+
+.memory-direction-btn,
+.memory-clear-btn {
+ background: var(--bg2);
+ color: var(--fg1);
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ padding: 4px 8px;
+ font-size: 12px;
+ cursor: pointer;
+}
+
+.memory-direction-btn:hover,
+.memory-clear-btn:hover:not(:disabled) {
+ background: var(--bg3);
+}
+
+.memory-clear-btn:disabled {
+ opacity: 0.5;
+ cursor: default;
+}
+
+.memory-chart-wrap {
+ padding: 10px 12px 12px;
+}
+
+.memory-chart {
+ display: block;
+ width: 100%;
+ height: 220px;
+}
+
+.memory-chart-bg {
+ fill: var(--bg1);
+}
+
+.memory-chart-grid {
+ stroke: var(--border-soft);
+ stroke-width: 1;
+}
+
+.memory-chart-grid-vert {
+ stroke-opacity: 0.45;
+}
+
+.memory-chart-line {
+ fill: none;
+ stroke: var(--accent);
+ stroke-width: 2;
+ stroke-linejoin: round;
+ stroke-linecap: round;
+}
+
+.memory-histogram {
+ height: 260px;
+}
+
+.memory-histogram-bar {
+ fill: var(--accent);
+ fill-opacity: 0.78;
+}
+
+.memory-histogram-bar:hover {
+ fill-opacity: 1;
+}
+
+.memory-grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 16px;
+}
+
+.memory-callstack-panel {
+ grid-column: 1 / -1;
+}
+
+.memory-table-wrap {
+ overflow: auto;
+ max-height: 360px;
+}
+
+.memory-table {
+ width: 100%;
+ border-collapse: collapse;
+}
+
+.memory-table th,
+.memory-table td {
+ padding: 8px 10px;
+ border-bottom: 1px solid var(--border-soft);
+ text-align: left;
+ vertical-align: top;
+}
+
+.memory-table th {
+ position: sticky;
+ top: 0;
+ background: var(--bg1);
+ z-index: 1;
+ font-size: 11px;
+ text-transform: uppercase;
+ letter-spacing: 0.4px;
+ color: var(--fg2);
+}
+
+.memory-table .num {
+ text-align: right;
+ white-space: nowrap;
+}
+
+.memory-table tbody tr {
+ cursor: pointer;
+}
+
+.memory-table tbody tr:hover {
+ background: var(--bg2);
+}
+
+.memory-table tbody tr.selected {
+ background: var(--accent-soft);
+}
+
+.memory-group-row td {
+ background: var(--bg2);
+ color: var(--fg2);
+ font-size: 11px;
+ text-transform: uppercase;
+ letter-spacing: 0.4px;
+ font-weight: 600;
+}
+
+.memory-summary-top-row {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 10px;
+}
+
+.memory-summary-top {
+ color: var(--fg0);
+ font-weight: 500;
+ word-break: break-word;
+ min-width: 0;
+ flex: 1;
+}
+
+.memory-summary-secondary {
+ margin-top: 3px;
+ color: var(--fg2);
+ font-size: 12px;
+ word-break: break-word;
+}
+
+.memory-summary-badges {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+ justify-content: flex-end;
+ flex-shrink: 0;
+}
+
+.memory-badge {
+ display: inline-block;
+ padding: 1px 6px;
+ border-radius: 999px;
+ background: var(--accent-soft);
+ color: var(--fg2);
+ font-size: 11px;
+}
+
+.memory-mark {
+ background: color-mix(in srgb, var(--accent) 28%, transparent);
+ color: inherit;
+ padding: 0 1px;
+ border-radius: 2px;
+}
+
+.memory-callstack-body {
+ padding: 12px 14px;
+ max-height: 320px;
+ overflow: auto;
+}
+
+.memory-callstack-list {
+ margin: 0;
+ padding-left: 22px;
+}
+
+.memory-callstack-list li {
+ margin: 0 0 8px;
+ word-break: break-word;
+}
+
+.memory-frame-index {
+ color: var(--fg2);
+ margin-right: 8px;
+}
+
+.memory-frame-display {
+ font-family: 'SF Mono', 'Cascadia Mono', Consolas, 'DejaVu Sans Mono', monospace;
+ color: var(--fg0);
+}
+
+.memory-frame-path {
+ margin-left: 8px;
+ font-size: 12px;
+}
+
+@media (max-width: 1200px) {
+ .memory-grid {
+ grid-template-columns: 1fr;
+ }
+}
diff --git a/src/zen/frontend/html/trace.js b/src/zen/frontend/html/trace.js
new file mode 100644
index 000000000..2910da15d
--- /dev/null
+++ b/src/zen/frontend/html/trace.js
@@ -0,0 +1,577 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+// Entry point: boots the viewer, owns the model, wires tabs / sidebar /
+// search / threads list / session panel.
+
+import { getSession, getThreads, getChannels, getScopeStats, getScopeNames, getLogCategories, getBookmarks, getRegions, getCsvCategories, getCsvStats } from "./api.js";
+import { Timeline } from "./timeline.js";
+import { StatsView } from "./stats.js";
+import { MemoryView } from "./memory.js";
+import { LogsView } from "./logs.js";
+import { CsvStatsView } from "./csvstats.js";
+
+function escapeHtml(s) {
+ return String(s).replace(/[&<>"']/g, (c) => ({
+ "&": "&amp;",
+ "<": "&lt;",
+ ">": "&gt;",
+ "\"": "&quot;",
+ "'": "&#39;",
+ }[c]));
+}
+
+function formatTimeMs(us) {
+ if (us < 1000) return `${us} µs`;
+ if (us < 1_000_000) return `${(us / 1000).toFixed(2)} ms`;
+ return `${(us / 1_000_000).toFixed(2)} s`;
+}
+
+function formatNum(n) {
+ return Number(n).toLocaleString();
+}
+
+function stripNul(s) {
+ return (s || "").replace(/\u0000/g, "");
+}
+
+function getThemePreference() {
+ const params = new URLSearchParams(window.location.search);
+ const theme = params.get("theme");
+ if (theme === "dark" || theme === "light" || theme === "system") {
+ return theme;
+ }
+ const stored = window.localStorage.getItem("zen-trace-theme");
+ if (stored === "dark" || stored === "light" || stored === "system") {
+ return stored;
+ }
+ return "system";
+}
+
+function applyTheme(theme) {
+ document.documentElement.setAttribute("data-theme", theme);
+ window.localStorage.setItem("zen-trace-theme", theme);
+ const url = new URL(window.location.href);
+ if (theme === "system") {
+ url.searchParams.delete("theme");
+ } else {
+ url.searchParams.set("theme", theme);
+ }
+ window.history.replaceState({}, "", url);
+ const btn = document.getElementById("theme-toggle");
+ if (btn) {
+ btn.textContent = theme === "dark" ? "Dark" : theme === "light" ? "Light" : "System";
+ btn.title = `Theme: ${theme}. Click to cycle.`;
+ }
+}
+
+function setupThemeToggle() {
+ const btn = document.getElementById("theme-toggle");
+ if (!btn) {
+ return;
+ }
+ const themes = ["system", "dark", "light"];
+ let theme = getThemePreference();
+ applyTheme(theme);
+ btn.addEventListener("click", () => {
+ const index = themes.indexOf(theme);
+ theme = themes[(index + 1) % themes.length];
+ applyTheme(theme);
+ });
+}
+
+async function main() {
+ setupThemeToggle();
+ const loadingEl = document.getElementById("loading");
+ try {
+ const [session, threads, channels, scopeStats, scopeNames, logCategories, bookmarks, regionsResponse, csvCategories, csvStats] = await Promise.all([
+ getSession(),
+ getThreads(),
+ getChannels(),
+ getScopeStats(),
+ getScopeNames(),
+ getLogCategories(),
+ getBookmarks(),
+ getRegions(),
+ getCsvCategories(),
+ getCsvStats(),
+ ]);
+
+ // Normalize strings (tourist sometimes leaves trailing NULs in FieldStr).
+ for (const t of threads) t.name = stripNul(t.name);
+ session.app_name = stripNul(session.app_name);
+ session.project_name = stripNul(session.project_name);
+ session.branch = stripNul(session.branch);
+ session.build_version = stripNul(session.build_version);
+ session.platform = stripNul(session.platform);
+ session.command_line = stripNul(session.command_line);
+ for (const s of scopeStats) s.name = stripNul(s.name);
+ for (let i = 0; i < scopeNames.length; i++) scopeNames[i] = stripNul(scopeNames[i]);
+ for (const c of logCategories) c.name = stripNul(c.name);
+ for (const b of bookmarks) {
+ b.text = stripNul(b.text);
+ b.file = stripNul(b.file);
+ }
+ const regionCategories = regionsResponse && regionsResponse.categories ? regionsResponse.categories : [];
+ for (const cat of regionCategories) {
+ cat.name = stripNul(cat.name);
+ for (const r of cat.regions) {
+ r.name = stripNul(r.name);
+ }
+ }
+ for (const cat of csvCategories) cat.name = stripNul(cat.name);
+ for (const s of csvStats) s.name = stripNul(s.name);
+
+ // Precompute name → id for highlight lookups.
+ const scopeNameIds = new Map();
+ for (let i = 0; i < scopeNames.length; i++) {
+ scopeNameIds.set(scopeNames[i], i);
+ }
+
+ const model = { session, threads, channels, scopeStats, scopeNames, scopeNameIds, logCategories, bookmarks, regionCategories, csvCategories, csvStats };
+
+ renderHeader(model);
+ renderSessionView(model);
+
+ const timeline = new Timeline({
+ canvas: document.getElementById("timeline-canvas"),
+ tooltip: document.getElementById("tooltip"),
+ selectionEl: document.getElementById("selection-panel"),
+ viewportInfoEl: document.getElementById("viewport-info"),
+ zoomResetBtn: document.getElementById("zoom-reset"),
+ model,
+ onScopeSelect: (name) => {
+ timeline.setHighlightName(name);
+ stats.selectByName(name);
+ },
+ });
+
+ const stats = new StatsView(
+ document.getElementById("stats-tbody"),
+ document.querySelector(".stats-table thead tr"),
+ model,
+ (name) => {
+ timeline.setHighlightName(name);
+ timeline.jumpToScopeName(name);
+ },
+ );
+
+ const memoryView = new MemoryView(model, document.getElementById("memory-content"));
+ const logsView = new LogsView(model, document.getElementById("logs-content"));
+ const csvView = new CsvStatsView(model, document.getElementById("csv-content"));
+
+ const threadsListApi = renderThreadsList(model, timeline);
+ renderRegionCategories(model, timeline);
+ setupTabs(memoryView, logsView, csvView);
+ setupSearch(model, timeline, stats);
+
+ const bookmarksToggle = document.getElementById("bookmarks-toggle");
+ bookmarksToggle.addEventListener("change", () => {
+ timeline.setBookmarksVisible(bookmarksToggle.checked);
+ });
+
+ const lodToggle = document.getElementById("lod-toggle");
+ lodToggle.addEventListener("change", () => {
+ timeline.setLodEnabled(lodToggle.checked);
+ });
+
+ // Enable all threads that actually have captured scopes by default; if
+ // none do, enable every thread so the swimlanes still show up empty.
+ const withScopes = model.threads.filter((t) => t.scope_count > 0).map((t) => t.thread_id);
+ const initialEnabled = withScopes.length > 0 ? withScopes : model.threads.map((t) => t.thread_id);
+ for (const id of initialEnabled) {
+ const cb = document.querySelector(`.thread-row input[data-tid="${id}"]`);
+ if (cb) cb.checked = true;
+ }
+ threadsListApi.syncAllGroupCheckboxes();
+ timeline.setEnabledThreads(initialEnabled);
+
+ // "deselect all / select all" toggle for the Threads panel
+ const toggleAllBtn = document.getElementById("threads-toggle-all");
+ const threadsList = document.getElementById("threads-list");
+ toggleAllBtn.addEventListener("click", () => {
+ const allBoxes = threadsList.querySelectorAll(".thread-row input[type=checkbox]");
+ const anyChecked = Array.from(allBoxes).some((cb) => cb.checked);
+ const newState = !anyChecked;
+ for (const cb of allBoxes) {
+ cb.checked = newState;
+ }
+ // Sync group checkboxes
+ for (const gcb of threadsList.querySelectorAll(".group-checkbox")) {
+ gcb.checked = newState;
+ gcb.indeterminate = false;
+ }
+ toggleAllBtn.textContent = newState ? "deselect all" : "select all";
+ const enabled = [];
+ if (newState) {
+ for (const cb of allBoxes) {
+ enabled.push(Number(cb.dataset.tid));
+ }
+ }
+ timeline.setEnabledThreads(enabled);
+ });
+
+ loadingEl.classList.add("hidden");
+ } catch (e) {
+ loadingEl.textContent = `Failed to load trace: ${e.message}`;
+ console.error(e);
+ }
+}
+
+function renderHeader(model) {
+ const { session } = model;
+ const hdrFile = document.getElementById("hdr-file");
+ hdrFile.textContent = session.file_path || "";
+ hdrFile.title = session.file_path || "";
+
+ const stats = document.getElementById("hdr-stats");
+ stats.innerHTML =
+ `<span><span class="k">events:</span><span class="v"></span></span>` +
+ `<span><span class="k">threads:</span><span class="v"></span></span>` +
+ `<span><span class="k">duration:</span><span class="v"></span></span>` +
+ `<span><span class="k">parse:</span><span class="v"></span></span>`;
+ const vs = stats.querySelectorAll(".v");
+ vs[0].textContent = formatNum(session.total_events || 0);
+ vs[1].textContent = formatNum(model.threads.length);
+ vs[2].textContent = formatTimeMs((session.trace_end_us || 0) - (session.trace_start_us || 0));
+ vs[3].textContent = `${session.parse_time_ms} ms`;
+}
+
+function renderSessionView(model) {
+ const { session, threads, channels } = model;
+ const el = document.getElementById("session-content");
+
+ const rows = [];
+ rows.push("<h2>Session</h2>");
+ rows.push("<dl>");
+ const row = (k, v) => v && rows.push(`<dt>${k}</dt><dd>${escapeHtml(v)}</dd>`);
+ row("File", session.file_path);
+ row("Size", `${formatNum(session.file_size)} bytes`);
+ row("Events", formatNum(session.total_events));
+ row("Parse time", `${session.parse_time_ms} ms`);
+ row("Platform", session.platform);
+ row("App", session.app_name);
+ row("Project", session.project_name);
+ row("Branch", session.branch);
+ row("Build", session.build_version);
+ if (session.changelist) row("Changelist", String(session.changelist));
+ row("Command line", session.command_line);
+ rows.push("</dl>");
+
+ rows.push("<h2>Threads</h2>");
+ rows.push("<table><thead><tr>");
+ rows.push(`<th>Name</th><th class="num">TID</th><th class="num">System ID</th><th class="num">Scopes</th>`);
+ rows.push("</tr></thead><tbody>");
+ for (const t of threads) {
+ rows.push(
+ `<tr><td>${escapeHtml(t.name || "")}</td>` +
+ `<td class="num">${t.thread_id}</td>` +
+ `<td class="num">${t.system_id}</td>` +
+ `<td class="num">${formatNum(t.scope_count || 0)}</td></tr>`,
+ );
+ }
+ rows.push("</tbody></table>");
+
+ if (channels && channels.length) {
+ rows.push("<h2>Trace channels</h2>");
+ rows.push("<table><thead><tr><th>Name</th><th>State</th></tr></thead><tbody>");
+ for (const c of channels) {
+ const cls = c.enabled ? "chan-enabled" : "chan-disabled";
+ const ro = c.readonly ? `<span class="chan-readonly">read-only</span>` : "";
+ rows.push(`<tr><td>${escapeHtml(c.name || "")}</td><td class="${cls}">${c.enabled ? "enabled" : "disabled"}${ro}</td></tr>`);
+ }
+ rows.push("</tbody></table>");
+ }
+
+ el.innerHTML = rows.join("");
+}
+
+function renderThreadsList(model, timeline) {
+ const list = document.getElementById("threads-list");
+ const cmp = (a, b) => {
+ // SortHint first (UE sets low values for important threads like GameThread)
+ if (a.sort_hint !== b.sort_hint) return a.sort_hint - b.sort_hint;
+ // Then threads with scopes before empty ones
+ if ((a.scope_count > 0) !== (b.scope_count > 0)) return b.scope_count - a.scope_count;
+ // Then by thread ID (lower = created earlier, main thread is typically first)
+ if (a.thread_id !== b.thread_id) return a.thread_id - b.thread_id;
+ return (a.name || "").localeCompare(b.name || "", undefined, { numeric: true });
+ };
+
+ // Build groups: ungrouped threads, named groups, and lanes
+ const lanes = model.threads.filter(t => t.is_lane).sort(cmp);
+ const grouped = new Map(); // groupName → [threads]
+ const ungrouped = [];
+ for (const t of model.threads) {
+ if (t.is_lane) continue;
+ const g = t.group || "";
+ if (g) {
+ if (!grouped.has(g)) grouped.set(g, []);
+ grouped.get(g).push(t);
+ } else {
+ ungrouped.push(t);
+ }
+ }
+ ungrouped.sort(cmp);
+ for (const [, threads] of grouped) threads.sort(cmp);
+
+ // Sort group names naturally
+ const groupNames = Array.from(grouped.keys()).sort((a, b) =>
+ a.localeCompare(b, undefined, { numeric: true }));
+
+ const collapsed = new Set();
+ const parts = [];
+
+ function renderGroup(label, threads, collapsible) {
+ if (threads.length === 0) return;
+ const collapseAttr = collapsible ? ` data-group="${label}"` : "";
+ const chevron = collapsible ? `<span class="group-chevron">&#x25BE;</span>` : "";
+ const groupCb = collapsible ? `<input type="checkbox" class="group-checkbox" data-group-toggle="${label}">` : "";
+ parts.push(`<div class="thread-group-header"${collapseAttr}>${groupCb}${chevron}${label} (${threads.length})</div>`);
+ for (const t of threads) {
+ const emptyCls = t.scope_count === 0 ? " empty" : "";
+ const laneCls = t.is_lane ? " lane" : "";
+ const groupAttr = collapsible ? ` data-group-member="${label}"` : "";
+ parts.push(
+ `<label class="thread-row${emptyCls}${laneCls}"${groupAttr}>` +
+ `<input type="checkbox" data-tid="${t.thread_id}">` +
+ `<span class="thread-name"></span>` +
+ `<span class="thread-count">${formatNum(t.scope_count || 0)}</span>` +
+ `</label>`,
+ );
+ }
+ }
+
+ // Ungrouped threads first, then named groups, then lanes
+ if (ungrouped.length > 0 && (grouped.size > 0 || lanes.length > 0)) {
+ renderGroup("Threads", ungrouped, false);
+ } else {
+ // No groups at all — render without a header
+ for (const t of ungrouped) {
+ const emptyCls = t.scope_count === 0 ? " empty" : "";
+ parts.push(
+ `<label class="thread-row${emptyCls}">` +
+ `<input type="checkbox" data-tid="${t.thread_id}">` +
+ `<span class="thread-name"></span>` +
+ `<span class="thread-count">${formatNum(t.scope_count || 0)}</span>` +
+ `</label>`,
+ );
+ }
+ }
+ for (const name of groupNames) renderGroup(name, grouped.get(name), true);
+ renderGroup("Lanes", lanes, lanes.length > 0);
+
+ list.innerHTML = parts.join("");
+
+ function syncTimeline() {
+ const enabled = new Set();
+ for (const box of list.querySelectorAll(".thread-row input[type=checkbox]")) {
+ if (box.checked) enabled.add(Number(box.dataset.tid));
+ }
+ timeline.setEnabledThreads(Array.from(enabled));
+ }
+
+ function syncGroupCheckbox(groupName) {
+ const members = list.querySelectorAll(`[data-group-member="${groupName}"] input[type=checkbox]`);
+ const gcb = list.querySelector(`.group-checkbox[data-group-toggle="${groupName}"]`);
+ if (!gcb || members.length === 0) return;
+ const checkedCount = Array.from(members).filter((c) => c.checked).length;
+ gcb.checked = checkedCount === members.length;
+ gcb.indeterminate = checkedCount > 0 && checkedCount < members.length;
+ }
+
+ function syncAllGroupCheckboxes() {
+ for (const gcb of list.querySelectorAll(".group-checkbox")) {
+ syncGroupCheckbox(gcb.dataset.groupToggle);
+ }
+ }
+
+ // Wire up collapsible group headers (click on the label area, not checkbox)
+ for (const hdr of list.querySelectorAll(".thread-group-header[data-group]")) {
+ hdr.addEventListener("click", (e) => {
+ // Don't collapse when clicking the group checkbox
+ if (e.target.classList.contains("group-checkbox")) return;
+ const group = hdr.dataset.group;
+ const isCollapsed = collapsed.has(group);
+ if (isCollapsed) {
+ collapsed.delete(group);
+ hdr.classList.remove("collapsed");
+ } else {
+ collapsed.add(group);
+ hdr.classList.add("collapsed");
+ }
+ for (const row of list.querySelectorAll(`[data-group-member="${group}"]`)) {
+ row.style.display = isCollapsed ? "" : "none";
+ }
+ });
+ }
+
+ // Wire up group checkboxes — toggle all children
+ for (const gcb of list.querySelectorAll(".group-checkbox")) {
+ gcb.addEventListener("change", () => {
+ const group = gcb.dataset.groupToggle;
+ const checked = gcb.checked;
+ for (const row of list.querySelectorAll(`[data-group-member="${group}"]`)) {
+ const cb = row.querySelector("input[type=checkbox]");
+ if (cb) cb.checked = checked;
+ }
+ syncTimeline();
+ });
+ }
+
+ // Wire up thread checkboxes
+ for (const row of list.querySelectorAll(".thread-row")) {
+ const cb = row.querySelector("input");
+ const nameEl = row.querySelector(".thread-name");
+ const tid = Number(cb.dataset.tid);
+ const thread = model.threads.find((t) => t.thread_id === tid);
+ nameEl.textContent = (thread && thread.name) || `tid ${tid}`;
+ cb.addEventListener("change", () => {
+ const groupMember = row.dataset.groupMember;
+ if (groupMember) syncGroupCheckbox(groupMember);
+ syncTimeline();
+ });
+ }
+
+ return { syncAllGroupCheckboxes };
+}
+
+function renderRegionCategories(model, timeline) {
+ const categories = model.regionCategories || [];
+ if (categories.length === 0) return;
+
+ const panel = document.getElementById("regions-panel");
+ panel.hidden = false;
+
+ const list = document.getElementById("regions-list");
+ const parts = [];
+ for (let i = 0; i < categories.length; i++) {
+ const cat = categories[i];
+ const count = cat.regions ? cat.regions.length : 0;
+ parts.push(
+ `<label class="thread-row">` +
+ `<input type="checkbox" data-cat-idx="${i}" checked>` +
+ `<span class="thread-name"></span>` +
+ `<span class="thread-count">${formatNum(count)}</span>` +
+ `</label>`,
+ );
+ }
+ list.innerHTML = parts.join("");
+
+ // Set label text via DOM to avoid XSS
+ for (const row of list.querySelectorAll(".thread-row")) {
+ const cb = row.querySelector("input");
+ const nameEl = row.querySelector(".thread-name");
+ const idx = Number(cb.dataset.catIdx);
+ nameEl.textContent = categories[idx].name || "Uncategorized";
+ }
+
+ function syncTimeline() {
+ const enabled = new Set();
+ for (const cb of list.querySelectorAll("input[type=checkbox]")) {
+ if (cb.checked) enabled.add(Number(cb.dataset.catIdx));
+ }
+ timeline.setEnabledRegionCategories(enabled);
+ }
+
+ for (const cb of list.querySelectorAll("input[type=checkbox]")) {
+ cb.addEventListener("change", syncTimeline);
+ }
+
+ // "deselect all / select all" toggle
+ const toggleBtn = document.getElementById("regions-toggle-all");
+ toggleBtn.addEventListener("click", () => {
+ const allBoxes = list.querySelectorAll("input[type=checkbox]");
+ const anyChecked = Array.from(allBoxes).some((cb) => cb.checked);
+ const newState = !anyChecked;
+ for (const cb of allBoxes) cb.checked = newState;
+ toggleBtn.textContent = newState ? "deselect all" : "select all";
+ syncTimeline();
+ });
+
+ // Initial state: all enabled
+ const allIndices = new Set(categories.map((_, i) => i));
+ timeline.setEnabledRegionCategories(allIndices);
+}
+
+function setupTabs(memoryView, logsView, csvView) {
+ const tabs = document.querySelectorAll(".tab");
+ const views = document.querySelectorAll(".view");
+ const validTabs = new Set(Array.from(tabs, (tab) => tab.dataset.tab));
+
+ function activateTab(key, updateUrl = true) {
+ for (const tab of tabs) {
+ tab.classList.toggle("active", tab.dataset.tab === key);
+ }
+ for (const view of views) {
+ view.hidden = view.dataset.view !== key;
+ }
+ if (updateUrl) {
+ const url = new URL(window.location.href);
+ url.searchParams.set("tab", key);
+ window.history.replaceState({}, "", url);
+ }
+ if (key === "memory" && memoryView) {
+ memoryView.ensureLoaded();
+ }
+ if (key === "logs" && logsView) {
+ logsView.ensureLoaded();
+ }
+ if (key === "csv" && csvView) {
+ csvView.ensureLoaded();
+ }
+ }
+
+ for (const tab of tabs) {
+ tab.addEventListener("click", () => activateTab(tab.dataset.tab));
+ }
+
+ const initialTab = new URLSearchParams(window.location.search).get("tab");
+ if (initialTab && validTabs.has(initialTab)) {
+ activateTab(initialTab, false);
+ }
+}
+
+function setupSearch(model, timeline, stats) {
+ const input = document.getElementById("search-input");
+ const results = document.getElementById("search-results");
+
+ function render() {
+ const q = input.value.trim().toLowerCase();
+ if (!q) {
+ results.innerHTML = "";
+ timeline.setHighlightName(null);
+ return;
+ }
+ const matches = [];
+ for (const s of model.scopeStats) {
+ if (s.name.toLowerCase().includes(q)) {
+ matches.push(s);
+ if (matches.length >= 50) break;
+ }
+ }
+ const parts = [];
+ for (const m of matches) {
+ parts.push(
+ `<div class="hit" data-name="${escapeHtml(m.name)}">` +
+ `<span class="hit-name">${escapeHtml(m.name)}</span>` +
+ `<span class="hit-count">${formatNum(m.count)}</span>` +
+ `</div>`,
+ );
+ }
+ results.innerHTML = parts.join("");
+ for (const hit of results.querySelectorAll(".hit")) {
+ hit.addEventListener("click", () => {
+ const name = hit.dataset.name;
+ timeline.setHighlightName(name);
+ timeline.jumpToScopeName(name);
+ stats.selectByName(name);
+ });
+ }
+ if (matches.length > 0) {
+ timeline.setHighlightName(matches[0].name);
+ }
+ }
+
+ input.addEventListener("input", render);
+}
+
+main();
diff --git a/src/zen/trace/callstack_formatter.cpp b/src/zen/trace/callstack_formatter.cpp
new file mode 100644
index 000000000..0c601d5c0
--- /dev/null
+++ b/src/zen/trace/callstack_formatter.cpp
@@ -0,0 +1,251 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#include "callstack_formatter.h"
+
+#include <zencore/fmtutils.h>
+#include <zencore/string.h>
+#include <zenutil/wildcard.h>
+
+#include <algorithm>
+
+namespace zen::trace_detail {
+
+namespace {
+
+ static bool IsKnownRuntimeModuleName(std::string_view ModuleName)
+ {
+ std::string LowerName = zen::ToLower(ModuleName);
+ return LowerName == "ntdll.dll" || LowerName == "kernel32.dll" || LowerName == "kernelbase.dll" || LowerName == "ucrtbase.dll" ||
+ LowerName == "ucrtbased.dll" || LowerName.starts_with("vcruntime") || LowerName.starts_with("msvcp") ||
+ LowerName.starts_with("api-ms-win-") || LowerName == "libc.so" || LowerName.starts_with("libc.so.") ||
+ LowerName == "libstdc++.so" || LowerName.starts_with("libstdc++.so.") || LowerName == "libgcc_s.so" ||
+ LowerName.starts_with("libgcc_s.so.") || LowerName == "libpthread.so" || LowerName.starts_with("libpthread.so.") ||
+ LowerName == "libm.so" || LowerName.starts_with("libm.so.") || LowerName == "ld-linux.so" ||
+ LowerName.starts_with("ld-linux") || LowerName == "libsystem_kernel.dylib" || LowerName == "libsystem_malloc.dylib" ||
+ LowerName == "libsystem_pthread.dylib" || LowerName == "libdyld.dylib";
+ }
+
+ static bool PathLooksThirdParty(std::string_view ModulePath)
+ {
+ std::string LowerPath = zen::ToLower(ModulePath);
+ return LowerPath.find("/thirdparty/") != std::string::npos || LowerPath.find("\\thirdparty\\") != std::string::npos ||
+ LowerPath.find("/third-party/") != std::string::npos || LowerPath.find("\\third-party\\") != std::string::npos ||
+ LowerPath.find("/external/") != std::string::npos || LowerPath.find("\\external\\") != std::string::npos ||
+ LowerPath.find("/extern/") != std::string::npos || LowerPath.find("\\extern\\") != std::string::npos ||
+ LowerPath.find("/engine/binaries/thirdparty/") != std::string::npos ||
+ LowerPath.find("\\engine\\binaries\\thirdparty\\") != std::string::npos ||
+ LowerPath.find("c:\\windows\\system32\\") != std::string::npos ||
+ LowerPath.find("c:\\windows\\syswow64\\") != std::string::npos || LowerPath.starts_with("/usr/lib/") ||
+ LowerPath.starts_with("/lib/") || LowerPath.starts_with("/system/");
+ }
+
+ static bool MatchesAnyPattern(std::string_view Text, const std::vector<std::string>& Patterns)
+ {
+ for (const std::string& Pattern : Patterns)
+ {
+ if (zen::MatchWildcard(Pattern, Text, /*CaseSensitive=*/false))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ static bool ShouldSkipFrameByPattern(const CallstackFilterOptions& Options,
+ const TraceModel& Model,
+ const ResolvedFrame& Frame,
+ std::string_view Description)
+ {
+ if (MatchesAnyPattern(Description, Options.SkipPatterns))
+ {
+ return true;
+ }
+ if (Frame.ModuleIndex != ~0u && Frame.ModuleIndex < Model.Modules.size())
+ {
+ const ModuleInfo& Module = Model.Modules[Frame.ModuleIndex];
+ if (MatchesAnyPattern(Module.Name, Options.SkipPatterns) || MatchesAnyPattern(Module.FullPath, Options.SkipPatterns))
+ {
+ return true;
+ }
+ }
+
+ static const std::vector<std::string> kDefaultSkipPatterns = {
+ "zen::MemoryTrace_*",
+ "*mi_*",
+ "*_mi_*",
+ "*rpmalloc*",
+ "*mimalloc*",
+ "*je_malloc*",
+ "*je_free*",
+ "*malloc*",
+ "*free*",
+ "*realloc*",
+ };
+ return MatchesAnyPattern(Description, kDefaultSkipPatterns);
+ }
+
+ static bool IsThirdPartyFrame(const TraceModel& Model, const ResolvedFrame& Frame, std::string_view Description)
+ {
+ if (Description.starts_with("std::"))
+ {
+ return true;
+ }
+ if (Frame.ModuleIndex == ~0u || Frame.ModuleIndex >= Model.Modules.size())
+ {
+ return false;
+ }
+
+ const ModuleInfo& Module = Model.Modules[Frame.ModuleIndex];
+ return IsKnownRuntimeModuleName(Module.Name) || PathLooksThirdParty(Module.FullPath);
+ }
+
+} // namespace
+
+CallstackFormatter::CallstackFormatter(const TraceModel& InModel, const SymbolResolver* InSymbols) : m_Model(InModel), m_Symbols(InSymbols)
+{
+}
+
+const CallstackEntry*
+CallstackFormatter::FindCallstackEntry(uint32_t CallstackId) const
+{
+ auto It =
+ eastl::lower_bound(m_Model.Callstacks.begin(), m_Model.Callstacks.end(), CallstackId, [](const CallstackEntry& E, uint32_t Id) {
+ return E.Id < Id;
+ });
+ if (It == m_Model.Callstacks.end() || It->Id != CallstackId)
+ {
+ return nullptr;
+ }
+ return &*It;
+}
+
+const std::string&
+CallstackFormatter::Describe(const ResolvedFrame& Frame)
+{
+ auto It = m_Cache.find(Frame.Address);
+ if (It != m_Cache.end())
+ {
+ return It->second;
+ }
+
+ std::string Result = m_Symbols ? m_Symbols->Resolve(Frame.Address) : std::string{};
+ if (Result.empty())
+ {
+ if (Frame.ModuleIndex != ~0u && Frame.ModuleIndex < m_Model.Modules.size())
+ {
+ Result = fmt::format("{} + 0x{:X}", m_Model.Modules[Frame.ModuleIndex].Name, Frame.Offset);
+ }
+ else
+ {
+ Result = fmt::format("0x{:X}", Frame.Address);
+ }
+ }
+
+ auto [InsertedIt, Inserted] = m_Cache.emplace(Frame.Address, std::move(Result));
+ ZEN_UNUSED(Inserted);
+ return InsertedIt->second;
+}
+
+FilteredCallstackView
+CallstackFormatter::BuildView(const CallstackEntry& Entry, const CallstackFilterOptions& Options)
+{
+ FilteredCallstackView Result;
+ Result.Frames.reserve(Entry.Frames.size());
+ if (Entry.Frames.empty())
+ {
+ return Result;
+ }
+
+ eastl::vector<bool> ExplicitSkip;
+ eastl::vector<bool> ThirdParty;
+ ExplicitSkip.reserve(Entry.Frames.size());
+ ThirdParty.reserve(Entry.Frames.size());
+
+ for (const ResolvedFrame& Frame : Entry.Frames)
+ {
+ const std::string& Description = Describe(Frame);
+ ExplicitSkip.push_back(ShouldSkipFrameByPattern(Options, m_Model, Frame, Description));
+ ThirdParty.push_back(IsThirdPartyFrame(m_Model, Frame, Description));
+ }
+
+ eastl::vector<size_t> VisibleFrameIndices;
+ VisibleFrameIndices.reserve(Entry.Frames.size());
+
+ if (!Options.EnableHeuristic)
+ {
+ for (size_t Index = 0; Index < Entry.Frames.size(); ++Index)
+ {
+ if (!ExplicitSkip[Index])
+ {
+ VisibleFrameIndices.push_back(Index);
+ }
+ }
+ if (VisibleFrameIndices.empty())
+ {
+ VisibleFrameIndices.push_back(0);
+ }
+ }
+ else
+ {
+ size_t FirstProgramIndex = Entry.Frames.size();
+ size_t BoundaryThirdPartyIndex = Entry.Frames.size();
+ for (size_t Index = 0; Index < Entry.Frames.size(); ++Index)
+ {
+ if (ExplicitSkip[Index])
+ {
+ continue;
+ }
+ if (ThirdParty[Index])
+ {
+ BoundaryThirdPartyIndex = Index;
+ continue;
+ }
+ FirstProgramIndex = Index;
+ break;
+ }
+
+ if (FirstProgramIndex == Entry.Frames.size())
+ {
+ for (size_t Index = 0; Index < Entry.Frames.size(); ++Index)
+ {
+ if (!ExplicitSkip[Index])
+ {
+ VisibleFrameIndices.push_back(Index);
+ }
+ }
+ if (VisibleFrameIndices.empty())
+ {
+ VisibleFrameIndices.push_back(0);
+ }
+ }
+ else
+ {
+ if (BoundaryThirdPartyIndex < Entry.Frames.size())
+ {
+ VisibleFrameIndices.push_back(BoundaryThirdPartyIndex);
+ Result.IncludedThirdPartyBoundary = true;
+ }
+ for (size_t Index = FirstProgramIndex; Index < Entry.Frames.size(); ++Index)
+ {
+ if (!ExplicitSkip[Index])
+ {
+ VisibleFrameIndices.push_back(Index);
+ }
+ }
+ if (VisibleFrameIndices.empty())
+ {
+ VisibleFrameIndices.push_back(FirstProgramIndex);
+ }
+ }
+ }
+
+ Result.HiddenPrefixCount = uint32_t(VisibleFrameIndices.front());
+ for (size_t FrameIndex : VisibleFrameIndices)
+ {
+ Result.Frames.push_back(
+ {.OriginalIndex = FrameIndex, .Frame = &Entry.Frames[FrameIndex], .Display = Describe(Entry.Frames[FrameIndex])});
+ }
+ return Result;
+}
+
+} // namespace zen::trace_detail
diff --git a/src/zen/trace/callstack_formatter.h b/src/zen/trace/callstack_formatter.h
new file mode 100644
index 000000000..067985f25
--- /dev/null
+++ b/src/zen/trace/callstack_formatter.h
@@ -0,0 +1,55 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#pragma once
+
+#include "symbol_resolver.h"
+#include "trace_model.h"
+
+#include <string>
+#include <vector>
+
+ZEN_THIRD_PARTY_INCLUDES_START
+#include <EASTL/hash_map.h>
+#include <EASTL/vector.h>
+ZEN_THIRD_PARTY_INCLUDES_END
+
+namespace zen::trace_detail {
+
+struct CallstackFilterOptions
+{
+ bool EnableHeuristic = true;
+ std::vector<std::string> SkipPatterns;
+};
+
+struct FilteredCallstackFrame
+{
+ size_t OriginalIndex = 0;
+ const ResolvedFrame* Frame = nullptr;
+ std::string Display;
+};
+
+struct FilteredCallstackView
+{
+ eastl::vector<FilteredCallstackFrame> Frames;
+ uint32_t HiddenPrefixCount = 0;
+ bool IncludedThirdPartyBoundary = false;
+};
+
+class CallstackFormatter
+{
+public:
+ CallstackFormatter(const TraceModel& InModel, const SymbolResolver* InSymbols);
+
+ const eastl::hash_map<uint64_t, std::string>& GetResolvedCache() const { return m_Cache; }
+
+ const CallstackEntry* FindCallstackEntry(uint32_t CallstackId) const;
+ const std::string& Describe(const ResolvedFrame& Frame);
+ FilteredCallstackView BuildView(const CallstackEntry& Entry, const CallstackFilterOptions& Options);
+
+private:
+ const TraceModel& m_Model;
+ const SymbolResolver* m_Symbols = nullptr;
+ eastl::hash_map<uint64_t, std::string> m_Cache;
+};
+
+} // namespace zen::trace_detail
diff --git a/src/zen/trace/symbol_resolver.cpp b/src/zen/trace/symbol_resolver.cpp
new file mode 100644
index 000000000..53374cd64
--- /dev/null
+++ b/src/zen/trace/symbol_resolver.cpp
@@ -0,0 +1,1631 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#include "symbol_resolver.h"
+
+#include <zencore/filesystem.h>
+#include <zencore/fmtutils.h>
+#include <zencore/logging.h>
+#include <zencore/process.h>
+#include <zencore/string.h>
+#include <zenhttp/httpclient.h>
+
+#include <algorithm>
+#include <filesystem>
+#include <mutex>
+#include <unordered_map>
+#include <vector>
+
+#if !ZEN_PLATFORM_WINDOWS
+# include <cerrno>
+# include <unistd.h>
+#endif
+
+#if ZEN_PLATFORM_WINDOWS
+
+ZEN_THIRD_PARTY_INCLUDES_START
+# include <Foundation/PDB_PointerUtil.h>
+# include <PDB.h>
+# include <PDB_CoalescedMSFStream.h>
+# include <PDB_DBIStream.h>
+# include <PDB_ImageSectionStream.h>
+# include <PDB_InfoStream.h>
+# include <PDB_ModuleInfoStream.h>
+# include <PDB_ModuleLineStream.h>
+# include <PDB_ModuleSymbolStream.h>
+# include <PDB_PublicSymbolStream.h>
+# include <PDB_RawFile.h>
+ZEN_THIRD_PARTY_INCLUDES_END
+
+# include <zencore/windows.h>
+
+ZEN_THIRD_PARTY_INCLUDES_START
+# include <DbgHelp.h>
+ZEN_THIRD_PARTY_INCLUDES_END
+
+#endif // ZEN_PLATFORM_WINDOWS
+
+namespace zen::trace_detail {
+
+//////////////////////////////////////////////////////////////////////////////
+// Null resolver (used when symbolication is off or unsupported)
+
+class NullSymbolResolver final : public SymbolResolver
+{
+public:
+ void LoadModule(const ModuleInfo&) override {}
+ std::string Resolve(uint64_t) const override { return {}; }
+};
+
+#if ZEN_PLATFORM_WINDOWS
+
+//////////////////////////////////////////////////////////////////////////////
+// Helpers shared by Windows backends
+
+static std::string
+FormatSymbol(std::string_view Name, uint64_t Displacement)
+{
+ if (Displacement == 0)
+ {
+ return std::string(Name);
+ }
+ return fmt::format("{} + 0x{:X}", Name, Displacement);
+}
+
+//////////////////////////////////////////////////////////////////////////////
+// Memory-mapped file helper
+
+namespace {
+
+ struct MappedFile
+ {
+ const void* Data = nullptr;
+ size_t Size = 0;
+ HANDLE FileHandle = INVALID_HANDLE_VALUE;
+ HANDLE MappingHandle = nullptr;
+
+ MappedFile() = default;
+ ~MappedFile() { Close(); }
+
+ bool Open(const std::filesystem::path& Path)
+ {
+ FileHandle = CreateFileW(Path.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
+ if (FileHandle == INVALID_HANDLE_VALUE)
+ {
+ return false;
+ }
+
+ LARGE_INTEGER FileSize;
+ if (!GetFileSizeEx(FileHandle, &FileSize) || FileSize.QuadPart == 0)
+ {
+ Close();
+ return false;
+ }
+
+ MappingHandle = CreateFileMappingW(FileHandle, nullptr, PAGE_READONLY, 0, 0, nullptr);
+ if (MappingHandle == nullptr)
+ {
+ Close();
+ return false;
+ }
+
+ Data = MapViewOfFile(MappingHandle, FILE_MAP_READ, 0, 0, 0);
+ if (Data == nullptr)
+ {
+ Close();
+ return false;
+ }
+
+ Size = size_t(FileSize.QuadPart);
+ return true;
+ }
+
+ void Close()
+ {
+ if (Data != nullptr)
+ {
+ UnmapViewOfFile(Data);
+ Data = nullptr;
+ }
+ if (MappingHandle != nullptr)
+ {
+ CloseHandle(MappingHandle);
+ MappingHandle = nullptr;
+ }
+ if (FileHandle != INVALID_HANDLE_VALUE)
+ {
+ CloseHandle(FileHandle);
+ FileHandle = INVALID_HANDLE_VALUE;
+ }
+ Size = 0;
+ }
+
+ MappedFile(const MappedFile&) = delete;
+ MappedFile& operator=(const MappedFile&) = delete;
+ };
+
+ // Format an ImageId (16-byte GUID + 4-byte Age) as the hex key used in
+ // symbol server URLs: <GUID_no_dashes><Age_hex>, e.g. "A1B2C3...1".
+ std::string FormatImageIdKey(const eastl::vector<uint8_t>& ImageId)
+ {
+ if (ImageId.size() < 20)
+ {
+ return {};
+ }
+
+ // GUID bytes are stored as {Data1 LE, Data2 LE, Data3 LE, Data4[8]}.
+ // The symbol server key encodes Data1/2/3 as big-endian hex.
+ const uint8_t* G = ImageId.data();
+
+ uint32_t Data1;
+ uint16_t Data2;
+ uint16_t Data3;
+ memcpy(&Data1, G + 0, 4);
+ memcpy(&Data2, G + 4, 2);
+ memcpy(&Data3, G + 6, 2);
+
+ uint32_t Age;
+ memcpy(&Age, ImageId.data() + 16, 4);
+
+ return fmt::format("{:08X}{:04X}{:04X}{:02X}{:02X}{:02X}{:02X}{:02X}{:02X}{:02X}{:02X}{:x}",
+ Data1,
+ Data2,
+ Data3,
+ G[8],
+ G[9],
+ G[10],
+ G[11],
+ G[12],
+ G[13],
+ G[14],
+ G[15],
+ Age);
+ }
+
+ // PdbName originates from module metadata in an untrusted trace file and is used
+ // to build both a filesystem path and an HTTP request path. Restrict it to a safe
+ // subset so a malicious trace cannot traverse out of the cache dir, inject URL
+ // syntax, or trip cross-platform path parsing quirks (e.g. `\` is a separator on
+ // Windows but not POSIX, so filename() doesn't always strip it).
+ bool IsSafePdbName(std::string_view Name)
+ {
+ constexpr AsciiSet SafeChars(
+ "abcdefghijklmnopqrstuvwxyz"
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+ "0123456789"
+ "._-+");
+
+ if (Name.empty() || Name.size() > 255 || Name == "." || Name == "..")
+ {
+ return false;
+ }
+ return AsciiSet::HasOnly(Name, SafeChars);
+ }
+
+ const std::filesystem::path& GetSymbolCacheDir()
+ {
+ // Use %TEMP%/zen-symbols as the default cache location.
+ static const std::filesystem::path s_CacheDir = [] {
+ std::filesystem::path TempDir = std::filesystem::temp_directory_path();
+ return TempDir / "zen-symbols";
+ }();
+ return s_CacheDir;
+ }
+
+ // Try to download a PDB from a symbol server URL.
+ // Returns the local cache path on success, empty path on failure.
+ std::filesystem::path DownloadPdb(std::string_view ServerUrl,
+ std::string_view PdbName,
+ const std::string& ImageIdKey,
+ const std::filesystem::path& CacheDir)
+ {
+ // Cache path mirrors the server structure
+ std::filesystem::path CachePath = CacheDir / PdbName / ImageIdKey / PdbName;
+
+ // Already cached?
+ std::error_code Ec;
+ if (std::filesystem::exists(CachePath, Ec))
+ {
+ return CachePath;
+ }
+
+ ZEN_INFO("Downloading {} from symbol server...", PdbName);
+
+ try
+ {
+ std::string RequestPath = fmt::format("/{}/{}/{}", PdbName, ImageIdKey, PdbName);
+
+ zen::HttpClientSettings Settings;
+ Settings.Timeout = std::chrono::milliseconds(30000);
+ Settings.ConnectTimeout = std::chrono::milliseconds(5000);
+ Settings.FollowRedirects = true;
+ Settings.ExpectedErrorCodes = {zen::HttpResponseCode::NotFound};
+
+ zen::HttpClient Http(ServerUrl, Settings);
+
+ zen::HttpClient::Response Response = Http.Get(RequestPath);
+
+ if (!Response)
+ {
+ ZEN_DEBUG("Symbol server: {} not found (HTTP {})", PdbName, int(Response.StatusCode));
+ return {};
+ }
+
+ // Write to cache using zencore file I/O
+ std::filesystem::create_directories(CachePath.parent_path(), Ec);
+ zen::WriteFile(CachePath, Response.ResponsePayload);
+
+ ZEN_INFO("Cached {} ({})", PdbName, zen::NiceBytes(Response.ResponsePayload.GetSize()));
+ return CachePath;
+ }
+ catch (const std::exception& Ex)
+ {
+ ZEN_WARN("Symbol server download failed for {}: {}", PdbName, Ex.what());
+ return {};
+ }
+ }
+
+ // Parse _NT_SYMBOL_PATH to extract symbol server URLs.
+ // Format: "srv*<cache>*<url>" or "symsrv*symsrv.dll*<cache>*<url>" or just a URL.
+ // Returns a list of server URLs to try.
+ const std::vector<std::string>& ParseSymbolPath()
+ {
+ static const std::vector<std::string> s_Servers = [] {
+ std::vector<std::string> Servers;
+
+ const char* EnvPath = std::getenv("_NT_SYMBOL_PATH");
+ if (EnvPath == nullptr || EnvPath[0] == '\0')
+ {
+ // Default to Microsoft public symbol server
+ Servers.push_back("https://msdl.microsoft.com/download/symbols");
+ return Servers;
+ }
+
+ std::string_view Path(EnvPath);
+
+ // Split on ';' for multiple entries
+ while (!Path.empty())
+ {
+ size_t Semi = Path.find(';');
+ std::string_view Entry = (Semi != std::string_view::npos) ? Path.substr(0, Semi) : Path;
+ Path = (Semi != std::string_view::npos) ? Path.substr(Semi + 1) : std::string_view{};
+
+ // Look for srv* or symsrv* prefix — the last '*'-delimited token is the server URL.
+ if (Entry.substr(0, 4) == "srv*" || Entry.substr(0, 7) == "symsrv*")
+ {
+ size_t LastStar = Entry.rfind('*');
+ if (LastStar != std::string_view::npos && LastStar + 1 < Entry.size())
+ {
+ std::string_view Url = Entry.substr(LastStar + 1);
+ if (Url.substr(0, 4) == "http")
+ {
+ Servers.emplace_back(Url);
+ }
+ }
+ }
+ }
+
+ if (Servers.empty())
+ {
+ Servers.push_back("https://msdl.microsoft.com/download/symbols");
+ }
+
+ return Servers;
+ }();
+ return s_Servers;
+ }
+
+ // Copy a local PDB into the symbol cache so that future analysis of traces
+ // from this build succeeds even after the binary is recompiled.
+ void CacheLocalPdb(const std::filesystem::path& PdbPath,
+ std::string_view PdbName,
+ const std::string& ImageIdKey,
+ const std::filesystem::path& CacheDir)
+ {
+ std::filesystem::path CachePath = CacheDir / PdbName / ImageIdKey / PdbName;
+ std::error_code Ec;
+ if (std::filesystem::exists(CachePath, Ec))
+ {
+ return;
+ }
+
+ std::filesystem::create_directories(CachePath.parent_path(), Ec);
+ if (Ec)
+ {
+ return;
+ }
+
+ std::filesystem::copy_file(PdbPath, CachePath, std::filesystem::copy_options::skip_existing, Ec);
+ if (!Ec)
+ {
+ uint64_t Size = std::filesystem::file_size(PdbPath, Ec);
+ ZEN_INFO("Cached local PDB {} ({})", PdbName, zen::NiceBytes(Size));
+ }
+ }
+
+ // Look for a PDB in the local symbol cache or download from symbol servers.
+ // Returns the cache path on success, empty path on failure.
+ std::filesystem::path FindPdbInCacheOrServer(std::string_view PdbName,
+ const std::string& ImageIdKey,
+ const std::filesystem::path& CacheDir)
+ {
+ if (ImageIdKey.empty())
+ {
+ return {};
+ }
+
+ // Check local cache first (includes previously cached local PDBs and
+ // earlier symbol server downloads).
+ std::filesystem::path CachePath = CacheDir / PdbName / ImageIdKey / PdbName;
+ std::error_code Ec;
+ if (std::filesystem::exists(CachePath, Ec))
+ {
+ return CachePath;
+ }
+
+ // Try symbol servers
+ const std::vector<std::string>& Servers = ParseSymbolPath();
+ for (const std::string& Server : Servers)
+ {
+ std::filesystem::path Downloaded = DownloadPdb(Server, PdbName, ImageIdKey, CacheDir);
+ if (!Downloaded.empty())
+ {
+ return Downloaded;
+ }
+ }
+
+ return {};
+ }
+
+} // namespace
+
+//////////////////////////////////////////////////////////////////////////////
+// RawPdb backend — reads PDB files directly
+
+class PdbSymbolResolver final : public SymbolResolver
+{
+public:
+ void LoadModule(const ModuleInfo& Module) override;
+ std::string Resolve(uint64_t Address) const override;
+
+private:
+ struct FunctionEntry
+ {
+ uint64_t Address;
+ uint32_t Size;
+ std::string Name;
+ };
+
+ struct LineEntry
+ {
+ uint64_t Address;
+ uint32_t CodeSize;
+ uint32_t Line;
+ std::string File; // shortened: basename only
+ };
+
+ std::vector<FunctionEntry> m_Functions;
+ std::vector<LineEntry> m_Lines;
+};
+
+void
+PdbSymbolResolver::LoadModule(const ModuleInfo& Module)
+{
+ if (Module.FullPath.empty() || Module.Base == 0)
+ {
+ return;
+ }
+
+ std::string ImageIdKey = FormatImageIdKey(Module.ImageId);
+ std::string PdbName = std::filesystem::path(Module.FullPath).filename().replace_extension(".pdb").string();
+ const std::filesystem::path& CacheDir = GetSymbolCacheDir();
+
+ if (!IsSafePdbName(PdbName))
+ {
+ ZEN_WARN("Rejecting unsafe PDB name from trace: '{}'", PdbName);
+ return;
+ }
+
+ // Try local PDB first (next to the binary)
+ std::filesystem::path PdbPath(Module.FullPath);
+ PdbPath.replace_extension(".pdb");
+
+ std::error_code Ec;
+ bool FromLocal = false;
+
+ if (std::filesystem::exists(PdbPath, Ec))
+ {
+ FromLocal = true;
+ }
+ else
+ {
+ // Try symbol cache / symbol server download
+ PdbPath = FindPdbInCacheOrServer(PdbName, ImageIdKey, CacheDir);
+ if (PdbPath.empty())
+ {
+ ZEN_DEBUG("PDB not found locally or on symbol server: {}", PdbName);
+ return;
+ }
+ }
+
+ MappedFile File;
+ if (!File.Open(PdbPath))
+ {
+ ZEN_DEBUG("Failed to open PDB: {}", PdbPath.string());
+ return;
+ }
+
+ if (PDB::ValidateFile(File.Data, File.Size) != PDB::ErrorCode::Success)
+ {
+ ZEN_DEBUG("Invalid PDB file: {}", PdbPath.string());
+ return;
+ }
+
+ PDB::RawFile RawFile = PDB::CreateRawFile(File.Data);
+ PDB::InfoStream PdbInfoStream(RawFile);
+
+ // Verify the PDB matches the traced module by comparing GUID + Age.
+ // The trace stores ImageId as 16 bytes GUID followed by 4 bytes Age.
+ if (Module.ImageId.size() >= 20)
+ {
+ const PDB::Header* PdbHeader = PdbInfoStream.GetHeader();
+
+ // Only compare the GUID, not the age. The symbol server may return a
+ // PDB with a higher age (from incremental linking) which is compatible.
+ if (memcmp(&PdbHeader->guid, Module.ImageId.data(), 16) != 0)
+ {
+ if (FromLocal)
+ {
+ // The local PDB no longer matches — the binary was recompiled
+ // since the trace was taken. Try the symbol cache / servers for
+ // the original PDB.
+ File.Close();
+ PdbPath = FindPdbInCacheOrServer(PdbName, ImageIdKey, CacheDir);
+ if (PdbPath.empty())
+ {
+ ZEN_WARN("PDB mismatch for {} — binary was recompiled and no cached symbols available", Module.Name);
+ return;
+ }
+
+ FromLocal = false;
+
+ if (!File.Open(PdbPath))
+ {
+ ZEN_DEBUG("Failed to open cached PDB: {}", PdbPath.string());
+ return;
+ }
+
+ if (PDB::ValidateFile(File.Data, File.Size) != PDB::ErrorCode::Success)
+ {
+ ZEN_DEBUG("Invalid cached PDB: {}", PdbPath.string());
+ return;
+ }
+
+ RawFile = PDB::CreateRawFile(File.Data);
+ PdbInfoStream = PDB::InfoStream(RawFile);
+
+ const PDB::Header* CachedHeader = PdbInfoStream.GetHeader();
+ if (memcmp(&CachedHeader->guid, Module.ImageId.data(), 16) != 0)
+ {
+ ZEN_WARN("PDB GUID mismatch for {} — skipping", Module.Name);
+ return;
+ }
+ }
+ else
+ {
+ ZEN_WARN("PDB GUID mismatch for {} — skipping", Module.Name);
+ return;
+ }
+ }
+ }
+
+ // Cache the local PDB so that future analysis of traces from this build
+ // succeeds even after the binary is recompiled.
+ if (FromLocal && !ImageIdKey.empty())
+ {
+ CacheLocalPdb(PdbPath, PdbName, ImageIdKey, CacheDir);
+ }
+
+ if (PDB::HasValidDBIStream(RawFile) != PDB::ErrorCode::Success)
+ {
+ return;
+ }
+
+ const PDB::DBIStream DbiStream = PDB::CreateDBIStream(RawFile);
+ if (DbiStream.HasValidImageSectionStream(RawFile) != PDB::ErrorCode::Success)
+ {
+ return;
+ }
+
+ const PDB::ImageSectionStream ImageSections = DbiStream.CreateImageSectionStream(RawFile);
+ uint64_t ModuleBase = Module.Base;
+ uint32_t SkippedModules = 0;
+ size_t FunctionCountBeforeModuleSymbols = m_Functions.size();
+
+ // Collect functions from module symbol streams (S_GPROC32 / S_LPROC32)
+ {
+ const PDB::ModuleInfoStream ModInfoStream = DbiStream.CreateModuleInfoStream(RawFile);
+ const PDB::ArrayView<PDB::ModuleInfoStream::Module> Modules = ModInfoStream.GetModules();
+ for (const PDB::ModuleInfoStream::Module& Mod : Modules)
+ {
+ if (!Mod.HasSymbolStream())
+ {
+ ++SkippedModules;
+ continue;
+ }
+
+ const PDB::ModuleSymbolStream SymStream = Mod.CreateSymbolStream(RawFile);
+
+ SymStream.ForEachSymbol([&](const PDB::CodeView::DBI::Record* Record) {
+ using Kind = PDB::CodeView::DBI::SymbolRecordKind;
+ const Kind K = Record->header.kind;
+ const auto& Data = Record->data;
+
+ if (K == Kind::S_GPROC32 || K == Kind::S_LPROC32 || K == Kind::S_GPROC32_ID || K == Kind::S_LPROC32_ID)
+ {
+ uint32_t Rva = ImageSections.ConvertSectionOffsetToRVA(Data.S_GPROC32.section, Data.S_GPROC32.offset);
+ if (Rva != 0)
+ {
+ m_Functions.push_back({ModuleBase + Rva, Data.S_GPROC32.codeSize, Data.S_GPROC32.name});
+ }
+ }
+ });
+ }
+ }
+
+ // Public symbols as fallback only when module symbol streams did not yield any
+ // functions. Building the coalesced symbol-record stream is expensive and can
+ // allocate tens of megabytes for large PDBs.
+ if (FunctionCountBeforeModuleSymbols == m_Functions.size() && DbiStream.HasValidPublicSymbolStream(RawFile) == PDB::ErrorCode::Success)
+ {
+ const PDB::PublicSymbolStream PubStream = DbiStream.CreatePublicSymbolStream(RawFile);
+ const PDB::CoalescedMSFStream SymRecords = DbiStream.CreateSymbolRecordStream(RawFile);
+
+ for (const PDB::HashRecord& Hash : PubStream.GetRecords())
+ {
+ const PDB::CodeView::DBI::Record* Record = SymRecords.GetDataAtOffset<PDB::CodeView::DBI::Record>(Hash.offset);
+ if (Record->header.kind == PDB::CodeView::DBI::SymbolRecordKind::S_PUB32)
+ {
+ uint32_t Rva = ImageSections.ConvertSectionOffsetToRVA(Record->data.S_PUB32.section, Record->data.S_PUB32.offset);
+ if (Rva != 0)
+ {
+ m_Functions.push_back({ModuleBase + Rva, 0, Record->data.S_PUB32.name});
+ }
+ }
+ }
+ }
+
+ // Collect line information from module line streams
+ if (PdbInfoStream.HasNamesStream())
+ {
+ const PDB::NamesStream NamesStream = PdbInfoStream.CreateNamesStream(RawFile);
+ const PDB::ModuleInfoStream ModInfoStream2 = DbiStream.CreateModuleInfoStream(RawFile);
+
+ for (const PDB::ModuleInfoStream::Module& Mod : ModInfoStream2.GetModules())
+ {
+ if (!Mod.HasLineStream())
+ {
+ continue;
+ }
+
+ const PDB::ModuleLineStream LineStream = Mod.CreateLineStream(RawFile);
+
+ // Two passes: first find the checksums section, then process lines.
+ const PDB::CodeView::DBI::FileChecksumHeader* ModuleChecksumBase = nullptr;
+
+ LineStream.ForEachSection([&](const PDB::CodeView::DBI::LineSection* Section) {
+ if (Section->header.kind == PDB::CodeView::DBI::DebugSubsectionKind::S_FILECHECKSUMS)
+ {
+ ModuleChecksumBase = &Section->checksumHeader;
+ }
+ });
+
+ if (ModuleChecksumBase == nullptr)
+ {
+ continue;
+ }
+
+ LineStream.ForEachSection([&](const PDB::CodeView::DBI::LineSection* Section) {
+ if (Section->header.kind != PDB::CodeView::DBI::DebugSubsectionKind::S_LINES)
+ {
+ return;
+ }
+
+ uint16_t SecIdx = Section->linesHeader.sectionIndex;
+ uint32_t SecOff = Section->linesHeader.sectionOffset;
+
+ LineStream.ForEachLinesBlock(Section,
+ [&](const PDB::CodeView::DBI::LinesFileBlockHeader* Block,
+ const PDB::CodeView::DBI::Line* Lines,
+ const PDB::CodeView::DBI::Column*) {
+ if (Block->numLines == 0)
+ {
+ return;
+ }
+
+ // Resolve filename for this block
+ const auto* Checksum = PDB::Pointer::Offset<const PDB::CodeView::DBI::FileChecksumHeader*>(
+ ModuleChecksumBase,
+ Block->fileChecksumOffset);
+ const char* FullFile = NamesStream.GetFilename(Checksum->filenameOffset);
+
+ // Extract basename
+ std::string_view FileView(FullFile);
+ size_t Cut = FileView.find_last_of("\\/");
+ std::string Basename(Cut != std::string_view::npos ? FileView.substr(Cut + 1) : FileView);
+
+ for (uint32_t I = 0; I < Block->numLines; ++I)
+ {
+ uint32_t Rva =
+ ImageSections.ConvertSectionOffsetToRVA(SecIdx, SecOff + Lines[I].offset);
+ if (Rva == 0)
+ {
+ continue;
+ }
+
+ uint32_t CodeSize = 0;
+ if (I + 1 < Block->numLines)
+ {
+ CodeSize = Lines[I + 1].offset - Lines[I].offset;
+ }
+ else
+ {
+ CodeSize = Section->linesHeader.codeSize - Lines[I].offset;
+ }
+
+ m_Lines.push_back({ModuleBase + Rva, CodeSize, Lines[I].linenumStart, Basename});
+ }
+ });
+ });
+ }
+ }
+
+ std::sort(m_Functions.begin(), m_Functions.end(), [](const FunctionEntry& A, const FunctionEntry& B) { return A.Address < B.Address; });
+
+ std::sort(m_Lines.begin(), m_Lines.end(), [](const LineEntry& A, const LineEntry& B) { return A.Address < B.Address; });
+
+ if (SkippedModules > 0)
+ {
+ ZEN_INFO("Loaded {} symbols, {} line records from {} ({} modules without embedded debug info)",
+ m_Functions.size(),
+ m_Lines.size(),
+ Module.Name,
+ SkippedModules);
+ }
+ else
+ {
+ ZEN_INFO("Loaded {} symbols, {} line records from {}", m_Functions.size(), m_Lines.size(), Module.Name);
+ }
+}
+
+std::string
+PdbSymbolResolver::Resolve(uint64_t Address) const
+{
+ if (m_Functions.empty())
+ {
+ return {};
+ }
+
+ // Resolve function name
+ auto FnIt = std::upper_bound(m_Functions.begin(), m_Functions.end(), Address, [](uint64_t Addr, const FunctionEntry& E) {
+ return Addr < E.Address;
+ });
+
+ if (FnIt == m_Functions.begin())
+ {
+ return {};
+ }
+
+ --FnIt;
+
+ if (FnIt->Size > 0 && Address >= FnIt->Address + FnIt->Size)
+ {
+ return {};
+ }
+
+ std::string Result = FormatSymbol(FnIt->Name, Address - FnIt->Address);
+
+ // Resolve file:line
+ if (!m_Lines.empty())
+ {
+ auto LineIt =
+ std::upper_bound(m_Lines.begin(), m_Lines.end(), Address, [](uint64_t Addr, const LineEntry& E) { return Addr < E.Address; });
+
+ if (LineIt != m_Lines.begin())
+ {
+ --LineIt;
+ if (LineIt->CodeSize == 0 || Address < LineIt->Address + LineIt->CodeSize)
+ {
+ Result += fmt::format(" [{}:{}]", LineIt->File, LineIt->Line);
+ }
+ }
+ }
+
+ return Result;
+}
+
+//////////////////////////////////////////////////////////////////////////////
+// DbgHelp backend — uses Windows symbol API, supports _NT_SYMBOL_PATH
+
+class DbgHelpSymbolResolver final : public SymbolResolver
+{
+public:
+ DbgHelpSymbolResolver();
+ ~DbgHelpSymbolResolver() override;
+
+ void LoadModule(const ModuleInfo& Module) override;
+ std::string Resolve(uint64_t Address) const override;
+
+private:
+ // Map trace addresses to DbgHelp addresses when the loaded base differs.
+ struct ModuleMapping
+ {
+ uint64_t TraceBase;
+ uint64_t TraceEnd;
+ int64_t Delta; // DbgHelpBase - TraceBase
+ };
+
+ HANDLE m_Process = nullptr;
+ std::vector<ModuleMapping> m_Mappings;
+ // DbgHelp is not thread-safe; its API functions require serialized access. This
+ // mutex covers every DbgHelp call (SymInitialize/SymLoadModuleExW/SymFromAddr/
+ // SymGetLineFromAddr64) and therefore serializes all parallel symbol lookups in
+ // trace_analyze. For workloads where lookup throughput matters, prefer
+ // PdbSymbolResolver, which parses PDBs directly and is lock-free per-module.
+ mutable std::mutex m_Mutex;
+};
+
+DbgHelpSymbolResolver::DbgHelpSymbolResolver()
+{
+ std::lock_guard Lock(m_Mutex);
+
+ // Use a unique pseudo-handle so we don't conflict with the runtime
+ // symbol handler used by callstack.cpp / crashhandler.cpp.
+ m_Process = reinterpret_cast<HANDLE>(static_cast<uintptr_t>(0xDEAD0042));
+
+ // NULL search path lets DbgHelp use _NT_SYMBOL_PATH and its defaults.
+ if (!SymInitialize(m_Process, nullptr, FALSE))
+ {
+ ZEN_WARN("DbgHelp: SymInitialize failed (error {})", GetLastError());
+ m_Process = nullptr;
+ }
+}
+
+DbgHelpSymbolResolver::~DbgHelpSymbolResolver()
+{
+ std::lock_guard Lock(m_Mutex);
+
+ if (m_Process != nullptr)
+ {
+ SymCleanup(m_Process);
+ }
+}
+
+void
+DbgHelpSymbolResolver::LoadModule(const ModuleInfo& Module)
+{
+ std::lock_guard Lock(m_Mutex);
+
+ if (m_Process == nullptr || Module.FullPath.empty() || Module.Base == 0)
+ {
+ return;
+ }
+
+ std::filesystem::path ModulePath(Module.FullPath);
+ std::wstring WidePath = ModulePath.wstring();
+
+ DWORD64 LoadedBase = SymLoadModuleExW(m_Process, nullptr, WidePath.c_str(), nullptr, Module.Base, Module.Size, nullptr, 0);
+
+ if (LoadedBase == 0)
+ {
+ DWORD Err = GetLastError();
+ if (Err != ERROR_SUCCESS)
+ {
+ ZEN_DEBUG("DbgHelp: failed to load {}: error {}", Module.Name, Err);
+ }
+ return;
+ }
+
+ int64_t Delta = int64_t(LoadedBase) - int64_t(Module.Base);
+ if (Delta != 0)
+ {
+ ZEN_DEBUG("DbgHelp: {} loaded at 0x{:X} (trace base 0x{:X}, delta {:+})", Module.Name, LoadedBase, Module.Base, Delta);
+ }
+
+ uint64_t TraceEnd = Module.Base + (Module.Size > 0 ? Module.Size : 0x1000000);
+ m_Mappings.push_back({Module.Base, TraceEnd, Delta});
+
+ ZEN_INFO("DbgHelp: loaded symbols for {}", Module.Name);
+}
+
+std::string
+DbgHelpSymbolResolver::Resolve(uint64_t Address) const
+{
+ std::lock_guard Lock(m_Mutex);
+
+ if (m_Process == nullptr)
+ {
+ return {};
+ }
+
+ // Translate the trace address to the DbgHelp address space
+ uint64_t DbgAddr = Address;
+ for (const ModuleMapping& M : m_Mappings)
+ {
+ if (Address >= M.TraceBase && Address < M.TraceEnd)
+ {
+ DbgAddr = uint64_t(int64_t(Address) + M.Delta);
+ break;
+ }
+ }
+
+ alignas(SYMBOL_INFO) char Buffer[sizeof(SYMBOL_INFO) + MAX_SYM_NAME];
+ SYMBOL_INFO* SymInfo = reinterpret_cast<SYMBOL_INFO*>(Buffer);
+ SymInfo->SizeOfStruct = sizeof(SYMBOL_INFO);
+ SymInfo->MaxNameLen = MAX_SYM_NAME;
+
+ DWORD64 Displacement = 0;
+ if (!SymFromAddr(m_Process, DbgAddr, &Displacement, SymInfo))
+ {
+ return {};
+ }
+
+ std::string Result = FormatSymbol(std::string_view(SymInfo->Name, SymInfo->NameLen), Displacement);
+
+ IMAGEHLP_LINE64 LineInfo = {};
+ LineInfo.SizeOfStruct = sizeof(IMAGEHLP_LINE64);
+ DWORD LineDisplacement = 0;
+ if (SymGetLineFromAddr64(m_Process, DbgAddr, &LineDisplacement, &LineInfo))
+ {
+ std::string_view FileView(LineInfo.FileName);
+ size_t Cut = FileView.find_last_of("\\/");
+ std::string_view Basename = (Cut != std::string_view::npos) ? FileView.substr(Cut + 1) : FileView;
+ Result += fmt::format(" [{}:{}]", Basename, LineInfo.LineNumber);
+ }
+
+ return Result;
+}
+
+#endif // ZEN_PLATFORM_WINDOWS
+
+//////////////////////////////////////////////////////////////////////////////
+// Shared helpers for subprocess-based backends
+
+namespace {
+
+ // CreateProc parses the command line as space-separated tokens, so argv[0]
+ // must be quoted if the resolved executable path contains spaces (e.g. an
+ // Xcode toolchain location on macOS).
+ std::string QuoteIfNeeded(std::string_view Path)
+ {
+ if (Path.find(' ') == std::string_view::npos)
+ {
+ return std::string(Path);
+ }
+ return fmt::format("\"{}\"", Path);
+ }
+
+} // namespace
+
+//////////////////////////////////////////////////////////////////////////////
+// llvm-symbolizer backend — cross-platform, shells out to `llvm-symbolizer`
+// and speaks its interactive protocol over pipes.
+//
+// Protocol (one request / response):
+// We write: "<path-to-binary> 0x<relative-address>\n"
+// It replies: "FunctionName\n"
+// "file:line:col\n"
+// "\n" <-- blank line terminates the record
+//
+// Launch flags:
+// --demangle demangle C++ names (default, but explicit)
+// --output-style=LLVM stable two-line format described above
+// --functions=linkage keep template arguments visible
+// --relative-address treat the address as an offset from module base
+// --inlining=false emit one frame per address (no inline expansion)
+
+class LlvmSymbolizerResolver final : public SymbolResolver
+{
+public:
+ LlvmSymbolizerResolver() = default;
+ ~LlvmSymbolizerResolver() override;
+
+ void LoadModule(const ModuleInfo& Module) override;
+ std::string Resolve(uint64_t Address) const override;
+
+private:
+ struct Module
+ {
+ std::string FullPath;
+ uint64_t Base = 0;
+ uint64_t End = 0;
+ };
+
+ const Module* FindModule(uint64_t Address) const;
+ bool EnsureProcess() const;
+ bool ReadLine(std::string& Out) const;
+ std::string DoQuery(const Module& M, uint64_t RelAddress) const;
+
+ std::vector<Module> m_Modules;
+
+ // Subprocess + IO state. All accesses serialized under m_Mutex.
+ mutable std::mutex m_Mutex;
+ mutable bool m_Attempted = false;
+ mutable bool m_Alive = false;
+ mutable zen::ProcessHandle m_Process;
+ mutable zen::StdinPipeHandles m_StdinPipe;
+ mutable zen::StdoutPipeHandles m_StdoutPipe;
+ mutable std::string m_ReadBuffer;
+
+ // Cache resolved addresses (same mutex).
+ mutable std::unordered_map<uint64_t, std::string> m_Cache;
+};
+
+LlvmSymbolizerResolver::~LlvmSymbolizerResolver()
+{
+ std::lock_guard Lock(m_Mutex);
+ if (m_Alive)
+ {
+ // Closing stdin lets llvm-symbolizer exit cleanly on EOF.
+ m_StdinPipe.CloseWriteEnd();
+ m_Process.Wait(2000);
+ if (m_Process.IsRunning())
+ {
+ m_Process.Terminate(0);
+ }
+ }
+}
+
+void
+LlvmSymbolizerResolver::LoadModule(const ModuleInfo& Mod)
+{
+ if (Mod.FullPath.empty() || Mod.Base == 0)
+ {
+ return;
+ }
+
+ // llvm-symbolizer auto-discovers adjacent debug info (Foo.dSYM on Mac,
+ // .gnu_debuglink / build-id sources on Linux, Foo.pdb on Windows). If the
+ // binary itself isn't present locally, there's nothing we can do.
+ std::error_code Ec;
+ if (!std::filesystem::exists(Mod.FullPath, Ec))
+ {
+ ZEN_DEBUG("llvm-symbolizer: binary not found for {} at {}", Mod.Name, Mod.FullPath);
+ return;
+ }
+
+ uint64_t End = Mod.Base + (Mod.Size > 0 ? Mod.Size : 0x1000000);
+
+ std::lock_guard Lock(m_Mutex);
+ m_Modules.push_back({Mod.FullPath, Mod.Base, End});
+ ZEN_INFO("llvm-symbolizer: registered {} [0x{:X}..0x{:X})", Mod.Name, Mod.Base, End);
+}
+
+std::string
+LlvmSymbolizerResolver::Resolve(uint64_t Address) const
+{
+ std::lock_guard Lock(m_Mutex);
+
+ auto CacheIt = m_Cache.find(Address);
+ if (CacheIt != m_Cache.end())
+ {
+ return CacheIt->second;
+ }
+
+ const Module* M = FindModule(Address);
+ if (M == nullptr)
+ {
+ m_Cache.emplace(Address, std::string{});
+ return {};
+ }
+
+ std::string Result = DoQuery(*M, Address - M->Base);
+ m_Cache.emplace(Address, Result);
+ return Result;
+}
+
+const LlvmSymbolizerResolver::Module*
+LlvmSymbolizerResolver::FindModule(uint64_t Address) const
+{
+ for (const Module& M : m_Modules)
+ {
+ if (Address >= M.Base && Address < M.End)
+ {
+ return &M;
+ }
+ }
+ return nullptr;
+}
+
+bool
+LlvmSymbolizerResolver::EnsureProcess() const
+{
+ if (m_Attempted)
+ {
+ return m_Alive;
+ }
+ m_Attempted = true;
+
+ std::filesystem::path Executable = SearchPathForExecutable("llvm-symbolizer");
+
+ if (!CreateStdinPipe(m_StdinPipe) || !CreateStdoutPipe(m_StdoutPipe))
+ {
+ ZEN_WARN("llvm-symbolizer: failed to create pipes");
+ return false;
+ }
+
+ // Build the command line. CommandLine begins with the executable name (arg[0]).
+ std::string CommandLine = fmt::format("{} --demangle --output-style=LLVM --functions=linkage --relative-address --inlining=false",
+ QuoteIfNeeded(Executable.string()));
+
+ CreateProcOptions Options;
+ Options.StdinPipe = &m_StdinPipe;
+ Options.StdoutPipe = &m_StdoutPipe;
+
+ CreateProcResult Handle = CreateProc(Executable, CommandLine, Options);
+
+#if ZEN_PLATFORM_WINDOWS
+ if (Handle == nullptr)
+#else
+ if (Handle <= 0)
+#endif
+ {
+ ZEN_WARN("llvm-symbolizer: failed to launch '{}' - install LLVM or add to PATH", Executable.string());
+ m_StdinPipe.Close();
+ m_StdoutPipe.Close();
+ return false;
+ }
+
+#if ZEN_PLATFORM_WINDOWS
+ m_Process.Initialize(Handle);
+#else
+ std::error_code Ec;
+ m_Process.Initialize(int(Handle), Ec);
+ if (Ec)
+ {
+ ZEN_WARN("llvm-symbolizer: ProcessHandle init failed: {}", Ec.message());
+ m_StdinPipe.Close();
+ m_StdoutPipe.Close();
+ return false;
+ }
+#endif
+
+ // Close the child-side handles in the parent.
+ m_StdinPipe.CloseReadEnd();
+ m_StdoutPipe.CloseWriteEnd();
+
+ m_Alive = true;
+ return true;
+}
+
+bool
+LlvmSymbolizerResolver::ReadLine(std::string& Out) const
+{
+ // Search for a newline already in the buffer; if not, read more.
+ for (;;)
+ {
+ size_t NewlinePos = m_ReadBuffer.find('\n');
+ if (NewlinePos != std::string::npos)
+ {
+ Out.assign(m_ReadBuffer, 0, NewlinePos);
+ m_ReadBuffer.erase(0, NewlinePos + 1);
+ // Trim a trailing \r (in case of CRLF line endings).
+ if (!Out.empty() && Out.back() == '\r')
+ {
+ Out.pop_back();
+ }
+ return true;
+ }
+
+ char Buffer[1024];
+#if ZEN_PLATFORM_WINDOWS
+ DWORD BytesRead = 0;
+ if (!::ReadFile(m_StdoutPipe.ReadHandle, Buffer, sizeof(Buffer), &BytesRead, nullptr) || BytesRead == 0)
+ {
+ return false;
+ }
+ m_ReadBuffer.append(Buffer, BytesRead);
+#else
+ ssize_t BytesRead = ::read(m_StdoutPipe.ReadFd, Buffer, sizeof(Buffer));
+ if (BytesRead <= 0)
+ {
+ if (BytesRead < 0 && errno == EINTR)
+ {
+ continue;
+ }
+ return false;
+ }
+ m_ReadBuffer.append(Buffer, static_cast<size_t>(BytesRead));
+#endif
+ }
+}
+
+std::string
+LlvmSymbolizerResolver::DoQuery(const Module& M, uint64_t RelAddress) const
+{
+ if (!EnsureProcess())
+ {
+ return {};
+ }
+
+ // Write "<path> 0x<addr>\n". Paths with spaces must be quoted for llvm-symbolizer
+ // interactive input; it accepts double quotes.
+ std::string Line;
+ if (M.FullPath.find(' ') != std::string::npos)
+ {
+ Line = fmt::format("\"{}\" 0x{:X}\n", M.FullPath, RelAddress);
+ }
+ else
+ {
+ Line = fmt::format("{} 0x{:X}\n", M.FullPath, RelAddress);
+ }
+
+#if ZEN_PLATFORM_WINDOWS
+ DWORD BytesWritten = 0;
+ if (!::WriteFile(m_StdinPipe.WriteHandle, Line.data(), static_cast<DWORD>(Line.size()), &BytesWritten, nullptr) ||
+ BytesWritten != Line.size())
+ {
+ ZEN_WARN("llvm-symbolizer: write failed, disabling backend");
+ m_Alive = false;
+ return {};
+ }
+#else
+ const char* Ptr = Line.data();
+ size_t Remaining = Line.size();
+ while (Remaining > 0)
+ {
+ ssize_t N = ::write(m_StdinPipe.WriteFd, Ptr, Remaining);
+ if (N <= 0)
+ {
+ if (N < 0 && errno == EINTR)
+ {
+ continue;
+ }
+ ZEN_WARN("llvm-symbolizer: write failed, disabling backend");
+ m_Alive = false;
+ return {};
+ }
+ Ptr += N;
+ Remaining -= static_cast<size_t>(N);
+ }
+#endif
+
+ // Read lines until a blank line terminates the record.
+ std::string Function;
+ std::string Location;
+ std::string Buf;
+ int LineIdx = 0;
+ while (ReadLine(Buf))
+ {
+ if (Buf.empty())
+ {
+ break;
+ }
+ if (LineIdx == 0)
+ {
+ Function = Buf;
+ }
+ else if (LineIdx == 1)
+ {
+ Location = Buf;
+ }
+ // Additional lines would be inline frames (--inlining=false suppresses them); ignore.
+ ++LineIdx;
+ }
+
+ if (Function.empty() || Function == "??")
+ {
+ return {};
+ }
+
+ std::string Result = std::move(Function);
+ if (!Location.empty() && Location != "??:0:0")
+ {
+ // Location is "path:line:col" — trim to "basename:line" to match Windows output.
+ std::string_view LocView(Location);
+ size_t LastColon = LocView.find_last_of(':');
+ if (LastColon != std::string_view::npos)
+ {
+ LocView = LocView.substr(0, LastColon);
+ }
+ size_t Slash = LocView.find_last_of("/\\");
+ std::string_view FileLine = (Slash == std::string_view::npos) ? LocView : LocView.substr(Slash + 1);
+ Result += fmt::format(" [{}]", FileLine);
+ }
+ return Result;
+}
+
+//////////////////////////////////////////////////////////////////////////////
+// atos backend — macOS only. Apple's symbolizer; ships with Xcode + the CLT.
+//
+// Unlike llvm-symbolizer, atos accepts only one binary per process. We keep
+// one subprocess per loaded module and demultiplex queries by module path.
+//
+// Protocol (one request / response):
+// We write: "0x<absolute-address>\n"
+// It replies: "Function (in Binary) (file.cpp:NN)\n"
+// or "Function (in Binary) + 0x<disp>\n" (no debug info)
+// or "0x<address>\n" (nothing known)
+//
+// Launched with: atos -o <binary> -l 0x<module-base>
+// atos subtracts -l from each input address to get the file offset.
+
+#if ZEN_PLATFORM_MAC
+
+class AtosSymbolizerResolver final : public SymbolResolver
+{
+public:
+ AtosSymbolizerResolver() = default;
+ ~AtosSymbolizerResolver() override;
+
+ void LoadModule(const ModuleInfo& Module) override;
+ std::string Resolve(uint64_t Address) const override;
+
+private:
+ struct Module
+ {
+ std::string FullPath;
+ uint64_t Base = 0;
+ uint64_t End = 0;
+ };
+
+ // One atos subprocess per loaded module (atos is single-binary).
+ struct AtosProcess
+ {
+ zen::ProcessHandle Process;
+ zen::StdinPipeHandles StdinPipe;
+ zen::StdoutPipeHandles StdoutPipe;
+ std::string ReadBuffer;
+ bool Alive = false;
+ };
+
+ const Module* FindModule(uint64_t Address) const;
+ AtosProcess* EnsureProcessFor(const Module& M) const;
+ bool ReadLine(AtosProcess& P, std::string& Out) const;
+ std::string DoQuery(const Module& M, uint64_t Address) const;
+
+ std::vector<Module> m_Modules;
+
+ mutable std::mutex m_Mutex;
+ mutable std::unordered_map<std::string, std::unique_ptr<AtosProcess>> m_Processes;
+ mutable std::unordered_map<uint64_t, std::string> m_Cache;
+};
+
+AtosSymbolizerResolver::~AtosSymbolizerResolver()
+{
+ std::lock_guard Lock(m_Mutex);
+ for (auto& [Path, P] : m_Processes)
+ {
+ if (P && P->Alive)
+ {
+ P->StdinPipe.CloseWriteEnd();
+ P->Process.Wait(2000);
+ if (P->Process.IsRunning())
+ {
+ P->Process.Terminate(0);
+ }
+ }
+ }
+}
+
+void
+AtosSymbolizerResolver::LoadModule(const ModuleInfo& Mod)
+{
+ if (Mod.FullPath.empty() || Mod.Base == 0)
+ {
+ return;
+ }
+
+ std::error_code Ec;
+ if (!std::filesystem::exists(Mod.FullPath, Ec))
+ {
+ ZEN_DEBUG("atos: binary not found for {} at {}", Mod.Name, Mod.FullPath);
+ return;
+ }
+
+ uint64_t End = Mod.Base + (Mod.Size > 0 ? Mod.Size : 0x1000000);
+
+ std::lock_guard Lock(m_Mutex);
+ m_Modules.push_back({Mod.FullPath, Mod.Base, End});
+ ZEN_INFO("atos: registered {} [0x{:X}..0x{:X})", Mod.Name, Mod.Base, End);
+}
+
+std::string
+AtosSymbolizerResolver::Resolve(uint64_t Address) const
+{
+ std::lock_guard Lock(m_Mutex);
+
+ auto CacheIt = m_Cache.find(Address);
+ if (CacheIt != m_Cache.end())
+ {
+ return CacheIt->second;
+ }
+
+ const Module* M = FindModule(Address);
+ if (M == nullptr)
+ {
+ m_Cache.emplace(Address, std::string{});
+ return {};
+ }
+
+ std::string Result = DoQuery(*M, Address);
+ m_Cache.emplace(Address, Result);
+ return Result;
+}
+
+const AtosSymbolizerResolver::Module*
+AtosSymbolizerResolver::FindModule(uint64_t Address) const
+{
+ for (const Module& M : m_Modules)
+ {
+ if (Address >= M.Base && Address < M.End)
+ {
+ return &M;
+ }
+ }
+ return nullptr;
+}
+
+AtosSymbolizerResolver::AtosProcess*
+AtosSymbolizerResolver::EnsureProcessFor(const Module& M) const
+{
+ auto It = m_Processes.find(M.FullPath);
+ if (It != m_Processes.end())
+ {
+ return It->second.get();
+ }
+
+ auto P = std::make_unique<AtosProcess>();
+
+ if (!CreateStdinPipe(P->StdinPipe) || !CreateStdoutPipe(P->StdoutPipe))
+ {
+ ZEN_WARN("atos: failed to create pipes for {}", M.FullPath);
+ auto [Ins, _] = m_Processes.emplace(M.FullPath, std::move(P));
+ return Ins->second.get(); // Alive = false
+ }
+
+ std::filesystem::path Executable = SearchPathForExecutable("atos");
+ std::string CommandLine = fmt::format("{} -o \"{}\" -l 0x{:X}", QuoteIfNeeded(Executable.string()), M.FullPath, M.Base);
+
+ CreateProcOptions Options;
+ Options.StdinPipe = &P->StdinPipe;
+ Options.StdoutPipe = &P->StdoutPipe;
+
+ CreateProcResult Handle = CreateProc(Executable, CommandLine, Options);
+ if (Handle <= 0)
+ {
+ ZEN_WARN("atos: failed to launch for {} - `atos` should be on PATH on macOS", M.FullPath);
+ P->StdinPipe.Close();
+ P->StdoutPipe.Close();
+ auto [Ins, _] = m_Processes.emplace(M.FullPath, std::move(P));
+ return Ins->second.get(); // Alive = false
+ }
+
+ std::error_code Ec;
+ P->Process.Initialize(int(Handle), Ec);
+ if (Ec)
+ {
+ ZEN_WARN("atos: ProcessHandle init failed for {}: {}", M.FullPath, Ec.message());
+ P->StdinPipe.Close();
+ P->StdoutPipe.Close();
+ auto [Ins, _] = m_Processes.emplace(M.FullPath, std::move(P));
+ return Ins->second.get(); // Alive = false
+ }
+
+ P->StdinPipe.CloseReadEnd();
+ P->StdoutPipe.CloseWriteEnd();
+ P->Alive = true;
+
+ auto [Ins, _] = m_Processes.emplace(M.FullPath, std::move(P));
+ return Ins->second.get();
+}
+
+bool
+AtosSymbolizerResolver::ReadLine(AtosProcess& P, std::string& Out) const
+{
+ for (;;)
+ {
+ size_t NewlinePos = P.ReadBuffer.find('\n');
+ if (NewlinePos != std::string::npos)
+ {
+ Out.assign(P.ReadBuffer, 0, NewlinePos);
+ P.ReadBuffer.erase(0, NewlinePos + 1);
+ return true;
+ }
+
+ char Buffer[1024];
+ ssize_t BytesRead = ::read(P.StdoutPipe.ReadFd, Buffer, sizeof(Buffer));
+ if (BytesRead <= 0)
+ {
+ if (BytesRead < 0 && errno == EINTR)
+ {
+ continue;
+ }
+ return false;
+ }
+ P.ReadBuffer.append(Buffer, static_cast<size_t>(BytesRead));
+ }
+}
+
+std::string
+AtosSymbolizerResolver::DoQuery(const Module& M, uint64_t Address) const
+{
+ AtosProcess* P = EnsureProcessFor(M);
+ if (P == nullptr || !P->Alive)
+ {
+ return {};
+ }
+
+ std::string Line = fmt::format("0x{:X}\n", Address);
+
+ const char* Ptr = Line.data();
+ size_t Remaining = Line.size();
+ while (Remaining > 0)
+ {
+ ssize_t N = ::write(P->StdinPipe.WriteFd, Ptr, Remaining);
+ if (N <= 0)
+ {
+ if (N < 0 && errno == EINTR)
+ {
+ continue;
+ }
+ ZEN_WARN("atos: write failed for {}, disabling", M.FullPath);
+ P->Alive = false;
+ return {};
+ }
+ Ptr += N;
+ Remaining -= static_cast<size_t>(N);
+ }
+
+ std::string Reply;
+ if (!ReadLine(*P, Reply) || Reply.empty())
+ {
+ return {};
+ }
+
+ // Parse "Function (in Binary) (file.cpp:NN)" or "... + 0xNN" or just "0xADDR".
+ // Extract everything before " (in " as the function name.
+ size_t InPos = Reply.find(" (in ");
+ if (InPos == std::string::npos)
+ {
+ // No match — either raw "0xADDR" (no info) or an error message. Skip.
+ return {};
+ }
+
+ std::string_view Function(Reply.data(), InPos);
+
+ // Look for a trailing "(file:line)" after the "(in ...)" block.
+ std::string_view LocationView;
+ size_t AfterIn = Reply.find(')', InPos);
+ if (AfterIn != std::string::npos)
+ {
+ size_t OpenParen = Reply.find('(', AfterIn);
+ if (OpenParen != std::string::npos)
+ {
+ size_t CloseParen = Reply.find(')', OpenParen);
+ if (CloseParen != std::string::npos && CloseParen > OpenParen + 1)
+ {
+ LocationView = std::string_view(Reply).substr(OpenParen + 1, CloseParen - OpenParen - 1);
+ }
+ }
+ }
+
+ std::string Result(Function);
+ if (!LocationView.empty())
+ {
+ // atos gives us "file.cpp:NN" directly — no need to strip a directory.
+ Result += fmt::format(" [{}]", LocationView);
+ }
+ return Result;
+}
+
+#endif // ZEN_PLATFORM_MAC
+
+//////////////////////////////////////////////////////////////////////////////
+// Factory
+
+namespace {
+
+#if ZEN_PLATFORM_MAC
+ // Probe PATH for a tool and return true if something usable was found.
+ // SearchPathForExecutable returns the input unchanged if the tool can't be
+ // found, so we compare against the filesystem to detect a hit.
+ bool ToolIsOnPath(std::string_view Name)
+ {
+ std::filesystem::path Resolved = SearchPathForExecutable(Name);
+ std::error_code Ec;
+ return std::filesystem::exists(Resolved, Ec) && std::filesystem::is_regular_file(Resolved, Ec);
+ }
+#endif
+
+ SymbolBackend ResolveAutoBackend()
+ {
+#if ZEN_PLATFORM_WINDOWS
+ return SymbolBackend::Pdb;
+#elif ZEN_PLATFORM_MAC
+ if (ToolIsOnPath("llvm-symbolizer"))
+ {
+ return SymbolBackend::LlvmSymbolizer;
+ }
+ return SymbolBackend::Atos;
+#else
+ // Linux: llvm-symbolizer is the only backend we ship.
+ return SymbolBackend::LlvmSymbolizer;
+#endif
+ }
+
+} // namespace
+
+std::unique_ptr<SymbolResolver>
+CreateSymbolResolver(SymbolBackend Backend)
+{
+ if (Backend == SymbolBackend::Auto)
+ {
+ Backend = ResolveAutoBackend();
+ }
+
+ if (Backend == SymbolBackend::Off)
+ {
+ return std::make_unique<NullSymbolResolver>();
+ }
+
+ if (Backend == SymbolBackend::LlvmSymbolizer)
+ {
+ return std::make_unique<LlvmSymbolizerResolver>();
+ }
+
+#if ZEN_PLATFORM_MAC
+ if (Backend == SymbolBackend::Atos)
+ {
+ return std::make_unique<AtosSymbolizerResolver>();
+ }
+#else
+ if (Backend == SymbolBackend::Atos)
+ {
+ ZEN_WARN("atos backend is macOS-only; falling back to llvm-symbolizer");
+ return std::make_unique<LlvmSymbolizerResolver>();
+ }
+#endif
+
+#if ZEN_PLATFORM_WINDOWS
+ if (Backend == SymbolBackend::DbgHelp)
+ {
+ return std::make_unique<DbgHelpSymbolResolver>();
+ }
+ return std::make_unique<PdbSymbolResolver>();
+#else
+ // Pdb / DbgHelp aren't available on non-Windows; any other request falls back to llvm-symbolizer.
+ return std::make_unique<LlvmSymbolizerResolver>();
+#endif
+}
+
+SymbolBackend
+ParseSymbolBackend(std::string_view Name)
+{
+ if (Name == "auto")
+ {
+ return SymbolBackend::Auto;
+ }
+ if (Name == "pdb")
+ {
+ return SymbolBackend::Pdb;
+ }
+ if (Name == "dbghelp")
+ {
+ return SymbolBackend::DbgHelp;
+ }
+ if (Name == "llvm" || Name == "llvm-symbolizer")
+ {
+ return SymbolBackend::LlvmSymbolizer;
+ }
+ if (Name == "atos")
+ {
+ return SymbolBackend::Atos;
+ }
+ if (Name == "off")
+ {
+ return SymbolBackend::Off;
+ }
+ return SymbolBackend::Off;
+}
+
+} // namespace zen::trace_detail
diff --git a/src/zen/trace/symbol_resolver.h b/src/zen/trace/symbol_resolver.h
new file mode 100644
index 000000000..4acdaf95e
--- /dev/null
+++ b/src/zen/trace/symbol_resolver.h
@@ -0,0 +1,45 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#pragma once
+
+#include "trace_model.h"
+
+#include <cstdint>
+#include <memory>
+#include <string>
+
+namespace zen::trace_detail {
+
+enum class SymbolBackend : uint8_t
+{
+ Off,
+ Auto, // Probe PATH and pick the best available backend for the platform
+ Pdb, // Windows only: RawPdb — fast, reads PDB files directly
+ DbgHelp, // Windows only: DbgHelp API — supports symbol servers and _NT_SYMBOL_PATH
+ LlvmSymbolizer, // Any platform: shells out to `llvm-symbolizer`, resolves dSYM (Mac) / DWARF (Linux) / PDB (Windows)
+ Atos // macOS only: shells out to `atos`, resolves Mach-O binaries and adjacent .dSYM bundles
+};
+
+// Resolves virtual addresses captured in a trace to function names.
+// Use CreateSymbolResolver() to obtain a concrete implementation.
+class SymbolResolver
+{
+public:
+ virtual ~SymbolResolver() = default;
+
+ // Load symbols for a module.
+ virtual void LoadModule(const ModuleInfo& Module) = 0;
+
+ // Resolve an absolute virtual address to "FunctionName + 0xNN" (or just
+ // "FunctionName" when the displacement is zero). Returns an empty string
+ // when the address cannot be resolved.
+ virtual std::string Resolve(uint64_t Address) const = 0;
+};
+
+std::unique_ptr<SymbolResolver> CreateSymbolResolver(SymbolBackend Backend);
+
+// Parse a string ("auto", "pdb", "dbghelp", "llvm", "llvm-symbolizer",
+// "atos", "off") into a SymbolBackend enum. Returns Off on unrecognised input.
+SymbolBackend ParseSymbolBackend(std::string_view Name);
+
+} // namespace zen::trace_detail
diff --git a/src/zen/trace/timeline_query.cpp b/src/zen/trace/timeline_query.cpp
new file mode 100644
index 000000000..d90c79a29
--- /dev/null
+++ b/src/zen/trace/timeline_query.cpp
@@ -0,0 +1,123 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#include "timeline_query.h"
+
+#include <algorithm>
+
+namespace zen::trace_detail {
+
+namespace {
+
+ // Pick the LOD level a given resolution should read from. Mirrors the
+ // historical selection in trace_viewer_service.cpp: resolution 0 reads the
+ // raw LOD 0; otherwise the smallest LOD whose ResolutionUs >= the request
+ // wins, falling back to the coarsest level if none qualify.
+ //
+ // Returned values: 0 == raw scopes (LOD 0), 1..kTimelineLodCount == DetailLevels[lod-1].
+ size_t SelectLodIndex(uint32_t ResolutionUs)
+ {
+ if (ResolutionUs == 0)
+ {
+ return 0;
+ }
+ for (size_t I = 0; I < kTimelineLodCount; ++I)
+ {
+ if (kTimelineLodResolutions[I] >= ResolutionUs)
+ {
+ return I + 1;
+ }
+ }
+ return kTimelineLodCount;
+ }
+
+ const eastl::vector<TimelineScope>& LodScopes(const ThreadTimeline& Timeline, size_t LodIndex)
+ {
+ if (LodIndex == 0)
+ {
+ return Timeline.Scopes;
+ }
+ return Timeline.DetailLevels[LodIndex - 1].Scopes;
+ }
+
+ void ExtractScopesInto(const ThreadTimeline& Timeline, const TimelineQueryRequest& Req, std::vector<TimelineScopeView>& Out)
+ {
+ const eastl::vector<TimelineScope>& Scopes = LodScopes(Timeline, SelectLodIndex(Req.ResolutionUs));
+
+ auto MidIt =
+ std::lower_bound(Scopes.begin(), Scopes.end(), Req.StartUs, [](const TimelineScope& S, uint32_t V) { return S.BeginUs < V; });
+
+ for (auto It = Scopes.begin(); It != MidIt; ++It)
+ {
+ if ((It->BeginUs + It->DurationUs) < Req.StartUs || It->DurationUs < Req.MinDurUs)
+ {
+ continue;
+ }
+ Out.push_back({It->BeginUs, It->DurationUs, It->NameId, It->Depth, It->MergeCount});
+ }
+ for (auto It = MidIt; It != Scopes.end(); ++It)
+ {
+ if (It->BeginUs > Req.EndUs)
+ {
+ break;
+ }
+ if (It->DurationUs < Req.MinDurUs)
+ {
+ continue;
+ }
+ Out.push_back({It->BeginUs, It->DurationUs, It->NameId, It->Depth, It->MergeCount});
+ }
+ }
+
+ const ThreadTimeline* FindThread(const TraceModel& Model, uint32_t ThreadId)
+ {
+ auto It = std::find_if(Model.Timelines.begin(), Model.Timelines.end(), [ThreadId](const ThreadTimeline& T) {
+ return T.ThreadId == ThreadId;
+ });
+ return (It != Model.Timelines.end()) ? &*It : nullptr;
+ }
+
+ class InMemoryTimelineQuery final : public TimelineQuery
+ {
+ public:
+ explicit InMemoryTimelineQuery(const TraceModel& Model) : m_Model(Model) {}
+
+ void QueryThread(uint32_t ThreadId, const TimelineQueryRequest& Req, std::vector<TimelineScopeView>& Out) const override
+ {
+ const ThreadTimeline* Timeline = FindThread(m_Model, ThreadId);
+ if (Timeline)
+ {
+ ExtractScopesInto(*Timeline, Req, Out);
+ }
+ }
+
+ void QueryBatch(std::span<const uint32_t> ThreadIds, const TimelineQueryRequest& Req, BatchResult& Out) const override
+ {
+ Out.Scopes.clear();
+ Out.Ranges.clear();
+ Out.Ranges.reserve(ThreadIds.size());
+
+ for (uint32_t ThreadId : ThreadIds)
+ {
+ const uint32_t Begin = uint32_t(Out.Scopes.size());
+ const ThreadTimeline* Timeline = FindThread(m_Model, ThreadId);
+ if (Timeline)
+ {
+ ExtractScopesInto(*Timeline, Req, Out.Scopes);
+ }
+ Out.Ranges.push_back({Begin, uint32_t(Out.Scopes.size())});
+ }
+ }
+
+ private:
+ const TraceModel& m_Model;
+ };
+
+} // namespace
+
+std::unique_ptr<TimelineQuery>
+MakeInMemoryTimelineQuery(const TraceModel& Model)
+{
+ return std::make_unique<InMemoryTimelineQuery>(Model);
+}
+
+} // namespace zen::trace_detail
diff --git a/src/zen/trace/timeline_query.h b/src/zen/trace/timeline_query.h
new file mode 100644
index 000000000..f773d8e58
--- /dev/null
+++ b/src/zen/trace/timeline_query.h
@@ -0,0 +1,69 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#pragma once
+
+#include "trace_model.h"
+
+#include <cstdint>
+#include <memory>
+#include <span>
+#include <vector>
+
+namespace zen::trace_detail {
+
+// Plain-data view of a single timeline scope returned by a TimelineQuery.
+// Mirrors the on-disk TimelineScope but is intentionally decoupled from the
+// in-memory model so that alternative backends can share the same result type.
+struct TimelineScopeView
+{
+ uint32_t BeginUs;
+ uint32_t DurationUs;
+ uint32_t NameId;
+ uint16_t Depth;
+ uint16_t MergeCount; // 0 == raw LOD 0, N>0 == N merged scopes
+};
+
+// Common parameters for a viewport-style timeline query.
+struct TimelineQueryRequest
+{
+ uint32_t StartUs;
+ uint32_t EndUs;
+ uint32_t MinDurUs;
+ uint32_t ResolutionUs; // 0 == LOD 0 (raw); >0 picks the smallest LOD with ResolutionUs >= this
+};
+
+// Backend-agnostic interface for serving timeline scope data to the trace
+// viewer HTTP handlers. Currently only the in-memory implementation exists,
+// but the abstraction is preserved as a clean swap point if a different
+// backend (e.g. on-disk indexed store) ever becomes useful.
+class TimelineQuery
+{
+public:
+ virtual ~TimelineQuery() = default;
+
+ // Append all scopes for a single thread matching the request to Out.
+ // Out is not cleared; callers can chain queries into the same buffer.
+ virtual void QueryThread(uint32_t ThreadId, const TimelineQueryRequest& Req, std::vector<TimelineScopeView>& Out) const = 0;
+
+ // Result of a batch query: a single flat scope vector plus per-thread
+ // ranges into it. Ranges[i] corresponds to ThreadIds[i] from the request.
+ struct BatchResult
+ {
+ struct Range
+ {
+ uint32_t Begin;
+ uint32_t End;
+ };
+ std::vector<TimelineScopeView> Scopes;
+ std::vector<Range> Ranges;
+ };
+
+ // Query several threads in one call. Out is cleared before being filled.
+ virtual void QueryBatch(std::span<const uint32_t> ThreadIds, const TimelineQueryRequest& Req, BatchResult& Out) const = 0;
+};
+
+// In-memory implementation. Holds a reference to Model — the model must
+// outlive the returned object.
+std::unique_ptr<TimelineQuery> MakeInMemoryTimelineQuery(const TraceModel& Model);
+
+} // namespace zen::trace_detail
diff --git a/src/zen/trace/trace_analyze.cpp b/src/zen/trace/trace_analyze.cpp
new file mode 100644
index 000000000..ff168cd9c
--- /dev/null
+++ b/src/zen/trace/trace_analyze.cpp
@@ -0,0 +1,812 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#include "trace_analyze.h"
+
+#include "callstack_formatter.h"
+#include "trace_cache.h"
+#include "zen.h"
+
+#include <zencore/basicfile.h>
+#include <zencore/fmtutils.h>
+#include <zencore/iobuffer.h>
+#include <zencore/logging.h>
+#include <zencore/scopeguard.h>
+#include <zencore/string.h>
+#include <zencore/thread.h>
+#include <zencore/workthreadpool.h>
+
+ZEN_THIRD_PARTY_INCLUDES_START
+#include <EASTL/hash_map.h>
+#include <EASTL/hash_set.h>
+#include <EASTL/vector.h>
+ZEN_THIRD_PARTY_INCLUDES_END
+
+#include <algorithm>
+
+namespace {
+
+using namespace zen::trace_detail;
+
+static void
+AppendHtmlEscaped(zen::StringBuilderBase& Out, std::string_view Text)
+{
+ for (char Ch : Text)
+ {
+ switch (Ch)
+ {
+ case '&':
+ Out << "&amp;";
+ break;
+ case '<':
+ Out << "&lt;";
+ break;
+ case '>':
+ Out << "&gt;";
+ break;
+ case '"':
+ Out << "&quot;";
+ break;
+ case '\'':
+ Out << "&#39;";
+ break;
+ default:
+ Out.Append(Ch);
+ break;
+ }
+ }
+}
+
+static CallstackFilterOptions
+BuildCallstackFilterOptions(const AnalyzeOptions& Options)
+{
+ CallstackFilterOptions Result;
+ Result.EnableHeuristic = Options.EnableCallstackHeuristic;
+ Result.SkipPatterns = Options.CallstackSkipPatterns;
+ return Result;
+}
+
+static std::string
+BuildThreadSummary(const TraceModel& Model, const eastl::fixed_vector<uint32_t, 4, true>& ThreadIds)
+{
+ std::string Result;
+ for (uint32_t Tid : ThreadIds)
+ {
+ if (!Result.empty())
+ {
+ Result += ", ";
+ }
+ auto TIt = std::find_if(Model.Threads.begin(), Model.Threads.end(), [Tid](const ThreadInfoEntry& T) { return T.ThreadId == Tid; });
+ if (TIt != Model.Threads.end() && !TIt->Name.empty())
+ {
+ Result += TIt->Name;
+ }
+ else
+ {
+ Result += fmt::format("tid:{}", Tid);
+ }
+ }
+ return Result;
+}
+
+static void
+AppendHtmlCallstack(zen::StringBuilderBase& Out, const AnalyzeOptions& Options, CallstackFormatter& Formatter, uint32_t CallstackId)
+{
+ const CallstackEntry* Entry = Formatter.FindCallstackEntry(CallstackId);
+ if (Entry == nullptr || Entry->Frames.empty())
+ {
+ Out << "<div class=\"muted\">No callstack frames recorded.</div>";
+ return;
+ }
+
+ FilteredCallstackView Filtered = Formatter.BuildView(*Entry, BuildCallstackFilterOptions(Options));
+ if (Filtered.HiddenPrefixCount > 0)
+ {
+ Out << "<div class=\"muted\">Skipped " << uint64_t(Filtered.HiddenPrefixCount) << " leading frame(s)";
+ if (Filtered.IncludedThirdPartyBoundary)
+ {
+ Out << "; kept boundary third-party callsite";
+ }
+ Out << ".</div>";
+ }
+
+ Out << "<ol class=\"frames\">";
+ for (const FilteredCallstackFrame& Frame : Filtered.Frames)
+ {
+ Out << "<li>";
+ AppendHtmlEscaped(Out, Frame.Display);
+ Out << "</li>";
+ }
+ Out << "</ol>";
+}
+
+static std::string_view
+FindHeapName(const TraceModel& Model, uint32_t HeapId)
+{
+ for (const HeapInfo& Heap : Model.Heaps)
+ {
+ if (Heap.Id == HeapId && !Heap.Name.empty())
+ {
+ return Heap.Name;
+ }
+ }
+ return "unknown";
+}
+
+static bool
+PassesChurnThreshold(const AnalyzeOptions& Options, const CallstackChurnStat& Stat)
+{
+ return Stat.MeanDistance <= double(Options.ChurnDistanceThreshold);
+}
+
+static uint64_t
+CountShownChurnSites(const TraceModel& Model, const AnalyzeOptions& Options, uint64_t Limit = 100)
+{
+ uint64_t Result = 0;
+ for (const CallstackChurnStat& Stat : Model.ChurnStats)
+ {
+ if (PassesChurnThreshold(Options, Stat) && Result < Limit)
+ {
+ ++Result;
+ }
+ }
+ return Result;
+}
+
+class ConsoleAnalyzeWriter
+{
+public:
+ ConsoleAnalyzeWriter(const TraceModel& InModel,
+ const AnalyzeOptions& InOptions,
+ const std::filesystem::path& InFilePath,
+ CallstackFormatter& InFrameFormatter)
+ : m_Model(InModel)
+ , m_Options(InOptions)
+ , m_FilePath(InFilePath)
+ , m_FrameFormatter(InFrameFormatter)
+ {
+ }
+
+ void Write() const
+ {
+ AppendSession();
+ AppendGeneralSummary();
+ AppendEventTypes();
+ AppendThreads();
+ AppendChannels();
+ AppendCpuScopeStats();
+ AppendMemorySummary();
+ AppendLiveAllocationCallstacks();
+ AppendChurnCallstacks();
+ }
+
+private:
+ void AppendSession() const
+ {
+ const SessionInfo& Session = m_Model.Session;
+ if (!Session.HasSession)
+ {
+ return;
+ }
+
+ ZEN_CONSOLE("Session:");
+ if (!Session.Platform.empty())
+ {
+ ZEN_CONSOLE(" Platform: {}", Session.Platform);
+ }
+ if (!Session.AppName.empty())
+ {
+ ZEN_CONSOLE(" App: {}", Session.AppName);
+ }
+ if (!Session.ProjectName.empty())
+ {
+ ZEN_CONSOLE(" Project: {}", Session.ProjectName);
+ }
+ if (!Session.Branch.empty())
+ {
+ ZEN_CONSOLE(" Branch: {}", Session.Branch);
+ }
+ if (!Session.BuildVersion.empty())
+ {
+ ZEN_CONSOLE(" Build: {}", Session.BuildVersion);
+ }
+ if (Session.ConfigurationType != 0)
+ {
+ constexpr const char* kConfigNames[] = {"Unknown", "Debug", "DebugGame", "Development", "Shipping", "Test"};
+ uint8_t Idx = Session.ConfigurationType;
+ const char* Name = (Idx < std::size(kConfigNames)) ? kConfigNames[Idx] : "Unknown";
+ ZEN_CONSOLE(" Config: {}", Name);
+ }
+ if (Session.Changelist != 0)
+ {
+ ZEN_CONSOLE(" CL: {}", Session.Changelist);
+ }
+ if (!Session.CommandLine.empty())
+ {
+ ZEN_CONSOLE(" Cmd: {}", Session.CommandLine);
+ }
+ ZEN_CONSOLE("");
+ }
+
+ void AppendGeneralSummary() const
+ {
+ uint64_t DurationUs = (m_Model.TraceEndUs > m_Model.TraceStartUs) ? (m_Model.TraceEndUs - m_Model.TraceStartUs) : 0;
+
+ ZEN_CONSOLE("Trace: {}", m_FilePath);
+ ZEN_CONSOLE("Size: {}", zen::NiceBytes(m_Model.FileSize));
+ ZEN_CONSOLE("Events: {}", zen::ThousandsNum(m_Model.TotalEvents));
+ ZEN_CONSOLE("Duration: {}", zen::NiceTimeSpanMs((DurationUs + 500) / 1000));
+ ZEN_CONSOLE("Threads: {}", m_Model.Threads.size());
+ ZEN_CONSOLE("Modules: {}", m_Model.Modules.size());
+ ZEN_CONSOLE("Parsed: {}", zen::NiceTimeSpanMs(m_Model.ParseTimeMs));
+ if (m_Model.ParseTimeMs > 0)
+ {
+ ZEN_CONSOLE("Rate: {} events/s", zen::ThousandsNum(m_Model.TotalEvents * 1000 / m_Model.ParseTimeMs));
+ }
+ ZEN_CONSOLE("");
+ }
+
+ void AppendEventTypes() const
+ {
+ if (m_Model.EventTypeCounts.empty())
+ {
+ return;
+ }
+
+ size_t MaxNameLen = 10;
+ for (const auto& Entry : m_Model.EventTypeCounts)
+ {
+ MaxNameLen = std::max(MaxNameLen, Entry.Name.size());
+ }
+
+ ZEN_CONSOLE("{:<{}} {:>14}", "Event Type", MaxNameLen, "Count");
+ ZEN_CONSOLE("{:-<{}}", "", MaxNameLen + 16);
+ for (const auto& Entry : m_Model.EventTypeCounts)
+ {
+ ZEN_CONSOLE("{:<{}} {:>14}", Entry.Name, MaxNameLen, zen::ThousandsNum(Entry.Count));
+ }
+ ZEN_CONSOLE("");
+ }
+
+ void AppendThreads() const
+ {
+ if (m_Model.Threads.empty())
+ {
+ return;
+ }
+
+ ZEN_CONSOLE("Threads:");
+ for (const ThreadInfoEntry& Thread : m_Model.Threads)
+ {
+ auto TimelineIt = std::find_if(m_Model.Timelines.begin(),
+ m_Model.Timelines.end(),
+ [Tid = Thread.ThreadId](const ThreadTimeline& T) { return T.ThreadId == Tid; });
+ uint64_t ScopeCount = (TimelineIt != m_Model.Timelines.end()) ? TimelineIt->Scopes.size() : 0;
+
+ if (!Thread.Name.empty())
+ {
+ ZEN_CONSOLE(" {:>5} {:<32} {} scopes", Thread.ThreadId, Thread.Name, zen::ThousandsNum(ScopeCount));
+ }
+ }
+ ZEN_CONSOLE("");
+ }
+
+ void AppendChannels() const
+ {
+ if (m_Model.Channels.empty())
+ {
+ return;
+ }
+
+ ZEN_CONSOLE("Channels:");
+ for (const ChannelInfo& Channel : m_Model.Channels)
+ {
+ ZEN_CONSOLE(" {:<32} {}", Channel.Name, Channel.Enabled ? "enabled" : "disabled");
+ }
+ ZEN_CONSOLE("");
+ }
+
+ void AppendCpuScopeStats() const
+ {
+ if (m_Model.ScopeStats.empty())
+ {
+ return;
+ }
+
+ ZEN_CONSOLE("CPU Profiling Scopes:");
+ ZEN_CONSOLE("");
+ ZEN_CONSOLE("{:<48} {:>8} {:>9} {:>9} {:>9} {:>9}", "Scope", "Count", "Min(ms)", "Mean(ms)", "Max(ms)", "SD(ms)");
+ ZEN_CONSOLE("{:-<{}}", "", 48 + 8 + 9 + 9 + 9 + 9 + 5);
+
+ constexpr double UsToMs = 1.0 / 1000.0;
+ for (const CpuScopeStat& Stat : m_Model.ScopeStats)
+ {
+ if (Stat.MaxUs < 500)
+ {
+ continue;
+ }
+
+ ZEN_CONSOLE("{:<48.48} {:>8} {:>9.3f} {:>9.3f} {:>9.3f} {:>9.3f}",
+ Stat.Name,
+ zen::ThousandsNum(Stat.Count),
+ double(Stat.MinUs) * UsToMs,
+ Stat.MeanUs * UsToMs,
+ double(Stat.MaxUs) * UsToMs,
+ Stat.StdDevUs * UsToMs);
+ }
+ ZEN_CONSOLE("");
+ }
+
+ void AppendMemorySummary() const
+ {
+ const AllocationSummary& AllocSummary = m_Model.AllocSummary;
+ if (!AllocSummary.HasMemoryData)
+ {
+ return;
+ }
+
+ ZEN_CONSOLE("Memory Allocations:");
+ ZEN_CONSOLE("");
+ ZEN_CONSOLE(" Allocs: {}", zen::ThousandsNum(AllocSummary.TotalAllocs));
+ ZEN_CONSOLE(" Frees: {}", zen::ThousandsNum(AllocSummary.TotalFrees));
+ ZEN_CONSOLE(" Reallocs: {} alloc / {} free",
+ zen::ThousandsNum(AllocSummary.TotalReallocAllocs),
+ zen::ThousandsNum(AllocSummary.TotalReallocFrees));
+ ZEN_CONSOLE(" Peak: {}", zen::NiceBytes(uint64_t(AllocSummary.PeakBytes)));
+ ZEN_CONSOLE(" End: {}", zen::NiceBytes(uint64_t(AllocSummary.EndBytes)));
+ ZEN_CONSOLE(" Live allocs: {}", zen::ThousandsNum(AllocSummary.LiveAllocations));
+
+ if (!m_Model.HeapStats.empty())
+ {
+ ZEN_CONSOLE("");
+ ZEN_CONSOLE(" {:<20} {:>14} {:>14} {:>10} {:>10}", "Heap", "Current", "Peak", "Allocs", "Frees");
+ ZEN_CONSOLE(" {:-<{}}", "", 20 + 14 + 14 + 10 + 10 + 4);
+
+ for (const HeapStat& Stat : m_Model.HeapStats)
+ {
+ std::string_view HeapName = FindHeapName(m_Model, Stat.HeapId);
+
+ ZEN_CONSOLE(" {:<20.20} {:>14} {:>14} {:>10} {:>10}",
+ HeapName,
+ zen::NiceBytes(uint64_t(Stat.CurrentBytes)),
+ zen::NiceBytes(uint64_t(Stat.PeakBytes)),
+ zen::ThousandsNum(Stat.AllocCount),
+ zen::ThousandsNum(Stat.FreeCount));
+ }
+ }
+ ZEN_CONSOLE("");
+ }
+
+ void PrintCallstack(uint32_t CallstackId) const
+ {
+ const CallstackEntry* Entry = m_FrameFormatter.FindCallstackEntry(CallstackId);
+ if (Entry == nullptr)
+ {
+ return;
+ }
+
+ FilteredCallstackView Filtered = m_FrameFormatter.BuildView(*Entry, BuildCallstackFilterOptions(m_Options));
+ if (Filtered.HiddenPrefixCount > 0)
+ {
+ if (Filtered.IncludedThirdPartyBoundary)
+ {
+ ZEN_CONSOLE(" [skipped {} leading frame(s); kept boundary third-party callsite]", Filtered.HiddenPrefixCount);
+ }
+ else
+ {
+ ZEN_CONSOLE(" [skipped {} leading frame(s)]", Filtered.HiddenPrefixCount);
+ }
+ }
+ for (const FilteredCallstackFrame& Frame : Filtered.Frames)
+ {
+ ZEN_CONSOLE(" {}", Frame.Display);
+ }
+ }
+
+ void AppendLiveAllocationCallstacks() const
+ {
+ if (m_Options.LiveAllocsLimit <= 0 || m_Model.CallstackStats.empty())
+ {
+ return;
+ }
+
+ size_t Count = std::min(size_t(m_Options.LiveAllocsLimit), m_Model.CallstackStats.size());
+ ZEN_CONSOLE("Live Allocation Callstacks (top {} by bytes):", Count);
+ ZEN_CONSOLE("");
+
+ for (size_t I = 0; I < Count; ++I)
+ {
+ const CallstackAllocStat& Stat = m_Model.CallstackStats[I];
+ std::string ThreadInfo = BuildThreadSummary(m_Model, Stat.ThreadIds);
+ ZEN_CONSOLE(" #{} {} in {} allocation(s) [callstack {}, {}]",
+ I + 1,
+ zen::NiceBytes(uint64_t(Stat.LiveBytes)),
+ zen::ThousandsNum(Stat.LiveCount),
+ Stat.CallstackId,
+ ThreadInfo);
+ PrintCallstack(Stat.CallstackId);
+ ZEN_CONSOLE("");
+ }
+ }
+
+ void AppendChurnCallstacks() const
+ {
+ if (m_Options.ChurnLimit <= 0 || m_Model.ChurnStats.empty())
+ {
+ return;
+ }
+
+ size_t Emitted = 0;
+ size_t Limit = size_t(m_Options.ChurnLimit);
+ ZEN_CONSOLE("Allocation Churn (top {}, event distance <= {}):", Limit, m_Options.ChurnDistanceThreshold);
+ ZEN_CONSOLE("");
+
+ for (const CallstackChurnStat& Stat : m_Model.ChurnStats)
+ {
+ if (Emitted >= Limit)
+ {
+ break;
+ }
+ if (!PassesChurnThreshold(m_Options, Stat))
+ {
+ continue;
+ }
+
+ ZEN_CONSOLE(" #{} {} short-lived allocs ({} total), {} churned, avg distance {:.0f} events [callstack {}]",
+ Emitted + 1,
+ zen::ThousandsNum(Stat.ChurnAllocs),
+ zen::ThousandsNum(Stat.TotalAllocs),
+ zen::NiceBytes(Stat.ChurnBytes),
+ Stat.MeanDistance,
+ Stat.CallstackId);
+ PrintCallstack(Stat.CallstackId);
+ ZEN_CONSOLE("");
+ ++Emitted;
+ }
+ }
+
+ const TraceModel& m_Model;
+ const AnalyzeOptions& m_Options;
+ const std::filesystem::path& m_FilePath;
+ CallstackFormatter& m_FrameFormatter;
+};
+
+class HtmlReportWriter
+{
+public:
+ HtmlReportWriter(const TraceModel& InModel,
+ const AnalyzeOptions& InOptions,
+ const std::filesystem::path& InFilePath,
+ CallstackFormatter& InFrameFormatter)
+ : m_Model(InModel)
+ , m_Options(InOptions)
+ , m_FilePath(InFilePath)
+ , m_FrameFormatter(InFrameFormatter)
+ {
+ }
+
+ void Write(const std::filesystem::path& OutputPath)
+ {
+ AppendDocument();
+ zen::WriteFile(OutputPath, zen::IoBuffer(zen::IoBuffer::Wrap, m_Html.Data(), m_Html.Size()));
+ }
+
+private:
+ void AppendDocument()
+ {
+ m_Html << "<!doctype html><html><head><meta charset=\"utf-8\"><title>zen trace analyze report</title>";
+ AppendStyles();
+ m_Html << "</head><body>";
+ AppendHeader();
+ AppendSummaryCards();
+ AppendLeaksSection();
+ AppendChurnSection();
+ m_Html << "</body></html>";
+ }
+
+ void AppendStyles()
+ {
+ m_Html << "<style>body{font:14px/1.45 system-ui,-apple-system,Segoe "
+ "UI,Roboto,sans-serif;margin:24px;color:#1f2937;background:#f8fafc;}";
+ m_Html << "h1,h2{margin:0 0 12px;}h1{font-size:28px;}h2{font-size:20px;margin-top:28px;}";
+ m_Html << ".meta,.card,details{background:#fff;border:1px solid #dbe2ea;border-radius:10px;box-shadow:0 1px 2px rgba(0,0,0,.04);}";
+ m_Html << ".meta,.card{padding:16px;margin-bottom:16px;}.report-table{width:100%;border-collapse:collapse;background:#fff;border:"
+ "1px solid #dbe2ea;border-radius:10px;overflow:hidden;table-layout:fixed;}";
+ m_Html << "th,td{padding:10px 12px;border-bottom:1px solid "
+ "#e5e7eb;vertical-align:top;text-align:left;}th{background:#f1f5f9;font-weight:600;}tr:last-child td{border-bottom:0;}";
+ m_Html
+ << "th.num,td.num{text-align:right;white-space:nowrap;font-variant-numeric:tabular-nums;}.muted{color:#64748b;}.pill{display:"
+ "inline-block;padding:2px 8px;border-radius:999px;background:#e2e8f0;color:#334155;font-size:12px;margin-right:6px;}";
+ m_Html << ".col-rank{width:56px;}.col-live-bytes,.col-alloc-count,.col-short-lived,.col-churn-bytes,.col-total-allocs,.col-avg-"
+ "distance{width:132px;}.col-threads{width:260px;}.col-callstack{width:auto;}";
+ m_Html << ".grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:12px;margin-bottom:18px;}details{"
+ "display:block;width:100%;box-sizing:border-box;padding:12px "
+ "14px;margin:0;}summary{cursor:pointer;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}";
+ m_Html << ".callstack-cell{width:100%;}.frames{margin:10px 0 0 "
+ "20px;padding:0;font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:12px;}.frames li{margin:4px "
+ "0;overflow-wrap:anywhere;word-break:break-word;}code{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;}a{"
+ "color:#2563eb;text-decoration:none;}a:hover{text-decoration:underline;}</style>";
+ }
+
+ void AppendHeader()
+ {
+ m_Html << "<h1>zen trace analyze memory report</h1>";
+ m_Html << "<div class=\"meta\"><div><span class=\"pill\">offline HTML</span><span class=\"pill\">top 100 churn "
+ "sites</span></div><p><strong>Trace:</strong> <code>";
+ AppendHtmlEscaped(m_Html, m_FilePath.string());
+ m_Html << "</code></p>";
+ if (m_Model.Session.HasSession && !m_Model.Session.AppName.empty())
+ {
+ m_Html << "<p><strong>App:</strong> ";
+ AppendHtmlEscaped(m_Html, m_Model.Session.AppName);
+ m_Html << "</p>";
+ }
+ m_Html << "<p class=\"muted\">Generated by zen trace analyze. Churn threshold: ";
+ AppendHtmlEscaped(m_Html, fmt::format("{} events", m_Options.ChurnDistanceThreshold));
+ m_Html << "</p></div>";
+ }
+
+ void AppendSummaryCards()
+ {
+ uint64_t DurationUs = (m_Model.TraceEndUs > m_Model.TraceStartUs) ? (m_Model.TraceEndUs - m_Model.TraceStartUs) : 0;
+ m_Html << "<div class=\"grid\">";
+ m_Html << "<div class=\"card\"><div class=\"muted\">Trace size</div><div><strong>" << zen::NiceBytes(m_Model.FileSize)
+ << "</strong></div></div>";
+ m_Html << "<div class=\"card\"><div class=\"muted\">Duration</div><div><strong>" << zen::NiceTimeSpanMs((DurationUs + 500) / 1000)
+ << "</strong></div></div>";
+ m_Html << "<div class=\"card\"><div class=\"muted\">Peak memory</div><div><strong>"
+ << zen::NiceBytes(uint64_t(m_Model.AllocSummary.PeakBytes)) << "</strong></div></div>";
+ m_Html << "<div class=\"card\"><div class=\"muted\">End memory</div><div><strong>"
+ << zen::NiceBytes(uint64_t(m_Model.AllocSummary.EndBytes)) << "</strong></div></div>";
+ m_Html << "<div class=\"card\"><div class=\"muted\">Live allocations</div><div><strong>"
+ << zen::ThousandsNum(m_Model.AllocSummary.LiveAllocations) << "</strong></div></div>";
+ m_Html << "<div class=\"card\"><div class=\"muted\">Leak callstacks</div><div><strong>"
+ << zen::ThousandsNum(m_Model.CallstackStats.size()) << "</strong></div></div>";
+ m_Html << "<div class=\"card\"><div class=\"muted\">Churn sites shown</div><div><strong>"
+ << zen::ThousandsNum(::CountShownChurnSites(m_Model, m_Options)) << "</strong></div></div>";
+ m_Html << "</div>";
+ }
+
+ void AppendLeaksSection()
+ {
+ m_Html << "<h2 id=\"leaks\">Memory leaks (all live-allocation callstacks)</h2>";
+ if (m_Model.CallstackStats.empty())
+ {
+ m_Html << "<div class=\"card muted\">No live allocation callstacks were present at the end of the trace.</div>";
+ return;
+ }
+
+ m_Html << "<table class=\"report-table\"><colgroup><col class=\"col-rank\"><col class=\"col-live-bytes\"><col "
+ "class=\"col-alloc-count\"><col class=\"col-threads\"><col class=\"col-callstack\"></colgroup><thead><tr><th "
+ "class=\"num\">#</th><th class=\"num\">Live bytes</th><th class=\"num\">Alloc "
+ "count</th><th>Threads</th><th>Callstack</th></tr></thead><tbody>";
+ for (size_t I = 0; I < m_Model.CallstackStats.size(); ++I)
+ {
+ const CallstackAllocStat& Stat = m_Model.CallstackStats[I];
+ std::string ThreadInfo = BuildThreadSummary(m_Model, Stat.ThreadIds);
+ m_Html << "<tr><td class=\"num\">" << uint64_t(I + 1) << "</td><td class=\"num\">" << zen::NiceBytes(uint64_t(Stat.LiveBytes))
+ << "</td><td class=\"num\">" << zen::ThousandsNum(Stat.LiveCount) << "</td><td>";
+ AppendHtmlEscaped(m_Html, ThreadInfo);
+ m_Html << "</td><td class=\"callstack-cell\"><details><summary>Callstack " << Stat.CallstackId << "</summary>";
+ AppendHtmlCallstack(m_Html, m_Options, m_FrameFormatter, Stat.CallstackId);
+ m_Html << "</details></td></tr>";
+ }
+ m_Html << "</tbody></table>";
+ }
+
+ void AppendChurnSection()
+ {
+ m_Html << "<h2 id=\"churn\">Allocation churn sites (top 100)</h2>";
+ if (m_Model.ChurnStats.empty())
+ {
+ m_Html << "<div class=\"card muted\">No churn statistics were available in this trace.</div>";
+ return;
+ }
+
+ m_Html << "<table class=\"report-table\"><colgroup><col class=\"col-rank\"><col class=\"col-short-lived\"><col "
+ "class=\"col-churn-bytes\"><col class=\"col-total-allocs\"><col class=\"col-avg-distance\"><col "
+ "class=\"col-callstack\"></colgroup><thead><tr><th class=\"num\">#</th><th class=\"num\">Short-lived allocs</th><th "
+ "class=\"num\">Churn bytes</th><th class=\"num\">Total allocs</th><th class=\"num\">Avg "
+ "distance</th><th>Callstack</th></tr></thead><tbody>";
+ size_t Emitted = 0;
+ for (const CallstackChurnStat& Stat : m_Model.ChurnStats)
+ {
+ if (Emitted >= 100)
+ {
+ break;
+ }
+ if (!PassesChurnThreshold(m_Options, Stat))
+ {
+ continue;
+ }
+ m_Html << "<tr><td class=\"num\">" << uint64_t(Emitted + 1) << "</td><td class=\"num\">" << zen::ThousandsNum(Stat.ChurnAllocs)
+ << "</td><td class=\"num\">" << zen::NiceBytes(Stat.ChurnBytes) << "</td><td class=\"num\">"
+ << zen::ThousandsNum(Stat.TotalAllocs) << "</td><td class=\"num\">" << fmt::format("{:.0f} events", Stat.MeanDistance)
+ << "</td><td class=\"callstack-cell\"><details><summary>Callstack " << Stat.CallstackId << "</summary>";
+ AppendHtmlCallstack(m_Html, m_Options, m_FrameFormatter, Stat.CallstackId);
+ m_Html << "</details></td></tr>";
+ ++Emitted;
+ }
+ m_Html << "</tbody></table>";
+ }
+
+ const TraceModel& m_Model;
+ const AnalyzeOptions& m_Options;
+ const std::filesystem::path& m_FilePath;
+ CallstackFormatter& m_FrameFormatter;
+ zen::ExtendableStringBuilder<32768> m_Html;
+};
+
+static void
+WriteAnalyzeHtmlReport(const TraceModel& Model,
+ const AnalyzeOptions& Options,
+ const std::filesystem::path& FilePath,
+ CallstackFormatter& FrameFormatter)
+{
+ std::filesystem::path OutputPath = std::filesystem::absolute(Options.HtmlReportPath);
+ if (OutputPath.empty())
+ {
+ return;
+ }
+
+ std::error_code Ec;
+ std::filesystem::path ParentPath = OutputPath.parent_path();
+ if (!ParentPath.empty())
+ {
+ std::filesystem::create_directories(ParentPath, Ec);
+ }
+
+ HtmlReportWriter Writer(Model, Options, FilePath, FrameFormatter);
+ Writer.Write(OutputPath);
+ ZEN_CONSOLE("HTML report: {}", OutputPath.string());
+}
+
+} // namespace
+
+namespace zen::trace_detail {
+
+void
+RunAnalyze(const std::filesystem::path& FilePath, const AnalyzeOptions& Options)
+{
+ std::filesystem::path CachePath = FilePath;
+ CachePath.replace_extension(".ucache_z");
+
+ TraceModel Model;
+ std::unique_ptr<SymbolResolver> Symbols;
+ bool LoadedFromCache = false;
+
+ // Try loading from cache
+ if (!Options.NoCache)
+ {
+ std::optional<CachedAnalysis> Cached = TryLoadAnalyzeCache(CachePath, FilePath);
+ if (Cached)
+ {
+ Model = std::move(Cached->Model);
+ Symbols = std::move(Cached->Symbols);
+ LoadedFromCache = true;
+ }
+ }
+
+ if (!LoadedFromCache)
+ {
+ WorkerThreadPool ThreadPool(gsl::narrow<int>(GetHardwareConcurrency()));
+ Model = BuildTraceModel(FilePath, ThreadPool);
+
+ if (Options.Symbols != SymbolBackend::Off)
+ {
+ Symbols = CreateSymbolResolver(Options.Symbols);
+ for (const ModuleInfo& Mod : Model.Modules)
+ {
+ Symbols->LoadModule(Mod);
+ }
+ }
+ }
+
+ CallstackFormatter FrameFormatter(Model, Symbols.get());
+ ConsoleAnalyzeWriter ConsoleWriter(Model, Options, FilePath, FrameFormatter);
+ ConsoleWriter.Write();
+
+ if (!Options.HtmlReportPath.empty())
+ {
+ WriteAnalyzeHtmlReport(Model, Options, FilePath, FrameFormatter);
+ }
+
+ // Write cache on fresh parse
+ if (!LoadedFromCache && !Options.NoCache)
+ {
+ // Build the complete symbol map for the cache. Start with whatever
+ // the formatter already resolved during display, then resolve every
+ // remaining callstack address in parallel.
+ eastl::hash_map<uint64_t, std::string> AllSymbols = FrameFormatter.GetResolvedCache();
+
+ // Collect unique addresses that still need resolving.
+ eastl::hash_set<uint64_t> Needed;
+ for (const CallstackEntry& CS : Model.Callstacks)
+ {
+ for (const ResolvedFrame& Frame : CS.Frames)
+ {
+ if (AllSymbols.find(Frame.Address) == AllSymbols.end())
+ {
+ Needed.insert(Frame.Address);
+ }
+ }
+ }
+
+ if (!Needed.empty() && Symbols)
+ {
+ // Flatten to a vector so we can partition into chunks.
+ eastl::vector<uint64_t> Addresses(Needed.begin(), Needed.end());
+ Needed.clear();
+
+ uint32_t ThreadCount = gsl::narrow<uint32_t>(GetHardwareConcurrency());
+ WorkerThreadPool ResolvePool(gsl::narrow<int>(ThreadCount));
+
+ // Each worker resolves a chunk and writes into its own local map.
+ eastl::vector<eastl::hash_map<uint64_t, std::string>> PerThread(ThreadCount);
+ uint32_t ChunkSize = uint32_t((Addresses.size() + ThreadCount - 1) / ThreadCount);
+
+ Latch Done(ThreadCount);
+ for (uint32_t T = 0; T < ThreadCount; ++T)
+ {
+ uint32_t Begin = T * ChunkSize;
+ uint32_t End = std::min(Begin + ChunkSize, uint32_t(Addresses.size()));
+ if (Begin >= End)
+ {
+ Done.CountDown();
+ continue;
+ }
+
+ ResolvePool.ScheduleWork(
+ [&Addresses, &PerThread, &Model, &Symbols, &Done, T, Begin, End]() {
+ auto _ = MakeGuard([&Done]() { Done.CountDown(); });
+ for (uint32_t I = Begin; I < End; ++I)
+ {
+ uint64_t Addr = Addresses[I];
+ std::string Symbol = Symbols->Resolve(Addr);
+ if (!Symbol.empty())
+ {
+ PerThread[T].emplace(Addr, std::move(Symbol));
+ }
+ }
+ },
+ WorkerThreadPool::EMode::EnableBacklog);
+ }
+ Done.Wait();
+
+ // Merge per-thread results.
+ for (auto& Map : PerThread)
+ {
+ for (auto& [Addr, Sym] : Map)
+ {
+ AllSymbols.emplace(Addr, std::move(Sym));
+ }
+ }
+ }
+
+ // Fill in module-name fallbacks for any addresses not resolved by the
+ // symbol resolver (same logic as CallstackFormatter::Describe).
+ for (const CallstackEntry& CS : Model.Callstacks)
+ {
+ for (const ResolvedFrame& Frame : CS.Frames)
+ {
+ if (AllSymbols.find(Frame.Address) != AllSymbols.end())
+ {
+ continue;
+ }
+ std::string Fallback;
+ if (Frame.ModuleIndex != ~0u && Frame.ModuleIndex < Model.Modules.size())
+ {
+ Fallback = fmt::format("{} + 0x{:X}", Model.Modules[Frame.ModuleIndex].Name, Frame.Offset);
+ }
+ else
+ {
+ Fallback = fmt::format("0x{:X}", Frame.Address);
+ }
+ AllSymbols.emplace(Frame.Address, std::move(Fallback));
+ }
+ }
+
+ WriteAnalyzeCache(CachePath, FilePath, Model, AllSymbols);
+ }
+}
+
+} // namespace zen::trace_detail
diff --git a/src/zen/trace/trace_analyze.h b/src/zen/trace/trace_analyze.h
new file mode 100644
index 000000000..7b6f4fccd
--- /dev/null
+++ b/src/zen/trace/trace_analyze.h
@@ -0,0 +1,29 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#pragma once
+
+#include "symbol_resolver.h"
+#include "trace_model.h"
+
+#include <cstdint>
+#include <filesystem>
+#include <string>
+#include <vector>
+
+namespace zen::trace_detail {
+
+struct AnalyzeOptions
+{
+ int LiveAllocsLimit = 50; // 0 = off
+ int ChurnLimit = 0; // 0 = off; top N churny callstacks
+ uint64_t ChurnDistanceThreshold = 1000; // event distance: allocs freed within N events are "churny"
+ SymbolBackend Symbols = SymbolBackend(1); // Pdb (default)
+ std::filesystem::path HtmlReportPath; // empty = off; standalone offline memory HTML report
+ bool NoCache = false; // skip reading/writing the .ucache_z cache
+ bool EnableCallstackHeuristic = true; // skip leading low-level / third-party frames while keeping the boundary callsite
+ std::vector<std::string> CallstackSkipPatterns; // wildcard patterns matched against symbol, module name, and module path
+};
+
+void RunAnalyze(const std::filesystem::path& FilePath, const AnalyzeOptions& Options = {});
+
+} // namespace zen::trace_detail
diff --git a/src/zen/trace/trace_cache.cpp b/src/zen/trace/trace_cache.cpp
new file mode 100644
index 000000000..165c1eecf
--- /dev/null
+++ b/src/zen/trace/trace_cache.cpp
@@ -0,0 +1,1104 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#include "trace_cache.h"
+
+#include <zencore/basicfile.h>
+#include <zencore/compress.h>
+#include <zencore/filesystem.h>
+#include <zencore/fmtutils.h>
+#include <zencore/iohash.h>
+#include <zencore/logging.h>
+#include <zencore/stream.h>
+
+ZEN_THIRD_PARTY_INCLUDES_START
+#include <EASTL/sort.h>
+#include <EASTL/vector.h>
+ZEN_THIRD_PARTY_INCLUDES_END
+
+#include <filesystem>
+
+namespace zen::trace_detail {
+
+// ===========================================================================
+// StringTableBuilder — write-path helper that deduplicates and packs strings
+//
+// Strings are appended back-to-back (null-terminated) in a single contiguous
+// block. Deduplication is keyed by (offset, length) pairs into that block so
+// no separate string copies are made. To look up an incoming string_view we
+// speculatively append it, build a key, and look it up. On duplicate the
+// append is rolled back by truncating the buffer.
+// ===========================================================================
+
+class StringTableBuilder
+{
+public:
+ StringTableBuilder() : m_IndexMap(0, StringHash{&m_Packed}, StringEq{&m_Packed}) { m_Packed.reserve(4096); }
+
+ // Intern a string and return its index. Deduplicates across calls.
+ uint32_t Intern(std::string_view Str)
+ {
+ // Speculatively append the string so that the hash/eq functors can
+ // read it from the packed buffer (avoids dangling string_view keys).
+ uint32_t SpecOffset = uint32_t(m_Packed.size());
+ uint32_t SpecLength = uint32_t(Str.size());
+
+ m_Packed.resize(m_Packed.size() + Str.size() + 1);
+ if (!Str.empty())
+ {
+ memcpy(m_Packed.data() + SpecOffset, Str.data(), Str.size());
+ }
+ m_Packed[SpecOffset + Str.size()] = '\0';
+
+ StringKey Key{SpecOffset, SpecLength};
+ auto It = m_IndexMap.find(Key);
+ if (It != m_IndexMap.end())
+ {
+ // Duplicate — roll back the speculative append.
+ m_Packed.resize(SpecOffset);
+ return It->second;
+ }
+
+ // New string — keep the append and record its index.
+ uint32_t Index = uint32_t(m_Offsets.size());
+ m_Offsets.push_back(SpecOffset);
+ m_IndexMap.emplace(Key, Index);
+ return Index;
+ }
+
+ // Serialize: [uint32_t count][uint32_t offsets[count]][packed strings]
+ SharedBuffer Serialize() const
+ {
+ BinaryWriter W;
+ uint32_t Count = uint32_t(m_Offsets.size());
+ W.Write(&Count, sizeof(Count));
+ if (Count > 0)
+ {
+ W.Write(m_Offsets.data(), m_Offsets.size() * sizeof(uint32_t));
+ }
+ if (!m_Packed.empty())
+ {
+ W.Write(m_Packed.data(), m_Packed.size());
+ }
+ return SharedBuffer(IoBuffer(IoBuffer::Clone, W.Data(), W.Size()));
+ }
+
+private:
+ struct StringKey
+ {
+ uint32_t Offset;
+ uint32_t Length;
+ };
+
+ struct StringHash
+ {
+ const eastl::vector<uint8_t>* Packed;
+ size_t operator()(const StringKey& K) const
+ {
+ std::string_view Sv(reinterpret_cast<const char*>(Packed->data()) + K.Offset, K.Length);
+ return std::hash<std::string_view>{}(Sv);
+ }
+ };
+
+ struct StringEq
+ {
+ const eastl::vector<uint8_t>* Packed;
+ bool operator()(const StringKey& A, const StringKey& B) const
+ {
+ if (A.Length != B.Length)
+ {
+ return false;
+ }
+ return memcmp(Packed->data() + A.Offset, Packed->data() + B.Offset, A.Length) == 0;
+ }
+ };
+
+ eastl::vector<uint8_t> m_Packed; // null-terminated strings back-to-back
+ eastl::vector<uint32_t> m_Offsets; // byte offset into m_Packed for each string
+
+ // Dedup map: StringKey (offset+length into m_Packed) → string index.
+ // Hash/eq functors hold a pointer to m_Packed (stable address) and read
+ // via data() at call time, so reallocation of m_Packed is safe.
+ eastl::hash_map<StringKey, uint32_t, StringHash, StringEq> m_IndexMap;
+};
+
+// ===========================================================================
+// StringTableReader — read-path helper for O(1) string lookup by index
+// ===========================================================================
+
+class StringTableReader
+{
+public:
+ bool Init(const SharedBuffer& Data)
+ {
+ if (Data.GetSize() < sizeof(uint32_t))
+ {
+ return false;
+ }
+
+ const uint8_t* Base = reinterpret_cast<const uint8_t*>(Data.GetData());
+ memcpy(&m_Count, Base, sizeof(uint32_t));
+
+ size_t RequiredHeader = sizeof(uint32_t) + size_t(m_Count) * sizeof(uint32_t);
+ if (Data.GetSize() < RequiredHeader)
+ {
+ return false;
+ }
+
+ m_Offsets = reinterpret_cast<const uint32_t*>(Base + sizeof(uint32_t));
+ m_PackedBase = reinterpret_cast<const char*>(Base + RequiredHeader);
+ m_PackedSize = Data.GetSize() - RequiredHeader;
+ m_OwningBuffer = Data;
+ return true;
+ }
+
+ std::string_view Get(uint32_t Index) const
+ {
+ if (Index >= m_Count)
+ {
+ return {};
+ }
+ uint32_t Off = m_Offsets[Index];
+ if (Off >= m_PackedSize)
+ {
+ return {};
+ }
+ return std::string_view(m_PackedBase + Off);
+ }
+
+ uint32_t Count() const { return m_Count; }
+
+private:
+ uint32_t m_Count = 0;
+ const uint32_t* m_Offsets = nullptr;
+ const char* m_PackedBase = nullptr;
+ size_t m_PackedSize = 0;
+ SharedBuffer m_OwningBuffer; // keeps the decompressed data alive
+};
+
+// ===========================================================================
+// CachedSymbolResolver — SymbolResolver backed by cache data
+// ===========================================================================
+
+class CachedSymbolResolver final : public SymbolResolver
+{
+public:
+ void LoadModule(const ModuleInfo&) override {}
+ std::string Resolve(uint64_t Address) const override
+ {
+ auto It = m_Symbols.find(Address);
+ if (It != m_Symbols.end())
+ {
+ return It->second;
+ }
+ return {};
+ }
+
+ eastl::hash_map<uint64_t, std::string> m_Symbols;
+};
+
+// ===========================================================================
+// Section writers (model → binary blob)
+// ===========================================================================
+
+namespace {
+
+ template<typename T>
+ void WritePod(BinaryWriter& W, const T& Value)
+ {
+ W.Write(&Value, sizeof(T));
+ }
+
+ template<typename T>
+ void WriteCount(BinaryWriter& W, uint32_t Count)
+ {
+ W.Write(&Count, sizeof(Count));
+ }
+
+ SharedBuffer ToSharedBuffer(const BinaryWriter& W) { return SharedBuffer(IoBuffer(IoBuffer::Clone, W.Data(), W.Size())); }
+
+ // -- Metadata section --
+
+ SharedBuffer WriteMetadataSection(const TraceModel& Model, StringTableBuilder& Strings)
+ {
+ BinaryWriter W;
+
+ MetadataPod M = {};
+ M.FileSize = Model.FileSize;
+ M.TotalEvents = Model.TotalEvents;
+ M.ParseTimeMs = Model.ParseTimeMs;
+ M.TraceStartUs = Model.TraceStartUs;
+ M.TraceEndUs = Model.TraceEndUs;
+
+ M.SessionPlatform = Strings.Intern(Model.Session.Platform);
+ M.SessionAppName = Strings.Intern(Model.Session.AppName);
+ M.SessionProjectName = Strings.Intern(Model.Session.ProjectName);
+ M.SessionCommandLine = Strings.Intern(Model.Session.CommandLine);
+ M.SessionBranch = Strings.Intern(Model.Session.Branch);
+ M.SessionBuildVersion = Strings.Intern(Model.Session.BuildVersion);
+ M.SessionChangelist = Model.Session.Changelist;
+ M.SessionConfigType = Model.Session.ConfigurationType;
+ M.SessionHasSession = Model.Session.HasSession ? 1 : 0;
+ WritePod(W, M);
+
+ // Threads
+ uint32_t ThreadCount = uint32_t(Model.Threads.size());
+ WritePod(W, ThreadCount);
+ for (const ThreadInfoEntry& T : Model.Threads)
+ {
+ ThreadInfoPod P = {};
+ P.ThreadId = T.ThreadId;
+ P.Name = Strings.Intern(T.Name);
+ P.GroupName = Strings.Intern(T.GroupName);
+ P.SystemId = T.SystemId;
+ P.SortHint = T.SortHint;
+ WritePod(W, P);
+ }
+
+ // Channels
+ uint32_t ChannelCount = uint32_t(Model.Channels.size());
+ WritePod(W, ChannelCount);
+ for (const ChannelInfo& C : Model.Channels)
+ {
+ ChannelInfoPod P = {};
+ P.Name = Strings.Intern(C.Name);
+ P.Enabled = C.Enabled ? 1 : 0;
+ P.ReadOnly = C.ReadOnly ? 1 : 0;
+ WritePod(W, P);
+ }
+
+ // Modules
+ uint32_t ModuleCount = uint32_t(Model.Modules.size());
+ WritePod(W, ModuleCount);
+
+ // First pass: compute ImageId blob layout
+ eastl::vector<uint32_t> ImageIdOffsets(ModuleCount);
+ uint32_t ImageIdBlobSize = 0;
+ for (uint32_t I = 0; I < ModuleCount; ++I)
+ {
+ ImageIdOffsets[I] = ImageIdBlobSize;
+ ImageIdBlobSize += uint32_t(Model.Modules[I].ImageId.size());
+ }
+
+ for (uint32_t I = 0; I < ModuleCount; ++I)
+ {
+ const ModuleInfo& Mod = Model.Modules[I];
+ ModuleInfoPod P = {};
+ P.Name = Strings.Intern(Mod.Name);
+ P.FullPath = Strings.Intern(Mod.FullPath);
+ P.Base = Mod.Base;
+ P.Size = Mod.Size;
+ P.ImageIdSize = uint32_t(Mod.ImageId.size());
+ P.ImageIdOffset = ImageIdOffsets[I];
+ WritePod(W, P);
+ }
+
+ // ImageId blob
+ for (const ModuleInfo& Mod : Model.Modules)
+ {
+ if (!Mod.ImageId.empty())
+ {
+ W.Write(Mod.ImageId.data(), Mod.ImageId.size());
+ }
+ }
+
+ // EventTypeCounts
+ uint32_t EventTypeCount = uint32_t(Model.EventTypeCounts.size());
+ WritePod(W, EventTypeCount);
+ for (const TraceModel::EventTypeCount& E : Model.EventTypeCounts)
+ {
+ EventTypeCountPod P = {};
+ P.Name = Strings.Intern(E.Name);
+ P.Count = E.Count;
+ WritePod(W, P);
+ }
+
+ // ScopeStats
+ uint32_t ScopeStatCount = uint32_t(Model.ScopeStats.size());
+ WritePod(W, ScopeStatCount);
+ for (const CpuScopeStat& S : Model.ScopeStats)
+ {
+ CpuScopeStatPod P = {};
+ P.Name = Strings.Intern(S.Name);
+ P.MinUs = S.MinUs;
+ P.MaxUs = S.MaxUs;
+ P.Count = S.Count;
+ P.MeanUs = S.MeanUs;
+ P.StdDevUs = S.StdDevUs;
+ WritePod(W, P);
+ }
+
+ return ToSharedBuffer(W);
+ }
+
+ // -- Memory section --
+
+ SharedBuffer WriteMemorySection(const TraceModel& Model, StringTableBuilder& Strings)
+ {
+ BinaryWriter W;
+
+ // AllocSummary
+ AllocSummaryPod A = {};
+ A.HasMemoryData = Model.AllocSummary.HasMemoryData ? 1 : 0;
+ A.PeakTimeUs = Model.AllocSummary.PeakTimeUs;
+ A.LiveAllocations = Model.AllocSummary.LiveAllocations;
+ A.TotalAllocs = Model.AllocSummary.TotalAllocs;
+ A.TotalFrees = Model.AllocSummary.TotalFrees;
+ A.TotalReallocAllocs = Model.AllocSummary.TotalReallocAllocs;
+ A.TotalReallocFrees = Model.AllocSummary.TotalReallocFrees;
+ A.PeakBytes = Model.AllocSummary.PeakBytes;
+ A.EndBytes = Model.AllocSummary.EndBytes;
+ WritePod(W, A);
+
+ // Heaps
+ uint32_t HeapCount = uint32_t(Model.Heaps.size());
+ WritePod(W, HeapCount);
+ for (const HeapInfo& H : Model.Heaps)
+ {
+ HeapInfoPod P = {};
+ P.Id = H.Id;
+ P.ParentId = H.ParentId;
+ P.Flags = H.Flags;
+ P.Name = Strings.Intern(H.Name);
+ WritePod(W, P);
+ }
+
+ // HeapStats
+ uint32_t HeapStatCount = uint32_t(Model.HeapStats.size());
+ WritePod(W, HeapStatCount);
+ for (const HeapStat& S : Model.HeapStats)
+ {
+ HeapStatPod P = {};
+ P.HeapId = S.HeapId;
+ P.CurrentBytes = S.CurrentBytes;
+ P.PeakBytes = S.PeakBytes;
+ P.AllocCount = S.AllocCount;
+ P.FreeCount = S.FreeCount;
+ WritePod(W, P);
+ }
+
+ // CallstackAllocStats
+ uint32_t AllocStatCount = uint32_t(Model.CallstackStats.size());
+ WritePod(W, AllocStatCount);
+ for (const CallstackAllocStat& S : Model.CallstackStats)
+ {
+ CallstackAllocStatPod P = {};
+ P.CallstackId = S.CallstackId;
+ P.LiveCount = S.LiveCount;
+ P.LiveBytes = S.LiveBytes;
+ P.ThreadIdCount = uint32_t(std::min(S.ThreadIds.size(), size_t(4)));
+ for (uint32_t I = 0; I < P.ThreadIdCount; ++I)
+ {
+ P.ThreadIds[I] = S.ThreadIds[I];
+ }
+ WritePod(W, P);
+ }
+
+ // ChurnStats
+ uint32_t ChurnCount = uint32_t(Model.ChurnStats.size());
+ WritePod(W, ChurnCount);
+ for (const CallstackChurnStat& S : Model.ChurnStats)
+ {
+ CallstackChurnStatPod P = {};
+ P.CallstackId = S.CallstackId;
+ P.ChurnAllocs = S.ChurnAllocs;
+ P.ChurnBytes = S.ChurnBytes;
+ P.TotalAllocs = S.TotalAllocs;
+ P.TotalBytes = S.TotalBytes;
+ P.MeanDistance = S.MeanDistance;
+ WritePod(W, P);
+ }
+
+ return ToSharedBuffer(W);
+ }
+
+ // -- Callstacks section --
+
+ SharedBuffer WriteCallstacksSection(const TraceModel& Model)
+ {
+ BinaryWriter W;
+
+ uint32_t Count = uint32_t(Model.Callstacks.size());
+ WritePod(W, Count);
+
+ // Compute frame offsets
+ uint32_t FrameOffset = 0;
+ for (const CallstackEntry& CS : Model.Callstacks)
+ {
+ CallstackHeaderPod H = {};
+ H.Id = CS.Id;
+ H.FrameCount = uint32_t(CS.Frames.size());
+ H.FrameOffset = FrameOffset;
+ WritePod(W, H);
+ FrameOffset += H.FrameCount;
+ }
+
+ // Write all frames
+ for (const CallstackEntry& CS : Model.Callstacks)
+ {
+ for (const ResolvedFrame& F : CS.Frames)
+ {
+ ResolvedFramePod P = {};
+ P.Address = F.Address;
+ P.ModuleIndex = F.ModuleIndex;
+ P.Offset = F.Offset;
+ WritePod(W, P);
+ }
+ }
+
+ return ToSharedBuffer(W);
+ }
+
+ // -- Symbols section --
+
+ SharedBuffer WriteSymbolsSection(const eastl::hash_map<uint64_t, std::string>& ResolvedSymbols, StringTableBuilder& Strings)
+ {
+ BinaryWriter W;
+
+ // Collect and sort entries by address for binary search on read
+ eastl::vector<SymbolEntryPod> Entries;
+ Entries.reserve(ResolvedSymbols.size());
+ for (const auto& [Address, SymbolStr] : ResolvedSymbols)
+ {
+ SymbolEntryPod E = {};
+ E.Address = Address;
+ E.StringIdx = Strings.Intern(SymbolStr);
+ Entries.push_back(E);
+ }
+ eastl::sort(Entries.begin(), Entries.end(), [](const SymbolEntryPod& A, const SymbolEntryPod& B) { return A.Address < B.Address; });
+
+ uint32_t Count = uint32_t(Entries.size());
+ WritePod(W, Count);
+ if (!Entries.empty())
+ {
+ W.Write(Entries.data(), Entries.size() * sizeof(SymbolEntryPod));
+ }
+
+ return ToSharedBuffer(W);
+ }
+
+ // -- Compression helper --
+
+ CompressedBuffer CompressSection(const SharedBuffer& Raw)
+ {
+ return CompressedBuffer::Compress(Raw, OodleCompressor::Mermaid, OodleCompressionLevel::VeryFast);
+ }
+
+ // ===========================================================================
+ // Section readers (binary blob → model)
+ // ===========================================================================
+
+ template<typename T>
+ bool ReadPod(BinaryReader& R, T& Out)
+ {
+ if (R.Remaining() < sizeof(T))
+ {
+ return false;
+ }
+ R.Read(&Out, sizeof(T));
+ return true;
+ }
+
+ bool ReadUint32(BinaryReader& R, uint32_t& Out) { return ReadPod(R, Out); }
+
+ bool ReadMetadataSection(const SharedBuffer& Data, const StringTableReader& Strings, TraceModel& Model)
+ {
+ BinaryReader R(Data.GetData(), Data.GetSize());
+
+ MetadataPod M;
+ if (!ReadPod(R, M))
+ {
+ return false;
+ }
+ Model.FileSize = M.FileSize;
+ Model.TotalEvents = M.TotalEvents;
+ Model.ParseTimeMs = M.ParseTimeMs;
+ Model.TraceStartUs = M.TraceStartUs;
+ Model.TraceEndUs = M.TraceEndUs;
+
+ Model.Session.Platform = std::string(Strings.Get(M.SessionPlatform));
+ Model.Session.AppName = std::string(Strings.Get(M.SessionAppName));
+ Model.Session.ProjectName = std::string(Strings.Get(M.SessionProjectName));
+ Model.Session.CommandLine = std::string(Strings.Get(M.SessionCommandLine));
+ Model.Session.Branch = std::string(Strings.Get(M.SessionBranch));
+ Model.Session.BuildVersion = std::string(Strings.Get(M.SessionBuildVersion));
+ Model.Session.Changelist = M.SessionChangelist;
+ Model.Session.ConfigurationType = M.SessionConfigType;
+ Model.Session.HasSession = (M.SessionHasSession != 0);
+
+ // Threads
+ uint32_t ThreadCount = 0;
+ if (!ReadUint32(R, ThreadCount))
+ {
+ return false;
+ }
+ Model.Threads.resize(ThreadCount);
+ for (uint32_t I = 0; I < ThreadCount; ++I)
+ {
+ ThreadInfoPod P;
+ if (!ReadPod(R, P))
+ {
+ return false;
+ }
+ Model.Threads[I].ThreadId = P.ThreadId;
+ Model.Threads[I].Name = std::string(Strings.Get(P.Name));
+ Model.Threads[I].GroupName = std::string(Strings.Get(P.GroupName));
+ Model.Threads[I].SystemId = P.SystemId;
+ Model.Threads[I].SortHint = P.SortHint;
+ }
+
+ // Channels
+ uint32_t ChannelCount = 0;
+ if (!ReadUint32(R, ChannelCount))
+ {
+ return false;
+ }
+ Model.Channels.resize(ChannelCount);
+ for (uint32_t I = 0; I < ChannelCount; ++I)
+ {
+ ChannelInfoPod P;
+ if (!ReadPod(R, P))
+ {
+ return false;
+ }
+ Model.Channels[I].Name = std::string(Strings.Get(P.Name));
+ Model.Channels[I].Enabled = (P.Enabled != 0);
+ Model.Channels[I].ReadOnly = (P.ReadOnly != 0);
+ }
+
+ // Modules
+ uint32_t ModuleCount = 0;
+ if (!ReadUint32(R, ModuleCount))
+ {
+ return false;
+ }
+
+ // Read ModuleInfoPod entries first, then the ImageId blob
+ eastl::vector<ModuleInfoPod> ModulePods(ModuleCount);
+ for (uint32_t I = 0; I < ModuleCount; ++I)
+ {
+ if (!ReadPod(R, ModulePods[I]))
+ {
+ return false;
+ }
+ }
+
+ // Compute total ImageId blob size
+ uint32_t TotalImageIdSize = 0;
+ for (const ModuleInfoPod& MP : ModulePods)
+ {
+ uint32_t End = MP.ImageIdOffset + MP.ImageIdSize;
+ if (End > TotalImageIdSize)
+ {
+ TotalImageIdSize = End;
+ }
+ }
+
+ const uint8_t* ImageIdBlobBase = nullptr;
+ if (TotalImageIdSize > 0)
+ {
+ if (R.Remaining() < TotalImageIdSize)
+ {
+ return false;
+ }
+ ImageIdBlobBase = reinterpret_cast<const uint8_t*>(R.GetView(TotalImageIdSize).GetData());
+ R.Skip(TotalImageIdSize);
+ }
+
+ Model.Modules.resize(ModuleCount);
+ for (uint32_t I = 0; I < ModuleCount; ++I)
+ {
+ const ModuleInfoPod& MP = ModulePods[I];
+ ModuleInfo& Mod = Model.Modules[I];
+ Mod.Name = std::string(Strings.Get(MP.Name));
+ Mod.FullPath = std::string(Strings.Get(MP.FullPath));
+ Mod.Base = MP.Base;
+ Mod.Size = MP.Size;
+ if (MP.ImageIdSize > 0 && ImageIdBlobBase != nullptr)
+ {
+ Mod.ImageId.assign(ImageIdBlobBase + MP.ImageIdOffset, ImageIdBlobBase + MP.ImageIdOffset + MP.ImageIdSize);
+ }
+ }
+
+ // EventTypeCounts
+ uint32_t EventTypeCount = 0;
+ if (!ReadUint32(R, EventTypeCount))
+ {
+ return false;
+ }
+ Model.EventTypeCounts.resize(EventTypeCount);
+ for (uint32_t I = 0; I < EventTypeCount; ++I)
+ {
+ EventTypeCountPod P;
+ if (!ReadPod(R, P))
+ {
+ return false;
+ }
+ Model.EventTypeCounts[I].Name = std::string(Strings.Get(P.Name));
+ Model.EventTypeCounts[I].Count = P.Count;
+ }
+
+ // ScopeStats
+ uint32_t ScopeStatCount = 0;
+ if (!ReadUint32(R, ScopeStatCount))
+ {
+ return false;
+ }
+ Model.ScopeStats.resize(ScopeStatCount);
+ for (uint32_t I = 0; I < ScopeStatCount; ++I)
+ {
+ CpuScopeStatPod P;
+ if (!ReadPod(R, P))
+ {
+ return false;
+ }
+ Model.ScopeStats[I].Name = std::string(Strings.Get(P.Name));
+ Model.ScopeStats[I].MinUs = P.MinUs;
+ Model.ScopeStats[I].MaxUs = P.MaxUs;
+ Model.ScopeStats[I].Count = P.Count;
+ Model.ScopeStats[I].MeanUs = P.MeanUs;
+ Model.ScopeStats[I].StdDevUs = P.StdDevUs;
+ }
+
+ return true;
+ }
+
+ bool ReadMemorySection(const SharedBuffer& Data, const StringTableReader& Strings, TraceModel& Model)
+ {
+ BinaryReader R(Data.GetData(), Data.GetSize());
+
+ // AllocSummary
+ AllocSummaryPod A;
+ if (!ReadPod(R, A))
+ {
+ return false;
+ }
+ Model.AllocSummary.HasMemoryData = (A.HasMemoryData != 0);
+ Model.AllocSummary.PeakTimeUs = A.PeakTimeUs;
+ Model.AllocSummary.LiveAllocations = A.LiveAllocations;
+ Model.AllocSummary.TotalAllocs = A.TotalAllocs;
+ Model.AllocSummary.TotalFrees = A.TotalFrees;
+ Model.AllocSummary.TotalReallocAllocs = A.TotalReallocAllocs;
+ Model.AllocSummary.TotalReallocFrees = A.TotalReallocFrees;
+ Model.AllocSummary.PeakBytes = A.PeakBytes;
+ Model.AllocSummary.EndBytes = A.EndBytes;
+
+ // Heaps
+ uint32_t HeapCount = 0;
+ if (!ReadUint32(R, HeapCount))
+ {
+ return false;
+ }
+ Model.Heaps.resize(HeapCount);
+ for (uint32_t I = 0; I < HeapCount; ++I)
+ {
+ HeapInfoPod P;
+ if (!ReadPod(R, P))
+ {
+ return false;
+ }
+ Model.Heaps[I].Id = P.Id;
+ Model.Heaps[I].ParentId = P.ParentId;
+ Model.Heaps[I].Flags = P.Flags;
+ Model.Heaps[I].Name = std::string(Strings.Get(P.Name));
+ }
+
+ // HeapStats
+ uint32_t HeapStatCount = 0;
+ if (!ReadUint32(R, HeapStatCount))
+ {
+ return false;
+ }
+ Model.HeapStats.resize(HeapStatCount);
+ for (uint32_t I = 0; I < HeapStatCount; ++I)
+ {
+ HeapStatPod P;
+ if (!ReadPod(R, P))
+ {
+ return false;
+ }
+ Model.HeapStats[I].HeapId = P.HeapId;
+ Model.HeapStats[I].CurrentBytes = P.CurrentBytes;
+ Model.HeapStats[I].PeakBytes = P.PeakBytes;
+ Model.HeapStats[I].AllocCount = P.AllocCount;
+ Model.HeapStats[I].FreeCount = P.FreeCount;
+ }
+
+ // CallstackAllocStats
+ uint32_t AllocStatCount = 0;
+ if (!ReadUint32(R, AllocStatCount))
+ {
+ return false;
+ }
+ Model.CallstackStats.resize(AllocStatCount);
+ for (uint32_t I = 0; I < AllocStatCount; ++I)
+ {
+ CallstackAllocStatPod P;
+ if (!ReadPod(R, P))
+ {
+ return false;
+ }
+ Model.CallstackStats[I].CallstackId = P.CallstackId;
+ Model.CallstackStats[I].LiveCount = P.LiveCount;
+ Model.CallstackStats[I].LiveBytes = P.LiveBytes;
+ for (uint32_t J = 0; J < P.ThreadIdCount && J < 4; ++J)
+ {
+ Model.CallstackStats[I].ThreadIds.push_back(P.ThreadIds[J]);
+ }
+ }
+
+ // ChurnStats
+ uint32_t ChurnCount = 0;
+ if (!ReadUint32(R, ChurnCount))
+ {
+ return false;
+ }
+ Model.ChurnStats.resize(ChurnCount);
+ for (uint32_t I = 0; I < ChurnCount; ++I)
+ {
+ CallstackChurnStatPod P;
+ if (!ReadPod(R, P))
+ {
+ return false;
+ }
+ Model.ChurnStats[I].CallstackId = P.CallstackId;
+ Model.ChurnStats[I].ChurnAllocs = P.ChurnAllocs;
+ Model.ChurnStats[I].ChurnBytes = P.ChurnBytes;
+ Model.ChurnStats[I].TotalAllocs = P.TotalAllocs;
+ Model.ChurnStats[I].TotalBytes = P.TotalBytes;
+ Model.ChurnStats[I].MeanDistance = P.MeanDistance;
+ }
+
+ return true;
+ }
+
+ bool ReadCallstacksSection(const SharedBuffer& Data, TraceModel& Model)
+ {
+ BinaryReader R(Data.GetData(), Data.GetSize());
+
+ uint32_t Count = 0;
+ if (!ReadUint32(R, Count))
+ {
+ return false;
+ }
+
+ // Read headers
+ eastl::vector<CallstackHeaderPod> Headers(Count);
+ for (uint32_t I = 0; I < Count; ++I)
+ {
+ if (!ReadPod(R, Headers[I]))
+ {
+ return false;
+ }
+ }
+
+ // Compute total frame count
+ uint32_t TotalFrames = 0;
+ for (const CallstackHeaderPod& H : Headers)
+ {
+ TotalFrames = std::max(TotalFrames, H.FrameOffset + H.FrameCount);
+ }
+
+ if (R.Remaining() < TotalFrames * sizeof(ResolvedFramePod))
+ {
+ return false;
+ }
+
+ // Read all frames
+ eastl::vector<ResolvedFramePod> AllFrames(TotalFrames);
+ for (uint32_t I = 0; I < TotalFrames; ++I)
+ {
+ if (!ReadPod(R, AllFrames[I]))
+ {
+ return false;
+ }
+ }
+
+ // Build CallstackEntry vector
+ Model.Callstacks.resize(Count);
+ for (uint32_t I = 0; I < Count; ++I)
+ {
+ const CallstackHeaderPod& H = Headers[I];
+ CallstackEntry& CS = Model.Callstacks[I];
+ CS.Id = H.Id;
+ CS.Frames.resize(H.FrameCount);
+ for (uint32_t J = 0; J < H.FrameCount; ++J)
+ {
+ const ResolvedFramePod& FP = AllFrames[H.FrameOffset + J];
+ CS.Frames[J].Address = FP.Address;
+ CS.Frames[J].ModuleIndex = FP.ModuleIndex;
+ CS.Frames[J].Offset = FP.Offset;
+ }
+ }
+
+ return true;
+ }
+
+ bool ReadSymbolsSection(const SharedBuffer& Data, const StringTableReader& Strings, CachedSymbolResolver& Resolver)
+ {
+ BinaryReader R(Data.GetData(), Data.GetSize());
+
+ uint32_t Count = 0;
+ if (!ReadUint32(R, Count))
+ {
+ return false;
+ }
+
+ for (uint32_t I = 0; I < Count; ++I)
+ {
+ SymbolEntryPod E;
+ if (!ReadPod(R, E))
+ {
+ return false;
+ }
+ std::string_view Str = Strings.Get(E.StringIdx);
+ if (!Str.empty())
+ {
+ Resolver.m_Symbols.emplace(E.Address, std::string(Str));
+ }
+ }
+
+ return true;
+ }
+
+ // ===========================================================================
+ // File-level helpers
+ // ===========================================================================
+
+ int64_t GetFileModTimeNs(const std::filesystem::path& Path)
+ {
+ std::error_code Ec;
+ auto ModTime = std::filesystem::last_write_time(Path, Ec);
+ if (Ec)
+ {
+ return 0;
+ }
+ auto Duration = ModTime.time_since_epoch();
+ return std::chrono::duration_cast<std::chrono::nanoseconds>(Duration).count();
+ }
+
+ SharedBuffer DecompressSection(const uint8_t* FileBase, const SectionDirectoryEntry& Dir)
+ {
+ IoBuffer CompressedIo(IoBuffer::Wrap, FileBase + Dir.FileOffset, Dir.CompressedSize);
+
+ IoHash RawHash;
+ uint64_t RawSize = 0;
+ CompressedBuffer CB = CompressedBuffer::FromCompressed(SharedBuffer(std::move(CompressedIo)), RawHash, RawSize);
+ if (CB.IsNull())
+ {
+ return {};
+ }
+ return CB.Decompress();
+ }
+
+} // namespace
+
+// ===========================================================================
+// Public API
+// ===========================================================================
+
+void
+WriteAnalyzeCache(const std::filesystem::path& CachePath,
+ const std::filesystem::path& SourcePath,
+ const TraceModel& Model,
+ const eastl::hash_map<uint64_t, std::string>& ResolvedSymbols)
+{
+ try
+ {
+ StringTableBuilder Strings;
+
+ // Build section payloads (order matters: Symbols and Metadata/Memory
+ // intern strings, so StringTable must be serialized LAST after all
+ // interning is done).
+ SharedBuffer MetadataRaw = WriteMetadataSection(Model, Strings);
+ SharedBuffer MemoryRaw = WriteMemorySection(Model, Strings);
+ SharedBuffer CallstacksRaw = WriteCallstacksSection(Model);
+ SharedBuffer SymbolsRaw = WriteSymbolsSection(ResolvedSymbols, Strings);
+ SharedBuffer StringTableRaw = Strings.Serialize();
+
+ // Compress each section
+ CompressedBuffer Sections[uint32_t(CacheSectionId::Count)];
+ Sections[uint32_t(CacheSectionId::StringTable)] = CompressSection(StringTableRaw);
+ Sections[uint32_t(CacheSectionId::Metadata)] = CompressSection(MetadataRaw);
+ Sections[uint32_t(CacheSectionId::Memory)] = CompressSection(MemoryRaw);
+ Sections[uint32_t(CacheSectionId::Callstacks)] = CompressSection(CallstacksRaw);
+ Sections[uint32_t(CacheSectionId::Symbols)] = CompressSection(SymbolsRaw);
+
+ // Build file header
+ CacheFileHeader Header = {};
+ Header.Magic = kCacheMagic;
+ Header.Version = kCacheVersion;
+
+ std::error_code Ec;
+ Header.SourceFileSize = std::filesystem::file_size(SourcePath, Ec);
+ Header.SourceModTimeNs = GetFileModTimeNs(SourcePath);
+
+ uint32_t SectionCount = uint32_t(CacheSectionId::Count);
+
+ // Compute section directory
+ uint64_t DataOffset = sizeof(CacheFileHeader) + SectionCount * sizeof(SectionDirectoryEntry);
+
+ SectionDirectoryEntry Directory[uint32_t(CacheSectionId::Count)];
+ for (uint32_t I = 0; I < SectionCount; ++I)
+ {
+ Directory[I].SectionId = I;
+ Directory[I].Reserved = 0;
+ Directory[I].FileOffset = DataOffset;
+ Directory[I].CompressedSize = Sections[I].GetCompressedSize();
+ DataOffset += Directory[I].CompressedSize;
+ }
+
+ // Assemble and write the file
+ BinaryWriter FileWriter;
+ FileWriter.Write(&Header, sizeof(Header));
+ FileWriter.Write(Directory, sizeof(Directory));
+
+ // Append compressed blobs
+ for (uint32_t I = 0; I < SectionCount; ++I)
+ {
+ SharedBuffer Flat = std::move(Sections[I]).GetCompressed().Flatten();
+ FileWriter.Write(Flat.GetData(), Flat.GetSize());
+ }
+
+ zen::TemporaryFile::SafeWriteFile(CachePath, FileWriter.GetView());
+
+ ZEN_INFO("Wrote analysis cache {} ({})", CachePath.filename().string(), zen::NiceBytes(FileWriter.Size()));
+ }
+ catch (const std::exception& Ex)
+ {
+ ZEN_WARN("Failed to write analysis cache: {}", Ex.what());
+ }
+}
+
+std::optional<CachedAnalysis>
+TryLoadAnalyzeCache(const std::filesystem::path& CachePath, const std::filesystem::path& SourcePath)
+{
+ std::error_code Ec;
+ if (!std::filesystem::exists(CachePath, Ec))
+ {
+ return std::nullopt;
+ }
+
+ try
+ {
+ FileContents Contents = zen::ReadFile(CachePath);
+ if (!Contents)
+ {
+ return std::nullopt;
+ }
+
+ IoBuffer FileData = Contents.Flatten();
+ if (FileData.Size() < sizeof(CacheFileHeader))
+ {
+ return std::nullopt;
+ }
+
+ const uint8_t* Base = reinterpret_cast<const uint8_t*>(FileData.Data());
+
+ // Validate header
+ CacheFileHeader Header;
+ memcpy(&Header, Base, sizeof(Header));
+
+ if (Header.Magic != kCacheMagic)
+ {
+ ZEN_DEBUG("Analysis cache: bad magic");
+ return std::nullopt;
+ }
+
+ if (Header.Version != kCacheVersion)
+ {
+ ZEN_DEBUG("Analysis cache: version mismatch ({} vs {})", Header.Version, kCacheVersion);
+ return std::nullopt;
+ }
+
+ // Validate source file hasn't changed
+ uint64_t CurrentSize = std::filesystem::file_size(SourcePath, Ec);
+ int64_t CurrentModTime = GetFileModTimeNs(SourcePath);
+
+ if (Header.SourceFileSize != CurrentSize || Header.SourceModTimeNs != CurrentModTime)
+ {
+ ZEN_DEBUG("Analysis cache: source file changed, invalidating");
+ return std::nullopt;
+ }
+
+ // Parse section directory
+ uint32_t SectionCount = uint32_t(CacheSectionId::Count);
+ size_t DirSize = SectionCount * sizeof(SectionDirectoryEntry);
+ if (FileData.Size() < sizeof(CacheFileHeader) + DirSize)
+ {
+ return std::nullopt;
+ }
+
+ SectionDirectoryEntry Directory[uint32_t(CacheSectionId::Count)];
+ memcpy(Directory, Base + sizeof(CacheFileHeader), DirSize);
+
+ // Validate all sections fit in the file
+ for (uint32_t I = 0; I < SectionCount; ++I)
+ {
+ if (Directory[I].FileOffset + Directory[I].CompressedSize > FileData.Size())
+ {
+ ZEN_DEBUG("Analysis cache: section {} truncated", I);
+ return std::nullopt;
+ }
+ }
+
+ // Decompress string table first
+ SharedBuffer StringTableData = DecompressSection(Base, Directory[uint32_t(CacheSectionId::StringTable)]);
+ if (StringTableData.IsNull())
+ {
+ ZEN_DEBUG("Analysis cache: failed to decompress string table");
+ return std::nullopt;
+ }
+
+ StringTableReader Strings;
+ if (!Strings.Init(StringTableData))
+ {
+ ZEN_DEBUG("Analysis cache: invalid string table");
+ return std::nullopt;
+ }
+
+ CachedAnalysis Result;
+ Result.Model.FilePath = SourcePath;
+
+ // Decompress and read each section
+ SharedBuffer MetaData = DecompressSection(Base, Directory[uint32_t(CacheSectionId::Metadata)]);
+ if (MetaData.IsNull() || !ReadMetadataSection(MetaData, Strings, Result.Model))
+ {
+ ZEN_DEBUG("Analysis cache: failed to read metadata section");
+ return std::nullopt;
+ }
+
+ SharedBuffer MemData = DecompressSection(Base, Directory[uint32_t(CacheSectionId::Memory)]);
+ if (MemData.IsNull() || !ReadMemorySection(MemData, Strings, Result.Model))
+ {
+ ZEN_DEBUG("Analysis cache: failed to read memory section");
+ return std::nullopt;
+ }
+
+ SharedBuffer CsData = DecompressSection(Base, Directory[uint32_t(CacheSectionId::Callstacks)]);
+ if (CsData.IsNull() || !ReadCallstacksSection(CsData, Result.Model))
+ {
+ ZEN_DEBUG("Analysis cache: failed to read callstacks section");
+ return std::nullopt;
+ }
+
+ SharedBuffer SymData = DecompressSection(Base, Directory[uint32_t(CacheSectionId::Symbols)]);
+ if (!SymData.IsNull())
+ {
+ auto Resolver = std::make_unique<CachedSymbolResolver>();
+ if (ReadSymbolsSection(SymData, Strings, *Resolver))
+ {
+ Result.Symbols = std::move(Resolver);
+ }
+ }
+
+ ZEN_INFO("Loaded analysis from cache ({})", zen::NiceBytes(FileData.Size()));
+ return Result;
+ }
+ catch (const std::exception& Ex)
+ {
+ ZEN_DEBUG("Analysis cache load failed: {}", Ex.what());
+ return std::nullopt;
+ }
+}
+
+} // namespace zen::trace_detail
diff --git a/src/zen/trace/trace_cache.h b/src/zen/trace/trace_cache.h
new file mode 100644
index 000000000..88778a020
--- /dev/null
+++ b/src/zen/trace/trace_cache.h
@@ -0,0 +1,253 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#pragma once
+
+#include "symbol_resolver.h"
+#include "trace_model.h"
+
+#include <zencore/sharedbuffer.h>
+
+#include <cstdint>
+#include <filesystem>
+#include <memory>
+#include <optional>
+#include <string_view>
+
+ZEN_THIRD_PARTY_INCLUDES_START
+#include <EASTL/hash_map.h>
+ZEN_THIRD_PARTY_INCLUDES_END
+
+namespace zen::trace_detail {
+
+// ---------------------------------------------------------------------------
+// File format constants
+// ---------------------------------------------------------------------------
+
+static constexpr uint32_t kCacheMagic = 0x005A4355; // "UCZ\0"
+static constexpr uint32_t kCacheVersion = 1;
+
+enum class CacheSectionId : uint32_t
+{
+ StringTable = 0,
+ Metadata = 1,
+ Memory = 2,
+ Callstacks = 3,
+ Symbols = 4,
+ Count
+};
+
+// ---------------------------------------------------------------------------
+// On-disk header structures (naturally aligned, no packing)
+//
+// Fields are ordered so natural alignment matches across compilers without
+// needing pragma pack. static_asserts at the bottom of this block pin the
+// layout so a reordering or added field cannot silently break cached files.
+// ---------------------------------------------------------------------------
+
+struct CacheFileHeader
+{
+ uint32_t Magic;
+ uint32_t Version;
+ uint64_t SourceFileSize;
+ int64_t SourceModTimeNs; // last_write_time as nanoseconds since epoch
+ uint64_t Reserved;
+};
+
+struct SectionDirectoryEntry
+{
+ uint32_t SectionId;
+ uint32_t Reserved;
+ uint64_t FileOffset; // byte offset from start of file
+ uint64_t CompressedSize; // size of the CompressedBuffer blob on disk
+};
+
+// ---------------------------------------------------------------------------
+// POD types for memory-mappable section content
+// ---------------------------------------------------------------------------
+
+struct MetadataPod
+{
+ uint64_t FileSize;
+ uint64_t TotalEvents;
+ uint64_t ParseTimeMs;
+ uint32_t TraceStartUs;
+ uint32_t TraceEndUs;
+ // SessionInfo string indices
+ uint32_t SessionPlatform;
+ uint32_t SessionAppName;
+ uint32_t SessionProjectName;
+ uint32_t SessionCommandLine;
+ uint32_t SessionBranch;
+ uint32_t SessionBuildVersion;
+ uint32_t SessionChangelist;
+ uint8_t SessionConfigType;
+ uint8_t SessionHasSession;
+ uint8_t Padding[2];
+};
+
+struct ThreadInfoPod
+{
+ uint32_t ThreadId;
+ uint32_t Name; // string index
+ uint32_t GroupName; // string index
+ uint32_t SystemId;
+ int32_t SortHint;
+ uint32_t Pad;
+};
+
+struct ChannelInfoPod
+{
+ uint32_t Name; // string index
+ uint8_t Enabled;
+ uint8_t ReadOnly;
+ uint8_t Pad[2];
+};
+
+struct ModuleInfoPod
+{
+ uint32_t Name; // string index
+ uint32_t FullPath; // string index
+ uint64_t Base;
+ uint32_t Size;
+ uint32_t ImageIdSize; // byte count in the ImageId blob area
+ uint32_t ImageIdOffset; // byte offset into the ImageId blob area
+ uint32_t Pad;
+};
+
+struct EventTypeCountPod
+{
+ uint32_t Name; // string index
+ uint32_t Pad;
+ uint64_t Count;
+};
+
+struct CpuScopeStatPod
+{
+ uint32_t Name; // string index
+ uint32_t MinUs;
+ uint32_t MaxUs;
+ uint32_t Pad;
+ uint64_t Count;
+ double MeanUs;
+ double StdDevUs;
+};
+
+struct AllocSummaryPod
+{
+ uint8_t HasMemoryData;
+ uint8_t Pad0[3];
+ uint32_t PeakTimeUs;
+ uint32_t LiveAllocations;
+ uint32_t Pad1;
+ uint64_t TotalAllocs;
+ uint64_t TotalFrees;
+ uint64_t TotalReallocAllocs;
+ uint64_t TotalReallocFrees;
+ int64_t PeakBytes;
+ int64_t EndBytes;
+};
+
+struct HeapInfoPod
+{
+ uint32_t Id;
+ uint32_t ParentId;
+ uint16_t Flags;
+ uint16_t Pad0;
+ uint32_t Name; // string index
+};
+
+struct HeapStatPod
+{
+ uint32_t HeapId;
+ uint32_t Pad;
+ int64_t CurrentBytes;
+ int64_t PeakBytes;
+ uint64_t AllocCount;
+ uint64_t FreeCount;
+};
+
+struct CallstackAllocStatPod
+{
+ uint32_t CallstackId;
+ uint32_t LiveCount;
+ int64_t LiveBytes;
+ uint32_t ThreadIdCount;
+ uint32_t ThreadIds[4];
+ uint32_t Pad;
+ uint32_t Pad2;
+};
+
+struct CallstackChurnStatPod
+{
+ uint32_t CallstackId;
+ uint32_t Pad;
+ uint64_t ChurnAllocs;
+ uint64_t ChurnBytes;
+ uint64_t TotalAllocs;
+ uint64_t TotalBytes;
+ double MeanDistance;
+};
+
+struct CallstackHeaderPod
+{
+ uint32_t Id;
+ uint32_t FrameCount;
+ uint32_t FrameOffset; // index into the frames array
+ uint32_t Pad;
+};
+
+struct ResolvedFramePod
+{
+ uint64_t Address;
+ uint32_t ModuleIndex;
+ uint32_t Pad;
+ uint64_t Offset;
+};
+
+struct SymbolEntryPod
+{
+ uint64_t Address;
+ uint32_t StringIdx; // index into the string table
+ uint32_t Pad;
+};
+
+// Pin the on-disk layout. Any change here is a cache format change and must
+// bump kCacheVersion.
+static_assert(sizeof(CacheFileHeader) == 32);
+static_assert(sizeof(SectionDirectoryEntry) == 24);
+static_assert(sizeof(MetadataPod) == 64);
+static_assert(sizeof(ThreadInfoPod) == 24);
+static_assert(sizeof(ChannelInfoPod) == 8);
+static_assert(sizeof(ModuleInfoPod) == 32);
+static_assert(sizeof(EventTypeCountPod) == 16);
+static_assert(sizeof(CpuScopeStatPod) == 40);
+static_assert(sizeof(AllocSummaryPod) == 64);
+static_assert(sizeof(HeapInfoPod) == 16);
+static_assert(sizeof(HeapStatPod) == 40);
+static_assert(sizeof(CallstackAllocStatPod) == 48);
+static_assert(sizeof(CallstackChurnStatPod) == 48);
+static_assert(sizeof(CallstackHeaderPod) == 16);
+static_assert(sizeof(ResolvedFramePod) == 24);
+static_assert(sizeof(SymbolEntryPod) == 16);
+
+// ---------------------------------------------------------------------------
+// Cache read / write API
+// ---------------------------------------------------------------------------
+
+struct CachedAnalysis
+{
+ TraceModel Model;
+ std::unique_ptr<SymbolResolver> Symbols;
+};
+
+// Try to load a cached analysis from the .ucache_z file next to a .utrace.
+// Returns nullopt on any failure (missing, stale, corrupt, version mismatch).
+std::optional<CachedAnalysis> TryLoadAnalyzeCache(const std::filesystem::path& CachePath, const std::filesystem::path& SourcePath);
+
+// Write the analysis cache for future reuse.
+void WriteAnalyzeCache(const std::filesystem::path& CachePath,
+ const std::filesystem::path& SourcePath,
+ const TraceModel& Model,
+ const eastl::hash_map<uint64_t, std::string>& ResolvedSymbols);
+
+} // namespace zen::trace_detail
diff --git a/src/zen/trace/trace_cmd.cpp b/src/zen/trace/trace_cmd.cpp
new file mode 100644
index 000000000..ca24c51a6
--- /dev/null
+++ b/src/zen/trace/trace_cmd.cpp
@@ -0,0 +1,416 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#include "trace_cmd.h"
+
+#include "browser_launcher.h"
+#include "consoleprogress.h"
+#include "symbol_resolver.h"
+#include "trace_analyze.h"
+#include "trace_model.h"
+#include "trace_viewer_service.h"
+
+#include <zencore/except_fmt.h>
+#include <zencore/filesystem.h>
+#include <zencore/fmtutils.h>
+#include <zencore/logging.h>
+#include <zencore/string.h>
+#include <zencore/thread.h>
+#include <zencore/workthreadpool.h>
+#include <zenhttp/httpclient.h>
+#include <zenhttp/httpcommon.h>
+#include <zenhttp/httpserver.h>
+
+#include <filesystem>
+#include <numeric>
+
+using namespace std::literals;
+
+namespace zen {
+
+namespace {
+
+#if ZEN_PLATFORM_WINDOWS
+ constexpr const char* kSymbolBackendHelp = "Symbol backend: auto (default), pdb, dbghelp, llvm, off";
+#elif ZEN_PLATFORM_MAC
+ constexpr const char* kSymbolBackendHelp = "Symbol backend: auto (default - prefers llvm, falls back to atos), llvm, atos, off";
+#else
+ constexpr const char* kSymbolBackendHelp = "Symbol backend: auto (default - uses llvm), llvm, off";
+#endif
+
+} // namespace
+
+//////////////////////////////////////////////////////////////////////////
+// TraceAnalyzeSubCmd
+
+TraceAnalyzeSubCmd::TraceAnalyzeSubCmd() : ZenSubCmdBase("analyze", "Analyze a .utrace file")
+{
+ SubOptions().add_option("", "", "file", "Path to .utrace file", cxxopts::value(m_TraceFilePath), "<filepath>");
+ SubOptions().add_option("",
+ "",
+ "live-allocs",
+ "Dump top N live-allocation callstacks (0 = off, default 50)",
+ cxxopts::value(m_LiveAllocs)->default_value("50"),
+ "<count>");
+ SubOptions().add_option("",
+ "",
+ "churn",
+ "Dump top N allocation-churn callstacks (0 = off, default 0)",
+ cxxopts::value(m_Churn)->default_value("0"),
+ "<count>");
+ SubOptions().add_option("",
+ "",
+ "churn-distance",
+ "Max event distance between alloc and free to count as churn (default 1000)",
+ cxxopts::value(m_ChurnMin)->default_value("1000"),
+ "<events>");
+ SubOptions().add_option("", "", "symbols", kSymbolBackendHelp, cxxopts::value(m_Symbols)->default_value("auto"), "<backend>");
+ SubOptions().add_option("",
+ "",
+ "html-report",
+ "Write a standalone HTML memory report (all live leaks + top 100 churn sites)",
+ cxxopts::value(m_HtmlReportPath),
+ "<filepath>");
+ SubOptions().add_option("",
+ "",
+ "callstack-skip",
+ "Semicolon-separated wildcard patterns for frames to hide from analyzed callstacks",
+ cxxopts::value(m_CallstackSkip),
+ "<pattern;...>");
+ SubOptions().add_option("",
+ "",
+ "no-callstack-heuristic",
+ "Disable leading third-party frame trimming in analyzed callstacks",
+ cxxopts::value(m_NoCallstackHeuristic)->default_value("false"),
+ "<no-callstack-heuristic>");
+ SubOptions().add_option("",
+ "",
+ "no-cache",
+ "Skip reading/writing the .ucache_z analysis cache",
+ cxxopts::value(m_NoCache)->default_value("false"),
+ "<no-cache>");
+ SubOptions().parse_positional({"file"});
+ SubOptions().positional_help("<file.utrace>");
+}
+
+void
+TraceAnalyzeSubCmd::Run(const ZenCliOptions& GlobalOptions)
+{
+ ZEN_UNUSED(GlobalOptions);
+
+ std::filesystem::path FilePath = trace_detail::ResolveTraceFile(m_TraceFilePath, SubOptions());
+
+ trace_detail::AnalyzeOptions Options;
+ Options.LiveAllocsLimit = m_LiveAllocs;
+ Options.ChurnLimit = m_Churn;
+ Options.ChurnDistanceThreshold = uint64_t(m_ChurnMin);
+ Options.Symbols = trace_detail::ParseSymbolBackend(m_Symbols);
+ Options.NoCache = m_NoCache;
+ Options.EnableCallstackHeuristic = !m_NoCallstackHeuristic;
+ Options.HtmlReportPath = m_HtmlReportPath;
+ ForEachStrTok(m_CallstackSkip, ';', [&Options](std::string_view Pattern) {
+ if (!Pattern.empty())
+ {
+ Options.CallstackSkipPatterns.emplace_back(Pattern);
+ }
+ return true;
+ });
+ trace_detail::RunAnalyze(FilePath, Options);
+}
+
+//////////////////////////////////////////////////////////////////////////
+// TraceInspectSubCmd
+
+TraceInspectSubCmd::TraceInspectSubCmd() : ZenSubCmdBase("inspect", "Inspect event schemas in a .utrace file")
+{
+ SubOptions().add_option("", "", "file", "Path to .utrace file", cxxopts::value(m_TraceFilePath), "<filepath>");
+ SubOptions().parse_positional({"file"});
+ SubOptions().positional_help("<file.utrace>");
+}
+
+void
+TraceInspectSubCmd::Run(const ZenCliOptions& GlobalOptions)
+{
+ ZEN_UNUSED(GlobalOptions);
+
+ std::filesystem::path FilePath = trace_detail::ResolveTraceFile(m_TraceFilePath, SubOptions());
+ trace_detail::RunInspect(FilePath);
+}
+
+//////////////////////////////////////////////////////////////////////////
+// TraceServeSubCmd
+
+TraceServeSubCmd::TraceServeSubCmd() : ZenSubCmdBase("serve", "Serve an interactive viewer for a .utrace file")
+{
+ AddAlias("view");
+ SubOptions().add_option("", "", "file", "Path to .utrace file", cxxopts::value(m_TraceFilePath), "<filepath>");
+ SubOptions().add_option("", "p", "port", "Port to listen on", cxxopts::value(m_Port)->default_value("1480"), "<port>");
+ SubOptions().add_option("", "", "bind", "Address to bind to", cxxopts::value(m_Bind)->default_value("127.0.0.1"), "<host>");
+ SubOptions().add_option("", "", "symbols", kSymbolBackendHelp, cxxopts::value(m_Symbols)->default_value("auto"), "<backend>");
+ SubOptions().add_option("",
+ "",
+ "no-browser",
+ "Do not launch a web browser after starting the server",
+ cxxopts::value(m_NoBrowser)->default_value("false"),
+ "<no-browser>");
+ SubOptions().parse_positional({"file"});
+ SubOptions().positional_help("<file.utrace>");
+}
+
+void
+TraceServeSubCmd::Run(const ZenCliOptions& GlobalOptions)
+{
+ ZEN_UNUSED(GlobalOptions);
+
+ std::filesystem::path FilePath = trace_detail::ResolveTraceFile(m_TraceFilePath, SubOptions());
+
+ WorkerThreadPool ThreadPool(gsl::narrow<int>(GetHardwareConcurrency()));
+
+ uint64_t FileSize = uint64_t(std::filesystem::file_size(FilePath));
+ ZEN_CONSOLE("Parsing {} ({})", FilePath.filename().string(), zen::NiceBytes(FileSize));
+
+ std::unique_ptr<ProgressBase> ProgressOwner(CreateConsoleProgress(ConsoleProgressMode::Pretty));
+ std::unique_ptr<ProgressBase::ProgressBar> Progress = ProgressOwner->CreateProgressBar("Parse");
+ trace_detail::TraceModel Model =
+ trace_detail::BuildTraceModel(FilePath, ThreadPool, [&](uint64_t BytesProcessed, uint64_t TotalBytes, uint64_t EventsSoFar) {
+ Progress->UpdateState(
+ {
+ .Task = "Parsing trace",
+ .Details = fmt::format("{} events", zen::ThousandsNum(EventsSoFar)),
+ .TotalCount = TotalBytes,
+ .RemainingCount = TotalBytes - std::min(BytesProcessed, TotalBytes),
+ },
+ false);
+ });
+ Progress->Finish();
+
+ ZEN_CONSOLE(" Events: {}", zen::ThousandsNum(Model.TotalEvents));
+ ZEN_CONSOLE(" Threads: {}", Model.Threads.size());
+ ZEN_CONSOLE(
+ " Scopes: {}",
+ zen::ThousandsNum(std::accumulate(Model.Timelines.begin(),
+ Model.Timelines.end(),
+ size_t(0),
+ [](size_t Acc, const trace_detail::ThreadTimeline& T) { return Acc + T.Scopes.size(); })));
+ ZEN_CONSOLE(" Time: {}", zen::NiceTimeSpanMs(Model.ParseTimeMs));
+
+ std::unique_ptr<trace_detail::SymbolResolver> Symbols = trace_detail::CreateSymbolResolver(trace_detail::ParseSymbolBackend(m_Symbols));
+ for (const trace_detail::ModuleInfo& Mod : Model.Modules)
+ {
+ Symbols->LoadModule(Mod);
+ }
+ ZEN_CONSOLE(" Symbols: {} modules loaded", Model.Modules.size());
+ ZEN_CONSOLE("");
+
+ HttpServerConfig Config;
+ Config.ServerClass = "asio";
+ Config.IsDedicatedServer = false;
+ Config.AllowPortProbing = true;
+ Config.ForceLoopback = (m_Bind == "127.0.0.1" || m_Bind == "localhost" || m_Bind == "::1");
+
+ Ref<HttpServer> Server = CreateHttpServer(Config);
+
+ std::filesystem::path TempDir = std::filesystem::temp_directory_path() / "zen-trace-viewer";
+ std::error_code Ec;
+ std::filesystem::create_directories(TempDir, Ec);
+
+ int EffectivePort = Server->Initialize(m_Port, TempDir);
+ if (EffectivePort <= 0)
+ {
+ throw zen::runtime_error("Failed to initialize HTTP server");
+ }
+
+ TraceViewerService ViewerService(Model, std::move(Symbols));
+ Server->RegisterService(ViewerService);
+
+ std::string Url = fmt::format("http://{}:{}{}", m_Bind, EffectivePort, ViewerService.BaseUri());
+ ZEN_CONSOLE("Serving trace viewer at {}", Url);
+ ZEN_CONSOLE("Press Ctrl+C to stop");
+
+ if (!m_NoBrowser)
+ {
+ try
+ {
+ LaunchBrowser(Url);
+ }
+ catch (const std::exception& E)
+ {
+ ZEN_WARN("Failed to launch browser: {}", E.what());
+ }
+ }
+
+ Server->Run(/*IsInteractiveSession=*/true);
+ Server->Close();
+}
+
+//////////////////////////////////////////////////////////////////////////
+// TraceTrimSubCmd
+
+TraceTrimSubCmd::TraceTrimSubCmd() : ZenSubCmdBase("trim", "Trim a .utrace file to a time range while preserving important events")
+{
+ SubOptions().add_option("", "", "file", "Path to input .utrace file", cxxopts::value(m_TraceFilePath), "<filepath>");
+ SubOptions().add_option("", "o", "output", "Path to output .utrace file", cxxopts::value(m_OutputPath), "<filepath>");
+ SubOptions().add_option("",
+ "",
+ "start",
+ "Start of the time window in seconds from the beginning of the trace",
+ cxxopts::value(m_StartSec)->default_value("0"),
+ "<seconds>");
+ SubOptions().add_option("",
+ "",
+ "end",
+ "End of the time window in seconds from the beginning of the trace",
+ cxxopts::value(m_EndSec)->default_value("0"),
+ "<seconds>");
+ SubOptions().parse_positional({"file"});
+ SubOptions().positional_help("<file.utrace>");
+}
+
+void
+TraceTrimSubCmd::Run(const ZenCliOptions& GlobalOptions)
+{
+ ZEN_UNUSED(GlobalOptions);
+
+ std::filesystem::path InputPath = trace_detail::ResolveTraceFile(m_TraceFilePath, SubOptions());
+
+ if (m_OutputPath.empty())
+ {
+ throw zen::OptionParseException("--output is required", SubOptions().help());
+ }
+ if (m_EndSec <= m_StartSec)
+ {
+ throw zen::OptionParseException("--end must be greater than --start", SubOptions().help());
+ }
+
+ trace_detail::TraceTrimArgs Args;
+ Args.InputPath = InputPath;
+ Args.OutputPath = std::filesystem::absolute(m_OutputPath);
+ Args.StartSec = m_StartSec;
+ Args.EndSec = m_EndSec;
+
+ trace_detail::RunTraceTrim(Args);
+}
+
+//////////////////////////////////////////////////////////////////////////
+// TraceStartSubCmd
+
+TraceStartSubCmd::TraceStartSubCmd() : ZenSubCmdBase("start", "Start zen server realtime tracing")
+{
+ SubOptions().add_option("", "u", "hosturl", ZenCmdBase::kHostUrlHelp, cxxopts::value(m_HostName)->default_value(""), "<hosturl>");
+ SubOptions().add_option("", "", "host", "Stream trace data to a remote host", cxxopts::value(m_TraceHost), "<hostip>");
+ SubOptions().add_option("", "", "file", "Write trace data to a file", cxxopts::value(m_TraceFile), "<filepath>");
+}
+
+void
+TraceStartSubCmd::Run(const ZenCliOptions& GlobalOptions)
+{
+ ZEN_UNUSED(GlobalOptions);
+
+ std::string ResolvedHost = ZenCmdBase::ResolveTargetHostSpec(m_HostName);
+ if (ResolvedHost.empty())
+ {
+ throw OptionParseException("Unable to resolve server specification", SubOptions().help());
+ }
+
+ if (m_TraceHost.empty() && m_TraceFile.empty())
+ {
+ throw OptionParseException("Either --host or --file is required", SubOptions().help());
+ }
+ if (!m_TraceHost.empty() && !m_TraceFile.empty())
+ {
+ throw OptionParseException("--host and --file are mutually exclusive", SubOptions().help());
+ }
+
+ std::string StartArg = m_TraceHost.empty() ? fmt::format("file={}", m_TraceFile) : fmt::format("host={}", m_TraceHost);
+
+ zen::HttpClient Http = ZenCmdBase::CreateHttpClient(ResolvedHost);
+ if (zen::HttpClient::Response Response = Http.Post(fmt::format("/admin/trace/start?{}"sv, StartArg)))
+ {
+ ZEN_CONSOLE("OK: {}", Response.ToText());
+ }
+ else
+ {
+ Response.ThrowError("Trace start failed");
+ }
+}
+
+//////////////////////////////////////////////////////////////////////////
+// TraceStopSubCmd
+
+TraceStopSubCmd::TraceStopSubCmd() : ZenSubCmdBase("stop", "Stop zen server realtime tracing")
+{
+ SubOptions().add_option("", "u", "hosturl", ZenCmdBase::kHostUrlHelp, cxxopts::value(m_HostName)->default_value(""), "<hosturl>");
+}
+
+void
+TraceStopSubCmd::Run(const ZenCliOptions& GlobalOptions)
+{
+ ZEN_UNUSED(GlobalOptions);
+
+ std::string ResolvedHost = ZenCmdBase::ResolveTargetHostSpec(m_HostName);
+ if (ResolvedHost.empty())
+ {
+ throw OptionParseException("Unable to resolve server specification", SubOptions().help());
+ }
+
+ zen::HttpClient Http = ZenCmdBase::CreateHttpClient(ResolvedHost);
+ if (zen::HttpClient::Response Response = Http.Post("/admin/trace/stop"sv))
+ {
+ ZEN_CONSOLE("OK: {}", Response.ToText());
+ }
+ else
+ {
+ Response.ThrowError("Trace stop failed");
+ }
+}
+
+//////////////////////////////////////////////////////////////////////////
+// TraceStatusSubCmd
+
+TraceStatusSubCmd::TraceStatusSubCmd() : ZenSubCmdBase("status", "Report zen server realtime tracing status")
+{
+ SubOptions().add_option("", "u", "hosturl", ZenCmdBase::kHostUrlHelp, cxxopts::value(m_HostName)->default_value(""), "<hosturl>");
+}
+
+void
+TraceStatusSubCmd::Run(const ZenCliOptions& GlobalOptions)
+{
+ ZEN_UNUSED(GlobalOptions);
+
+ std::string ResolvedHost = ZenCmdBase::ResolveTargetHostSpec(m_HostName);
+ if (ResolvedHost.empty())
+ {
+ throw OptionParseException("Unable to resolve server specification", SubOptions().help());
+ }
+
+ zen::HttpClient Http = ZenCmdBase::CreateHttpClient(ResolvedHost);
+ if (zen::HttpClient::Response Response = Http.Get("/admin/trace"sv))
+ {
+ ZEN_CONSOLE("OK: {}", Response.ToText());
+ }
+ else
+ {
+ Response.ThrowError("Trace status failed");
+ }
+}
+
+//////////////////////////////////////////////////////////////////////////
+// TraceCommand
+
+TraceCommand::TraceCommand()
+{
+ m_Options.add_options()("h,help", "Print help");
+ m_Options.add_option("__hidden__", "", "subcommand", "", cxxopts::value<std::string>(m_SubCommand)->default_value(""), "");
+ m_Options.parse_positional({"subcommand"});
+
+ AddSubCommand(m_AnalyzeSubCmd);
+ AddSubCommand(m_InspectSubCmd);
+ AddSubCommand(m_ServeSubCmd);
+ AddSubCommand(m_TrimSubCmd);
+ AddSubCommand(m_StartSubCmd);
+ AddSubCommand(m_StopSubCmd);
+ AddSubCommand(m_StatusSubCmd);
+}
+
+TraceCommand::~TraceCommand() = default;
+
+} // namespace zen
diff --git a/src/zen/trace/trace_cmd.h b/src/zen/trace/trace_cmd.h
new file mode 100644
index 000000000..bb2759241
--- /dev/null
+++ b/src/zen/trace/trace_cmd.h
@@ -0,0 +1,123 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#pragma once
+
+#include "zen.h"
+
+#include <filesystem>
+#include <string>
+
+namespace zen {
+
+class TraceAnalyzeSubCmd : public ZenSubCmdBase
+{
+public:
+ TraceAnalyzeSubCmd();
+ void Run(const ZenCliOptions& GlobalOptions) override;
+
+private:
+ std::filesystem::path m_TraceFilePath;
+ int m_LiveAllocs = 0;
+ int m_Churn = 0;
+ int m_ChurnMin = 1000;
+ std::string m_Symbols;
+ std::string m_CallstackSkip;
+ bool m_NoCache = false;
+ bool m_NoCallstackHeuristic = false;
+ std::filesystem::path m_HtmlReportPath;
+};
+
+class TraceInspectSubCmd : public ZenSubCmdBase
+{
+public:
+ TraceInspectSubCmd();
+ void Run(const ZenCliOptions& GlobalOptions) override;
+
+private:
+ std::filesystem::path m_TraceFilePath;
+};
+
+class TraceServeSubCmd : public ZenSubCmdBase
+{
+public:
+ TraceServeSubCmd();
+ void Run(const ZenCliOptions& GlobalOptions) override;
+
+private:
+ std::filesystem::path m_TraceFilePath;
+ std::string m_Symbols;
+ int m_Port = 0;
+ std::string m_Bind = "127.0.0.1";
+ bool m_NoBrowser = false;
+};
+
+class TraceTrimSubCmd : public ZenSubCmdBase
+{
+public:
+ TraceTrimSubCmd();
+ void Run(const ZenCliOptions& GlobalOptions) override;
+
+private:
+ std::filesystem::path m_TraceFilePath;
+ std::filesystem::path m_OutputPath;
+ double m_StartSec = 0.0;
+ double m_EndSec = 0.0;
+};
+
+class TraceStartSubCmd : public ZenSubCmdBase
+{
+public:
+ TraceStartSubCmd();
+ void Run(const ZenCliOptions& GlobalOptions) override;
+
+private:
+ std::string m_HostName;
+ std::string m_TraceHost;
+ std::string m_TraceFile;
+};
+
+class TraceStopSubCmd : public ZenSubCmdBase
+{
+public:
+ TraceStopSubCmd();
+ void Run(const ZenCliOptions& GlobalOptions) override;
+
+private:
+ std::string m_HostName;
+};
+
+class TraceStatusSubCmd : public ZenSubCmdBase
+{
+public:
+ TraceStatusSubCmd();
+ void Run(const ZenCliOptions& GlobalOptions) override;
+
+private:
+ std::string m_HostName;
+};
+
+class TraceCommand : public ZenCmdWithSubCommands
+{
+public:
+ static constexpr char Name[] = "trace";
+ static constexpr char Description[] = "Control zen realtime tracing and work with .utrace files";
+
+ TraceCommand();
+ ~TraceCommand();
+
+ cxxopts::Options& Options() override { return m_Options; }
+
+private:
+ cxxopts::Options m_Options{Name, Description};
+ std::string m_SubCommand;
+
+ TraceAnalyzeSubCmd m_AnalyzeSubCmd;
+ TraceInspectSubCmd m_InspectSubCmd;
+ TraceServeSubCmd m_ServeSubCmd;
+ TraceTrimSubCmd m_TrimSubCmd;
+ TraceStartSubCmd m_StartSubCmd;
+ TraceStopSubCmd m_StopSubCmd;
+ TraceStatusSubCmd m_StatusSubCmd;
+};
+
+} // namespace zen
diff --git a/src/zen/trace/trace_memory.cpp b/src/zen/trace/trace_memory.cpp
new file mode 100644
index 000000000..704b8bcde
--- /dev/null
+++ b/src/zen/trace/trace_memory.cpp
@@ -0,0 +1,901 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#include "trace_memory.h"
+
+#include "trace_model.h"
+
+#include <zencore/fmtutils.h>
+#include <zencore/logging.h>
+
+ZEN_THIRD_PARTY_INCLUDES_START
+#include <EASTL/sort.h>
+#include <analysis/analyzer.h>
+ZEN_THIRD_PARTY_INCLUDES_END
+
+using namespace zen::trace_detail;
+
+//////////////////////////////////////////////////////////////////////////////
+// Event outlines for Memory.* trace events
+//
+// Field names and types match the UE wire format exactly.
+// See Engine/Source/Runtime/Core/Private/ProfilingDebugging/MemoryTrace.cpp.
+
+// clang-format off
+begin_outline(Memory, Init)
+ field(uint8, Version)
+ field(uint32, MarkerPeriod)
+ field(uint8, MinAlignment)
+ field(uint8, SizeShift)
+ field(uint64, PageSize)
+end_outline()
+
+begin_outline(Memory, Marker)
+ field(uint64, Cycle)
+end_outline()
+
+begin_outline(Memory, Alloc)
+ field(uint64, Address)
+ field(uint32, CallstackId)
+ field(uint32, Size)
+ field(uint8, AlignmentPow2_SizeLower)
+ field(uint8, RootHeap)
+end_outline()
+
+begin_outline(Memory, AllocSystem)
+ field(uint64, Address)
+ field(uint32, CallstackId)
+ field(uint32, Size)
+ field(uint8, AlignmentPow2_SizeLower)
+end_outline()
+
+begin_outline(Memory, AllocVideo)
+ field(uint64, Address)
+ field(uint32, CallstackId)
+ field(uint32, Size)
+ field(uint8, AlignmentPow2_SizeLower)
+end_outline()
+
+begin_outline(Memory, Free)
+ field(uint64, Address)
+ field(uint32, CallstackId)
+ field(uint8, RootHeap)
+end_outline()
+
+begin_outline(Memory, FreeSystem)
+ field(uint64, Address)
+ field(uint32, CallstackId)
+end_outline()
+
+begin_outline(Memory, FreeVideo)
+ field(uint64, Address)
+ field(uint32, CallstackId)
+end_outline()
+
+begin_outline(Memory, ReallocAlloc)
+ field(uint64, Address)
+ field(uint32, CallstackId)
+ field(uint32, Size)
+ field(uint8, AlignmentPow2_SizeLower)
+ field(uint8, RootHeap)
+end_outline()
+
+begin_outline(Memory, ReallocAllocSystem)
+ field(uint64, Address)
+ field(uint32, CallstackId)
+ field(uint32, Size)
+ field(uint8, AlignmentPow2_SizeLower)
+end_outline()
+
+begin_outline(Memory, ReallocFree)
+ field(uint64, Address)
+ field(uint32, CallstackId)
+ field(uint8, RootHeap)
+end_outline()
+
+begin_outline(Memory, ReallocFreeSystem)
+ field(uint64, Address)
+ field(uint32, CallstackId)
+end_outline()
+
+begin_outline(Memory, HeapSpec)
+ field(uint32, Id)
+ field(uint32, ParentId)
+ field(uint16, Flags)
+ field(FieldStr, Name)
+end_outline()
+
+begin_outline(Memory, HeapMarkAlloc)
+ field(uint64, Address)
+ field(uint32, CallstackId)
+ field(uint16, Flags)
+ field(uint32, Heap)
+end_outline()
+
+begin_outline(Memory, HeapUnmarkAlloc)
+ field(uint64, Address)
+ field(uint32, CallstackId)
+ field(uint32, Heap)
+end_outline()
+
+begin_outline(Memory, TagSpec)
+ field(int32, Tag)
+ field(int32, Parent)
+ field(FieldStr, Display)
+end_outline()
+
+begin_outline(Memory, CallstackSpec)
+ field(uint32, CallstackId)
+ field(uint64[], Frames)
+end_outline()
+
+begin_outline(Memory, CallstackSpecDeltaVarInt)
+ field(uint32, CallstackId)
+ field(uint8[], CompressedFrames)
+end_outline()
+
+begin_outline(Memory, CallstackSpecDelta7bit)
+ field(uint32, CallstackId)
+ field(uint8[], CompressedFrames)
+end_outline()
+
+begin_outline(Memory, CallstackSpecXORAndRLE)
+ field(uint32, CallstackId)
+ field(uint8[], CompressedFrames)
+end_outline()
+ // clang-format on
+
+ //////////////////////////////////////////////////////////////////////////////
+ // Callstack decompression helpers
+
+ namespace
+{
+ inline int64_t ZigZagDecode(uint64_t Encoded) { return int64_t(Encoded >> 1) ^ -int64_t(Encoded & 1); }
+
+ // UE VarInt: leading 1-bits in the first byte indicate total byte count.
+ // 0xxxxxxx = 1 byte (7 value bits)
+ // 10xxxxxx = 2 bytes (14 value bits)
+ // 110xxxxx = 3 bytes (21 value bits) ...up to 9 bytes.
+ // Remaining bytes are big-endian value continuation.
+ eastl::vector<uint64_t> DecodeDeltaVarInt(const uint8_t* Data, uint32_t Size)
+ {
+ eastl::vector<uint64_t> Frames;
+ uint64_t Prev = 0;
+ const uint8_t* Cur = Data;
+ const uint8_t* End = Data + Size;
+
+ while (Cur < End)
+ {
+ uint8_t First = *Cur;
+ uint32_t ByteCount = 1;
+ uint8_t Mask = 0x80;
+ while ((First & Mask) && ByteCount < 9)
+ {
+ ByteCount++;
+ Mask >>= 1;
+ }
+
+ if (Cur + ByteCount > End)
+ {
+ break;
+ }
+
+ uint64_t Raw = 0;
+ if (ByteCount == 9)
+ {
+ // First byte is 0xFF; next 8 bytes are the raw value.
+ for (uint32_t I = 1; I <= 8; I++)
+ {
+ Raw = (Raw << 8) | Cur[I];
+ }
+ }
+ else
+ {
+ // First byte contributes value bits after stripping the length prefix.
+ uint8_t ValueMask = uint8_t((1u << (8 - ByteCount)) - 1);
+ Raw = First & ValueMask;
+ for (uint32_t I = 1; I < ByteCount; I++)
+ {
+ Raw = (Raw << 8) | Cur[I];
+ }
+ }
+ Cur += ByteCount;
+
+ int64_t Delta = ZigZagDecode(Raw);
+ Prev = uint64_t(int64_t(Prev) + Delta);
+ Frames.push_back(Prev);
+ }
+
+ return Frames;
+ }
+
+ // 7-bit continuation encoding: bit 7 = more bytes, bits 0-6 = value (little-endian).
+ eastl::vector<uint64_t> DecodeDelta7bit(const uint8_t* Data, uint32_t Size)
+ {
+ eastl::vector<uint64_t> Frames;
+ uint64_t Prev = 0;
+ const uint8_t* Cur = Data;
+ const uint8_t* End = Data + Size;
+
+ while (Cur < End)
+ {
+ uint64_t Raw = 0;
+ uint32_t Shift = 0;
+ for (;;)
+ {
+ if (Cur >= End)
+ {
+ break;
+ }
+ uint8_t Byte = *Cur++;
+ Raw |= uint64_t(Byte & 0x7F) << Shift;
+ Shift += 7;
+ if ((Byte & 0x80) == 0)
+ {
+ break;
+ }
+ }
+
+ int64_t Delta = ZigZagDecode(Raw);
+ Prev = uint64_t(int64_t(Prev) + Delta);
+ Frames.push_back(Prev);
+ }
+
+ return Frames;
+ }
+
+ // XOR + RLE: first byte = leading zero bit count in (frame XOR prev).
+ // Remaining ceil((64 - zeros) / 8) bytes are the non-zero suffix, little-endian.
+ eastl::vector<uint64_t> DecodeXORAndRLE(const uint8_t* Data, uint32_t Size)
+ {
+ eastl::vector<uint64_t> Frames;
+ uint64_t Prev = 0;
+ const uint8_t* Cur = Data;
+ const uint8_t* End = Data + Size;
+
+ while (Cur < End)
+ {
+ uint8_t LeadingZeros = *Cur++;
+ if (LeadingZeros >= 64)
+ {
+ Frames.push_back(Prev);
+ continue;
+ }
+
+ uint32_t ValueBits = 64 - LeadingZeros;
+ uint32_t ValueBytes = (ValueBits + 7) / 8;
+
+ if (Cur + ValueBytes > End)
+ {
+ break;
+ }
+
+ uint64_t XorVal = 0;
+ for (uint32_t I = 0; I < ValueBytes; I++)
+ {
+ XorVal |= uint64_t(Cur[I]) << (I * 8);
+ }
+ Cur += ValueBytes;
+
+ Prev ^= XorVal;
+ Frames.push_back(Prev);
+ }
+
+ return Frames;
+ }
+
+} // anonymous namespace
+
+//////////////////////////////////////////////////////////////////////////////
+// AllocationAnalyzer implementation
+
+AllocationAnalyzer::AllocationAnalyzer(const TraceTiming* Timing) : m_Timing(Timing)
+{
+}
+
+void
+AllocationAnalyzer::subscribe(Vector<Subscription>& Subs)
+{
+ Subs.emplace_back(this, &AllocationAnalyzer::OnInit);
+ Subs.emplace_back(this, &AllocationAnalyzer::OnMarker);
+ Subs.emplace_back(this, &AllocationAnalyzer::OnAlloc);
+ Subs.emplace_back(this, &AllocationAnalyzer::OnAllocSystem);
+ Subs.emplace_back(this, &AllocationAnalyzer::OnAllocVideo);
+ Subs.emplace_back(this, &AllocationAnalyzer::OnFree);
+ Subs.emplace_back(this, &AllocationAnalyzer::OnFreeSystem);
+ Subs.emplace_back(this, &AllocationAnalyzer::OnFreeVideo);
+ Subs.emplace_back(this, &AllocationAnalyzer::OnReallocAlloc);
+ Subs.emplace_back(this, &AllocationAnalyzer::OnReallocAllocSystem);
+ Subs.emplace_back(this, &AllocationAnalyzer::OnReallocFree);
+ Subs.emplace_back(this, &AllocationAnalyzer::OnReallocFreeSystem);
+ Subs.emplace_back(this, &AllocationAnalyzer::OnHeapSpec);
+ Subs.emplace_back(this, &AllocationAnalyzer::OnHeapMarkAlloc);
+ Subs.emplace_back(this, &AllocationAnalyzer::OnHeapUnmarkAlloc);
+ Subs.emplace_back(this, &AllocationAnalyzer::OnTagSpec);
+}
+
+//////////////////////////////////////////////////////////////////////////////
+// Internal helpers
+
+uint64_t
+AllocationAnalyzer::DecodeAllocSize(uint32_t RawSize, uint8_t AlignSizeLower) const
+{
+ uint32_t Shift = m_SizeShift;
+ uint32_t LowMask = (1u << Shift) - 1;
+ return (uint64_t(RawSize) << Shift) | (AlignSizeLower & LowMask);
+}
+
+void
+AllocationAnalyzer::HandleAlloc(uint64_t Address, uint64_t Size, uint8_t RootHeap, uint32_t CallstackId, uint32_t ThreadId, bool IsRealloc)
+{
+ // If address is already tracked (shouldn't normally happen), treat as
+ // implicit free of the old allocation so the counters stay consistent.
+ auto It = m_LiveAllocs.find(Address);
+ if (It != m_LiveAllocs.end())
+ {
+ // Heap-marked allocs were already subtracted from totals in OnHeapMarkAlloc.
+ if (!It->second.IsHeap)
+ {
+ int64_t OldSize = int64_t(It->second.Size);
+ uint8_t OldHeap = It->second.RootHeap;
+ m_CurrentBytes -= OldSize;
+ if (OldHeap == 0)
+ {
+ m_SystemBytes -= OldSize;
+ }
+ else if (OldHeap == 1)
+ {
+ m_VideoBytes -= OldSize;
+ }
+ auto HIt = m_RootHeapStats.find(OldHeap);
+ if (HIt != m_RootHeapStats.end())
+ {
+ HIt->second.CurrentBytes -= OldSize;
+ HIt->second.FreeCount++;
+ }
+ }
+ It->second = LiveAlloc{Size, CallstackId, ThreadId, m_AllocEventSeq, RootHeap, false};
+ }
+ else
+ {
+ m_LiveAllocs.insert({Address, LiveAlloc{Size, CallstackId, ThreadId, m_AllocEventSeq, RootHeap, false}});
+ }
+
+ int64_t SignedSize = int64_t(Size);
+ m_CurrentBytes += SignedSize;
+ if (RootHeap == 0)
+ {
+ m_SystemBytes += SignedSize;
+ }
+ else if (RootHeap == 1)
+ {
+ m_VideoBytes += SignedSize;
+ }
+
+ // Update per-root-heap stats
+ HeapStat& HStat = m_RootHeapStats[RootHeap];
+ HStat.HeapId = RootHeap;
+ HStat.CurrentBytes += SignedSize;
+ HStat.AllocCount++;
+ if (HStat.CurrentBytes > HStat.PeakBytes)
+ {
+ HStat.PeakBytes = HStat.CurrentBytes;
+ }
+
+ // Track global peak
+ if (m_CurrentBytes > m_PeakBytes)
+ {
+ m_PeakBytes = m_CurrentBytes;
+ m_PeakTimeUs = m_LastMarkerTimeUs;
+ }
+
+ if (IsRealloc)
+ {
+ m_TotalReallocAllocs++;
+ }
+ else
+ {
+ m_TotalAllocs++;
+ }
+
+ // Churn tracking
+ m_AllocEventSeq++;
+ if (CallstackId != 0)
+ {
+ ChurnAccum& Churn = m_ChurnByCallstack[CallstackId];
+ Churn.TotalAllocs++;
+ Churn.TotalBytes += Size;
+ }
+
+ // Size histogram: bucket 0 captures zero-size allocs, bucket i (i>=1)
+ // captures sizes in [2^(i-1)+1, 2^i]. Use ceil(log2) so power-of-two
+ // sizes land on their own bucket (e.g. 16 -> bucket 4 = (8, 16]).
+ size_t BucketIndex = 0;
+ if (Size > 0)
+ {
+ uint64_t Shifted = Size - 1;
+ while (Shifted > 0 && BucketIndex < kSizeHistogramBuckets - 1)
+ {
+ Shifted >>= 1;
+ ++BucketIndex;
+ }
+ }
+ m_SizeHistogram[BucketIndex].Count++;
+ m_SizeHistogram[BucketIndex].Bytes += Size;
+}
+
+void
+AllocationAnalyzer::HandleFree(uint64_t Address, uint8_t /*RootHeap*/, uint32_t /*CallstackId*/, bool IsRealloc)
+{
+ auto It = m_LiveAllocs.find(Address);
+ if (It == m_LiveAllocs.end())
+ {
+ // Allocation happened before the trace started -- nothing to subtract.
+ if (IsRealloc)
+ {
+ m_TotalReallocFrees++;
+ }
+ else
+ {
+ m_TotalFrees++;
+ }
+ return;
+ }
+
+ int64_t Size = int64_t(It->second.Size);
+ uint8_t AllocHeap = It->second.RootHeap;
+ bool WasHeap = It->second.IsHeap;
+ uint32_t AllocCsId = It->second.CallstackId;
+ uint64_t AllocEventSeq = It->second.EventSeq;
+
+ // Heap-marked allocs were already subtracted from totals in OnHeapMarkAlloc.
+ if (!WasHeap)
+ {
+ m_CurrentBytes -= Size;
+ if (AllocHeap == 0)
+ {
+ m_SystemBytes -= Size;
+ }
+ else if (AllocHeap == 1)
+ {
+ m_VideoBytes -= Size;
+ }
+
+ auto HIt = m_RootHeapStats.find(AllocHeap);
+ if (HIt != m_RootHeapStats.end())
+ {
+ HIt->second.CurrentBytes -= Size;
+ HIt->second.FreeCount++;
+ }
+ }
+
+ m_LiveAllocs.erase(It);
+
+ // Churn tracking: record event distance for this alloc→free pair.
+ // Short distances indicate short-lived (churny) allocations.
+ if (AllocCsId != 0)
+ {
+ uint64_t Distance = m_AllocEventSeq - AllocEventSeq;
+ auto ChurnIt = m_ChurnByCallstack.find(AllocCsId);
+ if (ChurnIt != m_ChurnByCallstack.end())
+ {
+ ChurnIt->second.ChurnDistanceSum += Distance;
+ ChurnIt->second.ChurnAllocs++;
+ ChurnIt->second.ChurnBytes += uint64_t(Size);
+ }
+ }
+
+ if (IsRealloc)
+ {
+ m_TotalReallocFrees++;
+ }
+ else
+ {
+ m_TotalFrees++;
+ }
+}
+
+void
+AllocationAnalyzer::MaybeEmitSample(uint32_t TimeUs)
+{
+ if (TimeUs < m_LastSampleTimeUs + kTimelineSampleIntervalUs)
+ {
+ return;
+ }
+ m_LastSampleTimeUs = TimeUs;
+ m_Timeline.push_back(MemoryTimelineSample{
+ .TimeUs = TimeUs,
+ .TotalAllocatedBytes = m_CurrentBytes,
+ .SystemBytes = m_SystemBytes,
+ .VideoBytes = m_VideoBytes,
+ });
+}
+
+//////////////////////////////////////////////////////////////////////////////
+// Event handlers
+
+void
+AllocationAnalyzer::OnInit(const ::Memory_Init& Ev)
+{
+ m_SizeShift = Ev.SizeShift();
+ m_Initialized = true;
+ ZEN_DEBUG("Memory trace init: version={}, sizeShift={}, minAlignment={}, markerPeriod={}, pageSize={}",
+ Ev.Version(),
+ m_SizeShift,
+ Ev.MinAlignment(),
+ Ev.MarkerPeriod(),
+ Ev.PageSize());
+}
+
+void
+AllocationAnalyzer::OnMarker(const ::Memory_Marker& Ev)
+{
+ if (!m_Timing || m_Timing->Freq == 0)
+ {
+ return;
+ }
+ uint32_t TimeUs = m_Timing->CycleToTimeUs(Ev.Cycle());
+ m_LastMarkerTimeUs = TimeUs;
+ m_HasReceivedMarker = true;
+ MaybeEmitSample(TimeUs);
+}
+
+void
+AllocationAnalyzer::OnAlloc(const ::Memory_Alloc& Ev)
+{
+ uint64_t Size = DecodeAllocSize(Ev.Size(), Ev.AlignmentPow2_SizeLower());
+ HandleAlloc(Ev.Address(), Size, Ev.RootHeap(), Ev.CallstackId(), Ev.get_thread_id(), /*IsRealloc=*/false);
+}
+
+void
+AllocationAnalyzer::OnAllocSystem(const ::Memory_AllocSystem& Ev)
+{
+ uint64_t Size = DecodeAllocSize(Ev.Size(), Ev.AlignmentPow2_SizeLower());
+ HandleAlloc(Ev.Address(), Size, /*RootHeap=*/0, Ev.CallstackId(), Ev.get_thread_id(), /*IsRealloc=*/false);
+}
+
+void
+AllocationAnalyzer::OnAllocVideo(const ::Memory_AllocVideo& Ev)
+{
+ uint64_t Size = DecodeAllocSize(Ev.Size(), Ev.AlignmentPow2_SizeLower());
+ HandleAlloc(Ev.Address(), Size, /*RootHeap=*/1, Ev.CallstackId(), Ev.get_thread_id(), /*IsRealloc=*/false);
+}
+
+void
+AllocationAnalyzer::OnFree(const ::Memory_Free& Ev)
+{
+ HandleFree(Ev.Address(), Ev.RootHeap(), Ev.CallstackId(), /*IsRealloc=*/false);
+}
+
+void
+AllocationAnalyzer::OnFreeSystem(const ::Memory_FreeSystem& Ev)
+{
+ HandleFree(Ev.Address(), /*RootHeap=*/0, Ev.CallstackId(), /*IsRealloc=*/false);
+}
+
+void
+AllocationAnalyzer::OnFreeVideo(const ::Memory_FreeVideo& Ev)
+{
+ HandleFree(Ev.Address(), /*RootHeap=*/1, Ev.CallstackId(), /*IsRealloc=*/false);
+}
+
+void
+AllocationAnalyzer::OnReallocAlloc(const ::Memory_ReallocAlloc& Ev)
+{
+ uint64_t Size = DecodeAllocSize(Ev.Size(), Ev.AlignmentPow2_SizeLower());
+ HandleAlloc(Ev.Address(), Size, Ev.RootHeap(), Ev.CallstackId(), Ev.get_thread_id(), /*IsRealloc=*/true);
+}
+
+void
+AllocationAnalyzer::OnReallocAllocSystem(const ::Memory_ReallocAllocSystem& Ev)
+{
+ uint64_t Size = DecodeAllocSize(Ev.Size(), Ev.AlignmentPow2_SizeLower());
+ HandleAlloc(Ev.Address(), Size, /*RootHeap=*/0, Ev.CallstackId(), Ev.get_thread_id(), /*IsRealloc=*/true);
+}
+
+void
+AllocationAnalyzer::OnReallocFree(const ::Memory_ReallocFree& Ev)
+{
+ HandleFree(Ev.Address(), Ev.RootHeap(), Ev.CallstackId(), /*IsRealloc=*/true);
+}
+
+void
+AllocationAnalyzer::OnReallocFreeSystem(const ::Memory_ReallocFreeSystem& Ev)
+{
+ HandleFree(Ev.Address(), /*RootHeap=*/0, Ev.CallstackId(), /*IsRealloc=*/true);
+}
+
+void
+AllocationAnalyzer::OnHeapSpec(const ::Memory_HeapSpec& Ev)
+{
+ uint32_t Id = Ev.Id();
+ HeapInfo& Info = m_Heaps[Id];
+ Info.Id = Id;
+ Info.ParentId = Ev.ParentId();
+ Info.Flags = Ev.Flags();
+ Info.Name = SafeFieldStr(Ev.Name());
+}
+
+void
+AllocationAnalyzer::OnHeapMarkAlloc(const ::Memory_HeapMarkAlloc& Ev)
+{
+ uint64_t Address = Ev.Address();
+ auto It = m_LiveAllocs.find(Address);
+ if (It == m_LiveAllocs.end())
+ {
+ return;
+ }
+
+ LiveAlloc& Alloc = It->second;
+ if (Alloc.IsHeap)
+ {
+ return; // already marked
+ }
+
+ Alloc.IsHeap = true;
+
+ // Remove this allocation from the running totals — heap-marked
+ // allocations represent address-space reservations (e.g. module images)
+ // and should not count towards the regular memory budget.
+ int64_t SignedSize = int64_t(Alloc.Size);
+ m_CurrentBytes -= SignedSize;
+ if (Alloc.RootHeap == 0)
+ {
+ m_SystemBytes -= SignedSize;
+ }
+ else if (Alloc.RootHeap == 1)
+ {
+ m_VideoBytes -= SignedSize;
+ }
+ auto HIt = m_RootHeapStats.find(Alloc.RootHeap);
+ if (HIt != m_RootHeapStats.end())
+ {
+ HIt->second.CurrentBytes -= SignedSize;
+ }
+}
+
+void
+AllocationAnalyzer::OnHeapUnmarkAlloc(const ::Memory_HeapUnmarkAlloc& Ev)
+{
+ uint64_t Address = Ev.Address();
+ auto It = m_LiveAllocs.find(Address);
+ if (It == m_LiveAllocs.end())
+ {
+ return;
+ }
+
+ LiveAlloc& Alloc = It->second;
+ if (!Alloc.IsHeap)
+ {
+ return; // not marked
+ }
+
+ Alloc.IsHeap = false;
+
+ // Add back to running totals.
+ int64_t SignedSize = int64_t(Alloc.Size);
+ m_CurrentBytes += SignedSize;
+ if (Alloc.RootHeap == 0)
+ {
+ m_SystemBytes += SignedSize;
+ }
+ else if (Alloc.RootHeap == 1)
+ {
+ m_VideoBytes += SignedSize;
+ }
+ auto HIt = m_RootHeapStats.find(Alloc.RootHeap);
+ if (HIt != m_RootHeapStats.end())
+ {
+ HIt->second.CurrentBytes += SignedSize;
+ }
+}
+
+void
+AllocationAnalyzer::OnTagSpec(const ::Memory_TagSpec& Ev)
+{
+ int32_t Tag = Ev.Tag();
+ TagInfo& Info = m_Tags[Tag];
+ Info.Tag = Tag;
+ Info.Parent = Ev.Parent();
+ Info.Display = SafeFieldStr(Ev.Display());
+}
+
+//////////////////////////////////////////////////////////////////////////////
+// Public accessors
+
+AllocationSummary
+AllocationAnalyzer::Summary() const
+{
+ AllocationSummary S;
+ S.HasMemoryData = m_Initialized || m_TotalAllocs > 0;
+ S.TotalAllocs = m_TotalAllocs;
+ S.TotalFrees = m_TotalFrees;
+ S.TotalReallocAllocs = m_TotalReallocAllocs;
+ S.TotalReallocFrees = m_TotalReallocFrees;
+ S.PeakBytes = m_PeakBytes;
+ S.PeakTimeUs = m_PeakTimeUs;
+ S.EndBytes = m_CurrentBytes;
+
+ uint32_t LiveCount = 0;
+ for (const auto& [Addr, Alloc] : m_LiveAllocs)
+ {
+ if (!Alloc.IsHeap)
+ {
+ ++LiveCount;
+ }
+ }
+ S.LiveAllocations = LiveCount;
+ return S;
+}
+
+void
+AllocationAnalyzer::EmitFinalSample(uint32_t TraceEndUs)
+{
+ if (!m_Initialized)
+ {
+ return;
+ }
+ // Force-emit a final sample at the trace end so the timeline captures
+ // the terminal memory state even if no Marker arrived recently.
+ uint32_t FinalTimeUs = m_HasReceivedMarker ? std::max(m_LastMarkerTimeUs, TraceEndUs) : TraceEndUs;
+ m_Timeline.push_back(MemoryTimelineSample{
+ .TimeUs = FinalTimeUs,
+ .TotalAllocatedBytes = m_CurrentBytes,
+ .SystemBytes = m_SystemBytes,
+ .VideoBytes = m_VideoBytes,
+ });
+}
+
+eastl::vector<CallstackAllocStat>
+AllocationAnalyzer::BuildCallstackStats() const
+{
+ eastl::hash_map<uint32_t, CallstackAllocStat> Map;
+ for (const auto& [Addr, Alloc] : m_LiveAllocs)
+ {
+ if (Alloc.CallstackId == 0 || Alloc.IsHeap)
+ {
+ continue;
+ }
+ CallstackAllocStat& S = Map[Alloc.CallstackId];
+ S.CallstackId = Alloc.CallstackId;
+ S.LiveBytes += int64_t(Alloc.Size);
+ S.LiveCount++;
+ if (eastl::find(S.ThreadIds.begin(), S.ThreadIds.end(), Alloc.ThreadId) == S.ThreadIds.end())
+ {
+ S.ThreadIds.push_back(Alloc.ThreadId);
+ }
+ }
+
+ eastl::vector<CallstackAllocStat> Result;
+ Result.reserve(Map.size());
+ for (auto& [Id, Stat] : Map)
+ {
+ Result.push_back(Stat);
+ }
+ eastl::sort(Result.begin(), Result.end(), [](const CallstackAllocStat& A, const CallstackAllocStat& B) {
+ return A.LiveBytes > B.LiveBytes;
+ });
+ return Result;
+}
+
+eastl::vector<CallstackChurnStat>
+AllocationAnalyzer::BuildChurnStats(uint64_t ChurnDistanceThreshold) const
+{
+ // The ChurnAccum already separates total allocs from churny allocs.
+ // ChurnAllocs/ChurnBytes count every freed allocation (regardless of
+ // distance). We now need to re-bucket using the threshold. But since
+ // we only stored the sum of distances (not per-alloc distances), we
+ // use the average: if MeanDistance <= threshold, all freed allocs from
+ // that callstack are considered churny. This is an approximation —
+ // a per-alloc histogram would be more precise but much more expensive.
+ eastl::vector<CallstackChurnStat> Result;
+ Result.reserve(m_ChurnByCallstack.size());
+ for (const auto& [Id, Churn] : m_ChurnByCallstack)
+ {
+ if (Churn.ChurnAllocs == 0)
+ {
+ continue;
+ }
+ double MeanDist = double(Churn.ChurnDistanceSum) / double(Churn.ChurnAllocs);
+ if (MeanDist > double(ChurnDistanceThreshold))
+ {
+ continue;
+ }
+ CallstackChurnStat S;
+ S.CallstackId = Id;
+ S.ChurnAllocs = Churn.ChurnAllocs;
+ S.ChurnBytes = Churn.ChurnBytes;
+ S.TotalAllocs = Churn.TotalAllocs;
+ S.TotalBytes = Churn.TotalBytes;
+ S.MeanDistance = MeanDist;
+ Result.push_back(S);
+ }
+ eastl::sort(Result.begin(), Result.end(), [](const CallstackChurnStat& A, const CallstackChurnStat& B) {
+ return A.ChurnAllocs > B.ChurnAllocs;
+ });
+ return Result;
+}
+
+eastl::vector<AllocSizeBucket>
+AllocationAnalyzer::BuildSizeHistogram() const
+{
+ eastl::vector<AllocSizeBucket> Result;
+ Result.reserve(kSizeHistogramBuckets);
+ for (size_t I = 0; I < kSizeHistogramBuckets; ++I)
+ {
+ const SizeBucketAccum& Accum = m_SizeHistogram[I];
+ if (Accum.Count == 0)
+ {
+ continue;
+ }
+ AllocSizeBucket Bucket;
+ if (I == 0)
+ {
+ Bucket.MinSize = 0;
+ Bucket.MaxSize = 0;
+ }
+ else
+ {
+ // Bucket i covers (2^(i-1), 2^i]; bucket 1 is just size 1.
+ Bucket.MinSize = (I == 1) ? 1 : ((uint64_t(1) << (I - 1)) + 1);
+ Bucket.MaxSize = (I >= 64) ? ~uint64_t(0) : (uint64_t(1) << I);
+ }
+ Bucket.Count = Accum.Count;
+ Bucket.Bytes = Accum.Bytes;
+ Result.push_back(Bucket);
+ }
+ return Result;
+}
+
+//////////////////////////////////////////////////////////////////////////////
+// CallstackAnalyzer implementation
+
+void
+CallstackAnalyzer::subscribe(Vector<Subscription>& Subs)
+{
+ Subs.emplace_back(this, &CallstackAnalyzer::OnCallstackSpec);
+ Subs.emplace_back(this, &CallstackAnalyzer::OnCallstackSpecDeltaVarInt);
+ Subs.emplace_back(this, &CallstackAnalyzer::OnCallstackSpecDelta7bit);
+ Subs.emplace_back(this, &CallstackAnalyzer::OnCallstackSpecXORAndRLE);
+}
+
+void
+CallstackAnalyzer::StoreCallstack(uint32_t Id, const uint64_t* Frames, size_t Count)
+{
+ if (Id == 0 || Count == 0)
+ {
+ return;
+ }
+ auto& Entry = m_Callstacks[Id];
+ Entry.assign(Frames, Frames + Count);
+}
+
+void
+CallstackAnalyzer::OnCallstackSpec(const ::Memory_CallstackSpec& Ev)
+{
+ Array<uint64[]> Frames = Ev.Frames();
+ StoreCallstack(Ev.CallstackId(), Frames.get(), Frames.get_count());
+}
+
+void
+CallstackAnalyzer::OnCallstackSpecDeltaVarInt(const ::Memory_CallstackSpecDeltaVarInt& Ev)
+{
+ Array<uint8[]> Compressed = Ev.CompressedFrames();
+ eastl::vector<uint64_t> Frames = DecodeDeltaVarInt(Compressed.get(), Compressed.get_size());
+ StoreCallstack(Ev.CallstackId(), Frames.data(), Frames.size());
+}
+
+void
+CallstackAnalyzer::OnCallstackSpecDelta7bit(const ::Memory_CallstackSpecDelta7bit& Ev)
+{
+ Array<uint8[]> Compressed = Ev.CompressedFrames();
+ eastl::vector<uint64_t> Frames = DecodeDelta7bit(Compressed.get(), Compressed.get_size());
+ StoreCallstack(Ev.CallstackId(), Frames.data(), Frames.size());
+}
+
+void
+CallstackAnalyzer::OnCallstackSpecXORAndRLE(const ::Memory_CallstackSpecXORAndRLE& Ev)
+{
+ Array<uint8[]> Compressed = Ev.CompressedFrames();
+ eastl::vector<uint64_t> Frames = DecodeXORAndRLE(Compressed.get(), Compressed.get_size());
+ StoreCallstack(Ev.CallstackId(), Frames.data(), Frames.size());
+}
diff --git a/src/zen/trace/trace_memory.h b/src/zen/trace/trace_memory.h
new file mode 100644
index 000000000..da33d8218
--- /dev/null
+++ b/src/zen/trace/trace_memory.h
@@ -0,0 +1,301 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#pragma once
+
+#include <zencore/zencore.h>
+
+ZEN_THIRD_PARTY_INCLUDES_START
+#include <EASTL/fixed_vector.h>
+#include <EASTL/hash_map.h>
+#include <EASTL/vector.h>
+#include <analysis/analyzer.h>
+ZEN_THIRD_PARTY_INCLUDES_END
+
+#include <cstdint>
+#include <string>
+
+// Forward declarations of outline types (defined in trace_memory.cpp).
+// These are global-scope structs created by the begin_outline() macro.
+struct Memory_Init;
+struct Memory_Marker;
+struct Memory_Alloc;
+struct Memory_AllocSystem;
+struct Memory_AllocVideo;
+struct Memory_Free;
+struct Memory_FreeSystem;
+struct Memory_FreeVideo;
+struct Memory_ReallocAlloc;
+struct Memory_ReallocAllocSystem;
+struct Memory_ReallocFree;
+struct Memory_ReallocFreeSystem;
+struct Memory_HeapSpec;
+struct Memory_HeapMarkAlloc;
+struct Memory_HeapUnmarkAlloc;
+struct Memory_TagSpec;
+struct Memory_CallstackSpec;
+struct Memory_CallstackSpecDeltaVarInt;
+struct Memory_CallstackSpecDelta7bit;
+struct Memory_CallstackSpecXORAndRLE;
+
+namespace zen::trace_detail {
+
+struct TraceTiming;
+
+// -- Allocation data structures --------------------------------------------
+
+struct HeapInfo
+{
+ uint32_t Id = 0;
+ uint32_t ParentId = ~0u;
+ uint16_t Flags = 0; // EMemoryTraceHeapFlags bits
+ std::string Name;
+};
+
+struct TagInfo
+{
+ int32_t Tag = 0;
+ int32_t Parent = 0;
+ std::string Display;
+};
+
+struct MemoryTimelineSample
+{
+ uint32_t TimeUs;
+ int64_t TotalAllocatedBytes;
+ int64_t SystemBytes;
+ int64_t VideoBytes;
+};
+
+struct HeapStat
+{
+ uint32_t HeapId = 0;
+ int64_t CurrentBytes = 0;
+ int64_t PeakBytes = 0;
+ uint64_t AllocCount = 0;
+ uint64_t FreeCount = 0;
+};
+
+struct AllocationSummary
+{
+ bool HasMemoryData = false;
+ uint64_t TotalAllocs = 0;
+ uint64_t TotalFrees = 0;
+ uint64_t TotalReallocAllocs = 0;
+ uint64_t TotalReallocFrees = 0;
+ int64_t PeakBytes = 0;
+ uint32_t PeakTimeUs = 0;
+ int64_t EndBytes = 0;
+ uint32_t LiveAllocations = 0;
+};
+
+// One power-of-two bucket of the allocation size histogram. The bucket covers
+// sizes in [MinSize, MaxSize] inclusive (MaxSize = MinSize*2 - 1, or 0 for the
+// zero-size bucket). Count and Bytes aggregate every alloc/realloc-alloc seen
+// during the trace (not just currently-live allocations).
+struct AllocSizeBucket
+{
+ uint64_t MinSize = 0;
+ uint64_t MaxSize = 0;
+ uint64_t Count = 0;
+ uint64_t Bytes = 0;
+};
+
+// -- Callstack data structures ---------------------------------------------
+
+// A single resolved stack frame. ModuleIndex references TraceModel::Modules;
+// ~0u means the frame did not map to any loaded module.
+struct ResolvedFrame
+{
+ uint64_t Address = 0;
+ uint32_t ModuleIndex = ~0u;
+ uint64_t Offset = 0;
+};
+
+// A decoded callstack: the ordered list of instruction-pointer frames
+// captured at the point of an allocation (or free).
+struct CallstackEntry
+{
+ uint32_t Id = 0;
+ eastl::vector<ResolvedFrame> Frames; // outermost (caller) first
+};
+
+// Per-callstack allocation churn statistics. "Churn" is measured by how
+// quickly an allocation is freed — specifically, the number of alloc events
+// that occur between the alloc and its matching free (event distance).
+struct CallstackChurnStat
+{
+ uint32_t CallstackId = 0;
+ uint64_t ChurnAllocs = 0; // allocations freed within the distance threshold
+ uint64_t ChurnBytes = 0; // cumulative bytes of those short-lived allocations
+ uint64_t TotalAllocs = 0; // all allocations from this callstack (for context)
+ uint64_t TotalBytes = 0;
+ double MeanDistance = 0.0; // average event distance for the churny allocs
+};
+
+// Per-callstack live allocation statistics.
+struct CallstackAllocStat
+{
+ uint32_t CallstackId = 0;
+ int64_t LiveBytes = 0;
+ uint32_t LiveCount = 0;
+ eastl::fixed_vector<uint32_t, 4, true> ThreadIds; // unique thread IDs that contributed allocations
+};
+
+// -- AllocationAnalyzer ----------------------------------------------------
+
+// Subscribes to Memory.* trace events and tracks aggregate allocation
+// statistics, a memory-over-time timeline, heap specs, and tag specs.
+// Intended to be instantiated by BuildTraceModel alongside the other
+// analyzers and registered with the Dispatcher.
+class AllocationAnalyzer : public Analyzer
+{
+public:
+ explicit AllocationAnalyzer(const TraceTiming* Timing);
+
+ void subscribe(Vector<Subscription>& Subs) override;
+
+ // -- Accessors (call after IterateTrace completes) --
+
+ bool Initialized() const { return m_Initialized; }
+ AllocationSummary Summary() const;
+ void EmitFinalSample(uint32_t TraceEndUs);
+
+ eastl::vector<MemoryTimelineSample>& MutableTimeline() { return m_Timeline; }
+ const eastl::hash_map<uint32_t, HeapInfo>& Heaps() const { return m_Heaps; }
+ const eastl::hash_map<int32_t, TagInfo>& Tags() const { return m_Tags; }
+ const eastl::hash_map<uint8_t, HeapStat>& RootHeapStats() const { return m_RootHeapStats; }
+
+ // Build per-callstack statistics from the current live allocation set.
+ eastl::vector<CallstackAllocStat> BuildCallstackStats() const;
+
+ // Build per-callstack churn statistics sorted by churn alloc count descending.
+ // ChurnDistanceThreshold: allocations freed within this many alloc-events are
+ // considered "short-lived" / churny.
+ eastl::vector<CallstackChurnStat> BuildChurnStats(uint64_t ChurnDistanceThreshold = 1000) const;
+
+ // Build a size-bucketed histogram of all observed allocations. Returns
+ // only populated buckets, ordered by MinSize ascending.
+ eastl::vector<AllocSizeBucket> BuildSizeHistogram() const;
+
+private:
+ // -- Event handlers --
+
+ void OnInit(const ::Memory_Init& Ev);
+ void OnMarker(const ::Memory_Marker& Ev);
+ void OnAlloc(const ::Memory_Alloc& Ev);
+ void OnAllocSystem(const ::Memory_AllocSystem& Ev);
+ void OnAllocVideo(const ::Memory_AllocVideo& Ev);
+ void OnFree(const ::Memory_Free& Ev);
+ void OnFreeSystem(const ::Memory_FreeSystem& Ev);
+ void OnFreeVideo(const ::Memory_FreeVideo& Ev);
+ void OnReallocAlloc(const ::Memory_ReallocAlloc& Ev);
+ void OnReallocAllocSystem(const ::Memory_ReallocAllocSystem& Ev);
+ void OnReallocFree(const ::Memory_ReallocFree& Ev);
+ void OnReallocFreeSystem(const ::Memory_ReallocFreeSystem& Ev);
+ void OnHeapSpec(const ::Memory_HeapSpec& Ev);
+ void OnHeapMarkAlloc(const ::Memory_HeapMarkAlloc& Ev);
+ void OnHeapUnmarkAlloc(const ::Memory_HeapUnmarkAlloc& Ev);
+ void OnTagSpec(const ::Memory_TagSpec& Ev);
+
+ // -- Internal helpers --
+
+ struct LiveAlloc
+ {
+ uint64_t Size;
+ uint32_t CallstackId;
+ uint32_t ThreadId;
+ uint64_t EventSeq; // alloc event sequence number for churn distance
+ uint8_t RootHeap;
+ bool IsHeap = false; // true after HeapMarkAlloc; excluded from totals
+ };
+
+ uint64_t DecodeAllocSize(uint32_t RawSize, uint8_t AlignSizeLower) const;
+ void HandleAlloc(uint64_t Address, uint64_t Size, uint8_t RootHeap, uint32_t CallstackId, uint32_t ThreadId, bool IsRealloc);
+ void HandleFree(uint64_t Address, uint8_t RootHeap, uint32_t CallstackId, bool IsRealloc);
+ void MaybeEmitSample(uint32_t TimeUs);
+
+ // -- State --
+
+ static constexpr uint32_t kTimelineSampleIntervalUs = 10'000; // 10ms
+
+ const TraceTiming* m_Timing = nullptr;
+
+ // Init params
+ uint8_t m_SizeShift = 3; // overridden by Memory.Init if present; 3 matches zencore's default
+ bool m_Initialized = false;
+
+ // Live allocation map (address -> size + root heap)
+ eastl::hash_map<uint64_t, LiveAlloc> m_LiveAllocs;
+
+ // Running byte counters
+ int64_t m_CurrentBytes = 0;
+ int64_t m_SystemBytes = 0;
+ int64_t m_VideoBytes = 0;
+ int64_t m_PeakBytes = 0;
+ uint32_t m_PeakTimeUs = 0;
+
+ // Event counters
+ uint64_t m_TotalAllocs = 0;
+ uint64_t m_TotalFrees = 0;
+ uint64_t m_TotalReallocAllocs = 0;
+ uint64_t m_TotalReallocFrees = 0;
+
+ // Timeline sampling
+ eastl::vector<MemoryTimelineSample> m_Timeline;
+ uint32_t m_LastSampleTimeUs = 0;
+ uint32_t m_LastMarkerTimeUs = 0;
+ bool m_HasReceivedMarker = false;
+
+ // Per-callstack churn counters: total allocs + short-lived alloc stats
+ struct ChurnAccum
+ {
+ uint64_t TotalAllocs = 0;
+ uint64_t TotalBytes = 0;
+ uint64_t ChurnAllocs = 0; // freed within the distance threshold
+ uint64_t ChurnBytes = 0;
+ uint64_t ChurnDistanceSum = 0; // sum of event distances for churny allocs
+ };
+ eastl::hash_map<uint32_t, ChurnAccum> m_ChurnByCallstack;
+ uint64_t m_AllocEventSeq = 0; // monotonic alloc event counter
+
+ // Allocation size histogram: bucket i covers sizes [2^(i-1)+1, 2^i], with
+ // bucket 0 reserved for zero-size allocations. 65 buckets covers up to 2^64.
+ static constexpr size_t kSizeHistogramBuckets = 65;
+ struct SizeBucketAccum
+ {
+ uint64_t Count = 0;
+ uint64_t Bytes = 0;
+ };
+ SizeBucketAccum m_SizeHistogram[kSizeHistogramBuckets] = {};
+
+ // Metadata
+ eastl::hash_map<uint32_t, HeapInfo> m_Heaps;
+ eastl::hash_map<int32_t, TagInfo> m_Tags;
+ eastl::hash_map<uint8_t, HeapStat> m_RootHeapStats;
+};
+
+// -- CallstackAnalyzer -----------------------------------------------------
+
+// Subscribes to Memory.CallstackSpec* trace events, decodes compressed
+// frames, and stores a callstack ID -> frame addresses mapping. Frame
+// addresses are raw instruction pointers; resolution to module+offset
+// happens in BuildTraceModel post-processing.
+class CallstackAnalyzer : public Analyzer
+{
+public:
+ void subscribe(Vector<Subscription>& Subs) override;
+
+ const eastl::hash_map<uint32_t, eastl::vector<uint64_t>>& RawCallstacks() const { return m_Callstacks; }
+
+private:
+ void OnCallstackSpec(const ::Memory_CallstackSpec& Ev);
+ void OnCallstackSpecDeltaVarInt(const ::Memory_CallstackSpecDeltaVarInt& Ev);
+ void OnCallstackSpecDelta7bit(const ::Memory_CallstackSpecDelta7bit& Ev);
+ void OnCallstackSpecXORAndRLE(const ::Memory_CallstackSpecXORAndRLE& Ev);
+
+ void StoreCallstack(uint32_t Id, const uint64_t* Frames, size_t Count);
+
+ eastl::hash_map<uint32_t, eastl::vector<uint64_t>> m_Callstacks;
+};
+
+} // namespace zen::trace_detail
diff --git a/src/zen/trace/trace_model.cpp b/src/zen/trace/trace_model.cpp
new file mode 100644
index 000000000..f92b0c04a
--- /dev/null
+++ b/src/zen/trace/trace_model.cpp
@@ -0,0 +1,3847 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#include "trace_model.h"
+
+#include <zencore/basicfile.h>
+#include <zencore/except_fmt.h>
+#include <zencore/fmtutils.h>
+#include <zencore/intmath.h>
+#include <zencore/logging.h>
+#include <zencore/scopeguard.h>
+#include <zencore/string.h>
+#include <zencore/thread.h>
+#include <zencore/timer.h>
+#include <zenutil/parallelsort.h>
+
+ZEN_THIRD_PARTY_INCLUDES_START
+#include <EASTL/hash_map.h>
+#include <EASTL/map.h>
+#include <EASTL/set.h>
+#include <EASTL/sort.h>
+#include <EASTL/vector.h>
+#include <analysis/analyzer.h>
+#include <analysis/dispatcher.h>
+#include <trace/trace.h>
+ZEN_THIRD_PARTY_INCLUDES_END
+
+#include <algorithm>
+#include <cmath>
+#include <cstring>
+
+using namespace std::literals;
+
+// Toggle to A/B test cross-platform parallel sort vs sequential eastl::sort.
+constexpr bool kUseParallelSort = true;
+
+namespace eastl {
+
+template<>
+struct hash<std::string>
+{
+ size_t operator()(const std::string& S) const { return eastl::hash<const char*>()(S.c_str()); }
+};
+
+} // namespace eastl
+
+//////////////////////////////////////////////////////////////////////////////
+// Trace analysis types (global namespace alongside tourist types)
+
+namespace {
+
+using zen::ReciprocalU64;
+
+// Welford's online algorithm for computing mean and standard deviation
+class Distribution
+{
+public:
+ void add(double X)
+ {
+ m_Count++;
+ if (m_Count == 1)
+ {
+ m_OldM = m_NewM = X;
+ m_OldS = 0.0;
+ }
+ else
+ {
+ m_NewM = m_OldM + (X - m_OldM) / double(m_Count);
+ m_NewS = m_OldS + (X - m_OldM) * (X - m_NewM);
+ m_OldM = m_NewM;
+ m_OldS = m_NewS;
+ }
+ }
+
+ uint32_t Count() const { return m_Count; }
+ double Mean() const { return (m_Count > 0) ? m_NewM : 0.0; }
+ double Variance() const { return (m_Count > 1) ? m_NewS / double(m_Count - 1) : 0.0; }
+ double StdDev() const { return std::sqrt(Variance()); }
+
+private:
+ double m_OldM = 0.0;
+ double m_NewM = 0.0;
+ double m_OldS = 0.0;
+ double m_NewS = 0.0;
+ uint32_t m_Count = 0;
+};
+
+class NameDepot
+{
+public:
+ uint64 Add(StringView Name)
+ {
+ uint64 NameHash = Hash(Name);
+ Add(NameHash, Name);
+ return NameHash;
+ }
+
+ void Add(uint64 NameHash, StringView Name)
+ {
+ if (auto It = m_Names.insert({NameHash, String()}); It.second)
+ {
+ It.first->second = Name;
+ }
+ }
+
+ StringView Get(uint64 NameHash) const
+ {
+ auto Iter = m_Names.find(NameHash);
+ if (Iter == m_Names.end())
+ {
+ return "???";
+ }
+ return Iter->second;
+ }
+
+private:
+ eastl::hash_map<uint64, String> m_Names;
+};
+
+struct CpuEventStat
+{
+ Distribution Dist;
+ uint32_t Min = ~0u;
+ uint32_t Max = 0;
+};
+
+class EventStats
+{
+public:
+ void Record(uint64 NameHash, uint32 DurationUs)
+ {
+ CpuEventStat& Stat = m_Stats[NameHash];
+ Stat.Min = std::min(Stat.Min, DurationUs);
+ Stat.Max = std::max(Stat.Max, DurationUs);
+ Stat.Dist.add(double(DurationUs));
+ }
+
+ auto begin() const { return m_Stats.begin(); }
+ auto end() const { return m_Stats.end(); }
+ bool empty() const { return m_Stats.empty(); }
+
+private:
+ eastl::hash_map<uint64, CpuEventStat> m_Stats;
+};
+
+//////////////////////////////////////////////////////////////////////////////
+// Event outlines
+
+// clang-format off
+begin_outline($Trace, NewTrace)
+ field(uint64, CycleFrequency)
+ field(uint64, StartCycle)
+end_outline()
+
+begin_outline(CpuProfiler, EventSpec)
+ field(uint32, Id)
+ field(FieldStr, Name)
+end_outline()
+
+begin_outline(CpuProfiler, EventBatch)
+ field(uint32, ThreadId)
+ field(uint8[], Data)
+end_outline()
+
+begin_outline(CpuProfiler, EventBatchV2)
+ field(uint8[], Data)
+end_outline()
+
+begin_outline(CpuProfiler, EventBatchV3)
+ field(uint8[], Data)
+end_outline()
+
+begin_outline(CpuProfiler, Metadata)
+ field(uint32, Id)
+ field(uint32, SpecId)
+ field(uint8[], Metadata)
+end_outline()
+
+begin_outline($Trace, ThreadInfo)
+ field(FieldStr, Name)
+ field(int32, SortHint)
+ field(uint32, ThreadId)
+ field(uint32, SystemId)
+end_outline()
+
+begin_outline($Trace, ThreadGroupBegin)
+ field(FieldStr, Name)
+end_outline()
+
+begin_outline($Trace, ThreadGroupEnd)
+end_outline()
+
+begin_outline(Diagnostics, Session2)
+ field(FieldStr, Platform)
+ field(FieldStr, AppName)
+ field(FieldStr, ProjectName)
+ field(FieldStr, CommandLine)
+ field(FieldStr, Branch)
+ field(FieldStr, BuildVersion)
+ field(uint32, Changelist)
+ field(uint8, ConfigurationType)
+ field(uint8, TargetType)
+end_outline()
+
+begin_outline(Diagnostics, ModuleInit)
+ field(FieldStr, SymbolFormat)
+ field(uint8, ModuleBaseShift)
+end_outline()
+
+begin_outline(Diagnostics, ModuleLoad)
+ field(FieldStr, Name)
+ field(uint64, Base)
+ field(uint32, Size)
+ field(uint8[], ImageId)
+end_outline()
+
+begin_outline(Diagnostics, ModuleUnload)
+ field(uint64, Base)
+end_outline()
+
+begin_outline(Trace, ChannelAnnounce)
+ field(uint32, Id)
+ field(uint8, IsEnabled)
+ field(uint8, ReadOnly)
+ field(FieldStr, Name)
+end_outline()
+
+begin_outline(Trace, ChannelToggle)
+ field(uint32, Id)
+ field(uint8, IsEnabled)
+end_outline()
+
+begin_outline(Logging, LogCategory)
+ field(uint64, CategoryPointer)
+ field(uint8, DefaultVerbosity)
+ field(FieldStr, Name)
+end_outline()
+
+begin_outline(Logging, LogMessageSpec)
+ field(uint64, LogPoint)
+ field(uint64, CategoryPointer)
+ field(int32, Line)
+ field(uint8, Verbosity)
+ field(FieldStr, FileName)
+ field(FieldStr, FormatString)
+end_outline()
+
+begin_outline(Logging, LogMessage)
+ field(uint64, LogPoint)
+ field(uint64, Cycle)
+ field(uint8[], FormatArgs)
+end_outline()
+
+begin_outline(Misc, BookmarkSpec)
+ field(uint64, BookmarkPoint)
+ field(int32, Line)
+ field(FieldStr, FormatString)
+ field(FieldStr, FileName)
+end_outline()
+
+begin_outline(Misc, Bookmark)
+ field(uint64, Cycle)
+ field(uint64, BookmarkPoint)
+ field(uint8[], FormatArgs)
+end_outline()
+
+begin_outline(Misc, RegionBegin)
+ field(uint64, Cycle)
+ field(uint8[], RegionName)
+ field(uint8[], Category)
+end_outline()
+
+begin_outline(Misc, RegionBeginWithId)
+ field(uint64, CycleAndId)
+ field(uint8[], RegionName)
+ field(uint8[], Category)
+end_outline()
+
+begin_outline(Misc, RegionEnd)
+ field(uint64, Cycle)
+ field(uint8[], RegionName)
+end_outline()
+
+begin_outline(Misc, RegionEndWithId)
+ field(uint64, Cycle)
+ field(uint64, RegionId)
+end_outline()
+
+// CsvProfiler events
+begin_outline(CsvProfiler, RegisterCategory)
+ field(int32, Index)
+ field(uint8[], Name)
+end_outline()
+
+begin_outline(CsvProfiler, DefineInlineStat)
+ field(uint64, StatId)
+ field(int32, CategoryIndex)
+ field(uint8[], Name)
+end_outline()
+
+begin_outline(CsvProfiler, DefineDeclaredStat)
+ field(uint64, StatId)
+ field(int32, CategoryIndex)
+ field(uint8[], Name)
+end_outline()
+
+begin_outline(CsvProfiler, BeginStat)
+ field(uint64, StatId)
+ field(uint64, Cycle)
+end_outline()
+
+begin_outline(CsvProfiler, EndStat)
+ field(uint64, StatId)
+ field(uint64, Cycle)
+end_outline()
+
+begin_outline(CsvProfiler, BeginExclusiveStat)
+ field(uint64, StatId)
+ field(uint64, Cycle)
+end_outline()
+
+begin_outline(CsvProfiler, EndExclusiveStat)
+ field(uint64, StatId)
+ field(uint64, Cycle)
+end_outline()
+
+begin_outline(CsvProfiler, CustomStatInt)
+ field(uint64, StatId)
+ field(uint64, Cycle)
+ field(int32, Value)
+ field(uint8, OpType)
+end_outline()
+
+begin_outline(CsvProfiler, CustomStatFloat)
+ field(uint64, StatId)
+ field(uint64, Cycle)
+ field(float, Value)
+ field(uint8, OpType)
+end_outline()
+
+begin_outline(CsvProfiler, Event)
+ field(uint64, Cycle)
+ field(int32, CategoryIndex)
+ field(uint8[], Text)
+end_outline()
+
+begin_outline(CsvProfiler, BeginCapture)
+ field(uint64, Cycle)
+ field(uint32, RenderThreadId)
+ field(uint32, RHIThreadId)
+ field(uint8, EnableCounts)
+ field(uint8[], FileName)
+end_outline()
+
+begin_outline(CsvProfiler, EndCapture)
+ field(uint64, Cycle)
+end_outline()
+
+begin_outline(CsvProfiler, Metadata)
+ field(uint8[], Key)
+ field(uint8[], Value)
+end_outline()
+ // clang-format on
+
+ //////////////////////////////////////////////////////////////////////////////
+ // Forward declarations needed by the helper analyzers below.
+
+ using zen::trace_detail::SafeFieldStr;
+
+//////////////////////////////////////////////////////////////////////////////
+// Minimal CBOR formatter
+//
+// CpuProfiler.Metadata payloads are CBOR-encoded (RFC 7049) blobs produced
+// by UE's FCborWriter. We don't need a full decoder -- we just walk the
+// bytes and append human-readable values to an output string. Handles the
+// subset actually emitted by UE's metadata scopes: unsigned / negative
+// integers, byte / text strings, arrays, maps, floats, and the boolean /
+// null simple values.
+
+static bool CborAppendValue(const uint8*& p, const uint8* end, std::string& out, int depth);
+
+static bool
+CborReadArg(const uint8*& p, const uint8* end, uint8 info, uint64& value)
+{
+ if (info < 24)
+ {
+ value = info;
+ return true;
+ }
+ if (info == 24)
+ {
+ if (end - p < 1)
+ return false;
+ value = *p++;
+ return true;
+ }
+ if (info == 25)
+ {
+ if (end - p < 2)
+ return false;
+ value = (uint64(p[0]) << 8) | p[1];
+ p += 2;
+ return true;
+ }
+ if (info == 26)
+ {
+ if (end - p < 4)
+ return false;
+ value = (uint64(p[0]) << 24) | (uint64(p[1]) << 16) | (uint64(p[2]) << 8) | p[3];
+ p += 4;
+ return true;
+ }
+ if (info == 27)
+ {
+ if (end - p < 8)
+ return false;
+ value = 0;
+ for (int i = 0; i < 8; ++i)
+ {
+ value = (value << 8) | p[i];
+ }
+ p += 8;
+ return true;
+ }
+ return false;
+}
+
+static bool
+CborAppendValue(const uint8*& p, const uint8* end, std::string& out, int depth)
+{
+ if (depth > 4 || p >= end)
+ {
+ return false;
+ }
+
+ const uint8 ib = *p++;
+ const uint8 major = ib >> 5;
+ const uint8 info = ib & 0x1f;
+
+ switch (major)
+ {
+ case 0: // unsigned integer
+ {
+ uint64 v;
+ if (!CborReadArg(p, end, info, v))
+ return false;
+ out += fmt::format("{}", v);
+ return true;
+ }
+ case 1: // negative integer: -1 - value
+ {
+ uint64 v;
+ if (!CborReadArg(p, end, info, v))
+ return false;
+ out += fmt::format("{}", -1 - int64_t(v));
+ return true;
+ }
+ case 2: // byte string
+ case 3: // text string
+ {
+ uint64 len;
+ if (!CborReadArg(p, end, info, len))
+ return false;
+ if (len > uint64(end - p))
+ return false;
+ out.append(reinterpret_cast<const char*>(p), size_t(len));
+ p += len;
+ return true;
+ }
+ case 4: // array
+ {
+ uint64 count;
+ if (!CborReadArg(p, end, info, count))
+ return false;
+ for (uint64 i = 0; i < count; ++i)
+ {
+ if (i > 0)
+ out += ", ";
+ if (!CborAppendValue(p, end, out, depth + 1))
+ return false;
+ }
+ return true;
+ }
+ case 5: // map
+ {
+ uint64 count;
+ if (!CborReadArg(p, end, info, count))
+ return false;
+ for (uint64 i = 0; i < count; ++i)
+ {
+ if (i > 0)
+ out += ", ";
+ if (!CborAppendValue(p, end, out, depth + 1))
+ return false;
+ out += "=";
+ if (!CborAppendValue(p, end, out, depth + 1))
+ return false;
+ }
+ return true;
+ }
+ case 7: // simple values / floats
+ {
+ if (info == 20)
+ {
+ out += "false";
+ return true;
+ }
+ if (info == 21)
+ {
+ out += "true";
+ return true;
+ }
+ if (info == 22)
+ {
+ out += "null";
+ return true;
+ }
+ if (info == 26)
+ {
+ if (end - p < 4)
+ return false;
+ uint32 bits = (uint32(p[0]) << 24) | (uint32(p[1]) << 16) | (uint32(p[2]) << 8) | p[3];
+ float v;
+ std::memcpy(&v, &bits, 4);
+ out += fmt::format("{}", v);
+ p += 4;
+ return true;
+ }
+ if (info == 27)
+ {
+ if (end - p < 8)
+ return false;
+ uint64 bits = 0;
+ for (int i = 0; i < 8; ++i)
+ {
+ bits = (bits << 8) | p[i];
+ }
+ p += 8;
+ double v;
+ std::memcpy(&v, &bits, 8);
+ out += fmt::format("{}", v);
+ return true;
+ }
+ return false;
+ }
+ default:
+ return false;
+ }
+}
+
+static std::string
+FormatMetadataValues(const uint8_t* Bytes, size_t Size)
+{
+ std::string out;
+ if (Size == 0)
+ {
+ return out;
+ }
+ const uint8* p = Bytes;
+ CborAppendValue(p, Bytes + Size, out, 0);
+ return out;
+}
+
+using zen::trace_detail::TraceTiming;
+
+//////////////////////////////////////////////////////////////////////////////
+// Metadata registry
+//
+// Subscribes to CpuProfiler.Metadata events and stores each payload's
+// CBOR-encoded bytes keyed by MetadataId, along with the SpecId they
+// reference. Both CpuAnalyzer and TimelineAnalyzer query the registry when
+// they encounter a V3 scope with the metadata bit set so the scope can be
+// rendered as `{base name} - {formatted values}`.
+
+struct MetadataEntry
+{
+ uint32_t SpecId = 0;
+ eastl::vector<uint8_t> Bytes;
+};
+
+class MetadataRegistry : public Analyzer
+{
+public:
+ void subscribe(Vector<Subscription>& Subs) override { Subs.emplace_back(this, &MetadataRegistry::OnMetadata); }
+
+ const MetadataEntry* Lookup(uint32_t MetadataId) const
+ {
+ auto It = m_Entries.find(MetadataId);
+ return (It != m_Entries.end()) ? &It->second : nullptr;
+ }
+
+private:
+ void OnMetadata(const CpuProfiler_Metadata& Ev)
+ {
+ uint32_t MetadataId = Ev.Id();
+ uint32_t SpecId = Ev.SpecId();
+ Array<uint8[]> Data = Ev.Metadata();
+
+ MetadataEntry& Entry = m_Entries[MetadataId];
+ Entry.SpecId = SpecId;
+ Entry.Bytes.assign(Data.get(), Data.get() + Data.get_size());
+ }
+
+ eastl::hash_map<uint32_t, MetadataEntry> m_Entries;
+};
+
+//////////////////////////////////////////////////////////////////////////////
+// Log message formatting
+//
+// UE's trace emits log messages as a sequence of typed arguments that need
+// to be substituted into a printf-style format string. The wire format is:
+//
+// [ArgumentCount: uint8]
+// [Descriptors: uint8 * ArgumentCount] // each byte = category | size
+// [Payload: bytes]
+//
+// Category bits live in the upper 2 bits (shifted by FormatArgTypeCode_-
+// CategoryBitShift == 6): Integer=1, Float=2, String=3. The low 6 bits are
+// the argument size in bytes; for strings the size is the per-character
+// width (1 == ANSI, 2 == UTF-16).
+//
+// We walk the format string, extract each specifier, pull the matching arg
+// from the stream and hand both to std::snprintf. Width/precision stars
+// (e.g. "%*.*f") are not supported; they're rare in log formats.
+
+struct FormatArgStream
+{
+ const uint8_t* Descriptors;
+ const uint8_t* Payload;
+ uint8_t Remaining;
+
+ bool HasNext() const { return Remaining > 0; }
+
+ uint8_t PeekCategory() const { return (*Descriptors) & 0xC0; }
+ uint8_t PeekSize() const { return (*Descriptors) & 0x3F; }
+
+ void Advance(size_t PayloadBytes)
+ {
+ Payload += PayloadBytes;
+ ++Descriptors;
+ --Remaining;
+ }
+};
+
+static bool
+InitFormatArgStream(FormatArgStream& Ctx, const uint8_t* Data, size_t Size)
+{
+ if (!Data || Size == 0)
+ {
+ Ctx.Remaining = 0;
+ return false;
+ }
+ uint8_t Count = Data[0];
+ if (size_t(1) + Count > Size)
+ {
+ Ctx.Remaining = 0;
+ return false;
+ }
+ Ctx.Descriptors = Data + 1;
+ Ctx.Payload = Data + 1 + Count;
+ Ctx.Remaining = Count;
+ return true;
+}
+
+static bool
+IsPrintfSpecifierChar(char c)
+{
+ switch (c)
+ {
+ case 'd':
+ case 'i':
+ case 'u':
+ case 'o':
+ case 'x':
+ case 'X':
+ case 'c':
+ case 'p':
+ case 'f':
+ case 'F':
+ case 'e':
+ case 'E':
+ case 'g':
+ case 'G':
+ case 'a':
+ case 'A':
+ case 's':
+ case 'S':
+ case 'n':
+ return true;
+ default:
+ return false;
+ }
+}
+
+static std::string
+FormatLogMessage(std::string_view Format, const uint8_t* ArgsData, size_t ArgsSize)
+{
+ FormatArgStream Stream{};
+ InitFormatArgStream(Stream, ArgsData, ArgsSize);
+
+ std::string Out;
+ Out.reserve(Format.size() + 32);
+
+ size_t i = 0;
+ while (i < Format.size())
+ {
+ char c = Format[i];
+ if (c != '%')
+ {
+ Out.push_back(c);
+ ++i;
+ continue;
+ }
+
+ // Handle "%%" -> literal percent.
+ if (i + 1 < Format.size() && Format[i + 1] == '%')
+ {
+ Out.push_back('%');
+ i += 2;
+ continue;
+ }
+
+ // Walk the specifier until we find a terminating character.
+ size_t SpecStart = i++;
+ while (i < Format.size() && !IsPrintfSpecifierChar(Format[i]))
+ {
+ ++i;
+ }
+ if (i >= Format.size())
+ {
+ // Truncated specifier -- copy the remainder literally.
+ Out.append(Format.substr(SpecStart));
+ break;
+ }
+
+ char Specifier = Format[i++];
+ std::string Spec(Format.substr(SpecStart, i - SpecStart));
+
+ if (!Stream.HasNext())
+ {
+ // Not enough arguments: emit the raw specifier so the user can
+ // at least tell something is missing.
+ Out.append(Spec);
+ continue;
+ }
+
+ const uint8_t Category = Stream.PeekCategory();
+ const uint8_t Size = Stream.PeekSize();
+
+ char Buf[512];
+ Buf[0] = '\0';
+
+ if (Category == 0x40) // integer
+ {
+ uint64_t Raw = 0;
+ if (Size <= sizeof(Raw) && Size > 0)
+ {
+ std::memcpy(&Raw, Stream.Payload, Size);
+ }
+
+ // Route through the correct snprintf type based on the
+ // specifier. Cast to int64_t for signed integer specifiers.
+ switch (Specifier)
+ {
+ case 'd':
+ case 'i':
+ {
+ // Sign-extend based on Size.
+ int64_t Signed = 0;
+ switch (Size)
+ {
+ case 1:
+ Signed = int8_t(Raw & 0xff);
+ break;
+ case 2:
+ Signed = int16_t(Raw & 0xffff);
+ break;
+ case 4:
+ Signed = int32_t(Raw & 0xffffffff);
+ break;
+ case 8:
+ Signed = int64_t(Raw);
+ break;
+ default:
+ Signed = int64_t(Raw);
+ break;
+ }
+ // Replace length modifier so snprintf interprets the
+ // correctly-sized value. Simplest: append "ll".
+ std::string AdjustedSpec = Spec;
+ AdjustedSpec.insert(AdjustedSpec.size() - 1, "ll");
+ std::snprintf(Buf, sizeof(Buf), AdjustedSpec.c_str(), static_cast<long long>(Signed));
+ break;
+ }
+ case 'u':
+ case 'o':
+ case 'x':
+ case 'X':
+ case 'p':
+ {
+ std::string AdjustedSpec = Spec;
+ AdjustedSpec.insert(AdjustedSpec.size() - 1, "ll");
+ std::snprintf(Buf, sizeof(Buf), AdjustedSpec.c_str(), static_cast<unsigned long long>(Raw));
+ break;
+ }
+ case 'c':
+ {
+ std::snprintf(Buf, sizeof(Buf), Spec.c_str(), int(Raw & 0xff));
+ break;
+ }
+ default:
+ std::snprintf(Buf, sizeof(Buf), "%llu", static_cast<unsigned long long>(Raw));
+ break;
+ }
+ Stream.Advance(Size);
+ }
+ else if (Category == 0x80) // floating point
+ {
+ double Value = 0.0;
+ if (Size == 4)
+ {
+ float F;
+ std::memcpy(&F, Stream.Payload, 4);
+ Value = double(F);
+ }
+ else if (Size == 8)
+ {
+ std::memcpy(&Value, Stream.Payload, 8);
+ }
+ std::snprintf(Buf, sizeof(Buf), Spec.c_str(), Value);
+ Stream.Advance(Size);
+ }
+ else if (Category == 0xC0) // string
+ {
+ std::string Tmp;
+ if (Size == 1)
+ {
+ const char* S = reinterpret_cast<const char*>(Stream.Payload);
+ size_t Len = std::strlen(S);
+ Tmp.assign(S, Len);
+ std::snprintf(Buf, sizeof(Buf), Spec.c_str(), Tmp.c_str());
+ Stream.Advance(Len + 1);
+ }
+ else if (Size == 2)
+ {
+ const char16_t* W = reinterpret_cast<const char16_t*>(Stream.Payload);
+ size_t Len = 0;
+ while (W[Len] != 0)
+ ++Len;
+ Tmp.reserve(Len);
+ for (size_t k = 0; k < Len; ++k)
+ {
+ char16_t ch = W[k];
+ Tmp.push_back(ch < 0x80 ? char(ch) : '?');
+ }
+ std::snprintf(Buf, sizeof(Buf), Spec.c_str(), Tmp.c_str());
+ Stream.Advance((Len + 1) * 2);
+ }
+ else
+ {
+ std::snprintf(Buf, sizeof(Buf), "<unsupported string width %u>", unsigned(Size));
+ Stream.Advance(0);
+ ++Stream.Descriptors;
+ --Stream.Remaining;
+ }
+ }
+ else
+ {
+ std::snprintf(Buf, sizeof(Buf), "<arg>");
+ Stream.Advance(Size);
+ }
+
+ Out.append(Buf);
+ }
+
+ return Out;
+}
+
+//////////////////////////////////////////////////////////////////////////////
+// Log analyzer
+
+class LogAnalyzer : public Analyzer
+{
+public:
+ explicit LogAnalyzer(const TraceTiming* Timing = nullptr) : m_Timing(Timing) {}
+
+ void subscribe(Vector<Subscription>& Subs) override
+ {
+ Subs.emplace_back(this, &LogAnalyzer::OnLogCategory);
+ Subs.emplace_back(this, &LogAnalyzer::OnLogMessageSpec);
+ Subs.emplace_back(this, &LogAnalyzer::OnLogMessage);
+ }
+
+ eastl::vector<zen::trace_detail::LogCategoryInfo> BuildCategories(eastl::hash_map<uint64_t, uint32_t>& OutPointerToIndex) const
+ {
+ eastl::vector<zen::trace_detail::LogCategoryInfo> Cats;
+ Cats.reserve(m_Categories.size());
+ OutPointerToIndex.clear();
+ for (const auto& [Ptr, Info] : m_Categories)
+ {
+ OutPointerToIndex[Ptr] = uint32_t(Cats.size());
+ Cats.push_back(Info);
+ }
+ return Cats;
+ }
+
+ const eastl::vector<zen::trace_detail::LogEntry>& Entries() const { return m_Entries; }
+ eastl::vector<zen::trace_detail::LogEntry>& MutableEntries() { return m_Entries; }
+
+ // The shared TraceTiming pointer lets external callers read the trace's
+ // cycle base / frequency without each analyzer having to own a copy.
+ const TraceTiming* Timing() const { return m_Timing; }
+
+ struct MessageSpec
+ {
+ uint64_t CategoryPointer = 0;
+ int32_t Line = 0;
+ uint8_t Verbosity = 0;
+ std::string File;
+ std::string FormatString;
+ };
+
+ const eastl::hash_map<uint64_t, MessageSpec>& MessageSpecs() const { return m_Specs; }
+
+private:
+ void OnLogCategory(const Logging_LogCategory& Ev)
+ {
+ uint64_t Ptr = Ev.CategoryPointer();
+ zen::trace_detail::LogCategoryInfo& Info = m_Categories[Ptr];
+ Info.Name = SafeFieldStr(Ev.Name());
+ Info.DefaultVerbosity = Ev.DefaultVerbosity();
+ }
+
+ void OnLogMessageSpec(const Logging_LogMessageSpec& Ev)
+ {
+ MessageSpec& Spec = m_Specs[Ev.LogPoint()];
+ Spec.CategoryPointer = Ev.CategoryPointer();
+ Spec.Line = Ev.Line();
+ Spec.Verbosity = Ev.Verbosity();
+ Spec.File = SafeFieldStr(Ev.FileName());
+ Spec.FormatString = SafeFieldStr(Ev.FormatString());
+ }
+
+ void OnLogMessage(const Logging_LogMessage& Ev)
+ {
+ uint64_t LogPoint = Ev.LogPoint();
+ auto SpecIt = m_Specs.find(LogPoint);
+ if (SpecIt == m_Specs.end())
+ {
+ return;
+ }
+ const MessageSpec& Spec = SpecIt->second;
+
+ uint32_t TimeUs = m_Timing ? m_Timing->CycleToTimeUs(Ev.Cycle()) : 0;
+
+ Array<uint8[]> Args = Ev.FormatArgs();
+ std::string Msg = FormatLogMessage(std::string_view(Spec.FormatString), Args.get(), Args.get_size());
+
+ zen::trace_detail::LogEntry Entry;
+ Entry.TimeUs = TimeUs;
+ Entry.CategoryIndex = ~0u; // resolved in BuildTraceModel
+ Entry.Verbosity = Spec.Verbosity;
+ Entry.Line = Spec.Line;
+ Entry.File = Spec.File;
+ Entry.Message = std::move(Msg);
+ // Use the category pointer temporarily so BuildTraceModel can resolve
+ // it against the categories table.
+ Entry.CategoryIndex = SpecToCategoryIndex(Spec.CategoryPointer);
+ m_Entries.push_back(std::move(Entry));
+ }
+
+ uint32_t SpecToCategoryIndex(uint64_t Ptr)
+ {
+ // Encoded pointer stuffed into uint32_t so BuildTraceModel can remap.
+ // Lossy but deterministic: use a stable sequential index per unique
+ // pointer so we never need the full 64-bit value beyond build time.
+ auto It = m_CategoryIndex.find(Ptr);
+ if (It != m_CategoryIndex.end())
+ {
+ return It->second;
+ }
+ uint32_t Idx = uint32_t(m_CategoryIndex.size());
+ m_CategoryIndex[Ptr] = Idx;
+ return Idx;
+ }
+
+public:
+ // Mapping from the intermediate index stored in LogEntry::CategoryIndex
+ // during capture to the real category pointer; BuildTraceModel uses
+ // this to remap entries against the flattened LogCategories array.
+ const eastl::hash_map<uint64_t, uint32_t>& CategoryPointerIndex() const { return m_CategoryIndex; }
+
+private:
+ const TraceTiming* m_Timing = nullptr;
+ eastl::hash_map<uint64_t, zen::trace_detail::LogCategoryInfo> m_Categories;
+ eastl::hash_map<uint64_t, MessageSpec> m_Specs;
+ eastl::hash_map<uint64_t, uint32_t> m_CategoryIndex;
+ eastl::vector<zen::trace_detail::LogEntry> m_Entries;
+};
+
+//////////////////////////////////////////////////////////////////////////////
+// Bookmarks and regions
+//
+// UE's bookmark wire format mirrors LogMessage: a BookmarkSpec introduces
+// a (FileName, Line, FormatString) triple keyed by a BookmarkPoint pointer,
+// and each Misc.Bookmark event carries that pointer, a cycle, and the same
+// FFormatArgsTrace payload the log pipeline already knows how to decode.
+// Region events come in two flavours: the legacy name-paired
+// RegionBegin/RegionEnd and the newer *WithId variants that pack a unique
+// id into the begin event's cycle.
+
+class BookmarksAnalyzer : public Analyzer
+{
+public:
+ explicit BookmarksAnalyzer(const TraceTiming* Timing = nullptr) : m_Timing(Timing) {}
+
+ void subscribe(Vector<Subscription>& Subs) override
+ {
+ Subs.emplace_back(this, &BookmarksAnalyzer::OnBookmarkSpec);
+ Subs.emplace_back(this, &BookmarksAnalyzer::OnBookmark);
+ Subs.emplace_back(this, &BookmarksAnalyzer::OnRegionBegin);
+ Subs.emplace_back(this, &BookmarksAnalyzer::OnRegionBeginWithId);
+ Subs.emplace_back(this, &BookmarksAnalyzer::OnRegionEnd);
+ Subs.emplace_back(this, &BookmarksAnalyzer::OnRegionEndWithId);
+ }
+
+ eastl::vector<zen::trace_detail::Bookmark>& MutableBookmarks() { return m_Bookmarks; }
+ eastl::vector<zen::trace_detail::RegionEntry>& MutableRegions() { return m_Regions; }
+
+private:
+ struct BookmarkSpec
+ {
+ int32_t Line = 0;
+ std::string File;
+ std::string FormatString;
+ };
+
+ uint32_t CycleToTimeUs(uint64_t Cycle) const { return m_Timing ? m_Timing->CycleToTimeUs(Cycle) : 0; }
+
+ void OnBookmarkSpec(const Misc_BookmarkSpec& Ev)
+ {
+ BookmarkSpec& Spec = m_Specs[Ev.BookmarkPoint()];
+ Spec.Line = Ev.Line();
+ Spec.File = SafeFieldStr(Ev.FileName());
+ Spec.FormatString = SafeFieldStr(Ev.FormatString());
+ }
+
+ void OnBookmark(const Misc_Bookmark& Ev)
+ {
+ auto SpecIt = m_Specs.find(Ev.BookmarkPoint());
+ if (SpecIt == m_Specs.end())
+ {
+ return;
+ }
+ const BookmarkSpec& Spec = SpecIt->second;
+
+ Array<uint8[]> Args = Ev.FormatArgs();
+ std::string Text = FormatLogMessage(std::string_view(Spec.FormatString), Args.get(), Args.get_size());
+
+ zen::trace_detail::Bookmark Out;
+ Out.TimeUs = CycleToTimeUs(Ev.Cycle());
+ Out.Line = Spec.Line;
+ Out.File = Spec.File;
+ Out.Text = std::move(Text);
+ m_Bookmarks.push_back(std::move(Out));
+ }
+
+ uint32_t CreatePartialRegion(uint32_t TimeUs, std::string Name, std::string Category)
+ {
+ zen::trace_detail::RegionEntry Entry;
+ Entry.BeginUs = TimeUs;
+ Entry.EndUs = ~uint32_t(0); // sentinel: still open
+ Entry.Depth = 0;
+ Entry.Reserved = 0;
+ Entry.Name = std::move(Name);
+ Entry.Category = std::move(Category);
+ uint32_t Idx = uint32_t(m_Regions.size());
+ m_Regions.push_back(std::move(Entry));
+ return Idx;
+ }
+
+ // Decodes the raw array bytes of a RegionName field into a std::string.
+ // UE emits RegionName as either AnsiString (1-byte) or WideString (2-byte)
+ // depending on the trace's age -- for the 2-byte case we do the same
+ // lossy ASCII fold tourist's FieldStr does, which is all we need for
+ // display.
+ static std::string DecodeRegionName(const Array<uint8[]>& Data)
+ {
+ const uint8_t* p = Data.get();
+ size_t size = Data.get_size();
+ uint32_t count = Data.get_count();
+ if (!p || size == 0 || count == 0)
+ {
+ return {};
+ }
+ if (size == count)
+ {
+ // 1 byte per element -- AnsiString.
+ return std::string(reinterpret_cast<const char*>(p), count);
+ }
+ if (size == count * 2)
+ {
+ // 2 bytes per element -- WideString. Lossy ASCII fold.
+ std::string out;
+ out.reserve(count);
+ const char16_t* w = reinterpret_cast<const char16_t*>(p);
+ for (uint32_t i = 0; i < count; ++i)
+ {
+ out.push_back(w[i] < 0x80 ? char(w[i]) : '?');
+ }
+ return out;
+ }
+ return {};
+ }
+
+ void OnRegionBegin(const Misc_RegionBegin& Ev)
+ {
+ uint32_t TimeUs = CycleToTimeUs(Ev.Cycle());
+ Array<uint8[]> NameArr = Ev.RegionName();
+ std::string Name = DecodeRegionName(NameArr);
+ std::string Category = DecodeRegionName(Ev.Category());
+ uint32_t Idx = CreatePartialRegion(TimeUs, Name, std::move(Category));
+ m_OpenByName[Name].push_back(Idx);
+ }
+
+ void OnRegionBeginWithId(const Misc_RegionBeginWithId& Ev)
+ {
+ // Despite its name, CycleAndId is just Cycles64() -- a plain 64-bit
+ // cycle count that doubles as a unique region identifier. The caller
+ // keeps the returned value and passes it back as RegionId at end.
+ uint64_t CycleAndId = Ev.CycleAndId();
+ uint32_t TimeUs = CycleToTimeUs(CycleAndId);
+ Array<uint8[]> NameArr = Ev.RegionName();
+ std::string Name = DecodeRegionName(NameArr);
+ std::string Category = DecodeRegionName(Ev.Category());
+ uint32_t Idx = CreatePartialRegion(TimeUs, std::move(Name), std::move(Category));
+ m_OpenById[CycleAndId] = Idx;
+ }
+
+ void OnRegionEnd(const Misc_RegionEnd& Ev)
+ {
+ uint32_t TimeUs = CycleToTimeUs(Ev.Cycle());
+ Array<uint8[]> NameArr = Ev.RegionName();
+ std::string Name = DecodeRegionName(NameArr);
+ auto It = m_OpenByName.find(Name);
+ if (It == m_OpenByName.end() || It->second.empty())
+ {
+ return;
+ }
+ uint32_t Idx = It->second.back();
+ It->second.pop_back();
+ m_Regions[Idx].EndUs = TimeUs;
+ }
+
+ void OnRegionEndWithId(const Misc_RegionEndWithId& Ev)
+ {
+ uint32_t TimeUs = CycleToTimeUs(Ev.Cycle());
+ uint64_t Id = Ev.RegionId();
+ auto It = m_OpenById.find(Id);
+ if (It == m_OpenById.end())
+ {
+ return;
+ }
+ m_Regions[It->second].EndUs = TimeUs;
+ m_OpenById.erase(It);
+ }
+
+ const TraceTiming* m_Timing = nullptr;
+ eastl::hash_map<uint64_t, BookmarkSpec> m_Specs;
+ eastl::vector<zen::trace_detail::Bookmark> m_Bookmarks;
+ eastl::vector<zen::trace_detail::RegionEntry> m_Regions;
+ eastl::hash_map<std::string, eastl::vector<uint32_t>> m_OpenByName;
+ eastl::hash_map<uint64_t, uint32_t> m_OpenById;
+};
+
+//////////////////////////////////////////////////////////////////////////////
+// CsvProfiler analyzer -- parses CSV stat categories, definitions, timing,
+// custom values, events, capture markers, and metadata.
+
+class CsvProfilerAnalyzer : public Analyzer
+{
+public:
+ explicit CsvProfilerAnalyzer(const TraceTiming* Timing = nullptr) : m_Timing(Timing) {}
+
+ void subscribe(Vector<Subscription>& Subs) override
+ {
+ Subs.emplace_back(this, &CsvProfilerAnalyzer::OnRegisterCategory);
+ Subs.emplace_back(this, &CsvProfilerAnalyzer::OnDefineInlineStat);
+ Subs.emplace_back(this, &CsvProfilerAnalyzer::OnDefineDeclaredStat);
+ Subs.emplace_back(this, &CsvProfilerAnalyzer::OnBeginStat);
+ Subs.emplace_back(this, &CsvProfilerAnalyzer::OnEndStat);
+ Subs.emplace_back(this, &CsvProfilerAnalyzer::OnBeginExclusiveStat);
+ Subs.emplace_back(this, &CsvProfilerAnalyzer::OnEndExclusiveStat);
+ Subs.emplace_back(this, &CsvProfilerAnalyzer::OnCustomStatInt);
+ Subs.emplace_back(this, &CsvProfilerAnalyzer::OnCustomStatFloat);
+ Subs.emplace_back(this, &CsvProfilerAnalyzer::OnEvent);
+ Subs.emplace_back(this, &CsvProfilerAnalyzer::OnBeginCapture);
+ Subs.emplace_back(this, &CsvProfilerAnalyzer::OnEndCapture);
+ Subs.emplace_back(this, &CsvProfilerAnalyzer::OnMetadata);
+ }
+
+ eastl::vector<zen::trace_detail::TraceModel::CsvCategory>& MutableCategories() { return m_Categories; }
+ eastl::vector<zen::trace_detail::TraceModel::CsvStatDef>& MutableStatDefs() { return m_StatDefs; }
+ eastl::vector<zen::trace_detail::TraceModel::CsvEvent>& MutableEvents() { return m_Events; }
+ eastl::vector<zen::trace_detail::TraceModel::CsvMeta>& MutableMetadata() { return m_Metadata; }
+
+ // Build the per-stat+thread time series from the accumulated samples.
+ eastl::vector<zen::trace_detail::TraceModel::CsvSeries> BuildTimeSeries()
+ {
+ eastl::vector<zen::trace_detail::TraceModel::CsvSeries> Result;
+ for (auto& [Key, Samples] : m_SeriesMap)
+ {
+ eastl::sort(Samples.begin(), Samples.end(), [](const auto& A, const auto& B) { return A.TimeUs < B.TimeUs; });
+ zen::trace_detail::TraceModel::CsvSeries S;
+ S.StatId = Key.StatId;
+ S.ThreadId = Key.ThreadId;
+ S.Samples = std::move(Samples);
+ Result.push_back(std::move(S));
+ }
+ return Result;
+ }
+
+private:
+ uint32_t CycleToTimeUs(uint64_t Cycle) const
+ {
+ if (!m_Timing || m_Timing->Freq == 0)
+ {
+ return 0;
+ }
+ uint64_t Elapsed = (Cycle >= m_Timing->Base) ? (Cycle - m_Timing->Base) : 0;
+ return uint32_t(Elapsed * 1'000'000 / m_Timing->Freq);
+ }
+
+ static std::string DecodeAnsiName(const Array<uint8[]>& Data)
+ {
+ const uint8_t* P = Data.get();
+ size_t Size = Data.get_size();
+ if (!P || Size == 0)
+ {
+ return {};
+ }
+ return std::string(reinterpret_cast<const char*>(P), Size);
+ }
+
+ static std::string DecodeWideName(const Array<uint8[]>& Data)
+ {
+ const uint8_t* P = Data.get();
+ size_t Size = Data.get_size();
+ uint32_t Count = Data.get_count();
+ if (!P || Size == 0 || Count == 0)
+ {
+ return {};
+ }
+ uint32_t ElemSize = Data.get_element_size();
+ if (ElemSize == 2)
+ {
+ std::string Out;
+ Out.reserve(Count);
+ const char16_t* W = reinterpret_cast<const char16_t*>(P);
+ for (uint32_t I = 0; I < Count; ++I)
+ {
+ Out.push_back(W[I] < 0x80 ? char(W[I]) : '?');
+ }
+ return Out;
+ }
+ return std::string(reinterpret_cast<const char*>(P), Size);
+ }
+
+ struct SeriesKey
+ {
+ uint64_t StatId;
+ uint32_t ThreadId;
+ bool operator==(const SeriesKey& O) const { return StatId == O.StatId && ThreadId == O.ThreadId; }
+ };
+ struct SeriesKeyHash
+ {
+ size_t operator()(const SeriesKey& K) const
+ {
+ return eastl::hash<uint64_t>{}(K.StatId) ^ (eastl::hash<uint32_t>{}(K.ThreadId) * 2654435761u);
+ }
+ };
+
+ void AddSample(uint64_t StatId, uint32_t ThreadId, uint32_t TimeUs, float Value)
+ {
+ m_SeriesMap[SeriesKey{StatId, ThreadId}].push_back({TimeUs, Value});
+ }
+
+ // -- Event handlers -----------------------------------------------
+
+ void OnRegisterCategory(const CsvProfiler_RegisterCategory& Ev)
+ {
+ zen::trace_detail::TraceModel::CsvCategory Cat;
+ Cat.Index = Ev.Index();
+ Cat.Name = DecodeAnsiName(Ev.Name());
+ m_Categories.push_back(std::move(Cat));
+ }
+
+ void OnDefineInlineStat(const CsvProfiler_DefineInlineStat& Ev)
+ {
+ DefineStat(Ev.StatId(), Ev.CategoryIndex(), DecodeAnsiName(Ev.Name()));
+ }
+
+ void OnDefineDeclaredStat(const CsvProfiler_DefineDeclaredStat& Ev)
+ {
+ DefineStat(Ev.StatId(), Ev.CategoryIndex(), DecodeAnsiName(Ev.Name()));
+ }
+
+ void DefineStat(uint64_t StatId, int32_t CategoryIndex, std::string Name)
+ {
+ if (m_StatIdToIndex.count(StatId))
+ {
+ return; // already defined
+ }
+ m_StatIdToIndex[StatId] = uint32_t(m_StatDefs.size());
+ zen::trace_detail::TraceModel::CsvStatDef Def;
+ Def.StatId = StatId;
+ Def.CategoryIndex = CategoryIndex;
+ Def.Name = std::move(Name);
+ m_StatDefs.push_back(std::move(Def));
+ }
+
+ void OnBeginStat(const CsvProfiler_BeginStat& Ev)
+ {
+ uint32_t ThreadId = Ev.get_thread_id();
+ uint32_t TimeUs = CycleToTimeUs(Ev.Cycle());
+ m_OpenStacks[{Ev.StatId(), ThreadId}].push_back(TimeUs);
+ }
+
+ void OnEndStat(const CsvProfiler_EndStat& Ev)
+ {
+ uint32_t ThreadId = Ev.get_thread_id();
+ uint32_t TimeUs = CycleToTimeUs(Ev.Cycle());
+ auto Key = SeriesKey{Ev.StatId(), ThreadId};
+ auto It = m_OpenStacks.find(Key);
+ if (It == m_OpenStacks.end() || It->second.empty())
+ {
+ return;
+ }
+ uint32_t BeginUs = It->second.back();
+ It->second.pop_back();
+ float DurationMs = float(TimeUs - BeginUs) / 1000.0f;
+ AddSample(Ev.StatId(), ThreadId, BeginUs, DurationMs);
+ }
+
+ void OnBeginExclusiveStat(const CsvProfiler_BeginExclusiveStat& Ev)
+ {
+ // For basic support, treat exclusive stats like regular stats.
+ uint32_t ThreadId = Ev.get_thread_id();
+ uint32_t TimeUs = CycleToTimeUs(Ev.Cycle());
+ m_OpenStacks[{Ev.StatId(), ThreadId}].push_back(TimeUs);
+ }
+
+ void OnEndExclusiveStat(const CsvProfiler_EndExclusiveStat& Ev)
+ {
+ uint32_t ThreadId = Ev.get_thread_id();
+ uint32_t TimeUs = CycleToTimeUs(Ev.Cycle());
+ auto Key = SeriesKey{Ev.StatId(), ThreadId};
+ auto It = m_OpenStacks.find(Key);
+ if (It == m_OpenStacks.end() || It->second.empty())
+ {
+ return;
+ }
+ uint32_t BeginUs = It->second.back();
+ It->second.pop_back();
+ float DurationMs = float(TimeUs - BeginUs) / 1000.0f;
+ AddSample(Ev.StatId(), ThreadId, BeginUs, DurationMs);
+ }
+
+ void OnCustomStatInt(const CsvProfiler_CustomStatInt& Ev)
+ {
+ uint32_t ThreadId = Ev.get_thread_id();
+ uint32_t TimeUs = CycleToTimeUs(Ev.Cycle());
+ AddSample(Ev.StatId(), ThreadId, TimeUs, float(Ev.Value()));
+ }
+
+ void OnCustomStatFloat(const CsvProfiler_CustomStatFloat& Ev)
+ {
+ uint32_t ThreadId = Ev.get_thread_id();
+ uint32_t TimeUs = CycleToTimeUs(Ev.Cycle());
+ AddSample(Ev.StatId(), ThreadId, TimeUs, Ev.Value());
+ }
+
+ void OnEvent(const CsvProfiler_Event& Ev)
+ {
+ zen::trace_detail::TraceModel::CsvEvent E;
+ E.TimeUs = CycleToTimeUs(Ev.Cycle());
+ E.CategoryIndex = Ev.CategoryIndex();
+ E.Text = DecodeWideName(Ev.Text());
+ m_Events.push_back(std::move(E));
+ }
+
+ void OnBeginCapture(const CsvProfiler_BeginCapture& Ev) { m_CaptureStartUs = CycleToTimeUs(Ev.Cycle()); }
+
+ void OnEndCapture(const CsvProfiler_EndCapture& Ev) { m_CaptureEndUs = CycleToTimeUs(Ev.Cycle()); }
+
+ void OnMetadata(const CsvProfiler_Metadata& Ev)
+ {
+ zen::trace_detail::TraceModel::CsvMeta M;
+ M.Key = DecodeWideName(Ev.Key());
+ M.Value = DecodeWideName(Ev.Value());
+ m_Metadata.push_back(std::move(M));
+ }
+
+ const TraceTiming* m_Timing = nullptr;
+
+ eastl::vector<zen::trace_detail::TraceModel::CsvCategory> m_Categories;
+ eastl::vector<zen::trace_detail::TraceModel::CsvStatDef> m_StatDefs;
+ eastl::hash_map<uint64_t, uint32_t> m_StatIdToIndex;
+
+ // Timing stacks: (StatId, ThreadId) -> stack of begin times
+ eastl::hash_map<SeriesKey, eastl::vector<uint32_t>, SeriesKeyHash> m_OpenStacks;
+
+ // Accumulated samples: (StatId, ThreadId) -> samples
+ eastl::hash_map<SeriesKey, eastl::vector<zen::trace_detail::TraceModel::CsvSample>, SeriesKeyHash> m_SeriesMap;
+
+ eastl::vector<zen::trace_detail::TraceModel::CsvEvent> m_Events;
+ eastl::vector<zen::trace_detail::TraceModel::CsvMeta> m_Metadata;
+
+ uint32_t m_CaptureStartUs = 0;
+ uint32_t m_CaptureEndUs = 0;
+};
+
+//////////////////////////////////////////////////////////////////////////////
+// Analyzers
+
+class CpuAnalyzer : public Analyzer
+{
+public:
+ CpuAnalyzer(EventStats& Stats, NameDepot& Names, const MetadataRegistry* Metadata)
+ : m_Names(Names)
+ , m_Stats(Stats)
+ , m_Metadata(Metadata)
+ {
+ Names.Add(NO_NAME, "???");
+ }
+
+ void subscribe(Vector<Subscription>& Subs) override
+ {
+ Subs.emplace_back(this, &CpuAnalyzer::OnNewTrace);
+ Subs.emplace_back(this, &CpuAnalyzer::OnCpuSpec);
+ Subs.emplace_back(this, &CpuAnalyzer::OnCpuBatch);
+ Subs.emplace_back(this, &CpuAnalyzer::OnCpuBatchV2);
+ Subs.emplace_back(this, &CpuAnalyzer::OnCpuBatchV3);
+ }
+
+private:
+ static constexpr uint32 NO_INDEX = ~0u;
+ static constexpr uint64 NO_NAME = ~0ull;
+ static constexpr uint32 METADATA_BIT = 0x8000'0000u;
+
+ uint64 ResolveNameHash(uint32 PackedId)
+ {
+ const bool IsMetadata = (PackedId & METADATA_BIT) != 0;
+ const uint32 Id = PackedId & ~METADATA_BIT;
+
+ if (IsMetadata && m_Metadata)
+ {
+ auto CachedIt = m_MetadataNames.find(Id);
+ if (CachedIt != m_MetadataNames.end())
+ {
+ return CachedIt->second;
+ }
+
+ const MetadataEntry* Entry = m_Metadata->Lookup(Id);
+ if (Entry)
+ {
+ auto BaseIt = m_Specs.find(Entry->SpecId);
+ StringView BaseName = (BaseIt != m_Specs.end()) ? m_Names.Get(BaseIt->second) : StringView("???");
+ std::string Formatted(BaseName);
+ std::string Values = FormatMetadataValues(Entry->Bytes.data(), Entry->Bytes.size());
+ if (!Values.empty())
+ {
+ Formatted += " - ";
+ Formatted += Values;
+ }
+ uint64 Hash = m_Names.Add(StringView(Formatted));
+ m_MetadataNames[Id] = Hash;
+ return Hash;
+ }
+ return NO_NAME;
+ }
+
+ auto It = m_Specs.find(Id);
+ return (It != m_Specs.end()) ? It->second : NO_NAME;
+ }
+
+ struct EventStack
+ {
+ uint32 Tail = NO_INDEX;
+ };
+
+ struct ScopeEvent
+ {
+ uint32 Id;
+ uint32 TimeUs;
+ union
+ {
+ uint32 Next;
+ uint32 Index;
+ };
+ };
+
+ struct EventPool
+ {
+ uint32 Alloc()
+ {
+ if (m_FreeHead == NO_INDEX)
+ {
+ uint32 Idx = uint32(m_Pool.size());
+ m_Pool.push_back({.Index = Idx});
+ return Idx;
+ }
+ uint32 Idx = m_FreeHead;
+ m_FreeHead = m_Pool[Idx].Index;
+ return Idx;
+ }
+
+ void Free(uint32 Idx)
+ {
+ m_Pool[Idx].Index = m_FreeHead;
+ m_FreeHead = Idx;
+ }
+
+ ScopeEvent& Get(uint32 Idx) { return m_Pool[Idx]; }
+
+ eastl::vector<ScopeEvent> m_Pool;
+ uint32 m_FreeHead = NO_INDEX;
+ };
+
+ void OnNewTrace(const $Trace_NewTrace& NewTrace)
+ {
+ m_Freq = NewTrace.CycleFrequency();
+ m_Base = NewTrace.StartCycle();
+ m_UsDiv = m_Freq / 1'000'000;
+ if (m_UsDiv == 0)
+ {
+ m_UsDiv = 1;
+ }
+ m_UsDivRecip = ReciprocalU64(m_UsDiv);
+ }
+
+ void OnCpuSpec(const CpuProfiler_EventSpec& Spec)
+ {
+ uint32 SpecId = Spec.Id();
+ FieldStr SpecName = Spec.Name();
+
+ StringView NameView = SpecName.as_view();
+
+ if (NameView.starts_with("Frame "))
+ {
+ NameView = "Frame";
+ }
+ if (size_t Pos = NameView.find("\""); Pos != StringView::npos)
+ {
+ NameView = NameView.substr(0, Pos);
+ }
+ if (size_t Pos = NameView.find("\\"); Pos != StringView::npos)
+ {
+ NameView = NameView.substr(0, Pos);
+ }
+
+ m_Specs[SpecId] = m_Names.Add(NameView);
+ }
+
+ void OnCpuBatch(const CpuProfiler_EventBatch& Batch)
+ {
+ uint32 ThreadId = Batch.get_thread_id();
+ Array<uint8[]> Data = Batch.Data();
+ AbsorbBatch(/*Version=*/1, ThreadId, Data);
+ }
+
+ void OnCpuBatchV2(const CpuProfiler_EventBatchV2& Batch)
+ {
+ uint32 ThreadId = Batch.get_thread_id();
+ Array<uint8[]> Data = Batch.Data();
+ AbsorbBatch(/*Version=*/2, ThreadId, Data);
+ }
+
+ void OnCpuBatchV3(const CpuProfiler_EventBatchV3& Batch)
+ {
+ uint32 ThreadId = Batch.get_thread_id();
+ Array<uint8[]> Data = Batch.Data();
+ AbsorbBatch(/*Version=*/3, ThreadId, Data);
+ }
+
+ // Decodes a CpuProfiler scope batch. Mirrors UE's reference
+ // TraceServices/.../CpuProfilerTraceAnalysis.cpp ProcessBuffer /
+ // ProcessBufferV2.
+ //
+ // Version 1 (`CpuProfiler.EventBatch`): cycle is `value >> 1`; bit 0 is
+ // IsEnter; IsEnter events carry a SpecId varint.
+ //
+ // Version 2 (`CpuProfiler.EventBatchV2`, UE 5.1..5.5) and Version 3
+ // (`CpuProfiler.EventBatchV3`, UE 5.6+): cycle is `value >> 2`; bit 0 is
+ // IsEnter, bit 1 is IsCoroutine. Coroutine begin events carry CoroutineId
+ // and TimerScopeDepth varints; coroutine end events carry a single
+ // TimerScopeDepth varint. V3 additionally reserves the low bit of the
+ // SpecId to mark metadata-bearing timers, so SpecId must be shifted
+ // right by 1 to recover the actual spec id.
+ void AbsorbBatch(uint32 Version, uint32 ThreadId, const Array<uint8[]>& Data)
+ {
+ const uint8* Cursor = Data.get();
+ const uint8* End = Cursor + Data.get_size();
+
+ auto Decode = [&]() {
+ uint64 Value = 0;
+ for (uint32 I = 1, J = 0; I; J += 7)
+ {
+ I = *Cursor++;
+ Value |= uint64(I & 0x7f) << J;
+ I &= 0x80;
+ }
+ return Value;
+ };
+
+ if (ThreadId >= m_Threads.size())
+ {
+ m_Threads.resize(ThreadId + 1);
+ }
+ EventStack& Stack = m_Threads[ThreadId];
+
+ const uint32 CycleShift = (Version == 1) ? 1u : 2u;
+
+ uint64 Base = m_Base;
+
+ uint64 Cycle = ~Base + 1;
+ while (Cursor < End)
+ {
+ uint64 Value = Decode();
+ uint32 IsEnter = (Value & 0b01);
+
+ if (Version > 1 && (Value & 0b10))
+ {
+ // Coroutine event -- not visualised, but the trailing varints
+ // still need to be consumed so we stay in sync with the
+ // stream.
+ if (IsEnter)
+ {
+ (void)Decode(); // CoroutineId
+ (void)Decode(); // TimerScopeDepth
+ }
+ else
+ {
+ (void)Decode(); // TimerScopeDepth
+ }
+ continue;
+ }
+
+ uint64 EventId = IsEnter ? Decode() : ~0ull;
+
+ Cycle += (Value >> CycleShift);
+ uint32 TimeUs = m_UsDivRecip.Divide(Cycle + (m_UsDiv >> 1));
+
+ if (IsEnter)
+ {
+ uint32 ScopeId = uint32(EventId);
+ bool IsMetadata = false;
+ if (Version == 3)
+ {
+ IsMetadata = (ScopeId & 1u) != 0;
+ ScopeId >>= 1;
+ }
+ uint32 EvIdx = m_Events.Alloc();
+ ScopeEvent& Ev = m_Events.Get(EvIdx);
+ // Pack the metadata flag in the high bit so the close path
+ // can distinguish metadata-id scopes from regular ones without
+ // an extra field.
+ Ev.Id = IsMetadata ? (ScopeId | 0x8000'0000u) : ScopeId;
+ Ev.TimeUs = TimeUs;
+ Ev.Next = Stack.Tail;
+ Stack.Tail = EvIdx;
+ continue;
+ }
+
+ if (Stack.Tail == NO_INDEX)
+ {
+ continue;
+ }
+
+ ScopeEvent& Ev = m_Events.Get(Stack.Tail);
+ uint32 DurationUs = TimeUs - Ev.TimeUs;
+ uint64 NameHash = ResolveNameHash(Ev.Id);
+ m_Stats.Record(NameHash, DurationUs);
+
+ uint32 NextIdx = Ev.Next;
+ m_Events.Free(Stack.Tail);
+ Stack.Tail = NextIdx;
+ }
+ }
+
+ uint64 m_Freq = 0;
+ uint64 m_Base = 0;
+ uint64 m_UsDiv = 1;
+ ReciprocalU64 m_UsDivRecip;
+ eastl::hash_map<uint32, uint64> m_Specs;
+ NameDepot& m_Names;
+ EventPool m_Events;
+ eastl::vector<EventStack> m_Threads;
+ EventStats& m_Stats;
+ const MetadataRegistry* m_Metadata = nullptr;
+ // Caches the resolved name hash for each MetadataId so we don't
+ // re-format the same CBOR payload on every scope-close.
+ eastl::hash_map<uint32, uint64> m_MetadataNames;
+};
+
+//////////////////////////////////////////////////////////////////////////////
+// Per-event CPU scope capture for the interactive trace viewer.
+//
+// Mirrors CpuAnalyzer's decode loop but instead of aggregating statistics,
+// it records one TimelineScope per closed CPU scope so the viewer can draw a
+// flame graph. Scope names are interned into a flat vector so each event only
+// stores a compact uint32 NameId.
+
+class TimelineAnalyzer : public Analyzer
+{
+public:
+ explicit TimelineAnalyzer(const MetadataRegistry* Metadata = nullptr, TraceTiming* SharedTiming = nullptr)
+ : m_SharedTiming(SharedTiming)
+ , m_Metadata(Metadata)
+ {
+ }
+
+ void subscribe(Vector<Subscription>& Subs) override
+ {
+ Subs.emplace_back(this, &TimelineAnalyzer::OnNewTrace);
+ Subs.emplace_back(this, &TimelineAnalyzer::OnCpuSpec);
+ Subs.emplace_back(this, &TimelineAnalyzer::OnCpuBatch);
+ Subs.emplace_back(this, &TimelineAnalyzer::OnCpuBatchV2);
+ Subs.emplace_back(this, &TimelineAnalyzer::OnCpuBatchV3);
+ }
+
+ struct ThreadData
+ {
+ eastl::vector<zen::trace_detail::TimelineScope> Scopes;
+ // Open-scope stack: parallel arrays keeping begin time and name id.
+ eastl::vector<uint32_t> OpenBeginUs;
+ eastl::vector<uint32_t> OpenNameIds;
+ };
+
+ const eastl::vector<std::string>& ScopeNames() const { return m_ScopeNames; }
+ const eastl::map<uint32_t, ThreadData>& Threads() const { return m_Threads; }
+ uint32_t MinBeginUs() const { return m_MinBeginUs; }
+ uint32_t MaxEndUs() const { return m_MaxEndUs; }
+
+private:
+ static constexpr uint32_t INVALID_NAME = ~0u;
+
+ uint32_t InternName(StringView Name)
+ {
+ String Key(Name);
+ auto [It, Inserted] = m_NameIndex.try_emplace(std::move(Key), 0);
+ if (Inserted)
+ {
+ It->second = uint32_t(m_ScopeNames.size());
+ m_ScopeNames.emplace_back(Name);
+ }
+ return It->second;
+ }
+
+ void OnNewTrace(const $Trace_NewTrace& NewTrace)
+ {
+ m_Freq = NewTrace.CycleFrequency();
+ m_Base = NewTrace.StartCycle();
+ m_UsDiv = m_Freq / 1'000'000;
+ if (m_UsDiv == 0)
+ {
+ m_UsDiv = 1;
+ }
+ m_UsDivRecip = ReciprocalU64(m_UsDiv);
+ if (m_SharedTiming)
+ {
+ m_SharedTiming->Freq = m_Freq;
+ m_SharedTiming->Base = m_Base;
+ m_SharedTiming->UsDiv = m_UsDiv;
+ }
+ }
+
+ void OnCpuSpec(const CpuProfiler_EventSpec& Spec)
+ {
+ uint32 SpecId = Spec.Id();
+ FieldStr SpecName = Spec.Name();
+
+ StringView NameView = SpecName.as_view();
+
+ if (NameView.starts_with("Frame "))
+ {
+ NameView = "Frame";
+ }
+ if (size_t Pos = NameView.find("\""); Pos != StringView::npos)
+ {
+ NameView = NameView.substr(0, Pos);
+ }
+ if (size_t Pos = NameView.find("\\"); Pos != StringView::npos)
+ {
+ NameView = NameView.substr(0, Pos);
+ }
+
+ m_Specs[SpecId] = InternName(NameView);
+ }
+
+ void OnCpuBatch(const CpuProfiler_EventBatch& Batch)
+ {
+ uint32 ThreadId = Batch.get_thread_id();
+ Array<uint8[]> Data = Batch.Data();
+ AbsorbBatch(/*Version=*/1, ThreadId, Data);
+ }
+
+ void OnCpuBatchV2(const CpuProfiler_EventBatchV2& Batch)
+ {
+ uint32 ThreadId = Batch.get_thread_id();
+ Array<uint8[]> Data = Batch.Data();
+ AbsorbBatch(/*Version=*/2, ThreadId, Data);
+ }
+
+ void OnCpuBatchV3(const CpuProfiler_EventBatchV3& Batch)
+ {
+ uint32 ThreadId = Batch.get_thread_id();
+ Array<uint8[]> Data = Batch.Data();
+ AbsorbBatch(/*Version=*/3, ThreadId, Data);
+ }
+
+ TraceTiming* m_SharedTiming = nullptr;
+
+ // See CpuAnalyzer::AbsorbBatch for a detailed description of the wire
+ // format for each version.
+ void AbsorbBatch(uint32 Version, uint32 ThreadId, const Array<uint8[]>& Data)
+ {
+ const uint8* Cursor = Data.get();
+ const uint8* End = Cursor + Data.get_size();
+
+ auto Decode = [&]() {
+ uint64 Value = 0;
+ for (uint32 I = 1, J = 0; I; J += 7)
+ {
+ I = *Cursor++;
+ Value |= uint64(I & 0x7f) << J;
+ I &= 0x80;
+ }
+ return Value;
+ };
+
+ ThreadData& Thread = m_Threads[ThreadId];
+
+ const uint32 CycleShift = (Version == 1) ? 1u : 2u;
+
+ uint64 Base = m_Base;
+
+ uint64 Cycle = ~Base + 1;
+ while (Cursor < End)
+ {
+ uint64 Value = Decode();
+ uint32 IsEnter = (Value & 0b01);
+
+ if (Version > 1 && (Value & 0b10))
+ {
+ // Coroutine event -- consume the trailing varints so we stay
+ // in sync with the stream, but drop the event on the floor.
+ if (IsEnter)
+ {
+ (void)Decode(); // CoroutineId
+ (void)Decode(); // TimerScopeDepth
+ }
+ else
+ {
+ (void)Decode(); // TimerScopeDepth
+ }
+ continue;
+ }
+
+ uint64 EventId = IsEnter ? Decode() : ~0ull;
+
+ Cycle += (Value >> CycleShift);
+ uint32 TimeUs = m_UsDivRecip.Divide(Cycle + (m_UsDiv >> 1));
+
+ if (IsEnter)
+ {
+ uint32 ScopeId = uint32(EventId);
+ bool IsMetadata = false;
+ if (Version == 3)
+ {
+ IsMetadata = (ScopeId & 1u) != 0;
+ ScopeId >>= 1;
+ }
+ uint32_t NameId = IsMetadata ? ResolveMetadataNameId(ScopeId) : LookupSpecNameId(ScopeId);
+ Thread.OpenBeginUs.push_back(TimeUs);
+ Thread.OpenNameIds.push_back(NameId);
+ continue;
+ }
+
+ if (Thread.OpenBeginUs.empty())
+ {
+ continue;
+ }
+
+ uint32_t BeginUs = Thread.OpenBeginUs.back();
+ uint32_t NameId = Thread.OpenNameIds.back();
+ Thread.OpenBeginUs.pop_back();
+ Thread.OpenNameIds.pop_back();
+
+ if (NameId == INVALID_NAME)
+ {
+ continue;
+ }
+
+ uint16_t Depth = uint16_t(Thread.OpenBeginUs.size());
+ Thread.Scopes.push_back(zen::trace_detail::TimelineScope{
+ .BeginUs = BeginUs,
+ .DurationUs = TimeUs - BeginUs,
+ .NameId = NameId,
+ .Depth = Depth,
+ .MergeCount = 0,
+ });
+
+ if (BeginUs < m_MinBeginUs)
+ {
+ m_MinBeginUs = BeginUs;
+ }
+ if (TimeUs > m_MaxEndUs)
+ {
+ m_MaxEndUs = TimeUs;
+ }
+ }
+ }
+
+ uint32_t LookupSpecNameId(uint32 SpecId) const
+ {
+ auto It = m_Specs.find(SpecId);
+ return (It != m_Specs.end()) ? It->second : INVALID_NAME;
+ }
+
+ uint32_t ResolveMetadataNameId(uint32 MetadataId)
+ {
+ if (!m_Metadata)
+ {
+ return INVALID_NAME;
+ }
+
+ auto CachedIt = m_MetadataNameIds.find(MetadataId);
+ if (CachedIt != m_MetadataNameIds.end())
+ {
+ return CachedIt->second;
+ }
+
+ const MetadataEntry* Entry = m_Metadata->Lookup(MetadataId);
+ if (!Entry)
+ {
+ return INVALID_NAME;
+ }
+
+ auto BaseIt = m_Specs.find(Entry->SpecId);
+ const std::string& BaseName = (BaseIt != m_Specs.end()) ? m_ScopeNames[BaseIt->second] : kUnknownName;
+ std::string Formatted = BaseName;
+ std::string Values = FormatMetadataValues(Entry->Bytes.data(), Entry->Bytes.size());
+ if (!Values.empty())
+ {
+ Formatted += " - ";
+ Formatted += Values;
+ }
+
+ uint32_t NameId = InternName(StringView(Formatted.data(), Formatted.size()));
+ m_MetadataNameIds[MetadataId] = NameId;
+ return NameId;
+ }
+
+ static inline const std::string kUnknownName{"???"};
+
+ uint64 m_Freq = 0;
+ uint64 m_Base = 0;
+ uint64 m_UsDiv = 1;
+ ReciprocalU64 m_UsDivRecip;
+ uint32_t m_MinBeginUs = ~0u;
+ uint32_t m_MaxEndUs = 0;
+ eastl::hash_map<uint32, uint32_t> m_Specs;
+ eastl::hash_map<String, uint32_t> m_NameIndex;
+ eastl::vector<std::string> m_ScopeNames;
+ eastl::map<uint32_t, ThreadData> m_Threads;
+ const MetadataRegistry* m_Metadata = nullptr;
+ eastl::hash_map<uint32_t, uint32_t> m_MetadataNameIds;
+};
+
+//////////////////////////////////////////////////////////////////////////////
+
+} // anonymous namespace
+
+std::string
+zen::trace_detail::SafeFieldStr(FieldStr&& Field)
+{
+ try
+ {
+ std::string_view View = Field.as_view();
+ // Some trace writers include the NUL terminator in the field length
+ // (see UE trace ToAnsiCheap / ThreadRegister). Strip any trailing NULs
+ // so downstream consumers don't see garbage.
+ while (!View.empty() && View.back() == '\0')
+ {
+ View.remove_suffix(1);
+ }
+ return std::string(View);
+ }
+ catch (const std::exception& E)
+ {
+ ZEN_DEBUG("Failed to decode trace string field: {}", E.what());
+ return {};
+ }
+}
+
+namespace {
+
+// Derive a thread group name from a thread name by stripping a trailing
+// integer suffix (optionally preceded by a separator). E.g. "IoPool Worker 3"
+// -> "IoPool Worker", "DbWorker_12" -> "DbWorker", "HttpThread42" ->
+// "HttpThread". Returns an empty string if no suffix is present or the
+// resulting prefix would be empty.
+static std::string
+SynthesizeThreadGroupFromName(std::string_view Name)
+{
+ size_t I = Name.size();
+ while (I > 0 && Name[I - 1] >= '0' && Name[I - 1] <= '9')
+ {
+ --I;
+ }
+ if (I == Name.size())
+ {
+ return {}; // no trailing digits
+ }
+ if (I > 0)
+ {
+ char C = Name[I - 1];
+ if (C == '_' || C == '-' || C == '.' || C == ':' || C == '#' || C == '/' || C == ' ' || C == '\t')
+ {
+ --I;
+ }
+ }
+ while (I > 0 && (Name[I - 1] == ' ' || Name[I - 1] == '\t'))
+ {
+ --I;
+ }
+ if (I == 0)
+ {
+ return {}; // pure-numeric name
+ }
+ return std::string(Name.substr(0, I));
+}
+
+class SessionAnalyzer : public Analyzer
+{
+public:
+ zen::trace_detail::SessionInfo Session;
+ eastl::map<uint32_t, zen::trace_detail::ThreadInfoEntry> ThreadNames;
+ eastl::map<uint32_t, zen::trace_detail::ChannelInfo> Channels;
+
+ void subscribe(Vector<Subscription>& Subs) override
+ {
+ Subs.emplace_back(this, &SessionAnalyzer::OnSession);
+ Subs.emplace_back(this, &SessionAnalyzer::OnThreadGroupBegin);
+ Subs.emplace_back(this, &SessionAnalyzer::OnThreadGroupEnd);
+ Subs.emplace_back(this, &SessionAnalyzer::OnThreadInfo);
+ Subs.emplace_back(this, &SessionAnalyzer::OnChannelAnnounce);
+ Subs.emplace_back(this, &SessionAnalyzer::OnChannelToggle);
+ }
+
+private:
+ eastl::vector<String> m_GroupStack;
+
+ void OnSession(const Diagnostics_Session2& Ev)
+ {
+ Session.Platform = SafeFieldStr(Ev.Platform());
+ Session.AppName = SafeFieldStr(Ev.AppName());
+ Session.ProjectName = SafeFieldStr(Ev.ProjectName());
+ Session.CommandLine = SafeFieldStr(Ev.CommandLine());
+ Session.Branch = SafeFieldStr(Ev.Branch());
+ Session.BuildVersion = SafeFieldStr(Ev.BuildVersion());
+ Session.Changelist = Ev.Changelist();
+ Session.ConfigurationType = Ev.ConfigurationType();
+ Session.HasSession = true;
+ }
+
+ void OnThreadGroupBegin(const $Trace_ThreadGroupBegin& Ev) { m_GroupStack.push_back(SafeFieldStr(Ev.Name())); }
+
+ void OnThreadGroupEnd(const $Trace_ThreadGroupEnd&)
+ {
+ if (!m_GroupStack.empty())
+ {
+ m_GroupStack.pop_back();
+ }
+ }
+
+ void OnThreadInfo(const $Trace_ThreadInfo& Ev)
+ {
+ uint32_t Tid = Ev.ThreadId();
+ zen::trace_detail::ThreadInfoEntry& Info = ThreadNames[Tid];
+ Info.ThreadId = Tid;
+ Info.Name = SafeFieldStr(Ev.Name());
+ Info.GroupName = m_GroupStack.empty() ? "" : m_GroupStack.back();
+ if (Info.GroupName.empty())
+ {
+ Info.GroupName = SynthesizeThreadGroupFromName(Info.Name);
+ }
+ Info.SystemId = Ev.SystemId();
+ Info.SortHint = Ev.SortHint();
+ }
+
+ void OnChannelAnnounce(const Trace_ChannelAnnounce& Ev)
+ {
+ uint32_t Id = Ev.Id();
+ zen::trace_detail::ChannelInfo& Info = Channels[Id];
+ Info.Name = SafeFieldStr(Ev.Name());
+ Info.Enabled = Ev.IsEnabled() != 0;
+ Info.ReadOnly = Ev.ReadOnly() != 0;
+ }
+
+ void OnChannelToggle(const Trace_ChannelToggle& Ev)
+ {
+ uint32_t Id = Ev.Id();
+ auto It = Channels.find(Id);
+ if (It != Channels.end())
+ {
+ It->second.Enabled = Ev.IsEnabled() != 0;
+ }
+ }
+};
+
+//////////////////////////////////////////////////////////////////////////////
+// Module analyzer
+//
+// Captures Diagnostics.Module{Init,Load,Unload} so TraceModel::Modules has a
+// populated list of loaded DLLs. These events are NoSync+Important so they
+// don't carry a Cycle field (no load/unload timestamps available) but they
+// do survive reconnects and the trim filter. The analyzer is intentionally
+// passive -- we stash the raw data here and leave symbolication and memory
+// attribution to whatever consumes TraceModel::Modules later.
+
+class ModuleAnalyzer : public Analyzer
+{
+public:
+ eastl::map<uint64_t, zen::trace_detail::ModuleInfo> ModulesByBase;
+ std::string SymbolFormat;
+ uint8_t BaseShift = 0;
+
+ void subscribe(Vector<Subscription>& Subs) override
+ {
+ Subs.emplace_back(this, &ModuleAnalyzer::OnModuleInit);
+ Subs.emplace_back(this, &ModuleAnalyzer::OnModuleLoad);
+ Subs.emplace_back(this, &ModuleAnalyzer::OnModuleUnload);
+ }
+
+private:
+ void OnModuleInit(const Diagnostics_ModuleInit& Ev)
+ {
+ SymbolFormat = SafeFieldStr(Ev.SymbolFormat());
+ BaseShift = Ev.ModuleBaseShift();
+ }
+
+ void OnModuleLoad(const Diagnostics_ModuleLoad& Ev)
+ {
+ // Older traces stored Base as a 32-bit value shifted right by
+ // ModuleBaseShift to fit. Modern traces set BaseShift to zero and
+ // Base is a full 64-bit address; applying the shift then is a
+ // harmless no-op.
+ uint64_t Base = uint64_t(Ev.Base()) << BaseShift;
+
+ zen::trace_detail::ModuleInfo& Info = ModulesByBase[Base];
+ Info.FullPath = SafeFieldStr(Ev.Name());
+ Info.Base = Base;
+ Info.Size = Ev.Size();
+ Info.Unloaded = false;
+
+ // Extract the basename without pulling in the whole filesystem
+ // library for a single operation. UE emits forward- or backslashes
+ // depending on platform, so handle both.
+ const std::string& Path = Info.FullPath;
+ size_t Cut = Path.find_last_of("/\\");
+ Info.Name = (Cut == std::string::npos) ? Path : Path.substr(Cut + 1);
+
+ ::Array<uint8[]> ImageId = Ev.ImageId();
+ const uint8* IdPtr = ImageId.get();
+ const uint32 IdSize = ImageId.get_size();
+ Info.ImageId.assign(IdPtr, IdPtr + IdSize);
+ }
+
+ void OnModuleUnload(const Diagnostics_ModuleUnload& Ev)
+ {
+ uint64_t Base = uint64_t(Ev.Base()) << BaseShift;
+ auto It = ModulesByBase.find(Base);
+ if (It != ModulesByBase.end())
+ {
+ It->second.Unloaded = true;
+ }
+ }
+};
+
+//////////////////////////////////////////////////////////////////////////////
+// Trim analyzer
+//
+// Decodes CpuProfiler batch events to extract per-batch timestamp ranges AND
+// to track open/close scope bracketing per thread. The scope tracker lets us
+// identify "must-keep" packets: any packet containing the Leave event for a
+// scope whose Enter was at or before the user's trim EndUs. Preserving those
+// Leaves is what lets the downstream TimelineAnalyzer (and Unreal Insights)
+// still render long-running scopes that span the window end -- if we dropped
+// their closing event the scope would sit unmatched on the open-scope stack
+// and not render at all.
+//
+// Attribution to raw packet indices is approximate due to Tourist's internal
+// per-thread packet buffering; the trim driver processes the trace one packet
+// at a time (bundle of size 1) to keep it as tight as possible. Packets that
+// never get an attributed time range are conservatively retained by the
+// caller.
+
+class TrimAnalyzer : public Analyzer
+{
+public:
+ // Maps packet index (matching both Tourist's Packet::get_index() and our
+ // raw walker's vector position) -> (MinUs, MaxUs) of all events attributed
+ // to that packet.
+ struct Range
+ {
+ uint32_t MinUs = ~0u;
+ uint32_t MaxUs = 0;
+ };
+
+ eastl::hash_map<uint32_t, Range> PacketRanges;
+
+ // Maps thread id -> the maximum packet index that contains a Leave event
+ // for a scope whose matching Enter was at or before EndUs. These packets
+ // must be retained so the downstream analyzer can close the scope.
+ eastl::hash_map<uint32_t, uint32_t> MustKeepPacketByThread;
+
+ // Set by the trim driver from TraceTrimArgs::EndSec before the analysis
+ // pass begins. Used by the scope tracker to decide which leaves are
+ // "must keep".
+ uint32_t EndUs = ~0u;
+
+ // Updated by the trim driver before each Proto.read call when the next
+ // packet is on a normal thread. Maps normal-thread id -> the most
+ // recently scattered packet's index.
+ eastl::hash_map<uint32_t, uint32_t> LastPacketIndexByThread;
+
+ void subscribe(Vector<Subscription>& Subs) override
+ {
+ Subs.emplace_back(this, &TrimAnalyzer::OnNewTrace);
+ Subs.emplace_back(this, &TrimAnalyzer::OnCpuBatch);
+ Subs.emplace_back(this, &TrimAnalyzer::OnCpuBatchV2);
+ Subs.emplace_back(this, &TrimAnalyzer::OnCpuBatchV3);
+ }
+
+ bool HasTimeBase() const { return m_Freq != 0; }
+
+private:
+ void OnNewTrace(const $Trace_NewTrace& NewTrace)
+ {
+ m_Freq = NewTrace.CycleFrequency();
+ m_Base = NewTrace.StartCycle();
+ m_UsDiv = (m_Freq > 0) ? (m_Freq / 1'000'000) : 1;
+ if (m_UsDiv == 0)
+ {
+ m_UsDiv = 1;
+ }
+ m_UsDivRecip = ReciprocalU64(m_UsDiv);
+ }
+
+ void OnCpuBatch(const CpuProfiler_EventBatch& Batch) { AbsorbBatchTimes(/*Version=*/1, Batch.get_thread_id(), Batch.Data()); }
+
+ void OnCpuBatchV2(const CpuProfiler_EventBatchV2& Batch) { AbsorbBatchTimes(/*Version=*/2, Batch.get_thread_id(), Batch.Data()); }
+
+ void OnCpuBatchV3(const CpuProfiler_EventBatchV3& Batch) { AbsorbBatchTimes(/*Version=*/3, Batch.get_thread_id(), Batch.Data()); }
+
+ // Decodes cycle deltas in a CpuProfiler batch to find the timestamp range
+ // AND to maintain a per-thread open-scope stack. Mirrors the wire format
+ // documented in CpuAnalyzer::AbsorbBatch. Scope ids are decoded just far
+ // enough to keep the varint cursor in sync; we don't store them.
+ void AbsorbBatchTimes(uint32 Version, uint32 ThreadId, const Array<uint8[]>& Data)
+ {
+ if (m_Freq == 0)
+ {
+ return;
+ }
+
+ auto It = LastPacketIndexByThread.find(ThreadId);
+ if (It == LastPacketIndexByThread.end())
+ {
+ return;
+ }
+ const uint32_t PacketIndex = It->second;
+
+ // The open-scope stack is maintained across every batch on a thread.
+ // Each entry stores the Enter time in microseconds from trace start.
+ eastl::vector<uint32_t>& OpenStack = m_OpenScopes[ThreadId];
+
+ const uint8* Cursor = Data.get();
+ const uint8* End = Cursor + Data.get_size();
+
+ auto Decode = [&]() {
+ uint64 Value = 0;
+ for (uint32 I = 1, J = 0; I; J += 7)
+ {
+ I = *Cursor++;
+ Value |= uint64(I & 0x7f) << J;
+ I &= 0x80;
+ }
+ return Value;
+ };
+
+ const uint32 CycleShift = (Version == 1) ? 1u : 2u;
+ const uint64 Base = m_Base;
+
+ uint32_t BatchMinUs = ~0u;
+ uint32_t BatchMaxUs = 0;
+ bool HasAny = false;
+
+ uint64 Cycle = ~Base + 1;
+ while (Cursor < End)
+ {
+ uint64 Value = Decode();
+ uint32 IsEnter = (Value & 0b01);
+
+ if (Version > 1 && (Value & 0b10))
+ {
+ // Coroutine event -- consume the trailing varints and skip.
+ // These don't participate in the scope bracket tracking; the
+ // existing TimelineAnalyzer ignores them for the same reason.
+ if (IsEnter)
+ {
+ (void)Decode(); // CoroutineId
+ (void)Decode(); // TimerScopeDepth
+ }
+ else
+ {
+ (void)Decode(); // TimerScopeDepth
+ }
+ continue;
+ }
+
+ if (IsEnter)
+ {
+ (void)Decode(); // EventId / SpecId
+ }
+
+ Cycle += (Value >> CycleShift);
+ uint32_t TimeUs = m_UsDivRecip.Divide(Cycle + (m_UsDiv >> 1));
+
+ if (!HasAny || TimeUs < BatchMinUs)
+ {
+ BatchMinUs = TimeUs;
+ }
+ if (!HasAny || TimeUs > BatchMaxUs)
+ {
+ BatchMaxUs = TimeUs;
+ }
+ HasAny = true;
+
+ if (IsEnter)
+ {
+ OpenStack.push_back(TimeUs);
+ }
+ else if (!OpenStack.empty())
+ {
+ uint32_t EnterTimeUs = OpenStack.back();
+ OpenStack.pop_back();
+
+ // If the scope started at or before the window end, we need
+ // its closing Leave event to survive so the downstream
+ // analyzer can render it. Mark the current packet (the one
+ // holding this Leave) as must-keep for the thread.
+ if (EnterTimeUs <= EndUs)
+ {
+ uint32_t& MustKeep = MustKeepPacketByThread[ThreadId];
+ if (PacketIndex > MustKeep)
+ {
+ MustKeep = PacketIndex;
+ }
+ }
+ }
+ }
+
+ if (!HasAny)
+ {
+ return;
+ }
+
+ Range& R = PacketRanges[PacketIndex];
+ R.MinUs = std::min(R.MinUs, BatchMinUs);
+ R.MaxUs = std::max(R.MaxUs, BatchMaxUs);
+ }
+
+ uint64 m_Freq = 0;
+ uint64 m_Base = 0;
+ uint64 m_UsDiv = 1;
+ ReciprocalU64 m_UsDivRecip;
+
+ // Per-thread open scope stack, carrying the Enter times in microseconds
+ // from trace start. Entries are pushed on Enter and popped on Leave; the
+ // stack may contain unclosed entries when decoding ends (scopes that
+ // outlive the captured trace).
+ eastl::hash_map<uint32_t, eastl::vector<uint32_t>> m_OpenScopes;
+};
+
+//////////////////////////////////////////////////////////////////////////////
+// Common trace iteration
+
+struct TraceSummary
+{
+ eastl::map<uint32_t, std::pair<std::string, uint64_t>> TypeInfo;
+ eastl::set<uint16_t> Threads;
+ uint64_t TotalEvents = 0;
+};
+
+template<typename ParcelCallback>
+static TraceSummary
+IterateTrace(::DataSource& Source, ParcelCallback OnParcel, const zen::trace_detail::ProgressCallback& OnProgress = {})
+{
+ TraceSummary Summary;
+
+ try
+ {
+ uint64_t TotalFileBytes = uint64_t(std::max(Source.get_size(), int64(0)));
+
+ ::Allocator TraceAllocator;
+ ::Preamble Pream(Source, TraceAllocator);
+ ::Transport Xport = Pream.get_transport();
+ ::Protocol Proto = Pream.get_protocol();
+
+ ::Packet Packets[128];
+ ::EventParcel Parcel;
+
+ while (::Bundle Bndl = Xport.read_packets(Packets))
+ {
+ Parcel.reset();
+ Proto.read(Parcel, Bndl);
+
+ OnParcel(Parcel);
+
+ for (const ::Type* TraceType : Parcel.new_types)
+ {
+ auto [LoggerName, EventName] = TraceType->get_name();
+ std::string TypeName = fmt::format("{}.{}", std::string_view(LoggerName), std::string_view(EventName));
+ Summary.TypeInfo[TraceType->get_uid()] = {std::move(TypeName), 0};
+ }
+
+ for (const ::Event& Ev : Parcel.events)
+ {
+ Summary.TotalEvents++;
+ Summary.Threads.insert(Ev.thread_id);
+
+ auto It = Summary.TypeInfo.find(Ev.uid);
+ if (It != Summary.TypeInfo.end())
+ {
+ It->second.second++;
+ }
+ }
+
+ if (OnProgress)
+ {
+ OnProgress(Xport.tell(), TotalFileBytes, Summary.TotalEvents);
+ }
+ }
+ }
+ catch (const DataStream::Eof&)
+ {
+ }
+ catch (const Exception::StreamError& E)
+ {
+ throw std::runtime_error(fmt::format("Trace stream error at position {}: {} (value: {})", E.position, E.message, E.value));
+ }
+
+ return Summary;
+}
+
+// Print session metadata
+static void
+PrintSessionInfo(const SessionAnalyzer& SessionAn)
+{
+ const zen::trace_detail::SessionInfo& Sess = SessionAn.Session;
+ if (!Sess.HasSession)
+ {
+ return;
+ }
+
+ ZEN_CONSOLE("Platform: {}", Sess.Platform);
+ ZEN_CONSOLE("App: {}", Sess.AppName);
+ if (!Sess.ProjectName.empty())
+ {
+ ZEN_CONSOLE("Project: {}", Sess.ProjectName);
+ }
+ if (!Sess.Branch.empty())
+ {
+ ZEN_CONSOLE("Branch: {}", Sess.Branch);
+ }
+ if (!Sess.BuildVersion.empty())
+ {
+ ZEN_CONSOLE("Build: {}", Sess.BuildVersion);
+ }
+ if (Sess.Changelist)
+ {
+ ZEN_CONSOLE("Changelist: {}", Sess.Changelist);
+ }
+ if (!Sess.CommandLine.empty())
+ {
+ ZEN_CONSOLE("CommandLine: {}", Sess.CommandLine);
+ }
+ ZEN_CONSOLE("");
+}
+
+// Print thread names
+static void
+PrintThreadInfo(const SessionAnalyzer& SessionAn)
+{
+ if (SessionAn.ThreadNames.empty())
+ {
+ return;
+ }
+
+ eastl::vector<std::pair<uint32_t, const zen::trace_detail::ThreadInfoEntry*>> ThreadsSorted;
+ for (const auto& [Tid, Info] : SessionAn.ThreadNames)
+ {
+ ThreadsSorted.emplace_back(Tid, &Info);
+ }
+ eastl::sort(ThreadsSorted.begin(), ThreadsSorted.end(), [](const auto& A, const auto& B) {
+ return A.second->SortHint < B.second->SortHint;
+ });
+
+ ZEN_CONSOLE("");
+ ZEN_CONSOLE("Threads:");
+ ZEN_CONSOLE("");
+ ZEN_CONSOLE("{:>6} {:>10} {}", "TID", "SystemID", "Name");
+ ZEN_CONSOLE("{:-<{}}", "", 6 + 10 + 40 + 4);
+ for (const auto& [Tid, Info] : ThreadsSorted)
+ {
+ ZEN_CONSOLE("{:>6} {:>10} {}", Tid, Info->SystemId, Info->Name);
+ }
+}
+
+// Print trace channel info
+static void
+PrintChannelInfo(const SessionAnalyzer& SessionAn)
+{
+ if (SessionAn.Channels.empty())
+ {
+ return;
+ }
+
+ eastl::vector<const zen::trace_detail::ChannelInfo*> ChannelsSorted;
+ for (const auto& [Id, Info] : SessionAn.Channels)
+ {
+ ChannelsSorted.push_back(&Info);
+ }
+ eastl::sort(ChannelsSorted.begin(), ChannelsSorted.end(), [](const auto* A, const auto* B) { return A->Name < B->Name; });
+
+ ZEN_CONSOLE("");
+ ZEN_CONSOLE("Trace Channels:");
+ ZEN_CONSOLE("");
+ for (const zen::trace_detail::ChannelInfo* Ch : ChannelsSorted)
+ {
+ std::string_view State = Ch->Enabled ? "enabled" : "disabled";
+ if (Ch->ReadOnly)
+ {
+ ZEN_CONSOLE(" {} ({}, read-only)", Ch->Name, State);
+ }
+ else
+ {
+ ZEN_CONSOLE(" {} ({})", Ch->Name, State);
+ }
+ }
+}
+
+} // namespace
+
+//////////////////////////////////////////////////////////////////////////////
+
+namespace zen::trace_detail {
+
+std::filesystem::path
+ResolveTraceFile(const std::filesystem::path& Input, cxxopts::Options& HelpOptions)
+{
+ if (Input.empty())
+ {
+ throw zen::OptionParseException("File path is required", HelpOptions.help());
+ }
+
+ std::filesystem::path FilePath = std::filesystem::absolute(Input);
+ if (!std::filesystem::exists(FilePath))
+ {
+ throw std::runtime_error(fmt::format("File not found: {}", FilePath));
+ }
+
+ return FilePath;
+}
+
+void
+RunInspect(const std::filesystem::path& FilePath)
+{
+ ::DataSource Source(FilePath);
+
+ SessionAnalyzer SessionAn;
+ ::Dispatcher Dispatch;
+ Dispatch.add_analyzer(SessionAn);
+
+ // Collect type schemas
+ struct TypeSchema
+ {
+ std::string FullName;
+ uint32_t Uid = 0;
+ uint32_t FieldCount = 0;
+ uint32_t Flags = 0;
+ uint64_t EventCount = 0;
+ eastl::vector<std::string> FieldNames;
+ eastl::vector<uint32_t> FieldSizes;
+ eastl::vector<uint32_t> FieldTypeInfos;
+ };
+
+ eastl::map<uint32_t, TypeSchema> Schemas;
+
+ TraceSummary Summary = IterateTrace(Source, [&](const ::EventParcel& Parcel) {
+ Dispatch.on_parcel(Parcel);
+
+ for (const ::Type* TraceType : Parcel.new_types)
+ {
+ auto [LoggerName, EventName] = TraceType->get_name();
+ uint32_t Uid = TraceType->get_uid();
+
+ TypeSchema& Schema = Schemas[Uid];
+ Schema.FullName = fmt::format("{}.{}", std::string_view(LoggerName), std::string_view(EventName));
+ Schema.Uid = Uid;
+ Schema.FieldCount = TraceType->get_field_count();
+ Schema.Flags = 0;
+ if (TraceType->has_flag(TYPE_FLAG_IMPORTANT))
+ {
+ Schema.Flags |= TYPE_FLAG_IMPORTANT;
+ }
+ if (TraceType->has_flag(TYPE_FLAG_AUX))
+ {
+ Schema.Flags |= TYPE_FLAG_AUX;
+ }
+
+ for (uint32_t I = 0; I < Schema.FieldCount; I++)
+ {
+ auto [FieldName, Field] = TraceType->get_field_info(I);
+ Schema.FieldNames.emplace_back(FieldName);
+ Schema.FieldSizes.push_back(Field.get_size());
+ Schema.FieldTypeInfos.push_back(Field.get_type_info());
+ }
+ }
+
+ for (const ::Event& Ev : Parcel.events)
+ {
+ auto It = Schemas.find(Ev.uid);
+ if (It != Schemas.end())
+ {
+ It->second.EventCount++;
+ }
+ }
+ });
+
+ // -- Session info --
+ PrintSessionInfo(SessionAn);
+
+ ZEN_CONSOLE("Trace: {}", FilePath);
+ ZEN_CONSOLE("Size: {}", zen::NiceBytes(uint64_t(std::filesystem::file_size(FilePath))));
+ ZEN_CONSOLE("Events: {}", zen::ThousandsNum(Summary.TotalEvents));
+ ZEN_CONSOLE("Threads: {}", Summary.Threads.size());
+ ZEN_CONSOLE("Types: {}", Schemas.size());
+
+ // -- Thread names --
+ PrintThreadInfo(SessionAn);
+
+ // -- Trace channels --
+ PrintChannelInfo(SessionAn);
+
+ // -- Event schemas --
+ ZEN_CONSOLE("");
+ ZEN_CONSOLE("Event Schemas:");
+ ZEN_CONSOLE("");
+
+ eastl::vector<const TypeSchema*> SortedSchemas;
+ SortedSchemas.reserve(Schemas.size());
+ for (const auto& [Uid, Schema] : Schemas)
+ {
+ SortedSchemas.push_back(&Schema);
+ }
+ eastl::sort(SortedSchemas.begin(), SortedSchemas.end(), [](const auto* A, const auto* B) { return A->FullName < B->FullName; });
+
+ auto FieldTypeStr = [](uint32_t TypeInfo, uint32_t Size) -> std::string_view {
+ uint32_t Cat = TypeInfo & TYPE_INFO_CAT_MASK;
+ if (Cat == TYPE_INFO_CAT_ARRAY)
+ {
+ return "array";
+ }
+ if (Cat == TYPE_INFO_CAT_FLOAT)
+ {
+ return (Size == 8) ? "float64" : "float32";
+ }
+ bool IsSigned = (TypeInfo & TYPE_INFO_SPECIAL_MASK) == TYPE_INFO_SPECIAL_SIGNED;
+ switch (Size)
+ {
+ case 1:
+ return IsSigned ? "int8" : "uint8";
+ case 2:
+ return IsSigned ? "int16" : "uint16";
+ case 4:
+ return IsSigned ? "int32" : "uint32";
+ case 8:
+ return IsSigned ? "int64" : "uint64";
+ default:
+ return "unknown";
+ }
+ };
+
+ for (const TypeSchema* Schema : SortedSchemas)
+ {
+ std::string Flags;
+ if (Schema->Flags & TYPE_FLAG_IMPORTANT)
+ {
+ Flags += " [important]";
+ }
+ if (Schema->Flags & TYPE_FLAG_AUX)
+ {
+ Flags += " [aux]";
+ }
+
+ ZEN_CONSOLE("{} (uid={}, events={}){}", Schema->FullName, Schema->Uid, zen::ThousandsNum(Schema->EventCount), Flags);
+
+ for (uint32_t I = 0; I < Schema->FieldCount; I++)
+ {
+ ZEN_CONSOLE(" {} {}", FieldTypeStr(Schema->FieldTypeInfos[I], Schema->FieldSizes[I]), Schema->FieldNames[I]);
+ }
+
+ if (Schema->FieldCount > 0)
+ {
+ ZEN_CONSOLE("");
+ }
+ }
+}
+
+// Build a single LOD level by merging Lod0 scopes below the given resolution.
+// Lod0 must already be sorted by BeginUs. Safe to call concurrently for
+// different (Level, Resolution) pairs sharing the same Lod0.
+static void
+BuildSingleLod(const eastl::vector<TimelineScope>& Lod0, TimelineDetailLevel& Level, uint32_t Resolution)
+{
+ Level.ResolutionUs = Resolution;
+
+ // Per-depth merge accumulators. Since depths are typically small (< 64),
+ // a flat array indexed by depth is more cache-friendly than a hash map.
+ struct PendingMerge
+ {
+ uint32_t BeginUs = 0;
+ uint32_t EndUs = 0;
+ uint32_t NameId = 0;
+ uint32_t MaxChildDur = 0;
+ uint16_t Depth = 0;
+ uint16_t Count = 0;
+ bool Active = false;
+ };
+
+ eastl::vector<PendingMerge> Pending(64); // grows if needed
+
+ auto FlushPending = [&Level](PendingMerge& P) {
+ if (!P.Active)
+ {
+ return;
+ }
+ Level.Scopes.push_back(TimelineScope{
+ .BeginUs = P.BeginUs,
+ .DurationUs = P.EndUs - P.BeginUs,
+ .NameId = P.NameId,
+ .Depth = P.Depth,
+ .MergeCount = P.Count,
+ });
+ P.Active = false;
+ };
+
+ // Single O(n) sweep over LOD 0 scopes (sorted by BeginUs). For each
+ // depth, merge adjacent small scopes that fall within one resolution
+ // bucket of each other. Large scopes (>= Resolution) pass through.
+ for (const TimelineScope& Scope : Lod0)
+ {
+ uint16_t Depth = Scope.Depth;
+ if (Depth >= Pending.size())
+ {
+ Pending.resize(Depth + 1);
+ }
+
+ if (Scope.DurationUs >= Resolution)
+ {
+ // Large scope -- flush any pending merge for this depth,
+ // then emit the scope un-merged.
+ FlushPending(Pending[Depth]);
+ Level.Scopes.push_back(TimelineScope{
+ .BeginUs = Scope.BeginUs,
+ .DurationUs = Scope.DurationUs,
+ .NameId = Scope.NameId,
+ .Depth = Scope.Depth,
+ .MergeCount = 1,
+ });
+ continue;
+ }
+
+ PendingMerge& P = Pending[Depth];
+ uint32_t EndUs = Scope.BeginUs + Scope.DurationUs;
+
+ if (P.Active && Scope.BeginUs < P.EndUs + Resolution)
+ {
+ // Extend the pending merge.
+ if (EndUs > P.EndUs)
+ {
+ P.EndUs = EndUs;
+ }
+ ++P.Count;
+ if (Scope.DurationUs > P.MaxChildDur)
+ {
+ P.MaxChildDur = Scope.DurationUs;
+ P.NameId = Scope.NameId;
+ }
+ }
+ else
+ {
+ // Start a new pending merge (flush previous if any).
+ FlushPending(P);
+ P.BeginUs = Scope.BeginUs;
+ P.EndUs = EndUs;
+ P.NameId = Scope.NameId;
+ P.MaxChildDur = Scope.DurationUs;
+ P.Depth = Scope.Depth;
+ P.Count = 1;
+ P.Active = true;
+ }
+ }
+
+ // Flush remaining per-depth accumulators.
+ for (PendingMerge& P : Pending)
+ {
+ FlushPending(P);
+ }
+
+ // Sort by (BeginUs, Depth) -- the per-depth flush may have interleaved
+ // entries from different depths. Tie-breaking on depth keeps the
+ // ordering consistent with LOD 0 (parents before nested children) so
+ // the front-end never sees a child rendered before its parent.
+ eastl::sort(Level.Scopes.begin(), Level.Scopes.end(), [](const TimelineScope& A, const TimelineScope& B) {
+ if (A.BeginUs != B.BeginUs)
+ {
+ return A.BeginUs < B.BeginUs;
+ }
+ return A.Depth < B.Depth;
+ });
+}
+
+void
+BuildTimelineLods(ThreadTimeline& Timeline)
+{
+ if (Timeline.Scopes.empty())
+ {
+ return;
+ }
+
+ for (size_t LodIdx = 0; LodIdx < kTimelineLodCount; ++LodIdx)
+ {
+ BuildSingleLod(Timeline.Scopes, Timeline.DetailLevels[LodIdx], kTimelineLodResolutions[LodIdx]);
+ }
+}
+
+namespace {
+
+ // Post-iteration phases, extracted from BuildTraceModel for clarity. Each one
+ // runs after the event-iteration pass has populated the analyzers and mutates
+ // only the pieces of TraceModel it owns.
+
+ void ComputeScopeStats(const TimelineAnalyzer& TimelineAn, TraceModel& Model)
+ {
+ const eastl::vector<std::string>& ScopeNames = TimelineAn.ScopeNames();
+ eastl::vector<Distribution> Dists(ScopeNames.size());
+ eastl::vector<uint32_t> Mins(ScopeNames.size(), ~0u);
+ eastl::vector<uint32_t> Maxs(ScopeNames.size(), 0u);
+
+ for (const auto& [Tid, Thread] : TimelineAn.Threads())
+ {
+ for (const TimelineScope& Scope : Thread.Scopes)
+ {
+ if (Scope.NameId >= Dists.size())
+ {
+ continue;
+ }
+ Dists[Scope.NameId].add(double(Scope.DurationUs));
+ Mins[Scope.NameId] = std::min(Mins[Scope.NameId], Scope.DurationUs);
+ Maxs[Scope.NameId] = std::max(Maxs[Scope.NameId], Scope.DurationUs);
+ }
+ }
+
+ Model.ScopeStats.reserve(ScopeNames.size());
+ for (size_t I = 0; I < ScopeNames.size(); ++I)
+ {
+ if (Dists[I].Count() == 0)
+ {
+ continue;
+ }
+ CpuScopeStat Entry;
+ Entry.Name = ScopeNames[I];
+ Entry.Count = Dists[I].Count();
+ Entry.MinUs = Mins[I];
+ Entry.MaxUs = Maxs[I];
+ Entry.MeanUs = Dists[I].Mean();
+ Entry.StdDevUs = Dists[I].StdDev();
+ Model.ScopeStats.push_back(std::move(Entry));
+ }
+ eastl::sort(Model.ScopeStats.begin(), Model.ScopeStats.end(), [](const CpuScopeStat& A, const CpuScopeStat& B) {
+ return A.Count > B.Count;
+ });
+ }
+
+ // Translate each LogEntry's captured CategoryIndex (a sequential id keyed on
+ // the source category pointer) into the flat LogCategories index the frontend
+ // consumes. Entries whose category pointer never got a matching LogCategory
+ // event are bucketed into a synthetic "(unknown)" category.
+ void ResolveLogCategories(LogAnalyzer& LogAn, TraceModel& Model)
+ {
+ const eastl::hash_map<uint64_t, uint32_t>& CategoryPtrToSeqIdx = LogAn.CategoryPointerIndex();
+
+ eastl::hash_map<uint64_t, uint32_t> RealPtrToFlatIdx;
+ Model.LogCategories = LogAn.BuildCategories(RealPtrToFlatIdx);
+
+ const uint32_t UnknownIdx = uint32_t(Model.LogCategories.size());
+ Model.LogCategories.push_back(LogCategoryInfo{.Name = "(unknown)", .DefaultVerbosity = 0});
+
+ eastl::vector<uint32_t> SeqToFlat(CategoryPtrToSeqIdx.size(), UnknownIdx);
+ for (const auto& [Ptr, SeqIdx] : CategoryPtrToSeqIdx)
+ {
+ auto It = RealPtrToFlatIdx.find(Ptr);
+ if (It != RealPtrToFlatIdx.end())
+ {
+ SeqToFlat[SeqIdx] = It->second;
+ }
+ }
+
+ Model.LogEntries = LogAn.MutableEntries();
+ for (LogEntry& E : Model.LogEntries)
+ {
+ E.CategoryIndex = (E.CategoryIndex < SeqToFlat.size()) ? SeqToFlat[E.CategoryIndex] : UnknownIdx;
+ }
+
+ eastl::sort(Model.LogEntries.begin(), Model.LogEntries.end(), [](const LogEntry& A, const LogEntry& B) {
+ return A.TimeUs < B.TimeUs;
+ });
+ }
+
+ // Finalize any still-open regions, group by category, and greedily pack each
+ // category's regions into non-overlapping lanes so the frontend can stack them
+ // without re-running collision detection.
+ void BuildRegionCategories(eastl::vector<RegionEntry>&& AllRegions, uint32_t TraceEndUs, TraceModel& Model)
+ {
+ for (RegionEntry& R : AllRegions)
+ {
+ if (R.EndUs == ~uint32_t(0))
+ {
+ R.EndUs = TraceEndUs;
+ }
+ if (R.EndUs < R.BeginUs)
+ {
+ R.EndUs = R.BeginUs;
+ }
+ }
+
+ eastl::map<std::string, eastl::vector<RegionEntry>> ByCategory;
+ for (RegionEntry& R : AllRegions)
+ {
+ ByCategory[R.Category].push_back(std::move(R));
+ }
+
+ for (auto& [CatName, Regions] : ByCategory)
+ {
+ eastl::sort(Regions.begin(), Regions.end(), [](const RegionEntry& A, const RegionEntry& B) {
+ if (A.BeginUs != B.BeginUs)
+ {
+ return A.BeginUs < B.BeginUs;
+ }
+ return A.EndUs < B.EndUs;
+ });
+
+ eastl::vector<uint32_t> LaneEndUs;
+ uint32_t MaxLane = 0;
+ for (RegionEntry& R : Regions)
+ {
+ uint16_t Depth = 0;
+ bool Assigned = false;
+ for (size_t I = 0; I < LaneEndUs.size(); ++I)
+ {
+ if (LaneEndUs[I] <= R.BeginUs)
+ {
+ Depth = uint16_t(I);
+ LaneEndUs[I] = R.EndUs;
+ Assigned = true;
+ break;
+ }
+ }
+ if (!Assigned)
+ {
+ Depth = uint16_t(LaneEndUs.size());
+ LaneEndUs.push_back(R.EndUs);
+ }
+ R.Depth = Depth;
+ if (Depth + 1u > MaxLane)
+ {
+ MaxLane = Depth + 1u;
+ }
+ }
+
+ RegionCategory Cat;
+ Cat.Name = CatName;
+ Cat.LaneCount = MaxLane;
+ Cat.Regions = std::move(Regions);
+ Model.RegionCategories.push_back(std::move(Cat));
+ }
+
+ // Sort: uncategorized (empty name) first, then alphabetical.
+ eastl::sort(Model.RegionCategories.begin(), Model.RegionCategories.end(), [](const RegionCategory& A, const RegionCategory& B) {
+ if (A.Name.empty() != B.Name.empty())
+ {
+ return A.Name.empty();
+ }
+ return A.Name < B.Name;
+ });
+ }
+
+ // Map callstack frame addresses to (module, offset) pairs using a sorted
+ // (Base, End) lookup over the already-populated Model.Modules.
+ void ResolveCallstacks(const ModuleAnalyzer& ModuleAn,
+ const CallstackAnalyzer& CallstackAn,
+ AllocationAnalyzer& AllocAn,
+ TraceModel& Model)
+ {
+ const auto& RawCallstacks = CallstackAn.RawCallstacks();
+
+ struct ModuleLookup
+ {
+ uint64_t Base;
+ uint64_t End;
+ uint32_t ModelIndex;
+ };
+ eastl::vector<ModuleLookup> Lookup;
+ Lookup.reserve(ModuleAn.ModulesByBase.size());
+ for (const auto& [Base, Info] : ModuleAn.ModulesByBase)
+ {
+ for (uint32_t I = 0; I < Model.Modules.size(); ++I)
+ {
+ if (Model.Modules[I].Base == Base)
+ {
+ Lookup.push_back({Base, Base + Info.Size, I});
+ break;
+ }
+ }
+ }
+ eastl::sort(Lookup.begin(), Lookup.end(), [](const ModuleLookup& A, const ModuleLookup& B) { return A.Base < B.Base; });
+
+ auto ResolveFrame = [&Lookup](uint64_t Address) -> ResolvedFrame {
+ ResolvedFrame F;
+ F.Address = Address;
+ auto It = eastl::upper_bound(Lookup.begin(), Lookup.end(), Address, [](uint64_t Addr, const ModuleLookup& M) {
+ return Addr < M.Base;
+ });
+ if (It != Lookup.begin())
+ {
+ --It;
+ if (Address < It->End)
+ {
+ F.ModuleIndex = It->ModelIndex;
+ F.Offset = Address - It->Base;
+ }
+ }
+ return F;
+ };
+
+ eastl::vector<uint32_t> SortedCallstackIds;
+ SortedCallstackIds.reserve(RawCallstacks.size());
+ for (const auto& [Id, RawFrames] : RawCallstacks)
+ {
+ ZEN_UNUSED(RawFrames);
+ SortedCallstackIds.push_back(Id);
+ }
+ eastl::sort(SortedCallstackIds.begin(), SortedCallstackIds.end());
+
+ Model.Callstacks.reserve(RawCallstacks.size());
+ for (uint32_t Id : SortedCallstackIds)
+ {
+ auto RawIt = RawCallstacks.find(Id);
+ ZEN_ASSERT(RawIt != RawCallstacks.end());
+ const eastl::vector<uint64_t>& RawFrames = RawIt->second;
+
+ CallstackEntry Entry;
+ Entry.Id = Id;
+ Entry.Frames.reserve(RawFrames.size());
+ for (uint64_t Addr : RawFrames)
+ {
+ Entry.Frames.push_back(ResolveFrame(Addr));
+ }
+ Model.Callstacks.push_back(std::move(Entry));
+ }
+
+ Model.CallstackStats = AllocAn.BuildCallstackStats();
+ Model.ChurnStats = AllocAn.BuildChurnStats(~uint64_t(0));
+ Model.AllocSizeHistogram = AllocAn.BuildSizeHistogram();
+ }
+
+} // namespace
+
+TraceModel
+BuildTraceModel(const std::filesystem::path& FilePath, WorkerThreadPool& ThreadPool, const ProgressCallback& OnProgress)
+{
+ ::DataSource Source(FilePath);
+
+ TraceTiming Timing;
+
+ SessionAnalyzer SessionAn;
+ ModuleAnalyzer ModuleAn;
+ MetadataRegistry MetadataReg;
+ TimelineAnalyzer TimelineAn(&MetadataReg, &Timing);
+ LogAnalyzer LogAn(&Timing);
+ BookmarksAnalyzer BookmarkAn(&Timing);
+ CsvProfilerAnalyzer CsvAn(&Timing);
+ AllocationAnalyzer AllocAn(&Timing);
+ CallstackAnalyzer CallstackAn;
+
+ // Tourist's Dispatcher only supports one subscription per event type, so we
+ // cannot run CpuAnalyzer alongside TimelineAnalyzer -- CpuAnalyzer would
+ // claim the CpuProfiler.Event* types first and TimelineAnalyzer would
+ // never receive any events. Instead, TimelineAnalyzer captures every
+ // scope interval and we derive the aggregate statistics from those
+ // intervals in a cheap post-pass below.
+ ::Dispatcher Dispatch;
+ Dispatch.add_analyzer(SessionAn);
+ Dispatch.add_analyzer(ModuleAn);
+ Dispatch.add_analyzer(MetadataReg);
+ Dispatch.add_analyzer(TimelineAn);
+ Dispatch.add_analyzer(LogAn);
+ Dispatch.add_analyzer(BookmarkAn);
+ Dispatch.add_analyzer(CsvAn);
+ Dispatch.add_analyzer(AllocAn);
+ Dispatch.add_analyzer(CallstackAn);
+
+ zen::Stopwatch Timer;
+ TraceSummary Summary = IterateTrace(
+ Source,
+ [&](const ::EventParcel& Parcel) { Dispatch.on_parcel(Parcel); },
+ OnProgress);
+ ZEN_INFO("Trace iteration complete: {} events in {}",
+ zen::ThousandsNum(Summary.TotalEvents),
+ zen::NiceTimeSpanMs(Timer.GetElapsedTimeMs()));
+
+ {
+ uint32_t StartUs = (TimelineAn.MinBeginUs() == ~0u) ? 0u : TimelineAn.MinBeginUs();
+ uint32_t EndUs = TimelineAn.MaxEndUs();
+ uint64_t DurationMs = (EndUs > StartUs) ? (uint64_t(EndUs - StartUs) + 500) / 1000 : 0;
+ ZEN_INFO("Trace duration: {}", zen::NiceTimeSpanMs(DurationMs));
+ }
+
+ TraceModel Model;
+ Model.FilePath = FilePath;
+ Model.FileSize = uint64_t(std::filesystem::file_size(FilePath));
+ Model.TotalEvents = Summary.TotalEvents;
+ Model.ParseTimeMs = Timer.GetElapsedTimeMs();
+ Model.Session = SessionAn.Session;
+
+ // Event type counts (sorted by count descending)
+ Model.EventTypeCounts.reserve(Summary.TypeInfo.size());
+ for (auto& [Uid, Info] : Summary.TypeInfo)
+ {
+ Model.EventTypeCounts.push_back({std::move(Info.first), Info.second});
+ }
+ eastl::sort(Model.EventTypeCounts.begin(), Model.EventTypeCounts.end(), [](const auto& A, const auto& B) { return A.Count > B.Count; });
+
+ // Flatten and sort threads by sort hint
+ Model.Threads.reserve(SessionAn.ThreadNames.size());
+ for (const auto& [Tid, Info] : SessionAn.ThreadNames)
+ {
+ Model.Threads.push_back(Info);
+ }
+ eastl::sort(Model.Threads.begin(), Model.Threads.end(), [](const ThreadInfoEntry& A, const ThreadInfoEntry& B) {
+ return A.SortHint < B.SortHint;
+ });
+
+ // Flatten and sort channels by name
+ Model.Channels.reserve(SessionAn.Channels.size());
+ for (const auto& [Id, Info] : SessionAn.Channels)
+ {
+ Model.Channels.push_back(Info);
+ }
+ eastl::sort(Model.Channels.begin(), Model.Channels.end(), [](const ChannelInfo& A, const ChannelInfo& B) { return A.Name < B.Name; });
+
+ {
+ ExtendableStringBuilder<512> Enabled;
+ for (const ChannelInfo& Ch : Model.Channels)
+ {
+ if (Ch.Enabled)
+ {
+ if (Enabled.Size() > 0)
+ {
+ Enabled.Append(", ");
+ }
+ Enabled.Append(Ch.Name);
+ }
+ }
+ if (Enabled.Size() > 0)
+ {
+ ZEN_INFO("Enabled channels: {}", Enabled);
+ }
+ }
+
+ // Flatten and sort modules by name
+ Model.Modules.reserve(ModuleAn.ModulesByBase.size());
+ for (const auto& [Base, Info] : ModuleAn.ModulesByBase)
+ {
+ Model.Modules.push_back(Info);
+ }
+ eastl::sort(Model.Modules.begin(), Model.Modules.end(), [](const ModuleInfo& A, const ModuleInfo& B) { return A.Name < B.Name; });
+
+ // CPU scope statistics and timeline building read from TimelineAn
+ // independently and write to separate Model fields, so overlap them.
+ Model.ScopeNames = TimelineAn.ScopeNames();
+
+ ZEN_INFO("Computing CPU scope statistics ({} scope names)", TimelineAn.ScopeNames().size());
+
+ // Kick off scope stats on a worker -- runs concurrently with the
+ // timeline copy + sort below.
+ Latch StatsLatch(1);
+ ThreadPool.ScheduleWork(
+ [&StatsLatch, &Model, &TimelineAn]() {
+ auto _ = MakeGuard([&StatsLatch]() { StatsLatch.CountDown(); });
+ ComputeScopeStats(TimelineAn, Model);
+ },
+ WorkerThreadPool::EMode::EnableBacklog);
+
+ // Timelines -- build per-thread sort + LODs in parallel.
+ {
+ const auto& Threads = TimelineAn.Threads();
+ size_t TotalScopes = 0;
+ for (const auto& [Tid, Thread] : Threads)
+ {
+ TotalScopes += Thread.Scopes.size();
+ }
+ ZEN_INFO("Building timelines: {} threads, {} scopes (sort + LODs)", Threads.size(), zen::ThousandsNum(TotalScopes));
+ Model.Timelines.resize(Threads.size());
+
+ // Populate timeline metadata on the main thread (cheap lookups).
+ size_t Idx = 0;
+ for (const auto& [Tid, Thread] : Threads)
+ {
+ ThreadTimeline& Timeline = Model.Timelines[Idx++];
+ Timeline.ThreadId = Tid;
+ auto It = SessionAn.ThreadNames.find(Tid);
+ if (It != SessionAn.ThreadNames.end())
+ {
+ Timeline.Name = It->second.Name;
+ Timeline.SortHint = It->second.SortHint;
+ }
+ Timeline.Scopes = Thread.Scopes;
+ }
+
+ // Phase 1: Sort LOD 0 scopes per thread.
+ // ParallelSort fans out internally using the pool, so it must be
+ // called from the main thread to avoid nested fan-out deadlocks.
+ // Small timelines are dispatched to workers first (they just call
+ // eastl::sort -- no nesting). Then large ones are sorted one at a
+ // time from the main thread with full pool utilisation each.
+ //
+ // Tie-break on Depth so that scopes which start at the same micro
+ // timestamp come out parent-first (lower depth wins). This keeps
+ // the scope ordering well-defined and lets the front-end rely on
+ // outer scopes appearing before their nested children regardless
+ // of the order the analyzer happened to emit them.
+ {
+ auto Cmp = [](const TimelineScope& A, const TimelineScope& B) {
+ if (A.BeginUs != B.BeginUs)
+ {
+ return A.BeginUs < B.BeginUs;
+ }
+ return A.Depth < B.Depth;
+ };
+
+ if constexpr (kUseParallelSort)
+ {
+ constexpr size_t kParallelThreshold = 65536;
+
+ // Dispatch small timelines to workers.
+ Latch SmallLatch(1);
+ for (size_t I = 0; I < Model.Timelines.size(); ++I)
+ {
+ if (Model.Timelines[I].Scopes.size() >= kParallelThreshold)
+ {
+ continue;
+ }
+ SmallLatch.AddCount(1);
+ ThreadPool.ScheduleWork(
+ [&SmallLatch, &Cmp, &Timeline = Model.Timelines[I]]() {
+ auto _ = MakeGuard([&SmallLatch]() { SmallLatch.CountDown(); });
+ eastl::sort(Timeline.Scopes.begin(), Timeline.Scopes.end(), Cmp);
+ },
+ WorkerThreadPool::EMode::EnableBacklog);
+ }
+ SmallLatch.CountDown();
+ SmallLatch.Wait();
+
+ // Sort large timelines from the main thread so ParallelSort
+ // can fan out across the (now idle) pool without deadlocking.
+ for (ThreadTimeline& Timeline : Model.Timelines)
+ {
+ if (Timeline.Scopes.size() >= kParallelThreshold)
+ {
+ zen::ParallelSort(ThreadPool, Timeline.Scopes.begin(), Timeline.Scopes.end(), Cmp);
+ }
+ }
+ }
+ else
+ {
+ Latch SortLatch(1);
+ for (size_t I = 0; I < Model.Timelines.size(); ++I)
+ {
+ SortLatch.AddCount(1);
+ ThreadPool.ScheduleWork(
+ [&SortLatch, &Cmp, &Timeline = Model.Timelines[I]]() {
+ auto _ = MakeGuard([&SortLatch]() { SortLatch.CountDown(); });
+ eastl::sort(Timeline.Scopes.begin(), Timeline.Scopes.end(), Cmp);
+ },
+ WorkerThreadPool::EMode::EnableBacklog);
+ }
+ SortLatch.CountDown();
+ SortLatch.Wait();
+ }
+ }
+
+ // Phase 2: Build LOD levels -- one task per (thread, LOD) pair.
+ // Flat dispatch avoids nested fan-out which could deadlock the pool.
+ Latch LodLatch(1);
+ for (size_t I = 0; I < Model.Timelines.size(); ++I)
+ {
+ if (Model.Timelines[I].Scopes.empty())
+ {
+ continue;
+ }
+ for (size_t L = 0; L < kTimelineLodCount; ++L)
+ {
+ LodLatch.AddCount(1);
+ ThreadPool.ScheduleWork(
+ [&LodLatch, &Timeline = Model.Timelines[I], L]() {
+ auto _ = MakeGuard([&LodLatch]() { LodLatch.CountDown(); });
+ BuildSingleLod(Timeline.Scopes, Timeline.DetailLevels[L], kTimelineLodResolutions[L]);
+ },
+ WorkerThreadPool::EMode::EnableBacklog);
+ }
+ }
+ LodLatch.CountDown();
+ LodLatch.Wait();
+ }
+ eastl::sort(Model.Timelines.begin(), Model.Timelines.end(), [](const ThreadTimeline& A, const ThreadTimeline& B) {
+ return A.SortHint < B.SortHint;
+ });
+
+ Model.TraceStartUs = (TimelineAn.MinBeginUs() == ~0u) ? 0u : TimelineAn.MinBeginUs();
+ Model.TraceEndUs = TimelineAn.MaxEndUs();
+
+ // Ensure scope stats computation (kicked off earlier) has finished.
+ StatsLatch.Wait();
+
+ ZEN_INFO("Processing {} log entries", zen::ThousandsNum(LogAn.Entries().size()));
+ ResolveLogCategories(LogAn, Model);
+
+ ZEN_INFO("Sorting {} bookmarks, {} regions", BookmarkAn.MutableBookmarks().size(), BookmarkAn.MutableRegions().size());
+
+ // Bookmarks: move and sort by TimeUs.
+ Model.Bookmarks = std::move(BookmarkAn.MutableBookmarks());
+ eastl::sort(Model.Bookmarks.begin(), Model.Bookmarks.end(), [](const Bookmark& A, const Bookmark& B) { return A.TimeUs < B.TimeUs; });
+
+ BuildRegionCategories(std::move(BookmarkAn.MutableRegions()), Model.TraceEndUs, Model);
+
+ // CsvProfiler data
+ {
+ Model.CsvCategories = std::move(CsvAn.MutableCategories());
+ Model.CsvStatDefs = std::move(CsvAn.MutableStatDefs());
+ Model.CsvTimeSeries = CsvAn.BuildTimeSeries();
+ Model.CsvEvents = std::move(CsvAn.MutableEvents());
+ eastl::sort(Model.CsvEvents.begin(), Model.CsvEvents.end(), [](const auto& A, const auto& B) { return A.TimeUs < B.TimeUs; });
+ Model.CsvMetadata = std::move(CsvAn.MutableMetadata());
+ ZEN_INFO("CSV profiler: {} categories, {} stats, {} series, {} events",
+ Model.CsvCategories.size(),
+ Model.CsvStatDefs.size(),
+ Model.CsvTimeSeries.size(),
+ Model.CsvEvents.size());
+ }
+
+ // Memory allocation data
+ {
+ AllocAn.EmitFinalSample(Model.TraceEndUs);
+ Model.AllocSummary = AllocAn.Summary();
+
+ // Flatten heaps map into sorted vector
+ Model.Heaps.reserve(AllocAn.Heaps().size());
+ for (const auto& [Id, Info] : AllocAn.Heaps())
+ {
+ Model.Heaps.push_back(Info);
+ }
+ eastl::sort(Model.Heaps.begin(), Model.Heaps.end(), [](const HeapInfo& A, const HeapInfo& B) { return A.Id < B.Id; });
+
+ // Flatten tags map into sorted vector
+ Model.Tags.reserve(AllocAn.Tags().size());
+ for (const auto& [Tag, Info] : AllocAn.Tags())
+ {
+ Model.Tags.push_back(Info);
+ }
+ eastl::sort(Model.Tags.begin(), Model.Tags.end(), [](const TagInfo& A, const TagInfo& B) { return A.Tag < B.Tag; });
+
+ // Move timeline (already time-ordered from Marker events)
+ Model.MemoryTimeline = std::move(AllocAn.MutableTimeline());
+
+ // Flatten per-root-heap stats into sorted vector
+ Model.HeapStats.reserve(AllocAn.RootHeapStats().size());
+ for (const auto& [HeapId, Stat] : AllocAn.RootHeapStats())
+ {
+ Model.HeapStats.push_back(Stat);
+ }
+ eastl::sort(Model.HeapStats.begin(), Model.HeapStats.end(), [](const HeapStat& A, const HeapStat& B) {
+ return A.HeapId < B.HeapId;
+ });
+
+ if (Model.AllocSummary.HasMemoryData)
+ {
+ ZEN_INFO("Memory: {} allocs, {} frees, peak {}, {} live, {} timeline samples",
+ zen::ThousandsNum(Model.AllocSummary.TotalAllocs + Model.AllocSummary.TotalReallocAllocs),
+ zen::ThousandsNum(Model.AllocSummary.TotalFrees + Model.AllocSummary.TotalReallocFrees),
+ zen::NiceBytes(uint64_t(Model.AllocSummary.PeakBytes)),
+ zen::ThousandsNum(Model.AllocSummary.LiveAllocations),
+ zen::ThousandsNum(Model.MemoryTimeline.size()));
+ }
+ }
+
+ ResolveCallstacks(ModuleAn, CallstackAn, AllocAn, Model);
+ ZEN_INFO("Callstacks: {} unique, {} with live allocations",
+ zen::ThousandsNum(Model.Callstacks.size()),
+ zen::ThousandsNum(Model.CallstackStats.size()));
+
+ return Model;
+}
+
+//////////////////////////////////////////////////////////////////////////////
+// Trace trim
+//
+// The trim pipeline operates entirely at the raw packet level: a .utrace on
+// disk is identical to the wire format (see src/zenserver/trace/tracerecorder.cpp
+// for the capture-side passthrough), so trimming reduces to "copy the preamble,
+// then copy only the packets we want to keep". We never re-encode or re-emit
+// any events, which sidesteps the fact that Tourist has no writer path.
+//
+// The algorithm:
+//
+// 1. Slurp the input file into memory and walk raw packets using the
+// [size:uint16][thread_id:uint16][payload] framing. This gives an ordered
+// list of packet descriptors keyed by file offset.
+//
+// 2. Classify packets by their on-disk thread_id:
+// TID_TYPE -> always keep (type definitions)
+// TID_IMPORTANT -> always keep (events of types marked TYPE_FLAG_IMPORTANT,
+// i.e. session info, thread names, channel state,
+// log categories, CPU specs, etc.)
+// TID_SYNC -> always keep (transport barriers)
+// TID_NORMAL+ -> keep only if the packet's events overlap the window
+//
+// 3. For normal-thread packets, run Tourist's reader with a bundle of size 1
+// so each Proto.read() scatters exactly one raw packet before emitting any
+// events. Before the read call, we record the file offset of the current
+// packet as the "latest packet" for its thread. TrimAnalyzer then decodes
+// CpuProfiler batch events and attributes their timestamp ranges back to
+// that thread's latest packet. The attribution can drift if Tourist
+// buffers multiple packets on one thread, but the failure mode is that
+// earlier packets lose attribution and are conservatively retained.
+//
+// 4. Write the output: the preamble bytes verbatim, followed by the raw
+// bytes of each kept packet in original order. There is no trailer; the
+// Tourist reader catches DataStream::Eof at the end of the stream.
+//
+// Coarse per-packet precision is accepted by design: a packet straddling a
+// window edge is kept in full. CpuProfiler batches are self-contained per
+// packet (each re-derives cycles from the trace-wide StartCycle), so dropping
+// packets does not desync delta decoding on surviving ones, and orphaned leave
+// events from half-open scopes are silently ignored by decoders.
+
+namespace {
+
+ struct TrimPacketDesc
+ {
+ uint64_t FileOffset = 0; // offset of the [size:uint16] header in the file
+ uint32_t Size = 0; // total size including the 4-byte header
+ uint16_t ThreadIdRaw = 0; // thread_id as stored on disk, including PACKET_FLAG_COMPRESSED
+ };
+
+ // Parses the .utrace preamble in place to determine the byte offset where
+ // packets begin. Mirrors Preamble::parse_header in Tourist so we can run the
+ // raw walker without spinning up a second DataSource. Throws on a malformed
+ // preamble.
+ static uint64_t ParsePreambleLength(const uint8_t* Data, uint64_t Size)
+ {
+ if (Size < 8)
+ {
+ throw zen::runtime_error("Trace file too small to contain a preamble ({} bytes)", Size);
+ }
+
+ uint32_t Magic = 0;
+ std::memcpy(&Magic, Data, sizeof(uint32_t));
+ if (Magic != 'TRC2')
+ {
+ throw zen::runtime_error("Unexpected trace file magic value 0x{:08x}", Magic);
+ }
+
+ uint16_t MetaSize = 0;
+ std::memcpy(&MetaSize, Data + 4, sizeof(uint16_t));
+
+ // magic(4) + meta_size(2) + metadata + transport(1) + protocol(1)
+ uint64_t PreambleLen = uint64_t(4) + 2 + MetaSize + 1 + 1;
+ if (PreambleLen > Size)
+ {
+ throw zen::runtime_error("Trace preamble extends past end of file ({} > {})", PreambleLen, Size);
+ }
+
+ return PreambleLen;
+ }
+
+ // Walks raw packets starting at PreambleLen. Returns one TrimPacketDesc per
+ // packet in original stream order. The walker stops gracefully on truncated
+ // data so partial traces still produce a usable packet list.
+ static eastl::vector<TrimPacketDesc> WalkRawPackets(const uint8_t* Data, uint64_t Size, uint64_t PreambleLen)
+ {
+ eastl::vector<TrimPacketDesc> Packets;
+ uint64_t Offset = PreambleLen;
+
+ while (Offset + 4 <= Size)
+ {
+ uint16_t PacketSize = 0;
+ uint16_t ThreadIdRaw = 0;
+ std::memcpy(&PacketSize, Data + Offset, sizeof(uint16_t));
+ std::memcpy(&ThreadIdRaw, Data + Offset + 2, sizeof(uint16_t));
+
+ if (PacketSize < 4)
+ {
+ // Malformed size; stop walking and accept whatever we have.
+ break;
+ }
+
+ if (Offset + PacketSize > Size)
+ {
+ // Truncated tail -- drop it.
+ break;
+ }
+
+ TrimPacketDesc Desc;
+ Desc.FileOffset = Offset;
+ Desc.Size = PacketSize;
+ Desc.ThreadIdRaw = ThreadIdRaw;
+ Packets.push_back(Desc);
+
+ Offset += PacketSize;
+ }
+
+ return Packets;
+ }
+
+} // namespace
+
+void
+RunTraceTrim(const TraceTrimArgs& Args)
+{
+ if (!(Args.EndSec > Args.StartSec))
+ {
+ throw zen::runtime_error("Invalid trim range: start={} end={}", Args.StartSec, Args.EndSec);
+ }
+
+ // --- Read the input file ---
+ zen::BasicFile InputFile(Args.InputPath, zen::BasicFile::Mode::kRead);
+ zen::IoBuffer InputBuffer = InputFile.ReadAll();
+ InputFile.Close();
+
+ const uint8_t* FileBytes = static_cast<const uint8_t*>(InputBuffer.GetData());
+ const uint64_t FileSize = InputBuffer.GetSize();
+
+ const uint64_t PreambleLen = ParsePreambleLength(FileBytes, FileSize);
+
+ // --- Raw packet walk ---
+ eastl::vector<TrimPacketDesc> Packets = WalkRawPackets(FileBytes, FileSize, PreambleLen);
+ if (Packets.empty())
+ {
+ throw zen::runtime_error("Trace file contains no packets");
+ }
+
+ // Initial keep classification: definitions, important events, sync are
+ // always retained. Normal-thread packets start as drop candidates and get
+ // promoted if their decoded time range overlaps the window.
+ eastl::vector<uint8_t> Keep(Packets.size(), 0);
+ size_t NumAlwaysKept = 0;
+ for (size_t I = 0; I < Packets.size(); ++I)
+ {
+ uint32_t Tid = Packets[I].ThreadIdRaw & ~PACKET_FLAG_COMPRESSED;
+ if (Tid == TID_TYPE || Tid == TID_IMPORTANT || Tid == TID_SYNC)
+ {
+ Keep[I] = 1;
+ ++NumAlwaysKept;
+ }
+ }
+
+ // --- Time-range classification via Tourist (bundle of 1) ---
+ TrimAnalyzer TrimAn;
+ TrimAn.EndUs = (Args.EndSec * 1e6 > double(~uint32_t(0))) ? ~uint32_t(0) : uint32_t(Args.EndSec * 1e6);
+ ::Dispatcher Dispatch;
+ Dispatch.add_analyzer(TrimAn);
+
+ {
+ ::DataSource Source(Args.InputPath);
+ ::Allocator TraceAllocator;
+ ::Preamble Pream(Source, TraceAllocator);
+ ::Transport Xport = Pream.get_transport();
+ ::Protocol Proto = Pream.get_protocol();
+
+ ::Packet OnePacket[1];
+ ::EventParcel Parcel;
+
+ try
+ {
+ while (::Bundle Bndl = Xport.read_packets(OnePacket))
+ {
+ if (Bndl.empty())
+ {
+ break;
+ }
+
+ const ::Packet& P = Bndl[0];
+ uint32_t Tid = P.get_thread_id();
+
+ if (Tid >= TID_NORMAL && Tid != TID_SYNC)
+ {
+ // Tourist's Packet::get_index() is the same sequential
+ // packet counter as our raw walker's vector position,
+ // since both read the stream from the start in order.
+ TrimAn.LastPacketIndexByThread[Tid] = P.get_index();
+ }
+
+ Parcel.reset();
+ Proto.read(Parcel, Bndl);
+ Dispatch.on_parcel(Parcel);
+ }
+ }
+ catch (const DataStream::Eof&)
+ {
+ }
+ catch (const Exception::StreamError& E)
+ {
+ throw zen::runtime_error("Trace stream error at position {}: {} (value: {})", E.position, E.message, E.value);
+ }
+ }
+
+ // --- Apply the window filter ---
+ //
+ // Per-packet filtering in the middle of a thread's stream is unsafe:
+ // Tourist's event parser holds per-thread continuation state (see
+ // EventParser::_fragment / _missing in
+ // thirdparty/tourist/trace/src/protocol.cpp) so an event can straddle a
+ // packet boundary on a normal thread. Removing a packet from the middle
+ // leaves subsequent packets on the same thread decoded against the wrong
+ // position in an in-flight event and Tourist crashes. We therefore only
+ // drop packets in two safe ways:
+ //
+ // 1. Whole-thread drop: a thread whose attributed packets are all
+ // outside the window has every one of its packets dropped. No
+ // surviving packet references that thread, so there is no state
+ // machine to corrupt.
+ //
+ // 2. Per-thread tail truncation: for a thread that does have in-window
+ // activity, drop every packet AFTER the latest in-window packet on
+ // that thread. Tail drops are safe because no later packet on the
+ // same thread can be looking forward to the dropped bytes; the
+ // parser just ends its stream for that thread at the truncation
+ // point, exactly like a trace that naturally stopped recording.
+ //
+ // Threads for which we never attributed any CpuProfiler batch events are
+ // retained in full; we have no evidence about their time range and
+ // can't safely drop them.
+ const uint32_t StartUs = uint32_t(std::max(0.0, Args.StartSec) * 1e6);
+ const uint32_t EndUs = (Args.EndSec * 1e6 > double(~uint32_t(0))) ? ~uint32_t(0) : uint32_t(Args.EndSec * 1e6);
+
+ struct ThreadInfo
+ {
+ bool HasAnyBatch = false;
+ bool HasInWindowBatch = false;
+ // First packet index on this thread whose attributed CPU batches are
+ // *entirely* past EndUs. Every packet on this thread with an index
+ // >= this value is safe to tail-drop. Defaults to size_t(-1) (no cut
+ // point) when the thread has no such packet.
+ size_t FirstPastWindowIdx = size_t(-1);
+ };
+ eastl::hash_map<uint32_t, ThreadInfo> ThreadInfos;
+
+ for (size_t I = 0; I < Packets.size(); ++I)
+ {
+ uint32_t Tid = Packets[I].ThreadIdRaw & ~PACKET_FLAG_COMPRESSED;
+ if (Tid < TID_NORMAL || Tid == TID_SYNC)
+ {
+ continue;
+ }
+
+ auto RangeIt = TrimAn.PacketRanges.find(uint32_t(I));
+ if (RangeIt == TrimAn.PacketRanges.end())
+ {
+ continue;
+ }
+
+ ThreadInfo& Info = ThreadInfos[Tid];
+ Info.HasAnyBatch = true;
+ const auto& Range = RangeIt->second;
+ if (Range.MaxUs >= StartUs && Range.MinUs <= EndUs)
+ {
+ Info.HasInWindowBatch = true;
+ }
+ if (Range.MinUs > EndUs && I < Info.FirstPastWindowIdx)
+ {
+ Info.FirstPastWindowIdx = I;
+ }
+ }
+
+ size_t NumThreadsKept = 0;
+ size_t NumThreadsDropped = 0;
+ for (const auto& [Tid, Info] : ThreadInfos)
+ {
+ if (Info.HasInWindowBatch)
+ {
+ ++NumThreadsKept;
+ }
+ else
+ {
+ ++NumThreadsDropped;
+ }
+ }
+
+ size_t NumInWindow = 0;
+ size_t NumTailDropped = 0;
+ size_t NumUnattributed = 0;
+ size_t NumDropped = 0;
+
+ for (size_t I = 0; I < Packets.size(); ++I)
+ {
+ if (Keep[I])
+ {
+ continue;
+ }
+
+ uint32_t Tid = Packets[I].ThreadIdRaw & ~PACKET_FLAG_COMPRESSED;
+ auto It = ThreadInfos.find(Tid);
+ if (It == ThreadInfos.end() || !It->second.HasAnyBatch)
+ {
+ // We have no evidence for this thread's time range. Retain all
+ // its packets conservatively to avoid breaking Tourist's per-
+ // thread parser state.
+ Keep[I] = 1;
+ ++NumUnattributed;
+ continue;
+ }
+
+ if (!It->second.HasInWindowBatch)
+ {
+ // Thread's attributed packets are all outside the window -- drop
+ // every packet on this thread.
+ ++NumDropped;
+ continue;
+ }
+
+ if (I >= It->second.FirstPastWindowIdx)
+ {
+ // Past the first entirely-after-window packet on this thread --
+ // candidate for tail truncation. Before dropping, check whether
+ // this packet carries a Leave event that closes a scope whose
+ // Enter was at or before the window end. If so, we MUST keep it
+ // so the downstream analyzer can render the long-running scope;
+ // otherwise the scope would sit unmatched on the open stack.
+ auto MustKeepIt = TrimAn.MustKeepPacketByThread.find(Tid);
+ if (MustKeepIt != TrimAn.MustKeepPacketByThread.end() && I <= MustKeepIt->second)
+ {
+ Keep[I] = 1;
+ ++NumInWindow;
+ continue;
+ }
+
+ ++NumTailDropped;
+ continue;
+ }
+
+ Keep[I] = 1;
+ ++NumInWindow;
+ }
+
+ // --- Write output ---
+ std::error_code Ec;
+ std::filesystem::create_directories(Args.OutputPath.parent_path(), Ec);
+
+ zen::BasicFile OutputFile(Args.OutputPath, zen::BasicFile::Mode::kTruncate);
+
+ uint64_t OutOffset = 0;
+ OutputFile.Write(FileBytes, PreambleLen, OutOffset);
+ OutOffset += PreambleLen;
+
+ uint64_t KeptBytes = 0;
+ for (size_t I = 0; I < Packets.size(); ++I)
+ {
+ if (!Keep[I])
+ {
+ continue;
+ }
+ OutputFile.Write(FileBytes + Packets[I].FileOffset, Packets[I].Size, OutOffset);
+ OutOffset += Packets[I].Size;
+ KeptBytes += Packets[I].Size;
+ }
+
+ OutputFile.Flush();
+ OutputFile.Close();
+
+ ZEN_CONSOLE("Trimmed trace written to {}", Args.OutputPath);
+ ZEN_CONSOLE(" Input: {} ({} packets)", zen::NiceBytes(FileSize), zen::ThousandsNum(Packets.size()));
+ ZEN_CONSOLE(" Output: {} ({} packets)",
+ zen::NiceBytes(OutOffset),
+ zen::ThousandsNum(NumAlwaysKept + NumInWindow + NumUnattributed));
+ ZEN_CONSOLE(" Always kept: {} packets (types / important / sync)", zen::ThousandsNum(NumAlwaysKept));
+ ZEN_CONSOLE(" Thread kept: {} packets from {} threads with in-window activity",
+ zen::ThousandsNum(NumInWindow),
+ zen::ThousandsNum(NumThreadsKept));
+ ZEN_CONSOLE(" Thread dropped: {} packets from {} threads with no in-window activity",
+ zen::ThousandsNum(NumDropped),
+ zen::ThousandsNum(NumThreadsDropped));
+ ZEN_CONSOLE(" Tail dropped: {} packets past the latest in-window packet on their thread", zen::ThousandsNum(NumTailDropped));
+ ZEN_CONSOLE(" Unattributed: {} packets (retained conservatively)", zen::ThousandsNum(NumUnattributed));
+ ZEN_UNUSED(KeptBytes);
+
+ // --- Diagnostic: summarise the attributed time range distribution ---
+ {
+ uint32_t GlobalMin = ~0u;
+ uint32_t GlobalMax = 0;
+ for (const auto& [Idx, R] : TrimAn.PacketRanges)
+ {
+ GlobalMin = std::min(GlobalMin, R.MinUs);
+ GlobalMax = std::max(GlobalMax, R.MaxUs);
+ }
+ ZEN_CONSOLE(" Attributed: {} packets, window {:.3f}s .. {:.3f}s",
+ zen::ThousandsNum(TrimAn.PacketRanges.size()),
+ double(GlobalMin) / 1e6,
+ double(GlobalMax) / 1e6);
+ }
+}
+
+} // namespace zen::trace_detail
diff --git a/src/zen/trace/trace_model.h b/src/zen/trace/trace_model.h
new file mode 100644
index 000000000..bd6dcc674
--- /dev/null
+++ b/src/zen/trace/trace_model.h
@@ -0,0 +1,314 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#pragma once
+
+#include "trace_memory.h"
+#include "zen.h"
+
+#include <zencore/workthreadpool.h>
+
+ZEN_THIRD_PARTY_INCLUDES_START
+#include <EASTL/vector.h>
+ZEN_THIRD_PARTY_INCLUDES_END
+
+#include <cstdint>
+#include <filesystem>
+#include <functional>
+#include <string>
+
+namespace zen::trace_detail {
+
+// Shared trace timing state. Tourist's Dispatcher only allows one subscriber
+// per event type, so only one analyzer can own the `$Trace.NewTrace`
+// subscription. Other analyzers that need to convert absolute Cycle64 values
+// read from this shared struct, which the owning analyzer fills in during its
+// OnNewTrace callback.
+struct TraceTiming
+{
+ uint64_t Freq = 0;
+ uint64_t Base = 0;
+ uint64_t UsDiv = 1;
+
+ uint32_t CycleToTimeUs(uint64_t Cycle) const
+ {
+ uint64_t CycleFromStart = (Cycle >= Base) ? (Cycle - Base) : 0;
+ uint64_t D = (UsDiv > 0) ? UsDiv : 1;
+ return uint32_t((CycleFromStart + (D >> 1)) / D);
+ }
+};
+
+// Safely convert a tourist FieldStr to std::string, stripping trailing NULs
+// and returning an empty string on failure.
+std::string SafeFieldStr(class FieldStr&& Field);
+
+struct SessionInfo
+{
+ std::string Platform;
+ std::string AppName;
+ std::string ProjectName;
+ std::string CommandLine;
+ std::string Branch;
+ std::string BuildVersion;
+ uint32_t Changelist = 0;
+ uint8_t ConfigurationType = 0;
+ bool HasSession = false;
+};
+
+struct ThreadInfoEntry
+{
+ uint32_t ThreadId = 0;
+ std::string Name;
+ std::string GroupName; // from $Trace.ThreadGroupBegin/End bracketing, or synthesized by stripping a numeric suffix from Name
+ uint32_t SystemId = 0;
+ int32_t SortHint = 0;
+};
+
+struct ChannelInfo
+{
+ std::string Name;
+ bool Enabled = false;
+ bool ReadOnly = false;
+};
+
+// A DLL / shared library that was loaded (or seen already loaded) during the
+// capture. Populated from the Diagnostics.Module{Init,Load,Unload} events
+// which are all marked NoSync|Important, so they survive reconnects and our
+// own trim filter. Load/unload timestamps aren't available because the events
+// don't carry a Cycle field.
+struct ModuleInfo
+{
+ std::string Name; // basename of FullPath
+ std::string FullPath; // full path as reported by the engine
+ uint64_t Base = 0;
+ uint32_t Size = 0;
+ bool Unloaded = false; // set when we see a matching ModuleUnload
+ eastl::vector<uint8_t> ImageId; // PDB GUID + Age, opaque -- for later symbol lookup
+};
+
+// UE verbosity values mirror ELogVerbosity::Type. We expose the raw integer
+// so the frontend can map it to a label / color.
+struct LogCategoryInfo
+{
+ std::string Name;
+ uint8_t DefaultVerbosity = 0;
+};
+
+struct LogEntry
+{
+ uint32_t TimeUs; // microseconds from the start of the trace
+ uint32_t CategoryIndex; // index into TraceModel::LogCategories (or ~0u)
+ uint8_t Verbosity;
+ int32_t Line;
+ std::string File;
+ std::string Message;
+};
+
+// Point-in-time marker emitted via TRACE_BOOKMARK / UE_TRACE_BOOKMARK.
+// Each entry's Text has already been formatted (FormatString + FormatArgs
+// substituted) during parsing.
+struct Bookmark
+{
+ uint32_t TimeUs;
+ int32_t Line;
+ std::string File;
+ std::string Text;
+};
+
+// A named time range announced via Misc.RegionBegin / Misc.RegionEnd
+// (or the newer *WithId variants). Depth is the lane index assigned by
+// the analyzer's greedy overlap-avoidance pass.
+struct RegionEntry
+{
+ uint32_t BeginUs;
+ uint32_t EndUs; // == TraceEndUs if still open at trace end
+ uint16_t Depth;
+ uint16_t Reserved;
+ std::string Name;
+ std::string Category;
+};
+
+// A group of regions sharing the same category label. Each category has its
+// own lane namespace so depths are assigned independently.
+struct RegionCategory
+{
+ std::string Name; // display name; empty categories get "Uncategorized"
+ uint32_t LaneCount = 0;
+ eastl::vector<RegionEntry> Regions; // sorted by BeginUs, Depth is per-category
+};
+
+struct CpuScopeStat
+{
+ std::string Name;
+ uint64_t Count = 0;
+ uint32_t MinUs = 0;
+ uint32_t MaxUs = 0;
+ double MeanUs = 0.0;
+ double StdDevUs = 0.0;
+};
+
+// Single CPU scope interval captured by TimelineAnalyzer. Packed for size:
+// timelines can easily contain millions of entries.
+struct TimelineScope
+{
+ uint32_t BeginUs; // microseconds from the start of the trace
+ uint32_t DurationUs; // scope duration in microseconds
+ uint32_t NameId; // index into TraceModel::ScopeNames
+ uint16_t Depth; // call-stack depth (0 == outermost)
+ uint16_t MergeCount; // 0 = raw (LOD 0), N>0 = N scopes merged (LOD 1+)
+};
+
+// Pre-computed detail level for a thread timeline. Each level merges scopes
+// shorter than ResolutionUs into "macro scopes" carrying the dominant name
+// (the name of the longest contributing scope). The merge count is stored in
+// TimelineScope::MergeCount.
+struct TimelineDetailLevel
+{
+ uint32_t ResolutionUs = 0;
+ eastl::vector<TimelineScope> Scopes; // sorted by BeginUs
+};
+
+// LOD resolutions in microseconds (geometric spacing inspired by Unreal Insights).
+// LOD 0 is the raw ThreadTimeline::Scopes; these are LOD 1-5.
+inline constexpr uint32_t kTimelineLodResolutions[] = {100, 1000, 8000, 40000, 200000};
+inline constexpr size_t kTimelineLodCount = sizeof(kTimelineLodResolutions) / sizeof(kTimelineLodResolutions[0]);
+
+struct ThreadTimeline
+{
+ uint32_t ThreadId = 0;
+ std::string Name;
+ int32_t SortHint = 0;
+ eastl::vector<TimelineScope> Scopes; // LOD 0 -- full resolution, sorted by BeginUs
+
+ TimelineDetailLevel DetailLevels[kTimelineLodCount]; // LOD 1-5
+};
+
+// Build pre-computed LOD levels for a ThreadTimeline whose Scopes vector is
+// already sorted by BeginUs. Called from BuildTraceModel after populating the
+// raw scopes.
+void BuildTimelineLods(ThreadTimeline& Timeline);
+
+// Complete in-memory view of a parsed .utrace file, produced by BuildTraceModel
+// and consumed by the `zen trace serve` subcommand.
+struct TraceModel
+{
+ std::filesystem::path FilePath;
+ uint64_t FileSize = 0;
+ uint64_t TotalEvents = 0;
+ uint64_t ParseTimeMs = 0;
+ uint32_t TraceStartUs = 0;
+ uint32_t TraceEndUs = 0;
+
+ SessionInfo Session;
+ eastl::vector<ThreadInfoEntry> Threads; // sorted by SortHint
+ eastl::vector<ChannelInfo> Channels; // sorted by name
+ eastl::vector<ModuleInfo> Modules; // sorted by Name
+
+ eastl::vector<std::string> ScopeNames; // referenced by TimelineScope::NameId
+ eastl::vector<CpuScopeStat> ScopeStats; // sorted by Count descending
+ eastl::vector<ThreadTimeline> Timelines; // one entry per thread that produced scopes
+
+ eastl::vector<LogCategoryInfo> LogCategories; // referenced by LogEntry::CategoryIndex
+ eastl::vector<LogEntry> LogEntries; // sorted by TimeUs
+
+ eastl::vector<Bookmark> Bookmarks; // sorted by TimeUs
+ eastl::vector<RegionCategory> RegionCategories; // sorted: uncategorized first, then alpha
+
+ // -- CsvProfiler --
+ struct CsvCategory
+ {
+ int32_t Index = 0;
+ std::string Name;
+ };
+
+ struct CsvStatDef
+ {
+ uint64_t StatId = 0;
+ int32_t CategoryIndex = 0;
+ std::string Name;
+ };
+
+ struct CsvSample
+ {
+ uint32_t TimeUs;
+ float Value;
+ };
+
+ // Time series for one stat on one thread.
+ struct CsvSeries
+ {
+ uint64_t StatId = 0;
+ uint32_t ThreadId = 0;
+ eastl::vector<CsvSample> Samples; // sorted by TimeUs
+ };
+
+ struct CsvEvent
+ {
+ uint32_t TimeUs;
+ int32_t CategoryIndex;
+ std::string Text;
+ };
+
+ struct CsvMeta
+ {
+ std::string Key;
+ std::string Value;
+ };
+
+ eastl::vector<CsvCategory> CsvCategories;
+ eastl::vector<CsvStatDef> CsvStatDefs;
+ eastl::vector<CsvSeries> CsvTimeSeries; // per stat+thread
+ eastl::vector<CsvEvent> CsvEvents; // sorted by TimeUs
+ eastl::vector<CsvMeta> CsvMetadata;
+
+ // -- Event type counts (sorted by count descending) --
+ struct EventTypeCount
+ {
+ std::string Name;
+ uint64_t Count = 0;
+ };
+ eastl::vector<EventTypeCount> EventTypeCounts;
+
+ // -- Memory allocations --
+ AllocationSummary AllocSummary;
+ eastl::vector<HeapInfo> Heaps; // sorted by Id
+ eastl::vector<TagInfo> Tags; // sorted by Tag
+ eastl::vector<MemoryTimelineSample> MemoryTimeline; // sorted by TimeUs
+ eastl::vector<HeapStat> HeapStats; // sorted by HeapId
+ eastl::vector<CallstackEntry> Callstacks; // sorted by Id
+ eastl::vector<CallstackAllocStat> CallstackStats; // sorted by LiveBytes desc
+ eastl::vector<CallstackChurnStat> ChurnStats; // sorted by TotalAllocs desc
+ eastl::vector<AllocSizeBucket> AllocSizeHistogram; // sorted by MinSize asc, populated buckets only
+};
+
+// Resolve and validate a .utrace file path. Throws OptionParseException when
+// the path is empty and runtime_error when the file does not exist.
+std::filesystem::path ResolveTraceFile(const std::filesystem::path& Input, cxxopts::Options& HelpOptions);
+
+// Parse a .utrace file and print the event-schema inspect report to the console.
+void RunInspect(const std::filesystem::path& FilePath);
+
+// Progress callback invoked once per bundle during trace iteration.
+// Arguments: BytesProcessed (estimated), TotalFileBytes, EventsSoFar.
+using ProgressCallback = std::function<void(uint64_t, uint64_t, uint64_t)>;
+
+// Parse a .utrace file into an in-memory TraceModel suitable for serving via
+// the trace viewer. A single pass runs the session, CPU-stats and timeline
+// analyzers. The optional progress callback is invoked once per bundle.
+TraceModel BuildTraceModel(const std::filesystem::path& FilePath, WorkerThreadPool& ThreadPool, const ProgressCallback& OnProgress = {});
+
+struct TraceTrimArgs
+{
+ std::filesystem::path InputPath;
+ std::filesystem::path OutputPath;
+ double StartSec = 0.0;
+ double EndSec = 0.0;
+};
+
+// Produce a trimmed .utrace file containing all type-definition and "important"
+// packets from the input, plus any regular thread packets whose events overlap
+// the [StartSec, EndSec] window. The output remains a valid .utrace that can be
+// read by Unreal Insights and zen's own trace tooling. Trimming is coarse at
+// the packet level: a packet that straddles the window boundary is kept in full.
+void RunTraceTrim(const TraceTrimArgs& Args);
+
+} // namespace zen::trace_detail
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
diff --git a/src/zen/trace/trace_viewer_service.h b/src/zen/trace/trace_viewer_service.h
new file mode 100644
index 000000000..f7bc51499
--- /dev/null
+++ b/src/zen/trace/trace_viewer_service.h
@@ -0,0 +1,71 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#pragma once
+
+#include "callstack_formatter.h"
+#include "timeline_query.h"
+#include "trace_model.h"
+
+#include <zenhttp/httpserver.h>
+#include <zenhttp/zipfs.h>
+
+#include <filesystem>
+#include <memory>
+
+namespace zen {
+
+// HttpService that serves an interactive flame-graph viewer for a parsed
+// TraceModel. Mounts at the server root; URIs beginning with "api/" return
+// JSON describing the model, everything else is resolved to a static asset
+// (first from the optional dev-mode directory, then from the embedded zip).
+class TraceViewerService final : public HttpService
+{
+public:
+ TraceViewerService(const trace_detail::TraceModel& Model,
+ std::unique_ptr<trace_detail::SymbolResolver> Symbols = {},
+ std::filesystem::path DevHtmlDir = {});
+ ~TraceViewerService() override;
+
+ [[nodiscard]] const char* BaseUri() const override;
+ void HandleRequest(HttpServerRequest& Request) override;
+
+private:
+ void HandleStaticAsset(HttpServerRequest& Request, std::string_view Uri);
+ void HandleApiRequest(HttpServerRequest& Request, std::string_view Path);
+
+ void HandleSessionApi(HttpServerRequest& Request);
+ void HandleThreadsApi(HttpServerRequest& Request);
+ void HandleChannelsApi(HttpServerRequest& Request);
+ void HandleScopeStatsApi(HttpServerRequest& Request);
+ void HandleScopeNamesApi(HttpServerRequest& Request);
+ void HandleTimelineApi(HttpServerRequest& Request);
+ void HandleTimelineBatchApi(HttpServerRequest& Request);
+ void HandleLogCategoriesApi(HttpServerRequest& Request);
+ void HandleLogsApi(HttpServerRequest& Request);
+ void HandleBookmarksApi(HttpServerRequest& Request);
+ void HandleRegionsApi(HttpServerRequest& Request);
+ void HandleCsvCategoriesApi(HttpServerRequest& Request);
+ void HandleCsvStatsApi(HttpServerRequest& Request);
+ void HandleCsvSeriesApi(HttpServerRequest& Request);
+ void HandleCsvEventsApi(HttpServerRequest& Request);
+ void HandleCsvMetadataApi(HttpServerRequest& Request);
+ void HandleAllocSummaryApi(HttpServerRequest& Request);
+ void HandleHeapsApi(HttpServerRequest& Request);
+ void HandleAllocTagsApi(HttpServerRequest& Request);
+ void HandleMemoryTimelineApi(HttpServerRequest& Request);
+ void HandleHeapStatsApi(HttpServerRequest& Request);
+ void HandleCallstacksApi(HttpServerRequest& Request);
+ void HandleCallstackStatsApi(HttpServerRequest& Request);
+ void HandleChurnStatsApi(HttpServerRequest& Request);
+ void HandleAllocSizeHistogramApi(HttpServerRequest& Request);
+
+ const trace_detail::TraceModel& m_Model;
+ std::filesystem::path m_DevHtmlDir;
+ std::unique_ptr<ZipFs> m_ZipFs;
+ std::unique_ptr<trace_detail::TimelineQuery> m_TimelineQuery;
+ std::unique_ptr<trace_detail::SymbolResolver> m_Symbols;
+ trace_detail::CallstackFilterOptions m_CallstackFilterOptions;
+ trace_detail::CallstackFormatter m_CallstackFormatter;
+};
+
+} // namespace zen
diff --git a/src/zen/xmake.lua b/src/zen/xmake.lua
index df249ade4..c4084231d 100644
--- a/src/zen/xmake.lua
+++ b/src/zen/xmake.lua
@@ -5,13 +5,57 @@ target("zen")
add_headerfiles("**.h")
add_files("**.cpp")
add_files("zen.cpp", {unity_ignored = true })
+ add_rules("utils.bin2c", {extensions = {".zip"}})
+ add_files(path.join(os.projectdir(), get_config("builddir") or get_config("buildir") or "build", "zen-frontend/zen-html.zip"))
add_deps("zencore", "zenhttp", "zenremotestore", "zenstore", "zenutil")
add_deps("zencompute", "zennet", "zentelemetry")
- add_deps("cxxopts", "fmt")
+ add_deps("cxxopts", "fmt", "raw_pdb", "tourist")
add_packages("json11")
add_includedirs(".")
+ add_defines("ZEN_EMBED_ZEN_HTML_ZIP=1")
set_symbols("debug")
+ on_load(function(target)
+ local html_dir = path.join(os.projectdir(), "src/zen/frontend/html")
+ local zip_dir = path.join(os.projectdir(), get_config("builddir") or get_config("buildir") or "build", "zen-frontend")
+ local zip_path = path.join(zip_dir, "zen-html.zip")
+
+ -- Check if zip needs regeneration
+ local need_update = not os.isfile(zip_path)
+ if not need_update then
+ local zip_mtime = os.mtime(zip_path)
+ for _, file in ipairs(os.files(path.join(html_dir, "**"))) do
+ if os.mtime(file) > zip_mtime then
+ need_update = true
+ break
+ end
+ end
+ end
+
+ if need_update then
+ print("Regenerating zen trace viewer frontend zip...")
+
+ os.tryrm(zip_path)
+ os.mkdir(zip_dir)
+
+ import("detect.tools.find_7z")
+ local cmd_7z = find_7z()
+ if cmd_7z then
+ os.execv(cmd_7z, {"a", "-bso0", zip_path, path.join(html_dir, "*")})
+ else
+ import("detect.tools.find_zip")
+ local zip_cmd = find_zip()
+ if zip_cmd then
+ local oldir = os.cd(html_dir)
+ os.execv(zip_cmd, {"-r", "-q", zip_path, "."})
+ os.cd(oldir)
+ else
+ raise("Unable to find a suitable zip tool (need 7z or zip)")
+ end
+ end
+ end
+ end)
+
if is_plat("windows") then
add_files("zen.rc")
add_ldflags("/subsystem:console,5.02", {force = true})
diff --git a/src/zen/zen.cpp b/src/zen/zen.cpp
index 984e8589b..02695419e 100644
--- a/src/zen/zen.cpp
+++ b/src/zen/zen.cpp
@@ -21,13 +21,13 @@
#include "cmds/service_cmd.h"
#include "cmds/status_cmd.h"
#include "cmds/top_cmd.h"
-#include "cmds/trace_cmd.h"
#include "cmds/ui_cmd.h"
#include "cmds/up_cmd.h"
#include "cmds/version_cmd.h"
#include "cmds/vfs_cmd.h"
#include "cmds/wipe_cmd.h"
#include "cmds/workspaces_cmd.h"
+#include "trace/trace_cmd.h"
#include <zencore/callstack.h>
#include <zencore/config.h>
@@ -270,20 +270,34 @@ ZenCmdWithSubCommands::PrintHelp()
Options().set_width(TuiConsoleColumns(80));
printf("%s\n", Options().help(Groups).c_str());
- // Append subcommand listing.
+ // Append subcommand listing. When a subcommand has aliases, display them as
+ // "name|alias1|alias2" in the left column so callers discover the alternate spellings.
+ auto FormatSubCmdName = [](ZenSubCmdBase& SubCmd) {
+ std::string Name(SubCmd.SubOptions().program());
+ for (const std::string& Alias : SubCmd.Aliases())
+ {
+ Name.push_back('|');
+ Name.append(Alias);
+ }
+ return Name;
+ };
+
+ std::vector<std::string> FormattedNames;
+ FormattedNames.reserve(m_SubCommands.size());
size_t MaxNameLen = 0;
for (ZenSubCmdBase* SubCmd : m_SubCommands)
{
- MaxNameLen = std::max(MaxNameLen, SubCmd->SubOptions().program().size());
+ FormattedNames.push_back(FormatSubCmdName(*SubCmd));
+ MaxNameLen = std::max(MaxNameLen, FormattedNames.back().size());
}
printf("subcommands:\n");
- for (ZenSubCmdBase* SubCmd : m_SubCommands)
+ for (size_t i = 0; i < m_SubCommands.size(); ++i)
{
printf(" %-*s %s\n",
static_cast<int>(MaxNameLen),
- SubCmd->SubOptions().program().c_str(),
- std::string(SubCmd->Description()).c_str());
+ FormattedNames[i].c_str(),
+ std::string(m_SubCommands[i]->Description()).c_str());
}
printf("\nFor global options run: zen --help\n");
}
@@ -308,16 +322,33 @@ ZenCmdWithSubCommands::PrintSubCommandHelp(cxxopts::Options& SubCmdOptions)
void
ZenCmdWithSubCommands::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv)
{
- std::vector<cxxopts::Options*> SubOptionPtrs;
- SubOptionPtrs.reserve(m_SubCommands.size());
- for (ZenSubCmdBase* SubCmd : m_SubCommands)
- {
- SubOptionPtrs.push_back(&SubCmd->SubOptions());
- }
-
+ ZenSubCmdBase* MatchedSubCmd = nullptr;
cxxopts::Options* MatchedSubOption = nullptr;
std::vector<char*> SubCommandArguments;
- int ParentArgc = GetSubCommand(Options(), argc, argv, SubOptionPtrs, MatchedSubOption, SubCommandArguments);
+ int ParentArgc = argc;
+ for (int i = 1; i < argc; ++i)
+ {
+ std::string_view Arg(argv[i]);
+ for (ZenSubCmdBase* SubCmd : m_SubCommands)
+ {
+ const bool NameMatch = SubCmd->SubOptions().program() == Arg;
+ const bool AliasMatch =
+ !NameMatch && std::find(SubCmd->Aliases().begin(), SubCmd->Aliases().end(), Arg) != SubCmd->Aliases().end();
+ if (NameMatch || AliasMatch)
+ {
+ MatchedSubCmd = SubCmd;
+ MatchedSubOption = &SubCmd->SubOptions();
+ break;
+ }
+ }
+ if (MatchedSubCmd != nullptr)
+ {
+ SubCommandArguments.push_back(argv[0]);
+ std::copy(&argv[i + 1], &argv[argc], std::back_inserter(SubCommandArguments));
+ ParentArgc = i + 1;
+ break;
+ }
+ }
// Intercept --help/-h in the parent arg range before calling ParseOptions so
// we can append subcommand information to the output. When a subcommand was
@@ -344,15 +375,6 @@ ZenCmdWithSubCommands::Run(const ZenCliOptions& GlobalOptions, int argc, char**
throw OptionParseException("No subcommand specified", {});
}
- ZenSubCmdBase* MatchedSubCmd = nullptr;
- for (ZenSubCmdBase* SubCmd : m_SubCommands)
- {
- if (&SubCmd->SubOptions() == MatchedSubOption)
- {
- MatchedSubCmd = SubCmd;
- break;
- }
- }
ZEN_ASSERT(MatchedSubCmd != nullptr);
// Intercept --help/-h in the subcommand args so we can show combined help
diff --git a/src/zen/zen.h b/src/zen/zen.h
index 98a9eee41..1f8fd78eb 100644
--- a/src/zen/zen.h
+++ b/src/zen/zen.h
@@ -102,15 +102,19 @@ class ZenSubCmdBase
public:
ZenSubCmdBase(std::string_view Name, std::string_view Description);
virtual ~ZenSubCmdBase() = default;
- cxxopts::Options& SubOptions() { return m_SubOptions; }
- std::string_view Description() const { return m_Description; }
- virtual void Run(const ZenCliOptions& GlobalOptions) = 0;
+ cxxopts::Options& SubOptions() { return m_SubOptions; }
+ std::string_view Description() const { return m_Description; }
+ const std::vector<std::string>& Aliases() const { return m_Aliases; }
+ virtual void Run(const ZenCliOptions& GlobalOptions) = 0;
protected:
+ void AddAlias(std::string Alias) { m_Aliases.push_back(std::move(Alias)); }
+
cxxopts::Options m_SubOptions;
private:
- std::string m_Description;
+ std::string m_Description;
+ std::vector<std::string> m_Aliases;
};
// Base for commands that host subcommands - handles all dispatch boilerplate
diff --git a/src/zencore/include/zencore/fmtutils.h b/src/zencore/include/zencore/fmtutils.h
index a263c6f04..f062c4147 100644
--- a/src/zencore/include/zencore/fmtutils.h
+++ b/src/zencore/include/zencore/fmtutils.h
@@ -90,7 +90,7 @@ struct fmt::formatter<std::filesystem::path> : formatter<string_view>
using namespace std::literals;
zen::ExtendableStringBuilder<128> String;
- String << Path.u8string();
+ zen::PathToUtf8(Path, String);
std::string_view PathView = String.ToView();
if (PathView.starts_with("\\\\?\\"sv))
diff --git a/src/zencore/include/zencore/intmath.h b/src/zencore/include/zencore/intmath.h
index 2b59d6f4a..b9f31d57b 100644
--- a/src/zencore/include/zencore/intmath.h
+++ b/src/zencore/include/zencore/intmath.h
@@ -38,6 +38,7 @@
#if ZEN_COMPILER_MSC || ZEN_PLATFORM_WINDOWS
# pragma intrinsic(_BitScanReverse)
# pragma intrinsic(_BitScanReverse64)
+# pragma intrinsic(_umul128)
#else
inline uint8_t
_BitScanReverse(unsigned long* Index, uint32_t Mask)
@@ -205,6 +206,37 @@ Max(auto x, auto y)
//////////////////////////////////////////////////////////////////////////
+// Precomputed reciprocal for fast 64-bit unsigned division by a constant.
+// Given divisor d, stores multiplier m and shift s such that
+// x / d == MulHi64(x, m) >> s for all x in the expected range.
+//
+// Uses the "round-up" method: m = ceil(2^(64+s) / d). The extra shift
+// parameter s is bumped when d is a power of two (where ceil would
+// overflow). For small divisors s == 0 always suffices.
+struct ReciprocalU64
+{
+ uint64_t Mul = 0;
+ uint32_t Shift = 0;
+
+ ReciprocalU64() = default;
+ explicit ReciprocalU64(uint64_t Divisor);
+
+ uint32_t Divide(uint64_t Value) const
+ {
+ if (Mul == 0)
+ {
+ return uint32_t(Value); // Divisor <= 1
+ }
+#if ZEN_PLATFORM_WINDOWS
+ uint64_t Hi;
+ _umul128(Value, Mul, &Hi);
+#else
+ uint64_t Hi = uint64_t(((unsigned __int128)Value * Mul) >> 64);
+#endif
+ return uint32_t(Hi >> Shift);
+ }
+};
+
void intmath_forcelink(); // internal
} // namespace zen
diff --git a/src/zencore/intmath.cpp b/src/zencore/intmath.cpp
index fedf76edc..b460b5b78 100644
--- a/src/zencore/intmath.cpp
+++ b/src/zencore/intmath.cpp
@@ -7,6 +7,43 @@
namespace zen {
+ReciprocalU64::ReciprocalU64(uint64_t Divisor)
+{
+ if (Divisor <= 1)
+ {
+ Mul = 0; // Sentinel — Divide() returns Value directly.
+ Shift = 0;
+ return;
+ }
+
+ // m = ceil(2^(64+s) / d). Start with s = 0; bump s only if
+ // the quotient doesn't fit in 64 bits (happens when d is a
+ // power of two, since 2^64 / 2^k = 2^(64-k) exactly and the
+ // +1 for ceil can overflow to zero).
+ for (uint32_t S = 0; S < 64; ++S)
+ {
+#if ZEN_PLATFORM_WINDOWS
+ uint64_t Remainder = 0;
+ uint64_t Quotient = _udiv128(uint64_t(1) << S, 0, Divisor, &Remainder);
+ uint64_t M = Quotient + (Remainder ? 1 : 0); // ceil
+#else
+ unsigned __int128 Num = (unsigned __int128)(uint64_t(1) << S) << 64;
+ uint64_t Quotient = uint64_t(Num / Divisor);
+ uint64_t Remainder = uint64_t(Num % Divisor);
+ uint64_t M = Quotient + (Remainder ? 1 : 0);
+#endif
+ if (M != 0)
+ {
+ Mul = M;
+ Shift = S;
+ return;
+ }
+ }
+ // Unreachable for any Divisor > 1.
+ Mul = 0;
+ Shift = 0;
+}
+
//////////////////////////////////////////////////////////////////////////
//
// Testing related code follows...
@@ -68,6 +105,63 @@ TEST_CASE("intmath")
CHECK(ByteSwap(uint64_t(0x214d'6172'7469'6e21ull)) == 0x216e'6974'7261'4d21ull);
}
+TEST_CASE("ReciprocalU64 matches integer division")
+{
+ uint64_t Divisors[] = {1, 2, 3, 4, 5, 7, 10, 100, 1000, 3000, 3579};
+
+ for (uint64_t D : Divisors)
+ {
+ ReciprocalU64 R(D);
+
+ uint64_t TestValues[] = {
+ 0,
+ 1,
+ D - 1,
+ D,
+ D + 1,
+ D * 2,
+ D * 2 + 1,
+ 1'000'000,
+ 10'000'000,
+ 100'000'000,
+ 1'000'000'000ULL,
+ 10'000'000'000ULL,
+ 100'000'000'000ULL,
+ 1'000'000'000'000ULL,
+ uint64_t(~0u),
+ };
+
+ for (uint64_t V : TestValues)
+ {
+ uint32_t Expected = uint32_t(V / D);
+ uint32_t Got = R.Divide(V);
+ CHECK_MESSAGE(Got == Expected, "V=", V, " D=", D, " expected=", Expected, " got=", Got);
+ }
+ }
+}
+
+TEST_CASE("ReciprocalU64 rounding division")
+{
+ // Verify the rounding pattern used in AbsorbBatch: (Cycle + half) / d
+ uint64_t Divisors[] = {3, 4, 5, 10, 3579};
+
+ for (uint64_t D : Divisors)
+ {
+ ReciprocalU64 R(D);
+ uint64_t Half = D >> 1;
+
+ uint64_t TestCycles[] = {0, 1, 100, 999'999, 1'000'000, 99'999'999, 1'000'000'000ULL, 50'000'000'000ULL};
+
+ for (uint64_t Cycle : TestCycles)
+ {
+ uint64_t Rounded = Cycle + Half;
+ uint32_t Expected = uint32_t(Rounded / D);
+ uint32_t Got = R.Divide(Rounded);
+ CHECK_MESSAGE(Got == Expected, "Cycle=", Cycle, " D=", D, " expected=", Expected, " got=", Got);
+ }
+ }
+}
+
TEST_SUITE_END();
#endif
diff --git a/src/zencore/string.cpp b/src/zencore/string.cpp
index 2691d14b8..34519b83b 100644
--- a/src/zencore/string.cpp
+++ b/src/zencore/string.cpp
@@ -56,20 +56,22 @@ bool
ToString(std::span<char> Buffer, uint64_t Num)
{
auto [Ptr, Ec] = std::to_chars(Buffer.data(), Buffer.data() + Buffer.size(), Num);
- if (Ec == std::errc{})
+ if (Ec != std::errc{} || Ptr == Buffer.data() + Buffer.size())
{
- *Ptr = '\0';
+ return false;
}
+ *Ptr = '\0';
return true;
}
bool
ToString(std::span<char> Buffer, int64_t Num)
{
auto [Ptr, Ec] = std::to_chars(Buffer.data(), Buffer.data() + Buffer.size(), Num);
- if (Ec == std::errc{})
+ if (Ec != std::errc{} || Ptr == Buffer.data() + Buffer.size())
{
- *Ptr = '\0';
+ return false;
}
+ *Ptr = '\0';
return true;
}
diff --git a/src/zenserver/xmake.lua b/src/zenserver/xmake.lua
index 4927422c2..e93df413f 100644
--- a/src/zenserver/xmake.lua
+++ b/src/zenserver/xmake.lua
@@ -268,4 +268,5 @@ target("zenserver")
end
copy_if_newer(path.join(installdir, "bin", nomad_bin), path.join(target:targetdir(), nomad_bin), nomad_bin)
end
+
end)
diff --git a/src/zenstore/cache/cachedisklayer.cpp b/src/zenstore/cache/cachedisklayer.cpp
index 45a4b6456..22b9d0be5 100644
--- a/src/zenstore/cache/cachedisklayer.cpp
+++ b/src/zenstore/cache/cachedisklayer.cpp
@@ -140,25 +140,37 @@ namespace cache::impl {
const char* LogExtension = ".slog";
const char* MetaExtension = ".meta";
+ std::filesystem::path GetBucketFilePath(const std::filesystem::path& BucketDir, const std::string& BucketName, const char* Extension)
+ {
+ ExtendablePathBuilder<256> Path;
+ Path.Append(BucketDir);
+ Path /= BucketName.c_str();
+ Path << Extension;
+ return Path.ToPath();
+ }
+
std::filesystem::path GetIndexPath(const std::filesystem::path& BucketDir, const std::string& BucketName)
{
- return BucketDir / (BucketName + IndexExtension);
+ return GetBucketFilePath(BucketDir, BucketName, IndexExtension);
}
std::filesystem::path GetMetaPath(const std::filesystem::path& BucketDir, const std::string& BucketName)
{
- return BucketDir / (BucketName + MetaExtension);
+ return GetBucketFilePath(BucketDir, BucketName, MetaExtension);
}
std::filesystem::path GetLogPath(const std::filesystem::path& BucketDir, const std::string& BucketName)
{
- return BucketDir / (BucketName + LogExtension);
+ return GetBucketFilePath(BucketDir, BucketName, LogExtension);
}
std::filesystem::path GetManifestPath(const std::filesystem::path& BucketDir, const std::string& BucketName)
{
ZEN_UNUSED(BucketName);
- return BucketDir / "zen_manifest";
+ ExtendablePathBuilder<256> Path;
+ Path.Append(BucketDir);
+ Path /= "zen_manifest";
+ return Path.ToPath();
}
bool ValidateCacheBucketIndexEntry(const DiskIndexEntry& Entry, std::string& OutReason)
diff --git a/src/zenutil/include/zenutil/parallelsort.h b/src/zenutil/include/zenutil/parallelsort.h
new file mode 100644
index 000000000..ed455ce9d
--- /dev/null
+++ b/src/zenutil/include/zenutil/parallelsort.h
@@ -0,0 +1,119 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#pragma once
+
+#include <zencore/scopeguard.h>
+#include <zencore/thread.h>
+#include <zencore/workthreadpool.h>
+
+ZEN_THIRD_PARTY_INCLUDES_START
+#include <EASTL/sort.h>
+#include <EASTL/vector.h>
+ZEN_THIRD_PARTY_INCLUDES_END
+
+#include <algorithm>
+
+namespace zen {
+
+// Bottom-up parallel merge sort using WorkerThreadPool + Latch.
+//
+// Splits the range into chunks, sorts each chunk in parallel via
+// eastl::sort, then iteratively merges adjacent pairs in parallel
+// using std::inplace_merge until a single sorted range remains.
+//
+// Falls back to eastl::sort for ranges below kMinParallelSortSize.
+template<typename RandomIt, typename Compare>
+void
+ParallelSort(WorkerThreadPool& Pool, RandomIt First, RandomIt Last, Compare Comp)
+{
+ constexpr size_t kMinParallelSortSize = 65536;
+ constexpr size_t kMinChunkSize = 65536;
+ constexpr size_t kMaxChunks = 64;
+
+ size_t Count = size_t(Last - First);
+ if (Count <= kMinParallelSortSize)
+ {
+ eastl::sort(First, Last, Comp);
+ return;
+ }
+
+ // Determine chunk count: enough to saturate workers, but not so many
+ // that scheduling overhead dominates.
+ size_t ChunkCount = (Count + kMinChunkSize - 1) / kMinChunkSize;
+ if (ChunkCount > kMaxChunks)
+ {
+ ChunkCount = kMaxChunks;
+ }
+ if (ChunkCount < 2)
+ {
+ ChunkCount = 2;
+ }
+
+ // Compute chunk boundaries.
+ eastl::vector<RandomIt> Boundaries;
+ Boundaries.reserve(ChunkCount + 1);
+ size_t ChunkSize = Count / ChunkCount;
+ for (size_t I = 0; I < ChunkCount; ++I)
+ {
+ Boundaries.push_back(First + ptrdiff_t(I * ChunkSize));
+ }
+ Boundaries.push_back(Last);
+
+ // Phase 1: Sort each chunk in parallel.
+ {
+ Latch Done(1);
+ for (size_t I = 0; I < ChunkCount; ++I)
+ {
+ Done.AddCount(1);
+ Pool.ScheduleWork(
+ [&Done, Begin = Boundaries[I], End = Boundaries[I + 1], &Comp]() {
+ auto Guard = MakeGuard([&Done]() { Done.CountDown(); });
+ eastl::sort(Begin, End, Comp);
+ },
+ WorkerThreadPool::EMode::EnableBacklog);
+ }
+ Done.CountDown();
+ Done.Wait();
+ }
+
+ // Phase 2: Pairwise merge rounds until a single sorted range remains.
+ // Each round merges non-overlapping adjacent pairs in parallel, then
+ // compacts the boundary list. An odd trailing chunk is carried forward.
+ while (ChunkCount > 1)
+ {
+ size_t Pairs = ChunkCount / 2;
+
+ {
+ Latch Done(1);
+ for (size_t I = 0; I < Pairs; ++I)
+ {
+ size_t Left = 2 * I;
+ Done.AddCount(1);
+ Pool.ScheduleWork(
+ [&Done, F = Boundaries[Left], M = Boundaries[Left + 1], L = Boundaries[Left + 2], &Comp]() {
+ auto Guard = MakeGuard([&Done]() { Done.CountDown(); });
+ std::inplace_merge(F, M, L, Comp);
+ },
+ WorkerThreadPool::EMode::EnableBacklog);
+ }
+ Done.CountDown();
+ Done.Wait();
+ }
+
+ // Compact boundaries: merged pairs collapse, odd chunk carried forward.
+ eastl::vector<RandomIt> NewBounds;
+ NewBounds.reserve((ChunkCount + 1) / 2 + 1);
+ for (size_t I = 0; I < ChunkCount; I += 2)
+ {
+ NewBounds.push_back(Boundaries[I]);
+ }
+ NewBounds.push_back(Last);
+
+ ChunkCount = NewBounds.size() - 1;
+ Boundaries = eastl::move(NewBounds);
+ }
+}
+
+void parallelsort_forcelink(); // internal
+
+} // namespace zen
diff --git a/src/zenutil/parallelsort.cpp b/src/zenutil/parallelsort.cpp
new file mode 100644
index 000000000..8a9f547bc
--- /dev/null
+++ b/src/zenutil/parallelsort.cpp
@@ -0,0 +1,148 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#include <zenutil/parallelsort.h>
+
+#include <zencore/testing.h>
+
+namespace zen {
+
+#if ZEN_WITH_TESTS
+
+void
+parallelsort_forcelink()
+{
+}
+
+TEST_SUITE_BEGIN("util.parallelsort");
+
+TEST_CASE("empty range")
+{
+ WorkerThreadPool Pool(2);
+ eastl::vector<int> Vec;
+ ParallelSort(Pool, Vec.begin(), Vec.end(), [](int A, int B) { return A < B; });
+ CHECK(Vec.empty());
+}
+
+TEST_CASE("single element")
+{
+ WorkerThreadPool Pool(2);
+ eastl::vector<int> Vec = {42};
+ ParallelSort(Pool, Vec.begin(), Vec.end(), [](int A, int B) { return A < B; });
+ CHECK(Vec.size() == 1);
+ CHECK(Vec[0] == 42);
+}
+
+TEST_CASE("small array below threshold")
+{
+ WorkerThreadPool Pool(2);
+ eastl::vector<int> Vec = {5, 3, 8, 1, 9, 2, 7, 4, 6, 0};
+ ParallelSort(Pool, Vec.begin(), Vec.end(), [](int A, int B) { return A < B; });
+ for (size_t I = 0; I < Vec.size(); ++I)
+ {
+ CHECK(Vec[I] == int(I));
+ }
+}
+
+TEST_CASE("large array triggers parallel path")
+{
+ WorkerThreadPool Pool(4);
+
+ // 200K elements — well above the 64K threshold.
+ constexpr size_t N = 200'000;
+ eastl::vector<uint32_t> Vec(N);
+
+ // Fill with descending values.
+ for (size_t I = 0; I < N; ++I)
+ {
+ Vec[I] = uint32_t(N - 1 - I);
+ }
+
+ ParallelSort(Pool, Vec.begin(), Vec.end(), [](uint32_t A, uint32_t B) { return A < B; });
+
+ for (size_t I = 0; I < N; ++I)
+ {
+ CHECK_MESSAGE(Vec[I] == uint32_t(I), "index=", I, " got=", Vec[I]);
+ }
+}
+
+TEST_CASE("already sorted")
+{
+ WorkerThreadPool Pool(4);
+
+ constexpr size_t N = 200'000;
+ eastl::vector<uint32_t> Vec(N);
+ for (size_t I = 0; I < N; ++I)
+ {
+ Vec[I] = uint32_t(I);
+ }
+
+ ParallelSort(Pool, Vec.begin(), Vec.end(), [](uint32_t A, uint32_t B) { return A < B; });
+
+ for (size_t I = 0; I < N; ++I)
+ {
+ CHECK_MESSAGE(Vec[I] == uint32_t(I), "index=", I, " got=", Vec[I]);
+ }
+}
+
+TEST_CASE("reverse sorted")
+{
+ WorkerThreadPool Pool(4);
+
+ constexpr size_t N = 200'000;
+ eastl::vector<uint32_t> Vec(N);
+ for (size_t I = 0; I < N; ++I)
+ {
+ Vec[I] = uint32_t(N - 1 - I);
+ }
+
+ ParallelSort(Pool, Vec.begin(), Vec.end(), [](uint32_t A, uint32_t B) { return A < B; });
+
+ for (size_t I = 0; I < N; ++I)
+ {
+ CHECK_MESSAGE(Vec[I] == uint32_t(I), "index=", I, " got=", Vec[I]);
+ }
+}
+
+TEST_CASE("duplicate keys")
+{
+ WorkerThreadPool Pool(4);
+
+ constexpr size_t N = 200'000;
+ eastl::vector<uint32_t> Vec(N);
+ for (size_t I = 0; I < N; ++I)
+ {
+ Vec[I] = uint32_t(I % 100); // only 100 distinct values
+ }
+
+ ParallelSort(Pool, Vec.begin(), Vec.end(), [](uint32_t A, uint32_t B) { return A < B; });
+
+ for (size_t I = 1; I < N; ++I)
+ {
+ CHECK_MESSAGE(Vec[I - 1] <= Vec[I], "not sorted at index=", I);
+ }
+}
+
+TEST_CASE("custom comparator descending")
+{
+ WorkerThreadPool Pool(4);
+
+ constexpr size_t N = 200'000;
+ eastl::vector<uint32_t> Vec(N);
+ for (size_t I = 0; I < N; ++I)
+ {
+ Vec[I] = uint32_t(I);
+ }
+
+ ParallelSort(Pool, Vec.begin(), Vec.end(), [](uint32_t A, uint32_t B) { return A > B; });
+
+ for (size_t I = 0; I < N; ++I)
+ {
+ CHECK_MESSAGE(Vec[I] == uint32_t(N - 1 - I), "index=", I, " got=", Vec[I]);
+ }
+}
+
+TEST_SUITE_END();
+
+#endif
+
+} // namespace zen
diff --git a/src/zenutil/xmake.lua b/src/zenutil/xmake.lua
index 83a6b7f93..e28f6e345 100644
--- a/src/zenutil/xmake.lua
+++ b/src/zenutil/xmake.lua
@@ -9,6 +9,9 @@ target('zenutil')
add_deps("zencore", "zenhttp")
add_deps("cxxopts")
add_deps("robin-map")
+ if is_plat("linux", "macosx") then
+ add_syslinks("dl")
+ end
add_packages("json11")
if is_plat("linux", "macosx") then
diff --git a/src/zenutil/zenutil.cpp b/src/zenutil/zenutil.cpp
index d2b8258eb..b9617b1ed 100644
--- a/src/zenutil/zenutil.cpp
+++ b/src/zenutil/zenutil.cpp
@@ -11,6 +11,7 @@
# include <zenutil/config/commandlineoptions.h>
# include <zenutil/filesystemutils.h>
# include <zenutil/invocationhistory.h>
+# include <zenutil/parallelsort.h>
# include <zenutil/rpcrecording.h>
# include <zenutil/splitconsole/logstreamlistener.h>
# include <zenutil/process/subprocessmanager.h>
@@ -28,6 +29,7 @@ zenutil_forcelinktests()
filesystemutils_forcelink();
imdscredentials_forcelink();
invocationhistory_forcelink();
+ parallelsort_forcelink();
logstreamlistener_forcelink();
subprocessmanager_forcelink();
s3client_forcelink();
diff --git a/thirdparty/VERSIONS.md b/thirdparty/VERSIONS.md
index fd734a2af..7d4ebf0db 100644
--- a/thirdparty/VERSIONS.md
+++ b/thirdparty/VERSIONS.md
@@ -24,6 +24,7 @@ dependency.
* ryml - v0.5.0 from https://github.com/biojppm/rapidyaml (note that there are submodules here which have also been fetched, after stripping all `.git` metadata, for future updates it's probably easier to just grab the .zip/.tar.gz since it includes all submodules)
* sol2 - v3.5.0 from https://github.com/ThePhD/sol2/archive/refs/tags/v3.5.0.tar.gz (single/single.py generates the headers)
* spdlog - v1.16.0 from https://github.com/gabime/spdlog/releases/tag/v1.16.0.zip
+* tourist - vendored from //depot/martin.ridgers/tourist/lib (global operator new/delete removed from malloc.cpp to avoid ODR conflict with zen allocators)
The above code should all be without modifications at the time of writing. It's not recommended
that you make changes in the third party code unless there are good reasons.
diff --git a/thirdparty/raw_pdb/.gitignore b/thirdparty/raw_pdb/.gitignore
new file mode 100644
index 000000000..7e1dd7fa4
--- /dev/null
+++ b/thirdparty/raw_pdb/.gitignore
@@ -0,0 +1,431 @@
+# CLion
+.idea/
+
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+##
+## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
+
+# User-specific files
+*.rsuser
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+*.env
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Mono auto generated files
+mono_crash.*
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+
+[Dd]ebug/x64/
+[Dd]ebugPublic/x64/
+[Rr]elease/x64/
+[Rr]eleases/x64/
+bin/x64/
+obj/x64/
+
+[Dd]ebug/x86/
+[Dd]ebugPublic/x86/
+[Rr]elease/x86/
+[Rr]eleases/x86/
+bin/x86/
+obj/x86/
+
+[Ww][Ii][Nn]32/
+[Aa][Rr][Mm]/
+[Aa][Rr][Mm]64/
+[Aa][Rr][Mm]64[Ee][Cc]/
+bld/
+[Oo]bj/
+[Oo]ut/
+[Ll]og/
+[Ll]ogs/
+
+# Build results on 'Bin' directories
+**/[Bb]in/*
+# Uncomment if you have tasks that rely on *.refresh files to move binaries
+# (https://github.com/github/gitignore/pull/3736)
+#!**/[Bb]in/*.refresh
+
+# Visual Studio 2015/2017 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# Visual Studio 2017 auto generated files
+Generated\ Files/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+*.trx
+
+# NUnit
+*.VisualState.xml
+TestResult.xml
+nunit-*.xml
+
+# Approval Tests result files
+*.received.*
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# Benchmark Results
+BenchmarkDotNet.Artifacts/
+
+# .NET Core
+project.lock.json
+project.fragment.lock.json
+artifacts/
+
+# ASP.NET Scaffolding
+ScaffoldingReadMe.txt
+
+# StyleCop
+StyleCopReport.xml
+
+# Files built by Visual Studio
+*_i.c
+*_p.c
+*_h.h
+*.ilk
+*.meta
+*.obj
+*.idb
+*.iobj
+*.pch
+*.pdb
+*.ipdb
+*.pgc
+*.pgd
+*.rsp
+# but not Directory.Build.rsp, as it configures directory-level build defaults
+!Directory.Build.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*_wpftmp.csproj
+*.log
+*.tlog
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# Visual Studio Trace Files
+*.e2e
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# AxoCover is a Code Coverage Tool
+.axoCover/*
+!.axoCover/settings.json
+
+# Coverlet is a free, cross platform Code Coverage Tool
+coverage*.json
+coverage*.xml
+coverage*.info
+
+# Visual Studio code coverage results
+*.coverage
+*.coveragexml
+
+# NCrunch
+_NCrunch_*
+.NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# Note: Comment the next line if you want to checkin your web deploy settings,
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# NuGet Symbol Packages
+*.snupkg
+# The packages folder can be ignored because of Package Restore
+**/[Pp]ackages/*
+# except build/, which is used as an MSBuild target.
+!**/[Pp]ackages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/[Pp]ackages/repositories.config
+# NuGet v3's project.json files produces more ignorable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+*.appx
+*.appxbundle
+*.appxupload
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!?*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+orleans.codegen.cs
+
+# Including strong name files can present a security risk
+# (https://github.com/github/gitignore/pull/2483#issue-259490424)
+#*.snk
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+ServiceFabricBackup/
+*.rptproj.bak
+
+# SQL Server files
+*.mdf
+*.ldf
+*.ndf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+*.rptproj.rsuser
+*- [Bb]ackup.rdl
+*- [Bb]ackup ([0-9]).rdl
+*- [Bb]ackup ([0-9][0-9]).rdl
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+node_modules/
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
+*.vbw
+
+# Visual Studio 6 workspace and project file (working project files containing files to include in project)
+*.dsw
+*.dsp
+
+# Visual Studio 6 technical files
+*.ncb
+*.aps
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+**/.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+**/.fake/
+
+# CodeRush personal settings
+**/.cr/personal
+
+# Python Tools for Visual Studio (PTVS)
+**/__pycache__/
+*.pyc
+
+# Cake - Uncomment if you are using it
+#tools/**
+#!tools/packages.config
+
+# Tabs Studio
+*.tss
+
+# Telerik's JustMock configuration file
+*.jmconfig
+
+# BizTalk build output
+*.btp.cs
+*.btm.cs
+*.odx.cs
+*.xsd.cs
+
+# OpenCover UI analysis results
+OpenCover/
+
+# Azure Stream Analytics local run output
+ASALocalRun/
+
+# MSBuild Binary and Structured Log
+*.binlog
+MSBuild_Logs/
+
+# AWS SAM Build and Temporary Artifacts folder
+.aws-sam
+
+# NVidia Nsight GPU debugger configuration file
+*.nvuser
+
+# MFractors (Xamarin productivity tool) working folder
+**/.mfractor/
+
+# Local History for Visual Studio
+**/.localhistory/
+
+# Visual Studio History (VSHistory) files
+.vshistory/
+
+# BeatPulse healthcheck temp database
+healthchecksdb
+
+# Backup folder for Package Reference Convert tool in Visual Studio 2017
+MigrationBackup/
+
+# Ionide (cross platform F# VS Code tools) working folder
+**/.ionide/
+
+# Fody - auto-generated XML schema
+FodyWeavers.xsd
+
+# VS Code files for those working on multiple tools
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+!.vscode/*.code-snippets
+
+# Local History for Visual Studio Code
+.history/
+
+# Built Visual Studio Code Extensions
+*.vsix
+
+# Windows Installer files from build outputs
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
diff --git a/thirdparty/raw_pdb/CMakeLists.txt b/thirdparty/raw_pdb/CMakeLists.txt
new file mode 100644
index 000000000..cc22ad5a0
--- /dev/null
+++ b/thirdparty/raw_pdb/CMakeLists.txt
@@ -0,0 +1,9 @@
+cmake_minimum_required(VERSION 3.16)
+
+project(raw_pdb)
+
+set(CMAKE_CXX_STANDARD 11)
+
+set_property(GLOBAL PROPERTY USE_FOLDERS ON)
+
+add_subdirectory(src) \ No newline at end of file
diff --git a/thirdparty/raw_pdb/LICENSE b/thirdparty/raw_pdb/LICENSE
new file mode 100644
index 000000000..d3fe23f47
--- /dev/null
+++ b/thirdparty/raw_pdb/LICENSE
@@ -0,0 +1,25 @@
+BSD 2-Clause License
+
+Copyright 2011-2022, Molecular Matters GmbH <[email protected]>
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/thirdparty/raw_pdb/README.md b/thirdparty/raw_pdb/README.md
new file mode 100644
index 000000000..d275d5e44
--- /dev/null
+++ b/thirdparty/raw_pdb/README.md
@@ -0,0 +1,190 @@
+# RawPDB
+
+**RawPDB** is a C++11 library that directly reads Microsoft Program DataBase PDB files. The code is extracted almost directly from <a href="https://liveplusplus.tech/">Live++ 2</a>, a battle-tested hot-reload tool for C++.
+
+## Design
+
+**RawPDB** gives you direct access to the stream data contained in a PDB file. It does not attempt to offer abstractions for iterating symbols, translation units, contributions, etc.
+
+Building a high-level abstraction over the provided low-level data is an ill-fated attempt that can never really be performant for everybody, because different tools like debuggers, hot-reload tools (e.g. <a href="https://liveplusplus.tech/">Live++</a>), profilers (e.g. <a href="https://superluminal.eu/">Superluminal</a>), need to perform different queries against the stored data.
+
+We therefore believe the best solution is to offer direct access to the underlying data, with applications bringing that data into their own structures.
+
+## Goal
+
+Eventually, we want **RawPDB** to become the de-facto replacement of <a href="https://docs.microsoft.com/en-us/visualstudio/debugger/debug-interface-access/debug-interface-access-sdk">Microsoft's DIA SDK</a> that most C++ developers (have to) use.
+
+## Features
+
+* Fast - **RawPDB** works directly with memory-mapped data, so only the data from the streams you touch affect performance. It is orders of magnitudes faster than the DIA SDK, and faster than comparable LLVM code
+* Scalable - **RawPDB's** API gives you access to individual streams that can all be read concurrently in a trivial fashion, since all returned data structures are immutable. There are no locks or waits anywhere inside the library
+* Lightweight - **RawPDB** is small and compiles in roughly 1 second
+* Allocation-friendly - **RawPDB** performs only a few allocations, and those can be overridden easily by changing the underlying macro
+* No STL - **RawPDB** does not need any STL containers or algorithms
+* No exceptions - **RawPDB** does not use exceptions
+* No RTTI - **RawPDB** does not need RTTI or use class hierarchies
+* High-quality code - **RawPDB** compiles clean under -Wall
+
+## Building
+
+The code compiles clean under Visual Studio 2015, 2017, 2019, or 2022. A solution for Visual Studio 2019 is included.
+
+## Performance
+
+Running the **Symbols** and **Contributions** examples on a 1GiB PDB yields the following output:
+
+<pre>
+Opening PDB file C:\Development\llvm-project\build\tools\clang\unittests\Tooling\RelWithDebInfo\ToolingTests.pdb
+
+Running example "Symbols"
+| Reading image section stream
+| ---> done in 0.066ms
+| Reading module info stream
+| ---> done in 0.562ms
+| Reading symbol record stream
+| ---> done in 25.185ms
+| Reading public symbol stream
+| ---> done in 1.133ms
+| Storing public symbols
+| ---> done in 46.171ms (212023 elements)
+| Reading global symbol stream
+| ---> done in 1.381ms
+| Storing global symbols
+| ---> done in 12.769ms (448957 elements)
+| Storing symbols from modules
+| ---> done in 145.849ms (2243 elements)
+---> done in 233.694ms (539611 elements)
+</pre>
+
+<pre>
+Opening PDB file C:\Development\llvm-project\build\tools\clang\unittests\Tooling\RelWithDebInfo\ToolingTests.pdb
+
+Running example "Contributions"
+| Reading image section stream
+| ---> done in 0.066ms
+| Reading module info stream
+| ---> done in 0.594ms
+| Reading section contribution stream
+| ---> done in 9.839ms
+| Storing contributions
+| ---> done in 67.346ms (630924 elements)
+| std::sort contributions
+| ---> done in 19.218ms
+---> done in 97.283ms
+20 largest contributions:
+1: 1896496 bytes from LLVMAMDGPUCodeGen.dir\RelWithDebInfo\AMDGPUInstructionSelector.obj
+2: 1700720 bytes from LLVMHexagonCodeGen.dir\RelWithDebInfo\HexagonInstrInfo.obj
+3: 1536470 bytes from LLVMRISCVCodeGen.dir\RelWithDebInfo\RISCVISelDAGToDAG.obj
+4: 1441408 bytes from LLVMAArch64CodeGen.dir\RelWithDebInfo\AArch64InstructionSelector.obj
+5: 1187048 bytes from LLVMRISCVCodeGen.dir\RelWithDebInfo\RISCVInstructionSelector.obj
+6: 1026504 bytes from LLVMARMCodeGen.dir\RelWithDebInfo\ARMInstructionSelector.obj
+7: 952080 bytes from LLVMAMDGPUDesc.dir\RelWithDebInfo\AMDGPUMCTargetDesc.obj
+8: 849888 bytes from LLVMX86Desc.dir\RelWithDebInfo\X86MCTargetDesc.obj
+9: 712176 bytes from LLVMHexagonCodeGen.dir\RelWithDebInfo\HexagonInstrInfo.obj
+10: 679035 bytes from LLVMX86CodeGen.dir\RelWithDebInfo\X86ISelDAGToDAG.obj
+11: 525174 bytes from LLVMAMDGPUDesc.dir\RelWithDebInfo\AMDGPUMCTargetDesc.obj
+12: 523035 bytes from * Linker *
+13: 519312 bytes from LLVMRISCVDesc.dir\RelWithDebInfo\RISCVMCTargetDesc.obj
+14: 512496 bytes from LLVMVEDesc.dir\RelWithDebInfo\VEMCTargetDesc.obj
+15: 498768 bytes from LLVMX86CodeGen.dir\RelWithDebInfo\X86InstructionSelector.obj
+16: 483528 bytes from LLVMMipsCodeGen.dir\RelWithDebInfo\MipsInstructionSelector.obj
+17: 449472 bytes from LLVMAMDGPUCodeGen.dir\RelWithDebInfo\AMDGPUISelDAGToDAG.obj
+18: 444246 bytes from C:\Development\llvm-project\build\tools\clang\lib\Basic\obj.clangBasic.dir\RelWithDebInfo\DiagnosticIDs.obj
+19: 371584 bytes from LLVMAArch64CodeGen.dir\RelWithDebInfo\AArch64ISelDAGToDAG.obj
+20: 370272 bytes from LLVMNVPTXDesc.dir\RelWithDebInfo\NVPTXMCTargetDesc.obj
+</pre>
+
+This is at least an order of magnitude faster than DIA, even though the example code is completely serial and uses std::vector, std::string, and std::sort, which are used for illustration purposes only.
+
+When reading streams in a concurrent fashion, you will most likely be limited by the speed at which the OS can bring the data into your process.
+
+Running the **Lines** example on a 1.37 GiB PDB yields the following output:
+
+<pre>
+
+Opening PDB file C:\pdb-test-files\clang-debug.pdb
+Version 20000404, signature 1658696914, age 1, GUID 563dd8f1-f32b-459b-8c2beae0e70bc19b
+
+Running example "Lines"
+| Reading image section stream
+| ---> done in 0.313ms
+| Reading module info stream
+| ---> done in 0.403ms
+| Reading names stream
+| ---> done in 0.126ms
+| Storing lines from modules
+| ---> done in 306.720ms (1847 elements)
+| std::sort sections
+| ---> done in 103.090ms (4023680 elements)
+
+</pre>
+
+## Supported streams
+
+**RawPDB** gives you access to the following PDB stream data:
+
+* DBI stream data
+ * Public symbols
+ * Global symbols
+ * Modules
+ * Module symbols
+ * Module lines (C13 line information)
+ * Image sections
+ * Info stream
+ * "/names" stream
+ * Section contributions
+ * Source files
+
+* IPI stream data
+
+* TPI stream data
+
+Furthermore, PDBs linked using /DEBUG:FASTLINK are not supported. These PDBs do not contain much information, since private symbol information is distributed among object files and library files.
+
+## Documentation
+
+If you are unfamiliar with the basic structure of a PDB file, the <a href="https://llvm.org/docs/PDB/index.html">LLVM documentation</a> serves as a good introduction.
+
+Consult the example code to see how to read and parse the PDB streams.
+
+## Directory structure
+
+* bin: contains final binary output files (.exe and .pdb)
+* build: contains Visual Studio 2019 solution and project files
+* lib: contains the RawPDB library output files (.lib and .pdb)
+* src: contains the RawPDB source code, as well as example code
+* temp: contains intermediate build artefacts
+
+## Examples
+
+### Symbols (<a href="https://github.com/MolecularMatters/raw_pdb/blob/main/src/Examples/ExampleSymbols.cpp">ExampleSymbols.cpp</a>)
+
+A basic example that shows how to load symbols from public, global, and module streams.
+
+### Contributions (<a href="https://github.com/MolecularMatters/raw_pdb/blob/main/src/Examples/ExampleContributions.cpp">ExampleContributions.cpp</a>)
+
+A basic example that shows how to load contributions, sort them by size, and output the 20 largest ones along with the object file they originated from.
+
+### Function symbols (<a href="https://github.com/MolecularMatters/raw_pdb/blob/main/src/Examples/ExampleFunctionSymbols.cpp">ExampleFunctionSymbols.cpp</a>)
+
+An example intended for profiler developers that shows how to enumerate all function symbols and retrieve or compute their code size.
+
+### Function variables (<a href="https://github.com/MolecularMatters/raw_pdb/blob/main/src/Examples/ExampleFunctionVariables.cpp">ExampleFunctionVariables.cpp</a>)
+
+An example intended for debugger developers that shows how to enumerate all function records needed for displaying function variables.
+
+### Lines (<a href="https://github.com/MolecularMatters/raw_pdb/blob/main/src/Examples/ExampleLines.cpp">ExampleLines.cpp</a>)
+
+An example that shows to how to load line information for all modules.
+
+### Types (<a href="https://github.com/MolecularMatters/raw_pdb/blob/main/src/Examples/ExampleTypes.cpp">ExampleTypes.cpp</a>)
+
+An example that prints all type records.
+
+### PDBSize (<a href="https://github.com/MolecularMatters/raw_pdb/blob/main/src/Examples/ExamplePDBSize.cpp">ExamplePDBSize.cpp</a>)
+
+An example that could serve as a starting point for people wanting to investigate and optimize the size of their PDBs.
+
+## Sponsoring or supporting RawPDB
+
+We have chosen a very liberal license to let **RawPDB** be used in as many scenarios as possible, including commercial applications. If you would like to support its development, consider licensing <a href="https://liveplusplus.tech/">Live++</a> instead. Not only do you give something back, but get a great productivity enhancement on top!
diff --git a/thirdparty/raw_pdb/raw_pdb.natvis b/thirdparty/raw_pdb/raw_pdb.natvis
new file mode 100644
index 000000000..e285ad03c
--- /dev/null
+++ b/thirdparty/raw_pdb/raw_pdb.natvis
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<AutoVisualizer xmlns="http://schemas.microsoft.com/vstudio/debugger/natvis/2010">
+ <Type Name="PDB::ArrayView&lt;*&gt;">
+ <DisplayString>{{ size={m_length} }}</DisplayString>
+ <Expand>
+ <ArrayItems>
+ <Size>m_length</Size>
+ <ValuePointer>m_data</ValuePointer>
+ </ArrayItems>
+ </Expand>
+ </Type>
+</AutoVisualizer>
diff --git a/thirdparty/raw_pdb/src/CMakeLists.txt b/thirdparty/raw_pdb/src/CMakeLists.txt
new file mode 100644
index 000000000..fdbe1e0c8
--- /dev/null
+++ b/thirdparty/raw_pdb/src/CMakeLists.txt
@@ -0,0 +1,112 @@
+set(SOURCES
+ Foundation/PDB_ArrayView.h
+ Foundation/PDB_Assert.h
+ Foundation/PDB_BitOperators.h
+ Foundation/PDB_BitUtil.h
+ Foundation/PDB_CRT.h
+ Foundation/PDB_Forward.h
+ Foundation/PDB_Log.h
+ Foundation/PDB_Macros.h
+ Foundation/PDB_Memory.h
+ Foundation/PDB_Move.h
+ Foundation/PDB_Platform.h
+ Foundation/PDB_PointerUtil.h
+ Foundation/PDB_TypeTraits.h
+ Foundation/PDB_Warnings.h
+
+ PDB.cpp
+ PDB.h
+ PDB_CoalescedMSFStream.cpp
+ PDB_CoalescedMSFStream.h
+ PDB_DBIStream.cpp
+ PDB_DBIStream.h
+ PDB_DBITypes.cpp
+ PDB_DBITypes.h
+ PDB_DirectMSFStream.cpp
+ PDB_DirectMSFStream.h
+ PDB_ErrorCodes.h
+ PDB_GlobalSymbolStream.cpp
+ PDB_GlobalSymbolStream.h
+ PDB_ImageSectionStream.cpp
+ PDB_ImageSectionStream.h
+ PDB_InfoStream.cpp
+ PDB_InfoStream.h
+ PDB_IPIStream.cpp
+ PDB_IPIStream.h
+ PDB_IPITypes.h
+ PDB_ModuleInfoStream.cpp
+ PDB_ModuleInfoStream.h
+ PDB_ModuleLineStream.cpp
+ PDB_ModuleLineStream.h
+ PDB_ModuleSymbolStream.cpp
+ PDB_ModuleSymbolStream.h
+ PDB_NamesStream.cpp
+ PDB_NamesStream.h
+ PDB_PCH.cpp
+ PDB_PCH.h
+ PDB_PublicSymbolStream.cpp
+ PDB_PublicSymbolStream.h
+ PDB_RawFile.cpp
+ PDB_RawFile.h
+ PDB_SectionContributionStream.cpp
+ PDB_SectionContributionStream.h
+ PDB_SourceFileStream.cpp
+ PDB_SourceFileStream.h
+ PDB_TPIStream.cpp
+ PDB_TPIStream.h
+ PDB_TPITypes.h
+ PDB_Types.cpp
+ PDB_Types.h
+ PDB_Util.h
+)
+
+source_group(src FILES
+ ${SOURCES}
+)
+
+add_library(raw_pdb
+ ${SOURCES}
+)
+
+target_include_directories(raw_pdb
+ PUBLIC
+ .
+)
+
+target_precompile_headers(raw_pdb
+ PRIVATE
+ PDB_PCH.h
+)
+
+option(RAWPDB_BUILD_EXAMPLES "Build Examples" ON)
+
+if (RAWPDB_BUILD_EXAMPLES)
+ add_subdirectory(Examples)
+endif()
+
+if (UNIX)
+ include(GNUInstallDirs)
+
+ install(
+ TARGETS raw_pdb
+ LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}"
+ )
+
+ file(GLOB_RECURSE HEADER_FILES
+ "${CMAKE_CURRENT_SOURCE_DIR}/*.h"
+ )
+
+ file(GLOB_RECURSE HEADER_FILES_FOUNDATION
+ "${CMAKE_CURRENT_SOURCE_DIR}/Foundation/*.h"
+ )
+
+ install(
+ FILES ${HEADER_FILES}
+ DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/raw_pdb/"
+ )
+
+ install(
+ FILES ${HEADER_FILES_FOUNDATION}
+ DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/raw_pdb/Foundation"
+ )
+endif (UNIX)
diff --git a/thirdparty/raw_pdb/src/Examples/CMakeLists.txt b/thirdparty/raw_pdb/src/Examples/CMakeLists.txt
new file mode 100644
index 000000000..6e59c1a9d
--- /dev/null
+++ b/thirdparty/raw_pdb/src/Examples/CMakeLists.txt
@@ -0,0 +1,39 @@
+project(Examples)
+
+set(SOURCES
+ ExampleContributions.cpp
+ ExampleFunctionSymbols.cpp
+ ExampleFunctionVariables.cpp
+ ExampleIPI.cpp
+ ExampleLines.cpp
+ ExampleMain.cpp
+ ExampleMemoryMappedFile.cpp
+ ExampleMemoryMappedFile.h
+ ExamplePDBSize.cpp
+ Examples_PCH.cpp
+ Examples_PCH.h
+ ExampleSymbols.cpp
+ ExampleTimedScope.cpp
+ ExampleTimedScope.h
+ ExampleTypes.cpp
+ ExampleTypeTable.cpp
+ ExampleTypeTable.h
+)
+
+source_group(src FILES
+ ${SOURCES}
+)
+
+add_executable(Examples
+ ${SOURCES}
+)
+
+target_link_libraries(Examples
+ PUBLIC
+ raw_pdb
+)
+
+target_precompile_headers(Examples
+ PUBLIC
+ Examples_PCH.h
+) \ No newline at end of file
diff --git a/thirdparty/raw_pdb/src/Examples/ExampleContributions.cpp b/thirdparty/raw_pdb/src/Examples/ExampleContributions.cpp
new file mode 100644
index 000000000..93c509117
--- /dev/null
+++ b/thirdparty/raw_pdb/src/Examples/ExampleContributions.cpp
@@ -0,0 +1,96 @@
+// Copyright 2011-2022, Molecular Matters GmbH <[email protected]>
+// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
+
+#include "Examples_PCH.h"
+#include "ExampleTimedScope.h"
+#include "PDB_RawFile.h"
+#include "PDB_DBIStream.h"
+
+
+namespace
+{
+ // we don't have to store std::string in the contributions, since all the data is memory-mapped anyway.
+ // we do it in this example to ensure that we don't "cheat" when reading the PDB file. memory-mapped data will only
+ // be faulted into the process once it's touched, so actually copying the string data makes us touch the needed data,
+ // giving us a real performance measurement.
+ struct Contribution
+ {
+ std::string objectFile;
+ uint32_t rva;
+ uint32_t size;
+ };
+}
+
+
+void ExampleContributions(const PDB::RawFile& rawPdbFile, const PDB::DBIStream& dbiStream);
+void ExampleContributions(const PDB::RawFile& rawPdbFile, const PDB::DBIStream& dbiStream)
+{
+ TimedScope total("\nRunning example \"Contributions\"");
+
+ // in order to keep the example easy to understand, we load the PDB data serially.
+ // note that this can be improved a lot by reading streams concurrently.
+
+ // prepare the image section stream first. it is needed for converting section + offset into an RVA
+ TimedScope sectionScope("Reading image section stream");
+ const PDB::ImageSectionStream imageSectionStream = dbiStream.CreateImageSectionStream(rawPdbFile);
+ sectionScope.Done();
+
+
+ // prepare the module info stream for matching contributions against files
+ TimedScope moduleScope("Reading module info stream");
+ const PDB::ModuleInfoStream moduleInfoStream = dbiStream.CreateModuleInfoStream(rawPdbFile);
+ moduleScope.Done();
+
+
+ // read contribution stream
+ TimedScope contributionScope("Reading section contribution stream");
+ const PDB::SectionContributionStream sectionContributionStream = dbiStream.CreateSectionContributionStream(rawPdbFile);
+ contributionScope.Done();
+
+ std::vector<Contribution> contributions;
+ {
+ TimedScope scope("Storing contributions");
+
+ const PDB::ArrayView<PDB::DBI::SectionContribution> sectionContributions = sectionContributionStream.GetContributions();
+ const size_t count = sectionContributions.GetLength();
+
+ contributions.reserve(count);
+
+ for (const PDB::DBI::SectionContribution& contribution : sectionContributions)
+ {
+ const uint32_t rva = imageSectionStream.ConvertSectionOffsetToRVA(contribution.section, contribution.offset);
+ if (rva == 0u)
+ {
+ printf("Contribution has invalid RVA\n");
+ continue;
+ }
+
+ const PDB::ModuleInfoStream::Module& module = moduleInfoStream.GetModule(contribution.moduleIndex);
+
+ contributions.push_back(Contribution { module.GetName().Decay(), rva, contribution.size });
+ }
+
+ scope.Done(count);
+ }
+
+ TimedScope sortScope("std::sort contributions");
+ std::sort(contributions.begin(), contributions.end(), [](const Contribution& lhs, const Contribution& rhs)
+ {
+ return lhs.size > rhs.size;
+ });
+ sortScope.Done();
+
+ total.Done();
+
+ // log the 20 largest contributions
+ {
+ printf("20 largest contributions:\n");
+
+ const size_t countToShow = std::min<size_t>(20ul, contributions.size());
+ for (size_t i = 0u; i < countToShow; ++i)
+ {
+ const Contribution& contribution = contributions[i];
+ printf("%zu: %u bytes from %s\n", i + 1u, contribution.size, contribution.objectFile.c_str());
+ }
+ }
+}
diff --git a/thirdparty/raw_pdb/src/Examples/ExampleFunctionSymbols.cpp b/thirdparty/raw_pdb/src/Examples/ExampleFunctionSymbols.cpp
new file mode 100644
index 000000000..fee212e2b
--- /dev/null
+++ b/thirdparty/raw_pdb/src/Examples/ExampleFunctionSymbols.cpp
@@ -0,0 +1,262 @@
+// Copyright 2011-2022, Molecular Matters GmbH <[email protected]>
+// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
+
+#include "Examples_PCH.h"
+#include "ExampleTimedScope.h"
+#include "PDB_RawFile.h"
+#include "PDB_DBIStream.h"
+
+namespace
+{
+ // in this example, we are only interested in function symbols: function name, RVA, and size.
+ // this is what most profilers need, they aren't interested in any other data.
+ struct FunctionSymbol
+ {
+ std::string name;
+ uint32_t rva;
+ uint32_t size;
+ const PDB::CodeView::DBI::Record* frameProc;
+ };
+}
+
+
+void ExampleFunctionSymbols(const PDB::RawFile& rawPdbFile, const PDB::DBIStream& dbiStream);
+void ExampleFunctionSymbols(const PDB::RawFile& rawPdbFile, const PDB::DBIStream& dbiStream)
+{
+ TimedScope total("\nRunning example \"Function symbols\"");
+
+ // in order to keep the example easy to understand, we load the PDB data serially.
+ // note that this can be improved a lot by reading streams concurrently.
+
+ // prepare the image section stream first. it is needed for converting section + offset into an RVA
+ TimedScope sectionScope("Reading image section stream");
+ const PDB::ImageSectionStream imageSectionStream = dbiStream.CreateImageSectionStream(rawPdbFile);
+ sectionScope.Done();
+
+
+ // prepare the module info stream for grabbing function symbols from modules
+ TimedScope moduleScope("Reading module info stream");
+ const PDB::ModuleInfoStream moduleInfoStream = dbiStream.CreateModuleInfoStream(rawPdbFile);
+ moduleScope.Done();
+
+
+ // prepare symbol record stream needed by the public stream
+ TimedScope symbolStreamScope("Reading symbol record stream");
+ const PDB::CoalescedMSFStream symbolRecordStream = dbiStream.CreateSymbolRecordStream(rawPdbFile);
+ symbolStreamScope.Done();
+
+
+ // note that we only use unordered_set in order to keep the example code easy to understand.
+ // using other hash set implementations like e.g. abseil's Swiss Tables (https://abseil.io/about/design/swisstables) is *much* faster.
+ std::vector<FunctionSymbol> functionSymbols;
+ std::unordered_set<uint32_t> seenFunctionRVAs;
+
+ // start by reading the module stream, grabbing every function symbol we can find.
+ // in most cases, this gives us ~90% of all function symbols already, along with their size.
+ {
+ TimedScope scope("Storing function symbols from modules");
+
+ const PDB::ArrayView<PDB::ModuleInfoStream::Module> modules = moduleInfoStream.GetModules();
+
+ for (const PDB::ModuleInfoStream::Module& module : modules)
+ {
+ if (!module.HasSymbolStream())
+ {
+ continue;
+ }
+
+ const PDB::ModuleSymbolStream moduleSymbolStream = module.CreateSymbolStream(rawPdbFile);
+ moduleSymbolStream.ForEachSymbol([&functionSymbols, &seenFunctionRVAs, &imageSectionStream](const PDB::CodeView::DBI::Record* record)
+ {
+ // only grab function symbols from the module streams
+ const char* name = nullptr;
+ uint32_t rva = 0u;
+ uint32_t size = 0u;
+ if (record->header.kind == PDB::CodeView::DBI::SymbolRecordKind::S_FRAMEPROC)
+ {
+ functionSymbols[functionSymbols.size() - 1].frameProc = record;
+ return;
+ }
+ else if (record->header.kind == PDB::CodeView::DBI::SymbolRecordKind::S_THUNK32)
+ {
+ if (record->data.S_THUNK32.thunk == PDB::CodeView::DBI::ThunkOrdinal::TrampolineIncremental)
+ {
+ // we have never seen incremental linking thunks stored inside a S_THUNK32 symbol, but better safe than sorry
+ name = "ILT";
+ rva = imageSectionStream.ConvertSectionOffsetToRVA(record->data.S_THUNK32.section, record->data.S_THUNK32.offset);
+ size = 5u;
+ }
+ }
+ else if (record->header.kind == PDB::CodeView::DBI::SymbolRecordKind::S_TRAMPOLINE)
+ {
+ // incremental linking thunks are stored in the linker module
+ name = "ILT";
+ rva = imageSectionStream.ConvertSectionOffsetToRVA(record->data.S_TRAMPOLINE.thunkSection, record->data.S_TRAMPOLINE.thunkOffset);
+ size = 5u;
+ }
+ else if (record->header.kind == PDB::CodeView::DBI::SymbolRecordKind::S_LPROC32)
+ {
+ name = record->data.S_LPROC32.name;
+ rva = imageSectionStream.ConvertSectionOffsetToRVA(record->data.S_LPROC32.section, record->data.S_LPROC32.offset);
+ size = record->data.S_LPROC32.codeSize;
+ }
+ else if (record->header.kind == PDB::CodeView::DBI::SymbolRecordKind::S_GPROC32)
+ {
+ name = record->data.S_GPROC32.name;
+ rva = imageSectionStream.ConvertSectionOffsetToRVA(record->data.S_GPROC32.section, record->data.S_GPROC32.offset);
+ size = record->data.S_GPROC32.codeSize;
+ }
+ else if (record->header.kind == PDB::CodeView::DBI::SymbolRecordKind::S_LPROC32_ID)
+ {
+ name = record->data.S_LPROC32_ID.name;
+ rva = imageSectionStream.ConvertSectionOffsetToRVA(record->data.S_LPROC32_ID.section, record->data.S_LPROC32_ID.offset);
+ size = record->data.S_LPROC32_ID.codeSize;
+ }
+ else if (record->header.kind == PDB::CodeView::DBI::SymbolRecordKind::S_GPROC32_ID)
+ {
+ name = record->data.S_GPROC32_ID.name;
+ rva = imageSectionStream.ConvertSectionOffsetToRVA(record->data.S_GPROC32_ID.section, record->data.S_GPROC32_ID.offset);
+ size = record->data.S_GPROC32_ID.codeSize;
+ }
+
+ if (rva == 0u)
+ {
+ return;
+ }
+
+ functionSymbols.push_back(FunctionSymbol { name, rva, size, nullptr });
+ seenFunctionRVAs.emplace(rva);
+ });
+ }
+
+ scope.Done(modules.GetLength());
+ }
+
+ // we don't need to touch global symbols in this case.
+ // most of the data we need can be obtained from the module symbol streams, and the global symbol stream only offers data symbols on top of that, which we are not interested in.
+ // however, there can still be public function symbols we haven't seen yet in any of the modules, especially for PDBs that don't provide module-specific information.
+
+ // read public symbols
+ TimedScope publicScope("Reading public symbol stream");
+ const PDB::PublicSymbolStream publicSymbolStream = dbiStream.CreatePublicSymbolStream(rawPdbFile);
+ publicScope.Done();
+ {
+ TimedScope scope("Storing public function symbols");
+
+ const PDB::ArrayView<PDB::HashRecord> hashRecords = publicSymbolStream.GetRecords();
+ const size_t count = hashRecords.GetLength();
+
+ for (const PDB::HashRecord& hashRecord : hashRecords)
+ {
+ const PDB::CodeView::DBI::Record* record = publicSymbolStream.GetRecord(symbolRecordStream, hashRecord);
+ if (record->header.kind != PDB::CodeView::DBI::SymbolRecordKind::S_PUB32)
+ {
+ // normally, a PDB only contains S_PUB32 symbols in the public symbol stream, but we have seen PDBs that also store S_CONSTANT as public symbols.
+ // ignore these.
+ continue;
+ }
+
+ if ((PDB_AS_UNDERLYING(record->data.S_PUB32.flags) & PDB_AS_UNDERLYING(PDB::CodeView::DBI::PublicSymbolFlags::Function)) == 0u)
+ {
+ // ignore everything that is not a function
+ continue;
+ }
+
+ const uint32_t rva = imageSectionStream.ConvertSectionOffsetToRVA(record->data.S_PUB32.section, record->data.S_PUB32.offset);
+ if (rva == 0u)
+ {
+ // certain symbols (e.g. control-flow guard symbols) don't have a valid RVA, ignore those
+ continue;
+ }
+
+ // check whether we already know this symbol from one of the module streams
+ const auto it = seenFunctionRVAs.find(rva);
+ if (it != seenFunctionRVAs.end())
+ {
+ // we know this symbol already, ignore it
+ continue;
+ }
+
+ // this is a new function symbol, so store it.
+ // note that we don't know its size yet.
+ functionSymbols.push_back(FunctionSymbol { record->data.S_PUB32.name, rva, 0u, nullptr });
+ }
+
+ scope.Done(count);
+ }
+
+
+ // we still need to find the size of the public function symbols.
+ // this can be deduced by sorting the symbols by their RVA, and then computing the distance between the current and the next symbol.
+ // this works since functions are always mapped to executable pages, so they aren't interleaved by any data symbols.
+ TimedScope sortScope("std::sort function symbols");
+ std::sort(functionSymbols.begin(), functionSymbols.end(), [](const FunctionSymbol& lhs, const FunctionSymbol& rhs)
+ {
+ return lhs.rva < rhs.rva;
+ });
+ sortScope.Done();
+
+ const size_t symbolCount = functionSymbols.size();
+ if (symbolCount != 0u)
+ {
+ TimedScope computeScope("Computing function symbol sizes");
+
+ size_t foundCount = 0u;
+
+ // we have at least 1 symbol.
+ // compute missing symbol sizes by computing the distance from this symbol to the next.
+ // note that this includes "int 3" padding after the end of a function. if you don't want that, but the actual number of bytes of
+ // the function's code, your best bet is to use a disassembler instead.
+ for (size_t i = 0u; i < symbolCount - 1u; ++i)
+ {
+ FunctionSymbol& currentSymbol = functionSymbols[i];
+ if (currentSymbol.size != 0u)
+ {
+ // the symbol's size is already known
+ continue;
+ }
+
+ const FunctionSymbol& nextSymbol = functionSymbols[i + 1u];
+ const size_t size = nextSymbol.rva - currentSymbol.rva;
+ (void)size; // unused
+ ++foundCount;
+ }
+
+ // we know have the sizes of all symbols, except the last.
+ // this can be found by going through the contributions, if needed.
+ FunctionSymbol& lastSymbol = functionSymbols[symbolCount - 1u];
+ if (lastSymbol.size == 0u)
+ {
+ // bad luck, we can't deduce the last symbol's size, so have to consult the contributions instead.
+ // we do a linear search in this case to keep the code simple.
+ const PDB::SectionContributionStream sectionContributionStream = dbiStream.CreateSectionContributionStream(rawPdbFile);
+ const PDB::ArrayView<PDB::DBI::SectionContribution> sectionContributions = sectionContributionStream.GetContributions();
+ for (const PDB::DBI::SectionContribution& contribution : sectionContributions)
+ {
+ const uint32_t rva = imageSectionStream.ConvertSectionOffsetToRVA(contribution.section, contribution.offset);
+ if (rva == 0u)
+ {
+ printf("Contribution has invalid RVA\n");
+ continue;
+ }
+
+ if (rva == lastSymbol.rva)
+ {
+ lastSymbol.size = contribution.size;
+ break;
+ }
+
+ if (rva > lastSymbol.rva)
+ {
+ // should have found the contribution by now
+ printf("Unknown contribution for symbol %s at RVA 0x%X", lastSymbol.name.c_str(), lastSymbol.rva);
+ break;
+ }
+ }
+ }
+
+ computeScope.Done(foundCount);
+ }
+
+ total.Done(functionSymbols.size());
+}
diff --git a/thirdparty/raw_pdb/src/Examples/ExampleFunctionVariables.cpp b/thirdparty/raw_pdb/src/Examples/ExampleFunctionVariables.cpp
new file mode 100644
index 000000000..85b561026
--- /dev/null
+++ b/thirdparty/raw_pdb/src/Examples/ExampleFunctionVariables.cpp
@@ -0,0 +1,382 @@
+// Copyright 2011-2022, Molecular Matters GmbH <[email protected]>
+// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
+
+#include "Examples_PCH.h"
+#include "ExampleTimedScope.h"
+#include "ExampleTypeTable.h"
+#include "PDB_RawFile.h"
+#include "PDB_DBIStream.h"
+#include "PDB_TPIStream.h"
+
+using SymbolRecordKind = PDB::CodeView::DBI::SymbolRecordKind;
+
+static std::string GetVariableTypeName(const TypeTable& typeTable, uint32_t typeIndex)
+{
+ // Defined in ExampleTypes.cpp
+ extern std::string GetTypeName(const TypeTable & typeTable, uint32_t typeIndex);
+
+ std::string typeName = GetTypeName(typeTable, typeIndex);
+
+ // Remove any '%s' substring used to insert a variable/field name.
+ const uint64_t markerPos = typeName.find("%s");
+ if (markerPos != typeName.npos)
+ {
+ typeName.erase(markerPos, 2);
+ }
+
+ return typeName;
+}
+
+static void Printf(uint32_t indent, const char* format, ...)
+{
+ va_list args;
+ va_start(args, format);
+
+ printf("%*s", indent * 4, "");
+ vprintf(format, args);
+
+ va_end(args);
+}
+
+void ExampleFunctionVariables(const PDB::RawFile& rawPdbFile, const PDB::DBIStream& dbiStream, const PDB::TPIStream& tpiStream);
+void ExampleFunctionVariables(const PDB::RawFile& rawPdbFile, const PDB::DBIStream& dbiStream, const PDB::TPIStream& tpiStream)
+{
+ TimedScope total("\nRunning example \"Function variables\"");
+
+ TimedScope typeTableScope("Create TypeTable");
+ TypeTable typeTable(tpiStream);
+ typeTableScope.Done();
+
+ // in order to keep the example easy to understand, we load the PDB data serially.
+ // note that this can be improved a lot by reading streams concurrently.
+
+ // prepare the image section stream first. it is needed for converting section + offset into an RVA
+ TimedScope sectionScope("Reading image section stream");
+ const PDB::ImageSectionStream imageSectionStream = dbiStream.CreateImageSectionStream(rawPdbFile);
+ sectionScope.Done();
+
+ // prepare the module info stream for grabbing function symbols from modules
+ TimedScope moduleScope("Reading module info stream");
+ const PDB::ModuleInfoStream moduleInfoStream = dbiStream.CreateModuleInfoStream(rawPdbFile);
+ moduleScope.Done();
+
+ // prepare symbol record stream needed by the public stream
+ TimedScope symbolStreamScope("Reading symbol record stream");
+ const PDB::CoalescedMSFStream symbolRecordStream = dbiStream.CreateSymbolRecordStream(rawPdbFile);
+ symbolStreamScope.Done();
+
+ {
+ TimedScope scope("Printing function variable records from modules\n");
+
+ const PDB::ArrayView<PDB::ModuleInfoStream::Module> modules = moduleInfoStream.GetModules();
+
+ uint32_t blockLevel = 0;
+ uint32_t recordCount = 0;
+
+ for (const PDB::ModuleInfoStream::Module& module : modules)
+ {
+ if (!module.HasSymbolStream())
+ {
+ continue;
+ }
+
+ const PDB::ModuleSymbolStream moduleSymbolStream = module.CreateSymbolStream(rawPdbFile);
+ moduleSymbolStream.ForEachSymbol([&typeTable, &imageSectionStream, &blockLevel, &recordCount](const PDB::CodeView::DBI::Record* record)
+ {
+ const SymbolRecordKind kind = record->header.kind;
+ const PDB::CodeView::DBI::Record::Data& data = record->data;
+
+ if (kind == SymbolRecordKind::S_END)
+ {
+ PDB_ASSERT(blockLevel > 0, "Block level for S_END is 0");
+ blockLevel--;
+ Printf(blockLevel, "S_END\n");
+
+ if (blockLevel == 0)
+ {
+ Printf(0, "\n");
+ }
+ }
+ else if(kind == SymbolRecordKind::S_SKIP)
+ {
+ Printf(blockLevel, "S_SKIP\n");
+ }
+ else if (kind == SymbolRecordKind::S_BLOCK32)
+ {
+ const uint32_t offset = imageSectionStream.ConvertSectionOffsetToRVA(data.S_BLOCK32.section, data.S_BLOCK32.offset);
+
+ Printf(blockLevel, "S_BLOCK32: '%s' | Code Offset 0x%X\n", data.S_BLOCK32.name, offset);
+ blockLevel++;
+ }
+ else if (kind == SymbolRecordKind::S_LABEL32)
+ {
+ Printf(blockLevel, "S_LABEL32: '%s' | Offset 0x%X\n", data.S_LABEL32.name, data.S_LABEL32.offset);
+ }
+ else if(kind == SymbolRecordKind::S_CONSTANT)
+ {
+ const std::string typeName = GetVariableTypeName(typeTable, data.S_CONSTANT.typeIndex);
+
+ Printf(blockLevel, "S_CONSTANT: '%s' -> '%s' | Value 0x%X\n", typeName.c_str(), data.S_CONSTANT.name, data.S_CONSTANT.value);
+ }
+ else if(kind == SymbolRecordKind::S_LOCAL)
+ {
+ const std::string typeName = GetVariableTypeName(typeTable, data.S_LOCAL.typeIndex);
+ Printf(blockLevel, "S_LOCAL: '%s' -> '%s' | Param: %s | Optimized Out: %s\n", typeName.c_str(), data.S_LOCAL.name, data.S_LOCAL.flags.fIsParam ? "True" : "False", data.S_LOCAL.flags.fIsOptimizedOut ? "True" : "False");
+ }
+ else if (kind == SymbolRecordKind::S_DEFRANGE_REGISTER)
+ {
+ Printf(blockLevel, "S_DEFRANGE_REGISTER: Register 0x%X\n", data.S_DEFRANGE_REGISTER.reg);
+ }
+ else if(kind == SymbolRecordKind::S_DEFRANGE_FRAMEPOINTER_REL)
+ {
+ Printf(blockLevel, "S_DEFRANGE_FRAMEPOINTER_REL: Frame Pointer Offset 0x%X | Range Start 0x%X | Range Section Start 0x%X | Range Length %u\n",
+ data.S_DEFRANGE_FRAMEPOINTER_REL.offsetFramePointer,
+ data.S_DEFRANGE_FRAMEPOINTER_REL.range.offsetStart,
+ data.S_DEFRANGE_FRAMEPOINTER_REL.range.isectionStart,
+ data.S_DEFRANGE_FRAMEPOINTER_REL.range.length);
+ }
+ else if(kind == SymbolRecordKind::S_DEFRANGE_SUBFIELD_REGISTER)
+ {
+ Printf(blockLevel, "S_DEFRANGE_SUBFIELD_REGISTER: Register %u | Parent offset 0x%X | Range Start 0x%X | Range Section Start 0x%X | Range Length %u\n",
+ data.S_DEFRANGE_SUBFIELD_REGISTER.reg,
+ data.S_DEFRANGE_SUBFIELD_REGISTER.offsetParent,
+ data.S_DEFRANGE_SUBFIELD_REGISTER.range.offsetStart,
+ data.S_DEFRANGE_SUBFIELD_REGISTER.range.isectionStart,
+ data.S_DEFRANGE_SUBFIELD_REGISTER.range.length);
+ }
+ else if (kind == SymbolRecordKind::S_DEFRANGE_FRAMEPOINTER_REL_FULL_SCOPE)
+ {
+ Printf(blockLevel, "S_DEFRANGE_FRAMEPOINTER_REL_FULL_SCOPE: Offset 0x%X\n", data.S_DEFRANGE_FRAMEPOINTER_REL_FULL_SCOPE.offsetFramePointer);
+ }
+ else if (kind == SymbolRecordKind::S_DEFRANGE_REGISTER_REL)
+ {
+ Printf(blockLevel, "S_DEFRANGE_REGISTER_REL: Base Register %u | Parent offset 0x%X | Base Register Offset 0x%X | Range Start 0x%X | Range Section Start 0x%X | Range Length %u\n",
+ data.S_DEFRANGE_REGISTER_REL.baseRegister,
+ data.S_DEFRANGE_REGISTER_REL.offsetParent,
+ data.S_DEFRANGE_REGISTER_REL.offsetBasePointer,
+ data.S_DEFRANGE_REGISTER_REL.offsetParent,
+ data.S_DEFRANGE_REGISTER_REL.range.offsetStart,
+ data.S_DEFRANGE_REGISTER_REL.range.isectionStart,
+ data.S_DEFRANGE_REGISTER_REL.range.length);
+ }
+ else if(kind == SymbolRecordKind::S_FILESTATIC)
+ {
+ Printf(blockLevel, "S_FILESTATIC: '%s'\n", data.S_FILESTATIC.name);
+ }
+ else if (kind == SymbolRecordKind::S_INLINESITE)
+ {
+ Printf(blockLevel, "S_INLINESITE: Parent 0x%X\n", data.S_INLINESITE.parent);
+ blockLevel++;
+ }
+ else if (kind == SymbolRecordKind::S_INLINESITE_END)
+ {
+ PDB_ASSERT(blockLevel > 0, "Block level for S_INLINESITE_END is 0");
+ blockLevel--;
+ Printf(blockLevel, "S_INLINESITE_END:\n");
+ }
+ else if (kind == SymbolRecordKind::S_CALLEES)
+ {
+ Printf(blockLevel, "S_CALLEES: Count %u\n", data.S_CALLEES.count);
+ }
+ else if (kind == SymbolRecordKind::S_CALLERS)
+ {
+ Printf(blockLevel, "S_CALLERS: Count %u\n", data.S_CALLERS.count);
+ }
+ else if (kind == SymbolRecordKind::S_INLINEES)
+ {
+ Printf(blockLevel, "S_INLINEES: Count %u\n", data.S_INLINEES.count);
+ }
+ else if (kind == SymbolRecordKind::S_LDATA32)
+ {
+ if (blockLevel > 0)
+ {
+ // Not sure why some type index 0 (T_NO_TYPE) are included in some PDBs.
+ if (data.S_LDATA32.typeIndex != 0) // PDB::CodeView::TPI::TypeIndexKind::T_NOTYPE)
+ {
+ const std::string typeName = GetVariableTypeName(typeTable, data.S_LDATA32.typeIndex);
+ Printf(blockLevel, "S_LDATA32: '%s' -> '%s'\n", data.S_LDATA32.name, typeName.c_str());
+ }
+ }
+ }
+ else if (kind == SymbolRecordKind::S_LTHREAD32)
+ {
+ if (blockLevel > 0)
+ {
+ const std::string typeName = GetVariableTypeName(typeTable, data.S_LTHREAD32.typeIndex);
+ Printf(blockLevel, "S_LTHREAD32: '%s' -> '%s'\n", data.S_LTHREAD32.name, typeName.c_str());
+ }
+ }
+ else if (kind == SymbolRecordKind::S_UDT)
+ {
+ const std::string typeName = GetVariableTypeName(typeTable, data.S_UDT.typeIndex);
+
+ Printf(blockLevel, "S_UDT: '%s' -> '%s'\n", data.S_UDT.name, typeName.c_str());
+ }
+ else if (kind == PDB::CodeView::DBI::SymbolRecordKind::S_REGISTER)
+ {
+ const std::string typeName = GetVariableTypeName(typeTable, data.S_REGSYM.typeIndex);
+
+ Printf(blockLevel, "S_REGSYM: '%s' -> '%s' | Register %i\n",
+ data.S_REGSYM.name, typeName.c_str(),
+ data.S_REGSYM.reg);
+ }
+ else if (kind == PDB::CodeView::DBI::SymbolRecordKind::S_BPREL32)
+ {
+ const std::string typeName = GetVariableTypeName(typeTable, data.S_BPRELSYM32.typeIndex);
+
+ Printf(blockLevel, "S_BPRELSYM32: '%s' -> '%s' | BP register Offset 0x%X\n",
+ data.S_BPRELSYM32.name, typeName.c_str(),
+ data.S_BPRELSYM32.offset);
+ }
+ else if (kind == PDB::CodeView::DBI::SymbolRecordKind::S_REGREL32)
+ {
+ const std::string typeName = GetVariableTypeName(typeTable, data.S_REGREL32.typeIndex);
+
+ Printf(blockLevel, "S_REGREL32: '%s' -> '%s' | Register %i | Register Offset 0x%X\n",
+ data.S_REGREL32.name, typeName.c_str(),
+ data.S_REGREL32.reg,
+ data.S_REGREL32.offset);
+ }
+ else if(kind == SymbolRecordKind::S_FRAMECOOKIE)
+ {
+ Printf(blockLevel, "S_FRAMECOOKIE: Offset 0x%X | Register %u | Type %u\n",
+ data.S_FRAMECOOKIE.offset,
+ data.S_FRAMECOOKIE.reg,
+ data.S_FRAMECOOKIE.cookietype);
+ }
+ else if(kind == SymbolRecordKind::S_CALLSITEINFO)
+ {
+ const std::string typeName = GetVariableTypeName(typeTable, data.S_CALLSITEINFO.typeIndex);
+ Printf(blockLevel, "S_CALLSITEINFO: '%s' | Offset 0x%X | Section %u\n", typeName.c_str(), data.S_CALLSITEINFO.offset, data.S_CALLSITEINFO.section);
+ }
+ else if(kind == SymbolRecordKind::S_HEAPALLOCSITE)
+ {
+ const std::string typeName = GetVariableTypeName(typeTable, data.S_HEAPALLOCSITE.typeIndex);
+ Printf(blockLevel, "S_HEAPALLOCSITE: '%s' | Offset 0x%X | Section %u | Instruction Length %u\n", typeName.c_str(),
+ data.S_HEAPALLOCSITE.offset,
+ data.S_HEAPALLOCSITE.section,
+ data.S_HEAPALLOCSITE.instructionLength);
+ }
+ else if (kind == SymbolRecordKind::S_FRAMEPROC)
+ {
+ Printf(blockLevel, "S_FRAMEPROC: Size %u | Padding %u | Padding Offset 0x%X | Callee Registers Size %u\n",
+ data.S_FRAMEPROC.cbFrame,
+ data.S_FRAMEPROC.cbPad,
+ data.S_FRAMEPROC.offPad,
+ data.S_FRAMEPROC.cbSaveRegs);
+ }
+ else if (kind == SymbolRecordKind::S_ANNOTATION)
+ {
+ Printf(blockLevel, "S_ANNOTATION: Offset 0x%X | Count %u\n", data.S_ANNOTATIONSYM.offset, data.S_ANNOTATIONSYM.annotationsCount);
+ // print N null-terminated annotation strings, skipping their null-terminators to get to the next string
+ const char* annotation = data.S_ANNOTATIONSYM.annotations;
+ for (int i = 0; i < data.S_ANNOTATIONSYM.annotationsCount; ++i, annotation += strlen(annotation) + 1)
+ Printf(blockLevel + 1, "S_ANNOTATION.%u: %s\n", i, annotation);
+ PDB_ASSERT(annotation <= (const char*)record + record->header.size + sizeof(record->header.size),
+ "Annotation strings end beyond the record size %X; annotaions count: %u", record->header.size, data.S_ANNOTATIONSYM.annotationsCount);
+ }
+ else if (kind == SymbolRecordKind::S_THUNK32)
+ {
+ PDB_ASSERT(blockLevel == 0, "BlockLevel %u != 0", blockLevel);
+
+ if (data.S_THUNK32.thunk == PDB::CodeView::DBI::ThunkOrdinal::TrampolineIncremental)
+ {
+ // we have never seen incremental linking thunks stored inside a S_THUNK32 symbol, but better safe than sorry
+ const uint32_t rva = imageSectionStream.ConvertSectionOffsetToRVA(data.S_THUNK32.section, data.S_THUNK32.offset);
+ Printf(blockLevel, "Function: 'ILT/Thunk' | RVA 0x%X\n", rva);
+ }
+ else
+ {
+ const uint32_t rva = imageSectionStream.ConvertSectionOffsetToRVA(data.S_THUNK32.section, data.S_THUNK32.offset);
+ Printf(blockLevel, "S_THUNK32 Function '%s' | RVA 0x%X\n", data.S_THUNK32.name, rva);
+ blockLevel++;
+ }
+ }
+ else if (kind == SymbolRecordKind::S_TRAMPOLINE)
+ {
+ PDB_ASSERT(blockLevel == 0, "BlockLevel %u != 0", blockLevel);
+ // incremental linking thunks are stored in the linker module
+ const uint32_t rva = imageSectionStream.ConvertSectionOffsetToRVA(data.S_TRAMPOLINE.thunkSection, data.S_TRAMPOLINE.thunkOffset);
+ Printf(blockLevel, "Function 'ILT/Trampoline' | RVA 0x%X\n", rva);
+ }
+ else if (kind == SymbolRecordKind::S_LPROC32)
+ {
+ PDB_ASSERT(blockLevel == 0, "BlockLevel %u != 0", blockLevel);
+ const uint32_t rva = imageSectionStream.ConvertSectionOffsetToRVA(data.S_LPROC32.section, data.S_LPROC32.offset);
+ Printf(blockLevel, "S_LPROC32 Function '%s' | RVA 0x%X\n", data.S_LPROC32.name, rva);
+ blockLevel++;
+ }
+ else if (kind == SymbolRecordKind::S_GPROC32)
+ {
+ PDB_ASSERT(blockLevel == 0, "BlockLevel %u != 0", blockLevel);
+ const uint32_t rva = imageSectionStream.ConvertSectionOffsetToRVA(data.S_GPROC32.section, data.S_GPROC32.offset);
+ Printf(blockLevel, "S_GPROC32 Function '%s' | RVA 0x%X\n", data.S_GPROC32.name, rva);
+ blockLevel++;
+ }
+ else if (kind == SymbolRecordKind::S_LPROC32_ID)
+ {
+ PDB_ASSERT(blockLevel == 0, "BlockLevel %u != 0", blockLevel);
+ const uint32_t rva = imageSectionStream.ConvertSectionOffsetToRVA(data.S_LPROC32_ID.section, data.S_LPROC32_ID.offset);
+ Printf(blockLevel, "S_LPROC32_ID Function '%s' | RVA 0x%X\n", data.S_LPROC32_ID.name, rva);
+ blockLevel++;
+ }
+ else if (kind == SymbolRecordKind::S_GPROC32_ID)
+ {
+ PDB_ASSERT(blockLevel == 0, "BlockLevel %u != 0", blockLevel);
+ const uint32_t rva = imageSectionStream.ConvertSectionOffsetToRVA(data.S_GPROC32_ID.section, data.S_GPROC32_ID.offset);
+ Printf(blockLevel, "S_GPROC32_ID Function '%s' | RVA 0x%X\n", data.S_GPROC32_ID.name, rva);
+ blockLevel++;
+ }
+ else if (kind == SymbolRecordKind::S_REGREL32_INDIR)
+ {
+ const std::string typeName = GetVariableTypeName(typeTable, data.S_REGREL32_INDIR.typeIndex);
+
+ Printf(blockLevel, "S_REGREL32_INDIR: '%s' -> '%s' | Register %i | Unknown1 0x%X | Unknown2 0x%X\n",
+ data.S_REGREL32_INDIR.name, typeName.c_str(),
+ data.S_REGREL32_INDIR.unknown1,
+ data.S_REGREL32_INDIR.unknown1);
+ }
+ else if (kind == SymbolRecordKind::S_REGREL32_ENCTMP)
+ {
+ const std::string typeName = GetVariableTypeName(typeTable, data.S_REGREL32.typeIndex);
+
+ Printf(blockLevel, "S_REGREL32_ENCTMP: '%s' -> '%s' | Register %i | Register Offset 0x%X\n",
+ data.S_REGREL32.name, typeName.c_str(),
+ data.S_REGREL32.reg,
+ data.S_REGREL32.offset);
+ }
+ else if (kind == SymbolRecordKind::S_UNAMESPACE)
+ {
+ Printf(blockLevel, "S_UNAMESPACE: '%s'\n", data.S_UNAMESPACE.name);
+ }
+ else if (kind == SymbolRecordKind::S_ARMSWITCHTABLE)
+ {
+ Printf(blockLevel, "S_ARMSWITCHTABLE: "
+ "Switch Type: %u | Num Entries: %u | Base Section: %u | Base Offset: 0x%X | "
+ "Branch Section: %u | Branch Offset: 0x%X | Table Section: %u | Table Offset: 0x%X\n",
+ data.S_ARMSWITCHTABLE.switchType,
+ data.S_ARMSWITCHTABLE.numEntries,
+ data.S_ARMSWITCHTABLE.sectionBase,
+ data.S_ARMSWITCHTABLE.offsetBase,
+ data.S_ARMSWITCHTABLE.sectionBranch,
+ data.S_ARMSWITCHTABLE.offsetBranch,
+ data.S_ARMSWITCHTABLE.sectionTable,
+ data.S_ARMSWITCHTABLE.offsetTable);
+ }
+ else
+ {
+ // We only care about records inside functions.
+ if (blockLevel > 0)
+ {
+ PDB_ASSERT(false, "Unhandled record kind 0x%X with block level %u\n", static_cast<uint16_t>(kind), blockLevel);
+ }
+ }
+
+ recordCount++;
+
+ });
+ }
+
+ scope.Done(recordCount);
+ }
+}
diff --git a/thirdparty/raw_pdb/src/Examples/ExampleIPI.cpp b/thirdparty/raw_pdb/src/Examples/ExampleIPI.cpp
new file mode 100644
index 000000000..5286689e9
--- /dev/null
+++ b/thirdparty/raw_pdb/src/Examples/ExampleIPI.cpp
@@ -0,0 +1,198 @@
+// Copyright 2011-2022, Molecular Matters GmbH <[email protected]>
+// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
+
+#include "Examples_PCH.h"
+#include "ExampleTimedScope.h"
+#include "ExampleTypeTable.h"
+#include "PDB_RawFile.h"
+#include "PDB_InfoStream.h"
+#include "PDB_IPIStream.h"
+#include "PDB_TPIStream.h"
+
+static std::string GetTypeNameIPI(const TypeTable& typeTable, uint32_t typeIndex)
+{
+ // Defined in ExampleTypes.cpp
+ extern std::string GetTypeName(const TypeTable & typeTable, uint32_t typeIndex);
+
+ std::string typeName = GetTypeName(typeTable, typeIndex);
+
+ // Remove any '%s' substring used to insert a variable/field name.
+ const uint64_t markerPos = typeName.find("%s");
+ if (markerPos != typeName.npos)
+ {
+ typeName.erase(markerPos, 2);
+ }
+
+ return typeName;
+}
+
+void ExampleIPI(const PDB::RawFile& rawPdbFile, const PDB::InfoStream& infoStream, const PDB::TPIStream& tpiStream, const PDB::IPIStream& ipiStream);
+
+void ExampleIPI(const PDB::RawFile& rawPdbFile, const PDB::InfoStream& infoStream, const PDB::TPIStream& tpiStream, const PDB::IPIStream& ipiStream)
+{
+ if (!infoStream.HasIPIStream())
+ {
+ return;
+ }
+
+ TimedScope total("\nRunning example \"IPI\"");
+
+ TimedScope typeTableScope("Create TypeTable");
+ TypeTable typeTable(tpiStream);
+ typeTableScope.Done();
+
+ // prepare names stream for grabbing file paths from lines
+ TimedScope namesScope("Reading names stream");
+ const PDB::NamesStream namesStream = infoStream.CreateNamesStream(rawPdbFile);
+ namesScope.Done();
+
+ const uint32_t firstTypeIndex = ipiStream.GetFirstTypeIndex();
+
+ PDB::ArrayView<const PDB::CodeView::IPI::Record*> records = ipiStream.GetTypeRecords();
+
+ std::vector<const char*> strings;
+
+ strings.resize(records.GetLength(), nullptr);
+
+ size_t index = 0;
+
+ for (const PDB::CodeView::IPI::Record* record : records)
+ {
+ const PDB::CodeView::IPI::RecordHeader& header = record->header;
+
+ if (header.kind == PDB::CodeView::IPI::TypeRecordKind::LF_STRING_ID)
+ {
+ strings[index] = record->data.LF_STRING_ID.name;
+ }
+
+ index++;
+ }
+
+ uint32_t identifier = firstTypeIndex;
+
+ std::string typeName, parentTypeName;
+
+ printf("\n --- IPI Records ---\n\n");
+
+ for(const PDB::CodeView::IPI::Record* record : records)
+ {
+ const PDB::CodeView::IPI::RecordHeader& header = record->header;
+
+ if (header.kind == PDB::CodeView::IPI::TypeRecordKind::LF_FUNC_ID)
+ {
+ typeName = GetTypeNameIPI(typeTable, record->data.LF_FUNC_ID.typeIndex);
+
+ printf("Kind: 'LF_FUNC_ID' Size: %i ID: %u\n", header.size, identifier);
+ printf(" Scope ID: %u\n Type: '%s'\n Name: '%s'\n\n",
+ record->data.LF_FUNC_ID.scopeId,
+ typeName.c_str(),
+ record->data.LF_FUNC_ID.name);
+
+ }
+ else if (header.kind == PDB::CodeView::IPI::TypeRecordKind::LF_MFUNC_ID)
+ {
+ typeName = GetTypeNameIPI(typeTable, record->data.LF_MFUNC_ID.typeIndex);
+ parentTypeName = GetTypeNameIPI(typeTable, record->data.LF_MFUNC_ID.parentTypeIndex);
+
+ printf("Kind: 'LF_MFUNC_ID' Size: %i ID: %u\n", header.size, identifier);
+ printf(" Parent Type: '%s'\n Type: '%s'\n Name: '%s'\n\n",
+ parentTypeName.c_str(),
+ typeName.c_str(),
+ record->data.LF_MFUNC_ID.name);
+
+ }
+ else if (header.kind == PDB::CodeView::IPI::TypeRecordKind::LF_BUILDINFO)
+ {
+ printf("Kind: 'LF_BUILDINFO' Size: %u ID: %u\n", header.size, identifier);
+
+ if (record->data.LF_BUILDINFO.count == 0)
+ {
+ continue;
+ }
+
+ printf("Strings: '%s'", strings[record->data.LF_BUILDINFO.typeIndices[0] - firstTypeIndex]);
+
+ for (uint32_t i = 1, size = record->data.LF_BUILDINFO.count; i < size; ++i)
+ {
+ const uint32_t stringIndex = record->data.LF_BUILDINFO.typeIndices[i];
+
+ if (stringIndex == 0)
+ {
+ printf(", ''");
+ }
+ else
+ {
+ printf(", '%s'", strings[stringIndex - firstTypeIndex]);
+ }
+ }
+
+ printf("\n\n");
+ }
+ else if (header.kind == PDB::CodeView::IPI::TypeRecordKind::LF_SUBSTR_LIST)
+ {
+ printf("Kind: 'LF_SUBSTR_LIST' Size: %u ID: %u\n", header.size, identifier);
+
+ if (record->data.LF_SUBSTR_LIST.count == 0)
+ {
+ continue;
+ }
+
+ printf(" Strings: '%s'", strings[record->data.LF_SUBSTR_LIST.typeIndices[0] - firstTypeIndex]);
+
+ for (uint32_t i = 1, size = record->data.LF_SUBSTR_LIST.count; i < size; ++i)
+ {
+ const uint32_t stringIndex = record->data.LF_SUBSTR_LIST.typeIndices[i];
+
+ if (stringIndex == 0)
+ {
+ printf(", ''");
+ }
+ else
+ {
+ printf(", '%s'", strings[stringIndex - firstTypeIndex]);
+ }
+ }
+
+ printf("\n\n");
+ }
+ else if (header.kind == PDB::CodeView::IPI::TypeRecordKind::LF_STRING_ID)
+ {
+ printf("Kind: 'LF_STRING_ID' Size: %u ID: %u\n", header.size, identifier);
+
+ printf(" Substring ID: %u\n Name: '%s'\n\n", record->data.LF_STRING_ID.id, record->data.LF_STRING_ID.name);
+ }
+ else if (header.kind == PDB::CodeView::IPI::TypeRecordKind::LF_UDT_SRC_LINE)
+ {
+ typeName = GetTypeNameIPI(typeTable, record->data.LF_UDT_SRC_LINE.typeIndex);
+
+ const uint32_t stringIndex = record->data.LF_UDT_SRC_LINE.stringIndex;
+
+ printf("Kind: 'LF_UDT_SRC_LINE' Size: %u ID: %u\n", header.size, identifier);
+
+ printf(" Type: '%s'\n Source Path: %s\n Line: %u\n\n",
+ typeName.c_str(),
+ strings[stringIndex - firstTypeIndex],
+ record->data.LF_UDT_SRC_LINE.line);
+ }
+ else if (header.kind == PDB::CodeView::IPI::TypeRecordKind::LF_UDT_MOD_SRC_LINE)
+ {
+ typeName = GetTypeNameIPI(typeTable, record->data.LF_UDT_MOD_SRC_LINE.typeIndex);
+
+ const char* string = namesStream.GetFilename(record->data.LF_UDT_MOD_SRC_LINE.stringIndex);
+
+ printf("Kind: 'LF_UDT_SRC_LINE' Size: %u ID: %u\n", header.size, identifier);
+
+ printf(" Type: '%s'\n Source Path: %s\n Line: %u\n Module Index: %u\n\n",
+ typeName.c_str(),
+ string,
+ record->data.LF_UDT_MOD_SRC_LINE.line,
+ record->data.LF_UDT_MOD_SRC_LINE.moduleIndex);
+ }
+ else
+ {
+ printf("Kind: 0x%X Size: %u ID: %u\n\n", static_cast<uint32_t>(header.kind), header.size, identifier);
+ }
+
+ identifier++;
+ }
+}
diff --git a/thirdparty/raw_pdb/src/Examples/ExampleLines.cpp b/thirdparty/raw_pdb/src/Examples/ExampleLines.cpp
new file mode 100644
index 000000000..f055b98c5
--- /dev/null
+++ b/thirdparty/raw_pdb/src/Examples/ExampleLines.cpp
@@ -0,0 +1,268 @@
+// Copyright 2011-2022, Molecular Matters GmbH <[email protected]>
+// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
+
+#include "Examples_PCH.h"
+#include "ExampleTimedScope.h"
+#include "Foundation/PDB_PointerUtil.h"
+#include "PDB_RawFile.h"
+#include "PDB_DBIStream.h"
+#include "PDB_InfoStream.h"
+
+#include <cstring>
+
+namespace
+{
+ struct Section
+ {
+ uint16_t index;
+ uint32_t offset;
+ size_t lineIndex;
+ };
+
+ struct Filename
+ {
+ uint32_t fileChecksumOffset;
+ uint32_t namesFilenameOffset;
+ PDB::CodeView::DBI::ChecksumKind checksumKind;
+ uint8_t checksumSize;
+ uint8_t checksum[32];
+ };
+
+ struct Line
+ {
+ uint32_t lineNumber;
+ uint32_t codeSize;
+ size_t filenameIndex;
+ };
+}
+
+void ExampleLines(const PDB::RawFile& rawPdbFile, const PDB::DBIStream& dbiStream, const PDB::InfoStream& infoStream);
+void ExampleLines(const PDB::RawFile& rawPdbFile, const PDB::DBIStream& dbiStream, const PDB::InfoStream& infoStream)
+{
+ if (!infoStream.HasNamesStream())
+ {
+ printf("PDB has no '/names' stream for looking up filenames for lines, skipping \"Lines\" example.");
+ return;
+ }
+
+ TimedScope total("\nRunning example \"Lines\"");
+
+ // prepare the image section stream first. it is needed for converting section + offset into an RVA
+ TimedScope sectionScope("Reading image section stream");
+ const PDB::ImageSectionStream imageSectionStream = dbiStream.CreateImageSectionStream(rawPdbFile);
+ sectionScope.Done();
+
+ // prepare the module info stream for grabbing function symbols from modules
+ TimedScope moduleScope("Reading module info stream");
+ const PDB::ModuleInfoStream moduleInfoStream = dbiStream.CreateModuleInfoStream(rawPdbFile);
+ moduleScope.Done();
+
+ // prepare names stream for grabbing file paths from lines
+ TimedScope namesScope("Reading names stream");
+ const PDB::NamesStream namesStream = infoStream.CreateNamesStream(rawPdbFile);
+ namesScope.Done();
+
+ // keeping sections and lines separate, as sorting the smaller Section struct is 2x faster in release builds
+ // than having all the fields in one big Line struct and sorting those.
+ std::vector<Section> sections;
+ std::vector<Filename> filenames;
+ std::vector<Line> lines;
+
+ {
+ TimedScope scope("Storing lines from modules");
+
+ const PDB::ArrayView<PDB::ModuleInfoStream::Module> modules = moduleInfoStream.GetModules();
+
+ for (const PDB::ModuleInfoStream::Module& module : modules)
+ {
+ if (!module.HasLineStream())
+ {
+ continue;
+ }
+
+ const PDB::ModuleLineStream moduleLineStream = module.CreateLineStream(rawPdbFile);
+
+ const size_t moduleFilenamesStartIndex = filenames.size();
+ const PDB::CodeView::DBI::FileChecksumHeader* moduleFileChecksumHeader = nullptr;
+
+ moduleLineStream.ForEachSection([&moduleLineStream, &namesStream, &moduleFileChecksumHeader, &sections, &filenames, &lines](const PDB::CodeView::DBI::LineSection* lineSection)
+ {
+ if (lineSection->header.kind == PDB::CodeView::DBI::DebugSubsectionKind::S_LINES)
+ {
+ moduleLineStream.ForEachLinesBlock(lineSection,
+ [&lineSection, &sections, &filenames, &lines](const PDB::CodeView::DBI::LinesFileBlockHeader* linesBlockHeader, const PDB::CodeView::DBI::Line* blocklines, const PDB::CodeView::DBI::Column* blockColumns)
+ {
+ if (linesBlockHeader->numLines == 0)
+ {
+ return;
+ }
+
+ const PDB::CodeView::DBI::Line& firstLine = blocklines[0];
+
+ const uint16_t sectionIndex = lineSection->linesHeader.sectionIndex;
+ const uint32_t sectionOffset = lineSection->linesHeader.sectionOffset;
+ const uint32_t fileChecksumOffset = linesBlockHeader->fileChecksumOffset;
+
+ const size_t filenameIndex = filenames.size();
+
+ // there will be duplicate filenames for any real world pdb.
+ // ideally the filenames would be stored in a map with the filename or checksum as the key.
+ // but that would complicate the logic in this example and therefore just use a vector to make it easier to understand.
+ filenames.push_back({ fileChecksumOffset, 0, PDB::CodeView::DBI::ChecksumKind::None, 0, {0} });
+
+ sections.push_back({ sectionIndex, sectionOffset, lines.size() });
+
+ // initially set code size of first line to 0, will be updated in loop below.
+ lines.push_back({ firstLine.linenumStart, 0, filenameIndex });
+
+ for(uint32_t i = 1, size = linesBlockHeader->numLines; i < size; ++i)
+ {
+ const PDB::CodeView::DBI::Line& line = blocklines[i];
+
+ // calculate code size of previous line by using the current line offset.
+ lines.back().codeSize = line.offset - blocklines[i-1].offset;
+
+ sections.push_back({ sectionIndex, sectionOffset + line.offset, lines.size() });
+ lines.push_back({ line.linenumStart, 0, filenameIndex });
+ }
+
+ // calc code size of last line
+ lines.back().codeSize = lineSection->linesHeader.codeSize - blocklines[linesBlockHeader->numLines-1].offset;
+
+ // columns are optional
+ if (blockColumns == nullptr)
+ {
+ return;
+ }
+
+ for (uint32_t i = 0, size = linesBlockHeader->numLines; i < size; ++i)
+ {
+ const PDB::CodeView::DBI::Column& column = blockColumns[i];
+ (void)column;
+ }
+ });
+ }
+ else if (lineSection->header.kind == PDB::CodeView::DBI::DebugSubsectionKind::S_FILECHECKSUMS)
+ {
+ // how to read checksums and their filenames from the Names Stream
+ moduleLineStream.ForEachFileChecksum(lineSection, [&namesStream](const PDB::CodeView::DBI::FileChecksumHeader* fileChecksumHeader)
+ {
+ const char* filename = namesStream.GetFilename(fileChecksumHeader->filenameOffset);
+ (void)filename;
+ });
+
+ // store the checksum header for the module, as there might be more lines after the checksums.
+ // so lines will get their checksum header values assigned after processing all line sections in the module.
+ PDB_ASSERT(moduleFileChecksumHeader == nullptr, "Module File Checksum Header already set");
+ moduleFileChecksumHeader = &lineSection->checksumHeader;
+ }
+ else if (lineSection->header.kind == PDB::CodeView::DBI::DebugSubsectionKind::S_INLINEELINES)
+ {
+ if (lineSection->inlineeHeader.kind == PDB::CodeView::DBI::InlineeSourceLineKind::Signature)
+ {
+ moduleLineStream.ForEachInlineeSourceLine(lineSection, [](const PDB::CodeView::DBI::InlineeSourceLine* inlineeSourceLine)
+ {
+ (void)inlineeSourceLine;
+
+ });
+ }
+ else
+ {
+ moduleLineStream.ForEachInlineeSourceLineEx(lineSection, [](const PDB::CodeView::DBI::InlineeSourceLineEx* inlineeSourceLineEx)
+ {
+ for (uint32_t i = 0; i < inlineeSourceLineEx->extraLines; ++i)
+ {
+ const uint32_t checksumOffset = inlineeSourceLineEx->extrafileChecksumOffsets[i];
+ (void)checksumOffset;
+ }
+ });
+ }
+ }
+ else
+ {
+ PDB_ASSERT(false, "Line Section kind 0x%X not handled", static_cast<uint32_t>(lineSection->header.kind));
+ }
+ });
+
+ // assign checksum values for each filename added in this module
+ for (size_t i = moduleFilenamesStartIndex, size = filenames.size(); i < size; ++i)
+ {
+ Filename& filename = filenames[i];
+
+ // look up the filename's checksum header in the module's checksums section
+ const PDB::CodeView::DBI::FileChecksumHeader* checksumHeader = PDB::Pointer::Offset<const PDB::CodeView::DBI::FileChecksumHeader*>(moduleFileChecksumHeader, filename.fileChecksumOffset);
+
+ PDB_ASSERT(checksumHeader->checksumKind >= PDB::CodeView::DBI::ChecksumKind::None &&
+ checksumHeader->checksumKind <= PDB::CodeView::DBI::ChecksumKind::SHA256,
+ "Invalid checksum kind %u", static_cast<uint16_t>(checksumHeader->checksumKind));
+
+ // store checksum values in filname struct
+ filename.namesFilenameOffset = checksumHeader->filenameOffset;
+ filename.checksumKind = checksumHeader->checksumKind;
+ filename.checksumSize = checksumHeader->checksumSize;
+ std::memcpy(filename.checksum, checksumHeader->checksum, checksumHeader->checksumSize);
+ }
+ }
+
+ scope.Done(modules.GetLength());
+
+ TimedScope sortScope("std::sort sections");
+
+ // sort sections, so we can iterate over lines by address order.
+ std::sort(sections.begin(), sections.end(), [](const Section& lhs, const Section& rhs)
+ {
+ if (lhs.index == rhs.index)
+ {
+ return lhs.offset < rhs.offset;
+ }
+
+ return lhs.index < rhs.index;
+ });
+
+ sortScope.Done(sections.size());
+
+// Disabled by default, as it will print a lot of lines for large PDBs :-)
+#if 0
+ // DIA2Dump style lines output
+ static const char hexChars[17] = "0123456789ABCDEF";
+ char checksumString[128];
+
+ printf("*** LINES RAW PDB\n");
+
+ const char* prevFilename = nullptr;
+
+ for (const Section& section : sections)
+ {
+ const Line& line = lines[section.lineIndex];
+ const Filename& lineFilename = filenames[line.filenameIndex];
+
+ const char* filename = namesStream.GetFilename(lineFilename.namesFilenameOffset);
+
+ const uint32_t rva = imageSectionStream.ConvertSectionOffsetToRVA(section.index, section.offset);
+
+ // only print filename for a line if it is different from the previous one.
+ if (filename != prevFilename)
+ {
+ for (size_t i = 0, j = 0; i < lineFilename.checksumSize; i++, j+=2)
+ {
+ checksumString[j] = hexChars[lineFilename.checksum[i] >> 4];
+ checksumString[j+1] = hexChars[lineFilename.checksum[i] & 0xF];
+ }
+
+ checksumString[lineFilename.checksumSize * 2] = '\0';
+
+ printf(" line %u at [0x%08X][0x%04X:0x%08X], len = 0x%X %s (0x%02X: %s)\n",
+ line.lineNumber, rva, section.index, section.offset, line.codeSize,
+ filename, static_cast<uint32_t>(lineFilename.checksumKind), checksumString);
+
+ prevFilename = filename;
+ }
+ else
+ {
+ printf(" line %u at [0x%08X][0x%04X:0x%08X], len = 0x%X\n",
+ line.lineNumber, rva, section.index, section.offset, line.codeSize);
+ }
+ }
+#endif
+ }
+}
diff --git a/thirdparty/raw_pdb/src/Examples/ExampleMain.cpp b/thirdparty/raw_pdb/src/Examples/ExampleMain.cpp
new file mode 100644
index 000000000..b4249422f
--- /dev/null
+++ b/thirdparty/raw_pdb/src/Examples/ExampleMain.cpp
@@ -0,0 +1,200 @@
+// Copyright 2011-2022, Molecular Matters GmbH <[email protected]>
+// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
+
+#include "Examples_PCH.h"
+#include "ExampleMemoryMappedFile.h"
+#include "PDB.h"
+#include "PDB_RawFile.h"
+#include "PDB_InfoStream.h"
+#include "PDB_DBIStream.h"
+#include "PDB_TPIStream.h"
+#include "PDB_IPIStream.h"
+#include "PDB_NamesStream.h"
+
+namespace
+{
+ PDB_NO_DISCARD static bool IsError(PDB::ErrorCode errorCode)
+ {
+ switch (errorCode)
+ {
+ case PDB::ErrorCode::Success:
+ return false;
+
+ case PDB::ErrorCode::InvalidSuperBlock:
+ printf("Invalid Superblock\n");
+ return true;
+
+ case PDB::ErrorCode::InvalidFreeBlockMap:
+ printf("Invalid free block map\n");
+ return true;
+
+ case PDB::ErrorCode::InvalidStream:
+ printf("Invalid stream\n");
+ return true;
+
+ case PDB::ErrorCode::InvalidSignature:
+ printf("Invalid stream signature\n");
+ return true;
+
+ case PDB::ErrorCode::InvalidStreamIndex:
+ printf("Invalid stream index\n");
+ return true;
+
+ case PDB::ErrorCode::InvalidDataSize:
+ printf("Invalid data size\n");
+ return true;
+
+ case PDB::ErrorCode::UnknownVersion:
+ printf("Unknown version\n");
+ return true;
+ }
+
+ // only ErrorCode::Success means there wasn't an error, so all other paths have to assume there was an error
+ return true;
+ }
+
+ PDB_NO_DISCARD static bool HasValidDBIStreams(const PDB::RawFile& rawPdbFile, const PDB::DBIStream& dbiStream)
+ {
+ // check whether the DBI stream offers all sub-streams we need
+ if (IsError(dbiStream.HasValidSymbolRecordStream(rawPdbFile)))
+ {
+ return false;
+ }
+
+ if (IsError(dbiStream.HasValidPublicSymbolStream(rawPdbFile)))
+ {
+ return false;
+ }
+
+ if (IsError(dbiStream.HasValidGlobalSymbolStream(rawPdbFile)))
+ {
+ return false;
+ }
+
+ if (IsError(dbiStream.HasValidSectionContributionStream(rawPdbFile)))
+ {
+ return false;
+ }
+
+ if (IsError(dbiStream.HasValidImageSectionStream(rawPdbFile)))
+ {
+ return false;
+ }
+
+ return true;
+ }
+}
+
+
+// declare all examples
+extern void ExamplePDBSize(const PDB::RawFile&, const PDB::DBIStream&);
+extern void ExampleTPISize(const PDB::TPIStream& tpiStream, const char* outPath);
+extern void ExampleContributions(const PDB::RawFile&, const PDB::DBIStream&);
+extern void ExampleSymbols(const PDB::RawFile&, const PDB::DBIStream&);
+extern void ExampleFunctionSymbols(const PDB::RawFile&, const PDB::DBIStream&);
+extern void ExampleFunctionVariables(const PDB::RawFile& rawPdbFile, const PDB::DBIStream& dbiStream, const PDB::TPIStream&);
+extern void ExampleLines(const PDB::RawFile& rawPdbFile, const PDB::DBIStream& dbiStream, const PDB::InfoStream& infoStream);
+extern void ExampleTypes(const PDB::TPIStream&);
+extern void ExampleIPI(const PDB::RawFile& rawPdbFile, const PDB::InfoStream& infoStream, const PDB::TPIStream& tpiStream, const PDB::IPIStream& ipiStream);
+
+int main(int argc, char** argv)
+{
+ if (argc != 2)
+ {
+ printf("Usage: Examples <PDB path>\nError: Incorrect usage\n");
+
+ return 1;
+ }
+
+ printf("Opening PDB file %s\n", argv[1]);
+
+ // try to open the PDB file and check whether all the data we need is available
+ MemoryMappedFile::Handle pdbFile = MemoryMappedFile::Open(argv[1]);
+ if (!pdbFile.baseAddress)
+ {
+ printf("Cannot memory-map file %s\n", argv[1]);
+
+ return 1;
+ }
+
+ if (IsError(PDB::ValidateFile(pdbFile.baseAddress, pdbFile.len)))
+ {
+ MemoryMappedFile::Close(pdbFile);
+
+ return 2;
+ }
+
+ const PDB::RawFile rawPdbFile = PDB::CreateRawFile(pdbFile.baseAddress);
+ if (IsError(PDB::HasValidDBIStream(rawPdbFile)))
+ {
+ MemoryMappedFile::Close(pdbFile);
+
+ return 3;
+ }
+
+ const PDB::InfoStream infoStream(rawPdbFile);
+ if (infoStream.UsesDebugFastLink())
+ {
+ printf("PDB was linked using unsupported option /DEBUG:FASTLINK\n");
+
+ MemoryMappedFile::Close(pdbFile);
+
+ return 4;
+ }
+
+ const auto h = infoStream.GetHeader();
+ printf("Version %u, signature %u, age %u, GUID %08x-%04x-%04x-%02x%02x%02x%02x%02x%02x%02x%02x\n",
+ static_cast<uint32_t>(h->version), h->signature, h->age,
+ h->guid.Data1, h->guid.Data2, h->guid.Data3,
+ h->guid.Data4[0], h->guid.Data4[1], h->guid.Data4[2], h->guid.Data4[3], h->guid.Data4[4], h->guid.Data4[5], h->guid.Data4[6], h->guid.Data4[7]);
+
+ const PDB::DBIStream dbiStream = PDB::CreateDBIStream(rawPdbFile);
+ if (!HasValidDBIStreams(rawPdbFile, dbiStream))
+ {
+ MemoryMappedFile::Close(pdbFile);
+
+ return 5;
+ }
+
+ if (IsError(PDB::HasValidTPIStream(rawPdbFile)))
+ {
+ MemoryMappedFile::Close(pdbFile);
+
+ return 5;
+ }
+ const PDB::TPIStream tpiStream = PDB::CreateTPIStream(rawPdbFile);
+
+ PDB::IPIStream ipiStream;
+
+ // It's perfectly possible that an old PDB does not have an IPI stream.
+ if(infoStream.HasIPIStream())
+ {
+ PDB::ErrorCode error = PDB::HasValidIPIStream(rawPdbFile);
+
+ if (error != PDB::ErrorCode::InvalidStream && IsError(error))
+ {
+ MemoryMappedFile::Close(pdbFile);
+
+ return 5;
+ }
+
+ ipiStream = PDB::CreateIPIStream(rawPdbFile);
+ }
+
+
+ // run all examples
+ ExamplePDBSize(rawPdbFile, dbiStream);
+ ExampleContributions(rawPdbFile, dbiStream);
+ ExampleSymbols(rawPdbFile, dbiStream);
+ ExampleFunctionSymbols(rawPdbFile, dbiStream);
+ ExampleFunctionVariables(rawPdbFile, dbiStream, tpiStream);
+ ExampleLines(rawPdbFile, dbiStream, infoStream);
+ ExampleTypes(tpiStream);
+ ExampleIPI(rawPdbFile, infoStream, tpiStream, ipiStream);
+ // uncomment to dump type sizes to a CSV
+ // ExampleTPISize(tpiStream, "output.csv");
+
+ MemoryMappedFile::Close(pdbFile);
+
+ return 0;
+}
diff --git a/thirdparty/raw_pdb/src/Examples/ExampleMemoryMappedFile.cpp b/thirdparty/raw_pdb/src/Examples/ExampleMemoryMappedFile.cpp
new file mode 100644
index 000000000..4b46b4bab
--- /dev/null
+++ b/thirdparty/raw_pdb/src/Examples/ExampleMemoryMappedFile.cpp
@@ -0,0 +1,100 @@
+// Copyright 2011-2022, Molecular Matters GmbH <[email protected]>
+// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
+
+#include "Examples_PCH.h"
+#include "ExampleMemoryMappedFile.h"
+
+
+MemoryMappedFile::Handle MemoryMappedFile::Open(const char* path)
+{
+#ifdef _WIN32
+ void* file = CreateFileA(path, GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_READONLY, nullptr);
+
+ if (file == INVALID_HANDLE_VALUE)
+ {
+ return Handle { INVALID_HANDLE_VALUE, INVALID_HANDLE_VALUE, nullptr, 0 };
+ }
+
+ void* fileMapping = CreateFileMappingW(file, nullptr, PAGE_READONLY, 0, 0, nullptr);
+
+ if (fileMapping == nullptr)
+ {
+ CloseHandle(file);
+
+ return Handle { INVALID_HANDLE_VALUE, INVALID_HANDLE_VALUE, nullptr, 0 };
+ }
+
+ void* baseAddress = MapViewOfFile(fileMapping, FILE_MAP_READ, 0, 0, 0);
+
+ if (baseAddress == nullptr)
+ {
+ CloseHandle(fileMapping);
+ CloseHandle(file);
+
+ return Handle { INVALID_HANDLE_VALUE, INVALID_HANDLE_VALUE, nullptr, 0 };
+ }
+
+ BY_HANDLE_FILE_INFORMATION fileInformation;
+ const bool getInformationResult = GetFileInformationByHandle(file, &fileInformation);
+ if (!getInformationResult)
+ {
+ UnmapViewOfFile(baseAddress);
+ CloseHandle(fileMapping);
+ CloseHandle(file);
+
+ return Handle { INVALID_HANDLE_VALUE, INVALID_HANDLE_VALUE, nullptr, 0 };
+ }
+
+ const size_t fileSizeHighBytes = static_cast<size_t>(fileInformation.nFileSizeHigh) << 32;
+ const size_t fileSizeLowBytes = fileInformation.nFileSizeLow;
+ const size_t fileSize = fileSizeHighBytes | fileSizeLowBytes;
+ return Handle { file, fileMapping, baseAddress, fileSize };
+#else
+ struct stat fileSb;
+
+ int file = open(path, O_RDONLY);
+
+ if (file == INVALID_HANDLE_VALUE)
+ {
+ return Handle { INVALID_HANDLE_VALUE, nullptr, 0 };
+ }
+
+ if (fstat(file, &fileSb) == -1)
+ {
+ close(file);
+
+ return Handle { INVALID_HANDLE_VALUE, nullptr, 0 };
+ }
+
+ void* baseAddress = mmap(nullptr, fileSb.st_size, PROT_READ, MAP_PRIVATE, file, 0);
+
+ if (baseAddress == MAP_FAILED)
+ {
+ close(file);
+
+ return Handle { INVALID_HANDLE_VALUE, nullptr, 0 };
+ }
+
+ return Handle { file, baseAddress, static_cast<size_t>(fileSb.st_size) };
+#endif
+}
+
+
+void MemoryMappedFile::Close(Handle& handle)
+{
+#ifdef _WIN32
+ UnmapViewOfFile(handle.baseAddress);
+ CloseHandle(handle.fileMapping);
+ CloseHandle(handle.file);
+
+ handle.file = nullptr;
+ handle.fileMapping = nullptr;
+#else
+ munmap(handle.baseAddress, handle.len);
+ close(handle.file);
+
+ handle.file = 0;
+#endif
+
+ handle.baseAddress = nullptr;
+}
diff --git a/thirdparty/raw_pdb/src/Examples/ExampleMemoryMappedFile.h b/thirdparty/raw_pdb/src/Examples/ExampleMemoryMappedFile.h
new file mode 100644
index 000000000..c14575336
--- /dev/null
+++ b/thirdparty/raw_pdb/src/Examples/ExampleMemoryMappedFile.h
@@ -0,0 +1,29 @@
+// Copyright 2011-2022, Molecular Matters GmbH <[email protected]>
+// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
+
+#ifndef _WIN32
+#include <sys/mman.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+#define INVALID_HANDLE_VALUE ((long)-1)
+#endif
+
+namespace MemoryMappedFile
+{
+ struct Handle
+ {
+#ifdef _WIN32
+ void* file;
+ void* fileMapping;
+#else
+ int file;
+#endif
+ void* baseAddress;
+ size_t len;
+ };
+
+ Handle Open(const char* path);
+ void Close(Handle& handle);
+}
diff --git a/thirdparty/raw_pdb/src/Examples/ExamplePDBSize.cpp b/thirdparty/raw_pdb/src/Examples/ExamplePDBSize.cpp
new file mode 100644
index 000000000..c0a4dc6a4
--- /dev/null
+++ b/thirdparty/raw_pdb/src/Examples/ExamplePDBSize.cpp
@@ -0,0 +1,124 @@
+// Copyright 2011-2022, Molecular Matters GmbH <[email protected]>
+// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
+
+#include "Examples_PCH.h"
+#include "ExampleTimedScope.h"
+#include "PDB_RawFile.h"
+#include "PDB_DBIStream.h"
+
+
+namespace
+{
+ struct Stream
+ {
+ std::string name;
+ uint32_t size;
+ };
+}
+
+
+void ExamplePDBSize(const PDB::RawFile& rawPdbFile, const PDB::DBIStream& dbiStream);
+void ExamplePDBSize(const PDB::RawFile& rawPdbFile, const PDB::DBIStream& dbiStream)
+{
+ TimedScope total("\nRunning example \"PDBSize\"");
+
+ std::vector<Stream> streams;
+
+ // print show general statistics
+ printf("General\n");
+ printf("-------\n");
+ {
+ const PDB::SuperBlock* superBlock = rawPdbFile.GetSuperBlock();
+ printf("PDB page size (block size): %u\n", superBlock->blockSize);
+ printf("PDB block count: %u\n", superBlock->blockCount);
+
+ const size_t rawSize = static_cast<size_t>(superBlock->blockSize) * static_cast<size_t>(superBlock->blockCount);
+ printf("PDB raw size: %zu MiB (%zu GiB)\n", rawSize >> 20u, rawSize >> 30u);
+ }
+
+ // print the sizes of all known streams
+ printf("\n");
+ printf("Sizes of known streams\n");
+ printf("----------------------\n");
+ {
+ const uint32_t streamCount = rawPdbFile.GetStreamCount();
+ const uint32_t tpiStreamSize = (streamCount > 2u) ? rawPdbFile.GetStreamSize(2u) : 0u;
+ const uint32_t dbiStreamSize = (streamCount > 3u) ? rawPdbFile.GetStreamSize(3u) : 0u;
+ const uint32_t ipiStreamSize = (streamCount > 4u) ? rawPdbFile.GetStreamSize(4u) : 0u;
+
+ printf("TPI stream size: %u KiB (%u MiB)\n", tpiStreamSize >> 10u, tpiStreamSize >> 20u);
+ printf("DBI stream size: %u KiB (%u MiB)\n", dbiStreamSize >> 10u, dbiStreamSize >> 20u);
+ printf("IPI stream size: %u KiB (%u MiB)\n", ipiStreamSize >> 10u, ipiStreamSize >> 20u);
+
+ streams.push_back(Stream { "TPI", tpiStreamSize });
+ streams.push_back(Stream { "DBI", dbiStreamSize });
+ streams.push_back(Stream { "IPI", ipiStreamSize });
+
+ const uint32_t globalSymbolStreamSize = rawPdbFile.GetStreamSize(dbiStream.GetHeader().globalStreamIndex);
+ const uint32_t publicSymbolStreamSize = rawPdbFile.GetStreamSize(dbiStream.GetHeader().publicStreamIndex);
+ const uint32_t symbolRecordStreamSize = rawPdbFile.GetStreamSize(dbiStream.GetHeader().symbolRecordStreamIndex);
+
+ printf("Global symbol stream size: %u KiB (%u MiB)\n", globalSymbolStreamSize >> 10u, globalSymbolStreamSize >> 20u);
+ printf("Public symbol stream size: %u KiB (%u MiB)\n", publicSymbolStreamSize >> 10u, publicSymbolStreamSize >> 20u);
+ printf("Symbol record stream size: %u KiB (%u MiB)\n", symbolRecordStreamSize >> 10u, symbolRecordStreamSize >> 20u);
+
+ streams.emplace_back(Stream { "Global", globalSymbolStreamSize });
+ streams.emplace_back(Stream { "Public", publicSymbolStreamSize });
+ streams.emplace_back(Stream { "Symbol", symbolRecordStreamSize });
+ }
+
+ // print the sizes of all module streams
+ printf("\n");
+ printf("Sizes of module streams\n");
+ printf("-----------------------\n");
+ {
+ const PDB::ModuleInfoStream moduleInfoStream = dbiStream.CreateModuleInfoStream(rawPdbFile);
+ const PDB::ArrayView<PDB::ModuleInfoStream::Module> modules = moduleInfoStream.GetModules();
+
+ for (const PDB::ModuleInfoStream::Module& module : modules)
+ {
+ const PDB::DBI::ModuleInfo* moduleInfo = module.GetInfo();
+ const char* name = module.GetName().Decay();
+ const char* objectName = module.GetObjectName().Decay();
+
+ const uint16_t streamIndex = module.HasSymbolStream() ? moduleInfo->moduleSymbolStreamIndex : 0u;
+ const uint32_t moduleStreamSize = (streamIndex != 0u) ? rawPdbFile.GetStreamSize(streamIndex) : 0u;
+
+ printf("Module %s (%s) stream size: %u KiB (%u MiB)\n", name, objectName, moduleStreamSize >> 10u, moduleStreamSize >> 20u);
+
+ streams.push_back(Stream { name, moduleStreamSize });
+ }
+ }
+
+ // sort the streams by their size
+ std::sort(streams.begin(), streams.end(), [](const Stream& lhs, const Stream& rhs)
+ {
+ return lhs.size > rhs.size;
+ });
+
+ // log the 20 largest stream
+ {
+ printf("\n");
+ printf("Sizes of 20 largest streams:\n");
+
+ const size_t countToShow = std::min<size_t>(20ul, streams.size());
+ for (size_t i = 0u; i < countToShow; ++i)
+ {
+ const Stream& stream = streams[i];
+ printf("%zu: %u KiB (%u MiB) from stream %s\n", i + 1u, stream.size >> 10u, stream.size >> 20u, stream.name.c_str());
+ }
+ }
+
+ // print the raw stream sizes
+ printf("\n");
+ printf("Raw sizes of all streams\n");
+ printf("------------------------\n");
+ {
+ const uint32_t streamCount = rawPdbFile.GetStreamCount();
+ for (uint32_t i = 0u; i < streamCount; ++i)
+ {
+ const uint32_t streamSize = rawPdbFile.GetStreamSize(i);
+ printf("Stream %u size: %u KiB (%u MiB)\n", i, streamSize >> 10u, streamSize >> 20u);
+ }
+ }
+}
diff --git a/thirdparty/raw_pdb/src/Examples/ExampleSymbols.cpp b/thirdparty/raw_pdb/src/Examples/ExampleSymbols.cpp
new file mode 100644
index 000000000..c1b2ef8ac
--- /dev/null
+++ b/thirdparty/raw_pdb/src/Examples/ExampleSymbols.cpp
@@ -0,0 +1,238 @@
+// Copyright 2011-2022, Molecular Matters GmbH <[email protected]>
+// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
+
+#include "Examples_PCH.h"
+#include "ExampleTimedScope.h"
+#include "PDB_RawFile.h"
+#include "PDB_DBIStream.h"
+
+
+namespace
+{
+ // we don't have to store std::string in the symbols, since all the data is memory-mapped anyway.
+ // we do it in this example to ensure that we don't "cheat" when reading the PDB file. memory-mapped data will only
+ // be faulted into the process once it's touched, so actually copying the string data makes us touch the needed data,
+ // giving us a real performance measurement.
+ struct Symbol
+ {
+ std::string name;
+ uint32_t rva;
+ };
+}
+
+
+void ExampleSymbols(const PDB::RawFile& rawPdbFile, const PDB::DBIStream& dbiStream);
+void ExampleSymbols(const PDB::RawFile& rawPdbFile, const PDB::DBIStream& dbiStream)
+{
+ TimedScope total("\nRunning example \"Symbols\"");
+
+ // in order to keep the example easy to understand, we load the PDB data serially.
+ // note that this can be improved a lot by reading streams concurrently.
+
+ // prepare the image section stream first. it is needed for converting section + offset into an RVA
+ TimedScope sectionScope("Reading image section stream");
+ const PDB::ImageSectionStream imageSectionStream = dbiStream.CreateImageSectionStream(rawPdbFile);
+ sectionScope.Done();
+
+
+ // prepare the module info stream for matching contributions against files
+ TimedScope moduleScope("Reading module info stream");
+ const PDB::ModuleInfoStream moduleInfoStream = dbiStream.CreateModuleInfoStream(rawPdbFile);
+ moduleScope.Done();
+
+
+ // prepare symbol record stream needed by both public and global streams
+ TimedScope symbolStreamScope("Reading symbol record stream");
+ const PDB::CoalescedMSFStream symbolRecordStream = dbiStream.CreateSymbolRecordStream(rawPdbFile);
+ symbolStreamScope.Done();
+
+ std::vector<Symbol> symbols;
+
+ // read public symbols
+ TimedScope publicScope("Reading public symbol stream");
+ const PDB::PublicSymbolStream publicSymbolStream = dbiStream.CreatePublicSymbolStream(rawPdbFile);
+ publicScope.Done();
+ {
+ TimedScope scope("Storing public symbols");
+
+ const PDB::ArrayView<PDB::HashRecord> hashRecords = publicSymbolStream.GetRecords();
+ const size_t count = hashRecords.GetLength();
+
+ symbols.reserve(count);
+
+ for (const PDB::HashRecord& hashRecord : hashRecords)
+ {
+ const PDB::CodeView::DBI::Record* record = publicSymbolStream.GetRecord(symbolRecordStream, hashRecord);
+ if (record->header.kind != PDB::CodeView::DBI::SymbolRecordKind::S_PUB32)
+ {
+ // normally, a PDB only contains S_PUB32 symbols in the public symbol stream, but we have seen PDBs that also store S_CONSTANT as public symbols.
+ // ignore these.
+ continue;
+ }
+
+ const uint32_t rva = imageSectionStream.ConvertSectionOffsetToRVA(record->data.S_PUB32.section, record->data.S_PUB32.offset);
+ if (rva == 0u)
+ {
+ // certain symbols (e.g. control-flow guard symbols) don't have a valid RVA, ignore those
+ continue;
+ }
+
+ symbols.push_back(Symbol { record->data.S_PUB32.name, rva });
+ }
+
+ scope.Done(count);
+ }
+
+
+ // read global symbols
+ TimedScope globalScope("Reading global symbol stream");
+ const PDB::GlobalSymbolStream globalSymbolStream = dbiStream.CreateGlobalSymbolStream(rawPdbFile);
+ globalScope.Done();
+ {
+ TimedScope scope("Storing global symbols");
+
+ const PDB::ArrayView<PDB::HashRecord> hashRecords = globalSymbolStream.GetRecords();
+ const size_t count = hashRecords.GetLength();
+
+ symbols.reserve(symbols.size() + count);
+
+ for (const PDB::HashRecord& hashRecord : hashRecords)
+ {
+ const PDB::CodeView::DBI::Record* record = globalSymbolStream.GetRecord(symbolRecordStream, hashRecord);
+
+ const char* name = nullptr;
+ uint32_t rva = 0u;
+ if (record->header.kind == PDB::CodeView::DBI::SymbolRecordKind::S_GDATA32)
+ {
+ name = record->data.S_GDATA32.name;
+ rva = imageSectionStream.ConvertSectionOffsetToRVA(record->data.S_GDATA32.section, record->data.S_GDATA32.offset);
+ }
+ else if (record->header.kind == PDB::CodeView::DBI::SymbolRecordKind::S_GTHREAD32)
+ {
+ name = record->data.S_GTHREAD32.name;
+ rva = imageSectionStream.ConvertSectionOffsetToRVA(record->data.S_GTHREAD32.section, record->data.S_GTHREAD32.offset);
+ }
+ else if (record->header.kind == PDB::CodeView::DBI::SymbolRecordKind::S_LDATA32)
+ {
+ name = record->data.S_LDATA32.name;
+ rva = imageSectionStream.ConvertSectionOffsetToRVA(record->data.S_LDATA32.section, record->data.S_LDATA32.offset);
+ }
+ else if (record->header.kind == PDB::CodeView::DBI::SymbolRecordKind::S_LTHREAD32)
+ {
+ name = record->data.S_LTHREAD32.name;
+ rva = imageSectionStream.ConvertSectionOffsetToRVA(record->data.S_LTHREAD32.section, record->data.S_LTHREAD32.offset);
+ }
+ else if (record->header.kind == PDB::CodeView::DBI::SymbolRecordKind::S_UDT)
+ {
+ name = record->data.S_UDT.name;
+ }
+ else if (record->header.kind == PDB::CodeView::DBI::SymbolRecordKind::S_UDT_ST)
+ {
+ name = record->data.S_UDT_ST.name;
+ }
+
+ if (rva == 0u)
+ {
+ // certain symbols (e.g. control-flow guard symbols) don't have a valid RVA, ignore those
+ continue;
+ }
+
+ symbols.push_back(Symbol { name, rva });
+ }
+
+ scope.Done(count);
+ }
+
+
+ // read module symbols
+ {
+ TimedScope scope("Storing symbols from modules");
+
+ const PDB::ArrayView<PDB::ModuleInfoStream::Module> modules = moduleInfoStream.GetModules();
+
+ for (const PDB::ModuleInfoStream::Module& module : modules)
+ {
+ if (!module.HasSymbolStream())
+ {
+ continue;
+ }
+
+ const PDB::ModuleSymbolStream moduleSymbolStream = module.CreateSymbolStream(rawPdbFile);
+ moduleSymbolStream.ForEachSymbol([&symbols, &imageSectionStream](const PDB::CodeView::DBI::Record* record)
+ {
+ const char* name = nullptr;
+ uint32_t rva = 0u;
+ if (record->header.kind == PDB::CodeView::DBI::SymbolRecordKind::S_THUNK32)
+ {
+ if (record->data.S_THUNK32.thunk == PDB::CodeView::DBI::ThunkOrdinal::TrampolineIncremental)
+ {
+ // we have never seen incremental linking thunks stored inside a S_THUNK32 symbol, but better be safe than sorry
+ name = "ILT";
+ rva = imageSectionStream.ConvertSectionOffsetToRVA(record->data.S_THUNK32.section, record->data.S_THUNK32.offset);
+ }
+ }
+ else if (record->header.kind == PDB::CodeView::DBI::SymbolRecordKind::S_TRAMPOLINE)
+ {
+ // incremental linking thunks are stored in the linker module
+ name = "ILT";
+ rva = imageSectionStream.ConvertSectionOffsetToRVA(record->data.S_TRAMPOLINE.thunkSection, record->data.S_TRAMPOLINE.thunkOffset);
+ }
+ else if (record->header.kind == PDB::CodeView::DBI::SymbolRecordKind::S_BLOCK32)
+ {
+ // blocks never store a name and are only stored for indicating whether other symbols are children of this block
+ }
+ else if (record->header.kind == PDB::CodeView::DBI::SymbolRecordKind::S_LABEL32)
+ {
+ // labels don't have a name
+ }
+ else if (record->header.kind == PDB::CodeView::DBI::SymbolRecordKind::S_LPROC32)
+ {
+ name = record->data.S_LPROC32.name;
+ rva = imageSectionStream.ConvertSectionOffsetToRVA(record->data.S_LPROC32.section, record->data.S_LPROC32.offset);
+ }
+ else if (record->header.kind == PDB::CodeView::DBI::SymbolRecordKind::S_GPROC32)
+ {
+ name = record->data.S_GPROC32.name;
+ rva = imageSectionStream.ConvertSectionOffsetToRVA(record->data.S_GPROC32.section, record->data.S_GPROC32.offset);
+ }
+ else if (record->header.kind == PDB::CodeView::DBI::SymbolRecordKind::S_LPROC32_ID)
+ {
+ name = record->data.S_LPROC32_ID.name;
+ rva = imageSectionStream.ConvertSectionOffsetToRVA(record->data.S_LPROC32_ID.section, record->data.S_LPROC32_ID.offset);
+ }
+ else if (record->header.kind == PDB::CodeView::DBI::SymbolRecordKind::S_GPROC32_ID)
+ {
+ name = record->data.S_GPROC32_ID.name;
+ rva = imageSectionStream.ConvertSectionOffsetToRVA(record->data.S_GPROC32_ID.section, record->data.S_GPROC32_ID.offset);
+ }
+ else if (record->header.kind == PDB::CodeView::DBI::SymbolRecordKind::S_REGREL32)
+ {
+ name = record->data.S_REGREL32.name;
+ // You can only get the address while running the program by checking the register value and adding the offset
+ }
+ else if (record->header.kind == PDB::CodeView::DBI::SymbolRecordKind::S_LDATA32)
+ {
+ name = record->data.S_LDATA32.name;
+ rva = imageSectionStream.ConvertSectionOffsetToRVA(record->data.S_LDATA32.section, record->data.S_LDATA32.offset);
+ }
+ else if (record->header.kind == PDB::CodeView::DBI::SymbolRecordKind::S_LTHREAD32)
+ {
+ name = record->data.S_LTHREAD32.name;
+ rva = imageSectionStream.ConvertSectionOffsetToRVA(record->data.S_LTHREAD32.section, record->data.S_LTHREAD32.offset);
+ }
+
+ if (rva == 0u)
+ {
+ // certain symbols (e.g. control-flow guard symbols) don't have a valid RVA, ignore those
+ return;
+ }
+
+ symbols.push_back(Symbol { name, rva });
+ });
+ }
+
+ scope.Done(modules.GetLength());
+ }
+
+ total.Done(symbols.size());
+}
diff --git a/thirdparty/raw_pdb/src/Examples/ExampleTimedScope.cpp b/thirdparty/raw_pdb/src/Examples/ExampleTimedScope.cpp
new file mode 100644
index 000000000..74b3fb04e
--- /dev/null
+++ b/thirdparty/raw_pdb/src/Examples/ExampleTimedScope.cpp
@@ -0,0 +1,54 @@
+// Copyright 2011-2022, Molecular Matters GmbH <[email protected]>
+// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
+
+#include "Examples_PCH.h"
+#include "ExampleTimedScope.h"
+
+namespace
+{
+ static unsigned int g_indent = 0u;
+
+ static void PrintIndent(void)
+ {
+ printf("%.*s", g_indent * 2u, "| | | | | | | | ");
+ }
+}
+
+
+TimedScope::TimedScope(const char* message)
+ : m_begin(std::chrono::high_resolution_clock::now())
+{
+ PrintIndent();
+ ++g_indent;
+
+ printf("%s\n", message);
+}
+
+
+void TimedScope::Done(void) const
+{
+ --g_indent;
+ PrintIndent();
+
+ const double milliSeconds = ReadMilliseconds();
+ printf("---> done in %.3fms\n", milliSeconds);
+}
+
+
+void TimedScope::Done(size_t count) const
+{
+ --g_indent;
+ PrintIndent();
+
+ const double milliSeconds = ReadMilliseconds();
+ printf("---> done in %.3fms (%zu elements)\n", milliSeconds, count);
+}
+
+
+double TimedScope::ReadMilliseconds(void) const
+{
+ const std::chrono::high_resolution_clock::time_point now = std::chrono::high_resolution_clock::now();
+ const std::chrono::duration<double> seconds = now - m_begin;
+
+ return seconds.count() * 1000.0;
+}
diff --git a/thirdparty/raw_pdb/src/Examples/ExampleTimedScope.h b/thirdparty/raw_pdb/src/Examples/ExampleTimedScope.h
new file mode 100644
index 000000000..0488dbbb1
--- /dev/null
+++ b/thirdparty/raw_pdb/src/Examples/ExampleTimedScope.h
@@ -0,0 +1,22 @@
+// Copyright 2011-2022, Molecular Matters GmbH <[email protected]>
+// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
+
+#include "Foundation/PDB_Macros.h"
+#include <chrono>
+
+
+class TimedScope
+{
+public:
+ explicit TimedScope(const char* message);
+
+ void Done(void) const;
+ void Done(size_t count) const;
+
+private:
+ double ReadMilliseconds(void) const;
+
+ const std::chrono::high_resolution_clock::time_point m_begin;
+
+ PDB_DISABLE_COPY_MOVE(TimedScope);
+};
diff --git a/thirdparty/raw_pdb/src/Examples/ExampleTypeTable.cpp b/thirdparty/raw_pdb/src/Examples/ExampleTypeTable.cpp
new file mode 100644
index 000000000..260b4d2ca
--- /dev/null
+++ b/thirdparty/raw_pdb/src/Examples/ExampleTypeTable.cpp
@@ -0,0 +1,41 @@
+// Copyright 2011-2022, Molecular Matters GmbH <[email protected]>
+// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
+
+#include "Examples_PCH.h"
+#include "ExampleTypeTable.h"
+#include "Foundation/PDB_Memory.h"
+
+TypeTable::TypeTable(const PDB::TPIStream& tpiStream) PDB_NO_EXCEPT
+ : typeIndexBegin(tpiStream.GetFirstTypeIndex()), typeIndexEnd(tpiStream.GetLastTypeIndex()),
+ m_recordCount(tpiStream.GetTypeRecordCount())
+{
+ // Create coalesced stream from TPI stream, so the records can be referenced directly using pointers.
+ const PDB::DirectMSFStream& directStream = tpiStream.GetDirectMSFStream();
+ m_stream = PDB::CoalescedMSFStream(directStream, directStream.GetSize(), 0);
+
+ // types in the TPI stream are accessed by their index from other streams.
+ // however, the index is not stored with types in the TPI stream directly, but has to be built while walking the stream.
+ // similarly, because types are variable-length records, there are no direct offsets to access individual types.
+ // we therefore walk the TPI stream once, and store pointers to the records for trivial O(1) array lookup by index later.
+ m_records = PDB_NEW_ARRAY(const PDB::CodeView::TPI::Record*, m_recordCount);
+
+ // parse the CodeView records
+ uint32_t typeIndex = 0u;
+
+ tpiStream.ForEachTypeRecordHeaderAndOffset([this, &typeIndex](const PDB::CodeView::TPI::RecordHeader& header, size_t offset)
+ {
+ // The header includes the record kind and size, which can be stored along with offset
+ // to allow for lazy loading of the types on-demand directly from the TPIStream::GetDirectMSFStream()
+ // using DirectMSFStream::ReadAtOffset(...). Thus not needing a CoalescedMSFStream to look up the types.
+ (void)header;
+
+ const PDB::CodeView::TPI::Record* record = m_stream.GetDataAtOffset<const PDB::CodeView::TPI::Record>(offset);
+ m_records[typeIndex] = record;
+ ++typeIndex;
+ });
+}
+
+TypeTable::~TypeTable() PDB_NO_EXCEPT
+{
+ PDB_DELETE_ARRAY(m_records);
+}
diff --git a/thirdparty/raw_pdb/src/Examples/ExampleTypeTable.h b/thirdparty/raw_pdb/src/Examples/ExampleTypeTable.h
new file mode 100644
index 000000000..7448952f3
--- /dev/null
+++ b/thirdparty/raw_pdb/src/Examples/ExampleTypeTable.h
@@ -0,0 +1,49 @@
+#pragma once
+
+#include "PDB_TPIStream.h"
+#include "PDB_CoalescedMSFStream.h"
+
+class TypeTable
+{
+public:
+ explicit TypeTable(const PDB::TPIStream& tpiStream) PDB_NO_EXCEPT;
+ ~TypeTable() PDB_NO_EXCEPT;
+
+ // Returns the index of the first type, which is not necessarily zero.
+ PDB_NO_DISCARD inline uint32_t GetFirstTypeIndex(void) const PDB_NO_EXCEPT
+ {
+ return typeIndexBegin;
+ }
+
+ // Returns the index of the last type.
+ PDB_NO_DISCARD inline uint32_t GetLastTypeIndex(void) const PDB_NO_EXCEPT
+ {
+ return typeIndexEnd;
+ }
+
+ PDB_NO_DISCARD inline const PDB::CodeView::TPI::Record* GetTypeRecord(uint32_t typeIndex) const PDB_NO_EXCEPT
+ {
+ if (typeIndex < typeIndexBegin || typeIndex > typeIndexEnd)
+ return nullptr;
+
+ return m_records[typeIndex - typeIndexBegin];
+ }
+
+ // Returns a view of all type records.
+ // Records identified by a type index can be accessed via "allRecords[typeIndex - firstTypeIndex]".
+ PDB_NO_DISCARD inline PDB::ArrayView<const PDB::CodeView::TPI::Record*> GetTypeRecords(void) const PDB_NO_EXCEPT
+ {
+ return PDB::ArrayView<const PDB::CodeView::TPI::Record*>(m_records, m_recordCount);
+ }
+
+private:
+ uint32_t typeIndexBegin;
+ uint32_t typeIndexEnd;
+
+ size_t m_recordCount;
+ const PDB::CodeView::TPI::Record **m_records;
+
+ PDB::CoalescedMSFStream m_stream;
+
+ PDB_DISABLE_COPY(TypeTable);
+};
diff --git a/thirdparty/raw_pdb/src/Examples/ExampleTypes.cpp b/thirdparty/raw_pdb/src/Examples/ExampleTypes.cpp
new file mode 100644
index 000000000..cd30b22a1
--- /dev/null
+++ b/thirdparty/raw_pdb/src/Examples/ExampleTypes.cpp
@@ -0,0 +1,1418 @@
+
+#include "Examples_PCH.h"
+#include "ExampleTimedScope.h"
+#include "ExampleTypeTable.h"
+#include "PDB_RawFile.h"
+#include "PDB_DBIStream.h"
+#include "PDB_TPIStream.h"
+#include <cstring>
+#include <cinttypes>
+
+// not all enumeration values are handled explicitly by some of the switch statements
+PDB_DISABLE_WARNING_MSVC(4061)
+PDB_DISABLE_WARNING_CLANG("-Wswitch-enum")
+
+// some format strings are not string literals
+PDB_DISABLE_WARNING_MSVC(4774)
+PDB_DISABLE_WARNING_CLANG("-Wformat-nonliteral")
+
+std::string GetTypeName(const TypeTable& typeTable, uint32_t typeIndex);
+
+static uint8_t GetLeafSize(PDB::CodeView::TPI::TypeRecordKind kind)
+{
+ if (kind < PDB::CodeView::TPI::TypeRecordKind::LF_NUMERIC)
+ {
+ // No leaf can have an index less than LF_NUMERIC (0x8000)
+ // so word is the value...
+ return sizeof(PDB::CodeView::TPI::TypeRecordKind);
+ }
+
+ switch (kind)
+ {
+ case PDB::CodeView::TPI::TypeRecordKind::LF_CHAR:
+ return sizeof(PDB::CodeView::TPI::TypeRecordKind) + sizeof(uint8_t);
+
+ case PDB::CodeView::TPI::TypeRecordKind::LF_USHORT:
+ case PDB::CodeView::TPI::TypeRecordKind::LF_SHORT:
+ return sizeof(PDB::CodeView::TPI::TypeRecordKind) + sizeof(uint16_t);
+
+ case PDB::CodeView::TPI::TypeRecordKind::LF_LONG:
+ case PDB::CodeView::TPI::TypeRecordKind::LF_ULONG:
+ return sizeof(PDB::CodeView::TPI::TypeRecordKind) + sizeof(uint32_t);
+
+ case PDB::CodeView::TPI::TypeRecordKind::LF_QUADWORD:
+ case PDB::CodeView::TPI::TypeRecordKind::LF_UQUADWORD:
+ return sizeof(PDB::CodeView::TPI::TypeRecordKind) + sizeof(uint64_t);
+
+ default:
+ printf("Error! 0x%04x bogus type encountered, aborting...\n", PDB_AS_UNDERLYING(kind));
+ }
+ return 0;
+}
+
+
+static const char* GetLeafName(const char* data, PDB::CodeView::TPI::TypeRecordKind kind)
+{
+ return &data[GetLeafSize(kind)];
+}
+
+
+static const char* GetTypeName(const TypeTable& typeTable, uint32_t typeIndex, uint8_t& pointerLevel, const PDB::CodeView::TPI::Record** referencedType, const PDB::CodeView::TPI::Record** modifierRecord)
+{
+ const char* typeName = nullptr;
+ const PDB::CodeView::TPI::Record* underlyingType = nullptr;
+
+ if (referencedType)
+ *referencedType = nullptr;
+
+ if (modifierRecord)
+ *modifierRecord = nullptr;
+
+ auto typeIndexBegin = typeTable.GetFirstTypeIndex();
+ if (typeIndex < typeIndexBegin)
+ {
+ auto type = static_cast<PDB::CodeView::TPI::TypeIndexKind>(typeIndex);
+ switch (type)
+ {
+ case PDB::CodeView::TPI::TypeIndexKind::T_NOTYPE:
+ return "<NO TYPE>";
+ case PDB::CodeView::TPI::TypeIndexKind::T_HRESULT:
+ return "HRESULT";
+ case PDB::CodeView::TPI::TypeIndexKind::T_32PHRESULT:
+ case PDB::CodeView::TPI::TypeIndexKind::T_64PHRESULT:
+ return "PHRESULT";
+
+ case PDB::CodeView::TPI::TypeIndexKind::T_UNKNOWN_0600:
+ return "UNKNOWN_0x0600";
+
+ case PDB::CodeView::TPI::TypeIndexKind::T_VOID:
+ return "void";
+ case PDB::CodeView::TPI::TypeIndexKind::T_32PVOID:
+ case PDB::CodeView::TPI::TypeIndexKind::T_64PVOID:
+ case PDB::CodeView::TPI::TypeIndexKind::T_PVOID:
+ return "PVOID";
+
+ case PDB::CodeView::TPI::TypeIndexKind::T_32PBOOL08:
+ case PDB::CodeView::TPI::TypeIndexKind::T_32PBOOL16:
+ case PDB::CodeView::TPI::TypeIndexKind::T_32PBOOL32:
+ case PDB::CodeView::TPI::TypeIndexKind::T_32PBOOL64:
+ case PDB::CodeView::TPI::TypeIndexKind::T_64PBOOL08:
+ case PDB::CodeView::TPI::TypeIndexKind::T_64PBOOL16:
+ case PDB::CodeView::TPI::TypeIndexKind::T_64PBOOL32:
+ case PDB::CodeView::TPI::TypeIndexKind::T_64PBOOL64:
+ return "PBOOL";
+
+ case PDB::CodeView::TPI::TypeIndexKind::T_BOOL08:
+ case PDB::CodeView::TPI::TypeIndexKind::T_BOOL16:
+ case PDB::CodeView::TPI::TypeIndexKind::T_BOOL32:
+ return "BOOL";
+
+ case PDB::CodeView::TPI::TypeIndexKind::T_RCHAR:
+ case PDB::CodeView::TPI::TypeIndexKind::T_CHAR:
+ return "CHAR";
+ case PDB::CodeView::TPI::TypeIndexKind::T_32PRCHAR:
+ case PDB::CodeView::TPI::TypeIndexKind::T_32PCHAR:
+ case PDB::CodeView::TPI::TypeIndexKind::T_64PRCHAR:
+ case PDB::CodeView::TPI::TypeIndexKind::T_64PCHAR:
+ case PDB::CodeView::TPI::TypeIndexKind::T_PRCHAR:
+ case PDB::CodeView::TPI::TypeIndexKind::T_PCHAR:
+ return "PCHAR";
+
+ case PDB::CodeView::TPI::TypeIndexKind::T_UCHAR:
+ return "UCHAR";
+ case PDB::CodeView::TPI::TypeIndexKind::T_32PUCHAR:
+ case PDB::CodeView::TPI::TypeIndexKind::T_64PUCHAR:
+ case PDB::CodeView::TPI::TypeIndexKind::T_PUCHAR:
+ return "PUCHAR";
+
+ case PDB::CodeView::TPI::TypeIndexKind::T_WCHAR:
+ return "WCHAR";
+ case PDB::CodeView::TPI::TypeIndexKind::T_32PWCHAR:
+ case PDB::CodeView::TPI::TypeIndexKind::T_64PWCHAR:
+ case PDB::CodeView::TPI::TypeIndexKind::T_PWCHAR:
+ return "PWCHAR";
+
+ case PDB::CodeView::TPI::TypeIndexKind::T_CHAR8:
+ return "CHAR8";
+ case PDB::CodeView::TPI::TypeIndexKind::T_PCHAR8:
+ case PDB::CodeView::TPI::TypeIndexKind::T_PFCHAR8:
+ case PDB::CodeView::TPI::TypeIndexKind::T_PHCHAR8:
+ case PDB::CodeView::TPI::TypeIndexKind::T_32PCHAR8:
+ case PDB::CodeView::TPI::TypeIndexKind::T_32PFCHAR8:
+ case PDB::CodeView::TPI::TypeIndexKind::T_64PCHAR8:
+ return "PCHAR8";
+
+ case PDB::CodeView::TPI::TypeIndexKind::T_CHAR16:
+ return "CHAR16";
+ case PDB::CodeView::TPI::TypeIndexKind::T_PCHAR16:
+ case PDB::CodeView::TPI::TypeIndexKind::T_32PCHAR16:
+ case PDB::CodeView::TPI::TypeIndexKind::T_64PCHAR16:
+ return "PCHAR16";
+
+ case PDB::CodeView::TPI::TypeIndexKind::T_CHAR32:
+ return "CHAR32";
+ case PDB::CodeView::TPI::TypeIndexKind::T_PCHAR32:
+ case PDB::CodeView::TPI::TypeIndexKind::T_32PCHAR32:
+ case PDB::CodeView::TPI::TypeIndexKind::T_64PCHAR32:
+ return "PCHAR32";
+
+ case PDB::CodeView::TPI::TypeIndexKind::T_SHORT:
+ return "SHORT";
+ case PDB::CodeView::TPI::TypeIndexKind::T_32PSHORT:
+ case PDB::CodeView::TPI::TypeIndexKind::T_64PSHORT:
+ case PDB::CodeView::TPI::TypeIndexKind::T_PSHORT:
+ return "PSHORT";
+ case PDB::CodeView::TPI::TypeIndexKind::T_USHORT:
+ return "USHORT";
+ case PDB::CodeView::TPI::TypeIndexKind::T_32PUSHORT:
+ case PDB::CodeView::TPI::TypeIndexKind::T_64PUSHORT:
+ case PDB::CodeView::TPI::TypeIndexKind::T_PUSHORT:
+ return "PUSHORT";
+ case PDB::CodeView::TPI::TypeIndexKind::T_LONG:
+ return "LONG";
+ case PDB::CodeView::TPI::TypeIndexKind::T_32PLONG:
+ case PDB::CodeView::TPI::TypeIndexKind::T_64PLONG:
+ case PDB::CodeView::TPI::TypeIndexKind::T_PLONG:
+ return "PLONG";
+ case PDB::CodeView::TPI::TypeIndexKind::T_ULONG:
+ return "ULONG";
+ case PDB::CodeView::TPI::TypeIndexKind::T_32PULONG:
+ case PDB::CodeView::TPI::TypeIndexKind::T_64PULONG:
+ case PDB::CodeView::TPI::TypeIndexKind::T_PULONG:
+ return "PULONG";
+ case PDB::CodeView::TPI::TypeIndexKind::T_REAL32:
+ return "FLOAT";
+ case PDB::CodeView::TPI::TypeIndexKind::T_32PREAL32:
+ case PDB::CodeView::TPI::TypeIndexKind::T_64PREAL32:
+ case PDB::CodeView::TPI::TypeIndexKind::T_PREAL32:
+ return "PFLOAT";
+ case PDB::CodeView::TPI::TypeIndexKind::T_REAL64:
+ return "DOUBLE";
+ case PDB::CodeView::TPI::TypeIndexKind::T_32PREAL64:
+ case PDB::CodeView::TPI::TypeIndexKind::T_64PREAL64:
+ case PDB::CodeView::TPI::TypeIndexKind::T_PREAL64:
+ return "PDOUBLE";
+ case PDB::CodeView::TPI::TypeIndexKind::T_REAL80:
+ return "REAL80";
+ case PDB::CodeView::TPI::TypeIndexKind::T_32PREAL80:
+ case PDB::CodeView::TPI::TypeIndexKind::T_64PREAL80:
+ case PDB::CodeView::TPI::TypeIndexKind::T_PREAL80:
+ return "PREAL80";
+ case PDB::CodeView::TPI::TypeIndexKind::T_QUAD:
+ return "LONGLONG";
+ case PDB::CodeView::TPI::TypeIndexKind::T_32PQUAD:
+ case PDB::CodeView::TPI::TypeIndexKind::T_64PQUAD:
+ case PDB::CodeView::TPI::TypeIndexKind::T_PQUAD:
+ return "PLONGLONG";
+ case PDB::CodeView::TPI::TypeIndexKind::T_UQUAD:
+ return "ULONGLONG";
+ case PDB::CodeView::TPI::TypeIndexKind::T_32PUQUAD:
+ case PDB::CodeView::TPI::TypeIndexKind::T_64PUQUAD:
+ case PDB::CodeView::TPI::TypeIndexKind::T_PUQUAD:
+ return "PULONGLONG";
+ case PDB::CodeView::TPI::TypeIndexKind::T_INT4:
+ return "INT";
+ case PDB::CodeView::TPI::TypeIndexKind::T_32PINT4:
+ case PDB::CodeView::TPI::TypeIndexKind::T_64PINT4:
+ case PDB::CodeView::TPI::TypeIndexKind::T_PINT4:
+ return "PINT";
+ case PDB::CodeView::TPI::TypeIndexKind::T_UINT4:
+ return "UINT";
+ case PDB::CodeView::TPI::TypeIndexKind::T_32PUINT4:
+ case PDB::CodeView::TPI::TypeIndexKind::T_64PUINT4:
+ case PDB::CodeView::TPI::TypeIndexKind::T_PUINT4:
+ return "PUINT";
+
+ case PDB::CodeView::TPI::TypeIndexKind::T_UINT8:
+ return "UINT8";
+ case PDB::CodeView::TPI::TypeIndexKind::T_PUINT8:
+ case PDB::CodeView::TPI::TypeIndexKind::T_PFUINT8:
+ case PDB::CodeView::TPI::TypeIndexKind::T_PHUINT8:
+ case PDB::CodeView::TPI::TypeIndexKind::T_32PUINT8:
+ case PDB::CodeView::TPI::TypeIndexKind::T_32PFUINT8:
+ case PDB::CodeView::TPI::TypeIndexKind::T_64PUINT8:
+ return "PUINT8";
+
+ case PDB::CodeView::TPI::TypeIndexKind::T_INT8:
+ return "INT8";
+ case PDB::CodeView::TPI::TypeIndexKind::T_PINT8:
+ case PDB::CodeView::TPI::TypeIndexKind::T_PFINT8:
+ case PDB::CodeView::TPI::TypeIndexKind::T_PHINT8:
+ case PDB::CodeView::TPI::TypeIndexKind::T_32PINT8:
+ case PDB::CodeView::TPI::TypeIndexKind::T_32PFINT8:
+ case PDB::CodeView::TPI::TypeIndexKind::T_64PINT8:
+ return "PINT8";
+
+ case PDB::CodeView::TPI::TypeIndexKind::T_OCT:
+ return "OCTAL";
+
+ case PDB::CodeView::TPI::TypeIndexKind::T_POCT:
+ case PDB::CodeView::TPI::TypeIndexKind::T_PFOCT:
+ case PDB::CodeView::TPI::TypeIndexKind::T_PHOCT:
+ case PDB::CodeView::TPI::TypeIndexKind::T_32POCT:
+ case PDB::CodeView::TPI::TypeIndexKind::T_32PFOCT:
+ case PDB::CodeView::TPI::TypeIndexKind::T_64POCT:
+ return "POCTAL";
+
+ case PDB::CodeView::TPI::TypeIndexKind::T_UOCT:
+ return "UOCTAL";
+
+ case PDB::CodeView::TPI::TypeIndexKind::T_PUOCT:
+ case PDB::CodeView::TPI::TypeIndexKind::T_PFUOCT:
+ case PDB::CodeView::TPI::TypeIndexKind::T_PHUOCT:
+ case PDB::CodeView::TPI::TypeIndexKind::T_32PUOCT:
+ case PDB::CodeView::TPI::TypeIndexKind::T_32PFUOCT:
+ case PDB::CodeView::TPI::TypeIndexKind::T_64PUOCT:
+ return "PUOCTAL";
+
+ default:
+ PDB_ASSERT(false, "Unhandled special type 0x%X", typeIndex);
+ return "unhandled_special_type";
+ }
+ }
+ else
+ {
+ auto typeRecord = typeTable.GetTypeRecord(typeIndex);
+ if (!typeRecord)
+ return nullptr;
+
+ switch (typeRecord->header.kind)
+ {
+ case PDB::CodeView::TPI::TypeRecordKind::LF_MODIFIER:
+ if(modifierRecord)
+ *modifierRecord = typeRecord;
+ return GetTypeName(typeTable, typeRecord->data.LF_MODIFIER.type, pointerLevel, referencedType, nullptr);
+ case PDB::CodeView::TPI::TypeRecordKind::LF_POINTER:
+ ++pointerLevel;
+ if(referencedType)
+ *referencedType = typeRecord;
+ if (typeRecord->data.LF_POINTER.utype >= typeIndexBegin)
+ {
+ underlyingType = typeTable.GetTypeRecord(typeRecord->data.LF_POINTER.utype);
+ if (!underlyingType)
+ return nullptr;
+
+ if(underlyingType->header.kind == PDB::CodeView::TPI::TypeRecordKind::LF_POINTER)
+ return GetTypeName(typeTable, typeRecord->data.LF_POINTER.utype, pointerLevel, referencedType, modifierRecord);
+
+ // Type record order can be LF_POINTER -> LF_MODIFIER -> LF_POINTER -> ...
+ if (underlyingType->header.kind == PDB::CodeView::TPI::TypeRecordKind::LF_MODIFIER)
+ {
+ if (modifierRecord)
+ *modifierRecord = underlyingType;
+
+ return GetTypeName(typeTable, underlyingType->data.LF_MODIFIER.type, pointerLevel, referencedType, nullptr);
+ }
+ }
+
+ return GetTypeName(typeTable, typeRecord->data.LF_POINTER.utype, pointerLevel, &typeRecord, modifierRecord);
+ case PDB::CodeView::TPI::TypeRecordKind::LF_PROCEDURE:
+ if (referencedType)
+ *referencedType = typeRecord;
+ return nullptr;
+ case PDB::CodeView::TPI::TypeRecordKind::LF_BITFIELD:
+ if (typeRecord->data.LF_BITFIELD.type < typeIndexBegin)
+ {
+ typeName = GetTypeName(typeTable, typeRecord->data.LF_BITFIELD.type, pointerLevel, nullptr, modifierRecord);
+ if (referencedType)
+ *referencedType = typeRecord;
+ return typeName;
+ }
+ else
+ {
+ if (referencedType)
+ *referencedType = typeRecord;
+ return nullptr;
+ }
+ case PDB::CodeView::TPI::TypeRecordKind::LF_ARRAY:
+ if (referencedType)
+ *referencedType = typeRecord;
+ return GetTypeName(typeTable, typeRecord->data.LF_ARRAY.elemtype, pointerLevel, &typeRecord, modifierRecord);
+ case PDB::CodeView::TPI::TypeRecordKind::LF_CLASS:
+ case PDB::CodeView::TPI::TypeRecordKind::LF_STRUCTURE:
+ return GetLeafName(typeRecord->data.LF_CLASS.data, typeRecord->header.kind);
+
+ case PDB::CodeView::TPI::TypeRecordKind::LF_CLASS2:
+ case PDB::CodeView::TPI::TypeRecordKind::LF_STRUCTURE2:
+ return GetLeafName(typeRecord->data.LF_CLASS2.data, typeRecord->header.kind);
+
+ case PDB::CodeView::TPI::TypeRecordKind::LF_UNION:
+ return GetLeafName(typeRecord->data.LF_UNION.data, typeRecord->header.kind);
+ case PDB::CodeView::TPI::TypeRecordKind::LF_ENUM:
+ return &typeRecord->data.LF_ENUM.name[0];
+ case PDB::CodeView::TPI::TypeRecordKind::LF_MFUNCTION:
+ if (referencedType)
+ *referencedType = typeRecord;
+ return nullptr;
+
+ default:
+ PDB_ASSERT(false, "Unhandled TypeRecordKind 0x%X", static_cast<uint16_t>(typeRecord->header.kind));
+ break;
+ }
+
+ }
+
+ return "unknown_type";
+}
+
+static const char* GetModifierName(const PDB::CodeView::TPI::Record* modifierRecord)
+{
+ if (modifierRecord->data.LF_MODIFIER.attr.MOD_const)
+ return "const";
+ else if (modifierRecord->data.LF_MODIFIER.attr.MOD_volatile)
+ return "volatile";
+ else if (modifierRecord->data.LF_MODIFIER.attr.MOD_unaligned)
+ return "unaligned";
+
+ return "";
+}
+
+static bool GetMethodPrototype(const TypeTable& typeTable, const PDB::CodeView::TPI::Record* methodRecord, std::string& methodPrototype);
+
+static bool GetFunctionPrototype(const TypeTable& typeTable, const PDB::CodeView::TPI::Record* functionRecord, std::string& functionPrototype)
+{
+ PDB_ASSERT(functionRecord->header.kind == PDB::CodeView::TPI::TypeRecordKind::LF_PROCEDURE, "TPI Record kind is 0x%X, expected 0x%X (LF_PROCEDURE)",
+ (uint32_t)functionRecord->header.kind, (uint32_t)PDB::CodeView::TPI::TypeRecordKind::LF_PROCEDURE);
+
+ std::string underlyingTypePrototype;
+
+ size_t markerPos = 0;
+ uint8_t pointerLevel = 0;
+ const PDB::CodeView::TPI::Record* referencedType = nullptr;
+ const PDB::CodeView::TPI::Record* underlyingType = nullptr;
+ const PDB::CodeView::TPI::Record* modifierRecord = nullptr;
+
+ functionPrototype.clear();
+
+ auto typeName = GetTypeName(typeTable, functionRecord->data.LF_PROCEDURE.rvtype, pointerLevel, &referencedType, &modifierRecord);
+ if (typeName)
+ {
+ if (modifierRecord)
+ {
+ functionPrototype += GetModifierName(modifierRecord);
+ functionPrototype += ' ';
+ }
+
+ functionPrototype += typeName;
+
+ for (size_t i = 0; i < pointerLevel; i++)
+ functionPrototype += '*';
+ }
+ else
+ {
+ PDB_ASSERT(referencedType->header.kind == PDB::CodeView::TPI::TypeRecordKind::LF_POINTER, "Referenced type kind 0x%X != LF_POINTER (0x%X)", (uint32_t)referencedType->header.kind, (uint32_t)PDB::CodeView::TPI::TypeRecordKind::LF_POINTER);
+
+ underlyingType = typeTable.GetTypeRecord(referencedType->data.LF_POINTER.utype);
+ if (!underlyingType)
+ return false;
+
+ if (underlyingType->header.kind == PDB::CodeView::TPI::TypeRecordKind::LF_PROCEDURE)
+ {
+ if (!GetFunctionPrototype(typeTable, underlyingType, underlyingTypePrototype))
+ return false;
+ }
+ else if (underlyingType->header.kind == PDB::CodeView::TPI::TypeRecordKind::LF_MFUNCTION)
+ {
+ if (!GetMethodPrototype(typeTable, underlyingType, underlyingTypePrototype))
+ return false;
+ }
+ else
+ {
+ PDB_ASSERT(false, "Unhandled underlyingType kind 0x%X", (uint32_t)underlyingType->header.kind);
+ }
+
+ markerPos = underlyingTypePrototype.find("%s");
+ underlyingTypePrototype.erase(markerPos, 2);
+ functionPrototype = underlyingTypePrototype;
+ }
+
+ functionPrototype += " (*%s)(";
+
+ if (functionRecord->data.LF_PROCEDURE.parmcount)
+ {
+ auto argList = typeTable.GetTypeRecord(functionRecord->data.LF_PROCEDURE.arglist);
+ if (!argList)
+ return false;
+
+ for (size_t i = 0; i < argList->data.LF_ARGLIST.count; i++)
+ {
+ pointerLevel = 0;
+ typeName = GetTypeName(typeTable, argList->data.LF_ARGLIST.arg[i], pointerLevel, &referencedType, &modifierRecord);
+ if (referencedType)
+ {
+ if (referencedType->data.LF_POINTER.utype >= typeTable.GetFirstTypeIndex())
+ {
+ underlyingType = typeTable.GetTypeRecord(referencedType->data.LF_POINTER.utype);
+ if (!underlyingType)
+ return false;
+ }
+
+ if (!underlyingType || (underlyingType->header.kind != PDB::CodeView::TPI::TypeRecordKind::LF_PROCEDURE && underlyingType->header.kind != PDB::CodeView::TPI::TypeRecordKind::LF_MFUNCTION))
+ {
+ if (modifierRecord)
+ {
+ functionPrototype += GetModifierName(modifierRecord);
+ functionPrototype += ' ';
+ }
+
+ functionPrototype += typeName;
+ functionPrototype += '*';
+
+ if (referencedType->data.LF_POINTER.attr.isvolatile)
+ functionPrototype += "volatile";
+ else if (referencedType->data.LF_POINTER.attr.isconst)
+ functionPrototype += "const";
+ }
+ else if(underlyingType->header.kind == PDB::CodeView::TPI::TypeRecordKind::LF_PROCEDURE)
+ {
+ if (!GetFunctionPrototype(typeTable, underlyingType, underlyingTypePrototype))
+ return false;
+
+ markerPos = underlyingTypePrototype.find("%s");
+ underlyingTypePrototype.erase(markerPos, 2);
+
+ for (size_t j = 1; j < pointerLevel; j++)
+ underlyingTypePrototype.insert(markerPos, 1, '*');
+
+ functionPrototype += underlyingTypePrototype;
+ }
+ else if(underlyingType->header.kind == PDB::CodeView::TPI::TypeRecordKind::LF_MFUNCTION)
+ {
+ functionPrototype += GetTypeName(typeTable, argList->data.LF_ARGLIST.arg[i]);
+ }
+ }
+ else
+ {
+ functionPrototype += typeName;
+ }
+
+ if (i < (argList->data.LF_ARGLIST.count - 1))
+ functionPrototype += ", ";
+ }
+ }
+
+ functionPrototype += ')';
+
+ return true;
+}
+
+
+static bool GetMethodPrototype(const TypeTable& typeTable, const PDB::CodeView::TPI::Record* methodRecord, std::string& methodPrototype)
+{
+ PDB_ASSERT(methodRecord->header.kind == PDB::CodeView::TPI::TypeRecordKind::LF_MFUNCTION, "TPI Record kind is 0x%X, expected 0x%X (LF_MFUNCTION)",
+ (uint32_t)methodRecord->header.kind, (uint32_t)PDB::CodeView::TPI::TypeRecordKind::LF_MFUNCTION);
+
+ std::string underlyingTypePrototype;
+
+ size_t markerPos = 0;
+ uint8_t pointerLevel = 0;
+ const PDB::CodeView::TPI::Record* referencedType = nullptr;
+ const PDB::CodeView::TPI::Record* underlyingType = nullptr;
+ const PDB::CodeView::TPI::Record* modifierRecord = nullptr;
+
+ methodPrototype.clear();
+
+ auto typeName = GetTypeName(typeTable, methodRecord->data.LF_MFUNCTION.rvtype, pointerLevel, &referencedType, &modifierRecord);
+ if (typeName)
+ {
+ if (modifierRecord)
+ {
+ methodPrototype += GetModifierName(modifierRecord);
+ methodPrototype += ' ';
+ }
+
+ methodPrototype += typeName;
+
+ for (size_t i = 0; i < pointerLevel; i++)
+ methodPrototype += '*';
+ }
+ else
+ {
+ underlyingType = typeTable.GetTypeRecord(referencedType->data.LF_POINTER.utype);
+ if (!underlyingType)
+ return false;
+
+ if (underlyingType->header.kind == PDB::CodeView::TPI::TypeRecordKind::LF_PROCEDURE)
+ {
+ if (!GetFunctionPrototype(typeTable, underlyingType, underlyingTypePrototype))
+ return false;
+ }
+ else if(underlyingType->header.kind == PDB::CodeView::TPI::TypeRecordKind::LF_MFUNCTION)
+ {
+ if (!GetMethodPrototype(typeTable, underlyingType, underlyingTypePrototype))
+ return false;
+ }
+ else
+ {
+ PDB_ASSERT(false, "Unhandled underlyingType kind 0x%X", (uint32_t)underlyingType->header.kind);
+ }
+
+ markerPos = underlyingTypePrototype.find("%s");
+ underlyingTypePrototype.erase(markerPos, 2);
+ methodPrototype = underlyingTypePrototype;
+ }
+
+ methodPrototype += " %s(";
+
+ if (methodRecord->data.LF_MFUNCTION.parmcount)
+ {
+ auto argList = typeTable.GetTypeRecord(methodRecord->data.LF_MFUNCTION.arglist);
+ if (!argList)
+ return false;
+
+ for (size_t i = 0; i < argList->data.LF_ARGLIST.count; i++)
+ {
+ pointerLevel = 0;
+ typeName = GetTypeName(typeTable, argList->data.LF_ARGLIST.arg[i], pointerLevel, &referencedType, &modifierRecord);
+ if (referencedType)
+ {
+ if (referencedType->data.LF_POINTER.utype >= typeTable.GetFirstTypeIndex())
+ {
+ underlyingType = typeTable.GetTypeRecord(referencedType->data.LF_POINTER.utype);
+ if (!underlyingType)
+ return false;
+ }
+
+ if (!underlyingType || (underlyingType->header.kind != PDB::CodeView::TPI::TypeRecordKind::LF_PROCEDURE && underlyingType->header.kind != PDB::CodeView::TPI::TypeRecordKind::LF_MFUNCTION))
+ {
+ if (modifierRecord)
+ {
+ methodPrototype += GetModifierName(modifierRecord);
+ methodPrototype += ' ';
+ }
+
+ if(typeName)
+ methodPrototype += typeName;
+
+ methodPrototype += '*';
+
+ if (referencedType->data.LF_POINTER.attr.isvolatile)
+ methodPrototype += "volatile";
+ else if (referencedType->data.LF_POINTER.attr.isconst)
+ methodPrototype += "const";
+ }
+ else if (underlyingType->header.kind == PDB::CodeView::TPI::TypeRecordKind::LF_PROCEDURE)
+ {
+ if (!GetFunctionPrototype(typeTable, underlyingType, underlyingTypePrototype))
+ return false;
+
+ markerPos = underlyingTypePrototype.find("%s");
+ underlyingTypePrototype.erase(markerPos, 2);
+
+ for (size_t j = 1; j < pointerLevel; j++)
+ underlyingTypePrototype.insert(markerPos, 1, '*');
+
+ methodPrototype += underlyingTypePrototype;
+ }
+ else if (underlyingType->header.kind == PDB::CodeView::TPI::TypeRecordKind::LF_MFUNCTION)
+ {
+ methodPrototype += GetTypeName(typeTable, argList->data.LF_ARGLIST.arg[i]);
+ }
+ }
+ else
+ {
+ methodPrototype += typeName;
+ }
+
+ if (i < (argList->data.LF_ARGLIST.count - 1))
+ methodPrototype += ", ";
+ }
+ }
+
+ methodPrototype += ')';
+
+ return true;
+}
+
+
+static const char* GetMethodName(const PDB::CodeView::TPI::FieldList* fieldRecord)
+{
+ auto methodAttributes = static_cast<PDB::CodeView::TPI::MethodProperty>(fieldRecord->data.LF_ONEMETHOD.attributes.mprop);
+ switch (methodAttributes)
+ {
+ case PDB::CodeView::TPI::MethodProperty::Intro:
+ case PDB::CodeView::TPI::MethodProperty::PureIntro:
+ return &reinterpret_cast<const char*>(fieldRecord->data.LF_ONEMETHOD.vbaseoff)[sizeof(uint32_t)];
+ default:
+ break;
+ }
+
+ return &reinterpret_cast<const char*>(fieldRecord->data.LF_ONEMETHOD.vbaseoff)[0];
+}
+
+
+static void DisplayFields(const TypeTable& typeTable, const PDB::CodeView::TPI::Record* record)
+{
+ const PDB::CodeView::TPI::Record* referencedType = nullptr;
+ const PDB::CodeView::TPI::Record* underlyingType = nullptr;
+ const PDB::CodeView::TPI::Record* modifierRecord = nullptr;
+ const char* leafName = nullptr;
+ const char* typeName = nullptr;
+ std::string functionPrototype;
+ uint16_t offset = 0;
+
+ auto maximumSize = record->header.size - sizeof(uint16_t);
+
+ for (size_t i = 0; i < maximumSize;)
+ {
+ uint8_t pointerLevel = 0;
+ auto fieldRecord = reinterpret_cast<const PDB::CodeView::TPI::FieldList*>(reinterpret_cast<const uint8_t*>(&record->data.LF_FIELD.list) + i);
+
+ // Other kinds of records are not implemented
+ PDB_ASSERT(
+ fieldRecord->kind == PDB::CodeView::TPI::TypeRecordKind::LF_BCLASS ||
+ fieldRecord->kind == PDB::CodeView::TPI::TypeRecordKind::LF_VBCLASS ||
+ fieldRecord->kind == PDB::CodeView::TPI::TypeRecordKind::LF_IVBCLASS ||
+ fieldRecord->kind == PDB::CodeView::TPI::TypeRecordKind::LF_INDEX ||
+ fieldRecord->kind == PDB::CodeView::TPI::TypeRecordKind::LF_VFUNCTAB ||
+ fieldRecord->kind == PDB::CodeView::TPI::TypeRecordKind::LF_NESTTYPE ||
+ fieldRecord->kind == PDB::CodeView::TPI::TypeRecordKind::LF_ENUM ||
+ fieldRecord->kind == PDB::CodeView::TPI::TypeRecordKind::LF_MEMBER ||
+ fieldRecord->kind == PDB::CodeView::TPI::TypeRecordKind::LF_STMEMBER ||
+ fieldRecord->kind == PDB::CodeView::TPI::TypeRecordKind::LF_METHOD ||
+ fieldRecord->kind == PDB::CodeView::TPI::TypeRecordKind::LF_ONEMETHOD,
+ "Unknown record kind %X",
+ static_cast<unsigned int>(fieldRecord->kind));
+
+ if (fieldRecord->kind == PDB::CodeView::TPI::TypeRecordKind::LF_MEMBER)
+ {
+ if (fieldRecord->data.LF_MEMBER.lfEasy.kind < PDB::CodeView::TPI::TypeRecordKind::LF_NUMERIC)
+ offset = *reinterpret_cast<const uint16_t*>(&fieldRecord->data.LF_MEMBER.offset[0]);
+ else
+ offset = *reinterpret_cast<const uint16_t*>(&fieldRecord->data.LF_MEMBER.offset[sizeof(PDB::CodeView::TPI::TypeRecordKind)]);
+
+ leafName = GetLeafName(fieldRecord->data.LF_MEMBER.offset, fieldRecord->data.LF_MEMBER.lfEasy.kind);
+
+ typeName = GetTypeName(typeTable, fieldRecord->data.LF_MEMBER.index, pointerLevel, &referencedType, &modifierRecord);
+ if (referencedType)
+ {
+ switch (referencedType->header.kind)
+ {
+ case PDB::CodeView::TPI::TypeRecordKind::LF_POINTER:
+ if (referencedType->data.LF_POINTER.utype >= typeTable.GetFirstTypeIndex())
+ {
+ underlyingType = typeTable.GetTypeRecord(referencedType->data.LF_POINTER.utype);
+ if (!underlyingType)
+ break;
+
+ if (underlyingType->header.kind != PDB::CodeView::TPI::TypeRecordKind::LF_PROCEDURE)
+ {
+ if (modifierRecord)
+ printf("[0x%X]%s %s", offset, GetModifierName(modifierRecord), typeName);
+ else
+ printf("[0x%X]%s", offset, typeName);
+
+ for (size_t j = 0; j < pointerLevel; j++)
+ printf("*");
+
+ printf(" %s\n", leafName);
+ }
+ else
+ {
+ if (!GetFunctionPrototype(typeTable, underlyingType, functionPrototype))
+ break;
+
+ printf("[0x%X]", offset);
+ printf(functionPrototype.c_str(), leafName);
+ printf("\n");
+ }
+ }
+ else
+ {
+ printf("[0x%X]%s", offset, typeName);
+
+ for (size_t j = 0; j < pointerLevel; j++)
+ printf("*");
+
+ if (referencedType->data.LF_POINTER.attr.isvolatile)
+ printf(" volatile");
+ else if (referencedType->data.LF_POINTER.attr.isconst)
+ printf(" const");
+
+ printf(" %s\n", leafName);
+ }
+ break;
+ case PDB::CodeView::TPI::TypeRecordKind::LF_BITFIELD:
+ if (typeName)
+ {
+ printf("[0x%X]%s %s : %d\n",
+ offset,
+ typeName,
+ leafName,
+ referencedType->data.LF_BITFIELD.length);
+ }
+ else
+ {
+ modifierRecord = typeTable.GetTypeRecord(referencedType->data.LF_BITFIELD.type);
+ if (!modifierRecord)
+ break;
+
+ printf("[0x%X]%s %s %s : %d\n",
+ offset,
+ GetModifierName(modifierRecord),
+ GetTypeName(typeTable, modifierRecord->data.LF_MODIFIER.type, pointerLevel, nullptr, nullptr),
+ leafName,
+ referencedType->data.LF_BITFIELD.length);
+ }
+ break;
+ case PDB::CodeView::TPI::TypeRecordKind::LF_ARRAY:
+ if (!modifierRecord)
+ {
+ printf("[0x%X]%s %s[] /*0x%X*/\n",
+ offset,
+ typeName,
+ leafName,
+ *reinterpret_cast<const uint16_t*>(referencedType->data.LF_ARRAY.data));
+ }
+ else
+ {
+ printf("[0x%X]%s %s %s[] /*0x%X*/\n",
+ offset,
+ GetModifierName(modifierRecord),
+ GetTypeName(typeTable, modifierRecord->data.LF_MODIFIER.type, pointerLevel, nullptr, nullptr),
+ leafName,
+ *reinterpret_cast<const uint16_t*>(referencedType->data.LF_ARRAY.data));
+ }
+ break;
+ default:
+ break;
+ }
+ }
+ else
+ {
+ if (modifierRecord)
+ printf("[0x%X]%s %s %s\n", offset, GetModifierName(modifierRecord), typeName, leafName);
+ else
+ printf("[0x%X]%s %s\n", offset, typeName, leafName);
+ }
+ }
+ else if (fieldRecord->kind == PDB::CodeView::TPI::TypeRecordKind::LF_NESTTYPE)
+ {
+ leafName = &fieldRecord->data.LF_NESTTYPE.name[0];
+ typeName = GetTypeName(typeTable, fieldRecord->data.LF_NESTTYPE.index, pointerLevel, &referencedType, &modifierRecord);
+
+ printf("%s %s\n", typeName, leafName);
+ }
+ else if (fieldRecord->kind == PDB::CodeView::TPI::TypeRecordKind::LF_STMEMBER)
+ {
+ leafName = &fieldRecord->data.LF_STMEMBER.name[0];
+ typeName = GetTypeName(typeTable, fieldRecord->data.LF_STMEMBER.index, pointerLevel, &referencedType, &modifierRecord);
+
+ if (!modifierRecord)
+ printf("%s %s\n", typeName, leafName);
+ else
+ printf("%s %s %s\n", GetModifierName(modifierRecord), typeName, leafName);
+ }
+ else if (fieldRecord->kind == PDB::CodeView::TPI::TypeRecordKind::LF_METHOD)
+ {
+ leafName = fieldRecord->data.LF_METHOD.name;
+
+ auto methodList = typeTable.GetTypeRecord(fieldRecord->data.LF_METHOD.mList);
+ if (!methodList)
+ break;
+
+ // https://github.com/microsoft/microsoft-pdb/blob/master/PDB/include/symtypeutils.h#L220
+ size_t offsetInMethodList = 0;
+ for (size_t j = 0; j < fieldRecord->data.LF_METHOD.count; j++)
+ {
+ size_t entrySize = 2 * sizeof(uint32_t);
+ const PDB::CodeView::TPI::MethodListEntry* entry = (const PDB::CodeView::TPI::MethodListEntry*)(methodList->data.LF_METHODLIST.mList + offsetInMethodList);
+ if (!GetMethodPrototype(typeTable, typeTable.GetTypeRecord(entry->index), functionPrototype))
+ break;
+ printf(functionPrototype.c_str(), leafName);
+ printf("\n");
+ PDB::CodeView::TPI::MethodProperty methodProp = (PDB::CodeView::TPI::MethodProperty)entry->attributes.mprop;
+ if (methodProp == PDB::CodeView::TPI::MethodProperty::Intro || methodProp == PDB::CodeView::TPI::MethodProperty::PureIntro)
+ entrySize += sizeof(uint32_t);
+ offsetInMethodList += entrySize;
+ }
+ }
+ else if (fieldRecord->kind == PDB::CodeView::TPI::TypeRecordKind::LF_ONEMETHOD)
+ {
+ leafName = GetMethodName(fieldRecord);
+
+ referencedType = typeTable.GetTypeRecord(fieldRecord->data.LF_ONEMETHOD.index);
+ if (!referencedType)
+ break;
+
+ if (!GetMethodPrototype(typeTable, referencedType, functionPrototype))
+ break;
+
+ printf(functionPrototype.c_str(), leafName);
+ printf("\n");
+ }
+ else if (fieldRecord->kind == PDB::CodeView::TPI::TypeRecordKind::LF_BCLASS)
+ {
+ leafName = GetLeafName(fieldRecord->data.LF_BCLASS.offset, fieldRecord->data.LF_BCLASS.lfEasy.kind);
+
+ i += static_cast<size_t>(leafName - reinterpret_cast<const char*>(fieldRecord));
+ i = (i + (sizeof(uint32_t) - 1)) & (0 - sizeof(uint32_t));
+ continue;
+ }
+ else if (fieldRecord->kind == PDB::CodeView::TPI::TypeRecordKind::LF_VBCLASS || fieldRecord->kind == PDB::CodeView::TPI::TypeRecordKind::LF_IVBCLASS)
+ {
+ // virtual base pointer offset from address point
+ // followed by virtual base offset from vbtable
+
+ const PDB::CodeView::TPI::TypeRecordKind vbpOffsetAddressPointKind = *(const PDB::CodeView::TPI::TypeRecordKind*)(fieldRecord->data.LF_IVBCLASS.vbpOffset);
+ const uint8_t vbpOffsetAddressPointSize = GetLeafSize(vbpOffsetAddressPointKind);
+
+ const PDB::CodeView::TPI::TypeRecordKind vbpOffsetVBTableKind = *(const PDB::CodeView::TPI::TypeRecordKind*)(fieldRecord->data.LF_IVBCLASS.vbpOffset + vbpOffsetAddressPointSize);
+ const uint8_t vbpOffsetVBTableSize = GetLeafSize(vbpOffsetVBTableKind);
+
+ i += sizeof(PDB::CodeView::TPI::FieldList::Data::LF_VBCLASS);
+ i += vbpOffsetAddressPointSize + vbpOffsetVBTableSize;
+ i = (i + (sizeof(uint32_t) - 1)) & (0 - sizeof(uint32_t));
+ continue;
+ }
+ else if (fieldRecord->kind == PDB::CodeView::TPI::TypeRecordKind::LF_INDEX)
+ {
+ i += sizeof(PDB::CodeView::TPI::FieldList::Data::LF_INDEX);
+ i = (i + (sizeof(uint32_t) - 1)) & (0 - sizeof(uint32_t));
+ continue;
+ }
+ else if (fieldRecord->kind == PDB::CodeView::TPI::TypeRecordKind::LF_VFUNCTAB)
+ {
+ i += sizeof(PDB::CodeView::TPI::FieldList::Data::LF_VFUNCTAB);
+ i = (i + (sizeof(uint32_t) - 1)) & (0 - sizeof(uint32_t));
+ continue;
+ }
+ else
+ {
+ break;
+ }
+
+ i += static_cast<size_t>(leafName - reinterpret_cast<const char*>(fieldRecord));
+ i += strnlen(leafName, maximumSize - i - 1) + 1;
+ i = (i + (sizeof(uint32_t) - 1)) & (0 - sizeof(uint32_t));
+ }
+}
+
+// Used in ExamplesFunctionVariables
+std::string GetTypeName(const TypeTable& typeTable, uint32_t typeIndex)
+{
+ uint8_t pointerLevel = 0;
+ const PDB::CodeView::TPI::Record* referencedType = nullptr;
+ const PDB::CodeView::TPI::Record* modifierRecord = nullptr;
+
+ const char* typeName = GetTypeName(typeTable, typeIndex, pointerLevel, &referencedType, &modifierRecord);
+
+ if (typeName == nullptr)
+ {
+ if (referencedType == nullptr && (typeIndex & 0x80000000) != 0)
+ {
+ // d3d12.pdb\1DEAE23C86E6462A86018FB180EB8E4A1, S_CALLSITE for `dynamic initializer for 'g_Telemetry'': typeIndex == 0x80900001
+ char typeIndexBuf[0x0C];
+ sprintf_s(typeIndexBuf, sizeof(typeIndexBuf), "%08X", typeIndex);
+ return std::string("<BAD_TYPE_INDEX:0x") + typeIndexBuf + ">";
+ }
+ PDB_ASSERT(referencedType != nullptr, "Neither typeName nor referencedType are set.");
+
+ if (referencedType->header.kind == PDB::CodeView::TPI::TypeRecordKind::LF_POINTER)
+ {
+ std::string pointerType = GetTypeName(typeTable, referencedType->data.LF_POINTER.utype);
+
+ for (size_t i = 0; i < pointerLevel; i++)
+ pointerType += '*';
+
+ return pointerType;
+ }
+ else if (referencedType->header.kind == PDB::CodeView::TPI::TypeRecordKind::LF_ARRAY)
+ {
+ const std::string elementType = GetTypeName(typeTable, referencedType->data.LF_ARRAY.elemtype);
+ const std::string indexType = GetTypeName(typeTable, referencedType->data.LF_ARRAY.idxtype);
+
+ return elementType + "[" + indexType + "]";
+ }
+ else if (referencedType->header.kind == PDB::CodeView::TPI::TypeRecordKind::LF_PROCEDURE)
+ {
+ std::string functionPrototype;
+
+ if (!GetFunctionPrototype(typeTable, referencedType, functionPrototype))
+ {
+ PDB_ASSERT(false, "Resolving function prototype failed");
+ return "resolving function type failed";
+ }
+
+ return functionPrototype;
+ }
+ else if (referencedType->header.kind == PDB::CodeView::TPI::TypeRecordKind::LF_MFUNCTION)
+ {
+ std::string methodPrototype;
+
+ if (!GetMethodPrototype(typeTable, referencedType, methodPrototype))
+ {
+ PDB_ASSERT(false, "Resolving method prototype failed");
+ return "resolving method type failed";
+ }
+
+ std::string classTypeName = GetTypeName(typeTable, referencedType->data.LF_MFUNCTION.classtype);
+ classTypeName += "::*";
+
+ const int stringLength = std::snprintf(nullptr, 0, methodPrototype.c_str(), classTypeName.c_str());
+ PDB_ASSERT(stringLength > 0, "String length %i <= 0", stringLength);
+
+ std::vector<char> resultString(static_cast<size_t>(stringLength) + 1u);
+
+ std::snprintf(&resultString[0], resultString.size(), methodPrototype.c_str(), classTypeName.c_str());
+
+ return std::string(resultString.data());
+ }
+ else
+ {
+ PDB_ASSERT(false, "Unhandled referencedType kind 0x%X", static_cast<uint16_t>(referencedType->header.kind));
+ return "not found";
+ }
+ }
+
+ return typeName;
+}
+
+static void DisplayEnumerates(const PDB::CodeView::TPI::Record* record, uint8_t underlyingTypeSize)
+{
+ const char* leafName = nullptr;
+ uint64_t value = 0;
+ const char* valuePtr = nullptr;
+
+ auto maximumSize = record->header.size - sizeof(uint16_t);
+
+ for (size_t i = 0; i < maximumSize;)
+ {
+ auto fieldRecord = reinterpret_cast<const PDB::CodeView::TPI::FieldList*>(reinterpret_cast<const uint8_t*>(&record->data.LF_FIELD.list) + i);
+
+ leafName = GetLeafName(fieldRecord->data.LF_ENUMERATE.value, fieldRecord->data.LF_ENUMERATE.lfEasy.kind);
+
+ if (fieldRecord->data.LF_ENUMERATE.lfEasy.kind < PDB::CodeView::TPI::TypeRecordKind::LF_NUMERIC)
+ valuePtr = &fieldRecord->data.LF_ENUMERATE.value[0];
+ else
+ valuePtr = &fieldRecord->data.LF_ENUMERATE.value[sizeof(PDB::CodeView::TPI::TypeRecordKind)];
+
+ switch (underlyingTypeSize)
+ {
+ case 1:
+ value = *reinterpret_cast<const uint8_t*>(&fieldRecord->data.LF_ENUMERATE.value[0]);
+ break;
+ case 2:
+ value = *reinterpret_cast<const uint16_t*>(&fieldRecord->data.LF_ENUMERATE.value[0]);
+ break;
+ case 4:
+ value = *reinterpret_cast<const uint32_t*>(&fieldRecord->data.LF_ENUMERATE.value[0]);
+ break;
+ case 8:
+ value = *reinterpret_cast<const uint64_t*>(&fieldRecord->data.LF_ENUMERATE.value[0]);
+ break;
+ default:
+ break;
+ }
+
+ printf("%s = %" PRIu64 "\n", leafName, value);
+
+ i += static_cast<size_t>(leafName - reinterpret_cast<const char*>(fieldRecord));
+ i += strnlen(leafName, maximumSize - i - 1) + 1;
+ i = (i + (sizeof(uint32_t) - 1)) & (0 - sizeof(uint32_t));
+
+ (void)valuePtr;
+ }
+}
+
+
+void ExampleTypes(const PDB::TPIStream& tpiStream);
+void ExampleTypes(const PDB::TPIStream& tpiStream)
+{
+ TimedScope total("\nRunning example \"Function types\"");
+
+ TimedScope typeTableScope("Create TypeTable");
+ TypeTable typeTable(tpiStream);
+ typeTableScope.Done();
+
+ for (const auto& record : typeTable.GetTypeRecords())
+ {
+ if ((record->header.kind == PDB::CodeView::TPI::TypeRecordKind::LF_CLASS) || (record->header.kind == PDB::CodeView::TPI::TypeRecordKind::LF_STRUCTURE))
+ {
+ if (record->data.LF_CLASS.property.fwdref)
+ continue;
+
+ auto typeRecord = typeTable.GetTypeRecord(record->data.LF_CLASS.field);
+ if (!typeRecord)
+ continue;
+
+ auto leafName = GetLeafName(record->data.LF_CLASS.data, record->data.LF_CLASS.lfEasy.kind);
+
+ printf("struct %s\n{\n", leafName);
+
+ DisplayFields(typeTable, typeRecord);
+
+ printf("}\n");
+ }
+ else if (record->header.kind == PDB::CodeView::TPI::TypeRecordKind::LF_UNION)
+ {
+ if (record->data.LF_UNION.property.fwdref)
+ continue;
+
+ auto typeRecord = typeTable.GetTypeRecord(record->data.LF_UNION.field);
+ if (!typeRecord)
+ continue;
+
+ auto leafName = GetLeafName(record->data.LF_UNION.data, static_cast<PDB::CodeView::TPI::TypeRecordKind>(0));
+
+ printf("union %s\n{\n", leafName);
+
+ DisplayFields(typeTable, typeRecord);
+
+ printf("}\n");
+ }
+ else if (record->header.kind == PDB::CodeView::TPI::TypeRecordKind::LF_ENUM)
+ {
+ if (record->data.LF_ENUM.property.fwdref)
+ continue;
+
+ auto typeRecord = typeTable.GetTypeRecord(record->data.LF_ENUM.field);
+ if (!typeRecord)
+ continue;
+
+ printf("enum %s\n{\n", record->data.LF_ENUM.name);
+
+ DisplayEnumerates(typeRecord, GetLeafSize(static_cast<PDB::CodeView::TPI::TypeRecordKind>(record->data.LF_ENUM.utype)));
+
+ printf("}\n");
+ }
+ }
+
+ total.Done(tpiStream.GetTypeRecordCount());
+}
+
+template<typename T>
+static void TagRecursively(const TypeTable& typeTable, uint32_t typeIndex, T setName);
+
+#define TAG_AND_CHECK(typeIndex) if (setName(typeIndex)) TagRecursively(typeTable, typeIndex, setName)
+
+template<typename T>
+static void TagChildren(const TypeTable& typeTable, const PDB::CodeView::TPI::Record* record, T setName)
+{
+ const char* leafName = nullptr;
+
+ auto maximumSize = record->header.size - sizeof(uint16_t);
+
+ for (size_t i = 0; i < maximumSize;)
+ {
+ auto fieldRecord = reinterpret_cast<const PDB::CodeView::TPI::FieldList*>(reinterpret_cast<const uint8_t*>(&record->data.LF_FIELD.list) + i);
+
+ // these are all the record kinds I have observed
+ PDB_ASSERT(
+ fieldRecord->kind == PDB::CodeView::TPI::TypeRecordKind::LF_BCLASS ||
+ fieldRecord->kind == PDB::CodeView::TPI::TypeRecordKind::LF_VBCLASS ||
+ fieldRecord->kind == PDB::CodeView::TPI::TypeRecordKind::LF_IVBCLASS ||
+ fieldRecord->kind == PDB::CodeView::TPI::TypeRecordKind::LF_INDEX ||
+ fieldRecord->kind == PDB::CodeView::TPI::TypeRecordKind::LF_VFUNCTAB ||
+ fieldRecord->kind == PDB::CodeView::TPI::TypeRecordKind::LF_NESTTYPE ||
+ fieldRecord->kind == PDB::CodeView::TPI::TypeRecordKind::LF_ENUM ||
+ fieldRecord->kind == PDB::CodeView::TPI::TypeRecordKind::LF_MEMBER ||
+ fieldRecord->kind == PDB::CodeView::TPI::TypeRecordKind::LF_STMEMBER ||
+ fieldRecord->kind == PDB::CodeView::TPI::TypeRecordKind::LF_METHOD ||
+ fieldRecord->kind == PDB::CodeView::TPI::TypeRecordKind::LF_ONEMETHOD ||
+ fieldRecord->kind == PDB::CodeView::TPI::TypeRecordKind::LF_ENUMERATE,
+ "Unknown record kind %X",
+ static_cast<unsigned int>(fieldRecord->kind));
+
+ if (fieldRecord->kind == PDB::CodeView::TPI::TypeRecordKind::LF_MEMBER)
+ {
+ leafName = GetLeafName(fieldRecord->data.LF_MEMBER.offset, fieldRecord->data.LF_MEMBER.lfEasy.kind);
+ TAG_AND_CHECK(fieldRecord->data.LF_MEMBER.index);
+ }
+ else if (fieldRecord->kind == PDB::CodeView::TPI::TypeRecordKind::LF_NESTTYPE)
+ {
+ leafName = &fieldRecord->data.LF_NESTTYPE.name[0];
+ TAG_AND_CHECK(fieldRecord->data.LF_NESTTYPE.index);
+ }
+ else if (fieldRecord->kind == PDB::CodeView::TPI::TypeRecordKind::LF_STMEMBER)
+ {
+ leafName = &fieldRecord->data.LF_STMEMBER.name[0];
+ TAG_AND_CHECK(fieldRecord->data.LF_STMEMBER.index);
+ }
+ else if (fieldRecord->kind == PDB::CodeView::TPI::TypeRecordKind::LF_METHOD)
+ {
+ leafName = fieldRecord->data.LF_METHOD.name;
+ setName(fieldRecord->data.LF_METHOD.mList);
+
+ auto methodList = typeTable.GetTypeRecord(fieldRecord->data.LF_METHOD.mList);
+ if (!methodList)
+ break;
+
+ // https://github.com/microsoft/microsoft-pdb/blob/master/PDB/include/symtypeutils.h#L220
+ size_t offsetInMethodList = 0;
+ for (size_t j = 0; j < fieldRecord->data.LF_METHOD.count; j++)
+ {
+ size_t entrySize = sizeof(PDB::CodeView::TPI::MethodListEntry);
+ const PDB::CodeView::TPI::MethodListEntry* entry = (const PDB::CodeView::TPI::MethodListEntry*)(methodList->data.LF_METHODLIST.mList + offsetInMethodList);
+ TAG_AND_CHECK(entry->index);
+ PDB::CodeView::TPI::MethodProperty methodProp = (PDB::CodeView::TPI::MethodProperty)entry->attributes.mprop;
+ if (methodProp == PDB::CodeView::TPI::MethodProperty::Intro || methodProp == PDB::CodeView::TPI::MethodProperty::PureIntro)
+ entrySize += sizeof(uint32_t);
+ offsetInMethodList += entrySize;
+ }
+ }
+ else if (fieldRecord->kind == PDB::CodeView::TPI::TypeRecordKind::LF_ONEMETHOD)
+ {
+ leafName = GetMethodName(fieldRecord);
+ TAG_AND_CHECK(fieldRecord->data.LF_ONEMETHOD.index);
+ }
+ else if (fieldRecord->kind == PDB::CodeView::TPI::TypeRecordKind::LF_BCLASS)
+ {
+ leafName = GetLeafName(fieldRecord->data.LF_BCLASS.offset, fieldRecord->data.LF_BCLASS.lfEasy.kind);
+
+ i += static_cast<size_t>(leafName - reinterpret_cast<const char*>(fieldRecord));
+ i = (i + (sizeof(uint32_t) - 1)) & (0 - sizeof(uint32_t));
+ continue;
+ }
+ else if (fieldRecord->kind == PDB::CodeView::TPI::TypeRecordKind::LF_VBCLASS || fieldRecord->kind == PDB::CodeView::TPI::TypeRecordKind::LF_IVBCLASS)
+ {
+ // virtual base pointer offset from address point
+ // followed by virtual base offset from vbtable
+
+ const PDB::CodeView::TPI::TypeRecordKind vbpOffsetAddressPointKind = *(const PDB::CodeView::TPI::TypeRecordKind*)(fieldRecord->data.LF_IVBCLASS.vbpOffset);
+ const uint8_t vbpOffsetAddressPointSize = GetLeafSize(vbpOffsetAddressPointKind);
+
+ const PDB::CodeView::TPI::TypeRecordKind vbpOffsetVBTableKind = *(const PDB::CodeView::TPI::TypeRecordKind*)(fieldRecord->data.LF_IVBCLASS.vbpOffset + vbpOffsetAddressPointSize);
+ const uint8_t vbpOffsetVBTableSize = GetLeafSize(vbpOffsetVBTableKind);
+
+ TAG_AND_CHECK(fieldRecord->data.LF_VBCLASS.vbpIndex);
+
+ i += sizeof(PDB::CodeView::TPI::FieldList::Data::LF_VBCLASS);
+ i += vbpOffsetAddressPointSize + vbpOffsetVBTableSize;
+ i = (i + (sizeof(uint32_t) - 1)) & (0 - sizeof(uint32_t));
+ continue;
+ }
+ else if (fieldRecord->kind == PDB::CodeView::TPI::TypeRecordKind::LF_INDEX)
+ {
+ // this is continued elsewhere
+ setName(fieldRecord->data.LF_INDEX.type);
+ auto continued = typeTable.GetTypeRecord(fieldRecord->data.LF_INDEX.type);
+ if (continued)
+ TagChildren(typeTable, continued, setName);
+
+ i += sizeof(PDB::CodeView::TPI::FieldList::Data::LF_INDEX);
+ i = (i + (sizeof(uint32_t) - 1)) & (0 - sizeof(uint32_t));
+ continue;
+ }
+ else if (fieldRecord->kind == PDB::CodeView::TPI::TypeRecordKind::LF_VFUNCTAB)
+ {
+ TAG_AND_CHECK(fieldRecord->data.LF_VFUNCTAB.type);
+ i += sizeof(PDB::CodeView::TPI::FieldList::Data::LF_VFUNCTAB);
+ i = (i + (sizeof(uint32_t) - 1)) & (0 - sizeof(uint32_t));
+ continue;
+ }
+ else if (fieldRecord->kind == PDB::CodeView::TPI::TypeRecordKind::LF_ENUMERATE)
+ {
+ leafName = GetLeafName(fieldRecord->data.LF_ENUMERATE.value, fieldRecord->data.LF_ENUMERATE.lfEasy.kind);
+ }
+ else
+ {
+ break;
+ }
+
+ i += static_cast<size_t>(leafName - reinterpret_cast<const char*>(fieldRecord));
+ i += strnlen(leafName, maximumSize - i - 1) + 1;
+ i = (i + (sizeof(uint32_t) - 1)) & (0 - sizeof(uint32_t));
+ }
+}
+
+template<typename T>
+static void TagRecursively(const TypeTable& typeTable, uint32_t typeIndex, T setName)
+{
+ const PDB::CodeView::TPI::Record* record = typeTable.GetTypeRecord(typeIndex);
+ if (!record)
+ return;
+ switch (record->header.kind)
+ {
+ case PDB::CodeView::TPI::TypeRecordKind::LF_ARRAY:
+ TAG_AND_CHECK(record->data.LF_ARRAY.elemtype);
+ TAG_AND_CHECK(record->data.LF_ARRAY.idxtype);
+ break;
+ case PDB::CodeView::TPI::TypeRecordKind::LF_POINTER:
+ TAG_AND_CHECK(record->data.LF_POINTER.utype);
+ break;
+ case PDB::CodeView::TPI::TypeRecordKind::LF_MODIFIER:
+ TAG_AND_CHECK(record->data.LF_MODIFIER.type);
+ break;
+ case PDB::CodeView::TPI::TypeRecordKind::LF_PROCEDURE:
+ TAG_AND_CHECK(record->data.LF_PROCEDURE.rvtype);
+ TAG_AND_CHECK(record->data.LF_PROCEDURE.arglist);
+ break;
+ case PDB::CodeView::TPI::TypeRecordKind::LF_ARGLIST:
+ {
+ size_t count = record->data.LF_ARGLIST.count;
+ for (size_t i = 0; i < count; i++)
+ {
+ uint32_t type = record->data.LF_ARGLIST.arg[i];
+ TAG_AND_CHECK(type);
+ }
+ break;
+ }
+ case PDB::CodeView::TPI::TypeRecordKind::LF_MFUNCTION:
+ TAG_AND_CHECK(record->data.LF_MFUNCTION.rvtype);
+ TAG_AND_CHECK(record->data.LF_MFUNCTION.arglist);
+ TAG_AND_CHECK(record->data.LF_MFUNCTION.thistype);
+ break;
+ case PDB::CodeView::TPI::TypeRecordKind::LF_FIELDLIST:
+ TagChildren(typeTable, record, setName);
+ break;
+ default:
+ break;
+ }
+}
+
+// This example takes a PDB's TPI stream and prints out a CSV file that contains all records in the TPI stream.
+// You can use it to figure out what's taking up space in the stream.
+//
+// The format of the CSV is Size; Kind; Name. "Size" is the size of the record in bytes, "Kind" is the kind of
+// the entry, and "Name" is a name associated with this entry.Type - definitions, member functions, and member
+// lists use their type as the name. The idea is that you can bucket by "Name" to get actionable information
+//and insight.
+//
+// The Name is set to "???" if no name was found, and it is set to "!!!" if multiple names reference the entry.
+void ExampleTPISize(const PDB::TPIStream& tpiStream, const char* outPath);
+void ExampleTPISize(const PDB::TPIStream& tpiStream, const char* outPath)
+{
+ TimedScope total("\nRunning example \"TPI Size\"");
+
+ FILE* f;
+#ifndef __unix
+ fopen_s(&f, outPath, "w");
+#else
+ f = fopen(outPath, "w");
+#endif
+ PDB_ASSERT(f, "Failed to open %s for writing", outPath);
+
+ fprintf(f, "Size;Kind;Name\n");
+
+ TimedScope typeTableScope("Create TypeTable");
+ TypeTable typeTable(tpiStream);
+ typeTableScope.Done();
+
+ std::vector<const char*> names;
+ names.resize(typeTable.GetTypeRecords().GetLength());
+
+ const size_t minIndex = typeTable.GetFirstTypeIndex();
+ // sets the name of an entry and returns whether the name changed (because it wasn't set, or because we've found
+ // conflicting information).
+ auto setNameGlobal = [&names, minIndex](uint32_t typeIndex, const char* name) -> bool {
+ if (!name || typeIndex < minIndex)
+ return false;
+ size_t idx = typeIndex - minIndex;
+ const char* prev = names[idx];
+ if (names[idx] == nullptr)
+ {
+ names[idx] = name;
+ return true;
+ }
+ else
+ {
+ names[idx] = "!!!"; // multiple references
+ return names[idx] != prev;
+ }
+ };
+
+ // collect base types and propagate their name
+ auto typeRecords = typeTable.GetTypeRecords();
+ for (size_t i = 0, n = typeRecords.GetLength(); i < n; i++)
+ {
+ const PDB::CodeView::TPI::Record* record = typeRecords[i];
+ PDB::CodeView::TPI::TypeRecordKind kind = record->header.kind;
+ if (kind == PDB::CodeView::TPI::TypeRecordKind::LF_STRUCTURE)
+ {
+ names[i] = GetLeafName(record->data.LF_CLASS.data, record->data.LF_CLASS.lfEasy.kind);
+ auto setName = [&setNameGlobal, names, i](uint32_t typeIndex) -> bool {
+ return setNameGlobal(typeIndex, names[i]);
+ };
+ TAG_AND_CHECK(record->data.LF_CLASS.field);
+ }
+ else if (kind == PDB::CodeView::TPI::TypeRecordKind::LF_CLASS)
+ {
+ names[i] = GetLeafName(record->data.LF_CLASS.data, record->data.LF_CLASS.lfEasy.kind);
+ auto setName = [&setNameGlobal, names, i](uint32_t typeIndex) -> bool {
+ return setNameGlobal(typeIndex, names[i]);
+ };
+ TAG_AND_CHECK(record->data.LF_CLASS.field);
+ }
+ else if (kind == PDB::CodeView::TPI::TypeRecordKind::LF_UNION)
+ {
+ names[i] = GetLeafName(record->data.LF_UNION.data, static_cast<PDB::CodeView::TPI::TypeRecordKind>(0));
+ auto setName = [&setNameGlobal, names, i](uint32_t typeIndex) -> bool {
+ return setNameGlobal(typeIndex, names[i]);
+ };
+ TAG_AND_CHECK(record->data.LF_UNION.field);
+ }
+ else if (kind == PDB::CodeView::TPI::TypeRecordKind::LF_ENUM)
+ {
+ names[i] = record->data.LF_ENUM.name;
+ auto setName = [&setNameGlobal, names, i](uint32_t typeIndex) -> bool {
+ return setNameGlobal(typeIndex, names[i]);
+ };
+ TAG_AND_CHECK(record->data.LF_ENUM.field);
+ }
+ else if (kind == PDB::CodeView::TPI::TypeRecordKind::LF_MFUNCTION)
+ {
+ const char* name = names[i];
+ if (!name)
+ {
+ const PDB::CodeView::TPI::Record* containingRecord = typeTable.GetTypeRecord((record->data.LF_MFUNCTION.classtype));
+ if (containingRecord) {
+ if (containingRecord->header.kind == PDB::CodeView::TPI::TypeRecordKind::LF_CLASS ||
+ containingRecord->header.kind == PDB::CodeView::TPI::TypeRecordKind::LF_STRUCTURE)
+ name = GetLeafName(containingRecord->data.LF_CLASS.data, containingRecord->data.LF_CLASS.lfEasy.kind);
+ else if (containingRecord->header.kind == PDB::CodeView::TPI::TypeRecordKind::LF_UNION)
+ name = GetLeafName(record->data.LF_UNION.data, static_cast<PDB::CodeView::TPI::TypeRecordKind>(0));
+ else
+ PDB_ASSERT(false, "unsupported");
+ }
+ }
+ auto setName = [&setNameGlobal, name](uint32_t typeIndex) -> bool {
+ return setNameGlobal(typeIndex, name);
+ };
+ uint32_t typeIndex = (uint32_t)(minIndex + i);
+ TAG_AND_CHECK(typeIndex);
+ }
+ }
+
+ for (size_t i = 0, n = typeRecords.GetLength(); i < n; i++)
+ {
+ const PDB::CodeView::TPI::Record* record = typeRecords[i];
+ const char* kindName = nullptr;
+ const char* typeName = i < names.size() ? names[i] : nullptr;
+ switch (record->header.kind)
+ {
+ case PDB::CodeView::TPI::TypeRecordKind::LF_VTSHAPE: kindName = "LF_VTSHAPE;"; break;
+ case PDB::CodeView::TPI::TypeRecordKind::LF_POINTER: kindName = "LF_POINTER;"; break;
+ case PDB::CodeView::TPI::TypeRecordKind::LF_MODIFIER: kindName = "LF_MODIFIER;"; break;
+ case PDB::CodeView::TPI::TypeRecordKind::LF_PROCEDURE: kindName = "LF_PROCEDURE;"; break;
+ case PDB::CodeView::TPI::TypeRecordKind::LF_FIELDLIST: kindName = "LF_FIELDLIST;"; break;
+ case PDB::CodeView::TPI::TypeRecordKind::LF_LABEL: kindName = "LF_LABEL;"; break;
+ case PDB::CodeView::TPI::TypeRecordKind::LF_ARGLIST: kindName = "LF_ARGLIST;"; break;
+ case PDB::CodeView::TPI::TypeRecordKind::LF_BITFIELD: kindName = "LF_BITFIELD;"; break;
+ case PDB::CodeView::TPI::TypeRecordKind::LF_METHODLIST: kindName = "LF_METHODLIST;"; break;
+ case PDB::CodeView::TPI::TypeRecordKind::LF_ARRAY: kindName = "LF_ARRAY;"; break;
+ case PDB::CodeView::TPI::TypeRecordKind::LF_PRECOMP: kindName = "LF_PRECOMP;"; break;
+ case PDB::CodeView::TPI::TypeRecordKind::LF_MFUNCTION: kindName = "LF_MFUNCTION;"; break;
+ case PDB::CodeView::TPI::TypeRecordKind::LF_STRUCTURE: kindName = "LF_STRUCTURE;"; break;
+ case PDB::CodeView::TPI::TypeRecordKind::LF_CLASS: kindName = "LF_CLASS;"; break;
+ case PDB::CodeView::TPI::TypeRecordKind::LF_UNION: kindName = "LF_UNION;"; break;
+ case PDB::CodeView::TPI::TypeRecordKind::LF_ENUM: kindName = "LF_ENUM;"; break;
+ default: break;
+ }
+
+ fprintf(f, "%hu;", 2 + record->header.size);
+ if (kindName)
+ fprintf(f, "%s;", kindName);
+ else
+ fprintf(f, "0x%04X;", static_cast<uint16_t>(record->header.kind));
+
+ if (typeName)
+ fprintf(f, "%s\n", typeName);
+ else
+ fprintf(f, "???\n");
+ }
+
+ fclose(f);
+ total.Done(tpiStream.GetTypeRecordCount());
+}
+#undef TAG_AND_CHECK
diff --git a/thirdparty/raw_pdb/src/Examples/Examples_PCH.cpp b/thirdparty/raw_pdb/src/Examples/Examples_PCH.cpp
new file mode 100644
index 000000000..993ae7de7
--- /dev/null
+++ b/thirdparty/raw_pdb/src/Examples/Examples_PCH.cpp
@@ -0,0 +1,4 @@
+// Copyright 2011-2022, Molecular Matters GmbH <[email protected]>
+// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
+
+#include "Examples_PCH.h"
diff --git a/thirdparty/raw_pdb/src/Examples/Examples_PCH.h b/thirdparty/raw_pdb/src/Examples/Examples_PCH.h
new file mode 100644
index 000000000..0a7f2e2ca
--- /dev/null
+++ b/thirdparty/raw_pdb/src/Examples/Examples_PCH.h
@@ -0,0 +1,53 @@
+// Copyright 2011-2022, Molecular Matters GmbH <[email protected]>
+// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
+
+#pragma once
+
+#include "Foundation/PDB_Warnings.h"
+
+// The following clang warnings must be disabled for the examples to build with 0 warnings
+#if PDB_COMPILER_CLANG
+# pragma clang diagnostic ignored "-Wformat-nonliteral" // format string is not a string literal
+# pragma clang diagnostic ignored "-Wswitch-default" // switch' missing 'default' label
+# pragma clang diagnostic ignored "-Wcast-align" // increases required alignment from X to Y
+# pragma clang diagnostic ignored "-Wold-style-cast" // use of old-style cast
+#endif
+
+#if PDB_COMPILER_MSVC
+# pragma warning(push, 0)
+#elif PDB_COMPILER_CLANG
+# pragma clang diagnostic push
+#endif
+
+#if PDB_COMPILER_MSVC
+ // we compile without exceptions
+# define _ALLOW_RTCc_IN_STL
+
+ // triggered by Windows.h
+# pragma warning (disable : 4668)
+
+ // triggered by xlocale in VS 2017
+# pragma warning (disable : 4625) // copy constructor was implicitly defined as deleted
+# pragma warning (disable : 4626) // assignment operator was implicitly defined as deleted
+# pragma warning (disable : 5026) // move constructor was implicitly defined as deleted
+# pragma warning (disable : 5027) // move assignment operator was implicitly defined as deleted
+# pragma warning (disable : 4774) // format string expected in argument 1 is not a string literal
+#endif
+
+#ifdef _WIN32
+# define NOMINMAX
+# include <Windows.h>
+# undef cdecl
+#endif
+# include <vector>
+# include <unordered_set>
+# include <chrono>
+# include <string>
+# include <algorithm>
+# include <cstdarg>
+
+#if PDB_COMPILER_MSVC
+# pragma warning(pop)
+#elif PDB_COMPILER_CLANG
+# pragma clang diagnostic pop
+#endif
diff --git a/thirdparty/raw_pdb/src/Foundation/PDB_ArrayView.h b/thirdparty/raw_pdb/src/Foundation/PDB_ArrayView.h
new file mode 100644
index 000000000..3c462ee80
--- /dev/null
+++ b/thirdparty/raw_pdb/src/Foundation/PDB_ArrayView.h
@@ -0,0 +1,68 @@
+// Copyright 2011-2022, Molecular Matters GmbH <[email protected]>
+// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
+
+#pragma once
+
+#include "PDB_Macros.h"
+#include "PDB_Assert.h"
+
+
+namespace PDB
+{
+ // A read-only view into arrays of any type and length.
+ template <typename T>
+ class PDB_NO_DISCARD ArrayView
+ {
+ public:
+ // Constructs an array view from a C array with explicit length.
+ inline constexpr explicit ArrayView(const T* const array, size_t length) PDB_NO_EXCEPT
+ : m_data(array)
+ , m_length(length)
+ {
+ }
+
+ PDB_DEFAULT_COPY_CONSTRUCTOR(ArrayView);
+ PDB_DEFAULT_MOVE_CONSTRUCTOR(ArrayView);
+
+ // Provides read-only access to the underlying array.
+ PDB_NO_DISCARD inline constexpr const T* Decay(void) const PDB_NO_EXCEPT
+ {
+ return m_data;
+ }
+
+ // Returns the length of the view.
+ PDB_NO_DISCARD inline constexpr size_t GetLength(void) const PDB_NO_EXCEPT
+ {
+ return m_length;
+ }
+
+ // Returns the i-th element.
+ PDB_NO_DISCARD inline const T& operator[](size_t i) const PDB_NO_EXCEPT
+ {
+ PDB_ASSERT(i < GetLength(), "Index %zu out of bounds [0, %zu).", i, GetLength());
+ return m_data[i];
+ }
+
+
+ // ------------------------------------------------------------------------------------------------
+ // Range-based for-loop support
+ // ------------------------------------------------------------------------------------------------
+
+ PDB_NO_DISCARD inline const T* begin(void) const PDB_NO_EXCEPT
+ {
+ return m_data;
+ }
+
+ PDB_NO_DISCARD inline const T* end(void) const PDB_NO_EXCEPT
+ {
+ return m_data + m_length;
+ }
+
+ private:
+ const T* const m_data;
+ const size_t m_length;
+
+ PDB_DISABLE_MOVE_ASSIGNMENT(ArrayView);
+ PDB_DISABLE_COPY_ASSIGNMENT(ArrayView);
+ };
+}
diff --git a/thirdparty/raw_pdb/src/Foundation/PDB_Assert.h b/thirdparty/raw_pdb/src/Foundation/PDB_Assert.h
new file mode 100644
index 000000000..6991e063e
--- /dev/null
+++ b/thirdparty/raw_pdb/src/Foundation/PDB_Assert.h
@@ -0,0 +1,27 @@
+// Copyright 2011-2022, Molecular Matters GmbH <[email protected]>
+// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
+
+#pragma once
+
+#include "PDB_Macros.h"
+#include "PDB_Log.h"
+
+
+PDB_PUSH_WARNING_CLANG
+PDB_DISABLE_WARNING_CLANG("-Wgnu-zero-variadic-macro-arguments")
+PDB_DISABLE_WARNING_CLANG("-Wreserved-identifier")
+
+extern "C" void __cdecl __debugbreak(void);
+
+#if PDB_COMPILER_MSVC
+# pragma intrinsic(__debugbreak)
+#endif
+
+
+#ifdef _DEBUG
+# define PDB_ASSERT(_condition, _msg, ...) (_condition) ? (void)true : (PDB_LOG_ERROR(_msg, ##__VA_ARGS__), __debugbreak())
+#else
+# define PDB_ASSERT(_condition, _msg, ...) PDB_NOOP(_condition, _msg, ##__VA_ARGS__)
+#endif
+
+PDB_POP_WARNING_CLANG
diff --git a/thirdparty/raw_pdb/src/Foundation/PDB_BitOperators.h b/thirdparty/raw_pdb/src/Foundation/PDB_BitOperators.h
new file mode 100644
index 000000000..04f17a44b
--- /dev/null
+++ b/thirdparty/raw_pdb/src/Foundation/PDB_BitOperators.h
@@ -0,0 +1,23 @@
+// Copyright 2011-2022, Molecular Matters GmbH <[email protected]>
+// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
+
+#pragma once
+
+#include "PDB_Macros.h"
+
+
+#define PDB_DEFINE_BIT_OPERATORS(_type) \
+ PDB_NO_DISCARD inline constexpr _type operator|(_type lhs, _type rhs) PDB_NO_EXCEPT \
+ { \
+ return static_cast<_type>(PDB_AS_UNDERLYING(lhs) | PDB_AS_UNDERLYING(rhs)); \
+ } \
+ \
+ PDB_NO_DISCARD inline constexpr _type operator&(_type lhs, _type rhs) PDB_NO_EXCEPT \
+ { \
+ return static_cast<_type>(PDB_AS_UNDERLYING(lhs) & PDB_AS_UNDERLYING(rhs)); \
+ } \
+ \
+ PDB_NO_DISCARD inline constexpr _type operator~(_type value) PDB_NO_EXCEPT \
+ { \
+ return static_cast<_type>(~PDB_AS_UNDERLYING(value)); \
+ }
diff --git a/thirdparty/raw_pdb/src/Foundation/PDB_BitUtil.h b/thirdparty/raw_pdb/src/Foundation/PDB_BitUtil.h
new file mode 100644
index 000000000..7dc5ee3e9
--- /dev/null
+++ b/thirdparty/raw_pdb/src/Foundation/PDB_BitUtil.h
@@ -0,0 +1,73 @@
+// Copyright 2011-2022, Molecular Matters GmbH <[email protected]>
+// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
+
+#pragma once
+
+#include "PDB_Assert.h"
+
+#ifdef _WIN32
+ PDB_PUSH_WARNING_CLANG
+ PDB_DISABLE_WARNING_CLANG("-Wreserved-identifier")
+
+ extern "C" unsigned char _BitScanForward(unsigned long* _Index, unsigned long _Mask);
+
+ PDB_POP_WARNING_CLANG
+
+# if PDB_COMPILER_MSVC
+# pragma intrinsic(_BitScanForward)
+# endif
+#endif
+
+
+namespace PDB
+{
+ namespace BitUtil
+ {
+ // Returns whether the given unsigned value is a power of two.
+ template <typename T>
+ PDB_NO_DISCARD inline constexpr bool IsPowerOfTwo(T value) PDB_NO_EXCEPT
+ {
+ PDB_ASSERT(value != 0u, "Invalid value.");
+
+ return (value & (value - 1u)) == 0u;
+ }
+
+
+ // Rounds the given unsigned value up to the next multiple.
+ template <typename T>
+ PDB_NO_DISCARD inline constexpr T RoundUpToMultiple(T numToRound, T multipleOf) PDB_NO_EXCEPT
+ {
+ PDB_ASSERT(IsPowerOfTwo(multipleOf), "Multiple must be a power-of-two.");
+
+ return (numToRound + (multipleOf - 1u)) & ~(multipleOf - 1u);
+ }
+
+
+ // Finds the position of the first set bit in the given value starting from the LSB, e.g. FindFirstSetBit(0b00000010) == 1.
+ // This operation is also known as CTZ (Count Trailing Zeros).
+ template <typename T>
+ PDB_NO_DISCARD inline uint32_t FindFirstSetBit(T value) PDB_NO_EXCEPT;
+
+ template <>
+ PDB_NO_DISCARD inline uint32_t FindFirstSetBit(uint32_t value) PDB_NO_EXCEPT
+ {
+ PDB_ASSERT(value != 0u, "Invalid value.");
+
+#ifdef _WIN32
+ unsigned long result = 0ul;
+
+ _BitScanForward(&result, value);
+#else
+ unsigned int result = 0u;
+
+ result = static_cast<unsigned int>(__builtin_ffs(static_cast<int>(value)));
+ if (result)
+ {
+ --result;
+ }
+#endif
+
+ return result;
+ }
+ }
+}
diff --git a/thirdparty/raw_pdb/src/Foundation/PDB_CRT.h b/thirdparty/raw_pdb/src/Foundation/PDB_CRT.h
new file mode 100644
index 000000000..539dab33e
--- /dev/null
+++ b/thirdparty/raw_pdb/src/Foundation/PDB_CRT.h
@@ -0,0 +1,14 @@
+// Copyright 2011-2022, Molecular Matters GmbH <[email protected]>
+// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
+
+#pragma once
+
+
+// avoid pulling in different headers just for a few declarations
+extern "C" int __cdecl printf(char const* const _Format, ...);
+
+extern "C" int __cdecl memcmp(void const* _Buf1, void const* _Buf2, size_t _Size);
+extern "C" void* __cdecl memcpy(void* _Dst, void const* _Src, size_t _Size);
+
+extern "C" size_t __cdecl strlen(char const* _Str);
+extern "C" int __cdecl strcmp(char const* _Str1, char const* _Str2);
diff --git a/thirdparty/raw_pdb/src/Foundation/PDB_Forward.h b/thirdparty/raw_pdb/src/Foundation/PDB_Forward.h
new file mode 100644
index 000000000..ba82dfee0
--- /dev/null
+++ b/thirdparty/raw_pdb/src/Foundation/PDB_Forward.h
@@ -0,0 +1,9 @@
+// Copyright 2011-2022, Molecular Matters GmbH <[email protected]>
+// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
+
+#pragma once
+
+
+// See Jonathan Mueller's blog for replacing std::move and std::forward:
+// https://foonathan.net/2021/09/move-forward/
+#define PDB_FORWARD(...) static_cast<decltype(__VA_ARGS__)&&>(__VA_ARGS__)
diff --git a/thirdparty/raw_pdb/src/Foundation/PDB_Log.h b/thirdparty/raw_pdb/src/Foundation/PDB_Log.h
new file mode 100644
index 000000000..83a8518ea
--- /dev/null
+++ b/thirdparty/raw_pdb/src/Foundation/PDB_Log.h
@@ -0,0 +1,15 @@
+// Copyright 2011-2022, Molecular Matters GmbH <[email protected]>
+// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
+
+#pragma once
+
+#include "PDB_Macros.h"
+#include "PDB_CRT.h"
+
+
+PDB_PUSH_WARNING_CLANG
+PDB_DISABLE_WARNING_CLANG("-Wgnu-zero-variadic-macro-arguments")
+
+#define PDB_LOG_ERROR(_format, ...) printf(_format, ##__VA_ARGS__)
+
+PDB_POP_WARNING_CLANG
diff --git a/thirdparty/raw_pdb/src/Foundation/PDB_Macros.h b/thirdparty/raw_pdb/src/Foundation/PDB_Macros.h
new file mode 100644
index 000000000..fddcccfa4
--- /dev/null
+++ b/thirdparty/raw_pdb/src/Foundation/PDB_Macros.h
@@ -0,0 +1,126 @@
+// Copyright 2011-2022, Molecular Matters GmbH <[email protected]>
+// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
+
+#pragma once
+
+#include "PDB_Platform.h"
+#include "PDB_TypeTraits.h"
+
+
+// ------------------------------------------------------------------------------------------------
+// ATTRIBUTES
+// ------------------------------------------------------------------------------------------------
+
+// Indicates to the compiler that the return value of a function or class should not be ignored.
+#if PDB_CPP_17
+# define PDB_NO_DISCARD [[nodiscard]]
+#else
+# define PDB_NO_DISCARD
+#endif
+
+// Indicates to the compiler that a function does not throw an exception.
+#define PDB_NO_EXCEPT noexcept
+
+
+// ------------------------------------------------------------------------------------------------
+// SPECIAL MEMBER FUNCTIONS
+// ------------------------------------------------------------------------------------------------
+
+// Default special member functions.
+#define PDB_DEFAULT_COPY_CONSTRUCTOR(_name) _name(const _name&) PDB_NO_EXCEPT = default
+#define PDB_DEFAULT_COPY_ASSIGNMENT(_name) _name& operator=(const _name&) PDB_NO_EXCEPT = default
+#define PDB_DEFAULT_MOVE_CONSTRUCTOR(_name) _name(_name&&) PDB_NO_EXCEPT = default
+#define PDB_DEFAULT_MOVE_ASSIGNMENT(_name) _name& operator=(_name&&) PDB_NO_EXCEPT = default
+
+// Default copy member functions.
+#define PDB_DEFAULT_COPY(_name) PDB_DEFAULT_COPY_CONSTRUCTOR(_name); PDB_DEFAULT_COPY_ASSIGNMENT(_name)
+
+// Default move member functions.
+#define PDB_DEFAULT_MOVE(_name) PDB_DEFAULT_MOVE_CONSTRUCTOR(_name); PDB_DEFAULT_MOVE_ASSIGNMENT(_name)
+
+// Single macro to default all copy and move member functions.
+#define PDB_DEFAULT_COPY_MOVE(_name) PDB_DEFAULT_COPY(_name); PDB_DEFAULT_MOVE(_name)
+
+// Disable special member functions.
+#define PDB_DISABLE_COPY_CONSTRUCTOR(_name) _name(const _name&) PDB_NO_EXCEPT = delete
+#define PDB_DISABLE_COPY_ASSIGNMENT(_name) _name& operator=(const _name&) PDB_NO_EXCEPT = delete
+#define PDB_DISABLE_MOVE_CONSTRUCTOR(_name) _name(_name&&) PDB_NO_EXCEPT = delete
+#define PDB_DISABLE_MOVE_ASSIGNMENT(_name) _name& operator=(_name&&) PDB_NO_EXCEPT = delete
+
+// Disable copy member functions.
+#define PDB_DISABLE_COPY(_name) PDB_DISABLE_COPY_CONSTRUCTOR(_name); PDB_DISABLE_COPY_ASSIGNMENT(_name)
+
+// Disable move member functions.
+#define PDB_DISABLE_MOVE(_name) PDB_DISABLE_MOVE_CONSTRUCTOR(_name); PDB_DISABLE_MOVE_ASSIGNMENT(_name)
+
+// Single macro to disable all copy and move member functions.
+#define PDB_DISABLE_COPY_MOVE(_name) PDB_DISABLE_COPY(_name); PDB_DISABLE_MOVE(_name)
+
+
+// ------------------------------------------------------------------------------------------------
+// COMPILER WARNINGS
+// ------------------------------------------------------------------------------------------------
+
+#if PDB_COMPILER_MSVC
+# define PDB_PRAGMA(_x) __pragma(_x)
+
+# define PDB_PUSH_WARNING_MSVC PDB_PRAGMA(warning(push))
+# define PDB_SUPPRESS_WARNING_MSVC(_number) PDB_PRAGMA(warning(suppress : _number))
+# define PDB_DISABLE_WARNING_MSVC(_number) PDB_PRAGMA(warning(disable : _number))
+# define PDB_POP_WARNING_MSVC PDB_PRAGMA(warning(pop))
+
+# define PDB_PUSH_WARNING_CLANG
+# define PDB_DISABLE_WARNING_CLANG(_diagnostic)
+# define PDB_POP_WARNING_CLANG
+#elif PDB_COMPILER_CLANG
+# define PDB_PRAGMA(_x) _Pragma(#_x)
+
+# define PDB_PUSH_WARNING_MSVC
+# define PDB_SUPPRESS_WARNING_MSVC(_number)
+# define PDB_DISABLE_WARNING_MSVC(_number)
+# define PDB_POP_WARNING_MSVC
+
+# define PDB_PUSH_WARNING_CLANG PDB_PRAGMA(clang diagnostic push)
+# define PDB_DISABLE_WARNING_CLANG(_diagnostic) PDB_PRAGMA(clang diagnostic ignored _diagnostic)
+# define PDB_POP_WARNING_CLANG PDB_PRAGMA(clang diagnostic pop)
+#elif PDB_COMPILER_GCC
+# define PDB_PRAGMA(_x) _Pragma(#_x)
+
+# define PDB_PUSH_WARNING_MSVC
+# define PDB_SUPPRESS_WARNING_MSVC(_number)
+# define PDB_DISABLE_WARNING_MSVC(_number)
+# define PDB_POP_WARNING_MSVC
+
+# define PDB_PUSH_WARNING_CLANG
+# define PDB_DISABLE_WARNING_CLANG(_diagnostic)
+# define PDB_POP_WARNING_CLANG
+#endif
+
+
+// ------------------------------------------------------------------------------------------------
+// MISCELLANEOUS
+// ------------------------------------------------------------------------------------------------
+
+// Trick to make other macros require a semicolon at the end.
+#define PDB_REQUIRE_SEMICOLON static_assert(true, "")
+
+// Defines a C-like flexible array member.
+#define PDB_FLEXIBLE_ARRAY_MEMBER(_type, _name) \
+ PDB_PUSH_WARNING_MSVC \
+ PDB_PUSH_WARNING_CLANG \
+ PDB_DISABLE_WARNING_MSVC(4200) \
+ PDB_DISABLE_WARNING_CLANG("-Wzero-length-array") \
+ _type _name[0]; \
+ PDB_POP_WARNING_MSVC \
+ PDB_POP_WARNING_CLANG \
+ PDB_REQUIRE_SEMICOLON
+
+// Casts any value to the value of the underlying type.
+#define PDB_AS_UNDERLYING(_value) static_cast<typename PDB::underlying_type<decltype(_value)>::type>(_value)
+
+// Signals to the compiler that a function should be ignored, but have its argument list parsed (and "used", so as to not generate "unused variable" warnings).
+#if PDB_COMPILER_MSVC
+# define PDB_NOOP __noop
+#else
+# define PDB_NOOP(...) (void)sizeof(__VA_ARGS__)
+#endif
diff --git a/thirdparty/raw_pdb/src/Foundation/PDB_Memory.h b/thirdparty/raw_pdb/src/Foundation/PDB_Memory.h
new file mode 100644
index 000000000..ccb7e8698
--- /dev/null
+++ b/thirdparty/raw_pdb/src/Foundation/PDB_Memory.h
@@ -0,0 +1,11 @@
+// Copyright 2011-2022, Molecular Matters GmbH <[email protected]>
+// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
+
+#pragma once
+
+
+#define PDB_NEW(_type) new _type
+#define PDB_NEW_ARRAY(_type, _length) new _type[_length]
+
+#define PDB_DELETE(_ptr) delete _ptr
+#define PDB_DELETE_ARRAY(_ptr) delete[] _ptr
diff --git a/thirdparty/raw_pdb/src/Foundation/PDB_Move.h b/thirdparty/raw_pdb/src/Foundation/PDB_Move.h
new file mode 100644
index 000000000..04bf78b0b
--- /dev/null
+++ b/thirdparty/raw_pdb/src/Foundation/PDB_Move.h
@@ -0,0 +1,11 @@
+// Copyright 2011-2022, Molecular Matters GmbH <[email protected]>
+// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
+
+#pragma once
+
+#include "PDB_TypeTraits.h"
+
+
+// See Jonathan Mueller's blog for replacing std::move and std::forward:
+// https://foonathan.net/2020/09/move-forward/
+#define PDB_MOVE(...) static_cast<PDB::remove_reference<decltype(__VA_ARGS__)>::type&&>(__VA_ARGS__)
diff --git a/thirdparty/raw_pdb/src/Foundation/PDB_Platform.h b/thirdparty/raw_pdb/src/Foundation/PDB_Platform.h
new file mode 100644
index 000000000..8775a548c
--- /dev/null
+++ b/thirdparty/raw_pdb/src/Foundation/PDB_Platform.h
@@ -0,0 +1,45 @@
+// Copyright 2011-2022, Molecular Matters GmbH <[email protected]>
+// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
+
+#pragma once
+
+
+// determine the compiler/toolchain used
+#if defined(__clang__)
+# define PDB_COMPILER_MSVC 0
+# define PDB_COMPILER_CLANG 1
+# define PDB_COMPILER_GCC 0
+#elif defined(_MSC_VER)
+# define PDB_COMPILER_MSVC 1
+# define PDB_COMPILER_CLANG 0
+# define PDB_COMPILER_GCC 0
+#elif defined(__GNUC__)
+# define PDB_COMPILER_MSVC 0
+# define PDB_COMPILER_CLANG 0
+# define PDB_COMPILER_GCC 1
+#else
+# error("Unknown compiler.");
+#endif
+
+// check whether C++17 is available
+#if __cplusplus >= 201703L
+# define PDB_CPP_17 1
+#else
+# define PDB_CPP_17 0
+#endif
+
+// define used standard types
+typedef decltype(sizeof(0)) size_t;
+static_assert(sizeof(sizeof(0)) == sizeof(size_t), "Wrong size.");
+
+typedef int int32_t;
+static_assert(sizeof(int32_t) == 4u, "Wrong size.");
+
+typedef unsigned char uint8_t;
+static_assert(sizeof(uint8_t) == 1u, "Wrong size.");
+
+typedef unsigned short uint16_t;
+static_assert(sizeof(uint16_t) == 2u, "Wrong size.");
+
+typedef unsigned int uint32_t;
+static_assert(sizeof(uint32_t) == 4u, "Wrong size.");
diff --git a/thirdparty/raw_pdb/src/Foundation/PDB_PointerUtil.h b/thirdparty/raw_pdb/src/Foundation/PDB_PointerUtil.h
new file mode 100644
index 000000000..014297df0
--- /dev/null
+++ b/thirdparty/raw_pdb/src/Foundation/PDB_PointerUtil.h
@@ -0,0 +1,33 @@
+// Copyright 2011-2022, Molecular Matters GmbH <[email protected]>
+// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
+
+#pragma once
+
+#include "PDB_Macros.h"
+#include "PDB_TypeTraits.h"
+
+
+namespace PDB
+{
+ namespace Pointer
+ {
+ // Offsets any pointer by a given number of bytes.
+ template <typename T, typename U, typename V>
+ PDB_NO_DISCARD inline T Offset(U* anyPointer, V howManyBytes) PDB_NO_EXCEPT
+ {
+ static_assert(PDB::is_pointer<T>::value == true, "Type T must be a pointer type.");
+
+ union
+ {
+ T as_T;
+ U* as_U_ptr;
+ char* as_char_ptr;
+ };
+
+ as_U_ptr = anyPointer;
+ as_char_ptr += howManyBytes;
+
+ return as_T;
+ }
+ }
+}
diff --git a/thirdparty/raw_pdb/src/Foundation/PDB_TypeTraits.h b/thirdparty/raw_pdb/src/Foundation/PDB_TypeTraits.h
new file mode 100644
index 000000000..928645394
--- /dev/null
+++ b/thirdparty/raw_pdb/src/Foundation/PDB_TypeTraits.h
@@ -0,0 +1,65 @@
+// Copyright 2011-2022, Molecular Matters GmbH <[email protected]>
+// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
+
+#pragma once
+
+
+// provide our own type traits to avoid pulling in unnecessary includes
+namespace PDB
+{
+ template <class T>
+ struct is_pointer
+ {
+ static constexpr bool value = false;
+ };
+
+ template <class T>
+ struct is_pointer<T*>
+ {
+ static constexpr bool value = true;
+ };
+
+ template <class T>
+ struct is_pointer<T* const>
+ {
+ static constexpr bool value = true;
+ };
+
+ template <class T>
+ struct is_pointer<T* volatile>
+ {
+ static constexpr bool value = true;
+ };
+
+ template <class T>
+ struct is_pointer<T* const volatile>
+ {
+ static constexpr bool value = true;
+ };
+
+
+ template <class T>
+ struct remove_reference
+ {
+ using type = T;
+ };
+
+ template <class T>
+ struct remove_reference<T&>
+ {
+ using type = T;
+ };
+
+ template <class T>
+ struct remove_reference<T&&>
+ {
+ using type = T;
+ };
+
+
+ template <class T>
+ struct underlying_type
+ {
+ using type = __underlying_type(T);
+ };
+}
diff --git a/thirdparty/raw_pdb/src/Foundation/PDB_Warnings.h b/thirdparty/raw_pdb/src/Foundation/PDB_Warnings.h
new file mode 100644
index 000000000..fbc8a9de2
--- /dev/null
+++ b/thirdparty/raw_pdb/src/Foundation/PDB_Warnings.h
@@ -0,0 +1,45 @@
+// Copyright 2011-2022, Molecular Matters GmbH <[email protected]>
+// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
+
+#pragma once
+
+#include "PDB_Platform.h"
+
+#if PDB_COMPILER_MSVC
+ // some warnings were introduced with different versions of Visual Studio, so we disable this warning instead of using a bunch of #if/#endif
+# pragma warning (disable : 4619) // there is no warning number N
+
+ // we compile with exceptions disabled
+# pragma warning (disable : 4530) // C++ exception handler used, but unwind semantics are not enabled.Specify / EHsc
+# pragma warning (disable : 4577) // 'noexcept' used with no exception handling mode specified; termination on exception is not guaranteed. Specify /EHsc
+
+ // ignore purely informational warnings
+# pragma warning (disable : 4514) // unreferenced inline function has been removed
+# pragma warning (disable : 4710) // function not inlined
+# pragma warning (disable : 4711) // function selected for automatic inline expansion
+# pragma warning (disable : 4820) // 'N' bytes padding added after data member 'm_member'
+# pragma warning (disable : 5045) // Compiler will insert Spectre mitigation for memory load if /Qspectre switch specified
+#elif PDB_COMPILER_CLANG
+ // turn on absolutely all available Clang warnings
+# pragma clang diagnostic warning "-Wall"
+# pragma clang diagnostic warning "-Wextra"
+# pragma clang diagnostic warning "-Weverything"
+# pragma clang diagnostic warning "-Wpedantic"
+
+ // these warnings contradict -Weverything
+# pragma clang diagnostic ignored "-Wc++98-compat"
+# pragma clang diagnostic ignored "-Wc++98-compat-pedantic"
+
+ // this warning is triggered for templates which are explicitly instantiated.
+ // forgetting to instantiate the template would trigger a linker error anyway, so we disable this warning.
+# pragma clang diagnostic ignored "-Wundefined-func-template"
+
+ // we don't strive for C++20 compatibility
+# pragma clang diagnostic ignored "-Wc++20-compat"
+
+ // some structures will have to be padded
+# pragma clang diagnostic ignored "-Wpadded"
+
+ // it's impossible to write C++ code using raw pointers without triggering this warning
+# pragma clang diagnostic ignored "-Wunsafe-buffer-usage"
+#endif
diff --git a/thirdparty/raw_pdb/src/PDB.cpp b/thirdparty/raw_pdb/src/PDB.cpp
new file mode 100644
index 000000000..0bbd3a7e9
--- /dev/null
+++ b/thirdparty/raw_pdb/src/PDB.cpp
@@ -0,0 +1,55 @@
+// Copyright 2011-2022, Molecular Matters GmbH <[email protected]>
+// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
+
+#include "PDB_PCH.h"
+#include "PDB.h"
+#include "PDB_Types.h"
+#include "PDB_Util.h"
+#include "PDB_RawFile.h"
+#include "Foundation/PDB_PointerUtil.h"
+#include "Foundation/PDB_CRT.h"
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB_NO_DISCARD PDB::ErrorCode PDB::ValidateFile(const void* data, size_t size) PDB_NO_EXCEPT
+{
+ // validate whether there is enough size for the super block
+ if (size < sizeof(SuperBlock))
+ {
+ return ErrorCode::InvalidDataSize;
+ }
+ // validate the super block
+ const SuperBlock* superBlock = Pointer::Offset<const SuperBlock*>(data, 0u);
+ {
+ // validate header magic
+ if (memcmp(superBlock->fileMagic, SuperBlock::MAGIC, sizeof(SuperBlock::MAGIC)) != 0)
+ {
+ return ErrorCode::InvalidSuperBlock;
+ }
+
+ // validate whether enough size is provided for the PDB file
+ // blockCount * blockSize is the size of the PDB file on disk
+ if (size < superBlock->blockCount * superBlock->blockSize)
+ {
+ return ErrorCode::InvalidDataSize;
+ }
+
+ // validate free block map.
+ // the free block map should always reside at either index 1 or 2.
+ if (superBlock->freeBlockMapIndex != 1u && superBlock->freeBlockMapIndex != 2u)
+ {
+ return ErrorCode::InvalidFreeBlockMap;
+ }
+ }
+
+ return ErrorCode::Success;
+}
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB_NO_DISCARD PDB::RawFile PDB::CreateRawFile(const void* data) PDB_NO_EXCEPT
+{
+ return RawFile(data);
+}
diff --git a/thirdparty/raw_pdb/src/PDB.h b/thirdparty/raw_pdb/src/PDB.h
new file mode 100644
index 000000000..3f17f9f1b
--- /dev/null
+++ b/thirdparty/raw_pdb/src/PDB.h
@@ -0,0 +1,21 @@
+// Copyright 2011-2022, Molecular Matters GmbH <[email protected]>
+// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
+
+#pragma once
+
+#include "Foundation/PDB_Macros.h"
+#include "PDB_ErrorCodes.h"
+
+
+// https://llvm.org/docs/PDB/index.html
+namespace PDB
+{
+ class RawFile;
+
+
+ // Validates whether a PDB file is valid.
+ PDB_NO_DISCARD ErrorCode ValidateFile(const void* data, size_t size) PDB_NO_EXCEPT;
+
+ // Creates a raw PDB file that must have been validated.
+ PDB_NO_DISCARD RawFile CreateRawFile(const void* data) PDB_NO_EXCEPT;
+}
diff --git a/thirdparty/raw_pdb/src/PDB_CoalescedMSFStream.cpp b/thirdparty/raw_pdb/src/PDB_CoalescedMSFStream.cpp
new file mode 100644
index 000000000..fe544e4e9
--- /dev/null
+++ b/thirdparty/raw_pdb/src/PDB_CoalescedMSFStream.cpp
@@ -0,0 +1,169 @@
+// Copyright 2011-2022, Molecular Matters GmbH <[email protected]>
+// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
+
+#include "PDB_PCH.h"
+#include "PDB_CoalescedMSFStream.h"
+#include "PDB_Util.h"
+#include "PDB_DirectMSFStream.h"
+#include "Foundation/PDB_PointerUtil.h"
+#include "Foundation/PDB_Memory.h"
+#include "Foundation/PDB_CRT.h"
+
+
+namespace
+{
+ // ------------------------------------------------------------------------------------------------
+ // ------------------------------------------------------------------------------------------------
+ PDB_NO_DISCARD static bool AreBlockIndicesContiguous(const uint32_t* blockIndices, uint32_t blockSize, uint32_t streamSize) PDB_NO_EXCEPT
+ {
+ const uint32_t blockCount = PDB::ConvertSizeToBlockCount(streamSize, blockSize);
+
+ // start with the first index, checking if all following indices are contiguous (N, N+1, N+2, ...)
+ uint32_t expectedIndex = blockIndices[0];
+ for (uint32_t i = 1u; i < blockCount; ++i)
+ {
+ ++expectedIndex;
+ if (blockIndices[i] != expectedIndex)
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+}
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB::CoalescedMSFStream::CoalescedMSFStream(void) PDB_NO_EXCEPT
+ : m_ownedData(nullptr)
+ , m_data(nullptr)
+ , m_size(0u)
+{
+}
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB::CoalescedMSFStream::CoalescedMSFStream(CoalescedMSFStream&& other) PDB_NO_EXCEPT
+ : m_ownedData(PDB_MOVE(other.m_ownedData))
+ , m_data(PDB_MOVE(other.m_data))
+ , m_size(PDB_MOVE(other.m_size))
+{
+ other.m_ownedData = nullptr;
+ other.m_data = nullptr;
+ other.m_size = 0u;
+}
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB::CoalescedMSFStream& PDB::CoalescedMSFStream::operator=(CoalescedMSFStream&& other) PDB_NO_EXCEPT
+{
+ if (this != &other)
+ {
+ PDB_DELETE_ARRAY(m_ownedData);
+
+ m_ownedData = PDB_MOVE(other.m_ownedData);
+ m_data = PDB_MOVE(other.m_data);
+ m_size = PDB_MOVE(other.m_size);
+
+ other.m_ownedData = nullptr;
+ other.m_data = nullptr;
+ other.m_size = 0u;
+ }
+
+ return *this;
+}
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB::CoalescedMSFStream::CoalescedMSFStream(const void* data, uint32_t blockSize, const uint32_t* blockIndices, uint32_t streamSize) PDB_NO_EXCEPT
+ : m_ownedData(nullptr)
+ , m_data(nullptr)
+ , m_size(streamSize)
+{
+ if (AreBlockIndicesContiguous(blockIndices, blockSize, streamSize))
+ {
+ // fast path, all block indices are contiguous, so we don't have to copy any data at all.
+ // instead, we directly point into the memory-mapped file at the correct offset.
+ const uint32_t index = blockIndices[0];
+ const size_t fileOffset = PDB::ConvertBlockIndexToFileOffset(index, blockSize);
+ m_data = Pointer::Offset<const Byte*>(data, fileOffset);
+ }
+ else
+ {
+ // slower path, we need to copy disjunct blocks into our own data array, block by block
+ m_ownedData = PDB_NEW_ARRAY(Byte, streamSize);
+ m_data = m_ownedData;
+
+ Byte* destination = m_ownedData;
+
+ // copy full blocks first
+ const uint32_t fullBlockCount = streamSize / blockSize;
+ for (uint32_t i = 0u; i < fullBlockCount; ++i)
+ {
+ const uint32_t index = blockIndices[i];
+
+ // read one single block at the correct offset in the stream
+ const size_t fileOffset = PDB::ConvertBlockIndexToFileOffset(index, blockSize);
+ const void* sourceData = Pointer::Offset<const void*>(data, fileOffset);
+ memcpy(destination, sourceData, blockSize);
+
+ destination += blockSize;
+ }
+
+ // account for non-full blocks
+ const uint32_t remainingBytes = streamSize - (fullBlockCount * blockSize);
+ if (remainingBytes != 0u)
+ {
+ const uint32_t index = blockIndices[fullBlockCount];
+
+ // read remaining bytes at correct offset in the stream
+ const size_t fileOffset = PDB::ConvertBlockIndexToFileOffset(index, blockSize);
+ const void* sourceData = Pointer::Offset<const void*>(data, fileOffset);
+ memcpy(destination, sourceData, remainingBytes);
+ }
+ }
+}
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB::CoalescedMSFStream::CoalescedMSFStream(const DirectMSFStream& directStream, uint32_t size, uint32_t offset) PDB_NO_EXCEPT
+ : m_ownedData(nullptr)
+ , m_data(nullptr)
+ , m_size(size)
+{
+ const DirectMSFStream::IndexAndOffset indexAndOffset = directStream.GetBlockIndexForOffset(offset);
+
+ // Note: we need to add the offset within the block to the size of the stream to determine if the block
+ // indices are contiguous. This is needed to deal with the case where reading the requested number of bytes
+ // from the specified offset would cross a block boundary. For example, if the offset within the block is
+ // 64 and we want to read 4096 bytes with a block size of 4096, we need to consider *two* block indices,
+ // not *one*, even though 4096 / 4096 = 1.
+ if (AreBlockIndicesContiguous(directStream.GetBlockIndices() + indexAndOffset.index, directStream.GetBlockSize(), indexAndOffset.offsetWithinBlock + size))
+ {
+ // fast path, all block indices inside the direct stream from (data + offset) to (data + offset + size) are contiguous
+ const size_t offsetWithinData = directStream.GetDataOffsetForIndexAndOffset(indexAndOffset);
+ m_data = Pointer::Offset<const Byte*>(directStream.GetData(), offsetWithinData);
+ }
+ else
+ {
+ // slower path, we need to copy from disjunct blocks, which is performed by the direct stream
+ m_ownedData = PDB_NEW_ARRAY(Byte, size);
+ m_data = m_ownedData;
+
+ directStream.ReadAtOffset(m_ownedData, size, offset);
+ }
+}
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB::CoalescedMSFStream::~CoalescedMSFStream(void) PDB_NO_EXCEPT
+{
+ PDB_DELETE_ARRAY(m_ownedData);
+}
diff --git a/thirdparty/raw_pdb/src/PDB_CoalescedMSFStream.h b/thirdparty/raw_pdb/src/PDB_CoalescedMSFStream.h
new file mode 100644
index 000000000..09d524cb5
--- /dev/null
+++ b/thirdparty/raw_pdb/src/PDB_CoalescedMSFStream.h
@@ -0,0 +1,71 @@
+// Copyright 2011-2022, Molecular Matters GmbH <[email protected]>
+// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
+
+#pragma once
+
+#include "Foundation/PDB_Assert.h"
+#include "Foundation/PDB_Macros.h"
+#include "PDB_Types.h"
+
+// https://llvm.org/docs/PDB/index.html#the-msf-container
+// https://llvm.org/docs/PDB/MsfFile.html
+namespace PDB
+{
+ class PDB_NO_DISCARD DirectMSFStream;
+
+
+ // provides access to a coalesced version of an MSF stream.
+ // inherently thread-safe, the stream doesn't carry any internal offset or similar.
+ // coalesces all blocks into a contiguous stream of data upon construction.
+ // very fast individual reads, useful when almost all data of a stream is needed anyway.
+ class PDB_NO_DISCARD CoalescedMSFStream
+ {
+ public:
+ CoalescedMSFStream(void) PDB_NO_EXCEPT;
+ CoalescedMSFStream(CoalescedMSFStream&& other) PDB_NO_EXCEPT;
+ CoalescedMSFStream& operator=(CoalescedMSFStream&& other) PDB_NO_EXCEPT;
+
+ explicit CoalescedMSFStream(const void* data, uint32_t blockSize, const uint32_t* blockIndices, uint32_t streamSize) PDB_NO_EXCEPT;
+
+ // Creates a coalesced stream from a direct stream at any offset.
+ explicit CoalescedMSFStream(const DirectMSFStream& directStream, uint32_t size, uint32_t offset) PDB_NO_EXCEPT;
+
+ ~CoalescedMSFStream(void) PDB_NO_EXCEPT;
+
+ // Returns the size of the stream.
+ PDB_NO_DISCARD inline size_t GetSize(void) const PDB_NO_EXCEPT
+ {
+ return m_size;
+ }
+
+ // Provides read-only access to the data.
+ template <typename T>
+ PDB_NO_DISCARD inline const T* GetDataAtOffset(size_t offset) const PDB_NO_EXCEPT
+ {
+ return reinterpret_cast<const T*>(m_data + offset);
+ }
+
+ template <typename T>
+ PDB_NO_DISCARD inline size_t GetPointerOffset(const T* pointer) const PDB_NO_EXCEPT
+ {
+ const Byte* bytePointer = reinterpret_cast<const Byte*>(pointer);
+ const Byte* dataEnd = m_data + m_size;
+
+ PDB_ASSERT(bytePointer >= m_data && bytePointer <= dataEnd, "Pointer 0x%p not within stream range [0x%p:0x%p]",
+ static_cast<const void*>(bytePointer), static_cast<const void*>(m_data), static_cast<const void*>(dataEnd));
+
+ return static_cast<size_t>(bytePointer - m_data);
+ }
+
+ private:
+ // contiguous, coalesced data, can be null
+ Byte* m_ownedData;
+
+ // either points to the owned data that has been copied from disjunct blocks, or points to the
+ // memory-mapped data directly in case all stream blocks are contiguous.
+ const Byte* m_data;
+ size_t m_size;
+
+ PDB_DISABLE_COPY(CoalescedMSFStream);
+ };
+}
diff --git a/thirdparty/raw_pdb/src/PDB_DBIStream.cpp b/thirdparty/raw_pdb/src/PDB_DBIStream.cpp
new file mode 100644
index 000000000..5c9bf1512
--- /dev/null
+++ b/thirdparty/raw_pdb/src/PDB_DBIStream.cpp
@@ -0,0 +1,335 @@
+// Copyright 2011-2022, Molecular Matters GmbH <[email protected]>
+// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
+
+#include "PDB_PCH.h"
+#include "PDB_DBIStream.h"
+#include "PDB_RawFile.h"
+
+
+namespace
+{
+ // the DBI stream always resides at index 3
+ static constexpr const uint32_t DBIStreamIndex = 3u;
+
+
+ // ------------------------------------------------------------------------------------------------
+ // ------------------------------------------------------------------------------------------------
+ PDB_NO_DISCARD static inline uint32_t GetModuleInfoSubstreamOffset(const PDB::DBI::StreamHeader& /* dbiHeader */) PDB_NO_EXCEPT
+ {
+ return sizeof(PDB::DBI::StreamHeader);
+ }
+
+
+ // ------------------------------------------------------------------------------------------------
+ // ------------------------------------------------------------------------------------------------
+ PDB_NO_DISCARD static inline uint32_t GetSectionContributionSubstreamOffset(const PDB::DBI::StreamHeader& dbiHeader) PDB_NO_EXCEPT
+ {
+ return GetModuleInfoSubstreamOffset(dbiHeader) + dbiHeader.moduleInfoSize;
+ }
+
+
+ // ------------------------------------------------------------------------------------------------
+ // ------------------------------------------------------------------------------------------------
+ PDB_NO_DISCARD static inline uint32_t GetSectionMapSubstreamOffset(const PDB::DBI::StreamHeader& dbiHeader) PDB_NO_EXCEPT
+ {
+ return GetSectionContributionSubstreamOffset(dbiHeader) + dbiHeader.sectionContributionSize;
+ }
+
+
+ // ------------------------------------------------------------------------------------------------
+ // ------------------------------------------------------------------------------------------------
+ PDB_NO_DISCARD static inline uint32_t GetSourceInfoSubstreamOffset(const PDB::DBI::StreamHeader& dbiHeader) PDB_NO_EXCEPT
+ {
+ return GetSectionMapSubstreamOffset(dbiHeader) + dbiHeader.sectionMapSize;
+ }
+
+
+ // ------------------------------------------------------------------------------------------------
+ // ------------------------------------------------------------------------------------------------
+ PDB_NO_DISCARD static inline uint32_t GetTypeServerMapSubstreamOffset(const PDB::DBI::StreamHeader& dbiHeader) PDB_NO_EXCEPT
+ {
+ return GetSourceInfoSubstreamOffset(dbiHeader) + dbiHeader.sourceInfoSize;
+ }
+
+
+ // ------------------------------------------------------------------------------------------------
+ // ------------------------------------------------------------------------------------------------
+ PDB_NO_DISCARD static inline uint32_t GetECSubstreamOffset(const PDB::DBI::StreamHeader& dbiHeader) PDB_NO_EXCEPT
+ {
+ return GetTypeServerMapSubstreamOffset(dbiHeader) + dbiHeader.typeServerMapSize;
+ }
+
+
+ // ------------------------------------------------------------------------------------------------
+ // ------------------------------------------------------------------------------------------------
+ PDB_NO_DISCARD static inline uint32_t GetDebugHeaderSubstreamOffset(const PDB::DBI::StreamHeader& dbiHeader) PDB_NO_EXCEPT
+ {
+ return GetECSubstreamOffset(dbiHeader) + dbiHeader.ecSize;
+ }
+
+
+ // ------------------------------------------------------------------------------------------------
+ // ------------------------------------------------------------------------------------------------
+ PDB_NO_DISCARD static inline bool HasDebugHeaderSubstream(const PDB::DBI::StreamHeader& dbiHeader) PDB_NO_EXCEPT
+ {
+ return dbiHeader.optionalDebugHeaderSize != 0u;
+ }
+}
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB::DBIStream::DBIStream(void) PDB_NO_EXCEPT
+ : m_header()
+ , m_stream()
+{
+}
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB::DBIStream::DBIStream(const RawFile& file, const DBI::StreamHeader& header) PDB_NO_EXCEPT
+ : m_header(header)
+ , m_stream(file.CreateMSFStream<DirectMSFStream>(DBIStreamIndex))
+{
+}
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB_NO_DISCARD PDB::ErrorCode PDB::HasValidDBIStream(const RawFile& file) PDB_NO_EXCEPT
+{
+ DirectMSFStream stream = file.CreateMSFStream<DirectMSFStream>(DBIStreamIndex);
+ if (stream.GetSize() < sizeof(DBI::StreamHeader))
+ {
+ return ErrorCode::InvalidStream;
+ }
+
+ const DBI::StreamHeader header = stream.ReadAtOffset<DBI::StreamHeader>(0u);
+ if (header.signature != DBI::StreamHeader::Signature)
+ {
+ return ErrorCode::InvalidSignature;
+ }
+ else if (header.version != DBI::StreamHeader::Version::V70)
+ {
+ return ErrorCode::UnknownVersion;
+ }
+
+ return ErrorCode::Success;
+}
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB_NO_DISCARD PDB::DBIStream PDB::CreateDBIStream(const RawFile& file) PDB_NO_EXCEPT
+{
+ DirectMSFStream stream = file.CreateMSFStream<DirectMSFStream>(DBIStreamIndex);
+ const DBI::StreamHeader header = stream.ReadAtOffset<DBI::StreamHeader>(0u);
+
+ return DBIStream { file, header };
+}
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB_NO_DISCARD PDB::ErrorCode PDB::DBIStream::HasValidSymbolRecordStream(const RawFile& /* file */) const PDB_NO_EXCEPT
+{
+ return (m_header.symbolRecordStreamIndex != PDB::NilStreamIndex) ? ErrorCode::Success : ErrorCode::InvalidStreamIndex;
+}
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB_NO_DISCARD PDB::ErrorCode PDB::DBIStream::HasValidImageSectionStream(const RawFile& /* file */) const PDB_NO_EXCEPT
+{
+ // the debug header stream is optional. if it's not there, we can't get the image section stream either.
+ if (!HasDebugHeaderSubstream(m_header))
+ {
+ return ErrorCode::InvalidStreamIndex;
+ }
+
+ // find the debug header sub-stream
+ const uint32_t debugHeaderOffset = GetDebugHeaderSubstreamOffset(m_header);
+
+ // validate that we have enough data to read the debug header
+ // (the header field optionalDebugHeaderSize might claim there's a debug header,
+ // but the stream might not have enough data - this happens with some .ni.pdb files)
+ if (debugHeaderOffset + sizeof(DBI::DebugHeader) > m_stream.GetSize())
+ {
+ return ErrorCode::InvalidStream;
+ }
+
+ const DBI::DebugHeader& debugHeader = m_stream.ReadAtOffset<DBI::DebugHeader>(debugHeaderOffset);
+
+ if (debugHeader.sectionHeaderStreamIndex == DBI::DebugHeader::InvalidStreamIndex)
+ {
+ return ErrorCode::InvalidStreamIndex;
+ }
+
+ return ErrorCode::Success;
+}
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB_NO_DISCARD PDB::ErrorCode PDB::DBIStream::HasValidPublicSymbolStream(const RawFile& file) const PDB_NO_EXCEPT
+{
+ if (m_header.publicStreamIndex == PDB::NilStreamIndex)
+ {
+ return ErrorCode::InvalidStreamIndex;
+ }
+
+ DirectMSFStream publicStream = file.CreateMSFStream<DirectMSFStream>(m_header.publicStreamIndex);
+
+ // the public symbol stream always begins with a header, we are not interested in that.
+ // following the public symbol stream header is a hash table header.
+ const HashTableHeader hashHeader = publicStream.ReadAtOffset<HashTableHeader>(sizeof(PublicStreamHeader));
+ if (hashHeader.signature != HashTableHeader::Signature)
+ {
+ return ErrorCode::InvalidSignature;
+ }
+ else if (hashHeader.version != HashTableHeader::Version)
+ {
+ return ErrorCode::UnknownVersion;
+ }
+
+ return ErrorCode::Success;
+}
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB_NO_DISCARD PDB::ErrorCode PDB::DBIStream::HasValidGlobalSymbolStream(const RawFile& file) const PDB_NO_EXCEPT
+{
+ if (m_header.globalStreamIndex == PDB::NilStreamIndex)
+ {
+ return ErrorCode::InvalidStreamIndex;
+ }
+
+ DirectMSFStream globalStream = file.CreateMSFStream<DirectMSFStream>(m_header.globalStreamIndex);
+
+ // the global symbol stream starts with a hash table header
+ const HashTableHeader hashHeader = globalStream.ReadAtOffset<HashTableHeader>(0u);
+ if (hashHeader.signature != HashTableHeader::Signature)
+ {
+ return ErrorCode::InvalidSignature;
+ }
+ else if (hashHeader.version != HashTableHeader::Version)
+ {
+ return ErrorCode::UnknownVersion;
+ }
+
+ return ErrorCode::Success;
+}
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB_NO_DISCARD PDB::ErrorCode PDB::DBIStream::HasValidSectionContributionStream(const RawFile& /* file */) const PDB_NO_EXCEPT
+{
+ if (m_header.sectionContributionSize < sizeof(DBI::SectionContribution::Version))
+ {
+ return ErrorCode::InvalidStream;
+ }
+
+ // find the section contribution sub-stream
+ // https://llvm.org/docs/PDB/DbiStream.html#section-contribution-substream
+ const uint32_t streamOffset = GetSectionContributionSubstreamOffset(m_header);
+
+ const DBI::SectionContribution::Version version = m_stream.ReadAtOffset<DBI::SectionContribution::Version>(streamOffset);
+ if (version != DBI::SectionContribution::Version::Ver60)
+ {
+ return ErrorCode::UnknownVersion;
+ }
+
+ return ErrorCode::Success;
+}
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB_NO_DISCARD PDB::CoalescedMSFStream PDB::DBIStream::CreateSymbolRecordStream(const RawFile& file) const PDB_NO_EXCEPT
+{
+ // the symbol record stream holds the actual CodeView data of the symbols
+ return file.CreateMSFStream<CoalescedMSFStream>(m_header.symbolRecordStreamIndex);
+}
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB_NO_DISCARD PDB::ImageSectionStream PDB::DBIStream::CreateImageSectionStream(const RawFile& file) const PDB_NO_EXCEPT
+{
+ // find the debug header sub-stream
+ const uint32_t debugHeaderOffset = GetDebugHeaderSubstreamOffset(m_header);
+ const DBI::DebugHeader& debugHeader = m_stream.ReadAtOffset<DBI::DebugHeader>(debugHeaderOffset);
+
+ // from there, grab the section header stream
+ return ImageSectionStream(file, debugHeader.sectionHeaderStreamIndex);
+}
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB_NO_DISCARD PDB::PublicSymbolStream PDB::DBIStream::CreatePublicSymbolStream(const RawFile& file) const PDB_NO_EXCEPT
+{
+ DirectMSFStream publicStream = file.CreateMSFStream<DirectMSFStream>(m_header.publicStreamIndex);
+
+ // the public symbol stream always begins with a header, we are not interested in that.
+ // following the public symbol stream header is a hash table header.
+ // we use this to work out how many symbol records are referenced by the public symbol stream.
+ const HashTableHeader hashHeader = publicStream.ReadAtOffset<HashTableHeader>(sizeof(PublicStreamHeader));
+ const uint32_t recordCount = hashHeader.size / sizeof(HashRecord);
+
+ return PublicSymbolStream(file, m_header.publicStreamIndex, recordCount);
+}
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB_NO_DISCARD PDB::GlobalSymbolStream PDB::DBIStream::CreateGlobalSymbolStream(const RawFile& file) const PDB_NO_EXCEPT
+{
+ DirectMSFStream globalStream = file.CreateMSFStream<DirectMSFStream>(m_header.globalStreamIndex);
+
+ // the global symbol stream starts with a hash table header.
+ // we use this to work out how many symbol records are referenced by the global symbol stream.
+ const HashTableHeader hashHeader = globalStream.ReadAtOffset<HashTableHeader>(0u);
+ const uint32_t recordCount = hashHeader.size / sizeof(HashRecord);
+
+ return GlobalSymbolStream(file, m_header.globalStreamIndex, recordCount);
+}
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB_NO_DISCARD PDB::SourceFileStream PDB::DBIStream::CreateSourceFileStream(const RawFile& /* file */) const PDB_NO_EXCEPT
+{
+ // find the source info sub-stream
+ // https://llvm.org/docs/PDB/DbiStream.html#file-info-substream
+ const uint32_t streamOffset = GetSourceInfoSubstreamOffset(m_header);
+
+ return SourceFileStream(m_stream, m_header.sourceInfoSize, streamOffset);
+}
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB_NO_DISCARD PDB::SectionContributionStream PDB::DBIStream::CreateSectionContributionStream(const RawFile& /* file */) const PDB_NO_EXCEPT
+{
+ // find the section contribution sub-stream
+ // https://llvm.org/docs/PDB/DbiStream.html#section-contribution-substream
+ const uint32_t streamOffset = GetSectionContributionSubstreamOffset(m_header);
+
+ return SectionContributionStream(m_stream, m_header.sectionContributionSize - sizeof(DBI::SectionContribution::Version), streamOffset + sizeof(DBI::SectionContribution::Version));
+}
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB_NO_DISCARD PDB::ModuleInfoStream PDB::DBIStream::CreateModuleInfoStream(const RawFile& /* file */) const PDB_NO_EXCEPT
+{
+ // find the module info sub-stream
+ // https://llvm.org/docs/PDB/DbiStream.html#module-info-substream
+ const uint32_t streamOffset = GetModuleInfoSubstreamOffset(m_header);
+
+ return ModuleInfoStream(m_stream, m_header.moduleInfoSize, streamOffset);
+}
diff --git a/thirdparty/raw_pdb/src/PDB_DBIStream.h b/thirdparty/raw_pdb/src/PDB_DBIStream.h
new file mode 100644
index 000000000..4b525980f
--- /dev/null
+++ b/thirdparty/raw_pdb/src/PDB_DBIStream.h
@@ -0,0 +1,65 @@
+// Copyright 2011-2022, Molecular Matters GmbH <[email protected]>
+// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
+
+#pragma once
+
+#include "Foundation/PDB_Macros.h"
+#include "PDB_ErrorCodes.h"
+#include "PDB_DBITypes.h"
+#include "PDB_CoalescedMSFStream.h"
+#include "PDB_DirectMSFStream.h"
+#include "PDB_ImageSectionStream.h"
+#include "PDB_PublicSymbolStream.h"
+#include "PDB_GlobalSymbolStream.h"
+#include "PDB_SourceFileStream.h"
+#include "PDB_SectionContributionStream.h"
+#include "PDB_ModuleInfoStream.h"
+
+
+// PDB DBI Stream
+// https://llvm.org/docs/PDB/DbiStream.html
+namespace PDB
+{
+ class RawFile;
+
+
+ class PDB_NO_DISCARD DBIStream
+ {
+ public:
+ DBIStream(void) PDB_NO_EXCEPT;
+ explicit DBIStream(const RawFile& file, const DBI::StreamHeader& header) PDB_NO_EXCEPT;
+
+ PDB_DEFAULT_MOVE(DBIStream);
+
+ PDB_NO_DISCARD ErrorCode HasValidSymbolRecordStream(const RawFile& file) const PDB_NO_EXCEPT;
+ PDB_NO_DISCARD ErrorCode HasValidImageSectionStream(const RawFile& file) const PDB_NO_EXCEPT;
+ PDB_NO_DISCARD ErrorCode HasValidPublicSymbolStream(const RawFile& file) const PDB_NO_EXCEPT;
+ PDB_NO_DISCARD ErrorCode HasValidGlobalSymbolStream(const RawFile& file) const PDB_NO_EXCEPT;
+ PDB_NO_DISCARD ErrorCode HasValidSectionContributionStream(const RawFile& file) const PDB_NO_EXCEPT;
+
+ PDB_NO_DISCARD CoalescedMSFStream CreateSymbolRecordStream(const RawFile& file) const PDB_NO_EXCEPT;
+ PDB_NO_DISCARD ImageSectionStream CreateImageSectionStream(const RawFile& file) const PDB_NO_EXCEPT;
+ PDB_NO_DISCARD PublicSymbolStream CreatePublicSymbolStream(const RawFile& file) const PDB_NO_EXCEPT;
+ PDB_NO_DISCARD GlobalSymbolStream CreateGlobalSymbolStream(const RawFile& file) const PDB_NO_EXCEPT;
+ PDB_NO_DISCARD SourceFileStream CreateSourceFileStream(const RawFile& file) const PDB_NO_EXCEPT;
+ PDB_NO_DISCARD SectionContributionStream CreateSectionContributionStream(const RawFile& file) const PDB_NO_EXCEPT;
+ PDB_NO_DISCARD ModuleInfoStream CreateModuleInfoStream(const RawFile& file) const PDB_NO_EXCEPT;
+
+ PDB_NO_DISCARD const DBI::StreamHeader& GetHeader(void) const PDB_NO_EXCEPT
+ {
+ return m_header;
+ }
+
+ private:
+ DBI::StreamHeader m_header;
+ DirectMSFStream m_stream;
+
+ PDB_DISABLE_COPY(DBIStream);
+ };
+
+ // Returns whether the given raw file provides a valid DBI stream.
+ PDB_NO_DISCARD ErrorCode HasValidDBIStream(const RawFile& file) PDB_NO_EXCEPT;
+
+ // Creates the DBI stream from a raw file.
+ PDB_NO_DISCARD DBIStream CreateDBIStream(const RawFile& file) PDB_NO_EXCEPT;
+}
diff --git a/thirdparty/raw_pdb/src/PDB_DBITypes.cpp b/thirdparty/raw_pdb/src/PDB_DBITypes.cpp
new file mode 100644
index 000000000..4eaedcf51
--- /dev/null
+++ b/thirdparty/raw_pdb/src/PDB_DBITypes.cpp
@@ -0,0 +1,9 @@
+// Copyright 2011-2022, Molecular Matters GmbH <[email protected]>
+// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
+
+#include "PDB_PCH.h"
+#include "PDB_DBITypes.h"
+
+
+const uint32_t PDB::DBI::StreamHeader::Signature = 0xffffffffu;
+const uint16_t PDB::DBI::DebugHeader::InvalidStreamIndex = 0xFFFFu;
diff --git a/thirdparty/raw_pdb/src/PDB_DBITypes.h b/thirdparty/raw_pdb/src/PDB_DBITypes.h
new file mode 100644
index 000000000..9a798d49e
--- /dev/null
+++ b/thirdparty/raw_pdb/src/PDB_DBITypes.h
@@ -0,0 +1,928 @@
+// Copyright 2011-2022, Molecular Matters GmbH <[email protected]>
+// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
+
+#pragma once
+
+#include "Foundation/PDB_Macros.h"
+#include "Foundation/PDB_BitOperators.h"
+
+
+namespace PDB
+{
+ namespace DBI
+ {
+ // https://llvm.org/docs/PDB/DbiStream.html#stream-header
+ // https://github.com/microsoft/microsoft-pdb/blob/master/PDB/dbi/dbi.h#L124
+ struct StreamHeader
+ {
+ static const uint32_t Signature;
+
+ enum class PDB_NO_DISCARD Version : uint32_t
+ {
+ VC41 = 930803u,
+ V50 = 19960307u,
+ V60 = 19970606u,
+ V70 = 19990903u,
+ V110 = 20091201u
+ };
+
+ uint32_t signature;
+ Version version;
+ uint32_t age;
+ uint16_t globalStreamIndex; // index of the global symbol stream
+ uint16_t toolchain;
+ uint16_t publicStreamIndex; // index of the public symbol stream
+ uint16_t pdbDllVersion;
+ uint16_t symbolRecordStreamIndex; // index of the symbol record stream
+ uint16_t pdbDllRbld;
+ uint32_t moduleInfoSize;
+ uint32_t sectionContributionSize;
+ uint32_t sectionMapSize;
+ uint32_t sourceInfoSize;
+ uint32_t typeServerMapSize;
+ uint32_t mfcTypeServerIndex;
+ uint32_t optionalDebugHeaderSize;
+ uint32_t ecSize;
+ uint16_t flags;
+ uint16_t machine;
+ uint32_t padding;
+ };
+
+ // https://llvm.org/docs/PDB/DbiStream.html#optional-debug-header-stream
+ struct DebugHeader
+ {
+ static const uint16_t InvalidStreamIndex;
+
+ uint16_t fpoDataStreamIndex; // IMAGE_DEBUG_TYPE_FPO
+ uint16_t exceptionDataStreamIndex; // IMAGE_DEBUG_TYPE_EXCEPTION
+ uint16_t fixupDataStreamIndex; // IMAGE_DEBUG_TYPE_FIXUP
+ uint16_t omapToSrcDataStreamIndex; // IMAGE_DEBUG_TYPE_OMAP_TO_SRC
+ uint16_t omapFromSrcDataStreamIndex; // IMAGE_DEBUG_TYPE_OMAP_FROM_SRC
+ uint16_t sectionHeaderStreamIndex; // a dump of all section headers (IMAGE_SECTION_HEADER) from the original executable
+ uint16_t tokenDataStreamIndex;
+ uint16_t xdataStreamIndex;
+ uint16_t pdataStreamIndex;
+ uint16_t newFpoDataStreamIndex;
+ uint16_t originalSectionHeaderDataStreamIndex;
+ };
+
+ // https://llvm.org/docs/PDB/DbiStream.html#section-contribution-substream
+ struct SectionContribution
+ {
+ enum class PDB_NO_DISCARD Version : uint32_t
+ {
+ Ver60 = 0xeffe0000u + 19970605u,
+ V2 = 0xeffe0000u + 20140516u
+ };
+
+ uint16_t section;
+ uint16_t padding;
+ uint32_t offset;
+ uint32_t size;
+ uint32_t characteristics;
+ uint16_t moduleIndex;
+ uint16_t padding2;
+ uint32_t dataCrc;
+ uint32_t relocationCrc;
+ };
+
+ // https://llvm.org/docs/PDB/DbiStream.html#module-info-substream
+ struct ModuleInfo
+ {
+ uint32_t unused;
+ SectionContribution sectionContribution;
+ uint16_t flags;
+ uint16_t moduleSymbolStreamIndex;
+ uint32_t symbolSize;
+ uint32_t c11Size;
+ uint32_t c13Size;
+ uint16_t sourceFileCount;
+ uint16_t padding;
+ uint32_t unused2;
+ uint32_t sourceFileNameIndex;
+ uint32_t pdbFilePathNameIndex;
+ };
+ }
+
+
+ namespace CodeView
+ {
+ namespace DBI
+ {
+ // code view type records that can appear in a DBI stream.
+ // this list is not exhaustive, but only contains what we need so far.
+ // https://llvm.org/docs/PDB/CodeViewSymbols.html
+ // https://llvm.org/docs/PDB/TpiStream.html#tpi-vs-ipi-stream
+ // https://github.com/microsoft/microsoft-pdb/blob/master/include/cvinfo.h#L2735
+ enum class PDB_NO_DISCARD SymbolRecordKind : uint16_t
+ {
+ S_END = 0x0006u, // block, procedure, "with" or thunk end
+ S_SKIP = 0x0007u, // Reserve symbol space in $$Symbols table
+ S_FRAMEPROC = 0x1012u, // extra frame and proc information
+ S_ANNOTATION = 0x1019u, // annotation string literals ("__annotation" intrinsic, e.g. via NT_ASSERT)
+ S_OBJNAME = 0x1101u, // full path to the original compiled .obj. can point to remote locations and temporary files, not necessarily the file that was linked into the executable
+ S_THUNK32 = 0x1102u, // thunk start
+ S_BLOCK32 = 0x1103u, // block start
+ S_LABEL32 = 0x1105u, // code label
+ S_REGISTER = 0x1106u, // register variable
+ S_CONSTANT = 0x1107u, // constant symbol
+ S_BPREL32 = 0x110Bu, // BP-relative address (almost like S_REGREL32)
+ S_LDATA32 = 0x110Cu, // (static) local data
+ S_GDATA32 = 0x110Du, // global data
+ S_PUB32 = 0x110Eu, // public symbol
+ S_LPROC32 = 0x110Fu, // local procedure start
+ S_GPROC32 = 0x1110u, // global procedure start
+ S_REGREL32 = 0x1111u, // register relative address
+ S_LTHREAD32 = 0x1112u, // (static) thread-local data
+ S_GTHREAD32 = 0x1113u, // global thread-local data
+ S_UNAMESPACE = 0x1124u, // using namespace
+ S_PROCREF = 0x1125u, // reference to function in any compiland
+ S_LPROCREF = 0x1127u, // local reference to function in any compiland
+ S_TRAMPOLINE = 0x112Cu, // incremental linking trampoline
+ S_SEPCODE = 0x1132u, // separated code (from the compiler)
+ S_SECTION = 0x1136u, // a COFF section in an executable
+ S_COFFGROUP = 0x1137u, // original COFF group before it was merged into executable sections by the linker, e.g. .CRT$XCU, .rdata, .bss, .lpp_prepatch_hooks
+ S_CALLSITEINFO = 0x1139u, // Indirect call site information
+ S_FRAMECOOKIE = 0x113Au, // Security cookie information
+ S_COMPILE3 = 0x113Cu, // replacement for S_COMPILE2, more info
+ S_ENVBLOCK = 0x113Du, // environment block split off from S_COMPILE2
+ S_LOCAL = 0x113Eu, // defines a local symbol in optimized code
+ S_DEFRANGE_REGISTER = 0x1141u, // ranges for en-registered symbol
+ S_DEFRANGE_FRAMEPOINTER_REL = 0x1142u, // range for stack symbol.
+ S_DEFRANGE_SUBFIELD_REGISTER = 0x1143u, // ranges for en-registered field of symbol
+ S_DEFRANGE_FRAMEPOINTER_REL_FULL_SCOPE = 0x1144u, // range for stack symbol span valid full scope of function body, gap might apply.
+ S_DEFRANGE_REGISTER_REL = 0x1145u, // range for symbol address as register + offset.
+ S_LPROC32_ID = 0x1146u, // S_PROC symbol that references ID instead of type
+ S_GPROC32_ID = 0x1147u, // S_PROC symbol that references ID instead of type
+ S_BUILDINFO = 0x114Cu, // build info/environment details of a compiland/translation unit
+ S_INLINESITE = 0x114Du, // inlined function callsite
+ S_INLINESITE_END = 0x114Eu,
+ S_PROC_ID_END = 0x114Fu,
+ S_FILESTATIC = 0x1153u,
+ S_LPROC32_DPC = 0x1155u,
+ S_LPROC32_DPC_ID = 0x1156u,
+ S_ARMSWITCHTABLE = 0x1159u,
+ S_CALLEES = 0x115Au,
+ S_CALLERS = 0x115Bu,
+ S_INLINESITE2 = 0x115Du, // extended inline site information
+ S_HEAPALLOCSITE = 0x115Eu, // heap allocation site
+ S_INLINEES = 0x1168u, // https://llvm.org/docs/PDB/CodeViewSymbols.html#s-inlinees-0x1168
+ S_REGREL32_INDIR = 0x1171u,
+ S_REGREL32_ENCTMP = 0x1179u,
+ S_UDT = 0x1108u, // user-defined type
+ S_UDT_ST = 0x1003u, // user-defined structured types
+ };
+
+ // https://docs.microsoft.com/en-us/visualstudio/debugger/debug-interface-access/thunk-ordinal
+ enum class PDB_NO_DISCARD ThunkOrdinal : uint8_t
+ {
+ NoType,
+ ThisAdjustor,
+ VirtualCall,
+ PCode,
+ DelayLoad,
+ TrampolineIncremental,
+ TrampolineBranchIsland
+ };
+
+ enum class PDB_NO_DISCARD TrampolineType : uint16_t
+ {
+ Incremental,
+ BranchIsland
+ };
+
+ enum class PDB_NO_DISCARD CookieType : uint8_t
+ {
+ COPY = 0,
+ XOR_SP,
+ XOR_BP,
+ XOR_R13,
+ };
+
+ // https://github.com/microsoft/microsoft-pdb/blob/master/include/cvconst.h#L392
+ enum class PDB_NO_DISCARD Register : uint16_t
+ {
+ EAX = 17,
+ ECX = 18,
+ EDX = 19,
+ EBX = 20,
+ ESP = 21,
+ EBP = 22,
+ ESI = 23,
+ EDI = 24,
+
+ RAX = 328,
+ RBX = 329,
+ RCX = 330,
+ RDX = 331,
+ RSI = 332,
+ RDI = 333,
+ RBP = 334,
+ RSP = 335,
+ R8 = 336,
+ R9 = 337,
+ R10 = 338,
+ R11 = 339,
+ R12 = 340,
+ R13 = 341,
+ R14 = 342,
+ R15 = 343,
+
+ RIP = 33, // also EIP for x32
+ EFLAGS = 34, // same for x64 and x32
+ };
+
+
+ // https://github.com/microsoft/microsoft-pdb/blob/master/include/cvinfo.h#L3038
+ enum class PDB_NO_DISCARD ProcedureFlags : uint8_t
+ {
+ None = 0u,
+ NoFPO = 1u << 0u,
+ InterruptReturn = 1u << 1u,
+ FarReturn = 1u << 2u,
+ NoReturn = 1u << 3u,
+ Unreachable = 1u << 4u,
+ CustomCallingConvention = 1u << 5u,
+ NoInline = 1u << 6u,
+ OptimizedDebugInformation = 1u << 7u
+ };
+ PDB_DEFINE_BIT_OPERATORS(ProcedureFlags);
+
+
+ // https://github.com/microsoft/microsoft-pdb/blob/master/include/cvinfo.h#L3676
+ enum class PDB_NO_DISCARD PublicSymbolFlags : uint32_t
+ {
+ None = 0u,
+ Code = 1u << 0u, // set if public symbol refers to a code address
+ Function = 1u << 1u, // set if public symbol is a function
+ ManagedCode = 1u << 2u, // set if managed code (native or IL)
+ ManagedILCode = 1u << 3u // set if managed IL code
+ };
+ PDB_DEFINE_BIT_OPERATORS(PublicSymbolFlags);
+
+
+ // https://github.com/microsoft/microsoft-pdb/blob/master/include/cvinfo.h#L3341
+ enum class PDB_NO_DISCARD CompileSymbolFlags : uint32_t
+ {
+ None = 0u,
+ SourceLanguageMask = 0xFFu,
+ EC = 1u << 8u,
+ NoDebugInfo = 1u << 9u,
+ LTCG = 1u << 10u,
+ NoDataAlign = 1u << 11u,
+ ManagedCodeOrDataPresent = 1u << 12u,
+ SecurityChecks = 1u << 13u,
+ HotPatch = 1u << 14u,
+ CVTCIL = 1u << 15u,
+ MSILModule = 1u << 16u,
+ SDL = 1u << 17u,
+ PGO = 1u << 18u,
+ Exp = 1u << 19u
+ };
+ PDB_DEFINE_BIT_OPERATORS(CompileSymbolFlags);
+
+
+ // https://github.com/microsoft/microsoft-pdb/blob/master/include/cvconst.h#L324
+ enum class PDB_NO_DISCARD CPUType : uint16_t
+ {
+ Intel8080 = 0x0,
+ Intel8086 = 0x1,
+ Intel80286 = 0x2,
+ Intel80386 = 0x3,
+ Intel80486 = 0x4,
+ Pentium = 0x5,
+ PentiumII = 0x6,
+ PentiumPro = PentiumII,
+ PentiumIII = 0x7,
+ MIPS = 0x10,
+ MIPSR4000 = MIPS,
+ MIPS16 = 0x11,
+ MIPS32 = 0x12,
+ MIPS64 = 0x13,
+ MIPSI = 0x14,
+ MIPSII = 0x15,
+ MIPSIII = 0x16,
+ MIPSIV = 0x17,
+ MIPSV = 0x18,
+ M68000 = 0x20,
+ M68010 = 0x21,
+ M68020 = 0x22,
+ M68030 = 0x23,
+ M68040 = 0x24,
+ Alpha = 0x30,
+ Alpha21164 = 0x31,
+ Alpha21164A = 0x32,
+ Alpha21264 = 0x33,
+ Alpha21364 = 0x34,
+ PPC601 = 0x40,
+ PPC603 = 0x41,
+ PPC604 = 0x42,
+ PPC620 = 0x43,
+ PPCFP = 0x44,
+ PPCBE = 0x45,
+ SH3 = 0x50,
+ SH3E = 0x51,
+ SH3DSP = 0x52,
+ SH4 = 0x53,
+ SHMedia = 0x54,
+ ARM3 = 0x60,
+ ARM4 = 0x61,
+ ARM4T = 0x62,
+ ARM5 = 0x63,
+ ARM5T = 0x64,
+ ARM6 = 0x65,
+ ARM_XMAC = 0x66,
+ ARM_WMMX = 0x67,
+ ARM7 = 0x68,
+ Omni = 0x70,
+ IA64 = 0x80,
+ IA64_1 = 0x80,
+ IA64_2 = 0x81,
+ CEE = 0x90,
+ AM33 = 0xA0,
+ M32R = 0xB0,
+ TriCore = 0xC0,
+ X64 = 0xD0,
+ AMD64 = X64,
+ EBC = 0xE0,
+ Thumb = 0xF0,
+ ARMNT = 0xF4,
+ ARM64 = 0xF6,
+ HybridX86ARM64 = 0xF7,
+ ARM64EC = 0xF8,
+ ARM64X = 0xF9,
+ D3D11_Shader = 0x100
+ };
+
+ // https://github.com/microsoft/microsoft-pdb/blob/master/include/cvinfo.h#L3100
+ // represents an address range, used for optimized code debug info
+ struct LocalVariableAddressRange // defines a range of addresses
+ {
+ uint32_t offsetStart;
+ uint16_t isectionStart;
+ uint16_t length;
+ };
+
+ // https://github.com/microsoft/microsoft-pdb/blob/master/include/cvinfo.h#L3108
+ // Represents the holes in overall address range, all address is pre-bbt.
+ // it is for compress and reduce the amount of relocations need.
+ struct LocalVariableAddressGap
+ {
+ uint16_t offset; // relative offset from the beginning of the live range.
+ uint16_t length; // length of this gap.
+ };
+
+ // https://github.com/microsoft/microsoft-pdb/blob/0fe89a942f9a0f8e061213313e438884f4c9b876/include/cvinfo.h#L4366
+ // https://github.com/microsoft/microsoft-pdb/blob/0fe89a942f9a0f8e061213313e438884f4c9b876/cvdump/dumpsym7.cpp#L5518
+ enum class ARMSwitchType : uint16_t
+ {
+ INT1 = 0, // signed byte
+ UINT1 = 1, // unsigned byte
+ INT2 = 2, // signed two byte
+ UINT2 = 3, // unsigned two byte
+ INT4 = 4, // signed four byte
+ UINT4 = 5, // unsigned four byte
+ POINTER = 6,
+ UINT1SHL1 = 7, // unsigned byte scaled by two
+ UINT2SHL1 = 8, // unsigned two byte scaled by two
+ INT1SHL1 = 9, // signed byte scaled by two
+ INT2SHL1 = 10, // signed two byte scaled by two
+ TBB = UINT1SHL1,
+ TBH = UINT2SHL1,
+ };
+
+ // https://llvm.org/docs/PDB/CodeViewTypes.html#leaf-types
+ struct RecordHeader
+ {
+ uint16_t size; // record length, not including this 2-byte field
+ SymbolRecordKind kind; // record kind
+ };
+
+ // all CodeView records are stored as a header, followed by variable-length data.
+ // internal Record structs such as S_PUB32, S_GDATA32, etc. correspond to the data layout of a CodeView record of that kind.
+ struct Record
+ {
+ RecordHeader header;
+ union Data
+ {
+#pragma pack(push, 1)
+ // https://github.com/microsoft/microsoft-pdb/blob/master/include/cvinfo.h#L4069
+ struct
+ {
+ uint32_t cbFrame; // count of bytes of total frame of procedure
+ uint32_t cbPad; // count of bytes of padding in the frame
+ uint32_t offPad; // offset (relative to frame poniter) to where
+ // padding starts
+ uint32_t cbSaveRegs; // count of bytes of callee save registers
+ uint32_t offExHdlr; // offset of exception handler
+ uint16_t sectExHdlr; // section id of exception handler
+
+ struct {
+ uint32_t fHasAlloca : 1; // function uses _alloca()
+ uint32_t fHasSetJmp : 1; // function uses setjmp()
+ uint32_t fHasLongJmp : 1; // function uses longjmp()
+ uint32_t fHasInlAsm : 1; // function uses inline asm
+ uint32_t fHasEH : 1; // function has EH states
+ uint32_t fInlSpec : 1; // function was speced as inline
+ uint32_t fHasSEH : 1; // function has SEH
+ uint32_t fNaked : 1; // function is __declspec(naked)
+ uint32_t fSecurityChecks : 1; // function has buffer security check introduced by /GS.
+ uint32_t fAsyncEH : 1; // function compiled with /EHa
+ uint32_t fGSNoStackOrdering : 1; // function has /GS buffer checks, but stack ordering couldn't be done
+ uint32_t fWasInlined : 1; // function was inlined within another function
+ uint32_t fGSCheck : 1; // function is __declspec(strict_gs_check)
+ uint32_t fSafeBuffers : 1; // function is __declspec(safebuffers)
+ uint32_t encodedLocalBasePointer : 2; // record function's local pointer explicitly.
+ uint32_t encodedParamBasePointer : 2; // record function's parameter pointer explicitly.
+ uint32_t fPogoOn : 1; // function was compiled with PGO/PGU
+ uint32_t fValidCounts : 1; // Do we have valid Pogo counts?
+ uint32_t fOptSpeed : 1; // Did we optimize for speed?
+ uint32_t fGuardCF : 1; // function contains CFG checks (and no write checks)
+ uint32_t fGuardCFW : 1; // function contains CFW checks and/or instrumentation
+ uint32_t pad : 9; // must be zero
+ } flags;
+ } S_FRAMEPROC;
+
+ struct
+ {
+ uint32_t offset;
+ uint16_t section;
+ uint16_t annotationsCount; // number of zero-terminated annotation strings
+ PDB_FLEXIBLE_ARRAY_MEMBER(char, annotations); // sequence of zero-terminated annotation strings
+ } S_ANNOTATIONSYM;
+
+ // https://github.com/microsoft/microsoft-pdb/blob/master/include/cvinfo.h#L3696
+ struct
+ {
+ PublicSymbolFlags flags;
+ uint32_t offset;
+ uint16_t section;
+ PDB_FLEXIBLE_ARRAY_MEMBER(char, name);
+ } S_PUB32;
+
+ struct
+ {
+ uint32_t typeIndex;
+ uint32_t offset;
+ uint16_t section;
+ PDB_FLEXIBLE_ARRAY_MEMBER(char, name);
+ } S_GDATA32, S_GTHREAD32, S_LDATA32, S_LTHREAD32;
+
+ struct
+ {
+ PDB_FLEXIBLE_ARRAY_MEMBER(char, name);
+ } S_UNAMESPACE;
+
+ struct
+ {
+ uint32_t signature;
+ PDB_FLEXIBLE_ARRAY_MEMBER(char, name);
+ } S_OBJNAME;
+
+ struct
+ {
+ TrampolineType type;
+ uint16_t size;
+ uint32_t thunkOffset;
+ uint32_t targetOffset;
+ uint16_t thunkSection;
+ uint16_t targetSection;
+ } S_TRAMPOLINE;
+
+ struct
+ {
+ uint16_t sectionNumber;
+ uint8_t alignment;
+ uint32_t rva;
+ uint32_t length;
+ uint32_t characteristics;
+ PDB_FLEXIBLE_ARRAY_MEMBER(char, name);
+ } S_SECTION;
+
+ struct
+ {
+ uint32_t size;
+ uint32_t characteristics;
+ uint32_t offset;
+ uint16_t section;
+ PDB_FLEXIBLE_ARRAY_MEMBER(char, name);
+ } S_COFFGROUP;
+
+ struct
+ {
+ uint32_t offset ; // offset of call site
+ uint16_t section; // section index of call site
+ uint16_t padding; // alignment padding field, must be zero
+ uint32_t typeIndex; // type index describing function signature
+ } S_CALLSITEINFO;
+
+ struct
+ {
+ uint32_t offset; // Frame relative offset
+ uint16_t reg; // Register index
+ CookieType cookietype; // Type of the cookie
+ uint8_t flags; // Flags describing this cookie
+ } S_FRAMECOOKIE;
+
+ struct
+ {
+ uint32_t parent;
+ uint32_t end;
+ uint32_t next;
+ uint32_t offset;
+ uint16_t section;
+ uint16_t length;
+ ThunkOrdinal thunk;
+ PDB_FLEXIBLE_ARRAY_MEMBER(char, name);
+ } S_THUNK32;
+
+ struct
+ {
+ uint32_t parent;
+ uint32_t end;
+ uint32_t next;
+ uint32_t codeSize;
+ uint32_t debugStart;
+ uint32_t debugEnd;
+ uint32_t typeIndex;
+ uint32_t offset;
+ uint16_t section;
+ ProcedureFlags flags;
+ PDB_FLEXIBLE_ARRAY_MEMBER(char, name);
+ } S_LPROC32, S_GPROC32, S_LPROC32_ID, S_GPROC32_ID, S_LPROC32_DPC, S_LPROC32_DPC_ID;
+
+ struct
+ {
+ uint32_t offset;
+ uint32_t typeIndex;
+ PDB_FLEXIBLE_ARRAY_MEMBER(char, name);
+ } S_BPRELSYM32;
+
+ struct
+ {
+ uint32_t offset;
+ uint32_t typeIndex;
+ Register reg;
+ PDB_FLEXIBLE_ARRAY_MEMBER(char, name);
+ } S_REGREL32, S_REGREL32_ENCTMP;
+
+ struct
+ {
+ uint32_t typeIndex;
+ Register reg;
+ PDB_FLEXIBLE_ARRAY_MEMBER(char, name);
+ } S_REGSYM;
+
+ struct
+ {
+ uint32_t parent;
+ uint32_t end;
+ uint32_t codeSize;
+ uint32_t offset;
+ uint16_t section;
+ PDB_FLEXIBLE_ARRAY_MEMBER(char, name);
+ } S_BLOCK32;
+
+ struct
+ {
+ uint32_t offset;
+ uint16_t section;
+ ProcedureFlags flags;
+ PDB_FLEXIBLE_ARRAY_MEMBER(char, name);
+ } S_LABEL32;
+
+ struct
+ {
+ uint32_t typeIndex;
+ uint16_t value;
+ PDB_FLEXIBLE_ARRAY_MEMBER(char, name);
+ } S_CONSTANT;
+
+ struct
+ {
+ uint32_t typeIndex; // refers to a type index in the IPI stream
+ } S_BUILDINFO;
+
+ struct
+ {
+ uint32_t parent; // pointer to the inliner
+ uint32_t end; // pointer to this block's end
+ uint32_t inlinee; // CV_ItemId of inlinee
+ PDB_FLEXIBLE_ARRAY_MEMBER(uint8_t, binaryAnnotations);
+ } S_INLINESITE;
+
+ // https://github.com/microsoft/microsoft-pdb/blob/master/include/cvinfo.h#L4199
+ struct
+ {
+ uint32_t typeIndex; // type index
+ uint32_t moduleFilenameOffset; // index of mod filename in stringtable
+
+ struct
+ {
+ uint16_t fIsParam : 1; // variable is a parameter
+ uint16_t fAddrTaken : 1; // address is taken
+ uint16_t fCompGenx : 1; // variable is compiler generated
+ uint16_t fIsAggregate : 1; // the symbol is splitted in temporaries,
+ // which are treated by compiler as
+ // independent entities
+ uint16_t fIsAggregated : 1; // Counterpart of fIsAggregate - tells
+ // that it is a part of a fIsAggregate symbol
+ uint16_t fIsAliased : 1; // variable has multiple simultaneous lifetimes
+ uint16_t fIsAlias : 1; // represents one of the multiple simultaneous lifetimes
+ uint16_t fIsRetValue : 1; // represents a function return value
+ uint16_t fIsOptimizedOut : 1; // variable has no lifetimes
+ uint16_t fIsEnregGlob : 1; // variable is an enregistered global
+ uint16_t fIsEnregStat : 1; // variable is an enregistered static
+ uint16_t unused : 5; // must be zero
+ } flags;
+
+ PDB_FLEXIBLE_ARRAY_MEMBER(char, name);
+ } S_FILESTATIC;
+
+ struct
+ {
+ CompileSymbolFlags flags;
+ CPUType machine;
+ uint16_t versionFrontendMajor;
+ uint16_t versionFrontendMinor;
+ uint16_t versionFrontendBuild;
+ uint16_t versionFrontendQFE;
+ uint16_t versionBackendMajor;
+ uint16_t versionBackendMinor;
+ uint16_t versionBackendBuild;
+ uint16_t versionBackendQFE;
+ PDB_FLEXIBLE_ARRAY_MEMBER(char, version);
+ } S_COMPILE3;
+
+ // https://github.com/microsoft/microsoft-pdb/blob/master/include/cvinfo.h#L3372
+ struct
+ {
+ uint8_t flags;
+ PDB_FLEXIBLE_ARRAY_MEMBER(char, strings);
+ } S_ENVBLOCK;
+
+ // https://github.com/microsoft/microsoft-pdb/blob/master/include/cvinfo.h#L4190
+ struct
+ {
+ uint32_t typeIndex;
+
+ struct
+ {
+ uint16_t fIsParam : 1; // variable is a parameter
+ uint16_t fAddrTaken : 1; // address is taken
+ uint16_t fCompGenx : 1; // variable is compiler generated
+ uint16_t fIsAggregate : 1; // the symbol is splitted in temporaries,
+ // which are treated by compiler as
+ // independent entities
+ uint16_t fIsAggregated : 1; // Counterpart of fIsAggregate - tells
+ // that it is a part of a fIsAggregate symbol
+ uint16_t fIsAliased : 1; // variable has multiple simultaneous lifetimes
+ uint16_t fIsAlias : 1; // represents one of the multiple simultaneous lifetimes
+ uint16_t fIsRetValue : 1; // represents a function return value
+ uint16_t fIsOptimizedOut : 1; // variable has no lifetimes
+ uint16_t fIsEnregGlob : 1; // variable is an enregistered global
+ uint16_t fIsEnregStat : 1; // variable is an enregistered static
+ uint16_t unused : 5; // must be zero
+ } flags;
+
+ PDB_FLEXIBLE_ARRAY_MEMBER(char, name);
+ } S_LOCAL;
+
+ // https://github.com/microsoft/microsoft-pdb/blob/master/include/cvinfo.h#L4236
+ struct
+ {
+ uint16_t reg; // Register to hold the value of the symbol
+
+ struct
+ {
+ uint16_t maybe : 1; // May have no user name on one of control flow path.
+ uint16_t padding : 15; // Padding for future use.
+ } attribute; // Attribute of the register range.
+
+ LocalVariableAddressRange range; // Range of addresses where this program is valid
+ PDB_FLEXIBLE_ARRAY_MEMBER(LocalVariableAddressGap, gaps); // The value is not available in following gaps.
+ } S_DEFRANGE_REGISTER;
+
+ // https://github.com/microsoft/microsoft-pdb/blob/master/include/cvinfo.h#L4245
+ struct
+ {
+ uint32_t offsetFramePointer;
+ LocalVariableAddressRange range; // Range of addresses where this program is valid
+ PDB_FLEXIBLE_ARRAY_MEMBER(LocalVariableAddressGap, gaps); // The value is not available in following gaps.
+ } S_DEFRANGE_FRAMEPOINTER_REL;
+
+ // https://github.com/microsoft/microsoft-pdb/blob/master/include/cvinfo.h#L4265
+ struct
+ {
+ uint16_t reg; // Register to hold the value of the symbol
+
+ struct
+ {
+ uint16_t maybe : 1; // May have no user name on one of control flow path.
+ uint16_t padding : 15; // Padding for future use.
+ } attribute; // Attribute of the register range.
+
+ uint32_t offsetParent : 12; // Offset in parent variable.
+ uint32_t padding : 20; // Padding for future use.
+ LocalVariableAddressRange range; // Range of addresses where this program is valid
+ PDB_FLEXIBLE_ARRAY_MEMBER(LocalVariableAddressGap, gaps); // The value is not available in following gaps.
+ } S_DEFRANGE_SUBFIELD_REGISTER;
+
+ // https://github.com/microsoft/microsoft-pdb/blob/master/include/cvinfo.h#L4255
+ struct
+ {
+ uint32_t offsetFramePointer; // offset to frame pointer
+ } S_DEFRANGE_FRAMEPOINTER_REL_FULL_SCOPE;
+
+ // https://github.com/microsoft/microsoft-pdb/blob/master/include/cvinfo.h#L4279
+ struct
+ {
+ uint16_t baseRegister; // Register to hold the base pointer of the symbol
+ uint16_t spilledUDTMember : 1; // Spilled member for s.i.
+ uint16_t padding : 3; // Padding for future use.
+ uint16_t offsetParent : 12; // Offset in parent variable.
+ uint32_t offsetBasePointer; // offset to register
+ LocalVariableAddressRange range; // Range of addresses where this program is valid
+ PDB_FLEXIBLE_ARRAY_MEMBER(LocalVariableAddressGap, gaps); // The value is not available in following gaps.
+ } S_DEFRANGE_REGISTER_REL;
+
+ // https://github.com/microsoft/microsoft-pdb/blob/master/include/cvinfo.h#L4500
+ struct
+ {
+ uint32_t offset; // offset of call site
+ uint16_t section; // section index of call site
+ uint16_t instructionLength; // length of heap allocation call instruction
+ uint32_t typeIndex; // type index describing function signature
+ } S_HEAPALLOCSITE;
+
+ // https://github.com/microsoft/microsoft-pdb/blob/master/include/cvinfo.h#L4402
+ struct
+ {
+ uint32_t offsetBase; // Section-relative offset to the base for switch offsets
+ uint16_t sectionBase; // Section index of the base for switch offsets
+ ARMSwitchType switchType; // type of each entry
+ uint32_t offsetBranch; // Section-relative offset to the table branch instruction
+ uint32_t offsetTable; // Section-relative offset to the start of the table
+ uint16_t sectionBranch; // Section index of the table branch instruction
+ uint16_t sectionTable; // Section index of the table
+ uint32_t numEntries; // number of switch table entries
+ } S_ARMSWITCHTABLE;
+
+ // https://github.com/microsoft/microsoft-pdb/blob/master/include/cvinfo.h#L4382
+ struct
+ {
+ uint32_t count; // Number of functions
+ PDB_FLEXIBLE_ARRAY_MEMBER(uint32_t, funcs); // List of functions, dim == count
+ // uint32_t invocations[CV_ZEROLEN]; Followed by a parallel array of
+ // invocation counts. Counts > reclen are assumed to be zero
+ } S_CALLERS, S_CALLEES, S_INLINEES;
+
+ struct
+ {
+ uint32_t typeIndex;
+ PDB_FLEXIBLE_ARRAY_MEMBER(char, name);
+ } S_UDT, S_UDT_ST;
+
+ struct
+ {
+ uint32_t unknown1;
+ uint32_t typeIndex;
+ uint32_t unknown2;
+ Register reg;
+ PDB_FLEXIBLE_ARRAY_MEMBER(char, name);
+
+ } S_REGREL32_INDIR;
+#pragma pack(pop)
+ } data;
+ };
+
+ // https://github.com/microsoft/microsoft-pdb/blob/master/include/cvinfo.h#L4576
+ enum class PDB_NO_DISCARD DebugSubsectionKind : uint32_t
+ {
+ S_IGNORE = 0x80000000, // if this bit is set in a subsection type then ignore the subsection contents
+
+ S_SYMBOLS = 0xF1,
+ S_LINES = 0xF2,
+ S_STRINGTABLE = 0xF3,
+ S_FILECHECKSUMS = 0xF4,
+ S_FRAMEDATA = 0xF5,
+ S_INLINEELINES = 0xF6,
+ S_CROSSSCOPEIMPORTS = 0xF7,
+ S_CROSSSCOPEEXPORTS = 0xF8,
+
+ S_IL_LINES = 0xF9,
+ S_FUNC_MDTOKEN_MAP = 0xFA,
+ S_TYPE_MDTOKEN_MAP = 0xFB,
+ S_MERGED_ASSEMBLYINPUT = 0xFC,
+
+ S_COFF_SYMBOL_RVA = 0xFD,
+ };
+
+ // https://github.com/microsoft/microsoft-pdb/blob/master/include/cvinfo.h#L4596
+ struct DebugSubsectionHeader
+ {
+ DebugSubsectionKind kind;
+ uint32_t size;
+ };
+
+ // https://github.com/microsoft/microsoft-pdb/blob/master/include/cvinfo.h#L4617
+ struct Line
+ {
+ uint32_t offset; // Offset to start of code bytes for line number
+ uint32_t linenumStart : 24; // line where statement/expression starts
+ uint32_t deltaLineEnd : 7; // delta to line where statement ends (optional)
+ uint32_t fStatement : 1; // true if a statement linenumber, else an expression line num
+ };
+
+ // https://github.com/microsoft/microsoft-pdb/blob/master/include/cvinfo.h#L4630
+ struct Column
+ {
+ uint16_t start;
+ uint16_t end;
+ };
+
+ // https://github.com/microsoft/microsoft-pdb/blob/master/include/cvinfo.h#L4601
+ struct LinesHeader
+ {
+ uint32_t sectionOffset;
+ uint16_t sectionIndex;
+ struct
+ {
+ uint16_t fHasColumns : 1;
+ uint16_t pad : 15;
+ } flags;
+
+ uint32_t codeSize;
+ };
+
+ // https://github.com/microsoft/microsoft-pdb/blob/master/include/cvinfo.h#L4608
+ struct LinesFileBlockHeader
+ {
+ uint32_t fileChecksumOffset;
+ uint32_t numLines;
+ uint32_t size;
+ // Line lines[numLines];
+ // Column columns[numLines]; Might not be present
+ };
+
+ // https://github.com/microsoft/microsoft-pdb/blob/master/include/cvconst.h#L88
+ enum class PDB_NO_DISCARD ChecksumKind : uint8_t
+ {
+ None = 0,
+ MD5 = 1,
+ SHA1 = 2,
+ SHA256 = 3,
+ };
+
+ // https://github.com/microsoft/microsoft-pdb/blob/master/cvdump/dumpsym7.cpp#L1097
+ struct FileChecksumHeader
+ {
+ uint32_t filenameOffset;
+ uint8_t checksumSize;
+ ChecksumKind checksumKind;
+ PDB_FLEXIBLE_ARRAY_MEMBER(uint8_t, checksum);
+ };
+
+ // https://github.com/microsoft/microsoft-pdb/blob/master/include/cvinfo.h#L4822
+ enum class InlineeSourceLineKind : uint32_t
+ {
+ Signature = 0,
+ SignatureEx = 1,
+ };
+
+ struct InlineeSourceLineHeader
+ {
+ InlineeSourceLineKind kind;
+ };
+
+ // https://github.com/microsoft/microsoft-pdb/blob/master/include/cvinfo.h#L4825
+ struct InlineeSourceLine
+ {
+ uint32_t inlinee;
+ uint32_t fileChecksumOffset;
+ uint32_t lineNumber;
+ };
+
+ struct InlineeSourceLineEx
+ {
+ uint32_t inlinee;
+ uint32_t fileChecksumOffset;
+ uint32_t lineNumber;
+ uint32_t extraLines;
+ PDB_FLEXIBLE_ARRAY_MEMBER(uint32_t, extrafileChecksumOffsets);
+ };
+
+ // Combine DebugSubsectionHeader and first subsection header into one struct.
+ struct LineSection
+ {
+ DebugSubsectionHeader header;
+ union
+ {
+ LinesHeader linesHeader;
+ FileChecksumHeader checksumHeader;
+ InlineeSourceLineHeader inlineeHeader;
+ };
+ };
+ }
+ }
+}
diff --git a/thirdparty/raw_pdb/src/PDB_DirectMSFStream.cpp b/thirdparty/raw_pdb/src/PDB_DirectMSFStream.cpp
new file mode 100644
index 000000000..442dc7637
--- /dev/null
+++ b/thirdparty/raw_pdb/src/PDB_DirectMSFStream.cpp
@@ -0,0 +1,115 @@
+// Copyright 2011-2022, Molecular Matters GmbH <[email protected]>
+// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
+
+#include "PDB_PCH.h"
+#include "PDB_DirectMSFStream.h"
+#include "Foundation/PDB_PointerUtil.h"
+#include "Foundation/PDB_BitUtil.h"
+#include "Foundation/PDB_Assert.h"
+#include "Foundation/PDB_CRT.h"
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB::DirectMSFStream::DirectMSFStream(void) PDB_NO_EXCEPT
+ : m_data(nullptr)
+ , m_blockIndices(nullptr)
+ , m_blockSize(0u)
+ , m_size(0u)
+ , m_blockSizeLog2(0u)
+{
+}
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB::DirectMSFStream::DirectMSFStream(const void* data, uint32_t blockSize, const uint32_t* blockIndices, uint32_t streamSize) PDB_NO_EXCEPT
+ : m_data(data)
+ , m_blockIndices(blockIndices)
+ , m_blockSize(blockSize)
+ , m_size(streamSize)
+ , m_blockSizeLog2(BitUtil::FindFirstSetBit(blockSize))
+{
+ PDB_ASSERT(BitUtil::IsPowerOfTwo(blockSize), "MSF block size must be a power of two.");
+}
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+void PDB::DirectMSFStream::ReadAtOffset(void* destination, size_t size, size_t offset) const PDB_NO_EXCEPT
+{
+ PDB_ASSERT(destination != nullptr, "Destination buffer not set");
+ PDB_ASSERT(offset + size <= m_size, "Not enough data left to read.");
+
+ // work out which block and offset within the block the read offset corresponds to
+ size_t blockIndex = offset >> m_blockSizeLog2;
+ const size_t offsetWithinBlock = offset & (m_blockSize - 1u);
+
+ // work out the offset within the data based on the block indices
+ size_t offsetWithinData = (static_cast<size_t>(m_blockIndices[blockIndex]) << m_blockSizeLog2) + offsetWithinBlock;
+ const size_t bytesLeftInBlock = m_blockSize - offsetWithinBlock;
+
+ if (bytesLeftInBlock >= size)
+ {
+ // fast path, all the data can be read in one go
+ const void* const sourceData = Pointer::Offset<const void*>(m_data, offsetWithinData);
+ memcpy(destination, sourceData, size);
+ }
+ else
+ {
+ // slower path, data is scattered across several blocks.
+ // read remaining bytes in current block first.
+ {
+ const void* const sourceData = Pointer::Offset<const void*>(m_data, offsetWithinData);
+ memcpy(destination, sourceData, bytesLeftInBlock);
+ }
+
+ // read remaining bytes from blocks
+ size_t bytesLeftToRead = size - bytesLeftInBlock;
+ while (bytesLeftToRead != 0u)
+ {
+ // advance to the next block
+ ++blockIndex;
+ offsetWithinData = static_cast<size_t>(m_blockIndices[blockIndex]) << m_blockSizeLog2;
+
+ void* const destinationData = Pointer::Offset<void*>(destination, size - bytesLeftToRead);
+ const void* const sourceData = Pointer::Offset<const void*>(m_data, offsetWithinData);
+
+ if (bytesLeftToRead > m_blockSize)
+ {
+ // copy a whole block at once
+ memcpy(destinationData, sourceData, m_blockSize);
+ bytesLeftToRead -= m_blockSize;
+ }
+ else
+ {
+ // copy remaining bytes
+ memcpy(destinationData, sourceData, bytesLeftToRead);
+ bytesLeftToRead -= bytesLeftToRead;
+ }
+ }
+ }
+}
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB_NO_DISCARD PDB::DirectMSFStream::IndexAndOffset PDB::DirectMSFStream::GetBlockIndexForOffset(uint32_t offset) const PDB_NO_EXCEPT
+{
+ // work out which block and offset within the block the offset corresponds to
+ const uint32_t blockIndex = offset >> m_blockSizeLog2;
+ const uint32_t offsetWithinBlock = offset & (m_blockSize - 1u);
+
+ return IndexAndOffset { blockIndex, offsetWithinBlock };
+}
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB_NO_DISCARD size_t PDB::DirectMSFStream::GetDataOffsetForIndexAndOffset(const IndexAndOffset& indexAndOffset) const PDB_NO_EXCEPT
+{
+ // work out the offset within the data based on the block indices
+ const size_t offsetWithinData = (static_cast<size_t>(m_blockIndices[indexAndOffset.index]) << m_blockSizeLog2) + indexAndOffset.offsetWithinBlock;
+
+ return offsetWithinData;
+}
diff --git a/thirdparty/raw_pdb/src/PDB_DirectMSFStream.h b/thirdparty/raw_pdb/src/PDB_DirectMSFStream.h
new file mode 100644
index 000000000..70024592f
--- /dev/null
+++ b/thirdparty/raw_pdb/src/PDB_DirectMSFStream.h
@@ -0,0 +1,84 @@
+// Copyright 2011-2022, Molecular Matters GmbH <[email protected]>
+// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
+
+#pragma once
+
+#include "Foundation/PDB_Macros.h"
+
+
+// https://llvm.org/docs/PDB/index.html#the-msf-container
+// https://llvm.org/docs/PDB/MsfFile.html
+namespace PDB
+{
+ // provides direct access to the data of an MSF stream.
+ // inherently thread-safe, the stream doesn't carry any internal offset or similar.
+ // trivial to construct.
+ // slower individual reads, but pays off when not all data of a stream is needed.
+ class PDB_NO_DISCARD DirectMSFStream
+ {
+ public:
+ DirectMSFStream(void) PDB_NO_EXCEPT;
+ explicit DirectMSFStream(const void* data, uint32_t blockSize, const uint32_t* blockIndices, uint32_t streamSize) PDB_NO_EXCEPT;
+
+ PDB_DEFAULT_MOVE(DirectMSFStream);
+
+ // Reads a number of bytes from the stream.
+ void ReadAtOffset(void* destination, size_t size, size_t offset) const PDB_NO_EXCEPT;
+
+ // Reads from the stream.
+ template <typename T>
+ PDB_NO_DISCARD inline T ReadAtOffset(size_t offset) const PDB_NO_EXCEPT
+ {
+ T data;
+ ReadAtOffset(&data, sizeof(T), offset);
+ return data;
+ }
+
+ // Returns the block size of the stream.
+ PDB_NO_DISCARD inline uint32_t GetBlockSize(void) const PDB_NO_EXCEPT
+ {
+ return m_blockSize;
+ }
+
+ // Returns the size of the stream.
+ PDB_NO_DISCARD inline uint32_t GetSize(void) const PDB_NO_EXCEPT
+ {
+ return m_size;
+ }
+
+ private:
+ friend class CoalescedMSFStream;
+
+ struct IndexAndOffset
+ {
+ uint32_t index;
+ uint32_t offsetWithinBlock;
+ };
+
+ // Returns the block index and offset within the block that correspond to the given offset.
+ PDB_NO_DISCARD IndexAndOffset GetBlockIndexForOffset(uint32_t offset) const PDB_NO_EXCEPT;
+
+ // Returns the offset into the data that corresponds to the given indices and offset within a block.
+ PDB_NO_DISCARD size_t GetDataOffsetForIndexAndOffset(const IndexAndOffset& indexAndOffset) const PDB_NO_EXCEPT;
+
+ // Provides read-only access to the memory-mapped data.
+ PDB_NO_DISCARD inline const void* GetData(void) const PDB_NO_EXCEPT
+ {
+ return m_data;
+ }
+
+ // Provides read-only access to the block indices.
+ PDB_NO_DISCARD inline const uint32_t* GetBlockIndices(void) const PDB_NO_EXCEPT
+ {
+ return m_blockIndices;
+ }
+
+ const void* m_data;
+ const uint32_t* m_blockIndices;
+ uint32_t m_blockSize;
+ uint32_t m_size;
+ uint32_t m_blockSizeLog2;
+
+ PDB_DISABLE_COPY(DirectMSFStream);
+ };
+}
diff --git a/thirdparty/raw_pdb/src/PDB_ErrorCodes.h b/thirdparty/raw_pdb/src/PDB_ErrorCodes.h
new file mode 100644
index 000000000..bac0c73b9
--- /dev/null
+++ b/thirdparty/raw_pdb/src/PDB_ErrorCodes.h
@@ -0,0 +1,26 @@
+// Copyright 2011-2022, Molecular Matters GmbH <[email protected]>
+// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
+
+#pragma once
+
+#include "Foundation/PDB_Macros.h"
+
+
+namespace PDB
+{
+ enum class PDB_NO_DISCARD ErrorCode : unsigned int
+ {
+ Success = 0u,
+
+ // main PDB validation
+ InvalidDataSize,
+ InvalidSuperBlock,
+ InvalidFreeBlockMap,
+
+ // stream validation
+ InvalidStream,
+ InvalidSignature,
+ InvalidStreamIndex,
+ UnknownVersion
+ };
+}
diff --git a/thirdparty/raw_pdb/src/PDB_GlobalSymbolStream.cpp b/thirdparty/raw_pdb/src/PDB_GlobalSymbolStream.cpp
new file mode 100644
index 000000000..461b86902
--- /dev/null
+++ b/thirdparty/raw_pdb/src/PDB_GlobalSymbolStream.cpp
@@ -0,0 +1,43 @@
+// Copyright 2011-2022, Molecular Matters GmbH <[email protected]>
+// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
+
+#include "PDB_PCH.h"
+#include "PDB_GlobalSymbolStream.h"
+#include "PDB_RawFile.h"
+#include "PDB_Types.h"
+#include "PDB_DBITypes.h"
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB::GlobalSymbolStream::GlobalSymbolStream(void) PDB_NO_EXCEPT
+ : m_stream()
+ , m_hashRecords(nullptr)
+ , m_count(0u)
+{
+}
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB::GlobalSymbolStream::GlobalSymbolStream(const RawFile& file, uint16_t streamIndex, uint32_t count) PDB_NO_EXCEPT
+ : m_stream(file.CreateMSFStream<CoalescedMSFStream>(streamIndex))
+ , m_hashRecords(m_stream.GetDataAtOffset<HashRecord>(sizeof(HashTableHeader)))
+ , m_count(count)
+{
+}
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB_NO_DISCARD const PDB::CodeView::DBI::Record* PDB::GlobalSymbolStream::GetRecord(const CoalescedMSFStream& symbolRecordStream, const HashRecord& hashRecord) const PDB_NO_EXCEPT
+{
+ // hash record offsets start at 1, not at 0
+ const uint32_t headerOffset = hashRecord.offset - 1u;
+
+ // the offset doesn't point to the global symbol directly, but to the CodeView record:
+ // https://llvm.org/docs/PDB/CodeViewSymbols.html
+ const CodeView::DBI::Record* record = symbolRecordStream.GetDataAtOffset<const CodeView::DBI::Record>(headerOffset);
+
+ return record;
+}
diff --git a/thirdparty/raw_pdb/src/PDB_GlobalSymbolStream.h b/thirdparty/raw_pdb/src/PDB_GlobalSymbolStream.h
new file mode 100644
index 000000000..cb5877754
--- /dev/null
+++ b/thirdparty/raw_pdb/src/PDB_GlobalSymbolStream.h
@@ -0,0 +1,49 @@
+// Copyright 2011-2022, Molecular Matters GmbH <[email protected]>
+// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
+
+#pragma once
+
+#include "Foundation/PDB_Macros.h"
+#include "Foundation/PDB_ArrayView.h"
+#include "PDB_CoalescedMSFStream.h"
+
+
+namespace PDB
+{
+ class RawFile;
+ struct HashRecord;
+
+ namespace CodeView
+ {
+ namespace DBI
+ {
+ struct Record;
+ }
+ }
+
+
+ class PDB_NO_DISCARD GlobalSymbolStream
+ {
+ public:
+ GlobalSymbolStream(void) PDB_NO_EXCEPT;
+ explicit GlobalSymbolStream(const RawFile& file, uint16_t streamIndex, uint32_t count) PDB_NO_EXCEPT;
+
+ PDB_DEFAULT_MOVE(GlobalSymbolStream);
+
+ // Turns a given hash record into a DBI record using the given symbol stream.
+ PDB_NO_DISCARD const CodeView::DBI::Record* GetRecord(const CoalescedMSFStream& symbolRecordStream, const HashRecord& hashRecord) const PDB_NO_EXCEPT;
+
+ // Returns a view of all the records in the stream.
+ PDB_NO_DISCARD inline ArrayView<HashRecord> GetRecords(void) const PDB_NO_EXCEPT
+ {
+ return ArrayView<HashRecord>(m_hashRecords, m_count);
+ }
+
+ private:
+ CoalescedMSFStream m_stream;
+ const HashRecord* m_hashRecords;
+ uint32_t m_count;
+
+ PDB_DISABLE_COPY(GlobalSymbolStream);
+ };
+}
diff --git a/thirdparty/raw_pdb/src/PDB_IPIStream.cpp b/thirdparty/raw_pdb/src/PDB_IPIStream.cpp
new file mode 100644
index 000000000..dfd21bc56
--- /dev/null
+++ b/thirdparty/raw_pdb/src/PDB_IPIStream.cpp
@@ -0,0 +1,140 @@
+// Copyright 2011-2022, Molecular Matters GmbH <[email protected]>
+// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
+
+#include "PDB_PCH.h"
+#include "PDB_IPIStream.h"
+#include "PDB_RawFile.h"
+#include "PDB_Util.h"
+#include "PDB_DirectMSFStream.h"
+#include "PDB_InfoStream.h"
+#include "Foundation/PDB_Memory.h"
+
+namespace
+{
+ // the IPI stream always resides at index 4
+ static constexpr const uint32_t IPIStreamIndex = 4u;
+}
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB::IPIStream::IPIStream(void) PDB_NO_EXCEPT
+ : m_header()
+ , m_stream()
+ , m_records(nullptr)
+ , m_recordCount(0u)
+{
+}
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB::IPIStream::IPIStream(IPIStream&& other) PDB_NO_EXCEPT
+ : m_header(PDB_MOVE(other.m_header))
+ , m_stream(PDB_MOVE(other.m_stream))
+ , m_records(PDB_MOVE(other.m_records))
+ , m_recordCount(PDB_MOVE(other.m_recordCount))
+{
+ other.m_records = nullptr;
+ other.m_recordCount = 0u;
+}
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB::IPIStream& PDB::IPIStream::operator=(IPIStream&& other) PDB_NO_EXCEPT
+{
+ if (this != &other)
+ {
+ PDB_DELETE_ARRAY(m_records);
+
+ m_header = PDB_MOVE(other.m_header);
+ m_stream = PDB_MOVE(other.m_stream);
+ m_records = PDB_MOVE(other.m_records);
+ m_recordCount = PDB_MOVE(other.m_recordCount);
+
+ other.m_records = nullptr;
+ other.m_recordCount = 0u;
+ }
+
+ return *this;
+}
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB::IPIStream::IPIStream(const RawFile& file, const IPI::StreamHeader& header) PDB_NO_EXCEPT
+ : m_header(header)
+ , m_stream(file.CreateMSFStream<CoalescedMSFStream>(IPIStreamIndex))
+ , m_records(nullptr)
+ , m_recordCount(GetLastTypeIndex() - GetFirstTypeIndex())
+{
+ // types in the IPI stream are accessed by their index from other streams.
+ // however, the index is not stored with types in the IPI stream directly, but has to be built while walking the stream.
+ // similarly, because types are variable-length records, there are no direct offsets to access individual types.
+ // we therefore walk the IPI stream once, and store pointers to the records for trivial O(N) array lookup by index later.
+ m_records = PDB_NEW_ARRAY(const CodeView::IPI::Record*, m_recordCount);
+
+ // ignore the stream's header
+ size_t offset = sizeof(IPI::StreamHeader);
+
+ // parse the CodeView records
+ uint32_t typeIndex = 0u;
+ while (offset < m_stream.GetSize())
+ {
+ // https://llvm.org/docs/PDB/CodeViewTypes.html
+ const CodeView::IPI::Record* record = m_stream.GetDataAtOffset<const CodeView::IPI::Record>(offset);
+ const uint32_t recordSize = GetCodeViewRecordSize(record);
+ m_records[typeIndex] = record;
+
+ // position the stream offset at the next record
+ offset += sizeof(CodeView::IPI::RecordHeader) + recordSize;
+
+ ++typeIndex;
+ }
+}
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB::IPIStream::~IPIStream(void) PDB_NO_EXCEPT
+{
+ PDB_DELETE_ARRAY(m_records);
+}
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB_NO_DISCARD PDB::ErrorCode PDB::HasValidIPIStream(const RawFile& file) PDB_NO_EXCEPT
+{
+ const PDB::InfoStream infoStream(file);
+ if (!infoStream.HasIPIStream())
+ {
+ return ErrorCode::InvalidStream;
+ }
+
+ DirectMSFStream stream = file.CreateMSFStream<DirectMSFStream>(IPIStreamIndex);
+ if (stream.GetSize() < sizeof(IPI::StreamHeader))
+ {
+ return ErrorCode::InvalidStream;
+ }
+
+ const IPI::StreamHeader header = stream.ReadAtOffset<IPI::StreamHeader>(0u);
+ if (header.version != IPI::StreamHeader::Version::V80)
+ {
+ return ErrorCode::UnknownVersion;
+ }
+
+ return ErrorCode::Success;
+}
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB_NO_DISCARD PDB::IPIStream PDB::CreateIPIStream(const RawFile& file) PDB_NO_EXCEPT
+{
+ DirectMSFStream stream = file.CreateMSFStream<DirectMSFStream>(IPIStreamIndex);
+
+ const IPI::StreamHeader header = stream.ReadAtOffset<IPI::StreamHeader>(0u);
+ return IPIStream { file, header };
+}
diff --git a/thirdparty/raw_pdb/src/PDB_IPIStream.h b/thirdparty/raw_pdb/src/PDB_IPIStream.h
new file mode 100644
index 000000000..0efdbe22a
--- /dev/null
+++ b/thirdparty/raw_pdb/src/PDB_IPIStream.h
@@ -0,0 +1,66 @@
+// Copyright 2011-2022, Molecular Matters GmbH <[email protected]>
+// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
+
+#pragma once
+
+#include "Foundation/PDB_Macros.h"
+#include "Foundation/PDB_ArrayView.h"
+#include "PDB_ErrorCodes.h"
+#include "PDB_IPITypes.h"
+#include "PDB_CoalescedMSFStream.h"
+
+
+// PDB IPI stream
+// https://llvm.org/docs/PDB/TpiStream.html
+namespace PDB
+{
+ class RawFile;
+
+
+ class PDB_NO_DISCARD IPIStream
+ {
+ public:
+ IPIStream(void) PDB_NO_EXCEPT;
+ IPIStream(IPIStream&& other) PDB_NO_EXCEPT;
+ IPIStream& operator=(IPIStream&& other) PDB_NO_EXCEPT;
+
+ explicit IPIStream(const RawFile& file, const IPI::StreamHeader& header) PDB_NO_EXCEPT;
+ ~IPIStream(void) PDB_NO_EXCEPT;
+
+ // Returns the index of the first type, which is not necessarily zero.
+ PDB_NO_DISCARD inline uint32_t GetFirstTypeIndex(void) const PDB_NO_EXCEPT
+ {
+ return m_header.typeIndexBegin;
+ }
+
+ // Returns the index of the last type.
+ PDB_NO_DISCARD inline uint32_t GetLastTypeIndex(void) const PDB_NO_EXCEPT
+ {
+ return m_header.typeIndexEnd;
+ }
+
+ // Returns a view of all type records.
+ // Records identified by a type index can be accessed via "allRecords[typeIndex - firstTypeIndex]".
+ PDB_NO_DISCARD inline ArrayView<const CodeView::IPI::Record*> GetTypeRecords(void) const PDB_NO_EXCEPT
+ {
+ return ArrayView<const CodeView::IPI::Record*>(m_records, m_recordCount);
+ }
+
+ private:
+ IPI::StreamHeader m_header;
+ CoalescedMSFStream m_stream;
+ const CodeView::IPI::Record** m_records;
+ size_t m_recordCount;
+
+ PDB_DISABLE_COPY(IPIStream);
+ };
+
+
+ // ------------------------------------------------------------------------------------------------
+ // General
+ // ------------------------------------------------------------------------------------------------
+
+ PDB_NO_DISCARD ErrorCode HasValidIPIStream(const RawFile& file) PDB_NO_EXCEPT;
+
+ PDB_NO_DISCARD IPIStream CreateIPIStream(const RawFile& file) PDB_NO_EXCEPT;
+}
diff --git a/thirdparty/raw_pdb/src/PDB_IPITypes.h b/thirdparty/raw_pdb/src/PDB_IPITypes.h
new file mode 100644
index 000000000..c9c4e086a
--- /dev/null
+++ b/thirdparty/raw_pdb/src/PDB_IPITypes.h
@@ -0,0 +1,144 @@
+// Copyright 2011-2022, Molecular Matters GmbH <[email protected]>
+// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
+
+#pragma once
+
+#include "Foundation/PDB_Macros.h"
+
+
+namespace PDB
+{
+ namespace IPI
+ {
+ // https://llvm.org/docs/PDB/TpiStream.html#tpi-header
+ struct StreamHeader
+ {
+ enum class PDB_NO_DISCARD Version : uint32_t
+ {
+ V40 = 19950410u,
+ V41 = 19951122u,
+ V50 = 19961031u,
+ V70 = 19990903u,
+ V80 = 20040203u
+ };
+
+ Version version;
+ uint32_t headerSize;
+ uint32_t typeIndexBegin;
+ uint32_t typeIndexEnd;
+ uint32_t typeRecordBytes;
+ uint16_t hashStreamIndex;
+ uint16_t hashAuxStreamIndex;
+ uint32_t hashKeySize;
+ uint32_t hashBucketCount;
+ uint32_t hashValueBufferOffset;
+ uint32_t hashValueBufferLength;
+ uint32_t indexOffsetBufferOffset;
+ uint32_t indexOffsetBufferLength;
+ uint32_t hashAdjBufferOffset;
+ uint32_t hashAdjBufferLength;
+ };
+ }
+
+
+ namespace CodeView
+ {
+ namespace IPI
+ {
+ // code view type records that can appear in an IPI stream
+ // https://llvm.org/docs/PDB/CodeViewTypes.html
+ // https://llvm.org/docs/PDB/TpiStream.html#tpi-vs-ipi-stream
+ enum class PDB_NO_DISCARD TypeRecordKind : uint16_t
+ {
+ LF_FUNC_ID = 0x1601u, // global function ID
+ LF_MFUNC_ID = 0x1602u, // member function ID
+ LF_BUILDINFO = 0x1603u, // build information
+ LF_SUBSTR_LIST = 0x1604u, // similar to LF_ARGLIST for a list of substrings
+ LF_STRING_ID = 0x1605u, // string ID
+ LF_UDT_SRC_LINE = 0x1606u, // source and line on where an UDT (User Defined Type) is defined, generated by the compiler
+ LF_UDT_MOD_SRC_LINE = 0x1607u // module, source and line on where an UDT is defined, generated by the linker
+ };
+
+ // https://github.com/microsoft/microsoft-pdb/blob/master/include/cvinfo.h#L1715
+ enum class PDB_NO_DISCARD BuildInfoType : uint8_t
+ {
+ CurrentDirectory, // compiler working directory
+ BuildTool, // tool path
+ SourceFile, // path to source file, relative or absolute
+ TypeServerPDB, // path to PDB file
+ CommandLine // command-line used to build the source file
+ };
+
+ struct RecordHeader
+ {
+ uint16_t size; // record length, not including this 2-byte field
+ TypeRecordKind kind; // record kind
+ };
+
+ // all CodeView records are stored as a header, followed by variable-length data.
+ // internal Record structs such as S_PUB32, S_GDATA32, etc. correspond to the data layout of a CodeView record of that kind.
+ struct Record
+ {
+ RecordHeader header;
+ union Data
+ {
+#pragma pack(push, 1)
+ // https://github.com/microsoft/microsoft-pdb/blob/master/include/cvinfo.h#L1680
+ struct
+ {
+ uint32_t scopeId; // parent scope of the ID, 0 if global
+ uint32_t typeIndex; // function type
+ PDB_FLEXIBLE_ARRAY_MEMBER(char, name);
+ } LF_FUNC_ID;
+
+ // https://github.com/microsoft/microsoft-pdb/blob/master/include/cvinfo.h#L1687
+ struct
+ {
+ uint32_t parentTypeIndex; // parent scope of the ID, 0 if global
+ uint32_t typeIndex; // function type
+ PDB_FLEXIBLE_ARRAY_MEMBER(char, name);
+ } LF_MFUNC_ID;
+
+ // https://github.com/microsoft/microsoft-pdb/blob/master/include/cvinfo.h#L1694
+ struct
+ {
+ uint32_t id; // ID to list of sub-string IDs
+ PDB_FLEXIBLE_ARRAY_MEMBER(char, name);
+ } LF_STRING_ID;
+
+ // https://github.com/microsoft/microsoft-pdb/blob/master/include/cvinfo.h#L1700
+ struct
+ {
+ uint32_t typeIndex; // UDT's type index
+ uint32_t stringIndex; // index to LF_STRING_ID record where source file name is saved
+ uint32_t line; // line number
+ } LF_UDT_SRC_LINE;
+
+ // https://github.com/microsoft/microsoft-pdb/blob/master/include/cvinfo.h#L1707
+ struct
+ {
+ uint32_t typeIndex; // UDT's type index
+ uint32_t stringIndex; // index into '/names' string table where source file name is saved
+ uint32_t line; // line number
+ uint16_t moduleIndex; // module that contributes this UDT definition
+ } LF_UDT_MOD_SRC_LINE;
+
+ // https://github.com/microsoft/microsoft-pdb/blob/master/include/cvinfo.h#L2043
+ struct
+ {
+ uint32_t count;
+ PDB_FLEXIBLE_ARRAY_MEMBER(uint32_t, typeIndices);
+ } LF_SUBSTR_LIST;
+
+ // https://github.com/microsoft/microsoft-pdb/blob/master/include/cvinfo.h#L1726
+ struct
+ {
+ uint16_t count;
+ PDB_FLEXIBLE_ARRAY_MEMBER(uint32_t, typeIndices);
+ } LF_BUILDINFO;
+#pragma pack(pop)
+ } data;
+ };
+ }
+ }
+}
diff --git a/thirdparty/raw_pdb/src/PDB_ImageSectionStream.cpp b/thirdparty/raw_pdb/src/PDB_ImageSectionStream.cpp
new file mode 100644
index 000000000..3d495ad0b
--- /dev/null
+++ b/thirdparty/raw_pdb/src/PDB_ImageSectionStream.cpp
@@ -0,0 +1,47 @@
+// Copyright 2011-2022, Molecular Matters GmbH <[email protected]>
+// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
+
+#include "PDB_PCH.h"
+#include "PDB_ImageSectionStream.h"
+#include "PDB_RawFile.h"
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB::ImageSectionStream::ImageSectionStream(void) PDB_NO_EXCEPT
+ : m_stream()
+ , m_headers(nullptr)
+ , m_count(0u)
+{
+}
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB::ImageSectionStream::ImageSectionStream(const RawFile& file, uint16_t streamIndex) PDB_NO_EXCEPT
+ : m_stream(file.CreateMSFStream<CoalescedMSFStream>(streamIndex))
+ , m_headers(m_stream.GetDataAtOffset<IMAGE_SECTION_HEADER>(0u))
+ , m_count(m_stream.GetSize() / sizeof(IMAGE_SECTION_HEADER))
+{
+}
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB_NO_DISCARD uint32_t PDB::ImageSectionStream::ConvertSectionOffsetToRVA(uint16_t oneBasedSectionIndex, uint32_t offsetInSection) const PDB_NO_EXCEPT
+{
+ if (oneBasedSectionIndex == 0u)
+ {
+ // should never happen, but prevent underflow
+ return 0u;
+ }
+ else if (oneBasedSectionIndex > m_count)
+ {
+ // this symbol is "contained" in a section that is neither part of the PDB, nor the EXE.
+ // it is a special compiler-generated or linker-generated symbol such as CFG symbols (e.g. __guard_fids_count, __guard_flags).
+ // we can safely ignore those symbols.
+ return 0u;
+ }
+
+ return m_headers[oneBasedSectionIndex - 1u].VirtualAddress + offsetInSection;
+}
diff --git a/thirdparty/raw_pdb/src/PDB_ImageSectionStream.h b/thirdparty/raw_pdb/src/PDB_ImageSectionStream.h
new file mode 100644
index 000000000..190c7223d
--- /dev/null
+++ b/thirdparty/raw_pdb/src/PDB_ImageSectionStream.h
@@ -0,0 +1,42 @@
+// Copyright 2011-2022, Molecular Matters GmbH <[email protected]>
+// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
+
+#pragma once
+
+#include "Foundation/PDB_Macros.h"
+#include "Foundation/PDB_ArrayView.h"
+#include "PDB_Types.h"
+#include "PDB_CoalescedMSFStream.h"
+
+
+namespace PDB
+{
+ class RawFile;
+ struct IMAGE_SECTION_HEADER;
+
+
+ class PDB_NO_DISCARD ImageSectionStream
+ {
+ public:
+ ImageSectionStream(void) PDB_NO_EXCEPT;
+ explicit ImageSectionStream(const RawFile& file, uint16_t streamIndex) PDB_NO_EXCEPT;
+
+ PDB_DEFAULT_MOVE(ImageSectionStream);
+
+ // Converts a one-based section offset into an RVA.
+ PDB_NO_DISCARD uint32_t ConvertSectionOffsetToRVA(uint16_t oneBasedSectionIndex, uint32_t offsetInSection) const PDB_NO_EXCEPT;
+
+ // Returns a view of all the sections in the stream.
+ PDB_NO_DISCARD inline ArrayView<IMAGE_SECTION_HEADER> GetImageSections(void) const PDB_NO_EXCEPT
+ {
+ return ArrayView<IMAGE_SECTION_HEADER>(m_headers, m_count);
+ }
+
+ private:
+ CoalescedMSFStream m_stream;
+ const IMAGE_SECTION_HEADER* m_headers;
+ size_t m_count;
+
+ PDB_DISABLE_COPY(ImageSectionStream);
+ };
+}
diff --git a/thirdparty/raw_pdb/src/PDB_InfoStream.cpp b/thirdparty/raw_pdb/src/PDB_InfoStream.cpp
new file mode 100644
index 000000000..fa166000f
--- /dev/null
+++ b/thirdparty/raw_pdb/src/PDB_InfoStream.cpp
@@ -0,0 +1,102 @@
+// Copyright 2011-2022, Molecular Matters GmbH <[email protected]>
+// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
+
+#include "PDB_PCH.h"
+#include "PDB_InfoStream.h"
+#include "PDB_RawFile.h"
+#include "Foundation/PDB_CRT.h"
+
+namespace
+{
+ // the PDB info stream always resides at index 1
+ static constexpr const uint32_t InfoStreamIndex = 1u;
+}
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB::InfoStream::InfoStream(void) PDB_NO_EXCEPT
+ : m_stream()
+ , m_header(nullptr)
+ , m_namesStreamIndex(0)
+ , m_usesDebugFastlink(false)
+ , m_hasIPIStream(false)
+{
+}
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB::InfoStream::InfoStream(const RawFile& file) PDB_NO_EXCEPT
+ : m_stream(file.CreateMSFStream<CoalescedMSFStream>(InfoStreamIndex))
+ , m_header(m_stream.GetDataAtOffset<const Header>(0u))
+ , m_namesStreamIndex(0)
+ , m_usesDebugFastlink(false)
+ , m_hasIPIStream(false)
+{
+ // the info stream starts with the header, followed by the named stream map, followed by the feature codes
+ // https://llvm.org/docs/PDB/PdbStream.html#named-stream-map
+ size_t streamOffset = sizeof(Header);
+
+ const NamedStreamMap* namedStreamMap = m_stream.GetDataAtOffset<const NamedStreamMap>(streamOffset);
+ streamOffset += sizeof(NamedStreamMap) + namedStreamMap->length;
+
+ const SerializedHashTable::Header* hashTableHeader = m_stream.GetDataAtOffset<const SerializedHashTable::Header>(streamOffset);
+ streamOffset += sizeof(SerializedHashTable::Header);
+
+ const SerializedHashTable::BitVector* presentBitVector = m_stream.GetDataAtOffset<const SerializedHashTable::BitVector>(streamOffset);
+ streamOffset += sizeof(SerializedHashTable::BitVector) + sizeof(uint32_t) * presentBitVector->wordCount;
+
+ const SerializedHashTable::BitVector* deletedBitVector = m_stream.GetDataAtOffset<const SerializedHashTable::BitVector>(streamOffset);
+ streamOffset += sizeof(SerializedHashTable::BitVector) + sizeof(uint32_t) * deletedBitVector->wordCount;
+
+ // the hash table entries can be used to identify the indices of certain common streams like:
+ // "/UDTSRCLINEUNDONE"
+ // "/src/headerblock"
+ // "/LinkInfo"
+ // "/TMCache"
+ // "/names"
+
+ const NamedStreamMap::HashTableEntry* namedStreamMapHashEntries = m_stream.GetDataAtOffset<const NamedStreamMap::HashTableEntry>(streamOffset);
+
+ // Find "/names" stream, used to look up filenames for lines.
+ for (uint32_t i = 0, size = hashTableHeader->size; i < size; ++i)
+ {
+ const NamedStreamMap::HashTableEntry& entry = namedStreamMapHashEntries[i];
+ const char* streamName = &namedStreamMap->stringTable[entry.stringTableOffset];
+
+ if (strcmp("/names", streamName) == 0)
+ {
+ m_namesStreamIndex = entry.streamIndex;
+ }
+ }
+
+ streamOffset += sizeof(NamedStreamMap::HashTableEntry) * hashTableHeader->size;
+
+ // read feature codes by consuming remaining bytes
+ // https://llvm.org/docs/PDB/PdbStream.html#pdb-feature-codes
+ const FeatureCode* featureCodes = m_stream.GetDataAtOffset<const FeatureCode>(streamOffset);
+ const size_t remainingBytes = m_stream.GetSize() - streamOffset;
+ const size_t count = remainingBytes / sizeof(FeatureCode);
+
+ for (size_t i=0u; i < count; ++i)
+ {
+ FeatureCode code = featureCodes[i];
+ if (code == PDB::FeatureCode::MinimalDebugInfo)
+ {
+ m_usesDebugFastlink = true;
+ }
+ else if (code == PDB::FeatureCode::VC110 || code == PDB::FeatureCode::VC140)
+ {
+ m_hasIPIStream = true;
+ }
+ }
+}
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB::NamesStream PDB::InfoStream::CreateNamesStream(const RawFile& file) const PDB_NO_EXCEPT
+{
+ return NamesStream(file, m_namesStreamIndex);
+}
diff --git a/thirdparty/raw_pdb/src/PDB_InfoStream.h b/thirdparty/raw_pdb/src/PDB_InfoStream.h
new file mode 100644
index 000000000..9e15ebc41
--- /dev/null
+++ b/thirdparty/raw_pdb/src/PDB_InfoStream.h
@@ -0,0 +1,62 @@
+// Copyright 2011-2022, Molecular Matters GmbH <[email protected]>
+// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
+
+#pragma once
+
+#include "Foundation/PDB_Macros.h"
+#include "PDB_Types.h"
+#include "PDB_CoalescedMSFStream.h"
+#include "PDB_NamesStream.h"
+
+namespace PDB
+{
+ class RawFile;
+
+
+ // PDB Info Stream
+ // https://llvm.org/docs/PDB/PdbStream.html
+ class PDB_NO_DISCARD InfoStream
+ {
+ public:
+ InfoStream(void) PDB_NO_EXCEPT;
+ explicit InfoStream(const RawFile& file) PDB_NO_EXCEPT;
+
+ PDB_DEFAULT_MOVE(InfoStream);
+
+ // Returns the header of the stream.
+ PDB_NO_DISCARD inline const Header* GetHeader(void) const PDB_NO_EXCEPT
+ {
+ return m_header;
+ }
+
+ // Returns whether the module has a names stream.
+ PDB_NO_DISCARD inline bool HasNamesStream(void) const PDB_NO_EXCEPT
+ {
+ return (m_namesStreamIndex != 0u);
+ }
+
+ // Returns whether the PDB file was linked using /DEBUG:FASTLINK.
+ PDB_NO_DISCARD inline bool UsesDebugFastLink(void) const PDB_NO_EXCEPT
+ {
+ return m_usesDebugFastlink;
+ }
+
+ // Returns whether the PDB file has an IPI stream.
+ PDB_NO_DISCARD inline bool HasIPIStream(void) const PDB_NO_EXCEPT
+ {
+ return m_hasIPIStream;
+ }
+
+ // Create names stream
+ PDB_NO_DISCARD NamesStream CreateNamesStream(const RawFile& file) const PDB_NO_EXCEPT;
+
+ private:
+ CoalescedMSFStream m_stream;
+ const Header* m_header;
+ uint32_t m_namesStreamIndex;
+ bool m_usesDebugFastlink;
+ bool m_hasIPIStream;
+
+ PDB_DISABLE_COPY(InfoStream);
+ };
+}
diff --git a/thirdparty/raw_pdb/src/PDB_ModuleInfoStream.cpp b/thirdparty/raw_pdb/src/PDB_ModuleInfoStream.cpp
new file mode 100644
index 000000000..86040fffb
--- /dev/null
+++ b/thirdparty/raw_pdb/src/PDB_ModuleInfoStream.cpp
@@ -0,0 +1,184 @@
+// Copyright 2011-2022, Molecular Matters GmbH <[email protected]>
+// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
+
+#include "PDB_PCH.h"
+#include "PDB_ModuleInfoStream.h"
+#include "Foundation/PDB_Memory.h"
+#include "Foundation/PDB_CRT.h"
+
+namespace
+{
+ static constexpr const char* LinkerSymbolName("* Linker *");
+
+
+ // ------------------------------------------------------------------------------------------------
+ // ------------------------------------------------------------------------------------------------
+ PDB_NO_DISCARD static inline size_t EstimateModuleCount(size_t streamSize) PDB_NO_EXCEPT
+ {
+ // work out how many modules are stored in the stream at most.
+ // the module info is stored in variable-length records, so we can't determine the exact number without walking the stream.
+ return streamSize / sizeof(PDB::DBI::ModuleInfo);
+ }
+}
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB::ModuleInfoStream::Module::Module(void) PDB_NO_EXCEPT
+ : m_info(nullptr)
+ , m_name(nullptr)
+ , m_nameLength(0u)
+ , m_objectName(nullptr)
+ , m_objectNameLength(0u)
+{
+}
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB::ModuleInfoStream::Module::Module(const DBI::ModuleInfo* info, const char* name, size_t nameLength, const char* objectName, size_t objectNameLength) PDB_NO_EXCEPT
+ : m_info(info)
+ , m_name(name)
+ , m_nameLength(nameLength)
+ , m_objectName(objectName)
+ , m_objectNameLength(objectNameLength)
+{
+}
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB_NO_DISCARD bool PDB::ModuleInfoStream::Module::HasSymbolStream(void) const PDB_NO_EXCEPT
+{
+ const uint16_t streamIndex = m_info->moduleSymbolStreamIndex;
+
+ // some modules don't have a symbol stream, i.e. no additional debug information is present.
+ // this usually happens when private symbols are stripped from a PDB.
+ return (streamIndex != 0xFFFFu);
+}
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB_NO_DISCARD bool PDB::ModuleInfoStream::Module::HasLineStream(void) const PDB_NO_EXCEPT
+{
+ return (m_info->c13Size > 0);
+}
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB_NO_DISCARD PDB::ModuleSymbolStream PDB::ModuleInfoStream::Module::CreateSymbolStream(const RawFile& file) const PDB_NO_EXCEPT
+{
+ PDB_ASSERT(HasSymbolStream(), "Module symbol stream index is invalid.");
+
+ return ModuleSymbolStream(file, m_info->moduleSymbolStreamIndex, m_info->symbolSize);
+}
+
+PDB_NO_DISCARD PDB::ModuleLineStream PDB::ModuleInfoStream::Module::CreateLineStream(const RawFile& file) const PDB_NO_EXCEPT
+{
+ PDB_ASSERT(HasLineStream(), "Module line stream is not present.");
+
+ return ModuleLineStream(file, m_info->moduleSymbolStreamIndex, m_info->symbolSize + m_info->c11Size + m_info->c13Size, m_info->symbolSize + m_info->c11Size);
+}
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB::ModuleInfoStream::ModuleInfoStream(void) PDB_NO_EXCEPT
+ : m_stream()
+ , m_modules(nullptr)
+ , m_moduleCount(0u)
+{
+}
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB::ModuleInfoStream::ModuleInfoStream(ModuleInfoStream&& other) PDB_NO_EXCEPT
+ : m_stream(PDB_MOVE(other.m_stream))
+ , m_modules(PDB_MOVE(other.m_modules))
+ , m_moduleCount(PDB_MOVE(other.m_moduleCount))
+{
+ other.m_modules = nullptr;
+ other.m_moduleCount = 0u;
+}
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB::ModuleInfoStream& PDB::ModuleInfoStream::operator=(ModuleInfoStream&& other) PDB_NO_EXCEPT
+{
+ if (this != &other)
+ {
+ PDB_DELETE_ARRAY(m_modules);
+
+ m_stream = PDB_MOVE(other.m_stream);
+ m_modules = PDB_MOVE(other.m_modules);
+ m_moduleCount = PDB_MOVE(other.m_moduleCount);
+
+ other.m_modules = nullptr;
+ other.m_moduleCount = 0u;
+ }
+
+ return *this;
+}
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB::ModuleInfoStream::ModuleInfoStream(const DirectMSFStream& directStream, uint32_t size, uint32_t offset) PDB_NO_EXCEPT
+ : m_stream(directStream, size, offset)
+ , m_modules(nullptr)
+ , m_moduleCount(0u)
+{
+ m_modules = PDB_NEW_ARRAY(Module, EstimateModuleCount(size));
+
+ size_t streamOffset = 0u;
+ while (streamOffset < size)
+ {
+ const DBI::ModuleInfo* moduleInfo = m_stream.GetDataAtOffset<const DBI::ModuleInfo>(streamOffset);
+ streamOffset += sizeof(DBI::ModuleInfo);
+
+ const char* name = m_stream.GetDataAtOffset<const char>(streamOffset);
+ const size_t nameLength = strlen(name);
+ streamOffset += nameLength + 1u;
+
+ const char* objectName = m_stream.GetDataAtOffset<const char>(streamOffset);
+ const size_t objectNameLength = strlen(objectName);
+ streamOffset += objectNameLength + 1u;
+
+ // the stream is aligned to 4 bytes
+ streamOffset = BitUtil::RoundUpToMultiple<size_t>(streamOffset, 4ul);
+
+ m_modules[m_moduleCount] = Module(moduleInfo, name, nameLength, objectName, objectNameLength);
+ ++m_moduleCount;
+ }
+}
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB::ModuleInfoStream::~ModuleInfoStream(void) PDB_NO_EXCEPT
+{
+ PDB_DELETE_ARRAY(m_modules);
+}
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB_NO_DISCARD const PDB::ModuleInfoStream::Module* PDB::ModuleInfoStream::FindLinkerModule(void) const PDB_NO_EXCEPT
+{
+ const size_t count = m_moduleCount;
+ for (size_t i = 0u; i < count; ++i)
+ {
+ // with both MSVC cl.exe and Clang, the linker symbol is the last one to be stored, so start searching from the end
+ const Module& module = m_modules[count - i - 1u];
+
+ // check if this is the linker symbol
+ if (strcmp(module.GetName().Decay(), LinkerSymbolName) == 0)
+ {
+ return &module;
+ }
+ }
+
+ return nullptr;
+}
diff --git a/thirdparty/raw_pdb/src/PDB_ModuleInfoStream.h b/thirdparty/raw_pdb/src/PDB_ModuleInfoStream.h
new file mode 100644
index 000000000..4fef0fe3b
--- /dev/null
+++ b/thirdparty/raw_pdb/src/PDB_ModuleInfoStream.h
@@ -0,0 +1,104 @@
+// Copyright 2011-2022, Molecular Matters GmbH <[email protected]>
+// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
+
+#pragma once
+
+#include "Foundation/PDB_Macros.h"
+#include "Foundation/PDB_ArrayView.h"
+#include "PDB_CoalescedMSFStream.h"
+#include "PDB_ModuleSymbolStream.h"
+#include "PDB_ModuleLineStream.h"
+
+namespace PDB
+{
+ class PDB_NO_DISCARD DirectMSFStream;
+
+ class PDB_NO_DISCARD ModuleInfoStream
+ {
+ public:
+ class PDB_NO_DISCARD Module
+ {
+ public:
+ Module(void) PDB_NO_EXCEPT;
+ explicit Module(const DBI::ModuleInfo* info, const char* name, size_t nameLength, const char* objectName, size_t objectNameLength) PDB_NO_EXCEPT;
+
+ PDB_DEFAULT_MOVE(Module);
+
+ // Returns whether the module has a symbol stream.
+ PDB_NO_DISCARD bool HasSymbolStream(void) const PDB_NO_EXCEPT;
+
+ // Returns whether the module has a line stream.
+ PDB_NO_DISCARD bool HasLineStream(void) const PDB_NO_EXCEPT;
+
+ // Creates a symbol stream for the module.
+ PDB_NO_DISCARD ModuleSymbolStream CreateSymbolStream(const RawFile& file) const PDB_NO_EXCEPT;
+
+ // Create a line stream for the module
+ PDB_NO_DISCARD ModuleLineStream CreateLineStream(const RawFile& file) const PDB_NO_EXCEPT;
+
+
+ // Returns the PDB module info.
+ PDB_NO_DISCARD inline const DBI::ModuleInfo* GetInfo(void) const PDB_NO_EXCEPT
+ {
+ return m_info;
+ }
+
+ // Returns the name of the module.
+ PDB_NO_DISCARD inline ArrayView<char> GetName(void) const PDB_NO_EXCEPT
+ {
+ return ArrayView<char>(m_name, m_nameLength);
+ }
+
+ // Returns the name of the object file of the module.
+ PDB_NO_DISCARD inline ArrayView<char> GetObjectName(void) const PDB_NO_EXCEPT
+ {
+ return ArrayView<char>(m_objectName, m_objectNameLength);
+ }
+
+ private:
+ // the module info is stored in variable-length arrays inside the stream, so rather than store an array directly,
+ // we need to store pointers to the individual data items inside the stream.
+ const DBI::ModuleInfo* m_info;
+
+ // the module name, e.g. the path to an object file or import library such as "Import:kernel32.dll"
+ const char* m_name;
+ size_t m_nameLength;
+
+ // the name of the object file. either the same as the module name, or the path to the archive that contained the module
+ const char* m_objectName;
+ size_t m_objectNameLength;
+
+ PDB_DISABLE_COPY(Module);
+ };
+
+ ModuleInfoStream(void) PDB_NO_EXCEPT;
+ ModuleInfoStream(ModuleInfoStream&& other) PDB_NO_EXCEPT;
+ ModuleInfoStream& operator=(ModuleInfoStream&& other) PDB_NO_EXCEPT;
+
+ explicit ModuleInfoStream(const DirectMSFStream& directStream, uint32_t size, uint32_t offset) PDB_NO_EXCEPT;
+
+ ~ModuleInfoStream(void) PDB_NO_EXCEPT;
+
+ // Tries to find the linker module corresponding to the linker, i.e. the module named "* Linker *".
+ PDB_NO_DISCARD const Module* FindLinkerModule(void) const PDB_NO_EXCEPT;
+
+ // Returns the module with the given index.
+ PDB_NO_DISCARD inline const Module& GetModule(uint32_t index) const PDB_NO_EXCEPT
+ {
+ return m_modules[index];
+ }
+
+ // Returns a view of all modules in the info stream.
+ PDB_NO_DISCARD inline ArrayView<Module> GetModules(void) const PDB_NO_EXCEPT
+ {
+ return ArrayView<Module>(m_modules, m_moduleCount);
+ }
+
+ private:
+ CoalescedMSFStream m_stream;
+ Module* m_modules;
+ size_t m_moduleCount;
+
+ PDB_DISABLE_COPY(ModuleInfoStream);
+ };
+}
diff --git a/thirdparty/raw_pdb/src/PDB_ModuleLineStream.cpp b/thirdparty/raw_pdb/src/PDB_ModuleLineStream.cpp
new file mode 100644
index 000000000..201983ed6
--- /dev/null
+++ b/thirdparty/raw_pdb/src/PDB_ModuleLineStream.cpp
@@ -0,0 +1,31 @@
+// Copyright 2011-2022, Molecular Matters GmbH <[email protected]>
+// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
+
+#include "PDB_PCH.h"
+#include "PDB_ModuleLineStream.h"
+#include "PDB_RawFile.h"
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB::ModuleLineStream::ModuleLineStream(void) PDB_NO_EXCEPT
+ : m_stream(), m_c13LineInfoOffset(0)
+{
+}
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB::ModuleLineStream::ModuleLineStream(const RawFile& file, uint16_t streamIndex, uint32_t streamSize, size_t c13LineInfoOffset) PDB_NO_EXCEPT
+ : m_stream(file.CreateMSFStream<CoalescedMSFStream>(streamIndex, streamSize)), m_c13LineInfoOffset(c13LineInfoOffset)
+{
+ // https://llvm.org/docs/PDB/ModiStream.html
+ // struct ModiStream {
+ // uint32_t Signature;
+ // uint8_t Symbols[SymbolSize - 4];
+ // uint8_t C11LineInfo[C11Size];
+ // uint8_t C13LineInfo[C13Size];
+ // uint32_t GlobalRefsSize;
+ // uint8_t GlobalRefs[GlobalRefsSize];
+ // };
+}
diff --git a/thirdparty/raw_pdb/src/PDB_ModuleLineStream.h b/thirdparty/raw_pdb/src/PDB_ModuleLineStream.h
new file mode 100644
index 000000000..d1148cc0c
--- /dev/null
+++ b/thirdparty/raw_pdb/src/PDB_ModuleLineStream.h
@@ -0,0 +1,151 @@
+// Copyright 2011-2022, Molecular Matters GmbH <[email protected]>
+// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
+
+#pragma once
+
+#include "Foundation/PDB_Macros.h"
+#include "Foundation/PDB_BitUtil.h"
+#include "PDB_DBITypes.h"
+#include "PDB_Util.h"
+#include "PDB_CoalescedMSFStream.h"
+
+
+namespace PDB
+{
+ class RawFile;
+
+ class PDB_NO_DISCARD ModuleLineStream
+ {
+ public:
+ ModuleLineStream(void) PDB_NO_EXCEPT;
+ explicit ModuleLineStream(const RawFile& file, uint16_t streamIndex, uint32_t streamSize, size_t c13LineInfoOffset) PDB_NO_EXCEPT;
+
+ PDB_DEFAULT_MOVE(ModuleLineStream);
+
+ template <typename F>
+ void ForEachSection(F&& functor) const PDB_NO_EXCEPT
+ {
+ size_t offset = m_c13LineInfoOffset;
+
+ // read the line stream sections
+ while (offset < m_stream.GetSize())
+ {
+ const CodeView::DBI::LineSection* section = m_stream.GetDataAtOffset<const CodeView::DBI::LineSection>(offset);
+
+ functor(section);
+
+ offset = BitUtil::RoundUpToMultiple<size_t>(offset + sizeof(CodeView::DBI::DebugSubsectionHeader) + section->header.size, 4u);
+ }
+ }
+
+ template <typename F>
+ void ForEachLinesBlock(const CodeView::DBI::LineSection* section, F&& functor) const PDB_NO_EXCEPT
+ {
+ PDB_ASSERT(section->header.kind == CodeView::DBI::DebugSubsectionKind::S_LINES,
+ "DebugSubsectionHeader::Kind %X != S_LINES (%X)",
+ static_cast<uint32_t>(section->header.kind), static_cast<uint32_t>(CodeView::DBI::DebugSubsectionKind::S_LINES));
+
+ size_t offset = m_stream.GetPointerOffset(section);
+ const size_t headerEnd = BitUtil::RoundUpToMultiple<size_t>(offset + sizeof(CodeView::DBI::DebugSubsectionHeader) + section->header.size, 4u);
+
+ offset = BitUtil::RoundUpToMultiple<size_t>(offset + sizeof(CodeView::DBI::DebugSubsectionHeader) + sizeof(CodeView::DBI::LinesHeader), 4u);
+
+ // read all blocks of lines
+ while (offset < headerEnd)
+ {
+ const CodeView::DBI::LinesFileBlockHeader* linesBlockHeader = m_stream.GetDataAtOffset<const CodeView::DBI::LinesFileBlockHeader>(offset);
+ const CodeView::DBI::Line* blockLines = m_stream.GetDataAtOffset<const CodeView::DBI::Line>(offset + sizeof(CodeView::DBI::LinesFileBlockHeader));
+
+ const size_t blockColumnsOffset = sizeof(CodeView::DBI::LinesFileBlockHeader) + (linesBlockHeader->numLines * (sizeof(CodeView::DBI::Line)));
+ const CodeView::DBI::Column* blockColumns = blockColumnsOffset < linesBlockHeader->size ? m_stream.GetDataAtOffset<const CodeView::DBI::Column>(offset) : nullptr;
+
+ functor(linesBlockHeader, blockLines, blockColumns);
+
+ offset = BitUtil::RoundUpToMultiple<size_t>(offset + linesBlockHeader->size, 4u);
+ }
+
+ PDB_ASSERT(offset == headerEnd, "Mismatch between offset %zu and header end %zu when reading lines blocks", offset, headerEnd);
+ }
+
+ template <typename F>
+ void ForEachFileChecksum(const CodeView::DBI::LineSection* section, F&& functor) const PDB_NO_EXCEPT
+ {
+ PDB_ASSERT(section->header.kind == CodeView::DBI::DebugSubsectionKind::S_FILECHECKSUMS,
+ "DebugSubsectionHeader::Kind %X != S_FILECHECKSUMS (%X)",
+ static_cast<uint32_t>(section->header.kind), static_cast<uint32_t>(CodeView::DBI::DebugSubsectionKind::S_FILECHECKSUMS));
+
+ size_t offset = m_stream.GetPointerOffset(section);
+ const size_t headerEnd = BitUtil::RoundUpToMultiple<size_t>(offset + sizeof(CodeView::DBI::DebugSubsectionHeader) + section->header.size, 4u);
+
+ offset = BitUtil::RoundUpToMultiple<size_t>(offset + sizeof(CodeView::DBI::DebugSubsectionHeader), 4u);
+
+ // read all file checksums
+ while (offset < headerEnd)
+ {
+ const CodeView::DBI::FileChecksumHeader* fileChecksumHeader = m_stream.GetDataAtOffset<const CodeView::DBI::FileChecksumHeader>(offset);
+
+ functor(fileChecksumHeader);
+
+ offset = BitUtil::RoundUpToMultiple<size_t>(offset + sizeof(CodeView::DBI::FileChecksumHeader) + fileChecksumHeader->checksumSize, 4u);
+ }
+
+ PDB_ASSERT(offset == headerEnd, "Mismatch between offset %zu and header end %zu when reading file checksums", offset, headerEnd);
+ }
+
+ template <typename F>
+ void ForEachInlineeSourceLine(const CodeView::DBI::LineSection* section, F&& functor) const PDB_NO_EXCEPT
+ {
+ PDB_ASSERT(section->header.kind == CodeView::DBI::DebugSubsectionKind::S_INLINEELINES,
+ "DebugSubsectionHeader::Kind %X != S_INLINEELINES (%X)",
+ static_cast<uint32_t>(section->header.kind), static_cast<uint32_t>(CodeView::DBI::DebugSubsectionKind::S_INLINEELINES));
+
+ PDB_ASSERT(section->inlineeHeader.kind == CodeView::DBI::InlineeSourceLineKind::Signature,
+ "InlineeSourceLineKind %X != :InlineeSourceLineKind::Signature (%X)", static_cast<uint32_t>(section->header.kind), static_cast<uint32_t>(CodeView::DBI::InlineeSourceLineKind::Signature));
+
+ size_t offset = m_stream.GetPointerOffset(section);
+ const size_t headerEnd = BitUtil::RoundUpToMultiple<size_t>(offset + sizeof(CodeView::DBI::DebugSubsectionHeader) + section->header.size, 4u);
+
+ offset = BitUtil::RoundUpToMultiple<size_t>(offset + sizeof(CodeView::DBI::DebugSubsectionHeader) + sizeof(CodeView::DBI::InlineeSourceLineHeader), 4u);
+
+ // read all file checksums
+ while (offset < headerEnd)
+ {
+ const CodeView::DBI::InlineeSourceLine* inlineeSourceLine = m_stream.GetDataAtOffset<const CodeView::DBI::InlineeSourceLine>(offset);
+
+ functor(inlineeSourceLine);
+
+ offset = BitUtil::RoundUpToMultiple<size_t>(offset + sizeof(CodeView::DBI::InlineeSourceLine), 4u);
+ }
+ }
+
+ template <typename F>
+ void ForEachInlineeSourceLineEx(const CodeView::DBI::LineSection* section, F&& functor) const PDB_NO_EXCEPT
+ {
+ PDB_ASSERT(section->header.kind == CodeView::DBI::DebugSubsectionKind::S_INLINEELINES,
+ "DebugSubsectionHeader::Kind %X != S_INLINEELINES (%X)", static_cast<uint32_t>(section->header.kind), static_cast<uint32_t>(CodeView::DBI::DebugSubsectionKind::S_INLINEELINES));
+
+ PDB_ASSERT(section->inlineeHeader.kind == CodeView::DBI::InlineeSourceLineKind::SignatureEx,
+ "InlineeSourceLineKind %X != :InlineeSourceLineKind::SignatureEx (%X)", static_cast<uint32_t>(section->header.kind), static_cast<uint32_t>(CodeView::DBI::InlineeSourceLineKind::SignatureEx));
+
+ size_t offset = m_stream.GetPointerOffset(section);
+ const size_t headerEnd = BitUtil::RoundUpToMultiple<size_t>(offset + sizeof(CodeView::DBI::DebugSubsectionHeader) + section->header.size, 4u);
+
+ offset = BitUtil::RoundUpToMultiple<size_t>(offset + sizeof(CodeView::DBI::DebugSubsectionHeader) + sizeof(CodeView::DBI::InlineeSourceLineHeader), 4u);
+
+ // read all file checksums
+ while (offset < headerEnd)
+ {
+ const CodeView::DBI::InlineeSourceLineEx* inlineeSourceLineEx = m_stream.GetDataAtOffset<const CodeView::DBI::InlineeSourceLineEx>(offset);
+
+ functor(inlineeSourceLineEx);
+
+ offset = BitUtil::RoundUpToMultiple<size_t>(offset + sizeof(CodeView::DBI::InlineeSourceLineEx) + (inlineeSourceLineEx->extraLines * sizeof(uint32_t)), 4u);
+ }
+ }
+ private:
+ CoalescedMSFStream m_stream;
+ size_t m_c13LineInfoOffset;
+
+ PDB_DISABLE_COPY(ModuleLineStream);
+ };
+}
diff --git a/thirdparty/raw_pdb/src/PDB_ModuleSymbolStream.cpp b/thirdparty/raw_pdb/src/PDB_ModuleSymbolStream.cpp
new file mode 100644
index 000000000..ae10a119a
--- /dev/null
+++ b/thirdparty/raw_pdb/src/PDB_ModuleSymbolStream.cpp
@@ -0,0 +1,61 @@
+// Copyright 2011-2022, Molecular Matters GmbH <[email protected]>
+// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
+
+#include "PDB_PCH.h"
+#include "PDB_ModuleSymbolStream.h"
+#include "PDB_RawFile.h"
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB::ModuleSymbolStream::ModuleSymbolStream(void) PDB_NO_EXCEPT
+ : m_stream()
+{
+}
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB::ModuleSymbolStream::ModuleSymbolStream(const RawFile& file, uint16_t streamIndex, uint32_t symbolStreamSize) PDB_NO_EXCEPT
+ : m_stream(file.CreateMSFStream<CoalescedMSFStream>(streamIndex, symbolStreamSize))
+{
+ // https://llvm.org/docs/PDB/ModiStream.html
+ // struct ModiStream {
+ // uint32_t Signature;
+ // uint8_t Symbols[SymbolSize - 4];
+ // uint8_t C11LineInfo[C11Size];
+ // uint8_t C13LineInfo[C13Size];
+ // uint32_t GlobalRefsSize;
+ // uint8_t GlobalRefs[GlobalRefsSize];
+ // };
+ // we are only interested in the symbols, but not the line information or global refs.
+ // the coalesced stream is therefore only built for the symbols, not all the data in the stream.
+ // this potentially saves a lot of memory and performance on large PDBs.
+}
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB_NO_DISCARD const PDB::CodeView::DBI::Record* PDB::ModuleSymbolStream::FindRecord(CodeView::DBI::SymbolRecordKind kind) const PDB_NO_EXCEPT
+{
+ // ignore the stream's 4-byte signature
+ size_t offset = sizeof(uint32_t);
+
+ // parse the CodeView records
+ while (offset < m_stream.GetSize())
+ {
+ // https://llvm.org/docs/PDB/CodeViewTypes.html
+ const CodeView::DBI::Record* record = m_stream.GetDataAtOffset<const CodeView::DBI::Record>(offset);
+ if (record->header.kind == kind)
+ {
+ return record;
+ }
+
+ const uint32_t recordSize = GetCodeViewRecordSize(record);
+
+ // position the module stream offset at the next record
+ offset = BitUtil::RoundUpToMultiple<size_t>(offset + sizeof(CodeView::DBI::RecordHeader) + recordSize, 4u);
+ }
+
+ return nullptr;
+}
diff --git a/thirdparty/raw_pdb/src/PDB_ModuleSymbolStream.h b/thirdparty/raw_pdb/src/PDB_ModuleSymbolStream.h
new file mode 100644
index 000000000..213870824
--- /dev/null
+++ b/thirdparty/raw_pdb/src/PDB_ModuleSymbolStream.h
@@ -0,0 +1,70 @@
+// Copyright 2011-2022, Molecular Matters GmbH <[email protected]>
+// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
+
+#pragma once
+
+#include "Foundation/PDB_Macros.h"
+#include "Foundation/PDB_BitUtil.h"
+#include "PDB_DBITypes.h"
+#include "PDB_Util.h"
+#include "PDB_CoalescedMSFStream.h"
+
+
+namespace PDB
+{
+ class RawFile;
+
+
+ class PDB_NO_DISCARD ModuleSymbolStream
+ {
+ public:
+ ModuleSymbolStream(void) PDB_NO_EXCEPT;
+ explicit ModuleSymbolStream(const RawFile& file, uint16_t streamIndex, uint32_t symbolStreamSize) PDB_NO_EXCEPT;
+
+ PDB_DEFAULT_MOVE(ModuleSymbolStream);
+
+ // Returns a record's parent record.
+ template <typename T>
+ PDB_NO_DISCARD inline const CodeView::DBI::Record* GetParentRecord(const T& record) const PDB_NO_EXCEPT
+ {
+ return m_stream.GetDataAtOffset<const CodeView::DBI::Record>(record.parent);
+ }
+
+ // Returns a record's end record.
+ template <typename T>
+ PDB_NO_DISCARD inline const CodeView::DBI::Record* GetEndRecord(const T& record) const PDB_NO_EXCEPT
+ {
+ return m_stream.GetDataAtOffset<const CodeView::DBI::Record>(record.end);
+ }
+
+ // Finds a record of a certain kind.
+ PDB_NO_DISCARD const CodeView::DBI::Record* FindRecord(CodeView::DBI::SymbolRecordKind Kind) const PDB_NO_EXCEPT;
+
+
+ // Iterates all records in the stream.
+ template <typename F>
+ void ForEachSymbol(F&& functor) const PDB_NO_EXCEPT
+ {
+ // ignore the stream's 4-byte signature
+ size_t offset = sizeof(uint32_t);
+
+ // parse the CodeView records
+ while (offset < m_stream.GetSize())
+ {
+ // https://llvm.org/docs/PDB/CodeViewTypes.html
+ const CodeView::DBI::Record* record = m_stream.GetDataAtOffset<const CodeView::DBI::Record>(offset);
+ const uint32_t recordSize = GetCodeViewRecordSize(record);
+
+ functor(record);
+
+ // position the module stream offset at the next record
+ offset = BitUtil::RoundUpToMultiple<size_t>(offset + sizeof(CodeView::DBI::RecordHeader) + recordSize, 4u);
+ }
+ }
+
+ private:
+ CoalescedMSFStream m_stream;
+
+ PDB_DISABLE_COPY(ModuleSymbolStream);
+ };
+}
diff --git a/thirdparty/raw_pdb/src/PDB_NamesStream.cpp b/thirdparty/raw_pdb/src/PDB_NamesStream.cpp
new file mode 100644
index 000000000..fae895e2b
--- /dev/null
+++ b/thirdparty/raw_pdb/src/PDB_NamesStream.cpp
@@ -0,0 +1,28 @@
+// Copyright 2011-2022, Molecular Matters GmbH <[email protected]>
+// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
+
+#include "PDB_PCH.h"
+#include "PDB_NamesStream.h"
+#include "PDB_RawFile.h"
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB::NamesStream::NamesStream(void) PDB_NO_EXCEPT
+ : m_stream()
+ , m_header(nullptr)
+ , m_stringTable(nullptr)
+{
+}
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB::NamesStream::NamesStream(const RawFile& file, uint32_t streamIndex) PDB_NO_EXCEPT
+ : m_stream(file.CreateMSFStream<CoalescedMSFStream>(streamIndex))
+ , m_header(m_stream.GetDataAtOffset<const NamesHeader>(0u))
+ , m_stringTable(nullptr)
+{
+ // grab a pointer into the string table
+ m_stringTable = m_stream.GetDataAtOffset<char>(sizeof(NamesHeader));
+}
diff --git a/thirdparty/raw_pdb/src/PDB_NamesStream.h b/thirdparty/raw_pdb/src/PDB_NamesStream.h
new file mode 100644
index 000000000..c305242a5
--- /dev/null
+++ b/thirdparty/raw_pdb/src/PDB_NamesStream.h
@@ -0,0 +1,48 @@
+// Copyright 2011-2022, Molecular Matters GmbH <[email protected]>
+// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
+
+#pragma once
+
+#include "Foundation/PDB_Macros.h"
+#include "PDB_Types.h"
+#include "PDB_CoalescedMSFStream.h"
+
+
+namespace PDB
+{
+ class RawFile;
+
+ struct NamesHeader
+ {
+ uint32_t magic;
+ uint32_t hashVersion;
+ uint32_t size;
+ };
+
+ class PDB_NO_DISCARD NamesStream
+ {
+ public:
+ NamesStream(void) PDB_NO_EXCEPT;
+ explicit NamesStream(const RawFile& file, uint32_t streamIndex) PDB_NO_EXCEPT;
+
+ PDB_DEFAULT_MOVE(NamesStream);
+
+ // Returns the header of the stream.
+ PDB_NO_DISCARD inline const NamesHeader* GetHeader(void) const PDB_NO_EXCEPT
+ {
+ return m_header;
+ }
+
+ PDB_NO_DISCARD inline const char* GetFilename(uint32_t filenameOffset) const PDB_NO_EXCEPT
+ {
+ return m_stringTable + filenameOffset;
+ }
+
+ private:
+ CoalescedMSFStream m_stream;
+ const NamesHeader* m_header;
+ const char* m_stringTable;
+
+ PDB_DISABLE_COPY(NamesStream);
+ };
+}
diff --git a/thirdparty/raw_pdb/src/PDB_PCH.cpp b/thirdparty/raw_pdb/src/PDB_PCH.cpp
new file mode 100644
index 000000000..70ca8d68d
--- /dev/null
+++ b/thirdparty/raw_pdb/src/PDB_PCH.cpp
@@ -0,0 +1,4 @@
+// Copyright 2011-2022, Molecular Matters GmbH <[email protected]>
+// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
+
+#include "PDB_PCH.h"
diff --git a/thirdparty/raw_pdb/src/PDB_PCH.h b/thirdparty/raw_pdb/src/PDB_PCH.h
new file mode 100644
index 000000000..8374e1012
--- /dev/null
+++ b/thirdparty/raw_pdb/src/PDB_PCH.h
@@ -0,0 +1,20 @@
+// Copyright 2011-2022, Molecular Matters GmbH <[email protected]>
+// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
+
+#pragma once
+
+// this needs to be the first include, since it determines the platform/toolchain we're compiling for
+#include "Foundation/PDB_Platform.h"
+#include "Foundation/PDB_Macros.h"
+#include "Foundation/PDB_Warnings.h"
+
+// library includes
+#include "Foundation/PDB_Log.h"
+#include "Foundation/PDB_Assert.h"
+#include "Foundation/PDB_Move.h"
+#include "Foundation/PDB_Forward.h"
+#include "Foundation/PDB_Memory.h"
+#include "Foundation/PDB_ArrayView.h"
+#include "Foundation/PDB_BitUtil.h"
+#include "Foundation/PDB_BitOperators.h"
+#include "Foundation/PDB_PointerUtil.h"
diff --git a/thirdparty/raw_pdb/src/PDB_PublicSymbolStream.cpp b/thirdparty/raw_pdb/src/PDB_PublicSymbolStream.cpp
new file mode 100644
index 000000000..5efad2238
--- /dev/null
+++ b/thirdparty/raw_pdb/src/PDB_PublicSymbolStream.cpp
@@ -0,0 +1,43 @@
+// Copyright 2011-2022, Molecular Matters GmbH <[email protected]>
+// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
+
+#include "PDB_PCH.h"
+#include "PDB_PublicSymbolStream.h"
+#include "PDB_RawFile.h"
+#include "PDB_Types.h"
+#include "PDB_DBITypes.h"
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB::PublicSymbolStream::PublicSymbolStream(void) PDB_NO_EXCEPT
+ : m_stream()
+ , m_hashRecords(nullptr)
+ , m_count(0u)
+{
+}
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB::PublicSymbolStream::PublicSymbolStream(const RawFile& file, uint16_t streamIndex, uint32_t count) PDB_NO_EXCEPT
+ : m_stream(file.CreateMSFStream<CoalescedMSFStream>(streamIndex))
+ , m_hashRecords(m_stream.GetDataAtOffset<HashRecord>(sizeof(PublicStreamHeader) + sizeof(HashTableHeader)))
+ , m_count(count)
+{
+}
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB_NO_DISCARD const PDB::CodeView::DBI::Record* PDB::PublicSymbolStream::GetRecord(const CoalescedMSFStream& symbolRecordStream, const HashRecord& hashRecord) const PDB_NO_EXCEPT
+{
+ // hash record offsets start at 1, not at 0
+ const uint32_t headerOffset = hashRecord.offset - 1u;
+
+ // the offset doesn't point to the public symbol directly, but to the CodeView record:
+ // https://llvm.org/docs/PDB/CodeViewSymbols.html
+ const CodeView::DBI::Record* record = symbolRecordStream.GetDataAtOffset<const CodeView::DBI::Record>(headerOffset);
+
+ return record;
+}
diff --git a/thirdparty/raw_pdb/src/PDB_PublicSymbolStream.h b/thirdparty/raw_pdb/src/PDB_PublicSymbolStream.h
new file mode 100644
index 000000000..681a613f9
--- /dev/null
+++ b/thirdparty/raw_pdb/src/PDB_PublicSymbolStream.h
@@ -0,0 +1,49 @@
+// Copyright 2011-2022, Molecular Matters GmbH <[email protected]>
+// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
+
+#pragma once
+
+#include "Foundation/PDB_Macros.h"
+#include "Foundation/PDB_ArrayView.h"
+#include "PDB_CoalescedMSFStream.h"
+
+
+namespace PDB
+{
+ class RawFile;
+ struct HashRecord;
+
+ namespace CodeView
+ {
+ namespace DBI
+ {
+ struct Record;
+ }
+ }
+
+
+ class PDB_NO_DISCARD PublicSymbolStream
+ {
+ public:
+ PublicSymbolStream(void) PDB_NO_EXCEPT;
+ explicit PublicSymbolStream(const RawFile& file, uint16_t streamIndex, uint32_t count) PDB_NO_EXCEPT;
+
+ PDB_DEFAULT_MOVE(PublicSymbolStream);
+
+ // Turns a given hash record into a DBI record using the given symbol stream.
+ PDB_NO_DISCARD const CodeView::DBI::Record* GetRecord(const CoalescedMSFStream& symbolRecordStream, const HashRecord& hashRecord) const PDB_NO_EXCEPT;
+
+ // Returns a view of all the records in the stream.
+ PDB_NO_DISCARD inline ArrayView<HashRecord> GetRecords(void) const PDB_NO_EXCEPT
+ {
+ return ArrayView<HashRecord>(m_hashRecords, m_count);
+ }
+
+ private:
+ CoalescedMSFStream m_stream;
+ const HashRecord* m_hashRecords;
+ uint32_t m_count;
+
+ PDB_DISABLE_COPY(PublicSymbolStream);
+ };
+}
diff --git a/thirdparty/raw_pdb/src/PDB_RawFile.cpp b/thirdparty/raw_pdb/src/PDB_RawFile.cpp
new file mode 100644
index 000000000..f135efe66
--- /dev/null
+++ b/thirdparty/raw_pdb/src/PDB_RawFile.cpp
@@ -0,0 +1,147 @@
+// Copyright 2011-2022, Molecular Matters GmbH <[email protected]>
+// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
+
+#include "PDB_PCH.h"
+#include "PDB_RawFile.h"
+#include "PDB_Types.h"
+#include "PDB_Util.h"
+#include "PDB_DirectMSFStream.h"
+#include "Foundation/PDB_PointerUtil.h"
+#include "Foundation/PDB_Memory.h"
+#include "Foundation/PDB_Assert.h"
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB::RawFile::RawFile(RawFile&& other) PDB_NO_EXCEPT
+ : m_data(PDB_MOVE(other.m_data))
+ , m_superBlock(PDB_MOVE(other.m_superBlock))
+ , m_directoryStream(PDB_MOVE(other.m_directoryStream))
+ , m_streamCount(PDB_MOVE(other.m_streamCount))
+ , m_streamSizes(PDB_MOVE(other.m_streamSizes))
+ , m_streamBlocks(PDB_MOVE(other.m_streamBlocks))
+{
+ other.m_data = nullptr;
+ other.m_superBlock = nullptr;
+ other.m_streamCount = 0u;
+ other.m_streamSizes = nullptr;
+ other.m_streamBlocks = nullptr;
+}
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB::RawFile& PDB::RawFile::operator=(RawFile&& other) PDB_NO_EXCEPT
+{
+ if (this != &other)
+ {
+ PDB_DELETE_ARRAY(m_streamBlocks);
+
+ m_data = PDB_MOVE(other.m_data);
+ m_superBlock = PDB_MOVE(other.m_superBlock);
+ m_directoryStream = PDB_MOVE(other.m_directoryStream);
+ m_streamCount = PDB_MOVE(other.m_streamCount);
+ m_streamSizes = PDB_MOVE(other.m_streamSizes);
+ m_streamBlocks = PDB_MOVE(other.m_streamBlocks);
+
+ other.m_data = nullptr;
+ other.m_superBlock = nullptr;
+ other.m_streamCount = 0u;
+ other.m_streamSizes = nullptr;
+ other.m_streamBlocks = nullptr;
+ }
+
+ return *this;
+}
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB::RawFile::RawFile(const void* data) PDB_NO_EXCEPT
+ : m_data(data)
+ , m_superBlock(Pointer::Offset<const SuperBlock*>(data, 0u))
+ , m_directoryStream()
+ , m_streamCount(0u)
+ , m_streamSizes(nullptr)
+ , m_streamBlocks(nullptr)
+{
+ // the SuperBlock stores an array of indices of blocks that make up the indices of directory blocks, which need to be stitched together to form the directory.
+ // the blocks holding the indices of directory blocks are not necessarily contiguous, so they need to be coalesced first.
+ const uint32_t directoryBlockCount = PDB::ConvertSizeToBlockCount(m_superBlock->directorySize, m_superBlock->blockSize);
+
+ // the directory is made up of directoryBlockCount blocks, so we need that many indices to be read from the blocks that make up the indices
+ CoalescedMSFStream directoryIndicesStream(data, m_superBlock->blockSize, m_superBlock->directoryBlockIndices, directoryBlockCount * sizeof(uint32_t));
+
+ // these are the indices of blocks making up the directory stream, now guaranteed to be contiguous
+ const uint32_t* directoryIndices = directoryIndicesStream.GetDataAtOffset<uint32_t>(0u);
+
+ m_directoryStream = CoalescedMSFStream(data, m_superBlock->blockSize, directoryIndices, m_superBlock->directorySize);
+
+ // https://llvm.org/docs/PDB/MsfFile.html#the-stream-directory
+ // parse the directory from its contiguous version. the directory matches the following struct:
+ // struct StreamDirectory
+ // {
+ // uint32_t streamCount;
+ // uint32_t streamSizes[streamCount];
+ // uint32_t streamBlocks[streamCount][];
+ // };
+ m_streamCount = *m_directoryStream.GetDataAtOffset<uint32_t>(0u);
+
+ // we can assign pointers into the stream directly, since the RawFile keeps ownership of the directory stream
+ m_streamSizes = m_directoryStream.GetDataAtOffset<uint32_t>(sizeof(uint32_t));
+ const uint32_t* directoryStreamBlocks = m_directoryStream.GetDataAtOffset<uint32_t>(sizeof(uint32_t) + sizeof(uint32_t) * m_streamCount);
+
+ // prepare indices for directly accessing individual streams
+ m_streamBlocks = PDB_NEW_ARRAY(const uint32_t*, m_streamCount);
+
+ const uint32_t* indicesForCurrentBlock = directoryStreamBlocks;
+ for (uint32_t i = 0u; i < m_streamCount; ++i)
+ {
+ const uint32_t sizeInBytes = GetStreamSize(i);
+ const uint32_t blockCount = ConvertSizeToBlockCount(sizeInBytes, m_superBlock->blockSize);
+ m_streamBlocks[i] = indicesForCurrentBlock;
+
+ indicesForCurrentBlock += blockCount;
+ }
+}
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB::RawFile::~RawFile(void) PDB_NO_EXCEPT
+{
+ PDB_DELETE_ARRAY(m_streamBlocks);
+}
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+template <typename T>
+PDB_NO_DISCARD T PDB::RawFile::CreateMSFStream(uint32_t streamIndex) const PDB_NO_EXCEPT
+{
+ PDB_ASSERT(streamIndex != PDB::NilStreamIndex, "Invalid stream index.");
+ PDB_ASSERT(streamIndex < m_streamCount, "Invalid stream index.");
+
+ return T(m_data, m_superBlock->blockSize, m_streamBlocks[streamIndex], GetStreamSize(streamIndex));
+}
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+template <typename T>
+PDB_NO_DISCARD T PDB::RawFile::CreateMSFStream(uint32_t streamIndex, uint32_t streamSize) const PDB_NO_EXCEPT
+{
+ PDB_ASSERT(streamIndex != PDB::NilStreamIndex, "Invalid stream index.");
+ PDB_ASSERT(streamIndex < m_streamCount, "Invalid stream index.");
+ PDB_ASSERT(streamSize <= GetStreamSize(streamIndex), "Invalid stream size.");
+
+ return T(m_data, m_superBlock->blockSize, m_streamBlocks[streamIndex], streamSize);
+}
+
+
+// explicit template instantiation
+template PDB::CoalescedMSFStream PDB::RawFile::CreateMSFStream<PDB::CoalescedMSFStream>(uint32_t streamIndex) const PDB_NO_EXCEPT;
+template PDB::DirectMSFStream PDB::RawFile::CreateMSFStream<PDB::DirectMSFStream>(uint32_t streamIndex) const PDB_NO_EXCEPT;
+
+template PDB::CoalescedMSFStream PDB::RawFile::CreateMSFStream<PDB::CoalescedMSFStream>(uint32_t streamIndex, uint32_t streamSize) const PDB_NO_EXCEPT;
+template PDB::DirectMSFStream PDB::RawFile::CreateMSFStream<PDB::DirectMSFStream>(uint32_t streamIndex, uint32_t streamSize) const PDB_NO_EXCEPT;
diff --git a/thirdparty/raw_pdb/src/PDB_RawFile.h b/thirdparty/raw_pdb/src/PDB_RawFile.h
new file mode 100644
index 000000000..bf886734c
--- /dev/null
+++ b/thirdparty/raw_pdb/src/PDB_RawFile.h
@@ -0,0 +1,66 @@
+// Copyright 2011-2022, Molecular Matters GmbH <[email protected]>
+// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
+
+#pragma once
+
+#include "Foundation/PDB_Macros.h"
+#include "PDB_CoalescedMSFStream.h"
+
+
+// https://llvm.org/docs/PDB/index.html
+namespace PDB
+{
+ struct SuperBlock;
+
+
+ class PDB_NO_DISCARD RawFile
+ {
+ public:
+ RawFile(RawFile&& other) PDB_NO_EXCEPT;
+ RawFile& operator=(RawFile&& other) PDB_NO_EXCEPT;
+
+ explicit RawFile(const void* data) PDB_NO_EXCEPT;
+ ~RawFile(void) PDB_NO_EXCEPT;
+
+ // Creates any type of MSF stream.
+ template <typename T>
+ PDB_NO_DISCARD T CreateMSFStream(uint32_t streamIndex) const PDB_NO_EXCEPT;
+
+ // Creates any type of MSF stream with the given size.
+ template <typename T>
+ PDB_NO_DISCARD T CreateMSFStream(uint32_t streamIndex, uint32_t streamSize) const PDB_NO_EXCEPT;
+
+
+ // Returns the SuperBlock.
+ PDB_NO_DISCARD inline const SuperBlock* GetSuperBlock(void) const PDB_NO_EXCEPT
+ {
+ return m_superBlock;
+ }
+
+ // Returns the number of streams in the PDB file.
+ PDB_NO_DISCARD inline uint32_t GetStreamCount(void) const PDB_NO_EXCEPT
+ {
+ return m_streamCount;
+ }
+
+ // Returns the size of the stream with the given index, taking into account nil page sizes.
+ PDB_NO_DISCARD inline uint32_t GetStreamSize(uint32_t streamIndex) const PDB_NO_EXCEPT
+ {
+ const uint32_t streamSize = m_streamSizes[streamIndex];
+
+ return (streamSize == NilPageSize) ? 0u : streamSize;
+ }
+
+ private:
+ const void* m_data;
+ const SuperBlock* m_superBlock;
+ CoalescedMSFStream m_directoryStream;
+
+ // stream directory
+ uint32_t m_streamCount;
+ const uint32_t* m_streamSizes;
+ const uint32_t** m_streamBlocks;
+
+ PDB_DISABLE_COPY(RawFile);
+ };
+}
diff --git a/thirdparty/raw_pdb/src/PDB_SectionContributionStream.cpp b/thirdparty/raw_pdb/src/PDB_SectionContributionStream.cpp
new file mode 100644
index 000000000..a8a944eb1
--- /dev/null
+++ b/thirdparty/raw_pdb/src/PDB_SectionContributionStream.cpp
@@ -0,0 +1,25 @@
+// Copyright 2011-2022, Molecular Matters GmbH <[email protected]>
+// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
+
+#include "PDB_PCH.h"
+#include "PDB_SectionContributionStream.h"
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB::SectionContributionStream::SectionContributionStream(void) PDB_NO_EXCEPT
+ : m_stream()
+ , m_contributions(nullptr)
+ , m_count(0u)
+{
+}
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB::SectionContributionStream::SectionContributionStream(const DirectMSFStream& directStream, uint32_t size, uint32_t offset) PDB_NO_EXCEPT
+ : m_stream(directStream, size, offset)
+ , m_contributions(m_stream.GetDataAtOffset<DBI::SectionContribution>(0u))
+ , m_count(size / sizeof(DBI::SectionContribution))
+{
+}
diff --git a/thirdparty/raw_pdb/src/PDB_SectionContributionStream.h b/thirdparty/raw_pdb/src/PDB_SectionContributionStream.h
new file mode 100644
index 000000000..c1a183d26
--- /dev/null
+++ b/thirdparty/raw_pdb/src/PDB_SectionContributionStream.h
@@ -0,0 +1,38 @@
+// Copyright 2011-2022, Molecular Matters GmbH <[email protected]>
+// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
+
+#pragma once
+
+#include "Foundation/PDB_Macros.h"
+#include "Foundation/PDB_ArrayView.h"
+#include "PDB_DBITypes.h"
+#include "PDB_CoalescedMSFStream.h"
+
+
+namespace PDB
+{
+ class PDB_NO_DISCARD DirectMSFStream;
+
+
+ class PDB_NO_DISCARD SectionContributionStream
+ {
+ public:
+ SectionContributionStream(void) PDB_NO_EXCEPT;
+ explicit SectionContributionStream(const DirectMSFStream& directStream, uint32_t size, uint32_t offset) PDB_NO_EXCEPT;
+
+ PDB_DEFAULT_MOVE(SectionContributionStream);
+
+ // Returns a view of all section contributions in the stream.
+ PDB_NO_DISCARD inline ArrayView<DBI::SectionContribution> GetContributions(void) const PDB_NO_EXCEPT
+ {
+ return ArrayView<DBI::SectionContribution>(m_contributions, m_count);
+ }
+
+ private:
+ CoalescedMSFStream m_stream;
+ const DBI::SectionContribution* m_contributions;
+ size_t m_count;
+
+ PDB_DISABLE_COPY(SectionContributionStream);
+ };
+}
diff --git a/thirdparty/raw_pdb/src/PDB_SourceFileStream.cpp b/thirdparty/raw_pdb/src/PDB_SourceFileStream.cpp
new file mode 100644
index 000000000..fc860f622
--- /dev/null
+++ b/thirdparty/raw_pdb/src/PDB_SourceFileStream.cpp
@@ -0,0 +1,68 @@
+// Copyright 2011-2022, Molecular Matters GmbH <[email protected]>
+// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
+
+#include "PDB_PCH.h"
+#include "PDB_SourceFileStream.h"
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB::SourceFileStream::SourceFileStream(void) PDB_NO_EXCEPT
+ : m_stream()
+ , m_moduleCount(0u)
+ , m_moduleIndices(nullptr)
+ , m_moduleFileCounts(nullptr)
+ , m_fileNameOffsets(nullptr)
+ , m_stringTable(nullptr)
+{
+}
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB::SourceFileStream::SourceFileStream(const DirectMSFStream& directStream, uint32_t size, uint32_t offset) PDB_NO_EXCEPT
+ : m_stream(directStream, size, offset)
+ , m_moduleCount(0u)
+ , m_moduleIndices(nullptr)
+ , m_moduleFileCounts(nullptr)
+ , m_fileNameOffsets(nullptr)
+ , m_stringTable(nullptr)
+{
+ // we are going to consume the whole source info sub-stream, so create a coalesced stream for faster read operations and direct access.
+ // the sub-stream has the following layout:
+ // struct SourceInfoSubstream
+ // {
+ // uint16_t moduleCount;
+ // uint16_t sourceFileCount;
+ // uint16_t moduleIndices[moduleCount];
+ // uint16_t moduleFileCounts[moduleCount];
+ // uint32_t fileNameOffsets[realSourceFileCount];
+ // char stringTable[][realSourceFileCount];
+ // };
+ m_moduleCount = *m_stream.GetDataAtOffset<uint16_t>(0u);
+ size_t readOffset = sizeof(uint16_t);
+
+ // skip number of source files. this would only support 64k unique files and is no longer used.
+ // the number of source files is computed dynamically instead.
+ readOffset += sizeof(uint16_t);
+
+ // grab direct pointers into the stream data
+ m_moduleIndices = m_stream.GetDataAtOffset<uint16_t>(readOffset);
+ readOffset += sizeof(uint16_t) * m_moduleCount;
+
+ m_moduleFileCounts = m_stream.GetDataAtOffset<uint16_t>(readOffset);
+ readOffset += sizeof(uint16_t) * m_moduleCount;
+
+ // count the actual number of source files
+ size_t sourceFileCount = 0u;
+ for (unsigned int i = 0u; i < m_moduleCount; ++i)
+ {
+ sourceFileCount += m_moduleFileCounts[i];
+ }
+
+ m_fileNameOffsets = m_stream.GetDataAtOffset<uint32_t>(readOffset);
+ readOffset += sizeof(uint32_t) * sourceFileCount;
+
+ // grab a pointer into the string table
+ m_stringTable = m_stream.GetDataAtOffset<char>(readOffset);
+}
diff --git a/thirdparty/raw_pdb/src/PDB_SourceFileStream.h b/thirdparty/raw_pdb/src/PDB_SourceFileStream.h
new file mode 100644
index 000000000..a32c4bd7f
--- /dev/null
+++ b/thirdparty/raw_pdb/src/PDB_SourceFileStream.h
@@ -0,0 +1,65 @@
+// Copyright 2011-2022, Molecular Matters GmbH <[email protected]>
+// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
+
+#pragma once
+
+#include "Foundation/PDB_Macros.h"
+#include "Foundation/PDB_ArrayView.h"
+#include "PDB_CoalescedMSFStream.h"
+
+
+namespace PDB
+{
+ class PDB_NO_DISCARD DirectMSFStream;
+
+
+ class PDB_NO_DISCARD SourceFileStream
+ {
+ public:
+ SourceFileStream(void) PDB_NO_EXCEPT;
+ explicit SourceFileStream(const DirectMSFStream& directStream, uint32_t size, uint32_t offset) PDB_NO_EXCEPT;
+
+ PDB_DEFAULT_MOVE(SourceFileStream);
+
+ // Returns the number of modules.
+ PDB_NO_DISCARD inline uint32_t GetModuleCount(void) const PDB_NO_EXCEPT
+ {
+ return m_moduleCount;
+ }
+
+ // Returns a view of all the filename offsets for the module with the given index.
+ PDB_NO_DISCARD inline ArrayView<uint32_t> GetModuleFilenameOffsets(size_t moduleIndex) const PDB_NO_EXCEPT
+ {
+ const uint16_t moduleStartIndex = m_moduleIndices[moduleIndex];
+ const uint16_t moduleFileCount = m_moduleFileCounts[moduleIndex];
+
+ return ArrayView<uint32_t>(m_fileNameOffsets + moduleStartIndex, moduleFileCount);
+ }
+
+ // Returns a filename for the given filename offset.
+ PDB_NO_DISCARD inline const char* GetFilename(uint32_t filenameOffset) const PDB_NO_EXCEPT
+ {
+ return m_stringTable + filenameOffset;
+ }
+
+ private:
+ CoalescedMSFStream m_stream;
+
+ // the number of modules
+ uint32_t m_moduleCount;
+
+ // the indices into the file name offsets, for each module
+ const uint16_t* m_moduleIndices;
+
+ // the number of files, for each module
+ const uint16_t* m_moduleFileCounts;
+
+ // the filename offsets into the string table, for all modules
+ const uint32_t* m_fileNameOffsets;
+
+ // the string table storing all filenames
+ const char* m_stringTable;
+
+ PDB_DISABLE_COPY(SourceFileStream);
+ };
+}
diff --git a/thirdparty/raw_pdb/src/PDB_TPIStream.cpp b/thirdparty/raw_pdb/src/PDB_TPIStream.cpp
new file mode 100644
index 000000000..164195ecd
--- /dev/null
+++ b/thirdparty/raw_pdb/src/PDB_TPIStream.cpp
@@ -0,0 +1,86 @@
+#include "PDB_PCH.h"
+#include "PDB_TPIStream.h"
+#include "PDB_RawFile.h"
+#include "PDB_DirectMSFStream.h"
+#include "Foundation/PDB_Memory.h"
+
+namespace
+{
+ // the TPI stream always resides at index 2
+ static constexpr const uint32_t TPIStreamIndex = 2u;
+}
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB::TPIStream::TPIStream(void) PDB_NO_EXCEPT
+ : m_stream()
+ , m_header()
+ , m_recordCount(0u)
+{
+}
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB::TPIStream::TPIStream(TPIStream&& other) PDB_NO_EXCEPT
+ : m_stream(PDB_MOVE(other.m_stream))
+ , m_header(PDB_MOVE(other.m_header))
+ , m_recordCount(PDB_MOVE(other.m_recordCount))
+{
+ other.m_recordCount = 0u;
+}
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB::TPIStream& PDB::TPIStream::operator=(TPIStream&& other) PDB_NO_EXCEPT
+{
+ if (this != &other)
+ {
+ m_stream = PDB_MOVE(other.m_stream);
+ m_header = PDB_MOVE(other.m_header);
+ m_recordCount = PDB_MOVE(other.m_recordCount);
+
+ other.m_recordCount = 0u;
+ }
+
+ return *this;
+}
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB::TPIStream::TPIStream(const RawFile& file) PDB_NO_EXCEPT
+ : m_stream(file.CreateMSFStream<DirectMSFStream>(TPIStreamIndex)),
+ m_header(m_stream.ReadAtOffset<TPI::StreamHeader>(0u)),
+ m_recordCount(GetLastTypeIndex() - GetFirstTypeIndex())
+{
+}
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB_NO_DISCARD PDB::ErrorCode PDB::HasValidTPIStream(const RawFile& file) PDB_NO_EXCEPT
+{
+ DirectMSFStream stream = file.CreateMSFStream<DirectMSFStream>(TPIStreamIndex);
+ if (stream.GetSize() < sizeof(TPI::StreamHeader))
+ {
+ return ErrorCode::InvalidStream;
+ }
+
+ const TPI::StreamHeader header = stream.ReadAtOffset<TPI::StreamHeader>(0u);
+ if (header.version != TPI::StreamHeader::Version::V80)
+ {
+ return ErrorCode::UnknownVersion;
+ }
+
+ return ErrorCode::Success;
+}
+
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+PDB_NO_DISCARD PDB::TPIStream PDB::CreateTPIStream(const RawFile& file) PDB_NO_EXCEPT
+{
+ return TPIStream { file };
+}
diff --git a/thirdparty/raw_pdb/src/PDB_TPIStream.h b/thirdparty/raw_pdb/src/PDB_TPIStream.h
new file mode 100644
index 000000000..81e9af066
--- /dev/null
+++ b/thirdparty/raw_pdb/src/PDB_TPIStream.h
@@ -0,0 +1,85 @@
+#pragma once
+
+#include "Foundation/PDB_Macros.h"
+#include "Foundation/PDB_ArrayView.h"
+#include "PDB_ErrorCodes.h"
+#include "PDB_TPITypes.h"
+#include "PDB_DirectMSFStream.h"
+#include "PDB_Util.h"
+
+// PDB TPI stream
+// https://llvm.org/docs/PDB/TpiStream.html
+namespace PDB
+{
+ class RawFile;
+
+
+ class PDB_NO_DISCARD TPIStream
+ {
+ public:
+ TPIStream(void) PDB_NO_EXCEPT;
+ TPIStream(TPIStream&& other) PDB_NO_EXCEPT;
+ TPIStream& operator=(TPIStream&& other) PDB_NO_EXCEPT;
+
+ explicit TPIStream(const RawFile& file) PDB_NO_EXCEPT;
+
+ PDB_NO_DISCARD inline const DirectMSFStream& GetDirectMSFStream(void) const PDB_NO_EXCEPT
+ {
+ return m_stream;
+ }
+
+ // Returns the index of the first type, which is not necessarily zero.
+ PDB_NO_DISCARD inline uint32_t GetFirstTypeIndex(void) const PDB_NO_EXCEPT
+ {
+ return m_header.typeIndexBegin;
+ }
+
+ // Returns the index of the last type.
+ PDB_NO_DISCARD inline uint32_t GetLastTypeIndex(void) const PDB_NO_EXCEPT
+ {
+ return m_header.typeIndexEnd;
+ }
+
+ // Returns the number of type records.
+ PDB_NO_DISCARD inline size_t GetTypeRecordCount(void) const PDB_NO_EXCEPT
+ {
+ return m_recordCount;
+ }
+
+ CodeView::TPI::RecordHeader ReadTypeRecordHeader(size_t offset) const PDB_NO_EXCEPT
+ {
+ const CodeView::TPI::RecordHeader header = m_stream.ReadAtOffset<CodeView::TPI::RecordHeader>(offset);
+ return header;
+ }
+
+ template <typename F>
+ void ForEachTypeRecordHeaderAndOffset(F&& functor) const PDB_NO_EXCEPT
+ {
+ // ignore the stream's header
+ size_t offset = sizeof(TPI::StreamHeader);
+
+ while (offset < m_stream.GetSize())
+ {
+ const CodeView::TPI::RecordHeader header = ReadTypeRecordHeader(offset);
+
+ functor(header, offset);
+
+ // position the stream offset at the next record
+ offset += sizeof(CodeView::TPI::RecordHeader) + header.size - sizeof(uint16_t);
+ }
+ }
+
+ private:
+ DirectMSFStream m_stream;
+ TPI::StreamHeader m_header;
+ size_t m_recordCount;
+
+ PDB_DISABLE_COPY(TPIStream);
+ };
+
+ // Returns whether the given raw file provides a valid TPI stream.
+ PDB_NO_DISCARD ErrorCode HasValidTPIStream(const RawFile& file) PDB_NO_EXCEPT;
+
+ // Creates the TPI stream from a raw file.
+ PDB_NO_DISCARD TPIStream CreateTPIStream(const RawFile& file) PDB_NO_EXCEPT;
+}
diff --git a/thirdparty/raw_pdb/src/PDB_TPITypes.h b/thirdparty/raw_pdb/src/PDB_TPITypes.h
new file mode 100644
index 000000000..c12a5ed0d
--- /dev/null
+++ b/thirdparty/raw_pdb/src/PDB_TPITypes.h
@@ -0,0 +1,867 @@
+#pragma once
+
+#include "Foundation/PDB_Macros.h"
+#include "Foundation/PDB_BitOperators.h"
+
+namespace PDB
+{
+ namespace TPI
+ {
+ // https://llvm.org/docs/PDB/TpiStream.html#stream-header
+ struct StreamHeader
+ {
+ enum class PDB_NO_DISCARD Version : uint32_t
+ {
+ V40 = 19950410u,
+ V41 = 19951122u,
+ V50 = 19961031u,
+ V70 = 19990903u,
+ V80 = 20040203u
+ };
+
+ Version version;
+ uint32_t headerSize;
+ uint32_t typeIndexBegin;
+ uint32_t typeIndexEnd;
+ uint32_t typeRecordBytes;
+
+ uint16_t hashStreamIndex;
+ uint16_t hashAuxStreamIndex;
+ uint32_t hashKeySize;
+ uint32_t numHashBuckets;
+
+ int32_t hashValueBufferOffset;
+ uint32_t hashValueBufferLength;
+
+ int32_t indexOffsetBufferOffset;
+ uint32_t indexOffsetBufferLength;
+
+ int32_t hashAdjBufferOffset;
+ uint32_t hashAdjBufferLength;
+ };
+ }
+
+
+ namespace CodeView
+ {
+ namespace TPI
+ {
+ // https://github.com/microsoft/microsoft-pdb/blob/master/include/cvinfo.h#L772
+ enum class PDB_NO_DISCARD TypeRecordKind : uint16_t
+ {
+ LF_POINTER = 0x1002u,
+ LF_MODIFIER = 0x1001u,
+ LF_PROCEDURE = 0x1008u,
+ LF_MFUNCTION = 0x1009u,
+ LF_LABEL = 0x000eu,
+ LF_ARGLIST = 0x1201u,
+ LF_FIELDLIST = 0x1203u,
+ LF_VTSHAPE = 0x000au,
+ LF_BITFIELD = 0x1205u,
+ LF_METHODLIST = 0x1206u,
+ LF_ENDPRECOMP = 0x0014u,
+
+ LF_BCLASS = 0x001400u,
+ LF_VBCLASS = 0x001401u,
+ LF_IVBCLASS = 0x001402u,
+ LF_FRIENDFCN_ST = 0x001403u,
+ LF_INDEX = 0x001404u,
+ LF_MEMBER_ST = 0x001405u,
+ LF_STMEMBER_ST = 0x001406u,
+ LF_METHOD_ST = 0x001407u,
+ LF_NESTTYPE_ST = 0x001408u,
+ LF_VFUNCTAB = 0x001409u,
+ LF_FRIENDCLS = 0x00140Au,
+ LF_ONEMETHOD_ST = 0x00140Bu,
+ LF_VFUNCOFF = 0x00140Cu,
+ LF_NESTTYPEEX_ST = 0x00140Du,
+ LF_MEMBERMODIFY_ST = 0x00140Eu,
+ LF_MANAGED_ST = 0x00140Fu,
+
+ LF_SMAX = 0x001500u,
+ LF_TYPESERVER = 0x001501u,
+ LF_ENUMERATE = 0x001502u,
+ LF_ARRAY = 0x001503u,
+ LF_CLASS = 0x001504u,
+ LF_STRUCTURE = 0x001505u,
+ LF_UNION = 0x001506u,
+ LF_ENUM = 0x001507u,
+ LF_DIMARRAY = 0x001508u,
+ LF_PRECOMP = 0x001509u,
+ LF_ALIAS = 0x00150Au,
+ LF_DEFARG = 0x00150Bu,
+ LF_FRIENDFCN = 0x00150Cu,
+ LF_MEMBER = 0x00150Du,
+ LF_STMEMBER = 0x00150Eu,
+ LF_METHOD = 0x00150Fu,
+ LF_NESTTYPE = 0x001510u,
+ LF_ONEMETHOD = 0x001511u,
+ LF_NESTTYPEEX = 0x001512u,
+ LF_MEMBERMODIFY = 0x001513u,
+ LF_MANAGED = 0x001514u,
+ LF_TYPESERVER2 = 0x001515u,
+ LF_CLASS2 = 0x001608u,
+ LF_STRUCTURE2 = 0x001609u,
+
+ LF_NUMERIC = 0x8000u,
+ LF_CHAR = 0x8000u,
+ LF_SHORT = 0x8001u,
+ LF_USHORT = 0x8002u,
+ LF_LONG = 0x8003u,
+ LF_ULONG = 0x8004u,
+ LF_REAL32 = 0x8005u,
+ LF_REAL64 = 0x8006u,
+ LF_REAL80 = 0x8007u,
+ LF_REAL128 = 0x8008u,
+ LF_QUADWORD = 0x8009u,
+ LF_UQUADWORD = 0x800au,
+ LF_REAL48 = 0x800bu,
+ LF_COMPLEX32 = 0x800cu,
+ LF_COMPLEX64 = 0x800du,
+ LF_COMPLEX80 = 0x800eu,
+ LF_COMPLEX128 = 0x800fu,
+ LF_VARSTRING = 0x8010u,
+
+ LF_OCTWORD = 0x8017u,
+ LF_UOCTWORD = 0x8018u,
+
+ LF_DECIMAL = 0x8019u,
+ LF_DATE = 0x801au,
+ LF_UTF8STRING = 0x801bu,
+
+ LF_REAL16 = 0x801cu
+ };
+
+ // https://github.com/microsoft/microsoft-pdb/blob/master/include/cvinfo.h#L328
+ // https://github.com/ValveSoftware/wine/blob/cd165953c8b379a78418711f07417022e503c81b/include/wine/mscvpdb.h
+ enum class TypeIndexKind : uint16_t
+ {
+ T_NOTYPE = 0x0000u, // uncharacterized type (no type)
+ T_ABS = 0x0001u, // absolute symbol
+ T_SEGMENT = 0x0002u, // segment type
+ T_VOID = 0x0003u, // void
+ T_HRESULT = 0x0008u, // OLE/COM HRESULT
+ T_32PHRESULT = 0x0408u, // OLE/COM HRESULT __ptr32 *
+ T_64PHRESULT = 0x0608u, // OLE/COM HRESULT __ptr64 *
+
+ // Emitted due to a compiler bug?
+ // 0x0600 bits appears to indicate a 64-bit pointer, but it has no type?
+ // Seen as type index for C11 "_Atomic uint32_t*" variable and constant.
+ T_UNKNOWN_0600 = 0x0600u,
+
+ T_PVOID = 0x0103u, // near pointer to void
+ T_PFVOID = 0x0203u, // far pointer to void
+ T_PHVOID = 0x0303u, // huge pointer to void
+ T_32PVOID = 0x0403u, // 32 bit pointer to void
+ T_32PFVOID = 0x0503u, // 16:32 pointer to void
+ T_64PVOID = 0x0603u, // 64 bit pointer to void
+ T_CURRENCY = 0x0004u, // BASIC 8 byte currency value
+ T_NBASICSTR = 0x0005u, // Near BASIC string
+ T_FBASICSTR = 0x0006u, // Far BASIC string
+ T_NOTTRANS = 0x0007u, // type not translated by cvpack
+ T_BIT = 0x0060u, // bit
+ T_PASCHAR = 0x0061u, // Pascal CHAR
+ T_BOOL32FF = 0x0062u, // 32-bit BOOL where true is 0xffffffff
+
+ T_CHAR = 0x0010u, // 8 bit signed
+ T_PCHAR = 0x0110u, // 16 bit pointer to 8 bit signed
+ T_PFCHAR = 0x0210u, // 16:16 far pointer to 8 bit signed
+ T_PHCHAR = 0x0310u, // 16:16 huge pointer to 8 bit signed
+ T_32PCHAR = 0x0410u, // 32 bit pointer to 8 bit signed
+ T_32PFCHAR = 0x0510u, // 16:32 pointer to 8 bit signed
+ T_64PCHAR = 0x0610u, // 64 bit pointer to 8 bit signed
+
+ T_UCHAR = 0x0020u, // 8 bit unsigned
+ T_PUCHAR = 0x0120u, // 16 bit pointer to 8 bit unsigned
+ T_PFUCHAR = 0x0220u, // 16:16 far pointer to 8 bit unsigned
+ T_PHUCHAR = 0x0320u, // 16:16 huge pointer to 8 bit unsigned
+ T_32PUCHAR = 0x0420u, // 32 bit pointer to 8 bit unsigned
+ T_32PFUCHAR = 0x0520u, // 16:32 pointer to 8 bit unsigned
+ T_64PUCHAR = 0x0620u, // 64 bit pointer to 8 bit unsigned
+
+ T_RCHAR = 0x0070u, // really a char
+ T_PRCHAR = 0x0170u, // 16 bit pointer to a real char
+ T_PFRCHAR = 0x0270u, // 16:16 far pointer to a real char
+ T_PHRCHAR = 0x0370u, // 16:16 huge pointer to a real char
+ T_32PRCHAR = 0x0470u, // 32 bit pointer to a real char
+ T_32PFRCHAR = 0x0570u, // 16:32 pointer to a real char
+ T_64PRCHAR = 0x0670u, // 64 bit pointer to a real char
+
+ // wide character types
+ T_WCHAR = 0x0071u, // wide char
+ T_PWCHAR = 0x0171u, // 16 bit pointer to a wide char
+ T_PFWCHAR = 0x0271u, // 16:16 far pointer to a wide char
+ T_PHWCHAR = 0x0371u, // 16:16 huge pointer to a wide char
+ T_32PWCHAR = 0x0471u, // 32 bit pointer to a wide char
+ T_32PFWCHAR = 0x0571u, // 16:32 pointer to a wide char
+ T_64PWCHAR = 0x0671u, // 64 bit pointer to a wide char
+
+ // 8-bit unicode char
+ T_CHAR8 = 0x007c, // 8-bit unicode char (C++ 20)
+ T_PCHAR8 = 0x017c, // Near pointer to 8-bit unicode char
+ T_PFCHAR8 = 0x027c, // Far pointer to 8-bit unicode char
+ T_PHCHAR8 = 0x037c, // Huge pointer to 8-bit unicode char
+ T_32PCHAR8 = 0x047c, // 16:32 near pointer to 8-bit unicode char
+ T_32PFCHAR8 = 0x057c, // 16:32 far pointer to 8-bit unicode char
+ T_64PCHAR8 = 0x067c, // 64 bit near pointer to 8-bit unicode char
+
+ // 16-bit unicode char
+ T_CHAR16 = 0x007au, // 16-bit unicode char
+ T_PCHAR16 = 0x017au, // 16 bit pointer to a 16-bit unicode char
+ T_PFCHAR16 = 0x027au, // 16:16 far pointer to a 16-bit unicode char
+ T_PHCHAR16 = 0x037au, // 16:16 huge pointer to a 16-bit unicode char
+ T_32PCHAR16 = 0x047au, // 32 bit pointer to a 16-bit unicode char
+ T_32PFCHAR16 = 0x057au, // 16:32 pointer to a 16-bit unicode char
+ T_64PCHAR16 = 0x067au, // 64 bit pointer to a 16-bit unicode char
+
+ // 32-bit unicode char
+ T_CHAR32 = 0x007bu, // 32-bit unicode char
+ T_PCHAR32 = 0x017bu, // 16 bit pointer to a 32-bit unicode char
+ T_PFCHAR32 = 0x027bu, // 16:16 far pointer to a 32-bit unicode char
+ T_PHCHAR32 = 0x037bu, // 16:16 huge pointer to a 32-bit unicode char
+ T_32PCHAR32 = 0x047bu, // 32 bit pointer to a 32-bit unicode char
+ T_32PFCHAR32 = 0x057bu, // 16:32 pointer to a 32-bit unicode char
+ T_64PCHAR32 = 0x067bu, // 64 bit pointer to a 32-bit unicode char
+
+ // 8 bit int types
+ T_INT1 = 0x0068u, // 8 bit signed int
+ T_PINT1 = 0x0168u, // 16 bit pointer to 8 bit signed int
+ T_PFINT1 = 0x0268u, // 16:16 far pointer to 8 bit signed int
+ T_PHINT1 = 0x0368u, // 16:16 huge pointer to 8 bit signed int
+ T_32PINT1 = 0x0468u, // 32 bit pointer to 8 bit signed int
+ T_32PFINT1 = 0x0568u, // 16:32 pointer to 8 bit signed int
+ T_64PINT1 = 0x0668u, // 64 bit pointer to 8 bit signed int
+
+ T_UINT1 = 0x0069u, // 8 bit unsigned int
+ T_PUINT1 = 0x0169u, // 16 bit pointer to 8 bit unsigned int
+ T_PFUINT1 = 0x0269u, // 16:16 far pointer to 8 bit unsigned int
+ T_PHUINT1 = 0x0369u, // 16:16 huge pointer to 8 bit unsigned int
+ T_32PUINT1 = 0x0469u, // 32 bit pointer to 8 bit unsigned int
+ T_32PFUINT1 = 0x0569u, // 16:32 pointer to 8 bit unsigned int
+ T_64PUINT1 = 0x0669u, // 64 bit pointer to 8 bit unsigned int
+
+ // 16 bit short types
+ T_SHORT = 0x0011u, // 16 bit signed
+ T_PSHORT = 0x0111u, // 16 bit pointer to 16 bit signed
+ T_PFSHORT = 0x0211u, // 16:16 far pointer to 16 bit signed
+ T_PHSHORT = 0x0311u, // 16:16 huge pointer to 16 bit signed
+ T_32PSHORT = 0x0411u, // 32 bit pointer to 16 bit signed
+ T_32PFSHORT = 0x0511u, // 16:32 pointer to 16 bit signed
+ T_64PSHORT = 0x0611u, // 64 bit pointer to 16 bit signed
+
+ T_USHORT = 0x0021u,
+ T_PUSHORT = 0x0121u,
+ T_PFUSHORT = 0x0221u,
+ T_PHUSHORT = 0x0321u,
+ T_32PUSHORT = 0x0421u,
+ T_32PFUSHORT = 0x0521u,
+ T_64PUSHORT = 0x0621u,
+
+ T_INT2 = 0x0072u,
+ T_PINT2 = 0x0172u,
+ T_PFINT2 = 0x0272u,
+ T_PHINT2 = 0x0372u,
+ T_32PINT2 = 0x0472u,
+ T_32PFINT2 = 0x0572u,
+ T_64PINT2 = 0x0672u,
+
+ T_UINT2 = 0x0073u,
+ T_PUINT2 = 0x0173u,
+ T_PFUINT2 = 0x0273u,
+ T_PHUINT2 = 0x0373u,
+ T_32PUINT2 = 0x0473u,
+ T_32PFUINT2 = 0x0573u,
+ T_64PUINT2 = 0x0673u,
+
+ T_LONG = 0x0012u,
+ T_PLONG = 0x0112u,
+ T_PFLONG = 0x0212u,
+ T_PHLONG = 0x0312u,
+ T_32PLONG = 0x0412u,
+ T_32PFLONG = 0x0512u,
+ T_64PLONG = 0x0612u,
+
+ T_ULONG = 0x0022u,
+ T_PULONG = 0x0122u,
+ T_PFULONG = 0x0222u,
+ T_PHULONG = 0x0322u,
+ T_32PULONG = 0x0422u,
+ T_32PFULONG = 0x0522u,
+ T_64PULONG = 0x0622u,
+
+ T_INT4 = 0x0074u,
+ T_PINT4 = 0x0174u,
+ T_PFINT4 = 0x0274u,
+ T_PHINT4 = 0x0374u,
+ T_32PINT4 = 0x0474u,
+ T_32PFINT4 = 0x0574u,
+ T_64PINT4 = 0x0674u,
+
+ T_UINT4 = 0x0075u,
+ T_PUINT4 = 0x0175u,
+ T_PFUINT4 = 0x0275u,
+ T_PHUINT4 = 0x0375u,
+ T_32PUINT4 = 0x0475u,
+ T_32PFUINT4 = 0x0575u,
+ T_64PUINT4 = 0x0675u,
+
+ T_QUAD = 0x0013u,
+ T_PQUAD = 0x0113u,
+ T_PFQUAD = 0x0213u,
+ T_PHQUAD = 0x0313u,
+ T_32PQUAD = 0x0413u,
+ T_32PFQUAD = 0x0513u,
+ T_64PQUAD = 0x0613u,
+
+ T_UQUAD = 0x0023u,
+ T_PUQUAD = 0x0123u,
+ T_PFUQUAD = 0x0223u,
+ T_PHUQUAD = 0x0323u,
+ T_32PUQUAD = 0x0423u,
+ T_32PFUQUAD = 0x0523u,
+ T_64PUQUAD = 0x0623u,
+
+ T_INT8 = 0x0076u,
+ T_PINT8 = 0x0176u,
+ T_PFINT8 = 0x0276u,
+ T_PHINT8 = 0x0376u,
+ T_32PINT8 = 0x0476u,
+ T_32PFINT8 = 0x0576u,
+ T_64PINT8 = 0x0676u,
+
+ T_UINT8 = 0x0077u,
+ T_PUINT8 = 0x0177u,
+ T_PFUINT8 = 0x0277u,
+ T_PHUINT8 = 0x0377u,
+ T_32PUINT8 = 0x0477u,
+ T_32PFUINT8 = 0x0577u,
+ T_64PUINT8 = 0x0677u,
+
+ T_OCT = 0x0014u,
+ T_POCT = 0x0114u,
+ T_PFOCT = 0x0214u,
+ T_PHOCT = 0x0314u,
+ T_32POCT = 0x0414u,
+ T_32PFOCT = 0x0514u,
+ T_64POCT = 0x0614u,
+
+ T_UOCT = 0x0024u,
+ T_PUOCT = 0x0124u,
+ T_PFUOCT = 0x0224u,
+ T_PHUOCT = 0x0324u,
+ T_32PUOCT = 0x0424u,
+ T_32PFUOCT = 0x0524u,
+ T_64PUOCT = 0x0624u,
+
+ T_INT16 = 0x0078u,
+ T_PINT16 = 0x0178u,
+ T_PFINT16 = 0x0278u,
+ T_PHINT16 = 0x0378u,
+ T_32PINT16 = 0x0478u,
+ T_32PFINT16 = 0x0578u,
+ T_64PINT16 = 0x0678u,
+
+ T_UINT16 = 0x0079u,
+ T_PUINT16 = 0x0179u,
+ T_PFUINT16 = 0x0279u,
+ T_PHUINT16 = 0x0379u,
+ T_32PUINT16 = 0x0479u,
+ T_32PFUINT16 = 0x0579u,
+ T_64PUINT16 = 0x0679u,
+
+ T_REAL32 = 0x0040u,
+ T_PREAL32 = 0x0140u,
+ T_PFREAL32 = 0x0240u,
+ T_PHREAL32 = 0x0340u,
+ T_32PREAL32 = 0x0440u,
+ T_32PFREAL32 = 0x0540u,
+ T_64PREAL32 = 0x0640u,
+
+ T_REAL48 = 0x0044u,
+ T_PREAL48 = 0x0144u,
+ T_PFREAL48 = 0x0244u,
+ T_PHREAL48 = 0x0344u,
+ T_32PREAL48 = 0x0444u,
+ T_32PFREAL48 = 0x0544u,
+ T_64PREAL48 = 0x0644u,
+
+ T_REAL64 = 0x0041u,
+ T_PREAL64 = 0x0141u,
+ T_PFREAL64 = 0x0241u,
+ T_PHREAL64 = 0x0341u,
+ T_32PREAL64 = 0x0441u,
+ T_32PFREAL64 = 0x0541u,
+ T_64PREAL64 = 0x0641u,
+
+ T_REAL80 = 0x0042u,
+ T_PREAL80 = 0x0142u,
+ T_PFREAL80 = 0x0242u,
+ T_PHREAL80 = 0x0342u,
+ T_32PREAL80 = 0x0442u,
+ T_32PFREAL80 = 0x0542u,
+ T_64PREAL80 = 0x0642u,
+
+ T_REAL128 = 0x0043u,
+ T_PREAL128 = 0x0143u,
+ T_PFREAL128 = 0x0243u,
+ T_PHREAL128 = 0x0343u,
+ T_32PREAL128 = 0x0443u,
+ T_32PFREAL128 = 0x0543u,
+ T_64PREAL128 = 0x0643u,
+
+ T_CPLX32 = 0x0050u,
+ T_PCPLX32 = 0x0150u,
+ T_PFCPLX32 = 0x0250u,
+ T_PHCPLX32 = 0x0350u,
+ T_32PCPLX32 = 0x0450u,
+ T_32PFCPLX32 = 0x0550u,
+ T_64PCPLX32 = 0x0650u,
+
+ T_CPLX64 = 0x0051u,
+ T_PCPLX64 = 0x0151u,
+ T_PFCPLX64 = 0x0251u,
+ T_PHCPLX64 = 0x0351u,
+ T_32PCPLX64 = 0x0451u,
+ T_32PFCPLX64 = 0x0551u,
+ T_64PCPLX64 = 0x0651u,
+
+ T_CPLX80 = 0x0052u,
+ T_PCPLX80 = 0x0152u,
+ T_PFCPLX80 = 0x0252u,
+ T_PHCPLX80 = 0x0352u,
+ T_32PCPLX80 = 0x0452u,
+ T_32PFCPLX80 = 0x0552u,
+ T_64PCPLX80 = 0x0652u,
+
+ T_CPLX128 = 0x0053u,
+ T_PCPLX128 = 0x0153u,
+ T_PFCPLX128 = 0x0253u,
+ T_PHCPLX128 = 0x0353u,
+ T_32PCPLX128 = 0x0453u,
+ T_32PFCPLX128 = 0x0553u,
+ T_64PCPLX128 = 0x0653u,
+
+ T_BOOL08 = 0x0030u,
+ T_PBOOL08 = 0x0130u,
+ T_PFBOOL08 = 0x0230u,
+ T_PHBOOL08 = 0x0330u,
+ T_32PBOOL08 = 0x0430u,
+ T_32PFBOOL08 = 0x0530u,
+ T_64PBOOL08 = 0x0630u,
+
+ T_BOOL16 = 0x0031u,
+ T_PBOOL16 = 0x0131u,
+ T_PFBOOL16 = 0x0231u,
+ T_PHBOOL16 = 0x0331u,
+ T_32PBOOL16 = 0x0431u,
+ T_32PFBOOL16 = 0x0531u,
+ T_64PBOOL16 = 0x0631u,
+
+ T_BOOL32 = 0x0032u,
+ T_PBOOL32 = 0x0132u,
+ T_PFBOOL32 = 0x0232u,
+ T_PHBOOL32 = 0x0332u,
+ T_32PBOOL32 = 0x0432u,
+ T_32PFBOOL32 = 0x0532u,
+ T_64PBOOL32 = 0x0632u,
+
+ T_BOOL64 = 0x0033u,
+ T_PBOOL64 = 0x0133u,
+ T_PFBOOL64 = 0x0233u,
+ T_PHBOOL64 = 0x0333u,
+ T_32PBOOL64 = 0x0433u,
+ T_32PFBOOL64 = 0x0533u,
+ T_64PBOOL64 = 0x0633u,
+
+ T_NCVPTR = 0x01F0u,
+ T_FCVPTR = 0x02F0u,
+ T_HCVPTR = 0x03F0u,
+ T_32NCVPTR = 0x04F0u,
+ T_32FCVPTR = 0x05F0u,
+ T_64NCVPTR = 0x06F0u
+ };
+
+ // https://github.com/microsoft/microsoft-pdb/blob/master/include/cvconst.h#L31
+ enum class CallingConvention : uint8_t
+ {
+ NEAR_C = 0x00u, // near right to left pushu, caller pops stack
+ FAR_C = 0x01u, // far right to left pushu, caller pops stack
+ NEAR_PASCAL = 0x02u,// near left to right pushu, callee pops stack
+ FAR_PASCAL = 0x03u, // far left to right pushu, callee pops stack
+ NEAR_FAST = 0x04u, // near left to right push with regsu, callee pops stack
+ FAR_FAST = 0x05u, // far left to right push with regsu, callee pops stack
+ SKIPPED = 0x06u, // skipped (unused) call index
+ NEAR_STD = 0x07u, // near standard call
+ FAR_STD = 0x08u, // far standard call
+ NEAR_SYS = 0x09u, // near sys call
+ FAR_SYS = 0x0au, // far sys call
+ THISCALL = 0x0bu, // this call (this passed in register)
+ MIPSCALL = 0x0cu, // Mips call
+ GENERIC = 0x0du, // Generic call sequence
+ ALPHACALL = 0x0eu, // Alpha call
+ PPCCALL = 0x0fu, // PPC call
+ SHCALL = 0x10u, // Hitachi SuperH call
+ ARMCALL = 0x11u, // ARM call
+ AM33CALL = 0x12u, // AM33 call
+ TRICALL = 0x13u, // TriCore Call
+ SH5CALL = 0x14u, // Hitachi SuperH-5 call
+ M32RCALL = 0x15u, // M32R Call
+ CLRCALL = 0x16u, // clr call
+ INLINE = 0x17u, // Marker for routines always inlined and thus lacking a convention
+ NEAR_VECTOR = 0x18u,// near left to right push with regsu, callee pops stack
+ RESERVED = 0x19u // first unused call enumeration
+
+ // Do NOT add any more machine specific conventions. This is to be used for
+ // calling conventions in the source only (e.g. __cdeclu, __stdcall).
+ };
+
+ // https://github.com/microsoft/microsoft-pdb/blob/master/include/cvinfo.h#L1049
+ enum class MethodProperty : uint8_t
+ {
+ Vanilla = 0x00u,
+ Virtual = 0x01u,
+ Static = 0x02u,
+ Friend = 0x03u,
+ Intro = 0x04u,
+ PureVirt = 0x05u,
+ PureIntro = 0x06u
+ };
+
+ // https://github.com/microsoft/microsoft-pdb/blob/master/include/cvinfo.h#L1120
+ struct TypeProperty
+ {
+ uint16_t packed : 1; // true if structure is packed
+ uint16_t ctor : 1; // true if constructors or destructors present
+ uint16_t ovlops : 1; // true if overloaded operators present
+ uint16_t isnested : 1; // true if this is a nested class
+ uint16_t cnested : 1; // true if this class contains nested types
+ uint16_t opassign : 1; // true if overloaded assignment (=)
+ uint16_t opcast : 1; // true if casting methods
+ uint16_t fwdref : 1; // true if forward reference (incomplete defn)
+ uint16_t scoped : 1; // scoped definition
+ uint16_t hasuniquename : 1; // true if there is a decorated name following the regular name
+ uint16_t sealed : 1; // true if class cannot be used as a base class
+ uint16_t hfa : 2; // CV_HFA_e
+ uint16_t intrinsic : 1; // true if class is an intrinsic type (e.g. __m128d)
+ uint16_t mocom : 2; // CV_MOCOM_UDe
+ };
+
+ // https://github.com/microsoft/microsoft-pdb/blob/master/include/cvinfo.h#L1142
+ struct MemberAttributes
+ {
+ uint16_t access : 2; // access protection CV_access_t
+ uint16_t mprop : 3; // method properties CV_methodprop_t
+ uint16_t pseudo : 1; // compiler generated fcn and does not exist
+ uint16_t noinherit : 1; // true if class cannot be inherited
+ uint16_t noconstruct : 1; // true if class cannot be constructed
+ uint16_t compgenx : 1; // compiler generated fcn and does exist
+ uint16_t sealed : 1; // true if method cannot be overridden
+ uint16_t unused : 6; // unused
+ };
+
+ // https://github.com/microsoft/microsoft-pdb/blob/master/include/cvinfo.h#L1156
+ struct FunctionAttributes
+ {
+ uint8_t cxxreturnudt : 1; // true if C++ style ReturnUDT
+ uint8_t ctor : 1; // true if func is an instance constructor
+ uint8_t ctorvbase : 1; // true if func is an instance constructor of a class with virtual bases
+ uint8_t unused : 5; // unused
+ };
+
+ struct RecordHeader
+ {
+ uint16_t size; // record length, not including this 2-byte field
+ TypeRecordKind kind; // record kind
+ };
+
+ struct LeafEasy
+ {
+ TypeRecordKind kind; // record kind
+ };
+
+ struct FieldList
+ {
+ TypeRecordKind kind; // record kind
+ union Data
+ {
+#pragma pack(push, 1)
+ // https://github.com/microsoft/microsoft-pdb/blob/master/include/cvinfo.h#L2499
+ struct
+ {
+ MemberAttributes attributes; // method attribute
+ uint32_t index; // type index of base class
+ union
+ {
+ PDB_FLEXIBLE_ARRAY_MEMBER(char, offset); // variable length offset of base within class
+ LeafEasy lfEasy;
+ };
+ }LF_BCLASS;
+
+ // https://github.com/microsoft/microsoft-pdb/blob/master/include/cvinfo.h#L2521
+ struct
+ {
+ MemberAttributes attributes; // attribute
+ uint32_t index; // type index of direct virtual base class
+ uint32_t vbpIndex; // type index of virtual base pointer
+ PDB_FLEXIBLE_ARRAY_MEMBER(char, vbpOffset); // virtual base pointer offset from address point
+ } LF_VBCLASS, LF_IVBCLASS;
+
+ // https://github.com/microsoft/microsoft-pdb/blob/master/include/cvinfo.h#L2483
+ // index leaf - contains type index of another leaf
+ // a major use of this leaf is to allow the compilers to emit a
+ // long complex list (LF_FIELD) in smaller pieces.
+ struct
+ {
+ uint16_t pad0; // internal padding, must be 0
+ uint32_t type; // type index of referenced leaf
+ } LF_INDEX;
+
+ // https://github.com/microsoft/microsoft-pdb/blob/master/include/cvinfo.h#L2615
+ struct
+ {
+ uint16_t pad0; // internal padding, must be 0.
+ uint32_t type; // type index of pointer
+ }LF_VFUNCTAB;
+
+ // https://github.com/microsoft/microsoft-pdb/blob/master/include/cvinfo.h#L2683
+ struct
+ {
+ MemberAttributes attributes;
+ union
+ {
+ PDB_FLEXIBLE_ARRAY_MEMBER(char, value);
+ LeafEasy lfEasy;
+ };
+ } LF_ENUMERATE;
+
+ // https://github.com/microsoft/microsoft-pdb/blob/master/include/cvinfo.h#L2693
+ struct
+ {
+ uint16_t pad0; // internal padding, must be 0
+ uint32_t index; // index of nested type definition
+ PDB_FLEXIBLE_ARRAY_MEMBER(char, name);
+ }LF_NESTTYPE;
+
+ // https://github.com/microsoft/microsoft-pdb/blob/master/include/cvinfo.h#L2650
+ struct
+ {
+ uint16_t count; // number of occurrences of function
+ uint32_t mList; // index to LF_METHODLIST record
+ PDB_FLEXIBLE_ARRAY_MEMBER(char, name);
+ }LF_METHOD;
+
+ // https://github.com/microsoft/microsoft-pdb/blob/master/include/cvinfo.h#L2671
+ struct
+ {
+ MemberAttributes attributes; // method attribute
+ uint32_t index; // index to type record for procedure
+ PDB_FLEXIBLE_ARRAY_MEMBER(uint32_t, vbaseoff); // offset in vfunctable if
+ }LF_ONEMETHOD;
+
+ // https://github.com/microsoft/microsoft-pdb/blob/master/include/cvinfo.h#L2580
+ struct
+ {
+ MemberAttributes attributes;
+ uint32_t index; // type index of referenced leaf
+ union
+ {
+ PDB_FLEXIBLE_ARRAY_MEMBER(char, offset);
+ LeafEasy lfEasy;
+ };
+ } LF_MEMBER;
+
+ // https://github.com/microsoft/microsoft-pdb/blob/master/include/cvinfo.h#L2592
+ struct
+ {
+ MemberAttributes attributes;
+ uint32_t index; // index of type record for field
+ PDB_FLEXIBLE_ARRAY_MEMBER(char, name);
+ }LF_STMEMBER;
+#pragma pack(pop)
+ } data;
+ };
+
+ // https://github.com/microsoft/microsoft-pdb/blob/master/include/cvinfo.h#L2131
+ struct MethodListEntry
+ {
+ MemberAttributes attributes; // method attribute
+ uint16_t pad0; // internal padding, must be 0
+ uint32_t index; // index to type record for procedure
+ PDB_FLEXIBLE_ARRAY_MEMBER(uint32_t, vbaseoff); // offset in vfunctable if virtual, empty otherwise.
+ };
+
+ // all CodeView records are stored as a header, followed by variable-length data.
+ // internal Record structs such as S_PUB32, S_GDATA32, etc. correspond to the data layout of a CodeView record of that kind.
+ struct Record
+ {
+ RecordHeader header;
+ union Data
+ {
+#pragma pack(push, 1)
+ // https://github.com/microsoft/microsoft-pdb/blob/master/include/cvinfo.h#L2144
+ struct
+ {
+ // This is actually a list of the MethodListEntry type above, but it has flexible
+ // size, so you need to manually iterate.
+ PDB_FLEXIBLE_ARRAY_MEMBER(char, mList);
+ } LF_METHODLIST;
+
+ // https://github.com/microsoft/microsoft-pdb/blob/master/include/cvinfo.h#L1801
+ struct
+ {
+ uint32_t rvtype; // type index of return value
+ uint32_t classtype; // type index of containing class
+ uint32_t thistype; // type index of this pointer (model specific)
+ uint8_t calltype; // calling convention (call_t)
+ FunctionAttributes funcattr; // attributes
+ uint16_t parmcount; // number of parameters
+ uint32_t arglist; // type index of argument list
+ int32_t thisadjust; // this adjuster (long because pad required anyway)
+ } LF_MFUNCTION;
+
+ // https://github.com/microsoft/microsoft-pdb/blob/master/include/cvinfo.h#L1460
+ struct
+ {
+ uint32_t type; // modified type
+
+ // https://github.com/microsoft/microsoft-pdb/blob/master/include/cvinfo.h#L1090
+ struct
+ {
+ uint16_t MOD_const : 1;
+ uint16_t MOD_volatile : 1;
+ uint16_t MOD_unaligned : 1;
+ uint16_t MOD_unused : 13;
+ } attr; // modifier attribute modifier_t
+ } LF_MODIFIER;
+
+ // https://github.com/microsoft/microsoft-pdb/blob/master/include/cvinfo.h#L1508
+ struct
+ {
+ uint32_t utype; // type index of the underlying type
+ struct PointerAttributes
+ {
+ uint32_t ptrtype : 5; // ordinal specifying pointer type (CV_ptrtype_e)
+ uint32_t ptrmode : 3; // ordinal specifying pointer mode (CV_ptrmode_e)
+ uint32_t isflat32 : 1; // TRUE if 0:32 pointer
+ uint32_t isvolatile : 1; // TRUE if volatile pointer
+ uint32_t isconst : 1; // TRUE if const pointer
+ uint32_t isunaligned : 1; // TRUE if unaligned pointer
+ uint32_t isrestrict : 1; // TRUE if restricted pointer (allow agressive opts)
+ uint32_t size : 6; // size of pointer (in bytes)
+ uint32_t ismocom : 1; // TRUE if it is a MoCOM pointer (^ or %)
+ uint32_t islref : 1; // TRUE if it is this pointer of member function with & ref-qualifier
+ uint32_t isrref : 1; // TRUE if it is this pointer of member function with && ref-qualifier
+ uint32_t unused : 10; // pad out to 32-bits for following cv_typ_t's
+ } attr;
+
+ union
+ {
+ struct
+ {
+ uint32_t pmclass; // index of containing class for pointer to member
+ uint16_t pmenum; // enumeration specifying pm format (CV_pmtype_e)
+ } pm;
+
+ uint16_t bseg; // base segment if PTR_BASE_SEG
+ PDB_FLEXIBLE_ARRAY_MEMBER(uint8_t, Sym); // copy of base symbol record (including length)
+
+ struct
+ {
+ uint32_t index; // type index if CV_PTR_BASE_TYPE
+ PDB_FLEXIBLE_ARRAY_MEMBER(char, name); // name of base type
+ } btype;
+ } pbase;
+ } LF_POINTER;
+
+ // https://github.com/microsoft/microsoft-pdb/blob/master/include/cvinfo.h#L1775
+ struct
+ {
+ uint32_t rvtype; // type index of return value
+ CallingConvention calltype; // calling convention (CV_call_t)
+ FunctionAttributes funcattr; // attributes
+ uint16_t parmcount; // number of parameters
+ uint32_t arglist; // type index of argument list
+ } LF_PROCEDURE;
+
+ // https://github.com/microsoft/microsoft-pdb/blob/master/include/cvinfo.h#L2043
+ struct
+ {
+ uint32_t count; // number of arguments
+ PDB_FLEXIBLE_ARRAY_MEMBER(uint32_t, arg);
+ } LF_ARGLIST;
+
+ // https://github.com/microsoft/microsoft-pdb/blob/master/include/cvinfo.h#L2164
+ struct
+ {
+ uint32_t type;
+ uint8_t length;
+ uint8_t position;
+ PDB_FLEXIBLE_ARRAY_MEMBER(char, data);
+ } LF_BITFIELD;
+
+ struct
+ {
+ uint32_t elemtype; // type index of element type
+ uint32_t idxtype; // type index of indexing type
+ PDB_FLEXIBLE_ARRAY_MEMBER(char, data); // variable length data specifying size in bytes and name
+ } LF_ARRAY;
+
+ // https://github.com/microsoft/microsoft-pdb/blob/master/include/cvinfo.h#L1631
+ struct
+ {
+ uint16_t count; // count of number of elements in class
+ TypeProperty property; // property attribute field
+ uint32_t field; // type index of LF_FIELD descriptor list
+ uint32_t derived; // type index of derived from list if not zero
+ uint32_t vshape; // type index of vshape table for this class
+ union
+ {
+ PDB_FLEXIBLE_ARRAY_MEMBER(char, data);
+ LeafEasy lfEasy;
+ };
+ } LF_CLASS;
+
+ struct
+ {
+ uint16_t count; // count of number of elements in class
+ uint32_t property; // property attribute field
+ uint32_t field; // type index of LF_FIELD descriptor list
+ uint32_t derived; // type index of derived from list if not zero
+ uint32_t vshape; // type index of vshape table for this class
+ union
+ {
+ PDB_FLEXIBLE_ARRAY_MEMBER(char, data);
+ LeafEasy lfEasy;
+ };
+ } LF_CLASS2;
+
+ // https://github.com/microsoft/microsoft-pdb/blob/master/include/cvinfo.h#L1647
+ struct
+ {
+ uint16_t count; // count of number of elements in class
+ TypeProperty property; // property attribute field
+ uint32_t field; // type index of LF_FIELD descriptor list
+ PDB_FLEXIBLE_ARRAY_MEMBER(char, data);
+ } LF_UNION;
+
+ // https://github.com/microsoft/microsoft-pdb/blob/master/include/cvinfo.h#L1752
+ struct
+ {
+ uint16_t count; // count of number of elements in class
+ TypeProperty property; // property attribute field
+ uint32_t utype; // underlying type of the enum
+ uint32_t field; // type index of LF_FIELD descriptor list
+ PDB_FLEXIBLE_ARRAY_MEMBER(char, name);
+ } LF_ENUM;
+
+ // https://github.com/microsoft/microsoft-pdb/blob/master/include/cvinfo.h#L2112
+ struct
+ {
+ FieldList list;
+ } LF_FIELD;
+#pragma pack(pop)
+ } data;
+ };
+ }
+ }
+}
diff --git a/thirdparty/raw_pdb/src/PDB_Types.cpp b/thirdparty/raw_pdb/src/PDB_Types.cpp
new file mode 100644
index 000000000..66c5cea48
--- /dev/null
+++ b/thirdparty/raw_pdb/src/PDB_Types.cpp
@@ -0,0 +1,12 @@
+// Copyright 2011-2022, Molecular Matters GmbH <[email protected]>
+// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
+
+#include "PDB_PCH.h"
+#include "PDB_Types.h"
+
+
+// https://github.com/Microsoft/microsoft-pdb/blob/master/PDB/msf/msf.cpp#L962
+const char PDB::SuperBlock::MAGIC[30u] = "Microsoft C/C++ MSF 7.00\r\n\x1a\x44\x53";
+
+const uint32_t PDB::HashTableHeader::Signature = 0xffffffffu;
+const uint32_t PDB::HashTableHeader::Version = 0xeffe0000u + 19990810u;
diff --git a/thirdparty/raw_pdb/src/PDB_Types.h b/thirdparty/raw_pdb/src/PDB_Types.h
new file mode 100644
index 000000000..39945fcad
--- /dev/null
+++ b/thirdparty/raw_pdb/src/PDB_Types.h
@@ -0,0 +1,167 @@
+// Copyright 2011-2022, Molecular Matters GmbH <[email protected]>
+// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
+
+#pragma once
+
+#include "Foundation/PDB_Macros.h"
+
+
+namespace PDB
+{
+ // emulating std::byte from C++17 to make the intention clear that we're dealing with untyped data in certain cases, without actually requiring C++17
+ enum class Byte : unsigned char {};
+
+ // PDB files have the notion of "nil" pages, denoted by a special size
+ // https://github.com/microsoft/microsoft-pdb/blob/master/PDB/msf/msf.cpp#L177
+ const uint32_t NilPageSize = 0xffffffffu;
+
+ // PDB files have the notion of a "nil" stream index
+ // https://github.com/microsoft/microsoft-pdb/blob/master/PDB/include/msf.h#L45
+ const uint16_t NilStreamIndex = 0xffffu;
+
+ // this matches the definition in guiddef.h, but we don't want to pull that in
+ struct GUID
+ {
+ uint32_t Data1;
+ uint16_t Data2;
+ uint16_t Data3;
+ uint8_t Data4[8];
+ };
+
+ static_assert(sizeof(GUID) == 16u, "Size mismatch.");
+
+ // this matches the definition in winnt.h, but we don't want to pull that in
+ struct IMAGE_SECTION_HEADER
+ {
+ uint8_t Name[8];
+ union
+ {
+ uint32_t PhysicalAddress;
+ uint32_t VirtualSize;
+ } Misc;
+ uint32_t VirtualAddress;
+ uint32_t SizeOfRawData;
+ uint32_t PointerToRawData;
+ uint32_t PointerToRelocations;
+ uint32_t PointerToLinenumbers;
+ uint16_t NumberOfRelocations;
+ uint16_t NumberOfLinenumbers;
+ uint32_t Characteristics;
+ };
+
+ static_assert(sizeof(IMAGE_SECTION_HEADER) == 40u, "Size mismatch.");
+
+ // https://llvm.org/docs/PDB/MsfFile.html#msf-superblock
+ struct PDB_NO_DISCARD SuperBlock
+ {
+ static const char MAGIC[30u];
+
+ char fileMagic[30u];
+ char padding[2u];
+ uint32_t blockSize;
+ uint32_t freeBlockMapIndex; // index of the free block map
+ uint32_t blockCount; // number of blocks in the file
+ uint32_t directorySize; // size of the stream directory in bytes
+ uint32_t unknown;
+ PDB_FLEXIBLE_ARRAY_MEMBER(uint32_t, directoryBlockIndices); // indices of the blocks that make up the directory indices
+ };
+
+ // https://llvm.org/docs/PDB/PdbStream.html#stream-header
+ struct Header
+ {
+ enum class PDB_NO_DISCARD Version : uint32_t
+ {
+ VC2 = 19941610u,
+ VC4 = 19950623u,
+ VC41 = 19950814u,
+ VC50 = 19960307u,
+ VC98 = 19970604u,
+ VC70Dep = 19990604u,
+ VC70 = 20000404u,
+ VC80 = 20030901u,
+ VC110 = 20091201u,
+ VC140 = 20140508u
+ };
+
+ Version version;
+ uint32_t signature;
+ uint32_t age;
+ GUID guid;
+ };
+
+ // https://llvm.org/docs/PDB/PdbStream.html
+ struct NamedStreamMap
+ {
+ uint32_t length;
+ PDB_FLEXIBLE_ARRAY_MEMBER(char, stringTable);
+
+ struct HashTableEntry
+ {
+ uint32_t stringTableOffset;
+ uint32_t streamIndex;
+ };
+ };
+
+ // https://llvm.org/docs/PDB/HashTable.html
+ struct SerializedHashTable
+ {
+ struct Header
+ {
+ uint32_t size;
+ uint32_t capacity;
+ };
+
+ struct BitVector
+ {
+ uint32_t wordCount;
+ PDB_FLEXIBLE_ARRAY_MEMBER(uint32_t, words);
+ };
+ };
+
+ // https://llvm.org/docs/PDB/PdbStream.html#pdb-feature-codes
+ enum class PDB_NO_DISCARD FeatureCode : uint32_t
+ {
+ VC110 = 20091201,
+ VC140 = 20140508,
+
+ // https://github.com/microsoft/microsoft-pdb/blob/master/PDB/include/pdbcommon.h#L23
+ NoTypeMerge = 0x4D544F4E, // "NOTM"
+ MinimalDebugInfo = 0x494E494D // "MINI", i.e. executable was linked with /DEBUG:FASTLINK
+ };
+
+ // header of the public stream, based on PSGSIHDR defined here:
+ // https://github.com/Microsoft/microsoft-pdb/blob/master/PDB/dbi/gsi.h#L240
+ struct PublicStreamHeader
+ {
+ uint32_t symHash;
+ uint32_t addrMap;
+ uint32_t thunkCount;
+ uint32_t sizeOfThunk;
+ uint16_t isectThunkTable;
+ uint16_t padding;
+ uint32_t offsetThunkTable;
+ uint16_t sectionCount;
+ uint16_t padding2;
+ };
+
+ // header of the hash tables used by the public and global symbol stream, based on GSIHashHdr defined here:
+ // https://github.com/Microsoft/microsoft-pdb/blob/master/PDB/dbi/gsi.h#L62
+ struct HashTableHeader
+ {
+ static const uint32_t Signature;
+ static const uint32_t Version;
+
+ uint32_t signature;
+ uint32_t version;
+ uint32_t size;
+ uint32_t bucketCount;
+ };
+
+ // hash record, based on HRFile defined here:
+ // https://github.com/Microsoft/microsoft-pdb/blob/master/PDB/dbi/gsi.h#L8
+ struct HashRecord
+ {
+ uint32_t offset; // offset into the symbol record stream
+ uint32_t cref;
+ };
+}
diff --git a/thirdparty/raw_pdb/src/PDB_Util.h b/thirdparty/raw_pdb/src/PDB_Util.h
new file mode 100644
index 000000000..c72265996
--- /dev/null
+++ b/thirdparty/raw_pdb/src/PDB_Util.h
@@ -0,0 +1,56 @@
+// Copyright 2011-2022, Molecular Matters GmbH <[email protected]>
+// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
+
+#pragma once
+
+#include "Foundation/PDB_Macros.h"
+
+
+namespace PDB
+{
+ // Converts a block index into a file offset, based on the block size of the PDB file
+ PDB_NO_DISCARD inline size_t ConvertBlockIndexToFileOffset(uint32_t blockIndex, uint32_t blockSize) PDB_NO_EXCEPT
+ {
+ // cast to size_t to avoid potential overflow in 64-bit
+ return static_cast<size_t>(blockIndex) * static_cast<size_t>(blockSize);
+ }
+
+ // Calculates how many blocks are needed for a certain number of bytes
+ PDB_NO_DISCARD inline uint32_t ConvertSizeToBlockCount(uint32_t sizeInBytes, uint32_t blockSize) PDB_NO_EXCEPT
+ {
+ // integer ceil to account for non-full blocks
+ return static_cast<uint32_t>((static_cast<size_t>(sizeInBytes) + blockSize - 1u) / blockSize);
+ };
+
+ // Returns the actual size of the data associated with a CodeView record, not including the size of the header
+ template <typename T>
+ PDB_NO_DISCARD inline uint32_t GetCodeViewRecordSize(const T* record) PDB_NO_EXCEPT
+ {
+ // the stored size includes the size of the 'kind' field, but not the size of the 'size' field itself
+ return record->header.size - sizeof(uint16_t);
+ }
+
+ template <typename Header, typename T>
+ PDB_NO_DISCARD inline size_t GetNameLength(const Header& header, const T& record) PDB_NO_EXCEPT
+ {
+ // we can estimate the length of the string from the size of the record
+ const size_t estimatedLength = header.size - sizeof(uint16_t) - sizeof(T);
+ if (estimatedLength == 0u)
+ {
+ return estimatedLength;
+ }
+
+ // we still need to account for padding after the string to find the real length
+ size_t nullTerminatorCount = 0u;
+ for (/* nothing */; nullTerminatorCount < estimatedLength; ++nullTerminatorCount)
+ {
+ if (record.name[estimatedLength - nullTerminatorCount - 1u] != '\0')
+ {
+ break;
+ }
+ }
+
+ const size_t length = estimatedLength - nullTerminatorCount;
+ return length;
+ }
+}
diff --git a/thirdparty/raw_pdb/xmake.lua b/thirdparty/raw_pdb/xmake.lua
new file mode 100644
index 000000000..d7dd3f16a
--- /dev/null
+++ b/thirdparty/raw_pdb/xmake.lua
@@ -0,0 +1,17 @@
+-- raw_pdb: A C++11 library for reading Microsoft PDB files
+-- https://github.com/MolecularMatters/raw_pdb
+
+target("raw_pdb")
+ set_kind("static")
+ set_group("thirdparty")
+
+ add_files("src/PDB*.cpp")
+ remove_files("src/PDB_PCH.cpp")
+
+ add_headerfiles("src/**.h")
+ add_includedirs("src", {public=true})
+
+ if is_plat("windows") then
+ add_cxxflags("/wd4324", {force=true}) -- structure was padded due to alignment specifier
+ add_cxxflags("/wd4702", {force=true}) -- unreachable code
+ end
diff --git a/thirdparty/tourist/analysis/include/analysis/analyzer.h b/thirdparty/tourist/analysis/include/analysis/analyzer.h
new file mode 100644
index 000000000..cf2f6fc31
--- /dev/null
+++ b/thirdparty/tourist/analysis/include/analysis/analyzer.h
@@ -0,0 +1,54 @@
+#pragma once
+
+#include "outline.h"
+
+//------------------------------------------------------------------------------
+class Analyzer
+{
+public:
+ virtual void subscribe(Vector<struct Subscription>& subs) = 0;
+};
+
+
+
+//------------------------------------------------------------------------------
+struct Subscription
+{
+ template <typename T, typename U> Subscription(T* t, void (T::*sink)(const U&));
+
+ using SinkType = void (Analyzer::*)(const Outline&);
+
+ Subscription() = default;
+ Subscription(const Subscription&) = delete;
+ Subscription(Subscription&& rhs) { move(std::move(rhs)); }
+ void operator = (Subscription&& rhs) { move(std::move(rhs)); }
+ void operator = (const Subscription&) = delete;
+ void move(Subscription&& rhs);
+ UniquePtr<Outline> outline;
+ void* analyzer;
+ SinkType sink;
+};
+
+//------------------------------------------------------------------------------
+template <typename T, typename U>
+Subscription::Subscription(T* a, void (T::*s)(const U&))
+: outline(UniquePtr<Outline>(new U()))
+, analyzer(a)
+{
+ // analyzers can't have vtables
+ static_assert(sizeof(s) == sizeof(Subscription::SinkType), "we're cheating");
+#if defined(_MSC_VER)
+ // Itanium ABI pointer-to-member-functions are always 2*sizeof(void*); the size
+ // check only holds under MSVC's single-inheritance PMF representation.
+ static_assert(sizeof(s) == sizeof(void*), "we're cheating");
+#endif
+ std::memcpy(&sink, &s, sizeof(sink));
+}
+
+//------------------------------------------------------------------------------
+inline void Subscription::move(Subscription&& rhs)
+{
+ std::swap(outline, rhs.outline);
+ std::swap(analyzer, rhs.analyzer);
+ std::swap(sink, rhs.sink);
+}
diff --git a/thirdparty/tourist/analysis/include/analysis/array.h b/thirdparty/tourist/analysis/include/analysis/array.h
new file mode 100644
index 000000000..5531d131d
--- /dev/null
+++ b/thirdparty/tourist/analysis/include/analysis/array.h
@@ -0,0 +1,65 @@
+#pragma once
+
+#include <foundation/malloc.h>
+
+//------------------------------------------------------------------------------
+template <typename T> struct Array;
+
+template <typename T>
+struct Array<T[]>
+ : public NoCopy
+{
+ Array(const Aux* aux, uint8 type_info)
+ : element_size(1ull << (type_info & TYPE_INFO_SIZE_MASK))
+ {
+
+ if ((owned = aux->partial) == 0)
+ {
+ ptr = uintptr(aux->data.get());
+ count = aux->size / element_size;
+ return;
+ }
+
+ uint32 size = 0;
+ for (const Aux* cursor = aux;; ++cursor)
+ {
+ size += cursor->size;
+ if (cursor->partial == 0)
+ break;
+ }
+
+ uint8* data = tt_malloc(size);
+ for (uint8* cursor = data;; ++aux)
+ {
+ std::memcpy(cursor, aux->data.get(), aux->size);
+ cursor += aux->size;
+ if (aux->partial == 0)
+ break;
+ }
+
+ ptr = uintptr(data);
+ count = size / element_size;
+ }
+
+ Array() = default;
+ ~Array() { if (owned) tt_free((void*)ptr); }
+ Array(Array<T[]>&& rhs) { move(std::move(rhs)); }
+ void operator = (Array<T[]>&& rhs) { move(std::move(rhs)); }
+ const T* get() const { return (T*)ptr; }
+ uint32 get_count() const { return count; }
+ uint32 get_element_size() const { return element_size; }
+ uint32 get_size() const { return uint32(count * element_size); }
+
+private:
+ friend class FieldStr;
+ void move(Array<T[]>&& rhs) {
+ std::swap(rhs.ptr, ptr);
+ std::swap(rhs.count, count);
+ std::swap(rhs.element_size, element_size);
+ std::swap(rhs.owned, owned);
+ }
+ uintptr ptr = 0;
+ uint32 count = 0;
+ uint16 element_size = 0;
+ uint8 owned = 0;
+};
diff --git a/thirdparty/tourist/analysis/include/analysis/dispatcher.h b/thirdparty/tourist/analysis/include/analysis/dispatcher.h
new file mode 100644
index 000000000..b30d3cde7
--- /dev/null
+++ b/thirdparty/tourist/analysis/include/analysis/dispatcher.h
@@ -0,0 +1,22 @@
+#pragma once
+
+#include <foundation/types.h>
+
+#include "analyzer.h"
+
+class Type;
+struct EventParcel;
+
+//------------------------------------------------------------------------------
+class Dispatcher
+{
+public:
+ void add_analyzer(Analyzer& analyzer);
+ void run(DataSource& data_source);
+ void on_parcel(const EventParcel& parcel);
+
+private:
+ void on_new_type(const Type* type);
+ Vector<Subscription> pending_subs;
+ Vector<Subscription> dispatchers;
+};
diff --git a/thirdparty/tourist/analysis/include/analysis/outline.h b/thirdparty/tourist/analysis/include/analysis/outline.h
new file mode 100644
index 000000000..8a56c129f
--- /dev/null
+++ b/thirdparty/tourist/analysis/include/analysis/outline.h
@@ -0,0 +1,153 @@
+#pragma once
+
+#include <foundation/types.h>
+#include <foundation/hash.h>
+#include <trace/trace.h>
+
+#include <../src/constants.h>
+
+#include "array.h"
+#include "string.h"
+
+class FieldStr;
+struct Aux;
+struct Event;
+template <typename T> struct Array;
+
+//------------------------------------------------------------------------------
+template <typename T> concept is_integer = std::integral<T>;
+template <typename T> concept is_real = std::floating_point<T>;
+template <typename T> concept is_array = std::is_unbounded_array<T>::value;
+template <typename T> concept is_integer_array = is_array<T> && is_integer<T>;
+template <typename T> concept is_real_array = is_array<T> && is_real<T>;
+
+//------------------------------------------------------------------------------
+struct Outline
+{
+ struct FieldBase
+ {
+ uint32 hash;
+ int16 offset = -1;
+ uint8 type_info;
+ uint8 set : 1;
+ uint8 index : 7;
+ };
+
+ template <typename T>
+ struct Field
+ : public FieldBase
+ {
+ using R = std::remove_all_extents<T>::type;
+ R operator () (const Event& event) const requires is_integer<T>;
+ R operator () (const Event& event) const requires is_real<T>;
+ FieldStr operator () (const Event& event) const requires std::is_same<T, FieldStr>::value;
+ Array<T> operator () (const Event& event) const requires is_array<T>;
+
+ private:
+ const Aux* find_aux(const Event& event) const;
+ };
+
+ Outline(uint32 h) : hash(h) {}
+ uint32 get_thread_id() const { return event->thread_id; }
+ FieldBase* fields() const { return (FieldBase*)(this + 1); }
+
+ const Event* event;
+ uint32 hash;
+ uint32 _unused;
+ //Field<void> fields[...];
+};
+static_assert(alignof(Outline::FieldBase) <= alignof(Outline));
+
+//------------------------------------------------------------------------------
+template <typename T>
+const Aux* Outline::Field<T>::find_aux(const Event& event) const
+{
+ if (!set)
+ return nullptr;
+
+ if ((type_info & TYPE_INFO_CAT_MASK) == 0)
+ return nullptr;
+
+ for (const Aux& aux : event.aux)
+ if (aux.index == index)
+ return &aux;
+
+ return nullptr;
+}
+
+//------------------------------------------------------------------------------
+template <typename T>
+Outline::Field<T>::R
+Outline::Field<T>::operator () (const Event& event) const requires is_integer<T>
+{
+ if (!set)
+ return T(0);
+
+ const uint8* data = event.data.get() + offset;
+ switch (1 << (type_info & TYPE_INFO_SIZE_MASK))
+ {
+ case 1: { uint8 r; std::memcpy(&r, data, 1); return T(r); }
+ case 2: { uint16 r; std::memcpy(&r, data, 2); return T(r); }
+ case 4: { uint32 r; std::memcpy(&r, data, 4); return T(r); }
+ case 8: { uint64 r; std::memcpy(&r, data, 8); return T(r); }
+ default: return T(0);
+ }
+}
+
+//------------------------------------------------------------------------------
+template <typename T>
+Outline::Field<T>::R
+Outline::Field<T>::operator () (const Event& event) const requires is_real<T>
+{
+ if (!set)
+ return T(0);
+
+ const uint8* data = event.data.get() + offset;
+ switch (1 << (type_info & TYPE_INFO_SIZE_MASK))
+ {
+ case 4: { float r; std::memcpy(&r, data, 4); return T(r); }
+ case 8: { double r; std::memcpy(&r, data, 8); return T(r); }
+ }
+ return T(0);
+}
+
+//------------------------------------------------------------------------------
+template <typename T>
+Array<T> Outline::Field<T>::operator () (const Event& event) const requires is_array<T>
+{
+ const Aux* aux = find_aux(event);
+ if (aux == nullptr)
+ return Array<T>();
+
+ return Array<T>(aux, type_info);
+}
+
+//------------------------------------------------------------------------------
+template <typename T>
+FieldStr Outline::Field<T>::operator () (const Event& event) const requires std::is_same<T, FieldStr>::value
+{
+ FieldStr ret;
+
+ const Aux* aux = find_aux(event);
+ if (aux == nullptr)
+ return ret;
+
+ Array<uint8[]> inner(aux, type_info);
+ ret.inner = std::move(inner);
+ return ret;
+}
+
+
+
+//------------------------------------------------------------------------------
+#define begin_outline(group, name) \
+ struct group##_##name : public Outline { \
+ group##_##name() : Outline(Hash(#group) * Hash(#name)) {}
+
+#define field(type, name) \
+ Outline::Field<type> _##name = { Hash(#name) }; \
+ auto name() const { return _##name(*(event)); }
+
+#define end_outline() \
+ Outline::Field<void> _$End = {}; \
+ };
diff --git a/thirdparty/tourist/analysis/include/analysis/string.h b/thirdparty/tourist/analysis/include/analysis/string.h
new file mode 100644
index 000000000..310bbca11
--- /dev/null
+++ b/thirdparty/tourist/analysis/include/analysis/string.h
@@ -0,0 +1,35 @@
+#pragma once
+
+//------------------------------------------------------------------------------
+class FieldStr
+ : public NoCopy
+{
+public:
+ FieldStr() = default;
+ ~FieldStr() { tt_free((void*)_data); }
+ FieldStr(FieldStr&& rhs) { std::swap(_data, rhs._data); }
+ FieldStr& operator = (FieldStr&& rhs) = delete;
+ StringView as_view() const { return StringView(get_data(), length()); }
+ explicit operator StringView () const { return as_view(); }
+ const char* get() const { return get_data(); }
+ uint32 length() const { return inner.get_count(); }
+
+private:
+ const char* get_data() const
+ {
+ if (_data != nullptr) return _data;
+ if (inner.element_size == 1) return (char*)(inner.get());
+ if (inner.element_size != 2) fatal("unsupported string character size");
+
+ const char16_t* source = (const char16_t*)(inner.get());
+ _data = (char*)tt_malloc(length() + 1);
+ for (int32 i = 0, n = length(); i < n; ++i, ++source)
+ _data[i] = char((*source > 0x7f) ? '?' : *source);
+ _data[length()] = '\0';
+ return _data;
+ }
+
+public:
+ Array<uint8[]> inner;
+ mutable char* _data = nullptr;
+};
diff --git a/thirdparty/tourist/analysis/src/dispatcher.cpp b/thirdparty/tourist/analysis/src/dispatcher.cpp
new file mode 100644
index 000000000..e7ea4a37e
--- /dev/null
+++ b/thirdparty/tourist/analysis/src/dispatcher.cpp
@@ -0,0 +1,148 @@
+#include <analysis/dispatcher.h>
+#include <foundation/scheduler.h>
+#include <trace/trace.h>
+
+//------------------------------------------------------------------------------
+void Dispatcher::add_analyzer(Analyzer& analyzer)
+{
+ analyzer.subscribe(pending_subs);
+}
+
+//------------------------------------------------------------------------------
+void Dispatcher::on_new_type(const Type* type)
+{
+ auto [group, name] = type->get_name();
+ uint32 type_hash = Hash(group) * Hash(name);
+
+ for (Subscription& sub : pending_subs)
+ {
+ Outline* outline = sub.outline.get();
+ if (outline->hash != type_hash)
+ continue;
+
+ for (uint32 i = 0; i < type->get_field_count(); ++i)
+ {
+ auto [field_name, field] = type->get_field_info(i);
+ uint32 field_hash = Hash(field_name);
+
+ for (Outline::FieldBase* f = outline->fields(); f->hash; ++f)
+ {
+ if (f->hash != field_hash)
+ continue;
+
+ f->type_info = uint8(field.get_type_info());
+ f->offset = int16(field.get_offset());
+ f->set = 1;
+ f->index = i;
+ break;
+ }
+ }
+
+ uint32 uid = type->get_uid();
+ if (uid >= dispatchers.size())
+ {
+ uint32 new_size = (uid + 16) & ~15;
+ dispatchers.resize(new_size);
+ }
+
+ std::swap(sub, pending_subs.back());
+ dispatchers[uid] = std::move(pending_subs.back());
+ pending_subs.pop_back();
+ break;
+ }
+}
+
+//------------------------------------------------------------------------------
+void Dispatcher::on_parcel(const EventParcel& parcel)
+{
+ if (!pending_subs.empty())
+ for (const Type* type : parcel.new_types)
+ on_new_type(type);
+
+ if (dispatchers.empty())
+ return;
+
+ for (const Event& event : parcel.events)
+ {
+ uint32 uid = event.uid;
+ if (uid >= dispatchers.size())
+ continue;
+
+ const Subscription& sub = dispatchers[uid];
+ Outline* outline = sub.outline.get();
+ if (outline == nullptr)
+ continue;
+
+ outline->event = &event;
+ auto* analyzer = (Analyzer*)(sub.analyzer);
+ (analyzer->*(sub.sink))(*outline);
+ }
+}
+
+//------------------------------------------------------------------------------
+void Dispatcher::run(DataSource& data_source)
+{
+ Allocator allocator;
+
+ Preamble preamble(data_source, allocator);
+ Transport transport = preamble.get_transport();
+ Protocol protocol = preamble.get_protocol();
+
+#if 0
+ Scheduler scheduler({
+ .concurrency = 3
+ });
+
+ struct State {
+ Bundle bundle;
+ Packet packets[128];
+ EventParcel parcel;
+ };
+ State states[3];
+
+ Task transport_task;
+ Task protocol_task;
+ Task analysis_task;
+
+ auto analysis_entry = [&, index=uint32(0)] () mutable {
+ auto& parcel = states[index].parcel;
+ on_parcel(parcel);
+ };
+
+ auto protocol_entry = [&, index=uint32(0)] () mutable {
+ auto& parcel = states[index].parcel;
+ auto& bundle = states[index].bundle;
+ parcel.reset();
+ protocol.read(parcel, bundle);
+ };
+
+ auto transport_entry = [&, index=uint32(0)] () mutable {
+ auto& bundle = states[index].bundle;
+ auto& packets = states[index].packets;
+ bundle = transport.read_packets(packets);
+ };
+
+ while (true)
+ {
+ transport_task = scheduler.create("transport", transport_entry);
+ protocol_task = scheduler.create("protocol", protocol_entry);
+ analysis_task = scheduler.create("analysis", analysis_entry);
+
+ scheduler.start_after(protocol_task, transport_task);
+ scheduler.start_after(analysis_task, protocol_task);
+ scheduler.submit(transport_task);
+
+ scheduler.wait(analysis_task);
+ };
+
+#else
+ Packet packets[128];
+ EventParcel parcel;
+ while (Bundle bundle = transport.read_packets(packets))
+ {
+ parcel.reset();
+ protocol.read(parcel, bundle);
+ on_parcel(parcel);
+ }
+#endif // 0
+}
diff --git a/thirdparty/tourist/foundation/include/foundation/buffer.h b/thirdparty/tourist/foundation/include/foundation/buffer.h
new file mode 100644
index 000000000..ab004248c
--- /dev/null
+++ b/thirdparty/tourist/foundation/include/foundation/buffer.h
@@ -0,0 +1,148 @@
+#pragma once
+
+#include "types.h"
+
+//------------------------------------------------------------------------------
+class BufferStream;
+class BufferRef;
+class Slab;
+
+//------------------------------------------------------------------------------
+class Buffer
+{
+public:
+ Buffer() = default;
+ ~Buffer();
+ Buffer(Buffer&& rhs);
+ void operator = (Buffer&& rhs);
+ const uint8* get_pointer() const;
+ uint32 get_size() const;
+ BufferStream create_stream() const;
+ BufferRef create_ref() const;
+ Buffer create_sub_buffer(uint32 left, uint32 right=~0u) const;
+ Buffer create_sub_buffer(const uint8* ptr, uint32 size) const;
+
+protected:
+ friend class Allocator;
+ friend class BufferStream;
+ void inc_ref();
+ void dec_ref();
+ void move(Buffer&& rhs);
+ Buffer clone() const;
+ Slab* _slab = nullptr;
+ uint32 _offset = 0;
+ uint32 _size;
+
+private:
+ Buffer(const Buffer&) = delete;
+ Buffer& operator = (const Buffer&) = delete;
+};
+
+
+
+//------------------------------------------------------------------------------
+class MutableBuffer
+ : public Buffer
+{
+public:
+ MutableBuffer() = default;
+ uint8* get_pointer();
+
+private:
+ friend class Allocator;
+ MutableBuffer(Slab* slab, uint32 size, uint32 offset);
+};
+
+
+
+//------------------------------------------------------------------------------
+class Pointer
+{
+public:
+ Pointer() = default;
+ ~Pointer();
+ Pointer(Pointer&& rhs);
+ void operator = (Pointer&& rhs);
+ bool is_valid() const;
+ void pin();
+ const uint8* get() const;
+
+protected:
+ friend class BufferStream;
+ Pointer(Slab* slab, const uint8* ptr);
+
+private:
+ friend class Allocator;
+ Slab* get_slab() const;
+
+ union {
+ struct {
+ uintptr _ptr : 47;
+ uintptr _pinned : 1;
+ uintptr _slab_offset : 16;
+ };
+ uintptr _value = 0;
+ };
+};
+
+
+
+//------------------------------------------------------------------------------
+class BufferRef
+ : public Pointer
+{
+private:
+ friend class Buffer;
+ BufferRef(Slab* slab, const uint8* ptr);
+};
+
+
+
+//------------------------------------------------------------------------------
+class Allocator
+ : public NoCopy
+ , public NoMove
+{
+public:
+ ~Allocator();
+ MutableBuffer create_buffer(uint32 size);
+ static Allocator& get_from(Buffer& buffer);
+ static Allocator& get_from(BufferRef& ref);
+ static Allocator& get_from(BufferStream& stream);
+ static Allocator& get_from(Pointer& pointer);
+
+private:
+ friend Slab;
+ void free_slab(Slab* slab);
+ static Allocator& get_from(const Slab* slab);
+ Mutex _lock;
+ Slab* _slab = nullptr;
+ uint32 _slab_free = 0;
+};
+
+
+
+//------------------------------------------------------------------------------
+class BufferStream
+{
+public:
+ BufferStream() = default;
+ bool has_data() const;
+ uint32 get_consumed() const;
+ uint32 get_remaining() const;
+ const uint8* read(uint32 size);
+ Buffer read_buf(uint32 size);
+ Pointer read_ptr(uint32 size);
+ template <typename T> T read();
+
+private:
+ friend class Allocator;
+ friend class Buffer;
+ BufferStream(Slab* slab, const uint8* ptr, uint32 size);
+ Slab* get_slab() const;
+ const uint8* _ptr;
+ uint32 _slab_offset = 0;
+ uint32 _end = 0;
+ uint32 _cursor = 0;
+ uint32 _unused;
+};
diff --git a/thirdparty/tourist/foundation/include/foundation/hash.h b/thirdparty/tourist/foundation/include/foundation/hash.h
new file mode 100644
index 000000000..0099eaa5f
--- /dev/null
+++ b/thirdparty/tourist/foundation/include/foundation/hash.h
@@ -0,0 +1,44 @@
+#pragma once
+
+#include <foundation/types.h>
+
+//------------------------------------------------------------------------------
+class Hash
+{
+public:
+ constexpr Hash(StringView str);
+ constexpr operator uint32 () const;
+ constexpr operator uint64 () const;
+ constexpr Hash operator * (Hash rhs) const;
+
+private:
+ Hash() = default;
+ uint64 value = 5381;
+};
+
+//------------------------------------------------------------------------------
+constexpr inline Hash::Hash(StringView str)
+{
+ for (uint32 c : str)
+ value = (value * 33) + c;
+}
+
+//------------------------------------------------------------------------------
+constexpr inline Hash Hash::operator * (Hash rhs) const
+{
+ Hash ret;
+ ret.value = (value * 33) + rhs.value;
+ return ret;
+}
+
+//------------------------------------------------------------------------------
+constexpr inline Hash::operator uint32 () const
+{
+ return uint32(value);
+}
+
+//------------------------------------------------------------------------------
+constexpr inline Hash::operator uint64 () const
+{
+ return uint64(value);
+}
diff --git a/thirdparty/tourist/foundation/include/foundation/malloc.h b/thirdparty/tourist/foundation/include/foundation/malloc.h
new file mode 100644
index 000000000..12b0c7430
--- /dev/null
+++ b/thirdparty/tourist/foundation/include/foundation/malloc.h
@@ -0,0 +1,31 @@
+#pragma once
+
+#include <algorithm>
+#include <concepts>
+#include <foundation/types.h>
+
+//------------------------------------------------------------------------------
+void tt_memory_canaries_on();
+void tt_memory_stomp_on();
+extern void* (*tt_malloc_impl)(size_t size, size_t alignment);
+extern void (*tt_free)(void* address);
+
+//------------------------------------------------------------------------------
+template <typename T=uint8>
+T* tt_malloc(
+ size_t size,
+ size_t alignment=std::max(alignof(std::max_align_t), alignof(T)))
+{
+ return (T*)tt_malloc_impl(size, alignment);
+}
+
+//------------------------------------------------------------------------------
+template <std::default_initializable T>
+T* tt_new(size_t count=1)
+{
+ size_t size = sizeof(T) * count;
+ auto* ret = (T*)tt_malloc_impl(size, alignof(T));
+ for (T& t : Span<T>(ret, count))
+ new (&t) T();
+ return ret;
+}
diff --git a/thirdparty/tourist/foundation/include/foundation/platform.h b/thirdparty/tourist/foundation/include/foundation/platform.h
new file mode 100644
index 000000000..cbae23bd2
--- /dev/null
+++ b/thirdparty/tourist/foundation/include/foundation/platform.h
@@ -0,0 +1,40 @@
+#pragma once
+
+#ifdef _WIN32
+
+#ifndef NOMINMAX
+#define NOMINMAX
+#define _TT_UNDEF_NOMINMAX
+#endif
+#ifndef NOGDI
+#define NOGDI
+#define _TT_UNDEF_NOGDI
+#endif
+#ifndef WIN32_LEAN_AND_MEAN
+#define WIN32_LEAN_AND_MEAN
+#define _TT_UNDEF_WIN32_LEAN_AND_MEAN
+#endif
+#include <Windows.h>
+#ifdef _TT_UNDEF_WIN32_LEAN_AND_MEAN
+#undef WIN32_LEAN_AND_MEAN
+#undef _TT_UNDEF_WIN32_LEAN_AND_MEAN
+#endif
+#ifdef _TT_UNDEF_NOGDI
+#undef NOGDI
+#undef _TT_UNDEF_NOGDI
+#endif
+#ifdef _TT_UNDEF_NOMINMAX
+#undef NOMINMAX
+#undef _TT_UNDEF_NOMINMAX
+#endif
+
+#else // POSIX
+
+#include <cstdlib>
+#include <csignal>
+#include <fcntl.h>
+#include <unistd.h>
+#include <sys/stat.h>
+#include <sys/mman.h>
+
+#endif
diff --git a/thirdparty/tourist/foundation/include/foundation/scheduler.h b/thirdparty/tourist/foundation/include/foundation/scheduler.h
new file mode 100644
index 000000000..54cd438a8
--- /dev/null
+++ b/thirdparty/tourist/foundation/include/foundation/scheduler.h
@@ -0,0 +1,113 @@
+#pragma once
+
+#include "types.h"
+
+struct Job;
+
+//------------------------------------------------------------------------------
+#if defined(__cpp_lib_hardware_interference_size) && __cpp_lib_hardware_interference_size >= 201603
+enum { INTERFERENCE_SIZE = std::hardware_destructive_interference_size };
+#else
+enum { INTERFERENCE_SIZE = 64 };
+#endif
+typedef uintptr WaitHandle;
+
+//------------------------------------------------------------------------------
+class Task
+ : public NoCopy
+{
+public:
+ Task() = default;
+ ~Task();
+ Task(Task&& rhs) { std::swap(_job, rhs._job); }
+ void operator = (Task&& rhs) { std::swap(_job, rhs._job); }
+ bool is_valid() const { return _job != nullptr; }
+
+private:
+ friend class Scheduler;
+ Task(Job* job);
+ operator Job* ();
+ Job* operator -> ();
+ Job* _job = nullptr;
+};
+
+//------------------------------------------------------------------------------
+class Scheduler
+ : public NoCopy
+ , public NoMove
+{
+public:
+ struct Setup
+ {
+ using WorkLoop = void (uintptr);
+ using ThreadFactory = Thread (uintptr, WorkLoop*, uintptr);
+ uint32 concurrency = 0;
+ uintptr factory_param = 0;
+ ThreadFactory* thread_factory = nullptr;
+ StringView name = "";
+ };
+ Scheduler(const Setup& desc);
+ ~Scheduler();
+ Task create(StringView name);
+ template <std::invocable T> Task create(StringView name, T& callable);
+ template <class T> Task create(StringView name, T* data, void (*entry)(T*));
+ void depends_on(Task& self, Task& that);
+ void start_after(Task& self, Task& runs_first);
+ void submit(Task& task);
+ template <typename T=void> auto wait(Task& task);
+
+private:
+ struct WaitSlot
+ {
+ Mutex lock;
+ ConditionVar cond_var;
+ };
+
+ using JobEntry = void (*)(void*);
+ Job* do_work(Job* job);
+ void do_work();
+ Task create_impl(StringView name, void* data, JobEntry entry);
+ void submit(Job* job);
+ WaitSlot& get_wait_slot(Job* job);
+ void* wait_impl(Task& task);
+ void unwait(Job* job);
+ Vector<Thread> _threads;
+ Job* _job_list = nullptr;
+ Mutex _list_lock;
+ WaitSlot _wait_slots[4];
+ ConditionVar _list_cond_var;
+ atomic_uint32 _running = 1;
+};
+
+//------------------------------------------------------------------------------
+template <class T>
+Task Scheduler::create(
+ StringView name,
+ T* data,
+ void (*entry)(T*))
+{
+ static_assert(sizeof(entry) == sizeof(void*));
+ return create_impl(name, data, JobEntry(entry));
+}
+
+//------------------------------------------------------------------------------
+template <std::invocable T>
+Task Scheduler::create(StringView name, T& callable)
+{
+ auto thunk = [] (T* callable) { callable->operator () (); };
+ return create<T>(name, &callable, thunk);
+}
+
+//------------------------------------------------------------------------------
+template <class T>
+auto Scheduler::wait(Task& task)
+{
+ return (T*)wait_impl(task);
+}
+
+//------------------------------------------------------------------------------
+template <>
+inline auto Scheduler::wait<void>(Task& task)
+{
+ wait_impl(task);
+}
diff --git a/thirdparty/tourist/foundation/include/foundation/types.h b/thirdparty/tourist/foundation/include/foundation/types.h
new file mode 100644
index 000000000..f7ada29a7
--- /dev/null
+++ b/thirdparty/tourist/foundation/include/foundation/types.h
@@ -0,0 +1,115 @@
+#pragma once
+
+#include <atomic>
+#include <condition_variable>
+#include <concepts>
+#include <cstdint>
+#include <deque>
+#include <filesystem>
+#include <map>
+#include <mutex>
+#include <set>
+#include <span>
+#include <string_view>
+#include <thread>
+#include <unordered_map>
+#include <vector>
+
+//------------------------------------------------------------------------------
+namespace FileSystem = std::filesystem;
+using ConditionVar = std::condition_variable;
+using Mutex = std::mutex;
+using Path = std::filesystem::path;
+using String = std::string;
+using StringView = std::string_view;
+using Thread = std::thread;
+using UniqueLock = std::unique_lock<Mutex>;
+template <typename... T> using Deque = std::deque<T...>;
+template <typename... T> using Map = std::map<T...>;
+template <typename... T> using Set = std::set<T...>;
+template <typename... T> using Span = std::span<T...>;
+template <typename... T> using Tuple = std::tuple<T...>;
+template <typename... T> using UniquePtr = std::unique_ptr<T...>;
+template <typename... T> using Vector = std::vector<T...>;
+template <typename... T> using UnorderedMap = std::unordered_map<T...>;
+
+//------------------------------------------------------------------------------
+using uint8 = uint8_t; using int8 = int8_t;
+using uint16 = uint16_t; using int16 = int16_t;
+using uint32 = uint32_t; using int32 = int32_t;
+using uint64 = uint64_t; using int64 = int64_t;
+using uintptr = uintptr_t; using intptr = intptr_t;
+
+//------------------------------------------------------------------------------
+template <std::integral T>
+struct Atomic
+ : protected std::atomic<T>
+{
+ using Base = std::atomic<T>;
+ Atomic() = default;
+ Atomic(T t) { store_relaxed(t); }
+ auto load_relaxed() const { return Base::load(std::memory_order_relaxed); }
+ auto load_acquire() const { return Base::load(std::memory_order_acquire); }
+ auto load_release() const { return Base::load(std::memory_order_release); }
+ auto store_relaxed(T t) { return Base::store(t, std::memory_order_relaxed); }
+ auto store_acquire(T t) { return Base::store(t, std::memory_order_acquire); }
+ auto store_release(T t) { return Base::store(t, std::memory_order_release); }
+ auto add_relaxed(T t) { return Base::fetch_add(t, std::memory_order_relaxed); }
+ auto add_acquire(T t) { return Base::fetch_add(t, std::memory_order_acquire); }
+ auto add_release(T t) { return Base::fetch_add(t, std::memory_order_release); }
+ auto and_relaxed(T t) { return Base::fetch_and(t, std::memory_order_relaxed); }
+ auto and_acquire(T t) { return Base::fetch_and(t, std::memory_order_acquire); }
+ auto and_release(T t) { return Base::fetch_and(t, std::memory_order_release); }
+ auto or_relaxed(T t) { return Base::fetch_or(t, std::memory_order_relaxed); }
+ auto or_acquire(T t) { return Base::fetch_or(t, std::memory_order_acquire); }
+ auto or_release(T t) { return Base::fetch_or(t, std::memory_order_release); }
+ auto xor_relaxed(T t) { return Base::fetch_xor(t, std::memory_order_relaxed); }
+ auto xor_acquire(T t) { return Base::fetch_xor(t, std::memory_order_acquire); }
+ auto xor_release(T t) { return Base::fetch_xor(t, std::memory_order_release); }
+ auto cas_relaxed(T u, T t) { return Base::compare_exchange_weak(u, t, std::memory_order_relaxed); }
+ auto cas_release(T u, T t) { return Base::compare_exchange_weak(u, t, std::memory_order_release); }
+ auto cas_acquire(T u, T t) { return Base::compare_exchange_weak(u, t, std::memory_order_acquire); }
+ auto exchg_relaxed(T t) { return Base::exchange(t, std::memory_order_relaxed); }
+ auto exchg_release(T t) { return Base::exchange(t, std::memory_order_release); }
+ auto exchg_acquire(T t) { return Base::exchange(t, std::memory_order_acquire); }
+};
+using atomic_int8 = Atomic<int8>; using atomic_uint8 = Atomic<uint8>;
+using atomic_int16 = Atomic<int16>; using atomic_uint16 = Atomic<uint16>;
+using atomic_int32 = Atomic<int32>; using atomic_uint32 = Atomic<uint32>;
+using atomic_int64 = Atomic<int64>; using atomic_uint64 = Atomic<uint64>;
+
+//------------------------------------------------------------------------------
+template <typename T, size_t N>
+constexpr uint32 sizeof_array(T const (&)[N])
+{
+ return N;
+}
+
+//------------------------------------------------------------------------------
+struct NoCopy
+{
+ NoCopy() = default;
+ NoCopy(NoCopy&&) = default;
+ NoCopy& operator = (NoCopy&&) = default;
+ NoCopy& operator = (const NoCopy&) = delete;
+ NoCopy(const NoCopy&) = delete;
+};
+
+struct NoMove
+{
+ NoMove() = default;
+ NoMove(NoMove&&) = delete;
+ NoMove& operator = (NoMove&&) = delete;
+};
+
+//------------------------------------------------------------------------------
+#if defined(_MSC_VER) && !defined(__clang__)
+# define WARNING_OFF(n) WARNING_OFF_(warning(disable:n))
+# define WARNING_OFF_(n) _Pragma(#n)
+ WARNING_OFF(4200) // zero-sized arrays
+ WARNING_OFF(4201) // nameless struct/union
+ // WARNING_OFF(4267) // integer narrowing
+ WARNING_OFF(4324) // struct align and zero-sized arrays
+# undef WARNING_OFF_
+# undef WARNING_OFF
+#endif
diff --git a/thirdparty/tourist/foundation/src/allocator.cpp b/thirdparty/tourist/foundation/src/allocator.cpp
new file mode 100644
index 000000000..c6f021ad2
--- /dev/null
+++ b/thirdparty/tourist/foundation/src/allocator.cpp
@@ -0,0 +1,69 @@
+#include <foundation/buffer.h>
+#include <foundation/malloc.h>
+
+#include "slab.h"
+
+//------------------------------------------------------------------------------
+Allocator::~Allocator()
+{
+ if (_slab != nullptr)
+ _slab->dec_ref();
+}
+
+//------------------------------------------------------------------------------
+MutableBuffer Allocator::create_buffer(uint32 size)
+{
+ auto create_slab = [this] (uint32 slab_size) {
+ auto* slab = tt_malloc<Slab>(slab_size + sizeof(Slab));
+ slab->_allocator = this;
+ slab->_size = slab_size;
+ slab->_refs.store_relaxed(0);
+ return slab;
+ };
+
+ enum {
+ PACKET_PAGE_SIZE = 4 << 10,
+ PACKET_SLAB_SIZE = 1 << 20,
+ PACKET_ALIGN = 32 - 1,
+ };
+
+ if (size > PACKET_PAGE_SIZE)
+ {
+ Slab* slab = create_slab(size);
+ return MutableBuffer(slab, size, 0);
+ }
+
+ uint32 alloc_size = (size + PACKET_ALIGN) & ~PACKET_ALIGN;
+
+ UniqueLock _(_lock);
+
+ if (alloc_size > _slab_free && _slab != nullptr)
+ {
+ _slab->dec_ref();
+ _slab = nullptr;
+ }
+
+ if (_slab == nullptr)
+ {
+ _slab_free = PACKET_SLAB_SIZE;
+ _slab = create_slab(_slab_free);
+ _slab->inc_ref();
+ }
+
+ uint32 offset = PACKET_SLAB_SIZE - _slab_free;
+ _slab_free -= alloc_size;
+ return MutableBuffer(_slab, size, offset);
+}
+
+//------------------------------------------------------------------------------
+void Allocator::free_slab(Slab* slab)
+{
+ tt_free(slab);
+}
+
+//------------------------------------------------------------------------------
+Allocator& Allocator::get_from(Buffer& buffer) { return get_from(buffer._slab); }
+Allocator& Allocator::get_from(BufferRef& ref) { return get_from(ref.get_slab()); }
+Allocator& Allocator::get_from(BufferStream& stream){ return get_from(stream.get_slab()); }
+Allocator& Allocator::get_from(Pointer& pointer) { return get_from(pointer.get_slab()); }
+Allocator& Allocator::get_from(const Slab* slab) { return slab->get_allocator(); }
diff --git a/thirdparty/tourist/foundation/src/buffer.cpp b/thirdparty/tourist/foundation/src/buffer.cpp
new file mode 100644
index 000000000..a4baccdef
--- /dev/null
+++ b/thirdparty/tourist/foundation/src/buffer.cpp
@@ -0,0 +1,117 @@
+#include <foundation/buffer.h>
+
+#include "slab.h"
+
+//------------------------------------------------------------------------------
+Buffer::~Buffer()
+{
+ dec_ref();
+}
+
+//------------------------------------------------------------------------------
+Buffer::Buffer(Buffer&& rhs)
+{
+ move(std::move(rhs));
+}
+
+//------------------------------------------------------------------------------
+void Buffer::operator = (Buffer&& rhs)
+{
+ move(std::move(rhs));
+}
+
+//------------------------------------------------------------------------------
+const uint8* Buffer::get_pointer() const
+{
+ return _slab->get_pointer() + _offset;
+}
+
+//------------------------------------------------------------------------------
+uint32 Buffer::get_size() const
+{
+ return _size;
+}
+
+//------------------------------------------------------------------------------
+BufferStream Buffer::create_stream() const
+{
+ return BufferStream(_slab, get_pointer(), get_size());
+}
+
+//------------------------------------------------------------------------------
+BufferRef Buffer::create_ref() const
+{
+ return BufferRef(_slab, get_pointer());
+}
+
+//------------------------------------------------------------------------------
+void Buffer::inc_ref()
+{
+ if (_slab != nullptr)
+ _slab->inc_ref();
+}
+
+//------------------------------------------------------------------------------
+void Buffer::dec_ref()
+{
+ if (_slab != nullptr)
+ _slab->dec_ref();
+ _slab = nullptr;
+}
+
+//------------------------------------------------------------------------------
+Buffer Buffer::create_sub_buffer(uint32 left, uint32 right) const
+{
+ right = std::min<uint32>(right, _size);
+ return create_sub_buffer(get_pointer() + left, right - left);
+}
+
+//------------------------------------------------------------------------------
+Buffer Buffer::create_sub_buffer(const uint8* ptr, uint32 size) const
+{
+ uint32 offset = uint32(ptrdiff_t(ptr - get_pointer()));
+ if (offset >= _size)
+ return Buffer();
+
+ Buffer buffer = clone();
+ buffer._offset += offset;
+ buffer._size = size;
+ return buffer;
+}
+
+//------------------------------------------------------------------------------
+void Buffer::move(Buffer&& rhs)
+{
+ dec_ref();
+ std::swap(_slab, rhs._slab);
+ std::swap(_offset, rhs._offset);
+ std::swap(_size, rhs._size);
+}
+
+//------------------------------------------------------------------------------
+Buffer Buffer::clone() const
+{
+ Buffer buffer;
+ buffer._slab = _slab;
+ buffer._offset = _offset;
+ buffer._size = _size;
+ buffer.inc_ref();
+ return buffer;
+}
+
+
+
+//------------------------------------------------------------------------------
+uint8* MutableBuffer::get_pointer()
+{
+ return _slab->get_pointer() + _offset;
+}
+
+//------------------------------------------------------------------------------
+MutableBuffer::MutableBuffer(Slab* slab, uint32 size, uint32 offset)
+{
+ _slab = slab;
+ _offset = offset;
+ _size = size;
+ inc_ref();
+}
diff --git a/thirdparty/tourist/foundation/src/malloc.cpp b/thirdparty/tourist/foundation/src/malloc.cpp
new file mode 100644
index 000000000..c8d4c5d66
--- /dev/null
+++ b/thirdparty/tourist/foundation/src/malloc.cpp
@@ -0,0 +1,211 @@
+#include <foundation/types.h>
+#include <foundation/malloc.h>
+#include <foundation/platform.h>
+
+//------------------------------------------------------------------------------
+struct Header
+{
+ enum : uint32 { Magic = 0xaa55aa55 };
+ size_t size;
+ uint32 bias;
+ uint32 canary;
+ uint8 ptr[];
+};
+static bool g_add_canaries = false;
+
+//------------------------------------------------------------------------------
+#ifdef _WIN32
+
+template <typename T=uint8>
+static T* tt_malloc_norm(size_t size, size_t alignment)
+{
+ alignment = uint32(std::max(alignment, alignof(std::max_align_t)));
+ if (!g_add_canaries)
+ return (T*)_aligned_malloc(size, alignment);
+
+ uint32 lead_size = uint32(sizeof(Header) + alignment - 1) & ~uint32(alignment - 1);
+ size_t alloc_size = lead_size + size + sizeof(uint32);
+ auto* alloc_ptr = (uint8*)_aligned_malloc(alloc_size, alignment);
+
+ auto* header = (Header*)(alloc_ptr + lead_size - sizeof(Header));
+ header->bias = lead_size;
+ header->size = size;
+ header->canary = Header::Magic;
+
+ std::memcpy(header->ptr + size, &header->canary, sizeof(uint32));
+
+ return header->ptr;
+}
+
+static void tt_free_norm(void* address)
+{
+ if (address == nullptr)
+ return;
+
+ if (!g_add_canaries)
+ return _aligned_free(address);
+
+ auto* header = (Header*)address - 1;
+
+ auto is_canary = [] (void* ptr) {
+ uint32 canary = Header::Magic;
+ return !std::memcmp(ptr, &canary, sizeof(uint32));
+ };
+ if (!is_canary(&header->canary)) __debugbreak();
+ if (!is_canary(header->ptr + header->size)) __debugbreak();
+
+ _aligned_free(header->ptr - header->bias);
+}
+
+#else // POSIX
+
+template <typename T=uint8>
+static T* tt_malloc_norm(size_t size, size_t alignment)
+{
+ alignment = std::max(alignment, alignof(std::max_align_t));
+ // std::aligned_alloc requires size to be a multiple of alignment
+ size_t aligned_size = (size + alignment - 1) & ~(alignment - 1);
+ if (aligned_size == 0)
+ aligned_size = alignment;
+
+ if (!g_add_canaries)
+ return (T*)std::aligned_alloc(alignment, aligned_size);
+
+ uint32 lead_size = uint32(sizeof(Header) + alignment - 1) & ~uint32(alignment - 1);
+ size_t alloc_size = lead_size + size + sizeof(uint32);
+ size_t alloc_aligned = (alloc_size + alignment - 1) & ~(alignment - 1);
+ auto* alloc_ptr = (uint8*)std::aligned_alloc(alignment, alloc_aligned);
+
+ auto* header = (Header*)(alloc_ptr + lead_size - sizeof(Header));
+ header->bias = lead_size;
+ header->size = size;
+ header->canary = Header::Magic;
+
+ std::memcpy(header->ptr + size, &header->canary, sizeof(uint32));
+
+ return header->ptr;
+}
+
+static void tt_free_norm(void* address)
+{
+ if (address == nullptr)
+ return;
+
+ if (!g_add_canaries)
+ return std::free(address);
+
+ auto* header = (Header*)address - 1;
+
+ auto is_canary = [] (void* ptr) {
+ uint32 canary = Header::Magic;
+ return !std::memcmp(ptr, &canary, sizeof(uint32));
+ };
+ if (!is_canary(&header->canary)) __builtin_trap();
+ if (!is_canary(header->ptr + header->size)) __builtin_trap();
+
+ std::free(header->ptr - header->bias);
+}
+
+#endif
+
+//------------------------------------------------------------------------------
+void tt_memory_canaries_on()
+{
+ g_add_canaries = true;
+}
+
+
+//------------------------------------------------------------------------------
+void* (*tt_malloc_impl)(size_t, size_t) = tt_malloc_norm;
+void (*tt_free)(void*) = tt_free_norm;
+
+
+
+//------------------------------------------------------------------------------
+#ifdef _WIN32
+
+template <typename T=uint8>
+static T* tt_malloc_stomp(size_t size, size_t alignment)
+{
+ enum { PAGE_SIZE = 64 << 10 };
+ size = (size + (alignment - 1)) & ~(alignment - 1);
+ size_t alloc_size = (size + (2 * PAGE_SIZE)) & ~(PAGE_SIZE - 1);
+ auto* ptr = (uint8*)VirtualAlloc(nullptr, alloc_size, MEM_COMMIT, PAGE_READWRITE);
+ ptr += alloc_size - PAGE_SIZE;
+ DWORD unused;
+ VirtualProtect(ptr, PAGE_SIZE, PAGE_NOACCESS, &unused);
+
+ return (T*)(ptr - size);
+}
+
+static void tt_free_stomp(void* address)
+{
+ if (address == nullptr)
+ return;
+
+ *(uint32*)address = 0x30493049;
+
+ MEMORY_BASIC_INFORMATION info;
+ VirtualQuery(address, &info, sizeof(info));
+ DWORD unused;
+ VirtualProtect(info.AllocationBase, info.RegionSize, PAGE_NOACCESS, &unused);
+}
+
+#else // POSIX
+
+static size_t tt_get_page_size()
+{
+ static size_t page_size = size_t(sysconf(_SC_PAGESIZE));
+ return page_size;
+}
+
+struct StompHeader
+{
+ void* base;
+ size_t total_size;
+};
+
+template <typename T=uint8>
+static T* tt_malloc_stomp(size_t size, size_t alignment)
+{
+ size_t page_size = tt_get_page_size();
+ size = (size + (alignment - 1)) & ~(alignment - 1);
+ size_t needed = sizeof(StompHeader) + size + page_size;
+ size_t alloc_size = (needed + (page_size - 1)) & ~(page_size - 1);
+ auto* base = (uint8*)mmap(nullptr, alloc_size, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+ if (base == MAP_FAILED)
+ return nullptr;
+
+ // Guard page at the end
+ uint8* guard = base + alloc_size - page_size;
+ mprotect(guard, page_size, PROT_NONE);
+
+ // User allocation right before the guard, header right before that
+ T* user_ptr = (T*)(guard - size);
+ auto* hdr = (StompHeader*)((uint8*)user_ptr - sizeof(StompHeader));
+ hdr->base = base;
+ hdr->total_size = alloc_size;
+
+ return user_ptr;
+}
+
+static void tt_free_stomp(void* address)
+{
+ if (address == nullptr)
+ return;
+
+ *(uint32*)address = 0x30493049;
+
+ auto* hdr = (StompHeader*)((uint8*)address - sizeof(StompHeader));
+ mprotect(hdr->base, hdr->total_size, PROT_NONE);
+}
+
+#endif
+
+//------------------------------------------------------------------------------
+void tt_memory_stomp_on()
+{
+ tt_malloc_impl = tt_malloc_stomp;
+ tt_free = tt_free_stomp;
+}
diff --git a/thirdparty/tourist/foundation/src/ref.cpp b/thirdparty/tourist/foundation/src/ref.cpp
new file mode 100644
index 000000000..feda5b0dd
--- /dev/null
+++ b/thirdparty/tourist/foundation/src/ref.cpp
@@ -0,0 +1,69 @@
+#include <foundation/buffer.h>
+
+#include "slab.h"
+
+//------------------------------------------------------------------------------
+Pointer::Pointer(Slab* slab, const uint8* ptr)
+: _ptr(uintptr(ptr))
+, _pinned(0)
+{
+ uintptr bias = (uintptr(ptr) - uintptr(slab)) / alignof(Slab);
+ _slab_offset = bias;
+}
+
+//------------------------------------------------------------------------------
+Pointer::Pointer(Pointer&& rhs)
+{
+ *this = std::move(rhs);
+}
+
+//------------------------------------------------------------------------------
+Pointer::~Pointer()
+{
+ if (_pinned)
+ get_slab()->dec_ref();
+}
+
+//------------------------------------------------------------------------------
+void Pointer::operator = (Pointer&& rhs)
+{
+ std::swap(_value, rhs._value);
+}
+
+//------------------------------------------------------------------------------
+bool Pointer::is_valid() const
+{
+ return _ptr != 0;
+}
+
+//------------------------------------------------------------------------------
+void Pointer::pin()
+{
+ if (!_pinned)
+ get_slab()->inc_ref();
+ _pinned = 1;
+}
+
+//------------------------------------------------------------------------------
+const uint8* Pointer::get() const
+{
+ return (uint8*)_ptr;
+}
+
+//------------------------------------------------------------------------------
+Slab* Pointer::get_slab() const
+{
+ uintptr bias = _slab_offset * alignof(Slab);
+ bias += uintptr(_ptr) & (alignof(Slab) - 1);
+ return (Slab*)(_ptr - bias);
+}
+
+
+
+//------------------------------------------------------------------------------
+BufferRef::BufferRef(Slab* slab, const uint8* ptr)
+: Pointer(slab, ptr)
+{
+ pin();
+}
+
diff --git a/thirdparty/tourist/foundation/src/scheduler.cpp b/thirdparty/tourist/foundation/src/scheduler.cpp
new file mode 100644
index 000000000..bb2499e0d
--- /dev/null
+++ b/thirdparty/tourist/foundation/src/scheduler.cpp
@@ -0,0 +1,339 @@
+#include <foundation/malloc.h>
+#include <foundation/scheduler.h>
+
+#include <cassert>
+
+//------------------------------------------------------------------------------
+struct alignas(INTERFERENCE_SIZE) Job
+{
+ StringView name;
+
+ void (*entry)(void*) = nullptr;
+ void* data = nullptr;
+
+ Job* fence = nullptr;
+ Job* first_child = nullptr;
+ Job* next_sibling = nullptr;
+
+ atomic_int16 num_refs = 0;
+ atomic_int16 dep_refs = 0;
+ uint16 _unused;
+ atomic_uint8 flags = 0;
+ uint8 wait_bit = 0;
+
+ enum : uint8
+ {
+ Flag_Locked = 1 << 0,
+ Flag_Scheduled = 1 << 1,
+ Flag_Running = 1 << 2,
+ Flag_Finished = 1 << 3,
+ Flag_HasWaiter = 1 << 4,
+ };
+};
+static_assert(sizeof(Job) <= INTERFERENCE_SIZE);
+
+
+
+//------------------------------------------------------------------------------
+class JobLock : NoMove, NoCopy
+{
+public:
+ JobLock(Job* job);
+ ~JobLock() { _job->flags.xor_release(Job::Flag_Locked); }
+ bool has_finished() const { return !!(_prev_flags & Job::Flag_Finished); }
+
+private:
+ void claim();
+ Job* _job;
+ uint8 _prev_flags;
+};
+
+//------------------------------------------------------------------------------
+JobLock::JobLock(Job* job)
+: _job(job)
+{
+ _prev_flags = job->flags.or_acquire(Job::Flag_Locked);
+ if (_prev_flags & Job::Flag_Locked)
+ claim();
+}
+
+//------------------------------------------------------------------------------
+void JobLock::claim()
+{
+ while (true)
+ {
+ _prev_flags = _job->flags.or_acquire(Job::Flag_Locked);
+ if ((_prev_flags & Job::Flag_Locked) == 0)
+ return;
+ }
+}
+
+
+
+//------------------------------------------------------------------------------
+Task::Task(Job* job)
+: _job(job)
+{
+ assert(job->num_refs.load_relaxed() >= 1);
+}
+
+//------------------------------------------------------------------------------
+Task::~Task()
+{
+ if (!is_valid())
+ return;
+
+ int32 prev_ref = _job->num_refs.add_relaxed(-1);
+ if (prev_ref != 1)
+ return;
+
+ tt_free(_job);
+}
+
+//------------------------------------------------------------------------------
+Task::operator Job* () { return _job; }
+Job* Task::operator -> () { return _job; }
+
+
+
+//------------------------------------------------------------------------------
+Scheduler::Scheduler(const Setup& desc)
+{
+ uint32 concurrency = desc.concurrency;
+ if (concurrency == 0)
+ concurrency = std::thread::hardware_concurrency();
+
+ Setup::ThreadFactory* factory = desc.thread_factory;
+ if (factory == nullptr)
+ {
+ auto default_factory = [] (uintptr, Setup::WorkLoop* loop, uintptr loop_param) {
+ return Thread(loop, loop_param);
+ };
+ factory = default_factory;
+ }
+
+ _threads.resize(concurrency);
+ for (Thread& thread : _threads)
+ {
+ thread = factory(desc.factory_param, [] (uintptr param) {
+ auto& scheduler = *(Scheduler*)param;
+ scheduler.do_work();
+ }, uintptr(this));
+ }
+}
+
+//------------------------------------------------------------------------------
+Scheduler::~Scheduler()
+{
+ _running.store_release(0);
+
+ _list_cond_var.notify_all();
+
+ for (Thread& thread : _threads)
+ thread.join();
+
+ assert(_job_list == nullptr);
+}
+
+//------------------------------------------------------------------------------
+Task Scheduler::create(StringView name)
+{
+ return create_impl(name, nullptr, nullptr);
+}
+
+//------------------------------------------------------------------------------
+void Scheduler::depends_on(Task& fence, Task& that)
+{
+ uint8 prev_flags = fence->flags.or_relaxed(Job::Flag_Scheduled);
+ if ((prev_flags & Job::Flag_Scheduled) == 0)
+ fence->num_refs.add_relaxed(1);
+
+ assert((that->flags.load_relaxed() & Job::Flag_Scheduled) == 0); // "Build fences first"
+ assert(that->fence == nullptr); // "Jobs may only have one dependant"
+
+ that->fence = fence;
+ fence->dep_refs.add_relaxed(1);
+}
+
+//------------------------------------------------------------------------------
+void Scheduler::start_after(Task& self, Task& runs_first)
+{
+ uint8 prev_flags = self->flags.or_relaxed(Job::Flag_Scheduled);
+ assert((prev_flags & Job::Flag_Scheduled) == 0);
+
+ self->num_refs.add_relaxed(1);
+
+ JobLock lock(runs_first);
+ if (lock.has_finished())
+ return submit((Job*)self);
+
+ self->next_sibling = runs_first->first_child;
+ runs_first->first_child = self;
+}
+
+//------------------------------------------------------------------------------
+void Scheduler::submit(Task& task)
+{
+ if (!task.is_valid())
+ return;
+
+ uint8 prev_flags = task->flags.or_relaxed(Job::Flag_Scheduled);
+ assert((prev_flags & Job::Flag_Scheduled) == 0);
+
+ Job* job = task;
+ job->num_refs.add_relaxed(1);
+ submit(job);
+}
+
+//------------------------------------------------------------------------------
+Task Scheduler::create_impl(StringView name, void* data, JobEntry entry)
+{
+ Job* job = tt_new<Job>();
+ job->name = name;
+ job->entry = entry;
+ job->data = data;
+
+ job->num_refs.add_relaxed(1);
+ return Task(job);
+}
+
+//------------------------------------------------------------------------------
+void Scheduler::do_work()
+{
+ while (true)
+ {
+ UniqueLock lock(_list_lock);
+
+ Job* job;
+ while (true)
+ {
+ if (!_running.load_relaxed())
+ return;
+
+ job = _job_list;
+ if (job != nullptr)
+ {
+ _job_list = job->next_sibling;
+ lock.unlock();
+ break;
+ }
+
+ _list_cond_var.wait(lock);
+ }
+
+ do
+ {
+ job = do_work(job);
+ }
+ while (job != nullptr);
+ }
+}
+
+//------------------------------------------------------------------------------
+Job* Scheduler::do_work(Job* job)
+{
+ Task task(job);
+
+ job->flags.or_acquire(Job::Flag_Running);
+
+ if (job->entry != nullptr)
+ job->entry(job->data);
+
+ uint8 prev_flags = job->flags.or_release(Job::Flag_Finished);
+
+ if (prev_flags & Job::Flag_HasWaiter)
+ unwait(job);
+
+ Job* next = nullptr;
+ if (Job* fence = job->fence)
+ {
+ uint32 prev_refs = fence->dep_refs.add_release(-1);
+ if (prev_refs == 1)
+ next = fence;
+ }
+
+ if (Job* child = job->first_child)
+ {
+ if (next == nullptr)
+ {
+ next = child;
+ child = child->next_sibling;
+ }
+
+ if (child != nullptr)
+ submit(child);
+ }
+
+ return next;
+}
+
+//------------------------------------------------------------------------------
+void Scheduler::submit(Job* job)
+{
+ uint32 count = 1;
+ Job* tail = job;
+ for (; tail->next_sibling != nullptr; ++count)
+ {
+ assert(tail->dep_refs.load_relaxed() == 0); // don't recall why I put this here
+ JobLock lock(tail);
+ tail = tail->next_sibling;
+ }
+
+ _list_lock.lock();
+ tail->next_sibling = _job_list;
+ _job_list = job;
+ _list_lock.unlock();
+
+ (count > 1) ? _list_cond_var.notify_all() : _list_cond_var.notify_one();
+}
+
+//------------------------------------------------------------------------------
+Scheduler::WaitSlot& Scheduler::get_wait_slot(Job* job)
+{
+ enum { A_RANDOM_PRIME_I_PICKED_ALL_ON_MY_OWN = 29 };
+ uint32 index = uint32(uintptr(job)) / sizeof(Job);
+ index *= A_RANDOM_PRIME_I_PICKED_ALL_ON_MY_OWN;
+ index %= sizeof_array(_wait_slots);
+ return _wait_slots[index];
+}
+
+//------------------------------------------------------------------------------
+void* Scheduler::wait_impl(Task& task)
+{
+ if (!task.is_valid())
+ return nullptr;
+
+ uint8 seen_flags;
+
+ for (uint32 i = 0; i < 50; ++i)
+ {
+ seen_flags = task->flags.load_acquire();
+ if (seen_flags & Job::Flag_Finished)
+ return task->data;
+
+ std::this_thread::yield();
+ }
+
+ seen_flags = task->flags.or_acquire(Job::Flag_HasWaiter);
+ if (seen_flags & Job::Flag_Finished)
+ return task->data;
+
+ WaitSlot& slot = get_wait_slot(task);
+
+ UniqueLock lock(slot.lock);
+ while (true)
+ {
+ seen_flags = task->flags.load_acquire();
+ if (seen_flags & Job::Flag_Finished)
+ return task->data;
+
+ slot.cond_var.wait(lock);
+ }
+}
+
+//------------------------------------------------------------------------------
+void Scheduler::unwait(Job* job)
+{
+ WaitSlot& slot = get_wait_slot(job);
+ slot.cond_var.notify_all();
+}
diff --git a/thirdparty/tourist/foundation/src/slab.h b/thirdparty/tourist/foundation/src/slab.h
new file mode 100644
index 000000000..36cac953b
--- /dev/null
+++ b/thirdparty/tourist/foundation/src/slab.h
@@ -0,0 +1,50 @@
+#pragma once
+
+#include <foundation/buffer.h>
+
+class Allocator;
+
+//------------------------------------------------------------------------------
+class alignas(256) Slab
+{
+public:
+ void inc_ref();
+ void dec_ref();
+ uint8* get_pointer();
+ Allocator& get_allocator() const;
+
+private:
+ friend Allocator;
+ Slab() = default;
+ ~Slab() = default;
+ Allocator* _allocator;
+ Atomic<int32> _refs;
+ int32 _size;
+ uint8 _data[];
+};
+
+//------------------------------------------------------------------------------
+inline void Slab::inc_ref()
+{
+ _refs.add_release(1);
+}
+
+//------------------------------------------------------------------------------
+inline void Slab::dec_ref()
+{
+ int32 prev_refs = _refs.add_acquire(-1);
+ if (prev_refs <= 1)
+ _allocator->free_slab(this);
+}
+
+//------------------------------------------------------------------------------
+inline uint8* Slab::get_pointer()
+{
+ return _data;
+}
+
+//------------------------------------------------------------------------------
+inline Allocator& Slab::get_allocator() const
+{
+ return *_allocator;
+}
diff --git a/thirdparty/tourist/foundation/src/stream.cpp b/thirdparty/tourist/foundation/src/stream.cpp
new file mode 100644
index 000000000..c560436d3
--- /dev/null
+++ b/thirdparty/tourist/foundation/src/stream.cpp
@@ -0,0 +1,72 @@
+#include <foundation/buffer.h>
+
+#include "slab.h"
+
+//------------------------------------------------------------------------------
+BufferStream::BufferStream(Slab* slab, const uint8* ptr, uint32 size)
+: _ptr(ptr)
+, _slab_offset(uint32(uintptr(ptr) - uintptr(slab)))
+, _end(size)
+{
+}
+
+//------------------------------------------------------------------------------
+Slab* BufferStream::get_slab() const
+{
+ return (Slab*)(_ptr - _slab_offset);
+}
+
+//------------------------------------------------------------------------------
+bool BufferStream::has_data() const
+{
+ return (_cursor < _end);
+}
+
+//------------------------------------------------------------------------------
+uint32 BufferStream::get_consumed() const
+{
+ return _cursor;
+}
+
+//------------------------------------------------------------------------------
+uint32 BufferStream::get_remaining() const
+{
+ return uint32(ptrdiff_t(_end - _cursor));
+}
+
+//------------------------------------------------------------------------------
+const uint8* BufferStream::read(uint32 size)
+{
+ _cursor += size;
+ return (uint8*)_ptr + _cursor - size;
+}
+
+//------------------------------------------------------------------------------
+Buffer BufferStream::read_buf(uint32 size)
+{
+ const uint8* ptr = read(size);
+
+ Buffer buffer;
+ buffer._slab = get_slab();
+ buffer._offset = uint32(uintptr(ptr) - uintptr(buffer._slab));
+ buffer._size = size;
+ return buffer;
+}
+
+//------------------------------------------------------------------------------
+Pointer BufferStream::read_ptr(uint32 size)
+{
+ const uint8* ret = read(size);
+ Slab* slab = get_slab();
+ return Pointer(slab, ret);
+}
+
+//------------------------------------------------------------------------------
+template <> int8 BufferStream::read< int8>() { return *( int8 *)read(sizeof( int8)); }
+template <> int16 BufferStream::read< int16>() { return *( int16*)read(sizeof( int16)); }
+template <> int32 BufferStream::read< int32>() { return *( int32*)read(sizeof( int32)); }
+template <> int64 BufferStream::read< int64>() { return *( int64*)read(sizeof( int64)); }
+template <> uint8 BufferStream::read<uint8>() { return *(uint8 *)read(sizeof(uint8)); }
+template <> uint16 BufferStream::read<uint16>() { return *(uint16*)read(sizeof(uint16)); }
+template <> uint32 BufferStream::read<uint32>() { return *(uint32*)read(sizeof(uint32)); }
+template <> uint64 BufferStream::read<uint64>() { return *(uint64*)read(sizeof(uint64)); }
diff --git a/thirdparty/tourist/trace/include/trace/detail/data.h b/thirdparty/tourist/trace/include/trace/detail/data.h
new file mode 100644
index 000000000..90a5ffa61
--- /dev/null
+++ b/thirdparty/tourist/trace/include/trace/detail/data.h
@@ -0,0 +1,45 @@
+#pragma once
+
+#include <foundation/buffer.h>
+
+class Allocator;
+class DataSource;
+
+//------------------------------------------------------------------------------
+class DataSource
+{
+public:
+ DataSource(const Path& path);
+ ~DataSource();
+ int32 read(void* out, uint32 size);
+ int64 get_size() const;
+
+private:
+ uintptr _handle;
+ mutable int64 _size = -1;
+};
+
+
+
+//------------------------------------------------------------------------------
+class DataStream
+{
+public:
+ struct Eof {};
+
+ DataStream(DataSource& data_source, Allocator& allocator);
+ uint64 tell() const;
+ uint32 get_available() const;
+ Buffer read(uint32 size);
+ template <typename T> T read();
+
+private:
+ enum : uint32 { BUFFER_SIZE = 2 << 20 };
+
+ const uint8* _read(uint32 size);
+ DataSource& _data_source;
+ Allocator& _allocator;
+ BufferStream _stream;
+ Buffer _buffer;
+ uint64 _position = 0;
+};
diff --git a/thirdparty/tourist/trace/include/trace/detail/exceptions.h b/thirdparty/tourist/trace/include/trace/detail/exceptions.h
new file mode 100644
index 000000000..13d6f0e7c
--- /dev/null
+++ b/thirdparty/tourist/trace/include/trace/detail/exceptions.h
@@ -0,0 +1,23 @@
+#pragma once
+
+namespace Exception
+{
+
+//------------------------------------------------------------------------------
+struct StreamError
+{
+ StreamError(const char* msg, uint64 pos, uint64 val=0);
+ const char* message;
+ uint64 position;
+ uint64 value;
+};
+
+//------------------------------------------------------------------------------
+inline StreamError::StreamError(const char* msg, uint64 pos, uint64 val)
+: message(msg)
+, position(pos)
+, value(val)
+{
+}
+
+}; // namespace Exception
diff --git a/thirdparty/tourist/trace/include/trace/detail/preamble.h b/thirdparty/tourist/trace/include/trace/detail/preamble.h
new file mode 100644
index 000000000..d0fdf8d87
--- /dev/null
+++ b/thirdparty/tourist/trace/include/trace/detail/preamble.h
@@ -0,0 +1,23 @@
+#pragma once
+
+#include "data.h"
+
+class Allocator;
+class Protocol;
+class Transport;
+
+//------------------------------------------------------------------------------
+class Preamble
+{
+public:
+ Preamble(DataSource& source, Allocator& allocator);
+ Transport get_transport();
+ Protocol get_protocol();
+
+private:
+ void parse_header();
+ DataStream _stream;
+ bool _parsed = false;
+ uint8 _transport_version;
+ uint8 _protocol_version;
+};
diff --git a/thirdparty/tourist/trace/include/trace/detail/protocol.h b/thirdparty/tourist/trace/include/trace/detail/protocol.h
new file mode 100644
index 000000000..dd98c83a3
--- /dev/null
+++ b/thirdparty/tourist/trace/include/trace/detail/protocol.h
@@ -0,0 +1,64 @@
+#pragma once
+
+#include <foundation/buffer.h>
+
+class Type;
+class ProtocolImpl;
+class Bundle;
+
+//------------------------------------------------------------------------------
+struct AuxInfo
+{
+ uint32 size : 24;
+ uint32 index : 7;
+ uint32 partial : 1;
+};
+
+struct Aux
+ : public AuxInfo
+{
+ uint32 _unused;
+ Pointer data;
+};
+
+struct Event
+ : public NoCopy
+{
+ uint16 uid;
+ uint16 thread_id;
+ int32 serial;
+ Pointer data;
+ Vector<Aux> aux;
+};
+
+
+
+//------------------------------------------------------------------------------
+struct EventParcel
+ : public NoCopy
+{
+ void reset();
+ Vector<Event> events;
+ Vector<const Type*> new_types;
+
+private:
+ friend class ProtocolImpl;
+ Vector<BufferRef> buffer_refs;
+};
+
+
+
+//------------------------------------------------------------------------------
+class Protocol
+ : public NoCopy
+ , public NoMove
+{
+public:
+ explicit Protocol(uint8 version = 7);
+ ~Protocol();
+ void enable_unordered();
+ void read(EventParcel& parcel, Bundle& bundle);
+
+private:
+ ProtocolImpl* _impl = nullptr;
+};
diff --git a/thirdparty/tourist/trace/include/trace/detail/transport.h b/thirdparty/tourist/trace/include/trace/detail/transport.h
new file mode 100644
index 000000000..cde8e16f7
--- /dev/null
+++ b/thirdparty/tourist/trace/include/trace/detail/transport.h
@@ -0,0 +1,70 @@
+#pragma once
+
+#include <foundation/buffer.h>
+#include "data.h"
+
+//------------------------------------------------------------------------------
+struct Packet
+{
+public:
+ uint32 get_index() const;
+ uint32 get_thread_id() const;
+ uint32 is_compressed() const;
+ Buffer& get_payload();
+ void decompress();
+ static int32 decompress(Buffer& payload);
+
+private:
+ friend class Transport;
+ uint32 thread_id;
+ uint32 position;
+ uint32 index;
+ uint32 _unused;
+ Buffer payload;
+};
+
+//------------------------------------------------------------------------------
+using Packets = Span<Packet>;
+
+//------------------------------------------------------------------------------
+class Bundle
+ : public Span<Packet>
+{
+public:
+ using Span<Packet>::Span;
+ using Span<Packet>::operator =;
+ Bundle(Span<Packet>&& rhs) : Span<Packet>(std::move(rhs)) {}
+ explicit operator bool () const { return !empty(); }
+};
+
+//------------------------------------------------------------------------------
+class Transport
+ : public NoCopy
+ , public NoMove
+{
+public:
+ Bundle read_packets(const Bundle& bundle);
+ uint64 tell() const { return _stream.tell(); }
+
+private:
+ friend class Preamble;
+
+ Transport(DataStream&& stream);
+ Bundle _read_packets(const Bundle& bundle);
+ DataStream _stream;
+
+public:
+ struct Result
+ {
+ uint32 packet_count;
+ uint32 data_used;
+ };
+ Result read_packets(Packets packets, const Buffer& buffer);
+
+private:
+ enum class State : uint16 { HEADER, PAYLOAD };
+ State _state = State::HEADER;
+ uint16 _size;
+ uint16 _thread_id;
+ uint32 _packet_count = 0;
+};
diff --git a/thirdparty/tourist/trace/include/trace/detail/type.h b/thirdparty/tourist/trace/include/trace/detail/type.h
new file mode 100644
index 000000000..d48bb64db
--- /dev/null
+++ b/thirdparty/tourist/trace/include/trace/detail/type.h
@@ -0,0 +1,39 @@
+#pragma once
+
+//------------------------------------------------------------------------------
+class Type
+{
+public:
+ struct Field
+ {
+ uint8 _internal_type; // see patch()
+ uint8 __padding;
+ uint16 _offset;
+ uint16 size;
+ uint8 type_info;
+ uint8 name_size;
+ uint32 get_type_info() const;
+ uint32 get_offset() const;
+ uint32 get_size() const;
+ };
+
+ using TypeName = struct { StringView logger; StringView event; };
+ using FieldInfo = struct { StringView name; Field field; };
+
+ uint32 get_uid() const;
+ uint32 get_field_count() const;
+ Field get_field(uint32 index) const;
+ bool has_flag(uint32 flag) const;
+ TypeName get_name() const;
+ FieldInfo get_field_info(uint32 index) const;
+
+private:
+ friend class TypeDesc;
+ void patch();
+ uint16 _uid;
+ uint8 _field_count;
+ uint8 _flags;
+ uint8 _logger_name_len;
+ uint8 _event_name_len;
+ const Field _fields[];
+};
diff --git a/thirdparty/tourist/trace/include/trace/trace.h b/thirdparty/tourist/trace/include/trace/trace.h
new file mode 100644
index 000000000..ae5a93ddc
--- /dev/null
+++ b/thirdparty/tourist/trace/include/trace/trace.h
@@ -0,0 +1,8 @@
+#pragma once
+
+#include "detail/data.h"
+#include "detail/exceptions.h"
+#include "detail/preamble.h"
+#include "detail/protocol.h"
+#include "detail/transport.h"
+#include "detail/type.h"
diff --git a/thirdparty/tourist/trace/src/constants.h b/thirdparty/tourist/trace/src/constants.h
new file mode 100644
index 000000000..fa5c947f9
--- /dev/null
+++ b/thirdparty/tourist/trace/src/constants.h
@@ -0,0 +1,43 @@
+#pragma once
+
+//------------------------------------------------------------------------------
+inline void fatal(const char* message)
+{
+ throw std::runtime_error(message);
+}
+
+//------------------------------------------------------------------------------
+enum
+{
+ PACKET_FLAG_COMPRESSED = 0x8000,
+
+ EVENT_IMPORTANT_SIZE = sizeof(uint16) + sizeof(uint16),
+ EVENT_UID_WELL_KNOWN = 15,
+ EVENT_LARGE_UID_BIT = 1,
+
+ TID_SYNC = 0x3fff,
+ TID_TYPE = 0,
+ TID_IMPORTANT = 1,
+ TID_NORMAL = 2,
+ TID_COUNT_,
+
+ TYPE_FLAG_IMPORTANT = 0x01,
+ TYPE_FLAG_AUX = 0x02,
+ TYPE_FLAG_NO_SERIAL = 0x04,
+ TYPE_FLAG_DEFINITION = 0x08,
+
+ TYPE_INFO_SIZE_MASK = 0003, // 0007 ?!
+ TYPE_INFO_CAT_MASK = 0300,
+ TYPE_INFO_CAT_INT = 0000,
+ TYPE_INFO_CAT_FLOAT = 0100,
+ TYPE_INFO_CAT_ARRAY = 0200,
+ TYPE_INFO_SPECIAL_MASK = 0070,
+ TYPE_INFO_SPECIAL_STR = 0010,
+ TYPE_INFO_SPECIAL_SIGNED = 0020,
+ TYPE_INFO_SPECIAL_REF = 0030,
+ TYPE_INFO_SPECIAL_DEF_ID = 0040,
+
+ FIELD_INTERNAL_VALUE = 0,
+ FIELD_INTERNAL_REF = 1,
+ FIELD_INTERNAL_DEF_ID = 2,
+};
diff --git a/thirdparty/tourist/trace/src/data.cpp b/thirdparty/tourist/trace/src/data.cpp
new file mode 100644
index 000000000..da42284dc
--- /dev/null
+++ b/thirdparty/tourist/trace/src/data.cpp
@@ -0,0 +1,166 @@
+#include <foundation/platform.h>
+#include <foundation/types.h>
+#include <trace/detail/data.h>
+
+//------------------------------------------------------------------------------
+#ifdef _WIN32
+
+DataSource::DataSource(const Path& path)
+{
+ HANDLE handle = CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ,
+ nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
+ if (handle == INVALID_HANDLE_VALUE)
+ throw std::runtime_error("Failed to open file");
+
+ _handle = uintptr(handle);
+}
+
+DataSource::~DataSource()
+{
+ auto handle = HANDLE(_handle);
+ CloseHandle(handle);
+}
+
+int32 DataSource::read(void* out, uint32 size)
+{
+ auto handle = HANDLE(_handle);
+
+ DWORD bytes_read;
+ if (!ReadFile(handle, out, size, &bytes_read, nullptr))
+ throw std::runtime_error("Read error");
+
+ return bytes_read;
+}
+
+int64 DataSource::get_size() const
+{
+ if (_size >= 0)
+ return _size;
+
+ auto handle = HANDLE(_handle);
+
+ LARGE_INTEGER out;
+ GetFileSizeEx(handle, &out);
+ return _size = out.QuadPart;
+}
+
+#else // POSIX
+
+DataSource::DataSource(const Path& path)
+{
+ int fd = ::open(path.string().c_str(), O_RDONLY);
+ if (fd < 0)
+ throw std::runtime_error("Failed to open file");
+
+ _handle = uintptr(fd);
+}
+
+DataSource::~DataSource()
+{
+ ::close(int(_handle));
+}
+
+int32 DataSource::read(void* out, uint32 size)
+{
+ ssize_t bytes_read = ::read(int(_handle), out, size);
+ if (bytes_read < 0)
+ throw std::runtime_error("Read error");
+
+ return int32(bytes_read);
+}
+
+int64 DataSource::get_size() const
+{
+ if (_size >= 0)
+ return _size;
+
+ struct stat st;
+ if (fstat(int(_handle), &st) < 0)
+ return _size = 0;
+
+ return _size = st.st_size;
+}
+
+#endif
+
+
+
+//------------------------------------------------------------------------------
+DataStream::DataStream(DataSource& data_source, Allocator& allocator)
+: _data_source(data_source)
+, _allocator(allocator)
+{
+}
+
+//------------------------------------------------------------------------------
+uint64 DataStream::tell() const
+{
+ return _position;
+}
+
+//------------------------------------------------------------------------------
+uint32 DataStream::get_available() const
+{
+ return _stream.get_remaining();
+}
+
+//------------------------------------------------------------------------------
+Buffer DataStream::read(uint32 size)
+{
+ const uint8* ptr = _read(size);
+ return _buffer.create_sub_buffer(ptr, size);
+}
+
+//------------------------------------------------------------------------------
+const uint8* DataStream::_read(uint32 size)
+{
+ _position += size;
+
+ if (_stream.get_remaining() >= size)
+ return _stream.read(size);
+
+ MutableBuffer buffer = _allocator.create_buffer(BUFFER_SIZE);
+ uint8* cursor = buffer.get_pointer();
+ uint32 buffer_size = buffer.get_size();
+
+ if (uint32 remaining = _stream.get_remaining())
+ {
+ const void* src = _stream.read(remaining);
+ std::memcpy(cursor, src, remaining);
+ cursor += remaining;
+ buffer_size -= remaining;
+ }
+
+ uint32 read_size = 0;
+ while (true)
+ {
+ int32 i = _data_source.read(cursor + read_size, buffer_size - read_size);
+ if (i <= 0)
+ throw Eof();
+
+ if ((read_size += i) >= size)
+ break;
+ }
+
+ if (read_size < buffer_size)
+ {
+ buffer_size = BUFFER_SIZE - buffer_size + read_size;
+ _buffer = buffer.create_sub_buffer(0u, buffer_size);
+ }
+ else
+ _buffer = std::move(buffer);
+
+ _stream = _buffer.create_stream();
+
+ return _stream.read(size);
+}
+
+//------------------------------------------------------------------------------
+template <> int8 DataStream::read< int8 >() { return *( int8 *)_read(sizeof( int8 )); }
+template <> int16 DataStream::read< int16>() { return *( int16*)_read(sizeof( int16)); }
+template <> int32 DataStream::read< int32>() { return *( int32*)_read(sizeof( int32)); }
+template <> int64 DataStream::read< int64>() { return *( int64*)_read(sizeof( int64)); }
+template <> uint8 DataStream::read<uint8 >() { return *(uint8 *)_read(sizeof(uint8 )); }
+template <> uint16 DataStream::read<uint16>() { return *(uint16*)_read(sizeof(uint16)); }
+template <> uint32 DataStream::read<uint32>() { return *(uint32*)_read(sizeof(uint32)); }
+template <> uint64 DataStream::read<uint64>() { return *(uint64*)_read(sizeof(uint64)); }
diff --git a/thirdparty/tourist/trace/src/preamble.cpp b/thirdparty/tourist/trace/src/preamble.cpp
new file mode 100644
index 000000000..7b54dbc8e
--- /dev/null
+++ b/thirdparty/tourist/trace/src/preamble.cpp
@@ -0,0 +1,55 @@
+#include <foundation/types.h>
+
+#include <trace/detail/exceptions.h>
+#include <trace/detail/preamble.h>
+#include <trace/detail/protocol.h>
+#include <trace/detail/transport.h>
+
+//------------------------------------------------------------------------------
+Preamble::Preamble(DataSource& source, Allocator& allocator)
+: _stream(source, allocator)
+{
+}
+
+//------------------------------------------------------------------------------
+Transport Preamble::get_transport()
+{
+ if (!_parsed)
+ parse_header();
+
+ return Transport(std::move(_stream));
+}
+
+//------------------------------------------------------------------------------
+Protocol Preamble::get_protocol()
+{
+ if (!_parsed)
+ parse_header();
+
+ return Protocol(_protocol_version);
+}
+
+//------------------------------------------------------------------------------
+void Preamble::parse_header()
+{
+ // magic
+ uint32 magic = _stream.read<uint32>();
+ if (magic != 'TRC2')
+ throw Exception::StreamError("Unexpected magic value", 0, magic);
+
+ // meta
+ uint32 meta_size = _stream.read<uint16>();
+ _stream.read(meta_size);
+
+ // transport
+ _transport_version = _stream.read<uint8>();
+ if (_transport_version != 4)
+ throw Exception::StreamError("Unexpected transport version", 0, _transport_version);
+
+ // protocol
+ _protocol_version = _stream.read<uint8>();
+ if (_protocol_version != 6 && _protocol_version != 7)
+ throw Exception::StreamError("Unexpected protocol version", 0, _protocol_version);
+
+ _parsed = true;
+}
diff --git a/thirdparty/tourist/trace/src/protocol.cpp b/thirdparty/tourist/trace/src/protocol.cpp
new file mode 100644
index 000000000..5297048ec
--- /dev/null
+++ b/thirdparty/tourist/trace/src/protocol.cpp
@@ -0,0 +1,850 @@
+#include <foundation/types.h>
+#include <foundation/buffer.h>
+#include <trace/detail/protocol.h>
+#include <trace/detail/transport.h>
+#include <trace/detail/type.h>
+
+#include "constants.h"
+
+//------------------------------------------------------------------------------
+class TypeDesc
+{
+public:
+ uint16 size : 13;
+ uint16 important : 1;
+ uint16 has_serial : 1;
+ uint16 maybe_aux : 1;
+ uint16 _unused[3];
+ const Type* type;
+ static Tuple<uint32, TypeDesc> parse(BufferStream& stream);
+};
+
+//------------------------------------------------------------------------------
+Tuple<uint32, TypeDesc> TypeDesc::parse(BufferStream& stream)
+{
+ uint32 zero_uid = stream.read<uint16>();
+ if (zero_uid != 0)
+ fatal("non-zero type uid");
+
+ uint32 info_size = stream.read<uint16>();
+ const uint8* type_info = stream.read(info_size);
+ auto* type = (Type*)type_info;
+ type->patch();
+
+ uint32 uid = type->get_uid();
+
+ uint32 type_size = 0;
+ for (uint32 i = 0, n = type->get_field_count(); i < n; ++i)
+ type_size += type->get_field(i).get_size();
+
+ bool important_ = type->has_flag(TYPE_FLAG_IMPORTANT);
+ bool has_serial_ = !type->has_flag(TYPE_FLAG_NO_SERIAL);
+ bool maybe_aux_ = type->has_flag(TYPE_FLAG_AUX);
+
+ TypeDesc desc = {
+ uint16(type_size),
+ important_,
+ has_serial_,
+ maybe_aux_,
+ {},
+ type
+ };
+ return { uid, desc };
+}
+
+
+
+//------------------------------------------------------------------------------
+class Types
+{
+public:
+ void parse(Buffer& buffer, Vector<const Type*>& new_types);
+ const TypeDesc* lookup(uint32 uid) const;
+
+private:
+ Vector<BufferRef> _buffer_refs;
+ Vector<TypeDesc> _descs;
+};
+
+//------------------------------------------------------------------------------
+void Types::parse(Buffer& buffer, Vector<const Type*>& new_types)
+{
+ BufferStream stream = buffer.create_stream();
+ do
+ {
+ auto [uid, desc] = TypeDesc::parse(stream);
+ if (_descs.size() <= uid)
+ {
+ uint32 new_size = (uid + 32) & ~31;
+ _descs.resize(new_size, TypeDesc{});
+ }
+ _descs[uid] = desc;
+
+ new_types.push_back(desc.type);
+ }
+ while (stream.has_data());
+
+ BufferRef buffer_ref = buffer.create_ref();
+ _buffer_refs.push_back(std::move(buffer_ref));
+}
+
+//------------------------------------------------------------------------------
+const TypeDesc* Types::lookup(uint32 uid) const
+{
+ if (uid >= _descs.size())
+ return nullptr;
+
+ const TypeDesc* ret = _descs.data() + uid;
+ return (ret->type != nullptr) ? ret : nullptr;
+}
+
+
+
+//------------------------------------------------------------------------------
+class Serial
+{
+public:
+ enum NoSync : uint32 { NO_SYNC = 0x0100'0000 };
+ enum Pending : uint32 { PENDING = 0xff00'0000 };
+
+ Serial() {}
+ explicit Serial(int32 v) : _v(v) {}
+ explicit Serial(NoSync) : _v(NO_SYNC) {}
+ explicit Serial(Pending) : _v(PENDING) {}
+ bool is_sync() const { return _v < NO_SYNC; }
+ int32 get_value() const { return _v & 0x00ff'ffff; }
+ explicit operator bool () const { return _v <= NO_SYNC; }
+ bool operator == (Pending) const { return _v == PENDING; }
+ bool operator == (Serial rhs) const { return !(_v - rhs._v); }
+ bool operator != (Serial rhs) const { return ! operator == (rhs); }
+ void operator ++ () { _v = (_v + 1) & 0x00ff'ffff; }
+
+ bool less(Serial lhs, Serial rhs) const
+ {
+ return (!lhs || !rhs)
+ ? lhs._v < rhs._v
+ : (lhs._v - _v) < (rhs._v - _v);
+ }
+
+private:
+ uint32 _v = 0xffff'ffff;
+};
+
+//------------------------------------------------------------------------------
+class EventParser
+ : public NoCopy
+{
+public:
+ EventParser() = default;
+ ~EventParser();
+ EventParser(EventParser&& rhs) { move(std::move(rhs)); }
+ EventParser& operator = (EventParser&& rhs) { move(std::move(rhs)); return *this; }
+ Serial parse_normal(BufferStream& stream, const Types& types);
+ Serial parse_important(BufferStream& stream, const Types& types);
+ Event consume();
+ void pin();
+ bool is_empty() const { return _stack.empty(); }
+
+private:
+ struct State
+ {
+ BufferStream& stream;
+ const Types& types;
+ };
+
+ void move(EventParser&& rhs);
+ Serial parse_normal(const State& state);
+ Serial parse_important(const State& state);
+ Serial parse_continue(const State& state);
+ Serial parse_uid(const State& state);
+ Serial parse_type(const State& state);
+ Serial parse_well_known(const State& state, uint32 uid);
+ void parse_aux(const State& state);
+ Serial parse_important_aux(const State& state);
+
+ struct StackItem
+ : public Event
+ {
+ Serial serial;
+ };
+
+ friend class ProtocolImpl;
+ Vector<StackItem> _stack;
+ MutableBuffer _fragment;
+ uint32 _missing = 0;
+ int32 _stage = 0;
+ uint32 _last_uid = 0;
+ uint8 _protocol_version = 7;
+};
+
+//------------------------------------------------------------------------------
+EventParser::~EventParser()
+{
+ // Don't throw from destructors — this causes std::terminate during
+ // stack unwinding (e.g. when the trace stream ends mid-parse).
+}
+
+//------------------------------------------------------------------------------
+void EventParser::move(EventParser&& rhs)
+{
+ std::swap(_stack, rhs._stack);
+ std::swap(_fragment, rhs._fragment);
+ std::swap(_missing, rhs._missing);
+ std::swap(_stage, rhs._stage);
+ std::swap(_last_uid, rhs._last_uid);
+ std::swap(_protocol_version, rhs._protocol_version);
+}
+
+//------------------------------------------------------------------------------
+Serial EventParser::parse_normal(BufferStream& stream, const Types& types)
+{
+ State state = { stream, types };
+ return parse_normal(state);
+}
+
+//------------------------------------------------------------------------------
+Serial EventParser::parse_important(BufferStream& stream, const Types& types)
+{
+ if (_missing == 0)
+ {
+ State state = { stream, types };
+ Serial ret = parse_important(state);
+ if (!ret && _stage != 1 && _missing == 0)
+ fatal("important parse should only fail on unknown uid");
+ return ret;
+ }
+
+ uint32 remaining = stream.get_remaining();
+ uint32 read_size = std::min(remaining, _missing);
+ uint8* dest = _fragment.get_pointer() + _fragment.get_size() - _missing;
+ std::memcpy(dest, stream.read(read_size), read_size);
+ if (_missing -= read_size)
+ return Serial();
+
+ BufferStream missing_stream = _fragment.create_stream();
+ State state = { missing_stream, types };
+
+ if (!_stack.empty())
+ return parse_important(state);
+
+ parse_important(state);
+ _missing = 0;
+ return parse_important(stream, types);
+}
+
+//------------------------------------------------------------------------------
+Event EventParser::consume()
+{
+ _stage = 0;
+ Event event = std::move(_stack.back());
+ _stack.pop_back();
+ return event;
+}
+
+//------------------------------------------------------------------------------
+void EventParser::pin()
+{
+ for (StackItem& item : _stack)
+ {
+ if (item.data.is_valid())
+ item.data.pin();
+
+ for (Aux& aux : item.aux)
+ aux.data.pin();
+ }
+}
+
+//------------------------------------------------------------------------------
+Serial EventParser::parse_normal(const State& state)
+{
+ switch (_stage)
+ {
+ case 0: return parse_uid(state);
+ case 1: return parse_type(state);
+ default: fatal("unexpected _stage value");
+ }
+ return Serial();
+}
+
+//------------------------------------------------------------------------------
+Serial EventParser::parse_important(const State& state)
+{
+ if (_stage == 1)
+ return parse_type(state);
+
+ BufferStream& stream = state.stream;
+
+ auto ok_or_capture_fragment = [this, &stream] (uint32 required) {
+ uint32 remaining = stream.get_remaining();
+ if (remaining >= required)
+ return true;
+
+ if (required >= (64 << 10)) // size field is uint16 so 64 KB is the hard upper bound
+ fatal("an important event seems to be rather too large");
+
+ Allocator& allocator = Allocator::get_from(stream);
+ _fragment = allocator.create_buffer(required);
+ std::memcpy(
+ _fragment.get_pointer(),
+ stream.read(remaining),
+ remaining
+ );
+ _missing = required - remaining;
+
+ return false;
+ };
+
+ if (!ok_or_capture_fragment(EVENT_IMPORTANT_SIZE))
+ return Serial(Serial::PENDING);
+
+ _stage = 1;
+
+ Event& top = _stack.emplace_back();
+ top.uid = stream.read<uint16>();
+
+ uint32 size = stream.read<uint16>();
+ if (!ok_or_capture_fragment(size))
+ return Serial(Serial::PENDING);
+
+ // Track how many bytes parse_type consumes so we can skip any
+ // remaining bytes in the declared important-event payload. The UE
+ // writer may include trailing data (e.g. attachment metadata) that
+ // our type parser does not consume.
+ uint32 before = stream.get_remaining();
+ Serial ret = parse_type(state);
+ uint32 consumed = before - stream.get_remaining();
+ if (consumed < size)
+ stream.read(size - consumed);
+
+ return ret;
+}
+
+//------------------------------------------------------------------------------
+Serial EventParser::parse_continue(const State& state)
+{
+ _stage = 0;
+ if (state.stream.has_data())
+ return parse_uid(state);
+
+ return _stack.empty() ? Serial() : Serial(Serial::PENDING);
+}
+
+//------------------------------------------------------------------------------
+Serial EventParser::parse_uid(const State& state)
+{
+ BufferStream& stream = state.stream;
+
+ uint32 uid = stream.read<uint8>();
+ if (uid & EVENT_LARGE_UID_BIT)
+ uid |= uint32(stream.read<uint8>()) << 8;
+ _last_uid = uid >>= 1;
+
+ if (uid <= EVENT_UID_WELL_KNOWN)
+ return parse_well_known(state, uid);
+
+ _stack.emplace_back().uid = uint16(uid);
+
+ _stage = 1;
+ return parse_type(state);
+}
+
+//------------------------------------------------------------------------------
+Serial EventParser::parse_type(const State& state)
+{
+ StackItem& top = _stack.back();
+
+ const TypeDesc* type_desc = state.types.lookup(top.uid);
+ if (type_desc == nullptr)
+ return Serial(Serial::PENDING);
+
+ BufferStream& stream = state.stream;
+
+ Serial serial(Serial::NO_SYNC);
+ if (type_desc->has_serial)
+ {
+ uint32 low_serial = stream.read<uint8>();
+ uint32 high_serial = stream.read<uint16>();
+ serial = Serial((high_serial << 8) | low_serial);
+ }
+
+ uint32 event_size = type_desc->size;
+ top.data = stream.read_ptr(event_size);
+ top.serial = serial;
+
+ if (type_desc->maybe_aux)
+ return type_desc->important
+ ? parse_important_aux(state)
+ : parse_continue(state);
+
+ _stage = -1;
+ return serial;
+}
+
+//------------------------------------------------------------------------------
+Serial EventParser::parse_well_known(const State& state, uint32 uid)
+{
+ BufferStream& stream = state.stream;
+
+ // AuxData
+ if (uid == 1)
+ {
+ parse_aux(state);
+ return parse_continue(state);
+ }
+
+ // AuxDataTerminal
+ if (uid == 3)
+ {
+ _stage = -1;
+ return Serial(_stack.back().serial);
+ }
+
+ // EnterScope
+ if (uid == 4)
+ return parse_continue(state);
+
+ // LeaveScope
+ if (uid == 5)
+ return parse_continue(state);
+
+ if (_protocol_version >= 7)
+ {
+ // EnterScope_T (protocol 7)
+ if (uid == 6 || uid == 8)
+ {
+ /*const uint8* timestamp =*/ stream.read(7);
+ return parse_continue(state);
+ }
+
+ // LeaveScope_T (protocol 7)
+ if (uid == 7 || uid == 9)
+ {
+ /*const uint8* timestamp =*/ stream.read(7);
+ return parse_continue(state);
+ }
+ }
+ else
+ {
+ // EnterScope_T (protocol 6)
+ if (uid == 8 || uid == 12)
+ {
+ /*const uint8* timestamp =*/ stream.read(7);
+ return parse_continue(state);
+ }
+ }
+
+ fatal("Unexpected uid");
+ return Serial();
+}
+
+//------------------------------------------------------------------------------
+void EventParser::parse_aux(const State& state)
+{
+ uint32 low_size = state.stream.read<uint8>();
+ uint32 high_size = state.stream.read<uint16>();
+ uint32 size = (low_size | (high_size << 8)) >> 5;
+
+ Event& top = _stack.back();
+ Aux& aux = top.aux.emplace_back();
+ aux.size = size;
+ aux.index = low_size & 0x1f;
+ aux.partial = 0;
+ if (top.aux.size() > 1)
+ {
+ Aux& prev = *(top.aux.rbegin() + 1);
+ prev.partial = (prev.index == aux.index);
+ }
+
+ BufferStream& stream = state.stream;
+ uint32 remaining = stream.get_remaining();
+ if (remaining < size)
+ fatal("aux size too large");
+
+ aux.data = stream.read_ptr(size);
+}
+
+//------------------------------------------------------------------------------
+Serial EventParser::parse_important_aux(const State& state)
+{
+ uint32 uid = state.stream.read<uint8>();
+
+ if (uid == 1) // AuxData
+ {
+ parse_aux(state);
+ return parse_important_aux(state);
+ }
+
+ if (uid == 3) // AuxDataTerminal
+ {
+ _stage = -1;
+ return Serial(Serial::NO_SYNC);
+ }
+
+ fatal("unsupported important sub-uid");
+ return Serial();
+}
+
+
+//------------------------------------------------------------------------------
+class PacketNodePool
+{
+public:
+ struct PacketNode
+ {
+ PacketNode* get_next() const { return (PacketNode*)next; }
+ void set_next(PacketNode* n) { next = uintptr(n); }
+ Buffer payload;
+ uintptr compressed : 1;
+ uintptr _unused : 15;
+ uintptr next : 48;
+ };
+
+ ~PacketNodePool();
+ PacketNode* alloc_pnode();
+ void free_pnode(PacketNode* node);
+
+private:
+ PacketNode* _free_nodes = nullptr;
+};
+
+//------------------------------------------------------------------------------
+PacketNodePool::~PacketNodePool()
+{
+ while (_free_nodes != nullptr)
+ {
+ PacketNode* next = _free_nodes->get_next();
+ delete _free_nodes;
+ _free_nodes = next;
+ }
+}
+
+//------------------------------------------------------------------------------
+PacketNodePool::PacketNode* PacketNodePool::alloc_pnode()
+{
+ if (_free_nodes == nullptr)
+ return new PacketNode();
+
+ PacketNode* ret = _free_nodes;
+ _free_nodes = ret->get_next();
+ return new (ret) PacketNode();
+}
+
+//------------------------------------------------------------------------------
+void PacketNodePool::free_pnode(PacketNode* node)
+{
+ node->payload = Buffer();
+ node->set_next(_free_nodes);
+ _free_nodes = node;
+}
+
+
+
+//------------------------------------------------------------------------------
+class ParserPool
+{
+public:
+ EventParser& get_parser(uint32 index);
+ uint16 alloc_parser();
+ void free_parser(uint32 index);
+
+private:
+ Vector<EventParser> _parsers;
+ Vector<uint16> _frees;
+};
+
+//------------------------------------------------------------------------------
+EventParser& ParserPool::get_parser(uint32 index)
+{
+ return _parsers[index];
+}
+
+//------------------------------------------------------------------------------
+uint16 ParserPool::alloc_parser()
+{
+ if (_frees.empty())
+ {
+ _parsers.emplace_back();
+ return uint16(_parsers.size() - 1);
+ }
+
+ uint16 index = _frees.back();
+ _frees.pop_back();
+ return index;
+}
+
+//------------------------------------------------------------------------------
+void ParserPool::free_parser(uint32 index)
+{
+ _parsers[index] = EventParser();
+ _frees.push_back(uint16(index));
+}
+
+
+
+//------------------------------------------------------------------------------
+class ProtocolImpl
+ : public PacketNodePool
+ , protected ParserPool
+ , public NoCopy
+ , public NoMove
+{
+public:
+ explicit ProtocolImpl(uint8 protocol_version);
+ void enable_unordered();
+ void read(EventParcel& parcel, Bundle& bundle);
+
+private:
+ struct Thread
+ {
+ Serial serial;
+ uint16 id;
+ uint16 parser_index;
+ PacketNode* head = nullptr;
+ PacketNode* tail = nullptr;
+ };
+
+ template <bool> bool read(EventParcel& parcel, Thread& thread);
+ void scatter(EventParcel& parcel, Packet& packet);
+ EventParser& get_parser(const Thread& thread);
+ Types _types;
+ Vector<Thread> _threads;
+ Serial _next_serial = Serial(0);
+ bool _serialised = true;
+ uint8 _protocol_version;
+};
+
+//------------------------------------------------------------------------------
+ProtocolImpl::ProtocolImpl(uint8 protocol_version)
+: _protocol_version(protocol_version)
+{
+ Thread& thread = _threads.emplace_back();
+ thread.id = TID_IMPORTANT;
+ thread.parser_index = alloc_parser();
+ get_parser(thread)._protocol_version = protocol_version;
+}
+
+//------------------------------------------------------------------------------
+void ProtocolImpl::enable_unordered()
+{
+ _serialised = false;
+}
+
+//------------------------------------------------------------------------------
+void ProtocolImpl::read(EventParcel& parcel, Bundle& bundle)
+{
+ // scatter packets to their respective threads
+ for (Packet& packet : bundle)
+ scatter(parcel, packet);
+
+ // important
+ read<false>(parcel, _threads[0]);
+
+ // read as many events as we can into the parcel
+ for (uint32 i = 1, n = uint32(_threads.size()); i < n; ++i)
+ {
+ Thread& thread = _threads[i];
+ if (thread.serial.is_sync())
+ continue;
+
+ if (!read<true>(parcel, thread))
+ continue;
+
+ thread = std::move(_threads.back());
+ _threads.pop_back();
+ --i, --n;
+ }
+
+ for (const bool do_gather = _serialised; do_gather;)
+ {
+ Serial prev_serial = _next_serial;
+ for (uint32 i = 1, n = uint32(_threads.size()); i < n; ++i)
+ {
+ Thread& thread = _threads[i];
+ if (thread.serial != _next_serial)
+ continue;
+
+ ++_next_serial;
+
+ Event event = get_parser(thread).consume();
+ event.thread_id = thread.id;
+ event.serial = thread.serial.is_sync() ? thread.serial.get_value() : -1;
+ parcel.events.push_back(std::move(event));
+
+ if (!read<true>(parcel, thread))
+ continue;
+
+ thread = std::move(_threads.back());
+ _threads.pop_back();
+ --i, --n;
+ }
+
+ if (prev_serial == _next_serial)
+ break;
+ }
+
+ for (Thread& thread : _threads)
+ get_parser(thread).pin();
+}
+
+//------------------------------------------------------------------------------
+void ProtocolImpl::scatter(EventParcel& parcel, Packet& packet)
+{
+ uint32 thread_id = packet.get_thread_id();
+
+ if (thread_id == TID_SYNC)
+ return;
+
+ Buffer& payload = packet.get_payload();
+
+ if (thread_id == TID_TYPE)
+ {
+ packet.decompress();
+ _types.parse(payload, parcel.new_types);
+ return;
+ }
+
+ PacketNode* node = alloc_pnode();
+ node->payload = std::move(payload);
+ node->compressed = packet.is_compressed();
+
+ Thread* thread = (thread_id == TID_IMPORTANT) ? _threads.data() : nullptr;
+ for (int32 i = 1, n = uint32(_threads.size()); i < n && !thread; ++i)
+ if (Thread& lookup = _threads[i]; lookup.id == thread_id)
+ thread = &lookup;
+
+ if (thread == nullptr)
+ {
+ thread = &(_threads.emplace_back());
+ thread->id = uint16(thread_id);
+ thread->parser_index = alloc_parser();
+ get_parser(*thread)._protocol_version = _protocol_version;
+ }
+
+ if (thread->tail != nullptr)
+ {
+ thread->tail->set_next(node);
+ thread->tail = node;
+ }
+ else
+ thread->head = thread->tail = node;
+}
+
+//------------------------------------------------------------------------------
+template <bool is_normal>
+bool ProtocolImpl::read(EventParcel& parcel, Thread& thread)
+{
+ EventParser& parser = get_parser(thread);
+
+ PacketNode* node = thread.head;
+ while (node != nullptr)
+ {
+ if (node->compressed)
+ {
+ Packet::decompress(node->payload);
+ node->compressed = 0;
+ }
+
+ BufferRef buffer_ref = node->payload.create_ref();
+ parcel.buffer_refs.push_back(std::move(buffer_ref));
+
+ BufferStream stream = node->payload.create_stream();
+ while (true)
+ {
+ Serial serial = is_normal
+ ? parser.parse_normal(stream, _types)
+ : parser.parse_important(stream, _types);
+
+ thread.serial = serial;
+
+ if (!serial)
+ break;
+
+ if (int32 cond = is_normal && serial.is_sync(); cond)
+ {
+ if ((_serialised == true) & (serial != _next_serial))
+ break;
+
+ ++_next_serial;
+ thread.serial = Serial(Serial::NO_SYNC);
+ }
+
+ Event event = parser.consume();
+ event.thread_id = thread.id;
+ event.serial = serial.is_sync() ? serial.get_value() : -1;
+ parcel.events.push_back(std::move(event));
+
+ if (!stream.has_data())
+ break;
+ }
+
+ if (stream.has_data())
+ {
+ Buffer& buffer = node->payload;
+ node->payload = buffer.create_sub_buffer(stream.get_consumed());
+ thread.head = node;
+ return false;
+ }
+
+ PacketNode* next = node->get_next();
+ free_pnode(node);
+
+ if ((thread.head = node = next) == nullptr)
+ thread.tail = nullptr;
+
+ if (bool cond = is_normal; cond)
+ if (thread.serial.is_sync())
+ return false;
+ }
+
+ if (bool cond = !is_normal; cond)
+ return false;
+
+ if (thread.serial == Serial::PENDING)
+ return false;
+
+ free_parser(thread.parser_index);
+ thread.serial = Serial();
+ return true;
+}
+
+//------------------------------------------------------------------------------
+EventParser& ProtocolImpl::get_parser(const Thread& thread)
+{
+ return ParserPool::get_parser(thread.parser_index);
+}
+
+
+
+//------------------------------------------------------------------------------
+void EventParcel::reset()
+{
+ events.clear();
+ new_types.clear();
+ buffer_refs.clear();
+}
+
+
+
+//------------------------------------------------------------------------------
+Protocol::Protocol(uint8 version)
+{
+ _impl = new ProtocolImpl(version);
+}
+
+//------------------------------------------------------------------------------
+Protocol::~Protocol()
+{
+ delete _impl;
+}
+
+//------------------------------------------------------------------------------
+void Protocol::enable_unordered()
+{
+ return _impl->enable_unordered();
+}
+
+//------------------------------------------------------------------------------
+void Protocol::read(EventParcel& parcel, Bundle& bundle)
+{
+ return _impl->read(parcel, bundle);
+}
diff --git a/thirdparty/tourist/trace/src/transport.cpp b/thirdparty/tourist/trace/src/transport.cpp
new file mode 100644
index 000000000..7aff3b25c
--- /dev/null
+++ b/thirdparty/tourist/trace/src/transport.cpp
@@ -0,0 +1,183 @@
+#include <foundation/types.h>
+#include <trace/detail/exceptions.h>
+#include <trace/detail/transport.h>
+
+#include "constants.h"
+
+#include <lz4.h>
+
+//------------------------------------------------------------------------------
+uint32 Packet::get_index() const
+{
+ return index;
+}
+
+//------------------------------------------------------------------------------
+uint32 Packet::get_thread_id() const
+{
+ return thread_id & ~PACKET_FLAG_COMPRESSED;
+}
+
+//------------------------------------------------------------------------------
+uint32 Packet::is_compressed() const
+{
+ return !!(thread_id & PACKET_FLAG_COMPRESSED);
+}
+
+//------------------------------------------------------------------------------
+Buffer& Packet::get_payload()
+{
+ return payload;
+}
+
+//------------------------------------------------------------------------------
+void Packet::decompress()
+{
+ if (!is_compressed())
+ return;
+
+ thread_id &= ~PACKET_FLAG_COMPRESSED;
+ if (int32 ret_code = decompress(payload); ret_code != 0)
+ throw Exception::StreamError("Lz4 decompress failure", position, ret_code);
+}
+
+//------------------------------------------------------------------------------
+int32 Packet::decompress(Buffer& payload)
+{
+ struct Lz4Block
+ {
+ uint16 decoded_size;
+ char data[];
+ };
+ const auto& lz4_block = *(Lz4Block*)(payload.get_pointer());
+ uint32 encoded_size = payload.get_size() - sizeof(uint16);
+
+ Allocator& allocator = Allocator::get_from(payload);
+ MutableBuffer buffer = allocator.create_buffer(lz4_block.decoded_size);
+ int lz4_ret = LZ4_decompress_safe(
+ lz4_block.data,
+ (char*)(buffer.get_pointer()),
+ encoded_size,
+ lz4_block.decoded_size
+ );
+ if (lz4_ret != lz4_block.decoded_size)
+ return lz4_ret;
+
+ payload = std::move(buffer);
+ return 0;
+}
+
+
+
+//------------------------------------------------------------------------------
+Bundle Transport::read_packets(const Bundle& bundle)
+{
+ Bundle ret;
+ try
+ {
+ ret = _read_packets(bundle);
+ }
+ catch (const DataStream::Eof&) {}
+
+ return ret;
+}
+
+//------------------------------------------------------------------------------
+Transport::Result Transport::read_packets(Packets packets, const Buffer& buffer)
+{
+ BufferStream stream = buffer.create_stream();
+
+ uint32 count = 0;
+ uint32 position = 0;
+
+ uint32 available = stream.get_remaining();
+ switch (_state)
+ {
+ while (available >= 4)
+ {
+ case State::HEADER:
+ _size = stream.read<uint16>();
+ _thread_id = stream.read<uint16>();
+
+ if (_size < 4)
+ throw Exception::StreamError("Unexpected size", position, _size);
+
+ available -= 4;
+ if (available < _size)
+ {
+ _state = State::PAYLOAD;
+ return { count, position };
+ }
+
+ case State::PAYLOAD:
+ Buffer payload;
+ uint32 payload_size = _size - sizeof(uint16) - sizeof(uint16);
+ payload = stream.read_buf(payload_size);
+
+ Packet& packet = packets[count];
+ packet.thread_id = _thread_id;
+ packet.position = position;
+ packet.index = _packet_count;
+ packet.payload = std::move(payload);
+ ++_packet_count;
+
+ position += _size;
+
+ ++count;
+ if (count == packets.size())
+ break;
+
+ available -= _size;
+ }
+ }
+
+ _state = State::HEADER;
+ return { count, position };
+}
+
+//------------------------------------------------------------------------------
+Transport::Transport(DataStream&& stream)
+: _stream(std::move(stream))
+{
+}
+
+//------------------------------------------------------------------------------
+Bundle Transport::_read_packets(const Bundle& bundle)
+{
+ int32 count = 0;
+ for (int32 available = _stream.get_available();;)
+ {
+ uint32 size = _stream.read<uint16>();
+ if (size < 4)
+ throw Exception::StreamError("Unexpected size", _stream.tell() - 2, size);
+
+ uint32 thread_id = _stream.read<uint16>();
+ uint64 position = _stream.tell();
+
+ Buffer payload;
+ uint32 payload_size = size - sizeof(uint16) - sizeof(uint16);
+ payload = _stream.read(payload_size);
+
+ Packet& packet = bundle[count];
+ packet.thread_id = thread_id;
+ packet.position = uint32(position);
+ packet.index = _packet_count;
+ packet.payload = std::move(payload);
+
+ ++_packet_count;
+ ++count;
+
+ available -= size;
+ if (available <= 4)
+ break;
+
+ if (count == bundle.size())
+ break;
+ }
+
+ for (Packet& packet : bundle.subspan(count))
+ packet = Packet();
+
+ auto ret = bundle.subspan(0, count);
+ return ret;
+}
diff --git a/thirdparty/tourist/trace/src/types.cpp b/thirdparty/tourist/trace/src/types.cpp
new file mode 100644
index 000000000..7327b34f3
--- /dev/null
+++ b/thirdparty/tourist/trace/src/types.cpp
@@ -0,0 +1,96 @@
+#include <foundation/types.h>
+#include <foundation/buffer.h>
+#include <trace/detail/type.h>
+
+#include "constants.h"
+
+//------------------------------------------------------------------------------
+uint32 Type::Field::get_type_info() const
+{
+ return type_info;
+}
+
+//------------------------------------------------------------------------------
+uint32 Type::Field::get_offset() const
+{
+ return _offset;
+}
+
+//------------------------------------------------------------------------------
+uint32 Type::Field::get_size() const
+{
+ return (type_info & TYPE_INFO_CAT_ARRAY) ? 0 : 1 << (type_info & 0003);
+}
+
+//------------------------------------------------------------------------------
+uint32 Type::get_uid() const
+{
+ return _uid;
+}
+
+//------------------------------------------------------------------------------
+uint32 Type::get_field_count() const
+{
+ return _field_count;
+}
+
+//------------------------------------------------------------------------------
+Type::Field Type::get_field(uint32 index) const
+{
+ return get_field_info(index).field;
+}
+
+//------------------------------------------------------------------------------
+bool Type::has_flag(uint32 flag) const
+{
+ return !!(_flags & flag);
+}
+
+//------------------------------------------------------------------------------
+Type::TypeName Type::get_name() const
+{
+ const auto* base = (const char*)(_fields + _field_count);
+ StringView logger_name(base, _logger_name_len);
+ base += _logger_name_len;
+ StringView event_name(base, _event_name_len);
+ return { logger_name, event_name };
+}
+
+//------------------------------------------------------------------------------
+Type::FieldInfo Type::get_field_info(uint32 index) const
+{
+ Field field = _fields[index];
+
+ // :(
+ const auto* base = (const char*)(_fields + _field_count);
+ base += _logger_name_len + _event_name_len;
+ for (const Field* cursor = _fields; index-- > 0; ++cursor)
+ base += cursor->name_size;
+
+ StringView name(base, field.name_size);
+ return { name, field };
+}
+
+//------------------------------------------------------------------------------
+void Type::patch()
+{
+ for (uint32 i = 0; i < get_field_count(); ++i)
+ {
+ Field* field = const_cast<Field*>(_fields + i);
+ if (field->_internal_type == FIELD_INTERNAL_VALUE)
+ continue;
+
+ if ((field->type_info & TYPE_INFO_SPECIAL_MASK) != 0)
+ fatal("unexpected special value in type");
+
+ if (field->_internal_type == FIELD_INTERNAL_REF)
+ {
+ field->type_info |= TYPE_INFO_SPECIAL_REF;
+ continue;
+ }
+
+ field->type_info = field->name_size;
+ field->type_info |= TYPE_INFO_SPECIAL_DEF_ID;
+ field->name_size = 0;
+ }
+}
diff --git a/thirdparty/xmake.lua b/thirdparty/xmake.lua
index ea861fc55..ba4dad905 100644
--- a/thirdparty/xmake.lua
+++ b/thirdparty/xmake.lua
@@ -137,3 +137,55 @@ target("fmt")
add_headerfiles("fmt/include/**.h")
add_includedirs("fmt/include", {public=true})
+includes("raw_pdb")
+
+target("tourist")
+ set_kind("static")
+ set_group("thirdparty")
+
+ add_files(
+ "tourist/foundation/src/allocator.cpp",
+ "tourist/foundation/src/buffer.cpp",
+ "tourist/foundation/src/malloc.cpp",
+ "tourist/foundation/src/ref.cpp",
+ "tourist/foundation/src/scheduler.cpp",
+ "tourist/foundation/src/stream.cpp",
+ "tourist/trace/src/data.cpp",
+ "tourist/trace/src/preamble.cpp",
+ "tourist/trace/src/protocol.cpp",
+ "tourist/trace/src/transport.cpp",
+ "tourist/trace/src/types.cpp",
+ "tourist/analysis/src/dispatcher.cpp"
+ )
+
+ add_includedirs(
+ "tourist/foundation/include",
+ "tourist/trace/include",
+ "tourist/analysis/include",
+ {public=true}
+ )
+
+ -- Internal headers (slab.h, constants.h)
+ add_includedirs(
+ "tourist/foundation/src",
+ "tourist/trace/src"
+ )
+
+ add_headerfiles(
+ "tourist/foundation/include/**.h",
+ "tourist/trace/include/**.h",
+ "tourist/analysis/include/**.h",
+ "tourist/foundation/src/slab.h",
+ "tourist/trace/src/constants.h"
+ )
+
+ -- LZ4 for trace packet decompression
+ add_packages("lz4")
+
+ -- Suppress warnings from tourist's code style under zen's strict settings
+ if is_plat("windows") then
+ add_cxxflags("/wd4668", {force=true}) -- 'X' is not defined as a preprocessor macro
+ add_cxxflags("/wd4189", {force=true}) -- local variable is initialized but not referenced
+ add_cxxflags("/wd4702", {force=true}) -- unreachable code
+ end
+
diff --git a/xmake.lua b/xmake.lua
index 13f6fdaca..27abf9b09 100644
--- a/xmake.lua
+++ b/xmake.lua
@@ -329,6 +329,10 @@ if is_os("windows") then
add_cxxflags("/Zc:u8EscapeEncoding") -- Enable UTF-8 encoding for u8 string literals (clang does this by default)
add_cxxflags("/Zc:preprocessor") -- Enable preprocessor conformance mode
add_cxxflags("/Zc:inline") -- Enforce inline semantics
+ if is_mode("release") then
+ add_cflags("/Zo") -- better debug info for optimized code
+ add_cxxflags("/Zo") -- better debug info for optimized code
+ end
end
-- add_ldflags("/MAP")
@@ -462,6 +466,7 @@ option("zentrace")
option_end()
add_define_by_config("ZEN_WITH_TRACE", "zentrace")
+
set_warnings("allextra", "error")
set_languages("cxx20")