perf: Compare byte values directly and parse only UUID-shaped values
projectionValuesEqual took a Uint8List (how a bytea column arrives) down its List branch and compared it byte by byte, and tryUuidValue parsed every value as a UUID by throwing and catching an error when it was none. Each byte of a payload thus cost a thrown TypeError, about 18 µs: two payloads of 2 KB took 36 ms to compare. A sync whose projection loaded a few thousand rows with payloads kept the server's isolate busy for minutes, during which it answered no other request and the client's method stream timed out, so the client sent the same changes again. Byte values are now compared as bytes, and tryUuidValue only parses what can hold a UUID (a UuidValue, 16 bytes, a 36-character string), returning null for anything else without throwing. A UUID compares with its string and its bytes as before. Longer byte values are no longer read as the UUID of their first 16 bytes, which made canonicalProjectionValue group payloads with the same beginning together. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
This commit is contained in:
parent
de3498aa20
commit
7eea8c66b0
2 changed files with 113 additions and 0 deletions
|
|
@ -1,3 +1,5 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:serverpod_database/serverpod_database.dart' show ColumnType;
|
||||
import 'package:serverpod_serialization/serverpod_serialization.dart';
|
||||
|
|
@ -349,6 +351,9 @@ String canonicalProjectionValue(Object? value) {
|
|||
bool projectionValuesEqual(Object? left, Object? right) {
|
||||
if (left == null || right == null) return left == right;
|
||||
if (left is String && right is String) return left == right;
|
||||
// Byte columns (`bytea`) arrive as Uint8List, which is a List: compared
|
||||
// element by element below, every byte would go through [tryUuidValue].
|
||||
if (left is Uint8List && right is Uint8List) return _bytesEqual(left, right);
|
||||
if (left is Map && right is Map) {
|
||||
return left.length == right.length &&
|
||||
left.entries.every(
|
||||
|
|
@ -373,11 +378,29 @@ bool projectionValuesEqual(Object? left, Object? right) {
|
|||
}
|
||||
|
||||
/// Parses [value] as a UUID when it is one, otherwise null.
|
||||
///
|
||||
/// Only what can hold a UUID is parsed: a [UuidValue], its 16 bytes, or a
|
||||
/// string as long as one written with dashes. Anything else is no UUID
|
||||
/// without trying, as this runs for every value compared and a thrown and
|
||||
/// caught error costs microseconds each.
|
||||
@internal
|
||||
UuidValue? tryUuidValue(Object? value) {
|
||||
final mayBeUuid = switch (value) {
|
||||
UuidValue() || Uint8List(length: 16) || String(length: 36) => true,
|
||||
_ => false,
|
||||
};
|
||||
if (!mayBeUuid) return null;
|
||||
try {
|
||||
return value.toUuidValue();
|
||||
} on Object {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
bool _bytesEqual(Uint8List left, Uint8List right) {
|
||||
if (left.length != right.length) return false;
|
||||
for (var index = 0; index < left.length; index++) {
|
||||
if (left[index] != right[index]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,90 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:serverpod_offline_sync/src/database/merge_utils/unique_resolver.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
void main() {
|
||||
group('Given byte values such as a bytea column holds,', () {
|
||||
final bytes = Uint8List.fromList(List.generate(4096, (i) => i % 251));
|
||||
|
||||
test(
|
||||
'when comparing equal and differing bytes, '
|
||||
'then they compare by content.',
|
||||
() {
|
||||
final same = Uint8List.fromList(bytes);
|
||||
final changed = Uint8List.fromList(bytes)..[4000] ^= 1;
|
||||
final shorter = Uint8List.sublistView(bytes, 0, 4095);
|
||||
|
||||
expect(projectionValuesEqual(bytes, same), isTrue);
|
||||
expect(projectionValuesEqual(bytes, changed), isFalse);
|
||||
expect(projectionValuesEqual(bytes, shorter), isFalse);
|
||||
expect(projectionValuesEqual(bytes, null), isFalse);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when comparing payloads alike in their first 16 bytes, '
|
||||
'then they are not taken for the same UUID.',
|
||||
() {
|
||||
final payload = Uint8List.fromList('{"schemaVersion":1,"a":1}'.codeUnits);
|
||||
final other = Uint8List.fromList('{"schemaVersion":1,"a":2}'.codeUnits);
|
||||
|
||||
expect(tryUuidValue(payload), isNull);
|
||||
expect(
|
||||
canonicalProjectionValue(payload),
|
||||
isNot(canonicalProjectionValue(other)),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when comparing many payloads of a few kilobytes, '
|
||||
'then it takes no more than a moment.',
|
||||
() {
|
||||
final same = Uint8List.fromList(bytes);
|
||||
final watch = Stopwatch()..start();
|
||||
for (var i = 0; i < 1000; i++) {
|
||||
projectionValuesEqual(bytes, same);
|
||||
}
|
||||
|
||||
// Byte by byte through a failing UUID parse, this took minutes.
|
||||
expect(watch.elapsed, lessThan(const Duration(seconds: 2)));
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
group('Given UUIDs in their forms,', () {
|
||||
final uuid = const Uuid().v7obj();
|
||||
|
||||
test(
|
||||
'when comparing a UUID with its string and its bytes, '
|
||||
'then they are the same.',
|
||||
() {
|
||||
expect(tryUuidValue(uuid.uuid), uuid);
|
||||
expect(tryUuidValue(Uint8List.fromList(uuid.toBytes())), uuid);
|
||||
expect(projectionValuesEqual(uuid, uuid.uuid), isTrue);
|
||||
expect(projectionValuesEqual(uuid.uuid, uuid), isTrue);
|
||||
expect(
|
||||
projectionValuesEqual(Uint8List.fromList(uuid.toBytes()), uuid),
|
||||
isTrue,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when comparing what is no UUID, '
|
||||
'then it is none and compares as itself.',
|
||||
() {
|
||||
expect(tryUuidValue('not a uuid'), isNull);
|
||||
expect(tryUuidValue('x' * 36), isNull);
|
||||
expect(tryUuidValue(42), isNull);
|
||||
expect(tryUuidValue(DateTime.utc(2026)), isNull);
|
||||
expect(projectionValuesEqual(42, 42), isTrue);
|
||||
expect(projectionValuesEqual(42, 43), isFalse);
|
||||
expect(projectionValuesEqual([1, 'a'], [1, 'a']), isTrue);
|
||||
expect(projectionValuesEqual({'a': 1}, {'a': 2}), isFalse);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
Loading…
Reference in a new issue