mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-22 00:42:47 +08:00
⚡️ perf(window): 降低空闲状态原生轮询频率
This commit is contained in:
@@ -1,14 +1,182 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
installNativeWindowActivityScheduler,
|
||||
resolveTitleBarToggleIconKey,
|
||||
resolveWindowsScaleCheckDelayMs,
|
||||
shouldApplyWindowsScaleFix,
|
||||
shouldResetWebViewZoomForScaleFix,
|
||||
shouldToggleMaximisedWindowForScaleFix,
|
||||
WINDOW_STATE_FALLBACK_INTERVAL_MS,
|
||||
WINDOWS_SCALE_FALLBACK_INTERVAL_MS,
|
||||
} from './windowStateUi';
|
||||
|
||||
class FakeWindowActivityTarget {
|
||||
private readonly listeners = new Map<string, Set<EventListener>>();
|
||||
private readonly intervals = new Map<number, TimerHandler>();
|
||||
private nextTimerId = 1;
|
||||
|
||||
readonly listenerCapture = new Map<string, boolean>();
|
||||
readonly intervalDelays: number[] = [];
|
||||
|
||||
addEventListener(type: string, listener: EventListener, capture = false) {
|
||||
const listeners = this.listeners.get(type) ?? new Set<EventListener>();
|
||||
listeners.add(listener);
|
||||
this.listeners.set(type, listeners);
|
||||
this.listenerCapture.set(type, capture);
|
||||
}
|
||||
|
||||
removeEventListener(type: string, listener: EventListener) {
|
||||
this.listeners.get(type)?.delete(listener);
|
||||
}
|
||||
|
||||
setInterval(handler: TimerHandler, delayMs?: number) {
|
||||
const timerId = this.nextTimerId++;
|
||||
this.intervals.set(timerId, handler);
|
||||
this.intervalDelays.push(Number(delayMs));
|
||||
return timerId;
|
||||
}
|
||||
|
||||
clearInterval(timerId: number) {
|
||||
this.intervals.delete(timerId);
|
||||
}
|
||||
|
||||
dispatch(type: string) {
|
||||
for (const listener of this.listeners.get(type) ?? []) {
|
||||
listener(new Event(type));
|
||||
}
|
||||
}
|
||||
|
||||
tickFallbacks() {
|
||||
for (const handler of this.intervals.values()) {
|
||||
if (typeof handler === 'function') handler();
|
||||
}
|
||||
}
|
||||
|
||||
get activeIntervalCount() {
|
||||
return this.intervals.size;
|
||||
}
|
||||
}
|
||||
|
||||
class FakeDocumentActivityTarget {
|
||||
visibilityState: DocumentVisibilityState = 'visible';
|
||||
private readonly listeners = new Map<string, Set<EventListener>>();
|
||||
|
||||
addEventListener(type: string, listener: EventListener) {
|
||||
const listeners = this.listeners.get(type) ?? new Set<EventListener>();
|
||||
listeners.add(listener);
|
||||
this.listeners.set(type, listeners);
|
||||
}
|
||||
|
||||
removeEventListener(type: string, listener: EventListener) {
|
||||
this.listeners.get(type)?.delete(listener);
|
||||
}
|
||||
|
||||
dispatch(type: string) {
|
||||
for (const listener of this.listeners.get(type) ?? []) {
|
||||
listener(new Event(type));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe('windowStateUi', () => {
|
||||
it('keeps 10-30 seconds of visible idle native-window fallback IPC within budget', () => {
|
||||
const normalWindowStateCallsPerTick = 4;
|
||||
const windowsScaleCallsPerTick = 1;
|
||||
const countNativeIpcCalls = (idleDurationMs: number) => (
|
||||
Math.floor(idleDurationMs / WINDOW_STATE_FALLBACK_INTERVAL_MS) * normalWindowStateCallsPerTick
|
||||
+ Math.floor(idleDurationMs / WINDOWS_SCALE_FALLBACK_INTERVAL_MS) * windowsScaleCallsPerTick
|
||||
);
|
||||
|
||||
expect(countNativeIpcCalls(10_000)).toBe(1);
|
||||
expect(countNativeIpcCalls(30_000)).toBe(11);
|
||||
});
|
||||
|
||||
it('uses activity events while suppressing native fallback work on hidden pages', () => {
|
||||
const windowTarget = new FakeWindowActivityTarget();
|
||||
const documentTarget = new FakeDocumentActivityTarget();
|
||||
const onResize = vi.fn();
|
||||
const onFocus = vi.fn();
|
||||
const onPageShow = vi.fn();
|
||||
const onVisibilityChange = vi.fn();
|
||||
const onPageHide = vi.fn();
|
||||
const onBeforeUnload = vi.fn();
|
||||
const onFallback = vi.fn();
|
||||
|
||||
const cleanup = installNativeWindowActivityScheduler({
|
||||
windowTarget: windowTarget as unknown as Window,
|
||||
documentTarget: documentTarget as unknown as Document,
|
||||
fallbackIntervalMs: WINDOW_STATE_FALLBACK_INTERVAL_MS,
|
||||
onFallback,
|
||||
handlers: {
|
||||
resize: onResize,
|
||||
focus: onFocus,
|
||||
pageshow: onPageShow,
|
||||
pagehide: onPageHide,
|
||||
beforeunload: onBeforeUnload,
|
||||
visibilitychange: onVisibilityChange,
|
||||
},
|
||||
});
|
||||
expect(windowTarget.intervalDelays).toEqual([WINDOW_STATE_FALLBACK_INTERVAL_MS]);
|
||||
|
||||
windowTarget.dispatch('resize');
|
||||
windowTarget.dispatch('focus');
|
||||
windowTarget.dispatch('pageshow');
|
||||
documentTarget.dispatch('visibilitychange');
|
||||
windowTarget.dispatch('pagehide');
|
||||
windowTarget.dispatch('beforeunload');
|
||||
expect([onResize, onFocus, onPageShow, onVisibilityChange, onPageHide, onBeforeUnload]
|
||||
.map((handler) => handler.mock.calls.length))
|
||||
.toEqual([1, 1, 1, 1, 1, 1]);
|
||||
expect(windowTarget.listenerCapture.get('pagehide')).toBe(true);
|
||||
expect(windowTarget.listenerCapture.get('beforeunload')).toBe(true);
|
||||
|
||||
documentTarget.visibilityState = 'hidden';
|
||||
documentTarget.dispatch('visibilitychange');
|
||||
expect(windowTarget.activeIntervalCount).toBe(0);
|
||||
windowTarget.tickFallbacks();
|
||||
expect(onFallback).not.toHaveBeenCalled();
|
||||
|
||||
documentTarget.visibilityState = 'visible';
|
||||
documentTarget.dispatch('visibilitychange');
|
||||
expect(windowTarget.activeIntervalCount).toBe(1);
|
||||
windowTarget.tickFallbacks();
|
||||
expect(onFallback).toHaveBeenCalledTimes(1);
|
||||
|
||||
cleanup();
|
||||
windowTarget.dispatch('focus');
|
||||
documentTarget.dispatch('visibilitychange');
|
||||
windowTarget.tickFallbacks();
|
||||
expect(onFocus).toHaveBeenCalledTimes(1);
|
||||
expect(onVisibilityChange).toHaveBeenCalledTimes(3);
|
||||
expect(onFallback).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('keeps Windows scale fallback dormant while the document is hidden', () => {
|
||||
const windowTarget = new FakeWindowActivityTarget();
|
||||
const documentTarget = new FakeDocumentActivityTarget();
|
||||
const onFallback = vi.fn();
|
||||
documentTarget.visibilityState = 'hidden';
|
||||
|
||||
const cleanup = installNativeWindowActivityScheduler({
|
||||
windowTarget: windowTarget as unknown as Window,
|
||||
documentTarget: documentTarget as unknown as Document,
|
||||
fallbackIntervalMs: WINDOWS_SCALE_FALLBACK_INTERVAL_MS,
|
||||
onFallback,
|
||||
handlers: {},
|
||||
});
|
||||
expect(windowTarget.intervalDelays).toEqual([]);
|
||||
|
||||
windowTarget.tickFallbacks();
|
||||
expect(onFallback).not.toHaveBeenCalled();
|
||||
documentTarget.visibilityState = 'visible';
|
||||
documentTarget.dispatch('visibilitychange');
|
||||
expect(windowTarget.intervalDelays).toEqual([WINDOWS_SCALE_FALLBACK_INTERVAL_MS]);
|
||||
windowTarget.tickFallbacks();
|
||||
expect(onFallback).toHaveBeenCalledTimes(1);
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it('does not re-toggle a maximized window on activation when focus returns', () => {
|
||||
expect(shouldToggleMaximisedWindowForScaleFix('activation', true)).toBe(false);
|
||||
});
|
||||
|
||||
@@ -3,6 +3,96 @@ export type WindowScaleFixReason = 'activation' | 'ratio-change' | 'restore' | '
|
||||
export type WindowsScaleCheckTrigger = 'focus' | 'pageshow' | 'poll' | 'resize' | 'visibilitychange';
|
||||
export type TitleBarToggleIconKey = 'maximize' | 'restore';
|
||||
|
||||
// resize/focus/pageshow/visibility 生命周期事件承担实时同步;轮询只作为 Wails
|
||||
// 未上报“仅移动窗口”等边缘场景的低频容错,避免空闲窗口持续跨 JS/Go 边界。
|
||||
export const WINDOW_STATE_FALLBACK_INTERVAL_MS = 15_000;
|
||||
export const WINDOWS_SCALE_FALLBACK_INTERVAL_MS = 10_000;
|
||||
|
||||
type NativeWindowActivityEventHandler = () => void;
|
||||
|
||||
export interface NativeWindowActivitySchedulerHandlers {
|
||||
resize?: NativeWindowActivityEventHandler;
|
||||
focus?: NativeWindowActivityEventHandler;
|
||||
blur?: NativeWindowActivityEventHandler;
|
||||
pageshow?: NativeWindowActivityEventHandler;
|
||||
pagehide?: NativeWindowActivityEventHandler;
|
||||
beforeunload?: NativeWindowActivityEventHandler;
|
||||
visibilitychange?: NativeWindowActivityEventHandler;
|
||||
}
|
||||
|
||||
export interface NativeWindowActivitySchedulerOptions {
|
||||
windowTarget: Window;
|
||||
documentTarget: Document;
|
||||
fallbackIntervalMs: number;
|
||||
onFallback: NativeWindowActivityEventHandler;
|
||||
handlers: NativeWindowActivitySchedulerHandlers;
|
||||
}
|
||||
|
||||
export const installNativeWindowActivityScheduler = ({
|
||||
windowTarget,
|
||||
documentTarget,
|
||||
fallbackIntervalMs,
|
||||
onFallback,
|
||||
handlers,
|
||||
}: NativeWindowActivitySchedulerOptions): (() => void) => {
|
||||
const windowListeners: Array<{
|
||||
type: keyof Pick<WindowEventMap, 'resize' | 'focus' | 'blur' | 'pageshow' | 'pagehide' | 'beforeunload'>;
|
||||
listener: EventListener;
|
||||
capture: boolean;
|
||||
}> = [];
|
||||
|
||||
const addWindowListener = (
|
||||
type: keyof Pick<WindowEventMap, 'resize' | 'focus' | 'blur' | 'pageshow' | 'pagehide' | 'beforeunload'>,
|
||||
handler: NativeWindowActivityEventHandler | undefined,
|
||||
capture = false,
|
||||
) => {
|
||||
if (!handler) return;
|
||||
const listener: EventListener = () => handler();
|
||||
windowTarget.addEventListener(type, listener, capture);
|
||||
windowListeners.push({ type, listener, capture });
|
||||
};
|
||||
|
||||
addWindowListener('resize', handlers.resize);
|
||||
addWindowListener('focus', handlers.focus);
|
||||
addWindowListener('blur', handlers.blur);
|
||||
addWindowListener('pageshow', handlers.pageshow);
|
||||
addWindowListener('pagehide', handlers.pagehide, true);
|
||||
addWindowListener('beforeunload', handlers.beforeunload, true);
|
||||
|
||||
let fallbackTimer: number | null = null;
|
||||
const stopFallback = () => {
|
||||
if (fallbackTimer === null) return;
|
||||
windowTarget.clearInterval(fallbackTimer);
|
||||
fallbackTimer = null;
|
||||
};
|
||||
const startFallback = () => {
|
||||
if (fallbackTimer !== null || documentTarget.visibilityState !== 'visible') return;
|
||||
fallbackTimer = windowTarget.setInterval(() => {
|
||||
if (documentTarget.visibilityState === 'visible') {
|
||||
onFallback();
|
||||
}
|
||||
}, fallbackIntervalMs);
|
||||
};
|
||||
const visibilityListener: EventListener = () => {
|
||||
if (documentTarget.visibilityState === 'visible') {
|
||||
startFallback();
|
||||
} else {
|
||||
stopFallback();
|
||||
}
|
||||
handlers.visibilitychange?.();
|
||||
};
|
||||
documentTarget.addEventListener('visibilitychange', visibilityListener);
|
||||
startFallback();
|
||||
|
||||
return () => {
|
||||
stopFallback();
|
||||
for (const { type, listener, capture } of windowListeners) {
|
||||
windowTarget.removeEventListener(type, listener, capture);
|
||||
}
|
||||
documentTarget.removeEventListener('visibilitychange', visibilityListener);
|
||||
};
|
||||
};
|
||||
|
||||
export const shouldApplyWindowsScaleFix = (
|
||||
reason: WindowScaleFixReason,
|
||||
hasViewportScaleDrift: boolean,
|
||||
|
||||
Reference in New Issue
Block a user