This is an automated email from the ASF dual-hosted git repository.

bamaer 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 f69c2e1790 Issue #8617 : Limit large String, JSON, Binary, and Avro 
values in data profiles (#8632)
f69c2e1790 is described below

commit f69c2e1790ab5d74ef4e5a421e81a8b3ac5c25a3
Author: Matt Casters <[email protected]>
AuthorDate: Mon Sep 28 08:20:51 2026 +0200

    Issue #8617 : Limit large String, JSON, Binary, and Avro values in data 
profiles (#8632)
    
    * Issue #8617 : Limit large String, JSON, Binary, and Avro values in data 
profiles
    
    An execution data profile can leave oversized String, JSON, Binary, and Avro
    values out of the sample rows it stores. An empty limit keeps today's 
behavior.
    Zero stores none of that type. A longer String or JSON cell is replaced 
with a
    short marker, and Binary or Avro cells over the limit are left empty.
    
    fixes #8617
    
    * Issue #8617 : Store sample markers in the field storage type
    
    Replacing a lazy-conversion value with a Java String made execution data
    registration fail for the whole tick. Markers now follow binary-string and
    indexed storage. Kept cells are cloned the same way as a normal row copy,
    a profiled row is measured once, and a negative or non-numeric limit is 
logged.
---
 .../hop/core/row/value/ValueMetaAvroRecord.java    |  46 +++
 .../metadata-types/execution-data-profile.png      | Bin 0 -> 111201 bytes
 .../metadata-types/execution-data-profile.adoc     |   8 +
 .../execution/profiling/ExecutionDataProfile.java  | 117 ++++--
 .../execution/sampler/IExecutionDataSampler.java   |  16 +
 .../hop/execution/sampler/SampledValueLimits.java  | 353 ++++++++++++++++++
 .../sampler/plugins/ExecutionDataSamplerBase.java  |  18 +
 .../dataprof/BasicDataProfilingDataSampler.java    |  68 +++-
 .../first/FirstRowsExecutionDataSampler.java       |   2 +-
 .../plugins/last/LastRowsExecutionDataSampler.java |   2 +-
 .../random/RandomRowsExecutionDataSampler.java     |   4 +-
 .../engines/local/LocalPipelineEngine.java         |   1 +
 .../profiling/messages/messages_en_US.properties   |  11 +-
 .../execution/sampler/SampledValueLimitsTest.java  | 415 +++++++++++++++++++++
 .../hop/beam/core/transform/TransformBaseFn.java   |   6 +-
 .../execution/SparkTransformExecutionSampling.java |   8 +-
 .../profiling/ExecutionDataProfileEditor.java      |  38 ++
 17 files changed, 1053 insertions(+), 60 deletions(-)

diff --git 
a/core/src/main/java/org/apache/hop/core/row/value/ValueMetaAvroRecord.java 
b/core/src/main/java/org/apache/hop/core/row/value/ValueMetaAvroRecord.java
index 95ebf68f09..39d66ef5d5 100644
--- a/core/src/main/java/org/apache/hop/core/row/value/ValueMetaAvroRecord.java
+++ b/core/src/main/java/org/apache/hop/core/row/value/ValueMetaAvroRecord.java
@@ -21,7 +21,9 @@ import java.io.DataInputStream;
 import java.io.DataOutputStream;
 import java.io.EOFException;
 import java.io.IOException;
+import java.io.OutputStream;
 import java.net.SocketTimeoutException;
+import java.nio.charset.StandardCharsets;
 import org.apache.avro.Schema;
 import org.apache.avro.generic.GenericData;
 import org.apache.avro.generic.GenericDatumReader;
@@ -456,6 +458,33 @@ public class ValueMetaAvroRecord extends ValueMetaBase {
     }
   }
 
+  /**
+   * Bytes of the compact schema JSON plus the binary datum. Sample storage 
uses this as the size of
+   * an Avro value: the schema is part of the payload and {@link #writeData} 
writes the datum.
+   *
+   * @param object An Avro {@link GenericRecord}
+   * @return The schema and datum size in bytes
+   * @throws HopValueException When the value is not a record or cannot be 
encoded
+   */
+  public static int storedPayloadBytes(Object object) throws HopValueException 
{
+    if (!(object instanceof GenericRecord genericRecord) || 
genericRecord.getSchema() == null) {
+      throw new HopValueException(
+          "An Avro GenericRecord with a schema is needed to measure its size");
+    }
+    Schema recordSchema = genericRecord.getSchema();
+    int schemaBytes = 
recordSchema.toString(false).getBytes(StandardCharsets.UTF_8).length;
+    CountingOutputStream counting = new CountingOutputStream();
+    try {
+      BinaryEncoder encoder = 
EncoderFactory.get().directBinaryEncoder(counting, null);
+      new GenericDatumWriter<GenericRecord>(recordSchema).write(genericRecord, 
encoder);
+      encoder.flush();
+    } catch (IOException e) {
+      throw new HopValueException("Unable to measure the size of an Avro 
record", e);
+    }
+    long total = (long) schemaBytes + counting.count;
+    return total > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) total;
+  }
+
   public Schema getSchema() {
     return schema;
   }
@@ -484,4 +513,21 @@ public class ValueMetaAvroRecord extends ValueMetaBase {
 
     return super.getInteger(object);
   }
+
+  /** Counts bytes written by an Avro encoder without keeping them. */
+  private static final class CountingOutputStream extends OutputStream {
+    private long count;
+
+    @Override
+    public void write(int b) {
+      count++;
+    }
+
+    @Override
+    public void write(byte[] bytes, int offset, int length) {
+      if (length > 0) {
+        count += length;
+      }
+    }
+  }
 }
diff --git 
a/docs/hop-user-manual/modules/ROOT/assets/images/metadata-types/execution-data-profile.png
 
b/docs/hop-user-manual/modules/ROOT/assets/images/metadata-types/execution-data-profile.png
new file mode 100644
index 0000000000..762de0af5d
Binary files /dev/null and 
b/docs/hop-user-manual/modules/ROOT/assets/images/metadata-types/execution-data-profile.png
 differ
diff --git 
a/docs/hop-user-manual/modules/ROOT/pages/metadata-types/execution-data-profile.adoc
 
b/docs/hop-user-manual/modules/ROOT/pages/metadata-types/execution-data-profile.adoc
index 27123d05aa..bac4c439e0 100644
--- 
a/docs/hop-user-manual/modules/ROOT/pages/metadata-types/execution-data-profile.adoc
+++ 
b/docs/hop-user-manual/modules/ROOT/pages/metadata-types/execution-data-profile.adoc
@@ -26,14 +26,22 @@ An Apache Hop Execution Data Profile builds data profiles 
as data flow through p
 
 == Options
 
+image::metadata-types/execution-data-profile.png[Execution data profile 
editor, with the large value limits,width="90%"]
+
 [options="header"]
 |===
 |Option |Description
 |Name|The name to be used for this Execution Data Profile
 |Description|A description to be used for this Execution Data Profile
+|Maximum string length|Maximum number of characters to keep for a String 
field. A JSON document kept in a String field uses this limit. Empty means no 
limit. `0` stores none. A longer value is replaced with `<not stored, N 
characters>`.
+|Maximum JSON length|Maximum number of characters of the compact JSON text to 
keep for a JSON field. Empty means no limit. `0` stores none. A longer value is 
replaced with a JSON text `<not stored, N characters>`.
+|Maximum binary size|Maximum number of bytes to keep for a Binary field. Empty 
means no limit. `0` stores none. A larger value is left empty in the sample.
+|Maximum Avro size|Maximum number of bytes of schema JSON plus Avro datum to 
keep for an Avro Record field. Empty means no limit. `0` stores none. A larger 
value is left empty in the sample.
 |Data Samplers to use|One or more data samplers to use with this Execution 
Data Profile. See details below.
 |===
 
+The four size limits apply to every sampler in the profile. Leave a field 
empty to keep today's behavior for that type. Minimum and maximum values are 
calculated only from values that were stored. Null counts and string lengths 
are still recorded for values that are left out. Minimum and maximum length 
example rows keep the other columns and replace the large value.
+
 == Data Samplers
 
 [options="header"]
diff --git 
a/engine/src/main/java/org/apache/hop/execution/profiling/ExecutionDataProfile.java
 
b/engine/src/main/java/org/apache/hop/execution/profiling/ExecutionDataProfile.java
index 9d36867d5a..f5828f4e15 100644
--- 
a/engine/src/main/java/org/apache/hop/execution/profiling/ExecutionDataProfile.java
+++ 
b/engine/src/main/java/org/apache/hop/execution/profiling/ExecutionDataProfile.java
@@ -20,7 +20,15 @@ package org.apache.hop.execution.profiling;
 
 import java.util.ArrayList;
 import java.util.List;
+import lombok.Getter;
+import lombok.Setter;
+import org.apache.hop.core.gui.plugin.GuiElementType;
+import org.apache.hop.core.gui.plugin.GuiPlugin;
+import org.apache.hop.core.gui.plugin.GuiWidgetElement;
+import org.apache.hop.core.gui.plugin.GuiWidgetGroupType;
+import org.apache.hop.core.variables.IVariables;
 import org.apache.hop.execution.sampler.IExecutionDataSampler;
+import org.apache.hop.execution.sampler.SampledValueLimits;
 import org.apache.hop.metadata.api.HopMetadata;
 import org.apache.hop.metadata.api.HopMetadataBase;
 import org.apache.hop.metadata.api.HopMetadataCategory;
@@ -32,6 +40,9 @@ import org.apache.hop.metadata.api.IHopMetadata;
  * This data profile makes it easy to specify what kind of information you 
want to capture about the
  * output of a transform during its execution.
  */
+@Getter
+@Setter
+@GuiPlugin(description = "Execution data profile widgets")
 @HopMetadata(
     key = "execution-data-profile",
     name = "i18n::ExecutionDataProfile.name",
@@ -44,8 +55,60 @@ import org.apache.hop.metadata.api.IHopMetadata;
 public class ExecutionDataProfile extends HopMetadataBase implements 
IHopMetadata, Cloneable {
 
   public static final String GUI_PLUGIN_ELEMENT_PARENT_ID = 
"ExecutionDataSamplerParent";
+  public static final String GUI_PLUGIN_LIMITS_PARENT_ID = 
"ExecutionDataProfileLimits";
+
+  private static final String LARGE_VALUES_GROUP = 
"i18n::ExecutionDataProfile.Group.LargeValues";
+
   @HopMetadataProperty private String description;
 
+  @GuiWidgetElement(
+      id = "stringValueLimit",
+      order = "100",
+      type = GuiElementType.TEXT,
+      parentId = GUI_PLUGIN_LIMITS_PARENT_ID,
+      label = "i18n::ExecutionDataProfile.Label.StringValueLimit",
+      toolTip = "i18n::ExecutionDataProfile.Tooltip.StringValueLimit",
+      groupType = GuiWidgetGroupType.BOXES,
+      group = LARGE_VALUES_GROUP)
+  @HopMetadataProperty
+  private String stringValueLimit;
+
+  @GuiWidgetElement(
+      id = "jsonValueLimit",
+      order = "110",
+      type = GuiElementType.TEXT,
+      parentId = GUI_PLUGIN_LIMITS_PARENT_ID,
+      label = "i18n::ExecutionDataProfile.Label.JsonValueLimit",
+      toolTip = "i18n::ExecutionDataProfile.Tooltip.JsonValueLimit",
+      groupType = GuiWidgetGroupType.BOXES,
+      group = LARGE_VALUES_GROUP)
+  @HopMetadataProperty
+  private String jsonValueLimit;
+
+  @GuiWidgetElement(
+      id = "binaryValueLimit",
+      order = "120",
+      type = GuiElementType.TEXT,
+      parentId = GUI_PLUGIN_LIMITS_PARENT_ID,
+      label = "i18n::ExecutionDataProfile.Label.BinaryValueLimit",
+      toolTip = "i18n::ExecutionDataProfile.Tooltip.BinaryValueLimit",
+      groupType = GuiWidgetGroupType.BOXES,
+      group = LARGE_VALUES_GROUP)
+  @HopMetadataProperty
+  private String binaryValueLimit;
+
+  @GuiWidgetElement(
+      id = "avroValueLimit",
+      order = "130",
+      type = GuiElementType.TEXT,
+      parentId = GUI_PLUGIN_LIMITS_PARENT_ID,
+      label = "i18n::ExecutionDataProfile.Label.AvroValueLimit",
+      toolTip = "i18n::ExecutionDataProfile.Tooltip.AvroValueLimit",
+      groupType = GuiWidgetGroupType.BOXES,
+      group = LARGE_VALUES_GROUP)
+  @HopMetadataProperty
+  private String avroValueLimit;
+
   @HopMetadataProperty(groupKey = "samplers", key = "sampler")
   private List<IExecutionDataSampler> samplers;
 
@@ -68,49 +131,35 @@ public class ExecutionDataProfile extends HopMetadataBase 
implements IHopMetadat
   public ExecutionDataProfile(ExecutionDataProfile profile) {
     this(profile.name);
     this.description = profile.description;
+    this.stringValueLimit = profile.stringValueLimit;
+    this.jsonValueLimit = profile.jsonValueLimit;
+    this.binaryValueLimit = profile.binaryValueLimit;
+    this.avroValueLimit = profile.avroValueLimit;
     for (IExecutionDataSampler<?> sampler : profile.samplers) {
       this.samplers.add(sampler.clone());
     }
   }
 
-  @Override
-  protected ExecutionDataProfile clone() {
-    return new ExecutionDataProfile(this);
-  }
-
-  /**
-   * Gets description
-   *
-   * @return value of description
-   */
-  public String getDescription() {
-    return description;
-  }
-
   /**
-   * Sets description
+   * Resolve this profile's value limits and attach them to every sampler that 
will store rows.
    *
-   * @param description value of description
+   * @param samplers Profile samplers and any extra samplers added by the 
engine
+   * @param variables Pipeline variables used to resolve the limit fields
    */
-  public void setDescription(String description) {
-    this.description = description;
-  }
-
-  /**
-   * Gets samplers
-   *
-   * @return value of samplers
-   */
-  public List<IExecutionDataSampler> getSamplers() {
-    return samplers;
+  public void applyLimits(List<IExecutionDataSampler> samplers, IVariables 
variables) {
+    if (samplers == null) {
+      return;
+    }
+    SampledValueLimits limits = SampledValueLimits.from(this, variables);
+    for (IExecutionDataSampler sampler : samplers) {
+      if (sampler != null) {
+        sampler.setSampledValueLimits(limits);
+      }
+    }
   }
 
-  /**
-   * Sets samplers
-   *
-   * @param samplers value of samplers
-   */
-  public void setSamplers(List<IExecutionDataSampler> samplers) {
-    this.samplers = samplers;
+  @Override
+  protected ExecutionDataProfile clone() {
+    return new ExecutionDataProfile(this);
   }
 }
diff --git 
a/engine/src/main/java/org/apache/hop/execution/sampler/IExecutionDataSampler.java
 
b/engine/src/main/java/org/apache/hop/execution/sampler/IExecutionDataSampler.java
index 748526350b..28ed68c938 100644
--- 
a/engine/src/main/java/org/apache/hop/execution/sampler/IExecutionDataSampler.java
+++ 
b/engine/src/main/java/org/apache/hop/execution/sampler/IExecutionDataSampler.java
@@ -18,6 +18,7 @@
 
 package org.apache.hop.execution.sampler;
 
+import com.fasterxml.jackson.annotation.JsonIgnore;
 import org.apache.hop.core.exception.HopException;
 import org.apache.hop.core.plugins.IPlugin;
 import org.apache.hop.core.plugins.PluginRegistry;
@@ -60,6 +61,21 @@ public interface IExecutionDataSampler<Store extends 
IExecutionDataSamplerStore>
   void sampleRow(Store samplerStore, IStream.StreamType streamType, IRowMeta 
rowMeta, Object[] row)
       throws HopException;
 
+  /**
+   * Limits resolved from the execution data profile before sampling starts. 
Not metadata: Beam and
+   * Spark must not write this into the sampler JSON.
+   */
+  @JsonIgnore
+  default void setSampledValueLimits(SampledValueLimits limits) {}
+
+  /**
+   * @return The limits for this sampler, or no limits when the profile did 
not set any.
+   */
+  @JsonIgnore
+  default SampledValueLimits getSampledValueLimits() {
+    return SampledValueLimits.unlimited();
+  }
+
   /**
    * This object factory is needed to instantiate the correct plugin class 
based on the value of the
    * Object ID which is simply the object ID.
diff --git 
a/engine/src/main/java/org/apache/hop/execution/sampler/SampledValueLimits.java 
b/engine/src/main/java/org/apache/hop/execution/sampler/SampledValueLimits.java
new file mode 100644
index 0000000000..51240bac8e
--- /dev/null
+++ 
b/engine/src/main/java/org/apache/hop/execution/sampler/SampledValueLimits.java
@@ -0,0 +1,353 @@
+/*
+ * 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.execution.sampler;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.TextNode;
+import java.nio.charset.StandardCharsets;
+import lombok.Getter;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hop.core.exception.HopValueException;
+import org.apache.hop.core.logging.LogChannel;
+import org.apache.hop.core.row.IRowMeta;
+import org.apache.hop.core.row.IValueMeta;
+import org.apache.hop.core.row.value.ValueMetaAvroRecord;
+import org.apache.hop.core.row.value.ValueMetaJson;
+import org.apache.hop.core.util.JsonUtil;
+import org.apache.hop.core.variables.IVariables;
+import org.apache.hop.execution.profiling.ExecutionDataProfile;
+
+/**
+ * Resolved limits for String, JSON, Binary, and Avro values kept by an 
execution data profile. A
+ * blank setting means no limit. A negative or non-numeric setting is logged 
and then treated as no
+ * limit. Zero stores none of that type.
+ */
+@Getter
+public final class SampledValueLimits {
+  public static final String NOT_STORED = "<not stored>";
+
+  private static final SampledValueLimits UNLIMITED =
+      new SampledValueLimits(null, null, null, null);
+  private static final String CHARACTERS = "characters";
+
+  private final Integer stringLimit;
+  private final Integer jsonLimit;
+  private final Integer binaryLimit;
+  private final Integer avroLimit;
+
+  private SampledValueLimits(
+      Integer stringLimit, Integer jsonLimit, Integer binaryLimit, Integer 
avroLimit) {
+    this.stringLimit = stringLimit;
+    this.jsonLimit = jsonLimit;
+    this.binaryLimit = binaryLimit;
+    this.avroLimit = avroLimit;
+  }
+
+  public static SampledValueLimits unlimited() {
+    return UNLIMITED;
+  }
+
+  /**
+   * Marker stored in place of a String or JSON value that is over its limit.
+   *
+   * @param size The measured size that was left out
+   * @param unit {@code characters} for String and JSON
+   * @return The short replacement text
+   */
+  public static String notStored(int size, String unit) {
+    return "<not stored, " + size + " " + unit + ">";
+  }
+
+  /**
+   * Resolve the profile fields with {@code variables}. A blank value is 
unlimited. A negative or
+   * non-numeric value is unlimited and logged, so a typo does not turn the 
limit off quietly. Zero
+   * stores nothing of that type.
+   */
+  public static SampledValueLimits from(ExecutionDataProfile profile, 
IVariables variables) {
+    if (profile == null) {
+      return unlimited();
+    }
+    return new SampledValueLimits(
+        parseLimit(resolve(variables, profile.getStringValueLimit())),
+        parseLimit(resolve(variables, profile.getJsonValueLimit())),
+        parseLimit(resolve(variables, profile.getBinaryValueLimit())),
+        parseLimit(resolve(variables, profile.getAvroValueLimit())));
+  }
+
+  public boolean hasLimits() {
+    return stringLimit != null || jsonLimit != null || binaryLimit != null || 
avroLimit != null;
+  }
+
+  /**
+   * @return true when this value must not be kept as itself. Nulls are kept 
as null.
+   */
+  public boolean omit(IValueMeta valueMeta, Object value) throws 
HopValueException {
+    return decide(valueMeta, value).omit;
+  }
+
+  /**
+   * Measure every field once. Profiling calls {@link #copyRow(IRowMeta, 
Object[], Decision[])}
+   * several times for the same row, and measuring JSON or Avro serialises the 
value.
+   */
+  public Decision[] decisionsFor(IRowMeta rowMeta, Object[] row) throws 
HopValueException {
+    if (row == null || rowMeta == null) {
+      return new Decision[0];
+    }
+    int fields = Math.min(rowMeta.size(), row.length);
+    Decision[] decisions = new Decision[fields];
+    for (int i = 0; i < fields; i++) {
+      decisions[i] = decide(rowMeta.getValueMeta(i), row[i]);
+    }
+    return decisions;
+  }
+
+  /**
+   * A new row array for storage. Cells over a limit are replaced. Cells that 
stay are cloned. The
+   * pipeline row is left unchanged, and a value that is dropped is not copied 
first.
+   */
+  public Object[] copyRow(IRowMeta rowMeta, Object[] row) throws 
HopValueException {
+    return copyRow(rowMeta, row, null);
+  }
+
+  /**
+   * Same as {@link #copyRow(IRowMeta, Object[])}, using decisions already 
computed for this row.
+   */
+  public Object[] copyRow(IRowMeta rowMeta, Object[] row, Decision[] decisions)
+      throws HopValueException {
+    if (row == null) {
+      return null;
+    }
+    if (rowMeta == null || !hasLimits()) {
+      return rowMeta == null ? row.clone() : rowMeta.cloneRow(row);
+    }
+
+    int fields = Math.min(rowMeta.size(), row.length);
+    if (decisions == null) {
+      decisions = decisionsFor(rowMeta, row);
+    } else {
+      fields = Math.min(fields, decisions.length);
+    }
+    boolean anyOmitted = false;
+    for (int i = 0; i < fields; i++) {
+      anyOmitted = anyOmitted || decisions[i].omit();
+    }
+    if (!anyOmitted) {
+      return rowMeta.cloneRow(row);
+    }
+
+    Object[] copy = row.clone();
+    for (int i = 0; i < fields; i++) {
+      if (decisions[i].omit()) {
+        copy[i] = decisions[i].replacement();
+      } else {
+        copy[i] = copyKept(rowMeta.getValueMeta(i), row[i]);
+      }
+    }
+    return copy;
+  }
+
+  private static Object copyKept(IValueMeta valueMeta, Object value) throws 
HopValueException {
+    if (value == null || valueMeta == null) {
+      return value;
+    }
+    return valueMeta.cloneValueData(value);
+  }
+
+  private Decision decide(IValueMeta valueMeta, Object value) throws 
HopValueException {
+    if (valueMeta == null || value == null || valueMeta.isNull(value)) {
+      return Decision.keep();
+    }
+    return switch (valueMeta.getType()) {
+      case IValueMeta.TYPE_STRING -> decideString(valueMeta, value);
+      case IValueMeta.TYPE_JSON -> decideJson(valueMeta, value);
+      case IValueMeta.TYPE_BINARY -> decideBinary(valueMeta, value);
+      case IValueMeta.TYPE_AVRO -> decideAvro(value);
+      default -> Decision.keep();
+    };
+  }
+
+  private Decision decideString(IValueMeta valueMeta, Object value) throws 
HopValueException {
+    if (stringLimit == null) {
+      return Decision.keep();
+    }
+    if (stringLimit == 0) {
+      return Decision.drop(marker(valueMeta, NOT_STORED));
+    }
+    int length = stringLength(valueMeta, value);
+    if (length > stringLimit) {
+      return Decision.drop(marker(valueMeta, notStored(length, CHARACTERS)));
+    }
+    return Decision.keep();
+  }
+
+  private Decision decideJson(IValueMeta valueMeta, Object value) throws 
HopValueException {
+    if (jsonLimit == null) {
+      return Decision.keep();
+    }
+    if (jsonLimit == 0) {
+      return Decision.drop(marker(valueMeta, NOT_STORED));
+    }
+    int length;
+    try {
+      length = jsonLength(valueMeta, value);
+    } catch (Exception e) {
+      return Decision.drop(marker(valueMeta, NOT_STORED));
+    }
+    if (length > jsonLimit) {
+      return Decision.drop(marker(valueMeta, notStored(length, CHARACTERS)));
+    }
+    return Decision.keep();
+  }
+
+  private Decision decideBinary(IValueMeta valueMeta, Object value) throws 
HopValueException {
+    if (binaryLimit == null) {
+      return Decision.keep();
+    }
+    if (binaryLimit == 0) {
+      return Decision.drop(null);
+    }
+    int size;
+    try {
+      size = binarySize(valueMeta, value);
+    } catch (Exception e) {
+      return Decision.drop(null);
+    }
+    if (size > binaryLimit) {
+      return Decision.drop(null);
+    }
+    return Decision.keep();
+  }
+
+  private Decision decideAvro(Object value) {
+    if (avroLimit == null) {
+      return Decision.keep();
+    }
+    if (avroLimit == 0) {
+      return Decision.drop(null);
+    }
+    int size;
+    try {
+      size = ValueMetaAvroRecord.storedPayloadBytes(value);
+    } catch (Exception e) {
+      return Decision.drop(null);
+    }
+    if (size > avroLimit) {
+      return Decision.drop(null);
+    }
+    return Decision.keep();
+  }
+
+  private static int stringLength(IValueMeta valueMeta, Object value) throws 
HopValueException {
+    if (valueMeta.getStorageType() == IValueMeta.STORAGE_TYPE_BINARY_STRING
+        && value instanceof byte[] bytes) {
+      String text = valueMeta.getString(value);
+      return text == null ? bytes.length : text.length();
+    }
+    if (value instanceof String text) {
+      return text.length();
+    }
+    String text = valueMeta.getString(value);
+    return text == null ? 0 : text.length();
+  }
+
+  private static int jsonLength(IValueMeta valueMeta, Object value) throws 
Exception {
+    if (valueMeta.getStorageType() == IValueMeta.STORAGE_TYPE_BINARY_STRING
+        && value instanceof byte[] bytes) {
+      return new String(bytes, StandardCharsets.UTF_8).length();
+    }
+    JsonNode node;
+    if (value instanceof JsonNode jsonNode) {
+      node = jsonNode;
+    } else if (valueMeta instanceof ValueMetaJson jsonMeta) {
+      node = jsonMeta.getJson(value);
+    } else {
+      node = valueMeta.getJson(value);
+    }
+    if (node == null) {
+      return 0;
+    }
+    String text = JsonUtil.mapJsonToString(node, false);
+    return text == null ? 0 : text.length();
+  }
+
+  private static int binarySize(IValueMeta valueMeta, Object value) throws 
HopValueException {
+    if (value instanceof byte[] bytes) {
+      return bytes.length;
+    }
+    byte[] bytes = valueMeta.getBinary(value);
+    return bytes == null ? 0 : bytes.length;
+  }
+
+  private static String resolve(IVariables variables, String value) {
+    if (value == null || variables == null) {
+      return value;
+    }
+    return variables.resolve(value);
+  }
+
+  /**
+   * A replacement that {@link 
org.apache.hop.core.row.value.ValueMetaBase#writeData} can store.
+   * Lazy conversion keeps a {@code byte[]} and an indexed field keeps an 
index, so a Java String
+   * marker would fail execution-data registration for the whole tick.
+   */
+  private static Object marker(IValueMeta valueMeta, String text) {
+    if (valueMeta == null) {
+      return text;
+    }
+    return switch (valueMeta.getStorageType()) {
+      case IValueMeta.STORAGE_TYPE_BINARY_STRING -> 
text.getBytes(StandardCharsets.UTF_8);
+      case IValueMeta.STORAGE_TYPE_INDEXED -> null;
+      default -> valueMeta.getType() == IValueMeta.TYPE_JSON ? 
TextNode.valueOf(text) : text;
+    };
+  }
+
+  private static Integer parseLimit(String raw) {
+    if (StringUtils.isBlank(raw)) {
+      return null;
+    }
+    String trimmed = raw.trim();
+    try {
+      int parsed = Integer.parseInt(trimmed);
+      if (parsed < 0) {
+        LogChannel.GENERAL.logError(
+            "Execution data profile value limit '"
+                + trimmed
+                + "' is negative and is ignored. The value is stored without a 
limit.");
+        return null;
+      }
+      return parsed;
+    } catch (NumberFormatException e) {
+      LogChannel.GENERAL.logError(
+          "Execution data profile value limit '"
+              + trimmed
+              + "' is not a number and is ignored. The value is stored without 
a limit.");
+      return null;
+    }
+  }
+
+  /** Whether one cell is kept, and the storage-typed marker used when it is 
not. */
+  public record Decision(boolean omit, Object replacement) {
+    private static Decision keep() {
+      return new Decision(false, null);
+    }
+
+    private static Decision drop(Object replacement) {
+      return new Decision(true, replacement);
+    }
+  }
+}
diff --git 
a/engine/src/main/java/org/apache/hop/execution/sampler/plugins/ExecutionDataSamplerBase.java
 
b/engine/src/main/java/org/apache/hop/execution/sampler/plugins/ExecutionDataSamplerBase.java
index ba36dd0172..c774a4b500 100644
--- 
a/engine/src/main/java/org/apache/hop/execution/sampler/plugins/ExecutionDataSamplerBase.java
+++ 
b/engine/src/main/java/org/apache/hop/execution/sampler/plugins/ExecutionDataSamplerBase.java
@@ -18,6 +18,7 @@
 
 package org.apache.hop.execution.sampler.plugins;
 
+import com.fasterxml.jackson.annotation.JsonIgnore;
 import java.util.Objects;
 import org.apache.hop.core.gui.plugin.GuiElementType;
 import org.apache.hop.core.gui.plugin.GuiWidgetElement;
@@ -25,6 +26,7 @@ import 
org.apache.hop.execution.profiling.ExecutionDataProfile;
 import org.apache.hop.execution.sampler.ExecutionDataSamplerMeta;
 import org.apache.hop.execution.sampler.IExecutionDataSampler;
 import org.apache.hop.execution.sampler.IExecutionDataSamplerStore;
+import org.apache.hop.execution.sampler.SampledValueLimits;
 import org.apache.hop.metadata.api.HopMetadataProperty;
 
 public abstract class ExecutionDataSamplerBase<Store extends 
IExecutionDataSamplerStore>
@@ -39,6 +41,9 @@ public abstract class ExecutionDataSamplerBase<Store extends 
IExecutionDataSampl
   @HopMetadataProperty
   protected String sampleSize;
 
+  /** Resolved from the profile at runtime. Not part of the sampler metadata. 
*/
+  @JsonIgnore protected SampledValueLimits sampledValueLimits;
+
   protected String pluginId;
 
   protected String pluginName;
@@ -57,6 +62,7 @@ public abstract class ExecutionDataSamplerBase<Store extends 
IExecutionDataSampl
     this.sampleSize = base.sampleSize;
     this.pluginId = base.pluginId;
     this.pluginName = base.pluginName;
+    this.sampledValueLimits = base.sampledValueLimits;
   }
 
   @Override
@@ -96,6 +102,18 @@ public abstract class ExecutionDataSamplerBase<Store 
extends IExecutionDataSampl
     this.sampleSize = sampleSize;
   }
 
+  @Override
+  @JsonIgnore
+  public void setSampledValueLimits(SampledValueLimits sampledValueLimits) {
+    this.sampledValueLimits = sampledValueLimits;
+  }
+
+  @Override
+  @JsonIgnore
+  public SampledValueLimits getSampledValueLimits() {
+    return sampledValueLimits == null ? SampledValueLimits.unlimited() : 
sampledValueLimits;
+  }
+
   /**
    * Gets pluginId
    *
diff --git 
a/engine/src/main/java/org/apache/hop/execution/sampler/plugins/dataprof/BasicDataProfilingDataSampler.java
 
b/engine/src/main/java/org/apache/hop/execution/sampler/plugins/dataprof/BasicDataProfilingDataSampler.java
index 568886d095..7ad676ab9a 100644
--- 
a/engine/src/main/java/org/apache/hop/execution/sampler/plugins/dataprof/BasicDataProfilingDataSampler.java
+++ 
b/engine/src/main/java/org/apache/hop/execution/sampler/plugins/dataprof/BasicDataProfilingDataSampler.java
@@ -18,9 +18,11 @@
 
 package org.apache.hop.execution.sampler.plugins.dataprof;
 
+import com.fasterxml.jackson.annotation.JsonIgnore;
 import java.util.HashMap;
 import java.util.Map;
 import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.exception.HopValueException;
 import org.apache.hop.core.gui.plugin.GuiElementType;
 import org.apache.hop.core.gui.plugin.GuiPlugin;
 import org.apache.hop.core.gui.plugin.GuiWidgetElement;
@@ -31,6 +33,7 @@ import 
org.apache.hop.execution.profiling.ExecutionDataProfile;
 import org.apache.hop.execution.sampler.ExecutionDataSamplerMeta;
 import org.apache.hop.execution.sampler.ExecutionDataSamplerPlugin;
 import org.apache.hop.execution.sampler.IExecutionDataSampler;
+import org.apache.hop.execution.sampler.SampledValueLimits;
 import org.apache.hop.i18n.BaseMessages;
 import org.apache.hop.metadata.api.HopMetadataProperty;
 import org.apache.hop.pipeline.transform.stream.IStream;
@@ -72,6 +75,9 @@ public class BasicDataProfilingDataSampler
   private String pluginId;
   private String pluginName;
 
+  /** Resolved from the profile at runtime. Not part of the sampler metadata. 
*/
+  @JsonIgnore private SampledValueLimits sampledValueLimits;
+
   // The metadata fields
   //
 
@@ -163,6 +169,7 @@ public class BasicDataProfilingDataSampler
     this.profilingNrNonNull = sampler.profilingNrNonNull;
     this.profilingMinLength = sampler.profilingMinLength;
     this.profilingMaxLength = sampler.profilingMaxLength;
+    this.sampledValueLimits = sampler.sampledValueLimits;
   }
 
   public BasicDataProfilingDataSampler clone() {
@@ -195,6 +202,12 @@ public class BasicDataProfilingDataSampler
 
     try {
 
+      // Measure each field once. Min, max, length and null samples all store 
a copy of this row.
+      //
+      SampledValueLimits limits = getSampledValueLimits();
+      SampledValueLimits.Decision[] decisions =
+          limits.hasLimits() ? limits.decisionsFor(rowMeta, row) : null;
+
       // Profile all columns
       //
       for (int i = 0; i < rowMeta.size(); i++) {
@@ -206,66 +219,71 @@ public class BasicDataProfilingDataSampler
           if (profilingNrNull) {
             long counter = store.getNullCounters().getOrDefault(name, 0L);
             store.getNullCounters().put(name, ++counter);
-            addSampleRow(store, name, ProfilingType.NrNulls, rowMeta, row);
+            addSampleRow(store, name, ProfilingType.NrNulls, rowMeta, row, 
decisions);
           }
         } else {
           if (profilingNrNonNull) {
             long counter = store.getNonNullCounters().getOrDefault(name, 0L);
             store.getNonNullCounters().put(name, ++counter);
-            addSampleRow(store, name, ProfilingType.NrNonNulls, rowMeta, row);
+            addSampleRow(store, name, ProfilingType.NrNonNulls, rowMeta, row, 
decisions);
           }
         }
 
+        // A value over its type limit is still counted above. It is not kept 
as a minimum or
+        // maximum, because that would retain the large value.
+        //
+        boolean storeValue = decisions == null || i >= decisions.length || 
!decisions[i].omit();
+
         // Minimum
         //
-        if (profilingMinimum) {
+        if (profilingMinimum && storeValue) {
           Object oldMin = store.getMinValues().get(name);
           if (oldMin == null) {
-            store.getMinValues().put(name, valueData);
+            store.getMinValues().put(name, 
valueMeta.cloneValueData(valueData));
             store.getMinMeta().put(name, valueMeta);
           } else {
             int compare = valueMeta.compare(valueData, oldMin);
             if (compare < 0) {
               // We have a new minimum
               //
-              store.getMinValues().put(name, valueData);
+              store.getMinValues().put(name, 
valueMeta.cloneValueData(valueData));
               store.getMinMeta().put(name, valueMeta);
 
               clearSampleRows(store, name, ProfilingType.MinValue);
 
               // Also save the row of data as a sample
               //
-              addSampleRow(store, name, ProfilingType.MinValue, rowMeta, row);
+              addSampleRow(store, name, ProfilingType.MinValue, rowMeta, row, 
decisions);
             } else if (compare == 0) {
               // We found another value at the current minimum
-              addSampleRow(store, name, ProfilingType.MinValue, rowMeta, row);
+              addSampleRow(store, name, ProfilingType.MinValue, rowMeta, row, 
decisions);
             }
           }
         }
 
         // Maximum
         //
-        if (profilingMaximum) {
+        if (profilingMaximum && storeValue) {
           Object oldMax = store.getMaxValues().get(name);
           if (oldMax == null) {
-            store.getMaxValues().put(name, valueData);
+            store.getMaxValues().put(name, 
valueMeta.cloneValueData(valueData));
             store.getMaxMeta().put(name, valueMeta);
           } else {
             int compare = valueMeta.compare(valueData, oldMax);
             if (compare > 0) {
               // We have a new maximum
               //
-              store.getMaxValues().put(name, valueData);
+              store.getMaxValues().put(name, 
valueMeta.cloneValueData(valueData));
               store.getMaxMeta().put(name, valueMeta);
 
               clearSampleRows(store, name, ProfilingType.MaxValue);
 
               // Also save the row of data as a sample
               //
-              addSampleRow(store, name, ProfilingType.MaxValue, rowMeta, row);
+              addSampleRow(store, name, ProfilingType.MaxValue, rowMeta, row, 
decisions);
             } else if (compare == 0) {
               // We found another value at the current maximum
-              addSampleRow(store, name, ProfilingType.MaxValue, rowMeta, row);
+              addSampleRow(store, name, ProfilingType.MaxValue, rowMeta, row, 
decisions);
             }
           }
         }
@@ -292,10 +310,10 @@ public class BasicDataProfilingDataSampler
 
                 // Also save the row of data as a sample
                 //
-                addSampleRow(store, name, ProfilingType.MinLength, rowMeta, 
row);
+                addSampleRow(store, name, ProfilingType.MinLength, rowMeta, 
row, decisions);
               } else if (length == oldMin) {
                 // We found another value at the current minimum length
-                addSampleRow(store, name, ProfilingType.MinLength, rowMeta, 
row);
+                addSampleRow(store, name, ProfilingType.MinLength, rowMeta, 
row, decisions);
               }
             }
           }
@@ -319,10 +337,10 @@ public class BasicDataProfilingDataSampler
 
                 // Also save the row of data as a sample
                 //
-                addSampleRow(store, name, ProfilingType.MaxLength, rowMeta, 
row);
+                addSampleRow(store, name, ProfilingType.MaxLength, rowMeta, 
row, decisions);
               } else if (length == oldMax) {
                 // We found another value at the current maximum length
-                addSampleRow(store, name, ProfilingType.MaxLength, rowMeta, 
row);
+                addSampleRow(store, name, ProfilingType.MaxLength, rowMeta, 
row, decisions);
               }
             }
           }
@@ -346,7 +364,9 @@ public class BasicDataProfilingDataSampler
       String name,
       ProfilingType profilingType,
       IRowMeta rowMeta,
-      Object[] row) {
+      Object[] row,
+      SampledValueLimits.Decision[] decisions)
+      throws HopValueException {
     synchronized (store.getProfileSamples()) {
       Map<ProfilingType, RowBuffer> typeBufferMap =
           store.getProfileSamples().computeIfAbsent(name, k -> new 
HashMap<>());
@@ -356,7 +376,7 @@ public class BasicDataProfilingDataSampler
       // Keep the memory consumption sane
       //
       if (rowBuffer.size() < store.getMaxRows()) {
-        rowBuffer.addRow(row);
+        rowBuffer.addRow(getSampledValueLimits().copyRow(rowMeta, row, 
decisions));
       }
     }
   }
@@ -544,4 +564,16 @@ public class BasicDataProfilingDataSampler
   public void setProfilingMaxLength(boolean profilingMaxLength) {
     this.profilingMaxLength = profilingMaxLength;
   }
+
+  @Override
+  @JsonIgnore
+  public void setSampledValueLimits(SampledValueLimits sampledValueLimits) {
+    this.sampledValueLimits = sampledValueLimits;
+  }
+
+  @Override
+  @JsonIgnore
+  public SampledValueLimits getSampledValueLimits() {
+    return sampledValueLimits == null ? SampledValueLimits.unlimited() : 
sampledValueLimits;
+  }
 }
diff --git 
a/engine/src/main/java/org/apache/hop/execution/sampler/plugins/first/FirstRowsExecutionDataSampler.java
 
b/engine/src/main/java/org/apache/hop/execution/sampler/plugins/first/FirstRowsExecutionDataSampler.java
index 27239e77a8..6cfbe00470 100644
--- 
a/engine/src/main/java/org/apache/hop/execution/sampler/plugins/first/FirstRowsExecutionDataSampler.java
+++ 
b/engine/src/main/java/org/apache/hop/execution/sampler/plugins/first/FirstRowsExecutionDataSampler.java
@@ -79,7 +79,7 @@ public class FirstRowsExecutionDataSampler
       if (rows.isEmpty()) {
         samplerStore.setRowMeta(rowMeta);
       }
-      rows.add(rowMeta.cloneRow(row));
+      rows.add(getSampledValueLimits().copyRow(rowMeta, row));
     }
   }
 }
diff --git 
a/engine/src/main/java/org/apache/hop/execution/sampler/plugins/last/LastRowsExecutionDataSampler.java
 
b/engine/src/main/java/org/apache/hop/execution/sampler/plugins/last/LastRowsExecutionDataSampler.java
index f7d1a707fa..c3ff990ef1 100644
--- 
a/engine/src/main/java/org/apache/hop/execution/sampler/plugins/last/LastRowsExecutionDataSampler.java
+++ 
b/engine/src/main/java/org/apache/hop/execution/sampler/plugins/last/LastRowsExecutionDataSampler.java
@@ -82,7 +82,7 @@ public class LastRowsExecutionDataSampler
       // Add to the front, remove the last on overflow
       // This is why we use a linked list
       //
-      rows.add(0, rowMeta.cloneRow(row));
+      rows.add(0, getSampledValueLimits().copyRow(rowMeta, row));
       if (rows.size() > samplerStore.getMaxRows()) {
         rows.remove(rows.size() - 1);
       }
diff --git 
a/engine/src/main/java/org/apache/hop/execution/sampler/plugins/random/RandomRowsExecutionDataSampler.java
 
b/engine/src/main/java/org/apache/hop/execution/sampler/plugins/random/RandomRowsExecutionDataSampler.java
index 6f5511551c..0d947422a1 100644
--- 
a/engine/src/main/java/org/apache/hop/execution/sampler/plugins/random/RandomRowsExecutionDataSampler.java
+++ 
b/engine/src/main/java/org/apache/hop/execution/sampler/plugins/random/RandomRowsExecutionDataSampler.java
@@ -83,11 +83,11 @@ public class RandomRowsExecutionDataSampler
         if (rows.isEmpty()) {
           samplerStore.setRowMeta(rowMeta);
         }
-        rows.add(row);
+        rows.add(getSampledValueLimits().copyRow(rowMeta, row));
       } else {
         int randomIndex = random.nextInt(samplerStore.getMaxRows());
         if (randomIndex < samplerStore.getMaxRows()) {
-          rows.set(randomIndex, rowMeta.cloneRow(row));
+          rows.set(randomIndex, getSampledValueLimits().copyRow(rowMeta, row));
         }
       }
     }
diff --git 
a/engine/src/main/java/org/apache/hop/pipeline/engines/local/LocalPipelineEngine.java
 
b/engine/src/main/java/org/apache/hop/pipeline/engines/local/LocalPipelineEngine.java
index 8359f8c884..7d77daea84 100644
--- 
a/engine/src/main/java/org/apache/hop/pipeline/engines/local/LocalPipelineEngine.java
+++ 
b/engine/src/main/java/org/apache/hop/pipeline/engines/local/LocalPipelineEngine.java
@@ -398,6 +398,7 @@ public class LocalPipelineEngine extends Pipeline 
implements IPipelineEngine<Pip
     if (samplers.isEmpty()) {
       return;
     }
+    profile.applyLimits(samplers, this);
 
     samplerStoresMap = new HashMap<>();
 
diff --git 
a/engine/src/main/resources/org/apache/hop/execution/profiling/messages/messages_en_US.properties
 
b/engine/src/main/resources/org/apache/hop/execution/profiling/messages/messages_en_US.properties
index 04a62301be..a65615d560 100644
--- 
a/engine/src/main/resources/org/apache/hop/execution/profiling/messages/messages_en_US.properties
+++ 
b/engine/src/main/resources/org/apache/hop/execution/profiling/messages/messages_en_US.properties
@@ -17,4 +17,13 @@
 #
 
 ExecutionDataProfile.name=Execution Data Profile
-ExecutionDataProfile.description=Describes how transform output data is 
profiled
\ No newline at end of file
+ExecutionDataProfile.description=Describes how transform output data is 
profiled
+ExecutionDataProfile.Group.LargeValues=Large values
+ExecutionDataProfile.Label.StringValueLimit=Maximum string length
+ExecutionDataProfile.Tooltip.StringValueLimit=Maximum number of characters of 
a String field to keep in sample rows and in minimum and maximum values. A REST 
or JSON body stored in a String field uses this limit. Leave empty for no 
limit. 0 stores none of these values. A longer value is replaced with '<not 
stored, N characters>'. You can use a variable, for example 
'${SAMPLE_STRING_LIMIT}'. Minimum and maximum values are calculated only from 
values that are stored.
+ExecutionDataProfile.Label.JsonValueLimit=Maximum JSON length
+ExecutionDataProfile.Tooltip.JsonValueLimit=Maximum number of characters of 
the compact JSON text to keep for a JSON field. Leave empty for no limit. 0 
stores none of these values. A longer value is replaced with a JSON text '<not 
stored, N characters>'. You can use a variable, for example 
'${SAMPLE_JSON_LIMIT}'.
+ExecutionDataProfile.Label.BinaryValueLimit=Maximum binary size (bytes)
+ExecutionDataProfile.Tooltip.BinaryValueLimit=Maximum number of bytes to keep 
for a Binary field. Leave empty for no limit. 0 stores none of these values. A 
larger value is left empty in the sample. You can use a variable, for example 
'${SAMPLE_BINARY_LIMIT}'.
+ExecutionDataProfile.Label.AvroValueLimit=Maximum Avro size (bytes)
+ExecutionDataProfile.Tooltip.AvroValueLimit=Maximum number of bytes of schema 
JSON plus Avro datum to keep for an Avro Record field. Leave empty for no 
limit. 0 stores none of these values. A larger value is left empty in the 
sample. You can use a variable, for example '${SAMPLE_AVRO_LIMIT}'.
\ No newline at end of file
diff --git 
a/engine/src/test/java/org/apache/hop/execution/sampler/SampledValueLimitsTest.java
 
b/engine/src/test/java/org/apache/hop/execution/sampler/SampledValueLimitsTest.java
new file mode 100644
index 0000000000..8f6ba079bf
--- /dev/null
+++ 
b/engine/src/test/java/org/apache/hop/execution/sampler/SampledValueLimitsTest.java
@@ -0,0 +1,415 @@
+/*
+ * 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.execution.sampler;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import java.lang.reflect.Field;
+import java.nio.charset.StandardCharsets;
+import java.sql.Timestamp;
+import org.apache.avro.Schema;
+import org.apache.avro.generic.GenericData;
+import org.apache.avro.generic.GenericRecord;
+import org.apache.hop.core.HopClientEnvironment;
+import org.apache.hop.core.gui.plugin.GuiElementType;
+import org.apache.hop.core.gui.plugin.GuiPlugin;
+import org.apache.hop.core.gui.plugin.GuiWidgetElement;
+import org.apache.hop.core.gui.plugin.GuiWidgetGroupType;
+import org.apache.hop.core.json.HopJson;
+import org.apache.hop.core.row.IRowMeta;
+import org.apache.hop.core.row.IValueMeta;
+import org.apache.hop.core.row.RowBuffer;
+import org.apache.hop.core.row.RowMeta;
+import org.apache.hop.core.row.value.ValueMetaAvroRecord;
+import org.apache.hop.core.row.value.ValueMetaBinary;
+import org.apache.hop.core.row.value.ValueMetaJson;
+import org.apache.hop.core.row.value.ValueMetaString;
+import org.apache.hop.core.row.value.ValueMetaTimestamp;
+import org.apache.hop.core.util.JsonUtil;
+import org.apache.hop.core.variables.Variables;
+import org.apache.hop.execution.ExecutionData;
+import org.apache.hop.execution.profiling.ExecutionDataProfile;
+import 
org.apache.hop.execution.sampler.plugins.dataprof.BasicDataProfilingDataSampler;
+import 
org.apache.hop.execution.sampler.plugins.dataprof.BasicDataProfilingDataSamplerStore;
+import 
org.apache.hop.execution.sampler.plugins.first.FirstRowsExecutionDataSampler;
+import 
org.apache.hop.execution.sampler.plugins.first.FirstRowsExecutionDataSamplerStore;
+import org.apache.hop.metadata.api.IHopMetadataProvider;
+import org.apache.hop.metadata.serializer.memory.MemoryMetadataProvider;
+import org.apache.hop.pipeline.transform.stream.IStream;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+class SampledValueLimitsTest {
+
+  @BeforeAll
+  static void initHop() throws Exception {
+    HopClientEnvironment.init();
+  }
+
+  @Test
+  void unlimitedCopyLeavesEveryValueInANewRow() throws Exception {
+    IRowMeta rowMeta = sampleRowMeta();
+    byte[] binary = new byte[] {1, 2, 3, 4};
+    JsonNode json = JsonUtil.jsonMapper().readTree("{\"ok\":true}");
+    GenericRecord avro = avroRecord("hi");
+    Object[] row = new Object[] {"text", json, binary, avro};
+
+    Object[] copy = SampledValueLimits.unlimited().copyRow(rowMeta, row);
+
+    assertNotSame(row, copy);
+    assertEquals("text", copy[0]);
+    assertEquals(json, copy[1]);
+    assertArrayEquals(binary, (byte[]) copy[2]);
+    assertNotSame(binary, copy[2]);
+    assertEquals("hi", ((GenericRecord) copy[3]).get("a").toString());
+    assertSame("text", row[0]);
+    assertSame(json, row[1]);
+    assertSame(binary, row[2]);
+    assertSame(avro, row[3]);
+  }
+
+  @Test
+  void zeroOnOneTypeReplacesOnlyThatType() throws Exception {
+    IRowMeta rowMeta = sampleRowMeta();
+    Object[] row =
+        new Object[] {
+          "text", JsonUtil.jsonMapper().readTree("{\"ok\":true}"), new byte[] 
{9}, avroRecord("hi")
+        };
+
+    Object[] copy = limits(null, null, "0", null).copyRow(rowMeta, row);
+
+    assertEquals("text", copy[0]);
+    assertEquals(row[1], copy[1]);
+    assertNull(copy[2]);
+    assertEquals("hi", ((GenericRecord) copy[3]).get("a").toString());
+    assertArrayEquals(new byte[] {9}, (byte[]) row[2]);
+  }
+
+  @Test
+  void positiveStringLimitKeepsShortValuesAndReplacesLongOnes() throws 
Exception {
+    IRowMeta rowMeta = new RowMeta();
+    rowMeta.addValueMeta(new ValueMetaString("body"));
+    String longText = "this is longer than five";
+    Object[] shortRow = new Object[] {"short"};
+    Object[] longRow = new Object[] {longText};
+    SampledValueLimits valueLimits = limits("5", null, null, null);
+
+    Object[] shortCopy = valueLimits.copyRow(rowMeta, shortRow);
+    Object[] longCopy = valueLimits.copyRow(rowMeta, longRow);
+
+    assertEquals("short", shortCopy[0]);
+    assertEquals(SampledValueLimits.notStored(longText.length(), 
"characters"), longCopy[0]);
+    assertSame(longText, longRow[0]);
+    assertNotSame(longRow, longCopy);
+  }
+
+  @Test
+  void binaryAndAvroOverTheLimitAreNullAndSchemaBytesCount() throws Exception {
+    IRowMeta rowMeta = sampleRowMeta();
+    byte[] binary = new byte[] {1, 2, 3, 4, 5};
+    GenericRecord small = avroRecord("hi");
+    int smallSize = ValueMetaAvroRecord.storedPayloadBytes(small);
+    GenericRecord wideSchema = wideSchemaRecord();
+    int wideSize = ValueMetaAvroRecord.storedPayloadBytes(wideSchema);
+    assertTrue(wideSize > smallSize);
+
+    Object[] overBinary =
+        limits(null, null, "4", null)
+            .copyRow(rowMeta, new Object[] {"text", json("{\"a\":1}"), binary, 
small});
+    assertNull(overBinary[2]);
+    assertNotNull(overBinary[3]);
+
+    Object[] overSchema =
+        limits(null, null, null, Integer.toString(smallSize))
+            .copyRow(rowMeta, new Object[] {"text", json("{\"a\":1}"), new 
byte[] {1}, wideSchema});
+    assertNull(overSchema[3]);
+
+    Object[] underSchema =
+        limits(null, null, "0", Integer.toString(smallSize))
+            .copyRow(rowMeta, new Object[] {"text", json("{\"a\":1}"), new 
byte[] {1}, small});
+    assertNull(underSchema[2]);
+    assertEquals("hi", ((GenericRecord) underSchema[3]).get("a").toString());
+    assertNotSame(small, underSchema[3]);
+  }
+
+  @Test
+  void binaryStringAndIndexedMarkersMatchTheFieldStorage() throws Exception {
+    String longText = "this is longer than five";
+    SampledValueLimits valueLimits = limits("5", "5", null, null);
+
+    ValueMetaString lazy = new ValueMetaString("body");
+    lazy.setStorageType(IValueMeta.STORAGE_TYPE_BINARY_STRING);
+    lazy.setStorageMetadata(new ValueMetaString("body"));
+    IRowMeta lazyMeta = new RowMeta();
+    lazyMeta.addValueMeta(lazy);
+    Object[] lazyCopy =
+        valueLimits.copyRow(lazyMeta, new Object[] 
{longText.getBytes(StandardCharsets.UTF_8)});
+    assertInstanceOf(byte[].class, lazyCopy[0]);
+    assertEquals(
+        SampledValueLimits.notStored(longText.length(), "characters"),
+        new String((byte[]) lazyCopy[0], StandardCharsets.UTF_8));
+
+    RowBuffer buffer = new RowBuffer(lazyMeta);
+    buffer.addRow(lazyCopy);
+    ExecutionData executionData = new ExecutionData();
+    executionData.getDataSets().put("rows", buffer);
+    assertNotNull(executionData.getRowsBinaryGzipBase64Encoded());
+
+    ValueMetaJson lazyJson = new ValueMetaJson("payload");
+    lazyJson.setStorageType(IValueMeta.STORAGE_TYPE_BINARY_STRING);
+    lazyJson.setStorageMetadata(new ValueMetaString("payload"));
+    IRowMeta jsonMeta = new RowMeta();
+    jsonMeta.addValueMeta(lazyJson);
+    byte[] jsonBytes = 
"{\"a\":\"0123456789\"}".getBytes(StandardCharsets.UTF_8);
+    Object[] jsonCopy = valueLimits.copyRow(jsonMeta, new Object[] 
{jsonBytes});
+    assertInstanceOf(byte[].class, jsonCopy[0]);
+
+    ValueMetaString indexed = new ValueMetaString("body");
+    indexed.setStorageType(IValueMeta.STORAGE_TYPE_INDEXED);
+    indexed.setIndex(new Object[] {"short", longText});
+    indexed.setStorageMetadata(new ValueMetaString("body"));
+    IRowMeta indexedMeta = new RowMeta();
+    indexedMeta.addValueMeta(indexed);
+    Object[] indexedRow = new Object[] {Integer.valueOf(1)};
+    Object[] indexedCopy = valueLimits.copyRow(indexedMeta, indexedRow);
+    assertNull(indexedCopy[0]);
+    assertEquals(Integer.valueOf(1), indexedRow[0]);
+  }
+
+  @Test
+  void aKeptTimestampIsClonedWhenAnotherCellIsOmitted() throws Exception {
+    IRowMeta rowMeta = new RowMeta();
+    rowMeta.addValueMeta(new ValueMetaString("body"));
+    rowMeta.addValueMeta(new ValueMetaTimestamp("when"));
+    Timestamp when = new Timestamp(1_700_000_000_000L);
+    when.setNanos(123_456);
+    Object[] row = new Object[] {"this is longer than five", when};
+
+    Object[] copy = limits("5", null, null, null).copyRow(rowMeta, row);
+
+    assertNotSame(when, copy[1]);
+    assertEquals(when, copy[1]);
+    assertEquals(when.getNanos(), ((Timestamp) copy[1]).getNanos());
+  }
+
+  @Test
+  void nonNumericLimitIsUnlimited() throws Exception {
+    IRowMeta rowMeta = new RowMeta();
+    rowMeta.addValueMeta(new ValueMetaString("body"));
+    String longText = "still stored";
+    Object[] row = new Object[] {longText};
+
+    Object[] copy = limits("many", null, null, null).copyRow(rowMeta, row);
+    Object[] negative = limits("-3", null, null, null).copyRow(rowMeta, row);
+
+    assertEquals(longText, copy[0]);
+    assertEquals(longText, negative[0]);
+  }
+
+  @Test
+  void variableLimitIsResolvedOnce() throws Exception {
+    ExecutionDataProfile profile = new ExecutionDataProfile("profile");
+    profile.setStringValueLimit("${MAX}");
+    Variables variables = new Variables();
+    variables.setVariable("MAX", "0");
+    IRowMeta rowMeta = new RowMeta();
+    rowMeta.addValueMeta(new ValueMetaString("body"));
+
+    Object[] copy =
+        SampledValueLimits.from(profile, variables).copyRow(rowMeta, new 
Object[] {"kept?"});
+
+    assertEquals(SampledValueLimits.NOT_STORED, copy[0]);
+  }
+
+  @Test
+  void profileCopyKeepsTheLimitFields() {
+    ExecutionDataProfile profile = new ExecutionDataProfile("profile");
+    profile.setStringValueLimit("10");
+    profile.setJsonValueLimit("0");
+    profile.setBinaryValueLimit("20");
+    profile.setAvroValueLimit("30");
+
+    ExecutionDataProfile copy = new ExecutionDataProfile(profile);
+
+    assertEquals("10", copy.getStringValueLimit());
+    assertEquals("0", copy.getJsonValueLimit());
+    assertEquals("20", copy.getBinaryValueLimit());
+    assertEquals("30", copy.getAvroValueLimit());
+  }
+
+  @Test
+  void limitsRoundTripThroughMetadata() throws Exception {
+    ExecutionDataProfile profile = new ExecutionDataProfile("limits");
+    profile.setStringValueLimit("10");
+    profile.setJsonValueLimit("0");
+    profile.setBinaryValueLimit("20");
+    profile.setAvroValueLimit("${SAMPLE_AVRO_LIMIT}");
+    IHopMetadataProvider provider = new MemoryMetadataProvider();
+    provider.getSerializer(ExecutionDataProfile.class).save(profile);
+
+    ExecutionDataProfile loaded = 
provider.getSerializer(ExecutionDataProfile.class).load("limits");
+
+    assertEquals("10", loaded.getStringValueLimit());
+    assertEquals("0", loaded.getJsonValueLimit());
+    assertEquals("20", loaded.getBinaryValueLimit());
+    assertEquals("${SAMPLE_AVRO_LIMIT}", loaded.getAvroValueLimit());
+  }
+
+  @Test
+  void profileDeclaresTheLimitWidgets() throws Exception {
+    assertNotNull(ExecutionDataProfile.class.getAnnotation(GuiPlugin.class));
+    assertLimitWidget("stringValueLimit");
+    assertLimitWidget("jsonValueLimit");
+    assertLimitWidget("binaryValueLimit");
+    assertLimitWidget("avroValueLimit");
+  }
+
+  private static void assertLimitWidget(String fieldName) throws Exception {
+    Field field = ExecutionDataProfile.class.getDeclaredField(fieldName);
+    GuiWidgetElement element = field.getAnnotation(GuiWidgetElement.class);
+    assertNotNull(element, fieldName);
+    assertEquals(fieldName, element.id());
+    assertEquals(GuiElementType.TEXT, element.type());
+    assertEquals(ExecutionDataProfile.GUI_PLUGIN_LIMITS_PARENT_ID, 
element.parentId());
+    assertEquals(GuiWidgetGroupType.BOXES, element.groupType());
+    assertEquals("i18n::ExecutionDataProfile.Group.LargeValues", 
element.group());
+    assertTrue(element.label().startsWith("i18n::"));
+    assertTrue(element.toolTip().startsWith("i18n::"));
+  }
+
+  @Test
+  void jacksonDoesNotWriteTheRuntimeLimits() throws Exception {
+    FirstRowsExecutionDataSampler sampler = new 
FirstRowsExecutionDataSampler("10");
+    sampler.setSampledValueLimits(limits("0", "0", "0", "0"));
+
+    String json = HopJson.newMapper().writeValueAsString(sampler);
+
+    assertFalse(json.contains("sampledValueLimits"));
+    assertFalse(json.contains("stringLimit"));
+  }
+
+  @Test
+  void profilingKeepsLengthStatsWithoutTheLongString() throws Exception {
+    BasicDataProfilingDataSampler sampler = new 
BasicDataProfilingDataSampler();
+    sampler.setSampledValueLimits(limits("5", null, null, null));
+    ExecutionDataSamplerMeta samplerMeta =
+        new ExecutionDataSamplerMeta("transform", "0", "log", false, true);
+    BasicDataProfilingDataSamplerStore store = 
sampler.createSamplerStore(samplerMeta);
+    IRowMeta rowMeta = new RowMeta();
+    rowMeta.addValueMeta(new ValueMetaString("name"));
+    rowMeta.addValueMeta(new ValueMetaString("body"));
+    store.init(new Variables(), rowMeta, rowMeta);
+
+    String longText = "this is long";
+    sampler.sampleRow(store, IStream.StreamType.OUTPUT, rowMeta, new Object[] 
{"a", "short"});
+    sampler.sampleRow(store, IStream.StreamType.OUTPUT, rowMeta, new Object[] 
{"b", longText});
+
+    assertEquals(2L, store.getNonNullCounters().get("body"));
+    assertEquals(longText.length(), store.getMaxLengths().get("body"));
+    assertEquals("short", store.getMaxValues().get("body"));
+    assertFalse(store.getMaxValues().containsValue(longText));
+
+    RowBuffer lengthRows =
+        store
+            .getProfileSamples()
+            .get("body")
+            .get(BasicDataProfilingDataSampler.ProfilingType.MaxLength);
+    assertEquals(1, lengthRows.size());
+    Object[] example = lengthRows.getBuffer().get(0);
+    assertEquals("b", example[0]);
+    assertEquals(SampledValueLimits.notStored(longText.length(), 
"characters"), example[1]);
+  }
+
+  @Test
+  void firstRowsReplaceJsonAndKeepTheOtherColumns() throws Exception {
+    FirstRowsExecutionDataSampler sampler = new 
FirstRowsExecutionDataSampler("10");
+    sampler.setSampledValueLimits(limits(null, "0", null, null));
+    ExecutionDataSamplerMeta samplerMeta =
+        new ExecutionDataSamplerMeta("transform", "0", "log", false, true);
+    FirstRowsExecutionDataSamplerStore store = 
sampler.createSamplerStore(samplerMeta);
+    IRowMeta rowMeta = new RowMeta();
+    rowMeta.addValueMeta(new ValueMetaString("id"));
+    rowMeta.addValueMeta(new ValueMetaJson("payload"));
+    store.init(new Variables(), rowMeta, rowMeta);
+    JsonNode payload = json("{\"n\":1}");
+    Object[] row = new Object[] {"row-1", payload};
+
+    sampler.sampleRow(store, IStream.StreamType.OUTPUT, rowMeta, row);
+
+    Object[] stored = store.getRows().get(0);
+    assertNotSame(row, stored);
+    assertEquals("row-1", stored[0]);
+    JsonNode marker = assertInstanceOf(JsonNode.class, stored[1]);
+    assertEquals(SampledValueLimits.NOT_STORED, marker.asText());
+    assertSame(payload, row[1]);
+  }
+
+  private static SampledValueLimits limits(
+      String stringLimit, String jsonLimit, String binaryLimit, String 
avroLimit) {
+    ExecutionDataProfile profile = new ExecutionDataProfile("profile");
+    profile.setStringValueLimit(stringLimit);
+    profile.setJsonValueLimit(jsonLimit);
+    profile.setBinaryValueLimit(binaryLimit);
+    profile.setAvroValueLimit(avroLimit);
+    return SampledValueLimits.from(profile, new Variables());
+  }
+
+  private static IRowMeta sampleRowMeta() {
+    IRowMeta rowMeta = new RowMeta();
+    rowMeta.addValueMeta(new ValueMetaString("text"));
+    rowMeta.addValueMeta(new ValueMetaJson("json"));
+    rowMeta.addValueMeta(new ValueMetaBinary("binary"));
+    rowMeta.addValueMeta(new ValueMetaAvroRecord("avro"));
+    return rowMeta;
+  }
+
+  private static JsonNode json(String text) throws Exception {
+    return JsonUtil.jsonMapper().readTree(text);
+  }
+
+  private static GenericRecord avroRecord(String value) {
+    Schema schema =
+        new Schema.Parser()
+            .parse(
+                
"{\"type\":\"record\",\"name\":\"R\",\"fields\":[{\"name\":\"a\",\"type\":\"string\"}]}");
+    GenericRecord record = new GenericData.Record(schema);
+    record.put("a", value);
+    return record;
+  }
+
+  private static GenericRecord wideSchemaRecord() {
+    Schema schema =
+        new Schema.Parser()
+            .parse(
+                "{\"type\":\"record\",\"name\":\"Wide\",\"doc\":\""
+                    + "x".repeat(400)
+                    + "\",\"fields\":[{\"name\":\"a\",\"type\":\"string\"}]}");
+    GenericRecord record = new GenericData.Record(schema);
+    record.put("a", "hi");
+    return record;
+  }
+}
diff --git 
a/plugins/engines/beam/src/main/java/org/apache/hop/beam/core/transform/TransformBaseFn.java
 
b/plugins/engines/beam/src/main/java/org/apache/hop/beam/core/transform/TransformBaseFn.java
index a7990fdc3f..2f49235912 100644
--- 
a/plugins/engines/beam/src/main/java/org/apache/hop/beam/core/transform/TransformBaseFn.java
+++ 
b/plugins/engines/beam/src/main/java/org/apache/hop/beam/core/transform/TransformBaseFn.java
@@ -166,8 +166,9 @@ public abstract class TransformBaseFn extends DoFn<HopRow, 
HopRow> {
           // If not there's nothing we have to do in this transform really
           //
           String profileName = runConf.getExecutionDataProfileName();
+          ExecutionDataProfile dataProfile = null;
           if (StringUtils.isNotEmpty(profileName)) {
-            ExecutionDataProfile dataProfile =
+            dataProfile =
                 
metadataProvider.getSerializer(ExecutionDataProfile.class).load(profileName);
             if (dataProfile != null) {
               dataSamplers.addAll(dataProfile.getSamplers());
@@ -180,6 +181,9 @@ public abstract class TransformBaseFn extends DoFn<HopRow, 
HopRow> {
                   HopJson.newMapper().readValue(dataSamplersJson, 
IExecutionDataSampler[].class);
               dataSamplers.addAll(Arrays.asList(extraSamplers));
             }
+            if (dataProfile != null) {
+              dataProfile.applyLimits(dataSamplers, variables);
+            }
 
             executionInfoLocation = location;
 
diff --git 
a/plugins/engines/spark/src/main/java/org/apache/hop/spark/execution/SparkTransformExecutionSampling.java
 
b/plugins/engines/spark/src/main/java/org/apache/hop/spark/execution/SparkTransformExecutionSampling.java
index 3c6a2c717c..f9064adebd 100644
--- 
a/plugins/engines/spark/src/main/java/org/apache/hop/spark/execution/SparkTransformExecutionSampling.java
+++ 
b/plugins/engines/spark/src/main/java/org/apache/hop/spark/execution/SparkTransformExecutionSampling.java
@@ -168,9 +168,9 @@ public class SparkTransformExecutionSampling {
     }
 
     String profileName = runConf.getExecutionDataProfileName();
+    ExecutionDataProfile dataProfile = null;
     if (StringUtils.isNotEmpty(profileName)) {
-      ExecutionDataProfile dataProfile =
-          
metadataProvider.getSerializer(ExecutionDataProfile.class).load(profileName);
+      dataProfile = 
metadataProvider.getSerializer(ExecutionDataProfile.class).load(profileName);
       if (dataProfile != null && dataProfile.getSamplers() != null) {
         dataSamplers.addAll(dataProfile.getSamplers());
       } else {
@@ -201,6 +201,10 @@ public class SparkTransformExecutionSampling {
       }
     }
 
+    if (dataProfile != null) {
+      dataProfile.applyLimits(dataSamplers, variables);
+    }
+
     if (dataSamplers.isEmpty()) {
       LogChannel.GENERAL.logBasic(
           "Spark sampling inactive for '"
diff --git 
a/ui/src/main/java/org/apache/hop/ui/execution/profiling/ExecutionDataProfileEditor.java
 
b/ui/src/main/java/org/apache/hop/ui/execution/profiling/ExecutionDataProfileEditor.java
index b1782a6fdd..2ac60109e2 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/execution/profiling/ExecutionDataProfileEditor.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/execution/profiling/ExecutionDataProfileEditor.java
@@ -44,6 +44,7 @@ import org.eclipse.swt.layout.FormData;
 import org.eclipse.swt.layout.FormLayout;
 import org.eclipse.swt.widgets.Composite;
 import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Group;
 import org.eclipse.swt.widgets.Label;
 import org.eclipse.swt.widgets.Listener;
 import org.eclipse.swt.widgets.Text;
@@ -66,8 +67,10 @@ public class ExecutionDataProfileEditor extends 
MetadataEditor<ExecutionDataProf
   private Text wDescription;
   private org.eclipse.swt.widgets.List wSamplers;
 
+  private Composite editorComposite;
   private Composite wPluginSpecificComp;
   private GuiCompositeWidgets guiCompositeWidgets;
+  private GuiCompositeWidgets limitsWidgets;
 
   private Map<String, IExecutionDataSampler> metaMap;
 
@@ -86,6 +89,7 @@ public class ExecutionDataProfileEditor extends 
MetadataEditor<ExecutionDataProf
 
   @Override
   public void createControl(Composite parent) {
+    this.editorComposite = parent;
     PropsUi props = PropsUi.getInstance();
 
     // Create a tabbed interface instead of the confusing left-hand side 
options
@@ -135,6 +139,18 @@ public class ExecutionDataProfileEditor extends 
MetadataEditor<ExecutionDataProf
     wDescription.setLayoutData(fdDescription);
     lastControl = wDescription;
 
+    limitsWidgets = new GuiCompositeWidgets(manager.getVariables());
+    limitsWidgets.createCompositeWidgets(
+        workingProfile,
+        null,
+        parent,
+        ExecutionDataProfile.GUI_PLUGIN_LIMITS_PARENT_ID,
+        wDescription);
+    Control limitsGroup = findGroup(parent);
+    if (limitsGroup != null) {
+      lastControl = limitsGroup;
+    }
+
     Label wlSamplers = new Label(parent, SWT.LEFT);
     PropsUi.setLook(wlSamplers);
     wlSamplers.setText(BaseMessages.getString(PKG, 
"ExecutionDataProfileEditor.label.Samplers"));
@@ -202,6 +218,25 @@ public class ExecutionDataProfileEditor extends 
MetadataEditor<ExecutionDataProf
     wName.addListener(SWT.Modify, modifyListener);
     wDescription.addListener(SWT.Modify, modifyListener);
     wSamplers.addListener(SWT.Selection, e -> addSamplerPluginWidgets());
+    limitsWidgets.setWidgetsListener(
+        new GuiCompositeWidgetsAdapter() {
+          @Override
+          public void widgetModified(
+              GuiCompositeWidgets compositeWidgets, Control changedWidget, 
String widgetId) {
+            setChanged();
+          }
+        });
+  }
+
+  /** The limits group is created on the editor composite, so the sampler list 
can sit under it. */
+  private static Control findGroup(Composite parent) {
+    Control group = null;
+    for (Control child : parent.getChildren()) {
+      if (child instanceof Group) {
+        group = child;
+      }
+    }
+    return group;
   }
 
   private IExecutionDataSampler previousSampler;
@@ -271,6 +306,8 @@ public class ExecutionDataProfileEditor extends 
MetadataEditor<ExecutionDataProf
 
     wName.setText(Const.NVL(workingProfile.getName(), ""));
     wDescription.setText(Const.NVL(workingProfile.getDescription(), ""));
+    limitsWidgets.setWidgetsContents(
+        workingProfile, editorComposite, 
ExecutionDataProfile.GUI_PLUGIN_LIMITS_PARENT_ID);
 
     // Add all the sampler plugins in the profile
     //
@@ -294,6 +331,7 @@ public class ExecutionDataProfileEditor extends 
MetadataEditor<ExecutionDataProf
 
     profile.setName(wName.getText());
     profile.setDescription(wDescription.getText());
+    limitsWidgets.getWidgetsContents(profile, 
ExecutionDataProfile.GUI_PLUGIN_LIMITS_PARENT_ID);
 
     savePreviousSampler();
 

Reply via email to