Skip to content

Commit c96db56

Browse files
authored
refactor: optimize queue job (#4859)
* refactor: optimize queue job * fix: gemini suggestion and test * refactor: cancelScheduleJob and SCHEDULE_MODE constant
1 parent 3faf830 commit c96db56

File tree

2 files changed

+164
-49
lines changed

2 files changed

+164
-49
lines changed

__tests__/renderer/queueJob.spec.ts

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ describe('JobQueue', () => {
195195
jobQueue['isFlushing'] = true
196196
jobQueue['isFlushPending'] = true
197197
jobQueue['scheduleId'] = 1
198+
jobQueue['scheduleMode'] = 'idle'
198199

199200
jobQueue.clearJobs()
200201

@@ -293,7 +294,7 @@ describe('JobQueue', () => {
293294
})
294295

295296
it('should handle errors in job callbacks', () => {
296-
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
297+
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
297298
const error = new Error('Test error')
298299
const job: Job = {
299300
id: '1',
@@ -379,6 +380,96 @@ describe('JobQueue', () => {
379380
})
380381
})
381382

383+
// Additional tests aligned with current JobQueue behavior
384+
describe('queueJob - dedup and priority reorder', () => {
385+
it('deduplicates by id and updates callback without growing queue', () => {
386+
const cb1 = vi.fn()
387+
const cb2 = vi.fn()
388+
const job1: Job = { id: 'dup', priority: JOB_PRIORITY.Update, cb: cb1 }
389+
const job2: Job = { id: 'dup', priority: JOB_PRIORITY.Update, cb: cb2 }
390+
391+
jobQueue.queueJob(job1)
392+
jobQueue.queueJob(job2)
393+
394+
expect(jobQueue['queue'].length).toBe(1)
395+
expect(jobQueue['queue'][0].cb).toBe(cb2)
396+
397+
jobQueue.flushJobsSync()
398+
expect(cb1).not.toHaveBeenCalled()
399+
expect(cb2).toHaveBeenCalled()
400+
})
401+
402+
it('re-orders existing job when its priority changes', () => {
403+
const cb = vi.fn()
404+
const edgeJob: Job = {
405+
id: 'e',
406+
priority: JOB_PRIORITY.RenderEdge,
407+
cb: vi.fn(),
408+
}
409+
const updateJob: Job = { id: 'u', priority: JOB_PRIORITY.Update, cb }
410+
411+
jobQueue.queueJob(edgeJob)
412+
jobQueue.queueJob(updateJob)
413+
414+
const upgraded: Job = { id: 'u', priority: JOB_PRIORITY.RenderNode, cb }
415+
jobQueue.queueJob(upgraded)
416+
417+
expect(jobQueue['queue'][0].id).toBe('u')
418+
expect(jobQueue['queue'][0].priority).toBe(JOB_PRIORITY.RenderNode)
419+
expect(jobQueue['queue'][1].id).toBe('e')
420+
})
421+
})
422+
423+
describe('flushJobs - idle deadline budget', () => {
424+
it('caps budget using idle deadline timeRemaining', () => {
425+
const mockCb1 = vi.fn()
426+
const mockCb2 = vi.fn()
427+
jobQueue.queueJob({ id: '1', priority: JOB_PRIORITY.Update, cb: mockCb1 })
428+
jobQueue.queueJob({ id: '2', priority: JOB_PRIORITY.Update, cb: mockCb2 })
429+
430+
const fakeDeadline = { timeRemaining: () => 5 } as any
431+
mockPerformanceNow
432+
.mockImplementationOnce(() => 0)
433+
.mockImplementationOnce(() => 6)
434+
435+
jobQueue.flushJobs(fakeDeadline)
436+
437+
expect(mockCb1).toHaveBeenCalled()
438+
expect(mockCb2).not.toHaveBeenCalled()
439+
expect(jobQueue['isFlushPending']).toBe(true)
440+
expect(jobQueue['isFlushing']).toBe(false)
441+
})
442+
})
443+
444+
describe('cancelScheduleJob - modes', () => {
445+
it('cancels RAF schedule when mode is raf', () => {
446+
let cancelRAFSpy: any
447+
if ('cancelAnimationFrame' in window) {
448+
cancelRAFSpy = vi.spyOn(window, 'cancelAnimationFrame')
449+
} else {
450+
const mock = vi.fn()
451+
Object.defineProperty(window, 'cancelAnimationFrame', { value: mock })
452+
cancelRAFSpy = vi.spyOn(window, 'cancelAnimationFrame' as any)
453+
}
454+
455+
jobQueue['scheduleMode'] = 'raf'
456+
jobQueue['scheduleId'] = 1
457+
458+
jobQueue.clearJobs()
459+
460+
expect(cancelRAFSpy).toHaveBeenCalled()
461+
})
462+
463+
it('cancels timeout schedule when mode is timeout', () => {
464+
jobQueue['scheduleMode'] = 'timeout'
465+
jobQueue['scheduleId'] = 1
466+
467+
jobQueue.clearJobs()
468+
469+
expect(mockClearTimeout).toHaveBeenCalled()
470+
})
471+
})
472+
382473
describe('getCurrentTime', () => {
383474
it('should use performance.now if available', () => {
384475
mockPerformanceNow.mockReturnValue(1000)

src/renderer/queueJob.ts

Lines changed: 72 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,42 @@
1+
const SCHEDULE_MODE = {
2+
idle: 'idle',
3+
raf: 'raf',
4+
timeout: 'timeout',
5+
} as const
6+
7+
type ScheduleMode = (typeof SCHEDULE_MODE)[keyof typeof SCHEDULE_MODE]
8+
19
export class JobQueue {
210
private isFlushing = false
311
private isFlushPending = false
412
private scheduleId = 0
513
private queue: Job[] = []
6-
private frameInterval = 33
14+
private frameInterval = 16
715
private initialTime = Date.now()
16+
private pendingJobs = new Map<string, Job>()
17+
private scheduleMode: ScheduleMode | null = null
818

919
queueJob(job: Job) {
1020
if (job.priority & JOB_PRIORITY.PRIOR) {
1121
job.cb()
1222
} else {
13-
const index = this.findInsertionIndex(job)
14-
if (index >= 0) {
23+
const existing = this.pendingJobs.get(job.id)
24+
if (existing) {
25+
// 仅更新已有任务的回调与优先级
26+
existing.cb = job.cb
27+
if (job.priority !== existing.priority) {
28+
existing.priority = job.priority
29+
const idx = this.queue.indexOf(existing)
30+
if (idx >= 0) {
31+
this.queue.splice(idx, 1)
32+
const newIndex = this.findInsertionIndex(existing)
33+
this.queue.splice(newIndex, 0, existing)
34+
}
35+
}
36+
} else {
37+
const index = this.findInsertionIndex(job)
1538
this.queue.splice(index, 0, job)
39+
this.pendingJobs.set(job.id, job)
1640
}
1741
}
1842
}
@@ -33,21 +57,29 @@ export class JobQueue {
3357

3458
clearJobs() {
3559
this.queue.length = 0
60+
this.pendingJobs.clear()
3661
this.isFlushing = false
3762
this.isFlushPending = false
3863
this.cancelScheduleJob()
3964
}
4065

41-
flushJobs() {
66+
flushJobs(deadline?: IdleDeadline) {
4267
this.isFlushPending = false
4368
this.isFlushing = true
4469

4570
const startTime = this.getCurrentTime()
71+
let budget = this.frameInterval
72+
if (deadline && typeof deadline.timeRemaining === 'function') {
73+
const remain = deadline.timeRemaining()
74+
// 防止过长占用单帧
75+
budget = Math.max(0, Math.min(budget, remain))
76+
}
4677

47-
let job
48-
while ((job = this.queue.shift())) {
78+
while (this.queue.length > 0) {
79+
const job = this.queue.shift()!
4980
job.cb()
50-
if (this.getCurrentTime() - startTime >= this.frameInterval) {
81+
this.pendingJobs.delete(job.id)
82+
if (this.getCurrentTime() - startTime >= budget) {
5183
break
5284
}
5385
}
@@ -63,14 +95,14 @@ export class JobQueue {
6395
this.isFlushPending = false
6496
this.isFlushing = true
6597

66-
let job
67-
while ((job = this.queue.shift())) {
98+
while (this.queue.length > 0) {
99+
const job = this.queue.shift()!
68100
try {
69101
job.cb()
70102
} catch (error) {
71-
// eslint-disable-next-line
72-
console.log(error)
103+
console.error(error)
73104
}
105+
this.pendingJobs.delete(job.id)
74106
}
75107

76108
this.isFlushing = false
@@ -94,33 +126,42 @@ export class JobQueue {
94126
}
95127

96128
private scheduleJob() {
129+
if (this.scheduleId) {
130+
this.cancelScheduleJob()
131+
}
97132
if ('requestIdleCallback' in window) {
98-
if (this.scheduleId) {
99-
this.cancelScheduleJob()
100-
}
101-
this.scheduleId = window.requestIdleCallback(this.flushJobs.bind(this), {
102-
timeout: 100,
103-
})
133+
this.scheduleMode = SCHEDULE_MODE.idle
134+
this.scheduleId = window.requestIdleCallback(
135+
(deadline: IdleDeadline) => this.flushJobs(deadline),
136+
{
137+
timeout: 100,
138+
},
139+
)
140+
} else if ('requestAnimationFrame' in window) {
141+
this.scheduleMode = SCHEDULE_MODE.raf
142+
this.scheduleId = (window as Window).requestAnimationFrame(() =>
143+
this.flushJobs(),
144+
)
104145
} else {
105-
if (this.scheduleId) {
106-
this.cancelScheduleJob()
107-
}
108-
this.scheduleId = (window as Window).setTimeout(this.flushJobs.bind(this))
146+
this.scheduleMode = SCHEDULE_MODE.timeout
147+
this.scheduleId = (window as Window).setTimeout(() => this.flushJobs())
109148
}
110149
}
111150

112151
private cancelScheduleJob() {
113-
if ('cancelIdleCallback' in window) {
114-
if (this.scheduleId) {
115-
window.cancelIdleCallback(this.scheduleId)
116-
}
117-
this.scheduleId = 0
118-
} else {
119-
if (this.scheduleId) {
120-
clearTimeout(this.scheduleId)
121-
}
122-
this.scheduleId = 0
152+
if (!this.scheduleId) return
153+
const cancelMethods: Partial<Record<ScheduleMode, (id: number) => void>> = {
154+
[SCHEDULE_MODE.idle]: window?.cancelIdleCallback,
155+
[SCHEDULE_MODE.raf]: window?.cancelAnimationFrame,
156+
[SCHEDULE_MODE.timeout]: window?.clearTimeout,
157+
}
158+
const mode = this.scheduleMode
159+
const cancelMethod = mode ? cancelMethods[mode] : undefined
160+
if (typeof cancelMethod === 'function') {
161+
cancelMethod(this.scheduleId as number)
123162
}
163+
this.scheduleId = 0
164+
this.scheduleMode = null
124165
}
125166

126167
private getCurrentTime() {
@@ -145,20 +186,3 @@ export enum JOB_PRIORITY {
145186
RenderNode = /**/ 1 << 3,
146187
PRIOR = /* */ 1 << 20,
147188
}
148-
149-
// function findInsertionIndex(job: Job) {
150-
// let start = 0
151-
// for (let i = 0, len = queue.length; i < len; i += 1) {
152-
// const j = queue[i]
153-
// if (j.id === job.id) {
154-
// console.log('xx', j.bit, job.bit)
155-
// }
156-
// if (j.id === job.id && (job.bit ^ (job.bit & j.bit)) === 0) {
157-
// return -1
158-
// }
159-
// if (j.priority <= job.priority) {
160-
// start += 1
161-
// }
162-
// }
163-
// return start
164-
// }

0 commit comments

Comments
 (0)