From 01a15543dbfcb3b957bd1748bd17d056ff14b8c6 Mon Sep 17 00:00:00 2001 From: Thomas Marmer Date: Wed, 4 Mar 2026 12:54:53 -0500 Subject: [PATCH 01/10] rewrite profiler plugin. add kotlin implementation --- Package.swift | 17 +- devtools/plugins/profiler/core/BUILD | 28 + devtools/plugins/profiler/core/package.json | 10 + .../core/src/__tests__/plugin.test.ts | 2429 +++++++++++++++++ .../src/addProfilerInterceptorsToHooks.ts | 129 + .../plugins/profiler/core/src/constants.ts | 25 + .../src/helpers/__tests__/profiler.test.ts | 104 + .../profiler/core/src/helpers/index.ts | 1 + .../profiler/core/src/helpers/profiler.ts | 113 + devtools/plugins/profiler/core/src/index.ts | 1 + devtools/plugins/profiler/core/src/plugin.ts | 132 + devtools/plugins/profiler/core/src/types.ts | 28 + devtools/plugins/profiler/ios/BUILD | 10 + .../ios/Sources/ProfilerDevtoolsPlugin.swift | 9 + .../Tests/ProfilerDevtoolsPluginTests.swift | 8 + devtools/plugins/profiler/jvm/BUILD | 26 + .../profiler/ProfilerDevtoolsPlugin.kt | 50 + .../profiler/ProfilerDevtoolsPluginTest.kt | 25 + devtools/plugins/profiler/react/BUILD | 25 + devtools/plugins/profiler/react/package.json | 10 + devtools/plugins/profiler/react/src/index.ts | 17 + ios/demo/BUILD | 1 + pnpm-lock.yaml | 24 + pnpm-workspace.yaml | 2 + 24 files changed, 3223 insertions(+), 1 deletion(-) create mode 100644 devtools/plugins/profiler/core/BUILD create mode 100644 devtools/plugins/profiler/core/package.json create mode 100644 devtools/plugins/profiler/core/src/__tests__/plugin.test.ts create mode 100644 devtools/plugins/profiler/core/src/addProfilerInterceptorsToHooks.ts create mode 100644 devtools/plugins/profiler/core/src/constants.ts create mode 100644 devtools/plugins/profiler/core/src/helpers/__tests__/profiler.test.ts create mode 100644 devtools/plugins/profiler/core/src/helpers/index.ts create mode 100644 devtools/plugins/profiler/core/src/helpers/profiler.ts create mode 100644 devtools/plugins/profiler/core/src/index.ts create mode 100644 devtools/plugins/profiler/core/src/plugin.ts create mode 100644 devtools/plugins/profiler/core/src/types.ts create mode 100644 devtools/plugins/profiler/ios/BUILD create mode 100644 devtools/plugins/profiler/ios/Sources/ProfilerDevtoolsPlugin.swift create mode 100644 devtools/plugins/profiler/ios/Tests/ProfilerDevtoolsPluginTests.swift create mode 100644 devtools/plugins/profiler/jvm/BUILD create mode 100644 devtools/plugins/profiler/jvm/src/main/kotlin/com/intuit/playerui/plugins/devtools/profiler/ProfilerDevtoolsPlugin.kt create mode 100644 devtools/plugins/profiler/jvm/src/test/kotlin/com/intuit/playerui/plugins/devtools/profiler/ProfilerDevtoolsPluginTest.kt create mode 100644 devtools/plugins/profiler/react/BUILD create mode 100644 devtools/plugins/profiler/react/package.json create mode 100644 devtools/plugins/profiler/react/src/index.ts diff --git a/Package.swift b/Package.swift index c604b81..4037add 100644 --- a/Package.swift +++ b/Package.swift @@ -25,6 +25,17 @@ let messengerPlugin: Target = .target( resources: [.process("Resources")] ) +let profilerPlugin: Target = .target( + name: "PlayerUIDevToolsProfilerDevtoolsPlugin", + dependencies: [ + playerUIDependency, + playerUISwiftUIDependency + ], + path: "devtools/plugins/profiler/ios", + exclude: excluded, + resources: [.process("Resources")] +) + // --- END DECLARATIONS --- // This is the Package.swift for our SPM release. @@ -42,11 +53,15 @@ let package = Package( .library( name: messengerPlugin.name, targets: [messengerPlugin.name] + ), + .library( + name: profilerPlugin.name, + targets: [profilerPlugin.name] ) ], dependencies: [ .package(url: "https://github.com/player-ui/playerui-swift-package.git", from: "0.11.2"), .package(url:"https://github.com/chiragramani/SwiftFlipper.git", from: "0.1.0"), ], - targets: [messengerPlugin] + targets: [messengerPlugin, profilerPlugin] ) diff --git a/devtools/plugins/profiler/core/BUILD b/devtools/plugins/profiler/core/BUILD new file mode 100644 index 0000000..8e6b237 --- /dev/null +++ b/devtools/plugins/profiler/core/BUILD @@ -0,0 +1,28 @@ +load("@npm//:defs.bzl", "npm_link_all_packages") +load("@rules_player//javascript:defs.bzl", "js_pipeline") +load("//helpers:defs.bzl", "NATIVE_BUILD_DEPS", "tsup_config", "vitest_config") + +npm_link_all_packages(name = "node_modules") + +tsup_config(name = "tsup_config") + +vitest_config(name = "vitest_config") + +js_pipeline( + package_name = "@player-devtools/profiler-plugin", + build_deps = NATIVE_BUILD_DEPS, + native_bundle = "ProfilerDevtoolsPlugin", + deps = [ + ":node_modules/@player-devtools/messenger", + ":node_modules/@player-devtools/plugin", + ":node_modules/@player-devtools/types", + "//:node_modules/@devtools-ui/plugin", + "//:node_modules/@player-ui/player", + "//:node_modules/@types/uuid", + "//:node_modules/dequal", + "//:node_modules/dset", + "//:node_modules/immer", + "//:node_modules/uuid", + "//:node_modules/tapable-ts", + ], +) diff --git a/devtools/plugins/profiler/core/package.json b/devtools/plugins/profiler/core/package.json new file mode 100644 index 0000000..067a1d1 --- /dev/null +++ b/devtools/plugins/profiler/core/package.json @@ -0,0 +1,10 @@ +{ + "name": "@player-devtools/profiler-plugin", + "version": "0.0.0-PLACEHOLDER", + "main": "src/index.ts", + "dependencies": { + "@player-devtools/messenger": "workspace:*", + "@player-devtools/plugin": "workspace:*", + "@player-devtools/types": "workspace:*" + } +} diff --git a/devtools/plugins/profiler/core/src/__tests__/plugin.test.ts b/devtools/plugins/profiler/core/src/__tests__/plugin.test.ts new file mode 100644 index 0000000..f6c5b44 --- /dev/null +++ b/devtools/plugins/profiler/core/src/__tests__/plugin.test.ts @@ -0,0 +1,2429 @@ +import { Flow, InProgressState, Player } from "@player-ui/player"; +import { describe, expect, test, vi } from "vitest"; +import { ProfilerDevtoolsPlugin } from "../plugin"; + +// mock performance.now +let count = 2490.0; +const now = vi.fn(() => { + count += 0.1; + return count; +}); +global.performance = { ...global.performance, now }; + +describe("Plugin", () => { + // This test is being used to setup a baseline snapshot of perf on a basic player flow. + test("should profile player hooks when navigating through a flow", async () => { + const profilerPlugin = new ProfilerDevtoolsPlugin({ + handler: { + checkIfDevtoolsIsActive: () => true, + processInteraction: () => {}, + }, + playerID: "ID", + }); + + const player = new Player({ plugins: [profilerPlugin] }); + profilerPlugin.processInteraction({ + payload: { + type: "start-profiling", + }, + type: "PLAYER_DEVTOOLS_PLUGIN_INTERACTION", + }); + + // This flow is used to navigate through common player steps. + const flow: Flow = { + id: "flow", + views: [ + { + id: "view1", + type: "foo", + value: "bar", + }, + ], + navigation: { + BEGIN: "FLOW_1", + FLOW_1: { + startState: "VIEW_1", + VIEW_1: { + state_type: "VIEW", + ref: "view1", + transitions: { + "*": "ACTION_1", + }, + }, + ACTION_1: { + state_type: "ACTION", + exp: "{{a}} = 1", + transitions: { + "*": "END_DONE", + }, + }, + END_DONE: { + state_type: "END", + outcome: "done", + }, + }, + }, + }; + const playerPromise = player.start(flow); + + // Wait for first view update to complete. + await vi.waitFor(() => { + const playerState = player.getState(); + expect(playerState.status).toBe("in-progress"); + expect( + (playerState as InProgressState).controllers.view.currentView + ?.lastUpdate + ).toBeDefined(); + }); + + // Transition to action state + (player.getState() as InProgressState).controllers.flow.transition("go"); + + // Wait for action state to transition to end state and complete + await playerPromise; + profilerPlugin.processInteraction({ + payload: { + type: "stop-profiling", + }, + type: "PLAYER_DEVTOOLS_PLUGIN_INTERACTION", + }); + + const storeState = profilerPlugin.store.getState(); + expect(storeState.plugins["player-ui-profiler-plugin"]?.flow.data) + .toMatchInlineSnapshot(` + { + "displayFlameGraph": true, + "durations": [ + { + "duration": "6.0000 ms", + "name": "afterTransition", + }, + { + "duration": "4.3000 ms", + "name": "transition", + }, + { + "duration": "0.4000 ms", + "name": "skipTransition", + }, + { + "duration": "0.4000 ms", + "name": "beforeTransition", + }, + { + "duration": "0.4000 ms", + "name": "resolveTransitionNode", + }, + { + "duration": "0.4000 ms", + "name": "transition", + }, + { + "duration": "0.3000 ms", + "name": "view", + }, + { + "duration": "0.3000 ms", + "name": "resolveDefaultValue", + }, + { + "duration": "0.2000 ms", + "name": "skipTransition", + }, + { + "duration": "0.2000 ms", + "name": "beforeTransition", + }, + { + "duration": "0.2000 ms", + "name": "resolveTransitionNode", + }, + { + "duration": "0.2000 ms", + "name": "transition", + }, + { + "duration": "0.2000 ms", + "name": "resolve", + }, + { + "duration": "0.1000 ms", + "name": "state", + }, + { + "duration": "0.1000 ms", + "name": "resolveFlowContent", + }, + { + "duration": "0.1000 ms", + "name": "onStart", + }, + { + "duration": "0.1000 ms", + "name": "flowController", + }, + { + "duration": "0.1000 ms", + "name": "bindingParser", + }, + { + "duration": "0.1000 ms", + "name": "schema", + }, + { + "duration": "0.1000 ms", + "name": "validationController", + }, + { + "duration": "0.1000 ms", + "name": "expressionEvaluator", + }, + { + "duration": "0.1000 ms", + "name": "dataController", + }, + { + "duration": "0.1000 ms", + "name": "viewController", + }, + { + "duration": "0.1000 ms", + "name": "state", + }, + { + "duration": "0.1000 ms", + "name": "flow", + }, + { + "duration": "0.1000 ms", + "name": "beforeStart", + }, + { + "duration": "0.1000 ms", + "name": "resolveTransitionNode", + }, + { + "duration": "0.1000 ms", + "name": "resolveView", + }, + { + "duration": "0.1000 ms", + "name": "onTemplatePluginCreated", + }, + { + "duration": "0.1000 ms", + "name": "templatePlugin", + }, + { + "duration": "0.1000 ms", + "name": "parser", + }, + { + "duration": "0.1000 ms", + "name": "parseNode", + }, + { + "duration": "0.1000 ms", + "name": "onParseObject", + }, + { + "duration": "0.1000 ms", + "name": "parseNode", + }, + { + "duration": "0.1000 ms", + "name": "parseNode", + }, + { + "duration": "0.1000 ms", + "name": "parseNode", + }, + { + "duration": "0.1000 ms", + "name": "onCreateASTNode", + }, + { + "duration": "0.1000 ms", + "name": "resolver", + }, + { + "duration": "0.1000 ms", + "name": "beforeUpdate", + }, + { + "duration": "0.1000 ms", + "name": "resolveOptions", + }, + { + "duration": "0.1000 ms", + "name": "skipResolve", + }, + { + "duration": "0.1000 ms", + "name": "beforeResolve", + }, + { + "duration": "0.1000 ms", + "name": "resolve", + }, + { + "duration": "0.1000 ms", + "name": "afterResolve", + }, + { + "duration": "0.1000 ms", + "name": "afterNodeUpdate", + }, + { + "duration": "0.1000 ms", + "name": "afterUpdate", + }, + { + "duration": "0.1000 ms", + "name": "onUpdate", + }, + { + "duration": "0.1000 ms", + "name": "afterTransition", + }, + { + "duration": "0.1000 ms", + "name": "resolveOptions", + }, + { + "duration": "0.1000 ms", + "name": "beforeEvaluate", + }, + { + "duration": "0.1000 ms", + "name": "skipOptimization", + }, + { + "duration": "0.1000 ms", + "name": "resolveDataStages", + }, + { + "duration": "0.1000 ms", + "name": "resolveTypeForBinding", + }, + { + "duration": "0.1000 ms", + "name": "onGet", + }, + { + "duration": "0.1000 ms", + "name": "onSet", + }, + { + "duration": "0.1000 ms", + "name": "onUpdate", + }, + { + "duration": "0.1000 ms", + "name": "onGet", + }, + { + "duration": "0.1000 ms", + "name": "serialize", + }, + { + "duration": "0.1000 ms", + "name": "state", + }, + { + "duration": "0.0100 ms", + "name": "afterTransition", + }, + { + "duration": "0.0100 ms", + "name": "skipTransition", + }, + { + "duration": "0.0100 ms", + "name": "beforeTransition", + }, + { + "duration": "0.0100 ms", + "name": "resolveTransitionNode", + }, + { + "duration": "0.0100 ms", + "name": "transition", + }, + { + "duration": "0.0100 ms", + "name": "resolve", + }, + { + "duration": "0.0100 ms", + "name": "skipTransition", + }, + { + "duration": "0.0100 ms", + "name": "skipTransition", + }, + { + "duration": "0.0100 ms", + "name": "skipTransition", + }, + { + "duration": "0.0100 ms", + "name": "beforeTransition", + }, + { + "duration": "0.0100 ms", + "name": "beforeTransition", + }, + { + "duration": "0.0100 ms", + "name": "beforeTransition", + }, + { + "duration": "0.0100 ms", + "name": "resolveTransitionNode", + }, + { + "duration": "0.0100 ms", + "name": "resolveTransitionNode", + }, + { + "duration": "0.0100 ms", + "name": "resolveTransitionNode", + }, + { + "duration": "0.0100 ms", + "name": "transition", + }, + { + "duration": "0.0100 ms", + "name": "transition", + }, + { + "duration": "0.0100 ms", + "name": "transition", + }, + { + "duration": "0.0100 ms", + "name": "afterTransition", + }, + { + "duration": "0.0100 ms", + "name": "afterTransition", + }, + { + "duration": "0.0100 ms", + "name": "afterTransition", + }, + { + "duration": "0.0100 ms", + "name": "afterTransition", + }, + { + "duration": "0.0100 ms", + "name": "afterTransition", + }, + { + "duration": "0.0100 ms", + "name": "afterTransition", + }, + { + "duration": "0.0100 ms", + "name": "afterTransition", + }, + { + "duration": "0.0100 ms", + "name": "afterTransition", + }, + { + "duration": "0.0100 ms", + "name": "afterTransition", + }, + { + "duration": "0.0100 ms", + "name": "afterTransition", + }, + { + "duration": "0.0100 ms", + "name": "afterTransition", + }, + { + "duration": "0.0100 ms", + "name": "afterTransition", + }, + { + "duration": "0.0100 ms", + "name": "afterTransition", + }, + { + "duration": "0.0100 ms", + "name": "afterTransition", + }, + { + "duration": "0.0100 ms", + "name": "afterTransition", + }, + ], + "profiling": false, + "rootNode": { + "children": [ + { + "children": [], + "endTime": 2490.2999999999997, + "name": "state", + "startTime": 2490.2, + "tooltip": "state, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2490.4999999999995, + "name": "resolveFlowContent", + "startTime": 2490.3999999999996, + "tooltip": "resolveFlowContent, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2490.6999999999994, + "name": "onStart", + "startTime": 2490.5999999999995, + "tooltip": "onStart, 0.1000 (ms)", + "value": 100, + }, + { + "children": [ + { + "children": [ + { + "children": [], + "endTime": 2492.6999999999975, + "name": "beforeStart", + "startTime": 2492.5999999999976, + "tooltip": "beforeStart, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2492.8999999999974, + "name": "resolveTransitionNode", + "startTime": 2492.7999999999975, + "tooltip": "resolveTransitionNode, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2497.2999999999934, + "name": "transition", + "startTime": 2492.9999999999973, + "tooltip": "transition, 4.3000 (ms)", + "value": 4300, + }, + { + "children": [ + { + "children": [ + { + "children": [], + "endTime": 2502.3999999999887, + "name": "skipTransition", + "startTime": undefined, + "tooltip": "skipTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2503.199999999988, + "name": "beforeTransition", + "startTime": undefined, + "tooltip": "beforeTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2503.9999999999873, + "name": "resolveTransitionNode", + "startTime": undefined, + "tooltip": "resolveTransitionNode, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2504.7999999999865, + "name": "transition", + "startTime": undefined, + "tooltip": "transition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [ + { + "children": [], + "endTime": 2505.9999999999854, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2506.7999999999847, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + ], + "endTime": 2505.599999999986, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2505.7999999999856, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [ + { + "children": [], + "endTime": 2505.9999999999854, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2506.7999999999847, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + ], + "endTime": 2506.399999999985, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2506.599999999985, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + ], + "endTime": 2497.599999999993, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2497.9999999999927, + "name": "skipTransition", + "startTime": undefined, + "tooltip": "skipTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2498.3999999999924, + "name": "beforeTransition", + "startTime": undefined, + "tooltip": "beforeTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2498.799999999992, + "name": "resolveTransitionNode", + "startTime": undefined, + "tooltip": "resolveTransitionNode, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2499.1999999999916, + "name": "transition", + "startTime": undefined, + "tooltip": "transition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2502.199999999989, + "name": "skipTransition", + "startTime": undefined, + "tooltip": "skipTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2502.299999999989, + "name": "skipTransition", + "startTime": undefined, + "tooltip": "skipTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2502.999999999988, + "name": "beforeTransition", + "startTime": undefined, + "tooltip": "beforeTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2503.099999999988, + "name": "beforeTransition", + "startTime": undefined, + "tooltip": "beforeTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2503.7999999999874, + "name": "resolveTransitionNode", + "startTime": undefined, + "tooltip": "resolveTransitionNode, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2503.8999999999874, + "name": "resolveTransitionNode", + "startTime": undefined, + "tooltip": "resolveTransitionNode, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2504.5999999999867, + "name": "transition", + "startTime": undefined, + "tooltip": "transition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2504.6999999999866, + "name": "transition", + "startTime": undefined, + "tooltip": "transition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [ + { + "children": [], + "endTime": 2502.3999999999887, + "name": "skipTransition", + "startTime": undefined, + "tooltip": "skipTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2503.199999999988, + "name": "beforeTransition", + "startTime": undefined, + "tooltip": "beforeTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2503.9999999999873, + "name": "resolveTransitionNode", + "startTime": undefined, + "tooltip": "resolveTransitionNode, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2504.7999999999865, + "name": "transition", + "startTime": undefined, + "tooltip": "transition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [ + { + "children": [], + "endTime": 2505.9999999999854, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2506.7999999999847, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + ], + "endTime": 2505.599999999986, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2505.7999999999856, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [ + { + "children": [], + "endTime": 2505.9999999999854, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2506.7999999999847, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + ], + "endTime": 2506.399999999985, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2506.599999999985, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + ], + "endTime": 2505.399999999986, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [ + { + "children": [], + "endTime": 2505.8999999999855, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2506.699999999985, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + ], + "endTime": 2505.499999999986, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2505.6999999999857, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [ + { + "children": [], + "endTime": 2502.3999999999887, + "name": "skipTransition", + "startTime": undefined, + "tooltip": "skipTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2503.199999999988, + "name": "beforeTransition", + "startTime": undefined, + "tooltip": "beforeTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2503.9999999999873, + "name": "resolveTransitionNode", + "startTime": undefined, + "tooltip": "resolveTransitionNode, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2504.7999999999865, + "name": "transition", + "startTime": undefined, + "tooltip": "transition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [ + { + "children": [], + "endTime": 2505.9999999999854, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2506.7999999999847, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + ], + "endTime": 2505.599999999986, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2505.7999999999856, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [ + { + "children": [], + "endTime": 2505.9999999999854, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2506.7999999999847, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + ], + "endTime": 2506.399999999985, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2506.599999999985, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + ], + "endTime": 2506.1999999999853, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [ + { + "children": [], + "endTime": 2505.8999999999855, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2506.699999999985, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + ], + "endTime": 2506.299999999985, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2506.499999999985, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + ], + "endTime": 2497.499999999993, + "name": "afterTransition", + "startTime": 2497.3999999999933, + "tooltip": "afterTransition, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2497.899999999993, + "name": "skipTransition", + "startTime": 2497.699999999993, + "tooltip": "skipTransition, 0.2000 (ms)", + "value": 200, + }, + { + "children": [], + "endTime": 2498.2999999999925, + "name": "beforeTransition", + "startTime": 2498.0999999999926, + "tooltip": "beforeTransition, 0.2000 (ms)", + "value": 200, + }, + { + "children": [], + "endTime": 2498.699999999992, + "name": "resolveTransitionNode", + "startTime": 2498.4999999999923, + "tooltip": "resolveTransitionNode, 0.2000 (ms)", + "value": 200, + }, + { + "children": [], + "endTime": 2499.0999999999917, + "name": "transition", + "startTime": 2498.899999999992, + "tooltip": "transition, 0.2000 (ms)", + "value": 200, + }, + { + "children": [], + "endTime": 2502.099999999989, + "name": "skipTransition", + "startTime": 2501.6999999999894, + "tooltip": "skipTransition, 0.4000 (ms)", + "value": 400, + }, + { + "children": [], + "endTime": 2502.8999999999883, + "name": "beforeTransition", + "startTime": 2502.4999999999886, + "tooltip": "beforeTransition, 0.4000 (ms)", + "value": 400, + }, + { + "children": [], + "endTime": 2503.6999999999875, + "name": "resolveTransitionNode", + "startTime": 2503.299999999988, + "tooltip": "resolveTransitionNode, 0.4000 (ms)", + "value": 400, + }, + { + "children": [], + "endTime": 2504.499999999987, + "name": "transition", + "startTime": 2504.099999999987, + "tooltip": "transition, 0.4000 (ms)", + "value": 400, + }, + { + "children": [ + { + "children": [ + { + "children": [], + "endTime": 2502.3999999999887, + "name": "skipTransition", + "startTime": undefined, + "tooltip": "skipTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2503.199999999988, + "name": "beforeTransition", + "startTime": undefined, + "tooltip": "beforeTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2503.9999999999873, + "name": "resolveTransitionNode", + "startTime": undefined, + "tooltip": "resolveTransitionNode, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2504.7999999999865, + "name": "transition", + "startTime": undefined, + "tooltip": "transition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [ + { + "children": [], + "endTime": 2505.9999999999854, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2506.7999999999847, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + ], + "endTime": 2505.599999999986, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2505.7999999999856, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [ + { + "children": [], + "endTime": 2505.9999999999854, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2506.7999999999847, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + ], + "endTime": 2506.399999999985, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2506.599999999985, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + ], + "endTime": 2497.599999999993, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2497.9999999999927, + "name": "skipTransition", + "startTime": undefined, + "tooltip": "skipTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2498.3999999999924, + "name": "beforeTransition", + "startTime": undefined, + "tooltip": "beforeTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2498.799999999992, + "name": "resolveTransitionNode", + "startTime": undefined, + "tooltip": "resolveTransitionNode, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2499.1999999999916, + "name": "transition", + "startTime": undefined, + "tooltip": "transition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2502.199999999989, + "name": "skipTransition", + "startTime": undefined, + "tooltip": "skipTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2502.299999999989, + "name": "skipTransition", + "startTime": undefined, + "tooltip": "skipTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2502.999999999988, + "name": "beforeTransition", + "startTime": undefined, + "tooltip": "beforeTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2503.099999999988, + "name": "beforeTransition", + "startTime": undefined, + "tooltip": "beforeTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2503.7999999999874, + "name": "resolveTransitionNode", + "startTime": undefined, + "tooltip": "resolveTransitionNode, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2503.8999999999874, + "name": "resolveTransitionNode", + "startTime": undefined, + "tooltip": "resolveTransitionNode, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2504.5999999999867, + "name": "transition", + "startTime": undefined, + "tooltip": "transition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2504.6999999999866, + "name": "transition", + "startTime": undefined, + "tooltip": "transition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [ + { + "children": [], + "endTime": 2502.3999999999887, + "name": "skipTransition", + "startTime": undefined, + "tooltip": "skipTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2503.199999999988, + "name": "beforeTransition", + "startTime": undefined, + "tooltip": "beforeTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2503.9999999999873, + "name": "resolveTransitionNode", + "startTime": undefined, + "tooltip": "resolveTransitionNode, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2504.7999999999865, + "name": "transition", + "startTime": undefined, + "tooltip": "transition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [ + { + "children": [], + "endTime": 2505.9999999999854, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2506.7999999999847, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + ], + "endTime": 2505.599999999986, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2505.7999999999856, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [ + { + "children": [], + "endTime": 2505.9999999999854, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2506.7999999999847, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + ], + "endTime": 2506.399999999985, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2506.599999999985, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + ], + "endTime": 2505.399999999986, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [ + { + "children": [], + "endTime": 2505.8999999999855, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2506.699999999985, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + ], + "endTime": 2505.499999999986, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2505.6999999999857, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [ + { + "children": [], + "endTime": 2502.3999999999887, + "name": "skipTransition", + "startTime": undefined, + "tooltip": "skipTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2503.199999999988, + "name": "beforeTransition", + "startTime": undefined, + "tooltip": "beforeTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2503.9999999999873, + "name": "resolveTransitionNode", + "startTime": undefined, + "tooltip": "resolveTransitionNode, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2504.7999999999865, + "name": "transition", + "startTime": undefined, + "tooltip": "transition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [ + { + "children": [], + "endTime": 2505.9999999999854, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2506.7999999999847, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + ], + "endTime": 2505.599999999986, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2505.7999999999856, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [ + { + "children": [], + "endTime": 2505.9999999999854, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2506.7999999999847, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + ], + "endTime": 2506.399999999985, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2506.599999999985, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + ], + "endTime": 2506.1999999999853, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [ + { + "children": [], + "endTime": 2505.8999999999855, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2506.699999999985, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + ], + "endTime": 2506.299999999985, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2506.499999999985, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + ], + "endTime": 2505.299999999986, + "name": "afterTransition", + "startTime": 2499.2999999999915, + "tooltip": "afterTransition, 6.0000 (ms)", + "value": 6000, + }, + { + "children": [ + { + "children": [ + { + "children": [], + "endTime": 2502.3999999999887, + "name": "skipTransition", + "startTime": undefined, + "tooltip": "skipTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2503.199999999988, + "name": "beforeTransition", + "startTime": undefined, + "tooltip": "beforeTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2503.9999999999873, + "name": "resolveTransitionNode", + "startTime": undefined, + "tooltip": "resolveTransitionNode, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2504.7999999999865, + "name": "transition", + "startTime": undefined, + "tooltip": "transition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [ + { + "children": [], + "endTime": 2505.9999999999854, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2506.7999999999847, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + ], + "endTime": 2505.599999999986, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2505.7999999999856, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [ + { + "children": [], + "endTime": 2505.9999999999854, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2506.7999999999847, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + ], + "endTime": 2506.399999999985, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2506.599999999985, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + ], + "endTime": 2497.599999999993, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2497.9999999999927, + "name": "skipTransition", + "startTime": undefined, + "tooltip": "skipTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2498.3999999999924, + "name": "beforeTransition", + "startTime": undefined, + "tooltip": "beforeTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2498.799999999992, + "name": "resolveTransitionNode", + "startTime": undefined, + "tooltip": "resolveTransitionNode, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2499.1999999999916, + "name": "transition", + "startTime": undefined, + "tooltip": "transition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2502.199999999989, + "name": "skipTransition", + "startTime": undefined, + "tooltip": "skipTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2502.299999999989, + "name": "skipTransition", + "startTime": undefined, + "tooltip": "skipTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2502.999999999988, + "name": "beforeTransition", + "startTime": undefined, + "tooltip": "beforeTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2503.099999999988, + "name": "beforeTransition", + "startTime": undefined, + "tooltip": "beforeTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2503.7999999999874, + "name": "resolveTransitionNode", + "startTime": undefined, + "tooltip": "resolveTransitionNode, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2503.8999999999874, + "name": "resolveTransitionNode", + "startTime": undefined, + "tooltip": "resolveTransitionNode, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2504.5999999999867, + "name": "transition", + "startTime": undefined, + "tooltip": "transition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2504.6999999999866, + "name": "transition", + "startTime": undefined, + "tooltip": "transition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [ + { + "children": [], + "endTime": 2502.3999999999887, + "name": "skipTransition", + "startTime": undefined, + "tooltip": "skipTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2503.199999999988, + "name": "beforeTransition", + "startTime": undefined, + "tooltip": "beforeTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2503.9999999999873, + "name": "resolveTransitionNode", + "startTime": undefined, + "tooltip": "resolveTransitionNode, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2504.7999999999865, + "name": "transition", + "startTime": undefined, + "tooltip": "transition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [ + { + "children": [], + "endTime": 2505.9999999999854, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2506.7999999999847, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + ], + "endTime": 2505.599999999986, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2505.7999999999856, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [ + { + "children": [], + "endTime": 2505.9999999999854, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2506.7999999999847, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + ], + "endTime": 2506.399999999985, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2506.599999999985, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + ], + "endTime": 2505.399999999986, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [ + { + "children": [], + "endTime": 2505.8999999999855, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2506.699999999985, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + ], + "endTime": 2505.499999999986, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2505.6999999999857, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [ + { + "children": [], + "endTime": 2502.3999999999887, + "name": "skipTransition", + "startTime": undefined, + "tooltip": "skipTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2503.199999999988, + "name": "beforeTransition", + "startTime": undefined, + "tooltip": "beforeTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2503.9999999999873, + "name": "resolveTransitionNode", + "startTime": undefined, + "tooltip": "resolveTransitionNode, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2504.7999999999865, + "name": "transition", + "startTime": undefined, + "tooltip": "transition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [ + { + "children": [], + "endTime": 2505.9999999999854, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2506.7999999999847, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + ], + "endTime": 2505.599999999986, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2505.7999999999856, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [ + { + "children": [], + "endTime": 2505.9999999999854, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2506.7999999999847, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + ], + "endTime": 2506.399999999985, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2506.599999999985, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + ], + "endTime": 2506.1999999999853, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [ + { + "children": [], + "endTime": 2505.8999999999855, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2506.699999999985, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + ], + "endTime": 2506.299999999985, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + { + "children": [], + "endTime": 2506.499999999985, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + ], + "endTime": 2506.0999999999854, + "name": "afterTransition", + "startTime": undefined, + "tooltip": "afterTransition, 0.0100 (ms)", + "value": 10, + }, + ], + "endTime": 2492.4999999999977, + "name": "flow", + "startTime": 2492.399999999998, + "tooltip": "flow, 0.1000 (ms)", + "value": 100, + }, + ], + "endTime": 2490.899999999999, + "name": "flowController", + "startTime": 2490.7999999999993, + "tooltip": "flowController, 0.1000 (ms)", + "value": 100, + }, + { + "children": [ + { + "children": [], + "endTime": 2500.2999999999906, + "name": "skipOptimization", + "startTime": 2500.1999999999907, + "tooltip": "skipOptimization, 0.1000 (ms)", + "value": 100, + }, + ], + "endTime": 2491.099999999999, + "name": "bindingParser", + "startTime": 2490.999999999999, + "tooltip": "bindingParser, 0.1000 (ms)", + "value": 100, + }, + { + "children": [ + { + "children": [], + "endTime": 2500.79999999999, + "name": "resolveTypeForBinding", + "startTime": 2500.6999999999903, + "tooltip": "resolveTypeForBinding, 0.1000 (ms)", + "value": 100, + }, + ], + "endTime": 2491.299999999999, + "name": "schema", + "startTime": 2491.199999999999, + "tooltip": "schema, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2491.4999999999986, + "name": "validationController", + "startTime": 2491.3999999999987, + "tooltip": "validationController, 0.1000 (ms)", + "value": 100, + }, + { + "children": [ + { + "children": [], + "endTime": 2499.5999999999913, + "name": "resolveOptions", + "startTime": 2499.4999999999914, + "tooltip": "resolveOptions, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2499.799999999991, + "name": "beforeEvaluate", + "startTime": 2499.699999999991, + "tooltip": "beforeEvaluate, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2500.099999999991, + "name": "resolve", + "startTime": 2499.899999999991, + "tooltip": "resolve, 0.2000 (ms)", + "value": 200, + }, + { + "children": [], + "endTime": 2501.5999999999894, + "name": "resolve", + "startTime": undefined, + "tooltip": "resolve, 0.0100 (ms)", + "value": 10, + }, + ], + "endTime": 2491.6999999999985, + "name": "expressionEvaluator", + "startTime": 2491.5999999999985, + "tooltip": "expressionEvaluator, 0.1000 (ms)", + "value": 100, + }, + { + "children": [ + { + "children": [], + "endTime": 2500.4999999999905, + "name": "resolveDataStages", + "startTime": 2500.3999999999905, + "tooltip": "resolveDataStages, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2500.89999999999, + "name": "resolveDefaultValue", + "startTime": 2500.5999999999904, + "tooltip": "resolveDefaultValue, 0.3000 (ms)", + "value": 300, + }, + { + "children": [], + "endTime": 2501.09999999999, + "name": "onGet", + "startTime": 2500.99999999999, + "tooltip": "onGet, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2501.2999999999897, + "name": "onSet", + "startTime": 2501.19999999999, + "tooltip": "onSet, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2501.4999999999895, + "name": "onUpdate", + "startTime": 2501.3999999999896, + "tooltip": "onUpdate, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2506.9999999999845, + "name": "onGet", + "startTime": 2506.8999999999846, + "tooltip": "onGet, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2507.1999999999844, + "name": "serialize", + "startTime": 2507.0999999999844, + "tooltip": "serialize, 0.1000 (ms)", + "value": 100, + }, + ], + "endTime": 2491.8999999999983, + "name": "dataController", + "startTime": 2491.7999999999984, + "tooltip": "dataController, 0.1000 (ms)", + "value": 100, + }, + { + "children": [ + { + "children": [], + "endTime": 2493.199999999997, + "name": "resolveView", + "startTime": 2493.099999999997, + "tooltip": "resolveView, 0.1000 (ms)", + "value": 100, + }, + { + "children": [ + { + "children": [], + "endTime": 2493.499999999997, + "name": "onTemplatePluginCreated", + "startTime": 2493.399999999997, + "tooltip": "onTemplatePluginCreated, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2493.7999999999965, + "name": "templatePlugin", + "startTime": 2493.6999999999966, + "tooltip": "templatePlugin, 0.1000 (ms)", + "value": 100, + }, + { + "children": [ + { + "children": [], + "endTime": 2494.199999999996, + "name": "parseNode", + "startTime": 2494.0999999999963, + "tooltip": "parseNode, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2494.399999999996, + "name": "onParseObject", + "startTime": 2494.299999999996, + "tooltip": "onParseObject, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2494.599999999996, + "name": "parseNode", + "startTime": 2494.499999999996, + "tooltip": "parseNode, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2494.7999999999956, + "name": "parseNode", + "startTime": 2494.6999999999957, + "tooltip": "parseNode, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2494.9999999999955, + "name": "parseNode", + "startTime": 2494.8999999999955, + "tooltip": "parseNode, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2495.1999999999953, + "name": "onCreateASTNode", + "startTime": 2495.0999999999954, + "tooltip": "onCreateASTNode, 0.1000 (ms)", + "value": 100, + }, + ], + "endTime": 2493.9999999999964, + "name": "parser", + "startTime": 2493.8999999999965, + "tooltip": "parser, 0.1000 (ms)", + "value": 100, + }, + { + "children": [ + { + "children": [], + "endTime": 2495.599999999995, + "name": "beforeUpdate", + "startTime": 2495.499999999995, + "tooltip": "beforeUpdate, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2495.7999999999947, + "name": "resolveOptions", + "startTime": 2495.699999999995, + "tooltip": "resolveOptions, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2495.9999999999945, + "name": "skipResolve", + "startTime": 2495.8999999999946, + "tooltip": "skipResolve, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2496.1999999999944, + "name": "beforeResolve", + "startTime": 2496.0999999999945, + "tooltip": "beforeResolve, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2496.399999999994, + "name": "resolve", + "startTime": 2496.2999999999943, + "tooltip": "resolve, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2496.599999999994, + "name": "afterResolve", + "startTime": 2496.499999999994, + "tooltip": "afterResolve, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2496.799999999994, + "name": "afterNodeUpdate", + "startTime": 2496.699999999994, + "tooltip": "afterNodeUpdate, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2496.9999999999936, + "name": "afterUpdate", + "startTime": 2496.8999999999937, + "tooltip": "afterUpdate, 0.1000 (ms)", + "value": 100, + }, + ], + "endTime": 2495.399999999995, + "name": "resolver", + "startTime": 2495.299999999995, + "tooltip": "resolver, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2497.1999999999935, + "name": "onUpdate", + "startTime": 2497.0999999999935, + "tooltip": "onUpdate, 0.1000 (ms)", + "value": 100, + }, + ], + "endTime": 2493.5999999999967, + "name": "view", + "startTime": 2493.299999999997, + "tooltip": "view, 0.3000 (ms)", + "value": 300, + }, + ], + "endTime": 2492.099999999998, + "name": "viewController", + "startTime": 2491.999999999998, + "tooltip": "viewController, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2492.299999999998, + "name": "state", + "startTime": 2492.199999999998, + "tooltip": "state, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2507.399999999984, + "name": "state", + "startTime": 2507.2999999999843, + "tooltip": "state, 0.1000 (ms)", + "value": 100, + }, + ], + "endTime": 2507.499999999984, + "name": "root", + "startTime": 2490.1, + "tooltip": "Profiler total time span 17.4000 (ms)", + "value": 1200, + }, + } + `); + }); +}); diff --git a/devtools/plugins/profiler/core/src/addProfilerInterceptorsToHooks.ts b/devtools/plugins/profiler/core/src/addProfilerInterceptorsToHooks.ts new file mode 100644 index 0000000..063f597 --- /dev/null +++ b/devtools/plugins/profiler/core/src/addProfilerInterceptorsToHooks.ts @@ -0,0 +1,129 @@ +import { + AsyncParallelBailHook, + AsyncParallelHook, + AsyncSeriesBailHook, + AsyncSeriesHook, + AsyncSeriesLoopHook, + AsyncSeriesWaterfallHook, + SyncBailHook, + SyncHook, + SyncLoopHook, + SyncWaterfallHook, +} from "tapable-ts"; +import { Profiler, ProfilerNode } from "./types"; + +/* Paths to hooks to ignore. + * Currently ignoring "view" hook on player since it acts as a shortcut to the viewController's view hook. Including it would duplicate a lot of profiling work. + */ +const IGNORED_PATHS = [["view"]]; + +// Would love to just check if things are `Hook` but tapable-ts doesn't export the base class ;-; +type AnyHook = + | AsyncParallelBailHook + | AsyncParallelHook + | AsyncSeriesBailHook + | AsyncSeriesHook + | AsyncSeriesLoopHook + | AsyncSeriesWaterfallHook + | SyncBailHook + | SyncHook + | SyncLoopHook + | SyncWaterfallHook; + +const isAnyHook = (obj: unknown): obj is AnyHook => { + return ( + obj instanceof AsyncParallelBailHook || + obj instanceof AsyncParallelHook || + obj instanceof AsyncSeriesBailHook || + obj instanceof AsyncSeriesHook || + obj instanceof AsyncSeriesLoopHook || + obj instanceof AsyncSeriesWaterfallHook || + obj instanceof SyncBailHook || + obj instanceof SyncHook || + obj instanceof SyncLoopHook || + obj instanceof SyncWaterfallHook + ); +}; + +/** Recursively add profiler interceptors to each hook in the "hooks" property of obj. */ +export const addProfilerInterceptorsToHooks = ( + obj: unknown, + profiler: Profiler, + getParent?: () => ProfilerNode, + currentPath: string[] = [] +): void => { + if (!hasHooks(obj)) { + return; + } + + const { startTimer, endTimer } = profiler; + + Object.entries(obj.hooks).forEach(([key, value]) => { + const nextPath = [...currentPath, key]; + if ( + !isAnyHook(value) || + IGNORED_PATHS.some((path) => isMatchingPaths(path, nextPath)) + ) { + return; + } + + let profilerNode: ProfilerNode = { + name: key, + children: [], + }; + + /** Since the object reference changing with `endTimer` calls needs to be kept for future parent references, use a function to get it. */ + const getNode = () => profilerNode; + + value.intercept({ + call: (...args) => { + // Might want to also check if `value` is specifically a `SyncHook` since other hooks aren't providing anything with more tapable stuff. + if (args.length > 0) { + addProfilerInterceptorsToHooks(args[0], profiler, getNode, nextPath); + } + + startTimer(key); + }, + done: () => { + profilerNode = endTimer({ + hookName: key, + parentNode: getParent?.(), + children: profilerNode.children, + }); + }, + result: () => { + profilerNode = endTimer({ + hookName: key, + parentNode: getParent?.(), + children: profilerNode.children, + }); + }, + error: () => { + // TODO: Can we mark this as "interrupted" instead of ending the timer as normal? + profilerNode = endTimer({ + hookName: key, + parentNode: getParent?.(), + children: profilerNode.children, + }); + }, + }); + }); +}; + +// TODO: Move all these where they should be +export const isMatchingPaths = (path1: string[], path2: string[]): boolean => { + if (path1.length !== path2.length) return false; + + return path1.every((val, idx) => val === path2[idx]); +}; + +export type ObjectWithHooks = { + hooks: Record; +}; + +export const isRecordType = (obj: unknown): obj is Record => + typeof obj === "object" && obj !== null && !Array.isArray(obj); + +export const hasHooks = (obj: unknown): obj is ObjectWithHooks => { + return isRecordType(obj) && "hooks" in obj && isRecordType(obj.hooks); +}; diff --git a/devtools/plugins/profiler/core/src/constants.ts b/devtools/plugins/profiler/core/src/constants.ts new file mode 100644 index 0000000..88e1cf0 --- /dev/null +++ b/devtools/plugins/profiler/core/src/constants.ts @@ -0,0 +1,25 @@ +import type { PluginData } from "@player-devtools/types"; + +export const PLUGIN_ID = "player-ui-profiler-plugin"; + +export const PLUGIN_NAME = "Player UI Profiler"; + +export const PLUGIN_DESCRIPTION = "Standard Player UI Profiler"; + +export const PLUGIN_VERSION = "__VERSION__"; + +export const VIEWS_IDS = { + PROFILER: "Profiler", +}; + +export const INTERACTIONS = { + START_PROFILING: "start-profiling", + STOP_PROFILING: "stop-profiling", +}; + +export const BASE_PLUGIN_DATA: Omit = { + id: PLUGIN_ID, + name: PLUGIN_NAME, + description: PLUGIN_DESCRIPTION, + version: PLUGIN_VERSION, +}; diff --git a/devtools/plugins/profiler/core/src/helpers/__tests__/profiler.test.ts b/devtools/plugins/profiler/core/src/helpers/__tests__/profiler.test.ts new file mode 100644 index 0000000..b7a47a5 --- /dev/null +++ b/devtools/plugins/profiler/core/src/helpers/__tests__/profiler.test.ts @@ -0,0 +1,104 @@ +import { describe, expect, test, vi } from "vitest"; +import { profiler } from "../profiler"; +import { ProfilerNode } from "../../types"; + +// mock performance.now +let count = 2490.0; +const now = vi.fn(() => { + count += 0.1; + return count; +}); +global.performance = { ...global.performance, now }; + +describe("Profiler", () => { + test("starts the profiler, keep track of the events, and return the profiler tree", () => { + const { startTimer, endTimer, stopProfiler, start } = profiler(); + + start(); + + // process with no children + startTimer("process1"); + endTimer({ hookName: "process1" }); + + // process with children + const parentNode: ProfilerNode = { + name: "process2", + children: [], + }; + + startTimer("process2"); + startTimer("process2.1"); + startTimer("process2.2"); + endTimer({ hookName: "process2.1", parentNode }); + endTimer({ hookName: "process2.2", parentNode }); + endTimer({ hookName: "process2", children: parentNode.children }); + + const rootNode = stopProfiler(); + + expect(rootNode).toStrictEqual({ + durations: [ + { name: "process2", duration: "0.5000 ms" }, + { name: "process2.1", duration: "0.2000 ms" }, + { name: "process2.2", duration: "0.2000 ms" }, + { name: "process1", duration: "0.1000 ms" }, + ], + rootNode: { + children: [ + { + children: [], + endTime: 2490.2999999999997, + name: "process1", + startTime: 2490.2, + tooltip: "process1, 0.1000 (ms)", + value: 100, + }, + { + children: [ + { + children: [], + endTime: 2490.6999999999994, + name: "process2.1", + startTime: 2490.4999999999995, + tooltip: "process2.1, 0.2000 (ms)", + value: 200, + }, + { + children: [], + endTime: 2490.7999999999993, + name: "process2.2", + startTime: 2490.5999999999995, + tooltip: "process2.2, 0.2000 (ms)", + value: 200, + }, + ], + endTime: 2490.899999999999, + name: "process2", + startTime: 2490.3999999999996, + tooltip: "process2, 0.5000 (ms)", + value: 500, + }, + ], + endTime: 2490.999999999999, + name: "root", + startTime: 2490.1, + tooltip: "Profiler total time span 0.9000 (ms)", + value: 600, + }, + }); + + // (re)start + start(); + const { rootNode: rootNode2, durations } = stopProfiler(); + + expect(durations).toStrictEqual([]); + + expect(rootNode2).toStrictEqual({ + name: "root", + endTime: 2491.199999999999, + startTime: 2491.099999999999, + tooltip: "Profiler total time span 0.1000 (ms)", + value: 100, + children: [], + }); + }); +}); diff --git a/devtools/plugins/profiler/core/src/helpers/index.ts b/devtools/plugins/profiler/core/src/helpers/index.ts new file mode 100644 index 0000000..f400962 --- /dev/null +++ b/devtools/plugins/profiler/core/src/helpers/index.ts @@ -0,0 +1 @@ +export * from "./profiler"; diff --git a/devtools/plugins/profiler/core/src/helpers/profiler.ts b/devtools/plugins/profiler/core/src/helpers/profiler.ts new file mode 100644 index 0000000..1279124 --- /dev/null +++ b/devtools/plugins/profiler/core/src/helpers/profiler.ts @@ -0,0 +1,113 @@ +import type { Profiler, ProfilerNode } from "../types"; + +const getNowTime = () => (performance ? performance.now() : Date.now()); + +export const profiler = (): Profiler => { + let rootNode: ProfilerNode = { + name: "root", + children: [], + }; + + let record: { [key: string]: number[] } = {}; + let durations: { hookName: string; duration: number }[] = []; + + const start = () => { + rootNode = { + name: "root", + startTime: getNowTime(), + children: [], + }; + record = {}; + durations = []; + }; + + const addNodeToTree = (newNode: ProfilerNode, parentNode: ProfilerNode) => { + parentNode.children.push(newNode); + return newNode; + }; + + const startTimer = (hookName: string) => { + const startTime = getNowTime(); + + if (!record[hookName] || record[hookName].length === 2) { + record[hookName] = []; + record[hookName].push(startTime); + } + }; + + const endTimer = ({ + hookName, + parentNode = rootNode, + children, + }: { + hookName: string; + parentNode?: ProfilerNode; + children?: ProfilerNode[]; + }) => { + let startTime: number | undefined; + let duration: number | undefined; + + const endTime = getNowTime(); + + for (const key in record) { + if (key === hookName && record[key]!.length === 1) { + [startTime] = record[key]!; + duration = endTime - startTime!; + record[key]!.push(endTime); + } + } + + const value = Math.ceil((duration || 0.01) * 1000); + + const newNode: ProfilerNode = { + name: hookName, + startTime, + endTime, + value, + tooltip: `${hookName}, ${(duration || 0.01).toFixed(4)} (ms)`, + children: children ?? [], + }; + + addNodeToTree(newNode, parentNode); + + // Push the hookName and duration into durations array + durations.push({ hookName, duration: duration ? duration : 0.01 }); + + return newNode; + }; + + const stopProfiler = (): { + rootNode: ProfilerNode; + durations: { name: string; duration: string }[]; + } => { + const endTime = getNowTime(); + const totalTime = endTime - (rootNode.startTime ?? 0); + + rootNode.endTime = endTime; + // set the stop profiler value is the sum of its children values + // otherwise the difference of width of the root and the other nodes + // make it impossible to see them into the flame graph + rootNode.value = + rootNode.children.reduce((acc, { value }) => (acc += value ?? 0), 0) || + Math.ceil((totalTime || 0.01) * 1000); + rootNode.tooltip = `Profiler total time span ${totalTime.toFixed(4)} (ms)`; + + // Sort durations array in descending order + durations.sort((a, b) => b.duration - a.duration); + + return { + rootNode, + durations: durations.map(({ hookName, duration }) => ({ + name: hookName, + duration: `${duration.toFixed(4)} ms`, + })), + }; + }; + + return { + start, + startTimer, + endTimer, + stopProfiler, + }; +}; diff --git a/devtools/plugins/profiler/core/src/index.ts b/devtools/plugins/profiler/core/src/index.ts new file mode 100644 index 0000000..39b9a61 --- /dev/null +++ b/devtools/plugins/profiler/core/src/index.ts @@ -0,0 +1 @@ +export * from "./plugin"; diff --git a/devtools/plugins/profiler/core/src/plugin.ts b/devtools/plugins/profiler/core/src/plugin.ts new file mode 100644 index 0000000..1a1c445 --- /dev/null +++ b/devtools/plugins/profiler/core/src/plugin.ts @@ -0,0 +1,132 @@ +import { + DevtoolsPlugin, + genDataChangeTransaction, + type DevtoolsPluginOptions, +} from "@player-devtools/plugin"; +import type { + DevtoolsPluginInteractionEvent, + PluginData, +} from "@player-devtools/types"; +import type { Flow, Player } from "@player-ui/player"; +import { dset } from "dset/merge"; +import { produce } from "immer"; +import { BASE_PLUGIN_DATA, INTERACTIONS } from "./constants"; +import { profiler } from "./helpers"; +import type { Profiler } from "./types"; +import { addProfilerInterceptorsToHooks } from "./addProfilerInterceptorsToHooks"; + +// TODO: Import content +const flow: Flow = {} as Flow; + +const pluginData: PluginData = { + ...BASE_PLUGIN_DATA, + flow: flow as Flow, +}; + +const pluginID = pluginData.id; + +export class ProfilerDevtoolsPlugin extends DevtoolsPlugin { + constructor(options: Omit) { + super({ + ...options, + pluginData, + }); + } + + name = "ProfilerDevtoolsPlugin"; + + startProfiler?: Profiler["start"]; + stopProfiler?: Profiler["stopProfiler"]; + + apply(player: Player): void { + if (!this.checkIfDevtoolsIsActive()) { + return; + } + + super.apply(player); + + const profilerObj = profiler(); + + this.stopProfiler = this.createProfilerStopFunction(profilerObj); + /** function to tap into hooks and start the profiler */ + this.startProfiler = this.createProfileStartFunction(player, profilerObj); + } + + private createProfileStartFunction = ( + player: Player, + profilerObj: Profiler + ): Profiler["start"] => { + const { start } = profilerObj; + + return () => { + start(); + + addProfilerInterceptorsToHooks(player, profilerObj); + + const newState = produce(this.store.getState(), (draft) => { + dset(draft, ["plugins", pluginID, "flow", "data", "rootNode"], { + name: "root", + children: [], + }); + dset(draft, ["plugins", pluginID, "flow", "data", "durations"], []); + dset(draft, ["plugins", pluginID, "flow", "data", "profiling"], true); + dset( + draft, + ["plugins", pluginID, "flow", "data", "displayFlameGraph"], + false + ); + }); + + const transaction = genDataChangeTransaction({ + playerID: this.playerID, + data: newState.plugins[pluginID]?.flow.data, + pluginID, + }); + + this.store.dispatch(transaction); + + this.lastProcessedInteraction += 1; + }; + }; + + private createProfilerStopFunction = ( + profiler: Profiler + ): Profiler["stopProfiler"] => { + return () => { + const { stopProfiler } = profiler; + const stopProfilerResult = stopProfiler(); + const { rootNode, durations } = stopProfilerResult; + + const newState = this.produceState( + [["plugins", pluginID, "flow", "data", "rootNode"], rootNode], + [["plugins", pluginID, "flow", "data", "durations"], durations], + [["plugins", pluginID, "flow", "data", "profiling"], false], + [["plugins", pluginID, "flow", "data", "displayFlameGraph"], true] + ); + + const transaction = genDataChangeTransaction({ + playerID: this.playerID, + data: newState.plugins[pluginID]?.flow.data, + pluginID, + }); + + this.store.dispatch(transaction); + + this.lastProcessedInteraction += 1; + return stopProfilerResult; + }; + }; + + processInteraction(interaction: DevtoolsPluginInteractionEvent): void { + const { + payload: { type }, + } = interaction; + if (type === INTERACTIONS.START_PROFILING && this.startProfiler) { + this.startProfiler(); + } + + if (type === INTERACTIONS.STOP_PROFILING && this.stopProfiler) { + this.stopProfiler(); + } + } +} diff --git a/devtools/plugins/profiler/core/src/types.ts b/devtools/plugins/profiler/core/src/types.ts new file mode 100644 index 0000000..a7077e9 --- /dev/null +++ b/devtools/plugins/profiler/core/src/types.ts @@ -0,0 +1,28 @@ +export interface Profiler { + start(): void; + startTimer(hookName: string): void; + endTimer(args: { + hookName: string; + parentNode?: ProfilerNode; + children?: ProfilerNode[]; + }): ProfilerNode; + stopProfiler(): { + rootNode: ProfilerNode; + durations: { name: string; duration: string }[]; + }; +} + +export type ProfilerNode = { + /** hook name */ + name: string; + /* startTime of the hook */ + startTime?: number; + /** endTime of the hook */ + endTime?: number; + /** duration casted to a positive integer (multiplied by 1000) */ + value?: number; + /** tooltip to be shown on hover */ + tooltip?: string; + /** subhook profiler nodes */ + children: ProfilerNode[]; +}; diff --git a/devtools/plugins/profiler/ios/BUILD b/devtools/plugins/profiler/ios/BUILD new file mode 100644 index 0000000..687b460 --- /dev/null +++ b/devtools/plugins/profiler/ios/BUILD @@ -0,0 +1,10 @@ +load("//helpers:ios.bzl", "ios_library") + +ios_library( + name = "ProfilerDevtoolsPlugin", + resources = ["//devtools/plugins/profiler/core:core_native_bundle"], + deps = [ + "//devtools/types/ios:PlayerUIDevToolsTypes", + "//devtools/utils/ios:PlayerUIDevToolsUtils", + ], +) diff --git a/devtools/plugins/profiler/ios/Sources/ProfilerDevtoolsPlugin.swift b/devtools/plugins/profiler/ios/Sources/ProfilerDevtoolsPlugin.swift new file mode 100644 index 0000000..9f0e665 --- /dev/null +++ b/devtools/plugins/profiler/ios/Sources/ProfilerDevtoolsPlugin.swift @@ -0,0 +1,9 @@ +import Foundation +import PlayerUI +import JavaScriptCore +import PlayerUIDevToolsTypes +import PlayerUIDevToolsUtils + +public class ProfilerDevtoolsPlugin { + +} diff --git a/devtools/plugins/profiler/ios/Tests/ProfilerDevtoolsPluginTests.swift b/devtools/plugins/profiler/ios/Tests/ProfilerDevtoolsPluginTests.swift new file mode 100644 index 0000000..a2ef4ee --- /dev/null +++ b/devtools/plugins/profiler/ios/Tests/ProfilerDevtoolsPluginTests.swift @@ -0,0 +1,8 @@ +import XCTest +import JavaScriptCore +@testable import PlayerUIProfilerDevtoolsPlugin +@preconcurrency import PlayerUIDevToolsTypes + +final class ProfilerDevtoolsPluginTests: XCTestCase { + +} \ No newline at end of file diff --git a/devtools/plugins/profiler/jvm/BUILD b/devtools/plugins/profiler/jvm/BUILD new file mode 100644 index 0000000..db9ee47 --- /dev/null +++ b/devtools/plugins/profiler/jvm/BUILD @@ -0,0 +1,26 @@ +load("@rules_jvm_external//:defs.bzl", "artifact") +load("//helpers:jvm.bzl", "kt_jvm") + +main_exports = [ + "//devtools/plugin/jvm:plugin", +] + +main_deps = main_exports + [] + +main_resources = [ + "//devtools/plugins/profiler/core:core_native_bundle", +] + +test_deps = [ + artifact("com.intuit.playerui:testutils"), + artifact("com.intuit.playerui:j2v8-all"), +] + +kt_jvm( + name = "profiler-plugin", + group = "com.intuit.playerui.plugins.devtools.profiler", + main_deps = main_deps, + main_exports = main_exports, + main_resources = main_resources, + test_deps = test_deps +) diff --git a/devtools/plugins/profiler/jvm/src/main/kotlin/com/intuit/playerui/plugins/devtools/profiler/ProfilerDevtoolsPlugin.kt b/devtools/plugins/profiler/jvm/src/main/kotlin/com/intuit/playerui/plugins/devtools/profiler/ProfilerDevtoolsPlugin.kt new file mode 100644 index 0000000..b37d3e2 --- /dev/null +++ b/devtools/plugins/profiler/jvm/src/main/kotlin/com/intuit/playerui/plugins/devtools/profiler/ProfilerDevtoolsPlugin.kt @@ -0,0 +1,50 @@ +package com.intuit.playerui.plugins.devtools.profiler + +import com.intuit.playerui.core.bridge.Node +import com.intuit.playerui.core.bridge.deserialize +import com.intuit.playerui.core.bridge.runtime.Runtime +import com.intuit.playerui.core.bridge.runtime.add +import com.intuit.playerui.core.bridge.serialization.serializers.NodeWrapperSerializer +import com.intuit.playerui.core.player.PlayerException +import com.intuit.playerui.core.plugins.RuntimePlugin +import com.intuit.playerui.devtools.DevtoolsHandler +import com.intuit.playerui.devtools.DevtoolsPlugin +import com.intuit.playerui.devtools.DevtoolsPluginInteractionEvent +import com.intuit.playerui.devtools.ModuleLoader +import com.intuit.playerui.devtools.PluginData +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient +import java.util.concurrent.atomic.AtomicInteger + +@Serializable(with = ProfilerDevtoolsPlugin.Serializer::class) +public class ProfilerDevtoolsPlugin(node: Node) : DevtoolsPlugin(node) { + + @Serializable + public data class Options( + public val playerID: String, + public val handler: DevtoolsHandler, + ) + + public companion object Module : RuntimePlugin by ModuleLoader( + ProfilerDevtoolsPlugin.NAME, + ProfilerDevtoolsPlugin.BUNDLED_SOURCE_PATH + ) { + private val count = AtomicInteger(0) + public fun Runtime<*>.ProfilerDevtoolsPlugin(options: Options): ProfilerDevtoolsPlugin { + runtime.execute("class WeakRef { value = null; constructor(value) { this.value = value }; deref() { return this.value } }") + + val argsKey = "profilerDevtoolsPluginArgs_${count.getAndIncrement()}" + runtime.add(argsKey, options) + apply(this) + val instance = runtime.execute("(new ${NAME}.${NAME}($argsKey))") as? Node + ?: throw PlayerException("Could not instantiate ProfilerDevtoolsPlugin") + return ProfilerDevtoolsPlugin(instance) + } + + private const val NAME = "ProfilerDevtoolsPlugin" + private const val BUNDLED_SOURCE_PATH = "devtools/plugins/profiler/core/dist/$NAME.native.js" + } + + internal object Serializer : KSerializer by NodeWrapperSerializer(::ProfilerDevtoolsPlugin) +} diff --git a/devtools/plugins/profiler/jvm/src/test/kotlin/com/intuit/playerui/plugins/devtools/profiler/ProfilerDevtoolsPluginTest.kt b/devtools/plugins/profiler/jvm/src/test/kotlin/com/intuit/playerui/plugins/devtools/profiler/ProfilerDevtoolsPluginTest.kt new file mode 100644 index 0000000..6028d96 --- /dev/null +++ b/devtools/plugins/profiler/jvm/src/test/kotlin/com/intuit/playerui/plugins/devtools/profiler/ProfilerDevtoolsPluginTest.kt @@ -0,0 +1,25 @@ +package com.intuit.playerui.plugins.devtools.profiler + +import com.intuit.playerui.devtools.DevtoolsHandler +import com.intuit.playerui.devtools.DevtoolsPluginInteractionEvent +import com.intuit.playerui.plugins.devtools.profiler.ProfilerDevtoolsPlugin.Module.ProfilerDevtoolsPlugin +import com.intuit.playerui.utils.test.RuntimeTest +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +class ProfilerDevtoolsPluginTest : RuntimeTest(), DevtoolsHandler { + + override fun processInteraction(interaction: DevtoolsPluginInteractionEvent) { + TODO("Not yet implemented") + } + + override fun checkIfDevtoolsIsActive(): Boolean { + return true + } + + @Test fun smoke() { + val plugin = runtime.ProfilerDevtoolsPlugin(ProfilerDevtoolsPlugin.Options("test", this)) + assertTrue(plugin.checkIfDevtoolsIsActive()) + plugin.store.getState().node + } +} diff --git a/devtools/plugins/profiler/react/BUILD b/devtools/plugins/profiler/react/BUILD new file mode 100644 index 0000000..5f89d03 --- /dev/null +++ b/devtools/plugins/profiler/react/BUILD @@ -0,0 +1,25 @@ +load("@npm//:defs.bzl", "npm_link_all_packages") +load("@rules_player//javascript:defs.bzl", "js_pipeline") +load("//helpers:defs.bzl", "tsup_config", "vitest_config") + +npm_link_all_packages(name = "node_modules") + +tsup_config(name = "tsup_config") + +vitest_config(name = "vitest_config") + +js_pipeline( + package_name = "@player-devtools/profiler-plugin-react", + deps = [ + ":node_modules/@player-devtools/plugin-react", + ":node_modules/@player-devtools/profiler-plugin", + ":node_modules/@player-devtools/types", + "//:node_modules/@devtools-ui/plugin", + "//:node_modules/@player-ui/react", + "//:node_modules/@types/uuid", + "//:node_modules/dequal", + "//:node_modules/dset", + "//:node_modules/immer", + "//:node_modules/uuid", + ], +) \ No newline at end of file diff --git a/devtools/plugins/profiler/react/package.json b/devtools/plugins/profiler/react/package.json new file mode 100644 index 0000000..a99233e --- /dev/null +++ b/devtools/plugins/profiler/react/package.json @@ -0,0 +1,10 @@ +{ + "name": "@player-devtools/profiler-plugin-react", + "version": "0.0.0-PLACEHOLDER", + "main": "src/index.tsx", + "dependencies": { + "@player-devtools/profiler-plugin": "workspace:*", + "@player-devtools/plugin-react": "workspace:*", + "@player-devtools/types": "workspace:*" + } +} diff --git a/devtools/plugins/profiler/react/src/index.ts b/devtools/plugins/profiler/react/src/index.ts new file mode 100644 index 0000000..9e986f4 --- /dev/null +++ b/devtools/plugins/profiler/react/src/index.ts @@ -0,0 +1,17 @@ +import { ReactDevtoolsPlugin } from "@player-devtools/plugin-react"; +import { ProfilerDevtoolsPlugin } from "@player-devtools/profiler-plugin"; + +export class ProfilerReactDevtoolsPlugin extends ReactDevtoolsPlugin { + name = "ProfilerReactDevtoolsPlugin"; + + corePlugin: ProfilerDevtoolsPlugin; + + constructor(id?: string) { + super(); + + this.corePlugin = new ProfilerDevtoolsPlugin({ + playerID: id ?? "default-id", + handler: this, + }); + } +} diff --git a/ios/demo/BUILD b/ios/demo/BUILD index 0eadd76..7dd1713 100644 --- a/ios/demo/BUILD +++ b/ios/demo/BUILD @@ -20,6 +20,7 @@ swift_library( # These are the components under development. "//devtools/plugin/ios:PlayerUIDevtoolsPlugins", "//devtools/plugins/basic/swiftui:PlayerUIDevtoolsBasicPlugin", + "//devtools/plugins/profiler/ios:PlayerUIDevToolsProfilerDevtoolsPlugin", ] ) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e0b86a5..4e862d9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -408,6 +408,30 @@ importers: specifier: workspace:* version: link:../../../types/core + devtools/plugins/profiler/core: + dependencies: + '@player-devtools/messenger': + specifier: workspace:* + version: link:../../../messenger/core + '@player-devtools/plugin': + specifier: workspace:* + version: link:../../../plugin/core + '@player-devtools/types': + specifier: workspace:* + version: link:../../../types/core + + devtools/plugins/profiler/react: + dependencies: + '@player-devtools/plugin-react': + specifier: workspace:* + version: link:../../../plugin/react + '@player-devtools/profiler-plugin': + specifier: workspace:* + version: link:../core + '@player-devtools/types': + specifier: workspace:* + version: link:../../../types/core + devtools/types/core: {} devtools/utils/ios/Resources/core: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index e5403ec..c0d7e77 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -6,6 +6,8 @@ packages: - devtools/plugin/react - devtools/plugins/basic/core - devtools/plugins/basic/react + - devtools/plugins/profiler/core + - devtools/plugins/profiler/react - devtools/messenger/core - devtools/utils/ios/Resources/core - mocks/ From fb9cd94f6a5eb86bc6cc32fa4747f2a7464a447f Mon Sep 17 00:00:00 2001 From: Thomas Marmer Date: Tue, 21 Apr 2026 13:32:11 -0400 Subject: [PATCH 02/10] fix build issues for profiler plugin --- devtools/plugin/core/src/helpers/uuid.ts | 8 +- devtools/plugin/core/src/plugin.ts | 2 +- .../profiler/core/src/helpers/profiler.ts | 4 +- .../profiler/core/src/plugin-flow.json | 317 ++++++++++++++++++ devtools/plugins/profiler/core/src/plugin.ts | 22 +- devtools/plugins/profiler/ios/BUILD | 5 +- .../ios/Sources/ProfilerDevtoolsPlugin.swift | 96 +++++- ios/BUILD | 1 + ios/demo/BUILD | 2 +- ios/demo/Sources/DemoApp.swift | 7 +- 10 files changed, 438 insertions(+), 26 deletions(-) create mode 100755 devtools/plugins/profiler/core/src/plugin-flow.json diff --git a/devtools/plugin/core/src/helpers/uuid.ts b/devtools/plugin/core/src/helpers/uuid.ts index 6a6bf3e..497467b 100644 --- a/devtools/plugin/core/src/helpers/uuid.ts +++ b/devtools/plugin/core/src/helpers/uuid.ts @@ -1,12 +1,10 @@ +const getNowTime = globalThis.performance ? performance.now : Date.now; + // TODO: Either polyfill crypto or use this (pulled from SO) export function generateUUID(): string { // Public Domain/MIT let d = new Date().getTime(); //Timestamp - let d2 = - (typeof performance !== "undefined" && - performance.now && - performance.now() * 1000) || - 0; //Time in microseconds since page-load or 0 if unsupported + let d2 = getNowTime() * 1000; return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) { let r = Math.random() * 16; //random number between 0 and 16 if (d > 0) { diff --git a/devtools/plugin/core/src/plugin.ts b/devtools/plugin/core/src/plugin.ts index 234255a..7a0cca2 100644 --- a/devtools/plugin/core/src/plugin.ts +++ b/devtools/plugin/core/src/plugin.ts @@ -83,7 +83,7 @@ export class DevtoolsPlugin implements PlayerPlugin, DevtoolsHandler { } registerMessenger( - messenger: Messenger, + messenger: Messenger ): Unsubscribe { // Propagate new messages from state to devtools via the messenger let lastMessageIndex = -1; diff --git a/devtools/plugins/profiler/core/src/helpers/profiler.ts b/devtools/plugins/profiler/core/src/helpers/profiler.ts index 1279124..b8e4a88 100644 --- a/devtools/plugins/profiler/core/src/helpers/profiler.ts +++ b/devtools/plugins/profiler/core/src/helpers/profiler.ts @@ -1,6 +1,8 @@ import type { Profiler, ProfilerNode } from "../types"; -const getNowTime = () => (performance ? performance.now() : Date.now()); +const getNowTime = globalThis.performance + ? () => globalThis.performance.now() + : () => Date.now(); export const profiler = (): Profiler => { let rootNode: ProfilerNode = { diff --git a/devtools/plugins/profiler/core/src/plugin-flow.json b/devtools/plugins/profiler/core/src/plugin-flow.json new file mode 100755 index 0000000..6e182f7 --- /dev/null +++ b/devtools/plugins/profiler/core/src/plugin-flow.json @@ -0,0 +1,317 @@ +{ + "id": "player-ui-profiler-devtools-plugin", + "views": [ + { + "id": "Profile", + "type": "stacked-view", + "header": { + "asset": { + "id": "Profile-header", + "type": "navigation", + "values": [ + { + "asset": { + "id": "Profile-header-values-0", + "type": "action", + "value": "Profile", + "label": { + "asset": { + "id": "Profile-header-values-0-label", + "type": "text", + "value": "Profile" + } + } + } + } + ] + } + }, + "main": { + "asset": { + "id": "Profile-main", + "type": "object-inspector", + "binding": "durations", + "label": { + "asset": { + "id": "Profile-main-label", + "type": "text", + "value": "Profile" + } + } + } + }, + "footer": { + "asset": { + "id": "Editor-header-values-0", + "type": "action", + "exp": "conditional({{profiling}} === true, publish('stop-profiling'), publish('start-profiling'))", + "label": { + "asset": { + "id": "Editor-header-values-0-label", + "type": "text", + "value": "@[conditional({{profiling}} === true, 'Stop', 'Start')]@" + } + } + } + } + } + ], + "navigation": { + "BEGIN": "Plugin", + "Plugin": { + "startState": "PROFILE", + "PROFILE": { + "state_type": "VIEW", + "ref": "Profile", + "transitions": { + "Profile": "PROFILE" + } + } + } + }, + "schema": { + "ROOT": { + "playerConfig": { + "type": "RecordType" + }, + "flow": { + "type": "RecordType" + }, + "expression": { + "type": "StringType", + "default": "", + "validation": [ + { + "type": "string" + } + ], + "format": { + "type": "string" + } + }, + "code": { + "type": "StringType", + "default": "", + "validation": [ + { + "type": "string" + } + ], + "format": { + "type": "string" + } + }, + "history": { + "type": "historyType", + "isArray": true + }, + "logs": { + "type": "logsType", + "isArray": true + } + }, + "logsType": { + "id": { + "type": "StringType", + "default": "", + "validation": [ + { + "type": "string" + } + ], + "format": { + "type": "string" + } + }, + "time": { + "type": "StringType", + "default": "", + "validation": [ + { + "type": "string" + } + ], + "format": { + "type": "string" + } + }, + "type": { + "type": "StringType", + "default": "", + "validation": [ + { + "type": "string" + } + ], + "format": { + "type": "string" + } + }, + "message": { + "type": "StringType", + "default": "", + "validation": [ + { + "type": "string" + } + ], + "format": { + "type": "string" + } + }, + "severity": { + "type": "StringType", + "default": "", + "validation": [ + { + "type": "string" + } + ], + "format": { + "type": "string" + } + }, + "binding": { + "type": "StringType", + "default": "", + "validation": [ + { + "type": "string" + } + ], + "format": { + "type": "string" + } + }, + "from": { + "type": "StringType", + "default": "", + "validation": [ + { + "type": "string" + } + ], + "format": { + "type": "string" + } + }, + "to": { + "type": "StringType", + "default": "", + "validation": [ + { + "type": "string" + } + ], + "format": { + "type": "string" + } + }, + "state": { + "type": "StringType", + "default": "", + "validation": [ + { + "type": "string" + } + ], + "format": { + "type": "string" + } + }, + "error": { + "type": "StringType", + "default": "", + "validation": [ + { + "type": "string" + } + ], + "format": { + "type": "string" + } + }, + "outcome": { + "type": "StringType", + "default": "", + "validation": [ + { + "type": "string" + } + ], + "format": { + "type": "string" + } + }, + "metricType": { + "type": "StringType", + "default": "", + "validation": [ + { + "type": "string" + } + ], + "format": { + "type": "string" + } + } + }, + "historyType": { + "id": { + "type": "StringType", + "default": "", + "validation": [ + { + "type": "string" + } + ], + "format": { + "type": "string" + } + }, + "expression": { + "type": "StringType", + "default": "", + "validation": [ + { + "type": "string" + } + ], + "format": { + "type": "string" + } + }, + "result": { + "type": "StringType", + "default": "", + "validation": [ + { + "type": "string" + } + ], + "format": { + "type": "string" + } + }, + "severity": { + "type": "StringType", + "default": "", + "validation": [ + { + "type": "string" + } + ], + "format": { + "type": "string" + } + } + } + }, + "data": { + "expression": "", + "flow": {}, + "history": [], + "logs": [], + "playerConfig": {} + } +} diff --git a/devtools/plugins/profiler/core/src/plugin.ts b/devtools/plugins/profiler/core/src/plugin.ts index 1a1c445..0050ac3 100644 --- a/devtools/plugins/profiler/core/src/plugin.ts +++ b/devtools/plugins/profiler/core/src/plugin.ts @@ -14,9 +14,7 @@ import { BASE_PLUGIN_DATA, INTERACTIONS } from "./constants"; import { profiler } from "./helpers"; import type { Profiler } from "./types"; import { addProfilerInterceptorsToHooks } from "./addProfilerInterceptorsToHooks"; - -// TODO: Import content -const flow: Flow = {} as Flow; +import flow from "./plugin-flow.json"; const pluginData: PluginData = { ...BASE_PLUGIN_DATA, @@ -47,19 +45,22 @@ export class ProfilerDevtoolsPlugin extends DevtoolsPlugin { const profilerObj = profiler(); - this.stopProfiler = this.createProfilerStopFunction(profilerObj); + this.stopProfiler = this.createProfilerStopFunction(player, profilerObj); /** function to tap into hooks and start the profiler */ this.startProfiler = this.createProfileStartFunction(player, profilerObj); + this.startProfiler(); } private createProfileStartFunction = ( player: Player, profilerObj: Profiler ): Profiler["start"] => { - const { start } = profilerObj; + const { start, startTimer } = profilerObj; return () => { + player.logger.debug("[ProfilerPlugin]: Starting..."); start(); + startTimer("profiler"); addProfilerInterceptorsToHooks(player, profilerObj); @@ -84,16 +85,17 @@ export class ProfilerDevtoolsPlugin extends DevtoolsPlugin { }); this.store.dispatch(transaction); - - this.lastProcessedInteraction += 1; }; }; private createProfilerStopFunction = ( + player: Player, profiler: Profiler ): Profiler["stopProfiler"] => { return () => { - const { stopProfiler } = profiler; + player.logger.debug("[ProfilerPlugin]: Stopping..."); + const { stopProfiler, endTimer } = profiler; + endTimer({ hookName: "profiler" }); const stopProfilerResult = stopProfiler(); const { rootNode, durations } = stopProfilerResult; @@ -111,13 +113,13 @@ export class ProfilerDevtoolsPlugin extends DevtoolsPlugin { }); this.store.dispatch(transaction); - - this.lastProcessedInteraction += 1; return stopProfilerResult; }; }; processInteraction(interaction: DevtoolsPluginInteractionEvent): void { + super.processInteraction(interaction); + const { payload: { type }, } = interaction; diff --git a/devtools/plugins/profiler/ios/BUILD b/devtools/plugins/profiler/ios/BUILD index 687b460..6dfdbf9 100644 --- a/devtools/plugins/profiler/ios/BUILD +++ b/devtools/plugins/profiler/ios/BUILD @@ -4,7 +4,8 @@ ios_library( name = "ProfilerDevtoolsPlugin", resources = ["//devtools/plugins/profiler/core:core_native_bundle"], deps = [ - "//devtools/types/ios:PlayerUIDevToolsTypes", - "//devtools/utils/ios:PlayerUIDevToolsUtils", + "//devtools/plugin/ios:PlayerUIDevtoolsPlugins", + "//devtools/utils/swiftui:PlayerUIDevtoolsUtilsSwiftUI", + "//devtools/plugin/swiftui:PlayerUIDevtoolsSwiftUIPlugins", ], ) diff --git a/devtools/plugins/profiler/ios/Sources/ProfilerDevtoolsPlugin.swift b/devtools/plugins/profiler/ios/Sources/ProfilerDevtoolsPlugin.swift index 9f0e665..e1eef81 100644 --- a/devtools/plugins/profiler/ios/Sources/ProfilerDevtoolsPlugin.swift +++ b/devtools/plugins/profiler/ios/Sources/ProfilerDevtoolsPlugin.swift @@ -1,9 +1,97 @@ -import Foundation import PlayerUI +import PlayerUILogger +import Foundation +import PlayerUIDevtoolsPlugins +import PlayerUIDevtoolsMessenger +import PlayerUIDevtoolsTypes import JavaScriptCore -import PlayerUIDevToolsTypes -import PlayerUIDevToolsUtils +import PlayerUIDevtoolsUtilsSwiftUI +import PlayerUIDevtoolsSwiftUIPlugins + +public class BaseProfilerDevtoolsPlugin: JSBasePlugin, BaseDevtoolsPlugin { + /// Matches `bundle_name` / `ios_library(name=...)` in `devtools/plugins/profiler/ios/BUILD` (the `apple_resource_bundle` base name). + private static let pluginResourceBundleName = "ProfilerDevtoolsPlugin" + + private let _playerID: String + // This is a var so a different handler can be provided for testing + var handler: DevtoolsHandler = Handler() + + public init(playerID: String) { + self._playerID = playerID + super.init( + fileName: "ProfilerDevtoolsPlugin.native", + pluginName: "ProfilerDevtoolsPlugin.ProfilerDevtoolsPlugin" + ) + } + + public final override func getUrlForFile(fileName: String) -> URL? { + if let url = Bundle.module.url(forResource: fileName, withExtension: "js") { + return url + } + if let bundleURL = Bundle.main.url( + forResource: Self.pluginResourceBundleName, + withExtension: "bundle" + ), + let pluginBundle = Bundle(url: bundleURL), + let url = pluginBundle.url(forResource: fileName, withExtension: "js") { + return url + } + return Bundle.main.url(forResource: fileName, withExtension: "js") + } -public class ProfilerDevtoolsPlugin { + public override func getArguments() -> [Any] { + guard let context else { return [] } + // TODO: replace with proper polyfill plugin after https://github.com/player-ui/player/issues/773 + context.polyfill() + + // PluginData is nil. The core basic plugin provides its own plugin data + let options = DevtoolsPluginOptions(in: context , playerID: _playerID, handler: handler) + return [options.jsCompatible] + } + /// This will process messages. The core plugin augments this handler with some logging and metadata + struct Handler: DevtoolsHandler { + var isActive = true + + // This plugin has no extra steps for processInteraction beyond the core impl. + func processInteraction(interaction: PlayerUIDevtoolsTypes.Message) {} + + // This plugin has no extra steps for log beyond the core impl. + func log(message: String) {} + } +} + +/// A Player Plugin that provides DevTools capabilities via Flipper. +/// This is entirely just a wrapper around the JSBasePlugin +public class ProfilerDevtoolsPlugin: BaseProfilerDevtoolsPlugin, DevtoolsPlugin { + /// Our connection to the flipper server + public let flipperPlugin: DevtoolsFlipperPlugin + /// Keep a reference so the messenger doesn't get garbage collected and destroyed + public var messenger: Messenger? + /// The IDs of all registered listeners associated with this plugin + public var listeners: [UUID] = [] + + public init(id: String, flipperPlugin: DevtoolsFlipperPlugin) { + self.flipperPlugin = flipperPlugin + super.init(playerID: id) + } + + /* Let flipper know that this plugin is going away. Deregister the listeners we + attached to the DevtoolsFlipperPlugin. + + Deinits will NOT run when the app is terminated. But if the app is terminated, + flipper will gracefully handle the abrupt, implicit disconnect, and deregistering + the listeners won't matter anymore since they won't be called if the app is dead. */ + deinit { + // If you make your own DevtoolsPlugin, you will need to implement your own + // deinit, exactly like this. The DevtoolsPlugin protocol cannot provide a deinit, + // unfortunately. + if let messenger { + messenger.destroy() + } else { + print("[DEBUG] Could not destroy messenger. Messenger already no longer exists.") + } + listeners.forEach { flipperPlugin.removeListener(id: $0) } + print("[DEBUG] BasicDevtoolsPlugin deinited") + } } diff --git a/ios/BUILD b/ios/BUILD index 1d6db10..caa8cde 100644 --- a/ios/BUILD +++ b/ios/BUILD @@ -18,5 +18,6 @@ xcodeproj( "//devtools/plugin/swiftui:PlayerUIDevtoolsSwiftUIPluginsViewInspectorTests", "//devtools/plugins/basic/ios:PlayerUIDevtoolsBaseBasicDevtoolsPluginTests", "//devtools/plugins/basic/swiftui:PlayerUIDevtoolsBasicPluginViewInspectorTests", + "//devtools/plugins/profiler/ios:PlayerUIDevtoolsProfilerDevtoolsPluginTests", ], ) \ No newline at end of file diff --git a/ios/demo/BUILD b/ios/demo/BUILD index 7dd1713..f03238d 100644 --- a/ios/demo/BUILD +++ b/ios/demo/BUILD @@ -20,7 +20,7 @@ swift_library( # These are the components under development. "//devtools/plugin/ios:PlayerUIDevtoolsPlugins", "//devtools/plugins/basic/swiftui:PlayerUIDevtoolsBasicPlugin", - "//devtools/plugins/profiler/ios:PlayerUIDevToolsProfilerDevtoolsPlugin", + "//devtools/plugins/profiler/ios:PlayerUIDevtoolsProfilerDevtoolsPlugin", ] ) diff --git a/ios/demo/Sources/DemoApp.swift b/ios/demo/Sources/DemoApp.swift index 868b389..1f65b3e 100644 --- a/ios/demo/Sources/DemoApp.swift +++ b/ios/demo/Sources/DemoApp.swift @@ -7,6 +7,7 @@ import SwiftFlipper import PlayerUIDevtoolsPlugins import PlayerUIReferenceAssets import PlayerUIDevtoolsBasicPlugin +import PlayerUIDevtoolsProfilerDevtoolsPlugin @main struct BazelApp: App { @@ -95,7 +96,8 @@ class DemoViewModel: ObservableObject { defaultPlugins = [ ReferenceAssetsPlugin(), PrintLoggerPlugin(level: .debug), - BasicDevtoolsPlugin(id: devtoolsPluginID, flipperPlugin: flipperPlugin) + BasicDevtoolsPlugin(id: devtoolsPluginID, flipperPlugin: flipperPlugin), + ProfilerDevtoolsPlugin(id: devtoolsPluginID, flipperPlugin: flipperPlugin) ] flipperClient.addPlugin(flipperPlugin) @@ -114,7 +116,8 @@ class DemoViewModel: ObservableObject { defaultPlugins = [ ReferenceAssetsPlugin(), PrintLoggerPlugin(level: .debug), - BasicDevtoolsPlugin(id: devtoolsPluginID, flipperPlugin: flipperPlugin) + BasicDevtoolsPlugin(id: devtoolsPluginID, flipperPlugin: flipperPlugin), + ProfilerDevtoolsPlugin(id: devtoolsPluginID, flipperPlugin: flipperPlugin) ] } } From 550e0641fea309dbb4229ba444fd2101c480e74d Mon Sep 17 00:00:00 2001 From: Thomas Marmer Date: Thu, 23 Apr 2026 12:01:07 -0400 Subject: [PATCH 03/10] fix mobile profiler plugin. update profiler plugin view to use flame chart --- devtools/plugin/core/src/plugin.ts | 2 +- .../core/src/__tests__/plugin.test.ts | 2421 +---------------- .../src/addProfilerInterceptorsToHooks.ts | 29 +- .../src/helpers/__tests__/profiler.test.ts | 112 +- .../profiler/core/src/helpers/profiler.ts | 35 +- .../profiler/core/src/plugin-flow.json | 259 +- devtools/plugins/profiler/core/src/plugin.ts | 109 +- devtools/plugins/profiler/core/src/types.ts | 4 + 8 files changed, 301 insertions(+), 2670 deletions(-) diff --git a/devtools/plugin/core/src/plugin.ts b/devtools/plugin/core/src/plugin.ts index 7a0cca2..234255a 100644 --- a/devtools/plugin/core/src/plugin.ts +++ b/devtools/plugin/core/src/plugin.ts @@ -83,7 +83,7 @@ export class DevtoolsPlugin implements PlayerPlugin, DevtoolsHandler { } registerMessenger( - messenger: Messenger + messenger: Messenger, ): Unsubscribe { // Propagate new messages from state to devtools via the messenger let lastMessageIndex = -1; diff --git a/devtools/plugins/profiler/core/src/__tests__/plugin.test.ts b/devtools/plugins/profiler/core/src/__tests__/plugin.test.ts index f6c5b44..3d0c1bf 100644 --- a/devtools/plugins/profiler/core/src/__tests__/plugin.test.ts +++ b/devtools/plugins/profiler/core/src/__tests__/plugin.test.ts @@ -22,12 +22,6 @@ describe("Plugin", () => { }); const player = new Player({ plugins: [profilerPlugin] }); - profilerPlugin.processInteraction({ - payload: { - type: "start-profiling", - }, - type: "PLAYER_DEVTOOLS_PLUGIN_INTERACTION", - }); // This flow is used to navigate through common player steps. const flow: Flow = { @@ -72,10 +66,17 @@ describe("Plugin", () => { expect(playerState.status).toBe("in-progress"); expect( (playerState as InProgressState).controllers.view.currentView - ?.lastUpdate + ?.lastUpdate, ).toBeDefined(); }); + // Live update: durations should already be populated while profiling is active + const liveData = + profilerPlugin.store.getState().plugins["player-ui-profiler-plugin"]?.flow + .data; + expect(liveData?.profiling).toBe(true); + expect((liveData?.durations as unknown[]).length).toBeGreaterThan(0); + // Transition to action state (player.getState() as InProgressState).controllers.flow.transition("go"); @@ -89,2341 +90,77 @@ describe("Plugin", () => { }); const storeState = profilerPlugin.store.getState(); - expect(storeState.plugins["player-ui-profiler-plugin"]?.flow.data) - .toMatchInlineSnapshot(` - { - "displayFlameGraph": true, - "durations": [ - { - "duration": "6.0000 ms", - "name": "afterTransition", - }, - { - "duration": "4.3000 ms", - "name": "transition", - }, - { - "duration": "0.4000 ms", - "name": "skipTransition", - }, - { - "duration": "0.4000 ms", - "name": "beforeTransition", - }, - { - "duration": "0.4000 ms", - "name": "resolveTransitionNode", - }, - { - "duration": "0.4000 ms", - "name": "transition", - }, - { - "duration": "0.3000 ms", - "name": "view", - }, - { - "duration": "0.3000 ms", - "name": "resolveDefaultValue", - }, - { - "duration": "0.2000 ms", - "name": "skipTransition", - }, - { - "duration": "0.2000 ms", - "name": "beforeTransition", - }, - { - "duration": "0.2000 ms", - "name": "resolveTransitionNode", - }, - { - "duration": "0.2000 ms", - "name": "transition", - }, - { - "duration": "0.2000 ms", - "name": "resolve", - }, - { - "duration": "0.1000 ms", - "name": "state", - }, - { - "duration": "0.1000 ms", - "name": "resolveFlowContent", - }, - { - "duration": "0.1000 ms", - "name": "onStart", - }, - { - "duration": "0.1000 ms", - "name": "flowController", - }, - { - "duration": "0.1000 ms", - "name": "bindingParser", - }, - { - "duration": "0.1000 ms", - "name": "schema", - }, - { - "duration": "0.1000 ms", - "name": "validationController", - }, - { - "duration": "0.1000 ms", - "name": "expressionEvaluator", - }, - { - "duration": "0.1000 ms", - "name": "dataController", - }, - { - "duration": "0.1000 ms", - "name": "viewController", - }, - { - "duration": "0.1000 ms", - "name": "state", - }, - { - "duration": "0.1000 ms", - "name": "flow", - }, - { - "duration": "0.1000 ms", - "name": "beforeStart", - }, - { - "duration": "0.1000 ms", - "name": "resolveTransitionNode", - }, - { - "duration": "0.1000 ms", - "name": "resolveView", - }, - { - "duration": "0.1000 ms", - "name": "onTemplatePluginCreated", - }, - { - "duration": "0.1000 ms", - "name": "templatePlugin", - }, - { - "duration": "0.1000 ms", - "name": "parser", - }, - { - "duration": "0.1000 ms", - "name": "parseNode", - }, - { - "duration": "0.1000 ms", - "name": "onParseObject", - }, - { - "duration": "0.1000 ms", - "name": "parseNode", - }, - { - "duration": "0.1000 ms", - "name": "parseNode", - }, - { - "duration": "0.1000 ms", - "name": "parseNode", - }, - { - "duration": "0.1000 ms", - "name": "onCreateASTNode", - }, - { - "duration": "0.1000 ms", - "name": "resolver", - }, - { - "duration": "0.1000 ms", - "name": "beforeUpdate", - }, - { - "duration": "0.1000 ms", - "name": "resolveOptions", - }, - { - "duration": "0.1000 ms", - "name": "skipResolve", - }, - { - "duration": "0.1000 ms", - "name": "beforeResolve", - }, - { - "duration": "0.1000 ms", - "name": "resolve", - }, - { - "duration": "0.1000 ms", - "name": "afterResolve", - }, - { - "duration": "0.1000 ms", - "name": "afterNodeUpdate", - }, - { - "duration": "0.1000 ms", - "name": "afterUpdate", - }, - { - "duration": "0.1000 ms", - "name": "onUpdate", - }, - { - "duration": "0.1000 ms", - "name": "afterTransition", - }, - { - "duration": "0.1000 ms", - "name": "resolveOptions", - }, - { - "duration": "0.1000 ms", - "name": "beforeEvaluate", - }, - { - "duration": "0.1000 ms", - "name": "skipOptimization", - }, - { - "duration": "0.1000 ms", - "name": "resolveDataStages", - }, - { - "duration": "0.1000 ms", - "name": "resolveTypeForBinding", - }, - { - "duration": "0.1000 ms", - "name": "onGet", - }, - { - "duration": "0.1000 ms", - "name": "onSet", - }, - { - "duration": "0.1000 ms", - "name": "onUpdate", - }, - { - "duration": "0.1000 ms", - "name": "onGet", - }, - { - "duration": "0.1000 ms", - "name": "serialize", - }, - { - "duration": "0.1000 ms", - "name": "state", - }, - { - "duration": "0.0100 ms", - "name": "afterTransition", - }, - { - "duration": "0.0100 ms", - "name": "skipTransition", - }, - { - "duration": "0.0100 ms", - "name": "beforeTransition", - }, - { - "duration": "0.0100 ms", - "name": "resolveTransitionNode", - }, - { - "duration": "0.0100 ms", - "name": "transition", - }, - { - "duration": "0.0100 ms", - "name": "resolve", - }, - { - "duration": "0.0100 ms", - "name": "skipTransition", - }, - { - "duration": "0.0100 ms", - "name": "skipTransition", - }, - { - "duration": "0.0100 ms", - "name": "skipTransition", - }, - { - "duration": "0.0100 ms", - "name": "beforeTransition", - }, - { - "duration": "0.0100 ms", - "name": "beforeTransition", - }, - { - "duration": "0.0100 ms", - "name": "beforeTransition", - }, - { - "duration": "0.0100 ms", - "name": "resolveTransitionNode", - }, - { - "duration": "0.0100 ms", - "name": "resolveTransitionNode", - }, - { - "duration": "0.0100 ms", - "name": "resolveTransitionNode", - }, - { - "duration": "0.0100 ms", - "name": "transition", - }, - { - "duration": "0.0100 ms", - "name": "transition", - }, - { - "duration": "0.0100 ms", - "name": "transition", - }, - { - "duration": "0.0100 ms", - "name": "afterTransition", - }, - { - "duration": "0.0100 ms", - "name": "afterTransition", - }, - { - "duration": "0.0100 ms", - "name": "afterTransition", - }, - { - "duration": "0.0100 ms", - "name": "afterTransition", - }, - { - "duration": "0.0100 ms", - "name": "afterTransition", - }, - { - "duration": "0.0100 ms", - "name": "afterTransition", - }, - { - "duration": "0.0100 ms", - "name": "afterTransition", - }, - { - "duration": "0.0100 ms", - "name": "afterTransition", - }, - { - "duration": "0.0100 ms", - "name": "afterTransition", - }, - { - "duration": "0.0100 ms", - "name": "afterTransition", - }, - { - "duration": "0.0100 ms", - "name": "afterTransition", - }, - { - "duration": "0.0100 ms", - "name": "afterTransition", - }, - { - "duration": "0.0100 ms", - "name": "afterTransition", - }, - { - "duration": "0.0100 ms", - "name": "afterTransition", - }, - { - "duration": "0.0100 ms", - "name": "afterTransition", - }, - ], - "profiling": false, - "rootNode": { - "children": [ - { - "children": [], - "endTime": 2490.2999999999997, - "name": "state", - "startTime": 2490.2, - "tooltip": "state, 0.1000 (ms)", - "value": 100, - }, - { - "children": [], - "endTime": 2490.4999999999995, - "name": "resolveFlowContent", - "startTime": 2490.3999999999996, - "tooltip": "resolveFlowContent, 0.1000 (ms)", - "value": 100, - }, - { - "children": [], - "endTime": 2490.6999999999994, - "name": "onStart", - "startTime": 2490.5999999999995, - "tooltip": "onStart, 0.1000 (ms)", - "value": 100, - }, - { - "children": [ - { - "children": [ - { - "children": [], - "endTime": 2492.6999999999975, - "name": "beforeStart", - "startTime": 2492.5999999999976, - "tooltip": "beforeStart, 0.1000 (ms)", - "value": 100, - }, - { - "children": [], - "endTime": 2492.8999999999974, - "name": "resolveTransitionNode", - "startTime": 2492.7999999999975, - "tooltip": "resolveTransitionNode, 0.1000 (ms)", - "value": 100, - }, - { - "children": [], - "endTime": 2497.2999999999934, - "name": "transition", - "startTime": 2492.9999999999973, - "tooltip": "transition, 4.3000 (ms)", - "value": 4300, - }, - { - "children": [ - { - "children": [ - { - "children": [], - "endTime": 2502.3999999999887, - "name": "skipTransition", - "startTime": undefined, - "tooltip": "skipTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2503.199999999988, - "name": "beforeTransition", - "startTime": undefined, - "tooltip": "beforeTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2503.9999999999873, - "name": "resolveTransitionNode", - "startTime": undefined, - "tooltip": "resolveTransitionNode, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2504.7999999999865, - "name": "transition", - "startTime": undefined, - "tooltip": "transition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [ - { - "children": [], - "endTime": 2505.9999999999854, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2506.7999999999847, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - ], - "endTime": 2505.599999999986, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2505.7999999999856, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [ - { - "children": [], - "endTime": 2505.9999999999854, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2506.7999999999847, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - ], - "endTime": 2506.399999999985, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2506.599999999985, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - ], - "endTime": 2497.599999999993, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2497.9999999999927, - "name": "skipTransition", - "startTime": undefined, - "tooltip": "skipTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2498.3999999999924, - "name": "beforeTransition", - "startTime": undefined, - "tooltip": "beforeTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2498.799999999992, - "name": "resolveTransitionNode", - "startTime": undefined, - "tooltip": "resolveTransitionNode, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2499.1999999999916, - "name": "transition", - "startTime": undefined, - "tooltip": "transition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2502.199999999989, - "name": "skipTransition", - "startTime": undefined, - "tooltip": "skipTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2502.299999999989, - "name": "skipTransition", - "startTime": undefined, - "tooltip": "skipTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2502.999999999988, - "name": "beforeTransition", - "startTime": undefined, - "tooltip": "beforeTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2503.099999999988, - "name": "beforeTransition", - "startTime": undefined, - "tooltip": "beforeTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2503.7999999999874, - "name": "resolveTransitionNode", - "startTime": undefined, - "tooltip": "resolveTransitionNode, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2503.8999999999874, - "name": "resolveTransitionNode", - "startTime": undefined, - "tooltip": "resolveTransitionNode, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2504.5999999999867, - "name": "transition", - "startTime": undefined, - "tooltip": "transition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2504.6999999999866, - "name": "transition", - "startTime": undefined, - "tooltip": "transition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [ - { - "children": [], - "endTime": 2502.3999999999887, - "name": "skipTransition", - "startTime": undefined, - "tooltip": "skipTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2503.199999999988, - "name": "beforeTransition", - "startTime": undefined, - "tooltip": "beforeTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2503.9999999999873, - "name": "resolveTransitionNode", - "startTime": undefined, - "tooltip": "resolveTransitionNode, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2504.7999999999865, - "name": "transition", - "startTime": undefined, - "tooltip": "transition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [ - { - "children": [], - "endTime": 2505.9999999999854, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2506.7999999999847, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - ], - "endTime": 2505.599999999986, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2505.7999999999856, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [ - { - "children": [], - "endTime": 2505.9999999999854, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2506.7999999999847, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - ], - "endTime": 2506.399999999985, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2506.599999999985, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - ], - "endTime": 2505.399999999986, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [ - { - "children": [], - "endTime": 2505.8999999999855, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2506.699999999985, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - ], - "endTime": 2505.499999999986, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2505.6999999999857, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [ - { - "children": [], - "endTime": 2502.3999999999887, - "name": "skipTransition", - "startTime": undefined, - "tooltip": "skipTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2503.199999999988, - "name": "beforeTransition", - "startTime": undefined, - "tooltip": "beforeTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2503.9999999999873, - "name": "resolveTransitionNode", - "startTime": undefined, - "tooltip": "resolveTransitionNode, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2504.7999999999865, - "name": "transition", - "startTime": undefined, - "tooltip": "transition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [ - { - "children": [], - "endTime": 2505.9999999999854, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2506.7999999999847, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - ], - "endTime": 2505.599999999986, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2505.7999999999856, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [ - { - "children": [], - "endTime": 2505.9999999999854, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2506.7999999999847, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - ], - "endTime": 2506.399999999985, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2506.599999999985, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - ], - "endTime": 2506.1999999999853, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [ - { - "children": [], - "endTime": 2505.8999999999855, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2506.699999999985, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - ], - "endTime": 2506.299999999985, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2506.499999999985, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - ], - "endTime": 2497.499999999993, - "name": "afterTransition", - "startTime": 2497.3999999999933, - "tooltip": "afterTransition, 0.1000 (ms)", - "value": 100, - }, - { - "children": [], - "endTime": 2497.899999999993, - "name": "skipTransition", - "startTime": 2497.699999999993, - "tooltip": "skipTransition, 0.2000 (ms)", - "value": 200, - }, - { - "children": [], - "endTime": 2498.2999999999925, - "name": "beforeTransition", - "startTime": 2498.0999999999926, - "tooltip": "beforeTransition, 0.2000 (ms)", - "value": 200, - }, - { - "children": [], - "endTime": 2498.699999999992, - "name": "resolveTransitionNode", - "startTime": 2498.4999999999923, - "tooltip": "resolveTransitionNode, 0.2000 (ms)", - "value": 200, - }, - { - "children": [], - "endTime": 2499.0999999999917, - "name": "transition", - "startTime": 2498.899999999992, - "tooltip": "transition, 0.2000 (ms)", - "value": 200, - }, - { - "children": [], - "endTime": 2502.099999999989, - "name": "skipTransition", - "startTime": 2501.6999999999894, - "tooltip": "skipTransition, 0.4000 (ms)", - "value": 400, - }, - { - "children": [], - "endTime": 2502.8999999999883, - "name": "beforeTransition", - "startTime": 2502.4999999999886, - "tooltip": "beforeTransition, 0.4000 (ms)", - "value": 400, - }, - { - "children": [], - "endTime": 2503.6999999999875, - "name": "resolveTransitionNode", - "startTime": 2503.299999999988, - "tooltip": "resolveTransitionNode, 0.4000 (ms)", - "value": 400, - }, - { - "children": [], - "endTime": 2504.499999999987, - "name": "transition", - "startTime": 2504.099999999987, - "tooltip": "transition, 0.4000 (ms)", - "value": 400, - }, - { - "children": [ - { - "children": [ - { - "children": [], - "endTime": 2502.3999999999887, - "name": "skipTransition", - "startTime": undefined, - "tooltip": "skipTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2503.199999999988, - "name": "beforeTransition", - "startTime": undefined, - "tooltip": "beforeTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2503.9999999999873, - "name": "resolveTransitionNode", - "startTime": undefined, - "tooltip": "resolveTransitionNode, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2504.7999999999865, - "name": "transition", - "startTime": undefined, - "tooltip": "transition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [ - { - "children": [], - "endTime": 2505.9999999999854, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2506.7999999999847, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - ], - "endTime": 2505.599999999986, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2505.7999999999856, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [ - { - "children": [], - "endTime": 2505.9999999999854, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2506.7999999999847, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - ], - "endTime": 2506.399999999985, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2506.599999999985, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - ], - "endTime": 2497.599999999993, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2497.9999999999927, - "name": "skipTransition", - "startTime": undefined, - "tooltip": "skipTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2498.3999999999924, - "name": "beforeTransition", - "startTime": undefined, - "tooltip": "beforeTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2498.799999999992, - "name": "resolveTransitionNode", - "startTime": undefined, - "tooltip": "resolveTransitionNode, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2499.1999999999916, - "name": "transition", - "startTime": undefined, - "tooltip": "transition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2502.199999999989, - "name": "skipTransition", - "startTime": undefined, - "tooltip": "skipTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2502.299999999989, - "name": "skipTransition", - "startTime": undefined, - "tooltip": "skipTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2502.999999999988, - "name": "beforeTransition", - "startTime": undefined, - "tooltip": "beforeTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2503.099999999988, - "name": "beforeTransition", - "startTime": undefined, - "tooltip": "beforeTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2503.7999999999874, - "name": "resolveTransitionNode", - "startTime": undefined, - "tooltip": "resolveTransitionNode, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2503.8999999999874, - "name": "resolveTransitionNode", - "startTime": undefined, - "tooltip": "resolveTransitionNode, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2504.5999999999867, - "name": "transition", - "startTime": undefined, - "tooltip": "transition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2504.6999999999866, - "name": "transition", - "startTime": undefined, - "tooltip": "transition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [ - { - "children": [], - "endTime": 2502.3999999999887, - "name": "skipTransition", - "startTime": undefined, - "tooltip": "skipTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2503.199999999988, - "name": "beforeTransition", - "startTime": undefined, - "tooltip": "beforeTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2503.9999999999873, - "name": "resolveTransitionNode", - "startTime": undefined, - "tooltip": "resolveTransitionNode, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2504.7999999999865, - "name": "transition", - "startTime": undefined, - "tooltip": "transition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [ - { - "children": [], - "endTime": 2505.9999999999854, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2506.7999999999847, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - ], - "endTime": 2505.599999999986, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2505.7999999999856, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [ - { - "children": [], - "endTime": 2505.9999999999854, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2506.7999999999847, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - ], - "endTime": 2506.399999999985, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2506.599999999985, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - ], - "endTime": 2505.399999999986, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [ - { - "children": [], - "endTime": 2505.8999999999855, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2506.699999999985, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - ], - "endTime": 2505.499999999986, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2505.6999999999857, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [ - { - "children": [], - "endTime": 2502.3999999999887, - "name": "skipTransition", - "startTime": undefined, - "tooltip": "skipTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2503.199999999988, - "name": "beforeTransition", - "startTime": undefined, - "tooltip": "beforeTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2503.9999999999873, - "name": "resolveTransitionNode", - "startTime": undefined, - "tooltip": "resolveTransitionNode, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2504.7999999999865, - "name": "transition", - "startTime": undefined, - "tooltip": "transition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [ - { - "children": [], - "endTime": 2505.9999999999854, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2506.7999999999847, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - ], - "endTime": 2505.599999999986, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2505.7999999999856, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [ - { - "children": [], - "endTime": 2505.9999999999854, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2506.7999999999847, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - ], - "endTime": 2506.399999999985, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2506.599999999985, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - ], - "endTime": 2506.1999999999853, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [ - { - "children": [], - "endTime": 2505.8999999999855, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2506.699999999985, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - ], - "endTime": 2506.299999999985, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2506.499999999985, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - ], - "endTime": 2505.299999999986, - "name": "afterTransition", - "startTime": 2499.2999999999915, - "tooltip": "afterTransition, 6.0000 (ms)", - "value": 6000, - }, - { - "children": [ - { - "children": [ - { - "children": [], - "endTime": 2502.3999999999887, - "name": "skipTransition", - "startTime": undefined, - "tooltip": "skipTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2503.199999999988, - "name": "beforeTransition", - "startTime": undefined, - "tooltip": "beforeTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2503.9999999999873, - "name": "resolveTransitionNode", - "startTime": undefined, - "tooltip": "resolveTransitionNode, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2504.7999999999865, - "name": "transition", - "startTime": undefined, - "tooltip": "transition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [ - { - "children": [], - "endTime": 2505.9999999999854, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2506.7999999999847, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - ], - "endTime": 2505.599999999986, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2505.7999999999856, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [ - { - "children": [], - "endTime": 2505.9999999999854, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2506.7999999999847, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - ], - "endTime": 2506.399999999985, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2506.599999999985, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - ], - "endTime": 2497.599999999993, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2497.9999999999927, - "name": "skipTransition", - "startTime": undefined, - "tooltip": "skipTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2498.3999999999924, - "name": "beforeTransition", - "startTime": undefined, - "tooltip": "beforeTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2498.799999999992, - "name": "resolveTransitionNode", - "startTime": undefined, - "tooltip": "resolveTransitionNode, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2499.1999999999916, - "name": "transition", - "startTime": undefined, - "tooltip": "transition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2502.199999999989, - "name": "skipTransition", - "startTime": undefined, - "tooltip": "skipTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2502.299999999989, - "name": "skipTransition", - "startTime": undefined, - "tooltip": "skipTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2502.999999999988, - "name": "beforeTransition", - "startTime": undefined, - "tooltip": "beforeTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2503.099999999988, - "name": "beforeTransition", - "startTime": undefined, - "tooltip": "beforeTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2503.7999999999874, - "name": "resolveTransitionNode", - "startTime": undefined, - "tooltip": "resolveTransitionNode, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2503.8999999999874, - "name": "resolveTransitionNode", - "startTime": undefined, - "tooltip": "resolveTransitionNode, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2504.5999999999867, - "name": "transition", - "startTime": undefined, - "tooltip": "transition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2504.6999999999866, - "name": "transition", - "startTime": undefined, - "tooltip": "transition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [ - { - "children": [], - "endTime": 2502.3999999999887, - "name": "skipTransition", - "startTime": undefined, - "tooltip": "skipTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2503.199999999988, - "name": "beforeTransition", - "startTime": undefined, - "tooltip": "beforeTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2503.9999999999873, - "name": "resolveTransitionNode", - "startTime": undefined, - "tooltip": "resolveTransitionNode, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2504.7999999999865, - "name": "transition", - "startTime": undefined, - "tooltip": "transition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [ - { - "children": [], - "endTime": 2505.9999999999854, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2506.7999999999847, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - ], - "endTime": 2505.599999999986, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2505.7999999999856, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [ - { - "children": [], - "endTime": 2505.9999999999854, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2506.7999999999847, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - ], - "endTime": 2506.399999999985, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2506.599999999985, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - ], - "endTime": 2505.399999999986, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [ - { - "children": [], - "endTime": 2505.8999999999855, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2506.699999999985, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - ], - "endTime": 2505.499999999986, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2505.6999999999857, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [ - { - "children": [], - "endTime": 2502.3999999999887, - "name": "skipTransition", - "startTime": undefined, - "tooltip": "skipTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2503.199999999988, - "name": "beforeTransition", - "startTime": undefined, - "tooltip": "beforeTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2503.9999999999873, - "name": "resolveTransitionNode", - "startTime": undefined, - "tooltip": "resolveTransitionNode, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2504.7999999999865, - "name": "transition", - "startTime": undefined, - "tooltip": "transition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [ - { - "children": [], - "endTime": 2505.9999999999854, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2506.7999999999847, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - ], - "endTime": 2505.599999999986, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2505.7999999999856, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [ - { - "children": [], - "endTime": 2505.9999999999854, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2506.7999999999847, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - ], - "endTime": 2506.399999999985, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2506.599999999985, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - ], - "endTime": 2506.1999999999853, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [ - { - "children": [], - "endTime": 2505.8999999999855, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2506.699999999985, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - ], - "endTime": 2506.299999999985, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - { - "children": [], - "endTime": 2506.499999999985, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - ], - "endTime": 2506.0999999999854, - "name": "afterTransition", - "startTime": undefined, - "tooltip": "afterTransition, 0.0100 (ms)", - "value": 10, - }, - ], - "endTime": 2492.4999999999977, - "name": "flow", - "startTime": 2492.399999999998, - "tooltip": "flow, 0.1000 (ms)", - "value": 100, - }, - ], - "endTime": 2490.899999999999, - "name": "flowController", - "startTime": 2490.7999999999993, - "tooltip": "flowController, 0.1000 (ms)", - "value": 100, - }, - { - "children": [ - { - "children": [], - "endTime": 2500.2999999999906, - "name": "skipOptimization", - "startTime": 2500.1999999999907, - "tooltip": "skipOptimization, 0.1000 (ms)", - "value": 100, - }, - ], - "endTime": 2491.099999999999, - "name": "bindingParser", - "startTime": 2490.999999999999, - "tooltip": "bindingParser, 0.1000 (ms)", - "value": 100, - }, - { - "children": [ - { - "children": [], - "endTime": 2500.79999999999, - "name": "resolveTypeForBinding", - "startTime": 2500.6999999999903, - "tooltip": "resolveTypeForBinding, 0.1000 (ms)", - "value": 100, - }, - ], - "endTime": 2491.299999999999, - "name": "schema", - "startTime": 2491.199999999999, - "tooltip": "schema, 0.1000 (ms)", - "value": 100, - }, - { - "children": [], - "endTime": 2491.4999999999986, - "name": "validationController", - "startTime": 2491.3999999999987, - "tooltip": "validationController, 0.1000 (ms)", - "value": 100, - }, - { - "children": [ - { - "children": [], - "endTime": 2499.5999999999913, - "name": "resolveOptions", - "startTime": 2499.4999999999914, - "tooltip": "resolveOptions, 0.1000 (ms)", - "value": 100, - }, - { - "children": [], - "endTime": 2499.799999999991, - "name": "beforeEvaluate", - "startTime": 2499.699999999991, - "tooltip": "beforeEvaluate, 0.1000 (ms)", - "value": 100, - }, - { - "children": [], - "endTime": 2500.099999999991, - "name": "resolve", - "startTime": 2499.899999999991, - "tooltip": "resolve, 0.2000 (ms)", - "value": 200, - }, - { - "children": [], - "endTime": 2501.5999999999894, - "name": "resolve", - "startTime": undefined, - "tooltip": "resolve, 0.0100 (ms)", - "value": 10, - }, - ], - "endTime": 2491.6999999999985, - "name": "expressionEvaluator", - "startTime": 2491.5999999999985, - "tooltip": "expressionEvaluator, 0.1000 (ms)", - "value": 100, - }, - { - "children": [ - { - "children": [], - "endTime": 2500.4999999999905, - "name": "resolveDataStages", - "startTime": 2500.3999999999905, - "tooltip": "resolveDataStages, 0.1000 (ms)", - "value": 100, - }, - { - "children": [], - "endTime": 2500.89999999999, - "name": "resolveDefaultValue", - "startTime": 2500.5999999999904, - "tooltip": "resolveDefaultValue, 0.3000 (ms)", - "value": 300, - }, - { - "children": [], - "endTime": 2501.09999999999, - "name": "onGet", - "startTime": 2500.99999999999, - "tooltip": "onGet, 0.1000 (ms)", - "value": 100, - }, - { - "children": [], - "endTime": 2501.2999999999897, - "name": "onSet", - "startTime": 2501.19999999999, - "tooltip": "onSet, 0.1000 (ms)", - "value": 100, - }, - { - "children": [], - "endTime": 2501.4999999999895, - "name": "onUpdate", - "startTime": 2501.3999999999896, - "tooltip": "onUpdate, 0.1000 (ms)", - "value": 100, - }, - { - "children": [], - "endTime": 2506.9999999999845, - "name": "onGet", - "startTime": 2506.8999999999846, - "tooltip": "onGet, 0.1000 (ms)", - "value": 100, - }, - { - "children": [], - "endTime": 2507.1999999999844, - "name": "serialize", - "startTime": 2507.0999999999844, - "tooltip": "serialize, 0.1000 (ms)", - "value": 100, - }, - ], - "endTime": 2491.8999999999983, - "name": "dataController", - "startTime": 2491.7999999999984, - "tooltip": "dataController, 0.1000 (ms)", - "value": 100, - }, - { - "children": [ - { - "children": [], - "endTime": 2493.199999999997, - "name": "resolveView", - "startTime": 2493.099999999997, - "tooltip": "resolveView, 0.1000 (ms)", - "value": 100, - }, - { - "children": [ - { - "children": [], - "endTime": 2493.499999999997, - "name": "onTemplatePluginCreated", - "startTime": 2493.399999999997, - "tooltip": "onTemplatePluginCreated, 0.1000 (ms)", - "value": 100, - }, - { - "children": [], - "endTime": 2493.7999999999965, - "name": "templatePlugin", - "startTime": 2493.6999999999966, - "tooltip": "templatePlugin, 0.1000 (ms)", - "value": 100, - }, - { - "children": [ - { - "children": [], - "endTime": 2494.199999999996, - "name": "parseNode", - "startTime": 2494.0999999999963, - "tooltip": "parseNode, 0.1000 (ms)", - "value": 100, - }, - { - "children": [], - "endTime": 2494.399999999996, - "name": "onParseObject", - "startTime": 2494.299999999996, - "tooltip": "onParseObject, 0.1000 (ms)", - "value": 100, - }, - { - "children": [], - "endTime": 2494.599999999996, - "name": "parseNode", - "startTime": 2494.499999999996, - "tooltip": "parseNode, 0.1000 (ms)", - "value": 100, - }, - { - "children": [], - "endTime": 2494.7999999999956, - "name": "parseNode", - "startTime": 2494.6999999999957, - "tooltip": "parseNode, 0.1000 (ms)", - "value": 100, - }, - { - "children": [], - "endTime": 2494.9999999999955, - "name": "parseNode", - "startTime": 2494.8999999999955, - "tooltip": "parseNode, 0.1000 (ms)", - "value": 100, - }, - { - "children": [], - "endTime": 2495.1999999999953, - "name": "onCreateASTNode", - "startTime": 2495.0999999999954, - "tooltip": "onCreateASTNode, 0.1000 (ms)", - "value": 100, - }, - ], - "endTime": 2493.9999999999964, - "name": "parser", - "startTime": 2493.8999999999965, - "tooltip": "parser, 0.1000 (ms)", - "value": 100, - }, - { - "children": [ - { - "children": [], - "endTime": 2495.599999999995, - "name": "beforeUpdate", - "startTime": 2495.499999999995, - "tooltip": "beforeUpdate, 0.1000 (ms)", - "value": 100, - }, - { - "children": [], - "endTime": 2495.7999999999947, - "name": "resolveOptions", - "startTime": 2495.699999999995, - "tooltip": "resolveOptions, 0.1000 (ms)", - "value": 100, - }, - { - "children": [], - "endTime": 2495.9999999999945, - "name": "skipResolve", - "startTime": 2495.8999999999946, - "tooltip": "skipResolve, 0.1000 (ms)", - "value": 100, - }, - { - "children": [], - "endTime": 2496.1999999999944, - "name": "beforeResolve", - "startTime": 2496.0999999999945, - "tooltip": "beforeResolve, 0.1000 (ms)", - "value": 100, - }, - { - "children": [], - "endTime": 2496.399999999994, - "name": "resolve", - "startTime": 2496.2999999999943, - "tooltip": "resolve, 0.1000 (ms)", - "value": 100, - }, - { - "children": [], - "endTime": 2496.599999999994, - "name": "afterResolve", - "startTime": 2496.499999999994, - "tooltip": "afterResolve, 0.1000 (ms)", - "value": 100, - }, - { - "children": [], - "endTime": 2496.799999999994, - "name": "afterNodeUpdate", - "startTime": 2496.699999999994, - "tooltip": "afterNodeUpdate, 0.1000 (ms)", - "value": 100, - }, - { - "children": [], - "endTime": 2496.9999999999936, - "name": "afterUpdate", - "startTime": 2496.8999999999937, - "tooltip": "afterUpdate, 0.1000 (ms)", - "value": 100, - }, - ], - "endTime": 2495.399999999995, - "name": "resolver", - "startTime": 2495.299999999995, - "tooltip": "resolver, 0.1000 (ms)", - "value": 100, - }, - { - "children": [], - "endTime": 2497.1999999999935, - "name": "onUpdate", - "startTime": 2497.0999999999935, - "tooltip": "onUpdate, 0.1000 (ms)", - "value": 100, - }, - ], - "endTime": 2493.5999999999967, - "name": "view", - "startTime": 2493.299999999997, - "tooltip": "view, 0.3000 (ms)", - "value": 300, - }, - ], - "endTime": 2492.099999999998, - "name": "viewController", - "startTime": 2491.999999999998, - "tooltip": "viewController, 0.1000 (ms)", - "value": 100, - }, - { - "children": [], - "endTime": 2492.299999999998, - "name": "state", - "startTime": 2492.199999999998, - "tooltip": "state, 0.1000 (ms)", - "value": 100, - }, - { - "children": [], - "endTime": 2507.399999999984, - "name": "state", - "startTime": 2507.2999999999843, - "tooltip": "state, 0.1000 (ms)", - "value": 100, - }, - ], - "endTime": 2507.499999999984, - "name": "root", - "startTime": 2490.1, - "tooltip": "Profiler total time span 17.4000 (ms)", - "value": 1200, + expect( + storeState.plugins["player-ui-profiler-plugin"]?.flow.data, + ).toMatchSnapshot(); + }); + + test("stop-profiling marks profiling complete; start-profiling resets state", async () => { + const profilerPlugin = new ProfilerDevtoolsPlugin({ + handler: { + checkIfDevtoolsIsActive: () => true, + processInteraction: () => {}, + }, + playerID: "ID", + }); + + const player = new Player({ plugins: [profilerPlugin] }); + + const flow: Flow = { + id: "flow2", + views: [{ id: "view1", type: "foo" }], + navigation: { + BEGIN: "FLOW_1", + FLOW_1: { + startState: "VIEW_1", + VIEW_1: { + state_type: "VIEW", + ref: "view1", + transitions: { "*": "END_DONE" }, }, - } - `); + END_DONE: { state_type: "END", outcome: "done" }, + }, + }, + }; + + const playerPromise = player.start(flow); + + // Wait for the view to render so hooks have fired + await vi.waitFor(() => { + const playerState = player.getState(); + expect(playerState.status).toBe("in-progress"); + expect( + (playerState as InProgressState).controllers.view.currentView + ?.lastUpdate, + ).toBeDefined(); + }); + + profilerPlugin.processInteraction({ + payload: { type: "stop-profiling" }, + type: "PLAYER_DEVTOOLS_PLUGIN_INTERACTION", + }); + + const dataAfterStop = + profilerPlugin.store.getState().plugins["player-ui-profiler-plugin"]?.flow + .data; + expect(dataAfterStop?.profiling).toBe(false); + expect(dataAfterStop?.displayFlameGraph).toBe(true); + expect((dataAfterStop?.durations as unknown[]).length).toBeGreaterThan(0); + + // Restart — state should flip back to active profiling + profilerPlugin.processInteraction({ + payload: { type: "start-profiling" }, + type: "PLAYER_DEVTOOLS_PLUGIN_INTERACTION", + }); + + const dataAfterRestart = + profilerPlugin.store.getState().plugins["player-ui-profiler-plugin"]?.flow + .data; + expect(dataAfterRestart?.profiling).toBe(true); + expect(dataAfterRestart?.displayFlameGraph).toBe(false); + + // Clean up + (player.getState() as InProgressState).controllers.flow.transition("go"); + await playerPromise; }); }); diff --git a/devtools/plugins/profiler/core/src/addProfilerInterceptorsToHooks.ts b/devtools/plugins/profiler/core/src/addProfilerInterceptorsToHooks.ts index 063f597..7d69401 100644 --- a/devtools/plugins/profiler/core/src/addProfilerInterceptorsToHooks.ts +++ b/devtools/plugins/profiler/core/src/addProfilerInterceptorsToHooks.ts @@ -11,6 +11,7 @@ import { SyncWaterfallHook, } from "tapable-ts"; import { Profiler, ProfilerNode } from "./types"; +import { Logger } from "@player-ui/player"; /* Paths to hooks to ignore. * Currently ignoring "view" hook on player since it acts as a shortcut to the viewController's view hook. Including it would duplicate a lot of profiling work. @@ -30,18 +31,26 @@ type AnyHook = | SyncLoopHook | SyncWaterfallHook; +// const isAnyHook = (obj: unknown): obj is AnyHook => { +// return ( +// obj instanceof AsyncParallelBailHook || +// obj instanceof AsyncParallelHook || +// obj instanceof AsyncSeriesBailHook || +// obj instanceof AsyncSeriesHook || +// obj instanceof AsyncSeriesLoopHook || +// obj instanceof AsyncSeriesWaterfallHook || +// obj instanceof SyncBailHook || +// obj instanceof SyncHook || +// obj instanceof SyncLoopHook || +// obj instanceof SyncWaterfallHook +// ); +// }; + const isAnyHook = (obj: unknown): obj is AnyHook => { return ( - obj instanceof AsyncParallelBailHook || - obj instanceof AsyncParallelHook || - obj instanceof AsyncSeriesBailHook || - obj instanceof AsyncSeriesHook || - obj instanceof AsyncSeriesLoopHook || - obj instanceof AsyncSeriesWaterfallHook || - obj instanceof SyncBailHook || - obj instanceof SyncHook || - obj instanceof SyncLoopHook || - obj instanceof SyncWaterfallHook + isRecordType(obj) && + "intercept" in obj && + typeof obj.intercept === "function" ); }; diff --git a/devtools/plugins/profiler/core/src/helpers/__tests__/profiler.test.ts b/devtools/plugins/profiler/core/src/helpers/__tests__/profiler.test.ts index b7a47a5..ee301d7 100644 --- a/devtools/plugins/profiler/core/src/helpers/__tests__/profiler.test.ts +++ b/devtools/plugins/profiler/core/src/helpers/__tests__/profiler.test.ts @@ -35,70 +35,66 @@ describe("Profiler", () => { const rootNode = stopProfiler(); - expect(rootNode).toStrictEqual({ - durations: [ - { name: "process2", duration: "0.5000 ms" }, - { name: "process2.1", duration: "0.2000 ms" }, - { name: "process2.2", duration: "0.2000 ms" }, - { name: "process1", duration: "0.1000 ms" }, - ], - rootNode: { - children: [ - { - children: [], - endTime: 2490.2999999999997, - name: "process1", - startTime: 2490.2, - tooltip: "process1, 0.1000 (ms)", - value: 100, - }, - { - children: [ - { - children: [], - endTime: 2490.6999999999994, - name: "process2.1", - startTime: 2490.4999999999995, - tooltip: "process2.1, 0.2000 (ms)", - value: 200, - }, - { - children: [], - endTime: 2490.7999999999993, - name: "process2.2", - startTime: 2490.5999999999995, - tooltip: "process2.2, 0.2000 (ms)", - value: 200, - }, - ], - endTime: 2490.899999999999, - name: "process2", - startTime: 2490.3999999999996, - tooltip: "process2, 0.5000 (ms)", - value: 500, - }, - ], - endTime: 2490.999999999999, - name: "root", - startTime: 2490.1, - tooltip: "Profiler total time span 0.9000 (ms)", - value: 600, - }, - }); + expect(rootNode).toMatchSnapshot(); // (re)start start(); const { rootNode: rootNode2, durations } = stopProfiler(); expect(durations).toStrictEqual([]); + expect(rootNode2.children).toStrictEqual([]); + expect(rootNode2.tooltip).toMatch(/^Profiler total time span/); + }); - expect(rootNode2).toStrictEqual({ - name: "root", - endTime: 2491.199999999999, - startTime: 2491.099999999999, - tooltip: "Profiler total time span 0.1000 (ms)", - value: 100, - children: [], - }); + test("calls onUpdate only after endTimer, not startTimer", () => { + const onUpdate = vi.fn(); + const { startTimer, endTimer, start } = profiler(onUpdate); + + // startTimer("profiler") fires at construction but no longer triggers onUpdate + const callsAfterConstruction = onUpdate.mock.calls.length; + expect(callsAfterConstruction).toBe(0); + + start(); + // start() resets state but doesn't call onUpdate itself + expect(onUpdate.mock.calls.length).toBe(callsAfterConstruction); + + startTimer("hookA"); + // startTimer no longer calls onUpdate + expect(onUpdate.mock.calls.length).toBe(callsAfterConstruction); + + endTimer({ hookName: "hookA" }); + expect(onUpdate.mock.calls.length).toBe(callsAfterConstruction + 1); + + startTimer("hookB"); + endTimer({ hookName: "hookB" }); + expect(onUpdate.mock.calls.length).toBe(callsAfterConstruction + 2); + }); + + test("getSnapshot returns incrementally sorted durations without finalizing rootNode", () => { + const { startTimer, endTimer, getSnapshot, start } = profiler(); + + start(); + + startTimer("slow"); + endTimer({ hookName: "slow" }); + + startTimer("fast"); + endTimer({ hookName: "fast" }); + + const snap = getSnapshot(); + + // Should be sorted descending by duration — slow was first so it has a longer duration + expect(snap.durations[0]!.name).toBe("slow"); + expect(snap.durations[1]!.name).toBe("fast"); + + // rootNode should not have endTime/tooltip set yet (not finalized) + expect(snap.rootNode.tooltip).toBeUndefined(); + expect(snap.rootNode.endTime).toBeUndefined(); + expect(snap.rootNode.children).toHaveLength(2); + + // Snapshot is a clone — mutating the live tree doesn't affect it + startTimer("extra"); + endTimer({ hookName: "extra" }); + expect(snap.rootNode.children).toHaveLength(2); }); }); diff --git a/devtools/plugins/profiler/core/src/helpers/profiler.ts b/devtools/plugins/profiler/core/src/helpers/profiler.ts index b8e4a88..40cc9a6 100644 --- a/devtools/plugins/profiler/core/src/helpers/profiler.ts +++ b/devtools/plugins/profiler/core/src/helpers/profiler.ts @@ -4,7 +4,7 @@ const getNowTime = globalThis.performance ? () => globalThis.performance.now() : () => Date.now(); -export const profiler = (): Profiler => { +export const profiler = (onUpdate?: () => void): Profiler => { let rootNode: ProfilerNode = { name: "root", children: [], @@ -28,6 +28,32 @@ export const profiler = (): Profiler => { return newNode; }; + const cloneNode = (node: ProfilerNode): ProfilerNode => { + const children = node.children.map(cloneNode); + const value = + node.value ?? + children.reduce((prev, current) => prev + (current?.value ?? 0), 0); + + return { + ...node, + value, + children, + }; + }; + + const getSnapshot = (): { + rootNode: ProfilerNode; + durations: { name: string; duration: string }[]; + } => { + const sorted = [...durations] + .sort((a, b) => b.duration - a.duration) + .map(({ hookName, duration }) => ({ + name: hookName, + duration: `${duration.toFixed(4)} ms`, + })); + return { rootNode: cloneNode(rootNode), durations: sorted }; + }; + const startTimer = (hookName: string) => { const startTime = getNowTime(); @@ -75,6 +101,8 @@ export const profiler = (): Profiler => { // Push the hookName and duration into durations array durations.push({ hookName, duration: duration ? duration : 0.01 }); + onUpdate?.(); + return newNode; }; @@ -106,10 +134,15 @@ export const profiler = (): Profiler => { }; }; + // Start profiling immediately on construction + start(); + startTimer("profiler"); + return { start, startTimer, endTimer, stopProfiler, + getSnapshot, }; }; diff --git a/devtools/plugins/profiler/core/src/plugin-flow.json b/devtools/plugins/profiler/core/src/plugin-flow.json index 6e182f7..c37fd17 100755 --- a/devtools/plugins/profiler/core/src/plugin-flow.json +++ b/devtools/plugins/profiler/core/src/plugin-flow.json @@ -28,16 +28,31 @@ }, "main": { "asset": { - "id": "Profile-main", - "type": "object-inspector", - "binding": "durations", - "label": { - "asset": { - "id": "Profile-main-label", - "type": "text", - "value": "Profile" + "type": "collection", + "id": "Profile-collection", + "values": [ + { + "asset": { + "id": "Profile-title", + "type": "text", + "value": "Flame Graph:" + } + }, + { + "asset": { + "id": "Profile-main", + "type": "flame-graph", + "binding": "rootNode", + "label": { + "asset": { + "id": "Profile-main-label", + "type": "text", + "value": "Profile" + } + } + } } - } + ] } }, "footer": { @@ -71,217 +86,22 @@ }, "schema": { "ROOT": { - "playerConfig": { - "type": "RecordType" - }, - "flow": { - "type": "RecordType" - }, - "expression": { - "type": "StringType", - "default": "", - "validation": [ - { - "type": "string" - } - ], - "format": { - "type": "string" - } + "profiling": { + "type": "BooleanType" }, - "code": { - "type": "StringType", - "default": "", - "validation": [ - { - "type": "string" - } - ], - "format": { - "type": "string" - } + "displayFlameGraph": { + "type": "BooleanType" }, - "history": { - "type": "historyType", - "isArray": true + "rootNode": { + "type": "RecordType" }, - "logs": { - "type": "logsType", + "durations": { + "type": "durationType", "isArray": true } }, - "logsType": { - "id": { - "type": "StringType", - "default": "", - "validation": [ - { - "type": "string" - } - ], - "format": { - "type": "string" - } - }, - "time": { - "type": "StringType", - "default": "", - "validation": [ - { - "type": "string" - } - ], - "format": { - "type": "string" - } - }, - "type": { - "type": "StringType", - "default": "", - "validation": [ - { - "type": "string" - } - ], - "format": { - "type": "string" - } - }, - "message": { - "type": "StringType", - "default": "", - "validation": [ - { - "type": "string" - } - ], - "format": { - "type": "string" - } - }, - "severity": { - "type": "StringType", - "default": "", - "validation": [ - { - "type": "string" - } - ], - "format": { - "type": "string" - } - }, - "binding": { - "type": "StringType", - "default": "", - "validation": [ - { - "type": "string" - } - ], - "format": { - "type": "string" - } - }, - "from": { - "type": "StringType", - "default": "", - "validation": [ - { - "type": "string" - } - ], - "format": { - "type": "string" - } - }, - "to": { - "type": "StringType", - "default": "", - "validation": [ - { - "type": "string" - } - ], - "format": { - "type": "string" - } - }, - "state": { - "type": "StringType", - "default": "", - "validation": [ - { - "type": "string" - } - ], - "format": { - "type": "string" - } - }, - "error": { - "type": "StringType", - "default": "", - "validation": [ - { - "type": "string" - } - ], - "format": { - "type": "string" - } - }, - "outcome": { - "type": "StringType", - "default": "", - "validation": [ - { - "type": "string" - } - ], - "format": { - "type": "string" - } - }, - "metricType": { - "type": "StringType", - "default": "", - "validation": [ - { - "type": "string" - } - ], - "format": { - "type": "string" - } - } - }, - "historyType": { - "id": { - "type": "StringType", - "default": "", - "validation": [ - { - "type": "string" - } - ], - "format": { - "type": "string" - } - }, - "expression": { - "type": "StringType", - "default": "", - "validation": [ - { - "type": "string" - } - ], - "format": { - "type": "string" - } - }, - "result": { + "durationType": { + "name": { "type": "StringType", "default": "", "validation": [ @@ -293,7 +113,7 @@ "type": "string" } }, - "severity": { + "duration": { "type": "StringType", "default": "", "validation": [ @@ -308,10 +128,9 @@ } }, "data": { - "expression": "", - "flow": {}, - "history": [], - "logs": [], - "playerConfig": {} + "profiling": false, + "displayFlameGraph": false, + "rootNode": {}, + "durations": [] } } diff --git a/devtools/plugins/profiler/core/src/plugin.ts b/devtools/plugins/profiler/core/src/plugin.ts index 0050ac3..091e395 100644 --- a/devtools/plugins/profiler/core/src/plugin.ts +++ b/devtools/plugins/profiler/core/src/plugin.ts @@ -33,7 +33,9 @@ export class ProfilerDevtoolsPlugin extends DevtoolsPlugin { name = "ProfilerDevtoolsPlugin"; - startProfiler?: Profiler["start"]; + private profilerObj?: Profiler; + + startProfiler?: () => void; stopProfiler?: Profiler["stopProfiler"]; apply(player: Player): void { @@ -43,33 +45,62 @@ export class ProfilerDevtoolsPlugin extends DevtoolsPlugin { super.apply(player); - const profilerObj = profiler(); + // Wire live updates: dispatch to store on every startTimer/endTimer call + this.profilerObj = profiler(() => { + if (!this.profilerObj) return; + const { durations, rootNode } = this.profilerObj.getSnapshot(); + const newState = this.produceState( + [["plugins", pluginID, "flow", "data", "durations"], durations], + [["plugins", pluginID, "flow", "data", "rootNode"], rootNode] + ); + this.store.dispatch( + genDataChangeTransaction({ + playerID: this.playerID, + data: newState.plugins[pluginID]?.flow.data, + pluginID, + }) + ); + }); - this.stopProfiler = this.createProfilerStopFunction(player, profilerObj); - /** function to tap into hooks and start the profiler */ - this.startProfiler = this.createProfileStartFunction(player, profilerObj); - this.startProfiler(); - } + this.startProfiler = this.createProfileStartFunction(player); + this.stopProfiler = this.createProfilerStopFunction(player); + + // Hook once for the lifetime of this Player instance + addProfilerInterceptorsToHooks( + player, + this.profilerObj, + undefined, + undefined + ); + + // Dispatch initial profiling-active state + const initialState = produce(this.store.getState(), (draft) => { + dset(draft, ["plugins", pluginID, "flow", "data", "profiling"], true); + dset( + draft, + ["plugins", pluginID, "flow", "data", "displayFlameGraph"], + false + ); + }); - private createProfileStartFunction = ( - player: Player, - profilerObj: Profiler - ): Profiler["start"] => { - const { start, startTimer } = profilerObj; + this.store.dispatch( + genDataChangeTransaction({ + playerID: this.playerID, + data: initialState.plugins[pluginID]?.flow.data, + pluginID, + }) + ); + } + private createProfileStartFunction = (player: Player): (() => void) => { return () => { - player.logger.debug("[ProfilerPlugin]: Starting..."); - start(); - startTimer("profiler"); + if (!this.profilerObj) return; - addProfilerInterceptorsToHooks(player, profilerObj); + // Reset internal profiler state; interceptors remain on the hooks + this.profilerObj.start(); + this.profilerObj.startTimer("profiler"); const newState = produce(this.store.getState(), (draft) => { - dset(draft, ["plugins", pluginID, "flow", "data", "rootNode"], { - name: "root", - children: [], - }); - dset(draft, ["plugins", pluginID, "flow", "data", "durations"], []); dset(draft, ["plugins", pluginID, "flow", "data", "profiling"], true); dset( draft, @@ -78,23 +109,23 @@ export class ProfilerDevtoolsPlugin extends DevtoolsPlugin { ); }); - const transaction = genDataChangeTransaction({ - playerID: this.playerID, - data: newState.plugins[pluginID]?.flow.data, - pluginID, - }); - - this.store.dispatch(transaction); + this.store.dispatch( + genDataChangeTransaction({ + playerID: this.playerID, + data: newState.plugins[pluginID]?.flow.data, + pluginID, + }) + ); }; }; private createProfilerStopFunction = ( - player: Player, - profiler: Profiler + player: Player ): Profiler["stopProfiler"] => { return () => { - player.logger.debug("[ProfilerPlugin]: Stopping..."); - const { stopProfiler, endTimer } = profiler; + if (!this.profilerObj) + return { rootNode: { name: "root", children: [] }, durations: [] }; + const { stopProfiler, endTimer } = this.profilerObj; endTimer({ hookName: "profiler" }); const stopProfilerResult = stopProfiler(); const { rootNode, durations } = stopProfilerResult; @@ -106,13 +137,14 @@ export class ProfilerDevtoolsPlugin extends DevtoolsPlugin { [["plugins", pluginID, "flow", "data", "displayFlameGraph"], true] ); - const transaction = genDataChangeTransaction({ - playerID: this.playerID, - data: newState.plugins[pluginID]?.flow.data, - pluginID, - }); + this.store.dispatch( + genDataChangeTransaction({ + playerID: this.playerID, + data: newState.plugins[pluginID]?.flow.data, + pluginID, + }) + ); - this.store.dispatch(transaction); return stopProfilerResult; }; }; @@ -123,6 +155,7 @@ export class ProfilerDevtoolsPlugin extends DevtoolsPlugin { const { payload: { type }, } = interaction; + if (type === INTERACTIONS.START_PROFILING && this.startProfiler) { this.startProfiler(); } diff --git a/devtools/plugins/profiler/core/src/types.ts b/devtools/plugins/profiler/core/src/types.ts index a7077e9..ef2c08b 100644 --- a/devtools/plugins/profiler/core/src/types.ts +++ b/devtools/plugins/profiler/core/src/types.ts @@ -10,6 +10,10 @@ export interface Profiler { rootNode: ProfilerNode; durations: { name: string; duration: string }[]; }; + getSnapshot(): { + rootNode: ProfilerNode; + durations: { name: string; duration: string }[]; + }; } export type ProfilerNode = { From abedb3baa5dc2cba4055a2e28c370a3a3a07ca63 Mon Sep 17 00:00:00 2001 From: Thomas Marmer Date: Mon, 27 Apr 2026 14:19:22 -0400 Subject: [PATCH 04/10] add basic rendering for profiler data --- .../__snapshots__/plugin.test.ts.snap | 1046 +++++++++++++++++ .../addProfilerInterceptorsToHooks.test.ts | 60 + .../src/addProfilerInterceptorsToHooks.ts | 60 +- .../__snapshots__/profiler.test.ts.snap | 18 + .../src/helpers/__tests__/profiler.test.ts | 403 ++++++- .../profiler/core/src/helpers/profiler.ts | 183 +-- .../profiler/core/src/plugin-flow.json | 20 +- devtools/plugins/profiler/core/src/plugin.ts | 48 +- devtools/plugins/profiler/core/src/types.ts | 13 +- 9 files changed, 1653 insertions(+), 198 deletions(-) create mode 100644 devtools/plugins/profiler/core/src/__tests__/__snapshots__/plugin.test.ts.snap create mode 100644 devtools/plugins/profiler/core/src/__tests__/addProfilerInterceptorsToHooks.test.ts create mode 100644 devtools/plugins/profiler/core/src/helpers/__tests__/__snapshots__/profiler.test.ts.snap diff --git a/devtools/plugins/profiler/core/src/__tests__/__snapshots__/plugin.test.ts.snap b/devtools/plugins/profiler/core/src/__tests__/__snapshots__/plugin.test.ts.snap new file mode 100644 index 0000000..fc6b1f6 --- /dev/null +++ b/devtools/plugins/profiler/core/src/__tests__/__snapshots__/plugin.test.ts.snap @@ -0,0 +1,1046 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Plugin > should profile player hooks when navigating through a flow 1`] = ` +{ + "displayFlameGraph": true, + "durations": [ + { + "duration": "6.4000 ms", + "name": "transition", + }, + { + "duration": "4.9000 ms", + "name": "afterTransition", + }, + { + "duration": "2.5000 ms", + "name": "resolve", + }, + { + "duration": "0.4000 ms", + "name": "view", + }, + { + "duration": "0.4000 ms", + "name": "resolveDefaultValue", + }, + { + "duration": "0.1000 ms", + "name": "state", + }, + { + "duration": "0.1000 ms", + "name": "resolveFlowContent", + }, + { + "duration": "0.1000 ms", + "name": "onStart", + }, + { + "duration": "0.1000 ms", + "name": "flowController", + }, + { + "duration": "0.1000 ms", + "name": "bindingParser", + }, + { + "duration": "0.1000 ms", + "name": "schema", + }, + { + "duration": "0.1000 ms", + "name": "validationController", + }, + { + "duration": "0.1000 ms", + "name": "expressionEvaluator", + }, + { + "duration": "0.1000 ms", + "name": "dataController", + }, + { + "duration": "0.1000 ms", + "name": "viewController", + }, + { + "duration": "0.1000 ms", + "name": "state", + }, + { + "duration": "0.1000 ms", + "name": "flow", + }, + { + "duration": "0.1000 ms", + "name": "beforeStart", + }, + { + "duration": "0.1000 ms", + "name": "resolveTransitionNode", + }, + { + "duration": "0.1000 ms", + "name": "resolveView", + }, + { + "duration": "0.1000 ms", + "name": "onTemplatePluginCreated", + }, + { + "duration": "0.1000 ms", + "name": "templatePlugin", + }, + { + "duration": "0.1000 ms", + "name": "parser", + }, + { + "duration": "0.1000 ms", + "name": "parseNode", + }, + { + "duration": "0.1000 ms", + "name": "onParseObject", + }, + { + "duration": "0.1000 ms", + "name": "parseNode", + }, + { + "duration": "0.1000 ms", + "name": "parseNode", + }, + { + "duration": "0.1000 ms", + "name": "parseNode", + }, + { + "duration": "0.1000 ms", + "name": "onCreateASTNode", + }, + { + "duration": "0.1000 ms", + "name": "resolver", + }, + { + "duration": "0.1000 ms", + "name": "beforeUpdate", + }, + { + "duration": "0.1000 ms", + "name": "resolveOptions", + }, + { + "duration": "0.1000 ms", + "name": "skipResolve", + }, + { + "duration": "0.1000 ms", + "name": "beforeResolve", + }, + { + "duration": "0.1000 ms", + "name": "resolve", + }, + { + "duration": "0.1000 ms", + "name": "afterResolve", + }, + { + "duration": "0.1000 ms", + "name": "afterNodeUpdate", + }, + { + "duration": "0.1000 ms", + "name": "afterUpdate", + }, + { + "duration": "0.1000 ms", + "name": "onUpdate", + }, + { + "duration": "0.1000 ms", + "name": "afterTransition", + }, + { + "duration": "0.1000 ms", + "name": "skipTransition", + }, + { + "duration": "0.1000 ms", + "name": "beforeTransition", + }, + { + "duration": "0.1000 ms", + "name": "resolveTransitionNode", + }, + { + "duration": "0.1000 ms", + "name": "transition", + }, + { + "duration": "0.1000 ms", + "name": "resolveOptions", + }, + { + "duration": "0.1000 ms", + "name": "beforeEvaluate", + }, + { + "duration": "0.1000 ms", + "name": "resolve", + }, + { + "duration": "0.1000 ms", + "name": "skipOptimization", + }, + { + "duration": "0.1000 ms", + "name": "resolveDataStages", + }, + { + "duration": "0.1000 ms", + "name": "resolveTypeForBinding", + }, + { + "duration": "0.1000 ms", + "name": "onGet", + }, + { + "duration": "0.1000 ms", + "name": "onSet", + }, + { + "duration": "0.1000 ms", + "name": "onUpdate", + }, + { + "duration": "0.1000 ms", + "name": "skipTransition", + }, + { + "duration": "0.1000 ms", + "name": "beforeTransition", + }, + { + "duration": "0.1000 ms", + "name": "resolveTransitionNode", + }, + { + "duration": "0.1000 ms", + "name": "transition", + }, + { + "duration": "0.1000 ms", + "name": "afterTransition", + }, + { + "duration": "0.1000 ms", + "name": "onGet", + }, + { + "duration": "0.1000 ms", + "name": "serialize", + }, + { + "duration": "0.1000 ms", + "name": "state", + }, + ], + "profilerIndex": 0, + "profiling": false, + "rootNodes": [ + { + "children": [], + "endTime": 2490.2, + "name": "state", + "startTime": 2490.1, + "tooltip": "state, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2490.4999999999995, + "name": "resolveFlowContent", + "startTime": 2490.3999999999996, + "tooltip": "resolveFlowContent, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2490.7999999999993, + "name": "onStart", + "startTime": 2490.6999999999994, + "tooltip": "onStart, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2491.099999999999, + "name": "flowController", + "startTime": 2490.999999999999, + "tooltip": "flowController, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2491.3999999999987, + "name": "bindingParser", + "startTime": 2491.299999999999, + "tooltip": "bindingParser, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2491.6999999999985, + "name": "schema", + "startTime": 2491.5999999999985, + "tooltip": "schema, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2491.999999999998, + "name": "validationController", + "startTime": 2491.8999999999983, + "tooltip": "validationController, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2492.299999999998, + "name": "expressionEvaluator", + "startTime": 2492.199999999998, + "tooltip": "expressionEvaluator, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2492.5999999999976, + "name": "dataController", + "startTime": 2492.4999999999977, + "tooltip": "dataController, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2492.8999999999974, + "name": "viewController", + "startTime": 2492.7999999999975, + "tooltip": "viewController, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2493.199999999997, + "name": "state", + "startTime": 2493.099999999997, + "tooltip": "state, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2493.499999999997, + "name": "flow", + "startTime": 2493.399999999997, + "tooltip": "flow, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2493.7999999999965, + "name": "beforeStart", + "startTime": 2493.6999999999966, + "tooltip": "beforeStart, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2494.0999999999963, + "name": "resolveTransitionNode", + "startTime": 2493.9999999999964, + "tooltip": "resolveTransitionNode, 0.1000 (ms)", + "value": 100, + }, + { + "children": [ + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "value": 100, + }, + { + "children": [], + "endTime": 2494.499999999996, + "name": "resolveView", + "startTime": 2494.399999999996, + "tooltip": "resolveView, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "value": 200, + }, + { + "children": [ + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "value": 100, + }, + { + "children": [], + "endTime": 2494.8999999999955, + "name": "onTemplatePluginCreated", + "startTime": 2494.7999999999956, + "tooltip": "onTemplatePluginCreated, 0.1000 (ms)", + "value": 100, + }, + ], + "endTime": 2495.0999999999954, + "name": "view", + "startTime": 2494.6999999999957, + "tooltip": "view, 0.4000 (ms)", + "value": 400, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "value": 200, + }, + { + "children": [], + "endTime": 2495.399999999995, + "name": "templatePlugin", + "startTime": 2495.299999999995, + "tooltip": "templatePlugin, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "value": 200, + }, + { + "children": [], + "endTime": 2495.699999999995, + "name": "parser", + "startTime": 2495.599999999995, + "tooltip": "parser, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "value": 200, + }, + { + "children": [], + "endTime": 2495.9999999999945, + "name": "parseNode", + "startTime": 2495.8999999999946, + "tooltip": "parseNode, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "value": 200, + }, + { + "children": [], + "endTime": 2496.2999999999943, + "name": "onParseObject", + "startTime": 2496.1999999999944, + "tooltip": "onParseObject, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "value": 200, + }, + { + "children": [], + "endTime": 2496.599999999994, + "name": "parseNode", + "startTime": 2496.499999999994, + "tooltip": "parseNode, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "value": 200, + }, + { + "children": [], + "endTime": 2496.8999999999937, + "name": "parseNode", + "startTime": 2496.799999999994, + "tooltip": "parseNode, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "value": 200, + }, + { + "children": [], + "endTime": 2497.1999999999935, + "name": "parseNode", + "startTime": 2497.0999999999935, + "tooltip": "parseNode, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "value": 200, + }, + { + "children": [], + "endTime": 2497.499999999993, + "name": "onCreateASTNode", + "startTime": 2497.3999999999933, + "tooltip": "onCreateASTNode, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "value": 200, + }, + { + "children": [], + "endTime": 2497.799999999993, + "name": "resolver", + "startTime": 2497.699999999993, + "tooltip": "resolver, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "value": 200, + }, + { + "children": [], + "endTime": 2498.0999999999926, + "name": "beforeUpdate", + "startTime": 2497.9999999999927, + "tooltip": "beforeUpdate, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "value": 200, + }, + { + "children": [], + "endTime": 2498.3999999999924, + "name": "resolveOptions", + "startTime": 2498.2999999999925, + "tooltip": "resolveOptions, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "value": 200, + }, + { + "children": [], + "endTime": 2498.699999999992, + "name": "skipResolve", + "startTime": 2498.599999999992, + "tooltip": "skipResolve, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "value": 200, + }, + { + "children": [], + "endTime": 2498.999999999992, + "name": "beforeResolve", + "startTime": 2498.899999999992, + "tooltip": "beforeResolve, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "value": 200, + }, + { + "children": [], + "endTime": 2499.2999999999915, + "name": "resolve", + "startTime": 2499.1999999999916, + "tooltip": "resolve, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "value": 200, + }, + { + "children": [], + "endTime": 2499.5999999999913, + "name": "afterResolve", + "startTime": 2499.4999999999914, + "tooltip": "afterResolve, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "value": 200, + }, + { + "children": [], + "endTime": 2499.899999999991, + "name": "afterNodeUpdate", + "startTime": 2499.799999999991, + "tooltip": "afterNodeUpdate, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "value": 200, + }, + { + "children": [], + "endTime": 2500.1999999999907, + "name": "afterUpdate", + "startTime": 2500.099999999991, + "tooltip": "afterUpdate, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "value": 200, + }, + { + "children": [], + "endTime": 2500.4999999999905, + "name": "onUpdate", + "startTime": 2500.3999999999905, + "tooltip": "onUpdate, 0.1000 (ms)", + "value": 100, + }, + ], + "endTime": 2500.6999999999903, + "name": "transition", + "startTime": 2494.299999999996, + "tooltip": "transition, 6.4000 (ms)", + "value": 6400, + }, + { + "children": [], + "endTime": 2500.99999999999, + "name": "afterTransition", + "startTime": 2500.89999999999, + "tooltip": "afterTransition, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2501.2999999999897, + "name": "skipTransition", + "startTime": 2501.19999999999, + "tooltip": "skipTransition, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2501.5999999999894, + "name": "beforeTransition", + "startTime": 2501.4999999999895, + "tooltip": "beforeTransition, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2501.899999999989, + "name": "resolveTransitionNode", + "startTime": 2501.7999999999893, + "tooltip": "resolveTransitionNode, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2502.199999999989, + "name": "transition", + "startTime": 2502.099999999989, + "tooltip": "transition, 0.1000 (ms)", + "value": 100, + }, + { + "children": [ + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "value": 100, + }, + { + "children": [], + "endTime": 2502.5999999999885, + "name": "resolveOptions", + "startTime": 2502.4999999999886, + "tooltip": "resolveOptions, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "value": 200, + }, + { + "children": [], + "endTime": 2502.8999999999883, + "name": "beforeEvaluate", + "startTime": 2502.7999999999884, + "tooltip": "beforeEvaluate, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "value": 200, + }, + { + "children": [ + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "value": 100, + }, + { + "children": [], + "endTime": 2503.299999999988, + "name": "resolve", + "startTime": 2503.199999999988, + "tooltip": "resolve, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "value": 200, + }, + { + "children": [], + "endTime": 2503.5999999999876, + "name": "skipOptimization", + "startTime": 2503.4999999999877, + "tooltip": "skipOptimization, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "value": 200, + }, + { + "children": [], + "endTime": 2503.8999999999874, + "name": "resolveDataStages", + "startTime": 2503.7999999999874, + "tooltip": "resolveDataStages, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "value": 200, + }, + { + "children": [ + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "value": 100, + }, + { + "children": [], + "endTime": 2504.299999999987, + "name": "resolveTypeForBinding", + "startTime": 2504.199999999987, + "tooltip": "resolveTypeForBinding, 0.1000 (ms)", + "value": 100, + }, + ], + "endTime": 2504.499999999987, + "name": "resolveDefaultValue", + "startTime": 2504.099999999987, + "tooltip": "resolveDefaultValue, 0.4000 (ms)", + "value": 400, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "value": 200, + }, + { + "children": [], + "endTime": 2504.7999999999865, + "name": "onGet", + "startTime": 2504.6999999999866, + "tooltip": "onGet, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "value": 200, + }, + { + "children": [], + "endTime": 2505.0999999999863, + "name": "onSet", + "startTime": 2504.9999999999864, + "tooltip": "onSet, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "value": 200, + }, + { + "children": [], + "endTime": 2505.399999999986, + "name": "onUpdate", + "startTime": 2505.299999999986, + "tooltip": "onUpdate, 0.1000 (ms)", + "value": 100, + }, + ], + "endTime": 2505.599999999986, + "name": "resolve", + "startTime": 2503.099999999988, + "tooltip": "resolve, 2.5000 (ms)", + "value": 2500, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "value": 200, + }, + { + "children": [], + "endTime": 2505.8999999999855, + "name": "skipTransition", + "startTime": 2505.7999999999856, + "tooltip": "skipTransition, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "value": 200, + }, + { + "children": [], + "endTime": 2506.1999999999853, + "name": "beforeTransition", + "startTime": 2506.0999999999854, + "tooltip": "beforeTransition, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "value": 200, + }, + { + "children": [], + "endTime": 2506.499999999985, + "name": "resolveTransitionNode", + "startTime": 2506.399999999985, + "tooltip": "resolveTransitionNode, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "value": 200, + }, + { + "children": [], + "endTime": 2506.7999999999847, + "name": "transition", + "startTime": 2506.699999999985, + "tooltip": "transition, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "value": 200, + }, + { + "children": [], + "endTime": 2507.0999999999844, + "name": "afterTransition", + "startTime": 2506.9999999999845, + "tooltip": "afterTransition, 0.1000 (ms)", + "value": 100, + }, + ], + "endTime": 2507.2999999999843, + "name": "afterTransition", + "startTime": 2502.3999999999887, + "tooltip": "afterTransition, 4.9000 (ms)", + "value": 4900, + }, + { + "children": [], + "endTime": 2507.599999999984, + "name": "onGet", + "startTime": 2507.499999999984, + "tooltip": "onGet, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2507.8999999999837, + "name": "serialize", + "startTime": 2507.799999999984, + "tooltip": "serialize, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2508.1999999999834, + "name": "state", + "startTime": 2508.0999999999835, + "tooltip": "state, 0.1000 (ms)", + "value": 100, + }, + ], +} +`; diff --git a/devtools/plugins/profiler/core/src/__tests__/addProfilerInterceptorsToHooks.test.ts b/devtools/plugins/profiler/core/src/__tests__/addProfilerInterceptorsToHooks.test.ts new file mode 100644 index 0000000..8feca27 --- /dev/null +++ b/devtools/plugins/profiler/core/src/__tests__/addProfilerInterceptorsToHooks.test.ts @@ -0,0 +1,60 @@ +import { SyncHook } from "tapable-ts"; +import { describe, expect, test } from "vitest"; +import { addProfilerInterceptorsToHooks } from "../addProfilerInterceptorsToHooks"; +import { profiler } from "../helpers"; + +describe("addProfilerInterceptorsToHooks", () => { + /** + * When a parent hook's call interceptor fires, it recursively calls + * addProfilerInterceptorsToHooks on args[0] to discover nested hooks. + * If the parent hook fires again with the same child object, the intercepted + * WeakSet prevents a second interceptor from being added — so the child hook + * always fires exactly once per call regardless of how many times the parent + * hook has fired. + */ + test("re-intercepting the same child object on repeated parent calls does not duplicate timers", () => { + const profilerInstance = profiler(); + profilerInstance.start(); + + // Child object whose hooks get discovered lazily via the parent's call arg + const childObj = { + hooks: { + afterTransition: new SyncHook<[]>(), + }, + }; + + // Parent hook that passes childObj as its argument (mirrors Player's "flow" hook + // passing a FlowInstance, which carries its own hooks like afterTransition) + const parentObj = { + hooks: { + flow: new SyncHook<[typeof childObj]>(), + }, + }; + + addProfilerInterceptorsToHooks(parentObj, profilerInstance); + + // First parent call: wires one interceptor onto childObj.hooks.afterTransition + parentObj.hooks.flow.call(childObj); + childObj.hooks.afterTransition.call(); + + const snapAfterFirst = profilerInstance.getSnapshot(); + expect(snapAfterFirst.rootNodes).toHaveLength(2); + expect(snapAfterFirst.rootNodes[0]!.name).toBe("flow"); + expect(snapAfterFirst.rootNodes[1]!.name).toBe("afterTransition"); + expect(snapAfterFirst.rootNodes[1]!.children).toHaveLength(0); + + profilerInstance.start(); + + // Second parent call with the same childObj: the WeakSet guard prevents a second + // interceptor from being added to afterTransition — it fires exactly once. + parentObj.hooks.flow.call(childObj); + childObj.hooks.afterTransition.call(); + + const snapAfterSecond = profilerInstance.getSnapshot(); + const afterTransitionNode = snapAfterSecond.rootNodes.find( + (n) => n.name === "afterTransition", + ); + expect(afterTransitionNode).toBeDefined(); + expect(afterTransitionNode!.children).toHaveLength(0); + }); +}); diff --git a/devtools/plugins/profiler/core/src/addProfilerInterceptorsToHooks.ts b/devtools/plugins/profiler/core/src/addProfilerInterceptorsToHooks.ts index 7d69401..75746f2 100644 --- a/devtools/plugins/profiler/core/src/addProfilerInterceptorsToHooks.ts +++ b/devtools/plugins/profiler/core/src/addProfilerInterceptorsToHooks.ts @@ -10,8 +10,7 @@ import { SyncLoopHook, SyncWaterfallHook, } from "tapable-ts"; -import { Profiler, ProfilerNode } from "./types"; -import { Logger } from "@player-ui/player"; +import { Profiler } from "./types"; /* Paths to hooks to ignore. * Currently ignoring "view" hook on player since it acts as a shortcut to the viewController's view hook. Including it would duplicate a lot of profiling work. @@ -31,21 +30,6 @@ type AnyHook = | SyncLoopHook | SyncWaterfallHook; -// const isAnyHook = (obj: unknown): obj is AnyHook => { -// return ( -// obj instanceof AsyncParallelBailHook || -// obj instanceof AsyncParallelHook || -// obj instanceof AsyncSeriesBailHook || -// obj instanceof AsyncSeriesHook || -// obj instanceof AsyncSeriesLoopHook || -// obj instanceof AsyncSeriesWaterfallHook || -// obj instanceof SyncBailHook || -// obj instanceof SyncHook || -// obj instanceof SyncLoopHook || -// obj instanceof SyncWaterfallHook -// ); -// }; - const isAnyHook = (obj: unknown): obj is AnyHook => { return ( isRecordType(obj) && @@ -58,8 +42,8 @@ const isAnyHook = (obj: unknown): obj is AnyHook => { export const addProfilerInterceptorsToHooks = ( obj: unknown, profiler: Profiler, - getParent?: () => ProfilerNode, - currentPath: string[] = [] + currentPath: string[] = [], + intercepted: WeakSet = new WeakSet(), ): void => { if (!hasHooks(obj)) { return; @@ -71,49 +55,35 @@ export const addProfilerInterceptorsToHooks = ( const nextPath = [...currentPath, key]; if ( !isAnyHook(value) || - IGNORED_PATHS.some((path) => isMatchingPaths(path, nextPath)) + IGNORED_PATHS.some((path) => isMatchingPaths(path, nextPath)) || + intercepted.has(value) ) { return; } - let profilerNode: ProfilerNode = { - name: key, - children: [], - }; - - /** Since the object reference changing with `endTimer` calls needs to be kept for future parent references, use a function to get it. */ - const getNode = () => profilerNode; + intercepted.add(value); value.intercept({ call: (...args) => { - // Might want to also check if `value` is specifically a `SyncHook` since other hooks aren't providing anything with more tapable stuff. if (args.length > 0) { - addProfilerInterceptorsToHooks(args[0], profiler, getNode, nextPath); + addProfilerInterceptorsToHooks( + args[0], + profiler, + nextPath, + intercepted, + ); } startTimer(key); }, done: () => { - profilerNode = endTimer({ - hookName: key, - parentNode: getParent?.(), - children: profilerNode.children, - }); + endTimer({ hookName: key }); }, result: () => { - profilerNode = endTimer({ - hookName: key, - parentNode: getParent?.(), - children: profilerNode.children, - }); + endTimer({ hookName: key }); }, error: () => { - // TODO: Can we mark this as "interrupted" instead of ending the timer as normal? - profilerNode = endTimer({ - hookName: key, - parentNode: getParent?.(), - children: profilerNode.children, - }); + endTimer({ hookName: key }); }, }); }); diff --git a/devtools/plugins/profiler/core/src/helpers/__tests__/__snapshots__/profiler.test.ts.snap b/devtools/plugins/profiler/core/src/helpers/__tests__/__snapshots__/profiler.test.ts.snap new file mode 100644 index 0000000..5d64149 --- /dev/null +++ b/devtools/plugins/profiler/core/src/helpers/__tests__/__snapshots__/profiler.test.ts.snap @@ -0,0 +1,18 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Profiler > stopProfiler returns the full rootNodes forest with sorted durations 1`] = ` +[ + { + "duration": "0.3000 ms", + "name": "a", + }, + { + "duration": "0.1000 ms", + "name": "a.child", + }, + { + "duration": "0.1000 ms", + "name": "b", + }, +] +`; diff --git a/devtools/plugins/profiler/core/src/helpers/__tests__/profiler.test.ts b/devtools/plugins/profiler/core/src/helpers/__tests__/profiler.test.ts index ee301d7..e8fafd5 100644 --- a/devtools/plugins/profiler/core/src/helpers/__tests__/profiler.test.ts +++ b/devtools/plugins/profiler/core/src/helpers/__tests__/profiler.test.ts @@ -1,6 +1,5 @@ import { describe, expect, test, vi } from "vitest"; import { profiler } from "../profiler"; -import { ProfilerNode } from "../../types"; // mock performance.now let count = 2490.0; @@ -11,66 +10,135 @@ const now = vi.fn(() => { global.performance = { ...global.performance, now }; describe("Profiler", () => { - test("starts the profiler, keep track of the events, and return the profiler tree", () => { + test("sequential top-level timers each become a separate rootNodes entry", () => { const { startTimer, endTimer, stopProfiler, start } = profiler(); start(); - // process with no children - startTimer("process1"); - endTimer({ hookName: "process1" }); + startTimer("hookA"); + endTimer({ hookName: "hookA" }); + + startTimer("hookB"); + endTimer({ hookName: "hookB" }); + + const { rootNodes, durations } = stopProfiler(); + + expect(rootNodes).toHaveLength(2); + expect(rootNodes[0]!.name).toBe("hookA"); + expect(rootNodes[1]!.name).toBe("hookB"); + expect(durations).toHaveLength(2); + }); + + test("nested timers become children of the outer timer", () => { + const { startTimer, endTimer, stopProfiler, start } = profiler(); - // process with children - const parentNode: ProfilerNode = { - name: "process2", - children: [], - }; + start(); - startTimer("process2"); - startTimer("process2.1"); - startTimer("process2.2"); - endTimer({ hookName: "process2.1", parentNode }); - endTimer({ hookName: "process2.2", parentNode }); - endTimer({ hookName: "process2", children: parentNode.children }); + startTimer("outer"); + startTimer("inner1"); + endTimer({ hookName: "inner1" }); + startTimer("inner2"); + endTimer({ hookName: "inner2" }); + endTimer({ hookName: "outer" }); - const rootNode = stopProfiler(); + const { rootNodes } = stopProfiler(); - expect(rootNode).toMatchSnapshot(); + expect(rootNodes).toHaveLength(1); + expect(rootNodes[0]!.name).toBe("outer"); + const realChildren = rootNodes[0]!.children.filter( + (c) => c.name !== "(work)", + ); + expect(realChildren).toHaveLength(2); + expect(realChildren[0]!.name).toBe("inner1"); + expect(realChildren[1]!.name).toBe("inner2"); + }); + + test("endTimer for unknown name warns and does nothing", () => { + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + const { startTimer, endTimer, stopProfiler, start } = profiler(); - // (re)start start(); - const { rootNode: rootNode2, durations } = stopProfiler(); + startTimer("hookA"); + endTimer({ hookName: "unknown" }); - expect(durations).toStrictEqual([]); - expect(rootNode2.children).toStrictEqual([]); - expect(rootNode2.tooltip).toMatch(/^Profiler total time span/); + const { rootNodes } = stopProfiler(); + + // hookA is still on the stack — not finalized, so rootNodes has it but without endTime + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining("'unknown' not found in stack"), + ); + // hookA was started but never ended — it's still a root node (was pushed to rootNodes on start) + expect(rootNodes[0]!.name).toBe("hookA"); + expect(rootNodes[0]!.endTime).toBeUndefined(); + + warnSpy.mockRestore(); + }); + + test("endTimer for buried name pops and warns about intermediate timers", () => { + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + const { startTimer, endTimer, stopProfiler, start } = profiler(); + + start(); + + startTimer("outer"); + startTimer("middle"); + startTimer("inner"); + // End "outer" without ending "inner" or "middle" first + endTimer({ hookName: "outer" }); + + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining("popping 'inner'"), + ); + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining("popping 'middle'"), + ); + + const { rootNodes, durations } = stopProfiler(); + + // All three should be finalized + expect(rootNodes).toHaveLength(1); + expect(rootNodes[0]!.name).toBe("outer"); + expect(durations).toHaveLength(3); + + warnSpy.mockRestore(); + }); + + test("start() resets all state", () => { + const { startTimer, endTimer, stopProfiler, start } = profiler(); + + start(); + startTimer("hookA"); + endTimer({ hookName: "hookA" }); + + start(); + const { rootNodes, durations } = stopProfiler(); + + expect(rootNodes).toHaveLength(0); + expect(durations).toHaveLength(0); }); test("calls onUpdate only after endTimer, not startTimer", () => { const onUpdate = vi.fn(); const { startTimer, endTimer, start } = profiler(onUpdate); - // startTimer("profiler") fires at construction but no longer triggers onUpdate - const callsAfterConstruction = onUpdate.mock.calls.length; - expect(callsAfterConstruction).toBe(0); + // No auto-start, so no calls yet + expect(onUpdate.mock.calls.length).toBe(0); start(); - // start() resets state but doesn't call onUpdate itself - expect(onUpdate.mock.calls.length).toBe(callsAfterConstruction); + expect(onUpdate.mock.calls.length).toBe(0); startTimer("hookA"); - // startTimer no longer calls onUpdate - expect(onUpdate.mock.calls.length).toBe(callsAfterConstruction); + expect(onUpdate.mock.calls.length).toBe(0); endTimer({ hookName: "hookA" }); - expect(onUpdate.mock.calls.length).toBe(callsAfterConstruction + 1); + expect(onUpdate.mock.calls.length).toBe(1); startTimer("hookB"); endTimer({ hookName: "hookB" }); - expect(onUpdate.mock.calls.length).toBe(callsAfterConstruction + 2); + expect(onUpdate.mock.calls.length).toBe(2); }); - test("getSnapshot returns incrementally sorted durations without finalizing rootNode", () => { + test("getSnapshot returns sorted durations and a deep clone of rootNodes", () => { const { startTimer, endTimer, getSnapshot, start } = profiler(); start(); @@ -83,18 +151,273 @@ describe("Profiler", () => { const snap = getSnapshot(); - // Should be sorted descending by duration — slow was first so it has a longer duration + // Sorted descending by duration — slow was measured first so has larger elapsed expect(snap.durations[0]!.name).toBe("slow"); expect(snap.durations[1]!.name).toBe("fast"); - // rootNode should not have endTime/tooltip set yet (not finalized) - expect(snap.rootNode.tooltip).toBeUndefined(); - expect(snap.rootNode.endTime).toBeUndefined(); - expect(snap.rootNode.children).toHaveLength(2); + expect(snap.rootNodes).toHaveLength(2); - // Snapshot is a clone — mutating the live tree doesn't affect it + // Snapshot is a clone — adding to live tree doesn't affect it startTimer("extra"); endTimer({ hookName: "extra" }); - expect(snap.rootNode.children).toHaveLength(2); + expect(snap.rootNodes).toHaveLength(2); + }); + + test("getSnapshot sets endTime and value on in-flight nodes using current time", () => { + const { startTimer, endTimer, getSnapshot, start } = profiler(); + + start(); + + // Finish one node so we have a reference + startTimer("finished"); + endTimer({ hookName: "finished" }); + + // Leave this one in-flight + startTimer("inflight"); + + const snap = getSnapshot(); + + const finished = snap.rootNodes.find((n) => n.name === "finished"); + const inflight = snap.rootNodes.find((n) => n.name === "inflight"); + + // Finished node is unchanged + expect(finished!.endTime).toBeDefined(); + expect(finished!.value).toBeGreaterThan(0); + + // In-flight node gets a synthetic endTime and value based on snapshot time + expect(inflight!.endTime).toBeDefined(); + expect(inflight!.value).toBeGreaterThan(0); + + // Live node is not mutated + const { + startTimer: s2, + endTimer: e2, + getSnapshot: gs2, + start: st2, + } = profiler(); + st2(); + s2("live"); + const liveBefore = gs2().rootNodes[0]!; + expect(liveBefore.endTime).toBeDefined(); // snapshot sets it + // but the original node in the stack must remain without endTime + // (verified indirectly: calling endTimer still works normally) + e2({ hookName: "live" }); + const liveAfter = gs2().rootNodes[0]!; + expect(liveAfter.endTime).toBeDefined(); + expect(liveAfter.value).toBeGreaterThan(0); + }); + + describe("insertSpacers", () => { + test("no spacer when child starts exactly at parent startTime", () => { + const { insertSpacers } = profiler(); + + const node = { + name: "parent", + startTime: 100, + endTime: 200, + value: 100000, + children: [ + { + name: "child", + startTime: 100, + endTime: 150, + value: 50000, + children: [], + }, + ], + }; + + const result = insertSpacers(node); + expect(result.children).toHaveLength(1); + expect(result.children[0]!.name).toBe("child"); + }); + + test("leading spacer when first child starts after parent startTime", () => { + const { insertSpacers } = profiler(); + + const node = { + name: "parent", + startTime: 100, + endTime: 200, + value: 100000, + children: [ + { + name: "child", + startTime: 110, + endTime: 150, + value: 40000, + children: [], + }, + ], + }; + + const result = insertSpacers(node); + expect(result.children).toHaveLength(2); + expect(result.children[0]!.backgroundColor).toBe("#000000"); + expect(result.children[0]!.value).toBe(Math.ceil((110 - 100) * 1000)); + expect(result.children[1]!.name).toBe("child"); + }); + + test("spacer between siblings with a gap", () => { + const { insertSpacers } = profiler(); + + const node = { + name: "parent", + startTime: 100, + endTime: 200, + value: 100000, + children: [ + { + name: "child1", + startTime: 100, + endTime: 130, + value: 30000, + children: [], + }, + { + name: "child2", + startTime: 150, + endTime: 180, + value: 30000, + children: [], + }, + ], + }; + + const result = insertSpacers(node); + expect(result.children).toHaveLength(3); + expect(result.children[0]!.name).toBe("child1"); + expect(result.children[1]!.backgroundColor).toBe("#000000"); + expect(result.children[1]!.value).toBe(Math.ceil((150 - 130) * 1000)); + expect(result.children[2]!.name).toBe("child2"); + }); + + test("no spacer between siblings with no gap", () => { + const { insertSpacers } = profiler(); + + const node = { + name: "parent", + startTime: 100, + endTime: 200, + value: 100000, + children: [ + { + name: "child1", + startTime: 100, + endTime: 130, + value: 30000, + children: [], + }, + { + name: "child2", + startTime: 130, + endTime: 160, + value: 30000, + children: [], + }, + ], + }; + + const result = insertSpacers(node); + expect(result.children).toHaveLength(2); + expect(result.children[0]!.name).toBe("child1"); + expect(result.children[1]!.name).toBe("child2"); + }); + + test("spacers are inserted recursively into nested children", () => { + const { insertSpacers } = profiler(); + + const node = { + name: "parent", + startTime: 100, + endTime: 300, + value: 200000, + children: [ + { + name: "child", + startTime: 100, + endTime: 300, + value: 200000, + children: [ + { + name: "grandchild", + startTime: 150, + endTime: 200, + value: 50000, + children: [], + }, + ], + }, + ], + }; + + const result = insertSpacers(node); + const child = result.children[0]!; + expect(child.name).toBe("child"); + // grandchild has a leading spacer of 50ms + expect(child.children).toHaveLength(2); + expect(child.children[0]!.backgroundColor).toBe("#000000"); + expect(child.children[0]!.value).toBe(Math.ceil((150 - 100) * 1000)); + expect(child.children[1]!.name).toBe("grandchild"); + }); + + test("no spacers inserted when parent or child lacks timing info", () => { + const { insertSpacers } = profiler(); + + // Parent missing startTime — skip spacer logic, return node unchanged + const nodeNoStartTime = { + name: "parent", + endTime: 200, + value: 100000, + children: [ + { + name: "child", + startTime: 110, + endTime: 150, + value: 40000, + children: [], + }, + ], + }; + const r1 = insertSpacers(nodeNoStartTime); + expect(r1.children).toHaveLength(1); + expect(r1.children[0]!.name).toBe("child"); + + // Child missing startTime — skip that child's spacer, pass it through + const nodeChildNoTiming = { + name: "parent", + startTime: 100, + endTime: 200, + value: 100000, + children: [{ name: "child", children: [] }], + }; + const r2 = insertSpacers(nodeChildNoTiming); + expect(r2.children).toHaveLength(1); + expect(r2.children[0]!.name).toBe("child"); + }); + }); + + test("stopProfiler returns the full rootNodes forest with sorted durations", () => { + const { startTimer, endTimer, stopProfiler, start } = profiler(); + + start(); + + startTimer("a"); + startTimer("a.child"); + endTimer({ hookName: "a.child" }); + endTimer({ hookName: "a" }); + + startTimer("b"); + endTimer({ hookName: "b" }); + + const { rootNodes, durations } = stopProfiler(); + + expect(rootNodes).toHaveLength(2); + expect( + rootNodes[0]!.children.filter((c) => c.name !== "(work)"), + ).toHaveLength(1); + // durations sorted descending + expect(durations[0]!.name).toBe("a"); + expect(durations).toMatchSnapshot(); }); }); diff --git a/devtools/plugins/profiler/core/src/helpers/profiler.ts b/devtools/plugins/profiler/core/src/helpers/profiler.ts index 40cc9a6..a93f5b7 100644 --- a/devtools/plugins/profiler/core/src/helpers/profiler.ts +++ b/devtools/plugins/profiler/core/src/helpers/profiler.ts @@ -5,44 +5,82 @@ const getNowTime = globalThis.performance : () => Date.now(); export const profiler = (onUpdate?: () => void): Profiler => { - let rootNode: ProfilerNode = { - name: "root", - children: [], - }; - - let record: { [key: string]: number[] } = {}; + let rootNodes: ProfilerNode[] = []; + let stack: ProfilerNode[] = []; let durations: { hookName: string; duration: number }[] = []; const start = () => { - rootNode = { - name: "root", - startTime: getNowTime(), - children: [], - }; - record = {}; + rootNodes = []; + stack = []; durations = []; }; - const addNodeToTree = (newNode: ProfilerNode, parentNode: ProfilerNode) => { - parentNode.children.push(newNode); - return newNode; - }; - - const cloneNode = (node: ProfilerNode): ProfilerNode => { - const children = node.children.map(cloneNode); + const cloneNode = ( + node: ProfilerNode, + snapshotTime?: number + ): ProfilerNode => { + const children = node.children.map((c) => cloneNode(c, snapshotTime)); + const endTime = + node.endTime ?? (snapshotTime !== undefined ? snapshotTime : undefined); const value = node.value ?? - children.reduce((prev, current) => prev + (current?.value ?? 0), 0); + (endTime !== undefined && node.startTime !== undefined + ? Math.ceil((endTime - node.startTime) * 1000) + : children.reduce((prev, current) => prev + (current?.value ?? 0), 0)); return { ...node, + endTime, value, children, }; }; + /** + * Inserts synthetic spacer nodes to represent idle time between a parent's + * startTime and its first child, and between consecutive siblings. This makes + * the flame graph accurate to the real timeline rather than showing hooks + * back-to-back regardless of when they fired. + */ + const insertSpacers = (node: ProfilerNode): ProfilerNode => { + if ( + node.children.length === 0 || + node.startTime === undefined || + node.endTime === undefined + ) { + return { ...node }; + } + + const spacedChildren: ProfilerNode[] = []; + let cursor = node.startTime; + + for (const child of node.children) { + if (child.startTime === undefined || child.endTime === undefined) { + spacedChildren.push(insertSpacers(child)); + continue; + } + + const gap = child.startTime - cursor; + if (gap > 0) { + spacedChildren.push({ + name: "(work)", + value: Math.ceil(gap * 1000), + children: [], + backgroundColor: "#000000", + color: "#000000", + tooltip: "Placeholder time between hooks", + }); + } + + spacedChildren.push(insertSpacers(child)); + cursor = child.endTime; + } + + return { ...node, children: spacedChildren }; + }; + const getSnapshot = (): { - rootNode: ProfilerNode; + rootNodes: ProfilerNode[]; durations: { name: string; duration: string }[]; } => { const sorted = [...durations] @@ -51,82 +89,76 @@ export const profiler = (onUpdate?: () => void): Profiler => { name: hookName, duration: `${duration.toFixed(4)} ms`, })); - return { rootNode: cloneNode(rootNode), durations: sorted }; + const now = getNowTime(); + return { + rootNodes: rootNodes.map((n) => cloneNode(n, now)).map(insertSpacers), + durations: sorted, + }; }; const startTimer = (hookName: string) => { - const startTime = getNowTime(); + const node: ProfilerNode = { + name: hookName, + startTime: getNowTime(), + children: [], + }; - if (!record[hookName] || record[hookName].length === 2) { - record[hookName] = []; - record[hookName].push(startTime); + if (stack.length > 0) { + stack[stack.length - 1]!.children.push(node); + } else { + rootNodes.push(node); } + + stack.push(node); }; - const endTimer = ({ - hookName, - parentNode = rootNode, - children, - }: { - hookName: string; - parentNode?: ProfilerNode; - children?: ProfilerNode[]; - }) => { - let startTime: number | undefined; - let duration: number | undefined; + const finalizeNode = (node: ProfilerNode, endTime: number) => { + const duration = + node.startTime !== undefined ? endTime - node.startTime : 0.01; + node.endTime = endTime; + node.value = Math.ceil(duration * 1000); + node.tooltip = `${node.name}, ${duration.toFixed(4)} (ms)`; + durations.push({ hookName: node.name, duration }); + }; - const endTime = getNowTime(); + const endTimer = ({ hookName }: { hookName: string }) => { + const idx = [...stack].reverse().findIndex((n) => n.name === hookName); - for (const key in record) { - if (key === hookName && record[key]!.length === 1) { - [startTime] = record[key]!; - duration = endTime - startTime!; - record[key]!.push(endTime); - } + if (idx === -1) { + console.warn(`endTimer: '${hookName}' not found in stack, ignoring`); + return; } - const value = Math.ceil((duration || 0.01) * 1000); + // stack index of the target (reverse idx → forward idx) + const targetIdx = stack.length - 1 - idx; + const endTime = getNowTime(); - const newNode: ProfilerNode = { - name: hookName, - startTime, - endTime, - value, - tooltip: `${hookName}, ${(duration || 0.01).toFixed(4)} (ms)`, - children: children ?? [], - }; + // Pop and finalize everything above the target, from top down + for (let i = stack.length - 1; i > targetIdx; i--) { + const orphan = stack[i]!; + console.warn( + `endTimer: popping '${orphan.name}' — timer was never explicitly ended` + ); + finalizeNode(orphan, endTime); + } - addNodeToTree(newNode, parentNode); + // Finalize the target + finalizeNode(stack[targetIdx]!, endTime); - // Push the hookName and duration into durations array - durations.push({ hookName, duration: duration ? duration : 0.01 }); + // Truncate stack + stack.length = targetIdx; onUpdate?.(); - - return newNode; }; const stopProfiler = (): { - rootNode: ProfilerNode; + rootNodes: ProfilerNode[]; durations: { name: string; duration: string }[]; } => { - const endTime = getNowTime(); - const totalTime = endTime - (rootNode.startTime ?? 0); - - rootNode.endTime = endTime; - // set the stop profiler value is the sum of its children values - // otherwise the difference of width of the root and the other nodes - // make it impossible to see them into the flame graph - rootNode.value = - rootNode.children.reduce((acc, { value }) => (acc += value ?? 0), 0) || - Math.ceil((totalTime || 0.01) * 1000); - rootNode.tooltip = `Profiler total time span ${totalTime.toFixed(4)} (ms)`; - - // Sort durations array in descending order durations.sort((a, b) => b.duration - a.duration); return { - rootNode, + rootNodes: rootNodes.map(insertSpacers), durations: durations.map(({ hookName, duration }) => ({ name: hookName, duration: `${duration.toFixed(4)} ms`, @@ -134,15 +166,12 @@ export const profiler = (onUpdate?: () => void): Profiler => { }; }; - // Start profiling immediately on construction - start(); - startTimer("profiler"); - return { start, startTimer, endTimer, stopProfiler, getSnapshot, + insertSpacers, }; }; diff --git a/devtools/plugins/profiler/core/src/plugin-flow.json b/devtools/plugins/profiler/core/src/plugin-flow.json index c37fd17..5ebee65 100755 --- a/devtools/plugins/profiler/core/src/plugin-flow.json +++ b/devtools/plugins/profiler/core/src/plugin-flow.json @@ -38,11 +38,19 @@ "value": "Flame Graph:" } }, + { + "asset": { + "id": "Profile-index", + "binding": "profilerIndex", + "type": "input" + } + }, { "asset": { "id": "Profile-main", "type": "flame-graph", - "binding": "rootNode", + "binding": "rootNodes[{{profilerIndex}}]", + "width": "1500", "label": { "asset": { "id": "Profile-main-label", @@ -92,8 +100,9 @@ "displayFlameGraph": { "type": "BooleanType" }, - "rootNode": { - "type": "RecordType" + "rootNodes": { + "type": "RecordType", + "isArray": true }, "durations": { "type": "durationType", @@ -130,7 +139,8 @@ "data": { "profiling": false, "displayFlameGraph": false, - "rootNode": {}, - "durations": [] + "rootNodes": [], + "durations": [], + "profilerIndex": 0 } } diff --git a/devtools/plugins/profiler/core/src/plugin.ts b/devtools/plugins/profiler/core/src/plugin.ts index 091e395..6542926 100644 --- a/devtools/plugins/profiler/core/src/plugin.ts +++ b/devtools/plugins/profiler/core/src/plugin.ts @@ -45,20 +45,23 @@ export class ProfilerDevtoolsPlugin extends DevtoolsPlugin { super.apply(player); - // Wire live updates: dispatch to store on every startTimer/endTimer call + // Wire live updates: dispatch to store on every endTimer call this.profilerObj = profiler(() => { if (!this.profilerObj) return; - const { durations, rootNode } = this.profilerObj.getSnapshot(); + const { durations, rootNodes } = this.profilerObj.getSnapshot(); const newState = this.produceState( [["plugins", pluginID, "flow", "data", "durations"], durations], - [["plugins", pluginID, "flow", "data", "rootNode"], rootNode] + [ + ["plugins", pluginID, "flow", "data", "rootNodes"], + rootNodes.filter((x) => x.value !== undefined && x.value > 0), + ], ); this.store.dispatch( genDataChangeTransaction({ playerID: this.playerID, data: newState.plugins[pluginID]?.flow.data, pluginID, - }) + }), ); }); @@ -66,12 +69,7 @@ export class ProfilerDevtoolsPlugin extends DevtoolsPlugin { this.stopProfiler = this.createProfilerStopFunction(player); // Hook once for the lifetime of this Player instance - addProfilerInterceptorsToHooks( - player, - this.profilerObj, - undefined, - undefined - ); + addProfilerInterceptorsToHooks(player, this.profilerObj); // Dispatch initial profiling-active state const initialState = produce(this.store.getState(), (draft) => { @@ -79,7 +77,7 @@ export class ProfilerDevtoolsPlugin extends DevtoolsPlugin { dset( draft, ["plugins", pluginID, "flow", "data", "displayFlameGraph"], - false + false, ); }); @@ -88,24 +86,24 @@ export class ProfilerDevtoolsPlugin extends DevtoolsPlugin { playerID: this.playerID, data: initialState.plugins[pluginID]?.flow.data, pluginID, - }) + }), ); } private createProfileStartFunction = (player: Player): (() => void) => { return () => { if (!this.profilerObj) return; + player.logger.debug("[ProfilerPlugin]: Starting..."); // Reset internal profiler state; interceptors remain on the hooks this.profilerObj.start(); - this.profilerObj.startTimer("profiler"); const newState = produce(this.store.getState(), (draft) => { dset(draft, ["plugins", pluginID, "flow", "data", "profiling"], true); dset( draft, ["plugins", pluginID, "flow", "data", "displayFlameGraph"], - false + false, ); }); @@ -114,27 +112,29 @@ export class ProfilerDevtoolsPlugin extends DevtoolsPlugin { playerID: this.playerID, data: newState.plugins[pluginID]?.flow.data, pluginID, - }) + }), ); }; }; private createProfilerStopFunction = ( - player: Player + player: Player, ): Profiler["stopProfiler"] => { return () => { - if (!this.profilerObj) - return { rootNode: { name: "root", children: [] }, durations: [] }; - const { stopProfiler, endTimer } = this.profilerObj; - endTimer({ hookName: "profiler" }); + if (!this.profilerObj) return { rootNodes: [], durations: [] }; + player.logger.debug("[ProfilerPlugin]: Stopping..."); + const { stopProfiler } = this.profilerObj; const stopProfilerResult = stopProfiler(); - const { rootNode, durations } = stopProfilerResult; + const { rootNodes, durations } = stopProfilerResult; const newState = this.produceState( - [["plugins", pluginID, "flow", "data", "rootNode"], rootNode], + [ + ["plugins", pluginID, "flow", "data", "rootNodes"], + rootNodes.filter((x) => x.value !== undefined && x.value > 0), + ], [["plugins", pluginID, "flow", "data", "durations"], durations], [["plugins", pluginID, "flow", "data", "profiling"], false], - [["plugins", pluginID, "flow", "data", "displayFlameGraph"], true] + [["plugins", pluginID, "flow", "data", "displayFlameGraph"], true], ); this.store.dispatch( @@ -142,7 +142,7 @@ export class ProfilerDevtoolsPlugin extends DevtoolsPlugin { playerID: this.playerID, data: newState.plugins[pluginID]?.flow.data, pluginID, - }) + }), ); return stopProfilerResult; diff --git a/devtools/plugins/profiler/core/src/types.ts b/devtools/plugins/profiler/core/src/types.ts index ef2c08b..5d88db5 100644 --- a/devtools/plugins/profiler/core/src/types.ts +++ b/devtools/plugins/profiler/core/src/types.ts @@ -1,19 +1,16 @@ export interface Profiler { start(): void; startTimer(hookName: string): void; - endTimer(args: { - hookName: string; - parentNode?: ProfilerNode; - children?: ProfilerNode[]; - }): ProfilerNode; + endTimer(args: { hookName: string }): void; stopProfiler(): { - rootNode: ProfilerNode; + rootNodes: ProfilerNode[]; durations: { name: string; duration: string }[]; }; getSnapshot(): { - rootNode: ProfilerNode; + rootNodes: ProfilerNode[]; durations: { name: string; duration: string }[]; }; + insertSpacers(node: ProfilerNode): ProfilerNode; } export type ProfilerNode = { @@ -29,4 +26,6 @@ export type ProfilerNode = { tooltip?: string; /** subhook profiler nodes */ children: ProfilerNode[]; + backgroundColor?: string; + color?: string; }; From 3c49a0e35ff9700a2f71d35677084e47ce0776fa Mon Sep 17 00:00:00 2001 From: Thomas Marmer Date: Wed, 6 May 2026 13:32:19 -0400 Subject: [PATCH 05/10] centralize getNowTime and mock its usage --- .../plugin/core/src/helpers/getNowTime.ts | 3 + devtools/plugin/core/src/helpers/index.ts | 1 + devtools/plugin/core/src/helpers/uuid.ts | 2 +- .../__snapshots__/plugin.test.ts.snap | 74 +++++++++---------- .../core/src/__tests__/plugin.test.ts | 14 ++-- .../src/helpers/__tests__/profiler.test.ts | 14 ++-- .../profiler/core/src/helpers/profiler.ts | 9 +-- devtools/plugins/profiler/core/src/plugin.ts | 32 +++++++- 8 files changed, 92 insertions(+), 57 deletions(-) create mode 100644 devtools/plugin/core/src/helpers/getNowTime.ts diff --git a/devtools/plugin/core/src/helpers/getNowTime.ts b/devtools/plugin/core/src/helpers/getNowTime.ts new file mode 100644 index 0000000..fd3192d --- /dev/null +++ b/devtools/plugin/core/src/helpers/getNowTime.ts @@ -0,0 +1,3 @@ +export const getNowTime = globalThis.performance + ? () => globalThis.performance.now() + : () => Date.now(); diff --git a/devtools/plugin/core/src/helpers/index.ts b/devtools/plugin/core/src/helpers/index.ts index 13ceb14..18dd223 100644 --- a/devtools/plugin/core/src/helpers/index.ts +++ b/devtools/plugin/core/src/helpers/index.ts @@ -1,2 +1,3 @@ export { generateUUID } from "./uuid"; export { genDataChangeTransaction } from "./genDataChangeTransaction"; +export { getNowTime } from "./getNowTime"; diff --git a/devtools/plugin/core/src/helpers/uuid.ts b/devtools/plugin/core/src/helpers/uuid.ts index 497467b..3c0419f 100644 --- a/devtools/plugin/core/src/helpers/uuid.ts +++ b/devtools/plugin/core/src/helpers/uuid.ts @@ -1,4 +1,4 @@ -const getNowTime = globalThis.performance ? performance.now : Date.now; +import { getNowTime } from "./getNowTime"; // TODO: Either polyfill crypto or use this (pulled from SO) export function generateUUID(): string { diff --git a/devtools/plugins/profiler/core/src/__tests__/__snapshots__/plugin.test.ts.snap b/devtools/plugins/profiler/core/src/__tests__/__snapshots__/plugin.test.ts.snap index fc6b1f6..4aca4e6 100644 --- a/devtools/plugins/profiler/core/src/__tests__/__snapshots__/plugin.test.ts.snap +++ b/devtools/plugins/profiler/core/src/__tests__/__snapshots__/plugin.test.ts.snap @@ -371,7 +371,7 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] "children": [], "color": "#000000", "name": "(work)", - "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "tooltip": "Placeholder time between hooks", "value": 100, }, { @@ -387,7 +387,7 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] "children": [], "color": "#000000", "name": "(work)", - "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "tooltip": "Placeholder time between hooks", "value": 200, }, { @@ -397,7 +397,7 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] "children": [], "color": "#000000", "name": "(work)", - "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "tooltip": "Placeholder time between hooks", "value": 100, }, { @@ -420,7 +420,7 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] "children": [], "color": "#000000", "name": "(work)", - "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "tooltip": "Placeholder time between hooks", "value": 200, }, { @@ -436,7 +436,7 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] "children": [], "color": "#000000", "name": "(work)", - "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "tooltip": "Placeholder time between hooks", "value": 200, }, { @@ -452,7 +452,7 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] "children": [], "color": "#000000", "name": "(work)", - "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "tooltip": "Placeholder time between hooks", "value": 200, }, { @@ -468,7 +468,7 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] "children": [], "color": "#000000", "name": "(work)", - "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "tooltip": "Placeholder time between hooks", "value": 200, }, { @@ -484,7 +484,7 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] "children": [], "color": "#000000", "name": "(work)", - "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "tooltip": "Placeholder time between hooks", "value": 200, }, { @@ -500,7 +500,7 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] "children": [], "color": "#000000", "name": "(work)", - "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "tooltip": "Placeholder time between hooks", "value": 200, }, { @@ -516,7 +516,7 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] "children": [], "color": "#000000", "name": "(work)", - "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "tooltip": "Placeholder time between hooks", "value": 200, }, { @@ -532,7 +532,7 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] "children": [], "color": "#000000", "name": "(work)", - "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "tooltip": "Placeholder time between hooks", "value": 200, }, { @@ -548,7 +548,7 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] "children": [], "color": "#000000", "name": "(work)", - "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "tooltip": "Placeholder time between hooks", "value": 200, }, { @@ -564,7 +564,7 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] "children": [], "color": "#000000", "name": "(work)", - "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "tooltip": "Placeholder time between hooks", "value": 200, }, { @@ -580,7 +580,7 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] "children": [], "color": "#000000", "name": "(work)", - "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "tooltip": "Placeholder time between hooks", "value": 200, }, { @@ -596,7 +596,7 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] "children": [], "color": "#000000", "name": "(work)", - "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "tooltip": "Placeholder time between hooks", "value": 200, }, { @@ -612,7 +612,7 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] "children": [], "color": "#000000", "name": "(work)", - "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "tooltip": "Placeholder time between hooks", "value": 200, }, { @@ -628,7 +628,7 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] "children": [], "color": "#000000", "name": "(work)", - "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "tooltip": "Placeholder time between hooks", "value": 200, }, { @@ -644,7 +644,7 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] "children": [], "color": "#000000", "name": "(work)", - "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "tooltip": "Placeholder time between hooks", "value": 200, }, { @@ -660,7 +660,7 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] "children": [], "color": "#000000", "name": "(work)", - "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "tooltip": "Placeholder time between hooks", "value": 200, }, { @@ -676,7 +676,7 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] "children": [], "color": "#000000", "name": "(work)", - "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "tooltip": "Placeholder time between hooks", "value": 200, }, { @@ -692,7 +692,7 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] "children": [], "color": "#000000", "name": "(work)", - "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "tooltip": "Placeholder time between hooks", "value": 200, }, { @@ -757,7 +757,7 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] "children": [], "color": "#000000", "name": "(work)", - "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "tooltip": "Placeholder time between hooks", "value": 100, }, { @@ -773,7 +773,7 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] "children": [], "color": "#000000", "name": "(work)", - "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "tooltip": "Placeholder time between hooks", "value": 200, }, { @@ -789,7 +789,7 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] "children": [], "color": "#000000", "name": "(work)", - "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "tooltip": "Placeholder time between hooks", "value": 200, }, { @@ -799,7 +799,7 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] "children": [], "color": "#000000", "name": "(work)", - "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "tooltip": "Placeholder time between hooks", "value": 100, }, { @@ -815,7 +815,7 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] "children": [], "color": "#000000", "name": "(work)", - "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "tooltip": "Placeholder time between hooks", "value": 200, }, { @@ -831,7 +831,7 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] "children": [], "color": "#000000", "name": "(work)", - "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "tooltip": "Placeholder time between hooks", "value": 200, }, { @@ -847,7 +847,7 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] "children": [], "color": "#000000", "name": "(work)", - "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "tooltip": "Placeholder time between hooks", "value": 200, }, { @@ -857,7 +857,7 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] "children": [], "color": "#000000", "name": "(work)", - "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "tooltip": "Placeholder time between hooks", "value": 100, }, { @@ -880,7 +880,7 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] "children": [], "color": "#000000", "name": "(work)", - "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "tooltip": "Placeholder time between hooks", "value": 200, }, { @@ -896,7 +896,7 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] "children": [], "color": "#000000", "name": "(work)", - "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "tooltip": "Placeholder time between hooks", "value": 200, }, { @@ -912,7 +912,7 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] "children": [], "color": "#000000", "name": "(work)", - "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "tooltip": "Placeholder time between hooks", "value": 200, }, { @@ -935,7 +935,7 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] "children": [], "color": "#000000", "name": "(work)", - "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "tooltip": "Placeholder time between hooks", "value": 200, }, { @@ -951,7 +951,7 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] "children": [], "color": "#000000", "name": "(work)", - "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "tooltip": "Placeholder time between hooks", "value": 200, }, { @@ -967,7 +967,7 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] "children": [], "color": "#000000", "name": "(work)", - "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "tooltip": "Placeholder time between hooks", "value": 200, }, { @@ -983,7 +983,7 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] "children": [], "color": "#000000", "name": "(work)", - "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "tooltip": "Placeholder time between hooks", "value": 200, }, { @@ -999,7 +999,7 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] "children": [], "color": "#000000", "name": "(work)", - "tooltip": "This interval does not represent a single unit of work — it covers untracked time between hooks.", + "tooltip": "Placeholder time between hooks", "value": 200, }, { diff --git a/devtools/plugins/profiler/core/src/__tests__/plugin.test.ts b/devtools/plugins/profiler/core/src/__tests__/plugin.test.ts index 3d0c1bf..05196da 100644 --- a/devtools/plugins/profiler/core/src/__tests__/plugin.test.ts +++ b/devtools/plugins/profiler/core/src/__tests__/plugin.test.ts @@ -2,13 +2,17 @@ import { Flow, InProgressState, Player } from "@player-ui/player"; import { describe, expect, test, vi } from "vitest"; import { ProfilerDevtoolsPlugin } from "../plugin"; -// mock performance.now let count = 2490.0; -const now = vi.fn(() => { - count += 0.1; - return count; +vi.mock("@player-devtools/plugin", async () => { + const actual = await vi.importActual("@player-devtools/plugin"); + return { + ...actual, + getNowTime: vi.fn(() => { + count += 0.1; + return count; + }), + }; }); -global.performance = { ...global.performance, now }; describe("Plugin", () => { // This test is being used to setup a baseline snapshot of perf on a basic player flow. diff --git a/devtools/plugins/profiler/core/src/helpers/__tests__/profiler.test.ts b/devtools/plugins/profiler/core/src/helpers/__tests__/profiler.test.ts index e8fafd5..0aeaede 100644 --- a/devtools/plugins/profiler/core/src/helpers/__tests__/profiler.test.ts +++ b/devtools/plugins/profiler/core/src/helpers/__tests__/profiler.test.ts @@ -1,13 +1,17 @@ import { describe, expect, test, vi } from "vitest"; import { profiler } from "../profiler"; -// mock performance.now let count = 2490.0; -const now = vi.fn(() => { - count += 0.1; - return count; +vi.mock("@player-devtools/plugin", async () => { + const actual = await vi.importActual("@player-devtools/plugin"); + return { + ...actual, + getNowTime: vi.fn(() => { + count += 0.1; + return count; + }), + }; }); -global.performance = { ...global.performance, now }; describe("Profiler", () => { test("sequential top-level timers each become a separate rootNodes entry", () => { diff --git a/devtools/plugins/profiler/core/src/helpers/profiler.ts b/devtools/plugins/profiler/core/src/helpers/profiler.ts index a93f5b7..e21b823 100644 --- a/devtools/plugins/profiler/core/src/helpers/profiler.ts +++ b/devtools/plugins/profiler/core/src/helpers/profiler.ts @@ -1,8 +1,5 @@ import type { Profiler, ProfilerNode } from "../types"; - -const getNowTime = globalThis.performance - ? () => globalThis.performance.now() - : () => Date.now(); +import { getNowTime } from "@player-devtools/plugin"; export const profiler = (onUpdate?: () => void): Profiler => { let rootNodes: ProfilerNode[] = []; @@ -17,7 +14,7 @@ export const profiler = (onUpdate?: () => void): Profiler => { const cloneNode = ( node: ProfilerNode, - snapshotTime?: number + snapshotTime?: number, ): ProfilerNode => { const children = node.children.map((c) => cloneNode(c, snapshotTime)); const endTime = @@ -137,7 +134,7 @@ export const profiler = (onUpdate?: () => void): Profiler => { for (let i = stack.length - 1; i > targetIdx; i--) { const orphan = stack[i]!; console.warn( - `endTimer: popping '${orphan.name}' — timer was never explicitly ended` + `endTimer: popping '${orphan.name}' — timer was never explicitly ended`, ); finalizeNode(orphan, endTime); } diff --git a/devtools/plugins/profiler/core/src/plugin.ts b/devtools/plugins/profiler/core/src/plugin.ts index 6542926..b2a50f2 100644 --- a/devtools/plugins/profiler/core/src/plugin.ts +++ b/devtools/plugins/profiler/core/src/plugin.ts @@ -12,7 +12,7 @@ import { dset } from "dset/merge"; import { produce } from "immer"; import { BASE_PLUGIN_DATA, INTERACTIONS } from "./constants"; import { profiler } from "./helpers"; -import type { Profiler } from "./types"; +import type { Profiler, ProfilerNode } from "./types"; import { addProfilerInterceptorsToHooks } from "./addProfilerInterceptorsToHooks"; import flow from "./plugin-flow.json"; @@ -38,6 +38,32 @@ export class ProfilerDevtoolsPlugin extends DevtoolsPlugin { startProfiler?: () => void; stopProfiler?: Profiler["stopProfiler"]; + private transformProfilerData(nodes: ProfilerNode[]): ProfilerNode[] { + let previous: ProfilerNode | undefined; + const result: ProfilerNode[] = []; + + for (const node of nodes) { + if (node.value === undefined || node.value <= 0) { + continue; + } + + if (node.name === "(work)" && previous?.name === "(work)") { + previous.value = (previous.value ?? 0) + node.value; + previous.endTime = node.endTime; + continue; + } + + previous = { + ...node, + children: this.transformProfilerData(node.children), + }; + + result.push(previous); + } + + return result; + } + apply(player: Player): void { if (!this.checkIfDevtoolsIsActive()) { return; @@ -53,7 +79,7 @@ export class ProfilerDevtoolsPlugin extends DevtoolsPlugin { [["plugins", pluginID, "flow", "data", "durations"], durations], [ ["plugins", pluginID, "flow", "data", "rootNodes"], - rootNodes.filter((x) => x.value !== undefined && x.value > 0), + this.transformProfilerData(rootNodes), ], ); this.store.dispatch( @@ -130,7 +156,7 @@ export class ProfilerDevtoolsPlugin extends DevtoolsPlugin { const newState = this.produceState( [ ["plugins", pluginID, "flow", "data", "rootNodes"], - rootNodes.filter((x) => x.value !== undefined && x.value > 0), + this.transformProfilerData(rootNodes), ], [["plugins", pluginID, "flow", "data", "durations"], durations], [["plugins", pluginID, "flow", "data", "profiling"], false], From 091bf62fd432bbe90e67c7d7351f68504724a40b Mon Sep 17 00:00:00 2001 From: Thomas Marmer Date: Thu, 7 May 2026 14:17:35 -0400 Subject: [PATCH 06/10] return to using rootNode in plugin. code cleanup --- .../__snapshots__/plugin.test.ts.snap | 1773 +++++++++-------- .../addProfilerInterceptorsToHooks.test.ts | 4 +- .../core/src/__tests__/plugin.test.ts | 6 +- .../src/addProfilerInterceptorsToHooks.ts | 17 +- .../src/helpers/__tests__/profiler.test.ts | 350 +--- .../profiler/core/src/helpers/index.ts | 1 + .../profiler/core/src/helpers/profiler.ts | 149 +- .../core/src/helpers/transformProfilerData.ts | 51 + .../profiler/core/src/plugin-flow.json | 18 +- devtools/plugins/profiler/core/src/plugin.ts | 207 +- devtools/plugins/profiler/core/src/types.ts | 15 - 11 files changed, 1266 insertions(+), 1325 deletions(-) create mode 100644 devtools/plugins/profiler/core/src/helpers/transformProfilerData.ts diff --git a/devtools/plugins/profiler/core/src/__tests__/__snapshots__/plugin.test.ts.snap b/devtools/plugins/profiler/core/src/__tests__/__snapshots__/plugin.test.ts.snap index 4aca4e6..16504a8 100644 --- a/devtools/plugins/profiler/core/src/__tests__/__snapshots__/plugin.test.ts.snap +++ b/devtools/plugins/profiler/core/src/__tests__/__snapshots__/plugin.test.ts.snap @@ -249,798 +249,987 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] "name": "state", }, ], - "profilerIndex": 0, "profiling": false, - "rootNodes": [ - { - "children": [], - "endTime": 2490.2, - "name": "state", - "startTime": 2490.1, - "tooltip": "state, 0.1000 (ms)", - "value": 100, - }, - { - "children": [], - "endTime": 2490.4999999999995, - "name": "resolveFlowContent", - "startTime": 2490.3999999999996, - "tooltip": "resolveFlowContent, 0.1000 (ms)", - "value": 100, - }, - { - "children": [], - "endTime": 2490.7999999999993, - "name": "onStart", - "startTime": 2490.6999999999994, - "tooltip": "onStart, 0.1000 (ms)", - "value": 100, - }, - { - "children": [], - "endTime": 2491.099999999999, - "name": "flowController", - "startTime": 2490.999999999999, - "tooltip": "flowController, 0.1000 (ms)", - "value": 100, - }, - { - "children": [], - "endTime": 2491.3999999999987, - "name": "bindingParser", - "startTime": 2491.299999999999, - "tooltip": "bindingParser, 0.1000 (ms)", - "value": 100, - }, - { - "children": [], - "endTime": 2491.6999999999985, - "name": "schema", - "startTime": 2491.5999999999985, - "tooltip": "schema, 0.1000 (ms)", - "value": 100, - }, - { - "children": [], - "endTime": 2491.999999999998, - "name": "validationController", - "startTime": 2491.8999999999983, - "tooltip": "validationController, 0.1000 (ms)", - "value": 100, - }, - { - "children": [], - "endTime": 2492.299999999998, - "name": "expressionEvaluator", - "startTime": 2492.199999999998, - "tooltip": "expressionEvaluator, 0.1000 (ms)", - "value": 100, - }, - { - "children": [], - "endTime": 2492.5999999999976, - "name": "dataController", - "startTime": 2492.4999999999977, - "tooltip": "dataController, 0.1000 (ms)", - "value": 100, - }, - { - "children": [], - "endTime": 2492.8999999999974, - "name": "viewController", - "startTime": 2492.7999999999975, - "tooltip": "viewController, 0.1000 (ms)", - "value": 100, - }, - { - "children": [], - "endTime": 2493.199999999997, - "name": "state", - "startTime": 2493.099999999997, - "tooltip": "state, 0.1000 (ms)", - "value": 100, - }, - { - "children": [], - "endTime": 2493.499999999997, - "name": "flow", - "startTime": 2493.399999999997, - "tooltip": "flow, 0.1000 (ms)", - "value": 100, - }, - { - "children": [], - "endTime": 2493.7999999999965, - "name": "beforeStart", - "startTime": 2493.6999999999966, - "tooltip": "beforeStart, 0.1000 (ms)", - "value": 100, - }, - { - "children": [], - "endTime": 2494.0999999999963, - "name": "resolveTransitionNode", - "startTime": 2493.9999999999964, - "tooltip": "resolveTransitionNode, 0.1000 (ms)", - "value": 100, - }, - { - "children": [ - { - "backgroundColor": "#000000", - "children": [], - "color": "#000000", - "name": "(work)", - "tooltip": "Placeholder time between hooks", - "value": 100, - }, - { - "children": [], - "endTime": 2494.499999999996, - "name": "resolveView", - "startTime": 2494.399999999996, - "tooltip": "resolveView, 0.1000 (ms)", - "value": 100, - }, - { - "backgroundColor": "#000000", - "children": [], - "color": "#000000", - "name": "(work)", - "tooltip": "Placeholder time between hooks", - "value": 200, - }, - { - "children": [ - { - "backgroundColor": "#000000", - "children": [], - "color": "#000000", - "name": "(work)", - "tooltip": "Placeholder time between hooks", - "value": 100, - }, - { - "children": [], - "endTime": 2494.8999999999955, - "name": "onTemplatePluginCreated", - "startTime": 2494.7999999999956, - "tooltip": "onTemplatePluginCreated, 0.1000 (ms)", - "value": 100, - }, - ], - "endTime": 2495.0999999999954, - "name": "view", - "startTime": 2494.6999999999957, - "tooltip": "view, 0.4000 (ms)", - "value": 400, - }, - { - "backgroundColor": "#000000", - "children": [], - "color": "#000000", - "name": "(work)", - "tooltip": "Placeholder time between hooks", - "value": 200, - }, - { - "children": [], - "endTime": 2495.399999999995, - "name": "templatePlugin", - "startTime": 2495.299999999995, - "tooltip": "templatePlugin, 0.1000 (ms)", - "value": 100, - }, - { - "backgroundColor": "#000000", - "children": [], - "color": "#000000", - "name": "(work)", - "tooltip": "Placeholder time between hooks", - "value": 200, - }, - { - "children": [], - "endTime": 2495.699999999995, - "name": "parser", - "startTime": 2495.599999999995, - "tooltip": "parser, 0.1000 (ms)", - "value": 100, - }, - { - "backgroundColor": "#000000", - "children": [], - "color": "#000000", - "name": "(work)", - "tooltip": "Placeholder time between hooks", - "value": 200, - }, - { - "children": [], - "endTime": 2495.9999999999945, - "name": "parseNode", - "startTime": 2495.8999999999946, - "tooltip": "parseNode, 0.1000 (ms)", - "value": 100, - }, - { - "backgroundColor": "#000000", - "children": [], - "color": "#000000", - "name": "(work)", - "tooltip": "Placeholder time between hooks", - "value": 200, - }, - { - "children": [], - "endTime": 2496.2999999999943, - "name": "onParseObject", - "startTime": 2496.1999999999944, - "tooltip": "onParseObject, 0.1000 (ms)", - "value": 100, - }, - { - "backgroundColor": "#000000", - "children": [], - "color": "#000000", - "name": "(work)", - "tooltip": "Placeholder time between hooks", - "value": 200, - }, - { - "children": [], - "endTime": 2496.599999999994, - "name": "parseNode", - "startTime": 2496.499999999994, - "tooltip": "parseNode, 0.1000 (ms)", - "value": 100, - }, - { - "backgroundColor": "#000000", - "children": [], - "color": "#000000", - "name": "(work)", - "tooltip": "Placeholder time between hooks", - "value": 200, - }, - { - "children": [], - "endTime": 2496.8999999999937, - "name": "parseNode", - "startTime": 2496.799999999994, - "tooltip": "parseNode, 0.1000 (ms)", - "value": 100, - }, - { - "backgroundColor": "#000000", - "children": [], - "color": "#000000", - "name": "(work)", - "tooltip": "Placeholder time between hooks", - "value": 200, - }, - { - "children": [], - "endTime": 2497.1999999999935, - "name": "parseNode", - "startTime": 2497.0999999999935, - "tooltip": "parseNode, 0.1000 (ms)", - "value": 100, - }, - { - "backgroundColor": "#000000", - "children": [], - "color": "#000000", - "name": "(work)", - "tooltip": "Placeholder time between hooks", - "value": 200, - }, - { - "children": [], - "endTime": 2497.499999999993, - "name": "onCreateASTNode", - "startTime": 2497.3999999999933, - "tooltip": "onCreateASTNode, 0.1000 (ms)", - "value": 100, - }, - { - "backgroundColor": "#000000", - "children": [], - "color": "#000000", - "name": "(work)", - "tooltip": "Placeholder time between hooks", - "value": 200, - }, - { - "children": [], - "endTime": 2497.799999999993, - "name": "resolver", - "startTime": 2497.699999999993, - "tooltip": "resolver, 0.1000 (ms)", - "value": 100, - }, - { - "backgroundColor": "#000000", - "children": [], - "color": "#000000", - "name": "(work)", - "tooltip": "Placeholder time between hooks", - "value": 200, - }, - { - "children": [], - "endTime": 2498.0999999999926, - "name": "beforeUpdate", - "startTime": 2497.9999999999927, - "tooltip": "beforeUpdate, 0.1000 (ms)", - "value": 100, - }, - { - "backgroundColor": "#000000", - "children": [], - "color": "#000000", - "name": "(work)", - "tooltip": "Placeholder time between hooks", - "value": 200, - }, - { - "children": [], - "endTime": 2498.3999999999924, - "name": "resolveOptions", - "startTime": 2498.2999999999925, - "tooltip": "resolveOptions, 0.1000 (ms)", - "value": 100, - }, - { - "backgroundColor": "#000000", - "children": [], - "color": "#000000", - "name": "(work)", - "tooltip": "Placeholder time between hooks", - "value": 200, - }, - { - "children": [], - "endTime": 2498.699999999992, - "name": "skipResolve", - "startTime": 2498.599999999992, - "tooltip": "skipResolve, 0.1000 (ms)", - "value": 100, - }, - { - "backgroundColor": "#000000", - "children": [], - "color": "#000000", - "name": "(work)", - "tooltip": "Placeholder time between hooks", - "value": 200, - }, - { - "children": [], - "endTime": 2498.999999999992, - "name": "beforeResolve", - "startTime": 2498.899999999992, - "tooltip": "beforeResolve, 0.1000 (ms)", - "value": 100, - }, - { - "backgroundColor": "#000000", - "children": [], - "color": "#000000", - "name": "(work)", - "tooltip": "Placeholder time between hooks", - "value": 200, - }, - { - "children": [], - "endTime": 2499.2999999999915, - "name": "resolve", - "startTime": 2499.1999999999916, - "tooltip": "resolve, 0.1000 (ms)", - "value": 100, - }, - { - "backgroundColor": "#000000", - "children": [], - "color": "#000000", - "name": "(work)", - "tooltip": "Placeholder time between hooks", - "value": 200, - }, - { - "children": [], - "endTime": 2499.5999999999913, - "name": "afterResolve", - "startTime": 2499.4999999999914, - "tooltip": "afterResolve, 0.1000 (ms)", - "value": 100, - }, - { - "backgroundColor": "#000000", - "children": [], - "color": "#000000", - "name": "(work)", - "tooltip": "Placeholder time between hooks", - "value": 200, - }, - { - "children": [], - "endTime": 2499.899999999991, - "name": "afterNodeUpdate", - "startTime": 2499.799999999991, - "tooltip": "afterNodeUpdate, 0.1000 (ms)", - "value": 100, - }, - { - "backgroundColor": "#000000", - "children": [], - "color": "#000000", - "name": "(work)", - "tooltip": "Placeholder time between hooks", - "value": 200, - }, - { - "children": [], - "endTime": 2500.1999999999907, - "name": "afterUpdate", - "startTime": 2500.099999999991, - "tooltip": "afterUpdate, 0.1000 (ms)", - "value": 100, - }, - { - "backgroundColor": "#000000", - "children": [], - "color": "#000000", - "name": "(work)", - "tooltip": "Placeholder time between hooks", - "value": 200, - }, - { - "children": [], - "endTime": 2500.4999999999905, - "name": "onUpdate", - "startTime": 2500.3999999999905, - "tooltip": "onUpdate, 0.1000 (ms)", - "value": 100, - }, - ], - "endTime": 2500.6999999999903, - "name": "transition", - "startTime": 2494.299999999996, - "tooltip": "transition, 6.4000 (ms)", - "value": 6400, - }, - { - "children": [], - "endTime": 2500.99999999999, - "name": "afterTransition", - "startTime": 2500.89999999999, - "tooltip": "afterTransition, 0.1000 (ms)", - "value": 100, - }, - { - "children": [], - "endTime": 2501.2999999999897, - "name": "skipTransition", - "startTime": 2501.19999999999, - "tooltip": "skipTransition, 0.1000 (ms)", - "value": 100, - }, - { - "children": [], - "endTime": 2501.5999999999894, - "name": "beforeTransition", - "startTime": 2501.4999999999895, - "tooltip": "beforeTransition, 0.1000 (ms)", - "value": 100, - }, - { - "children": [], - "endTime": 2501.899999999989, - "name": "resolveTransitionNode", - "startTime": 2501.7999999999893, - "tooltip": "resolveTransitionNode, 0.1000 (ms)", - "value": 100, - }, - { - "children": [], - "endTime": 2502.199999999989, - "name": "transition", - "startTime": 2502.099999999989, - "tooltip": "transition, 0.1000 (ms)", - "value": 100, - }, - { - "children": [ - { - "backgroundColor": "#000000", - "children": [], - "color": "#000000", - "name": "(work)", - "tooltip": "Placeholder time between hooks", - "value": 100, - }, - { - "children": [], - "endTime": 2502.5999999999885, - "name": "resolveOptions", - "startTime": 2502.4999999999886, - "tooltip": "resolveOptions, 0.1000 (ms)", - "value": 100, - }, - { - "backgroundColor": "#000000", - "children": [], - "color": "#000000", - "name": "(work)", - "tooltip": "Placeholder time between hooks", - "value": 200, - }, - { - "children": [], - "endTime": 2502.8999999999883, - "name": "beforeEvaluate", - "startTime": 2502.7999999999884, - "tooltip": "beforeEvaluate, 0.1000 (ms)", - "value": 100, - }, - { - "backgroundColor": "#000000", - "children": [], - "color": "#000000", - "name": "(work)", - "tooltip": "Placeholder time between hooks", - "value": 200, - }, - { - "children": [ - { - "backgroundColor": "#000000", - "children": [], - "color": "#000000", - "name": "(work)", - "tooltip": "Placeholder time between hooks", - "value": 100, - }, - { - "children": [], - "endTime": 2503.299999999988, - "name": "resolve", - "startTime": 2503.199999999988, - "tooltip": "resolve, 0.1000 (ms)", - "value": 100, - }, - { - "backgroundColor": "#000000", - "children": [], - "color": "#000000", - "name": "(work)", - "tooltip": "Placeholder time between hooks", - "value": 200, - }, - { - "children": [], - "endTime": 2503.5999999999876, - "name": "skipOptimization", - "startTime": 2503.4999999999877, - "tooltip": "skipOptimization, 0.1000 (ms)", - "value": 100, - }, - { - "backgroundColor": "#000000", - "children": [], - "color": "#000000", - "name": "(work)", - "tooltip": "Placeholder time between hooks", - "value": 200, - }, - { - "children": [], - "endTime": 2503.8999999999874, - "name": "resolveDataStages", - "startTime": 2503.7999999999874, - "tooltip": "resolveDataStages, 0.1000 (ms)", - "value": 100, - }, - { - "backgroundColor": "#000000", - "children": [], - "color": "#000000", - "name": "(work)", - "tooltip": "Placeholder time between hooks", - "value": 200, - }, - { - "children": [ - { - "backgroundColor": "#000000", - "children": [], - "color": "#000000", - "name": "(work)", - "tooltip": "Placeholder time between hooks", - "value": 100, - }, - { - "children": [], - "endTime": 2504.299999999987, - "name": "resolveTypeForBinding", - "startTime": 2504.199999999987, - "tooltip": "resolveTypeForBinding, 0.1000 (ms)", - "value": 100, - }, - ], - "endTime": 2504.499999999987, - "name": "resolveDefaultValue", - "startTime": 2504.099999999987, - "tooltip": "resolveDefaultValue, 0.4000 (ms)", - "value": 400, - }, - { - "backgroundColor": "#000000", - "children": [], - "color": "#000000", - "name": "(work)", - "tooltip": "Placeholder time between hooks", - "value": 200, - }, - { - "children": [], - "endTime": 2504.7999999999865, - "name": "onGet", - "startTime": 2504.6999999999866, - "tooltip": "onGet, 0.1000 (ms)", - "value": 100, - }, - { - "backgroundColor": "#000000", - "children": [], - "color": "#000000", - "name": "(work)", - "tooltip": "Placeholder time between hooks", - "value": 200, - }, - { - "children": [], - "endTime": 2505.0999999999863, - "name": "onSet", - "startTime": 2504.9999999999864, - "tooltip": "onSet, 0.1000 (ms)", - "value": 100, - }, - { - "backgroundColor": "#000000", - "children": [], - "color": "#000000", - "name": "(work)", - "tooltip": "Placeholder time between hooks", - "value": 200, - }, - { - "children": [], - "endTime": 2505.399999999986, - "name": "onUpdate", - "startTime": 2505.299999999986, - "tooltip": "onUpdate, 0.1000 (ms)", - "value": 100, - }, - ], - "endTime": 2505.599999999986, - "name": "resolve", - "startTime": 2503.099999999988, - "tooltip": "resolve, 2.5000 (ms)", - "value": 2500, - }, - { - "backgroundColor": "#000000", - "children": [], - "color": "#000000", - "name": "(work)", - "tooltip": "Placeholder time between hooks", - "value": 200, - }, - { - "children": [], - "endTime": 2505.8999999999855, - "name": "skipTransition", - "startTime": 2505.7999999999856, - "tooltip": "skipTransition, 0.1000 (ms)", - "value": 100, - }, - { - "backgroundColor": "#000000", - "children": [], - "color": "#000000", - "name": "(work)", - "tooltip": "Placeholder time between hooks", - "value": 200, - }, - { - "children": [], - "endTime": 2506.1999999999853, - "name": "beforeTransition", - "startTime": 2506.0999999999854, - "tooltip": "beforeTransition, 0.1000 (ms)", - "value": 100, - }, - { - "backgroundColor": "#000000", - "children": [], - "color": "#000000", - "name": "(work)", - "tooltip": "Placeholder time between hooks", - "value": 200, - }, - { - "children": [], - "endTime": 2506.499999999985, - "name": "resolveTransitionNode", - "startTime": 2506.399999999985, - "tooltip": "resolveTransitionNode, 0.1000 (ms)", - "value": 100, - }, - { - "backgroundColor": "#000000", - "children": [], - "color": "#000000", - "name": "(work)", - "tooltip": "Placeholder time between hooks", - "value": 200, - }, - { - "children": [], - "endTime": 2506.7999999999847, - "name": "transition", - "startTime": 2506.699999999985, - "tooltip": "transition, 0.1000 (ms)", - "value": 100, - }, - { - "backgroundColor": "#000000", - "children": [], - "color": "#000000", - "name": "(work)", - "tooltip": "Placeholder time between hooks", - "value": 200, - }, - { - "children": [], - "endTime": 2507.0999999999844, - "name": "afterTransition", - "startTime": 2506.9999999999845, - "tooltip": "afterTransition, 0.1000 (ms)", - "value": 100, - }, - ], - "endTime": 2507.2999999999843, - "name": "afterTransition", - "startTime": 2502.3999999999887, - "tooltip": "afterTransition, 4.9000 (ms)", - "value": 4900, - }, - { - "children": [], - "endTime": 2507.599999999984, - "name": "onGet", - "startTime": 2507.499999999984, - "tooltip": "onGet, 0.1000 (ms)", - "value": 100, - }, - { - "children": [], - "endTime": 2507.8999999999837, - "name": "serialize", - "startTime": 2507.799999999984, - "tooltip": "serialize, 0.1000 (ms)", - "value": 100, - }, - { - "children": [], - "endTime": 2508.1999999999834, - "name": "state", - "startTime": 2508.0999999999835, - "tooltip": "state, 0.1000 (ms)", - "value": 100, - }, - ], + "rootNode": { + "children": [ + { + "children": [], + "endTime": 2490.2, + "name": "state", + "startTime": 2490.1, + "tooltip": "state, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "Placeholder time between hooks", + "value": 200, + }, + { + "children": [], + "endTime": 2490.4999999999995, + "name": "resolveFlowContent", + "startTime": 2490.3999999999996, + "tooltip": "resolveFlowContent, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "Placeholder time between hooks", + "value": 200, + }, + { + "children": [], + "endTime": 2490.7999999999993, + "name": "onStart", + "startTime": 2490.6999999999994, + "tooltip": "onStart, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "Placeholder time between hooks", + "value": 200, + }, + { + "children": [], + "endTime": 2491.099999999999, + "name": "flowController", + "startTime": 2490.999999999999, + "tooltip": "flowController, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "Placeholder time between hooks", + "value": 200, + }, + { + "children": [], + "endTime": 2491.3999999999987, + "name": "bindingParser", + "startTime": 2491.299999999999, + "tooltip": "bindingParser, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "Placeholder time between hooks", + "value": 200, + }, + { + "children": [], + "endTime": 2491.6999999999985, + "name": "schema", + "startTime": 2491.5999999999985, + "tooltip": "schema, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "Placeholder time between hooks", + "value": 200, + }, + { + "children": [], + "endTime": 2491.999999999998, + "name": "validationController", + "startTime": 2491.8999999999983, + "tooltip": "validationController, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "Placeholder time between hooks", + "value": 200, + }, + { + "children": [], + "endTime": 2492.299999999998, + "name": "expressionEvaluator", + "startTime": 2492.199999999998, + "tooltip": "expressionEvaluator, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "Placeholder time between hooks", + "value": 200, + }, + { + "children": [], + "endTime": 2492.5999999999976, + "name": "dataController", + "startTime": 2492.4999999999977, + "tooltip": "dataController, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "Placeholder time between hooks", + "value": 200, + }, + { + "children": [], + "endTime": 2492.8999999999974, + "name": "viewController", + "startTime": 2492.7999999999975, + "tooltip": "viewController, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "Placeholder time between hooks", + "value": 200, + }, + { + "children": [], + "endTime": 2493.199999999997, + "name": "state", + "startTime": 2493.099999999997, + "tooltip": "state, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "Placeholder time between hooks", + "value": 200, + }, + { + "children": [], + "endTime": 2493.499999999997, + "name": "flow", + "startTime": 2493.399999999997, + "tooltip": "flow, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "Placeholder time between hooks", + "value": 200, + }, + { + "children": [], + "endTime": 2493.7999999999965, + "name": "beforeStart", + "startTime": 2493.6999999999966, + "tooltip": "beforeStart, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "Placeholder time between hooks", + "value": 200, + }, + { + "children": [], + "endTime": 2494.0999999999963, + "name": "resolveTransitionNode", + "startTime": 2493.9999999999964, + "tooltip": "resolveTransitionNode, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "Placeholder time between hooks", + "value": 200, + }, + { + "children": [ + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "Placeholder time between hooks", + "value": 100, + }, + { + "children": [], + "endTime": 2494.499999999996, + "name": "resolveView", + "startTime": 2494.399999999996, + "tooltip": "resolveView, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "Placeholder time between hooks", + "value": 200, + }, + { + "children": [ + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "Placeholder time between hooks", + "value": 100, + }, + { + "children": [], + "endTime": 2494.8999999999955, + "name": "onTemplatePluginCreated", + "startTime": 2494.7999999999956, + "tooltip": "onTemplatePluginCreated, 0.1000 (ms)", + "value": 100, + }, + ], + "endTime": 2495.0999999999954, + "name": "view", + "startTime": 2494.6999999999957, + "tooltip": "view, 0.4000 (ms)", + "value": 400, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "Placeholder time between hooks", + "value": 200, + }, + { + "children": [], + "endTime": 2495.399999999995, + "name": "templatePlugin", + "startTime": 2495.299999999995, + "tooltip": "templatePlugin, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "Placeholder time between hooks", + "value": 200, + }, + { + "children": [], + "endTime": 2495.699999999995, + "name": "parser", + "startTime": 2495.599999999995, + "tooltip": "parser, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "Placeholder time between hooks", + "value": 200, + }, + { + "children": [], + "endTime": 2495.9999999999945, + "name": "parseNode", + "startTime": 2495.8999999999946, + "tooltip": "parseNode, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "Placeholder time between hooks", + "value": 200, + }, + { + "children": [], + "endTime": 2496.2999999999943, + "name": "onParseObject", + "startTime": 2496.1999999999944, + "tooltip": "onParseObject, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "Placeholder time between hooks", + "value": 200, + }, + { + "children": [], + "endTime": 2496.599999999994, + "name": "parseNode", + "startTime": 2496.499999999994, + "tooltip": "parseNode, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "Placeholder time between hooks", + "value": 200, + }, + { + "children": [], + "endTime": 2496.8999999999937, + "name": "parseNode", + "startTime": 2496.799999999994, + "tooltip": "parseNode, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "Placeholder time between hooks", + "value": 200, + }, + { + "children": [], + "endTime": 2497.1999999999935, + "name": "parseNode", + "startTime": 2497.0999999999935, + "tooltip": "parseNode, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "Placeholder time between hooks", + "value": 200, + }, + { + "children": [], + "endTime": 2497.499999999993, + "name": "onCreateASTNode", + "startTime": 2497.3999999999933, + "tooltip": "onCreateASTNode, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "Placeholder time between hooks", + "value": 200, + }, + { + "children": [], + "endTime": 2497.799999999993, + "name": "resolver", + "startTime": 2497.699999999993, + "tooltip": "resolver, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "Placeholder time between hooks", + "value": 200, + }, + { + "children": [], + "endTime": 2498.0999999999926, + "name": "beforeUpdate", + "startTime": 2497.9999999999927, + "tooltip": "beforeUpdate, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "Placeholder time between hooks", + "value": 200, + }, + { + "children": [], + "endTime": 2498.3999999999924, + "name": "resolveOptions", + "startTime": 2498.2999999999925, + "tooltip": "resolveOptions, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "Placeholder time between hooks", + "value": 200, + }, + { + "children": [], + "endTime": 2498.699999999992, + "name": "skipResolve", + "startTime": 2498.599999999992, + "tooltip": "skipResolve, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "Placeholder time between hooks", + "value": 200, + }, + { + "children": [], + "endTime": 2498.999999999992, + "name": "beforeResolve", + "startTime": 2498.899999999992, + "tooltip": "beforeResolve, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "Placeholder time between hooks", + "value": 200, + }, + { + "children": [], + "endTime": 2499.2999999999915, + "name": "resolve", + "startTime": 2499.1999999999916, + "tooltip": "resolve, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "Placeholder time between hooks", + "value": 200, + }, + { + "children": [], + "endTime": 2499.5999999999913, + "name": "afterResolve", + "startTime": 2499.4999999999914, + "tooltip": "afterResolve, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "Placeholder time between hooks", + "value": 200, + }, + { + "children": [], + "endTime": 2499.899999999991, + "name": "afterNodeUpdate", + "startTime": 2499.799999999991, + "tooltip": "afterNodeUpdate, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "Placeholder time between hooks", + "value": 200, + }, + { + "children": [], + "endTime": 2500.1999999999907, + "name": "afterUpdate", + "startTime": 2500.099999999991, + "tooltip": "afterUpdate, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "Placeholder time between hooks", + "value": 200, + }, + { + "children": [], + "endTime": 2500.4999999999905, + "name": "onUpdate", + "startTime": 2500.3999999999905, + "tooltip": "onUpdate, 0.1000 (ms)", + "value": 100, + }, + ], + "endTime": 2500.6999999999903, + "name": "transition", + "startTime": 2494.299999999996, + "tooltip": "transition, 6.4000 (ms)", + "value": 6400, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "Placeholder time between hooks", + "value": 200, + }, + { + "children": [], + "endTime": 2500.99999999999, + "name": "afterTransition", + "startTime": 2500.89999999999, + "tooltip": "afterTransition, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "Placeholder time between hooks", + "value": 200, + }, + { + "children": [], + "endTime": 2501.2999999999897, + "name": "skipTransition", + "startTime": 2501.19999999999, + "tooltip": "skipTransition, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "Placeholder time between hooks", + "value": 200, + }, + { + "children": [], + "endTime": 2501.5999999999894, + "name": "beforeTransition", + "startTime": 2501.4999999999895, + "tooltip": "beforeTransition, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "Placeholder time between hooks", + "value": 200, + }, + { + "children": [], + "endTime": 2501.899999999989, + "name": "resolveTransitionNode", + "startTime": 2501.7999999999893, + "tooltip": "resolveTransitionNode, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "Placeholder time between hooks", + "value": 200, + }, + { + "children": [], + "endTime": 2502.199999999989, + "name": "transition", + "startTime": 2502.099999999989, + "tooltip": "transition, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "Placeholder time between hooks", + "value": 200, + }, + { + "children": [ + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "Placeholder time between hooks", + "value": 100, + }, + { + "children": [], + "endTime": 2502.5999999999885, + "name": "resolveOptions", + "startTime": 2502.4999999999886, + "tooltip": "resolveOptions, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "Placeholder time between hooks", + "value": 200, + }, + { + "children": [], + "endTime": 2502.8999999999883, + "name": "beforeEvaluate", + "startTime": 2502.7999999999884, + "tooltip": "beforeEvaluate, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "Placeholder time between hooks", + "value": 200, + }, + { + "children": [ + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "Placeholder time between hooks", + "value": 100, + }, + { + "children": [], + "endTime": 2503.299999999988, + "name": "resolve", + "startTime": 2503.199999999988, + "tooltip": "resolve, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "Placeholder time between hooks", + "value": 200, + }, + { + "children": [], + "endTime": 2503.5999999999876, + "name": "skipOptimization", + "startTime": 2503.4999999999877, + "tooltip": "skipOptimization, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "Placeholder time between hooks", + "value": 200, + }, + { + "children": [], + "endTime": 2503.8999999999874, + "name": "resolveDataStages", + "startTime": 2503.7999999999874, + "tooltip": "resolveDataStages, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "Placeholder time between hooks", + "value": 200, + }, + { + "children": [ + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "Placeholder time between hooks", + "value": 100, + }, + { + "children": [], + "endTime": 2504.299999999987, + "name": "resolveTypeForBinding", + "startTime": 2504.199999999987, + "tooltip": "resolveTypeForBinding, 0.1000 (ms)", + "value": 100, + }, + ], + "endTime": 2504.499999999987, + "name": "resolveDefaultValue", + "startTime": 2504.099999999987, + "tooltip": "resolveDefaultValue, 0.4000 (ms)", + "value": 400, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "Placeholder time between hooks", + "value": 200, + }, + { + "children": [], + "endTime": 2504.7999999999865, + "name": "onGet", + "startTime": 2504.6999999999866, + "tooltip": "onGet, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "Placeholder time between hooks", + "value": 200, + }, + { + "children": [], + "endTime": 2505.0999999999863, + "name": "onSet", + "startTime": 2504.9999999999864, + "tooltip": "onSet, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "Placeholder time between hooks", + "value": 200, + }, + { + "children": [], + "endTime": 2505.399999999986, + "name": "onUpdate", + "startTime": 2505.299999999986, + "tooltip": "onUpdate, 0.1000 (ms)", + "value": 100, + }, + ], + "endTime": 2505.599999999986, + "name": "resolve", + "startTime": 2503.099999999988, + "tooltip": "resolve, 2.5000 (ms)", + "value": 2500, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "Placeholder time between hooks", + "value": 200, + }, + { + "children": [], + "endTime": 2505.8999999999855, + "name": "skipTransition", + "startTime": 2505.7999999999856, + "tooltip": "skipTransition, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "Placeholder time between hooks", + "value": 200, + }, + { + "children": [], + "endTime": 2506.1999999999853, + "name": "beforeTransition", + "startTime": 2506.0999999999854, + "tooltip": "beforeTransition, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "Placeholder time between hooks", + "value": 200, + }, + { + "children": [], + "endTime": 2506.499999999985, + "name": "resolveTransitionNode", + "startTime": 2506.399999999985, + "tooltip": "resolveTransitionNode, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "Placeholder time between hooks", + "value": 200, + }, + { + "children": [], + "endTime": 2506.7999999999847, + "name": "transition", + "startTime": 2506.699999999985, + "tooltip": "transition, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "Placeholder time between hooks", + "value": 200, + }, + { + "children": [], + "endTime": 2507.0999999999844, + "name": "afterTransition", + "startTime": 2506.9999999999845, + "tooltip": "afterTransition, 0.1000 (ms)", + "value": 100, + }, + ], + "endTime": 2507.2999999999843, + "name": "afterTransition", + "startTime": 2502.3999999999887, + "tooltip": "afterTransition, 4.9000 (ms)", + "value": 4900, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "Placeholder time between hooks", + "value": 200, + }, + { + "children": [], + "endTime": 2507.599999999984, + "name": "onGet", + "startTime": 2507.499999999984, + "tooltip": "onGet, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "Placeholder time between hooks", + "value": 200, + }, + { + "children": [], + "endTime": 2507.8999999999837, + "name": "serialize", + "startTime": 2507.799999999984, + "tooltip": "serialize, 0.1000 (ms)", + "value": 100, + }, + { + "backgroundColor": "#000000", + "children": [], + "color": "#000000", + "name": "(work)", + "tooltip": "Placeholder time between hooks", + "value": 200, + }, + { + "children": [], + "endTime": 2508.1999999999834, + "name": "state", + "startTime": 2508.0999999999835, + "tooltip": "state, 0.1000 (ms)", + "value": 100, + }, + ], + "endTime": 2508.1999999999834, + "name": "root", + "startTime": 2490.1, + "value": 18100, + }, } `; diff --git a/devtools/plugins/profiler/core/src/__tests__/addProfilerInterceptorsToHooks.test.ts b/devtools/plugins/profiler/core/src/__tests__/addProfilerInterceptorsToHooks.test.ts index 8feca27..bce09ac 100644 --- a/devtools/plugins/profiler/core/src/__tests__/addProfilerInterceptorsToHooks.test.ts +++ b/devtools/plugins/profiler/core/src/__tests__/addProfilerInterceptorsToHooks.test.ts @@ -1,7 +1,7 @@ import { SyncHook } from "tapable-ts"; import { describe, expect, test } from "vitest"; import { addProfilerInterceptorsToHooks } from "../addProfilerInterceptorsToHooks"; -import { profiler } from "../helpers"; +import { Profiler } from "../helpers"; describe("addProfilerInterceptorsToHooks", () => { /** @@ -13,7 +13,7 @@ describe("addProfilerInterceptorsToHooks", () => { * hook has fired. */ test("re-intercepting the same child object on repeated parent calls does not duplicate timers", () => { - const profilerInstance = profiler(); + const profilerInstance = new Profiler(); profilerInstance.start(); // Child object whose hooks get discovered lazily via the parent's call arg diff --git a/devtools/plugins/profiler/core/src/__tests__/plugin.test.ts b/devtools/plugins/profiler/core/src/__tests__/plugin.test.ts index 05196da..b5750a6 100644 --- a/devtools/plugins/profiler/core/src/__tests__/plugin.test.ts +++ b/devtools/plugins/profiler/core/src/__tests__/plugin.test.ts @@ -70,7 +70,7 @@ describe("Plugin", () => { expect(playerState.status).toBe("in-progress"); expect( (playerState as InProgressState).controllers.view.currentView - ?.lastUpdate, + ?.lastUpdate ).toBeDefined(); }); @@ -95,7 +95,7 @@ describe("Plugin", () => { const storeState = profilerPlugin.store.getState(); expect( - storeState.plugins["player-ui-profiler-plugin"]?.flow.data, + storeState.plugins["player-ui-profiler-plugin"]?.flow.data ).toMatchSnapshot(); }); @@ -135,7 +135,7 @@ describe("Plugin", () => { expect(playerState.status).toBe("in-progress"); expect( (playerState as InProgressState).controllers.view.currentView - ?.lastUpdate, + ?.lastUpdate ).toBeDefined(); }); diff --git a/devtools/plugins/profiler/core/src/addProfilerInterceptorsToHooks.ts b/devtools/plugins/profiler/core/src/addProfilerInterceptorsToHooks.ts index 75746f2..013e9a0 100644 --- a/devtools/plugins/profiler/core/src/addProfilerInterceptorsToHooks.ts +++ b/devtools/plugins/profiler/core/src/addProfilerInterceptorsToHooks.ts @@ -10,7 +10,7 @@ import { SyncLoopHook, SyncWaterfallHook, } from "tapable-ts"; -import { Profiler } from "./types"; +import { Profiler } from "./helpers"; /* Paths to hooks to ignore. * Currently ignoring "view" hook on player since it acts as a shortcut to the viewController's view hook. Including it would duplicate a lot of profiling work. @@ -30,6 +30,7 @@ type AnyHook = | SyncLoopHook | SyncWaterfallHook; +// Note: cannot use instanceof to check against the hook classes due to how JS is loaded in swift and kotlin const isAnyHook = (obj: unknown): obj is AnyHook => { return ( isRecordType(obj) && @@ -43,14 +44,12 @@ export const addProfilerInterceptorsToHooks = ( obj: unknown, profiler: Profiler, currentPath: string[] = [], - intercepted: WeakSet = new WeakSet(), + intercepted: WeakSet = new WeakSet() ): void => { if (!hasHooks(obj)) { return; } - const { startTimer, endTimer } = profiler; - Object.entries(obj.hooks).forEach(([key, value]) => { const nextPath = [...currentPath, key]; if ( @@ -70,20 +69,20 @@ export const addProfilerInterceptorsToHooks = ( args[0], profiler, nextPath, - intercepted, + intercepted ); } - startTimer(key); + profiler.startTimer(key); }, done: () => { - endTimer({ hookName: key }); + profiler.endTimer({ hookName: key }); }, result: () => { - endTimer({ hookName: key }); + profiler.endTimer({ hookName: key }); }, error: () => { - endTimer({ hookName: key }); + profiler.endTimer({ hookName: key }); }, }); }); diff --git a/devtools/plugins/profiler/core/src/helpers/__tests__/profiler.test.ts b/devtools/plugins/profiler/core/src/helpers/__tests__/profiler.test.ts index 0aeaede..8ef618f 100644 --- a/devtools/plugins/profiler/core/src/helpers/__tests__/profiler.test.ts +++ b/devtools/plugins/profiler/core/src/helpers/__tests__/profiler.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test, vi } from "vitest"; -import { profiler } from "../profiler"; +import { Profiler } from "../profiler"; let count = 2490.0; vi.mock("@player-devtools/plugin", async () => { @@ -15,17 +15,17 @@ vi.mock("@player-devtools/plugin", async () => { describe("Profiler", () => { test("sequential top-level timers each become a separate rootNodes entry", () => { - const { startTimer, endTimer, stopProfiler, start } = profiler(); + const p = new Profiler(); - start(); + p.start(); - startTimer("hookA"); - endTimer({ hookName: "hookA" }); + p.startTimer("hookA"); + p.endTimer({ hookName: "hookA" }); - startTimer("hookB"); - endTimer({ hookName: "hookB" }); + p.startTimer("hookB"); + p.endTimer({ hookName: "hookB" }); - const { rootNodes, durations } = stopProfiler(); + const { rootNodes, durations } = p.stopProfiler(); expect(rootNodes).toHaveLength(2); expect(rootNodes[0]!.name).toBe("hookA"); @@ -34,38 +34,35 @@ describe("Profiler", () => { }); test("nested timers become children of the outer timer", () => { - const { startTimer, endTimer, stopProfiler, start } = profiler(); + const p = new Profiler(); - start(); + p.start(); - startTimer("outer"); - startTimer("inner1"); - endTimer({ hookName: "inner1" }); - startTimer("inner2"); - endTimer({ hookName: "inner2" }); - endTimer({ hookName: "outer" }); + p.startTimer("outer"); + p.startTimer("inner1"); + p.endTimer({ hookName: "inner1" }); + p.startTimer("inner2"); + p.endTimer({ hookName: "inner2" }); + p.endTimer({ hookName: "outer" }); - const { rootNodes } = stopProfiler(); + const { rootNodes } = p.stopProfiler(); expect(rootNodes).toHaveLength(1); expect(rootNodes[0]!.name).toBe("outer"); - const realChildren = rootNodes[0]!.children.filter( - (c) => c.name !== "(work)", - ); - expect(realChildren).toHaveLength(2); - expect(realChildren[0]!.name).toBe("inner1"); - expect(realChildren[1]!.name).toBe("inner2"); + expect(rootNodes[0]!.children).toHaveLength(2); + expect(rootNodes[0]!.children[0]!.name).toBe("inner1"); + expect(rootNodes[0]!.children[1]!.name).toBe("inner2"); }); test("endTimer for unknown name warns and does nothing", () => { const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); - const { startTimer, endTimer, stopProfiler, start } = profiler(); + const p = new Profiler(); - start(); - startTimer("hookA"); - endTimer({ hookName: "unknown" }); + p.start(); + p.startTimer("hookA"); + p.endTimer({ hookName: "unknown" }); - const { rootNodes } = stopProfiler(); + const { rootNodes } = p.stopProfiler(); // hookA is still on the stack — not finalized, so rootNodes has it but without endTime expect(warnSpy).toHaveBeenCalledWith( @@ -80,15 +77,15 @@ describe("Profiler", () => { test("endTimer for buried name pops and warns about intermediate timers", () => { const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); - const { startTimer, endTimer, stopProfiler, start } = profiler(); + const p = new Profiler(); - start(); + p.start(); - startTimer("outer"); - startTimer("middle"); - startTimer("inner"); + p.startTimer("outer"); + p.startTimer("middle"); + p.startTimer("inner"); // End "outer" without ending "inner" or "middle" first - endTimer({ hookName: "outer" }); + p.endTimer({ hookName: "outer" }); expect(warnSpy).toHaveBeenCalledWith( expect.stringContaining("popping 'inner'"), @@ -97,7 +94,7 @@ describe("Profiler", () => { expect.stringContaining("popping 'middle'"), ); - const { rootNodes, durations } = stopProfiler(); + const { rootNodes, durations } = p.stopProfiler(); // All three should be finalized expect(rootNodes).toHaveLength(1); @@ -108,14 +105,14 @@ describe("Profiler", () => { }); test("start() resets all state", () => { - const { startTimer, endTimer, stopProfiler, start } = profiler(); + const p = new Profiler(); - start(); - startTimer("hookA"); - endTimer({ hookName: "hookA" }); + p.start(); + p.startTimer("hookA"); + p.endTimer({ hookName: "hookA" }); - start(); - const { rootNodes, durations } = stopProfiler(); + p.start(); + const { rootNodes, durations } = p.stopProfiler(); expect(rootNodes).toHaveLength(0); expect(durations).toHaveLength(0); @@ -123,37 +120,37 @@ describe("Profiler", () => { test("calls onUpdate only after endTimer, not startTimer", () => { const onUpdate = vi.fn(); - const { startTimer, endTimer, start } = profiler(onUpdate); + const p = new Profiler(onUpdate); // No auto-start, so no calls yet expect(onUpdate.mock.calls.length).toBe(0); - start(); + p.start(); expect(onUpdate.mock.calls.length).toBe(0); - startTimer("hookA"); + p.startTimer("hookA"); expect(onUpdate.mock.calls.length).toBe(0); - endTimer({ hookName: "hookA" }); + p.endTimer({ hookName: "hookA" }); expect(onUpdate.mock.calls.length).toBe(1); - startTimer("hookB"); - endTimer({ hookName: "hookB" }); + p.startTimer("hookB"); + p.endTimer({ hookName: "hookB" }); expect(onUpdate.mock.calls.length).toBe(2); }); test("getSnapshot returns sorted durations and a deep clone of rootNodes", () => { - const { startTimer, endTimer, getSnapshot, start } = profiler(); + const p = new Profiler(); - start(); + p.start(); - startTimer("slow"); - endTimer({ hookName: "slow" }); + p.startTimer("slow"); + p.endTimer({ hookName: "slow" }); - startTimer("fast"); - endTimer({ hookName: "fast" }); + p.startTimer("fast"); + p.endTimer({ hookName: "fast" }); - const snap = getSnapshot(); + const snap = p.getSnapshot(); // Sorted descending by duration — slow was measured first so has larger elapsed expect(snap.durations[0]!.name).toBe("slow"); @@ -162,24 +159,24 @@ describe("Profiler", () => { expect(snap.rootNodes).toHaveLength(2); // Snapshot is a clone — adding to live tree doesn't affect it - startTimer("extra"); - endTimer({ hookName: "extra" }); + p.startTimer("extra"); + p.endTimer({ hookName: "extra" }); expect(snap.rootNodes).toHaveLength(2); }); test("getSnapshot sets endTime and value on in-flight nodes using current time", () => { - const { startTimer, endTimer, getSnapshot, start } = profiler(); + const p = new Profiler(); - start(); + p.start(); // Finish one node so we have a reference - startTimer("finished"); - endTimer({ hookName: "finished" }); + p.startTimer("finished"); + p.endTimer({ hookName: "finished" }); // Leave this one in-flight - startTimer("inflight"); + p.startTimer("inflight"); - const snap = getSnapshot(); + const snap = p.getSnapshot(); const finished = snap.rootNodes.find((n) => n.name === "finished"); const inflight = snap.rootNodes.find((n) => n.name === "inflight"); @@ -193,233 +190,36 @@ describe("Profiler", () => { expect(inflight!.value).toBeGreaterThan(0); // Live node is not mutated - const { - startTimer: s2, - endTimer: e2, - getSnapshot: gs2, - start: st2, - } = profiler(); - st2(); - s2("live"); - const liveBefore = gs2().rootNodes[0]!; + const p2 = new Profiler(); + p2.start(); + p2.startTimer("live"); + const liveBefore = p2.getSnapshot().rootNodes[0]!; expect(liveBefore.endTime).toBeDefined(); // snapshot sets it // but the original node in the stack must remain without endTime // (verified indirectly: calling endTimer still works normally) - e2({ hookName: "live" }); - const liveAfter = gs2().rootNodes[0]!; + p2.endTimer({ hookName: "live" }); + const liveAfter = p2.getSnapshot().rootNodes[0]!; expect(liveAfter.endTime).toBeDefined(); expect(liveAfter.value).toBeGreaterThan(0); }); - describe("insertSpacers", () => { - test("no spacer when child starts exactly at parent startTime", () => { - const { insertSpacers } = profiler(); - - const node = { - name: "parent", - startTime: 100, - endTime: 200, - value: 100000, - children: [ - { - name: "child", - startTime: 100, - endTime: 150, - value: 50000, - children: [], - }, - ], - }; - - const result = insertSpacers(node); - expect(result.children).toHaveLength(1); - expect(result.children[0]!.name).toBe("child"); - }); - - test("leading spacer when first child starts after parent startTime", () => { - const { insertSpacers } = profiler(); - - const node = { - name: "parent", - startTime: 100, - endTime: 200, - value: 100000, - children: [ - { - name: "child", - startTime: 110, - endTime: 150, - value: 40000, - children: [], - }, - ], - }; - - const result = insertSpacers(node); - expect(result.children).toHaveLength(2); - expect(result.children[0]!.backgroundColor).toBe("#000000"); - expect(result.children[0]!.value).toBe(Math.ceil((110 - 100) * 1000)); - expect(result.children[1]!.name).toBe("child"); - }); - - test("spacer between siblings with a gap", () => { - const { insertSpacers } = profiler(); - - const node = { - name: "parent", - startTime: 100, - endTime: 200, - value: 100000, - children: [ - { - name: "child1", - startTime: 100, - endTime: 130, - value: 30000, - children: [], - }, - { - name: "child2", - startTime: 150, - endTime: 180, - value: 30000, - children: [], - }, - ], - }; - - const result = insertSpacers(node); - expect(result.children).toHaveLength(3); - expect(result.children[0]!.name).toBe("child1"); - expect(result.children[1]!.backgroundColor).toBe("#000000"); - expect(result.children[1]!.value).toBe(Math.ceil((150 - 130) * 1000)); - expect(result.children[2]!.name).toBe("child2"); - }); - - test("no spacer between siblings with no gap", () => { - const { insertSpacers } = profiler(); - - const node = { - name: "parent", - startTime: 100, - endTime: 200, - value: 100000, - children: [ - { - name: "child1", - startTime: 100, - endTime: 130, - value: 30000, - children: [], - }, - { - name: "child2", - startTime: 130, - endTime: 160, - value: 30000, - children: [], - }, - ], - }; - - const result = insertSpacers(node); - expect(result.children).toHaveLength(2); - expect(result.children[0]!.name).toBe("child1"); - expect(result.children[1]!.name).toBe("child2"); - }); - - test("spacers are inserted recursively into nested children", () => { - const { insertSpacers } = profiler(); - - const node = { - name: "parent", - startTime: 100, - endTime: 300, - value: 200000, - children: [ - { - name: "child", - startTime: 100, - endTime: 300, - value: 200000, - children: [ - { - name: "grandchild", - startTime: 150, - endTime: 200, - value: 50000, - children: [], - }, - ], - }, - ], - }; - - const result = insertSpacers(node); - const child = result.children[0]!; - expect(child.name).toBe("child"); - // grandchild has a leading spacer of 50ms - expect(child.children).toHaveLength(2); - expect(child.children[0]!.backgroundColor).toBe("#000000"); - expect(child.children[0]!.value).toBe(Math.ceil((150 - 100) * 1000)); - expect(child.children[1]!.name).toBe("grandchild"); - }); - - test("no spacers inserted when parent or child lacks timing info", () => { - const { insertSpacers } = profiler(); - - // Parent missing startTime — skip spacer logic, return node unchanged - const nodeNoStartTime = { - name: "parent", - endTime: 200, - value: 100000, - children: [ - { - name: "child", - startTime: 110, - endTime: 150, - value: 40000, - children: [], - }, - ], - }; - const r1 = insertSpacers(nodeNoStartTime); - expect(r1.children).toHaveLength(1); - expect(r1.children[0]!.name).toBe("child"); - - // Child missing startTime — skip that child's spacer, pass it through - const nodeChildNoTiming = { - name: "parent", - startTime: 100, - endTime: 200, - value: 100000, - children: [{ name: "child", children: [] }], - }; - const r2 = insertSpacers(nodeChildNoTiming); - expect(r2.children).toHaveLength(1); - expect(r2.children[0]!.name).toBe("child"); - }); - }); - test("stopProfiler returns the full rootNodes forest with sorted durations", () => { - const { startTimer, endTimer, stopProfiler, start } = profiler(); + const p = new Profiler(); - start(); + p.start(); - startTimer("a"); - startTimer("a.child"); - endTimer({ hookName: "a.child" }); - endTimer({ hookName: "a" }); + p.startTimer("a"); + p.startTimer("a.child"); + p.endTimer({ hookName: "a.child" }); + p.endTimer({ hookName: "a" }); - startTimer("b"); - endTimer({ hookName: "b" }); + p.startTimer("b"); + p.endTimer({ hookName: "b" }); - const { rootNodes, durations } = stopProfiler(); + const { rootNodes, durations } = p.stopProfiler(); expect(rootNodes).toHaveLength(2); - expect( - rootNodes[0]!.children.filter((c) => c.name !== "(work)"), - ).toHaveLength(1); + expect(rootNodes[0]!.children).toHaveLength(1); // durations sorted descending expect(durations[0]!.name).toBe("a"); expect(durations).toMatchSnapshot(); diff --git a/devtools/plugins/profiler/core/src/helpers/index.ts b/devtools/plugins/profiler/core/src/helpers/index.ts index f400962..d1c5bbb 100644 --- a/devtools/plugins/profiler/core/src/helpers/index.ts +++ b/devtools/plugins/profiler/core/src/helpers/index.ts @@ -1 +1,2 @@ export * from "./profiler"; +export * from "./transformProfilerData"; diff --git a/devtools/plugins/profiler/core/src/helpers/profiler.ts b/devtools/plugins/profiler/core/src/helpers/profiler.ts index e21b823..94cf860 100644 --- a/devtools/plugins/profiler/core/src/helpers/profiler.ts +++ b/devtools/plugins/profiler/core/src/helpers/profiler.ts @@ -1,22 +1,21 @@ -import type { Profiler, ProfilerNode } from "../types"; +import type { ProfilerNode } from "../types"; import { getNowTime } from "@player-devtools/plugin"; -export const profiler = (onUpdate?: () => void): Profiler => { - let rootNodes: ProfilerNode[] = []; - let stack: ProfilerNode[] = []; - let durations: { hookName: string; duration: number }[] = []; - - const start = () => { - rootNodes = []; - stack = []; - durations = []; - }; - - const cloneNode = ( - node: ProfilerNode, - snapshotTime?: number, - ): ProfilerNode => { - const children = node.children.map((c) => cloneNode(c, snapshotTime)); +export class Profiler { + private rootNodes: ProfilerNode[] = []; + private stack: ProfilerNode[] = []; + private durations: { hookName: string; duration: number }[] = []; + + constructor(private readonly onUpdate?: () => void) {} + + start(): void { + this.rootNodes = []; + this.stack = []; + this.durations = []; + } + + private cloneNode(node: ProfilerNode, snapshotTime?: number): ProfilerNode { + const children = node.children.map((c) => this.cloneNode(c, snapshotTime)); const endTime = node.endTime ?? (snapshotTime !== undefined ? snapshotTime : undefined); const value = @@ -31,56 +30,13 @@ export const profiler = (onUpdate?: () => void): Profiler => { value, children, }; - }; - - /** - * Inserts synthetic spacer nodes to represent idle time between a parent's - * startTime and its first child, and between consecutive siblings. This makes - * the flame graph accurate to the real timeline rather than showing hooks - * back-to-back regardless of when they fired. - */ - const insertSpacers = (node: ProfilerNode): ProfilerNode => { - if ( - node.children.length === 0 || - node.startTime === undefined || - node.endTime === undefined - ) { - return { ...node }; - } - - const spacedChildren: ProfilerNode[] = []; - let cursor = node.startTime; - - for (const child of node.children) { - if (child.startTime === undefined || child.endTime === undefined) { - spacedChildren.push(insertSpacers(child)); - continue; - } - - const gap = child.startTime - cursor; - if (gap > 0) { - spacedChildren.push({ - name: "(work)", - value: Math.ceil(gap * 1000), - children: [], - backgroundColor: "#000000", - color: "#000000", - tooltip: "Placeholder time between hooks", - }); - } - - spacedChildren.push(insertSpacers(child)); - cursor = child.endTime; - } - - return { ...node, children: spacedChildren }; - }; + } - const getSnapshot = (): { + getSnapshot(): { rootNodes: ProfilerNode[]; durations: { name: string; duration: string }[]; - } => { - const sorted = [...durations] + } { + const sorted = [...this.durations] .sort((a, b) => b.duration - a.duration) .map(({ hookName, duration }) => ({ name: hookName, @@ -88,38 +44,38 @@ export const profiler = (onUpdate?: () => void): Profiler => { })); const now = getNowTime(); return { - rootNodes: rootNodes.map((n) => cloneNode(n, now)).map(insertSpacers), + rootNodes: this.rootNodes.map((n) => this.cloneNode(n, now)), durations: sorted, }; - }; + } - const startTimer = (hookName: string) => { + startTimer(hookName: string): void { const node: ProfilerNode = { name: hookName, startTime: getNowTime(), children: [], }; - if (stack.length > 0) { - stack[stack.length - 1]!.children.push(node); + if (this.stack.length > 0) { + this.stack[this.stack.length - 1]!.children.push(node); } else { - rootNodes.push(node); + this.rootNodes.push(node); } - stack.push(node); - }; + this.stack.push(node); + } - const finalizeNode = (node: ProfilerNode, endTime: number) => { + private finalizeNode(node: ProfilerNode, endTime: number): void { const duration = node.startTime !== undefined ? endTime - node.startTime : 0.01; node.endTime = endTime; node.value = Math.ceil(duration * 1000); node.tooltip = `${node.name}, ${duration.toFixed(4)} (ms)`; - durations.push({ hookName: node.name, duration }); - }; + this.durations.push({ hookName: node.name, duration }); + } - const endTimer = ({ hookName }: { hookName: string }) => { - const idx = [...stack].reverse().findIndex((n) => n.name === hookName); + endTimer({ hookName }: { hookName: string }): void { + const idx = [...this.stack].reverse().findIndex((n) => n.name === hookName); if (idx === -1) { console.warn(`endTimer: '${hookName}' not found in stack, ignoring`); @@ -127,48 +83,39 @@ export const profiler = (onUpdate?: () => void): Profiler => { } // stack index of the target (reverse idx → forward idx) - const targetIdx = stack.length - 1 - idx; + const targetIdx = this.stack.length - 1 - idx; const endTime = getNowTime(); // Pop and finalize everything above the target, from top down - for (let i = stack.length - 1; i > targetIdx; i--) { - const orphan = stack[i]!; + for (let i = this.stack.length - 1; i > targetIdx; i--) { + const orphan = this.stack[i]!; console.warn( `endTimer: popping '${orphan.name}' — timer was never explicitly ended`, ); - finalizeNode(orphan, endTime); + this.finalizeNode(orphan, endTime); } // Finalize the target - finalizeNode(stack[targetIdx]!, endTime); + this.finalizeNode(this.stack[targetIdx]!, endTime); // Truncate stack - stack.length = targetIdx; + this.stack.length = targetIdx; - onUpdate?.(); - }; + this.onUpdate?.(); + } - const stopProfiler = (): { + stopProfiler(): { rootNodes: ProfilerNode[]; durations: { name: string; duration: string }[]; - } => { - durations.sort((a, b) => b.duration - a.duration); + } { + this.durations.sort((a, b) => b.duration - a.duration); return { - rootNodes: rootNodes.map(insertSpacers), - durations: durations.map(({ hookName, duration }) => ({ + rootNodes: [...this.rootNodes], + durations: this.durations.map(({ hookName, duration }) => ({ name: hookName, duration: `${duration.toFixed(4)} ms`, })), }; - }; - - return { - start, - startTimer, - endTimer, - stopProfiler, - getSnapshot, - insertSpacers, - }; -}; + } +} diff --git a/devtools/plugins/profiler/core/src/helpers/transformProfilerData.ts b/devtools/plugins/profiler/core/src/helpers/transformProfilerData.ts new file mode 100644 index 0000000..25fa2fa --- /dev/null +++ b/devtools/plugins/profiler/core/src/helpers/transformProfilerData.ts @@ -0,0 +1,51 @@ +import type { ProfilerNode } from "../types"; + +const createSpacer = (gap: number) => ({ + name: "(work)", + value: Math.ceil(gap * 1000), + children: [], + backgroundColor: "#000000", + color: "#000000", + tooltip: "Placeholder time between hooks", +}); + +export const transformProfilerData = (root: ProfilerNode): ProfilerNode => { + return { + ...root, + children: transformProfilerDataHelper(root.children, root.startTime), + }; +}; + +const transformProfilerDataHelper = ( + nodes: ProfilerNode[], + parentStart: number = 0 +): ProfilerNode[] => { + const merged: ProfilerNode[] = []; + let cursor = parentStart; + + for (const node of nodes) { + if ( + node.startTime === undefined || + node.endTime === undefined || + node.value === undefined || + node.value <= 0 + ) { + continue; + } + + const next = { + ...node, + children: transformProfilerDataHelper(node.children, node.startTime), + }; + + const gap = node.startTime - cursor; + if (gap > 0) { + merged.push(createSpacer(gap)); + } + cursor = node.endTime; + + merged.push(next); + } + + return merged; +}; diff --git a/devtools/plugins/profiler/core/src/plugin-flow.json b/devtools/plugins/profiler/core/src/plugin-flow.json index 5ebee65..1bb3937 100755 --- a/devtools/plugins/profiler/core/src/plugin-flow.json +++ b/devtools/plugins/profiler/core/src/plugin-flow.json @@ -38,19 +38,12 @@ "value": "Flame Graph:" } }, - { - "asset": { - "id": "Profile-index", - "binding": "profilerIndex", - "type": "input" - } - }, { "asset": { "id": "Profile-main", "type": "flame-graph", - "binding": "rootNodes[{{profilerIndex}}]", - "width": "1500", + "binding": "rootNode", + "width": "@[{{rootNode.value}} / 200]@", "label": { "asset": { "id": "Profile-main-label", @@ -139,8 +132,9 @@ "data": { "profiling": false, "displayFlameGraph": false, - "rootNodes": [], - "durations": [], - "profilerIndex": 0 + "rootNode": { + "value": 0 + }, + "durations": [] } } diff --git a/devtools/plugins/profiler/core/src/plugin.ts b/devtools/plugins/profiler/core/src/plugin.ts index b2a50f2..405f05d 100644 --- a/devtools/plugins/profiler/core/src/plugin.ts +++ b/devtools/plugins/profiler/core/src/plugin.ts @@ -11,11 +11,37 @@ import type { Flow, Player } from "@player-ui/player"; import { dset } from "dset/merge"; import { produce } from "immer"; import { BASE_PLUGIN_DATA, INTERACTIONS } from "./constants"; -import { profiler } from "./helpers"; -import type { Profiler, ProfilerNode } from "./types"; +import { Profiler, transformProfilerData } from "./helpers"; +import type { ProfilerNode } from "./types"; import { addProfilerInterceptorsToHooks } from "./addProfilerInterceptorsToHooks"; import flow from "./plugin-flow.json"; +const wrapInRoot = (nodes: ProfilerNode[]): ProfilerNode => { + const startTime = + nodes.reduce( + (min, n) => + n.startTime !== undefined && (min === undefined || n.startTime < min) + ? n.startTime + : min, + undefined + ) ?? 0; + const endTime = + nodes.reduce( + (max, n) => + n.endTime !== undefined && (max === undefined || n.endTime > max) + ? n.endTime + : max, + undefined + ) ?? startTime; + return { + name: "root", + startTime, + endTime, + value: Math.ceil((endTime - startTime) * 1000), + children: nodes, + }; +}; + const pluginData: PluginData = { ...BASE_PLUGIN_DATA, flow: flow as Flow, @@ -24,156 +50,105 @@ const pluginData: PluginData = { const pluginID = pluginData.id; export class ProfilerDevtoolsPlugin extends DevtoolsPlugin { + name = "ProfilerDevtoolsPlugin"; + + private readonly profilerObj: Profiler; + constructor(options: Omit) { super({ ...options, pluginData, }); - } - - name = "ProfilerDevtoolsPlugin"; - - private profilerObj?: Profiler; - - startProfiler?: () => void; - stopProfiler?: Profiler["stopProfiler"]; - private transformProfilerData(nodes: ProfilerNode[]): ProfilerNode[] { - let previous: ProfilerNode | undefined; - const result: ProfilerNode[] = []; - - for (const node of nodes) { - if (node.value === undefined || node.value <= 0) { - continue; - } - - if (node.name === "(work)" && previous?.name === "(work)") { - previous.value = (previous.value ?? 0) + node.value; - previous.endTime = node.endTime; - continue; - } - - previous = { - ...node, - children: this.transformProfilerData(node.children), - }; - - result.push(previous); - } - - return result; - } - - apply(player: Player): void { - if (!this.checkIfDevtoolsIsActive()) { - return; - } - - super.apply(player); - - // Wire live updates: dispatch to store on every endTimer call - this.profilerObj = profiler(() => { - if (!this.profilerObj) return; + this.profilerObj = new Profiler(() => { const { durations, rootNodes } = this.profilerObj.getSnapshot(); const newState = this.produceState( [["plugins", pluginID, "flow", "data", "durations"], durations], [ - ["plugins", pluginID, "flow", "data", "rootNodes"], - this.transformProfilerData(rootNodes), - ], + ["plugins", pluginID, "flow", "data", "rootNode"], + transformProfilerData(wrapInRoot(rootNodes)), + ] ); this.store.dispatch( genDataChangeTransaction({ playerID: this.playerID, data: newState.plugins[pluginID]?.flow.data, pluginID, - }), + }) ); }); + } - this.startProfiler = this.createProfileStartFunction(player); - this.stopProfiler = this.createProfilerStopFunction(player); - - // Hook once for the lifetime of this Player instance - addProfilerInterceptorsToHooks(player, this.profilerObj); - - // Dispatch initial profiling-active state - const initialState = produce(this.store.getState(), (draft) => { + private startProfiler(): void { + this.profilerObj.start(); + const newState = produce(this.store.getState(), (draft) => { dset(draft, ["plugins", pluginID, "flow", "data", "profiling"], true); dset( draft, ["plugins", pluginID, "flow", "data", "displayFlameGraph"], - false, + false ); }); - this.store.dispatch( genDataChangeTransaction({ playerID: this.playerID, - data: initialState.plugins[pluginID]?.flow.data, + data: newState.plugins[pluginID]?.flow.data, pluginID, - }), + }) ); } - private createProfileStartFunction = (player: Player): (() => void) => { - return () => { - if (!this.profilerObj) return; - player.logger.debug("[ProfilerPlugin]: Starting..."); - - // Reset internal profiler state; interceptors remain on the hooks - this.profilerObj.start(); - - const newState = produce(this.store.getState(), (draft) => { - dset(draft, ["plugins", pluginID, "flow", "data", "profiling"], true); - dset( - draft, - ["plugins", pluginID, "flow", "data", "displayFlameGraph"], - false, - ); - }); + private stopProfiler(): ReturnType { + const result = this.profilerObj.stopProfiler(); + const { rootNodes, durations } = result; + const newState = this.produceState( + [ + ["plugins", pluginID, "flow", "data", "rootNode"], + transformProfilerData(wrapInRoot(rootNodes)), + ], + [["plugins", pluginID, "flow", "data", "durations"], durations], + [["plugins", pluginID, "flow", "data", "profiling"], false], + [["plugins", pluginID, "flow", "data", "displayFlameGraph"], true] + ); + this.store.dispatch( + genDataChangeTransaction({ + playerID: this.playerID, + data: newState.plugins[pluginID]?.flow.data, + pluginID, + }) + ); + return result; + } - this.store.dispatch( - genDataChangeTransaction({ - playerID: this.playerID, - data: newState.plugins[pluginID]?.flow.data, - pluginID, - }), - ); - }; - }; + apply(player: Player): void { + if (!this.checkIfDevtoolsIsActive()) { + return; + } - private createProfilerStopFunction = ( - player: Player, - ): Profiler["stopProfiler"] => { - return () => { - if (!this.profilerObj) return { rootNodes: [], durations: [] }; - player.logger.debug("[ProfilerPlugin]: Stopping..."); - const { stopProfiler } = this.profilerObj; - const stopProfilerResult = stopProfiler(); - const { rootNodes, durations } = stopProfilerResult; + super.apply(player); - const newState = this.produceState( - [ - ["plugins", pluginID, "flow", "data", "rootNodes"], - this.transformProfilerData(rootNodes), - ], - [["plugins", pluginID, "flow", "data", "durations"], durations], - [["plugins", pluginID, "flow", "data", "profiling"], false], - [["plugins", pluginID, "flow", "data", "displayFlameGraph"], true], - ); + // Hook once for the lifetime of this Player instance + addProfilerInterceptorsToHooks(player, this.profilerObj); - this.store.dispatch( - genDataChangeTransaction({ - playerID: this.playerID, - data: newState.plugins[pluginID]?.flow.data, - pluginID, - }), + // Start profiling and dispatch initial state + this.profilerObj.start(); + const initialState = produce(this.store.getState(), (draft) => { + dset(draft, ["plugins", pluginID, "flow", "data", "profiling"], true); + dset( + draft, + ["plugins", pluginID, "flow", "data", "displayFlameGraph"], + false ); + }); - return stopProfilerResult; - }; - }; + this.store.dispatch( + genDataChangeTransaction({ + playerID: this.playerID, + data: initialState.plugins[pluginID]?.flow.data, + pluginID, + }) + ); + } processInteraction(interaction: DevtoolsPluginInteractionEvent): void { super.processInteraction(interaction); @@ -182,11 +157,11 @@ export class ProfilerDevtoolsPlugin extends DevtoolsPlugin { payload: { type }, } = interaction; - if (type === INTERACTIONS.START_PROFILING && this.startProfiler) { + if (type === INTERACTIONS.START_PROFILING) { this.startProfiler(); } - if (type === INTERACTIONS.STOP_PROFILING && this.stopProfiler) { + if (type === INTERACTIONS.STOP_PROFILING) { this.stopProfiler(); } } diff --git a/devtools/plugins/profiler/core/src/types.ts b/devtools/plugins/profiler/core/src/types.ts index 5d88db5..add8b4b 100644 --- a/devtools/plugins/profiler/core/src/types.ts +++ b/devtools/plugins/profiler/core/src/types.ts @@ -1,18 +1,3 @@ -export interface Profiler { - start(): void; - startTimer(hookName: string): void; - endTimer(args: { hookName: string }): void; - stopProfiler(): { - rootNodes: ProfilerNode[]; - durations: { name: string; duration: string }[]; - }; - getSnapshot(): { - rootNodes: ProfilerNode[]; - durations: { name: string; duration: string }[]; - }; - insertSpacers(node: ProfilerNode): ProfilerNode; -} - export type ProfilerNode = { /** hook name */ name: string; From c4f59b409ec29a23c211d2ceb917e8f0ce2ec8c3 Mon Sep 17 00:00:00 2001 From: Thomas Marmer Date: Thu, 7 May 2026 14:49:20 -0400 Subject: [PATCH 07/10] add raw data tab to profiler plugin --- .../__snapshots__/plugin.test.ts.snap | 495 ++++++++++++++++++ .../profiler/core/src/plugin-flow.json | 95 +++- devtools/plugins/profiler/core/src/plugin.ts | 4 +- 3 files changed, 590 insertions(+), 4 deletions(-) diff --git a/devtools/plugins/profiler/core/src/__tests__/__snapshots__/plugin.test.ts.snap b/devtools/plugins/profiler/core/src/__tests__/__snapshots__/plugin.test.ts.snap index 16504a8..a9b16c8 100644 --- a/devtools/plugins/profiler/core/src/__tests__/__snapshots__/plugin.test.ts.snap +++ b/devtools/plugins/profiler/core/src/__tests__/__snapshots__/plugin.test.ts.snap @@ -250,6 +250,501 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] }, ], "profiling": false, + "rawNodes": [ + { + "children": [], + "endTime": 2490.2, + "name": "state", + "startTime": 2490.1, + "tooltip": "state, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2490.4999999999995, + "name": "resolveFlowContent", + "startTime": 2490.3999999999996, + "tooltip": "resolveFlowContent, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2490.7999999999993, + "name": "onStart", + "startTime": 2490.6999999999994, + "tooltip": "onStart, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2491.099999999999, + "name": "flowController", + "startTime": 2490.999999999999, + "tooltip": "flowController, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2491.3999999999987, + "name": "bindingParser", + "startTime": 2491.299999999999, + "tooltip": "bindingParser, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2491.6999999999985, + "name": "schema", + "startTime": 2491.5999999999985, + "tooltip": "schema, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2491.999999999998, + "name": "validationController", + "startTime": 2491.8999999999983, + "tooltip": "validationController, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2492.299999999998, + "name": "expressionEvaluator", + "startTime": 2492.199999999998, + "tooltip": "expressionEvaluator, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2492.5999999999976, + "name": "dataController", + "startTime": 2492.4999999999977, + "tooltip": "dataController, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2492.8999999999974, + "name": "viewController", + "startTime": 2492.7999999999975, + "tooltip": "viewController, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2493.199999999997, + "name": "state", + "startTime": 2493.099999999997, + "tooltip": "state, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2493.499999999997, + "name": "flow", + "startTime": 2493.399999999997, + "tooltip": "flow, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2493.7999999999965, + "name": "beforeStart", + "startTime": 2493.6999999999966, + "tooltip": "beforeStart, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2494.0999999999963, + "name": "resolveTransitionNode", + "startTime": 2493.9999999999964, + "tooltip": "resolveTransitionNode, 0.1000 (ms)", + "value": 100, + }, + { + "children": [ + { + "children": [], + "endTime": 2494.499999999996, + "name": "resolveView", + "startTime": 2494.399999999996, + "tooltip": "resolveView, 0.1000 (ms)", + "value": 100, + }, + { + "children": [ + { + "children": [], + "endTime": 2494.8999999999955, + "name": "onTemplatePluginCreated", + "startTime": 2494.7999999999956, + "tooltip": "onTemplatePluginCreated, 0.1000 (ms)", + "value": 100, + }, + ], + "endTime": 2495.0999999999954, + "name": "view", + "startTime": 2494.6999999999957, + "tooltip": "view, 0.4000 (ms)", + "value": 400, + }, + { + "children": [], + "endTime": 2495.399999999995, + "name": "templatePlugin", + "startTime": 2495.299999999995, + "tooltip": "templatePlugin, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2495.699999999995, + "name": "parser", + "startTime": 2495.599999999995, + "tooltip": "parser, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2495.9999999999945, + "name": "parseNode", + "startTime": 2495.8999999999946, + "tooltip": "parseNode, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2496.2999999999943, + "name": "onParseObject", + "startTime": 2496.1999999999944, + "tooltip": "onParseObject, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2496.599999999994, + "name": "parseNode", + "startTime": 2496.499999999994, + "tooltip": "parseNode, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2496.8999999999937, + "name": "parseNode", + "startTime": 2496.799999999994, + "tooltip": "parseNode, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2497.1999999999935, + "name": "parseNode", + "startTime": 2497.0999999999935, + "tooltip": "parseNode, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2497.499999999993, + "name": "onCreateASTNode", + "startTime": 2497.3999999999933, + "tooltip": "onCreateASTNode, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2497.799999999993, + "name": "resolver", + "startTime": 2497.699999999993, + "tooltip": "resolver, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2498.0999999999926, + "name": "beforeUpdate", + "startTime": 2497.9999999999927, + "tooltip": "beforeUpdate, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2498.3999999999924, + "name": "resolveOptions", + "startTime": 2498.2999999999925, + "tooltip": "resolveOptions, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2498.699999999992, + "name": "skipResolve", + "startTime": 2498.599999999992, + "tooltip": "skipResolve, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2498.999999999992, + "name": "beforeResolve", + "startTime": 2498.899999999992, + "tooltip": "beforeResolve, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2499.2999999999915, + "name": "resolve", + "startTime": 2499.1999999999916, + "tooltip": "resolve, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2499.5999999999913, + "name": "afterResolve", + "startTime": 2499.4999999999914, + "tooltip": "afterResolve, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2499.899999999991, + "name": "afterNodeUpdate", + "startTime": 2499.799999999991, + "tooltip": "afterNodeUpdate, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2500.1999999999907, + "name": "afterUpdate", + "startTime": 2500.099999999991, + "tooltip": "afterUpdate, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2500.4999999999905, + "name": "onUpdate", + "startTime": 2500.3999999999905, + "tooltip": "onUpdate, 0.1000 (ms)", + "value": 100, + }, + ], + "endTime": 2500.6999999999903, + "name": "transition", + "startTime": 2494.299999999996, + "tooltip": "transition, 6.4000 (ms)", + "value": 6400, + }, + { + "children": [], + "endTime": 2500.99999999999, + "name": "afterTransition", + "startTime": 2500.89999999999, + "tooltip": "afterTransition, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2501.2999999999897, + "name": "skipTransition", + "startTime": 2501.19999999999, + "tooltip": "skipTransition, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2501.5999999999894, + "name": "beforeTransition", + "startTime": 2501.4999999999895, + "tooltip": "beforeTransition, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2501.899999999989, + "name": "resolveTransitionNode", + "startTime": 2501.7999999999893, + "tooltip": "resolveTransitionNode, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2502.199999999989, + "name": "transition", + "startTime": 2502.099999999989, + "tooltip": "transition, 0.1000 (ms)", + "value": 100, + }, + { + "children": [ + { + "children": [], + "endTime": 2502.5999999999885, + "name": "resolveOptions", + "startTime": 2502.4999999999886, + "tooltip": "resolveOptions, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2502.8999999999883, + "name": "beforeEvaluate", + "startTime": 2502.7999999999884, + "tooltip": "beforeEvaluate, 0.1000 (ms)", + "value": 100, + }, + { + "children": [ + { + "children": [], + "endTime": 2503.299999999988, + "name": "resolve", + "startTime": 2503.199999999988, + "tooltip": "resolve, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2503.5999999999876, + "name": "skipOptimization", + "startTime": 2503.4999999999877, + "tooltip": "skipOptimization, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2503.8999999999874, + "name": "resolveDataStages", + "startTime": 2503.7999999999874, + "tooltip": "resolveDataStages, 0.1000 (ms)", + "value": 100, + }, + { + "children": [ + { + "children": [], + "endTime": 2504.299999999987, + "name": "resolveTypeForBinding", + "startTime": 2504.199999999987, + "tooltip": "resolveTypeForBinding, 0.1000 (ms)", + "value": 100, + }, + ], + "endTime": 2504.499999999987, + "name": "resolveDefaultValue", + "startTime": 2504.099999999987, + "tooltip": "resolveDefaultValue, 0.4000 (ms)", + "value": 400, + }, + { + "children": [], + "endTime": 2504.7999999999865, + "name": "onGet", + "startTime": 2504.6999999999866, + "tooltip": "onGet, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2505.0999999999863, + "name": "onSet", + "startTime": 2504.9999999999864, + "tooltip": "onSet, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2505.399999999986, + "name": "onUpdate", + "startTime": 2505.299999999986, + "tooltip": "onUpdate, 0.1000 (ms)", + "value": 100, + }, + ], + "endTime": 2505.599999999986, + "name": "resolve", + "startTime": 2503.099999999988, + "tooltip": "resolve, 2.5000 (ms)", + "value": 2500, + }, + { + "children": [], + "endTime": 2505.8999999999855, + "name": "skipTransition", + "startTime": 2505.7999999999856, + "tooltip": "skipTransition, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2506.1999999999853, + "name": "beforeTransition", + "startTime": 2506.0999999999854, + "tooltip": "beforeTransition, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2506.499999999985, + "name": "resolveTransitionNode", + "startTime": 2506.399999999985, + "tooltip": "resolveTransitionNode, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2506.7999999999847, + "name": "transition", + "startTime": 2506.699999999985, + "tooltip": "transition, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2507.0999999999844, + "name": "afterTransition", + "startTime": 2506.9999999999845, + "tooltip": "afterTransition, 0.1000 (ms)", + "value": 100, + }, + ], + "endTime": 2507.2999999999843, + "name": "afterTransition", + "startTime": 2502.3999999999887, + "tooltip": "afterTransition, 4.9000 (ms)", + "value": 4900, + }, + { + "children": [], + "endTime": 2507.599999999984, + "name": "onGet", + "startTime": 2507.499999999984, + "tooltip": "onGet, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2507.8999999999837, + "name": "serialize", + "startTime": 2507.799999999984, + "tooltip": "serialize, 0.1000 (ms)", + "value": 100, + }, + { + "children": [], + "endTime": 2508.1999999999834, + "name": "state", + "startTime": 2508.0999999999835, + "tooltip": "state, 0.1000 (ms)", + "value": 100, + }, + ], "rootNode": { "children": [ { diff --git a/devtools/plugins/profiler/core/src/plugin-flow.json b/devtools/plugins/profiler/core/src/plugin-flow.json index 1bb3937..7301a6a 100755 --- a/devtools/plugins/profiler/core/src/plugin-flow.json +++ b/devtools/plugins/profiler/core/src/plugin-flow.json @@ -22,6 +22,20 @@ } } } + }, + { + "asset": { + "id": "Profile-header-values-1", + "type": "action", + "value": "Raw", + "label": { + "asset": { + "id": "Profile-header-values-1-label", + "type": "text", + "value": "Raw" + } + } + } } ] } @@ -58,12 +72,73 @@ }, "footer": { "asset": { - "id": "Editor-header-values-0", + "id": "Profile-footer-action", "type": "action", "exp": "conditional({{profiling}} === true, publish('stop-profiling'), publish('start-profiling'))", "label": { "asset": { - "id": "Editor-header-values-0-label", + "id": "Profile-footer-action-label", + "type": "text", + "value": "@[conditional({{profiling}} === true, 'Stop', 'Start')]@" + } + } + } + } + }, + { + "id": "Raw", + "type": "stacked-view", + "header": { + "asset": { + "id": "Raw-header", + "type": "navigation", + "values": [ + { + "asset": { + "id": "Raw-header-values-0", + "type": "action", + "value": "Profile", + "label": { + "asset": { + "id": "Raw-header-values-0-label", + "type": "text", + "value": "Profile" + } + } + } + }, + { + "asset": { + "id": "Raw-header-values-1", + "type": "action", + "value": "Raw", + "label": { + "asset": { + "id": "Raw-header-values-1-label", + "type": "text", + "value": "Raw" + } + } + } + } + ] + } + }, + "main": { + "asset": { + "id": "Raw-main", + "type": "object-inspector", + "binding": "rawNodes" + } + }, + "footer": { + "asset": { + "id": "Raw-footer-action", + "type": "action", + "exp": "conditional({{profiling}} === true, publish('stop-profiling'), publish('start-profiling'))", + "label": { + "asset": { + "id": "Raw-footer-action-label", "type": "text", "value": "@[conditional({{profiling}} === true, 'Stop', 'Start')]@" } @@ -80,7 +155,16 @@ "state_type": "VIEW", "ref": "Profile", "transitions": { - "Profile": "PROFILE" + "Profile": "PROFILE", + "Raw": "RAW" + } + }, + "RAW": { + "state_type": "VIEW", + "ref": "Raw", + "transitions": { + "Profile": "PROFILE", + "Raw": "RAW" } } } @@ -97,6 +181,10 @@ "type": "RecordType", "isArray": true }, + "rawNodes": { + "type": "RecordType", + "isArray": true + }, "durations": { "type": "durationType", "isArray": true @@ -135,6 +223,7 @@ "rootNode": { "value": 0 }, + "rawNodes": [], "durations": [] } } diff --git a/devtools/plugins/profiler/core/src/plugin.ts b/devtools/plugins/profiler/core/src/plugin.ts index 405f05d..17fa2f8 100644 --- a/devtools/plugins/profiler/core/src/plugin.ts +++ b/devtools/plugins/profiler/core/src/plugin.ts @@ -67,7 +67,8 @@ export class ProfilerDevtoolsPlugin extends DevtoolsPlugin { [ ["plugins", pluginID, "flow", "data", "rootNode"], transformProfilerData(wrapInRoot(rootNodes)), - ] + ], + [["plugins", pluginID, "flow", "data", "rawNodes"], rootNodes] ); this.store.dispatch( genDataChangeTransaction({ @@ -106,6 +107,7 @@ export class ProfilerDevtoolsPlugin extends DevtoolsPlugin { ["plugins", pluginID, "flow", "data", "rootNode"], transformProfilerData(wrapInRoot(rootNodes)), ], + [["plugins", pluginID, "flow", "data", "rawNodes"], rootNodes], [["plugins", pluginID, "flow", "data", "durations"], durations], [["plugins", pluginID, "flow", "data", "profiling"], false], [["plugins", pluginID, "flow", "data", "displayFlameGraph"], true] From 1df221c60c59fc0d1ee70b4e7e6bbd51636c2a4b Mon Sep 17 00:00:00 2001 From: Thomas Marmer Date: Thu, 7 May 2026 15:47:47 -0400 Subject: [PATCH 08/10] add clear option for profiler --- .../__snapshots__/plugin.test.ts.snap | 246 ------------------ .../core/src/__tests__/plugin.test.ts | 4 +- .../plugins/profiler/core/src/constants.ts | 1 + .../__snapshots__/profiler.test.ts.snap | 18 -- .../src/helpers/__tests__/profiler.test.ts | 91 +++++-- .../profiler/core/src/helpers/profiler.ts | 49 ++-- .../profiler/core/src/plugin-flow.json | 113 ++++---- devtools/plugins/profiler/core/src/plugin.ts | 24 +- 8 files changed, 172 insertions(+), 374 deletions(-) delete mode 100644 devtools/plugins/profiler/core/src/helpers/__tests__/__snapshots__/profiler.test.ts.snap diff --git a/devtools/plugins/profiler/core/src/__tests__/__snapshots__/plugin.test.ts.snap b/devtools/plugins/profiler/core/src/__tests__/__snapshots__/plugin.test.ts.snap index a9b16c8..a943604 100644 --- a/devtools/plugins/profiler/core/src/__tests__/__snapshots__/plugin.test.ts.snap +++ b/devtools/plugins/profiler/core/src/__tests__/__snapshots__/plugin.test.ts.snap @@ -3,252 +3,6 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] = ` { "displayFlameGraph": true, - "durations": [ - { - "duration": "6.4000 ms", - "name": "transition", - }, - { - "duration": "4.9000 ms", - "name": "afterTransition", - }, - { - "duration": "2.5000 ms", - "name": "resolve", - }, - { - "duration": "0.4000 ms", - "name": "view", - }, - { - "duration": "0.4000 ms", - "name": "resolveDefaultValue", - }, - { - "duration": "0.1000 ms", - "name": "state", - }, - { - "duration": "0.1000 ms", - "name": "resolveFlowContent", - }, - { - "duration": "0.1000 ms", - "name": "onStart", - }, - { - "duration": "0.1000 ms", - "name": "flowController", - }, - { - "duration": "0.1000 ms", - "name": "bindingParser", - }, - { - "duration": "0.1000 ms", - "name": "schema", - }, - { - "duration": "0.1000 ms", - "name": "validationController", - }, - { - "duration": "0.1000 ms", - "name": "expressionEvaluator", - }, - { - "duration": "0.1000 ms", - "name": "dataController", - }, - { - "duration": "0.1000 ms", - "name": "viewController", - }, - { - "duration": "0.1000 ms", - "name": "state", - }, - { - "duration": "0.1000 ms", - "name": "flow", - }, - { - "duration": "0.1000 ms", - "name": "beforeStart", - }, - { - "duration": "0.1000 ms", - "name": "resolveTransitionNode", - }, - { - "duration": "0.1000 ms", - "name": "resolveView", - }, - { - "duration": "0.1000 ms", - "name": "onTemplatePluginCreated", - }, - { - "duration": "0.1000 ms", - "name": "templatePlugin", - }, - { - "duration": "0.1000 ms", - "name": "parser", - }, - { - "duration": "0.1000 ms", - "name": "parseNode", - }, - { - "duration": "0.1000 ms", - "name": "onParseObject", - }, - { - "duration": "0.1000 ms", - "name": "parseNode", - }, - { - "duration": "0.1000 ms", - "name": "parseNode", - }, - { - "duration": "0.1000 ms", - "name": "parseNode", - }, - { - "duration": "0.1000 ms", - "name": "onCreateASTNode", - }, - { - "duration": "0.1000 ms", - "name": "resolver", - }, - { - "duration": "0.1000 ms", - "name": "beforeUpdate", - }, - { - "duration": "0.1000 ms", - "name": "resolveOptions", - }, - { - "duration": "0.1000 ms", - "name": "skipResolve", - }, - { - "duration": "0.1000 ms", - "name": "beforeResolve", - }, - { - "duration": "0.1000 ms", - "name": "resolve", - }, - { - "duration": "0.1000 ms", - "name": "afterResolve", - }, - { - "duration": "0.1000 ms", - "name": "afterNodeUpdate", - }, - { - "duration": "0.1000 ms", - "name": "afterUpdate", - }, - { - "duration": "0.1000 ms", - "name": "onUpdate", - }, - { - "duration": "0.1000 ms", - "name": "afterTransition", - }, - { - "duration": "0.1000 ms", - "name": "skipTransition", - }, - { - "duration": "0.1000 ms", - "name": "beforeTransition", - }, - { - "duration": "0.1000 ms", - "name": "resolveTransitionNode", - }, - { - "duration": "0.1000 ms", - "name": "transition", - }, - { - "duration": "0.1000 ms", - "name": "resolveOptions", - }, - { - "duration": "0.1000 ms", - "name": "beforeEvaluate", - }, - { - "duration": "0.1000 ms", - "name": "resolve", - }, - { - "duration": "0.1000 ms", - "name": "skipOptimization", - }, - { - "duration": "0.1000 ms", - "name": "resolveDataStages", - }, - { - "duration": "0.1000 ms", - "name": "resolveTypeForBinding", - }, - { - "duration": "0.1000 ms", - "name": "onGet", - }, - { - "duration": "0.1000 ms", - "name": "onSet", - }, - { - "duration": "0.1000 ms", - "name": "onUpdate", - }, - { - "duration": "0.1000 ms", - "name": "skipTransition", - }, - { - "duration": "0.1000 ms", - "name": "beforeTransition", - }, - { - "duration": "0.1000 ms", - "name": "resolveTransitionNode", - }, - { - "duration": "0.1000 ms", - "name": "transition", - }, - { - "duration": "0.1000 ms", - "name": "afterTransition", - }, - { - "duration": "0.1000 ms", - "name": "onGet", - }, - { - "duration": "0.1000 ms", - "name": "serialize", - }, - { - "duration": "0.1000 ms", - "name": "state", - }, - ], "profiling": false, "rawNodes": [ { diff --git a/devtools/plugins/profiler/core/src/__tests__/plugin.test.ts b/devtools/plugins/profiler/core/src/__tests__/plugin.test.ts index b5750a6..206a569 100644 --- a/devtools/plugins/profiler/core/src/__tests__/plugin.test.ts +++ b/devtools/plugins/profiler/core/src/__tests__/plugin.test.ts @@ -74,12 +74,11 @@ describe("Plugin", () => { ).toBeDefined(); }); - // Live update: durations should already be populated while profiling is active + // Live update: profiling is active const liveData = profilerPlugin.store.getState().plugins["player-ui-profiler-plugin"]?.flow .data; expect(liveData?.profiling).toBe(true); - expect((liveData?.durations as unknown[]).length).toBeGreaterThan(0); // Transition to action state (player.getState() as InProgressState).controllers.flow.transition("go"); @@ -149,7 +148,6 @@ describe("Plugin", () => { .data; expect(dataAfterStop?.profiling).toBe(false); expect(dataAfterStop?.displayFlameGraph).toBe(true); - expect((dataAfterStop?.durations as unknown[]).length).toBeGreaterThan(0); // Restart — state should flip back to active profiling profilerPlugin.processInteraction({ diff --git a/devtools/plugins/profiler/core/src/constants.ts b/devtools/plugins/profiler/core/src/constants.ts index 88e1cf0..7691215 100644 --- a/devtools/plugins/profiler/core/src/constants.ts +++ b/devtools/plugins/profiler/core/src/constants.ts @@ -15,6 +15,7 @@ export const VIEWS_IDS = { export const INTERACTIONS = { START_PROFILING: "start-profiling", STOP_PROFILING: "stop-profiling", + RESET_PROFILING: "reset-profiling", }; export const BASE_PLUGIN_DATA: Omit = { diff --git a/devtools/plugins/profiler/core/src/helpers/__tests__/__snapshots__/profiler.test.ts.snap b/devtools/plugins/profiler/core/src/helpers/__tests__/__snapshots__/profiler.test.ts.snap deleted file mode 100644 index 5d64149..0000000 --- a/devtools/plugins/profiler/core/src/helpers/__tests__/__snapshots__/profiler.test.ts.snap +++ /dev/null @@ -1,18 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`Profiler > stopProfiler returns the full rootNodes forest with sorted durations 1`] = ` -[ - { - "duration": "0.3000 ms", - "name": "a", - }, - { - "duration": "0.1000 ms", - "name": "a.child", - }, - { - "duration": "0.1000 ms", - "name": "b", - }, -] -`; diff --git a/devtools/plugins/profiler/core/src/helpers/__tests__/profiler.test.ts b/devtools/plugins/profiler/core/src/helpers/__tests__/profiler.test.ts index 8ef618f..19990ea 100644 --- a/devtools/plugins/profiler/core/src/helpers/__tests__/profiler.test.ts +++ b/devtools/plugins/profiler/core/src/helpers/__tests__/profiler.test.ts @@ -25,12 +25,11 @@ describe("Profiler", () => { p.startTimer("hookB"); p.endTimer({ hookName: "hookB" }); - const { rootNodes, durations } = p.stopProfiler(); + const { rootNodes } = p.stopProfiler(); expect(rootNodes).toHaveLength(2); expect(rootNodes[0]!.name).toBe("hookA"); expect(rootNodes[1]!.name).toBe("hookB"); - expect(durations).toHaveLength(2); }); test("nested timers become children of the outer timer", () => { @@ -94,12 +93,11 @@ describe("Profiler", () => { expect.stringContaining("popping 'middle'"), ); - const { rootNodes, durations } = p.stopProfiler(); + const { rootNodes } = p.stopProfiler(); // All three should be finalized expect(rootNodes).toHaveLength(1); expect(rootNodes[0]!.name).toBe("outer"); - expect(durations).toHaveLength(3); warnSpy.mockRestore(); }); @@ -112,17 +110,15 @@ describe("Profiler", () => { p.endTimer({ hookName: "hookA" }); p.start(); - const { rootNodes, durations } = p.stopProfiler(); + const { rootNodes } = p.stopProfiler(); expect(rootNodes).toHaveLength(0); - expect(durations).toHaveLength(0); }); - test("calls onUpdate only after endTimer, not startTimer", () => { + test("calls onUpdate on endTimer() and clear(), not on startTimer() or start()", () => { const onUpdate = vi.fn(); const p = new Profiler(onUpdate); - // No auto-start, so no calls yet expect(onUpdate.mock.calls.length).toBe(0); p.start(); @@ -137,9 +133,13 @@ describe("Profiler", () => { p.startTimer("hookB"); p.endTimer({ hookName: "hookB" }); expect(onUpdate.mock.calls.length).toBe(2); + + p.startTimer("hookC"); + p.clear(); + expect(onUpdate.mock.calls.length).toBe(3); }); - test("getSnapshot returns sorted durations and a deep clone of rootNodes", () => { + test("getSnapshot returns a deep clone of rootNodes", () => { const p = new Profiler(); p.start(); @@ -152,10 +152,6 @@ describe("Profiler", () => { const snap = p.getSnapshot(); - // Sorted descending by duration — slow was measured first so has larger elapsed - expect(snap.durations[0]!.name).toBe("slow"); - expect(snap.durations[1]!.name).toBe("fast"); - expect(snap.rootNodes).toHaveLength(2); // Snapshot is a clone — adding to live tree doesn't affect it @@ -203,7 +199,69 @@ describe("Profiler", () => { expect(liveAfter.value).toBeGreaterThan(0); }); - test("stopProfiler returns the full rootNodes forest with sorted durations", () => { + describe("clear()", () => { + test("clears rootNodes but leaves in-progress timers on the stack", () => { + const p = new Profiler(); + p.start(); + + p.startTimer("finished"); + p.endTimer({ hookName: "finished" }); + + p.startTimer("inflight"); + + p.clear(); + + const { rootNodes } = p.stopProfiler(); + // rootNodes from before the clear are gone; the in-flight timer produces one new root + expect(rootNodes.find((n) => n.name === "finished")).toBeUndefined(); + expect(rootNodes.find((n) => n.name === "inflight")).toBeDefined(); + }); + + test("resets startTime of in-progress timers to current time", () => { + const p = new Profiler(); + p.start(); + + p.startTimer("inflight"); + const beforeClear = p.getSnapshot().rootNodes[0]!.startTime!; + + p.clear(); + + const afterClear = p.getSnapshot().rootNodes[0]!.startTime!; + expect(afterClear).toBeGreaterThan(beforeClear); + }); + + test("fires onUpdate", () => { + const onUpdate = vi.fn(); + const p = new Profiler(onUpdate); + p.start(); + p.startTimer("hookA"); + onUpdate.mockClear(); + + p.clear(); + + expect(onUpdate).toHaveBeenCalledTimes(1); + }); + + test("endTimer still works normally for in-progress timers after clear", () => { + const p = new Profiler(); + p.start(); + + p.startTimer("outer"); + p.startTimer("inner"); + + p.clear(); + + p.endTimer({ hookName: "inner" }); + p.endTimer({ hookName: "outer" }); + + const { rootNodes } = p.stopProfiler(); + expect(rootNodes).toHaveLength(1); + expect(rootNodes[0]!.name).toBe("outer"); + expect(rootNodes[0]!.children).toHaveLength(1); + }); + }); + + test("stopProfiler returns the full rootNodes forest", () => { const p = new Profiler(); p.start(); @@ -216,12 +274,9 @@ describe("Profiler", () => { p.startTimer("b"); p.endTimer({ hookName: "b" }); - const { rootNodes, durations } = p.stopProfiler(); + const { rootNodes } = p.stopProfiler(); expect(rootNodes).toHaveLength(2); expect(rootNodes[0]!.children).toHaveLength(1); - // durations sorted descending - expect(durations[0]!.name).toBe("a"); - expect(durations).toMatchSnapshot(); }); }); diff --git a/devtools/plugins/profiler/core/src/helpers/profiler.ts b/devtools/plugins/profiler/core/src/helpers/profiler.ts index 94cf860..0720254 100644 --- a/devtools/plugins/profiler/core/src/helpers/profiler.ts +++ b/devtools/plugins/profiler/core/src/helpers/profiler.ts @@ -4,14 +4,29 @@ import { getNowTime } from "@player-devtools/plugin"; export class Profiler { private rootNodes: ProfilerNode[] = []; private stack: ProfilerNode[] = []; - private durations: { hookName: string; duration: number }[] = []; constructor(private readonly onUpdate?: () => void) {} start(): void { this.rootNodes = []; this.stack = []; - this.durations = []; + this.onUpdate?.(); + } + + clear(): void { + const now = getNowTime(); + // Reset each in-progress node: fresh startTime, no accumulated children + for (const node of this.stack) { + node.startTime = now; + node.children = node.children.filter((x) => x.endTime === undefined); + } + // Re-wire parent→child links through the stack chain + for (let i = 1; i < this.stack.length; i++) { + this.stack[i - 1]!.children = [this.stack[i]!]; + } + // rootNodes keeps only the outermost in-progress node (if any) + this.rootNodes = this.stack.slice(0, 1); + this.onUpdate?.(); } private cloneNode(node: ProfilerNode, snapshotTime?: number): ProfilerNode { @@ -32,20 +47,10 @@ export class Profiler { }; } - getSnapshot(): { - rootNodes: ProfilerNode[]; - durations: { name: string; duration: string }[]; - } { - const sorted = [...this.durations] - .sort((a, b) => b.duration - a.duration) - .map(({ hookName, duration }) => ({ - name: hookName, - duration: `${duration.toFixed(4)} ms`, - })); + getSnapshot(): { rootNodes: ProfilerNode[] } { const now = getNowTime(); return { rootNodes: this.rootNodes.map((n) => this.cloneNode(n, now)), - durations: sorted, }; } @@ -71,7 +76,6 @@ export class Profiler { node.endTime = endTime; node.value = Math.ceil(duration * 1000); node.tooltip = `${node.name}, ${duration.toFixed(4)} (ms)`; - this.durations.push({ hookName: node.name, duration }); } endTimer({ hookName }: { hookName: string }): void { @@ -90,7 +94,7 @@ export class Profiler { for (let i = this.stack.length - 1; i > targetIdx; i--) { const orphan = this.stack[i]!; console.warn( - `endTimer: popping '${orphan.name}' — timer was never explicitly ended`, + `endTimer: popping '${orphan.name}' — timer was never explicitly ended` ); this.finalizeNode(orphan, endTime); } @@ -104,18 +108,7 @@ export class Profiler { this.onUpdate?.(); } - stopProfiler(): { - rootNodes: ProfilerNode[]; - durations: { name: string; duration: string }[]; - } { - this.durations.sort((a, b) => b.duration - a.duration); - - return { - rootNodes: [...this.rootNodes], - durations: this.durations.map(({ hookName, duration }) => ({ - name: hookName, - duration: `${duration.toFixed(4)} ms`, - })), - }; + stopProfiler(): { rootNodes: ProfilerNode[] } { + return { rootNodes: [...this.rootNodes] }; } } diff --git a/devtools/plugins/profiler/core/src/plugin-flow.json b/devtools/plugins/profiler/core/src/plugin-flow.json index 7301a6a..7a2263a 100755 --- a/devtools/plugins/profiler/core/src/plugin-flow.json +++ b/devtools/plugins/profiler/core/src/plugin-flow.json @@ -72,16 +72,38 @@ }, "footer": { "asset": { - "id": "Profile-footer-action", - "type": "action", - "exp": "conditional({{profiling}} === true, publish('stop-profiling'), publish('start-profiling'))", - "label": { - "asset": { - "id": "Profile-footer-action-label", - "type": "text", - "value": "@[conditional({{profiling}} === true, 'Stop', 'Start')]@" + "id": "Profile-footer", + "type": "collection", + "values": [ + { + "asset": { + "id": "Profile-footer-action", + "type": "action", + "exp": "conditional({{profiling}} === true, publish('stop-profiling'), publish('start-profiling'))", + "label": { + "asset": { + "id": "Profile-footer-action-label", + "type": "text", + "value": "@[conditional({{profiling}} === true, 'Stop', 'Start')]@" + } + } + } + }, + { + "asset": { + "id": "Profile-footer-clear", + "type": "action", + "exp": "publish('reset-profiling')", + "label": { + "asset": { + "id": "Profile-footer-clear-label", + "type": "text", + "value": "Reset" + } + } + } } - } + ] } } }, @@ -133,16 +155,38 @@ }, "footer": { "asset": { - "id": "Raw-footer-action", - "type": "action", - "exp": "conditional({{profiling}} === true, publish('stop-profiling'), publish('start-profiling'))", - "label": { - "asset": { - "id": "Raw-footer-action-label", - "type": "text", - "value": "@[conditional({{profiling}} === true, 'Stop', 'Start')]@" + "id": "Raw-footer", + "type": "collection", + "values": [ + { + "asset": { + "id": "Raw-footer-action", + "type": "action", + "exp": "conditional({{profiling}} === true, publish('stop-profiling'), publish('start-profiling'))", + "label": { + "asset": { + "id": "Raw-footer-action-label", + "type": "text", + "value": "@[conditional({{profiling}} === true, 'Stop', 'Start')]@" + } + } + } + }, + { + "asset": { + "id": "Raw-footer-clear", + "type": "action", + "exp": "publish('clear-profiling')", + "label": { + "asset": { + "id": "Raw-footer-clear-label", + "type": "text", + "value": "Clear" + } + } + } } - } + ] } } } @@ -184,36 +228,6 @@ "rawNodes": { "type": "RecordType", "isArray": true - }, - "durations": { - "type": "durationType", - "isArray": true - } - }, - "durationType": { - "name": { - "type": "StringType", - "default": "", - "validation": [ - { - "type": "string" - } - ], - "format": { - "type": "string" - } - }, - "duration": { - "type": "StringType", - "default": "", - "validation": [ - { - "type": "string" - } - ], - "format": { - "type": "string" - } } } }, @@ -223,7 +237,6 @@ "rootNode": { "value": 0 }, - "rawNodes": [], - "durations": [] + "rawNodes": [] } } diff --git a/devtools/plugins/profiler/core/src/plugin.ts b/devtools/plugins/profiler/core/src/plugin.ts index 17fa2f8..ffbc3b7 100644 --- a/devtools/plugins/profiler/core/src/plugin.ts +++ b/devtools/plugins/profiler/core/src/plugin.ts @@ -54,6 +54,12 @@ export class ProfilerDevtoolsPlugin extends DevtoolsPlugin { private readonly profilerObj: Profiler; + private readonly interactionMap: Map void> = new Map([ + [INTERACTIONS.START_PROFILING, () => this.startProfiler()], + [INTERACTIONS.STOP_PROFILING, () => this.stopProfiler()], + [INTERACTIONS.RESET_PROFILING, () => this.clearProfiler()], + ]); + constructor(options: Omit) { super({ ...options, @@ -61,9 +67,8 @@ export class ProfilerDevtoolsPlugin extends DevtoolsPlugin { }); this.profilerObj = new Profiler(() => { - const { durations, rootNodes } = this.profilerObj.getSnapshot(); + const { rootNodes } = this.profilerObj.getSnapshot(); const newState = this.produceState( - [["plugins", pluginID, "flow", "data", "durations"], durations], [ ["plugins", pluginID, "flow", "data", "rootNode"], transformProfilerData(wrapInRoot(rootNodes)), @@ -99,16 +104,19 @@ export class ProfilerDevtoolsPlugin extends DevtoolsPlugin { ); } + private clearProfiler(): void { + this.profilerObj.clear(); + } + private stopProfiler(): ReturnType { const result = this.profilerObj.stopProfiler(); - const { rootNodes, durations } = result; + const { rootNodes } = result; const newState = this.produceState( [ ["plugins", pluginID, "flow", "data", "rootNode"], transformProfilerData(wrapInRoot(rootNodes)), ], [["plugins", pluginID, "flow", "data", "rawNodes"], rootNodes], - [["plugins", pluginID, "flow", "data", "durations"], durations], [["plugins", pluginID, "flow", "data", "profiling"], false], [["plugins", pluginID, "flow", "data", "displayFlameGraph"], true] ); @@ -159,12 +167,6 @@ export class ProfilerDevtoolsPlugin extends DevtoolsPlugin { payload: { type }, } = interaction; - if (type === INTERACTIONS.START_PROFILING) { - this.startProfiler(); - } - - if (type === INTERACTIONS.STOP_PROFILING) { - this.stopProfiler(); - } + this.interactionMap.get(type)?.(); } } From 06811be4aa0709667189d6c34179c7908f468034 Mon Sep 17 00:00:00 2001 From: Thomas Marmer Date: Fri, 8 May 2026 12:18:56 -0400 Subject: [PATCH 09/10] split swift implementation into swiftui and ios plugins --- .../__tests__/transformProfilerData.test.ts | 148 +++++++++++++++++ .../profiler/core/src/plugin-flow.json | 39 ++--- devtools/plugins/profiler/ios/BUILD | 3 +- ...swift => BaseProfilerDevtoolsPlugin.swift} | 52 +----- .../BaseProfilerDevtoolsPluginTests.swift | 149 ++++++++++++++++++ .../Tests/ProfilerDevtoolsPluginTests.swift | 8 - .../profiler/ProfilerDevtoolsPluginTest.kt | 65 +++++++- devtools/plugins/profiler/swiftui/BUILD | 11 ++ .../Sources/ProfilerDevtoolsPlugin.swift | 40 +++++ .../ProfilerDevtoolsPluginTests.swift | 138 ++++++++++++++++ ios/BUILD | 3 +- ios/demo/BUILD | 2 +- ios/demo/Sources/DemoApp.swift | 2 +- 13 files changed, 569 insertions(+), 91 deletions(-) create mode 100644 devtools/plugins/profiler/core/src/helpers/__tests__/transformProfilerData.test.ts rename devtools/plugins/profiler/ios/Sources/{ProfilerDevtoolsPlugin.swift => BaseProfilerDevtoolsPlugin.swift} (54%) create mode 100644 devtools/plugins/profiler/ios/Tests/BaseProfilerDevtoolsPluginTests.swift delete mode 100644 devtools/plugins/profiler/ios/Tests/ProfilerDevtoolsPluginTests.swift create mode 100644 devtools/plugins/profiler/swiftui/BUILD create mode 100644 devtools/plugins/profiler/swiftui/Sources/ProfilerDevtoolsPlugin.swift create mode 100644 devtools/plugins/profiler/swiftui/ViewInspector/ProfilerDevtoolsPluginTests.swift diff --git a/devtools/plugins/profiler/core/src/helpers/__tests__/transformProfilerData.test.ts b/devtools/plugins/profiler/core/src/helpers/__tests__/transformProfilerData.test.ts new file mode 100644 index 0000000..1a7e329 --- /dev/null +++ b/devtools/plugins/profiler/core/src/helpers/__tests__/transformProfilerData.test.ts @@ -0,0 +1,148 @@ +import { describe, expect, test } from "vitest"; +import { transformProfilerData } from "../transformProfilerData"; +import type { ProfilerNode } from "../../types"; + +const node = ( + name: string, + startTime: number, + endTime: number, + children: ProfilerNode[] = [] +): ProfilerNode => ({ + name, + startTime, + endTime, + value: Math.ceil((endTime - startTime) * 1000), + children, +}); + +describe("transformProfilerData", () => { + test("returns root with transformed children, preserving root fields", () => { + const root = node("root", 0, 1, [node("a", 0, 1)]); + const result = transformProfilerData(root); + + expect(result.name).toBe("root"); + expect(result.startTime).toBe(0); + expect(result.endTime).toBe(1); + }); + + test("nodes with no gap between them produce no spacers", () => { + const root = node("root", 0, 2, [node("a", 0, 1), node("b", 1, 2)]); + const { children } = transformProfilerData(root); + + expect(children).toHaveLength(2); + expect(children[0]!.name).toBe("a"); + expect(children[1]!.name).toBe("b"); + }); + + test("gap before first child inserts a spacer", () => { + const root = node("root", 0, 2, [node("a", 1, 2)]); + const { children } = transformProfilerData(root); + + expect(children).toHaveLength(2); + expect(children[0]!.name).toBe("(work)"); + expect(children[0]!.value).toBe(1000); + expect(children[1]!.name).toBe("a"); + }); + + test("gap between children inserts a spacer between them", () => { + const root = node("root", 0, 3, [node("a", 0, 1), node("b", 2, 3)]); + const { children } = transformProfilerData(root); + + expect(children).toHaveLength(3); + expect(children[0]!.name).toBe("a"); + expect(children[1]!.name).toBe("(work)"); + expect(children[1]!.value).toBe(1000); + expect(children[2]!.name).toBe("b"); + }); + + test("spacer value is gap duration in milliseconds (ceil)", () => { + // gap of 0.0015 seconds → ceil(0.0015 * 1000) = ceil(1.5) = 2 + const root = node("root", 0, 2, [node("a", 0.0015, 2)]); + const { children } = transformProfilerData(root); + + expect(children[0]!.name).toBe("(work)"); + expect(children[0]!.value).toBe(2); + }); + + test("spacer has correct styling fields", () => { + const root = node("root", 0, 2, [node("a", 1, 2)]); + const { children } = transformProfilerData(root); + const spacer = children[0]!; + + expect(spacer.backgroundColor).toBe("#000000"); + expect(spacer.color).toBe("#000000"); + expect(spacer.tooltip).toBe("Placeholder time between hooks"); + expect(spacer.children).toHaveLength(0); + }); + + test("nodes missing startTime, endTime, or value are filtered out", () => { + const root: ProfilerNode = { + name: "root", + startTime: 0, + endTime: 2, + value: 2000, + children: [ + { name: "no-start", endTime: 1, value: 1000, children: [] }, + { name: "no-end", startTime: 0, value: 1000, children: [] }, + { name: "no-value", startTime: 0, endTime: 1, children: [] }, + node("valid", 0, 1), + ], + }; + + const { children } = transformProfilerData(root); + expect(children).toHaveLength(1); + expect(children[0]!.name).toBe("valid"); + }); + + test("nodes with value of zero are filtered out", () => { + const root: ProfilerNode = { + name: "root", + startTime: 0, + endTime: 2, + value: 2000, + children: [ + { name: "zero-value", startTime: 0, endTime: 0, value: 0, children: [] }, + node("valid", 0, 1), + ], + }; + + const { children } = transformProfilerData(root); + expect(children).toHaveLength(1); + expect(children[0]!.name).toBe("valid"); + }); + + test("root with no children returns empty children array", () => { + const root = node("root", 0, 1); + const { children } = transformProfilerData(root); + expect(children).toHaveLength(0); + }); + + test("nested children are recursively transformed", () => { + const inner = node("inner", 1.5, 2, []); + const outer = node("outer", 0, 2, [inner]); + const root = node("root", 0, 2, [outer]); + + const result = transformProfilerData(root); + const outerResult = result.children[0]!; + + // gap before inner inside outer → spacer inserted + expect(outerResult.children[0]!.name).toBe("(work)"); + expect(outerResult.children[1]!.name).toBe("inner"); + }); + + test("multiple gaps each produce their own spacer", () => { + const root = node("root", 0, 6, [ + node("a", 1, 2), + node("b", 4, 5), + ]); + + const { children } = transformProfilerData(root); + + // spacer(0→1), a, spacer(2→4), b + expect(children).toHaveLength(4); + expect(children[0]!.name).toBe("(work)"); + expect(children[1]!.name).toBe("a"); + expect(children[2]!.name).toBe("(work)"); + expect(children[3]!.name).toBe("b"); + }); +}); diff --git a/devtools/plugins/profiler/core/src/plugin-flow.json b/devtools/plugins/profiler/core/src/plugin-flow.json index 7a2263a..87edd1b 100755 --- a/devtools/plugins/profiler/core/src/plugin-flow.json +++ b/devtools/plugins/profiler/core/src/plugin-flow.json @@ -18,7 +18,7 @@ "asset": { "id": "Profile-header-values-0-label", "type": "text", - "value": "Profile" + "value": "Flame Graph" } } } @@ -42,32 +42,17 @@ }, "main": { "asset": { - "type": "collection", - "id": "Profile-collection", - "values": [ - { - "asset": { - "id": "Profile-title", - "type": "text", - "value": "Flame Graph:" - } - }, - { - "asset": { - "id": "Profile-main", - "type": "flame-graph", - "binding": "rootNode", - "width": "@[{{rootNode.value}} / 200]@", - "label": { - "asset": { - "id": "Profile-main-label", - "type": "text", - "value": "Profile" - } - } - } + "id": "Profile-main", + "type": "flame-graph", + "binding": "rootNode", + "width": "@[{{rootNode.value}} / 200]@", + "label": { + "asset": { + "id": "Profile-main-label", + "type": "text", + "value": "Profile" } - ] + } } }, "footer": { @@ -124,7 +109,7 @@ "asset": { "id": "Raw-header-values-0-label", "type": "text", - "value": "Profile" + "value": "Flame Graph" } } } diff --git a/devtools/plugins/profiler/ios/BUILD b/devtools/plugins/profiler/ios/BUILD index 6dfdbf9..47e18b4 100644 --- a/devtools/plugins/profiler/ios/BUILD +++ b/devtools/plugins/profiler/ios/BUILD @@ -1,11 +1,10 @@ load("//helpers:ios.bzl", "ios_library") ios_library( - name = "ProfilerDevtoolsPlugin", + name = "BaseProfilerDevtoolsPlugin", resources = ["//devtools/plugins/profiler/core:core_native_bundle"], deps = [ "//devtools/plugin/ios:PlayerUIDevtoolsPlugins", "//devtools/utils/swiftui:PlayerUIDevtoolsUtilsSwiftUI", - "//devtools/plugin/swiftui:PlayerUIDevtoolsSwiftUIPlugins", ], ) diff --git a/devtools/plugins/profiler/ios/Sources/ProfilerDevtoolsPlugin.swift b/devtools/plugins/profiler/ios/Sources/BaseProfilerDevtoolsPlugin.swift similarity index 54% rename from devtools/plugins/profiler/ios/Sources/ProfilerDevtoolsPlugin.swift rename to devtools/plugins/profiler/ios/Sources/BaseProfilerDevtoolsPlugin.swift index e1eef81..af19a00 100644 --- a/devtools/plugins/profiler/ios/Sources/ProfilerDevtoolsPlugin.swift +++ b/devtools/plugins/profiler/ios/Sources/BaseProfilerDevtoolsPlugin.swift @@ -6,16 +6,15 @@ import PlayerUIDevtoolsMessenger import PlayerUIDevtoolsTypes import JavaScriptCore import PlayerUIDevtoolsUtilsSwiftUI -import PlayerUIDevtoolsSwiftUIPlugins -public class BaseProfilerDevtoolsPlugin: JSBasePlugin, BaseDevtoolsPlugin { +open class BaseProfilerDevtoolsPlugin: JSBasePlugin, BaseDevtoolsPlugin { /// Matches `bundle_name` / `ios_library(name=...)` in `devtools/plugins/profiler/ios/BUILD` (the `apple_resource_bundle` base name). private static let pluginResourceBundleName = "ProfilerDevtoolsPlugin" private let _playerID: String // This is a var so a different handler can be provided for testing var handler: DevtoolsHandler = Handler() - + public init(playerID: String) { self._playerID = playerID super.init( @@ -23,7 +22,7 @@ public class BaseProfilerDevtoolsPlugin: JSBasePlugin, BaseDevtoolsPlugin { pluginName: "ProfilerDevtoolsPlugin.ProfilerDevtoolsPlugin" ) } - + public final override func getUrlForFile(fileName: String) -> URL? { if let url = Bundle.module.url(forResource: fileName, withExtension: "js") { return url @@ -43,55 +42,20 @@ public class BaseProfilerDevtoolsPlugin: JSBasePlugin, BaseDevtoolsPlugin { guard let context else { return [] } // TODO: replace with proper polyfill plugin after https://github.com/player-ui/player/issues/773 context.polyfill() - - // PluginData is nil. The core basic plugin provides its own plugin data + + // PluginData is nil. The core profiler plugin provides its own plugin data let options = DevtoolsPluginOptions(in: context , playerID: _playerID, handler: handler) return [options.jsCompatible] } - + /// This will process messages. The core plugin augments this handler with some logging and metadata struct Handler: DevtoolsHandler { var isActive = true - + // This plugin has no extra steps for processInteraction beyond the core impl. func processInteraction(interaction: PlayerUIDevtoolsTypes.Message) {} - + // This plugin has no extra steps for log beyond the core impl. func log(message: String) {} } } - -/// A Player Plugin that provides DevTools capabilities via Flipper. -/// This is entirely just a wrapper around the JSBasePlugin -public class ProfilerDevtoolsPlugin: BaseProfilerDevtoolsPlugin, DevtoolsPlugin { - /// Our connection to the flipper server - public let flipperPlugin: DevtoolsFlipperPlugin - /// Keep a reference so the messenger doesn't get garbage collected and destroyed - public var messenger: Messenger? - /// The IDs of all registered listeners associated with this plugin - public var listeners: [UUID] = [] - - public init(id: String, flipperPlugin: DevtoolsFlipperPlugin) { - self.flipperPlugin = flipperPlugin - super.init(playerID: id) - } - - /* Let flipper know that this plugin is going away. Deregister the listeners we - attached to the DevtoolsFlipperPlugin. - - Deinits will NOT run when the app is terminated. But if the app is terminated, - flipper will gracefully handle the abrupt, implicit disconnect, and deregistering - the listeners won't matter anymore since they won't be called if the app is dead. */ - deinit { - // If you make your own DevtoolsPlugin, you will need to implement your own - // deinit, exactly like this. The DevtoolsPlugin protocol cannot provide a deinit, - // unfortunately. - if let messenger { - messenger.destroy() - } else { - print("[DEBUG] Could not destroy messenger. Messenger already no longer exists.") - } - listeners.forEach { flipperPlugin.removeListener(id: $0) } - print("[DEBUG] BasicDevtoolsPlugin deinited") - } -} diff --git a/devtools/plugins/profiler/ios/Tests/BaseProfilerDevtoolsPluginTests.swift b/devtools/plugins/profiler/ios/Tests/BaseProfilerDevtoolsPluginTests.swift new file mode 100644 index 0000000..55a9868 --- /dev/null +++ b/devtools/plugins/profiler/ios/Tests/BaseProfilerDevtoolsPluginTests.swift @@ -0,0 +1,149 @@ +import XCTest +import JavaScriptCore +import PlayerUI +import PlayerUIDevtoolsPlugins +import PlayerUIDevtoolsTypes +@testable import PlayerUIDevtoolsBaseProfilerDevtoolsPlugin + +final class BaseProfilerDevtoolsPluginTests: XCTestCase { + + var context: JSContext! + var testHandler: TestHandler! + var plugin: BaseProfilerDevtoolsPlugin! + let testPlayerID = "test-player-123" + + override func setUp() { + super.setUp() + context = JSContext() + testHandler = TestHandler() + plugin = BaseProfilerDevtoolsPlugin(playerID: testPlayerID) + plugin.handler = testHandler + + // Actually load the JS plugin + plugin.context = context + plugin.setup(context: context) + } + + override func tearDown() { + plugin = nil + super.tearDown() + } + + // MARK: - Initialization Tests + + func testInitializationWithVariousPlayerIDs() { + let testCases = [ + "player-1", + "", + "player-@#$%^&*()", + "player-🎮-🎯", + String(repeating: "a", count: 1000) + ] + + for playerID in testCases { + let testPlugin = BaseProfilerDevtoolsPlugin(playerID: playerID) + XCTAssertNotNil(testPlugin) + } + } + + // MARK: - Arguments Tests + + func testGetArgumentsStructure() { + let arguments = plugin.getArguments() + XCTAssertEqual(arguments.count, 1) + + guard let jsCompatibleDict = arguments.first as? [String: Any] else { + XCTFail("First argument should be a dictionary") + return + } + + // Verify playerID + XCTAssertEqual(jsCompatibleDict["playerID"] as? String, testPlayerID) + + // Verify handler structure + guard let handler = jsCompatibleDict["handler"] as? [String: Any] else { + XCTFail("Handler should be a dictionary") + return + } + + XCTAssertTrue(handler.keys.contains("checkIfDevtoolsIsActive")) + XCTAssertTrue(handler.keys.contains("processInteraction")) + } + + // MARK: - Handler Functionality Tests + + func testJSCompatibleHandlerHasCorrectProperties() { + let arguments = plugin.getArguments() + guard let jsCompatibleDict = arguments.first as? [String: Any], + let handler = jsCompatibleDict["handler"] as? [String: Any] else { + XCTFail("Handler should be a dictionary") + return + } + + // Verify handler has exactly the required methods + XCTAssertEqual(handler.keys.count, 2) + XCTAssertTrue(handler.keys.contains("checkIfDevtoolsIsActive")) + XCTAssertTrue(handler.keys.contains("processInteraction")) + } + + func testJSCompatibleIsActive() { + let arguments = plugin.getArguments() + guard let jsCompatibleDict = arguments.first as? [String: Any], + let handler = jsCompatibleDict["handler"] as? [String: Any], + let checkIfActiveFn = handler["checkIfDevtoolsIsActive"] as? JSValue + else { + XCTFail("Handler should be a dictionary") + return + } + XCTAssert(checkIfActiveFn.call(withArguments: []).toBool()) + } + + func testJSCompatibleProcessInteraction() { + let arguments = plugin.getArguments() + guard let jsCompatibleDict = arguments.first as? [String: Any], + let handler = jsCompatibleDict["handler"] as? [String: Any], + let processInteractionFn = handler["processInteraction"] as? JSValue + else { + XCTFail("Handler should be a dictionary") + return + } + + let interaction: Message = ["type": "PLAYER_DEVTOOLS_PLUGIN_INTERACTION", "data": "test-data"] + processInteractionFn.call(withArguments: [interaction]) + XCTAssertEqual(testHandler.interactionsProcessed, 1) + } + + func testIsActiveReturnsTrue() { + XCTAssert(plugin.isActive) + } + + func testProcessInteractionWithVariousMessages() { + let testMessages: [Message] = [ + ["type": "PLAYER_DEVTOOLS_PLUGIN_INTERACTION", "data": "test-data"], + ["type": "PLAYER_DEVTOOLS_PLUGIN_INTERACTION", "data": ["nested": "value", "array": [1, 2, 3]]], + ["type": "PLAYER_DEVTOOLS_PLUGIN_INTERACTION", "data": NSNull()] + ] + + for message in testMessages { + testHandler.processInteraction(interaction: message) + } + + XCTAssertEqual(testHandler.interactionsProcessed, testMessages.count) + } + + // MARK: - Plugin Configuration Tests + + func testPluginConfiguration() { + XCTAssertEqual(plugin.pluginName, "ProfilerDevtoolsPlugin.ProfilerDevtoolsPlugin") + XCTAssertEqual(plugin.fileName, "ProfilerDevtoolsPlugin.native") + } +} + +class TestHandler: DevtoolsHandler { + var interactionsProcessed = 0 + var isActive = true + + func processInteraction(interaction: PlayerUIDevtoolsTypes.Message) { + interactionsProcessed += 1 + } +} diff --git a/devtools/plugins/profiler/ios/Tests/ProfilerDevtoolsPluginTests.swift b/devtools/plugins/profiler/ios/Tests/ProfilerDevtoolsPluginTests.swift deleted file mode 100644 index a2ef4ee..0000000 --- a/devtools/plugins/profiler/ios/Tests/ProfilerDevtoolsPluginTests.swift +++ /dev/null @@ -1,8 +0,0 @@ -import XCTest -import JavaScriptCore -@testable import PlayerUIProfilerDevtoolsPlugin -@preconcurrency import PlayerUIDevToolsTypes - -final class ProfilerDevtoolsPluginTests: XCTestCase { - -} \ No newline at end of file diff --git a/devtools/plugins/profiler/jvm/src/test/kotlin/com/intuit/playerui/plugins/devtools/profiler/ProfilerDevtoolsPluginTest.kt b/devtools/plugins/profiler/jvm/src/test/kotlin/com/intuit/playerui/plugins/devtools/profiler/ProfilerDevtoolsPluginTest.kt index 6028d96..44c85a2 100644 --- a/devtools/plugins/profiler/jvm/src/test/kotlin/com/intuit/playerui/plugins/devtools/profiler/ProfilerDevtoolsPluginTest.kt +++ b/devtools/plugins/profiler/jvm/src/test/kotlin/com/intuit/playerui/plugins/devtools/profiler/ProfilerDevtoolsPluginTest.kt @@ -4,22 +4,73 @@ import com.intuit.playerui.devtools.DevtoolsHandler import com.intuit.playerui.devtools.DevtoolsPluginInteractionEvent import com.intuit.playerui.plugins.devtools.profiler.ProfilerDevtoolsPlugin.Module.ProfilerDevtoolsPlugin import com.intuit.playerui.utils.test.RuntimeTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test class ProfilerDevtoolsPluginTest : RuntimeTest(), DevtoolsHandler { + private val interactions = mutableListOf() + override fun processInteraction(interaction: DevtoolsPluginInteractionEvent) { - TODO("Not yet implemented") + interactions.add(interaction) + } + + override fun checkIfDevtoolsIsActive(): Boolean = true + + private fun plugin(id: String = "test") = + runtime.ProfilerDevtoolsPlugin(ProfilerDevtoolsPlugin.Options(id, this)) + + // MARK: - Initialization + + @Test fun `plugin is active when handler reports active`() { + assertTrue(plugin().checkIfDevtoolsIsActive()) + } + + @Test fun `playerID matches the id passed to options`() { + val plugin = plugin("my-player") + assertEquals("my-player", plugin.playerID) + } + + @Test fun `pluginID is the profiler plugin id`() { + val plugin = plugin() + assertEquals("player-ui-profiler-plugin", plugin.pluginID) } - override fun checkIfDevtoolsIsActive(): Boolean { - return true + @Test fun `store is accessible after construction`() { + val plugin = plugin() + assertNotNull(plugin.store.getState().node) } - @Test fun smoke() { - val plugin = runtime.ProfilerDevtoolsPlugin(ProfilerDevtoolsPlugin.Options("test", this)) - assertTrue(plugin.checkIfDevtoolsIsActive()) - plugin.store.getState().node + // MARK: - Interactions + + @Test fun `start-profiling interaction is handled without throwing`() { + val plugin = plugin() + plugin.processInteraction(interactionEvent("start-profiling")) + } + + @Test fun `stop-profiling interaction is handled without throwing`() { + val plugin = plugin() + plugin.processInteraction(interactionEvent("stop-profiling")) + } + + @Test fun `reset-profiling interaction is handled without throwing`() { + val plugin = plugin() + plugin.processInteraction(interactionEvent("reset-profiling")) + } + + @Test fun `unknown interaction type is handled without throwing`() { + val plugin = plugin() + plugin.processInteraction(interactionEvent("unknown-interaction")) } } + +private fun interactionEvent(type: String) = DevtoolsPluginInteractionEvent( + payload = DevtoolsPluginInteractionEvent.Payload(type = type, payload = ""), + id = 0, + timestamp = 0, + sender = "test", + context = kotlinx.serialization.json.JsonPrimitive("player"), + tag = false, +) diff --git a/devtools/plugins/profiler/swiftui/BUILD b/devtools/plugins/profiler/swiftui/BUILD new file mode 100644 index 0000000..90fc358 --- /dev/null +++ b/devtools/plugins/profiler/swiftui/BUILD @@ -0,0 +1,11 @@ +load("//helpers:ios.bzl", "swiftui_plugin") + +swiftui_plugin( + name = "ProfilerPlugin", + deps = [ + "@swiftpkg_swiftflipper//:SwiftFlipper", + "//devtools/plugin/swiftui:PlayerUIDevtoolsSwiftUIPlugins", + "//devtools/plugins/profiler/ios:PlayerUIDevtoolsBaseProfilerDevtoolsPlugin" + ], + test_deps = [] +) diff --git a/devtools/plugins/profiler/swiftui/Sources/ProfilerDevtoolsPlugin.swift b/devtools/plugins/profiler/swiftui/Sources/ProfilerDevtoolsPlugin.swift new file mode 100644 index 0000000..2cb46e3 --- /dev/null +++ b/devtools/plugins/profiler/swiftui/Sources/ProfilerDevtoolsPlugin.swift @@ -0,0 +1,40 @@ +import Foundation +import PlayerUIDevtoolsMessenger +import PlayerUIDevtoolsPlugins +import PlayerUIDevtoolsSwiftUIPlugins +import PlayerUIDevtoolsBaseProfilerDevtoolsPlugin + +/// A Player Plugin that provides DevTools capabilities via Flipper. +/// This is entirely just a wrapper around the JSBasePlugin +public class ProfilerDevtoolsPlugin: BaseProfilerDevtoolsPlugin, DevtoolsPlugin { + /// Our connection to the flipper server + public let flipperPlugin: DevtoolsFlipperPlugin + /// Keep a reference so the messenger doesn't get garbage collected and destroyed + public var messenger: Messenger? + /// The IDs of all registered listeners associated with this plugin + public var listeners: [UUID] = [] + + public init(id: String, flipperPlugin: DevtoolsFlipperPlugin) { + self.flipperPlugin = flipperPlugin + super.init(playerID: id) + } + + /* Let flipper know that this plugin is going away. Deregister the listeners we + attached to the DevtoolsFlipperPlugin. + + Deinits will NOT run when the app is terminated. But if the app is terminated, + flipper will gracefully handle the abrupt, implicit disconnect, and deregistering + the listeners won't matter anymore since they won't be called if the app is dead. */ + deinit { + // If you make your own DevtoolsPlugin, you will need to implement your own + // deinit, exactly like this. The DevtoolsPlugin protocol cannot provide a deinit, + // unfortunately. + if let messenger { + messenger.destroy() + } else { + print("[DEBUG] Could not destroy messenger. Messenger already no longer exists.") + } + listeners.forEach { flipperPlugin.removeListener(id: $0) } + print("[DEBUG] ProfilerDevtoolsPlugin deinited") + } +} diff --git a/devtools/plugins/profiler/swiftui/ViewInspector/ProfilerDevtoolsPluginTests.swift b/devtools/plugins/profiler/swiftui/ViewInspector/ProfilerDevtoolsPluginTests.swift new file mode 100644 index 0000000..d25a19f --- /dev/null +++ b/devtools/plugins/profiler/swiftui/ViewInspector/ProfilerDevtoolsPluginTests.swift @@ -0,0 +1,138 @@ +import XCTest +import PlayerUI +import JavaScriptCore +import PlayerUIDevtoolsTypes +import PlayerUIDevtoolsPlugins +import PlayerUIDevtoolsBaseProfilerDevtoolsPlugin +@testable import PlayerUIDevtoolsProfilerPlugin + +// MARK: - ProfilerDevtoolsPluginTests +final class ProfilerDevtoolsPluginTests: XCTestCase { + + func testPluginNameIsProfilerDevtoolsPlugin() { + let plugin = ProfilerDevtoolsPlugin(id: "test-id") + XCTAssertEqual(plugin.pluginName, "ProfilerDevtoolsPlugin.ProfilerDevtoolsPlugin") + } + + // MARK: - Initialization Tests + + func testInitializationWithValidID() { + let testID = "player-123" + let plugin = ProfilerDevtoolsPlugin(id: testID) + XCTAssertNotNil(plugin) + } + + func testInitializationWithEmptyID() { + let plugin = ProfilerDevtoolsPlugin(id: "") + XCTAssertNotNil(plugin) + } + + func testInitializationWithSpecialCharactersInID() { + let testID = "player-@#$%^&*()" + let plugin = ProfilerDevtoolsPlugin(id: testID) + XCTAssertNotNil(plugin) + } + + func testInitializationWithLongID() { + let testID = String(repeating: "a", count: 1000) + let plugin = ProfilerDevtoolsPlugin(id: testID) + XCTAssertNotNil(plugin) + } + + // MARK: - Flipper Plugin Tests + + func testFlipperPluginIsInitialized() { + let plugin = ProfilerDevtoolsPlugin(id: "test-id") + XCTAssertNotNil(plugin.flipperPlugin) + } + + func testFlipperPluginIDIsCorrect() { + let plugin = ProfilerDevtoolsPlugin(id: "test-id") + XCTAssertEqual(plugin.flipperPlugin.id, "player-ui-devtools") + } + + func testFlipperPluginDoesNotRunInBackground() { + let plugin = ProfilerDevtoolsPlugin(id: "test-id") + XCTAssertFalse(plugin.flipperPlugin.runInBackground) + } + + // MARK: - Multiple Instance Tests + + func testMultiplePluginInstancesWithDifferentIDs() { + let plugin1 = ProfilerDevtoolsPlugin(id: "player-1") + let plugin2 = ProfilerDevtoolsPlugin(id: "player-2") + + XCTAssertNotNil(plugin1) + XCTAssertNotNil(plugin2) + XCTAssertFalse(plugin1 === plugin2) + } + + func testEachPluginInstanceHasOwnFlipperPlugin() { + let plugin1 = ProfilerDevtoolsPlugin(id: "player-1") + let plugin2 = ProfilerDevtoolsPlugin(id: "player-2") + + XCTAssertNotEqual(ObjectIdentifier(plugin1.flipperPlugin), ObjectIdentifier(plugin2.flipperPlugin)) + } + + // MARK: - Plugin Name Tests + + func testPluginNameConsistency() { + let plugin1 = ProfilerDevtoolsPlugin(id: "id-1") + let plugin2 = ProfilerDevtoolsPlugin(id: "id-2") + + XCTAssertEqual(plugin1.pluginName, plugin2.pluginName) + XCTAssertEqual(plugin1.pluginName, "ProfilerDevtoolsPlugin.ProfilerDevtoolsPlugin") + } + + // MARK: - Flipper Plugin Configuration Tests + + func testFlipperPluginHasCorrectConfiguration() { + let plugin = ProfilerDevtoolsPlugin(id: "test-id") + + XCTAssertEqual(plugin.flipperPlugin.id, "player-ui-devtools") + XCTAssertFalse(plugin.flipperPlugin.runInBackground) + } + + func testMultiplePluginsHaveIndependentFlipperPlugins() { + let plugin1 = ProfilerDevtoolsPlugin(id: "player-1") + let plugin2 = ProfilerDevtoolsPlugin(id: "player-2") + + let id1 = ObjectIdentifier(plugin1.flipperPlugin) + let id2 = ObjectIdentifier(plugin2.flipperPlugin) + + XCTAssertNotEqual(id1, id2) + } + + // MARK: - Deinit Tests + + func testDeinitRemovesListeners() { + let flipperPlugin = DevtoolsFlipperPlugin() + var plugin: ProfilerDevtoolsPlugin? = ProfilerDevtoolsPlugin(id: "test-id", flipperPlugin: flipperPlugin) + + let listenerID = flipperPlugin.addListener { _ in } + plugin?.listeners.append(listenerID) + + XCTAssertEqual(flipperPlugin.listeners.count, 1) + + plugin = nil + + XCTAssertEqual(flipperPlugin.listeners.count, 0) + } + + func testDeinitDestroysMessenger() { + let flipperPlugin = DevtoolsFlipperPlugin() + var plugin: ProfilerDevtoolsPlugin? = ProfilerDevtoolsPlugin(id: "test-id", flipperPlugin: flipperPlugin) + + plugin = nil + + if let jsException = plugin?.context?.exception { + XCTFail("Destroy failed") + } + } +} + +extension ProfilerDevtoolsPlugin { + convenience init(id: String) { + self.init(id: id, flipperPlugin: .init()) + } +} diff --git a/ios/BUILD b/ios/BUILD index caa8cde..dbb87ed 100644 --- a/ios/BUILD +++ b/ios/BUILD @@ -18,6 +18,7 @@ xcodeproj( "//devtools/plugin/swiftui:PlayerUIDevtoolsSwiftUIPluginsViewInspectorTests", "//devtools/plugins/basic/ios:PlayerUIDevtoolsBaseBasicDevtoolsPluginTests", "//devtools/plugins/basic/swiftui:PlayerUIDevtoolsBasicPluginViewInspectorTests", - "//devtools/plugins/profiler/ios:PlayerUIDevtoolsProfilerDevtoolsPluginTests", + "//devtools/plugins/profiler/ios:PlayerUIDevtoolsBaseProfilerDevtoolsPluginTests", + "//devtools/plugins/profiler/swiftui:PlayerUIDevtoolsProfilerPluginViewInspectorTests", ], ) \ No newline at end of file diff --git a/ios/demo/BUILD b/ios/demo/BUILD index f03238d..5002506 100644 --- a/ios/demo/BUILD +++ b/ios/demo/BUILD @@ -20,7 +20,7 @@ swift_library( # These are the components under development. "//devtools/plugin/ios:PlayerUIDevtoolsPlugins", "//devtools/plugins/basic/swiftui:PlayerUIDevtoolsBasicPlugin", - "//devtools/plugins/profiler/ios:PlayerUIDevtoolsProfilerDevtoolsPlugin", + "//devtools/plugins/profiler/swiftui:PlayerUIDevtoolsProfilerPlugin", ] ) diff --git a/ios/demo/Sources/DemoApp.swift b/ios/demo/Sources/DemoApp.swift index 1f65b3e..52f2ac2 100644 --- a/ios/demo/Sources/DemoApp.swift +++ b/ios/demo/Sources/DemoApp.swift @@ -7,7 +7,7 @@ import SwiftFlipper import PlayerUIDevtoolsPlugins import PlayerUIReferenceAssets import PlayerUIDevtoolsBasicPlugin -import PlayerUIDevtoolsProfilerDevtoolsPlugin +import PlayerUIDevtoolsProfilerPlugin @main struct BazelApp: App { From 219060259b4575d29dcfc018910252455948406f Mon Sep 17 00:00:00 2001 From: Thomas Marmer Date: Fri, 8 May 2026 14:08:47 -0400 Subject: [PATCH 10/10] update snapshots and fix lint issues --- .../__snapshots__/plugin.test.ts.snap | 492 +++++++++--------- .../core/src/__tests__/plugin.test.ts | 6 +- .../src/addProfilerInterceptorsToHooks.ts | 4 +- .../src/helpers/__tests__/profiler.test.ts | 12 +- .../__tests__/transformProfilerData.test.ts | 15 +- .../profiler/core/src/helpers/profiler.ts | 2 +- .../core/src/helpers/transformProfilerData.ts | 2 +- devtools/plugins/profiler/core/src/plugin.ts | 20 +- .../profiler/ProfilerDevtoolsPlugin.kt | 17 +- .../profiler/ProfilerDevtoolsPluginTest.kt | 25 +- 10 files changed, 299 insertions(+), 296 deletions(-) diff --git a/devtools/plugins/profiler/core/src/__tests__/__snapshots__/plugin.test.ts.snap b/devtools/plugins/profiler/core/src/__tests__/__snapshots__/plugin.test.ts.snap index a943604..a362c0c 100644 --- a/devtools/plugins/profiler/core/src/__tests__/__snapshots__/plugin.test.ts.snap +++ b/devtools/plugins/profiler/core/src/__tests__/__snapshots__/plugin.test.ts.snap @@ -7,113 +7,113 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] "rawNodes": [ { "children": [], - "endTime": 2490.2, + "endTime": 2490.2999999999997, "name": "state", - "startTime": 2490.1, + "startTime": 2490.2, "tooltip": "state, 0.1000 (ms)", "value": 100, }, { "children": [], - "endTime": 2490.4999999999995, + "endTime": 2490.5999999999995, "name": "resolveFlowContent", - "startTime": 2490.3999999999996, + "startTime": 2490.4999999999995, "tooltip": "resolveFlowContent, 0.1000 (ms)", "value": 100, }, { "children": [], - "endTime": 2490.7999999999993, + "endTime": 2490.899999999999, "name": "onStart", - "startTime": 2490.6999999999994, + "startTime": 2490.7999999999993, "tooltip": "onStart, 0.1000 (ms)", "value": 100, }, { "children": [], - "endTime": 2491.099999999999, + "endTime": 2491.199999999999, "name": "flowController", - "startTime": 2490.999999999999, + "startTime": 2491.099999999999, "tooltip": "flowController, 0.1000 (ms)", "value": 100, }, { "children": [], - "endTime": 2491.3999999999987, + "endTime": 2491.4999999999986, "name": "bindingParser", - "startTime": 2491.299999999999, + "startTime": 2491.3999999999987, "tooltip": "bindingParser, 0.1000 (ms)", "value": 100, }, { "children": [], - "endTime": 2491.6999999999985, + "endTime": 2491.7999999999984, "name": "schema", - "startTime": 2491.5999999999985, + "startTime": 2491.6999999999985, "tooltip": "schema, 0.1000 (ms)", "value": 100, }, { "children": [], - "endTime": 2491.999999999998, + "endTime": 2492.099999999998, "name": "validationController", - "startTime": 2491.8999999999983, + "startTime": 2491.999999999998, "tooltip": "validationController, 0.1000 (ms)", "value": 100, }, { "children": [], - "endTime": 2492.299999999998, + "endTime": 2492.399999999998, "name": "expressionEvaluator", - "startTime": 2492.199999999998, + "startTime": 2492.299999999998, "tooltip": "expressionEvaluator, 0.1000 (ms)", "value": 100, }, { "children": [], - "endTime": 2492.5999999999976, + "endTime": 2492.6999999999975, "name": "dataController", - "startTime": 2492.4999999999977, + "startTime": 2492.5999999999976, "tooltip": "dataController, 0.1000 (ms)", "value": 100, }, { "children": [], - "endTime": 2492.8999999999974, + "endTime": 2492.9999999999973, "name": "viewController", - "startTime": 2492.7999999999975, + "startTime": 2492.8999999999974, "tooltip": "viewController, 0.1000 (ms)", "value": 100, }, { "children": [], - "endTime": 2493.199999999997, + "endTime": 2493.299999999997, "name": "state", - "startTime": 2493.099999999997, + "startTime": 2493.199999999997, "tooltip": "state, 0.1000 (ms)", "value": 100, }, { "children": [], - "endTime": 2493.499999999997, + "endTime": 2493.5999999999967, "name": "flow", - "startTime": 2493.399999999997, + "startTime": 2493.499999999997, "tooltip": "flow, 0.1000 (ms)", "value": 100, }, { "children": [], - "endTime": 2493.7999999999965, + "endTime": 2493.8999999999965, "name": "beforeStart", - "startTime": 2493.6999999999966, + "startTime": 2493.7999999999965, "tooltip": "beforeStart, 0.1000 (ms)", "value": 100, }, { "children": [], - "endTime": 2494.0999999999963, + "endTime": 2494.199999999996, "name": "resolveTransitionNode", - "startTime": 2493.9999999999964, + "startTime": 2494.0999999999963, "tooltip": "resolveTransitionNode, 0.1000 (ms)", "value": 100, }, @@ -121,9 +121,9 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] "children": [ { "children": [], - "endTime": 2494.499999999996, + "endTime": 2494.599999999996, "name": "resolveView", - "startTime": 2494.399999999996, + "startTime": 2494.499999999996, "tooltip": "resolveView, 0.1000 (ms)", "value": 100, }, @@ -131,207 +131,207 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] "children": [ { "children": [], - "endTime": 2494.8999999999955, + "endTime": 2494.9999999999955, "name": "onTemplatePluginCreated", - "startTime": 2494.7999999999956, + "startTime": 2494.8999999999955, "tooltip": "onTemplatePluginCreated, 0.1000 (ms)", "value": 100, }, ], - "endTime": 2495.0999999999954, + "endTime": 2495.1999999999953, "name": "view", - "startTime": 2494.6999999999957, + "startTime": 2494.7999999999956, "tooltip": "view, 0.4000 (ms)", "value": 400, }, { "children": [], - "endTime": 2495.399999999995, + "endTime": 2495.499999999995, "name": "templatePlugin", - "startTime": 2495.299999999995, + "startTime": 2495.399999999995, "tooltip": "templatePlugin, 0.1000 (ms)", "value": 100, }, { "children": [], - "endTime": 2495.699999999995, + "endTime": 2495.7999999999947, "name": "parser", - "startTime": 2495.599999999995, + "startTime": 2495.699999999995, "tooltip": "parser, 0.1000 (ms)", "value": 100, }, { "children": [], - "endTime": 2495.9999999999945, + "endTime": 2496.0999999999945, "name": "parseNode", - "startTime": 2495.8999999999946, + "startTime": 2495.9999999999945, "tooltip": "parseNode, 0.1000 (ms)", "value": 100, }, { "children": [], - "endTime": 2496.2999999999943, + "endTime": 2496.399999999994, "name": "onParseObject", - "startTime": 2496.1999999999944, + "startTime": 2496.2999999999943, "tooltip": "onParseObject, 0.1000 (ms)", "value": 100, }, { "children": [], - "endTime": 2496.599999999994, + "endTime": 2496.699999999994, "name": "parseNode", - "startTime": 2496.499999999994, + "startTime": 2496.599999999994, "tooltip": "parseNode, 0.1000 (ms)", "value": 100, }, { "children": [], - "endTime": 2496.8999999999937, + "endTime": 2496.9999999999936, "name": "parseNode", - "startTime": 2496.799999999994, + "startTime": 2496.8999999999937, "tooltip": "parseNode, 0.1000 (ms)", "value": 100, }, { "children": [], - "endTime": 2497.1999999999935, + "endTime": 2497.2999999999934, "name": "parseNode", - "startTime": 2497.0999999999935, + "startTime": 2497.1999999999935, "tooltip": "parseNode, 0.1000 (ms)", "value": 100, }, { "children": [], - "endTime": 2497.499999999993, + "endTime": 2497.599999999993, "name": "onCreateASTNode", - "startTime": 2497.3999999999933, + "startTime": 2497.499999999993, "tooltip": "onCreateASTNode, 0.1000 (ms)", "value": 100, }, { "children": [], - "endTime": 2497.799999999993, + "endTime": 2497.899999999993, "name": "resolver", - "startTime": 2497.699999999993, + "startTime": 2497.799999999993, "tooltip": "resolver, 0.1000 (ms)", "value": 100, }, { "children": [], - "endTime": 2498.0999999999926, + "endTime": 2498.1999999999925, "name": "beforeUpdate", - "startTime": 2497.9999999999927, + "startTime": 2498.0999999999926, "tooltip": "beforeUpdate, 0.1000 (ms)", "value": 100, }, { "children": [], - "endTime": 2498.3999999999924, + "endTime": 2498.4999999999923, "name": "resolveOptions", - "startTime": 2498.2999999999925, + "startTime": 2498.3999999999924, "tooltip": "resolveOptions, 0.1000 (ms)", "value": 100, }, { "children": [], - "endTime": 2498.699999999992, + "endTime": 2498.799999999992, "name": "skipResolve", - "startTime": 2498.599999999992, + "startTime": 2498.699999999992, "tooltip": "skipResolve, 0.1000 (ms)", "value": 100, }, { "children": [], - "endTime": 2498.999999999992, + "endTime": 2499.0999999999917, "name": "beforeResolve", - "startTime": 2498.899999999992, + "startTime": 2498.999999999992, "tooltip": "beforeResolve, 0.1000 (ms)", "value": 100, }, { "children": [], - "endTime": 2499.2999999999915, + "endTime": 2499.3999999999915, "name": "resolve", - "startTime": 2499.1999999999916, + "startTime": 2499.2999999999915, "tooltip": "resolve, 0.1000 (ms)", "value": 100, }, { "children": [], - "endTime": 2499.5999999999913, + "endTime": 2499.699999999991, "name": "afterResolve", - "startTime": 2499.4999999999914, + "startTime": 2499.5999999999913, "tooltip": "afterResolve, 0.1000 (ms)", "value": 100, }, { "children": [], - "endTime": 2499.899999999991, + "endTime": 2499.999999999991, "name": "afterNodeUpdate", - "startTime": 2499.799999999991, + "startTime": 2499.899999999991, "tooltip": "afterNodeUpdate, 0.1000 (ms)", "value": 100, }, { "children": [], - "endTime": 2500.1999999999907, + "endTime": 2500.2999999999906, "name": "afterUpdate", - "startTime": 2500.099999999991, + "startTime": 2500.1999999999907, "tooltip": "afterUpdate, 0.1000 (ms)", "value": 100, }, { "children": [], - "endTime": 2500.4999999999905, + "endTime": 2500.5999999999904, "name": "onUpdate", - "startTime": 2500.3999999999905, + "startTime": 2500.4999999999905, "tooltip": "onUpdate, 0.1000 (ms)", "value": 100, }, ], - "endTime": 2500.6999999999903, + "endTime": 2500.79999999999, "name": "transition", - "startTime": 2494.299999999996, + "startTime": 2494.399999999996, "tooltip": "transition, 6.4000 (ms)", "value": 6400, }, { "children": [], - "endTime": 2500.99999999999, + "endTime": 2501.09999999999, "name": "afterTransition", - "startTime": 2500.89999999999, + "startTime": 2500.99999999999, "tooltip": "afterTransition, 0.1000 (ms)", "value": 100, }, { "children": [], - "endTime": 2501.2999999999897, + "endTime": 2501.3999999999896, "name": "skipTransition", - "startTime": 2501.19999999999, + "startTime": 2501.2999999999897, "tooltip": "skipTransition, 0.1000 (ms)", "value": 100, }, { "children": [], - "endTime": 2501.5999999999894, + "endTime": 2501.6999999999894, "name": "beforeTransition", - "startTime": 2501.4999999999895, + "startTime": 2501.5999999999894, "tooltip": "beforeTransition, 0.1000 (ms)", "value": 100, }, { "children": [], - "endTime": 2501.899999999989, + "endTime": 2501.999999999989, "name": "resolveTransitionNode", - "startTime": 2501.7999999999893, + "startTime": 2501.899999999989, "tooltip": "resolveTransitionNode, 0.1000 (ms)", "value": 100, }, { "children": [], - "endTime": 2502.199999999989, + "endTime": 2502.299999999989, "name": "transition", - "startTime": 2502.099999999989, + "startTime": 2502.199999999989, "tooltip": "transition, 0.1000 (ms)", "value": 100, }, @@ -339,17 +339,17 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] "children": [ { "children": [], - "endTime": 2502.5999999999885, + "endTime": 2502.6999999999884, "name": "resolveOptions", - "startTime": 2502.4999999999886, + "startTime": 2502.5999999999885, "tooltip": "resolveOptions, 0.1000 (ms)", "value": 100, }, { "children": [], - "endTime": 2502.8999999999883, + "endTime": 2502.999999999988, "name": "beforeEvaluate", - "startTime": 2502.7999999999884, + "startTime": 2502.8999999999883, "tooltip": "beforeEvaluate, 0.1000 (ms)", "value": 100, }, @@ -357,25 +357,25 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] "children": [ { "children": [], - "endTime": 2503.299999999988, + "endTime": 2503.399999999988, "name": "resolve", - "startTime": 2503.199999999988, + "startTime": 2503.299999999988, "tooltip": "resolve, 0.1000 (ms)", "value": 100, }, { "children": [], - "endTime": 2503.5999999999876, + "endTime": 2503.6999999999875, "name": "skipOptimization", - "startTime": 2503.4999999999877, + "startTime": 2503.5999999999876, "tooltip": "skipOptimization, 0.1000 (ms)", "value": 100, }, { "children": [], - "endTime": 2503.8999999999874, + "endTime": 2503.9999999999873, "name": "resolveDataStages", - "startTime": 2503.7999999999874, + "startTime": 2503.8999999999874, "tooltip": "resolveDataStages, 0.1000 (ms)", "value": 100, }, @@ -383,118 +383,118 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] "children": [ { "children": [], - "endTime": 2504.299999999987, + "endTime": 2504.399999999987, "name": "resolveTypeForBinding", - "startTime": 2504.199999999987, + "startTime": 2504.299999999987, "tooltip": "resolveTypeForBinding, 0.1000 (ms)", "value": 100, }, ], - "endTime": 2504.499999999987, + "endTime": 2504.5999999999867, "name": "resolveDefaultValue", - "startTime": 2504.099999999987, + "startTime": 2504.199999999987, "tooltip": "resolveDefaultValue, 0.4000 (ms)", "value": 400, }, { "children": [], - "endTime": 2504.7999999999865, + "endTime": 2504.8999999999864, "name": "onGet", - "startTime": 2504.6999999999866, + "startTime": 2504.7999999999865, "tooltip": "onGet, 0.1000 (ms)", "value": 100, }, { "children": [], - "endTime": 2505.0999999999863, + "endTime": 2505.199999999986, "name": "onSet", - "startTime": 2504.9999999999864, + "startTime": 2505.0999999999863, "tooltip": "onSet, 0.1000 (ms)", "value": 100, }, { "children": [], - "endTime": 2505.399999999986, + "endTime": 2505.499999999986, "name": "onUpdate", - "startTime": 2505.299999999986, + "startTime": 2505.399999999986, "tooltip": "onUpdate, 0.1000 (ms)", "value": 100, }, ], - "endTime": 2505.599999999986, + "endTime": 2505.6999999999857, "name": "resolve", - "startTime": 2503.099999999988, + "startTime": 2503.199999999988, "tooltip": "resolve, 2.5000 (ms)", "value": 2500, }, { "children": [], - "endTime": 2505.8999999999855, + "endTime": 2505.9999999999854, "name": "skipTransition", - "startTime": 2505.7999999999856, + "startTime": 2505.8999999999855, "tooltip": "skipTransition, 0.1000 (ms)", "value": 100, }, { "children": [], - "endTime": 2506.1999999999853, + "endTime": 2506.299999999985, "name": "beforeTransition", - "startTime": 2506.0999999999854, + "startTime": 2506.1999999999853, "tooltip": "beforeTransition, 0.1000 (ms)", "value": 100, }, { "children": [], - "endTime": 2506.499999999985, + "endTime": 2506.599999999985, "name": "resolveTransitionNode", - "startTime": 2506.399999999985, + "startTime": 2506.499999999985, "tooltip": "resolveTransitionNode, 0.1000 (ms)", "value": 100, }, { "children": [], - "endTime": 2506.7999999999847, + "endTime": 2506.8999999999846, "name": "transition", - "startTime": 2506.699999999985, + "startTime": 2506.7999999999847, "tooltip": "transition, 0.1000 (ms)", "value": 100, }, { "children": [], - "endTime": 2507.0999999999844, + "endTime": 2507.1999999999844, "name": "afterTransition", - "startTime": 2506.9999999999845, + "startTime": 2507.0999999999844, "tooltip": "afterTransition, 0.1000 (ms)", "value": 100, }, ], - "endTime": 2507.2999999999843, + "endTime": 2507.399999999984, "name": "afterTransition", - "startTime": 2502.3999999999887, + "startTime": 2502.4999999999886, "tooltip": "afterTransition, 4.9000 (ms)", "value": 4900, }, { "children": [], - "endTime": 2507.599999999984, + "endTime": 2507.699999999984, "name": "onGet", - "startTime": 2507.499999999984, + "startTime": 2507.599999999984, "tooltip": "onGet, 0.1000 (ms)", "value": 100, }, { "children": [], - "endTime": 2507.8999999999837, + "endTime": 2507.9999999999836, "name": "serialize", - "startTime": 2507.799999999984, + "startTime": 2507.8999999999837, "tooltip": "serialize, 0.1000 (ms)", "value": 100, }, { "children": [], - "endTime": 2508.1999999999834, + "endTime": 2508.2999999999834, "name": "state", - "startTime": 2508.0999999999835, + "startTime": 2508.1999999999834, "tooltip": "state, 0.1000 (ms)", "value": 100, }, @@ -503,9 +503,9 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] "children": [ { "children": [], - "endTime": 2490.2, + "endTime": 2490.2999999999997, "name": "state", - "startTime": 2490.1, + "startTime": 2490.2, "tooltip": "state, 0.1000 (ms)", "value": 100, }, @@ -519,9 +519,9 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] }, { "children": [], - "endTime": 2490.4999999999995, + "endTime": 2490.5999999999995, "name": "resolveFlowContent", - "startTime": 2490.3999999999996, + "startTime": 2490.4999999999995, "tooltip": "resolveFlowContent, 0.1000 (ms)", "value": 100, }, @@ -535,9 +535,9 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] }, { "children": [], - "endTime": 2490.7999999999993, + "endTime": 2490.899999999999, "name": "onStart", - "startTime": 2490.6999999999994, + "startTime": 2490.7999999999993, "tooltip": "onStart, 0.1000 (ms)", "value": 100, }, @@ -551,9 +551,9 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] }, { "children": [], - "endTime": 2491.099999999999, + "endTime": 2491.199999999999, "name": "flowController", - "startTime": 2490.999999999999, + "startTime": 2491.099999999999, "tooltip": "flowController, 0.1000 (ms)", "value": 100, }, @@ -567,9 +567,9 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] }, { "children": [], - "endTime": 2491.3999999999987, + "endTime": 2491.4999999999986, "name": "bindingParser", - "startTime": 2491.299999999999, + "startTime": 2491.3999999999987, "tooltip": "bindingParser, 0.1000 (ms)", "value": 100, }, @@ -583,9 +583,9 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] }, { "children": [], - "endTime": 2491.6999999999985, + "endTime": 2491.7999999999984, "name": "schema", - "startTime": 2491.5999999999985, + "startTime": 2491.6999999999985, "tooltip": "schema, 0.1000 (ms)", "value": 100, }, @@ -599,9 +599,9 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] }, { "children": [], - "endTime": 2491.999999999998, + "endTime": 2492.099999999998, "name": "validationController", - "startTime": 2491.8999999999983, + "startTime": 2491.999999999998, "tooltip": "validationController, 0.1000 (ms)", "value": 100, }, @@ -615,9 +615,9 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] }, { "children": [], - "endTime": 2492.299999999998, + "endTime": 2492.399999999998, "name": "expressionEvaluator", - "startTime": 2492.199999999998, + "startTime": 2492.299999999998, "tooltip": "expressionEvaluator, 0.1000 (ms)", "value": 100, }, @@ -631,9 +631,9 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] }, { "children": [], - "endTime": 2492.5999999999976, + "endTime": 2492.6999999999975, "name": "dataController", - "startTime": 2492.4999999999977, + "startTime": 2492.5999999999976, "tooltip": "dataController, 0.1000 (ms)", "value": 100, }, @@ -647,9 +647,9 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] }, { "children": [], - "endTime": 2492.8999999999974, + "endTime": 2492.9999999999973, "name": "viewController", - "startTime": 2492.7999999999975, + "startTime": 2492.8999999999974, "tooltip": "viewController, 0.1000 (ms)", "value": 100, }, @@ -663,9 +663,9 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] }, { "children": [], - "endTime": 2493.199999999997, + "endTime": 2493.299999999997, "name": "state", - "startTime": 2493.099999999997, + "startTime": 2493.199999999997, "tooltip": "state, 0.1000 (ms)", "value": 100, }, @@ -679,9 +679,9 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] }, { "children": [], - "endTime": 2493.499999999997, + "endTime": 2493.5999999999967, "name": "flow", - "startTime": 2493.399999999997, + "startTime": 2493.499999999997, "tooltip": "flow, 0.1000 (ms)", "value": 100, }, @@ -695,9 +695,9 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] }, { "children": [], - "endTime": 2493.7999999999965, + "endTime": 2493.8999999999965, "name": "beforeStart", - "startTime": 2493.6999999999966, + "startTime": 2493.7999999999965, "tooltip": "beforeStart, 0.1000 (ms)", "value": 100, }, @@ -711,9 +711,9 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] }, { "children": [], - "endTime": 2494.0999999999963, + "endTime": 2494.199999999996, "name": "resolveTransitionNode", - "startTime": 2493.9999999999964, + "startTime": 2494.0999999999963, "tooltip": "resolveTransitionNode, 0.1000 (ms)", "value": 100, }, @@ -737,9 +737,9 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] }, { "children": [], - "endTime": 2494.499999999996, + "endTime": 2494.599999999996, "name": "resolveView", - "startTime": 2494.399999999996, + "startTime": 2494.499999999996, "tooltip": "resolveView, 0.1000 (ms)", "value": 100, }, @@ -763,16 +763,16 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] }, { "children": [], - "endTime": 2494.8999999999955, + "endTime": 2494.9999999999955, "name": "onTemplatePluginCreated", - "startTime": 2494.7999999999956, + "startTime": 2494.8999999999955, "tooltip": "onTemplatePluginCreated, 0.1000 (ms)", "value": 100, }, ], - "endTime": 2495.0999999999954, + "endTime": 2495.1999999999953, "name": "view", - "startTime": 2494.6999999999957, + "startTime": 2494.7999999999956, "tooltip": "view, 0.4000 (ms)", "value": 400, }, @@ -786,9 +786,9 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] }, { "children": [], - "endTime": 2495.399999999995, + "endTime": 2495.499999999995, "name": "templatePlugin", - "startTime": 2495.299999999995, + "startTime": 2495.399999999995, "tooltip": "templatePlugin, 0.1000 (ms)", "value": 100, }, @@ -802,9 +802,9 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] }, { "children": [], - "endTime": 2495.699999999995, + "endTime": 2495.7999999999947, "name": "parser", - "startTime": 2495.599999999995, + "startTime": 2495.699999999995, "tooltip": "parser, 0.1000 (ms)", "value": 100, }, @@ -818,9 +818,9 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] }, { "children": [], - "endTime": 2495.9999999999945, + "endTime": 2496.0999999999945, "name": "parseNode", - "startTime": 2495.8999999999946, + "startTime": 2495.9999999999945, "tooltip": "parseNode, 0.1000 (ms)", "value": 100, }, @@ -834,9 +834,9 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] }, { "children": [], - "endTime": 2496.2999999999943, + "endTime": 2496.399999999994, "name": "onParseObject", - "startTime": 2496.1999999999944, + "startTime": 2496.2999999999943, "tooltip": "onParseObject, 0.1000 (ms)", "value": 100, }, @@ -850,9 +850,9 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] }, { "children": [], - "endTime": 2496.599999999994, + "endTime": 2496.699999999994, "name": "parseNode", - "startTime": 2496.499999999994, + "startTime": 2496.599999999994, "tooltip": "parseNode, 0.1000 (ms)", "value": 100, }, @@ -866,9 +866,9 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] }, { "children": [], - "endTime": 2496.8999999999937, + "endTime": 2496.9999999999936, "name": "parseNode", - "startTime": 2496.799999999994, + "startTime": 2496.8999999999937, "tooltip": "parseNode, 0.1000 (ms)", "value": 100, }, @@ -882,9 +882,9 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] }, { "children": [], - "endTime": 2497.1999999999935, + "endTime": 2497.2999999999934, "name": "parseNode", - "startTime": 2497.0999999999935, + "startTime": 2497.1999999999935, "tooltip": "parseNode, 0.1000 (ms)", "value": 100, }, @@ -898,9 +898,9 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] }, { "children": [], - "endTime": 2497.499999999993, + "endTime": 2497.599999999993, "name": "onCreateASTNode", - "startTime": 2497.3999999999933, + "startTime": 2497.499999999993, "tooltip": "onCreateASTNode, 0.1000 (ms)", "value": 100, }, @@ -914,9 +914,9 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] }, { "children": [], - "endTime": 2497.799999999993, + "endTime": 2497.899999999993, "name": "resolver", - "startTime": 2497.699999999993, + "startTime": 2497.799999999993, "tooltip": "resolver, 0.1000 (ms)", "value": 100, }, @@ -930,9 +930,9 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] }, { "children": [], - "endTime": 2498.0999999999926, + "endTime": 2498.1999999999925, "name": "beforeUpdate", - "startTime": 2497.9999999999927, + "startTime": 2498.0999999999926, "tooltip": "beforeUpdate, 0.1000 (ms)", "value": 100, }, @@ -946,9 +946,9 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] }, { "children": [], - "endTime": 2498.3999999999924, + "endTime": 2498.4999999999923, "name": "resolveOptions", - "startTime": 2498.2999999999925, + "startTime": 2498.3999999999924, "tooltip": "resolveOptions, 0.1000 (ms)", "value": 100, }, @@ -962,9 +962,9 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] }, { "children": [], - "endTime": 2498.699999999992, + "endTime": 2498.799999999992, "name": "skipResolve", - "startTime": 2498.599999999992, + "startTime": 2498.699999999992, "tooltip": "skipResolve, 0.1000 (ms)", "value": 100, }, @@ -978,9 +978,9 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] }, { "children": [], - "endTime": 2498.999999999992, + "endTime": 2499.0999999999917, "name": "beforeResolve", - "startTime": 2498.899999999992, + "startTime": 2498.999999999992, "tooltip": "beforeResolve, 0.1000 (ms)", "value": 100, }, @@ -994,9 +994,9 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] }, { "children": [], - "endTime": 2499.2999999999915, + "endTime": 2499.3999999999915, "name": "resolve", - "startTime": 2499.1999999999916, + "startTime": 2499.2999999999915, "tooltip": "resolve, 0.1000 (ms)", "value": 100, }, @@ -1010,9 +1010,9 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] }, { "children": [], - "endTime": 2499.5999999999913, + "endTime": 2499.699999999991, "name": "afterResolve", - "startTime": 2499.4999999999914, + "startTime": 2499.5999999999913, "tooltip": "afterResolve, 0.1000 (ms)", "value": 100, }, @@ -1026,9 +1026,9 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] }, { "children": [], - "endTime": 2499.899999999991, + "endTime": 2499.999999999991, "name": "afterNodeUpdate", - "startTime": 2499.799999999991, + "startTime": 2499.899999999991, "tooltip": "afterNodeUpdate, 0.1000 (ms)", "value": 100, }, @@ -1042,9 +1042,9 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] }, { "children": [], - "endTime": 2500.1999999999907, + "endTime": 2500.2999999999906, "name": "afterUpdate", - "startTime": 2500.099999999991, + "startTime": 2500.1999999999907, "tooltip": "afterUpdate, 0.1000 (ms)", "value": 100, }, @@ -1058,16 +1058,16 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] }, { "children": [], - "endTime": 2500.4999999999905, + "endTime": 2500.5999999999904, "name": "onUpdate", - "startTime": 2500.3999999999905, + "startTime": 2500.4999999999905, "tooltip": "onUpdate, 0.1000 (ms)", "value": 100, }, ], - "endTime": 2500.6999999999903, + "endTime": 2500.79999999999, "name": "transition", - "startTime": 2494.299999999996, + "startTime": 2494.399999999996, "tooltip": "transition, 6.4000 (ms)", "value": 6400, }, @@ -1081,9 +1081,9 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] }, { "children": [], - "endTime": 2500.99999999999, + "endTime": 2501.09999999999, "name": "afterTransition", - "startTime": 2500.89999999999, + "startTime": 2500.99999999999, "tooltip": "afterTransition, 0.1000 (ms)", "value": 100, }, @@ -1097,9 +1097,9 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] }, { "children": [], - "endTime": 2501.2999999999897, + "endTime": 2501.3999999999896, "name": "skipTransition", - "startTime": 2501.19999999999, + "startTime": 2501.2999999999897, "tooltip": "skipTransition, 0.1000 (ms)", "value": 100, }, @@ -1113,9 +1113,9 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] }, { "children": [], - "endTime": 2501.5999999999894, + "endTime": 2501.6999999999894, "name": "beforeTransition", - "startTime": 2501.4999999999895, + "startTime": 2501.5999999999894, "tooltip": "beforeTransition, 0.1000 (ms)", "value": 100, }, @@ -1129,9 +1129,9 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] }, { "children": [], - "endTime": 2501.899999999989, + "endTime": 2501.999999999989, "name": "resolveTransitionNode", - "startTime": 2501.7999999999893, + "startTime": 2501.899999999989, "tooltip": "resolveTransitionNode, 0.1000 (ms)", "value": 100, }, @@ -1145,9 +1145,9 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] }, { "children": [], - "endTime": 2502.199999999989, + "endTime": 2502.299999999989, "name": "transition", - "startTime": 2502.099999999989, + "startTime": 2502.199999999989, "tooltip": "transition, 0.1000 (ms)", "value": 100, }, @@ -1171,9 +1171,9 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] }, { "children": [], - "endTime": 2502.5999999999885, + "endTime": 2502.6999999999884, "name": "resolveOptions", - "startTime": 2502.4999999999886, + "startTime": 2502.5999999999885, "tooltip": "resolveOptions, 0.1000 (ms)", "value": 100, }, @@ -1187,9 +1187,9 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] }, { "children": [], - "endTime": 2502.8999999999883, + "endTime": 2502.999999999988, "name": "beforeEvaluate", - "startTime": 2502.7999999999884, + "startTime": 2502.8999999999883, "tooltip": "beforeEvaluate, 0.1000 (ms)", "value": 100, }, @@ -1213,9 +1213,9 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] }, { "children": [], - "endTime": 2503.299999999988, + "endTime": 2503.399999999988, "name": "resolve", - "startTime": 2503.199999999988, + "startTime": 2503.299999999988, "tooltip": "resolve, 0.1000 (ms)", "value": 100, }, @@ -1229,9 +1229,9 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] }, { "children": [], - "endTime": 2503.5999999999876, + "endTime": 2503.6999999999875, "name": "skipOptimization", - "startTime": 2503.4999999999877, + "startTime": 2503.5999999999876, "tooltip": "skipOptimization, 0.1000 (ms)", "value": 100, }, @@ -1245,9 +1245,9 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] }, { "children": [], - "endTime": 2503.8999999999874, + "endTime": 2503.9999999999873, "name": "resolveDataStages", - "startTime": 2503.7999999999874, + "startTime": 2503.8999999999874, "tooltip": "resolveDataStages, 0.1000 (ms)", "value": 100, }, @@ -1271,16 +1271,16 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] }, { "children": [], - "endTime": 2504.299999999987, + "endTime": 2504.399999999987, "name": "resolveTypeForBinding", - "startTime": 2504.199999999987, + "startTime": 2504.299999999987, "tooltip": "resolveTypeForBinding, 0.1000 (ms)", "value": 100, }, ], - "endTime": 2504.499999999987, + "endTime": 2504.5999999999867, "name": "resolveDefaultValue", - "startTime": 2504.099999999987, + "startTime": 2504.199999999987, "tooltip": "resolveDefaultValue, 0.4000 (ms)", "value": 400, }, @@ -1294,9 +1294,9 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] }, { "children": [], - "endTime": 2504.7999999999865, + "endTime": 2504.8999999999864, "name": "onGet", - "startTime": 2504.6999999999866, + "startTime": 2504.7999999999865, "tooltip": "onGet, 0.1000 (ms)", "value": 100, }, @@ -1310,9 +1310,9 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] }, { "children": [], - "endTime": 2505.0999999999863, + "endTime": 2505.199999999986, "name": "onSet", - "startTime": 2504.9999999999864, + "startTime": 2505.0999999999863, "tooltip": "onSet, 0.1000 (ms)", "value": 100, }, @@ -1326,16 +1326,16 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] }, { "children": [], - "endTime": 2505.399999999986, + "endTime": 2505.499999999986, "name": "onUpdate", - "startTime": 2505.299999999986, + "startTime": 2505.399999999986, "tooltip": "onUpdate, 0.1000 (ms)", "value": 100, }, ], - "endTime": 2505.599999999986, + "endTime": 2505.6999999999857, "name": "resolve", - "startTime": 2503.099999999988, + "startTime": 2503.199999999988, "tooltip": "resolve, 2.5000 (ms)", "value": 2500, }, @@ -1349,9 +1349,9 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] }, { "children": [], - "endTime": 2505.8999999999855, + "endTime": 2505.9999999999854, "name": "skipTransition", - "startTime": 2505.7999999999856, + "startTime": 2505.8999999999855, "tooltip": "skipTransition, 0.1000 (ms)", "value": 100, }, @@ -1365,9 +1365,9 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] }, { "children": [], - "endTime": 2506.1999999999853, + "endTime": 2506.299999999985, "name": "beforeTransition", - "startTime": 2506.0999999999854, + "startTime": 2506.1999999999853, "tooltip": "beforeTransition, 0.1000 (ms)", "value": 100, }, @@ -1381,9 +1381,9 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] }, { "children": [], - "endTime": 2506.499999999985, + "endTime": 2506.599999999985, "name": "resolveTransitionNode", - "startTime": 2506.399999999985, + "startTime": 2506.499999999985, "tooltip": "resolveTransitionNode, 0.1000 (ms)", "value": 100, }, @@ -1397,9 +1397,9 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] }, { "children": [], - "endTime": 2506.7999999999847, + "endTime": 2506.8999999999846, "name": "transition", - "startTime": 2506.699999999985, + "startTime": 2506.7999999999847, "tooltip": "transition, 0.1000 (ms)", "value": 100, }, @@ -1413,16 +1413,16 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] }, { "children": [], - "endTime": 2507.0999999999844, + "endTime": 2507.1999999999844, "name": "afterTransition", - "startTime": 2506.9999999999845, + "startTime": 2507.0999999999844, "tooltip": "afterTransition, 0.1000 (ms)", "value": 100, }, ], - "endTime": 2507.2999999999843, + "endTime": 2507.399999999984, "name": "afterTransition", - "startTime": 2502.3999999999887, + "startTime": 2502.4999999999886, "tooltip": "afterTransition, 4.9000 (ms)", "value": 4900, }, @@ -1436,9 +1436,9 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] }, { "children": [], - "endTime": 2507.599999999984, + "endTime": 2507.699999999984, "name": "onGet", - "startTime": 2507.499999999984, + "startTime": 2507.599999999984, "tooltip": "onGet, 0.1000 (ms)", "value": 100, }, @@ -1452,9 +1452,9 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] }, { "children": [], - "endTime": 2507.8999999999837, + "endTime": 2507.9999999999836, "name": "serialize", - "startTime": 2507.799999999984, + "startTime": 2507.8999999999837, "tooltip": "serialize, 0.1000 (ms)", "value": 100, }, @@ -1468,16 +1468,16 @@ exports[`Plugin > should profile player hooks when navigating through a flow 1`] }, { "children": [], - "endTime": 2508.1999999999834, + "endTime": 2508.2999999999834, "name": "state", - "startTime": 2508.0999999999835, + "startTime": 2508.1999999999834, "tooltip": "state, 0.1000 (ms)", "value": 100, }, ], - "endTime": 2508.1999999999834, + "endTime": 2508.2999999999834, "name": "root", - "startTime": 2490.1, + "startTime": 2490.2, "value": 18100, }, } diff --git a/devtools/plugins/profiler/core/src/__tests__/plugin.test.ts b/devtools/plugins/profiler/core/src/__tests__/plugin.test.ts index 206a569..e618123 100644 --- a/devtools/plugins/profiler/core/src/__tests__/plugin.test.ts +++ b/devtools/plugins/profiler/core/src/__tests__/plugin.test.ts @@ -70,7 +70,7 @@ describe("Plugin", () => { expect(playerState.status).toBe("in-progress"); expect( (playerState as InProgressState).controllers.view.currentView - ?.lastUpdate + ?.lastUpdate, ).toBeDefined(); }); @@ -94,7 +94,7 @@ describe("Plugin", () => { const storeState = profilerPlugin.store.getState(); expect( - storeState.plugins["player-ui-profiler-plugin"]?.flow.data + storeState.plugins["player-ui-profiler-plugin"]?.flow.data, ).toMatchSnapshot(); }); @@ -134,7 +134,7 @@ describe("Plugin", () => { expect(playerState.status).toBe("in-progress"); expect( (playerState as InProgressState).controllers.view.currentView - ?.lastUpdate + ?.lastUpdate, ).toBeDefined(); }); diff --git a/devtools/plugins/profiler/core/src/addProfilerInterceptorsToHooks.ts b/devtools/plugins/profiler/core/src/addProfilerInterceptorsToHooks.ts index 013e9a0..c23c6f3 100644 --- a/devtools/plugins/profiler/core/src/addProfilerInterceptorsToHooks.ts +++ b/devtools/plugins/profiler/core/src/addProfilerInterceptorsToHooks.ts @@ -44,7 +44,7 @@ export const addProfilerInterceptorsToHooks = ( obj: unknown, profiler: Profiler, currentPath: string[] = [], - intercepted: WeakSet = new WeakSet() + intercepted: WeakSet = new WeakSet(), ): void => { if (!hasHooks(obj)) { return; @@ -69,7 +69,7 @@ export const addProfilerInterceptorsToHooks = ( args[0], profiler, nextPath, - intercepted + intercepted, ); } diff --git a/devtools/plugins/profiler/core/src/helpers/__tests__/profiler.test.ts b/devtools/plugins/profiler/core/src/helpers/__tests__/profiler.test.ts index 19990ea..31c00f9 100644 --- a/devtools/plugins/profiler/core/src/helpers/__tests__/profiler.test.ts +++ b/devtools/plugins/profiler/core/src/helpers/__tests__/profiler.test.ts @@ -115,28 +115,28 @@ describe("Profiler", () => { expect(rootNodes).toHaveLength(0); }); - test("calls onUpdate on endTimer() and clear(), not on startTimer() or start()", () => { + test("calls onUpdate on start(), endTimer(), and clear(), not on startTimer()", () => { const onUpdate = vi.fn(); const p = new Profiler(onUpdate); expect(onUpdate.mock.calls.length).toBe(0); p.start(); - expect(onUpdate.mock.calls.length).toBe(0); + expect(onUpdate.mock.calls.length).toBe(1); p.startTimer("hookA"); - expect(onUpdate.mock.calls.length).toBe(0); + expect(onUpdate.mock.calls.length).toBe(1); p.endTimer({ hookName: "hookA" }); - expect(onUpdate.mock.calls.length).toBe(1); + expect(onUpdate.mock.calls.length).toBe(2); p.startTimer("hookB"); p.endTimer({ hookName: "hookB" }); - expect(onUpdate.mock.calls.length).toBe(2); + expect(onUpdate.mock.calls.length).toBe(3); p.startTimer("hookC"); p.clear(); - expect(onUpdate.mock.calls.length).toBe(3); + expect(onUpdate.mock.calls.length).toBe(4); }); test("getSnapshot returns a deep clone of rootNodes", () => { diff --git a/devtools/plugins/profiler/core/src/helpers/__tests__/transformProfilerData.test.ts b/devtools/plugins/profiler/core/src/helpers/__tests__/transformProfilerData.test.ts index 1a7e329..24908ce 100644 --- a/devtools/plugins/profiler/core/src/helpers/__tests__/transformProfilerData.test.ts +++ b/devtools/plugins/profiler/core/src/helpers/__tests__/transformProfilerData.test.ts @@ -6,7 +6,7 @@ const node = ( name: string, startTime: number, endTime: number, - children: ProfilerNode[] = [] + children: ProfilerNode[] = [], ): ProfilerNode => ({ name, startTime, @@ -101,7 +101,13 @@ describe("transformProfilerData", () => { endTime: 2, value: 2000, children: [ - { name: "zero-value", startTime: 0, endTime: 0, value: 0, children: [] }, + { + name: "zero-value", + startTime: 0, + endTime: 0, + value: 0, + children: [], + }, node("valid", 0, 1), ], }; @@ -131,10 +137,7 @@ describe("transformProfilerData", () => { }); test("multiple gaps each produce their own spacer", () => { - const root = node("root", 0, 6, [ - node("a", 1, 2), - node("b", 4, 5), - ]); + const root = node("root", 0, 6, [node("a", 1, 2), node("b", 4, 5)]); const { children } = transformProfilerData(root); diff --git a/devtools/plugins/profiler/core/src/helpers/profiler.ts b/devtools/plugins/profiler/core/src/helpers/profiler.ts index 0720254..9b8746d 100644 --- a/devtools/plugins/profiler/core/src/helpers/profiler.ts +++ b/devtools/plugins/profiler/core/src/helpers/profiler.ts @@ -94,7 +94,7 @@ export class Profiler { for (let i = this.stack.length - 1; i > targetIdx; i--) { const orphan = this.stack[i]!; console.warn( - `endTimer: popping '${orphan.name}' — timer was never explicitly ended` + `endTimer: popping '${orphan.name}' — timer was never explicitly ended`, ); this.finalizeNode(orphan, endTime); } diff --git a/devtools/plugins/profiler/core/src/helpers/transformProfilerData.ts b/devtools/plugins/profiler/core/src/helpers/transformProfilerData.ts index 25fa2fa..ab8f7d8 100644 --- a/devtools/plugins/profiler/core/src/helpers/transformProfilerData.ts +++ b/devtools/plugins/profiler/core/src/helpers/transformProfilerData.ts @@ -18,7 +18,7 @@ export const transformProfilerData = (root: ProfilerNode): ProfilerNode => { const transformProfilerDataHelper = ( nodes: ProfilerNode[], - parentStart: number = 0 + parentStart: number = 0, ): ProfilerNode[] => { const merged: ProfilerNode[] = []; let cursor = parentStart; diff --git a/devtools/plugins/profiler/core/src/plugin.ts b/devtools/plugins/profiler/core/src/plugin.ts index ffbc3b7..d64119c 100644 --- a/devtools/plugins/profiler/core/src/plugin.ts +++ b/devtools/plugins/profiler/core/src/plugin.ts @@ -23,7 +23,7 @@ const wrapInRoot = (nodes: ProfilerNode[]): ProfilerNode => { n.startTime !== undefined && (min === undefined || n.startTime < min) ? n.startTime : min, - undefined + undefined, ) ?? 0; const endTime = nodes.reduce( @@ -31,7 +31,7 @@ const wrapInRoot = (nodes: ProfilerNode[]): ProfilerNode => { n.endTime !== undefined && (max === undefined || n.endTime > max) ? n.endTime : max, - undefined + undefined, ) ?? startTime; return { name: "root", @@ -73,14 +73,14 @@ export class ProfilerDevtoolsPlugin extends DevtoolsPlugin { ["plugins", pluginID, "flow", "data", "rootNode"], transformProfilerData(wrapInRoot(rootNodes)), ], - [["plugins", pluginID, "flow", "data", "rawNodes"], rootNodes] + [["plugins", pluginID, "flow", "data", "rawNodes"], rootNodes], ); this.store.dispatch( genDataChangeTransaction({ playerID: this.playerID, data: newState.plugins[pluginID]?.flow.data, pluginID, - }) + }), ); }); } @@ -92,7 +92,7 @@ export class ProfilerDevtoolsPlugin extends DevtoolsPlugin { dset( draft, ["plugins", pluginID, "flow", "data", "displayFlameGraph"], - false + false, ); }); this.store.dispatch( @@ -100,7 +100,7 @@ export class ProfilerDevtoolsPlugin extends DevtoolsPlugin { playerID: this.playerID, data: newState.plugins[pluginID]?.flow.data, pluginID, - }) + }), ); } @@ -118,14 +118,14 @@ export class ProfilerDevtoolsPlugin extends DevtoolsPlugin { ], [["plugins", pluginID, "flow", "data", "rawNodes"], rootNodes], [["plugins", pluginID, "flow", "data", "profiling"], false], - [["plugins", pluginID, "flow", "data", "displayFlameGraph"], true] + [["plugins", pluginID, "flow", "data", "displayFlameGraph"], true], ); this.store.dispatch( genDataChangeTransaction({ playerID: this.playerID, data: newState.plugins[pluginID]?.flow.data, pluginID, - }) + }), ); return result; } @@ -147,7 +147,7 @@ export class ProfilerDevtoolsPlugin extends DevtoolsPlugin { dset( draft, ["plugins", pluginID, "flow", "data", "displayFlameGraph"], - false + false, ); }); @@ -156,7 +156,7 @@ export class ProfilerDevtoolsPlugin extends DevtoolsPlugin { playerID: this.playerID, data: initialState.plugins[pluginID]?.flow.data, pluginID, - }) + }), ); } diff --git a/devtools/plugins/profiler/jvm/src/main/kotlin/com/intuit/playerui/plugins/devtools/profiler/ProfilerDevtoolsPlugin.kt b/devtools/plugins/profiler/jvm/src/main/kotlin/com/intuit/playerui/plugins/devtools/profiler/ProfilerDevtoolsPlugin.kt index b37d3e2..5bd85a0 100644 --- a/devtools/plugins/profiler/jvm/src/main/kotlin/com/intuit/playerui/plugins/devtools/profiler/ProfilerDevtoolsPlugin.kt +++ b/devtools/plugins/profiler/jvm/src/main/kotlin/com/intuit/playerui/plugins/devtools/profiler/ProfilerDevtoolsPlugin.kt @@ -1,7 +1,6 @@ package com.intuit.playerui.plugins.devtools.profiler import com.intuit.playerui.core.bridge.Node -import com.intuit.playerui.core.bridge.deserialize import com.intuit.playerui.core.bridge.runtime.Runtime import com.intuit.playerui.core.bridge.runtime.add import com.intuit.playerui.core.bridge.serialization.serializers.NodeWrapperSerializer @@ -9,17 +8,15 @@ import com.intuit.playerui.core.player.PlayerException import com.intuit.playerui.core.plugins.RuntimePlugin import com.intuit.playerui.devtools.DevtoolsHandler import com.intuit.playerui.devtools.DevtoolsPlugin -import com.intuit.playerui.devtools.DevtoolsPluginInteractionEvent import com.intuit.playerui.devtools.ModuleLoader -import com.intuit.playerui.devtools.PluginData import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable -import kotlinx.serialization.Transient import java.util.concurrent.atomic.AtomicInteger @Serializable(with = ProfilerDevtoolsPlugin.Serializer::class) -public class ProfilerDevtoolsPlugin(node: Node) : DevtoolsPlugin(node) { - +public class ProfilerDevtoolsPlugin( + node: Node, +) : DevtoolsPlugin(node) { @Serializable public data class Options( public val playerID: String, @@ -28,17 +25,19 @@ public class ProfilerDevtoolsPlugin(node: Node) : DevtoolsPlugin(node) { public companion object Module : RuntimePlugin by ModuleLoader( ProfilerDevtoolsPlugin.NAME, - ProfilerDevtoolsPlugin.BUNDLED_SOURCE_PATH + ProfilerDevtoolsPlugin.BUNDLED_SOURCE_PATH, ) { private val count = AtomicInteger(0) + public fun Runtime<*>.ProfilerDevtoolsPlugin(options: Options): ProfilerDevtoolsPlugin { runtime.execute("class WeakRef { value = null; constructor(value) { this.value = value }; deref() { return this.value } }") val argsKey = "profilerDevtoolsPluginArgs_${count.getAndIncrement()}" runtime.add(argsKey, options) apply(this) - val instance = runtime.execute("(new ${NAME}.${NAME}($argsKey))") as? Node - ?: throw PlayerException("Could not instantiate ProfilerDevtoolsPlugin") + val instance = + runtime.execute("(new $NAME.$NAME($argsKey))") as? Node + ?: throw PlayerException("Could not instantiate ProfilerDevtoolsPlugin") return ProfilerDevtoolsPlugin(instance) } diff --git a/devtools/plugins/profiler/jvm/src/test/kotlin/com/intuit/playerui/plugins/devtools/profiler/ProfilerDevtoolsPluginTest.kt b/devtools/plugins/profiler/jvm/src/test/kotlin/com/intuit/playerui/plugins/devtools/profiler/ProfilerDevtoolsPluginTest.kt index 44c85a2..0a41565 100644 --- a/devtools/plugins/profiler/jvm/src/test/kotlin/com/intuit/playerui/plugins/devtools/profiler/ProfilerDevtoolsPluginTest.kt +++ b/devtools/plugins/profiler/jvm/src/test/kotlin/com/intuit/playerui/plugins/devtools/profiler/ProfilerDevtoolsPluginTest.kt @@ -9,8 +9,9 @@ import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test -class ProfilerDevtoolsPluginTest : RuntimeTest(), DevtoolsHandler { - +class ProfilerDevtoolsPluginTest : + RuntimeTest(), + DevtoolsHandler { private val interactions = mutableListOf() override fun processInteraction(interaction: DevtoolsPluginInteractionEvent) { @@ -19,8 +20,7 @@ class ProfilerDevtoolsPluginTest : RuntimeTest(), DevtoolsHandler { override fun checkIfDevtoolsIsActive(): Boolean = true - private fun plugin(id: String = "test") = - runtime.ProfilerDevtoolsPlugin(ProfilerDevtoolsPlugin.Options(id, this)) + private fun plugin(id: String = "test") = runtime.ProfilerDevtoolsPlugin(ProfilerDevtoolsPlugin.Options(id, this)) // MARK: - Initialization @@ -66,11 +66,12 @@ class ProfilerDevtoolsPluginTest : RuntimeTest(), DevtoolsHandler { } } -private fun interactionEvent(type: String) = DevtoolsPluginInteractionEvent( - payload = DevtoolsPluginInteractionEvent.Payload(type = type, payload = ""), - id = 0, - timestamp = 0, - sender = "test", - context = kotlinx.serialization.json.JsonPrimitive("player"), - tag = false, -) +private fun interactionEvent(type: String) = + DevtoolsPluginInteractionEvent( + payload = DevtoolsPluginInteractionEvent.Payload(type = type, payload = ""), + id = 0, + timestamp = 0, + sender = "test", + context = kotlinx.serialization.json.JsonPrimitive("player"), + tag = false, + )