Skip to content

Commit 44ecd74

Browse files
authored
Sync JSON web decoders (#1028)
This syncs the web JSON decoder used internally. The new decoder uses `js_interop` instead of the Dart standard library to improve performance when compiled to JS. ## Benchmarks master, JS: protobuf_from_json_string(RunTimeRaw): 1423.134328358209 us. PR, JS: protobuf_from_json_string(RunTimeRaw): 971 us. Wasm and VM benchmarks don't change as they keep using the old implementation. The new implementation is slower with Wasm as it does more JS interop, so we keep using the old one.
1 parent f7f65d4 commit 44ecd74

File tree

6 files changed

+542
-50
lines changed

6 files changed

+542
-50
lines changed

protobuf/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,12 @@
88
* Some of the private `PbFieldType` members are made public, to allow using
99
them in internal libraries. This type is for internal use only. ([#1027])
1010

11+
* Improve performance of `GeneratedMessage` members: `writeToJsonMap`,
12+
`writeToJson`, `mergeFromJson`, `mergeFromJsonMap`. ([#1028])
13+
1114
[#1026]: https://github.com/google/protobuf.dart/pull/1026
1215
[#1027]: https://github.com/google/protobuf.dart/pull/1027
16+
[#1028]: https://github.com/google/protobuf.dart/pull/1028
1317

1418
## 4.1.1
1519

protobuf/lib/src/protobuf/generated_message.dart

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ abstract class GeneratedMessage {
227227
/// Unknown field data, data for which there is no metadata for the associated
228228
/// field, will only be included if this message was deserialized from the
229229
/// same wire format.
230-
Map<String, dynamic> writeToJsonMap() => _writeToJsonMap(_fieldSet);
230+
Map<String, dynamic> writeToJsonMap() => json_lib.writeToJsonMap(_fieldSet);
231231

232232
/// Returns a JSON string that encodes this message.
233233
///
@@ -246,7 +246,7 @@ abstract class GeneratedMessage {
246246
/// Unknown field data, data for which there is no metadata for the associated
247247
/// field, will only be included if this message was deserialized from the
248248
/// same wire format.
249-
String writeToJson() => jsonEncode(writeToJsonMap());
249+
String writeToJson() => json_lib.writeToJsonString(_fieldSet);
250250

251251
/// Returns an Object representing Proto3 JSON serialization of `this`.
252252
///
@@ -318,27 +318,17 @@ abstract class GeneratedMessage {
318318
String data, [
319319
ExtensionRegistry extensionRegistry = ExtensionRegistry.EMPTY,
320320
]) {
321-
/// Disable lazy creation of Dart objects for a dart2js speedup.
322-
/// This is a slight regression on the Dart VM.
323-
/// TODO(skybrian) we could skip the reviver if we're running
324-
/// on the Dart VM for a slight speedup.
325-
final Map<String, dynamic> jsonMap = jsonDecode(
326-
data,
327-
reviver: _emptyReviver,
328-
);
329-
_mergeFromJsonMap(_fieldSet, jsonMap, extensionRegistry);
321+
json_lib.mergeFromJsonString(_fieldSet, data, extensionRegistry);
330322
}
331323

332-
static Object? _emptyReviver(Object? k, Object? v) => v;
333-
334324
/// Merges field values from a JSON object represented as a Dart map.
335325
///
336326
/// The encoding is described in [GeneratedMessage.writeToJson].
337327
void mergeFromJsonMap(
338328
Map<String, dynamic> json, [
339329
ExtensionRegistry extensionRegistry = ExtensionRegistry.EMPTY,
340330
]) {
341-
_mergeFromJsonMap(_fieldSet, json, extensionRegistry);
331+
json_lib.mergeFromJsonMap(_fieldSet, json, extensionRegistry);
342332
}
343333

344334
/// Adds an extension field value to a repeated field.

protobuf/lib/src/protobuf/internal.dart

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,15 @@
88
library;
99

1010
import 'dart:collection' show ListBase, MapBase;
11-
import 'dart:convert'
12-
show
13-
Utf8Decoder,
14-
Utf8Encoder,
15-
base64Decode,
16-
base64Encode,
17-
jsonDecode,
18-
jsonEncode;
11+
import 'dart:convert' show Utf8Decoder, Utf8Encoder, base64Decode, base64Encode;
1912
import 'dart:math' as math;
2013
import 'dart:typed_data' show ByteData, Endian, Uint8List;
2114

2215
import 'package:fixnum/fixnum.dart' show Int64;
2316
import 'package:meta/meta.dart' show UseResult;
2417

2518
import 'consts.dart';
19+
import 'json/json.dart' as json_lib;
2620
import 'json_parsing_context.dart';
2721
import 'permissive_compare.dart';
2822
import 'type_registry.dart';
@@ -45,7 +39,6 @@ part 'field_set.dart';
4539
part 'field_type.dart';
4640
part 'generated_message.dart';
4741
part 'generated_service.dart';
48-
part 'json.dart';
4942
part 'message_set.dart';
5043
part 'pb_list.dart';
5144
part 'pb_map.dart';

protobuf/lib/src/protobuf/json.dart renamed to protobuf/lib/src/protobuf/json/json.dart

Lines changed: 38 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,20 @@
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
44

5-
part of 'internal.dart';
5+
import 'dart:convert' show base64Decode, base64Encode;
66

7-
Map<String, dynamic> _writeToJsonMap(FieldSet fs) {
7+
import 'package:fixnum/fixnum.dart' show Int64;
8+
9+
import '../consts.dart';
10+
import '../internal.dart';
11+
import '../utils.dart';
12+
13+
// Use json_vm.dart with VM and dart2wasm, json_web.dart with dart2js.
14+
// json_web.dart uses JS interop for parsing, and JS interop is too slow on
15+
// Wasm. VM's patch performs better in Wasm.
16+
export 'json_vm.dart' if (dart.library.html) 'json_web.dart';
17+
18+
Map<String, dynamic> writeToJsonMap(FieldSet fs) {
819
dynamic convertToMap(dynamic fieldValue, int fieldType) {
920
final baseType = PbFieldType.baseType(fieldType);
1021

@@ -61,15 +72,15 @@ Map<String, dynamic> _writeToJsonMap(FieldSet fs) {
6172
List writeMap(PbMap fieldValue, MapFieldInfo fi) => List.from(
6273
fieldValue.entries.map(
6374
(MapEntry e) => {
64-
'${PbMap._keyFieldNumber}': convertToMap(e.key, fi.keyFieldType),
65-
'${PbMap._valueFieldNumber}': convertToMap(e.value, fi.valueFieldType),
75+
'$mapKeyFieldNumber': convertToMap(e.key, fi.keyFieldType),
76+
'$mapValueFieldNumber': convertToMap(e.value, fi.valueFieldType),
6677
},
6778
),
6879
);
6980

7081
final result = <String, dynamic>{};
71-
for (final fi in fs._infosSortedByTag) {
72-
final value = fs._values[fi.index!];
82+
for (final fi in fs.infosSortedByTag) {
83+
final value = fs.values[fi.index!];
7384
if (value == null || (value is List && value.isEmpty)) {
7485
continue; // It's missing, repeated, or an empty byte array.
7586
}
@@ -82,18 +93,18 @@ Map<String, dynamic> _writeToJsonMap(FieldSet fs) {
8293
}
8394
result['${fi.tagNumber}'] = convertToMap(value, fi.type);
8495
}
85-
final extensions = fs._extensions;
96+
final extensions = fs.extensions;
8697
if (extensions != null) {
87-
for (final tagNumber in sorted(extensions._tagNumbers)) {
88-
final value = extensions._values[tagNumber];
98+
for (final tagNumber in sorted(extensions.tagNumbers)) {
99+
final value = extensions.values[tagNumber];
89100
if (value is List && value.isEmpty) {
90101
continue; // It's repeated or an empty byte array.
91102
}
92-
final fi = extensions._getInfoOrNull(tagNumber)!;
103+
final fi = extensions.getInfoOrNull(tagNumber)!;
93104
result['$tagNumber'] = convertToMap(value, fi.type);
94105
}
95106
}
96-
final unknownJsonData = fs._unknownJsonData;
107+
final unknownJsonData = fs.unknownJsonData;
97108
if (unknownJsonData != null) {
98109
unknownJsonData.forEach((key, value) {
99110
result[key] = value;
@@ -104,20 +115,20 @@ Map<String, dynamic> _writeToJsonMap(FieldSet fs) {
104115

105116
// Merge fields from a previously decoded JSON object.
106117
// (Called recursively on nested messages.)
107-
void _mergeFromJsonMap(
118+
void mergeFromJsonMap(
108119
FieldSet fs,
109120
Map<String, dynamic> json,
110121
ExtensionRegistry? registry,
111122
) {
112-
fs._ensureWritable();
123+
fs.ensureWritable();
113124
final keys = json.keys;
114-
final meta = fs._meta;
125+
final meta = fs.meta;
115126
for (final key in keys) {
116127
var fi = meta.byTagAsString[key];
117128
if (fi == null) {
118-
fi = registry?.getExtension(fs._messageName, int.parse(key));
129+
fi = registry?.getExtension(fs.messageName, int.parse(key));
119130
if (fi == null) {
120-
(fs._unknownJsonData ??= {})[key] = json[key];
131+
(fs.unknownJsonData ??= {})[key] = json[key];
121132
continue;
122133
}
123134
}
@@ -144,7 +155,7 @@ void _appendJsonList(
144155
FieldInfo fi,
145156
ExtensionRegistry? registry,
146157
) {
147-
final repeated = fi._ensureRepeatedField(meta, fs);
158+
final repeated = fi.ensureRepeatedField(meta, fs);
148159
// Micro optimization. Using "for in" generates the following and iterator
149160
// alloc:
150161
// for (t1 = J.get$iterator$ax(json), t2 = fi.tagNumber, t3 = fi.type,
@@ -175,23 +186,23 @@ void _appendJsonMap(
175186
ExtensionRegistry? registry,
176187
) {
177188
final entryMeta = fi.mapEntryBuilderInfo;
178-
final map = fi._ensureMapField(meta, fs);
189+
final map = fi.ensureMapField(meta, fs);
179190
for (final jsonEntryDynamic in jsonList) {
180191
final jsonEntry = jsonEntryDynamic as Map<String, dynamic>;
181192
final entryFieldSet = FieldSet(null, entryMeta);
182193
final convertedKey = _convertJsonValue(
183194
entryMeta,
184195
entryFieldSet,
185-
jsonEntry['${PbMap._keyFieldNumber}'],
186-
PbMap._keyFieldNumber,
196+
jsonEntry['$mapKeyFieldNumber'],
197+
mapKeyFieldNumber,
187198
fi.keyFieldType,
188199
registry,
189200
);
190201
var convertedValue = _convertJsonValue(
191202
entryMeta,
192203
entryFieldSet,
193-
jsonEntry['${PbMap._valueFieldNumber}'],
194-
PbMap._valueFieldNumber,
204+
jsonEntry['$mapValueFieldNumber'],
205+
mapValueFieldNumber,
195206
fi.valueFieldType,
196207
registry,
197208
);
@@ -223,10 +234,10 @@ void _setJsonField(
223234
// Therefore we run _validateField for debug builds only to validate
224235
// correctness of conversion.
225236
assert(() {
226-
fs._validateField(fi, value);
237+
fs.validateField(fi, value);
227238
return true;
228239
}());
229-
fs._setFieldUnchecked(meta, fi, value);
240+
fs.setFieldUnchecked(meta, fi, value);
230241
}
231242

232243
/// Converts [value] from the JSON format to the Dart data type suitable for
@@ -298,7 +309,7 @@ dynamic _convertJsonValue(
298309
// The following call will return null if the enum value is unknown.
299310
// In that case, we want the caller to ignore this value, so we return
300311
// null from this method as well.
301-
return meta._decodeEnum(tagNumber, registry, value);
312+
return meta.decodeEnum(tagNumber, registry, value);
302313
}
303314
expectedType = 'int or stringified int';
304315
break;
@@ -333,8 +344,8 @@ dynamic _convertJsonValue(
333344
case PbFieldType.MESSAGE_BIT:
334345
if (value is Map) {
335346
final messageValue = value as Map<String, dynamic>;
336-
final subMessage = meta._makeEmptyMessage(tagNumber, registry);
337-
_mergeFromJsonMap(subMessage._fieldSet, messageValue, registry);
347+
final subMessage = meta.makeEmptyMessage(tagNumber, registry);
348+
mergeFromJsonMap(subMessage.fieldSet, messageValue, registry);
338349
return subMessage;
339350
}
340351
expectedType = 'nested message or group';
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'dart:convert' show jsonDecode, jsonEncode;
6+
7+
import '../internal.dart';
8+
import 'json.dart';
9+
10+
String writeToJsonString(FieldSet fs) => jsonEncode(writeToJsonMap(fs));
11+
12+
/// Merge fields from a [json] string.
13+
void mergeFromJsonString(
14+
FieldSet fs,
15+
String json,
16+
ExtensionRegistry? registry,
17+
) {
18+
final jsonMap = jsonDecode(json);
19+
if (jsonMap is! Map<String, dynamic>) {
20+
throw ArgumentError.value(json, 'json', 'Does not parse to a JSON object.');
21+
}
22+
mergeFromJsonMap(fs, jsonMap, registry);
23+
}

0 commit comments

Comments
 (0)