C# - OutOfMemoryException сохранение списка в файл JSON

Я пытаюсь сохранить потоковые данные карты давления. В основном у меня есть матрица давления, определяемая как:

double[,] pressureMatrix = new double[e.Data.GetLength(0), e.Data.GetLength(1)];

В основном, я получаю одно из этого pressureMatrix каждые 10 миллисекунд, и я хочу сохранить всю информацию в файле JSON, чтобы иметь возможность воспроизвести ее позже.

В первую очередь я пишу заголовок со всеми настройками, использованными для записи, следующим образом:

recordedData.softwareVersion = Assembly.GetExecutingAssembly().GetName().Version.Major.ToString() + "." + Assembly.GetExecutingAssembly().GetName().Version.Minor.ToString();
recordedData.calibrationConfiguration = calibrationConfiguration;
recordedData.representationConfiguration = representationSettings;
recordedData.pressureData = new List<PressureMap>();

var json = JsonConvert.SerializeObject(csvRecordedData, Formatting.None);

File.WriteAllText(this.filePath, json);

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

var newPressureMatrix = new PressureMap(datos, DateTime.Now);
recordedData.pressureData.Add(newPressureMatrix);
var json = JsonConvert.SerializeObject(recordedData, Formatting.None);
File.WriteAllText(this.filePath, json);

Примерно через 20-30 минут я получаю исключение OutOfMemory, потому что система не может содержать записанную переменную data, потому что List<PressureMatrix> в нем слишком большой.

Как я могу справиться с этим, чтобы сохранить данные? Я хотел бы сохранить информацию 24-48 часов.

1 ответ

Решение

Ваша основная проблема заключается в том, что вы храните все свои образцы карты давления в памяти, а не записываете каждый из них по отдельности, а затем позволяете собирать их. Что еще хуже, вы делаете это в двух разных местах:

  1. Вы сериализуете весь свой список образцов в строку JSON json перед записью строки в файл.

    Вместо этого, как объясняется в разделе Советы по повышению производительности: оптимизировать использование памяти, в таких ситуациях следует сериализовать и десериализовать непосредственно в файл и из него. Инструкции о том, как это сделать, см. В этом ответе Можно ли Json.NET сериализовать / десериализовать в / из потока? а также сериализовать JSON в файл.

  2. recordedData.pressureData = new List<PressureMap>(); накапливает все образцы карты давления, а затем записывает их все каждый раз, когда делается образец.

    Лучшим решением было бы написать каждый образец один раз и забыть его, но требование, чтобы каждый образец был вложен в некоторые контейнерные объекты в JSON, делает неочевидным, как это сделать.

Итак, как атаковать вопрос № 2?

Во-первых, давайте изменим вашу модель данных следующим образом, разделив данные заголовка на отдельный класс:

public class PressureMap
{
    public double[,] PressureMatrix { get; set; }
}

public class CalibrationConfiguration 
{
    // Data model not included in question
}

public class RepresentationConfiguration 
{
    // Data model not included in question
}

public class RecordedDataHeader
{
    public string SoftwareVersion { get; set; }
    public CalibrationConfiguration CalibrationConfiguration { get; set; }
    public RepresentationConfiguration RepresentationConfiguration { get; set; }
}

public class RecordedData
{
    // Ensure the header is serialized first.
    [JsonProperty(Order = 1)]
    public RecordedDataHeader RecordedDataHeader { get; set; }
    // Ensure the pressure data is serialized last.
    [JsonProperty(Order = 2)]
    public IEnumerable<PressureMap> PressureData { get; set; }
}

Вариант № 1 является версией шаблона производитель-потребитель. Это включает в себя раскручивание двух потоков: один для генерации PressureData образцы и один для сериализации RecordedData, Первый поток будет генерировать образцы и добавлять их в BlockingCollection<PressureMap> Коллекция, которая передается во второй поток. Затем второй поток будет сериализован BlockingCollection<PressureMap>.GetConsumingEnumerable() как значение RecordedData.PressureData,

Следующий код дает скелет для того, как это сделать:

var sampleCount = 400;    // Or whatever stopping criterion you prefer
var sampleInterval = 10;  // in ms

using (var pressureData = new BlockingCollection<PressureMap>())
{
    // Adapted from
    // https://docs.microsoft.com/en-us/dotnet/standard/collections/thread-safe/blockingcollection-overview
    // https://docs.microsoft.com/en-us/dotnet/api/system.collections.concurrent.blockingcollection-1?view=netframework-4.7.2

    // Spin up a Task to sample the pressure maps
    using (Task t1 = Task.Factory.StartNew(() =>
    {
        for (int i = 0; i < sampleCount; i++)
        {
            var data = GetPressureMap(i);
            Console.WriteLine("Generated sample {0}", i);
            pressureData.Add(data);
            System.Threading.Thread.Sleep(sampleInterval);
        }
        pressureData.CompleteAdding();
    }))
    {
        // Spin up a Task to consume the BlockingCollection
        using (Task t2 = Task.Factory.StartNew(() =>
        {
            var recordedDataHeader = new RecordedDataHeader
            {
                SoftwareVersion = softwareVersion,
                CalibrationConfiguration = calibrationConfiguration,
                RepresentationConfiguration = representationConfiguration,
            };

            var settings = new JsonSerializerSettings
            {
                ContractResolver = new CamelCasePropertyNamesContractResolver(),
            };

            using (var stream = new FileStream(this.filePath, FileMode.Create))
            using (var textWriter = new StreamWriter(stream))
            using (var jsonWriter = new JsonTextWriter(textWriter))
            {
                int j = 0;

                var query = pressureData
                    .GetConsumingEnumerable()
                    .Select(p => 
                            { 
                                // Flush the writer periodically in case the process terminates abnormally
                                jsonWriter.Flush();
                                Console.WriteLine("Serializing item {0}", j++);
                                return p;
                            });

                var recordedData = new RecordedData
                {
                    RecordedDataHeader = recordedDataHeader,
                    // Since PressureData is declared as IEnumerable<PressureMap>, evaluation will be lazy.
                    PressureData = query,
                };                          

                Console.WriteLine("Beginning serialization of {0} to {1}:", recordedData, this.filePath);
                JsonSerializer.CreateDefault(settings).Serialize(textWriter, recordedData);
                Console.WriteLine("Finished serialization of {0} to {1}.", recordedData, this.filePath);
            }
        }))
        {
            Task.WaitAll(t1, t2);
        }
    }
}

Заметки:

  • Это решение использует тот факт, что при сериализации IEnumerable<T> Json.NET не материализует перечислимое в виде списка. Вместо этого он в полной мере воспользуется ленивой оценкой и будет просто перечислять ее, писать и забывать каждый встреченный элемент.

  • Первые образцы ниток PressureData и добавляет их в коллекцию блокирующих.

  • Второй поток оборачивает блокирующую коллекцию в IEnumerable<PressureData> затем сериализует это как RecordedData.PressureData,

    Во время сериализации сериализатор будет перечислять через IEnumerable<PressureData> перечислимый, передавая каждый файл в файл JSON, затем переходя к следующему - эффективно блокируя, пока один не станет доступным.

  • Вам нужно будет поэкспериментировать, чтобы убедиться, что поток сериализации может "идти в ногу" с потоком выборки, возможно, путем установки BoundedCapacity во время строительства. Если нет, вам может потребоваться принять другую стратегию.

  • PressureMap GetPressureMap(int count) должен быть ваш метод (не показан в вопросе), который возвращает текущий образец карты давления.

  • В этом методе файл JSON остается открытым на время сеанса выборки. Если выборка заканчивается неправильно, файл может быть обрезан. Я делаю некоторую попытку улучшить проблему, периодически промывая писателя.

  • В то время как сериализация данных больше не потребует неограниченного количества памяти, десериализация RecordedData позже будет десериализовать PressureData массив в бетон List<PressureMap>, Это может вызвать проблемы с памятью во время последующей обработки.

Демо-скрипка №1 здесь.

Вариант № 2 будет состоять в том, чтобы переключиться из файла JSON в файл JSON с разделителем-новой строкой. Такой файл состоит из последовательностей объектов JSON, разделенных символами новой строки. В вашем случае вы бы сделали первый объект, содержащий RecordedDataHeader информация, и последующие объекты будут иметь тип PressureMap:

var sampleCount = 100; // Or whatever
var sampleInterval = 10;

var recordedDataHeader = new RecordedDataHeader
{
    SoftwareVersion = softwareVersion,
    CalibrationConfiguration = calibrationConfiguration,
    RepresentationConfiguration = representationConfiguration,
};

var settings = new JsonSerializerSettings
{
    ContractResolver = new CamelCasePropertyNamesContractResolver(),
};

// Write the header
Console.WriteLine("Beginning serialization of sample data to {0}.", this.filePath);

using (var stream = new FileStream(this.filePath, FileMode.Create))
{
    JsonExtensions.ToNewlineDelimitedJson(stream, new[] { recordedDataHeader });
}

// Write each sample incrementally

for (int i = 0; i < sampleCount; i++)
{
    Thread.Sleep(sampleInterval);
    Console.WriteLine("Performing sample {0} of {1}", i, sampleCount);
    var map = GetPressureMap(i);

    using (var stream = new FileStream(this.filePath, FileMode.Append))
    {
        JsonExtensions.ToNewlineDelimitedJson(stream, new[] { map });
    }
}

Console.WriteLine("Finished serialization of sample data to {0}.", this.filePath);

Используя методы расширения:

public static partial class JsonExtensions
{
    // Adapted from the answer to
    // https://stackru.com/questions/44787652/serialize-as-ndjson-using-json-net
    // by dbc https://stackoverflow.com/users/3744182/dbc
    public static void ToNewlineDelimitedJson<T>(Stream stream, IEnumerable<T> items)
    {
        // Let caller dispose the underlying stream 
        using (var textWriter = new StreamWriter(stream, new UTF8Encoding(false, true), 1024, true))
        {
            ToNewlineDelimitedJson(textWriter, items);
        }
    }

    public static void ToNewlineDelimitedJson<T>(TextWriter textWriter, IEnumerable<T> items)
    {
        var serializer = JsonSerializer.CreateDefault();

        foreach (var item in items)
        {
            // Formatting.None is the default; I set it here for clarity.
            using (var writer = new JsonTextWriter(textWriter) { Formatting = Formatting.None, CloseOutput = false })
            {
                serializer.Serialize(writer, item);
            }
            // http://specs.okfnlabs.org/ndjson/
            // Each JSON text MUST conform to the [RFC7159] standard and MUST be written to the stream followed by the newline character \n (0x0A). 
            // The newline charater MAY be preceeded by a carriage return \r (0x0D). The JSON texts MUST NOT contain newlines or carriage returns.
            textWriter.Write("\n");
        }
    }

    // Adapted from the answer to 
    // https://stackru.com/questions/29729063/line-delimited-json-serializing-and-de-serializing
    // by Yuval Itzchakov https://stackoverflow.com/users/1870803/yuval-itzchakov
    public static IEnumerable<TBase> FromNewlineDelimitedJson<TBase, THeader, TRow>(TextReader reader)
        where THeader : TBase
        where TRow : TBase
    {
        bool first = true;

        using (var jsonReader = new JsonTextReader(reader) { CloseInput = false, SupportMultipleContent = true })
        {
            var serializer = JsonSerializer.CreateDefault();

            while (jsonReader.Read())
            {
                if (jsonReader.TokenType == JsonToken.Comment)
                    continue;
                if (first)
                {
                    yield return serializer.Deserialize<THeader>(jsonReader);
                    first = false;
                }
                else
                {
                    yield return serializer.Deserialize<TRow>(jsonReader);
                }
            }
        }
    }
}

Позже вы можете обработать файл JSON с разделителями новой строки следующим образом:

using (var stream = File.OpenRead(filePath))
using (var textReader = new StreamReader(stream))
{
    foreach (var obj in JsonExtensions.FromNewlineDelimitedJson<object, RecordedDataHeader, PressureMap>(textReader))
    {
        if (obj is RecordedDataHeader)
        {
            var header = (RecordedDataHeader)obj;
            // Process the header
            Console.WriteLine(JsonConvert.SerializeObject(header));
        }
        else
        {
            var row = (PressureMap)obj;
            // Process the row.
            Console.WriteLine(JsonConvert.SerializeObject(row));
        }
    }
}

Заметки:

  • Этот подход выглядит проще, потому что примеры добавляются постепенно в конец файла, а не вставляются в какой-либо общий контейнер JSON.

  • При таком подходе и сериализация, и последующая обработка могут выполняться с использованием ограниченной памяти.

  • Файл сэмпла не остается открытым во время выборки, поэтому вероятность его усечения меньше.

  • В последующих приложениях могут отсутствовать встроенные инструменты для обработки JSON с разделителями.

  • Эта стратегия может интегрироваться с вашим текущим многопоточным кодом.

Демо-скрипка № 2 здесь.

Другие вопросы по тегам