From 8989bbb1afb41404f27c76a0b083f7bb46d7fc9e Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Mon, 6 Mar 2023 11:07:50 -0500 Subject: [PATCH] Add FPS utilities, resolves #658 --- .changeset/young-walls-own.md | 47 ++++++++++ ember-resources/package.json | 4 + .../function-based/immediate-invocation.ts | 14 +-- ember-resources/src/util/fps.ts | 88 +++++++++++++++++++ test-app/package.json | 2 +- test-app/tests/utils/fps/rendering-test.gts | 63 +++++++++++++ .../utils/function-resource/clock-test.gts | 11 ++- 7 files changed, 220 insertions(+), 9 deletions(-) create mode 100644 .changeset/young-walls-own.md create mode 100644 ember-resources/src/util/fps.ts create mode 100644 test-app/tests/utils/fps/rendering-test.gts diff --git a/.changeset/young-walls-own.md b/.changeset/young-walls-own.md new file mode 100644 index 000000000..efbf40b30 --- /dev/null +++ b/.changeset/young-walls-own.md @@ -0,0 +1,47 @@ +--- +"ember-resources": minor +--- + +New Utils: UpdateFrequency and FrameRate + +
FrameRate + + 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'; + + + ``` + +
+ + +
FrameRate + + + 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); + + + } + ``` + + NOTE: the function passed to UpdateFrequency may not set tracked data. + +
diff --git a/ember-resources/package.json b/ember-resources/package.json index 550277d96..a1dca230e 100644 --- a/ember-resources/package.json +++ b/ember-resources/package.json @@ -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", @@ -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" ], diff --git a/ember-resources/src/core/function-based/immediate-invocation.ts b/ember-resources/src/core/function-based/immediate-invocation.ts index b026ef4ad..04113ce18 100644 --- a/ember-resources/src/core/function-based/immediate-invocation.ts +++ b/ember-resources/src/core/function-based/immediate-invocation.ts @@ -134,7 +134,7 @@ class ResourceInvokerManager { * }) * ``` */ -export function resourceFactory( +export function resourceFactory( wrapperFn: (...args: Args) => ReturnType> /** * This is a bonkers return type. @@ -161,21 +161,21 @@ export function resourceFactory = - /** - * type for JS invocation with @use - * @use a = A.from(() => [b, c, d]) - */ - | ((thunk: () => SpreadFor) => ReturnType>) /** * 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) => ReturnType>) /** * Not passing args is allowed, too - * @use a = A.from() + * @use a = A() * * {{A}} */ diff --git a/ember-resources/src/util/fps.ts b/ember-resources/src/util/fps.ts new file mode 100644 index 000000000..be63688fd --- /dev/null +++ b/ember-resources/src/util/fps.ts @@ -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'; + * + * + * ``` + */ +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); + * + * + * } + * ``` + * + * 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; + }; + }); +}); diff --git a/test-app/package.json b/test-app/package.json index d364a1ad2..58faa605e 100644 --- a/test-app/package.json +++ b/test-app/package.json @@ -137,7 +137,7 @@ }, "packageManager": "pnpm@7.1.2", "volta": { - "extends": "../../package.json" + "extends": "../package.json" }, "msw": { "workerDirectory": "public" diff --git a/test-app/tests/utils/fps/rendering-test.gts b/test-app/tests/utils/fps/rendering-test.gts new file mode 100644 index 000000000..9fea87ce9 --- /dev/null +++ b/test-app/tests/utils/fps/rendering-test.gts @@ -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(); + + + 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++; + + + } + + await render( + + ); + + 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'); + }); + }); + +}); diff --git a/test-app/tests/utils/function-resource/clock-test.gts b/test-app/tests/utils/function-resource/clock-test.gts index c077855f0..e9041143b 100644 --- a/test-app/tests/utils/function-resource/clock-test.gts +++ b/test-app/tests/utils/function-resource/clock-test.gts @@ -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, {