dongjoon-hyun commented on code in PR #58881:
URL: https://github.com/apache/spark/pull/58881#discussion_r4328851424


##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/codegen/CodeGenerator.scala:
##########
@@ -1322,22 +1322,17 @@ class CodegenContext extends Logging {
     val args = mutable.LinkedHashMap.empty[String, VariableValue]
     val inputsToEvaluate = mutable.ArrayBuffer.empty[ExprCode]
     // Whether the code can read `v` from the method: false for a value no 
parameter can carry
-    // where the policy asks for names. `ExpandExec` hands out a 
`VariableValue` naming a slot of a
-    // compacted mutable state array, and a `SimpleExprValue` is an expression 
rather than a name
+    // where the policy asks for names. A `SimpleExprValue` is an expression 
rather than a name
     // -- `posexplode_outer` gives its position the nullness `index == -1`, 
and a `Byte` or `Short`
     // literal's value is `(byte)1`. A field or a literal needs no parameter 
and is read as it
     // stands; so is a field handed out as a `VariableValue` where the policy 
says so, passing one
-    // being refused by the split methods' own check, a slot of a compacted 
array included.
+    // being refused by the split methods' own check.
     def take(v: ExprValue): Boolean = v match {
       case local: VariableValue =>
         val name = local.variableName
         val isName = name.nonEmpty && 
Character.isJavaIdentifierStart(name.head) &&
           name.forall(Character.isJavaIdentifierPart)
-        // A slot of a compacted array is a field under every policy, since no 
parameter can be
-        // named `array[i]`, where the whole stage split is on; with it off, 
every policy answers
-        // as it did before that split existed. Another field is passed where 
the policy says so.
-        val isField = (isCompactedSlot(name) && wholeStageSplit != 
WholeStageSplit.Off) ||
-          (policy.fieldsNeedNoArgument && mutableStateNames.contains(name))
+        val isField = policy.fieldsNeedNoArgument && 
mutableStateNames.contains(name)
         val taken = isName || isField || !policy.onlyNames
         if (taken && !isField) {

Review Comment:
   Minor: With the compacted-slot rule gone, nothing catches the next producer 
that hands out a slot as a `VariableValue`. Under `InputPolicy.operatorMethod` 
(`onlyNames = false`), `take` adds any `VariableValue` to the arguments 
whatever its name, so a `subExpr`, aggregate or switch-case method would 
declare `UTF8String x_mutableStateArray_0[1]` again. The testing-only "cannot 
be a global variable" checks at L1714 and L1803 cannot see it, since 
`mutableStateNames` holds the array names but never a slot. Tests would stay 
green unless one happens to split such a method, while production would 
silently fall back. The `@param onlyNames` doc at L125 also says such a value 
is "passed over", which only holds for a `SimpleExprValue`. Could you assert 
here, under `Utils.isTesting` as `splitExpressions` and `splitCall` do, that 
what is passed is a name? For example:
   
   ```scala
           if (taken && !isField) {
             if (Utils.isTesting) {
               assert(isName, s"$name is handed out as a local variable, but no 
parameter can " +
                 "carry it; hand out a field with JavaCode.global or 
isNullGlobal")
             }
             args.getOrElseUpdate(name, local)
           }
   ```
   
   Every `VariableValue` and `INPUT_ROW` that reaches `operatorMethod` in the 
tree today is a name, and the `mutableStateArray_0[3]` case in 
`WithExpressionEvalSuite` only reaches `commonExpressionMethod`, so nothing 
should trip it.



##########
sql/core/src/test/scala/org/apache/spark/sql/execution/WholeStageCodegenSuite.scala:
##########
@@ -2767,7 +2796,7 @@ class WholeStageCodegenSuite extends SharedSparkSession
   test("SPARK-33301: a split method reads a slot of a compacted mutable state 
array as a field") {
     withSplitAlways {
       // `ExpandExec` holds an output of a type that is not primitive in a 
slot of a compacted
-      // mutable state array, such as `expand_mutableStateArray_0[0]`: a 
field, though not a name.
+      // mutable state array, such as `expand_mutableStateArray_0[0]`, and 
hands it out as a field.
       // An UNPIVOT of string columns gives such an output, which the 
projection above reads
       // directly when the operators' consume functions are not split into 
methods of their own.
       // Every block reads it, the ELSE too, and each reads it as the field it 
is, taking no

Review Comment:
   Nit: The check behind "taking no argument for it", 
`splitCaseWhenParameters(df).flatten.forall(!_.contains("mutableStateArray"))` 
at L2815, cannot fail on its own. A CASE WHEN split method takes only what 
`collectInputs` collects under `InputPolicy.wholeStageSplit`: a `GlobalValue` 
is never an argument, a slot handed out as a `VariableValue` is refused as no 
name, and an array or inlined name is a field. So if `ExpandExec` went back to 
`JavaCode.variable`, every block would stay inline and 
`assert(splitsCaseWhen(df))` at L2811 would fail first. That was already so 
before this PR, but this test is now the CASE WHEN guard for the relabel. Could 
L2815 go, or check something a regression could break on its own, e.g. 
`forall(!_.contains("UTF8String"))`, as the new test's `stringParameter` does?



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/codegen/CodeGenerator.scala:
##########
@@ -1322,22 +1322,17 @@ class CodegenContext extends Logging {
     val args = mutable.LinkedHashMap.empty[String, VariableValue]
     val inputsToEvaluate = mutable.ArrayBuffer.empty[ExprCode]
     // Whether the code can read `v` from the method: false for a value no 
parameter can carry
-    // where the policy asks for names. `ExpandExec` hands out a 
`VariableValue` naming a slot of a
-    // compacted mutable state array, and a `SimpleExprValue` is an expression 
rather than a name
+    // where the policy asks for names. A `SimpleExprValue` is an expression 
rather than a name
     // -- `posexplode_outer` gives its position the nullness `index == -1`, 
and a `Byte` or `Short`
     // literal's value is `(byte)1`. A field or a literal needs no parameter 
and is read as it
     // stands; so is a field handed out as a `VariableValue` where the policy 
says so, passing one
-    // being refused by the split methods' own check, a slot of a compacted 
array included.
+    // being refused by the split methods' own check.

Review Comment:
   Minor: Without ", a slot of a compacted array included", this sentence still 
reads as if any field handed out as a `VariableValue` were read as it stands 
where the policy says so. But `isField` only matches names in 
`mutableStateNames`, and `addMutableState` never adds a slot there, only the 
array name (L600) and inlined names (L680). So a slot handed out as a 
`VariableValue`, which the new `ExpandExec` comment rightly calls a field, is 
refused under `wholeStageSplit` and `commonExpressionMethod`, and becomes a 
parameter that does not compile under `operatorMethod`; "the split methods' own 
check" cannot see it either. The `fieldsNeedNoArgument` doc at L126-127 and the 
new test name at CollectInputsSuite.scala:142-143 read the same way, though 
that test's variable half needs `forceInline = true`. Could the comment say 
which fields it means? For example:
   
   ```suggestion
       // literal's value is `(byte)1`. A field or a literal needs no parameter 
and is read as it
       // stands; so is an inlined field (a name `addMutableState` registered) 
handed out as a
       // `VariableValue` where the policy says so, passing one being refused 
by the split methods'
       // own check; a slot of a compacted array is no such name, so its 
producer hands it out as a
       // `GlobalValue`, as `ExpandExec` does.
   ```



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/ExpandExec.scala:
##########
@@ -187,9 +187,15 @@ case class ExpandExec(
             "resultValue",
             v => s"$v = ${CodeGenerator.defaultValue(firstExpr.dataType)};")
 
+          // `addMutableState` hands back a field: a name of its own, or a 
slot in a compacted array
+          // (`mutableStateArray_0[3]`) for a type it cannot inline. Either is 
reachable from every
+          // method of the generated class and is not a local variable, so a 
consumer collecting
+          // local variables to pass into a split function must not take it 
for one -- a slot
+          // expression is not a legal parameter name. Whole-stage 
subexpression elimination wraps
+          // its own mutable state the same way.
           ExprCode(
-            JavaCode.isNullVariable(isNull),
-            JavaCode.variable(value, firstExpr.dataType))
+            JavaCode.isNullGlobal(isNull),
+            JavaCode.global(value, firstExpr.dataType))

Review Comment:
   **Pre-existing and outside this diff: the switch-case split at L243-250 
calls its functions by their unqualified name, so it does not compile once one 
of them spills into a nested class.** The states labelled here are written by 
the `switchCaseCode` functions that L243 adds with 
`ctx.addNewFunction(switchCaseFunc, ...)`, but L250 calls 
`$switchCaseFunc(...)` and drops the name `addNewFunction` returns. Once the 
current class has passed `GENERATED_CLASS_SIZE_THRESHOLD` (1000000 chars), 
`addNewFunction` puts the function in a private inner class and returns 
`nestedClassInstance_N.switchCaseCode_M` (CodeGenerator.scala:885, :897). The 
unqualified call then compiles only from inside that inner class: not from 
`processNext` in the outer class, nor from another inner class, such as the one 
the child's `doConsume_N` wrapper lands in when it is added after the 
switch-case functions have filled a class.
   
   For example, a sliding window like `GROUP BY window(ts, '1 hour', '10 
seconds')` makes an Expand of 360 projections. With 
`spark.sql.subexpressionElimination.enabled=false` on master, or by default on 
released versions, I estimate its switch-case functions at a few MB. Janino 
would then report `A method named "expand_switchCaseCode_..." is not declared 
in any enclosing class ...`, and the stage falls back to running without 
whole-stage codegen, or the query fails with 
`spark.sql.codegen.fallback=false`. I traced this by reading the code and 
estimated the sizes; I did not run it.
   
   The other splits on this path call the name `addNewFunction` returns: 
subexpression elimination (CodeGenerator.scala:2232), the split aggregate 
functions (AggregateCodegenSupport.scala:358-366) and 
`constructDoConsumeFunction` (WholeStageCodegenExec.scala:235). This has been 
there since SPARK-35329 in 3.2.0, so it is not caused by this PR. Could it be a 
separate JIRA that calls the returned name here too?



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/codegen/CodeGenerator.scala:
##########
@@ -2149,7 +2135,7 @@ class CodegenContext extends Logging {
     // WHEN or a `With` definition's method. A generation that splits the 
stage's expressions
     // (`WholeStageSplit.Split`) removes those functions then; any other keeps 
them, as the class
     // did before that split existed, so that a stage kept in one piece, and 
the split turned off,
-    // give back the code generated before it.
+    // have the functions they had before it.

Review Comment:
   Nit: After this edit, the "so that" clause only restates the "as" clause 
before it. Read for the whole stage, it also no longer holds with the conf off, 
since this PR changes those stages' methods: no slot or null-flag parameters, 
and a `With` definition that reads a slot now gets its method. Could it name 
the generations it covers instead? For example:
   
   ```suggestion
       // (`WholeStageSplit.Split`) removes those functions then; any other, a 
stage kept in one
       // piece or the split turned off, keeps them, as the class did before 
that split existed.
   ```



##########
sql/core/src/test/scala/org/apache/spark/sql/execution/WholeStageCodegenSuite.scala:
##########
@@ -1741,6 +1741,35 @@ class WholeStageCodegenSuite extends SharedSparkSession
       "sinh(v) should be evaluated only once per input row without function 
splitting")
   }
 
+  test("SPARK-59589: a subexpression method reads an Expand's varying output 
as a field") {

Review Comment:
   Minor: This covers the `subExpr` methods, but the relabel also fixes two 
other places that took the Expand's slot as a parameter, and no test runs 
either. One is a nested Expand's switch-case split, whose 
`getLocalInputVariableValues` (ExpandExec.scala:214) collected the child 
Expand's slot; the other is the split aggregate functions 
(AggregateCodegenSupport.scala:337-366). Both are reached when the consumer is 
not wrapped in a consume method of its own, e.g. with 
`spark.sql.codegen.splitConsumeFuncByOperator=false` or a schema wide enough to 
skip the wrapper. Before this PR, with 
`spark.sql.codegen.wholeStage.splitExpressions` off, these failed to compile:
   
   ```sql
   -- spark.sql.codegen.splitConsumeFuncByOperator=false, 
spark.sql.codegen.methodSplitThreshold=1
   SELECT count(DISTINCT upper(v)), count(DISTINCT lower(v)) FROM t UNPIVOT (v 
FOR c IN (a, b));
   SELECT c, sum(length(upper(v))), sum(length(lower(v))) FROM t UNPIVOT (v FOR 
c IN (a, b)) GROUP BY c;
   ```
   
   I traced these by reading the code and did not run them. They fail the same 
way on 4.1-4.3, so they would also help the backports. Could you add them here 
as sibling cases that compile the code and check the answers?



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/codegen/CodeGenerator.scala:
##########
@@ -1322,22 +1322,17 @@ class CodegenContext extends Logging {
     val args = mutable.LinkedHashMap.empty[String, VariableValue]
     val inputsToEvaluate = mutable.ArrayBuffer.empty[ExprCode]
     // Whether the code can read `v` from the method: false for a value no 
parameter can carry
-    // where the policy asks for names. `ExpandExec` hands out a 
`VariableValue` naming a slot of a
-    // compacted mutable state array, and a `SimpleExprValue` is an expression 
rather than a name
+    // where the policy asks for names. A `SimpleExprValue` is an expression 
rather than a name
     // -- `posexplode_outer` gives its position the nullness `index == -1`, 
and a `Byte` or `Short`
     // literal's value is `(byte)1`. A field or a literal needs no parameter 
and is read as it
     // stands; so is a field handed out as a `VariableValue` where the policy 
says so, passing one
-    // being refused by the split methods' own check, a slot of a compacted 
array included.
+    // being refused by the split methods' own check.
     def take(v: ExprValue): Boolean = v match {
       case local: VariableValue =>
         val name = local.variableName
         val isName = name.nonEmpty && 
Character.isJavaIdentifierStart(name.head) &&
           name.forall(Character.isJavaIdentifierPart)
-        // A slot of a compacted array is a field under every policy, since no 
parameter can be
-        // named `array[i]`, where the whole stage split is on; with it off, 
every policy answers
-        // as it did before that split existed. Another field is passed where 
the policy says so.
-        val isField = (isCompactedSlot(name) && wholeStageSplit != 
WholeStageSplit.Off) ||
-          (policy.fieldsNeedNoArgument && mutableStateNames.contains(name))
+        val isField = policy.fieldsNeedNoArgument && 
mutableStateNames.contains(name)
         val taken = isName || isField || !policy.onlyNames

Review Comment:
   Nit: With the slot clause gone, `isField` implies `isName`, since every name 
in `mutableStateNames` is a Java identifier (fresh array names and inlined 
names, never a slot). So `|| isField` no longer decides anything here:
   
   ```suggestion
           val taken = isName || !policy.onlyNames
   ```
   
   Relatedly, after the relabel no producer in the tree hands an inlined field 
to `take` as a `VariableValue`, so `fieldsNeedNoArgument` only decides for the 
value built by hand at CollectInputsSuite.scala:155-157. Dropping it would 
leave a future mislabel to the testing check in `splitCall` (L1803-1804), at 
the producer, which is the direction of this PR; that could be a follow-up. 
Could you simplify this line at least?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


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

Reply via email to