feat(registry): commands, watches, samples, the timeline and conditions
The app side of Crafter.Debug, minus the session:
- Command(name, description, handler): the handler takes nothing or
one argument struct and returns void, a value, Result<T> or
Async<T>. Arguments are decoded before it runs, so a mistyped call
is answered with every mistake and never reaches it; a throwing
handler becomes an Exception error instead of unwinding into the
frame loop. This replaces 3DForts' three-file pattern per verb.
- Watch(path, variable | getter [, setter]) with bounds, help and
read-only, and Sample(path, value) from any thread with a
frame-stamped history. Paths resolve into values ("bodies[1].pos.y")
and set the same way, writing a member writes the whole value back.
Unknown paths and commands get a suggestion.
- Group: registrations under a prefix that end with their owner. Not
movable, so neither is its owner: its watches refer to the owner's
members, and a move would leave them pointing at the old object.
- Log, LogWarn, LogError, Check and Emit on one frame-stamped
timeline, with source locations captured without macros. A check
failing every frame is counted every time but stored sparsely, and
the session's listener hears every failure.
- Condition: what `wait` and `pause --when` evaluate each frame. A
condition that cannot be decided reports why instead of being false,
which would make a wait hang.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
89a12bcd61
commit
3500b73ecf
12 changed files with 1829 additions and 7 deletions
131
implementations/Crafter.Debug-Timeline.cpp
Normal file
131
implementations/Crafter.Debug-Timeline.cpp
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
// 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<double>(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<double>(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);
|
||||
}
|
||||
|
||||
std::vector<TimelineEntry> Timeline::Query(const TimelineQuery& query) const {
|
||||
std::vector<TimelineEntry> matched;
|
||||
std::lock_guard lock(mutex);
|
||||
for (const TimelineEntry& entry : entries) {
|
||||
if (entry.sequence <= query.afterSequence) continue;
|
||||
if (query.sinceFrame && entry.frame < *query.sinceFrame) continue;
|
||||
if (Level(entry.kind) < query.minLevel) continue;
|
||||
if (!query.kinds.empty() && std::ranges::find(query.kinds, entry.kind) == query.kinds.end()) continue;
|
||||
if (!query.category.empty() && !GlobMatch(query.category, entry.category)) continue;
|
||||
if (!query.grep.empty() && entry.message.find(query.grep) == std::string::npos && entry.category.find(query.grep) == std::string::npos) continue;
|
||||
matched.push_back(entry);
|
||||
}
|
||||
if (query.last != 0 && matched.size() > query.last) matched.erase(matched.begin(), matched.end() - static_cast<std::ptrdiff_t>(query.last));
|
||||
return matched;
|
||||
}
|
||||
|
||||
std::uint64_t Timeline::LastSequence() const {
|
||||
std::lock_guard lock(mutex);
|
||||
return nextSequence - 1;
|
||||
}
|
||||
|
||||
std::vector<CheckSite> Timeline::CheckSites() const {
|
||||
std::lock_guard lock(mutex);
|
||||
std::vector<CheckSite> 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<void(const TimelineEntry&)> callback) {
|
||||
std::lock_guard lock(mutex);
|
||||
listener = callback ? std::make_shared<const std::function<void(const TimelineEntry&)>>(std::move(callback)) : nullptr;
|
||||
}
|
||||
|
||||
void Timeline::Notify(const TimelineEntry& entry) const {
|
||||
std::shared_ptr<const std::function<void(const TimelineEntry&)>> current;
|
||||
{
|
||||
std::lock_guard lock(mutex);
|
||||
current = listener;
|
||||
}
|
||||
if (current) (*current)(entry);
|
||||
}
|
||||
Loading…
Reference in a new issue