Files
ObsidianDragon/src/ui/windows/console_model.cpp
T
DanSandClaude Opus 4.8 116a997a3a fix(console): bound the pending queue by lines and bytes
The console's pending, not-yet-drained queue was unbounded, so a hidden
Console page that kept receiving verbose daemon-log chunks could grow
without limit.

- Pending lines are now bounded by both a line count and a byte budget.
  The oldest pending lines are dropped once either is exceeded, while
  the newest line is always kept so one huge line is never lost.
- A formatted RPC result is ingested as one block under a single lock,
  so lines from other producers can never interleave inside it.

The front-sequence accounting is also corrected: a pending line dropped
by the cap never entered the visible buffer, so it no longer advances
the front sequence. Only a front eviction or a clear does.

Tests: the line and byte caps keep the newest lines, a single oversized
line survives, a batch appends contiguously, and the front sequence
advances only on a front eviction or clear.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 08:23:30 -05:00

95 lines
3.0 KiB
C++

// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "console_model.h"
#include <utility>
namespace dragonx {
namespace ui {
void ConsoleModel::trimPendingLocked()
{
// Keep at least the newest line so one huge line is never dropped on arrival. Dropping the OLDEST pending
// lines past max_lines_ is lossless for the view (drain() would evict them immediately); the byte budget
// is the only lossy part and only bites while nobody drains (Console page closed).
while (pending_.size() > 1 && (pending_.size() > max_lines_ || pending_bytes_ > max_pending_bytes_)) {
pending_bytes_ -= pending_.front().text.size();
pending_.pop_front();
++dropped_;
}
}
void ConsoleModel::ingest(const std::string& text, ConsoleChannel channel, int foldSpan)
{
ConsoleModelLine line; // build outside the lock
line.text = text;
line.channel = channel;
line.foldSpan = foldSpan;
const std::size_t bytes = line.text.size();
std::lock_guard<std::mutex> lock(ingest_mutex_);
pending_.push_back(std::move(line));
pending_bytes_ += bytes;
trimPendingLocked();
}
void ConsoleModel::ingestBatch(std::vector<ConsoleModelLine>&& lines)
{
if (lines.empty()) return;
std::size_t bytes = 0;
for (const auto& l : lines) bytes += l.text.size();
std::lock_guard<std::mutex> lock(ingest_mutex_); // ONE acquisition: the block stays contiguous
for (auto& l : lines) pending_.push_back(std::move(l));
pending_bytes_ += bytes;
trimPendingLocked();
}
ConsoleModel::DrainResult ConsoleModel::drain()
{
std::deque<ConsoleModelLine> incoming;
std::size_t dropped = 0;
{
std::lock_guard<std::mutex> lock(ingest_mutex_);
if (pending_.empty()) return {};
incoming.swap(pending_);
pending_bytes_ = 0;
dropped = dropped_;
dropped_ = 0;
}
DrainResult result;
result.added = incoming.size();
result.dropped = dropped;
// A dropped pending line never entered lines_, so it does not advance first_seq_ (the seq of
// lines_.front()); only a front eviction below or clear() does. Keeps the wrap cache aligned.
for (auto& line : incoming)
lines_.push_back(std::move(line));
while (lines_.size() > max_lines_) {
lines_.pop_front();
++result.popped;
++first_seq_;
}
++revision_; // deque changed — lets the view memoize its filter/layout passes
return result;
}
void ConsoleModel::clear()
{
first_seq_ += lines_.size(); // the discarded lines keep their sequence numbers (view caches realign by seq)
lines_.clear();
++revision_;
}
bool ConsoleModel::toggleCollapsed(std::size_t i)
{
if (i >= lines_.size() || lines_[i].foldSpan <= 0) return false;
lines_[i].collapsed = !lines_[i].collapsed;
++revision_; // fold change alters which lines are visible
return lines_[i].collapsed;
}
} // namespace ui
} // namespace dragonx