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
15708
                )
140
15708
                .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
64
            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
27325
        for step in &plan.operations {
189
51341
            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
27325
    async fn operate(
213
27325
        &mut self,
214
27325
        operation: &Operation,
215
27325
        results: &[OperationResult],
216
27325
        measurements: &Measurements,
217
27325
    ) -> OperationResult {
218
27324
        match operation {
219
17209
            Operation::FindProduct(op) => self.operate(op, results, measurements).await,
220
15520
            Operation::LookupProduct(op) => self.operate(op, results, measurements).await,
221
3713
            Operation::CreateCart(op) => self.operate(op, results, measurements).await,
222
12172
            Operation::AddProductToCart(op) => self.operate(op, results, measurements).await,
223
1105
            Operation::RateProduct(op) => self.operate(op, results, measurements).await,
224
1621
            Operation::Checkout(op) => self.operate(op, results, measurements).await,
225
        }
226
54649
    }
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
27345
    pub fn begin(&self, label: &'static str, metric: Metric) -> Measurement<'_> {
361
27345
        Measurement {
362
27345
            target: &self.sender,
363
27345
            label,
364
27345
            metric,
365
27345
            start: Instant::now(),
366
27345
        }
367
27345
    }
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
27345
    pub fn finish(self) {
379
27345
        let duration = Instant::now()
380
27345
            .checked_duration_since(self.start)
381
27345
            .expect("time went backwards. Restart benchmarks.");
382
27345
        self.target
383
27345
            .send((self.label, self.metric, duration))
384
27345
            .unwrap();
385
27345
    }
386
}
387

            
388
87298
#[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
68
        String::from("0s")
416
2792
    } else if nanoseconds < 1_000. {
417
        format_float(nanoseconds, "ns")
418
2792
    } else if nanoseconds < 1_000_000. {
419
282
        format_float(nanoseconds / 1_000., "us")
420
2510
    } else if nanoseconds < 1_000_000_000. {
421
2369
        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
2792
fn format_float(value: f64, suffix: &str) -> String {
431
2792
    if value < 10. {
432
1433
        format!("{:.3}{}", value, suffix)
433
1359
    } else if value < 100. {
434
604
        format!("{:.2}{}", value, suffix)
435
    } else {
436
755
        format!("{:.1}{}", value, suffix)
437
    }
438
2792
}
439

            
440
#[derive(Clone, Debug)]
441
struct NanosRange(RangeInclusive<Nanos>);
442
3624
#[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
49393
    fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> i32 {
456
49393
        let limited_size = limit.1 - limit.0;
457
49393
        let full_size = self.0.end().0 + 1 - self.0.start().0;
458
49393
        let normalized_offset = value.0.saturating_sub(self.0.start().0) as f64 / full_size as f64;
459
49393
        limit.0 + (normalized_offset * limited_size as f64) as i32
460
49393
    }
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
27349
    while let Ok((label, metric, duration)) = metric_receiver.recv() {
520
27345
        let metric_results = all_results.entry(metric).or_default();
521
27345
        let label_results = metric_results.entry(label).or_default();
522
27345
        let nanos = u64::try_from(duration.as_nanos()).unwrap();
523
27345
        label_results.push(nanos);
524
27345
        let label_duration = accumulated_label_stats.entry(label).or_default();
525
27345
        longest_by_metric
526
27345
            .entry(metric)
527
27345
            .and_modify(|existing: &mut Duration| {
528
27317
                *existing = (*existing).max(duration);
529
27345
            })
530
27345
            .or_insert(duration);
531
27345
        *label_duration += duration;
532
27345
    }
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
27485
                for &nanos in stats {
544
27345
                    sum += nanos;
545
27345
                    min = min.min(nanos);
546
27345
                    max = max.max(nanos);
547
27345
                }
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
27485
                for &nanos in stats {
556
27345
                    let diff = (nanos as f64 - average).abs();
557
27345
                    let diff_magnitude = diff / stddev;
558
27345
                    if stats.len() == 1 || diff_magnitude < 3. {
559
26996
                        plottable_stats.push(nanos);
560
26996
                        min_plottable = min_plottable.min(nanos);
561
26996
                        max_plottable = max_plottable.max(nanos);
562
26996
                    } else {
563
349
                        // Outlier
564
349
                        outliers.push(diff_magnitude);
565
349
                    }
566
                }
567

            
568
140
                if !outliers.is_empty() {
569
100
                    eprintln!("Not plotting {} outliers for {}", outliers.len(), label);
570
100
                }
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
27136
            for &nanos in &metrics.plottable_stats {
646
26996
                let bucket = (nanos / bucket_width) as usize;
647
26996
                histogram[bucket] += 1;
648
26996
            }
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
1784
                            let bucket_value = bucket as u64 * bucket_width;
656
1784

            
657
1784
                            Some(HistogramBar::new(
658
1784
                                (Nanos(bucket_value), count),
659
1784
                                bucket_width,
660
1784
                                label_to_color(label),
661
1784
                            ))
662
                        } else {
663
7036
                            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
1784
                .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
1784
                .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
1784
                .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
27345
                for (index, &nanos) in stats.iter().enumerate() {
731
27345
                    elapsed += nanos;
732
27345
                    running_data.push((index, Nanos(elapsed)));
733
27345
                }
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
1784
    pub fn new(coord: (Nanos, u64), width: u64, color: RGBColor) -> Self {
864
1784
        Self {
865
1784
            upper_left: (Nanos(coord.0 .0), coord.1),
866
1784
            lower_right: (Nanos(coord.0 .0 + width), 0),
867
1784
            color,
868
1784
        }
869
1784
    }
870
}
871

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

            
883
1784
        Ok(())
884
1784
    }
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
1784
    fn point_iter(self) -> Self::IntoIter {
893
1784
        HistogramBarIter::UpperLeft(self)
894
1784
    }
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
3568
    fn next(&mut self) -> Option<Self::Item> {
907
3568
        let (next, result) = match self {
908
1784
            HistogramBarIter::UpperLeft(bar) => {
909
1784
                (HistogramBarIter::LowerRight(bar), Some(&bar.upper_left))
910
            }
911
1784
            HistogramBarIter::LowerRight(bar) => (HistogramBarIter::Done, Some(&bar.lower_right)),
912
            HistogramBarIter::Done => (HistogramBarIter::Done, None),
913
        };
914
3568
        *self = next;
915
3568
        result
916
3568
    }
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
54690
            .map(|value| {
936
54690
                let diff = average - (*value as f64);
937
54690

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

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