Skip to content
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

[performance] BatchImageBuilder: write .class files in batches #2948

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

jukzi
Copy link
Contributor

@jukzi jukzi commented Sep 12, 2024

ProcessTaskManager

  • use java.util.concurrent for queue
  • signal Cancel/Exception/Stop via the queue
  • implements AutoCloseable for try-with-resource
  • drain as much Elements as possible from the queue

Improves the performance of "Clean all projects"

For example building platform workspace on Windows AbstractImageBuilder.compile(): 120 sec -> 91 sec

With this change the Compiler is actually waiting for parsing most time and not for the write to FileSystem anymore.

@jukzi
Copy link
Contributor Author

jukzi commented Sep 12, 2024

@jukzi
Copy link
Contributor Author

jukzi commented Sep 12, 2024

Example sampling. Before:
image
With this PR:
image

@jukzi
Copy link
Contributor Author

jukzi commented Sep 17, 2024

If anyone wants to review please let me know. Otherwise i plan to submit tomorrow.

@jukzi
Copy link
Contributor Author

jukzi commented Sep 17, 2024

Sampling from latest revision. writing the class files took only 11sec :-) :
image
double checked:
image

@iloveeclipse
Copy link
Member

@szarnekow : would be good if you could take a look here too.

Copy link
Member

@iloveeclipse iloveeclipse left a comment

Choose a reason for hiding this comment

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

I will be available next week for further review comments.

It would be nice if someone else also would review this PR if you plan to submit this before next week.

private final CompilationGroup compilationGroup;

/* leave 2 threads for compiler + reader.*/
private static final ExecutorService WRITER_EXECUTOR = Executors.newFixedThreadPool(Math.max(1, Runtime.getRuntime().availableProcessors() - 2), r -> {
Copy link
Member

Choose a reason for hiding this comment

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

  1. Could we have more convenient name like WRITER_SERVICE?
  2. We have also UI thread that shouldn't compete with compiler for CPU time. So leave 2 threads for compiler + reader should be leave 3 threads for compiler + reader + UI ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

  1. WRITER_SERVICE ok
  2. By now the UI does not lag. the CPU is <<100% since most time the writer thread is waiting anyway.

}
}

// add unit to the queue - wait if no space is available
Copy link
Member

Choose a reason for hiding this comment

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

wait if no space is available is obsoleted now

Copy link
Contributor Author

Choose a reason for hiding this comment

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

java.util.concurrent.BlockingQueue.put(E):
Inserts the specified element into this queue, waiting if necessary for space to become available.

Copy link
Member

Choose a reason for hiding this comment

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

So comment is not needed anyway, as it is just javadoc copy

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Well, the caller should not need to dig into the implementation. I think it's good to keep as an explanation.

// ignore
this.units.put(newElement);
} catch (InterruptedException interrupt) {
throw new RuntimeException(interrupt);
Copy link
Member

Choose a reason for hiding this comment

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

  1. Why is this wrapped? Usually if interrupted exception is handled, it should be flagged by Thread.interrupted() call, but neither this code nor caller does that.

  2. Shouldn't this be AbortCompilation exception if we encounter problems during queue writing?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

InterruptedException is a checked exception. that is not supported by executor.submit().
By now nothing should try to interrupt the executor, so it would be an error to receive one!
All other places in JDT just ignore InterruptedException. we could do that here too but that would need an endless loop until not interrupted. AbortCompilation is not supported here either as nothing reads the future. Only close() does - but that happens anyway only at the end.

return elements;
}
} catch (InterruptedException interrupt) {
throw new RuntimeException(interrupt);
Copy link
Member

Choose a reason for hiding this comment

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

Same here regarding InterruptedException

Copy link
Contributor Author

Choose a reason for hiding this comment

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

in this thread we can use throw new AbortCompilation(true/* silent */, new RuntimeException(interrupt));

throw (RuntimeException) this.caughtException;
}
return null;
/** returns null when no more elements can be expected **/
Copy link
Member

Choose a reason for hiding this comment

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

Can we return empty collection instead? It is always more convenient on the caller side to check for empty and not for null/not null

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Currently the call blocks until some elements are returned or end reached. But the idea was that if the queue is empty the caller could meanwhile flush the result. I forgot why it is not needed that anymore. I may think about it, should not be a relevant difference.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I am happy you asked that. Since the current implementation returns as many CUs as available we know the next call would block until new units available=> the CPU has time to flush. I added flush after every call. This way it looks like the flush would take longer (more often only few files flushed), but the overall build time gets even faster, as the latency reduces:
image

if (next == STOP_SIGNAL) {
return null;
}
throw new IllegalStateException(String.valueOf(next));
Copy link
Member

Choose a reason for hiding this comment

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

Could we have a speaking exception message like "Received unexpected element to process: " + next

Copy link
Contributor Author

Choose a reason for hiding this comment

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

we should never see it, but ok.

try {
this.compiler.process(unitToProcess, index);
} catch (AbortCompilation abortCompilation) {
unitToProcess.cleanUp();
Copy link
Member

Choose a reason for hiding this comment

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

unitToProcess.cleanUp(); seem to be added here - why? It is done in finally below.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

good question, i forgot why. i will remove it

try {
this.processingTask.get(250, TimeUnit.MILLISECONDS); // do not wait forever
} catch (InterruptedException | ExecutionException | TimeoutException ignored) {
// ignore
Copy link
Member

Choose a reason for hiding this comment

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

Not changed in this PR, but shouldn't Thread.interrupted() flag been cleared here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If you want some consistent InterruptedException behaviour you would need to change it everywhere. by now just ignoring is the pattern used all over in jdt.

@Override
public void close() {
try {
this.processingTask.get(250, TimeUnit.MILLISECONDS); // do not wait forever
Copy link
Member

Choose a reason for hiding this comment

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

should this be this.processingTask.cancel(true)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Maybe. That would interrupt the processing() thread, possibly interrupting addNextUnit() which is waiting for a overfilled queue-> RuntimeException -> Future ends. Can you elaborate why would we do so? I don't know the usecase when this should be relevant. should be similar to this.units.clear(). But normally that queue is emptied anyway at this stage. may only make a difference on exception handling.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I tested: both units.clear() and cancel(true) let addNextUnit() progress. However cancel would only work if not anything else in the progress catches the interrupt. So i added both.

@jukzi jukzi force-pushed the batch_write branch 4 times, most recently from c721d56 to af89d66 Compare September 17, 2024 18:00
@iloveeclipse
Copy link
Member

In debugger I see 30 idle threads staying after compilation (I have 32 core workstation). I wonder if they could be shut down after some time?

image

@jukzi
Copy link
Contributor Author

jukzi commented Sep 17, 2024

I wonder if they could be shut down after some time?

https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/concurrent/ThreadPoolExecutor.html#%3Cinit%3E(int,int,long,java.util.concurrent.TimeUnit,java.util.concurrent.BlockingQueue)
has a keepAliveTime. But you did not complain about the "Source File Reader" threads either so i wonder what is the benefit of shutting the threads down?

@jukzi
Copy link
Contributor Author

jukzi commented Sep 17, 2024

@iloveeclipse take your time next week, this can wait

@iloveeclipse
Copy link
Member

We often have to read thread dumps and having 30+ more threads there would make my life harder as it should be. Beside this, I don't like to waste resources (memory/system) if not needed. At the end, the number of (real) threads is limited per process.

@jukzi
Copy link
Contributor Author

jukzi commented Sep 17, 2024

there was a request from @laeubi #1937 to reuse threads. What keepAliveTime would you suggest?

@iloveeclipse
Copy link
Member

there was a request from @laeubi #1937 to reuse threads. What keepAliveTime would you suggest?

Let say 5 minutes.

@jukzi
Copy link
Contributor Author

jukzi commented Sep 18, 2024

Let say 5 minutes.

done

@jukzi
Copy link
Contributor Author

jukzi commented Sep 24, 2024

@iloveeclipse do plan to continue review?

Copy link
Member

@iloveeclipse iloveeclipse left a comment

Choose a reason for hiding this comment

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

I wonder we don't have tests showing startingIndex + 1.

Also would be cool to have a test that validates that one not written class file from the batch will be reported as compilation error.

return t;
});
private static final ExecutorService READER_SERVICE = THREAD_COUNT <= 0 ? null
: new ThreadPoolExecutor(THREAD_COUNT, THREAD_COUNT, /* keepAliveTime */ 5, TimeUnit.MINUTES,
Copy link
Member

Choose a reason for hiding this comment

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

I believe in order to stop idling threads, it should be something like new ThreadPoolExecutor(1, THREAD_COUNT). At least I don't see threads disappearing with the current patch state.

Same for BatchImageBuilder

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Unfortunately that API seems to be not intuitive - corePoolSize=1 would never create more then 1 thread with an unbounded queue.
I'll take this workaround:
https://stackoverflow.com/a/19528622

ProcessTaskManager
* use java.util.concurrent for queue
* signal Cancel/Exception/Stop via the queue
* implements AutoCloseable for try-with-resource
* drain as much Elements as possible from the queue

Improves the performance of "Clean all projects"

For example building platform workspace on Windows
AbstractImageBuilder.compile(): 120 sec -> 80 sec

With this change the Compiler is actually waiting for parsing most time
and not for the write to FileSystem anymore.
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.

3 participants