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

vogievetsky pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/druid.git


The following commit(s) were added to refs/heads/master by this push:
     new 9658e1ad7fb Web console: fix query timer issues (#16235)
9658e1ad7fb is described below

commit 9658e1ad7fb413a53834cebf6b51e1af04ec74d5
Author: Vadim Ogievetsky <[email protected]>
AuthorDate: Thu Apr 4 13:13:31 2024 -0700

    Web console: fix query timer issues (#16235)
    
    * fix timer issues
    
    * wording
---
 .../src/singletons/ace-editor-state-cache.ts       | 10 ++--
 .../src/singletons/execution-state-cache.ts        |  8 +--
 .../src/singletons/workbench-running-promises.ts   |  9 ++--
 web-console/src/utils/druid-query.ts               |  1 +
 .../execution-summary-panel.tsx                    | 13 ++++-
 .../execution-timer-panel.spec.tsx.snap            | 60 +++++++++++++++++++++-
 .../execution-timer-panel.spec.tsx                 | 10 +++-
 .../execution-timer-panel.tsx                      |  5 +-
 .../views/workbench-view/query-tab/query-tab.tsx   | 27 ++++++----
 9 files changed, 115 insertions(+), 28 deletions(-)

diff --git a/web-console/src/singletons/ace-editor-state-cache.ts 
b/web-console/src/singletons/ace-editor-state-cache.ts
index 9e1b73aaba8..62a440ad52d 100644
--- a/web-console/src/singletons/ace-editor-state-cache.ts
+++ b/web-console/src/singletons/ace-editor-state-cache.ts
@@ -23,24 +23,24 @@ interface EditorState {
 }
 
 export class AceEditorStateCache {
-  static states: Record<string, EditorState> = {};
+  static states = new Map<string, EditorState>();
 
   static saveState(id: string, editor: Ace.Editor): void {
     const session = editor.getSession();
     const undoManager: any = session.getUndoManager();
-    AceEditorStateCache.states[id] = {
+    AceEditorStateCache.states.set(id, {
       undoManager,
-    };
+    });
   }
 
   static applyState(id: string, editor: Ace.Editor): void {
-    const state = AceEditorStateCache.states[id];
+    const state = AceEditorStateCache.states.get(id);
     if (!state) return;
     const session = editor.getSession();
     session.setUndoManager(state.undoManager);
   }
 
   static deleteState(id: string): void {
-    delete AceEditorStateCache.states[id];
+    AceEditorStateCache.states.delete(id);
   }
 }
diff --git a/web-console/src/singletons/execution-state-cache.ts 
b/web-console/src/singletons/execution-state-cache.ts
index ef1b2d60aeb..83773ede86d 100644
--- a/web-console/src/singletons/execution-state-cache.ts
+++ b/web-console/src/singletons/execution-state-cache.ts
@@ -20,17 +20,17 @@ import type { Execution } from '../druid-models';
 import type { DruidError, QueryState } from '../utils';
 
 export class ExecutionStateCache {
-  private static readonly cache: Record<string, QueryState<Execution, 
DruidError, Execution>> = {};
+  private static readonly cache = new Map<string, QueryState<Execution, 
DruidError, Execution>>();
 
   static storeState(id: string, report: QueryState<Execution, DruidError, 
Execution>): void {
-    ExecutionStateCache.cache[id] = report;
+    ExecutionStateCache.cache.set(id, report);
   }
 
   static getState(id: string): QueryState<Execution, DruidError, Execution> | 
undefined {
-    return ExecutionStateCache.cache[id];
+    return ExecutionStateCache.cache.get(id);
   }
 
   static deleteState(id: string): void {
-    delete ExecutionStateCache.cache[id];
+    ExecutionStateCache.cache.delete(id);
   }
 }
diff --git a/web-console/src/singletons/workbench-running-promises.ts 
b/web-console/src/singletons/workbench-running-promises.ts
index 5a14ec136b7..996070537d3 100644
--- a/web-console/src/singletons/workbench-running-promises.ts
+++ b/web-console/src/singletons/workbench-running-promises.ts
@@ -21,24 +21,25 @@ import type { QueryResult } from '@druid-toolkit/query';
 export interface WorkbenchRunningPromise {
   promise: Promise<QueryResult>;
   prefixLines: number;
+  startTime: Date;
 }
 
 export class WorkbenchRunningPromises {
-  private static readonly promises: Record<string, WorkbenchRunningPromise> = 
{};
+  private static readonly promises = new Map<string, 
WorkbenchRunningPromise>();
 
   static isWorkbenchRunningPromise(x: any): x is WorkbenchRunningPromise {
     return Boolean(x.promise);
   }
 
   static storePromise(id: string, promise: WorkbenchRunningPromise): void {
-    WorkbenchRunningPromises.promises[id] = promise;
+    WorkbenchRunningPromises.promises.set(id, promise);
   }
 
   static getPromise(id: string): WorkbenchRunningPromise | undefined {
-    return WorkbenchRunningPromises.promises[id];
+    return WorkbenchRunningPromises.promises.get(id);
   }
 
   static deletePromise(id: string): void {
-    delete WorkbenchRunningPromises.promises[id];
+    WorkbenchRunningPromises.promises.delete(id);
   }
 }
diff --git a/web-console/src/utils/druid-query.ts 
b/web-console/src/utils/druid-query.ts
index 6040950d110..c94bfca3d1c 100644
--- a/web-console/src/utils/druid-query.ts
+++ b/web-console/src/utils/druid-query.ts
@@ -267,6 +267,7 @@ export class DruidError extends Error {
   public startRowColumn?: RowColumn;
   public endRowColumn?: RowColumn;
   public suggestion?: QuerySuggestion;
+  public queryDuration?: number;
 
   // Deprecated
   public error?: string;
diff --git 
a/web-console/src/views/workbench-view/execution-summary-panel/execution-summary-panel.tsx
 
b/web-console/src/views/workbench-view/execution-summary-panel/execution-summary-panel.tsx
index 720711500b6..75999b5ee15 100644
--- 
a/web-console/src/views/workbench-view/execution-summary-panel/execution-summary-panel.tsx
+++ 
b/web-console/src/views/workbench-view/execution-summary-panel/execution-summary-panel.tsx
@@ -36,6 +36,7 @@ import './execution-summary-panel.scss';
 
 export interface ExecutionSummaryPanelProps {
   execution: Execution | undefined;
+  queryErrorDuration: number | undefined;
   onExecutionDetail(): void;
   onReset?: () => void;
 }
@@ -43,12 +44,22 @@ export interface ExecutionSummaryPanelProps {
 export const ExecutionSummaryPanel = React.memo(function ExecutionSummaryPanel(
   props: ExecutionSummaryPanelProps,
 ) {
-  const { execution, onExecutionDetail, onReset } = props;
+  const { execution, queryErrorDuration, onExecutionDetail, onReset } = props;
   const [showDestinationPages, setShowDestinationPages] = useState(false);
   const queryResult = execution?.result;
 
   const buttons: JSX.Element[] = [];
 
+  if (typeof queryErrorDuration === 'number') {
+    buttons.push(
+      <Button
+        key="timing"
+        minimal
+        text={`Error after ${formatDurationHybrid(queryErrorDuration)}`}
+      />,
+    );
+  }
+
   if (queryResult) {
     const wrapQueryLimit = queryResult.getSqlOuterLimit();
     let resultCount: string;
diff --git 
a/web-console/src/views/workbench-view/execution-timer-panel/__snapshots__/execution-timer-panel.spec.tsx.snap
 
b/web-console/src/views/workbench-view/execution-timer-panel/__snapshots__/execution-timer-panel.spec.tsx.snap
index bb0be1741d9..e4af6c079fc 100644
--- 
a/web-console/src/views/workbench-view/execution-timer-panel/__snapshots__/execution-timer-panel.spec.tsx.snap
+++ 
b/web-console/src/views/workbench-view/execution-timer-panel/__snapshots__/execution-timer-panel.spec.tsx.snap
@@ -1,6 +1,6 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`AnchoredQueryTimer matches snapshot 1`] = `
+exports[`AnchoredQueryTimer matches snapshot with execution 1`] = `
 <div
   class="bp4-button-group execution-timer-panel"
 >
@@ -57,3 +57,61 @@ exports[`AnchoredQueryTimer matches snapshot 1`] = `
   </button>
 </div>
 `;
+
+exports[`AnchoredQueryTimer matches snapshot with startTime 1`] = `
+<div
+  class="bp4-button-group execution-timer-panel"
+>
+  <button
+    class="bp4-button bp4-minimal timer"
+    type="button"
+  >
+    <span
+      aria-hidden="true"
+      class="bp4-icon bp4-icon-stopwatch"
+      icon="stopwatch"
+    >
+      <svg
+        data-icon="stopwatch"
+        height="16"
+        role="img"
+        viewBox="0 0 16 16"
+        width="16"
+      >
+        <path
+          d="M9 2v1.083A6.002 6.002 0 018 15 6 6 0 017 3.083V2H6a1 1 0 
110-2h4a1 1 0 010 2H9zM8 5a4 4 0 104 4H8V5z"
+          fill-rule="evenodd"
+        />
+      </svg>
+    </span>
+    <span
+      class="bp4-button-text"
+    >
+      1.00s
+    </span>
+  </button>
+  <button
+    class="bp4-button bp4-minimal"
+    type="button"
+  >
+    <span
+      aria-hidden="true"
+      class="bp4-icon bp4-icon-cross"
+      icon="cross"
+    >
+      <svg
+        data-icon="cross"
+        height="16"
+        role="img"
+        viewBox="0 0 16 16"
+        width="16"
+      >
+        <path
+          d="M9.41 8l3.29-3.29c.19-.18.3-.43.3-.71a1.003 1.003 0 00-1.71-.71L8 
6.59l-3.29-3.3a1.003 1.003 0 00-1.42 1.42L6.59 8 3.3 
11.29c-.19.18-.3.43-.3.71a1.003 1.003 0 001.71.71L8 9.41l3.29 
3.29c.18.19.43.3.71.3a1.003 1.003 0 00.71-1.71L9.41 8z"
+          fill-rule="evenodd"
+        />
+      </svg>
+    </span>
+  </button>
+</div>
+`;
diff --git 
a/web-console/src/views/workbench-view/execution-timer-panel/execution-timer-panel.spec.tsx
 
b/web-console/src/views/workbench-view/execution-timer-panel/execution-timer-panel.spec.tsx
index 7e1013e2cde..deb3edb317e 100644
--- 
a/web-console/src/views/workbench-view/execution-timer-panel/execution-timer-panel.spec.tsx
+++ 
b/web-console/src/views/workbench-view/execution-timer-panel/execution-timer-panel.spec.tsx
@@ -37,13 +37,21 @@ describe('AnchoredQueryTimer', () => {
     jest.restoreAllMocks();
   });
 
-  it('matches snapshot', () => {
+  it('matches snapshot with execution', () => {
     const { container } = render(
       <ExecutionTimerPanel
         execution={new Execution({ engine: 'sql-msq-task', id: 'xxx', 
startTime: new Date(start) })}
+        startTime={undefined}
         onCancel={() => {}}
       />,
     );
     expect(container.firstChild).toMatchSnapshot();
   });
+
+  it('matches snapshot with startTime', () => {
+    const { container } = render(
+      <ExecutionTimerPanel execution={undefined} startTime={new Date(start)} 
onCancel={() => {}} />,
+    );
+    expect(container.firstChild).toMatchSnapshot();
+  });
 });
diff --git 
a/web-console/src/views/workbench-view/execution-timer-panel/execution-timer-panel.tsx
 
b/web-console/src/views/workbench-view/execution-timer-panel/execution-timer-panel.tsx
index 3202240ee2a..ea09913d584 100644
--- 
a/web-console/src/views/workbench-view/execution-timer-panel/execution-timer-panel.tsx
+++ 
b/web-console/src/views/workbench-view/execution-timer-panel/execution-timer-panel.tsx
@@ -29,15 +29,16 @@ import './execution-timer-panel.scss';
 
 export interface ExecutionTimerPanelProps {
   execution: Execution | undefined;
+  startTime: Date | undefined;
   onCancel(): void;
 }
 
 export const ExecutionTimerPanel = React.memo(function ExecutionTimerPanel(
   props: ExecutionTimerPanelProps,
 ) {
-  const { execution, onCancel } = props;
+  const { execution, startTime, onCancel } = props;
   const [showCancelConfirm, setShowCancelConfirm] = useState(false);
-  const [mountTime] = useState(Date.now());
+  const [mountTime] = useState(startTime?.valueOf() ?? Date.now());
   const [currentTime, setCurrentTime] = useState(Date.now());
 
   useInterval(() => {
diff --git a/web-console/src/views/workbench-view/query-tab/query-tab.tsx 
b/web-console/src/views/workbench-view/query-tab/query-tab.tsx
index bd3a7dc769c..69234b58454 100644
--- a/web-console/src/views/workbench-view/query-tab/query-tab.tsx
+++ b/web-console/src/views/workbench-view/query-tab/query-tab.tsx
@@ -170,16 +170,16 @@ export const QueryTab = React.memo(function 
QueryTab(props: QueryTabProps) {
 
   const queryInputRef = useRef<FlexibleQueryInput | null>(null);
 
+  const cachedExecutionState = ExecutionStateCache.getState(id);
+  const currentRunningPromise = WorkbenchRunningPromises.getPromise(id);
   const [executionState, queryManager] = useQueryManager<
     WorkbenchQuery | WorkbenchRunningPromise | LastExecution,
     Execution,
     Execution,
     DruidError
   >({
-    initQuery: ExecutionStateCache.getState(id)
-      ? undefined
-      : WorkbenchRunningPromises.getPromise(id) || query.getLastExecution(),
-    initState: ExecutionStateCache.getState(id),
+    initQuery: cachedExecutionState ? undefined : currentRunningPromise || 
query.getLastExecution(),
+    initState: cachedExecutionState,
     processQuery: async (q, cancelToken) => {
       if (q instanceof WorkbenchQuery) {
         ExecutionStateCache.deleteState(id);
@@ -214,6 +214,7 @@ export const QueryTab = React.memo(function QueryTab(props: 
QueryTabProps) {
 
             onQueryChange(props.query.changeLastExecution(undefined));
 
+            const startTime = new Date();
             let result: QueryResult;
             try {
               const resultPromise = queryRunner.runQuery({
@@ -223,13 +224,19 @@ export const QueryTab = React.memo(function 
QueryTab(props: QueryTabProps) {
                   nativeQueryCancelFnRef.current = cancelFn;
                 }),
               });
-              WorkbenchRunningPromises.storePromise(id, { promise: 
resultPromise, prefixLines });
+              WorkbenchRunningPromises.storePromise(id, {
+                promise: resultPromise,
+                prefixLines,
+                startTime,
+              });
 
               result = await resultPromise;
               nativeQueryCancelFnRef.current = undefined;
             } catch (e) {
               nativeQueryCancelFnRef.current = undefined;
-              throw new DruidError(e, prefixLines);
+              const druidError = new DruidError(e, prefixLines);
+              druidError.queryDuration = Date.now() - startTime.valueOf();
+              throw druidError;
             }
 
             return Execution.fromResult(engine, result);
@@ -240,11 +247,9 @@ export const QueryTab = React.memo(function 
QueryTab(props: QueryTabProps) {
         try {
           result = await q.promise;
         } catch (e) {
-          WorkbenchRunningPromises.deletePromise(id);
           throw new DruidError(e, q.prefixLines);
         }
 
-        WorkbenchRunningPromises.deletePromise(id);
         return Execution.fromResult('sql-native', result);
       } else {
         switch (q.engine) {
@@ -265,9 +270,9 @@ export const QueryTab = React.memo(function QueryTab(props: 
QueryTabProps) {
   });
 
   useEffect(() => {
-    if (!executionState.data) return;
+    if (!executionState.data && !executionState.error) return;
+    WorkbenchRunningPromises.deletePromise(id);
     ExecutionStateCache.storeState(id, executionState);
-    // eslint-disable-next-line react-hooks/exhaustive-deps
   }, [executionState.data, executionState.error]);
 
   const incrementWorkVersion = useStore(
@@ -397,12 +402,14 @@ export const QueryTab = React.memo(function 
QueryTab(props: QueryTabProps) {
             {executionState.isLoading() && (
               <ExecutionTimerPanel
                 execution={executionState.intermediate}
+                startTime={currentRunningPromise?.startTime}
                 onCancel={() => queryManager.cancelCurrent()}
               />
             )}
             {(execution || executionState.error) && (
               <ExecutionSummaryPanel
                 execution={execution}
+                queryErrorDuration={executionState.error?.queryDuration}
                 onExecutionDetail={() => onDetails(statsTaskId!)}
                 onReset={() => {
                   queryManager.reset();


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to