From 1dbc91acac4996c39ca2aad712d67ff293a346cb Mon Sep 17 00:00:00 2001 From: kanno <812137533@qq.com> Date: Tue, 26 Nov 2024 10:28:12 +0800 Subject: [PATCH] make cache happy (#7) * feat: reset should cleanup loc * do some optimize * feat: support cache * feat: make clone fast --- src/etoile/graph/box.ts | 18 ++++++++-------- src/etoile/graph/display.ts | 43 +++++++++++++++++++++---------------- src/etoile/graph/layer.ts | 7 +++++- src/primitives/component.ts | 3 +++ src/primitives/event.ts | 38 +++++++++++++++++++++----------- 5 files changed, 68 insertions(+), 41 deletions(-) diff --git a/src/etoile/graph/box.ts b/src/etoile/graph/box.ts index f379d94..a43890b 100644 --- a/src/etoile/graph/box.ts +++ b/src/etoile/graph/box.ts @@ -85,25 +85,25 @@ export class Box extends C { clone() { const box = new Box() if (this.elements.length) { - const traverse = (elements: Display[], parent: Box) => { - const els: Display[] = [] + const stack: { elements: Display[]; parent: Box }[] = [{ elements: this.elements, parent: box }] + + while (stack.length > 0) { + const { elements, parent } = stack.pop()! const cap = elements.length for (let i = 0; i < cap; i++) { const element = elements[i] if (asserts.isBox(element)) { - const box = new Box() - box.parent = parent - box.add(...traverse(element.elements, box)) - els.push(box) + const newBox = new Box() + newBox.parent = parent + parent.add(newBox) + stack.push({ elements: element.elements, parent: newBox }) } else if (asserts.isGraph(element)) { const el = element.clone() el.parent = parent - els.push(el) + parent.add(el) } } - return els } - box.add(...traverse(this.elements, box)) } return box } diff --git a/src/etoile/graph/display.ts b/src/etoile/graph/display.ts index 082d25c..baf0e7e 100644 --- a/src/etoile/graph/display.ts +++ b/src/etoile/graph/display.ts @@ -81,47 +81,52 @@ type Mod< > = T[K] extends (...args: any) => any ? [K, Parameters] : never interface Instruction extends InstructionAssignMappings, InstructionWithFunctionCall { - mods: Mod[] + mods: Array<{ mod: Mod; type: number }> } const ASSIGN_MAPPINGS = { - fillStyle: !0, - strokeStyle: !0, - font: !0, - lineWidth: !0, - textAlign: !0, - textBaseline: !0 -} + fillStyle: 0o1, + strokeStyle: 0o2, + font: 0o4, + lineWidth: 0o10, + textAlign: 0o20, + textBaseline: 0o40 +} as const + +export const ASSIGN_MAPPINGS_MODE = ASSIGN_MAPPINGS.fillStyle | ASSIGN_MAPPINGS.strokeStyle | ASSIGN_MAPPINGS.font | + ASSIGN_MAPPINGS.lineWidth | ASSIGN_MAPPINGS.textAlign | ASSIGN_MAPPINGS.textBaseline + +export const CALL_MAPPINGS_MODE = 0o0 function createInstruction() { return { mods: [], fillStyle(...args) { - this.mods.push(['fillStyle', args]) + this.mods.push({ mod: ['fillStyle', args], type: ASSIGN_MAPPINGS.fillStyle }) }, fillRect(...args) { - this.mods.push(['fillRect', args]) + this.mods.push({ mod: ['fillRect', args], type: CALL_MAPPINGS_MODE }) }, strokeStyle(...args) { - this.mods.push(['strokeStyle', args]) + this.mods.push({ mod: ['strokeStyle', args], type: ASSIGN_MAPPINGS.strokeStyle }) }, lineWidth(...args) { - this.mods.push(['lineWidth', args]) + this.mods.push({ mod: ['lineWidth', args], type: ASSIGN_MAPPINGS.lineWidth }) }, strokeRect(...args) { - this.mods.push(['strokeRect', args]) + this.mods.push({ mod: ['strokeRect', args], type: CALL_MAPPINGS_MODE }) }, fillText(...args) { - this.mods.push(['fillText', args]) + this.mods.push({ mod: ['fillText', args], type: CALL_MAPPINGS_MODE }) }, font(...args) { - this.mods.push(['font', args]) + this.mods.push({ mod: ['font', args], type: ASSIGN_MAPPINGS.font }) }, textBaseline(...args) { - this.mods.push(['textBaseline', args]) + this.mods.push({ mod: ['textBaseline', args], type: ASSIGN_MAPPINGS.textBaseline }) }, textAlign(...args) { - this.mods.push(['textAlign', args]) + this.mods.push({ mod: ['textAlign', args], type: ASSIGN_MAPPINGS.textAlign }) } } } @@ -168,9 +173,9 @@ export abstract class Graph extends S { const cap = this.instruction.mods.length for (let i = 0; i < cap; i++) { - const mod = this.instruction.mods[i] + const { mod, type } = this.instruction.mods[i] const [direct, ...args] = mod - if (direct in ASSIGN_MAPPINGS) { + if (type & ASSIGN_MAPPINGS_MODE) { // @ts-expect-error ctx[direct] = args[0] continue diff --git a/src/etoile/graph/layer.ts b/src/etoile/graph/layer.ts index 13c90f9..813d5ad 100644 --- a/src/etoile/graph/layer.ts +++ b/src/etoile/graph/layer.ts @@ -44,10 +44,15 @@ export class Layer extends C implements S { writeBoundingRectForCanvas(this.c.c.canvas, options.width || 0, options.height || 0, options.devicePixelRatio || 1) } - setCacheSnapshot(c: HTMLCanvasElement) { + cleanCacheSnapshot() { const dpr = this.options.devicePixelRatio || 1 const matrix = this.matrix.create({ a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 }) this.ctx.clearRect(0, 0, this.options.width, this.options.height) + return { dpr, matrix } + } + + setCacheSnapshot(c: HTMLCanvasElement) { + const { matrix, dpr } = this.cleanCacheSnapshot() matrix.transform(this.x, this.y, this.scaleX, this.scaleY, this.rotation, this.skewX, this.skewY) applyCanvasTransform(this.ctx, matrix, dpr) this.ctx.drawImage(c, 0, 0, this.options.width / dpr, this.options.height / dpr) diff --git a/src/primitives/component.ts b/src/primitives/component.ts index 1084a29..585c0ff 100644 --- a/src/primitives/component.ts +++ b/src/primitives/component.ts @@ -177,6 +177,9 @@ export class TreemapLayout extends etoile.Schedule { for (const node of this.layoutNodes) { this.drawBackgroundNode(node) } + } else { + // Unlike foreground layer, background laer don't need clone so we should reset the loc informaton + this.bgLayer.initLoc() } if (!this.fgBox.elements.length || refresh) { this.render.ctx.textBaseline = 'middle' diff --git a/src/primitives/event.ts b/src/primitives/event.ts index 5e43030..f29d3e4 100644 --- a/src/primitives/event.ts +++ b/src/primitives/event.ts @@ -5,7 +5,7 @@ import { createFillBlock, mixin } from '../shared' import { Display, S } from '../etoile/graph/display' -import { Schedule, Event as _Event, easing, etoile } from '../etoile' +import { Schedule, Event as _Event, asserts, drawGraphIntoCanvas, easing, etoile } from '../etoile' import type { BindThisParameter } from '../etoile' import type { ColorDecoratorResultRGB } from '../etoile/native/runtime' import type { InheritedCollections } from '../shared' @@ -251,7 +251,6 @@ export class SelfEvent extends RegisterModule { refreshBackgroundLayer(this) } this.treemap.reset() - stackMatrixTransform(this.treemap.backgroundLayer, 0, 0, 0) stackMatrixTransformWithGraphAndLayer(this.treemap.elements, this.self.translateX, this.self.translateY, this.self.scaleRatio) this.treemap.update() } @@ -302,7 +301,6 @@ export class SelfEvent extends RegisterModule { treemap.reset() this.self.highlight.reset() this.self.highlight.setDisplayLayerForHighlight() - stackMatrixTransform(this.treemap.backgroundLayer, 0, 0, 0) const factor = absWheelDelta > 3 ? 1.4 : absWheelDelta > 1 ? 1.2 : 1.1 const delta = wheelDelta > 0 ? factor : 1 / factor self.scaleRatio *= delta @@ -431,6 +429,11 @@ function onZoom(ctx: SelfEventContenxt, node: LayoutModule, root: LayoutModule | const c = treemap.render.canvas const boundingClientRect = c.getBoundingClientRect() const [w, h] = estimateZoomingArea(node, root, boundingClientRect.width, boundingClientRect.height) + if (self.layoutHeight !== w || self.layoutHeight !== h) { + // remove font caches + delete treemap.fontsCaches[node.node.id] + delete treemap.ellispsisWidthCache[node.node.id] + } resetLayout(treemap, w, h) const module = findRelativeNodeById(node.node.id, treemap.layoutNodes) if (module) { @@ -443,11 +446,7 @@ function onZoom(ctx: SelfEventContenxt, node: LayoutModule, root: LayoutModule | const initialTranslateY = self.translateY const startTime = Date.now() const animationDuration = 300 - if (self.layoutHeight !== w || self.layoutHeight !== h) { - // remove font caches - delete treemap.fontsCaches[module.node.id] - delete treemap.ellispsisWidthCache[module.node.id] - } + const { run, stop } = createEffectScope() run(() => { const elapsed = Date.now() - startTime @@ -524,9 +523,24 @@ function createHighlight(): HighlightContext { } } -// TODO: cache the background layer -function refreshBackgroundLayer(c: SelfEventContenxt) { - const { treemap } = c - const { backgroundLayer } = treemap +function refreshBackgroundLayer(c: SelfEventContenxt): boolean | void { + const { treemap, self } = c + const { backgroundLayer, render } = treemap + const { canvas, ctx, options: { width: ow, height: oh } } = render + const { layoutWidth: sw, layoutHeight: sh, scaleRatio: ss } = self + + const capture = sw * ss >= ow || sh * ss >= oh backgroundLayer.__refresh__ = false + if (!capture) { + resetLayout(treemap, sw * ss, sh * ss) + render.clear(ow, oh) + const { dpr } = backgroundLayer.cleanCacheSnapshot() + drawGraphIntoCanvas(backgroundLayer, { c: canvas, ctx, dpr }, (opts, graph) => { + if (asserts.isLayer(graph) && !graph.__refresh__) { + graph.setCacheSnapshot(opts.c) + } + }) + self.triggerZoom = false + return true + } }