You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
234 lines
7.3 KiB
234 lines
7.3 KiB
import { director, Director } from 'cc';
|
|
import { Singleton } from '../../tools/Singleton';
|
|
import { LoadPriority, LoadTask, ScheduledTask } from './LoadPolicy';
|
|
|
|
const PRIORITY_ORDER: LoadPriority[] = ['critical', 'interactive', 'background', 'silent'];
|
|
|
|
const BUDGET_MS: Record<Exclude<LoadPriority, 'silent'>, number> = {
|
|
critical: 16,
|
|
interactive: 8,
|
|
background: 2,
|
|
};
|
|
|
|
/**
|
|
* 分优先级任务调度器。
|
|
* - critical / interactive:单 worker + 帧预算(保交互)
|
|
* - background:单 inflight,挂主循环
|
|
* - silent:定时器泵,不占 AFTER_UPDATE;用户加载期间停发
|
|
*/
|
|
export class LoadScheduler extends Singleton {
|
|
private _nextId = 1;
|
|
private _queues: Record<LoadPriority, ScheduledTask[]> = {
|
|
critical: [],
|
|
interactive: [],
|
|
background: [],
|
|
silent: [],
|
|
};
|
|
private _running = false;
|
|
private _backgroundInflight = false;
|
|
private _silentInflight = 0;
|
|
private _silentMaxConcurrent = 1;
|
|
private _silentBudgetMs = 4;
|
|
private _silentGapMs = 50;
|
|
private _silentPaused = false;
|
|
private _silentSlowdown = false;
|
|
private _silentBudgetScale = 1;
|
|
private _backgroundBudgetScale = 1;
|
|
private _userLoadCount = 0;
|
|
private _silentTimer: ReturnType<typeof setTimeout> | null = null;
|
|
private _backgroundAllowed: () => boolean = () => true;
|
|
private _boundTick: () => void;
|
|
private _started = false;
|
|
|
|
constructor() {
|
|
super();
|
|
this._boundTick = this._tick.bind(this);
|
|
}
|
|
|
|
start(): void {
|
|
if (this._started) return;
|
|
this._started = true;
|
|
director.on(Director.EVENT_AFTER_UPDATE, this._boundTick);
|
|
this._armSilentTimer();
|
|
}
|
|
|
|
stop(): void {
|
|
if (!this._started) return;
|
|
this._started = false;
|
|
director.off(Director.EVENT_AFTER_UPDATE, this._boundTick);
|
|
this._clearSilentTimer();
|
|
}
|
|
|
|
/** 玩家点开页面 / 进战:停发新的 silent */
|
|
beginUserLoad(): void {
|
|
this._userLoadCount++;
|
|
}
|
|
|
|
/** 与 beginUserLoad 配对 */
|
|
endUserLoad(): void {
|
|
if (this._userLoadCount <= 0) return;
|
|
this._userLoadCount--;
|
|
}
|
|
|
|
/** 恢复失败:清掉未配对的用户加载门闩 */
|
|
resetUserLoad(): void {
|
|
this._userLoadCount = 0;
|
|
}
|
|
|
|
/** 是否有用户可见加载 */
|
|
isUserLoadActive(): boolean {
|
|
return this._userLoadCount > 0;
|
|
}
|
|
|
|
/** 丢弃未开始的 silent(进行中的原生 loadBundle 无法取消) */
|
|
clearSilentQueue(): void {
|
|
const queue = this._queues.silent;
|
|
while (queue.length > 0) {
|
|
const item = queue.shift();
|
|
if (item) item.resolve();
|
|
}
|
|
}
|
|
|
|
setSilentPaused(paused: boolean): void {
|
|
this._silentPaused = paused;
|
|
}
|
|
|
|
setSilentSlowdown(slow: boolean): void {
|
|
this._silentSlowdown = slow;
|
|
}
|
|
|
|
setSilentMaxConcurrent(n: number): void {
|
|
if (!Number.isFinite(n)) return;
|
|
this._silentMaxConcurrent = Math.max(1, Math.min(8, Math.floor(n)));
|
|
}
|
|
|
|
setSilentBudgetMs(ms: number): void {
|
|
if (!Number.isFinite(ms)) return;
|
|
this._silentBudgetMs = Math.max(1, Math.min(32, ms));
|
|
}
|
|
|
|
/** silent 定时器间隔(毫秒),与主循环无关 */
|
|
setSilentGapMs(ms: number): void {
|
|
if (!Number.isFinite(ms)) return;
|
|
this._silentGapMs = Math.max(16, Math.min(500, ms));
|
|
}
|
|
|
|
setSilentBudgetScale(scale: number): void {
|
|
this._silentBudgetScale = this._clampScale(scale);
|
|
}
|
|
|
|
setBackgroundBudgetScale(scale: number): void {
|
|
this._backgroundBudgetScale = this._clampScale(scale);
|
|
}
|
|
|
|
private _clampScale(scale: number): number {
|
|
if (!Number.isFinite(scale)) return 1;
|
|
return Math.max(0.25, Math.min(1, scale));
|
|
}
|
|
|
|
setBackgroundAllowed(allowed: () => boolean): void {
|
|
this._backgroundAllowed = allowed;
|
|
}
|
|
|
|
enqueue(priority: LoadPriority, task: LoadTask): Promise<void> {
|
|
return new Promise<void>((resolve, reject) => {
|
|
const item: ScheduledTask = {
|
|
id: this._nextId++,
|
|
priority,
|
|
run: task,
|
|
resolve,
|
|
reject,
|
|
};
|
|
this._queues[priority].push(item);
|
|
});
|
|
}
|
|
|
|
private _tick(): void {
|
|
if (!this._running) {
|
|
for (const priority of PRIORITY_ORDER) {
|
|
if (priority === 'silent') break;
|
|
if (priority === 'background') {
|
|
if (this._backgroundInflight || !this._backgroundAllowed()) continue;
|
|
const queue = this._queues.background;
|
|
if (queue.length === 0) continue;
|
|
const item = queue.shift()!;
|
|
this._backgroundInflight = true;
|
|
void item.run()
|
|
.then(() => item.resolve())
|
|
.catch((e) => item.reject(e))
|
|
.finally(() => {
|
|
this._backgroundInflight = false;
|
|
});
|
|
return;
|
|
}
|
|
const queue = this._queues[priority];
|
|
if (queue.length === 0) continue;
|
|
void this._drainQueue(queue, BUDGET_MS[priority]);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
private _armSilentTimer(): void {
|
|
if (this._silentTimer != null || !this._started) return;
|
|
this._silentTimer = setTimeout(this._onSilentTimer, this._silentGapMs);
|
|
}
|
|
|
|
private _clearSilentTimer(): void {
|
|
if (this._silentTimer == null) return;
|
|
clearTimeout(this._silentTimer);
|
|
this._silentTimer = null;
|
|
}
|
|
|
|
private _onSilentTimer = (): void => {
|
|
this._silentTimer = null;
|
|
this._pumpSilent();
|
|
this._armSilentTimer();
|
|
};
|
|
|
|
/** silent 独立并行池:定时器驱动,用户加载时停发 */
|
|
private _pumpSilent(): void {
|
|
if (this._silentPaused || this._userLoadCount > 0) return;
|
|
if (this._silentSlowdown && this._queues.interactive.length > 0) return;
|
|
|
|
const queue = this._queues.silent;
|
|
if (queue.length === 0) return;
|
|
|
|
const budget = this._silentBudgetMs
|
|
* this._silentBudgetScale
|
|
* (this._silentSlowdown ? 0.5 : 1);
|
|
const deadline = performance.now() + budget;
|
|
let started = 0;
|
|
|
|
while (
|
|
queue.length > 0
|
|
&& this._silentInflight < this._silentMaxConcurrent
|
|
&& (started === 0 || performance.now() < deadline)
|
|
) {
|
|
const item = queue.shift()!;
|
|
this._silentInflight++;
|
|
started++;
|
|
void item.run()
|
|
.then(() => item.resolve())
|
|
.catch((e) => item.reject(e))
|
|
.finally(() => {
|
|
this._silentInflight--;
|
|
});
|
|
}
|
|
}
|
|
|
|
private async _drainQueue(queue: ScheduledTask[], budgetMs: number): Promise<void> {
|
|
this._running = true;
|
|
const deadline = performance.now() + budgetMs;
|
|
while (queue.length > 0 && performance.now() < deadline) {
|
|
const item = queue.shift()!;
|
|
try {
|
|
await item.run();
|
|
item.resolve();
|
|
} catch (e) {
|
|
item.reject(e);
|
|
}
|
|
}
|
|
this._running = false;
|
|
}
|
|
}
|
|
|