‫Google Chat چگونه بیش‌از ۳۰ مشکل حافظه را با Chrome DevTools برای عامل‌ها برطرف کرد

Vatan Aggarwal
Vatan Aggarwal Software Engineer

تاریخ انتشار: ۲۹ اکتبر ۲۰۲۶

Google Chat یک برنامه بزرگ تک‌صفحه‌ای است. کاربران معمولاً برنامه وب را برای روزها یا هفته‌ها در زبانه مرورگرشان باز می‌گذارند. در برنامه‌هایی که طول عمر زیادی دارند، سلامت حافظه سمت کارخواه نگرانی اصلی است. بستارهای جمع‌آوری‌نشده و درخت‌های DOM جداشده به‌آرامی ایجاد می‌شوند و درنهایت بر عملکرد واسط کاربر، وقفه‌های جمع‌آوری زباله، یا ازکارافتادن برگه‌ها در دستگاه‌های محدود تأثیر می‌گذارند.

برای رفع این مشکل، تیم Google Chat طرحی را برای شناسایی و رفع مشکلات حافظه راه‌اندازی کرد. آن‌ها از Chrome DevTools برای عامل‌ها، که شامل سرور DevTools MCP است، استفاده کردند تا فرایند دستی دشواری را به گردش کار خودکار تبدیل کنند.

چالش: سوزن در انبار کاه ۵۰۰ مگابایتی

شناسایی دقیق اینکه چرا شیء نشت‌شده در حافظه نگهداری می‌شود مستلزم ردیابی مسیرهای نگهداری ازطریق کد به‌شدت کوچک‌شده است. اگرچه بهبودهای اخیر در پانل حافظه Chrome DevTools (برای مثال، فیلتر کردن پیشرفته لبه و پرسمان شیء) این کار را آسان‌تر می‌کند، انجام دستی آن در یک عکس آنی ۵۰۰ مگابایتی همچنان می‌تواند کند و از نظر ذهنی خسته‌کننده باشد.

و رفع نشتی به دو مجموعه دانش متمایز نیاز دارد:

  • باید بدانید موتور جاوا اسکریپت V8 چگونه بایت‌کد کامپایل‌شده را مدیریت می‌کند.
  • همچنین باید مدیریت وضعیت خاص برنامه و چرخه حیات مؤلفه را درک کنید.

پل زدن بین این دو مجموعه دانش متمایز به‌صورت دستی معمولاً چند روز طول می‌کشد تحقیق و همکاری با چندین توسعه‌دهنده در هر نشت.

راه‌حل: پیمایش انبوهه با نماینده

تیم متوجه شد که اگرچه مدل‌های زبانی بزرگ در استدلال روی داده‌های ساختاریافته بسیار خوب عمل می‌کنند، اما برای پردازش فایل‌های حجیم به ابزارهای مناسبی نیاز دارند تا مصرف داده‌واحد را در حد معقول نگه دارند. با متصل کردن Google Antigravity به سرور MCP متن‌باز Chrome DevTools با ابزارهای حافظه فعال (--memoryDebugging=true)، آن‌ها به عامل دسترسی برنامه‌ریزی‌شده مستقیم به توده V8 دادند.

به‌جای اینکه توسعه‌دهنده به‌صورت دستی درخت‌های نگهدارنده را در پانل حافظه Chrome DevTools گسترش دهد، عامل مستقیماً از عکس آنی پرس‌وجو کرد. تیم از پیام‌واره‌ای ساده برای شروع فرایند استفاده کرد:

Discover memory leaks in the heap snapshot chat.heapsnapshot using Chrome DevTools memory tools.

بااستفاده از مهارت اشکال‌زدایی حافظه، عامل گردش کار ساختاریافته‌ای را بااستفاده از ابزارهای MCP خاص اجرا کرد:

  1. ضبط یا بار کردن عکس آنی: بااستفاده از ابزارهایی مثل take_heapsnapshot یا خواندن فایل موجود.
  2. تجزیه‌وتحلیل ساختار پشته: بااستفاده از get_heapsnapshot_details و compare_heapsnapshots، منابع رایج نشت را اسکن کرد.
  3. ردیابی مسیرهای نگه‌دارنده: از get_heapsnapshot_retaining_paths برای بازگشت به ریشه‌های جمع‌آوری زباله در درخت غالب استفاده کرد.
  4. تولید اصلاحات: مسیرهای نگه‌دارنده را با کد منبع برنامه ارجاع متقابل داد تا وصله‌های بسیار خاصی را خروجی دهد.

اطلاعات آماری فنی: سه الگوی رایج نشت حافظه

این عامل بیش‌از ۳۰ نشت متمایز را شناسایی کرد. سه مورد زیر تأثیرگذارترین الگوهایی هستند که عامل پیدا کرده است و نشان‌دهنده بیشترین صرفه‌جویی در حافظه و رایج‌ترین الگوهای ضد معماری در برنامه‌های وب مدرن هستند.

تله ردیابی پشته‌ای خطای ایستا

عامل تشخیص داد که بزرگ‌ترین نشت تکی مربوط به اشیاء خطای ازپیش اختصاص‌داده‌شده بود که به‌طور دائم نزدیک به ۲۰ مگابایت حافظه را حفظ می‌کردند. برای مثال، یک ثابت خطای ایستا (EMPTY_KEY_ERROR) به‌تنهایی ۱۴٫۱ مگابایت را حفظ کرد.

مشکل: در موتورهای جاوا اسکریپت مدرن مانند V8، ایجاد یک شیء Error یک عکس آنی از پشته فراخوانی فعلی را برای کمک به اشکال‌زدایی ثبت می‌کند. فضای حفظ‌شده عظیم به‌دلیل همین سازوکار رخ داده است:

  • خطا یک ثابت سراسری بود: درطول ارزیابی اولیه ماژول نمونه‌سازی شد.
  • موتور جاوا اسکریپت پشته را ضبط کرد: تابع فعال در پشته در آن زمان، پوشاننده مقداردهی اولیه اصلی برنامه بود (یک عبارت تابع فراخوانی‌شده فوری یا IIFE).
  • آرایه حافظه را به دام انداخت: چون برنامه هرگز خطا را جمع‌آوری زباله نکرد، آرایه ردیابی پشته داخلی آن ارجاعی دائمی به تابع مقداردهی اولیه بسته اصلی داشت.
// Ineffective pattern: Instantiating Error objects at module evaluation time
export const EMPTY_KEY_ERROR = new Error("Key cannot be empty");

export function validateKey(key: string) {
  if (!key) {
    throw EMPTY_KEY_ERROR;
  }
}

راه‌حل: تیم کد را تغییر داد تا خطاها به‌صورت پویا هنگام ایجاد شدن تولید شوند. اگر خطای ثابت چندبارمصرف کاملاً ضروری است، پاک کردن صریح پشته (EMPTY_KEY_ERROR.stack = '';) بلافاصله فریم‌های داخلی V8 ضبط‌شده را آزاد می‌کند.

// Recommended: Construct errors dynamically
export function validateKey(key: string) {
  if (!key) {
    throw new Error("Key cannot be empty");
  }
}

// Or clear the stack property if a static instance is necessary:
export const REUSABLE_ERROR = new Error("Static error");
REUSABLE_ERROR.stack = ""; // Releases the captured initialization frame

مقیاس‌بندی شنوندگان رویداد پنجره جهانی

عامل مشکل مقیاس‌بندی حافظه مربوط به visibilitychange شنونده رویداد را پیدا کرد. While this is not a strict "leak" (since they are eventually cleaned up when the component unmounts), it increased memory usage because there was one listener for every message on the screen. عکس آنی ۱٬۱۴۲ شنونده رویداد داخلی را نشان داد که ۱۲۸ شنونده فعال مستقیماً به شیء پنجره سراسری متصل بودند.

مشکل: در جاوا اسکریپت، فراخوانی‌های برگشتی شنونده رویداد از نوع بستار هستند. یک closure ارجاعاتی به دامنه واژگانی دربرگیرنده خود نگه می‌دارد تا اطمینان حاصل شود که متغیرهای موردنیاز آن قابل‌دسترسی باقی می‌مانند. The agent tracked 126 of these listeners back to a single component: the RelativeTimeController. هر مُهر زمان در واسط کاربر Chat (برای مثال، «۵ دقیقه پیش») کنترل‌کننده خودش را مدیریت می‌کرد که شنونده رؤیت‌پذیری خصوصی را برای به‌روزرسانی زمان ثبت می‌کرد. چون هر شنونده نمونه کنترل‌کننده خود را به ریشه پنجره متصل کرده بود، شبکه متراکمی از بستارهای فعال بخش قابل‌توجهی از حافظه را در طول عمر رشته گپ قابل‌مشاهده اشغال می‌کرد.

// Ineffective pattern: Attaching global window listeners in individual UI controllers
export class RelativeTimeController {
  constructor() {
    this.visibilityListener = () => this.updateTime();
    window.addEventListener("visibilitychange", this.visibilityListener);
  }

  // If dispose() fails to run or unregister, the closure pins the controller
}

راه‌حل: تیم رویدادهای پنجره جهانی را متمرکز کرد. TimerService مشترک اکنون دقیقاً یک شنونده رؤیت‌پذیری را در پنجره ثبت می‌کند و به‌روزرسانی‌ها را برای همه کنترل‌کننده‌های مُهر زمان فعال پخش می‌کند. این کار فوراً ۱۲۶ شنونده را به دقیقاً ۱ شنونده کاهش داد.

// Recommended: Centralized event coordination using a shared service
export class VisibilityService {
  private listeners = new Set<() => void>();

  constructor() {
    // Single global listener throttled for the entire application
    window.addEventListener("visibilitychange", () => this.notifyAll());
  }

  subscribe(callback: () => void): () => void {
    this.listeners.add(callback);
    return () => this.listeners.delete(callback); // Unsubscribe handle
  }
}

کیت توسعه نرم‌افزار طرف سوم و نگهدارنده‌های iframe جاسازی‌شده

گره‌های جداشده DOM عناصری هستند که از درخت سند برداشته شده‌اند اما هنوز متغیر جاوا اسکریپت به آن‌ها ارجاع می‌دهد. اگر یک عنصر در درخت DOM جداشده توسط متغیر ریشه حفظ شود، کل درخت جداشده را نمی‌توان بازیابی کرد.

مشکل: وقتی کاربران بین فضاهای گپ پیمایش می‌کردند، نماینده متوجه شد که کل درخت‌های DOM جداشده در حافظه باقی می‌مانند. مسیر نگهداری را به پخش‌کننده رسانه جاسازی‌شده در واسط کاربر فضای گپ ردیابی کرد. وقتی فضای گپ جدا شد، مؤلفه واسط کاربر از صفحه برداشته شد، اما کیت توسعه نرم‌افزار پخش‌کننده رسانه اصلی هرگز به‌طور صریح ازبین نرفت. کتابخانه طرف سوم یک واژه‌نامه جهانی داخلی از شناسه‌های iframe فعال را حفظ می‌کرد و DOM جداشده را زنده نگه می‌داشت.

راه‌حل: مطمئن شوید که همه iframeهای جاسازی‌شده و ابزاره‌های طرف سوم به چرخه‌های حیات حذف مؤلفه مرتبط هستند. فراخوانی صریح روش .destroy() در پخش‌کننده، ورودی را از دفتر ثبت جهانی کیت توسعه نرم‌افزار پاک می‌کند و به DOM اجازه می‌دهد تا زباله‌روبی شود.

// Recommended: Explicit teardown of embedded frames and SDK instances
export class MediaPlayerController {
  private player: ThirdPartyPlayer | null = null;

  dispose(): void {
    if (this.player) {
      this.player.destroy(); // Clears entry from global SDK registry
      this.player = null;
    }
  }
}

الگوهای دیگر

درحالی‌که سه الگوی ذکرشده در بالا ازنظر مگابایت‌های خالص ذخیره‌شده بیشترین تأثیر را داشتند، عامل بیش‌از ۲۷ نشتی دیگر را نیز شناسایی و برطرف کرد. این موارد عبارت‌اند از:

  • دور سنجی آرایه تصویر: جایی که درخت‌های DOM جداشده توسط آرایه‌های تشخیصی سفارشی که سنجه‌های تصویر بالای خط تا را ردیابی می‌کنند، زنده نگه داشته شدند
  • اشکالات چرخه حیات مؤلفه چارچوب: جایی که منوهای کرکره‌ای سفارشی به بدنه سند منتقل شدند و از لغو نصب والدین اصلی خود عبور کردند
  • تله بسته شدن شیء آرگومان‌های ضمنی: جایی که بسته‌های داخلی به‌طور ناخواسته کل آرایه آرگومان‌های توابع خارجی را ضبط کردند

تأثیر: مدیریت پیش‌کنشی حافظه به‌عنوان گردش کار

تیم Google Chat با مجهز کردن عامل کدنویسی به Chrome DevTools برای عامل‌ها، تکلیفی بسیار تخصصی را خودکارسازی کرد. ردیابی نشت حافظه پیچیده از چند روز پیمایش دستی توده به چند دقیقه تجزیه‌وتحلیل خودکار کاهش یافت.

درست مانند P2ER که «تأیید تجربی اجباری» را برای اعمال پایداری رابط کاربری پیاده‌سازی کرد، تیم‌ها می‌توانند این رویکرد ممیزی حافظه را به‌عنوان یک حفاظ پیشگیرانه اتخاذ کنند. توسعه‌دهندگان اغلب مشکلات حافظه را نادیده می‌گیرند، با این فرض که این مشکلات فقط در برنامه‌های بسیار بزرگ یا پیچیده رخ می‌دهند. بااین‌حال، همان‌طور که این مطالعه موردی نشان می‌دهد، الگوهای رایج مانند شنوندگان رویداد استاندارد یا خطاهای ایستا می‌توانند به‌طور خاموش انباشته شوند. ادغام ابزارهایی مانند Chrome DevTools برای عامل‌ها در چرخه توسعه روزانه شما، هوش مصنوعی را از یک تولیدکننده کد پایه به یک دروازه کیفیت قابل‌اعتماد تبدیل می‌کند که رگرسیون‌های خاموش را قبل‌از رسیدن به مرحله تولید شناسایی می‌کند.