منذ وقت ليس ببعيد ، واجهت مهمة بسيطة إلى حد ما وفي نفس الوقت مثيرة للاهتمام: تنفيذ محطة للقراءة فقط في تطبيق ويب. تم إعطاء الاهتمام بالمهمة من خلال ثلاثة جوانب مهمة:
- دعم تسلسل هروب ANSI الأساسي
- دعم ما لا يقل عن 50،000 خطوط البيانات
- عرض البيانات عندما تصبح متاحة.
سوف أتحدث في هذا المقال عن كيفية تنفيذها وكيف نجحت في تحسينها.
إخلاء المسئولية: لست مطور ويب ذي خبرة ، لذلك قد تبدو بعض الأشياء واضحة لك ، والاستنتاجات أو القرارات خاطئة. عن التصحيحات والإيضاحات ، سأكون ممتنا.
لماذا كان الأمر متروكًا
المهمة بأكملها هي كما يلي: يتم تشغيل البرنامج النصي على الخادم (bash ، python ، وما إلى ذلك) ويكتب شيئًا ما ليقدمه. ويجب عرض هذا الاستنتاج على صفحة الويب فور وصولها. في الوقت نفسه ، ينبغي أن يبدو على الجهاز (مع التنسيق ، ونقل المؤشر ، وما إلى ذلك)
لا أتحكم في البرنامج النصي نفسه وإخراجه بأي شكل من الأشكال وأعرضه بشكل خالص.
بالطبع ، بين واجهة الويب والبرنامج النصي يجب أن يكون هناك وسيط - خادم الويب. وإذا لم يكن لتفكيك - لدي بالفعل تطبيق ويب وخادم ، والعمل بطريقة أو بأخرى. يبدو المخطط مثل هذا:

ولكن قبل ذلك ، كان الخادم مسؤولاً عن المعالجة والتنسيق. وأردت تحسينه لعدد كبير من الأسباب:
- معالجة البيانات المزدوجة - أول تحليل على الخادم ، ثم التحول إلى مكونات HTML على العميل
- الخوارزمية غير المثالية بسبب إعداد البيانات للعميل
- تحميل كثيف على الخادم - معالجة الإخراج من برنامج نصي واحد يمكن تحميل مؤشر ترابط واحد بالكامل على الخادم
- دعم غير كامل لتسلسلات ANSI Escape
- البق خفية
- كان أداء العميل ضعيفًا جدًا مع عرض خطوط منسقة حتى 10 آلاف
لذلك ، تقرر نقل منطق التحليل بالكامل إلى تطبيق الويب ، وترك بيانات التدفق الأولية فقط إلى الخادم
بيان المشكلة
أجزاء من النص تأتي إلى العميل. يجب على العميل تحليلها إلى مكونات: نص عادي ، سطر تغذية ، حرف إرجاع ، وأوامر ANSI خاصة. لا توجد ضمانات في سلامة الأجزاء - يمكن أن يأتي أمر واحد أو كلمة في مجموعات مختلفة.
يمكن أن تؤثر أوامر ANSI على تنسيق النص (اللون ، الخلفية ، النمط) ، موضع المؤشر (حيث يجب عرض النص التالي من) ، أو لمسح جزء من الشاشة.
مثال على ما يبدو عليه:

بالإضافة إلى ذلك ، قد يكون هناك عناوين URL بين النص تحتاج أيضًا إلى التعرف عليها وتسليط الضوء عليها.
نأخذ المكتبة النهائية و ...
فهمت أن المعالجة الصحيحة والسريعة لجميع الأوامر ليست مهمة سهلة. لذلك ، قررت البحث عن مكتبة جاهزة. وها ، أنا تعثرت على الفور على xterm.js . مكون جاهز للمحطة ، والذي يستخدم بالفعل في العديد من الأماكن ، وبالإضافة إلى ذلك ، "سريع للغاية ، حتى أنه يشمل عارض تسريع GPU" . هذا الأخير كان الأهم بالنسبة لي ، لأنه أردت أخيرًا أن أحصل على عميل سريع جدًا.
على الرغم من أنني أحبّ أن أكتب دراجاتي الخاصة ، إلا أنني كنت سعيدًا جدًا لأنني لا أستطيع توفير الوقت فحسب ، ولكن أيضًا الحصول على مجموعة من الوظائف المفيدة مجانًا.
استغرق الأمر مني الساعة 2 مساءً لمحاولة توصيل الجهاز ولم أستطع التعامل معه. تماما.
ارتفاعات مختلفة للخطوط ، اختيار ملتوي ، حجم متكيف للمحطة ، واجهة برمجة تطبيقات غريبة جدًا ، عدم وجود وثائق عاقلة ...
لكن ما زلت لدي القليل من الإلهام وأعتقد أنه يمكنني التعامل مع هذه المشاكل.
حتى أطعمت اختبار خطوط 10K بلدي إلى المحطة ... مات. ودفن معي بقايا آمالي.
وصف الخوارزمية النهائية
بادئ ذي بدء ، قمت بنسخ الخوارزمية الحالية المطبقة في بيثون وتكييفها لجافا سكريبت (فقط إزالة الأقواس المتعرجة والآخر لبناء الجملة).
كنت أعرف جميع إيجابيات وسلبيات الخوارزمية القديمة ، لذلك كنت بحاجة فقط لتحسين الأماكن غير الفعالة فيها.
بعد المداولة والتجربة والخطأ ، استقرت على الخيار التالي: نقسم الخوارزمية إلى مكونين:
- نموذج لتحليل النص وتخزين الحالة الحالية لـ "المحطة الطرفية"
- تعيين يترجم النموذج إلى HTML
نموذج (الهيكل والخوارزمية)
- يتم تخزين جميع الصفوف في صفيف (رقم الصف = فهرس في الصفيف)
- يتم تخزين أنماط النص في مجموعة منفصلة.
- يتم تخزين موضع المؤشر الحالي ويمكن تغييره بواسطة الأوامر
- تقوم الخوارزمية نفسها بالتحقق من حرف إدخال البيانات حسب الحرف:
- إذا كان هذا مجرد نص ، فأضفه إلى السطر الحالي
- إذا كسر السطر ، ثم زيادة مؤشر الصف الحالي
- إذا كان هذا أحد رموز الأوامر ، فسنضعه في مخزن الأوامر مؤقتًا وننتظر الحرف التالي
- إذا كان المخزن المؤقت للأمر صحيحًا ، فقم بتشغيل هذا الأمر ، وإلا فإننا نكتب هذا المخزن المؤقت كنص
- يقوم النموذج بإخطار المستمعين حول الأسطر التي تغيرت بعد معالجة النصوص الواردة
في تنفيذي ، يكون تعقيد الخوارزمية هو O ( n log n ) ، حيث log n هي إعداد الأسطر التي تم تغييرها للإعلام (التفرد والفرز). في وقت كتابة هذا التقرير ، أدركت أنه بالنسبة لحالة خاصة ، يمكنك التخلص من log n ، نظرًا لأن الخطوط يتم إضافتها غالبًا إلى النهاية.
عرض
- يعرض النص كعناصر HTML
- إذا تم تغيير السلسلة ، يستبدل كل عناصر السلسلة تمامًا
- يكسر كل سطر بناءً على الأنماط: لكل قطعة منمقة عنصرها الخاص
مع مثل هذا الهيكل ، يعد الاختبار مهمة بسيطة إلى حد ما - فنحن ننقل النص إلى النموذج (في حزمة واحدة أو في أجزاء) ونتحقق من الحالة الحالية لجميع الخطوط والأنماط الموجودة فيها. ولعرض عدد قليل من الاختبارات ، لأن دائما يعيد رسم الخطوط التي تم تغييرها.
ميزة هامة هي أيضا بعض الكسل من الشاشة. إذا قمنا في نص واحد بالكتابة فوق نفس السطر (على سبيل المثال ، شريط التقدم) ، ثم بعد أن يعمل النموذج ، لعرضه سيبدو كخط واحد تم تغييره.
دوم مقابل قماش
أود أن أتحدث قليلاً عن سبب اختيار DOM ، على الرغم من أن الهدف كان الأداء. الجواب بسيط - الكسل. بالنسبة لي ، يبدو أن جعل كل شيء في Canvas لوحدي مهمة شاقة للغاية. مع الحفاظ على قابلية الاستخدام: تسليط الضوء على الشاشة ونسخها وتغيير حجمها وتبدو أنيقة وغير ذلك. المثال xterm.js أظهر لي بوضوح أن هذا ليس سهلاً على الإطلاق. كان تجسيدهم للقماش أبعد ما يكون عن المثالية.
بالإضافة إلى ذلك ، يعد تصحيح شجرة DOM في المستعرض والقدرة على تغطية اختبارات الوحدة ميزة هامة.
في النهاية ، كان هدفي هو 50 ألف خطًا ، وكنت أعرف أن DOM يجب أن تتعامل مع هذا ، استنادًا إلى عمل الخوارزمية القديمة.
الأمثل
كانت الخوارزمية جاهزة ومصححة وعملت ببطء ولكن بثبات. لقد حان الوقت لفتح الملف التعريفي وتحسينه. بالنظر إلى المستقبل ، أقول إن معظم التحسينات كانت مفاجأة لي (كما يحدث عادة).
تم إجراء التنميط على 10 كيلو خطوط ، يحتوي كل منها على عناصر منمقة. إجمالي عدد عناصر DOM حوالي 100 كيلو.
لم يتم استخدام أساليب وأدوات خاصة. أدوات تطوير Chrome فقط واثنين من عمليات الإطلاق لكل قياس. في الممارسة العملية ، في عمليات الإطلاق ، تختلف القيم المطلقة للقياسات (عدد الثواني المراد إكمالها) ، ولكن ليس النسبة المئوية بين الطرق. لذلك ، أنا أعتبر هذه التقنية كافية مشروط.
أدناه ، أود أن أتناول بالتفصيل مزيدًا من التفاصيل حول التحسينات الأكثر إثارة للاهتمام. بالنسبة للمبتدئين ، رسم بياني لما كان:

تم بناء جميع الرسومات التعريف بعد التنفيذ ، من خلال deoptimizing رمز من الذاكرة.
string.trim
بادئ ذي بدء ، صادفت سلسلة غير مفهومة. استهلك كمية ملحوظة للغاية من وحدة المعالجة المركزية (يبدو لي أن هذا كان حوالي 10-20 ٪)

تقليم () هي الوظيفة الأساسية للغة. لماذا هو استخدام نوع من المكتبة؟ وحتى لو كان نوعًا من polyfill ، فلماذا يتم تشغيل أحدث إصدار من chrome؟
تم العثور على googling قليلاً والإجابة: https://babeljs.io/docs/en/babel-preset-env . بشكل افتراضي ، فإنه يُمكّن polyfill لعدد كبير إلى حد ما من المتصفحات ، ويفعل ذلك في مرحلة الترجمة. كان الحل بالنسبة لي هو تحديد 'targets': '> 0.25%, not dead'
لكن في النهاية ، حذفت استدعاء القطع تمامًا ، لأنه غير ضروري.
Vue.js
في العام الماضي ، قمت بنقل المكون الطرفي إلى Vue.js. الآن اضطررت إلى نقلها مرة أخرى إلى الفانيليا ، والسبب في لقطة الشاشة أدناه (راجع عدد الخطوط التي تتضمن Vue.js):

تركت فقط المجمع ، والأساليب ومعالجة الماوس في مكون Vue. ذهب كل ما يتعلق بإنشاء عناصر DOM إلى JS خالص ، وهو متصل بمكون Vue كحقل عادي (لا تتم مراقبته بواسطة الإطار).
created() { this.terminalModel = new TerminalModel(); this.terminal = new Terminal(this.terminalModel); },
أنا لا أعتبر هذا ناقصًا أو عيبًا في Vue.js. إنه فقط أن الأطر والأداء أنفسهم لا يختلطان جيدًا. حسنًا ، عندما تقوم بإسقاط عشرات ومئات الآلاف من الكائنات في إطار تفاعلي ، يكون من الصعب للغاية توقع المعالجة منه في غضون بضع ميلي ثانية. ولكي أكون صادقًا ، فقد فوجئت أيضًا بأن Vue.js كان جيدًا.
إضافة عناصر جديدة
كل شيء بسيط هنا - إذا كان لديك عدة آلاف من العناصر الجديدة وتريد إضافتها إلى المكون الأصل ، فإن إجراء appendChild ليس فكرة جيدة. يجب أن يقوم المتصفح بمعالجة مرات أكثر قليلاً وقضاء وقت أطول في العرض. كان واحدا من الآثار الجانبية في حالتي تباطؤ في autoscroll ، كما يفرض إعادة فرز جميع المكونات المضافة.

لحل المشكلة ، هناك DocumentFragment. أولاً ، نضيف جميع العناصر إليها ، ثم نضيفها إلى المكون الأصل. سوف يعتني المتصفح المضمنة للمكونات الواردة.
يقلل هذا النهج من مقدار الوقت الذي يقضيه المتصفح في تقديم العناصر وترتيبها.
كما جربت طرقًا أخرى لتسريع إضافة العناصر. لا يمكن لأي منهم إضافة أي شيء أعلى DocumentFragment.
تمتد مقابل div
في الواقع ، يمكن أن يسمى هذا display:inline
(span) مقابل display:block
(div).
في البداية ، كان لدي كل سطر في النطاق وانتهي بحرف فاصل الأسطر. ومع ذلك ، من حيث الأداء ، هذا غير فعال للغاية: يجب على المتصفح معرفة أين يبدأ العنصر وينتهي. مع عرض: كتلة ، هذه الحسابات هي أبسط بكثير.
استبدال مع div تسريع التقديم بنسبة 2 مرات تقريبا.
لسوء الحظ ، في حالة display:block
تمييز أسطر النص المتعددة أسوأ:

لفترة طويلة لم أتمكن من تحديد أيهما أفضل - ثانيتين إضافيتين من التقديم أو الاختيار البشري. نتيجة لذلك ، هزمت العملية الجمال.
المستوى 10 CSS معالج
تم تقريبًا 10٪ من وقت العرض تقريبًا بواسطة "تحسين" CSS ، والذي يمكنني استخدامه لتنسيق النص.
قلة الخبرة في تطوير الويب وفهم الأساسيات التي لعبت ضدي. اعتقدت أنه كلما كان المحددون أكثر دقة ، كان ذلك أفضل ، ولكن على وجه التحديد في حالتي ، لم يكن الأمر كذلك.
لتنسيق النص في المحطة ، استخدمت المحددات التالية:
#script-panel-container .log-content > div > span.text_color_green,
لكن (في chrome) ، يكون الخيار التالي أسرع قليلاً:
span.text_color_green
أنا لا أحب هذا المحدد ، لأنه عالمي للغاية ، لكن الأداء أكثر تكلفة.
string.split
إذا كان لديك deja vu بسبب إحدى النقاط السابقة ، فهذا خطأ. هذه المرة لا تدور حول polyfill ، ولكن حول التنفيذ القياسي في chrome:

(أنا ملفوفة string.split في defSplit بحيث تظهر وظيفة في منشئ ملفات التعريف)
1 ٪ هي تفاهات. لكن الدراج المثالي في مسكون. في حالتي ، يتم التقسيم دائمًا حرفًا واحدًا في كل مرة وبدون أي أنظمة عادية. لذلك ، قمت بتنفيذ خيار بسيط. هذه هي النتيجة:

fastSplit function fastSplit(str, separatorChar) { if (str === '') { return ['']; } let result = []; let lastIndex = 0; for (let i = 0; i < str.length; i++) { const char = str[i]; if (char === separatorChar) { const chunk = str.substr(lastIndex, i - lastIndex); lastIndex = i + 1; result.push(chunk); } } if (lastIndex < str.length) { const lastChunk = str.substr(lastIndex, str.length - lastIndex); result.push(lastChunk); } return result; }
أعتقد أنه بعد ذلك ، يتعين عليهم نقلي إلى فريق Google Chrome بدون مقابلة.
التحسين ، بعد الكلمة
التحسين هو عملية بلا نهاية ويمكن تحسين شيء ما إلى أجل غير مسمى. لا سيما بالنظر إلى أن حالات الاستخدام المختلفة تتطلب تحسينات مختلفة (ومتضاربة).
بالنسبة لحالتي ، اخترت حالة الاستخدام الرئيسية وحسنت وقت التشغيل من 15 ثانية إلى 5 ثوان. على هذا قررت التوقف.

لا يزال هناك بضعة أماكن أخطط لتحسينها ، ولكن هذا بفضل التجربة المكتسبة.
مكافأة. اختبار طفرة.
لقد حدث أنه خلال الأشهر القليلة الماضية ، غالبًا ما صادفت مصطلح "اختبار طفري". وقررت أن هذه المهمة طريقة رائعة لتجربة هذا الوحش. خاصةً بعد أن لم أحصل على تغطية الكود في Webstorm ، لإجراء اختبارات على karma.
نظرًا لأن كل من التقنية والمكتبة جديدان بالنسبة لي ، فقد قررت أن أمضي قليلاً من الدم: لاختبار مكون واحد فقط - النموذج. في هذه الحالة ، يمكنك الإشارة بوضوح إلى الملف الذي نختبره وأي مجموعة اختبار مخصصة له.
ولكن بغض النظر عما قد يقوله المرء ، كان علي أن العبث كثيرًا من أجل تحقيق التكامل مع karma و webpack.
في النهاية ، بدأ كل شيء ، وبعد نصف ساعة رأيت نتائج حزينة: نجا حوالي نصف المسوخ. لقد قتلت الجزء فورًا ، والجزء الأيسر للمستقبل (عندما قمت بتنفيذ أوامر ANSI المفقودة).
بعد ذلك ، فاز الكسل ، وفي الوقت الحالي كانت النتائج كما يلي (لـ 128 اختبارًا):
Ran 79.04 tests per mutant on average. ------------------|---------|----------|-----------|------------|---------| File | % score | # killed | # timeout | # survived | # error | ------------------|---------|----------|-----------|------------|---------| terminal_model.js | 73.10 | 312 | 25 | 124 | 1 | ------------------|---------|----------|-----------|------------|---------| 23:01:08 (18212) INFO Stryker Done in 26 minutes 32 seconds.
بشكل عام ، بدا هذا النهج مفيدًا جدًا بالنسبة لي (من الواضح أنه أفضل من تغطية الشفرة) ومضحك. السلبية الوحيدة هي فترة طويلة بشكل رهيب - 30 دقيقة لكل فئة الكثير.
والأهم من ذلك أن هذا النهج جعلني أفكر مرة أخرى في تغطية 100 ٪ وما إذا كان الأمر يستحق تغطية كل شيء مع الاختبارات: الآن رأيي أقرب إلى "نعم" عند الإجابة على هذا السؤال.
استنتاج
في رأيي ، يعد تحسين الأداء طريقة جيدة لتعلم شيء أعمق. كما أنه تمرين جيد للدماغ. ومن المؤسف جدًا أن هذا نادرًا ما يكون ضروريًا (على الأقل في مشاريعي).
وكما هو الحال دائمًا ، يعمل نهج "التنميط الأول ثم التحسين" بشكل أفضل بكثير من الحدس.
مراجع
التنفيذ القديم:
تطبيق جديد:
لسوء الحظ ، لا يوجد عرض توضيحي لمكون الويب ، لذلك لن تتمكن من بثه. لذلك أعتذر مقدما
شكرا لك على وقتك ، وسأكون سعيدا للتعليقات والاقتراحات والنقد المعقول!