8000 feat(): add rxScheduleTask function by Karnaukhov-kh · Pull Request #1633 · rx-angular/rx-angular · GitHub
[go: up one dir, main page]

Skip to content

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

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
feat(): add rxScheduleTask function
  • Loading branch information
Karnaukhov-kh committed Oct 30, 2023
commit d4687b49c8b343ee1527c43ccef69e83338f90a2
112 changes: 112 additions & 0 deletions apps/docs/docs/cdk/render-strategies/rx-schedule-task.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# 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 considers all tasks that taking more than 50ms as long tasks. If task runs more than 50ms, users will start noticing lags. Optimally all user interactions should happen at 30 fps framerate with 32ms budget per browser task. In ideal world it should be 60 fps and 16ms budget.

> 💡 In reality browser has a reserved overhead of 4ms, try to stick to 28ms of work for 30 fps and 12ms for 60 fps.

## Scheduling mechanisms in browser

Most common ways of delaying task execution are:

- `setTimeout`
- `requestAnimationFrame`
- `requestIdleCallback`

`rxScheduleTask` provides 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. This 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](https://github.com/rx-angular/rx-angular/blob/main/libs/cdk/render-strategies/docs/concurrent-strategies.md).

## Usage examples

### 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](https://github.com/rx-angular/rx-angular/blob/main/libs/cdk/render-strategies/docs/concurrent-strategies.md))
- `delay` which is responsible for delaying the task execution (default is 0ms)
- `ngZone` if you want your function be executed withing ngzone (default scheduling runs out of zone)

### Return type

Function returns a callback that you can use to cancel already scheduled tasks.

### Default usage

```typescript
import { rxScheduleTask } from '@rx-angular/cdk/render-strategies';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
import { rxScheduleTask } from '@rx-angular/cdk/render-strategies';
import { rxScheduleTask } from '@rx-angular/cdk/render-strategies';

I don't like it that we export it from render-strategies. Because it doesn't do anything with rendering.

Copy link
Member Author

Choose a reason for hiding this comment

The 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()) ¯_(ツ)_/¯.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can introduce a new sub-package for scheduling?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd go for it!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmmm

Copy link
Member

Choose a reason for hiding this comment

The 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

Copy link
Member Author

Choose a reason for hiding this comment

The 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

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 for scheduling

...

function updateStateAndBackup<T>(data: T) {
this.stateService.set(data);

rxScheduleTask(() => localStorage.setItem('state', JSON.stringify(state)));
}
```

### 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)
67 changes: 67 additions & 0 deletions libs/cdk/render-strategies/spec/rx-schedule-task.spec.ts
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();
});
});
35 changes: 22 additions & 13 deletions libs/cdk/render-strategies/src/index.ts
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';
52 changes: 52 additions & 0 deletions libs/cdk/render-strategies/src/lib/rx-schedule-task.ts
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);
};
};
8 changes: 7 additions & 1 deletion libs/cdk/tsconfig.spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,11 @@
"types": ["jest", "node"]
},
"files": ["src/test-setup.ts"],
"include": ["**/*.spec.ts", "**/*.test.ts", "**/*.d.ts", "jest.config.ts"]
"include": [
"**/*.spec.ts",
"**/*.test.ts",
"**/*.d.ts",
"jest.config.ts",
"render-strategies/spec/rx-schedule-task.spec.ts"
]
}
0