Repository: LiveSplit/livesplit-core
Pull request #339 (2020-06-10 15:57:43)

Introduce a Timer Snapshot Mechanism
This introduces the ability to create snapshots of the timer. At the moment of snapshot creation the current time of the timer is evaluated. The snapshot then represents the timer with the current time frozen at that specific time. This allows layout state evaluation and other calculations to work with a consistent timer that is entirely frozen, allowing for more consistent results. However almost more importantly, this reduces the amount of times we need to take time stamps from the underlying system, improving the performance a bit.

![https://i.imgur.com/vDkucUu.png](https://i.imgur.com/vDkucUu.png)

In the web the performance improvement is even more significant. There the layout state calculation improved from 8.5us to 5us.


Patch:
diff --git a/benches/layout_state.rs b/benches/layout_state.rs
index d323a2e..8d6dfef 100644
--- a/benches/layout_state.rs
+++ b/benches/layout_state.rs
@@ -36,16 +36,18 @@ fn real() -> (Timer, Layout) {
 fn no_reuse_real(c: &mut Criterion) {
     let (timer, mut layout) = real();
 
-    c.bench_function("No Reuse (Real)", move |b| b.iter(|| layout.state(&timer)));
+    c.bench_function("No Reuse (Real)", move |b| {
+        b.iter(|| layout.state(&timer.snapshot()))
+    });
 }
 
 fn reuse_real(c: &mut Criterion) {
     let (timer, mut layout) = real();
 
-    let mut state = layout.state(&timer);
+    let mut state = layout.state(&timer.snapshot());
 
     c.bench_function("Reuse (Real)", move |b| {
-        b.iter(|| layout.update_state(&mut state, &timer))
+        b.iter(|| layout.update_state(&mut state, &timer.snapshot()))
     });
 }
 
@@ -53,16 +55,16 @@ fn no_reuse_artificial(c: &mut Criterion) {
     let (timer, mut layout) = artificial();
 
     c.bench_function("No Reuse (Artificial)", move |b| {
-        b.iter(|| layout.state(&timer))
+        b.iter(|| layout.state(&timer.snapshot()))
     });
 }
 
 fn reuse_artificial(c: &mut Criterion) {
     let (timer, mut layout) = artificial();
 
-    let mut state = layout.state(&timer);
+    let mut state = layout.state(&timer.snapshot());
 
     c.bench_function("Reuse (Artificial)", move |b| {
-        b.iter(|| layout.update_state(&mut state, &timer))
+        b.iter(|| layout.update_state(&mut state, &timer.snapshot()))
     });
 }
diff --git a/capi/src/current_pace_component.rs b/capi/src/current_pace_component.rs
index 92c89e5..0d5ee5a 100644
--- a/capi/src/current_pace_component.rs
+++ b/capi/src/current_pace_component.rs
@@ -39,7 +39,7 @@ pub extern "C" fn CurrentPaceComponent_state_as_json(
     timer: &Timer,
 ) -> Json {
     output_vec(|o| {
-        this.state(timer).write_json(o).unwrap();
+        this.state(&timer.snapshot()).write_json(o).unwrap();
     })
 }
 
@@ -49,5 +49,5 @@ pub extern "C" fn CurrentPaceComponent_state(
     this: &mut CurrentPaceComponent,
     timer: &Timer,
 ) -> OwnedKeyValueComponentState {
-    Box::new(this.state(timer))
+    Box::new(this.state(&timer.snapshot()))
 }
diff --git a/capi/src/delta_component.rs b/capi/src/delta_component.rs
index a947809..060ede7 100644
--- a/capi/src/delta_component.rs
+++ b/capi/src/delta_component.rs
@@ -37,7 +37,9 @@ pub extern "C" fn DeltaComponent_state_as_json(
     layout_settings: &GeneralLayoutSettings,
 ) -> Json {
     output_vec(|o| {
-        this.state(timer, layout_settings).write_json(o).unwrap();
+        this.state(&timer.snapshot(), layout_settings)
+            .write_json(o)
+            .unwrap();
     })
 }
 
@@ -49,5 +51,5 @@ pub extern "C" fn DeltaComponent_state(
     timer: &Timer,
     layout_settings: &GeneralLayoutSettings,
 ) -> OwnedKeyValueComponentState {
-    Box::new(this.state(timer, layout_settings))
+    Box::new(this.state(&timer.snapshot(), layout_settings))
 }
diff --git a/capi/src/detailed_timer_component.rs b/capi/src/detailed_timer_component.rs
index b2db90d..cbff4e2 100644
--- a/capi/src/detailed_timer_component.rs
+++ b/capi/src/detailed_timer_component.rs
@@ -41,7 +41,9 @@ pub extern "C" fn DetailedTimerComponent_state_as_json(
     layout_settings: &GeneralLayoutSettings,
 ) -> Json {
     output_vec(|o| {
-        this.state(timer, layout_settings).write_json(o).unwrap();
+        this.state(&timer.snapshot(), layout_settings)
+            .write_json(o)
+            .unwrap();
     })
 }
 
@@ -53,5 +55,5 @@ pub extern "C" fn DetailedTimerComponent_state(
     timer: &Timer,
     layout_settings: &GeneralLayoutSettings,
 ) -> OwnedDetailedTimerComponentState {
-    Box::new(this.state(timer, layout_settings))
+    Box::new(this.state(&timer.snapshot(), layout_settings))
 }
diff --git a/capi/src/graph_component.rs b/capi/src/graph_component.rs
index 35144f3..ac36a47 100644
--- a/capi/src/graph_component.rs
+++ b/capi/src/graph_component.rs
@@ -38,7 +38,9 @@ pub extern "C" fn GraphComponent_state_as_json(
     layout_settings: &GeneralLayoutSettings,
 ) -> Json {
     output_vec(|o| {
-        this.state(timer, layout_settings).write_json(o).unwrap();
+        this.state(&timer.snapshot(), layout_settings)
+            .write_json(o)
+            .unwrap();
     })
 }
 
@@ -50,5 +52,5 @@ pub extern "C" fn GraphComponent_state(
     timer: &Timer,
     layout_settings: &GeneralLayoutSettings,
 ) -> OwnedGraphComponentState {
-    Box::new(this.state(timer, layout_settings))
+    Box::new(this.state(&timer.snapshot(), layout_settings))
 }
diff --git a/capi/src/layout.rs b/capi/src/layout.rs
index 047727e..c577eda 100644
--- a/capi/src/layout.rs
+++ b/capi/src/layout.rs
@@ -90,13 +90,13 @@ pub unsafe extern "C" fn Layout_parse_original_livesplit(
 /// Calculates and returns the layout's state based on the timer provided.
 #[no_mangle]
 pub extern "C" fn Layout_state(this: &mut Layout, timer: &Timer) -> OwnedLayoutState {
-    Box::new(this.state(timer))
+    Box::new(this.state(&timer.snapshot()))
 }
 
 /// Updates the layout's state based on the timer provided.
 #[no_mangle]
 pub extern "C" fn Layout_update_state(this: &mut Layout, state: &mut LayoutState, timer: &Timer) {
-    this.update_state(state, timer)
+    this.update_state(state, &timer.snapshot())
 }
 
 /// Updates the layout's state based on the timer provided and encodes it as
@@ -107,7 +107,7 @@ pub extern "C" fn Layout_update_state_as_json(
     state: &mut LayoutState,
     timer: &Timer,
 ) -> Json {
-    this.update_state(state, timer);
+    this.update_state(state, &timer.snapshot());
     output_vec(|o| {
         state.write_json(o).unwrap();
     })
@@ -118,7 +118,7 @@ pub extern "C" fn Layout_update_state_as_json(
 #[no_mangle]
 pub extern "C" fn Layout_state_as_json(this: &mut Layout, timer: &Timer) -> Json {
     output_vec(|o| {
-        this.state(timer).write_json(o).unwrap();
+        this.state(&timer.snapshot()).write_json(o).unwrap();
     })
 }
 
diff --git a/capi/src/layout_editor.rs b/capi/src/layout_editor.rs
index b6b488a..d3b5145 100644
--- a/capi/src/layout_editor.rs
+++ b/capi/src/layout_editor.rs
@@ -47,7 +47,7 @@ pub extern "C" fn LayoutEditor_layout_state_as_json(
     timer: &Timer,
 ) -> Json {
     output_vec(|o| {
-        this.layout_state(timer).write_json(o).unwrap();
+        this.layout_state(&timer.snapshot()).write_json(o).unwrap();
     })
 }
 
diff --git a/capi/src/pb_chance_component.rs b/capi/src/pb_chance_component.rs
index f2259d3..04206cd 100644
--- a/capi/src/pb_chance_component.rs
+++ b/capi/src/pb_chance_component.rs
@@ -35,7 +35,7 @@ pub extern "C" fn PbChanceComponent_into_generic(this: OwnedPbChanceComponent) -
 #[no_mangle]
 pub extern "C" fn PbChanceComponent_state_as_json(this: &PbChanceComponent, timer: &Timer) -> Json {
     output_vec(|o| {
-        this.state(timer).write_json(o).unwrap();
+        this.state(&timer.snapshot()).write_json(o).unwrap();
     })
 }
 
@@ -45,5 +45,5 @@ pub extern "C" fn PbChanceComponent_state(
     this: &PbChanceComponent,
     timer: &Timer,
 ) -> OwnedKeyValueComponentState {
-    Box::new(this.state(timer))
+    Box::new(this.state(&timer.snapshot()))
 }
diff --git a/capi/src/possible_time_save_component.rs b/capi/src/possible_time_save_component.rs
index 7c377c8..50f6c53 100644
--- a/capi/src/possible_time_save_component.rs
+++ b/capi/src/possible_time_save_component.rs
@@ -40,7 +40,7 @@ pub extern "C" fn PossibleTimeSaveComponent_state_as_json(
     timer: &Timer,
 ) -> Json {
     output_vec(|o| {
-        this.state(timer).write_json(o).unwrap();
+        this.state(&timer.snapshot()).write_json(o).unwrap();
     })
 }
 
@@ -50,5 +50,5 @@ pub extern "C" fn PossibleTimeSaveComponent_state(
     this: &PossibleTimeSaveComponent,
     timer: &Timer,
 ) -> OwnedKeyValueComponentState {
-    Box::new(this.state(timer))
+    Box::new(this.state(&timer.snapshot()))
 }
diff --git a/capi/src/previous_segment_component.rs b/capi/src/previous_segment_component.rs
index 5e87b6b..d2ec15a 100644
--- a/capi/src/previous_segment_component.rs
+++ b/capi/src/previous_segment_component.rs
@@ -42,7 +42,9 @@ pub extern "C" fn PreviousSegmentComponent_state_as_json(
     layout_settings: &GeneralLayoutSettings,
 ) -> Json {
     output_vec(|o| {
-        this.state(timer, layout_settings).write_json(o).unwrap();
+        this.state(&timer.snapshot(), layout_settings)
+            .write_json(o)
+            .unwrap();
     })
 }
 
@@ -54,5 +56,5 @@ pub extern "C" fn PreviousSegmentComponent_state(
     timer: &Timer,
     layout_settings: &GeneralLayoutSettings,
 ) -> OwnedKeyValueComponentState {
-    Box::new(this.state(timer, layout_settings))
+    Box::new(this.state(&timer.snapshot(), layout_settings))
 }
diff --git a/capi/src/splits_component.rs b/capi/src/splits_component.rs
index 1ae4b75..04e02a0 100644
--- a/capi/src/splits_component.rs
+++ b/capi/src/splits_component.rs
@@ -40,7 +40,9 @@ pub extern "C" fn SplitsComponent_state_as_json(
     layout_settings: &GeneralLayoutSettings,
 ) -> Json {
     output_vec(|o| {
-        this.state(timer, layout_settings).write_json(o).unwrap();
+        this.state(&timer.snapshot(), layout_settings)
+            .write_json(o)
+            .unwrap();
     })
 }
 
@@ -52,7 +54,7 @@ pub extern "C" fn SplitsComponent_state(
     timer: &Timer,
     layout_settings: &GeneralLayoutSettings,
 ) -> OwnedSplitsComponentState {
-    Box::new(this.state(timer, layout_settings))
+    Box::new(this.state(&timer.snapshot(), layout_settings))
 }
 
 /// Scrolls up the window of the segments that are shown. Doesn't move the
diff --git a/capi/src/timer.rs b/capi/src/timer.rs
index 5922266..55282ca 100644
--- a/capi/src/timer.rs
+++ b/capi/src/timer.rs
@@ -303,5 +303,5 @@ pub extern "C" fn Timer_print_debug(this: &Timer) {
 /// Time has not been initialized.
 #[no_mangle]
 pub extern "C" fn Timer_current_time(this: &Timer) -> *const Time {
-    output_time(this.current_time())
+    output_time(this.snapshot().current_time())
 }
diff --git a/capi/src/timer_component.rs b/capi/src/timer_component.rs
index 0d31e7d..56ec961 100644
--- a/capi/src/timer_component.rs
+++ b/capi/src/timer_component.rs
@@ -38,7 +38,9 @@ pub extern "C" fn TimerComponent_state_as_json(
     layout_settings: &GeneralLayoutSettings,
 ) -> Json {
     output_vec(|o| {
-        this.state(timer, layout_settings).write_json(o).unwrap();
+        this.state(&timer.snapshot(), layout_settings)
+            .write_json(o)
+            .unwrap();
     })
 }
 
@@ -50,5 +52,5 @@ pub extern "C" fn TimerComponent_state(
     timer: &Timer,
     layout_settings: &GeneralLayoutSettings,
 ) -> OwnedTimerComponentState {
-    Box::new(this.state(timer, layout_settings))
+    Box::new(this.state(&timer.snapshot(), layout_settings))
 }
diff --git a/src/analysis/current_pace.rs b/src/analysis/current_pace.rs
index cc9fd21..fdf2319 100644
--- a/src/analysis/current_pace.rs
+++ b/src/analysis/current_pace.rs
@@ -2,12 +2,12 @@
 //! provided. If there's no active attempt, the final time of the comparison is
 //! returned instead.
 
-use crate::{analysis, TimeSpan, Timer, TimerPhase};
+use crate::{analysis, timing::Snapshot, TimeSpan, TimerPhase};
 
 /// Calculates the current pace of the active attempt based on the comparison
 /// provided. If there's no active attempt, the final time of the comparison is
 /// returned instead.
-pub fn calculate(timer: &Timer, comparison: &str) -> Option<TimeSpan> {
+pub fn calculate(timer: &Snapshot<'_>, comparison: &str) -> Option<TimeSpan> {
     let timing_method = timer.current_timing_method();
     let last_segment = timer.run().segments().last().unwrap();
 
diff --git a/src/analysis/delta.rs b/src/analysis/delta.rs
index 19b18c3..19b6201 100644
--- a/src/analysis/delta.rs
+++ b/src/analysis/delta.rs
@@ -4,14 +4,14 @@
 //! the moment. This may be the case when the current attempt is slower than the
 //! comparison at the current split.
 
-use crate::{analysis, TimeSpan, Timer, TimerPhase};
+use crate::{analysis, timing::Snapshot, TimeSpan, TimerPhase};
 
 /// Calculates the delta of the current attempt to the comparison provided.
 /// Additionally a value is returned that indicates whether the delta value is a
 /// live delta. A live delta indicates that the value is actively changing at
 /// the moment. This may be the case when the current attempt is slower than the
 /// comparison at the current split.
-pub fn calculate(timer: &Timer, comparison: &str) -> (Option<TimeSpan>, bool) {
+pub fn calculate(timer: &Snapshot<'_>, comparison: &str) -> (Option<TimeSpan>, bool) {
     let timing_method = timer.current_timing_method();
     let last_segment = timer.run().segments().last().unwrap();
 
diff --git a/src/analysis/pb_chance/mod.rs b/src/analysis/pb_chance/mod.rs
index 0ea5fc7..73ac600 100644
--- a/src/analysis/pb_chance/mod.rs
+++ b/src/analysis/pb_chance/mod.rs
@@ -10,7 +10,7 @@
 //! where the Balanced PB would source its split times.
 
 use super::SkillCurve;
-use crate::{comparison, Run, Segment, TimeSpan, Timer, TimingMethod};
+use crate::{comparison, timing::Snapshot, Run, Segment, TimeSpan, TimingMethod};
 
 #[cfg(test)]
 mod tests;
@@ -40,7 +40,7 @@ pub fn for_run(run: &Run, method: TimingMethod) -> f64 {
 /// the current attempt. If there is no attempt in progress it yields the same
 /// result as the PB chance for the run. The value is being reported as a
 /// floating point number in the range from 0 (0%) to 1 (100%).
-pub fn for_timer(timer: &Timer) -> f64 {
+pub fn for_timer(timer: &Snapshot<'_>) -> f64 {
     let method = timer.current_timing_method();
     let all_segments = timer.run().segments();
 
diff --git a/src/analysis/possible_time_save.rs b/src/analysis/possible_time_save.rs
index 396cde3..9aad30f 100644
--- a/src/analysis/possible_time_save.rs
+++ b/src/analysis/possible_time_save.rs
@@ -4,7 +4,7 @@
 //! theoretically perfect segment times, this information is only an
 //! approximation of how much time can actually be saved.
 
-use crate::{analysis, TimeSpan, Timer};
+use crate::{analysis, timing::Snapshot, TimeSpan};
 
 /// Calculates how much time could be saved on the given segment with the given
 /// comparison. This information is based on the best segments. Considering the
@@ -16,7 +16,7 @@ use crate::{analysis, TimeSpan, Timer};
 /// shrinks towards zero as time goes on. The time returned by this function can
 /// never be below zero.
 pub fn calculate(
-    timer: &Timer,
+    timer: &Snapshot<'_>,
     segment_index: usize,
     comparison: &str,
     live: bool,
@@ -73,7 +73,7 @@ pub fn calculate(
 /// actually be saved. This information is always live, so the total possible
 /// time save will shrink towards zero throughout the run and when time is lost
 /// on a segment. The time returned by this function can never be below zero.
-pub fn calculate_total(timer: &Timer, segment_index: usize, comparison: &str) -> TimeSpan {
+pub fn calculate_total(timer: &Snapshot<'_>, segment_index: usize, comparison: &str) -> TimeSpan {
     let mut total = TimeSpan::zero();
 
     for index in segment_index..timer.run().len() {
diff --git a/src/analysis/state_helper.rs b/src/analysis/state_helper.rs
index 9a94121..fddf4c2 100644
--- a/src/analysis/state_helper.rs
+++ b/src/analysis/state_helper.rs
@@ -2,7 +2,7 @@
 
 use crate::comparison::best_segments;
 use crate::settings::SemanticColor;
-use crate::{Run, Segment, TimeSpan, Timer, TimerPhase, TimingMethod};
+use crate::{timing::Snapshot, Run, Segment, TimeSpan, Timer, TimerPhase, TimingMethod};
 
 /// Gets the last non-live delta in the run starting from `segment_index`.
 ///
@@ -187,7 +187,7 @@ pub fn previous_segment_time(
 /// Returns the length of the segment leading up to `segment_index`, returning
 /// the live segment time if the split is not completed yet.
 pub fn live_segment_time(
-    timer: &Timer,
+    timer: &Snapshot<'_>,
     segment_index: usize,
     method: TimingMethod,
 ) -> Option<TimeSpan> {
@@ -235,7 +235,7 @@ pub fn previous_segment_delta(
 /// Returns the segment delta for a certain split, returning the live segment
 /// delta if the split is not completed yet.
 pub fn live_segment_delta(
-    timer: &Timer,
+    timer: &Snapshot<'_>,
     segment_index: usize,
     comparison: &str,
     method: TimingMethod,
@@ -260,7 +260,7 @@ pub fn live_segment_delta(
 ///
 /// Returns the current live delta.
 pub fn check_live_delta(
-    timer: &Timer,
+    timer: &Snapshot<'_>,
     split_delta: bool,
     comparison: &str,
     method: TimingMethod,
diff --git a/src/component/current_pace.rs b/src/component/current_pace.rs
index 06a3f07..ddcecc3 100644
--- a/src/component/current_pace.rs
+++ b/src/component/current_pace.rs
@@ -7,8 +7,11 @@ use super::key_value;
 use crate::analysis::current_pace;
 use crate::platform::prelude::*;
 use crate::settings::{Color, Field, Gradient, SettingsDescription, Value};
-use crate::timing::formatter::{Accuracy, Regular, TimeFormatter};
-use crate::{comparison, Timer, TimerPhase};
+use crate::timing::{
+    formatter::{Accuracy, Regular, TimeFormatter},
+    Snapshot,
+};
+use crate::{comparison, TimerPhase};
 use alloc::borrow::Cow;
 use core::fmt::Write;
 use serde::{Deserialize, Serialize};
@@ -97,7 +100,7 @@ impl Component {
     }
 
     /// Updates the component's state based on the timer provided.
-    pub fn update_state(&self, state: &mut key_value::State, timer: &Timer) {
+    pub fn update_state(&self, state: &mut key_value::State, timer: &Snapshot<'_>) {
         let comparison = comparison::resolve(&self.settings.comparison_override, timer);
         let comparison = comparison::or_current(comparison, timer);
         let key = self.text(Some(comparison));
@@ -154,7 +157,7 @@ impl Component {
     }
 
     /// Calculates the component's state based on the timer provided.
-    pub fn state(&self, timer: &Timer) -> key_value::State {
+    pub fn state(&self, timer: &Snapshot<'_>) -> key_value::State {
         let mut state = Default::default();
         self.update_state(&mut state, timer);
         state
diff --git a/src/component/delta/mod.rs b/src/component/delta/mod.rs
index 474886a..1815a3d 100644
--- a/src/component/delta/mod.rs
+++ b/src/component/delta/mod.rs
@@ -6,8 +6,11 @@ use super::key_value;
 use crate::analysis::{delta, state_helper};
 use crate::platform::prelude::*;
 use crate::settings::{Color, Field, Gradient, SemanticColor, SettingsDescription, Value};
-use crate::timing::formatter::{Accuracy, Delta, TimeFormatter};
-use crate::{comparison, GeneralLayoutSettings, Timer};
+use crate::timing::{
+    formatter::{Accuracy, Delta, TimeFormatter},
+    Snapshot,
+};
+use crate::{comparison, GeneralLayoutSettings};
 use alloc::borrow::Cow;
 use core::fmt::Write;
 use serde::{Deserialize, Serialize};
@@ -92,7 +95,7 @@ impl Component {
     pub fn update_state(
         &self,
         state: &mut key_value::State,
-        timer: &Timer,
+        timer: &Snapshot<'_>,
         layout_settings: &GeneralLayoutSettings,
     ) {
         let comparison = comparison::resolve(&self.settings.comparison_override, timer);
@@ -148,7 +151,7 @@ impl Component {
     /// settings provided.
     pub fn state(
         &self,
-        timer: &Timer,
+        timer: &Snapshot<'_>,
         layout_settings: &GeneralLayoutSettings,
     ) -> key_value::State {
         let mut state = Default::default();
diff --git a/src/component/detailed_timer/mod.rs b/src/component/detailed_timer/mod.rs
index 99f446a..b10070e 100644
--- a/src/component/detailed_timer/mod.rs
+++ b/src/component/detailed_timer/mod.rs
@@ -9,8 +9,11 @@ use crate::analysis::comparison_single_segment_time;
 use crate::comparison::{self, best_segments, none};
 use crate::platform::prelude::*;
 use crate::settings::{CachedImageId, Field, Gradient, ImageData, SettingsDescription, Value};
-use crate::timing::formatter::{Accuracy, DigitsFormat, SegmentTime, TimeFormatter};
-use crate::{GeneralLayoutSettings, Segment, TimeSpan, Timer, TimerPhase};
+use crate::timing::{
+    formatter::{Accuracy, DigitsFormat, SegmentTime, TimeFormatter},
+    Snapshot,
+};
+use crate::{GeneralLayoutSettings, Segment, TimeSpan, TimerPhase};
 use core::fmt::Write;
 use serde::{Deserialize, Serialize};
 
@@ -180,7 +183,7 @@ impl Component {
     pub fn update_state(
         &mut self,
         state: &mut State,
-        timer: &Timer,
+        timer: &Snapshot<'_>,
         layout_settings: &GeneralLayoutSettings,
     ) {
         let current_phase = timer.current_phase();
@@ -286,7 +289,11 @@ impl Component {
 
     /// Calculates the component's state based on the timer and layout settings
     /// provided.
-    pub fn state(&mut self, timer: &Timer, layout_settings: &GeneralLayoutSettings) -> State {
+    pub fn state(
+        &mut self,
+        timer: &Snapshot<'_>,
+        layout_settings: &GeneralLayoutSettings,
+    ) -> State {
         let mut state = Default::default();
         self.update_state(&mut state, timer, layout_settings);
         state
diff --git a/src/component/graph.rs b/src/component/graph.rs
index 8f948f8..a48e2c5 100644
--- a/src/component/graph.rs
+++ b/src/component/graph.rs
@@ -5,7 +5,9 @@
 
 use crate::platform::prelude::*;
 use crate::settings::{Color, Field, SettingsDescription, Value};
-use crate::{analysis, comparison, GeneralLayoutSettings, TimeSpan, Timer, TimerPhase};
+use crate::{
+    analysis, comparison, timing::Snapshot, GeneralLayoutSettings, TimeSpan, Timer, TimerPhase,
+};
 use alloc::borrow::Cow;
 use serde::{Deserialize, Serialize};
 
@@ -200,7 +202,7 @@ impl Component {
     pub fn update_state(
         &self,
         state: &mut State,
-        timer: &Timer,
+        timer: &Snapshot<'_>,
         layout_settings: &GeneralLayoutSettings,
     ) {
         let comparison = comparison::resolve(&self.settings.comparison_override, timer);
@@ -223,7 +225,7 @@ impl Component {
 
     /// Calculates the component's state based on the timer and layout settings
     /// provided.
-    pub fn state(&self, timer: &Timer, layout_settings: &GeneralLayoutSettings) -> State {
+    pub fn state(&self, timer: &Snapshot<'_>, layout_settings: &GeneralLayoutSettings) -> State {
         let mut state = Default::default();
         self.update_state(&mut state, timer, layout_settings);
         state
@@ -434,7 +436,7 @@ impl Component {
             && analysis::check_best_segment(timer, split_number, timer.current_timing_method())
     }
 
-    fn calculate_final_split(&self, timer: &Timer, draw_info: &mut DrawInfo) {
+    fn calculate_final_split(&self, timer: &Snapshot<'_>, draw_info: &mut DrawInfo) {
         if timer.current_phase() != TimerPhase::NotRunning {
             if self.settings.live_graph {
                 let current_time = timer.current_time();
@@ -457,7 +459,12 @@ impl Component {
         }
     }
 
-    fn check_live_segment_delta(&self, timer: &Timer, comparison: &str, draw_info: &mut DrawInfo) {
+    fn check_live_segment_delta(
+        &self,
+        timer: &Snapshot<'_>,
+        comparison: &str,
+        draw_info: &mut DrawInfo,
+    ) {
         if self.settings.live_graph {
             let current_phase = timer.current_phase();
             if current_phase == TimerPhase::Running || current_phase == TimerPhase::Paused {
diff --git a/src/component/pb_chance.rs b/src/component/pb_chance.rs
index 92976b8..eadbaf0 100644
--- a/src/component/pb_chance.rs
+++ b/src/component/pb_chance.rs
@@ -7,7 +7,7 @@
 use super::key_value;
 use crate::platform::prelude::*;
 use crate::settings::{Color, Field, Gradient, SettingsDescription, Value};
-use crate::{analysis::pb_chance, Timer};
+use crate::{analysis::pb_chance, timing::Snapshot};
 use core::fmt::Write;
 use serde::{Deserialize, Serialize};
 
@@ -75,7 +75,7 @@ impl Component {
     }
 
     /// Updates the component's state based on the timer provided.
-    pub fn update_state(&self, state: &mut key_value::State, timer: &Timer) {
+    pub fn update_state(&self, state: &mut key_value::State, timer: &Snapshot<'_>) {
         let chance = pb_chance::for_timer(timer);
 
         state.background = self.settings.background;
@@ -94,7 +94,7 @@ impl Component {
     }
 
     /// Calculates the component's state based on the timer provided.
-    pub fn state(&self, timer: &Timer) -> key_value::State {
+    pub fn state(&self, timer: &Snapshot<'_>) -> key_value::State {
         let mut state = Default::default();
         self.update_state(&mut state, timer);
         state
diff --git a/src/component/possible_time_save.rs b/src/component/possible_time_save.rs
index 85fe993..daec18a 100644
--- a/src/component/possible_time_save.rs
+++ b/src/component/possible_time_save.rs
@@ -8,8 +8,11 @@ use super::key_value;
 use crate::analysis::possible_time_save;
 use crate::platform::prelude::*;
 use crate::settings::{Color, Field, Gradient, SettingsDescription, Value};
-use crate::timing::formatter::{Accuracy, SegmentTime, TimeFormatter};
-use crate::{comparison, Timer, TimerPhase};
+use crate::timing::{
+    formatter::{Accuracy, SegmentTime, TimeFormatter},
+    Snapshot,
+};
+use crate::{comparison, TimerPhase};
 use alloc::borrow::Cow;
 use core::fmt::Write as FmtWrite;
 use serde::{Deserialize, Serialize};
@@ -108,7 +111,7 @@ impl Component {
     }
 
     /// Updates the component's state based on the timer provided.
-    pub fn update_state(&self, state: &mut key_value::State, timer: &Timer) {
+    pub fn update_state(&self, state: &mut key_value::State, timer: &Snapshot<'_>) {
         let segment_index = timer.current_split_index();
         let current_phase = timer.current_phase();
         let comparison = comparison::resolve(&self.settings.comparison_override, timer);
@@ -160,7 +163,7 @@ impl Component {
     }
 
     /// Calculates the component's state based on the timer provided.
-    pub fn state(&self, timer: &Timer) -> key_value::State {
+    pub fn state(&self, timer: &Snapshot<'_>) -> key_value::State {
         let mut state = Default::default();
         self.update_state(&mut state, timer);
         state
diff --git a/src/component/previous_segment.rs b/src/component/previous_segment.rs
index b97a1cf..7e60074 100644
--- a/src/component/previous_segment.rs
+++ b/src/component/previous_segment.rs
@@ -8,8 +8,11 @@
 use super::key_value;
 use crate::platform::prelude::*;
 use crate::settings::{Color, Field, Gradient, SemanticColor, SettingsDescription, Value};
-use crate::timing::formatter::{Accuracy, Delta, SegmentTime, TimeFormatter};
-use crate::{analysis, comparison, GeneralLayoutSettings, Timer, TimerPhase};
+use crate::timing::{
+    formatter::{Accuracy, Delta, SegmentTime, TimeFormatter},
+    Snapshot,
+};
+use crate::{analysis, comparison, GeneralLayoutSettings, TimerPhase};
 use alloc::borrow::Cow;
 use core::fmt::Write as FmtWrite;
 use serde::{Deserialize, Serialize};
@@ -113,7 +116,7 @@ impl Component {
     pub fn update_state(
         &self,
         state: &mut key_value::State,
-        timer: &Timer,
+        timer: &Snapshot<'_>,
         layout_settings: &GeneralLayoutSettings,
     ) {
         let mut time_change = None;
@@ -235,7 +238,7 @@ impl Component {
     /// settings provided.
     pub fn state(
         &self,
-        timer: &Timer,
+        timer: &Snapshot<'_>,
         layout_settings: &GeneralLayoutSettings,
     ) -> key_value::State {
         let mut state = Default::default();
diff --git a/src/component/splits/column.rs b/src/component/splits/column.rs
index ccb0c8a..dbc5892 100644
--- a/src/component/splits/column.rs
+++ b/src/component/splits/column.rs
@@ -4,8 +4,11 @@ use crate::{
     clear_vec::Clear,
     comparison,
     settings::{Color, SemanticColor},
-    timing::formatter::{Delta, Regular, SegmentTime, TimeFormatter},
-    GeneralLayoutSettings, Segment, TimeSpan, Timer, TimingMethod,
+    timing::{
+        formatter::{Delta, Regular, SegmentTime, TimeFormatter},
+        Snapshot,
+    },
+    GeneralLayoutSettings, Segment, TimeSpan, TimingMethod,
 };
 use core::fmt::Write;
 use serde::{Deserialize, Serialize};
@@ -138,7 +141,7 @@ enum ColumnFormatter {
 pub fn update_state(
     state: &mut ColumnState,
     column: &ColumnSettings,
-    timer: &Timer,
+    timer: &Snapshot<'_>,
     layout_settings: &GeneralLayoutSettings,
     segment: &Segment,
     segment_index: usize,
@@ -209,7 +212,7 @@ pub fn update_state(
 
 fn column_update_value(
     column: &ColumnSettings,
-    timer: &Timer,
+    timer: &Snapshot<'_>,
     segment: &Segment,
     segment_index: usize,
     current_split: Option<usize>,
diff --git a/src/component/splits/mod.rs b/src/component/splits/mod.rs
index 89e4d15..261295e 100644
--- a/src/component/splits/mod.rs
+++ b/src/component/splits/mod.rs
@@ -11,7 +11,8 @@ use crate::{
     settings::{
         CachedImageId, Color, Field, Gradient, ImageData, ListGradient, SettingsDescription, Value,
     },
-    GeneralLayoutSettings, Timer,
+    timing::Snapshot,
+    GeneralLayoutSettings,
 };
 use core::cmp::{max, min};
 use serde::{Deserialize, Serialize};
@@ -273,7 +274,7 @@ impl Component {
     pub fn update_state(
         &mut self,
         state: &mut State,
-        timer: &Timer,
+        timer: &Snapshot<'_>,
         layout_settings: &GeneralLayoutSettings,
     ) {
         // Reset Scroll Offset when any movement of the split index is observed.
@@ -415,7 +416,11 @@ impl Component {
 
     /// Calculates the component's state based on the timer and layout settings
     /// provided.
-    pub fn state(&mut self, timer: &Timer, layout_settings: &GeneralLayoutSettings) -> State {
+    pub fn state(
+        &mut self,
+        timer: &Snapshot<'_>,
+        layout_settings: &GeneralLayoutSettings,
+    ) -> State {
         let mut state = Default::default();
         self.update_state(&mut state, timer, layout_settings);
         state
diff --git a/src/component/timer.rs b/src/component/timer.rs
index dab0772..d60ca8d 100644
--- a/src/component/timer.rs
+++ b/src/component/timer.rs
@@ -7,8 +7,11 @@ use crate::analysis::split_color;
 use crate::palette::{rgb::LinSrgb, Hsv};
 use crate::platform::prelude::*;
 use crate::settings::{Color, Field, Gradient, SemanticColor, SettingsDescription, Value};
-use crate::timing::formatter::{timer as formatter, Accuracy, DigitsFormat, TimeFormatter};
-use crate::{GeneralLayoutSettings, TimeSpan, Timer, TimerPhase, TimingMethod};
+use crate::timing::{
+    formatter::{timer as formatter, Accuracy, DigitsFormat, TimeFormatter},
+    Snapshot,
+};
+use crate::{GeneralLayoutSettings, TimeSpan, TimerPhase, TimingMethod};
 use core::fmt::Write;
 use serde::{Deserialize, Serialize};
 
@@ -131,7 +134,7 @@ impl Component {
     pub fn update_state(
         &self,
         state: &mut State,
-        timer: &Timer,
+        timer: &Snapshot<'_>,
         layout_settings: &GeneralLayoutSettings,
     ) {
         let method = self
@@ -237,7 +240,7 @@ impl Component {
 
     /// Calculates the component's state based on the timer and the layout
     /// settings provided.
-    pub fn state(&self, timer: &Timer, layout_settings: &GeneralLayoutSettings) -> State {
+    pub fn state(&self, timer: &Snapshot<'_>, layout_settings: &GeneralLayoutSettings) -> State {
         let mut state = Default::default();
         self.update_state(&mut state, timer, layout_settings);
         state
@@ -314,7 +317,7 @@ pub fn top_and_bottom_color(color: Color) -> (Color, Color) {
 }
 
 fn calculate_live_segment_time(
-    timer: &Timer,
+    timer: &Snapshot<'_>,
     timing_method: TimingMethod,
     last_split_index: usize,
 ) -> Option<TimeSpan> {
diff --git a/src/layout/component.rs b/src/layout/component.rs
index 1166d6e..77bf0b9 100644
--- a/src/layout/component.rs
+++ b/src/layout/component.rs
@@ -6,7 +6,7 @@ use crate::component::{
 };
 use crate::platform::prelude::*;
 use crate::settings::{SettingsDescription, Value};
-use crate::Timer;
+use crate::timing::Snapshot;
 use alloc::borrow::Cow;
 
 /// A Component provides information about a run in a way that is easy to
@@ -57,7 +57,7 @@ impl Component {
     pub fn update_state(
         &mut self,
         state: &mut ComponentState,
-        timer: &Timer,
+        timer: &Snapshot<'_>,
         layout_settings: &GeneralSettings,
     ) {
         match (state, self) {
@@ -120,7 +120,11 @@ impl Component {
     /// provided. The timer provides the information to visualize and the layout
     /// settings provide general information about how to expose that
     /// information in the state.
-    pub fn state(&mut self, timer: &Timer, layout_settings: &GeneralSettings) -> ComponentState {
+    pub fn state(
+        &mut self,
+        timer: &Snapshot<'_>,
+        layout_settings: &GeneralSettings,
+    ) -> ComponentState {
         match self {
             Component::BlankSpace(component) => ComponentState::BlankSpace(component.state()),
             Component::CurrentComparison(component) => {
diff --git a/src/layout/editor/mod.rs b/src/layout/editor/mod.rs
index ca15fdb..61a71dc 100644
--- a/src/layout/editor/mod.rs
+++ b/src/layout/editor/mod.rs
@@ -6,7 +6,7 @@
 
 use super::{Component, Layout, LayoutState};
 use crate::settings::Value;
-use crate::Timer;
+use crate::timing::Snapshot;
 use core::result::Result as StdResult;
 
 mod state;
@@ -59,7 +59,7 @@ impl Editor {
     /// Calculates the layout's state based on the timer provided. You can use
     /// this to visualize all of the components of a layout, while it is still
     /// being edited by the Layout Editor.
-    pub fn layout_state(&mut self, timer: &Timer) -> LayoutState {
+    pub fn layout_state(&mut self, timer: &Snapshot<'_>) -> LayoutState {
         self.layout.state(timer)
     }
 
diff --git a/src/layout/mod.rs b/src/layout/mod.rs
index efb7f54..8dbe156 100644
--- a/src/layout/mod.rs
+++ b/src/layout/mod.rs
@@ -24,7 +24,7 @@ pub use self::layout_state::LayoutState;
 
 use crate::component::{previous_segment, splits, timer, title};
 use crate::platform::prelude::*;
-use crate::timing::Timer;
+use crate::timing::Snapshot;
 
 /// A Layout allows you to combine multiple components together to visualize a
 /// variety of information the runner is interested in.
@@ -88,7 +88,7 @@ impl Layout {
 
     /// Updates the layout's state based on the timer provided. You can use this
     /// to visualize all of the components of a layout.
-    pub fn update_state(&mut self, state: &mut LayoutState, timer: &Timer) {
+    pub fn update_state(&mut self, state: &mut LayoutState, timer: &Snapshot<'_>) {
         let settings = &self.settings;
 
         state.components.truncate(self.components.len());
@@ -111,7 +111,7 @@ impl Layout {
 
     /// Calculates the layout's state based on the timer provided. You can use
     /// this to visualize all of the components of a layout.
-    pub fn state(&mut self, timer: &Timer) -> LayoutState {
+    pub fn state(&mut self, timer: &Snapshot<'_>) -> LayoutState {
         let mut state = Default::default();
         self.update_state(&mut state, timer);
         state
diff --git a/src/timing/mod.rs b/src/timing/mod.rs
index 8df5ea3..2bc2371 100644
--- a/src/timing/mod.rs
+++ b/src/timing/mod.rs
@@ -16,6 +16,6 @@ pub use self::time_span::{ParseError, TimeSpan};
 pub use self::time_stamp::TimeStamp;
 #[cfg(feature = "std")]
 pub use self::timer::SharedTimer;
-pub use self::timer::{CreationError as TimerCreationError, Timer};
+pub use self::timer::{CreationError as TimerCreationError, Snapshot, Timer};
 pub use self::timer_phase::TimerPhase;
 pub use self::timing_method::TimingMethod;
diff --git a/src/timing/timer/mod.rs b/src/timing/timer/mod.rs
index 2522234..6a787d3 100644
--- a/src/timing/timer/mod.rs
+++ b/src/timing/timer/mod.rs
@@ -2,7 +2,7 @@ use crate::comparison::personal_best;
 use crate::platform::prelude::*;
 use crate::TimerPhase::*;
 use crate::{AtomicDateTime, Run, Segment, Time, TimeSpan, TimeStamp, TimerPhase, TimingMethod};
-use core::mem;
+use core::{mem, ops::Deref};
 
 #[cfg(test)]
 mod tests;
@@ -58,6 +58,29 @@ pub struct Timer {
     loading_times: Option<TimeSpan>,
 }
 
+/// A snapshot represents a specific point in time that the timer was observed
+/// at. The snapshot dereferences to the timer. Everything you perceive through
+/// the snapshot is entirely frozen in time.
+pub struct Snapshot<'timer> {
+    timer: &'timer Timer,
+    time: Time,
+}
+
+impl Snapshot<'_> {
+    /// Returns the time the timer was at when the snapshot was taken. The Game
+    /// Time is None if the Game Time has not been initialized.
+    pub fn current_time(&self) -> Time {
+        self.time
+    }
+}
+
+impl Deref for Snapshot<'_> {
+    type Target = Timer;
+    fn deref(&self) -> &Self::Target {
+        self.timer
+    }
+}
+
 /// A Shared Timer is a wrapper around the Timer that can be shared across
 /// multiple threads with multiple owners.
 #[cfg(feature = "std")]
@@ -168,9 +191,7 @@ impl Timer {
         self.phase
     }
 
-    /// Returns the current time of the Timer. The Game Time is None if the
-    /// Game Time has not been initialized.
-    pub fn current_time(&self) -> Time {
+    fn current_time(&self) -> Time {
         let real_time = match self.phase {
             NotRunning => Some(self.run.offset()),
             Running => Some(TimeStamp::now() - self.adjusted_start_time),
@@ -197,6 +218,17 @@ impl Timer {
             .with_game_time(game_time)
     }
 
+    /// Creates a new snapshot of the timer at the point in time of this call.
+    /// It represents a frozen state of the timer such that calculations can
+    /// work with an entirely consistent view of the timer without the current
+    /// time changing underneath.
+    pub fn snapshot(&self) -> Snapshot<'_> {
+        Snapshot {
+            timer: self,
+            time: self.current_time(),
+        }
+    }
+
     /// Returns the currently selected Timing Method.
     #[inline]
     pub fn current_timing_method(&self) -> TimingMethod {


Test patch:
diff --git a/src/analysis/pb_chance/tests.rs b/src/analysis/pb_chance/tests.rs
index 12a46b5..9121f1b 100644
--- a/src/analysis/pb_chance/tests.rs
+++ b/src/analysis/pb_chance/tests.rs
@@ -5,7 +5,7 @@ use crate::tests_helper::{
 use crate::{Timer, TimerPhase};
 
 fn chance(timer: &Timer) -> u32 {
-    (for_timer(timer) * 100.0).round() as _
+    (for_timer(&timer.snapshot()) * 100.0).round() as _
 }
 
 #[test]
diff --git a/src/component/delta/tests.rs b/src/component/delta/tests.rs
index 09f3e09..53b2e4f 100644
--- a/src/component/delta/tests.rs
+++ b/src/component/delta/tests.rs
@@ -14,7 +14,7 @@ fn comparison_text() {
     delta_comp.settings_mut().comparison_override = None;
     assert_eq!(delta_comp.name(), "Delta"); // Displayed in Layout Editor
     assert_eq!(
-        &*delta_comp.state(&timer, &settings).key,
+        &*delta_comp.state(&timer.snapshot(), &settings).key,
         timer.current_comparison()
     ); // Displayed in Layout
 
@@ -22,26 +22,26 @@ fn comparison_text() {
     let mut comp = "Personal Best";
     delta_comp.settings_mut().comparison_override = Some(comp.to_owned());
     assert_eq!(delta_comp.name(), format!("Delta ({})", comp));
-    assert_eq!(&*delta_comp.state(&timer, &settings).key, comp);
+    assert_eq!(&*delta_comp.state(&timer.snapshot(), &settings).key, comp);
 
     // Good Override
     comp = "Best Segments";
     delta_comp.settings_mut().comparison_override = Some(comp.to_owned());
     assert_eq!(delta_comp.name(), format!("Delta ({})", comp));
-    assert_eq!(&*delta_comp.state(&timer, &settings).key, comp);
+    assert_eq!(&*delta_comp.state(&timer.snapshot(), &settings).key, comp);
 
     // Good Override
     comp = "None";
     delta_comp.settings_mut().comparison_override = Some(comp.to_owned());
     assert_eq!(delta_comp.name(), format!("Delta ({})", comp));
-    assert_eq!(&*delta_comp.state(&timer, &settings).key, comp);
+    assert_eq!(&*delta_comp.state(&timer.snapshot(), &settings).key, comp);
 
     // Bad Override
     comp = "Fake Comparison";
     delta_comp.settings_mut().comparison_override = Some(comp.to_owned());
     assert_eq!(delta_comp.name(), format!("Delta ({})", comp));
     assert_eq!(
-        &*delta_comp.state(&timer, &settings).key,
+        &*delta_comp.state(&timer.snapshot(), &settings).key,
         timer.current_comparison()
     );
 }
diff --git a/src/component/detailed_timer/tests.rs b/src/component/detailed_timer/tests.rs
index 34f89a8..6556fce 100644
--- a/src/component/detailed_timer/tests.rs
+++ b/src/component/detailed_timer/tests.rs
@@ -21,7 +21,12 @@ fn prepare() -> (Timer, Component, GeneralLayoutSettings) {
 fn doesnt_show_segment_name_outside_attempt() {
     let (timer, mut component, layout_settings) = prepare();
 
-    assert_eq!(component.state(&timer, &layout_settings).segment_name, None);
+    assert_eq!(
+        component
+            .state(&timer.snapshot(), &layout_settings)
+            .segment_name,
+        None
+    );
 }
 
 #[test]
@@ -32,7 +37,7 @@ fn shows_segment_name_during_attempt() {
 
     assert_eq!(
         component
-            .state(&timer, &layout_settings)
+            .state(&timer.snapshot(), &layout_settings)
             .segment_name
             .unwrap(),
         "foo",
@@ -48,7 +53,7 @@ fn shows_segment_name_at_the_end_of_an_attempt() {
 
     assert_eq!(
         component
-            .state(&timer, &layout_settings)
+            .state(&timer.snapshot(), &layout_settings)
             .segment_name
             .unwrap(),
         "foo",
@@ -63,7 +68,12 @@ fn stops_showing_segment_name_when_resetting() {
     timer.split();
     timer.reset(true);
 
-    assert_eq!(component.state(&timer, &layout_settings).segment_name, None);
+    assert_eq!(
+        component
+            .state(&timer.snapshot(), &layout_settings)
+            .segment_name,
+        None
+    );
 }
 
 #[test]
@@ -71,13 +81,13 @@ fn doesnt_show_icon_outside_attempt() {
     let (timer, mut component, layout_settings) = prepare();
 
     assert!(component
-        .state(&timer, &layout_settings)
+        .state(&timer.snapshot(), &layout_settings)
         .icon_change
         .filter(|i| i.is_empty())
         .is_some());
 
     assert!(component
-        .state(&timer, &layout_settings)
+        .state(&timer.snapshot(), &layout_settings)
         .icon_change
         .is_none());
 }
@@ -86,18 +96,18 @@ fn doesnt_show_icon_outside_attempt() {
 fn shows_icon_during_attempt() {
     let (mut timer, mut component, layout_settings) = prepare();
 
-    component.state(&timer, &layout_settings);
+    component.state(&timer.snapshot(), &layout_settings);
 
     timer.start();
 
     assert!(component
-        .state(&timer, &layout_settings)
+        .state(&timer.snapshot(), &layout_settings)
         .icon_change
         .filter(|s| !s.is_empty())
         .is_some());
 
     assert!(component
-        .state(&timer, &layout_settings)
+        .state(&timer.snapshot(), &layout_settings)
         .icon_change
         .is_none());
 }
@@ -106,16 +116,16 @@ fn shows_icon_during_attempt() {
 fn still_shows_icon_of_last_segment_at_the_end_of_an_attempt() {
     let (mut timer, mut component, layout_settings) = prepare();
 
-    component.state(&timer, &layout_settings);
+    component.state(&timer.snapshot(), &layout_settings);
 
     timer.start();
 
-    component.state(&timer, &layout_settings);
+    component.state(&timer.snapshot(), &layout_settings);
 
     timer.split();
 
     assert!(component
-        .state(&timer, &layout_settings)
+        .state(&timer.snapshot(), &layout_settings)
         .icon_change
         .is_none());
 }
@@ -124,26 +134,26 @@ fn still_shows_icon_of_last_segment_at_the_end_of_an_attempt() {
 fn stops_showing_icon_when_resetting() {
     let (mut timer, mut component, layout_settings) = prepare();
 
-    component.state(&timer, &layout_settings);
+    component.state(&timer.snapshot(), &layout_settings);
 
     timer.start();
 
-    component.state(&timer, &layout_settings);
+    component.state(&timer.snapshot(), &layout_settings);
 
     timer.split();
 
-    component.state(&timer, &layout_settings);
+    component.state(&timer.snapshot(), &layout_settings);
 
     timer.reset(true);
 
     assert!(component
-        .state(&timer, &layout_settings)
+        .state(&timer.snapshot(), &layout_settings)
         .icon_change
         .filter(|i| i.is_empty())
         .is_some());
 
     assert!(component
-        .state(&timer, &layout_settings)
+        .state(&timer.snapshot(), &layout_settings)
         .icon_change
         .is_none());
 }
@@ -152,16 +162,16 @@ fn stops_showing_icon_when_resetting() {
 fn shows_icon_again_when_remounting() {
     let (mut timer, mut component, layout_settings) = prepare();
 
-    component.state(&timer, &layout_settings);
+    component.state(&timer.snapshot(), &layout_settings);
 
     timer.start();
 
-    component.state(&timer, &layout_settings);
+    component.state(&timer.snapshot(), &layout_settings);
 
     component.remount();
 
     assert!(component
-        .state(&timer, &layout_settings)
+        .state(&timer.snapshot(), &layout_settings)
         .icon_change
         .filter(|s| !s.is_empty())
         .is_some());
@@ -169,7 +179,7 @@ fn shows_icon_again_when_remounting() {
     component.remount();
 
     assert!(component
-        .state(&timer, &layout_settings)
+        .state(&timer.snapshot(), &layout_settings)
         .icon_change
         .filter(|s| !s.is_empty())
         .is_some());
diff --git a/src/component/splits/tests/column.rs b/src/component/splits/tests/column.rs
index b027377..8f750a1 100644
--- a/src/component/splits/tests/column.rs
+++ b/src/component/splits/tests/column.rs
@@ -494,41 +494,41 @@ fn check_columns(
         ..Default::default()
     });
 
-    let state = component.state(&timer, &layout_settings);
+    let state = component.state(&timer.snapshot(), &layout_settings);
     check_column_state(&state, 0, &expected_values);
 
     timer.set_game_time(TimeSpan::from_seconds(8.5));
     timer.split();
 
-    let state = component.state(&timer, &layout_settings);
+    let state = component.state(&timer.snapshot(), &layout_settings);
     check_column_state(&state, 1, &expected_values);
 
     timer.skip_split();
 
-    let state = component.state(&timer, &layout_settings);
+    let state = component.state(&timer.snapshot(), &layout_settings);
     check_column_state(&state, 2, &expected_values);
 
     timer.set_game_time(TimeSpan::from_seconds(10.0));
     timer.split();
 
-    let state = component.state(&timer, &layout_settings);
+    let state = component.state(&timer.snapshot(), &layout_settings);
     check_column_state(&state, 3, &expected_values);
 
     timer.set_game_time(TimeSpan::from_seconds(17.5));
     timer.split();
 
-    let state = component.state(&timer, &layout_settings);
+    let state = component.state(&timer.snapshot(), &layout_settings);
     check_column_state(&state, 4, &expected_values);
 
     timer.skip_split();
 
-    let state = component.state(&timer, &layout_settings);
+    let state = component.state(&timer.snapshot(), &layout_settings);
     check_column_state(&state, 5, &expected_values);
 
     timer.set_game_time(TimeSpan::from_seconds(25.0));
     timer.split();
 
-    let state = component.state(&timer, &layout_settings);
+    let state = component.state(&timer.snapshot(), &layout_settings);
     check_column_state(&state, 6, &expected_values);
 }
 
@@ -1083,25 +1083,25 @@ fn check_columns_update_trigger(
         ..Default::default()
     });
 
-    let state = component.state(&timer, &layout_settings);
+    let state = component.state(&timer.snapshot(), &layout_settings);
     // Timer at 0, contextual shouldn't show live times yet
     check_column_state(&state, 0, &expected_values);
 
     timer.set_game_time(TimeSpan::from_seconds(2.0));
 
-    let state = component.state(&timer, &layout_settings);
+    let state = component.state(&timer.snapshot(), &layout_settings);
     // Shorter than the best segment, contextual shouldn't show live times yet
     check_column_state(&state, 1, &expected_values);
 
     timer.set_game_time(TimeSpan::from_seconds(3.5));
 
-    let state = component.state(&timer, &layout_settings);
+    let state = component.state(&timer.snapshot(), &layout_settings);
     // Longer than the best segment, contextual should show live times
     check_column_state(&state, 2, &expected_values);
 
     timer.set_game_time(TimeSpan::from_seconds(5.5));
 
-    let state = component.state(&timer, &layout_settings);
+    let state = component.state(&timer.snapshot(), &layout_settings);
     // Behind the personal best, contextual should show live times
     check_column_state(&state, 3, &expected_values);
 
@@ -1109,14 +1109,14 @@ fn check_columns_update_trigger(
     timer.skip_split();
     timer.set_game_time(TimeSpan::from_seconds(11.0));
 
-    let state = component.state(&timer, &layout_settings);
+    let state = component.state(&timer.snapshot(), &layout_settings);
     // Shorter than the combined best segment, contextual shouldn't show live
     // times yet
     check_column_state(&state, 4, &expected_values);
 
     timer.set_game_time(TimeSpan::from_seconds(13.0));
 
-    let state = component.state(&timer, &layout_settings);
+    let state = component.state(&timer.snapshot(), &layout_settings);
     // Longer than the combined best segment, contextual should show live times
     check_column_state(&state, 5, &expected_values);
 
@@ -1125,13 +1125,13 @@ fn check_columns_update_trigger(
     timer.skip_split();
     timer.set_game_time(TimeSpan::from_seconds(29.0));
 
-    let state = component.state(&timer, &layout_settings);
+    let state = component.state(&timer.snapshot(), &layout_settings);
     // Not behind the personal best, contextual should not show live times yet
     check_column_state(&state, 6, &expected_values);
 
     timer.set_game_time(TimeSpan::from_seconds(31.0));
 
-    let state = component.state(&timer, &layout_settings);
+    let state = component.state(&timer.snapshot(), &layout_settings);
     // Behind the personal best, contextual should show live times only if a
     // split time or delta column
     check_column_state(&state, 7, &expected_values);
@@ -1160,13 +1160,13 @@ fn column_delta_best_segment_colors() {
         ..Default::default()
     });
 
-    let state = component.state(&timer, &layout_settings);
+    let state = component.state(&timer.snapshot(), &layout_settings);
     check_column_color(&state, 0, Text);
 
     timer.set_game_time(TimeSpan::from_seconds(5.1));
     timer.split();
 
-    let state = component.state(&timer, &layout_settings);
+    let state = component.state(&timer.snapshot(), &layout_settings);
     // 5.1 is longer than the best segment of 5.0, so this isn't a best segment
     check_column_color(&state, 0, Text);
 
@@ -1174,13 +1174,13 @@ fn column_delta_best_segment_colors() {
     timer.set_game_time(TimeSpan::from_seconds(4.9));
     timer.split();
 
-    let state = component.state(&timer, &layout_settings);
+    let state = component.state(&timer.snapshot(), &layout_settings);
     // 4.9 is shorter than the best segment of 5.0, so this is a best segment
     check_column_color(&state, 0, Best);
 
     timer.skip_split();
 
-    let state = component.state(&timer, &layout_settings);
+    let state = component.state(&timer.snapshot(), &layout_settings);
     // After skipping a split, the first best segment should stay
     check_column_color(&state, 0, Best);
     // The skipped split is not a best segment
@@ -1189,7 +1189,7 @@ fn column_delta_best_segment_colors() {
     timer.set_game_time(TimeSpan::from_seconds(12.0));
     timer.split();
 
-    let state = component.state(&timer, &layout_settings);
+    let state = component.state(&timer.snapshot(), &layout_settings);
     // The first best segment should stay
     check_column_color(&state, 0, Best);
     // The second split is still skipped, so still not a best segment
@@ -1202,7 +1202,7 @@ fn column_delta_best_segment_colors() {
     timer.set_game_time(TimeSpan::from_seconds(11.8));
     timer.split();
 
-    let state = component.state(&timer, &layout_settings);
+    let state = component.state(&timer.snapshot(), &layout_settings);
     // The first best segment should stay
     check_column_color(&state, 0, Best);
     // The second split is still skipped, so still not a best segment
@@ -1214,14 +1214,14 @@ fn column_delta_best_segment_colors() {
     timer.set_game_time(TimeSpan::from_seconds(21.0));
     timer.split();
 
-    let state = component.state(&timer, &layout_settings);
+    let state = component.state(&timer.snapshot(), &layout_settings);
     // The best segment is empty, so the segment of 9.2 is a best segment
     check_column_color(&state, 3, Best);
 
     timer.set_game_time(TimeSpan::from_seconds(28.9));
     timer.split();
 
-    let state = component.state(&timer, &layout_settings);
+    let state = component.state(&timer.snapshot(), &layout_settings);
     // The segment of 7.9 is shorter than the best segment of 8.0, so this is a best segment
     check_column_color(&state, 4, Best);
 
@@ -1229,7 +1229,7 @@ fn column_delta_best_segment_colors() {
     timer.set_game_time(TimeSpan::from_seconds(29.1));
     timer.split();
 
-    let state = component.state(&timer, &layout_settings);
+    let state = component.state(&timer.snapshot(), &layout_settings);
     // The segment of 8.1 is longer than the best segment of 8.0, so this is not a best segment
     check_column_color(&state, 4, Text);
 }
@@ -1267,7 +1267,7 @@ fn delta_or_split_time() {
         ],
     );
     check_column_state(
-        &component.state(&timer, &layout_settings),
+        &component.state(&timer.snapshot(), &layout_settings),
         0,
         &[(
             ["0:05", "0:06", "0:15", "0:20", "0:25", "0:30"],
@@ -1284,7 +1284,7 @@ fn delta_or_split_time() {
         &[Some(4.0), None, Some(8.0), Some(12.0), None, Some(20.0)],
     );
     check_column_state(
-        &component.state(&timer, &layout_settings),
+        &component.state(&timer.snapshot(), &layout_settings),
         0,
         &[(
             ["−1.0", "—", "−7.0", "−8.0", "—", "−10.0"],
@@ -1309,7 +1309,7 @@ fn delta_or_split_time() {
         ],
     );
     check_column_state(
-        &component.state(&timer, &layout_settings),
+        &component.state(&timer.snapshot(), &layout_settings),
         0,
         &[(
             ["−2.0", "0:04", "−2.0", "−4.0", "0:12", "−6.0"],
diff --git a/src/component/splits/tests/mod.rs b/src/component/splits/tests/mod.rs
index a866df0..c8cbb80 100644
--- a/src/component/splits/tests/mod.rs
+++ b/src/component/splits/tests/mod.rs
@@ -19,19 +19,19 @@ fn zero_visual_split_count_always_shows_all_splits() {
         ..Default::default()
     });
 
-    let mut state = component.state(&timer, &layout_settings);
+    let mut state = component.state(&timer.snapshot(), &layout_settings);
     assert_eq!(state.splits.len(), 32);
 
     component.scroll_down();
-    state = component.state(&timer, &layout_settings);
+    state = component.state(&timer.snapshot(), &layout_settings);
     assert_eq!(state.splits.len(), 32);
 
     component.scroll_down();
-    state = component.state(&timer, &layout_settings);
+    state = component.state(&timer.snapshot(), &layout_settings);
     assert_eq!(state.splits.len(), 32);
 
     component.scroll_up();
-    state = component.state(&timer, &layout_settings);
+    state = component.state(&timer.snapshot(), &layout_settings);
     assert_eq!(state.splits.len(), 32);
 }
 
@@ -59,7 +59,7 @@ fn negative_segment_times() {
     timer.pause_game_time();
     timer.set_game_time(TimeSpan::from_seconds(-1.0));
 
-    let state = component.state(&timer, &layout_settings);
+    let state = component.state(&timer.snapshot(), &layout_settings);
     assert_eq!(state.splits[0].columns[0].value, "−0:01");
 }
 
@@ -77,7 +77,7 @@ fn unique_split_indices() {
         ..Default::default()
     });
 
-    let state = component.state(&timer, &Default::default());
+    let state = component.state(&timer.snapshot(), &Default::default());
 
     let mut indices = state
         .splits
diff --git a/tests/rendering.rs b/tests/rendering.rs
index 06b594e..98d59af 100644
--- a/tests/rendering.rs
+++ b/tests/rendering.rs
@@ -42,7 +42,7 @@ fn default() {
     tests_helper::start_run(&mut timer);
     tests_helper::make_progress_run_with_splits_opt(&mut timer, &[Some(5.0), None, Some(10.0)]);
 
-    let state = layout.state(&timer);
+    let state = layout.state(&timer.snapshot());
 
     check(&state, "luCCVRJIPLE=", "default");
 }
@@ -55,7 +55,11 @@ fn actual_split_file() {
     let timer = Timer::new(run).unwrap();
     let mut layout = Layout::default_layout();
 
-    check(&layout.state(&timer), "jMDEKxBAPLE=", "actual_split_file");
+    check(
+        &layout.state(&timer.snapshot()),
+        "jMDEKxBAPLE=",
+        "actual_split_file",
+    );
 }
 
 #[test]
@@ -64,7 +68,12 @@ fn wsplit() {
     let timer = Timer::new(run).unwrap();
     let mut layout = lsl(layout_files::WSPLIT);
 
-    check_dims(&layout.state(&timer), [250, 300], "j/jc3dn8t/c=", "wsplit");
+    check_dims(
+        &layout.state(&timer.snapshot()),
+        [250, 300],
+        "j/jc3dn8t/c=",
+        "wsplit",
+    );
 }
 
 #[test]
@@ -78,7 +87,7 @@ fn all_components() {
         &[Some(10.0), None, Some(20.0), Some(55.0)],
     );
 
-    let state = layout.state(&timer);
+    let state = layout.state(&timer.snapshot());
 
     check_dims(&state, [300, 800], "4eH3scnJt7E=", "all_components");
 
@@ -93,10 +102,11 @@ fn score_split() {
     let timer = Timer::new(run).unwrap();
     let mut layout = Layout::default_layout();
 
-    let mut state = layout.state(&timer);
+    let mut state = layout.state(&timer.snapshot());
     let prev_seg = state.components.pop().unwrap();
     state.components.pop();
-    let mut timer_state = timer::Component::new().state(&timer, layout.general_settings());
+    let mut timer_state =
+        timer::Component::new().state(&timer.snapshot(), layout.general_settings());
     timer_state.time = "50346".into();
     timer_state.fraction = "PTS".into();
     state.components.push(ComponentState::Timer(timer_state));
@@ -111,7 +121,11 @@ fn dark_layout() {
     let timer = Timer::new(run).unwrap();
     let mut layout = lsl(layout_files::DARK);
 
-    check(&layout.state(&timer), "D8IAQiBYxgM=", "dark_layout");
+    check(
+        &layout.state(&timer.snapshot()),
+        "D8IAQiBYxgM=",
+        "dark_layout",
+    );
 }
 
 #[test]
@@ -127,7 +141,7 @@ fn subsplits_layout() {
     );
 
     check_dims(
-        &layout.state(&timer),
+        &layout.state(&timer.snapshot()),
         [300, 800],
         "8/Pz4/Pz/8c=",
         "subsplits_layout",
@@ -150,7 +164,7 @@ fn display_two_rows() {
     layout.push(component);
 
     check_dims(
-        &layout.state(&timer),
+        &layout.state(&timer.snapshot()),
         [200, 100],
         "R00aWs1J9sE=",
         "display_two_rows",
@@ -173,7 +187,7 @@ fn single_line_title() {
     layout.push(component);
 
     check_dims(
-        &layout.state(&timer),
+        &layout.state(&timer.snapshot()),
         [300, 60],
         "QCRbT0dxaAE=",
         "single_line_title",
@@ -207,7 +221,7 @@ fn horizontal() {
     );
 
     check_dims(
-        &layout.state(&timer),
+        &layout.state(&timer.snapshot()),
         [1500, 40],
         "YmJicmJSUmM=",
         "horizontal",