diff --git a/2021/README.md b/2021/README.md
index bbbf89b..dc7705c 100644
--- a/2021/README.md
+++ b/2021/README.md
@@ -20,3 +20,15 @@ OPTIONS:
-i, --input Read input from the given file instead of stdin
-t, --time Print time taken
```
+
+## That goal was achieved
+
+Runtime benchmarked with [Criterion], reading input directly from memory to avoid disk IO
+inconsistencies.
+
+
+
+
+
+
+[Criterion]: https://github.com/bheisler/criterion.rs
diff --git a/2021/create_timing_plots.py b/2021/create_timing_plots.py
new file mode 100755
index 0000000..10aef43
--- /dev/null
+++ b/2021/create_timing_plots.py
@@ -0,0 +1,97 @@
+#!/usr/bin/env python3
+import json
+from pathlib import Path
+from typing import Dict
+
+import numpy as np
+import matplotlib.pyplot as plt
+
+
+def read_timings() -> Dict[int, Dict]:
+ timings = {}
+
+ for day in Path('target/criterion/part1').iterdir():
+ with open(day / 'new' / 'estimates.json', mode='rb') as f:
+ timings[int(day.parts[-1])] = {
+ 1: json.load(f)
+ }
+
+ for day in Path('target/criterion/part2').iterdir():
+ with open(day / 'new' / 'estimates.json', mode='rb') as f:
+ timings[int(day.parts[-1])][2] = json.load(f)
+
+ return timings
+
+
+def plot_cumulative_time(timings: Dict[int, Dict]):
+ plt.clf()
+
+ times = [0]
+
+ for day in range(min(timings.keys()), max(timings.keys()) + 1):
+ times.append(timings[day][1]['mean']['point_estimate'])
+ if day < 25:
+ times.append(timings[day][2]['mean']['point_estimate'])
+ else:
+ times.append(0)
+
+ cumulative = np.cumsum(times)
+ # Convert from nanoseconds to seconds
+ cumulative /= 1e9
+
+ x = np.arange(0.0, 25.5, 0.5)
+
+ plt.plot(x, cumulative, label="Cumulative time", drawstyle='steps-post')
+ plt.plot([0, 25], [0, 0.5], label="Target time")
+ plt.ylabel('Cumulative time (s)')
+ plt.xlabel('Days completed')
+
+ plt.legend()
+ plt.tight_layout()
+
+ plt.xlim(0, 25)
+ plt.ylim(0, 0.5)
+
+ plt.savefig('cumulative-time.svg')
+
+
+def plot_individual_times(timings: Dict[int, Dict]):
+ plt.clf()
+
+ def plot(parts, **kwargs):
+ x = np.arange(1, len(parts) + 1)
+
+ values = np.array(list(part['mean']['point_estimate'] for part in parts))
+ upper = np.array(list(part['mean']['confidence_interval']['upper_bound'] for part in parts))
+ lower = np.array(list(part['mean']['confidence_interval']['lower_bound'] for part in parts))
+
+ # Convert from ns to s
+ yerr = np.array([upper - values, lower - values]) / 1e9
+ values = values / 1e9
+
+ plt.bar(x, values, yerr=yerr, align='edge', log=True, **kwargs)
+ pass
+
+ plot(list(timings[day][1] for day in range(1, 26)), label="Part 1", width=-0.4)
+ plot(list(timings[day][2] for day in range(1, 25)), label="Part 2", width=0.4)
+
+ plt.ylabel('Runtime (s)')
+ plt.xlabel('Day')
+
+ plt.xlim(0, 26)
+ plt.xticks(np.arange(1, 26))
+
+ plt.legend()
+ plt.tight_layout()
+
+ plt.savefig('individual-time.svg')
+
+
+def main():
+ timings = read_timings()
+ plot_cumulative_time(timings)
+ plot_individual_times(timings)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/2021/cumulative-time.svg b/2021/cumulative-time.svg
new file mode 100644
index 0000000..82ee8aa
--- /dev/null
+++ b/2021/cumulative-time.svg
@@ -0,0 +1 @@
+
diff --git a/2021/individual-time.svg b/2021/individual-time.svg
new file mode 100644
index 0000000..6a1794f
--- /dev/null
+++ b/2021/individual-time.svg
@@ -0,0 +1 @@
+