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>
95 lines
3.0 KiB
C++
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
|