From c1aebfc95d9920cc70c357c4ca9840a4f6125721 Mon Sep 17 00:00:00 2001 From: Niranjan Artal <50492963+nartal1@users.noreply.github.com> Date: Thu, 5 May 2022 10:49:42 -0700 Subject: [PATCH] Qualification tool: Parsing Execs to get the ExecInfo #2 (#5426) * Qualification tool: Parsing Execs to get the ExecInfo #2 Signed-off-by: Niranjan Artal --- .../CartesianProductExecParser.scala | 42 +++++++ .../tool/planparser/GenerateExecParser.scala | 42 +++++++ .../planparser/GlobalLimitExecParser.scala | 42 +++++++ .../planparser/LocalLimitExecParser.scala | 42 +++++++ .../tool/planparser/SQLPlanParser.scala | 10 ++ .../tool/planparser/SortExecParser.scala | 42 +++++++ .../global_local_limit_eventlog.zstd | Bin 0 -> 22350 bytes .../tool/planparser/SqlPlanParserSuite.scala | 104 ++++++++++++++++-- 8 files changed, 315 insertions(+), 9 deletions(-) create mode 100644 tools/src/main/scala/com/nvidia/spark/rapids/tool/planparser/CartesianProductExecParser.scala create mode 100644 tools/src/main/scala/com/nvidia/spark/rapids/tool/planparser/GenerateExecParser.scala create mode 100644 tools/src/main/scala/com/nvidia/spark/rapids/tool/planparser/GlobalLimitExecParser.scala create mode 100644 tools/src/main/scala/com/nvidia/spark/rapids/tool/planparser/LocalLimitExecParser.scala create mode 100644 tools/src/main/scala/com/nvidia/spark/rapids/tool/planparser/SortExecParser.scala create mode 100644 tools/src/test/resources/spark-events-qualification/global_local_limit_eventlog.zstd diff --git a/tools/src/main/scala/com/nvidia/spark/rapids/tool/planparser/CartesianProductExecParser.scala b/tools/src/main/scala/com/nvidia/spark/rapids/tool/planparser/CartesianProductExecParser.scala new file mode 100644 index 00000000000..794627b895c --- /dev/null +++ b/tools/src/main/scala/com/nvidia/spark/rapids/tool/planparser/CartesianProductExecParser.scala @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2022, NVIDIA CORPORATION. + * + * Licensed 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 com.nvidia.spark.rapids.tool.planparser + +import com.nvidia.spark.rapids.tool.qualification.PluginTypeChecker + +import org.apache.spark.sql.execution.ui.SparkPlanGraphNode + +case class CartesianProductExecParser( + node: SparkPlanGraphNode, + checker: PluginTypeChecker, + sqlID: Long) extends ExecParser { + + val fullExecName = node.name + "Exec" + + override def parse: Seq[ExecInfo] = { + // CartesianProduct doesn't have duration + val duration = None + val (speedupFactor, isSupported) = if (checker.isExecSupported(fullExecName)) { + (checker.getSpeedupFactor(fullExecName), true) + } else { + (1, false) + } + // TODO - add in parsing expressions - average speedup across? + Seq(ExecInfo(sqlID, node.name, "", speedupFactor, + duration, node.id, isSupported, None)) + } +} diff --git a/tools/src/main/scala/com/nvidia/spark/rapids/tool/planparser/GenerateExecParser.scala b/tools/src/main/scala/com/nvidia/spark/rapids/tool/planparser/GenerateExecParser.scala new file mode 100644 index 00000000000..86cf63610f5 --- /dev/null +++ b/tools/src/main/scala/com/nvidia/spark/rapids/tool/planparser/GenerateExecParser.scala @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2022, NVIDIA CORPORATION. + * + * Licensed 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 com.nvidia.spark.rapids.tool.planparser + +import com.nvidia.spark.rapids.tool.qualification.PluginTypeChecker + +import org.apache.spark.sql.execution.ui.SparkPlanGraphNode + +case class GenerateExecParser( + node: SparkPlanGraphNode, + checker: PluginTypeChecker, + sqlID: Long) extends ExecParser { + + val fullExecName = node.name + "Exec" + + override def parse: Seq[ExecInfo] = { + // Generate doesn't have duration + val duration = None + val (speedupFactor, isSupported) = if (checker.isExecSupported(fullExecName)) { + (checker.getSpeedupFactor(fullExecName), true) + } else { + (1, false) + } + // TODO - add in parsing expressions - average speedup across? + Seq(ExecInfo(sqlID, node.name, "", speedupFactor, + duration, node.id, isSupported, None)) + } +} diff --git a/tools/src/main/scala/com/nvidia/spark/rapids/tool/planparser/GlobalLimitExecParser.scala b/tools/src/main/scala/com/nvidia/spark/rapids/tool/planparser/GlobalLimitExecParser.scala new file mode 100644 index 00000000000..4046b816ab7 --- /dev/null +++ b/tools/src/main/scala/com/nvidia/spark/rapids/tool/planparser/GlobalLimitExecParser.scala @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2022, NVIDIA CORPORATION. + * + * Licensed 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 com.nvidia.spark.rapids.tool.planparser + +import com.nvidia.spark.rapids.tool.qualification.PluginTypeChecker + +import org.apache.spark.sql.execution.ui.SparkPlanGraphNode + +case class GlobalLimitExecParser( + node: SparkPlanGraphNode, + checker: PluginTypeChecker, + sqlID: Long) extends ExecParser { + + val fullExecName = node.name + "Exec" + + override def parse: Seq[ExecInfo] = { + // GlobalLimit doesn't have duration + val duration = None + val (speedupFactor, isSupported) = if (checker.isExecSupported(fullExecName)) { + (checker.getSpeedupFactor(fullExecName), true) + } else { + (1, false) + } + // TODO - add in parsing expressions - average speedup across? + Seq(ExecInfo(sqlID, node.name, "", speedupFactor, + duration, node.id, isSupported, None)) + } +} diff --git a/tools/src/main/scala/com/nvidia/spark/rapids/tool/planparser/LocalLimitExecParser.scala b/tools/src/main/scala/com/nvidia/spark/rapids/tool/planparser/LocalLimitExecParser.scala new file mode 100644 index 00000000000..f5cc285d6eb --- /dev/null +++ b/tools/src/main/scala/com/nvidia/spark/rapids/tool/planparser/LocalLimitExecParser.scala @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2022, NVIDIA CORPORATION. + * + * Licensed 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 com.nvidia.spark.rapids.tool.planparser + +import com.nvidia.spark.rapids.tool.qualification.PluginTypeChecker + +import org.apache.spark.sql.execution.ui.SparkPlanGraphNode + +case class LocalLimitExecParser( + node: SparkPlanGraphNode, + checker: PluginTypeChecker, + sqlID: Long) extends ExecParser { + + val fullExecName = node.name + "Exec" + + override def parse: Seq[ExecInfo] = { + // LocalLimit doesn't have duration + val duration = None + val (speedupFactor, isSupported) = if (checker.isExecSupported(fullExecName)) { + (checker.getSpeedupFactor(fullExecName), true) + } else { + (1, false) + } + // TODO - add in parsing expressions - average speedup across? + Seq(ExecInfo(sqlID, node.name, "", speedupFactor, + duration, node.id, isSupported, None)) + } +} diff --git a/tools/src/main/scala/com/nvidia/spark/rapids/tool/planparser/SQLPlanParser.scala b/tools/src/main/scala/com/nvidia/spark/rapids/tool/planparser/SQLPlanParser.scala index eea51e747a0..a0eab582ead 100644 --- a/tools/src/main/scala/com/nvidia/spark/rapids/tool/planparser/SQLPlanParser.scala +++ b/tools/src/main/scala/com/nvidia/spark/rapids/tool/planparser/SQLPlanParser.scala @@ -80,6 +80,8 @@ object SQLPlanParser extends Logging { app: AppBase ): Seq[ExecInfo] = { node match { + case c if (c.name == "CartesianProduct") => + CartesianProductExecParser(c, checker, sqlID).parse case c if (c.name == "Coalesce") => CoalesceExecParser(c, checker, sqlID).parse case c if (c.name == "CollectLimit") => @@ -88,12 +90,20 @@ object SQLPlanParser extends Logging { ExpandExecParser(e, checker, sqlID).parse case f if (f.name == "Filter") => FilterExecParser(f, checker, sqlID).parse + case g if (g.name == "Generate") => + GenerateExecParser(g, checker, sqlID).parse + case g if (g.name == "GlobalLimit") => + GlobalLimitExecParser(g, checker, sqlID).parse + case l if (l.name == "LocalLimit") => + LocalLimitExecParser(l, checker, sqlID).parse case p if (p.name == "Project") => ProjectExecParser(p, checker, sqlID).parse case r if (r.name == "Range") => RangeExecParser(r, checker, sqlID).parse case s if (s.name == "Sample") => SampleExecParser(s, checker, sqlID).parse + case s if (s.name == "Sort") => + SortExecParser(s, checker, sqlID).parse case t if (t.name == "TakeOrderedAndProject") => TakeOrderedAndProjectExecParser(t, checker, sqlID).parse case u if (u.name == "Union") => diff --git a/tools/src/main/scala/com/nvidia/spark/rapids/tool/planparser/SortExecParser.scala b/tools/src/main/scala/com/nvidia/spark/rapids/tool/planparser/SortExecParser.scala new file mode 100644 index 00000000000..50f52108f8e --- /dev/null +++ b/tools/src/main/scala/com/nvidia/spark/rapids/tool/planparser/SortExecParser.scala @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2022, NVIDIA CORPORATION. + * + * Licensed 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 com.nvidia.spark.rapids.tool.planparser + +import com.nvidia.spark.rapids.tool.qualification.PluginTypeChecker + +import org.apache.spark.sql.execution.ui.SparkPlanGraphNode + +case class SortExecParser( + node: SparkPlanGraphNode, + checker: PluginTypeChecker, + sqlID: Long) extends ExecParser { + + val fullExecName = node.name + "Exec" + + override def parse: Seq[ExecInfo] = { + // Sort doesn't have duration + val duration = None + val (speedupFactor, isSupported) = if (checker.isExecSupported(fullExecName)) { + (checker.getSpeedupFactor(fullExecName), true) + } else { + (1, false) + } + // TODO - add in parsing expressions - average speedup across? + Seq(ExecInfo(sqlID, node.name, "", speedupFactor, + duration, node.id, isSupported, None)) + } +} diff --git a/tools/src/test/resources/spark-events-qualification/global_local_limit_eventlog.zstd b/tools/src/test/resources/spark-events-qualification/global_local_limit_eventlog.zstd new file mode 100644 index 0000000000000000000000000000000000000000..558e63d46cd2e2484b84ba7eda70f4a5f339bd0d GIT binary patch literal 22350 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 val (eventLog, _) = ToolTestUtils.generateEventLog(eventLogDir, "sqlmetric") { spark => import spark.implicits._ @@ -48,6 +50,7 @@ class SQLPlanParserSuite extends FunSuite with BeforeAndAfterEach with Logging { df.select( $"value" as "a") .join(df2.select($"value" as "b"), $"a" === $"b") .filter($"b" < 100) + .sort($"b") } TrampolineUtil.withTempDir { outpath => @@ -63,25 +66,33 @@ class SQLPlanParserSuite extends FunSuite with BeforeAndAfterEach with Logging { assert(app.sqlPlans.size == 1) app.sqlPlans.foreach { case(sqlID, plan) => val planInfo = SQLPlanParser.parseSQLPlan(plan, sqlID, pluginTypeChecker, app) - assert(planInfo.execInfo.size == 9) + assert(planInfo.execInfo.size == 11) val wholeStages = planInfo.execInfo.filter(_.exec.contains("WholeStageCodegen")) - assert(wholeStages.size == 5) - // only 2 in the above example have projects and filters + assert(wholeStages.size == 6) + // only 2 in the above example have projects and filters and the other 3 have sort val numSupported = wholeStages.filter(_.isSupported).size - assert(numSupported == 2) + assert(numSupported == 5) assert(wholeStages.forall(_.duration.nonEmpty)) val allChildren = wholeStages.flatMap(_.children).flatten - assert(allChildren.size == 9) - val filters = allChildren.filter(_.exec == "FilterExec") + assert(allChildren.size == 10) + val filters = allChildren.filter(_.exec == "Filter") + assert(filters.size == 2) assert(filters.forall(_.speedupFactor == 2)) assert(filters.forall(_.isSupported == true)) assert(filters.forall(_.children.isEmpty)) assert(filters.forall(_.duration.isEmpty)) - val projects = allChildren.filter(_.exec == "ProjectExec") + val projects = allChildren.filter(_.exec == "Project") + assert(projects.size == 2) assert(projects.forall(_.speedupFactor == 2)) assert(projects.forall(_.isSupported == true)) assert(projects.forall(_.children.isEmpty)) assert(projects.forall(_.duration.isEmpty)) + val sorts = allChildren.filter(_.exec == "Sort") + assert(sorts.size == 3) + assert(sorts.forall(_.speedupFactor == 2)) + assert(sorts.forall(_.isSupported == true)) + assert(sorts.forall(_.children.isEmpty)) + assert(sorts.forall(_.duration.isEmpty)) } } } @@ -136,4 +147,79 @@ class SQLPlanParserSuite extends FunSuite with BeforeAndAfterEach with Logging { } } } + + test("Parse Execs - CartesianProduct and Generate") { + TrampolineUtil.withTempDir { eventLogDir => + val (eventLog, _) = ToolTestUtils.generateEventLog(eventLogDir, "sqlmetric") { spark => + import spark.implicits._ + val genDf = spark.sparkContext.parallelize(List(List(1, 2, 3), List(4, 5, 6)), 4).toDF + val joinDf1 = spark.sparkContext.makeRDD(1 to 10, 4).toDF + val joinDf2 = spark.sparkContext.makeRDD(1 to 10, 4).toDF + genDf.select(explode($"value")).collect + joinDf1.crossJoin(joinDf2) + } + TrampolineUtil.withTempDir { outpath => + val hadoopConf = new Configuration() + val (_, allEventLogs) = EventLogPathProcessor.processAllPaths( + None, None, List(eventLog), hadoopConf) + val pluginTypeChecker = new PluginTypeChecker() + assert(allEventLogs.size == 1) + val appOption = QualificationAppInfo.createApp(allEventLogs.head, hadoopConf, + pluginTypeChecker, 20) + assert(appOption.nonEmpty) + val app = appOption.get + assert(app.sqlPlans.size == 2) + val supportedExecs = Array("CartesianProduct", "Generate") + app.sqlPlans.foreach { case (sqlID, plan) => + val planInfo = SQLPlanParser.parseSQLPlan(plan, sqlID, pluginTypeChecker, app) + for (execName <- supportedExecs) { + val supportedExec = planInfo.execInfo.filter(_.exec == execName) + if (supportedExec.nonEmpty) { + assert(supportedExec.size == 1) + assert(supportedExec.forall(_.children.isEmpty)) + assert(supportedExec.forall(_.duration.isEmpty)) + assert(supportedExec.forall(_.speedupFactor == 2), execName) + assert(supportedExec.forall(_.isSupported == true)) + } + } + } + } + } + } + + // GlobalLimit and LocalLimit is not in physical plan when collect is called on the dataframe. + // We are reading from static eventlogs to test these execs. + test("Parse execs - LocalLimit and GlobalLimit") { + val logFile = s"$logDir/global_local_limit_eventlog.zstd" + TrampolineUtil.withTempDir { outpath => + val hadoopConf = new Configuration() + val (_, allEventLogs) = EventLogPathProcessor.processAllPaths( + None, None, List(logFile), hadoopConf) + val pluginTypeChecker = new PluginTypeChecker() + assert(allEventLogs.size == 1) + val appOption = QualificationAppInfo.createApp(allEventLogs.head, hadoopConf, + pluginTypeChecker, 20) + assert(appOption.nonEmpty) + val app = appOption.get + assert(app.sqlPlans.size == 1) + val supportedExecs = Array("GlobalLimit", "LocalLimit") + app.sqlPlans.foreach { case (sqlID, plan) => + val planInfo = SQLPlanParser.parseSQLPlan(plan, sqlID, pluginTypeChecker, app) + // GlobalLimit and LocalLimit are inside WholeStageCodegen. So getting the children of + // WholeStageCodegenExec + val wholeStages = planInfo.execInfo.filter(_.exec.contains("WholeStageCodegen")) + val allChildren = wholeStages.flatMap(_.children).flatten + for (execName <- supportedExecs) { + val supportedExec = allChildren.filter(_.exec == execName) + if (supportedExec.nonEmpty) { + assert(supportedExec.size == 1) + assert(supportedExec.forall(_.children.isEmpty)) + assert(supportedExec.forall(_.duration.isEmpty)) + assert(supportedExec.forall(_.speedupFactor == 2), execName) + assert(supportedExec.forall(_.isSupported == true)) + } + } + } + } + } }