rxjs icon indicating copy to clipboard operation
rxjs copied to clipboard

animationFrameScheduler stops working unexpectedly

Open EoinGriffin-AI opened this issue 1 year ago • 12 comments

Describe the bug

These operators in RxJS (7.5.4) just... stop working sometimes. What's going on? How can I fix this?

Update: I also tried this with auditTime instead and the same issue occurs, so I believe it's more related to animationFrameScheduler and not debounceTime

import { animationFrameScheduler, Observable, OperatorFunction } from 'rxjs';
import { debounceTime, tap } from 'rxjs/operators';


export function debounceAnimated<T>(): OperatorFunction<T, T> {
  return (source$: Observable<T>) => source$.pipe(
    tap(() => console.log('A')),
    debounceTime(0, animationFrameScheduler),
    tap(() => console.log('B')),
  );
}

This operator works just fine most of the time. I see in my app that the action using this operator works correctly, and in the console I see a bunch of A's followed by a bunch of B's, as expected when the debounceTime fires.

The problem is that sometimes this operator just... breaks somehow. I get a whole bunch of A's when I perform the user action (in this case using the mouse scroll to zoom in and out of a chart), but the B's stop completely and downstream observable subscriptions stop working too. The application doesn't recover when this happens. Everything else seems to be fine, CPU usage is low, and other components that don't use this operator continue to work normally.



For a bit more context, this feature has been working fine for a few years, but a recent RxJS upgrade (we upgraded from 6.6.2 to 7.5.4 to get a necessary bugfix) broke the previous implementation of this operator and so now I'm trying to find some alternative. The previous implementation used this code, but it no longer debounces anything when written like this: debounce(() => EMPTY.pipe(observeOn(scheduler)))

I also posted this same question on StackOverflow, in case anyone wants to follow there too. https://stackoverflow.com/questions/72916854/rxjs-debouncetime-operator-with-animationframescheduler-stops-working-unexpected

Expected behavior

debounceTime with animationFrameScheduler should continue to work indefinitely as long as there are active subscribers.

Reproduction code

In my case this code is run many times. Several Observables (maybe 10?) use this custom debounceAnimated operator and it gets his about 50 times and so logs 'A' 50 times on a single mouse scroll event. Obviously I don't want to re-render my UI 50 times, which is why I need this to work.


import { animationFrameScheduler, Observable, OperatorFunction } from 'rxjs';
import { debounceTime, tap } from 'rxjs/operators';


export function debounceAnimated<T>(): OperatorFunction<T, T> {
  return (source$: Observable<T>) => source$.pipe(
    tap(() => console.log('A')),
    debounceTime(0, animationFrameScheduler),
    tap(() => console.log('B')),
  );
}

Reproduction URL

No response

Version

7.5.4

Environment

Chrome Version 103.0.5060.66 (Official Build) (64-bit) Windows 10 Angular 13.2.6 Typescript 4.5.5

Additional context

No response

EoinGriffin-AI avatar Jul 08 '22 20:07 EoinGriffin-AI

A bit more info: When I set a breakpoint in the animationFrameScheduler flush() function it stops getting called when this bug occurs.

EoinGriffin-AI avatar Jul 08 '22 22:07 EoinGriffin-AI

Alright, some more information, and a workaround. I added this setInterval to manually flush the scheduler, and it works! If the bug occurs, then within 5 seconds this will completely flush the scheduler and things will start working correctly again. From scrolling in and out a bunch of times, it looks like a bunch of actions pile up in the animationFrameScheduler but nothing is handling them so they just lay dormant.

But why does this work? Why is this necessary? Sorry but this is where my understanding of the inner working of RxJS is limited. Can someone please advise? This is a kind of ugly workaround. I'm going to shorten the delay to 100ms or something, but it's still not great.

import { animationFrameScheduler, Observable, OperatorFunction } from 'rxjs';
import { debounceTime } from 'rxjs/operators';


setInterval(() => {
  while (animationFrameScheduler.actions.length > 0) {
    animationFrameScheduler.flush();
  }
}, 5000);

export function debounceAnimated<T>(): OperatorFunction<T, T> {
  return (source$: Observable<T>) => source$.pipe(
    debounceTime(0, animationFrameScheduler)
  );
}

EoinGriffin-AI avatar Jul 09 '22 00:07 EoinGriffin-AI

I think this bug has the same root cause as https://github.com/ReactiveX/rxjs/issues/7017 .

As I mentionned there, the bug seem to have been caused by this code change

fdeslandes-wk avatar Jul 28 '22 15:07 fdeslandes-wk

Another similar problem that may be related:

we're using the animationFrameScheduler as a heartbeat. this used to work just fine:

hartBeat$ = scheduled(of(null), animationFrameScheduler).pipe(
    repeat(),
    share(),
);

with rxjs at version 7.5.6, this only gives a single beat now.

curiously, the fix for this is using the deprecated style of of using the scheduler argument:

hartBeat$ = of(null, animationFrameScheduler).pipe(
    repeat(),
    share(),
);

c-goldschmidt avatar Aug 05 '22 07:08 c-goldschmidt

@c-goldschmidt interesting that's faster. of(null, scheduler) is the same as schedule([null], scheduler).

Honestly, though, I'd recommend using the animationFrames() API instead for your use case.

benlesh avatar Sep 07 '22 20:09 benlesh

@EoinGriffin-AI FYI, I'd recommend trying debounce with animationFrames.

import { animationFrames, debounce } from 'rxjs';

source$.pipe(debounce(animationFrames()))

Otherwise, this looks like a bug related to #7017

benlesh avatar Sep 07 '22 20:09 benlesh

@benlesh Looks like that worked! Thanks!

It's noticeably slower than the setInterval workaround I implemented though. Maybe that's due to the bug you mentioned. I'll play around with this a bit and see if I can get some better performance by restructuring my subscriptions.

EoinGriffin-AI avatar Sep 07 '22 21:09 EoinGriffin-AI

Yeah, the schedulers are due for a major overhaul in version 8. Long overdue, actually.

The fact that your solution is "faster" is a little disturbing though. I'll need to do more digging.

benlesh avatar Sep 09 '22 00:09 benlesh

@EoinGriffin-AI thank you for getting me to look into this. We were creating way too many Subscription instances in that code. There's a PR up now to resolve that issue at #7060

benlesh avatar Sep 09 '22 00:09 benlesh

@benlesh thanks for all your work on this. I'm just wondering where this issue is at, I can see there are commits referencing this that were deployed in 7.5.7 but I'm still having similar problems as @EoinGriffin-AI and still stuck on 7.4.0. Is that what you would expect?

jscheid avatar Oct 10 '22 04:10 jscheid

@jscheid, please check this comment that is related to this issue as well.

jakovljevic-mladen avatar Oct 10 '22 17:10 jakovljevic-mladen

checking

const s1 = new Subject();

const s2 = s1.pipe(map(x => (x + 1)));

combineLatest([s1, s2]).pipe(debounce((i) => animationFrames())).subscribe((x) => console.log('1: ', x))

s1.next(2) // initial;

s1.next(4)

but it doesnt consolelog any values in rxjs 7.8.1 Playground: https://playcode.io/1648054

kievsash avatar Oct 31 '23 15:10 kievsash