Skip to content

Commit

Permalink
refactor(editor): simplify renderer state (#10441)
Browse files Browse the repository at this point in the history
The redundant `monitoring` state has been removed. The new `ready` state is used for querying if DOM elements can be safely optimized away.
  • Loading branch information
doodlewind committed Feb 26, 2025
1 parent c058f94 commit 0f8c837
Show file tree
Hide file tree
Showing 4 changed files with 75 additions and 18 deletions.
6 changes: 2 additions & 4 deletions blocksuite/affine/block-root/src/widgets/linked-doc/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,8 @@ import { choose } from 'lit/directives/choose.js';
import { repeat } from 'lit/directives/repeat.js';
import { styleMap } from 'lit/directives/style-map.js';

import {
type PageRootBlockComponent,
RootBlockConfigExtension,
} from '../../index.js';
import type { PageRootBlockComponent } from '../../page/page-root-block.js';
import { RootBlockConfigExtension } from '../../root-config.js';
import {
type AFFINE_LINKED_DOC_WIDGET,
getMenus,
Expand Down
22 changes: 13 additions & 9 deletions blocksuite/affine/shared/src/viewport-renderer/dom-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import { type Viewport } from '@blocksuite/block-std/gfx';
import { Pane } from 'tweakpane';

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';

export function syncCanvasSize(canvas: HTMLCanvasElement, host: HTMLElement) {
Expand Down Expand Up @@ -98,15 +102,15 @@ export function initTweakpane(
paneElement.style.right = '10px';
paneElement.style.width = '250px';
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', () => {
renderer.invalidate();
});
}

export function debugLog(message: string, state: RenderingState) {
console.log(
`%c[ViewportTurboRenderer]%c ${message} | state=${state}`,
'color: #4285f4; font-weight: bold;',
'color: inherit;'
);
}
9 changes: 9 additions & 0 deletions blocksuite/affine/shared/src/viewport-renderer/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,12 @@ export interface TextRect {
rect: Rect;
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';
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,17 @@ import {
StdIdentifier,
} from '@blocksuite/block-std';
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 { type Pane } from 'tweakpane';

import {
debugLog,
getViewportLayout,
initTweakpane,
syncCanvasSize,
} from './dom-utils.js';
import { type ViewportLayout } from './types.js';
import type { RenderingState, ViewportLayout } from './types.js';

export const ViewportTurboRendererIdentifier = LifeCycleWatcherIdentifier(
'ViewportTurboRenderer'
Expand All @@ -29,9 +30,10 @@ interface Tile {
const zoomThreshold = 1;

export class ViewportTurboRendererExtension extends LifeCycleWatcher {
state: 'monitoring' | 'paused' = 'paused';
state: RenderingState = 'inactive';
disposables = new DisposableGroup();
private layoutVersion = 0;
private readonly debug = false; // Toggle for debug logs

static override setup(di: Container) {
di.addImpl(ViewportTurboRendererIdentifier, this, [StdIdentifier]);
Expand All @@ -48,6 +50,7 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
this.worker = new Worker(new URL('./painter.worker.ts', import.meta.url), {
type: 'module',
});
this.debugLog('Initialized ViewportTurboRenderer');
}

override mounted() {
Expand All @@ -59,7 +62,7 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {

this.viewport.elementReady.once(() => {
syncCanvasSize(this.canvas, this.std.host);
this.state = 'monitoring';
this.setState('pending');
this.disposables.add(
this.viewport.viewportUpdated.on(() => {
this.refresh().catch(console.error);
Expand All @@ -76,6 +79,7 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
}

override unmounted() {
this.debugLog('Unmounting renderer');
this.clearTile();
if (this.debugPane) {
this.debugPane.dispose();
Expand All @@ -84,27 +88,34 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
this.worker.terminate();
this.canvas.remove();
this.disposables.dispose();
this.setState('inactive');
}

get viewport() {
return this.std.get(GfxControllerIdentifier).viewport;
}

async refresh() {
if (this.state === 'paused') return;
if (this.state === 'inactive') return;

this.clearCanvas();
if (this.viewport.zoom > zoomThreshold) {
this.debugLog('Zoom above threshold, falling back to DOM rendering');
this.setState('pending');
return;
} else if (this.canUseBitmapCache()) {
this.debugLog('Using cached bitmap');
this.drawCachedBitmap(this.layoutCache!);
this.setState('ready');
} else {
if (!this.layoutCache) {
this.updateLayoutCache();
}
const layout = this.layoutCache!;
this.setState('rendering');
await this.paintLayout(layout);
this.drawCachedBitmap(layout);
// State will be updated to 'ready' in handlePaintedBitmap if successful
}
}

Expand All @@ -121,17 +132,26 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
this.layoutCache = null;
this.clearTile();
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() {
const layout = getViewportLayout(this.std.host, this.viewport);
this.layoutCache = layout;
this.debugLog('Layout cache updated');
}

private clearTile() {
if (this.tile) {
this.tile.bitmap.close();
this.tile = null;
this.debugLog('Tile cleared');
}
}

Expand All @@ -142,6 +162,7 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
const dpr = window.devicePixelRatio;
const currentVersion = this.layoutVersion;

this.debugLog(`Requesting bitmap painting (version=${currentVersion})`);
this.worker.postMessage({
type: 'paintLayout',
data: {
Expand All @@ -157,9 +178,16 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
this.worker.onmessage = (e: MessageEvent) => {
if (e.data.type === 'bitmapPainted') {
if (e.data.version === this.layoutVersion) {
this.debugLog(
`Bitmap painted successfully (version=${e.data.version})`
);
this.handlePaintedBitmap(e.data.bitmap, resolve);
} else {
this.debugLog(
`Received outdated bitmap (got=${e.data.version}, current=${this.layoutVersion})`
);
e.data.bitmap.close();
this.setState('pending');
resolve();
}
}
Expand All @@ -175,6 +203,7 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
bitmap,
zoom: this.viewport.zoom,
};
this.setState('ready');
resolve();
}

Expand All @@ -188,10 +217,12 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
const ctx = this.canvas.getContext('2d');
if (!ctx) return;
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.debugLog('Canvas cleared');
}

private drawCachedBitmap(layout: ViewportLayout) {
if (!this.tile) {
this.debugLog('No cached bitmap available, requesting refresh');
this.debouncedRefresh();
return; // version mismatch
}
Expand All @@ -213,5 +244,20 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
layout.rect.w * 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;
}
}

0 comments on commit 0f8c837

Please sign in to comment.