You've already forked godot
mirror of
https://github.com/godotengine/godot.git
synced 2025-11-05 12:10:55 +00:00
Improve JSON::stringify performance
- Changed stringify to call static function _stringify directly, instead of creating JSON object - Changed colon and end_statement from String to const char * to avoid extra allocations in each _stringify call - Pass result String reference to each _stringify call to append to instead of allocating new String in each call These changes make JSON::stringify around 2-3x faster in most cases
This commit is contained in:
105
core/io/json.cpp
105
core/io/json.cpp
@@ -47,42 +47,47 @@ const char *JSON::tk_name[TK_MAX] = {
|
|||||||
"EOF",
|
"EOF",
|
||||||
};
|
};
|
||||||
|
|
||||||
String JSON::_make_indent(const String &p_indent, int p_size) {
|
void JSON::_add_indent(String &r_result, const String &p_indent, int p_size) {
|
||||||
return p_indent.repeat(p_size);
|
for (int i = 0; i < p_size; i++) {
|
||||||
|
r_result += p_indent;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String JSON::_stringify(const Variant &p_var, const String &p_indent, int p_cur_indent, bool p_sort_keys, HashSet<const void *> &p_markers, bool p_full_precision) {
|
void JSON::_stringify(String &r_result, const Variant &p_var, const String &p_indent, int p_cur_indent, bool p_sort_keys, HashSet<const void *> &p_markers, bool p_full_precision) {
|
||||||
ERR_FAIL_COND_V_MSG(p_cur_indent > Variant::MAX_RECURSION_DEPTH, "...", "JSON structure is too deep. Bailing.");
|
if (p_cur_indent > Variant::MAX_RECURSION_DEPTH) {
|
||||||
|
r_result += "...";
|
||||||
String colon = ":";
|
ERR_FAIL_MSG("JSON structure is too deep. Bailing.");
|
||||||
String end_statement = "";
|
|
||||||
|
|
||||||
if (!p_indent.is_empty()) {
|
|
||||||
colon += " ";
|
|
||||||
end_statement += "\n";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const char *colon = p_indent.is_empty() ? ":" : ": ";
|
||||||
|
const char *end_statement = p_indent.is_empty() ? "" : "\n";
|
||||||
|
|
||||||
switch (p_var.get_type()) {
|
switch (p_var.get_type()) {
|
||||||
case Variant::NIL:
|
case Variant::NIL:
|
||||||
return "null";
|
r_result += "null";
|
||||||
|
return;
|
||||||
case Variant::BOOL:
|
case Variant::BOOL:
|
||||||
return p_var.operator bool() ? "true" : "false";
|
r_result += p_var.operator bool() ? "true" : "false";
|
||||||
|
return;
|
||||||
case Variant::INT:
|
case Variant::INT:
|
||||||
return itos(p_var);
|
r_result += itos(p_var);
|
||||||
|
return;
|
||||||
case Variant::FLOAT: {
|
case Variant::FLOAT: {
|
||||||
double num = p_var;
|
double num = p_var;
|
||||||
|
|
||||||
// Only for exactly 0. If we have approximately 0 let the user decide how much
|
// Only for exactly 0. If we have approximately 0 let the user decide how much
|
||||||
// precision they want.
|
// precision they want.
|
||||||
if (num == double(0)) {
|
if (num == double(0)) {
|
||||||
return String("0.0");
|
r_result += "0.0";
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
double magnitude = std::log10(Math::abs(num));
|
double magnitude = std::log10(Math::abs(num));
|
||||||
int total_digits = p_full_precision ? 17 : 14;
|
int total_digits = p_full_precision ? 17 : 14;
|
||||||
int precision = MAX(1, total_digits - (int)Math::floor(magnitude));
|
int precision = MAX(1, total_digits - (int)Math::floor(magnitude));
|
||||||
|
|
||||||
return String::num(num, precision);
|
r_result += String::num(num, precision);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
case Variant::PACKED_INT32_ARRAY:
|
case Variant::PACKED_INT32_ARRAY:
|
||||||
case Variant::PACKED_INT64_ARRAY:
|
case Variant::PACKED_INT64_ARRAY:
|
||||||
@@ -91,13 +96,19 @@ String JSON::_stringify(const Variant &p_var, const String &p_indent, int p_cur_
|
|||||||
case Variant::PACKED_STRING_ARRAY:
|
case Variant::PACKED_STRING_ARRAY:
|
||||||
case Variant::ARRAY: {
|
case Variant::ARRAY: {
|
||||||
Array a = p_var;
|
Array a = p_var;
|
||||||
if (a.is_empty()) {
|
if (p_markers.has(a.id())) {
|
||||||
return "[]";
|
r_result += "\"[...]\"";
|
||||||
|
ERR_FAIL_MSG("Converting circular structure to JSON.");
|
||||||
}
|
}
|
||||||
String s = "[";
|
|
||||||
s += end_statement;
|
|
||||||
|
|
||||||
ERR_FAIL_COND_V_MSG(p_markers.has(a.id()), "\"[...]\"", "Converting circular structure to JSON.");
|
if (a.is_empty()) {
|
||||||
|
r_result += "[]";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
r_result += '[';
|
||||||
|
r_result += end_statement;
|
||||||
|
|
||||||
p_markers.insert(a.id());
|
p_markers.insert(a.id());
|
||||||
|
|
||||||
bool first = true;
|
bool first = true;
|
||||||
@@ -105,21 +116,27 @@ String JSON::_stringify(const Variant &p_var, const String &p_indent, int p_cur_
|
|||||||
if (first) {
|
if (first) {
|
||||||
first = false;
|
first = false;
|
||||||
} else {
|
} else {
|
||||||
s += ",";
|
r_result += ',';
|
||||||
s += end_statement;
|
r_result += end_statement;
|
||||||
}
|
}
|
||||||
s += _make_indent(p_indent, p_cur_indent + 1) + _stringify(var, p_indent, p_cur_indent + 1, p_sort_keys, p_markers);
|
_add_indent(r_result, p_indent, p_cur_indent + 1);
|
||||||
|
_stringify(r_result, var, p_indent, p_cur_indent + 1, p_sort_keys, p_markers);
|
||||||
}
|
}
|
||||||
s += end_statement + _make_indent(p_indent, p_cur_indent) + "]";
|
r_result += end_statement;
|
||||||
|
_add_indent(r_result, p_indent, p_cur_indent);
|
||||||
|
r_result += ']';
|
||||||
p_markers.erase(a.id());
|
p_markers.erase(a.id());
|
||||||
return s;
|
return;
|
||||||
}
|
}
|
||||||
case Variant::DICTIONARY: {
|
case Variant::DICTIONARY: {
|
||||||
String s = "{";
|
|
||||||
s += end_statement;
|
|
||||||
Dictionary d = p_var;
|
Dictionary d = p_var;
|
||||||
|
if (p_markers.has(d.id())) {
|
||||||
|
r_result += "\"{...}\"";
|
||||||
|
ERR_FAIL_MSG("Converting circular structure to JSON.");
|
||||||
|
}
|
||||||
|
|
||||||
ERR_FAIL_COND_V_MSG(p_markers.has(d.id()), "\"{...}\"", "Converting circular structure to JSON.");
|
r_result += '{';
|
||||||
|
r_result += end_statement;
|
||||||
p_markers.insert(d.id());
|
p_markers.insert(d.id());
|
||||||
|
|
||||||
LocalVector<Variant> keys = d.get_key_list();
|
LocalVector<Variant> keys = d.get_key_list();
|
||||||
@@ -129,24 +146,30 @@ String JSON::_stringify(const Variant &p_var, const String &p_indent, int p_cur_
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool first_key = true;
|
bool first_key = true;
|
||||||
for (const Variant &E : keys) {
|
for (const Variant &key : keys) {
|
||||||
if (first_key) {
|
if (first_key) {
|
||||||
first_key = false;
|
first_key = false;
|
||||||
} else {
|
} else {
|
||||||
s += ",";
|
r_result += ',';
|
||||||
s += end_statement;
|
r_result += end_statement;
|
||||||
}
|
}
|
||||||
s += _make_indent(p_indent, p_cur_indent + 1) + _stringify(String(E), p_indent, p_cur_indent + 1, p_sort_keys, p_markers);
|
_add_indent(r_result, p_indent, p_cur_indent + 1);
|
||||||
s += colon;
|
_stringify(r_result, String(key), p_indent, p_cur_indent + 1, p_sort_keys, p_markers);
|
||||||
s += _stringify(d[E], p_indent, p_cur_indent + 1, p_sort_keys, p_markers);
|
r_result += colon;
|
||||||
|
_stringify(r_result, d[key], p_indent, p_cur_indent + 1, p_sort_keys, p_markers);
|
||||||
}
|
}
|
||||||
|
|
||||||
s += end_statement + _make_indent(p_indent, p_cur_indent) + "}";
|
r_result += end_statement;
|
||||||
|
_add_indent(r_result, p_indent, p_cur_indent);
|
||||||
|
r_result += '}';
|
||||||
p_markers.erase(d.id());
|
p_markers.erase(d.id());
|
||||||
return s;
|
return;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return "\"" + String(p_var).json_escape() + "\"";
|
r_result += '"';
|
||||||
|
r_result += String(p_var).json_escape();
|
||||||
|
r_result += '"';
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -568,10 +591,10 @@ String JSON::get_parsed_text() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String JSON::stringify(const Variant &p_var, const String &p_indent, bool p_sort_keys, bool p_full_precision) {
|
String JSON::stringify(const Variant &p_var, const String &p_indent, bool p_sort_keys, bool p_full_precision) {
|
||||||
Ref<JSON> json;
|
String result;
|
||||||
json.instantiate();
|
|
||||||
HashSet<const void *> markers;
|
HashSet<const void *> markers;
|
||||||
return json->_stringify(p_var, p_indent, 0, p_sort_keys, markers, p_full_precision);
|
_stringify(result, p_var, p_indent, 0, p_sort_keys, markers, p_full_precision);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
Variant JSON::parse_string(const String &p_json_string) {
|
Variant JSON::parse_string(const String &p_json_string) {
|
||||||
|
|||||||
@@ -71,8 +71,8 @@ class JSON : public Resource {
|
|||||||
|
|
||||||
static const char *tk_name[];
|
static const char *tk_name[];
|
||||||
|
|
||||||
static String _make_indent(const String &p_indent, int p_size);
|
static void _add_indent(String &r_result, const String &p_indent, int p_size);
|
||||||
static String _stringify(const Variant &p_var, const String &p_indent, int p_cur_indent, bool p_sort_keys, HashSet<const void *> &p_markers, bool p_full_precision = false);
|
static void _stringify(String &r_result, const Variant &p_var, const String &p_indent, int p_cur_indent, bool p_sort_keys, HashSet<const void *> &p_markers, bool p_full_precision = false);
|
||||||
static Error _get_token(const char32_t *p_str, int &index, int p_len, Token &r_token, int &line, String &r_err_str);
|
static Error _get_token(const char32_t *p_str, int &index, int p_len, Token &r_token, int &line, String &r_err_str);
|
||||||
static Error _parse_value(Variant &value, Token &token, const char32_t *p_str, int &index, int p_len, int &line, int p_depth, String &r_err_str);
|
static Error _parse_value(Variant &value, Token &token, const char32_t *p_str, int &index, int p_len, int &line, int p_depth, String &r_err_str);
|
||||||
static Error _parse_array(Array &array, const char32_t *p_str, int &index, int p_len, int &line, int p_depth, String &r_err_str);
|
static Error _parse_array(Array &array, const char32_t *p_str, int &index, int p_len, int &line, int p_depth, String &r_err_str);
|
||||||
|
|||||||
@@ -36,6 +36,91 @@
|
|||||||
|
|
||||||
namespace TestJSON {
|
namespace TestJSON {
|
||||||
|
|
||||||
|
TEST_CASE("[JSON] Stringify single data types") {
|
||||||
|
CHECK(JSON::stringify(Variant()) == "null");
|
||||||
|
CHECK(JSON::stringify(false) == "false");
|
||||||
|
CHECK(JSON::stringify(true) == "true");
|
||||||
|
CHECK(JSON::stringify(0) == "0");
|
||||||
|
CHECK(JSON::stringify(12345) == "12345");
|
||||||
|
CHECK(JSON::stringify(0.75) == "0.75");
|
||||||
|
CHECK(JSON::stringify("test") == "\"test\"");
|
||||||
|
CHECK(JSON::stringify("\\\b\f\n\r\t\v\"") == "\"\\\\\\b\\f\\n\\r\\t\\v\\\"\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("[JSON] Stringify arrays") {
|
||||||
|
CHECK(JSON::stringify(Array()) == "[]");
|
||||||
|
|
||||||
|
Array int_array;
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
int_array.push_back(i);
|
||||||
|
}
|
||||||
|
CHECK(JSON::stringify(int_array) == "[0,1,2,3,4,5,6,7,8,9]");
|
||||||
|
|
||||||
|
Array str_array;
|
||||||
|
str_array.push_back("Hello");
|
||||||
|
str_array.push_back("World");
|
||||||
|
str_array.push_back("!");
|
||||||
|
CHECK(JSON::stringify(str_array) == "[\"Hello\",\"World\",\"!\"]");
|
||||||
|
|
||||||
|
Array indented_array;
|
||||||
|
Array nested_array;
|
||||||
|
for (int i = 0; i < 5; i++) {
|
||||||
|
indented_array.push_back(i);
|
||||||
|
nested_array.push_back(i);
|
||||||
|
}
|
||||||
|
indented_array.push_back(nested_array);
|
||||||
|
CHECK(JSON::stringify(indented_array, "\t") == "[\n\t0,\n\t1,\n\t2,\n\t3,\n\t4,\n\t[\n\t\t0,\n\t\t1,\n\t\t2,\n\t\t3,\n\t\t4\n\t]\n]");
|
||||||
|
|
||||||
|
ERR_PRINT_OFF
|
||||||
|
Array self_array;
|
||||||
|
self_array.push_back(self_array);
|
||||||
|
CHECK(JSON::stringify(self_array) == "[\"[...]\"]");
|
||||||
|
self_array.clear();
|
||||||
|
|
||||||
|
Array max_recursion_array;
|
||||||
|
for (int i = 0; i < Variant::MAX_RECURSION_DEPTH + 1; i++) {
|
||||||
|
Array next;
|
||||||
|
next.push_back(max_recursion_array);
|
||||||
|
max_recursion_array = next;
|
||||||
|
}
|
||||||
|
CHECK(JSON::stringify(max_recursion_array).contains("[...]"));
|
||||||
|
ERR_PRINT_ON
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("[JSON] Stringify dictionaries") {
|
||||||
|
CHECK(JSON::stringify(Dictionary()) == "{}");
|
||||||
|
|
||||||
|
Dictionary single_entry;
|
||||||
|
single_entry["key"] = "value";
|
||||||
|
CHECK(JSON::stringify(single_entry) == "{\"key\":\"value\"}");
|
||||||
|
|
||||||
|
Dictionary indented;
|
||||||
|
indented["key1"] = "value1";
|
||||||
|
indented["key2"] = 2;
|
||||||
|
CHECK(JSON::stringify(indented, "\t") == "{\n\t\"key1\": \"value1\",\n\t\"key2\": 2\n}");
|
||||||
|
|
||||||
|
Dictionary outer;
|
||||||
|
Dictionary inner;
|
||||||
|
inner["key"] = "value";
|
||||||
|
outer["inner"] = inner;
|
||||||
|
CHECK(JSON::stringify(outer) == "{\"inner\":{\"key\":\"value\"}}");
|
||||||
|
|
||||||
|
ERR_PRINT_OFF
|
||||||
|
Dictionary self_dictionary;
|
||||||
|
self_dictionary["key"] = self_dictionary;
|
||||||
|
CHECK(JSON::stringify(self_dictionary) == "{\"key\":\"{...}\"}");
|
||||||
|
self_dictionary.clear();
|
||||||
|
|
||||||
|
Dictionary max_recursion_dictionary;
|
||||||
|
for (int i = 0; i < Variant::MAX_RECURSION_DEPTH + 1; i++) {
|
||||||
|
Dictionary next;
|
||||||
|
next["key"] = max_recursion_dictionary;
|
||||||
|
max_recursion_dictionary = next;
|
||||||
|
}
|
||||||
|
CHECK(JSON::stringify(max_recursion_dictionary).contains("{...:...}"));
|
||||||
|
ERR_PRINT_ON
|
||||||
|
}
|
||||||
|
|
||||||
// NOTE: The current JSON parser accepts many non-conformant strings such as
|
// NOTE: The current JSON parser accepts many non-conformant strings such as
|
||||||
// single-quoted strings, duplicate commas and trailing commas.
|
// single-quoted strings, duplicate commas and trailing commas.
|
||||||
// This is intentionally not tested as users shouldn't rely on this behavior.
|
// This is intentionally not tested as users shouldn't rely on this behavior.
|
||||||
|
|||||||
Reference in New Issue
Block a user