Public Access
Site: the devlog, the blog's roro9stack posts in the site's style; Devlog replaces Blog in the navigation
Site / build (pull_request) Successful in 9s
Site / build (pull_request) Successful in 9s
Seven posts imported into site/content/devlog/ with their text unchanged, links between them pointing to /devlog/, and shortcodes (sign, cast, steps, asides, folded sections, diagrams, captions) restyled in the site's palette. The 17 inline SVG diagrams carried <style> blocks and style attributes the site's Content-Security-Policy refuses: their rules moved to devlog-diagrams.css, and a diagram's minimum width is a class. An Atom feed at /devlog/atom.xml. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 4.3 KiB |
@@ -0,0 +1,59 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 330" role="img" aria-label="The path of one Gemini fetch. The Gemini App asks for a URL and goes on drawing. A short-lived Gemini task opens a TLS connection to port 1965 with verification off, reads the server's certificate fingerprint and compares it with the one pinned for that host: the first time it pins it, if it changed it stops and the App asks. It sends the URL, reads the status line, and streams the body to a cache file on the SD card in 1 KB pieces, each written by the storage task. The connection closes and its memory comes back. Then the page is loaded into memory from the card, as far as the 40 KB floor allows; a bigger page is windowed and read as you scroll. The App gets the page.">
|
||||
<defs>
|
||||
<marker id="gx-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="currentColor"/></marker>
|
||||
<marker id="gx-arrow-hot" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="f-accent" d="M0,0 L10,5 L0,10 z"/></marker>
|
||||
<marker id="gx-arrow-bad" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="f-red" d="M0,0 L10,5 L0,10 z"/></marker>
|
||||
</defs>
|
||||
<g font-family="JetBrains Mono, ui-monospace, monospace" fill="currentColor">
|
||||
<rect class="gx-panel" x="16" y="20" width="150" height="70" rx="8"/>
|
||||
<text x="28" y="44" font-size="12" font-weight="600">Gemini App</text>
|
||||
<text x="28" y="64" font-size="11" class="gx-dim">asks, then keeps</text>
|
||||
<text x="28" y="80" font-size="11" class="gx-dim">drawing</text>
|
||||
|
||||
<rect class="gx-hot" x="210" y="20" width="534" height="200" rx="8"/>
|
||||
<text x="224" y="42" font-size="12" font-weight="600">Gemini task (one job at a time, gone when idle)</text>
|
||||
|
||||
<rect class="gx-box" x="224" y="56" width="150" height="66" rx="6"/>
|
||||
<text x="236" y="76" font-size="11" font-weight="600">1 TLS, port 1965</text>
|
||||
<text x="236" y="94" font-size="10" class="gx-dim">fingerprint vs</text>
|
||||
<text x="236" y="108" font-size="10" class="gx-dim">the pinned one</text>
|
||||
|
||||
<rect class="gx-box" x="392" y="56" width="150" height="66" rx="6"/>
|
||||
<text x="404" y="76" font-size="11" font-weight="600">2 URL, status</text>
|
||||
<text x="404" y="94" font-size="10" class="gx-dim">"20 text/gemini"</text>
|
||||
<text x="404" y="108" font-size="10" class="gx-dim">or 1x 3x 4x 5x</text>
|
||||
|
||||
<rect class="gx-box" x="560" y="56" width="170" height="66" rx="6"/>
|
||||
<text x="572" y="76" font-size="11" font-weight="600">3 body to the card</text>
|
||||
<text x="572" y="94" font-size="10" class="gx-dim">1 KB pieces, by the</text>
|
||||
<text x="572" y="108" font-size="10" class="gx-dim">storage task</text>
|
||||
|
||||
<rect class="gx-box" x="392" y="142" width="150" height="62" rx="6"/>
|
||||
<text x="404" y="162" font-size="11" font-weight="600">4 connection</text>
|
||||
<text x="404" y="178" font-size="11" font-weight="600"> closed</text>
|
||||
<text x="404" y="194" font-size="10" class="gx-dim">~45 KB back</text>
|
||||
|
||||
<rect class="gx-box" x="560" y="142" width="170" height="62" rx="6"/>
|
||||
<text x="572" y="162" font-size="11" font-weight="600">5 into memory</text>
|
||||
<text x="572" y="178" font-size="10" class="gx-dim">as far as 40 KB allows;</text>
|
||||
<text x="572" y="194" font-size="10" class="gx-dim">bigger: windowed</text>
|
||||
|
||||
<path class="gx-line" d="M374 89 H388" marker-end="url(#gx-arrow)"/>
|
||||
<path class="gx-line" d="M542 89 H556" marker-end="url(#gx-arrow)"/>
|
||||
<path class="gx-line" d="M645 122 V128 H467 V138" marker-end="url(#gx-arrow)"/>
|
||||
<path class="gx-line" d="M542 173 H556" marker-end="url(#gx-arrow)"/>
|
||||
|
||||
<path class="gx-line-hot" d="M166 55 H206" marker-end="url(#gx-arrow-hot)"/>
|
||||
<text class="f-accent" x="172" y="48" font-size="10">URL</text>
|
||||
<path class="gx-line-hot" d="M645 204 V260 H91 V94" marker-end="url(#gx-arrow-hot)"/>
|
||||
<text class="f-accent" x="300" y="254" font-size="10">the page, or a window of it</text>
|
||||
|
||||
<rect class="gx-warn" x="224" y="142" width="150" height="62" rx="6"/>
|
||||
<text class="f-red" x="236" y="162" font-size="11" font-weight="600">changed?</text>
|
||||
<text x="236" y="178" font-size="10" class="gx-dim">stop; the App shows</text>
|
||||
<text x="236" y="194" font-size="10" class="gx-dim">both, and asks</text>
|
||||
<path class="gx-line-bad" d="M299 122 V138" marker-end="url(#gx-arrow-bad)"/>
|
||||
|
||||
<text x="16" y="300" font-size="11" class="gx-dim">No card: steps 3 and 5 happen in memory instead, under the same two floors.</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
@@ -0,0 +1,169 @@
|
||||
+++
|
||||
title = '''A browser in the RAM IRC left over'''
|
||||
description = '''roro9stack gets a Gemini client: pages over TLS with trust on first use, links, search prompts, bookmarks, and pages saved to the SD card to read offline. On a device where IRC already holds most of the memory, every page goes through the card, and the big ones are read from it as you scroll.'''
|
||||
date = 2026-10-05T02:10:00+02:00
|
||||
|
||||
[extra]
|
||||
topics = '''ESP32-S3 · Gemini · TLS'''
|
||||
read_label = '''Read it, offline if you like →'''
|
||||
uid = '''<b>gemini:</b> 20 text/gemini, 1184 bytes'''
|
||||
dek = "Gemini is a small internet: text pages, links, one request per TLS connection, no cookies, no scripts, no ads. It's the obvious thing to browse from a [keyboard computer](/devlog/roro9stack/) with a 240×135 screen. It took one night to write. Most of that night went into a question Gemini itself never asks: where do you put a 31 KB page when the largest free block of memory is 31 KB, and IRC wants it too?"
|
||||
byline = '''designed by interrogation, then by two decisions I hadn't planned to make, and one I had to take back'''
|
||||
|
||||
[extra.sign]
|
||||
label = "Times the heap fell to 436 bytes"
|
||||
note = "It got up again. Most of us did."
|
||||
count = "1"
|
||||
tone = "red"
|
||||
|
||||
[[extra.cast]]
|
||||
name = "Gemini"
|
||||
role = "port 1965, TLS, text/gemini"
|
||||
text = "A protocol from 2019 that fits in a page: send a URL and CRLF, get a status line and a body. Its page format, gemtext, has six kinds of line, which is exactly as many as a 40-column screen needs."
|
||||
|
||||
[[extra.cast]]
|
||||
name = "The capsules"
|
||||
role = "geminiprotocol.net, Kennedy, Cosmos, Bubble"
|
||||
text = "Gemini's sites are called capsules. These four were the test bench: the project's own, a search engine, an aggregator that redirects, and a bulletin board full of emoji my fonts can't draw."
|
||||
|
||||
[[extra.cast]]
|
||||
name = "The heap"
|
||||
role = "about 68 KB free with IRC connected"
|
||||
text = "What's left once Wi-Fi, IRC over TLS, the screen and the debug tools have taken theirs. A second TLS connection needs about 45 KB of it, for a while."
|
||||
|
||||
[[extra.cast]]
|
||||
name = "The SD card"
|
||||
role = "7.3 GB, mostly empty"
|
||||
text = "Where the memory problem went to be solved. Pages stream through it, and Saved Pages live on it."
|
||||
+++
|
||||
|
||||
## TL;DR
|
||||
|
||||
- **A Gemini App** on the Cardputer: gemtext rendered for a 40-column screen, Tab between links, Enter to follow, Back to where you were, `g` for an address.
|
||||
- **Trust on first use**, as Gemini expects: each capsule's certificate is pinned the first time; if it changes, the page stops and a dialog shows both fingerprints.
|
||||
- **Redirects, input prompts (searching works) and server errors** handled; links to the web are refused politely.
|
||||
- **Bookmarks** with `b`, and **Saved Pages** with `s`, or `S` for a page and everything it links to on the same capsule. They're listed on the start page and **read offline**.
|
||||
- **Every page streams through the SD card**, so it arrives whole even with IRC connected, and **pages bigger than memory are read from the card as you scroll**.
|
||||
- Two memory floors, measured on the device, and one dip I decided to accept.
|
||||
- Tagged **v0.5.0**; the code is [on my Gitea](https://git.twis.la/twisla/roro9stack/src/tag/v0.5.0).
|
||||
|
||||
## Why Gemini
|
||||
|
||||
The Cardputer already talks IRC and walks the Wi-Fi spectrum. A browser was the natural next app, and the web, with its megabyte pages and its JavaScript, is not something an ESP32-S3 with 340 KB of RAM should be asked to render. Gemini is. A page is a text file. A request is one line. The format is so small that the [whole specification](https://geminiprotocol.net/docs/protocol-specification.gmi) reads in a coffee break, and the community writes for exactly this kind of screen: gemlogs, link lists, bulletin boards, search engines, all in plain text.
|
||||
|
||||
It also wasn't on the plan. Milestone three, the LoRa radio, was next, so this became a side milestone, **G1**, with the same routine as the others: questions first, tests first, measured on the device.
|
||||
|
||||
## The cast
|
||||
|
||||
{{ cast() }}
|
||||
|
||||
## Designed by interrogation, round four
|
||||
|
||||
Sixteen questions this time, Q70 to Q85: trust on first use, which responses to handle, how big a page can be, how gemtext should look on 240 pixels, which keys do what, what the start page shows. Then a late request of my own: **save pages to the card to read later, offline**. That added four more questions (Q82 to Q85) and a word to the glossary. A *Saved Page* is kept until you delete it, and Storage Clean-up never offers it by age, the same rule as Notes: you chose to keep it.
|
||||
|
||||
Two more questions came up only once the device had answered some of mine, and one of those I answered twice. They're the memory story below.
|
||||
|
||||
## The parsers, tested first
|
||||
|
||||
Before any network code, three small parsers, host-tested like everything else in this project:
|
||||
|
||||
- **URLs.** A gemtext link is usually relative (`docs/faq.gmi`, `../`, `?q`), so the client has to resolve it the way RFC 3986 says, dot segments and all. The [resolver](https://git.twis.la/twisla/roro9stack/src/tag/v0.5.0/lib/gemini/src/gemini_url.cpp#L61) passes the RFC's own 32 reference examples, normal and abnormal, with `gemini://` in place of `http://`. `../../../g` from three levels deep is `gemini://a/g`, in case you were wondering. Nobody ever is, until a link breaks.
|
||||
- **The response header:** two digits, a space, up to 1024 bytes of meta. `20 text/gemini; charset=utf-8` is a page, `31 gemini://…` a redirect, `10 Enter search query` a prompt.
|
||||
- **Gemtext**, line by line: three heading levels, lists, quotes, links, and preformatted blocks between ` ``` ` lines, where nothing is interpreted.
|
||||
|
||||
Then a fourth piece I hadn't planned, for the fonts. They're Latin-1: accents fine, emoji and CJK not. So text goes through a filter that keeps everything up to U+00FF and turns the rest into `?`. Bubble, the bulletin board, labels its links with emoji, and on the Cardputer they read `? Subspaces` and `? Help`. Honest, if not pretty.
|
||||
|
||||
## A fetch, and whose certificate it is
|
||||
|
||||
{{ diagram(src="fetch.svg", min_width=580, caption="One fetch, start to finish. The App never waits: a TLS handshake takes a second or two, and the main loop's watchdog bites after five.") }}
|
||||
|
||||
Each fetch runs on a short-lived task of its own. A TLS handshake can take two seconds, and the main loop has a watchdog since the last milestone. Also, an idle browser should cost nothing, not a 6 KB stack sitting around waiting.
|
||||
|
||||
Gemini capsules mostly use self-signed certificates, so checking them against a certificate authority would reject half of Geminispace. The convention is **trust on first use**: the first time the Cardputer meets a capsule, it [pins the certificate's SHA-256](https://git.twis.la/twisla/roro9stack/src/tag/v0.5.0/src/services/gemini_service.cpp#L100) in NVS. If the certificate is ever different, the page doesn't load; a dialog shows the old and new fingerprints and asks. The IRC client already did exactly this for self-signed IRC servers, so it was a matter of doing it again, per host and port, under a key short enough for NVS's 15-character limit (two letters and 12 hex digits of a hash).
|
||||
|
||||
{{ figure(src="gemini-browse.png", alt="Four Cardputer screens at 2x in a grid. Top left: the Gemini start page, with a help line, Bookmarks, and a bookmarked Project Gemini link. Top right: Project Gemini's home page, the title in blue, Gemini in 100 words in bold, and wrapped text. Bottom left: further down the page, a link highlighted: Or, if you'd prefer, here's a video overview. Bottom right: the same screen after pressing Enter on it, with Not Gemini: https://www.youtube.com/watch?v=DoE in orange where the URL was", width=976, height=556, landscape=true, full=true, caption=`The start page, Project Gemini, a selected link, and what happens when that link is to YouTube. Captured over Wi-Fi with the Debug Console's screenshot command.`) }}
|
||||
|
||||
## Where to put a page
|
||||
|
||||
This is where the night went.
|
||||
|
||||
The first version read the body into one `std::string`. Then Cosmos, an aggregator, came back cut short at 8.7 KB with 107 KB free. Two reasons. Growing a string copies it into a block twice its size, and my check, rightly, refused to let that happen near the floor. Worse: with IRC connected, the largest free block of memory is about **31 KB**. A 64 KB page in one piece was never going to happen, however much memory was free in total.
|
||||
|
||||
So a page became a [`TextBuffer`](https://git.twis.la/twisla/roro9stack/src/tag/v0.5.0/lib/gemini/src/text_buffer.h#L13): lines packed into 4 KB chunks, with an index of 6 bytes per line. No big block, no doubling copies. Storing each line as its own string would have cost about 40 bytes a line in overhead; on a 400-line page, that's a page.
|
||||
|
||||
Then the next wall. While the TLS connection is open it holds about 45 KB, so a single "stay above 40 KB" check during the download left room for barely 20 KB of page, and with IRC connected, none. Cosmos stopped at **4.6 KB**. But the connection's memory comes back the moment it closes. So, first decision: **two floors**. Above 40 KB once the page is in; above 20 KB while the connection is open. And no fetch starts below 55 KB free.
|
||||
|
||||
Without IRC, Cosmos now arrived whole: 31.6 KB, 419 lines. With IRC, it still stopped at 4.6 KB, because during the transfer the heap sits right at the 20 KB line. Second decision: **every page streams to the card**. While the connection is open, the body goes to a cache file on the SD card in 1 KB pieces, each written by the storage task, which owns every card access. Only once the connection has closed and given its 45 KB back is the page loaded into memory, as much of it as the steady floor allows. Cosmos with IRC connected: the whole page on the card, 20 KB of it on screen.
|
||||
|
||||
And the rest of it? That was "Cut short: only part of it fits in memory", until I asked for the obvious.
|
||||
|
||||
## Reading from the card as you scroll
|
||||
|
||||
{{ diagram(src="window.svg", min_width=580, caption="A page bigger than memory: indexed once, read a window at a time. Cosmos with IRC connected: 226 of its 419 lines in memory, then lines 192 to 419, then back to 64 and 0 as I scrolled up again.") }}
|
||||
|
||||
Opening a page that doesn't fit, [one pass over its file](https://git.twis.la/twisla/roro9stack/src/tag/v0.5.0/src/services/gemini_service.cpp#L705) counts the lines and notes where every 64th one starts, plus whether it's inside a preformatted block, so a window starting there knows how to draw it. That's 4 bytes per 64 lines: about 1 KB for a 1 MB page. The same pass loads the first window. Scroll near the end of it and the next window is read in the background, starting at the index entry just before the line on top of the screen, so what you're reading doesn't move. The scrollbar follows your place in the whole page, not the window. Tab, at the last link in memory, pages down instead of wrapping to the top.
|
||||
|
||||
The first window read while scrolling held 20 lines. Its budget was computed with the old window still in memory, so it got what was left beside it. Now the request says how much the old window will give back. The next window was 227 lines: the whole rest of the page.
|
||||
|
||||
Pages for the screen alternate between two cache files, so the page you're reading is never overwritten by the next fetch. Background jobs, like saving a capsule's linked pages, use a third.
|
||||
|
||||
{{ figure(src="cosmos.png", alt="Cosmos on the Cardputer at 2x, deep into the page: a line ending situación de calle [Crónica], then links including Ploum.net ? Ah ouais, quand même, on en est là?! and Ploum.net ? Les mécanismes de compensation carbone expliqués à mon hamster, with a scrollbar on the right about two thirds down", width=480, height=270, caption=`Two thirds of the way down Cosmos, read from the card with IRC connected. Accents fine, emoji as question marks, hamsters explained.`) }}
|
||||
|
||||
## The dip I accepted
|
||||
|
||||
{{ diagram(src="memory.svg", min_width=580, caption="Free heap around one fetch with IRC connected. Before, during the handshake, while the body streams to the card, and once the connection has closed and the page is loaded.") }}
|
||||
|
||||
With IRC's TLS connection open and a Gemini one alongside, the lowest free heap during a fetch came out at 24 KB the first time I measured it, then 19.5, 15.5 and **13 KB**. Same page, same code. The receive buffers grow with the size of the records the server sends, so the dip depends on the other end. My own code keeps its allocations above 20 KB; the TLS stack doesn't ask.
|
||||
|
||||
The options were: refuse to fetch below 70 KB, which with IRC connected would refuse almost every fetch; close IRC's connection for every page; or accept a dip to about 12 KB for a second or two. I took the third. It's written down with the numbers, in the plan's own words, as a decision and not an accident.
|
||||
|
||||
## Saved Pages
|
||||
|
||||
{{ figure(src="gemini-tools.png", alt="Four Cardputer screens at 2x in a grid. Top left: an input prompt over a page, Enter search query, with an empty input box. Top right: Kennedy search results, 'cardputer' - ? Kennedy Search, 2 matches on ? Image Search, Showing 1 - 15 of 87 results, 1. M5Stack Cardputer. Bottom left: a Saved Page, the URL bar reading saved 2026-10-05 01:13, a quote line Saved from gemini://geminiprotocol.net/docs/faq.gmi on 2026-10-05 01:13, then Project Gemini FAQ. Bottom right: a dialog, Certificate changed, geminiprotocol.net now shows a different certificate, with Cancel and Trust it buttons", width=976, height=556, landscape=true, full=true, caption=`Kennedy's search prompt and its 87 results for "cardputer"; a Saved Page, which says where and when it came from; and the dialog for a changed certificate, which I faked by pinning a wrong fingerprint on purpose.`) }}
|
||||
|
||||
`s` copies the page from the cache file to `/gemini/saved/<capsule>/<path>.gmi`, with one line added at the top: `> Saved from <url> on <date>`. It shows as a quote, so you know what you're reading, and it gives relative links their base. `S` saves the page and everything it links to on the same capsule, up to 30 pages, in the background with progress Toasts: Project Gemini and its five linked pages, six of six, in about twenty seconds.
|
||||
|
||||
The start page lists them by capsule, newest first. Inside a Saved Page, a link to another saved page opens the saved copy, and anything else goes online if it can, or says "Not saved, and offline" if it can't. `r` refreshes a Saved Page from the network, `d` deletes it after asking.
|
||||
|
||||
## Where it hurt
|
||||
|
||||
- **The bug from the first post, again.** A status message ("Not Gemini: https://…") was cleared before it was ever drawn. It was timestamped with `millis()` after the main loop had read the clock, and in unsigned arithmetic, three milliseconds in the future is 49 days ago. That's exactly the bug that made toasts expire before they appeared in [the first post](/devlog/roro9stack/). Same fix: a signed comparison. Same feeling.
|
||||
- **A space where none was.** Wrapped link and list lines are indented, which meant re-wrapping their continuation rows. I rebuilt that text by joining the rows with spaces, so a long URL the first wrap had cut mid-word came out as `faq.gm i`. Now it re-wraps the exact rest of the line.
|
||||
- **436 bytes.** The refresh job needed to know where a Saved Page came from, so it opened the page, all of it, next to the App's copy of the same page and a fresh TLS connection. The heap's lowest point since boot: 436 bytes. Nothing crashed, which is less reassuring than it sounds. Now it reads one line, and the 55 KB start floor guards every fetch, not just the ones the App asks for. Lowest afterwards: 53.8 KB.
|
||||
- **The same trap, twice in one milestone.** A forward declaration of `NetworkClientSecure` inside the project's namespace declares a different class that doesn't exist. I'd made that mistake in the Debug Console two milestones ago. I made it again, in the same way, and the compiler explained it again, at the same length.
|
||||
- **Antenna is down.** The aggregator I meant as a default bookmark doesn't answer, from the Cardputer or from my PC. Cosmos took its place, and its redirect became the test case for redirects.
|
||||
|
||||
## By the numbers
|
||||
|
||||
{% table() %}
|
||||
| | |
|
||||
| --- | --- |
|
||||
| Commits from v0.4.0 to v0.5.0 | 9 |
|
||||
| Lines added | about 2,400, 223 of them tests |
|
||||
| Tests | 338, 14 of them new |
|
||||
| Release firmware | 1.71 MB of 3.3 MB, 127 KB more than v0.4.0 |
|
||||
| A fetch, from Enter to page | 0.7 to 2.2 s, mostly the TLS handshake |
|
||||
| Cosmos with IRC connected | 31.6 KB on the card, 226 of 419 lines in memory at first |
|
||||
| Index for a windowed page | 4 bytes per 64 lines |
|
||||
| Search results for "cardputer" | 87 |
|
||||
| Lowest free heap during a fetch with IRC | 13 KB, accepted |
|
||||
| Lowest free heap, ever, this milestone | 436 bytes, fixed |
|
||||
{% end %}
|
||||
|
||||
## Where it stands
|
||||
|
||||
{% steps() %}
|
||||
1. ~~M0 and M1: the skeleton, Wi-Fi, IRC, Wi-Fi Tools.~~ v0.1.0 to v0.2.1, [the first post](/devlog/roro9stack/).
|
||||
|
||||
2. ~~Updates and debugging over the air.~~ v0.3.0, [Look, no cables](/devlog/roro9stack-ota/).
|
||||
|
||||
3. ~~M2: GNSS.~~ v0.4.0, [Seventeen satellites](/devlog/roro9stack-gnss/).
|
||||
|
||||
4. ~~G1: Gemini, with Saved Pages to read offline.~~ v0.5.0, this post.
|
||||
|
||||
5. Next, M3: the LoRa radio. The first thing this device will ever transmit.
|
||||
{% end %}
|
||||
|
||||
{% signoff() %}
|
||||
The Cardputer now carries a small library on its SD card: capsules saved on the train, read on the plane, refreshed when Wi-Fi comes back. All of it in the memory IRC wasn't using.
|
||||
{% end %}
|
||||
@@ -0,0 +1,33 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 290" role="img" aria-label="Free heap around one Gemini fetch of Cosmos with IRC connected over TLS. About 68 KB before the fetch, above the 55 KB start floor. During the TLS handshake it dips to between 24 KB in the best run and 13 KB in the worst, under the 20 KB the firmware keeps for its own allocations: that dip, from the TLS receive buffers, is accepted. While the body streams to the card, about 21 KB. After the connection closes and part of the page is loaded, 43 KB, above the 40 KB steady floor.">
|
||||
<g font-family="JetBrains Mono, ui-monospace, monospace" fill="currentColor">
|
||||
<line class="gm-grid" x1="70" y1="230" x2="740" y2="230"/>
|
||||
<g font-size="10" class="gm-dim" text-anchor="end">
|
||||
<text x="62" y="233">0</text><text x="62" y="173">20</text><text x="62" y="113">40</text><text x="62" y="53">60 KB</text>
|
||||
</g>
|
||||
<!-- y = 230 - 3 * KB -->
|
||||
<line class="gm-floor s-muted" x1="70" y1="65" x2="740" y2="65"/>
|
||||
<text x="736" y="60" font-size="10" text-anchor="end" class="gm-dim">55 KB: no fetch starts below</text>
|
||||
<line class="gm-floor s-yellow" x1="70" y1="110" x2="740" y2="110"/>
|
||||
<text class="f-yellow" x="736" y="123" font-size="10" text-anchor="end">40 KB: left once the page is in</text>
|
||||
<line class="gm-floor s-green" x1="70" y1="170" x2="740" y2="170"/>
|
||||
<text class="f-green" x="736" y="165" font-size="10" text-anchor="end">20 KB: the firmware's own allocations</text>
|
||||
|
||||
<path class="gm-step" d="M80 26 H230 V158 H260 V167 H480 V101 H730"/>
|
||||
<path class="gm-worst" d="M230 26 V191 H260"/>
|
||||
|
||||
<g font-size="11" text-anchor="middle">
|
||||
<text x="155" y="20">68 KB</text>
|
||||
<text class="f-accent" x="245" y="152">24</text>
|
||||
<text class="f-red" x="245" y="208">13</text>
|
||||
<text x="370" y="161">~21 KB</text>
|
||||
<text x="605" y="95">43 KB, page loaded</text>
|
||||
</g>
|
||||
<g font-size="11" text-anchor="middle" class="gm-dim">
|
||||
<text x="155" y="252">before</text>
|
||||
<text x="245" y="252">handshake</text>
|
||||
<text x="370" y="252">body to the card</text>
|
||||
<text x="605" y="252">closed, loaded</text>
|
||||
</g>
|
||||
<text x="70" y="280" font-size="10" class="gm-dim">Measured on the device, Debug Build, IRC on TLS. Dashed red: the worst handshake seen. Accepted.</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1,37 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 260" role="img" aria-label="Reading a page bigger than memory. On the card, the page's file of 419 lines. Opening it, one pass records where every 64th line starts, and whether it's inside a preformatted block: 7 entries, 4 bytes each. In memory, a window of about 226 lines, of which the screen shows 8 rows. Scrolling near the end of the window asks for the next window, starting at the index entry before the line on top, so what's on screen stays; near the top, the previous one.">
|
||||
<defs>
|
||||
<marker id="gw-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="currentColor"/></marker>
|
||||
</defs>
|
||||
<g font-family="JetBrains Mono, ui-monospace, monospace" fill="currentColor">
|
||||
<text x="16" y="24" font-size="12" font-weight="600">on the card: the page, 419 lines</text>
|
||||
<rect class="gw-box" x="16" y="36" width="728" height="34" rx="4"/>
|
||||
<!-- 419 lines over 728 px: 1.737 px a line; ticks every 64 lines (111 px) -->
|
||||
<g>
|
||||
<line class="gw-tick" x1="16" y1="70" x2="16" y2="80"/><line class="gw-tick" x1="127" y1="70" x2="127" y2="80"/>
|
||||
<line class="gw-tick" x1="238" y1="70" x2="238" y2="80"/><line class="gw-tick" x1="349" y1="70" x2="349" y2="80"/>
|
||||
<line class="gw-tick" x1="461" y1="70" x2="461" y2="80"/><line class="gw-tick" x1="572" y1="70" x2="572" y2="80"/>
|
||||
<line class="gw-tick" x1="683" y1="70" x2="683" y2="80"/>
|
||||
</g>
|
||||
<g font-size="10" class="gw-dim" text-anchor="middle">
|
||||
<text x="16" y="92">0</text><text x="127" y="92">64</text><text x="238" y="92">128</text><text x="349" y="92">192</text>
|
||||
<text x="461" y="92">256</text><text x="572" y="92">320</text><text x="683" y="92">384</text>
|
||||
</g>
|
||||
<text x="16" y="112" font-size="11" class="gw-dim">index: where every 64th line starts (and if it's in a ``` block): 7 x 4 bytes</text>
|
||||
|
||||
<rect class="gw-hot" x="349" y="38" width="393" height="30" rx="3"/>
|
||||
<rect class="gw-screen" x="420" y="40" width="14" height="26"/>
|
||||
<text x="356" y="58" font-size="10">in memory: lines 192 to 419</text>
|
||||
|
||||
<text x="16" y="150" font-size="12" font-weight="600">in memory: one window</text>
|
||||
<rect class="gw-hot" x="16" y="160" width="460" height="40" rx="6"/>
|
||||
<rect class="gw-screen" x="96" y="164" width="70" height="32" rx="3"/>
|
||||
<text x="104" y="184" font-size="10">screen</text>
|
||||
<text x="180" y="185" font-size="11">~226 lines, as text in 4 KB chunks</text>
|
||||
|
||||
<path class="gw-line" d="M476 180 H520" marker-end="url(#gw-arrow)"/>
|
||||
<text x="528" y="176" font-size="11">near the end: the next window,</text>
|
||||
<text x="528" y="192" font-size="11" class="gw-dim">from the index entry before the</text>
|
||||
<text x="528" y="208" font-size="11" class="gw-dim">line on top, so the screen stays</text>
|
||||
<text x="16" y="236" font-size="11" class="gw-dim">Near the top: the previous one. The scrollbar follows the line, over the whole page.</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
Reference in New Issue
Block a user