What threading and performance considerations apply to a Tooling API ProgressListener, and how do you handle them in an IDE?
answer
- called on Gradle's own thread, not UI/EDT
- synchronous in the event path — keep it fast
- marshal to EDT/Display.asyncExec
- coalesce thousands of events
- don't throw, don't block, no I/O in callback
basics
~10 sGradle invokes statusChanged on its own internal thread, not your UI thread, and synchronously in the event path. Keep the callback fast and non-blocking, and marshal any UI updates onto the IDE's UI thread.
solid answer
~50 sA Tooling API `ProgressListener` is called back by Gradle on an **internal thread it controls**, not on the caller's thread or the IDE's UI thread. The dispatch is effectively synchronous with the event stream, so a slow or blocking `statusChanged` can stall event delivery and slow the whole build's reporting path. Two rules follow. First, do the **minimum** in the callback: capture the descriptor/result into a lightweight model or queue, and return quickly — never do I/O, network calls, or hold locks contended with the build. Second, because UI toolkits (Swing/EDT in IntelliJ, SWT in Eclipse) require updates on their own thread, you must **marshal** the captured data onto that UI thread (e.g. via the IDE's invokeLater/asyncExec) rather than touching widgets directly from Gradle's thread. High-volume builds can emit thousands of TASK/TEST events, so coalescing or batching updates on the consumer side keeps the UI from thrashing. The listener also shouldn't throw — an exception escaping the callback can disrupt the operation.
code
kotlin · 15 linesprivate val pending = ConcurrentLinkedQueue<EventSnapshot>()
launcher.addProgressListener(
ProgressListener { event ->
// 1) cheap capture, return immediately
pending += EventSnapshot.from(event)
},
OperationType.TASK, OperationType.TEST
)
// 2) flush to UI on a timer, coalesced
uiTimer.schedule(50) {
val batch = drain(pending)
invokeLater { tree.applyBatch(batch) }
}go deeper
Just know the callback isn't on the UI thread, so don't touch widgets directly.
Explain marshalling to the UI thread and keeping the callback quick and non-blocking.
Cover event-volume coalescing, no-I/O/no-lock discipline, exception safety, and ordering assumptions under parallel execution.
Define a reusable consumer contract (snapshot+batch+marshal) and reason about back-pressure across the daemon boundary for IDE-scale integrations.
## Where the callback runs When you register a `ProgressListener` and call `run()`/`get()`, Gradle (in the daemon, surfaced through the Tooling API client) invokes `statusChanged(ProgressEvent)` on a **thread Gradle owns** — not the thread that called `run()` and not any UI thread. Treat it like a producer pushing events to you synchronously. ## Why "fast and non-blocking" matters The event-delivery path is shared. If your callback: - performs blocking I/O (writing a file per event, an HTTP call), - acquires a lock that the build also needs, - or does heavy CPU work, then you slow down the reporting pipeline and can introduce back-pressure. The correct shape is **capture-and-return**: pull the immutable bits you need (`event.descriptor.displayName`, `event.eventTime`, for a `FinishEvent` the `result`) into a small object and hand it off. ## Crossing onto the UI thread UI toolkits are single-threaded: - IntelliJ/Swing → updates must run on the EDT, e.g. `ApplicationManager.getApplication().invokeLater { ... }`. - Eclipse/SWT → `Display.asyncExec { ... }`. Touching widgets directly from Gradle's thread is a concurrency bug (corruption, exceptions). So the listener marshals: ```kotlin launcher.addProgressListener( ProgressListener { event -> val snapshot = Snapshot(event.descriptor.displayName, event is FinishEvent) uiExecutor.execute { renderOnUiThread(snapshot) } // hop to UI thread }, OperationType.TASK, OperationType.TEST ) ``` ## Volume and coalescing Large multi-project builds with many tasks and tests can fire **thousands** of start/finish events. Pushing each one straight to the UI causes layout thrash. Common mitigations: buffer events and flush on a timer (e.g. every ~50–100 ms), update only changed tree nodes, or aggregate counts. The Tooling API itself doesn't coalesce — that's the consumer's job. ## Robustness - The callback **should not throw**; an exception propagating out can disrupt the operation and is hard to attribute. - Don't retain references that outlive the build (memory leaks of descriptors/results). - Don't assume ordering guarantees beyond start-before-finish for a given operation; sibling operations interleave, especially with parallel execution. ## Summary contract Fast, non-blocking, exception-free callback that snapshots data and marshals UI work onto the toolkit thread, with consumer-side batching for high event volumes.
- What goes wrong if you update Swing components directly inside statusChanged?You're mutating UI state off the EDT from Gradle's thread, which violates Swing's single-thread rule — leading to intermittent rendering corruption, exceptions, or deadlocks. You must invokeLater onto the EDT.
- How do you keep a huge build from flooding the UI with events?Coalesce on the consumer side: buffer snapshots and flush on a short timer, update only changed nodes, or aggregate counts. The Tooling API does not batch for you.
- Should the listener perform network or disk I/O per event?No. The callback is on Gradle's event-delivery thread; blocking I/O there back-pressures the stream. Capture data and offload any I/O to a separate worker.
Treat the listener like a courier at a busy dock: grab the package and hand it to a runner who delivers it (the UI thread). If the courier stops to unpack and file each box on the spot, the whole line backs up.
saying these in an interview costs you the question
- Assuming statusChanged runs on the UI/EDT thread.
- Doing blocking I/O or acquiring build-contended locks inside the callback.
- Letting exceptions escape the listener.