acrylic-origami / HHReactor

Producer-based Reactor for Hack
MIT License
2 stars 0 forks source link

Implement debounce #1

Closed acrylic-origami closed 7 years ago

acrylic-origami commented 7 years ago

Bouncing around some problems and solutions

Almost everything about debounce resists implementation in Hack. Fundamentally, it differs from all of the other operators because it's the absence of items that drives it to emit. The implementation that comes to mind the most naturally: race the SleepWaitHandle for the debouncing period with the next item to be emitted, and if the SleepWaitHandle resolves first, emit the item that's paired with it. The last item being delayed by that debouncing period is not helping at all, because for many other operators, a ConditionWaitHandle wrapping Producer::collapse() (or an equivalent) lasted long enough for any resolutions to hit it before the Producer ended. In this case, however, the last item can't enjoy this guarantee.

Wrapping the SleepWaitHandle in a ConditionWaitHandle and catching the "ConditionWaitHandle not notified by child" exception to emit the next item feels unclean, but it could work.

The other half of the problem is that somehow this SleepWaitHandle-Producer::next() race needs to push the values through a yield. The most likely candidate right now is an async block that closes over the race and a Wrapper to the current value, awaits that race and yields the current value conditionally on the exception from ConditionWaitHandle.

An edge case is lurking: if all exceptions are routed through the ConditionWaitHandle hosting the race (very likely — such that the consuming scope can receive these exceptions), and if a ConditionWaitHandle isn't notified by a child further up the stack, all those specific, "true" exceptions will be lost because awaiting scope (the async block) will assume it's just the signal to emit the last value. I'm unsure if the status of the SleepWaitHandle helps in this situation, because a race condition might permit the SleepWaitHandle to be finished while a true exception is thrown.

The best-case scenario is that a thin wrapper around the SleepWaitHandle could notify the race ConditionWaitHandle that wraps it. Apparently, the only way to do this soundly is with AsyncPoll, although I'm convinced that so long a SleepWaitHandle isn't eagerly executed, we can make a self-referential pointer in a ConditionWaitHandle to resolve itself like this:

$self_wrapper = new Wrapper(null);
$self_wrapper->set(ConditionWaitHandle::create(async {
  await \HH\Asio\usleep($usecs);
  $self = $self_wrapper->get();
  invariant(!is_null($self), 'I hope this is guaranteed');
  if(!\HH\Asio\has_finished($self))
    $self->succeed(null);
});
acrylic-origami commented 7 years ago

Resolve with 54a41af. The behavior breakdown at low debouncing delays is probably a bigger phenomenon than the operator itself, and will probably improve only from improving the efficiency of Producer as a whole.