July 29, 2021

Wie klingt die Ereignisschleife? | von Ronen Lahat | AT&T Israel Tech-Blog | Juli 2021

Eine musikalische Reise ins Innere von Node

Das Libuv-Logo von Node über einem Max for Live Connection Kit

Es gibt eine Vorstellung, dass Codierung und Debugging rein intellektuelle Aufgaben sind. Automechaniker können Motorprobleme feststellen, indem sie dem Motor zuhören und sein Startgeräusch und seinen Nachhall untersuchen, wie es ein Arzt mit seinem Stethoskop tun würde. Währenddessen stecken die Entwickler bei der Terminal Bell – ASCII-Code 7 – einem Relikt aus elektronischen Schreibmaschinen. Es gibt viel mehr, als man auf den ersten Blick sieht.

Dieser Mangel an hörbarem Feedback schränkt unsere Intuition ein. Bis vor kurzem hatten wir Geräusche: die Telefontöne von DFÜ-Internet, die Drehzahl und die metallischen Nadeln der Festplatte und die Lüfter, die sich unter hoher Rechenlast frenetisch drehen. Man konnte „spüren“, was vor sich ging.

Das Schicksal von Computergeräuschen ist dem Elektroauto ähnlich, wo Fortschritt und klares Design die Dinge glatt bis nicht vorhanden gemacht haben und nur Stille, poliertes Glas und ein glänzendes Metallgehäuse hinterlassen haben. Währenddessen stellen die Autohersteller schicke Sounddesigner ein, um „skeuomorphisieren“ das Geräusch eines beschleunigenden Motors, der dem Fahrer einen spürbaren Schub gibt und hoffentlich abgelenkte Fußgänger warnt.

Entwickler sollten das Gefühl haben, dass mit ihrer Laufzeit „etwas nicht stimmt“. Dies könnte auch sehbehinderten Entwicklern helfen, die durch eine geringe Informationsbandbreite durch ihre Augen eingeschränkt sind.

Ich meine nicht nervige Pieptöne; Ich spreche von schön gestalteten Materialklängen. Als ob das Innere einer Software je nach Thema dezent metallisch, glasig oder gar aus Holz und Granit wäre. Die Themen könnten auch harmonisch reichen, von atonalen perkussiven Klängen bis hin zu chromatischen, variierend in Tonarten und Stimmungen.

Ich stelle mir diese Klänge als nicht aufdringliche Hintergrundelemente vor, das heißt, sie stören nicht Ihre Musikwiedergabeliste. Ich würde nicht sagen angenehm; Das hängt von der Leistung Ihrer App ab.

Rudi Halbmeir, Sounddesigner bei Audi, nimmt Didgeridoo-Sounds für Elektroauto-Motoren auf.

Für mein Projekt werde ich versuchen, in die Ereignisschleife von Node einzutauchen und ihr etwas Körperlichkeit zu verleihen. Dazu richte ich Hooks ein, die MIDI-Signale erzeugen, und verarbeite die Signale in Echtzeit, um Sound zu erzeugen.

Ich bin kein Sounddesigner, bastele aber gerne an interessanter Software. Ich wurde kürzlich von Ableton Live und Max for Live inspiriert. Live ist eine Digital Audio Workstation (kurz DAW), die für Live-Auftritte entwickelt wurde und bei elektronischen Musikern beliebt ist. Max, ein „Software zum Experimentieren und Erfinden,“ ist eine visuelle Programmiersprache von Miller Puckette und später David Zicarelli (Gründer von Cycling ’74) zur Verarbeitung von Tonsignalen aus dem Jahr 1986. Ein Nachkomme von MUSIC-N, einem Programm von Max – daher der Name – Mathews im Jahr 1957 bei BellLabs.

In Max arbeitet man in einem „Patcher“-Fenster, das seine UX-Metaphern aus dem „Patching“ von modularen Synthesizern zusammen mit Kabeln übernimmt. Über die Metapher hinaus kann man Max sogar mit physischen Patchkabeln und Komponenten erweitern. Die visuellen Metaphern verbinden sich perfekt mit der objektorientierten Programmierung, bei der sich Objekte gegenseitig Nachrichten senden und der Code wie eine UML-Repräsentation seines Objekts aussieht. Darüber hinaus ist Max datenflussorientiert, wobei das gesamte Programm von der Eingabe bis zur Ausgabe ein gerichteter Graph ist.

Screenshot eines Max-Patcher-Fensters von Cycling ’74

Max-for-Live ist eine Integration von Max in Ableton Live, sodass Ihre Patches nativ in Live funktionieren, und ist in der Ableton Live Suite (und der großzügigen 90-Tage-Testversion) enthalten. Max for Live hat eine breite Community. In der neuesten Version wurde Node for Max eingeführt, mit dem ich ein Nodejs-Skript in ein Max-Objekt verwandeln kann. Ableton und Cycling ’74 bildeten eine Partnerschaft (ein unabhängig geführtes Unternehmen, das vollständig im Besitz von Ableton ist). Eine mir weniger bekannte Open-Source-Alternative ist Pure Data (Pd), ebenfalls entwickelt von Max’s Miller Puckette.

Warnung: Beim Basteln mit Ton ist es sehr wichtig, die Dinge mit sicheren Pegeln auszugeben, um eine Beschädigung der Lautsprecher oder schlimmer: des Gehörs zu vermeiden.

Es gibt keine gute Möglichkeit, sich von Node selbst an die Ereignisschleife anzuschließen. Ja, es gibt async-hooks und process._getActiveHandles(), aber ich will das echte Ding. Beim Graben fand ich den Drachen in der Höhle: libuv. Ursprünglich als Unix-only Libev von Marc Lehmann geschrieben, ist libuv die plattformübergreifende Bibliothek von Nodejs, die eine Abstraktion über I/O-Polling-Mechanismen bietet.

Libuv ist schlank, performant und hat eine stabile API (gleiche API für Windows- und Unix-Systeme!). Neben Nodejs wälzt sich Libuv unter der Haube vieler anderer interessanter Projekte.

Libuvs Logo: der Unicorn Velociraptor. Wo ist das Merchandise?

Ich werde mit der Unix-Implementierung arbeiten. Libuv ist nicht nur ein eigenes Repo, sondern befindet sich auch in Node, unter deps/uv.

Libuv ist asynchron und ereignisgesteuert. Es fragt I/O über eine Ereignisschleife ab und plant Rückrufe. Dies ist so ziemlich die Beschreibung von Nodejs, Libuv hat den gesamten Charakter von Node seit seiner Gründung definiert.

In einer Ereignisschleife registriert eine Anwendung einen Rückruf für ein Ereignis und wartet auf das Eintreten dieses Ereignisses, während die Schleife solange wiederholt wird, wie aktive Handles vorhanden sind. Dies ist für die E/A-Planung sehr effizient, da es nicht blockierend ist und daher viele Clients mit wenigen Threads verarbeiten kann. Es ist nicht das beste Paradigma für umfangreiche Berechnungen, da sie die Ereignisschleife blockieren und somit den Thread hängen lassen können.

Das Herzstück der Schleife ist die Funktion uv_run Innerhalb core.c. Der Zustand der Schleife ist eine als Referenz übergebene Struktur, uv_loop_t (definiert als uv_loop_s in der Header-Datei hier). Hier ist die while Schleife hält den Knoten am Leben:

Die obige Schleife kann durch das berühmte Diagramm aus der Nodejs-Dokumentation dargestellt werden:

Eine vereinfachte Übersicht über die Operationsreihenfolge der Ereignisschleife. Von Event-Loop-Timer-und-nextick

Hinweis: Ich kann die Schleifenstruktur nicht einfach drucken, wie wir es in JavaScript tun würden (zB: console.log(loop)). Dies liegt an der mangelnden „Reflexion“ der Sprache. Reflexion ist die Fähigkeit einer Sprache, ihre eigenen Strukturen zu sehen. Wir müssen jedes Feld selbst drucken. Wissen welche Schleife, die wir ausführen, können wir einfach ihre Zeigeradresse mit dem %p Platzhalter:

printf("uv_run loop: %p mode: %dn", loop, mode);

Lassen Sie uns die Protokollzeilen hinzufügen (ab Zeile 365) und kompilieren, indem wir Folgendes ausführen:

$ ./configure
$ make -j4

Das -j4 Option wird verursachen make um 4 gleichzeitige Kompilierungsjobs auszuführen, was die Buildzeit reduzieren kann. Es dauert immer noch etwa eine Stunde.

Kompilieren…

Sobald ich eine habe node Binärdatei kann ich sie mit Eingabe ausführen und sehen, wie die Ereignisschleifen ausgeführt werden. Bestehen der -e (bewerten) Flag können wir JavaScript einfach als String einbinden.

./node -e "console.log("hello node")"

Ergebnisse in:

uv_run loop: 0x7fbdd780d8e8 mode: UV_RUN_DEFAULT
hello node
uv_run loop: 0x111dda800 mode: UV_RUN_DEFAULT
uv_run loop: 0x111dda800 mode: UV_RUN_ONCE
uv_run loop: 0x111dda800 mode: UV_RUN_ONCE
uv_run loop: 0x7fbdd780d208 mode: UV_RUN_ONCE

Hinweis: Ich habe die int-Werte der ‘mode’-Enumeration für ihren Namen ersetzt.

Wir können sehen, wie sich die Ereignisschleife dreht. Nodejs verwendet die Standardschleife als Ereignisschleife.

Das nächste, was von Interesse ist, sind Griffe. Dies sind die eigentlichen Jobs, die die Schleife am Leben erhalten. Sie können unterschiedlichen Typs sein und werden der Warteschlange innerhalb der Schleifendatenstruktur hinzugefügt. Seine Initialisierungsfunktion ist in uv-common.h definiert als defined uv__handle_init. Dies ist eigentlich keine C-Funktion, sondern ein Makro, Code, der vom Compiler ersetzt werden soll. Ich füge dort eine Protokollzeile hinzu, sowie in uv__handle_start und uv__handle_stop.

Jetzt,

./node -e "console.log("hello node")"

Ausgänge:

uv__handle_init 0x7ffa1100d528 0x7ffa1100d208 UV_SIGNAL
uv__handle_init 0x7ffa1100d2d0 0x7ffa1100d208 UV_ASYNC
uv__handle_start 0x7ffa1100d2d0
uv__handle_init 0x7ffa1100d708 0x7ffa1100d208 UV_ASYNC
uv__handle_start 0x7ffa1100d708
uv__handle_init 0x7ffa11808608 0x7ffa118082e8 UV_SIGNAL
uv__handle_init 0x7ffa118083b0 0x7ffa118082e8 UV_ASYNC
uv__handle_start 0x7ffa118083b0
uv__handle_init 0x7ffa11808718 0x7ffa118082e8 UV_ASYNC
uv__handle_start 0x7ffa11808718
uv_run 0x7ffa118082e8, mode UV_RUN_DEFAULT
uv__handle_init 0x10ef80b20 0x10ef80800 UV_SIGNAL
uv__handle_init 0x10ef808c8 0x10ef80800 UV_ASYNC
uv__handle_start 0x10ef808c8
uv__handle_init 0x7ffa10d087e0 0x10ef80800 UV_ASYNC
uv__handle_start 0x7ffa10d087e0
uv__handle_init 0x10ef76ab8 0x10ef80800 UV_ASYNC
uv__handle_start 0x10ef76ab8
uv__handle_init 0x7ffa01018130 0x10ef80800 UV_TIMER
uv__handle_init 0x7ffa010181c8 0x10ef80800 UV_CHECK
uv__handle_init 0x7ffa01018240 0x10ef80800 UV_IDLE
uv__handle_start 0x7ffa010181c8
uv__handle_init 0x7ffa010182b8 0x10ef80800 UV_PREPARE
uv__handle_init 0x7ffa01018330 0x10ef80800 UV_CHECK
uv__handle_init 0x7ffa010183a8 0x10ef80800 UV_ASYNC
uv__handle_start 0x7ffa010183a8
uv__handle_start 0x7ffa010182b8
uv__handle_start 0x7ffa01018330
uv__handle_init 0x7ffa02305b30 0x10ef80800 UV_TTY
uv__handle_init 0x7ffa024044c8 0x10ef80800 UV_SIGNAL
uv__handle_start 0x7ffa024044c8
hello nodeuv_run 0x10ef80800, mode UV_RUN_DEFAULT
uv__handle_stop 0x7ffa02305b30 // UV_TTY
uv__handle_stop 0x7ffa024044c8 // UV_SIGNAL
uv__handle_stop 0x7ffa010181c8 // UV_CHECK
uv__handle_stop 0x7ffa010182b8 // UV_PREPARE
uv__handle_stop 0x7ffa01018330 // UV_CHECK
uv__handle_stop 0x7ffa010183a8 // UV_ASYNC
uv_run 0x10ef80800, mode UV_RUN_ONCE
uv__handle_stop 0x10ef76ab8 // UV_ASYNC
uv_run 0x10ef80800, mode UV_RUN_ONCE
uv__handle_stop 0x7ffa10d087e0 // UV_ASYNC
uv__handle_stop 0x7ffa11808718 // UV_ASYNC
uv__handle_stop 0x7ffa1100d708 // UV_ASYNC
uv_run 0x7ffa1100d208, mode UV_RUN_ONCE

Das ist ziemlich cool – wir können sogar einen Handle für „TTY“ sehen, den Terminal-Handle, der zum Drucken unserer . verwendet wird console.log.

Wissenswertes: TTY steht für Teletype, ein Unternehmen für Fernschreiber. Dies war die Konsole für Computer, bevor es Monitore gab.

Ich könnte weitermachen und das ausdrucken requests, die kleineren Arbeitseinheiten in Libuv, aber ich denke, das wäre an dieser Stelle zu viel.