Skip to content

Commit c71c45b

Browse files
committed
Perf option: ascii happy path
1 parent 76c63a5 commit c71c45b

File tree

4 files changed

+189
-12
lines changed

4 files changed

+189
-12
lines changed

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ features = [
4747
"fast-rng" # Use a faster (but still sufficiently random) RNG
4848
]
4949

50+
[[bench]]
51+
name = "parse_bench"
52+
harness = false
53+
5054
[dev-dependencies]
5155
difference = "2"
5256
regex = "1.11.1"

benches/parse_bench.rs

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
use content_tag::{Options, Preprocessor};
2+
use std::time::Instant;
3+
4+
fn bench_parse(name: &str, src: &str, iterations: u32) -> f64 {
5+
// Warmup
6+
for _ in 0..100 {
7+
let p = Preprocessor::new();
8+
let _ = p.parse(src, Options::default());
9+
}
10+
11+
// Run 3 rounds, take the minimum
12+
let mut best = f64::MAX;
13+
for _ in 0..3 {
14+
let start = Instant::now();
15+
for _ in 0..iterations {
16+
let p = Preprocessor::new();
17+
let _ = p.parse(src, Options::default());
18+
}
19+
let elapsed = start.elapsed();
20+
let per_iter = elapsed.as_nanos() as f64 / iterations as f64;
21+
if per_iter < best {
22+
best = per_iter;
23+
}
24+
}
25+
26+
println!(
27+
"{:<55} {:>8.1}µs per parse ({} chars)",
28+
name,
29+
best / 1000.0,
30+
src.len(),
31+
);
32+
best / 1000.0
33+
}
34+
35+
fn main() {
36+
// Global warmup: run a few hundred parses to warm CPU caches
37+
// before any measured benchmarks.
38+
{
39+
let w = "import Component from '@glimmer/component';\nclass C extends Component { <template>hi</template> }";
40+
for _ in 0..500 {
41+
let p = Preprocessor::new();
42+
let _ = p.parse(w, Options::default());
43+
}
44+
}
45+
46+
// The same component is used as baseline across all tests.
47+
let base_component = r#"
48+
import Component from '@glimmer/component';
49+
class Comp extends Component {
50+
<template>
51+
<div class="container">
52+
<h1>{{this.title}}</h1>
53+
<p>{{this.description}}</p>
54+
</div>
55+
</template>
56+
}
57+
"#;
58+
59+
// =========================================================
60+
// Test 1: Scaling by number of templates
61+
// Same component repeated N times.
62+
// =========================================================
63+
println!("=== Scaling by template count ===\n");
64+
65+
for repeats in [1, 2, 5, 10, 20] {
66+
let src = base_component.repeat(repeats);
67+
bench_parse(
68+
&format!("{} templates ({} chars)", repeats, src.len()),
69+
&src,
70+
3000,
71+
);
72+
}
73+
74+
// =========================================================
75+
// Test 2: Scaling by template content size
76+
// Same component, but with extra rows inside the template.
77+
// =========================================================
78+
println!("\n=== Scaling by template content size ===\n");
79+
80+
let extra_row = " <div class=\"item\">{{this.value}}</div>\n";
81+
82+
// Baseline: the component as-is (0 extra rows)
83+
bench_parse(
84+
&format!("0 extra rows ({} chars)", base_component.len()),
85+
base_component,
86+
3000,
87+
);
88+
89+
for num_rows in [10, 50, 200] {
90+
let extra_content = extra_row.repeat(num_rows);
91+
let src = base_component.replace(
92+
" <p>{{this.description}}</p>",
93+
&format!(" <p>{{{{this.description}}}}</p>\n{}", extra_content),
94+
);
95+
bench_parse(
96+
&format!("{} extra rows inside template ({} chars)", num_rows, src.len()),
97+
&src,
98+
3000,
99+
);
100+
}
101+
102+
// =========================================================
103+
// Test 3: Scaling by JS code before the template
104+
// Same component, but with extra JS lines before it.
105+
// =========================================================
106+
println!("\n=== Scaling by JS code before template ===\n");
107+
108+
let extra_line = "const x = 'some padding code to increase byte offset';\n";
109+
110+
// Baseline: the component as-is (0 extra lines)
111+
bench_parse(
112+
&format!("0 extra lines ({} chars)", base_component.len()),
113+
base_component,
114+
3000,
115+
);
116+
117+
for num_lines in [10, 50, 200] {
118+
let prefix = extra_line.repeat(num_lines);
119+
let src = format!("{}{}", prefix, base_component);
120+
bench_parse(
121+
&format!(
122+
"{} extra JS lines before template ({} chars)",
123+
num_lines,
124+
src.len()
125+
),
126+
&src,
127+
3000,
128+
);
129+
}
130+
131+
// =========================================================
132+
// Test 4: Typical real-world files
133+
// =========================================================
134+
println!("\n=== Typical files ===\n");
135+
136+
let no_template = r#"
137+
import { tracked } from '@glimmer/tracking';
138+
import { action } from '@ember/object';
139+
import Service, { service } from '@ember/service';
140+
141+
export default class AuthService extends Service {
142+
@service declare session: any;
143+
@tracked count = 0;
144+
145+
@action
146+
increment() { this.count++; }
147+
148+
get doubled() { return this.count * 2; }
149+
}"#;
150+
151+
bench_parse(
152+
&format!("base component (1 template, {} chars)", base_component.len()),
153+
base_component,
154+
5000,
155+
);
156+
bench_parse("utility file (no template)", no_template, 5000);
157+
}

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ impl Preprocessor {
8888

8989
let mut visitor = locate::LocateContentTagVisitor {
9090
occurrences: Default::default(),
91+
is_ascii: src.is_ascii(),
9192
src: src.to_string(),
9293
};
9394

src/locate.rs

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use swc_ecma_visit::{Visit, VisitWith};
1010
pub struct LocateContentTagVisitor {
1111
pub occurrences: Vec<Occurrence>,
1212
pub src: String,
13+
pub is_ascii: bool,
1314
}
1415

1516
#[derive(Eq, PartialEq, Debug, Serialize)]
@@ -32,10 +33,10 @@ impl LocateContentTagVisitor {
3233
kind,
3334
tag_name: "template".to_owned(),
3435
contents: contents.value.to_string(),
35-
range: Range::new(&self.src, span),
36-
start_range: Range::new(&self.src, &opening.span),
37-
content_range: Range::new(&self.src, &contents.span),
38-
end_range: Range::new(&self.src, &closing.span),
36+
range: Range::new(&self.src, span, self.is_ascii),
37+
start_range: Range::new(&self.src, &opening.span, self.is_ascii),
38+
content_range: Range::new(&self.src, &contents.span, self.is_ascii),
39+
end_range: Range::new(&self.src, &closing.span, self.is_ascii),
3940
};
4041

4142
self.occurrences.push(occurrence);
@@ -108,14 +109,28 @@ pub struct Range {
108109
end_utf16_codepoint: usize,
109110
}
110111
impl Range {
111-
pub fn new(src: &str, span: &Span) -> Range {
112-
Range {
113-
start_byte: span.lo.0 as usize - 1,
114-
end_byte: span.hi.0 as usize - 1,
115-
start_char: src[..span.lo.0 as usize - 1].chars().count(),
116-
end_char: src[..span.hi.0 as usize - 1].chars().count(),
117-
start_utf16_codepoint: src[..span.lo.0 as usize - 1].encode_utf16().count(),
118-
end_utf16_codepoint: src[..span.hi.0 as usize - 1].encode_utf16().count(),
112+
pub fn new(src: &str, span: &Span, is_ascii: bool) -> Range {
113+
let start_byte = span.lo.0 as usize - 1;
114+
let end_byte = span.hi.0 as usize - 1;
115+
if is_ascii {
116+
// For ASCII sources, byte/char/utf16 offsets are all identical.
117+
Range {
118+
start_byte,
119+
end_byte,
120+
start_char: start_byte,
121+
end_char: end_byte,
122+
start_utf16_codepoint: start_byte,
123+
end_utf16_codepoint: end_byte,
124+
}
125+
} else {
126+
Range {
127+
start_byte,
128+
end_byte,
129+
start_char: src[..start_byte].chars().count(),
130+
end_char: src[..end_byte].chars().count(),
131+
start_utf16_codepoint: src[..start_byte].encode_utf16().count(),
132+
end_utf16_codepoint: src[..end_byte].encode_utf16().count(),
133+
}
119134
}
120135
}
121136
}

0 commit comments

Comments
 (0)