Skip to content
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

v4: Throttling, Debouncing, and Limiting #271

Closed
TysonMN opened this issue Aug 24, 2020 · 29 comments
Closed

v4: Throttling, Debouncing, and Limiting #271

TysonMN opened this issue Aug 24, 2020 · 29 comments

Comments

@TysonMN
Copy link
Member

TysonMN commented Aug 24, 2020

wrapDispatch was removed from v4 in PR #256 (because it was deemed inferior to a mutually exclusive feature). A partial migration path is to implement stream adjustment behavior like throttling, debouncing, and limiting. This partial migration path will help at least one user (c.f. #269).

As a first step, I want to precisely define what I mean by throttling, debouncing, and limiting, and I want to find existing implementations of these behaviors.

@Evangelink
Copy link
Contributor

+1 user for the throttling function. I am using it for a slider.

@TysonMN
Copy link
Member Author

TysonMN commented Jan 26, 2021

A slider is what original motivated me to add wrapDispatch. (See #114.)

@cmeeren
Copy link
Member

cmeeren commented Jan 26, 2021

@Evangelink Could you please verify that the throttling is actually necessary? IIRC, when we have discussed this issue previously, it seemed that WPF throttles/debounces itself (e.g. for sliders), and thus it may not be needed at all in Elmish.WPF. (It may, in other words, be a premature optimization.) AFAIK, so far we haven't had any explanation/demonstration of a use-case where throttling/debouncing/etc. it is actually needed (#269 (comment)).

@Evangelink
Copy link
Contributor

I might be doing something wrong but I confirm that I had to recently introduce the throttle with the dispatchWrap as described in another ticket because of the slowness when dragging the thumb index on the slider. Actually that's not exactly a slowness but more of a thumb index acting jaggy (doing being jumps forward and backward). I didn't noticed that before but I am not sure if that's a miss, the fact that the model was a lot simpler or because the slider wasn't yet styled. We have changed the control template to make it look like youtube bar so maybe we broke something.

I will try to create a small app with a simple model and my slider to see if that's related. Not sure exactly when I will be able to make this but I will try to have it done by the end of the week.

@Evangelink
Copy link
Contributor

@cmeeren I have created a little project which kind of reproduce the behavior (jaggy effect) I am having with the third-party component on my real app https://github.com/Evangelink/Elmish.Wpf.Experiments/tree/main/ElmishThrottle

Launch the app, click and drag the thumb forward and/or backward and you should notice the behavior (frames jump).

@TysonMN
Copy link
Member Author

TysonMN commented Feb 9, 2021

@Evangelink, can you also create and share videos or GIFs of this jaggy behavior and then the same of the improved behavior when throttling?

@Evangelink
Copy link
Contributor

Jaggy behavior (there is a bounce back and worth while dragging in only one direction):
jaggy

With the throttle, there is a bit of latency (thumb is not at the exact mouse position) but the jaggy behavior disappear:
throttled

@cmeeren
Copy link
Member

cmeeren commented Feb 9, 2021

IMHO both of those are poor UX. Ideally it should be debounced in the UI and the slider should not be locked to the exact value from the Elmish model, to ensure a smooth UI, though I have no idea how to make that work cleanly with MVU (perhaps my one main gripes with it).

@Evangelink
Copy link
Contributor

I was digging even more on this subject and noticed that there is a Delay property on the binding (I wasn't aware of it) and it does seem to provide an even better UX (I will need to find the right threshold but it looks good):

2021-02-09_13h26_54

@TysonMN
Copy link
Member Author

TysonMN commented Feb 9, 2021

Can you share a snippet of code in which you use this delay?

@Evangelink
Copy link
Contributor

Sure, here you go (note that the important part is on the binding of Value):

<Slider Margin="10" IsSnapToTickEnabled="True"
        IsMoveToPointEnabled="True" IsSelectionRangeEnabled="False" LargeChange="5"
        Maximum="{Binding FrameMaxIndex, FallbackValue=1, TargetNullValue=1}"
        Minimum="0" SmallChange="1"
        Value="{Binding CurrentFrameIndex, Mode=TwoWay, Delay=100}" />

You can also see Evangelink/Elmish.Wpf.Experiments#3

@TysonMN
Copy link
Member Author

TysonMN commented Feb 9, 2021

Excellent!

The BindingBase.Delay documentation says

Gets or sets the amount of time, in milliseconds, to wait before updating the binding source after the value on the target changes.
[...]
To avoid updating the source object with every keystroke, set the Delay property to a reasonable value to cause the binding to update only after that amount of time has elapsed since the user stopped typing.

In my understanding of the difference between throttling, debouncing, and limiting, this delay feature of a WPF binding is implementing debouncing where a single "bounce" is passed along at the end of the relevant interval. See this SO answer, which links to this beautiful demo. The debouncing in that demo is the same as WPF's binding delay feature (i.e. both pass along a single event at the end...instead of the beginning...of the relevant interval).

Here is a video of that behavior in @Evangelink's after branch after making the highlighted change so that the console logging is visible.
2021-02-09_09-54-19_513

@TysonMN
Copy link
Member Author

TysonMN commented Feb 9, 2021

As a first steps, I want to precisely define what I mean by throttling, debouncing, and limiting, and I want to find existing implementations of these behaviors.

Here is that beautiful demo again of throttling vs debouncing. I think each of those also has a choice of passing along an event at the beginning of the corresponding interval or at the end. Both of those do so at the end. I think of the difference between throttling and (rate) limiting as whether data is lost. I think of throttling drops data to ensure a maximum rate is not exceeded. I think of rate limiting using a queue to store all incoming events (so no data is lost) and then pass them along at a rate that does not exceed a given maximum. I get the impression that some people define rate limiting the same as I have defined throttling.

@xperiandri
Copy link

So is dispatch wrapping no longer available at all?
Delay on `Binding is not supported on UWP

@TysonMN
Copy link
Member Author

TysonMN commented Mar 8, 2021

So is dispatch wrapping no longer available at all?

Correct. (c.f. #364 (comment)).

Delay on `Binding is not supported on UWP

Ok.

As I said in #364 (comment), I think we can implement throttling in a way that is compatible with mapMsg. This implementation could exist in a different NuGet package called Elmish.WPF.Reactive that depends on FSharp.Control.Reactive.

I am probably oversimplifying there and missing some issues. However, I don't plan to look into this until after I have the composable binding API complete (c.f. #263).

@xperiandri
Copy link

@TysonMN so what are we going to do with throttling? So that I can migrate Elmish.Uno to be compatible with Elmish.WPF v4

@TysonMN
Copy link
Member Author

TysonMN commented Jul 13, 2021

What is your use case for throttling?

I haven't spent any time working on throttling, deboucing, and limiting because I am still working on the composable binding API, but I still think they will be possible to implement.

@xperiandri
Copy link

Entering text to text fields and calling API with that text automatically

@xperiandri
Copy link

Like autocomplte

@TysonMN
Copy link
Member Author

TysonMN commented Jul 14, 2021

That is a very good use case. Thanks for mentioning it.

@TysonMN
Copy link
Member Author

TysonMN commented Aug 13, 2021

Great news!

I just added to the master branch support for composable monomorphic dispatch wrapping. By monomorphic, I mean the signature of wrapDispatch is (obj -> unit) -> obj -> unit. In the v3 branch, the type of wrapDispatch is ('msg -> unit) -> 'msg -> unit, which is polymorphic (with type parameter 'msg).

To see this in action, try using the slider in the SingleCounter sample in the branch example/wrap_dispatch. It uses System.Reactive. Only now as I am typing this and rereading this issue did I realize that using FSharp.Control.Reactive would probably be better. Improving this is a problem for future me.

2021-08-12_23-02-36_665

@xperiandri, is this feature sufficient for your use case?

(CC @cmeeren: Just wanted to bring to your attention this great improvement. No need to replay.)

@xperiandri
Copy link

What is the main difference to v3 implementation?

@xperiandri
Copy link

Why signature has changed?

@TysonMN
Copy link
Member Author

TysonMN commented Aug 15, 2021

Sorry for the delayed response. I haven't had time to write a clear(er) explanation.

And actually, I still don't. I am going to delay my response further because I have an idea that might allow me to improve this feature. This might take a week. Stay tuned!

@TysonMN
Copy link
Member Author

TysonMN commented Aug 20, 2021

What is the main difference to v3 implementation?
Why signature has changed?

These answer are a bit moot now (see next comment), but here are brief answers.

The signature of the wrapDispatch parameter in v3 is

('msg -> unit) -> 'msg -> unit

while the signature of the parameter demonstrated in the above code is

(obj -> unit) -> obj -> unit

This feature is unsound in the sense that it is possible for the user to write code involving it that complies but fails at runtime due to an InvalidCastException. However, I think it is better than no dispatch wrapping.

I changed the 'msg type parameter to obj because I couldn't figure out how to make it work with 'msg.

But forget about this. I now have something better!

@TysonMN
Copy link
Member Author

TysonMN commented Aug 20, 2021

Dispatch wrapping is back! See the branch polymorphic_dispatch_wrapping for a proof of concept. (I still need to do lots of clean up.)

2021-08-19_20-24-34_675

This time, dispatch wrapping is

  • sound,
  • polymorphic, and
  • covariant in 'msg (in the sense that the returned dispatch function doesn't have to have the same type as the given dispatch function...and the way in which it is is allowed to differ is consistent with the covariance of 'msg).

It couldn't be any better!! :D

So @xperiandri, you will not lose any functionality (related to dispatch wrapping) in v4. In fact, it will be slightly more expressive than it was before.

I opened this issue because I thought I would have to introduce a breaking change in v4 by removing all of the optional wrapDispatch parameters. Now those can be added back so that it is not a breaking change. I will deprecate them though and remove them in the next major version. I will create a new issue to track this work.

I don't like the name wrapDispatch. I don't think the user thinks about dispatching (which is essentially enqueuing a message into the Elmish message queue).

Instead, of the name function name addWrapDispatch, my current favorite name is alterMsgStream.

(CC @cmeeren: Just wanted to bring to your attention this even greater improvement. No need to replay.)

@TysonMN TysonMN closed this as completed Aug 20, 2021
@xperiandri
Copy link

xperiandri commented Aug 20, 2021

👏
Sounds amazing! I will have a look

@cmeeren
Copy link
Member

cmeeren commented Aug 20, 2021

This is excellent, @TysonMN! 😁 Good work! 👍

@TysonMN
Copy link
Member Author

TysonMN commented Aug 22, 2021

I now better understand the limits of F# against which I was fighting. Below is a rather minimal reproduction. It doesn't perfectly reproduce the problem, but it is close.


First, the recursion I need seems particularly difficult for F#'s type inference. To compensate, I need to make everything explicit: argument types, the return type, and even type parameters.

One of the last things I figure out or tried was specifying the return type. Without specifying the return type (of the method Initialize), I get this error.

Error FS0006 A use of the function 'Initalize' does not match a type inferred elsewhere. The inferred type of the function is
Elmish.WPF.VmBinding2<'model,'msg> -> Microsoft.FSharp.Core.unit -> Elmish.WPF.VmBinding<'model,'msg> Microsoft.FSharp.Core.option.
The type of the function required at this point of use is
Elmish.WPF.VmBinding2<'model,'msg> -> Microsoft.FSharp.Core.unit -> Elmish.WPF.VmBinding<'model,'msg> Microsoft.FSharp.Core.option
This error may be due to limitations associated with generic recursion within a 'let rec' collection or within a group of classes. Consider giving a full type signature for the targets of recursive calls including type annotations for both argument and return types.

Notice that the type I provided and the desired type are the same but the F# compiler seems to suggest they are different. Even so, it was the suggestion at the bottom that lead me to add a type annotation for the return type on each of my recursive functions.

In my reproduction below, there is a less informative warning message when the return type is not specified.

Warning FS0064 This construct causes code to be less generic than indicated by the type annotations. The type variable 'b has been constrained to be type 'obj'.

I constantly saw this message at other times during my coding with 'model and 'msg in place of 'b.


Second, in order to have explicit type parameters, I either need to use a method or a function in a module. I cannot use a function in a class. Trying to do so results in this error.

Error FS0665 Explicit type parameters may only be used on module or member bindings

Because of this and because so many of the types involved are depend on each other recursively (and because the 'model and 'msg type parameters are in scope within ViewModel<'model, 'msg>), I think the best way to organize the code is to similar to what I have done in that proof of concept branch. Specifically, these key recursive functions should be defined as methods in classes other than ViewModel<_, _>.


Here is my small (near) reproduction.

module TopLevelModule

[<RequireQualifiedAccess>]
type HeterogeneousValueList<'a> =
  | Empty
  | HeadTail of 'a * HeterogeneousValueList<obj>

[<RequireQualifiedAccess>]
type HeterogeneousFunctionList<'a, 'b> =
  | Empty
  | HeadTail of ('a -> 'b) * HeterogeneousFunctionList<obj, obj>


module HeterogeneousValueList =
  let rec mapAsFunctionBindingInModule<'a, 'b>
      (fs: HeterogeneousFunctionList<'a, 'b>)
      (aList: HeterogeneousValueList<'a>)
      : HeterogeneousValueList<'b> =
    match fs, aList with
    | HeterogeneousFunctionList.Empty, _
    | _, HeterogeneousValueList.Empty ->
        HeterogeneousValueList.Empty
    | HeterogeneousFunctionList.HeadTail(f, fsTail), HeterogeneousValueList.HeadTail (a, aTail) ->
        let b: 'b = f a
        let tail = mapAsFunctionBindingInModule fsTail aTail
        HeterogeneousValueList.HeadTail(b, tail)


type HeterogeneousValueList () =

  //let rec mapAsFunctionBindingInClass<'a, 'b> // FS0665 Explicit type parameters may only be used on module or member bindings
  //    (fs: HeterogeneousFunctionList<'a, 'b>)
  //    (aList: HeterogeneousValueList<'a>)
  //    : HeterogeneousValueList<'b> =
  //  match fs, aList with
  //  | HeterogeneousFunctionList.Empty, _
  //  | _, HeterogeneousValueList.Empty ->
  //      HeterogeneousValueList.Empty
  //  | HeterogeneousFunctionList.HeadTail(f, fsTail), HeterogeneousValueList.HeadTail (a, aTail) ->
  //      let b: 'b = f a
  //      let tail = mapAsFunctionBindingInClass fsTail aTail
  //      HeterogeneousValueList.HeadTail(b, tail)

  static member mapAsMemberBindingInClass<'a, 'b>
      (fs: HeterogeneousFunctionList<'a, 'b>,
       aList: HeterogeneousValueList<'a>)
      : HeterogeneousValueList<'b> =
    match fs, aList with
    | HeterogeneousFunctionList.Empty, _
    | _, HeterogeneousValueList.Empty ->
        HeterogeneousValueList.Empty
    | HeterogeneousFunctionList.HeadTail(f, fsTail), HeterogeneousValueList.HeadTail (a, aTail) ->
        let b: 'b = f a
        let tail = HeterogeneousValueList.mapAsMemberBindingInClass(fsTail, aTail)
        HeterogeneousValueList.HeadTail(b, tail)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants