1use anyhow::{bail, Result};
3use serde::{Deserialize, Serialize};
4use std::fmt::Write;
5use std::sync::Mutex;
6
7use rd_util::*;
8
9lazy_static::lazy_static! {
10 static ref ARGS_STR: String = format!(
11 "-d, --dir=[TOPDIR] 'Top-level dir for operation and scratch files (default: {dfl_dir})'
12 -s, --scratch=[DIR] 'Scratch dir for workloads to use (default: $TOPDIR/scratch)'
13 -D, --dev=[NAME] 'Override storage device autodetection (e.g. sda, nvme0n1)'
14 -r, --rep-retention=[SECS] '1s report retention in seconds (default: {dfl_rep_ret:.1}h)'
15 -R, --rep-1min-retention=[SECS] '1m report retention in seconds (default: {dfl_rep_1m_ret:.1}h)'
16 --systemd-timeout=[SECS] 'Systemd timeout (default: {dfl_systemd_timeout})'
17 --passive=[SELS] 'Avoid system config changes (SELS=ALL/all/cpu/mem/io/fs/oomd/none)'
18 -a, --args=[FILE] 'Load base command line arguments from FILE'
19 --no-iolat 'Disable bpf-based io latency stat monitoring'
20 --force 'Ignore startup check results and proceed'
21 --force-running 'Ignore bench requirements and enter Running state'
22 --prepare 'Prepare the files and directories and exit'
23 --linux-tar=[FILE] 'Path to linux source tarball for compile sideload (__SKIP__ to skip)'
24 --bench-file=[FILE] 'Bench file name override'
25 --reset 'Reset all states except for bench results, linux.tar and testfiles'
26 --keep-reports 'Don't delete expired report files, also affects --reset'
27 --bypass 'Skip startup and periodic health checks'
28 -v... 'Sets the level of verbosity'
29 --logfile=[FILE] 'Specify file to dump logs'",
30 dfl_dir = Args::default().dir,
31 dfl_rep_ret = Args::default().rep_retention as f64 / 3600.0,
32 dfl_rep_1m_ret = Args::default().rep_1min_retention as f64 / 3600.0,
33 dfl_systemd_timeout = format_duration(Args::default().systemd_timeout),
34 );
35
36 static ref BANDIT_MEM_HOG_USAGE: String = format!(
37 "-w, --wbps=[BPS] 'Write BPS (memory growth rate, default 0)'
38 -r, --rbps=[BPS] 'Read BPS (re-read rate, default 0)'
39 -R, --readers=[NR] 'Number of readers (default: 1)'
40 -d, --debt=[DUR] 'Maximum debt accumulation (default, 10s)'
41 -c, --compressibility=[FRAC] 'Content compressibility (default: 0)
42 -p, --report=[PATH] 'Report file path'"
43 );
44
45 static ref HELP_BODY: Mutex<&'static str> = Mutex::new("");
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct EnforceConfig {
50 pub crit_mem_prot: bool,
51 pub cpu: bool,
52 pub mem: bool,
53 pub io: bool,
54 pub fs: bool,
55 pub oomd: bool,
56}
57
58impl Default for EnforceConfig {
59 fn default() -> Self {
60 Self {
61 crit_mem_prot: true,
62 cpu: true,
63 mem: true,
64 io: true,
65 fs: true,
66 oomd: true,
67 }
68 }
69}
70
71impl EnforceConfig {
72 pub fn set_all_passive(&mut self) -> &mut Self {
73 *self = Self {
74 crit_mem_prot: false,
75 cpu: false,
76 mem: false,
77 io: false,
78 fs: false,
79 oomd: false,
80 };
81 self
82 }
83
84 pub fn set_crit_mem_prot_only(&mut self) -> &mut Self {
85 self.set_all_passive().crit_mem_prot = true;
86 self
87 }
88
89 pub fn parse_and_merge(&mut self, input: &str) -> Result<()> {
90 for passive in input.split(&[',', '/'][..]) {
91 match passive {
92 "ALL" => {
93 self.set_all_passive();
94 }
95 "all" => {
96 self.set_crit_mem_prot_only();
97 }
98 "cpu" => self.cpu = false,
99 "mem" => self.mem = false,
100 "io" => self.io = false,
101 "fs" => self.fs = false,
102 "oomd" => self.oomd = false,
103 "none" => *self = Default::default(),
104 "" => {}
105 v => bail!("Unknown --passive value {:?}", &v),
106 }
107 }
108 Ok(())
109 }
110
111 pub fn to_passive_string(&self) -> String {
112 let mut buf = String::new();
113 if !self.crit_mem_prot {
114 write!(buf, "ALL").unwrap();
115 } else if !self.cpu && !self.mem && !self.io && !self.fs && !self.oomd {
116 write!(buf, "all").unwrap();
117 } else {
118 if !self.cpu {
119 write!(buf, "cpu/").unwrap();
120 }
121 if !self.mem {
122 write!(buf, "mem/").unwrap();
123 }
124 if !self.io {
125 write!(buf, "io/").unwrap();
126 }
127 if !self.fs {
128 write!(buf, "fs/").unwrap();
129 }
130 if !self.oomd {
131 write!(buf, "oomd/").unwrap();
132 }
133 buf.pop();
134 }
135 buf
136 }
137
138 pub fn all(&self) -> bool {
139 self.crit_mem_prot && self.cpu && self.mem && self.io && self.fs && self.oomd
140 }
141}
142
143#[derive(Debug, Clone, Serialize, Deserialize)]
144pub struct BanditMemHogArgs {
145 pub wbps: String,
146 pub rbps: String,
147 pub max_debt: f64,
148 pub nr_readers: u32,
149 pub comp: f64,
150 pub report: Option<String>,
151}
152
153impl Default for BanditMemHogArgs {
154 fn default() -> Self {
155 Self {
156 wbps: "0".to_owned(),
157 rbps: "0".to_owned(),
158 max_debt: 10.0,
159 nr_readers: 1,
160 comp: 0.0,
161 report: None,
162 }
163 }
164}
165
166#[derive(Debug, Clone, Serialize, Deserialize)]
167pub enum Bandit {
168 MemHog(BanditMemHogArgs),
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize)]
172#[serde(default)]
173pub struct Args {
174 pub dir: String,
175 pub scratch: Option<String>,
176 pub dev: Option<String>,
177 pub rep_retention: u64,
178 pub rep_1min_retention: u64,
179 pub systemd_timeout: f64,
180 pub enforce: EnforceConfig,
181
182 #[serde(skip)]
183 pub no_iolat: bool,
184 #[serde(skip)]
185 pub force: bool,
186 #[serde(skip)]
187 pub force_running: bool,
188 #[serde(skip)]
189 pub prepare: bool,
190 #[serde(skip)]
191 pub linux_tar: Option<String>,
192 #[serde(skip)]
193 pub bench_file: Option<String>,
194 #[serde(skip)]
195 pub reset: bool,
196 #[serde(skip)]
197 pub keep_reports: bool,
198 #[serde(skip)]
199 pub bypass: bool,
200 #[serde(skip)]
201 pub verbosity: u32,
202 #[serde(skip)]
203 pub logfile: Option<String>,
204
205 pub bandit: Option<Bandit>,
206}
207
208impl Default for Args {
209 fn default() -> Self {
210 Self {
211 dir: "/var/lib/resctl-demo".into(),
212 scratch: None,
213 dev: None,
214 rep_retention: 3600,
215 rep_1min_retention: 24 * 3600,
216 systemd_timeout: systemd::SYSTEMD_DFL_TIMEOUT,
217 enforce: Default::default(),
218 no_iolat: false,
219 force: false,
220 force_running: false,
221 prepare: false,
222 linux_tar: None,
223 bench_file: None,
224 reset: false,
225 keep_reports: false,
226 bypass: false,
227 verbosity: 0,
228 logfile: None,
229 bandit: None,
230 }
231 }
232}
233
234impl JsonLoad for Args {}
235impl JsonSave for Args {}
236
237impl Args {
238 pub fn set_help_body(help: &'static str) {
239 *HELP_BODY.lock().unwrap() = help;
240 }
241
242 fn process_bandit(&mut self, bandit: &str, subm: &clap::ArgMatches) -> bool {
243 let mut updated_base = false;
244 match bandit {
245 "bandit-mem-hog" => {
246 let mut args = match self.bandit.as_ref() {
247 Some(Bandit::MemHog(args)) => args.clone(),
248 None => Default::default(),
249 };
250 if let Some(v) = subm.value_of("wbps") {
251 args.wbps = v.to_owned();
252 updated_base = true;
253 }
254 if let Some(v) = subm.value_of("rbps") {
255 args.rbps = v.to_owned();
256 updated_base = true;
257 }
258 if let Some(v) = subm.value_of("readers") {
259 args.nr_readers = v.parse::<u32>().expect("failed to parse \"readers\"");
260 updated_base = true;
261 }
262 if let Some(v) = subm.value_of("debt") {
263 args.max_debt = parse_duration(v).expect("failed to parse \"debt\"");
264 updated_base = true;
265 }
266 if let Some(v) = subm.value_of("compressibility") {
267 args.comp = parse_frac(v).unwrap();
268 updated_base = true;
269 }
270 if let Some(v) = subm.value_of("report") {
271 args.report = if v.len() == 0 {
272 None
273 } else {
274 Some(v.to_owned())
275 };
276 updated_base = true;
277 }
278 self.bandit = Some(Bandit::MemHog(args));
279 }
280 _ => {}
281 }
282 updated_base
283 }
284}
285
286impl JsonArgs for Args {
287 fn match_cmdline() -> clap::ArgMatches<'static> {
288 clap::App::new("rd-agent")
289 .version((*super::FULL_VERSION).as_str())
290 .author(clap::crate_authors!("\n"))
291 .about(*HELP_BODY.lock().unwrap())
292 .args_from_usage(&ARGS_STR)
293 .subcommand(
294 clap::SubCommand::with_name("bandit-mem-hog")
295 .about("Bandit mode - keep bloating up memory")
296 .args_from_usage(&BANDIT_MEM_HOG_USAGE),
297 )
298 .setting(clap::AppSettings::UnifiedHelpMessage)
299 .setting(clap::AppSettings::DeriveDisplayOrder)
300 .get_matches()
301 }
302
303 fn verbosity(matches: &clap::ArgMatches) -> u32 {
304 matches.occurrences_of("v") as u32
305 }
306
307 fn log_file(matches: &clap::ArgMatches) -> String {
308 match matches.value_of("logfile") {
309 Some(v) => v.to_string(),
310 None => "".to_string(),
311 }
312 }
313
314 fn process_cmdline(&mut self, matches: &clap::ArgMatches) -> bool {
315 let dfl = Args::default();
316 let mut updated_base = false;
317
318 if let Some(v) = matches.value_of("dir") {
319 self.dir = if v.len() > 0 {
320 v.to_string()
321 } else {
322 dfl.dir.clone()
323 };
324 updated_base = true;
325 }
326 if let Some(v) = matches.value_of("scratch") {
327 self.scratch = if v.len() > 0 {
328 Some(v.to_string())
329 } else {
330 None
331 };
332 updated_base = true;
333 }
334 if let Some(v) = matches.value_of("dev") {
335 self.dev = if v.len() > 0 {
336 Some(v.to_string())
337 } else {
338 None
339 };
340 updated_base = true;
341 }
342
343 if let Some(v) = matches.value_of("rep-retention") {
344 self.rep_retention = if v.len() > 0 {
345 v.parse::<u64>().unwrap().max(0)
346 } else {
347 dfl.rep_retention
348 };
349 updated_base = true;
350 }
351
352 if let Some(v) = matches.value_of("rep-1min-retention") {
353 self.rep_1min_retention = if v.len() > 0 {
354 v.parse::<u64>().unwrap().max(0)
355 } else {
356 dfl.rep_1min_retention
357 };
358 updated_base = true;
359 }
360
361 if let Some(v) = matches.value_of("systemd-timeout") {
362 self.systemd_timeout = if v.len() > 0 {
363 parse_duration(v).unwrap().max(1.0)
364 } else {
365 dfl.systemd_timeout
366 };
367 updated_base = true;
368 }
369
370 self.no_iolat = matches.is_present("no-iolat");
371 self.force = matches.is_present("force");
372 self.force_running = matches.is_present("force-running");
373 self.prepare = matches.is_present("prepare");
374 self.linux_tar = matches.value_of("linux-tar").map(|x| x.to_string());
375 self.bench_file = matches.value_of("bench-file").map(|x| x.to_string());
376 self.reset = matches.is_present("reset");
377 self.keep_reports = matches.is_present("keep-reports");
378 self.verbosity = Self::verbosity(&matches);
379 self.logfile = matches.value_of("logfile").map(|x| x.to_string());
380 self.bypass = matches.is_present("bypass");
381
382 match matches.value_of("passive") {
383 Some(passives) => self.enforce.parse_and_merge(passives).unwrap(),
384 None => self.enforce = Default::default(),
385 }
386
387 if let (bandit, Some(subm)) = matches.subcommand() {
388 updated_base |= self.process_bandit(bandit, subm);
389 }
390
391 updated_base
392 }
393}