use cfait::config::{PausedSortBehavior, SortPreset};
use cfait::context::TestContext;
use cfait::model::Task;
use cfait::model::parser::tokenize_smart_input;
use cfait::store::{FilterOptions, TaskStore};
use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main};
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
const SAMPLE_SIZE: usize = 20;
const MEASUREMENT_SECS: u64 = 3;
fn criterion_config() -> Criterion {
Criterion::default()
.sample_size(SAMPLE_SIZE)
.measurement_time(std::time::Duration::from_secs(MEASUREMENT_SECS))
}
fn build_flat_store(n: usize) -> TaskStore {
let ctx = Arc::new(TestContext::new());
let mut store = TaskStore::new(ctx);
let aliases = HashMap::new();
let calendars = ["cal1", "cal2", "cal3"];
for i in 0..n {
let prio = (i % 9) + 1; let tag = match i % 4 {
0 => "#work",
1 => "#home",
2 => "#bench",
_ => "#errands",
};
let due = match i % 5 {
0 => "@tomorrow",
1 => "@next week",
2 => "@in 3d",
_ => "",
};
let mut t = Task::new(
&format!("Flat task number {i} !{prio} {tag} {due}"),
&aliases,
None,
);
t.calendar_href = calendars[i % calendars.len()].to_string();
store.add_task(t);
}
store
}
fn build_hierarchy_store(groups: usize, branching: usize) -> TaskStore {
let ctx = Arc::new(TestContext::new());
let mut store = TaskStore::new(ctx);
let aliases = HashMap::new();
for g in 0..groups {
let root_uid = format!("root-{g}");
let mut root = Task::new(
&format!("Project {g} !1 #project @next week"),
&aliases,
None,
);
root.uid = root_uid.clone();
root.calendar_href = "cal1".to_string();
store.add_task(root);
for c in 0..branching {
let child_uid = format!("root-{g}-c{c}");
let mut child = Task::new(
&format!("Subtask {c} of project {g} !2 #sub @tomorrow"),
&aliases,
None,
);
child.uid = child_uid.clone();
child.parent_uid = Some(root_uid.clone());
child.calendar_href = "cal1".to_string();
store.add_task(child);
for gc in 0..branching {
let mut grand = Task::new(
&format!("Grandchild {gc} under {g}-{c} !3 #leaf"),
&aliases,
None,
);
grand.uid = format!("root-{g}-c{c}-g{gc}");
grand.parent_uid = Some(child_uid.clone());
grand.calendar_href = "cal1".to_string();
store.add_task(grand);
}
}
}
store
}
fn default_options<'a>(
empty_h: &'a HashSet<String>,
empty_m: &'a HashMap<String, Vec<String>>,
) -> FilterOptions<'a> {
FilterOptions {
active_cal_href: None,
hidden_calendars: empty_h,
selected_categories: empty_h,
selected_locations: empty_h,
match_all_categories: false,
search_term: "",
hide_completed_global: false,
hide_fully_completed_tags: false,
hide_aliases_in_sidebar: false,
cutoff_date: None,
min_duration: None,
max_duration: None,
include_unset_duration: true,
urgent_days: 7,
urgent_prio: 5,
default_priority: 5,
start_grace_period_days: 1,
sort_standard_by_priority: false,
sort_preset: SortPreset::default(),
expanded_done_groups: empty_h,
expanded_tags: empty_h,
expanded_locations: empty_h,
max_done_roots: usize::MAX,
max_done_subtasks: usize::MAX,
tag_aliases: empty_m,
search_collapsed_tasks: empty_h,
focused_task_uid: None,
paused_sort_behavior: PausedSortBehavior::default(),
sort_tiebreak_recent: false,
}
}
fn search_options<'a>(
empty_h: &'a HashSet<String>,
empty_m: &'a HashMap<String, Vec<String>>,
term: &'a str,
) -> FilterOptions<'a> {
let mut o = default_options(empty_h, empty_m);
o.search_term = term;
o
}
const FLAT_SIZES: &[usize] = &[1_000, 10_000, 100_000];
fn bench_filter_flat(c: &mut Criterion) {
let mut group = c.benchmark_group("filter/flat");
let empty_h = HashSet::new();
let empty_m = HashMap::new();
for &n in FLAT_SIZES {
let store = build_flat_store(n);
group.bench_with_input(BenchmarkId::new("default", n), &n, |b, _| {
b.iter(|| store.filter(default_options(&empty_h, &empty_m)))
});
group.bench_with_input(BenchmarkId::new("search", n), &n, |b, _| {
b.iter(|| store.filter(search_options(&empty_h, &empty_m, "task")))
});
}
group.finish();
}
fn bench_filter_hierarchy(c: &mut Criterion) {
let mut group = c.benchmark_group("filter/hierarchy");
let empty_h = HashSet::new();
let empty_m = HashMap::new();
for (groups, branching) in [(100, 9), (1_000, 9), (5_000, 3)] {
let total = groups * (1 + branching + branching * branching);
let store = build_hierarchy_store(groups, branching);
group.bench_with_input(
BenchmarkId::new("default", format!("{total}_tasks")),
&total,
|b, _| b.iter(|| store.filter(default_options(&empty_h, &empty_m))),
);
group.bench_with_input(
BenchmarkId::new("search", format!("{total}_tasks")),
&total,
|b, _| b.iter(|| store.filter(search_options(&empty_h, &empty_m, "project"))),
);
}
group.finish();
}
fn bench_parse_task_new(c: &mut Criterion) {
let aliases = HashMap::new();
let inputs: &[(&str, &str)] = &[
("simple", "Buy milk"),
("priority", "Ship the release !1"),
(
"full",
"Prepare demo !2 #work @@Office @tomorrow ~45m rec:FREQ=WEEKLY",
),
(
"escaped",
"@@\"San Francisco\" desc:\"Line one\\nLine two\" url:example.com",
),
];
let mut group = c.benchmark_group("parse/task_new");
for (label, input) in inputs {
group.bench_function(*label, |b| b.iter(|| Task::new(input, &aliases, None)));
}
group.finish();
}
fn bench_parse_apply(c: &mut Criterion) {
let aliases = HashMap::new();
let mut group = c.benchmark_group("parse/apply_smart_input");
let input = "Reschedule !1 #work @@Home @next week ~2h rec:FREQ=DAILY";
group.bench_function("reparse_full", |b| {
b.iter_batched(
|| Task::new("Original task", &aliases, None),
|mut task| task.apply_smart_input(input, &aliases, None),
criterion::BatchSize::SmallInput,
)
});
group.finish();
}
fn bench_parse_tokenize(c: &mut Criterion) {
let mut group = c.benchmark_group("parse/tokenize");
let inputs: &[(&str, &str)] = &[
("short", "Buy milk !1 #errands"),
(
"medium",
"Prepare demo !2 #work @Office ^tomorrow *45m desc:\"long body here\"",
),
(
"braces",
"gaming{genre={metroidvania, platform}, multiplayer{coop, online}} !1 #fun",
),
];
for (label, input) in inputs {
group.bench_function(*label, |b| b.iter(|| tokenize_smart_input(input, false)));
}
group.finish();
}
criterion_group! {
name = benches;
config = criterion_config();
targets =
bench_filter_flat,
bench_filter_hierarchy,
bench_parse_task_new,
bench_parse_apply,
bench_parse_tokenize
}
criterion_main!(benches);