Skip to content

Commit d4a1fa7

Browse files
committed
Add balanced line wrapping for binary operators, profiler, specify
continuation line definition Close #270
1 parent 924f87f commit d4a1fa7

23 files changed

Lines changed: 889 additions & 422 deletions

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,22 @@
22

33
This file documents the changes made to the formatter with each release.
44

5+
## Unreleased: 0.22.0
6+
7+
### Added
8+
9+
- Added a profiling option to the benchmark script compatible with the samply profiler
10+
11+
### Changed
12+
13+
- Implemented new balanced line wrapping algorithm for long chains of expressions, including operations and boolean expressions
14+
- Specified the definition for continuation lines and changed function calls and other statements and expressions to use a single extra indent instead of two by default
15+
- Always force lambda functions to have a line return after the function declaration, like regular functions
16+
17+
### Fixed
18+
19+
- Conditions that exceed max-line-length by only a few columns get backslash + attribute-dot wrapping instead of the parenthesized and-wrap used for larger overflows (#270)
20+
521
## Release 0.21.0 (2026-07-16)
622

723
### Added

Cargo.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,10 @@ path = "src/main.rs"
6767
[profile.release]
6868
strip = true
6969
lto = true
70+
71+
# For the profiler, we want optimizations but also debug symbols so we can see
72+
# function names in the profiler UI.
73+
[profile.profiling]
74+
inherits = "release"
75+
debug = true
76+
strip = false

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,27 @@ To run the formatter's test suite, use this command:
402402
cargo test
403403
```
404404

405+
### Benchmarking and profiling
406+
407+
Run the timing benchmark to compare total formatter runtime between revisions:
408+
409+
```bash
410+
cargo run --bin benchmark --release
411+
```
412+
413+
The output result is the median time of multiple batches of formatter runs over the same files.
414+
415+
To profile the run and see which functions use the most CPU time, I use the sampling profiler [samply](https://github.com/mstange/samply). Install it like this, then I've prepared a script to run the formatter while running the profiler:
416+
417+
```bash
418+
cargo install --locked samply
419+
./benchmarks/profile.sh
420+
```
421+
422+
Samply allows you to view the profiling results in Firefox's profiler UI.
423+
424+
The script runs 5000 iterations by default, but you can pass a different count as an argument, for example `./benchmarks/profile.sh 10000`.
425+
405426
### Debugging and visualizing the tree structure
406427

407428
You can use the tree-sitter command line tool to parse GDScript files and visualize the syntax tree. This shows you the structure that the tree-sitter parser generates, which you can then use to write formatting rules:

benchmarks/profile.sh

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#!/bin/sh
2+
set -e
3+
4+
# Read first cli argument and default to 5000 otherwise.
5+
iterations="${1:-5000}"
6+
cargo build --profile profiling --bin benchmark
7+
exec samply record target/profiling/benchmark --profile-long "$iterations"

src/bin/benchmark.rs

Lines changed: 141 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@
33
//!
44
//! Run cargo run --bin benchmark --release to compile and run the benchmark.
55
//! You can use it in a shell script to compare performance between two git revisions.
6+
//! To profile the CPU usage of the benchmark, run:
7+
//!
8+
//! ```sh
9+
//! cargo build --profile profiling --bin benchmark
10+
//! samply record target/profiling/benchmark --profile-long
11+
//! ```
612
//!
713
//! For example, to compare between this commit and the previous one:
814
//!
@@ -14,110 +20,168 @@
1420
//! git checkout -
1521
//! ```
1622
use gdscript_formatter::{FormatterConfiguration, RenderElement, format_gdscript_with_buffers};
17-
use std::{fs, time::Instant};
23+
use std::{
24+
env, fs,
25+
hint::black_box,
26+
time::{Duration, Instant},
27+
};
1828

19-
const ITERATIONS: u16 = 40;
20-
21-
fn main() -> Result<(), Box<dyn std::error::Error>> {
22-
let short_content = fs::read_to_string("benchmarks/gdscript_files/short.gd")?;
23-
let long_content = fs::read_to_string("benchmarks/gdscript_files/long.gd")?;
24-
let config = FormatterConfiguration::default();
29+
const PROFILE_ITERATIONS: usize = 5_000;
30+
const WARMUP_DURATION: Duration = Duration::from_millis(200);
31+
const SAMPLE_DURATION: Duration = Duration::from_millis(200);
32+
const SAMPLE_COUNT: usize = 10;
2533

26-
println!("Running GDScript Formatter Benchmark...");
34+
/// A struct that stores data and has functions to benchmark the GDScript
35+
/// formatter and measure performance.
36+
struct BenchmarkRunner {
37+
render_elements: Vec<RenderElement>,
38+
output: String,
39+
}
2740

28-
let mut render_elements: Vec<RenderElement> = Vec::new();
29-
let mut output = String::new();
41+
struct BenchmarkMeasurement {
42+
median_seconds_per_iteration: f64,
43+
total_iterations: usize,
44+
}
3045

31-
println!("Running short file warmup (10 iterations)");
32-
for _ in 0..10 {
33-
format_gdscript_with_buffers(&short_content, &config, &mut render_elements, &mut output)?;
46+
impl BenchmarkRunner {
47+
fn new() -> Self {
48+
Self {
49+
render_elements: Vec::new(),
50+
output: String::new(),
51+
}
3452
}
3553

36-
println!("Benchmarking short file ({} iterations)", ITERATIONS);
37-
let mut start = Instant::now();
38-
for _ in 0..ITERATIONS {
39-
format_gdscript_with_buffers(&short_content, &config, &mut render_elements, &mut output)?;
54+
fn format(&mut self, source: &str, config: &FormatterConfiguration) -> Result<(), String> {
55+
format_gdscript_with_buffers(
56+
black_box(source),
57+
black_box(config),
58+
&mut self.render_elements,
59+
&mut self.output,
60+
)?;
61+
black_box(&self.output);
62+
Ok(())
4063
}
41-
let duration_short_file = start.elapsed();
4264

43-
println!("Benchmarking long file ({} iterations)...", ITERATIONS);
44-
start = Instant::now();
45-
for _ in 0..ITERATIONS {
46-
format_gdscript_with_buffers(&long_content, &config, &mut render_elements, &mut output)?;
47-
}
48-
let long_time = start.elapsed();
65+
fn measure(
66+
&mut self,
67+
source: &str,
68+
config: &FormatterConfiguration,
69+
) -> Result<BenchmarkMeasurement, String> {
70+
let warmup_start = Instant::now();
71+
while warmup_start.elapsed() < WARMUP_DURATION {
72+
self.format(source, config)?;
73+
}
4974

50-
let safe_config = FormatterConfiguration {
51-
safe: true,
52-
..config
53-
};
75+
let mut seconds_per_iteration = Vec::with_capacity(SAMPLE_COUNT);
76+
let mut total_iterations = 0;
77+
let mut sample_index = 0;
78+
while sample_index < SAMPLE_COUNT {
79+
let sample_start = Instant::now();
80+
let mut sample_iterations = 0;
81+
while sample_start.elapsed() < SAMPLE_DURATION {
82+
self.format(source, config)?;
83+
sample_iterations += 1;
84+
}
85+
let sample_seconds = sample_start.elapsed().as_secs_f64();
86+
seconds_per_iteration.push(sample_seconds / sample_iterations as f64);
87+
total_iterations += sample_iterations;
88+
sample_index += 1;
89+
}
5490

55-
println!(
56-
"Benchmarking short file with safe mode ({} iterations)...",
57-
ITERATIONS
58-
);
59-
start = Instant::now();
60-
for _ in 0..ITERATIONS {
61-
format_gdscript_with_buffers(
62-
&short_content,
63-
&safe_config,
64-
&mut render_elements,
65-
&mut output,
66-
)?;
91+
let mut current_index = 1;
92+
while current_index < seconds_per_iteration.len() {
93+
let mut sorted_index = current_index;
94+
while sorted_index > 0
95+
&& seconds_per_iteration[sorted_index] < seconds_per_iteration[sorted_index - 1]
96+
{
97+
seconds_per_iteration.swap(sorted_index, sorted_index - 1);
98+
sorted_index -= 1;
99+
}
100+
current_index += 1;
101+
}
102+
103+
Ok(BenchmarkMeasurement {
104+
median_seconds_per_iteration: seconds_per_iteration[SAMPLE_COUNT / 2],
105+
total_iterations,
106+
})
67107
}
68-
let duration_short_file_safe = start.elapsed();
108+
}
69109

70-
println!(
71-
"Benchmarking long file with safe mode ({} iterations)...",
72-
ITERATIONS
73-
);
74-
start = Instant::now();
75-
for _ in 0..ITERATIONS {
76-
format_gdscript_with_buffers(
77-
&long_content,
78-
&safe_config,
79-
&mut render_elements,
80-
&mut output,
81-
)?;
110+
fn main() -> Result<(), Box<dyn std::error::Error>> {
111+
let short_content = fs::read_to_string("benchmarks/gdscript_files/short.gd")?;
112+
let long_content = fs::read_to_string("benchmarks/gdscript_files/long.gd")?;
113+
let config = FormatterConfiguration::default();
114+
115+
let mut arguments = env::args();
116+
arguments.next();
117+
if let Some(argument) = arguments.next() {
118+
if argument != "--profile-long" {
119+
return Err(format!("Unknown benchmark argument: {argument}").into());
120+
}
121+
let iterations = if let Some(value) = arguments.next() {
122+
value.parse::<usize>()?
123+
} else {
124+
PROFILE_ITERATIONS
125+
};
126+
if arguments.next().is_some() {
127+
return Err("Usage: benchmark --profile-long [iterations]".into());
128+
}
129+
130+
let mut runner = BenchmarkRunner::new();
131+
for _ in 0..10 {
132+
runner.format(&long_content, &config)?;
133+
}
134+
println!("Profiling long GDScript file ({iterations} iterations)...");
135+
for _ in 0..iterations {
136+
runner.format(&long_content, &config)?;
137+
}
138+
return Ok(());
82139
}
83-
let long_time_safe = start.elapsed();
84140

85-
let average_time_short = duration_short_file.as_micros() as f64 / f64::from(ITERATIONS);
86-
let average_time_long = long_time.as_micros() as f64 / f64::from(ITERATIONS);
87-
let average_time_safe_short =
88-
duration_short_file_safe.as_micros() as f64 / f64::from(ITERATIONS);
89-
let average_time_safe_long = long_time_safe.as_micros() as f64 / f64::from(ITERATIONS);
141+
println!("Running GDScript Formatter Benchmark...");
142+
143+
let safe_config = FormatterConfiguration {
144+
safe: true,
145+
..config.clone()
146+
};
147+
let mut runner = BenchmarkRunner::new();
148+
println!("Benchmarking short file...");
149+
let short = runner.measure(&short_content, &config)?;
150+
println!("Benchmarking short file with safe mode...");
151+
let short_safe = runner.measure(&short_content, &safe_config)?;
152+
println!("Benchmarking long file...");
153+
let long = runner.measure(&long_content, &config)?;
154+
println!("Benchmarking long file with safe mode...");
155+
let long_safe = runner.measure(&long_content, &safe_config)?;
90156

91157
let short_slowdown =
92-
((average_time_safe_short - average_time_short) / average_time_short) * 100.0;
93-
let long_slowdown = ((average_time_safe_long - average_time_long) / average_time_long) * 100.0;
158+
(short_safe.median_seconds_per_iteration / short.median_seconds_per_iteration - 1.0)
159+
* 100.0;
160+
let long_slowdown =
161+
(long_safe.median_seconds_per_iteration / long.median_seconds_per_iteration - 1.0) * 100.0;
94162

95163
println!("\nBenchmark Results:");
96164
println!("=================");
97165
println!(
98-
"Short file ({} iterations): {:?} (avg: {:.2}ms per iteration)",
99-
ITERATIONS,
100-
duration_short_file,
101-
average_time_short / 1000.0
166+
"Short file ({} iterations): {:.3}ms median per iteration",
167+
short.total_iterations,
168+
short.median_seconds_per_iteration * 1000.0
102169
);
103170
println!(
104-
"Long file ({} iterations): {:?} (avg: {:.2}ms per iteration)",
105-
ITERATIONS,
106-
long_time,
107-
average_time_long / 1000.0
171+
"Long file ({} iterations): {:.3}ms median per iteration",
172+
long.total_iterations,
173+
long.median_seconds_per_iteration * 1000.0
108174
);
109175
println!(
110-
"Short file with safe mode ({} iterations): {:?} (avg: {:.2}ms per iteration, {:.1}% slower)",
111-
ITERATIONS,
112-
duration_short_file_safe,
113-
average_time_safe_short / 1000.0,
176+
"Short file with safe mode ({} iterations): {:.3}ms median per iteration, {:.1}% slower",
177+
short_safe.total_iterations,
178+
short_safe.median_seconds_per_iteration * 1000.0,
114179
short_slowdown
115180
);
116181
println!(
117-
"Long file with safe mode ({} iterations): {:?} (avg: {:.2}ms per iteration, {:.1}% slower)",
118-
ITERATIONS,
119-
long_time_safe,
120-
average_time_safe_long / 1000.0,
182+
"Long file with safe mode ({} iterations): {:.3}ms median per iteration, {:.1}% slower",
183+
long_safe.total_iterations,
184+
long_safe.median_seconds_per_iteration * 1000.0,
121185
long_slowdown
122186
);
123187

0 commit comments

Comments
 (0)