diff --git a/editor/src/components/canvas/ui-jsx.test-utils.tsx b/editor/src/components/canvas/ui-jsx.test-utils.tsx index 62f1e81f2cb2..1b8796cbcb92 100644 --- a/editor/src/components/canvas/ui-jsx.test-utils.tsx +++ b/editor/src/components/canvas/ui-jsx.test-utils.tsx @@ -569,7 +569,7 @@ export async function renderTestEditorWithModel( } const workers = new UtopiaTsWorkersImplementation( - new FakeParserPrinterWorker(), + [new FakeParserPrinterWorker()], new FakeLinterWorker(), new FakeWatchdogWorker(), ) @@ -1042,7 +1042,7 @@ export function createBuiltinDependenciesWithTestWorkers( extraBuiltinDependencies: BuiltInDependencies, ): BuiltInDependencies { const workers = new UtopiaTsWorkersImplementation( - new FakeParserPrinterWorker(), + [new FakeParserPrinterWorker()], new FakeLinterWorker(), new FakeWatchdogWorker(), ) diff --git a/editor/src/components/editor/store/dispatch-strategies.spec.tsx b/editor/src/components/editor/store/dispatch-strategies.spec.tsx index f2788b0c7837..b85b7cdf2280 100644 --- a/editor/src/components/editor/store/dispatch-strategies.spec.tsx +++ b/editor/src/components/editor/store/dispatch-strategies.spec.tsx @@ -105,7 +105,7 @@ function createEditorStore( }, }, workers: new UtopiaTsWorkersImplementation( - new FakeParserPrinterWorker(), + [new FakeParserPrinterWorker()], new FakeLinterWorker(), new FakeWatchdogWorker(), ), diff --git a/editor/src/components/editor/store/dispatch-strategies.tsx b/editor/src/components/editor/store/dispatch-strategies.tsx index 994c9e322cdc..8d2c0f818fdb 100644 --- a/editor/src/components/editor/store/dispatch-strategies.tsx +++ b/editor/src/components/editor/store/dispatch-strategies.tsx @@ -51,13 +51,12 @@ import type { StrategyApplicationResult, } from '../../canvas/canvas-strategies/canvas-strategy-types' import { strategyApplicationResult } from '../../canvas/canvas-strategies/canvas-strategy-types' -import { isFeatureEnabled } from '../../../utils/feature-switches' -import { PERFORMANCE_MARKS_ALLOWED } from '../../../common/env-vars' import { last } from '../../../core/shared/array-utils' import type { BuiltInDependencies } from '../../../core/es-modules/package-manager/built-in-dependencies-list' import { isInsertMode } from '../editor-modes' import { patchedCreateRemixDerivedDataMemo } from './remix-derived-data' import { allowedToEditProject } from './collaborative-editing' +import { canMeasurePerformance } from '../../../core/performance/performance-utils' interface HandleStrategiesResult { unpatchedEditorState: EditorState @@ -672,10 +671,7 @@ export function handleStrategies( result: EditorStoreUnpatched, oldDerivedState: DerivedState, ): HandleStrategiesResult & { patchedDerivedState: DerivedState } { - const MeasureDispatchTime = - (isFeatureEnabled('Debug – Performance Marks (Fast)') || - isFeatureEnabled('Debug – Performance Marks (Slow)')) && - PERFORMANCE_MARKS_ALLOWED + const MeasureDispatchTime = canMeasurePerformance() if (MeasureDispatchTime) { window.performance.mark('strategies_begin') diff --git a/editor/src/components/editor/store/dispatch.tsx b/editor/src/components/editor/store/dispatch.tsx index 0b3d0e8ab4cd..e8a522d2d7f1 100644 --- a/editor/src/components/editor/store/dispatch.tsx +++ b/editor/src/components/editor/store/dispatch.tsx @@ -1,4 +1,3 @@ -import { PERFORMANCE_MARKS_ALLOWED } from '../../../common/env-vars' import type { UtopiaTsWorkers } from '../../../core/workers/common/worker-types' import { getParseResult } from '../../../core/workers/common/worker-types' import { runLocalCanvasAction } from '../../../templates/editor-canvas' @@ -59,7 +58,6 @@ import { getProjectChanges, sendVSCodeChanges, } from './vscode-changes' -import { isFeatureEnabled } from '../../../utils/feature-switches' import { handleStrategies, updatePostActionState } from './dispatch-strategies' import type { MetaCanvasStrategy } from '../../canvas/canvas-strategies/canvas-strategies' @@ -89,6 +87,15 @@ 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 { + getParserChunkCount, + getParserWorkerCount, + isConcurrencyLoggingEnabled, +} from '../../../core/workers/common/concurrency-utils' +import { + canMeasurePerformance, + startPerformanceMeasure, +} from '../../../core/performance/performance-utils' import { getParseCacheOptions } from '../../../core/shared/parse-cache-utils' type DispatchResultFields = { @@ -327,15 +334,27 @@ function maybeRequestModelUpdate( // Should anything need to be sent across, do so here. if (filesToUpdate.length > 0) { + const { endMeasure } = startPerformanceMeasure('file-parse', { uniqueId: true }) const parseFinished = getParseResult( workers, filesToUpdate, getFilePathMappings(projectContents), existingUIDs, isSteganographyEnabled(), + getParserChunkCount(), getParseCacheOptions(), ) .then((parseResult) => { + const duration = endMeasure() + if (isConcurrencyLoggingEnabled() && filesToUpdate.length > 1) { + console.info( + `parse finished for ${ + filesToUpdate.length + } files, using ${getParserChunkCount()} chunks and ${getParserWorkerCount()} workers, in ${duration.toFixed( + 2, + )}ms`, + ) + } const updates = parseResult.map((fileResult) => { return parseResultToWorkerUpdates(fileResult) }) @@ -810,10 +829,7 @@ function editorDispatchInner( ): DispatchResult { // console.log('DISPATCH', simpleStringifyActions(dispatchedActions), dispatchedActions) - const MeasureDispatchTime = - (isFeatureEnabled('Debug – Performance Marks (Fast)') || - isFeatureEnabled('Debug – Performance Marks (Slow)')) && - PERFORMANCE_MARKS_ALLOWED + const MeasureDispatchTime = canMeasurePerformance() if (MeasureDispatchTime) { window.performance.mark('dispatch_begin') diff --git a/editor/src/components/editor/store/editor-dispatch-performance-logging.tsx b/editor/src/components/editor/store/editor-dispatch-performance-logging.tsx index 6f1290045faf..457bf3b37c58 100644 --- a/editor/src/components/editor/store/editor-dispatch-performance-logging.tsx +++ b/editor/src/components/editor/store/editor-dispatch-performance-logging.tsx @@ -1,14 +1,11 @@ -import { PERFORMANCE_MARKS_ALLOWED } from '../../../common/env-vars' +import { canMeasurePerformance } from '../../../core/performance/performance-utils' import { isFeatureEnabled } from '../../../utils/feature-switches' import type { EditorAction } from '../action-types' import { simpleStringifyActions } from '../actions/action-utils' export function createPerformanceMeasure() { const MeasureSelectorsEnabled = isFeatureEnabled('Debug – Measure Selectors') - const PerformanceMarks = - (isFeatureEnabled('Debug – Performance Marks (Slow)') || - isFeatureEnabled('Debug – Performance Marks (Fast)')) && - PERFORMANCE_MARKS_ALLOWED + const PerformanceMarks = canMeasurePerformance() let stringifiedActions = '' diff --git a/editor/src/components/editor/store/worker-update-race.spec.tsx b/editor/src/components/editor/store/worker-update-race.spec.tsx index 1f84c4963a60..bd3a636d7210 100644 --- a/editor/src/components/editor/store/worker-update-race.spec.tsx +++ b/editor/src/components/editor/store/worker-update-race.spec.tsx @@ -27,6 +27,7 @@ jest.mock('../../../core/workers/common/worker-types', () => ({ filePathMappings: FilePathMappings, alreadyExistingUIDs: Set, applySteganography: SteganographyMode, + parserChunkCount: number, parsingCacheOptions: ParseCacheOptions, ): Promise> { mockParseStartedCount++ @@ -38,6 +39,7 @@ jest.mock('../../../core/workers/common/worker-types', () => ({ filePathMappings, alreadyExistingUIDs, applySteganography, + parserChunkCount, parsingCacheOptions, ) mockLock2.resolve() diff --git a/editor/src/components/navigator/left-pane/roll-your-own-pane.tsx b/editor/src/components/navigator/left-pane/roll-your-own-pane.tsx index 936840a19ff8..5a47482e00bf 100644 --- a/editor/src/components/navigator/left-pane/roll-your-own-pane.tsx +++ b/editor/src/components/navigator/left-pane/roll-your-own-pane.tsx @@ -30,8 +30,10 @@ type GridFeatures = { type PerformanceFeatures = { parseCache: boolean - verboseLogCache: boolean + parallelParsing: boolean cacheArbitraryCode: boolean + verboseLogCache: boolean + logParseTimings: boolean } type RollYourOwnFeaturesTypes = { @@ -47,6 +49,8 @@ const featureToFeatureFlagMap: Record, Featur parseCache: 'Use Parsing Cache', verboseLogCache: 'Verbose Log Cache', cacheArbitraryCode: 'Arbitrary Code Cache', + parallelParsing: 'Parallel Parsing', + logParseTimings: 'Log Parse Timings', } const defaultRollYourOwnFeatures: () => RollYourOwnFeatures = () => ({ @@ -64,8 +68,10 @@ const defaultRollYourOwnFeatures: () => RollYourOwnFeatures = () => ({ }, Performance: { parseCache: getFeatureFlagValue('parseCache'), - verboseLogCache: getFeatureFlagValue('verboseLogCache'), + parallelParsing: getFeatureFlagValue('parallelParsing'), cacheArbitraryCode: getFeatureFlagValue('cacheArbitraryCode'), + logParseTimings: getFeatureFlagValue('logParseTimings'), + verboseLogCache: getFeatureFlagValue('verboseLogCache'), }, }) @@ -256,10 +262,15 @@ const SimpleFeatureControls = React.memo(({ subsection }: { subsection: Section {Object.keys(defaultFeatures[subsection]).map((key) => { const feat = key as keyof RollYourOwnFeaturesTypes[Section] + const featName = Object.keys(featureToFeatureFlagMap).includes( + feat as keyof typeof featureToFeatureFlagMap, + ) + ? featureToFeatureFlagMap[feat as keyof typeof featureToFeatureFlagMap] + : feat const value = features[subsection][feat] ?? defaultFeatures[subsection][feat] return ( - {feat} + {featName} {typeof value === 'boolean' ? ( ) : typeof value === 'string' ? ( diff --git a/editor/src/core/performance/performance-utils.ts b/editor/src/core/performance/performance-utils.ts index d67246dacfc1..ca0ca4c0bc2a 100644 --- a/editor/src/core/performance/performance-utils.ts +++ b/editor/src/core/performance/performance-utils.ts @@ -1,3 +1,6 @@ +import { PERFORMANCE_MARKS_ALLOWED } from '../../common/env-vars' +import { isFeatureEnabled } from '../../utils/feature-switches' + export function timeFunction(fnName: string, fn: () => any, iterations: number = 100) { const start = Date.now() for (var i = 0; i < iterations; i++) { @@ -8,3 +11,38 @@ export function timeFunction(fnName: string, fn: () => any, iterations: number = // eslint-disable-next-line no-console console.log(`${fnName} took ${timeTaken}ms`) } + +export function canMeasurePerformance(): boolean { + return ( + (isFeatureEnabled('Debug – Performance Marks (Fast)') || + isFeatureEnabled('Debug – Performance Marks (Slow)')) && + PERFORMANCE_MARKS_ALLOWED + ) +} + +export function startPerformanceMeasure( + measureName: string, + { uniqueId }: { uniqueId?: boolean } = {}, +): { id: string; endMeasure: () => number } { + const id = uniqueId ? `${measureName}-${Math.random()}` : measureName + if (PERFORMANCE_MARKS_ALLOWED) { + performance.mark(`${id}-start`) + } + return { + id: id, + endMeasure: () => endPerformanceMeasure(id), + } +} + +export function endPerformanceMeasure(id: string): number { + if (PERFORMANCE_MARKS_ALLOWED) { + performance.mark(`${id}-end`) + performance.measure(`${id}-duration`, `${id}-start`, `${id}-end`) + const measurements = performance.getEntriesByName(`${id}-duration`) + const latestMeasurement = measurements[measurements.length - 1] + if (latestMeasurement != null) { + return latestMeasurement.duration + } + } + return 0 +} diff --git a/editor/src/core/property-controls/property-controls-local-parser-bridge.ts b/editor/src/core/property-controls/property-controls-local-parser-bridge.ts index 08a55f0e197d..466827543689 100644 --- a/editor/src/core/property-controls/property-controls-local-parser-bridge.ts +++ b/editor/src/core/property-controls/property-controls-local-parser-bridge.ts @@ -7,6 +7,7 @@ import type { Imports } from '../shared/project-file-types' import { isParseFailure, isParseSuccess } from '../shared/project-file-types' import { emptySet } from '../shared/set-utils' import { isSteganographyEnabled } from '../shared/stegano-text' +import { getParserChunkCount } from '../workers/common/concurrency-utils' import type { UtopiaTsWorkers } from '../workers/common/worker-types' import { createParseFile, getParseResult } from '../workers/common/worker-types' import { ARBITRARY_CODE_FILE_NAME } from '../workers/common/worker-types' @@ -51,6 +52,7 @@ async function getParseResultForUserStrings( [], emptySet(), isSteganographyEnabled(), + getParserChunkCount(), getParseCacheOptions(), ) diff --git a/editor/src/core/shared/array-utils.spec.ts b/editor/src/core/shared/array-utils.spec.ts index 1b92c3856055..4ecdfaa5c882 100644 --- a/editor/src/core/shared/array-utils.spec.ts +++ b/editor/src/core/shared/array-utils.spec.ts @@ -3,6 +3,8 @@ import { intersection, mapAndFilter, possiblyUniqueInArray, + sortArrayByAndReturnPermutation, + revertArrayOrder, strictEvery, } from './array-utils' @@ -53,6 +55,24 @@ describe('aperture', () => { }) }) +describe('sortArrayByAndReturnPermutation', () => { + it('should sort the array', () => { + const { sortedArray } = sortArrayByAndReturnPermutation([3, 1, 2], (a) => a) + expect(sortedArray).toEqual([1, 2, 3]) + }) + it('should respect the sort order', () => { + const { sortedArray } = sortArrayByAndReturnPermutation([3, 1, 2], (a) => a, false) + expect(sortedArray).toEqual([3, 2, 1]) + }) + it('should return the permutation that reverses the sort', () => { + const originalArray = [10, 5, 6, 32, 102, 7, 91] + const { sortedArray, permutation } = sortArrayByAndReturnPermutation(originalArray, (a) => a) + expect(sortedArray).toEqual([5, 6, 7, 10, 32, 91, 102]) + const reversedToOriginal = revertArrayOrder(sortedArray, permutation) + expect(reversedToOriginal).toEqual(originalArray) + }) +}) + describe('mapAndFilter', () => { const input = [1, 2, 3, 4, 5] const mapFn = (n: number) => n + 10 diff --git a/editor/src/core/shared/array-utils.ts b/editor/src/core/shared/array-utils.ts index d4381589a4be..3823f2dde770 100644 --- a/editor/src/core/shared/array-utils.ts +++ b/editor/src/core/shared/array-utils.ts @@ -538,3 +538,41 @@ export function matrixGetter(array: T[], width: number): (row: number, column return array[row * width + column] } } + +export function chunkArrayEqually( + sortedArray: T[], + numberOfChunks: number, + valueFn: (t: T) => number, +): T[][] { + const chunks: T[][] = Array.from({ length: numberOfChunks }, () => []) + const chunkSums: number[] = Array(numberOfChunks).fill(0) + for (const data of sortedArray) { + let minIndex = 0 + for (let i = 1; i < numberOfChunks; i++) { + if (chunkSums[i] < chunkSums[minIndex]) { + minIndex = i + } + } + chunks[minIndex].push(data) + chunkSums[minIndex] += valueFn(data) + } + return chunks.filter((chunk) => chunk.length > 0) +} + +export function sortArrayByAndReturnPermutation( + array: T[], + sortFn: (t: T) => number, + ascending: boolean = true, +): { sortedArray: T[]; permutation: number[] } { + const permutation = array.map((_, index) => index) + permutation.sort((a, b) => { + const sortResult = sortFn(array[a]) - sortFn(array[b]) + return ascending ? sortResult : -sortResult + }) + const sortedArray = permutation.map((index) => array[index]) + return { sortedArray, permutation } +} + +export function revertArrayOrder(array: T[], permutation: number[]): T[] { + return array.map((_, index) => array[permutation.indexOf(index)]) +} diff --git a/editor/src/core/shared/parser-projectcontents-utils.ts b/editor/src/core/shared/parser-projectcontents-utils.ts index 0199fdbe41b8..8e17834abefb 100644 --- a/editor/src/core/shared/parser-projectcontents-utils.ts +++ b/editor/src/core/shared/parser-projectcontents-utils.ts @@ -35,6 +35,7 @@ import { fastForEach } from '../../core/shared/utils' import { codeNeedsPrinting, codeNeedsParsing } from '../../core/workers/common/project-file-utils' import { isFeatureEnabled } from '../../utils/feature-switches' import { isSteganographyEnabled } from './stegano-text' +import { getParserChunkCount } from '../workers/common/concurrency-utils' import { getParseCacheOptions } from './parse-cache-utils' export function parseResultToWorkerUpdates(fileResult: ParseOrPrintResult): WorkerUpdate { @@ -204,6 +205,7 @@ export async function updateProjectContentsWithParseResults( getFilePathMappings(projectContents), existingUIDs, isSteganographyEnabled(), + getParserChunkCount(), getParseCacheOptions(), ) diff --git a/editor/src/core/workers/common/concurrency-utils.ts b/editor/src/core/workers/common/concurrency-utils.ts new file mode 100644 index 000000000000..c8d5e015e22d --- /dev/null +++ b/editor/src/core/workers/common/concurrency-utils.ts @@ -0,0 +1,23 @@ +import { isFeatureEnabled } from '../../../utils/feature-switches' + +// TODO: this will be configurable from the RYO menu +export const PARSE_CONCURRENCY = 3 +const PARSER_CONCURRENCY_FEATURE = 'Parallel Parsing' +const LOG_CONCURRENCY_TIMINGS_FEATURE = 'Log Parse Timings' + +export function isConcurrencyEnabled() { + return isFeatureEnabled(PARSER_CONCURRENCY_FEATURE) +} + +export function getParserWorkerCount() { + return isConcurrencyEnabled() ? PARSE_CONCURRENCY : 1 +} + +export function getParserChunkCount() { + return isConcurrencyEnabled() ? PARSE_CONCURRENCY : 1 +} + +// TODO: this will be configurable from the RYO menu +export function isConcurrencyLoggingEnabled() { + return isFeatureEnabled(LOG_CONCURRENCY_TIMINGS_FEATURE) +} diff --git a/editor/src/core/workers/common/worker-types.ts b/editor/src/core/workers/common/worker-types.ts index c094cc8e71c5..e8d3a89145ab 100644 --- a/editor/src/core/workers/common/worker-types.ts +++ b/editor/src/core/workers/common/worker-types.ts @@ -1,3 +1,8 @@ +import { + chunkArrayEqually, + sortArrayByAndReturnPermutation, + revertArrayOrder, +} from '../../../core/shared/array-utils' import type { ParseCacheOptions } from '../../shared/parse-cache-utils' import type { ProjectContentTreeRoot } from '../../../components/assets' import type { ErrorMessage } from '../../shared/error-messages' @@ -11,6 +16,8 @@ import type { import type { SteganographyMode } from '../parser-printer/parser-printer' import type { RawSourceMap } from '../ts/ts-typings/RawSourceMap' import type { FilePathMappings } from './project-file-utils' +import type { ParserPrinterWorker } from '../workers' +import { isParseableFile } from '../../../core/shared/file-utils' export const ARBITRARY_CODE_FILE_NAME = 'code.tsx' @@ -207,7 +214,75 @@ export function createParsePrintFilesRequest( let PARSE_PRINT_MESSAGE_COUNTER: number = 0 +const fileParsableLength = (file: ParseOrPrint) => + file.type === 'parsefile' && isParseableFile(file.filename) ? file.content.length : 0 + export function getParseResult( + workers: UtopiaTsWorkers, + files: Array, + filePathMappings: FilePathMappings, + alreadyExistingUIDs: Set, + applySteganography: SteganographyMode, + numChunks: number = 1, + parsingCacheOptions: ParseCacheOptions, +): Promise> { + // this is to eliminate unnecessary overhead when numChunks is 1 + if (numChunks === 1 || files.length === 1) { + return getParseResultSerial( + workers, + files, + filePathMappings, + alreadyExistingUIDs, + applySteganography, + parsingCacheOptions, + ) + } else { + return getParseResultChunked( + workers, + files, + filePathMappings, + alreadyExistingUIDs, + applySteganography, + numChunks, + parsingCacheOptions, + ) + } +} + +export async function getParseResultChunked( + workers: UtopiaTsWorkers, + files: Array, + filePathMappings: FilePathMappings, + alreadyExistingUIDs: Set, + applySteganography: SteganographyMode, + numChunks: number = 1, + parsingCacheOptions: ParseCacheOptions, +): Promise> { + const { sortedArray, permutation } = sortArrayByAndReturnPermutation( + [...files], + fileParsableLength, + false, + ) + const chunks = chunkArrayEqually(sortedArray, numChunks, fileParsableLength) + + const promises = chunks.map((chunk) => + getParseResultSerial( + workers, + chunk, + filePathMappings, + alreadyExistingUIDs, + applySteganography, + parsingCacheOptions, + ), + ) + const results = await Promise.all(promises) + const flattenedResults = results.flat() + + // this is to return the results in the original order + return revertArrayOrder(flattenedResults, permutation) +} + +export function getParseResultSerial( workers: UtopiaTsWorkers, files: Array, filePathMappings: FilePathMappings, @@ -217,6 +292,7 @@ export function getParseResult( ): Promise> { const messageIDForThisRequest = PARSE_PRINT_MESSAGE_COUNTER++ return new Promise((resolve, reject) => { + const availableWorker = workers.getNextParserPrinterWorker() const handleMessage = (e: MessageEvent) => { const data = e.data as ParsePrintResultMessage // Ensure that rapidly fired requests are distinguished between the handlers. @@ -224,19 +300,19 @@ export function getParseResult( switch (data.type) { case 'parseprintfilesresult': { resolve(data.files) - workers.removeParserPrinterEventListener(handleMessage) + workers.removeParserPrinterEventListener(handleMessage, availableWorker) break } case 'parseprintfailed': { reject() - workers.removeParserPrinterEventListener(handleMessage) + workers.removeParserPrinterEventListener(handleMessage, availableWorker) break } } } } - workers.addParserPrinterEventListener(handleMessage) + workers.addParserPrinterEventListener(handleMessage, availableWorker) workers.sendParsePrintMessage( createParsePrintFilesRequest( files, @@ -246,6 +322,7 @@ export function getParseResult( applySteganography, parsingCacheOptions, ), + availableWorker, ) }) } @@ -407,11 +484,18 @@ export function createInitTSWorkerMessage( } export interface UtopiaTsWorkers { - sendParsePrintMessage: (request: ParsePrintFilesRequest) => void + getNextParserPrinterWorker: () => ParserPrinterWorker + sendParsePrintMessage: (request: ParsePrintFilesRequest, worker: ParserPrinterWorker) => void sendClearParseCacheMessage: (parsingCacheOptions: ParseCacheOptions) => void sendLinterRequestMessage: (filename: string, content: string) => void - addParserPrinterEventListener: (handler: (e: MessageEvent) => void) => void - removeParserPrinterEventListener: (handler: (e: MessageEvent) => void) => void + addParserPrinterEventListener: ( + handler: (e: MessageEvent) => void, + worker: ParserPrinterWorker, + ) => void + removeParserPrinterEventListener: ( + handler: (e: MessageEvent) => void, + worker: ParserPrinterWorker, + ) => void addLinterResultEventListener: (handler: (e: MessageEvent) => void) => void removeLinterResultEventListener: (handler: (e: MessageEvent) => void) => void initWatchdogWorker(projectID: string): void diff --git a/editor/src/core/workers/workers.ts b/editor/src/core/workers/workers.ts index a034212948a7..667439e27381 100644 --- a/editor/src/core/workers/workers.ts +++ b/editor/src/core/workers/workers.ts @@ -15,11 +15,13 @@ import { createClearParseCacheMessage, } from './common/worker-types' import type { ProjectContentTreeRoot } from '../../components/assets' +import { FakeParserPrinterWorker } from './test-workers' import type { ParseCacheOptions } from '../shared/parse-cache-utils' export class UtopiaTsWorkersImplementation implements UtopiaTsWorkers { + private parserArrayCounter = 0 constructor( - private parserPrinterWorker: ParserPrinterWorker, + private parserPrinterWorkerArray: ParserPrinterWorker[], private linterWorker: LinterWorker, private watchdogWorker: WatchdogWorker, ) {} @@ -28,20 +30,34 @@ export class UtopiaTsWorkersImplementation implements UtopiaTsWorkers { this.linterWorker.sendLinterRequestMessage(filename, content) } - sendParsePrintMessage(request: ParsePrintFilesRequest): void { - this.parserPrinterWorker.sendParsePrintMessage(request) + getNextParserPrinterWorker(): ParserPrinterWorker { + const parserPrinterWorker = this.parserPrinterWorkerArray[this.parserArrayCounter] + this.parserArrayCounter = (this.parserArrayCounter + 1) % this.parserPrinterWorkerArray.length + return parserPrinterWorker + } + + sendParsePrintMessage(request: ParsePrintFilesRequest, worker: ParserPrinterWorker): void { + worker.sendParsePrintMessage(request) } sendClearParseCacheMessage(parsingCacheOptions: ParseCacheOptions): void { - this.parserPrinterWorker.sendClearParseCacheMessage(parsingCacheOptions) + this.parserPrinterWorkerArray.forEach((worker) => { + worker.sendClearParseCacheMessage(parsingCacheOptions) + }) } - addParserPrinterEventListener(handler: (e: MessageEvent) => void): void { - this.parserPrinterWorker.addParseFileResultEventListener(handler) + addParserPrinterEventListener( + handler: (e: MessageEvent) => void, + worker: ParserPrinterWorker, + ): void { + worker.addParseFileResultEventListener(handler) } - removeParserPrinterEventListener(handler: (e: MessageEvent) => void): void { - this.parserPrinterWorker.removeParseFileResultEventListener(handler) + removeParserPrinterEventListener( + handler: (e: MessageEvent) => void, + worker: ParserPrinterWorker, + ): void { + worker.removeParseFileResultEventListener(handler) } addLinterResultEventListener(handler: (e: MessageEvent) => void): void { @@ -193,6 +209,10 @@ export class RealWatchdogWorker implements WatchdogWorker { } export class MockUtopiaTsWorkers implements UtopiaTsWorkers { + getNextParserPrinterWorker(): ParserPrinterWorker { + return new FakeParserPrinterWorker() + } + sendInitMessage( _typeDefinitions: TypeDefinitions, _projectContents: ProjectContentTreeRoot, diff --git a/editor/src/templates/editor.tsx b/editor/src/templates/editor.tsx index 8f9350ce9114..d65daff780fb 100644 --- a/editor/src/templates/editor.tsx +++ b/editor/src/templates/editor.tsx @@ -10,7 +10,6 @@ import { useAtomsDevtools } from 'jotai-devtools' import '../utils/vite-hmr-config' import { getProjectID, - PERFORMANCE_MARKS_ALLOWED, PROBABLY_ELECTRON, PRODUCTION_ENV, requireElectron, @@ -101,7 +100,7 @@ import * as EP from '../core/shared/element-path' import { waitUntil } from '../core/shared/promise-utils' import { sendSetVSCodeTheme } from '../core/vscode/vscode-bridge' import type { ElementPath } from '../core/shared/project-file-types' -import { uniq, uniqBy } from '../core/shared/array-utils' +import { createArrayWithLength, uniq, uniqBy } from '../core/shared/array-utils' import { updateUserDetailsWhenAuthenticated } from '../core/shared/github/helpers' import { DispatchContext } from '../components/editor/store/dispatch-context' import { @@ -132,6 +131,8 @@ import { runDomSamplerGroups, } from '../components/canvas/dom-sampler' import { omitWithPredicate } from '../core/shared/object-utils' +import { getParserWorkerCount } from '../core/workers/common/concurrency-utils' +import { canMeasurePerformance } from '../core/performance/performance-utils' import { getChildGroupsForNonGroupParents } from '../components/canvas/canvas-strategies/strategies/fragment-like-helpers' if (PROBABLY_ELECTRON) { @@ -250,7 +251,7 @@ export class Editor { ) const workers = new UtopiaTsWorkersImplementation( - new RealParserPrinterWorker(), + createArrayWithLength(getParserWorkerCount(), () => new RealParserPrinterWorker()), new RealLinterWorker(), watchdogWorker, ) @@ -442,10 +443,7 @@ export class Editor { Measure.logActions(dispatchedActions) const MeasureSelectors = isFeatureEnabled('Debug – Measure Selectors') - const PerformanceMarks = - (isFeatureEnabled('Debug – Performance Marks (Slow)') || - isFeatureEnabled('Debug – Performance Marks (Fast)')) && - PERFORMANCE_MARKS_ALLOWED + const PerformanceMarks = canMeasurePerformance() const runDispatch = () => { const oldEditorState = this.storedState diff --git a/editor/src/utils/feature-switches.ts b/editor/src/utils/feature-switches.ts index 95a3541fbd1c..31674c965b38 100644 --- a/editor/src/utils/feature-switches.ts +++ b/editor/src/utils/feature-switches.ts @@ -14,6 +14,8 @@ export type FeatureName = | 'Project Thumbnail Generation' | 'Debug - Print UIDs' | 'Debug – Connections' + | 'Parallel Parsing' + | 'Log Parse Timings' | 'Condensed Navigator Entries' | 'Use Parsing Cache' | 'Verbose Log Cache' @@ -35,6 +37,8 @@ export const AllFeatureNames: FeatureName[] = [ 'Project Thumbnail Generation', 'Debug - Print UIDs', 'Debug – Connections', + 'Parallel Parsing', + 'Log Parse Timings', 'Condensed Navigator Entries', 'Use Parsing Cache', 'Verbose Log Cache', @@ -56,6 +60,8 @@ let FeatureSwitches: { [feature in FeatureName]: boolean } = { 'Project Thumbnail Generation': false, 'Debug - Print UIDs': false, 'Debug – Connections': false, + 'Parallel Parsing': !IS_TEST_ENVIRONMENT, + 'Log Parse Timings': false, Tailwind: false, 'Condensed Navigator Entries': !IS_TEST_ENVIRONMENT, 'Use Parsing Cache': !IS_TEST_ENVIRONMENT, @@ -69,6 +75,8 @@ export const FeaturesHiddenFromMainSettingsPane: FeatureName[] = [ 'Use Parsing Cache', 'Verbose Log Cache', 'Arbitrary Code Cache', + 'Parallel Parsing', + 'Log Parse Timings', ] export const STEGANOGRAPHY_ENABLED = false