Nadro/3799/perf (#4145)
* chore: building out perf testing * chore: adding my printing code for the different formats of the marks * feat: adding invocation count table * fix: markOnce iunstead * fix: typescript additions * fix: adding more types * chore: adding telemetry panel as MVP, gonna remove the pane * chore: view telemetry from command bar in file route and home route * fix: deleting unused imports * fix: deleting some unused files * fix: auto cleanup * chore: adding other routes, these will need to be moved... * chore: moving some printing logic around and unit testing some of it * fix: moving command init * fix: removing debugging marks * fix: adding some comments * fix: fixed a bug with generating the go to page commands * chore: adding will pages load within the router config * chore: implementing marks for routes * fix: auto fixes and checkers * chore: implemented a route watcher at the root level... * fix: auto fixes, removing unused code * chore: timing for syntax highlighting and auto fixes * fix: didAuth issue and syntax highlighting in the packaged application. Constructor name gets renamed * fix: fixing typescript checks * chore: adding mag bar chart icon for telemetry * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest) * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest) * A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest) * chore: swapped telemetry icon for stopwatch * A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest) * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest) * A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest) * A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest) * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest) * chore: writing telemetry to disk * fix: auto fixers * chore: getting args parsed for cli flags and writing telemetry file * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest) * chore: swapped mark for markOnce since we infinitely write marks to a JS array... need to solve this run time marking in another way. We only need this for startup right now * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest) * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest) * A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest) * A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest) * chore: writing raw marks to disk as well * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest) * A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest) * A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest) * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest) * A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest) * fix: cleaned up the testing names * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest) * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest) * Fix fmt and codespell * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest-8-cores) * A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores) * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest-8-cores) * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest-8-cores) * A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores) * A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores) * A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores) * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest-8-cores) * fix: moving this route loader data stuff * chore: adding comment * fix: fmt * A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores) * empty :( * A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores) * empty :( --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: 49fl <ircsurfer33@gmail.com>
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB |
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 51 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 53 KiB |
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 74 KiB |
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 65 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
1
interface.d.ts
vendored
@ -78,6 +78,7 @@ export interface IElectronAPI {
|
||||
) => Electron.IpcRenderer
|
||||
onUpdateError: (callback: (value: { error: Error }) => void) => Electron
|
||||
appRestart: () => void
|
||||
getArgvParsed: () => any
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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: (
|
||||
<CommandBarProvider>
|
||||
<SettingsAuthProvider>
|
||||
<LspProvider>
|
||||
<ProjectsContextProvider>
|
||||
<KclContextProvider>
|
||||
<AppStateProvider>
|
||||
<MachineManagerProvider>
|
||||
<Outlet />
|
||||
</MachineManagerProvider>
|
||||
</AppStateProvider>
|
||||
</KclContextProvider>
|
||||
</ProjectsContextProvider>
|
||||
</LspProvider>
|
||||
</SettingsAuthProvider>
|
||||
<RouteProvider>
|
||||
<SettingsAuthProvider>
|
||||
<LspProvider>
|
||||
<ProjectsContextProvider>
|
||||
<KclContextProvider>
|
||||
<AppStateProvider>
|
||||
<MachineManagerProvider>
|
||||
<Outlet />
|
||||
</MachineManagerProvider>
|
||||
</AppStateProvider>
|
||||
</KclContextProvider>
|
||||
</ProjectsContextProvider>
|
||||
</LspProvider>
|
||||
</SettingsAuthProvider>
|
||||
</RouteProvider>
|
||||
</CommandBarProvider>
|
||||
),
|
||||
errorElement: <ErrorPage />,
|
||||
@ -124,6 +129,16 @@ const router = createRouter([
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: PATHS.FILE + 'TELEMETRY',
|
||||
loader: telemetryLoader,
|
||||
children: [
|
||||
{
|
||||
path: makeUrlPathRelative(PATHS.TELEMETRY),
|
||||
element: <Telemetry />,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@ -149,6 +164,11 @@ const router = createRouter([
|
||||
loader: settingsLoader,
|
||||
element: <Settings />,
|
||||
},
|
||||
{
|
||||
path: makeUrlPathRelative(PATHS.TELEMETRY),
|
||||
loader: telemetryLoader,
|
||||
element: <Telemetry />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
12
src/commandLineArgs.ts
Normal file
@ -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
|
@ -1161,6 +1161,29 @@ const CustomIconMap = {
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
stopwatch: (
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M7.95705 5.99046C7.05643 6.44935 6.33654 7.19809 5.91336 8.11602C5.49019 9.03396 5.38838 10.0676 5.62434 11.0505C5.8603 12.0334 6.42029 12.9081 7.21408 13.5339C8.00787 14.1597 8.98922 14.5 10 14.5C11.0108 14.5 11.9921 14.1597 12.7859 13.5339C13.5797 12.9082 14.1397 12.0334 14.3757 11.0505C14.6116 10.0676 14.5098 9.03396 14.0866 8.11603C13.6635 7.19809 12.9436 6.44935 12.043 5.99046L12.497 5.09946C13.5977 5.66032 14.4776 6.57544 14.9948 7.69737C15.512 8.81929 15.6364 10.0827 15.348 11.2839C15.0596 12.4852 14.3752 13.5544 13.405 14.3192C12.4348 15.0841 11.2354 15.5 10 15.5C8.7646 15.5 7.56517 15.0841 6.59499 14.3192C5.6248 13.5544 4.94037 12.4852 4.65197 11.2839C4.36357 10.0827 4.488 8.81929 5.00522 7.69736C5.52243 6.57544 6.40231 5.66032 7.50306 5.09946L7.95705 5.99046Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path d="M10 5.5V4M10 4H8M10 4H12" stroke="currentColor" />
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M12.8536 7.85356L10.3536 10.3536C10.1583 10.5488 9.84171 10.5488 9.64645 10.3536C9.45118 10.1583 9.45118 9.84172 9.64645 9.64645L12.1464 7.14645L12.8536 7.85356Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
} as const
|
||||
|
||||
export type CustomIconName = keyof typeof CustomIconMap
|
||||
|
@ -29,6 +29,7 @@ import {
|
||||
KclSamplesManifestItem,
|
||||
} from 'lib/getKclSamplesManifest'
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import { markOnce } from 'lib/performance'
|
||||
|
||||
type MachineContext<T extends AnyStateMachine> = {
|
||||
state: StateFrom<T>
|
||||
@ -54,6 +55,7 @@ export const FileMachineProvider = ({
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
markOnce('code/didLoadFile')
|
||||
async function fetchKclSamples() {
|
||||
setKclSamples(await getKclSamplesManifest())
|
||||
}
|
||||
|
@ -96,6 +96,23 @@ export function LowerRightControls({
|
||||
Report a bug
|
||||
</Tooltip>
|
||||
</a>
|
||||
<Link
|
||||
to={
|
||||
location.pathname.includes(PATHS.FILE)
|
||||
? filePath + PATHS.TELEMETRY + '?tab=project'
|
||||
: PATHS.HOME + PATHS.TELEMETRY
|
||||
}
|
||||
data-testid="telemetry-link"
|
||||
>
|
||||
<CustomIcon
|
||||
name="stopwatch"
|
||||
className={`w-5 h-5 ${linkOverrideClassName}`}
|
||||
/>
|
||||
<span className="sr-only">Telemetry</span>
|
||||
<Tooltip position="top" contentClassName="text-xs">
|
||||
Telemetry
|
||||
</Tooltip>
|
||||
</Link>
|
||||
<Link
|
||||
to={
|
||||
location.pathname.includes(PATHS.FILE)
|
||||
|
@ -129,7 +129,9 @@ export const KclEditorPane = () => {
|
||||
closeBrackets(),
|
||||
highlightActiveLine(),
|
||||
highlightSelectionMatches(),
|
||||
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
|
||||
syntaxHighlighting(defaultHighlightStyle, {
|
||||
fallback: true,
|
||||
}),
|
||||
rectangularSelection(),
|
||||
dropCursor(),
|
||||
interact({
|
||||
|
33
src/components/RouteProvider.tsx
Normal file
@ -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 (
|
||||
<RouteProviderContext.Provider value={{}}>
|
||||
{children}
|
||||
</RouteProviderContext.Provider>
|
||||
)
|
||||
}
|
@ -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<T extends AnyStateMachine> = {
|
||||
state: StateFrom<T>
|
||||
@ -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)
|
||||
|
72
src/components/TelemetryExplorer.tsx
Normal file
@ -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 (
|
||||
<div>
|
||||
<h1 className="pb-4">Marks</h1>
|
||||
<div className="max-w-xl max-h-64 overflow-auto select-all">
|
||||
{marks.map((mark, index) => {
|
||||
return (
|
||||
<pre className="text-xs" key={index}>
|
||||
<code key={index}>{JSON.stringify(mark, null, 2)}</code>
|
||||
</pre>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<h1 className="pb-4">Startup Performance</h1>
|
||||
<div className="max-w-xl max-h-64 overflow-auto select-all">
|
||||
{markdownTable.map((line, index) => {
|
||||
return (
|
||||
<pre className="text-xs" key={index}>
|
||||
<code key={index}>{line}</code>
|
||||
</pre>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<h1 className="pb-4">Delta and Totals</h1>
|
||||
<div className="max-w-xl max-h-64 overflow-auto select-all">
|
||||
{deltaTotalTable.map((line, index) => {
|
||||
return (
|
||||
<pre className="text-xs" key={index}>
|
||||
<code key={index}>{line}</code>
|
||||
</pre>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<h1 className="pb-4">Raw Marks</h1>
|
||||
<div className="max-w-xl max-h-64 overflow-auto select-all">
|
||||
{rawMarks.map((line, index) => {
|
||||
return (
|
||||
<pre className="text-xs" key={index}>
|
||||
<code key={index}>{line}</code>
|
||||
</pre>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<h1 className="pb-4">Invocation Count</h1>
|
||||
<div className="max-w-xl max-h-64 overflow-auto select-all">
|
||||
{invocationCount.map((line, index) => {
|
||||
return (
|
||||
<pre className="text-xs" key={index}>
|
||||
<code key={index}>{line}</code>
|
||||
</pre>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -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<boolean>()
|
||||
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 {
|
||||
|
@ -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'
|
||||
|
@ -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.
|
||||
|
@ -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',
|
||||
|
52
src/lib/commandBarConfigs/routeCommandConfig.ts
Normal file
@ -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 }
|
||||
}
|
@ -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'
|
||||
|
@ -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<Project | undefined> => {
|
||||
|
@ -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}`
|
||||
|
||||
|
127
src/lib/performance.ts
Normal file
@ -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
|
@ -44,6 +44,12 @@ export const settingsLoader: LoaderFunction = async ({
|
||||
return settings
|
||||
}
|
||||
|
||||
export const telemetryLoader: LoaderFunction = async ({
|
||||
params,
|
||||
}): Promise<null> => {
|
||||
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()
|
||||
|
109
src/lib/telemetry.test.ts
Normal file
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
170
src/lib/telemetry.ts
Normal file
@ -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<string> {
|
||||
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<PerformanceMark>
|
||||
): Array<PerformanceDeltaTotal> {
|
||||
let startTime = -1
|
||||
let total = 0
|
||||
const deltaTotalArray: Array<PerformanceDeltaTotal> = 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<PerformanceMark>): 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<PerformanceMark>): 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<PerformanceMark>): 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)
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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.
|
||||
|
@ -117,6 +117,10 @@ const listMachines = async (
|
||||
const getMachineApiIp = async (): Promise<String | null> =>
|
||||
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,
|
||||
})
|
||||
|
@ -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()
|
||||
}, [])
|
||||
|
||||
|
72
src/routes/Telemetry.tsx
Normal file
@ -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 (
|
||||
<Transition appear show={true} as={Fragment}>
|
||||
<Dialog
|
||||
as="div"
|
||||
open={true}
|
||||
onClose={close}
|
||||
className="fixed inset-0 z-40 overflow-y-auto p-4 grid place-items-center"
|
||||
>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-75"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Dialog.Overlay className="fixed inset-0 bg-chalkboard-110/30 dark:bg-chalkboard-110/50" />
|
||||
</Transition.Child>
|
||||
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-75"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Dialog.Panel className="rounded relative mx-auto bg-chalkboard-10 dark:bg-chalkboard-100 border dark:border-chalkboard-70 max-w-3xl w-full max-h-[66vh] shadow-lg flex flex-col gap-8">
|
||||
<div className="p-5 pb-0 flex justify-between items-center">
|
||||
<h1 className="text-2xl font-bold">Telemetry</h1>
|
||||
<div className="flex gap-4 items-start">
|
||||
<button
|
||||
onClick={close}
|
||||
className="p-0 m-0 focus:ring-0 focus:outline-none border-none hover:bg-destroy-10 focus:bg-destroy-10 dark:hover:bg-destroy-80/50 dark:focus:bg-destroy-80/50"
|
||||
data-testid="settings-close-button"
|
||||
>
|
||||
<CustomIcon name="close" className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="flex-1 grid items-stretch pl-4 pr-5 pb-5 gap-2 overflow-scroll"
|
||||
style={{
|
||||
gridTemplateColumns: 'auto 1fr',
|
||||
gridTemplateRows: '1fr',
|
||||
}}
|
||||
>
|
||||
<TelemetryExplorer />
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
)
|
||||
}
|
@ -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==
|
||||
|