From f13b4b760abede416911058e1702bc1dd40d29e8 Mon Sep 17 00:00:00 2001 From: aaronp64 Date: Sat, 29 Jun 2024 22:53:25 -0400 Subject: [PATCH] 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 --- core/io/json.cpp | 105 +++++++++++++++++++++++--------------- core/io/json.h | 4 +- tests/core/io/test_json.h | 85 ++++++++++++++++++++++++++++++ 3 files changed, 151 insertions(+), 43 deletions(-) diff --git a/core/io/json.cpp b/core/io/json.cpp index 0945b8fea77..0582329bc39 100644 --- a/core/io/json.cpp +++ b/core/io/json.cpp @@ -47,42 +47,47 @@ const char *JSON::tk_name[TK_MAX] = { "EOF", }; -String JSON::_make_indent(const String &p_indent, int p_size) { - return p_indent.repeat(p_size); +void JSON::_add_indent(String &r_result, const String &p_indent, int 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 &p_markers, bool p_full_precision) { - ERR_FAIL_COND_V_MSG(p_cur_indent > Variant::MAX_RECURSION_DEPTH, "...", "JSON structure is too deep. Bailing."); - - String colon = ":"; - String end_statement = ""; - - if (!p_indent.is_empty()) { - colon += " "; - end_statement += "\n"; +void JSON::_stringify(String &r_result, const Variant &p_var, const String &p_indent, int p_cur_indent, bool p_sort_keys, HashSet &p_markers, bool p_full_precision) { + if (p_cur_indent > Variant::MAX_RECURSION_DEPTH) { + r_result += "..."; + ERR_FAIL_MSG("JSON structure is too deep. Bailing."); } + const char *colon = p_indent.is_empty() ? ":" : ": "; + const char *end_statement = p_indent.is_empty() ? "" : "\n"; + switch (p_var.get_type()) { case Variant::NIL: - return "null"; + r_result += "null"; + return; case Variant::BOOL: - return p_var.operator bool() ? "true" : "false"; + r_result += p_var.operator bool() ? "true" : "false"; + return; case Variant::INT: - return itos(p_var); + r_result += itos(p_var); + return; case Variant::FLOAT: { double num = p_var; // Only for exactly 0. If we have approximately 0 let the user decide how much // precision they want. if (num == double(0)) { - return String("0.0"); + r_result += "0.0"; + return; } double magnitude = std::log10(Math::abs(num)); int total_digits = p_full_precision ? 17 : 14; 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_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::ARRAY: { Array a = p_var; - if (a.is_empty()) { - return "[]"; + if (p_markers.has(a.id())) { + 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()); bool first = true; @@ -105,21 +116,27 @@ String JSON::_stringify(const Variant &p_var, const String &p_indent, int p_cur_ if (first) { first = false; } else { - s += ","; - s += end_statement; + r_result += ','; + 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()); - return s; + return; } case Variant::DICTIONARY: { - String s = "{"; - s += end_statement; 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()); LocalVector 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; - for (const Variant &E : keys) { + for (const Variant &key : keys) { if (first_key) { first_key = false; } else { - s += ","; - s += end_statement; + r_result += ','; + 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); - s += colon; - s += _stringify(d[E], p_indent, p_cur_indent + 1, p_sort_keys, p_markers); + _add_indent(r_result, p_indent, p_cur_indent + 1); + _stringify(r_result, String(key), 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()); - return s; + return; } 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) { - Ref json; - json.instantiate(); + String result; HashSet 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) { diff --git a/core/io/json.h b/core/io/json.h index 9169e57862d..06edeffac10 100644 --- a/core/io/json.h +++ b/core/io/json.h @@ -71,8 +71,8 @@ class JSON : public Resource { static const char *tk_name[]; - static String _make_indent(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 &p_markers, bool p_full_precision = false); + static void _add_indent(String &r_result, const String &p_indent, int p_size); + static void _stringify(String &r_result, const Variant &p_var, const String &p_indent, int p_cur_indent, bool p_sort_keys, HashSet &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 _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); diff --git a/tests/core/io/test_json.h b/tests/core/io/test_json.h index 15137d5ca7e..87448a24bcb 100644 --- a/tests/core/io/test_json.h +++ b/tests/core/io/test_json.h @@ -36,6 +36,91 @@ 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 // single-quoted strings, duplicate commas and trailing commas. // This is intentionally not tested as users shouldn't rely on this behavior.