Improve enum decoding -- alternative (#981)
When decoding an enum value, we call a callback in the enum field's `FieldInfo`. The callback then indexes a map mapping enum numbers to Dart values.
When these conditions hold:
- The known enum numbers are all positive. (so that we can use a list and index it with the number)
- The known enum numbers are more than 70% of the large known enum number. (so that the list won't have a lot of `null` entries, wasting heap space)
We now generate a list instead of a map to map enum numbers to Dart values.
Similar to the map, the list is runtime allocated. No new code generated per message or enum type.
AOT benchmarks:
- Before: `protobuf_PackedEnumDecoding(RunTimeRaw): 47585.14 us.`
- After: `protobuf_PackedEnumDecoding(RunTimeRaw): 38974.566666666666 us.`
- Diff: -18%
Wasm benchmarks:
- Before: `protobuf_PackedEnumDecoding(RunTimeRaw): 52225.0 us.`
- After: `protobuf_PackedEnumDecoding(RunTimeRaw): 34283.33333333333 us.`
- Diff: -34%
**Alternatives considered:**
- #980 uses a map always, but eliminates the `valueOf` closure.
- #985 uses a list always, and does binary search in the list when the list is "shallow".
- #987 is the same as #985, but instead of calling the `valueOf` closure it stores an extra field in `FieldInfo`s for whether to binary search or directly index.
These are all slower than the current PR.
diff --git a/protobuf/lib/src/protobuf/protobuf_enum.dart b/protobuf/lib/src/protobuf/protobuf_enum.dart
index b96c119..46c173e 100644
--- a/protobuf/lib/src/protobuf/protobuf_enum.dart
+++ b/protobuf/lib/src/protobuf/protobuf_enum.dart
@@ -2,6 +2,8 @@
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
+// ignore_for_file: non_constant_identifier_names
+
part of '../../protobuf.dart';
/// A base class for all proto enum types.
@@ -37,12 +39,28 @@
/// Creates a new constant [ProtobufEnum] using [value] and [name].
const ProtobufEnum(this.value, this.name);
- /// Creates a Map for all of the [ProtobufEnum]s in [byIndex], mapping each
+ /// This function is for generated code.
+ ///
+ /// Creates a Map for all of the [ProtobufEnum]s in [enumValues], mapping each
/// [ProtobufEnum]'s [value] to the [ProtobufEnum].
- static Map<int, T> initByValue<T extends ProtobufEnum>(List<T> byIndex) {
+ ///
+ /// @nodoc
+ static Map<int, T> initByValue<T extends ProtobufEnum>(List<T> enumValues) {
final byValue = <int, T>{};
- for (final v in byIndex) {
- byValue[v.value] = v;
+ for (final enumValue in enumValues) {
+ byValue[enumValue.value] = enumValue;
+ }
+ return byValue;
+ }
+
+ /// This function is for generated code.
+ ///
+ /// @nodoc
+ static List<T?> $_initByValueList<T extends ProtobufEnum>(
+ List<T> enumValues, int maxEnumValue) {
+ final byValue = List<T?>.filled(maxEnumValue + 1, null);
+ for (final enumValue in enumValues) {
+ byValue[enumValue.value] = enumValue;
}
return byValue;
}
diff --git a/protoc_plugin/Makefile b/protoc_plugin/Makefile
index bab82e3..6c04fa5 100644
--- a/protoc_plugin/Makefile
+++ b/protoc_plugin/Makefile
@@ -30,6 +30,7 @@
entity \
enum_extension \
enum_name \
+ enums \
extend_unittest \
ExtensionEnumNameConflict \
ExtensionNameConflict \
diff --git a/protoc_plugin/lib/src/enum_generator.dart b/protoc_plugin/lib/src/enum_generator.dart
index a1cd8b5..287a4fe 100644
--- a/protoc_plugin/lib/src/enum_generator.dart
+++ b/protoc_plugin/lib/src/enum_generator.dart
@@ -175,11 +175,37 @@
out.println('];');
out.println();
- out.println(
- 'static final $coreImportPrefix.Map<$coreImportPrefix.int, $classname> _byValue ='
- ' $protobufImportPrefix.ProtobufEnum.initByValue(values);');
- out.println('static $classname? valueOf($coreImportPrefix.int value) =>'
- ' _byValue[value];');
+ var maxEnumValue = -1;
+ for (final valueDescriptor in _canonicalValues) {
+ if (valueDescriptor.number.isNegative) {
+ maxEnumValue = -1; // don't use list
+ break;
+ }
+ if (valueDescriptor.number > maxEnumValue) {
+ maxEnumValue = valueDescriptor.number;
+ }
+ }
+
+ final useList = _canonicalValues.isEmpty ||
+ (maxEnumValue >= 0 &&
+ _canonicalValues.length / (maxEnumValue + 1) >= 0.7);
+
+ if (useList) {
+ out.println(
+ 'static final $coreImportPrefix.List<$classname?> _byValue ='
+ ' $protobufImportPrefix.ProtobufEnum.\$_initByValueList(values, $maxEnumValue);');
+
+ out.println('static $classname? valueOf($coreImportPrefix.int value) =>'
+ ' value < 0 || value >= _byValue.length ? null : _byValue[value];');
+ } else {
+ out.println(
+ 'static final $coreImportPrefix.Map<$coreImportPrefix.int, $classname> _byValue ='
+ ' $protobufImportPrefix.ProtobufEnum.initByValue(values);');
+
+ out.println('static $classname? valueOf($coreImportPrefix.int value) =>'
+ ' _byValue[value];');
+ }
+
out.println();
out.println('const $classname._(super.v, super.n);');
diff --git a/protoc_plugin/lib/src/generated/descriptor.pbenum.dart b/protoc_plugin/lib/src/generated/descriptor.pbenum.dart
index dc6f349..15909ed 100644
--- a/protoc_plugin/lib/src/generated/descriptor.pbenum.dart
+++ b/protoc_plugin/lib/src/generated/descriptor.pbenum.dart
@@ -88,9 +88,10 @@
TYPE_SINT64,
];
- static final $core.Map<$core.int, FieldDescriptorProto_Type> _byValue =
- $pb.ProtobufEnum.initByValue(values);
- static FieldDescriptorProto_Type? valueOf($core.int value) => _byValue[value];
+ static final $core.List<FieldDescriptorProto_Type?> _byValue =
+ $pb.ProtobufEnum.$_initByValueList(values, 18);
+ static FieldDescriptorProto_Type? valueOf($core.int value) =>
+ value < 0 || value >= _byValue.length ? null : _byValue[value];
const FieldDescriptorProto_Type._(super.v, super.n);
}
@@ -111,10 +112,10 @@
LABEL_REPEATED,
];
- static final $core.Map<$core.int, FieldDescriptorProto_Label> _byValue =
- $pb.ProtobufEnum.initByValue(values);
+ static final $core.List<FieldDescriptorProto_Label?> _byValue =
+ $pb.ProtobufEnum.$_initByValueList(values, 3);
static FieldDescriptorProto_Label? valueOf($core.int value) =>
- _byValue[value];
+ value < 0 || value >= _byValue.length ? null : _byValue[value];
const FieldDescriptorProto_Label._(super.v, super.n);
}
@@ -137,9 +138,10 @@
LITE_RUNTIME,
];
- static final $core.Map<$core.int, FileOptions_OptimizeMode> _byValue =
- $pb.ProtobufEnum.initByValue(values);
- static FileOptions_OptimizeMode? valueOf($core.int value) => _byValue[value];
+ static final $core.List<FileOptions_OptimizeMode?> _byValue =
+ $pb.ProtobufEnum.$_initByValueList(values, 3);
+ static FileOptions_OptimizeMode? valueOf($core.int value) =>
+ value < 0 || value >= _byValue.length ? null : _byValue[value];
const FileOptions_OptimizeMode._(super.v, super.n);
}
@@ -159,9 +161,10 @@
STRING_PIECE,
];
- static final $core.Map<$core.int, FieldOptions_CType> _byValue =
- $pb.ProtobufEnum.initByValue(values);
- static FieldOptions_CType? valueOf($core.int value) => _byValue[value];
+ static final $core.List<FieldOptions_CType?> _byValue =
+ $pb.ProtobufEnum.$_initByValueList(values, 2);
+ static FieldOptions_CType? valueOf($core.int value) =>
+ value < 0 || value >= _byValue.length ? null : _byValue[value];
const FieldOptions_CType._(super.v, super.n);
}
@@ -185,9 +188,10 @@
JS_NUMBER,
];
- static final $core.Map<$core.int, FieldOptions_JSType> _byValue =
- $pb.ProtobufEnum.initByValue(values);
- static FieldOptions_JSType? valueOf($core.int value) => _byValue[value];
+ static final $core.List<FieldOptions_JSType?> _byValue =
+ $pb.ProtobufEnum.$_initByValueList(values, 2);
+ static FieldOptions_JSType? valueOf($core.int value) =>
+ value < 0 || value >= _byValue.length ? null : _byValue[value];
const FieldOptions_JSType._(super.v, super.n);
}
@@ -212,10 +216,10 @@
IDEMPOTENT,
];
- static final $core.Map<$core.int, MethodOptions_IdempotencyLevel> _byValue =
- $pb.ProtobufEnum.initByValue(values);
+ static final $core.List<MethodOptions_IdempotencyLevel?> _byValue =
+ $pb.ProtobufEnum.$_initByValueList(values, 2);
static MethodOptions_IdempotencyLevel? valueOf($core.int value) =>
- _byValue[value];
+ value < 0 || value >= _byValue.length ? null : _byValue[value];
const MethodOptions_IdempotencyLevel._(super.v, super.n);
}
diff --git a/protoc_plugin/lib/src/generated/plugin.pbenum.dart b/protoc_plugin/lib/src/generated/plugin.pbenum.dart
index 399158c..2f427c9 100644
--- a/protoc_plugin/lib/src/generated/plugin.pbenum.dart
+++ b/protoc_plugin/lib/src/generated/plugin.pbenum.dart
@@ -27,10 +27,10 @@
FEATURE_PROTO3_OPTIONAL,
];
- static final $core.Map<$core.int, CodeGeneratorResponse_Feature> _byValue =
- $pb.ProtobufEnum.initByValue(values);
+ static final $core.List<CodeGeneratorResponse_Feature?> _byValue =
+ $pb.ProtobufEnum.$_initByValueList(values, 1);
static CodeGeneratorResponse_Feature? valueOf($core.int value) =>
- _byValue[value];
+ value < 0 || value >= _byValue.length ? null : _byValue[value];
const CodeGeneratorResponse_Feature._(super.v, super.n);
}
diff --git a/protoc_plugin/test/generated_message_test.dart b/protoc_plugin/test/generated_message_test.dart
index a25f6a4..63f8f1e 100644
--- a/protoc_plugin/test/generated_message_test.dart
+++ b/protoc_plugin/test/generated_message_test.dart
@@ -10,6 +10,7 @@
import '../out/protos/constructor_args/google/protobuf/unittest_import.pb.dart'
as constructor_args_unittest_import;
import '../out/protos/duplicate_names_import.pb.dart';
+import '../out/protos/enums.pb.dart';
import '../out/protos/google/protobuf/unittest.pb.dart';
import '../out/protos/google/protobuf/unittest_import.pb.dart';
import '../out/protos/google/protobuf/unittest_optimize_for.pb.dart';
@@ -890,4 +891,27 @@
// constructor arguments, to be able to reuse `assertAllFieldsSet`.
assertAllFieldsSet(TestAllTypes.fromBuffer(value.writeToBuffer()));
});
+
+ test('Handling enums defined out of order', () {
+ final message = MessageWithEnums();
+ for (final enum_ in DenseEnum.values) {
+ message.denseEnums.add(enum_);
+ }
+ for (final enum_ in DenseEnumOutOfOrder.values) {
+ message.denseOutOfOrderEnums.add(enum_);
+ }
+ for (final enum_ in SparseEnum.values) {
+ message.sparseEnums.add(enum_);
+ }
+ for (final enum_ in SparseEnumOutOfOrder.values) {
+ message.sparseOutOfOrderEnums.add(enum_);
+ }
+
+ final encoded = message.writeToBuffer();
+ final decoded = MessageWithEnums.fromBuffer(encoded);
+ expect(decoded.denseEnums, DenseEnum.values);
+ expect(decoded.denseOutOfOrderEnums, DenseEnumOutOfOrder.values);
+ expect(decoded.sparseEnums, SparseEnum.values);
+ expect(decoded.sparseOutOfOrderEnums, SparseEnumOutOfOrder.values);
+ });
}
diff --git a/protoc_plugin/test/goldens/deprecations.pbenum b/protoc_plugin/test/goldens/deprecations.pbenum
index 30bc5d7..c21797e 100644
--- a/protoc_plugin/test/goldens/deprecations.pbenum
+++ b/protoc_plugin/test/goldens/deprecations.pbenum
@@ -24,9 +24,10 @@
A2,
];
- static final $core.Map<$core.int, A> _byValue =
- $pb.ProtobufEnum.initByValue(values);
- static A? valueOf($core.int value) => _byValue[value];
+ static final $core.List<A?> _byValue =
+ $pb.ProtobufEnum.$_initByValueList(values, 1);
+ static A? valueOf($core.int value) =>
+ value < 0 || value >= _byValue.length ? null : _byValue[value];
const A._(super.v, super.n);
}
diff --git a/protoc_plugin/test/goldens/doc_comments.pbenum b/protoc_plugin/test/goldens/doc_comments.pbenum
index 7cde50e..1a35f24 100644
--- a/protoc_plugin/test/goldens/doc_comments.pbenum
+++ b/protoc_plugin/test/goldens/doc_comments.pbenum
@@ -26,9 +26,10 @@
A2,
];
- static final $core.Map<$core.int, A> _byValue =
- $pb.ProtobufEnum.initByValue(values);
- static A? valueOf($core.int value) => _byValue[value];
+ static final $core.List<A?> _byValue =
+ $pb.ProtobufEnum.$_initByValueList(values, 1);
+ static A? valueOf($core.int value) =>
+ value < 0 || value >= _byValue.length ? null : _byValue[value];
const A._(super.v, super.n);
}
diff --git a/protoc_plugin/test/goldens/enum b/protoc_plugin/test/goldens/enum
index 812247d..2dca720 100644
--- a/protoc_plugin/test/goldens/enum
+++ b/protoc_plugin/test/goldens/enum
@@ -11,8 +11,8 @@
WORK,
];
- static final $core.Map<$core.int, PhoneType> _byValue = $pb.ProtobufEnum.initByValue(values);
- static PhoneType? valueOf($core.int value) => _byValue[value];
+ static final $core.List<PhoneType?> _byValue = $pb.ProtobufEnum.$_initByValueList(values, 2);
+ static PhoneType? valueOf($core.int value) => value < 0 || value >= _byValue.length ? null : _byValue[value];
const PhoneType._(super.v, super.n);
}
diff --git a/protoc_plugin/test/goldens/messageGeneratorEnums b/protoc_plugin/test/goldens/messageGeneratorEnums
index 3469c7b..3d673da 100644
--- a/protoc_plugin/test/goldens/messageGeneratorEnums
+++ b/protoc_plugin/test/goldens/messageGeneratorEnums
@@ -11,8 +11,8 @@
WORK,
];
- static final $core.Map<$core.int, PhoneNumber_PhoneType> _byValue = $pb.ProtobufEnum.initByValue(values);
- static PhoneNumber_PhoneType? valueOf($core.int value) => _byValue[value];
+ static final $core.List<PhoneNumber_PhoneType?> _byValue = $pb.ProtobufEnum.$_initByValueList(values, 2);
+ static PhoneNumber_PhoneType? valueOf($core.int value) => value < 0 || value >= _byValue.length ? null : _byValue[value];
const PhoneNumber_PhoneType._(super.v, super.n);
}
diff --git a/protoc_plugin/test/goldens/topLevelEnum.pbenum b/protoc_plugin/test/goldens/topLevelEnum.pbenum
index 30d81a2..69cb70d 100644
--- a/protoc_plugin/test/goldens/topLevelEnum.pbenum
+++ b/protoc_plugin/test/goldens/topLevelEnum.pbenum
@@ -26,8 +26,8 @@
WORK,
];
- static final $core.Map<$core.int, PhoneType> _byValue = $pb.ProtobufEnum.initByValue(values);
- static PhoneType? valueOf($core.int value) => _byValue[value];
+ static final $core.List<PhoneType?> _byValue = $pb.ProtobufEnum.$_initByValueList(values, 2);
+ static PhoneType? valueOf($core.int value) => value < 0 || value >= _byValue.length ? null : _byValue[value];
const PhoneType._(super.v, super.n);
}
diff --git a/protoc_plugin/test/protos/enums.proto b/protoc_plugin/test/protos/enums.proto
new file mode 100644
index 0000000..03647c9
--- /dev/null
+++ b/protoc_plugin/test/protos/enums.proto
@@ -0,0 +1,54 @@
+enum DenseEnum {
+ DENSE_ENUM_0 = 0;
+ DENSE_ENUM_1 = 1;
+ DENSE_ENUM_2 = 2;
+ DENSE_ENUM_3 = 3;
+ DENSE_ENUM_4 = 4;
+}
+
+enum DenseEnumOutOfOrder {
+ DENSE_ENUM_OOO_0 = 0;
+ DENSE_ENUM_OOO_2 = 2;
+ DENSE_ENUM_OOO_4 = 4;
+ DENSE_ENUM_OOO_3 = 3;
+ DENSE_ENUM_OOO_1 = 1;
+}
+
+enum SparseEnum {
+ SPARSE_ENUM_ZERO = 0;
+ SPARSE_ENUM_MIN_INT = -2147483648;
+ SPARSE_ENUM_1 = -1000000000;
+ SPARSE_ENUM_2 = -100000000;
+ SPARSE_ENUM_3 = -10000000;
+ SPARSE_ENUM_4 = -1000000;
+ SPARSE_ENUM_5 = -100000;
+ SPARSE_ENUM_6 = 100000;
+ SPARSE_ENUM_7 = 1000000;
+ SPARSE_ENUM_8 = 10000000;
+ SPARSE_ENUM_9 = 100000000;
+ SPARSE_ENUM_10 = 1000000000;
+ SPARSE_ENUM_MAX_INT = 2147483647;
+}
+
+enum SparseEnumOutOfOrder {
+ SPARSE_ENUM_OOO_ZERO = 0;
+ SPARSE_ENUM_OOO_1 = -1000000000;
+ SPARSE_ENUM_OOO_MAX_INT = 2147483647;
+ SPARSE_ENUM_OOO_4 = -1000000;
+ SPARSE_ENUM_OOO_6 = 100000;
+ SPARSE_ENUM_OOO_MIN_INT = -2147483648;
+ SPARSE_ENUM_OOO_7 = 1000000;
+ SPARSE_ENUM_OOO_5 = -100000;
+ SPARSE_ENUM_OOO_8 = 10000000;
+ SPARSE_ENUM_OOO_3 = -10000000;
+ SPARSE_ENUM_OOO_10 = 1000000000;
+ SPARSE_ENUM_OOO_2 = -100000000;
+ SPARSE_ENUM_OOO_9 = 100000000;
+}
+
+message MessageWithEnums {
+ repeated DenseEnum denseEnums = 1;
+ repeated DenseEnumOutOfOrder denseOutOfOrderEnums = 2;
+ repeated SparseEnum sparseEnums = 3;
+ repeated SparseEnumOutOfOrder sparseOutOfOrderEnums = 4;
+}