Barretenberg
The ZK-SNARK library at the core of Aztec
Loading...
Searching...
No Matches
bb_bench.cpp
Go to the documentation of this file.
2#include <cstdint>
3#include <sys/types.h>
4#if !defined(__wasm__) || defined(ENABLE_WASM_BENCH)
6#include "bb_bench.hpp"
7#include <algorithm>
8#include <cassert>
9#include <chrono>
10#include <cmath>
11#include <functional>
12#include <iomanip>
13#include <iostream>
14#include <mutex>
15#include <ostream>
16#include <set>
17#include <sstream>
18#include <thread>
19#include <vector>
20
21namespace {
22// ANSI color codes
23struct Colors {
24 static constexpr const char* WHITE = "\033[37m";
25 static constexpr const char* RESET = "\033[0m";
26 static constexpr const char* BOLD = "\033[1m";
27 static constexpr const char* CYAN = "\033[36m";
28 static constexpr const char* GREEN = "\033[32m";
29 static constexpr const char* YELLOW = "\033[33m";
30 static constexpr const char* MAGENTA = "\033[35m";
31 static constexpr const char* DIM = "\033[2m";
32 static constexpr const char* RED = "\033[31m";
33};
34
35// Format time value with appropriate unit
36std::string format_time(double time_ms)
37{
38 std::ostringstream oss;
39 if (time_ms >= 1000.0) {
40 oss << std::fixed << std::setprecision(2) << (time_ms / 1000.0) << " s";
41 } else if (time_ms >= 1.0 && time_ms < 1000.0) {
42 oss << std::fixed << std::setprecision(2) << time_ms << " ms";
43 } else {
44 oss << std::fixed << std::setprecision(1) << (time_ms * 1000.0) << " μs";
45 }
46 return oss.str();
47}
48
49// Format time with fixed width for alignment
50std::string format_time_aligned(double time_ms)
51{
52 std::ostringstream oss;
53 if (time_ms >= 1000.0) {
54 std::ostringstream time_oss;
55 time_oss << std::fixed << std::setprecision(2) << (time_ms / 1000.0) << "s";
56 oss << std::left << std::setw(10) << time_oss.str();
57 } else {
58 std::ostringstream time_oss;
59 time_oss << std::fixed << std::setprecision(1) << time_ms << "ms";
60 oss << std::left << std::setw(10) << time_oss.str();
61 }
62 return oss.str();
63}
64
65// Helper to format percentage value
66std::string format_percentage_value(double percentage, const char* color)
67{
68 std::ostringstream oss;
69 oss << color << " " << std::left << std::fixed << std::setprecision(1) << std::setw(5) << percentage << "%"
70 << Colors::RESET;
71 return oss.str();
72}
73
74// Helper to format percentage with color based on percentage value
75std::string format_percentage(double value, double total, double min_threshold = 0.0)
76{
77 double percentage = (total <= 0) ? 0.0 : (value / total) * 100.0;
78 if (total <= 0 || percentage < min_threshold) {
79 return " ";
80 }
81
82 // Choose color based on percentage value (like time colors)
83 const char* color = Colors::CYAN; // Default color
84
85 return format_percentage_value(percentage, color);
86}
87
88// Helper to format percentage section
89std::string format_percentage_section(double time_ms, double parent_time, size_t indent_level)
90{
91 if (parent_time > 0 && indent_level > 0) {
92 return format_percentage(time_ms * 1000000.0, parent_time);
93 }
94 return " ";
95}
96
97// Helper to format time section
98std::string format_time_section(double time_ms)
99{
100 std::ostringstream oss;
101 oss << " ";
102 if (time_ms >= 100.0 && time_ms < 1000.0) {
103 oss << Colors::DIM << format_time_aligned(time_ms) << Colors::RESET;
104 } else {
105 oss << format_time_aligned(time_ms);
106 }
107 return oss.str();
108}
109
110// Helper to format call stats
111std::string format_call_stats(double time_ms, uint64_t count)
112{
113 if (!(time_ms >= 100.0 && count > 1)) {
114 return "";
115 }
116 double avg_ms = time_ms / static_cast<double>(count);
117 std::ostringstream oss;
118 oss << Colors::DIM << " (" << format_time(avg_ms) << " x " << count << ")" << Colors::RESET;
119 return oss.str();
120}
121
122std::string format_aligned_section(double time_ms, double parent_time, uint64_t count, size_t indent_level)
123{
124 std::ostringstream oss;
125
126 // Add indent level indicator at the beginning with different color
127 oss << Colors::MAGENTA << "[" << indent_level << "] " << Colors::RESET;
128
129 // Format percentage FIRST
130 oss << format_percentage_section(time_ms, parent_time, indent_level);
131
132 // Format time AFTER percentage with appropriate color (with more spacing)
133 oss << format_time_section(time_ms);
134
135 // Format calls/threads info - only show for >= 100ms, make it DIM
136 oss << format_call_stats(time_ms, count);
137
138 return oss.str();
139}
140
141// Get color based on time threshold
142struct TimeColor {
143 const char* name_color;
144 const char* time_color;
145};
146
147TimeColor get_time_colors(double time_ms)
148{
149 if (time_ms >= 1000.0) {
150 return { Colors::BOLD, Colors::WHITE };
151 }
152 if (time_ms >= 100.0) {
153 return { Colors::YELLOW, Colors::YELLOW };
154 }
155 return { Colors::DIM, Colors::DIM };
156}
157
158// Print separator line
159void print_separator(std::ostream& os, bool thick = true)
160{
161 const char* line = thick ? "═══════════════════════════════════════════════════════════════════════════════════════"
162 "═════════════════════"
163 : "───────────────────────────────────────────────────────────────────────────────────────"
164 "─────────────────────";
165 os << Colors::BOLD << Colors::CYAN << line << Colors::RESET << "\n";
166}
167} // anonymous namespace
168
169namespace bb::detail {
170
171// use_bb_bench is also set by --print_bench and --bench_out flags
172// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
173bool use_bb_bench = std::getenv("BB_BENCH") == nullptr ? false : std::string(std::getenv("BB_BENCH")) == "1";
174// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
175using OperationKey = std::string_view;
176
178{
179 if (stats.count == 0) {
180 return;
181 }
182 // Account for aggregate time and count
183 time += stats.time;
184 count += stats.count;
185 time_max = std::max(stats.time, time_max);
186 // Use Welford's method to be able to track the variance
187 num_threads++;
188 double delta = static_cast<double>(stats.time) - time_mean;
189 time_mean += delta / static_cast<double>(num_threads);
190 double delta2 = static_cast<double>(stats.time) - time_mean;
191 time_m2 += delta * delta2;
192}
193
195{
196 // Calculate standard deviation
197 if (num_threads > 1) {
198 return std::sqrt(time_m2 / static_cast<double>(num_threads - 1));
199 }
200 return 0;
201}
202
203// Normalize the raw benchmark data into a clean structure for display
205{
206 AggregateData result;
207
208 // Each count has a unique [thread, key] combo.
209 // We therefore treat each count as a thread's contribution to that key.
210 for (const std::shared_ptr<TimeStatsEntry>& entry : entries) {
211 // A map from parent key => AggregateEntry
212 auto& entry_map = result[entry->key];
213 // combine all entries with same parent key
214 std::map<OperationKey, TimeAndCount> parent_key_to_stats;
215
216 // For collection-time performance, we allow multiple stat blocks with the same parent. It'd be simpler to have
217 // one but we just have to combine them here.
218 for (const TimeStats* stats = &entry->count; stats != nullptr; stats = stats->next.get()) {
219 OperationKey parent_key = stats->parent != nullptr ? stats->parent->key : "";
220 parent_key_to_stats[parent_key].count += stats->count;
221 parent_key_to_stats[parent_key].time += stats->time;
222 }
223
224 for (auto [parent_key, stats] : parent_key_to_stats) {
225 auto& normalized_entry = entry_map[parent_key];
226 normalized_entry.key = entry->key;
227 normalized_entry.parent = parent_key;
228 normalized_entry.add_thread_time_sample(stats);
229 }
230 }
231
232 return result;
233}
234
241
243{
245 entry->key = key;
246 entries.push_back(entry);
247}
248
250{
251 std::cout << "GlobalBenchStatsContainer::print() START" << "\n";
252 for (const std::shared_ptr<TimeStatsEntry>& entry : entries) {
253 print_stats_recursive(entry->key, &entry->count, "");
254 }
255 std::cout << "GlobalBenchStatsContainer::print() END" << "\n";
256}
257
259 const TimeStats* stats,
260 const std::string& indent) const
261{
262 if (stats->count > 0) {
263 std::cout << indent << key << "\t" << stats->count << "\n";
264 }
265 if (stats->time > 0) {
266 std::cout << indent << key << "(t)\t" << static_cast<double>(stats->time) / 1000000.0 << "ms\n";
267 }
268
269 if (stats->next != nullptr) {
270 print_stats_recursive(key, stats->next.get(), indent + " ");
271 }
272}
273
274void GlobalBenchStatsContainer::print_aggregate_counts(std::ostream& os, size_t indent) const
275{
276 os << '{';
277 bool first = true;
278 for (const auto& [key, entry_map] : aggregate()) {
279 // Loop for a flattened view
280 uint64_t time = 0;
281 for (auto& [parent_key, entry] : entry_map) {
282 time += entry.time_max;
283 }
284
285 if (!first) {
286 os << ',';
287 }
288 if (indent > 0) {
289 os << "\n" << std::string(indent, ' ');
290 }
291 os << '"' << key << "\":" << time;
292 first = false;
293 }
294 if (indent > 0) {
295 os << "\n";
296 }
297 os << '}' << "\n";
298}
299
300// Serializable structure for a single benchmark entry (msgpack-compatible)
312
314{
316
317 // Convert AggregateData to a msgpack-serializable map
319
320 for (const auto& [key, parent_map] : data) {
322
323 for (const auto& [parent_key, entry] : parent_map) {
324 // Skip _root entries that have zero time (never called at root level)
325 if (parent_key.empty() && entry.time == 0) {
326 continue;
327 }
328
329 entries.push_back(SerializableEntry{ .parent = parent_key.empty() ? "_root" : std::string(parent_key),
330 .time = entry.time,
331 .time_max = entry.time_max,
332 .time_mean = entry.time_mean,
333 .time_stddev = entry.get_std_dev(),
334 .count = entry.count,
335 .num_threads = entry.num_threads });
336 }
337
338 // Only add functions that have non-empty entries
339 if (!entries.empty()) {
340 serializable_data[std::string(key)] = entries;
341 }
342 }
343
344 // Use msgpack to serialize and convert to JSON
345 msgpack::sbuffer buffer;
346 msgpack::pack(buffer, serializable_data);
347 msgpack::object_handle oh = msgpack::unpack(buffer.data(), buffer.size());
348 os << oh.get() << std::endl;
349}
350
352{
353 AggregateData aggregated = aggregate();
354
355 if (aggregated.empty()) {
356 os << "No benchmark data collected\n";
357 return;
358 }
359
360 // Print header
361 os << "\n";
362 print_separator(os, true);
363 os << Colors::BOLD << " Benchmark Results" << Colors::RESET << "\n";
364 print_separator(os, true);
365
367 std::set<OperationKey> printed_in_detail;
368 for (auto& [key, entry_map] : aggregated) {
369 for (auto& [parent_key, entry] : entry_map) {
370 if (entry.count > 0) {
371 keys_to_parents[key].insert(parent_key);
372 }
373 }
374 }
375
376 // Helper function to print a stat line with tree drawing
377 auto print_entry = [&](const AggregateEntry& entry, size_t indent_level, bool is_last, uint64_t parent_time) {
378 std::string indent(indent_level * 2, ' ');
379 std::string prefix = (indent_level == 0) ? "" : (is_last ? "└─ " : "├─ ");
380
381 // Use exactly 80 characters for function name without indent
382 const size_t name_width = 80;
383 std::string display_name = std::string(entry.key);
384 if (display_name.length() > name_width) {
385 display_name = display_name.substr(0, name_width - 3) + "...";
386 }
387
388 double time_ms = static_cast<double>(entry.time_max) / 1000000.0;
389 auto colors = get_time_colors(time_ms);
390
391 // Print indent + prefix + name (exactly 80 chars) + time/percentage/calls
392 os << indent << prefix << colors.name_color;
393 if (time_ms >= 1000.0 && colors.name_color == Colors::BOLD) {
394 os << Colors::YELLOW; // Special case: bold yellow for >= 1s
395 }
396 os << std::left << std::setw(static_cast<int>(name_width)) << display_name << Colors::RESET;
397
398 // Print time if available with aligned section including indent level
399 if (entry.time_max > 0) {
400 if (time_ms < 100.0) {
401 // Minimal format for <100ms: only [level] and percentage, no time display
402 std::ostringstream minimal_oss;
403 minimal_oss << Colors::MAGENTA << "[" << indent_level << "] " << Colors::RESET;
404 minimal_oss << format_percentage_section(time_ms, static_cast<double>(parent_time), indent_level);
405 minimal_oss << " " << std::setw(10) << ""; // Add spacing to replace where time would be
406 os << " " << colors.time_color << std::setw(40) << std::left << minimal_oss.str() << Colors::RESET;
407 } else {
408 std::string aligned_section =
409 format_aligned_section(time_ms, static_cast<double>(parent_time), entry.count, indent_level);
410 os << " " << colors.time_color << std::setw(40) << std::left << aligned_section << Colors::RESET;
411 if (entry.num_threads > 1) {
412 double mean_ms = entry.time_mean / 1000000.0;
413 double stddev_percentage = floor(entry.get_std_dev() * 100 / entry.time_mean);
414 os << " " << entry.num_threads << " threads " << mean_ms << "ms average " << stddev_percentage
415 << "% stddev";
416 }
417 }
418 }
419
420 os << "\n";
421 };
422
423 // Recursive function to print hierarchy
424 std::function<void(OperationKey, size_t, bool, uint64_t, OperationKey)> print_hierarchy;
425 print_hierarchy = [&](OperationKey key,
426 size_t indent_level,
427 bool is_last,
428 uint64_t parent_time,
429 OperationKey current_parent) -> void {
430 auto it = aggregated.find(key);
431 if (it == aggregated.end()) {
432 return;
433 }
434
435 // Find the entry with the specific parent context
436 const AggregateEntry* entry_to_print = nullptr;
437 for (const auto& [parent_key, entry] : it->second) {
438 if ((indent_level == 0 && parent_key.empty()) || (indent_level > 0 && parent_key == current_parent)) {
439 entry_to_print = &entry;
440 break;
441 }
442 }
443
444 if (!entry_to_print) {
445 return;
446 }
447
448 // Print this entry
449 print_entry(*entry_to_print, indent_level, is_last, parent_time);
450
451 // Find and print children - operations that have this key as parent (only those with meaningful time >= 0.5ms)
453 if (!printed_in_detail.contains(key)) {
454 for (const auto& [child_key, parent_map] : aggregated) {
455 for (const auto& [parent_key, entry] : parent_map) {
456 if (parent_key == key && entry.time_max >= 500000) { // 0.5ms in nanoseconds
457 children.push_back(child_key);
458 break;
459 }
460 }
461 }
462 printed_in_detail.insert(key);
463 }
464
465 // Sort children by their time in THIS parent context
466 std::ranges::sort(children, [&](OperationKey a, OperationKey b) {
467 uint64_t time_a = 0;
468 uint64_t time_b = 0;
469 if (auto it = aggregated.find(a); it != aggregated.end()) {
470 for (const auto& [parent_key, entry] : it->second) {
471 if (parent_key == key) {
472 time_a = entry.time_max;
473 break;
474 }
475 }
476 }
477 if (auto it = aggregated.find(b); it != aggregated.end()) {
478 for (const auto& [parent_key, entry] : it->second) {
479 if (parent_key == key) {
480 time_b = entry.time_max;
481 break;
482 }
483 }
484 }
485 return time_a > time_b;
486 });
487
488 // Calculate time spent in children and add "(other)" if >5% unaccounted
489 uint64_t children_total_time = 0;
490 for (const auto& child_key : children) {
491 if (auto it = aggregated.find(child_key); it != aggregated.end()) {
492 for (const auto& [parent_key, entry] : it->second) {
493 if (parent_key == key && entry.time_max >= 500000) { // 0.5ms in nanoseconds
494 children_total_time += entry.time_max;
495 }
496 }
497 }
498 }
499 uint64_t parent_total_time = entry_to_print->time_max;
500 bool should_add_other = false;
501 if (!children.empty() && parent_total_time > 0 && children_total_time < parent_total_time) {
502 uint64_t unaccounted = parent_total_time - children_total_time;
503 double percentage = (static_cast<double>(unaccounted) / static_cast<double>(parent_total_time)) * 100.0;
504 should_add_other = percentage > 5.0 && unaccounted > 0;
505 }
506 uint64_t other_time = should_add_other ? (parent_total_time - children_total_time) : 0;
507
508 if (!children.empty() && keys_to_parents[key].size() > 1) {
509 os << std::string(indent_level * 2, ' ') << " ├─ NOTE: Shared children. Can add up to > 100%.\n";
510 }
511
512 // Print children
513 for (size_t i = 0; i < children.size(); ++i) {
514 bool is_last_child = (i == children.size() - 1) && !should_add_other;
515 print_hierarchy(children[i], indent_level + 1, is_last_child, entry_to_print->time, key);
516 }
517
518 // Print "(other)" category if significant unaccounted time exists
519 if (should_add_other && keys_to_parents[key].size() <= 1) {
520 AggregateEntry other_entry;
521 other_entry.key = "(other)";
522 other_entry.time = other_time;
523 other_entry.time_max = other_time;
524 other_entry.count = 1;
525 other_entry.num_threads = 1;
526 print_entry(other_entry, indent_level + 1, true, parent_total_time); // always last
527 }
528 };
529
530 // Find root entries (those that ONLY have empty parent key and significant time)
532 for (const auto& [key, parent_map] : aggregated) {
533 auto empty_parent_it = parent_map.find("");
534 if (empty_parent_it != parent_map.end() && empty_parent_it->second.time > 0) {
535 roots.push_back(key);
536 }
537 }
538
539 // Sort roots by time (descending)
540 std::ranges::sort(roots, [&](OperationKey a, OperationKey b) {
541 uint64_t time_a = 0;
542 uint64_t time_b = 0;
543 if (auto it_a = aggregated.find(a); it_a != aggregated.end()) {
544 if (auto parent_it = it_a->second.find(""); parent_it != it_a->second.end()) {
545 time_a = parent_it->second.time_max;
546 }
547 }
548 if (auto it_b = aggregated.find(b); it_b != aggregated.end()) {
549 if (auto parent_it = it_b->second.find(""); parent_it != it_b->second.end()) {
550 time_b = parent_it->second.time_max;
551 }
552 }
553 return time_a > time_b;
554 });
555
556 // Print hierarchies starting from roots
557 for (size_t i = 0; i < roots.size(); ++i) {
558 print_hierarchy(roots[i], 0, i == roots.size() - 1, 0, "");
559 }
560
561 // Print summary
562 print_separator(os, false);
563
564 // Calculate totals from root entries
565 std::set<OperationKey> unique_funcs;
566 for (const auto& [key, _] : aggregated) {
567 unique_funcs.insert(key);
568 }
569 size_t unique_functions_count = unique_funcs.size();
570
571 uint64_t shared_count = 0;
572 for (const auto& [key, parents] : keys_to_parents) {
573 if (parents.size() > 1) {
574 shared_count++;
575 }
576 }
577
578 uint64_t total_time = 0;
579 for (const auto& [_, parent_map] : aggregated) {
580 if (auto it = parent_map.find(""); it != parent_map.end()) {
581 total_time = std::max(total_time, it->second.time_max);
582 }
583 }
584
585 uint64_t total_calls = 0;
586 for (const auto& [_, parent_map] : aggregated) {
587 for (const auto& [__, entry] : parent_map) {
588 total_calls += entry.count;
589 }
590 }
591
592 double total_time_ms = static_cast<double>(total_time) / 1000000.0;
593
594 os << " " << Colors::BOLD << "Total: " << Colors::RESET << Colors::MAGENTA << unique_functions_count
595 << " functions" << Colors::RESET;
596 if (shared_count > 0) {
597 os << " (" << Colors::RED << shared_count << " shared" << Colors::RESET << ")";
598 }
599 os << ", " << Colors::GREEN << total_calls << " measurements" << Colors::RESET << ", " << Colors::YELLOW;
600 if (total_time_ms >= 1000.0) {
601 os << std::fixed << std::setprecision(2) << (total_time_ms / 1000.0) << " seconds";
602 } else {
603 os << std::fixed << std::setprecision(2) << total_time_ms << " ms";
604 }
605 os << Colors::RESET;
606
607 os << "\n";
608 print_separator(os, true);
609 os << "\n";
610}
611
613{
616 entry->count = TimeStats();
617 }
618}
619
620// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
622
624 : parent(nullptr)
625 , stats(entry)
626 , time(0)
627{
628 if (stats == nullptr) {
629 return;
630 }
631 // Track the current parent context
633 auto now = std::chrono::high_resolution_clock::now();
634 auto now_ns = std::chrono::time_point_cast<std::chrono::nanoseconds>(now);
635 time = static_cast<uint64_t>(now_ns.time_since_epoch().count());
636}
638{
639 if (stats == nullptr) {
640 return;
641 }
642 auto now = std::chrono::high_resolution_clock::now();
643 auto now_ns = std::chrono::time_point_cast<std::chrono::nanoseconds>(now);
644 // Add, taking advantage of our parent context
645 stats->count.track(parent, static_cast<uint64_t>(now_ns.time_since_epoch().count()) - time);
646
647 // Unwind to previous parent
649}
650} // namespace bb::detail
651#endif
const std::vector< MemoryValue > data
FF a
FF b
uint8_t buffer[RANDOM_BUFFER_SIZE]
Definition engine.cpp:34
GlobalBenchStatsContainer GLOBAL_BENCH_STATS
Definition bb_bench.cpp:621
std::unordered_map< OperationKey, std::map< OperationKey, AggregateEntry > > AggregateData
Definition bb_bench.hpp:78
bool use_bb_bench
Definition bb_bench.cpp:173
std::string_view OperationKey
Definition bb_bench.cpp:175
constexpr decltype(auto) get(::tuplet::tuple< T... > &&t) noexcept
Definition tuple.hpp:13
Definition bb_bench.hpp:57
void add_thread_time_sample(const TimeAndCount &stats)
Definition bb_bench.cpp:177
uint64_t time
Definition bb_bench.hpp:61
double time_m2
Definition bb_bench.hpp:69
double time_mean
Definition bb_bench.hpp:64
size_t num_threads
Definition bb_bench.hpp:63
uint64_t time_max
Definition bb_bench.hpp:65
double get_std_dev() const
Definition bb_bench.cpp:194
OperationKey key
Definition bb_bench.hpp:59
uint64_t count
Definition bb_bench.hpp:62
BenchReporter(TimeStatsEntry *entry)
Definition bb_bench.cpp:623
TimeStatsEntry * parent
Definition bb_bench.hpp:182
TimeStatsEntry * stats
Definition bb_bench.hpp:183
void print_stats_recursive(const OperationKey &key, const TimeStats *stats, const std::string &indent) const
Definition bb_bench.cpp:258
void print_aggregate_counts_hierarchical(std::ostream &) const
Definition bb_bench.cpp:351
void print_aggregate_counts(std::ostream &, size_t) const
Definition bb_bench.cpp:274
void serialize_aggregate_data_json(std::ostream &) const
Definition bb_bench.cpp:313
void add_entry(const char *key, const std::shared_ptr< TimeStatsEntry > &entry)
Definition bb_bench.cpp:242
std::vector< std::shared_ptr< TimeStatsEntry > > entries
Definition bb_bench.hpp:86
static thread_local TimeStatsEntry * parent
Definition bb_bench.hpp:83
Definition bb_bench.cpp:301
double time_mean
Definition bb_bench.cpp:305
std::string parent
Definition bb_bench.cpp:302
uint64_t time
Definition bb_bench.cpp:303
MSGPACK_FIELDS(parent, time, time_max, time_mean, time_stddev, count, num_threads)
double time_stddev
Definition bb_bench.cpp:306
uint64_t time_max
Definition bb_bench.cpp:304
uint64_t num_threads
Definition bb_bench.cpp:308
uint64_t count
Definition bb_bench.cpp:307
Definition bb_bench.hpp:154
TimeStats count
Definition bb_bench.hpp:156
void track(TimeStatsEntry *current_parent, uint64_t time_val)
Definition bb_bench.hpp:121
std::unique_ptr< TimeStats > next
Definition bb_bench.hpp:112
#define RED
#define RESET