Hallo, mein Name ist Dmitry Karlovsky und ich ... arbeitslos. Daher habe ich viel Freizeit, um Musik, Sport, Kreativität, Sprachen, JS-Konferenzen und Informatik zu spielen. Ich erzähle Ihnen von den neuesten Forschungsergebnissen auf dem Gebiet der halbautomatischen Aufteilung langer Berechnungen in kleine Quanten von mehreren Millisekunden, die zu einer Miniaturbibliothek $mol_fiber
. Aber lassen Sie uns zuerst die Probleme skizzieren, die wir lösen werden.

Dies ist eine Textversion der gleichnamigen Aufführung bei HolyJS 2018 Piter . Sie können es entweder als Artikel lesen oder in der Präsentationsoberfläche öffnen oder ein Video ansehen .
Problem: Geringe Reaktionsfähigkeit
Wenn wir stabile 60 Bilder pro Sekunde haben möchten, haben wir nur 16 mit einer Kleinigkeit von Millisekunden, um die ganze Arbeit zu erledigen, einschließlich dessen, was der Browser tut, um die Ergebnisse auf dem Bildschirm anzuzeigen.
Aber was ist, wenn wir länger fließen? Dann wird der Benutzer eine verzögerte Oberfläche beobachten, die die Animation und dergleichen der UX-Verschlechterung verhindert.

Problem: Kein Entkommen
Es kommt vor, dass das Ergebnis für uns nicht mehr interessant ist, während wir die Berechnungen durchführen. Zum Beispiel haben wir eine virtuelle Schriftrolle, die der Benutzer aktiv zieht, aber wir können nicht mithalten und den tatsächlichen Bereich nicht rendern, bis das vorherige Rendering die Kontrolle über die Verarbeitung von Benutzerereignissen zurückgibt.

Unabhängig davon, wie lange wir arbeiten, sollten wir im Idealfall weiterhin Ereignisse verarbeiten und jederzeit in der Lage sein, die begonnenen, aber noch nicht abgeschlossenen Arbeiten abzubrechen.
Ich bin schnell und ich weiß es
Aber was ist, wenn unsere Arbeit nicht eine, sondern mehrere, sondern ein Strom ist? Stellen Sie sich vor, Sie fahren mit Ihrem frisch gekauften gelben Lotus und fahren bis zum Bahnübergang. Wenn es kostenlos ist, können Sie es in Sekundenbruchteilen verschieben. Aber ..

Problem: Keine Parallelität
Wenn die Kreuzung von einem Kilometerzug besetzt ist, müssen Sie stehen und zehn Minuten warten, bis sie vorbeifährt. Nicht dafür hast du einen Sportwagen gekauft, oder?

Und wie cool wäre es, wenn dieser Zug in 10 Züge zu je 100 Metern aufgeteilt würde und zwischen ihnen mehrere Minuten Zeit wären, um durchzukommen! Sie würden dann nicht so spät sein.
Was sind nun die Lösungen für diese Probleme in der JS-Welt?
Lösung: Arbeiter
Das erste, was mir in den Sinn kommt: Lassen Sie uns einfach alle komplexen Berechnungen in einem separaten Thread zusammenfassen. Dazu haben wir einen Mechanismus für WebWorker.

Ereignisse aus dem UI-Stream werden an den Worker übergeben. Dort werden sie verarbeitet und Anweisungen, was und wie auf der Seite geändert werden soll, werden bereits zurückgegeben. Auf diese Weise speichern wir den UI-Stream vor einer großen Rechenschicht, aber nicht alle Probleme werden auf diese Weise gelöst, und zusätzlich werden neue hinzugefügt.
Arbeiter: Probleme: (De) Serialisierung
Die Kommunikation zwischen Streams erfolgt durch Senden von Nachrichten, die in einen Byte-Stream serialisiert, in einen anderen Stream übertragen und dort in Objekte analysiert werden. All dies ist viel langsamer als ein direkter Methodenaufruf innerhalb eines einzelnen Threads.

Arbeiter: Probleme: Nur asynchron
Nachrichten werden streng asynchron übertragen. Und dies bedeutet, dass einige Funktionen, die ich Sie bitte, nicht verfügbar sind. Beispielsweise können Sie den Aufstieg eines UI-Ereignisses von einem Worker nicht stoppen, da das Ereignis im UI-Thread zum Zeitpunkt des Starts des Handlers bereits seinen Lebenszyklus abschließt.

Arbeiter: Probleme: Begrenzte APIs
Die folgenden APIs stehen uns in den Workern nicht zur Verfügung.
- DOM, CSSOM
- Leinwand
- GeoLocation
- Geschichte & Ort
- HTTP-Anforderungen synchronisieren
- XMLHttpRequest.responseXML
- Fenster
Arbeiter: Probleme: Kann nicht storniert werden
Und wieder haben wir keine Möglichkeit, die Berechnungen im Woker zu stoppen.

Ja, wir können den gesamten Arbeiter stoppen, aber das wird alle darin enthaltenen Aufgaben stoppen.
Ja, Sie können jede Aufgabe in einem separaten Worker ausführen, dies ist jedoch sehr ressourcenintensiv.
Lösung: Faser reagieren
Sicherlich haben viele gehört, dass FaceBook React heldenhaft umschreibt und alle darin enthaltenen Berechnungen in eine Reihe kleiner Funktionen aufteilt, die von einem speziellen Scheduler gestartet werden.

Ich werde nicht auf Details seiner Implementierung eingehen, da dies ein separates großes Thema ist. Ich werde nur einige Funktionen erwähnen, aufgrund derer es möglicherweise nicht zu Ihnen passt.
Faser reagieren: Reaktion erforderlich
Wenn Sie Angular, Vue oder ein anderes Framework als React verwenden, ist React Fibre natürlich für Sie nutzlos.

React Fibre: Nur Rendern
Reagieren - deckt nur die Rendering-Ebene ab. Alle anderen Schichten der Anwendung bleiben ohne Quantisierung.

React Fibre speichert Sie nicht, wenn Sie beispielsweise einen großen Datenblock nach schwierigen Bedingungen filtern müssen.
React Fibre: Quantisierung ist deaktiviert
Trotz der behaupteten Unterstützung für die Quantisierung ist sie standardmäßig immer noch deaktiviert, da sie die Abwärtskompatibilität beeinträchtigt.

Die Quantisierung in React ist immer noch eine experimentelle Sache. Seid vorsichtig!
React Fibre: Debug ist Schmerz
Wenn Sie die Quantisierung aktivieren, stimmt Callstack nicht mehr mit Ihrem Code überein, was das Debuggen erheblich erschwert. Aber wir werden auf dieses Thema zurückkommen.

Lösung: Quantisierung
Versuchen wir, den React Fibre-Ansatz zu verallgemeinern, um die genannten Nachteile zu beseitigen. Wir möchten im Rahmen eines Streams bleiben, aber lange Berechnungen in kleine Quanten aufteilen, zwischen denen der Browser die bereits an der Seite vorgenommenen Änderungen rendern kann, und auf Ereignisse reagieren.

Oben sehen Sie eine lange Berechnung, die die ganze Welt um mehr als 100 ms gestoppt hat. Und von unten - die gleiche Berechnung, jedoch in Zeitscheiben von etwa 16 ms unterteilt, was durchschnittlich 60 Bilder pro Sekunde ergab. Da wir normalerweise nicht wissen, wie viel Zeit die Berechnungen in Anspruch nehmen werden, können wir sie nicht manuell im Voraus in 16 ms aufteilen. Daher benötigen wir eine Art Laufzeitmechanismus, der die Zeit misst, die zum Abschließen der Aufgabe benötigt wird, und wenn das Quantum überschritten wird, wodurch die Ausführung bis zum nächsten Animationsframe angehalten wird. Lassen Sie uns darüber nachdenken, welche Mechanismen wir haben, um solche angehaltenen Aufgaben hier zu implementieren.
Parallelität: Fasern - stapelbare Coroutinen
In Sprachen wie Go und D gibt es eine Redewendung wie "Coroutine mit Stapel", es ist auch eine "Faser" oder "Faser".
import { Future } from 'node-fibers' const one = ()=> Future.wait( future => setTimeout( future.return ) ) const two = ()=> one() + 1 const three = ()=> two() + 1 const four = ()=> three() + 1 Future.task( four ).detach()
Im Codebeispiel sehen Sie die one
Funktion, die die aktuelle Glasfaser anhalten kann, aber selbst eine vollständig synchrone Schnittstelle hat. Die two
, three
und four
Funktionen sind reguläre synchrone Funktionen, die nichts über Glasfaser wissen. In ihnen können Sie alle Funktionen von Javascript vollständig nutzen. Und schließlich führen wir in der letzten Zeile einfach die four
Funktionen in einer separaten Faser aus.
Die Verwendung von Fasern ist recht praktisch, aber um sie zu unterstützen, benötigen Sie Laufzeitunterstützung, die die meisten JS-Interpreter nicht haben. Für NodeJS gibt es jedoch eine native node-fibers
Erweiterung, die diese Unterstützung hinzufügt. Leider sind in keinem Browser Browser verfügbar.
Parallelität: FSM - stapellose Coroutinen
In Sprachen wie C # und jetzt JS werden "stapellose Coroutinen" oder "asynchrone Funktionen" unterstützt. Solche Funktionen sind eine Zustandsmaschine unter der Haube und wissen nichts über den Stapel. Daher müssen sie mit dem speziellen Schlüsselwort "async" gekennzeichnet werden, und Orte, an denen sie angehalten werden können, werden "erwartet".
const one = ()=> new Promise( done => setTimeout( done ) ) const two = async ()=> ( await one() ) + 1 const three = async ()=> ( await two() ) + 1 const four = async ()=> ( await three() ) + 1 four()
Da wir die Berechnung möglicherweise jederzeit verschieben müssen, müssen fast alle Funktionen in der Anwendung asynchron ausgeführt werden. Dies ist nicht nur die Komplexität des Codes, sondern wirkt sich auch stark auf die Leistung aus. Darüber hinaus unterstützen viele APIs, die Rückrufe akzeptieren, immer noch keine asynchronen Rückrufe. Ein auffälliges Beispiel ist die reduce
eines Arrays.
Parallelität: Halbfasern - Neustart
Versuchen wir, etwas Ähnliches wie Glasfaser zu tun, indem wir nur die Funktionen verwenden, die uns in jedem modernen Browser zur Verfügung stehen.
import { $mol_fiber_async , $mol_fiber_start } from 'mol_fiber/web' const one = ()=> $mol_fiber_async( back => setTimeout( back ) ) const two = ()=> one() + 1 const three = ()=> two() + 1 const four = ()=> three() + 1 $mol_fiber_start( four )
Wie Sie sehen können, wissen die Zwischenfunktionen nichts über Unterbrechungen - dies ist reguläres JS. Nur die one
Funktion kennt die Möglichkeit der Federung. Um die Berechnung abzubrechen, wirft sie einfach Promise
als Ausnahme. In der letzten Zeile führen wir die four
Funktionen in einer separaten Pseudofaser aus, die die darin ausgelösten Ausnahmen überwacht. Wenn Promise
eintrifft, abonniert es seine resolve
und startet die Faser neu.
Um zu zeigen, wie Pseudofasern funktionieren, schreiben wir einen kniffligen Code.

Stellen wir uns vor, dass die step
hier etwas in die Konsole schreibt und 20 ms lang andere harte Arbeit leistet. Die walk
Funktion ruft step
zweimal auf und protokolliert den gesamten Prozess. In der Mitte wird angezeigt, was jetzt in der Konsole angezeigt wird. Und rechts ist der Zustand des Pseudofaserbaums.
$ mol_fiber: keine Quantisierung
Lassen Sie uns diesen Code ausführen und sehen, was passiert.

Bisher ist alles einfach und offensichtlich. Der Pseudofaserbaum ist natürlich nicht beteiligt. Und alles wäre in Ordnung, aber dieser Code wird länger als 40 ms ausgeführt, was wertlos ist.
$ mol_fiber: zuerst zwischenspeichern
Lassen Sie uns beide Funktionen in einen speziellen Wrapper einwickeln, der sie in einer Pseudofaser ausführt, und sehen, was passiert.

Hierbei ist zu beachten, dass für jeden Ort, an dem die one
Funktion innerhalb der walk
Faser aufgerufen wird, eine separate Faser erstellt wurde. Das Ergebnis des ersten Aufrufs wurde zwischengespeichert, aber anstelle des zweiten wurde Promise
geworfen, da wir unsere Zeitscheibe erschöpft hatten.
$ mol_fiber: Cache Sekunde
Im ersten Frame wird das Promise
im nächsten automatisch aufgelöst, was zu einem Neustart der walk
.

Wie Sie sehen können, geben wir aufgrund des Neustarts erneut "start" und "first done" an die Konsole aus, aber "first begin" ist bereits weg, da es sich in der Glasfaser befindet und der Cache früher gefüllt wurde, weshalb der Handler mehr ist nicht angerufen. Wenn der Cache der walk
Faser gefüllt ist, werden alle eingebetteten Fasern zerstört, da die Ausführung sie niemals erreichen wird.
Warum wurde first begin
Drucken begonnen und first done
first begin
Drucken? Es geht nur um Idempotenz. console.log
- nicht idempotenter Vorgang, wie oft Sie ihn aufrufen, so oft wird der Konsole ein Eintrag hinzugefügt. Die Faser, die in einer anderen Faser ausgeführt wird, ist jedoch idempotent. Sie führt das Handle nur beim ersten Aufruf aus und gibt bei nachfolgenden Aufrufen sofort das Ergebnis aus dem Cache zurück, ohne dass zusätzliche Nebenwirkungen auftreten.
$ mol_fiber: Idempotenz zuerst
Lassen Sie uns console.log
in eine Glasfaser einwickeln, um sie idempotent zu machen, und sehen, wie sich das Programm verhält.

Wie Sie sehen können, haben wir jetzt im Faserbaum Einträge für jeden Aufruf der log
.
$ mol_fiber: zweite Idempotenz
Beim nächsten Neustart der walk
Fiber führen wiederholte Aufrufe der log
nicht mehr zu Aufrufen der realen console.log
, aber sobald wir zur Ausführung der Fibers mit einem leeren Cache gelangen, werden die Aufrufe der console.log
fortgesetzt.

Bitte beachten Sie, dass in der Konsole jetzt nichts Überflüssiges angezeigt wird - genau das, was im synchronen Code ohne Faser und Quantifizierung angezeigt würde.
$ mol_fiber: Pause
Wie unterbricht die Berechnung? Zu Beginn des Quantums wird eine Frist festgelegt. Und bevor jede Faser gestartet wird, wird geprüft, ob wir sie erreicht haben. Und wenn Sie erreichen, dann eilt Promise
, das im nächsten Frame aufgelöst wird und ein neues Quantum startet.
if( Date.now() > $mol_fiber.deadline ) { throw new Promise( $mol_fiber.schedule ) }
$ mol_fiber: Frist
Die Frist für das Quantum ist einfach festzulegen. Zur aktuellen Zeit werden 8 Millisekunden hinzugefügt. Warum genau 8, weil es bis zu 16 gibt, um den Schuss vorzubereiten? Tatsache ist, dass wir nicht im Voraus wissen, wie lange der Browser rendern muss, sodass wir etwas Zeit einplanen müssen, damit er funktioniert. Aber manchmal kommt es vor, dass der Browser nichts rendern muss, und dann können wir mit 8-ms-Quanten ein weiteres Quantum in denselben Frame einfügen, was eine dichte Packung von Quanten mit minimalen Prozessorausfallzeiten ergibt.
const now = Date.now() const quant = 8 const elapsed = Math.max( 0 , now - $mol_fiber.deadline ) const resistance = Math.min( elapsed , 1000 ) / 10
Wenn wir jedoch alle 8 ms eine Ausnahme auslösen, wird das Debuggen mit aktiviertem Ausnahmestopp zu einem kleinen Zweig der Hölle. Wir brauchen einen Mechanismus, um diesen Debugger-Modus zu erkennen. Leider kann dies nur indirekt verstanden werden: Eine Person braucht ungefähr eine Sekunde, um zu verstehen, ob sie die Ausführung fortsetzen soll oder nicht. Dies bedeutet, dass entweder das Debugger gestoppt wurde oder eine umfangreiche Berechnung durchgeführt wurde, wenn das Steuerelement längere Zeit nicht zum Skript zurückkehrte. Um auf beiden Stühlen zu sitzen, addieren wir 10% der verstrichenen Zeit zum Quantum, jedoch nicht mehr als 100 ms. Dies hat keinen großen Einfluss auf die FPS, verringert jedoch die Stoppfrequenz des Debuggers aufgrund der Quantisierung um eine Größenordnung.
Debug: versuchen / fangen
Was denken Sie, an welcher Stelle dieses Codes stoppt der Debugger, da wir über das Debuggen sprechen?
function foo() { throw new Error( 'Something wrong' ) // [1] } try { foo() } catch( error ) { handle( error ) throw error // [2] }
In der Regel muss er dort anhalten, wo die Ausnahme zum ersten Mal ausgelöst wird. In Wirklichkeit stoppt er jedoch nur dort, wo sie das letzte Mal ausgelöst wurde, was normalerweise sehr weit von dem Ort entfernt ist, an dem sie aufgetreten ist. Um das Debuggen nicht zu erschweren, sollten Ausnahmen daher niemals durch Try-Catch abgefangen werden. Aber auch ohne Ausnahmebehandlung ist es unmöglich.
Debug: nicht behandelte Ereignisse
In der Regel stellt eine Laufzeit ein globales Ereignis bereit, das für jede nicht erfasste Ausnahme auftritt.
function foo() { throw new Error( 'Something wrong' ) } window.addEventListener( 'error' , event => handle( event.error ) ) foo()
Zusätzlich zur Umständlichkeit hat diese Lösung einen solchen Nachteil, dass alle Ausnahmen hier fallen und es ziemlich schwierig ist zu verstehen, von welcher Faser und Faser das Ereignis aufgetreten ist.
Debug: Versprechen
Versprechen sind der beste Weg, um mit Ausnahmen umzugehen.
function foo() { throw new Error( 'Something wrong' ) } new Promise( ()=> { foo() } ).catch( error => handle( error ) )
Die an Promise übergebene Funktion wird sofort synchron aufgerufen, aber die Ausnahme wird nicht abgefangen und stoppt den Debugger sicher an der Stelle seines Auftretens. Wenig später wird asynchron bereits der Fehlerhandler aufgerufen, in dem wir genau wissen, welche Glasfaser den Fehler verursacht hat und welcher Fehler. Dies ist genau der Mechanismus, der in $ mol_fiber verwendet wird.
Stapelspur: Faser reagieren
Werfen wir einen Blick auf die Stapelverfolgung, die Sie in React Fibre erhalten.

Wie Sie sehen können, bekommen wir viel Darmreaktion. Von dem hier nützlichen sind nur der Punkt des Auftretens der Ausnahme und die Namen der Komponenten in der Hierarchie höher. Nicht viel.
Stapelverfolgung: $ mol_fiber
In $ mol_fiber erhalten wir eine viel nützlichere Stapelverfolgung: keine Eingeweide, nur bestimmte Punkte im Anwendungscode, durch die es zu einer Ausnahme kam.

Dies wird durch die Verwendung des nativen Stapels, Versprechen und die automatische Entfernung des Darms erreicht. Wenn Sie möchten, können Sie den Fehler in der Konsole wie im Screenshot erweitern und die Eingeweide sehen, aber es gibt nichts Interessantes.
$ mol_fiber: behandeln
Um ein Quantum zu unterbrechen, wird Promise geworfen.
limit() { if( Date.now() > $mol_fiber.deadline ) { throw new Promise( $mol_fiber.schedule ) }
Aber wie Sie sich vorstellen können, kann Promise absolut alles sein - für eine Glasfaser spielt es im Allgemeinen keine Rolle, was zu erwarten ist: der nächste Frame, der Abschluss des Datenladens oder etwas anderes.
fail( error : Error ) { if( error instanceof Promise ) { const listener = ()=> self.start() return error.then( listener , listener ) }
Fibre abonniert einfach, um Versprechen zu lösen und neu zu starten. Das manuelle Werfen und Fangen von Versprechungen ist jedoch nicht erforderlich, da das Paket mehrere nützliche Wrapper enthält.
$ mol_fiber: Funktionen
Um eine synchrone Funktion in eine idempotente Faser zu verwandeln, wickeln Sie sie einfach in $mol_fiber_func
.
import { $mol_fiber_func as fiberize } from 'mol_fiber/web' const log = fiberize( console.log ) export const main = fiberize( ()=> { log( getData( 'goo.gl' ).data ) } )
Hier haben wir console.log
idempotent gemacht und main
gelernt zu unterbrechen, während wir auf den Download warten.
$ mol_fiber: Fehlerbehandlung
Aber wie soll man auf Ausnahmen reagieren, wenn wir try-catch
nicht verwenden wollen? Dann können wir den Fehlerhandler mit $mol_fiber_catch
...
import { $mol_fiber_func as fiberize , $mol_fiber_catch as onError } from 'mol_fiber' const getConfig = fiberize( ()=> { onError( error => ({ user : 'Anonymous' }) ) return getData( '/config' ).data } )
Wenn wir etwas anderes als den darin enthaltenen Fehler zurückgeben, ist dies das Ergebnis der aktuellen Faser. In diesem Beispiel gibt die Funktion getConfig
die Konfiguration standardmäßig zurück, wenn es nicht möglich ist, die Konfiguration vom Server herunterzuladen.
$ mol_fiber: Methoden
Natürlich können Sie nicht nur Funktionen, sondern auch Methoden mit einem Dekorateur umschließen.
import { $mol_fiber_method as action } from 'mol_fiber/web' export class Mover { @action move() { sendData( 'ya.ru' , getData( 'goo.gl' ) ) } }
Hier haben wir beispielsweise Daten von Google hochgeladen und auf Yandex hochgeladen.
$ mol_fiber: verspricht
Um Daten vom Server herunterzuladen, reicht es beispielsweise aus, die asynchrone Funktion fetch
und mit einem Handgriff synchron zu machen.
import { $mol_fiber_sync as sync } from 'mol_fiber/web' export const getData = sync( fetch )
Diese Implementierung ist für alle gut, unterstützt jedoch nicht das Abbrechen einer Anforderung, wenn ein Faserbaum zerstört wird. Daher müssen wir eine verwirrendere API
.
$ mol_fiber: Anfrage abbrechen
import { $mol_fiber_async as async } from 'mol_fiber/web' function getData( uri : string ) : Response { return async( back => { var controller = new AbortController(); fetch( uri , { signal : controller.signal } ).then( back( res => res ) , back( error => { throw error } ) , ) return ()=> controller.abort() } ) }
Die an den async
Wrapper übergebene Funktion wird nur einmal aufgerufen, und der back
Wrapper wird an ihn übergeben, in dem Sie die Rückrufe umbrechen müssen. Dementsprechend müssen Sie in diesen Rückrufen entweder den Wert zurückgeben oder eine Ausnahme auslösen. Was auch immer das Ergebnis des Rückrufs ist, es wird auch das Ergebnis der Faser sein. Bitte beachten Sie, dass wir am Ende eine Funktion zurückgeben, die bei vorzeitiger Zerstörung der Faser aufgerufen wird.
$ mol_fiber: Antwort abbrechen
Auf der Serverseite kann es auch nützlich sein, die Berechnung abzubrechen, wenn der Client heruntergefallen ist. Implementieren wir einen Wrapper über midleware
, der eine Faser erstellt, in der die ursprüngliche midleware
wird. , , , .
import { $mol_fiber_make as Fiber } from 'mol_fiber' const middle_fiber = middleware => ( req , res ) => { const fiber = Fiber( ()=> middleware( req , res ) ) req.on( 'close' , ()=> fiber.destructor() ) fiber.start() } app.get( '/foo' , middle_fiber( ( req , res ) => { // do something } ) )
$mol_fiber: concurrency
, . , 3 : , , - ..

: , . . , , .
$mol_fiber: properties
, ..
Pros:
- Runtime support isn't required
- Can be cancelled at any time
- High FPS
- Concurrent execution
- Debug friendly
- ~ 3KB gzipped
Cons:
- Instrumentation is required
- All code should be idempotent
- Longer total execution
$mol_fiber — , . — , . , , . , , , , . , . .
Links
Call back

: , , )
: , .
: . , .
: . , . , .
: , . , )
: , .
: - . , , .
: . , , .
: , . 16ms, ? 16 8 , 8, . , . , «».
: — . Vielen Dank!
: . , . Liebte es!
: , . .
: , , , , , / , .
: , .
: .
: , . mol.
: , , . , , , .
: .
: , . , $mol, , .
: , , . — . .
: - , .
: $mol , . (pdf, ) , .
: , . , .
: , ) .
: . .
: In some places I missed what the reporter was saying. The conversation was about how to use the "Mola" library and "why?". But how it works remains a mystery for me.To smoke an source code is for the overhead.
: , .
: . , . . .
: : . - (, ). , : 16?
: . . , mol_fiber … , 30fps 60fps — . — .