Ruby Metaprogrammieren
Ich bin nicht sicher, ob Metaprogrammierung die richtige Bezeichnung ist - vor allem, weil es etwas hochtrabend klingt. Es geht um ein simples Problem: Man hat einen String und will eine Funktion oder eine Variable aufrufen, die so heißt wie der Inhalt des Strings.
Also: Ich habe "auto" in der Variable id
und will A.autoBahn
aufrufen. Das passiert in der Praxis öfter mal, wenn in bestimmten Teilen des Codes Variablen mit einer id konstruiert werden, z.B. autoBahn, fahrradBahn, eisenBahn…
class A attr_accessor :autoBahn def initialize self.autoBahn = "A44" end end a = A.new id = "auto" puts a.send(auto + "Bahn") # => A44
Ruby kennt auch eval, was sich genau so verhält wie erwartet: Es führt den Inhalt eines String als Programmcode aus. Nichts für von Nutzern wählbare Variablen, mächtiger als nur zum Variablen oder Methoden aufrufen, aber auch dafür kann man es nutzen.
eval "A.#{id}Bahn" # => A44
Variablen dynamisch zu lesen ist super. Es fehlt nur noch, sie ebenso schreiben zu können. Mit instance_variable_set
geht das auch. Ich bin anfangs nur darüber gestolpert, dass das erste Zeichen des Namens ein @ sein muss.
A.instance_variable_set('@Landstrasse', "B44")
Vielleicht ist es meistens nicht toll, so etwas benutzen zu müssen. Aber richtig eingesetzt kann es sehr viele Zeilen voller Wiederholungen auf einige wenige komprimieren.
Cyber Acoustics CA-1001WB
Darum gehts. Wobei, es ist ein Mittel zum Zweck.
In meinem PC moderten zwei Laufwerke für sich hin. Das DVD-Laufwerk könnte nochmal praktisch sein. Aber für den CD-Brenner werde ich wohl eher keine Verwendung mehr haben. Beide Laufwerke sind nicht angeschlossen. Sie blieben nur im Gehäuse, weil mir die Blenden fehlen um das Loch zu schließen, das sie hinterlassen würden. Die sind beim vorletzten Umzug verschwunden.
Gleichzeitig steht der PC nicht mehr im gleichen Zimmer wie die Stereoanlage, die vorher mit dem PC verbunden war. Die war durchaus praktisch um Sound für mehrere Personen zu haben, beispielsweise für Filme. Stattdessen kaufte ich einen kleinen mobilen Lautsprecher, der nach gutem Ersteindruck ziemlich sofort kaputtging.
Und in genau diesem Szenario ist das CA-1001 eine gute Idee. Es ist ein Lautsprecher, der statt eines Laufwerks in einen 5,25"-Schacht gesteckt wird. Dann verbindet man den Lautsprecher mit dem beigelegtem Kabel über eine PCI-Slot-Blende mit dem hinteren Ausgang der Soundkarte. Strom dazu und der PC hat integrierten Sound.
Ich habe erwartet, dass dieser Sound furchtbar sein würde. Dem ist nicht so. Ich will das Ding nicht zu sehr loben, aber ich war überrascht, dass Musik weder unerträglich kratzig noch zu leise abgespielt wird. Wobei ich die Lautstärke schon aufs Maximum drehen musste, um eine besonders leise Serie gut zu verstehen. Trotzdem, um ohne besondere Ansprüche einen Film zu gucken reicht es allemal. Und schlechter als das CD-Laufwerk oder das alternative Loch im Gehäuse sieht es auch nicht aus.
Syncthing statt Dropbox
Dropbox steht schon länger auf meiner Liste auszuwechselnder Dienste. Da mein Heimserver wieder läuft und aufgeschreckt von der Ankündigung des baldigst wegfallenden Spacerace-Speicherplatzes habe ich Dropbox durch syncthing ersetzt.
Vorläufiges Fazit: Das funktioniert und ist in manchen Belangen besser als Dropbox, in anderen dagegen hinkt es hinterher.
Einrichtung
Die Installation ist einfach genug. Für Arch liegt es in den Quellen und ist ein pacman -S snycthing
entfernt. Für Ubuntu gibt es ein PPA (Danke an Gerald für den Hinweis):
curl -s https://syncthing.net/release-key.txt | sudo apt-key add - echo "deb http://apt.syncthing.net/ syncthing release" | sudo tee /etc/apt/sources.list.d/syncthing.list sudo apt-get update sudo apt-get install syncthing
Danach startet der Befehl syncthing
den Dienst und auch die Weboberfläche auf Port 8080. Dort können Ordner hinzugefügt werden, bei mir beispielweise der Dropbox-Ordner.
Besser ist es, syncthing-inotify ebenfalls zu installieren und den Rescan-Intervall des Sync-Ordners auf 0 zu setzen. Syncthing selbst fehlt das Feature, Änderungen direkt zu erkennen, stattdessen wird alle x Sekunden der Syncordner gescannt. Das ist Verschwendung und war hier schlicht inakzeptabel, weil so die Festplatte des Servers sich nie abschalten würde.
Zum Glück ist auch syncthing-inotify nicht zu kompliziert, man kann einfach das Release herunterladen, es nach /usr/local/bin/ verschieben und dann mit
syncthing-inotify -api=xxxxxxx
starten. Der API-Key findet sich in den Einstellungen.
Syncthing vs Dropbox
Im direkten Vergleich schlägt syncthing sich wacker.
ID statt Account ++/+
Syncthing nutzt ein ID-System. Statt einen Account zu haben, Dropbox zu installieren und sich dann im Client mit dem Account anzumelden, funktioniert bei syncthing alles über die Kontrolle des Rechners. Die ID des anderen Rechners eingeben, dies gegenseitig bestätigen, fertig. Es ist etwas ungewohnt, das spricht für Dropbox. Aber das ID-System ist letztendlich auch nicht komplizierter, und der Wegfall des Passworts ist praktisch.
Server statt Cloud ++/+-
Der große Unterschied ist natürlich, dass syncthing nicht mit einem zentralen Server funktioniert. Statt proprietärer Cloud-Lösung freie Software zum Selberhosten. Der Nachteil dabei ist, dass ich meinen Bürorechner nie mit meinem Heimrechner syncen könnte, hätte ich nicht zusätzlich den Pogo als Heimserver, der als Brücke dient. Denn die beiden sind nie gleichzeitig an. Der Vorteil ist der unbegrenzte Speicherplatz, soweit die Festplatten eben mitmachen - und ausschlaggebend ist die volle Kontrolle über die eigenen Daten.
Konfigurierbare Backups ++/+
Das Backupsystem gefällt mir sehr gut. Für jeden Ordner auf jedem Client kann eingestellt werden, wieviele Revisionen gesichert werden sollen, und der sehr hübsche Staggered File Versioning-Modus sichert dynamisch viele Backup, je nach Intervall der Änderungen. Dropbox hat auch Backups, und sie sind in der Cloud sicher vor Unbill daheim, aber zumindest ohne zu zahlen sind sie weniger mächtig.
Firewalls durchlöchern +/+
Wirklich wichtig und entsprechend positiv war auch, dass die Firewall des Routers kein Problem ist. Für Dropbox ja auch nicht, aber Dropbox ist nur ein Client, kein Server. Ich war nicht sicher, on der Pogo problemlos erreicht würde, oder ob Portweiterleitungen und dyn-dns-Einträge nötig sein würden. Nein, das lief direkt.
Edit-Push
Ohne syncthing-inotify wäre das fehlende Erkennen von Änderungen ein ernstes Problem gewesen, so nachgerüstet ist das ok.
Konflikte -/+
Andererseits hat syncthing kein Verständnis für Konflikte. Kommt eine Änderung verspätet an und die Datei wurde lokal schon verändert, werden diese lokalen Änderungen schlicht überschrieben. Gerade am Anfang, als ich den Syncordner zeitversetzt auf allen Rechnern freigeben musste, war das nicht vertrauenserweckend - funktioniert dort aber. Und immerhin ist das kein Problem, in das ich bei meiner üblichen Nutzung rennen sollte. Trotzdem, Dropbox macht das besser.
CPU und Ram -/++
Negativ ist die Ressourcennutzung. Nein, es hat meinen PC im Normalbetrieb nicht merklich ausgebremst. Aber am Anfang gab es Probleme: Der auf allen Clients schon vorhandene Sync-Ordner wurde nach dem Hinzufügen auf dem letzten Client, dem Laptop, daraufhin wohl als neu markiert und sowohl an meinen Desktop als auch an den Pogo zurückgeschickt. Auf dem Desktop nutzte dieser Sync 270% meiner Dreikern-CPU, also fast alles, und dauerte trotzdem über eine Stunde. Dropbox hat solche Kinderkrankheiten schon lange nicht mehr und arbeitet unbemerkt im Hintergrund.
Auch eine solche Kinderkrankheit: Im Normalbetrieb schießt die CPU-Nutzung auf 150%, wenn die Webadministration aufgerufen wird, ist sie zu sind es immerhin nur ~2%. 2,5GB Ram werden gerade belegt, plus 800MB für snycthing-inotify. Das ist zu viel, wenn es echte Nutzung wäre, aber die Zahlen könnten so hoch sein, weil 12GB im Rechner sind und syncthing sich ungenutzte Ressourcen anrechnen lässt. Immerhin funktioniert es auch auf dem Pogo, und der hat nur 256MB Ram.Verschlüsselung -/-
Schade auch, dass Dateien auf dem Server nicht von syncthing verschlüsselt werden. Denn das bedeutet, dass ich meine encfs-Verschlüsselung nicht einmotten kann, obwohl die Daten nicht mehr in die Cloud geschickt werden.
Fazit
In kurz: syncthing ist nett. Eine echte Alternative zu Dropbox ist es aber nur, wenn man irgendwo einen Server stehen hat. Und es hat noch nicht Dropboxs Feinschliff, massenmarktreif ist es also nicht. Aber solange der Pogo läuft ist syncthing für mich die bessere Lösung.
Wer es etwas einfacher oder Dropbox-ähnlicher haben will, der sollte sich Syncthing-GTK anschauen. Beispielweise beinhaltet das direkt inotify-Support. Die UI könnte für alles außer Server generell der richtige Weg sein, syncthing zu nutzen.
Grub failcheck deaktivieren
Eine weitere Entdeckung über Grub 2: Eines der Probleme mit ihm lässt sich einfach lösen, man muss nur wissen wie.
Mein Ubuntu folgt nicht dem Standard, ich benutze keine Desktopumgebung und keinen Displaymanager. Eines der Ergebnisse davon war, dass seit einiger Zeit der Timeout von Grub fehlt. Statt automatisch nach kurzer Zeit zu startete wartete er im Bootmenü auf das Drücken der Enter-Taste, und ich wusste nicht wieso. Nach einiger Zeit und etwas Testen kam ich dann darauf, dass es am failcheck liegt: Irgendetwas an meinem Setup führt dazu, dass Grub denkt, der letzte Start wäre gescheitert. Und auch das dauerte etwas, um es herauszufinden: Diesen Check kann man deaktivieren oder umgehen.
Um ihn zu umgehen, muss nur die Zeile
GRUB_RECORDFAIL_TIMEOUT=5
in die /etc/default/grub hinzugefügt und ein sudo update-grub
ausgeführt werden. Schon ist der Countdown wieder da, der failcheck entschärft. Nun läuft er bis zur Passworteingabe durch, was ich sehr angenehm finde.
Grub 2 verschönern
Mir war nicht klar, dass das inzwischen ohne größeren Aufwand möglich ist. Grub 2 unterstützt Themes, und inzwischen gibt es sogar welche. Das Bootmenü sieht so doch etwas hübscher aus.
Ich wählt fürs erste das Theme Grau:
Das Archiv herunterladen, entpacken, im Terminal mit
sudo ./install.sh
installieren, fertig.
Statistik in Ruby mit Statsample
Ich muss immer mal wieder Statistik anwenden, meist ohne dann wirklich Zeit dafür zu haben. R oder Julia ist mir fremd, daher griff ich meist auf handgeschriebene Bash-Skripts zurück und machte die schwierigeren Sachen, wie die Faktor- oder Signifikanzanalyse, mit externen Tools. Was eben gerade da ist.
Diesmal bin ich über statsample gestolpert. Das gem implementiert die relevanten Statistikfunktionen. Ich benutzte diesmal sowieso Ruby, da passte das gut in meine Datenverarbeitungsskripte. Und in meinen Augen hat es sich bewährt.
Zwei Beispiele: Ich habe in einer SQLite-Datenbank eine Sammlung von Kommentaren mit ihren Upvotes, und die Kommentare kann ich in zwei Gruppen unterteilen. Ich will wissen, ob die Verteilung der Upvotes zwischen den Gruppen sich signifikant unterscheidet, und wähle dafür einen t-Test. Das geht so:
require 'sqlite3' require 'statsample' include Statsample::Test db = SQLite3::Database.new "database.db" aUpvotes = db.execute("SELECT upvotes FROM comments WHERE group = 'a'";).flatten.to_scale bUpvotes = db.execute("SELECT upvotes FROM comments WHERE group = 'b'";).flatten.to_scale t_2=Statsample::Test::T::TwoSamplesIndependent.new(aUpvotes, bUpvotes) puts t_2.summary
Die Ausgabe enthält dann alle wichtigen Informationen und verrät mir, dass die zweite Stichprobe deutlich kleiner, die Varianz gleich ist und das benötigte Signifikanzniveau p = 0.5522
weit von statistischer Signifikanz weg ist.
= Two Sample T Test Mean and standard deviation +----------+--------+---------+------+ | Variable | mean | sd | n | +----------+--------+---------+------+ | Vector 1 | 9.1267 | 44.8019 | 3332 | | Vector 2 | 7.4539 | 59.6217 | 293 | +----------+--------+---------+------+ Levene test for equality of variances : F(1, 3623) = 0.4437 , p = 0.5054 T statistics +--------------------+--------+----------+----------------+ | Type | t | df | p (both tails) | +--------------------+--------+----------+----------------+ | Equal variance | 0.5945 | 3623 | 0.5522 | | Non equal variance | 0.4687 | 321.6479 | 0.6396 | +--------------------+--------+----------+----------------+ Effect size +-------+--------+ | x1-x2 | 1.6727 | | d | 0.2467 | +-------+--------+
Das zweite Beispiel ist ein chi²-Test um zwischen zwei Stichproben mit Kategorien zu unterscheiden. Ich habe bei der ersten Stichprobe 60 Leute in Kategorie A, 10 in B, 30 in C. Bei der zweiten sind es 60 in A, 25 in B und 15 in C. Signifikanter Unterschied?
m=Matrix[[60, 10, 30], [60, 25, 15]] x_2=Statsample::Test.chi_square(m) puts x_2.probability # => 0.003…
Hier gibt es keine schöne Ausgabe, sondern nur den p-Wert. Der ist mit 0.003 unterhalb einiger typischer Signifikanzniveaus, also ist der Unterschied zwischen den Beobachtungen für p < 0.01
beispielsweise signifikant.
Ich habe ein bisschen das Drumrum erklärt weil dort das Problem des Gems ist: Es fehlt Dokumentation. Klar, es ist nicht die direkte Aufgabe des Gems, dem Nutzer einen Statistikkurs zu geben. Es ist ein Werkzeug, mit dem Leute, die wissen was sie machen müssen, diese Aufgabe durchführen können. Aber selbst wenn man eine vage Ahnung hat findet man zu wenig darüber, wie man die Funktionen aufzurufen hat.
The Darkness 2
The Darkness 2 ist ein Ego-Shooter mit Fokus auf der Story und mit ein paar Rollenspielelementen. Der Protagonist ist von einem Dämon (der Darkness) besessen, der ihm Tentakel und Schlangenköpfe aus dem Leib wachsen lässt, die zusätzlich zu den Pistolen und Gewehren als Waffen fungieren. Mit ihnen können Gegner attackiert und Gegenstände aufgenommen und geworfen werden.
Das Ganze ist wohl eine Comicumsetzung. Erklärt vielleicht, warum das Spiel ziemlich brutal ist. Und der Grafikstil ist etwas ungewöhnlich, wohl nicht ganz Cell-Shading, aber fast. Das sieht nicht schlecht aus.
Es gibt ein paar Parallelen zu Bioshock Infinite. Beide Spiele sind gut gemachte Shooter mit ein paar netten Zusatzelementen, aber spielerisch nicht weltbewegend. Über weite Teile des Spiels gibt es einen Begleiter, der hier in Schleichpassagen selbst gespielt wird. Zwar reicht seine Inszenierung längst nicht an Elizabeth heran, schlecht gemacht ist er aber auch nicht.
Der Anfang ist bombastisch, die Enden schwierig zu mögen aber bemerkenswert. Generell zieht das Spiel viel seines Reizes aus der düsteren Story und aus dem Spiel zwischen den zwei Realitätsebenen, das erinnert dann zwischendurch an Spec Ops: The Line. Obendrauf gibt es retrospektive Erzählungen in theatermonologähnlichen Zwischensequenzen. Mir hat das gut gefallen.
Performance-Tuning einer Webapp mit Index und Arbeitsvermeidung
Meinen Feedreader feedtragón nutze ich praktisch jeden Tag. Ich bin soweit zufrieden, aber nach der Eingewöhnungsphase fiel mir auf, dass er etwas langsam war. Gemessen und uff: Die längste Wartezeit kam durchs Warten auf eine Antwort vom Server. Kein gutes Zeichen. In diesem Artikel werde ich beschreiben, wie ich das Problem anging und dabei diese Verbesserung produzierte:
Die Wartezeit wurde reduziert auf ein 84stel, von 2,8 Sekunden am Anfang auf ~33ms.
Schritt 1: Index setzen
Jedes mal, wenn eine Seite geladen wird, holt feedtragón eine Liste aller abonnierten Feeds vom Server, und prüft dann jeden Feed auf ungelesene Einträge. Dafür muss er die Einträge aus der Datenbank holen, das macht er mit diesem Query:
SELECT url, title, content, id FROM entries WHERE feed = ? AND read = 0 AND id > ? LIMIT 10;
Er beschränkt sich also auf 10 Einträge mit einer höheren ID als die Start-ID, so funktioniert das Endless-Scrolling, aber das ist hier nicht weiter wichtig. Wichtig ist der Rest des WHERE-Statemends: feed = ? AND read = 0
. Da werden also zwei Spalten abgefragt. Und insgesamt dauerte das alles 2,8 Sekunden - also nicht nur dieser Query, sondern alle zusammen.
Ich bin kein Datenbankexperte, aber ich vermutete, dass diese Abfrage schneller funktionieren könnte. Das ist eine SQLite-Datenbank, dort kann man das mit EXPLAIN QUERY PLAN
prüfen:
sqlite> EXPLAIN QUERY PLAN SELECT url, title, content, id FROM entries WHERE feed = ? AND read = 0 AND id > ? LIMIT 10; 0|0|0|SEARCH TABLE entries USING INTEGER PRIMARY KEY (rowid>?)
Was heißt das jetzt? Ich musste es nachgucken. Dass er hier nur mit dem Primary Key sucht heißt, dass er für read und feed keine andere Möglichkeit hat als manuell zu suchen. Also muss dafür je ein Index her:
sqlite> CREATE INDEX entries_read_idx ON entries(read); sqlite> CREATE INDEX entries_feed_idx ON entries(feed); sqlite> EXPLAIN QUERY PLAN SELECT url, title, content, id FROM entries WHERE feed = ? AND read = 0 AND id > ? LIMIT 10; 0|0|0|SEARCH TABLE entries USING INDEX entries_feed_idx (feed=? AND rowid>?)
feedtragón gestartet, den Browser aufgemacht, in die Netzwerkanalyse geschaut und mich gefreut: Die Wartezeit auf den Server schrumpfte von 2,8s auf 0,5s. Damit war die Sache für mich erstmal abgehakt und feedtragón ausreichend optimiert.
Schritt 2: Join (Arbeitsvermeidung)
Später kam mir dann der Gedanke, dass es besser gehen müsste. Denn das Vorgehen, das ich oben beschrieb - alle Feeds holen, ans Template schicken, dort deren Einträge holen, dann nur die Feeds mit ungelesenen Einträgen anzeigen - ist keineswegs effizient. Es folgt aus einer strikten Einhaltung der für dsnblog skizzierten Architekturprinzipen: Objekte ohne Controller, die für ihre Daten zuständig sind. Das ist toll zu schreiben, besonders das Datenbanklayer bleibt einfach beherrschbar, aber Dee erwähnte damals das möglicherweise kostspielige Befüllen der Daten. Und die Einträge des Feeds aus der Datenbank zu holen ist kostspielig.
Vorher holte er also alle Feeds aus der Datenbank:
SELECT url, id, name FROM feeds;
und filterte dann im Template die Feeds aus, die keine neuen Einträge haben:
feeds.each do |feed| erb :feedlink, :locals => {:feed => feed, :current_feed_id => current_feed_id} if ! feed.entries.nil? && feed.entries.size > 0 end
wobei feed.entries() wieder die Datenbank aufruft, mit dem im ersten Schritt optimierten Query.
Stattdessen könnte man auch direkt nur die Feeds übergeben, die ungelesen Einträge haben. Dafür muss nur die Feed- und die Eintragstabelle gejoint werden:
SELECT DISTINCT feeds.url, feeds.id, feeds.name FROM feeds JOIN entries ON (entries.feed = feeds.id) WHERE entries.read = 0;
Das Filtern kann dann entfallen. Und so schrumpften die 0,5s noch einmal, auf diesmal 0,033s.
Das ist vielleicht etwas spezifisch. Der Kern hier ist: Unnötige Datenbankabfragen durch einen geschickten Join zu vermeiden ist eine gute Idee, und es auch wert wenn es etwas gegen die vorhandene Struktur geht.
Schlusswort
Natürlich sind das keine konstanten Werte. Wären mehr Feeds und mehr Einträge in der Datenbank bräuchte alles länger. Der Server hat wahrscheinlich eine langsamere Platte als mein Rechner mit seiner SSD. Außerdem ist nun nur die Wartezeit auf den Server minimiert, er muss immer noch danach etwaige Bilder herunterladen, außerdem kommt die Transportzeit zum Server dazu.
Trotzdem: Der Unterschied ist auch auf dem Server da. Statt "Klick, Warten, Rendern, da" ist es nun "Klick, Rendern, da" - den Schritt auf realistisch gesehen etwa unter eine Sekunde Ladezeit, von vorher über 3, spürt man deutlich.
pc-kombo empfiehlt Arbeitsspeicher
In den Hardwareempfehler pc-kombo habe ich beim monatlichen Update nun Empfehlungen für Arbeitsspeicher eingebaut.
Er empfiehlt DDR3-1600, für Billig-PCs ein 2GB-Modul, für PCs bis 500€ ein 4GB-Kit und für alles darüber ein 8GB-Kit. Man sieht an den harten Grenzen vielleicht: Völlig ausgereift ist das noch nicht, und die Empfehlung basiert auf Tests, subjektive Einschätzung und momentanen Preis, nicht auf Benchmarks. Aber es sind in meinen Augen dennoch sinnvolle Empfehlungen.
Mit der neuen Hardwarekategorie funktionierte das alte Design nicht mehr gut. Zusammen mit den Mainboardempfehlungen geht es jetzt mehr in Richtung vollständigen PC-Empfehler, daher gibt es jetzt eine Listensicht mit integriertem Link zu Amazon, statt wie vorher für die Links eine separate Sektion unten zu haben und den Prozessor neben die GPU zu setzen. Ich finds gut, so geht es langsam wieder zu der Ursprungsidee eines vollständigen PC-Konfigurators zurück. Nur fehlen mir immer noch gute Ideen für den Rest des Interface.
Edit Und der im Screenshot sichtbare kleine Dreher mit der Geforce GTX 690 ist nun auch behoben