Stranger Than Usual

Noch ein Fordite-Update

Ich bin endlich mal wieder dazu gekommen, an Fordite weiterzuarbeiten.

Es gab nicht viele Verbesserungen (ich warte auch immer noch auf Rückmeldung von jemandem, der mir etwas über Berechnungen von zueinander passenden Farben sagen wollte), aber dafür gibt es auch eine neue Animation.

Verbesserung 1: Fix für Zufallsauswahl von Farben

Ich habe einen kleinen Bug behoben, der bei ein paar Farbschichtschemata dafür gesorgt hat, dass die Farben nicht komplett gleichverteilt waren. Die Änderungen sind vermutlich kaum sichtbar, aber vermutlich werden einige Konfigurationen mit demselben seed jetzt anders aussehen als vorher.

Verbesserung 2: Höhe ist jetzt Fließkommazahl statt Ganzzahl

Für die Entscheidung, welche Farbe für einen Pixel gewählt wurde habe ich ursprünglich eine ganzzahlige Höhenkarte verwendet.

Das war am Anfang auch gut, aber dann habe ich Fordite umgebaut und einige Farbschichtenschemata hinzugefügt, mit denen das Probleme bereitet. Die Rundungsfehler haben zu seltsamen Artefakten geführt.

Jetzt habe ich die Höhenberechnungen auf Fließkommazahlen umgestellt, und die Verbesserung ist deutlich sichtbar.

Vorher:

Das alte Feurgott-Fordite. Viele Kanten sind gezackt, andere Stellen blockiger als nötig

Nachher:

Das neue Feuergott-Fordite. Die Kanten sehen deutlich runder und natürlicher aus

Reguläre Kantenglättung habe ich absichtlich weiterhin nicht eingebaut. Erstens sind die pixeligen Kanten Teil des Kunstwerks, zweitens habe ich keine Lust, das einzubauen und drittens lassen sich die Bilder so viel besser komprimieren.

Verbesserung 3: Nebenläufigkeit

Bisher hat Fordite immer nur einen Thread auf einmal verwendet. Das war für viele Anwendungen gut, wenn ich allerdings die Frames für eine lange Animation erstellen wollte, dauerte mir das zu lang.

Nun gibt es vermutlich eine Menge Optimierungspotential, aber das Einfachste war vorerst, die Bildgenerierung zu parallelisieren. Da jedes Bild unabhängig von den anderen gerendert wird, und das Rendern der Bilder am meisten Zeit kostet, kann man so die Zeit auf einen Bruchteil verkürzen, so lange man entsprechend viele Prozessorkerne hat.

Die Anzahl der Threads wird hier automatisch ausgewählt, maximal das Minimum aus Anzahl zu rendernder Bilder und der auf diesem System möglichen Parallelität. Letztere habe ich mit der Rust-Funktion available_parallelism bestimmt. Das mag nicht immer das erwartete Ergebnis liefern, sollte aber in den meisten Fällen brauchbar sein.

Irgendwann baue ich vielleicht noch ein, dass man die Zahl der Threads selber bestimmen kann (als Kommandozeilenparameter oder so). Das hat aber keine Priorität. Außerdem gibt ersetzt das keine anderen möglichen Optimierungen.

Aufpassen sollte man allerdings, wenn man mehrere Konfigurationen hat, die in dieselbe Datei schreiben. Nach wie vor überprüft Fordite nicht, ob das der Fall ist. Bisher wurde dann einfach nur das letzte Bild mit einem entsprechendem Namen erstellt, alle anderen wurden überschrieben. Das ist jetzt nicht mehr stabil und kann je nach Timing auch zu Fehlern führen. Also aufpassen!

Tanz eines Feuergottes

Zuletzt habe ich endlich eine Animation fertiggestellt, die mir schon lange im Kopf herumschwirrte. Als Basis dient das oben gezeigte Feuergott-Fordite.

Der Feuergott besteht aus einer kreisförmigen Sinuskurve als Höhenkarte und zylinderförmigen Farbschichten. Im Gegensatz zu den anderen Animationen habe ich hier nicht die Höhenkarte über die Zeit verändert, sondern wie die Farbschichten erstellt werden.

Zur Erinnerung: Die zylinderförmigen Farbschichten sind Schichten, die sich zylinderförmig um eine zentrale Achse wickeln. Ich habe mir jetzt zwei Punkte genommen, die sich jeweils in einem gewissen quaderförmigen Bereich herumbewegen und von den Wänden dieses Bereiches abprallen. Durch diese zwei Punkte läuft die Achse des Zylinders.

Das Ergebnis ist wieder auf Youtube und gefällt mir recht gut.

Ursprünglich wollte ich auch die Höhenkarte verschieben. Das hat aber zu seltsamen Bildsprüngen geführt, deswegen habe ich das wieder rausgenommen. Ich hätte der Sache auch auf den Grund gehen können, aber dazu hat mir der Nerv gefehlt.

Dadurch ergibt sich eine Auffälligkeit: in der Bildmitte ist eine Art Fokuspunkt. Ob das jetzt gut ist oder nicht, lasse ich mal dahingestellt.

Für das Script zu dieser Animation gibt es noch eine ganze Menge Stellschrauben, mit denen man herumspielen kann. Wer Lust dazu hat (oder den seltsamen Bildsprüngen bei bewegter Höhenkarte auf den Grund gehen möchte), findet die Konfiguration hier.

Update 2022-11-07:

Nach kurzer Suche bin ich darauf gekommen, was beim Bewegen der Höhenkarte schiefgelaufen ist: Ein Vorzeichenfehler. Den habe ich behoben, daneben noch ein paar andere Tweaks gemacht und ein weiteres Feuergott-Video hochgeladen.

Optimierungsspaß

Zu meinem momentanen Job gehört es unter anderem, Übungsaufgaben für Studenten eines Kurses zu erstellen. Das mache ich zusammen mit einem Kollegen, und für die nächste Aufgabe kommt etwas mit DGAs dran.

DGA steht für „domain-name generation algorithm“ und bezeichnet Algorithmen, die nach bestimmten Regeln domain names erstellen, unter denen Bots in Botnets dann ihre Kontrollserver erreichen. Die Idee dabei ist, dass diese Domains sich häufig ändern, damit das Botnetz vor Entdeckung geschützt wird.

Dazu gibt es eine ganze Menge Forschung, das ist aber nicht mein Forschungsbereich. Wenn es um IT-Security geht, wäre es natürlich schön, schnell erkennen zu können welche Geräte infiziert und somit Teil eines Botnetzes sind. Ein Weg dazu ist, auf verdächtige Domainanfragen zu achten.

Klassifizierung

Dafür braucht man einen automatischen Weg, reguläre Domains von DGA-generierten Domains zu unterscheiden. Da gibt es eine Menge Methoden, darum geht es hier aber nicht. Für unsere Aufgabe haben wir einen sehr simplen Klassifikator gebaut, der von harmlosen Domains weniger als 5% als generiert einstuft (false positive), von einfachen generierten Domains hingegen mehr als 50% (true positive).

Das ist nicht super gut, aber wir brauchen es ja nur für die Übungsaufgabe und es hat wirklich kaum Zeit gebraucht, das zu implementieren. Die Idee ist, dass die Studenten einen DGA entwickeln, dessen Domains nicht so leicht erkannt werden.

Aber nun genug zu DGAs. Wichtig ist: Wir haben ein Python-script geschrieben, dass über eine Liste von Domainnamen geht und für jeden Namen entscheidet, ob er generiert ist oder nicht. Dazu haben wir vier Kriterien verwendet.

  1. Anzahl der Ziffern im Domainnamen
  2. längste Folge von Konsonanten (also Zeichenfolgen ohne Vokale)
  3. Verhältnis von Vokalen zu Konsonanten
  4. Entropie der Domain

Wenn irgendeine der Kategorien über einem gewissen Schwellwert liegt, wird die Domain als DGA-generiert eingestuft.

Zum Testen haben wir das ganze über ein paar Testlisten laufen lassen. Eine davon ist die Tranco-list, eine Liste von einer Million populärer Websites (nach irgendeinem Ranking, dass ich mir nicht näher angeschaut habe). Wie oben schon erwähnt, gibt es hier knapp 5% false positives.

Die andere Testliste ist eine Liste von knapp 3,2 Millionen arithmetisch generierter Domainnamen, also praktisch zufällige Buchstaben-Zahlenkombinationen Hier haben wir eine Erkennungsrate vonetwa 55%.

Minimal raffiniertere DGAs, die z.B. darauf basieren, Wörter aus Wörterbüchern aneinanderzuhängen, erkennt unser Script praktisch überhaupt nicht. Wie gesagt, das war in diesem Kontext auch nicht wichtig.

Optimierungen

Kommen wir nun zum eigentlichen Kern dieses Eintrags: Diese Klassifizierung über 3,2 Millionen Einträge laufen zu lassen kostet Zeit. Mit dem Python-Script etwa 30 Sekunden (oder 20 Sekunden auf meinem Arbeitsrechner).

Ich habe mit meinem Kollegen gescherzt, dass ich eine Rust-Version davon implementieren möchte, nur um zu sehen, wie viel schneller das sein kann. Nun war ich ein paar Tage krank und das war genau die Art von geistig weniger Anstrengender Tätigkeit, die ich noch machen konnte.

Außerdem haben ich ja schon ein paar Male erwähnt, dass es mir gelegentlich Spaß macht, an mehr oder weniger nutzlosen Dingen herumzuoptimieren.

Also habe ich einen Haufen Varianten davon in Rust geschrieben und getestet. Der gesamte Code ist hier zu finden. Als Input habe ich zum einen die tranco-List und zum Anderen eine Liste von arithmetisch generierten Domains, die mir einer der Professoren, die die Vorlesung halten, gegeben hat. Diese Liste stammt aus dem DGArchive, allerdings mit einiger Vorauswahl, so dass am Ende 3,2 Millionen Domains auf der Liste standen.

Messungen

Zum Messen der Zeit habe ich einfach nur time verwendet, die Ergebnisse waren hier recht stabil (im Gegensatz zum Beispiel zu diesem Eintrag).

Allerdings nicht das bash-eigene time sondern /usr/bin/time, weil das mit dem -v-Flag noch einige weitere Informationen, zum Beispiel den maximalen Speichergebrauch, liefert.

Die Ausgaben mit -v sind recht lang, ich habe sie hier immer auf diese Zeilen zusammengekürzt:

User time (seconds):
System time (seconds):
Maximum resident set size (kbytes):

Testfall 1: Das python-Script

Um einen Vergleich zu haben, hier zunächst die Laufzeit des Python-Scriptes (base.py im Repo).

Für die generierten domains:

User time (seconds): 30.47
System time (seconds): 0.11
Maximum resident set size (kbytes): 270856

Und für die Tranco-Liste:

User time (seconds): 8.27
System time (seconds): 0.03
Maximum resident set size (kbytes): 86848

Der Speicherverbrauch kann sich sehen lassen. Die Eingabedateien sind 62 MiB bzw. 15 MiB groß, der Speicherbedarf des Scriptes ist ein Vielfaches davon.

Testfall 2: Das Gleiche in Rust

Ich habe die genaue Funktionsweise in Rust nachprogrammiert (base.rs im Repo). Die Laufzeiten hier sind für die Compileroptimierte Version (die Debug-Version ist sogar noch langsamer als die Python-Version).

Für die generierten Domains:

User time (seconds): 1.45
System time (seconds): 0.06
Maximum resident set size (kbytes): 64972

Und die Tranco-Liste:

User time (seconds): 0.40
System time (seconds): 0.03
Maximum resident set size (kbytes): 17256

Ohne großen Aufwand direkt zwanzig Mal schneller. Auch der Speicherbedarf ist deutlich geringer. Ich weiß nicht, wo der ganze Overhead in Python herkam (möglicherweise war der Garbage-Collector nicht schnell genug mit dem Aufräumen). Nichtsdestoweniger ist hier noch Optimierungspotential.

Testfall 3: Streaming

Die bisherigen Varianten haben immer die ganze Datei geladen und dann darauf gearbeitet. Das sorgt natürlich für hohen Speicherbedarf. Also habe ich es anders gemacht und einen buffered Reader verwendet, der die Datei Zeilenweise einliest, und zwar so, dass die gelesene Zeile jedes Mal in denselben Buffer geschrieben wird. Sonst hätten wir mehrere Millionen Speicherallokationen, und das wäre kontraproduktiv.

(Datei streaming.rs im Repo.)

Für die generierten Domains:

User time (seconds): 1.53
System time (seconds): 0.00
Maximum resident set size (kbytes): 2420

Und für die Tranco-Liste:

User time (seconds): 0.43
System time (seconds): 0.00
Maximum resident set size (kbytes): 2564

Wir sehen: Es ist ein bisschen langsamer, aber der Speicherverbrauch ist deutlich geringer. Also behalten wir das mal bei und versuchen, die Geschwindigkeit anderswo wieder hereinzuholen.

Testfall 4: floating point precision

Bisher wurden alle Berechnungen mit 64-Bit-Fließkommazahlen (f64 in Rust) vorgenommen. Eine solche Präzision brauchen wir hier nicht, also habe ich mal ausprobiert was passiert, wenn ich nur 32-Bit-Fließkommazahlen (f32 in Rust) nehme (Datei f32.rs im Repo).

Für die generierten Domains:

User time (seconds): 1.36
System time (seconds): 0.01
Maximum resident set size (kbytes): 2544

Für die Tranco-Liste:

User time (seconds): 0.38
System time (seconds): 0.01
Maximum resident set size (kbytes): 2492

Wow. Ich hatte nicht wirklich gedacht, dass das so einen Unterschied macht. Das Klassifizierungsergebnis war übrigens das Gleiche wie in der 64-Bit-Fließkommazahl-Version, also keine Verluste hier.

Dennoch, die Verbesserung war eher klein. Wie weit kommen wir noch runter (ohne viel Aufwand zu betreiben)?

Testfall 5: Rohe bytes

Mein nächster Gedanke war: Die Domains sind alle mehr oder weniger ascii, und wenn ich alles in einen String lade muss Rust muss einen kleinen Overhead betreiben um sicherzustellen, dass alles gültiges Unicode ist, außerdem wird das Iterieren über die scalar values mühsamer, weil die Strings intern in utf-8 codiert sind.

Also die f32-Version genommen, aber alles auf byte arrays umgestellt (Datei bytes.rs im Repo).

Die generierten Domains:

User time (seconds): 1.91
System time (seconds): 0.00
Maximum resident set size (kbytes): 2620

Die Tranco-Liste:

User time (seconds): 0.52
System time (seconds): 0.00
Maximum resident set size (kbytes): 2536

Huch. Das ist deutlich langsamer. Ich weiß nicht genau, warum. Vermutlich wird schon recht gut optimiert und ich habe irgendwo eine Entscheidung getroffen, die alles langsamer macht. Auf jeden Fall werde ich diesen Ansatz nicht länger verfolgen.

Testfall 6: Weniger chars-Iteratoren

Ok, die Idee mit dem bytearrays war ein Irrweg. Das ändert aber nichts an der Sache, dass ich den chars()-Iterator über den String verwende. Das ist mehr Aufwand als einfach über ein Array zu iterieren. Und ich benutze diesen Iterator gleich fünf Mal:

  • um die Ziffern zu zählen
  • um die längste Konsonantenfolge zu zählen
  • um die Vokale zu zählen
  • um die Konsonanten zu zählen
  • um die Entropie zu berechnen

Ich lasse die Entropieberechnung mal außen vor, die steckt isoliert in ihrer Funktion und das ist gut so. Die anderen Sachen kann ich aber bequem in einer Schleife erledigen. Das ist vielleicht nicht mehr so funktional, aber vielleicht macht es die Sache ja schneller (Datei one_char_iterator.rs im Repo).

Generierte Domains:

User time (seconds): 1.06
System time (seconds): 0.02
Maximum resident set size (kbytes): 2484

Tranco-Liste:

User time (seconds): 0.31
System time (seconds): 0.00
Maximum resident set size (kbytes): 2548

Wow. Nach dem Fiasko mit den byte-arrays hätte ich nicht gedacht, dass aus dieser Optimierung so viel herauszuholen ist. Aber so langsam geht mir die Puste aus. Also nur noch ein weiteres Experiment.

Testfall 7: Simplify

Diese Optimierung ist nicht ganz so neutral wie die anderen. Die anderen Optimierungen haben im schlimmsten Fall den Code etwas unschöner gemacht. Diese Optimierung hingegen entfernt ein paar Features, die aber ungenutzt waren.

Hintergrund ist: Im Python-Script haben wir ein paar Mechanismen eingebaut, mit denen die Studenten einfacher in die Bewertung eingrätschen konnten um den Klassifikator anzupassen. Das hat u.a. zur Folge, dass mehr Berechnungen durchgeführt werden, aber viel wichtiger, dass es keinen early exit gibt.

Die Entropieberechnung zum Beispiel enthält eine Menge Fließkommaoperationen (u.a. Logarithmusberechungen). Wenn wir uns aufgrund eines der anderen Kriterien schon entschieden haben sollten, dass die Domain generiert ist, müssen wir die Entropie nicht mehr berechnen.

Also habe ich diese ungenutzen Parameter herausgeworfen (Datei less_flexible.rs im Repo).

Generierte Domains:

User time (seconds): 0.71
System time (seconds): 0.01
Maximum resident set size (kbytes): 2524

Tranco-Liste:

User time (seconds): 0.31
System time (seconds): 0.01
Maximum resident set size (kbytes): 2484

Was sagt man dazu? Das Ergebnis wurde noch einmal ordentlich verbessert. Allerdings nur dort, wo es eine nennenswerte Anzahl von early-Exits gab. Bei der Tranco-Liste, wo die meisten Domains korrekterweise als harmlos eingestuft wurden, muss für alle diese Domains auch die Entropie berechnet werden.

Fazit

Was habe ich daraus gelernt? Dass eine Rust-Implementierung schneller sein wird als eine Python-Implementierung ist keine große Überraschung.

Aber es gab ein paar Überraschungen. Manche Optimierungen waren unerwartet effektiv. Andere waren hingegen Kontraproduktiv.

Also gilt, denke ich mal, nach wie vor die Regel: Optimiere nicht zu früh, und wenn du optimierst, stelle sicher, dass die Änderungen auch wirklich etwas verbessern.

Jungle of Code

Der Advent of Code 2022 hat begonnen. Ein Adventskalender mit fünfundzwanzig Programmierpuzzeln und einer (hoffentlich wie immer recht skurrilen) Geschichte.

Zum Advent of Code habe ich schon vorher einige Male etwas geschrieben. Bisher hat es immer Spaß gemacht, auch wenn es stressig werden kann. Ich habe mir jedes Mal vorgenommen, auszuhören sobald es zu stressig wird, aber habe das nie eingehalten. Solltet ihr also zu ähnlichem Fehlverhalte neigen… naja, probiert es trotzdem, es macht Spaß.

Nach dem wir in den letzten Jahren eine Zeitreise unternommen, Santa vom Rand des Sonnensystems abgeholt, Urlaub gemacht haben und die Schlüssel zu Santas Schlitten vom Meeresgrund fischten, geht es nun in den Dschungel um spezielles Rentierfutter zu besorgen.

Wie üblich fängt Tag 1 mit einem einfachen Puzzle an, aber ich vermute, dass die Schwierigkeit (und damit der Spaß) nach und nach ansteigen wird, wie jedes Jahr.

Auch dieses Jahr scheint Gary Grady wieder Comics zu den Puzzles zu machen, so wie im letzten Jahr. Ist halt auf Twitter, also schaut auch das lieber zeitnah an, wer weiß, wie sich diese Sache entwickelt.

Update

Natürlich kann man meine Lösungen auch wieder in einem git-repo finden. Aufgrund diverser anderer Verpflichtungen war ich nur bis gerade eben nicht dazu gekommen, das auch einzurichten.

knacker-attacker.com

Wie neulich schon erwähnt haben wir für die Studierenden einer Vorlesung eine Aufgabe erstellt, in der es sich um domain-name-generation-Algorithmen (DGA) dreht.

Ein Teil der Aufgabe war, einen eigenen DGA zu entwickeln, der der Erkennung durch unseren sehr simplen Klassifikator entgehen kann.

Um sicherzugehen, dass das auch funktioniert, habe ich selber einen gebaut. Ein recht trivialer DGA: er sucht sich zwei Wörter aus einem Wörterbuch, verbindet sie mit einem Bindestrich und hängt .com ans Ende.

Die meisten damit erzeugten Domains ergeben natürlich keinen Sinn, aber schon beim ersten Überfliegen der Ergebnisse sind mit einige Perlen aufgefallen.

Sinnvolle Namen

Da wären zunächst Namen, die tatsächlich einen Sinn ergeben könnten:

  • proud-meetings.com
  • bonfires-addicts.com
  • unacceptably-fireproofed.com
  • diplomat-harlots.com
  • bin-portability.com
  • gambling-fatality.com
  • fortnight-swoops.com
  • tombstone-unintelligible.com
  • musically-unengaged.com
  • queen-trend.com
  • misanthropic-bosses.com
  • unsurvivable-responsibility.com
  • appointment-casualties.com
  • secure-avalanches.com
  • tyrannic-pub.com
  • cursed-queens.com
  • dumbest-romances.com
  • fruitcake-exhibition.com
  • copyright-boat.com
  • unsellable-herrings.com
  • weightlifters-weightless.com
  • pasteurisation-atlas.com
  • meatiest-basilisks.com
  • evil-scarlet.com
  • obscenity-warfare.com
  • evidently-misquoted.com
  • darn-drain.com
  • lengthened-ahem.com
  • kangaroo-prowler.com
  • gulls-roc.com

Natürlich hat mein Gehirn dann sofort angefangen, sich zu überlegen, was das für Seiten sein könnten. proud-meetings.com könnte zum Beispiel ein Anbieter für Videokonferenzen sein. bonfire-addicts.com eine Shop für Feuerwerk. unacceptably-fireproofed.com macht sich über schlechten Brandschutz lustig (vermutlich eine Seite, wo Leute Fotos einsenden können).

bin-portability.com wäre eine Seite für Softwareentwickler. fortnight-swoops.com eine Gaming-Website und diplomat-harlots.com vermutlich Pornografie.

Nun ist es natürlich so, dass keine dieser Domains registriert ist (was ich vielleicht ändern sollte). Aber sie klingen in meinen Ohren alle recht plausibel.

Ein bisschen aus der Reihe fällt gulls-roc.com. Hier fand ich einfach die Vorstellung eines Möwen-Rochs lustig.

Namen, die interessant klingen

Daneben gibt es natürlich auch Domainnamen, die einfach nur gut klingen (und eventuell trotzdem Sinn ergeben. Dazu gehören z.B. Namen mit interessanten Alliterationen oder Reimen:

  • townships-tableau.com
  • aqueducts-avenue.com
  • monocle-mongrels.com
  • knacker-attacker.com

townships-tableau.com könnte zum Beispiel eine Newsseite sein. Mein persönlicher Favorit ist aber knacker-attacker.com, einfach weil es so schön klingt (Notiz am Rande: Ich habe natürlich nachgeschaut, was ein knacker ist. Die deutsche Übersetzung wäre „Abdecker“, ein alter Beruf, bei dem es um die Entsorgung und Verwertung von Tierkadavern geht).

Verstörende Namen

Ein paar Namen sind recht verstörend. Diese Seiten würde ich nicht besuchen wollen:

  • limbs-pulverised.com
  • jettisoning-yourself.com

Fazit

Es ist immer wieder faszinierend, an welchen Stellen man Material zum Prokrastinieren findet. Ich muss mindestens 20 Minuten damit zugebracht haben, zufällige Namenslisten durchzugehen.

Ich glaube, ich sollte mit zumindest knacker-attacker.com registrieren. Vielleicht kann ich ja was Lustiges damit machen.

Viele Ventile und Tödliches Tetris

SPOILER ALERT: Dieser Artikel enthält Informationen über die Story und über Puzzle des Advent of Code 2022. Wer die Geschichte noch selber lesen oder die Puzzles noch selber lösen will, sollte nicht weiterlesen.

Wie schon erwähnt, mache ich dieses Jahr wieder beim Advent of Code mit. Gestern, Tag 16, war der erste Tag, der mich wirklich gefordert hat. Mit anderen Worten: Es wird langsam interessant.

Doch fangen wir mal weiter vorne an…

Notrufsignale

Nach einem Unfall mit einer kaputten Hängebrücke (bei der man vom Rest der Gruppe getrennt wurde), einigen nervigen Affen, einer Kletterpartie, um besseren Empfang zu bekommen (Tag 12, hier habe ich bei Teil zwei wider besseren Wissens eine sehr unperformante Lösung gewählt, weil sie deutlich schneller zu implementieren war und trotzdem in einer Sekunde durchlief) habe ich an Tag 13 ein Notrufsignal empfangen.

Das Signal kam aus einer Höhle (hinter einem Wasserfall, versteht sich), und nach einigen Problemen mit fallendem und rutschendem Sand (Tag 14) konnte man den Ursprung des Notrufsignals ausfindig machen (Tag 15).

Wie schon häufig waren hier die Eingaben so gewählt, dass eine simple Brute-Force-Methode nicht funktioniert hätte. Man hätte ein quadratisches Areal mit vier Millionen Einheiten Kantenlänge durchsuchen müssen.

Glücklicherweise hatte ich in Teil 1 eine recht effiziente Lösung für das Durchsuchen einer Zeile geschrieben. In Teil zwei habe ich es mir dann erspart, eine effiziente Lösung zu finden und habe einfach vier Millionen mal den Algorithmus von Teil 1 aufgerufen, was passabel schnell war (kleiner als eine Sekunde mit Compileroptimierung in rust).

Viele Ventile

An Tag 16, also gestern, ging es dann aber ans Eingemachte. Das Notrufsignal stammte von einer Herde Elefanten in der Höhle, die es irgendwie geschafft hatten, ein Funkgerät zu bedienen. Just in diesem Moment entschied sich der Vulkan, der unter der Höhle liegt, auszubrechen.

Der Vulkan schien das auch in der Vergangenheit schon getan zu haben, jedenfalls hatte jemand ein System aus Leitungen und Ventilen angelegt, um Druck abzulassen.

Dummerweise hatte man nur sehr begrenzt Zeit, um die Ventile zu öffnen. Zu allem Überfluss waren auch noch die meisten Ventile kaputt und über ein großes Höhlensystem verteilt.

Die Aufgabe war also, die Ventile so aufzumachen, dass sie den größtmöglichen Druckablass ermöglichten. Man hatte allerdings nur 30 Minuten Zeit, zwischen Ventilen hin- und herzulaufen kostet Zeit, und Ventile zu öffnen kostet auch Zeit. Zudem war der Druckablass abhängig davon, wann ein Ventil geöffnet wurde. Je früher geöffnet, desto höher der gesamte Druckablass.

Ich habe mir eine Weile den Kopf darüber zerbrochen, wie ich das lösen kann. Am Ende habe ich eine Graphenexploration über die möglichen Zustände gemacht. Teil eines jeden Zustands waren aktuelle Position, verbliebene Zeit, geöffnete Ventile und erreichte Punkte.

Die Lösung funktioniert für die Beispieldaten, war aber viel zu langsam für die echten Daten.

Optimierungen

Nun ist in so einem Fall recht schnell klar, was der Grund ist: Es gibt einfach zu viele erreichbare Zustände, der Suchraum explodiert. Wie reduziere ich also die möglichen Zustände?

Nach einigem Überlegen habe ich die Punkte aus dem Zustand herausgenommen. Schließlich ist es egal, wie viele Punkte ich an einem Zustand habe, um herauszufinden, wie ich von diesem Zustand aus noch mehr Punkte bekommen kann.

Dafür musste ich die Punkte natürlich trotzdem behandeln, also habe ich die Graphenexploration so modifiziert, dass Zustände mit den meisten Punkten bevorzugt behandelt wurden, und wenn ich in einem Zustand war, den ich schon früher mal mit mehr Punkten besucht habe, habe ich ihn für die Exploration einfach ignoriert.

Das hat den Suchraum dann ausreichen eingeschränkt, dass ich in passabler Zeit ein Ergebnis bekommen habe.

Elefanten helfen

Nur, dass das natürlich nicht ausreichte. Also musste ich in Teil 2 des Puzzles auch noch einen Elefanten anlernen, die Ventile zu öffnen.

Das kostete vier Minuten, also war zumindest der Suchraum ein bisschen kleiner. Auf der anderen Seite wurde der Suchraum enorm größer, weil der Elefant nun auch eine Position hatte. Die Suche nach der optimalen Lösung lief also wieder zu langsam um benutzbar zu sein.

Gleiches Spiel wie vorher: Wie können wir die Anzahl der Zustände verringern? Nach ein paar fehlgeschlagenen Versuchen die Zeit oder die geöffneten Ventile ähnlich der Punkte auszulagern, ist mir am Input etwas aufgefallen.

Die vorher erwähnten kaputten Ventile: die meisten von ihnen hatten nur direkte Verbindungen zu zwei anderen Ventilen. Ich könnte also diese Ventile wegoptimieren und durch direkte Wege zu den nicht-kaputten Ventilen ersetzen.

Dummerweise musste ich dafür Wegkosten einführen. Das heißt es reichte nicht mehr, nur meine eigene Zeit zu tracken und den Elefanten gleichzeitig mit mir agieren zu lassen (was vorher möglich war, weil jede Aktion gleich viel Zeit kostete). Der Elefant brauchte einen eigenen timer, und ich musste auch noch aufpassen, dass bei meiner Berechnung der Aktionen trotzdem alles in der richtigen Reihenfolge passierte.

Trotzdem war das Programm noch nicht schnell genug.

Ein paar zunehmend verzweifeltere Stunden und viele unbrauchbare Ansätze später kam mir endlich der Geistesblitz: Ein Problem ist, dass ein Großteil des Zustandsraumes darin besteht, dass sowohl ich als auch der Elefant nur hin- und hergehen ohne etwas zu tun. Das ist kein sinnvolles Verhalten, ist aber Teil des Zustandsraumes und wird deswegen auch untersucht.

Also habe ich das zu-einem-Ventil-gehen und das Öffnen eines Ventils zu einer Aktion zusammengefasst. Und wie schon im letzten Jahr, wo ich den Algorithmus von Dijkstra implementieren konnte, habe ich hier den Floyd-Warshall-Algorithms zum ersten Mal außerhalb einer Vorlesung verwendet. Im Gegensatz zum Dijsktra-Algorithmus musste ich aber die Details hier nachschauen. Ich habe mir den Floyd-Algorithmus nie merken können.

Damit war die Lösung schon viel schneller, zumindest für die Beispieldaten und für den ersten Puzzleteil. Teil 2 lief aber immer noch lange und verschlang viel Speicher.

Mein Gehirn war von der stundenlangen Konzentration schon ganz vernebelt. Irgendwann ist mir dann aufgefallen, dass es ja eine Symmetrie zwischen mir und dem Elefanten gibt, was einige Zustände identisch macht und damit den Zustandsraum wieder einschränkt.

Trotzdem war es noch zu langsam… dachte ich. Tatsächlich lief es aber in etwas über einer Minute durch. Da es mittlerweile nach 23:00 Uhr war, war ich damit zufrieden und schlief ein.

Tödliches Tetris

Die Ventil-Aktion hatte mir und den Elefanten ein bisschen Zeit erkauft. Es wurde jedoch wärmer, und Steine fingen an, von der Höhlendecke zu fallen und sich in Tetris-Manier aufzuschichten. Um herauszufinden, wo es sicher war, musste man eine Simulation erstellen.

Teil 1 war also recht einfach, wenn ich nicht diesen Tippfehler gehabt hätte, der zu einer falschen Lösung geführt hat. Sobald der behoben war, war meine Steinfallsimulation recht elegant gelöst und funktionierte super.

Dann (Teil 2) wollten diese undankbaren Dickhäuter aber lieber auf Nummer sicher gehen und eine Billion Steine simuliert sehen. Mit anderen Worten: ein typischer Fall von: das passt nicht in den Speicher, wir müssen tricksen.

Mein üblicher Ansatz hier ist, irgendwelche Regelmäßigkeiten zu finden, mit denen man den Wert nach einer Billion Iterationen dann einfach ausrechnen kann.

Da mein Gehirn vom Vortag aber immer noch matschig war, konnte ich partout keine Zyklen erkennen. Nach Stunden der Suche habe ich dann mit einem sehr holprigen Zyklusdetektor eine Lösung gefunden.

Jetzt gehe ich schlafen.

Baaaaaahn!

Captain Kirk „KHAN!“-Meme. Die Bildunterschrift lautet „BAAAAAAHN!“

Zu diesem Zeitpunkt hätte ich schon bereits zwei Drittel auf dem Weg nach Hamburg sein können. Ich wollte Freunde in Hamburg besuchen, die ich seit Monaten nicht gesehen habe.

Stattdessen habe ich mein Ticket storniert und sitze jetzt wieder zu Hause. Und bevor ihr mich fragt: „Wieso kannst du dein Ticket noch am Reisetag stornieren?“: Die Antwort ist: Verspätung.

Ich habe eine Stunde bei Minusgraden am Bahnhof herumgehangen, mit immer mehr Verspätung auf dem Zug, bis dieser schlussendlich komplett ausfiel. Zu diesem Zeitpunkt war meine Laune schon auf dem Tiefstand. Der Besuch in Hamburg sollte nur kurz sein, und wenn ich erst spät Abends ankäme, hätte es sich kaum gelohnt.

Und mit Blick auf die ganzen anderen Züge, die alle zu spät kamen und der Erfahrung, dass Verspätungen nur noch mehr Verspätungen hervorrufen, habe ich aufgegeben.

Glücklicherweise kann man in diesem Fall das Ticket komplett stornieren und ich sollte den kompletten Preis zurückbezahlt bekommen. Die Mitarbeitet der Bahn konnten die Stornierung über ein Fahrgastrechteformular für mich erledigen.

Das war ein Glück, denn diese furchtbare Bahn-Website (ich bin vom Fach, glaubt mir, ich erkenne eine furchtbare Website, wenn ich sie benutze) hat mich schon gestern zur Weißglut getrieben.

Das Sahnehäubchen war, dass die S-Bahn, mit der ich zurückfuhr, statt der Fahrplanmäßigen acht Minuten achtzehn Minuten gebraucht hat. Neun davon alleine weniger als einen Kilometer vor meiner Zielhaltestelle wartend (mit anderen Worten: Hätte ich dort aussteigen können, wäre ich zu Fuß schneller gewesen).

Advent of Code 2022: Rückblick

SPOILER ALERT: Wer sich zum Plot oder zu den Puzzeln des Advent of Code 2022 und teilweise auch des Advent of Codes 2019 nicht spoilern möchte, sollte hier nicht weiterlesen.

Ich hatte ja Mitte des Monats schon einen Zwischenstand zum Advent of Code geposted. Mittlerweile bin ich durch, das letzte Puzzle habe ich am 25. Dezember gelöst. Wie schon vorher geschrieben sind alle meine Lösungen auf github zu finden. Die Lösungen für 2019 ebenso. Hier ein kleiner Rückblick:

Tag 18: Obsidian

Nachdem man in Tag 17 (wie schon im Zwischenstand erwähnt) einem Haufen in Tetris-Manier fallenden Steinen ausweichen musste, ging es in Tag 18 darum, die Oberfläche eines aus Voxeln bestehenden Magmastroms zu berechnen, welcher in Wasser floss und zu Obsidian erhärtete. Minecraft lässt grüßen.

Tag 19: Geoden

Überhaupt hatten viele der Puzzle Ähnlichkeiten mit Computer- oder Brettspielen. An Tag 19 musste man, um Geoden zu knacken, Roboter bauen. Ein Geodenknackroboter braucht Erz und Obsidian. Ein Obsidianabbauroboter braucht Erz und Ton. Ein Tonabbauroboter braucht Erz, und ein Erzabbauroboter braucht auch Erz.

Man hatte verschiedene Sets von Bauplänen, ein Zeitlimit (die geretteten Elefanten wurden langsam hungrig) einen Erzroboter zum Starten und die Aufgabe, herauszufinden, wie man bei Ablauf der Zeit die meisten Geoden geknackt hat. Teil zwei war dann das gleiche Problem, aber man hatte mehr Zeit und weniger Baupläne zum Durchprobieren (weil die Elefanten den Rest schon gefressen hatten).

Im Prinzip sehr ähnlich zu Tag 16, der ja bis dahin der schwierigste Tag gewesen ist. Auch in diesem Fall musste ich viel herumüberlegen, wie ich die Anzahl möglicher Zustände möglichst klein halten kann, weil mir ansonsten Speicherbedarf und Rechenzeit explodierten.

Glücklicherweise hatte ich Tag 16 noch in guter Erinnerung, so dass ich einiges, das ich gelernt habe, hier wieder anwenden konnte und deutlich schneller fertig war. Die erste brauchbare (also eine, die in meinen Arbeitsspeicher passte) brauchte wenige Minuten aber lieferte das richtige Ergebnis.

Dann fiel mir noch ein, dass ich die möglichen Zustände noch verringern konnte, indem ich nicht mehr Roboter baute, als ich entsprechende Rohstoffe in derselben Zeit verbrauchen konnte. Damit ging die Laufzeit dann runter auf vier Sekunden, also akzeptabel.

Zwischenspiel: 2019 Tag 18

Mit dem Wissen von Tag 16 und Tag 19 konnte ich dann auch Tag 18 von 2019 lösen. Dort hatte ich damals einen ähnlichen kürzeste-Pfade-Algorithmus versucht, war aber in Teil 2 an der schieren Menge von möglichen Zuständen gescheitert.

Ich habe also erst Teil 1 verbessert und dann Teil 2 implementiert. Nicht perfekt, aber es lief ein sieben Sekunden durch und ich hatte das Thema endlich abgehakt.

Tag 20: Grove Positioning System

An Tag 20 ging es dann endlich weiter mit dem eigentlichen Ziel. Allerdings war man ja von der Gruppe getrennt worden und musste erst die Koordinaten zum geheimen Hain mit den Sternfrüchten entschlüsseln.

Da es mit der Weihnachtselfischen Verschlüsselungstechnologie nicht so weit her war, war das auch recht einfach, so lange man aufpasste, mit ein paar Modulooperationen die Anzahl der Rechenschritte klein zu halten.

Tag 21: Mathematische Affen

An Tag 21 traf man dann wieder auf die Affenhorde, die man schon an Tag 11 getroffen hatte. Dieses SMal klauten sie zum Glück nicht die Ausrüstung. Laut der Übersetzung der Elefanten boten sie mir an, mir eine Abkürzung zum Sternfruchthain zu zeigen, wenn ich ein Rätsel löste.

Das war relativ simpel, trotz einiger Missverständnisse der Elefantenübersetzer.

Tag 22: Die Würfel des Wahnsinns

Tag 22 fing eigentlich ganz harmlos an: Die Affen haben mir die Abkürzung gezeigt und mir eine (etwas seltsam geformte) Karte gegeben. Auf dieser Karte muss ich einen bestimmten Weg entlangehen (ganz in Schatzsuchermanier: 15 Schritte geradeaus, Drehung nach links, 5 Schritte geradeaus, Drehung nach rechts, usw.) um an ein Passwort für den Hain zu kommen.

Das war einfach genug, doch Teil zwei hatte es in sich: bei näherer Betrachtung stellte sich die Karte als ein Würfelnetz heraus, das zu einem Würfel gefaltet werden musste und dann musste ich auf der Oberfläche dieses Würfels entlanggehen.

Ich habe zweieinhalb Tage an dieser Aufgabe gesessen und sie erst an Heiligabend gelöst. Es ist ja nicht nur so, dass ich herausfinden musste, welche der Quadrate des Würfelnetzes (für beliebige Würfelnetze) an welche Seite grenzte, ich musste ja auch die Orientierung der Koordinatensysteme der einzelnen Seiten zueinander herausfinden.

Ähnlich zu Tag 19 im letzten Jahr, wo ich mir ein 3D-Koordinatensystem aus Lego gebaut habe, habe ich mir für dieses Puzzle einige Würfelnetze aus Papier ausgeschnitten, Koordinatensysteme auf die einzelnen Seiten gemalt und diese als Hilfestellung verwendet, damit ich nicht alles im Kopf machen musste:

Fünf verschiedene Würfelnetze aus Papier. Auf jeder Seitenfläche ist ein Koordinatensystem eingezeichnet.

Nach vielen, vielen gehirnverdrehenden Stunden hatte ich irgendwann zumindest einen Ansatz: Ich nehme mir eine Art Normwürfel, bei der ich weiß, welche Seiten mit welchen verbunden und wie diese zueinander gedreht sind und versuche dann, meinen Inputwürfel um diesen Würfel herumzufalten.

Das hat am Ende funktioniert. Allerdings war mein Gehirn am Ende ziemlich matschig. Es ist unglaublich leicht, etwas aus Versehen gegen den Uhrzeigersinn zu drehen, wenn es eigentlich im Uhrzeigersinn gedreht werden müsste. Ich hatte am Ende einen ganzen haufen Tests, die nur geprüft haben, ob das Falten für die Beispieleingabe passt und sogar, ob ich den Normwürfel richtig zusammengestellt habe.

Danach musste ich „nur noch“ (es war Heiligabend und die Zeit war knapp) dumme Tippfehler in dem eigentlichen Laufalgorithmus beheben, und ich hatte endlich das richtige Ergebnis!

Tag 23: Bäume pflanzen

Tag 23 habe ich vor Tag 22 gelöst, aus den oben beschriebenen Gründen. Endlich beim Sternfruchthain angekommen musste man den Elfen helfen, die Pflanzen für das nächste Jahr zu setzen.

Das war ein zellulärer Automat und ziemlich einfach. Ich hatte zwar befürchtet, Teil 2 könnte wieder kompliziert werden, das war aber nicht der Fall.

Tag 24: Durch den Schneesturm

Tag 24 war wieder recht leicht, man musste einen sicheren Weg durch ein sich ständig (aber periodisch) veränderndes Muster von Blizzards bahnen.

In Teil 2 musste man noch einmal zurück und wieder hin, um die Snacks zu holen, die irgendein schusseliger Elf hat liegenlassen. Das war auch relativ leicht, da mir eingefallen ist, dass ich ja einfach die Karte um 180° drehen konnte und den Algorithmus aus Teil 1 unverändert auch für den Rückweg verwenden konnte.

Dieses Mal gab es auch keine riesigen Zustandsräume, also alles ganz harmlos. Umso mehr Zeit habe ich dann auf das Puzzle von Tag 22 verwendet.

Tag 25: Heißluftballons

Tag 25 war wieder recht einfach. Wie traditionell jedes Jahr gab es auch dieses Jahr am 25. nur ein Puzzle. Man musste Zahlen in einem seltsamen Format (das, wie ich später herausgefunden habe, an das balancierte Ternärsystem angelehnt war, nur halt mit Basis 5, also eigentlich ein balanciertes Quinärsystem) parsen (einfach), zusammenrechnen (trivial) und wieder im ursprünglichen Format ausgeben (ebenfalls recht einfach).

Damit war ich durch. Weihnachten war gerettet.

Nachspiel: 2019, Tag 22

Irgendwann zwischendurch hatte ich ja schon Tag 18 aus dem AoC 2019 nachgeholt. Als ich zwischendurch ein bisschen Zeit hatte, habe ich auch angefangen, Tag 22 aus 2019 nachzuholen. Hier ging es darum, eine große Anzahl Karten zu mischen (nach einem vorgegebenen, deterministischen Verfahren).

Teil 1 hatte ich damals schon gelöst. Die Anzahl der Karten war hier noch relativ klein, eine deutlich größere Zahl hätte aber auch keine Probleme bereitet. Es wurde nämlich nur verlangt, die Endposition der Karte herauszufinden, die am Anfang auf Position 2019 war. Schon damals hatte ich erkannt, dass das Mischverfahren so angelegt ist, dass ich die Position der einzelnen Karte unabhängig von den anderen berechnen konnte.

Teil 2 hingegen war deutlich kniffliger. Nicht nur hatte man deutlich mehr Karten (über hundert Billionen), sondern musste den Stapel auch über hundert Billionen Mal mischen. Millionen Mal? Kein Problem. Milliarden wäre auch noch mit Brute force gegangen, aber Billionen war unmöglich.

Ich habe eine ganze Weile gebraucht. Erst ist mir aufgefallen, dass ich alle Teilschritte des Mischverfahrens in eine Modulo-Multiplikation und eine Modulo-Addition vereinigen konnte.

Damit alleine war mir aber noch nicht geholfen. Das hätte zwar ein paar Milliarden Mischoperationen möglich gemacht, aber keine hundert Billionen.

Allerdings war diese vereinfachte Operation deutlich leichter mit modularer Arithmetik zu behandeln. Heraus kam ein bisschen modulares potenzieren und zwei modulare Divisionen. Ich musste mir echt einiges wieder anlesen, aber am Ende hatte ich das Ergebnis in konstanter Zeit ausgerechnet.

Naja, ich musste von i64 auch i128 umsteigen, weil die Multiplikationen trotz Modulo nicht mit i64 machbar waren, aber darüber werde ich mich nicht beschweren.

Fazit

Wieder ein gelungener Advent of Code. Ich kann anscheinend immer noch einiges, auch wenn ich ein paar Mal bewusst den leichteren (und langsameren) Weg genommen habe.

Einige meiner Lösungen waren deutlich langsamer als angegeben (15 Sekunden auf zehn Jahre alter Hardware), aber damit gebe ich mich erst einmal zufrieden.

Ich habe ein paar Sachen gelernt, ein paar Sachen wiederholt, die ich vergessen habe und einige der Puzzle waren für mich wirklich fordernd. Außerdem habe ich ein paar Puzzle von vor ein paar Jahren nachgeholt.

Ich habe mir noch nicht fest vorgenommen, nächstes Jahr auch teilzunehmen, aber ich werde es vermutlich tun. Auch wenn es schon eine ganze Menge Zeit frisst.

ChatGPT und der Tyrannosaurus Trump

Nach dem ganzen Hype in den letzten Wochen musste ich auch endlich mal dieses ChatGPT ausprobieren. Und ich will hier auch überhaupt nicht in eine der Diskussionen einsteigen, ob und wie das die Gesellschaft verändern wird, ob das gut oder schlecht ist oder ob ChatGPT-Einreichungen beim Advent of Code unfair sind.

Ich habe mir das Teil angeschaut und mein erster Eindruck ist, dass es ein wirklich beeindruckendes Stück Technik ist. Hier einmal meine jüngste Anfrage (in einem Chat hatte irgendjemand mit Dino-Propaganda angefangen, und ich hatte mir gedacht, ich lasse ChatGPT was daraus machen):

I need a satirical speech in the following setting: It is the end of the cretaceous period. Tyrannosaurus Trump gives a speech about how the imepnding Meteor ist just a lie to weaken the economy and that his arms are not small.

Genau so habe ich das eingegeben, Tippfehler inklusive. Das Ergebnis war:

Ladies and gentlemen, esteemed dinosaurs, I stand before you today to address the rumors of an impending meteor strike. I want to be completely clear: this is fake news. It's a hoax perpetrated by the liberal media and their fossil fuel friends to weaken our economy and make us look bad.

Let me ask you, have any of you seen this meteor with your own eyes? Of course not. It's all based on computer models and "scientific evidence" that can easily be manipulated. Trust me, I know a thing or two about fake news.

And let me tell you, my arms are definitely not small. They are huge, the biggest in the history of the Cretaceous period. Believe me.

But even if this meteor were real, which it's not, we would be perfectly fine. We have the best scientists, the best technology, and the best defense systems. We will protect ourselves and come out on top, as we always do.

So don't believe the lies. Keep your heads held high and let's keep making the Cretaceous great again. Thank you.

Großartig. Nur drei Sätze als Eingabe, in natürlicher Sprache, und dass es um Donald Trump geht ist nur durch „Tyrannosaurus Trump“ angedeutet. Daraus macht ChatGPT eine großartige Satire einer Trumpschen Rede.

ChatGPT kann also nicht nur sprachliche Eingaben verstehen, es ist anscheinend auch recht gut darin, sie mit Kontextwissen zu verbinden und daraus etwas zu machen, was nicht nur Sinn ergibt, sondern auch ziemlich genau das ist, was ich wollte.

Natürlich kommen nicht immer so wunderbare Sachen dabei raus. Manchmal muss man ein paar Mal nachhaken und spezifischer werden, sonst sind die Ergebnisse eher mäßig. Die größten Probleme hatte ich bisher bei Lyrik. Eine einfache Anfrage nach einem Limerick:

I need a limerick about how Morgoth stole the Silmarils

wurde noch mit einem brauchbaren Limerick beantwortet:

Once Morgoth, dark lord of hate,
Stole the Silmarils, shining so great.
He hid them deep in the earth
In a land of eternal dearth,
But the Elves' quest for them was not late.

Kompliziertere Dinge, wie eine Parodie auf den Zauberlehrling, gelangen nicht so gut. Es kam zwar ein Gedicht heraus, das von der Handlung auch sehr dem Zauberlehrling ähnelte, aber der Stil war völlig anders.

Eine Anfrage auf ein nicht ganz triviales Programmierproblem (gegeben eine einfach verkettete Liste, gib mir einen Algorithmus, der in linearer Zeit und konstantem Speicher herausfindet, ob die Liste eine Schleife enthält) kriegte ChatGPT noch hin. In Pseudocode, aber auch in rust. Als ich eine Rust-Version ohne pointer wollte, hat es mir die auch ausgespuckt, aber mit der Anmerkung, dass diese Version nicht funktioniert, wenn die Liste eine Schleife enthält (mit anderen Worten: sie ist nutzlos).

Die Anmerkungen, die ChatGPT macht, sind übrigens auch interessant. Ich habe mit ChatGPT über Verschwörungsideologien gesprochen, und ein fiktives Beispiel aus einer Rollenspielkampagne verwendet. Kurz zusammengefasst: Welche Argumente und Verschwörungsmythen kann ein rassistischer Elf verwenden, um nekromantische Experimente an anderen Spezies durchzuführen?

Die Ergebnisse waren erschreckend gut. Auf meinen Kommentar „Oh god I created a monster“ kam glücklicherweise die Antwort:

It is important to remember that I am just a model trained on text and I do not have the ability to create or think for myself. The character you have described is fictional and the justifications provided are based on the information you've given. It's important to consider the potential consequences and ethical implications of the actions of any characters in a story, whether they're fictional or real-life. It is also important to consider the fact that many people in the real world use similar reasoning to justify harmful and unethical actions. It's important to critically evaluate any argument and be aware of the potential harm it can cause.

ChatGPT ist auch fähig, eine (zugegebenermaßen relativ langweilige) Fantasy-Spezies zu erfinden und mit DnD5-Stats dazu zu geben, sogar mit der Anmerkung, dass diese Spezies schwierig zu balancen sei.

Im Großen und Ganzen bin ich von ChatGPT wirklich beeindruckt. Wenn man die Fragen richtig stellt, kann man für viele Fälle coole Antworten bekommen. Was man jedoch bedenken sollte ist, dass einige Leute schon erlebt haben, wie ChatGPT einfach Sachen erfindet, wenn es nicht weiter weiß. Da ich es hauptsächlich benutzt habe, um Sachen zu erfinden, war das natürlich kein Problem.

Verschloptimierung

Diesen Blogpost schiebe ich schon eine Weile vor mir her. Glücklicherweise hat sich in der Zeit mehr Material für den Blogpost angesammelt, so dass ich es nicht bereue, ihn jetzt erst zu schreiben.

Resourcen zu Optimierungen

Kurz nachdem ich meinen letzten Blogpost über Optimierung geschrieben habe, bin ich auf einen Vortrag aus 2019 aufmerksam geworden. Darin geht es auch um Optimierung von Programmen, und dass es nicht immer offensichtlich ist, ob eine Verbesserung statistisch signifikant ist und ob die vorgenommene Änderung wirklich Ursache für die bessere Performance war. Oft genug sind es einfach Nebeneffekte der Compileroptimierung, die halbzufällig anders arbeiten.

Wenn man ernstzunehmende Optimierung machen will, ist der Vortrag sehenswert. Da die Optimierungen, die ich bisher in diesem Blog beschrieben habe mehr aus Spaß als aus ernstzunehmender Notwendigkeit entsprungen sind, habe ich mir so aufwändige Analysen gespart.

Noch ein paar Tage später hat dann Daniel Stenberg, der Maintainer von curl einen Blogpost über eine Optimierung des base-64 encoders in curl geschrieben. Auch durchaus lesenswert.

Und dann, noch ein bisschen später, habe ich diesen Vortrag über code-profiling in rust gelesen (gefunden dank This Week in Rust. Ein schöner Vortrag, die meisten Methoden habe ich noch nicht angewandt.

Fallstudie: Ein Brainfuckinterpreter

Nun zu dem, was ich gemacht habe. Irgendwann Ende Januar ist mir ein Repo mit einem rust-Brainfuckinterpreter aufgefallen, der noch bei mir herumlag. Aus dem Jahr 2016, also kurz nachdem ich überhaupt mit rust angefangen habe. Rust-Edition: pre-2018.

Vermutlich etwas, was ich geschrieben habe, um mich in rust einzuarbeiten. Der Code war nicht das, was ich als „idiomatisches rust“ bezeichnen würde. Aber die erste Verbesserung war, einen Bug in der Schleifenbehandlung zu beheben.

Ausgangslage

Dannn konnte ich die Zeit auf einem in brainfuck geschriebenen Primzahlenberechner messen. Unglücklicherweise war das Ergebnis ungefähr auf einem Niveau des Brainfuckinterpreters, den ich 2009 in C geschrieben habe. Und der benutzte eine doppelt verkettete Liste als Band, weil ich das damals für eine gute Idee hielt.

Glücklicherweise konnte ich die mittlere Laufzeit (von etwa 13,5 Sekunden) um etwa zwei Sekunden verbessern, indem ich einfach das tat, was ich sowieso wollte: idiomatisches Rust schreiben. Vorher hatte ich irgendwelche umständlichen Dinge mit closures gemacht, jetzt parse ich den Input als Liste von enums und lasse den Interpreter dann darüber laufen.

Die Optimierung, die alles schlimmer machte

Der nächste Schritt war dann, ein bisschen Optimierung zu betreiben. Meine Idee war: Hey, wir haben häufig viele gleichartige Operationen hintereinander. + und - zum Beispiel. Ich kann mehrere +- und --Operationen zusammenfügen, anstatt also zum Beispiel für +++++ fünf mal die aktuelle Zelle zu inkrementieren, addiere ich ein Mal 5 zur aktuellen Zelle.

Gesagt, getan. Das Ergebnis: Ich bin wieder auf dem Niveau meines C-Brainfuckinterpreters. Warum? Ich weiß es nicht. Meine Vermutung: Irgendwelche Compileroptimierungen, die jetzt nicht mehr so gut greifen. Oder irgendwelche CPU-Optimierungen, die für die unoptimierte Variante besser greifen. Hier sei noch einmal auf den ersten Vortrag, den ich oben verlinkt habe, verwiesen.

Vielleicht war es inlining? Nein, selbst wenn ich den Compiler mit der Nase darauf stoße, dass er inlinen soll, verändert sich nichts. Vermutlich inlined er hier sowieso. Ich sollte mir also den zweiten Vortrag, den ich oben verlinkt habe, zu Herzen nehmen und ein gründliches Profiling machen. Wäre eine nette Übung. Aber nicht jetzt.

Hugo

Anfang dieses Jahres (und Ende letzen Jahres) habe ich angefangen, ein kleines Lexikon für eine meiner Rollenspielrunden zusammenzuschreiben. Die Welt, in der das Rollenspiel spielt, hat sich unsere Spielleiterin selber ausgedacht, und da es in der Kampagne darum geht, längst vergessene Geheimnisse eines versunkenen Kontinents zu erkunden, ist Wissen über die Welt auch wichtig.

Aber es ist eine ganze Menge, verteilt über viele kleine Schnipsel, hin und wieder sogar mit Illustrationen, ebenfalls von unserer Spielleiterin angefertigt. Also wollte ich das ganze mal als Lexikon-Website erfassen.

Ideal wäre natürlich eine Art Wiki-Software gewesen. Dafür gemacht, schnell damit zu arbeiten und viele Informationen einzupflegen, mit mehreren Leuten. Aber das hätte einen ganzen Rattenschwanz von anderen Dingen hinter sich her gezogen: Datenbanken, Updates, Benutzeraccounts etc. Viel zu viel Arbeit für so ein kleines Projekt.

Aber ich wollte schon länger einmal Hugo ausprobieren. Für dieses Blog hier habe ich ja meinen eigenen maßgeschneiderten Generator geschrieben, aber das ist alles andere als ökonomisch sinnvoll. Also Hugo als static site generator.

Die positiven Seiten

Seiten erstellen mit Hugo ist ziemlich befriedigend. Es gibt einige hilfreiche Tutorials und Dokumentationen. Die Software selber erstellt mit einem Kommando die Seite. Es gibt ein paar Methoden, Templates zu schreiben, die man dann mit Markdown füllen kann. Dinge wie Tags und Kategorien kann man mit Bordmitteln einfach hinzufügen, und mit minimal mehr Aufwand bietet das „taxonomy“-System auch andere Möglichkeiten, Inhalte zu gruppieren.

Es gibt Präprozessoren für scss und tools, um mit Bildern umzugehen, so kann man zum Beispiel in Templates einfach kleinere Versionen von Bildern erstellen, um Bandbreite zu sparen. Man kann den Markdown-Dateien in einem Header noch beliebige Attribute hinzufügen, wenn man noch ein paar Metadaten für die Inhalte braucht.

Kurz: Fast alles was man braucht, ist von Anfang an dabei.

Die negativen Seiten

Natürlich gibt es auch ein paar Dinge, die eher unschön sind. Dieser ganze taxonomy-Kram ist eher unhandlich, es ist schwierig, auf beliebigen Seiten zum Beispiel die Artikel genau einer Kategorie zu listen.

Die Bildverarbeitung ist eingeschränkt: Es ist sehr einfach möglich, skalierte Versionen von Bildern in Templates zu verwenden. Aber meistens will man die Bilder ja in den Markdown-Dateien einbinden. Dort stehen die Tools zum Skalieren nicht zu Verfügung. Die einzige Lösung dafür, die ich bisher gefunden habe, ist, eigene shorthands dafür zu schreiben. Zumindest glaube ich, dass das funktionieren könnte.

Auch gibt es keine Möglichkeit, die erstellten Seiten direkt beim Generieren zu komprimieren. Wenn ich also gzip oder brotli als transfer-encoding verwenden möchte, muss ich das manuell machen.

Da ergibt sich aber das nächste Problem: Hugo baut immer alle Seiten. Selbst wenn sich die Quelle für die Seiten nicht geändert hat. Das bedeutet, dass zum Beispiel rsync mit Standardeinstellungen immer alles neu hochlädt. Um das zu verhindern, muss man das --checksum-flag mitbenutzen, was aber bedeutet, dass rsync auf beiden Seiten alle Dateien einmal komplett einlesen muss. Außerdem wird es dadurch noch schwieriger, die Dateien in einem weiteren Schritt zu komprimieren, weil das lange dauert und ich es gerne nur für die Dateien mache, die es nötig haben.

Fazit

Trotz allem ein ausgereifter static site generator. Für kleinere Projekte definitiv zu empfehlen, für größere Seiten muss man schauen. Aber die meisten größeren Seiten haben ja sowieso meist Bedarf für dynamischen Inhalt, das kann ein static site generator nicht.

Ach, und die erstellte Lexikonseite ist nicht wirklich öffentlich, also verlinke ich sie hier nicht.

Logging

Ich bin zurück aus dem Urlaub (seit einer Woche schon, aber man kommt ja zu nichts) und muss jetzt erst einmal ein paar Themen aufarbeiten, die mir in letzter Zeit Gedanken gemacht haben. Das erste davon ist: logging.

Ich schaue mir wöchentlich die Seite This Week in Rust an. Dort werden aktuelle Entwicklungen der Programmiersprache rust sowie alle möglichen Artikel zu rust verlinkt. In Ausgabe 487 von vor ein paar Wochen war dieser Artikel über logging verlinkt. Darin werden einige Ratschläge gemacht, denen ich wirklich widersprechen muss.

Hier stimme ich zu

Zuerst einmal stehen in dem Artikel durchaus ein paar Aussagen, denen ich so zustimme. Logging kann sehr hilfreich zur Fehlersuche sein. Nicht in jeder Situation kann man das Programm einfach in einem Debugger laufen lassen. Und ein Loggingframework kann helfen, übersichtlichere logs zu schreiben (aber es sollte relativ klein und simpel sein, sonst passiert so etwas wie bei log4j.

Aber hier stimme ich nicht zu

Aaaaber… In dem Artikel wird empfohlen:

You should log as much as possible.

Every time you make a decision, you should log it. Every time you query a URL or build a string of some kind, you should log it. Every time you load a config parameter, you should definitely log it.

Nun ist anzumerken, dass es hier um ein debug-loglevel geht, also eins, das im Produktivbetrieb nicht aktiv ist. Aber dazu später mehr.

Aber der Grundaussage kann ich nicht zustimmen. Konfigurationsparameter loggen? Geschenkt, sofern es keine secrets sind. Angefragte URLs loggen? Von mir aus. Jeden string loggen? Bitte nicht. Jede Entscheidung loggen? Um Himmels Willen, nein!

Ich habe meine Erfahrung mit Services, die zu viel loggen. Da schaltet man log-level debug ein und sieht dann vor lauter Logmeldungen nicht mehr das, was man eigentlich untersuchen möchte. In der Regel sind die Informationen, die man wirklich braucht, sowieso nicht dabei. Logzeilen sausen vor deinen Augen vorbei, zu schnell, als dass du irgendetwas erkennen kannst, zu viele, als dass man sie im Nachhinein durchlesen kann.

Loglevel

Ein kleiner Exkurs zu logleveln: Die meisten Logging-Bibliotheken unterstützen das Konzept von „logleveln“: Auf der einen Seite wird jeder Logmitteilung ein Level zugewiesen. Auf der anderen Seite wird global gesetzt, auf welchem Level gelogged wird. Im laufenden Programm werden dann immer nur die Logmeldungen tatsächlich geschrieben, die mindestens so schwerwiegend wie das globale loglevel sind.

Als Beispiel: Wenn es die loglevel „error“, „warning“, „info“ und „debug“ gibt, und das globale loglevel auf „warning“ gesetzt ist, werden nur logmeldungen mit level „error“ und „warning“ gelogged.

Wie das globale loglevel gesetzt wird, ist unterschiedlich. Das geht von statisch (zur compilezeit wird entschieden, welche Logmeldungen es in den Code schaffen) über konfigurierbar (das loglevel steht in einer Konfigurationsdatei und wird beim Start des Programms gelesen) bis dazu, zur laufzeit konfigurierbar zu sein (man kann das loglevel ändern, ohne das Programm neu zu starten).

Debuggen auf Produktivsystemen

Üblicherweise ist das loglevel auf Produktivsystemen „error“ oder „warning“. Den ganzen Debug-Müll, den man der obengenannten Empfehlung nach generieren würde, würde hier also nicht gelogged werden.

Also muss man sie, wenn man auf Fehlersuche ist, erst aktivieren. Wenn wir ein Produktivsystem debuggen möchte — viel Spaß. Als erstes müssen wir das Loglevel ändern. Bei statischen logleveln müssen wir neu bauen und neu deployen. Bei konfigurierbaren logleveln müssen wir die Konfiguration ändern und gegebenenfalls das Programm neu starten.

Angenommen, das Programm ist ein Webservice. Umkonfigurieren wäre hier der Idealfall. Aber heutzutage wird ja alles in Docker-Container verpackt und deployed. Also muss man am Ende doch den ganzen Container neu bauen, um die Konfiguration zu ändern. Und dann ist der Unterschied zu „wir müssen neu compilieren“ auch nicht mehr so groß.

Wenn man aber sowieso neu bauen und deployen muss, kann man es sich auch sparen, grundsätzlich jedes Detail zu loggen. Stattdessen kann man hier eingreifen und nur die Teile loggen, die für das Problem relevant sind.

Denn ansonsten hat man den oben erwähnten Wust von Logmeldungen, von denen nur sehr wenige relevant sind (und die wirklich interessanten Teile vermutlich trotzdem fehlen). Außerdem kann es vorkommen, dass aus Versehen irgendwo sensible Daten gelogged werden. Je mehr gelogged wird, desto größer die Wahrscheinlichkeit, dass das passiert.

Aber was soll ich denn jetzt loggen?

Was soll ich loggen? Einfach: Fehler. Und zwar auf loglevel „error“. Und es dürfen keine Fehler gelogged werden.

Klingt widersprüchlich, ist es aber nicht. Mit „Fehler“ meine ich keine Benutzerfehler, sondern entweder Fehler im Programm oder Laufzeitfehler, die verhindern, dass der Service korrekt läuft. Eine essentielle Datei ist nicht da? Fehler loggen. Irgendwo wird ein ungültiger Zustand erreicht? Fehler loggen. Wenn man dem Benutzer trotzdem noch eine sinnvolle Antwort geben kann, gerne, aber trotzdem Fehler loggen.

Die Idee dahinter ist folgende: Das sind Fehler, die das Programm nicht selber beheben kann, die also Programmier- oder Konfigurationsfehler sind. Hier muss jemand draufschauen. Wenn ein Fehler gelogged wird, muss er entweder behoben werden (wenn es wirklich ein Fehler ist) oder er darf nicht gelogged werden (weil es nicht wirklich ein Fehler ist).

Die Logdatei bleibt so übersichtlich. Wenn sie leer ist, ist alles in Ordnung. Wenn nicht, muss etwas getan werden. Man muss sich nicht erst durch Gigabytes von Logmeldungen quälen um zu sehen, ob etwas nicht stimmt.

Warum keine Warnungen loggen?

Ok, also im Normalbetrieb werden nur Fehler gelogged, beim Debuggen auch Debugmeldungen. Was ist mit Warnungen?

Warnungen werden nicht gelogged. Warnungen sind eine halbgare Sache. Muss jetzt etwas getan werden? Oder nicht? Darf ich die Warnung ignorieren? Hier gibt es keine klare Handlungsstrategie. Eine Warnung bedeutet „Hier ist etwas ungewöhnliches passiert, das vielleicht ein Problem ist, vielleicht aber auch nicht, aber ich (das Programm) kann normal weitermachen“.

Was man für solche Fälle wirklich haben möchte, ist ein Monitoring-System. Also etwas, dass zählt, wie oft man in diesen ungewöhnlichen Zustand kommt. Passiert es ein Mal? Vermutlich kein Problem. Passiert es gehäuft? Dann kann es auf Probleme hinweisen, z.B. Programmierfehler, Konfigurationsfehler, Usability-Probleme oder sogar ein Angriff.

Aber im Log haben Warnugen nichts zu suchen.

Und das info-Loglevel?

Reine Informationen sollten auch nicht ständig gelogged werden, sondern nur, wenn es zu Problemen kommt. Dann ist es egal, ob auf dem loglevel debug oder dem loglevel info.

Eine Anekdote mit Gigabytes an Logdaten

Bei meinem vorherigen Arbeitgeber hatten wir ein paar Fälle, wo zu viel gelogged wurde. In einem oder zwei Fällen wurde das auch richtig teuer, weil dabei pro Tag Gigabyteweise Logdaten durch die Gegend geschickt wurden. Die Teams in diesen Projekten haben dann einiges an Aufwand betrieben, um die Datenflut auf ein erträgliches Maß zu reduzieren. Das war erfolgreich, am Ende war aus der Logflut ein Logtröpfeln geworden.

Auch in einem meiner Projekte wurde zu viel gelogged, allerdings nicht Gigabytes pro Tag. Wir haben das stetig zurückgebaut, bis es erträglich wurde. Insbesondere im Frontend hat ein Kollege sehr viel Wert darauf gelegt, dass in der Javascript-Console nichts gelogged wird. Das macht das Debuggen, wenn man dann temporär ein paar Logmeldungen einbaut, viel erträglicher.

Microsofts Preiserhöhung

Der zweite meiner Nach-Urlaub-Einträge, in dem ich Themen aufgreife, die mir vor oder während des Urlaubs untergekommen sind.

Vor ein paar Wochen ging die große Entrüstung herum, als Microsoft die Preise für Cloud-Produkte um bis zu 11% erhöht hat.

Um ehrlich zu sein, ich habe das kein großes Mitleid. Seid Jahrzehnten empfehlen Befürworter freier Software, sich nicht von quasi-Monopolisten wie Google und Microsoft abhängig zu machen. Seit Jahren wird vor der Gefahr durch ein Vendor lock-in in Cloudprodukten wie dem von Google, Microsoft oder Amazon gewarnt.

Trotzdem werden diese Cloud-Dienste benutzt. Warum? Weil es einfacher ist, weil es günstiger ist. Zum Beispiel ist es einfacher, Cloud-eigene Dienste zu verwenden anstatt in der Cloud selber Dienste zu deployen, die dasselbe machen und auch auf anderen Cloudplattformen laufen würden. Es ist einfacher, Office 365 zu abonnieren als sich mit Libreoffice und Nextcloud oder so herumschlagen zu müssen.

Wenn jemand in einem Betrieb eine Entscheidung für ein Cloud-Produkt von Microsoft getroffen hat, dann müssen dieser Person die potentiellen Nachteile wie zum Beispiel eine drastische Preiserhöhung bewusst gewesen sein (und wenn sie das nicht waren, dann war das Inkompetenz).

Insofern ist es also eine business-Entscheidung: Jetzt Geld sparen, jetzt schneller Ergebnisse bekommen auf die Gefahr hin, dass es später teurer werden kann und Umsteigen auf andere Anbieter noch teurer wird.

Das ist ja auch durchaus eine akzeptable Entscheidung. Aber dann muss man halt mit den Konsequenzen leben. Es wurde gewarnt, dass genau das passieren kann. Jetzt ist es passiert. Deal with it.

Webdesignsünde endless scrolling

Wer zur HÖLLE ist eigentlich auf die verdammte Idee gekommen, Endlosscrolling auf einer Website sei eine gute Idee? Und damit meine ich sowohl automatisch nachladendes Endlosscrolling als auch Endlosscrolling mit einem „mehr“ button, wobei Klicken auf diesen Button zum Nachladen von mehr Inhalt AUF DERSELBEN SEITE führt.

Beispiel Patreon: Ich fange an, einen Creator zu unterstützen, der schon eine Weile dabei ist, also eine Menge Inhalt hat. In chronologischer Reihenfolge anschauen geht nicht, außer ich will mir sehr viel Mühe mit den Filtern geben. Also in umgekehrt chronologischer Reihenfolge.

Ich lese mir Sachen durch, schaue Sachen an, bis ich am unteren Ende der Seit angekommen bin. Dort ist ein „mehr“-Button. Auf den klicke ich, Inhalt wird nachgeladen, ich kann weiterlesen.

Wo ist das Problem? Nun, nehmen wir an, ich muss die Seite aus irgendwelchen Gründen neu laden. Vieleicht weil ich den Browser geschlossen habe. DANN BIN ICH WIEDER FUCKING GANZ OBEN UND MUSS ALLES BIS ZU DEM PUNKT NACHLADEN WO ICH VORHER AUFGEHÖRT HABE ZU LESEN. Das ist wie wenn man einen Film anschaut, mittendrin unterbricht, und dann nicht direkt wieder dort anfangen kann, wo man aufgehört hat sondern erst einmal wie bei einem alten VHS-Spieler zur richtigen Stelle spulen muss.

Erschwerend kommt hinzu, das dieses VERDAMMTE GUEST HOUSE IN NIJMEGEN ES SEIT VIER MONATEN NICHT GESCHAFFT HAT, MEINE INTERNETVERBINDUNG ZU STABILISIEREN. Ich habe also ständig Verbindungsabbrüche. Wenn die Verbindung genau dann abbricht, wenn ich Inhalte nachladen will, kann ich nicht später versuchen, die Inhalte noch einmal nachzuladen. Der Button wird ja durch das Ladesymbol ersetzt, wenn man runterscolled. Also bleibt mit nur, die Seite neu zu laden und mich wieder komplett durchzuscrollen und dabei ALLES NOCH EINMAL ZU LADEN. Das ist ungefähr so als wenn ich einen Film (bleiben wir beim VHS-Beispiel) anschauen möchte, und aller zehn bis zwanzig Minuten kommt jemand vorbei um den Film zurückzuspulen, und ich muss dann wieder die richtige Stelle finden.

Dabei gibt es doch so eine einfache Lösung für das Problem: Paging. Man macht diskrete Seiten aus einer Gruppe von Posts, und jede dieser Seiten hat eine eigene URL. Das macht es viel einfacher, die Seite neu zu laden, und als Bonus kann man die Seite auch verlinken! Und nicht nur das, aus Entwicklerperspektive ist es auch einfacher zu implementieren als ein Endlosscroller. Es hat nur Vorteile, aber AUS IRGENDEINEM GRUND FINDEN WAHNSINNIG VIELE WEBDESIGNER ENDLOSSCROLLING UNGLAUBLICH TOLL. AAAAAAAAAAAAHHHHHHHHHHH!

Das NRW-Abidebakel

Das ist jetzt natürlich ein paar Monate zu spät, aber ich wollte im Rahmen der Dinge, über die ich nach meinem Urlaub schreiben wollte, auch über das Abiturdownloaddebakel in NRW schreiben. Dann mache ich das halt jetzt.

Über die Sicherheitslücken, die im Anschluss gefunden wurden, lasse ich mich mal nicht aus. Das kann Lilith Wittmann viel besser.

Aber verdammt noch mal, ich will eine Erklärung, warum der Server beim Download der Abiturprüfungen zusammengebrochen ist. Mein erster Gedanke war, kurz zu überschlagen, wie viele Schulen da potentiell gleichzeitig etwas herunterladen wollten.

Anhand der Anzahl betroffener Schüler (die im Online-Artikel genannt wurde) bin ich auf etwa 1000 Schulen gekommen (damit lag ich erstaunlich nah an den später angegebenen ca. 900 Schulen). Also 1000 gleichzeitige Downloads, vielleicht 3000, wenn unterschiedliche Fächer separate Downloads haben.

Warum ist das so unglaublich peinlich?

Nun ist es natürlich nichts Neues, wenn Server mal unter plötzlichem Ansturm zusammenbrechen. Wenn zehntausende bis Millionen gleichzeitig zugreifen und man nicht mit so einem Ansturm gerechnet hat.

In diesem Fall kann der Ansturm aber keine Entschuldigung sein. Denn erstens wussten die Verantwortlichen ziemlich genau, wie viele Anfragen kommen: schließlich kennen sie die Anzahl der Schulen, die die Aufgaben brauchen.

Und zweitens: 3000 gleichzeitige Anfragen sind nichts. Insbesondere, wenn der Server nur Daten rausschaufeln muss. Das belastet die CPU nicht einmal sonderlich, die ist den größten Teil der Zeit damit beschäftigt, zu warten, bis die Daten auf die Leitung geschoben sind. Die Daten sind statisch, die müssen nicht aufwändig aus Templates gerendert werden, brauchen keine teuren Datenbankoperationen. Einfach eine (einmalige) Authentifizierung des Clients, dann können die Daten rübergeschoben werden.

Und jetzt kommt mir nicht mit „aber das sind ja ein paar Gigabyte pro Aufgabe, da wird der Server halt überlastet”. Nein! Das bedeutet vielleicht, dass jeder einzelne Download langsam ist, aber der Server bricht davon nicht zusammen.

Der Notfallplan, die Daten woanders hochzuladen und einen Downloadlink herumzuschicken, ist dann anscheinend daran gescheitert, dass der Link herumgeschickt wurde, bevor der Upload abgeschlossen war und außerdem der Upload fehlschlug.

Auch hier: Warum? Und man hätte die Aufgaben zur Not auch in einem fucking Google Drive hochladen können. Verschlüsselt, den Schlüssel hätte man dann ja über einen anderen Kanal geschickt. Selbst Google kann dann nicht den Inhalt sehen. Und über Datenschutz hätte man sich hier auch keine Gedanken machen müssen: Schließlich laden aus Sicht von Google Schulen, nicht Personen die Daten runter. Und dass eine Schule die Daten herunterlädt, ist jetzt nicht überraschend.

Aufklärung

Ich habe mich wieder an das Thema erinnert, weil Zerforschung gestern einen Artikel über die kaputte Plattform für die Verteilung des Freundschaftspass-Deutschland-Frankreich online gestellt haben (lesenswerter Artikel, aber ich schweife ab).

Nun sind seitdem Abiturdebakel zwei Monate vergangen und ich frage mich: Was ist mittlerweile dabei herausgekommen?

Nun, viel habe ich nicht gefunden. Der erste Artikel, der die Ursachen erklären will, ist vom 21.4.2023, also kurz nach dem Debakel, und sagt nicht mehr, als ich oben schon beschrieben habe. Das ist keine Erklärung, was da schiefgelaufen ist. Das wirft nur noch mehr Fragen auf (Fragen à la „WTF WARUM?“).

Das nächste, was ich finden konnte, war dieser Artikel, bei dem es aber nur darum geht, welche Konsequenzen gezogen werden. Aber immerhin:

Der Vertrag mit dem externen Dienstleister […] laufe ohnehin im Sommer aus, man werde darum Aufgaben neu ausschreiben. Und zwar mit höheren Anforderungen an das Daten-Volumen […], die Testung neuer Verfahren und die Redundanz, also, dass es quasi einen Plan B für Störungen gibt.

Nur wirklich schade, dass die Ursache nie wirklich öffentlich geklärt wurde. Ich hätte da gerne mehr Details erfahren. Oder vielleicht gabe es die Details, aber ich habe sie im Internet einfach nicht gefunden.

Rückblick: PKW-Maut

Ich habe gerade diese Schlagzeile gelesen: „Bund zahlt 243 Millionen Euro Schadensersatz“. Da geht es darum, dass wegen der gekippten PKW-Maut eine Riesensumme Entschädigung an diverse Firmen gezahlt werden muss, weil die Verträge mit denen gelündigt wurden.

Nun ist das hier die richtige Entscheidung. Ich will mich auch nicht darüber auslassen, dass der Scheuer die Verträge nicht hatte abschließen dürfen. Meine Probleme fangen weitaus früher an. Ich habe diese Geschichte zur Anfangszeit verfolgt, und fast alles, was ich damals gelesen habe hat schon auf das hingedeutet, was am Ende passiert ist: der EuGH hat die Maut gekippt.

Wie hat die Sache angefangen? Dazu gibt es einen ganzen Wikipedia-Artikel, aber ich versuche das mal, aus meinem Gedächtnis zu rekonstruieren:

Damals wollte der Dobrindt eine PKW-Maut einführen. Natürlich wollte sich der Dobrindt nicht den deutschen Autofahrer zum Feind machen (eine Tendenz, die immer noch den Verkehrswandel behindert). Also wurde die Maut als Ausländermaut vermarktet, so dass am Ende nur für PKWs Maut erhoben werden sollte, die kein deutsches Kennzeichen haben. Klassischer populistischer „wir Innländer gegen die Ausländer“-Unsinn. Nur gab es zwei Probleme:

  1. Sind das garnicht so viele ausländische PKW auf deutschen Straßen, die überwiegende Mehrheit ist deutsch. Für LKW ist das vielleicht anders, aber für die gab (und gibt) es ja schon eine Maut. Je nachdem, wem man zuhörte sollte die Maut hunderte Millionen einbringen, aber realistischere Schätzungen gingen davon aus, dass die Maut im besten Fall nur wenig einbringt, aber wahrscheinlich nicht einmal die Kosten deckt.
  2. Wäre das eine Diskriminierung gegenüber anderen EU-Bürgern. Das sollte damit herausgetrickst werden, dass die deutschen Autofahrer trotzdem die Maut zahlen, aber sie durch Steuervergünstigungen wieder zurück kriegen.

Kurz: Das war von Anfang an eine Scheißidee. Und alle haben es gewusst. Trotzdem wurde das Ding durchgezogen. Die CDU hat das mitgemacht, um die CSU zu beschwichtigen. Am Ende ist die Maut dann 2019 vom EuGH gekippt worden, was absolut niemanden überrascht hat. Außer den Herrn Scheuer, der schon einen Haufen Verträge abgeschlossen hatte, die nun gekündigt wurden, was am Ende zur oben genannten Schlagzeile geführt hat.

Ich würde ja sagen, das mir kein anderes Gesetz einfällt, das mit Ansage genau so verkackt wurde. Aber das wäre gelogen. Die Vorratsdatenspeicherung war sogar zwei Mal da und wurde jedes Mal von obersten Gerichten gekippt. Und momentan rennen wir sehenden Auges in die Klimakatastrophe, aber die Gesetze, die daran etwas ändern könnten, werden eifrig entkernt.

Und wieder ist klar, dass das ein Fehler ist und dass wir später teuer dafür bezahlen werden. Aber egal, hauptsache, wir schwächen die Wirtschaft jetzt nicht.