• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

randombit / botan / 22765580681

06 Mar 2026 01:30PM UTC coverage: 90.272% (-0.002%) from 90.274%
22765580681

push

github

web-flow
Merge pull request #5413 from randombit/jack/ecc-bench-cli

Minor improvements to EC benchmarking cli utils

103671 of 114843 relevant lines covered (90.27%)

11660315.1 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

71.43
/src/cli/speed.cpp
1
/*
2
* (C) 2009,2010,2014,2015,2017,2018,2024 Jack Lloyd
3
* (C) 2015 Simon Warta (Kullo GmbH)
4
*
5
* Botan is released under the Simplified BSD License (see license.txt)
6
*/
7

8
#include "cli.h"
9
#include "perf.h"
10

11
#include <algorithm>
12
#include <iomanip>
13
#include <map>
14
#include <set>
15
#include <sstream>
16

17
// Always available:
18
#include <botan/version.h>
19
#include <botan/internal/target_info.h>
20

21
#if defined(BOTAN_HAS_CPUID)
22
   #include <botan/internal/cpuid.h>
23
#endif
24

25
#if defined(BOTAN_HAS_OS_UTILS)
26
   #include <botan/internal/os_utils.h>
27
#endif
28

29
#if defined(BOTAN_HAS_ECC_GROUP)
30
   #include <botan/ec_group.h>
31
#endif
32

33
namespace Botan_CLI {
34

35
namespace {
36

37
class JSON_Output final {
2✔
38
   public:
39
      void add(const Timer& timer) { m_results.push_back(timer); }
2✔
40

41
      std::string print() const {
1✔
42
         std::ostringstream out;
1✔
43

44
         out << "[\n";
1✔
45

46
         out << "{"
1✔
47
             << R"("arch": ")" << BOTAN_TARGET_ARCH << "\", "
48
             << R"("version": ")" << Botan::short_version_cstr() << "\", ";
1✔
49

50
         if(auto vc_revision = Botan::version_vc_revision()) {
1✔
51
            out << R"("git": ")" << *vc_revision << "\", ";
×
52
         }
×
53

54
         out << R"("compiler": ")" << BOTAN_COMPILER_INVOCATION_STRING << "\""
1✔
55
             << "},\n";
1✔
56

57
         for(size_t i = 0; i != m_results.size(); ++i) {
3✔
58
            const Timer& t = m_results[i];
2✔
59

60
            out << "{"
2✔
61
                << R"("algo": ")" << t.get_name() << "\", "
2✔
62
                << R"("op": ")" << t.doing() << "\", "
2✔
63
                << "\"events\": " << t.events() << ", ";
2✔
64

65
            if(t.cycles_consumed() > 0) {
4✔
66
               out << "\"cycles\": " << t.cycles_consumed() << ", ";
4✔
67
            }
68

69
            if(t.buf_size() > 0) {
2✔
70
               out << "\"bps\": " << static_cast<uint64_t>(t.events() / (t.nanoseconds() / 1000000000.0)) << ", ";
2✔
71
               out << "\"buf_size\": " << t.buf_size() << ", ";
2✔
72
            }
73

74
            out << "\"nanos\": " << t.value() << "}";
2✔
75

76
            if(i != m_results.size() - 1) {
2✔
77
               out << ",";
1✔
78
            }
79

80
            out << "\n";
2✔
81
         }
82
         out << "]\n";
1✔
83

84
         return out.str();
2✔
85
      }
1✔
86

87
   private:
88
      std::vector<Timer> m_results;
89
};
90

91
class Summary final {
1✔
92
   public:
93
      Summary() = default;
1✔
94

95
      void add(const Timer& t) {
2✔
96
         if(t.buf_size() == 0) {
2✔
97
            m_ops_entries.push_back(t);
×
98
         } else {
99
            m_bps_entries[std::make_pair(t.doing(), t.get_name())].push_back(t);
4✔
100
         }
101
      }
2✔
102

103
      std::string print() {
1✔
104
         const size_t name_padding = 35;
1✔
105
         const size_t op_name_padding = 16;
1✔
106
         const size_t op_padding = 16;
1✔
107

108
         std::ostringstream result_ss;
1✔
109
         result_ss << std::fixed;
1✔
110

111
         if(!m_bps_entries.empty()) {
1✔
112
            result_ss << "\n";
1✔
113

114
            // add table header
115
            result_ss << std::setw(name_padding) << std::left << "algo" << std::setw(op_name_padding) << std::left
1✔
116
                      << "operation";
1✔
117

118
            for(const Timer& t : m_bps_entries.begin()->second) {
2✔
119
               result_ss << std::setw(op_padding) << std::right << (std::to_string(t.buf_size()) + " bytes");
2✔
120
            }
121
            result_ss << "\n";
1✔
122

123
            // add table entries
124
            for(const auto& entry : m_bps_entries) {
3✔
125
               if(entry.second.empty()) {
2✔
126
                  continue;
×
127
               }
128

129
               result_ss << std::setw(name_padding) << std::left << (entry.first.second) << std::setw(op_name_padding)
2✔
130
                         << std::left << (entry.first.first);
2✔
131

132
               for(const Timer& t : entry.second) {
4✔
133
                  if(t.events() == 0) {
2✔
134
                     result_ss << std::setw(op_padding) << std::right << "N/A";
×
135
                  } else {
136
                     result_ss << std::setw(op_padding) << std::right << std::setprecision(2)
2✔
137
                               << (t.bytes_per_second() / 1000.0);
2✔
138
                  }
139
               }
140

141
               result_ss << "\n";
2✔
142
            }
143

144
            result_ss << "\n[results are the number of 1000s bytes processed per second]\n";
1✔
145
         }
146

147
         if(!m_ops_entries.empty()) {
1✔
148
            result_ss << std::setprecision(6) << "\n";
×
149

150
            // sort entries
151
            std::sort(m_ops_entries.begin(), m_ops_entries.end());
×
152

153
            // add table header
154
            result_ss << std::setw(name_padding) << std::left << "algo" << std::setw(op_name_padding) << std::left
×
155
                      << "operation" << std::setw(op_padding) << std::right << "sec/op" << std::setw(op_padding)
×
156
                      << std::right << "op/sec"
×
157
                      << "\n";
×
158

159
            // add table entries
160
            for(const Timer& entry : m_ops_entries) {
×
161
               result_ss << std::setw(name_padding) << std::left << entry.get_name() << std::setw(op_name_padding)
×
162
                         << std::left << entry.doing() << std::setw(op_padding) << std::right
×
163
                         << entry.seconds_per_event() << std::setw(op_padding) << std::right
×
164
                         << entry.events_per_second() << "\n";
×
165
            }
166
         }
167

168
         return result_ss.str();
2✔
169
      }
1✔
170

171
   private:
172
      std::map<std::pair<std::string, std::string>, std::vector<Timer>> m_bps_entries;
173
      std::vector<Timer> m_ops_entries;
174
};
175

176
std::vector<size_t> unique_buffer_sizes(const std::string& cmdline_arg) {
29✔
177
   const size_t MAX_BUF_SIZE = 64 * 1024 * 1024;
29✔
178

179
   std::set<size_t> buf;
29✔
180
   for(const std::string& size_str : Command::split_on(cmdline_arg, ',')) {
56✔
181
      size_t x = 0;
30✔
182
      try {
30✔
183
         size_t converted = 0;
30✔
184
         x = static_cast<size_t>(std::stoul(size_str, &converted, 0));
30✔
185

186
         if(converted != size_str.size()) {
29✔
187
            throw CLI_Usage_Error("Invalid integer");
×
188
         }
189
      } catch(std::exception&) {
1✔
190
         throw CLI_Usage_Error("Invalid integer value '" + size_str + "' for option buf-size");
2✔
191
      }
1✔
192

193
      if(x == 0) {
29✔
194
         throw CLI_Usage_Error("Cannot have a zero-sized buffer");
2✔
195
      }
196

197
      if(x > MAX_BUF_SIZE) {
28✔
198
         throw CLI_Usage_Error("Specified buffer size is too large");
2✔
199
      }
200

201
      buf.insert(x);
27✔
202
   }
29✔
203

204
   return std::vector<size_t>(buf.begin(), buf.end());
29✔
205
}
26✔
206

207
std::string format_timer(const Timer& t, size_t time_unit) {
415✔
208
   constexpr size_t MiB = 1024 * 1024;
415✔
209

210
   std::ostringstream oss;
415✔
211

212
   oss << t.get_name() << " ";
415✔
213

214
   const uint64_t events = t.events();
415✔
215

216
   if(t.buf_size() == 0) {
415✔
217
      // Report operations/time unit
218

219
      if(events == 0) {
392✔
220
         oss << "no events ";
×
221
      } else {
222
         oss << static_cast<uint64_t>(t.events_per_second()) << ' ' << t.doing() << "/sec; ";
784✔
223

224
         if(time_unit == 1000) {
392✔
225
            oss << std::setprecision(2) << std::fixed << (t.milliseconds() / events) << " ms/op ";
392✔
226
         } else if(time_unit == 1000 * 1000) {
×
227
            oss << std::setprecision(2) << std::fixed << (t.microseconds() / events) << " us/op ";
×
228
         } else if(time_unit == 1000 * 1000 * 1000) {
×
229
            oss << std::setprecision(0) << std::fixed << (t.nanoseconds() / events) << " ns/op ";
×
230
         }
231

232
         if(t.cycles_consumed() != 0 && events > 0) {
784✔
233
            const double cycles_per_op = static_cast<double>(t.cycles_consumed()) / events;
392✔
234
            const int precision = (cycles_per_op < 10000) ? 2 : 0;
392✔
235
            oss << std::fixed << std::setprecision(precision) << cycles_per_op << " cycles/op ";
392✔
236
         }
237

238
         oss << "(" << events << " " << (events == 1 ? "op" : "ops") << " in " << t.milliseconds() << " ms)";
660✔
239
      }
240
   } else {
241
      // Bulk op - report bytes/time unit
242

243
      const double MiB_total = static_cast<double>(events) / MiB;
23✔
244
      const double MiB_per_sec = MiB_total / t.seconds();
23✔
245

246
      if(!t.doing().empty()) {
23✔
247
         oss << t.doing() << " ";
23✔
248
      }
249

250
      if(t.buf_size() > 0) {
23✔
251
         oss << "buffer size " << t.buf_size() << " bytes: ";
23✔
252
      }
253

254
      if(events == 0) {
23✔
255
         oss << "N/A ";
×
256
      } else {
257
         oss << std::fixed << std::setprecision(3) << MiB_per_sec << " MiB/sec ";
23✔
258
      }
259

260
      if(t.cycles_consumed() != 0 && events > 0) {
46✔
261
         const double cycles_per_byte = static_cast<double>(t.cycles_consumed()) / events;
23✔
262
         oss << std::fixed << std::setprecision(2) << cycles_per_byte << " cycles/byte ";
23✔
263
      }
264

265
      oss << "(" << MiB_total << " MiB in " << t.milliseconds() << " ms)";
23✔
266
   }
267

268
   return oss.str();
830✔
269
}
415✔
270

271
std::vector<std::string> interpret_ecc_groups(const std::string& arg) {
29✔
272
   if(arg.empty()) {
29✔
273
      return {"secp256r1", "secp384r1", "secp521r1", "brainpool256r1", "brainpool384r1", "brainpool512r1"};
29✔
274
   }
275
   if(arg == "nist") {
×
276
      return {"secp224r1", "secp256r1", "secp384r1", "secp521r1"};
×
277
   }
278

279
#if defined(BOTAN_HAS_ECC_GROUP)
280
   if(arg == "all") {
×
281
      const auto& all = Botan::EC_Group::known_named_groups();
×
282
      return std::vector<std::string>(all.begin(), all.end());
×
283
   }
284

285
   if(arg == "generic") {
×
286
      std::vector<std::string> groups;
×
287
      for(const auto& group_name : Botan::EC_Group::known_named_groups()) {
×
288
         const Botan::EC_Group group(group_name);
×
289
         if(group.engine() == Botan::EC_Group_Engine::Generic) {
×
290
            groups.push_back(group_name);
×
291
         }
292
      }
×
293
      return groups;
×
294
   }
×
295

296
   if(arg == "pcurves") {
×
297
      std::vector<std::string> groups;
×
298
      for(const auto& group_name : Botan::EC_Group::known_named_groups()) {
×
299
         const Botan::EC_Group group(group_name);
×
300
         if(group.engine() == Botan::EC_Group_Engine::Optimized) {
×
301
            groups.push_back(group_name);
×
302
         }
303
      }
×
304
      return groups;
×
305
   }
×
306
#endif
307

308
   return Command::split_on(arg, ',');
×
309
}
310

311
}  // namespace
312

313
class Speed final : public Command {
×
314
   public:
315
      Speed() :
30✔
316
            Command(
317
               "speed --msec=500 --format=default --time-unit=ms --ecc-groups= --buf-size=1024 --clear-cpuid= --cpu-clock-speed=0 --cpu-clock-ratio=1.0 *algos") {
60✔
318
      }
30✔
319

320
      static std::vector<std::string> default_benchmark_list() {
×
321
         /*
322
         This is not intended to be exhaustive: it just hits the high
323
         points of the most interesting or widely used algorithms.
324
         */
325
         // clang-format off
326
         return {
×
327
            /* Block ciphers */
328
            "AES-128",
329
            "AES-192",
330
            "AES-256",
331
            "ARIA-128",
332
            "ARIA-192",
333
            "ARIA-256",
334
            "Blowfish",
335
            "CAST-128",
336
            "Camellia-128",
337
            "Camellia-192",
338
            "Camellia-256",
339
            "DES",
340
            "TripleDES",
341
            "GOST-28147-89",
342
            "IDEA",
343
            "Noekeon",
344
            "SHACAL2",
345
            "SM4",
346
            "Serpent",
347
            "Threefish-512",
348
            "Twofish",
349

350
            /* Cipher modes */
351
            "AES-128/CBC",
352
            "AES-128/CTR-BE",
353
            "AES-128/EAX",
354
            "AES-128/OCB",
355
            "AES-128/GCM",
356
            "AES-128/XTS",
357
            "AES-128/SIV",
358
            "Ascon-AEAD128",
359

360
            "Serpent/CBC",
361
            "Serpent/CTR-BE",
362
            "Serpent/EAX",
363
            "Serpent/OCB",
364
            "Serpent/GCM",
365
            "Serpent/XTS",
366
            "Serpent/SIV",
367

368
            "ChaCha20Poly1305",
369

370
            /* Stream ciphers */
371
            "RC4",
372
            "Salsa20",
373
            "ChaCha20",
374

375
            /* Hashes */
376
            "SHA-1",
377
            "SHA-256",
378
            "SHA-512",
379
            "SHA-3(256)",
380
            "SHA-3(512)",
381
            "Ascon-Hash256",
382
            "RIPEMD-160",
383
            "Skein-512",
384
            "Blake2b",
385
            "Whirlpool",
386

387
            /* XOFs */
388
            "SHAKE-128",
389
            "SHAKE-256",
390
            "Ascon-XOF128",
391

392
            /* MACs */
393
            "CMAC(AES-128)",
394
            "HMAC(SHA-256)",
395

396
            /* pubkey */
397
            "RSA",
398
            "DH",
399
            "ECDH",
400
            "ECDSA",
401
            "Ed25519",
402
            "Ed448",
403
            "X25519",
404
            "X448",
405
            "ML-KEM",
406
            "ML-DSA",
407
            "SLH-DSA",
408
            "FrodoKEM",
409
            "HSS-LMS",
410
         };
×
411
         // clang-format on
412
      }
413

414
      std::string group() const override { return "misc"; }
1✔
415

416
      std::string description() const override { return "Measures the speed of algorithms"; }
1✔
417

418
      void go() override {
29✔
419
         const uint64_t milliseconds = get_arg_sz("msec");
29✔
420
         const std::string ecc_groups_arg = get_arg("ecc-groups");
29✔
421
         const std::string format = get_arg("format");
32✔
422
         const std::string clock_ratio = get_arg("cpu-clock-ratio");
32✔
423

424
         const size_t clock_speed = get_arg_sz("cpu-clock-speed");
29✔
425

426
         double clock_cycle_ratio = std::strtod(clock_ratio.c_str(), nullptr);
29✔
427

428
         m_time_unit = [](std::string_view tu) {
119✔
429
            if(tu == "ms") {
29✔
430
               return 1000;
29✔
431
            } else if(tu == "us") {
×
432
               return 1000 * 1000;
×
433
            } else if(tu == "ns") {
×
434
               return 1000 * 1000 * 1000;
×
435
            } else {
436
               throw CLI_Usage_Error("Unknown time unit (supported: ms, us, ns)");
×
437
            }
438
         }(get_arg("time-unit"));
29✔
439

440
         /*
441
         * This argument is intended to be the ratio between the cycle counter
442
         * and the actual machine cycles. It is extremely unlikely that there is
443
         * any machine where the cycle counter increments faster than the actual
444
         * clock.
445
         */
446
         if(clock_cycle_ratio < 0.0 || clock_cycle_ratio > 1.0) {
29✔
447
            throw CLI_Usage_Error("Unlikely CPU clock ratio of " + clock_ratio);
×
448
         }
449

450
         clock_cycle_ratio = 1.0 / clock_cycle_ratio;
29✔
451

452
#if defined(BOTAN_HAS_OS_UTILS)
453
         if(clock_speed != 0 && Botan::OS::get_cpu_cycle_counter() != 0) {
29✔
454
            error_output() << "The --cpu-clock-speed option is only intended to be used on "
×
455
                              "platforms without access to a cycle counter.\n"
456
                              "Expect incorrect results\n\n";
×
457
         }
458
#endif
459

460
         if(format == "table") {
29✔
461
            m_summary = std::make_unique<Summary>();
1✔
462
         } else if(format == "json") {
28✔
463
            m_json = std::make_unique<JSON_Output>();
4✔
464
         } else if(format != "default") {
27✔
465
            throw CLI_Usage_Error("Unknown --format type '" + format + "'");
×
466
         }
467

468
         const auto ecc_groups = interpret_ecc_groups(ecc_groups_arg);
29✔
469

470
         std::vector<std::string> algos = get_arg_list("algos");
29✔
471

472
         const std::vector<size_t> buf_sizes = unique_buffer_sizes(get_arg("buf-size"));
61✔
473

474
#if defined(BOTAN_HAS_CPUID)
475
         for(const std::string& cpuid_to_clear : Command::split_on(get_arg("clear-cpuid"), ',')) {
27✔
476
            if(auto bit = Botan::CPUID::bit_from_string(cpuid_to_clear)) {
1✔
477
               Botan::CPUID::clear_cpuid_bit(*bit);
×
478
            } else {
479
               error_output() << "Warning don't know CPUID flag '" << cpuid_to_clear << "'\n";
1✔
480
            }
481
         }
26✔
482
#endif
483

484
         if(verbose() || m_summary) {
26✔
485
#if defined(BOTAN_HAS_CPUID)
486
            output() << Botan::version_string() << "\n"
2✔
487
                     << "CPUID: " << Botan::CPUID::to_string() << "\n\n";
3✔
488
#else
489
            output() << Botan::version_string() << "\n\n";
490
#endif
491
         }
492

493
         const bool using_defaults = (algos.empty());
26✔
494
         if(using_defaults) {
26✔
495
            algos = default_benchmark_list();
×
496
         }
497

498
         const PerfConfig perf_config([&](const Timer& t) { this->record_result(t); },
443✔
499
                                      clock_speed,
500
                                      clock_cycle_ratio,
501
                                      milliseconds,
502
                                      ecc_groups,
503
                                      buf_sizes,
504
                                      this->error_output(),
505
                                      this->rng());
26✔
506

507
         for(const auto& algo : algos) {
68✔
508
            if(auto perf = PerfTest::get(algo)) {
42✔
509
               perf->go(perf_config);
42✔
510
            } else if(verbose() || !using_defaults) {
×
511
               error_output() << "Unknown algorithm '" << algo << "'\n";
×
512
            }
42✔
513
         }
514

515
         if(m_json) {
26✔
516
            output() << m_json->print();
2✔
517
         }
518
         if(m_summary) {
26✔
519
            output() << m_summary->print() << "\n";
3✔
520
         }
521

522
         if(verbose() && clock_speed == 0 && m_cycles_consumed > 0 && m_ns_taken > 0) {
26✔
523
            const double seconds = static_cast<double>(m_ns_taken) / 1000000000;
×
524
            const double Hz = static_cast<double>(m_cycles_consumed) / seconds;
×
525
            const double MHz = Hz / 1000000;
×
526
            output() << "\nEstimated clock speed " << MHz << " MHz\n";
×
527
         }
528
      }
64✔
529

530
   private:
531
      size_t m_time_unit = 0;
532
      uint64_t m_cycles_consumed = 0;
533
      uint64_t m_ns_taken = 0;
534
      std::unique_ptr<Summary> m_summary;
535
      std::unique_ptr<JSON_Output> m_json;
536

537
      void record_result(const Timer& t) {
417✔
538
         m_ns_taken += t.value();
417✔
539
         m_cycles_consumed += t.cycles_consumed();
417✔
540
         if(m_json) {
417✔
541
            m_json->add(t);
2✔
542
         } else {
543
            output() << format_timer(t, m_time_unit) << "\n" << std::flush;
830✔
544

545
            if(m_summary) {
415✔
546
               m_summary->add(t);
2✔
547
            }
548
         }
549
      }
417✔
550
};
551

552
BOTAN_REGISTER_COMMAND("speed", Speed);
30✔
553

554
}  // namespace Botan_CLI
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc