يمكنك الحفاظ على استهلاك الذاكرة في تطبيقك بشكل فعّال وبدون أخطاء في التجزئة باستخدام Firebase Crashlytics. تستكشف هذه الجلسة مجموعتنا الشاملة من أدوات تحديد مشاكل الذاكرة وحلّها المتاحة في وحدة تحكّم Firebase وفي "استوديو Android". سنبدأ بشرح كيفية الاستفادة من إحصاءات أخطاء ANR وتقارير GWP-ASan لتشخيص أخطاء الذاكرة التي يصعب رصدها، ثم سنتعمّق في أحدث أدواتنا الخاصة بأحداث نفاد الذاكرة (OOM) والإيقاف بسبب تجاوز الحد الأقصى للذاكرة (MLK)، والمصمّمة خصيصًا لمساعدتك في التعامل مع حدود الذاكرة الجديدة التي تم تقديمها في Android 17 والتكيّف معها.
ApplicationExitInfo لعرض آثار سلاسل التعليمات المتعددة وتحديد سلاسل التعليمات الرئيسية التي تسبب الحظر.ProfilingManager في Android 17 لجمع لقطات لعناصر متعدّدة تم إلغاء تشويشها ويمكن فحصها في Perfetto أو من خلال Android CLI.النص
بولينا غوفوركوفا
مرحبًا،
مرحبًا بك في دليل تحديد مشاكل الذاكرة وحلّها باستخدام Firebase Crashlytics.
انتظر، يا له من لقب مؤسسي.
لنغيّر قليلاً.
الحفاظ على كفاءة التطبيقات وسرعتها باستخدام Firebase Crashlytics
هذا يبدو أفضل.
اسمي بولينا، وأنا من كبار مطوّري البرامج في فريق Firebase Crashlytics، وسأقدّم لكم هذه الجلسة اليوم.
في مرحلة ما من دورة التطوير، ستشعر أنك قد حللت جميع الأخطاء.
أخطاء تحليل JSON، والتحقّق من المؤشرات الفارغة المنسية، كلها اختفت!
وقد تسأل نفسك، "هل انتهيت؟"
لديّ أخبار سيئة لك، لأنّك لست كذلك.
لأنّ ما سيتبقّى لك هو المشاكل الصعبة.
وأدّعي اليوم أنّ الفئتَين اللتَين تندرج ضمنهما أصعب المشاكل التي ستواجهونها هما الاستجابة والذاكرة.
الخبر السار هو أنّنا سنساعدك.
سنستعرض اليوم الأدوات الحالية والجديدة في Android وCrashlytics لمساعدتك في تحديد السبب الأساسي لمشاكل الاستجابة التي تظهر من خلال أخطاء "التطبيق لا يستجيب"، واستثناءات نفاد الذاكرة، وتسرُّب الذاكرة، وتلف الذاكرة الأساسية.
هيّا نبدأ.
لماذا تُعدّ مشاكل الاستجابة والذاكرة شائعة جدًا؟
لنأخذ خطأً كلاسيكيًا: أنت تعلن عن سلسلة، وتضبطها على قيمة فارغة، وفي مرحلة ما، تحاول قراءة طولها.
ما الذي تحصل عليه؟
استثناء NullPointerException قديم وجيد.
ولكن في الواقع، الأمر سهل هنا، لأنّ تتبُّع تسلسل استدعاء الدوال البرمجية للاستثناء يشير إلى سطر الرمز البرمجي الذي تسبّب في تعطُّل تطبيقك.
انظر إلى تتبع تسلسل استدعاء الدوال البرمجية على اليسار.
java.lang.NullPointerException
at MainActivity.onCreate(MainActivity.kt:42)
at android.app.Activity.performCreate
at android.os.Handler.dispatchMessage
at android.os.Looper.loopOnce
at android.app.ActivityThread.mainيمكننا أن نلاحظ على الفور أنّ المشكلة تكمن في السطر 42 في MainActivity.kt.
وهذا النهج صحيح بالنسبة لمعظم الأخطاء البسيطة.
الآن، عندما تحدث مشكلة في الاستجابة أو الذاكرة، مثل خطأ "التطبيق لا يستجيب" على اليمين، لا يشير تتبُّع تسلسل استدعاء الدوال البرمجية إلى السبب الحقيقي تقريبًا.
"main" prio=5 tid=1 Blocked
| group="main" sCount=1 dsCount=0 flags=1
| state=S schedstat=( 1450239 48192 145 )
at DataManager.getData(DataManager.kt:25)
- waiting to lock <0x0a1b> held by thread 12
at MainActivity.onResume(MainActivity.kt:50)
at android.app.Activity.performResume
at android.os.Handler.dispatchMessage
at android.os.Looper.loopOnce
at android.app.ActivityThread.mainفي أفضل الأحوال، يعرض لك فقط التأثير، وهو عبارة عن سلسلة محادثات أو تخصيص غير ضار حدث أثناء تجميد التطبيق أو نفاد الذاكرة.
في حين أنّ السبب الأساسي قد حدث قبل ذلك بكثير، أو ربما في مكان آخر تمامًا.
لحسن الحظ، لا داعي للتخمين في ما يتعلّق بهذه المشاكل، لأنّ Firebase Crashlytics يوفّر لنا الإجابة.
قبل أن نبدأ بتحديد المشاكل وحلّها، ارفع يدك إذا كنت على دراية بـ Crashlytics.
رائع.
أحب رؤية كل هذه الأيدي.
بالنسبة إلى المستخدمين الجدد، Firebase Crashlytics هو حلّ لإعداد تقارير الأخطاء في الوقت الفعلي يوفّر بيانات ميدانية، بالإضافة إلى إحصاءات مستندة إلى الذكاء الاصطناعي والسياق، ونصائح لتحديد المشاكل وحلّها، وإشارات واضحة حول الإصدارات الجديدة، وكل ذلك لمساعدتك في حلّ المشاكل بشكل أسرع.
تحظى أداة Crashlytics بشعبية كبيرة بين المطوّرين، فهي الحل الأكثر استخدامًا لإعداد تقارير الأعطال على الأجهزة الجوّالة، وتعمل على تشغيل أكثر من 370,000 تطبيق أصلي، بما في ذلك %57 من أهم تطبيقات Android.
لنتعرّف على كيفية تتبُّع الأخطاء في Crashlytics، لنلقِ نظرة على مثال ملموس.
اليوم، أريد أن أروي لكم قصة ChromaClip.
إنّها أداة لاستخراج تشكيلة الألوان من الفيديو، وقد عملتُ على تطويرها، وأريد أن أخبركم كيف تعاملتُ مع أخطاء الاستجابة والذاكرة أثناء تطويرها.
الفكرة وراء ChromaClip بسيطة: يختار المستخدمون فيديو، مثل لقطة لغروب شمس رائع أو مشهد بتدرج ألوان مميز، ويحلّل التطبيق الإطارات لإنشاء لوحة ألوان مخصّصة يمكن للمستخدمين استخدامها في تصاميمهم أو فنونهم الرقمية أو لوحات المزاج أو أي شيء آخر.
تبدأ قصتنا قبل إصداري الأول.
أردت التأكّد من أنّها تعمل بشكل مثالي قبل إطلاقها، لذا أجريت اختبارات نهائية على جهازيّ، فاخترت الفيديو ونقرت على الزر لاستخراج تشكيلة الألوان، وظهرت لي رسالة الخطأ التي يكرهها جميع مطوّري برامج Android.
لا يستجيب ChromaClip.
هذا خطأ "التطبيق لا يستجيب"، ويُعرف أيضًا باسم ANR.
بولينا غوفوركوفا
إذًا، ماذا نفعل في هذه الحالة، إلى جانب الشعور بالذعر؟
باتّباع حدسنا الأول، بما أنّ هاتف الاختبار متصل مباشرةً بالكمبيوتر المحمول، نفتح "استوديو Android" ونتحقّق من Logcat.
حسنًا، نرى تتبُّع تسلسل استدعاء الدوال البرمجية لسلسلة التعليمات الرئيسية.
14:32:15.105 1205-1250/system_process E/ActivityManager: ANR in com.chromaclip.app (com.chromaclip.app/.MainActivity)
PID: 14205
Reason: Input dispatching timed out
Dumped ANR trace to /data/anr/traces.txt
"main" prio=5 tid=1 Native
| group="main" sCount=1 dsCount=0 flags=1 obj=0x72a8c3d0 self=0x759fb92000
| sysTid=14205 nice=-10 cgrp=default sched=0/0 handle=0x7623910548
| state=S schedstat=( 1450239120 4819203 1453 ) utm=132 stm=13 core=4 HZ=100
| stack=0x7fed2a0000-0x7fed2a2000 stackSize=8MB
at sun.misc.Unsafe.park(Native method)
- waiting on an unknown object
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:190)
at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:279)
at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:85)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:59)
at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source:1)
at com.chromaclip.app.extractor.PaletteExtractor.extractColors(PaletteExtractor.kt:42)ما الذي يخبرنا به في الواقع؟
أولاً، في الأعلى مباشرةً، نحصل على تأكيد بأنّها مشكلة ANR، تحدث في MainActivity.
وماذا أيضًا؟
نرى السبب، وهو انتهاء مهلة إرسال الإدخال.
هذا يعني أنّ المستخدم نقر على الشاشة.
نعم، لقد فعلنا ذلك.
ولكنّ سلسلة التعليمات الرئيسية كانت مشغولة جدًا لدرجة أنّها لم تتمكّن من معالجة هذا الحدث خلال مهلة المراقبة التي تبلغ خمس ثوانٍ في Android.
بعد ذلك، نرى أيضًا أنّ النظام قد أفرغ الملف في traces.txt، وتحته مباشرةً، يطبع logcat تتبُّع تسلسل استدعاء الدوال البرمجية لسلسلة التعليمات الرئيسية.
لاحظ أن الحالة تساوي S هناك.
لا تستهلك سلسلة التعليمات الرئيسية وحدة المعالجة المركزية بشكل نشط.
يرمز الحرف S إلى النوم.
لماذا تكون سلسلة التعليمات الرئيسية في وضع السكون أثناء الانتظار؟
لنلقِ نظرة عن كثب على إطارات حزمة التكديس.
في منتصف التتبُّع، نرى أنّ سلسلة التعليمات الرئيسية متوقفة داخل Unsafe.park في انتظار عنصر غير معروف.
لماذا لا نعرف ما الذي ينتظره؟
في الواقع، في بعض أخطاء ANR، مثل حظر سلسلة تعليمات بواسطة قفل مزامنة Java الكلاسيكي، يكون تتبُّع تسلسل استدعاء الدوال البرمجية لسلسلة التعليمات الرئيسية مفيدًا بما يكفي لإخبارنا بما تنتظره السلسلة، وما الذي يحافظ على القفل.
ولكن عند استخدام عناصر أساسية للتزامن ذات مستوى أعلى، فإنّ كل ما يخبرنا به logcat هو أنّنا ننتظر عنصرًا غير معروف.
لذلك، ليس لدينا أي فكرة عن العنصر الذي ننتظره، أو سلسلة الوحدات العاملة في الخلفية التي ننتظرها تحديدًا، أو سبب استغراقها أكثر من 5 ثوانٍ.
وبما أنّنا نختبر محليًا، يمكننا سحب traces.txt من الجهاز والتمرير يدويًا عبر عشرات سلاسل المحادثات غير النشطة، وربما العثور على الجاني أو ربما استخدام الرموز المميزة لسؤال الذكاء الاصطناعي.
في بعض الحالات، قد ينجح ذلك، وفي الحقيقة، سينجح في هذه الحالة.
لكنني لا أريد حقًا قراءة تفريغات سلاسل المحادثات الأولية يدويًا، بصراحة، هذا أمر قديم جدًا، وأعلم أنّ هذا النهج ليس قابلاً للتطبيق على الإطلاق بعد أن نكون في مرحلة الإنتاج.
لا يمكننا الوصول مباشرةً إلى ملف traces.txt في هواتف المستخدمين، أليس كذلك؟
لذلك، من الجيد أنّنا دمجنا تطبيقنا مع Firebase Crashlytics.
لأنّها تغني عن الحاجة إلى البحث وتصل إلى السبب الأساسي لمشكلة ANR حتى في مرحلة الإنتاج.
كيف؟
وهنا يأتي دور واجهة برمجة التطبيقات ApplicationExitInfo في Android.
اعتبارًا من الإصدار 11 من نظام التشغيل Android، عندما يرصد النظام خطأ ANR أو ينهي عملية تطبيقك، يسجّل نظام التشغيل بيانات وصفية مفصّلة عن الخروج، بما في ذلك السبب الدقيق للعطل وحالة الذاكرة وسجلّ تتبّع النظام الكامل لكل سلسلة محادثات قيد التشغيل في العملية في الوقت الحالي.
وأفضل ما في الأمر هو أنّك لست بحاجة إلى تثبيت أي مكوّنات إضافية أو كتابة سطر واحد من الرموز البرمجية.
طالما أنّك تستخدم الإصدار 18.3.5 أو إصدارًا أحدث من حزمة تطوير البرامج (SDK) لخدمة Crashlytics، ستعمل الميزة تلقائيًا.
لذلك، بمجرد إعادة تشغيل تطبيقك، ستستعلم حزمة تطوير البرامج (SDK) لخدمة Crashlytics تلقائيًا عن ApplicationExitInfo، وتلتقط تفريغ الذاكرة الكامل المتعدد السلاسل إلى جانب بيانات القياس عن بُعد للجهاز، وترسلها إلى الواجهة الخلفية لخدمة Crashlytics.
لنفتح وحدة تحكّم Crashlytics ونرى ما تخبرنا به.
هذه هي طريقة عرض تفاصيل المشكلة في وحدة تحكّم Firebase Crashlytics لخطأ ANR، ونرى على الفور ضمن علامة التبويب "تتبُّع تسلسل استدعاء الدوال البرمجية" العامل DefaultDispatcher.
DefaultDispatcher-worker-1 (Root blocking)
dalvik.system.VMRuntime.allocateNode (Native method)
android.graphics.Bitmap.createBitmap (Bitmap.java:1124)
android.graphics.Bitmap.createBitmap (Bitmap.java:1091)
com.chromaclip.app.extractor.FrameDecoder.getUncompressedFrame (FrameDecoder.kt:112)
com.chromaclip.app.extractor.PaletteExtractor$processVideo$2.invokeSuspend (PaletteExtractor.kt:65)
kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith (ContinuationImpl.kt:33)
kotlinx.coroutines.DispatchedTask.run (DispatchedTask.kt:106)
java.util.concurrent.ThreadPoolExecutor.runWorker (ThreadPoolExecutor.java:1167)
java.util.concurrent.ThreadPoolExecutor$Worker.run (ThreadPoolExecutor.java:641)
java.lang.Thread.run (Thread.java:923)هذا هو في الواقع سلسلة المحادثات التي ظهرت في أعلى الصفحة بواسطة Crashlytics والتي تمثل عنق الزجاجة لسلسلة المحادثات الرئيسية لدينا، ويضع Crashlytics علامة عليها بشارة تسمى حظر الجذر.
كما يوضّح التلميح على الشارة، فإنّ سلسلة الوحدات العاملة هذه هي التي تحظر سلسلة التعليمات التي أدّت إلى حدوث خطأ ANR.
بالمناسبة، تتضمّن Crashlytics شارات ذكية لأسباب أخرى أيضًا.
على سبيل المثال، إذا توقّف مؤشر ترابط عن قراءة ملف ضخم أو تنفيذ طلب بحث بطيء جدًا في قاعدة البيانات، ثم تسبّب في حدوث خطأ ANR، سيتم وضع علامة على مؤشر الترابط على أنّه حظر جذر وحدات الإدخال والإخراج.
وإذا كان هناك سلسلتان عالقتان في انتظار بعضهما البعض، فستقوم Crashlytics بوضع علامة عليهما على أنهما في حالة توقف تام.
لنلقِ نظرة الآن على إطارات حزمة التكديس نفسها ضمن سلسلة التعليمات الرئيسية التي تسبّب الحظر.
لدينا PaletteExtractor processVideo الذي يستدعي FrameDecoder getUncompressedFrame المميّز باللون الأزرق، وهو عالق تمامًا داخل createBitmap أثناء محاولة تخصيص ذاكرة لإطار واحد.
بما أنّنا نعلم أنّ التطبيق لا ينتظر قفلًا آخر، وأنّه ليس في مكالمة عبر الشبكة، فإنّ المشتبه به الرئيسي هو الذاكرة، ويمكننا تأكيد ذلك على الفور من خلال التحقّق من الذاكرة الخالية في الجهاز هنا في وحدة تحكّم Crashlytics أيضًا.
انظر إلى المكانَين اللذين تعرض فيهما Crashlytics هذه المعلومات.
في علامة التبويب "البيانات" (Data) للحدث المحدّد بجانب علامة التبويب "تتبُّع تسلسل استدعاء الدوال البرمجية" (Stack trace) التي كنا نستكشفها للتو، وأيضًا في أعلى يسار الصفحة التي تعرض المتوسط في جميع الأحداث.
وفي كلتا الحالتين، يبدو أنّ حجم الملف صغير جدًا، ولا يتجاوز حتى 100 كيلوبايت.
لنفكّر في الأمر.
ما حدث هنا هو أنّه بسبب نقص الذاكرة في العملية، قضى وقت تشغيل Android كل وقته في تشغيل عملية جمع المهملات بشكل محموم، محاولاً العثور على مساحة كافية للصورة النقطية التالية، ولم يحصل عليها أبدًا.
تجميد سلسلة الوحدات العاملة (worker thread) وحرمان واجهة المستخدم لأكثر من خمس ثوانٍ.
إليك نصيحة سريعة.
قبل أن تحصل على، كلما حصلت على خطأ ANR مقترن بهذا النوع من، ذاكرة الوصول العشوائي المجانية المنخفضة، فهذا هو دليلك القاطع على أن خطأ ANR هو في الواقع إما تسرّب للذاكرة أو مشكلة تضخم.
والسؤال الآن هو: هل تطبيقنا هو الضحية أم أنّه هو أيضًا الذي يستهلك الذاكرة؟
لذا، لنرجع إلى قائمة المشاكل في مشروعنا ونرى ما إذا كان هناك أي شيء آخر يبدو ذا صلة.
بولينا غوفوركوفا
وانظروا إلى هذا.
في أسفل الصفحة، تظهر قائمة المشاكل، وفوق مشكلة ANR التي تتضمّن ثمانية أحداث مباشرةً، يظهر استثناء OutOfMemory.
لذا، قد نكون نحن المذنبين في النهاية.
وهذا في الواقع دليل كبير.
لأنّ خطأ Java OutOfMemoryError على Android، أو OOM باختصار، لا يعني فقط أنّ ذاكرة الوصول العشوائي (RAM) في الهاتف كانت منخفضة.
هذا يعني أنّ عملية التطبيق تجاوزت الحدّ الأقصى المسموح به في الذاكرة الديناميكية في Java لوقت تشغيل Android.
لذا، في هذه المرحلة، نظريتي العملية هي أنّ خطأ ANR وخطأ OOM هما في الواقع وجهان لعملة واحدة.
بناءً على توقيت تشغيل الاختبار، قد يتعطّل ChromaClip بسبب تكرار عمليات جمع المهملات لفترة طويلة بما يكفي لإحداث خطأ ANR، أو قد يستنفد الذاكرة الديناميكية بالكامل، ما يؤدي إلى نفاد الذاكرة (OOM).
لنتحقّق من استثناء OutOfMemory للتأكّد من ذلك.
بدءًا من تتبُّع تسلسل استدعاء الدوال البرمجية
java.lang.OutOfMemoryError: Failed to allocate a 256 byte allocation
at android.graphics.Bitmap.createBitmap
at android.view.LayoutInflater.inflate(LayoutInflater.java:532)
at com.chromaclip.app.SettingsActivity.onCreate(SettingsActivity.kt:21)انتظِر قليلاً.
Bitmap.createBitmap، هذا مألوف.
ألم نشاهد هذا الفيديو من قبل؟
حسنًا، باستثناء النظر إلى المكان الذي تم استدعاؤه منه هذه المرة.
تعذّر تخصيص 256 بايت عند فتح نشاط الإعدادات.
نحن نقع في الفخ نفسه مرة أخرى.
إذا تعطّل تطبيقك بسبب عدم قدرته على تضخيم رمز إعدادات صغير، فلن يكون نشاط الإعدادات هو السبب.
بل هو مجرد المتصل غير المحظوظ الذي طلب قطرة من الذاكرة عندما لم يكن هناك أي منها.
لذا، للعثور على السبب الحقيقي، نحتاج إلى التصغير ورؤية كل ما كان موجودًا في الذاكرة عند حدوث التعطُّل.
بعبارة أخرى، نحتاج إلى لقطة لأجزاء من الذاكرة في Java.
إذا كنت تعرف كيفية تشغيل حالة نفاد الذاكرة (OOM)، يمكنك الحصول يدويًا على لقطة لأجزاء من ذاكرة Java باستخدام محلّل الذاكرة في استوديو Android.
ولكن عندما يحدث نفاد الذاكرة بشكل غير متوقّع، سواء أثناء عمليات الاختبار أو في بيئة الاستخدام الفعلية، لن تنجح هذه الطريقة أيضًا.
بولينا غوفوركوفا
هذه ميزة أخرى رائعة في Firebase Crashlytics.
بالإضافة إلى معلومات الخروج من التطبيق، يتكامل Crashlytics الآن مع واجهة برمجة التطبيقات ProfilingManager في Android.
تتضمّن واجهة ProfilingManager API في الإصدار 17 من نظام التشغيل Android ميزة تحليل استخدام الذاكرة التي يتم تشغيلها من خلال النظام، وهذا بالضبط ما نحتاجه هنا.
يعني ذلك أنّه عندما يواجه تطبيقك خطأ OutOfMemory أو يتجاوز حدود ذاكرة النظام، سيلتقط نظام التشغيل لقطة لعناصر متعدّدة في الخلفية، ثم في Crashlytics، ستتوفّر المجموعة التي تم تفعيلها مجانًا بدون أي إعدادات إضافية طالما أنّك تستخدم الإصدار 20.1.0 من حزمة تطوير البرامج (SDK) لخدمة Crashlytics أو إصدارًا أحدث.
بعد ذلك، ستجمع Crashlytics تلقائيًا حالات نفاد الذاكرة (OOM) وحالات الإيقاف بسبب Memory Limiter، وسنتحدث عنها أكثر لاحقًا في لوحة البيانات.
وعند الموافقة على جمع لقطات لأجزاء من الذاكرة من خلال وحدة تحكُّم Firebase أو واجهة سطر الأوامر، ستبدأ Crashlytics تلقائيًا في جمع لقطات العناصر المتعدّدة التي تم التقاطها، وإزالة الغموض عنها، وستحصل بشكل أساسي على صورة أشعة سينية كاملة لكل ما هو موجود في أجزاء ذاكرة Java في تطبيقك في لحظة التعطُّل.
إليك شكل خطأ نفاد الذاكرة (OOM) في ChromaClip في وحدة تحكّم Crashlytics بعد تسجيل بيانات الذاكرة الديناميكية.
في أعلى الصفحة، يظهر شرح لسبب استنفاد Java للذاكرة المؤقتة وسبب حدوث ذلك.
بعد ذلك، ستظهر لك أيضًا نصائح مفيدة لتحديد المشاكل وحلّها ومؤشرات إلى المراجع الخارجية.
في علامة التبويب "تتبُّع تسلسل استدعاء الدوال البرمجية"، فوق نشاط الإعدادات المضلِّل مباشرةً، تشير Crashlytics إلى أنّ تتبُّع تسلسل استدعاء الدوال البرمجية لحالات نفاد الذاكرة ليس حتميًا، حتى لا يضيّع المستخدمون الوقت في الاطّلاع على تتبُّع تسلسل استدعاء الدوال البرمجية كما فعلنا للتو.
بعد ذلك، تُرفق Crashlytics ملف Perfetto الذي تمت إزالة الغموض عنه، وهو لقطة لأجزاء من الذاكرة، للجلسة.
ومن هنا، يمكننا تنزيل الملف.
يمكننا تصفُّح جميع ملفات تفريغ الذاكرة في Google Cloud Storage، أو يمكننا فتحها في واجهة مستخدم Perfetto.
وبالتالي، يؤدي النقر على "تحليل" (Analyze) في واجهة مستخدم Perfetto إلى فتح مستكشف لقطات أجزاء من الذاكرة في Perfetto.
الآن، ارفع يدك إذا كنت قد رأيت هذا بالفعل.
حسنًا، لدينا بعض الأيدي.
بالنسبة إليّ، هذه الواجهة مربكة جدًا، ولكن في هذه الحالة، المشكلة بسيطة جدًا في الواقع.
انظر إلى مجموعة الأشرطة الضخمة التي تمتد على طول كومة Java بالكامل تقريبًا.
لذا، في الغالب، هي مصفوفة من كائنات زوجية، كل منها يلف لونًا وبعض مثيلات السلسلة.
أوه!
أعتقد أنّني أعرف ما يحدث.
يخصّص الرمز لونًا وبعض البيانات الداعمة لكل بكسل في كل إطار، ويحتفظ بالملايين منها في قائمة.
لذا، ليس من المستغرب أن نشاط الإعدادات لم يتمكن من تخصيص تلك الـ 256 بايت.
حسنًا، تم حل لغز واحد على الأقل.
وبالتالي، يجب معالجة هذه النقطة.
في هذه المرحلة، لدينا خياران.
بما أنّ Perfetto وجّهنا إلى PaletteExtractor، يمكننا فتح بيئة التطوير المتكاملة وإعادة كتابة الحلقة يدويًا، أو استخدام وكيل الترميز المستند إلى الذكاء الاصطناعي.
أو يمكننا تجاوز فحص الذاكرة اليدوي تمامًا، والاستفادة من Android CLI.
# 1. Install Android CLI & Perfetto skills
$ android skills add android-cli perfetto
# 2. List recent heap dumps from Crashlytics
$ android signals heaps list --kind oom --timeframe=90d
# 3. Download a heap dump as a Perfetto trace
$ android signals heaps download <heap-dump-id> \
--output ./heaps
# 4. Query top retained objects in the heap dump
$ trace_processor query ./heaps/oom.perfetto-trace ...
# 5. Fetch matching Crashlytics events
$ android signals heaps describe <heap-dump-id>وبالتالي، فإنّ تثبيت Android CLI وPerfetto سيتيح لك عرض وتنزيل لقطات لأجزاء من الذاكرة في Crashlytics تم إلغاء تشويشها، والاستعلام عن أهم العناصر المحفوظة من هذه اللقطات من خلال trace_processor، وجلب بيانات المشاكل المطابقة في Crashlytics، مثل المفاتيح المخصّصة والسجلّات وأشرطة التنقّل وعمليات الطرح المرتبطة بها والمزيد، وكل ذلك من خلال الوحدة الطرفية.
والأفضل من ذلك، انظر إلى أسفل الشرائح.
لا تنفِّذ هذه الأوامر بنفسك.
مرة أخرى، اطلب من وكيل الترميز المفضّل لديك المستنِد إلى الذكاء الاصطناعي بلغة طبيعية فحص أحدث لقطة لأجزاء من الذاكرة (OOM) باستخدام أمر Android وإرسال الإصلاح.
# Or ask your favorite LLM in natural language
> Using `android` command, inspect the latest heap dump
for my OOM issue and propose a fix
● Ran android signals heaps list · download · describe
● Analyzed ./heaps/oom.perfetto-trace (perfetto)
● Updated PaletteExtractor.kt (+6 -4 lines)وسيكون لديه سياق كافٍ لتنفيذ العملية بأكملها نيابةً عنك.
وبالتالي، يمكن إدراج حالات نفاد الذاكرة وتنزيل بيانات التتبُّع وإجراء التحليل وإنشاء رمز التصحيح مباشرةً في مشروعك.
وألقِ نظرة على هذا التصحيح في ملف PaletteExtractor.
// PaletteExtractor.kt
fun extractPalette(framePixels: IntArray): SparseIntArray {
- val pixelColors = mutableListOf<Pair<Color, String>>() // 8.3M objects per 4K frame!
- for (px in framePixels) {
- pixelColors.add(Color.valueOf(px) to "#%06X".format(px))
- }
+ val colorHistogram = SparseIntArray() // Downsampled & packed ARGB ints!
+ for (i in framePixels.indices step 16) {
+ val argb = framePixels[i]
+ colorHistogram.put(argb, colorHistogram.get(argb) + 1)
+ }
+ return colorHistogram
}بدلاً من تخصيص ملايين العناصر، أصبحنا الآن نُجري عملية اختزال للإطار ونحصي الأعداد الصحيحة الأولية مباشرةً.
لذا، لنطبّق الإصلاح، ونغلق المشاكل، ونحدّث الإصدار، ونواصل الاختبار.
نأمل ألا يختفي خطأ نفاد الذاكرة فحسب، بل أن يختفي أيضًا خطأ ANR المزعج، لأنّنا لن نضطر إلى تكرار عملية جمع المهملات.
لذلك، نشرنا إصدار التصحيح على أجهزتنا الاختبارية، واخترنا مقطعًا جديدًا، وبدأنا في استخراج لوحات الألوان.
وتتجمّد واجهة المستخدم مرة أخرى.
خطأ ANR آخر.
ومن المثير للاهتمام أنّه بعد إجراء المزيد من الاختبارات، اكتشفنا أنّ بعض الأجهزة لا تزال تظهر فيها أخطاء ANR، ولكن في أجهزة أخرى، يتعطّل التطبيق الآن بشكل عشوائي وبشكل متكرّر أيضًا، وبمعدّل أكبر من أخطاء نفاد الذاكرة.
ولكننا أصلحنا مشكلة نفاد الذاكرة، فما الذي يحدث؟
بولينا غوفوركوفا
حسنًا، لنعد إلى Crashlytics.
لنبدأ بالأخبار السارة.
اطّلِع على قائمة المشاكل.
لقد اختفى استثناء نفاد الذاكرة تمامًا، كما كنا نأمل.
الخبر السيئ هو…
بما أنّ ChromaClip أصبح يدوم لفترة أطول، أصبح لديه الوقت للكشف عن العناصر الأخرى التي كانت مخفية تحته.
وبالتالي، لم يتم حلّ مشكلة ANR.
والآن، لدينا عطلان داخليان في نظام التشغيل في أسفل الصفحة، ولحسن الحظ، لم يحدثا إلا في حدثين فقط.
وفي أعلى القائمة، لدينا 11 حدثًا لعملية الإيقاف التي نفّذتها ميزة "محدِّد الذاكرة"، وقد تم وضع شارة عليها بأنّها تتضمّن لقطة لأجزاء من الذاكرة.
مهلاً
ما هو الإيقاف الناتج عن "محدِّد الذاكرة"؟
هل يمكنني أن أطلب منكم رفع أيديكم، من منكم سمع عنها؟
حسنًا.
بعض الأيدي النادرة.
لنتعلّم معًا.
إذا لم يسبق لك رؤية ميزة "الحدّ من استهلاك الذاكرة"، فذلك لأنّها جديدة تمامًا في Android 17.
ولا يحدث نفاد الذاكرة (OOM) في Java إلا عند تجاوز الحد الأقصى لكومة ذاكرة Java المُدارة، ولكن عندما يتضمّن إجمالي استهلاك الذاكرة في تطبيقك أيضًا ذاكرة RAM الأصلية، مثل مخازن الرسومات المؤقتة وتخصيصات C++، لم يكن هناك حد أقصى لكل تطبيق في السابق.
لذا، كان لدينا قاتل الذاكرة المنخفضة، والذي كان عالميًا.
وكان يتدخل عندما يستهلك تطبيق ما ذاكرة الوصول العشوائي (RAM) للنظام بالكامل إلى أن يتوقف الهاتف عن العمل.
الآن، بدءًا من Android 17، يضع Memory Limiter حدًا أقصى لاستهلاك كل تطبيق للذاكرة.
لذلك، عندما تستهلك العملية قدرًا غير متناسب من ذاكرة الوصول العشوائي، ينهي نظام التشغيل هذا التطبيق المحدد.
وتسجّل Crashlytics كل هذه التفاصيل وتجمع حالات الإنهاء معًا في لوحة البيانات.
لذا، عند النقر على "إيقاف محدِّد الذاكرة" في Crashlytics، كما هو الحال مع نفاد الذاكرة، ستحصل على شرح لسبب تدخّل نظام التشغيل، وبالتالي لن تحتاج إلى حفظ ما قلته للتو.
وستحصل أيضًا على نصائح حول كيفية معالجة المشكلة، وروابط مباشرة إلى مستندات فرض استخدام الذاكرة في Android.
وكما هو الحال مع أخطاء نفاد الذاكرة، يلقي تتبُّع تسلسل استدعاء الدوال البرمجية باللوم على إطار بريء عندما يوقف نظام التشغيل التطبيق، وتبرز لوحة بيانات Crashlytics هذا الإطار.
في الوقت الحالي، تحدث عمليات الإيقاف التي تنفّذها ميزة "الحدّ من استخدام الذاكرة" بشكل أساسي في الرموز البرمجية الأصلية لأنّ البيئات الأصلية لا تتضمّن تلك الطبقة التي تتعامل مع أمان الذاكرة وتخصيصها.
وإلا، لكانت حالة نفاد الذاكرة (OOM).
لذا، السؤال الأول هو: هل تحتفظ كائنات Kotlin بتخصيصات ضخمة مستندة إلى لغة أصلية؟
أم أنّه رمز C++ أولي يتسرّب من تلقاء نفسه؟
لنتعرّف على ذلك من خلال النقر على "تحليل" (Analyze) في واجهة مستخدم Perfetto.
انتقِل إلى علامة التبويب "نظرة عامة" (Overview) في متصفّح لقطات أجزاء من الذاكرة في Perfetto.
ضمن المعلومات العامة، نرى أنّ ذاكرة RSS المجهولة ومساحة الإبدال تشيران إلى أنّ إجمالي ذاكرة العمليات بلغ 4 غيغابايت.
حسنًا
ومع ذلك، نرى أدناه أنّه ضمن "وحدات البايت التي يحتفظ بها الذاكرة الديناميكية"، تبلغ سعة الذاكرة الديناميكية المُدارة بالكامل، بما في ذلك الذاكرة الأصلية المرتبطة بعناصر Kotlin مثل الصور النقطية، 42 ميغابايت فقط.
حسنًا
لذلك، في غضون 10 ثوانٍ، استبعدنا جميع رموز Kotlin وJava.
لذلك، يجب أن يكون السبب هو تسرُّب من طبقة C++ أصلية.
وبما أنّ ChromaClip يحتوي على طبقة C++ رفيعة لمعالجة اللقطات، لن نضطر إلى البحث بعيدًا.
من خلال توجيه وكيل الترميز أو أداة تحليل الأداء الأصلية في "استوديو Android" إلى NativePaletteEngine.cpp processFrame، يمكننا العثور على الخطأ بسهولة.
// NativePaletteEngine.cpp
void NativePaletteEngine::processFrame(const uint8_t* rgba, size_t size) {
uint8_t* scratch = new uint8_t[size]; // Temporary frame buffer
transformColorSpace(rgba, scratch, size);
// BUG: Allocated every frame, but never freed!
+ delete[] scratch; // Free native buffer when done
}لذلك، في كل إطار فيديو، يخصّص رمز C++ مخزنًا مؤقتًا، وهو ليس مؤقتًا إذا لم يتم تحريره، وهذا خطأ مبتدئ.
حان الوقت لربط النقاط.
ماذا حدث؟
بمجرد إصلاح تسرب Java، سيكون لدى تسرب C ++ الوقت للتراكم والتشغيل بشكل جامح.
على Android 17، رصدت ميزة "محدِّد الذاكرة" هذا التسريب وأغلقت التطبيق.
ولكن على الأجهزة القديمة التي لم تكن ميزة "محدِّد الذاكرة" متوفّرة فيها بعد، ظهرت هذه المشكلة على شكل خطأ ANR.
أخيرًا، أصبح كل شيء منطقيًا.
لنضِف delete[] scratch هنا، ونحدّث الإصدار، ونطرح التحديث.
بعد التخلّص من حالات الإغلاق بسبب "محدِّد الذاكرة" وأخطاء ANR، من المفترض أن نكون جاهزين لإطلاق الإصدار 1.0.
هذا رائع.
وبفضل سرعة ChromaClip واستقراره الآن، يحقق تطبيقنا نجاحًا كبيرًا.
ويحمّل آلاف المستخدمين الفيديوهات ويعيدون مزجها كل يوم.
ولكن مع كل هذه الزيارات، هل تتذكر الأعطال الداخلية الغامضة في نظام التشغيل التي رأيناها سابقًا في أسفل لوحة البيانات؟
حسنًا، إنّها تنتشر أيضًا بشكل كبير.
وفجأة، أصبح هناك المزيد منها.
ولم ينتهِ عملنا بعد.
بولينا غوفوركوفا
لذا، عند فتح هذا العطل الداخلي في نظام التشغيل في Crashlytics، اطّلِع على تتبُّع تسلسل استدعاء الدوال البرمجية.
لا يوجد سطر واحد من التعليمات البرمجية من تطبيقنا.
تعطُّل RenderThread في أعماق libc.so أثناء إخلاء الذاكرة
إليك نصيحة أخرى: عندما يتعطّل مؤشر ترابط النظام داخل أداة تخصيص الذاكرة بدون وجود أي من رموز تطبيقك بداخله، فهذه علامة واضحة على تلف الذاكرة الأصلية.
لذا، فإنّ رمز C++ الخاص بنا قد أتلف المخزن المؤقت أو أفرغه في وقت سابق، وتعثّر مؤشر ترابط العرض فيه لاحقًا.
إذًا، كيف يمكننا العثور على السبب الحقيقي في اللحظة التي حدث فيها التلف؟
يوفّر لنا Android أداتَين فعّالتَين، وتتوافق أداة Crashlytics مع كلتيهما بشكل تلقائي.
أولاً، GWP-ASan، وهي شبكة أمان خفيفة الوزن تم إنشاؤها خصيصًا للإنتاج.
وتأخذ عيّنات من جزء صغير من عمليات تخصيص الذاكرة الديناميكية الأصلية وتحيطها بصفحات حماية بحيث تكون تكلفة وحدة المعالجة المركزية والذاكرة صفرًا تقريبًا.
وعندما يواجه خطأ ما ذاكرة تم إخلاؤها أو ذاكرة خارج الحدود في عملية تخصيص تم أخذ عيّنات منها، ترصد أداة GWP-ASan هذا الخطأ على الفور في آلاف جلسات المستخدمين في بيئة حقيقية.
ومع ذلك، بما أنّ GWP-ASan احتمالية بطبيعتها، فعند اختبار الأشياء محليًا، لن تحتاج إلى تشغيل هاتف الاختبار لمدة 16 ساعة أو أكثر للوصول إلى العينة العشوائية.
إذًا، ما الذي تحصل عليه؟
لتصحيح الأخطاء محليًا بشكل حتمي، يمكنك استخدام ميزة Memory Tagging Extension (MTE) من ARM.
تستخدم MTE مؤشرًا للأجهزة وعلامات الذاكرة للتحقّق من كل عملية تخصيص، وهي متاحة على أجهزة ARM v9، مثل Pixel 8 والإصدارات الأحدث.
لذا، يمكنك إقرانها مع GWP-ASan في قناة الإصدار العلني للحصول على أفضل ما في الميزتين.
وكما هو متوقّع، عندما ترصد أي من الأداتَين الخطأ، تضع Crashlytics تلقائيًا شارة على المشكلة وتتيح لك الفلترة حسب GWP-ASan أو MTE وتثبّت بطاقة معلومات تتضمّن سبب التلف الدقيق في لوحة البيانات.
لذا، لنلقِ نظرة على ما يحدث في وحدة تحكّم Crashlytics عندما يرصد GWP-ASan العطل.
بجانب علامة التبويب العادية لتتبع تسلسل استدعاء الدوال البرمجية، لدينا تتبع تسلسل استدعاء الدوال البرمجية للذاكرة.
وفي الأعلى، نرى سبب التلف بالضبط، وهو "الاستخدام بعد الإخلاء".
وبدلاً من عرض تعطُّل سلسلة العرض في libc.so، تمنحنا أداة GWP-ASan الآن السيرة الكاملة لهذا المخزن المؤقت داخل effect_pipeline.cc.
لذا، ضمن Allocated By، نرى مكان إنشاء المخزن المؤقت.
وأسفل Deallocated By مباشرةً، نرى السبب الدقيق الذي حررها من أسفل سلسلة العرض.
وهو مخفي في السطر 208 في مسار التأثيرات.
لذلك، نفتح الرموز البرمجية الأصلية في EffectPipeline، ويظهر لنا الخطأ.
// effect_pipeline.cc
void EffectPipeline::onFilterSwitched(FilterPresetId preset) {
// BUG: Freed on worker thread while RenderThread is still reading it!
+ std::lock_guard<std::mutex> lock(lut_mutex_);
releaseLutBuffer(); // Frees old color-grading LUT
loadFilterPreset(preset); // Allocates new color-grading LUT
}عندما يبدّل المستخدم فلتر الألوان في سلسلة التعليمات في الخلفية، يتم إخلاء المخزن المؤقت القديم بينما لا يزال RenderThread يقرأه.
حسنًا، أضفنا قفل الاستبعاد المتبادل حول عملية تبديل المخزن المؤقت، وأرسلنا التحديث، وأخيرًا شاهدنا تقارير GWP-ASan وجميع تلك الأعطال الغامضة تختفي بين عشية وضحاها تقريبًا.
وارتفع مقياس المستخدمين الذين لم يواجهوا أي تعطُّل إلى %100.
أحسنت.
تذكَّروا أنّنا في بداية حديثنا طرحنا على أنفسنا السؤال التالي: "هل انتهيت؟"
قبل أن تواجه كل المشاكل المتعلقة بالاستجابة والذاكرة.
تم حلّ جميع المشاكل المتعلقة بأخطاء ANR وأخطاء نفاد الذاكرة وعمليات الإيقاف بسبب تجاوز الحد الأقصى للذاكرة وأعطال تلف الذاكرة الأصلية.
وحتى إذا كان تتبُّع تسلسل استدعاء الدوال البرمجية غير دقيق وكان السبب مخفيًا في مكان ما في سلاسل المحادثات في C++، يوفّر لك Android وFirebase Crashlytics الأداة اللازمة لتتبُّع المشكلة وحلّها بثقة.
بولينا غوفوركوفا
لنلخّص ما يمكنك استخدامه فور العودة إلى الكمبيوتر المحمول: إحصاءات سلاسل المحادثات حول أخطاء ANR والبيانات الوصفية الأخرى ذات الصلة من خلال ApplicationExitInfo، والجمع التلقائي لمشغّلات الإيقاف بسبب نفاد الذاكرة (OOM) ومحدِّد الذاكرة من خلال ProfilingManager، والدعم الكامل لـ GWP-ASan وArm MTE في ما يتعلّق بتلف الذاكرة الأصلية.
ونطلق أيضًا في نسخة حصرية لقطات لأجزاء من الذاكرة في الميدان على Android 17 أو الإصدارات الأحدث.
إذا أردت أن تكون من أوائل من يجرّبون ميزة جمع لقطات لأجزاء من الذاكرة تلقائيًا في Firebase Crashlytics وتحليلها باستخدام Perfetto وAndroid CLI، يُرجى التقاط صورة سريعة للشريحة والتسجيل من خلال الرابط.
بعد أن تناولنا معظم جوانب الموضوع، أودّ أن أشكركم على انضمامكم إليّ اليوم وعلى وقتكم.
نتمنى لك أن تظل سلسلة التعليمات الرئيسية غير محظورة، وأن تظل الذاكرة الديناميكية نظيفة، وأن يكون إصدارك التالي هو الأكثر استقرارًا حتى الآن.
لذا، أتمنى لك تصحيح أخطاء سعيد.
شكرًا جزيلاً لك مرة أخرى.
java.lang.NullPointerException
at MainActivity.onCreate(MainActivity.kt:42)
at android.app.Activity.performCreate
at android.os.Handler.dispatchMessage
at android.os.Looper.loopOnce
at android.app.ActivityThread.main"main" prio=5 tid=1 Blocked
| group="main" sCount=1 dsCount=0 flags=1
| state=S schedstat=( 1450239 48192 145 )
at DataManager.getData(DataManager.kt:25)
- waiting to lock <0x0a1b> held by thread 12
at MainActivity.onResume(MainActivity.kt:50)
at android.app.Activity.performResume
at android.os.Handler.dispatchMessage
at android.os.Looper.loopOnce
at android.app.ActivityThread.main14:32:15.105 1205-1250/system_process E/ActivityManager: ANR in com.chromaclip.app (com.chromaclip.app/.MainActivity)
PID: 14205
Reason: Input dispatching timed out
Dumped ANR trace to /data/anr/traces.txt
"main" prio=5 tid=1 Native
| group="main" sCount=1 dsCount=0 flags=1 obj=0x72a8c3d0 self=0x759fb92000
| sysTid=14205 nice=-10 cgrp=default sched=0/0 handle=0x7623910548
| state=S schedstat=( 1450239120 4819203 1453 ) utm=132 stm=13 core=4 HZ=100
| stack=0x7fed2a0000-0x7fed2a2000 stackSize=8MB
at sun.misc.Unsafe.park(Native method)
- waiting on an unknown object
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:190)
at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:279)
at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:85)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:59)
at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source:1)
at com.chromaclip.app.extractor.PaletteExtractor.extractColors(PaletteExtractor.kt:42)DefaultDispatcher-worker-1 (Root blocking)
dalvik.system.VMRuntime.allocateNode (Native method)
android.graphics.Bitmap.createBitmap (Bitmap.java:1124)
android.graphics.Bitmap.createBitmap (Bitmap.java:1091)
com.chromaclip.app.extractor.FrameDecoder.getUncompressedFrame (FrameDecoder.kt:112)
com.chromaclip.app.extractor.PaletteExtractor$processVideo$2.invokeSuspend (PaletteExtractor.kt:65)
kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith (ContinuationImpl.kt:33)
kotlinx.coroutines.DispatchedTask.run (DispatchedTask.kt:106)
java.util.concurrent.ThreadPoolExecutor.runWorker (ThreadPoolExecutor.java:1167)
java.util.concurrent.ThreadPoolExecutor$Worker.run (ThreadPoolExecutor.java:641)
java.lang.Thread.run (Thread.java:923)java.lang.OutOfMemoryError: Failed to allocate a 256 byte allocation
at android.graphics.Bitmap.createBitmap
at android.view.LayoutInflater.inflate(LayoutInflater.java:532)
at com.chromaclip.app.SettingsActivity.onCreate(SettingsActivity.kt:21)# 1. Install Android CLI & Perfetto skills
$ android skills add android-cli perfetto
# 2. List recent heap dumps from Crashlytics
$ android signals heaps list --kind oom --timeframe=90d
# 3. Download a heap dump as a Perfetto trace
$ android signals heaps download <heap-dump-id> \
--output ./heaps
# 4. Query top retained objects in the heap dump
$ trace_processor query ./heaps/oom.perfetto-trace ...
# 5. Fetch matching Crashlytics events
$ android signals heaps describe <heap-dump-id># Or ask your favorite LLM in natural language
> Using `android` command, inspect the latest heap dump
for my OOM issue and propose a fix
● Ran android signals heaps list · download · describe
● Analyzed ./heaps/oom.perfetto-trace (perfetto)
● Updated PaletteExtractor.kt (+6 -4 lines)// PaletteExtractor.kt
fun extractPalette(framePixels: IntArray): SparseIntArray {
- val pixelColors = mutableListOf<Pair<Color, String>>() // 8.3M objects per 4K frame!
- for (px in framePixels) {
- pixelColors.add(Color.valueOf(px) to "#%06X".format(px))
- }
+ val colorHistogram = SparseIntArray() // Downsampled & packed ARGB ints!
+ for (i in framePixels.indices step 16) {
+ val argb = framePixels[i]
+ colorHistogram.put(argb, colorHistogram.get(argb) + 1)
+ }
+ return colorHistogram
}// NativePaletteEngine.cpp
void NativePaletteEngine::processFrame(const uint8_t* rgba, size_t size) {
uint8_t* scratch = new uint8_t[size]; // Temporary frame buffer
transformColorSpace(rgba, scratch, size);
// BUG: Allocated every frame, but never freed!
+ delete[] scratch; // Free native buffer when done
}// effect_pipeline.cc
void EffectPipeline::onFilterSwitched(FilterPresetId preset) {
// BUG: Freed on worker thread while RenderThread is still reading it!
+ std::lock_guard<std::mutex> lock(lut_mutex_);
releaseLutBuffer(); // Frees old color-grading LUT
loadFilterPreset(preset); // Allocates new color-grading LUT
}