perf(mobile): isolate native signing and measure Dart comparison

Signed-off-by: Tom Brow <tomb@block.xyz>
This commit is contained in:
Tom Brow
2026-10-20 10:20:19 -07:00
parent 05ceb11e1b
commit 58f54a20b9
5 changed files with 102 additions and 8 deletions

View File

@@ -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

View File

@@ -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,

View 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;
}
}

View File

@@ -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');

View File

@@ -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
}