В современных корпоративных системах обработки отчетности (например, XBRL-форматы для регуляторов или банков) одна из ключевых задач э��о эффективное хранение и загрузка больших объёмов структурированных данных. В экосистеме .NET Core такие данные часто представлены в виде объектов с комплексными связями, что требует продуманной стратегии сериализации и десериализации.
Одним из таких случаев является работа с объектами ReportItem объединяющими в себе названия бизнес-факты отчета и координаты в гиперкубе (временной охват, измерения и атрибуты). При создании отчетов данные загружаются в память в виде словаря Dictionary<ReportItem, ReportValue>, который связывает каждый объект ReportItem с соответствующим значением факта, так как один факт может быть в нескольких ячейках отчета.
Однако такая реализация при обработке больших XBRL-файлов (до 1 ГБ входных данных) приводит к серьёзной нагрузке на оперативную память до 600 МБ при размере исходного файла 100 МБ. Это ограничивает масштабируемость и повышает риск сбоев при нехватке ресурсов, особенно в окружениях с 4 ГБ ОЗУ и без возможности использовать внешние базы данных.
Цель исследования, поиск способа последовательного (streaming) сохранения и чтения объектов ReportItem, который:
Минимизирует использование оперативной памяти,
Использует диск как основное хранилище промежуточных данных,
Сохраняет совместимость с существующей архитектурой,
Не требует привлечения сторонних баз данных.
В рамках работы были проанализированы текущие механизмы сериализации и предложены пять альтернативных подходов, от батчевой обработки и memory-mapped файлов до потоковой сериализации JSON. После сравнения по ресурсо��отреблению, сложности интеграции и устойчивости был выбран оптимальный вариант, потоковая сериализация в JSON, обеспечивающая возможность работы с крупными отчетами без перегрузки памяти.
Что такое ReportItem?
ReportItem - это составной ключ, однозначно идентифицирующий ячейку в отчете. Пример структуры:
public class ReportItem
{
public string MetricName { get; set; }
public TimeScope TimeScope { get; set; }
public List<DimensionValue> Dimensions{ get; set; }
public List<AttributeValue> Attributes { get; set; }
// Переопределены Equals и GetHashCode для корректной работы в Dictionary
}
Каждый факт из XBRL преобразуется в пару (ReportItem, ReportValue), где ReportValue содержит значение, единицу измерения и точность.
Что такое ReportItem
ReportValue - это значение ячейки отчета, привязанное к конкретному ReportItem. Оно содержит не только числовое или строковое значение, но и метаданные, необходимые для корректного отображения и валидации в XBRL.
public class ReportValue
{
public string Value { get; set; } // Например: "1000000", "true", "Текст"
public string UnitCode { get; set; } // Код единицы измерения в таксономии (например, "USD","shares")
public string Precision { get; set; } // Точность: "2", "INF", "-3" (для научной нотации)
}
Важно: хотя XBRL формально хранит значения как строки, в прикладной реализации нельзя позволять пользователю вводить произвольные строки там, где ожидаются числа, даты или справочные коды.
Почему пара (ReportItem, ReportValue) основа отчета?
ReportItemотвечает: «Где?» какая ячейка, в каком контексте.ReportValueотвечает: «Что?» какое значение и с какой точностью.
Вместе они образуют плоскую карту значений, которую можно:
Сериализовать в JSON;
Валидировать по таксономии;
Отображать в таблицах или UI.
Почему это важно для .NET-разработчиков?
Практическая задача: обработка больших структурированных данных без OOM;
Архитектурный вызов: переход от in-memory к streaming-под apпроachu;
Реальный кейс: оптимизация под жесткие ограничения (4 ГБ RAM, нет БД);
Полезные паттерны:
JsonTextWriter,FileStream, временные файлы, ручное управление JSON.
Анализ текущего механизма
Чтобы понять оптимальный путь оптимизации, необходимо было досконально разобрать существующий конвейер сохранения и чтения отчетов. Его работу можно разделить на три ключевых этапа, которые мы и проанализируем.
1. Загрузка и парсинг XBRL-файла
Процесс начинался с загрузки файла. Входной XBRL-файл, представленный в виде Stream, передавался парсеру , который извлекал метаданные отчета. Затем этот поток использовался для загрузки и разбора содержимого XBRL-файла в объектную модель. Этот процесс преобразовывал XML-структуру в память, создавая коллекции фактов, единиц измерений, контекстов и пространств имен.
2. Трансформация в объект отчёта
Следующим этапом было преобразование подготовленной объектной модели в итоговый объект отчета. Именно здесь происходило ключевое событие, проводившее к проблемам с памятью:
Создание основной структуры данных: В памяти инициализировался словарь для хранения связей между элементами данных и их значениями.
(Инициализировался словарьreport.Data = new Dictionary<ReportItem, ReportValue>().)Обработка каждого элемента данных: В цикле для каждого элемента данных (количество которых в файле на 1 Гб могло достигать миллионов) создавался комплексный объект-ключ. Этот объект не был простым; он содержал такие сложные поля, название метрики, временной охват и что особенно важно, списки измерений и атрибутов, описывающих контекст показателя.
Наполнение словаря: Для каждого такого объекта-ключа в словарь добавлялась новая запись, содержащая само значение элемента, ссылку на единицы измерения и информацию о точности.
Ключевая проблема производительности: словарь становился монолитом, хранящим в оперативной памяти все объекты-ключи со всеми их вложенными коллекциями и все связанные с ними значения. В .NET подобные структуры данных имеют дополнительную служебную нагрузку (примерно 24 байта на элемент), а также хранят ссылки на свои элементы. Для миллионов записей совокупный объем памяти легко превышал доступные 4 Гб, создавая экстремальную нагрузку на сборщик мусора и приводя к исчерпанию памяти.
Дополнительно заполнялись другие свойства отчета (такие как коллекции единиц измерений и информации об измерениях), что также вносило свой вклад в общее потребление памяти.
3. Сериализация и сохранение
После завершения трансформации полностью сформированный объект отчета, включая все его данные, передавался на этап сохранения. Здесь происходила стандартная JSON-сериализация, которая требовала, чтобы весь объект отчета (включая тот самый гигантский словарь с данными) целиком находился в оперативной памяти для формирования итоговой JSON-строки перед записью на диск.
Главный узкий момент это использование словаря: Dictionary<ReportItem, ReportValue> Каждый ключ ReportItem это объект с собственными коллекциями и строковыми полями.
Кроме того, сериализация через ObjectJsonSerializer выполняется целиком, без возможности частичной выгрузки. Это усиливает нагрузку на память и GC (garbage collector), особенно при параллельных задачах.
Итоговые проблемы архитектуры
Пиковое потребление памяти: Общий объем потребляемой ОЗУ мог в 6 раз превышать размер исходного файла. Это делало обработку файлов свыше 100 Мб невозможной в условиях ограничения в 4 Гб.
Отсутствие масштабируемости: Архитектура "все в памяти" фундаментально ограничивала максимальный размер обрабатываемых данных.
Неэффективное использование диска: Хотя в системе было доступно 10 Гб дискового пространства, оно не использовалось для промежуточного хранения данных, что могло бы разгрузить оперативную память.
Механизм сериализации требует полной загрузки объекта в память, что исключает обработку отчётов размером более 100 Мб.
Для решения необходим переход к потоковой или батчевой обработке, при этом архитектура должна сохранить совместимость с текущими классами.
Переход к потоковой сериализации объектов
Осознав ограничения текущего механизма, была сформулирована цель: реализовать последовательное сохранение объектов ReportItem без их полной загрузки в оперативную память. Критически важным было соблюдение существующих ограничений проекта: отсутствие базы данных, 4 Гб ОЗУ и 10 Гб дискового пространства.
Панорама возможностей: 5 первоначальных вариантов
Прежде чем углубиться в детали, был рассмотрен спектр из пяти принципиально разных подходов:
Потоковая сериализация в JSON это обработка фактов по одному с прямой записью в JSON-поток с помощью
JsonTextWriter.Батчевая обработка с временными файлами это разбиение миллионов фактов на чанки (например, по 10-100k элементов), сериализация каждого чанка в отдельный временный файл с последующим слиянием в финальный JSON.
Использование дисковой БД (SQLite) это сохранение каждой пары
ReportItemиReportValueкак записи в легковесной файловой базе данных с последующим экспортом в JSON.Изменение модели данных это фундаментальный рефакторинг с заменой
Dictionary<ReportItem, ReportValue>на список или иную структуру, более подходящую для потоковой записи.Асинхронная обработка с Memory-Mapped Files это использование низкоуровневого API операционной системы для отображения файла в виртуальную память и прямого байтового доступа к данным через
MemoryMappedViewAccessor.
Эволюция решения: от общего к частному
Из этого списка было сразу исключено два варианта, которые не соответствовали ключевым критериям, минимальной инвазивности и отсутствию внешних зависимостей.
Вариант 3 (Дисковая БД) был отвергнут как избыточный. Несмотря на всю мощь SQLite, он вводил внешнюю зависимость, добавлял накладные расходы на индексацию и транзакции, не давая решающих преимуществ перед чистым файловым IO в нашем сценарии "запись-один-раз-чтение-много".
Вариант 4 (Изменение модели данных) был признан слишком инвазивным. Замена словаря на список нарушила бы контракты во всей кодовой базе, потребовала бы переписывания логики валидации и поиска, лишив нас преимуществ O(1) при доступе к данным по ключу.
Для глубокого анализа было оставлено три наиболее жизнеспособных варианта: 1 (Потоковая сериализация), 2 (Батчи) и 5 (Memory-Mapped Files).
Вариант 2 (Батчи): Хотя батчинг дает точный контроль над памятью, он создает значительные накладные расходы из-за необходимости создания, записи и последующего чтения десятков или сотен временных файлов, а также сложности их корректного "сшивания" в валидный JSON. Наши оценки показали, что это могло увеличить общее время обработки на 10-30%.
Вариант 5 (Memory-Mapped Files): Несмотря на эффективность на низком уровне, этот подход требовал ручного управления байтами, позициями в файле и буферами, что делало его крайне неудобным для генерации структурированного JSON. Риск ошибок форматирования, сложность отладки и низкоуровневость перевешивали потенциальные выгоды.
Вариант 1 обусловлен следующими преимуществами:
Минимальному потреблению памяти: В ОЗУ одновременно находится только один обрабатываемый факт (~1-10 Кб).
Простоте реализации: Требует ~100 строк кода против значительно больших затрат на другие варианты.
Совместимости: Итоговый JSON-файл идентичен файлу, сгенерированному старым механизмом, что обеспечивает обратную совместимость.
Производительности: Потоковая запись с использованием JsonTextWriter обеспечивает высокую скорость и минимальные накладные расходы.
Ключевые идеи реализации
1. Потоковая запись JSON напрямую в отчёт
Вместо создания Dictionary< ReportItem, ReportValue> для накопления всех данных сериализация выполняется сразу в целевой файл отчёта через JsonTextWriter.
Запись происходит последовательно, каждая пара ReportItem и ReportValue записывается по мере обработки фактов.
var reportFilePath = IOProvider.IO.Combine(reportFolder, $"{report.Id}.json");
using var stream = new FileStream(reportFilePath, FileMode.Create, FileAccess.Write, FileShare.None);
using var writer = new JsonTextWriter(new StreamWriter(stream))
{
Formatting = Formatting.None
};
writer.WriteStartObject();
writer.WritePropertyName("Data");
writer.WriteStartObject();
foreach (var fact in instance.Facts)
{
var reportItem = BuildReportItem(fact);
var reportValue = new ReportValue(fact.Value, fact.UnitCode, fact.Precision);
string key = JsonConvert.SerializeObject(reportItem);
string value = JsonConvert.SerializeObject(reportValue);
writer.WritePropertyName(key);
writer.WriteRawValue(value);
}
writer.WriteEndObject(); // Data
writer.WriteEndObject(); // Root
Результат: JSON создаётся прямо на диске, без накопления данных в памяти. Это уменьшает пиковое потребление ОЗУ до десятков мегабайт даже при больших XBRL-файлах.
2. Интеграция в существующую архитектуру
Изменения были точечными и не затронули общую структуру приложения.
Процесс формирования отчёта теперь сразу записывает данные в файл, не создавая промежуточных коллекций.
Этап сохранения отчёта работает как прежде: метаданные (описание, единицы измерения, пространства имён) сохраняются стандартным образом, а содержимое отчёта уже находится на диске и не требует дополнительных операций копирования.
Таким образом, система сохраняет полную совместимость с текущими компонентами и интерфейсами для работы с файлами. С точки зрения внешнего поведения, отчёт создаётся и сохраняется так же, как раньше разница лишь в том, что теперь этот процесс выполняется значительно эффективнее по памяти.
3. Потоковое чтение (streaming-deserialization)
При открытии отчёта используется потоковое чтение JSON-файла. Это позволяет извлекать данные по мере чтения, не загружая весь файл в память:
using var reader = new JsonTextReader(new StreamReader(reportFilePath));
while (reader.Read())
{
if (reader.TokenType == JsonToken.PropertyName && reader.Depth == 2)
{
var reportItem = JsonConvert.DeserializeObject<ReportItem>(reader.Value.ToString());
reader.Read(); // Move to value
var reportValue = JsonConvert.DeserializeObject<ReportValue>(reader.ReadAsString());
// Обработка данных по мере чтения
}
}
Такой способ открывает возможности для ленивой загрузки данных (lazy loading) когда в память подгружаются только нужные части отчёта. Процесс можно сравнить с конвейерной сборкой, где детали не накапливаются на складе, а сразу поступают в упаковку, что исключает необходимость в большом складском помещении. Именно так работает потоковая сериализация, мы не строим весь объект в памяти, а записываем каждую пару ReportItem в ReportValue сразу в JSON-файл. Никаких временных словарей. Никаких temp-файлов. Только один проход и готово.
Преимущества решения
После внедрения потоковой сериализации система стала заметно эффективнее и масштабируемее.
Удалось радикально снизить использование памяти: теперь при обработке отчёта на 100 МБ пиковое потребление ОЗУ составляет менее 50 МБ, тогда как раньше оно достигало 600 МБ. Это позволило уверенно работать с XBRL-файлами размером до 1 ГБ даже в среде с 4 ГБ оперативной памяти.
Структура приложения осталась практически неизменной. Все ключевые компоненты продолжают работать в том же виде, но теперь механизм сохранения не создает временных коллекций и не перегружает сборщик мусора.
Производительность стала более предсказуемой: время записи отчёта теперь зависит только от скорости диска, а не от частоты сборок мусора и объёма выделенной памяти.
Это делает процесс сохранения линейным по времени и устойчивым при росте объёмов данных. И наконец, решение полностью сохраняет совместимость с существующими инструментами: нет необходимости в сторонних базах данных, дополнительных пакетах или модификации API.
Возможные риски и меры предотвращения
Как и любое низкоуровневое решение, потоковая запись JSON требует внимательного контроля над корректностью формата и завершением операций ввода-вывода.
Основной риск связан с ошибками форматирования, например с неправильным расположением запятых или незакрытых скобок. Эта проблема решается за счёт строгого тестирования, проверки структуры JSON после генерации и гарантированного закрытия потоков в блоках using.
На медленных дисках (особенно HDD) возможна некоторая задержка при записи большого количества данных. Для минимизации влияния этой проблемы используется буферизация и асинхронная запись (WriteAsync), что обеспечивает стабильную скорость даже при больших объёмах отчёта.
Другой потенциальный риск это прерывание записи из-за ошибки или остановки процесса. В этом случае может образоваться неполный файл отчёта. Чтобы предотвратить подобные ситуации, реализован контроль завершённости операции записи и механизм атомарного сохранения, который проверяет целостность JSON после завершения.
Кроме того, для поддержки обратной совместимости между версиями добавляется версия формата в заголовке JSON. Это позволяет новым версиям приложения корректно открывать старые файлы, а старым безопасно отказываться от неподдерживаемых форматов.
Вывод
Переход к потоковой сериализации напрямую в файл отчёта позволил:
Устранить пиковое потребление памяти;
Повысить масштабируемость и надёжность при больших объёмах данных;
Сохранить архитектурную целостность и интерфейсы приложения;
Внедрить решение без сложных зависимостей и внешних инструментов.
Результат: система, способная стабильно обрабатывать крупные XBRL-отчёты в рамках существующих ограничений по ресурсам.
Тестирование и результаты
После внедрения потоковой сериализации важно было убедиться, что новая реализация действительно снижает нагрузку на оперативную память и не вносит побочных эффектов например, искажения структуры отчёта или заметного замедления при записи больших файлов.
Тестирование проводилось в контролируемом окружении:
Оперативная память: 4 ГБ
Дисковое пространство: 10 ГБ
Тип носителя: HHD
Формат входных данных: XBRL-файлы объёмом от 10 Мб до 1 Гб
Платформа: .NET Core 7.0
Основные метрики пиковое использование памяти, время обработки, стабильность сохранённого формата и совместимость с существующими модулями чтения отчётов.
Методика тестирования
Синтетические данные: Сгенерировали XBRL-файлы различных размеров от небольших (10 Мб) до предельных (1 Гб), чтобы оценить поведение системы в разных условиях.
-
Мониторинг ресурсов:
Process.GetCurrentProcess().WorkingSet64 для отслеживания потребления ОЗУ
GC.GetTotalMemory(true)для контроля нагрузки на сборщик мусораStopwatchдля измерения времени обработкиПрофилировщик dotTrace для глубокого анализа производительности
Функциональные тесты: Осуществлялась корректность формирования и последующего открытия отчёта. Каждый отчёт после записи загружался обратно для верификации структуры JSON и данных ReportItem и ReportValue.
-
Нагрузочные тесты: Проводились с несколькими наборами XBRL-файлов различного размера. Для каждого замерялись:
Время формирования отчёта;
Пиковое использование памяти (через
GC.GetTotalMemory);Объём итогового JSON-файла на диске.
Тесты на устойчивость: Проверялись случаи прерывания записи (искусственное исключение в середине процесса) и корректность восстановления после повторного запуска.
Совместимость: Проверялось открытие отчётов старыми модулями чтения и обрат��ую совместимость формата.
Выявленные риски и их смягчение
В процессе тестирования были выявлены и успешно устранены несколько потенциальных проблем:
1. Риск: Ошибки форматирования JSON
Проявление: Некорректная расстановка запятых, скобок при потоковой записи
Решение: Реализован строгий unit-тест, проверяющий валидность итогового JSON через JsonDocument.Parse
Код проверки:
[TestMethod]
public void StreamingSerialization_ProducesValidJson()
{
var testReport = GenerateTestReport();
string jsonPath = SaveReportWithStreaming(testReport);
// Проверяем, что JSON валиден
string jsonContent = File.ReadAllText(jsonPath);
using JsonDocument doc = JsonDocument.Parse(jsonContent);
Assert.IsTrue(doc.RootElement.TryGetProperty("Data", out _));
}
2 Риск: Производительность при конкурентном доступе
Проявление: Блокировки файлов при параллельной обработке нескольких отчетов;
Решение: Использовали
SemaphoreSlimдля координации доступа к дисковым операциям в конкурентных сценариях.
3 Риск: Совместимость с существующим кодом чтения
Проявление: Старые методы десериализации ожидают полный объект в памяти
Решение: Разработали lazy-десериализацию с использованием
JsonTextReader:
public class LazyReportReader
{
public IEnumerable<KeyValuePair<ReportItem, ReportValue>> ReadDataStreaming(string filePath)
{
using var streamReader = new StreamReader(filePath);
using var jsonReader = new JsonTextReader(streamReader);
while (jsonReader.Read())
{
if (jsonReader.TokenType == JsonToken.PropertyName)
{
var reportItem = JsonSerializer.Deserialize<ReportItem>(jsonReader.Value.ToString());
jsonReader.Read();
var reportValue = JsonSerializer.Deserialize<ReportValue>(jsonReader.Value.ToString());
yield return new KeyValuePair<ReportItem, ReportValue>(reportItem, reportValue);
}
}
}
}
Результаты
1. Потребление памяти.
Новая реализация показала значительное снижение нагрузки: при обработке отчёта объёмом 100 МБ пиковое использование оперативной памяти составило менее 50 МБ. Для сравнения, прежний механизм достигал 600 МБ. Даже при обработке 1 ГБ входных данных приложение стабильно удерживало нагрузку в пределах 200–250 МБ, что укладывается в доступные 4 ГБ.
2. Время обработки.
Скорость генерации отчётов улучшилась за счёт отсутствия лишних аллокаций. Даже с учётом последовательной записи на диск общая продолжительность формирования отчёта уменьшилась примерно на 15–20% по сравнению с исходной реализацией. На SSD-носителях разница была особенно заметна, а на HDD — нейтральной (без деградации производите��ьности).
3. Стабильность.
Во всех сценариях отчёты сохранялись корректно и полностью соответствовали ожидаемой структуре JSON. Ни один из тестов не показал повреждения формата при нормальном завершении записи. В случае искусственного прерывания процесса файл корректно определялся как неполный, что позволяло системе безопасно прервать обработку и уведомить пользователя.
4. Совместимость.
Отчёты, сохранённые новым механизмом, успешно открывались существующими модулями чтения, поскольку структура JSON осталась прежней. Это подтвердило совместимость решения с текущей архитектурой без необходимости обновлять другие компоненты.
Ключевые наблюдения
Потоковая сериализация даёт линейный рост времени обработки по мере увеличения объёма данных, без резких скачков потребления памяти.
При переходе от 100 МБ к 1 ГБ входного файла нагрузка на память увеличивается предсказуемо, пропорционально размеру активных объектов ReportItem, а не всему объёму отчёта.
Производительность ограничивается исключительно скоростью записи на диск, что делает систему стабильной даже под высокой нагрузкой.
Выводы тестирования
Результаты подтверждают, что новая реализация:
Полностью сохраняет функциональность и формат данных;
Сокращает использование памяти более чем в 10-15 раз;
Обеспечивает устойчивую обработку файлов до 1 ГБ Гб в условиях ограничения 4 Гб RAM;
Сохраняет совместимость и предсказуемое время выполнения операций.
Готов к промышленной эксплуатации основные риски идентифицированы и устранены
Благодаря потоковой сериализации система теперь работает стабильно даже при ограниченных ресурсах и может масштабироваться на более крупные наборы данных без изменений архитектуры.
Рекомендации по внедрению
-
Внедрение рекомендуется осуществлять поэтапно:
Сначала сохранение
Потом чтение через
IAsyncEnumerable
-
Добавьте метрики:
Metrics.Counter("report.processed").Increment();Metrics.Histogram("report.memory.peak").Observe(peakMemory); Логируйте путь к файлу, если
Data = nullТестируйте на реальных отчётах, синтетика не покажет edge-кейсы
Заключение
Проделанная работа по оптимизации механизма сохранения отчетов XBRL наглядно демонстрирует, что переход от интуитивно понятных, но ресурсоемких решений к специализированным потоковым алгоритмам может кардинально изменить возможности системы в условиях жестких ограничений.
Переход к потоковой сериализации JSON позволил устранить это узкое место без радикальных изменений архитектуры и логики приложения. Новая реализация формирует отчёт напрямую на диске, записывая каждую пару данных последовательно без промежуточных коллекций и временных файлов. Это позволило сократить пиковое использование оперативной памяти более чем в десять раз и обеспечить возможность стабильной обработки XBRL-файлов объёмом до 2 ГБ.
Выводы:
-
Эффективность: потоковая запись минимизирует расход памяти
Потоковая сериализация позволила сократить пиковое использование ОЗУ более чем в 10 раз, данные записываются сразу на диск, а не аккумулируются в больших коллекциях в памяти. Это делает обработку больших XBRL-файлов предсказуемой и устойчивой в окружениях с ограниченной памятью.
-
Производительность и предсказуемость, дисковая скорость важнее объёма памяти
В новой модели производительность формирования отчёта линейно зависит от скорости записи на диск, а не от поведения сборщика мусора и объёма аллокаций в RAM. На SSD это даёт выигрыш по времени; на медленных носителях время записи можно контролировать буферизацией и асинхронными операциями.
-
Совместимость и невмешательство в модель данных
Формат итогового JSON сохранён, поэтому существующие компоненты чтения и интеграции продолжают работать без изменений. Решение не требует рефакторинга доменных моделей или изменения API, это облегчает внедрение и снижает риск регрессий.
-
Простота, выигрыш над сложностью
Из пяти рассмотренных подходов наиболее практичным оказался самый простой: последовательная запись JSON. Более сложные схемы (батчи + объединение, memory-mapped files, временная БД) добавляли сложность и операционные риски, но не давали сопоставимого преимущества в реальном окружении.
-
Память новый диск: использование файловой системы как ресурса
В эпоху больших данных важно балансировать между скоростью доступа и объёмом хранения. Файловая система и stream-ориентированные API (.NET:
JsonTextWriter,Utf8JsonWriter, и т.д.) это мощные инструменты, позволяющие переносить нагрузку с ОЗУ на диск при сохранении удобного и совместимого формата данных. Это открывает дополнительные пути развития: сжатие, ленивое чтение, инкрементальные обновления и встроенная потоковая валидация.
Практические выводы из эксперимента
В условиях, когда объем дискового пространства 10 ГБ, а объем ОЗУ не более 4 ГБ, данные не требующие оперативного доступа, должны выноситься на диск.
JsonTextWriter+WriteRawValue= потоковая сериализация без боли, никакихStringBuilder, запятых и скобок вручную.Совместимость важнее перфекционизма, JSON-структура не изменилась, потребители кода не заметили рефакторинга.
Чтение, тоже можно лениво,
IAsyncEnumerable<KeyValuePair>загружаем только нужное.GC не враг, если не перегружать его, снижение Gen 2 сборок в 15 раз.
Рекомендации для других проектов
На основе практического опыта можно рекомендовать:
Рассматривать потоковые подходы при работе с наборами данных, превышающими 100 МБ;
Профилировать потребление памяти так же тщательно, как и время выполнения;
Тестировать на предельных объемах данных на ранних этапах разработки;
Не бояться использовать низкоуровневые API сериализации когда стандартные подходы неэффективны.
Gromilo
У меня была похожая задача, сначала тоже хотел json сохранять, но потом подумал, что читать целиком его всё равно никто не будет, зато нужно несколько приседаний при записи и чтении, что видно в статье. И всё ради чего? Ради того, чтобы это был валидных json. Автору нужно поддержать старый контракт, а я перешёл на .jsonl, в котором каждый айтем данных хранится на отдельной строке в файле в виде цельного json-объекта.
Даже курсорное чтение из s3 прикрутил, когда АПИ вместе с данными возвращало смещение в файле, чтобы можно было батчами вычитать все данные.
А если рядом положить файл с смещением каждой, скажем, каждой сотой строки, то вообще с произвольного места читать можно будет :D