Pull to refresh

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

Reading time5 min
Views3.3K

Некоторое время назад я писал приложение на 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 — если кто заинтересовался — пишите в комментариях.

Tags:
Hubs:
+5
Comments3

Articles