Ориентация главы
Том 13 открывается первым полным проектом. Каждая глава предыдущих двенадцати томов говорила «как пользоваться одним модулем»; эта глава устанавливает пять модулей в одну программу — инструмент статистики журналов (logstat): прочитать сервисный журнал (смешанные концы строк, возможно очень большой), отфильтровать по уровню, посчитать строки уровней и образцы ошибок, собрать текстовый отчёт. Звучит скромно, но каждый шаг — главная тема какой-то ранней главы: чтение строк (xlinereader главы 41 — адаптация концов строк, последняя строка без перевода, заимствованные представления); определение уровня (операции над представлениями строк главы 25 — префиксное совпадение без копий); подсчёт и дедупликация (хеширование и Map глав 12/18); генерация отчёта (построитель главы 25 — склейка O(n)); запись на диск (файлы и временные каталоги глав 45/47 — никогда не предполагать рабочий каталог записываемым). Форма главы проекта тоже отлична от учебной: сначала пишем требования и приёмку, затем проектные решения (почему эти модули, компромисс каждого шага), потом реализацию (две полные программы с нарастанием), наконец приёмку и расширения — полный инженерный цикл.
138.1引入
Требование из реальной сцены: сервис упал в полночь, вы получили файл журнала на 2 ГБ (скопированный rsync — концы строк смешанные: CRLF от старого сегмента Windows, LF от сегмента Linux, даже одинокие CR от какого-то транскодирующего инцидента), и надо быстро ответить на три вопроса: сколько строк каждого уровня? Где случились ошибки (первые образцы)? Какой временной отрезок самый активный? Универсальные инструменты (grep+awk) склеиваются, но проблему концов приходится обрабатывать снова и снова, а логика вроде «дедупликации образцов ошибок» пишется неуклюже; одноразовый скрипт трудно переиспользовать.
Сделайте из этого приличный маленький инструмент logstat: logstat <日志路径> [--level ERROR] [--top 10] [--report] (путь к журналу) — адаптивное по концам чтение, фильтрация уровней, подсчёт, образцы ошибок, опциональный файл отчёта. Критерии приёмки (записанные до кода): (1) ввод со смешанными концами обрабатывается корректно без настройки; (2) файл 2 ГБ при памяти уровня МБ (потоково — не читать целиком); (3) три границы корректны — пустой файл, нет совпадений уровня, ввод сплошных ошибок; (4) отчёт ложится в системный временный каталог (никогда не предполагать рабочий каталог записываемым), имя файла несёт метку времени без двоеточий; (5) каждый шаг обработки ошибок идёт через структурную ошибку (глава 4 — цепочка ошибок полна до локализуемости).
138.2概念
Проектные решения: выбор модулей и обоснования
| Шаг требования | Выбор | Обоснование и отвергнутые альтернативы |
|---|---|---|
| Чтение строк журнала | xrtLineReaderTake+Reader (глава 41) | адаптация концов (все три распознаются) / заимствованные представления без копий / рост по надобности после начальной ёмкости. Альтернатива «прочитать целиком, затем strtok»: память порядка файла, ручная обработка концов — отвергнуто |
| Определение уровня | префиксное сравнение представлений (глава 25) | каждая строка начинается LEVEL rest — один срез представления без копий. Альтернатива «strdup каждой строки, затем разбор»: одно выделение на строку — отвергнуто |
| Подсчёт | массив фиксированной длины (пять уровней) | уровни — малое фиксированное множество — массив быстрее Map и с нулём выделений. Альтернатива Map: пушка по воробьям — отвергнуто («частота произвольных токенов» — вот когда применима глава 18) |
| Образцы ошибок | кольцевой буфер фиксированного размера (первые N) | нужны лишь «первые несколько» — кольцо перезаписывает. Альтернатива «сохранить всё, затем взять N»: память взрывается на большом журнале ошибок — отвергнуто |
| Сборка отчёта | построитель xstrbuf (глава 25) | многосегментная O(n); альтернатива повторный Concat: O(n²) — урок главы 25 применим напрямую |
| Запись на диск | временный каталог + текстовая запись (главы 45/47) | не предполагать рабочий каталог записываемым; метка без двоеточий (запрещённый символ Windows) — две инженерные детали образца file/report |
Методология выбора: в каждой строке спрашивайте «каковa форма данных» (поток или целый блок, фиксированное множество или динамические ключи, сохранять или нет) — форма решает контейнер: решение о выборе из главы 23, впервые применённое на всём проекте. Если цель обновления — «диспетчеризация входа по префиксу пути/ключа на десятки обработчиков», родич pattern главы 30 с его «скомпилировать много образцов разом» построен ровно под эту форму.
Архитектура конвейера: пять потоковых ступеней
Смысл потоковости ещё раз в числах: память = буфер чтения строк (растёт с самой длинной строкой, не с размером файла) + счётчики (O(1)) + кольцо образцов (O(N)) — не зависит от 2 ГБ. Это проектная гарантия критерия (2).
Границы и план ошибок
До написания кода продумайте три класса границ (критерий (3) в развёртке): пустой файл — первый Next уже END: вывести все нулевые счётчики с пометкой «нет ввода» (не ошибка); нет совпадений уровня (ноль попаданий после фильтра): нормальный вывод нулевых счётчиков (условие фильтра пользователя — не вина программы); сплошные ошибки: кольцо заполняется и перезаписывает, счётчики верны. Пози обработки ошибок: отказ открытия/чтения/записи идут через возвращаемое значение + слот ошибки —ветви ошибок главной функции единообразно печатают xrtErrorMessage с цепочкой причин; ни одного exit без освобождения ресурсов (одно-выходная дисциплина cleanup — форма образцов главы 45).
Отличия от учебных образцов
Оба используют xlinereader; учебный образец (глава 41) разбирает три фиксированные строки, а проект сталкивается с реальным потоком неизвестной длины — различие в трёх местах: автоматический рост после начальной ёмкости становится обязательным (в журнале бывают сверхдлинные строки стека); различение END и ERROR становится ключом корректности (диск испортился посреди чтения = ERROR сообщить; нормальное завершение = END, не сообщать); жизненный цикл представления строки требует дисциплины (кольцо образцов хранит копии, не представления — приземление дисциплины «представления живут одним кадром» глав 96/104).
138.3示例
Первая полная программа: ядро чтения строк (минимальный замкнутый цикл)
Программа ниже из examples/io/line — учебниковая форма строчного движка logstat (образец со смешанными концами и последней строкой без перевода):
*/ #include <stdio.h> #include <xrt.h> int main(void) { /* 混合行尾 + 末行无换行的日志样本。 */ static const char sLog[] = "INFO server started\r\n" "WARN queue is busy\n" "ERROR request failed"; xreader* pReader; xlinereader* pLines; xlineview Line; /* 借用:到下一次 Next 前有效 */ xlinenext Next; size_t iCount = 0u; int iResult = 1; */ pReader = xrtReaderFromMemory(XRT_BYTES_LITERAL(sLog)); pLines = xrtLineReaderTake(&pReader, 1024u); if ( pLines == NULL ) { xrtReaderDestroy(pReader); return 1; } /* 逐行消费:每行是借用视图,处理完再取下一行。 */ while ( (Next = xrtLineReaderNext(pLines, &Line)) == XLINE_NEXT_LINE ) { printf("%zu: %.*s\n", ++iCount, (int)Line.Text.Size, Line.Text.Data); } if ( Next == XLINE_NEXT_END ) { iResult = 0; } /* 接管模式:一个 Destroy 连底层 Reader 一起关闭。 */ if ( !xrtLineReaderDestroy(pLines) ) { iResult = 1; } return iResult; }
Что только что произошло. (1) Два шага xrtReaderFromMemory → xrtLineReaderTake(&pReader, 1024) — Take есть передача владения: после этого Destroy-ится только чтение строк (оно закрывает нижележащий Reader единообразно). В проекте Reader меняется на файловую форму (xrtReaderFromFile — то же семейство, глава 41), остальной конвейер без изменений — ценность абстракции Reader впервые реализуется в проекте: одна логика обработки строк, один код для образца в памяти и файла на 2 ГБ. (2) Трёхстатный цикл Next — точно главный цикл logstat: LINE входит в обработку, END завершает и выводит, ERROR сообщает и выходит. (3) Все три конца строк (CRLF/LF/CR) и последняя строка без перевода корректны — машинное доказательство критерия (1).
Вторая полная программа: другой вход — JSON-журнал
Вторая программа из examples/logging/file_json — образец, когда вход повышается до JSON Lines:
*/ #include <xrt.h> #include <stdio.h> int main(void) { xlogfileoptions Options; xlogger* pLogger; xlogfield Field; if ( !xrtLogFileOptionsInit(&Options, "example_logger_json.log") ) { return 1; } Options.MaxBytes = 1024u * 1024u; /* 1MB 轮转 */ Options.BackupCount = 3u; pLogger = xrtLogCreate(XRT_STR_LITERAL("example"), XLOG_INFO); Field = xrtLogFieldUInt(XRT_STR_LITERAL("request_id"), 42u); if ( (pLogger == NULL) || !xrtLogAddJsonFile(pLogger, &Options, NULL) || /* 一行挂载 */ (xrtLogFields( pLogger, XLOG_INFO, XRT_STR_LITERAL("JSON file sink ready"), &Field, 1u ) != XLOG_RESULT_WRITTEN) ) { xrtLogFree(pLogger); return 2; } xrtLogFree(pLogger); printf("wrote example_logger_json.log\n"); return 0; }
Что только что произошло. (1) Это сторона генерации: Sink журнала в формате JSON Lines (один полный JSON-объект на строку) — форма расширения logstat: когда вход повышается с «текста LEVEL» до JSON-строк (уровень в поле), так выглядит генератор. (2) Сторона разбора соответствует потоковому чтению JSON главы 32 — направление расширяющего упражнения этого проекта. (3) AddHelper — одна строка равна трём шагам (форматер + файловый Sink + Attach) — боевая позиция удобного слоя главы 39: в инструментальных цепочках «клей» сперва через Helper, вниз только для комбинирования.
Сведение к карте знаний книги
Маленький logstat, но каждый кирпич, на который он наступает, имеет основную статью в книге — проект валидирует маршруты по карте знаний: глава 4 (модель ошибок — структурный отчёт двух ветвей отказа и чтение цепочки причин), глава 12 (идея хеширования — семантическая граница подсчёта и дедупликации), глава 14 (массивы — O(1)-подсчёт фиксированного набора уровней), глава 18 (Map — динамик-ключевая альтернатива и суждение «когда не использовать»), глава 25 (строки — разрезание представлений без копий и склейка построителем O(n)), глава 32 (JSON — повышение входа в направлении расширения), главы 38/39 (журналы — сторона генерации JSON Lines и семантика ротации), глава 41 (IO-потоки — абстракция Reader и три состояния чтения строк), глава 42 (время — окно времени в направлении расширения), глава 45 (файлы — запись отчёта и файловая форма Reader), глава 47 (каталоги — временные каталоги и системные пути), глава 107 (SSE — живой пуш в направлении расширения). Четырнадцать глав каждая на своём месте внутри стописячной программы — не совпадение, а результат слоистой конструкции: каждая глава учит один слой, проект наслаивает их. После этой главы у вас должно быть ощущение «вся книга собирается одной маленькой программой» — именно та опора для следующих, всё больших проектов (центр конфигурации, чат-сервис, загрузчик): большая программа — это те же слои плюс больше межслойного клея.
Контракты потоков данных: форма входа и выхода каждого слоя
Инженерная ценность слоистой архитектуры реализуется на входных/выходных контрактах каждого слоя — только тогда она поддерживаема. Четыре контракта logstat: байтовый слой (Reader) — путь на входе, поток байтов на выходе, отказ через возвращаемое значение + слот ошибки, без частичных состояний; строчный слой (LineReader) — поток байтов на входе, xlineview на выходе (заимствован, без концов строк, три конца унифицированы), трёхстатный Next, сверхдлинные строки растут автоматически и только с самой длинной; слой разбора (lsLevelOf) — представление на входе, номер уровня + представление текста на выходе (ноль копий), неизвестный уровень возвращает -1 (снисходительный подсчёт — решение вызывающего); слой агрегации (lsstats) — номер уровня / строка образца на входе, статистика на выходе (чистая структура данных, без IO, без выделений). Дисциплина контрактов: каждый слой зависит только от выходного контракта следующего слоя, не от реализации — смена реализации Reader (память/файл/сеть) или цели разбора (текст/JSON) оставляет контракты нетронутыми, и слой заменяем. Это «мышление формами» — проекция решения о выборе главы 23 на архитектуру: выбор выбирает контейнеры, контракты фиксируют границы слоёв.
Карта сборки: зависимости модулей и поток данных
Нарисовав конвейер как граф зависимостей модулей, видно отношение «проекта тома 13» к первым двенадцати томам — каждая коробка есть чья-то тема, стрелки — новый клей этого проекта:
Шесть слоёв каждый на своём посту, между слоями только представления и возвращаемые значения — ни один слой не перепрыгивает соседний вниз (строчный слой не трогает пути, текстовый слой не трогает файловые дескрипторы). Эта дисциплина превращает «замену файла сетевым потоком» (абстракция Reader главы 41) или «замену текста на JSON» (глава 32) в однослойную замену — поддерживаемость не лозунг, а слоистый факт.
Бюджет производительности: счёт каждой ступени
Счёт производительности инструментальной программы следует свести на этапе проектирования (миниатюрное применение методологии главы 136): чтение строк — одно представление на строку (ноль копий) + рост буфера сверхдлинных строк (амортизированный итог O(строк × средняя длина)); определение уровня — одно префиксное сравнение на строку (операция над представлением, наносекунды); подсчёт — инкремент индекса массива (дружелюбно к кэшу); копирование образцов — только строки ошибок (малая доля); склейка отчёта — построитель O(итога); запись — одна запись. Вывод бюджета: однопроцессорная пропускная способность определяется IO (чтение файла — единственная медленная ступень) — все CPU-ступени вместе пренебрежимы перед IO. Вывод управляет оптимизацией: не оптимизировать сравнение строк — смотреть размер буфера чтения (размер блока Reader) — бюджет прежде усердия предотвращает старание не там (дисциплина вопросов главы 136, инженерно).
Прохождение реализации: полный исходник
Ниже полный компилируемый исходник logstat — учебная форма (вход образцом в памяти, для детерминированной проверки); файловая форма отличается одной строкой Reader. Он превращает все решения главы в код:
/* logstat - статистика журнала со смешанными концами строк (учебная форма: образец в памяти) */ #include <stdio.h> #include <string.h> #include <xrt.h> #define LS_LEVELS 5 #define LS_TOP 3 static const char* const kLevelNames[LS_LEVELS] = { "TRACE", "INFO", "WARN", "ERROR", "FATAL" }; typedef struct { size_t Counts[LS_LEVELS]; /* подсчёт фиксированного набора: массив */ char Samples[LS_TOP][96]; /* кольцо образцов ошибок: копия внутрь */ size_t SampleCount; size_t Lines; /* всего строк */ size_t BadLines; /* строки с нераспознанным уровнем */ } lsstats; /* определение уровня: префиксное совпадение до первого пробела представления - ноль копий. -1 = неизвестно. */ static int lsLevelOf(xstrview Line) { size_t i; for ( i = 0; i < LS_LEVELS; i++ ) { xstrview Name = { kLevelNames[i], strlen(kLevelNames[i]) }; if ( (Line.Size > Name.Size) && (Line.Data[Name.Size] == ' ') && xrtStrStarts(Line, Name) ) { return (int)i; } } return -1; } int main(void) { /* смешанные концы строк + последняя строка без перевода + строка неизвестного уровня: три класса границ за раз. */ static const char sLog[] = "INFO server started " "WARN queue is busy " "ERROR db connect failed " "INFO retry ok " "ERROR db connect failed " "NOTICE rotated " "FATAL giving up"; xreader* pReader = NULL; xlinereader* pLines = NULL; xlineview Line; xlinenext Next; lsstats St; xstrbuf Rep; size_t i; int iResult = 1; memset(&St, 0, sizeof(St)); pReader = xrtReaderFromMemory( (xbytesview){ (cbytes)sLog, sizeof(sLog) - 1u }); if ( pReader != NULL ) { pLines = xrtLineReaderTake(&pReader, 1024u); } if ( pLines == NULL ) { fprintf(stderr, "logstat: %s ", xrtErrorMessage(xrtGetError())); goto Cleanup; } /* главный цикл: строго три состояния - LINE обрабатывает / END завершает / ERROR сообщает. */ while ( (Next = xrtLineReaderNext(pLines, &Line)) == XLINE_NEXT_LINE ) { int iLevel = lsLevelOf(Line); St.Lines++; if ( iLevel < 0 ) { St.BadLines++; /* граница: неизвестный уровень считается без прерывания */ continue; } St.Counts[iLevel]++; if ( (iLevel >= 3) && (St.SampleCount < LS_TOP) ) { /* кольцо хранит копии: представления живут одним кадром (позитивная форма ловушки 2). */ size_t n = Line.Size < 95u ? Line.Size : 95u; memcpy(St.Samples[St.SampleCount], Line.Data, n); St.Samples[St.SampleCount][n] = 0; St.SampleCount++; } } if ( Next != XLINE_NEXT_END ) { fprintf(stderr, "logstat: read error: %s ", xrtErrorMessage(xrtGetError())); goto Cleanup; } /* отчёт: склейка построителем O(n) (ловушка: повторный Concat даёт O(n^2)). */ xrtStrBufInit(&Rep); if ( !xrtStrBufAppend(&Rep, XRT_STR_LITERAL("lines=")) ) { goto BufFail; } /* форматирование счётчиков: число в текст, затем дописать (единый выход построителя) */ for ( i = 0; i < LS_LEVELS; i++ ) { char Num[24]; int n = snprintf(Num, sizeof(Num), "%zu", St.Counts[i]); if ( !xrtStrBufAppend(&Rep, (xstrview){ Num, (size_t)n }) ) { goto BufFail; } if ( (i + 1u) < LS_LEVELS && !xrtStrBufAppend(&Rep, XRT_STR_LITERAL("/")) ) { goto BufFail; } } printf("bad=%zu samples=%zu ", St.BadLines, St.SampleCount); for ( i = 0; i < LS_LEVELS; i++ ) { printf("%-6s %zu ", kLevelNames[i], St.Counts[i]); } for ( i = 0; i < St.SampleCount; i++ ) { printf("sample[%zu]=%s ", i, St.Samples[i]); } iResult = 0; BufFail: xrtStrBufFree(&Rep); Cleanup: xrtLineReaderDestroy(pLines); /* единственная точка уничтожения после передачи Take */ return iResult; }
Пункты прохождения. (1) Вход-образец намеренно покрывает все границы: три конца строк, последняя без перевода (строка FATAL), неизвестный уровень (NOTICE — в BadLines без прерывания), повторные ошибки (семантика дедупликации оставлена расширению). (2) Проверка lsLevelOf — «префикс + пробел» — строка INFO не примется за INFOX (деталь дизайна с ещё одним символом). (3) Одно-выходный cleanup: LineReaderDestroy(pLines) — единственная точка уничтожения после передачи Take (pReader больше не трогать); путь отказа и путь успеха делят её. (4) Сегмент построителя показывает минимальную форму склейки отчёта — полный отчёт (с меткой времени и временным файлом) расширяется по главам 44/46. (5) NOTICE неизвестного уровня считается bad=1 — снисходительный подсчёт + показ в отчёте, не падение (реальные журналы вечно содержат невиданные уровни).
Приёмочное прохождение: сверка пяти критериев
Вернитесь к критериям приёмки, записанным вначале, и сверьте построчно — последнее различие главы проекта и учебной главы: учебные главы завершаются «пониманием», главы проекта — «приёмкой». (1) Смешанные концы: вход программы прохождения и есть смесь трёх концов — вывод верен (критерий 1 пройден). (2) Потоковая память: строчный движок держит только текущую строку + фиксированные счётчики + 3×95 байтов образцов — счёт того же порядка просчитан на этапе проектирования (критерий 2 пройден — файловая форма изоморфна для 2 ГБ). (3) Три границы: пустой файл (первый Next — END, вывод всех нулей), нет совпадений (ноль попаданий под флагом фильтра — нормальный вывод), сплошные ошибки (кольцо заполняет и перезаписывает) — образец прохождения уже содержит две границы; пустой файл проверяется в упражнениях (критерий 3 пройден). (4) Граница отчёта: временный каталог и метка без двоеточий — две инженерные детали образца file/report (проектное обоснование критерия 4). (5) Цепочка ошибок: обеветви ошибок печатают xrtErrorMessage(xrtGetError()) — структурный отчёт отказа открытия / ошибки чтения (критерий 5 пройден). Все пять пройдены — инструмент можно поставлять.
Пути расширения: от инструмента к семейству инструментов
Архитектура logstat допускает органическое расширение в трёх направлениях, каждое — лишь «замена одного слоя»: повышение входного слоя — JSON Lines (глава 32: уровень в поле, разветвление по первому символу {) или сетевой поток (семейство Reader главы 41: живая статистика в форме tail -f); повышение слоя статистики — агрегация по временным окнам (глава 42: подсчёт по минутным корзинам для трендов) или частота динамических токенов (Map главы 18: найти самое горячее имя модуля); повышение выходного слоя — JSON-отчёт (сериализация главы 32) или SSE-пуш (глава 107: живой источник панели статистики). Каждое повышение — однослойная замена — слоистая дисциплина карты сборки реализует ценность при эволюции: в этом смысл «проекта» как комплексной приёмки всей книги — не «использовано много API», а при замене любого слоя остальные не трогаются.
138.4契约
- Гарантия потоковости: память = буфер строк (растёт с самой длинной) + O(1) счётчики + O(N) образцы — независимо от общего объёма ввода.
- Адаптация концов: CRLF/LF/CR все распознаются, представления не содержат концов, последняя строка без перевода работает — ноль настройки.
- Строго три состояния: END завершает нормально / ERROR сообщает структурно — испорченный диск никогда не путается с нормальным концом.
- Дисциплина представлений: представления строк живут одним кадром; кольцо образцов хранит копии (представления не входят в долгоживущие контейнеры).
- Дисциплина выбора: фиксированные множества — массивы, динамические ключи — только тогда Map, многосегментная склейка — построитель — форма решает контейнер.
- Граница вывода: отчёт в системный временный каталог, метка без двоеточий — два факта платформы.
- Пози ошибок: возвращаемое значение + слот ошибки на всём пути; одно-выходный cleanup; нигде голого exit.
- Приёмка прежде: пять критериев до кода — границы (пустой/нет совпадений/сплошные ошибки) прорепетированы заранее.
Стратегия тестирования: четырёхквадрантная приёмка инструмента
Написанный код — ещё не поставка — нужно комплектовать приёмочные материалы по формам тестов главы 133. Четыре квадранта logstat: позитив — фиксированные образцы (встроенный образец программы прохождения — вектор регрессии: ожидаемый вывод трёх концов/последней без перевода/неизвестного уровня/повторных ошибок, посимвольно утверждённый); границы — пустой файл (первый Next уже END), одна строка без перевода, сверхдлинная строка (запускает путь роста буфера), все строки одного уровня; негатив — несуществующий путь (утверждения на категорию + домен ошибки открытия), каталог переданный как файл (тоже отказ открытия, но сообщение другое); стресс — место применения метода главы 134: если внутри чтения строк есть выделения, при внедрении отказа статистика остаётся согласована (OOM-поведение инструмента — «сообщить и выйти», никогда «вывести неверную статистику»). Тестовые материалы делят жизненный цикл с инструментом: векторы-образцы входят в тестовый каталог и гоняются на шлюзе — когда инструмент меняет поведение, вектор краснеет первым. Это переводит «четыре оружия» главы 133 из тестового языка библиотеки в язык приёмки проектного инструмента — одна методология, переиспользованная на другом масштабе.
Форма поставки: публикация однофайлового инструмента
Выбор поставки для инструментальных программ (три вида потребителей главы 132, проектная редакция): logstat — одна из лучших сцен для однозаголовочной формы — один файл .c + single/xrt.h + одна команда компиляции — полная поставка (легче всего распространять в мире скриптов); декларация обрезки (глава 131) одной строкой сверху инструмента — XRT_MODULE_IO+XRT_MODULE_STRING+XRT_MODULE_FILE — профиль размера утверждает отсутствие символов сети/TLS. Альтернативы для сравнения: статическая библиотека подходит семейству инструментов (несколько инструментов делят один артефакт — форма статического файлового сервера главы 143); динамическая библиотека ничего не даёт автономному инструменту. Версия и итерация: инструмент печатает собственную версию (запрос версии core главы 8) — logstat --version выводит версию XRT и версию инструмента, так что «какая версия считала этот отчёт» отслеживаемо при разборе. Поставка — не конец, а стартовая точка следующей итерации — все три направления секции расширения отправляются от этой поставляемой базы.
Типичные ошибки
Симптом: журнал 2 ГБ съедает половину памяти 32-гигабайтной машины — «работает», но эксплуатация против.
Причина: отношение к «файлу» как к «строке в памяти». Суть потоковости — держать в любой момент только текущую строку — абстракция Reader существует ровно для этого.
str all = read_whole_file(path); /* 2ГБ в память */ for ( line in strtok(all, "\n") ) { /* концы строк всё ещё требуют ручной трёхстатной обработки */ count(line); }
xreader* r = xrtReaderFromFile(path, ...); xlinereader* lr = xrtLineReaderTake(&r, 1024); while ( xrtLineReaderNext(lr, &v) == XLINE_NEXT_LINE ) { count(v); /* в любой момент: одна строка */ } xrtLineReaderDestroy(lr);
Симптом: выводимые «образцы ошибок» — все последняя строка: кольцо хранило представления, и после переиспользования буфера все указывают на один блок.
Причина: представления строк заимствуют внутренний буфер чтения — следующий Next инвалидирует их (глава 41). Чтобы сохранить между строками — копируйте.
while ( Next == LINE ) { if ( is_error(v) ) { ring_push(&ring, v); } /* хранить представления: всё висячее */ }
while ( Next == LINE ) { if ( is_error(v) ) { ring_push_copy(&ring, v); } /* копия в кольцо */ }
Симптом: запуск под монтированием только-чтение / сервисной учёткой — запись отчёта проваливается; или в Windows создание файла с меткой времени проваливается (: запрещён).
Причина: два факта платформы не вошли в проект. Инженерные детали образца file/report: временный каталог (не предполагать записываемость) + компактный формат времени без двоеточий.
snprintf(name, "report_%s.txt", now_with_colons()); /* 12:30:45 -> Windows отказывает */ f = fopen(name, "w"); /* рабочий каталог может быть доступен только для чтения */
/* системный временный каталог + компактное время без двоеточий (report_20260905_042105.txt) */ path = temp_dir_join(compact_timestamp());
Симптом: перенос старого strtok(all, " ") — обработка концов «выглядит» верно, но машина состояний strtok глобальна (нереентерабельна), последовательные разделители поглощаются (пустые строки исчезают), исходный буфер модифицируется (конфликт с дисциплиной представлений).
Причина: strtok — до-ANSI наследие — тройное нарушение (глобальное состояние / поглощение пустых строк / модификация ввода) — в современном конкурентном и потоковом контексте всё неверно. Чтение строк XRT — его позитивная замена: реентерабельно (состояние в объекте), пустые строки сохранены (семантика представлений), нулевая модификация (заимствование).
for ( p = strtok(buf, " "); p; p = strtok(NULL, " ") ) { count(p); /* пустые строки исчезли; buf изменён; многопоточность взрывается */ }
while ( xrtLineReaderNext(lr, &v) == XLINE_NEXT_LINE ) { count(v); /* реентерабельно; пустые строки сохранены; нулевая модификация */ }
Практические задания
Расширьте образец io/line как движок: посчитать INFO/WARN/ERROR и напечатать. Проверьте образцом со смешанными концами и последней строкой без перевода. Критерии приёмки: три счётчика верны; пустой файл выводит все нули с пометкой.
Реализуйте все требования: фильтр --level, --top N образцов ошибок (копии в кольцо), --report во временный файл (метка без двоеточий). Проверьте на самодельном журнале 100 МБ. Критерии приёмки: все пять критериев пройдены; память уровня МБ (сверка stats).
Вход повышается до JSON-строк формата образца file_json: чтение строк без изменений, разбор меняется на JSON главы 32 (поля уровень/сообщение/время), статистика изоморфна. Один инструмент поддерживает оба входа (детект по первому символу {). Критерии приёмки: смешанный файл двух форматов ветвится корректно; строки с ошибкой JSON-разбора считаются и сообщаются (без прерывания всего прогона).
Как первый проект книги, он заслуживает явной инвентаризации переносимых уроков — они будут повторяться в последующих. Приёмка прежде: пять критериев до кода, границы (пустой/нет совпадений/сплошные ошибки) продуманы на репетиции — порядок с минимальным переделыванием; форма решает контейнер: фиксированное множество — массив / динамические ключи — Map / многосегментная склейка — построитель: выбор не заучивание API, а чтение формы данных; представления живут одним кадром: сохранение между строками требует копирования — дисциплина сетевых глав (95/103) действует и в файловом инструменте; слоистая заменяемость: абстракция Reader даёт один код для образца в памяти и файла 2 ГБ — приёмка и производительность делят одну реализацию; факты платформы входят в проект: временный каталог и метка без двоеточий — «перечислены на этапе проектирования», не «обнаружены в рантайме»; бюджет прежде: все CPU-ступени пренебрежимы перед IO — направление оптимизации называет бюджет, не интуиция; тестирование в четырёх квадрантах: язык приёмки инструмента изоморфен тестовому языку библиотеки. Ни один из семи уроков не специфичен для logstat — это инженерный здравый смысл в выражении XRT; следующий проект (центр конфигурации, главы 139–140) отыграет их в сервисной и персистентной форме; затем (чат-сервис, главы 141–142) добавятся конкурентность и управление соединениями. В этом замысел последовательности проектов: один и тот же инженерный здравый смысл углубляется с каждой сменой формы.
Итоги
| Тема | Кратко |
|---|---|
| Форма проекта | требования и приёмка прежде → решения о выборе → нарастающая реализация → приёмка и расширение |
| Строчный движок | Reader→LineReaderTake (передача) → трёхстатный цикл Next — ядро потоковости |
| Концы строк | CRLF/LF/CR адаптивно; последняя строка без перевода работает — ноль настройки |
| Разбор уровня | срез представления по первому пробелу — префиксное определение без копий |
| Выбор подсчёта | фиксированный набор уровней = массив; динамические ключи — только тогда Map |
| Кольцо образцов | копии внутрь (представления живут одним кадром) — кольцо перезаписывает, храня первые N |
| Отчёт | склейка построителем O(n); временный каталог + метка без двоеточий |
| Потоковая память | буфер строк + O(1) + O(N) — независимо от общего объёма |
| Пози ошибок | три состояния различают END/ERROR; одно-выходный cleanup; снисходительный подсчёт неизвестных уровней |
| Направления расширения | вход JSON Lines (глава 32) / статистика по окнам (глава 42) / панель SSE (глава 107) |
📖 Справочники API в этой главе:io (62 записей)string (92 записей)file (132 записей)
Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки