Ориентация главы
Глава 5 стянула память всей библиотеки в пять точек входа; эта глава выплачивает ещё два дивиденда стока: внедрение сбоев — xrtMemDebugFailAfter заставляет (N+1)-е выделение упасть точно в срок, чтобы проверить чистоту отката путей отказа; и статистику — xrtMemStatsGet набором счётчиков отвечает «сколько выделялось, какова доля попаданий в пул, каков пик». Модули чётко делят труд: отладочный модуль записывает каждую операцию подробно (дорого, для локализации проблем), модуль статистики только считает (почти бесплатно, можно держать включённым в продакшене). Каждый тестовый вариант _oom в остальной части книги построен на точке внедрения этой главы — одна из методологических опор инженерного качества XRT.
6.1引入
Задайте жестокий вопрос: что делает ваша программа, когда 1000-е выделение падает? Честный ответ большинства кода — «не знаем»: путь отказа никогда систематически не выполнялся, ведь в нормальных запусках 1000-е выделение всегда успешно. Когда в продакшене память иссякает, эти никогда не выполнявшиеся откаты запускаются впервые — а именно их проще всего написать неправильно: пропущенное освобождение, освобождение в неправильном порядке, продолжение работы с полуинициализированными структурами.
Старый приём тестирования — заменить malloc на падающий макрос, но это покрывает только ваш код. Поскольку XRT стянут в одно горлышко на всю библиотеку (глава 5), одна строка xrtMemDebugFailAfter(N) заставляет библиотеку и бизнес-код упасть вместе на (N+1)-м выделении — разобранный наполовину JSON, рукопожатие TLS на середине: их откат при неудаче впервые можно проверять поодиночке. Это общий фундамент сотен OOM-вариантов тестов в репозитории.
Контраст внедрению сбоев — наблюдение на стороне продакшена: память резидентного сервиса медленно ползёт вверх, и прежде чем ловить отдельные случаи, нужно ответить «что именно растёт». Пара модулей XRT ровно соответствует этим двум направлениям — последний раздел главы собирает их в полную воронку расследования.
6.2概念
Два модуля, два рода наблюдения
| Модуль | Что записывает | Цена | Когда включать |
|---|---|---|---|
memory_stats | Счётчики: число вызовов, байты, пики, попадания в пул | Почти нулевая | Постоянно в продакшене или окнами |
memory_debug | Подробности каждого выделения/освобождения с позициями вызовов | Заметная | При локализации утечек и внедрении сбоев |
Принцип разделения труда: числами смотрят тренды, деталями ищут отдельные случаи. Подозреваете медленный рост памяти — сначала включите статистику и смотрите кривую живых байт; утечка подтверждена — включайте отладочную кучу и потоком событий зафиксируйте место рождения. Переключатели двух модулей независимы, можно включать любой один: ежедневная рекомендация — только статистика, а отладочную кучу берегите на день, когда что-то пойдёт не так.
Внедрение сбоев: точная семантика FailAfter
Две семантические детали диктуют написание тестов. Первая: FailAfter(1) означает «1-е успешно, 2-е падает» — параметр N считает первые N успехов, неудача случается на (N+1)-м, а не на N-м. Вторая: FailClear сбрасывает внедрение и флаг «сработало» вместе, а xrtMemDebugFailTriggered запрашивает, сработало ли — утверждение «неудача действительно случилась в ожидаемом месте» держится на нём.
Поток событий: воспроизведение в порядке времени
xrtMemDebugVisit в форме обратного вызова посещает отладочные события (каждое выделение, освобождение, внедрение) в порядке времени, а xrtMemDebugEventName превращает перечисление событий в стабильное имя. Вместе с информацией позиций семейства At из главы 5 цепочка информации для локализации одной утечки полна: поток событий говорит «какое выделение не освободили», информация позиций — «в какой строке оно родилось».
В том же семействе три инструмента поудобнее: xrtMemDebugVisitLive посещает только ещё живые выделения (поиск пар сразу пропускает шум освобождённых); xrtMemDebugSnapshot одним вызовом снимает «текущее число живых операций и байт», удобно для контрольных точек на бизнес-этапах («после обработки этой пачки запросов живые байты должны вернуться к базовой линии»); xrtMemDebugReport форматирует подробности в файл или обратный вызов, годится для архивируемых отчётов об утечках. Мнемоника выбора: сравнение — снимком, поиск утечек — Live, архив — Report.
Чтение статистики: как смотреть на числа
Среди счётчиков xmemstats самую диагностическую ценность имеет связь трёх чисел: число выделений на стороне бизнеса, попадания в пул, сквозные проходы к бэкенду — и фактическое число системных вызовов. Малые блоки, попадающие в кэш классов размеров, не порождают системных вызовов — «100 бизнес-выделений, система вызвана 3 раза» означает, что пулинг работает. Окно статистии размечает xrtMemStatsReset, после отрезка нагрузки показания снимаются одним xrtMemStatsGet. Пик вместе с текущим значением отвечает и на вопросы планирования ёмкости: кто вносит вклад в потолок резидентной памяти процесса и растёт ли он пропорционально нагрузке — такие вопросы системно разворачиваются в главе тома 12 об анализе производительности.
6.3示例
Полная программа: полный цикл FailAfter
Из примера репозитория examples/memory/fail_inject/main.c — замкнутый цикл внедрить, сработать, запросить, очистить, сбросить:
*/ #include <stdio.h> #include <xrt.h> /* 事件访问器:只打印前几个 alloc 事件的名字。 */ static bool printEvent(const xmemdebugevent* pEvent, ptr pUserData) { size_t* pCount = (size_t*)pUserData; if ( *pCount < 1u && pEvent->Kind == XMEMDEBUG_ALLOC ) { printf("event[%zu]: %s\n", *pCount, xrtMemDebugEventName(pEvent->Kind)); } (*pCount)++; return true; } int main(void) { ptr pFirst; ptr pSecond; ptr pThird; size_t iEvents = 0; (void)xrtMemDebugReset(); (void)xrtMemDebugEnable(true); printf("enabled=%d\n", xrtMemDebugEnabled() ? 1 : 0); /* 注入:第 2 次分配失败。 */ (void)xrtMemDebugFailAfter(1u); pFirst = xrtMalloc(16); pSecond = xrtMalloc(16); printf("fail-after-2: 1st=%s 2nd=%s triggered=%d\n", pFirst != NULL ? "ok" : "NULL", pSecond != NULL ? "ok" : "NULL", xrtMemDebugFailTriggered() ? 1 : 0); xrtFree(pFirst); /* 事件流:最早的 alloc 事件可按名访问。 */ (void)xrtMemDebugVisit(printEvent, &iEvents); /* 清除未触发注入后再分配恢复成功;已触发的标志保持。 */ xrtMemDebugFailClear(); pThird = xrtMalloc(16); printf("cleared: 3rd=%s triggered-still=%d\n", pThird != NULL ? "ok" : "NULL", xrtMemDebugFailTriggered() ? 1 : 0); xrtFree(pThird); (void)xrtMemDebugEnable(false); printf("reset=%d\n", xrtMemDebugReset() ? 1 : 0); return 0; }
Что только что произошло. (1) xrtMemDebugEnable включает отладочную кучу — и внедрение, и поток событий зависят от неё. (2) После FailAfter(2): 1-е выделение успешно, 2-е возвращает NULL, FailTriggered подтверждает срабатывание — три утверждения прибивают «неудача случилась в ожидаемом месте». (3) xrtMemDebugVisit воспроизводит поток событий, печатая стабильное имя первого события выделения alloc. (4) После FailClear 3-е выделение возвращается к норме, и флаг срабатывания обнуляется вместе. (5) Выключение отладочной кучи всё сбрасывает. Эта 50-строчная программа — скелет всех OOM-тестов: замените середину вашими бизнес-вызовами — и можно проверять их откат при неудаче.
Превратить скелет в полный тест — два шага. Первый: превратить «одно внедрение» в «полный перебор» — N растёт от 0 до общего числа выделений, и на каждом N бизнес-функция прогоняется целиком; падение, провал утверждения или ненулевая живая память на любом N ловит дефект пути отказа. Второй: добавить «утверждение восстановления» — после конца сканирования внедрение выключается, и бизнес должен полностью вернуться к нормальному поведению. Тестовые варианты _oom в репозитории (JSON, TLS, сети — они есть у каждого модуля) все такой формы; в томе 12 вы встретите их целыми подразделениями.
Полная программа: чтение статистики
Из examples/memory/stats/main.c — измерение эффекта пулинга на отрезке выделений через окно статистики:
*/ #include <stdio.h> #include <xrt.h> int main(void) { xmemstats tStats; ptr pSmall; ptr pLarge; /* 开关可查询:先确认当前状态,再打开并清零。 */ printf("before=%d\n", xrtMemStatsEnabled() ? 1 : 0); xrtMemStatsEnable(true); printf("after=%d\n", xrtMemStatsEnabled() ? 1 : 0); xrtMemStatsReset(); /* 一次小块(走池)+ 一次大块(直通)。 */ pSmall = xrtMalloc(48); pLarge = xrtMalloc(4096); if ( (pSmall == NULL) || (pLarge == NULL) ) { xrtFree(pSmall); xrtFree(pLarge); return 1; } xrtFree(pSmall); xrtFree(pLarge); /* 读取窗口内计数(字段语义见文件头读数解释)。 */ xrtMemStatsGet(&tStats); printf("malloc_calls=%llu pooled=%llu direct=%llu backing=%llu\n", (unsigned long long)tStats.MallocCalls, (unsigned long long)tStats.PooledAllocCalls, (unsigned long long)tStats.DirectAllocCalls, (unsigned long long)tStats.BackingAllocCalls); /* 用完关闭,回到零开销模式。 */ xrtMemStatsEnable(false); return 0; }
Что только что произошло. (1) xrtMemStatsEnable включает сбор; before=0 after=1 подтверждает срабатывание переключателя. (2) Бизнес делает два xrtMalloc: малый блок 48 байт попадает в кэш классов размеров (pooled=1, ноль системных вызовов), большой блок 4096 байт идёт напрямую в бэкенд (direct=1). (3) backing=3 — фактическое число обращений к системному аллокатору: одно сквозное плюс внутренние выделения пула на прогрев классов размеров; «2 бизнес-вызова, 3 системных» говорит о только что прогретом пуле — в устоявшемся режиме это отношение продолжает сжиматься. Читайте сначала связь этих трёх чисел, затем объёмы байт и пик.
Применение этих показаний к реальному сервису — измерение окном: снимайте снимок на двух бизнес-этапах — например «до обработки пачки запросов» и «после полной обработки и возврата» — и сравнивайте, возвращаются ли живые байты к базовой линии. Вернулись — пачка ничего не утекла; нет — разница и есть подозреваемый объём, и тогда включайте отладочную кучу и выборками Live выуживайте отдельные случаи. Статистика отвечает «есть ли проблема и какова величина», отладка — «какое именно выделение»: двухслойная воронка, чтобы не бросаться сразу в дорогой режим детализации.
6.4契约
- Дисциплина расходов: отладочная куча включается только при локализации проблем; чувствительные к производительности пути и продакшн-норма используют модуль статистики; любые цифры производительности, снятые при включённой отладочной куче, не считаются.
- Семантика внедрения:
FailAfter(N)= первые N успешны, (N+1)-е падает;FailClearсбрасывает вместе с флагом срабатывания;FailTriggered— основание для утверждений. - Тройка утверждений: до внедрения не сработало, неудача действительно случилась, флаг сработывания перевернулся в истину — стандартный скелет OOM-теста; после перебора всех N бизнес возвращается к норме.
- Окно статистики:
Resetотмечает начало, прогоняется нагрузка,Getснимает показания; в окне переключателями не трогать; возврат памяти пачкой оценивается сравнением «снапшотов до/после». - События только читаются: в обратных вызовах
Visit/VisitLiveнельзя выделять память (рекурсия в отладочную кучу) — только записывать и сопоставлять.
Типичные ошибки
Симптомы: OOM-тест «иногда» не попадает в целевой путь — внедрение включено, а перехватить нужное выделение не вышло, или перехват на одно раньше.
Причина: FailAfter(N) читают как «N-е падает». Фактическая семантика — первые N успешны, (N+1)-е падает: чтобы перехватить самое первое выделение, используйте N=0.
xrtMemDebugFailAfter(1); /* хотели, чтобы ПЕРВОЕ выделение упало */ p = xrtMalloc(64); /* на деле: это прошло успешно */ p = xrtMalloc(64); /* неудача случается здесь — на такт позже */
xrtMemDebugFailAfter(0); /* первые 0 успешны */ p = xrtMalloc(64); /* первое же выделение падает: ровно то, что нужно */
Симптомы: цифры бенчмарка сильно просели; после «оптимизации» перемер всё равно плох; точку изменения в истории не найти.
Причина: отладочная куча записывает подробности каждого выделения с позициями, в разы дороже обычного выделения — цифры, снятые при включённой куче, не являются реальной производительностью программы.
xrtMemDebugEnable(true); run_benchmark(); /* в цифры замешана цена самой отладочной кучи */ xrtMemDebugEnable(false);
xrtMemStatsEnable(); /* наблюдать — так статистикой: почти бесплатно */ xrtMemStatsReset(); run_benchmark(); xrtMemStatsGet(&Stats); /* счётчики на месте, цифры производительности заслуживают доверия */
Практические задания
Следуя примеру fail_inject: после FailAfter(1) сделайте два выделения подряд и утвердите: первое успешно, второе падает, флаг срабатывания истинен; после FailClear третье успешно, а флаг снова ноль. Все четыре утверждения печатаются как ok.
Заверните главный поток примера MemDup из главы 5 в цикл внедрения: точку внедрения увеличивайте от N=0, после каждого прогона утверждайте, что программа завершается с ожидаемым кодом неудачи и живые байты равны нулю (проверьте своим счётным аллокатором или статистикой). Подсказка: именно так пишутся варианты _oom репозитория; если при каком-то внедрении программа не завершается как неудача (скажем, продолжает работать) — это уже дефект пути отказа: запишите его, это самый ценный результат упражнения.
Напишите помощник leak_check(): включить отладочную кучу → прогнать исследуемую функцию (с одним намеренно спрятанным пропущенным освобождением) → Visit потока событий, подсчитав непарные выделения → напечатать размер и позицию каждой утечки (__FILE__:__LINE__). Критерий приёмки: для тестового кода «пропущен один Free» точно сообщается одна строка утечки; для чистого кода — ноль утечек.
Итоги
| Тема | Краткая справка |
|---|---|
| Разделение труда | stats считает и всегда включён для трендов; debug детализирует по надобности для случаев — двухслойная воронка |
| Внедрение | FailAfter(N): первые N успешны, (N+1)-е падает; FailClear полностью сбрасывает |
| Утверждения | до внедрения не сработало → неудача случилась → FailTriggered перевернулся в истину |
| Поток событий | Visit воспроизводит по времени; EventName стабильные имена; выделять в обратный вызовах запрещено |
| Снапшот/Live/Report | сравнение — снимком, поиск утечек — Live, архив — Report |
| Окно статистики | Enable → Reset → нагрузка → Get; смотреть связь трёх чисел pooled/direct/backing |
| Скелет OOM-теста | внедрение + бизнес-вызов + утверждение отката + проверка нуля живых; полный перебор всех N |
📖 Справочники API в этой главе:memory_debug (19 записей)memory_stats (5 записей)
Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки