| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879 |
- /** 运动帧间隔窗口(票 59 采样口径修正)。
- *
- * 为什么需要:导出里的 movingFrameIntervals 曾把"静止→运动"的第一段也收进窗口,
- * 而这一段包含按需渲染(requestRenderMode)的空档,几百毫秒的假尖峰正是这样来的;
- * 反过来,真正的长停顿又因 1000ms 上限被静默丢弃、连计数都没有。
- *
- * 现在的口径:只收**连续运动帧之间**的间隔(上一帧也在运动)。起步段、隐藏期、
- * 以及 >=maxGapMs 的空档都不进窗口,但都会被**计数**并保留最大被排除值,
- * 所以既去掉起步空档,也不会把长停顿藏起来。
- */
- export const MOTION_WINDOW_DEFAULTS = { maxSamples: 240, maxGapMs: 1000 }
- /**
- * @param {{maxSamples?: number, maxGapMs?: number}} [options]
- * @returns {{push: (tMs: number, moving: boolean) => string, markGap: (tMs: number) => void,
- * reset: () => void, snapshot: () => object}}
- */
- export function createMotionIntervalWindow(options = {}) {
- const configured = Number(options.maxSamples)
- const maxSamples = Number.isFinite(configured) && configured >= 1
- ? Math.trunc(configured)
- : MOTION_WINDOW_DEFAULTS.maxSamples
- const ceiling = Number(options.maxGapMs)
- const maxGapMs = Number.isFinite(ceiling) ? ceiling : MOTION_WINDOW_DEFAULTS.maxGapMs
- let intervals = []
- let previous = null
- let restartGapsExcluded = 0
- let longGapsExcluded = 0
- let maxExcludedMs = null
- const exclude = (kind, dt) => {
- if (kind === 'restart-gap') restartGapsExcluded += 1
- else longGapsExcluded += 1
- if (maxExcludedMs === null || dt > maxExcludedMs) maxExcludedMs = dt
- return kind
- }
- return {
- /** @returns {'first'|'sampled'|'restart-gap'|'long-gap'|'idle'} */
- push(tMs, moving) {
- const at = Number(tMs)
- if (!Number.isFinite(at)) return 'idle'
- const isMoving = !!moving
- if (previous === null) { previous = { at, moving: isMoving }; return 'first' }
- const dt = at - previous.at
- const wasMoving = previous.moving
- previous = { at, moving: isMoving }
- if (!isMoving || dt <= 0) return 'idle'
- if (!wasMoving) return exclude('restart-gap', dt)
- if (dt >= maxGapMs) return exclude('long-gap', dt)
- intervals.push(dt)
- if (intervals.length > maxSamples) intervals.shift()
- return 'sampled'
- },
- /** 隐藏期间的帧不采样,但必须打断"连续运动",否则恢复可见的首段会被当成连续帧。 */
- markGap(tMs) {
- const at = Number(tMs)
- if (Number.isFinite(at)) previous = { at, moving: false }
- },
- reset() {
- intervals = []
- previous = null
- restartGapsExcluded = 0
- longGapsExcluded = 0
- maxExcludedMs = null
- },
- snapshot() {
- const sorted = [...intervals].sort((a, b) => a - b)
- return {
- samples: sorted.length,
- p50Ms: sorted.length ? +sorted[Math.floor(sorted.length * .5)].toFixed(2) : null,
- p95Ms: sorted.length ? +sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * .95))].toFixed(2) : null,
- maxMs: sorted.length ? +sorted[sorted.length - 1].toFixed(2) : null,
- over50Ms: sorted.filter(n => n > 50).length,
- restartGapsExcluded,
- longGapsExcluded,
- maxExcludedMs: maxExcludedMs === null ? null : +maxExcludedMs.toFixed(2),
- }
- },
- }
- }
|