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>
This commit is contained in:
@@ -9,27 +9,60 @@
|
||||
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)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(ingest_mutex_);
|
||||
ConsoleModelLine line;
|
||||
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::vector<ConsoleModelLine> incoming;
|
||||
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));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user