mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
ci(lexer): remove oxc_lexer benchmark (#28664)
Remove the wallclock benchmark for `oxc_lexer`. It's not accurate enough to be useful.
This commit is contained in:
1 parent
f56aac570f
commit
82007bd941
9 files changed
+185
-429
No files matched your search
@@ -754,55 +754,6 @@ jobs:
|
||||
{ echo "::error::Expected ${{ matrix.variant }}, but the harness reported otherwise"; exit 1; }
|
||||
git diff --exit-code
|
||||
|
||||
lexer-benchmark:
|
||||
# Benchmark `oxc_lexer`'s AVX2 implementation against the lexer in `oxc_parser`, built without the AVX2 flags
|
||||
# (see `lexer` job above).
|
||||
#
|
||||
# Measures wallclock time, so doesn't use CodSpeed. `oxc_lexer` is designed to exploit out-of-order execution
|
||||
# and branch prediction, which CodSpeed's CPU simulation does not model.
|
||||
# Results are only printed in the log. The "change" reported for `oxc_lexer` is relative to `oxc_parser`.
|
||||
#
|
||||
# Only runs when `oxc_lexer` or the benchmark itself changes.
|
||||
# See the LEXER section of `justfile` for the recipe.
|
||||
name: Lexer Benchmark
|
||||
runs-on: namespace-profile-linux-x64-default
|
||||
steps:
|
||||
- uses: taiki-e/checkout-action@7d1e50e93dc4fb3bba58f85018fadf77898aee8b # v1.4.2
|
||||
|
||||
- name: Check if lexer benchmark should run
|
||||
id: filter
|
||||
uses: $/.github/actions/check-changes
|
||||
with:
|
||||
paths: crates/oxc_lexer/,tasks/benchmark/benches/lexer_wallclock_old.rs,tasks/benchmark/benches/lexer_wallclock_new.rs,tasks/benchmark/benches/lexer_common.rs,.cargo/lexer-avx2.toml,justfile,.github/workflows/ci.yml
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
pr-number: ${{ github.event.pull_request.number }}
|
||||
|
||||
- uses: namespacelabs/nscloud-cache-action@1124a6f3ce44e5cf84cc22111530961f4d2a15f9 # v1.7.0
|
||||
if: steps.filter.outputs.changed == 'true'
|
||||
with:
|
||||
path: /home/runner/.rustup
|
||||
cache: rust
|
||||
|
||||
- uses: taiki-e/install-action@f7e5d7c961414b23f5b25b2da9294395d08513ad # v2.87.26
|
||||
if: steps.filter.outputs.changed == 'true'
|
||||
with:
|
||||
tool: just
|
||||
|
||||
- name: Benchmark
|
||||
# The `oxc_lexer` benchmark prints which implementation it was built with (`oxc_lexer::IMPLEMENTATION`).
|
||||
# Checking it here is what guarantees this job benchmarked the AVX2 implementation.
|
||||
# `just bench-lexer-avx2` builds the `oxc_parser` benchmark without the AVX2 flags (not checked here).
|
||||
if: steps.filter.outputs.changed == 'true'
|
||||
env:
|
||||
# Keep criterion's saved results out of `target`, which may be cached.
|
||||
# Otherwise criterion would also compare `oxc_parser` against an earlier CI run.
|
||||
CRITERION_HOME: ${{ runner.temp }}/criterion
|
||||
run: |
|
||||
set -o pipefail
|
||||
just bench-lexer-avx2 | tee "$RUNNER_TEMP/bench.log"
|
||||
grep -qxF "Lexer implementation: AVX2" "$RUNNER_TEMP/bench.log" ||
|
||||
{ echo "::error::Expected AVX2, but the benchmark reported otherwise"; exit 1; }
|
||||
|
||||
test-codegen:
|
||||
name: Test Codegen
|
||||
runs-on: namespace-profile-linux-x64-default
|
||||
|
||||
Generated
-1
@@ -1855,7 +1855,6 @@ dependencies = [
|
||||
"oxc_estree_tokens",
|
||||
"oxc_formatter",
|
||||
"oxc_isolated_declarations",
|
||||
"oxc_lexer",
|
||||
"oxc_linter",
|
||||
"oxc_mangler",
|
||||
"oxc_minifier",
|
||||
|
||||
@@ -222,37 +222,6 @@ lint-lexer-avx2 *args='':
|
||||
lint-lexer-generic *args='':
|
||||
just lint-lexer {{_lexer-generic}} {{args}}
|
||||
|
||||
# The lexer benchmarks measure wallclock time. Unlike the other benchmarks, they don't run on CodSpeed.
|
||||
#
|
||||
# `oxc_parser`'s lexer is always built without the AVX2 flags, so it's benchmarked in a separate build
|
||||
# from `oxc_lexer`. Its results are saved as criterion baseline `lexer_old`, and then `oxc_lexer`
|
||||
# is compared against that baseline.
|
||||
# The "change" which criterion reports for `oxc_lexer` is its time relative to `oxc_parser`'s.
|
||||
# The "change" reported for `oxc_parser` (if any) is relative to its previous run.
|
||||
#
|
||||
# Both benchmarks are built before either is run, so the 2 runs happen back-to-back,
|
||||
# without a build between them.
|
||||
#
|
||||
# `args` are passed to criterion, e.g. `just bench-lexer App.tsx` to run only that file.
|
||||
# The recipes set the baseline options themselves, so don't pass those.
|
||||
#
|
||||
# On an ARM host, `bench-lexer-avx2` compares `oxc_parser` running natively against `oxc_lexer`
|
||||
# running under emulation, so the comparison is meaningless.
|
||||
_bench-lexer := "cargo bench -p oxc_benchmark --no-default-features --features lexer_wallclock --bench"
|
||||
|
||||
# Benchmark `oxc_lexer`, built for the host, against `oxc_parser`'s lexer
|
||||
bench-lexer *args='':
|
||||
{{_bench-lexer}} lexer_wallclock_old --bench lexer_wallclock_new --no-run
|
||||
{{_bench-lexer}} lexer_wallclock_old -- --save-baseline lexer_old {{args}}
|
||||
{{_bench-lexer}} lexer_wallclock_new -- --baseline lexer_old {{args}}
|
||||
|
||||
# Benchmark `oxc_lexer`'s AVX2 implementation against `oxc_parser`'s lexer
|
||||
bench-lexer-avx2 *args='':
|
||||
{{_bench-lexer}} lexer_wallclock_old --no-run
|
||||
{{_bench-lexer}} lexer_wallclock_new {{_lexer-avx2}} --no-run
|
||||
{{_bench-lexer}} lexer_wallclock_old -- --save-baseline lexer_old {{args}}
|
||||
{{_bench-lexer}} lexer_wallclock_new {{_lexer-avx2}} -- --baseline lexer_old {{args}}
|
||||
|
||||
# `ready-lexer` fails if any step fails, or if conformance changes the lexer snapshots.
|
||||
# Only the lexer snapshots are checked for changes, so it can be run with other uncommitted changes.
|
||||
# Snapshots are checked after each conformance run, because each run overwrites the snapshots from the one before.
|
||||
|
||||
@@ -9,9 +9,6 @@ license.workspace = true
|
||||
publish = false
|
||||
repository.workspace = true
|
||||
description.workspace = true
|
||||
# Every bench is declared below. Without this, Cargo would also treat `benches/lexer_common.rs`
|
||||
# (code shared by the lexer benches) as a benchmark.
|
||||
autobenches = false
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -25,17 +22,6 @@ doctest = false
|
||||
name = "lexer"
|
||||
harness = false
|
||||
|
||||
# Wallclock only - not run on CodSpeed. Built separately from each other. See `just bench-lexer`.
|
||||
[[bench]]
|
||||
name = "lexer_wallclock_old"
|
||||
harness = false
|
||||
required-features = ["lexer_wallclock"]
|
||||
|
||||
[[bench]]
|
||||
name = "lexer_wallclock_new"
|
||||
harness = false
|
||||
required-features = ["lexer_wallclock"]
|
||||
|
||||
[[bench]]
|
||||
name = "parser"
|
||||
harness = false
|
||||
@@ -90,7 +76,6 @@ oxc_codegen = { workspace = true, optional = true, features = ["sourcemap"] }
|
||||
oxc_estree_tokens = { workspace = true, optional = true }
|
||||
oxc_formatter = { workspace = true, optional = true }
|
||||
oxc_isolated_declarations = { workspace = true, optional = true }
|
||||
oxc_lexer = { workspace = true, optional = true }
|
||||
oxc_linter = { workspace = true, optional = true }
|
||||
oxc_mangler = { workspace = true, optional = true }
|
||||
oxc_minifier = { workspace = true, optional = true }
|
||||
@@ -106,7 +91,7 @@ oxc_transformer = { workspace = true, optional = true }
|
||||
|
||||
criterion2 = { workspace = true }
|
||||
|
||||
# Only for lexer benchmarks
|
||||
# Only for lexer benchmark
|
||||
cow-utils = { workspace = true, optional = true }
|
||||
|
||||
# Only for NAPI benchmark
|
||||
@@ -156,17 +141,6 @@ react_compiler = [
|
||||
"dep:oxc_span",
|
||||
"dep:oxc_tasks_common",
|
||||
]
|
||||
# `oxc_lexer` vs `oxc_parser`'s lexer, wallclock. Not part of "compiler", so CodSpeed doesn't build it.
|
||||
lexer_wallclock = [
|
||||
"dep:oxc_allocator",
|
||||
"dep:oxc_ast",
|
||||
"dep:oxc_ast_visit",
|
||||
"dep:oxc_lexer",
|
||||
"dep:oxc_parser",
|
||||
"dep:oxc_span",
|
||||
"dep:oxc_tasks_common",
|
||||
"dep:cow-utils",
|
||||
]
|
||||
|
||||
[dev-dependencies]
|
||||
rustc-hash = { workspace = true }
|
||||
@@ -181,7 +155,6 @@ ignored = [
|
||||
"oxc_estree_tokens",
|
||||
"oxc_formatter",
|
||||
"oxc_isolated_declarations",
|
||||
"oxc_lexer",
|
||||
"oxc_linter",
|
||||
"oxc_mangler",
|
||||
"oxc_minifier",
|
||||
|
||||
@@ -11,20 +11,3 @@ git checkout main
|
||||
cargo bench -p oxc_benchmark --bench parser -- --save-baseline main
|
||||
critcmp # via `cargo binstall critcmp`
|
||||
```
|
||||
|
||||
## Lexer
|
||||
|
||||
`oxc_lexer` is benchmarked head-to-head against the lexer in `oxc_parser`, measuring wallclock time.
|
||||
This benchmark does not run on CodSpeed, because its CPU simulation does not model the out-of-order
|
||||
execution and branch prediction which `oxc_lexer` is designed to exploit.
|
||||
|
||||
```sh
|
||||
just bench-lexer # Built for the host (Neon on ARM, generic on x86_64)
|
||||
just bench-lexer-avx2 # AVX2 (x86_64 only - on ARM Mac, it runs under emulation)
|
||||
```
|
||||
|
||||
`oxc_parser` is always built without the AVX2 flags, so the 2 lexers are benchmarked in separate builds.
|
||||
The "change" which criterion reports for `oxc_lexer` is its time relative to `oxc_parser`'s.
|
||||
The "change" reported for `oxc_parser` (if any) is relative to its previous run.
|
||||
|
||||
CI runs `just bench-lexer-avx2` when `oxc_lexer` changes, and prints the results in the job log.
|
||||
@@ -1,10 +1,18 @@
|
||||
mod lexer_common;
|
||||
use std::borrow::Cow;
|
||||
|
||||
use cow_utils::CowUtils;
|
||||
|
||||
use oxc_allocator::Allocator;
|
||||
use oxc_ast::ast::*;
|
||||
use oxc_ast_visit::Visit;
|
||||
use oxc_benchmark::{BenchmarkId, Criterion, criterion_group, criterion_main};
|
||||
use oxc_parser::config::NoTokensLexerConfig;
|
||||
|
||||
use lexer_common::{cleaned_test_files, lex_whole_file};
|
||||
use oxc_parser::{
|
||||
Parser,
|
||||
config::{LexerConfig, NoTokensLexerConfig},
|
||||
lexer::{Kind, Lexer},
|
||||
};
|
||||
use oxc_span::SourceType;
|
||||
use oxc_tasks_common::{TestFile, TestFiles};
|
||||
|
||||
fn bench_lexer(criterion: &mut Criterion) {
|
||||
let mut group = criterion.benchmark_group("lexer");
|
||||
@@ -27,5 +35,177 @@ fn bench_lexer(criterion: &mut Criterion) {
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Get the test files, with source text cleaned.
|
||||
///
|
||||
/// The lexer lacks awareness of JS grammar, so it gets confused by a few things
|
||||
/// without the parser driving it. So simplify the input for it, by replacing these syntaxes
|
||||
/// with plain strings. This ensures lexing completes without generating any errors,
|
||||
/// which is more realistic.
|
||||
///
|
||||
/// It's unfortunate that the benchmark doesn't exercise the code paths for these syntaxes,
|
||||
/// but this is the closest we can get to a realistic benchmark of the lexer in isolation.
|
||||
fn cleaned_test_files() -> Vec<TestFile> {
|
||||
let mut allocator = Allocator::default();
|
||||
|
||||
TestFiles::minimal()
|
||||
.files()
|
||||
.iter()
|
||||
.map(|file| {
|
||||
let source_type = file.source_type;
|
||||
let source_text = clean(&file.source_text, source_type, &allocator);
|
||||
|
||||
allocator.reset();
|
||||
|
||||
TestFile {
|
||||
url: file.url.clone(),
|
||||
file_name: file.file_name.clone(),
|
||||
source_text,
|
||||
source_type,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
// `#[inline(always)]` to ensure this is inlined into benchmark.
|
||||
// It's also used in `SourceCleaner` below.
|
||||
#[expect(clippy::inline_always)]
|
||||
#[inline(always)]
|
||||
fn lex_whole_file<'a, C: LexerConfig>(
|
||||
allocator: &'a Allocator,
|
||||
source_text: &'a str,
|
||||
source_type: SourceType,
|
||||
config: C,
|
||||
) -> Lexer<'a, C> {
|
||||
let mut lexer = Lexer::new_for_benchmarks(allocator, source_text, source_type, config);
|
||||
if lexer.first_token().kind() != Kind::Eof {
|
||||
// Use `next_token_for_benchmarks` instead of `next_token`, to work around problem
|
||||
// where `next_token` wasn't inlined here.
|
||||
// `next_token_for_benchmarks` is identical to `next_token`, but is marked `#[inline(always)]`.
|
||||
while lexer.next_token_for_benchmarks().kind() != Kind::Eof {}
|
||||
}
|
||||
lexer
|
||||
}
|
||||
|
||||
/// Clean source text.
|
||||
///
|
||||
/// Purpose is to allow lexer to complete without any errors.
|
||||
/// Usually sources Oxc is asked to parse will not produce lexer errors, and generating diagnostics is
|
||||
/// fairly expensive, so is unrealistic for benchmarking purposes.
|
||||
///
|
||||
/// Certain syntax will parse without error, but the lexer alone does not have the context to understand
|
||||
/// they're fine. Notably this includes syntax where the lexer only consumes the first character and
|
||||
/// parser would then call back into lexer to complete the job.
|
||||
///
|
||||
/// So replace these syntaxes with strings so that lexer can complete without error:
|
||||
/// * `RegExpLiteral`
|
||||
/// * `TemplateLiteral`
|
||||
/// * `TSTemplateLiteralType` (TypeScript type-level template literal, lexically identical
|
||||
/// to `TemplateLiteral` so the lexer cannot tell them apart without parser context)
|
||||
/// * `JSXText`
|
||||
fn clean<'a>(source_text: &'a str, source_type: SourceType, allocator: &'a Allocator) -> String {
|
||||
// Parse
|
||||
let parser_ret = Parser::new(allocator, source_text, source_type).parse();
|
||||
assert!(parser_ret.diagnostics.is_empty());
|
||||
let program = parser_ret.program;
|
||||
|
||||
// Visit AST and compile list of replacements
|
||||
let mut cleaner = SourceCleaner::new(source_text);
|
||||
cleaner.visit_program(&program);
|
||||
let mut replacements = cleaner.replacements;
|
||||
|
||||
// Make replacements
|
||||
replacements.sort_unstable_by_key(|replacement| replacement.span);
|
||||
|
||||
let mut clean_source_text = String::with_capacity(cleaner.clean_source_text_len);
|
||||
let mut last_index = 0;
|
||||
for Replacement { span, text } in replacements {
|
||||
clean_source_text.push_str(&source_text[last_index..span.start as usize]);
|
||||
clean_source_text.push_str(&text);
|
||||
last_index = span.end as usize;
|
||||
}
|
||||
clean_source_text.push_str(&source_text[last_index..]);
|
||||
|
||||
// Check lexer can lex it without any errors
|
||||
let lexer = lex_whole_file(allocator, &clean_source_text, source_type, NoTokensLexerConfig);
|
||||
assert!(lexer.errors().is_empty());
|
||||
|
||||
clean_source_text
|
||||
}
|
||||
|
||||
struct SourceCleaner<'a> {
|
||||
source_text: &'a str,
|
||||
replacements: Vec<Replacement>,
|
||||
clean_source_text_len: usize,
|
||||
}
|
||||
|
||||
struct Replacement {
|
||||
span: Span,
|
||||
text: String,
|
||||
}
|
||||
|
||||
impl<'a> SourceCleaner<'a> {
|
||||
fn new(source_text: &'a str) -> Self {
|
||||
Self { source_text, replacements: vec![], clean_source_text_len: source_text.len() }
|
||||
}
|
||||
|
||||
fn replace(&mut self, span: Span, text: String) {
|
||||
self.clean_source_text_len += text.len() - span.size() as usize;
|
||||
self.replacements.push(Replacement { span, text });
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Visit<'a> for SourceCleaner<'a> {
|
||||
fn visit_reg_exp_literal(&mut self, regexp: &RegExpLiteral<'a>) {
|
||||
let pattern_text = regexp.regex.pattern.text.as_str();
|
||||
let span = Span::sized(regexp.span.start, u32::try_from(pattern_text.len()).unwrap() + 2);
|
||||
let text = convert_to_string(pattern_text);
|
||||
self.replace(span, text);
|
||||
}
|
||||
|
||||
fn visit_template_literal(&mut self, lit: &TemplateLiteral<'a>) {
|
||||
let span = lit.span;
|
||||
let text = span.shrink(1).source_text(self.source_text);
|
||||
let text = convert_to_string(&text.cow_replace('\n', " "));
|
||||
self.replace(span, text);
|
||||
}
|
||||
|
||||
fn visit_ts_template_literal_type(&mut self, lit: &TSTemplateLiteralType<'a>) {
|
||||
// TS type-level template literals (e.g. `${T}-${U}` in conditional/mapped types) are
|
||||
// lexically identical to value-level template literals. Without parser context, the
|
||||
// bench-mode lexer can swallow large spans as a single `TemplateHead` when adjacent
|
||||
// type templates are separated by other type syntax. Replace with a string just like
|
||||
// `visit_template_literal` does.
|
||||
let span = lit.span;
|
||||
let text = span.shrink(1).source_text(self.source_text);
|
||||
let text = convert_to_string(&text.cow_replace('\n', " "));
|
||||
self.replace(span, text);
|
||||
}
|
||||
|
||||
fn visit_jsx_text(&mut self, jsx_text: &JSXText<'a>) {
|
||||
let span = jsx_text.span;
|
||||
let text = span.source_text(self.source_text);
|
||||
let text = convert_to_string(&text.cow_replace('\n', " "));
|
||||
self.replace(span, text);
|
||||
}
|
||||
}
|
||||
|
||||
#[expect(clippy::naive_bytecount)]
|
||||
fn convert_to_string(text: &str) -> String {
|
||||
let single_quote_count = text.as_bytes().iter().filter(|&&b| b == b'\'').count();
|
||||
let double_quote_count = text.as_bytes().iter().filter(|&&b| b == b'"').count();
|
||||
|
||||
let (quote, other_quote) =
|
||||
if single_quote_count <= double_quote_count { ('\'', "\"") } else { ('"', "'") };
|
||||
|
||||
#[expect(clippy::disallowed_methods)]
|
||||
let text = if single_quote_count == 0 || double_quote_count == 0 {
|
||||
Cow::Borrowed(text)
|
||||
} else {
|
||||
Cow::Owned(text.replace(quote, other_quote))
|
||||
};
|
||||
|
||||
format!("{quote}{text}{quote}")
|
||||
}
|
||||
|
||||
criterion_group!(lexer, bench_lexer);
|
||||
criterion_main!(lexer);
|
||||
@@ -1,188 +0,0 @@
|
||||
//! Code shared by the lexer benchmarks.
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use cow_utils::CowUtils;
|
||||
|
||||
use oxc_allocator::Allocator;
|
||||
use oxc_ast::ast::*;
|
||||
use oxc_ast_visit::Visit;
|
||||
use oxc_parser::{
|
||||
Parser,
|
||||
config::{LexerConfig, NoTokensLexerConfig},
|
||||
lexer::{Kind, Lexer},
|
||||
};
|
||||
use oxc_span::SourceType;
|
||||
use oxc_tasks_common::{TestFile, TestFiles};
|
||||
|
||||
/// Get the test files, with source text cleaned.
|
||||
///
|
||||
/// `oxc_parser`'s lexer lacks awareness of JS grammar, so it gets confused by a few things
|
||||
/// without the parser driving it. So simplify the input for it, by replacing these syntaxes
|
||||
/// with plain strings. This ensures lexing completes without generating any errors,
|
||||
/// which is more realistic.
|
||||
///
|
||||
/// It's unfortunate that the benchmarks don't exercise the code paths for these syntaxes,
|
||||
/// but this is the closest we can get to a realistic benchmark of `oxc_parser`'s lexer in isolation.
|
||||
pub fn cleaned_test_files() -> Vec<TestFile> {
|
||||
let mut allocator = Allocator::default();
|
||||
|
||||
TestFiles::minimal()
|
||||
.files()
|
||||
.iter()
|
||||
.map(|file| {
|
||||
let source_type = file.source_type;
|
||||
let source_text = clean(&file.source_text, source_type, &allocator);
|
||||
|
||||
allocator.reset();
|
||||
|
||||
TestFile {
|
||||
url: file.url.clone(),
|
||||
file_name: file.file_name.clone(),
|
||||
source_text,
|
||||
source_type,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
// `#[inline(always)]` to ensure this is inlined into benchmark.
|
||||
// It's also used in `SourceCleaner` below.
|
||||
#[expect(clippy::inline_always)]
|
||||
#[inline(always)]
|
||||
pub fn lex_whole_file<'a, C: LexerConfig>(
|
||||
allocator: &'a Allocator,
|
||||
source_text: &'a str,
|
||||
source_type: SourceType,
|
||||
config: C,
|
||||
) -> Lexer<'a, C> {
|
||||
let mut lexer = Lexer::new_for_benchmarks(allocator, source_text, source_type, config);
|
||||
if lexer.first_token().kind() != Kind::Eof {
|
||||
// Use `next_token_for_benchmarks` instead of `next_token`, to work around problem
|
||||
// where `next_token` wasn't inlined here.
|
||||
// `next_token_for_benchmarks` is identical to `next_token`, but is marked `#[inline(always)]`.
|
||||
while lexer.next_token_for_benchmarks().kind() != Kind::Eof {}
|
||||
}
|
||||
lexer
|
||||
}
|
||||
|
||||
/// Clean source text.
|
||||
///
|
||||
/// Purpose is to allow lexer to complete without any errors.
|
||||
/// Usually sources Oxc is asked to parse will not produce lexer errors, and generating diagnostics is
|
||||
/// fairly expensive, so is unrealistic for benchmarking purposes.
|
||||
///
|
||||
/// Certain syntax will parse without error, but the lexer alone does not have the context to understand
|
||||
/// they're fine. Notably this includes syntax where the lexer only consumes the first character and
|
||||
/// parser would then call back into lexer to complete the job.
|
||||
///
|
||||
/// So replace these syntaxes with strings so that lexer can complete without error:
|
||||
/// * `RegExpLiteral`
|
||||
/// * `TemplateLiteral`
|
||||
/// * `TSTemplateLiteralType` (TypeScript type-level template literal, lexically identical
|
||||
/// to `TemplateLiteral` so the lexer cannot tell them apart without parser context)
|
||||
/// * `JSXText`
|
||||
fn clean<'a>(source_text: &'a str, source_type: SourceType, allocator: &'a Allocator) -> String {
|
||||
// Parse
|
||||
let parser_ret = Parser::new(allocator, source_text, source_type).parse();
|
||||
assert!(parser_ret.diagnostics.is_empty());
|
||||
let program = parser_ret.program;
|
||||
|
||||
// Visit AST and compile list of replacements
|
||||
let mut cleaner = SourceCleaner::new(source_text);
|
||||
cleaner.visit_program(&program);
|
||||
let mut replacements = cleaner.replacements;
|
||||
|
||||
// Make replacements
|
||||
replacements.sort_unstable_by_key(|replacement| replacement.span);
|
||||
|
||||
let mut clean_source_text = String::with_capacity(cleaner.clean_source_text_len);
|
||||
let mut last_index = 0;
|
||||
for Replacement { span, text } in replacements {
|
||||
clean_source_text.push_str(&source_text[last_index..span.start as usize]);
|
||||
clean_source_text.push_str(&text);
|
||||
last_index = span.end as usize;
|
||||
}
|
||||
clean_source_text.push_str(&source_text[last_index..]);
|
||||
|
||||
// Check lexer can lex it without any errors
|
||||
let lexer = lex_whole_file(allocator, &clean_source_text, source_type, NoTokensLexerConfig);
|
||||
assert!(lexer.errors().is_empty());
|
||||
|
||||
clean_source_text
|
||||
}
|
||||
|
||||
struct SourceCleaner<'a> {
|
||||
source_text: &'a str,
|
||||
replacements: Vec<Replacement>,
|
||||
clean_source_text_len: usize,
|
||||
}
|
||||
|
||||
struct Replacement {
|
||||
span: Span,
|
||||
text: String,
|
||||
}
|
||||
|
||||
impl<'a> SourceCleaner<'a> {
|
||||
fn new(source_text: &'a str) -> Self {
|
||||
Self { source_text, replacements: vec![], clean_source_text_len: source_text.len() }
|
||||
}
|
||||
|
||||
fn replace(&mut self, span: Span, text: String) {
|
||||
self.clean_source_text_len += text.len() - span.size() as usize;
|
||||
self.replacements.push(Replacement { span, text });
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Visit<'a> for SourceCleaner<'a> {
|
||||
fn visit_reg_exp_literal(&mut self, regexp: &RegExpLiteral<'a>) {
|
||||
let pattern_text = regexp.regex.pattern.text.as_str();
|
||||
let span = Span::sized(regexp.span.start, u32::try_from(pattern_text.len()).unwrap() + 2);
|
||||
let text = convert_to_string(pattern_text);
|
||||
self.replace(span, text);
|
||||
}
|
||||
|
||||
fn visit_template_literal(&mut self, lit: &TemplateLiteral<'a>) {
|
||||
let span = lit.span;
|
||||
let text = span.shrink(1).source_text(self.source_text);
|
||||
let text = convert_to_string(&text.cow_replace('\n', " "));
|
||||
self.replace(span, text);
|
||||
}
|
||||
|
||||
fn visit_ts_template_literal_type(&mut self, lit: &TSTemplateLiteralType<'a>) {
|
||||
// TS type-level template literals (e.g. `${T}-${U}` in conditional/mapped types) are
|
||||
// lexically identical to value-level template literals. Without parser context, the
|
||||
// bench-mode lexer can swallow large spans as a single `TemplateHead` when adjacent
|
||||
// type templates are separated by other type syntax. Replace with a string just like
|
||||
// `visit_template_literal` does.
|
||||
let span = lit.span;
|
||||
let text = span.shrink(1).source_text(self.source_text);
|
||||
let text = convert_to_string(&text.cow_replace('\n', " "));
|
||||
self.replace(span, text);
|
||||
}
|
||||
|
||||
fn visit_jsx_text(&mut self, jsx_text: &JSXText<'a>) {
|
||||
let span = jsx_text.span;
|
||||
let text = span.source_text(self.source_text);
|
||||
let text = convert_to_string(&text.cow_replace('\n', " "));
|
||||
self.replace(span, text);
|
||||
}
|
||||
}
|
||||
|
||||
#[expect(clippy::naive_bytecount)]
|
||||
fn convert_to_string(text: &str) -> String {
|
||||
let single_quote_count = text.as_bytes().iter().filter(|&&b| b == b'\'').count();
|
||||
let double_quote_count = text.as_bytes().iter().filter(|&&b| b == b'"').count();
|
||||
|
||||
let (quote, other_quote) =
|
||||
if single_quote_count <= double_quote_count { ('\'', "\"") } else { ('"', "'") };
|
||||
|
||||
#[expect(clippy::disallowed_methods)]
|
||||
let text = if single_quote_count == 0 || double_quote_count == 0 {
|
||||
Cow::Borrowed(text)
|
||||
} else {
|
||||
Cow::Owned(text.replace(quote, other_quote))
|
||||
};
|
||||
|
||||
format!("{quote}{text}{quote}")
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
//! Wallclock benchmark of `oxc_lexer` against the lexer in `oxc_parser`, head-to-head.
|
||||
//!
|
||||
//! Not run on CodSpeed. `oxc_lexer` is designed to exploit out-of-order execution and branch prediction,
|
||||
//! which CodSpeed's CPU simulation does not model. So these benchmarks measure wallclock time instead.
|
||||
//!
|
||||
//! `oxc_lexer` compiles to its AVX2, Neon or generic implementation depending on target features,
|
||||
//! which apply to the whole build. So the 2 lexers are benchmarked in separate builds:
|
||||
//!
|
||||
//! 1. `lexer_wallclock_old` benchmarks `oxc_parser`'s lexer, built without the AVX2 target features,
|
||||
//! and saves the results as criterion baseline `lexer_old`.
|
||||
//! 2. This benchmark benchmarks `oxc_lexer` (any implementation), and compares it against that baseline.
|
||||
//! The "change" which criterion reports is `oxc_lexer`'s time relative to `oxc_parser`'s.
|
||||
//!
|
||||
//! When saving the baseline, criterion also compares `oxc_parser` against its previous run, if there is one.
|
||||
//! So the "change" reported for `oxc_parser` is relative to its previous run.
|
||||
//!
|
||||
//! Both lexers lex the same cleaned source text, so the contest is fair.
|
||||
//! See `cleaned_test_files` for why the source text needs cleaning.
|
||||
//!
|
||||
//! Run with `just bench-lexer` (built for the host) or `just bench-lexer-avx2` (AVX2 implementation).
|
||||
//! CI runs `just bench-lexer-avx2` when `oxc_lexer` changes, and prints the results in the job log.
|
||||
|
||||
mod lexer_common;
|
||||
|
||||
use oxc_benchmark::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
|
||||
use oxc_lexer::{LexOptions, PAD};
|
||||
|
||||
use lexer_common::cleaned_test_files;
|
||||
|
||||
#[expect(clippy::print_stdout)]
|
||||
fn bench_lexer_wallclock_new(criterion: &mut Criterion) {
|
||||
// Report which implementation of `oxc_lexer` was built.
|
||||
// CI checks this line to make sure the build it expected is the build it got.
|
||||
println!("Lexer implementation: {}", oxc_lexer::IMPLEMENTATION);
|
||||
|
||||
// Group name and benchmark IDs must match `lexer_wallclock_old`, so criterion can compare the two
|
||||
let mut group = criterion.benchmark_group("lexer_wallclock");
|
||||
|
||||
for file in cleaned_test_files() {
|
||||
let source_text = file.source_text.as_str();
|
||||
let source_type = file.source_type;
|
||||
let len = u32::try_from(source_text.len()).unwrap();
|
||||
|
||||
// `oxc_lexer` requires source text to be followed by `PAD` zeroed bytes.
|
||||
// The caller will be required to supply source text with padding, so don't include it in benchmark.
|
||||
let mut padded = Vec::with_capacity(source_text.len() + PAD);
|
||||
padded.extend_from_slice(source_text.as_bytes());
|
||||
padded.resize(source_text.len() + PAD, 0);
|
||||
|
||||
let options = LexOptions {
|
||||
source_type_module: source_type.is_module(),
|
||||
jsx: source_type.is_jsx(),
|
||||
ts: source_type.is_typescript(),
|
||||
..LexOptions::default()
|
||||
};
|
||||
|
||||
// Lex once outside the benchmark, to check `oxc_lexer` completes without errors,
|
||||
// and to get an `Arena` to reuse.
|
||||
// Reusing it is the equivalent of `oxc_parser`'s benchmark reusing its allocator.
|
||||
let (result, mut arena) = oxc_lexer::lex_utf8(&padded, len, options);
|
||||
assert!(result.diagnostics().is_empty());
|
||||
|
||||
group.throughput(Throughput::Bytes(u64::from(len)));
|
||||
group.bench_function(BenchmarkId::from_parameter(&file.file_name), |b| {
|
||||
b.iter(|| oxc_lexer::lex_utf8_arena(&padded, len, options, &mut arena));
|
||||
});
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group!(lexer_wallclock_new, bench_lexer_wallclock_new);
|
||||
criterion_main!(lexer_wallclock_new);
|
||||
@@ -1,38 +0,0 @@
|
||||
//! Wallclock benchmark of the lexer in `oxc_parser`, as the baseline for `lexer_wallclock_new`.
|
||||
//!
|
||||
//! See `lexer_wallclock_new.rs` for how the 2 benchmarks fit together.
|
||||
|
||||
mod lexer_common;
|
||||
|
||||
use oxc_allocator::Allocator;
|
||||
use oxc_benchmark::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
|
||||
use oxc_parser::config::NoTokensLexerConfig;
|
||||
|
||||
use lexer_common::{cleaned_test_files, lex_whole_file};
|
||||
|
||||
fn bench_lexer_wallclock_old(criterion: &mut Criterion) {
|
||||
// Group name and benchmark IDs must match `lexer_wallclock_new`, so criterion can compare the two
|
||||
let mut group = criterion.benchmark_group("lexer_wallclock");
|
||||
|
||||
for file in cleaned_test_files() {
|
||||
let source_text = file.source_text.as_str();
|
||||
let source_type = file.source_type;
|
||||
|
||||
group.throughput(Throughput::Bytes(u64::try_from(source_text.len()).unwrap()));
|
||||
group.bench_function(BenchmarkId::from_parameter(&file.file_name), |b| {
|
||||
// Do not include initializing allocator in benchmark.
|
||||
// User code would likely reuse the same allocator over and over to parse multiple files,
|
||||
// so we do the same here.
|
||||
let mut allocator = Allocator::default();
|
||||
b.iter(|| {
|
||||
lex_whole_file(&allocator, source_text, source_type, NoTokensLexerConfig);
|
||||
allocator.reset();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group!(lexer_wallclock_old, bench_lexer_wallclock_old);
|
||||
criterion_main!(lexer_wallclock_old);
|
||||
Reference in new issue
Block a user