Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf: introduce parseAstAsync and parallelize parsing AST #5202

Merged
merged 10 commits into from
Oct 31, 2023
10 changes: 10 additions & 0 deletions browser/src/wasm.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,12 @@
// eslint-disable-next-line import/no-unresolved
export { parse, xxhashBase64Url } from '../../wasm/bindings_wasm.js';

// eslint-disable-next-line import/no-unresolved
import { parse } from '../../wasm/bindings_wasm.js';
export async function parseAsync(
code: string,
allowReturnOutsideFunction: boolean,
_signal?: AbortSignal | undefined | null
) {
return parse(code, allowReturnOutsideFunction);
}
31 changes: 31 additions & 0 deletions docs/javascript-api/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -369,3 +369,34 @@ assert.deepEqual(
}
);
```

There is also an asynchronous version that parses in a different thread in the non-wasm builds of Rollup:

```js
import { parseAstAsync } from 'rollup/parseAst';
import assert from 'node:assert';

assert.deepEqual(
await parseAstAsync('return 42;', { allowReturnOutsideFunction: true }),
{
type: 'Program',
start: 0,
end: 10,
body: [
{
type: 'ReturnStatement',
start: 0,
end: 10,
argument: {
type: 'Literal',
start: 7,
end: 9,
raw: '42',
value: 42
}
}
],
sourceType: 'module'
}
);
```
1 change: 1 addition & 0 deletions native.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@
/* auto-generated by NAPI-RS */

export function parse(code: string, allowReturnOutsideFunction: boolean): Buffer
export function parseAsync(code: string, allowReturnOutsideFunction: boolean, signal?: AbortSignal | undefined | null): Promise<Buffer>
export function xxhashBase64Url(input: Uint8Array): string
3 changes: 2 additions & 1 deletion native.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,10 @@ If this is important to you, please consider supporting Rollup to make a native

const packageBase = imported.musl && isMusl() ? imported.musl : imported.base;
const localName = `./rollup.${packageBase}.node`;
const { parse, xxhashBase64Url } = require(
const { parse, parseAsync, xxhashBase64Url } = require(
existsSync(join(__dirname, localName)) ? localName : `@rollup/rollup-${packageBase}`
);

module.exports.parse = parse;
module.exports.parseAsync = parseAsync;
module.exports.xxhashBase64Url = xxhashBase64Url;
2 changes: 2 additions & 0 deletions native.wasm.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
const { parse, xxhashBase64Url } = require('./wasm-node/bindings_wasm.js');

exports.parse = parse;
exports.parseAsync = async (code, allowReturnOutsideFunction, _signal) =>
parse(code, allowReturnOutsideFunction);
exports.xxhashBase64Url = xxhashBase64Url;
34 changes: 34 additions & 0 deletions rust/bindings_napi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,45 @@ use parse_ast::parse_ast;
#[global_allocator]
static ALLOC: mimalloc_rust::GlobalMiMalloc = mimalloc_rust::GlobalMiMalloc;

pub struct ParseTask {
pub code: String,
pub allow_return_outside_function: bool,
}

#[napi]
impl Task for ParseTask {
type Output = Buffer;
type JsValue = Buffer;

fn compute(&mut self) -> Result<Self::Output> {
Ok(parse_ast(self.code.clone(), self.allow_return_outside_function).into())
}

fn resolve(&mut self, _env: Env, output: Self::Output) -> Result<Self::JsValue> {
Ok(output)
}
}

#[napi]
pub fn parse(code: String, allow_return_outside_function: bool) -> Buffer {
parse_ast(code, allow_return_outside_function).into()
}

#[napi]
pub fn parse_async(
code: String,
allow_return_outside_function: bool,
signal: Option<AbortSignal>,
) -> AsyncTask<ParseTask> {
AsyncTask::with_optional_signal(
ParseTask {
code,
allow_return_outside_function,
},
signal,
)
}

#[napi]
pub fn xxhash_base64_url(input: Uint8Array) -> String {
xxhash::xxhash_base64_url(&input)
Expand Down
16 changes: 12 additions & 4 deletions src/Module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ import {
logShimmedExport,
logSyntheticNamedExportsNeedNamespaceExport
} from './utils/logs';
import { parseAst } from './utils/parseAst';
import { parseAst, parseAstAsync } from './utils/parseAst';
import {
doAttributesDiffer,
getAttributesFromImportExportDeclaration
Expand Down 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 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
Original file line number Diff line number Diff line change
Expand Up @@ -317,10 +317,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
10 changes: 10 additions & 0 deletions src/rollup/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,16 @@ export type ParseAst = (
options?: { allowReturnOutsideFunction?: boolean }
) => AstNode;

// declare AbortSignal here for environments without DOM lib or @types/node
declare global {
interface AbortSignal {}
}

export type ParseAstAsync = (
input: string,
options?: { allowReturnOutsideFunction?: boolean; signal?: AbortSignal }
) => Promise<AstNode>;

export interface PluginContext extends MinimalPluginContext {
addWatchFile: (id: string) => void;
cache: PluginCache;
Expand Down
3 changes: 2 additions & 1 deletion src/utils/logs.ts
Original file line number Diff line number Diff line change
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
17 changes: 14 additions & 3 deletions src/utils/parseAst.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
import { parse } from '../../native';
import type { ParseAst } from '../rollup/types';
import { parse, parseAsync } from '../../native';
import type { ParseAst, ParseAstAsync } from '../rollup/types';
import { convertProgram } from './convert-ast';
import getReadStringFunction from './getReadStringFunction';

export const parseAst: ParseAst = (input, { allowReturnOutsideFunction = false } = {}) => {
const astBuffer = parse(input, allowReturnOutsideFunction);
const readString = getReadStringFunction(astBuffer);
return convertProgram(astBuffer.buffer, readString);
const result = convertProgram(astBuffer.buffer, readString);
return result;
};

export const parseAstAsync: ParseAstAsync = async (
input,
{ allowReturnOutsideFunction = false, signal } = {}
) => {
const astBuffer = await parseAsync(input, allowReturnOutsideFunction, signal);
const readString = getReadStringFunction(astBuffer);
const result = convertProgram(astBuffer.buffer, readString);
return result;
};
3 changes: 2 additions & 1 deletion src/utils/parseAstType.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { ParseAst } from '../rollup/types';
import type { ParseAst, ParseAstAsync } from '../rollup/types';

export const parseAst: ParseAst;
export const parseAstAsync: ParseAstAsync;
36 changes: 35 additions & 1 deletion test/misc/parse-ast.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const assert = require('node:assert');
const { parseAst } = require('../../dist/parseAst');
const { parseAst, parseAstAsync } = require('../../dist/parseAst');

describe('parseAst', () => {
it('parses an AST', async () => {
Expand Down Expand Up @@ -109,3 +109,37 @@ describe('parseAst', () => {
assert.ok(key !== value);
});
});

describe('parseAstAsync', () => {
it('parses an AST', async () => {
assert.deepStrictEqual(await parseAstAsync('console.log("ok")'), {
type: 'Program',
start: 0,
end: 17,
body: [
{
type: 'ExpressionStatement',
start: 0,
end: 17,
expression: {
type: 'CallExpression',
start: 0,
end: 17,
arguments: [{ type: 'Literal', start: 12, end: 16, raw: '"ok"', value: 'ok' }],
callee: {
type: 'MemberExpression',
start: 0,
end: 11,
computed: false,
object: { type: 'Identifier', start: 0, end: 7, name: 'console' },
optional: false,
property: { type: 'Identifier', start: 8, end: 11, name: 'log' }
},
optional: false
}
}
],
sourceType: 'module'
});
});
});