@atlaskit/editor-plugin-table 2.7.2 → 2.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -29,6 +29,23 @@ import {
29
29
  import { pluginKey } from '../../../plugins/table/pm-plugins/plugin-key';
30
30
  import { TablePluginState } from '../../../plugins/table/types';
31
31
 
32
+ const mockStartMeasure = jest.fn();
33
+ const mockEndMeasure = jest.fn(() => {
34
+ return [51, 52, 53, 54];
35
+ });
36
+ const mockCountFrames = jest.fn();
37
+
38
+ jest.mock('../../../plugins/table/utils/analytics', () => ({
39
+ ...jest.requireActual('../../../plugins/table/utils/analytics'),
40
+ useMeasureFramerate: () => {
41
+ return {
42
+ startMeasure: mockStartMeasure,
43
+ endMeasure: mockEndMeasure,
44
+ countFrames: mockCountFrames,
45
+ };
46
+ },
47
+ }));
48
+
32
49
  describe('table -> nodeviews -> TableContainer.tsx', () => {
33
50
  const createEditor = createEditorFactory<TablePluginState>();
34
51
  const editor = (
@@ -202,7 +219,7 @@ describe('table -> nodeviews -> TableContainer.tsx', () => {
202
219
  fireEvent.mouseMove(container.querySelector('.resizer-handle-right')!);
203
220
  fireEvent.mouseUp(container.querySelector('.resizer-handle-right')!);
204
221
 
205
- expect(analyticsMock).toHaveBeenLastCalledWith({
222
+ expect(analyticsMock).toHaveBeenCalledWith({
206
223
  action: TABLE_ACTION.RESIZED,
207
224
  actionSubject: ACTION_SUBJECT.TABLE,
208
225
  eventType: EVENT_TYPE.TRACK,
@@ -215,6 +232,45 @@ describe('table -> nodeviews -> TableContainer.tsx', () => {
215
232
  totalColumnCount: 3,
216
233
  },
217
234
  });
235
+
236
+ expect(analyticsMock).toHaveBeenCalledWith({
237
+ action: TABLE_ACTION.RESIZE_PERF_SAMPLING,
238
+ actionSubject: ACTION_SUBJECT.TABLE,
239
+ eventType: EVENT_TYPE.OPERATIONAL,
240
+ attributes: {
241
+ docSize: 22,
242
+ frameRate: 51,
243
+ isInitialSample: true,
244
+ nodeSize: 20,
245
+ },
246
+ });
247
+
248
+ expect(analyticsMock).toHaveBeenCalledWith({
249
+ action: TABLE_ACTION.RESIZE_PERF_SAMPLING,
250
+ actionSubject: ACTION_SUBJECT.TABLE,
251
+ eventType: EVENT_TYPE.OPERATIONAL,
252
+ attributes: {
253
+ docSize: 22,
254
+ frameRate: 53,
255
+ isInitialSample: false,
256
+ nodeSize: 20,
257
+ },
258
+ });
259
+
260
+ analyticsMock.mockReset();
261
+ });
262
+
263
+ test('calls useMeasureFramerate handlers', async () => {
264
+ const { container } = buildContainer({ layout: 'wide' });
265
+
266
+ fireEvent.mouseDown(container.querySelector('.resizer-handle-right')!);
267
+ fireEvent.mouseMove(container.querySelector('.resizer-handle-right')!, {
268
+ clientX: 100,
269
+ });
270
+ fireEvent.mouseUp(container.querySelector('.resizer-handle-right')!);
271
+
272
+ expect(mockStartMeasure).toHaveBeenCalled();
273
+ expect(mockEndMeasure).toHaveBeenCalled();
218
274
  });
219
275
  });
220
276
  });
@@ -0,0 +1,98 @@
1
+ import { renderHook } from '@testing-library/react-hooks';
2
+
3
+ import {
4
+ ACTION_SUBJECT,
5
+ EVENT_TYPE,
6
+ TABLE_ACTION,
7
+ } from '@atlaskit/editor-common/analytics';
8
+
9
+ import {
10
+ generateResizeFrameRatePayloads,
11
+ reduceResizeFrameRateSamples,
12
+ useMeasureFramerate,
13
+ } from '../../../plugins/table/utils/analytics';
14
+
15
+ describe('reduceResizeFrameRateSamples()', () => {
16
+ it('should return the same array if it has only one element', () => {
17
+ expect(reduceResizeFrameRateSamples([1])).toEqual([1]);
18
+ });
19
+
20
+ it('should return the first element and the average of the array if length > 1', () => {
21
+ expect(reduceResizeFrameRateSamples([3, 2, 4, 6])).toEqual([3, 4]);
22
+ });
23
+ });
24
+
25
+ describe('generateResizeFrameRatePayloads()', () => {
26
+ it('should return an empty array if the array is empty', () => {
27
+ expect(
28
+ generateResizeFrameRatePayloads({
29
+ docSize: 10,
30
+ frameRateSamples: [],
31
+ originalNode: { nodeSize: 5 } as any,
32
+ }),
33
+ ).toEqual([]);
34
+ });
35
+
36
+ it('should return an array of payloads with the correct attributes', () => {
37
+ expect(
38
+ generateResizeFrameRatePayloads({
39
+ docSize: 10,
40
+ frameRateSamples: [3, 2, 4, 6],
41
+ originalNode: { nodeSize: 5 } as any,
42
+ }),
43
+ ).toEqual([
44
+ {
45
+ action: TABLE_ACTION.RESIZE_PERF_SAMPLING,
46
+ actionSubject: ACTION_SUBJECT.TABLE,
47
+ eventType: EVENT_TYPE.OPERATIONAL,
48
+ attributes: {
49
+ frameRate: 3,
50
+ nodeSize: 5,
51
+ docSize: 10,
52
+ isInitialSample: true,
53
+ },
54
+ },
55
+ {
56
+ action: TABLE_ACTION.RESIZE_PERF_SAMPLING,
57
+ actionSubject: ACTION_SUBJECT.TABLE,
58
+ eventType: EVENT_TYPE.OPERATIONAL,
59
+ attributes: {
60
+ frameRate: 4,
61
+ nodeSize: 5,
62
+ docSize: 10,
63
+ isInitialSample: false,
64
+ },
65
+ },
66
+ ]);
67
+ });
68
+ });
69
+
70
+ describe('useMeasureFramerate()', () => {
71
+ jest.useFakeTimers('modern');
72
+
73
+ it('should return the correct handlers', () => {
74
+ const { result } = renderHook(() => useMeasureFramerate());
75
+ const { startMeasure, endMeasure, countFrames } = result.current;
76
+
77
+ expect(startMeasure).toBeInstanceOf(Function);
78
+ expect(endMeasure).toBeInstanceOf(Function);
79
+ expect(countFrames).toBeInstanceOf(Function);
80
+ });
81
+
82
+ it('should return the correct frame rate sample', async () => {
83
+ const { result } = renderHook(() =>
84
+ useMeasureFramerate({ minTimeMs: 0, minFrames: 0, sampleRateMs: 0 }),
85
+ );
86
+
87
+ const { startMeasure, endMeasure, countFrames } = result.current;
88
+ jest.advanceTimersByTime(100);
89
+ startMeasure();
90
+ jest.advanceTimersByTime(100);
91
+ countFrames();
92
+ const samples = endMeasure();
93
+
94
+ expect(samples).toEqual([10]);
95
+
96
+ jest.useRealTimers();
97
+ });
98
+ });
@@ -8,12 +8,7 @@ import React, {
8
8
 
9
9
  import rafSchd from 'raf-schd';
10
10
 
11
- import {
12
- ACTION_SUBJECT,
13
- EVENT_TYPE,
14
- TABLE_ACTION,
15
- TableEventPayload,
16
- } from '@atlaskit/editor-common/analytics';
11
+ import { TableEventPayload } from '@atlaskit/editor-common/analytics';
17
12
  import { getGuidelinesWithHighlights } from '@atlaskit/editor-common/guideline';
18
13
  import type { GuidelineConfig } from '@atlaskit/editor-common/guideline';
19
14
  import {
@@ -25,21 +20,24 @@ import { resizerHandleShadowClassName } from '@atlaskit/editor-common/styles';
25
20
  import { Node as PMNode } from '@atlaskit/editor-prosemirror/model';
26
21
  import { Transaction } from '@atlaskit/editor-prosemirror/state';
27
22
  import { EditorView } from '@atlaskit/editor-prosemirror/view';
28
- import { TableMap } from '@atlaskit/editor-tables';
29
23
  import { findTable } from '@atlaskit/editor-tables/utils';
30
24
 
31
25
  import {
32
26
  COLUMN_MIN_WIDTH,
33
27
  getColgroupChildrenLength,
34
- hasTableBeenResized,
35
28
  previewScaleTable,
36
29
  scaleTable,
37
30
  } from '../pm-plugins/table-resizing/utils';
38
31
  import { pluginKey as tableWidthPluginKey } from '../pm-plugins/table-width';
39
32
  import { TABLE_HIGHLIGHT_GAP, TABLE_SNAP_GAP } from '../ui/consts';
40
- import { getTableWidth } from '../utils';
33
+ import {
34
+ generateResizedPayload,
35
+ generateResizeFrameRatePayloads,
36
+ useMeasureFramerate,
37
+ } from '../utils/analytics';
41
38
  import { defaultGuidelines, defaultGuidelineWidths } from '../utils/guidelines';
42
39
  import { findClosestSnap } from '../utils/snapping';
40
+
43
41
  interface TableResizerProps {
44
42
  width: number;
45
43
  maxWidth: number;
@@ -57,29 +55,6 @@ interface TableResizerProps {
57
55
  const handles = { right: true };
58
56
  const tableHandleMarginTop = 12;
59
57
 
60
- const generateResizedPayload = (props: {
61
- originalNode: PMNode;
62
- resizedNode: PMNode;
63
- }): TableEventPayload => {
64
- const tableMap = TableMap.get(props.resizedNode);
65
-
66
- return {
67
- action: TABLE_ACTION.RESIZED,
68
- actionSubject: ACTION_SUBJECT.TABLE,
69
- eventType: EVENT_TYPE.TRACK,
70
- attributes: {
71
- newWidth: props.resizedNode.attrs.width,
72
- prevWidth: props.originalNode.attrs.width ?? null,
73
- nodeSize: props.resizedNode.nodeSize,
74
- totalTableWidth: hasTableBeenResized(props.resizedNode)
75
- ? getTableWidth(props.resizedNode)
76
- : null,
77
- totalRowCount: tableMap.height,
78
- totalColumnCount: tableMap.width,
79
- },
80
- };
81
- };
82
-
83
58
  const getResizerHandleHeight = (tableRef: HTMLTableElement) => {
84
59
  const tableHeight = tableRef?.clientHeight;
85
60
  let handleHeightSize: HandleHeightSizeType | undefined = 'small';
@@ -129,6 +104,8 @@ export const TableResizer = ({
129
104
  const resizerMinWidth = getResizerMinWidth(node);
130
105
  const handleHeightSize = getResizerHandleHeight(tableRef);
131
106
 
107
+ const { startMeasure, endMeasure, countFrames } = useMeasureFramerate();
108
+
132
109
  const updateActiveGuidelines = useCallback(
133
110
  ({ gap, keys }: { gap: number; keys: string[] }) => {
134
111
  if (gap !== currentGap.current) {
@@ -157,6 +134,8 @@ export const TableResizer = ({
157
134
  );
158
135
 
159
136
  const handleResizeStart = useCallback(() => {
137
+ startMeasure();
138
+
160
139
  const {
161
140
  dispatch,
162
141
  state: { tr },
@@ -165,7 +144,7 @@ export const TableResizer = ({
165
144
  dispatch(tr.setMeta(tableWidthPluginKey, { resizing: true }));
166
145
 
167
146
  setSnappingEnabled(displayGuideline(defaultGuidelines));
168
- }, [displayGuideline, editorView]);
147
+ }, [displayGuideline, editorView, startMeasure]);
169
148
 
170
149
  const handleResizeStop = useCallback<HandleResize>(
171
150
  (originalState, delta) => {
@@ -174,6 +153,18 @@ export const TableResizer = ({
174
153
  const pos = getPos();
175
154
 
176
155
  let tr = state.tr.setMeta(tableWidthPluginKey, { resizing: false });
156
+ const frameRateSamples = endMeasure();
157
+
158
+ if (frameRateSamples.length > 0) {
159
+ const resizeFrameRatePayloads = generateResizeFrameRatePayloads({
160
+ docSize: state.doc.nodeSize,
161
+ frameRateSamples,
162
+ originalNode: node,
163
+ });
164
+ resizeFrameRatePayloads.forEach((payload) => {
165
+ attachAnalyticsEvent(payload)?.(tr);
166
+ });
167
+ }
177
168
 
178
169
  if (typeof pos === 'number') {
179
170
  tr = tr.setNodeMarkup(pos, undefined, {
@@ -219,11 +210,13 @@ export const TableResizer = ({
219
210
  tableRef,
220
211
  displayGuideline,
221
212
  attachAnalyticsEvent,
213
+ endMeasure,
222
214
  ],
223
215
  );
224
216
 
225
217
  const handleResize = useCallback(
226
218
  (originalState, delta) => {
219
+ countFrames();
227
220
  const newWidth = originalState.width + delta.width;
228
221
  const pos = getPos();
229
222
  if (typeof pos !== 'number') {
@@ -254,7 +247,15 @@ export const TableResizer = ({
254
247
 
255
248
  return newWidth;
256
249
  },
257
- [editorView, getPos, node, tableRef, updateWidth, updateActiveGuidelines],
250
+ [
251
+ editorView,
252
+ getPos,
253
+ node,
254
+ tableRef,
255
+ updateWidth,
256
+ updateActiveGuidelines,
257
+ countFrames,
258
+ ],
258
259
  );
259
260
 
260
261
  const scheduleResize = useMemo(() => rafSchd(handleResize), [handleResize]);
@@ -36,41 +36,45 @@ import { Props, TableOptions } from './types';
36
36
 
37
37
  type ForwardRef = (node: HTMLElement | null) => void;
38
38
 
39
- const tableAttributes = (
39
+ const tableAttributes = (node: PmNode) => {
40
+ return {
41
+ 'data-number-column': node.attrs.isNumberColumnEnabled,
42
+ 'data-layout': node.attrs.layout,
43
+ 'data-autosize': node.attrs.__autoSize,
44
+ 'data-table-local-id': node.attrs.localId || '',
45
+ 'data-table-width': node.attrs.width,
46
+ };
47
+ };
48
+
49
+ const getInlineWidth = (
40
50
  node: PmNode,
41
51
  options: Props['options'],
42
52
  state: EditorState,
43
53
  pos: number | undefined,
44
- ) => {
54
+ ): number | undefined => {
45
55
  // provide a width for tables when custom table width is supported
46
56
  // this is to ensure 'responsive' tables (colgroup widths are undefined) become fixed to
47
57
  // support screen size adjustments
48
58
  const shouldHaveInlineWidth =
49
59
  options?.isTableResizingEnabled && !isTableNested(state, pos);
50
60
 
51
- let style = `width: ${
52
- node.attrs.isNumberColumnEnabled
53
- ? getTableContainerWidth(node) - akEditorTableNumberColumnWidth
54
- : getTableContainerWidth(node)
55
- }px`;
61
+ let widthValue = getTableContainerWidth(node);
56
62
 
57
- const dataAttrsInTable = {
58
- 'data-number-column': node.attrs.isNumberColumnEnabled,
59
- 'data-layout': node.attrs.layout,
60
- 'data-autosize': node.attrs.__autoSize,
61
- 'data-table-local-id': node.attrs.localId || '',
62
- 'data-table-width': node.attrs.width,
63
- };
64
-
65
- if (shouldHaveInlineWidth) {
66
- // this should be fixed because style will overwrite any existing styles, current found conflict with sticky headers
67
- return {
68
- ...dataAttrsInTable,
69
- style,
70
- };
63
+ if (node.attrs.isNumberColumnEnabled) {
64
+ widthValue -= akEditorTableNumberColumnWidth;
71
65
  }
72
66
 
73
- return dataAttrsInTable;
67
+ return shouldHaveInlineWidth ? widthValue : undefined;
68
+ };
69
+
70
+ const handleInlineTableWidth = (
71
+ table: HTMLElement,
72
+ width: number | undefined,
73
+ ) => {
74
+ if (!table || !width) {
75
+ return;
76
+ }
77
+ table.style.setProperty('width', `${width}px`);
74
78
  };
75
79
 
76
80
  const toDOM = (node: PmNode, props: Props) => {
@@ -82,7 +86,7 @@ const toDOM = (node: PmNode, props: Props) => {
82
86
 
83
87
  return [
84
88
  'table',
85
- tableAttributes(node, props.options, props.view.state, props.getPos()),
89
+ tableAttributes(node),
86
90
  colgroup,
87
91
  ['tbody', 0],
88
92
  ] as DOMOutputSpec;
@@ -123,8 +127,18 @@ export default class TableView extends ReactNodeView<Props> {
123
127
  contentDOM?: HTMLElement;
124
128
  };
125
129
 
130
+ const tableInlineWidth = getInlineWidth(
131
+ this.node,
132
+ this.reactComponentProps.options,
133
+ this.reactComponentProps.view.state,
134
+ this.reactComponentProps.getPos(),
135
+ );
136
+
126
137
  if (rendered.dom) {
127
138
  this.table = rendered.dom;
139
+ if (tableInlineWidth) {
140
+ handleInlineTableWidth(this.table, tableInlineWidth);
141
+ }
128
142
  }
129
143
 
130
144
  return rendered;
@@ -135,15 +149,22 @@ export default class TableView extends ReactNodeView<Props> {
135
149
  return;
136
150
  }
137
151
 
138
- const attrs = tableAttributes(
152
+ const attrs = tableAttributes(node);
153
+ (Object.keys(attrs) as Array<keyof typeof attrs>).forEach((attr) => {
154
+ this.table!.setAttribute(attr, attrs[attr]);
155
+ });
156
+
157
+ // handle inline style when table been resized
158
+ const tableInlineWidth = getInlineWidth(
139
159
  node,
140
160
  (this.reactComponentProps as Props).options,
141
161
  this.view.state,
142
162
  this.getPos(),
143
163
  );
144
- (Object.keys(attrs) as Array<keyof typeof attrs>).forEach((attr) => {
145
- this.table!.setAttribute(attr, attrs[attr]);
146
- });
164
+
165
+ if (tableInlineWidth) {
166
+ handleInlineTableWidth(this.table, tableInlineWidth);
167
+ }
147
168
  }
148
169
 
149
170
  getNode = () => {
@@ -1,13 +1,25 @@
1
+ import { useEffect, useRef } from 'react';
2
+
1
3
  import type {
2
4
  AnalyticsEventPayload,
3
5
  AnalyticsEventPayloadCallback,
4
6
  EditorAnalyticsAPI,
7
+ TableEventPayload,
8
+ } from '@atlaskit/editor-common/analytics';
9
+ import {
10
+ ACTION_SUBJECT,
11
+ EVENT_TYPE,
12
+ TABLE_ACTION,
5
13
  } from '@atlaskit/editor-common/analytics';
6
14
  import { HigherOrderCommand } from '@atlaskit/editor-common/types';
15
+ import type { Node as PMNode } from '@atlaskit/editor-prosemirror/model';
7
16
  import { Selection } from '@atlaskit/editor-prosemirror/state';
8
17
  import { TableMap } from '@atlaskit/editor-tables/table-map';
9
18
  import { findTable, getSelectionRect } from '@atlaskit/editor-tables/utils';
10
19
 
20
+ import { hasTableBeenResized } from '../pm-plugins/table-resizing/utils';
21
+ import { getTableWidth } from '../utils';
22
+
11
23
  export function getSelectedTableInfo(selection: Selection) {
12
24
  let map;
13
25
  let totalRowCount = 0;
@@ -78,3 +90,159 @@ export const withEditorAnalyticsAPI =
78
90
  view,
79
91
  );
80
92
  };
93
+
94
+ interface UseMeasureFramerateConfig {
95
+ maxSamples?: number;
96
+ minFrames?: number;
97
+ minTimeMs?: number;
98
+ sampleRateMs?: number;
99
+ timeoutMs?: number;
100
+ }
101
+
102
+ export const generateResizedPayload = (props: {
103
+ originalNode: PMNode;
104
+ resizedNode: PMNode;
105
+ }): TableEventPayload => {
106
+ const tableMap = TableMap.get(props.resizedNode);
107
+
108
+ return {
109
+ action: TABLE_ACTION.RESIZED,
110
+ actionSubject: ACTION_SUBJECT.TABLE,
111
+ eventType: EVENT_TYPE.TRACK,
112
+ attributes: {
113
+ newWidth: props.resizedNode.attrs.width,
114
+ prevWidth: props.originalNode.attrs.width ?? null,
115
+ nodeSize: props.resizedNode.nodeSize,
116
+ totalTableWidth: hasTableBeenResized(props.resizedNode)
117
+ ? getTableWidth(props.resizedNode)
118
+ : null,
119
+ totalRowCount: tableMap.height,
120
+ totalColumnCount: tableMap.width,
121
+ },
122
+ };
123
+ };
124
+
125
+ export const reduceResizeFrameRateSamples = (frameRateSamples: number[]) => {
126
+ if (frameRateSamples.length > 1) {
127
+ const frameRateSum = frameRateSamples.reduce((sum, frameRate, index) => {
128
+ if (index === 0) {
129
+ return sum;
130
+ } else {
131
+ return sum + frameRate;
132
+ }
133
+ }, 0);
134
+ const averageFrameRate = Math.round(
135
+ frameRateSum / (frameRateSamples.length - 1),
136
+ );
137
+ return [frameRateSamples[0], averageFrameRate];
138
+ } else {
139
+ return frameRateSamples;
140
+ }
141
+ };
142
+
143
+ export const generateResizeFrameRatePayloads = (props: {
144
+ docSize: number;
145
+ frameRateSamples: number[];
146
+ originalNode: PMNode;
147
+ }): TableEventPayload[] => {
148
+ const reducedResizeFrameRateSamples = reduceResizeFrameRateSamples(
149
+ props.frameRateSamples,
150
+ );
151
+ return reducedResizeFrameRateSamples.map((frameRateSample, index) => ({
152
+ action: TABLE_ACTION.RESIZE_PERF_SAMPLING,
153
+ actionSubject: ACTION_SUBJECT.TABLE,
154
+ eventType: EVENT_TYPE.OPERATIONAL,
155
+ attributes: {
156
+ frameRate: frameRateSample,
157
+ nodeSize: props.originalNode.nodeSize,
158
+ docSize: props.docSize,
159
+ isInitialSample: index === 0,
160
+ },
161
+ }));
162
+ };
163
+
164
+ /**
165
+ * Measures the framerate of a component over a given time period.
166
+ */
167
+ export const useMeasureFramerate = (config: UseMeasureFramerateConfig = {}) => {
168
+ const {
169
+ maxSamples = 10,
170
+ minFrames = 5,
171
+ minTimeMs = 500,
172
+ sampleRateMs = 1000,
173
+ timeoutMs = 200,
174
+ } = config;
175
+
176
+ let frameCount = useRef(0);
177
+ let lastTime = useRef(0);
178
+ let timeoutId = useRef<NodeJS.Timeout | undefined>();
179
+ let frameRateSamples = useRef<number[]>([]);
180
+
181
+ useEffect(() => {
182
+ return () => {
183
+ if (timeoutId.current) {
184
+ clearTimeout(timeoutId.current);
185
+ }
186
+ };
187
+ }, []);
188
+
189
+ const startMeasure = () => {
190
+ frameCount.current = 0;
191
+ lastTime.current = performance.now();
192
+ };
193
+
194
+ /**
195
+ * Returns an array of frame rate samples as integers.
196
+ */
197
+ const endMeasure = () => {
198
+ const samples = frameRateSamples.current;
199
+ frameRateSamples.current = [];
200
+ return samples;
201
+ };
202
+
203
+ const sampleFrameRate = (delay = 0) => {
204
+ const currentTime = performance.now();
205
+ const deltaTime = currentTime - lastTime.current - delay;
206
+ const isValidSample =
207
+ deltaTime > minTimeMs && frameCount.current >= minFrames;
208
+ if (isValidSample) {
209
+ const frameRate = Math.round(frameCount.current / (deltaTime / 1000));
210
+ frameRateSamples.current.push(frameRate);
211
+ }
212
+ frameCount.current = 0;
213
+ lastTime.current = 0;
214
+ };
215
+
216
+ /**
217
+ * Counts the number of frames that occur within a given time period. Intended to be called
218
+ * inside a `requestAnimationFrame` callback.
219
+ */
220
+ const countFrames = () => {
221
+ if (frameRateSamples.current.length >= maxSamples && timeoutId.current) {
222
+ clearTimeout(timeoutId.current);
223
+ return;
224
+ }
225
+
226
+ /**
227
+ * Allows us to keep counting frames even if `startMeasure` is not called
228
+ */
229
+ if (lastTime.current === 0) {
230
+ lastTime.current = performance.now();
231
+ }
232
+ frameCount.current++;
233
+
234
+ if (timeoutId.current) {
235
+ clearTimeout(timeoutId.current);
236
+ }
237
+ if (performance.now() - lastTime.current > sampleRateMs) {
238
+ sampleFrameRate();
239
+ } else {
240
+ timeoutId.current = setTimeout(
241
+ () => sampleFrameRate(timeoutMs),
242
+ timeoutMs,
243
+ );
244
+ }
245
+ };
246
+
247
+ return { startMeasure, endMeasure, countFrames };
248
+ };