This is an automated email from the ASF dual-hosted git repository.
gengliangwang pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/spark.git
The following commit(s) were added to refs/heads/master by this push:
new bcad90da1266 [SPARK-57915][SQL][TEST] Add a TPC-DS whole-stage-codegen
size benchmark
bcad90da1266 is described below
commit bcad90da12663e31fc5d13e8469482af20d8ac81
Author: Gengliang Wang <[email protected]>
AuthorDate: Sat Jul 4 09:38:58 2026 -0700
[SPARK-57915][SQL][TEST] Add a TPC-DS whole-stage-codegen size benchmark
### What changes were proposed in this pull request?
Add `WholeStageCodegenSizeBenchmark`, a planning-only benchmark that
measures the size of the Java
code generated by whole-stage codegen across the TPC-DS queries. Unlike
`TPCDSQueryBenchmark`, it
does not execute the queries and needs no data: it creates the TPC-DS
tables empty (with SF=100 stats
injected so plan shapes are realistic), plans each query to its
`executedPlan`, walks the
`WholeStageCodegenExec` subtrees via the existing `debug.codegenStringSeq`,
and reports grand totals:
- source code size (chars of generated Java, summed over stages);
- max method bytecode, summed over stages, and the single max -- the metric
behind the HotSpot 8KB
JIT-compilation threshold and the 64KB method limit;
- number of generated inner classes;
- constant-pool entries (sum and max);
- total Janino compile time (ms), measured cold via
`CodeGenerator.resetCompileTime()`;
- codegen fallbacks (stages whose generated code failed to compile);
- whole-stage-codegen stage count and number of queries measured.
Results are written to
`sql/core/benchmarks/WholeStageCodegenSizeBenchmark-results.txt`.
### Why are the changes needed?
The umbrella
[SPARK-56908](https://issues.apache.org/jira/browse/SPARK-56908) tracks changes
that
reduce the size of generated whole-stage-codegen Java (smaller generated
methods reduce Janino
compilation work, JIT work, and pressure against the JVM 64KB method limit
/ HotSpot 8KB
JIT-compilation threshold). Those changes so far have been justified with
ad-hoc, out-of-tree
measurements. This adds a standard, reproducible, CI-runnable instrument so
any such change has a
consistent before/after size measurement over a realistic query set.
### Does this PR introduce _any_ user-facing change?
No. This is a test-only benchmark.
### How was this patch tested?
Ran `build/sbt "sql/Test/runMain
org.apache.spark.sql.execution.benchmark.WholeStageCodegenSizeBenchmark"`
locally: it plans all TPC-DS v1.4 + v2.7 queries with no data location and
prints the summary; the
committed results file was produced with
`SPARK_GENERATE_BENCHMARK_FILES=1`. Codegen fallbacks are 0
on master.
### Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Opus 4.8)
Closes #56981 from gengliangwang/SPARK-57915-codegen-size-benchmark.
Authored-by: Gengliang Wang <[email protected]>
Signed-off-by: Gengliang Wang <[email protected]>
---
.../WholeStageCodegenSizeBenchmark-results.txt | 16 ++
.../benchmark/WholeStageCodegenSizeBenchmark.scala | 236 +++++++++++++++++++++
2 files changed, 252 insertions(+)
diff --git a/sql/core/benchmarks/WholeStageCodegenSizeBenchmark-results.txt
b/sql/core/benchmarks/WholeStageCodegenSizeBenchmark-results.txt
new file mode 100644
index 000000000000..7dc8f3ae5e46
--- /dev/null
+++ b/sql/core/benchmarks/WholeStageCodegenSizeBenchmark-results.txt
@@ -0,0 +1,16 @@
+================================================================================================
+TPCDS codegen size
+================================================================================================
+
+queries measured 135
+codegen stages 2247
+source code size (chars) 23794382
+max method bytecode (sum) 925512
+max method bytecode (max) 5843
+inner classes 516
+constant pool (sum) 440540
+constant pool (max) 770
+codegen fallbacks 0
+compile time (ms) 10618.3
+
+
diff --git
a/sql/core/src/test/scala/org/apache/spark/sql/execution/benchmark/WholeStageCodegenSizeBenchmark.scala
b/sql/core/src/test/scala/org/apache/spark/sql/execution/benchmark/WholeStageCodegenSizeBenchmark.scala
new file mode 100644
index 000000000000..0a9fa43ea397
--- /dev/null
+++
b/sql/core/src/test/scala/org/apache/spark/sql/execution/benchmark/WholeStageCodegenSizeBenchmark.scala
@@ -0,0 +1,236 @@
+/*
+ * 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.spark.sql.execution.benchmark
+
+import scala.util.control.NonFatal
+
+import org.apache.spark.SparkConf
+import org.apache.spark.benchmark.Benchmark
+import org.apache.spark.internal.Logging
+import org.apache.spark.sql.{SparkSession, TPCDSSchema, TPCDSTableStats}
+import org.apache.spark.sql.catalyst.TableIdentifier
+import org.apache.spark.sql.catalyst.expressions.codegen.{ByteCodeStats,
CodeGenerator}
+import org.apache.spark.sql.catalyst.util._
+import org.apache.spark.sql.execution.debug._
+import org.apache.spark.sql.internal.SQLConf
+
+/**
+ * Benchmark to measure the size of the Java code generated by whole-stage
codegen for the TPC-DS
+ * queries. Unlike [[TPCDSQueryBenchmark]], this does not execute the queries
and needs no data: it
+ * plans each query over empty TPC-DS tables, collects the whole-stage-codegen
subtrees, and reports
+ * how large the generated (and compiled) code is. It is the standard
instrument for changes that
+ * aim to reduce generated Java size (see the umbrella SPARK-56908).
+ *
+ * The reported grand totals (lower is better, except the query/stage counts)
are:
+ * - source code size: characters of generated Java summed over all stages;
+ * - max method bytecode (sum): the per-stage largest compiled method size,
summed over stages --
+ * the metric behind the HotSpot 8KB JIT-compilation threshold and the
64KB method limit;
+ * - max method bytecode (max): the single largest compiled method across
all stages;
+ * - inner classes: number of generated inner classes summed over all stages;
+ * - constant pool (sum / max): compiled constant-pool entries, summed and
the per-stage max;
+ * - compile time: total Janino compilation time (ms), measured cold via
+ * [[CodeGenerator.resetCompileTime()]];
+ * - codegen fallbacks: stages whose generated code failed to compile (fell
back to interpreted);
+ * - stages / queries: the whole-stage-codegen stage count and the number of
queries measured.
+ *
+ * To run this benchmark:
+ * {{{
+ * 1. without sbt:
+ * bin/spark-submit --jars <spark core test jar>,<spark catalyst test
jar>
+ * --class <this class> <spark sql test jar>
+ * 2. build/sbt "sql/Test/runMain <this class>"
+ * 3. generate result:
+ * SPARK_GENERATE_BENCHMARK_FILES=1 build/sbt "sql/Test/runMain <this
class>"
+ * Results will be written to
+ * "sql/core/benchmarks/WholeStageCodegenSizeBenchmark-results.txt".
+ * }}}
+ */
+object WholeStageCodegenSizeBenchmark extends SqlBasedBenchmark with Logging {
+
+ // Inject SF=100 table stats so join/aggregate strategy selection (and
therefore the shape of the
+ // generated code) resembles planning on real data, matching
PlanStabilitySuite's sf100 plans.
+ private val injectStats = true
+
+ override def getSparkSession: SparkSession = {
+ val conf = new SparkConf()
+ .setMaster(System.getProperty("spark.sql.test.master", "local[1]"))
+ .setAppName(this.getClass.getCanonicalName)
+ .set(SQLConf.WHOLESTAGE_CODEGEN_ENABLED.key, "true")
+ // Keep the codegen fallback path so we can count stages that fail to
compile, instead of
+ // failing the benchmark.
+ .set(SQLConf.CODEGEN_FALLBACK.key, "true")
+ // Disable AQE so the physical plan (and its whole-stage-codegen stages)
is fully materialized
+ // at planning time, without needing to execute the query to resolve
query stages.
+ .set(SQLConf.ADAPTIVE_EXECUTION_ENABLED.key, "false")
+ .set(SQLConf.SHUFFLE_PARTITIONS.key, "4")
+ .set("spark.sql.crossJoin.enabled", "true")
+ .set("spark.sql.sources.default", "parquet")
+
+ val spark = SparkSession.builder().config(conf).getOrCreate()
+ if (injectStats) {
+ // Stats-based strategies (broadcast vs shuffle join, etc.) need the CBO
on to be applied.
+ spark.conf.set(SQLConf.CBO_ENABLED.key, "true")
+ spark.conf.set(SQLConf.PLAN_STATS_ENABLED.key, "true")
+ spark.conf.set(SQLConf.JOIN_REORDER_ENABLED.key, "true")
+ }
+ spark
+ }
+
+ /** Grand-total accumulators across all measured stages of all queries. */
+ private case class Totals(
+ var queries: Int = 0,
+ var stages: Int = 0,
+ var sourceChars: Long = 0L,
+ var maxMethodBytecodeSum: Long = 0L,
+ var maxMethodBytecodeMax: Long = 0L,
+ var innerClasses: Long = 0L,
+ var constPoolSum: Long = 0L,
+ var constPoolMax: Long = 0L,
+ var fallbacks: Int = 0)
+
+ private def createTables(): Unit = {
+ CodegenSizeTpcdsSchema.tableNames.foreach { tableName =>
+ spark.sql(
+ s"""
+ |CREATE TABLE `$tableName`
(${CodegenSizeTpcdsSchema.columnsDDL(tableName)})
+ |USING parquet
+ |${CodegenSizeTpcdsSchema.partitionedByClause(tableName)}
+ """.stripMargin)
+ if (injectStats) {
+ spark.sessionState.catalog.alterTableStats(
+ TableIdentifier(tableName),
Some(TPCDSTableStats.sf100TableStats(tableName)))
+ }
+ }
+ }
+
+ private def dropTables(): Unit = {
+ CodegenSizeTpcdsSchema.tableNames.foreach { tableName =>
+ spark.sessionState.catalog.dropTable(TableIdentifier(tableName),
ignoreIfNotExists = true,
+ purge = true)
+ }
+ }
+
+ /** Plans one query and folds its whole-stage-codegen size metrics into
`totals`. */
+ private def measureQuery(queryLocation: String, name: String, totals:
Totals): Unit = {
+ val queryString = resourceToString(s"$queryLocation/$name.sql",
+ classLoader = Thread.currentThread().getContextClassLoader)
+ try {
+ val plan = spark.sql(queryString).queryExecution.executedPlan
+ // codegenStringSeq walks all WholeStageCodegenExec subtrees (recursing
subqueries), calls
+ // doCodeGen() for the source and CodeGenerator.compile() for the
compiled ByteCodeStats.
+ val stages = codegenStringSeq(plan)
+ totals.queries += 1
+ stages.foreach { case (_, source, stats) =>
+ totals.stages += 1
+ totals.sourceChars += source.length
+ if (stats == ByteCodeStats.UNAVAILABLE) {
+ totals.fallbacks += 1
+ } else {
+ totals.maxMethodBytecodeSum += stats.maxMethodCodeSize
+ totals.maxMethodBytecodeMax =
+ math.max(totals.maxMethodBytecodeMax, stats.maxMethodCodeSize)
+ totals.innerClasses += stats.numInnerClasses
+ totals.constPoolSum += stats.maxConstPoolSize
+ totals.constPoolMax = math.max(totals.constPoolMax,
stats.maxConstPoolSize)
+ }
+ }
+ } catch {
+ case NonFatal(e) =>
+ logWarning(s"Skipping query $name: failed to plan (${e.getMessage})")
+ }
+ }
+
+ override def runBenchmarkSuite(mainArgs: Array[String]): Unit = {
+ createTables()
+ try {
+ val queriesV1_4 = CodegenSizeTpcdsSchema.tpcdsQueries
+ val queriesV2_7 = CodegenSizeTpcdsSchema.tpcdsQueriesV2_7_0
+
+ // Measure compile time cold: reset the accumulator, let
codegenStringSeq trigger the Janino
+ // compiles (the compile cache is keyed on the source string, so
distinct queries miss), then
+ // read the total below.
+ CodeGenerator.resetCompileTime()
+ val totals = Totals()
+ queriesV1_4.foreach(measureQuery("tpcds", _, totals))
+ queriesV2_7.foreach(measureQuery("tpcds-v2.7.0", _, totals))
+ val compileTimeMs = CodeGenerator.compileTime / 1000000.0
+
+ runBenchmark("TPCDS codegen size") {
+ // Use a Benchmark instance only for its `out` PrintStream, which tees
to stdout and the
+ // results file; the metrics here are static sizes, not wall-clock
timings.
+ val benchmark = new Benchmark("TPCDS codegen size (grand totals, lower
is better)",
+ totals.queries.toLong, output = output)
+ val out = benchmark.out
+ // scalastyle:off println
+ def row(label: String, value: Any): Unit =
+ out.println("%-32s %s".format(label, value))
+ row("queries measured", totals.queries)
+ row("codegen stages", totals.stages)
+ row("source code size (chars)", totals.sourceChars)
+ row("max method bytecode (sum)", totals.maxMethodBytecodeSum)
+ row("max method bytecode (max)", totals.maxMethodBytecodeMax)
+ row("inner classes", totals.innerClasses)
+ row("constant pool (sum)", totals.constPoolSum)
+ row("constant pool (max)", totals.constPoolMax)
+ row("codegen fallbacks", totals.fallbacks)
+ row("compile time (ms)", "%.1f".format(compileTimeMs))
+ out.println()
+ // scalastyle:on println
+ }
+ } finally {
+ dropTables()
+ }
+ }
+}
+
+/**
+ * Exposes the protected TPC-DS schema/query metadata (from [[TPCDSSchema]])
that the benchmark
+ * object needs. The benchmark cannot mix in `TPCDSBase` because that trait
extends the
+ * ScalaTest-based `SharedSparkSession`.
+ */
+private object CodegenSizeTpcdsSchema extends TPCDSSchema {
+ val tableNames: Iterable[String] = tableColumns.keys
+
+ def columnsDDL(tableName: String): String = tableColumns(tableName)
+
+ def partitionedByClause(tableName: String): String =
+ tablePartitionColumns.get(tableName) match {
+ case Some(cols) if cols.nonEmpty => s"PARTITIONED BY (${cols.mkString(",
")})"
+ case _ => ""
+ }
+
+ // TPC-DS v1.4 queries (the same list as TPCDSBase, without the test-only
exclusions).
+ val tpcdsQueries: Seq[String] = Seq(
+ "q1", "q2", "q3", "q4", "q5", "q6", "q7", "q8", "q9", "q10", "q11",
+ "q12", "q13", "q14a", "q14b", "q15", "q16", "q17", "q18", "q19", "q20",
+ "q21", "q22", "q23a", "q23b", "q24a", "q24b", "q25", "q26", "q27", "q28",
"q29", "q30",
+ "q31", "q32", "q33", "q34", "q35", "q36", "q37", "q38", "q39a", "q39b",
"q40",
+ "q41", "q42", "q43", "q44", "q45", "q46", "q47", "q48", "q49", "q50",
+ "q51", "q52", "q53", "q54", "q55", "q56", "q57", "q58", "q59", "q60",
+ "q61", "q62", "q63", "q64", "q65", "q66", "q67", "q68", "q69", "q70",
+ "q71", "q72", "q73", "q74", "q75", "q76", "q77", "q78", "q79", "q80",
+ "q81", "q82", "q83", "q84", "q85", "q86", "q87", "q88", "q89", "q90",
+ "q91", "q92", "q93", "q94", "q95", "q96", "q97", "q98", "q99")
+
+ // TPC-DS v2.7 queries that differ from v1.4.
+ val tpcdsQueriesV2_7_0: Seq[String] = Seq(
+ "q5a", "q6", "q10a", "q11", "q12", "q14", "q14a", "q18a",
+ "q20", "q22", "q22a", "q24", "q27a", "q34", "q35", "q35a", "q36a", "q47",
"q49",
+ "q51a", "q57", "q64", "q67a", "q70a", "q72", "q74", "q75", "q77a", "q78",
+ "q80a", "q86a", "q98")
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]