|
| 1 | +namespace TaskSeq.Tests |
| 2 | + |
| 3 | +open System |
| 4 | +open System.Threading |
| 5 | +open System.Threading.Tasks |
| 6 | +open System.Diagnostics |
| 7 | +open System.Collections.Generic |
| 8 | + |
| 9 | +open Xunit |
| 10 | +open Xunit.Abstractions |
| 11 | +open FsUnit.Xunit |
| 12 | + |
| 13 | +open FSharp.Control |
| 14 | + |
| 15 | +/// Milliseconds |
| 16 | +[<Measure>] |
| 17 | +type ms |
| 18 | + |
| 19 | +/// Microseconds |
| 20 | +[<Measure>] |
| 21 | +type µs |
| 22 | + |
| 23 | +/// Helpers for short waits, as Task.Delay has about 15ms precision. |
| 24 | +/// Inspired by IoT code: https://github.com/dotnet/iot/pull/235/files |
| 25 | +module DelayHelper = |
| 26 | + |
| 27 | + /// <summary> |
| 28 | + /// Delay for at least the specified <paramref name="microseconds"/>. |
| 29 | + /// </summary> |
| 30 | + /// <param name="microseconds">The number of microseconds to delay.</param> |
| 31 | + /// <param name="allowThreadYield"> |
| 32 | + /// True to allow yielding the thread. If this is set to false, on single-proc systems |
| 33 | + /// this will prevent all other code from running. |
| 34 | + /// </param> |
| 35 | + let spinWaitDelay (microseconds: int64<µs>) (allowThreadYield: bool) = |
| 36 | + let start = Stopwatch.GetTimestamp() |
| 37 | + let minimumTicks = int64 microseconds * Stopwatch.Frequency / 1_000_000L |
| 38 | + |
| 39 | + // FIXME: though this is part of official IoT code, the `allowThreadYield` version is extremely slow |
| 40 | + // slower than would be expected from a simple SpinOnce. Though this may be caused by scenarios with |
| 41 | + // many tasks at once. Have to investigate. See perf smoke tests. |
| 42 | + if allowThreadYield then |
| 43 | + let spinWait = SpinWait() |
| 44 | + |
| 45 | + while Stopwatch.GetTimestamp() - start < minimumTicks do |
| 46 | + spinWait.SpinOnce(1) |
| 47 | + |
| 48 | + else |
| 49 | + while Stopwatch.GetTimestamp() - start < minimumTicks do |
| 50 | + Thread.SpinWait(1) |
| 51 | + |
| 52 | + let delayTask (µsecMin: int64<µs>) (µsecMax: int64<µs>) f = task { |
| 53 | + let rnd = Random() |
| 54 | + let rnd () = rnd.NextInt64(int64 µsecMin, int64 µsecMax) * 1L<µs> |
| 55 | + |
| 56 | + // ensure unequal running lengths and points-in-time for assigning the variable |
| 57 | + // DO NOT use Thead.Sleep(), it's blocking! |
| 58 | + // WARNING: Task.Delay only has a 15ms timer resolution!!! |
| 59 | + |
| 60 | + // TODO: check this! The following comment may not be correct |
| 61 | + // this creates a resume state, which seems more efficient than SpinWait.SpinOnce, see DelayHelper. |
| 62 | + let! _ = Task.Delay 0 |
| 63 | + let delay = rnd () |
| 64 | + |
| 65 | + // typical minimum accuracy of Task.Delay is 15.6ms |
| 66 | + // for delay-cases shorter than that, we use SpinWait |
| 67 | + if delay < 15_000L<µs> then |
| 68 | + do spinWaitDelay (rnd ()) false |
| 69 | + else |
| 70 | + do! Task.Delay(int <| float delay / 1_000.0) |
| 71 | + |
| 72 | + return f () |
| 73 | + } |
| 74 | + |
| 75 | +/// <summary> |
| 76 | +/// Creates dummy backgroundTasks with a randomized delay and a mutable state, |
| 77 | +/// to ensure we properly test whether processing is done ordered or not. |
| 78 | +/// Default for <paramref name="µsecMin" /> and <paramref name="µsecMax" /> |
| 79 | +/// are 10,000µs and 30,000µs respectively (or 10ms and 30ms). |
| 80 | +/// </summary> |
| 81 | +type DummyTaskFactory(µsecMin: int64<µs>, µsecMax: int64<µs>) = |
| 82 | + let mutable x = 0 |
| 83 | + |
| 84 | + /// <summary> |
| 85 | + /// Creates dummy tasks with a randomized delay and a mutable state, |
| 86 | + /// to ensure we properly test whether processing is done ordered or not. |
| 87 | + /// Uses the defaults for <paramref name="µsecMin" /> and <paramref name="µsecMax" /> |
| 88 | + /// with 10,000µs and 30,000µs respectively (or 10ms and 30ms). |
| 89 | + /// </summary> |
| 90 | + new() = new DummyTaskFactory(10_000L<µs>, 30_000L<µs>) |
| 91 | + |
| 92 | + |
| 93 | + /// Bunch of delayed tasks that randomly have a yielding delay of 10-30ms, therefore having overlapping execution times. |
| 94 | + member _.CreateDelayedTasks_SideEffect total = [ |
| 95 | + for i in 0 .. total - 1 do |
| 96 | + fun () -> DelayHelper.delayTask µsecMin µsecMax (fun _ -> Interlocked.Increment &x) |
| 97 | + ] |
| 98 | + |
| 99 | +/// Just some dummy task generators, copied over from the base test project, with artificial delays, |
| 100 | +/// mostly to ensure sequential async operation of side effects. |
| 101 | +module Gen = |
| 102 | + /// Joins two tasks using merely BCL methods. This approach is what you can use to |
| 103 | + /// properly, sequentially execute a chain of tasks in a non-blocking, non-overlapping way. |
| 104 | + let joinWithContinuation tasks = |
| 105 | + let simple (t: unit -> Task<_>) (source: unit -> Task<_>) : unit -> Task<_> = |
| 106 | + fun () -> |
| 107 | + source() |
| 108 | + .ContinueWith((fun (_: Task) -> t ()), TaskContinuationOptions.OnlyOnRanToCompletion) |
| 109 | + .Unwrap() |
| 110 | + :?> Task<_> |
| 111 | + |
| 112 | + let rec combine acc (tasks: (unit -> Task<_>) list) = |
| 113 | + match tasks with |
| 114 | + | [] -> acc |
| 115 | + | t :: tail -> combine (simple t acc) tail |
| 116 | + |
| 117 | + match tasks with |
| 118 | + | first :: rest -> combine first rest |
| 119 | + | [] -> failwith "oh oh, no tasks given!" |
| 120 | + |
| 121 | + let joinIdentityHotStarted tasks () = task { return tasks |> List.map (fun t -> t ()) } |
| 122 | + |
| 123 | + let joinIdentityDelayed tasks () = task { return tasks } |
| 124 | + |
| 125 | + let createAndJoinMultipleTasks total joiner : Task<_> = |
| 126 | + // the actual creation of tasks |
| 127 | + let tasks = DummyTaskFactory().CreateDelayedTasks_SideEffect total |
| 128 | + let combinedTask = joiner tasks |
| 129 | + // start the combined tasks |
| 130 | + combinedTask () |
0 commit comments