1
use std::{
2
    collections::{BTreeMap, HashMap},
3
    ops::{Range, RangeInclusive},
4
    path::Path,
5
    sync::Arc,
6
    time::{Duration, Instant},
7
};
8

            
9
use bonsaidb::core::async_trait::async_trait;
10
use cli_table::{Cell, Table};
11
use futures::{stream::FuturesUnordered, StreamExt};
12
use plotters::{
13
    coord::ranged1d::{NoDefaultFormatting, ValueFormatter},
14
    element::{BackendCoordOnly, CoordMapper, Drawable, PointCollection},
15
    prelude::*,
16
};
17
use plotters_backend::DrawingErrorKind;
18
use rand::{rngs::SmallRng, SeedableRng};
19
use serde::{Deserialize, Serialize};
20
use tera::Tera;
21
use tokio::runtime::Runtime;
22

            
23
use crate::{
24
    bonsai::{Bonsai, BonsaiBackend},
25
    model::{InitialDataSet, InitialDataSetConfig},
26
    plan::{
27
        AddProductToCart, Checkout, CreateCart, FindProduct, Load, LookupProduct, Operation,
28
        OperationResult, Plan, ReviewProduct, ShopperPlanConfig,
29
    },
30
    plot::{label_to_color, BACKGROUND_COLOR, TEXT_COLOR},
31
    utils::{current_timestamp_string, local_git_rev},
32
};
33

            
34
4
pub fn execute_plans_for_all_backends(
35
4
    name_filter: &str,
36
4
    plans: &[Arc<Plan>],
37
4
    initial_data: &Arc<InitialDataSet>,
38
4
    number_of_agents: usize,
39
4
    measurements: &Measurements,
40
4
) {
41
4
    if name_filter.is_empty()
42
        || name_filter == "bonsaidb"
43
        || name_filter.starts_with("bonsaidb-local")
44
4
    {
45
4
        println!("Executing bonsaidb-local");
46
4
        BonsaiBackend::execute_async(
47
4
            Bonsai::Local,
48
4
            plans,
49
4
            initial_data,
50
4
            number_of_agents,
51
4
            measurements,
52
4
        );
53
4
    }
54
4
    if name_filter.is_empty()
55
        || name_filter == "bonsaidb"
56
        || name_filter.starts_with("bonsaidb-local+lz4")
57
4
    {
58
4
        println!("Executing bonsaidb-local+lz4");
59
4
        BonsaiBackend::execute_async(
60
4
            Bonsai::LocalLz4,
61
4
            plans,
62
4
            initial_data,
63
4
            number_of_agents,
64
4
            measurements,
65
4
        );
66
4
    }
67
4
    if name_filter.is_empty()
68
        || name_filter == "bonsaidb"
69
        || name_filter.starts_with("bonsaidb-quic")
70
4
    {
71
4
        println!("Executing bonsaidb-quic");
72
4
        BonsaiBackend::execute_async(
73
4
            Bonsai::Quic,
74
4
            plans,
75
4
            initial_data,
76
4
            number_of_agents,
77
4
            measurements,
78
4
        );
79
4
    }
80
4
    if name_filter.is_empty() || name_filter == "bonsaidb" || name_filter.starts_with("bonsaidb-ws")
81
4
    {
82
4
        println!("Executing bonsaidb-ws");
83
4
        BonsaiBackend::execute_async(
84
4
            Bonsai::WebSockets,
85
4
            plans,
86
4
            initial_data,
87
4
            number_of_agents,
88
4
            measurements,
89
4
        );
90
4
    }
91
    #[cfg(feature = "postgresql")]
92
4
    if name_filter.is_empty() || name_filter.starts_with("postgresql") {
93
4
        if let Ok(url) = std::env::var("COMMERCE_POSTGRESQL_URL") {
94
4
            println!("Executing postgresql");
95
4
            crate::postgres::Postgres::execute_async(
96
4
                url,
97
4
                plans,
98
4
                initial_data,
99
4
                number_of_agents,
100
4
                measurements,
101
4
            );
102
4
        } else {
103
            eprintln!("postgresql feature is enabled, but environment variable COMMERCE_POSTGRESQL_URL is missing.");
104
        }
105
    }
106
4
}
107

            
108
#[async_trait]
109
pub trait Backend: Sized + Send + Sync + 'static {
110
    type Operator: BackendOperator;
111
    type Config: Send + Sync;
112

            
113
    async fn new(config: Self::Config) -> Self;
114

            
115
    fn label(&self) -> &'static str;
116

            
117
20
    fn execute_async(
118
20
        config: Self::Config,
119
20
        plans: &[Arc<Plan>],
120
20
        initial_data: &Arc<InitialDataSet>,
121
20
        concurrent_agents: usize,
122
20
        measurements: &Measurements,
123
20
    ) {
124
20
        let (plan_sender, plan_receiver) = flume::bounded(concurrent_agents * 2);
125
20
        let runtime = Runtime::new().unwrap();
126
20
        let backend = runtime.block_on(Self::new(config));
127
20
        // Load the initial data
128
20
        println!("Loading data");
129
20
        runtime.block_on(async {
130
20
            let _ = backend
131
20
                .new_operator_async()
132
8
                .await
133
20
                .operate(
134
20
                    &Load {
135
20
                        initial_data: initial_data.clone(),
136
20
                    },
137
20
                    &[],
138
20
                    measurements,
139
19406
                )
140
19406
                .await;
141
20
        });
142
20
        println!("Executing plans");
143
20
        let agent_handles = FuturesUnordered::new();
144
45
        for _ in 0..concurrent_agents {
145
45
            let operator = runtime.block_on(backend.new_operator_async());
146
45
            agent_handles.push(runtime.spawn(agent::<Self>(
147
45
                operator,
148
45
                plan_receiver.clone(),
149
45
                measurements.clone(),
150
45
            )));
151
45
        }
152
20
        runtime.block_on(async {
153
            // Send the plans to the channel that the agents are waiting for
154
            // them on.
155
2520
            for plan in plans {
156
2500
                plan_sender.send_async(plan.clone()).await.unwrap();
157
            }
158
            // Disconnect the receivers, allowing the agents to exit once there
159
            // are no more plans in queue.
160
20
            drop(plan_sender);
161
            // Wait for each of the agents to return.
162
65
            for result in agent_handles.collect::<Vec<_>>().await {
163
45
                result.unwrap();
164
45
            }
165
20
        })
166
20
    }
167

            
168
    async fn new_operator_async(&self) -> Self::Operator;
169
}
170

            
171
#[async_trait]
172
pub trait Operator<T> {
173
    async fn operate(
174
        &mut self,
175
        operation: &T,
176
        results: &[OperationResult],
177
        measurements: &Measurements,
178
    ) -> OperationResult;
179
}
180

            
181
45
async fn agent<B: Backend>(
182
45
    mut operator: B::Operator,
183
45
    plan_receiver: flume::Receiver<Arc<Plan>>,
184
45
    measurements: Measurements,
185
45
) {
186
2545
    while let Ok(plan) = plan_receiver.recv_async().await {
187
2500
        let mut results = Vec::with_capacity(plan.operations.len());
188
26565
        for step in &plan.operations {
189
49367
            results.push(operator.operate(step, &results, &measurements).await)
190
        }
191
    }
192
45
}
193

            
194
pub trait BackendOperator:
195
    Operator<Load>
196
    + Operator<LookupProduct>
197
    + Operator<FindProduct>
198
    + Operator<CreateCart>
199
    + Operator<AddProductToCart>
200
    + Operator<ReviewProduct>
201
    + Operator<Checkout>
202
    + Send
203
    + Sync
204
{
205
}
206

            
207
#[async_trait]
208
impl<T> Operator<Operation> for T
209
where
210
    T: BackendOperator,
211
{
212
26565
    async fn operate(
213
26565
        &mut self,
214
26565
        operation: &Operation,
215
26565
        results: &[OperationResult],
216
26565
        measurements: &Measurements,
217
26565
    ) -> OperationResult {
218
26565
        match operation {
219
16885
            Operation::FindProduct(op) => self.operate(op, results, measurements).await,
220
14497
            Operation::LookupProduct(op) => self.operate(op, results, measurements).await,
221
3718
            Operation::CreateCart(op) => self.operate(op, results, measurements).await,
222
11577
            Operation::AddProductToCart(op) => self.operate(op, results, measurements).await,
223
1038
            Operation::RateProduct(op) => self.operate(op, results, measurements).await,
224
1655
            Operation::Checkout(op) => self.operate(op, results, measurements).await,
225
        }
226
53130
    }
227
}
228

            
229
4
#[derive(Serialize, Deserialize, Debug)]
230
pub struct BenchmarkSummary {
231
    label: String,
232
    timestamp: String,
233
    revision: String,
234
    plan_count: usize,
235
    agent_count: usize,
236
    product_count: usize,
237
    category_count: usize,
238
    customer_count: usize,
239
    order_count: usize,
240
    summaries: Vec<BackendSummary>,
241
    operations: Vec<MetricSummary>,
242
}
243

            
244
impl BenchmarkSummary {
245
4
    pub fn render_to(&self, location: &Path, tera: &Tera) {
246
4
        std::fs::write(
247
4
            location,
248
4
            tera.render("run.html", &tera::Context::from_serialize(self).unwrap())
249
4
                .unwrap()
250
4
                .as_bytes(),
251
4
        )
252
4
        .unwrap()
253
4
    }
254
}
255

            
256
20
#[derive(Serialize, Deserialize, Debug)]
257
pub struct BackendSummary {
258
    backend: String,
259
    transport: String,
260
    total_time: String,
261
    wall_time: String,
262
}
263

            
264
28
#[derive(Serialize, Deserialize, Debug)]
265
pub struct MetricSummary {
266
    metric: Metric,
267
    description: String,
268
    invocations: usize,
269
    summaries: Vec<OperationSummary>,
270
}
271

            
272
140
#[derive(Serialize, Deserialize, Debug)]
273
pub struct OperationSummary {
274
    backend: String,
275
    avg: String,
276
    min: String,
277
    max: String,
278
    stddev: String,
279
    outliers: String,
280
}
281

            
282
pub struct Benchmark<'a> {
283
    pub label: String,
284
    pub seed: Option<u64>,
285
    pub agents: Option<usize>,
286
    pub shoppers: Option<usize>,
287
    pub data_config: &'a InitialDataSetConfig,
288
    pub shopper_config: &'a ShopperPlanConfig,
289
}
290

            
291
impl<'a> Benchmark<'a> {
292
4
    pub fn execute(
293
4
        self,
294
4
        name_filter: &str,
295
4
        plot_dir: impl AsRef<Path>,
296
4
        tera: Arc<Tera>,
297
4
    ) -> BTreeMap<&'static str, Duration> {
298
4
        let plot_dir = plot_dir.as_ref().to_path_buf();
299
4
        std::fs::create_dir_all(&plot_dir).unwrap();
300

            
301
4
        let mut rng = if let Some(seed) = self.seed {
302
3
            SmallRng::seed_from_u64(seed)
303
        } else {
304
1
            SmallRng::from_entropy()
305
        };
306
4
        let initial_data = Arc::new(self.data_config.fake(&mut rng));
307
4
        let number_of_agents = self.agents.unwrap_or_else(num_cpus::get);
308
4
        let shoppers = self.shoppers.unwrap_or(number_of_agents * 100);
309
4

            
310
4
        println!(
311
4
            "Running {} plans across {} agents",
312
4
            shoppers, number_of_agents
313
4
        );
314
4
        // Generate plans to execute.
315
4
        let mut plans = Vec::with_capacity(shoppers as usize);
316
500
        for _ in 0..shoppers {
317
500
            plans.push(Arc::new(
318
500
                self.shopper_config.random_plan(&mut rng, &initial_data),
319
500
            ));
320
500
        }
321
        // Set up our statistics gathering thread
322
4
        let (metric_sender, metric_receiver) = flume::unbounded();
323
4
        let measurements = Measurements {
324
4
            sender: metric_sender,
325
4
        };
326
4
        let thread_initial_data = initial_data.clone();
327
4
        let stats_thread = std::thread::spawn(move || {
328
4
            stats_thread(
329
4
                self.label,
330
4
                metric_receiver,
331
4
                shoppers,
332
4
                number_of_agents,
333
4
                &thread_initial_data,
334
4
                &plot_dir,
335
4
                &tera,
336
4
            )
337
4
        });
338
4
        // Perform all benchmarks
339
4
        execute_plans_for_all_backends(
340
4
            name_filter,
341
4
            &plans,
342
4
            &initial_data,
343
4
            number_of_agents,
344
4
            &measurements,
345
4
        );
346
4
        // Drop the measurements instance to allow the stats thread to know
347
4
        // there are no more metrics coming.
348
4
        drop(measurements);
349
4
        // Wait for the statistics thread to report all the results.
350
4
        stats_thread.join().unwrap()
351
4
    }
352
}
353

            
354
45
#[derive(Clone)]
355
pub struct Measurements {
356
    sender: flume::Sender<(&'static str, Metric, Duration)>,
357
}
358

            
359
impl Measurements {
360
26585
    pub fn begin(&self, label: &'static str, metric: Metric) -> Measurement<'_> {
361
26585
        Measurement {
362
26585
            target: &self.sender,
363
26585
            label,
364
26585
            metric,
365
26585
            start: Instant::now(),
366
26585
        }
367
26585
    }
368
}
369

            
370
pub struct Measurement<'a> {
371
    target: &'a flume::Sender<(&'static str, Metric, Duration)>,
372
    label: &'static str,
373
    metric: Metric,
374
    start: Instant,
375
}
376

            
377
impl<'a> Measurement<'a> {
378
26585
    pub fn finish(self) {
379
26585
        let duration = Instant::now()
380
26585
            .checked_duration_since(self.start)
381
26585
            .expect("time went backwards. Restart benchmarks.");
382
26585
        self.target
383
26585
            .send((self.label, self.metric, duration))
384
26585
            .unwrap();
385
26585
    }
386
}
387

            
388
85328
#[derive(Serialize, Deserialize, Clone, Copy, Hash, Eq, PartialEq, Debug, Ord, PartialOrd)]
389
pub enum Metric {
390
    Load,
391
    LookupProduct,
392
    FindProduct,
393
    CreateCart,
394
    AddProductToCart,
395
    Checkout,
396
    RateProduct,
397
}
398

            
399
impl Metric {
400
28
    pub fn description(&self) -> &'static str {
401
28
        match self {
402
4
            Metric::Load => "Measures the time spent loading the initial data set and performing any pre-cache operations that most database administrators would perform on their databases periodically to ensure good performance.",
403
4
            Metric::LookupProduct => "Meaures the time spent looking up a product by its id. This operation is meant to simulate the basic needs of the database to provide a product details page after a user clicked a direct link that contians the product's unique id, including the product's current rating.",
404
4
            Metric::FindProduct => "Measures the time spent looking up a product by its name (exact match, indexed). This operation is meant to simulate the basic needs of the database to provide a product details after finding a product by its name, including the product's current rating.",
405
4
            Metric::CreateCart => "Measures the time spent creating a shopping cart.",
406
4
            Metric::AddProductToCart => "Measures the time spent adding a product to a shopping cart.",
407
4
            Metric::RateProduct => "Measures the time spent adding or updating a review of a product by a customer. Each customer can only have one review per product. When this operation is complete, all subsequent calls to LookupProduct and FindProduct should reflect the new rating. This simulates an 'upsert' (insert or update) operation using a unique index.",
408
4
            Metric::Checkout => "Measures the time spent converting a shopping cart into an order for a customer."
409
        }
410
28
    }
411
}
412

            
413
2860
fn format_nanoseconds(nanoseconds: f64) -> String {
414
2860
    if nanoseconds <= f64::EPSILON {
415
73
        String::from("0s")
416
2787
    } else if nanoseconds < 1_000. {
417
        format_float(nanoseconds, "ns")
418
2787
    } else if nanoseconds < 1_000_000. {
419
329
        format_float(nanoseconds / 1_000., "us")
420
2458
    } else if nanoseconds < 1_000_000_000. {
421
2317
        format_float(nanoseconds / 1_000_000., "ms")
422
141
    } else if nanoseconds < 1_000_000_000_000. {
423
141
        format_float(nanoseconds / 1_000_000_000., "s")
424
    } else {
425
        // this hopefully is unreachable...
426
        format_float(nanoseconds / 1_000_000_000. / 60., "m")
427
    }
428
2860
}
429

            
430
2787
fn format_float(value: f64, suffix: &str) -> String {
431
2787
    if value < 10. {
432
1667
        format!("{:.3}{}", value, suffix)
433
1120
    } else if value < 100. {
434
307
        format!("{:.2}{}", value, suffix)
435
    } else {
436
813
        format!("{:.1}{}", value, suffix)
437
    }
438
2787
}
439

            
440
#[derive(Clone, Debug)]
441
struct NanosRange(RangeInclusive<Nanos>);
442
3512
#[derive(Debug, Clone, Copy, Ord, PartialOrd, Eq, PartialEq)]
443
struct Nanos(u64);
444

            
445
impl ValueFormatter<Nanos> for NanosRange {
446
1680
    fn format(value: &Nanos) -> String {
447
1680
        format_nanoseconds(value.0 as f64)
448
1680
    }
449
}
450

            
451
impl Ranged for NanosRange {
452
    type ValueType = Nanos;
453
    type FormatOption = NoDefaultFormatting;
454

            
455
48521
    fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> i32 {
456
48521
        let limited_size = limit.1 - limit.0;
457
48521
        let full_size = self.0.end().0 + 1 - self.0.start().0;
458
48521
        let normalized_offset = value.0.saturating_sub(self.0.start().0) as f64 / full_size as f64;
459
48521
        limit.0 + (normalized_offset * limited_size as f64) as i32
460
48521
    }
461

            
462
336
    fn key_points<Hint: plotters::coord::ranged1d::KeyPointHint>(
463
336
        &self,
464
336
        hint: Hint,
465
336
    ) -> Vec<Self::ValueType> {
466
336
        let total_range = self.0.end().0 - self.0.start().0;
467
336
        let num_points = hint.max_num_points();
468
336
        let mut important_points = Vec::with_capacity(num_points);
469
336
        important_points.push(*self.0.start());
470
336
        if num_points > 2 {
471
336
            let steps = num_points - 2;
472
336
            let step_size = total_range as f64 / steps as f64;
473
336
            important_points.extend(
474
336
                (1..num_points - 1)
475
17808
                    .map(|step| Nanos(self.0.start().0 + (step as f64 * step_size) as u64)),
476
336
            );
477
336
        }
478
336
        important_points.push(*self.0.end());
479
336

            
480
336
        important_points
481
336
    }
482

            
483
    fn range(&self) -> std::ops::Range<Self::ValueType> {
484
        Nanos(self.0.start().0)..Nanos(self.0.end().0 + 1)
485
    }
486
}
487

            
488
impl DiscreteRanged for NanosRange {
489
    fn size(&self) -> usize {
490
        (self.0.end().0 - self.0.start().0) as usize
491
    }
492

            
493
    fn index_of(&self, value: &Self::ValueType) -> Option<usize> {
494
        if value.0 <= self.0.end().0 {
495
            if let Some(index) = value.0.checked_sub(self.0.start().0) {
496
                return Some(index as usize);
497
            }
498
        }
499
        None
500
    }
501

            
502
    fn from_index(&self, index: usize) -> Option<Self::ValueType> {
503
        Some(Nanos(self.0.start().0 + index as u64))
504
    }
505
}
506

            
507
4
fn stats_thread(
508
4
    label: String,
509
4
    metric_receiver: flume::Receiver<(&'static str, Metric, Duration)>,
510
4
    number_of_plans: usize,
511
4
    number_of_agents: usize,
512
4
    initial_data: &InitialDataSet,
513
4
    plot_dir: &Path,
514
4
    tera: &Tera,
515
4
) -> BTreeMap<&'static str, Duration> {
516
4
    let mut all_results: BTreeMap<Metric, BTreeMap<&'static str, Vec<u64>>> = BTreeMap::new();
517
4
    let mut accumulated_label_stats: BTreeMap<&'static str, Duration> = BTreeMap::new();
518
4
    let mut longest_by_metric = HashMap::new();
519
26589
    while let Ok((label, metric, duration)) = metric_receiver.recv() {
520
26585
        let metric_results = all_results.entry(metric).or_default();
521
26585
        let label_results = metric_results.entry(label).or_default();
522
26585
        let nanos = u64::try_from(duration.as_nanos()).unwrap();
523
26585
        label_results.push(nanos);
524
26585
        let label_duration = accumulated_label_stats.entry(label).or_default();
525
26585
        longest_by_metric
526
26585
            .entry(metric)
527
26585
            .and_modify(|existing: &mut Duration| {
528
26557
                *existing = (*existing).max(duration);
529
26585
            })
530
26585
            .or_insert(duration);
531
26585
        *label_duration += duration;
532
26585
    }
533

            
534
4
    let mut operations = BTreeMap::new();
535
4
    let mut metric_ranges = HashMap::new();
536
32
    for (metric, label_metrics) in all_results {
537
28
        let label_stats = label_metrics
538
28
            .iter()
539
140
            .map(|(label, stats)| {
540
140
                let mut sum = 0;
541
140
                let mut min = u64::MAX;
542
140
                let mut max = 0;
543
26725
                for &nanos in stats {
544
26585
                    sum += nanos;
545
26585
                    min = min.min(nanos);
546
26585
                    max = max.max(nanos);
547
26585
                }
548
140
                let average = sum as f64 / stats.len() as f64;
549
140
                let stddev = stddev(stats, average);
550
140

            
551
140
                let mut outliers = Vec::new();
552
140
                let mut plottable_stats = Vec::new();
553
140
                let mut min_plottable = u64::MAX;
554
140
                let mut max_plottable = 0;
555
26725
                for &nanos in stats {
556
26585
                    let diff = (nanos as f64 - average).abs();
557
26585
                    let diff_magnitude = diff / stddev;
558
26585
                    if stats.len() == 1 || diff_magnitude < 3. {
559
26252
                        plottable_stats.push(nanos);
560
26252
                        min_plottable = min_plottable.min(nanos);
561
26252
                        max_plottable = max_plottable.max(nanos);
562
26252
                    } else {
563
333
                        // Outlier
564
333
                        outliers.push(diff_magnitude);
565
333
                    }
566
                }
567

            
568
140
                if !outliers.is_empty() {
569
98
                    eprintln!("Not plotting {} outliers for {}", outliers.len(), label);
570
98
                }
571

            
572
140
                metric_ranges
573
140
                    .entry(metric)
574
140
                    .and_modify(|range: &mut Range<u64>| {
575
112
                        range.start = range.start.min(min_plottable);
576
112
                        range.end = range.end.max(max_plottable + 1);
577
140
                    })
578
140
                    .or_insert(min_plottable..max_plottable + 1);
579
140

            
580
140
                (
581
140
                    label,
582
140
                    MetricStats {
583
140
                        average,
584
140
                        min,
585
140
                        max,
586
140
                        stddev,
587
140
                        outliers,
588
140
                        plottable_stats,
589
140
                    },
590
140
                )
591
140
            })
592
28
            .collect::<BTreeMap<_, _>>();
593
28
        println!(
594
28
            "{:?}: {} operations",
595
28
            metric,
596
28
            label_metrics.values().next().unwrap().len()
597
28
        );
598
28
        cli_table::print_stdout(
599
28
            label_metrics
600
28
                .iter()
601
140
                .map(|(label, stats)| {
602
140
                    let average = stats.iter().sum::<u64>() as f64 / stats.len() as f64;
603
140
                    let min = *stats.iter().min().unwrap() as f64;
604
140
                    let max = *stats.iter().max().unwrap() as f64;
605
140
                    let stddev = stddev(stats, average);
606
140

            
607
140
                    vec![
608
140
                        label.cell(),
609
140
                        format_nanoseconds(average).cell(),
610
140
                        format_nanoseconds(min).cell(),
611
140
                        format_nanoseconds(max).cell(),
612
140
                        format_nanoseconds(stddev).cell(),
613
140
                    ]
614
140
                })
615
28
                .table()
616
28
                .title(vec![
617
28
                    "Backend".cell(),
618
28
                    "Avg".cell(),
619
28
                    "Min".cell(),
620
28
                    "Max".cell(),
621
28
                    "StdDev".cell(),
622
28
                ]),
623
28
        )
624
28
        .unwrap();
625
28

            
626
28
        let mut label_chart_data = BTreeMap::new();
627
140
        for (label, metrics) in label_stats.iter() {
628
140
            let report = operations.entry(metric).or_insert_with(|| MetricSummary {
629
28
                metric,
630
28
                invocations: label_metrics.values().next().unwrap().len(),
631
28
                description: metric.description().to_string(),
632
28
                summaries: Vec::new(),
633
140
            });
634
140
            report.summaries.push(OperationSummary {
635
140
                backend: label.to_string(),
636
140
                avg: format_nanoseconds(metrics.average),
637
140
                min: format_nanoseconds(metrics.min as f64),
638
140
                max: format_nanoseconds(metrics.max as f64),
639
140
                stddev: format_nanoseconds(metrics.stddev),
640
140
                outliers: metrics.outliers.len().to_string(),
641
140
            });
642
140
            let mut histogram = vec![0; 63];
643
140
            let range = &metric_ranges[&metric];
644
140
            let bucket_width = range.end / (histogram.len() as u64 - 1);
645
26392
            for &nanos in &metrics.plottable_stats {
646
26252
                let bucket = (nanos / bucket_width) as usize;
647
26252
                histogram[bucket] += 1;
648
26252
            }
649
140
            let chart_data = HistogramBars {
650
140
                bars: histogram
651
140
                    .iter()
652
140
                    .enumerate()
653
8820
                    .filter_map(|(bucket, &count)| {
654
8820
                        if count > 0 {
655
1728
                            let bucket_value = bucket as u64 * bucket_width;
656
1728

            
657
1728
                            Some(HistogramBar::new(
658
1728
                                (Nanos(bucket_value), count),
659
1728
                                bucket_width,
660
1728
                                label_to_color(label),
661
1728
                            ))
662
                        } else {
663
7092
                            None
664
                        }
665
8820
                    })
666
140
                    .collect::<Vec<_>>(),
667
140
            };
668
140
            label_chart_data.insert(label, chart_data);
669
        }
670
28
        let highest_count = Iterator::max(
671
28
            label_chart_data
672
28
                .values()
673
1728
                .flat_map(|chart_data| chart_data.bars.iter().map(|bar| bar.upper_left.1)),
674
28
        )
675
28
        .unwrap();
676
28
        let highest_nanos = Iterator::max(
677
28
            label_chart_data
678
28
                .values()
679
1728
                .flat_map(|chart_data| chart_data.bars.iter().map(|bar| bar.lower_right.0)),
680
28
        )
681
28
        .unwrap();
682
28
        let lowest_nanos = Iterator::min(
683
28
            label_chart_data
684
28
                .values()
685
1728
                .flat_map(|chart_data| chart_data.bars.iter().map(|bar| bar.upper_left.0)),
686
28
        )
687
28
        .unwrap();
688
168
        for (label, chart_data) in label_chart_data {
689
140
            println!("Plotting {}: {:?}", label, metric);
690
140
            let chart_path = plot_dir.join(format!("{}-{:?}.png", label, metric));
691
140
            let chart_root = BitMapBackend::new(&chart_path, (800, 240)).into_drawing_area();
692
140
            chart_root.fill(&BACKGROUND_COLOR).unwrap();
693
140
            let mut chart = ChartBuilder::on(&chart_root)
694
140
                .caption(
695
140
                    format!("{}: {:?}", label, metric),
696
140
                    ("sans-serif", 30., &TEXT_COLOR),
697
140
                )
698
140
                .margin_left(10)
699
140
                .margin_right(50)
700
140
                .margin_bottom(10)
701
140
                .x_label_area_size(50)
702
140
                .y_label_area_size(80)
703
140
                .build_cartesian_2d(
704
140
                    NanosRange(lowest_nanos..=highest_nanos),
705
140
                    0..highest_count + 1,
706
140
                )
707
140
                .unwrap();
708
140

            
709
140
            chart
710
140
                .configure_mesh()
711
140
                .disable_x_mesh()
712
140
                .y_desc("Count")
713
140
                .x_desc("Execution Time")
714
140
                .axis_desc_style(("sans-serif", 15, &TEXT_COLOR))
715
140
                .x_label_style(&TEXT_COLOR)
716
140
                .y_label_style(&TEXT_COLOR)
717
140
                .light_line_style(&TEXT_COLOR.mix(0.1))
718
140
                .bold_line_style(&TEXT_COLOR.mix(0.3))
719
140
                .draw()
720
140
                .unwrap();
721
140

            
722
140
            chart.draw_series(chart_data).unwrap();
723
140
            chart_root.present().unwrap();
724
140
        }
725
28
        let label_lines = label_metrics
726
28
            .iter()
727
140
            .map(|(label, stats)| {
728
140
                let mut running_data = Vec::new();
729
140
                let mut elapsed = 0;
730
26585
                for (index, &nanos) in stats.iter().enumerate() {
731
26585
                    elapsed += nanos;
732
26585
                    running_data.push((index, Nanos(elapsed)));
733
26585
                }
734
140
                (label, running_data)
735
140
            })
736
28
            .collect::<BTreeMap<_, _>>();
737
28
        let metric_chart_path = plot_dir.join(format!("{:?}.png", metric));
738
28
        let metric_chart_root =
739
28
            BitMapBackend::new(&metric_chart_path, (800, 480)).into_drawing_area();
740
28
        metric_chart_root.fill(&BACKGROUND_COLOR).unwrap();
741
28
        let mut metric_chart = ChartBuilder::on(&metric_chart_root)
742
28
            .caption(format!("{:?}", metric), ("sans-serif", 30., &TEXT_COLOR))
743
28
            .margin_left(10)
744
28
            .margin_right(50)
745
28
            .margin_bottom(10)
746
28
            .x_label_area_size(50)
747
28
            .y_label_area_size(80)
748
28
            .build_cartesian_2d(
749
28
                0..label_lines
750
28
                    .iter()
751
140
                    .map(|(_, data)| data.len())
752
28
                    .max()
753
28
                    .unwrap(),
754
28
                NanosRange(
755
28
                    Nanos(0)
756
28
                        ..=label_lines
757
28
                            .iter()
758
140
                            .map(|(_, stats)| stats.last().unwrap().1)
759
28
                            .max()
760
28
                            .unwrap(),
761
28
                ),
762
28
            )
763
28
            .unwrap();
764
28

            
765
28
        metric_chart
766
28
            .configure_mesh()
767
28
            .disable_x_mesh()
768
28
            .x_desc("Invocations")
769
28
            .y_desc("Accumulated Execution Time")
770
28
            .axis_desc_style(("sans-serif", 15, &TEXT_COLOR))
771
28
            .x_label_style(&TEXT_COLOR)
772
28
            .y_label_style(&TEXT_COLOR)
773
28
            .light_line_style(&TEXT_COLOR.mix(0.1))
774
28
            .bold_line_style(&TEXT_COLOR.mix(0.3))
775
28
            .draw()
776
28
            .unwrap();
777

            
778
168
        for (label, data) in label_lines {
779
140
            metric_chart
780
140
                .draw_series(LineSeries::new(data.into_iter(), &label_to_color(label)))
781
140
                .unwrap()
782
140
                .label(label.to_string())
783
140
                .legend(|(x, y)| {
784
140
                    PathElement::new(vec![(x, y), (x + 20, y)], &label_to_color(label))
785
140
                });
786
140
        }
787
28
        metric_chart
788
28
            .configure_series_labels()
789
28
            .border_style(&TEXT_COLOR)
790
28
            .background_style(&BACKGROUND_COLOR)
791
28
            .label_font(&TEXT_COLOR)
792
28
            .position(SeriesLabelPosition::UpperLeft)
793
28
            .draw()
794
28
            .unwrap();
795
28
        metric_chart_root.present().unwrap();
796
    }
797
4
    cli_table::print_stdout(
798
4
        accumulated_label_stats
799
4
            .iter()
800
20
            .map(|(label, duration)| {
801
20
                vec![
802
20
                    label.cell(),
803
20
                    format_nanoseconds(duration.as_nanos() as f64).cell(),
804
20
                ]
805
20
            })
806
4
            .table()
807
4
            .title(vec![
808
4
                "Backend".cell(),
809
4
                format!("Total Execution Time across {} agents", number_of_agents).cell(),
810
4
            ]),
811
4
    )
812
4
    .unwrap();
813
4
    BenchmarkSummary {
814
4
        label,
815
4
        timestamp: current_timestamp_string(),
816
4
        revision: local_git_rev(),
817
4
        plan_count: number_of_plans,
818
4
        agent_count: number_of_agents,
819
4
        product_count: initial_data.products.len(),
820
4
        category_count: initial_data.categories.len(),
821
4
        customer_count: initial_data.customers.len(),
822
4
        order_count: initial_data.orders.len(),
823
4
        summaries: accumulated_label_stats
824
4
            .iter()
825
20
            .map(|(&backend, duration)| BackendSummary {
826
20
                backend: backend.to_string(),
827
20
                transport: match backend {
828
20
                    "bonsaidb-local" => String::from("None"),
829
16
                    "bonsaidb-quic" => String::from("UDP with TLS"),
830
12
                    _ => String::from("TCP"),
831
                },
832
20
                total_time: format_nanoseconds(duration.as_nanos() as f64),
833
20
                wall_time: format_nanoseconds(duration.as_nanos() as f64 / number_of_agents as f64),
834
20
            })
835
4
            .collect(),
836
28
        operations: operations.into_iter().map(|(_k, v)| v).collect(),
837
4
    }
838
4
    .render_to(&plot_dir.join("index.html"), tera);
839
4
    accumulated_label_stats
840
4
}
841

            
842
struct MetricStats {
843
    average: f64,
844
    min: u64,
845
    max: u64,
846
    stddev: f64,
847
    plottable_stats: Vec<u64>,
848
    outliers: Vec<f64>,
849
}
850

            
851
struct HistogramBars {
852
    bars: Vec<HistogramBar>,
853
}
854

            
855
struct HistogramBar {
856
    upper_left: (Nanos, u64),
857
    lower_right: (Nanos, u64),
858

            
859
    color: RGBColor,
860
}
861

            
862
impl HistogramBar {
863
1728
    pub fn new(coord: (Nanos, u64), width: u64, color: RGBColor) -> Self {
864
1728
        Self {
865
1728
            upper_left: (Nanos(coord.0 .0), coord.1),
866
1728
            lower_right: (Nanos(coord.0 .0 + width), 0),
867
1728
            color,
868
1728
        }
869
1728
    }
870
}
871

            
872
impl<'a> Drawable<BitMapBackend<'a>> for HistogramBar {
873
1728
    fn draw<I: Iterator<Item = <BackendCoordOnly as CoordMapper>::Output>>(
874
1728
        &self,
875
1728
        mut pos: I,
876
1728
        backend: &mut BitMapBackend,
877
1728
        _parent_dim: (u32, u32),
878
1728
    ) -> Result<(), DrawingErrorKind<<BitMapBackend as DrawingBackend>::ErrorType>> {
879
1728
        let upper_left = pos.next().unwrap();
880
1728
        let lower_right = pos.next().unwrap();
881
1728
        backend.draw_rect(upper_left, lower_right, &self.color, true)?;
882

            
883
1728
        Ok(())
884
1728
    }
885
}
886

            
887
impl<'a> PointCollection<'a, (Nanos, u64)> for &'a HistogramBar {
888
    type Point = &'a (Nanos, u64);
889

            
890
    type IntoIter = HistogramBarIter<'a>;
891

            
892
1728
    fn point_iter(self) -> Self::IntoIter {
893
1728
        HistogramBarIter::UpperLeft(self)
894
1728
    }
895
}
896

            
897
enum HistogramBarIter<'a> {
898
    UpperLeft(&'a HistogramBar),
899
    LowerRight(&'a HistogramBar),
900
    Done,
901
}
902

            
903
impl<'a> Iterator for HistogramBarIter<'a> {
904
    type Item = &'a (Nanos, u64);
905

            
906
3456
    fn next(&mut self) -> Option<Self::Item> {
907
3456
        let (next, result) = match self {
908
1728
            HistogramBarIter::UpperLeft(bar) => {
909
1728
                (HistogramBarIter::LowerRight(bar), Some(&bar.upper_left))
910
            }
911
1728
            HistogramBarIter::LowerRight(bar) => (HistogramBarIter::Done, Some(&bar.lower_right)),
912
            HistogramBarIter::Done => (HistogramBarIter::Done, None),
913
        };
914
3456
        *self = next;
915
3456
        result
916
3456
    }
917
}
918

            
919
impl IntoIterator for HistogramBars {
920
    type Item = HistogramBar;
921

            
922
    type IntoIter = std::vec::IntoIter<HistogramBar>;
923

            
924
140
    fn into_iter(self) -> Self::IntoIter {
925
140
        self.bars.into_iter()
926
140
    }
927
}
928

            
929
280
fn stddev(data: &[u64], average: f64) -> f64 {
930
280
    if data.is_empty() {
931
        0.
932
    } else {
933
280
        let variance = data
934
280
            .iter()
935
53170
            .map(|value| {
936
53170
                let diff = average - (*value as f64);
937
53170

            
938
53170
                diff * diff
939
53170
            })
940
280
            .sum::<f64>()
941
280
            / data.len() as f64;
942
280

            
943
280
        variance.sqrt()
944
    }
945
280
}