refactor(editor): simplify renderer state (#10441)

The redundant `monitoring` state has been removed. The new `ready` state is used for querying if DOM elements can be safely optimized away.
This commit is contained in:
doodlewind
2025-02-26 08:39:58 +00:00
parent c058f94e15
commit 0f8c837fbe
4 changed files with 75 additions and 18 deletions

View File

@@ -20,10 +20,8 @@ import { choose } from 'lit/directives/choose.js';
import { repeat } from 'lit/directives/repeat.js'; import { repeat } from 'lit/directives/repeat.js';
import { styleMap } from 'lit/directives/style-map.js'; import { styleMap } from 'lit/directives/style-map.js';
import { import type { PageRootBlockComponent } from '../../page/page-root-block.js';
type PageRootBlockComponent, import { RootBlockConfigExtension } from '../../root-config.js';
RootBlockConfigExtension,
} from '../../index.js';
import { import {
type AFFINE_LINKED_DOC_WIDGET, type AFFINE_LINKED_DOC_WIDGET,
getMenus, getMenus,

View File

@@ -2,7 +2,11 @@ import { type Viewport } from '@blocksuite/block-std/gfx';
import { Pane } from 'tweakpane'; import { Pane } from 'tweakpane';
import { getSentenceRects, segmentSentences } from './text-utils.js'; import { getSentenceRects, segmentSentences } from './text-utils.js';
import type { ParagraphLayout, ViewportLayout } from './types.js'; import type {
ParagraphLayout,
RenderingState,
ViewportLayout,
} from './types.js';
import type { ViewportTurboRendererExtension } from './viewport-renderer.js'; import type { ViewportTurboRendererExtension } from './viewport-renderer.js';
export function syncCanvasSize(canvas: HTMLCanvasElement, host: HTMLElement) { export function syncCanvasSize(canvas: HTMLCanvasElement, host: HTMLElement) {
@@ -98,15 +102,15 @@ export function initTweakpane(
paneElement.style.right = '10px'; paneElement.style.right = '10px';
paneElement.style.width = '250px'; paneElement.style.width = '250px';
debugPane.title = 'Viewport Turbo Renderer'; debugPane.title = 'Viewport Turbo Renderer';
debugPane
.addBinding({ paused: false }, 'paused', {
label: 'Paused',
})
.on('change', ({ value }) => {
renderer.state = value ? 'paused' : 'monitoring';
});
debugPane.addButton({ title: 'Invalidate' }).on('click', () => { debugPane.addButton({ title: 'Invalidate' }).on('click', () => {
renderer.invalidate(); renderer.invalidate();
}); });
} }
export function debugLog(message: string, state: RenderingState) {
console.log(
`%c[ViewportTurboRenderer]%c ${message} | state=${state}`,
'color: #4285f4; font-weight: bold;',
'color: inherit;'
);
}

View File

@@ -32,3 +32,12 @@ export interface TextRect {
rect: Rect; rect: Rect;
text: string; text: string;
} }
/**
* Represents the rendering state of the ViewportTurboRenderer
* - inactive: Renderer is not active
* - pending: Bitmap is invalid or not yet available, falling back to DOM rendering
* - rendering: Currently rendering to a bitmap (async operation in progress)
* - ready: Bitmap is valid and rendered, DOM elements can be safely removed
*/
export type RenderingState = 'inactive' | 'pending' | 'rendering' | 'ready';

View File

@@ -5,16 +5,17 @@ import {
StdIdentifier, StdIdentifier,
} from '@blocksuite/block-std'; } from '@blocksuite/block-std';
import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx'; import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
import { type Container, type ServiceIdentifier } from '@blocksuite/global/di'; import type { Container, ServiceIdentifier } from '@blocksuite/global/di';
import { debounce, DisposableGroup } from '@blocksuite/global/utils'; import { debounce, DisposableGroup } from '@blocksuite/global/utils';
import { type Pane } from 'tweakpane'; import { type Pane } from 'tweakpane';
import { import {
debugLog,
getViewportLayout, getViewportLayout,
initTweakpane, initTweakpane,
syncCanvasSize, syncCanvasSize,
} from './dom-utils.js'; } from './dom-utils.js';
import { type ViewportLayout } from './types.js'; import type { RenderingState, ViewportLayout } from './types.js';
export const ViewportTurboRendererIdentifier = LifeCycleWatcherIdentifier( export const ViewportTurboRendererIdentifier = LifeCycleWatcherIdentifier(
'ViewportTurboRenderer' 'ViewportTurboRenderer'
@@ -29,9 +30,10 @@ interface Tile {
const zoomThreshold = 1; const zoomThreshold = 1;
export class ViewportTurboRendererExtension extends LifeCycleWatcher { export class ViewportTurboRendererExtension extends LifeCycleWatcher {
state: 'monitoring' | 'paused' = 'paused'; state: RenderingState = 'inactive';
disposables = new DisposableGroup(); disposables = new DisposableGroup();
private layoutVersion = 0; private layoutVersion = 0;
private readonly debug = false; // Toggle for debug logs
static override setup(di: Container) { static override setup(di: Container) {
di.addImpl(ViewportTurboRendererIdentifier, this, [StdIdentifier]); di.addImpl(ViewportTurboRendererIdentifier, this, [StdIdentifier]);
@@ -48,6 +50,7 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
this.worker = new Worker(new URL('./painter.worker.ts', import.meta.url), { this.worker = new Worker(new URL('./painter.worker.ts', import.meta.url), {
type: 'module', type: 'module',
}); });
this.debugLog('Initialized ViewportTurboRenderer');
} }
override mounted() { override mounted() {
@@ -59,7 +62,7 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
this.viewport.elementReady.once(() => { this.viewport.elementReady.once(() => {
syncCanvasSize(this.canvas, this.std.host); syncCanvasSize(this.canvas, this.std.host);
this.state = 'monitoring'; this.setState('pending');
this.disposables.add( this.disposables.add(
this.viewport.viewportUpdated.on(() => { this.viewport.viewportUpdated.on(() => {
this.refresh().catch(console.error); this.refresh().catch(console.error);
@@ -76,6 +79,7 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
} }
override unmounted() { override unmounted() {
this.debugLog('Unmounting renderer');
this.clearTile(); this.clearTile();
if (this.debugPane) { if (this.debugPane) {
this.debugPane.dispose(); this.debugPane.dispose();
@@ -84,6 +88,7 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
this.worker.terminate(); this.worker.terminate();
this.canvas.remove(); this.canvas.remove();
this.disposables.dispose(); this.disposables.dispose();
this.setState('inactive');
} }
get viewport() { get viewport() {
@@ -91,20 +96,26 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
} }
async refresh() { async refresh() {
if (this.state === 'paused') return; if (this.state === 'inactive') return;
this.clearCanvas(); this.clearCanvas();
if (this.viewport.zoom > zoomThreshold) { if (this.viewport.zoom > zoomThreshold) {
this.debugLog('Zoom above threshold, falling back to DOM rendering');
this.setState('pending');
return; return;
} else if (this.canUseBitmapCache()) { } else if (this.canUseBitmapCache()) {
this.debugLog('Using cached bitmap');
this.drawCachedBitmap(this.layoutCache!); this.drawCachedBitmap(this.layoutCache!);
this.setState('ready');
} else { } else {
if (!this.layoutCache) { if (!this.layoutCache) {
this.updateLayoutCache(); this.updateLayoutCache();
} }
const layout = this.layoutCache!; const layout = this.layoutCache!;
this.setState('rendering');
await this.paintLayout(layout); await this.paintLayout(layout);
this.drawCachedBitmap(layout); this.drawCachedBitmap(layout);
// State will be updated to 'ready' in handlePaintedBitmap if successful
} }
} }
@@ -121,17 +132,26 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
this.layoutCache = null; this.layoutCache = null;
this.clearTile(); this.clearTile();
this.clearCanvas(); // Should clear immediately after content updates this.clearCanvas(); // Should clear immediately after content updates
this.setState('pending');
this.debugLog(`Invalidated renderer (layoutVersion=${this.layoutVersion})`);
}
private debugLog(message: string): void {
if (!this.debug) return;
debugLog(message, this.state);
} }
private updateLayoutCache() { private updateLayoutCache() {
const layout = getViewportLayout(this.std.host, this.viewport); const layout = getViewportLayout(this.std.host, this.viewport);
this.layoutCache = layout; this.layoutCache = layout;
this.debugLog('Layout cache updated');
} }
private clearTile() { private clearTile() {
if (this.tile) { if (this.tile) {
this.tile.bitmap.close(); this.tile.bitmap.close();
this.tile = null; this.tile = null;
this.debugLog('Tile cleared');
} }
} }
@@ -142,6 +162,7 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
const dpr = window.devicePixelRatio; const dpr = window.devicePixelRatio;
const currentVersion = this.layoutVersion; const currentVersion = this.layoutVersion;
this.debugLog(`Requesting bitmap painting (version=${currentVersion})`);
this.worker.postMessage({ this.worker.postMessage({
type: 'paintLayout', type: 'paintLayout',
data: { data: {
@@ -157,9 +178,16 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
this.worker.onmessage = (e: MessageEvent) => { this.worker.onmessage = (e: MessageEvent) => {
if (e.data.type === 'bitmapPainted') { if (e.data.type === 'bitmapPainted') {
if (e.data.version === this.layoutVersion) { if (e.data.version === this.layoutVersion) {
this.debugLog(
`Bitmap painted successfully (version=${e.data.version})`
);
this.handlePaintedBitmap(e.data.bitmap, resolve); this.handlePaintedBitmap(e.data.bitmap, resolve);
} else { } else {
this.debugLog(
`Received outdated bitmap (got=${e.data.version}, current=${this.layoutVersion})`
);
e.data.bitmap.close(); e.data.bitmap.close();
this.setState('pending');
resolve(); resolve();
} }
} }
@@ -175,6 +203,7 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
bitmap, bitmap,
zoom: this.viewport.zoom, zoom: this.viewport.zoom,
}; };
this.setState('ready');
resolve(); resolve();
} }
@@ -188,10 +217,12 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
const ctx = this.canvas.getContext('2d'); const ctx = this.canvas.getContext('2d');
if (!ctx) return; if (!ctx) return;
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.debugLog('Canvas cleared');
} }
private drawCachedBitmap(layout: ViewportLayout) { private drawCachedBitmap(layout: ViewportLayout) {
if (!this.tile) { if (!this.tile) {
this.debugLog('No cached bitmap available, requesting refresh');
this.debouncedRefresh(); this.debouncedRefresh();
return; // version mismatch return; // version mismatch
} }
@@ -213,5 +244,20 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
layout.rect.w * window.devicePixelRatio * this.viewport.zoom, layout.rect.w * window.devicePixelRatio * this.viewport.zoom,
layout.rect.h * window.devicePixelRatio * this.viewport.zoom layout.rect.h * window.devicePixelRatio * this.viewport.zoom
); );
this.debugLog('Bitmap drawn to canvas');
}
setState(newState: RenderingState): void {
if (this.state === newState) return;
this.debugLog(`State change: ${this.state} -> ${newState}`);
this.state = newState;
}
canOptimizeDOM(): boolean {
const isReady = this.state === 'ready';
const isBelowZoomThreshold = this.viewport.zoom <= zoomThreshold;
const result = isReady && isBelowZoomThreshold;
return result;
} }
} }