aboutsummaryrefslogtreecommitdiff
path: root/src/zencore/include
diff options
context:
space:
mode:
Diffstat (limited to 'src/zencore/include')
-rw-r--r--src/zencore/include/zencore/logging/backlogsink.h104
-rw-r--r--src/zencore/include/zencore/logging/broadcastsink.h15
-rw-r--r--src/zencore/include/zencore/memory/memoryarena.h52
-rw-r--r--src/zencore/include/zencore/oidguid.h81
-rw-r--r--src/zencore/include/zencore/profiling/counterstrace.h221
-rw-r--r--src/zencore/include/zencore/session.h4
6 files changed, 470 insertions, 7 deletions
diff --git a/src/zencore/include/zencore/logging/backlogsink.h b/src/zencore/include/zencore/logging/backlogsink.h
new file mode 100644
index 000000000..be170db9d
--- /dev/null
+++ b/src/zencore/include/zencore/logging/backlogsink.h
@@ -0,0 +1,104 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#pragma once
+
+#include <zencore/logging/sink.h>
+#include <zencore/memory/memoryarena.h>
+#include <zencore/thread.h>
+
+ZEN_THIRD_PARTY_INCLUDES_START
+#include <EASTL/vector.h>
+ZEN_THIRD_PARTY_INCLUDES_END
+
+#include <atomic>
+#include <chrono>
+#include <limits>
+#include <memory>
+
+namespace zen::logging {
+
+/// Captures every message it sees so that sinks attached *after* logging
+/// has already been emitting can replay the early window into themselves.
+///
+/// Typical lifecycle:
+/// 1. At process start, install one BacklogSink as a child of the
+/// default broadcast sink. Capture begins immediately.
+/// 2. As real sinks come online (file, console, in-proc session, …),
+/// they're attached to the broadcast and can call Replay() to
+/// receive the lines emitted before they joined.
+/// 3. At a well-defined "we're past the bootstrap window" point — the
+/// server's `OnReady()` for zenserver, command dispatch for the zen
+/// CLI — call Disable() to stop capturing and free everything.
+///
+/// Storage is bounded and arena-backed: the vector and every captured
+/// string are allocated from a single MemoryArena that gets dropped
+/// wholesale on Disable(). The vector is reserved up front so push_back
+/// never reallocates; once full we drop new entries (the early-startup
+/// lines are the ones we most want to preserve, so keep-oldest is the
+/// right policy here).
+class BacklogSink : public Sink
+{
+public:
+ /// @param MaxEntries Hard cap on captured messages. The vector is
+ /// reserved up front from the arena. 256 is a
+ /// comfortable cap for the bootstrap window —
+ /// typical zen process startup emits a few dozen
+ /// lines, well under the limit.
+ explicit BacklogSink(size_t MaxEntries = 256);
+ ~BacklogSink() override;
+
+ BacklogSink(const BacklogSink&) = delete;
+ BacklogSink& operator=(const BacklogSink&) = delete;
+
+ void Log(const LogMessage& Msg) override;
+ void Flush() override;
+ void SetFormatter(std::unique_ptr<Formatter> InFormatter) override;
+
+ /// Replay captured messages into Target in arrival order. By default
+ /// every entry currently in the buffer is replayed; pass @p MaxEntries
+ /// to stop after that many (useful when a caller has snapshotted a
+ /// cursor to avoid re-delivering messages the target already received
+ /// live — see AttachSinkWithBacklogReplay). Target's ShouldLog() filter
+ /// is honored. Replay does NOT remove entries from the buffer — it's
+ /// safe to call multiple times.
+ void Replay(Sink& Target, size_t MaxEntries = std::numeric_limits<size_t>::max());
+
+ /// Stop capturing and free the entire arena (vector buffer + every
+ /// captured string in one shot). Subsequent Log() calls return
+ /// immediately. Idempotent.
+ void Disable();
+
+ bool IsEnabled() const { return m_Enabled.load(std::memory_order_acquire); }
+ size_t Size() const;
+ size_t Dropped() const { return m_DroppedCount.load(std::memory_order_relaxed); }
+
+private:
+ struct Entry
+ {
+ // LogPoint pointers come from static / near-static instances
+ // owned by the ZEN_LOG macros (see asyncsink.cpp's note); the
+ // pointer outlives any plausible backlog buffer.
+ const LogPoint* Point = nullptr;
+
+ // Both strings live in m_Arena — null-terminated, so we keep them
+ // as plain const char* and reconstruct string_views on replay.
+ const char* LoggerName = nullptr;
+ const char* Payload = nullptr;
+
+ std::chrono::system_clock::time_point Time;
+ int ThreadId = 0;
+ };
+
+ mutable RwLock m_Lock;
+ // Declaration order matters: m_Arena must be initialized before
+ // m_Entries because m_Entries' allocator references m_Arena. Both
+ // outlive every captured Entry — they're released as a unit when
+ // this BacklogSink is destroyed.
+ MemoryArena m_Arena{32768};
+ eastl::vector<Entry, ArenaAlloc> m_Entries{ArenaAlloc(m_Arena)};
+ std::atomic<bool> m_Enabled{true};
+ std::atomic<uint64_t> m_DroppedCount{0};
+ size_t m_MaxEntries;
+};
+
+} // namespace zen::logging
diff --git a/src/zencore/include/zencore/logging/broadcastsink.h b/src/zencore/include/zencore/logging/broadcastsink.h
index 474662888..2b96e04f4 100644
--- a/src/zencore/include/zencore/logging/broadcastsink.h
+++ b/src/zencore/include/zencore/logging/broadcastsink.h
@@ -72,6 +72,21 @@ public:
m_Sinks.push_back(std::move(InSink));
}
+ /// Add a child sink and run @p OnAddedUnderLock with the broadcast's
+ /// exclusive lock still held. Useful for callers that need to capture
+ /// some external state atomically with the moment the sink becomes
+ /// visible to fanout — e.g. snapshotting a backlog cursor so a
+ /// subsequent replay can de-duplicate against messages that already
+ /// reached the new sink via Log(). The callback must not call back
+ /// into this BroadcastSink (would self-deadlock).
+ template<typename F>
+ void AddSinkAtomic(SinkPtr InSink, F&& OnAddedUnderLock)
+ {
+ RwLock::ExclusiveLockScope Lock(m_Lock);
+ m_Sinks.push_back(std::move(InSink));
+ OnAddedUnderLock();
+ }
+
void RemoveSink(const SinkPtr& InSink)
{
RwLock::ExclusiveLockScope Lock(m_Lock);
diff --git a/src/zencore/include/zencore/memory/memoryarena.h b/src/zencore/include/zencore/memory/memoryarena.h
index 551415aac..167449036 100644
--- a/src/zencore/include/zencore/memory/memoryarena.h
+++ b/src/zencore/include/zencore/memory/memoryarena.h
@@ -3,7 +3,8 @@
#pragma once
#include <zencore/thread.h>
-#include <vector>
+
+#include <EASTL/fixed_vector.h>
namespace zen {
@@ -14,6 +15,10 @@ namespace zen {
* All allocations are freed when the arena is destroyed, therefore there
* is no support for individual deallocation.
*
+ * Chunk size is configurable at construction (default 64 KiB). Allocations
+ * larger than the configured chunk size get their own dedicated chunk so
+ * they don't waste or overrun the normal chunk.
+ *
* For convenience, we include operator new/delete overloads below which
* take a MemoryArena reference as a placement argument.
*
@@ -22,7 +27,12 @@ namespace zen {
class MemoryArena
{
public:
- MemoryArena() = default;
+ /// Default chunk size (64 KiB). Sized to fit comfortably in L2 and
+ /// to minimize the number of chunks needed for typical workloads.
+ static constexpr size_t kDefaultChunkSize = 64 * 1024;
+
+ /// @param ChunkSize Bytes per chunk. Zero is treated as the default.
+ explicit MemoryArena(size_t ChunkSize = kDefaultChunkSize);
~MemoryArena();
void* AllocateAligned(size_t ByteCount, size_t align);
@@ -30,17 +40,45 @@ public:
void* Allocate(size_t Size);
const char* DuplicateString(std::string_view Str);
+ /// Configured chunk size in bytes.
+ size_t GetChunkSize() const { return m_ChunkSize; }
+
+ /// Total bytes the arena currently holds across all chunks
+ /// (regular + oversize). This is the reserved footprint, not the
+ /// fraction of it that's been bump-pointer-allocated to callers —
+ /// useful as a "how big did this arena ever grow" diagnostic.
+ size_t GetReservedBytes() const;
+
MemoryArena(const MemoryArena&) = delete;
MemoryArena& operator=(const MemoryArena&) = delete;
private:
- static constexpr size_t ChunkSize = 16 * 1024;
+ // True when the request's worst-case footprint exceeds m_ChunkSize.
+ // Such requests get a dedicated chunk so the regular bump pointer is
+ // preserved for small allocations.
+ bool IsOversizeRequest(size_t WorstCaseBytes) const;
+
+ // Allocates a chunk and pushes it into m_Chunks. Caller is responsible
+ // for any bookkeeping (current pointer/offset). Must be called with
+ // m_Lock held exclusive. Returns nullptr on out-of-memory.
+ uint8_t* AllocateDedicatedChunk(size_t Capacity);
+
// TODO: should just chain the memory blocks together and avoid this
// vector altogether, saving us a memory allocation
- std::vector<uint8_t*> m_Chunks;
- uint8_t* m_CurrentChunk = nullptr;
- size_t m_CurrentOffset = 0;
- RwLock m_Lock;
+ struct Chunk
+ {
+ uint8_t* Base = nullptr;
+ size_t Capacity = 0; // May exceed m_ChunkSize for oversize allocations.
+ };
+ // 32 inline chunks keeps the vector itself allocation-free for any
+ // arena that stays under ~2 MiB at the default chunk size; growth
+ // beyond that falls back to heap.
+ eastl::fixed_vector<Chunk, 32> m_Chunks;
+ uint8_t* m_CurrentChunk = nullptr;
+ size_t m_CurrentChunkCapacity = 0;
+ size_t m_CurrentOffset = 0;
+ size_t m_ChunkSize;
+ mutable RwLock m_Lock;
};
// Allocator suitable for use with EASTL
diff --git a/src/zencore/include/zencore/oidguid.h b/src/zencore/include/zencore/oidguid.h
new file mode 100644
index 000000000..16d8cd101
--- /dev/null
+++ b/src/zencore/include/zencore/oidguid.h
@@ -0,0 +1,81 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#pragma once
+
+#include <zencore/guid.h>
+#include <zencore/uid.h>
+
+#include <cstdint>
+#include <cstring>
+
+namespace zen {
+
+/// Convert a 12-byte Oid into a UUID-shaped 16-byte Guid that
+/// round-trips back through GuidToOid. Used to bridge zen session ids
+/// (Oid, 12 bytes) and tools that consume UUID-shaped session
+/// identifiers — most notably UE Trace, whose session id is an FGuid
+/// memcpy'd into the trace stream's `Desc.SessionGuid`.
+///
+/// The transform packs the 12 bytes as three big-endian uint32s into
+/// (A, B, C), stashes the bits of B/C that we're about to overwrite
+/// into D, then sets the RFC 4122 version-4 (random) and variant
+/// (10xx) markers in B and C. The result is a syntactically valid v4
+/// UUID — analyzers won't reject it — and the original 12 bytes are
+/// recoverable byte-for-byte via GuidToOid.
+///
+/// Note: the inverse direction (Guid→Oid→Guid for an arbitrary input
+/// Guid) is NOT lossless. GuidToOid restores 6 bits (version + variant)
+/// from D regardless of where they came from in the input, so the
+/// version/variant nibbles of an arbitrary FGuid are clobbered.
+/// Lossless only in the Oid→Guid→Oid direction, which is exactly what
+/// we need for "given a session Oid, get a UUID I can put in the
+/// trace; given a UUID from a trace I produced, get the session Oid
+/// back."
+inline Guid
+OidToGuid(const Oid& Id)
+{
+ const uint8_t* Bytes = reinterpret_cast<const uint8_t*>(Id.OidBits);
+ uint32_t A = (uint32_t(Bytes[0]) << 24) | (uint32_t(Bytes[1]) << 16) | (uint32_t(Bytes[2]) << 8) | uint32_t(Bytes[3]);
+ uint32_t B = (uint32_t(Bytes[4]) << 24) | (uint32_t(Bytes[5]) << 16) | (uint32_t(Bytes[6]) << 8) | uint32_t(Bytes[7]);
+ uint32_t C = (uint32_t(Bytes[8]) << 24) | (uint32_t(Bytes[9]) << 16) | (uint32_t(Bytes[10]) << 8) | uint32_t(Bytes[11]);
+
+ // Stash the two bytes that the version/variant markers below are
+ // about to overwrite. D ends up with only those two bytes set; all
+ // other bits are zero.
+ uint32_t D = (B & 0x0000FF00u) | (C & 0xFF000000u);
+
+ // RFC 4122 markers: version 4 in B[15..12], variant 10xx in C[31..30].
+ B = (B & ~0x0000F000u) | 0x00004000u;
+ C = (C & ~0xC0000000u) | 0x80000000u;
+
+ return Guid{A, B, C, D};
+}
+
+/// Inverse of OidToGuid. Restores the two bytes of B and C that were
+/// clobbered with version/variant markers by pulling them back out of
+/// D, then unpacks the three uint32s into 12 big-endian bytes.
+inline Oid
+GuidToOid(const Guid& Source)
+{
+ const uint32_t Bits0 = Source.A;
+ const uint32_t Bits1 = (Source.B & ~0x0000FF00u) | (Source.D & 0x0000FF00u);
+ const uint32_t Bits2 = (Source.C & ~0xFF000000u) | (Source.D & 0xFF000000u);
+
+ Oid Out{};
+ uint8_t* Bytes = reinterpret_cast<uint8_t*>(Out.OidBits);
+ Bytes[0] = uint8_t(Bits0 >> 24);
+ Bytes[1] = uint8_t(Bits0 >> 16);
+ Bytes[2] = uint8_t(Bits0 >> 8);
+ Bytes[3] = uint8_t(Bits0);
+ Bytes[4] = uint8_t(Bits1 >> 24);
+ Bytes[5] = uint8_t(Bits1 >> 16);
+ Bytes[6] = uint8_t(Bits1 >> 8);
+ Bytes[7] = uint8_t(Bits1);
+ Bytes[8] = uint8_t(Bits2 >> 24);
+ Bytes[9] = uint8_t(Bits2 >> 16);
+ Bytes[10] = uint8_t(Bits2 >> 8);
+ Bytes[11] = uint8_t(Bits2);
+ return Out;
+}
+
+} // namespace zen
diff --git a/src/zencore/include/zencore/profiling/counterstrace.h b/src/zencore/include/zencore/profiling/counterstrace.h
new file mode 100644
index 000000000..1fa40a5f3
--- /dev/null
+++ b/src/zencore/include/zencore/profiling/counterstrace.h
@@ -0,0 +1,221 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#pragma once
+
+// Counter trace points: emit named time-series of int or float values into the
+// active .utrace stream. Wire-compatible with UE's Counters.Spec /
+// SetValueInt / SetValueFloat events (CountersChannel), so traces produced by
+// zen are readable by Unreal Insights and zen's own `zen trace` tooling.
+//
+// Typical use:
+//
+// ZEN_TRACE_INT_VALUE("MySubsystem/QueueDepth", QueueSize);
+// ZEN_TRACE_FLOAT_VALUE("MySubsystem/HitRate", Hits / Total);
+// ZEN_TRACE_MEMORY_VALUE("MySubsystem/CacheBytes", BytesInUse);
+//
+// Each macro instantiates a function-static counter object the first time it
+// is hit, lazily registering the counter (Spec event) and then emitting a
+// SetValue event. Repeating the same value is suppressed (matching UE's
+// TCounter::Set semantics) -- use the _ALWAYS variants if every sample must
+// be emitted.
+
+#include <zencore/zencore.h>
+
+#include <atomic>
+#include <cstdint>
+#include <string_view>
+#include <type_traits>
+
+namespace zen {
+
+enum class TraceCounterType : uint8_t
+{
+ Int = 0,
+ Float = 1,
+};
+
+enum class TraceCounterDisplayHint : uint8_t
+{
+ None = 0,
+ Memory = 1,
+};
+
+#if ZEN_WITH_TRACE
+
+namespace counters_detail {
+
+ // Returns the assigned counter id (1..0xFFFF) on success, or 0 if tracing is
+ // disabled / the channel is off. Emits a Counters.Spec event on first
+ // successful registration.
+ uint16_t OutputInitCounter(const char* Name, TraceCounterType Type, TraceCounterDisplayHint Hint);
+
+ // Emit a SetValueInt / SetValueFloat event. No-op when Id == 0.
+ void OutputSetValueInt(uint16_t Id, int64_t Value);
+ void OutputSetValueFloat(uint16_t Id, double Value);
+
+ // True when the Counters channel is currently active. The macros use this to
+ // skip work when nobody is listening.
+ bool IsCountersChannelEnabled();
+
+} // namespace counters_detail
+
+template<typename ValueType, TraceCounterType Type, typename StoredType = ValueType, bool bUnchecked = false>
+class TraceCounter
+{
+public:
+ TraceCounter(const char* InName, TraceCounterDisplayHint InHint) : m_Name(InName), m_Hint(InHint), m_Id(0)
+ {
+ uint32_t Id = counters_detail::OutputInitCounter(InName, Type, InHint);
+ m_Id.store(Id, std::memory_order_relaxed);
+ }
+
+ TraceCounter(const TraceCounter&) = delete;
+ TraceCounter& operator=(const TraceCounter&) = delete;
+
+ ValueType Get() const { return ValueType(m_Value); }
+
+ void Set(ValueType InValue)
+ {
+ if (bUnchecked || ValueType(m_Value) != InValue)
+ {
+ m_Value = InValue;
+ Emit();
+ }
+ }
+
+ void SetAlways(ValueType InValue)
+ {
+ m_Value = InValue;
+ Emit();
+ }
+
+ void Add(ValueType InValue)
+ {
+ if (bUnchecked || InValue != ValueType{})
+ {
+ m_Value += InValue;
+ Emit();
+ }
+ }
+
+ void Subtract(ValueType InValue)
+ {
+ if (bUnchecked || InValue != ValueType{})
+ {
+ m_Value -= InValue;
+ Emit();
+ }
+ }
+
+ void Increment()
+ {
+ ++m_Value;
+ Emit();
+ }
+
+ void Decrement()
+ {
+ --m_Value;
+ Emit();
+ }
+
+private:
+ void Emit()
+ {
+ if (!counters_detail::IsCountersChannelEnabled())
+ {
+ return;
+ }
+ LateInit();
+ uint16_t Id = uint16_t(m_Id.load(std::memory_order_relaxed));
+ if constexpr (Type == TraceCounterType::Int)
+ {
+ counters_detail::OutputSetValueInt(Id, int64_t(ValueType(m_Value)));
+ }
+ else
+ {
+ counters_detail::OutputSetValueFloat(Id, double(ValueType(m_Value)));
+ }
+ }
+
+ void LateInit()
+ {
+ uint32_t Old = m_Id.load(std::memory_order_relaxed);
+ if (Old != 0)
+ {
+ return;
+ }
+ uint32_t New = counters_detail::OutputInitCounter(m_Name, Type, m_Hint);
+ if (New != 0)
+ {
+ m_Id.compare_exchange_strong(Old, New, std::memory_order_relaxed);
+ }
+ }
+
+ StoredType m_Value{};
+ const char* m_Name = nullptr;
+ TraceCounterDisplayHint m_Hint = TraceCounterDisplayHint::None;
+ std::atomic<uint32_t> m_Id;
+};
+
+using TraceCounterInt = TraceCounter<int64_t, TraceCounterType::Int>;
+using TraceCounterFloat = TraceCounter<double, TraceCounterType::Float>;
+using TraceCounterAtomicInt = TraceCounter<int64_t, TraceCounterType::Int, std::atomic<int64_t>>;
+
+#else // ZEN_WITH_TRACE
+
+// Stub implementation -- inlines compile to nothing.
+namespace counters_detail {
+ inline uint16_t OutputInitCounter(const char*, TraceCounterType, TraceCounterDisplayHint) { return 0; }
+ inline void OutputSetValueInt(uint16_t, int64_t) {}
+ inline void OutputSetValueFloat(uint16_t, double) {}
+ inline bool IsCountersChannelEnabled() { return false; }
+} // namespace counters_detail
+
+template<typename ValueType, TraceCounterType, typename = ValueType, bool = false>
+class TraceCounter
+{
+public:
+ TraceCounter(const char*, TraceCounterDisplayHint) {}
+ ValueType Get() const { return ValueType{}; }
+ void Set(ValueType) {}
+ void SetAlways(ValueType) {}
+ void Add(ValueType) {}
+ void Subtract(ValueType) {}
+ void Increment() {}
+ void Decrement() {}
+};
+
+using TraceCounterInt = TraceCounter<int64_t, TraceCounterType::Int>;
+using TraceCounterFloat = TraceCounter<double, TraceCounterType::Float>;
+using TraceCounterAtomicInt = TraceCounter<int64_t, TraceCounterType::Int>;
+
+#endif // ZEN_WITH_TRACE
+
+} // namespace zen
+
+// ---------------------------------------------------------------------------
+// Public emit macros (mirror UE TRACE_INT_VALUE / TRACE_FLOAT_VALUE etc.)
+// ---------------------------------------------------------------------------
+
+#define ZEN_COUNTERS_TRACE_CONCAT_IMPL(a, b) a##b
+#define ZEN_COUNTERS_TRACE_CONCAT(a, b) ZEN_COUNTERS_TRACE_CONCAT_IMPL(a, b)
+
+#define ZEN_COUNTERS_TRACE_DECLARE_AND_SET(CounterCls, Name, Hint, Value) \
+ do \
+ { \
+ static ::zen::CounterCls ZEN_COUNTERS_TRACE_CONCAT(__ZenTraceCounter_, __LINE__){(Name), (Hint)}; \
+ ZEN_COUNTERS_TRACE_CONCAT(__ZenTraceCounter_, __LINE__).Set(Value); \
+ } while (0)
+
+// Set an integer counter to Value. Suppresses repeated identical values.
+#define ZEN_TRACE_INT_VALUE(Name, Value) \
+ ZEN_COUNTERS_TRACE_DECLARE_AND_SET(TraceCounterInt, Name, ::zen::TraceCounterDisplayHint::None, int64_t(Value))
+
+// Set a floating-point counter to Value.
+#define ZEN_TRACE_FLOAT_VALUE(Name, Value) \
+ ZEN_COUNTERS_TRACE_DECLARE_AND_SET(TraceCounterFloat, Name, ::zen::TraceCounterDisplayHint::None, double(Value))
+
+// Set a memory-style counter (formatted as bytes by the viewer).
+#define ZEN_TRACE_MEMORY_VALUE(Name, Bytes) \
+ ZEN_COUNTERS_TRACE_DECLARE_AND_SET(TraceCounterInt, Name, ::zen::TraceCounterDisplayHint::Memory, int64_t(Bytes))
diff --git a/src/zencore/include/zencore/session.h b/src/zencore/include/zencore/session.h
index 10c33da24..807af878e 100644
--- a/src/zencore/include/zencore/session.h
+++ b/src/zencore/include/zencore/session.h
@@ -12,4 +12,8 @@ struct Oid;
[[nodiscard]] Oid GetSessionId();
[[nodiscard]] std::string_view GetSessionIdString();
+void SetParentSessionId(const Oid& ParentSessionId);
+[[nodiscard]] Oid GetParentSessionId();
+[[nodiscard]] std::string_view GetParentSessionIdString();
+
} // namespace zen