From 6a1fa70f0b882f4d5dbea43fb89cc4eaee151c4a Mon Sep 17 00:00:00 2001 From: Ahmed Hussein <50450311+amahussein@users.noreply.github.com> Date: Tue, 17 May 2022 21:02:33 -0500 Subject: [PATCH] Qualification tool support UI code generation (#5470) * QualUI. add notice file to resources [skip ci] Signed-off-by: Ahmed Hussein (amahussein) * QualUI. pull UI dependencies during build [skip ci] Signed-off-by: Ahmed Hussein (amahussein) * ui-fix. add disclaimer and config app-details-view Signed-off-by: Ahmed Hussein (amahussein) 1- add disclaimer at top of page 2- add conf to enable/disable app view details. Default is false 3- Matt requests Change "Total Speed-up" to "Estimated Speed-up" and round to 1 digit after the decimal GPU Opportunity description typo: "speed-up" should be "sped up" or even change to "accelerated" add disclaimer at top of dashboard page: "Disclaimer: projections are based on TPC-DS benchmark queries run using Spark 3.X. Additionally, estimates are given assuming supported data formats and expressions are used in the application." 4- qualUI. generate js file 5- qualUI. add uiEnabledArgument 6- qualUI. add qualArgs ui enabled by default 7- qualUI. remove appInoRecords from UI usage 8- qualUI. change ui config and gitignore 9- qualUI. cleanup code * fix notice/license and address PR comments Signed-off-by: Ahmed Hussein (amahussein) * remove comment block from js code Signed-off-by: Ahmed Hussein (amahussein) * checksum verification of ui dowanloaded files Signed-off-by: Ahmed Hussein (amahussein) * modify license file Signed-off-by: Ahmed Hussein (amahussein) * modify license file-2 Signed-off-by: Ahmed Hussein (amahussein) * add license to jar and address cosmetic UI comments Signed-off-by: Ahmed Hussein (amahussein) --- LICENSE | 154 +++++++ NOTICE | 26 ++ NOTICE-binary | 27 ++ aggregator/pom.xml | 2 +- pom.xml | 1 + tools/.gitignore | 4 + tools/pom.xml | 96 ++++ tools/prepare-ui-libraries.xml | 267 +++++++++++ .../resources/ui/css/rapids-dashboard.css | 75 +++ tools/src/main/resources/ui/html/index.html | 310 +++++++++++++ tools/src/main/resources/ui/html/raw.html | 206 +++++++++ tools/src/main/resources/ui/js/qual-report.js | 433 ++++++++++++++++++ tools/src/main/resources/ui/js/raw-report.js | 146 ++++++ tools/src/main/resources/ui/js/ui-config.js | 129 ++++++ tools/src/main/resources/ui/js/uiutils.js | 363 +++++++++++++++ .../tool/qualification/QualOutputWriter.scala | 11 +- .../tool/qualification/Qualification.scala | 7 +- .../qualification/QualificationArgs.scala | 5 + .../qualification/QualificationMain.scala | 4 +- .../qualification/QualificationAppInfo.scala | 6 +- .../ui/QualificationReportGenerator.scala | 169 +++++++ 21 files changed, 2433 insertions(+), 8 deletions(-) create mode 100644 tools/prepare-ui-libraries.xml create mode 100644 tools/src/main/resources/ui/css/rapids-dashboard.css create mode 100644 tools/src/main/resources/ui/html/index.html create mode 100644 tools/src/main/resources/ui/html/raw.html create mode 100644 tools/src/main/resources/ui/js/qual-report.js create mode 100644 tools/src/main/resources/ui/js/raw-report.js create mode 100644 tools/src/main/resources/ui/js/ui-config.js create mode 100644 tools/src/main/resources/ui/js/uiutils.js create mode 100644 tools/src/main/scala/org/apache/spark/sql/rapids/tool/ui/QualificationReportGenerator.scala diff --git a/LICENSE b/LICENSE index 261eeb9e9f8..2d0582508df 100644 --- a/LICENSE +++ b/LICENSE @@ -199,3 +199,157 @@ 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. + + +MIT License +----------- + +Boostrap V4.6.1 + bootstrap.bundle.min.js + bootstrap.min.css + + The MIT License (MIT) + + Copyright (c) 2011-2019 Twitter, Inc. + Copyright (c) 2011-2019 The Bootstrap Authors + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + + +DataTables + DataTables-1.12.0/jquery.dataTables.min.js + DataTables-1.12.0/dataTables.bootstrap4.min.css + DataTables-1.12.0/dataTables.bootstrap4.min.js + searchpanes-2.0.1/searchPanes.bootstrap4.min.css + searchpanes-2.0.1/searchPanes.bootstrap4.min.js + searchpanes-2.0.1/dataTables.searchPanes.min.js + select-1.4.0/select.bootstrap4.min.css + select-1.4.0/dataTables.select.min.js + buttons-2.2.3/buttons.bootstrap4.min.css + buttons-2.2.3/buttons.bootstrap4.min.js + buttons-2.2.3/buttons.html5.min.js + buttons-2.2.3/dataTables.buttons.min.js + responsive-2.3.0/dataTables.responsive.min.js + responsive-2.3.0/responsive.bootstrap4.min.css + responsive-2.3.0/responsive.bootstrap4.min.js + + The MIT License (MIT) + + Copyright (C) 2008-2022, SpryMedia Ltd. + + Permission is hereby granted, free of charge, to any person obtaining a copy of this software + and associated documentation files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all copies or + substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING + BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +jQuery V3.6.0 + jquery.min.js + + Copyright JS Foundation and other contributors, https://js.foundation/ + + This software consists of voluntary contributions made by many + individuals. For exact contribution history, see the revision history + available at https://github.com/jquery/sizzle + + The following license applies to all parts of this software except as + documented below: + + ==== + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + ==== + + All files located in the node_modules and external directories are + externally maintained libraries used by this software which have their + own licenses; we recommend you read them, as their terms may differ from + the terms above. + +Mustache.js V4.10 + mustache.min.js + + The MIT License + + Copyright (c) 2009 Chris Wanstrath (Ruby) + Copyright (c) 2010-2014 Jan Lehnardt (JavaScript) + Copyright (c) 2010-2015 The mustache.js community + + Permission is hereby granted, free of charge, to any person obtaining a copy of this software + and associated documentation files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all copies or + substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING + BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Spur Dashboard V1.1.0 + spur.min.css + + The MIT License (MIT) + + Copyright 2018 Alexander Rechsteiner + + Permission is hereby granted, free of charge, to any person obtaining a copy of this software + and associated documentation files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all copies or + substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING + BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/NOTICE b/NOTICE index ed3155c54cd..5086dc62912 100644 --- a/NOTICE +++ b/NOTICE @@ -11,3 +11,29 @@ Copyright 2014 and onwards The Apache Software Foundation This product includes software developed at The Apache Software Foundation (http://www.apache.org/). + + +This product bundles various third-party components under other open source licenses. + +Boostrap V4.6.1 - The MIT License (MIT) + License Text ( https://github.com/twbs/bootstrap/blob/v4.6.1/LICENSE ) + Copyright (c) 2011-2021 Twitter, Inc. + Copyright (c) 2011-2021 The Bootstrap Authors + +DataTablesSrc V1.12 - The MIT License (MIT) + License Text ( https://datatables.net/license/ ) + Copyright (C) 2008-present, SpryMedia Ltd. + +jQuery V3.6.0 - The MIT License (MIT) + License Text ( https://jquery.org/license/ ) + Copyright OpenJS Foundation and other contributors, https://openjsf.org/ + +Mustache.js V4.10 - The MIT License (MIT) + License Text ( https://github.com/janl/mustache.js/blob/master/LICENSE ) + Copyright (c) 2009 Chris Wanstrath (Ruby) + Copyright (c) 2010-2014 Jan Lehnardt (JavaScript) + Copyright (c) 2010-2015 The mustache.js community + +Spur Dashboard V1.1.0 - The MIT License (MIT) + License Text ( https://github.com/HackerThemes/spur-template/blob/master/LICENSE ) + Copyright 2016 - 2019 Alexander Rechsteiner diff --git a/NOTICE-binary b/NOTICE-binary index 0c0021116d0..9b0b50f20b5 100644 --- a/NOTICE-binary +++ b/NOTICE-binary @@ -52,3 +52,30 @@ PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +--------------------------------------------------------------------- + +This product bundles various third-party components under other open source licenses. + +Boostrap V4.6.1 - The MIT License (MIT) + License Text ( https://github.com/twbs/bootstrap/blob/v4.6.1/LICENSE ) + Copyright (c) 2011-2021 Twitter, Inc. + Copyright (c) 2011-2021 The Bootstrap Authors + +DataTablesSrc V1.12 - The MIT License (MIT) + License Text ( https://datatables.net/license/ ) + Copyright (C) 2008-present, SpryMedia Ltd. + +jQuery V3.6.0 - The MIT License (MIT) + License Text ( https://jquery.org/license/ ) + Copyright OpenJS Foundation and other contributors, https://openjsf.org/ + +Mustache.js V4.10 - The MIT License (MIT) + License Text ( https://github.com/janl/mustache.js/blob/master/LICENSE ) + Copyright (c) 2009 Chris Wanstrath (Ruby) + Copyright (c) 2010-2014 Jan Lehnardt (JavaScript) + Copyright (c) 2010-2015 The mustache.js community + +Spur Dashboard V1.1.0 - The MIT License (MIT) + License Text ( https://github.com/HackerThemes/spur-template/blob/master/LICENSE ) + Copyright 2016 - 2019 Alexander Rechsteiner diff --git a/aggregator/pom.xml b/aggregator/pom.xml index 9805d233733..62896e62512 100644 --- a/aggregator/pom.xml +++ b/aggregator/pom.xml @@ -171,7 +171,7 @@ maven-clean-plugin - 3.1.0 + ${maven.clean.plugin.version} clean-reduced-dependency-poms diff --git a/pom.xml b/pom.xml index 5831552c5ab..ca853f8051e 100644 --- a/pom.xml +++ b/pom.xml @@ -861,6 +861,7 @@ 3.3.1 org/scala-lang/scala-library/${scala.version}/scala-library-${scala.version}.jar ${spark.version.classifier} + 3.1.0 diff --git a/tools/.gitignore b/tools/.gitignore index 62f4e172158..637311cdef2 100644 --- a/tools/.gitignore +++ b/tools/.gitignore @@ -2,3 +2,7 @@ *.iml target event_log_profiling.log +## ignore qual-ui static files +**/ui/assets/ +**/ui-dependencies-cache/ +**/ui/js/data-output.js diff --git a/tools/pom.xml b/tools/pom.xml index 92401534ea4..0e51a4acb44 100644 --- a/tools/pom.xml +++ b/tools/pom.xml @@ -37,6 +37,10 @@ ${spark311.version} spark311 spark311 + ${project.build.directory}/ui-dependencies-cache + src/main/resources/ui + ${project.basedir}/${ui.resources.relative} + assets @@ -86,6 +90,14 @@ ${project.basedir}/src/main/resources + + ${project.basedir}/.. + META-INF + + + LICENSE + + @@ -138,6 +150,90 @@ net.alchim31.maven scala-maven-plugin + + org.apache.maven.plugins + maven-antrun-plugin + + + download-ui-dependencies + generate-sources + + run + + + + + + + + + + + + + + + + + + + + + + + + copy-notice + + run + + process-resources + + + + + + + + + + + + + + + + + maven-clean-plugin + ${maven.clean.plugin.version} + + + clean-qual-ui-dependencies + clean + + clean + + + + + ${ui.resources.relative} + + ${ui.resources.external.folder}/ + + + + + + + diff --git a/tools/prepare-ui-libraries.xml b/tools/prepare-ui-libraries.xml new file mode 100644 index 00000000000..a65a2a19f79 --- /dev/null +++ b/tools/prepare-ui-libraries.xml @@ -0,0 +1,267 @@ + + + + + Ant task to download dependency files from internet and extract during maven package. + Those files are used to render the Qualification UI report file. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Checksum error + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tools/src/main/resources/ui/css/rapids-dashboard.css b/tools/src/main/resources/ui/css/rapids-dashboard.css new file mode 100644 index 00000000000..7a9f8d3a0d7 --- /dev/null +++ b/tools/src/main/resources/ui/css/rapids-dashboard.css @@ -0,0 +1,75 @@ +/*! + * 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. + */ + +/* Customised Styling for the RAPIDS dashboard */ + +.arrow-open { + width: 0; + height: 0; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-top: 5px solid #08c; + display: inline-block; + margin-bottom: 2px; +} + +.arrow-closed { + width: 0; + height: 0; + border-top: 5px solid transparent; + border-bottom: 5px solid transparent; + border-left: 5px solid #08c; + display: inline-block; + margin-left: 2px; + margin-right: 3px; +} + +table.dataTable tr.dtrg-group.dtrg-level-0 td { + background-color: #f0f1f7; +} + +.badge-strong-recommended { + color: #fff; + background-color: #1DAB47; } +a.badge-strong-recommended:hover, a.badge-strong-recommended:focus { + color: #fff; + background-color: #167f35; } +a.badge-strong-recommended:focus, a.badge-strong-recommended.focus { + outline: 0; + -webkit-box-shadow: 0 0 0 0.2rem rgba(29, 171, 71, 0.5); + box-shadow: 0 0 0 0.2rem rgba(29, 171, 71, 0.5); } + +.badge-recommended { + color: #fff; + background-color: rgba(10, 102, 35, 0.98); } +a.badge-recommended:hover, a.badge-recommended:focus { + color: #fff; + background-color: #0a6623; } +a.badge-recommended:focus, a.badge-recommended.focus { + outline: 0; + -webkit-box-shadow: 0 0 0 0.2rem rgba(29, 171, 71, 0.5); + box-shadow: 0 0 0 0.2rem rgba(29, 171, 71, 0.5); } + +.badge-not-recommended { + color: #212529; + background-color: #FCAE3F; } +a.badge-not-recommended:hover, a.badge-not-recommended:focus { + color: #212529; + background-color: #fb990d; } +a.badge-not-recommended:focus, a.badge-not-recommended.focus { + outline: 0; + -webkit-box-shadow: 0 0 0 0.2rem rgba(252, 174, 63, 0.5); + box-shadow: 0 0 0 0.2rem rgba(252, 174, 63, 0.5); } \ No newline at end of file diff --git a/tools/src/main/resources/ui/html/index.html b/tools/src/main/resources/ui/html/index.html new file mode 100644 index 00000000000..a23cfe15f83 --- /dev/null +++ b/tools/src/main/resources/ui/html/index.html @@ -0,0 +1,310 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Qualification Tool Dashboard + + + +
+ +
+
+ + + + +
+ GitHub +
+
+
+

Qualification Tool

+
+ +
+
+ +
+
+
+
+
+
+
+
+
+ + + + +
+
GPU Recommendations Table
+
+ +
+
+
+
+ + + + + + + + + + + +
+ App Name + + App ID + + App Duration + + Estimated Speed-up + + Recommendation +
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + diff --git a/tools/src/main/resources/ui/html/raw.html b/tools/src/main/resources/ui/html/raw.html new file mode 100644 index 00000000000..758bab94590 --- /dev/null +++ b/tools/src/main/resources/ui/html/raw.html @@ -0,0 +1,206 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Qualification Tool Dashboard – Raw Data + + + +
+ +
+
+ + + + +
+ GitHub +
+
+
+
+

Full Report

+
+
+ +
+
+
+
+
+
+
+ + + + +
+
Raw Data of all Applications
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
App NameApp ID + SQL DF Duration + SQL Dataframe Task Duration + App Duration + Executor CPU Time PercentSQL Duration with Potential ProblemsSQL Ids with FailuresRead Score PercentRead File Format ScoreUnsupported Read File Formats and TypesWrite Data FormatComplex TypesNested Complex TypesEstimated DurationUnsupported DurationSpeed-up DurationSpeed-up FactorTotal Speed-upSpeed-up BucketLongest SqlDuration
+
+
+
+
+
+
+
+
+
+ + + + + + + + + diff --git a/tools/src/main/resources/ui/js/qual-report.js b/tools/src/main/resources/ui/js/qual-report.js new file mode 100644 index 00000000000..b45f89dab30 --- /dev/null +++ b/tools/src/main/resources/ui/js/qual-report.js @@ -0,0 +1,433 @@ +/* + * 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. + */ + +/* global $, Mustache, formatDuration, jQuery, qualificationRecords, qualReportSummary */ + +function resetCollapsableGrps(groupArr, flag) { + groupArr.forEach(grpElemnt => grpElemnt.collapsed = flag); +} + +/* + * HTML template used to render the application details in the collapsible + * rows of the GPURecommendationTable. + */ +function getExpandedAppDetails(rowData) { + let fullDetailsContent = + ''; + let tableContent = + '' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + '
#ValueDescription
Estimated Speed-up {{totalSpeedup_display}} ' + toolTipsValues.gpuRecommendations.details.mathFormatted.totalSpeedup + '
App Duration {{durationCollection.appDuration}} ' + toolTipsValues.gpuRecommendations["App Duration"] + '
GPU Estimated Duration {{durationCollection.estimatedDurationWallClock}} ' + toolTipsValues.gpuRecommendations.details.estimatedDuration + '
SQL Duration {{durationCollection.sqlDFDuration}} ' + toolTipsValues.gpuRecommendations.details.sqlDFDuration + '
GPU Opportunity {{durationCollection.accelerationOpportunity}} ' + toolTipsValues.gpuRecommendations.details.gpuOpportunity + '
'; + + if (UIConfig.fullAppView.enabled) { + return tableContent + fullDetailsContent; + } + return tableContent ; +} + + +function formatAppGPURecommendation ( rowData) { + var text = Mustache.render(getExpandedAppDetails(rowData), rowData); + return text; +} + +let definedDataTables = {}; +let gpuRecommendationTableID = "datatables.gpuRecommendations"; + +function expandAllGpuRowEntries() { + expandAllGpuRows(definedDataTables[gpuRecommendationTableID]); +} + +function collapseAllGpuRowEntries() { + collapseAllGpuRows(definedDataTables[gpuRecommendationTableID]); +} + +function expandAllGpuRows(gpuTable) { + resetCollapsableGrps(recommendationContainer, false); + + // Enumerate all rows + gpuTable.rows().every(function(){ + // If row has details collapsed + if (!this.child.isShown()){ + // Open this row + this.child(formatAppGPURecommendation(this.data())).show(); + $(this.node()).addClass('shown'); + } + }); + gpuTable.draw(false); + +} + +function collapseAllGpuRows(gpuTable) { + resetCollapsableGrps(recommendationContainer, true); + // Enumerate all rows + gpuTable.rows().every(function(){ + // If row has details expanded + if(this.child.isShown()){ + // Collapse row details + this.child.hide(); + $(this.node()).removeClass('shown'); + } + }); + gpuTable.draw(false); +} + +$(document).ready(function(){ + // do the required filtering here + let attemptArray = processRawData(qualificationRecords); + let initGpuRecommendationConf = UIConfig[gpuRecommendationTableID]; + // Start implementation of GPU Recommendations Apps + + let recommendGPUColName = "gpuRecommendation" + let totalSpeedupColumnName = "totalSpeedup" + let sortColumnForGPURecommend = totalSpeedupColumnName + let gpuRecommendationConf = { + responsive: true, + info: true, + paging: (attemptArray.length > defaultPageLength), + pageLength: defaultPageLength, + lengthMenu: defaultLengthMenu, + stripeClasses: [], + "data": attemptArray, + "columns": [ + { + "className": 'dt-control', + "orderable": false, + "data": null, + "defaultContent": '' + }, + {data: "appName"}, + { + data: "appId", + render: (appId, type, row) => { + if (type === 'display' || type === 'filter') { + if (UIConfig.fullAppView.enabled) { + return `
${appId}` + } + } + return appId; + } + }, + { + name: 'appDuration', + data: 'appDuration', + type: 'numeric', + searchable: false, + render: function (data, type, row) { + if (type === 'display' || type === 'filter') { + return formatDuration(data) + } + return data; + }, + fnCreatedCell: (nTd, sData, oData, _ignored_iRow, _ignored_iCol) => { + if (oData.estimated) { + $(nTd).css('color', 'blue'); + } + } + }, + { + name: totalSpeedupColumnName, + data: 'totalSpeedup_display', + searchable: false, + type: 'numeric', + }, + { + name: recommendGPUColName, + data: 'gpuCategory', + render: function (data, type, row) { + if (type === 'display') { + let recommendGroup = recommendationsMap.get(data); + return `` + recommendGroup.displayName + ``; + } + return data; + }, + fnCreatedCell: (nTd, sData, oData, _ignored_iRow, _ignored_iCol) => { + let recommendGroup = recommendationsMap.get(sData); + let toolTipVal = recommendGroup.description; + $(nTd).attr('data-toggle', "tooltip"); + $(nTd).attr('data-placement', "top"); + $(nTd).attr('html', "true"); + $(nTd).attr('data-html', "true"); + $(nTd).attr('title', toolTipVal); + } + } + ], + //dom with search panes + //dom: 'Bfrtlip', + //dom: '<"dtsp-dataTable"Bfrtip>', + dom: 'Bfrtlip', + initComplete: function(settings, json) { + // Add custom Tool Tip to the headers of the table + $('#gpu-recommendation-table thead th').each(function () { + var $td = $(this); + var toolTipVal = toolTipsValues.gpuRecommendations[$td.text().trim()]; + $td.attr('data-toggle', "tooltip"); + $td.attr('data-placement', "top"); + $td.attr('html', "true"); + $td.attr('data-html', "true"); + $td.attr('title', toolTipVal); + }); + } + }; + + gpuRecommendationConf.order = + [[getColumnIndex(gpuRecommendationConf.columns, sortColumnForGPURecommend), "desc"]]; + if (initGpuRecommendationConf["rowgroup.enabled"] == true) { + gpuRecommendationConf.rowGroup = { + startRender: function (rows, group) { + // var collapsed = !!(collapsedGroups[group]); + let collapsedBool = recommendationsMap[group].collapsed; + rows.nodes().each(function (r) { + r.style.display = ''; + if (collapsedBool) { + r.style.display = 'none'; + } + }); + // Iterate group rows and close open child rows. + if (collapsedBool) { + rows.every(function (rowIdx, tableLoop, rowLoop) { + if (this.child.isShown()) { + var tr = $(this.node()); + this.child.hide(); + + tr.removeClass('shown'); + } + }); + } + var arrow = collapsedBool ? + ' ' + : ' '; + + let toolTip = 'data-toggle=\"tooltip\" data-html=\"true\" data-placement=\"top\" ' + + 'title=\"' + recommendationsMap[group].description + '\"'; + var addToolTip = true; + return $('') + .append('' + + arrow + ' ' + + group + + ' (' + rows.count() + ')' + + '') + .attr('data-name', group) + .toggleClass('collapsed', collapsedBool); + }, + dataSrc: function (row) { + var recommendedGroup = recommendationContainer.find(grp => grp.isGroupOf(row)) + return recommendedGroup.displayName; + } + } + } // rowGrouping by recommendations + + + // set the dom of the tableConf + gpuRecommendationConf.dom = initGpuRecommendationConf["Dom"].default; + + if (initGpuRecommendationConf.hasOwnProperty('searchPanes')) { + let searchPanesConf = initGpuRecommendationConf['searchPanes'] + if (searchPanesConf["enabled"]) { + // disable searchpanes on default columns + gpuRecommendationConf.columnDefs = [{ + "searchPanes": { + show: false, + }, + "targets": ['_all'] + }]; + // add custom panes + gpuRecommendationConf.searchPanes = searchPanesConf["dtConfigurations"]; + + // add the searchpanes to the dom + gpuRecommendationConf.dom = 'P' + gpuRecommendationConf.dom; + // add custom panes to display recommendations + let panesConfigurations = searchPanesConf["panes"]; + // first define values of the first recommendation Pane + let gpuCatgeoryOptions = function() { + let categoryOptions = []; + for (let i in recommendationContainer) { + let currOption = { + label: recommendationContainer[i].displayName, + value: function(rowData, rowIdx) { + return (rowData["gpuCategory"] === recommendationContainer[i].displayName); + } + } + categoryOptions.push(currOption); + } + return categoryOptions; + }; + // define the display options of the recommendation pane + let gpuRecommendationPane = function() { + let gpuPaneConfig = panesConfigurations["recommendation"]; + let recommendationPaneConf = {}; + recommendationPaneConf.header = gpuPaneConfig["header"]; + recommendationPaneConf.options = gpuCatgeoryOptions(); + recommendationPaneConf.dtOpts = { + "searching": gpuPaneConfig["search"], + "order": gpuPaneConfig["order"], + } + recommendationPaneConf.combiner = 'and'; + return recommendationPaneConf; + } + // define searchPanes for users + let sparkUsersOptions = function() { + let sparkUserOptions = []; + sparkUsers.forEach((data, userName) => { + let currOption = { + label: userName, + value: function(rowData, rowIdx) { + // get spark user + return (rowData["user"] === userName); + }, + } + sparkUserOptions.push(currOption); + }); + return sparkUserOptions; + }; + // define the display options of the user filter pane + let userPane = function() { + let userPaneConfig = panesConfigurations["users"]; + let recommendationPaneConf = {}; + recommendationPaneConf.header = userPaneConfig["header"]; + recommendationPaneConf.options = sparkUsersOptions(); + recommendationPaneConf.dtOpts = { + "searching": userPaneConfig["search"], + "order": userPaneConfig["order"], + } + recommendationPaneConf.combiner = 'and'; + return recommendationPaneConf; + } + gpuRecommendationConf.searchPanes.panes = [ + gpuRecommendationPane(), userPane() + ]; + } + } + + // add buttons if enabled + if (initGpuRecommendationConf.hasOwnProperty('buttons')) { + let buttonsConf = initGpuRecommendationConf['buttons']; + if (buttonsConf["enabled"]) { + // below if to avoid the buttons messed up with length page + // dom: "<'row'<'col-sm-12 col-md-10'><'col-sm-12 col-md-2'B>>" + + // "<'row'<'col-sm-12 col-md-6'l><'col-sm-12 col-md-6'f>>" + + // "<'row'<'col-sm-12'tr>>" + + // "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>", + gpuRecommendationConf["buttons"] = buttonsConf.buttons + gpuRecommendationConf.dom = 'B' + gpuRecommendationConf.dom + } + } + + var gpuRecommendationTable = $('#gpu-recommendation-table').DataTable(gpuRecommendationConf); + + definedDataTables[gpuRecommendationTableID] = gpuRecommendationTable; + + //TODO: we need to expand the rowGroups on search events + //There is a possible solution + // https://stackoverflow.com/questions/57692989/datatables-trigger-rowgroup-click-with-search-filter + + $('#gpu-recommendation-table tbody').on('click', 'tr.dtrg-start', function () { + var name = $(this).data('name'); + // we may need to hide tooltip hangs + // $('#gpu-recommendation-table [data-toggle="tooltip"]').tooltip('hide'); + recommendationsMap[name].toggleCollapsed(); + gpuRecommendationTable.draw(false); + }); + + // Add event listener for opening and closing details + $('#gpu-recommendation-table tbody').on('click', 'td.dt-control', function () { + var tr = $(this).closest('tr'); + var row = gpuRecommendationTable.row( tr ); + + if ( row.child.isShown() ) { + // This row is already open - close it + row.child.hide(); + tr.removeClass('shown'); + } + else { + // Open this row + row.child( formatAppGPURecommendation(row.data()) ).show(); + tr.addClass('shown'); + } + }); + + // Handle click on "Expand All" button + $('#btn-show-all-children').on('click', function() { + expandAllGpuRows(gpuRecommendationTable); + }); + + // Handle click on "Collapse All" button + $('#btn-hide-all-children').on('click', function() { + collapseAllGpuRows(gpuRecommendationTable); + }); + + // set the template of the report qualReportSummary + var template = $("#qual-report-summary-template").html(); + var text = Mustache.render(template, qualReportSummary); + $("#qual-report-summary").html(text); + + // set the template of the Qualification runtimeInformation + if (false) { + //TODO: fill the template of the execution: last executed, how long it took..etc + var template = $("#qual-report-runtime-information-template").html(); + var text = Mustache.render(template, qualReportSummary); + $("#qual-report-runtime-information").html(text); + } + // set the tootTips for the table + $('#gpu-recommendation-card [data-toggle="tooltip"]').tooltip({ + container: 'body', + html: true, + animation: true, + placement:"bottom", + delay: {show: 0, hide: 10.0}}); + + setupNavigation(); +}); diff --git a/tools/src/main/resources/ui/js/raw-report.js b/tools/src/main/resources/ui/js/raw-report.js new file mode 100644 index 00000000000..9e4d22f4b42 --- /dev/null +++ b/tools/src/main/resources/ui/js/raw-report.js @@ -0,0 +1,146 @@ +/* + * 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. + */ + +/* global $, Mustache, formatDuration, jQuery, qualificationRecords */ + +$(document).ready(function() { + let attemptArray = processRawData(qualificationRecords); + let rawDataTableConf = { + // TODO: To use horizontal scroll for wide table + //"scrollX": true, + responsive: true, + paging: (attemptArray.length > defaultPageLength), + pageLength: defaultPageLength, + lengthMenu: defaultLengthMenu, + info: true, + data: attemptArray, + columns: [ + {data: "appName"}, + { + data: "appId", + render: (appId, type, row) => { + if (type === 'display' || type === 'filter') { + return `${appId}` + } + return appId; + } + }, + { + name: 'sqlDataFrameDuration', + data: 'sqlDataFrameDuration', + searchable: false, + render: function (data, type, row) { + if (type === 'display' || type === 'filter') { + return formatDuration(data) + } + return data; + }, + }, + { + name: 'sqlDataframeTaskDuration', + data: 'sqlDataframeTaskDuration', + searchable: false, + render: function (data, type, row) { + if (type === 'display' || type === 'filter') { + return formatDuration(data) + } + return data; + }, + }, + { + name: 'appDuration', + data: 'appDuration', + type: 'numeric', + searchable: false, + render: function (data, type, row) { + if (type === 'display' || type === 'filter') { + return formatDuration(data) + } + return data; + }, + fnCreatedCell: (nTd, sData, oData, _ignored_iRow, _ignored_iCol) => { + if (oData.estimated) { + $(nTd).css('color', 'blue'); + } + } + }, + { + data: "executorCpuTimePercent", + searchable: false, + fnCreatedCell: (nTd, sData, oData, _ignored_iRow, _ignored_iCol) => { + if (oData.executorCpuTimePercent >= 0) { + $(nTd).css('color', totalCPUPercentageColor(oData.executorCpuTimePercent)); + $(nTd).css('background', totalCPUPercentageStyle(oData.executorCpuTimePercent)); + } + } + }, + { + data: "sqlDurationForProblematic", + searchable: false, + }, + {data: "failedSQLIds"}, + {data: "readScorePercent"}, + {data: "readFileFormatScore"}, + {data: "readFileFormatAndTypesNotSupported"}, + { + data: "writeDataFormat", + orderable: false, + }, + { + data: "complexTypes", + orderable: false, + }, + { + data: "nestedComplexTypes", + orderable: false, + }, + { + data: "estimatedDuration", + }, + { + data: "unsupportedDuration", + }, + { + data: "speedupDuration", + }, + { + data: "speedupFactor", + }, + { + data: "totalSpeedup", + }, + { + data: "speedupBucket", + }, + { + data: "longestSqlDuration", + } + ], + // dom: "<'row'<'col-sm-12 col-md-6'B><'col-sm-12 col-md-6'>>" + + // "<'row'<'col-sm-12 col-md-6'l><'col-sm-12 col-md-6'f>>" + + // "<'row'<'col-sm-12'tr>>" + + // "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>", + dom: 'Bfrtlip', + buttons: [{ + extend: 'csv', + text: 'Export' + }] + }; + var rawAppsTable = $('#all-apps-raw-data-table').DataTable(rawDataTableConf); + $('#all-apps-raw-data [data-toggle="tooltip"]').tooltip(); + + setupNavigation(); +}); diff --git a/tools/src/main/resources/ui/js/ui-config.js b/tools/src/main/resources/ui/js/ui-config.js new file mode 100644 index 00000000000..4282c1d2f42 --- /dev/null +++ b/tools/src/main/resources/ui/js/ui-config.js @@ -0,0 +1,129 @@ +/* + * 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. + */ + +let qualReportSummary = { + "config": { + "showTLCSummary": false + }, + "totalApps": { + "numeric": 0, + "header": "Total Applications", + "statsPercentage": "%", + "statsTimeFrame": "Apps with Estimated End Time", + "totalAppsDurations": "0 ms", + "totalAppsDurationLabel": "Total Run Durations", + }, + "candidates": { + "numeric": 0, + "header": "RAPIDS Candidates", + "statsPercentage": "%", + "statsTimeFrame": "Fit for GPU acceleration", + }, + "speedups": { + "numeric": "N/A", + "header": "GPU Opportunity", + "statsPercentage": "%", + "statsTimeFrame": "Supported SQL DF Durations", + "totalSqlDataframeTaskDuration" : "0 ms", + "totalSqlDFDurationsLabel" : "Total SqlDF Durations", + }, + "tlc": { + "numeric": 0, + "header": "Apps that need TLC", + "statsPercentage": "% Needs more information", + "statsTimeFrame": "We found apps with potential problems", + }, +}; + +let toolTipsValues = { + "gpuRecommendations": { + "App Name": "Name of the application", + "App ID": "An application is referenced by its application ID, \app-id\<\/em\>. " + + "\ When running on YARN, each application may have multiple attempts, but there are " + + "attempt IDs only for applications in cluster mode, not applications in client mode. " + + "Applications in YARN cluster mode can be identified by their \attempt-id\<\/em\>.", + "App Duration": "Wall-Clock time measured since the application starts till it is completed. " + + "If an app is not completed an estimated completion time would be computed.", + "GPU Opportunity": "Expected percentage of Sql Task Duration that could be accelerated by the GPU ", + "Recommendation": "Recommendation based on \Estimated Speed-up Factor\<\/em\>.", + "Estimated Speed-up": "Speed-up factor estimated for the app. Calculated as the ratio between \App Duration\<\/em\> and \GPU Estimated Duration\<\/em\>", + "details": { + "mathFormatted": { + "totalSpeedup": + // math tags inside tooltip does not work on Chrome. Using Sup and Sub as a work-around for now. + //"Speed-up factor estimated for the app. Calculated as (App DurationGPU Estimated Duration)", + "Speed-up factor estimated for the app. Calculated as (App DurationGPU Estimated Duration)", + }, + "totalSpeedup": + "Speed-up factor estimated for the app. Calculated as (App DurationGPU Estimated Duration)", + "nonSqlTaskDurationAndOverhead": "total duration of the app not involving SQL", + "estimatedDuration": "Predicted runtime of the app if it was run on GPU", + "speedupDuration": "Duration of SQL operations that are supported on GPU. It is calculated as (sqlDuration - unsupportedDuration)", + "unsupportedDuration": "An estimate total duration of SQL operations that are not supported on GPU", + "sqlDFDuration": "Time duration that includes only SQL-Dataframe queries.", + "gpuOpportunity": "Wall-Clock time that shows how much of the SQL duration can be speed-up on the GPU." + } + } +} + +let UIConfig = { + "dataProcessing": { + // name of the column used to decide on the category of the app + // total SpeedUp is a factor between 1.0 and 10.0 + "gpuRecommendation.appColumn": "totalSpeedup", + // when set to true, the JS will generate random value for recommendations + "simulateRecommendation": false + }, + "datatables.gpuRecommendations": { + "rowgroup.enabled": false, + "searchPanes": { + enabled: true, + "dtConfigurations": { + initCollapsed: true, + viewTotal: true, + // Note that there is a bug in cascading that breaks paging of the table + cascadePanes: true, + show: false, + }, + "panes": { + "recommendation": { + "header": "Recommendations", + "search": true, + "order": [[0, 'desc']], + }, + "users":{ + "header": "Spark User", + "search": true, + } + } + }, + "Dom" : { + default: 'frtlip', + }, + "buttons": { + enabled: true, + buttons: [ + { + extend: 'csv', + text: 'Export' + } + ], + } + }, + "fullAppView": { + enabled: false + } +}; diff --git a/tools/src/main/resources/ui/js/uiutils.js b/tools/src/main/resources/ui/js/uiutils.js new file mode 100644 index 00000000000..9e7255936bb --- /dev/null +++ b/tools/src/main/resources/ui/js/uiutils.js @@ -0,0 +1,363 @@ +/* + * 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. + */ + +/* globals $, Mustache, qualReportSummary */ + +const twoDecimalFormatter = new Intl.NumberFormat('en-US', { + minimumFractionDigits: 2, + maximumFractionDigits: 2, +}); + +function padZeroes(num) { + return ("0" + num).slice(-2); +} + +/* eslint-disable no-unused-vars */ +function formatTimeMillis(timeMillis) { + if (timeMillis <= 0) { + return "-"; + } else { + var dt = new Date(timeMillis); + return formatDateString(dt); + } +} + +/* eslint-enable no-unused-vars */ + +function formatDateString(dt) { + return dt.getFullYear() + "-" + + padZeroes(dt.getMonth() + 1) + "-" + + padZeroes(dt.getDate()) + " " + + padZeroes(dt.getHours()) + ":" + + padZeroes(dt.getMinutes()) + ":" + + padZeroes(dt.getSeconds()); +} + +function formatDuration(milliseconds) { + if (milliseconds < 100) { + return parseInt(milliseconds).toFixed(1) + " ms"; + } + var seconds = milliseconds * 1.0 / 1000; + if (seconds < 1) { + return seconds.toFixed(1) + " s"; + } + if (seconds < 60) { + return seconds.toFixed(0) + " s"; + } + var minutes = seconds / 60; + if (minutes < 10) { + return minutes.toFixed(1) + " min"; + } else if (minutes < 60) { + return minutes.toFixed(0) + " min"; + } + var hours = minutes / 60; + return hours.toFixed(1) + " h"; +} + +function getColumnIndex(columns, columnName) { + for (var i = 0; i < columns.length; i++) { + if (columns[i].name == columnName) + return i; + } + return -1; +} + +// The maximum is inclusive and the minimum is inclusive +function getRandomIntInclusive(min, max) { + min = Math.ceil(min); + max = Math.floor(max); + return Math.floor(Math.random() * (max - min + 1) + min); +} + +/** calculations of CPU Processor **/ + +var CPUPercentThreshold = 40.0; + +function totalCPUPercentageStyle(cpuPercent) { + // Red if GC time over GCTimePercent of total time + return (cpuPercent < CPUPercentThreshold) ? + ("hsl(0, 100%, 50%, " + totalCPUPercentageAlpha(CPUPercentThreshold - cpuPercent) + ")") : ""; +} + +function totalCPUPercentageAlpha(actualCPUPercentage) { + return actualCPUPercentage >= 0 ? + (Math.min(actualCPUPercentage / 40.0 + 0.4, 1)) : 1; +} + +function totalCPUPercentageColor(cpuPercent) { + return (cpuPercent < CPUPercentThreshold) ? "white" : "black"; +} + +/** recommendation icons display */ +function recommendationTableCellStyle(recommendation) { + return "hsla("+ recommendation * 10.0 +",100%,50%)"; +} + +/* define recommendation grouping */ +const recommendationRanges = { + "A": {low: 2.5, high: 10.0}, + "B": {low: 1.25, high: 2.5}, + "C": {low: -1000.0, high: 1.25}, +} + +class GpuRecommendationCategory { + constructor(id, relRate, printName, descr, displayClass, initCollapsed = false) { + this.id = id; + this.displayName = printName; + this.range = recommendationRanges[id]; + this.collapsed = initCollapsed; + this.description = descr; + this.rate = relRate; + this.badgeDisplay = displayClass; + } + + // Method + isGroupOf(row) { + return row.gpuRecommendation >= this.range.low + && row.gpuRecommendation < this.range.high; + } + + toggleCollapsed() { + this.collapsed = !this.collapsed; + } + + getBadgeDisplay(row) { + return this.badgeDisplay; + } +} + +let recommendationContainer = [ + new GpuRecommendationCategory("A", 5, + "Strongly Recommended", + "Spark Rapids is expected to speedup the App", + "badge badge-pill badge-strong-recommended"), + new GpuRecommendationCategory("B", 4, + "Recommended", + "Using Spark RAPIDS expected to give a moderate speedup.", + "badge badge-pill badge-recommended"), + new GpuRecommendationCategory("C", 3, + "Not Recommended", + "[Not-Recommended]: It is not likely that GPU Acceleration will be tangible", + "badge badge-pill badge-not-recommended"), +]; + + +function createRecommendationGroups(recommendationsArr) { + let map = new Map() + recommendationsArr.forEach(object => { + map.set(object.displayName, object); + }); + return map; +} + +let recommendationsMap = new Map(createRecommendationGroups(recommendationContainer)); + +let sparkUsers = new Map(); + + +/* define constants for the tables configurations */ +let defaultPageLength = 20; +let defaultLengthMenu = [[20, 40, 60, 100, -1], [20, 40, 60, 100, "All"]]; + +let appFieldAccCriterion = UIConfig.dataProcessing["gpuRecommendation.appColumn"]; +let simulateRecommendationEnabled = UIConfig.dataProcessing["simulateRecommendation"]; + +function simulateGPURecommendations(appsArray, maxScore) { + for (let i in appsArray) { + appsArray[i]["gpuRecommendation"] = + simulateRecommendationEnabled ? getRandomIntInclusive(1, 10) + : appsArray[i][appFieldAccCriterion]; + } +} + +// bind the raw data top the GPU recommendations +function setGPURecommendations(appsArray) { + for (let i in appsArray) { + let appCategory = recommendationContainer.find(grp => grp.isGroupOf(appsArray[i])) + appsArray[i]["gpuCategory"] = appCategory.displayName; + } +} + +function setAppInfoRecord(appRecord) { + // set default values + sparkUsers.set(appRecord["user"], true); +} + +// which maps into wallclock time that shows how much of the SQL duration we think we can +// speed up on the GPU +function calculateAccOpportunityAsDuration(appRec) { + let ratio = (appRec["speedupDuration"] * 1.0) / appRec["sqlDataframeTaskDuration"]; + return appRec["sqlDataFrameDuration"] * ratio; +} + +function setAppTaskDuration(appRec) { + // appTaskDuration = nonSql + sqlTask Durations + appRec["appTaskDuration"] = + appRec["sqlDataframeTaskDuration"] + + appRec["nonSqlTaskDurationAndOverhead"] +} + +function calculateAccOpportunity(appRec) { + return (appRec["speedupDuration"] * 100.0) / appRec["appTaskDuration"]; +} + +function processRawData(rawRecords) { + let processedRecords = []; + let maxOpportunity = 0; + for (let i in rawRecords) { + let appRecord = JSON.parse(JSON.stringify(rawRecords[i])); + appRecord["estimated"] = appRecord["appDurationEstimated"]; + appRecord["cpuPercent"] = appRecord["executorCPUPercent"]; + // set default longestSqlDuration for backward compatibility + if (!appRecord.hasOwnProperty("longestSqlDuration")) { + appRecord["longestSqlDuration"] = 0; + } + appRecord["durationCollection"] = { + "appDuration": formatDuration(appRecord["appDuration"]), + "sqlDFDuration": formatDuration(appRecord["sqlDataFrameDuration"]), + "sqlDFTaskDuration": formatDuration(appRecord["sqlDataframeTaskDuration"]), + "sqlDurationProblems": formatDuration(appRecord["sqlDurationForProblematic"]), + "nonSqlTaskDurationAndOverhead": formatDuration(appRecord["nonSqlTaskDurationAndOverhead"]), + "estimatedDuration": formatDuration(appRecord["estimatedDuration"]), + "estimatedDurationWallClock": + formatDuration((appRecord["appDuration"] * 1.0) / appRecord["totalSpeedup"]), + "accelerationOpportunity": formatDuration(calculateAccOpportunityAsDuration(appRecord)), + "unsupportedDuration": formatDuration(appRecord["unsupportedDuration"]), + "speedupDuration": formatDuration(appRecord["speedupDuration"]), + "longestSqlDuration": formatDuration(appRecord["longestSqlDuration"]), + } + + appRecord["totalSpeedup_display"] = + parseFloat(appRecord["totalSpeedup"]).toFixed(1); + setAppInfoRecord(appRecord); + maxOpportunity = + (maxOpportunity < appRecord[appFieldAccCriterion]) + ? appRecord[appFieldAccCriterion] : maxOpportunity; + if (UIConfig.fullAppView.enabled) { + appRecord["attemptDetailsURL"] = "application.html?app_id=" + appRecord.appId; + } else { + appRecord["attemptDetailsURL"] = "#!" + } + + setAppTaskDuration(appRecord); + appRecord["accelerationOpportunity"] = calculateAccOpportunity(appRecord); + processedRecords.push(appRecord) + } + simulateGPURecommendations(processedRecords, maxOpportunity); + setGPURecommendations(processedRecords); + setGlobalReportSummary(processedRecords); + return processedRecords; +} + +function processReadFormatSchema(rawDSInfoRecords) { + let rawDSInfoRecordsContainer = { + records: rawDSInfoRecords, + allFormats: new Map() + } + for (let i in rawDSInfoRecords) { + let dsRec = rawDSInfoRecords[i] + for (let j in dsRec["dsData"]) { + let readRec = dsRec["dsData"][j]; + rawDSInfoRecordsContainer.allFormats.set(readRec["format"], 'true'); + } + } + return rawDSInfoRecordsContainer; +} + +function setGlobalReportSummary(processedApps) { + let totalEstimatedApps = 0; + let recommendedCnt = 0; + let tlcCount = 0; + let totalDurations = 0; + let totalSqlDataframeTaskDuration = 0; + // only count apps that are recommended + let totalSpeedUpDurations = 0; + for (let i in processedApps) { + // check if completedTime is estimated + if (processedApps[i]["estimated"]) { + totalEstimatedApps += 1; + } + totalDurations += processedApps[i].appDuration; + totalSqlDataframeTaskDuration += processedApps[i].sqlDataframeTaskDuration; + // check if the app is recommended or needs more information + let recommendedGroup = recommendationsMap.get(processedApps[i]["gpuCategory"]) + if (recommendedGroup.id < "C") { + // this is a recommended app + // aggregate for GPU recommendation box + recommendedCnt += 1; + totalSpeedUpDurations += processedApps[i]["speedupDuration"] + } else { + if (recommendedGroup.id === "D") { + tlcCount += 1; + } + } + + } + let estimatedPercentage = 0.0; + let gpuPercent = 0.0; + let tlcPercent = 0.0; + let speedUpPercent = 0.0; + + if (processedApps.length != 0) { + // calculate percentage of estimatedEndTime; + estimatedPercentage = (totalEstimatedApps * 100.0) / processedApps.length; + // calculate percentage of recommended GPUs + gpuPercent = (100.0 * recommendedCnt) / processedApps.length; + // percent of apps missing information + tlcPercent = (100.0 * tlcCount) / processedApps.length; + speedUpPercent = (100.0 * totalSpeedUpDurations) / totalSqlDataframeTaskDuration; + } + qualReportSummary.totalApps.numeric = processedApps.length; + qualReportSummary.totalApps.totalAppsDurations = formatDuration(totalDurations); + // speedups + qualReportSummary.speedups.numeric = + formatDuration(totalSpeedUpDurations); + qualReportSummary.speedups.totalSqlDataframeTaskDuration = + formatDuration(totalSqlDataframeTaskDuration); + qualReportSummary.speedups.statsPercentage = twoDecimalFormatter.format(speedUpPercent) + + qualReportSummary.speedups.statsPercentage; + + // candidates + qualReportSummary.candidates.numeric = recommendedCnt; + qualReportSummary.tlc.numeric = tlcCount; + qualReportSummary.totalApps.statsPercentage = + twoDecimalFormatter.format(estimatedPercentage) + + qualReportSummary.totalApps.statsPercentage; + qualReportSummary.candidates.statsPercentage = + twoDecimalFormatter.format(gpuPercent) + + qualReportSummary.candidates.statsPercentage; + qualReportSummary.tlc.statsPercentage = + twoDecimalFormatter.format(tlcPercent) + + qualReportSummary.tlc.statsPercentage; +} + +function setupNavigation() { + $(".dash-nav-dropdown-toggle").click(function () { + $(this).closest(".dash-nav-dropdown") + .toggleClass("show") + .find(".dash-nav-dropdown") + .removeClass("show"); + + $(this).parent() + .siblings() + .removeClass("show"); + }); + + $(".menu-toggle").click(function () { + $(".dash").toggleClass("dash-compact"); + }); + +} diff --git a/tools/src/main/scala/com/nvidia/spark/rapids/tool/qualification/QualOutputWriter.scala b/tools/src/main/scala/com/nvidia/spark/rapids/tool/qualification/QualOutputWriter.scala index d5494efb527..aa49da200d3 100644 --- a/tools/src/main/scala/com/nvidia/spark/rapids/tool/qualification/QualOutputWriter.scala +++ b/tools/src/main/scala/com/nvidia/spark/rapids/tool/qualification/QualOutputWriter.scala @@ -22,6 +22,7 @@ import com.nvidia.spark.rapids.tool.ToolTextFileWriter import com.nvidia.spark.rapids.tool.profiling.ProfileUtils.replaceDelimiter import org.apache.spark.sql.rapids.tool.qualification.QualificationSummaryInfo +import org.apache.spark.sql.rapids.tool.ui.QualificationReportGenerator /** * This class handles the output files for qualification. @@ -31,7 +32,8 @@ import org.apache.spark.sql.rapids.tool.qualification.QualificationSummaryInfo * @param reportReadSchema Whether to include the read data source schema in csv output * @param printStdout Indicates if the summary report should be printed to stdout as well */ -class QualOutputWriter(outputDir: String, reportReadSchema: Boolean, printStdout: Boolean) { +class QualOutputWriter(outputDir: String, reportReadSchema: Boolean, printStdout: Boolean, + uiEnabled: Boolean) { // a file extension will be added to this later private val logFileName = "rapids_4_spark_qualification_output" @@ -57,6 +59,13 @@ class QualOutputWriter(outputDir: String, reportReadSchema: Boolean, printStdout } } + def writeDetailedReport(sums: Seq[QualificationSummaryInfo]): Unit = { + writeCSV(sums) + if (uiEnabled) { + QualificationReportGenerator.generateDashBoard(outputDir, sums) + } + } + // write the text summary report def writeReport(summaries: Seq[QualificationSummaryInfo], numOutputRows: Int) : Unit = { val textFileWriter = new ToolTextFileWriter(outputDir, s"${logFileName}.log", diff --git a/tools/src/main/scala/com/nvidia/spark/rapids/tool/qualification/Qualification.scala b/tools/src/main/scala/com/nvidia/spark/rapids/tool/qualification/Qualification.scala index 053d9b3f879..1a9ca07498d 100644 --- a/tools/src/main/scala/com/nvidia/spark/rapids/tool/qualification/Qualification.scala +++ b/tools/src/main/scala/com/nvidia/spark/rapids/tool/qualification/Qualification.scala @@ -34,7 +34,7 @@ import org.apache.spark.sql.rapids.tool.qualification._ class Qualification(outputDir: String, numRows: Int, hadoopConf: Configuration, timeout: Option[Long], nThreads: Int, order: String, pluginTypeChecker: PluginTypeChecker, readScorePercent: Int, - reportReadSchema: Boolean, printStdout: Boolean) extends Logging { + reportReadSchema: Boolean, printStdout: Boolean, uiEnabled: Boolean) extends Logging { private val allApps = new ConcurrentLinkedQueue[QualificationSummaryInfo]() // default is 24 hours @@ -73,8 +73,9 @@ class Qualification(outputDir: String, numRows: Int, hadoopConf: Configuration, val sortedDesc = allAppsSum.sortBy(sum => { (-sum.score, -sum.sqlDataFrameDuration, -sum.appDuration) }) - val qWriter = new QualOutputWriter(getReportOutputPath, reportReadSchema, printStdout) - qWriter.writeCSV(sortedDesc) + val qWriter = new QualOutputWriter(getReportOutputPath, reportReadSchema, printStdout, + uiEnabled) + qWriter.writeDetailedReport(sortedDesc) val sortedForReport = if (QualificationArgs.isOrderAsc(order)) { allAppsSum.sortBy(sum => { diff --git a/tools/src/main/scala/com/nvidia/spark/rapids/tool/qualification/QualificationArgs.scala b/tools/src/main/scala/com/nvidia/spark/rapids/tool/qualification/QualificationArgs.scala index 80a64c2230b..aa1137b314c 100644 --- a/tools/src/main/scala/com/nvidia/spark/rapids/tool/qualification/QualificationArgs.scala +++ b/tools/src/main/scala/com/nvidia/spark/rapids/tool/qualification/QualificationArgs.scala @@ -130,6 +130,10 @@ Usage: java -cp rapids-4-spark-tools_2.12-.jar:$SPARK_HOME/jars/* val userName: ScallopOption[String] = opt[String](required = false, descr = "Applications which a particular user has submitted." ) + val uiEnabled: ScallopOption[Boolean] = + opt[Boolean](required = false, + descr = "Whether to render the report into HTML pages. Default is false", + default = Some(QualificationArgs.DEFAULT_UI_ENABLED)) validate(order) { case o if (QualificationArgs.isOrderAsc(o) || QualificationArgs.isOrderDesc(o)) => Right(Unit) @@ -165,6 +169,7 @@ Usage: java -cp rapids-4-spark-tools_2.12-.jar:$SPARK_HOME/jars/* object QualificationArgs { val DEFAULT_READ_SCORE_PERCENT = 20 + val DEFAULT_UI_ENABLED = false def isOrderAsc(order: String): Boolean = { order.toLowerCase.startsWith("asc") diff --git a/tools/src/main/scala/com/nvidia/spark/rapids/tool/qualification/QualificationMain.scala b/tools/src/main/scala/com/nvidia/spark/rapids/tool/qualification/QualificationMain.scala index 1aa09e047bd..3b871990f93 100644 --- a/tools/src/main/scala/com/nvidia/spark/rapids/tool/qualification/QualificationMain.scala +++ b/tools/src/main/scala/com/nvidia/spark/rapids/tool/qualification/QualificationMain.scala @@ -54,6 +54,7 @@ object QualificationMain extends Logging { val readScorePercent = appArgs.readScorePercent.getOrElse(20) val reportReadSchema = appArgs.reportReadSchema.getOrElse(false) val order = appArgs.order.getOrElse("desc") + val uiEnabled = appArgs.uiEnabled.getOrElse(false) val hadoopConf = new Configuration() @@ -86,7 +87,8 @@ object QualificationMain extends Logging { } val qual = new Qualification(outputDirectory, numOutputRows, hadoopConf, timeout, - nThreads, order, pluginTypeChecker, readScorePercent, reportReadSchema, printStdout) + nThreads, order, pluginTypeChecker, readScorePercent, reportReadSchema, printStdout, + uiEnabled) val res = qual.qualifyApps(filteredLogs) (0, res) } diff --git a/tools/src/main/scala/org/apache/spark/sql/rapids/tool/qualification/QualificationAppInfo.scala b/tools/src/main/scala/org/apache/spark/sql/rapids/tool/qualification/QualificationAppInfo.scala index f6359ea2577..55fcac3fc91 100644 --- a/tools/src/main/scala/org/apache/spark/sql/rapids/tool/qualification/QualificationAppInfo.scala +++ b/tools/src/main/scala/org/apache/spark/sql/rapids/tool/qualification/QualificationAppInfo.scala @@ -378,7 +378,7 @@ class QualificationAppInfo( readScoreHumanPercentRounded, notSupportFormatAndTypesString, getAllReadFileFormats, writeFormat, allComplexTypes, nestedComplexTypes, longestSQLDuration, nonSQLDuration, estimatedDuration, unsupportedDuration, - speedupDuration, speedupFactor, totalSpeedup, speedupBucket) + speedupDuration, speedupFactor, totalSpeedup, speedupBucket, info.sparkUser, info.startTime) } } @@ -464,7 +464,9 @@ case class QualificationSummaryInfo( speedupDuration: Long, speedupFactor: Double, totalSpeedup: Double, - speedupBucket: String) + speedupBucket: String, + user: String, + startTime: Long) object QualificationAppInfo extends Logging { def createApp( diff --git a/tools/src/main/scala/org/apache/spark/sql/rapids/tool/ui/QualificationReportGenerator.scala b/tools/src/main/scala/org/apache/spark/sql/rapids/tool/ui/QualificationReportGenerator.scala new file mode 100644 index 00000000000..3c1d2bb009e --- /dev/null +++ b/tools/src/main/scala/org/apache/spark/sql/rapids/tool/ui/QualificationReportGenerator.scala @@ -0,0 +1,169 @@ +/* + * 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 org.apache.spark.sql.rapids.tool.ui + +import java.nio.file +import java.nio.file.{Files, FileSystems, Paths} + +import scala.collection.JavaConverters.mapAsJavaMapConverter + +import org.apache.hadoop.conf.Configuration +import org.apache.hadoop.fs.{FileSystem, FSDataOutputStream, Path} +import org.json4s.DefaultFormats +import org.json4s.jackson.Serialization + +import org.apache.spark.internal.Logging +import org.apache.spark.sql.rapids.tool.qualification.QualificationSummaryInfo +import org.apache.spark.util.Utils + +class QualificationReportGenerator(outputDir: String, + sumArr: Seq[QualificationSummaryInfo]) extends Logging { + + import QualificationReportGenerator._ + implicit val formats = DefaultFormats + + val outputWorkPath = new Path(outputDir) + val fs = Some(FileSystem.get(outputWorkPath.toUri, new Configuration())) + + def launch(): Unit = { + val uiRootPath = getPathForResource(RAPIDS_UI_ASSETS_DIR) + logDebug(s"Generating UI files into... ${outputWorkPath.toUri}") + copyAssetFolderRecursively(uiRootPath, outputWorkPath) + } + + def copyAssetFolderRecursively(srcFolderPath: java.nio.file.Path, dstPath: Path): Unit = { + logDebug(s"UI code generator: Copying ... ${srcFolderPath.toUri}") + if (Files.isDirectory(srcFolderPath)) { + val destinationPath = new Path(dstPath, srcFolderPath.getFileName.toString) + fs.map { dstFileSys => + dstFileSys.mkdirs(destinationPath) + Files.list(srcFolderPath).forEach { childPath => + if (Files.isDirectory(childPath)) { + copyAssetFolderRecursively(childPath, destinationPath) + } else { + tryCopyAssetFile(childPath, new Path(destinationPath, childPath.getFileName.toString)) + } + } + } + } + } + + def generateJSFiles(): Unit = { + // Serializing the entire list of sums may stress the memory. + // Serializing one record at a time is slower but it would reduce the memory peak consumption. + val outputPath = new Path(outputWorkPath, RAPIDS_UI_JS_DATA) + val mainIndexPath =new Path(outputWorkPath, RAPIDS_UI_INDEX_PATH) + logInfo(s"Generating UI data in ${mainIndexPath.toUri}") + val fileHeader = + s""" + |let qualificationRecords = [ + """.stripMargin + val fileFooter = + s"""|]; + """.stripMargin + fs.foreach { dfs => + val outFile = dfs.create(outputPath) + Utils.tryWithSafeFinally { + outFile.writeBytes(fileHeader) + if (sumArr.nonEmpty) { + if (sumArr.size > 1) { + for (ind <- 0.until(sumArr.size - 1)) { + writeAppRecord(sumArr(ind), outFile) + } + } + writeAppRecord(sumArr.last, outFile, "") + } + outFile.writeBytes(fileFooter) + } { + outFile.flush() + outFile.close() + } + } + } + + private def writeAppRecord(appRec: QualificationSummaryInfo, + outStream: FSDataOutputStream, sep: String =","): Unit = { + val sumRec = + s"""|\t${Serialization.write(appRec)}$sep + """.stripMargin + outStream.writeBytes(sumRec) + } + + def tryCopyAssetFile(srcFilePath: java.nio.file.Path, dstPath: Path) : Unit = { + logDebug(s"Copying UI assets: ${srcFilePath.toUri.toString} to ${dstPath.toUri.toString}") + fs.foreach { dstFileSys => + Utils.tryWithResource(Files.newInputStream(srcFilePath)) { in => + val out = dstFileSys.create(dstPath) + Utils.tryWithSafeFinally { + val buffer = new Array[Byte](130 * 1024) + Iterator.continually(in.read(buffer)).takeWhile(_ != -1).foreach { bCount => + out.write(buffer, 0, bCount) + } + } { + out.flush() + out.close() + } + } + } + } + + def close(): Unit = { + jarFS.foreach { jFS => + jFS.close() + } + } +} + +object QualificationReportGenerator extends Logging { + val RAPIDS_UI_ASSETS_DIR = "/ui" + val RAPIDS_UI_JS_DATA = s"ui/js/data-output.js" + val RAPIDS_UI_INDEX_PATH = s"ui/html/index.html" + var jarFS : Option[file.FileSystem] = None + + private def getPathForResource(filename: String): java.nio.file.Path = { + val url = getClass.getResource(filename) + if (url.getPath.contains("jar")) { // this is a jar resource + val jFs = jarFS.getOrElse(setJarFileSystem(filename)) + jFs.getPath(filename) + } else { + Paths.get(url.toURI) + } + } + + private def setJarFileSystem(fileName: String): file.FileSystem = { + val jFileSys = FileSystems.newFileSystem(getClass.getResource(fileName).toURI, + Map[String, String]().asJava) + jarFS = Some(jFileSys) + jFileSys + } + + def generateDashBoard(outDir: String, sumArr: Seq[QualificationSummaryInfo]) : Unit = { + val generatorOp = Some(new QualificationReportGenerator(outDir, sumArr)) + var startTime = 0L; + generatorOp.foreach { generator => + Utils.tryWithSafeFinally { + startTime = System.currentTimeMillis() + generator.launch() + generator.generateJSFiles() + } { + generator.close() + val endTime = System.currentTimeMillis() + logInfo(s"Took ${endTime - startTime}ms to process ") + } + } + } +}