Web console: fix query timer issues (#16235)

* fix timer issues

* wording
This commit is contained in:
Vadim Ogievetsky 2024-04-04 13:13:31 -07:00 committed by GitHub
parent 7759f25095
commit 9658e1ad7f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 115 additions and 28 deletions

View File

@ -23,24 +23,24 @@ interface EditorState {
} }
export class AceEditorStateCache { export class AceEditorStateCache {
static states: Record<string, EditorState> = {}; static states = new Map<string, EditorState>();
static saveState(id: string, editor: Ace.Editor): void { static saveState(id: string, editor: Ace.Editor): void {
const session = editor.getSession(); const session = editor.getSession();
const undoManager: any = session.getUndoManager(); const undoManager: any = session.getUndoManager();
AceEditorStateCache.states[id] = { AceEditorStateCache.states.set(id, {
undoManager, undoManager,
}; });
} }
static applyState(id: string, editor: Ace.Editor): void { static applyState(id: string, editor: Ace.Editor): void {
const state = AceEditorStateCache.states[id]; const state = AceEditorStateCache.states.get(id);
if (!state) return; if (!state) return;
const session = editor.getSession(); const session = editor.getSession();
session.setUndoManager(state.undoManager); session.setUndoManager(state.undoManager);
} }
static deleteState(id: string): void { static deleteState(id: string): void {
delete AceEditorStateCache.states[id]; AceEditorStateCache.states.delete(id);
} }
} }

View File

@ -20,17 +20,17 @@ import type { Execution } from '../druid-models';
import type { DruidError, QueryState } from '../utils'; import type { DruidError, QueryState } from '../utils';
export class ExecutionStateCache { 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 { 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 { static getState(id: string): QueryState<Execution, DruidError, Execution> | undefined {
return ExecutionStateCache.cache[id]; return ExecutionStateCache.cache.get(id);
} }
static deleteState(id: string): void { static deleteState(id: string): void {
delete ExecutionStateCache.cache[id]; ExecutionStateCache.cache.delete(id);
} }
} }

View File

@ -21,24 +21,25 @@ import type { QueryResult } from '@druid-toolkit/query';
export interface WorkbenchRunningPromise { export interface WorkbenchRunningPromise {
promise: Promise<QueryResult>; promise: Promise<QueryResult>;
prefixLines: number; prefixLines: number;
startTime: Date;
} }
export class WorkbenchRunningPromises { 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 { static isWorkbenchRunningPromise(x: any): x is WorkbenchRunningPromise {
return Boolean(x.promise); return Boolean(x.promise);
} }
static storePromise(id: string, promise: WorkbenchRunningPromise): void { static storePromise(id: string, promise: WorkbenchRunningPromise): void {
WorkbenchRunningPromises.promises[id] = promise; WorkbenchRunningPromises.promises.set(id, promise);
} }
static getPromise(id: string): WorkbenchRunningPromise | undefined { static getPromise(id: string): WorkbenchRunningPromise | undefined {
return WorkbenchRunningPromises.promises[id]; return WorkbenchRunningPromises.promises.get(id);
} }
static deletePromise(id: string): void { static deletePromise(id: string): void {
delete WorkbenchRunningPromises.promises[id]; WorkbenchRunningPromises.promises.delete(id);
} }
} }

View File

@ -267,6 +267,7 @@ export class DruidError extends Error {
public startRowColumn?: RowColumn; public startRowColumn?: RowColumn;
public endRowColumn?: RowColumn; public endRowColumn?: RowColumn;
public suggestion?: QuerySuggestion; public suggestion?: QuerySuggestion;
public queryDuration?: number;
// Deprecated // Deprecated
public error?: string; public error?: string;

View File

@ -36,6 +36,7 @@ import './execution-summary-panel.scss';
export interface ExecutionSummaryPanelProps { export interface ExecutionSummaryPanelProps {
execution: Execution | undefined; execution: Execution | undefined;
queryErrorDuration: number | undefined;
onExecutionDetail(): void; onExecutionDetail(): void;
onReset?: () => void; onReset?: () => void;
} }
@ -43,12 +44,22 @@ export interface ExecutionSummaryPanelProps {
export const ExecutionSummaryPanel = React.memo(function ExecutionSummaryPanel( export const ExecutionSummaryPanel = React.memo(function ExecutionSummaryPanel(
props: ExecutionSummaryPanelProps, props: ExecutionSummaryPanelProps,
) { ) {
const { execution, onExecutionDetail, onReset } = props; const { execution, queryErrorDuration, onExecutionDetail, onReset } = props;
const [showDestinationPages, setShowDestinationPages] = useState(false); const [showDestinationPages, setShowDestinationPages] = useState(false);
const queryResult = execution?.result; const queryResult = execution?.result;
const buttons: JSX.Element[] = []; const buttons: JSX.Element[] = [];
if (typeof queryErrorDuration === 'number') {
buttons.push(
<Button
key="timing"
minimal
text={`Error after ${formatDurationHybrid(queryErrorDuration)}`}
/>,
);
}
if (queryResult) { if (queryResult) {
const wrapQueryLimit = queryResult.getSqlOuterLimit(); const wrapQueryLimit = queryResult.getSqlOuterLimit();
let resultCount: string; let resultCount: string;

View File

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AnchoredQueryTimer matches snapshot 1`] = ` exports[`AnchoredQueryTimer matches snapshot with execution 1`] = `
<div <div
class="bp4-button-group execution-timer-panel" class="bp4-button-group execution-timer-panel"
> >
@ -57,3 +57,61 @@ exports[`AnchoredQueryTimer matches snapshot 1`] = `
</button> </button>
</div> </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>
`;

View File

@ -37,13 +37,21 @@ describe('AnchoredQueryTimer', () => {
jest.restoreAllMocks(); jest.restoreAllMocks();
}); });
it('matches snapshot', () => { it('matches snapshot with execution', () => {
const { container } = render( const { container } = render(
<ExecutionTimerPanel <ExecutionTimerPanel
execution={new Execution({ engine: 'sql-msq-task', id: 'xxx', startTime: new Date(start) })} execution={new Execution({ engine: 'sql-msq-task', id: 'xxx', startTime: new Date(start) })}
startTime={undefined}
onCancel={() => {}} onCancel={() => {}}
/>, />,
); );
expect(container.firstChild).toMatchSnapshot(); expect(container.firstChild).toMatchSnapshot();
}); });
it('matches snapshot with startTime', () => {
const { container } = render(
<ExecutionTimerPanel execution={undefined} startTime={new Date(start)} onCancel={() => {}} />,
);
expect(container.firstChild).toMatchSnapshot();
});
}); });

View File

@ -29,15 +29,16 @@ import './execution-timer-panel.scss';
export interface ExecutionTimerPanelProps { export interface ExecutionTimerPanelProps {
execution: Execution | undefined; execution: Execution | undefined;
startTime: Date | undefined;
onCancel(): void; onCancel(): void;
} }
export const ExecutionTimerPanel = React.memo(function ExecutionTimerPanel( export const ExecutionTimerPanel = React.memo(function ExecutionTimerPanel(
props: ExecutionTimerPanelProps, props: ExecutionTimerPanelProps,
) { ) {
const { execution, onCancel } = props; const { execution, startTime, onCancel } = props;
const [showCancelConfirm, setShowCancelConfirm] = useState(false); const [showCancelConfirm, setShowCancelConfirm] = useState(false);
const [mountTime] = useState(Date.now()); const [mountTime] = useState(startTime?.valueOf() ?? Date.now());
const [currentTime, setCurrentTime] = useState(Date.now()); const [currentTime, setCurrentTime] = useState(Date.now());
useInterval(() => { useInterval(() => {

View File

@ -170,16 +170,16 @@ export const QueryTab = React.memo(function QueryTab(props: QueryTabProps) {
const queryInputRef = useRef<FlexibleQueryInput | null>(null); const queryInputRef = useRef<FlexibleQueryInput | null>(null);
const cachedExecutionState = ExecutionStateCache.getState(id);
const currentRunningPromise = WorkbenchRunningPromises.getPromise(id);
const [executionState, queryManager] = useQueryManager< const [executionState, queryManager] = useQueryManager<
WorkbenchQuery | WorkbenchRunningPromise | LastExecution, WorkbenchQuery | WorkbenchRunningPromise | LastExecution,
Execution, Execution,
Execution, Execution,
DruidError DruidError
>({ >({
initQuery: ExecutionStateCache.getState(id) initQuery: cachedExecutionState ? undefined : currentRunningPromise || query.getLastExecution(),
? undefined initState: cachedExecutionState,
: WorkbenchRunningPromises.getPromise(id) || query.getLastExecution(),
initState: ExecutionStateCache.getState(id),
processQuery: async (q, cancelToken) => { processQuery: async (q, cancelToken) => {
if (q instanceof WorkbenchQuery) { if (q instanceof WorkbenchQuery) {
ExecutionStateCache.deleteState(id); ExecutionStateCache.deleteState(id);
@ -214,6 +214,7 @@ export const QueryTab = React.memo(function QueryTab(props: QueryTabProps) {
onQueryChange(props.query.changeLastExecution(undefined)); onQueryChange(props.query.changeLastExecution(undefined));
const startTime = new Date();
let result: QueryResult; let result: QueryResult;
try { try {
const resultPromise = queryRunner.runQuery({ const resultPromise = queryRunner.runQuery({
@ -223,13 +224,19 @@ export const QueryTab = React.memo(function QueryTab(props: QueryTabProps) {
nativeQueryCancelFnRef.current = cancelFn; nativeQueryCancelFnRef.current = cancelFn;
}), }),
}); });
WorkbenchRunningPromises.storePromise(id, { promise: resultPromise, prefixLines }); WorkbenchRunningPromises.storePromise(id, {
promise: resultPromise,
prefixLines,
startTime,
});
result = await resultPromise; result = await resultPromise;
nativeQueryCancelFnRef.current = undefined; nativeQueryCancelFnRef.current = undefined;
} catch (e) { } catch (e) {
nativeQueryCancelFnRef.current = undefined; 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); return Execution.fromResult(engine, result);
@ -240,11 +247,9 @@ export const QueryTab = React.memo(function QueryTab(props: QueryTabProps) {
try { try {
result = await q.promise; result = await q.promise;
} catch (e) { } catch (e) {
WorkbenchRunningPromises.deletePromise(id);
throw new DruidError(e, q.prefixLines); throw new DruidError(e, q.prefixLines);
} }
WorkbenchRunningPromises.deletePromise(id);
return Execution.fromResult('sql-native', result); return Execution.fromResult('sql-native', result);
} else { } else {
switch (q.engine) { switch (q.engine) {
@ -265,9 +270,9 @@ export const QueryTab = React.memo(function QueryTab(props: QueryTabProps) {
}); });
useEffect(() => { useEffect(() => {
if (!executionState.data) return; if (!executionState.data && !executionState.error) return;
WorkbenchRunningPromises.deletePromise(id);
ExecutionStateCache.storeState(id, executionState); ExecutionStateCache.storeState(id, executionState);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [executionState.data, executionState.error]); }, [executionState.data, executionState.error]);
const incrementWorkVersion = useStore( const incrementWorkVersion = useStore(
@ -397,12 +402,14 @@ export const QueryTab = React.memo(function QueryTab(props: QueryTabProps) {
{executionState.isLoading() && ( {executionState.isLoading() && (
<ExecutionTimerPanel <ExecutionTimerPanel
execution={executionState.intermediate} execution={executionState.intermediate}
startTime={currentRunningPromise?.startTime}
onCancel={() => queryManager.cancelCurrent()} onCancel={() => queryManager.cancelCurrent()}
/> />
)} )}
{(execution || executionState.error) && ( {(execution || executionState.error) && (
<ExecutionSummaryPanel <ExecutionSummaryPanel
execution={execution} execution={execution}
queryErrorDuration={executionState.error?.queryDuration}
onExecutionDetail={() => onDetails(statsTaskId!)} onExecutionDetail={() => onDetails(statsTaskId!)}
onReset={() => { onReset={() => {
queryManager.reset(); queryManager.reset();