You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by wi...@apache.org on 2017/10/09 10:29:11 UTC

[couchdb-fauxton] branch master updated: Display Mango execution statistics, when available (#993)

This is an automated email from the ASF dual-hosted git repository.

willholley pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/couchdb-fauxton.git


The following commit(s) were added to refs/heads/master by this push:
     new 15ffaf2  Display Mango execution statistics, when available (#993)
15ffaf2 is described below

commit 15ffaf2120dae5c0afa083740aa02a4b9fa13e1d
Author: Will Holley <wi...@gmail.com>
AuthorDate: Mon Oct 9 11:29:08 2017 +0100

    Display Mango execution statistics, when available (#993)
    
    Display execution statistics for Query
    
    When running a Mango Query, display the execution stats and any
    warnings returned. The query execution time and presence of
    a warning is indicated in the query editor after a query
    has been run. An overlay popup displays the detailed
    execution statistics and warning text.
    
    Not all versions of CouchDB support execution statistics for Mango,
    so detect that the feature is supported before requesting them
    at query time.
    
    In the case that execution statistics are not available, we still
    display any warnings that are returned.
---
 .../documents/assets/less/index-results.less       |  27 +++++
 app/addons/documents/index-results/actions/base.js |   6 +-
 .../documents/index-results/actions/fetch.js       |   4 +-
 app/addons/documents/index-results/reducers.js     |   4 +-
 .../documents/mango/__tests__/mango.api.test.js    |   2 +-
 .../documents/mango/components/ExecutionStats.js   | 127 +++++++++++++++++++++
 .../documents/mango/components/MangoQueryEditor.js |   4 +
 .../mango/components/MangoQueryEditorContainer.js  |   5 +-
 app/addons/documents/mango/mango.api.js            |  59 ++++++++--
 9 files changed, 218 insertions(+), 20 deletions(-)

diff --git a/app/addons/documents/assets/less/index-results.less b/app/addons/documents/assets/less/index-results.less
index 51f296f..ead6b89 100644
--- a/app/addons/documents/assets/less/index-results.less
+++ b/app/addons/documents/assets/less/index-results.less
@@ -194,3 +194,30 @@ a.document-result-screen__toolbar-create-btn:visited {
     width: 300px;
   }
 }
+
+.execution-stats-popup {
+  font-size: 14px;
+  .execution-stats-popup-component {
+    [data-status="false"] {
+      color: @fontGrey;
+    }
+    [data-status="true"] {
+      .value {
+        font-weight: bold;
+      }
+    }
+  }
+  .warning {
+    color: @errorAlertColor;
+    margin-top: 8px;
+  }
+}
+
+.execution-stats {
+  font-size: 13px;
+  padding: 8px 8px 8px 0;
+
+  .fonticon-attention-circled {
+    margin-right: 4px;
+  }
+}
\ No newline at end of file
diff --git a/app/addons/documents/index-results/actions/base.js b/app/addons/documents/index-results/actions/base.js
index 7a0d2fd..6fac0ff 100644
--- a/app/addons/documents/index-results/actions/base.js
+++ b/app/addons/documents/index-results/actions/base.js
@@ -25,13 +25,15 @@ export const resetState = () => {
   };
 };
 
-export const newResultsAvailable = (docs, params, canShowNext, docType) => {
+export const newResultsAvailable = (docs, params, canShowNext, docType, executionStats, warning) => {
   return {
     type: ActionTypes.INDEX_RESULTS_REDUX_NEW_RESULTS,
     docs: docs,
     params: params,
     canShowNext: canShowNext,
-    docType: docType
+    docType: docType,
+    executionStats: executionStats,
+    warning: warning
   };
 };
 
diff --git a/app/addons/documents/index-results/actions/fetch.js b/app/addons/documents/index-results/actions/fetch.js
index 973c64c..2521a36 100644
--- a/app/addons/documents/index-results/actions/fetch.js
+++ b/app/addons/documents/index-results/actions/fetch.js
@@ -91,7 +91,7 @@ export const fetchDocs = (queryDocs, fetchParams, queryOptionsParams) => {
     dispatch(nowLoading());
 
     // now fetch the results
-    return queryDocs(params).then(({ docs, docType }) => {
+    return queryDocs(params).then(({ docs, docType, executionStats, warning }) => {
       const {
         finalDocList,
         canShowNext
@@ -101,7 +101,7 @@ export const fetchDocs = (queryDocs, fetchParams, queryOptionsParams) => {
         dispatch(changeLayout(Constants.LAYOUT_ORIENTATION.JSON));
       }
       // dispatch that we're all done
-      dispatch(newResultsAvailable(finalDocList, params, canShowNext, docType));
+      dispatch(newResultsAvailable(finalDocList, params, canShowNext, docType, executionStats, warning));
     }).catch((error) => {
       FauxtonAPI.addNotification({
         msg: 'Error running query. ' + errorReason(error),
diff --git a/app/addons/documents/index-results/reducers.js b/app/addons/documents/index-results/reducers.js
index c492127..cfa73d5 100644
--- a/app/addons/documents/index-results/reducers.js
+++ b/app/addons/documents/index-results/reducers.js
@@ -105,7 +105,9 @@ export default function resultsState(state = initialState, action) {
           canShowNext: action.canShowNext
         }),
         docType: action.docType,
-        selectedLayout: selectedLayout
+        selectedLayout: selectedLayout,
+        executionStats: action.executionStats,
+        warning: action.warning
       });
 
     case ActionTypes.INDEX_RESULTS_REDUX_CHANGE_LAYOUT:
diff --git a/app/addons/documents/mango/__tests__/mango.api.test.js b/app/addons/documents/mango/__tests__/mango.api.test.js
index 523757d..0836633 100644
--- a/app/addons/documents/mango/__tests__/mango.api.test.js
+++ b/app/addons/documents/mango/__tests__/mango.api.test.js
@@ -34,7 +34,7 @@ describe('Mango API', () => {
 
   describe('mangoQueryDocs', () => {
     it('returns document type INDEX_RESULTS_DOC_TYPE.MANGO_QUERY', (done) => {
-      fetchMock.once("*", {});
+      fetchMock.mock("*", { times: 2 });
       MangoAPI.mangoQueryDocs('myDB', {}, {}).then((res) => {
         assert.equal(res.docType, Constants.INDEX_RESULTS_DOC_TYPE.MANGO_QUERY);
         done();
diff --git a/app/addons/documents/mango/components/ExecutionStats.js b/app/addons/documents/mango/components/ExecutionStats.js
new file mode 100644
index 0000000..503358d
--- /dev/null
+++ b/app/addons/documents/mango/components/ExecutionStats.js
@@ -0,0 +1,127 @@
+// 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.
+import React from 'react';
+import { Popover, OverlayTrigger } from 'react-bootstrap';
+
+const TOO_MANY_DOCS_SCANNED_WARNING = "The number of documents examined is high in proportion to the number of results returned. Consider adding an index to improve this.";
+
+export default class ExecutionStats extends React.Component {
+  constructor (props) {
+    super(props);
+  }
+
+  humanizeDuration(milliseconds) {
+    if (milliseconds < 1000) {
+      return Math.round(milliseconds) + ' ms';
+    }
+    let seconds = milliseconds / 1000;
+    if (seconds < 60) {
+      return Math.floor(seconds) + ' seconds';
+    }
+    const minutes = Math.floor(seconds / 60);
+    seconds = seconds - (minutes * 60);
+
+    return minutes + 'minute, ' + seconds + 'seconds';
+  }
+
+  getWarning(executionStats, warning) {
+    if (!executionStats) { return warning; }
+
+    // warn if many documents scanned in relation to results returned
+    if (!warning && executionStats.results_returned) {
+      const docsExamined = executionStats.total_docs_examined || executionStats.total_quorum_docs_examined;
+      if (docsExamined / executionStats.results_returned > 10) {
+        return TOO_MANY_DOCS_SCANNED_WARNING;
+      }
+    }
+
+    return warning;
+  }
+
+  warningPopupComponent(warningText) {
+    if (!!warningText) {
+      return (<div className="warning">
+        <i className="fonticon-attention-circled"></i> {warningText}
+      </div>);
+    }
+  }
+
+  executionStatsLine(title, value, alwaysShow = false, units = "") {
+    const hasValue = value === 0 && !alwaysShow ? "false" : "true";
+    return <div data-status={hasValue}>{title + ": "}<span className="value">{value.toLocaleString()} {units}</span></div>;
+  }
+
+  executionStatsPopupComponent(executionStats) {
+    if (!executionStats) return null;
+    return (
+      <div className="execution-stats-popup-component">
+        {/* keys examined always 0 so hide it for now */}
+        {/* {this.executionStatsLine("keys examined", executionStats.total_keys_examined)} */}
+        {this.executionStatsLine("documents examined", executionStats.total_docs_examined)}
+        {this.executionStatsLine("documents examined (quorum)", executionStats.total_quorum_docs_examined)}
+        {this.executionStatsLine("results returned", executionStats.results_returned, true)}
+        {this.executionStatsLine("execution time", executionStats.execution_time_ms, false, "ms")}
+      </div>
+    );
+  }
+
+  popup(executionStats, warningText) {
+    return (
+      <Popover id="popover-execution-stats" title="Execution Statistics">
+        <div className="execution-stats-popup">
+          {this.executionStatsPopupComponent(executionStats)}
+          {this.warningPopupComponent(warningText)}
+        </div>
+      </Popover>
+    );
+  }
+
+  render() {
+    const {
+      executionStats,
+      warning
+    } = this.props;
+
+    const warningText = this.getWarning(executionStats, warning);
+
+    let warningComponent = null;
+    if (!!warningText) {
+      warningComponent = <i className="fonticon-attention-circled"></i>;
+    }
+
+    let executionStatsComponent = null;
+    if (executionStats) {
+      executionStatsComponent = (
+        <span className="execution-stats-component">Executed in {this.humanizeDuration(executionStats.execution_time_ms)}</span>
+      );
+    } else if (!!warningText) {
+      executionStatsComponent = (
+        <span className="execution-stats-component">Warning</span>
+      );
+    }
+
+    const popup = this.popup(executionStats, warningText);
+    return (
+        <OverlayTrigger trigger={['hover', 'focus', 'click']} placement="right" overlay={popup}>
+          <span className="execution-stats">
+            {warningComponent}
+            {executionStatsComponent}
+          </span>
+        </OverlayTrigger>
+    );
+  }
+};
+
+ExecutionStats.propTypes = {
+  executionStats: React.PropTypes.object,
+  warning: React.PropTypes.string
+};
diff --git a/app/addons/documents/mango/components/MangoQueryEditor.js b/app/addons/documents/mango/components/MangoQueryEditor.js
index c37008f..88e9a2f 100644
--- a/app/addons/documents/mango/components/MangoQueryEditor.js
+++ b/app/addons/documents/mango/components/MangoQueryEditor.js
@@ -18,6 +18,7 @@ import "../../../../../assets/js/plugins/prettify";
 import app from "../../../../app";
 import FauxtonAPI from "../../../../core/api";
 import ReactComponents from "../../../components/react-components";
+import ExecutionStats from './ExecutionStats';
 
 const PaddedBorderedBox = ReactComponents.PaddedBorderedBox;
 const CodeEditorPanel = ReactComponents.CodeEditorPanel;
@@ -91,6 +92,9 @@ export default class MangoQueryEditor extends Component {
                 onClick={(ev) => {this.runExplain(ev);} }>Explain</button>
               <a className="edit-link" style={{} } onClick={(ev) => {this.manageIndexes(ev);}}>manage indexes</a>
             </div>
+            <div>
+              <ExecutionStats {...this.props} />
+            </div>
           </div>
         </form>
       </div>
diff --git a/app/addons/documents/mango/components/MangoQueryEditorContainer.js b/app/addons/documents/mango/components/MangoQueryEditorContainer.js
index c9ad1d0..6df41bd 100644
--- a/app/addons/documents/mango/components/MangoQueryEditorContainer.js
+++ b/app/addons/documents/mango/components/MangoQueryEditorContainer.js
@@ -57,8 +57,9 @@ const mapStateToProps = (state, ownProps) => {
     description: ownProps.description,
     editorTitle: ownProps.editorTitle,
     additionalIndexesText: ownProps.additionalIndexesText,
-
-    fetchParams: indexResults.fetchParams
+    fetchParams: indexResults.fetchParams,
+    executionStats: indexResults.executionStats,
+    warning: indexResults.warning
   };
 };
 
diff --git a/app/addons/documents/mango/mango.api.js b/app/addons/documents/mango/mango.api.js
index 823bbe6..5d4353c 100644
--- a/app/addons/documents/mango/mango.api.js
+++ b/app/addons/documents/mango/mango.api.js
@@ -84,6 +84,27 @@ export const fetchIndexes = (databaseName, params) => {
     });
 };
 
+// assume all databases being accessed are on the same
+// host / CouchDB version
+let supportsExecutionStatsCache = null;
+const supportsExecutionStats = (databaseName) => {
+  if (supportsExecutionStatsCache === null) {
+    return new FauxtonAPI.Promise((resolve) => {
+      mangoQuery(databaseName, {
+        selector: {
+          "_id": {"$gt": "a" }
+       },
+        execution_stats: true
+      }, {limit: 1})
+      .then(resp => {
+        supportsExecutionStatsCache = resp.status == 200;
+        resolve(supportsExecutionStatsCache);
+      });
+    });
+  }
+  return Promise.resolve(supportsExecutionStatsCache);
+};
+
 // Determines what params need to be sent to couch based on the Mango query entered
 // by the user and what fauxton is using to emulate pagination (fetchParams).
 export const mergeFetchParams = (queryCode, fetchParams) => {
@@ -104,9 +125,10 @@ export const mergeFetchParams = (queryCode, fetchParams) => {
   };
 };
 
-export const mangoQueryDocs = (databaseName, queryCode, fetchParams) => {
+export const mangoQuery = (databaseName, queryCode, fetchParams) => {
   const url = FauxtonAPI.urls('mango', 'query-server', encodeURIComponent(databaseName));
   const modifiedQuery = mergeFetchParams(queryCode, fetchParams);
+
   return fetch(url, {
     headers: {
       'Accept': 'application/json',
@@ -115,15 +137,28 @@ export const mangoQueryDocs = (databaseName, queryCode, fetchParams) => {
     credentials: 'include',
     method: 'POST',
     body: JSON.stringify(modifiedQuery)
-  })
-    .then((res) => res.json())
-    .then((json) => {
-      if (json.error) {
-        throw new Error('(' + json.error + ') ' + json.reason);
-      }
-      return {
-        docs: json.docs,
-        docType: Constants.INDEX_RESULTS_DOC_TYPE.MANGO_QUERY
-      };
-    });
+  });
+};
+
+export const mangoQueryDocs = (databaseName, queryCode, fetchParams) => {
+  // we can only add the execution_stats field if it is supported by the server
+  // otherwise Couch throws an error
+  return supportsExecutionStats(databaseName).then((shouldFetchExecutionStats) => {
+    if (shouldFetchExecutionStats) {
+      queryCode.execution_stats = true;
+    }
+    return mangoQuery(databaseName, queryCode, fetchParams)
+      .then((res) => res.json())
+      .then((json) => {
+        if (json.error) {
+          throw new Error('(' + json.error + ') ' + json.reason);
+        }
+        return {
+          docs: json.docs,
+          docType: Constants.INDEX_RESULTS_DOC_TYPE.MANGO_QUERY,
+          executionStats: json.execution_stats,
+          warning: json.warning
+        };
+      });
+  });
 };

-- 
To stop receiving notification emails like this one, please contact
['"commits@couchdb.apache.org" <co...@couchdb.apache.org>'].