Skip to content

Commit 4be176a

Browse files
committed
fix: optimize performance
1 parent 9f83478 commit 4be176a

File tree

6 files changed

+48
-111
lines changed

6 files changed

+48
-111
lines changed

src/components/QueryResultTable/QueryResultTable.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import type {Column, Settings} from '@gravity-ui/react-data-table';
66
import type {ColumnType, KeyValueRow} from '../../types/api/query';
77
import {cn} from '../../utils/cn';
88
import {DEFAULT_TABLE_SETTINGS} from '../../utils/constants';
9-
import {getColumnType, prepareQueryResponse} from '../../utils/query';
9+
import {getColumnType} from '../../utils/query';
1010
import {isNumeric} from '../../utils/utils';
1111
import type {ResizeableDataTableProps} from '../ResizeableDataTable/ResizeableDataTable';
1212
import {ResizeableDataTable} from '../ResizeableDataTable/ResizeableDataTable';
@@ -80,16 +80,18 @@ interface QueryResultTableProps
8080
}
8181

8282
export const QueryResultTable = (props: QueryResultTableProps) => {
83-
const {columns: rawColumns, data: rawData, ...restProps} = props;
83+
const {columns: rawColumns, data, ...restProps} = props;
8484

85-
const data = React.useMemo(() => prepareQueryResponse(rawData), [rawData]);
8685
const columns = React.useMemo(() => {
86+
if (!data?.length) {
87+
return [];
88+
}
8789
return rawColumns ? prepareTypedColumns(rawColumns, data) : prepareGenericColumns(data);
8890
}, [data, rawColumns]);
8991

9092
// empty data is expected to be be an empty array
9193
// undefined data is not rendered at all
92-
if (!Array.isArray(rawData)) {
94+
if (!Array.isArray(data)) {
9395
return null;
9496
}
9597

src/services/api/viewer.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -394,10 +394,7 @@ export class ViewerAPI extends BaseYdbAPI {
394394
true,
395395
);
396396

397-
// Create parser state and tracking in closure
398-
let parserState = {
399-
lastProcessedLength: 0,
400-
};
397+
let lastProcessedLength = 0;
401398

402399
return this.get<string>(
403400
this.getPath('/viewer/query'),
@@ -421,11 +418,11 @@ export class ViewerAPI extends BaseYdbAPI {
421418
},
422419
onDownloadProgress: (progressEvent) => {
423420
const response = progressEvent.event.target as XMLHttpRequest;
424-
const {chunks, state} = parseMultipart({
421+
const {chunks, lastProcessedLength: currentLength} = parseMultipart({
425422
responseText: response.responseText,
426-
state: parserState,
423+
lastProcessedLength,
427424
});
428-
parserState = state;
425+
lastProcessedLength = currentLength;
429426
let mergedChunk = null;
430427

431428
for (const chunk of chunks) {
Lines changed: 8 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,41 @@
11
/* eslint-disable camelcase */
22
import type {StreamingChunk} from '../../types/store/streaming';
33

4-
// interface Headers {
5-
// contentType?: string;
6-
// contentLength?: number;
7-
// }
8-
9-
interface MultipartState {
10-
lastProcessedLength: number;
11-
}
12-
134
interface MultipartResult {
145
chunks: StreamingChunk[];
15-
state: MultipartState;
6+
lastProcessedLength: number;
167
}
178

18-
// function parseHeaders(headerLines: string[]): Headers {
19-
// const headers: Headers = {};
20-
// for (const line of headerLines) {
21-
// const [key, value] = line.split(': ');
22-
// switch (key.toLowerCase()) {
23-
// case 'content-type':
24-
// headers.contentType = value;
25-
// break;
26-
// case 'content-length':
27-
// headers.contentLength = Number(value);
28-
// break;
29-
// }
30-
// }
31-
// return headers;
32-
// }
33-
349
const CRLF = '\r\n';
3510

3611
export function parseMultipart({
3712
responseText,
38-
state = {lastProcessedLength: 0},
13+
lastProcessedLength,
3914
boundary = 'boundary',
4015
}: {
4116
responseText: string;
42-
state?: MultipartState;
17+
lastProcessedLength: number;
4318
boundary?: string;
4419
}): MultipartResult {
45-
// Combine buffer with new data
46-
const newData = responseText.slice(state.lastProcessedLength);
20+
const newData = responseText.slice(lastProcessedLength);
4721

4822
if (!newData) {
49-
return {chunks: [], state};
23+
return {chunks: [], lastProcessedLength};
5024
}
5125

5226
// Split on boundary with double dashes and CRLF
5327
const boundaryStr = `--${boundary}${CRLF}`;
5428
const parts = newData.split(boundaryStr);
5529

56-
let currentPosition = state.lastProcessedLength;
30+
let currentPosition = lastProcessedLength;
5731
const chunks: StreamingChunk[] = [];
5832

5933
for (let i = 0; i < parts.length; i++) {
6034
const part = parts[i];
6135
const isLastPart = i === parts.length - 1;
6236

63-
// Split part into lines by CRLF
6437
const lines = part.split(CRLF);
6538

66-
// Find the empty line that separates headers from content
6739
const emptyLineIndex = lines.findIndex((line) => line === '');
6840
if (emptyLineIndex === -1 || !lines[emptyLineIndex + 1]) {
6941
if (isLastPart) {
@@ -72,38 +44,20 @@ export function parseMultipart({
7244
continue;
7345
}
7446

75-
// const headers = parseHeaders(lines.slice(0, emptyLineIndex));
76-
7747
const jsonContent = lines[emptyLineIndex + 1];
7848

79-
// if (headers.contentLength) {
80-
// jsonContent = jsonContent.slice(0, headers.contentLength);
81-
// }
82-
83-
// if (headers.contentLength && jsonContent.length < headers.contentLength) {
84-
// newBuffer = jsonContent;
85-
// break;
86-
// }
87-
8849
let parsedChunk: StreamingChunk | null = null;
8950
try {
9051
parsedChunk = JSON.parse(jsonContent) as StreamingChunk;
9152
} catch {}
9253

93-
// If it's the last part and not a complete chunk, store in buffer
9454
if (!parsedChunk) {
9555
break;
9656
}
97-
// Track position by adding boundary length and full part length
57+
9858
chunks.push(parsedChunk);
9959
currentPosition += boundaryStr.length + part.length;
10060
}
10161

102-
// Update state with precise position tracking
103-
const newState: MultipartState = {
104-
// Only update lastProcessedLength if we actually processed some chunks
105-
lastProcessedLength: currentPosition,
106-
};
107-
108-
return {chunks, state: newState};
62+
return {chunks, lastProcessedLength: currentPosition};
10963
}

src/store/reducers/query/query.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,13 @@ import {TracingLevelNumber} from '../../../types/api/query';
66
import type {QueryAction, QueryRequestParams, QuerySettings} from '../../../types/store/query';
77
import type {StreamingChunk} from '../../../types/store/streaming';
88
import {QUERIES_HISTORY_KEY} from '../../../utils/constants';
9-
import {isQueryErrorResponse} from '../../../utils/query';
9+
import {isQueryErrorResponse, parseResult} from '../../../utils/query';
1010
import {isNumeric} from '../../../utils/utils';
1111
import {api} from '../api';
1212

1313
import {prepareQueryData} from './prepareQueryData';
1414
import type {QueryResult, QueryState} from './types';
1515
import {
16-
convertToKeyValueRows,
1716
getActionAndSyntaxFromQueryMode,
1817
getQueryInHistory,
1918
isQueryResponseChunk,
@@ -161,15 +160,15 @@ const slice = createSlice({
161160
}
162161
// Convert and append new rows
163162
if (rows.length > 0 && state.result.data.resultSets?.[0]?.columns) {
164-
const convertedRows = convertToKeyValueRows(
163+
const formattedRows = parseResult(
165164
rows,
166165
state.result.data.resultSets[0].columns,
167166
);
168167

169168
if (state.result.data.resultSets[0].result) {
170-
state.result.data.resultSets[0].result.push(...convertedRows);
169+
state.result.data.resultSets[0].result.push(...formattedRows);
171170
} else {
172-
state.result.data.resultSets[0].result = convertedRows;
171+
state.result.data.resultSets[0].result = formattedRows;
173172
}
174173
}
175174
} else if (isQueryResponseChunk(chunk)) {

src/types/store/streaming.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type {ColumnType, QueryPlan, ScriptPlan, TKqpStatsQuery} from '../api/query';
1+
import type {ArrayRow, ColumnType, QueryPlan, ScriptPlan, TKqpStatsQuery} from '../api/query';
22

33
export interface SessionChunk {
44
meta: {
@@ -17,7 +17,7 @@ export interface StreamDataChunk {
1717
};
1818
result: {
1919
columns?: ColumnType[];
20-
rows: unknown[];
20+
rows: ArrayRow[];
2121
};
2222
}
2323

src/utils/query.ts

Lines changed: 24 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -139,15 +139,30 @@ export const getColumnType = (type: string) => {
139139
}
140140
};
141141

142-
/** parse response result from ArrayRow to KeyValueRow */
143-
const parseResult = (rows: ArrayRow[], columns: ColumnType[]) => {
144-
return rows.map((row) => {
145-
return row.reduce<KeyValueRow>((newRow, cellData, columnIndex) => {
146-
const {name} = columns[columnIndex];
147-
newRow[name] = cellData;
148-
return newRow;
149-
}, {});
150-
});
142+
/** parse response result from ArrayRow to KeyValueRow and format values */
143+
export const parseResult = (rows: ArrayRow[], columns: ColumnType[]) => {
144+
const result = new Array(rows.length);
145+
for (let i = 0; i < rows.length; i++) {
146+
const row = rows[i];
147+
const obj: KeyValueRow = {};
148+
for (let j = 0; j < row.length; j++) {
149+
const value = row[j];
150+
const columnName = columns[j].name;
151+
152+
// Format the value based on its type
153+
if (
154+
(value !== null && typeof value === 'object') ||
155+
typeof value === 'boolean' ||
156+
Array.isArray(value)
157+
) {
158+
obj[columnName] = JSON.stringify(value);
159+
} else {
160+
obj[columnName] = value;
161+
}
162+
}
163+
result[i] = obj;
164+
}
165+
return result;
151166
};
152167

153168
const parseExecuteResponse = (data: ExecuteResponse): IQueryResult => {
@@ -251,36 +266,6 @@ export const parseQueryExplainPlan = (plan: ScriptPlan | QueryPlan): QueryPlan =
251266
return plan;
252267
};
253268

254-
export const prepareQueryResponse = (data?: KeyValueRow[]) => {
255-
if (!Array.isArray(data)) {
256-
return [];
257-
}
258-
259-
return data.map((row) => {
260-
const formattedData: KeyValueRow = {};
261-
262-
for (const field in row) {
263-
if (Object.prototype.hasOwnProperty.call(row, field)) {
264-
const type = typeof row[field];
265-
266-
// Although typeof null == 'object'
267-
// null result should be preserved
268-
if (
269-
(row[field] !== null && type === 'object') ||
270-
type === 'boolean' ||
271-
Array.isArray(row[field])
272-
) {
273-
formattedData[field] = JSON.stringify(row[field]);
274-
} else {
275-
formattedData[field] = row[field];
276-
}
277-
}
278-
}
279-
280-
return formattedData;
281-
});
282-
};
283-
284269
export const parseQueryError = (error: unknown): ErrorResponse | string | undefined => {
285270
if (typeof error === 'string' || isQueryErrorResponse(error)) {
286271
return error;

0 commit comments

Comments
 (0)