mirror of
https://github.com/block/buzz.git
synced 2026-11-14 13:21:02 +00:00
perf(mobile): merge only enough small verified-name batches to fill one
At the four-partial cap, retire the smallest partial batches only until the new subjects fill a batch (at least one when they would leave a partial of their own), instead of re-reading every partial. Replace a batch whose subject was evicted and tracked again before the sync, so its check mark and any pruned cached head are restored. Co-authored-by: studio <dc2fc481343672df31a689840c09299972e704dac592e0da6f18486646a7ee3e@localhost> Signed-off-by: Tom Brow <tomb@squareup.com>
This commit is contained in:
@@ -21,13 +21,18 @@ const _discoveryRefresh = Duration(minutes: 5);
|
|||||||
/// tolerated, a flood before EOSE fails the batch instead of buffering it.
|
/// tolerated, a flood before EOSE fails the batch instead of buffering it.
|
||||||
const _maxEarlyPerSubject = 4;
|
const _maxEarlyPerSubject = 4;
|
||||||
|
|
||||||
/// Batches smaller than [verifiedProfileBatchSize] allowed before they are
|
/// Batches smaller than [verifiedProfileBatchSize] allowed before the smallest
|
||||||
/// merged into full ones.
|
/// are merged into the next new one until it fills a batch.
|
||||||
const _maxPartialBatches = 4;
|
const _maxPartialBatches = 4;
|
||||||
|
|
||||||
class _Batch {
|
class _Batch {
|
||||||
_Batch(this.subjects);
|
_Batch(this.subjects, this.interest);
|
||||||
final List<String> subjects;
|
final List<String> subjects;
|
||||||
|
|
||||||
|
/// The newest interest when the batch opened. A subject whose interest is
|
||||||
|
/// newer was evicted and tracked again since, which dropped its `_checked`
|
||||||
|
/// mark and maybe its cached head, so the batch must be read again.
|
||||||
|
final int interest;
|
||||||
final List<NostrEvent> early = [];
|
final List<NostrEvent> early = [];
|
||||||
|
|
||||||
/// Live events awaiting proof, newest per subject, so a fast stream cannot
|
/// Live events awaiting proof, newest per subject, so a fast stream cannot
|
||||||
@@ -284,14 +289,19 @@ class VerifiedProfilesNotifier extends Notifier<Map<String, VerifiedProfile>> {
|
|||||||
_publish();
|
_publish();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Keep every open batch whose subjects are all still tracked, so newly
|
// Keep every open batch whose subjects are all still tracked since it
|
||||||
// tracked people get a batch of their own instead of re-reading everyone
|
// opened, so newly tracked people get a batch of their own instead of
|
||||||
// already checked. A retried batch is replaced so a fresh snapshot can
|
// re-reading everyone already checked. A retried batch is replaced so a
|
||||||
// prove absence, and a batch that lost a subject to eviction is replaced
|
// fresh snapshot can prove absence, and a batch that lost a subject to
|
||||||
// too.
|
// eviction is replaced too, even if the subject was tracked again.
|
||||||
for (final id in _batches.keys.toList()) {
|
for (final id in _batches.keys.toList()) {
|
||||||
final batch = _batches[id]!;
|
final batch = _batches[id]!;
|
||||||
if (!batch.stale && batch.subjects.every(_wanted.containsKey)) continue;
|
if (!batch.stale &&
|
||||||
|
batch.subjects.every(
|
||||||
|
(subject) => (_wanted[subject] ?? 1 << 62) <= batch.interest,
|
||||||
|
)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
_close(id);
|
_close(id);
|
||||||
}
|
}
|
||||||
var uncovered = _uncoveredSubjects();
|
var uncovered = _uncoveredSubjects();
|
||||||
@@ -300,14 +310,28 @@ class VerifiedProfilesNotifier extends Notifier<Map<String, VerifiedProfile>> {
|
|||||||
_publish();
|
_publish();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Small additions each open a small batch. Past a few of those, merge
|
// Small additions each open a small batch. Past a few of those, merge the
|
||||||
// them into full ones, so the subscription count stays near the minimum.
|
// smallest into the new subjects until they fill a batch, so the
|
||||||
|
// subscription count stays near the minimum while re-reading as few
|
||||||
|
// checked subjects as possible.
|
||||||
final partial = [
|
final partial = [
|
||||||
for (final MapEntry(:key, :value) in _batches.entries)
|
for (final batch in _batches.entries)
|
||||||
if (value.subjects.length < verifiedProfileBatchSize) key,
|
if (batch.value.subjects.length < verifiedProfileBatchSize) batch,
|
||||||
];
|
];
|
||||||
if (partial.length >= _maxPartialBatches) {
|
if (partial.length >= _maxPartialBatches) {
|
||||||
partial.forEach(_close);
|
partial.sort(
|
||||||
|
(a, b) => a.value.subjects.length.compareTo(b.value.subjects.length),
|
||||||
|
);
|
||||||
|
// Retire at least one unless the new subjects split evenly, since
|
||||||
|
// they can add a partial batch of their own.
|
||||||
|
var size = uncovered.length;
|
||||||
|
var retired = size % verifiedProfileBatchSize == 0;
|
||||||
|
for (final MapEntry(:key, :value) in partial) {
|
||||||
|
if (retired && size >= verifiedProfileBatchSize) break;
|
||||||
|
size += value.subjects.length;
|
||||||
|
retired = true;
|
||||||
|
_close(key);
|
||||||
|
}
|
||||||
uncovered = _uncoveredSubjects();
|
uncovered = _uncoveredSubjects();
|
||||||
}
|
}
|
||||||
for (var i = 0; i < uncovered.length; i += verifiedProfileBatchSize) {
|
for (var i = 0; i < uncovered.length; i += verifiedProfileBatchSize) {
|
||||||
@@ -342,7 +366,7 @@ class VerifiedProfilesNotifier extends Notifier<Map<String, VerifiedProfile>> {
|
|||||||
VerifiedProfileAuthority authority,
|
VerifiedProfileAuthority authority,
|
||||||
int generation,
|
int generation,
|
||||||
) async {
|
) async {
|
||||||
final batch = _Batch(chunk);
|
final batch = _Batch(chunk, _interest);
|
||||||
_batches[id] = batch;
|
_batches[id] = batch;
|
||||||
void fail() {
|
void fail() {
|
||||||
if (!batch.active) return;
|
if (!batch.active) return;
|
||||||
|
|||||||
@@ -226,23 +226,41 @@ void main() {
|
|||||||
expect(h.profiles[subjectB]?.checked, isTrue);
|
expect(h.profiles[subjectB]?.checked, isTrue);
|
||||||
});
|
});
|
||||||
|
|
||||||
journey('small batches merge into one past four', (h) {
|
journey('past four small batches, the smallest merge until one is full', (h) {
|
||||||
String key(int i) => i.toRadixString(16).padLeft(64, '0');
|
String key(int i) => i.toRadixString(16).padLeft(64, '0');
|
||||||
h.start([key(0)]);
|
|
||||||
final notifier = h.container.read(verifiedProfilesProvider.notifier);
|
final notifier = h.container.read(verifiedProfilesProvider.notifier);
|
||||||
|
h.start([for (var i = 0; i < 64; i++) key(i)]);
|
||||||
|
for (var group = 1; group < 4; group++) {
|
||||||
|
notifier.track([for (var i = 0; i < 64; i++) key(group * 64 + i)]);
|
||||||
|
h.settle();
|
||||||
|
}
|
||||||
|
final before = h.dFilters.length;
|
||||||
|
expect(before, 4, reason: 'four partial batches of 64');
|
||||||
|
notifier.track([key(256)]);
|
||||||
|
h.settle();
|
||||||
|
// Two of the 64s fill one batch with the newcomer; the other two stay.
|
||||||
|
final reads = [
|
||||||
|
for (final f in h.dFilters.skip(before)) (f['#d'] as List).cast<String>(),
|
||||||
|
];
|
||||||
|
expect([for (final r in reads) r.length], [128, 1]);
|
||||||
|
expect(reads.expand((r) => r), contains(key(256)));
|
||||||
|
});
|
||||||
|
|
||||||
|
journey('a large addition past four small batches still retires one', (h) {
|
||||||
|
String key(int i) => i.toRadixString(16).padLeft(64, '0');
|
||||||
|
final notifier = h.container.read(verifiedProfilesProvider.notifier);
|
||||||
|
h.start([key(0)]);
|
||||||
for (var i = 1; i < 4; i++) {
|
for (var i = 1; i < 4; i++) {
|
||||||
notifier.track([key(i)]);
|
notifier.track([key(i)]);
|
||||||
h.settle();
|
h.settle();
|
||||||
}
|
}
|
||||||
final before = h.dFilters.length;
|
final before = h.dFilters.length;
|
||||||
expect(before, 4);
|
notifier.track([for (var i = 4; i < 133; i++) key(i)]);
|
||||||
notifier.track([key(4)]);
|
|
||||||
h.settle();
|
h.settle();
|
||||||
|
// 129 newcomers would leave a fifth partial; one single folds in.
|
||||||
expect(
|
expect(
|
||||||
[for (final f in h.dFilters.skip(before)) f['#d']],
|
[for (final f in h.dFilters.skip(before)) (f['#d'] as List).length],
|
||||||
[
|
[128, 2],
|
||||||
[for (var i = 0; i < 5; i++) key(i)],
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -261,6 +279,44 @@ void main() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
journey('a subject evicted and tracked again before the sync is re-read', (
|
||||||
|
h,
|
||||||
|
) {
|
||||||
|
String key(int i) => i.toRadixString(16).padLeft(64, '0');
|
||||||
|
h.relay.seed([signedProjection(profileTags(subjectA))]);
|
||||||
|
h.start([subjectA]);
|
||||||
|
final notifier = h.container.read(verifiedProfilesProvider.notifier);
|
||||||
|
notifier.track([for (var i = 0; i < 1023; i++) key(i)]);
|
||||||
|
h.settle();
|
||||||
|
expect(h.profiles[subjectA]?.checked, isTrue);
|
||||||
|
// A is evicted, then tracked again, which evicts key(0) in another batch.
|
||||||
|
notifier
|
||||||
|
..track([key(1023)])
|
||||||
|
..track([subjectA]);
|
||||||
|
h.settle();
|
||||||
|
expect(h.profiles[subjectA]?.checked, isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
journey('a re-tracked subject keeps its name when the head cache trims', (h) {
|
||||||
|
String key(int i) => i.toRadixString(16).padLeft(64, '0');
|
||||||
|
h.relay.seed([signedProjection(profileTags(subjectA))]);
|
||||||
|
h.start([subjectA]);
|
||||||
|
final notifier = h.container.read(verifiedProfilesProvider.notifier);
|
||||||
|
// Each round evicts A, may trim its cached head, then tracks it again
|
||||||
|
// before the sync; past 4,096 cached heads only a re-read restores it.
|
||||||
|
for (var round = 0; round < 6; round++) {
|
||||||
|
final subjects = [for (var i = 0; i < 1023; i++) key(round * 1023 + i)];
|
||||||
|
h.relay.seed([
|
||||||
|
for (final s in subjects) signedProjection(profileTags(s)),
|
||||||
|
]);
|
||||||
|
notifier
|
||||||
|
..track(subjects)
|
||||||
|
..track([subjectA]);
|
||||||
|
h.settle();
|
||||||
|
expect(h.profiles[subjectA]?.displayName, 'Ada Lovelace');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
journey('a large roster keeps the 1,024 most recently tracked subjects', (h) {
|
journey('a large roster keeps the 1,024 most recently tracked subjects', (h) {
|
||||||
String key(int i) => i.toRadixString(16).padLeft(64, '0');
|
String key(int i) => i.toRadixString(16).padLeft(64, '0');
|
||||||
h.start([for (var i = 0; i < 10; i++) key(i)]);
|
h.start([for (var i = 0; i < 10; i++) key(i)]);
|
||||||
|
|||||||
Reference in New Issue
Block a user