perf: batch gcode file metadata requests (#1737)
this results in fewer re-renders of the table, significantly reducing load time Signed-off-by: Nathan Regner <nathanregner@gmail.com>
This commit is contained in:
@@ -1235,11 +1235,12 @@ export default class GcodefilesPanel extends Mixins(BaseMixin, ControlMixin) {
|
|||||||
|
|
||||||
refreshMetadata(data: FileStateGcodefile[]) {
|
refreshMetadata(data: FileStateGcodefile[]) {
|
||||||
const items = data.filter((file) => !file.isDirectory && !file.metadataRequested && !file.metadataPulled)
|
const items = data.filter((file) => !file.isDirectory && !file.metadataRequested && !file.metadataPulled)
|
||||||
items.forEach((file: FileStateGcodefile) => {
|
this.$store.dispatch(
|
||||||
this.$store.dispatch('files/requestMetadata', {
|
'files/requestMetadata',
|
||||||
|
items.map((file: FileStateGcodefile) => ({
|
||||||
filename: 'gcodes' + this.currentPath + '/' + file.filename,
|
filename: 'gcodes' + this.currentPath + '/' + file.filename,
|
||||||
})
|
}))
|
||||||
})
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
clickRow(item: FileStateGcodefile, force = false) {
|
clickRow(item: FileStateGcodefile, force = false) {
|
||||||
|
@@ -343,12 +343,12 @@ export default class StatusPanelGcodefiles extends Mixins(BaseMixin, ControlMixi
|
|||||||
const requestItems = gcodes.filter(
|
const requestItems = gcodes.filter(
|
||||||
(file: FileStateGcodefile) => !file.metadataRequested && !file.metadataPulled
|
(file: FileStateGcodefile) => !file.metadataRequested && !file.metadataPulled
|
||||||
)
|
)
|
||||||
requestItems.forEach((file: FileStateGcodefile) => {
|
this.$store.dispatch(
|
||||||
this.$store.dispatch('files/requestMetadata', {
|
'files/requestMetadata',
|
||||||
|
requestItems.map((file: FileStateGcodefile) => ({
|
||||||
filename: 'gcodes/' + file.filename,
|
filename: 'gcodes/' + file.filename,
|
||||||
})
|
}))
|
||||||
})
|
)
|
||||||
|
|
||||||
return gcodes
|
return gcodes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -10,6 +10,7 @@ export class WebSocketClient {
|
|||||||
reconnectInterval = 1000
|
reconnectInterval = 1000
|
||||||
reconnects = 0
|
reconnects = 0
|
||||||
keepAliveTimeout = 1000
|
keepAliveTimeout = 1000
|
||||||
|
messageId: number = 0
|
||||||
timerId: number | null = null
|
timerId: number | null = null
|
||||||
store: Store<RootState> | null = null
|
store: Store<RootState> | null = null
|
||||||
waits: Wait[] = []
|
waits: Wait[] = []
|
||||||
@@ -25,6 +26,54 @@ export class WebSocketClient {
|
|||||||
this.url = url
|
this.url = url
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleMessage(data: any) {
|
||||||
|
const wait = this.getWaitById(data.id)
|
||||||
|
|
||||||
|
// report error messages
|
||||||
|
if (data.error?.message) {
|
||||||
|
// only report errors, if not disconnected and no init component
|
||||||
|
if (data.error?.message !== 'Klippy Disconnected' && !wait?.action?.startsWith('server/')) {
|
||||||
|
window.console.error(`Response Error: ${data.error.message} (${wait?.action ?? 'no action'})`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wait?.id) {
|
||||||
|
const modulename = wait.action?.split('/')[1] ?? null
|
||||||
|
|
||||||
|
if (modulename && wait.action?.startsWith('server/') && initableServerComponents.includes(modulename)) {
|
||||||
|
const component = wait.action.replace('server/', '').split('/')[0]
|
||||||
|
window.console.error(`init server component ${component} failed`)
|
||||||
|
this.store?.dispatch('server/addFailedInitComponent', component)
|
||||||
|
this.store?.dispatch('socket/removeInitComponent', `server/${component}/`)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.removeWaitById(wait.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// pass it to socket/onMessage, if no wait exists
|
||||||
|
if (!wait) {
|
||||||
|
this.store?.dispatch('socket/onMessage', data)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// pass result to action
|
||||||
|
if (wait?.action) {
|
||||||
|
let result = data.result
|
||||||
|
if (result === 'ok') result = { result: result }
|
||||||
|
if (typeof result === 'string') result = { result: result }
|
||||||
|
|
||||||
|
const preload = {}
|
||||||
|
if (wait.actionPayload) Object.assign(preload, wait.actionPayload)
|
||||||
|
Object.assign(preload, { requestParams: wait.params })
|
||||||
|
Object.assign(preload, result)
|
||||||
|
this.store?.dispatch(wait.action, preload)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.removeWaitById(wait.id)
|
||||||
|
}
|
||||||
|
|
||||||
async connect() {
|
async connect() {
|
||||||
this.store?.dispatch('socket/setData', {
|
this.store?.dispatch('socket/setData', {
|
||||||
isConnecting: true,
|
isConnecting: true,
|
||||||
@@ -58,55 +107,13 @@ export class WebSocketClient {
|
|||||||
if (this.store === null) return
|
if (this.store === null) return
|
||||||
|
|
||||||
const data = JSON.parse(msg.data)
|
const data = JSON.parse(msg.data)
|
||||||
const wait = this.getWaitById(data.id)
|
if (Array.isArray(data)) {
|
||||||
|
for (const message of data) {
|
||||||
// report error messages
|
this.handleMessage(message)
|
||||||
if (data.error?.message) {
|
|
||||||
// only report errors, if not disconnected and no init component
|
|
||||||
if (data.error?.message !== 'Klippy Disconnected' && !wait?.action?.startsWith('server/')) {
|
|
||||||
window.console.error(`Response Error: ${data.error.message} (${wait?.action ?? 'no action'})`)
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
if (wait?.id) {
|
this.handleMessage(data)
|
||||||
const modulename = wait.action?.split('/')[1] ?? null
|
|
||||||
|
|
||||||
if (
|
|
||||||
modulename &&
|
|
||||||
wait.action?.startsWith('server/') &&
|
|
||||||
initableServerComponents.includes(modulename)
|
|
||||||
) {
|
|
||||||
const component = wait.action.replace('server/', '').split('/')[0]
|
|
||||||
window.console.error(`init server component ${component} failed`)
|
|
||||||
this.store?.dispatch('server/addFailedInitComponent', component)
|
|
||||||
this.store?.dispatch('socket/removeInitComponent', `server/${component}/`)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.removeWaitById(wait.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// pass it to socket/onMessage, if no wait exists
|
|
||||||
if (!wait) {
|
|
||||||
this.store?.dispatch('socket/onMessage', data)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// pass result to action
|
|
||||||
if (wait?.action) {
|
|
||||||
let result = data.result
|
|
||||||
if (result === 'ok') result = { result: result }
|
|
||||||
if (typeof result === 'string') result = { result: result }
|
|
||||||
|
|
||||||
const preload = {}
|
|
||||||
if (wait.actionPayload) Object.assign(preload, wait.actionPayload)
|
|
||||||
Object.assign(preload, { requestParams: wait.params })
|
|
||||||
Object.assign(preload, result)
|
|
||||||
this.store?.dispatch(wait.action, preload)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.removeWaitById(wait.id)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,7 +137,7 @@ export class WebSocketClient {
|
|||||||
emit(method: string, params: Params, options: emitOptions = {}): void {
|
emit(method: string, params: Params, options: emitOptions = {}): void {
|
||||||
if (this.instance?.readyState !== WebSocket.OPEN) return
|
if (this.instance?.readyState !== WebSocket.OPEN) return
|
||||||
|
|
||||||
const id = Math.floor(Math.random() * 10000) + 1
|
const id = this.messageId++
|
||||||
this.waits.push({
|
this.waits.push({
|
||||||
id: id,
|
id: id,
|
||||||
params: params,
|
params: params,
|
||||||
@@ -150,6 +157,33 @@ export class WebSocketClient {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
emitBatch(messages: BatchMessage[]): void {
|
||||||
|
if (messages.length === 0) return
|
||||||
|
if (this.instance?.readyState !== WebSocket.OPEN) return
|
||||||
|
|
||||||
|
const body = []
|
||||||
|
for (const { method, params, emitOptions = {} } of messages) {
|
||||||
|
const id = this.messageId++
|
||||||
|
this.waits.push({
|
||||||
|
id: id,
|
||||||
|
params: params,
|
||||||
|
action: emitOptions.action ?? null,
|
||||||
|
actionPayload: emitOptions.actionPayload ?? {},
|
||||||
|
loading: emitOptions.loading ?? null,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (emitOptions.loading) this.store?.dispatch('socket/addLoading', { name: emitOptions.loading })
|
||||||
|
body.push({
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
method,
|
||||||
|
params,
|
||||||
|
id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
this.instance.send(JSON.stringify(body))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function WebSocketPlugin(Vue: typeof _Vue, options: WebSocketPluginOptions): void {
|
export function WebSocketPlugin(Vue: typeof _Vue, options: WebSocketPluginOptions): void {
|
||||||
@@ -169,6 +203,13 @@ export interface WebSocketClient {
|
|||||||
connect(): void
|
connect(): void
|
||||||
close(): void
|
close(): void
|
||||||
emit(method: string, params: Params, emitOptions: emitOptions): void
|
emit(method: string, params: Params, emitOptions: emitOptions): void
|
||||||
|
emitBatch(messages: BatchMessage[]): void
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BatchMessage {
|
||||||
|
method: string
|
||||||
|
params: Params
|
||||||
|
emitOptions: emitOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Wait {
|
export interface Wait {
|
||||||
|
@@ -11,6 +11,7 @@ import { RootState } from '@/store/types'
|
|||||||
import i18n from '@/plugins/i18n'
|
import i18n from '@/plugins/i18n'
|
||||||
import { hiddenDirectories, validGcodeExtensions } from '@/store/variables'
|
import { hiddenDirectories, validGcodeExtensions } from '@/store/variables'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
import { BatchMessage } from '@/plugins/webSocketClient'
|
||||||
|
|
||||||
export const actions: ActionTree<FileState, RootState> = {
|
export const actions: ActionTree<FileState, RootState> = {
|
||||||
reset({ commit }) {
|
reset({ commit }) {
|
||||||
@@ -159,13 +160,26 @@ export const actions: ActionTree<FileState, RootState> = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
requestMetadata({ commit }, payload: { filename: string }) {
|
requestMetadata({ commit }, payload: { filename: string }[]) {
|
||||||
const rootPath = payload.filename.slice(0, payload.filename.indexOf('/'))
|
// request file metadata in batches to reduce the number of table re-renders when responses are received
|
||||||
if (rootPath === 'gcodes') {
|
let messages: BatchMessage[] = []
|
||||||
const requestFilename = payload.filename.slice(7)
|
for (const { filename } of payload) {
|
||||||
commit('setMetadataRequested', { filename: requestFilename })
|
if (messages.length >= 100) {
|
||||||
Vue.$socket.emit('server.files.metadata', { filename: requestFilename }, { action: 'files/getMetadata' })
|
Vue.$socket.emitBatch(messages)
|
||||||
|
messages = []
|
||||||
|
}
|
||||||
|
const rootPath = filename.slice(0, filename.indexOf('/'))
|
||||||
|
if (rootPath === 'gcodes') {
|
||||||
|
const requestFilename = filename.slice(7)
|
||||||
|
commit('setMetadataRequested', { filename: requestFilename })
|
||||||
|
messages.push({
|
||||||
|
method: 'server.files.metadata',
|
||||||
|
params: { filename: requestFilename },
|
||||||
|
emitOptions: { action: 'files/getMetadata' },
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Vue.$socket.emitBatch(messages)
|
||||||
},
|
},
|
||||||
|
|
||||||
getMetadata({ commit, rootState }, payload) {
|
getMetadata({ commit, rootState }, payload) {
|
||||||
@@ -203,9 +217,11 @@ export const actions: ActionTree<FileState, RootState> = {
|
|||||||
payload.item.root === 'gcodes' &&
|
payload.item.root === 'gcodes' &&
|
||||||
validGcodeExtensions.includes(payload.item.path.slice(payload.item.path.lastIndexOf('.')))
|
validGcodeExtensions.includes(payload.item.path.slice(payload.item.path.lastIndexOf('.')))
|
||||||
) {
|
) {
|
||||||
await dispatch('requestMetadata', {
|
await dispatch('requestMetadata', [
|
||||||
filename: 'gcodes/' + payload.item.path,
|
{
|
||||||
})
|
filename: 'gcodes/' + payload.item.path,
|
||||||
|
},
|
||||||
|
])
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user