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}}
+
+ ```
+
+
+
+
+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);
+
+
+ {{this.updateFrequency}}
+
+ }
+ ```
+
+ 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';
+ *
+ *
+ * {{FrameRate}}
+ *
+ * ```
+ */
+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);
+ *
+ *
+ * {{this.updateFrequency}}
+ *
+ * }
+ * ```
+ *
+ * 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(
+ {{FrameRate}}
+ );
+
+
+ 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++;
+
+
+
+ {{this.updateFrequency}}
+
+ }
+
+ 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, {