-
-
Notifications
You must be signed in to change notification settings - Fork 122
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Cmd throttling #1070
base: main
Are you sure you want to change the base?
Cmd throttling #1070
Conversation
@h0lg Thank you very much for your PR. It's looking very good. Would you have an example where |
Sure, please have a closer look at above example. It uses I've tried to describe this here: Feel free to suggest a better name for the concept. Maybe I found this visualization helpful to grasp the difference between throttle and debounce: https://web.archive.org/web/20220117092326/http://demo.nimius.net/debounce_throttle/ |
I've added a type Msg = | Batch of int list // enables batching values of the same type for dispatch
// a factory that batches and dispatches the pending values every 100 ms; takes a value and produces a Cmd
// declare per program or long running background task
let createThrottledMsgBatchCmd = batchedThrottle 100 (fun values ->
System.Diagnostics.Debug.WriteLine("dispatching a batch of values to the MVU loop " + values.ToString())
Batch values)
// an optional wrapper for usage inside of Cmd.ofEffect giving the factory function a dispatch signature
let dispatchToThrottledFactory value =
System.Diagnostics.Debug.WriteLine("dispatching a single value to the throttled batch factory " + value.ToString())
createThrottledMsgBatchCmd value |> List.iter (fun effect -> effect dispatch) // make the MVU dispatch available to the returned command
produceIntegersFast dispatchToThrottledFactory // prevents this function from spamming the MVU loop |
I've added a second return value to let createCmd, awaitNextDispatch = Cmd.batchedThrottle 100 NewValues
... some awaited (!) producers using createCmd ...
// wait until next dispatch plus a little optional buffer to avoid race conditions
let! _ = awaitNextDispatch (Some(TimeSpan.FromMilliseconds(10)))
// I can be sure now all messages have been dispatched What do you think about this pattern? |
473db05
to
8108219
Compare
In the latest iteration I've rewritten This version, I can for example write extensions like type AsyncEnumerableExtensions =
[<Extension>]
static member dispatchTo((this: System.Collections.Generic.IAsyncEnumerable<'result>), (dispatch: 'result -> unit)) =
async {
let results = this.GetAsyncEnumerator()
let rec dispatchResults () =
async {
let! hasNext = results.MoveNextAsync().AsTask() |> Async.AwaitTask
if hasNext then
results.Current |> dispatch
do! dispatchResults ()
}
do! dispatchResults ()
}
[<Extension>]
static member dispatchBatchThrottledTo
(
(this: System.Collections.Generic.IAsyncEnumerable<'result>),
throttleInterval,
(mapPendingResultsToBatchMsg: 'result list -> 'msg),
(dispatch: 'msg -> unit)
) =
async {
// create a throttled dispatch of a batch of pending results at regular intervals
let dispatchSingleResult, awaitNextDispatch =
dispatch.batchThrottled (throttleInterval, mapPendingResultsToBatchMsg)
do! this.dispatchTo dispatchSingleResult // dispatch single results using throttled method
do! awaitNextDispatch (Some throttleInterval) // to make sure all results are dispatched before calling it done
} and then throttle the progress reporting as well as the result dispatch effectively: type Msg
| SearchProgressReports of BatchProgress list
| SearchResults of SearchResult list
| SearchCompleted
let private searchCmd model =
fun dispatch ->
async {
let command = mapToSearchCommand model
let dispatchProgress, awaitNextProgressDispatch = dispatch.batchThrottled(100, SearchProgressReports)
let reporter = Progress<BatchProgress>(dispatchProgress)
use cts = new CancellationTokenSource()
do! searchAsync(command, reporter, cts.Token).dispatchBatchThrottledTo (100, SearchResults, dispatch)
do! awaitNextProgressDispatch (Some 100) // to make sure all progresses are dispatched before calling it done
dispatch SearchCompleted
} |> Async.StartImmediate
|> Cmd.ofEffect Whether - and if, in what form - you want this in Fabulous is up to you. But I found it this helpful to prevent the MVU loop from choking up when feeding too many messages into it too rapidly. |
to prevent misuse
… command factories similar to Cmd.debounce
…o await the next dispatch Should debounce and bufferedThrottle follow the same API?
because it feels more natural to use it that way with a dispatch inside an ofEffect that produces values rapidly
da1f926
to
63f739a
Compare
The throttling methods are intended for stuff like throttling
Progress<>
updates from background tasks like the following search does while yielding results from anIAsyncEnumerable
:I'm a Fabulous and F# freshie, so please have a good hard look at my changes and above intended use case. Feel free to point out anything that looks weird or cumbersome to you - I'm here to learn :)