Skip to content

Commit 0ce3d75

Browse files
committed
Merge pull request godotengine#93783 from aaronp64/json_stringify_performance
Improve `JSON::stringify` performance
2 parents 5a71e5a + f13b4b7 commit 0ce3d75

File tree

3 files changed

+150
-42
lines changed

3 files changed

+150
-42
lines changed

core/io/json.cpp

Lines changed: 63 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -47,42 +47,47 @@ const char *JSON::tk_name[TK_MAX] = {
4747
"EOF",
4848
};
4949

50-
String JSON::_make_indent(const String &p_indent, int p_size) {
51-
return p_indent.repeat(p_size);
50+
void JSON::_add_indent(String &r_result, const String &p_indent, int p_size) {
51+
for (int i = 0; i < p_size; i++) {
52+
r_result += p_indent;
53+
}
5254
}
5355

54-
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) {
55-
ERR_FAIL_COND_V_MSG(p_cur_indent > Variant::MAX_RECURSION_DEPTH, "...", "JSON structure is too deep. Bailing.");
56-
57-
String colon = ":";
58-
String end_statement = "";
59-
60-
if (!p_indent.is_empty()) {
61-
colon += " ";
62-
end_statement += "\n";
56+
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) {
57+
if (p_cur_indent > Variant::MAX_RECURSION_DEPTH) {
58+
r_result += "...";
59+
ERR_FAIL_MSG("JSON structure is too deep. Bailing.");
6360
}
6461

62+
const char *colon = p_indent.is_empty() ? ":" : ": ";
63+
const char *end_statement = p_indent.is_empty() ? "" : "\n";
64+
6565
switch (p_var.get_type()) {
6666
case Variant::NIL:
67-
return "null";
67+
r_result += "null";
68+
return;
6869
case Variant::BOOL:
69-
return p_var.operator bool() ? "true" : "false";
70+
r_result += p_var.operator bool() ? "true" : "false";
71+
return;
7072
case Variant::INT:
71-
return itos(p_var);
73+
r_result += itos(p_var);
74+
return;
7275
case Variant::FLOAT: {
7376
double num = p_var;
7477

7578
// Only for exactly 0. If we have approximately 0 let the user decide how much
7679
// precision they want.
7780
if (num == double(0)) {
78-
return String("0.0");
81+
r_result += "0.0";
82+
return;
7983
}
8084

8185
double magnitude = std::log10(Math::abs(num));
8286
int total_digits = p_full_precision ? 17 : 14;
8387
int precision = MAX(1, total_digits - (int)Math::floor(magnitude));
8488

85-
return String::num(num, precision);
89+
r_result += String::num(num, precision);
90+
return;
8691
}
8792
case Variant::PACKED_INT32_ARRAY:
8893
case Variant::PACKED_INT64_ARRAY:
@@ -91,35 +96,47 @@ String JSON::_stringify(const Variant &p_var, const String &p_indent, int p_cur_
9196
case Variant::PACKED_STRING_ARRAY:
9297
case Variant::ARRAY: {
9398
Array a = p_var;
99+
if (p_markers.has(a.id())) {
100+
r_result += "\"[...]\"";
101+
ERR_FAIL_MSG("Converting circular structure to JSON.");
102+
}
103+
94104
if (a.is_empty()) {
95-
return "[]";
105+
r_result += "[]";
106+
return;
96107
}
97-
String s = "[";
98-
s += end_statement;
99108

100-
ERR_FAIL_COND_V_MSG(p_markers.has(a.id()), "\"[...]\"", "Converting circular structure to JSON.");
109+
r_result += '[';
110+
r_result += end_statement;
111+
101112
p_markers.insert(a.id());
102113

103114
bool first = true;
104115
for (const Variant &var : a) {
105116
if (first) {
106117
first = false;
107118
} else {
108-
s += ",";
109-
s += end_statement;
119+
r_result += ',';
120+
r_result += end_statement;
110121
}
111-
s += _make_indent(p_indent, p_cur_indent + 1) + _stringify(var, p_indent, p_cur_indent + 1, p_sort_keys, p_markers);
122+
_add_indent(r_result, p_indent, p_cur_indent + 1);
123+
_stringify(r_result, var, p_indent, p_cur_indent + 1, p_sort_keys, p_markers);
112124
}
113-
s += end_statement + _make_indent(p_indent, p_cur_indent) + "]";
125+
r_result += end_statement;
126+
_add_indent(r_result, p_indent, p_cur_indent);
127+
r_result += ']';
114128
p_markers.erase(a.id());
115-
return s;
129+
return;
116130
}
117131
case Variant::DICTIONARY: {
118-
String s = "{";
119-
s += end_statement;
120132
Dictionary d = p_var;
133+
if (p_markers.has(d.id())) {
134+
r_result += "\"{...}\"";
135+
ERR_FAIL_MSG("Converting circular structure to JSON.");
136+
}
121137

122-
ERR_FAIL_COND_V_MSG(p_markers.has(d.id()), "\"{...}\"", "Converting circular structure to JSON.");
138+
r_result += '{';
139+
r_result += end_statement;
123140
p_markers.insert(d.id());
124141

125142
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_
129146
}
130147

131148
bool first_key = true;
132-
for (const Variant &E : keys) {
149+
for (const Variant &key : keys) {
133150
if (first_key) {
134151
first_key = false;
135152
} else {
136-
s += ",";
137-
s += end_statement;
153+
r_result += ',';
154+
r_result += end_statement;
138155
}
139-
s += _make_indent(p_indent, p_cur_indent + 1) + _stringify(String(E), p_indent, p_cur_indent + 1, p_sort_keys, p_markers);
140-
s += colon;
141-
s += _stringify(d[E], p_indent, p_cur_indent + 1, p_sort_keys, p_markers);
156+
_add_indent(r_result, p_indent, p_cur_indent + 1);
157+
_stringify(r_result, String(key), p_indent, p_cur_indent + 1, p_sort_keys, p_markers);
158+
r_result += colon;
159+
_stringify(r_result, d[key], p_indent, p_cur_indent + 1, p_sort_keys, p_markers);
142160
}
143161

144-
s += end_statement + _make_indent(p_indent, p_cur_indent) + "}";
162+
r_result += end_statement;
163+
_add_indent(r_result, p_indent, p_cur_indent);
164+
r_result += '}';
145165
p_markers.erase(d.id());
146-
return s;
166+
return;
147167
}
148168
default:
149-
return "\"" + String(p_var).json_escape() + "\"";
169+
r_result += '"';
170+
r_result += String(p_var).json_escape();
171+
r_result += '"';
172+
return;
150173
}
151174
}
152175

@@ -568,10 +591,10 @@ String JSON::get_parsed_text() const {
568591
}
569592

570593
String JSON::stringify(const Variant &p_var, const String &p_indent, bool p_sort_keys, bool p_full_precision) {
571-
Ref<JSON> json;
572-
json.instantiate();
594+
String result;
573595
HashSet<const void *> markers;
574-
return json->_stringify(p_var, p_indent, 0, p_sort_keys, markers, p_full_precision);
596+
_stringify(result, p_var, p_indent, 0, p_sort_keys, markers, p_full_precision);
597+
return result;
575598
}
576599

577600
Variant JSON::parse_string(const String &p_json_string) {

core/io/json.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,8 @@ class JSON : public Resource {
7171

7272
static const char *tk_name[];
7373

74-
static String _make_indent(const String &p_indent, int p_size);
75-
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);
74+
static void _add_indent(String &r_result, const String &p_indent, int p_size);
75+
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);
7676
static Error _get_token(const char32_t *p_str, int &index, int p_len, Token &r_token, int &line, String &r_err_str);
7777
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);
7878
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);

tests/core/io/test_json.h

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,91 @@
3636

3737
namespace TestJSON {
3838

39+
TEST_CASE("[JSON] Stringify single data types") {
40+
CHECK(JSON::stringify(Variant()) == "null");
41+
CHECK(JSON::stringify(false) == "false");
42+
CHECK(JSON::stringify(true) == "true");
43+
CHECK(JSON::stringify(0) == "0");
44+
CHECK(JSON::stringify(12345) == "12345");
45+
CHECK(JSON::stringify(0.75) == "0.75");
46+
CHECK(JSON::stringify("test") == "\"test\"");
47+
CHECK(JSON::stringify("\\\b\f\n\r\t\v\"") == "\"\\\\\\b\\f\\n\\r\\t\\v\\\"\"");
48+
}
49+
50+
TEST_CASE("[JSON] Stringify arrays") {
51+
CHECK(JSON::stringify(Array()) == "[]");
52+
53+
Array int_array;
54+
for (int i = 0; i < 10; i++) {
55+
int_array.push_back(i);
56+
}
57+
CHECK(JSON::stringify(int_array) == "[0,1,2,3,4,5,6,7,8,9]");
58+
59+
Array str_array;
60+
str_array.push_back("Hello");
61+
str_array.push_back("World");
62+
str_array.push_back("!");
63+
CHECK(JSON::stringify(str_array) == "[\"Hello\",\"World\",\"!\"]");
64+
65+
Array indented_array;
66+
Array nested_array;
67+
for (int i = 0; i < 5; i++) {
68+
indented_array.push_back(i);
69+
nested_array.push_back(i);
70+
}
71+
indented_array.push_back(nested_array);
72+
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]");
73+
74+
ERR_PRINT_OFF
75+
Array self_array;
76+
self_array.push_back(self_array);
77+
CHECK(JSON::stringify(self_array) == "[\"[...]\"]");
78+
self_array.clear();
79+
80+
Array max_recursion_array;
81+
for (int i = 0; i < Variant::MAX_RECURSION_DEPTH + 1; i++) {
82+
Array next;
83+
next.push_back(max_recursion_array);
84+
max_recursion_array = next;
85+
}
86+
CHECK(JSON::stringify(max_recursion_array).contains("[...]"));
87+
ERR_PRINT_ON
88+
}
89+
90+
TEST_CASE("[JSON] Stringify dictionaries") {
91+
CHECK(JSON::stringify(Dictionary()) == "{}");
92+
93+
Dictionary single_entry;
94+
single_entry["key"] = "value";
95+
CHECK(JSON::stringify(single_entry) == "{\"key\":\"value\"}");
96+
97+
Dictionary indented;
98+
indented["key1"] = "value1";
99+
indented["key2"] = 2;
100+
CHECK(JSON::stringify(indented, "\t") == "{\n\t\"key1\": \"value1\",\n\t\"key2\": 2\n}");
101+
102+
Dictionary outer;
103+
Dictionary inner;
104+
inner["key"] = "value";
105+
outer["inner"] = inner;
106+
CHECK(JSON::stringify(outer) == "{\"inner\":{\"key\":\"value\"}}");
107+
108+
ERR_PRINT_OFF
109+
Dictionary self_dictionary;
110+
self_dictionary["key"] = self_dictionary;
111+
CHECK(JSON::stringify(self_dictionary) == "{\"key\":\"{...}\"}");
112+
self_dictionary.clear();
113+
114+
Dictionary max_recursion_dictionary;
115+
for (int i = 0; i < Variant::MAX_RECURSION_DEPTH + 1; i++) {
116+
Dictionary next;
117+
next["key"] = max_recursion_dictionary;
118+
max_recursion_dictionary = next;
119+
}
120+
CHECK(JSON::stringify(max_recursion_dictionary).contains("{...:...}"));
121+
ERR_PRINT_ON
122+
}
123+
39124
// NOTE: The current JSON parser accepts many non-conformant strings such as
40125
// single-quoted strings, duplicate commas and trailing commas.
41126
// This is intentionally not tested as users shouldn't rely on this behavior.

0 commit comments

Comments
 (0)