Skip to content

Commit

Permalink
Add FPS utilities, resolves #658
Browse files Browse the repository at this point in the history
  • Loading branch information
NullVoxPopuli committed Mar 7, 2023
1 parent 832adb0 commit 8989bbb
Show file tree
Hide file tree
Showing 7 changed files with 220 additions and 9 deletions.
47 changes: 47 additions & 0 deletions .changeset/young-walls-own.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
---
"ember-resources": minor
---

New Utils: UpdateFrequency and FrameRate

<details><summary>FrameRate</summary>

Utility that uses requestAnimationFrame to report
how many frames per second the current monitor is
rendering at.

The result is rounded to two decimal places.

```js
import { FramRate } from 'ember-resources/util/fps';

<template>
{{FrameRate}}
</template>
```

</details>


<details><summary>FrameRate</summary>


Utility that will report the frequency of updates to tracked data.

```js
import { UpdateFrequency } from 'ember-resources/util/fps';
export default class Demo extends Component {
@tracked someProp;
@use updateFrequency = UpdateFrequency(() => this.someProp);
<template>
{{this.updateFrequency}}
</template>
}
```

NOTE: the function passed to UpdateFrequency may not set tracked data.

</details>
4 changes: 4 additions & 0 deletions ember-resources/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"./util": "./dist/util/index.js",
"./util/cell": "./dist/util/cell.js",
"./util/keep-latest": "./dist/util/keep-latest.js",
"./util/fps": "./dist/util/fps.js",
"./util/map": "./dist/util/map.js",
"./util/helper": "./dist/util/helper.js",
"./util/remote-data": "./dist/util/remote-data.js",
Expand Down Expand Up @@ -43,6 +44,9 @@
"util/function": [
"dist/util/function.d.ts"
],
"util/fps": [
"dist/util/fps.d.ts"
],
"util/map": [
"dist/util/map.d.ts"
],
Expand Down
14 changes: 7 additions & 7 deletions ember-resources/src/core/function-based/immediate-invocation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ class ResourceInvokerManager {
* })
* ```
*/
export function resourceFactory<Value = unknown, Args extends unknown[] = unknown[]>(
export function resourceFactory<Value = unknown, Args extends any[] = any[]>(
wrapperFn: (...args: Args) => ReturnType<typeof resource<Value>>
/**
* This is a bonkers return type.
Expand All @@ -161,21 +161,21 @@ export function resourceFactory<Value = unknown, Args extends unknown[] = unknow
}

type ResourceBlueprint<Value, Args> =
/**
* type for JS invocation with @use
* @use a = A.from(() => [b, c, d])
*/
| ((thunk: () => SpreadFor<Args>) => ReturnType<typeof resource<Value>>)
/**
* Type for template invocation
* {{#let (A b c d) as |a|}}
* {{a}}
* {{/let}}
*
* This could also be used in JS w/ invocation with @use
* @use a = A(() => b)
*
* NOTE: it is up to the function passed to resourceFactory to handle some of the parameter ambiguity
*/
| ((...args: SpreadFor<Args>) => ReturnType<typeof resource<Value>>)
/**
* Not passing args is allowed, too
* @use a = A.from()
* @use a = A()
*
* {{A}}
*/
Expand Down
88 changes: 88 additions & 0 deletions ember-resources/src/util/fps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { cell, resource, resourceFactory } from '../index';

/**
* Utility that uses requestAnimationFrame to report
* how many frames per second the current monitor is
* rendering at.
*
* The result is rounded to two decimal places.
*
* ```js
* import { FramRate } from 'ember-resources/util/fps';
*
* <template>
* {{FrameRate}}
* </template>
* ```
*/
export const FrameRate = resource(({ on }) => {
let value = cell(0);
let startTime = new Date().getTime();
let frame: number;

let update = () => {
// simulate receiving data as fast as possible
frame = requestAnimationFrame(() => {
value.current++;
update();
});
};

on.cleanup(() => cancelAnimationFrame(frame));

// Start the infinite requestAnimationFrame chain
update();

return () => {
let elapsed = (new Date().getTime() - startTime) * 0.001;
let fps = value.current * Math.pow(elapsed, -1);
let rounded = Math.round(fps * 100) * 0.01;
// account for https://stackoverflow.com/a/588014/356849
let formatted = `${rounded}`.substring(0, 5);

return formatted;
};
});

/**
* Utility that will report the frequency of updates to tracked data.
*
* ```js
* import { UpdateFrequency } from 'ember-resources/util/fps';
*
* export default class Demo extends Component {
* @tracked someProp;
*
* @use updateFrequency = UpdateFrequency(() => this.someProp);
*
* <template>
* {{this.updateFrequency}}
* </template>
* }
* ```
*
* NOTE: the function passed to UpdateFrequency may not set tracked data.
*/
export const UpdateFrequency = resourceFactory((ofWhat: () => unknown, updateInterval = 500) => {
updateInterval ||= 500;

let multiplier = 1000 / updateInterval;
let framesSinceUpdate = 0;

return resource(({ on }) => {
let value = cell(0);
let interval = setInterval(() => {
value.current = framesSinceUpdate * multiplier;
framesSinceUpdate = 0;
}, updateInterval);

on.cleanup(() => clearInterval(interval));

return () => {
ofWhat();
framesSinceUpdate++;

return value.current;
};
});
});
2 changes: 1 addition & 1 deletion test-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@
},
"packageManager": "pnpm@7.1.2",
"volta": {
"extends": "../../package.json"
"extends": "../package.json"
},
"msw": {
"workerDirectory": "public"
Expand Down
63 changes: 63 additions & 0 deletions test-app/tests/utils/fps/rendering-test.gts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
// @ts-ignore
import { on } from '@ember/modifier';
import { click, find, render } from '@ember/test-helpers';
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';

import { use } from 'ember-resources';
import { FrameRate, UpdateFrequency } from 'ember-resources/util/fps';

module('Utils | FPS | rendering', function (hooks) {
setupRenderingTest(hooks);

module('FrameRate', function() {
test('it works', async function (assert) {
await render(<template>
<out>{{FrameRate}}</out>
</template>);


let text = find('out')?.innerHTML?.trim() || ''

assert.notStrictEqual(text, '', 'Content is rendered');
});


});

module('UpdateFrequency', function() {
test('it works', async function (assert) {
class Demo extends Component {
@tracked someProp = 0;

@use updateFrequency = UpdateFrequency(() => this.someProp);

inc = () => this.someProp++;

<template>
<button type="button" {{on "click" this.inc}}>Inc</button>
<out>{{this.updateFrequency}}</out>
</template>
}

await render(
<template>
<Demo />
</template>
);

assert.dom('out').hasText('0', 'Initial value is 0');

for (let i = 0; i < 100; i++) {
await click('button');
}

let text = find('out')?.innerHTML?.trim() || ''

assert.notStrictEqual(text, '', 'Content is rendered');
});
});

});
11 changes: 10 additions & 1 deletion test-app/tests/utils/function-resource/clock-test.gts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,17 @@ module('Examples | resource | Clock', function (hooks) {
assert.timeout(3000);
});

interface ClockArgs {
start?: Date;
locale?: string;
}

// Wrapper functions are the only way to pass Args to a resource.
const Clock = resourceFactory(({ start, locale = 'en-US' }: { start?: Date, locale?: string }) => {
const Clock = resourceFactory((options: ClockArgs | (() => ClockArgs)) => {
let opts = (typeof options === 'function') ? options() : options;
let start = opts.start;
let locale = opts.locale ?? 'en-US';

// For a persistent state across arg changes, `Resource` may be better`
let time = cell(start);
let formatter = new Intl.DateTimeFormat(locale, {
Expand Down

0 comments on commit 8989bbb

Please sign in to comment.