Skip to content

Format files with multiple threads - #1041

Merged
ctubbsii merged 10 commits into
revelc:mainfrom
jvierling:main
Jul 21, 2026
Merged

Format files with multiple threads#1041
ctubbsii merged 10 commits into
revelc:mainfrom
jvierling:main

Conversation

@jvierling

Copy link
Copy Markdown
Contributor

Implements multi-threaded formatting using a fixed-size thread pool (resolves #537). The number of threads can be configured by a Mojo parameter and defaults to 1. The formatters that may not be thread-safe are instantiated as ThreadLocals. As preparatory step the type of the formatter options are narrowed to ImmutableMap to rule out any mutation.

jvierling added 4 commits June 9, 2026 21:14
This is a preparatory step for the implementation of concurrent
formatting to rule out mutation of the options.
* Adds mojo parameter `threads` to control the number of threads used
for formatting.
* Java and Javascript formatters use thread locals

@ctubbsii ctubbsii left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Overall, it looks really good. I have a few suggestions. If you address those, I'd like to test it on a large project to see what the overall improvement is before merging.

One thing I'm not sure about is the thread-safety of the hash cache. Can you point me to the part of the code where it's clear that it's okay to use that concurrently in multiple threads?

Comment thread src/main/java/net/revelc/code/formatter/FormatterMojo.java Outdated
* the cfg
*/
void init(Map<String, String> options, ConfigurationSource cfg);
void init(ImmutableMap<String, String> options, ConfigurationSource cfg);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is it really necessary to change this interface type? This PR can be a lot smaller.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It is not really necessary to change that interface. However, using the ImmutableMap makes it obvious that these options are not a problem for the thread safety. In principle, we could achieve the same by using ImmutableMap just inside the formatters and taking a copy of the options passed via Formatter#init. Nevertheless, it seems natural to me to require that options are not modified by the initialization. Let me know what you prefer.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I prefer the smaller diff, and to avoid the Guava-specific type in the APIs. It gives more flexibility for maintenance in the future, and makes it easier to verify the changeset today. I really like Google's implementation of ImmutableMap, with its builders and everything, but I just don't want to be tied to it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I added a commit that avoided these changes. A review of the immutability of the configs as they get passed around can be done in a subsequent change with careful consideration. I just don't think it's necessary to do that in this PR, as it's a distraction from reviewing the essential changes.

Comment on lines +363 to +373
/**
* Number of threads to use for formatting files in parallel. Defaults to {@code 1}, which preserves the historical
* single-threaded behavior. A value of {@code 0} or any negative value selects a value equal to the number of
* available processors (i.e. {@link Runtime#availableProcessors()}). Larger projects may benefit substantially from
* parallel formatting.
*
* @since 2.30.0
*/
@Parameter(defaultValue = "1", property = "formatter.threads")
private int threads;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It might be nice to specify 1.5C or 2C or 0.75C to specify the number of cores by some multiplier factor, with the result being a minimum of 1 core.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I have extended the threads options such that additionally to the fixed number of threads it also permits specifying a multiplier. Is this what you had in mind?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Yes, exactly. I added a commit that extended your implementation a bit, though, so it's a little more flexible.

// Html Setup
if (this.configHtmlFile != null) {
this.htmlFormatter.init(this.getOptionsFromPropertiesFile(this.configHtmlFile), this);
this.htmlFormatter.init(ImmutableMap.copyOf(this.getOptionsFromPropertiesFile(this.configHtmlFile)), this);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Pretty sure these configs are read only anyway. We don't need to make a copy in an ImmutableMap. We can just make sure that the method returns an immutable type (Collections.unmodifiable wrapper or Map.copyOf())

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The options/configs returned by FormatterMojo#getOptionsFromPropertiesFile are almost read-only. In the case of the XML and the JSON parser the options are extended by an option before being passed to Formatter#init. That is why I did not propagate the ImmutableMap further into getOptionsFromPropertiesFile.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It's just unnecessary copying. The configs are read directly from their source, and used once when launching the respective formatter right away. They are never written again. I understand that it would be "safer" to pass them around as immutables, but I just don't think it's worth it to make all those extra copies. These are internal APIs only, so I'm not worried about them being modified by another thread. We control all the threads internally that have access to them.

Comment thread src/main/java/net/revelc/code/formatter/FormatterMojo.java Outdated
@ctubbsii ctubbsii added this to the 2.30.0 milestone Jul 15, 2026
@jvierling

Copy link
Copy Markdown
Contributor Author

@ctubbsii Concerning the thread-safety: The hash cache is a java.util.Properties and its javadoc says

This class is thread-safe: multiple threads can share a single
 * {@code Properties} object without the need for external synchronization.

This alone does not yet guarantee thread-safety. However, we use the hash cash in a special way: Each thread processes one file at a time and each file is, of course, processed only once. The hash cache is accessed at the beginning of the processing of a file (in FormatterMojo#doFormatFile) to read the file's cached hash and after formatting to store the file's new hash. The hash cache is not used anywhere else. Since, every file has a different key (its path) this concurrent usage of the hash cache should not result in any race-conditions.

That said, I do not particularly like this construction and it would be a lot cleaner to pass just a file's cached hash to the formatting of that file and to collect all the new hashes after formatting instead of bundling the hashes in the Properties.

jvierling and others added 3 commits July 19, 2026 21:41
* Revert unneeded defensive immutable copies of maps; the configs are
  read once, optionally modified by the formatter plugin code, and
  passed directly to the formatter to read. They don't need all that
  extra copying.
* Expand regex for parsing the new parameter, and make it handle more
  cases (added tests).
@ctubbsii

Copy link
Copy Markdown
Member

@jvierling wrote:

That said, I do not particularly like this construction and it would be a lot cleaner to pass just a file's cached hash to the formatting of that file and to collect all the new hashes after formatting instead of bundling the hashes in the Properties.

@hazendaz knows more about the cache than I do, and would probably be better suited to evaluate that change. That said, I think those changes can be done in a new PR, separate from these.

@ctubbsii
ctubbsii merged commit 20da263 into revelc:main Jul 21, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow to use concurrent threads for faster formatting

2 participants