Neues aus dem GraalVM-Universum

Lange nichts mehr zu Graal(VM) geschrieben. Eigentlich überflüssig, diese einleitenden Worte zu verwenden, denn so wenig, wie ich hier schreibe, ist es naturgemäß bei und zu praktisch jedem Thema lange her.

Jedenfalls gibt es ein neues Release, quasi passend und synchron zum Erscheinen von Java 21. Viel hat sich getan im Java-Universum – Projekt Loom aka “Virtual Threads” hat es nun endlich final ins Release geschafft, etwas das Projekt Panama aka “Foreign Memory and Function and Vector API” leider nicht vergönnt war. Egal, nach dem LTS-Release ist vor dem LTS-Release, machen wir solange halt JNA. Aber ich schweife ab.

Die GraalVM-Release-Notes vermelden neben den fast schon üblichen – teilweise weiterhin dramatischen – Verbesserungen im Bereich Memory Footprint, Startup Time und Throughput zwei aus meiner Sicht ganz wichtige Neuerungen: zum einen ist man das unselige “gu”-Zeugs los geworden. Bedeutet: man hat nicht mehr ein Graal-JDK rumliegen das man per “gu”-Aufruf quasi in situ und systemspezifisch anreichert und updated – z.B. mit Truffle-Sprachen – sondern die ganze polyglotte Geschichte ist nun in “einfache” Maven-Abhängigkeiten gewandert, d.h. man kann (bzw. sollte können – ich hab’s nicht selbst verifiziert) nun z.B. tatsächlich sinnvoll die Rhino- bzw. Nashorn-Engine tatsächlich in Rente schicken und stattdessen die Javascript-Implementierung von Graal verwenden. Das ganze läuft unter dem Namen “Truffle Unchained”. Schöne Sache, weil dadurch die ganze Geschichte mit der polyglotten Programmierung endlich anständig und erwartungskonform ins Java-Ökosystem integriert wird anstatt eine GraalVM-Insellösung zu bleiben. Man sollte nur darauf achten, die “community”-Varianten als dependency anzuziehen, es sei denn man will sich mal wieder durch eine neue Variante einer Oracle-Lizenz (“GFTC”) kämpfen, und gleichzeitig immer auf der Hut sein, dass Oracle nicht mal wieder was Signifikantes daran ändert.

Zweite wichtige Geschichte: Der G1-GC ist jetzt vollständig integriert und optimiert – im Juni-Release war davon ja schon einiges zu sehen, jetzt gibt es vernünftige Ergebnisse. Die GraalVM hatte da ja Nachholbedarf v.a. im Native-Image-Bereich, was für teils erhebliche Laufzeitnachteile gesorgt hat. Nicht zuletzt durch diese Neuerung ist es gelungen, dass AOT jetzt performancetechnisch beim Durchsatz auf demselben Niveau operiert wie “heated up HotSpot JIT” – lange Zeit war es ja eine Art Tradeoff, ob der Startup-Time-Vorteil von Native Image wichtiger ist oder der Throughput-Vorteil des HotSpot-JIT. Diese Überlegung kann man sich jetzt sparen, wenn die Benchmarks von den Graal-Entwicklern der Wahrheit nahekommen.

Für Mac-Freunde mit ARM-CPU und Linux-on-ARM-Exoten vielleicht auch noch interessant: AArch64 aka ARM 64bit ist nun auch verfügbar. War es wohl schon bei früheren Graal-Releases, ich hab’s nur nicht mitgekriegt. Aber der oben genannte G1-GC hat es wohl jetzt erst in die AArch64-Native-Image-Implementierung geschafft. Warum auch immer…da will ich nicht länger drüber nachdenken.

OpenJDK Overflow

Früher war die Welt noch einfach. Wenn man Java wollte (und ich rede jetzt nur mal von Java SE, sonst gibt das gleich wieder eine hundertseitige Abhandlung), ging man zu java.sun.com (oder gar zu java.de), hat JDK oder JRE seiner Wahl runtergeladen, und gut war. Klar, eher nicht das JRE, weil Sun irgendwann mal anfing, irgendwelche Ad-Ware damit zu bundeln, und weil es immer Installer-gebunden war und nicht als schnödes nacktes ZIP zur Verfügung stand.

Heute, im Zeitalter von OpenJDK auf der einen Seite und Oracles wöchentlich wechselnden Lizenzbedingen auf der anderen Seite schaut man sich natürlich nach einem passenden “Stamm-Provider” eines OpenJDK-Pakets an. AdoptOpenJDK war früher mal meine erste Wahl – Auswahl zwischen HotSpot und OpenJ9, sowohl JRE als auch JDK, viele Plattformen unterstützt, sowohl x86 als auch x64. Inzwischen ist das zu “Eclipse Temurin” von “Adoptium” gemorphed, die Auswahl zwischen HotSpot und OpenJ9 scheint es nicht mehr zu geben, aber dafür gibt es bei IBM (wider besseren Wissens setze ich jetzt mal einen Link, wohl wissend, dass der in wenigen Monaten auf Nimmerwiedersehen einer Restrukturierung der IBM-Webpräsenz zum Opfer fällt) ein OpenJDK namens “IBM Semeru”, in den Geschmacksrichtungen “Certified Edition” oder “Open Edition”, für den typischen IBM-Bauchladen (System/390, AIX) aber auch für Windows/Linux/MacOS als OpenJDK-OpenJ9-Bundle. Sinn? Übersichtlichkeit? Keine Ahnung. Derweil notiere ich: für die Top-IBM-Plattformen S/390 und AIX gibt es als neueste Version Java 11. Auf IBM-Plattformen meint “long term” eben meistens die lange Zeit, bis aktuelle Versionen zur Verfügung stehen. Meine letzte gute Erfahrung mit IBM JDKs war mit Java 1.3, als auf einem linux-basierten ThinClient mit fast nacktem XServer das IBM-Java deutlich besser mit TrueType-Fonts umgehen konnte als die Sun-Konkurrenz. Lange her. Ansonsten zeichnete sich die IBM-Variante oftmals durch höheren Speicherverbrauch aus (ich erinnere mich an die Class-Sharing-Lösung in JDK 1.5, die zwar relativ gesehen pro zusätzlicher JVM-Instanz eine gute Menge Speicher sparte, aber absolut gesehen gegenüber der Sun-Lösung leider deutlich mehr verbrauchte) und dazu kamen noch abstruse Bugs im JIT-Bereich sowie irgendein dubioses Encoding-Problem im XML-Bereich, das mir den Schlaf raubte.

Ich will jetzt nicht alle OpenJDK-Distributionen aufzählen, die Menge ist inzwischen unüberschaubar, Anbieter kommen und gehen, zeichnen sich durch unvollständige Unterstützung diverser Plattformen aus, haben undurchschaubare Eigeninteressen…unterm Strich habe ich mich jetzt auf Bell Software eingeschossen, die bieten eine OpenJDK-Distribution namens “Liberica JDK” zum Download an. Besonderheit aus meiner Sicht: alle Versionen bis zurück zu Java 8, alle für mich relevante Plattformen, sowohl JRE als auch JDK, sowohl x86 als auch x64, und als besonderen Service auch noch “Full JRE” und “Full JDK”, ein Bundle bestehend aus Java SE und OpenJFX. Und dazu noch pre-built docker images, z.B. in einer Minimalversion mit Alpine Linux mit musl-Standard-Lib. Seit Längerem verwende ich praktisch nur noch die diversen Bell-Distributionen, alles funktioniert prächtig – ich habe aufgehört, nach besseren Alternativen zu suchen.

Eine interessante, aus meiner Sicht aber noch eher experimentelle Variante: OpenJDK in der GraalVM Community Edition. Mit dem Graal-JIT, der je nach Workload durchaus Vorteile bringen kann. Derzeit downloadbar als Version 22.1 in den Geschmacksrichtungen Java 11 und Java 17. Quasi der Außenseitertipp unter den OpenJDK-Distributionen.

Zum Schluss noch ein typisches “wann ist mir denn das entgangen” beim Abprüfen, ob noch alle Anbieter an die ich mich erinnere im Rennen sind: was ist eigentlich JITServer? Das habe ich gerade zum ersten Mal gehört bzw. gelesen. Es gibt aber schon seriöse Infos zu diesem Thema von Januar 2020, mit Andeutungen eines existierenden Prototyps namens “JIT-as-a-Service” schon Mitte 2019. Auch so eine IBM-OpenJ9-voll-das-fancy-Cloud-Zeugs-Geschichte. Dieses Java-Ökosystem kann einen in den Wahnsinn treiben, wenn man da auf dem Laufenden sein und bleiben will. Nächstes Mal zu diesem Thema: warum fängt man mit “Project Leyden” an, wenn man schon Graal hat? Die Antwort wird wohl eine ähnliche sein wie bei “warum Java Modules, wenn es schon OSGi gibt”. Es kann was Gutes draus werden, aber man weiß es noch nicht, und es ist ein Haufen Arbeit auf einer ganz langen Zeitschiene. Und entlang dieser Zeitschiene liegen jede Menge Leichen aus früheren gut gemeinten Ansätzen.

Gedanken zum aktuellen Log4j-Problem

Viele haben sich schon zu unterschiedlichen Aspekten des aktuell in der Sonne der Aufmerksamkeit stehenden Log4j-Remote-Code-Execution-Problems-durch-clever-formulierten-User-Input-der-geloggt-wird geäußert. Mir fällt dabei vor allem auf, dass die meisten Kommentatoren zwar kleine Teilaspekte des zugrundeliegenden Problems meist korrekt beschreiben, es aber an abwägendem sowohl-als-auch eher mangelt. Denn m.E. ist diese Misere keins von den Problemen, die eine einfache Lösung haben.

Ich kann mit vielen der bereits von Anderen genannten Teilaspekte mitgehen. Ja, in Java kann man z.B. durch fortgeschrittene Featuritis und Libraritis und Frameworkitis sehr komplexe Lösungen bauen, die keiner mehr durchschaut. Aber das geht in praktisch jeder Programmiersprache, außer in denen, die schon unhandlich werden, sobald man versucht mäßig komplexe Probleme zu lösen. Hier ein Java-Spezifikum zu sehen ist komplett absurd. Ja, 3rd-party-Abhängigkeiten die man nicht durchverstanden hat sind potenziell gefährlich. Aber selbstgebaute Lösungen sind ebenfalls gefährlich. Ja, der Zeitdruck und die inadäquate Bezahlung in vielen Softwareprojekten ist eine der vielen Problemursachen. Aber auch ohne Zeitdruck und mit unendlichen Mitteln können Katastrophen programmiert werden. Ja, man sollte danach streben, möglichst einfache Lösungen zu bauen. Aber einfache Lösungen, die gleichzeitig problemadäquat sind, fallen auch nicht vom Himmel und brauchen Zeit und Geld – Vereinfachung ohne es zu einfach zu machen ist eine hohe Kunst.

Beim gerade aktuellen Problem kann man es sich einfach machen und sagen “was kann an Logging schon so schwer sein”. Ja dann nehmt doch einfach System.out.println, wenn das ausreichend ist. Kurze Zeit später werdet ihr feststellen, dass es ganz nützlich wäre, mehrere Logkanäle zu bedienen. Den Loglevel von außen konfigurierbar zu machen. Beim Schreiben von Logfiles jedes einzelne File nicht zu groß werden zu lassen. Auf Effizienz beim Loggen achten, also am besten asynchron arbeiten. Und die Log-Datenbank xy anbinden. Für das Loganalysetool der Wahl den Timestamp der Logmeldung parametrierbar zu machen. Ich glaube nicht, dass eine signifikante Anzahl der Log4j-Features “einfach so” eingebaut wurden, da steckt schon jeweils ein valider Usecase dahinter. Und es hat seinen Grund, warum es nicht nur Log4j gibt, sondern auch JUL oder Logback oder tinylog – oder der gute alte IBM LogManager, falls den noch jemand kennt. Wer die Features eines Log4j nicht will oder braucht, versteckt sich hinter slf4j und überlässt dem Kunden die Auswahl der finalen Logimplementierung – aber öffnet gleichzeitig einen interessanten neuen Angriffsvektor, wenn Schadcode beim Deployment als slf4j-implementierendes Jar eingeschleust wird. Und wenn ich dann nach aufwändiger Analyse zum Schluss komme, dass Logback alle meine Wünsche erfüllt, kommt morgen eine neue Anforderung um die Ecke, die nur durch Log4j abgebildet werden kann – was dann? Anforderung ablehnen? Lib wechseln? Von vorne anfangen?

Man soll sich keinen Illusionen hingeben. Was bei Log4j passiert ist, kann mit jeder Programmiersprache, auf jedem Betriebssystem, ja sogar auf jeder CPU passieren. Spectre konnte über JavaScript im Browser ausgenutzt werden – noch so viele Schichten zwischen Quellcode und Ausführung haben uns nicht vor den Auswirkungen eines CPU-Bugs geschützt. Schon im guten alten Z80 gab es “illegale” Opcodes, die trotzdem etwas Vernünftiges gemacht haben, als Seiteneffekt der Implementierung. Nur 8500 Transistoren, und trotzdem ein solcher “Bug” (oder “Feature”, je nachdem wen man fragt). Der aktuelle Apple M1 Max besteht aus 57 Milliarden Transistoren. Natürlich nicht direkt vergleichbar, weil jede Menge Cache damit realisiert ist, aber auch durch Caches können ja interessante Effekte entstehen (Stichwort Seitenkanalangriff). Jedenfalls ein geeignetes Beispiel für die immens gewachsene Komplexität auf allen beteiligten Ebenen.

Ich bin auch ein Fan davon, mit möglichst wenigen Abhängigkeiten in meinen Java-Anwendungen auszukommen. Aber diese Strategie trägt eben auch nicht unendlich weit. Beispiel: wenn Persistenzierung in einer Datenbank gefragt ist, kann man von Hand JDBC machen oder Hibernate verwenden oder JPA oder JDO oder was auch immer das Framework der Wahl so mitbringt. Oder man speichert seine Daten in einer simplen Datei. Oder doch über BerkeleyDB oder SQLite? Wenn die Anforderung lautet, einen Webservice zu schreiben, beginnt man dann mit seinem eigenen Server und seiner eigenen Security-Schicht, oder nimmt man doch was von der Stange und vertraut darauf, dass die Profis es entwickelt haben? Ist eine Bibliothek, die hundert unnütze Funktionen zusätzlich zu den für mich nützlichen zehn bietet, denn notwendigerweise die falsche Wahl, wenn die Konkurrenzbibliothek mit nur zwanzig unnützen Funktionen z.B. closed source ist oder viel weniger Nutzer hat oder eine viel kleinere Community? Oder alles drei? Ist es wirklich sinnvoll, sich auf den Featureumfang der Java-Plattform zu beschränken und sklavisch auf 3rd-party-Libs zu verzichten – ist denn sichergestellt, dass die “Bordmittel” wirklich von höherer Qualität sind als Drittbibliotheken?

Auf Basis von Post-hoc-Erkenntnissen klug daher schwätzen (“hättet ihr mal nicht Log4j verwendet” – ersetze Log4j durch “Intel-CPU” oder “OpenSSL” oder “PHP” oder “Linux” oder “Docker” oder “IIS” oder “Applets”) kann jeder. Aber was ist die “richtige Strategie” im Angesicht des Ungewissen, wenn man vor einer erheblichen zu implementierenden Komplexität steht, die nicht mal schnell ein Profi-Entwickler im Alleingang und minimalen Abhängigkeiten hindengelt (und dieser Profi dann auch dauerhaft für die Wartung zur Verfügung stehen muss)? Es wird letztlich immer eine Abwägungsfrage bleiben. Tritt kein Problem auf, war die Abwägung tendenziell richtig – oder man hat einfach nur Glück gehabt.

Vermutlich ist das die einzige vielversprechend Strategie. Einfach Glück haben.

Da fällt mir ein: schon 2015 habe ich unter der schönen Wortschöpfung “Abstraktionskaskade” diese Problematik beleuchtet. Und schon damals war ich nicht der erste.

Zwischenprojekt: KVMPortSwitcher

Meine Entwicklungsstrategie für private Projekte folgt meist einer Stack-Strategie: je älter die Idee, desto tiefer vergraben, ständig kommt neues Zeugs oben drauf, und in seltenen Fällen findet das jüngste Projekt direkt zur Reife. Und so geschah es mit dem jüngsten Spross meines bunten Straußes an Java-Projekten: KVMPortSwitcher. Wer nur den technischen Sermon lesen und den Code sehen will, darf direkt zum Projekt auf GitHub abbiegen.

Um was geht es? Seit einiger Zeit besitze ich einen “TESmart 16-port HDMI KVM Switch” (das exakte Modell gibt es offenbar nicht mehr, aber hier ist der Nachfolger zu finden). TESmart ist der einzige mir bekannte Hersteller, der KVM-Switches mit 8 Ports und mehr zu einigermaßen bezahlbaren Preisen anbietet und trotzdem problemlos funktioniert – meine Anforderungen damals waren “4K, HDMI, 8 Ports oder mehr, keine Spezialkabel”. Dadurch schrumpfte die Zahl der zur Auswahl stehenden Geräte nach ausführlicher Recherche auf “1”.

Nun gehört alles mit 8 Ports und mehr offenbar üblicherweise der Profi-Fraktion an, und so hat dieser Switch zusätzlich einen Ethernet-Anschluss, mit dem man ihn ins LAN integrieren kann, um ihn so über IP steuern zu können. Warum würde man das wollen? Zum einen zwecks Konfiguration des Geräts. Aber vor allem zur Steuerung des ausgewählten Ports. Denn die anderen Wege, den Port zu wechseln, sind mit verschiedenen Problemen und Nicklichkeiten verbunden, die einem ganz schön auf die Nerven gehen können.

Man kann die Knöpfe an der Front des KVM-Switches verwenden. Prinzipiell keine schlechte Idee, aber beim 16-Port-Modell braucht man dabei für die Geräte 10-16 zwei Tastendrücke. Oder sogar drei, denn wenn der Display-Timeout zugeschlagen hat, dient der erste Tastendruck gar nicht dem Umschalten, sondern dem Aktivieren. Also zwei oder drei Tastendrücke, und man muss natürlich die Hand von der Tastatur nehmen.

Wie wäre es mit der IR-Fernbedienung? Anders, aber nicht besser. Wenn man sie denn mal gefunden hat (vorsichtshalber steht nur “Remote Control” drauf, damit man sie ja nicht klar zuordnen kann), kann man zwar die Ports 2-9 durch einen Tastendruck auswählen, die Ports 1 und 10-16 hingegen nicht – was besonders bei “1” sehr dumm ist, da drückt man auf “1” und wundert sich, dass nix passiert, bis dem Gerät klar wird, dass keine Eingabe mehr erfolgt. Wer jetzt denkt, er könne durch zwei Tastendrücke “0” und “1” das Gerät überlisten, sieht sich getäuscht. auch “1” gefolgt von “OK” hilft nicht. Immerhin braucht es den “Aktivierungstastendruck” wie bei den Front-Panel-Buttons nicht.

Wie wäre es mit der eingebauten Möglichkeit, über die Tastatur den Port zu wechseln? Im Prinzip eine gute Idee, aber: es funktioniert nur mit den im Gerät hartcodierten Shortcuts, und die sind eher komisch, aber natürlich so gewählt, dass sie möglichst nicht mit anderen gängigen Tastatursteuerungen kollidieren: zwei Mal Scroll Lock (aka “Rollen”), dann 1-n. Man erkennt direkt das Problem für die 16-Port-Variante: bei Rollen-Rollen-1 gibt es die von der Fernbedienung bekannte Gedenksekunde, bis klar ist, dass man wirklich “1” meint und nicht “10” oder “13” und nur langsam tippt. Und noch ein Haken: das funktioniert natürlich nur, wenn die Tastatur im dafür vorgesehenen spezifischen USB-Port steckt, der diese Logik enthält – dieser Port ist aber nicht kompatibel mit allen Tastaturen dieser Welt, und so muss man manchmal auf den “nackten” USB-Port ausweichen. Und schon ist es Essig mit der Hotkey-Funktionalität.

Insgesamt gibt es also ausreichend Gründe, alternative Möglichkeiten zum Umschalten der Ports zu suchen. Eine davon bietet TESmart selbst an mit einem kleinen Tool, das eine IP-Verbindung aufbauen kann, dadurch den aktiven Port feststellt und dann per Mausclick das Umschalten erlaubt. Schön, aber natürlich eher keine Verbesserung zu den anderen drei Varianten. Tatsächlich dauert es doch recht lange, bis das Tool endlich den “Connect” hergestellt hat – keine Ahnung, warum. Mehrere Sekunden jedenfalls.

Nun muss man TESmart zugutehalten, dass sie das Protokoll über die IP-Schnittstelle zur Ansteuerung der Funktionalität öffentlich dokumentiert haben – leider muss man solche Dinge ja positiv vermerken, da es sich nicht um eine Selbstverständlichkeit handelt. Jedenfalls ist es denkbar einfach: IP-Verbindung herstellen, ein paar Bytes senden, fertig ist die Laube.

Ich hatte also so ein Projekt schon länger im Hinterkopf – meine Brot-und-Butter-Implementierungssprache Java schien da aber ungeeignet, weil man dort keine systemweiten Keyboard-Shortcuts beim Betriebssystem registrieren kann, und eine weitere grafische Oberfläche, wo man per Click den KVM-Switch bedienen kann, wollte ich nun auch nicht bauen. Zufällig bin ich bei meinen Streifzügen durch das Internet dann aber über eine Bibliothek namens JNativeHook gestolpert, die für alle gängigen Betriebssysteme und CPU-Architekturen native code zusammen mit etwas Java-Glue-Code zur Verfügung stellt, um solche globalen Hotkey-Listener zu implementieren. Kurz getestet, tut, also frisch ans Werk.

Der Code an sich und die UI war natürlich trivial. Kurz noch ein TrayIcon dazugeschraubt (denn für eine Anwendung, die normalerweise im Hintergrund läuft, ist es immer geschickt, noch einen Haken zur Kontrolle derselben zu haben), ausprobiert. Funktionierte – meistens. Denn es gab ein interessantes Phänomen, dass der NativeKey-Event-Dispatcher manchmal, wenn der Port umgeschaltet wurde, munter weiter denselben Event wieder und wieder feuerte. Ich bin dem noch nicht auf den Grund gegangen, aber habe einen klassischen Workaround gefunden: asynchrone Verarbeitung, der Listener löst nicht direkt den Port-Switch aus, sondern scheduled einen extra Thread, der kurz wartet und dann erst umschaltet. Das scheint das Problem zuverlässig gelöst zu haben.

Wer bis hierhin durchgehalten hat, darf jetzt zum GitHub-Projekt abbiegen. Es gibt auch ein runnable jar nebst inkludiertem jnativehook-Jar als Release 0.1.0 zum Download bei GitHub, Java 8 wird zur Ausführung benötigt. Wer also zufällig einen TESmart HDMI-KVM-Switch sein Eigen nennt und ihn in sein Heimnetz integriert (hat), kann direkt loslegen.

Auf der GitHub-Projektseite kann man auch nachlesen, was noch die TODOs sind. Konfigurierbare Port-Namen (bei 16 Ports kann man schon mal den Überblick verlieren), konfigurierbare Keyboard-Shortcuts, i18n (bis jetzt ist alles hartcodiert englisch). Und vielleicht auch noch für die old-school-Generation die Steuermöglichkeit über die serielle Schnittstelle des TESmart-KVM-Switches?

Java. Swing. Dem Wahnsinn nahe.

Seit Swing 1.0.2, als das Package noch com.sun.java.swing hieß und Java 1.1 gerade der heiße Scheiß war, programmiere ich UIs mit Java und Swing. Es ist so eine Art Hassliebe. Nach wie vor halte ich es für den einfachsten und besten Weg, Cross-Platform-Anwendungen zu schreiben, die mehr oder weniger problemlos unter Windows und Linux und MacOS funktionieren. Und an sonnigen Tagen auch unter AIX und Solaris.

Aber manchmal…manchmal…graue Haare, Stirnrunzeln beim Debugging, dem Wahnsinn nahe. Ich baue gerade an einem kleinen Projekt dessen tieferer Sinn oder Unsinn hier nichts zur Sache tun soll. Jedenfalls wird dort als Anzeigekomponente die allseits beliebe JEditorPane mit HTML-Inhalt verwendet. Das ist von jeher etwas kitzelig weil ein etwas merkwürdiges HTML-3.2-Sub-und-Superset-mit-etwas-CSS unterstützt wird, und man sollte sehr genau wissen welches HTML man reinsteckt und was unfallfrei gerendert werden kann. Immer spannend war das Thema Grafiken. Im Prinzip sollte es ja ein img-Tag mit passender URL dahinter tun. Blabla.class.getResource(“/schnickschnack.png”) liefert ja schon eine lokale File-URL zurück, was kann da schon schiefgehen.

Eine ganze Menge. Und was ist, wenn man gar keine Datei hat, sondern ein beliebiges Image in-memory? Weil das so ist, haben schlaue Menschen einen Weg ersonnen, wie man der JEditorPane – oder besser gesagt dem HTMLDocument, das über den HTMLEditorKit beim ContentType “text/html” bekanntlich entsteht – ein beliebiges Image unterjubeln kann, indem man es in einen “imageCache” unter der im img-Tag hinterlegten URL steckt.

Hier ein Link zu Sourcecode, der illustriert, wie das prinzipiell geht. Sehr heikel, weil es sich auf ein Implementierungsdetail verlässt, aber Swing ist bekanntlich tot…äh…stabil, und so wird das wohl auch in Zukunft problemlos funktionieren. Dachte ich.

Wenn man nun aber obigen Sourcecode mal in der IDE der Wahl compiliert und zur Ausführung bringt, erlebt man leider eine Überraschung: mit Java 7 funktioniert das noch problemlos, mit Java 8 und neuer aber nicht. Ein beherztes Debugging an der richtigen Stelle – javax.swing.text.html.ImageView.loadImage – brachte keinen Aufschluss – das Image wird korrekt aus dem Cache geholt, seine Größe wird inspiziert, es wird layoutet, und dann…wird nichts angezeigt.

Ich will es nicht zu spannend machen, denn es gibt einen Workaround. Auch wenn ich gerade überhaupt nicht verstehe, warum der was ändert, weil genau das, was er eventuell ändern könnte, im Debugging nicht nach dem Problem ausgesehen hat.

Wenn man die Zeile

"<img src=\""+localImageSrc+"\">\n" +

ändert in

"<img src=\""+localImageSrc+"\" width=100 height=50>\n" +

dann funktioniert es plötzlich prima von Java 7 bis einschließlich Java 16.

Wer auch immer das im Detail analysieren will – Freiwillige vor.

SwingHelpViewer – ein einfacher Online-Hilfe-Viewer

Seit einiger Zeit schraube ich an diversen Anwendungen auf Basis von Java Swing, die wirklich demnächst die Release-Reife erreichen sollten. Die meisten aus der Reihe “für RISC OS, aber nicht unter RISC OS lauffähig” (da Java-basiert, und es unter RISC OS keine kompetente JVM gibt). Hier kann man ein paar wenige Details zu diesen geplanten Anwendungen nachlesen.

Wie dem auch sei, solche klassischen FatClient-Anwendungen brauchen typischerweise eine Online-Hilfe. Allein um dem Benutzer mitzuteilen, welche 3rd-Party-Bibliotheken genutzt werden und unter welcher Lizenz diese stehen.

Standard unter Java ist für solche Zwecke JavaHelp, vor Urzeiten von Sun entwickelt, aber seit fast so langer Zeit auch nicht weiter gepflegt. Diverse ungefixte Bugs, unklare Lizenzsituation (alle Sourcefiles reden von GPLv2, irgendwo steht “GPLv2 with Classpath Exception”, und es gibt auch Hinweise, dass es dual lizenziert ist sowohl unter GPLv2 als auch unter CDDL), merkwürdige indexbasierte Suche die außer Englisch recht wenig versteht, unflexibel in der Integration. Aber durchaus mit coolen Features für die damalige Zeit, inklusive einer JSP-basierten Server-Variante.

Allein die Lizenzsituation bewog mich, schnell mal was Eigenes zu bauen. Mit dem typischen “wie schwer kann das schon sein” auf den Lippen begann ich während eines langweiligen Fußballspiels (es könnte der VfB Stuttgart involviert gewesen sein), schnell mal einen Hilfe-Viewer ähnlich des JavaHelp-Viewers zu bauen. Ungefähr 10 Stunden später hatte ich einen Viewer, der ein JavaHelp-HelpSet (.hs-Datei) parsen und sowohl TOC als auch Index eines solchen HelpSets erfolgreich darstellen. Es war also tatsächlich nicht so schwer, am längsten hat noch das XML-Gefrickel gedauert, weil ich mir in den Kopf gesetzt hatte, ganz ohne 3rd-Party-Dependencies auszukommen, und das DOM-Zeugs in Java SE ist halt sehr mit-der-Hand-am-Arm.

Code aufräumen, Icons malen und jetzt seit neuestem eine einfache Volltextsuche dazuzubauen hat nochmal ein paar Stunden gekostet, alles in allem schätze ich mal 3 PT an Gesamtaufwand. Swing ist wohl doch nicht so schlecht wie viele seit etwa 1999 nicht müde werden zu behaupten.

Das Ergebnis dieser Bemühungen kann man auf GitHub inspizieren. Und damit es wirklich von jedem auf welche Weise auch immer verwendet werden kann, habe ich als Lizenz die “Unlicense” ausgewählt. Ist bestimmt auch wieder inkompatibel mit irgendwas, aber es ist die freieste Lizenz, die ich gefunden habe.

Wird es für den SwingHelpViewer eine Weiterentwicklung geben? Natürlich liegen einige Erweiterungen auf der Hand (indexbasierte Suche z.B. über Apache Lucene, ein Bookmark-System, Unterstützung für weitere JavaHelp-Features wie “merged help sets” und Direkthilfe-Action und Hilfe-Cursor und so, Unterstützung für andere Hilfedaten wie CHM, HLP oder Eclipse Help, Einbindung anderer Anzeigekomponenten die mehr von HTML und CSS verstehen als JEditorPane), aber der jetzige Zustand dürfte für meine Zwecke wohl reichen.

Wer die Herausforderung sucht, könnte z.B. mal schnell eine Java 1.2-kompatible Version des Codes bauen. Im Moment läuft alles ab Java 7, wenn man den Diamond Operator rauswirft müsste Java 5 gehen, wenn man die Generics rauswirft Java 1.4, und wenn man das XML-Parsing anders löst (wer erinnert sich noch an crimson.jar…), dürfte man bei Java 1.2 angekommen sein. Der Code ist sehr “old-school”, und Swing ist ja im Prinzip seit Java 1.2 unverändert.

Was GitHub angeht, ist SwingHelpViewer mein erstes dort publiziertes Projekt. Wenn ich die jetzige Geschwindigkeit beibehalte, werden meine über die Jahre entstandenen programmiertechnischen Ergüsse spätestens bis 2050 komplett auf GitHub gelandet sein.

Stümpern mit Niveau

Ab und zu – die Frequenz ist uneinheitlich, die Rahmenbedingungen unbekannt – neige ich dazu, irgendein IT-Thema mal genauer zu betrachten und ein paar Experimente durchzuführen. Quasi um immer wieder das “innere Komplexitätsgefühl” neu zu justieren. Ich stelle eine gewisse Häufung beim Thema “Protokolle” fest, vielleicht die masochistische Ader die mich schon zur Implementierung einer CD-/DVD-/BluRay-Brenner-Software gebracht hat. Ziel ist immer, bei einer Sache von Quasi-Null-Wissen auf mindestens 3 zusätzliche Seiten im persönlichen Handbuch des nutzlosen Wissens zu kommen. Quasi qualifiziertes Halbwissen, das jede Fachdiskussion unter einer Stunde locker überlebt.

Diesmal das Thema: IPP, das “Internet Printing Protocol”, das im Rahmen einer Diskussion im Kontext meines Lieblingsbetriebssystems RISC OS eher zufällig aufgekommen ist. Früher dachte ich, IPP sei quasi nur “Druckdatenstrom per HTTP an den Drucker übermitteln”. Sowas wie “JetDirect über HTTP”. Oh Mann, lag ich falsch. Oder besser: es ist eine sehr simplifizierte Beschreibung der Wirklichkeit, und qualifizierte mich damit als “Quasi-Null-Wissenden”.

In Wahrheit hat IPP interessantes Potenzial für die RISC OS-Welt, nämlich “driverless printing”. Man kann per standardisierten Requests den Drucker zu seinen Eigenschaften befragen (Papierzufuhroptionen, Nachbearbeitungsoptionen, Duplex oder nicht und wenn ja wie), zu seinem Status (Tonerstand/Tintenstand, Papierstau etc.) und es gibt ein plattformübergreifendes Rasterformat (“PWG Raster Format” – PWG ist die “Printer Working Group”, die hinter IPP steht), das zum Druck verwendet wird. Zusätzlich vermelden netzwerkverbundene Drucker ihre Anwesenheit über das Bonjour-Protokoll, aber auch direkt verbundene USB-Drucker werden unterstützt. Das Gesamtkunstwerk nennt sich IPP Everywhere. Also die Lösung aller Probleme der Art “klar gibt es da diesen Drucker XYZ, aber für Betriebssystem ABC gibt es leider keine Treiber” – heute behilft man sich da (zumindest im RISC OS-Markt) mit der oberen Preisklasse der PostScript- und PDF-fähigen Drucker, muss aber immer noch tricksen um mit speziellen Druckerfeatures arbeiten zu können.

Die IT wäre nicht die IT, wenn nicht zwei konkurrierende andere Lösungen gleichzeitig um die Gunst des Anwenders (interessiert den das eigentlich?) buhlen würden: AirPrint (von Apples Gnaden) und Wi-Fi Direct Print. So weit, so ärgerlich. Zurück zu IPP. Dessen Charme ist die Verfügbarkeit eines gut dokumentierten Standards, bei den beiden Konkurrenten sieht es da nicht ganz so gut aus, dafür waren diese früher am Markt und sind (deshalb?) deutlich weiter verbreitet – IPP Everywhere wird heute eigentlich nur von einer Reihe von HP-Druckern unterstützt.

Wie auch immer, ich fasste ein Ziel ins Auge: mit meinem IPP-fähigen (aber natürlich nicht IPP Everywhere-kompatiblen, weil Baujahr 2011) Oki C530 ein PDF zu drucken, indem über IPP der PDF-Datenstrom an den Drucker geschickt wird.

Wie anfangen? Der IPP-Guide erzählt von 40 Spezifikationen mit insgesamt fast 2000 Seiten, das schien jetzt etwas ambitioniert. Aber es gibt JIPP, eine Java-Implementierung von IPP von HP, verfügbar auf GitHub. Unter der guten MIT-Lizenz. Da waren Beispiele dabei. Wie schwer kann es also sein?

Erste Hürde: das Github-Projekt hostet nur Source-Releases. Kein Problem, man kann ja selber bauen. Stellt sich raus: ist Kotlin-Code, gebaut mit Gradle. Hm. Bin ich jetzt für beide Dinge nicht so der Experte, vielleicht ein Experiment für ein anderes Mal. Google führt mich zu einer sinnvollen Paketierung des Codes inklusive aller seiner Abhängigkeiten (wie der Kotlin-Runtime – auch nicht gerade schmal das Dingens). Also schnell ein IDE-Projekt aufgesetzt und los geht’s. Ich arbeite mit Version 0.6.16 – nur falls jemand ein paar Jahre später über diesen Artikel stolpert und bis hierher durchgehalten hat.

Das jprint-Beispiel (inzwischen übrigens – ohne meinen Input und erst vor wenigen Stunden! – signifikant verbessert, an einigen der Stolperfallen) erst mal als Basis genommen. Ein PDF an die bekannte IPP-URL des Druckers geschickt. Eine dubiose Exception war das Ergebnis (aus dem HTTP-OutputStream: java.io.IOException: Error writing request body to server) – wies darauf hin, das irgendwas mit der URL nicht stimmte. OK. Ein paar Varianten durchprobiert (offenbar gibt es typische Implementierungen der Form ipp://irgendneip/ipp und ipp://irgendneip/ipp/lp und http://irgendeineip/ipp und http://irgendeineip:631/ipp – an dieser Stelle die Nebenerkenntnis, dass IPP schlicht HTTP-mit-631-als-Default-Port ist). Manchmal den gleichen Fehler, manchmal einen anderen Fehler (java.net.SocketException: Software caused connection abort: recv failed). Dubios. Weiter rumprobiert. Stellt sich raus: auch bei derselben URL kommt manchmal die eine Exception, manchmal die andere. Aha. Dann doch kein Connection-/URL-Problem? Mal den Code genauer anschauen. Als Transportmechanismus wird eine Klasse namens “IppPacket” verwendet. Da gibt es einen alternativen Konstruktor, der auch eine “ippVersion” (ein int wie es sich gehört) entgegen nimmt. Könnte es sein, dass mein Drucker (aus der Zeit von IPP v1.1) mit dem Default von JIPP (IPP v2.x) einfach nicht klarkommt? Mehrfaches Ausführen des Codes mit stets derselben URL führt tatsächlich manchmal zu einer Exception-losen Verarbeitung des Kommandos mit der Fehler-Rückmeldung “server-error-version-not-supported”, was den Verdacht erhärtet.

Etwas später ist klar: dummerweise erlaubt der Konstruktor von IppPacket, der eine explizite Versionsangabe erlaubt, nicht dieselben Typen an Restparametern, insbesondere nicht den “Operation”-Enum, sondern auch nur einen int. OK, ein paar RFCs zum IPP gelesen und herausgefunden, dass das Kommando getPrinterAttributes 0x0B ist und printJob 0x02. Minuten später sehe ich, dass der Enum innen drin die ints eh auch definiert hat. Grmpf. Ist bestimmt so ein Kotlin-Automatismus. Bleibt die Frage: wie spezifiziert man jetzt in einem int eigentlich “1.1” als Version? Na per 8+8 Bit natürlich. Also 0x0101 für meine Zwecke. Prima. Erstmal getPrinterAttributes probieren statt gleich das PDF zu schicken. Das Kommando wird jetzt zuverlässig verschickt, aber JIPP kann das Resultat nicht unfallfrei parsen – ob das eine Schwäche von JIPP ist oder eine Abweichung von der Spezifikation seitens meines Druckers ist noch ungeklärt, aber Gott sei Dank hat der JIPP-Programmierer bei solchen Parse-Fehlern daran gedacht, die Rohdaten des Replys, der nicht parsebar waren, herauszudumpen. Also konnte ich einfach inspizieren, was mein Drucker so als Fähigkeiten zurückmeldet.

Nachdem das Kommando-Schicken nun völlig problemlos funktionierte, machte ich mich an die eigentliche Aufgabe, den Druck eines PDFs – der Oki unterstützt sowohl direktes Drucken per PostScript als auch per PDF, und PDFs finden sich auf der Platte entschieden leichter. Ein einseitiges Dokument identifiziert und per offiziellem PDF-MIME-Type “application/pdf” zum Drucker geschickt. Kommt zurück: “client-error-attributes-or-values-not-supported”. Eine sprechende Fehlermeldung! Sehr schön. Inspektion der bei getPrinterAttributes zurückgemeldeten Fähigkeiten zeigt: “application/pdf” wird auch gar nicht akzeptiert, nur “application/octet-stream”, “application/vnd.hp-PCL” oder “application/postscript”. Einfach mal “application/octet-stream” geraten, und siehe da: es wird gedruckt.

Na das war ja einfach.

Zu klärende offene Fragen: 1. Nützt mir das jetzt was für IPP unter RISC OS? 2. Wie erzeuge ich möglichst einfach das PWG-Raster-Format aus einem beliebigen Quellformat? 3. Warum drucke ich nicht einfach PDF über einen CUPS-Druckerserver, der ja auch IPP out-of-the-box kann?

Antworten: 1. Noch nicht. 2. Sieht nicht so schwierig aus, auch keine ultrakomplexe Kompression, sondern simples PackedBits-Format und anständig dokumentiert. 2a. Aber auch JPEG wird als Format im Standard zwingend vorausgesetzt – noch einfacher! 3. Weil das ja jeder kann.

Interessanter Beifang: AppImage als Paketiervariante für Linux-Anwendungen. Spannend, kannte ich noch nicht.

Graal(VM) – Versuch eines Überblicks

Die Themen Graal bzw. GraalVM beschäftigen mich schon eine ganze Weile, und wie ich erneut auf dem Java Forum Stuttgart 2019 bemerkt habe, hat das Graal-Universum derart viele Facetten, die sich zudem alle in einem unterschiedlichen Stand der Nützlichkeit und Nutzbarkeit befinden, so dass ich hier mal versuchen werde, einen Überblick zu geben.

Das Graal-Universum (auch gerne “GraalVM ecosystem” genannt) ist ein Oracle-Projekt, hervorgegangen aus zahlreichen Forschungsprojekten der Oracle Labs in Kooperation mit der Uni Linz – früher hieß das Projekt “Metropolis” (und noch früher Maxine VM) und hatte ursprünglich neben Forschung angeblich nur das Ziel, einen JIT-Compiler für die Hotspot-JVM in Java neu zu schreiben, weil vor allem der in C++ geschriebene “C2” (auch als “HotSpot-Compiler der Server-JVM” bekannt) das Ende seiner Erweiterbarkeit und Wartbarkeit erreicht hatte. Inzwischen ist sehr viel mehr daraus geworden.

Übrigens steht “Graal” für “General Recursive Applicative and Algorithmic Language”. Was ein Glück, dass niemand auf die daraus eigentlich resultierende Schreibweise “GRAAL” besteht. Ein weiterer wichtiger Hinweis: das Wording wurde über die Zeit verändert, bitte nicht verwirren lassen von Graal vs. GraalVM vs. Graal-Compiler vs. Graal-JIT. Derzeit scheint man sich auf “GraalVM” für das Gesamtpaket entschieden zu haben, mit “Graal” als Kurzform, wenn man drüber spricht.

Die (JVM-)Welt vor GraalVM

Viel wurde geschrieben zur JVM, zu Bytecode, zu Interpretern, zu JIT-Compilern und der dynamischen Recompilierung à la HotSpot. Vor GraalVM war die HotSpot-JVM von Sun/Oracle die übliche Laufzeitumgebung für Bytecode aller Art (Dinge wie Excelsior JET, IBM/Eclipse J9, Azul Zing oder sogar gcj aus früheren Zeiten waren und sind ja eher Exoten). Die HotSpot-JVM hat im Prinzip drei Möglichkeiten, um Bytecode zu behandeln: entweder er wird interpretiert, oder er wird mit dem C1-Compiler (auch “client compiler” genannt) in Maschinencode übersetzt (das geht schnell, ist aber wenig optimierter Code), oder er wird mit dem C2-Compiler (auch “server compiler” genannt) in Maschinencode übersetzt (das dauert etwas länger, ist aber sehr gut optimiert, und kann zur Laufzeit, wenn eine Methode beispielsweise sehr häufig aufgerufen wird, weiter optimiert werden – das ist der “HotSpot”-Aspekt der JVM). Das Zusammenspiel aus C1-C2 wird oft auch “tiered compilation” genannt.

Nun ist das C++-Duo C1-C2 schon gut 20 Jahre alt und nach verschiedenen Aussagen diverser Beteiligter am Ende von Weiterentwicklung und Wartbarkeit angekommen. C2 wurde in den letzten Jahren nur noch mit spitzen Fingern angefasst, es wurden keinerlei Verbesserungen an der Architektur oder der prinzipiellen Optimierungsmöglichkeiten vorgenommen, nur Bugfixing und diverse Optimierungen im Bereich Intrinsics (also das Erkennen spezifischer Codepatterns und Verwendung von optimiertem Maschinencode um diese direkt zu ersetzen), was aber als Backend-Optimierung für jede unterstützte Prozessorarchitektur separat gemacht werden musste (oder der Einfachheit halber gleich unterblieb).

Man könnte argumentieren, dass das (also Stabilität und wenige Änderungen) auch gut so ist: durch die Ausführung als native code in der JVM besteht schließlich immer die Gefahr, bei einem unerwarteten Fehler die komplette JVM mit einem Coredump nach einem Segmentation Fault herunterzureißen. Und schließlich muss man ja zugeben, dass der C2 insbesondere Java-Bytecode in wirklich effizienten Maschinencode überführt, der für viele Anwendungen den Performancevergleich mit statisch compilierten Sprachen wie C oder C++ nicht zu scheuen braucht, und teilweise aufgrund seiner dynamischen Natur auf Basis von Informationen, die erst zur Laufzeit zur Verfügung stehen, diese sogar performancetechnisch übertreffen kann.

Diese uns allen bekannte JVM-Welt hat aber natürlich auch bekannte Schwächen, so dass das Festhalten am Status Quo kaum als eine valide Vorgehensweise für die Zukunft erscheint. Da ist zum einen der nicht zu vernachlässigende Performancenachteil beim Startup einer Anwendung – es dauert seine Zeit, bis die entscheidenden “heißen” Codepfade durch C2 entsprechend optimiert wurden, ein Prozess, der oft als “heating up the JVM” oder ähnlich bezeichnet wird. Und während dieser Zeit genehmigt sich die JVM ja durchaus auch erkleckliche Mengen an Speicher, und der Garbage Collector darf regelmäßig aufräumen. So schön das Prinzip einer virtuellen Maschine auch sein mag, in gewissen Anwendungsszenarien wie z.B. Desktop-Anwendungen oder Cloud-Services sind die Nachteile unübersehbar.

Dazu kommt, dass sich mit der Zeit herausgestellt hat, dass die Techniken des C2 zu nah an Java und der Art und Weise, wie javac Bytecode erzeugt, gebaut sind. Das führt zu suboptimalen Ergebnissen bei der Ausführung von Bytecode aus anderen Quellen, sei es Scala, JRuby, Jython oder Clojure.

Und dann ist da natürlich das Problem des “Systembruchs” bei der Weiterentwicklung des JITs – man muss immer zwei Welten verstehen, entwickeln, analysieren und debuggen – die native C++-Welt und die Java-Welt. Qualifiziertes Personal, das in beiden Welten zuhause ist, ist naturgemäß schwer zu finden.

Jedenfalls kann man nicht wegdiskutieren, dass das Großthema Performance in den Teilbereichen “startup” und “footprint” mit der bisherigen Strategie kaum ausreichend gelöst werden kann, vor allem wenn im Serverbereich nicht mehr länger große, langlaufende Prozesse das übliche Architekturmuster sind, sondern stattdessen kurzlaufende, containerisierte, über ihre schiere Anzahl skalierende Microservices. Bisher wurden diese Themen nur im Rahmen des gewohnten Umfelds angegangen – Class Sharing, Modularisierung des JDK, AOT seit Java 9. Mit begrenztem Erfolg.

Graal der JIT-Compiler aka “GraalVM compiler”

Angeblich beginnt die Geschichte von Graal(VM) etwa 2009 mit einer Portierung von C1/C2 nach Java durch Thomas Würthinger. Heute kann man den Graal-JIT-Compiler recht einfach in JDK 8 oder JDK 11 integrieren, kann also von jedem Java-Entwickler sehr einfach ausprobiert werden. So kann man in Ruhe für seinen spezifischen Workload herausfinden, ob die fortgeschrittenen Optimierungstechniken tatsächlich einen Laufzeitvorteil bedeuten, und wie groß dieser gegebenenfalls ausfällt.

Vor geraumer Zeit (Java 8, zunächst aber nicht öffentlich dokumentiert, offiziell dabei seit Java 11) wurde das JVMCI (JVM Compiler Interface, JEP 243) eingeführt, eine (hoffentlich langzeit-)stabile Schnittstelle der JVM zum Andocken alternativer JIT-Compiler. Graal kann genau diese Schnittstelle nutzen.

Wo bietet Graal nun Vorteile gegenüber dem bisherigen C2? Insbesondere die sogenannte “Escape Analysis” scheint sehr viel leistungsfähiger zu sein. D.h. der Compiler ist viel besser beim Erkennen von Objekten, deren Gültigkeitszeit z.B. auf Methodenebene beschränkt ist und deshalb problemlos auf dem Stack statt auf dem Heap allokiert werden kann. Das hat doppelten Nutzen, denn die Stack-Allokation ist schneller und der GC muss hinterher nicht aufräumen. Besonders bei Scala-Code scheint diese Verbesserung für erhebliche Performanceverbesserungen zu sorgen. Aber auch das Inlining von Code soll mit Graal besser funktionieren.

Die Tatsache, dass Graal in Java geschrieben ist, hat interessante Konsequenzen. Zum einen wird nun der JIT-Compiler selbst in der JVM ausgeführt und optimiert sich so mit der Zeit selbst. Speicherallokation für den Compiler findet auf dem JVM-Heap statt anstelle aus dem Native-Pool zu kommen. Da nun auch die Performance des Compilers einer Warm-Up-Zeit unterliegt, verändert sich auch das Performance-Profil zur Ausführungszeit. Und der GC ist natürlich potenziell stärker beschäftigt, weil er sich nun sowohl mit der eigentlichen Anwendung als auch mit dem Compiler beschäftigen muss.

Um dieses Szenario zu nutzen, kann man einfach die GraalVM oder einen der OpenJDK-Builds vom GraalVM-Team nutzen und entsprechend konfigurieren. Im Prinzip ein “drop-in replacement” für das gewohnte JDK.

Und was ist libgraal?

libgraal ist der Versuch, den Graal-JIT-Compiler mittels GraalVM-native-image (dazu später mehr) in eine Bibliothek zu überführen, die das Performance-Problem des Compile-Vorgangs des Graal-JIT-Compilers in der JVM lösen soll. Da schon alles in nativem Code vorliegt, muss sich der JIT-Compiler nur noch um den Anwendungscode kümmern und nicht mehr um sich selbst. Im Prinzip wird dadurch Graal zu einem – was das Laufzeitverhalten angeht – sehr ähnlichen JIT-Compiler wie der klassische C1 und C2 in der HotSpot-JVM. Langsame JIT-Compilierung während der JVM-Warm-Up-Phase wird dadurch verhindert.

Die aktuellen Versionen von GraalVM sowie die GraalVM-OpenJDK-Builds enthalten allesamt libgraal out-of-the-box.

GraalVM und die AOT-Compilierung

Seit Java 9 gibt es im JDK ein als experimentell markiertes Tool namens jaotc, das (eine recht frühe Version von) Graal als Backend verwendet. Die Implementierung erfolgte im Rahmen von JEP 295, dort kann man viele der Einschränkungen nachlesen. Im Prinzip gibt man jaotc vor, welcher Code vorcompiliert werden soll, und es wird ein Binär-Blob erzeugt, der dann den sonst durch den JIT-Compiler erzeugten Maschinencode enthält. Man spart also die Zeit, die der JIT-Compiler sonst zur Laufzeit braucht, um den Code zu compilieren.

Es gibt bei jaotc jede Menge Einschränkungen, die den Einsatz in der Praxis wirkungsvoll verhindert haben – zumindest in der Breite. Immerhin gab es bei Java 10 einige Bemühungen, um diverse gravierende Einschränkungen wie “funktioniert nur unter Linux-x64” aufzuheben. GraalVM geht heute aber noch einen großen Schritt weiter mit “native-image”.

GraalVM und das “Native Image”-Feature

Um Memory-Footprint und Startup-Zeit zu minimieren, unterstützt GraalVM die Möglichkeit, ein “Native Image” zu erzeugen. Simpel gesagt: ein standalone ausführbares Programm, das keine separate JVM benötigt. Die bisherigen Releaseversionen von GraalVM inklusive der derzeit aktuellen 19.1.0 erlauben die Erstellung eines solchen “Native Images” für Linux und macOS. Dazu wird neben der GraalVM-Distribution auch noch eine native Toolchain für das Betriebssystem benötigt, für Linux beispielsweise gcc nebst Bibliotheken wie glibc und zlib. Für die noch sehr experimentelle Windows-Unterstützung wird eine Installation von “Windows SDK for Windows 7” benötigt.

Es gibt ein paar Dinge neu zu durchdenken, wenn man ein “Native Image” erzeugt. Beispielsweise die Frage, was eigentlich schon zur Compilezeit aufgelöst wird und was erst zur Laufzeit. Stichwort “Initialisierung statischer Member”. Man überlege sich, inwieweit sich die Semantik des Programms ändert, wenn in einem static initializer eine Variable mit “new Date()” belegt wird und dies einmal zur Compilezeit passiert. Für solche Fälle werden Compile-Time-Switches angeboten.

Prinzipbedingt gibt es einige Limitierungen beim native-image-Feature. Wichtig ist die “closed-world assumption”. GraalVM muss bei der statischen Compilierung schließlich entscheiden können, welche Klassen für das Image eingebunden werden müssen. Das schließt übliche Java-Mittel wie dynamisches Nachladen von Klassen zur Laufzeit aus, deren Notwendigkeit auch erst zur Laufzeit auftaucht – typische Service-Lookup-Konstruktionen, beispielsweise im OSGi-Umfeld, gehören zu diesem Problemkreis. Ein Feature, um über einen Tracing Agent auch zur Laufzeit dynamisch nachgeladene Klassen sicher zu identifizieren, befindet sich derzeit in der Entwicklung. Aber auch das würde natürlich vollständig dynamisches Nachladen nur dann erfassen, wenn alle entscheidenden Codepfade auch tatsächlich durchlaufen werden, während der Tracing Agent aktiv ist.

Erstaunlicherweise ist trotzdem partieller Support für Reflection verfügbar, was uns zum nächsten Teilthema führt…

GraalVM und die Substrate VM

Auch nativer Code braucht natürlich, wenn die Semantik eines Java-Programms möglichst unangetastet bleiben soll, eine gewisse Laufzeitumgebung. Die wird im Falle eines GraalVM-erzeugten “Native Image” durch die eingebettete Substrate VM zur Verfügung gestellt. Dazu gehören Runtime-Routinen und der Garbage Collector. Die Komponenten der Substrate VM sind in Java geschrieben.

GraalVM und Truffle

Bisher waren die Punkte sehr Java-spezifisch, oder genauer gesagt Bytecode-spezifisch – alles problemlos nutzbar für das JVM-Bytecode-Ökosystem, also neben Java auch Kotlin, Scala, Groovy, Clojure und Konsorten. So weit, so unspektakulär. Andere Programmiersprachen haben häufig Bytecode-spezifische Forks als Implementierung wie Jython oder JRuby, und das JDK hatte bekanntlich seinen eigenen JavaScript-Interpreter intus namens Rhino (ab Java 6) und Nashorn (ab Java 8).

Aber GraalVM bietet mehr. Das Truffle-Framework erlaubt die einfache Implementierung von Interpretern, die dann vollautomatisch zu einem Bestandteil der GraalVM werden können – quasi eine automatische Generierung eines Compilers aus einem Interpreter.

Interpreter für Sprachen wie JavaScript, Ruby, R und Python sind bereits heute in verschiedenen Stadien der Fertigstellung bereit zu Experimenten. Die JavaScript-Implementierung scheint am weitesten fortgeschritten und umfasst bereits den Sprachumfang von ECMAScript 2019. Das war übrigens das Hauptproblem mit den Java-internen JavaScript-Lösungen – sie konnten mit der schnellen Weiterentwicklung von ECMAScript nicht mithalten und waren schnell veraltet und schwer wartbar.

Eine gute Zusammenfassung in wenigen Worten: “Truffle makes it easy, Graal makes it fast.” Wer schon mal versucht hat, für eine einfache Sprache einen Interpreter zu entwickeln, weiß, dass das durchaus machbar ist. Ein optimierter Interpreter ist schon schwieriger. Aber einen gescheit optimierenden Compiler dafür zu schreibe, ist sauschwer. Soll der auch noch mit in anderen Sprachen geschriebenem Code kommunizieren, wird es langsam wirklich komplex. Das GraalVM-Ökosystem verspricht hier eine einfach handhabbare und trotzdem sehr leistungsfähige und performante Lösung.

GraalVM als polyglotte VM

Nun wäre es ja schon ein echter Fortschritt, die GraalVM als universelle Laufzeitumgebung für allerlei Sprachen zur Verfügung zu haben. Beispielsweise könnte man sich für die gängigen Interpreter-Sprachen wie eben Ruby oder Python oder auch Lua die zeitaufwändige Entwicklung einer effizienten Laufzeitumgebung schlicht sparen – warum immer wieder neu einen JIT-Compiler implementieren, wenn es in GraalVM schon einen gibt? Aber wenn das nun schon möglich ist, wie wäre es denn dann, wenn man die Sprachen beliebig mischen könnte?

Das wäre natürlich phantastisch, und GraalVM bietet genau das. Es existieren Schnittstellen, um die Sprachen kontrolliert miteinander verbinden zu können. Und zwar, weil alle in demselben VM-Kontext laufen, quasi ohne zusätzlichen Overhead. So werden Java-Objekte für JavaScript-Skripte zugreifbar, Methoden aufrufbar, das volle Programm. Die technische Magie dazu steckt im “polyglot interoperability protocol” des Truffle-Frameworks, das genau diese Dinge standardisiert und damit diese Interoperabilität automatisch sicherstellt.

Der alte Traum von pick-and-mix bezüglich unterschiedlicher Programmiersprachen mit ihren spezifischen Stärken und leistungsfähigen Bibliotheken, hier wird er Wirklichkeit.

Wirklich cool ist beispielsweise eine für GraalVM erweiterte Variante von VisualVM. Man kann damit den laufenden Code in Ruby, R oder JavaScript genau untersuchen hinsichtlich der Threads und der Objekte auf dem Heap.

Und was ist Sulong?

Sulong ist eine weitere Schicht der GraalVM-Welt – ein High-Performance-Interpreter für LLVM Bitcode. Das erlaubt es der GraalVM, Programmcode, der in diversen Sprachen auf Basis der LLVM-Compiler-Infrastruktur erzeugt wird (besonders prominent: C, C++, Objective-C, Swift und Rust), ebenfalls auszuführen. Mit voller Interoperabilität mit allen anderen unterstützten Sprachen des GraalVM-Universums.

Ich erkenne hier eine leichte Ironie, schließlich hat Azul Systems für ihre Zing JVM einen neuen JIT-Compiler namens “Falcon” entwickelt, der auf der LLVM-Compiler-Infrastruktur basiert. Quasi genau andersrum.

Community Edition vs. Enterprise Edition

Oracle ist nicht gerade bekannt dafür, Projekte aus reiner Menschenfreundlichkeit kostenlos unter die Leute zu bringen. Demzufolge strebt man auch im Graal-Universum einen RoI an, und der heißt “GraalVM Enterprise Edition”. Es gibt einige Optimierungen, die nur in der Enterprise-Variante zur Verfügung stehen, Stand heute scheint das vor allem die Autovektorisierung zu sein. Man kann die Enterprise-Variante jederzeit zu Evaluierungszwecken herunterladen und so prüfen, ob das beim eigenen Workload etwas bringt – die Evaluierungsversion darf jedoch nicht in Produktion verwendet werden.

Ob und wann Optimierungen von der Enterprise-Version in die Community-Version wandern, muss abgewartet werden. Oracle wird sicherlich anstreben, immer einen Respektabstand zwischen den beiden Versionen zu wahren und vor allem Optimierungen für die Großen der Branche möglicherweise exklusiv für die Enterprise Edition reservieren. Letztlich muss der Performance-Gewinn durch die Enterprise-Edition ja im realen Leben gegen eingesparte Kosten z.B. im Cloud-Umfeld gerechnet werden – der Business-Case muss möglichst leicht nachvollziehbar sein, wenn man Lizenzen verkaufen will.

Bezüglich der Lizenzsituation bleibt festzuhalten, dass praktisch alle GraalVM-Komponenten unter GPL2 mit Classpath-Exception stehen.

Fazit, Status Quo, Ausblick, Glaskugel

Das Potenzial ist gewaltig – viele Schwächen des Java-Ökosystems können mit GraalVM gelöst oder zumindest gemildert werden. Dazu kommt mit der Idee der polyglotten VM der alte Traum der völligen Austauschbarkeit von Programmiersprachen und Bibliotheken in greifbare Nähe. Mit der “Native Image”-Möglichkeit wird Java auch wieder (oder zum ersten Mal?) eine sehr gute Lösung für klassische Apps und Anwendungen neben den unbestreitbaren Vorteilen für servserseitig kurzlaufende Services in einem containerisierten Cloudumfeld.

Was hält uns davon ab, genau jetzt vollständig in die GraalVM-Welt einzutauchen? Aus meiner Sicht ist das GraalVM-Ökosystem im Moment in einem uneinheitlichen Zustand. Während das Szenario “GraalVM als JIT-Plugin für die HotSpot-JVM” stabil ist, ist der Rest eher von wechselhafter Stabilität. Vor allem das Feature “native image” scheint im Moment eher limitiert und zurecht als “Early Adopter technology” gekennzeichnet, nicht zuletzt durch die Beschränkung auf Linux und macOS – Windows ist für diesen Anwendungsfall als “experimental” markiert. Wenn man sich die GitHub-Issues zu diesem Thema durchliest, wird klar, dass da noch eine Menge Feinarbeit zu leisten ist. AOT hingegen scheint weitgehend stabil zu sein, ist es doch seit Java 9 Teil des JDK.

Was die Polyglot-Fähigkeiten angeht, scheint die JavaScript-Implementierung recht stabil und zudem vollständig zu sein – Node.js ist ein sehr wichtiges Zielszenario, und man braucht durch die Abkündigung der Nashorn-Engine in Java 11 auch beizeiten einen Ersatz dafür. Bei Ruby, R, Python und LLVM-Bitcode steht eher noch ein Fragezeichen bezüglich Vollständigkeit, Performance und Stabilität. Vom technischen Standpunkt aus ist eine Out-of-the-box-Lauffähigkeit von realen Python-Programmen natürlich auch deutlich schwerer zu bewerkstelligen als bei JavaScript, weil das Python-Ökosystem bei vielgenutzten Bibliotheken voller native code zwecks besserer Performance steckt. NumPy und SciPy seien hier als Beispiele genannt. Bei R ist es ähnlich, wo die performance-relevanten Berechnungen oft in nativem Fortran-basierten Code passiert.

Angesichts der kürzlich erfolgten Abkündigung von Nashorn, der JavaScript-Engine im JDK, dürfte eine stabile und performante JavaScript-Fähigkeit ganz oben auf der Prioritätenliste zu sein. Außerdem scheint GraalVM als Ausführungsplattform für das Node.js-Universum auch attraktiv zu sein, vor allem natürlich wegen der einfachen und performanten Interoperabilität zwischen Java und JavaScript.

Die Native-Image-Fähigkeit ist wie gemacht für Microservices, und so gibt es bereits drei seriöse Frameworks, die GraalVM zu diesem Zweck nutzen: Quarkus, Micronaut und Helidon. Die Benchmarks bezüglich Memory-Footprint und Startup-Zeiten sehen schon beeindruckend aus.

Sehr interessant ist die Möglichkeit, die Native-Image-Fähigkeit für Plattformen zu verwenden, wo keine virtuelle Maschine zulässig ist – OK, “Plattformen” ist hier meines Wissens zu Unrecht die Mehrzahl, denn ich kenne nur iOS, das unter dieser willkürlichen Apple-Entscheidung zu leiden hat. Hier ein Artikel, der die Möglichkeiten beleuchtet. Und zurecht anmerkt, dass es schon viele vergebliche Versuche gab, iOS für das Java-Ökosystem einfach zugänglich zu machen.

Nicht zuletzt wäre die Native-Image-Fähigkeit für die wenigen verbliebenen Java-Entwickler, die Desktop-Anwendungen schreiben, eine schöne Sache. Eine einfache exe hat bei der Distribution von Software einfach Vorteile gegenüber dem Bundling einer kompletten Java-Runtime-Umgebung, möglicherweise nebst Einsatz von Obfuscator und exe-Wrapper.

Ein weiterer hier nicht weiter beleuchteter Anwendungszweck ist das Embedden von GraalVM-Technologie in andere Plattformen. Es werden die Oracle-Datenbank und MySQL derzeit genannt, mit diesem Anwendungsfall habe ich mich nicht näher beschäftigt.

Die Entwicklung schreitet im Moment zügig voran – wer am Ball bleiben will, sollte regelmäßig die Release Notes für neue Versionen durchlesen, man bekommt dann ein ganz gutes Gefühl für die Reife von manchem Szenario.

In der optimal vergraalten Welt der Zukunft wäre es ein Leichtes, mit dem Truffle-Framework seine persönliche Lieblingssprache in verhältnismäßig kurzer Zeit aus dem Boden zu stampfen und sofort eine komplette Toolchain inklusive hochoptimierender VM und nativen Code erzeugendem Compiler zur Verfügung zu haben. Inklusive voller Interoperabilität mit den Ökosystemen aller relevanten Programmiersprachen von Java über C++ bis Python. Die Grundbausteine dafür sind vorhanden und durchaus schon von hoher Qualität, aber es fehlt noch reichlich Feinschliff. Ich fürchte aber, dass die Lösung des Gesamtproblems der 80-20-Regel folgt. Wenn nicht gar 90-10.

Aber es wäre in einem ausgereiften Endzustand schon nahe am heiligen Gra(a)l. Ich hoffe, man verzettelt sich nicht in der Vielfalt der Möglichkeiten. Jedenfalls wäre Manpower in der GraalVM-Ecke m.E. deutlich besser aufgehoben als bei mancher heißdiskutierter Spracherweiterung seit Java 9. Ich sage mal “Text Blocks”. “Type Inference”. Private Methoden in Interfaces. Oder JDK-Erweiterungen wie jshell oder Epsilon GC, deren überragende Nützlichkeit sich auf den ersten und auch zweiten Blick nicht direkt erschließt.

Andere Quellen

Erster Anlaufpunkt ist natürlich die GraalVM-Homepage. Sehr empfehlenswert sind YouTube-Videos von Thomas Würthinger und Christian Thalinger (als früherer Entwickler des C2 natürlich spezialisiert auf den GraalVM-JIT aka GraalVM compiler, der früher verwirrenderweise Graal JIT oder Graal compiler hieß).

Wer ganz tief einsteigen will – die Sourcen werden auf GitHub gehostet.

Anlass für diesen Blog-Eintrag war ein Übersichtsartikel auf Heise Developer, der es doch etwas an Tiefe vermissen lies und durch Vermischung der unterschiedlichen Aspekte beim Uneingeweihten möglicherweise für Verwirrung sorgt. Ich überlasse es dem geneigten Leser zu entscheiden, ob mein Artikel eventuell noch wirrer und verwirrender ist.

Java Forum Stuttgart 2019

Über meine Gründe, das Java Forum Stuttgart zu besuchen, habe ich bereits 2017 was geschrieben, und daran hat sich nichts geändert. 2018 habe ich aufgrund wenig vielversprechender Themenauswahl mal sausen lassen, also war es 2019 mal wieder an der Zeit.

Die Rahmenbedingungen waren dieselben: Liederhalle, top organisiert, Verpflegung OK (auch wenn ich wohl nie verstehen werde, warum jemand Coke Light anbietet statt Coke Zero, aber über Geschmack kann man nicht streiten – vielleicht war es ja wenigstens billiger), abwechslungsreiches Vortragsprogramm. Und ein Code of Conduct scheint heutzutage wohl unvermeidlich, natürlich in gendergerechter Sprache anstatt in richtigem Deutsch. Man könnte den ganzen Sermon inhaltlich auch schneller auf den Punkt bringen: “Es gelten die Regeln der westlichen Zivilisation und des allgemeinen Anstands”. Oder “Verhalte Dich stets so, dass Deine Mutter auf Dich stolz wäre.”

Zum eigentlichen Thema. Gab es diesmal einen der typischen Themenschwerpunkte, den klassischen Hype, auf den sich alle stürzen und zu dem es zig Vorträge gibt? Immerhin vier Mal war Graal(VM) (Teil-)Thema, was m.E. zeigt, dass das Thema “Performance” insbesondere im Bereich Startup-Zeiten und Speicherverbrauch in der Java-Welt wie schon vor fast 25 Jahren topaktuell bleibt. Ansonsten scheint jeder – auch ohne Vorliegen zwingender Gründe – sein Heil in der Microservice-Welt – gerne auch in der Cloud – zu suchen. Der Mobile- und IoT-Hype schein hingegen etwas abgeflacht zu sein.

Zum Vortrag “Cross-Platform Mobile-Apps mit Java – 2019” von Dr. Daniel Thommes, Geschäftsführer der NeverNull GmbH. Ein Technologieüberblick, durchaus ins Detail gehend, zur ewig aktuellen Frage “write once, run anywhere, und bitte plattformübergreifend single source und an jede Plattform optimal angepasst”. Daran ist bekanntlich schon Swing gescheitert. Und die Zeichen stehen ehrlich gesagt nicht besonders gut, dass es für so divergierende Anforderungen wie “App auf iOS, App auf Android, und Desktop auf Windows und Linux” jemals eine einheitliche und schöne Lösung geben könnte. Aber der Stand der Dinge hat mich schon etwas erschreckt, denn die “Lösungen” wie Transpiler, portierte Runtimes oder halbgare eigene UI-Toolkits sind doch eher ein Armutszeugnis. Es war ein Non-Sponsored-Talk, was den Vortragenden erkennbar in Gewissensprobleme stürzte und er den Eindruck, die eigene Lösung “MobileUI” seiner Firma zu sehr in den Vordergrund zu stellen, gewissenhaft zu vermeiden suchte. Tut so einem Vortrag nicht gut. Wobei man die Basics durchaus gut beherrscht, wie die “Java Forum Stuttgart”-App – natürlich auf relativ niedrigem Komplexitätsniveau, aber eine Lösung, die die einfachen Sachen sehr gut und sicher beherrscht ist ja auch wertvoll – zeigt. Ebenfalls fehlte erkennbar Tiefe für die diversen altbekannten Fallstricke der Cross-Plattform-UI-Entwicklung. Wenn etwas leidlich Komplexes in Demos nicht angeschnitten wird, liegt das erfahrungsgemäß daran, dass es nicht anständig funktioniert. Aber das ist natürlich nur wilde Spekulation kraft meiner 20-jährigen Erfahrung mit UI-Toolkits aller Art.

Michael Wiedeking war – fast selbstverständlich – auch wieder dabei, diesmal mit “Der eilige Graal” mit einer gewohnt locker präsentierten Reise durch die wunderbare Welt von JIT-Compilern, Hotspots, Bytecode und natürlich Graal. Der Vortrag krankte etwas an der unscharfen Abgrenzung zwischen Graal, GraalVM, Graal-JIT, Graal AOT und Graal native image-Feature. Für den Uneingeweihten ging es da manchmal zu sehr durcheinander, dank ausreichender Vorbildung konnte ich jedoch folgen. Aber selbst die Graal-Erfinder selbst tun sich da oft schwer, eben weil das Graal-Universum so viele Zielrichtungen verfolgt. Wie man das in 45 Minuten überhaupt vorstellen könnte – ich weiß es auch nicht.

Auch Stephan Frind versuchte sich unter dem Titel “Graal oder nicht Graal –ist das wirklich eine Frage?” an diesem Themenkomplex. Deutlich high-leveliger als Michael Wiedeking, und deshalb erfolgreicher beim Vermitteln des allgemeinen Überblicks. Leider aber dadurch eben auch fehlende Tiefe. Die Compilerbau-Details und die C1-C2-Abgrenzung in der JVM waren nicht immer ganz korrekt, aber das tat nicht weiter weh. Details, die nur einem passionierten i-Dipfeles-Scheißer auffallen. Die Abgrenzung Graal vs. GraalVM war auch hier eher unscharf, Schwächen der derzeitigen native image-Lösung kamen etwas zu kurz. Am Ende der Hinweis “Testen! GraalVM etwas schwieriger” könnte in der Kategorie “Untertreibung des Jahres” durchaus weit vorne landen. Man schaue sich die diversen Issues zum Thema native image auf GitHub an, um ein Gefühl für den präzisen Stand der Dinge bezüglich Stabilität und Umfang zu bekommen.

Aufgrund sparsamer Konkurrenz im Zeitslot verschlug es mich auch in “Aus eins mach zehn: Neuentwicklung mit Microservices” von Lars Alvincz und Bastian Feigl (andrena objects ag). Im Abstract war schon angekündigt, dass auch “Null vermeiden” ein Thema sein sollte, und das störte doch gewaltig – wie kann man etwa 10 Minuten der wertvollen Zeit auf so eine Nichtigkeit verschwenden, wo es doch um das Thema “wir lösen ein monolithisches System durch Microservices ab” ging? Mit all seinen interessanten Fragestellungen von “sind Microservices der richtige Ansatz” über “was hat die Neuentwicklung veranlasst” bis zu “das war der Ressourcenbedarf an CPU und Speicher vorher und nachher”. Neben den Klassikern “wie schneide ich die Services richtig” und “wie schaffe ich es, transaktionales DB-Verhalten mit dem Microservice-Umfeld zu verheiraten” natürlich. Es wurde nicht mal ansatzweise versucht, hier mehr als ein paar Worte zu den interessanten Teilgebieten zu verlieren. Besonders ärgerlich, weil die dargestellten Anforderungen von Kundenseite keineswegs eine Microservice-Architektur nahelegten oder gar erzwangen. Warum bei einer eher trivialen Standardanwendung, zudem mit zwingender Datenmigration und klarer Vorgabe an Funktionalität ein “Big Up Front-Design nicht sinnvoll/möglich” gewesen sein soll – behauptet wurde es, aber begründet nicht. Zusätzlich fiel mir wieder auf, dass die Lösung mit zwei Vortragenden nur ganz selten wirklich gut funktioniert. Auch die Themen “Versionsverwaltung des Codes” und “Trunk-based development vs. Feature Branches” wurde kurz gestreift, ohne dass klar wurde, warum das in diesem Kontext unbedingt Erwähnung finden musste. Insgesamt kein überzeugender Vortrag. In keinem der behandelten Unterthemen.

Da mein tägliches Brot immer noch Java 8 ist (böse Zungen würden behaupten, dass man in Java 8 natürlich immer noch genauso wie in Java 1.4 programmieren kann und schon die Einführung von Generics ein Fehler war) und ich die Neuerungen aus Java 9 bis Java 13 zwar kenne, aber nicht seriös im produktiven Einsatz habe, habe ich mir “Power Catch-up: Alles Praktische und Wichtige aus Java 9 bis 12” von Benjamin Schmid angeschaut. Sehr komprimiert, ohne Füllstoff, kompakt und doch mit Mehrwert gegenüber dem bloßen Lesen von Artikeln zum Thema. Ich war zufrieden. Auch wenn einzelne Themen wie die neuen GC-Varianten oder Project Loom natürlich in 45 Minuten nicht annähernd in gebührender Ausführlichkeit behandelt werden können. Der Vortragende hat sich aber mit potenziell verwirrenden Details angenehm zurückgehalten. Sehr gut.

Und das Beste kommt zum Schluss. “RxJava2: von 0 auf 100(?) in 45 Minuten” von und mit Johannes Schneider. Sensationell. Einer der besten Vorträgem egal zu welchem Thema, den ich je auf einer Konferenz gehört habe. Genau getimed auf die 45 Minuten, professionell und trotzdem humorvoll vorgetragen, voller Anregungen und Informationen. Didaktisch vom Feinsten. Ich muss gestehen, dass das den 2017er Vortrag zur reaktiven Programmierung, den ich anno dazumal gar nicht so schlecht fand, im Nachhinein ziemlich blass wirken lässt, Mein Kompliment an Johannes Schneider.

Was bleibt hängen? Zwei Dinge sollte man sich (ich mir) dringend im Detail anschauen: natürlich Graal(VM), und RxJava/ReactoveX, gerne auch im Swing-Kontext (auch wenn meine heimische Library natürlich die allermeisten Dinge schon auf anderem Wege erreicht – aber mit RxJava gäbe es hier die Möglichkeit, die Code-Lesbarkeit zu verbessern). Dafür kann man glatt seine Vorbehalte gegen 3rd-party-Abhängigkeiten über Bord werfen.

Eine Swing-Komponente – HexDumpView

Nach dem großen Erfolg der JVM-Memory-Indicator-Komponente und dem sensationellen Layout-Manager nun eine weitere unverzichtbare Komponente in jeder grafischen Oberfläche, die aus unverständlichen Gründen nicht schon standardmäßig in Swing ausgeliefert wird: die Anzeige eines klassischen Hexdumps. In der ebenso klassischen 8 + 16 + 16-Darstellung: 8-stellige Adresse, danach 16 Bytes in Hex-Darstellung, danach 16 ASCII-Zeichen als direkte Übersetzung der Bytes. Mit möglichst simpler API: hier ist das Byte-Array, mach’ mir die HexDump-View als Swing-Komponente dazu, die ich zur Nutzung nur noch in eine JScrollPane reinsetzen muss. Natürlich superschnell und superspeichersparend.

Testkandidat war eine 800KiB-Datei (ein adf-Floppyimage, aber das tut nichts zur Sache, hat aber mit dem Ursprung des Wunsches nach dieser Komponente zu tun – ich hoffe, da kann ich demnächst Vollzug melden, dann voraussichtlich nebenan im RISC OS-Blog), resultierend in einem rund 50000 (genau 51200) Zeilen umfassenden Hexdump.

Wie ist es implementiert? Da gab es natürlich eine Menge Varianten, die mir spontan in den Sinn kamen und die wie immer mit unterschiedlichen Kompromissen behaftet sind, die ich kurz anreißen will.

Variante 1: Erzeugen einer String-Repräsentation der Binärdaten und Nutzung einer “normalen” JTextArea oder JEditorPane. Geht schnell, ist unproblematisch in der Realisierung, alle typischen Dinge einer Textkomponente sind direkt verfügbar (auch wenn in diesem Falle das Copy&Paste vermutlich nicht ganz das ist, was der Anwender erwartet). Aber speicher- und laufzeittechnisch sicher suboptimal – die Konvertierung in eine lange Zeichenkette, obwohl man diese ja bei größeren Datensätzen niemals gleichzeitig auf dem Schirm sieht, ist doch verhältnismäßig teuer, und die Wahrscheinlichkeit, dass sowohl das Quell-Byte-Array als auch die Zeichenkette im Speicher verbleiben müssen, ist ja doch recht groß (vermutlich will der Verwender seine Quelldaten ja nicht wegwerfen, sondern noch weiter verarbeiten), und pro Byte kostet die String-Repräsentation ja zusätzlich etwa 14 Bytes (73 Zeichen pro 16 Bytes.

Variante 1a: Erzeugen einer String-Repräsentation der Binärdaten und wrappen in ein java.swing.text.Document als Unterfütterung einer JTextArea/JEditorPane. Spontan würde ich denken, dass so ein reines ReadOnly-Document etwas besser bei Speicher- und Laufzeitverhalten ist als ein Wald- und Wiesen-Document aus dem Swing-Bauchladen.

Variante 2: Erzeugen einer String-Repräsentation der Binärdaten und direkter Redraw in paintComponent. Habe ich getestet, für mäßig große Binärdaten funktioniert das erschreckenderweise recht gut (in meinem Falle waren das rund 50000 Zeilen des Hexdumps), was glaube ich eher zeigt, wie schnell die Maschinen heutzutage sind, und nicht, wie man ein solches Problem angehen sollte. Speichertechnisch ist das etwas günstiger als eine JTextArea oder JEditorPane weil man die ganze unnütze Funktionalität weglässt, aber das Hauptproblem der Erzeugung der großen Zeichenkette bleibt.

Variante 2a: wie 2, aber mit optimiertem Redraw (paintComponent) – nur die Zeilen zeichnen, die laut Clipping Rectangle tatsächlich gezeichnet werden. Das ist geschwindigkeitstechnisch schon erheblich besser als Variante 2, weil eben nur sagen wir 100 Zeilen gezeichnet werden statt 50000, und deshalb aus der vollständigen String-Repräsentation auch nur 100 Substring-Operationen anstatt 50000 durchgeführt werden müssen. Ich habe allerdings nicht analysiert, ob denn den Substring-Operationen im Vergleich zum dann weggeclippten drawString wirklich die teurere Operation ist – einigen wir uns auf “hilft beides nicht”.

Variante 3: kein Erzeugen der vollständigen String-Repräsentation, sondern Erzeugung der zu malenden Textzeile “on demand” aus den Quelldaten, dem Byte-Array. Dabei Inspektion des Clipping Rectangles, um nur die notwendigen Operationen durchzuführen.

Nachfolgend beispielhaft die Implementierungsvariante 3 (die anderen sind zu trivial :-)), als ganz nacktes Java – keine externen Abhängigkeiten, und vermutlich würde der Code so schon unter Java 1.1 mit Swing 1.1 funktionieren. Es gibt hier natürlich Lücken (es sind nur rund 200 Zeilen Code, für eine vollständige JComponent, inklusive JavaDoc!), die selbstverständlich nur als Aufforderung zur Übung an den Leser zu verstehen sind. Encoding-Unterstützung für den Textbereich, Implementierung eines Carets und einer Selektionsmöglichkeit, Unterstützung für nicht-Monospaced-Schriften, on-demand-loading der Binärdaten aus einer Datei größer als MaxHeap während der Benutzer scrollt, suchen nach Text und Bytes und Adressen, Darstellung in Byte- und Word-Form, hinzufügen der Zeilennummer…

Auch eine Variante 4 – ein Document das allein durch die Quelldaten unterfüttert ist und on-the-fly den Text für die JTextComponent generiert, wäre eine interessante Forschungsaufgabe.

/*
 * (c) hubersn Software
 * www.hubersn.com
 *
 * Use wherever you like, change whatever you want. It's free!
 */
package com.hubersn.playground.swing;

import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Insets;
import java.awt.Rectangle;

import javax.swing.JComponent;

/**
 * Component showing a hexdump.
 */
public class HexDumpView extends JComponent {

  /** Number of characters in one line of a hexdump: 8 characters address, 16 2-character-bytes, 16 characters, spaces. */
  private static final int LINE_LENGTH = 8 + 1 + 3 * 16 + 16;

  private byte[] data;

  private int lineCount = -1;

  private FontMetrics fm = null;

  private int lineHeight = 0;

  private int lineAscent = 0;

  private int characterWidth;

  private int calculatedWidth;

  private int calculatedHeight;

  /**
   * Creates a hexdump view based on the given data with a monospaced 12pt font.
   *
   * @param data hexdump data.
   */
  public HexDumpView(final byte[] data) {
    setFont(new Font("Monospaced", Font.PLAIN, 12));
    setData(data);
  }

  /**
   * Sets the data to be visualized by this hexdump view.
   *
   * @param data hexdump data.
   */
  public void setData(final byte[] data) {
    this.data = data;
    refresh();
  }

  /**
   * Sets the font to be used for the hexdump - note that only a monospaced font will yield sensible results.
   */
  @Override
  public void setFont(final Font font) {
    super.setFont(font);
    this.fm = null;
    refresh();
  }

  private void refresh() {
    if (this.data == null) {
      return;
    }
    calculateSizes();
    revalidate();
  }

  private int calculateLineCount() {
    if (this.data == null) {
      return 0;
    }
    int lc = this.data.length / 16;
    if (this.data.length % 16 > 0) {
      lc++;
    }
    return lc;
  }

  private void calculateSizes() {
    if (this.fm == null) {
      this.fm = getFontMetrics(getFont());
      this.lineHeight = this.fm.getHeight();
      this.lineAscent = this.fm.getAscent();
      this.characterWidth = this.fm.stringWidth("M");
    }
    this.lineCount = calculateLineCount();
    this.calculatedWidth = getInsets().left + getInsets().right;
    this.calculatedWidth += LINE_LENGTH * this.characterWidth;
    this.calculatedHeight = getInsets().top + getInsets().bottom;
    this.calculatedHeight += this.lineCount * this.lineHeight + this.lineAscent;
  }

  @Override
  public Dimension getPreferredSize() {
    return new Dimension(this.calculatedWidth, this.calculatedHeight);
  }

  @Override
  protected void paintComponent(Graphics g) {
    super.paintComponent(g);
    if (this.data == null) {
      return;
    }
    Rectangle clipBounds = g.getClipBounds();
    Insets insets = getInsets();
    int y = insets.top + this.lineAscent;
    for (int i = 0; i < this.lineCount; i++) {
      // optimized redraw - check if clipping bounds contains line
      if (shouldDraw(clipBounds, y)) {
        String str = getHexDumpLine(this.data, 0, i * 16);
        g.drawString(str, insets.left, y);
      }
      y += this.lineHeight;
    }
  }

  private boolean shouldDraw(Rectangle r, int y) {
    return !(y + this.lineHeight < r.y || y - this.lineHeight > r.y + r.height);
  }

  //
  // -- utility methods for hex(dump) representation
  //

  /**
   * Converts a value between 0 and 15 into the corresponding hex character - throws an
   * IllegalArgumentException on illegal input values.
   *
   * @param value value to convert.
   * @param useUpperCase use upper case letters A-F?
   * @return hex character representing value.
   */
  private static char valueToHexChar(final long value) {
    if (value < 0 || value > 15) {
      throw new IllegalArgumentException("Value " + value + " out of range - must be 0-15");
    }
    if (value < 10) return (char) ('0' + value);

    return (char) ('A' + (value - 10));
  }

  /**
   * Converts the given value to a number of hex characters specified in nibble count value.
   *
   * @param value
   * @param nibbleCount amount of nibbles (characters) to return.
   * @param useUpperCase use upper case hex characters.
   * @return hex representation of value.
   */
  private static String valueToHexChars(final long value, final int nibbleCount) {
    char[] hexChars = new char[nibbleCount];
    for (int i = 0; i < nibbleCount; i++) {
      hexChars[i] = valueToHexChar(0b1111 & (value >> (4 * (nibbleCount - 1 - i))));
    }
    return new String(hexChars);
  }

  /**
   * Returns a line (not terminated) for a hexdump output - either 16 bytes starting from offset or less if data has less than 16 bytes available.
   *
   * @param data data for dump.
   * @param startAddress start address of data.
   * @param offset offset into data.
   * @return hexdump line.
   */
  private static String getHexDumpLine(final byte[] data, final long startAddress, final int offset) {
    StringBuilder sb = new StringBuilder(LINE_LENGTH);
    sb.append(valueToHexChars(startAddress + offset, 8));
    sb.append(' ');
    for (int lineOffset = 0; lineOffset < 16; lineOffset++) {
      if (offset + lineOffset < data.length) {
        sb.append(valueToHexChars(getUnsigned(data[offset + lineOffset]), 2));
        sb.append(' ');
      } else {
        sb.append("   ");
      }
    }
    for (int lineOffset = 0; lineOffset < 16; lineOffset++) {
      if (offset + lineOffset < data.length) {
        sb.append(getPrintableCharacter(getUnsigned(data[offset + lineOffset])));
      }
    }
    return sb.toString();
  }

  private static int getUnsigned(final byte b) {
    if (b < 0) {
      return ((int) b + 256);
    }
    return b;
  }

  private static char getPrintableCharacter(final int byteValue) {
    if (byteValue < 32 || byteValue > 255 || (byteValue > 128 && byteValue < 160)) {
      return '.';
    }
    return (char) byteValue;
  }

}