Skip to content

Commit

Permalink
Parse in a worker
Browse files Browse the repository at this point in the history
  • Loading branch information
lukastaegert committed Oct 17, 2023
1 parent 19ee60c commit a509b7b
Show file tree
Hide file tree
Showing 12 changed files with 149 additions and 12 deletions.
7 changes: 4 additions & 3 deletions .eslintrc.js
Expand Up @@ -116,12 +116,13 @@ module.exports = {
// 'fsevents' is only available on macOS, and not installed on linux/windows
ignore: [
'fsevents',
'help.md',
'help\\.md',
'is-reference',
'package.json',
'types',
'examples.json',
'locate-character'
'examples\\.json',
'locate-character',
'^emit:'
]
}
],
Expand Down
12 changes: 12 additions & 0 deletions browser/src/parseAstAsync.ts
@@ -0,0 +1,12 @@
import type { AstNode, ParseAst } from '../../src/rollup/types';
import { parseAst } from '../../src/utils/parseAst';

// We could also do things in a web worker here, but that would make bundling
// the
// browser build much more complicated
const parseAstAsync = async (
code: Parameters<ParseAst>[0],
options?: Parameters<ParseAst>[1]
): Promise<AstNode> => new Promise(resolve => resolve(parseAst(code, options)));

export const getParseAstAsync = () => parseAstAsync;
33 changes: 33 additions & 0 deletions build-plugins/emit-file.ts
@@ -0,0 +1,33 @@
import type { Plugin } from 'rollup';

const EMIT_PREFIX = 'emit:';

export default function emitFile(): Plugin {
return {
load(id) {
if (id.startsWith(EMIT_PREFIX)) {
const entryId = id.slice(EMIT_PREFIX.length);
const referenceId = this.emitFile({
id: entryId,
type: 'chunk'
});
return `export default import.meta.ROLLUP_FILE_URL_${referenceId};`;
}
},
name: 'emit-file',
resolveFileUrl({ moduleId, relativePath, format }) {
if (moduleId.startsWith(EMIT_PREFIX)) {
return format === 'cjs'
? `__dirname + '/${relativePath}'`
: `new URL('${relativePath}', import.meta.url)`;
}
},
async resolveId(source, importer) {
if (source.startsWith(EMIT_PREFIX)) {
return `${EMIT_PREFIX}${
(await this.resolve(source.slice(EMIT_PREFIX.length), importer))!.id
}`;
}
}
};
}
5 changes: 3 additions & 2 deletions build-plugins/replace-browser-modules.ts
Expand Up @@ -12,7 +12,8 @@ const JS_REPLACED_MODULES = [
'performance',
'process',
'resolveId',
'initWasm'
'initWasm',
'parseAstAsync'
];

type ModulesMap = [string, string][];
Expand Down Expand Up @@ -41,7 +42,7 @@ export default function replaceBrowserModules(): Plugin & RollupPlugin {
}
},
transformIndexHtml(html) {
// Unfortunately, picomatch sneaks as a dedendency into the dev bundle.
// Unfortunately, picomatch sneaks as a dependency into the dev bundle.
// This fixes an error.
return html.replace('</head>', '<script>window.process={}</script></head>');
}
Expand Down
4 changes: 3 additions & 1 deletion rollup.config.ts
Expand Up @@ -12,6 +12,7 @@ import addCliEntry from './build-plugins/add-cli-entry';
import { moduleAliases } from './build-plugins/aliases';
import cleanBeforeWrite from './build-plugins/clean-before-write';
import { copyBrowserTypes, copyNodeTypes } from './build-plugins/copy-types';
import emitFile from './build-plugins/emit-file';
import emitModulePackageFile from './build-plugins/emit-module-package-file';
import { emitNativeEntry } from './build-plugins/emit-native-entry';
import emitWasmFile from './build-plugins/emit-wasm-file';
Expand Down Expand Up @@ -50,7 +51,8 @@ const nodePlugins: readonly Plugin[] = [
}),
typescript(),
cleanBeforeWrite('dist'),
externalNativeImport()
externalNativeImport(),
emitFile()
];

export default async function (
Expand Down
2 changes: 2 additions & 0 deletions src/Graph.ts
Expand Up @@ -25,6 +25,7 @@ import {
logImplicitDependantIsNotIncluded,
logMissingExport
} from './utils/logs';
import { getParseAstAsync } from './utils/parseAstAsync';
import type { PureFunctions } from './utils/pureFunctions';
import { getPureFunctions } from './utils/pureFunctions';
import { timeEnd, timeStart } from './utils/timers';
Expand Down Expand Up @@ -60,6 +61,7 @@ export default class Graph {
readonly moduleLoader: ModuleLoader;
readonly modulesById = new Map<string, Module | ExternalModule>();
needsTreeshakingPass = false;
readonly parseAstAsync = getParseAstAsync();
phase: BuildPhase = BuildPhase.LOAD_AND_PARSE;
readonly pluginDriver: PluginDriver;
readonly pureFunctions: PureFunctions;
Expand Down
14 changes: 11 additions & 3 deletions src/Module.ts
Expand Up @@ -785,7 +785,7 @@ export default class Module {
return { source, usesTopLevelAwait };
}

setSource({
async setSource({
ast,
code,
customTransformCache,
Expand All @@ -799,7 +799,7 @@ export default class Module {
}: TransformModuleJSON & {
resolvedIds?: ResolvedIdMap;
transformFiles?: EmittedFile[] | undefined;
}): void {
}): Promise<void> {
if (code.startsWith('#!')) {
const shebangEndPosition = code.indexOf('\n');
this.shebang = code.slice(2, shebangEndPosition);
Expand Down Expand Up @@ -831,7 +831,7 @@ export default class Module {
this.transformDependencies = transformDependencies;
this.customTransformCache = customTransformCache;
this.updateOptions(moduleOptions);
const moduleAst = ast ?? this.tryParse();
const moduleAst = ast ?? (await this.tryParseAsync());

timeEnd('generate ast', 3);
timeStart('analyze ast', 3);
Expand Down Expand Up @@ -1334,6 +1334,14 @@ export default class Module {
return this.error(logModuleParseError(error_, this.id), error_.pos);
}
}

private async tryParseAsync(): Promise<ProgramAst> {
try {
return (await this.graph.parseAstAsync(this.info.code!)) as ProgramAst;
} catch (error_: any) {
return this.error(logModuleParseError(error_, this.id), error_.pos);
}
}
}

// if there is a cyclic import in the reexport chain, we should not
Expand Down
4 changes: 2 additions & 2 deletions src/ModuleLoader.ts
Expand Up @@ -311,10 +311,10 @@ export class ModuleLoader {
for (const emittedFile of cachedModule.transformFiles)
this.pluginDriver.emitFile(emittedFile);
}
module.setSource(cachedModule);
await module.setSource(cachedModule);
} else {
module.updateOptions(sourceDescription);
module.setSource(
await module.setSource(
await transform(sourceDescription, module, this.pluginDriver, this.options.onLog)
);
}
Expand Down
3 changes: 2 additions & 1 deletion src/utils/logs.ts
Expand Up @@ -824,7 +824,8 @@ export function logModuleParseError(error: Error, moduleId: string): RollupLog {
cause: error,
code: PARSE_ERROR,
id: moduleId,
message
message,
stack: error.stack
};
}

Expand Down
58 changes: 58 additions & 0 deletions src/utils/parseAstAsync.ts
@@ -0,0 +1,58 @@
import { Worker } from 'node:worker_threads';
import parseWorkerUrl from 'emit:./parseAstWorkerEntry.ts';
import type { AstNode, ParseAst } from '../rollup/types';
import { convertProgram } from './convert-ast';
import getReadStringFunction from './getReadStringFunction';

type MessageResolvers = Map<number, [(buffer: Buffer) => void, (error: unknown) => void]>;

export const getParseAstAsync = () => {
let workerWithResolvers: { worker: Worker; resolvers: MessageResolvers } | null = null;
let nextId = 0;

const parseToBuffer = (code: string, allowReturnOutsideFunction: boolean): Promise<Buffer> => {
if (!workerWithResolvers) {
const worker = new Worker(parseWorkerUrl);
const resolvers: MessageResolvers = new Map();

worker.on('message', ([id, buffer]: [id: number, buffer: Buffer]) => {
resolvers.get(id)![0](buffer);
resolvers.delete(id);
if (resolvers.size === 0) {
// We wait one macro task tick to no close the worker if there are
// additional tasks directly queued up
setTimeout(async () => {
if (resolvers.size === 0) {
workerWithResolvers = null;
await worker.terminate();
}
});
}
});

worker.on('error', error => {
if (workerWithResolvers?.worker === worker) {
workerWithResolvers = null;
for (const [, reject] of resolvers.values()) {
reject(error);

Check warning on line 37 in src/utils/parseAstAsync.ts

View check run for this annotation

Codecov / codecov/patch

src/utils/parseAstAsync.ts#L35-L37

Added lines #L35 - L37 were not covered by tests
}
resolvers.clear();

Check warning on line 39 in src/utils/parseAstAsync.ts

View check run for this annotation

Codecov / codecov/patch

src/utils/parseAstAsync.ts#L39

Added line #L39 was not covered by tests
}
});
workerWithResolvers = { resolvers, worker };
}
const id = nextId++;
return new Promise<Buffer>((resolve, reject) => {
workerWithResolvers!.resolvers.set(id, [resolve, reject]);
workerWithResolvers!.worker.postMessage([id, code, allowReturnOutsideFunction]);
});
};

return async (
code: Parameters<ParseAst>[0],
{ allowReturnOutsideFunction = false }: Parameters<ParseAst>[1] = {}
): Promise<AstNode> => {
const astBuffer = await parseToBuffer(code, allowReturnOutsideFunction);
return convertProgram(astBuffer.buffer, getReadStringFunction(astBuffer));
};
};
14 changes: 14 additions & 0 deletions src/utils/parseAstWorkerEntry.ts
@@ -0,0 +1,14 @@
import { parentPort } from 'node:worker_threads';
import { parse } from '../../native';

parentPort!.on(
'message',
([id, code, allowReturnOutsideFunction]: [
id: number,
code: string,
allowReturnOutsideFunction: boolean
]) => {
const buffer = parse(code, allowReturnOutsideFunction);
parentPort!.postMessage([id, buffer], [buffer.buffer]);
}
);
5 changes: 5 additions & 0 deletions typings/declarations.d.ts
Expand Up @@ -4,6 +4,11 @@ declare module 'help.md' {
export default value;
}

declare module 'emit:*' {
const value: string;
export default value;
}

// external libs
declare module 'rollup-plugin-string' {
import type { PluginImpl } from 'rollup';
Expand Down

0 comments on commit a509b7b

Please sign in to comment.