azul_core/
task.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
use std::{
    sync::{Arc, Mutex, Weak, atomic::{AtomicUsize, Ordering}},
    thread::{self, JoinHandle},
    time::{Duration, Instant},
};
use crate::{
    FastHashMap,
    callbacks::{
        Redraw, DontRedraw, TimerCallback, TimerCallbackInfo, RefAny,
        TimerCallbackReturn, TimerCallbackType, UpdateScreen,
    },
    app_resources::AppResources,
};

/// Should a timer terminate or not - used to remove active timers
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum TerminateTimer {
    /// Remove the timer from the list of active timers
    Terminate,
    /// Do nothing and let the timers continue to run
    Continue,
}

static MAX_DAEMON_ID: AtomicUsize = AtomicUsize::new(0);

/// ID for uniquely identifying a timer
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct TimerId { id: usize }

impl TimerId {
    /// Generates a new, unique `TimerId`.
    pub fn new() -> Self {
        TimerId { id: MAX_DAEMON_ID.fetch_add(1, Ordering::SeqCst) }
    }
}

/// A `Timer` is a function that is run on every frame.
///
/// There are often a lot of visual tasks such as animations or fetching the
/// next frame for a GIF or video, etc. - that need to run every frame or every X milliseconds,
/// but they aren't heavy enough to warrant creating a thread - otherwise the framework
/// would create too many threads, which leads to a lot of context switching and bad performance.
///
/// The callback of a `Timer` should be fast enough to run under 16ms,
/// otherwise running timers will block the main UI thread.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub struct Timer {
    /// Stores when the timer was created (usually acquired by `Instant::now()`)
    pub created: Instant,
    /// When the timer was last called (`None` only when the timer hasn't been called yet).
    pub last_run: Option<Instant>,
    /// If the timer shouldn't start instantly, but rather be delayed by a certain timeframe
    pub delay: Option<Duration>,
    /// How frequently the timer should run, i.e. set this to `Some(Duration::from_millis(16))`
    /// to run the timer every 16ms. If this value is set to `None`, (the default), the timer
    /// will execute the timer as-fast-as-possible (i.e. at a faster framerate
    /// than the framework itself) - which might be  performance intensive.
    pub interval: Option<Duration>,
    /// When to stop the timer (for example, you can stop the
    /// execution after 5s using `Some(Duration::from_secs(5))`).
    pub timeout: Option<Duration>,
    /// Callback to be called for this timer
    pub callback: TimerCallback,
}

impl Timer {

    /// Create a new timer
    pub fn new(callback: TimerCallbackType) -> Self {
        Timer {
            created: Instant::now(),
            last_run: None,
            delay: None,
            interval: None,
            timeout: None,
            callback: TimerCallback(callback),
        }
    }

    /// Delays the timer to not start immediately but rather
    /// start after a certain time frame has elapsed.
    #[inline]
    pub fn with_delay(mut self, delay: Duration) -> Self {
        self.delay = Some(delay);
        self
    }

    /// Converts the timer into a timer, running the function only
    /// if the given `Duration` has elapsed since the last run
    #[inline]
    pub fn with_interval(mut self, interval: Duration) -> Self {
        self.interval = Some(interval);
        self
    }

    /// Converts the timer into a countdown, by giving it a maximum duration
    /// (counted from the creation of the Timer, not the first use).
    #[inline]
    pub fn with_timeout(mut self, timeout: Duration) -> Self {
        self.timeout = Some(timeout);
        self
    }

    /// Crate-internal: Invokes the timer if the timer and
    /// the `self.timeout` allow it to
    pub fn invoke<'a>(&mut self, info: TimerCallbackInfo<'a>) -> TimerCallbackReturn {

        let instant_now = Instant::now();
        let delay = self.delay.unwrap_or_else(|| Duration::from_millis(0));

        // Check if the timers timeout is reached
        if let Some(timeout) = self.timeout {
            if instant_now - self.created > timeout {
                return (DontRedraw, TerminateTimer::Terminate);
            }
        }

        if let Some(interval) = self.interval {
            let last_run = match self.last_run {
                Some(s) => s,
                None => self.created + delay,
            };
            if instant_now - last_run < interval {
                return (DontRedraw, TerminateTimer::Continue);
            }
        }

        let res = (self.callback.0)(info);

        self.last_run = Some(instant_now);

        res
    }
}

/// Simple struct that is used by Azul internally to determine when the thread has finished executing.
/// When this struct goes out of scope, Azul will call `.join()` on the thread (so in order to not
/// block the main thread, simply let it go out of scope naturally.
pub struct DropCheck(Arc<()>);

/// A `Task` is a seperate thread that is owned by the framework.
///
/// In difference to a `Thread`, you don't have to `await()` the result of a `Task`,
/// you can just hand the task to the framework (via `AppResources::add_task`) and
/// the framework will automatically update the UI when the task is finished.
/// This is useful to offload actions such as loading long files, etc. to a background thread.
///
/// Azul will join the thread automatically after it is finished (joining won't block the UI).
pub struct Task {
    // Thread handle of the currently in-progress task
    join_handle: Option<JoinHandle<UpdateScreen>>,
    dropcheck: Weak<()>,
    /// Timer that will run directly after this task is completed.
    pub after_completion_timer: Option<Timer>,
}

pub type TaskCallback<U> = fn(Arc<Mutex<U>>, DropCheck) -> UpdateScreen;

impl Task {

    /// Creates a new task from a callback and a set of input data - which has to be wrapped in an `Arc<Mutex<T>>>`.
    pub fn new<U: Send + 'static>(data: Arc<Mutex<U>>, callback: TaskCallback<U>) -> Self {

        let thread_check = Arc::new(());
        let thread_weak = Arc::downgrade(&thread_check);
        let thread_handle = thread::spawn(move || callback(data, DropCheck(thread_check)));

        Self {
            join_handle: Some(thread_handle),
            dropcheck: thread_weak,
            after_completion_timer: None,
        }
    }

    /// Stores a `Timer` that will run after the task has finished.
    ///
    /// Often necessary to "clean up" or copy data from the background task into the UI.
    #[inline]
    pub fn then(mut self, timer: Timer) -> Self {
        self.after_completion_timer = Some(timer);
        self
    }

    /// Returns true if the task has been finished, false otherwise
    pub fn is_finished(&self) -> bool {
        self.dropcheck.upgrade().is_none()
    }
}

impl Drop for Task {
    fn drop(&mut self) {
        if let Some(thread_handle) = self.join_handle.take() {
            let _ = thread_handle.join().unwrap();
        }
    }
}

/// A `Thread` is a simple abstraction over `std::thread` that allows to offload a pure
/// function to a different thread (essentially emulating async / await for older compilers).
///
/// # Warning
///
/// `Thread` panics if it goes out of scope before `.block()` was called.
pub struct Thread<T> {
    join_handle: Option<JoinHandle<T>>,
}

/// Error that can happen while calling `.block()`
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum BlockError {
    /// Arc::into_inner() failed
    ArcUnlockError,
    /// The background thread panicked
    ThreadJoinError,
    /// Mutex::into_inner() failed
    MutexIntoInnerError,
}

impl<T> Thread<T> {

    /// Creates a new thread that spawns a certain (pure) function on a separate thread.
    /// This is a workaround until `await` is implemented. Note that invoking this function
    /// will create an OS-level thread.
    ///
    /// **Warning**: You *must* call `.await()`, otherwise the `Thread` will panic when it is dropped!
    ///
    /// # Example
    ///
    /// ```rust
    /// # extern crate azul_core;
    /// # use azul_core::task::Thread;
    /// #
    /// fn pure_function(input: usize) -> usize { input + 1 }
    ///
    /// let thread_1 = Thread::new(5, pure_function);
    /// let thread_2 = Thread::new(10, pure_function);
    /// let thread_3 = Thread::new(20, pure_function);
    ///
    /// // thread_1, thread_2 and thread_3 run in parallel here...
    ///
    /// let result_1 = thread_1.block();
    /// let result_2 = thread_2.block();
    /// let result_3 = thread_3.block();
    ///
    /// assert_eq!(result_1, Ok(6));
    /// assert_eq!(result_2, Ok(11));
    /// assert_eq!(result_3, Ok(21));
    /// ```
    pub fn new<U>(initial_data: U, callback: fn(U) -> T) -> Self where T: Send + 'static, U: Send + 'static {
        Self {
            join_handle: Some(thread::spawn(move || callback(initial_data))),
        }
    }

    /// Block until the internal thread has finished and return T
    pub fn block(mut self) -> Result<T, BlockError> {
        // .block() can only be called once, so these .unwrap()s are safe
        let handle = self.join_handle.take().unwrap();
        let data = handle.join().map_err(|_| BlockError::ThreadJoinError)?;
        Ok(data)
    }
}

impl<T> Drop for Thread<T> {
    fn drop(&mut self) {
        if self.join_handle.take().is_some() {
            panic!("Thread has not been await()-ed correctly!");
        }
    }
}

/// Run all currently registered timers
#[must_use]
pub fn run_all_timers(
    timers: &mut FastHashMap<TimerId, Timer>,
    data: &mut RefAny,
    resources: &mut AppResources,
) -> UpdateScreen {

    let mut should_update_screen = DontRedraw;
    let mut timers_to_terminate = Vec::new();

    for (key, timer) in timers.iter_mut() {
        let (should_update, should_terminate) = timer.invoke(TimerCallbackInfo {
            state: data,
            app_resources: resources,
        });

        if should_update == Redraw {
            should_update_screen = Redraw;
        }

        if should_terminate == TerminateTimer::Terminate {
            timers_to_terminate.push(key.clone());
        }
    }

    for key in timers_to_terminate {
        timers.remove(&key);
    }

    should_update_screen
}

/// Remove all tasks that have finished executing
#[must_use]
pub fn clean_up_finished_tasks<T>(
    tasks: &mut Vec<Task>,
    timers: &mut FastHashMap<TimerId, Timer>,
) -> UpdateScreen {

    let old_count = tasks.len();
    let mut timers_to_add = Vec::new();

    tasks.retain(|task| {
        if task.is_finished() {
            if let Some(timer) = task.after_completion_timer {
                timers_to_add.push((TimerId::new(), timer));
            }
            false
        } else {
            true
        }
    });

    let timers_is_empty = timers_to_add.is_empty();
    let new_count = tasks.len();

    // Start all the timers that should run after the completion of the task
    for (timer_id, timer) in timers_to_add {
        timers.insert(timer_id, timer);
    }

    if old_count == new_count && timers_is_empty {
        DontRedraw
    } else {
        Redraw
    }
}