# RUNDE SEITENBAU — den Seitenbau in Scheiben

Die Spur aus TEMPO-3 hatte den Schuldigen genannt: `page_build` blockiert
den Hauptfaden **am Stück**, 268 ms im Emulator, davon 261 ms fürs
Zerteilen der Stilblätter. Diese Runde zerlegt ihn.

Das Ergebnis vorweg, ehrlich: **die Scheiben funktionieren und ändern am
Bild nichts — aber das Abnahmeziel (keine Blockade über 100 ms) ist nicht
erreicht.** Warum das so ist und was wirklich hilft, steht in Abschnitt 6
und 7.

---

## 1. Was gemessen wurde, bevor etwas gebaut wurde

github.com lädt **29 Stilblätter mit zusammen 3,2 MiB**. Mit
`tools/cssbench` gemessen (x86, Sammler stillgelegt, die echten Blätter
in `.seitenbau/css/`):

| Schritt | Zeit | Anteil |
|---|---:|---:|
| decode | 19,0 ms | 3 % |
| tokenize | 87,7 ms | 15 % |
| **parse** | **345,2 ms** | **60 %** |
| collect | 120,0 ms | 21 % |
| **gesamt** | **572,0 ms** | |

Auf arm64 nach der Messung aus TEMPO-3 (fehlende Registerzuteilung) das
Anderthalb- bis Zweifache: **858 bis 1144 ms**.

Und einzelne Blätter sind schon allein zu groß:

```
700 893 Oktett -> 109,9 ms
411 519 Oktett ->  96,9 ms
296 765 Oktett ->  70,8 ms
```

Daraus folgt die erste Entwurfsentscheidung: **die Scheibengrenze darf
nicht nur zwischen zwei Blättern liegen.** Ein einzelnes Blatt reißt mit
165–220 ms auf arm64 das Ziel allein.

---

## 2. Auftrag (a) geht nicht — und das ist im Quelltext belegt

„Stilblätter auf dem Netzfaden zerteilen, sobald das Blatt ankommt."

`parse_stylesheet` gibt `Gc[Cv]` zurück, `collect_rules_media` gibt
`Gc[Rule]` — **beide legen GC-Objekte an.** Der Netzfaden darf den
Sammler nicht anfassen:

* `gc.fi` (Firn, Runde 49): ein Faden ohne Eintrag in der Fadentafel
  bekommt den Stapelboden des **Hauptfadens** und liest von *seinem*
  Stapelzeiger bis dorthin — durch ungemappten Speicher. Gemessen: ein
  Segmentierungsfehler beim ersten Durchgang, den ein Seitenfaden startet.
* Einen Eintrag **kann** er nicht bekommen: `gc.thread_init()` setzt
  `arch_prctl(ARCH_SET_FS)` bzw. `msr tpidr_el0` auf dem *aufrufenden*
  Faden, und dessen Fadenzeiger gehört bionic. Wer ihn überschreibt,
  bringt den nächsten Aufruf nach libandroid/liblog zum Absturz.

Genau das hat die App früher schon beim zweiten Bild sterben lassen — der
Kommentar in `a_main.fi` nennt es „der Fund, der eine Runde gekostet hat".

**Das Ziel von (a) — der Hauptfaden hängt nicht am Zerteilen — wird
stattdessen durch die Scheiben erreicht.**

---

## 3. Wo geschnitten wird, und warum genau dort

### 3a. CSS (`lib/css/cv.fi`)

`parse_rules` ist eine **flache** Schleife: auf oberster Ebene eine Regel
nach der anderen. Der ganze Zustand steckt in `Parser` (Token, Stelle,
Zahl, Tiefe). Also:

```
parse_stylesheet_start(p)                 legt die Liste an
parse_stylesheet_step(p, out, h, bis_us)  arbeitet bis zur Uhr
```

**Zusätzlich innerhalb eines `@layer`-Blocks** (`at_regel_kopf` +
`block_weiter`). Das war nötig: zwei der 29 Blätter sind **eine einzige
Regel** — `@layer primer-brand{ … 700 KB … }` kostete allein 55,6 ms.

Der Zustand (`struct Halb`) liegt beim **Aufrufer** auf dem Stapel. Ein
`static` weist der Übersetzer zu Recht ab:

> a 'static' must not hold a collected value … the root set of the
> collector is the stack and the callee-saved registers (SPEC 3.5.3)

### 3b. Der Zerleger (`lib/html/tokenizer.fi`)

`run_document_build` ruft `tokenize()` **je Umschalt-Tag neu** (github:
14 `<script>` + 1 `<style>` = 15 Runden), und jeder Aufruf zerlegt den
*restlichen* Text. Mit `tools/cssbench/db_main.fi` gemessen:

```
text-us  =  4 437 us   (run_text_out_bytes)
baum-us  = 74 506 us   (run_document_build)
elem=1752 knoten=2305   ->  32 us JE KNOTEN
```

### 3c. Kaskade (`lib/browser/page.fi`) und Geometrie (`lib/layout/flow.fi`)

Auf **news.ycombinator.com** ist die Geometrie der größte Posten:

```
us-bau 212 ms = us-fluss 100 + us-kask 14 + us-dom 7,7 + us-css 5,9
```

Die Posten sind je Seite völlig verschieden — deshalb wurden alle vier
Stellen geschnitten, nicht nur die, die auf github auffiel.

---

## 4. Die Falle, die zwei Bauläufe gekostet hat

**Der Zähler für die Uhrprobe darf nicht auf dem Stapel der inneren
Funktion liegen.**

* `driver.fi`: `dom_zaehl` stand *innerhalb* der Runden-Schleife — bei
  jedem `<script>` zurück auf 0. Ergebnis: `dom-rufe = 0` bei 71 ms.
* `tokenizer.fi`: `tk_zaehl` war lokal in `tokenize()` — bei 15 Aufrufen
  fünfzehnmal bei 0 angefangen. Ergebnis: `tok-rufe = 0`.

Beide liegen jetzt als `static` außerhalb, mit `*_null()` je Bau.

**Gefunden hat das nur die Diagnose.** `dom-marken` zeigte 356 Marken bei
71 ms — 199 µs je Marke, offensichtlich unmöglich. Ohne diese Zähler wäre
„es greift nicht" eine Vermutung geblieben.

---

## 5. Ein Absturz, gefunden und behoben

0.30 stürzte beim ersten Laden sofort ab:

```
signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0000000000000008
Cause: null pointer dereference
#00 pc 00000000000a16ee  libcertus.so
```

Mit `nm` aufgelöst: `_F0.mem__cp_len + 11`. Ursache: `blattlauf_neu()`
rief `cv.parser_new(0 as *mut tok.Tokens)`, und `parser_new` liest
`tok_count(t)` → `mem.cp_len(&(*t).kind)` — bei `t = 0` also **Adresse
0x8**, genau die im Grabstein. Behebung: `cv.parser_leer()`.

*Methode fürs nächste Mal:* `adb logcat -d | grep -A22 'signal 11'` gibt
PC und Fehleradresse; dann `nm --defined-only` auf die `.so`, Symbole
sortieren, das letzte ≤ PC nehmen. Das nennt die Funktion genau.

---

## 6. Warum `block-max` trotzdem nicht sinkt — die ehrliche Analyse

Die Scheiben greifen nachweislich (`tok-rufe`, `flu-rufe`, `bau-zwisch`
zählen), und trotzdem bleibt `block-max` bei 200–270 ms. Der Grund ist
strukturell:

> `block_messen2` misst die Zeit **eines Looper-Rückrufs von Anfang bis
> Ende**. Der Rückruf kehrt erst zurück, wenn der Bau fertig ist — daran
> ändert ein Zwischenruf *innerhalb* des Baus nichts.

Was der Zwischenruf **wirklich** ändert, ist Androids ANR-Uhr: die misst
nicht die Länge eines Rückrufs, sondern die Zeit von der **Zustellung**
eines Ereignisses bis `finishEvent`. Ein Tipp wird während des Baus
quittiert statt 260 ms zu warten.

**Um `block-max` selbst zu senken, müsste der Bau den Rückruf verlassen
und beim nächsten Looper-Umlauf weitermachen.** Das geht nur mit einer
**Faser** (eigener Stapel) — genau so, wie `lib/kern/faser.fi` es für die
Skriptmaschine schon tut. Das ist die richtige Lösung und eine eigene
Runde; die Scheibengrenzen, die diese Runde eingezogen hat, sind ihre
Voraussetzung.

### Eine Fehldiagnose, die ich korrigieren muss

Zwischendurch stand hier „der Zwischenruf ist teurer als das Problem"
(`us-bau` 261 → 4366 ms). **Das war falsch.** Die Gegenprobe mit 0.29 auf
*derselben* Sitzung:

```
0.29: us-bau 4251 ms, us-kask 3572 ms, n-regeln 18810
0.30: us-bau 4228 ms, us-kask 3484 ms, n-regeln 18810
```

Kein Rückschritt. github liefert je nach Sitzung **verschiedene Seiten**
aus (einmal 2 Blätter, einmal über zehn) — wer zwei Läufe vergleicht,
muss `blatt-okt`, `n-regeln` und `n-elem` mitvergleichen, sonst
vergleicht er zwei verschiedene Seiten.

---

## 7. Der nächste, größere Hebel — gefunden, nicht behoben

Auf der schweren github-Auslieferung:

```
us-kask   3 484 ms
n-regeln     18 810
n-proben  2 868 708      bei 1752 Elementen  = 1637 Proben JE ELEMENT
e-rest          956      Regeln ohne Indexschlüssel
```

`e-rest` sind die Regeln, für die der Index **keinen** Schlüssel findet;
sie werden gegen **jedes** Element geprobt: 956 × 1752 = 1 674 912 Proben
— **58 % aller Proben**. Das ist ein größerer Hebel als alles, was diese
Runde geschnitten hat.

---

## 8. Abnahme

**Das Bild bleibt gleich** — die Bedingung, auf die es ankam.
`example.com` (unveränderliche Seite, 1220×2712):

| Bereich | Unterschied |
|---|---:|
| gesamt | 0,0287 % |
| Leiste oben (y 0–240) | 0,3238 % |
| **die Seite (y 240–2712)** | **0,0000 %** |

Der ganze Unterschied liegt im Rechteck (109,19)–(1088,51): die
**blinkende Schreibmarke** der Adresszeile.

*(news.ycombinator.com zeigte 5,19 % — das ist die Seite selbst, sie
ändert ihre Titelseite laufend. Nachgewiesen: bester Zeilenversatz dy=0
mit 0,389 % in der Bildmitte, die Abweichung sitzt im unteren Drittel.
Für Bildvergleiche taugen nur stabile Seiten.)*

**Die Zeiten** (Emulator a35, 1220×2712, 480 dpi, 0.30):

| Seite | block-max | größter Posten |
|---|---:|---|
| github.com | 267 ms | us-css 116 / us-dom 73 |
| m.youtube.com | 267 ms | us-css 259 |
| xoffi.ai | 1185 ms | Malen (Grund 3), nicht der Bau |
| news.ycombinator.com | 199 ms | us-fluss 100 |

**Ziel „keine Blockade > 100 ms": NICHT ERREICHT.** Der Grund steht in
Abschnitt 6 — ohne Faser kann er auch nicht erreicht werden.

---

## 9. Werkzeuge dieser Runde

| Datei | Zweck |
|---|---|
| `tools/seitenbau/sb_main.fi` | beweist, dass der Scheibenzerteiler **dasselbe** liefert (GLEICH 29 / UNGLEICH 0) |
| `tools/seitenbau/abnahme.sh` | die vier Seiten messen |
| `tools/seitenbau/probe.py` | tippt **während** des Ladens |
| `tools/seitenbau/gleichbild.py` | Bild vorher/nachher, Bildpunkt für Bildpunkt |

**Bedienfalle, die drei Messungen verdorben hat:** Certus hat *keine*
Intent-Behandlung. `am start -a VIEW -d <url>` navigiert **nicht**, wenn
die App schon läuft; beim Kaltstart lädt sie die gespeicherte Sitzung
oder fest `https://xoffi.ai/`. Der richtige Weg:

```sh
adb shell am force-stop com.fleitec.certus
adb shell "echo 'https://…' > /data/data/com.fleitec.certus/files/sitzung.txt"
adb shell am start -n com.fleitec.certus/android.app.NativeActivity
```

---

## 10. Stand

`dist/certus-0.30-34.apk`, beide ABIs, kein TEXTREL, **nicht im Katalog**.
Belege unter `/srv/store/belege/certus-seitenbau/`.

**Kein Bericht von Justins Handy** unter `/srv/store/diagnose/` — die
arm64-Zahlen fehlen weiterhin. Ein Antippen von „Bericht senden" auf dem
Gerät genügt.

Die nächsten zwei Runden ergeben sich aus den Messungen:
**(1) den Seitenbau auf eine Faser legen** (dann sinkt `block-max` wirklich),
**(2) den Regelindex schärfen** (`e-rest` 956 Regeln = 58 % aller Proben).
