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[]) {
|
||||
const items = data.filter((file) => !file.isDirectory && !file.metadataRequested && !file.metadataPulled)
|
||||
items.forEach((file: FileStateGcodefile) => {
|
||||
this.$store.dispatch('files/requestMetadata', {
|
||||
this.$store.dispatch(
|
||||
'files/requestMetadata',
|
||||
items.map((file: FileStateGcodefile) => ({
|
||||
filename: 'gcodes' + this.currentPath + '/' + file.filename,
|
||||
})
|
||||
})
|
||||
}))
|
||||
)
|
||||
}
|
||||
|
||||
clickRow(item: FileStateGcodefile, force = false) {
|
||||
|
@@ -343,12 +343,12 @@ export default class StatusPanelGcodefiles extends Mixins(BaseMixin, ControlMixi
|
||||
const requestItems = gcodes.filter(
|
||||
(file: FileStateGcodefile) => !file.metadataRequested && !file.metadataPulled
|
||||
)
|
||||
requestItems.forEach((file: FileStateGcodefile) => {
|
||||
this.$store.dispatch('files/requestMetadata', {
|
||||
this.$store.dispatch(
|
||||
'files/requestMetadata',
|
||||
requestItems.map((file: FileStateGcodefile) => ({
|
||||
filename: 'gcodes/' + file.filename,
|
||||
})
|
||||
})
|
||||
|
||||
}))
|
||||
)
|
||||
return gcodes
|
||||
}
|
||||
|
||||
|
@@ -10,6 +10,7 @@ export class WebSocketClient {
|
||||
reconnectInterval = 1000
|
||||
reconnects = 0
|
||||
keepAliveTimeout = 1000
|
||||
messageId: number = 0
|
||||
timerId: number | null = null
|
||||
store: Store<RootState> | null = null
|
||||
waits: Wait[] = []
|
||||
@@ -25,6 +26,54 @@ export class WebSocketClient {
|
||||
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() {
|
||||
this.store?.dispatch('socket/setData', {
|
||||
isConnecting: true,
|
||||
@@ -58,55 +107,13 @@ export class WebSocketClient {
|
||||
if (this.store === null) return
|
||||
|
||||
const data = JSON.parse(msg.data)
|
||||
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 (Array.isArray(data)) {
|
||||
for (const message of data) {
|
||||
this.handleMessage(message)
|
||||
}
|
||||
|
||||
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
|
||||
} else {
|
||||
this.handleMessage(data)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
if (this.instance?.readyState !== WebSocket.OPEN) return
|
||||
|
||||
const id = Math.floor(Math.random() * 10000) + 1
|
||||
const id = this.messageId++
|
||||
this.waits.push({
|
||||
id: id,
|
||||
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 {
|
||||
@@ -169,6 +203,13 @@ export interface WebSocketClient {
|
||||
connect(): void
|
||||
close(): 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 {
|
||||
|
@@ -11,6 +11,7 @@ import { RootState } from '@/store/types'
|
||||
import i18n from '@/plugins/i18n'
|
||||
import { hiddenDirectories, validGcodeExtensions } from '@/store/variables'
|
||||
import axios from 'axios'
|
||||
import { BatchMessage } from '@/plugins/webSocketClient'
|
||||
|
||||
export const actions: ActionTree<FileState, RootState> = {
|
||||
reset({ commit }) {
|
||||
@@ -159,13 +160,26 @@ export const actions: ActionTree<FileState, RootState> = {
|
||||
}
|
||||
},
|
||||
|
||||
requestMetadata({ commit }, payload: { filename: string }) {
|
||||
const rootPath = payload.filename.slice(0, payload.filename.indexOf('/'))
|
||||
if (rootPath === 'gcodes') {
|
||||
const requestFilename = payload.filename.slice(7)
|
||||
commit('setMetadataRequested', { filename: requestFilename })
|
||||
Vue.$socket.emit('server.files.metadata', { filename: requestFilename }, { action: 'files/getMetadata' })
|
||||
requestMetadata({ commit }, payload: { filename: string }[]) {
|
||||
// request file metadata in batches to reduce the number of table re-renders when responses are received
|
||||
let messages: BatchMessage[] = []
|
||||
for (const { filename } of payload) {
|
||||
if (messages.length >= 100) {
|
||||
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) {
|
||||
@@ -203,9 +217,11 @@ export const actions: ActionTree<FileState, RootState> = {
|
||||
payload.item.root === 'gcodes' &&
|
||||
validGcodeExtensions.includes(payload.item.path.slice(payload.item.path.lastIndexOf('.')))
|
||||
) {
|
||||
await dispatch('requestMetadata', {
|
||||
filename: 'gcodes/' + payload.item.path,
|
||||
})
|
||||
await dispatch('requestMetadata', [
|
||||
{
|
||||
filename: 'gcodes/' + payload.item.path,
|
||||
},
|
||||
])
|
||||
}
|
||||
break
|
||||
|
||||
|
Reference in New Issue
Block a user