Ориентация главы

Глава 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

внедрено(N+1)-е выделение возвращает NULL →сработало

Две семантические детали диктуют написание тестов. Первая: 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 — замкнутый цикл внедрить, сработать, запросить, очистить, сбросить:

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;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single -include xrt.h impl.c examples/memory/fail_inject/main.c -lws2_32 -liphlpapienabled=1fail-after-2: 1st=ok 2nd=NULL triggered=1event[0]: alloccleared: 3rd=ok triggered-still=0reset=1

Что только что произошло. (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 — измерение эффекта пулинга на отрезке выделений через окно статистики:

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;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single impl.c examples/memory/stats/main.c -lws2_32 -liphlpapibefore=0 after=1malloc_calls=2 pooled=1 direct=1 backing=3

Что только что произошло. (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 нельзя выделять память (рекурсия в отладочную кучу) — только записывать и сопоставлять.

Типичные ошибки

Ловушка 1: ошибка на единицу в понимании FailAfter:Ловушка 1: ошибка на единицу в понимании FailAfter

Симптомы: OOM-тест «иногда» не попадает в целевой путь — внедрение включено, а перехватить нужное выделение не вышло, или перехват на одно раньше.

Причина: FailAfter(N) читают как «N-е падает». Фактическая семантика — первые N успешны, (N+1)-е падает: чтобы перехватить самое первое выделение, используйте N=0.

错误写法
xrtMemDebugFailAfter(1);   /* хотели, чтобы ПЕРВОЕ выделение упало */
p = xrtMalloc(64);         /* на деле: это прошло успешно */
p = xrtMalloc(64);         /* неудача случается здесь — на такт позже */
正确写法
xrtMemDebugFailAfter(0);   /* первые 0 успешны */
p = xrtMalloc(64);         /* первое же выделение падает: ровно то, что нужно */
Ловушка 2: бенчмарки на включённой отладочной куче:Ловушка 2: бенчмарки на включённой отладочной куче

Симптомы: цифры бенчмарка сильно просели; после «оптимизации» перемер всё равно плох; точку изменения в истории не найти.

Причина: отладочная куча записывает подробности каждого выделения с позициями, в разы дороже обычного выделения — цифры, снятые при включённой куче, не являются реальной производительностью программы.

错误写法
xrtMemDebugEnable(true);
run_benchmark();          /* в цифры замешана цена самой отладочной кучи */
xrtMemDebugEnable(false);
正确写法
xrtMemStatsEnable();      /* наблюдать — так статистикой: почти бесплатно */
xrtMemStatsReset();
run_benchmark();
xrtMemStatsGet(&Stats);   /* счётчики на месте, цифры производительности заслуживают доверия */

Практические задания

Базовое: внедрить и восстановить

Следуя примеру fail_inject: после FailAfter(1) сделайте два выделения подряд и утвердите: первое успешно, второе падает, флаг срабатывания истинен; после FailClear третье успешно, а флаг снова ноль. Все четыре утверждения печатаются как ok.

Продвинутое: OOM-осмотр кода главы 5

Заверните главный поток примера 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 записей)

Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки