From 30a827070b43dc10687b304911b91462933e4080 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Martin?= Date: Mon, 5 Oct 2026 19:19:52 +0200 Subject: [PATCH] Gemini: count the App's per-line tables in the page budget Found in M3 while checking floors with the radio on: a windowed page left 1.5 to 3 KB less than the 40 KB steady floor (Q86), radio or not. The budget counted TextBuffer's index (8 B/line) but not the App's tables, which also grew by doubling. Now 16 B/line, and the App sizes them exactly. The FAQ (1051 lines, windowed): 38.5 -> 39.6 KB after, radio asleep; 37.1 -> 39.0 KB with the radio listening. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT --- src/apps/gemini_app.cpp | 8 ++++++-- src/services/gemini_service.cpp | 8 ++++---- src/services/gemini_service.h | 4 ++++ 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/apps/gemini_app.cpp b/src/apps/gemini_app.cpp index b9704f0..7e7c104 100644 --- a/src/apps/gemini_app.cpp +++ b/src/apps/gemini_app.cpp @@ -103,8 +103,11 @@ void GeminiApp::show(GeminiPage&& page) { } void GeminiApp::analyse() { - types_.clear(); - links_.clear(); + // Sized exactly: the Service budgeted these bytes per line (kBytesPerLine), growth by + // doubling would take up to twice that. + std::vector().swap(types_); + std::vector().swap(links_); + types_.reserve(page_.text.lineCount()); // A window may start inside a preformatted block: the index says (bit 31). size_t entry = page_.windowStart / GeminiService::kIndexEvery; bool pre = page_.windowed && entry < page_.lineIndex.size() && (page_.lineIndex[entry] & 0x80000000u); @@ -114,6 +117,7 @@ void GeminiApp::analyse() { types_.push_back(static_cast(t)); if (t == LineType::Link) links_.push_back(i); } + links_.shrink_to_fit(); layoutDirty_ = true; requestRedraw(); } diff --git a/src/services/gemini_service.cpp b/src/services/gemini_service.cpp index 805c15d..b1842e5 100644 --- a/src/services/gemini_service.cpp +++ b/src/services/gemini_service.cpp @@ -192,7 +192,7 @@ bool GeminiService::loadWindow(const GeminiPage& page, size_t firstLine) { windowPage_.lineIndex = page.lineIndex; windowLine_ = firstLine; // The App drops its current window once the new one is in: count that memory as coming back. - windowReleasing_ = page.text.bytes() + page.text.lineCount() * 8; + windowReleasing_ = page.text.bytes() + page.text.lineCount() * kBytesPerLine; return start(Job::Window); } @@ -618,7 +618,7 @@ void GeminiService::fetchOne(const std::string& url, GeminiPage& page, bool load // what was free before minus the steady floor; meanwhile the heap must stay above the // transient floor. Both counted with the next 4 KB chunk and the line index. size_t take = std::min(n - i, kMaxBody - page.text.bytes()); - size_t pageCost = page.text.bytes() + page.text.lineCount() * 8 + TextBuffer::kChunk + take; + size_t pageCost = page.text.bytes() + page.text.lineCount() * kBytesPerLine + TextBuffer::kChunk + take; if (take < static_cast(n - i)) page.truncatedWhy = "longer than 64 KB"; else if (freeBefore < kSteadyFloor + pageCost) page.truncatedWhy = "not enough memory to keep it"; else if (esp_get_free_heap_size() < kTransientFloor + TextBuffer::kChunk + take) @@ -720,7 +720,7 @@ void GeminiService::loadFromCard(GeminiPage& page, size_t freeBefore, const std: if (page.totalLines % kIndexEvery == 0) page.lineIndex.push_back(lineStart | (pre ? 0x80000000u : 0)); if (line.compare(0, 3, "```") == 0) pre = !pre; if (loading) { - if (page.text.bytes() + page.text.lineCount() * 8 + line.size() + 1 > budget) loading = false; + if (page.text.bytes() + page.text.lineCount() * kBytesPerLine + line.size() + 1 > budget) loading = false; else { line += '\n'; page.text.append(line.data(), line.size()); @@ -768,7 +768,7 @@ void GeminiService::readWindow(GeminiPage& page, size_t firstLine, size_t budget continue; } if (lineNo >= firstLine) { - if (page.text.bytes() + page.text.lineCount() * 8 + line.size() + 1 > budget) done = true; + if (page.text.bytes() + page.text.lineCount() * kBytesPerLine + line.size() + 1 > budget) done = true; else { line += '\n'; page.text.append(line.data(), line.size()); diff --git a/src/services/gemini_service.h b/src/services/gemini_service.h index 8988f92..3b43e05 100644 --- a/src/services/gemini_service.h +++ b/src/services/gemini_service.h @@ -64,6 +64,10 @@ class GeminiService { static constexpr int kMaxLinkedPages = 30; // Q83 static constexpr size_t kIndexEvery = 64; // lines between lineIndex entries static constexpr size_t kMaxLineBytes = 8192; // longer lines are cut + // What a page costs per line on top of its text: TextBuffer's index (8) and the App's tables + // (type, first row, links: about 8). Counting only the first left the steady floor 1.5 to 3 KB + // short on a windowed page (found in M3). + static constexpr size_t kBytesPerLine = 16; static constexpr const char* kBookmarks = "/gemini/bookmarks.gmi"; static constexpr const char* kStartUrl = "about:start";