Skip to content

Commit

Permalink
perf: add cache when rendering
Browse files Browse the repository at this point in the history
  • Loading branch information
danshuitaihejie committed Oct 14, 2023
1 parent 8ac23c6 commit 0e74320
Show file tree
Hide file tree
Showing 5 changed files with 190 additions and 100 deletions.
3 changes: 3 additions & 0 deletions src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 || (() => {}),
Expand Down
113 changes: 64 additions & 49 deletions src/positioning/Coordinates.spec.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
100 changes: 69 additions & 31 deletions src/positioning/Coordinates.ts
Original file line number Diff line number Diff line change
@@ -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<Array<number>> = [];
Expand All @@ -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;
}
Expand All @@ -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;
Expand All @@ -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);
Expand All @@ -128,5 +167,4 @@ export class Coordinates {
distance(left: string, right: string) {
return this.getPosition(right) - this.getPosition(left);
}

}
Loading

0 comments on commit 0e74320

Please sign in to comment.