-
Notifications
You must be signed in to change notification settings - Fork 29.3k
[SPARK-26504][SQL] Rope-wise dumping of Spark plans #23406
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 6 commits
de38cf8
af5c9ed
0a36a26
07ae72d
f621de7
ca4aed8
d727959
544b80e
059dcf4
fe3bbcf
707c80b
074e9b8
29b62bf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -87,4 +87,28 @@ object StringUtils { | |
| } | ||
| funcNames.toSeq | ||
| } | ||
|
|
||
| class StringRope { | ||
| private var list = List.empty[String] | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's use a
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I supposed this reverse is relatively cheap O(n). Coping a string would be more expensive than adding element to the head of list inside of the reverse() method. In any case, need to traverse over the list in
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I replaced |
||
| private var length: Int = 0 | ||
|
|
||
| def append(s: String): Unit = { | ||
| if (s != null) { | ||
| list = s :: list | ||
| length += s.length | ||
| } | ||
| } | ||
|
|
||
| override def toString: String = { | ||
| val buffer = new StringBuffer(length) | ||
| var reversed = list.reverse | ||
|
|
||
| while (!reversed.isEmpty) { | ||
| buffer.append(reversed.head) | ||
| reversed = reversed.tail | ||
| } | ||
|
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why the new line?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. for beauty |
||
| buffer.toString | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,11 +17,10 @@ | |
|
|
||
| package org.apache.spark.sql.execution | ||
|
|
||
| import java.io.{BufferedWriter, OutputStreamWriter, Writer} | ||
| import java.io.{BufferedWriter, OutputStreamWriter} | ||
| import java.nio.charset.StandardCharsets | ||
| import java.sql.{Date, Timestamp} | ||
|
|
||
| import org.apache.commons.io.output.StringBuilderWriter | ||
| import org.apache.hadoop.fs.Path | ||
|
|
||
| import org.apache.spark.rdd.RDD | ||
|
|
@@ -31,6 +30,7 @@ import org.apache.spark.sql.catalyst.analysis.UnsupportedOperationChecker | |
| import org.apache.spark.sql.catalyst.plans.logical.{LogicalPlan, ReturnAnswer} | ||
| import org.apache.spark.sql.catalyst.rules.Rule | ||
| import org.apache.spark.sql.catalyst.util.DateTimeUtils | ||
| import org.apache.spark.sql.catalyst.util.StringUtils.StringRope | ||
| import org.apache.spark.sql.catalyst.util.truncatedString | ||
| import org.apache.spark.sql.execution.command.{DescribeTableCommand, ExecutedCommandExec, ShowTablesCommand} | ||
| import org.apache.spark.sql.execution.exchange.{EnsureRequirements, ReuseExchange} | ||
|
|
@@ -108,10 +108,6 @@ class QueryExecution( | |
| ReuseExchange(sparkSession.sessionState.conf), | ||
| ReuseSubquery(sparkSession.sessionState.conf)) | ||
|
|
||
| protected def stringOrError[A](f: => A): String = | ||
| try f.toString catch { case e: AnalysisException => e.toString } | ||
|
|
||
|
|
||
| /** | ||
| * Returns the result as a hive compatible sequence of strings. This is used in tests and | ||
| * `SparkSQLDriver` for CLI applications. | ||
|
|
@@ -197,55 +193,65 @@ class QueryExecution( | |
| } | ||
|
|
||
| def simpleString: String = withRedaction { | ||
| s"""== Physical Plan == | ||
| |${stringOrError(executedPlan.treeString(verbose = false))} | ||
| """.stripMargin.trim | ||
| val rope = new StringRope() | ||
|
|
||
| rope.append("== Physical Plan ==\n") | ||
| appendOrError(rope.append)( | ||
| executedPlan.treeString(_, false, false, SQLConf.get.maxToStringFields)) | ||
| rope.append("\n") | ||
|
|
||
| rope.toString | ||
| } | ||
|
|
||
| private def writeOrError(writer: Writer)(f: Writer => Unit): Unit = { | ||
| try f(writer) | ||
| private def appendOrError(append: String => Unit)(f: (String => Unit) => Unit): Unit = { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You always call this method on a QueryPlan. Can we specialize this method for this scenario and pass the plan and all the needed treeString parameters? Different question. Is it possible that treeString already writes to the appender before throwing an exception. It it does the output might look pretty weird, because it will and contain a part of the tree and the exception thrown.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
In the case of writing to a file, I think it is possible. I believe it will be a nice feature in trouble shooting. I would image a huge (maybe wrong) plan causes OOMs at some point. If we write some part of the plan to file, it would be helpful in debugging.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
@hvanhovell Do you mean changes like in the PR MaxGekk#16 ? Unfortunately it doesn't work well because plan's constructor can produce
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah that would be it. You can pass in a call-by-name parameter if you want to capture errors during construction. I prefer his over having some overly generic function.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will be ok if I move object QueryPlan extends PredicateHelper {
/**
* Converts the query plan to string and appends it via provided function.
*/
def append[T <: QueryPlan[T]](
plan: => QueryPlan[T],
append: String => Unit,
verbose: Boolean,
addSuffix: Boolean,
maxFields: Int = SQLConf.get.maxToStringFields): Unit = { () }
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure that works. |
||
| try f(append) | ||
| catch { | ||
| case e: AnalysisException => writer.write(e.toString) | ||
| case e: AnalysisException => append(e.toString) | ||
| } | ||
| } | ||
|
|
||
| private def writePlans(writer: Writer, maxFields: Int): Unit = { | ||
| private def writePlans(append: String => Unit, maxFields: Int): Unit = { | ||
| def stringOrError[A](f: => A): String = { | ||
| try f.toString catch { case e: AnalysisException => e.toString } | ||
| } | ||
| val (verbose, addSuffix) = (true, false) | ||
|
|
||
| writer.write("== Parsed Logical Plan ==\n") | ||
| writeOrError(writer)(logical.treeString(_, verbose, addSuffix, maxFields)) | ||
| writer.write("\n== Analyzed Logical Plan ==\n") | ||
| append("== Parsed Logical Plan ==\n") | ||
| appendOrError(append)(logical.treeString(_, verbose, addSuffix, maxFields)) | ||
| append("\n== Analyzed Logical Plan ==\n") | ||
| val analyzedOutput = stringOrError(truncatedString( | ||
| analyzed.output.map(o => s"${o.name}: ${o.dataType.simpleString}"), ", ", maxFields)) | ||
| writer.write(analyzedOutput) | ||
| writer.write("\n") | ||
| writeOrError(writer)(analyzed.treeString(_, verbose, addSuffix, maxFields)) | ||
| writer.write("\n== Optimized Logical Plan ==\n") | ||
| writeOrError(writer)(optimizedPlan.treeString(_, verbose, addSuffix, maxFields)) | ||
| writer.write("\n== Physical Plan ==\n") | ||
| writeOrError(writer)(executedPlan.treeString(_, verbose, addSuffix, maxFields)) | ||
| append(analyzedOutput) | ||
| append("\n") | ||
| appendOrError(append)(analyzed.treeString(_, verbose, addSuffix, maxFields)) | ||
| append("\n== Optimized Logical Plan ==\n") | ||
| appendOrError(append)(optimizedPlan.treeString(_, verbose, addSuffix, maxFields)) | ||
| append("\n== Physical Plan ==\n") | ||
| appendOrError(append)(executedPlan.treeString(_, verbose, addSuffix, maxFields)) | ||
| } | ||
|
|
||
| override def toString: String = withRedaction { | ||
| val writer = new StringBuilderWriter() | ||
| try { | ||
| writePlans(writer, SQLConf.get.maxToStringFields) | ||
| writer.toString | ||
| } finally { | ||
| writer.close() | ||
| } | ||
| val rope = new StringRope() | ||
|
|
||
| writePlans(rope.append, SQLConf.get.maxToStringFields) | ||
| rope.toString | ||
| } | ||
|
|
||
| def stringWithStats: String = withRedaction { | ||
| val rope = new StringRope() | ||
| val maxFields = SQLConf.get.maxToStringFields | ||
|
|
||
| // trigger to compute stats for logical plans | ||
| optimizedPlan.stats | ||
|
|
||
| // only show optimized logical plan and physical plan | ||
| s"""== Optimized Logical Plan == | ||
| |${stringOrError(optimizedPlan.treeString(verbose = true, addSuffix = true))} | ||
| |== Physical Plan == | ||
| |${stringOrError(executedPlan.treeString(verbose = true))} | ||
| """.stripMargin.trim | ||
| rope.append("== Optimized Logical Plan ==\n") | ||
| appendOrError(rope.append)(optimizedPlan.treeString(_, true, true, maxFields)) | ||
| rope.append("\n== Physical Plan ==\n") | ||
| appendOrError(rope.append)(executedPlan.treeString(_, true, false, maxFields)) | ||
| rope.append("\n") | ||
|
|
||
| rope.toString | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -282,17 +288,17 @@ class QueryExecution( | |
| /** | ||
| * Dumps debug information about query execution into the specified file. | ||
| * | ||
| * @param maxFields maximim number of fields converted to string representation. | ||
|
MaxGekk marked this conversation as resolved.
|
||
| * @param maxFields maximum number of fields converted to string representation. | ||
| */ | ||
| def toFile(path: String, maxFields: Int = Int.MaxValue): Unit = { | ||
| val filePath = new Path(path) | ||
| val fs = filePath.getFileSystem(sparkSession.sessionState.newHadoopConf()) | ||
| val writer = new BufferedWriter(new OutputStreamWriter(fs.create(filePath))) | ||
|
|
||
| try { | ||
| writePlans(writer, maxFields) | ||
| writePlans(writer.write, maxFields) | ||
| writer.write("\n== Whole Stage Codegen ==\n") | ||
| org.apache.spark.sql.execution.debug.writeCodegen(writer, executedPlan) | ||
| org.apache.spark.sql.execution.debug.writeCodegen(writer.write, executedPlan) | ||
| } finally { | ||
| writer.close() | ||
| } | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.