Eine Geschichte über das Lösen des Leistungsproblems von Moment.j.

Moment.js ist eine der beliebtesten JavaScript-Bibliotheken zum Parsen und Formatieren von Daten. WhereTo verwendet Node.js, daher war die Verwendung dieser Bibliothek für sie ein völlig natürlicher Schritt. Probleme mit der Servernutzung von Moment.js wurden nicht erwartet. Am Ende nutzten sie diese Bibliothek von Anfang an, um Daten anzuzeigen, und waren mit ihrer Arbeit zufrieden. Die Tatsache, dass die Bibliothek auf dem Client eine gute Leistung erbrachte, bedeutete jedoch nicht, dass es auch keine Probleme mit dem Server geben würde.



Das Material, dessen Übersetzung wir heute veröffentlichen, ist der Geschichte der Lösung des Leistungsproblems Moment.js gewidmet.

Projektwachstum und Produktivitätsrückgang


In letzter Zeit hat sich die Anzahl der vom WhereTo-System zurückgegebenen Flugaufzeichnungen um das Zehnfache erhöht. Dann mussten wir einen sehr starken Leistungsabfall hinnehmen. Es stellte sich heraus, dass der Renderzyklus, der weniger als 100 Millisekunden dauerte, jetzt mehr als 3 Sekunden dauert, um etwa 5.000 Suchergebnisse anzuzeigen. Unser Team hat mit der Forschung begonnen. Nach mehreren Profiling-Sitzungen haben wir festgestellt, dass mehr als 99% dieser Zeit in einer einzigen Funktion namens createInZone .


Die Funktion createInZone dauert ca. 3,3 Sekunden.

Als wir unsere Untersuchung der Situation fortsetzten, stellten wir fest, dass diese Funktion von der parseZone Funktion Moment.js parseZone wird. Warum ist sie so langsam? Wir hatten das Gefühl, dass die Moment.js-Bibliothek für allgemeine Anwendungsfälle entwickelt wurde und daher versucht, die Eingabezeichenfolge auf verschiedene Arten zu verarbeiten. Vielleicht solltest du es einschränken? Nachdem wir die Dokumentation gelesen hatten, stellten wir fest, dass die parseZone Funktion ein optionales Argument akzeptiert, das das parseZone angibt:

 moment.parseZone(input, [format]) 

Das erste, was wir getan haben, war zu versuchen, die parseZone Funktion zu verwenden, um Informationen über das Datumsformat an dieses zu übergeben, aber dies führte, wie die Leistungstests zeigten, zu nichts:

 $ node bench.js moment#parseZone x 22,999 ops/sec ±7.57% (68 runs sampled) moment#parseZone (with format) x 30,010 ops/sec ±8.09% (77 runs sampled) 

Obwohl die parseZone Funktion jetzt etwas schneller arbeitete, reichte diese Geschwindigkeit für unsere Anforderungen eindeutig nicht aus.

Projektspezifische Optimierung


Wir haben Moment.js verwendet, um Daten zu analysieren, die von der API unseres Anbieters (Travelport) abgerufen wurden. Wir haben festgestellt, dass Daten immer im gleichen Format zurückgegeben werden:

 "2019-12-03T14:05:00.000-07:00" 

In diesem Wissen begannen wir, die interne Struktur von Moment.js zu verstehen, um (wie wir gehofft hatten) eine viel effizientere Funktion zu schreiben, die dieselben Ergebnisse liefert.

Erstellen einer schnelleren Alternative zu parseZone


Um loszulegen, mussten wir herausfinden, wie die Moment.js-Objekte aussehen. Es war ziemlich leicht zu verstehen:

 > const m = moment() > console.log(m) Moment {  _isAMomentObject: true,  _i: '2019-12-03T14:05:00.000-07:00',  _f: 'YYYY-MM-DDTHH:mm:ss.SSSSZ',  _tzm: -420,  _isUTC: true,  _pf: { ...snip },  _locale: [object Locale],  _d: 2019-12-03T14:05:00.000Z,  _isValid: true,  _offset: -420 } 

Der nächste Schritt bestand darin, Moment ohne Verwendung eines Konstruktors zu instanziieren:

 export function parseTravelportTimestamp(input: string) {  const m = {}  // $FlowIgnore  m.__proto__ = moment.prototype  return m } 

Nun schien es, als hätten wir viele Eigenschaften der Moment-Instanz, die wir einfach festlegen konnten (ich gehe nicht auf die Details ein, wie wir davon erfahren haben, aber wenn Sie sich den Quellcode von Moment.js ansehen, werden Sie verstehen):

 const FAKE = moment() const TRAVELPORT_FORMAT = 'YYYY-MM-DDTHH:mm:ss.SSSSZ' export function parseTravelportTimestamp(input: string) {  const m = {}  // $FlowIgnore  m.__proto__ = moment.prototype  const offset = 0 // TODO  const date = new Date(input.slice(0, 23))  m._isAMomentObject = true  m._i = input  m._f = TRAVELPORT_FORMAT  m._tzm = offset  m._isUTC = true  m._locale = FAKE._locale  m._d = date  m._isValid = true  m._offset = offset  return m } 

Der letzte Schritt unserer Arbeit bestand darin, herauszufinden, wie der offset des Zeitstempels analysiert werden kann. Es stellte sich heraus, dass dies immer die gleiche Position in der Linie ist. Dadurch konnten wir Folgendes optimieren:

 function parseTravelportDateOffset(input: string) {  const hrs = +input.slice(23, 26)  const mins = +input.slice(27, 29)  return hrs * 60 + (hrs < 0 ? -mins : mins) } 

Folgendes ist passiert, nachdem wir alles zusammengestellt haben:

 const FAKE = moment() const TRAVELPORT_FORMAT = 'YYYY-MM-DDTHH:mm:ss.SSSSZ' function parseTravelportDateOffset(input: string) {  const hrs = +input.slice(23, 26)  const mins = +input.slice(27, 29)  return hrs * 60 + (hrs < 0 ? -mins : mins) } /** *     ISO-8601,    : * - "2019-12-03T12:30:00.000-07:00" */ export function parseTravelportTimestamp(input: string): moment {  const m = {}  // $FlowIgnore  m.__proto__ = moment.prototype  const offset = parseTravelportDateOffset(input)  const date = new Date(input.slice(0, 23))  m._isAMomentObject = true  m._i = input  m._f = TRAVELPORT_FORMAT  m._tzm = offset  m._isUTC = true  m._locale = FAKE._locale  m._d = date  m._isValid = true  m._offset = offset  return m } 

Leistungstests


Wir haben die Leistung der resultierenden Lösung mit dem Benchmark-npm-Modul getestet. Hier ist der Benchmark-Code:

 const FAKE = moment() const TRAVELPORT_FORMAT = 'YYYY-MM-DDTHH:mm:ss.SSSSZ' function parseTravelportDateOffset(input: string) {  const hrs = +input.slice(23, 26)  const mins = +input.slice(27, 29)  return hrs * 60 + (hrs < 0 ? -mins : mins) } /** *     ISO-8601,    : * - "2019-12-03T12:30:00.000-07:00" */ export function parseTravelportTimestamp(input: string): moment {  const m = {}  // $FlowIgnore  m.__proto__ = moment.prototype  const offset = parseTravelportDateOffset(input)  const date = new Date(input.slice(0, 23))  m._isAMomentObject = true  m._i = input  m._f = TRAVELPORT_FORMAT  m._tzm = offset  m._isUTC = true  m._locale = FAKE._locale  m._d = date  m._isValid = true  m._offset = offset  return m } 

Hier sind die Ergebnisse unserer Leistungsforschung:

 $ node fastMoment.bench.js moment#parseZone x 21,063 ops/sec ±7.62% (73 runs sampled) moment#parseZone (with format) x 24,620 ops/sec ±6.11% (71 runs sampled) fast#parseTravelportTimestamp x 1,357,870 ops/sec ±5.24% (79 runs sampled) Fastest is fast#parseTravelportTimestamp 

Wie sich herausstellte, konnten wir die Analyse von Zeitstempeln um das 64-fache beschleunigen. Aber wie hat sich das auf den tatsächlichen Betrieb des Systems ausgewirkt? Folgendes ist als Ergebnis der Profilerstellung passiert.


Die Gesamtausführungszeit von parseTravelportTimestamp beträgt weniger als 40 ms.

Die Ergebnisse waren einfach unglaublich: Wir begannen mit 3,3 Sekunden, gingen in Parsing-Daten und kamen auf weniger als 40 Millisekunden.

Zusammenfassung


Als wir anfingen, an unserer Plattform zu arbeiten, mussten wir nur eine schreckliche Menge von Problemen lösen. Wir wussten nur, dass wir uns wiederholten: „Lass es zuerst funktionieren, aber du kannst später optimieren“.

In den letzten Jahren ist die Komplexität unseres Projekts enorm gewachsen. Zum Glück sind wir jetzt an dem Ort angelangt, an dem wir zum zweiten Teil unseres „Mantras“ übergehen können - der Optimierung.

Bibliothekslösungen halfen dem Projekt, dorthin zu gelangen, wo es heute ist. Wir stehen jedoch vor einem "Bibliotheks" -Problem. Bei der Lösung haben wir gelernt, dass wir durch die Schaffung eines eigenen Mechanismus, der auf unsere Bedürfnisse ausgerichtet ist, den Code im Hinblick auf den Verbrauch von Systemressourcen „einfacher“ machen und wertvolle Zeit für unsere Benutzer sparen können.

Liebe Leser! Haben Sie ähnliche Probleme wie in diesem Artikel festgestellt?


Source: https://habr.com/ru/post/de468411/


All Articles