1use std::{
2 collections::HashMap,
3 fs::{self, File},
4 io::{BufReader, BufWriter},
5 ops::Sub,
6 path::{Path, PathBuf},
7 process::Command,
8 time::Duration,
9};
10
11use chrono::{DateTime, Duration as ChronoDuration, NaiveDateTime, Utc};
12use getset::{Getters, MutGetters, Setters};
13use serde::{Deserialize, Serialize};
14use tempfile::NamedTempFile;
15use url::Url;
16
17use crate::{
18 benchmark::{Benchmark, BenchmarkBuilder},
19 error::WrkError,
20 result::{Deviation, WrkResult, WrkResultBuilder},
21 Gnuplot, LuaScript, Result,
22};
23
24const DATE_FORMAT: &str = "%Y-%m-%d-%H:%M:%S-%z";
25
26#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
27pub enum HistoryPeriod {
28 Last,
29 Hour,
30 Day,
31 Week,
32 Month,
33 Forever,
34}
35
36impl Default for HistoryPeriod {
37 fn default() -> Self {
38 HistoryPeriod::Last
39 }
40}
41
42impl HistoryPeriod {
43 pub fn last_valid_datapoint(&self) -> DateTime<Utc> {
44 let now = Utc::now();
45 match self {
46 Self::Last => now,
47 Self::Hour => now.sub(ChronoDuration::hours(1)),
48 Self::Day => now.sub(ChronoDuration::days(1)),
49 Self::Week => now.sub(ChronoDuration::weeks(1)),
50 Self::Month => now.sub(ChronoDuration::weeks(4)),
51 Self::Forever => DateTime::from_utc(NaiveDateTime::from_timestamp(1, 0), Utc),
52 }
53 }
54}
55
56pub type Benchmarks = Vec<WrkResult>;
57pub type Headers = HashMap<String, String>;
58
59#[derive(Debug, Clone, Serialize, Deserialize, Getters, Setters, MutGetters, Builder)]
61pub struct Wrk {
62 #[getset(get = "pub", set = "pub", get_mut = "pub")]
65 url: String,
66 #[serde(skip)]
68 #[builder(default = "1")]
69 #[getset(get = "pub", set = "pub", get_mut = "pub")]
70 timeout: u8,
71 #[builder(default)]
73 #[getset(get = "pub", set = "pub", get_mut = "pub")]
74 benchmarks: Benchmarks,
75 #[builder(default)]
77 #[getset(get = "pub", set = "pub", get_mut = "pub")]
78 benchmarks_history: Benchmarks,
79 #[builder(default = "Path::new(\".\").join(\".wrk-api-bench\")")]
81 #[getset(get = "pub", set = "pub", get_mut = "pub")]
82 history_dir: PathBuf,
83 #[builder(default)]
87 #[getset(get = "pub", set = "pub", get_mut = "pub")]
88 user_script: Option<PathBuf>,
89 #[builder(default)]
91 #[getset(get = "pub", set = "pub", get_mut = "pub")]
92 headers: Headers,
93 #[builder(default = "String::from(\"GET\")")]
95 #[getset(get = "pub", set = "pub", get_mut = "pub")]
96 method: String,
97 #[builder(default)]
99 #[getset(get = "pub", set = "pub", get_mut = "pub")]
100 body: String,
101 #[builder(default = "2")]
103 #[getset(get = "pub", set = "pub", get_mut = "pub")]
104 max_error_percentage: u8,
105 #[serde(skip)]
107 #[builder(default)]
108 #[getset(get = "pub", set = "pub", get_mut = "pub")]
109 benchmark_date: Option<DateTime<Utc>>,
110}
111
112impl Wrk {
113 fn wrk_args(&self, benchmark: &Benchmark, url: &Url, lua_script: &Path) -> Result<Vec<String>> {
114 Ok(vec![
115 "-t".to_string(),
116 benchmark.threads().to_string(),
117 "-c".to_string(),
118 benchmark.connections().to_string(),
119 "-d".to_string(),
120 format!("{}s", benchmark.duration().as_secs()),
121 "--timeout".to_string(),
122 format!("{}s", self.timeout()),
123 "-s".to_string(),
124 lua_script.to_string_lossy().to_string(),
125 url.to_string(),
126 ])
127 }
128
129 fn wrk_result(&self, wrk_json: &str) -> WrkResult {
130 match serde_json::from_str::<WrkResult>(wrk_json) {
131 Ok(mut run) => {
132 let error_percentage = run.errors() / 100.0 * run.requests();
133 if error_percentage < *self.max_error_percentage() as f64 {
134 *run.success_mut() = true;
135 } else {
136 error!(
137 "Errors percentage is {}%, which is more than {}%",
138 error_percentage, self.max_error_percentage
139 );
140 }
141 run
142 }
143 Err(e) => {
144 error!("Wrk JSON result deserialize failed: {}", e);
145 WrkResult::fail(e.to_string())
146 }
147 }
148 }
149
150 pub fn bench(&mut self, benchmarks: &Vec<Benchmark>) -> Result<()> {
151 if !self.history_dir().exists() {
152 fs::create_dir(self.history_dir()).unwrap_or_else(|e| {
153 error!(
154 "Unable to create storage dir {}: {}. Statistics calculation could be impaired",
155 self.history_dir().display(),
156 e
157 );
158 });
159 }
160 let date = Utc::now();
161 *self.benchmark_date_mut() = Some(date);
162 let url = Url::parse(self.url())?;
163 let mut script_file = NamedTempFile::new()?;
164 LuaScript::render(
165 &mut script_file,
166 self.user_script().as_ref(),
167 url.path(),
168 self.method(),
169 self.headers(),
170 self.body(),
171 )?;
172 for benchmark in benchmarks {
173 let mut run = match Command::new("wrk")
174 .args(self.wrk_args(benchmark, &url, script_file.path())?)
175 .output()
176 {
177 Ok(wrk) => {
178 let output = String::from_utf8_lossy(&wrk.stdout);
179 let error = String::from_utf8_lossy(&wrk.stderr);
180 if wrk.status.success() {
181 debug!("Wrk execution succeded:\n{}", output);
182 let wrk_json = output
183 .split("JSON")
184 .nth(1)
185 .ok_or_else(|| WrkError::Lua("Wrk returned empty JSON".to_string()))?;
186 self.wrk_result(wrk_json)
187 } else {
188 error!("Wrk execution failed.\nOutput: {}\nError: {}", output, error);
189 WrkResult::fail(error.to_string())
190 }
191 }
192 Err(e) => {
193 error!("Wrk execution failed: {}", e);
194 WrkResult::fail(e.to_string())
195 }
196 };
197 *run.date_mut() = date;
198 *run.benchmark_mut() = benchmark.clone();
199 self.benchmarks_mut().push(run);
200 }
201 script_file.keep()?;
202 self.dump(date)?;
203 Ok(())
204 }
205
206 pub fn bench_exponential(&mut self, duration: Option<Duration>) -> Result<()> {
207 self.bench(&BenchmarkBuilder::exponential(duration))?;
208 Ok(())
209 }
210
211 fn dump(&self, date: DateTime<Utc>) -> Result<()> {
212 let filename = format!("result.{}.json", date.format(DATE_FORMAT));
213 let file = File::create(self.history_dir().join(&filename))?;
214 let writer = BufWriter::new(file);
215 println!("Writing current benchmark to {}", filename);
216 serde_json::to_writer(writer, &self.benchmarks())?;
217 Ok(())
218 }
219
220 fn load(&mut self, period: HistoryPeriod, best: bool) -> Result<()> {
221 if !self.history_dir().exists() {
222 fs::create_dir(self.history_dir())?;
223 }
224 let mut paths: Vec<_> = fs::read_dir(self.history_dir())?.map(|r| r.unwrap()).collect();
225 paths.sort_by_key(|dir| {
226 let metadata = fs::metadata(dir.path()).unwrap();
227 metadata.modified().unwrap()
228 });
229 let mut history = Benchmarks::new();
230 if period == HistoryPeriod::Last {
231 let file = File::open(paths.pop().unwrap().path())?;
232 let mut reader = BufReader::new(file);
233 history = serde_json::from_reader(&mut reader)?;
234 let benchmark = history.pop().unwrap();
235 if let Some(benchmark_date) = self.benchmark_date() {
236 if benchmark_date == benchmark.date() && !paths.is_empty() {
237 let file = File::open(paths.pop().unwrap().path())?;
238 let mut reader = BufReader::new(file);
239 history = serde_json::from_reader(&mut reader)?;
240 if best {
241 let best = self.best_benchmark(&history)?;
242 history = vec![best];
243 }
244 } else {
245 return Err(WrkError::History(
246 "Unable to load history with a single measurement".to_string(),
247 ));
248 }
249 }
250 } else {
251 for path in paths {
252 if let Some(date_str) = path.file_name().to_string_lossy().split('.').nth(1) {
253 let date = DateTime::parse_from_str(date_str, DATE_FORMAT)?;
254 if date >= period.last_valid_datapoint() {
255 let file = File::open(path.path())?;
256 let mut reader = BufReader::new(file);
257 let mut benchmarks: Vec<_> = serde_json::from_reader(&mut reader)?;
258 benchmarks.retain(|x| !self.benchmarks_history().contains(x));
259 if best {
260 let best = self.best_benchmark(&benchmarks)?;
261 history.push(best);
262 } else {
263 history.append(&mut benchmarks);
264 }
265 }
266 }
267 }
268 }
269 *self.benchmarks_history_mut() = history;
270 Ok(())
271 }
272
273 fn best_benchmark(&self, benchmarks: &Benchmarks) -> Result<WrkResult> {
274 let best = benchmarks.iter().filter(|v| *v.success()).max_by(|a, b| {
275 (*a.requests_sec() as i64)
276 .cmp(&(*b.requests_sec() as i64))
277 .then((*a.successes() as i64).cmp(&(*b.successes() as i64)))
278 .then((*a.requests() as i64).cmp(&(*b.requests() as i64)))
279 .then((*a.requests() as i64).cmp(&(*b.requests() as i64)))
280 .then((*a.transfer_mb() as i64).cmp(&(*b.transfer_mb() as i64)))
281 });
282 best.cloned().ok_or_else(|| {
283 WrkError::Stats(format!(
284 "Unable to calculate best in a set of {} elements",
285 benchmarks.len()
286 ))
287 })
288 }
289
290 fn best(&self) -> Result<WrkResult> {
291 self.best_benchmark(self.benchmarks())
292 }
293
294 fn historical_best(&self) -> Result<WrkResult> {
295 self.best_benchmark(self.benchmarks_history())
296 }
297
298 pub fn all_benchmarks(&self) -> Benchmarks {
299 let mut history = self.benchmarks_history().clone();
300 history.append(&mut self.benchmarks().clone());
301 history
302 }
303
304 pub fn deviation(&mut self, period: HistoryPeriod) -> Result<Deviation> {
305 self.load(period, false)?;
306 let new = self.best()?;
307 let old = self.historical_best()?;
308 Ok(Deviation::new(new, old))
309 }
310
311 pub fn plot(&self, title: &str, output: &Path, benchmarks: &Benchmarks) -> Result<()> {
312 Gnuplot::new(title, output).plot(benchmarks)
313 }
314}
315
316#[cfg(test)]
317mod tests {
318 use std::{net::SocketAddr, thread, time::Duration};
319
320 use super::*;
321 use crate::benchmark::BenchmarkBuilder;
322 use axum::{
323 http::StatusCode,
324 response::IntoResponse,
325 routing::{get, post},
326 Json, Router,
327 };
328 use http::Request;
329 use hyper::Body;
330
331 async fn server() {
332 let app = Router::new().route("/", get(|| async { "Hello, world!" }));
333
334 let addr = SocketAddr::from(([127, 0, 0, 1], 13734));
335 println!("server listening on {}", addr);
336 axum::Server::bind(&addr).serve(app.into_make_service()).await.unwrap();
337 }
338
339 #[tokio::test]
340 async fn benchmark() {
341 tokio::spawn(server());
342 tokio::time::sleep(Duration::from_secs(1)).await;
343 let client = hyper::Client::new();
344
345 let response = client
346 .request(
347 Request::builder()
348 .uri("http://127.0.0.1:13734/")
349 .body(Body::empty())
350 .unwrap(),
351 )
352 .await
353 .unwrap();
354 let body = hyper::body::to_bytes(response.into_body()).await.unwrap();
355
356 let mut wrk = WrkBuilder::default()
357 .url("http://127.0.0.1:13734".to_string())
358 .build()
359 .unwrap();
360 wrk.bench(&vec![BenchmarkBuilder::default()
362 .duration(Duration::from_secs(5))
363 .build()
364 .unwrap()])
365 .unwrap();
366 }
371}