-
Notifications
You must be signed in to change notification settings - Fork 902
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement concurrent formatting #6091
Comments
Thanks for looking into this and for the thoughtful proposal! I feel like multi-threaded rustfmt has popped up in conversation before, but there aren"t any links that immediately come to mind. I"ll bring this up for discussion in the next team meeting. |
Hello, someone who doesn"t work on rustfmt but has been thinking about it recently here. I feel like cargo-fmt is probably the place to put the parallelism. I made an initial impl and it works fine. Plus it is much less complex than the one in the PR. It would also solve the chunking problem that cargo-fmt is susceptible to on Windows. |
Hey, I checked out out your branch and it does work, but it doesn"t impact performance considerably, locally it"s actually almost the same as running it single-threaded. Shelling out for every file introduces so much overhead that it almost cancels out. (Unscientifically I did get <10% faster results on a small repo). Did you get a larger speedup, could you try comparing that with this branch as well? On tokio https://github.com/tokio-rs/tokio I get 0.266 on main, 0.247 with your branch, and on my branch I get 0.077*, without the extra dependency overhead, although it"s a complexity/dependency trade-off. Edit belowI went a bit too fast late at night and retested it today, I see you"re chunking to 64, I missed that yesterday, sorry about that. Generally I"ve tested running
So there"s a significant speedup, but some problems with the implementation, first off and most important: Concurrent printing to stdout/stderrEven though you"re now multi-threading, you"re still using stdout::inherit on the child-process, on unix a child process default inherits the parent"s fds. This means that all parallel children will try to write to the same stdout/stderr at the same time, which could create a terminal mess, that"s the main motivation for pushing through the Chunk sizingThis chunks into sizes of 64, how was 64 chosen? This means that repos with fewer than 64 files doesn"t benefit at all from the solution. Map reduce inside of a loopThe implementation runs a map-reduce inside of a loop, I think in practice, most workspaces doesn"t have a lot of different edition crates in them, but if they do, files are chunked, then blocked on per edition, instead of running them in parallel which leaves some performance on the table. Doesn"t apply to
|
I"ve been looking a bit closer at the code.
Which in practice means that in a project with a single crate ( The reason that the above implementations are faster, especially for crates that are larger, is that things under This can be measured by running There isn"t much difference on my branch between |
That was a 10 minute implementation. The 64 file chuck was made up. It can obviously be improved, I just wanted to see if it worked as I thought it might. |
Sorry, since you provided the code and said it worked I figured you wanted critique on the solution. Yeah you can parallelize it from Biggest pro is definitely simplicity, since you can embargo both |
Just for posterity, I did dig into the code more and think I understand some of the fundamental problems with implementing multithreading in Rust internals, I heard a bit about it before and multithreading on the crate-level but the reason, at least in this project, is a bit more clear. Parsing seems to be done at the crate-level and produces |
That was a bad idea, it sort of works but in some cases it produces invalid states. I think it"s either that the formatting thread has it"s local state changed while processing "accidentally" or the local state change made by the format causes some incompatibility between the formatting thread and the parse thread. |
#3162 is one of the links I was referencing in #6091 (comment). @MarcusGrass I really appreciate all the thought you"ve put into this. I also want to be upfront and say that right now this is a low priority issue for the team and #6095 will remain parked for the time being. That said, if you continue to look into this or have any other ideas it would be great if you could share those details here! |
Completely understandable, It"s a complexity mess for dubious gain. I think most people just rustfmt single files from their editor. Maybe if |
Hi,
I"ve been rummaging around this repo for a bit lately, getting a bit more familiar with the code by looking at some bugs.
What I really like working with is performance though and I noticed that even though files are formatting independently of each other (in some cases), it"s all run single-threaded.
In most cases that"s entirely reasonable since
rustfmt
is using rustc-types that are inherently not thread-safe, but in some cases it"s possible to run each format file in a thread. So I tested that out.The performance benefits are pretty staggering, of course, the measurements are not especially scientific, and it"s only measured on my machine, but we"re not talking about percentage points here but 3x - 10x for big projects on my machine.
Running
rustfmt
on all files intokio
takes about 400ms while running multithreadedrustfmt
takes about 50ms.Running
rustfmt
on all files an unpublished large codebase takes about 5000ms, while multithreaded is about 800ms.Running
cargo fmt --all
ontokio
is around 265ms, while runningcargo fmt --all
multithreaded is around 80ms.Running
cargo fmt --all
on the same unpublished large codebase is around 1800ms, multithreaded it"s 570ms.On a smaller repo with just a 7 files, multithreaded was about 2x faster.
Downsides
Making
rustfmt
run concurrently will cause problems because of the good oldC
-ghost, shared globals, in this case it"sstdout
andstderr
, any kind of interaction with those facilities will cause a mess.Exemplified:
File A and File B is processed concurrently, in verbose.
There"s a diff in File B, and an error in file A.
You could get some output lite:
"Using rustfmt config file rustfmt.cfg for filea.rs"
"Using rustfmt config file rustfmt.cfg for fileb.rs"
Diff at fileb.rs l 322
Error writing files {msg}
(Error in file A, but you won"t know that).This is a non-trivial problem since
stdout
andstderr
while they are internally synced for a given(e)println
are not synced on the file-parsing level. And lockingstdout
and/orstderr
until a file-pass finishes would nullify most of the benefits of multithreading completely.Proposed solution
Reword formatting runs to take an output buffer and then print that chronologically based on which file is processed.
This preserves previous printing behaviour completely, but requires a pretty sweeping change where these buffers have to be passed down anywhere printing is wanted.
In practice there"s only one part of the code where this becomes particularly problematic, and that"s the diff-writer which uses
term
, which prints colored messages to the terminal.There"s another potential issue, which is preserving ordering between writes to
stdout
andstderr
.I.E. Buffers are passed down into the application, the code prints into those instead.
Now the code first prints with an
eprintln
then aprintln
. The calling code gets the buffers back and is responsible for flushing them tostderr
andstdout
respectively, in which order should the calling code flush them?This isn"t necessarily a huge problem. Ideally code deep down the call-stack shouldn"t be printing, and refactoring that may be the best thing to do for clarity anyway, but it"s something to consider.
The text was updated successfully, but these errors were encountered: