Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 4.3k
ARROW-15016: [R] show_exec_plan for an arrow_dplyr_query#13541
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
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
403d9df4a1f94ef03ced0fdd20be350bea3c889ec012605401f4801d254e14c6294765cb5d5b7424f12accd8f7683e5492b9c0c3007de2500e3b8943aab4e03355135b9b4d494779f085730cec23f5f222eb3cdb2b334954ff3065d686f9dd452a40f4a1ba4ac7d756761b66da36f826d887d42effc9634571df4d1e4ce7e2220fe3c96fb7068dc6dfb2a5a0c14a497aac9edc3faecc61afcce91de3447f40e5d28aae3c3fae74aeebd4a02311b70bd0bc159d7834b09a02651ac37a9d238e4069a342551db36b62153451807aa886141c459af14031f88ff7fbb4c1eda73019ee9035466dcd220482c6cad5024f20799f921bc8c36376bb25751543e29a8351309e0f5f890dcc4912a45aaa3cdce8b17e0ffa30a6a8c7537fc20ca35e9ef8File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Uh oh!
There was an error while loading. Please reload this page.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -219,6 +219,53 @@ tail.arrow_dplyr_query <- function(x, n = 6L, ...) { | ||
| x | ||
| } | ||
| #' Show the details of an Arrow Execution Plan | ||
| #' | ||
| #' This is a function which gives more details about the logical query plan | ||
| #' that will be executed when evaluating an `arrow_dplyr_query` object. | ||
| #' It calls the C++ `ExecPlan` object's print method. | ||
| #' Functionally, it is similar to `dplyr::explain()`. This function is used as | ||
| #' the `dplyr::explain()` and `dplyr::show_query()` methods. | ||
| #' | ||
| #' @param x an `arrow_dplyr_query` to print the `ExecPlan` for. | ||
| #' | ||
| #' @return `x`, invisibly. | ||
| #' @export | ||
| #' | ||
| #' @examplesIf arrow_with_dataset() && requireNamespace("dplyr", quietly = TRUE) | ||
| #' library(dplyr) | ||
| #' mtcars %>% | ||
| #' arrow_table() %>% | ||
| #' filter(mpg > 20) %>% | ||
| #' mutate(x = gear/carb) %>% | ||
| #' show_exec_plan() | ||
| show_exec_plan <- function(x) { | ||
| adq <- as_adq(x) | ||
dragosmg marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| plan <- ExecPlan$create() | ||
| # do not show the plan if we have a nested query (as this will force the | ||
| # evaluation of the inner query/queries) | ||
| # TODO see if we can remove after ARROW-16628 | ||
| if (is_collapsed(x) && has_head_tail(x$.data)) { | ||
| warn("The `ExecPlan` cannot be printed for a nested query.") | ||
| return(invisible(x)) | ||
| } | ||
| final_node <- plan$Build(adq) | ||
| cat(plan$BuildAndShow(final_node)) | ||
| invisible(x) | ||
| } | ||
| show_query.arrow_dplyr_query <- function(x, ...) { | ||
| show_exec_plan(x) | ||
| } | ||
| show_query.Dataset <- show_query.ArrowTabular <- show_query.RecordBatchReader <- show_query.arrow_dplyr_query | ||
| explain.arrow_dplyr_query <- function(x, ...) { | ||
| show_exec_plan(x) | ||
| } | ||
| explain.Dataset <- explain.ArrowTabular <- explain.RecordBatchReader <- explain.arrow_dplyr_query | ||
| ensure_group_vars <- function(x) { | ||
| if (inherits(x, "arrow_dplyr_query")) { | ||
| # Before pulling data from Arrow, make sure all group vars are in the projection | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -193,6 +193,8 @@ ExecPlan <- R6Class("ExecPlan", | ||
| node | ||
| }, | ||
| Run = function(node, as_table = FALSE) { | ||
| # a section of this code is used by `BuildAndShow()` too - the 2 need to be in sync | ||
| # Start of chunk used in `BuildAndShow()` | ||
| assert_is(node, "ExecNode") | ||
| # Sorting and head/tail (if sorted) are handled in the SinkNode, | ||
| @@ -210,6 +212,8 @@ ExecPlan <- R6Class("ExecPlan", | ||
| sorting$orders <- as.integer(sorting$orders) | ||
| } | ||
| # End of chunk used in `BuildAndShow()` | ||
| # If we are going to return a Table anyway, we do this in one step and | ||
| # entirely in one C++ call to ensure that we can execute user-defined | ||
| # functions from the worker threads spawned by the ExecPlan. If not, we | ||
| @@ -273,6 +277,39 @@ ExecPlan <- R6Class("ExecPlan", | ||
| ... | ||
| ) | ||
| }, | ||
| # SinkNodes (involved in arrange and/or head/tail operations) are created in | ||
| # ExecPlan_run and are not captured by the regulat print method. We take a | ||
dragosmg marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| # similar approach to expose them before calling the print method. | ||
| BuildAndShow = function(node) { | ||
| # a section of this code is copied from `Run()` - the 2 need to be in sync | ||
| # Start of chunk copied from `Run()` | ||
| assert_is(node, "ExecNode") | ||
| # Sorting and head/tail (if sorted) are handled in the SinkNode, | ||
| # created in ExecPlan_run | ||
| sorting <- node$extras$sort %||% list() | ||
| select_k <- node$extras$head %||% -1L | ||
| has_sorting <- length(sorting) > 0 | ||
| if (has_sorting) { | ||
| if (!is.null(node$extras$tail)) { | ||
| # Reverse the sort order and take the top K, then after we'll reverse | ||
| # the resulting rows so that it is ordered as expected | ||
| sorting$orders <- !sorting$orders | ||
| select_k <- node$extras$tail | ||
| } | ||
| sorting$orders <- as.integer(sorting$orders) | ||
| } | ||
dragosmg marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| # End of chunk copied from `Run()` | ||
| ExecPlan_BuildAndShow( | ||
| self, | ||
| node, | ||
| sorting, | ||
| select_k | ||
| ) | ||
| }, | ||
| Stop = function() ExecPlan_StopProducing(self) | ||
| ) | ||
| ) | ||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Uh oh!
There was an error while loading. Please reload this page.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Uh oh!
There was an error while loading. Please reload this page.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -340,3 +340,80 @@ test_that("dplyr method not implemented messages", { | ||
| fixed = TRUE | ||
| ) | ||
| }) | ||
| test_that("show_exec_plan(), show_query() and explain() with datasets", { | ||
| # show_query() and explain() are wrappers around show_exec_plan() and are not | ||
| # tested separately | ||
| ds <- open_dataset(dataset_dir, partitioning = schema(part = uint8())) | ||
dragosmg marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| # minimal test | ||
| expect_output( | ||
| ds %>% | ||
| show_exec_plan(), | ||
| regexp = paste0( | ||
| "ExecPlan with .* nodes:.*", # boiler plate for ExecPlan | ||
| "ProjectNode.*", # output columns | ||
| "SourceNode" # entry point | ||
| ) | ||
| ) | ||
| # filter and select | ||
| expect_output( | ||
| ds %>% | ||
| select(string = chr, integer = int, part) %>% | ||
| filter(integer > 6L & part == 1) %>% | ||
| show_exec_plan(), | ||
| regexp = paste0( | ||
| "ExecPlan with .* nodes:.*", # boiler plate for ExecPlan | ||
| "ProjectNode.*", # output columns | ||
| "FilterNode.*", # filter node | ||
| "int > 6.*cast.*", # filtering expressions + auto-casting of part | ||
| "SourceNode" # entry point | ||
| ) | ||
| ) | ||
| # group_by and summarise | ||
| expect_output( | ||
| ds %>% | ||
| group_by(part) %>% | ||
| summarise(avg = mean(int)) %>% | ||
| show_exec_plan(), | ||
| regexp = paste0( | ||
| "ExecPlan with .* nodes:.*", # boiler plate for ExecPlan | ||
| "ProjectNode.*", # output columns | ||
| "GroupByNode.*", # group by node | ||
| "keys=.*part.*", # key for aggregations | ||
| "aggregates=.*hash_mean.*", # aggregations | ||
| "ProjectNode.*", # input columns | ||
| "SourceNode" # entry point | ||
| ) | ||
| ) | ||
| # arrange and head | ||
| expect_output( | ||
| ds %>% | ||
| filter(lgl) %>% | ||
| arrange(chr) %>% | ||
| show_exec_plan(), | ||
| regexp = paste0( | ||
| "ExecPlan with .* nodes:.*", # boiler plate for ExecPlan | ||
| "OrderBySinkNode.*chr.*ASC.*", # arrange goes via the OrderBy sink node | ||
| "ProjectNode.*", # output columns | ||
| "FilterNode.*", # filter node | ||
| "filter=lgl.*", # filtering expression | ||
| "SourceNode" # entry point | ||
| ) | ||
| ) | ||
| # printing the ExecPlan for a nested query would currently force the | ||
| # evaluation of the inner one(s), which we want to avoid => no output | ||
| expect_warning( | ||
| ds %>% | ||
| filter(lgl) %>% | ||
| arrange(chr) %>% | ||
| head() %>% | ||
| show_exec_plan(), | ||
| "The `ExecPlan` cannot be printed for a nested query." | ||
| ) | ||
| }) | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't follow the discussion: why not use show_query() or explain() here? I saw something about wanting to massage the output to make explain() prettier, but why not use this for explain() today since that's something people know, and it's about logical plans?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's a link to the design doc, also linked in the PR description and the Jira ticket.
In short, in my opinion, a first step would be to have
show_exec_plan()as a function that outputs exactly what we get from C++. That's what I interpreted the scope of the ticket to be.My proposal rests on this point-of-view: I think we need, for accessibility reasons, 2 separate ways of surfacing details regarding the ExecPlan, for 2 separate audiences (one readable by the seasoned Arrow developer and one readable by the regular R user).
show_exec_plan()would cater to the first audience and, thus, focus on minimising cognitive load by keeping things the same across languages.explain()should probably not aim to be both and would be targeted towards the second audience and do it well. By well I understand here as in a way that makes sense to the regular R user, who expects arrow to just work. In this context,explain()would be a tool allowing them to inspect what is going on in a dplyr-like pipeline, but do so in a language they are familiar with.Having an
explain()method would absolutely be super useful, but I do not think that is fully scoped out yet. Probably, before we start the implementation ofexplain(), we should flesh out what the output ofexplain()will look like. I strongly believe we should start from the description of the generic which says: "gives more details about an object (...) and is more focused on human readable output" (my underlining).In this PR there are already some really good suggestions of stuff we could include in the output of a function like
explain()(e.g. to coverquery_can_stream()).If we add to the above the time constraint of the impending release, I do not think extending this PR to cover
explain()would be the best course of action.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, I see your case for why not
explain(), you want to make something more R-user friendly. So then this seems likeshow_query(). The only reason I see in your doc for not using show_query is "show_query()states that one of its aims is to provide a more human readable output thatstr()". That's a really low bar here, given what str() does with R6 classes.My recommendation, for what it's worth: in this PR, make this function be both
show_queryandexplain, and in a followup, add more human embellishment toexplain(). (dbplyr:::explain.tbl_sql, for example, calls show_query() and then prints more details after it.)Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Happy with your recommendation. One of my earlier drafts looked a bit more like
explain.tbl_sql.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@nealrichardson Just to clarify, is your recommendation to have
show_exec_plan()as a stand-alone function +show_query.arrow_dplyr_query()andexplain.arrow_dplyr_query()or just the 2 methods and to discardshow_exec_plan()?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My recommendation was the latter, but I don't object to also having a standalone show_exec_plan()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the idea of having
show_exec_plan()separately as it allows us to write a bit of documentation vsshow_query()andexplain(). I'm also aware that this would be the first instance in which document dplyr-like behaviour (AFAIK there is no such documentation in the pkgdown website / help files).