Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 88e40a7

Browse files
committedFeb 4, 2024
perf: throttle queue + subscribe simplification
1 parent d0b5fea commit 88e40a7

File tree

8 files changed

+208
-150
lines changed

8 files changed

+208
-150
lines changed
 

‎packages/app-backend-core/src/component.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { stringify, BridgeEvents, parse, SharedData } from '@vue-devtools/shared-utils'
1+
import { stringify, BridgeEvents, parse, SharedData, createThrottleQueue } from '@vue-devtools/shared-utils'
22
import { AppRecord, BackendContext, BuiltinBackendFeature } from '@vue-devtools/app-backend-api'
33
import { getAppRecord } from './app'
44
import { App, ComponentInstance, EditStatePayload } from '@vue/devtools-api'
@@ -131,11 +131,16 @@ export async function refreshComponentTreeSearch (ctx: BackendContext) {
131131
await sendComponentTreeData(ctx.currentAppRecord, '_root', ctx.currentAppRecord.componentFilter, null, false, ctx)
132132
}
133133

134-
export async function sendComponentUpdateTracking (instanceId: string, ctx: BackendContext) {
134+
const updateTrackingQueue = createThrottleQueue(500)
135+
136+
export function sendComponentUpdateTracking (instanceId: string, time: number, ctx: BackendContext) {
135137
if (!instanceId) return
136-
const payload = {
137-
instanceId,
138-
time: Date.now(), // Use normal date
139-
}
140-
ctx.bridge.send(BridgeEvents.TO_FRONT_COMPONENT_UPDATED, payload)
138+
139+
updateTrackingQueue.add(instanceId, () => {
140+
const payload = {
141+
instanceId,
142+
time,
143+
}
144+
ctx.bridge.send(BridgeEvents.TO_FRONT_COMPONENT_UPDATED, payload)
145+
})
141146
}

‎packages/app-backend-core/src/index.ts

Lines changed: 136 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
SharedData,
2020
isBrowser,
2121
raf,
22+
createThrottleQueue,
2223
} from '@vue-devtools/shared-utils'
2324
import debounce from 'lodash/debounce'
2425
import throttle from 'lodash/throttle'
@@ -118,27 +119,13 @@ async function connect () {
118119

119120
// Components
120121

121-
const sendComponentUpdate = throttle(async (appRecord: AppRecord, id: string) => {
122-
try {
123-
// Update component inspector
124-
if (ctx.currentInspectedComponentId === id) {
125-
await sendSelectedComponentData(appRecord, ctx.currentInspectedComponentId, ctx)
126-
}
127-
128-
// Update tree (tags)
129-
if (isSubscribed(BridgeSubscriptions.COMPONENT_TREE, sub => sub.payload.instanceId === id)) {
130-
await sendComponentTreeData(appRecord, id, appRecord.componentFilter, 0, false, ctx)
131-
}
132-
} catch (e) {
133-
if (SharedData.debugInfo) {
134-
console.error(e)
135-
}
136-
}
137-
}, 100)
122+
const throttleQueue = createThrottleQueue(500)
138123

139124
hook.on(HookEvents.COMPONENT_UPDATED, async (app, uid, parentUid, component) => {
140125
try {
141126
if (!app || (typeof uid !== 'number' && !uid) || !component) return
127+
const now = Date.now()
128+
142129
let id: string
143130
let appRecord: AppRecord
144131
if (app && uid != null) {
@@ -149,15 +136,40 @@ async function connect () {
149136
appRecord = ctx.currentAppRecord
150137
}
151138

152-
if (SharedData.trackUpdates) {
153-
await sendComponentUpdateTracking(id, ctx)
154-
}
139+
throttleQueue.add(`update:${id}`, async () => {
140+
try {
141+
const time = performance.now()
155142

156-
if (SharedData.flashUpdates) {
157-
await flashComponent(component, appRecord.backend)
158-
}
143+
const trackUpdateNow = performance.now()
144+
if (SharedData.trackUpdates) {
145+
sendComponentUpdateTracking(id, now, ctx)
146+
}
147+
const trackUpdateTime = performance.now() - trackUpdateNow
148+
149+
const flashNow = performance.now()
150+
if (SharedData.flashUpdates) {
151+
await flashComponent(component, appRecord.backend)
152+
}
153+
const flashTime = performance.now() - flashNow
159154

160-
await sendComponentUpdate(appRecord, id)
155+
const sendUpdateNow = performance.now()
156+
// Update component inspector
157+
if (ctx.currentInspectedComponentId === id) {
158+
await sendSelectedComponentData(appRecord, ctx.currentInspectedComponentId, ctx)
159+
}
160+
161+
// Update tree (tags)
162+
if (isSubscribed(BridgeSubscriptions.COMPONENT_TREE, id)) {
163+
await sendComponentTreeData(appRecord, id, appRecord.componentFilter, 0, false, ctx)
164+
}
165+
const sendUpdateTime = performance.now() - sendUpdateNow
166+
// console.log('COMPONENT_UPDATED', id, Math.round(performance.now() - time) + 'ms', { trackUpdateTime, flashTime, sendUpdateTime, updatedData: ctx.currentInspectedComponentId === id })
167+
} catch (e) {
168+
if (SharedData.debugInfo) {
169+
console.error(e)
170+
}
171+
}
172+
})
161173
} catch (e) {
162174
if (SharedData.debugInfo) {
163175
console.error(e)
@@ -168,51 +180,63 @@ async function connect () {
168180
hook.on(HookEvents.COMPONENT_ADDED, async (app, uid, parentUid, component) => {
169181
try {
170182
if (!app || (typeof uid !== 'number' && !uid) || !component) return
183+
const now = Date.now()
171184
const id = await getComponentId(app, uid, component, ctx)
172-
const appRecord = await getAppRecord(app, ctx)
173-
if (component) {
174-
if (component.__VUE_DEVTOOLS_UID__ == null) {
175-
component.__VUE_DEVTOOLS_UID__ = id
176-
}
177-
if (appRecord?.instanceMap) {
178-
if (!appRecord.instanceMap.has(id)) {
179-
appRecord.instanceMap.set(id, component)
180-
}
181-
}
182-
}
183185

184-
if (parentUid != null && appRecord?.instanceMap) {
185-
const parentInstances = await appRecord.backend.api.walkComponentParents(component)
186-
if (parentInstances.length) {
187-
// Check two parents level to update `hasChildren
188-
for (let i = 0; i < parentInstances.length; i++) {
189-
const parentId = await getComponentId(app, parentUid, parentInstances[i], ctx)
190-
if (i < 2 && isSubscribed(BridgeSubscriptions.COMPONENT_TREE, sub => sub.payload.instanceId === parentId)) {
191-
raf(() => {
192-
sendComponentTreeData(appRecord, parentId, appRecord.componentFilter, null, false, ctx)
193-
})
186+
throttleQueue.add(`add:${id}`, async () => {
187+
try {
188+
const time = performance.now()
189+
const appRecord = await getAppRecord(app, ctx)
190+
if (component) {
191+
if (component.__VUE_DEVTOOLS_UID__ == null) {
192+
component.__VUE_DEVTOOLS_UID__ = id
193+
}
194+
if (appRecord?.instanceMap) {
195+
if (!appRecord.instanceMap.has(id)) {
196+
appRecord.instanceMap.set(id, component)
197+
}
194198
}
199+
}
200+
201+
if (parentUid != null && appRecord?.instanceMap) {
202+
const parentInstances = await appRecord.backend.api.walkComponentParents(component)
203+
if (parentInstances.length) {
204+
// Check two parents level to update `hasChildren
205+
for (let i = 0; i < parentInstances.length; i++) {
206+
const parentId = await getComponentId(app, parentUid, parentInstances[i], ctx)
207+
if (i < 2 && isSubscribed(BridgeSubscriptions.COMPONENT_TREE, parentId)) {
208+
raf(() => {
209+
sendComponentTreeData(appRecord, parentId, appRecord.componentFilter, null, false, ctx)
210+
})
211+
}
195212

196-
if (SharedData.trackUpdates) {
197-
await sendComponentUpdateTracking(parentId, ctx)
213+
if (SharedData.trackUpdates) {
214+
sendComponentUpdateTracking(parentId, now, ctx)
215+
}
216+
}
198217
}
199218
}
200-
}
201-
}
202219

203-
if (ctx.currentInspectedComponentId === id) {
204-
await sendSelectedComponentData(appRecord, id, ctx)
205-
}
220+
if (ctx.currentInspectedComponentId === id) {
221+
await sendSelectedComponentData(appRecord, id, ctx)
222+
}
206223

207-
if (SharedData.trackUpdates) {
208-
await sendComponentUpdateTracking(id, ctx)
209-
}
224+
if (SharedData.trackUpdates) {
225+
sendComponentUpdateTracking(id, now, ctx)
226+
}
210227

211-
if (SharedData.flashUpdates) {
212-
await flashComponent(component, appRecord.backend)
213-
}
228+
if (SharedData.flashUpdates) {
229+
await flashComponent(component, appRecord.backend)
230+
}
214231

215-
await refreshComponentTreeSearch(ctx)
232+
await refreshComponentTreeSearch(ctx)
233+
// console.log('COMPONENT_ADDED', id, Math.round(performance.now() - time) + 'ms')
234+
} catch (e) {
235+
if (SharedData.debugInfo) {
236+
console.error(e)
237+
}
238+
}
239+
})
216240
} catch (e) {
217241
if (SharedData.debugInfo) {
218242
console.error(e)
@@ -223,39 +247,50 @@ async function connect () {
223247
hook.on(HookEvents.COMPONENT_REMOVED, async (app, uid, parentUid, component) => {
224248
try {
225249
if (!app || (typeof uid !== 'number' && !uid) || !component) return
226-
const appRecord = await getAppRecord(app, ctx)
227-
if (parentUid != null && appRecord) {
228-
const parentInstances = await appRecord.backend.api.walkComponentParents(component)
229-
if (parentInstances.length) {
230-
const parentId = await getComponentId(app, parentUid, parentInstances[0], ctx)
231-
if (isSubscribed(BridgeSubscriptions.COMPONENT_TREE, sub => sub.payload.instanceId === parentId)) {
232-
raf(async () => {
233-
try {
234-
const appRecord = await getAppRecord(app, ctx)
235-
236-
if (appRecord) {
237-
sendComponentTreeData(appRecord, parentId, appRecord.componentFilter, null, false, ctx)
238-
}
239-
} catch (e) {
240-
if (SharedData.debugInfo) {
241-
console.error(e)
242-
}
250+
const id = await getComponentId(app, uid, component, ctx)
251+
252+
throttleQueue.add(`remove:${id}`, async () => {
253+
try {
254+
const time = performance.now()
255+
const appRecord = await getAppRecord(app, ctx)
256+
if (parentUid != null && appRecord) {
257+
const parentInstances = await appRecord.backend.api.walkComponentParents(component)
258+
if (parentInstances.length) {
259+
const parentId = await getComponentId(app, parentUid, parentInstances[0], ctx)
260+
if (isSubscribed(BridgeSubscriptions.COMPONENT_TREE, parentId)) {
261+
raf(async () => {
262+
try {
263+
const appRecord = await getAppRecord(app, ctx)
264+
265+
if (appRecord) {
266+
sendComponentTreeData(appRecord, parentId, appRecord.componentFilter, null, false, ctx)
267+
}
268+
} catch (e) {
269+
if (SharedData.debugInfo) {
270+
console.error(e)
271+
}
272+
}
273+
})
243274
}
244-
})
275+
}
245276
}
246-
}
247-
}
248277

249-
const id = await getComponentId(app, uid, component, ctx)
250-
if (isSubscribed(BridgeSubscriptions.SELECTED_COMPONENT_DATA, sub => sub.payload.instanceId === id)) {
251-
await sendEmptyComponentData(id, ctx)
252-
}
278+
if (isSubscribed(BridgeSubscriptions.SELECTED_COMPONENT_DATA, id)) {
279+
await sendEmptyComponentData(id, ctx)
280+
}
253281

254-
if (appRecord) {
255-
appRecord.instanceMap.delete(id)
256-
}
282+
if (appRecord) {
283+
appRecord.instanceMap.delete(id)
284+
}
257285

258-
await refreshComponentTreeSearch(ctx)
286+
await refreshComponentTreeSearch(ctx)
287+
// console.log('COMPONENT_REMOVED', id, Math.round(performance.now() - time) + 'ms')
288+
} catch (e) {
289+
if (SharedData.debugInfo) {
290+
console.error(e)
291+
}
292+
}
293+
})
259294
} catch (e) {
260295
if (SharedData.debugInfo) {
261296
console.error(e)
@@ -264,7 +299,7 @@ async function connect () {
264299
})
265300

266301
hook.on(HookEvents.TRACK_UPDATE, (id, ctx) => {
267-
sendComponentUpdateTracking(id, ctx)
302+
sendComponentUpdateTracking(id, Date.now(), ctx)
268303
})
269304

270305
hook.on(HookEvents.FLASH_UPDATE, (instance, backend) => {
@@ -273,22 +308,22 @@ async function connect () {
273308

274309
// Component perf
275310

276-
hook.on(HookEvents.PERFORMANCE_START, async (app, uid, vm, type, time) => {
277-
await performanceMarkStart(app, uid, vm, type, time, ctx)
311+
hook.on(HookEvents.PERFORMANCE_START, (app, uid, vm, type, time) => {
312+
performanceMarkStart(app, uid, vm, type, time, ctx)
278313
})
279314

280-
hook.on(HookEvents.PERFORMANCE_END, async (app, uid, vm, type, time) => {
281-
await performanceMarkEnd(app, uid, vm, type, time, ctx)
315+
hook.on(HookEvents.PERFORMANCE_END, (app, uid, vm, type, time) => {
316+
performanceMarkEnd(app, uid, vm, type, time, ctx)
282317
})
283318

284319
// Highlighter
285320

286-
hook.on(HookEvents.COMPONENT_HIGHLIGHT, async instanceId => {
287-
await highlight(ctx.currentAppRecord.instanceMap.get(instanceId), ctx.currentAppRecord.backend, ctx)
321+
hook.on(HookEvents.COMPONENT_HIGHLIGHT, instanceId => {
322+
highlight(ctx.currentAppRecord.instanceMap.get(instanceId), ctx.currentAppRecord.backend, ctx)
288323
})
289324

290-
hook.on(HookEvents.COMPONENT_UNHIGHLIGHT, async () => {
291-
await unHighlight()
325+
hook.on(HookEvents.COMPONENT_UNHIGHLIGHT, () => {
326+
unHighlight()
292327
})
293328

294329
// Timeline
@@ -409,12 +444,12 @@ async function connect () {
409444
function connectBridge () {
410445
// Subscriptions
411446

412-
ctx.bridge.on(BridgeEvents.TO_BACK_SUBSCRIBE, ({ type, payload }) => {
413-
subscribe(type, payload)
447+
ctx.bridge.on(BridgeEvents.TO_BACK_SUBSCRIBE, ({ type, key }) => {
448+
subscribe(type, key)
414449
})
415450

416-
ctx.bridge.on(BridgeEvents.TO_BACK_UNSUBSCRIBE, ({ type, payload }) => {
417-
unsubscribe(type, payload)
451+
ctx.bridge.on(BridgeEvents.TO_BACK_UNSUBSCRIBE, ({ type, key }) => {
452+
unsubscribe(type, key)
418453
})
419454

420455
// Tabs
@@ -450,7 +485,7 @@ function connectBridge () {
450485

451486
ctx.bridge.on(BridgeEvents.TO_BACK_COMPONENT_TREE, async ({ instanceId, filter, recursively }) => {
452487
ctx.currentAppRecord.componentFilter = filter
453-
subscribe(BridgeSubscriptions.COMPONENT_TREE, { instanceId })
488+
subscribe(BridgeSubscriptions.COMPONENT_TREE, instanceId)
454489
await sendComponentTreeData(ctx.currentAppRecord, instanceId, filter, null, recursively, ctx)
455490
})
456491

‎packages/app-backend-core/src/perf.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ export async function performanceMarkEnd (
145145
if (change) {
146146
// Update component tree
147147
const id = await getComponentId(app, uid, instance, ctx)
148-
if (isSubscribed(BridgeSubscriptions.COMPONENT_TREE, sub => sub.payload.instanceId === id)) {
148+
if (isSubscribed(BridgeSubscriptions.COMPONENT_TREE, id)) {
149149
raf(() => {
150150
sendComponentTreeData(appRecord, id, ctx.currentAppRecord.componentFilter, null, false, ctx)
151151
})
Lines changed: 8 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,26 @@
1-
interface Subscription {
2-
payload: any
3-
rawPayload: string
4-
}
5-
6-
const activeSubs: Map<string, Subscription[]> = new Map()
1+
const activeSubs: Map<string, Map<string, boolean>> = new Map()
72

83
function getSubs (type: string) {
94
let subs = activeSubs.get(type)
105
if (!subs) {
11-
subs = []
6+
subs = new Map()
127
activeSubs.set(type, subs)
138
}
149
return subs
1510
}
1611

17-
export function subscribe (type: string, payload: any) {
18-
const rawPayload = getRawPayload(payload)
19-
getSubs(type).push({
20-
payload,
21-
rawPayload,
22-
})
12+
export function subscribe (type: string, key: string) {
13+
getSubs(type).set(key, true)
2314
}
2415

25-
export function unsubscribe (type: string, payload: any) {
26-
const rawPayload = getRawPayload(payload)
16+
export function unsubscribe (type: string, key: string) {
2717
const subs = getSubs(type)
28-
let index: number
29-
while ((index = subs.findIndex(sub => sub.rawPayload === rawPayload)) !== -1) {
30-
subs.splice(index, 1)
31-
}
32-
}
33-
34-
function getRawPayload (payload: any) {
35-
const data = Object.keys(payload).sort().reduce((acc, key) => {
36-
acc[key] = payload[key]
37-
return acc
38-
}, {})
39-
return JSON.stringify(data)
18+
subs.delete(key)
4019
}
4120

4221
export function isSubscribed (
4322
type: string,
44-
predicate: (sub: Subscription) => boolean = () => true,
23+
key: string,
4524
) {
46-
return getSubs(type).some(predicate)
25+
return getSubs(type).has(key)
4726
}

‎packages/app-frontend/src/features/bridge/index.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ import { Bridge, BridgeEvents } from '@vue-devtools/shared-utils'
33

44
let bridge: Bridge
55

6+
interface Sub {
7+
type: string
8+
key: string
9+
}
10+
611
export function useBridge () {
712
const cbs = []
813

@@ -11,18 +16,18 @@ export function useBridge () {
1116
bridge.on(event, cb)
1217
}
1318

14-
const subs = []
19+
const subs: Sub[] = []
1520

16-
function subscribe (type: string, payload: any) {
17-
const sub = { type, payload }
21+
function subscribe (type: string, key: string) {
22+
const sub = { type, key }
1823
subs.push(sub)
19-
bridge.send(BridgeEvents.TO_BACK_SUBSCRIBE, sub)
24+
bridge.send(BridgeEvents.TO_BACK_SUBSCRIBE, key)
2025
return () => {
2126
const index = subs.indexOf(sub)
2227
if (index !== -1) {
2328
subs.splice(index, 1)
2429
}
25-
bridge.send(BridgeEvents.TO_BACK_UNSUBSCRIBE, sub)
30+
bridge.send(BridgeEvents.TO_BACK_UNSUBSCRIBE, key)
2631
}
2732
}
2833

@@ -32,7 +37,7 @@ export function useBridge () {
3237
}
3338

3439
for (const sub of subs) {
35-
bridge.send(BridgeEvents.TO_BACK_UNSUBSCRIBE, sub)
40+
bridge.send(BridgeEvents.TO_BACK_UNSUBSCRIBE, sub.key)
3641
}
3742
})
3843

‎packages/app-frontend/src/features/components/composable/components.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,7 @@ export function useComponents () {
8080
}
8181

8282
if (value != null) {
83-
unsub = subscribe(BridgeSubscriptions.SELECTED_COMPONENT_DATA, {
84-
instanceId: value,
85-
})
83+
unsub = subscribe(BridgeSubscriptions.SELECTED_COMPONENT_DATA, value)
8684
}
8785
}, {
8886
immediate: true,
@@ -168,9 +166,7 @@ export function useComponent (instance: Ref<ComponentTreeNode>) {
168166
}
169167

170168
if (value != null) {
171-
unsub = subscribe(BridgeSubscriptions.COMPONENT_TREE, {
172-
instanceId: value,
173-
})
169+
unsub = subscribe(BridgeSubscriptions.COMPONENT_TREE, value)
174170
}
175171
}, {
176172
immediate: true,

‎packages/shared-utils/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export * from './plugin-settings'
88
export * from './shared-data'
99
export * from './shell'
1010
export * from './storage'
11+
export * from './throttle'
1112
export * from './transfer'
1213
export * from './util'
1314
export * from './raf'

‎packages/shared-utils/src/throttle.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import throttle from 'lodash/throttle'
2+
3+
interface ThrottleQueueItem {
4+
fn: Function
5+
key: string
6+
}
7+
8+
export function createThrottleQueue (wait: number) {
9+
const queue: ThrottleQueueItem[] = []
10+
const tracker: Map<string, ThrottleQueueItem> = new Map()
11+
12+
function flush () {
13+
for (const item of queue) {
14+
item.fn()
15+
tracker.delete(item.key)
16+
}
17+
queue.length = 0
18+
}
19+
20+
const throttledFlush = throttle(flush, wait)
21+
22+
function add (key: string, fn: Function) {
23+
if (!tracker.has(key)) {
24+
const item = { key, fn }
25+
queue.push(item)
26+
tracker.set(key, item)
27+
throttledFlush()
28+
} else {
29+
const item = tracker.get(key)!
30+
item.fn = fn
31+
}
32+
}
33+
34+
return {
35+
add,
36+
}
37+
}

0 commit comments

Comments
 (0)
Please sign in to comment.