JsonWriterSax — библиотека для создания JSON

от автора

Некоторое время назад я писал приложение на c++/Qt, которое отправляло по сети большие объемы данных в формате JSON. Использовался стандартный QJsonDocument. При внедрении столкнулся с низкой производительностью, а так же неудобным дизайном классов, который не позволял нормально детектировать ошибки при работе. В результат появилась библиотека JsonWriterSax, позволяющая писать JSON документы в SAX стиле с высокой скоростью, которую и публикую на github.com под лицензией MIT. Кому интересно — прошу под кат.

Немного теории

JSON (JavaScript Object Notation) — структуированный текстовый формат данных, разработанный Дугласом Крокфордом и являющийся подмножеством языка ECMAScript (на его основе созданы JavaScript, JScript и др.). JSON пришел на смену XML, расширяя возможности вложенности и добавляя типы данных. В настоящее время является активно применяется в интернете.

Но в JSON имеются и недостатки. На мой взгляд среди стандартных типов явно не хватает типа DateTime — приходится передавать значение в виде числа или строки, а при разборе принимать решение уже в зависимости от контекста. Но стоит отметить, что и в ECMAScript тип Date создавался давно, не был продуман, и в мире js для работы с датами используют сторонние библиотеки.

Для парсинга и создания структуированных документов имеется 2 основных подхода — SAX и DOM. Они появились еще для XML, но могут использоваться как паттерны и для создания обработчиков других форматов.

SAX (Simple API for XML)

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

DOM (Document Object Model)

При данном способе в памяти строится дерево документа, которое может сериализоваться, десериализоваться и изменяться. Основной недостаток — это высокий расход память и увеличение времени обработки. Под капотом обычно используется SAX обработчик.

Проблемы QJsonDocument

Стандартный QJsonDocument использует DOM подход. При создании документа скорость невысока — можно посмотреть бенчмарки в конце статьи. Но самой большой проблемой для меня оказался непродуманный дизайн возврата ошибки.

auto max = std::numeric_limits<int>::max(); QJsonArray ja; for(auto i = 0; i < max; ++i) {     ja.append(i);     if(ja.size() - 1 != i) {         break;     } }

В данном примере при нехватке памяти запишется в поток ошибок сообщение

QJson: Document too large to store in data structure
и данные перестанут добавляться. В случае с массивом можно проверять условие

ja.size() - 1 != i

Но что делать при работе с объектом? Постоянно проверять, что новый ключ добавился? Парсить лог в поисках ошибки?

Библиотека

Библиотека JsonWriterSax позволяет писать JSON документ в QTextStream в SAX стиле и доступна на github по лицензии MIT. Контроль за памятью возлагается на приложение. Библиотека контролирует целостность JSON — при некорректном добавлении элемента функция записи вернет ошибку. Для контроля используется КС-грамматика. Были написаны тесты, но возможно какой-то кейс остался без внимания. Если кто-то зафиксирует некорректную работу проверки и сообщит для исправления ошибки — буду очень благодарен.

Считаю, что лучшее описание библиотеки для программиста — пример кода =)

Примеры

Создание массива

QByteArray ba; QTextStream stream(&ba); stream.setCodec("utf-8"); JsonWriterSax writer(stream); writer.writeStartArray(); for(auto i = 0; i < 10; ++i) {     writer.write(i); } writer.writeEndArray(); if(writer.end()) {     stream.flush(); } else {     qWarning() << "Error json"; }

В результате получим

[0,1,2,3,4,5,6,7,8,9]

Создание объекта

QByteArray ba; QTextStream stream(&ba); stream.setCodec("utf-8"); JsonWriterSax writer(stream); writer.writeStartObject(); for(auto i = 0; i < 5; ++i) {     writer.write(QString::number(i), i); } for(auto i = 5; i < 10; ++i) {     writer.write(QString::number(i), QString::number(i)); } writer.writeKey("arr"); writer.writeStartArray(); writer.writeEndArray(); writer.writeKey("o"); writer.writeStartObject(); writer.writeEndObject(); writer.writeKey("n"); writer.writeNull(); writer.write(QString::number(11), QVariant(11)); writer.write("dt", QVariant(QDateTime::fromMSecsSinceEpoch(10))); writer.writeEndObject(); if(writer.end()) {     stream.flush(); } else {     qWarning() << "Error json"; }

В результате получим

{"0":0,"1":1,"2":2,"3":3,"4":4,"5":"5","6":"6","7":"7","8":"8","9":"9","arr":[],"o":{},"n":null,"11":11,"dt":"1970-01-01T03:00:00.010"}

Создание документа с вложенностью и разными типами

QByteArray ba; QTextStream stream(&ba); stream.setCodec("utf-8"); JsonWriterSax writer(stream); writer.writeStartArray(); for(auto i = 0; i < 1000; ++i) {     writer.writeStartObject();     writer.writeKey("key");     writer.writeStartObject();     for(auto j = 0; j < 1000; ++j) {         writer.write(QString::number(j), j);     }     writer.writeEndObject();     writer.writeEndObject(); } writer.writeEndArray(); if(writer.end()) {     stream.flush(); } else {     qWarning() << "Error json"; }

Benchmarks

Использовался QBENCHMARK при release-сборке. Функциональность реализована в классе JsonWriterSaxTest.

elementary OS 5.0 Juno, kernel 4.15.0-38-generic, cpu Intel® Core(TM)2 Quad CPU 9550 @ 2.83GHz, 4G RAM, Qt 5.11.2 GCC 5.3.1

Long number array

  • QJsonDocument: 42 msecs per iteration (total: 85, iterations: 2)
  • JsonWriterSax: 23 msecs per iteration (total: 93, iterations: 4)

Big one-level object

  • QJsonDocument: 1,170 msecs per iteration (total: 1,170, iterations: 1)
  • JsonWriterSax: 53 msecs per iteration (total: 53, iterations: 1)

Big complex document

  • QJsonDocument: 1,369 msecs per iteration (total: 1,369, iterations: 1)
  • JsonWriterSax: 463 msecs per iteration (total: 463, iterations: 1)

elementary OS 5.0 Juno, kernel 4.15.0-38-generic, cpu Intel® Core(TM) i7-7500U CPU @ 2.70GHz, 8G RAM, Qt 5.11.2 GCC 5.3.1

Long number array

  • QJsonDocument: 29.5 msecs per iteration (total: 118, iterations: 4)
  • JsonWriterSax: 13 msecs per iteration (total: 52, iterations: 4)

Big one-level object

  • QJsonDocument: 485 msecs per iteration (total: 485, iterations: 1)
  • JsonWriterSax: 31 msecs per iteration (total: 62, iterations: 2)

Big complex document

  • QJsonDocument: 734 msecs per iteration (total: 734, iterations: 1)
  • JsonWriterSax: 271 msecs per iteration (total: 271, iterations: 1)

MS Windows 7 SP1, cpu Intel® Core(TM) i7-4770 CPU @ 3.40GHz, 8G RAM, Qt 5.11.0 GCC 5.3.0

Long number array

  • QJsonDocument: 669 msecs per iteration (total: 669, iterations: 1)
  • JsonWriterSax: 20 msecs per iteration (total: 81, iterations: 4)

Big one-level object

  • QJsonDocument: 1,568 msecs per iteration (total: 1,568, iterations: 1)
  • JsonWriterSax: 44 msecs per iteration (total: 88, iterations: 2)

Big complex document

  • QJsonDocument: 1,167 msecs per iteration (total: 1,167, iterations: 1)
  • JsonWriterSax: 375 msecs per iteration (total: 375, iterations: 1)

MS Windows 7 SP1, cpu Intel® Core(TM) i3-3220 CPU @ 3.30GHz, 8G RAM, Qt 5.11.0 GCC 5.3.0

Long number array

  • QJsonDocument: 772 msecs per iteration (total: 772, iterations: 1)
  • JsonWriterSax: 26 msecs per iteration (total: 52, iterations: 2)

Big one-level object

  • QJsonDocument: 2,029 msecs per iteration (total: 2,029, iterations: 1)
  • JsonWriterSax: 59 msecs per iteration (total: 59, iterations: 1)

Big complex document

  • QJsonDocument: 1,530 msecs per iteration (total: 1,530, iterations: 1)
  • JsonWriterSax: 495 msecs per iteration (total: 495, iterations: 1)

Перспективы

В последующих версиях планирую добавить возможность описывать формат пользовательских данных через lambda-функции с помощью с QVariant, добавить возможность использовать разделители для форматирования документа (pretty document) и возможно, если сообщество заинтересуется, добавлю SAX парсер.

Кстати для нахождения ошибки переполнения мне помогла моя библиотека, позволяющая для qInfo(), qDebug(), qWarning() задавать формат и выводить в стиле модуля Python logging. Данную библиотеку так же планирую выложить в opensource — если кто заинтересовался — пишите в комментариях.


ссылка на оригинал статьи https://habr.com/post/427899/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *