@player-tools/devtools-profiler-web-plugin 0.6.1--canary.125.2996

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.
Files changed (38) hide show
  1. package/dist/cjs/index.cjs +847 -0
  2. package/dist/cjs/index.cjs.map +1 -0
  3. package/dist/index.legacy-esm.js +812 -0
  4. package/dist/index.mjs +812 -0
  5. package/dist/index.mjs.map +1 -0
  6. package/package.json +42 -0
  7. package/src/WrapperComponent.tsx +121 -0
  8. package/src/__tests__/__snapshots__/index.test.tsx.snap +90 -0
  9. package/src/__tests__/index.test.tsx +307 -0
  10. package/src/constants/index.ts +28 -0
  11. package/src/content/common/Screen.tsx +20 -0
  12. package/src/content/common/index.ts +1 -0
  13. package/src/content/index.ts +19 -0
  14. package/src/content/navigation/index.ts +27 -0
  15. package/src/content/schema/index.ts +22 -0
  16. package/src/content/views/ProfilerView.tsx +75 -0
  17. package/src/content/views/index.ts +3 -0
  18. package/src/helpers/__tests__/genDataChangeTransaction.test.ts +40 -0
  19. package/src/helpers/__tests__/profiler.test.ts +104 -0
  20. package/src/helpers/genDataChangeTransaction.ts +39 -0
  21. package/src/helpers/index.ts +2 -0
  22. package/src/helpers/profiler.ts +111 -0
  23. package/src/index.tsx +490 -0
  24. package/src/types.ts +26 -0
  25. package/types/WrapperComponent.d.ts +4 -0
  26. package/types/constants/index.d.ts +15 -0
  27. package/types/content/common/Screen.d.ts +8 -0
  28. package/types/content/common/index.d.ts +2 -0
  29. package/types/content/index.d.ts +31 -0
  30. package/types/content/navigation/index.d.ts +7 -0
  31. package/types/content/schema/index.d.ts +23 -0
  32. package/types/content/views/ProfilerView.d.ts +3 -0
  33. package/types/content/views/index.d.ts +3 -0
  34. package/types/helpers/genDataChangeTransaction.d.ts +16 -0
  35. package/types/helpers/index.d.ts +3 -0
  36. package/types/helpers/profiler.d.ts +18 -0
  37. package/types/index.d.ts +7 -0
  38. package/types/types.d.ts +29 -0
@@ -0,0 +1,28 @@
1
+ import type { PluginData } from "@player-tools/devtools-types";
2
+
3
+ export const PLUGIN_ID = "player-ui-profiler-plugin";
4
+
5
+ export const PLUGIN_NAME = "Player UI Profiler";
6
+
7
+ export const PLUGIN_DESCRIPTION = "Standard Player UI Profiler";
8
+
9
+ export const PLUGIN_VERSION = "0.0.1";
10
+
11
+ export const VIEWS_IDS = {
12
+ PROFILER: "Profiler",
13
+ };
14
+
15
+ export const INTERACTIONS = {
16
+ START_PROFILING: "start-profiling",
17
+ STOP_PROFILING: "stop-profiling",
18
+ };
19
+
20
+ export const BASE_PLUGIN_DATA: Omit<PluginData, "flow"> = {
21
+ id: PLUGIN_ID,
22
+ name: PLUGIN_NAME,
23
+ description: PLUGIN_DESCRIPTION,
24
+ version: PLUGIN_VERSION,
25
+ };
26
+
27
+ export const PLUGIN_INACTIVE_WARNING =
28
+ "The plugin has been registered, but the Player development tools are not active. If you are working in a production environment, it is recommended to remove the plugin. Either way, you can activate the Player development tools by clicking on the extension popup and refreshing the page.";
@@ -0,0 +1,20 @@
1
+ import React from "react";
2
+ import { StackedView } from "@devtools-ui/plugin";
3
+
4
+ export const Screen = ({
5
+ main,
6
+ header,
7
+ footer,
8
+ id,
9
+ }: {
10
+ id: string;
11
+ main: React.ReactNode;
12
+ header?: React.ReactNode;
13
+ footer?: React.ReactNode;
14
+ }) => (
15
+ <StackedView id={id}>
16
+ {header && <StackedView.Header>{header}</StackedView.Header>}
17
+ <StackedView.Main>{main}</StackedView.Main>
18
+ {footer && <StackedView.Footer>{footer}</StackedView.Footer>}
19
+ </StackedView>
20
+ );
@@ -0,0 +1 @@
1
+ export * from "./Screen";
@@ -0,0 +1,19 @@
1
+ import { PLUGIN_ID } from "../constants";
2
+ import { navigation } from "./navigation";
3
+ import { schema } from "./schema";
4
+ import { views } from "./views";
5
+
6
+ export default {
7
+ id: PLUGIN_ID,
8
+ views,
9
+ navigation,
10
+ schema,
11
+ data: {
12
+ rootNode: {
13
+ name: "profiler time span",
14
+ value: 0,
15
+ },
16
+ displayFlameGraph: false,
17
+ profiling: false,
18
+ },
19
+ };
@@ -0,0 +1,27 @@
1
+ import { VIEWS_IDS } from "../../constants";
2
+
3
+ const transitions = Object.entries(VIEWS_IDS).reduce(
4
+ (acc, [key, value]) => ({
5
+ ...acc,
6
+ [value]: key,
7
+ }),
8
+ {} as Record<string, string>
9
+ );
10
+
11
+ export const navigation = {
12
+ BEGIN: "Plugin",
13
+ Plugin: {
14
+ startState: Object.keys(VIEWS_IDS)[0],
15
+ ...Object.entries(VIEWS_IDS).reduce(
16
+ (acc, [key, value]) => ({
17
+ ...acc,
18
+ [key]: {
19
+ state_type: "VIEW",
20
+ ref: value,
21
+ transitions,
22
+ },
23
+ }),
24
+ {} as Record<string, unknown>
25
+ ),
26
+ },
27
+ };
@@ -0,0 +1,22 @@
1
+ import type { Schema } from "@player-ui/types";
2
+ import { dataTypes } from "@player-ui/common-types-plugin";
3
+ import { DSLSchema, makeBindingsForObject } from "@player-tools/dsl";
4
+
5
+ const RecordType: Schema.DataType<Record<string, unknown>> = {
6
+ type: "RecordType",
7
+ };
8
+
9
+ export const schema = {
10
+ playerConfig: RecordType,
11
+ rootNode: RecordType,
12
+ profiling: dataTypes.BooleanType,
13
+ displayFlameGraph: dataTypes.BooleanType,
14
+ durations: [
15
+ {
16
+ name: dataTypes.StringType,
17
+ duration: dataTypes.StringType,
18
+ },
19
+ ] as [DSLSchema],
20
+ };
21
+
22
+ export const bindings = makeBindingsForObject(schema);
@@ -0,0 +1,75 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import React from "react";
3
+ import { expression as e } from "@player-tools/dsl";
4
+ import {
5
+ Action,
6
+ Collection,
7
+ FlameGraph,
8
+ Table,
9
+ Text,
10
+ } from "@devtools-ui/plugin";
11
+ import { INTERACTIONS, VIEWS_IDS } from "../../constants";
12
+ import { Screen } from "../common";
13
+ import { bindings } from "../schema";
14
+
15
+ const startProfilerExpression = e` publish('${INTERACTIONS.START_PROFILING}') `;
16
+ const stopProfilerExpression = e` publish('${INTERACTIONS.STOP_PROFILING}') `;
17
+
18
+ export const ProfilerView = (
19
+ <Screen
20
+ id={VIEWS_IDS.PROFILER}
21
+ header={
22
+ <Collection>
23
+ <Collection.Values>
24
+ <Action
25
+ applicability={e` {{profiling}} === true ` as any}
26
+ key="stopProfiler"
27
+ exp={stopProfilerExpression.toString()}
28
+ >
29
+ <Action.Label>
30
+ <Text>Stop</Text>
31
+ </Action.Label>
32
+ </Action>
33
+ <Action
34
+ applicability={e` {{profiling}} === false ` as any}
35
+ key="startProfiler"
36
+ exp={startProfilerExpression.toString()}
37
+ >
38
+ <Action.Label>
39
+ <Text>Start</Text>
40
+ </Action.Label>
41
+ </Action>
42
+ </Collection.Values>
43
+ </Collection>
44
+ }
45
+ main={
46
+ <Collection>
47
+ <Collection.Values>
48
+ <Table
49
+ applicability={e` {{displayFlameGraph}} === true ` as any}
50
+ binding={bindings.durations as any}
51
+ />
52
+ <FlameGraph
53
+ applicability={e` {{displayFlameGraph}} === true ` as any}
54
+ binding={bindings.rootNode as any}
55
+ height={200}
56
+ />
57
+ <Text
58
+ applicability={
59
+ e` {{displayFlameGraph}} === false && {{profiling}} === true` as any
60
+ }
61
+ >
62
+ Profiling...
63
+ </Text>
64
+ <Text
65
+ applicability={
66
+ e` {{displayFlameGraph}} === false && {{profiling}} === false ` as any
67
+ }
68
+ >
69
+ Start the profiler to generate the flame graph.
70
+ </Text>
71
+ </Collection.Values>
72
+ </Collection>
73
+ }
74
+ />
75
+ );
@@ -0,0 +1,3 @@
1
+ import { ProfilerView } from "./ProfilerView";
2
+
3
+ export const views = [ProfilerView];
@@ -0,0 +1,40 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import type {
3
+ DevtoolsDataChangeEvent,
4
+ Transaction,
5
+ } from "@player-tools/devtools-types";
6
+ import { genDataChangeTransaction } from "../genDataChangeTransaction";
7
+
8
+ describe("genDataChangeTransaction", () => {
9
+ it("should correctly generate a data change transaction for valid input", () => {
10
+ const input = {
11
+ playerID: "player1",
12
+ data: {
13
+ key: "value",
14
+ anotherKey: 123,
15
+ },
16
+ pluginID: "pluginA",
17
+ };
18
+
19
+ const expectedOutput: Transaction<DevtoolsDataChangeEvent> = {
20
+ id: -1,
21
+ type: "PLAYER_DEVTOOLS_PLUGIN_DATA_CHANGE",
22
+ payload: {
23
+ pluginID: "pluginA",
24
+ data: {
25
+ key: "value",
26
+ anotherKey: 123,
27
+ },
28
+ },
29
+ sender: "player1",
30
+ context: "player",
31
+ target: "player",
32
+ timestamp: expect.any(Number),
33
+ _messenger_: true,
34
+ };
35
+
36
+ const result = genDataChangeTransaction(input);
37
+
38
+ expect(result).toEqual(expect.objectContaining(expectedOutput));
39
+ });
40
+ });
@@ -0,0 +1,104 @@
1
+ import { describe, expect, test, vi } from "vitest";
2
+ import { profiler } from "..";
3
+ import { ProfilerNode } from "../../types";
4
+
5
+ // mock performance.now
6
+ let count = 2490.0;
7
+ const now = vi.fn(() => {
8
+ count += 0.1;
9
+ return count;
10
+ });
11
+ global.performance = { ...global.performance, now };
12
+
13
+ describe("Profiler", () => {
14
+ test("starts the profiler, keep track of the events, and return the profiler tree", () => {
15
+ const { startTimer, endTimer, stopProfiler, start } = profiler();
16
+
17
+ start();
18
+
19
+ // process with no children
20
+ startTimer("process1");
21
+ endTimer({ hookName: "process1" });
22
+
23
+ // process with children
24
+ const parentNode: ProfilerNode = {
25
+ name: "process2",
26
+ children: [],
27
+ };
28
+
29
+ startTimer("process2");
30
+ startTimer("process2.1");
31
+ startTimer("process2.2");
32
+ endTimer({ hookName: "process2.1", parentNode });
33
+ endTimer({ hookName: "process2.2", parentNode });
34
+ endTimer({ hookName: "process2", children: parentNode.children });
35
+
36
+ const rootNode = stopProfiler();
37
+
38
+ expect(rootNode).toStrictEqual({
39
+ durations: [
40
+ { name: "process2", duration: "0.5000 ms" },
41
+ { name: "process2.1", duration: "0.2000 ms" },
42
+ { name: "process2.2", duration: "0.2000 ms" },
43
+ { name: "process1", duration: "0.1000 ms" },
44
+ ],
45
+ rootNode: {
46
+ children: [
47
+ {
48
+ children: [],
49
+ endTime: 2490.2999999999997,
50
+ name: "process1",
51
+ startTime: 2490.2,
52
+ tooltip: "process1, 0.1000 (ms)",
53
+ value: 100,
54
+ },
55
+ {
56
+ children: [
57
+ {
58
+ children: [],
59
+ endTime: 2490.6999999999994,
60
+ name: "process2.1",
61
+ startTime: 2490.4999999999995,
62
+ tooltip: "process2.1, 0.2000 (ms)",
63
+ value: 200,
64
+ },
65
+ {
66
+ children: [],
67
+ endTime: 2490.7999999999993,
68
+ name: "process2.2",
69
+ startTime: 2490.5999999999995,
70
+ tooltip: "process2.2, 0.2000 (ms)",
71
+ value: 200,
72
+ },
73
+ ],
74
+ endTime: 2490.899999999999,
75
+ name: "process2",
76
+ startTime: 2490.3999999999996,
77
+ tooltip: "process2, 0.5000 (ms)",
78
+ value: 500,
79
+ },
80
+ ],
81
+ endTime: 2490.999999999999,
82
+ name: "root",
83
+ startTime: 2490.1,
84
+ tooltip: "Profiler total time span 0.9000 (ms)",
85
+ value: 600,
86
+ },
87
+ });
88
+
89
+ // (re)start
90
+ start();
91
+ const { rootNode: rootNode2, durations } = stopProfiler();
92
+
93
+ expect(durations).toStrictEqual([]);
94
+
95
+ expect(rootNode2).toStrictEqual({
96
+ name: "root",
97
+ endTime: 2491.199999999999,
98
+ startTime: 2491.099999999999,
99
+ tooltip: "Profiler total time span 0.1000 (ms)",
100
+ value: 100,
101
+ children: [],
102
+ });
103
+ });
104
+ });
@@ -0,0 +1,39 @@
1
+ import type {
2
+ DevtoolsDataChangeEvent,
3
+ Transaction,
4
+ } from "@player-tools/devtools-types";
5
+ import type { Flow } from "@player-ui/react";
6
+
7
+ const NOOP_ID = -1;
8
+
9
+ /**
10
+ * Generates a data change transaction for the player devtools plugin.
11
+ *
12
+ * This function creates a transaction object that represents a change in data
13
+ * within a player devtools plugin. The transaction includes details such as the
14
+ * plugin ID, the changed data, and the player ID. It is used to communicate
15
+ * changes between the plugin and devtools.
16
+ */
17
+ export const genDataChangeTransaction = ({
18
+ playerID,
19
+ data,
20
+ pluginID,
21
+ }: {
22
+ playerID: string;
23
+ data: Flow["data"];
24
+ pluginID: string;
25
+ }): Transaction<DevtoolsDataChangeEvent> => {
26
+ return {
27
+ id: NOOP_ID,
28
+ type: "PLAYER_DEVTOOLS_PLUGIN_DATA_CHANGE",
29
+ payload: {
30
+ pluginID,
31
+ data,
32
+ },
33
+ sender: playerID,
34
+ context: "player",
35
+ target: "player",
36
+ timestamp: Date.now(),
37
+ _messenger_: true,
38
+ };
39
+ };
@@ -0,0 +1,2 @@
1
+ export { genDataChangeTransaction } from "./genDataChangeTransaction";
2
+ export { profiler } from "./profiler";
@@ -0,0 +1,111 @@
1
+ import type { ProfilerNode } from "../types";
2
+
3
+ export const profiler = () => {
4
+ let rootNode: ProfilerNode = {
5
+ name: "root",
6
+ children: [],
7
+ };
8
+
9
+ let record: { [key: string]: number[] } = {};
10
+ let durations: { hookName: string; duration: number }[] = [];
11
+
12
+ const start = () => {
13
+ rootNode = {
14
+ name: "root",
15
+ startTime: performance.now(),
16
+ children: [],
17
+ };
18
+ record = {};
19
+ durations = [];
20
+ };
21
+
22
+ const addNodeToTree = (newNode: ProfilerNode, parentNode: ProfilerNode) => {
23
+ parentNode.children.push(newNode);
24
+ return newNode;
25
+ };
26
+
27
+ const startTimer = (hookName: string) => {
28
+ const startTime = performance.now();
29
+
30
+ if (!record[hookName] || record[hookName].length === 2) {
31
+ record[hookName] = [];
32
+ record[hookName].push(startTime);
33
+ }
34
+ };
35
+
36
+ const endTimer = ({
37
+ hookName,
38
+ parentNode = rootNode,
39
+ children,
40
+ }: {
41
+ hookName: string;
42
+ parentNode?: ProfilerNode;
43
+ children?: ProfilerNode[];
44
+ }) => {
45
+ let startTime: number | undefined;
46
+ let duration: number | undefined;
47
+
48
+ const endTime = performance.now();
49
+
50
+ for (const key in record) {
51
+ if (key === hookName && record[key].length === 1) {
52
+ [startTime] = record[key];
53
+ duration = endTime - startTime;
54
+ record[key].push(endTime);
55
+ }
56
+ }
57
+
58
+ const value = Math.ceil((duration || 0.01) * 1000);
59
+
60
+ const newNode: ProfilerNode = {
61
+ name: hookName,
62
+ startTime,
63
+ endTime,
64
+ value,
65
+ tooltip: `${hookName}, ${(duration || 0.01).toFixed(4)} (ms)`,
66
+ children: children ?? [],
67
+ };
68
+
69
+ addNodeToTree(newNode, parentNode);
70
+
71
+ // Push the hookName and duration into durations array
72
+ durations.push({ hookName, duration: duration ? duration : 0.01 });
73
+
74
+ return newNode;
75
+ };
76
+
77
+ const stopProfiler = (): {
78
+ rootNode: ProfilerNode;
79
+ durations: { name: string; duration: string }[];
80
+ } => {
81
+ const endTime = performance.now();
82
+ const totalTime = endTime - (rootNode.startTime ?? 0);
83
+
84
+ rootNode.endTime = endTime;
85
+ // set the stop profiler value is the sum of its children values
86
+ // otherwise the difference of width of the root and the other nodes
87
+ // make it impossible to see them into the flame graph
88
+ rootNode.value =
89
+ rootNode.children.reduce((acc, { value }) => (acc += value ?? 0), 0) ||
90
+ Math.ceil((totalTime || 0.01) * 1000);
91
+ rootNode.tooltip = `Profiler total time span ${totalTime.toFixed(4)} (ms)`;
92
+
93
+ // Sort durations array in descending order
94
+ durations.sort((a, b) => b.duration - a.duration);
95
+
96
+ return {
97
+ rootNode,
98
+ durations: durations.map(({ hookName, duration }) => ({
99
+ name: hookName,
100
+ duration: `${duration.toFixed(4)} ms`,
101
+ })),
102
+ };
103
+ };
104
+
105
+ return {
106
+ start,
107
+ startTimer,
108
+ endTimer,
109
+ stopProfiler,
110
+ };
111
+ };