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:
overlookmotel committed 2026-11-01 22:10:34 +00:00
1 parent f56aac570f
commit 82007bd941
9 files changed
+185 -429

No files matched your search

-49
View File
@@ -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
View File
@@ -1855,7 +1855,6 @@ dependencies = [
"oxc_estree_tokens",
"oxc_formatter",
"oxc_isolated_declarations",
"oxc_lexer",
"oxc_linter",
"oxc_mangler",
"oxc_minifier",
-31
View File
@@ -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.
+1 -28
View File
@@ -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",
-17
View File
@@ -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.
+184 -4
View File
@@ -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);
-188
View File
@@ -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);