[prev in list] [next in list] [prev in thread] [next in thread]
List: openjdk-openjfx-dev
Subject: Aw: Gauging interest in bindings that can delay changing their value (debounce/throttle)
From: Marius Hanl <mariushanl () web ! de>
Date: 2023-03-30 22:19:30
Message-ID: trinity-3f77e3cf-afa4-4c84-bf34-5410664659f6-1680214770794 () 3c-app-webde-bap33
[Download RAW message or body]
<html><head></head><body><div style="font-family: Verdana;font-size: 12.0px;"><div>+ \
1 for this. Debouncing is a common functionality for observables.</div>
<div>One of the common scenarios is obviously something like a search filter \
functionality, where typing in characters triggers an expensive calculation.</div>
<div>Debouncing solves the problem by doing that when nothing happened for some time, \
which is typically met when the user finished typing.</div>
<div> </div>
<div>-- Marius</div>
<div>
<div>
<div name="quote" style="margin:10px 5px 5px 10px; padding: 10px 0 10px 10px; \
border-left:2px solid #C3D9E5; word-wrap: break-word; -webkit-nbsp-mode: space; \
-webkit-line-break: after-white-space;"> <div style="margin:0 0 10px \
0;"><b>Gesendet:</b> Donnerstag, 23. März 2023 um 18:09 Uhr<br/> \
<b>Von:</b> "John Hendrikx" <john.hendrikx@gmail.com><br/> \
<b>An:</b> openjfx-dev@openjdk.org<br/> <b>Betreff:</b> Gauging interest in \
bindings that can delay changing their value (debounce/throttle)</div>
<div name="quoted-content">Hi list,<br/>
<br/>
I've been working on a potential new API (and proof of concept<br/>
implementation) for adding a new type of fluent binding which can delay<br/>
changing their values, and I'm wondering how much interest there is in<br/>
such a thing.<br/>
<br/>
The main purpose of such an API is to prevent being flooded with changes<br/>
when properties change often, or to simply delay certain actions until<br/>
the user has settled on a selection or has stopped typing.<br/>
<br/>
For this purpose I would like to introduce a default method on<br/>
`ObservableValue` with the signature:<br/>
<br/>
ObservableValue<T> throttle(Throttler throttler);<br/>
<br/>
The parameter `Throttler` can be obtained via static methods of a helper<br/>
class named `FXThrottlers`. These provide various pre-configured<br/>
throttlers that work correctly with JavaFX's event thread model. My<br/>
current proof of concept provides:<br/>
<br/>
public static Throttler debounce(Duration quietPeriod);<br/>
public static Throttler debounceTrailing(Duration \
quietPeriod);<br/> public static Throttler throttle(Duration \
period);<br/> public static Throttler throttleTrailing(Duration \
period);<br/> <br/>
These are variations of similar concepts, and vary mostly in when<br/>
exactly they will allow value changes; debouncers will wait for a period<br/>
without any changes, while throttlers will periodically allow changes.<br/>
The trailing variants will not immediately emit the first change but<br/>
will wait for the period to elapse first; all variants will eventually<br/>
take on the value of the source observable. Debouncing is typically<br/>
used when you wish for an input to settle before taking action (like<br/>
typing in a search bar), while throttling is used to give regular<br/>
feedback but avoid doing so too often (like feedback during window<br/>
resizing).<br/>
<br/>
Usage example which updates a preview panel when the user has finished<br/>
(cursor) scrolling through a list view:<br/>
<br/>
ObjectProperty<T> selectedItem =<br/>
listView.getSelectionModel().selectedItemProperty();<br/>
<br/>
selectedItem<br/>
.throttle(FXThrottlers.debounceTrailing(Duration.ofMillis(500)))<br/>
.addListener((obs, old, current) -> \
{<br/> if \
(current != null) {<br/> \
\
updatePreviewPanel(current);<br/> \
}<br/> \
});<br/> <br/>
Implementation details:<br/>
<br/>
ObservableValue is part of javafx.base, and as such can't use animations<br/>
or call Platform::runLater. The ThrottledBinding implementation has<br/>
abstracted all of these out into the Throttler class, and FXThrottlers<br/>
(which would live in javafx.graphics) therefore provides the necessary<br/>
call backs to integrate property changes correctly back onto the JavaFX<br/>
event thread. The Throttler class also simplifies testing; the test can<br/>
provide its own timing source and background scheduler. The Throttler<br/>
interface has the following methods:<br/>
<br/>
/**<br/>
* Schedules a command to run on an unspecified thread after \
the time<br/> * given by {@code nanos} elapses.<br/>
*<br/>
* @param command a command to run, cannot be {@code \
null}<br/> * @param nanos a time in nanoseconds<br/>
*/<br/>
void schedule(Runnable command, long nanos);<br/>
<br/>
/**<br/>
* Provides the current time in nanoseconds.<br/>
*<br/>
* @return the current time in nanoseconds<br/>
*/<br/>
long nanoTime();<br/>
<br/>
/**<br/>
* Runs the given command as soon as possible on a thread \
specified<br/> by this<br/>
* throttler for updating property values.<br/>
*<br/>
* @param command a command to run, cannot be {@code \
null}<br/> */<br/>
void update(Runnable command);<br/>
<br/>
/**<br/>
* Given the current elapsed time in the current change \
window, and the<br/> * amount of time elapsed since the last \
change was detected,<br/> determines<br/>
* if and by how much the current change window should be \
extended.<br/> *<br/>
* @param elapsed nanoseconds elapsed since the start of \
the<br/> current change window<br/>
* @param elapsedSinceLastChange nanoseconds elapsed since \
the last<br/> change<br/>
* @return nanoseconds to extend the window with<br/>
*/<br/>
long determineInterval(long elapsed, long \
elapsedSinceLastChange);<br/> <br/>
For testing purposes, the schedule and nanoTime can be provided such<br/>
that the throttle function can be tested deterministically. For<br/>
integrating with JavaFX, update is implemented as<br/>
`Platform.runLater(command)`. The schedule and nanoTime methods<br/>
delegate to an Executor and System.nanoTime respectively. When using<br/>
properties without JavaFX, Throttler implementations can be provided<br/>
which run property updates on a scheduler thread (just calling<br/>
Runnable::run on the current thread) or via some user provided executor.<br/>
<br/>
A sample test case looks like this (read with a mono space font :-)):<br/>
<br/>
@Test<br/>
void testThrottleLeadingAndTrailing() {<br/>
// create Throttler with deterministic behavior:<br/>
Throttler throttler =<br/>
create(Throttler.IntervalHandler.throttle(Duration.ofNanos(4));<br/>
<br/>
// create throttled observable:<br/>
ObservableValue<String> binding = \
source.throttle(throttler);<br/> <br/>
assertChanges(<br/>
binding,<br/>
\
"--a-b--c---d-----e-------f-g-----------f-g-----",<br/> \
\
"--a---b---c---d---e------f---g---------f---g---"<br/> \
);<br/> <br/>
assertInvalidations(<br/>
binding,<br/>
\
"--a-b--c---d-----e-------f-g-----------f-g-----",<br/> \
\
"--i---i---i---i---i------i---i---------i---i---"<br/> \
);<br/> }<br/>
<br/>
Thanks for reading, I look forward to your feedback!<br/>
<br/>
--John<br/>
<br/>
<br/>
<br/>
</div>
</div>
</div>
</div></div></body></html>
[prev in list] [next in list] [prev in thread] [next in thread]
Configure |
About |
News |
Add a list |
Sponsored by KoreLogic