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

Reply via email to