Skip to content

Commit 14c9662

Browse files
authored
Improve performance by directly constructing AST from buffer (#5391)
* Use consistent module resolution * Fix number literal casing * Store esTreeNode of import expression argument on ImportExpression * Move ReadString type * Pre-generate keys for iteration * Separate parsing and node construction * Add missing AST nodes and improve some names * Add additional Node keys and adapt some logic This prepares for the new buffer parsing * Initial version of bufferParsers TODO: Special logic in parseNode of some nodes * Use absolute positions This should make future template logic easier * Move annotation flags to the annotated nodes * Move invalid annotation handling to Program * Improve expression * Make parse and panic errors proper nodes * Directly consume buffer We still need to update generate-buffer-parsers with the provided changes. * Provide correct scopes to nodes * Add post-processing steps * Move static flag to first position * Add remaining customizations * Create new perf script that measures against node_modules rollup * Run form tests twice to improve coverage The second run uses the cache * Avoid unnecessary cache generation * Do not generate cache on CLI unless requested * Keep AST if cache is not explicitly disabled * Test panic errors for synchronous parsing * Do not run the leak test locally on npm test This does not work on non-x86 architectures * Test custom AST * Remove unnecessary conditional * Add descriptions for all relevant test categories
1 parent 5493159 commit 14c9662

File tree

100 files changed

+2375
-823
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

100 files changed

+2375
-823
lines changed

.eslintrc.js

+1
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ module.exports = {
151151
'unicorn/no-null': 'off',
152152
'unicorn/no-this-assignment': 'off',
153153
'unicorn/no-useless-undefined': 'off',
154+
'unicorn/number-literal-case': 'off',
154155
'unicorn/prefer-at': 'off',
155156
'unicorn/prefer-code-point': 'off',
156157
'unicorn/prefer-math-trunc': 'off',

CONTRIBUTING.md

+26
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,32 @@ npm run test:quick
5656

5757
Note that this does not run the browser tests and a few CLI tests will fail.
5858

59+
### How to write tests
60+
61+
For any new feature or bug fix, sufficient test coverage is crucial.
62+
63+
Note that Rollup does not really have unit tests, only the external APIs are tested with the full Rollup build. While this may seem unusual, the tests are still very stable and fast. This provides us with the ability to perform major refactorings of the code base while ensuring full compatibility with the previous versions.
64+
65+
There are different test categories. Most of these tests are directory-based where you have a directory with a `_config.js` file that contains the test description and configuration and several code files. See [/test/types.d.ts](./test/types.d.ts) for a full list of available test configuration options for all directory based test types. By default, unless specified otherwise, the `main.js` file is the entry point for the test. To run the tests in an IDE, configure a ["Mocha" compatible test runner](https://mochajs.org/#editor-plugins) that uses `test/test.js` as the entry point.
66+
67+
- **[`test/function`](./test/function/samples)**: These tests bundle to CommonJS and then run the entry point provided by `main.js`. The `assert` function from `node:assert` is injected as a global variable, so you can make inline assertions in the code. You can also use the `exports` configuration key to make assertions on the exported values. These are very stable and meaningful tests and should be your first choice for new tests.
68+
- For regression testing when Rollup produces invalid code or crashes
69+
- For testing plugin interactions. To do so, import `node:assert` in your `_config.js` file and make assertions in your plugin hooks as needed.
70+
- For testing expected bundling errors, warnings and logs (use the `error`, `generateError`, `warnings` and `logs` configuration keys)
71+
- For asserting on the generated bundle object (use the `bundle` configuration key)
72+
- **[`test/form`](./test/form/samples)**: These tests bundle to all output formats and do not run the code. They compare the bundled code against an `_expected` directory that contains the output for all formats. If the format is not important, you can specify an `_expected.js` file instead, which will be compared against the output when bundling to ES module format.
73+
- For testing tree-shaking
74+
- For testing code that does not run on all supported NodeJS platforms
75+
- **[`test/chunking-form`](./test/chunking-form/samples)**: Similar to the `form` tests, these tests support multiple output files and assets. Instead of a single file, there is a directory for each output format.
76+
- **[`test/cli`](./test/cli/samples)**: These tests run the Rollup CLI with a given configuration. They can compare the generated files against provided files and make assertions on stderr output. They can also optionally run the generated files.
77+
- **[`test/watch`](./test/watch)**: Test that watch mode works as expected. These tests are actually in the `index.js` file and only use the `samples` directory for input files.
78+
- **[`test/browser`](./test/browser/samples)**: These tests bundle with the browser build of Rollup. They compare the output to an `_expected` directory and allow to make assertions on bundling errors. Note that you need to provide all input files via plugins.
79+
- **[`test/sourcemaps`](./test/sourcemaps/samples)**: Tests to make assertions on the generated sourcemaps.
80+
- **[`test/incremental`](./test/incremental)**: For testing the caching behaviour of Rollup. As these tests need to run Rollup more than once, it was not easily possible to implement them as directory-based tests.
81+
- **[`test/file-hashes`](./test/file-hashes/samples)**: Relevant for testing that different outputs have different file hashes. With the new hashing algorithm, these tests are not as important as they used to be and are kept mostly for historical reasons.
82+
- **[`test/hooks`](./test/hooks)**: Do not add new tests here. These tests were the original tests for the plugin interface. For new tests, `function` tests are preferred as they are much easier to maintain.
83+
- **[`test/misc`](./test/misc)**: General tests that do not fit into the other categories.
84+
5985
### Developing with the website
6086

6187
Running

cli/run/index.ts

+6
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,12 @@ export default async function runRollup(command: Record<string, any>): Promise<v
6161
const { options, warnings } = await getConfigs(command);
6262
try {
6363
for (const inputOptions of options) {
64+
if (!inputOptions.cache) {
65+
// We explicitly disable the cache when unused as the CLI will not
66+
// use the cache object on the bundle when not in watch mode. This
67+
// improves performance as the cache is not generated.
68+
inputOptions.cache = false;
69+
}
6470
await build(inputOptions, warnings, command.silent);
6571
}
6672
if (command.failAfterWarnings && warnings.warningOccurred) {

package.json

+2-3
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,7 @@
6363
"lint:markdown:nofix": "prettier --check \"**/*.md\"",
6464
"lint:rust": "cd rust && cargo fmt && cargo clippy --fix --allow-dirty",
6565
"lint:rust:nofix": "cd rust && cargo fmt --check && cargo clippy",
66-
"perf": "npm run build:cjs && node --expose-gc scripts/perf.js",
67-
"perf:init": "node scripts/perf-init.js",
66+
"perf": "npm run build && node --expose-gc scripts/perf.js",
6867
"prepare": "husky && node scripts/check-release.js || npm run build:prepare",
6968
"prepublishOnly": "node scripts/check-release.js && node scripts/prepublish.js",
7069
"postpublish": "node scripts/postpublish.js",
@@ -75,7 +74,7 @@
7574
"test:update-snapshots": "node scripts/update-snapshots.js",
7675
"test:cjs": "npm run build:cjs && npm run test:only",
7776
"test:quick": "mocha -b test/test.js",
78-
"test:all": "concurrently --kill-others-on-fail -c green,blue,magenta,cyan,red 'npm:test:only' 'npm:test:browser' 'npm:test:typescript' 'npm:test:leak' 'npm:test:package' 'npm:test:options'",
77+
"test:all": "concurrently --kill-others-on-fail -c green,blue,magenta,cyan,red 'npm:test:only' 'npm:test:browser' 'npm:test:typescript' 'npm:test:package' 'npm:test:options'",
7978
"test:coverage": "npm run build:cjs && shx rm -rf coverage/* && nyc --reporter html mocha test/test.js",
8079
"test:coverage:browser": "npm run build && shx rm -rf coverage/* && nyc mocha test/browser/index.js",
8180
"test:leak": "node --expose-gc test/leak/index.js",

rust/parse_ast/src/convert_ast/converter.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -2397,7 +2397,7 @@ impl<'a> AstConverter<'a> {
23972397
self.add_explicit_end(end_position, self.code.len() as u32);
23982398
// annotations, these need to come after end so that trailing comments are
23992399
// included
2400-
self.update_reference_position(end_position + PROGRAM_ANNOTATIONS_OFFSET);
2400+
self.update_reference_position(end_position + PROGRAM_INVALID_ANNOTATIONS_OFFSET);
24012401
self.index_converter.invalidate_collected_annotations();
24022402
let invalid_annotations = self.index_converter.take_invalid_annotations();
24032403
self.convert_item_list(&invalid_annotations, |ast_converter, annotation| {

rust/parse_ast/src/convert_ast/converter/ast_constants.rs

+11-7
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
// This file is generated by scripts/generate-ast-converters.js.
22
// Do not edit this file directly.
33

4-
pub const TYPE_PARSE_ERROR: [u8; 4] = 0u32.to_ne_bytes();
5-
pub const TYPE_PANIC_ERROR: [u8; 4] = 1u32.to_ne_bytes();
4+
pub const TYPE_PANIC_ERROR_INLINED_MESSAGE: [u8; 4] = 0u32.to_ne_bytes();
5+
pub const TYPE_PARSE_ERROR_INLINED_MESSAGE: [u8; 4] = 1u32.to_ne_bytes();
66
pub const TYPE_ARRAY_EXPRESSION_INLINED_ELEMENTS: [u8; 4] = 2u32.to_ne_bytes();
77
pub const TYPE_ARRAY_PATTERN_INLINED_ELEMENTS: [u8; 4] = 3u32.to_ne_bytes();
88
pub const TYPE_ARROW_FUNCTION_EXPRESSION_INLINED_ANNOTATIONS: [u8; 4] = 4u32.to_ne_bytes();
@@ -81,6 +81,10 @@ pub const TYPE_VARIABLE_DECLARATOR_INLINED_ID: [u8; 4] = 76u32.to_ne_bytes();
8181
pub const TYPE_WHILE_STATEMENT_INLINED_TEST: [u8; 4] = 77u32.to_ne_bytes();
8282
pub const TYPE_YIELD_EXPRESSION: [u8; 4] = 78u32.to_ne_bytes();
8383

84+
pub const PANIC_ERROR_RESERVED_BYTES: usize = 4;
85+
86+
pub const PARSE_ERROR_RESERVED_BYTES: usize = 4;
87+
8488
pub const ARRAY_EXPRESSION_RESERVED_BYTES: usize = 4;
8589

8690
pub const ARRAY_PATTERN_RESERVED_BYTES: usize = 4;
@@ -245,8 +249,8 @@ pub const META_PROPERTY_PROPERTY_OFFSET: usize = 4;
245249

246250
pub const METHOD_DEFINITION_RESERVED_BYTES: usize = 16;
247251
pub const METHOD_DEFINITION_FLAGS_OFFSET: usize = 4;
248-
pub const METHOD_DEFINITION_COMPUTED_FLAG: u32 = 1;
249-
pub const METHOD_DEFINITION_STATIC_FLAG: u32 = 2;
252+
pub const METHOD_DEFINITION_STATIC_FLAG: u32 = 1;
253+
pub const METHOD_DEFINITION_COMPUTED_FLAG: u32 = 2;
250254
pub const METHOD_DEFINITION_VALUE_OFFSET: usize = 8;
251255
pub const METHOD_DEFINITION_KIND_OFFSET: usize = 12;
252256

@@ -261,7 +265,7 @@ pub const OBJECT_PATTERN_RESERVED_BYTES: usize = 4;
261265
pub const PRIVATE_IDENTIFIER_RESERVED_BYTES: usize = 4;
262266

263267
pub const PROGRAM_RESERVED_BYTES: usize = 8;
264-
pub const PROGRAM_ANNOTATIONS_OFFSET: usize = 4;
268+
pub const PROGRAM_INVALID_ANNOTATIONS_OFFSET: usize = 4;
265269

266270
pub const PROPERTY_RESERVED_BYTES: usize = 20;
267271
pub const PROPERTY_FLAGS_OFFSET: usize = 4;
@@ -274,8 +278,8 @@ pub const PROPERTY_KIND_OFFSET: usize = 16;
274278

275279
pub const PROPERTY_DEFINITION_RESERVED_BYTES: usize = 12;
276280
pub const PROPERTY_DEFINITION_FLAGS_OFFSET: usize = 4;
277-
pub const PROPERTY_DEFINITION_COMPUTED_FLAG: u32 = 1;
278-
pub const PROPERTY_DEFINITION_STATIC_FLAG: u32 = 2;
281+
pub const PROPERTY_DEFINITION_STATIC_FLAG: u32 = 1;
282+
pub const PROPERTY_DEFINITION_COMPUTED_FLAG: u32 = 2;
279283
pub const PROPERTY_DEFINITION_VALUE_OFFSET: usize = 8;
280284

281285
pub const REST_ELEMENT_RESERVED_BYTES: usize = 4;

rust/parse_ast/src/error_emit.rs

+12-5
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ use parking_lot::Mutex;
55
use swc_common::errors::{DiagnosticBuilder, Emitter, Handler, Level, HANDLER};
66
use swc_ecma_ast::Program;
77

8-
use crate::convert_ast::converter::{ast_constants::TYPE_PARSE_ERROR, convert_string};
8+
use crate::convert_ast::converter::{
9+
ast_constants::{PARSE_ERROR_RESERVED_BYTES, TYPE_PARSE_ERROR_INLINED_MESSAGE},
10+
convert_string,
11+
};
912

1013
#[derive(Clone, Default)]
1114
struct Writer(Arc<Mutex<Vec<u8>>>);
@@ -65,9 +68,9 @@ where
6568
}
6669

6770
fn create_error_buffer(wr: &Writer, code: &str) -> Vec<u8> {
68-
let mut buffer = TYPE_PARSE_ERROR.to_vec();
71+
let mut buffer = TYPE_PARSE_ERROR_INLINED_MESSAGE.to_vec();
6972
let mut lock = wr.0.lock();
70-
let mut error_buffer = take(&mut *lock);
73+
let error_buffer = take(&mut *lock);
7174
let pos = u32::from_ne_bytes(error_buffer[0..4].try_into().unwrap());
7275
let mut utf_16_pos: u32 = 0;
7376
for (utf_8_pos, char) in code.char_indices() {
@@ -76,7 +79,11 @@ fn create_error_buffer(wr: &Writer, code: &str) -> Vec<u8> {
7679
}
7780
utf_16_pos += char.len_utf16() as u32;
7881
}
79-
error_buffer[0..4].copy_from_slice(&utf_16_pos.to_ne_bytes());
80-
buffer.extend_from_slice(&error_buffer);
82+
// start
83+
buffer.extend_from_slice(&utf_16_pos.to_ne_bytes());
84+
// end
85+
buffer.resize(buffer.len() + PARSE_ERROR_RESERVED_BYTES, 0);
86+
// message
87+
buffer.extend_from_slice(&error_buffer[4..]);
8188
buffer
8289
}

rust/parse_ast/src/lib.rs

+17-16
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
use std::panic::{catch_unwind, AssertUnwindSafe};
22

3-
use convert_ast::converter::ast_constants::TYPE_PANIC_ERROR;
3+
use convert_ast::converter::ast_constants::{
4+
PANIC_ERROR_RESERVED_BYTES, TYPE_PANIC_ERROR_INLINED_MESSAGE,
5+
};
46
use convert_ast::converter::{convert_string, AstConverter};
57
use swc_common::sync::Lrc;
68
use swc_common::{FileName, FilePathMapping, Globals, SourceMap, GLOBALS};
@@ -52,20 +54,19 @@ pub fn parse_ast(code: String, allow_return_outside_function: bool) -> Vec<u8> {
5254
}
5355
}
5456
}));
55-
match result {
56-
Ok(buffer) => buffer,
57-
Err(err) => {
58-
let msg = if let Some(msg) = err.downcast_ref::<&str>() {
59-
msg
60-
} else if let Some(msg) = err.downcast_ref::<String>() {
61-
msg
62-
} else {
63-
"Unknown rust panic message"
64-
};
65-
let mut buffer = TYPE_PANIC_ERROR.to_vec();
66-
convert_string(&mut buffer, msg);
67-
buffer
68-
}
69-
}
57+
result.unwrap_or_else(|err| {
58+
let msg = if let Some(msg) = err.downcast_ref::<&str>() {
59+
msg
60+
} else if let Some(msg) = err.downcast_ref::<String>() {
61+
msg
62+
} else {
63+
"Unknown rust panic message"
64+
};
65+
let mut buffer = TYPE_PANIC_ERROR_INLINED_MESSAGE.to_vec();
66+
// reserve for start and end even though they are unused
67+
buffer.resize(buffer.len() + 4 + PANIC_ERROR_RESERVED_BYTES, 0);
68+
convert_string(&mut buffer, msg);
69+
buffer
70+
})
7071
})
7172
}

0 commit comments

Comments
 (0)