diff --git a/.circleci/config.yml b/.circleci/config.yml index c9beed1..2de3de1 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -10,8 +10,7 @@ parameters: orbs: macos: circleci/macos@2.5.4 - # android: circleci/android@3.1.0 - codecov: codecov/codecov@5.4.3 + codecov: codecov/codecov@6.0.0 executors: base: diff --git a/Package.swift b/Package.swift index 3f54ec5..695923f 100644 --- a/Package.swift +++ b/Package.swift @@ -12,6 +12,7 @@ let excluded = ["ViewInspector", "UITests", "Tests", "BUILD"] let playerUIDependency: Target.Dependency = .product(name: "PlayerUI", package: "playerui-swift-package") let playerUILoggerDependency: Target.Dependency = .product(name: "PlayerUILogger", package: "playerui-swift-package") +let playerUISwiftUIDependency: Target.Dependency = .product(name: "PlayerUISwiftUI", package: "playerui-swift-package") let swiftFlipperDependency: Target.Dependency = .product(name: "SwiftFlipper", package: "SwiftFlipper") let utils: Target = .target( @@ -105,6 +106,30 @@ let basicPlugin: Target = .target( exclude: excluded ) +let baseProfilerDevtoolsPlugin: Target = .target( + name: "PlayerUIDevtoolsBaseProfilerDevtoolsPlugin", + dependencies: [ + playerUIDependency, + playerUILoggerDependency, + "PlayerUIDevtoolsPlugin", + "PlayerUIDevtoolsUtilsSwiftUI" + ], + path: "devtools/plugins/profiler/ios", + exclude: excluded, + resources: [.process("Resources")] +) + +let profilerPlugin: Target = .target( + name: "PlayerUIDevtoolsProfilerPlugin", + dependencies: [ + swiftFlipperDependency, + "PlayerUIDevtoolsSwiftUIPlugin", + "PlayerUIDevtoolsBaseProfilerDevtoolsPlugin" + ], + path: "devtools/plugins/profiler/swiftui", + exclude: excluded +) + // --- END DECLARATIONS --- let allTargets: [Target] = [ @@ -115,7 +140,9 @@ let allTargets: [Target] = [ plugin, swiftUIPlugin, baseBasicDevtoolsPlugin, - basicPlugin + basicPlugin, + baseProfilerDevtoolsPlugin, + profilerPlugin ] // This is the Package.swift for our SPM release. 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 0d29030..3c0419f 100644 --- a/devtools/plugin/core/src/helpers/uuid.ts +++ b/devtools/plugin/core/src/helpers/uuid.ts @@ -1,11 +1,10 @@ +import { getNowTime } from "./getNowTime"; + +// 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/plugins/profiler/android/BUILD b/devtools/plugins/profiler/android/BUILD new file mode 100644 index 0000000..9626e3c --- /dev/null +++ b/devtools/plugins/profiler/android/BUILD @@ -0,0 +1,34 @@ +# Android lib that consumes jvm similar to how react consumes core +load("@rules_jvm_external//:defs.bzl", "artifact") +load("//helpers:android.bzl", "kt_android") + +main_exports = [ + "//devtools/plugin/android:plugin-android", +] + +main_deps = main_exports + [ + "//devtools/plugins/profiler/jvm:profiler-plugin", +] + +main_resources = [] + +test_deps = [ + "//helpers:kotlin_serialization", + artifact("com.intuit.playerui:testutils"), + artifact("com.intuit.playerui:j2v8-all"), +] + +kt_android( + name = "profiler-android", + group = "com.intuit.playerui.plugins.devtools.profiler", + main_deps = main_deps, + main_exports = main_exports, + main_resources = main_resources, + unit_test_deps = test_deps, + unit_test_package = "com.intuit.playerui.plugins.devtools.profiler", +) + +alias( + name = "android", + actual = "profiler-android", +) diff --git a/devtools/plugins/profiler/android/src/main/AndroidManifest.xml b/devtools/plugins/profiler/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..d086f67 --- /dev/null +++ b/devtools/plugins/profiler/android/src/main/AndroidManifest.xml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/devtools/plugins/profiler/android/src/main/kotlin/com/intuit/playerui/plugins/devtools/profiler/ProfilerAndroidDevtoolsPlugin.kt b/devtools/plugins/profiler/android/src/main/kotlin/com/intuit/playerui/plugins/devtools/profiler/ProfilerAndroidDevtoolsPlugin.kt new file mode 100644 index 0000000..a9145dd --- /dev/null +++ b/devtools/plugins/profiler/android/src/main/kotlin/com/intuit/playerui/plugins/devtools/profiler/ProfilerAndroidDevtoolsPlugin.kt @@ -0,0 +1,29 @@ +package com.intuit.playerui.plugins.devtools.profiler + +import androidx.annotation.StyleRes +import com.intuit.playerui.android.AndroidPlayer +import com.intuit.playerui.core.bridge.runtime.Runtime +import com.intuit.playerui.devtools.AndroidDevtoolsPlugin +import com.intuit.playerui.plugins.devtools.profiler.ProfilerDevtoolsPlugin.Module.ProfilerDevtoolsPlugin + +public class ProfilerAndroidDevtoolsPlugin( + private val id: String, + @StyleRes private val overlayStyle: Int? = R.style.ProfilerAndroidDevtoolsPlugin, +) : AndroidDevtoolsPlugin() { + override fun Runtime<*>.buildCorePlugin(): ProfilerDevtoolsPlugin = + ProfilerDevtoolsPlugin( + ProfilerDevtoolsPlugin.Options(id, this@ProfilerAndroidDevtoolsPlugin), + ) + + override fun apply(androidPlayer: AndroidPlayer) { + if (!checkIfDevtoolsIsActive()) return + + super.apply(androidPlayer) + + overlayStyle?.let(::listOf)?.let { + androidPlayer.hooks.context.tap(this::class.simpleName!!) { _, context -> + androidPlayer.getCachedStyledContext(context, it) + } + } + } +} diff --git a/devtools/plugins/profiler/android/src/main/res/values/styles.xml b/devtools/plugins/profiler/android/src/main/res/values/styles.xml new file mode 100644 index 0000000..05f069f --- /dev/null +++ b/devtools/plugins/profiler/android/src/main/res/values/styles.xml @@ -0,0 +1,6 @@ + + + + diff --git a/devtools/plugins/profiler/content/BUILD b/devtools/plugins/profiler/content/BUILD new file mode 100644 index 0000000..396c5d4 --- /dev/null +++ b/devtools/plugins/profiler/content/BUILD @@ -0,0 +1,47 @@ +load("@npm//:defs.bzl", "npm_link_all_packages") +load("@rules_player//javascript:defs.bzl", "js_pipeline") +load("@rules_player//player:defs.bzl", "dsl_compile", create_base_dsl_config = "create_base_config") +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") + +create_base_dsl_config( + name = "dsl_config", + presets = [], +) + +entrypoint = ["src/flow.tsx"] + +dsl_compile( + name = "compiled_flow", + srcs = entrypoint, + config = ":dsl_config", + data = glob( + ["src/**"], + entrypoint, + ) + [ + "//:node_modules/@devtools-ui/plugin", + "//:node_modules/@player-tools/dsl", + "//:node_modules/@player-ui/common-types-plugin", + "//:node_modules/react", + ], + input_dir = "src", + output_dir = "_generated", + skip_test = True, +) + +js_pipeline( + package_name = "@player-devtools/profiler-plugin-content", + srcs = [ + "src/constants.ts", + "src/index.ts", + ":compiled_flow", + ], + deps = [ + ":node_modules/@player-devtools/types", + ], +) diff --git a/devtools/plugins/profiler/content/package.json b/devtools/plugins/profiler/content/package.json new file mode 100644 index 0000000..7024d45 --- /dev/null +++ b/devtools/plugins/profiler/content/package.json @@ -0,0 +1,8 @@ +{ + "name": "@player-devtools/profiler-plugin-content", + "version": "0.0.0-PLACEHOLDER", + "main": "src/index.ts", + "dependencies": { + "@player-devtools/types": "workspace:*" + } +} diff --git a/devtools/plugins/profiler/content/src/common/ProfilerFooter.tsx b/devtools/plugins/profiler/content/src/common/ProfilerFooter.tsx new file mode 100644 index 0000000..fab225a --- /dev/null +++ b/devtools/plugins/profiler/content/src/common/ProfilerFooter.tsx @@ -0,0 +1,30 @@ +import React from "react"; +import { Collection, Action, Text } from "@devtools-ui/plugin"; +import { expression as e } from "@player-tools/dsl"; +import type { Expression } from "@player-tools/dsl"; +import { INTERACTIONS } from "../constants"; +import { bindings } from "../schema"; + +const toggleProfiling = e`conditional(${bindings.profiling} === true, publish('${INTERACTIONS.STOP_PROFILING}'), publish('${INTERACTIONS.START_PROFILING}'))`; + +const toggleLabel = e`conditional(${bindings.profiling} === true, 'Stop', 'Start')`; + +const reset = e`publish('${INTERACTIONS.RESET_PROFILING}')`; + +/** Shared footer with a Start/Stop toggle and a Reset action. */ +export const ProfilerFooter = ( + + + + + {toggleLabel} + + + + + Reset + + + + +); diff --git a/devtools/plugins/profiler/content/src/common/Screen.tsx b/devtools/plugins/profiler/content/src/common/Screen.tsx new file mode 100644 index 0000000..cf3df4e --- /dev/null +++ b/devtools/plugins/profiler/content/src/common/Screen.tsx @@ -0,0 +1,41 @@ +import React from "react"; +import { Navigation, Action, Text, StackedView } from "@devtools-ui/plugin"; +import { VIEWS_IDS } from "../constants"; + +/** Display labels for the navigation actions, keyed by view id. */ +const NAV_LABELS: Record = { + [VIEWS_IDS.PROFILE]: "Flame Graph", + [VIEWS_IDS.RAW]: "Raw", +}; + +const Nav = () => ( + + + {Object.values(VIEWS_IDS).map((viewId) => ( + + + {NAV_LABELS[viewId] ?? viewId} + + + ))} + + +); + +export const Screen = ({ + main, + footer, + id, +}: { + id: string; + main: React.ReactNode; + footer?: React.ReactNode; +}) => ( + + +