tool/open_bench.dart times, on the VM or compiled to JavaScript, the opening in memory of a fixture of each kind, time_only, time_and_key with its .dkk in formats 1, 2 and 3, and format 3 with its file, each with the verification of its release, and the throughput of a capsule opened from a source in streaming, which test/large_capsule.dart makes from the fixtures: 64 MiB on the VM, 16 MiB on Node.js. Medians of three runs on the VM: 48 to 73 ms for a fixture, 1.50 s for 64 MiB in format 1 and 2.64 s for 64 MiB in one file of format 3, whose SHA-256 adds 17 ms per MiB; on Node.js about 0.8 s for a fixture, 0.73 s for 16 MiB in format 1 and 0.92 s in format 3. It never passes a sink as `c ? null : sink`: dart2js of Dart 3.13 loses the writes to an object that reaches a field that way, and its counter of bytes read 0 afterwards. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>v0.11
parent
f03a0acf1c
commit
cd40a682b5
@ -0,0 +1,192 @@
|
||||
// Times the opening of datekeys-dart, stage 4c, on the VM and compiled to
|
||||
// JavaScript: the opening in memory of a fixture of each kind (time_only,
|
||||
// time_and_key with a .dkk, and format 3 with its files), each with the
|
||||
// verification of its release, and the throughput of a capsule of 64 MiB
|
||||
// opened from a source in streaming, in format 1 and in format 3, which
|
||||
// large_capsule.dart makes from the fixtures. It reads no file: the
|
||||
// fixtures are those of test/vectors/open_vectors.g.dart.
|
||||
//
|
||||
// dart run tool/open_bench.dart
|
||||
//
|
||||
// dart compile js -O2 -o open_bench.js tool/open_bench.dart
|
||||
// node -e "globalThis.self = globalThis; require('./open_bench.js')"
|
||||
//
|
||||
// ignore_for_file: avoid_print
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:datekeys/datekeys.dart';
|
||||
|
||||
import '../test/large_capsule.dart';
|
||||
import '../test/vectors/open_vectors.g.dart';
|
||||
|
||||
typedef Json = Map<String, Object?>;
|
||||
|
||||
final Json _fixtures = jsonDecode(openFixturesJson) as Json;
|
||||
final Json _records = jsonDecode(openRecordsJson) as Json;
|
||||
|
||||
Uint8List fixture(String file) => fromHex(_fixtures[file]! as String);
|
||||
|
||||
Json record(String file) => _records[file]! as Json;
|
||||
|
||||
Release releaseOf(Json r) {
|
||||
final rel = r['release']! as Json;
|
||||
return Release(rel['round']! as int, fromHex(rel['signature']! as String));
|
||||
}
|
||||
|
||||
/// A sink of bytes that counts them and keeps nothing.
|
||||
final class Discard implements ByteSink {
|
||||
int n = 0;
|
||||
|
||||
@override
|
||||
void add(Uint8List bytes) => n += bytes.length;
|
||||
|
||||
@override
|
||||
void close() {}
|
||||
|
||||
@override
|
||||
void abort(Object reason) {}
|
||||
}
|
||||
|
||||
/// A sink of files that counts their bytes and keeps nothing.
|
||||
final class DiscardFiles implements FileSink {
|
||||
int n = 0;
|
||||
|
||||
@override
|
||||
void begin(Head head) {}
|
||||
|
||||
@override
|
||||
ByteSink create(int i) => _Count(this);
|
||||
|
||||
@override
|
||||
void commit() {}
|
||||
|
||||
@override
|
||||
void abort(Object reason) {}
|
||||
}
|
||||
|
||||
final class _Count implements ByteSink {
|
||||
_Count(this.s);
|
||||
final DiscardFiles s;
|
||||
|
||||
@override
|
||||
void add(Uint8List bytes) => s.n += bytes.length;
|
||||
|
||||
@override
|
||||
void close() {}
|
||||
|
||||
@override
|
||||
void abort(Object reason) {}
|
||||
}
|
||||
|
||||
OpenOptions optionsOf(
|
||||
Json r, {
|
||||
ByteSink? output,
|
||||
FileSink? sink,
|
||||
Uint8List? dkk,
|
||||
}) => OpenOptions(
|
||||
source: suppliedRelease(releaseOf(r)),
|
||||
now: () => parseRfc3339(r['unlock_at']! as String),
|
||||
accessKeyFile: dkk,
|
||||
output: output,
|
||||
sink: sink,
|
||||
);
|
||||
|
||||
double median(List<double> xs) {
|
||||
final s = [...xs]..sort();
|
||||
return s[s.length ~/ 2];
|
||||
}
|
||||
|
||||
Future<void> main() async {
|
||||
final js = identical(1, 1.0);
|
||||
final runs = js ? 5 : 15;
|
||||
print(
|
||||
'datekeys-dart, the opening, ${js ? 'compiled to JavaScript' : 'on the VM'}',
|
||||
);
|
||||
|
||||
// The verification of a release is kept for the next opening of the same
|
||||
// round: a release of another round between two openings makes each pay
|
||||
// for its own pairing, as an application that opens one capsule does.
|
||||
final other = record('time_only_extensions.dkc');
|
||||
for (final (label, file, withDkk) in [
|
||||
('time_only, format 1', 'time_only_extensions.dkc', false),
|
||||
('time_and_key with a .dkk, format 1', 'time_and_key_portable.dkc', true),
|
||||
(
|
||||
'time_and_key with a .dkk, format 2',
|
||||
'format2_time_and_key_recipients.dkc',
|
||||
true,
|
||||
),
|
||||
('format 3, one file', 'format3_single.dkc', false),
|
||||
(
|
||||
'format 3, time_and_key with a .dkk',
|
||||
'format3_time_and_key_portable.dkc',
|
||||
true,
|
||||
),
|
||||
]) {
|
||||
final r = record(file);
|
||||
final dkc = fixture(file);
|
||||
final dkk = withDkk ? fixture(r['access_key_file']! as String) : null;
|
||||
final format3 = r['format'] == 3;
|
||||
final times = <double>[];
|
||||
for (var i = 0; i < runs; i++) {
|
||||
final evict = releaseOf(r).round == releaseOf(other).round
|
||||
? record('time_and_key_portable.dkc')
|
||||
: other;
|
||||
verifyRelease(quicknet(), releaseOf(evict).round, releaseOf(evict));
|
||||
final sw = Stopwatch()..start();
|
||||
final o = await openCapsule(
|
||||
dkc,
|
||||
format3
|
||||
? optionsOf(r, sink: DiscardFiles(), dkk: dkk)
|
||||
: optionsOf(r, output: Discard(), dkk: dkk),
|
||||
);
|
||||
times.add(sw.elapsedMicroseconds / 1000);
|
||||
if (!o.ok) throw StateError('$label: ${o.error}');
|
||||
}
|
||||
print(' $label: ${median(times).toStringAsFixed(1)} ms');
|
||||
}
|
||||
|
||||
// Throughput: 64 MiB on the VM, 16 MiB on Node.js, from a source.
|
||||
final mib = js ? 16 : 64;
|
||||
final content = pattern(mib << 20);
|
||||
final r1 = record('time_only_extensions.dkc');
|
||||
final big1 = withContent1(
|
||||
fixture('time_only_extensions.dkc'),
|
||||
fromHex(r1['payload_identity']! as String),
|
||||
content,
|
||||
);
|
||||
final r3 = record('format3_single.dkc');
|
||||
final big3 = withFiles3(
|
||||
fixture('format3_single.dkc'),
|
||||
fromHex(r3['payload_identity']! as String),
|
||||
releaseOf(r3),
|
||||
[('grande.bin', content)],
|
||||
);
|
||||
for (final (label, dkc, r, format3) in [
|
||||
('format 1', big1, r1, false),
|
||||
('format 3, one file', big3, r3, true),
|
||||
]) {
|
||||
final times = <double>[];
|
||||
for (var i = 0; i < 3; i++) {
|
||||
final out = Discard();
|
||||
final files = DiscardFiles();
|
||||
final sw = Stopwatch()..start();
|
||||
final o = await openCapsuleSource(
|
||||
BytesSource(dkc),
|
||||
// Never `c ? null : sink`: dart2js of Dart 3.13 loses the writes to
|
||||
// an object that reaches a field that way, and reads its counter as
|
||||
// 0 afterwards (see the README).
|
||||
format3 ? optionsOf(r, sink: files) : optionsOf(r, output: out),
|
||||
);
|
||||
times.add(sw.elapsedMicroseconds / 1e6);
|
||||
if (!o.ok || (format3 ? files.n : out.n) != content.length) {
|
||||
throw StateError('$label: ${o.error}');
|
||||
}
|
||||
}
|
||||
final s = median(times);
|
||||
print(
|
||||
' $mib MiB, $label, from a source: ${s.toStringAsFixed(2)} s, '
|
||||
'${(mib / s).toStringAsFixed(1)} MiB/s',
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in new issue