// 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 struct LocFmt { std::format_string format; std::source_location location; template requires std::convertible_to 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 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 kinds; }; // Whether one entry passes a query's filters (all but `last`), for a // follower that sees entries one at a time. bool Matches(const TimelineQuery& query, const TimelineEntry& entry); 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 Query(const TimelineQuery& query) const; std::uint64_t LastSequence() const; std::vector 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 listener); private: mutable std::mutex mutex; std::deque entries; std::uint64_t nextSequence = 1; struct SiteCount { std::string category; std::uint64_t failures = 0; }; std::map, SiteCount> checks; std::shared_ptr> 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 void Log(std::string_view category, LocFmt...> format, A&&... args) { Detail::Timeline::Instance().Append(Detail::MakeEntry(EntryKind::Log, category, std::format(format.format, std::forward(args)...), format.location)); } template void LogWarn(std::string_view category, LocFmt...> format, A&&... args) { Detail::Timeline::Instance().Append(Detail::MakeEntry(EntryKind::Warn, category, std::format(format.format, std::forward(args)...), format.location)); } template void LogError(std::string_view category, LocFmt...> format, A&&... args) { Detail::Timeline::Instance().Append(Detail::MakeEntry(EntryKind::Error, category, std::format(format.format, std::forward(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 bool Check(bool ok, std::string_view category, LocFmt...> format, A&&... args) { if (!ok) Detail::Timeline::Instance().RecordCheck(category, std::format(format.format, std::forward(args)...), format.location); return ok; } // A structured event, which `crafter-debug wait --event ` waits for. template 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)); } }