Ed Post über JavaScript (2/3)

Teil 2: Besondere JavaScript-Spezialitäten

Ed Post ist wohl einer der allgemein anerkanntesten Experten im Bereich der Programmiersprachen sowie deren Ökosysteme. Wir haben Ed Post nach seiner Keynote im Commnucation Convention Center »Mitte« getroffen, um mit ihm über das Thema “Ist JS (k)eine General Purpose Sprache?” zu diskutieren. In Teil 1 dieses Interviews haben wir uns mit den wilden Jahren von JavaScript befasst, im folgenden Teil 2 sprechen wir über besondere JavaScript-Spezialitäten und in Teil 3 wagt Ed Post ein Fazit und eine Prognose zu JavaScript.

… Schon früh hat Ed Post am MIT mit seinen Thesen und Arbeiten zur Software-Programmierung auf sich aufmerksam gemacht und dabei eine Vorreiterrolle übernommen. Ihm ist es zu verdanken, dass ein allgemeines Umdenken im Bereich der Software-Entwicklung, insbesondere der Programmierung, mitte der 80er Jahre des letzten Jahrhunderts stattgefunden hat. Er hat damit den Weg in die moderne IT, wie wir sie heute kennen, erst ermöglicht …” [POS82]

Das JavaScript Thread-Modell

Frage: Sie haben vorhin Node.js erwähnt (siehe Teil 1 dieses Interviews, Anm.d.Red.). Node.js ist ja ziemlich abgefahren, was hat es damit auf sich?

Ed Post: Node.js ist der Versuch, mit JavaScript serverseitige Web-Anwendungen zu erzwingen. Man verspricht sich davon, dass die selben JS-Entwickelnden, die im Browser das Frontend bauen, auch gleich die Serverfunktionalität mit umsetzen können.

Node.js bringt JavaScript auf den Server […]

… und damit eine Programmiersprache sowohl für den Client wie auch den Server?

Frage: Sie sagen “Versuch” und “erzwingen”, das hört sich ja an, als ob Sie nicht besonders begeistert von Node.js sind?

Ed Post: Das stimmt. Sehen wir uns zum Beispiel das Thread-Modell von JavaScript an [RUN18]. JavaScript arbeitet mit einer sogenannten Main Event-Loop und mit Workern. Im Browser nennen sich diese dann Web-Worker und Worker-Threads bei Node.js. Mit einem echten Thread hat so ein Worker allerdings wenig gemein…

Frage: Wieso? Die Worker laufen doch parallel zur Main Event-Loop?

Ed Post: Das schon, aber die Main Event-Loop und die Worker teilen sich keinen gemeinsamen Speicher, so wie es Threads tun. Ein Thread teilt sich seinen Speicher mit anderen Threads aus seiner Thread-Gruppe. Das gilt für Linux wie auch für Windows. Ein Prozess ist, im Gegensatz dazu, von anderen Prozessen strikt getrennt und kann nur über spezielle Mechanismen wie dem Nachrichtenaustausch mit anderen Prozessen kollaborieren. Auch das gilt sowohl für Linux wie auch für Windows. Ein Worker hat also eher Gemeinsamkeiten mit einem schwergewichtigen Prozess.

Das JavaScript Thread-Modell ist anachronistisch […]

… denn die Main Event-Loop und die Threads betreiben Inter Process Communication nur(!) mittels teurem Nachrichtenversand.

Frage: Die Worker können sich doch über das DOM im Sinne eines Blackboards austauschen [BLA18]. Damit wäre JavaScript wieder im Rennen!

Ed Post: Die Worker können nicht auf Nicht-Thread-Sichere Elemente zugreifen, im Fall der Web-Worker also nicht auf das DOM [ABO18]. Also gerade in Browser-Anwendungen sind diese Worker recht unbrauchbar. Daten müssen immer dediziert zwischen der Main Event-Loop und den Workern übergeben werden.

… There’s no access to non-threadsafe components or the DOM. And you have to pass specific data in and out of a thread through serialized objects …” [ABO18]

Frage: Sie sagten, dass ein Worker eher mit einem schwergewichtigen Prozess zu vergleichen wäre und das Daten “dediziert” zu übergeben wären. Können Sie das genauer ausführen?

Das JS Thread-Modell ist ein starres Konstrukt […]

… das aus der Main Event-Loop garniert um angeflanschte Worker-Threads bzw. Web-Workern besteht.

Ed Post: Wenn verschiedene Worker an einem gemeinsamen Anliegen arbeiten, bekommt jeder Worker seine private Kopie der Daten von der Main Event-Loop per Nachrichtenversand zugeschickt, arbeitet mit diesen, gibt diese wieder zurück an die Main Event-Loop, auch per Nachrichtenversand, was wieder in einer Kopie resultiert, damit die Main Event-Loop dann wiederum die anderen Worker über die geänderten Daten, mittels Nachrichtenversand, in Form weiterer Kopien informiert (puh!). Wir haben es hier also eher mit einer schwergewichtigen IPC zu tun (IPC steht für “Inter Process Communication”, Anm.d.Red.).

Threads in JavaScript müssen auf schwergewichtiges IPC zurückgreifen […]

… um gemeinsame Anliegen zu verarbeiten, und entsprechen damit eher der Definition eines Prozesses denn der eines Threads.

Frage: Und was ist so schlimm am Nachrichtenversand bzw. den Kopien bzw. der IPC?

Ed Post: Schlimm ist, dass es in JavaScript keine Alternativen gibt. Hier wird unglaublich viel Blindleistung produziert. JS-Entwickelnde sind mangels Alternativen gezwungen, Ressourcen zu verschwenden. Was bedeutet denn Nachrichtenversand?

Frage: Sagen sie es mir! Was bedeutet denn Nachrichtenversand?

Ed Post: Nachrichtenversand bedeutet, dass ein Quell-Objekt, etwa aus der Main Event-Loop, serialisert wird, um beim Worker als Kopie wieder deserialisiert zu werden. Es entsteht das Ziel-Objekt. Das bedeutet, dass der Nachrichtenversand Speicherplatz benötigt, einmal für das Quell-Objekt, dann für das Ziel-Objekt und für den Kopierpuffer. Bei ausgiebiger Nutzung dieses Mechanismus wird, versteckt vor den JS-Entwickelnden, eine hohe Last auf der Maschine erzeugt.

Die Kommunikation zwischen JS-Threads und der Main Event-Loop ist komplex […]

… und teuer, was dem unsausweichlichen Nachrichtenversand geschuldet ist.

Frage: Ich habe aber gehört, dass dieser Mechanismus durchaus sinnvoll ist, da der Zugriff mehrerer Threads auf denselben Speicher nur zu Problemen führt!

Ed Post: Ich kann bei entsprechenden anderen Programmiersprachen durchaus auf dieselbe Instanz eines Objekts im Speicher mit mehreren Threads gleichzeitig zugreifen, was auch Fehlersituationen hervorrufen kann. Ich habe aber die Wahl, auch mit dedizierten Kopien meiner Objekte je Thread zu arbeiten.

Bei anderen Programmiersprachen habe ich die Wahl, den JavaScript-Weg des IPC zu gehen […]

… muss es im Gegensatz zu JavaScript aber nicht, denn bei entsprechenden Programmiersprachen habe ich Alternativen.

Ed Post: Und diese Möglichkeit der Wahl fehlt mir bei JavaScript. Darüber hinaus gehört das Arbeiten mit Threads meines Erachtens zum Handwerkszeug beim programmieren. Jede Programmiersprache, die Multi-Threading unterstützt, bietet des weiteren geeignete Mittel, um über Threads hinweg sicher auf gemeinsame Objekte zuzugreifen. Entsprechende Konzepte wie Streams tun ihr übriges, um einen sicheren Zugriff zu gewährleisten.

Frage: Dafür bewahrt einen JavaScript mit seinem Thread-Modell vor Problemen der Nebenläufigkeit!

Viele JS-Entwicklende kommen mit Nebenläufigkeit nicht zurecht […]

… und sind deshalb dankbar, dass JavaScript diesbezüglich sehr eingeschränkt ist bzw. betrachten diese Beschränktheit sogar als Vorteil.

Ed Post: …und erzeugt neue Probleme: Gerade wenn es um Transaktionen oder andere gemeinsamen Ressourcen geht, kommt man mit dem JavaScript Thread-Modell schnell an seine Grenzen. Das kann bis hin zu Deadlocks reichen. Ich kenne serverseitige JavaScript -Anwendungen, da müssen mehrere Node.js Instanzen auf einer Maschine hinter einem Load-Balancer geparkt werden, um Probleme mit Transaktionen zu lindern. Diese Applikation würde auf einer einzelnen Node.js Instanz nach kurzer Zeit schlichtweg blocken.

Node.js mit seiner einzigen Main Event-Loop verklemmt schnell […]

… gerade wenn es um Transaktionen oder andere gemeinsamen Ressourcen geht.” [DON19]

Frage: Heutzutage sind doch Rechenleistung und Speicher satt vorhanden, da sollte doch so ein Thread-Modell wie es JavaScript implementiert, gar kein Problem mehr sein…

Ed Post: Stimmt nicht, es gibt Projekte, die sind gescheitert, weil das Serialisieren und Deserialisieren von Durchlaufdaten eine viel zu hohe Rechenlast und viel zu hohen Speicherverbrauch erzeugt haben. Wie gesagt, es wird unglaublich viel Blindleistung produziert. In JavaScript lässt sich das Serialisieren und wieder Deserialisieren, und damit das Kopieren von Objekten, nicht vermeiden, wenn die Main Event-Loop und die Worker an einer gemeinsamen Aufgabe arbeiten. Und die Main Event-Loop muss alle Worker koordinieren, hier müssen die JS-Entwickelnden also zusätzlich aufpassen, dass der Aufwand, z.B. für das Zusammenführen der Ergebnisse, nicht zu hoch wird. Stichwort “Flaschenhals“…

Die Main Event-Loop ist der(!) Flaschenhals einer JavaScript Anwendung schlechthin […]

… sie kann in einer JS-Instanz einfach nicht in die “Breite” skaliert werden.” [HOR18]

WebAssembly als Alternative

Frage: Aber der Austausch von Daten mit WebAssembly sollte doch hochperformant sein, kennt WebAssembly doch das Konzept des Shared Linear Memory und in JavaScript kann man darauf zugreifen. Hier wird also offensichtlich nicht mittels Nachrichtenaustausch zwischen JavaScript und WebAssembly-Threads gearbeitet…

Ed Post: Sehen wir uns erst einmal an, was WebAssembly überhaupt ist. WebAssembly ist Bytecode, der auf einer stackbasierten virtuellen Maschine ausgeführt wird [WEB18]. WebAssembly bekommt gerade einige Features, die das Potential haben, JavaScript abzulösen [GAR17]. Und WebAssembly ist polyglott, was Entwickelnden mehr Freiheiten bei der Auswahl der Werkzeuge gibt. Zusätzlich ermöglicht WebAssembly die Interoperabilität mit JavaScript mittels Shared Linear Memory.

WebAssembly hat das Potential, JavaScript als einzige Programmiersprache für Browser-Anwendungen abzulösen […]

… und sorgt damit zu einer gewissen Aufregung im JavaScript Lager, müssen doch liebgewonnene Gewissheiten in der JavaScript Filterblase überdacht werden.

Frage: Und Shared Linear Memory stellt offensichtlich kein Nachrichtenaustausch dar und ist daher natürlich hochperformant.

Ed Post: Was stimmt ist, dass WebAssembly das Konzept des Shared Linear Memory einführt, um z.B. mit JavaScript an gemeinsamen Anliegen zu arbeiten. Dieser Shared Linear Memory wird aber auf der JavaScript-Seite leider wieder ad-absurdum geführt, auch wenn es wie gemeinsamer Speicher aussieht. Und was die Performance angeht: Auf Seiten von JavaScript ist Shared Linear Memory sicherlich nicht hochperformant.

Frage: Ad absurdum, inwiefern? Nicht performant, inwiefern?

WebAssembly’s Shared Linear Memory und das JS-Speichermodell lassen sich nur unter Anstrengungen unter einen Hut bringen […]

… und zwar auf Kosten der Performance und des Ressourcen-Verbrauchs

Ed Post: Bei Shared Linear Memory handelt es sich um ein Byte-Feld [LYN18]. Also nicht um ein Feld mit Referenzen auf Byte-Objekte - die es übrigens in JavaScript nicht gibt (die Byte-Objekte, Anm.d.Red.), sondern um ein Feld mit linear hintereinander angeordneten Bytes. JavaScript kennt aber nur den “Zahlen”-Typ Number, dargestellt als 64 Bit langer Floating-Point Wert nach IEEE-754 [RIN14].

Frage: Das bedeutet, dass bei der Arbeit mit Shared Linear Memory von und nach Number konvertiert werden muss?

Ed Post: Ja, bei einem Zugriff auf den Shared Linear Memory von JavaScript aus muss einiges an Konvertierung nach Number, also Float, erfolgen. Da in JavaScript alles ein Objekt ist oder wird(!), wird eine Number, je nach Verwendung, noch zusätzlich mit einem Container umgeben [NUM18]. Stichwort Autoboxing [KLI13].

JavaScript kennt bis Dato nur den Zahlentyp Float […]

… während es sich beim Shared Linear Memory von WebAssembly um ein Byte-Feld handelt.

Frage: Und was ist schlimm daran? JavaScript kennt zwar mit Number für Zahlen nur Float nach IEEE 754. Ein Byte passt jedoch leicht in einen Float-Wert nach IEEE 754?

Ed Post: Das stimmt zwar, ein Byte passt in einen solchen Float. Der volle Wertebereich eines Integers übrigens nicht. Ungünstig ist, dass der Zugriff in JavaScript auf den gemeinsamen Shared Linear Memory nicht ohne Aufwand abläuft, auch wenn es für JS-Entwickelnde wie der Zugriff auf ein Number-Array aussieht.

Frage: Inwiefern?

Ed Post: Unter der Haube haben wir es mit einem ArrayBuffer zu tun. Das sind erst einmal rohe Binärdaten [ARR18]. Damit man damit etwas anfangen kann, muss man in JavaScript entweder eine DataView verwenden, die den ArrayBuffer “interpretiert” …

… You can think of the returned object (DataView, Anm.d.Red.) as an “interpreter” of the array buffer of bytes — it knows how to convert numbers to fit within the buffer correctly, both when reading and writing to it …” [DAT18]

Ed Post: … oder auf den ArrayBuffer einen TypedArray setzen, was nur eine array-like Darstellung des ArrayBuffer ist:

… A TypedArray object describes an array-like view of an underlying binary data buffer …” [TAR18]

Ed Post: In beiden Fällen handelt es sich mehr um eine Simulation eines Arrays denn um einen direkten Zugriff auf einen linearen Speicherbereich.

Frage: Das ist doch in Ordnung, solange es für JS-Entwickelnde wie ein Array aussieht, ist es doch egal, wie es technisch realisiert wurde!

Das Mapping des linearen Speichers von WebAssembly nach JavaScript muss […]

… über den Zahlentyp Float und einer Menge indirekter Zugriffe sowie Datentypkonvertierungen bewerkstelligt werden

Ed Post: Es sieht einfach aus, ist aber aufwändig. Bleiben wir beim Beispiel des Shared Linear Memory: Ein Zugriff aus JavaScript auf ein gemeinsames Byte im Shared Linear Memory benötigt nämlich die Konvertierung dieses Bytes in eine Number, und zwar je nach Verwendung eingebettet in einen Container, also als Objekt. Die interne Darstellung einer Number ist ja eine Andere als die des Bytes im Shared Linear Memory, weshalb JavaScript nicht direkt in den Shared Linear Memory referenzieren kann. So ist oder wird(!) in JavaScript irgendwann alles ein Objekt [CRO11]. Wenn es eine Number ist, kann damit weiter gearbeitet werden…

Frage: Ein Array in JavaScript kann aber sehr wohl primitive Typen enthalten [CRO10]…

Ed Post: Und diese wären? Als primitive Typen werden in JavaScript null, undefined, Boolean, Number sowie String gehandelt, die bei Bedarf in Container gepackt werden, um als Objekt verwendet zu werden. Also eine Form des Autoboxing [KLI13]. Diese Typen können nicht durch eine Referenz in ein Byte-Feld, wie es der Shared Linear Memory ist, abgebildet werden. Des weiteren ist in JavaScript ein Array nicht das, was man in anderen Programmiersprachen unter einem Array versteht:

… Traditionally an array reserves a continuous allocation of memory of predefined length. In JavaScript this is not the case. A JavaScript array is simply a glorified object with a unique constructor and literal syntax and an additional set of properties and methods …” [CRO10]

Frage: Das bedeutet, dass dieser Shared Linear Memory nicht 1:1 im JavaScript-Code verwendet werden kann, weil JavaScript ganz anders mit Daten umgeht? Wir können also nicht einfach im Sinne eines Arrays mit primitiven Bytes darüber verfügen?

Ed Post: Genau, das Speicher-Layout und das Zahlenkonzept von JavaScript lässt den Zugriff auf den Shared Linear Memory ohne großen Aufwand nicht zu. Bei jedem Zugriff auf den Shared Linear Memory muss für jedes zugegriffene Byte eine Konvertierung nach Float, das Allozieren von Speicher für den Objekt-Container, und das Zurückgeben einer Referenz auf dieses Objekt an das JavaScript Programm erfolgen. Dieses arbeitet dann über diese Referenz mit dem Wert, bis der Garbage-Collector dieses Objekt abräumen darf.

Frage: Warum eine Konvertierung nach Float?

Ed Post: Weil Number der einzige Zahlentyp in JavaScript ist, und Number wird intern als Float dargestellt.

Frage: Wir haben also im schlimmsten Fall eine Kopie des Bytes als Objekt, die auch noch den Garbage-Collector belastet, während im Shared Linear Memory dieses Byte weiterhin unbekümmert koexistiert?

Ed Post: Richtig. Wir arbeiten also quasi wieder mit einer sehr aufwändigen Darstellung des Bytes, selbst wenn dem JS-Interpreter unter der Haube zwecks Performance noch Tricks beigebracht würden. So wäre es denkbar, dass JavaScript intern verschiedene Repräsentation von Number kennt, also ggf. auch eine interne Byte-Darstellung. Das wird aber wieder Implikationen bei Operationen mit Float-Werten nach sich ziehen. Wie auch immer, es ist immer großer Aufwand für die Speicherverwaltung notwendig, und das alles spiegelt sich in der Ausführungsgeschwindigkeit der Anwendungen wieder.

Frage: Und dann muss ja auch noch bei Schreiboperationen ein Byte zurück in den Shared Linear Memory übertragen werden…

Ed Post: Wobei wir ja das Float-Objekt als Grundlage haben. Also das ganze Spiel in umgekehrter Reihenfolge. Mal eben über einen Array “loopen” ist eben nicht. Das fatale daran ist: Die JS-Entwickelnden “sehen” diesen exorbitanten Overhead nicht, den der JS-Interpreter leisten muss.

Frage: Jetzt wundere ich mich allerdings nicht mehr über die lausige Ausführungsgeschwindigkeit so mancher Website. Große Mengen an Daten (z.B. Bilddaten, Anm.d.Red.) mit JavaScript zu verarbeiten, scheint keine gute Idee zu sein…

Ed Post: Stimmt. Für JS-Entwickelnde sieht alles schön nach Array-Zugriff aus, der Zugriff auf den Shared Linear Memory wird vor dem JS-Programmierer “weg abstrahiert”, so dass sich dieser über die lausige Performance wundert. Was aber unter der Haube passiert, ist eine unglaubliche Verschwendung von Ressourcen. Übertroffen wird die Illusion der Einfachheit durch Operationen wie memory.grow(n), die den Shared Linear Memory Buffer in JavaScript vergrößern können. Dabei wird der Buffer aber keineswegs vergrößert, sondern weggeworfen und ein neuer Buffer alloziert.

… Note: Since an ArrayBuffer’s byteLength is immutable, after a successful Memory.prototype.grow() operation the buffer getter will return a new ArrayBuffer object (with the new byteLength) and any previous ArrayBuffer objects become “detached”, or disconnected from the underlying memory they previously pointed to …” [GRO18]

Frage: Ist da Besserung in Sicht, im Sinne von Lessons learned?

Ed Post: Ja, tatsächlich, und zum Glück nicht im Sinne vom unsäglichen strict mode (lacht). Es wird an einem primitiven Typen BigInt gearbeitet, also einem schmerzlich vermissten Integer-Typen, der dann als TypedArray solch einen Shared Linear Memory optimaler abbilden könnte [BIG18].

Reaktive Programmierung

Frage: Wechseln wir das Thema. JavaScript ermöglicht die Reaktive Programmierung, womit JavaScript extrem responsiv und schnell wird. So etwas hat man in anderen Programmiersprachen noch nicht gesehen!

Reaktive Programmierung ist in JavaScript nicht deshalb so populär, weil “reactive” so toll ist […]

… sondern weil reaktive Programmierung schlichtweg notwendig ist, um mit dem verfügbaren Thread-Modell überhaupt arbeiten zu können. Um mit der Main Event-Loop so etwas, das wie kooperatives Multitasking aussieht, zu simulieren.

Ed Post: Stimmt nicht. Reaktive Programmierung kann man in den allermeisten Programmiersprachen auf die eine oder andere Art umsetzen. Allein das Observer-Pattern [OBS18] ist eine Ausprägung des reaktiven Stils [BER13]. Streams wären eine weitere Ausprägung für reaktive Programmierung. Reaktive Programmierung bedeutet ja am Ende des Tages das Propagieren von Änderungen, um diese zu Verarbeiten, also um eine Verarbeitung von Ereignissen anzustoßen…

… Das zugrunde liegende Ausführungsmodell propagiert Änderungen in den Datenflüssen automatisch. Ein gutes Beispiel für ein Programm, welches reaktiv arbeitet, ist Excel. Ändert man einen Wert in einer Zelle, dann ändert sich auch der Wert in der Summenzelle …” [REA18]

Ed Post: Und nein, reaktive Programmierung macht den Code nicht schneller. Man kann Wartezeiten auf Ergebnisse nutzen, um andere Aufgaben im selben Thread durchzuführen. Das wird bei JavaScript genutzt, ja, man muss es sogar nutzen, um mit dem einen Thread, den man hat (die Main Event-Loop, Anm.d.Red.), halbwegs wirtschaftlich arbeiten zu können. Die Worker-Threads bzw. Web-Worker sind ja leider keine echte Alternative…

Frage: Ein Thread, hier die Main Event-Loop, wird jedoch optimal genutzt!

Ed Post: Ja, ein Thread wird optimaler genutzt, da dieser nur verarbeitet, und nicht wartet. Übrigens, ein wartender Thread bei Programmiersprachen, die Multi-Threading unterstützen, verbraucht keine(!) Rechenzeit, die steht anderen Threads zur Verfügung. Der Kontextwechsel von einem Thread zu einem anderen Thread bedeutet Aufwand, der allerdings dem entsprechen sollte, was die Main Event-Loop in JavaScript leistet, wenn zwischen wartenden und aktiven Callbacks gewechselt wird. Auch hier muss der Kontext der beteiligten Callbacks vorgehalten werden.

Frage: Sie erwähnen die Callbacks. Diese sind ja ein zentraler Bestandteil der reaktiven Programmierung…

Ed Post: Immer wenn beispielsweise in der Main Event-Loop auf ein Ergebnis gewartet wird, geschieht dies über ein sogenannten Callback. Dieser wird mehr oder weniger elegant in Sprachkonstrukten verkleidet, um die Asnychonität zu verschleiern, und der Thread kann weiterarbeiten. Reaktive Programmierung ist jedoch erst dann interessant, wenn die Anforderungen eine solche erfordern, und nicht weil die technischen Gegebenheiten dies Verlangen.

JavaScript bedingt reaktive Programmierung, es gibt dort keine echte Alternative […]

… jedoch ist reaktive Programmierung nur sinnvoll, wenn die Anforderungen dies erfordern, und nicht weil mangels Alternativen kein Weg darum herum führt.

Frage: Wieso? Man kann doch jedes Problem auch reaktiv darstellen.

Ed Post: Das stimmt. In JavaScript will man das aber in Wirklichkeit gar nicht. Durch Programmkonstrukte wird sogar versucht zu verschleiern, dass ein reaktives Programmiermodell hinter dem Code steckt. Reaktive Programmierung ist eben nicht für jedes Problem geeignet und nicht für jede Audienz nachvollziehbar. Der reaktiven Programmierung wird deshalb der Anstrich des Sequentiellen gegeben, da das reaktive Modell nicht immer sinnvoll aber “dank” des JavaScript Thread-Modells notwendig ist.

… With promises we write asynchronous code that emulates synchronous code but with async/await we write asynchronous code that looks like synchronous code. As a consequence this often leads to misconception …” [CLA18]

Ed Post: Man kann übrigens auch jedes Problem mit der Turing-Maschine lösen. Nur wird diese Lösung nicht in jedem Fall wartbarer oder verständlicher. Das Gegenteil ist häufig der Fall. Wenn ich jetzt aus technischen Gründen gezwungen werde, reaktiv zu Programmieren, kann sich meine Lösung nachher als nicht mehr Wartbar erweisen.

Frage: Dafür verhindert die reaktiven Programmierung das Blocken der Main Event-Loop!

Ed Post: Nein, in JavaScript kann trotz reaktiver Programmierung jederzeit die gesamten Main Event-Loop blockieren, da wir hier so etwas wie kooperatives Multitasking betreiben. Im Gegensatz dazu steht echtes Multitasking, etwa mit n Threads in einer Applikation, die auf dieselben Objekte zugreifen können.

Frage: Wir haben heutzutage so viel Rechenpower, da kann man doch locker alles in der Main Event-Loop abfrühstücken!

Ed Post: Nein, ein moderner Prozessor verfügt heutzutage über duzende Cores, je Core sind dank SMP mehrere Hardware-Threads verfügbar [SMT18], also mindestens zwei. Bei 32 Cores haben wir schon 64 Hardware-Threads, bei 64 Cores wären es 128 Hardware-Threads, usw.

Verikal und horizontal skalieren

Frage: Der Zukunft gehören also stark parallelisierten Anwendungen mit gemeinsamen Speicher [SYM18].

JavaScript geht einher mit einer alten Denkweise, wo man auf vertikale Skalierung setzen konnte (eine CPU mit immer mehr MHz-Taktung, Anm.d.Red.) und Moores Law immerwährend zu gelten schien […]

… aber in Zeiten, wo man horizontal skaliert (viele CPUs parallel, da sich die MHz-Taktungen nicht mehr beliebig steigern lassen, Anm.d. Red.) und Multi-Core CPUs in jedem PC zum Einsatz kommen, kommt auch das JavaScript Thread-Modell an seine Grenzen

Ed Post: Und jetzt kommt JavaScript: Da haben wir effektiv ein Singe-Threaded Thread-Modell. Wir nutzen also für alles, was in der Main Event-Loop ausgeführt wird, einen einzigen Hardware-Thread von Dutzenden verfügbaren. Wollen wir in JavaScript weitere Threads nutzen, müssen wir die kostspieligen Worker-Threads ins Spiel bringen, womit wir die Main Event-Loop zusätzlich belasten, sprich Nachrichtenversand und Serialisierung sowie Deserialisierung betreiben. Also keine Rede von gemeinsamem Speicher.

Frage: Nun, im Fall von Node.js startet man einfach mehrere Node.js Instanzen für eine Anwendung!

Ed Post: Starten wir mehrere Node.js Instanzen der gleichen Anwendung, so starten wir mehrere JavaScript Anwendungen in jeweils eigenen Adressräumen auf einer Maschine. Also jeweils in einem eigenen Prozess ohne gemeinsam genutzte Ressourcen. Das kostet, da sich die so gestarteten Prozesse keinen Speicher teilen. Sollen die Node.js Anwendungen am gleichen Anliegen arbeiten, werden diese hinter einem Load-Balancer vereint. Damit das alles funktioniert, muss die Anwendung jedoch “stateless” (Zustandslos, Anm.d.Red.) sein. Und ist sie “stateless”, können die verschiedenen Prozesse nicht an den selben Berechnungen arbeiten oder müssen sich über komplizierte Mechanismen synchronisieren (etwa mittels Nachrichtenversand, Anm.d.Red.).

Frage: Das hört sich nicht effizient an. Und in der Cloud kosten Rechenleistung und Speicher Geld, Bit für Bit, Operation für Operation. Für rechenintensive Aufgaben ist JavaScript offensichtlich nicht geeignet und kann für kleine Lösungen schnell teuer werden …

Für rechenintensive Aufgaben ist JavaScript offensichtlich nicht geeignet […]

… kennt es doch nur eine Main Event-Loop und kostspielige Worker

Ed Post: Die reaktive Programmierung im Fall von JavaScript hilft uns auch nicht bei der Vermeidung von Deadlocks. Etwa wenn mit Datenbank-Transaktionen gearbeitet wird. Da können sich dann die verschiedenen Callbacks gegenseitig blockieren und die Main Event-Loop steht still. Da muss dann tatsächlich mit Kanonen auf Spatzen geschossen werden, etwa mehrere Node.js Instanzen hinter einem Load-Balancer. Hier werden Probleme gelöst, die in einer Multi-Threading Umgebung gar nicht existieren.

Frage: Man kann also sagen, dass die Main Event-Loop der Flaschenhals einer JavaScript Anwendung schlechthin und in Zeiten von Multi-Core Prozessoren nicht mehr zeitgemäß ist?

Bei JavaScript wird man gezwungen, mit Kanonen auf Spatzen zu schießen […]

… die bei Programmiersprachen mit Multi-Threading dort schlichtweg nicht herumflattern.

Ed Post: (nickt)

Der JavaScript Paketmanager

Frage: Vor kurzem war zu lesen, dass eine JS-Bibliothek über den Paketmanager npm sog. Malware zum Klauen von BitCoins verteilt hat [BÖC18]. Es gab früher schon einen ähnlichen Fall von Malware in der JS-Szene [PAR18]. Ist das nun typisch für JavaScript?

Ed Post: Das ist jetzt kein typisches JavaScript-Problem, wohl eher ein Problem des Paketmanagers npm, der als Quasi-Standard von vielen JS-Entwickelnden zum Auflösen externer JS-Bibliotheken in eigenen Projekten verwendet wird. Das Problem scheint aber symptomatisch für die Attitüde in der JS-Community zu sein…

Frage: Von welcher Attitüde sprechen Sie denn bitte?

Ed Post: Die JS-Community macht allgemein den Eindruck, dass geflickt wird, bis etwas oberflächlich betrachtet passt. Sehen wir uns nur die Objektorientierung, das Thread-Modell oder npm an. Das zieht sich wie ein roter Faden durch die Geschichte von JavaScript.

Die Attitüde zum Thema Sicherheit in der JS-Gemeinde ähnelt derjeniger Microsofts in den 80er und 90er Jahren […]

… wo Sicherheit als lästig bei der Umsetzung von Features angesehen wurde. Damals herrschte das Motto: “Lieber ein bequemes Feature als etwas mehr Sicherheit”, Microsoft hält von dieser Praxis mittlerweile Abstand.” (siehe auch [MWV18], vergl. das Vorkommen der Begriffe Security und Feature)

Frage: Was macht npm denn falsch, was solche Sicherheitslücken erlaubt, und was ist denn so symptomatisch an der JS-Community?

Ed Post: Symptomatisch ist, dass die JS-Community gerne für alles Mögliche externe Pakete verwendet (Pakete sind im weitesten Sinne Bibliotheken, Anm.d.Red.). Selbst für die Zahl PI gibt es ein eigenes Paket [ION17]. Je mehr Pakete eine Anwendung “zieht”, desto größer der Angriffsvektor für Schadcode [UPR18].

Frage: Und was macht npm denn falsch?

Ed Post: Die JS-Entwickelnden mögen es gerne “einfach”, und diese Nachfrage bedient npm: Alle Entwickelnden mit einem npm Account können auch gleich weltweit eigene Pakete über npm veröffentlichen. Es findet keine Identitätsprüfung statt. Ein fiktiver Benutzername und dazu ein Passwort sind vollkommen ausreichend [SIG18], um vollwertiges Mitglied der npm Gemeinde zu werden. Und veröffentlichte Pakete müssen nicht signiert werden.

Der JavaScript Paketmanager npm öffent Tür und Tor für Misbrauch […]

… worauf auch die Satire “What Happened When I Peeked Into My Node_Modules Directory” anhand fiktiver Beispiele augenzwinkernd hinweist.” [JSC16]

Frage: Das ist doch gut, so vereinfachtes es doch vieles!

Ed Post: Und das bedeutet, dass sich weder der Urheber eines Pakets nachverfolgen noch die unrechtmäßige Manipulationen von Paketen feststellen lässt. Da gibt es auch eine schöne Story, oder sollte ich besser sagen “Anleitung”, zu diesem Thema:

I’m harvesting credit card numbers and passwords from your site. Here’s how.” [GIL18]

Frage: Es gab ja auch den Fall, dass Pakete wieder aus npm entfernt wurden, womit dann eine Vielzahl von Projekten nicht mehr gebaut werden konnte [WIL16]. Musste npm da nicht erst gehackt werden, damit man daraus nachträglich wieder etwas entfernen kann?

Ed Post: Nein, das ist passiert, weil npm ein so genanntes unpublish unterstützt, also die nachträgliche beliebige Rückabwicklung einer Publikation. Die Dokumentation schreibt dazu lediglich:

… It is generally considered bad behavior to remove versions of a library that others are depending on! …” [UNP18]

Frage: Und ein unpublish kann dann dafür sorgen, dass einige Projekte nicht mehr bauen…

Andere Paketmanager als npm arbeiten da sehr viel restriktiver […]

… was das Publizieren oder Löschen von Modulen angeht. So liegt die Latte für Schindluder bei npm extrem niedrig.

Ed Post: Man kann sich bei npm nicht darauf verlassen, dass dort irgendetwas Bestand hat und es ist sehr einfach für den Urheber einer JS-Bibliothek, diese, selbst wenn von Anderen verwendet, einfach wieder aus npm zu löschen. So etwas kennt man von entsprechenden Tools bei anderen Ökosystemen so nicht. Dort ist der Prozess sehr viel aufwändiger, wenn z.B. aus rechtlichen Gründen wieder etwas aus den offiziellen Verzeichnissen der Paketmanager genommen werden muss.

Designfehler

Frage: Früher wurde viel über JavaScript Designfehler gesprochen. Jetzt ist es still um dieses Thema geworden. War das eine überhitze Diskussion?

Alle Programmiersprachen haben ihre Designfehler […]

… einfach weil keine einzige Sprache perfekt sein kann, so wie es bei (fast) allen anderen Dingen auch ist.

Ed Post: Ich provoziere mal und sage nur: Comparisons, Equality, Identity, Autocasting oder Primitives:

Comparison: “<, <=, =, >=, >

null >= 0;			// --> true, WTF?
null <= 0;			// --> true, WTF?
null == 0;			// --> false, OK
null > 0;			// --> false, OK
null < 0;			// --> false, OK

Equality: “==

6 == 6;				// --> true, OK
6 == [6];			// --> true, WTF?
6 == [[6]];			// --> true, WTF?
'' == '0';			// --> false, OK
0 == '';			// --> true, WTF?
false == null;			// --> false, OK
false == undefined;		// --> false, hmm?
false == '0';			// --> true, WTF?

Identiry: “===

6 === 6;			// --> true, OK
6 === [6];			// --> false, OK
6 === [[6]];			// --> false, OK
'' === '0';			// --> false, OK
0 === '';			// --> false, OK
false === null;			// --> false, OK
false === undefined;		// --> false, OK
false === '0';			// --> false, OK

Autocasting:

result = '4' + 2;		// result --> "42" (String), OK
result = '4' - 2;		// result --> 2 (Number), WTF?

Primitives:

undefined = 42;			// undefined --> 42 (Number), WTF primitives?

Frage: Sind das jetzt Designfehler?

Ed Post: Zum einen muss man vielleicht zwischen echten Designfehlern und evolutionärer Weiterentwicklung einer Programmiersprache unterscheiden. Zum Anderen kann man das Thema bezüglich der Nachvollziehbarkeit beurteilen, also nach dem Motto: “Kann ich mir das Verhalten der Programmiersprache in bestimmten Fällen noch herleiten oder nicht?

Frage: Und hier hatte JavaScript einige “Features” zu bieten, die es in sich hatten. Ich spreche hier von Diskussionen wie [PET12], [ENG17], [ENG16], [DIS16], [MIC15], [ENG15], [CUN10] sowie die legendäre “Designfehler”-Liste auf [WTF16], Beiträge mit so sprechenden Titeln wie “JavaScript is a Dysfunctional Programming Language”, “10 design flaws of JavaScript”, “Why is JavaScript so hated?” oder schlicht “WTFJS” (What the Fuck JavaScript, Anm.d.Red.) …

Ed Post: (Fast) jede Programmiersprache hat Designfehler. Auch ist es oft strittig, ob etwas ein Fehler oder eine Funktion ist. So werden Bugs gerne als undokumentierte Features proklamiert [UND18]. Tatsache ist, dass mit TypeScript und den fortschreitenden Versionen von ECMAScript sowie mit Dart versucht wird, JavaScript “besser” zu machen bzw. mit Lintern Programmierfehler zu vermeiden. Es wird ein Bedarf gedeckt, den JavaScript selber nicht bedienen kann.

Frage: Und TypeScript, Linter oder Dart bügeln die Designfehler dann auch aus?

Ed Post: Jein. So ist etwa jeder JavaScript-Code ist auch vollwertiger TypeScript-Code:

… TypeScript is a strict superset of ECMAScript 2015, which is itself a superset of ECMAScript 5, commonly referred to as JavaScript. As such, a JavaScript program is also a valid TypeScript program, and a TypeScript program can seamlessly consume JavaScript …” [COM18]

Tool Fatigue

Ed Post: Da Abwärtskompatibliltät stets gewahrt bleibt, ist in TypeScript weiterhin alles möglich und rechtens, was in JavaScript möglich und rechtens ist, also die ganze Liste aus [WTF16]. Hier kommen dann die Linter ins Spiel, das sind Werkzeuge, die den Skript-Code auf problematische Stellen hin untersuchen. Am Ende wird mittels Transpilern aus TypeScript oder Dart dann JavaScript erzeugt. Dart hat es allerdings einfacher, ist es doch eine eigenständige Programmiersprache, die nicht auf Abwärtskompatibliltät zu JavaScript achten muss. Es wird aber dennoch nach JavaScript transpiliert.

Frage: Und das Transpilieren sowie das Linten vermeiden dann schlechten Stil im JavaScript-Code?

Ed Post: Sowohl für TypeScript- wie auch für JavaScript-Code, ja. Jetzt verlieren wir aber die Einfachheit der JavaScript Entwicklung, die überall lautgesprochen wird: Wir müssen transpilieren und linten, also Transpiler und Linter in unsere Toolchain einbinden, um nur Einige zu nennen [UMA18]. Zusätzlich mit npm als Paketmanager ist eine JS-Entwicklungsumgebung alles andere als einfach. Zusätzlich wird gerne auch noch minifiziert, um etwa die Download-Größe des JavaScript-Codes für den Browser zu reduzieren.

JavaScript Tool Fatigue […]

… eine typische JS-Entwicklungsumgebung mit zugehöriger Toolchain ist mittlerweile alles andere als einfach.

Ed Post: Und will man sinnvoll Debuggen, braucht es Source-Maps, die den Bezug zwischen TypeScript bzw. minifiziertem JavaScript bzw. Dart und dem zugrunde liegenden JavaScript herstellen [USE18]. Diese Source-Maps müssen irgendwo erzeugt werden, also meist beim Transpilieren, und irgendwo unterstützt werden, also meist beim Debuggen. Alles zusammen, also Linten, Transpilieren, Minifizieren, Source-Maps einbinden und Repositories wie npm integrieren, wird tatsächlich schon als “JavaScript Tool Fatigue” bezeichnet [DEV16].

Frage: Einfach geht anders. All das sind ja Hilfsmittel, um mit Anstand um die Designfehler herumzueiern…

Ed Post: Ganz zu schweigen von den Stolperfallen, wie der anfangs erwähnte strict mode oder fehlende Integer-Datentypen.

Weiter geht es mit Teil 3, wo Ed Post ein Fazit und eine Prognose zu JavaScript wagt…

In Teil 1 dieses Interviews haben wir uns mit den wilden Jahren von JavaScript befasst, in Teil 2 sprachen wir über besondere JavaScript-Spezialitäten von JavaScript und in Teil 3 wagt Ed Post ein Fazit und eine Prognose zu JavaScript.


[ABO18]: MDN, 2018
[ARR18]: MDN, 2018
[BER13]: Michael Berry, 2013
[BIG18], Chrome Platform Status, 2018
[BLA18]: Wikipedia, 2018
[BÖC18]: Hanno Böck, 2018
[BUC18]: Craig Buckler, 2018
[CLA18]: Luc Claustres, 2018
[COM18]: Wikipedia, 2018
[CRO10]: Angus Croll, 2010
[CRO11]: Angus Croll, 2010
[CUN10]: Cunningham & Cunningham, 2010
[DAT18]: MDN, 2018
[DEL16]: Carlos Delgado, 2016
[DON19]: Node.js, 2019
[DST18]: Wikipedia, 2018
[DEV16]: Anne Dev, 2016
[DIS16]: Peter DiSalvo, 2016
[DUC18]: Wikipedia, 2018
[ECM18]: Wikipedia, 2018
[EMB16]: Ember, 2016
[ENG15]: Richard Kenneth Eng, 2016
[ENG16]: Richard Kenneth Eng, 2016
[ENG17]: Richard Kenneth Eng, 2017
[EPO19]: Ed Post, 2019
[FUN18]: Wikipedia, 2018
[GAR17]: Andreas Rossberg, 2017
[GIL18]: David Gilbertson, 2018
[GRO18]: MDN, 2018
[HOR18]: Wikipedia, 2018
[ION17]: Ionică Bizău, 2017
[JAV18, Wikipedia, 2018
[JSC16]: Jordan Scales, 2016
[JSG18]: Wikipedia, 2018
[JVM18]: Wikipedia, 2018
[KLI13]: Felix Kling, 2013
[LYN18]: WebAssembly, 2018
[MCC17]: Judy McConnell, 2017
[MIC15]: James Mickens, 2015
[MOS15]: Brian Moschel, 2015
[MWV18]: Wikipedia 2018
[NUM18]: MDN, 2018
[NYS13]: Bob Nystrom, 2013
[OBS18]: Wikipedia, 2018
[PAR18]: Matthias Parbel, 2018
[PET12]: Ke Pi, 2012
[POS82]: Ed Post, 1982
[PRI18]: Wikipedia, 2018
[PRO18]: Wikipedia, 2018
[RAJ16]: Raja Rao, 2016
[RAU11]: Dr. Axel Rauschmayer, 2011
[REA18]: Wikipedia, 2018
[RIN14]: Brian Rinaldi, 2014
[RUN18]: MDN, 2018
[SIG18]: NPM, 2018
[SLO18]: MDN, 2018
[SMT18]: Wikipedia, 2018
[STR18]: MDN, 2018
[SYM18]: Wikipedia, 2018
[TAR18]: MDN, 2018
[TCO18]: Wikipedia, 2018
[TYP18]: Wikipedia, 2018
[UND18]: Wikipedia 2018
[UMA18]: u/marosurbanec, 2016
[UNP18]: NPM, 2018
[UPR18]: u/ProbablyNotCanadian, 2018
[USE18]: MDN, 2018
[VER18]: Wikipedia, 2018
[WEA18]: Wikipedia, 2018
[WEB18]: Wikipedia, 2018
[WIL16]: Chris Williams, 2016
[WIN14]: Topher Winward, 2014
[WTF16]: Brian LeRoux & Andrew Lunny, 2016
[YOU08]: Michael Youssef, 2008