✨ feat(block): virtualized workspace#119
Conversation
Signed-off-by: SimonShiki <sinangentoo@gmail.com>
Deploying clipcc-preview with
|
| Latest commit: |
1a740cc
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://0f2975ed.clipcc-preview.pages.dev |
| Branch Preview URL: | https://feat-modern-blockly-virtuali.clipcc-preview.pages.dev |
Signed-off-by: SimonShiki <sinangentoo@gmail.com>
Signed-off-by: SimonShiki <sinangentoo@gmail.com>
Signed-off-by: SimonShiki <sinangentoo@gmail.com>
Signed-off-by: SimonShiki <sinangentoo@gmail.com>
Signed-off-by: SimonShiki <sinangentoo@gmail.com>
Signed-off-by: SimonShiki <sinangentoo@gmail.com>
Signed-off-by: SimonShiki <sinangentoo@gmail.com>
Signed-off-by: SimonShiki <sinangentoo@gmail.com>
Signed-off-by: SimonShiki <sinangentoo@gmail.com>
Signed-off-by: SimonShiki <sinangentoo@gmail.com>
Signed-off-by: SimonShiki <sinangentoo@gmail.com>
Signed-off-by: SimonShiki <sinangentoo@gmail.com>
not necessary for current calculation scale, but necessary if we virtualize in block level Signed-off-by: SimonShiki <sinangentoo@gmail.com>
d23919e to
1d0f40d
Compare
Signed-off-by: SimonShiki <sinangentoo@gmail.com>
Signed-off-by: SimonShiki <sinangentoo@gmail.com>
Signed-off-by: SimonShiki <sinangentoo@gmail.com>
Signed-off-by: SimonShiki <sinangentoo@gmail.com>
Signed-off-by: SimonShiki <sinangentoo@gmail.com>
…Children Signed-off-by: SimonShiki <sinangentoo@gmail.com>
Signed-off-by: SimonShiki <sinangentoo@gmail.com>
Signed-off-by: SimonShiki <sinangentoo@gmail.com>
Signed-off-by: SimonShiki <sinangentoo@gmail.com>
Signed-off-by: SimonShiki <sinangentoo@gmail.com>
Signed-off-by: SimonShiki <sinangentoo@gmail.com>
Signed-off-by: SimonShiki <sinangentoo@gmail.com>
There was a problem hiding this comment.
Pull request overview
This PR implements workspace virtualization for Blockly to improve performance when rendering large programs by hiding off-screen blocks. The implementation uses a QuadTree spatial data structure to efficiently track block positions and determine which blocks should be visible based on the current viewport.
Key Changes
- Implements a VirtualizedManager class that manages block visibility using viewport-based culling and a QuadTree for efficient spatial queries
- Adds a QuadTree utility class for managing block positions and performing range queries
- Enables virtualization by default in the injectWorkspace function with an opt-out configuration option
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 16 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/block/tsconfig.json | Updates TypeScript target to ES2022 to enable WeakRef support required for virtualization |
| packages/block/tests/helpers/playground.ts | Disables virtualization in test environment to prevent interference with existing tests |
| packages/block/src/virtualized_manager.ts | Core implementation of the virtualization manager with event handling and block visibility management |
| packages/block/src/utils/quad_tree.ts | QuadTree spatial data structure for efficient position-based queries of blocks |
| packages/block/src/styles/blockly.css | CSS rules for hiding virtualized blocks using display:none and content-visibility |
| packages/block/src/index.ts | Integrates virtualization into the public API with virtualized option in ClipCCBlockOptions |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| If false, it uses workspace change listener to update visibility, which may be delayed. and you | ||
| need to call `virtualize()` manually when window resized. | ||
| Default to true. |
There was a problem hiding this comment.
The documentation comment has formatting inconsistencies with extra leading whitespace on line 404. The lines starting with "If false" should align with the rest of the parameter description. This makes the documentation harder to read and may cause issues with documentation generation tools.
| If false, it uses workspace change listener to update visibility, which may be delayed. and you | |
| need to call `virtualize()` manually when window resized. | |
| Default to true. | |
| * If false, it uses workspace change listener to update visibility, which may be delayed, and you | |
| * need to call `virtualize()` manually when the window is resized. | |
| * Default to true. |
| insert(item: T, rect: Blockly.utils.Rect): void { | ||
| if (this.level === 0) { | ||
| if (this.itemMap!.has(item)) { | ||
| this.remove(item); | ||
| } | ||
| this.itemMap!.set(item, rect); | ||
|
|
||
| while (!this.contains(this.bounds, rect)) { | ||
| this.grow(rect); | ||
| } | ||
| } | ||
|
|
||
| this.insertInternal(item, rect); | ||
| } |
There was a problem hiding this comment.
The insert method can cause infinite growth of the QuadTree if the item's rectangle has extreme coordinates or the grow logic doesn't properly expand to contain the rectangle. The while loop at line 110-112 lacks a safety limit, which could lead to a stack overflow or excessive memory consumption. Add a maximum growth iterations check or validate that the rectangle has reasonable bounds before attempting to grow.
|
|
||
| /* Language and Environment */ | ||
| "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ | ||
| "target": "es2022", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ |
There was a problem hiding this comment.
Changing the TypeScript target from ES2016 to ES2022 introduces a potential breaking change for environments that don't support ES2022 features. This change enables the use of WeakRef (used in virtualized_manager.ts) which requires ES2021+, but may break compatibility with older browsers or Node.js versions. Ensure this change is documented and that the minimum supported environment versions are updated accordingly. Consider whether the target should be ES2021 instead of ES2022 to match the actual features used.
| "target": "es2022", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ | |
| "target": "es2021", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ |
| protected get workspace(): Blockly.WorkspaceSvg { | ||
| return this.workspaceRef.deref()!; |
There was a problem hiding this comment.
The workspace getter assumes the WeakRef will always return a valid value by using the non-null assertion operator. However, the WeakRef can return undefined if the workspace has been garbage collected. While the comment suggests it "always exists since the manager is tied to the workspace's lifecycle," this isn't guaranteed since dispose() can be called independently and virtualizeInternal() already checks for workspace existence at line 204. Add a null check or handle the case where the workspace has been collected to prevent potential runtime errors.
| protected get workspace(): Blockly.WorkspaceSvg { | |
| return this.workspaceRef.deref()!; | |
| /** | |
| * Get the workspace from WeakRef. | |
| * May return undefined if the workspace has been garbage collected. | |
| */ | |
| protected get workspace(): Blockly.WorkspaceSvg | undefined { | |
| return this.workspaceRef.deref(); |
| /** | ||
| * @license | ||
| * Copyright 2025 Clip Team | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| import * as Blockly from 'blockly/core'; | ||
| import {QuadTree} from './utils/quad_tree'; | ||
|
|
||
| export class VirtualizedManager { | ||
| /** | ||
| * Hold a weak reference to the workspace to avoid memory leaks. | ||
| */ | ||
| protected workspaceRef: WeakRef<Blockly.WorkspaceSvg>; | ||
| /** | ||
| * Blocks being observed for virtualization. | ||
| */ | ||
| protected observedBlocks = new Map<string, Blockly.BlockSvg>(); | ||
| /** | ||
| * The quad tree used to manage block positions. | ||
| */ | ||
| protected quadTree: QuadTree<Blockly.BlockSvg>; | ||
| /** | ||
| * Set of blocks that are currently hidden. | ||
| */ | ||
| protected hiddenBlocks = new Set<Blockly.BlockSvg>(); | ||
| /** | ||
| * Whether to update block visibility immediately on viewport changes. | ||
| * If true, workspace methods will be hooked to listen viewport changes immediately. | ||
| */ | ||
| protected immediate: boolean; | ||
| /** | ||
| * Whether a virtualization check has been requested. | ||
| * Any requested check will perform in next microtask. | ||
| */ | ||
| protected requestedCheck = false; | ||
|
|
||
| constructor(workspace: Blockly.WorkspaceSvg, immediate = true) { | ||
| this.workspaceRef = new WeakRef(workspace); | ||
| this.immediate = immediate; | ||
|
|
||
| const rect = this.getViewportRect(); | ||
| const viewWidth = rect.right - rect.left; | ||
| const viewHeight = rect.bottom - rect.top; | ||
| const cx = (rect.left + rect.right) / 2; | ||
| const cy = (rect.top + rect.bottom) / 2; | ||
|
|
||
| const halfW = viewWidth * 2; | ||
| const halfH = viewHeight * 2; | ||
|
|
||
| this.quadTree = new QuadTree( | ||
| new Blockly.utils.Rect(cy - halfH, cy + halfH, cx - halfW, cx + halfW) | ||
| ); | ||
|
|
||
| for (const block of workspace.getAllBlocks(false)) { | ||
| this.observe(block); | ||
| } | ||
|
|
||
| this.virtualize(); | ||
|
|
||
| if (immediate) { | ||
| this.hookWorkspace(); | ||
| } | ||
| workspace.addChangeListener(this.workspaceChangeListener); | ||
| } | ||
|
|
||
| /** | ||
| * Get the workspace from WeakRef. | ||
| * Always exists since the manager is tied to the workspace's lifecycle. | ||
| * @returns The workspace. | ||
| */ | ||
| protected get workspace(): Blockly.WorkspaceSvg { | ||
| return this.workspaceRef.deref()!; | ||
| } | ||
|
|
||
| /** | ||
| * Hook workspace methods to update block visibility immediately on viewport changes. | ||
| */ | ||
| protected hookWorkspace() { | ||
| const proto: Blockly.WorkspaceSvg = Object.getPrototypeOf(this.workspace); | ||
| // eslint-disable-next-line @typescript-eslint/no-this-alias | ||
| const manager = this; | ||
| const originalMaybeFireViewportChangeEvent = proto.maybeFireViewportChangeEvent; | ||
| this.workspace.maybeFireViewportChangeEvent = function() { | ||
| originalMaybeFireViewportChangeEvent.call(this); | ||
| manager.virtualize(); | ||
| }; | ||
|
|
||
| const originalResize = proto.resize; | ||
| this.workspace.resize = function() { | ||
| originalResize.call(this); | ||
| manager.virtualize(); | ||
| }; | ||
|
|
||
| const originalDispose = proto.dispose; | ||
| this.workspace.dispose = function() { | ||
| originalDispose.call(this); | ||
| manager.dispose(); | ||
| }; | ||
| } | ||
|
|
||
| protected workspaceChangeListener = (e: Blockly.Events.Abstract) => { | ||
| switch (e.type) { | ||
| case Blockly.Events.BLOCK_CREATE: { | ||
| const event = e as Blockly.Events.BlockCreate; | ||
| if (!event.ids) break; | ||
|
|
||
| for (const id of event.ids) { | ||
| const block = this.workspace.getBlockById(id); | ||
| if (block) this.observe(block); | ||
| } | ||
| this.virtualize(); | ||
| break; | ||
| } | ||
| case Blockly.Events.BLOCK_DELETE: { | ||
| const event = e as Blockly.Events.BlockDelete; | ||
| if (!event.blockId) break; | ||
| this.unobserve(event.blockId); | ||
| this.virtualize(); | ||
| break; | ||
| } | ||
| case Blockly.Events.BLOCK_MOVE: | ||
| case Blockly.Events.BLOCK_CHANGE: { | ||
| const event = e as (Blockly.Events.BlockMove | Blockly.Events.BlockChange); | ||
| if (!event.blockId) break; | ||
| if (event instanceof Blockly.Events.BlockChange) { | ||
| if (event.element === 'disabled' || event.element === 'comment') { | ||
| // No need to update position for these changes. | ||
| break; | ||
| } | ||
| } | ||
| const block = this.workspace.getBlockById(event.blockId); | ||
| if (block) { | ||
| const descendants = block.getDescendants(false); | ||
| for (const desc of descendants) { | ||
| if (this.observedBlocks.has(desc.id)) { | ||
| this.updateObserve(desc); | ||
| } else { | ||
| this.observe(desc); | ||
| } | ||
| } | ||
| } | ||
| break; | ||
| } | ||
| case Blockly.Events.BLOCK_DRAG: { | ||
| const event = e as Blockly.Events.BlockDrag; | ||
| if (!event.blockId) break; | ||
| const block = this.workspace.getBlockById(event.blockId); | ||
| if (!block) break; | ||
| this.addDraggingBuffer(block); | ||
| break; | ||
| } | ||
| case Blockly.Events.VIEWPORT_CHANGE: { | ||
| if (this.immediate) break; | ||
| this.virtualize(); | ||
| break; | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| /** | ||
| * Check whether these blocks are offscreen, then update their visibility. | ||
| * This method performs in next microtask to batch multiple calls. | ||
| */ | ||
| protected virtualize(): void { | ||
| if (this.requestedCheck) return; | ||
| this.requestedCheck = true; | ||
|
|
||
| // Perform in next microtask. | ||
| Promise.resolve().then(() => { | ||
| this.requestedCheck = false; | ||
| this.virtualizeInternal(); | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * Get the current viewport rectangle in workspace coordinates. | ||
| * @returns The viewport rectangle. | ||
| */ | ||
| protected getViewportRect(): Blockly.utils.Rect { | ||
| const scale = this.workspace.getScale(); | ||
| const metrics = this.workspace.getMetrics(); | ||
| // metrics.flyoutWidth always return 0 since it's not always open. | ||
| const flyoutWidth = this.workspace.getFlyout()?.getWidth() ?? 0; | ||
| const viewLeft = (metrics.viewLeft - flyoutWidth) / scale; | ||
| const viewTop = metrics.viewTop / scale; | ||
| const viewWidth = (metrics.viewWidth + flyoutWidth) / scale; | ||
| const viewHeight = metrics.viewHeight / scale; | ||
|
|
||
| return new Blockly.utils.Rect( | ||
| viewTop, | ||
| viewTop + viewHeight, | ||
| viewLeft, | ||
| viewLeft + viewWidth | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Actual logic to check whether these blocks are offscreen, | ||
| * then update their visibility. | ||
| */ | ||
| protected virtualizeInternal(): void { | ||
| // Check workspace here since it may get called in public. | ||
| if (!this.workspace) { | ||
| this.dispose(); | ||
| return; | ||
| } | ||
|
|
||
| const viewRect = this.getViewportRect(); | ||
| const visibleBlocks = new Set(this.quadTree.query(viewRect)); | ||
| // Found the last visible blocks in their stacks. | ||
| const lastVisibleBlocks = new Set<Blockly.BlockSvg>(); | ||
| const processedIds = new Set<string>(); | ||
| for (const block of visibleBlocks) { | ||
| let current: Blockly.BlockSvg | null = block; | ||
| while (current) { | ||
| // Avoid processing the same block again. | ||
| if (processedIds.has(current.id)) break; | ||
| processedIds.add(current.id); | ||
| const next = current.getNextBlock(); | ||
| if (!next) { // Reached the end of the stack... | ||
| lastVisibleBlocks.add(current); // then it's the last visible block! | ||
| break; | ||
| } | ||
| if (!visibleBlocks.has(next)) { // Next block is not visible... | ||
| lastVisibleBlocks.add(current); // then it's the last visible block! | ||
| break; | ||
| } | ||
| current = next; | ||
| } | ||
| } | ||
|
|
||
| const blocksToHide = new Set<Blockly.BlockSvg>(); | ||
| const blocksToShow = new Set<Blockly.BlockSvg>(); | ||
|
|
||
| // Update top block's visibility | ||
| const topBlocks = this.workspace.getTopBlocks(false); | ||
| for (const block of topBlocks) { | ||
| if (!visibleBlocks.has(block)) { | ||
| blocksToHide.add(block); | ||
| } | ||
| } | ||
|
|
||
| // Hide the block after the last visible blocks if has. | ||
| for (const block of lastVisibleBlocks) { | ||
| const root = block.getRootBlock(); | ||
| blocksToHide.delete(root); | ||
| const target = block.getNextBlock(); | ||
| if (target) { | ||
| blocksToHide.add(target); | ||
| } | ||
|
|
||
| // Show previous hidden blocks | ||
| let prev: Blockly.BlockSvg | null = block; | ||
| while (prev) { | ||
| if (blocksToShow.has(prev)) break; | ||
| blocksToShow.add(prev); | ||
| blocksToHide.delete(prev); | ||
| prev = prev.getPreviousBlock(); | ||
| } | ||
| } | ||
|
|
||
| // Finally hide proposed blocks | ||
| for (const block of blocksToHide) { | ||
| if (blocksToShow.has(block)) continue; | ||
| if (!this.hiddenBlocks.has(block)) { | ||
| this.setBlockVisibility(block, false); | ||
| } | ||
| } | ||
|
|
||
| for (const block of blocksToShow) { | ||
| if (this.hiddenBlocks.has(block)) { | ||
| this.setBlockVisibility(block, true); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Display possible-visible blocks during dragging. | ||
| * @param block The block to add extra buffer blocks. | ||
| */ | ||
| protected addDraggingBuffer(block: Blockly.BlockSvg): void { | ||
| const scale = this.workspace.getScale(); | ||
| const metrics = this.workspace.getMetrics(); | ||
| const viewHeight = metrics.viewHeight / scale; | ||
| const buffer = Math.ceil(viewHeight / block.height) * 2; | ||
| let current: Blockly.BlockSvg | null = block.getNextBlock(); | ||
| for (let i = 0; i < buffer; ++i) { | ||
| if (!current) break; | ||
| if (this.hiddenBlocks.has(current)) { | ||
| this.setBlockVisibility(current, true); | ||
| } | ||
| current = current.getNextBlock(); | ||
| } | ||
| if (current) { | ||
| this.setBlockVisibility(current, false); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Check whether a block is visible. | ||
| * @param block The block to check. | ||
| * @returns Whether the block is visible. | ||
| */ | ||
| protected isBlockVisible(block: Blockly.BlockSvg): boolean { | ||
| return !this.hiddenBlocks.has(block); | ||
| } | ||
|
|
||
| /** | ||
| * Set the visibility of a block. | ||
| * @param block The block to set visibility for. | ||
| * @param visible Whether the block visible. | ||
| */ | ||
| protected setBlockVisibility(block: Blockly.BlockSvg, visible: boolean): void { | ||
| const svgRoot = block.getSvgRoot(); | ||
| if (visible) { | ||
| Blockly.utils.dom.removeClass(svgRoot, 'blocklyVirtualizedHidden'); | ||
| this.hiddenBlocks.delete(block); | ||
| } else { | ||
| Blockly.utils.dom.addClass(svgRoot, 'blocklyVirtualizedHidden'); | ||
| this.hiddenBlocks.add(block); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Start observing a block for virtualization. | ||
| * @param block The block to observe. | ||
| */ | ||
| protected observe(block: Blockly.BlockSvg): void { | ||
| if (this.observedBlocks.has(block.id)) return; | ||
| // Track statement blocks only. | ||
| if (block.outputConnection?.isConnected()) return; | ||
|
|
||
| const rect = this.getBlockBoundingRect(block); | ||
| this.observedBlocks.set(block.id, block); | ||
| this.quadTree.insert(block, rect); | ||
| } | ||
|
|
||
| /** | ||
| * Stop observing a block. | ||
| * @param blockId The block ID to stop observing. | ||
| */ | ||
| protected unobserve(blockId: string): void { | ||
| const block = this.observedBlocks.get(blockId); | ||
| if (!block) return; | ||
| this.observedBlocks.delete(blockId); | ||
| this.quadTree.remove(block); | ||
| // Make sure the block is visible when unobserved. | ||
| this.setBlockVisibility(block, true); | ||
| } | ||
|
|
||
| /** | ||
| * Update the status for observed block. | ||
| * @param block The block to update. | ||
| */ | ||
| protected updateObserve(block: Blockly.BlockSvg): void { | ||
| if (!this.observedBlocks.has(block.id)) return; | ||
| if (block.outputConnection?.isConnected()) { | ||
| this.unobserve(block.id); | ||
| return; | ||
| } | ||
|
|
||
| const rect = this.getBlockBoundingRect(block); | ||
| this.quadTree.insert(block, rect); | ||
| } | ||
|
|
||
| /** | ||
| * Get the bounding rectangle of a block relative to the workspace surface. | ||
| * Consider inputs but not next blocks. | ||
| * @param block The block to get bounding rectangle for. | ||
| * @returns The bounding rectangle. | ||
| */ | ||
| protected getBlockBoundingRect(block: Blockly.BlockSvg): Blockly.utils.Rect { | ||
| const blockXY = block.getRelativeToSurfaceXY(); | ||
| let left; | ||
| let right; | ||
| if (block.RTL) { | ||
| left = blockXY.x - block.width; | ||
| right = blockXY.x; | ||
| } else { | ||
| left = blockXY.x; | ||
| right = blockXY.x + block.width; | ||
| } | ||
| return new Blockly.utils.Rect(blockXY.y, blockXY.y + block.height, left, right); | ||
| } | ||
|
|
||
| /** | ||
| * Dispose the manager. | ||
| */ | ||
| dispose(): void { | ||
| this.observedBlocks.clear(); | ||
| this.quadTree.clear(); | ||
| if (this.workspace) { | ||
| this.workspace.removeChangeListener(this.workspaceChangeListener); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Virtualize the given workspace. Make blocks offscreen invisible. | ||
| * @param workspace The workspace to virtualize. | ||
| * @param immediate Whether update block's visibility immediately. | ||
| * Immediate mode hijacks workspace methods to update visibility immediately on viewport changes. | ||
| If false, it uses workspace change listener to update visibility, which may be delayed. and you | ||
| need to call `virtualize()` manually when window resized. | ||
| Default to true. | ||
| * @returns The VirtualizedManager instance. | ||
| */ | ||
| export function virtualize(workspace: Blockly.WorkspaceSvg, immediate = true): VirtualizedManager { | ||
| if (workspace.getFlyout()) { | ||
| virtualize(workspace.getFlyout()!.getWorkspace(), immediate); | ||
| } | ||
|
|
||
| return new VirtualizedManager(workspace, immediate); | ||
| } |
There was a problem hiding this comment.
The new VirtualizedManager and virtualize functionality lack test coverage. The tests directory shows no test files for virtualized_manager.ts or quad_tree.ts, while other similar features in the codebase have comprehensive unit tests. Given the complexity of the virtualization logic and its performance-critical nature, tests should cover: QuadTree operations (insert, remove, query, grow), block visibility toggling, viewport changes, block creation/deletion/movement events, and edge cases like workspace disposal and dragging.
There was a problem hiding this comment.
hard to actual test since there's no real browser environment now
Signed-off-by: SimonShiki <sinangentoo@gmail.com>
Signed-off-by: SimonShiki <sinangentoo@gmail.com>
Signed-off-by: SimonShiki <sinangentoo@gmail.com>
Signed-off-by: SimonShiki <sinangentoo@gmail.com>
|
|
||
| @supports not (content-visibility: hidden) { | ||
| .blocklyBlock .blocklyVirtualizedHidden { | ||
| visibility: hidden; |
There was a problem hiding this comment.
content-visibility is Baseline 2024 now, so most people can benefit from this.
visibility optimizes only on Chrome-based browsers.
|
|
||
| /* Language and Environment */ | ||
| "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ | ||
| "target": "es2021", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ |
There was a problem hiding this comment.
The oldest version to use WeakRef
Signed-off-by: SimonShiki <sinangentoo@gmail.com>
Signed-off-by: SimonShiki <sinangentoo@gmail.com>
Signed-off-by: SimonShiki <sinangentoo@gmail.com>
| /** | ||
| * Height of each buffered block during dragging, in pixels. | ||
| */ | ||
| static readonly BUFFERED_BLOCK_HEIGHT = 40; |
There was a problem hiding this comment.
Reporter's height. not sure why statement's height is wrong
| protected observe(block: Blockly.BlockSvg): void { | ||
| if (this.observedBlocks.has(block.id)) return; | ||
| // Track root-level blocks only. | ||
| if (block.outputConnection?.isConnected()) return; |
There was a problem hiding this comment.
not sure it's worth tracking all blocks. it theoretically benefits rendering performance when we reached the end of a block stack, but may slow down the intersect performance.
| const state = ScratchBlocks.loadWorkspace(JSON.parse(textarea.value), mainWorkspace, true); | ||
| } | ||
|
|
||
| function spaghetti(n = 8) { |
There was a problem hiding this comment.
port from blockly's playground. used to test performance
Signed-off-by: SimonShiki <sinangentoo@gmail.com>
Signed-off-by: SimonShiki <sinangentoo@gmail.com>
Signed-off-by: SimonShiki <sinangentoo@gmail.com>
Signed-off-by: SimonShiki <sinangentoo@gmail.com>
Overview
Blockly, as a visual programming tool, exhibits concerning performance when handling large programs. A significant bottleneck arises because Blockly renders all blocks svg in the workspace onto the screen. Once the number of blocks reaches a certain threshold, the workspace becomes extremely unresponsive. To prevent performance regression in the migrated clipcc-block, this pull request proposed an improvement: hiding all top blocks that are off-screen to enhance performance. Basic performance tests have confirmed this approach effectively improves rendering performance.
Proposals
Known Issues
maybeFireViewportChangeEventto listen that event immediately. We also provide aimmediateparam to control whether we should use hooks to listen event changes.workspace.setResizeHandlerWrapperto replace default resize handler and callvirtualizeby manual. It won't happen if you use immediate mode mentioned above.