diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-1-Google-Chrome-linux.png index d5e244dd0..2450b64d1 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-1-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-1-Google-Chrome-win32.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-1-Google-Chrome-win32.png index f6fb63304..3c526ec8a 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-1-Google-Chrome-win32.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-1-Google-Chrome-win32.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-2-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-2-Google-Chrome-linux.png index ee85d2fdd..fa7c3287f 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-2-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-2-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-2-Google-Chrome-win32.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-2-Google-Chrome-win32.png index 7f43f1aa4..cdc1bdcbf 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-2-Google-Chrome-win32.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-2-Google-Chrome-win32.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-1-Google-Chrome-linux.png index b2dc6ee66..ab801a598 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-1-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-1-Google-Chrome-win32.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-1-Google-Chrome-win32.png index a8624a7c0..b17b2b566 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-1-Google-Chrome-win32.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-1-Google-Chrome-win32.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-2-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-2-Google-Chrome-linux.png index 15de7faa7..c448e34f9 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-2-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-2-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-2-Google-Chrome-win32.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-2-Google-Chrome-win32.png index 965a62b64..ade086c0d 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-2-Google-Chrome-win32.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-2-Google-Chrome-win32.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-circle-should-look-right-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-circle-should-look-right-1-Google-Chrome-linux.png index 039d4b37c..5df4be739 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-circle-should-look-right-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-circle-should-look-right-1-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-circle-should-look-right-1-Google-Chrome-win32.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-circle-should-look-right-1-Google-Chrome-win32.png index 141a2a5c9..b481cb02a 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-circle-should-look-right-1-Google-Chrome-win32.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-circle-should-look-right-1-Google-Chrome-win32.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-rectangles-should-look-right-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-rectangles-should-look-right-1-Google-Chrome-linux.png index 1f156f74e..96422ab7a 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-rectangles-should-look-right-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-rectangles-should-look-right-1-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-rectangles-should-look-right-1-Google-Chrome-win32.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-rectangles-should-look-right-1-Google-Chrome-win32.png index aa7d7718d..8fdf3768c 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-rectangles-should-look-right-1-Google-Chrome-win32.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-rectangles-should-look-right-1-Google-Chrome-win32.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-1-Google-Chrome-linux.png index f088bb6bd..38d1cb522 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-1-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-1-Google-Chrome-win32.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-1-Google-Chrome-win32.png index 95df01018..fe490db6f 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-1-Google-Chrome-win32.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-1-Google-Chrome-win32.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-2-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-2-Google-Chrome-linux.png index 385274758..a3a2f4088 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-2-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-2-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-2-Google-Chrome-win32.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-2-Google-Chrome-win32.png index 4ed1f33ca..0a26cefc2 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-2-Google-Chrome-win32.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-2-Google-Chrome-win32.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Grid-visibility-Grid-turned-off-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Grid-visibility-Grid-turned-off-1-Google-Chrome-linux.png index 0b65be0ff..378c43abb 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Grid-visibility-Grid-turned-off-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Grid-visibility-Grid-turned-off-1-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Grid-visibility-Grid-turned-off-1-Google-Chrome-win32.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Grid-visibility-Grid-turned-off-1-Google-Chrome-win32.png index a6b9bab59..be55c744a 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Grid-visibility-Grid-turned-off-1-Google-Chrome-win32.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Grid-visibility-Grid-turned-off-1-Google-Chrome-win32.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Grid-visibility-Grid-turned-on-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Grid-visibility-Grid-turned-on-1-Google-Chrome-linux.png index 6eccdf765..0fc060736 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Grid-visibility-Grid-turned-on-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Grid-visibility-Grid-turned-on-1-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Grid-visibility-Grid-turned-on-1-Google-Chrome-win32.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Grid-visibility-Grid-turned-on-1-Google-Chrome-win32.png index 64ba34e24..83a3c1d39 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Grid-visibility-Grid-turned-on-1-Google-Chrome-win32.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Grid-visibility-Grid-turned-on-1-Google-Chrome-win32.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Sketch-on-face-with-none-z-up-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Sketch-on-face-with-none-z-up-1-Google-Chrome-linux.png index 329a36b9c..140271cb2 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Sketch-on-face-with-none-z-up-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Sketch-on-face-with-none-z-up-1-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Sketch-on-face-with-none-z-up-1-Google-Chrome-win32.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Sketch-on-face-with-none-z-up-1-Google-Chrome-win32.png index ed36aa851..110ae2f07 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Sketch-on-face-with-none-z-up-1-Google-Chrome-win32.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Sketch-on-face-with-none-z-up-1-Google-Chrome-win32.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--XY-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--XY-1-Google-Chrome-linux.png index 58f0281c7..06b144cfc 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--XY-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--XY-1-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--XZ-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--XZ-1-Google-Chrome-linux.png index 56b53d595..6414f5ec1 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--XZ-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--XZ-1-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--XZ-1-Google-Chrome-win32.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--XZ-1-Google-Chrome-win32.png index 8448bb283..ed11f9eec 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--XZ-1-Google-Chrome-win32.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--XZ-1-Google-Chrome-win32.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--YZ-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--YZ-1-Google-Chrome-linux.png index 31a79978a..9b6fb6c3a 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--YZ-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--YZ-1-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--YZ-1-Google-Chrome-win32.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--YZ-1-Google-Chrome-win32.png index 893ff191b..2fd9e0982 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--YZ-1-Google-Chrome-win32.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--YZ-1-Google-Chrome-win32.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-XZ-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-XZ-1-Google-Chrome-linux.png index 0fb86d04c..af3ff5c42 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-XZ-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-XZ-1-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-YZ-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-YZ-1-Google-Chrome-linux.png index 801a70566..2e8fff59e 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-YZ-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-YZ-1-Google-Chrome-linux.png differ diff --git a/interface.d.ts b/interface.d.ts index 8c06070fa..af41a148d 100644 --- a/interface.d.ts +++ b/interface.d.ts @@ -78,6 +78,7 @@ export interface IElectronAPI { ) => Electron.IpcRenderer onUpdateError: (callback: (value: { error: Error }) => void) => Electron appRestart: () => void + getArgvParsed: () => any } declare global { diff --git a/package.json b/package.json index 3f5e19103..39732d132 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,8 @@ "vscode-languageserver-protocol": "^3.17.5", "vscode-uri": "^3.0.8", "web-vitals": "^3.5.2", - "xstate": "^5.17.4" + "xstate": "^5.17.4", + "yargs": "^17.7.2" }, "scripts": { "start": "vite", diff --git a/src/App.tsx b/src/App.tsx index a0ca7ec08..dcc0bed9f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -23,6 +23,10 @@ import { UnitsMenu } from 'components/UnitsMenu' import { CameraProjectionToggle } from 'components/CameraProjectionToggle' import EngineStreamContext from 'hooks/useEngineStreamContext' import { EngineStream } from 'components/EngineStream' +import { maybeWriteToDisk } from 'lib/telemetry' +maybeWriteToDisk() + .then(() => {}) + .catch(() => {}) export function App() { const { project, file } = useLoaderData() as IndexLoaderData diff --git a/src/Router.tsx b/src/Router.tsx index d135cfc76..82d42762e 100644 --- a/src/Router.tsx +++ b/src/Router.tsx @@ -8,6 +8,7 @@ import { } from 'react-router-dom' import { ErrorPage } from './components/ErrorPage' import { Settings } from './routes/Settings' +import { Telemetry } from './routes/Telemetry' import Onboarding, { onboardingRoutes } from './routes/Onboarding' import SignIn from './routes/SignIn' import { Auth } from './Auth' @@ -28,6 +29,7 @@ import { homeLoader, onboardingRedirectLoader, settingsLoader, + telemetryLoader, } from 'lib/routeLoaders' import { CommandBarProvider } from 'components/CommandBar/CommandBarProvider' import SettingsAuthProvider from 'components/SettingsAuthProvider' @@ -43,6 +45,7 @@ import { coreDump } from 'lang/wasm' import { useMemo } from 'react' import { AppStateProvider } from 'AppState' import { reportRejection } from 'lib/trap' +import { RouteProvider } from 'components/RouteProvider' import { ProjectsContextProvider } from 'components/ProjectsContextProvider' const createRouter = isDesktop() ? createHashRouter : createBrowserRouter @@ -56,19 +59,21 @@ const router = createRouter([ * inefficient re-renders, use the react profiler to see. */ element: ( - - - - - - - - - - - - - + + + + + + + + + + + + + + + ), errorElement: , @@ -124,6 +129,16 @@ const router = createRouter([ }, ], }, + { + id: PATHS.FILE + 'TELEMETRY', + loader: telemetryLoader, + children: [ + { + path: makeUrlPathRelative(PATHS.TELEMETRY), + element: , + }, + ], + }, ], }, { @@ -149,6 +164,11 @@ const router = createRouter([ loader: settingsLoader, element: , }, + { + path: makeUrlPathRelative(PATHS.TELEMETRY), + loader: telemetryLoader, + element: , + }, ], }, { diff --git a/src/commandLineArgs.ts b/src/commandLineArgs.ts new file mode 100644 index 000000000..658364f66 --- /dev/null +++ b/src/commandLineArgs.ts @@ -0,0 +1,12 @@ +import yargs from 'yargs' +import { hideBin } from 'yargs/helpers' + +const argv = yargs(hideBin(process.argv)) + .option('telemetry', { + alias: 't', + type: 'boolean', + description: 'Writes startup telemetry to file on disk.', + }) + .parse() + +export default argv diff --git a/src/components/CustomIcon.tsx b/src/components/CustomIcon.tsx index b99ec4fdb..17a2700f3 100644 --- a/src/components/CustomIcon.tsx +++ b/src/components/CustomIcon.tsx @@ -1161,6 +1161,29 @@ const CustomIconMap = { /> ), + stopwatch: ( + + + + + + ), } as const export type CustomIconName = keyof typeof CustomIconMap diff --git a/src/components/FileMachineProvider.tsx b/src/components/FileMachineProvider.tsx index 9c6788e89..3f08619e5 100644 --- a/src/components/FileMachineProvider.tsx +++ b/src/components/FileMachineProvider.tsx @@ -29,6 +29,7 @@ import { KclSamplesManifestItem, } from 'lib/getKclSamplesManifest' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' +import { markOnce } from 'lib/performance' type MachineContext = { state: StateFrom @@ -54,6 +55,7 @@ export const FileMachineProvider = ({ ) useEffect(() => { + markOnce('code/didLoadFile') async function fetchKclSamples() { setKclSamples(await getKclSamplesManifest()) } diff --git a/src/components/LowerRightControls.tsx b/src/components/LowerRightControls.tsx index ad3ddb808..41531ef42 100644 --- a/src/components/LowerRightControls.tsx +++ b/src/components/LowerRightControls.tsx @@ -96,6 +96,23 @@ export function LowerRightControls({ Report a bug + + + Telemetry + + Telemetry + + { closeBrackets(), highlightActiveLine(), highlightSelectionMatches(), - syntaxHighlighting(defaultHighlightStyle, { fallback: true }), + syntaxHighlighting(defaultHighlightStyle, { + fallback: true, + }), rectangularSelection(), dropCursor(), interact({ diff --git a/src/components/RouteProvider.tsx b/src/components/RouteProvider.tsx new file mode 100644 index 000000000..103c18eae --- /dev/null +++ b/src/components/RouteProvider.tsx @@ -0,0 +1,33 @@ +import { useEffect, useState, createContext, ReactNode } from 'react' +import { useNavigation, useLocation } from 'react-router-dom' +import { PATHS } from 'lib/paths' +import { markOnce } from 'lib/performance' + +export const RouteProviderContext = createContext({}) + +export function RouteProvider({ children }: { children: ReactNode }) { + const [first, setFirstState] = useState(true) + const navigation = useNavigation() + const location = useLocation() + useEffect(() => { + // On initialization, the react-router-dom does not send a 'loading' state event. + // it sends an idle event first. + const pathname = first ? location.pathname : navigation.location?.pathname + const isHome = pathname === PATHS.HOME + const isFile = + pathname?.includes(PATHS.FILE) && + pathname?.substring(pathname?.length - 4) === '.kcl' + if (isHome) { + markOnce('code/willLoadHome') + } else if (isFile) { + markOnce('code/willLoadFile') + } + setFirstState(false) + }, [navigation]) + + return ( + + {children} + + ) +} diff --git a/src/components/SettingsAuthProvider.tsx b/src/components/SettingsAuthProvider.tsx index b26d0dfee..d04f2b930 100644 --- a/src/components/SettingsAuthProvider.tsx +++ b/src/components/SettingsAuthProvider.tsx @@ -1,7 +1,7 @@ import { trap } from 'lib/trap' import { useMachine } from '@xstate/react' import { useNavigate, useRouteLoaderData, useLocation } from 'react-router-dom' -import { PATHS } from 'lib/paths' +import { PATHS, BROWSER_PATH } from 'lib/paths' import { authMachine, TOKEN_PERSIST_KEY } from '../machines/authMachine' import withBaseUrl from '../lib/withBaseURL' import React, { createContext, useEffect, useState } from 'react' @@ -42,6 +42,7 @@ import { getAppSettingsFilePath } from 'lib/desktop' import { isDesktop } from 'lib/isDesktop' import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher' import { codeManager } from 'lib/singletons' +import { createRouteCommands } from 'lib/commandBarConfigs/routeCommandConfig' type MachineContext = { state: StateFrom @@ -288,6 +289,44 @@ export const SettingsAuthProviderBase = ({ settingsWithCommandConfigs, ]) + // Due to the route provider, i've moved this to the SettingsAuthProvider instead of CommandBarProvider + // This will register the commands to route to Telemetry, Home, and Settings. + useEffect(() => { + const filePath = + PATHS.FILE + + '/' + + encodeURIComponent(loadedProject?.file?.path || BROWSER_PATH) + const { RouteTelemetryCommand, RouteHomeCommand, RouteSettingsCommand } = + createRouteCommands(navigate, location, filePath) + commandBarSend({ + type: 'Remove commands', + data: { + commands: [ + RouteTelemetryCommand, + RouteHomeCommand, + RouteSettingsCommand, + ], + }, + }) + if (location.pathname === PATHS.HOME) { + commandBarSend({ + type: 'Add commands', + data: { commands: [RouteTelemetryCommand, RouteSettingsCommand] }, + }) + } else if (location.pathname.includes(PATHS.FILE)) { + commandBarSend({ + type: 'Add commands', + data: { + commands: [ + RouteTelemetryCommand, + RouteSettingsCommand, + RouteHomeCommand, + ], + }, + }) + } + }, [location]) + // Listen for changes to the system theme and update the app theme accordingly // This is only done if the theme setting is set to 'system'. // It can't be done in XState (in an invoked callback, for example) diff --git a/src/components/TelemetryExplorer.tsx b/src/components/TelemetryExplorer.tsx new file mode 100644 index 000000000..24c0d85a2 --- /dev/null +++ b/src/components/TelemetryExplorer.tsx @@ -0,0 +1,72 @@ +import { getMarks } from 'lib/performance' + +import { + printDeltaTotal, + printInvocationCount, + printMarkDownTable, + printRawMarks, +} from 'lib/telemetry' + +export function TelemetryExplorer() { + const marks = getMarks() + const markdownTable = printMarkDownTable(marks) + const rawMarks = printRawMarks(marks) + const deltaTotalTable = printDeltaTotal(marks) + const invocationCount = printInvocationCount(marks) + // TODO data-telemetry-type + // TODO data-telemetry-name + return ( +
+

Marks

+
+ {marks.map((mark, index) => { + return ( +
+              {JSON.stringify(mark, null, 2)}
+            
+ ) + })} +
+

Startup Performance

+
+ {markdownTable.map((line, index) => { + return ( +
+              {line}
+            
+ ) + })} +
+

Delta and Totals

+
+ {deltaTotalTable.map((line, index) => { + return ( +
+              {line}
+            
+ ) + })} +
+

Raw Marks

+
+ {rawMarks.map((line, index) => { + return ( +
+              {line}
+            
+ ) + })} +
+

Invocation Count

+
+ {invocationCount.map((line, index) => { + return ( +
+              {line}
+            
+ ) + })} +
+
+ ) +} diff --git a/src/editor/manager.ts b/src/editor/manager.ts index 4ed81375c..6bedf03d2 100644 --- a/src/editor/manager.ts +++ b/src/editor/manager.ts @@ -1,4 +1,5 @@ import { EditorView, ViewUpdate } from '@codemirror/view' +import { syntaxTree } from '@codemirror/language' import { EditorSelection, Annotation, Transaction } from '@codemirror/state' import { engineCommandManager } from 'lib/singletons' import { modelingMachine, ModelingMachineEvent } from 'machines/modelingMachine' @@ -12,6 +13,7 @@ import { setDiagnosticsEffect, } from '@codemirror/lint' import { StateFrom } from 'xstate' +import { markOnce } from 'lib/performance' const updateOutsideEditorAnnotation = Annotation.define() export const updateOutsideEditorEvent = updateOutsideEditorAnnotation.of(true) @@ -59,6 +61,48 @@ export default class EditorManager { setEditorView(editorView: EditorView) { this._editorView = editorView + this.overrideTreeHighlighterUpdateForPerformanceTracking() + } + + overrideTreeHighlighterUpdateForPerformanceTracking() { + // @ts-ignore + this._editorView?.plugins.forEach((e) => { + let sawATreeDiff = false + + // we cannot use <>.constructor.name since it will get destroyed + // when packaging the application. + const isTreeHighlightPlugin = + e.value.hasOwnProperty('tree') && + e.value.hasOwnProperty('decoratedTo') && + e.value.hasOwnProperty('decorations') + + if (isTreeHighlightPlugin) { + let originalUpdate = e.value.update + // @ts-ignore + function performanceTrackingUpdate(args) { + /** + * TreeHighlighter.update will be called multiple times on start up. + * We do not want to track the highlight performance of an empty update. + * mark the syntax highlight one time when the new tree comes in with the + * initial code + */ + const treeIsDifferent = + // @ts-ignore + !sawATreeDiff && this.tree !== syntaxTree(args.state) + if (treeIsDifferent && !sawATreeDiff) { + markOnce('code/willSyntaxHighlight') + } + // Call the original function + // @ts-ignore + originalUpdate.apply(this, [args]) + if (treeIsDifferent && !sawATreeDiff) { + markOnce('code/didSyntaxHighlight') + sawATreeDiff = true + } + } + e.value.update = performanceTrackingUpdate + } + }) } get editorView(): EditorView | null { diff --git a/src/index.tsx b/src/index.tsx index 77aa7fe83..12d1c361a 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -8,8 +8,10 @@ import ModalContainer from 'react-modal-promise' import { isDesktop } from 'lib/isDesktop' import { AppStreamProvider } from 'AppState' import { ToastUpdate } from 'components/ToastUpdate' +import { markOnce } from 'lib/performance' import { AUTO_UPDATER_TOAST_ID } from 'lib/constants' +markOnce('code/willAuth') // uncomment for xstate inspector // import { DEV } from 'env' // import { inspect } from '@xstate/inspect' diff --git a/src/lang/KclSingleton.ts b/src/lang/KclSingleton.ts index ce5ed236b..c164e9fcd 100644 --- a/src/lang/KclSingleton.ts +++ b/src/lang/KclSingleton.ts @@ -21,6 +21,7 @@ import { import { getNodeFromPath } from './queryAst' import { codeManager, editorManager, sceneInfra } from 'lib/singletons' import { Diagnostic } from '@codemirror/lint' +import { markOnce } from 'lib/performance' import { Node } from 'wasm-lib/kcl/bindings/Node' interface ExecuteArgs { @@ -257,6 +258,7 @@ export class KclManager { } const ast = args.ast || this.ast + markOnce('code/startExecuteAst') const currentExecutionId = args.executionId || Date.now() this._cancelTokens.set(currentExecutionId, false) @@ -325,6 +327,7 @@ export class KclManager { }) this._cancelTokens.delete(currentExecutionId) + markOnce('code/endExecuteAst') } // NOTE: this always updates the code state and editor. // DO NOT CALL THIS from codemirror ever. diff --git a/src/lang/std/engineConnection.ts b/src/lang/std/engineConnection.ts index 8ec5fa50c..590b1aa2d 100644 --- a/src/lang/std/engineConnection.ts +++ b/src/lang/std/engineConnection.ts @@ -28,6 +28,7 @@ import { } from 'lib/constants' import { KclManager } from 'lang/KclSingleton' import { reportRejection } from 'lib/trap' +import { markOnce } from 'lib/performance' import { MachineManager } from 'components/MachineManagerProvider' // TODO(paultag): This ought to be tweakable. @@ -330,6 +331,7 @@ class EngineConnection extends EventTarget { token?: string callbackOnEngineLiteConnect?: () => void }) { + markOnce('code/startInitialEngineConnect') super() this.engineCommandManager = engineCommandManager @@ -786,6 +788,7 @@ class EngineConnection extends EventTarget { this.dispatchEvent( new CustomEvent(EngineConnectionEvents.Opened, { detail: this }) ) + markOnce('code/endInitialEngineConnect') } this.unreliableDataChannel?.addEventListener( 'open', diff --git a/src/lib/commandBarConfigs/routeCommandConfig.ts b/src/lib/commandBarConfigs/routeCommandConfig.ts new file mode 100644 index 000000000..4c058137d --- /dev/null +++ b/src/lib/commandBarConfigs/routeCommandConfig.ts @@ -0,0 +1,52 @@ +import { Command } from '../commandTypes' +import { PATHS } from 'lib/paths' +import { NavigateFunction, Location } from 'react-router-dom' +export function createRouteCommands( + navigate: NavigateFunction, + location: Location, + filePath: string +) { + const RouteTelemetryCommand: Command = { + name: 'Go to Telemetry', + displayName: `Go to Telemetry`, + description: 'View the Telemetry metrics', + groupId: 'routes', + icon: 'settings', + needsReview: false, + onSubmit: (data) => { + const path = location.pathname.includes(PATHS.FILE) + ? filePath + PATHS.TELEMETRY + '?tab=project' + : PATHS.HOME + PATHS.TELEMETRY + navigate(path) + }, + } + + const RouteHomeCommand: Command = { + name: 'Go to Home', + displayName: `Go to Home`, + description: 'Go to the home page', + groupId: 'routes', + icon: 'settings', + needsReview: false, + onSubmit: (data) => { + navigate(PATHS.HOME) + }, + } + + const RouteSettingsCommand: Command = { + name: 'Go to Settings', + displayName: `Go to Settings`, + description: 'Go to the settings page', + groupId: 'routes', + icon: 'settings', + needsReview: false, + onSubmit: (data) => { + const path = location.pathname.includes(PATHS.FILE) + ? filePath + PATHS.SETTINGS + '?tab=project' + : PATHS.HOME + PATHS.SETTINGS + navigate(path) + }, + } + + return { RouteTelemetryCommand, RouteHomeCommand, RouteSettingsCommand } +} diff --git a/src/lib/constants.ts b/src/lib/constants.ts index ae0ec5ea9..049d5ef20 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -69,6 +69,8 @@ export const SETTINGS_FILE_NAME = 'settings.toml' export const TOKEN_FILE_NAME = 'token.txt' export const PROJECT_SETTINGS_FILE_NAME = 'project.toml' export const COOKIE_NAME = '__Secure-next-auth.session-token' +export const TELEMETRY_FILE_NAME = 'boot.txt' +export const TELEMETRY_RAW_FILE_NAME = 'raw-metrics.txt' /** localStorage key to determine if we're in Playwright tests */ export const PLAYWRIGHT_KEY = 'playwright' diff --git a/src/lib/desktop.ts b/src/lib/desktop.ts index 3e4c1e08d..035f2ca81 100644 --- a/src/lib/desktop.ts +++ b/src/lib/desktop.ts @@ -12,6 +12,8 @@ import { PROJECT_FOLDER, PROJECT_SETTINGS_FILE_NAME, SETTINGS_FILE_NAME, + TELEMETRY_FILE_NAME, + TELEMETRY_RAW_FILE_NAME, TOKEN_FILE_NAME, } from './constants' import { DeepPartial } from './types' @@ -419,6 +421,34 @@ const getTokenFilePath = async () => { return window.electron.path.join(fullPath, TOKEN_FILE_NAME) } +const getTelemetryFilePath = async () => { + const appConfig = await window.electron.getPath('appData') + const fullPath = window.electron.path.join(appConfig, getAppFolderName()) + try { + await window.electron.stat(fullPath) + } catch (e) { + // File/path doesn't exist + if (e === 'ENOENT') { + await window.electron.mkdir(fullPath, { recursive: true }) + } + } + return window.electron.path.join(fullPath, TELEMETRY_FILE_NAME) +} + +const getRawTelemetryFilePath = async () => { + const appConfig = await window.electron.getPath('appData') + const fullPath = window.electron.path.join(appConfig, getAppFolderName()) + try { + await window.electron.stat(fullPath) + } catch (e) { + // File/path doesn't exist + if (e === 'ENOENT') { + await window.electron.mkdir(fullPath, { recursive: true }) + } + } + return window.electron.path.join(fullPath, TELEMETRY_RAW_FILE_NAME) +} + const getProjectSettingsFilePath = async (projectPath: string) => { try { await window.electron.stat(projectPath) @@ -552,6 +582,18 @@ export const writeTokenFile = async (token: string) => { return window.electron.writeFile(tokenFilePath, token) } +export const writeTelemetryFile = async (content: string) => { + const telemetryFilePath = await getTelemetryFilePath() + if (err(content)) return Promise.reject(content) + return window.electron.writeFile(telemetryFilePath, content) +} + +export const writeRawTelemetryFile = async (content: string) => { + const rawTelemetryFilePath = await getRawTelemetryFilePath() + if (err(content)) return Promise.reject(content) + return window.electron.writeFile(rawTelemetryFilePath, content) +} + let appStateStore: Project | undefined = undefined export const getState = async (): Promise => { diff --git a/src/lib/paths.ts b/src/lib/paths.ts index 785acaceb..952b72fd6 100644 --- a/src/lib/paths.ts +++ b/src/lib/paths.ts @@ -42,6 +42,7 @@ export const PATHS = { SETTINGS_KEYBINDINGS: `${SETTINGS}?tab=keybindings` as const, SIGN_IN: '/signin', ONBOARDING: prependRoutes(onboardingPaths)('/onboarding') as OnboardingPaths, + TELEMETRY: '/telemetry', } as const export const BROWSER_PATH = `%2F${BROWSER_PROJECT_NAME}%2F${BROWSER_FILE_NAME}${FILE_EXT}` diff --git a/src/lib/performance.ts b/src/lib/performance.ts new file mode 100644 index 000000000..c9b359bf0 --- /dev/null +++ b/src/lib/performance.ts @@ -0,0 +1,127 @@ +import { isDesktop } from 'lib/isDesktop' + +function isWeb(): boolean { + // Identify browser environment when following property is not present + // https://nodejs.org/dist/latest-v16.x/docs/api/perf_hooks.html#performancenodetiming + return ( + typeof performance === 'object' && + typeof performance.mark === 'function' && + // @ts-ignore + !performance.nodeTiming + ) +} + +function isNode(): boolean { + // @ts-ignore + return typeof process === 'object' && performance.nodeTiming +} + +function getRuntime(): string { + if (isDesktop()) { + return 'electron' + } else if (isNode()) { + return 'nodejs' + } else if (isWeb()) { + return 'web' + } + return 'runtime unknown, could not detect' +} + +export interface PerformanceMarkDetail { + [key: string]: any +} + +export interface PerformanceMark { + name: string + startTime: number + entryType: string + detail: null | PerformanceMarkDetail + duration?: number +} + +export interface MarkHelpers { + mark(name: string, options?: PerformanceMark): void + markOnce(name: string, options?: PerformanceMark): void + getMarks(): PerformanceMark[] +} + +/** + * Detect performance API environment, either Web or Node.js + */ +function detectEnvironment(): MarkHelpers { + const seenMarks: { [key: string]: boolean } = {} + if (isWeb() || isNode() || isDesktop()) { + // in a browser context, reuse performance-util + // https://developer.mozilla.org/en-US/docs/Web/API/Performance + + function _mark(name: string, options?: PerformanceMark) { + const _options = { + ...options, + } + + // Automatically append detail data for a canonical form + if (!_options.detail) { + _options.detail = {} + } + _options.detail.runtime = getRuntime() + + performance.mark(name, _options) + } + + const _helpers: MarkHelpers = { + mark(name: string, options?: PerformanceMark) { + _mark(name, options) + }, + markOnce(name: string, options?: PerformanceMark) { + if (seenMarks[name]) { + return + } + _mark(name, options) + seenMarks[name] = true + }, + getMarks() { + let timeOrigin = performance.timeOrigin + const result: PerformanceMark[] = [ + { + name: 'code/timeOrigin', + startTime: Math.round(timeOrigin), + detail: { runtime: getRuntime() }, + entryType: 'mark', + }, + ] + for (const entry of performance.getEntriesByType('mark')) { + result.push({ + name: entry.name, + // Make everything unix time + startTime: Math.round(timeOrigin + entry.startTime), + // @ts-ignore - we can assume this is just any object with [key:string]: any + detail: entry.detail, + entryType: entry.entryType, + }) + } + return result + }, + } + return _helpers + } else { + // This would be browsers that do not support the performance API. + // TODO: Implement a polyfill + console.error('No performance API found globally. Going to be a bad time.') + return { + mark() { + /*no op*/ + }, + markOnce() { + /*no op*/ + }, + getMarks() { + return [] + }, + } + } +} + +const env = detectEnvironment() +export const mark = env.mark +export const getMarks = env.getMarks +export const markOnce = env.markOnce diff --git a/src/lib/routeLoaders.ts b/src/lib/routeLoaders.ts index e9f78ef73..268207c55 100644 --- a/src/lib/routeLoaders.ts +++ b/src/lib/routeLoaders.ts @@ -44,6 +44,12 @@ export const settingsLoader: LoaderFunction = async ({ return settings } +export const telemetryLoader: LoaderFunction = async ({ + params, +}): Promise => { + return null +} + // Redirect users to the appropriate onboarding page if they haven't completed it export const onboardingRedirectLoader: ActionFunction = async (args) => { const { settings } = await loadAndValidateSettings() diff --git a/src/lib/telemetry.test.ts b/src/lib/telemetry.test.ts new file mode 100644 index 000000000..9c1d6b45e --- /dev/null +++ b/src/lib/telemetry.test.ts @@ -0,0 +1,109 @@ +import { + columnWidth, + MaxWidth, + printHeader, + printDivider, + printRow, +} from 'lib/telemetry' + +describe('Telemetry', () => { + describe('columnWidth', () => { + it('should return 0', () => { + const actual = columnWidth([{ '': '' }], '') + const expected = 0 + expect(actual).toBe(expected) + }) + it('should return 10 from column length', () => { + const actual = columnWidth([{ thisisten_: 'dog' }], 'thisisten_') + const expected = 10 + expect(actual).toBe(expected) + }) + it('should return 5 from the key length', () => { + const actual = columnWidth([{ mph: 'five5' }], 'mph') + const expected = 5 + expect(actual).toBe(expected) + }) + it('should return 6 from multiple values', () => { + const actual = columnWidth( + [ + { mph: '555' }, + { mph: '33' }, + { mph: '789' }, + { mph: '1231' }, + { mph: '129532' }, + ], + 'mph' + ) + const expected = 6 + expect(actual).toBe(expected) + }) + }) + describe('printHeader', () => { + it('should print a header based on MaxWidth interface with value lengths', () => { + const widths: MaxWidth = { + metricA: 7, + metricB: 8, + metricC: 9, + metricD: 10, + } + const actual = printHeader(widths) + const expected = '| metricA | metricB | metricC | metricD |' + expect(actual).toBe(expected) + }) + it('should print a header based on MaxWidth interface with key lengths', () => { + const widths: MaxWidth = { + aa: 2, + bb: 2, + cc: 2, + dd: 2, + } + const actual = printHeader(widths) + const expected = '| aa | bb | cc | dd |' + expect(actual).toBe(expected) + }) + }) + describe('printDivider', () => { + it('should print a divider based on MaxWidth interface with value lengths', () => { + const widths: MaxWidth = { + metricA: 7, + metricB: 8, + metricC: 9, + metricD: 10, + } + const actual = printDivider(widths) + const expected = '| ------- | -------- | --------- | ---------- |' + expect(actual).toBe(expected) + }) + + it('should print a divider based on MaxWidth interface with key lengths', () => { + const widths: MaxWidth = { + aa: 2, + bb: 2, + cc: 2, + dd: 2, + } + const actual = printDivider(widths) + const expected = '| -- | -- | -- | -- |' + expect(actual).toBe(expected) + }) + }) + describe('printRow', () => { + it('should print a row', () => { + const widths: MaxWidth = { + metricA: 7, + metricB: 8, + metricC: 9, + metricD: 10, + } + const row = { + metricA: 'aa', + metricB: 'bb', + metricC: 'cc', + metricD: 'dd', + } + const actual = printRow(row, widths) + const expected = '| aa | bb | cc | dd |' + expect(actual).toBe(expected) + }) + }) +}) diff --git a/src/lib/telemetry.ts b/src/lib/telemetry.ts new file mode 100644 index 000000000..6258d8f89 --- /dev/null +++ b/src/lib/telemetry.ts @@ -0,0 +1,170 @@ +import { PerformanceMark, getMarks } from 'lib/performance' +import { writeTelemetryFile, writeRawTelemetryFile } from 'lib/desktop' +let args: any = null + +// Get the longest width of values or column name +export function columnWidth(arr: { [key: string]: any }, key: string): number { + let maxLength = key.length + + // for each value of that key, check if the length is longer + arr.forEach((value: any) => { + const valueAsString = String(value[key]) + maxLength = + valueAsString.length > maxLength ? valueAsString.length : maxLength + }) + return maxLength +} + +export function printHeader(columnWidths: MaxWidth): string { + const headers = ['|'] + const padLeft = ' ' + Object.keys(columnWidths).forEach((key) => { + const maxWidth = columnWidths[key] + const padLength = maxWidth - key.length + const paddingRight = ' '.repeat(padLength + 1) + headers.push(padLeft, key, paddingRight, '|') + }) + return headers.join('') +} + +export function printDivider(columnWidths: MaxWidth): string { + const headers = ['|'] + const padLeft = ' ' + Object.keys(columnWidths).forEach((key) => { + const keyMaxLength = columnWidths[key] + const dashedLines = '-'.repeat(keyMaxLength) + headers.push(padLeft, dashedLines, ' ', '|') + }) + return headers.join('') +} + +export function printRow( + row: { [key: string]: any }, + columnWidths: MaxWidth +): string { + const _row = ['|'] + const padLeft = ' ' + Object.keys(row).forEach((key) => { + const value = String(row[key]) + const valueLength = value && value.length ? value.length : 0 + const padLength = columnWidths[key] - valueLength + const paddingRight = ' '.repeat(padLength + 1) + _row.push(padLeft, value, paddingRight, '|') + }) + return _row.join('') +} + +export interface MaxWidth { + [key: string]: number +} + +export function printMarkDownTable( + marks: Array<{ [key: string]: any }> +): Array { + if (marks.length === 0) { + return [] + } + const sample = marks[0] + const columnWidths: MaxWidth = {} + Object.keys(sample).forEach((key) => { + const width = columnWidth(marks, key) + columnWidths[key] = width + }) + + const lines = [] + lines.push(printHeader(columnWidths)) + lines.push(printDivider(columnWidths)) + marks.forEach((row) => { + lines.push(printRow(row, columnWidths)) + }) + return lines +} + +export interface PerformanceDeltaTotal { + name: string + startTime: number + delta: string + total: string +} + +export function computeDeltaTotal( + marks: Array +): Array { + let startTime = -1 + let total = 0 + const deltaTotalArray: Array = marks.map( + (row: PerformanceMark) => { + const delta = + startTime === -1 ? 0 : Number(row.startTime) - Number(startTime) + startTime = row.startTime + total += delta + const formatted: PerformanceDeltaTotal = { + name: row.name, + startTime: row.startTime, + delta: delta.toFixed(2), + total: total.toFixed(2), + } + return formatted + } + ) + return deltaTotalArray +} + +export function printDeltaTotal(marks: Array): string[] { + const deltaTotalArray = computeDeltaTotal(marks) + return printMarkDownTable(deltaTotalArray) +} + +export function printRawRow(row: { [key: string]: any }): string { + const _row = [''] + Object.keys(row).forEach((key) => { + const value = String(row[key]) + _row.push(value, ' ') + }) + return _row.join('') +} + +export function printRawMarks(marks: Array): string[] { + const headers = ['Name', 'Timestamp', 'Delta', 'Total', 'Detail'] + const lines = ['```', headers.join(' ')] + const deltaTotalArray = computeDeltaTotal(marks) + deltaTotalArray.forEach((row) => { + lines.push(printRawRow(row)) + }) + lines.push('```') + return lines +} + +export function printInvocationCount(marks: Array): string[] { + const counts: { [key: string]: number } = {} + marks.forEach((mark: PerformanceMark) => { + counts[mark.name] = + counts[mark.name] === undefined ? 1 : counts[mark.name] + 1 + }) + + const formattedCounts = Object.entries(counts).map((entry) => { + return { + name: entry[0], + count: entry[1], + } + }) + return printMarkDownTable(formattedCounts) +} + +export async function maybeWriteToDisk() { + if (!args) { + args = await window.electron.getArgvParsed() + } + if (args.telemetry) { + setInterval(() => { + const marks = getMarks() + const deltaTotalTable = printDeltaTotal(marks) + writeTelemetryFile(deltaTotalTable.join('\n')) + .then(() => {}) + .catch(() => {}) + writeRawTelemetryFile(JSON.stringify(marks)) + .then(() => {}) + .catch(() => {}) + }, 5000) + } +} diff --git a/src/machines/authMachine.ts b/src/machines/authMachine.ts index a55cd6d46..5a7b4a0e9 100644 --- a/src/machines/authMachine.ts +++ b/src/machines/authMachine.ts @@ -14,6 +14,7 @@ import { writeTokenFile, } from 'lib/desktop' import { COOKIE_NAME } from 'lib/constants' +import { markOnce } from 'lib/performance' const SKIP_AUTH = VITE_KC_SKIP_AUTH === 'true' && DEV @@ -156,6 +157,7 @@ async function getUser(input: { token?: string }) { LOCAL_USER.image = '' } + markOnce('code/didAuth') return { user: LOCAL_USER, token, @@ -181,6 +183,7 @@ async function getUser(input: { token?: string }) { if ('error_code' in user) return Promise.reject(new Error(user.message)) + markOnce('code/didAuth') return { user: user as Models['User_type'], token, diff --git a/src/main.ts b/src/main.ts index 93400b1ce..ec1a899e4 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,6 +1,5 @@ // Some of the following was taken from bits and pieces of the vite-typescript // template that ElectronJS provides. - import dotenv from 'dotenv' import { app, @@ -20,6 +19,7 @@ import minimist from 'minimist' import getCurrentProjectFile from 'lib/getCurrentProjectFile' import os from 'node:os' import { reportRejection } from 'lib/trap' +import argvFromYargs from './commandLineArgs' let mainWindow: BrowserWindow | null = null @@ -158,6 +158,10 @@ ipcMain.handle('shell.openExternal', (event, data) => { return shell.openExternal(data) }) +ipcMain.handle('argv.parser', (event, data) => { + return argvFromYargs +}) + ipcMain.handle('startDeviceFlow', async (_, host: string) => { // Do an OAuth 2.0 Device Authorization Grant dance to get a token. // We quiet ts because we are not using this in the standard way. diff --git a/src/preload.ts b/src/preload.ts index 5a3c6962e..ff6427cea 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -117,6 +117,10 @@ const listMachines = async ( const getMachineApiIp = async (): Promise => ipcRenderer.invoke('find_machine_api') +const getArgvParsed = () => { + return ipcRenderer.invoke('argv.parser') +} + contextBridge.exposeInMainWorld('electron', { startDeviceFlow, loginWithDeviceFlow, @@ -184,4 +188,5 @@ contextBridge.exposeInMainWorld('electron', { onUpdateDownloaded, onUpdateError, appRestart, + getArgvParsed, }) diff --git a/src/routes/Home.tsx b/src/routes/Home.tsx index d1a335089..3c4839637 100644 --- a/src/routes/Home.tsx +++ b/src/routes/Home.tsx @@ -20,6 +20,7 @@ import { useRefreshSettings } from 'hooks/useRefreshSettings' import { LowerRightControls } from 'components/LowerRightControls' import { ProjectSearchBar, useProjectSearch } from 'components/ProjectSearchBar' import { Project } from 'lib/project' +import { markOnce } from 'lib/performance' import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher' import { useProjectsLoader } from 'hooks/useProjectsLoader' import { useProjectsContext } from 'hooks/useProjectsContext' @@ -41,6 +42,7 @@ const Home = () => { // Cancel all KCL executions while on the home page useEffect(() => { + markOnce('code/didLoadHome') kclManager.cancelAllExecutions() }, []) diff --git a/src/routes/Telemetry.tsx b/src/routes/Telemetry.tsx new file mode 100644 index 000000000..a5cd91b0d --- /dev/null +++ b/src/routes/Telemetry.tsx @@ -0,0 +1,72 @@ +import { useLocation, useNavigate } from 'react-router-dom' +import { useHotkeys } from 'react-hotkeys-hook' +import { PATHS } from 'lib/paths' +import { useDotDotSlash } from 'hooks/useDotDotSlash' +import { Fragment } from 'react' +import { Dialog, Transition } from '@headlessui/react' +import { CustomIcon } from 'components/CustomIcon' +import { TelemetryExplorer } from 'components/TelemetryExplorer' + +export const Telemetry = () => { + const navigate = useNavigate() + const close = () => navigate(location.pathname.replace(PATHS.TELEMETRY, '')) + const location = useLocation() + const dotDotSlash = useDotDotSlash() + useHotkeys('esc', () => navigate(dotDotSlash())) + return ( + + + + + + + + +
+

Telemetry

+
+ +
+
+
+ +
+
+
+
+
+ ) +} diff --git a/yarn.lock b/yarn.lock index 2b1d721ec..56965dc6c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9954,7 +9954,7 @@ yargs@^16.0.2: y18n "^5.0.5" yargs-parser "^20.2.2" -yargs@^17.0.1, yargs@^17.6.2: +yargs@^17.0.1, yargs@^17.6.2, yargs@^17.7.2: version "17.7.2" resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==