Enable hashCode memoization for frozen protos. (#355)
diff --git a/protobuf/CHANGELOG.md b/protobuf/CHANGELOG.md
index b2c0c07..f9c1bf9 100644
--- a/protobuf/CHANGELOG.md
+++ b/protobuf/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 1.0.3
+
+* Enable hashCode memoization for frozen protos.
+
## 1.0.2
* Fix hashcode of bytes fields.
diff --git a/protobuf/lib/src/protobuf/field_set.dart b/protobuf/lib/src/protobuf/field_set.dart
index 4fd47c2..cb4b376 100644
--- a/protobuf/lib/src/protobuf/field_set.dart
+++ b/protobuf/lib/src/protobuf/field_set.dart
@@ -23,8 +23,23 @@
///
/// If the handler returns normally, the modification is allowed, and execution
/// proceeds as if the message was writable.
-FrozenMessageErrorHandler frozenMessageModificationHandler =
+FrozenMessageErrorHandler _frozenMessageModificationHandler =
defaultFrozenMessageModificationHandler;
+FrozenMessageErrorHandler get frozenMessageModificationHandler =>
+ _frozenMessageModificationHandler;
+set frozenMessageModificationHandler(FrozenMessageErrorHandler value) {
+ _hashCodesCanBeMemoized = false;
+ _frozenMessageModificationHandler = value;
+}
+
+/// Indicator for whether the FieldSet hashCodes can be memoized.
+///
+/// HashCode memoization relies on the [defaultFrozenMessageModificationHandler]
+/// behavior--that is, after freezing, field set values can't ever be changed.
+/// This keeps track of whether an application has ever modified the
+/// [FrozenMessageErrorHandler] used, not allowing hashCodes to be memoized if
+/// it ever changed.
+bool _hashCodesCanBeMemoized = true;
/// All the data in a GeneratedMessage.
///
@@ -35,7 +50,6 @@
final GeneratedMessage _message;
final BuilderInfo _meta;
final EventPlugin _eventPlugin;
- bool _isReadOnly = false;
/// The value of each non-extension field in a fixed-length array.
/// The index of a field can be found in [FieldInfo.index].
@@ -48,6 +62,35 @@
/// Contains all the unknown fields, or null if there aren't any.
UnknownFieldSet _unknownFields;
+ /// Encodes whether `this` has been frozen, and if so, also memoizes the
+ /// hash code.
+ ///
+ /// Will always be a `bool` or `int`.
+ ///
+ /// If the message is mutable: `false`
+ /// If the message is frozen and no hash code has been computed: `true`
+ /// If the message is frozen and a hash code has been computed: the hash
+ /// code as an `int`.
+ Object _frozenState = false;
+
+ /// Returns the value of [_frozenState] as if it were a boolean indicator
+ /// for whether `this` is read-only (has been frozen).
+ ///
+ /// If the value is not a `bool`, then it must contain the memoized hash code
+ /// value, in which case the proto must be read-only.
+ bool get _isReadOnly => _frozenState is bool ? _frozenState : true;
+
+ /// Returns the value of [_frozenState] if it contains the pre-computed value
+ /// of the hashCode for the frozen field sets.
+ ///
+ /// Computing the hashCode of a proto object can be very expensive for large
+ /// protos. Frozen protos don't allow any mutations, which means the contents
+ /// of the field set should be stable.
+ ///
+ /// If [_frozenState] contains a boolean, the hashCode hasn't been memoized,
+ /// so it will return null.
+ int get _memoizedHashCode => _frozenState is int ? _frozenState : null;
+
// Maps a oneof decl index to the tag number which is currently set. If the
// index is not present, the oneof field is unset.
final Map<int, int> _oneofCases;
@@ -122,7 +165,7 @@
void _markReadOnly() {
if (_isReadOnly) return;
- _isReadOnly = true;
+ _frozenState = true;
for (var field in _meta.sortedByTag) {
if (field.isRepeated) {
final entries = _values[field.index];
@@ -592,7 +635,14 @@
///
/// The hash may change when any field changes (recursively).
/// Therefore, protobufs used as map keys shouldn't be changed.
+ ///
+ /// If the protobuf contents have been frozen, and the
+ /// [FrozenMessageErrorHandler] has not been changed from the default
+ /// behavior, the hashCode can be memoized to speed up performance.
int get _hashCode {
+ if (_hashCodesCanBeMemoized && _memoizedHashCode != null) {
+ return _memoizedHashCode;
+ }
// Hashes the value of one field (recursively).
int hashField(int hash, FieldInfo fi, value) {
if (value is List && value.isEmpty) {
@@ -640,6 +690,10 @@
if (_hasUnknownFields) {
hash = _HashUtils._combine(hash, _unknownFields.hashCode);
}
+
+ if (_isReadOnly && _hashCodesCanBeMemoized) {
+ _frozenState = hash;
+ }
return hash;
}
diff --git a/protobuf/pubspec.yaml b/protobuf/pubspec.yaml
index c5517d4..288f0a5 100644
--- a/protobuf/pubspec.yaml
+++ b/protobuf/pubspec.yaml
@@ -1,5 +1,5 @@
name: protobuf
-version: 1.0.2
+version: 1.0.3
description: >
Runtime library for protocol buffers support.
Use https://pub.dartlang.org/packages/protoc_plugin to generate dart code for your '.proto' files.
diff --git a/protobuf/test/readonly_message_test.dart b/protobuf/test/readonly_message_test.dart
index 81cdb42..b6fab67 100644
--- a/protobuf/test/readonly_message_test.dart
+++ b/protobuf/test/readonly_message_test.dart
@@ -92,6 +92,22 @@
}
});
+ test('eagerly computes hashCode if a custom read-only handler is used', () {
+ try {
+ final message = Rec.getDefault();
+ final initialHashCode = message.hashCode;
+
+ frozenMessageModificationHandler =
+ (String messageName, [String methodName]) {};
+ message.setField(1, 456);
+ final modifiedHashCode = message.hashCode;
+ expect(initialHashCode == modifiedHashCode, isFalse);
+ } finally {
+ frozenMessageModificationHandler =
+ defaultFrozenMessageModificationHandler;
+ }
+ });
+
test("can't clear a read-only message", () {
expect(
() => Rec.getDefault().clear(),