Skip to content

Commit

Permalink
feat(parser): parse cache (#6381)
Browse files Browse the repository at this point in the history
This PR introduces `IndexedDB` cache for parse results, keyed by file
name and string contents (so every change in the file contents
invalidates the parse cache). This greatly improves the >1st time load
on every project.
<video
src="https://github.com/user-attachments/assets/faa3560b-81ca-4f1d-9d92-52526bf7f004"></video>

Important points:
1. The cache is implemented in the worker level - to keep our current
parse flow as identical to now as possible. If the feature flag is on,
the worker tries to look for the file in the cache, compares the content
- and if there is a cache hit it returns it from the cache instead of
parsing it.
2. When a parsing does happen - if the feature flag is on, the parsed
results are stored in the cache.
3. The cache is not project specific - allowing for cached results to be
shared between projects (if the file name and contents are similar)
4. Currently arbitrary code (chunks of code we send as `code.tsx`) is
not being cached, this can be controlled using a feature flag.
5. This PR also adds a settings pane for controlling cache behavior. The
pane allows controlling the cache, the cache log and manually clear the
cache if necessary.
The cache settings toggle (with sound 🔉) :
<video
src="https://github.com/user-attachments/assets/a3c901c6-1b87-4013-a9ab-a2f531fb4457"></video>

**Commit Details:**
- The main logic changes are in `parser-printer-worker.ts` and
`parse-cache-utils.worker.ts`.
- The worker now gets a `parsingCacheOptions` argument, which controls
whether or not to use the cache (and also logging)

**Manual Tests:**
I hereby swear that:

- [X] I opened a hydrogen project and it loaded
- [X] I could navigate to various routes in Preview mode

Fixes #5659
  • Loading branch information
liady authored Oct 4, 2024
1 parent 25f465f commit 328b3ed
Show file tree
Hide file tree
Showing 15 changed files with 515 additions and 108 deletions.
41 changes: 26 additions & 15 deletions editor/src/components/editor/actions/actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,12 @@ import {
import * as PP from '../../../core/shared/property-path'
import { assertNever, fastForEach, getProjectLockedKey, identity } from '../../../core/shared/utils'
import { emptyImports, mergeImports } from '../../../core/workers/common/project-file-utils'
import type { UtopiaTsWorkers } from '../../../core/workers/common/worker-types'
import {
createParseAndPrintOptions,
createParseFile,
createPrintAndReparseFile,
type UtopiaTsWorkers,
} from '../../../core/workers/common/worker-types'
import type { IndexPosition } from '../../../utils/utils'
import Utils from '../../../utils/utils'
import type { ProjectContentTreeRoot } from '../../assets'
Expand Down Expand Up @@ -622,6 +627,7 @@ import { getDefaultedRemixRootDir } from '../store/remix-derived-data'
import { isReplaceKeepChildrenAndStyleTarget } from '../../navigator/navigator-item/component-picker-context-menu'
import { canCondenseJSXElementChild } from '../../../utils/can-condense'
import { getNavigatorTargetsFromEditorState } from '../../navigator/navigator-utils'
import { getParseCacheOptions } from '../../../core/shared/parse-cache-utils'
import { applyValuesAtPath } from '../../canvas/commands/adjust-number-command'
import { styleP } from '../../inspector/inspector-common'

Expand Down Expand Up @@ -4107,13 +4113,13 @@ export const UPDATE_FNS = {
getAllUniqueUidsFromMapping(getUidMappings(editor.projectContents).filePathToUids),
)
const parsedResult = getParseFileResult(
newFileName,
getFilePathMappings(editor.projectContents),
templateFile.fileContents.code,
null,
1,
existingUIDs,
isSteganographyEnabled(),
createParseFile(newFileName, templateFile.fileContents.code, null, 1),
createParseAndPrintOptions(
getFilePathMappings(editor.projectContents),
existingUIDs,
isSteganographyEnabled(),
getParseCacheOptions(),
),
)

// 3. write the new text file
Expand Down Expand Up @@ -4974,13 +4980,18 @@ export const UPDATE_FNS = {
const workerUpdates = filesToUpdateResult.filesToUpdate.flatMap((fileToUpdate) => {
if (fileToUpdate.type === 'printandreparsefile') {
const printParsedContent = getPrintAndReparseCodeResult(
fileToUpdate.filename,
filePathMappings,
fileToUpdate.parseSuccess,
fileToUpdate.stripUIDs,
fileToUpdate.versionNumber,
filesToUpdateResult.existingUIDs,
isSteganographyEnabled(),
createPrintAndReparseFile(
fileToUpdate.filename,
fileToUpdate.parseSuccess,
fileToUpdate.stripUIDs,
fileToUpdate.versionNumber,
),
createParseAndPrintOptions(
filePathMappings,
filesToUpdateResult.existingUIDs,
isSteganographyEnabled(),
getParseCacheOptions(),
),
)
const updateAction = workerCodeAndParsedUpdate(
printParsedContent.filename,
Expand Down
2 changes: 2 additions & 0 deletions editor/src/components/editor/store/dispatch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ import {
import type { PropertyControlsInfo } from '../../custom-code/code-file'
import { getFilePathMappings } from '../../../core/model/project-file-utils'
import type { ElementInstanceMetadataMap } from '../../../core/shared/element-template'
import { getParseCacheOptions } from '../../../core/shared/parse-cache-utils'

type DispatchResultFields = {
nothingChanged: boolean
Expand Down Expand Up @@ -332,6 +333,7 @@ function maybeRequestModelUpdate(
getFilePathMappings(projectContents),
existingUIDs,
isSteganographyEnabled(),
getParseCacheOptions(),
)
.then((parseResult) => {
const updates = parseResult.map((fileResult) => {
Expand Down
13 changes: 12 additions & 1 deletion editor/src/components/editor/store/worker-update-race.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { getProjectFileByFilePath } from '../../assets'
import { renderTestEditorWithModel } from '../../canvas/ui-jsx.test-utils'
import { updateFile } from '../actions/action-creators'
import { StoryboardFilePath } from './editor-state'
import type { ParseCacheOptions } from '../../../core/shared/parse-cache-utils'
import type { FilePathMappings } from '../../../core/model/project-file-utils'

// We have to prefix all of these with "mock" otherwise Jest won't allow us to use them below
const mockDefer = defer
Expand All @@ -22,13 +24,22 @@ jest.mock('../../../core/workers/common/worker-types', () => ({
async getParseResult(
workers: any,
files: Array<ParseOrPrint>,
filePathMappings: FilePathMappings,
alreadyExistingUIDs: Set<string>,
applySteganography: SteganographyMode,
parsingCacheOptions: ParseCacheOptions,
): Promise<Array<ParseOrPrintResult>> {
mockParseStartedCount++
const result = await jest
.requireActual('../../../core/workers/common/worker-types')
.getParseResult(workers, files, alreadyExistingUIDs, applySteganography)
.getParseResult(
workers,
files,
filePathMappings,
alreadyExistingUIDs,
applySteganography,
parsingCacheOptions,
)
mockLock2.resolve()
await mockLock1
mockLock1 = mockDefer()
Expand Down
178 changes: 147 additions & 31 deletions editor/src/components/navigator/left-pane/roll-your-own-pane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,13 @@ import { UIGridRow } from '../../inspector/widgets/ui-grid-row'
import { atomWithStorage } from 'jotai/utils'
import { IS_TEST_ENVIRONMENT } from '../../../common/env-vars'
import { Ellipsis } from './github-pane/github-file-changes-list'
import { deleteParseCache } from '../../../core/shared/parse-cache-utils'
import { useRefEditorState } from '../../../components/editor/store/store-hook'
import type { FeatureName } from '../../../utils/feature-switches'
import { isFeatureEnabled, setFeatureEnabled } from '../../../utils/feature-switches'
import { getParseCacheVersion } from '../../../core/workers/parser-printer/parse-cache-utils.worker'

const sections = ['Grid'] as const
const sections = ['Grid', 'Performance'] as const
type Section = (typeof sections)[number]

type GridFeatures = {
Expand All @@ -23,15 +28,28 @@ type GridFeatures = {
activeGridBackground: string
}

type PerformanceFeatures = {
parseCache: boolean
verboseLogCache: boolean
cacheArbitraryCode: boolean
}

type RollYourOwnFeaturesTypes = {
Grid: GridFeatures
Performance: PerformanceFeatures
}

type RollYourOwnFeatures = {
[K in Section]: RollYourOwnFeaturesTypes[K]
}

const defaultRollYourOwnFeatures: RollYourOwnFeatures = {
const featureToFeatureFlagMap: Record<keyof Partial<PerformanceFeatures>, FeatureName> = {
parseCache: 'Use Parsing Cache',
verboseLogCache: 'Verbose Log Cache',
cacheArbitraryCode: 'Arbitrary Code Cache',
}

const defaultRollYourOwnFeatures: () => RollYourOwnFeatures = () => ({
Grid: {
dragVerbatim: false,
dragMagnetic: false,
Expand All @@ -44,21 +62,41 @@ const defaultRollYourOwnFeatures: RollYourOwnFeatures = {
inactiveGridColor: '#00000033',
shadowOpacity: 0.1,
},
}
Performance: {
parseCache: getFeatureFlagValue('parseCache'),
verboseLogCache: getFeatureFlagValue('verboseLogCache'),
cacheArbitraryCode: getFeatureFlagValue('cacheArbitraryCode'),
},
})

const ROLL_YOUR_OWN_FEATURES_KEY: string = 'roll-your-own-features'

const rollYourOwnFeaturesAtom = IS_TEST_ENVIRONMENT
? atom(defaultRollYourOwnFeatures)
: atomWithStorage(ROLL_YOUR_OWN_FEATURES_KEY, defaultRollYourOwnFeatures)
let rollYourOwnFeaturesAtom:
| ReturnType<typeof atom<RollYourOwnFeatures>>
| ReturnType<typeof atomWithStorage<RollYourOwnFeatures>>
| null = null

function getRollYourOwnFeaturesAtom() {
if (rollYourOwnFeaturesAtom == null) {
rollYourOwnFeaturesAtom = IS_TEST_ENVIRONMENT
? atom(defaultRollYourOwnFeatures())
: atomWithStorage(ROLL_YOUR_OWN_FEATURES_KEY, defaultRollYourOwnFeatures())
}
return rollYourOwnFeaturesAtom
}

export function useRollYourOwnFeatures() {
const [features] = useAtom(rollYourOwnFeaturesAtom)
const [features] = useAtom(getRollYourOwnFeaturesAtom())
const defaultFeatures = defaultRollYourOwnFeatures()
const merged: RollYourOwnFeatures = {
Grid: {
...defaultRollYourOwnFeatures.Grid,
...defaultFeatures.Grid,
...features.Grid,
},
Performance: {
...defaultFeatures.Performance,
...syncWithFeatureFlags(features.Performance),
},
}
return merged
}
Expand Down Expand Up @@ -115,6 +153,7 @@ export const RollYourOwnFeaturesPane = React.memo(() => {
</FlexRow>

{when(currentSection === 'Grid', <GridSection />)}
{when(currentSection === 'Performance', <PerformanceSection />)}
</Section>
</FlexColumn>
)
Expand All @@ -134,40 +173,90 @@ function getNewFeatureValueOrNull(currentValue: any, e: React.ChangeEvent<HTMLIn
}
}

/** GRID SECTION */
const GridSection = React.memo(() => {
const features = useRollYourOwnFeatures()
const setFeatures = useSetAtom(rollYourOwnFeaturesAtom)
return (
<FlexColumn style={{ gap: 10 }}>
<ResetDefaultsButton subsection='Grid' />
<SimpleFeatureControls subsection='Grid' />
</FlexColumn>
)
})
GridSection.displayName = 'GridSection'

/** PERFORMANCE SECTION */
const PerformanceSection = React.memo(() => {
const workersRef = useRefEditorState((store) => {
return store.workers
})
const handleDeleteParseCache = React.useCallback(() => {
deleteParseCache(workersRef.current)
}, [workersRef])
return (
<FlexColumn style={{ gap: 10 }}>
<SimpleFeatureControls subsection='Performance' />
<UIGridRow padded variant='<--1fr--><--1fr-->' key={`feat-cache-version`}>
<Ellipsis title='Cache Version'>Cache Version</Ellipsis>
<Ellipsis title={getParseCacheVersion()}>{getParseCacheVersion()}</Ellipsis>
</UIGridRow>
<UIGridRow padded variant='<-------------1fr------------->'>
<Button highlight spotlight onClick={handleDeleteParseCache}>
Clear cache
</Button>
</UIGridRow>
</FlexColumn>
)
})
PerformanceSection.displayName = 'PerformanceSection'

/** GENERAL COMPONENTS */

const ResetDefaultsButton = React.memo(({ subsection }: { subsection: Section }) => {
const setFeatures = useSetAtom(getRollYourOwnFeaturesAtom())
const defaultFeatures = defaultRollYourOwnFeatures()
const resetDefaults = React.useCallback(() => {
setFeatures((prevFeatures) => ({
...prevFeatures,
[subsection]: defaultFeatures[subsection],
}))
}, [defaultFeatures, setFeatures, subsection])
return (
<UIGridRow padded variant='<-------------1fr------------->'>
<Button highlight spotlight onClick={resetDefaults}>
Reset defaults
</Button>
</UIGridRow>
)
})
ResetDefaultsButton.displayName = 'ResetDefaultsButton'

const SimpleFeatureControls = React.memo(({ subsection }: { subsection: Section }) => {
const features = useRollYourOwnFeatures()
const setFeatures = useSetAtom(getRollYourOwnFeaturesAtom())
const onChange = React.useCallback(
(feat: keyof GridFeatures) => (e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = getNewFeatureValueOrNull(features.Grid[feat], e)
(feat: keyof RollYourOwnFeaturesTypes[Section]) => (e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = getNewFeatureValueOrNull(features[subsection][feat], e)
if (newValue != null) {
setFeatures({
...features,
Grid: {
...features.Grid,
[subsection]: {
...features[subsection],
[feat]: newValue,
},
})
if (typeof newValue === 'boolean') {
syncFeatureFlagIfExists(feat, newValue)
}
}
},
[features, setFeatures],
[features, setFeatures, subsection],
)

const resetDefaults = React.useCallback(() => {
setFeatures(defaultRollYourOwnFeatures)
}, [setFeatures])

const defaultFeatures = defaultRollYourOwnFeatures()
return (
<FlexColumn style={{ gap: 10 }}>
<UIGridRow padded variant='<-------------1fr------------->'>
<Button highlight spotlight onClick={resetDefaults}>
Reset defaults
</Button>
</UIGridRow>
{Object.keys(defaultRollYourOwnFeatures.Grid).map((key) => {
const feat = key as keyof GridFeatures
const value = features.Grid[feat] ?? defaultRollYourOwnFeatures.Grid[feat]
<React.Fragment>
{Object.keys(defaultFeatures[subsection]).map((key) => {
const feat = key as keyof RollYourOwnFeaturesTypes[Section]
const value = features[subsection][feat] ?? defaultFeatures[subsection][feat]
return (
<UIGridRow padded variant='<----------1fr---------><-auto->' key={`feat-${feat}`}>
<Ellipsis title={feat}>{feat}</Ellipsis>
Expand All @@ -181,7 +270,34 @@ const GridSection = React.memo(() => {
</UIGridRow>
)
})}
</FlexColumn>
</React.Fragment>
)
})
GridSection.displayName = 'GridSection'
SimpleFeatureControls.displayName = 'SimpleFeatureControls'

function getFeatureFlagValue(featureName: keyof typeof featureToFeatureFlagMap): boolean {
const featureFlag = featureToFeatureFlagMap[featureName]
return isFeatureEnabled(featureFlag)
}

function syncFeatureFlagIfExists(
featureName: keyof typeof featureToFeatureFlagMap,
value: boolean,
) {
const featureFlag = featureToFeatureFlagMap[featureName]
if (featureFlag == null) {
return
}
setFeatureEnabled(featureFlag, value)
}

function syncWithFeatureFlags(features: Record<string, any>) {
return Object.fromEntries(
Object.entries(features).map(([key, value]) => {
if (typeof value === 'boolean' && key in featureToFeatureFlagMap) {
return [key, getFeatureFlagValue(key as keyof typeof featureToFeatureFlagMap)]
}
return [key, value]
}),
)
}
9 changes: 7 additions & 2 deletions editor/src/components/navigator/left-pane/settings-pane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
toggleFeatureEnabled,
isFeatureEnabled,
AllFeatureNames,
FeaturesHiddenFromMainSettingsPane,
} from '../../../utils/feature-switches'
import json5 from 'json5'
import { load } from '../../../components/editor/actions/actions'
Expand Down Expand Up @@ -59,13 +60,17 @@ const themeOptions = [
const defaultTheme = themeOptions[0]

export const FeatureSwitchesSection = React.memo(() => {
if (AllFeatureNames.length > 0) {
const featuresToDisplay = React.useMemo(
() => AllFeatureNames.filter((name) => !FeaturesHiddenFromMainSettingsPane.includes(name)),
[],
)
if (featuresToDisplay.length > 0) {
return (
<Section>
<UIGridRow padded variant='<---1fr--->|------172px-------|'>
<H2>Experimental Toggle Features</H2>
</UIGridRow>
{AllFeatureNames.map((name) => (
{featuresToDisplay.map((name) => (
<FeatureSwitchRow key={`feature-switch-${name}`} name={name} />
))}
</Section>
Expand Down
Loading

0 comments on commit 328b3ed

Please sign in to comment.