This is an automated email from the ASF dual-hosted git repository.
hansva pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git
The following commit(s) were added to refs/heads/main by this push:
new 17eaa54a1e Soe performance improvements, fixes #7761 (#7764)
17eaa54a1e is described below
commit 17eaa54a1e4a2d287963111ceafcf9db563cbd61
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Tue Aug 4 11:16:13 2026 +0200
Soe performance improvements, fixes #7761 (#7764)
* Soe performance improvements, fixes #7761
* fix broken test
---
.../apache/hop/core/BlockingBatchingRowSet.java | 4 +-
.../main/java/org/apache/hop/core/row/RowMeta.java | 52 +++++--
.../core/row/RowMetaSnapshotConcurrencyTest.java | 151 +++++++++++++++++++++
.../hop/pipeline/transform/BaseTransform.java | 18 ++-
.../transforms/processfiles/ProcessFilesTest.java | 6 +-
.../transforms/selectvalues/SelectValues.java | 56 ++++++--
.../transforms/selectvalues/SelectValuesData.java | 10 ++
.../fileinput/text/TextFileInputUtils.java | 27 ++--
.../pipeline/transforms/writetolog/WriteToLog.java | 8 ++
9 files changed, 284 insertions(+), 48 deletions(-)
diff --git a/core/src/main/java/org/apache/hop/core/BlockingBatchingRowSet.java
b/core/src/main/java/org/apache/hop/core/BlockingBatchingRowSet.java
index fda80f622c..a05807f80a 100644
--- a/core/src/main/java/org/apache/hop/core/BlockingBatchingRowSet.java
+++ b/core/src/main/java/org/apache/hop/core/BlockingBatchingRowSet.java
@@ -55,8 +55,8 @@ public class BlockingBatchingRowSet extends BaseRowSet
implements Comparable<IRo
// create a fixed sized queue for max performance
//
- putArray = new ArrayBlockingQueue<>(BATCHSIZE, true);
- getArray = new ArrayBlockingQueue<>(BATCHSIZE, true);
+ putArray = new ArrayBlockingQueue<>(BATCHSIZE, false);
+ getArray = new ArrayBlockingQueue<>(BATCHSIZE, false);
size = maxSize / BATCHSIZE; // each buffer's size
Object[][] buffer;
diff --git a/core/src/main/java/org/apache/hop/core/row/RowMeta.java
b/core/src/main/java/org/apache/hop/core/row/RowMeta.java
index 295720c15a..a9a15108f8 100644
--- a/core/src/main/java/org/apache/hop/core/row/RowMeta.java
+++ b/core/src/main/java/org/apache/hop/core/row/RowMeta.java
@@ -63,6 +63,15 @@ public class RowMeta implements IRowMeta {
List<IValueMeta> valueMetaList;
List<Integer> needRealClone;
+ /**
+ * Immutable view of {@link #valueMetaList}, replaced wholesale under the
write lock whenever the
+ * list changes. Index and size lookups happen for every field of every row,
so they read this
+ * instead of taking the read lock; a reader sees either the array from
before a mutation or the
+ * one from after it, which is the same guarantee the read lock gave. Never
null and never mutated
+ * in place - always replaced via {@link #refreshSnapshot()}.
+ */
+ private volatile IValueMeta[] snapshot;
+
public RowMeta() {
this(new ArrayList<>(), new RowMetaCache());
}
@@ -81,6 +90,8 @@ public class RowMeta implements IRowMeta {
iValueMeta, targetType == null ? iValueMeta.getType() :
targetType));
}
this.needRealClone = rowMeta.needRealClone;
+ // the delegated constructor above snapshotted an empty list; republish
now that it is filled
+ refreshSnapshot();
}
private RowMeta(List<IValueMeta> valueMetaList, RowMetaCache rowMetaCache) {
@@ -88,6 +99,16 @@ public class RowMeta implements IRowMeta {
this.cache = rowMetaCache;
this.valueMetaList = valueMetaList;
this.needRealClone = new ArrayList<>();
+ refreshSnapshot();
+ }
+
+ /**
+ * Publish the current contents of {@link #valueMetaList} to {@link
#snapshot}. Must be called
+ * while holding the write lock, after every change to the list, so that
lock-free readers cannot
+ * observe a snapshot that disagrees with the list.
+ */
+ private void refreshSnapshot() {
+ snapshot = valueMetaList.toArray(new IValueMeta[0]);
}
@Override
@@ -193,6 +214,7 @@ public class RowMeta implements IRowMeta {
cache.storeMapping(valueMeta.getName(), i);
}
this.needRealClone = null;
+ refreshSnapshot();
} finally {
lock.writeLock().unlock();
}
@@ -203,12 +225,9 @@ public class RowMeta implements IRowMeta {
*/
@Override
public int size() {
- lock.readLock().lock();
- try {
- return valueMetaList.size();
- } finally {
- lock.readLock().unlock();
- }
+ // Read from the snapshot rather than taking the read lock: this runs for
every field of every
+ // row, and the lock showed up as a measurable share of transform time.
+ return snapshot.length;
}
/**
@@ -274,6 +293,7 @@ public class RowMeta implements IRowMeta {
valueMetaList.add(newMeta);
cache.storeMapping(newMeta.getName(), sz);
needRealClone = null;
+ refreshSnapshot();
} finally {
lock.writeLock().unlock();
}
@@ -304,6 +324,7 @@ public class RowMeta implements IRowMeta {
// backwards.
cache.insertAtMapping(newMeta.getName(), index);
needRealClone = null;
+ refreshSnapshot();
} finally {
lock.writeLock().unlock();
}
@@ -318,15 +339,13 @@ public class RowMeta implements IRowMeta {
*/
@Override
public IValueMeta getValueMeta(int index) {
- lock.readLock().lock();
- try {
- if ((index >= 0) && (index < valueMetaList.size())) {
- return valueMetaList.get(index);
- } else {
- return null;
- }
- } finally {
- lock.readLock().unlock();
+ // Snapshot read instead of the read lock, for the same reason as size().
Read the volatile
+ // once into a local so the bounds check and the access cannot straddle a
mutation.
+ IValueMeta[] current = snapshot;
+ if ((index >= 0) && (index < current.length)) {
+ return current[index];
+ } else {
+ return null;
}
}
@@ -354,6 +373,7 @@ public class RowMeta implements IRowMeta {
valueMetaList.set(index, newMeta);
cache.replaceMapping(old.getName(), newMeta.getName(), index);
needRealClone = null;
+ refreshSnapshot();
} finally {
lock.writeLock().unlock();
}
@@ -913,6 +933,7 @@ public class RowMeta implements IRowMeta {
valueMetaList.clear();
cache.invalidate();
needRealClone = null;
+ refreshSnapshot();
} finally {
lock.writeLock().unlock();
}
@@ -940,6 +961,7 @@ public class RowMeta implements IRowMeta {
valueMetaList.remove(index);
cache.invalidate();
needRealClone = null;
+ refreshSnapshot();
} finally {
lock.writeLock().unlock();
}
diff --git
a/core/src/test/java/org/apache/hop/core/row/RowMetaSnapshotConcurrencyTest.java
b/core/src/test/java/org/apache/hop/core/row/RowMetaSnapshotConcurrencyTest.java
new file mode 100644
index 0000000000..2cf973aeff
--- /dev/null
+++
b/core/src/test/java/org/apache/hop/core/row/RowMetaSnapshotConcurrencyTest.java
@@ -0,0 +1,151 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.core.row;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import org.apache.hop.core.row.value.ValueMetaString;
+import org.junit.jupiter.api.Test;
+
+/**
+ * {@link RowMeta#size()} and {@link RowMeta#getValueMeta(int)} read a
volatile snapshot instead of
+ * taking the read lock. These tests pin the two properties that makes rely
on: the snapshot always
+ * agrees with the list it was published from, and concurrent readers never
see a torn or stale one
+ * while another thread mutates the row meta.
+ */
+class RowMetaSnapshotConcurrencyTest {
+
+ private static IValueMeta field(int i) {
+ return new ValueMetaString("field" + i);
+ }
+
+ @Test
+ void snapshotTracksEveryMutation() throws Exception {
+ RowMeta rowMeta = new RowMeta();
+ assertEquals(0, rowMeta.size());
+
+ for (int i = 0; i < 10; i++) {
+ rowMeta.addValueMeta(field(i));
+ assertEquals(i + 1, rowMeta.size());
+ assertEquals("field" + i, rowMeta.getValueMeta(i).getName());
+ }
+
+ rowMeta.addValueMeta(3, new ValueMetaString("inserted"));
+ assertEquals(11, rowMeta.size());
+ assertEquals("inserted", rowMeta.getValueMeta(3).getName());
+
+ rowMeta.setValueMeta(0, new ValueMetaString("replaced"));
+ assertEquals("replaced", rowMeta.getValueMeta(0).getName());
+
+ rowMeta.removeValueMeta(3);
+ assertEquals(10, rowMeta.size());
+ assertEquals("field3", rowMeta.getValueMeta(3).getName());
+
+ List<IValueMeta> wholesale = new ArrayList<>();
+ wholesale.add(new ValueMetaString("only"));
+ rowMeta.setValueMetaList(wholesale);
+ assertEquals(1, rowMeta.size());
+ assertEquals("only", rowMeta.getValueMeta(0).getName());
+
+ // a clone must carry its own populated snapshot, not the empty one from
the delegated
+ // constructor
+ RowMeta copy = rowMeta.clone();
+ assertEquals(1, copy.size());
+ assertEquals("only", copy.getValueMeta(0).getName());
+
+ rowMeta.clear();
+ assertEquals(0, rowMeta.size());
+ }
+
+ @Test
+ void readersSeeAConsistentSnapshotWhileTheRowMetaChanges() throws Exception {
+ RowMeta rowMeta = new RowMeta();
+ for (int i = 0; i < 20; i++) {
+ rowMeta.addValueMeta(field(i));
+ }
+
+ int readers = 4;
+ CountDownLatch start = new CountDownLatch(1);
+ AtomicReference<Throwable> failure = new AtomicReference<>();
+ List<Thread> threads = new ArrayList<>();
+
+ for (int r = 0; r < readers; r++) {
+ Thread t =
+ new Thread(
+ () -> {
+ try {
+ start.await();
+ for (int n = 0; n < 200_000; n++) {
+ int size = rowMeta.size();
+ // size() and getValueMeta() are two separate operations,
so the row meta may
+ // change in between - that was equally true when each
took the read lock, and
+ // is not what this asserts. What must hold is that a read
never throws, never
+ // tears, and never hands back a half-published value meta.
+ IValueMeta vm = rowMeta.getValueMeta(size - 1);
+ if (vm != null) {
+ assertNotNull(vm.getName());
+ assertTrue(
+ vm.getName().startsWith("field") ||
vm.getName().startsWith("extra"),
+ "unexpected field name: " + vm.getName());
+ }
+ // comfortably out of range must be null rather than
throwing
+ assertEquals(null, rowMeta.getValueMeta(size + 1000));
+ assertEquals(null, rowMeta.getValueMeta(-1));
+ }
+ } catch (Throwable e) {
+ failure.compareAndSet(null, e);
+ }
+ });
+ threads.add(t);
+ t.start();
+ }
+
+ Thread writer =
+ new Thread(
+ () -> {
+ try {
+ start.await();
+ for (int n = 0; n < 20_000; n++) {
+ rowMeta.addValueMeta(new ValueMetaString("extra" + n));
+ rowMeta.removeValueMeta(rowMeta.size() - 1);
+ }
+ } catch (Throwable e) {
+ failure.compareAndSet(null, e);
+ }
+ });
+ threads.add(writer);
+ writer.start();
+
+ start.countDown();
+ for (Thread t : threads) {
+ t.join(TimeUnit.MINUTES.toMillis(1));
+ }
+
+ if (failure.get() != null) {
+ throw new AssertionError("concurrent access failed", failure.get());
+ }
+ assertEquals(20, rowMeta.size());
+ }
+}
diff --git
a/engine/src/main/java/org/apache/hop/pipeline/transform/BaseTransform.java
b/engine/src/main/java/org/apache/hop/pipeline/transform/BaseTransform.java
index 3cac9a4d45..f131141868 100644
--- a/engine/src/main/java/org/apache/hop/pipeline/transform/BaseTransform.java
+++ b/engine/src/main/java/org/apache/hop/pipeline/transform/BaseTransform.java
@@ -193,6 +193,13 @@ public class BaseTransform<Meta extends ITransformMeta,
Data extends ITransformD
*/
protected volatile Long dataVolumeOut;
+ /**
+ * Whether {@link Const#HOP_METRIC_DATA_VOLUME} byte counting is on.
Resolved once in {@link
+ * #init()} rather than per row: it is a configuration flag, so looking it
up in the variable map
+ * on every row is pointless work on the hottest path in the engine.
+ */
+ private boolean dataVolumeMetricEnabled;
+
private boolean distributed;
private final IRowDistribution rowDistribution;
@@ -498,6 +505,9 @@ public class BaseTransform<Meta extends ITransformMeta,
Data extends ITransformD
setVariable(Const.INTERNAL_VARIABLE_TRANSFORM_COPYNR,
Integer.toString(copyNr));
+ dataVolumeMetricEnabled =
+
Const.toBoolean(getPipeline().getVariable(Const.HOP_METRIC_DATA_VOLUME, "N"));
+
// See if fields and types are not null when running.
// Since this is expensive we're only going to enable it when safe mode
checking is on.
//
@@ -889,7 +899,7 @@ public class BaseTransform<Meta extends ITransformMeta,
Data extends ITransformD
*/
@Override
public Long getDataVolume() {
- if
(!Const.toBoolean(getPipeline().getVariable(Const.HOP_METRIC_DATA_VOLUME,
"N"))) {
+ if (!dataVolumeMetricEnabled) {
return null;
}
return dataVolume;
@@ -910,7 +920,7 @@ public class BaseTransform<Meta extends ITransformMeta,
Data extends ITransformD
if (getPipeline() == null) {
return;
}
- if
(!Const.toBoolean(getPipeline().getVariable(Const.HOP_METRIC_DATA_VOLUME,
"N"))) {
+ if (!dataVolumeMetricEnabled) {
return;
}
Long size = RowMeta.getRowSizeEstimateFromRow(row);
@@ -936,7 +946,7 @@ public class BaseTransform<Meta extends ITransformMeta,
Data extends ITransformD
*/
@Override
public Long getDataVolumeIn() {
- if
(!Const.toBoolean(getPipeline().getVariable(Const.HOP_METRIC_DATA_VOLUME,
"N"))) {
+ if (!dataVolumeMetricEnabled) {
return null;
}
return dataVolumeIn;
@@ -950,7 +960,7 @@ public class BaseTransform<Meta extends ITransformMeta,
Data extends ITransformD
*/
@Override
public Long getDataVolumeOut() {
- if
(!Const.toBoolean(getPipeline().getVariable(Const.HOP_METRIC_DATA_VOLUME,
"N"))) {
+ if (!dataVolumeMetricEnabled) {
return null;
}
return dataVolumeOut;
diff --git
a/plugins/transforms/processfiles/src/test/java/org/apache/hop/pipeline/transforms/processfiles/ProcessFilesTest.java
b/plugins/transforms/processfiles/src/test/java/org/apache/hop/pipeline/transforms/processfiles/ProcessFilesTest.java
index 6b0f0555f9..8c659f98c5 100644
---
a/plugins/transforms/processfiles/src/test/java/org/apache/hop/pipeline/transforms/processfiles/ProcessFilesTest.java
+++
b/plugins/transforms/processfiles/src/test/java/org/apache/hop/pipeline/transforms/processfiles/ProcessFilesTest.java
@@ -99,8 +99,10 @@ class ProcessFilesTest {
int.class,
PipelineMeta.class,
Pipeline.class);
- return kons.newInstance(
- helper.transformMeta, meta, data, 0, helper.pipelineMeta,
helper.pipeline);
+ ProcessFiles transform =
+ kons.newInstance(helper.transformMeta, meta, data, 0,
helper.pipelineMeta, helper.pipeline);
+ transform.init();
+ return transform;
}
private RowMeta rowMeta() {
diff --git
a/plugins/transforms/selectvalues/src/main/java/org/apache/hop/pipeline/transforms/selectvalues/SelectValues.java
b/plugins/transforms/selectvalues/src/main/java/org/apache/hop/pipeline/transforms/selectvalues/SelectValues.java
index 16b89b77b5..b139e2417f 100644
---
a/plugins/transforms/selectvalues/src/main/java/org/apache/hop/pipeline/transforms/selectvalues/SelectValues.java
+++
b/plugins/transforms/selectvalues/src/main/java/org/apache/hop/pipeline/transforms/selectvalues/SelectValues.java
@@ -44,6 +44,11 @@ import org.apache.hop.pipeline.transform.TransformMeta;
public class SelectValues extends BaseTransform<SelectValuesMeta,
SelectValuesData> {
private static final Class<?> PKG = SelectValuesMeta.class;
+
+ /** Constant name of the "none" value meta type; resolving it hits the
plugin registry. */
+ private static final String NONE_TYPE_NAME =
+ ValueMetaFactory.getValueMetaName(IValueMeta.TYPE_NONE);
+
public static final String CONST_SELECT_VALUES_LOG_COULD_NOT_FIND_FIELD =
"SelectValues.Log.CouldNotFindField";
@@ -94,6 +99,29 @@ public class SelectValues extends
BaseTransform<SelectValuesMeta, SelectValuesDa
}
}
+ // Work out once which fields cloneValueData() can actually copy. It
only returns a new
+ // object for normally-stored Date and Binary values; anything else
hands back the same
+ // reference, so the row loop can assign it straight across. Unknown
types keep cloning so
+ // the exception cloneValueData() raises for them is still raised.
+ //
+ data.needsClone = new boolean[data.fieldnrs.length];
+ for (int i = 0; i < data.fieldnrs.length; i++) {
+ IValueMeta valueMeta = rowMeta.getValueMeta(data.fieldnrs[i]);
+ data.needsClone[i] =
+ valueMeta != null
+ && valueMeta.getStorageType() == IValueMeta.STORAGE_TYPE_NORMAL
+ && switch (valueMeta.getType()) {
+ case IValueMeta.TYPE_STRING,
+ IValueMeta.TYPE_NUMBER,
+ IValueMeta.TYPE_INTEGER,
+ IValueMeta.TYPE_BOOLEAN,
+ IValueMeta.TYPE_BIGNUMBER,
+ IValueMeta.TYPE_SERIALIZABLE ->
+ false;
+ default -> true;
+ };
+ }
+
// Check for doubles in the selected fields... AFTER renaming!!
//
int[] cnt = new int[meta.getSelectOption().getSelectFields().size()];
@@ -161,22 +189,21 @@ public class SelectValues extends
BaseTransform<SelectValuesMeta, SelectValuesDa
// Get the field values
//
- for (int idx : data.fieldnrs) {
+ int rowMetaSize = rowMeta.size();
+ for (int i = 0; i < data.fieldnrs.length; i++) {
+ int idx = data.fieldnrs[i];
// Normally this can't happen, except when streams are mixed with
different
// number of fields.
//
- if (idx < rowMeta.size()) {
- IValueMeta valueMeta = rowMeta.getValueMeta(idx);
-
- // TODO: Clone might be a 'bit' expensive as it is only needed in case
you want to copy a
- // single field to 2 or
- // more target fields.
- // And even then it is only required for the last n-1 target fields.
- // Perhaps we can consider the requirements for cloning at init(),
store it in a boolean[]
- // and just consider
- // this at runtime
- //
- outputData[outputIndex++] = valueMeta.cloneValueData(rowData[idx]);
+ if (idx < rowMetaSize) {
+ // Cloning only produces a new object for normally-stored Date and
Binary values; for every
+ // other type cloneValueData() hands back the same reference.
data.needsClone was worked out
+ // once from the input row meta, so the common types skip both the
value meta lookup and the
+ // virtual call here.
+ outputData[outputIndex++] =
+ data.needsClone[i]
+ ? rowMeta.getValueMeta(idx).cloneValueData(rowData[idx])
+ : rowData[idx];
} else {
if (isDetailed()) {
logDetailed(
@@ -368,8 +395,7 @@ public class SelectValues extends
BaseTransform<SelectValuesMeta, SelectValuesDa
.equals(storageTypeCodes[IValueMeta.STORAGE_TYPE_NORMAL])) {
rowData[index] = fromMeta.convertBinaryStringToNativeType((byte[])
rowData[index]);
}
- if (meta.getSelectOption().getMeta().get(i).getType()
- != ValueMetaFactory.getValueMetaName(IValueMeta.TYPE_NONE)
+ if (meta.getSelectOption().getMeta().get(i).getType() != NONE_TYPE_NAME
&& fromMeta.getType() != toMeta.getType()) {
rowData[index] = toMeta.convertData(fromMeta, rowData[index]);
}
diff --git
a/plugins/transforms/selectvalues/src/main/java/org/apache/hop/pipeline/transforms/selectvalues/SelectValuesData.java
b/plugins/transforms/selectvalues/src/main/java/org/apache/hop/pipeline/transforms/selectvalues/SelectValuesData.java
index e8e6816ec7..a37a6f7caf 100644
---
a/plugins/transforms/selectvalues/src/main/java/org/apache/hop/pipeline/transforms/selectvalues/SelectValuesData.java
+++
b/plugins/transforms/selectvalues/src/main/java/org/apache/hop/pipeline/transforms/selectvalues/SelectValuesData.java
@@ -25,6 +25,16 @@ import org.apache.hop.pipeline.transform.ITransformData;
public class SelectValuesData extends BaseTransformData implements
ITransformData {
public int[] fieldnrs;
+
+ /**
+ * Per selected field: whether {@link
+ * org.apache.hop.core.row.IValueMeta#cloneValueData(java.lang.Object)} can
actually hand back a
+ * different object. It only does so for normally-stored Date and Binary
values; every other type
+ * returns the same reference. Deciding this once lets the row loop skip
both the value meta
+ * lookup and the virtual call for the common types.
+ */
+ public boolean[] needsClone;
+
public int[] extraFieldnrs;
public int[] removenrs;
public int[] metanrs;
diff --git
a/plugins/transforms/textfile/src/main/java/org/apache/hop/pipeline/transforms/fileinput/text/TextFileInputUtils.java
b/plugins/transforms/textfile/src/main/java/org/apache/hop/pipeline/transforms/fileinput/text/TextFileInputUtils.java
index b9b937112a..f450b72ad4 100644
---
a/plugins/transforms/textfile/src/main/java/org/apache/hop/pipeline/transforms/fileinput/text/TextFileInputUtils.java
+++
b/plugins/transforms/textfile/src/main/java/org/apache/hop/pipeline/transforms/fileinput/text/TextFileInputUtils.java
@@ -444,13 +444,15 @@ public class TextFileInputUtils {
"" + rowNr);
if (info.getErrorHandling().isErrorIgnored()) {
- log.logDetailed(
- fname,
- BaseMessages.getString(PKG, "TextFileInput.Log.Warning")
- + ": "
- + message
- + " : "
- + e.getMessage());
+ if (log.isDetailed()) {
+ log.logDetailed(
+ fname,
+ BaseMessages.getString(PKG, "TextFileInput.Log.Warning")
+ + ": "
+ + message
+ + " : "
+ + e.getMessage());
+ }
value = null;
@@ -817,10 +819,15 @@ public class TextFileInputUtils {
}
// replace the escaped escape with escape...
- containsEscapedEscape = pol.contains(inf.getEscapeCharacter() +
inf.getEscapeCharacter());
+ // Without an escape character there is nothing to unescape:
escapeCharacter is then the
+ // empty string and String.contains("") is always true, which would
send every field of
+ // every row through a no-op regex replace.
+ String escapeCharacter = inf.getEscapeCharacter();
+ containsEscapedEscape =
+ !Utils.isEmpty(escapeCharacter) && pol.contains(escapeCharacter
+ escapeCharacter);
if (containsEscapedEscape) {
- String replace = inf.getEscapeCharacter() +
inf.getEscapeCharacter();
- String replaceWith = inf.getEscapeCharacter();
+ String replace = escapeCharacter + escapeCharacter;
+ String replaceWith = escapeCharacter;
pol = Const.replace(pol, replace, replaceWith);
}
diff --git
a/plugins/transforms/writetolog/src/main/java/org/apache/hop/pipeline/transforms/writetolog/WriteToLog.java
b/plugins/transforms/writetolog/src/main/java/org/apache/hop/pipeline/transforms/writetolog/WriteToLog.java
index 7931e4a49c..567654ec46 100644
---
a/plugins/transforms/writetolog/src/main/java/org/apache/hop/pipeline/transforms/writetolog/WriteToLog.java
+++
b/plugins/transforms/writetolog/src/main/java/org/apache/hop/pipeline/transforms/writetolog/WriteToLog.java
@@ -90,6 +90,14 @@ public class WriteToLog extends
BaseTransform<WriteToLogMeta, WriteToLogData> {
}
} // end if first
+ // Building the message reads and formats every logged field, so only do
it when the configured
+ // level is actually visible under the current log level. Otherwise this
is per-row work whose
+ // result is thrown away in setLog().
+ if (!data.logLevel.isVisible(getLogChannel().getLogLevel())) {
+ putRow(getInputRowMeta(), row); // copy row to output
+ return true;
+ }
+
StringBuilder out = new StringBuilder();
out.append(
Const.CR