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 {
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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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>
`;

View File

@ -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();
});
});

View File

@ -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(() => {

View File

@ -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();