Compare commits
5 Commits
v0.26.3
...
derive-doc
Author | SHA1 | Date | |
---|---|---|---|
9e1cf90c81 | |||
062fae1e54 | |||
d7660e221c | |||
938e27adac | |||
17b9af2416 |
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: 49 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 65 KiB |
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 37 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: 38 KiB After Width: | Height: | Size: 38 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: 42 KiB After Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 37 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",
|
||||
|
@ -22,6 +22,10 @@ import Gizmo from 'components/Gizmo'
|
||||
import { CoreDumpManager } from 'lib/coredump'
|
||||
import { UnitsMenu } from 'components/UnitsMenu'
|
||||
import { CameraProjectionToggle } from 'components/CameraProjectionToggle'
|
||||
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)
|
||||
|
@ -4,7 +4,7 @@ import { Themes, getSystemTheme } from 'lib/theme'
|
||||
import { useMemo, useRef } from 'react'
|
||||
import { highlightSelectionMatches, searchKeymap } from '@codemirror/search'
|
||||
import { lineHighlightField } from 'editor/highlightextension'
|
||||
import { roundOff } from 'lib/utils'
|
||||
import { onMouseDragMakeANewNumber, onMouseDragRegex } from 'lib/utils'
|
||||
import {
|
||||
lineNumbers,
|
||||
rectangularSelection,
|
||||
@ -129,7 +129,9 @@ export const KclEditorPane = () => {
|
||||
closeBrackets(),
|
||||
highlightActiveLine(),
|
||||
highlightSelectionMatches(),
|
||||
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
|
||||
syntaxHighlighting(defaultHighlightStyle, {
|
||||
fallback: true,
|
||||
}),
|
||||
rectangularSelection(),
|
||||
dropCursor(),
|
||||
interact({
|
||||
@ -137,29 +139,12 @@ export const KclEditorPane = () => {
|
||||
// a rule for a number dragger
|
||||
{
|
||||
// the regexp matching the value
|
||||
regexp: /-?\b\d+\.?\d*\b/g,
|
||||
regexp: onMouseDragRegex,
|
||||
// set cursor to "ew-resize" on hover
|
||||
cursor: 'ew-resize',
|
||||
// change number value based on mouse X movement on drag
|
||||
onDrag: (text, setText, e) => {
|
||||
const multiplier =
|
||||
e.shiftKey && e.metaKey
|
||||
? 0.01
|
||||
: e.metaKey
|
||||
? 0.1
|
||||
: e.shiftKey
|
||||
? 10
|
||||
: 1
|
||||
|
||||
const delta = e.movementX * multiplier
|
||||
|
||||
const newVal = roundOff(
|
||||
Number(text) + delta,
|
||||
multiplier === 0.01 ? 2 : multiplier === 0.1 ? 1 : 0
|
||||
)
|
||||
|
||||
if (isNaN(newVal)) return
|
||||
setText(newVal.toString())
|
||||
onMouseDragMakeANewNumber(text, setText, e)
|
||||
},
|
||||
},
|
||||
],
|
||||
|
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)
|
||||
@ -331,6 +333,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
|
||||
@ -785,6 +787,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)
|
||||
}
|
||||
}
|
129
src/lib/utils.ts
@ -53,11 +53,6 @@ export function isOverlap(a: SourceRange, b: SourceRange) {
|
||||
return lastOfFirst >= firstOfSecond
|
||||
}
|
||||
|
||||
export function roundOff(num: number, places: number = 2): number {
|
||||
const x = Math.pow(10, places)
|
||||
return Math.round(num * x) / x
|
||||
}
|
||||
|
||||
export function getLength(a: [number, number], b: [number, number]): number {
|
||||
const x = b[0] - a[0]
|
||||
const y = b[1] - a[1]
|
||||
@ -269,3 +264,127 @@ export function XOR(bool1: boolean, bool2: boolean): boolean {
|
||||
export function getActorNextEvents(snapshot: AnyMachineSnapshot) {
|
||||
return [...new Set([...snapshot._nodes.flatMap((sn) => sn.ownEvents)])]
|
||||
}
|
||||
|
||||
export const onMouseDragRegex = /-?\.?\b\d+\.?\d*\b/g
|
||||
|
||||
export function simulateOnMouseDragMatch(text: string) {
|
||||
return text.match(onMouseDragRegex)
|
||||
}
|
||||
|
||||
export function roundOff(num: number, precision: number = 2): number {
|
||||
const x = Math.pow(10, precision)
|
||||
return Math.round(num * x) / x
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the number as a string has any precision in the decimal places
|
||||
* '1' -> 0
|
||||
* '1.0' -> 1
|
||||
* '1.01' -> 2
|
||||
*/
|
||||
function getPrecision(text: string): number {
|
||||
const wholeFractionSplit = text.split('.')
|
||||
const precision =
|
||||
wholeFractionSplit.length === 2 ? wholeFractionSplit[1].split('').length : 0
|
||||
return precision
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a number string has a leading digit
|
||||
* 0.1 -> yes
|
||||
* -0.1 -> yes
|
||||
* .1 -> no
|
||||
* 10.1 -> no
|
||||
* The text.split('.') should evaluate to ['','<decimals>']
|
||||
*/
|
||||
export function hasLeadingZero(text: string): boolean {
|
||||
const wholeFractionSplit = text.split('.')
|
||||
return wholeFractionSplit.length === 2
|
||||
? wholeFractionSplit[0] === '0' || wholeFractionSplit[0] === '-0'
|
||||
: false
|
||||
}
|
||||
|
||||
export function hasDigitsLeftOfDecimal(text: string): boolean | undefined {
|
||||
const wholeFractionSplit = text.split('.')
|
||||
|
||||
if (wholeFractionSplit.length === 2) {
|
||||
const wholeNumber = wholeFractionSplit[0]
|
||||
|
||||
if (wholeNumber.length === 0) {
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if (wholeFractionSplit.length === 1) {
|
||||
return true
|
||||
}
|
||||
|
||||
// What if someone passes in 1..2.3.1...1.1.43
|
||||
return undefined
|
||||
}
|
||||
|
||||
export function onDragNumberCalculation(text: string, e: MouseEvent) {
|
||||
const multiplier =
|
||||
e.shiftKey && e.metaKey ? 0.01 : e.metaKey ? 0.1 : e.shiftKey ? 10 : 1
|
||||
|
||||
const delta = e.movementX * multiplier
|
||||
const hasPeriod = text.includes('.')
|
||||
const leadsWithZero = hasLeadingZero(text)
|
||||
const addition = Number(text) + delta
|
||||
const positiveAddition = e.movementX > 0
|
||||
const negativeAddition = e.movementX < 0
|
||||
const containsDigitsLeftOfDecimal = hasDigitsLeftOfDecimal(text)
|
||||
let precision = Math.max(
|
||||
getPrecision(text),
|
||||
getPrecision(multiplier.toString())
|
||||
)
|
||||
const newVal = roundOff(addition, precision)
|
||||
|
||||
if (isNaN(newVal)) {
|
||||
return
|
||||
}
|
||||
|
||||
let formattedString = newVal.toString()
|
||||
if (hasPeriod && !formattedString.includes('.')) {
|
||||
// If the original number included a period lets add that back to the output string
|
||||
// e.g. '1.0' add +1 then we get 2, we want to send '2.0' back since the original one had a decimal place
|
||||
formattedString = formattedString.toString() + '.0'
|
||||
}
|
||||
|
||||
/**
|
||||
* Whenever you add two numbers you can always remove the the leading zero the result will make sense
|
||||
* 1 + -0.01 = 0.99, the code would remove the leading 0 to make it .99 but since the number has a
|
||||
* digit left of the decimal to begin with I want to make it 0.99.
|
||||
* negativeAddition with fractional numbers will provide a leading 0.
|
||||
*/
|
||||
const removeZeros =
|
||||
positiveAddition ||
|
||||
(negativeAddition && multiplier < 1 && !containsDigitsLeftOfDecimal)
|
||||
|
||||
/**
|
||||
* If the original value has no leading 0
|
||||
* If if the new updated value has a leading zero
|
||||
* If the math operation means you can actually remove the zero.
|
||||
*/
|
||||
if (!leadsWithZero && hasLeadingZero(formattedString) && removeZeros) {
|
||||
if (formattedString[0] === '-') {
|
||||
return ['-', formattedString.split('.')[1]].join('.')
|
||||
} else {
|
||||
return formattedString.substring(1)
|
||||
}
|
||||
}
|
||||
|
||||
return formattedString
|
||||
}
|
||||
|
||||
export function onMouseDragMakeANewNumber(
|
||||
text: string,
|
||||
setText: (t: string) => void,
|
||||
e: MouseEvent
|
||||
) {
|
||||
const newVal = onDragNumberCalculation(text, e)
|
||||
if (!newVal) return
|
||||
setText(newVal)
|
||||
}
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
10
src/wasm-lib/Cargo.lock
generated
@ -737,7 +737,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "derive-docs"
|
||||
version = "0.1.29"
|
||||
version = "0.1.30"
|
||||
dependencies = [
|
||||
"Inflector",
|
||||
"anyhow",
|
||||
@ -1524,9 +1524,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "image"
|
||||
version = "0.25.3"
|
||||
version = "0.25.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d97eb9a8e0cd5b76afea91d7eecd5cf8338cd44ced04256cf1f800474b227c52"
|
||||
checksum = "cd6f44aed642f18953a158afeb30206f4d50da59fbc66ecb53c66488de73563b"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"byteorder-lite",
|
||||
@ -1673,7 +1673,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kcl-lib"
|
||||
version = "0.2.23"
|
||||
version = "0.2.24"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"approx 0.5.1",
|
||||
@ -1748,7 +1748,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kcl-test-server"
|
||||
version = "0.1.15"
|
||||
version = "0.1.16"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"hyper 0.14.30",
|
||||
|
@ -24,7 +24,7 @@ wasm-bindgen-futures = "0.4.44"
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = "1"
|
||||
image = { version = "0.25.3", default-features = false, features = ["png"] }
|
||||
image = { version = "0.25.5", default-features = false, features = ["png"] }
|
||||
kittycad = { workspace = true, default-features = true }
|
||||
kittycad-modeling-cmds = { workspace = true }
|
||||
pretty_assertions = "1.4.1"
|
||||
|
@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "derive-docs"
|
||||
description = "A tool for generating documentation from Rust derive macros"
|
||||
version = "0.1.29"
|
||||
version = "0.1.30"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/KittyCAD/modeling-app"
|
||||
|
@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "kcl-test-server"
|
||||
description = "A test server for KCL"
|
||||
version = "0.1.15"
|
||||
version = "0.1.16"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "kcl-lib"
|
||||
description = "KittyCAD Language implementation and tools"
|
||||
version = "0.2.23"
|
||||
version = "0.2.24"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/KittyCAD/modeling-app"
|
||||
@ -26,7 +26,7 @@ futures = { version = "0.3.31" }
|
||||
git_rev = "0.1.0"
|
||||
gltf-json = "1.4.1"
|
||||
http = { workspace = true }
|
||||
image = { version = "0.25.3", default-features = false, features = ["png"] }
|
||||
image = { version = "0.25.5", default-features = false, features = ["png"] }
|
||||
indexmap = { version = "2.6.0", features = ["serde"] }
|
||||
kittycad = { workspace = true }
|
||||
kittycad-modeling-cmds = { workspace = true }
|
||||
@ -85,7 +85,7 @@ criterion = { version = "0.5.1", features = ["async_tokio"] }
|
||||
expectorate = "1.1.0"
|
||||
handlebars = "6.2.0"
|
||||
iai = "0.1"
|
||||
image = { version = "0.25.3", default-features = false, features = ["png"] }
|
||||
image = { version = "0.25.5", default-features = false, features = ["png"] }
|
||||
insta = { version = "1.41.1", features = ["json", "filters"] }
|
||||
itertools = "0.13.0"
|
||||
pretty_assertions = "1.4.1"
|
||||
|
@ -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==
|
||||
|