mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2026-07-28 08:55:50 +00:00
fix(editor): 修复 IdleTask 任务异常或 clearTasks 时队列卡住的问题
单个任务失败不再中断整批依赖收集,并在 runTaskQueue 中正确重置 taskHandle、始终通过实例队列消费任务。
This commit is contained in:
parent
a123747e9d
commit
3fe9cf9b66
@ -1,5 +1,7 @@
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
import { error } from '../logger';
|
||||
|
||||
export interface IdleTaskEvents {
|
||||
finish: [];
|
||||
'hight-level-finish': [];
|
||||
@ -64,7 +66,7 @@ export class IdleTask<T = any> extends EventEmitter {
|
||||
this.taskHandle = null;
|
||||
|
||||
this.emit('update-task-length', {
|
||||
length: this.taskList.length + this.hightLevelTaskList.length,
|
||||
length: this.getTaskLength(),
|
||||
hightLevelLength: this.hightLevelTaskList.length,
|
||||
});
|
||||
}
|
||||
@ -88,51 +90,80 @@ export class IdleTask<T = any> extends EventEmitter {
|
||||
}
|
||||
|
||||
private runTaskQueue(deadline: IdleDeadline) {
|
||||
const { hightLevelTaskList, taskList } = this;
|
||||
// 本次回调已触发,句柄随之失效;置空后 clearTasks / enqueueTask 才能正确判断是否需要重新调度
|
||||
this.taskHandle = null;
|
||||
|
||||
// 动画会占用空闲时间,当任务一直无法执行时,看看是否有动画正在播放
|
||||
// 根据空闲时间的多少来决定执行的任务数,保证页面不卡死的情况下尽量多执行任务,不然当任务数巨大时,执行时间会很久
|
||||
// 执行不完不会影响配置,但是会影响画布渲染
|
||||
while (deadline.timeRemaining() > 0 && (taskList.length || hightLevelTaskList.length)) {
|
||||
const timeRemaining = deadline.timeRemaining();
|
||||
let times = 0;
|
||||
if (timeRemaining <= 5) {
|
||||
times = 10;
|
||||
} else if (timeRemaining <= 10) {
|
||||
times = 100;
|
||||
} else if (timeRemaining <= 15) {
|
||||
times = 300;
|
||||
try {
|
||||
// 动画会占用空闲时间,当任务一直无法执行时,看看是否有动画正在播放
|
||||
// 根据空闲时间的多少来决定执行的任务数,保证页面不卡死的情况下尽量多执行任务,不然当任务数巨大时,执行时间会很久
|
||||
// 执行不完不会影响配置,但是会影响画布渲染
|
||||
while (deadline.timeRemaining() > 0 && this.getTaskLength()) {
|
||||
const timeRemaining = deadline.timeRemaining();
|
||||
let times = 0;
|
||||
if (timeRemaining <= 5) {
|
||||
times = 10;
|
||||
} else if (timeRemaining <= 10) {
|
||||
times = 100;
|
||||
} else if (timeRemaining <= 15) {
|
||||
times = 300;
|
||||
} else {
|
||||
times = 600;
|
||||
}
|
||||
|
||||
for (let i = 0; i < times; i++) {
|
||||
// 每次都从实例上取队列,任务执行过程中调用 clearTasks 能立即生效,不会继续消费已被清空的旧队列
|
||||
const task = this.hightLevelTaskList.length > 0 ? this.hightLevelTaskList.shift() : this.taskList.shift();
|
||||
if (task) {
|
||||
this.runTask(task);
|
||||
}
|
||||
|
||||
if (!this.getTaskLength()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
// 必须放在 finally 中:一旦这里被跳过,taskHandle 会一直是真值,
|
||||
// enqueueTask 也就不会再重新调度,剩余任务与任务数将永久停在当前状态
|
||||
this.finishRun();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 单个任务失败不能中断整个队列,否则后续任务永远不会被执行,
|
||||
* 依赖收集会停在半路(收集中状态与剩余任务数都不再变化)
|
||||
*/
|
||||
private runTask(task: TaskList<T>[number]) {
|
||||
try {
|
||||
task.handler(task.data);
|
||||
} catch (e) {
|
||||
error('magic editor: 空闲任务执行失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
private finishRun() {
|
||||
try {
|
||||
if (!this.hightLevelTaskList.length) {
|
||||
this.emit('hight-level-finish');
|
||||
}
|
||||
} finally {
|
||||
if (this.getTaskLength()) {
|
||||
// 任务执行或事件监听中可能已经重新调度过
|
||||
if (!this.taskHandle) {
|
||||
this.taskHandle = globalThis.requestIdleCallback(this.runTaskQueue.bind(this), { timeout: 300 });
|
||||
}
|
||||
} else {
|
||||
times = 600;
|
||||
this.emit('finish');
|
||||
}
|
||||
|
||||
for (let i = 0; i < times; i++) {
|
||||
const task = hightLevelTaskList.length > 0 ? hightLevelTaskList.shift() : taskList.shift();
|
||||
if (task) {
|
||||
task.handler(task.data);
|
||||
}
|
||||
|
||||
if (hightLevelTaskList.length === 0 && taskList.length === 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.emit('update-task-length', {
|
||||
length: this.getTaskLength(),
|
||||
hightLevelLength: this.hightLevelTaskList.length,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!hightLevelTaskList.length) {
|
||||
this.emit('hight-level-finish');
|
||||
}
|
||||
|
||||
if (hightLevelTaskList.length || taskList.length) {
|
||||
this.taskHandle = globalThis.requestIdleCallback(this.runTaskQueue.bind(this), { timeout: 300 });
|
||||
} else {
|
||||
this.taskHandle = 0;
|
||||
|
||||
this.emit('finish');
|
||||
}
|
||||
|
||||
this.emit('update-task-length', {
|
||||
length: taskList.length + hightLevelTaskList.length,
|
||||
hightLevelLength: hightLevelTaskList.length,
|
||||
});
|
||||
private getTaskLength() {
|
||||
return this.taskList.length + this.hightLevelTaskList.length;
|
||||
}
|
||||
}
|
||||
|
||||
@ -297,6 +297,29 @@ describe('Dep service', () => {
|
||||
expect(depService.get('collecting')).toBe(false);
|
||||
});
|
||||
|
||||
test('target 收集抛错时批次仍会结算,collecting 与 taskLength 复位', async () => {
|
||||
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
||||
depService.addTarget(
|
||||
new Target({
|
||||
id: 'throw-target',
|
||||
type: DepTargetType.DATA_SOURCE,
|
||||
isTarget: () => {
|
||||
throw new Error('boom');
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const nodes = Array.from({ length: 50 }, (_, i) => ({ id: `n${i}`, type: 'text', text: 'x' })) as any;
|
||||
await expect(depService.collectIdle(nodes, {}, false, DepTargetType.DATA_SOURCE)).resolves.toBe(true);
|
||||
|
||||
expect(depService.get('collecting')).toBe(false);
|
||||
// taskLength 的更新做了 1s 节流,等节流窗口结束后才会同步到 0
|
||||
await new Promise((resolve) => setTimeout(resolve, 1100));
|
||||
expect(depService.get('taskLength')).toBe(0);
|
||||
|
||||
errorSpy.mockRestore();
|
||||
});
|
||||
|
||||
test('destroy 会 reset 并移除监听', () => {
|
||||
depService.addTarget(makeTarget('destroy-me'));
|
||||
expect(() => depService.destroy()).not.toThrow();
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import { IdleTask } from '@editor/utils/dep/idle-task';
|
||||
import * as logger from '@editor/utils/logger';
|
||||
|
||||
const fakeIdleDeadline = (timeRemaining: number, callsBeforeZero = 1): IdleDeadline => {
|
||||
let remainingCalls = callsBeforeZero;
|
||||
@ -127,6 +128,77 @@ describe('IdleTask', () => {
|
||||
expect(scheduled.length).toBeGreaterThan(1);
|
||||
});
|
||||
|
||||
test('单个任务抛错不会中断整个队列', () => {
|
||||
const errorSpy = vi.spyOn(logger, 'error').mockImplementation(() => undefined);
|
||||
const task = new IdleTask<number>();
|
||||
const done: number[] = [];
|
||||
const finishHandler = vi.fn();
|
||||
task.on('finish', finishHandler);
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
task.enqueueTask((n) => {
|
||||
if (n === 3) throw new Error('boom');
|
||||
done.push(n);
|
||||
}, i);
|
||||
}
|
||||
|
||||
expect(() => scheduled[0].cb(fakeIdleDeadline(50))).not.toThrow();
|
||||
expect(done).toEqual([0, 1, 2, 4, 5, 6, 7, 8, 9]);
|
||||
expect(finishHandler).toHaveBeenCalled();
|
||||
expect(errorSpy).toHaveBeenCalled();
|
||||
errorSpy.mockRestore();
|
||||
});
|
||||
|
||||
test('任务抛错且仍有剩余任务时继续调度下一轮并同步任务数', () => {
|
||||
const errorSpy = vi.spyOn(logger, 'error').mockImplementation(() => undefined);
|
||||
const task = new IdleTask<number>();
|
||||
const updateHandler = vi.fn();
|
||||
task.on('update-task-length', updateHandler);
|
||||
|
||||
for (let i = 0; i < 200; i++) {
|
||||
task.enqueueTask((n) => {
|
||||
if (n === 0) throw new Error('boom');
|
||||
}, i);
|
||||
}
|
||||
|
||||
// 单批最多执行 10 个,执行完仍有剩余任务
|
||||
scheduled[0].cb(fakeIdleDeadline(3, 1));
|
||||
|
||||
expect(scheduled.length).toBeGreaterThan(1);
|
||||
expect(updateHandler).toHaveBeenCalledWith({ length: 190, hightLevelLength: 0 });
|
||||
errorSpy.mockRestore();
|
||||
});
|
||||
|
||||
test('任务执行过程中 clearTasks 会立即停止消费已清空的队列', () => {
|
||||
const task = new IdleTask<number>();
|
||||
const handler = vi.fn((n: number) => {
|
||||
if (n === 0) {
|
||||
task.clearTasks();
|
||||
}
|
||||
});
|
||||
for (let i = 0; i < 200; i++) task.enqueueTask(handler, i);
|
||||
|
||||
scheduled[0].cb(fakeIdleDeadline(50));
|
||||
|
||||
expect(handler).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('任务执行过程中清空并重新入队,新任务仍会被执行', () => {
|
||||
const task = new IdleTask<number>();
|
||||
const afterHandler = vi.fn();
|
||||
task.enqueueTask((n) => {
|
||||
if (n === 0) {
|
||||
task.clearTasks();
|
||||
task.enqueueTask(afterHandler, 999);
|
||||
}
|
||||
}, 0);
|
||||
for (let i = 1; i < 200; i++) task.enqueueTask(() => undefined, i);
|
||||
|
||||
scheduled[0].cb(fakeIdleDeadline(50));
|
||||
|
||||
expect(afterHandler).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('clearTasks - 取消挂起任务并重置队列', () => {
|
||||
const task = new IdleTask<number>();
|
||||
task.enqueueTask(() => undefined, 1);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user