Некоторое время назад я писал приложение на 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 — если кто заинтересовался — пишите в комментариях.
Wilk
Здравствуйте!
Проводили ли Вы сравнение вашей разработки с RapidJSON SAX Writer?
dmitriym09 Автор
Приветствую! Пока нет. Была цель сделать библиотеку именно на Qt-типах. Но в планах сравнить есть. Думаю скорость будет зависеть от того, используется ли QVariant