Skip to content

Commit 2b5dc13

Browse files
committed
fix: fix bug of animation end
1 parent d222b6a commit 2b5dc13

27 files changed

Lines changed: 922 additions & 271 deletions
Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
import { performance } from 'perf_hooks';
2+
import { createRect } from '@visactor/vrender-core';
3+
import { Animate } from '../../src/animate';
4+
import { commitAnimationStaticAttrs } from '../../src/custom/static-truth';
5+
import { applyAnimationFrameAttributes, applyAnimationTransientAttributes } from '../../src/custom/transient';
6+
import { Step } from '../../src/step';
7+
8+
const runPerf = process.env.VRENDER_ANIMATE_PERF === '1' ? describe : describe.skip;
9+
const fakeTimeline = {
10+
addAnimate() {
11+
// benchmark no-op
12+
},
13+
removeAnimate() {
14+
// benchmark no-op
15+
}
16+
} as any;
17+
18+
function createTarget() {
19+
return {
20+
attribute: { x: 0, width: 10 },
21+
applyTransientAttributes(attrs: Record<string, any>) {
22+
Object.assign(this.attribute, attrs);
23+
},
24+
addUpdateBoundTag() {
25+
// benchmark no-op
26+
},
27+
addUpdatePositionTag() {
28+
// benchmark no-op
29+
},
30+
addUpdateShapeAndBoundsTag() {
31+
// benchmark no-op
32+
},
33+
onAttributeUpdate() {
34+
// benchmark no-op
35+
}
36+
} as any;
37+
}
38+
39+
function createPreparedStep(target: any) {
40+
const step = new Step('to' as any, { x: 100, width: 20 }, 100, 'linear') as any;
41+
step.target = target;
42+
step.animate = {
43+
target,
44+
interpolateUpdateFunction: null,
45+
validAttr: () => true,
46+
getStartProps: () => ({ x: 0, width: 10 })
47+
};
48+
step.props = { x: 100, width: 20 };
49+
step.propKeys = ['x', 'width'];
50+
step.fromProps = { x: 0, width: 10 };
51+
step.determineInterpolateUpdateFunction();
52+
return step;
53+
}
54+
55+
function timeLoop(label: string, fn: () => void): number {
56+
const start = performance.now();
57+
fn();
58+
const duration = performance.now() - start;
59+
process.stdout.write(`${label}: ${duration.toFixed(3)}ms\n`);
60+
return duration;
61+
}
62+
63+
function runLegacyDirectInterpolation(step: any, target: any, iterations: number) {
64+
const funcs = step.interpolateUpdateFunctions;
65+
const propKeys = step.propKeys;
66+
const from = step.fromProps;
67+
const to = step.props;
68+
69+
for (let i = 0; i < iterations; i++) {
70+
const ratio = (i % 100) / 100;
71+
for (let j = 0; j < funcs.length; j++) {
72+
const key = propKeys[j];
73+
funcs[j](key, from[key], to[key], ratio, step, target);
74+
}
75+
}
76+
}
77+
78+
function runCurrentFastInterpolation(step: any, iterations: number) {
79+
for (let i = 0; i < iterations; i++) {
80+
step.runInterpolateUpdate(step.fromProps, step.props, (i % 100) / 100);
81+
}
82+
}
83+
84+
function createPreventStep(keys: string[]) {
85+
const props: Record<string, number> = {};
86+
const fromProps: Record<string, number> = {};
87+
keys.forEach((key, index) => {
88+
props[key] = index + 100;
89+
fromProps[key] = index;
90+
});
91+
92+
const step = new Step('to' as any, props, 100, 'linear') as any;
93+
step.props = props;
94+
step.fromProps = fromProps;
95+
step.propKeys = keys.slice();
96+
step.interpolateUpdateFunctions = keys.map(() => () => {
97+
// benchmark no-op
98+
});
99+
return step;
100+
}
101+
102+
function createPreventAnimate(keys: string[], stepCount: number) {
103+
const animate = new Animate(undefined, fakeTimeline, true) as any;
104+
animate._startProps = {};
105+
animate._endProps = {};
106+
keys.forEach((key, index) => {
107+
animate._startProps[key] = index;
108+
animate._endProps[key] = index + 100;
109+
});
110+
111+
let firstStep: any = null;
112+
let lastStep: any = null;
113+
for (let i = 0; i < stepCount; i++) {
114+
const step = createPreventStep(keys);
115+
if (!firstStep) {
116+
firstStep = step;
117+
}
118+
if (lastStep) {
119+
lastStep.next = step;
120+
}
121+
lastStep = step;
122+
}
123+
animate._firstStep = firstStep;
124+
animate._lastStep = lastStep;
125+
return animate as Animate;
126+
}
127+
128+
function createCommitTarget() {
129+
const finalAttrs = {
130+
x: 100,
131+
y: 20,
132+
y1: 200,
133+
width: 30,
134+
fillOpacity: 0.8,
135+
lineWidth: 2
136+
};
137+
return {
138+
attribute: {},
139+
baseAttributes: {},
140+
finalAttribute: finalAttrs,
141+
context: { finalAttrs },
142+
getFinalAttribute() {
143+
return this.finalAttribute;
144+
},
145+
setAttributes(attrs: Record<string, any>) {
146+
const baseAttributes = this.baseAttributes as Record<string, any>;
147+
const attribute = this.attribute as Record<string, any>;
148+
const finalAttribute = this.finalAttribute as Record<string, any>;
149+
for (const key in attrs) {
150+
if (Object.prototype.hasOwnProperty.call(attrs, key)) {
151+
baseAttributes[key] = attrs[key];
152+
attribute[key] = attrs[key];
153+
finalAttribute[key] = attrs[key];
154+
}
155+
}
156+
}
157+
} as any;
158+
}
159+
160+
runPerf('animation frame performance baseline', () => {
161+
test('default numeric interpolation fast path stays within the configured baseline ratio', () => {
162+
const iterations = Number(process.env.VRENDER_ANIMATE_PERF_ITERATIONS ?? 1_000_000);
163+
const maxRatio =
164+
process.env.VRENDER_ANIMATE_PERF_MAX_RATIO == null
165+
? Number.POSITIVE_INFINITY
166+
: Number(process.env.VRENDER_ANIMATE_PERF_MAX_RATIO);
167+
168+
const directTarget = createTarget();
169+
const directStep = createPreparedStep(directTarget);
170+
const currentTarget = createTarget();
171+
const currentStep = createPreparedStep(currentTarget);
172+
173+
runLegacyDirectInterpolation(directStep, directTarget, Math.min(iterations, 50_000));
174+
runCurrentFastInterpolation(currentStep, Math.min(iterations, 50_000));
175+
176+
const direct = timeLoop('legacy-direct-frame-write', () => {
177+
runLegacyDirectInterpolation(directStep, directTarget, iterations);
178+
});
179+
const current = timeLoop('current-fast-frame-write', () => {
180+
runCurrentFastInterpolation(currentStep, iterations);
181+
});
182+
const ratio = current / direct;
183+
184+
process.stdout.write(
185+
JSON.stringify({
186+
benchmark: 'animation-frame-default-interpolation',
187+
iterations,
188+
directMs: Number(direct.toFixed(3)),
189+
currentMs: Number(current.toFixed(3)),
190+
ratio: Number(ratio.toFixed(3)),
191+
maxRatio: Number.isFinite(maxRatio) ? maxRatio : null
192+
}) + '\n'
193+
);
194+
195+
expect(ratio).toBeLessThanOrEqual(maxRatio);
196+
});
197+
198+
test('batched conflict prevention reports ownership cleanup cost', () => {
199+
const animateCount = Number(process.env.VRENDER_ANIMATE_PERF_ANIMATES ?? 20_000);
200+
const stepCount = Number(process.env.VRENDER_ANIMATE_PERF_STEPS ?? 3);
201+
const maxMs =
202+
process.env.VRENDER_ANIMATE_PERF_MAX_CONFLICT_MS == null
203+
? Number.POSITIVE_INFINITY
204+
: Number(process.env.VRENDER_ANIMATE_PERF_MAX_CONFLICT_MS);
205+
const keys = ['x', 'width', 'fillOpacity', 'lineWidth'];
206+
const preventedKeys = ['x', 'width'];
207+
208+
const warmupAnimates = Array.from({ length: 100 }, () => createPreventAnimate(keys, stepCount));
209+
warmupAnimates.forEach(animate => animate.preventAttrs(preventedKeys));
210+
211+
const animates = Array.from({ length: animateCount }, () => createPreventAnimate(keys, stepCount));
212+
const duration = timeLoop('batched-conflict-prevent', () => {
213+
animates.forEach(animate => animate.preventAttrs(preventedKeys));
214+
});
215+
216+
process.stdout.write(
217+
JSON.stringify({
218+
benchmark: 'animation-conflict-prevent',
219+
animateCount,
220+
stepCount,
221+
preventedKeyCount: preventedKeys.length,
222+
durationMs: Number(duration.toFixed(3)),
223+
maxMs: Number.isFinite(maxMs) ? maxMs : null
224+
}) + '\n'
225+
);
226+
227+
expect(duration).toBeLessThanOrEqual(maxMs);
228+
});
229+
230+
test('static end commit reports final truth commit cost', () => {
231+
const iterations = Number(process.env.VRENDER_ANIMATE_PERF_COMMIT_ITERATIONS ?? 200_000);
232+
const maxMs =
233+
process.env.VRENDER_ANIMATE_PERF_MAX_COMMIT_MS == null
234+
? Number.POSITIVE_INFINITY
235+
: Number(process.env.VRENDER_ANIMATE_PERF_MAX_COMMIT_MS);
236+
const target = createCommitTarget();
237+
const keys = ['x', 'y', 'y1', 'width', 'fillOpacity', 'lineWidth'];
238+
const animate = {
239+
validAttr: (key: string) => key !== 'lineWidth'
240+
} as any;
241+
242+
for (let i = 0; i < 10_000; i++) {
243+
commitAnimationStaticAttrs(target, keys, animate);
244+
}
245+
246+
const duration = timeLoop('static-end-commit', () => {
247+
for (let i = 0; i < iterations; i++) {
248+
commitAnimationStaticAttrs(target, keys, animate);
249+
}
250+
});
251+
252+
process.stdout.write(
253+
JSON.stringify({
254+
benchmark: 'animation-static-end-commit',
255+
iterations,
256+
keyCount: keys.length,
257+
durationMs: Number(duration.toFixed(3)),
258+
maxMs: Number.isFinite(maxMs) ? maxMs : null
259+
}) + '\n'
260+
);
261+
262+
expect(duration).toBeLessThanOrEqual(maxMs);
263+
});
264+
265+
test('custom animation frame helper avoids the core transient wrapper cost', () => {
266+
const iterations = Number(process.env.VRENDER_ANIMATE_PERF_FRAME_HELPER_ITERATIONS ?? 200_000);
267+
const maxRatio =
268+
process.env.VRENDER_ANIMATE_PERF_MAX_FRAME_HELPER_RATIO == null
269+
? Number.POSITIVE_INFINITY
270+
: Number(process.env.VRENDER_ANIMATE_PERF_MAX_FRAME_HELPER_RATIO);
271+
const wrapperRect = createRect({ x: 0, y: 0, y1: 100, width: 10 });
272+
const frameRect = createRect({ x: 0, y: 0, y1: 100, width: 10 });
273+
const attrs = { x: 10, width: 20 };
274+
275+
for (let i = 0; i < 10_000; i++) {
276+
applyAnimationTransientAttributes(wrapperRect, attrs);
277+
applyAnimationFrameAttributes(frameRect, attrs);
278+
}
279+
280+
const wrapper = timeLoop('core-transient-frame-helper', () => {
281+
for (let i = 0; i < iterations; i++) {
282+
attrs.x = i % 100;
283+
attrs.width = 20 + (i % 10);
284+
applyAnimationTransientAttributes(wrapperRect, attrs);
285+
}
286+
});
287+
const frame = timeLoop('direct-frame-helper', () => {
288+
for (let i = 0; i < iterations; i++) {
289+
attrs.x = i % 100;
290+
attrs.width = 20 + (i % 10);
291+
applyAnimationFrameAttributes(frameRect, attrs);
292+
}
293+
});
294+
const ratio = frame / wrapper;
295+
296+
process.stdout.write(
297+
JSON.stringify({
298+
benchmark: 'animation-custom-frame-helper',
299+
iterations,
300+
wrapperMs: Number(wrapper.toFixed(3)),
301+
frameMs: Number(frame.toFixed(3)),
302+
ratio: Number(ratio.toFixed(3)),
303+
maxRatio: Number.isFinite(maxRatio) ? maxRatio : null
304+
}) + '\n'
305+
);
306+
307+
expect(ratio).toBeLessThanOrEqual(maxRatio);
308+
});
309+
});

0 commit comments

Comments
 (0)