bamaer commented on PR #9142:
URL: https://github.com/apache/hop/pull/9142#issuecomment-6506689277
@hansva thanks, this looks good. I built core and engine, ran the new and
existing row set, BaseTransform and Pipeline tests, and pushed 5M rows through
7 Dummy transforms (including a 4-copy transform and a copy-to-two-targets
split that joins again). Every row arrived, lock-free was about 30% faster
(14.5 s vs 21 s), and stopping the pipeline mid-run ended in 5 ms.
**Should fix**
- **Idle transforms use a lot more CPU.** A transform waiting for rows wakes
up every 1 ms (`MAX_PARK_NANOS`, also the cap in `awaitAny`). With 30 idle
Dummy transforms that was 25% of a core, against 1.5% with the current row
sets. That matters for long-running pipelines that are mostly idle. A plain
backoff makes slow trickles of rows much later, because a writer only wakes a
parked reader at `wakeBatch`. What worked for me was all three of these
together:
1. Back off in `getRowLockFree` from 1 ms up to 20 ms.
2. While the reader is backed off, have the writer wake it on its next row
(a `wakeOnAnyRow` flag next to `parkedConsumer`).
3. Back off only when every input is a `LockFreeRowSet`, since hops that
spill to disk cannot wake the reader.
On the same pipelines: throughput unchanged, idle CPU 1.7%, and a single
row every 100 ms got through in 1.3 ms median instead of 4.1 ms. One cost: the
writer then reads `parkedConsumer` for every row, not every 64th. I saw no
difference, but it is worth checking on your own benchmarks. Diff below.
Minor: the buffer size is rounded up to a power of two, so the default of
10,000 rows becomes 16,384. A line about that in the docs would help anyone who
sized their buffers to save memory.
<details><summary>Diff</summary>
```diff
--- a/core/src/main/java/org/apache/hop/core/LockFreeRowSet.java
+++ b/core/src/main/java/org/apache/hop/core/LockFreeRowSet.java
@@ -133,14 +133,15 @@
* that many rows.
*/
private void rowAdded(long index) {
- if ((index & wakeCheckMask) != 0) {
+ Thread consumer = parkedConsumer;
+ if (consumer == null) {
return;
}
- Thread consumer = parkedConsumer;
- // claim the parked thread first, so it is unparked once and not again
for every row added
- // before it gets to run
- if (consumer != null
- && producerIndex - consumerIndex >= wakeBatch
+ boolean anyRow = wakeOnAnyRow; // read after parkedConsumer, which the
consumer writes last
+ if ((index & wakeCheckMask) != 0 && !anyRow) {
+ return;
+ }
+ if ((anyRow || producerIndex - consumerIndex >= wakeBatch)
&& PARKED_CONSUMER.compareAndSet(this, consumer, null)) {
LockSupport.unpark(consumer);
}
@@ -278,18 +279,33 @@
Thread.onSpinWait();
}
Thread current = Thread.currentThread();
+ // a long park only when every input can wake us; other row sets are
polled every 1 ms
+ boolean deep = maxNanos > MAX_PARK_NANOS;
+ for (IRowSet rowSet : rowSets) {
+ if (!(rowSet instanceof LockFreeRowSet)) {
+ deep = false;
+ maxNanos = MAX_PARK_NANOS;
+ break;
+ }
+ }
for (IRowSet rowSet : rowSets) {
if (rowSet instanceof LockFreeRowSet lockFree) {
+ if (deep) {
+ lockFree.wakeOnAnyRow = true;
+ }
lockFree.parkedConsumer = current;
}
}
// check again after announcing ourselves on every input, so a wake-up
is not lost
if (!anyReady(rowSets)) {
- LockSupport.parkNanos(rowSets, Math.min(maxNanos, MAX_PARK_NANOS));
+ LockSupport.parkNanos(rowSets, maxNanos);
}
for (IRowSet rowSet : rowSets) {
if (rowSet instanceof LockFreeRowSet lockFree) {
PARKED_CONSUMER.compareAndSet(lockFree, current, null);
+ if (deep) {
+ lockFree.wakeOnAnyRow = false;
+ }
}
}
}
@@ -411,6 +427,7 @@
/** Written by the consumer when it parks, read by producers for every row.
*/
abstract class LockFreeRowSetParkedConsumer extends LockFreeRowSetPad3 {
volatile Thread parkedConsumer;
+ volatile boolean wakeOnAnyRow;
LockFreeRowSetParkedConsumer(int maxSize) {
super(maxSize);
---
a/engine/src/main/java/org/apache/hop/pipeline/transform/BaseTransform.java
+++
b/engine/src/main/java/org/apache/hop/pipeline/transform/BaseTransform.java
@@ -168,6 +168,9 @@
/** The longest a transform parks waiting on its lock-free inputs before
it checks them again. */
private static final long LOCK_FREE_MAX_WAIT_NANOS =
TimeUnit.MILLISECONDS.toNanos(1);
+ /** The longest a transform parks once its lock-free inputs have been
idle for a while. */
+ private static final long LOCK_FREE_MAX_IDLE_NANOS =
TimeUnit.MILLISECONDS.toNanos(20);
+
private static final VarHandle LINES_READ;
private static final VarHandle LINES_WRITTEN;
private static final VarHandle LINES_INPUT;
@@ -2029,6 +2032,7 @@
*/
private Object[] getRowLockFree(IRowSet inputRowSet) {
int emptyInputs = 0;
+ long idleWait = LOCK_FREE_MAX_WAIT_NANOS;
while (!isStopped()) {
Object[] row = inputRowSet.getRowImmediate();
if (row == null && inputRowSet.isDone()) {
@@ -2057,7 +2061,9 @@
return row;
}
if (++emptyInputs >= inputRowSets.size()) {
- LockFreeRowSet.awaitAny(inputRowSets, LOCK_FREE_MAX_WAIT_NANOS);
+ LockFreeRowSet.awaitAny(inputRowSets, idleWait);
+ // back off while nothing arrives: lock-free inputs then wake us on
their next row
+ idleWait = Math.min(idleWait * 2, LOCK_FREE_MAX_IDLE_NANOS);
emptyInputs = 0;
}
nextInputStream();
```
</details>
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]