ReactiveX / RxSwift

Reactive Programming in Swift
MIT License
24.39k stars 4.17k forks source link

`TestScheduler` resolution yields inaccurate results when throttled #2377

Closed funct7 closed 6 months ago

funct7 commented 3 years ago

:warning: If you don't have something to report in the following format, it will probably be easier and faster to ask in the slack channel first. :warning:

:warning: Please take you time to fill in the fields below. If we aren't provided with this basic information about your issue we probably won't be able to help you and there won't be much we can do except to close the issue :( :warning:

If you still want to report issue, please delete above statements before submitting an issue.

Short description of the issue:

When using TestScheduler with custom resolution, throttle operation doesn't seem to work precisely.

Expected outcome:

If TestScheduler(initialClock: 0, resolution: 0.001), and the throttle interval is .seconds(5), each emission to be spaced out by 5000 ticks.

What actually happens:

The following is the interval measured: (resolution, clock)

1      5
0.1    46
0.01   451
0.001  4501

Self contained code example that reproduces the issue:


class TestCase : XCTestCase {

    func test() {
//        let scheduler = TestScheduler(initialClock: 0)
//        let scheduler = TestScheduler(initialClock: 0, resolution: 0.1)
//        let scheduler = TestScheduler(initialClock: 0, resolution: 0.01)
        let scheduler = TestScheduler(initialClock: 0, resolution: 0.001)
        let scope = DisposeBag()

        let store = MockStore()

        let sut = SomeViewModel(
            scheduler: scheduler,
            store: store)

        let events: TestableObservable<Void> = {
//            let taps = (0...20).map { Recorded.next($0, ()) } // >>> [next(0) @ 1, next(0) @ 6, next(0) @ 11, next(0) @ 16]
//            let taps = (0...200).map { Recorded.next($0, ()) } // >>> [next(0) @ 1, next(0) @ 47, next(0) @ 93, next(0) @ 139, next(0) @ 185]
//            let taps = (0...2000).map { Recorded.next($0, ()) } // >>> [next(0) @ 1, next(0) @ 452, next(0) @ 903, next(0) @ 1354, next(0) @ 1805]
            let taps = (0...20_000).map { Recorded.next($0, ()) } // >>> [next(0) @ 1, next(0) @ 4502, next(0) @ 9003, next(0) @ 13504, next(0) @ 18005]
            return scheduler.createHotObservable(taps)
        }()

        let result: TestableObserver<Int> = scheduler.start(created: 0, subscribed: 0, disposed: 30_000) {
            events.subscribe(onNext: { sut.didTapRefresh() }).disposed(by: scope)
            return store.autoRefreshCalls
        }

        print(">>>", result.events)
    }

}

class MockStore {
    // using Int just to check for equality
    let autoRefreshCalls = PublishSubject<Int>()
    func autoRefresh() {
        autoRefreshCalls.onNext(0)
    }
}

class SomeViewModel {
    private let refreshSignal = PublishSubject<Void>()
    func didTapRefresh() {
        refreshSignal.onNext(())
    }
    private let scope = DisposeBag()

    init(scheduler: SchedulerType,
         store: MockStore)
    {
        refreshSignal
            .throttle(.seconds(5), latest: false, scheduler: scheduler)
            .subscribe(onNext: { store.autoRefresh() })
            .disposed(by: scope)
    }
}

RxSwift/RxCocoa/RxBlocking/RxTest version/commit

RxSwift (5.1.2) RxTest (5.1.2)

Platform/Environment

How easy is to reproduce? (chances of successful reproduce after running the self contained code)

Xcode version:

Xcode 12.5

:warning: Fields below are optional for general issues or in case those questions aren't related to your issue, but filling them out will increase the chances of getting your issue resolved. :warning:

Installation method:

I have multiple versions of Xcode installed: (so we can know if this is a potential cause of your issue)

Level of RxSwift knowledge: (this is so we can understand your level of knowledge and formulate the response in an appropriate manner)

freak4pc commented 2 years ago

Is this still an issue? Not sure I entirely understand the use case for that level of resolution. Can you make a small project with the issue?

Thanks

funct7 commented 2 years ago

The use case is shown in the sample code. The app wants to throttle how often the user can refresh the contents. Throttling behavior would most likely happen at the second resolution level, but if you have debouncing behavior in some other part of the code--which is likely to be a few hundred ms--, or any logic that requires millisecond resolution, you will need the scheduler with a higher resolution, and the test will fail.