From 0e7432051c8b44b8aca46fd5ace3ccc2968b547a Mon Sep 17 00:00:00 2001 From: danshuitaihejie <474182370@qq.com> Date: Sun, 15 Oct 2023 03:53:01 +0800 Subject: [PATCH] perf: add cache when rendering --- src/core.ts | 3 + src/positioning/Coordinates.spec.ts | 113 +++++++++++++++------------ src/positioning/Coordinates.ts | 100 ++++++++++++++++-------- src/positioning/WidthProviderFunc.ts | 52 +++++++----- src/utils/RenderingCache.ts | 22 ++++++ 5 files changed, 190 insertions(+), 100 deletions(-) create mode 100644 src/utils/RenderingCache.ts diff --git a/src/core.ts b/src/core.ts index 589cebc8f..2456ac12e 100644 --- a/src/core.ts +++ b/src/core.ts @@ -17,6 +17,7 @@ import "./themes/theme-dark.css"; import Block from "./components/DiagramFrame/SeqDiagram/MessageLayer/Block/Block.vue"; import Comment from "./components/DiagramFrame/SeqDiagram/MessageLayer/Block/Statement/Comment/Comment.vue"; import { getStartTime, calculateCostTime } from "./utils/CostTime"; +import { clearCache } from "./utils/RenderingCache"; const logger = parentLogger.child({ name: "core" }); interface Config { @@ -78,7 +79,9 @@ export default class ZenUml implements IZenUml { this.store.state.stickyOffset = config?.stickyOffset || 0; this.store.state.theme = this._theme || "default"; this._currentTimeout = setTimeout(async () => { + console.debug("rendering start"); const start = getStartTime(); + clearCache(); this.store.commit( "onContentChange", config?.onContentChange || (() => {}), diff --git a/src/positioning/Coordinates.spec.ts b/src/positioning/Coordinates.spec.ts index 5435639ba..5a6dbc80d 100644 --- a/src/positioning/Coordinates.spec.ts +++ b/src/positioning/Coordinates.spec.ts @@ -1,94 +1,109 @@ -import { RootContext } from '@/parser'; +import { RootContext } from "@/parser"; // max(MIN_GAP, old_g, new_g, w/2 + left-part-w/2 + MARGIN) -import {ARROW_HEAD_WIDTH, MARGIN, MIN_PARTICIPANT_WIDTH, OCCURRENCE_WIDTH} from '@/positioning/Constants'; -import { Coordinates } from './Coordinates'; -import { stubWidthProvider } from '../../test/unit/parser/fixture/Fixture'; +import { + ARROW_HEAD_WIDTH, + MARGIN, + MIN_PARTICIPANT_WIDTH, + OCCURRENCE_WIDTH, +} from "@/positioning/Constants"; +import { Coordinates } from "./Coordinates"; +import { stubWidthProvider } from "../../test/unit/parser/fixture/Fixture"; +import { clearCache } from "@/utils/RenderingCache"; +describe("get absolute position of a participant", () => { + beforeEach(() => { + clearCache(); + }); -describe('get absolute position of a participant', () => { - it('One wide participant', () => { - let rootContext = RootContext('A300'); + it("One wide participant", () => { + const rootContext = RootContext("A300"); const coordinates = new Coordinates(rootContext, stubWidthProvider); - expect(coordinates.getPosition('A300')).toBe(160); + expect(coordinates.getPosition("A300")).toBe(160); }); - it('wide participant label and error scenario', () => { - let rootContext = RootContext('A200 group {B300} C400'); + it("wide participant label and error scenario", () => { + const rootContext = RootContext("A200 group {B300} C400"); const coordinates = new Coordinates(rootContext, stubWidthProvider); - expect(() => coordinates.getPosition('NotExist')).toThrow('Participant NotExist not found'); - expect(coordinates.getPosition('A200')).toBe(110); - expect(coordinates.getPosition('B300')).toBe(380); - expect(coordinates.getPosition('C400')).toBe(750); + expect(() => coordinates.getPosition("NotExist")).toThrow( + "Participant NotExist not found", + ); + expect(coordinates.getPosition("A200")).toBe(110); + expect(coordinates.getPosition("B300")).toBe(380); + expect(coordinates.getPosition("C400")).toBe(750); expect(coordinates.getWidth()).toBe(960); }); it.each([ - ['A1 B1', 0, 80, 240], - ['A1 group {B1}', 0, 80, 240], // group does not change absolute positions - ])('Use MINI_GAP (100) for %s', (code, posStarter, posA1, posB1) => { - let rootContext = RootContext(code); + ["A1 B1", 0, 80, 240], + ["A1 group {B1}", 0, 80, 240], // group does not change absolute positions + ])("Use MINI_GAP (100) for %s", (code, posStarter, posA1, posB1) => { + const rootContext = RootContext(code); const coordinates = new Coordinates(rootContext, stubWidthProvider); - expect(coordinates.getPosition('_STARTER_')).toBe(posStarter); + expect(coordinates.getPosition("_STARTER_")).toBe(posStarter); // margin for _STARTER_ + half MINI_GAP - expect(coordinates.getPosition('A1')).toBe(posA1); + expect(coordinates.getPosition("A1")).toBe(posA1); // margin + half MINI_GAP + position of A1 - expect(coordinates.getPosition('B1')).toBe(posB1); + expect(coordinates.getPosition("B1")).toBe(posB1); }); - it('wide method', () => { - let rootContext = RootContext('A1.m800'); + it("wide method", () => { + const rootContext = RootContext("A1.m800"); const coordinates = new Coordinates(rootContext, stubWidthProvider); - expect(coordinates.getPosition('_STARTER_')).toBe(0); - expect(coordinates.getPosition('A1')).toBe(824); + expect(coordinates.getPosition("_STARTER_")).toBe(0); + expect(coordinates.getPosition("A1")).toBe(824); }); - it('should not duplicate participants', () => { - let rootContext = RootContext('A1.a1 A1.a1 B1.a1'); + it("should not duplicate participants", () => { + const rootContext = RootContext("A1.a1 A1.a1 B1.a1"); const coordinates = new Coordinates(rootContext, stubWidthProvider); - expect(coordinates.getPosition('_STARTER_')).toBe(0); - expect(coordinates.getPosition('A1')).toBe(80); - expect(coordinates.getPosition('B1')).toBe(240); + expect(coordinates.getPosition("_STARTER_")).toBe(0); + expect(coordinates.getPosition("A1")).toBe(80); + expect(coordinates.getPosition("B1")).toBe(240); }); it.each([ - ['new A1', 'A1', 104], - ['new A200', 'A200', 134], - ])('creation method: %s', (code, name, pos) => { - let rootContext = RootContext(code); + ["new A1", "A1", 104], + ["new A200", "A200", 134], + ])("creation method: %s", (code, name, pos) => { + const rootContext = RootContext(code); const coordinates = new Coordinates(rootContext, stubWidthProvider); - expect(coordinates.getPosition('_STARTER_')).toBe(0); + expect(coordinates.getPosition("_STARTER_")).toBe(0); // half participant width + Starter Position + margin expect(coordinates.getPosition(name)).toBe(pos); }); it.each([ - ['A1->B1: m1\nB1->C1: m1\nA1->C1: m800'], - ['A1->B1: m1\nB1->C1: m1\nC1->A1: m800'], // backwards - ['A1->B1: m1\nB1->C1: m1\nB1->C1: m1\nC1->A1: m800'], // repeating message B1->C1:m1 - ])('non-adjacent long message: %s', (code: string) => { + ["A1->B1: m1\nB1->C1: m1\nA1->C1: m800"], + ["A1->B1: m1\nB1->C1: m1\nC1->A1: m800"], // backwards + ["A1->B1: m1\nB1->C1: m1\nB1->C1: m1\nC1->A1: m800"], // repeating message B1->C1:m1 + ])("non-adjacent long message: %s", (code: string) => { const messageLength = 820; - let rootContext = RootContext(code); + const rootContext = RootContext(code); const coordinates = new Coordinates(rootContext, stubWidthProvider); const positionA = MIN_PARTICIPANT_WIDTH / 2 + MARGIN / 2; - expect(coordinates.getPosition('A1')).toBe(80); //70 + expect(coordinates.getPosition("A1")).toBe(80); //70 // position is optimised for even distribution - expect(coordinates.getPosition('B1')).toBe(492); //190 + expect(coordinates.getPosition("B1")).toBe(492); //190 // positionC is not impacted by position of B1 - const positionC = messageLength + positionA + ARROW_HEAD_WIDTH + OCCURRENCE_WIDTH; - expect(coordinates.getPosition('C1')).toBe(positionC); + const positionC = + messageLength + positionA + ARROW_HEAD_WIDTH + OCCURRENCE_WIDTH; + expect(coordinates.getPosition("C1")).toBe(positionC); }); }); -describe('Let us focus on order', () => { - it('should add Starter to the left', () => { - let rootContext = RootContext('A1 B1->A1:m1'); +describe("Let us focus on order", () => { + beforeEach(() => { + clearCache(); + }); + it("should add Starter to the left", () => { + const rootContext = RootContext("A1 B1->A1:m1"); const coordinates = new Coordinates(rootContext, stubWidthProvider); - expect(coordinates.getPosition('B1')).toBe(80); - expect(coordinates.getPosition('A1')).toBe(240); + expect(coordinates.getPosition("B1")).toBe(80); + expect(coordinates.getPosition("A1")).toBe(240); }); }); diff --git a/src/positioning/Coordinates.ts b/src/positioning/Coordinates.ts index ff5f66b2e..d0060ee0a 100644 --- a/src/positioning/Coordinates.ts +++ b/src/positioning/Coordinates.ts @@ -1,10 +1,17 @@ -import {ARROW_HEAD_WIDTH, MARGIN, MIN_PARTICIPANT_WIDTH, MINI_GAP, OCCURRENCE_WIDTH} from './Constants'; -import {TextType, WidthFunc} from './Coordinate'; -import {OrderedParticipants} from './OrderedParticipants'; -import {IParticipantModel} from './ParticipantListener'; -import {find_optimal} from './david/DavidEisenstat'; -import {AllMessages} from './MessageContextListener'; -import {OwnableMessage, OwnableMessageType} from './OwnableMessage'; +import { + ARROW_HEAD_WIDTH, + MARGIN, + MIN_PARTICIPANT_WIDTH, + MINI_GAP, + OCCURRENCE_WIDTH, +} from "./Constants"; +import { TextType, WidthFunc } from "./Coordinate"; +import { OrderedParticipants } from "./OrderedParticipants"; +import { IParticipantModel } from "./ParticipantListener"; +import { find_optimal } from "./david/DavidEisenstat"; +import { AllMessages } from "./MessageContextListener"; +import { OwnableMessage, OwnableMessageType } from "./OwnableMessage"; +import { getCache, setCache } from "./../utils/RenderingCache"; export class Coordinates { private m: Array> = []; @@ -25,12 +32,20 @@ export class Coordinates { } getPosition(participantName: string | undefined): number { - const pIndex = this.participantModels.findIndex((p) => p.name === participantName); + const cacheKey = `getPosition_${participantName}`; + const cachedPosition = getCache(cacheKey); + if (cachedPosition != null) { + return cachedPosition; + } + const pIndex = this.participantModels.findIndex( + (p) => p.name === participantName, + ); if (pIndex === -1) { throw Error(`Participant ${participantName} not found`); } const leftGap = this.getParticipantGap(this.participantModels[0]); const position = leftGap + find_optimal(this.m)[pIndex]; + setCache(cacheKey, position); console.debug(`Position of ${participantName} is ${position}`); return position; } @@ -42,32 +57,39 @@ export class Coordinates { private withMessageGaps( ownableMessages: OwnableMessage[], - participantModels: IParticipantModel[] + participantModels: IParticipantModel[], ) { ownableMessages.forEach((message) => { - const indexFrom = participantModels.findIndex((p) => p.name === message.from); + const indexFrom = participantModels.findIndex( + (p) => p.name === message.from, + ); const indexTo = participantModels.findIndex((p) => p.name === message.to); if (indexFrom === -1 || indexTo === -1) { console.warn(`Participant ${message.from} or ${message.to} not found`); return; } - let leftIndex = Math.min(indexFrom, indexTo); - let rightIndex = Math.max(indexFrom, indexTo); + const leftIndex = Math.min(indexFrom, indexTo); + const rightIndex = Math.max(indexFrom, indexTo); try { - let messageWidth = this.getMessageWidth(message); + const messageWidth = this.getMessageWidth(message); this.m[leftIndex][rightIndex] = Math.max( messageWidth + ARROW_HEAD_WIDTH + OCCURRENCE_WIDTH, - this.m[leftIndex][rightIndex] + this.m[leftIndex][rightIndex], ); } catch (e) { - console.warn(`Could not set message gap between ${message.from} and ${message.to}`); + console.warn( + `Could not set message gap between ${message.from} and ${message.to}`, + ); } }); } private getMessageWidth(message: OwnableMessage) { const halfSelf = Coordinates.half(this.widthProvider, message.to); - let messageWidth = this.widthProvider(message.signature, TextType.MessageContent); + let messageWidth = this.widthProvider( + message.signature, + TextType.MessageContent, + ); // hack for creation message if (message.type === OwnableMessageType.CreationMessage) { messageWidth += halfSelf; @@ -83,42 +105,59 @@ export class Coordinates { } private getParticipantGap(p: IParticipantModel) { - let leftNameOrLabel = this.labelOrName(p.left); + const leftNameOrLabel = this.labelOrName(p.left); const halfLeft = Coordinates.half(this.widthProvider, leftNameOrLabel); const halfSelf = Coordinates.half(this.widthProvider, p.label || p.name); // TODO: convert name to enum type - const leftIsVisible = p.left && p.left !== '_STARTER_'; - const selfIsVisible = p.name && p.name !== '_STARTER_'; - return ((leftIsVisible && halfLeft) || 0) + ((selfIsVisible && halfSelf) || 0); + const leftIsVisible = p.left && p.left !== "_STARTER_"; + const selfIsVisible = p.name && p.name !== "_STARTER_"; + return ( + ((leftIsVisible && halfLeft) || 0) + ((selfIsVisible && halfSelf) || 0) + ); } private labelOrName(name: string) { const pIndex = this.participantModels.findIndex((p) => p.name === name); - if (pIndex === -1) return ''; - return this.participantModels[pIndex].label || this.participantModels[pIndex].name; + if (pIndex === -1) return ""; + return ( + this.participantModels[pIndex].label || + this.participantModels[pIndex].name + ); } static half(widthProvider: WidthFunc, participantName: string | undefined) { - if (participantName === '_STARTER_') { + if (participantName === "_STARTER_") { return MARGIN / 2; } - const halfLeftParticipantWidth = this.halfWithMargin(widthProvider, participantName); + const halfLeftParticipantWidth = this.halfWithMargin( + widthProvider, + participantName, + ); return Math.max(halfLeftParticipantWidth, MINI_GAP / 2); } - private static halfWithMargin(widthProvider: WidthFunc, participant: string | undefined) { - return this._getParticipantWidth(widthProvider, participant) / 2 + MARGIN / 2; + private static halfWithMargin( + widthProvider: WidthFunc, + participant: string | undefined, + ) { + return ( + this._getParticipantWidth(widthProvider, participant) / 2 + MARGIN / 2 + ); } - private static _getParticipantWidth(widthProvider: WidthFunc, participant: string | undefined) { + private static _getParticipantWidth( + widthProvider: WidthFunc, + participant: string | undefined, + ) { return Math.max( - widthProvider(participant || '', TextType.ParticipantName), - MIN_PARTICIPANT_WIDTH + widthProvider(participant || "", TextType.ParticipantName), + MIN_PARTICIPANT_WIDTH, ); } getWidth() { - const lastParticipant = this.participantModels[this.participantModels.length - 1].name; + const lastParticipant = + this.participantModels[this.participantModels.length - 1].name; const calculatedWidth = this.getPosition(lastParticipant) + Coordinates.half(this.widthProvider, lastParticipant); @@ -128,5 +167,4 @@ export class Coordinates { distance(left: string, right: string) { return this.getPosition(right) - this.getPosition(left); } - } diff --git a/src/positioning/WidthProviderFunc.ts b/src/positioning/WidthProviderFunc.ts index cb7c7fcbd..fe036945a 100644 --- a/src/positioning/WidthProviderFunc.ts +++ b/src/positioning/WidthProviderFunc.ts @@ -1,26 +1,37 @@ -import {TextType} from '@/positioning/Coordinate'; - -export default function WidthProviderOnBrowser(text: string, type: TextType): number { - let hiddenDiv = document.querySelector('.textarea-hidden-div') as HTMLDivElement; +import { TextType } from "@/positioning/Coordinate"; +import { getCache, setCache } from "./../utils/RenderingCache"; +export default function WidthProviderOnBrowser( + text: string, + type: TextType, +): number { + const cacheKey = `WidthProviderOnBrowser_${text}_${type}`; + const cacheValue = getCache(cacheKey); + if (cacheValue != null) { + return cacheValue; + } + let hiddenDiv = document.querySelector( + ".textarea-hidden-div", + ) as HTMLDivElement; if (!hiddenDiv) { - const newDiv = document.createElement('div'); - newDiv.className = 'textarea-hidden-div '; - newDiv.style.fontSize = (type === TextType.MessageContent) ? '0.875rem' : '1rem'; - newDiv.style.fontFamily = 'Helvetica, Verdana, serif'; - newDiv.style.display = 'inline'; + const newDiv = document.createElement("div"); + newDiv.className = "textarea-hidden-div "; + newDiv.style.fontSize = + type === TextType.MessageContent ? "0.875rem" : "1rem"; + newDiv.style.fontFamily = "Helvetica, Verdana, serif"; + newDiv.style.display = "inline"; // newDiv.style.zIndex = '-9999'; - newDiv.style.whiteSpace = 'nowrap'; - newDiv.style.visibility = 'hidden'; - newDiv.style.position = 'absolute'; - newDiv.style.top = '0'; - newDiv.style.left = '0'; - newDiv.style.overflow = 'hidden'; - newDiv.style.width = '0px'; + newDiv.style.whiteSpace = "nowrap"; + newDiv.style.visibility = "hidden"; + newDiv.style.position = "absolute"; + newDiv.style.top = "0"; + newDiv.style.left = "0"; + newDiv.style.overflow = "hidden"; + newDiv.style.width = "0px"; // newDiv.style.height = '0px'; - newDiv.style.paddingLeft = '0px'; - newDiv.style.paddingRight = '0px'; - newDiv.style.margin = '0px'; - newDiv.style.border = '0px'; + newDiv.style.paddingLeft = "0px"; + newDiv.style.paddingRight = "0px"; + newDiv.style.margin = "0px"; + newDiv.style.border = "0px"; document.body.appendChild(newDiv); hiddenDiv = newDiv; } @@ -28,5 +39,6 @@ export default function WidthProviderOnBrowser(text: string, type: TextType): nu hiddenDiv.textContent = text; const scrollWidth = hiddenDiv.scrollWidth; + setCache(cacheKey, scrollWidth, true); return scrollWidth; } diff --git a/src/utils/RenderingCache.ts b/src/utils/RenderingCache.ts new file mode 100644 index 000000000..0fb375e68 --- /dev/null +++ b/src/utils/RenderingCache.ts @@ -0,0 +1,22 @@ +let dic: Record = {}; +const persistDic: Record = {}; + +export const getCache = (key: string | undefined): any => { + if (key != null) { + const cacheValue = dic[key] ?? persistDic[key]; + return cacheValue !== undefined ? cacheValue : null; + } + return null; +}; + +export const setCache = (key: string, value: any, persist: boolean = false) => { + dic[key] = value; + if (persist) { + persistDic[key] = value; + } +}; + +//Need call clearCache before rendering. +export const clearCache = () => { + dic = {}; +};