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 ответ
Ваша основная проблема заключается в том, что вы храните все свои образцы карты давления в памяти, а не записываете каждый из них по отдельности, а затем позволяете собирать их. Что еще хуже, вы делаете это в двух разных местах:
Вы сериализуете весь свой список образцов в строку JSON
json
перед записью строки в файл.Вместо этого, как объясняется в разделе Советы по повышению производительности: оптимизировать использование памяти, в таких ситуациях следует сериализовать и десериализовать непосредственно в файл и из него. Инструкции о том, как это сделать, см. В этом ответе Можно ли Json.NET сериализовать / десериализовать в / из потока? а также сериализовать JSON в файл.
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 здесь.