-
Notifications
You must be signed in to change notification settings - Fork 31
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
fix: Heap usage request throttling #1450
Merged
Merged
Changes from 7 commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
6c41147
fix: Throttle HeapUsage fetch
bmingles 509cdbe
Fixed runtime error in TestUtils
bmingles ae3a053
Updated comment
bmingles 96e4cb7
Fixed dependency path
bmingles 0e45379
Fixed package-lock
bmingles 56c2433
Fixed package-lock
bmingles 1d22858
Cancel setTimeout on unmount
bmingles af1fba9
Split out useIsMountedRef hook
bmingles File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
import { renderHook, act } from '@testing-library/react-hooks'; | ||
import { TestUtils } from '@deephaven/utils'; | ||
import useAsyncInterval from './useAsyncInterval'; | ||
|
||
beforeEach(() => { | ||
jest.clearAllMocks(); | ||
expect.hasAssertions(); | ||
jest.useFakeTimers(); | ||
jest.spyOn(window, 'setTimeout').mockName('setTimeout'); | ||
}); | ||
|
||
afterAll(() => { | ||
jest.useRealTimers(); | ||
}); | ||
|
||
describe('useAsyncInterval', () => { | ||
function createCallback(ms: number) { | ||
return jest.fn( | ||
async (): Promise<void> => | ||
new Promise(resolve => { | ||
setTimeout(resolve, ms); | ||
}) | ||
); | ||
} | ||
|
||
const targetIntervalMs = 1000; | ||
|
||
it('should call the callback function after the target interval', async () => { | ||
const callback = createCallback(50); | ||
|
||
renderHook(() => useAsyncInterval(callback, targetIntervalMs)); | ||
|
||
// First tick should be scheduled for target interval | ||
expect(callback).not.toHaveBeenCalled(); | ||
expect(window.setTimeout).toHaveBeenCalledWith( | ||
expect.any(Function), | ||
targetIntervalMs | ||
); | ||
|
||
// Callback should be called after target interval | ||
act(() => jest.advanceTimersByTime(targetIntervalMs)); | ||
expect(callback).toHaveBeenCalledTimes(1); | ||
}); | ||
|
||
it('should adjust the target interval based on how long async call takes', async () => { | ||
const callbackDelayMs = 50; | ||
const callback = createCallback(callbackDelayMs); | ||
|
||
renderHook(() => useAsyncInterval(callback, targetIntervalMs)); | ||
|
||
// Callback should be called after target interval | ||
expect(callback).not.toHaveBeenCalled(); | ||
act(() => jest.advanceTimersByTime(targetIntervalMs)); | ||
expect(callback).toHaveBeenCalledTimes(1); | ||
|
||
jest.clearAllMocks(); | ||
|
||
// Mimick the callback Promise resolving | ||
act(() => jest.advanceTimersByTime(callbackDelayMs)); | ||
await TestUtils.flushPromises(); | ||
|
||
// Next target interval should be adjusted based on how long the callback took | ||
const nextTargetIntervalMs = targetIntervalMs - callbackDelayMs; | ||
|
||
expect(callback).not.toHaveBeenCalled(); | ||
expect(window.setTimeout).toHaveBeenCalledTimes(1); | ||
expect(window.setTimeout).toHaveBeenCalledWith( | ||
expect.any(Function), | ||
nextTargetIntervalMs | ||
); | ||
|
||
act(() => jest.advanceTimersByTime(nextTargetIntervalMs)); | ||
expect(callback).toHaveBeenCalledTimes(1); | ||
}); | ||
|
||
it('should schedule the next callback immediately if the callback takes longer than the target interval', async () => { | ||
const callbackDelayMs = targetIntervalMs + 50; | ||
const callback = createCallback(callbackDelayMs); | ||
|
||
renderHook(() => useAsyncInterval(callback, targetIntervalMs)); | ||
|
||
// Callback should be called after target interval | ||
expect(callback).not.toHaveBeenCalled(); | ||
act(() => jest.advanceTimersByTime(targetIntervalMs)); | ||
expect(callback).toHaveBeenCalledTimes(1); | ||
|
||
jest.clearAllMocks(); | ||
|
||
// Mimick the callback Promise resolving | ||
act(() => jest.advanceTimersByTime(callbackDelayMs)); | ||
await TestUtils.flushPromises(); | ||
|
||
expect(callback).not.toHaveBeenCalled(); | ||
expect(window.setTimeout).toHaveBeenCalledTimes(1); | ||
expect(window.setTimeout).toHaveBeenCalledWith(expect.any(Function), 0); | ||
|
||
act(() => jest.advanceTimersByTime(0)); | ||
expect(callback).toHaveBeenCalledTimes(1); | ||
}); | ||
|
||
it('should stop calling the callback function after unmounting', async () => { | ||
const callback = createCallback(50); | ||
|
||
const { unmount } = renderHook(() => | ||
useAsyncInterval(callback, targetIntervalMs) | ||
); | ||
|
||
unmount(); | ||
|
||
act(() => jest.advanceTimersByTime(targetIntervalMs)); | ||
|
||
expect(callback).not.toHaveBeenCalled(); | ||
}); | ||
|
||
it('should not re-schedule callback if callback resolves after unmounting', async () => { | ||
const callbackDelayMs = 50; | ||
const callback = createCallback(callbackDelayMs); | ||
|
||
const { unmount } = renderHook(() => | ||
useAsyncInterval(callback, targetIntervalMs) | ||
); | ||
|
||
act(() => jest.advanceTimersByTime(targetIntervalMs)); | ||
expect(callback).toHaveBeenCalledTimes(1); | ||
jest.clearAllMocks(); | ||
|
||
unmount(); | ||
|
||
// Mimick the callback Promise resolving | ||
act(() => jest.advanceTimersByTime(callbackDelayMs)); | ||
await TestUtils.flushPromises(); | ||
|
||
expect(window.setTimeout).not.toHaveBeenCalled(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
import { useCallback, useEffect, useRef } from 'react'; | ||
import Log from '@deephaven/log'; | ||
|
||
const log = Log.module('useAsyncInterval'); | ||
|
||
/** | ||
* Calls the given async callback at a target interval. | ||
* | ||
* If the callback takes less time than the target interval, the target interval | ||
* for the next tick will be adjusted to target the remaining time in the current | ||
* interval. | ||
* | ||
* e.g. If the target interval is 1000ms, and the callback takes 50ms to resolve, | ||
* the next tick will be scheduled for 950ms from now via `setTimeout(callback, 950)`. | ||
* | ||
* If the callback takes longer than the target interval, the next tick will be | ||
* scheduled immediately via `setTimeout(callback, 0)`. In such cases, the time | ||
* between ticks may be > than the target interval, but this guarantees that | ||
* a callback won't be scheduled until after the previous one has resolved. | ||
* @param callback Callback to call at the target interval | ||
* @param targetIntervalMs Target interval in milliseconds to call the callback | ||
*/ | ||
export function useAsyncInterval( | ||
callback: () => Promise<void>, | ||
targetIntervalMs: number | ||
) { | ||
const isCancelledRef = useRef(false); | ||
const trackingRef = useRef({ count: 0, started: Date.now() }); | ||
const setTimeoutRef = useRef(0); | ||
|
||
const tick = useCallback(async () => { | ||
const now = Date.now(); | ||
let elapsedSinceLastTick = now - trackingRef.current.started; | ||
|
||
trackingRef.current.count += 1; | ||
trackingRef.current.started = now; | ||
|
||
log.debug( | ||
`tick #${trackingRef.current.count}.`, | ||
elapsedSinceLastTick, | ||
'ms elapsed since last tick.' | ||
); | ||
|
||
await callback(); | ||
|
||
if (isCancelledRef.current) { | ||
return; | ||
} | ||
|
||
elapsedSinceLastTick += Date.now() - trackingRef.current.started; | ||
|
||
// If elapsed time is > than the target interval, adjust the next tick interval | ||
const nextTickInterval = Math.max( | ||
0, | ||
Math.min( | ||
targetIntervalMs, | ||
targetIntervalMs - (elapsedSinceLastTick - targetIntervalMs) | ||
) | ||
); | ||
|
||
log.debug('adjusted minIntervalMs:', nextTickInterval); | ||
|
||
setTimeoutRef.current = window.setTimeout(tick, nextTickInterval); | ||
}, [callback, targetIntervalMs]); | ||
|
||
useEffect(() => { | ||
log.debug('Setting interval minIntervalMs:', targetIntervalMs); | ||
|
||
setTimeoutRef.current = window.setTimeout(tick, targetIntervalMs); | ||
|
||
return () => { | ||
isCancelledRef.current = true; | ||
bmingles marked this conversation as resolved.
Show resolved
Hide resolved
|
||
window.clearTimeout(setTimeoutRef.current); | ||
}; | ||
}, [targetIntervalMs, tick]); | ||
} | ||
|
||
export default useAsyncInterval; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the component unmounts during this callback, I think the timer will continue on - it returns from this callback, and there's nothing to stop it from calling
window.setTimeout
at the end.Should set an
isCancelled
ref when unmounted, or you could overload thesetTimeoutRef
instead.