mirror of
https://github.com/block/buzz.git
synced 2026-10-27 12:40:30 +00:00
perf(mobile): isolate native signing and measure Dart comparison
Signed-off-by: Tom Brow <tomb@block.xyz>
This commit is contained in:
@@ -83,8 +83,8 @@ runner. The internal release toolchain is configured separately in
|
||||
|
||||
Use `shared/crypto/event_signer.dart` for signed events. iOS calls the existing
|
||||
CSecp256k1 library through synchronous FFI; other platforms retain the Dart
|
||||
signer. Burst-producing media authentication retains its bounded worker path
|
||||
because Debug Simulator bursts still exceed a frame budget. Event hashing remains in `nostr.Event.unsigned`. The native context is
|
||||
signer. This change preserves existing call scheduling; worker offloading is a
|
||||
separate change. Event hashing remains in `nostr.Event.unsigned`. The native context is
|
||||
randomized once and shared read-only; each call creates, self-verifies with,
|
||||
and destroys its own keypair. It never caches account secrets.
|
||||
|
||||
@@ -94,6 +94,12 @@ cross-implementation tests additionally accept `BUZZ_SIGNER_LIBRARY` pointing
|
||||
to a locally compiled adapter library. `tool/benchmark_event_signer.dart`
|
||||
measures complete host calls; `tool/native_signing_probe.dart` is an explicit
|
||||
Simulator entry point for complete event, media-auth, and NIP-98 measurements.
|
||||
`tool/compare_event_signers.dart` compares the existing Dart signer (with a
|
||||
cached public key) and native signing in the same iOS process, on the UI isolate,
|
||||
with identical inputs and alternating order. Seven warm samples on a Debug
|
||||
iPhone 17 Pro Simulator measured median 64-event bursts of 209.7 ms (Dart) and
|
||||
50.2 ms (native), a 76% reduction. These are synchronous event-construction
|
||||
timings, not UIKit main-thread CPU measurements or rendered-frame timings.
|
||||
Neither benchmark represents physical-device release performance.
|
||||
|
||||
## Run
|
||||
|
||||
@@ -7,11 +7,13 @@ import 'package:nostr/nostr.dart' as nostr;
|
||||
/// Signs canonical Nostr events with libsecp256k1 on iOS.
|
||||
///
|
||||
/// Native keypairs exist only for this call and are wiped before returning.
|
||||
/// Other platforms retain the existing Dart implementation.
|
||||
/// Other platforms retain the existing Dart implementation, including the
|
||||
/// optional cached [pubkey]. iOS derives its public key from the native keypair.
|
||||
nostr.Event signEvent({
|
||||
required int kind,
|
||||
required String content,
|
||||
required String secretKey,
|
||||
String? pubkey,
|
||||
int? createdAt,
|
||||
List<List<String>> tags = const [],
|
||||
bool verify = false,
|
||||
@@ -21,6 +23,7 @@ nostr.Event signEvent({
|
||||
kind: kind,
|
||||
content: content,
|
||||
secretKey: secretKey,
|
||||
pubkey: pubkey,
|
||||
createdAt: createdAt,
|
||||
tags: tags,
|
||||
verify: verify,
|
||||
|
||||
87
mobile/tool/compare_event_signers.dart
Normal file
87
mobile/tool/compare_event_signers.dart
Normal file
@@ -0,0 +1,87 @@
|
||||
import 'dart:io';
|
||||
import 'package:buzz/shared/crypto/event_signer.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:nostr/nostr.dart' as nostr;
|
||||
|
||||
// Run on iOS with the same event construction on the UI isolate for both paths.
|
||||
void main() {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
try {
|
||||
if (!Platform.isIOS) throw StateError('Requires iOS');
|
||||
final results = <String>[];
|
||||
final key = '0' * 63 + '3';
|
||||
final publicKey = nostr.Keys(key).public;
|
||||
nostr.Event create(bool native) {
|
||||
final tags = [
|
||||
['t', 'get'],
|
||||
['expiration', '1800000060'],
|
||||
];
|
||||
return native
|
||||
? signEvent(
|
||||
kind: 24242,
|
||||
content: 'Get buzz-media',
|
||||
tags: tags,
|
||||
createdAt: 1800000000,
|
||||
secretKey: key,
|
||||
pubkey: publicKey,
|
||||
)
|
||||
: nostr.Event.from(
|
||||
kind: 24242,
|
||||
content: 'Get buzz-media',
|
||||
tags: tags,
|
||||
createdAt: 1800000000,
|
||||
secretKey: key,
|
||||
pubkey: publicKey,
|
||||
);
|
||||
}
|
||||
|
||||
for (final native in [false, true]) {
|
||||
final watch = Stopwatch()..start();
|
||||
final event = create(native);
|
||||
watch.stop();
|
||||
results.add(
|
||||
'first measured ${native ? "native" : "Dart"}: ${watch.elapsedMicroseconds} us',
|
||||
);
|
||||
if (!event.isValid()) throw StateError('Invalid signature');
|
||||
}
|
||||
if (create(false).id != create(true).id) {
|
||||
throw StateError('Different events');
|
||||
}
|
||||
for (var warm = 0; warm < 10; warm++) {
|
||||
create(false);
|
||||
create(true);
|
||||
}
|
||||
for (final count in [1, 8, 32, 64]) {
|
||||
final samples = {false: <int>[], true: <int>[]};
|
||||
for (var round = 0; round < 7; round++) {
|
||||
for (final native in round.isEven ? [false, true] : [true, false]) {
|
||||
final watch = Stopwatch()..start();
|
||||
for (var i = 0; i < count; i++) {
|
||||
create(native);
|
||||
}
|
||||
watch.stop();
|
||||
samples[native]!.add(watch.elapsedMicroseconds);
|
||||
}
|
||||
}
|
||||
for (final native in [false, true]) {
|
||||
final values = samples[native]!..sort();
|
||||
results.add(
|
||||
'burst $count ${native ? "native" : "Dart"}: '
|
||||
'median ${values[3]} min ${values.first} max ${values.last} us',
|
||||
);
|
||||
}
|
||||
}
|
||||
for (final result in results) {
|
||||
debugPrint('SIGNING_PROBE $result');
|
||||
}
|
||||
debugPrint('SIGNING_PROBE COMPLETE');
|
||||
runApp(
|
||||
MaterialApp(
|
||||
home: Scaffold(body: SafeArea(child: Text(results.join('\n')))),
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
debugPrint('SIGNING_PROBE FAILED: $error');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
@@ -61,7 +61,7 @@ Future<void> runProbe() async {
|
||||
final watch = Stopwatch()..start();
|
||||
final proofs = <Map<String, String>>[];
|
||||
for (var i = 0; i < 64; i++) {
|
||||
proofs.add(await media.headersForAsync('https://relay.example/media/$i'));
|
||||
proofs.add(media.headersFor('https://relay.example/media/$i'));
|
||||
}
|
||||
results.add('64 complete media headers: ${watch.elapsedMicroseconds} us');
|
||||
for (final proof in proofs) {
|
||||
@@ -69,9 +69,7 @@ Future<void> runProbe() async {
|
||||
final event = nostr.Event.fromJson(utf8.decode(base64Decode(encoded)));
|
||||
if (!event.isValid()) throw StateError('Invalid media proof');
|
||||
}
|
||||
if ((await media.headersForAsync(
|
||||
'https://foreign.example/media/x',
|
||||
)).isNotEmpty) {
|
||||
if ((media.headersFor('https://foreign.example/media/x')).isNotEmpty) {
|
||||
throw StateError('Foreign host received credentials');
|
||||
}
|
||||
results.add('media signatures and foreign-host isolation: PASS');
|
||||
|
||||
@@ -11,7 +11,7 @@ xcodebuild -workspace mobile/ios/Runner.xcworkspace -scheme Runner \
|
||||
-configuration Debug -sdk iphonesimulator -destination "id=$device" \
|
||||
-derivedDataPath "${BUZZ_SIGNING_DERIVED_DATA:-$fixture/build}" -jobs 2 \
|
||||
BUNDLE_IDENTIFIER="$bundle" CODE_SIGNING_ALLOWED=NO ONLY_ACTIVE_ARCH=YES \
|
||||
FLUTTER_TARGET=tool/native_signing_probe.dart build > "$fixture/build.log" 2>&1 || {
|
||||
FLUTTER_TARGET="${BUZZ_SIGNING_PROBE_TARGET:-tool/native_signing_probe.dart}" build > "$fixture/build.log" 2>&1 || {
|
||||
cat "$fixture/build.log"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user