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

andygrove pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/datafusion-comet.git


The following commit(s) were added to refs/heads/main by this push:
     new 5627ab8c01 perf: reduce allocations when collecting cache statistics 
(#5734)
5627ab8c01 is described below

commit 5627ab8c01473b186db2fbe73c91c599b1387f9b
Author: Peter Lee <[email protected]>
AuthorDate: Wed Sep 9 20:10:36 2026 +0800

    perf: reduce allocations when collecting cache statistics (#5734)
    
    * perf: specialize native cache statistics collection
    
    * refactor: address cache statistics review feedback
---
 .../arrow/ArrowCachedBatchSerializer.scala         | 258 ++++++++++++++++-----
 .../comet/exec/CometInMemoryCacheSuite.scala       | 151 ++++++++++++
 .../benchmark/CometInMemoryCacheBenchmark.scala    |  43 ++++
 3 files changed, 394 insertions(+), 58 deletions(-)

diff --git 
a/spark/src/main/scala/org/apache/spark/sql/comet/execution/arrow/ArrowCachedBatchSerializer.scala
 
b/spark/src/main/scala/org/apache/spark/sql/comet/execution/arrow/ArrowCachedBatchSerializer.scala
index 46a51ad877..821f84e0c2 100644
--- 
a/spark/src/main/scala/org/apache/spark/sql/comet/execution/arrow/ArrowCachedBatchSerializer.scala
+++ 
b/spark/src/main/scala/org/apache/spark/sql/comet/execution/arrow/ArrowCachedBatchSerializer.scala
@@ -19,6 +19,8 @@
 
 package org.apache.spark.sql.comet.execution.arrow
 
+import java.lang.{Boolean => JBoolean, Byte => JByte, Double => JDouble, Float 
=> JFloat, Integer => JInteger, Long => JLong, Short => JShort}
+
 import scala.collection.JavaConverters._
 import scala.util.control.NonFatal
 
@@ -26,6 +28,7 @@ import org.apache.spark.TaskContext
 import org.apache.spark.rdd.RDD
 import org.apache.spark.sql.catalyst.InternalRow
 import org.apache.spark.sql.catalyst.expressions.{Attribute, Expression, 
GenericInternalRow, IsNotNull, IsNull, UnsafeProjection}
+import org.apache.spark.sql.catalyst.util.TypeUtils
 import org.apache.spark.sql.columnar.{CachedBatch, SimpleMetricsCachedBatch, 
SimpleMetricsCachedBatchSerializer}
 import org.apache.spark.sql.comet.util.Utils
 import org.apache.spark.sql.execution.columnar.{DefaultCachedBatch, 
DefaultCachedBatchSerializer}
@@ -33,7 +36,7 @@ import org.apache.spark.sql.internal.SQLConf
 import org.apache.spark.sql.types._
 import org.apache.spark.sql.vectorized.{ColumnarBatch, ColumnVector}
 import org.apache.spark.storage.StorageLevel
-import org.apache.spark.unsafe.types.{ByteArray, UTF8String}
+import org.apache.spark.unsafe.types.UTF8String
 import org.apache.spark.util.io.ChunkedByteBuffer
 
 import org.apache.comet.CometArrowAllocator
@@ -78,40 +81,220 @@ class ArrowCachedBatchSerializer extends 
SimpleMetricsCachedBatchSerializer {
   // Bounds and null counts per column, gathered before the batch is 
serialized: serializing
   // clears the batch's vectors, and the per-column byte sizes that complete 
the statistics row
   // are only known afterwards. See statsRow.
-  private def gatherColumnStats(
+  private[sql] def gatherColumnStats(
       batch: ColumnarBatch,
       attrs: Seq[Attribute]): (Array[Any], Array[Any], Array[Int]) = {
     val numCols = attrs.length
     val lower = new Array[Any](numCols)
     val upper = new Array[Any](numCols)
-    val nulls = Array.fill[Int](numCols)(0)
+    val nulls = new Array[Int](numCols)
     val numRows = batch.numRows()
 
     var c = 0
     while (c < numCols) {
-      val dt = attrs(c).dataType
       val col = batch.column(c)
-      var r = 0
-      while (r < numRows) {
-        if (col.isNullAt(r)) {
-          nulls(c) += 1
-        } else if (tracksBounds(dt)) {
-          val value = readValue(col, dt, r)
-          if (lower(c) == null || compare(dt, value, lower(c)) < 0) {
-            lower(c) = value
-          }
-          if (upper(c) == null || compare(dt, value, upper(c)) > 0) {
-            upper(c) = value
+      val (min, max, nullCount) = attrs(c).dataType match {
+        case BooleanType => gatherBooleanStats(col, numRows)
+        case ByteType => gatherByteStats(col, numRows)
+        case ShortType => gatherShortStats(col, numRows)
+        case IntegerType | DateType => gatherIntStats(col, numRows)
+        case LongType | TimestampType | TimestampNTZType => 
gatherLongStats(col, numRows)
+        case FloatType => gatherFloatStats(col, numRows)
+        case DoubleType => gatherDoubleStats(col, numRows)
+        case d: DecimalType => gatherDecimalStats(col, numRows, d)
+        case StringType => gatherStringStats(col, numRows)
+        case other =>
+          assert(!tracksBounds(other), s"Missing cache bounds implementation 
for $other")
+          var nullCount = 0
+          var r = 0
+          while (r < numRows) {
+            if (col.isNullAt(r)) nullCount += 1
+            r += 1
           }
-        }
-        r += 1
+          (null, null, nullCount)
       }
+      if (nullCount < numRows) {
+        lower(c) = min
+        upper(c) = max
+      }
+      nulls(c) = nullCount
       c += 1
     }
 
     (lower, upper, nulls)
   }
 
+  // Keep each loop specialized and box only its final bounds. r == nullCount 
identifies
+  // the first non-null value, including when a column starts with nulls.
+  private def gatherBooleanStats(col: ColumnVector, numRows: Int): (Boolean, 
Boolean, Int) = {
+    var min = false
+    var max = false
+    var nullCount = 0
+    var r = 0
+    while (r < numRows) {
+      if (col.isNullAt(r)) {
+        nullCount += 1
+      } else {
+        val value = col.getBoolean(r)
+        if (r == nullCount || JBoolean.compare(value, min) < 0) min = value
+        if (r == nullCount || JBoolean.compare(value, max) > 0) max = value
+      }
+      r += 1
+    }
+    (min, max, nullCount)
+  }
+
+  private def gatherByteStats(col: ColumnVector, numRows: Int): (Byte, Byte, 
Int) = {
+    var min = 0.toByte
+    var max = 0.toByte
+    var nullCount = 0
+    var r = 0
+    while (r < numRows) {
+      if (col.isNullAt(r)) {
+        nullCount += 1
+      } else {
+        val value = col.getByte(r)
+        if (r == nullCount || JByte.compare(value, min) < 0) min = value
+        if (r == nullCount || JByte.compare(value, max) > 0) max = value
+      }
+      r += 1
+    }
+    (min, max, nullCount)
+  }
+
+  private def gatherShortStats(col: ColumnVector, numRows: Int): (Short, 
Short, Int) = {
+    var min = 0.toShort
+    var max = 0.toShort
+    var nullCount = 0
+    var r = 0
+    while (r < numRows) {
+      if (col.isNullAt(r)) {
+        nullCount += 1
+      } else {
+        val value = col.getShort(r)
+        if (r == nullCount || JShort.compare(value, min) < 0) min = value
+        if (r == nullCount || JShort.compare(value, max) > 0) max = value
+      }
+      r += 1
+    }
+    (min, max, nullCount)
+  }
+
+  private def gatherIntStats(col: ColumnVector, numRows: Int): (Int, Int, Int) 
= {
+    var min = 0
+    var max = 0
+    var nullCount = 0
+    var r = 0
+    while (r < numRows) {
+      if (col.isNullAt(r)) {
+        nullCount += 1
+      } else {
+        val value = col.getInt(r)
+        if (r == nullCount || JInteger.compare(value, min) < 0) min = value
+        if (r == nullCount || JInteger.compare(value, max) > 0) max = value
+      }
+      r += 1
+    }
+    (min, max, nullCount)
+  }
+
+  private def gatherLongStats(col: ColumnVector, numRows: Int): (Long, Long, 
Int) = {
+    var min = 0L
+    var max = 0L
+    var nullCount = 0
+    var r = 0
+    while (r < numRows) {
+      if (col.isNullAt(r)) {
+        nullCount += 1
+      } else {
+        val value = col.getLong(r)
+        if (r == nullCount || JLong.compare(value, min) < 0) min = value
+        if (r == nullCount || JLong.compare(value, max) > 0) max = value
+      }
+      r += 1
+    }
+    (min, max, nullCount)
+  }
+
+  private def gatherFloatStats(col: ColumnVector, numRows: Int): (Float, 
Float, Int) = {
+    var min = 0.0f
+    var max = 0.0f
+    var nullCount = 0
+    var r = 0
+    while (r < numRows) {
+      if (col.isNullAt(r)) {
+        nullCount += 1
+      } else {
+        val value = col.getFloat(r)
+        if (r == nullCount || JFloat.compare(value, min) < 0) min = value
+        if (r == nullCount || JFloat.compare(value, max) > 0) max = value
+      }
+      r += 1
+    }
+    (min, max, nullCount)
+  }
+
+  private def gatherDoubleStats(col: ColumnVector, numRows: Int): (Double, 
Double, Int) = {
+    var min = 0.0d
+    var max = 0.0d
+    var nullCount = 0
+    var r = 0
+    while (r < numRows) {
+      if (col.isNullAt(r)) {
+        nullCount += 1
+      } else {
+        val value = col.getDouble(r)
+        if (r == nullCount || JDouble.compare(value, min) < 0) min = value
+        if (r == nullCount || JDouble.compare(value, max) > 0) max = value
+      }
+      r += 1
+    }
+    (min, max, nullCount)
+  }
+
+  private def gatherDecimalStats(
+      col: ColumnVector,
+      numRows: Int,
+      dt: DecimalType): (Decimal, Decimal, Int) = {
+    var min: Decimal = null
+    var max: Decimal = null
+    var nullCount = 0
+    var r = 0
+    while (r < numRows) {
+      if (col.isNullAt(r)) {
+        nullCount += 1
+      } else {
+        val value = col.getDecimal(r, dt.precision, dt.scale)
+        if (r == nullCount || value.compare(min) < 0) min = value
+        if (r == nullCount || value.compare(max) > 0) max = value
+      }
+      r += 1
+    }
+    (min, max, nullCount)
+  }
+
+  private def gatherStringStats(
+      col: ColumnVector,
+      numRows: Int): (UTF8String, UTF8String, Int) = {
+    val ordering = TypeUtils.getInterpretedOrdering(StringType)
+    var min: UTF8String = null
+    var max: UTF8String = null
+    var nullCount = 0
+    var r = 0
+    while (r < numRows) {
+      if (col.isNullAt(r)) {
+        nullCount += 1
+      } else {
+        val value = col.getUTF8String(r)
+        // Borrow the value for comparison, but retain owned copies of the 
bounds.
+        if (r == nullCount || ordering.compare(value, min) < 0) min = 
value.copy()
+        if (r == nullCount || ordering.compare(value, max) > 0) max = 
value.copy()
+      }
+      r += 1
+    }
+    (min, max, nullCount)
+  }
+
   // Build the statistics row expected by SimpleMetricsCachedBatchSerializer.
   // For each cached column Spark expects five values in this order:
   // lower bound, upper bound, null count, row count, and size in bytes.
@@ -149,47 +332,6 @@ class ArrowCachedBatchSerializer extends 
SimpleMetricsCachedBatchSerializer {
     case _ => false
   }
 
-  // Read a non-null value from a ColumnVector using Spark's internal value 
type
-  // for the corresponding DataType.
-  private def readValue(col: ColumnVector, dt: DataType, rowId: Int): Any = dt 
match {
-    case BooleanType => col.getBoolean(rowId)
-    case ByteType => col.getByte(rowId)
-    case ShortType => col.getShort(rowId)
-    case IntegerType | DateType => col.getInt(rowId)
-    case LongType | TimestampType | TimestampNTZType => col.getLong(rowId)
-    case FloatType => col.getFloat(rowId)
-    case DoubleType => col.getDouble(rowId)
-    case d: DecimalType => col.getDecimal(rowId, d.precision, d.scale)
-    case StringType => col.getUTF8String(rowId).copy()
-    case _ => null
-  }
-
-  // Compare values using the same physical representation used in the stats 
row.
-  private def compare(dt: DataType, left: Any, right: Any): Int = dt match {
-    case BooleanType =>
-      java.lang.Boolean.compare(left.asInstanceOf[Boolean], 
right.asInstanceOf[Boolean])
-    case ByteType =>
-      java.lang.Byte.compare(left.asInstanceOf[Byte], right.asInstanceOf[Byte])
-    case ShortType =>
-      java.lang.Short.compare(left.asInstanceOf[Short], 
right.asInstanceOf[Short])
-    case IntegerType | DateType =>
-      java.lang.Integer.compare(left.asInstanceOf[Int], 
right.asInstanceOf[Int])
-    case LongType | TimestampType | TimestampNTZType =>
-      java.lang.Long.compare(left.asInstanceOf[Long], right.asInstanceOf[Long])
-    case FloatType =>
-      java.lang.Float.compare(left.asInstanceOf[Float], 
right.asInstanceOf[Float])
-    case DoubleType =>
-      java.lang.Double.compare(left.asInstanceOf[Double], 
right.asInstanceOf[Double])
-    case _: DecimalType =>
-      left.asInstanceOf[Decimal].compare(right.asInstanceOf[Decimal])
-    case StringType =>
-      ByteArray.compareBinary(
-        left.asInstanceOf[UTF8String].getBytes,
-        right.asInstanceOf[UTF8String].getBytes)
-    case other =>
-      throw new IllegalStateException(s"compare called for unsupported type 
$other")
-  }
-
   // Compute Spark-compatible cache stats before serializing each batch to 
Arrow.
   // The stats are stored beside the Arrow bytes so Spark's cache filter can 
prune
   // CometCachedBatch without decoding the batch first.
diff --git 
a/spark/src/test/scala/org/apache/comet/exec/CometInMemoryCacheSuite.scala 
b/spark/src/test/scala/org/apache/comet/exec/CometInMemoryCacheSuite.scala
index 065299f101..e72f4d10f7 100644
--- a/spark/src/test/scala/org/apache/comet/exec/CometInMemoryCacheSuite.scala
+++ b/spark/src/test/scala/org/apache/comet/exec/CometInMemoryCacheSuite.scala
@@ -36,6 +36,7 @@ import org.apache.spark.sql.execution.exchange.{Exchange, 
ReusedExchangeExec, Sh
 import org.apache.spark.sql.execution.joins.{BroadcastHashJoinExec, 
SortMergeJoinExec}
 import org.apache.spark.sql.functions.max
 import org.apache.spark.sql.internal.{SQLConf, StaticSQLConf}
+import org.apache.spark.sql.types._
 import org.apache.spark.storage.StorageLevel
 
 import org.apache.comet.{CometArrowAllocator, CometConf}
@@ -483,6 +484,156 @@ class CometInMemoryCacheSuite extends CometTestBase {
     }
   }
 
+  test("Comet in-memory cache statistics preserve typed bounds and null 
counts") {
+    withSQLConf(
+      CometConf.COMET_ENABLED.key -> "false",
+      SQLConf.ADAPTIVE_EXECUTION_ENABLED.key -> "false") {
+      val types = Seq(
+        "boolean",
+        "tinyint",
+        "smallint",
+        "int",
+        "bigint",
+        "float",
+        "double",
+        "decimal(10,2)",
+        "decimal(38,2)",
+        "string",
+        "date",
+        "timestamp",
+        "timestamp_ntz",
+        "binary")
+      val expressions = types.zipWithIndex.map { case (dt, i) =>
+        val value = dt match {
+          case "date" => "date_add(DATE '2000-01-01', cast(v AS INT))"
+          case "timestamp" | "timestamp_ntz" =>
+            s"cast(date_add(DATE '2000-01-01', cast(v AS INT)) AS $dt)"
+          case "string" | "binary" => s"cast(concat('字', cast(v AS STRING)) AS 
$dt)"
+          case _ => s"cast(v AS $dt)"
+        }
+        s"$value AS c$i"
+      }
+      // Leading nulls, updates in both directions, duplicate values, all-null 
and single-value
+      // columns exercise initialization as well as the primitive and 
reference bounds loops.
+      Seq("(NULL), (2), (-3), (0), (1), (2), (NULL)", "(NULL), (NULL)", 
"(NULL), (1)").foreach {
+        values =>
+          val df = spark
+            .sql(s"SELECT ${expressions.mkString(", ")} FROM VALUES $values AS 
t(v)")
+            .coalesce(1)
+          // Compute the reference through Spark before caching, using its 
internal value types.
+          df.createOrReplaceTempView("typed_stats_input")
+          val expected = spark
+            .sql(
+              s"SELECT ${types.indices.flatMap(i => Seq(s"min(c$i)", 
s"max(c$i)")).mkString(", ")} " +
+                "FROM typed_stats_input")
+            .queryExecution
+            .toRdd
+            .map(_.copy())
+            .collect()
+            .head
+          val expectedNulls = df.filter("c0 IS NULL").count().toInt
+          val expectedRows = df.count().toInt
+          def checkStats(view: String): Unit = {
+            val relation = 
spark.sharedState.cacheManager.lookupCachedData(spark.table(view)).get
+            val batches = 
relation.cachedRepresentation.cacheBuilder.cachedColumnBuffers.collect()
+            assert(batches.length == 1)
+            val stats = 
batches.head.asInstanceOf[SimpleMetricsCachedBatch].stats
+            df.schema.fields.zipWithIndex.foreach { case (field, i) =>
+              if (field.dataType == BinaryType) {
+                assert(stats.isNullAt(i * 5) && stats.isNullAt(i * 5 + 1))
+              } else {
+                assert(stats.get(i * 5, field.dataType) == expected.get(i * 2, 
field.dataType))
+                assert(
+                  stats.get(i * 5 + 1, field.dataType) ==
+                    expected.get(i * 2 + 1, field.dataType))
+              }
+              assert(stats.getInt(i * 5 + 2) == expectedNulls)
+              assert(stats.getInt(i * 5 + 3) == expectedRows)
+            }
+          }
+
+          df.cache()
+          try {
+            df.count()
+            checkStats("typed_stats_input")
+          } finally {
+            df.unpersist(blocking = true)
+            spark.catalog.dropTempView("typed_stats_input")
+          }
+
+          withSparkColumnarCache("typed_stats_columnar")(path => 
df.write.parquet(path)) {
+            val relation = spark.sharedState.cacheManager
+              .lookupCachedData(spark.table("typed_stats_columnar"))
+              .get
+              .cachedRepresentation
+            assert(relation.cacheBuilder.cachedPlan.supportsColumnar)
+            checkStats("typed_stats_columnar")
+          }
+      }
+    }
+  }
+
+  test("Comet in-memory cache statistics preserve numeric extremes and 
floating-point ordering") {
+    withSQLConf(
+      CometConf.COMET_ENABLED.key -> "false",
+      SQLConf.ADAPTIVE_EXECUTION_ENABLED.key -> "false") {
+      val df = spark
+        .sql("""
+        SELECT
+          CAST(if(id = 0, -128, 127) AS TINYINT) AS b,
+          CAST(if(id = 0, -32768, 32767) AS SMALLINT) AS s,
+          CAST(if(id = 0, -2147483648, 2147483647) AS INT) AS i,
+          if(id = 0, -9223372036854775808L, 9223372036854775807L) AS l,
+          CAST(v AS FLOAT) AS f,
+          CAST(v AS DOUBLE) AS d,
+          CAST(if(id = 0, '0.0', '-0.0') AS FLOAT) AS fz,
+          CAST(if(id = 0, '0.0', '-0.0') AS DOUBLE) AS dz
+        FROM VALUES (0, '0.0'), (1, '-0.0'), (2, '-Infinity'), (3, 
'Infinity'), (4, 'NaN')
+        AS t(id, v)
+      """)
+        .coalesce(1)
+
+      def checkStats(view: String): Unit = {
+        val relation = 
spark.sharedState.cacheManager.lookupCachedData(spark.table(view)).get
+        val batches = 
relation.cachedRepresentation.cacheBuilder.cachedColumnBuffers.collect()
+        assert(batches.length == 1)
+        val stats = batches.head.asInstanceOf[SimpleMetricsCachedBatch].stats
+        assert(stats.getByte(0) == Byte.MinValue && stats.getByte(1) == 
Byte.MaxValue)
+        assert(stats.getShort(5) == Short.MinValue && stats.getShort(6) == 
Short.MaxValue)
+        assert(stats.getInt(10) == Int.MinValue && stats.getInt(11) == 
Int.MaxValue)
+        assert(stats.getLong(15) == Long.MinValue && stats.getLong(16) == 
Long.MaxValue)
+        assert(stats.getFloat(20) == Float.NegativeInfinity && 
stats.getFloat(21).isNaN)
+        assert(stats.getDouble(25) == Double.NegativeInfinity && 
stats.getDouble(26).isNaN)
+        // Spark aggregates consider signed zeros equal; the cache stores 
Java's total ordering.
+        assert(
+          java.lang.Float.floatToRawIntBits(stats.getFloat(30)) ==
+            java.lang.Float.floatToRawIntBits(-0.0f))
+        assert(java.lang.Float.floatToRawIntBits(stats.getFloat(31)) == 0)
+        assert(
+          java.lang.Double.doubleToRawLongBits(stats.getDouble(35)) ==
+            java.lang.Double.doubleToRawLongBits(-0.0d))
+        assert(java.lang.Double.doubleToRawLongBits(stats.getDouble(36)) == 0L)
+        (0 until 8).foreach { c =>
+          assert(stats.getInt(c * 5 + 2) == 0)
+          assert(stats.getInt(c * 5 + 3) == 5)
+        }
+      }
+
+      df.createOrReplaceTempView("extreme_stats_input")
+      df.cache()
+      try {
+        df.count()
+        checkStats("extreme_stats_input")
+      } finally {
+        df.unpersist(blocking = true)
+        spark.catalog.dropTempView("extreme_stats_input")
+      }
+      withSparkColumnarCache("extreme_stats_columnar")(path => 
df.write.parquet(path)) {
+        checkStats("extreme_stats_columnar")
+      }
+    }
+  }
+
   test("Comet in-memory cache supports stats-based batch pruning") {
     withSQLConf(
       SQLConf.ADAPTIVE_EXECUTION_ENABLED.key -> "false",
diff --git 
a/spark/src/test/scala/org/apache/spark/sql/benchmark/CometInMemoryCacheBenchmark.scala
 
b/spark/src/test/scala/org/apache/spark/sql/benchmark/CometInMemoryCacheBenchmark.scala
index 959b5590b3..7d171ac26e 100644
--- 
a/spark/src/test/scala/org/apache/spark/sql/benchmark/CometInMemoryCacheBenchmark.scala
+++ 
b/spark/src/test/scala/org/apache/spark/sql/benchmark/CometInMemoryCacheBenchmark.scala
@@ -19,10 +19,17 @@
 
 package org.apache.spark.sql.benchmark
 
+import java.nio.charset.StandardCharsets
+
 import org.apache.spark.SparkConf
 import org.apache.spark.benchmark.Benchmark
 import org.apache.spark.sql.SparkSession
+import org.apache.spark.sql.catalyst.expressions.AttributeReference
+import org.apache.spark.sql.comet.execution.arrow.ArrowCachedBatchSerializer
+import org.apache.spark.sql.execution.vectorized.OnHeapColumnVector
 import org.apache.spark.sql.internal.SQLConf
+import org.apache.spark.sql.types.{DataType, LongType, StringType}
+import org.apache.spark.sql.vectorized.{ColumnarBatch, ColumnVector}
 
 import org.apache.comet.{CometConf, CometSparkSessionExtensions}
 
@@ -30,6 +37,7 @@ object CometInMemoryCacheBenchmark extends CometBenchmarkBase 
{
   private val numRows = 5 * 1000 * 1000
   private val cacheTable = "comet_cache_bench"
   private val sourceTable = "comet_cache_bench_src"
+  @volatile private var statsResult: (Array[Any], Array[Any], Array[Int]) = _
 
   override def getSparkSession: SparkSession = {
     val conf = new SparkConf()
@@ -61,6 +69,10 @@ object CometInMemoryCacheBenchmark extends 
CometBenchmarkBase {
   }
 
   override def runCometBenchmark(args: Array[String]): Unit = {
+    runStatsBenchmark()
+    // Run just the JVM statistics loop without constructing or scanning a 
cached relation.
+    if (args.contains("--stats-only")) return
+
     withTempTable(sourceTable, cacheTable) {
       spark
         .range(0, numRows, 1, 16)
@@ -102,6 +114,37 @@ object CometInMemoryCacheBenchmark extends 
CometBenchmarkBase {
     }
   }
 
+  private def runStatsBenchmark(): Unit = {
+    val batchSize = 10000
+    val types: Seq[DataType] = Seq.fill(3)(LongType) ++ Seq.fill(3)(StringType)
+    val attrs = types.zipWithIndex.map { case (dt, i) => 
AttributeReference(s"c$i", dt)() }
+    val columns = types.map(dt => new OnHeapColumnVector(batchSize, dt))
+    val batch = new ColumnarBatch(columns.map(c => c: ColumnVector).toArray, 
batchSize)
+    try {
+      var r = 0
+      while (r < batchSize) {
+        columns(0).putLong(r, r.toLong)
+        columns(1).putLong(r, r % 1000)
+        columns(2).putLong(r, r + 1)
+        columns(3).putByteArray(r, s"str_a_${r % 
100000}".getBytes(StandardCharsets.UTF_8))
+        columns(4).putByteArray(r, s"str_b_${r % 
7919}".getBytes(StandardCharsets.UTF_8))
+        columns(5).putByteArray(r, 
s"str_c_$r".getBytes(StandardCharsets.UTF_8))
+        r += 1
+      }
+      val serializer = new ArrowCachedBatchSerializer
+      val benchmark = new Benchmark("in-memory cache statistics", numRows, 
output = output)
+      // One case measures this collector across commits; Spark's default 
cache has its own collector.
+      benchmark.addCase("Comet statistics collector") { _ =>
+        var i = 0
+        while (i < numRows / batchSize) {
+          statsResult = serializer.gatherColumnStats(batch, attrs)
+          i += 1
+        }
+      }
+      benchmark.run()
+    } finally batch.close()
+  }
+
   private def runCacheBenchmark(name: String, query: String): Unit = {
     withCachedTable {
       withSQLConf(cacheConf(nativeCacheEnabled = false): _*) {


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to