// SPDX-License-Identifier: LGPL-3.0-only // SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® module Crafter.Debug:Timeline_impl; import std; import Crafter.Debug.Core; import :Registry; import :Timeline; using namespace Crafter::Debug; using namespace Crafter::Debug::Detail; namespace { constexpr std::size_t capacity = 8192; bool Stored(std::uint64_t failures) { return failures <= 10 || std::has_single_bit(failures); } } std::string_view Crafter::Debug::EntryKindName(EntryKind kind) { switch (kind) { case EntryKind::Log: return "log"; case EntryKind::Warn: return "warn"; case EntryKind::Error: return "error"; case EntryKind::Check: return "check"; case EntryKind::Event: return "event"; case EntryKind::Agent: return "agent"; case EntryKind::Pause: return "pause"; case EntryKind::Stdio: return "stdio"; } return "unknown"; } std::uint8_t Crafter::Debug::Detail::Level(EntryKind kind) { switch (kind) { case EntryKind::Warn: return 1; case EntryKind::Error: case EntryKind::Check: return 2; default: return 0; } } TimelineEntry Crafter::Debug::Detail::MakeEntry(EntryKind kind, std::string_view category, std::string message, const std::source_location& location, Value payload) { TimelineEntry entry; entry.kind = kind; entry.category = std::string(category); entry.message = std::move(message); entry.file = location.file_name(); entry.line = location.line(); entry.payload = std::move(payload); return entry; } Timeline& Timeline::Instance() { static Timeline timeline; return timeline; } std::uint64_t Timeline::Append(TimelineEntry entry) { entry.frame = CurrentFrame(); { std::lock_guard lock(mutex); entry.seconds = std::chrono::duration(std::chrono::steady_clock::now() - start).count(); entry.sequence = nextSequence++; entries.push_back(entry); if (entries.size() > capacity) entries.pop_front(); } Notify(entry); return entry.sequence; } void Timeline::RecordCheck(std::string_view category, std::string message, const std::source_location& location) { TimelineEntry entry = MakeEntry(EntryKind::Check, category, std::move(message), location); entry.frame = CurrentFrame(); { std::lock_guard lock(mutex); entry.seconds = std::chrono::duration(std::chrono::steady_clock::now() - start).count(); SiteCount& site = checks[{ entry.file, entry.line }]; site.category = entry.category; entry.count = ++site.failures; if (Stored(entry.count)) { entry.sequence = nextSequence++; entries.push_back(entry); if (entries.size() > capacity) entries.pop_front(); } } Notify(entry); } bool Crafter::Debug::Detail::Matches(const TimelineQuery& query, const TimelineEntry& entry) { if (entry.sequence <= query.afterSequence) return false; if (query.sinceFrame && entry.frame < *query.sinceFrame) return false; if (Level(entry.kind) < query.minLevel) return false; if (!query.kinds.empty() && std::ranges::find(query.kinds, entry.kind) == query.kinds.end()) return false; if (!query.category.empty() && !GlobMatch(query.category, entry.category)) return false; if (!query.grep.empty() && entry.message.find(query.grep) == std::string::npos && entry.category.find(query.grep) == std::string::npos) return false; return true; } std::vector Timeline::Query(const TimelineQuery& query) const { std::vector matched; std::lock_guard lock(mutex); for (const TimelineEntry& entry : entries) { if (Matches(query, entry)) matched.push_back(entry); } if (query.last != 0 && matched.size() > query.last) matched.erase(matched.begin(), matched.end() - static_cast(query.last)); return matched; } std::uint64_t Timeline::LastSequence() const { std::lock_guard lock(mutex); return nextSequence - 1; } std::vector Timeline::CheckSites() const { std::lock_guard lock(mutex); std::vector sites; for (const auto& [site, count] : checks) sites.push_back({ site.first, site.second, count.category, count.failures }); return sites; } void Timeline::SetListener(std::function callback) { std::lock_guard lock(mutex); listener = callback ? std::make_shared>(std::move(callback)) : nullptr; } void Timeline::Notify(const TimelineEntry& entry) const { std::shared_ptr> current; { std::lock_guard lock(mutex); current = listener; } if (current) (*current)(entry); }