perf(engine-render): adapt scrollbar seek rendering (#7513)

This commit is contained in:
Univer
2026-08-15 00:04:57 +08:00
committed by GitHub
parent 8f242203bc
commit 823663e326
2 changed files with 57 additions and 49 deletions
@@ -333,7 +333,7 @@ describe('engine scene viewport extra', () => {
engine.dispose();
});
it('defers obsolete detail renders during a large scrollbar seek and paints the settled target', () => {
it('keeps cheap content live during a large scrollbar seek', () => {
const { engine, scene, viewport } = createFixture();
const layer = scene.getLayer(1);
const renderSpy = vi.spyOn(layer, 'render');
@@ -341,47 +341,69 @@ describe('engine scene viewport extra', () => {
const scrollbarRenderSpy = vi.spyOn(viewport, 'renderScrollbarOnly');
vi.spyOn(engine, 'getEstimatedFrameInterval').mockReturnValue(1000 / 120);
let now = 0;
nowSpy.mockImplementation(() => now);
nowSpy
.mockReturnValueOnce(0)
.mockReturnValueOnce(4)
.mockReturnValueOnce(10)
.mockReturnValueOnce(10)
.mockReturnValueOnce(46)
.mockReturnValueOnce(50)
.mockReturnValueOnce(50)
.mockReturnValueOnce(54)
.mockReturnValueOnce(60)
.mockReturnValueOnce(60)
.mockReturnValueOnce(64);
scene.render();
renderSpy.mockClear();
scene.beginScrollbarDrag(viewport);
viewport.scrollToViewportPos({ viewportScrollY: 40 });
scene.updateScrollbarDrag(viewport);
scene.render();
expect(renderSpy).toHaveBeenCalledTimes(1);
renderSpy.mockClear();
now = 10;
viewport.scrollToViewportPos({ viewportScrollY: 240 });
scene.updateScrollbarDrag(viewport);
scene.render();
expect(renderSpy).not.toHaveBeenCalled();
expect(renderSpy).toHaveBeenCalledTimes(1);
expect(scrollbarRenderSpy).toHaveBeenCalled();
now = 90;
scene.render();
expect(renderSpy).toHaveBeenCalledTimes(1);
renderSpy.mockClear();
now = 100;
viewport.scrollToViewportPos({ viewportScrollY: 300 });
scene.updateScrollbarDrag(viewport);
scene.render();
expect(renderSpy).not.toHaveBeenCalled();
now = 164;
scene.render();
expect(renderSpy).toHaveBeenCalledTimes(1);
renderSpy.mockClear();
viewport.scrollToViewportPos({ viewportScrollY: 320 });
scene.updateScrollbarDrag(viewport);
scene.endScrollbarDrag(viewport);
scene.render();
expect(renderSpy).toHaveBeenCalledTimes(2);
scene.dispose();
engine.dispose();
});
it('keeps content live while its render cost stays within the seek budget', () => {
const { engine, scene, viewport } = createFixture();
const layer = scene.getLayer(1);
const renderSpy = vi.spyOn(layer, 'render');
const nowSpy = vi.spyOn(Tools, 'now');
vi.spyOn(engine, 'getEstimatedFrameInterval').mockReturnValue(1000 / 120);
nowSpy.mockReturnValueOnce(0).mockReturnValueOnce(12);
scene.render();
renderSpy.mockClear();
let now = 20;
nowSpy.mockImplementation(() => now);
scene.beginScrollbarDrag(viewport);
viewport.scrollToViewportPos({ viewportScrollY: 240 });
scene.updateScrollbarDrag(viewport);
scene.render();
expect(renderSpy).toHaveBeenCalledTimes(1);
now = 28;
viewport.scrollToViewportPos({ viewportScrollY: 300 });
scene.updateScrollbarDrag(viewport);
scene.render();
expect(renderSpy).toHaveBeenCalledTimes(2);
scene.dispose();
engine.dispose();
});
+14 -28
View File
@@ -39,12 +39,9 @@ import { Transformer } from './scene.transformer';
export const MAIN_VIEW_PORT_KEY = 'viewMain';
const SCROLLBAR_SEEK_SETTLE_MS = 64;
const SCROLLBAR_SEEK_MIN_PREVIEW_INTERVAL_MS = 100;
const SCROLLBAR_SEEK_MAX_PREVIEW_INTERVAL_MS = 240;
const SCROLLBAR_SEEK_FRAME_INTERVALS = 8;
const SCROLLBAR_SEEK_RENDER_COST_MULTIPLIER = 4;
const SCROLLBAR_SEEK_EXPENSIVE_RENDER_MIN_MS = 32;
const SCROLLBAR_SEEK_EXPENSIVE_RENDER_FRAME_INTERVALS = 2;
const SCROLLBAR_SEEK_RENDER_COST_SAMPLE_WEIGHT = 0.25;
export interface ISceneInputControlOptions {
enableDown: boolean;
@@ -176,8 +173,7 @@ export class Scene extends Disposable {
private _isScrollbarSeeking = false;
private _isScrollbarPreviewDirty = false;
private _lastScrollbarSeekInputAt = Number.NEGATIVE_INFINITY;
private _lastScrollbarSeekRenderAt = Number.NEGATIVE_INFINITY;
private _lastFullRenderDuration = 0;
private _estimatedFullRenderDuration = 0;
private _renderedViewportScrollPositions = new Map<string, IViewportScrollPosition>();
private _cursor: CURSOR_TYPE = CURSOR_TYPE.DEFAULT;
@@ -433,7 +429,6 @@ export class Scene extends Disposable {
Math.abs(viewport.viewportScrollY - renderedPosition.viewportScrollY) >= viewportHeight;
if (isOutsideRenderedViewport) {
this._isScrollbarSeeking = true;
this._lastScrollbarSeekRenderAt = now;
}
}
@@ -920,18 +915,6 @@ export class Scene extends Disposable {
this._isScrollbarPreviewDirty = false;
}
private _getScrollbarSeekPreviewInterval() {
const frameInterval = this.getEngine()?.getEstimatedFrameInterval() ?? 1000 / 60;
return Tools.clamp(
Math.max(
frameInterval * SCROLLBAR_SEEK_FRAME_INTERVALS,
this._lastFullRenderDuration * SCROLLBAR_SEEK_RENDER_COST_MULTIPLIER
),
SCROLLBAR_SEEK_MIN_PREVIEW_INTERVAL_MS,
SCROLLBAR_SEEK_MAX_PREVIEW_INTERVAL_MS
);
}
private _shouldDeferScrollbarSeekRender(now: number) {
if (!this._isScrollbarSeeking) {
return false;
@@ -947,11 +930,7 @@ export class Scene extends Disposable {
SCROLLBAR_SEEK_EXPENSIVE_RENDER_MIN_MS,
frameInterval * SCROLLBAR_SEEK_EXPENSIVE_RENDER_FRAME_INTERVALS
);
if (this._lastFullRenderDuration >= expensiveRenderThreshold) {
return true;
}
return now - this._lastScrollbarSeekRenderAt < this._getScrollbarSeekPreviewInterval();
return this._estimatedFullRenderDuration >= expensiveRenderThreshold;
}
private _recordRenderedViewportScrollPositions() {
@@ -965,6 +944,16 @@ export class Scene extends Disposable {
}
}
private _recordFullRenderDuration(duration: number, isScrollbarSeekRender: boolean) {
if (!isScrollbarSeekRender || this._estimatedFullRenderDuration === 0) {
this._estimatedFullRenderDuration = duration;
return;
}
this._estimatedFullRenderDuration +=
(duration - this._estimatedFullRenderDuration) * SCROLLBAR_SEEK_RENDER_COST_SAMPLE_WEIGHT;
}
render(parentCtx?: UniverRenderingContext) {
if (!this.isDirty()) {
return;
@@ -1017,10 +1006,7 @@ export class Scene extends Disposable {
this._afterRender$.next(canvasInstance);
this._recordRenderedViewportScrollPositions();
if (shouldMeasureFullRender) {
this._lastFullRenderDuration = Tools.now() - fullRenderStartedAt;
if (isScrollbarSeekRender) {
this._lastScrollbarSeekRenderAt = fullRenderStartedAt;
}
this._recordFullRenderDuration(Tools.now() - fullRenderStartedAt, isScrollbarSeekRender);
}
}