Skip to content

Commit 82ff675

Browse files
Merge pull request #1276 from RedisInsight/fe/feature/RI-3595_Speed_up_initial_load_keys
#RI-3595 - Speed up initial load keys
2 parents 77f72f0 + 8da0330 commit 82ff675

File tree

11 files changed

+285
-44
lines changed

11 files changed

+285
-44
lines changed

redisinsight/api/src/modules/browser/controllers/keys/keys.controller.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ export class KeysController extends BaseController {
6060
);
6161
}
6262

63-
@Post('get-infos')
63+
@Post('get-metadata')
6464
@HttpCode(200)
6565
@ApiOperation({ description: 'Get info for multiple keys' })
6666
@ApiBody({ type: GetKeysInfoDto })

redisinsight/api/test/api/keys/POST-instance-id-keys-get_infos.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const { server, request, constants, rte } = deps;
1515

1616
// endpoint to test
1717
const endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>
18-
request(server).post(`/instance/${instanceId}/keys/get-infos`);
18+
request(server).post(`/instance/${instanceId}/keys/get-metadata`);
1919

2020
const responseSchema = Joi.array().items(Joi.object().keys({
2121
name: JoiRedisString.required(),
@@ -37,7 +37,7 @@ const mainCheckFn = async (testCase) => {
3737
});
3838
};
3939

40-
describe('POST /instance/:instanceId/keys/get-infos', () => {
40+
describe('POST /instance/:instanceId/keys/get-metadata', () => {
4141
before(async () => await rte.data.generateKeys(true));
4242

4343
describe('Modes', () => {

redisinsight/ui/src/components/virtual-tree/components/Node/Node.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ const Node = ({
3737
}, [keys, isSelected])
3838

3939
const handleClick = () => {
40-
if (isLeaf && keys) {
40+
if (isLeaf && keys && !isSelected) {
4141
setItems?.(keys)
4242
updateStatusSelected?.(fullName, keys)
4343
}

redisinsight/ui/src/constants/api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ enum ApiEndpoints {
1111
REDIS_CLOUD_DATABASES = 'redis-enterprise/cloud/get-databases',
1212
SENTINEL_MASTERS = 'sentinel/get-masters',
1313
KEYS = 'keys',
14+
KEYS_METADATA = 'keys/get-metadata',
1415
KEY_INFO = 'keys/get-info',
1516
KEY_NAME = 'keys/name',
1617
KEY_TTL = 'keys/ttl',

redisinsight/ui/src/pages/browser/components/key-list/KeyList.spec.tsx

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import React from 'react'
2-
import { render } from 'uiSrc/utils/test-utils'
2+
import { render, waitFor } from 'uiSrc/utils/test-utils'
33
import { KeysStoreData, KeyViewType } from 'uiSrc/slices/interfaces/keys'
44
import { keysSelector, setLastBatchKeys } from 'uiSrc/slices/browser/keys'
5+
import { apiService } from 'uiSrc/services'
56
import KeyList from './KeyList'
67

78
const propsMock = {
@@ -98,4 +99,75 @@ describe('KeyList', () => {
9899

99100
expect(setLastBatchKeys).not.toBeCalled()
100101
})
102+
103+
it('should call apiService.post to get key info', async () => {
104+
const apiServiceMock = jest.fn().mockResolvedValue([...propsMock.keysState.keys])
105+
apiService.post = apiServiceMock
106+
107+
const { rerender } = render(<KeyList {...propsMock} keysState={{ ...propsMock.keysState, keys: [] }} />)
108+
109+
rerender(<KeyList
110+
{...propsMock}
111+
keysState={{
112+
...propsMock.keysState,
113+
keys: propsMock.keysState.keys.map(({ name }) => ({ name })) }}
114+
/>)
115+
116+
await waitFor(async () => {
117+
expect(apiServiceMock).toBeCalled()
118+
}, { timeout: 150 })
119+
})
120+
121+
it('apiService.post should be called with only keys without info', async () => {
122+
const params = { params: { encoding: 'buffer' } }
123+
const apiServiceMock = jest.fn().mockResolvedValue([...propsMock.keysState.keys])
124+
apiService.post = apiServiceMock
125+
126+
const { rerender } = render(<KeyList {...propsMock} keysState={{ ...propsMock.keysState, keys: [] }} />)
127+
128+
rerender(<KeyList
129+
{...propsMock}
130+
keysState={{
131+
...propsMock.keysState,
132+
keys: [
133+
...propsMock.keysState.keys.map(({ name }) => ({ name })),
134+
{ name: 'key5', size: 100, length: 100 }, // key with info
135+
] }}
136+
/>)
137+
138+
await waitFor(async () => {
139+
expect(apiServiceMock).toBeCalledTimes(2)
140+
141+
expect(apiServiceMock.mock.calls[0]).toEqual([
142+
'/instance//keys/get-metadata',
143+
{ keys: ['key1'] },
144+
params,
145+
])
146+
147+
expect(apiServiceMock.mock.calls[1]).toEqual([
148+
'/instance//keys/get-metadata',
149+
{ keys: ['key1', 'key2', 'key3'] },
150+
params,
151+
])
152+
}, { timeout: 150 })
153+
})
154+
155+
it('key info loadings (type, ttl, size) should be in the DOM if keys do not have info', async () => {
156+
const { rerender, queryAllByTestId } = render(
157+
<KeyList {...propsMock} keysState={{ ...propsMock.keysState, keys: [] }} />
158+
)
159+
160+
rerender(<KeyList
161+
{...propsMock}
162+
keysState={{
163+
...propsMock.keysState,
164+
keys: [
165+
...propsMock.keysState.keys.map(({ name }) => ({ name })),
166+
] }}
167+
/>)
168+
169+
expect(queryAllByTestId(/ttl-loading/).length).toEqual(propsMock.keysState.keys.length)
170+
expect(queryAllByTestId(/type-loading/).length).toEqual(propsMock.keysState.keys.length)
171+
expect(queryAllByTestId(/size-loading/).length).toEqual(propsMock.keysState.keys.length)
172+
})
101173
})

redisinsight/ui/src/pages/browser/components/key-list/KeyList.tsx

Lines changed: 98 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useRef,
22
import { useDispatch, useSelector } from 'react-redux'
33
import cx from 'classnames'
44
import { useParams } from 'react-router-dom'
5+
import { debounce, isUndefined, reject } from 'lodash'
56

67
import {
78
EuiText,
89
EuiToolTip,
910
EuiTextColor,
11+
EuiLoadingContent,
1012
} from '@elastic/eui'
1113
import {
1214
formatBytes,
@@ -25,6 +27,7 @@ import {
2527
ScanNoResultsFoundText,
2628
} from 'uiSrc/constants/texts'
2729
import {
30+
fetchKeysMetadata,
2831
keysDataSelector,
2932
keysSelector,
3033
selectedKeySelector,
@@ -40,7 +43,7 @@ import { SCAN_COUNT_DEFAULT } from 'uiSrc/constants/api'
4043
import { KeysStoreData, KeyViewType } from 'uiSrc/slices/interfaces/keys'
4144
import VirtualTable from 'uiSrc/components/virtual-table/VirtualTable'
4245
import { ITableColumn } from 'uiSrc/components/virtual-table/interfaces'
43-
import { OVER_RENDER_BUFFER_COUNT, Pages, TableCellAlignment, TableCellTextAlignment } from 'uiSrc/constants'
46+
import { Pages, TableCellAlignment, TableCellTextAlignment } from 'uiSrc/constants'
4447
import { IKeyPropTypes } from 'uiSrc/constants/prop-types/keys'
4548
import { getBasedOnViewTypeEvent, sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'
4649

@@ -70,9 +73,10 @@ const KeyList = forwardRef((props: Props, ref) => {
7073
const { isSearched, isFiltered, viewType } = useSelector(keysSelector)
7174
const { keyList: { scrollTopPosition } } = useSelector(appContextBrowser)
7275

73-
const [items, setItems] = useState(keysState.keys)
76+
const [, rerender] = useState({})
7477

75-
const formattedLastIndexRef = useRef(OVER_RENDER_BUFFER_COUNT)
78+
const itemsRef = useRef(keysState.keys)
79+
const renderedRowsIndexesRef = useRef({ startIndex: 0, lastIndex: 0 })
7680

7781
const dispatch = useDispatch()
7882

@@ -87,20 +91,20 @@ const KeyList = forwardRef((props: Props, ref) => {
8791
if (viewType === KeyViewType.Tree) {
8892
return
8993
}
90-
setItems((prevItems) => {
91-
dispatch(setLastBatchKeys(prevItems.slice(-SCAN_COUNT_DEFAULT)))
92-
return []
94+
rerender(() => {
95+
dispatch(setLastBatchKeys(itemsRef.current?.slice(-SCAN_COUNT_DEFAULT)))
9396
})
9497
}, [])
9598

9699
useEffect(() => {
97-
const newKeys = bufferFormatRangeItems(keysState.keys, 0, OVER_RENDER_BUFFER_COUNT, formatItem)
98-
99-
if (keysState.keys.length < items.length) {
100-
formattedLastIndexRef.current = 0
100+
itemsRef.current = [...keysState.keys]
101+
if (itemsRef.current.length === 0) {
102+
return
101103
}
102104

103-
setItems(newKeys)
105+
const { lastIndex, startIndex } = renderedRowsIndexesRef.current
106+
onRowsRendered(startIndex, lastIndex)
107+
rerender({})
104108
}, [keysState.keys])
105109

106110
const onNoKeysLinkClick = () => {
@@ -130,8 +134,7 @@ const KeyList = forwardRef((props: Props, ref) => {
130134
}
131135

132136
const onLoadMoreItems = (props: { startIndex: number, stopIndex: number }) => {
133-
const formattedAllKeys = bufferFormatRangeItems(items, formattedLastIndexRef.current, items.length, formatItem)
134-
loadMoreItems?.(formattedAllKeys, props)
137+
loadMoreItems?.(itemsRef.current, props)
135138
}
136139

137140
const onWheelSearched = (event: React.WheelEvent) => {
@@ -159,34 +162,96 @@ const KeyList = forwardRef((props: Props, ref) => {
159162
nameString: bufferToString(item.name)
160163
}), [])
161164

162-
const bufferFormatRows = (lastIndex: number) => {
163-
const newItems = bufferFormatRangeItems(items, formattedLastIndexRef.current, lastIndex, formatItem)
165+
const onRowsRendered = debounce(async (startIndex: number, lastIndex: number) => {
166+
renderedRowsIndexesRef.current = { lastIndex, startIndex }
164167

165-
setItems(newItems)
168+
const newItems = bufferFormatRows(startIndex, lastIndex)
166169

167-
if (lastIndex > formattedLastIndexRef.current) {
168-
formattedLastIndexRef.current = lastIndex
169-
}
170+
getMetadata(startIndex, lastIndex, newItems)
171+
}, 100)
172+
173+
const bufferFormatRows = (startIndex: number, lastIndex: number): GetKeyInfoResponse[] => {
174+
const newItems = bufferFormatRangeItems(
175+
itemsRef.current, startIndex, lastIndex, formatItem
176+
)
177+
itemsRef.current.splice(startIndex, newItems.length, ...newItems)
170178

171179
return newItems
172180
}
173181

182+
const getMetadata = (
183+
startIndex: number,
184+
lastIndex: number,
185+
itemsInit: GetKeyInfoResponse[] = []
186+
): void => {
187+
const isSomeNotUndefined = ({ type, size, length }: GetKeyInfoResponse) =>
188+
!isUndefined(type) || !isUndefined(size) || !isUndefined(length)
189+
190+
const emptyItems = reject(itemsInit, isSomeNotUndefined)
191+
192+
if (!emptyItems.length) return
193+
194+
dispatch(fetchKeysMetadata(
195+
emptyItems.map(({ name }) => name),
196+
(loadedItems) =>
197+
onSuccessFetchedMetadata({
198+
startIndex,
199+
lastIndex,
200+
loadedItems,
201+
isFirstEmpty: !isSomeNotUndefined(itemsInit[0]),
202+
})
203+
))
204+
}
205+
206+
const onSuccessFetchedMetadata = (data: {
207+
startIndex: number,
208+
lastIndex: number,
209+
isFirstEmpty: boolean
210+
loadedItems: GetKeyInfoResponse[],
211+
}) => {
212+
const {
213+
startIndex,
214+
lastIndex,
215+
isFirstEmpty,
216+
loadedItems,
217+
} = data
218+
const items = loadedItems.map(formatItem)
219+
const startIndexDel = isFirstEmpty ? startIndex : lastIndex - items.length + 1
220+
221+
itemsRef.current.splice(startIndexDel, items.length, ...items)
222+
223+
rerender({})
224+
}
225+
174226
const columns: ITableColumn[] = [
175227
{
176228
id: 'type',
177229
label: 'Type',
178230
absoluteWidth: 'auto',
179231
minWidth: 126,
180-
render: (cellData: any, { nameString: name }: any) => <GroupBadge type={cellData} name={name} />,
232+
render: (cellData: any, { nameString: name }: any) => (
233+
isUndefined(cellData)
234+
? <EuiLoadingContent lines={1} className={styles.keyInfoLoading} data-testid="type-loading" />
235+
: <GroupBadge type={cellData} name={name} />
236+
)
181237
},
182238
{
183239
id: 'nameString',
184240
label: 'Key',
185241
minWidth: 100,
186242
truncateText: true,
187-
render: (cellData: string = '') => {
243+
render: (cellData: string) => {
244+
if (isUndefined(cellData)) {
245+
return (
246+
<EuiLoadingContent
247+
lines={1}
248+
className={cx(styles.keyInfoLoading, styles.keyNameLoading)}
249+
data-testid="name-loading"
250+
/>
251+
)
252+
}
188253
// Better to cut the long string, because it could affect virtual scroll performance
189-
const name = cellData
254+
const name = cellData || ''
190255
const cellContent = replaceSpaces(name?.substring(0, 200))
191256
const tooltipContent = formatLongName(name)
192257
return (
@@ -214,6 +279,9 @@ const KeyList = forwardRef((props: Props, ref) => {
214279
truncateText: true,
215280
alignment: TableCellAlignment.Right,
216281
render: (cellData: number, { nameString: name }: GetKeyInfoResponse) => {
282+
if (isUndefined(cellData)) {
283+
return <EuiLoadingContent lines={1} className={styles.keyInfoLoading} data-testid="ttl-loading" />
284+
}
217285
if (cellData === -1) {
218286
return (
219287
<EuiTextColor color="subdued" data-testid={`ttl-${name}`}>
@@ -252,6 +320,10 @@ const KeyList = forwardRef((props: Props, ref) => {
252320
alignment: TableCellAlignment.Right,
253321
textAlignment: TableCellTextAlignment.Right,
254322
render: (cellData: number, { nameString: name }: GetKeyInfoResponse) => {
323+
if (isUndefined(cellData)) {
324+
return <EuiLoadingContent lines={1} className={styles.keyInfoLoading} data-testid="size-loading" />
325+
}
326+
255327
if (!cellData) {
256328
return (
257329
<EuiText color="subdued" size="s" style={{ maxWidth: '100%' }} data-testid={`size-${name}`}>
@@ -297,15 +369,16 @@ const KeyList = forwardRef((props: Props, ref) => {
297369
loadMoreItems={onLoadMoreItems}
298370
onWheel={onWheelSearched}
299371
loading={loading}
300-
items={items}
372+
items={itemsRef.current}
301373
totalItemsCount={keysState.total ? keysState.total : Infinity}
302374
scanned={isSearched || isFiltered ? keysState.scanned : 0}
303375
noItemsMessage={getNoItemsMessage()}
304376
selectedKey={selectedKey}
305377
scrollTopProp={scrollTopPosition}
306378
setScrollTopPosition={setScrollTopPosition}
307379
hideFooter={hideFooter}
308-
onRowsRendered={({ overscanStopIndex }) => bufferFormatRows(overscanStopIndex)}
380+
onRowsRendered={({ overscanStartIndex, overscanStopIndex }) =>
381+
onRowsRendered(overscanStartIndex, overscanStopIndex)}
309382
/>
310383
</div>
311384
</div>
@@ -314,4 +387,4 @@ const KeyList = forwardRef((props: Props, ref) => {
314387
)
315388
})
316389

317-
export default KeyList
390+
export default React.memo(KeyList)

redisinsight/ui/src/pages/browser/components/key-list/styles.module.scss

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,20 @@
3838
transition: opacity 250ms ease-in-out;
3939
}
4040

41+
.keyInfoLoading {
42+
width: 44px;
43+
44+
:global(.euiLoadingContent__singleLine) {
45+
margin-bottom: 0;
46+
}
47+
}
48+
49+
.keyNameLoading {
50+
width: 50%;
51+
min-width: 100px;
52+
max-width: 300px;
53+
}
54+
4155
:global(.table-row-selected) .action {
4256
opacity: 1;
4357
}

0 commit comments

Comments
 (0)