// Times the opening of datekeys-dart, stages 4c and 5c, 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; the opening of the two fixtures of // format 3 whose security area holds a signature of alg 2 and a seal of // seal_type 2, with the time of the evaluation of that area alone; 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 and securitycms_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'; import '../test/vectors/securitycms_vectors.g.dart'; typedef Json = Map; 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 xs) { final s = [...xs]..sort(); return s[s.length ~/ 2]; } Future 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 = []; 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'); } // The fixtures with certificates: the opening, and inside it the // evaluation of the security area by the default evaluator, the reader of // CMS included. final cms = ((jsonDecode(securityCmsFixturesJson) as Json)['fixtures']! as List) .cast(); for (final (label, r) in [ ('format 3, a signature of alg 2 by two signers, each sealed', cms[0]), ('format 3, a signature of alg 1 and a seal of seal_type 2', cms[1]), ]) { final dkc = fromHex(r['dkc']! as String); final times = []; final evaluations = []; for (var i = 0; i < runs; i++) { verifyRelease(quicknet(), releaseOf(other).round, releaseOf(other)); final evaluation = Stopwatch(); final sw = Stopwatch()..start(); final o = await openCapsule( dkc, OpenOptions( source: suppliedRelease(releaseOf(r)), now: () => parseRfc3339(r['unlock_at']! as String), sink: DiscardFiles(), evaluator: (input) { evaluation.start(); final v = evaluateSecurityInput(input); evaluation.stop(); return v; }, ), ); times.add(sw.elapsedMicroseconds / 1000); evaluations.add(evaluation.elapsedMicroseconds / 1000); final want = (r['verdicts']! as Json)['signature']; if (!o.ok || o.verdicts?.signature?.code != want) { throw StateError('$label: ${o.error} ${o.verdicts}'); } } print( ' $label: ${median(times).toStringAsFixed(1)} ms, of which the ' 'security area ${median(evaluations).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 = []; 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', ); } }