Skip to content

Comments

Make Buffer::write_element non-failable#6613

Merged
MichaReiser merged 1 commit intomainfrom
make-buffer-write-element-non-failable
Aug 16, 2023
Merged

Make Buffer::write_element non-failable#6613
MichaReiser merged 1 commit intomainfrom
make-buffer-write-element-non-failable

Conversation

@MichaReiser
Copy link
Member

@MichaReiser MichaReiser commented Aug 16, 2023

Summary

This PR changes the Buffer::write_element return type from FormatResult<()> to () (void).

Local benchmarks show that this improves performance significantly and the only possible error for write_element to fail is if Vec fails to allocate, which we don't need to handle.

There's one downside to this. The reason why Buffer::write_element is that a wrapper Buffer (see the removed PreambleBuffer) can intercept the writing of an element and write additional content.
This is a neat feature but wrapping Buffers (especially if done recursively, e.g. in every Expression) comes with a significant performance penalty because each wrapping requires a dynamic dispatch call. That's why Buffers haven't been used as much and are generally discouraged.

There's a more performant solution for preambles today that didn't exist when the Buffer was introduced.

  • Create a snapshot
  • write the preamble
  • perform a rollback if no content was written since the preamble

However, this doesn't work for use cases where the preamble itself is expensive OR for cases where an element should be written as a response to another element (e.g. write a line break after each "break_here" text).

Test Plan

cargo test

Gnuplot not found, using plotters backend
formatter/numpy/globals.py
                        time:   [38.464 µs 38.507 µs 38.553 µs]
                        thrpt:  [76.535 MiB/s 76.627 MiB/s 76.712 MiB/s]
                 change:
                        time:   [-4.3802% -4.2067% -4.0365%] (p = 0.00 < 0.05)
                        thrpt:  [+4.2063% +4.3914% +4.5809%]
                        Performance has improved.
Found 3 outliers among 100 measurements (3.00%)
  2 (2.00%) high mild
  1 (1.00%) high severe
formatter/pydantic/types.py
                        time:   [769.99 µs 770.45 µs 771.00 µs]
                        thrpt:  [33.078 MiB/s 33.102 MiB/s 33.121 MiB/s]
                 change:
                        time:   [-2.5591% -2.3013% -2.0730%] (p = 0.00 < 0.05)
                        thrpt:  [+2.1168% +2.3555% +2.6263%]
                        Performance has improved.
Found 7 outliers among 100 measurements (7.00%)
  4 (4.00%) high mild
  3 (3.00%) high severe
formatter/numpy/ctypeslib.py
                        time:   [378.12 µs 378.37 µs 378.65 µs]
                        thrpt:  [43.975 MiB/s 44.007 MiB/s 44.037 MiB/s]
                 change:
                        time:   [-2.0623% -1.7352% -1.4174%] (p = 0.00 < 0.05)
                        thrpt:  [+1.4378% +1.7659% +2.1057%]
                        Performance has improved.
Found 11 outliers among 100 measurements (11.00%)
  2 (2.00%) high mild
  9 (9.00%) high severe
Benchmarking formatter/large/dataset.py: Warming up for 3.0000 s
Warning: Unable to complete 100 samples in 5.0s. You may wish to increase target time to 9.7s, enable flat sampling, or reduce sample count to 50.
formatter/large/dataset.py
                        time:   [1.9180 ms 1.9197 ms 1.9216 ms]
                        thrpt:  [21.171 MiB/s 21.192 MiB/s 21.211 MiB/s]
                 change:
                        time:   [-2.0365% -1.8731% -1.7127%] (p = 0.00 < 0.05)
                        thrpt:  [+1.7426% +1.9089% +2.0788%]
                        Performance has improved.
Found 4 outliers among 100 measurements (4.00%)
  4 (4.00%) high mild

@MichaReiser
Copy link
Member Author

Current dependencies on/for this PR:

This comment was auto-generated by Graphite.

@MichaReiser MichaReiser added performance Potential performance improvement formatter Related to the formatter labels Aug 16, 2023
@github-actions
Copy link
Contributor

PR Check Results

Benchmark

Linux

group                                      main                                   pr
-----                                      ----                                   --
formatter/large/dataset.py                 1.03      3.7±0.02ms    11.0 MB/sec    1.00      3.6±0.03ms    11.3 MB/sec
formatter/numpy/ctypeslib.py               1.01    759.4±3.85µs    21.9 MB/sec    1.00    749.5±3.47µs    22.2 MB/sec
formatter/numpy/globals.py                 1.00     78.6±0.34µs    37.6 MB/sec    1.02     80.2±0.39µs    36.8 MB/sec
formatter/pydantic/types.py                1.02   1498.3±8.70µs    17.0 MB/sec    1.00  1467.2±23.91µs    17.4 MB/sec
linter/all-rules/large/dataset.py          1.00     10.3±0.04ms     4.0 MB/sec    1.00     10.3±0.30ms     3.9 MB/sec
linter/all-rules/numpy/ctypeslib.py        1.00      2.8±0.00ms     6.0 MB/sec    1.00      2.8±0.01ms     6.0 MB/sec
linter/all-rules/numpy/globals.py          1.00    393.2±0.45µs     7.5 MB/sec    1.00    393.4±1.03µs     7.5 MB/sec
linter/all-rules/pydantic/types.py         1.00      5.3±0.02ms     4.8 MB/sec    1.00      5.3±0.03ms     4.8 MB/sec
linter/default-rules/large/dataset.py      1.00      5.5±0.01ms     7.4 MB/sec    1.00      5.4±0.01ms     7.5 MB/sec
linter/default-rules/numpy/ctypeslib.py    1.00   1207.5±1.35µs    13.8 MB/sec    1.00   1204.5±4.99µs    13.8 MB/sec
linter/default-rules/numpy/globals.py      1.00    141.6±0.28µs    20.8 MB/sec    1.00    141.6±0.39µs    20.8 MB/sec
linter/default-rules/pydantic/types.py     1.01      2.5±0.01ms    10.3 MB/sec    1.00      2.5±0.01ms    10.4 MB/sec

Windows

group                                      main                                   pr
-----                                      ----                                   --
formatter/large/dataset.py                 1.01      5.0±0.17ms     8.2 MB/sec    1.00      4.9±0.18ms     8.2 MB/sec
formatter/numpy/ctypeslib.py               1.02  1008.0±54.99µs    16.5 MB/sec    1.00   992.6±44.39µs    16.8 MB/sec
formatter/numpy/globals.py                 1.03    104.0±4.68µs    28.4 MB/sec    1.00    101.4±6.64µs    29.1 MB/sec
formatter/pydantic/types.py                1.00      2.0±0.07ms    12.7 MB/sec    1.00  1997.4±78.68µs    12.8 MB/sec
linter/all-rules/large/dataset.py          1.00     15.5±0.26ms     2.6 MB/sec    1.00     15.5±0.26ms     2.6 MB/sec
linter/all-rules/numpy/ctypeslib.py        1.01      4.2±0.14ms     3.9 MB/sec    1.00      4.2±0.08ms     4.0 MB/sec
linter/all-rules/numpy/globals.py          1.01   535.6±21.51µs     5.5 MB/sec    1.00   530.6±18.95µs     5.6 MB/sec
linter/all-rules/pydantic/types.py         1.00      8.0±0.17ms     3.2 MB/sec    1.00      8.0±0.17ms     3.2 MB/sec
linter/default-rules/large/dataset.py      1.00      8.4±0.14ms     4.8 MB/sec    1.00      8.4±0.11ms     4.8 MB/sec
linter/default-rules/numpy/ctypeslib.py    1.00  1790.2±43.80µs     9.3 MB/sec    1.00  1786.0±36.92µs     9.3 MB/sec
linter/default-rules/numpy/globals.py      1.01    212.7±7.15µs    13.9 MB/sec    1.00   211.4±12.05µs    14.0 MB/sec
linter/default-rules/pydantic/types.py     1.00      3.8±0.10ms     6.8 MB/sec    1.00      3.8±0.09ms     6.8 MB/sec

Copy link
Member

@charliermarsh charliermarsh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! I'm surprised this is so expensive, did it surprise you or did you have intuition here?

@MichaReiser
Copy link
Member Author

MichaReiser commented Aug 16, 2023

Nice! I'm surprised this is so expensive, did it surprise you or did you have intuition here?

I had some intuition:

  • Each ? introduces a new branch. I don't know how many the branch predictor can predict correctly
  • All our basic builders use write_element and we emit a lot of source_position, text and group elements.
  • I can't find the blog post right now but the try operator (used to) can be slower than some hand written macros that perform the same (using an early return) . That's why I suspected that removing some of the hot branches may help performance.

I'm mainly surprised that we can't see the same results on our benchmark runner.

@MichaReiser MichaReiser merged commit daac31d into main Aug 16, 2023
@MichaReiser MichaReiser deleted the make-buffer-write-element-non-failable branch August 16, 2023 13:13
@charliermarsh
Copy link
Member

I can't find the blog post right now but the try operator (used to) can be slower than some hand written macros that perform the same (using an early return) . That's why I suspected that removing some of the hot branches may help performance.

Ah yeah, I've seen this somewhere, maybe it's just an issue in the Rust repo...

graphite-app bot pushed a commit to oxc-project/oxc that referenced this pull request Nov 25, 2025
This PR is a follow-up to astral-sh/ruff#6613.

### Why can we do it

https://github.com/oxc-project/oxc/blob/622e000934d97ade5588f1e8cc3badde96e7328e/crates/oxc_formatter/src/formatter/diagnostics.rs#L11-L29

We can also make it non-failable since the entire program only has two points that can trigger an error, neither of which needs to interrupt the program immediately, and both can be improved to avoid making FormatResult everywhere

1. `FormatError::PoorLayout`

This error may occur only when handling call arguments, and only the first or the last argument is a `Function` or `ArrowFunctionExpression`. The error may be thrown during the formatting of them. I commented out the related logic of this in this PR and fixed it in #16093 to make it easier to review

2. `FormatError::InvalidDocument`

This is a printer error, but it may also occur only when we format `TaggedTemplateExpression`. There is a formatting logic that will format the template expression and print the IR immediately. If the IR we generated is incorrect, then the error will occur, but this is quite rare! So it's not worth using `FormatResult` everywhere to handle it. Currently, I just `unwrap` it. If we really need to handle it, we can store the error somewhere and return it after formatting in the future.

https://github.com/oxc-project/oxc/blob/78cd5910985cece594e74884717f77ad75347b8a/crates/oxc_formatter/src/write/template.rs#L602-L603

### Key changes

The most significant change is:

```diff
pub trait Format<'ast, T = ()> {
    - fn fmt(&self, f: &mut Formatter<'_, 'ast>) -> FormatResult<()>;
    + fn fmt(&self, f: &mut Formatter<'_, 'ast>);
}
```

All remaining changes to the order stem from the above change:

1. Remove all `FormatResult` usages
2. Remove all `?` operators that pair with the `format!` macro
4. Commented out `PoorLayout` related logic, which is going to be fixed in the #16093

### Benefits

We don't need to deal with `Option`, and removing all `Option` handling makes the code cleaner and more readable. Of course, the performance improved a little bit
leaysgur pushed a commit to oxc-project/oxc that referenced this pull request Nov 26, 2025
This PR is a follow-up to astral-sh/ruff#6613.

### Why can we do it

https://github.com/oxc-project/oxc/blob/622e000934d97ade5588f1e8cc3badde96e7328e/crates/oxc_formatter/src/formatter/diagnostics.rs#L11-L29

We can also make it non-failable since the entire program only has two points that can trigger an error, neither of which needs to interrupt the program immediately, and both can be improved to avoid making FormatResult everywhere

1. `FormatError::PoorLayout`

This error may occur only when handling call arguments, and only the first or the last argument is a `Function` or `ArrowFunctionExpression`. The error may be thrown during the formatting of them. I commented out the related logic of this in this PR and fixed it in #16093 to make it easier to review

2. `FormatError::InvalidDocument`

This is a printer error, but it may also occur only when we format `TaggedTemplateExpression`. There is a formatting logic that will format the template expression and print the IR immediately. If the IR we generated is incorrect, then the error will occur, but this is quite rare! So it's not worth using `FormatResult` everywhere to handle it. Currently, I just `unwrap` it. If we really need to handle it, we can store the error somewhere and return it after formatting in the future.

https://github.com/oxc-project/oxc/blob/78cd5910985cece594e74884717f77ad75347b8a/crates/oxc_formatter/src/write/template.rs#L602-L603

### Key changes

The most significant change is:

```diff
pub trait Format<'ast, T = ()> {
    - fn fmt(&self, f: &mut Formatter<'_, 'ast>) -> FormatResult<()>;
    + fn fmt(&self, f: &mut Formatter<'_, 'ast>);
}
```

All remaining changes to the order stem from the above change:

1. Remove all `FormatResult` usages
2. Remove all `?` operators that pair with the `format!` macro
4. Commented out `PoorLayout` related logic, which is going to be fixed in the #16093

### Benefits

We don't need to deal with `Option`, and removing all `Option` handling makes the code cleaner and more readable. Of course, the performance improved a little bit
taearls pushed a commit to taearls/oxc that referenced this pull request Dec 11, 2025
This PR is a follow-up to astral-sh/ruff#6613.

### Why can we do it

https://github.com/oxc-project/oxc/blob/622e000934d97ade5588f1e8cc3badde96e7328e/crates/oxc_formatter/src/formatter/diagnostics.rs#L11-L29

We can also make it non-failable since the entire program only has two points that can trigger an error, neither of which needs to interrupt the program immediately, and both can be improved to avoid making FormatResult everywhere

1. `FormatError::PoorLayout`

This error may occur only when handling call arguments, and only the first or the last argument is a `Function` or `ArrowFunctionExpression`. The error may be thrown during the formatting of them. I commented out the related logic of this in this PR and fixed it in oxc-project#16093 to make it easier to review

2. `FormatError::InvalidDocument`

This is a printer error, but it may also occur only when we format `TaggedTemplateExpression`. There is a formatting logic that will format the template expression and print the IR immediately. If the IR we generated is incorrect, then the error will occur, but this is quite rare! So it's not worth using `FormatResult` everywhere to handle it. Currently, I just `unwrap` it. If we really need to handle it, we can store the error somewhere and return it after formatting in the future.

https://github.com/oxc-project/oxc/blob/78cd5910985cece594e74884717f77ad75347b8a/crates/oxc_formatter/src/write/template.rs#L602-L603

### Key changes

The most significant change is:

```diff
pub trait Format<'ast, T = ()> {
    - fn fmt(&self, f: &mut Formatter<'_, 'ast>) -> FormatResult<()>;
    + fn fmt(&self, f: &mut Formatter<'_, 'ast>);
}
```

All remaining changes to the order stem from the above change:

1. Remove all `FormatResult` usages
2. Remove all `?` operators that pair with the `format!` macro
4. Commented out `PoorLayout` related logic, which is going to be fixed in the oxc-project#16093

### Benefits

We don't need to deal with `Option`, and removing all `Option` handling makes the code cleaner and more readable. Of course, the performance improved a little bit
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

formatter Related to the formatter performance Potential performance improvement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants