mirror of
https://github.com/block/buzz.git
synced 2026-11-06 09:21:45 +00:00
perf(mobile): memoize npub labels (#9137)
Opening a big channel on mobile stalls while member labels resolve. On
an iPhone 12 mini (wired Profile build of main 120d75d976), opening a
5,892-member channel had frames of 217, 113 and 101 ms. The channel
identity scope recomputed 13 times during the open as profiles arrived,
25 to 35 ms each: 235 ms of 1,755 ms UI CPU, 190 ms of it bech32
encoding. Every recompute labels each member without a profile name by
npub (`missingOwnerProfiles` → `IdentityNameSources.factFor` →
`shortPubkey` → `fullNpub`), and encoded the same keys again. The same
chain cost 152 ms in a 15 s resume with the channel open.
`fullNpub` now memoizes encodings by hex key, bounded at 8,192 entries.
Once full it keeps what it has, so a roster larger than the limit still
reuses most of its encodings. Labels are unchanged.
### Validation
- Added tests: `fullNpub reuses the encoding of a key it has already
encoded` fails without the memo; `a roster larger than the cache still
reuses its encodings` fails if the cache clears when full.
- Host benchmark, `shortPubkey` over 5,900 random keys: 53.5 ms on the
first pass, about 3.1 ms on later passes.
- Full mobile suite passes except the 10 known notification/push
failures and `push_lease_revocation_outbox_test`, which also fail on
main. Analyze clean.
- Automated simulator run of this branch (debug build, Builderlab
community): searched "studio", opened a channel result and came back;
people results show the same short npub labels as main (for example
`npub194z...aq3k`).
- No post-change device capture. The scope still recomputes as profiles
arrive; this removes the encoding cost from each recompute.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Signed-off-by: Tom Brow <tomb@squareup.com>
Co-authored-by: Tom Brow <tomb@squareup.com>
Co-authored-by: studio <dc2fc481343672df31a689840c09299972e704dac592e0da6f18486646a7ee3e@buzz.block.builderlab.xyz>
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import 'package:flutter/foundation.dart' show visibleForTesting;
|
||||
import 'package:nostr/nostr.dart' as nostr;
|
||||
|
||||
/// Neutral label rendered when an identity string cannot be read as a Nostr
|
||||
@@ -49,8 +50,24 @@ String? fullNpub(String identity) {
|
||||
}
|
||||
}
|
||||
|
||||
String _encodeNpub(String hexPubkey) =>
|
||||
nostr.Nip19.encode(prefix: nostr.Nip19Prefix.npub, data: hexPubkey);
|
||||
/// Encoded npubs by lowercase hex key. Identity views label every roster
|
||||
/// member without a profile name by npub on each rebuild, and bech32 encoding
|
||||
/// dominated those rebuilds on large channels. Bounded: once full it keeps
|
||||
/// what it has, so a roster larger than the limit still reuses most keys.
|
||||
final _npubs = <String, String>{};
|
||||
@visibleForTesting
|
||||
const npubCacheLimit = 8192;
|
||||
|
||||
String _encodeNpub(String hexPubkey) {
|
||||
final cached = _npubs[hexPubkey];
|
||||
if (cached != null) return cached;
|
||||
final npub = nostr.Nip19.encode(
|
||||
prefix: nostr.Nip19Prefix.npub,
|
||||
data: hexPubkey,
|
||||
);
|
||||
if (_npubs.length < npubCacheLimit) _npubs[hexPubkey] = npub;
|
||||
return npub;
|
||||
}
|
||||
|
||||
/// The canonical compact public-key label: the first 8 and last 4 characters
|
||||
/// of the full npub joined by an ellipsis — the same truncation desktop uses
|
||||
|
||||
@@ -16,6 +16,23 @@ void main() {
|
||||
const canonicalCompact = 'npub180c\u2026h6w6';
|
||||
|
||||
group('fullNpub', () {
|
||||
test('reuses the encoding of a key it has already encoded', () {
|
||||
// Identity views relabel a large roster on every rebuild; each key's
|
||||
// bech32 encoding must happen once, not once per rebuild.
|
||||
final first = fullNpub(canonicalHex);
|
||||
expect(first, canonicalNpub);
|
||||
expect(identical(fullNpub(canonicalHex.toUpperCase()), first), isTrue);
|
||||
});
|
||||
|
||||
test('a roster larger than the cache still reuses its encodings', () {
|
||||
String key(int i) => i.toRadixString(16).padLeft(64, '0');
|
||||
final firstPass = [
|
||||
for (var i = 0; i <= npubCacheLimit; i++) fullNpub(key(i)),
|
||||
];
|
||||
// A second scan in the same order finds the early keys still cached.
|
||||
expect(identical(fullNpub(key(1)), firstPass[1]), isTrue);
|
||||
});
|
||||
|
||||
test('canonicalizes every accepted input wrapper', () {
|
||||
// Every accepted wrapper — lowercase hex, uppercase hex, whitespace-
|
||||
// padded, and an already-npub key — converges on the canonical npub.
|
||||
|
||||
Reference in New Issue
Block a user