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:
2026-10-13 08:23:30 -05:00
co-authored by Claude Opus 4.8
parent 70e96f75f0
commit 116a997a3a
4 changed files with 100 additions and 16 deletions
+31 -7
View File
@@ -7313,19 +7313,22 @@ void testConsoleWrapCache()
EXPECT_EQ(c2.size(), static_cast<std::size_t>(0));
}
// 7) ConsoleModel::firstSeq: +1 per evicted line, +size() on clear(), untouched by fold toggles.
// 7) ConsoleModel::firstSeq advances only when a line leaves the FRONT of lines_ (cap eviction or
// clear); a pending line dropped by the byte/line budget never entered lines_ and does not advance
// it. Untouched by fold toggles. (cap 3: 0..4 ingested -> pending drops "0","1" before they reach
// lines_, so lines_=[2,3,4] with zero front evictions -> firstSeq 0.)
{
ConsoleModel cm(3);
for (int i = 0; i < 5; ++i) cm.ingest(std::to_string(i), ConsoleChannel::None);
cm.drain();
EXPECT_EQ(cm.firstSeq(), static_cast<std::uint64_t>(2));
EXPECT_EQ(cm.firstSeq(), static_cast<std::uint64_t>(0)); // "2","3","4" land; nothing evicted from front
cm.ingest("{", ConsoleChannel::JsonBrace, 2);
cm.drain();
EXPECT_EQ(cm.firstSeq(), static_cast<std::uint64_t>(3));
EXPECT_EQ(cm.firstSeq(), static_cast<std::uint64_t>(1)); // "{" pushes over cap -> evict "2" from front
cm.toggleCollapsed(2);
EXPECT_EQ(cm.firstSeq(), static_cast<std::uint64_t>(3));
EXPECT_EQ(cm.firstSeq(), static_cast<std::uint64_t>(1));
cm.clear();
EXPECT_EQ(cm.firstSeq(), static_cast<std::uint64_t>(6));
EXPECT_EQ(cm.firstSeq(), static_cast<std::uint64_t>(4)); // + lines_.size() (3) on clear
}
}
@@ -7536,13 +7539,34 @@ void testConsoleModel()
ConsoleModel m(3); // cap = 3
for (int i = 0; i < 5; ++i) m.ingest(std::to_string(i), ConsoleChannel::None);
auto dr = m.drain();
EXPECT_EQ(dr.added, static_cast<size_t>(5));
EXPECT_EQ(dr.popped, static_cast<size_t>(2));
EXPECT_EQ(dr.added, static_cast<size_t>(3)); // pending is capped at ingest: "0","1" never reach the deque
EXPECT_EQ(dr.popped, static_cast<size_t>(0));
EXPECT_EQ(dr.dropped, static_cast<size_t>(2));
EXPECT_EQ(m.size(), static_cast<size_t>(3));
EXPECT_EQ(m[0].text, std::string("2")); // "0" and "1" evicted
EXPECT_EQ(m[2].text, std::string("4"));
}
// Pending byte budget keeps the newest lines; a single oversized line is never dropped.
{
ConsoleModel m(100, 10);
for (int i = 0; i < 5; ++i) m.ingest("abcd", ConsoleChannel::None);
auto dr = m.drain();
EXPECT_EQ(dr.added, static_cast<size_t>(2));
EXPECT_EQ(dr.dropped, static_cast<size_t>(3));
ConsoleModel big(100, 10);
big.ingest(std::string(50, 'x'), ConsoleChannel::None);
EXPECT_EQ(big.drain().added, static_cast<size_t>(1));
// ingestBatch appends a block contiguously.
std::vector<dragonx::ui::ConsoleModelLine> blk(3);
blk[0].text = "B0"; blk[1].text = "B1"; blk[2].text = "B2";
ConsoleModel bm;
bm.ingestBatch(std::move(blk));
bm.drain();
EXPECT_EQ(bm.size(), static_cast<size_t>(3));
EXPECT_EQ(bm[2].text, std::string("B2"));
}
// clear() drops the visible lines but not lines still queued for the next drain.
{
ConsoleModel m;