The app's end of the connection, independent of the transport, which reaches it through Detail::Exchange: one request's way back, safe from any thread and never blocking. - What needs only the session's own state (hello, status, list, log, pause, resume) is answered on the transport thread. What touches the app (calls, get, set, trace, screenshot) queues for the next Pump and runs on the app's thread between frames. Waits and steps reply from the Pump that sees them satisfied, events from the thread that emits them. No request ever holds a thread while it waits. - A request whose timeout passes before Pump is answered Timeout and is guaranteed not to have run, so an agent never wonders whether a command went through; an Async result arrives on the Pump it is ready; requests left when the Session ends are answered ShuttingDown. - Advance() for frame loops and Paused()/TakeSteps() for fixed-step loops hold what the app gates on them; pausing an app that gates nothing is refused instead of silently doing nothing. pause --when stops the frame its condition holds; pause --on-check stops before the next Advance in the frame a check fails, optionally raising SIGTRAP for an attached debugger. - A wait that times out says what its condition's paths were; a log follower gets matching entries in order, from the backlog on, and is dropped when it cannot keep up. - Request parameters go through Codec, so a mistyped request gets the same did-you-mean replies a mistyped command does. The session sets PR_SET_PTRACER_ANY so a debugger the agent starts can attach. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
135 lines
4.7 KiB
C++
135 lines
4.7 KiB
C++
// 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);
|
|
}
|
|
|
|
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<TimelineEntry> Timeline::Query(const TimelineQuery& query) const {
|
|
std::vector<TimelineEntry> 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<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);
|
|
}
|