-
-
Notifications
You must be signed in to change notification settings - Fork 205
feat(): add rxScheduleTask function #1633
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
base: main
Are you sure you want to change the base?
Changes from all commits
d4687b4
db2d414
8ec9208
cdaa34d
18e9e4d
a7570a3
3f01a05
83dc4d5
31e8c6c
7628ecb
bd65ad3
3607b37
6603ea7
1ab7c31
e2ee520
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,114 @@ | ||||||
# rxScheduleTask | ||||||
|
||||||
`rxScheduleTask` provides a helper function to schedule function execution. It is a minimal building block for making performance optimizations in your code. | ||||||
|
||||||
## Motivation | ||||||
|
||||||
Chromium based browsers consider all tasks that take more than 50ms as long tasks. If task runs for more than 50ms, users will start noticing lags. Optimally all user interactions should happen at 30 fps frame-rate with 32ms budget per browser task. In an ideal world, it should be 60 fps and 16ms budget. | ||||||
|
||||||
> 💡 To achieve 30 fps or 60 fps in web apps, you can't just focus on JavaScript execution time. Remember to account for the browser's other tasks, like style recalculations, layout, and painting. | ||||||
|
||||||
## Scheduling mechanisms in browser | ||||||
|
||||||
Most common ways of delaying a task execution are: | ||||||
|
||||||
- `setTimeout` | ||||||
- `requestAnimationFrame` | ||||||
- `requestIdleCallback` | ||||||
- `Promise.resolve` | ||||||
- `queueMicrotask` | ||||||
|
||||||
`rxScheduleTask` provides a similar API but comes with huge benefits of notion of frame budget and priority configuration. | ||||||
|
||||||
## Concurrent strategies | ||||||
|
||||||
> 💡 Under the hood all our concurrent strategies are based on MessageChannel technology. | ||||||
|
||||||
To address the problem of long tasks and help browser split the work @rx-angular/cdk provides concurrent strategies. These strategies will help browser to chunk the work into non-blocking tasks whenever it's possible. | ||||||
|
||||||
You can read detailed information about concurrent strategies [here](strategies/concurrent-strategies.md). | ||||||
|
||||||
## Usage examples | ||||||
|
||||||
Karnaukhov-kh marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
### Default usage | ||||||
|
||||||
```typescript | ||||||
import { rxScheduleTask } from '@rx-angular/cdk/render-strategies'; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
I don't like it that we export it from render-strategies. Because it doesn't do anything with rendering. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you think about better place? We had this discussion in old PR and decided that this is the best option we currently have. We can say the same about RxStrategyProvider, aren't we? This is a service for scheduling any type of work, but it's still in the render-strategies. Same reasoning goes to rxScheduleTask. You can make it "related" to rendering by doing rxScheduleTask(() => this.cdRef.detectChanges()) ¯_(ツ)_/¯. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we can introduce a new sub-package for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd go for it! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hmmm There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would do a rethink before and maybe see what other code we want to move There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Whatever we decide, please let's not have it frozen for another 1.5 years :D There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. +1 for |
||||||
... | ||||||
|
||||||
up 8000 dateStateAndBackup<T>(data: T) { | ||||||
this.stateService.set(data); | ||||||
|
||||||
rxScheduleTask(() => localStorage.setItem('state', JSON.stringify(state))); | ||||||
} | ||||||
``` | ||||||
|
||||||
### Input params | ||||||
|
||||||
- Just as common delaying apis this method `accepts` a work function that should be scheduled. | ||||||
- It also accepts configuration object as an optional second parameter | ||||||
- `strategy` which will be used for scheduling (`normal` is default, for full list of available strategies see [concurrent strategies documentation](strategies/concurrent-strategies.md)) | ||||||
- `delay` which is responsible for delaying the task execution (default is 0ms) | ||||||
- `ngZone` if you want your function be executed within ngzone (default scheduling runs out of zone) | ||||||
|
||||||
### Return type | ||||||
|
||||||
Function returns a callback that you can use to cancel already scheduled tasks. | ||||||
|
||||||
### Usage with non-default strategy | ||||||
|
||||||
```typescript | ||||||
import { rxScheduleTask } from '@rx-angular/cdk/render-strategies'; | ||||||
... | ||||||
|
||||||
function updateStateAndBackup<T>(data: T) { | ||||||
this.stateService.set(data); | ||||||
|
||||||
rxScheduleTask( | ||||||
() => localStorage.setItem('state', JSON.stringify(state)), | ||||||
{strategy: 'idle'} | ||||||
); | ||||||
} | ||||||
``` | ||||||
|
||||||
### Usage with options | ||||||
|
||||||
```typescript | ||||||
import { rxScheduleTask } from '@rx-angular/cdk/render-strategies'; | ||||||
... | ||||||
|
||||||
function updateStateAndBackup<T>(data: T) { | ||||||
this.stateService.set(data); | ||||||
|
||||||
rxScheduleTask( | ||||||
() => localStorage.setItem('state', JSON.stringify(state)), | ||||||
{ delay: 200, zone: this.ngZone, strategy: 'idle' } | ||||||
); | ||||||
} | ||||||
``` | ||||||
|
||||||
### Cancel scheduled task | ||||||
|
||||||
```typescript | ||||||
import { rxScheduleTask } from '@rx-angular/cdk/render-strategies'; | ||||||
... | ||||||
|
||||||
let saveToLocalStorageCallback; | ||||||
|
||||||
function updateStateAndBackup<T>(data: T) { | ||||||
this.stateService.set(data); | ||||||
|
||||||
if (saveToLocalStorageCallback) { | ||||||
saveToLocalStorageCallback(); | ||||||
} | ||||||
|
||||||
saveToLocalStorageCallback = rxScheduleTask(() => | ||||||
localStorage.setItem('state', JSON.stringify(state)) | ||||||
); | ||||||
} | ||||||
``` | ||||||
|
||||||
## Links | ||||||
|
||||||
- [Detailed information about strategies](https://github.com/rx-angular/rx-angular/tree/master/libs/cdk/render-strategies) | ||||||
- [MessageChannel documentation](https://developer.mozilla.org/en-US/docs/Web/API/MessageChannel) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
import { NgZone } from '@angular/core'; | ||
import * as scheduler from '@rx-angular/cdk/internals/scheduler'; | ||
import { rxScheduleTask } from '../src'; | ||
|
||
describe('rxScheduleTask', () => { | ||
let work: jest.Mock; | ||
let ngZone: NgZone; | ||
let scheduleSpy: jest.SpyInstance; | ||
let cancelSpy: jest.SpyInstance; | ||
|
||
beforeEach(() => { | ||
work = jest.fn(); | ||
// Mocking NgZone | ||
ngZone = { run: (fn: Function) => fn() } as any; | ||
|
||
// Spying on the scheduleCallback and cancelCallback functions | ||
scheduleSpy = jest.spyOn(scheduler, 'scheduleCallback'); | ||
cancelSpy = jest.spyOn(scheduler, 'cancelCallback'); | ||
}); | ||
|
||
afterEach(() => { | ||
jest.clearAllMocks(); | ||
}); | ||
|
||
it('should use normal strategy as default', () => { | ||
rxScheduleTask(work); | ||
expect(scheduleSpy).toHaveBeenCalledWith( | ||
expect.anything(), | ||
expect.any(Function), | ||
{ delay: undefined, ngZone: undefined } | ||
); | ||
}); | ||
|
||
it('should schedule work with the specified strategy', () => { | ||
rxScheduleTask(work, { strategy: 'low' }); | ||
expect(scheduleSpy).toHaveBeenCalledWith( | ||
expect.anything(), | ||
expect.any(Function), | ||
{ delay: undefined, ngZone: undefined } | ||
); | ||
}); | ||
|
||
it('should schedule work with the specified delay', () => { | ||
const delay = 200; | ||
rxScheduleTask(work, { delay }); | ||
expect(scheduleSpy).toHaveBeenCalledWith( | ||
expect.anything(), | ||
expect.any(Function), | ||
{ delay, ngZone: undefined } | ||
); | ||
}); | ||
|
||
it('should schedule work inside the specified NgZone', () => { | ||
rxScheduleTask(work, { ngZone }); | ||
expect(scheduleSpy).toHaveBeenCalledWith( | ||
expect.anything(), | ||
expect.any(Function), | ||
{ delay: undefined, ngZone } | ||
); | ||
}); | ||
|
||
it('should cancel the scheduled work', () => { | ||
const cancel = rxScheduleTask(work); | ||
cancel(); | ||
expect(cancelSpy).toHaveBeenCalled(); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,19 +1,28 @@ | ||
export { RxStrategyProvider } from './lib/strategy-provider.service'; | ||
export { ScheduleOnStrategyOptions } from './lib/model'; | ||
export { | ||
RX_CONCURRENT_STRATEGIES, | ||
RxConcurrentStrategies, | ||
} from './lib/concurrent-strategies'; | ||
export { RX_NATIVE_STRATEGIES, RxNativeStrategies } from './lib/native-strategies'; | ||
export { | ||
RX_RENDER_STRATEGIES_CONFIG, | ||
RxRenderStrategiesConfig, | ||
} from './lib/config'; | ||
export { | ||
RxConcurrentStrategyNames, | ||
RxCustomStrategyCredentials, | ||
RxDefaultStrategyNames, | ||
RxNativeStrategyNames, | ||
RxRenderBehavior, | ||
RxRenderWork, | ||
RxStrategies, | ||
RxStrategyCredentials, | ||
RxStrategyNames, | ||
ScheduleOnStrategyOptions, | ||
} from './lib/model'; | ||
export { | ||
RX_NATIVE_STRATEGIES, | ||
RxNativeStrategies, | ||
} from './lib/native-strategies'; | ||
export { onStrategy } from './lib/onStrategy'; | ||
export { rxScheduleTask } from './lib/rx-schedule-task'; | ||
export { strategyHandling } from './lib/strategy-handling'; | ||
export { RxStrategies } from './lib/model'; | ||
export { RxStrategyNames } from './lib/model'; | ||
export { RxDefaultStrategyNames } from './lib/model'; | ||
export { RxConcurrentStrategyNames } from './lib/model'; | ||
export { RxNativeStrategyNames } from './lib/model'; | ||
export { RxCustomStrategyCredentials } from './lib/model'; | ||
export { RxStrategyCredentials } from './lib/model'; | ||
export { RxRenderBehavior } from './lib/model'; | ||
export { RxRenderWork } from './lib/model'; | ||
export { RX_RENDER_STRATEGIES_CONFIG, RxRenderStrategiesConfig } from './lib/config'; | ||
export { RxStrategyProvider } from './lib/strategy-provider.service'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
import { NgZone } from '@angular/core'; | ||
import { | ||
PriorityLevel, | ||
cancelCallback, | ||
scheduleCallback, | ||
} from '@rx-angular/cdk/internals/scheduler'; | ||
import { RxConcurrentStrategyNames } from './model'; | ||
|
||
type StrategiesPriorityRecord = Record< | ||
RxConcurrentStrategyNames, | ||
PriorityLevel | ||
>; | ||
|
||
const strategiesPrio: StrategiesPriorityRecord = { | ||
immediate: PriorityLevel.ImmediatePriority, | ||
userBlocking: PriorityLevel.UserBlockingPriority, | ||
normal: PriorityLevel.NormalPriority, | ||
low: PriorityLevel.LowPriority, | ||
idle: PriorityLevel.IdlePriority, | ||
}; | ||
|
||
const defaultStrategy: keyof StrategiesPriorityRecord = 'normal'; | ||
|
||
/** | ||
* @description | ||
* This function is used to schedule a task with a certain priority. | ||
* It is useful for tasks that can be done asynchronously. | ||
* | ||
* ```ts | ||
* const task = rxScheduleTask(() => localStorage.setItem(state, JSON.stringify(state))); | ||
* ``` | ||
*/ | ||
export const rxScheduleTask = ( | ||
work: (...args: any[]) => void, | ||
{ | ||
strategy = defaultStrategy, | ||
delay, | ||
ngZone, | ||
}: { | ||
strategy?: keyof StrategiesPriorityRecord; | ||
delay?: number; | ||
ngZone?: NgZone; | ||
} = {} | ||
) => { | ||
const task = scheduleCallback(strategiesPrio[strategy], () => work(), { | ||
delay, | ||
ngZone, | ||
}); | ||
return () => { | ||
cancelCallback(task); | ||
}; | ||
}; |
Uh oh!
There was an error while loading. Please reload this page.