aboutsummaryrefslogtreecommitdiff
path: root/src/zen/trace/trace_model.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/zen/trace/trace_model.cpp')
-rw-r--r--src/zen/trace/trace_model.cpp209
1 files changed, 209 insertions, 0 deletions
diff --git a/src/zen/trace/trace_model.cpp b/src/zen/trace/trace_model.cpp
index ac81161a1..c11e2c47c 100644
--- a/src/zen/trace/trace_model.cpp
+++ b/src/zen/trace/trace_model.cpp
@@ -387,6 +387,26 @@ begin_outline(CsvProfiler, Metadata)
field(uint8[], Key)
field(uint8[], Value)
end_outline()
+
+// Counters trace events (UE CountersTrace / zen ZEN_TRACE_INT_VALUE).
+begin_outline(Counters, Spec)
+ field(uint16, Id)
+ field(uint8, Type)
+ field(uint8, DisplayHint)
+ field(uint8[], Name)
+end_outline()
+
+begin_outline(Counters, SetValueInt)
+ field(uint64, Cycle)
+ field(int64, Value)
+ field(uint16, CounterId)
+end_outline()
+
+begin_outline(Counters, SetValueFloat)
+ field(uint64, Cycle)
+ field(double, Value)
+ field(uint16, CounterId)
+end_outline()
// clang-format on
//////////////////////////////////////////////////////////////////////////////
@@ -1471,6 +1491,138 @@ private:
};
//////////////////////////////////////////////////////////////////////////////
+// Counters analyzer -- consumes Counters.Spec / SetValueInt / SetValueFloat
+// (UE TRACE_INT_VALUE / TRACE_FLOAT_VALUE / zen ZEN_TRACE_INT_VALUE etc.).
+// Spec events register a counter id; SetValue events emit a sample. We keep
+// per-counter time series for the viewer / report to render.
+
+class CountersAnalyzer : public Analyzer
+{
+public:
+ explicit CountersAnalyzer(const TraceTiming* Timing = nullptr) : m_Timing(Timing) {}
+
+ void subscribe(Vector<Subscription>& Subs) override
+ {
+ Subs.emplace_back(this, &CountersAnalyzer::OnSpec);
+ Subs.emplace_back(this, &CountersAnalyzer::OnSetValueInt);
+ Subs.emplace_back(this, &CountersAnalyzer::OnSetValueFloat);
+ }
+
+ struct EditableSeries
+ {
+ zen::trace_detail::TraceModel::CounterSeries Series;
+ bool HasMin = false;
+ };
+
+ const eastl::hash_map<uint16_t, zen::trace_detail::TraceModel::CounterDef>& Defs() const { return m_Defs; }
+ eastl::hash_map<uint16_t, EditableSeries>& MutableSeriesMap() { return m_Series; }
+
+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);
+ }
+
+ void OnSpec(const Counters_Spec& Ev)
+ {
+ uint16_t Id = uint16_t(Ev.Id());
+ if (Id == 0)
+ {
+ return;
+ }
+ zen::trace_detail::TraceModel::CounterDef Def;
+ Def.Id = Id;
+ Def.Type = uint8_t(Ev.Type());
+ Def.DisplayHint = uint8_t(Ev.DisplayHint());
+ Def.Name = DecodeAnsiName(Ev.Name());
+ if (Def.Name.empty())
+ {
+ Def.Name = fmt::format("counter_{}", Id);
+ }
+ m_Defs[Id] = std::move(Def);
+ }
+
+ EditableSeries& EnsureSeries(uint16_t Id, uint8_t Type)
+ {
+ auto It = m_Series.find(Id);
+ if (It == m_Series.end())
+ {
+ EditableSeries E;
+ E.Series.Id = Id;
+ E.Series.Type = Type;
+ It = m_Series.emplace(Id, std::move(E)).first;
+ }
+ return It->second;
+ }
+
+ void OnSetValueInt(const Counters_SetValueInt& Ev)
+ {
+ uint16_t Id = uint16_t(Ev.CounterId());
+ if (Id == 0)
+ {
+ return;
+ }
+ EditableSeries& E = EnsureSeries(Id, /*Int*/ 0);
+ double Value = double(int64_t(Ev.Value()));
+ uint32_t TimeUs = CycleToTimeUs(Ev.Cycle());
+ E.Series.Samples.push_back({TimeUs, Value});
+ ++E.Series.Count;
+ if (!E.HasMin || Value < E.Series.Min)
+ {
+ E.Series.Min = Value;
+ E.HasMin = true;
+ }
+ if (Value > E.Series.Max)
+ {
+ E.Series.Max = Value;
+ }
+ }
+
+ void OnSetValueFloat(const Counters_SetValueFloat& Ev)
+ {
+ uint16_t Id = uint16_t(Ev.CounterId());
+ if (Id == 0)
+ {
+ return;
+ }
+ EditableSeries& E = EnsureSeries(Id, /*Float*/ 1);
+ double Value = double(Ev.Value());
+ uint32_t TimeUs = CycleToTimeUs(Ev.Cycle());
+ E.Series.Samples.push_back({TimeUs, Value});
+ ++E.Series.Count;
+ if (!E.HasMin || Value < E.Series.Min)
+ {
+ E.Series.Min = Value;
+ E.HasMin = true;
+ }
+ if (Value > E.Series.Max)
+ {
+ E.Series.Max = Value;
+ }
+ }
+
+ const TraceTiming* m_Timing = nullptr;
+ eastl::hash_map<uint16_t, zen::trace_detail::TraceModel::CounterDef> m_Defs;
+ eastl::hash_map<uint16_t, EditableSeries> m_Series;
+};
+
+//////////////////////////////////////////////////////////////////////////////
// Analyzers
class CpuAnalyzer : public Analyzer
@@ -3165,6 +3317,7 @@ BuildTraceModel(const std::filesystem::path& FilePath, WorkerThreadPool& ThreadP
LogAnalyzer LogAn(&Timing);
BookmarksAnalyzer BookmarkAn(&Timing);
CsvProfilerAnalyzer CsvAn(&Timing);
+ CountersAnalyzer CountersAn(&Timing);
AllocationAnalyzer AllocAn(&Timing);
CallstackAnalyzer CallstackAn;
@@ -3182,6 +3335,7 @@ BuildTraceModel(const std::filesystem::path& FilePath, WorkerThreadPool& ThreadP
Dispatch.add_analyzer(LogAn);
Dispatch.add_analyzer(BookmarkAn);
Dispatch.add_analyzer(CsvAn);
+ Dispatch.add_analyzer(CountersAn);
Dispatch.add_analyzer(AllocAn);
Dispatch.add_analyzer(CallstackAn);
@@ -3434,6 +3588,61 @@ BuildTraceModel(const std::filesystem::path& FilePath, WorkerThreadPool& ThreadP
Model.CsvEvents.size());
}
+ // Counters (TRACE_INT_VALUE / TRACE_FLOAT_VALUE)
+ {
+ using CounterDefT = zen::trace_detail::TraceModel::CounterDef;
+ using CounterSeriesT = zen::trace_detail::TraceModel::CounterSeries;
+ using CounterSampleT = zen::trace_detail::TraceModel::CounterSample;
+
+ Model.CounterDefs.reserve(CountersAn.Defs().size());
+ for (const auto& [Id, Def] : CountersAn.Defs())
+ {
+ Model.CounterDefs.push_back(Def);
+ }
+
+ auto& SeriesMap = CountersAn.MutableSeriesMap();
+ Model.CounterTimeSeries.reserve(SeriesMap.size());
+ for (auto& [Id, Editable] : SeriesMap)
+ {
+ // Each counter's samples were appended in stream order. Tourist
+ // guarantees per-thread monotonicity but counters can be set from
+ // any thread, so a final sort by TimeUs is required.
+ eastl::sort(Editable.Series.Samples.begin(),
+ Editable.Series.Samples.end(),
+ [](const CounterSampleT& A, const CounterSampleT& B) { return A.TimeUs < B.TimeUs; });
+ // If the counter never produced a Spec event we still want the
+ // series visible. Synthesize a default def so the viewer has a name.
+ if (CountersAn.Defs().find(Id) == CountersAn.Defs().end())
+ {
+ CounterDefT Synth;
+ Synth.Id = Id;
+ Synth.Type = Editable.Series.Type;
+ Synth.Name = fmt::format("counter_{}", Id);
+ Model.CounterDefs.push_back(std::move(Synth));
+ }
+ Model.CounterTimeSeries.push_back(std::move(Editable.Series));
+ }
+ eastl::sort(Model.CounterDefs.begin(), Model.CounterDefs.end(), [](const CounterDefT& A, const CounterDefT& B) {
+ return A.Id < B.Id;
+ });
+ eastl::sort(Model.CounterTimeSeries.begin(), Model.CounterTimeSeries.end(), [](const CounterSeriesT& A, const CounterSeriesT& B) {
+ return A.Id < B.Id;
+ });
+
+ size_t TotalSamples = 0;
+ for (const CounterSeriesT& S : Model.CounterTimeSeries)
+ {
+ TotalSamples += S.Samples.size();
+ }
+ if (!Model.CounterDefs.empty() || !Model.CounterTimeSeries.empty())
+ {
+ ZEN_INFO("Counters: {} defined, {} with samples ({} samples total)",
+ Model.CounterDefs.size(),
+ Model.CounterTimeSeries.size(),
+ zen::ThousandsNum(TotalSamples));
+ }
+ }
+
// Memory allocation data
{
AllocAn.EmitFinalSample(Model.TraceEndUs);