-
Notifications
You must be signed in to change notification settings - Fork 130
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
Conversation
If anyone wants to review please let me know. Otherwise i plan to submit tomorrow. |
org.eclipse.jdt.core/model/org/eclipse/jdt/internal/core/builder/BatchImageBuilder.java
Outdated
Show resolved
Hide resolved
org.eclipse.jdt.core/model/org/eclipse/jdt/internal/core/builder/BatchImageBuilder.java
Outdated
Show resolved
Hide resolved
org.eclipse.jdt.core/model/org/eclipse/jdt/internal/core/builder/BatchImageBuilder.java
Outdated
Show resolved
Hide resolved
@szarnekow : would be good if you could take a look here too. |
There was a problem hiding this 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 -> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Could we have more convenient name like WRITER_SERVICE?
- We have also UI thread that shouldn't compete with compiler for CPU time. So
leave 2 threads for compiler + reader
should beleave 3 threads for compiler + reader + UI
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- WRITER_SERVICE ok
- 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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
...clipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/ProcessTaskManager.java
Outdated
Show resolved
Hide resolved
// ignore | ||
this.units.put(newElement); | ||
} catch (InterruptedException interrupt) { | ||
throw new RuntimeException(interrupt); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
-
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.
-
Shouldn't this be AbortCompilation exception if we encounter problems during queue writing?
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here regarding InterruptedException
There was a problem hiding this comment.
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 **/ |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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:
if (next == STOP_SIGNAL) { | ||
return null; | ||
} | ||
throw new IllegalStateException(String.valueOf(next)); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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)
?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
c721d56
to
af89d66
Compare
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) |
@iloveeclipse take your time next week, this can wait |
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. |
done |
@iloveeclipse do plan to continue review? |
org.eclipse.jdt.core/model/org/eclipse/jdt/internal/core/builder/BatchImageBuilder.java
Outdated
Show resolved
Hide resolved
...clipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/ProcessTaskManager.java
Show resolved
Hide resolved
...clipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/ProcessTaskManager.java
Show resolved
Hide resolved
There was a problem hiding this 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.
org.eclipse.jdt.core/model/org/eclipse/jdt/internal/core/builder/BatchImageBuilder.java
Outdated
Show resolved
Hide resolved
org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/ReadManager.java
Outdated
Show resolved
Hide resolved
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, thanks for the updated thread pool fix.
ProcessTaskManager
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.