Crafter.Debug/interfaces/Crafter.Debug-Timeline.cppm

172 lines
7.2 KiB
Text
Raw Normal View History

2026-11-01 01:40:59 +01:00
// SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
// The timeline: what the app logged, the checks that failed, the events it
// emitted, and what the agent did, in one sequence stamped with frame
// numbers and source locations, so `crafter-debug log` shows cause next to
// effect:
//
// 1790 AGENT call Player_Fire {}
// 1838 WARN physics brace 17 strain 4.2 > limit Forts3D-Brace.cpp:212
//
// Debug::Log("net", "peer {} joined slot {}", peerId, slot); // also LogWarn, LogError
// Debug::Emit("MatchEnded", MatchEnded{ .winnerSlot = 1 });
// if (!Debug::Check(mass > 0, "physics", "body {} has mass {}", id, mass)) return;
//
// The format string is checked at compile time, as std::format's is, and the
// caller's location is captured without a macro (macros don't cross
// `import`). Every function here is safe from any thread. The timeline
// keeps the last 8192 entries.
export module Crafter.Debug:Timeline;
import std;
import Crafter.Debug.Core;
import :Registry;
export namespace Crafter::Debug {
enum class EntryKind : std::uint8_t {
Log,
Warn,
Error,
Check,
Event,
Agent,
Pause,
Stdio,
};
// "log", "warn", "error", "check", "event", "agent", "pause", "stdio".
std::string_view EntryKindName(EntryKind kind);
struct TimelineEntry {
// 0 for a check failure that was counted but not stored (see Check).
std::uint64_t sequence = 0;
std::uint64_t frame = 0;
// Since the timeline started, on the steady clock.
double seconds = 0;
EntryKind kind = EntryKind::Log;
// The log category, or an event's name.
std::string category;
std::string message;
std::string file;
std::uint32_t line = 0;
// An event's payload.
Value payload;
// For a check: its site's failures so far, this one included.
std::uint64_t count = 1;
};
// A format string and where it was written, captured at the call site.
template<class... A>
struct LocFmt {
std::format_string<A...> format;
std::source_location location;
template<class S>
requires std::convertible_to<const S&, std::string_view>
consteval LocFmt(const S& text, std::source_location where = std::source_location::current()) : format(text), location(where) {}
};
}
export namespace Crafter::Debug::Detail {
// 0 for information (log, event, agent, pause, stdio), 1 for warn, 2 for
// error and check: what `log --level` compares against.
std::uint8_t Level(EntryKind kind);
struct TimelineQuery {
// Only entries after this sequence number: what a follower has seen.
std::uint64_t afterSequence = 0;
std::optional<std::uint64_t> sinceFrame;
// The newest this many of the matches; 0 for all of them.
std::size_t last = 0;
std::uint8_t minLevel = 0;
// A glob on the category; empty for any.
std::string category;
// A substring of the message or the category; empty for any.
std::string grep;
// Empty for any kind.
std::vector<EntryKind> kinds;
};
struct CheckSite {
std::string file;
std::uint32_t line = 0;
std::string category;
std::uint64_t failures = 0;
};
class Timeline {
public:
static Timeline& Instance();
// Stamps the entry with its sequence, frame and time, stores it and
// tells the listener. Returns its sequence number.
std::uint64_t Append(TimelineEntry entry);
// A failed Check. Counted per site; stored while the site has failed
// at most ten times and then at every power of two, so a check that
// fails every frame cannot push everything else out. The listener
// hears every failure, stored or not.
void RecordCheck(std::string_view category, std::string message, const std::source_location& location);
std::vector<TimelineEntry> Query(const TimelineQuery& query) const;
std::uint64_t LastSequence() const;
std::vector<CheckSite> CheckSites() const;
// Called with every stored entry and every check failure, on the
// thread that recorded it, after the timeline's lock is released:
// the session uses it to pause on a check and to wake followers.
void SetListener(std::function<void(const TimelineEntry&)> listener);
private:
mutable std::mutex mutex;
std::deque<TimelineEntry> entries;
std::uint64_t nextSequence = 1;
struct SiteCount {
std::string category;
std::uint64_t failures = 0;
};
std::map<std::pair<std::string, std::uint32_t>, SiteCount> checks;
std::shared_ptr<const std::function<void(const TimelineEntry&)>> listener;
std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now();
void Notify(const TimelineEntry& entry) const;
};
TimelineEntry MakeEntry(EntryKind kind, std::string_view category, std::string message, const std::source_location& location, Value payload = {});
}
export namespace Crafter::Debug {
template<class... A>
void Log(std::string_view category, LocFmt<std::type_identity_t<A>...> format, A&&... args) {
Detail::Timeline::Instance().Append(Detail::MakeEntry(EntryKind::Log, category, std::format(format.format, std::forward<A>(args)...), format.location));
}
template<class... A>
void LogWarn(std::string_view category, LocFmt<std::type_identity_t<A>...> format, A&&... args) {
Detail::Timeline::Instance().Append(Detail::MakeEntry(EntryKind::Warn, category, std::format(format.format, std::forward<A>(args)...), format.location));
}
template<class... A>
void LogError(std::string_view category, LocFmt<std::type_identity_t<A>...> format, A&&... args) {
Detail::Timeline::Instance().Append(Detail::MakeEntry(EntryKind::Error, category, std::format(format.format, std::forward<A>(args)...), format.location));
}
// A soft assert: returns `ok`, and when it is false records the failure
// (the message is only formatted then, so a Check in hot code is cheap).
// An agent can make a failure pause the app (`pause --on-check`) or stop
// it in an attached debugger (`--trap`).
template<class... A>
bool Check(bool ok, std::string_view category, LocFmt<std::type_identity_t<A>...> format, A&&... args) {
if (!ok) Detail::Timeline::Instance().RecordCheck(category, std::format(format.format, std::forward<A>(args)...), format.location);
return ok;
}
// A structured event, which `crafter-debug wait --event <name>` waits for.
template<class T>
void Emit(std::string_view event, const T& payload, std::source_location location = std::source_location::current()) {
Detail::Timeline::Instance().Append(Detail::MakeEntry(EntryKind::Event, event, {}, location, Debug::Encode(payload)));
}
inline void Emit(std::string_view event, std::source_location location = std::source_location::current()) {
Detail::Timeline::Instance().Append(Detail::MakeEntry(EntryKind::Event, event, {}, location));
}
}