From 3935a87cb15fb9e019dab8ab91d9cc547ef6f281 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Rypu=C5=82a?= Date: Tue, 7 Jan 2020 18:21:56 +0100 Subject: [PATCH] Adds ability to test example performance. Improoves spring-and-damper force calculations which speeds up Wheel example. 15 seconds of simulation: BEFORE average was 3.77s (min 3.02s, max 4.16s), AFTER average is 3.12s (min 3.06s, max 3.27s). --- src/index.html | 2 +- src/lib/core/forces/spring-and-damper.ts | 33 +++++++++++++- src/lib/core/forces/thrust.ts | 3 +- src/lib/examples/abstract-example.ts | 2 +- src/lib/examples/constants.ts | 3 ++ .../examples/example-advanced-wheel.spec.ts | 34 ++++++++++++++ src/lib/examples/models.ts | 15 +++++++ src/lib/examples/node/cli.ts | 12 ++++- src/lib/examples/tools.ts | 45 +++++++++++++++++++ src/lib/examples/web/web-runner.ts | 18 +++++--- 10 files changed, 156 insertions(+), 11 deletions(-) create mode 100644 src/lib/examples/constants.ts create mode 100644 src/lib/examples/example-advanced-wheel.spec.ts create mode 100644 src/lib/examples/models.ts create mode 100644 src/lib/examples/tools.ts diff --git a/src/index.html b/src/index.html index 6894efd..87ce0e3 100644 --- a/src/index.html +++ b/src/index.html @@ -5,7 +5,7 @@ Simple Forces - +

Simple Forces

diff --git a/src/lib/core/forces/spring-and-damper.ts b/src/lib/core/forces/spring-and-damper.ts index 1165ad3..d4d34d7 100644 --- a/src/lib/core/forces/spring-and-damper.ts +++ b/src/lib/core/forces/spring-and-damper.ts @@ -6,6 +6,7 @@ import { Line } from '@core/constraints-hosts/line'; import { Point } from '@core/constraints-hosts/point'; import { Force, ForceSource } from '@core/force'; import { ForceType } from '@core/models'; +import { SimplePoint } from '@core/simple-point'; import { World } from '@core/world'; /*tslint:disable:max-classes-per-file*/ @@ -16,6 +17,26 @@ export class SpringAndDamperForce extends Force { } public calculateForce(point: Point): void { + if (this.isSecondEnd(point)) { + return; + } + + const line: Line = this.forceSource.line; + const unitAngle: number = line.getUnitAngle(); + const origin: SimplePoint = line.pointA.cloneAsSimplePoint(); + const simplePointLineB: SimplePoint = line.pointB.cloneAsSimplePoint().transform(origin, unitAngle); + + const springForce: number = -(line.length - simplePointLineB.position.x) * this.forceSource.k; + const dampingForce: number = simplePointLineB.velocity.x * this.forceSource.b; + let finalForce: number = springForce + dampingForce; + + this.forceSource.includeMass && (finalForce *= point.mass); + + simplePointLineB.force = Complex.create(finalForce, 0); + this.vector = simplePointLineB.transformBack(origin, unitAngle).force; + this.forceSource.pointBForce.vector = simplePointLineB.force.clone().multiplyScalar(-1); + + /* const springMountingPoint: Point = this.forceSource.line.pointA === point ? this.forceSource.line.pointB : this.forceSource.line.pointA; const direction: Complex = point.position.clone().subtract(springMountingPoint.position); @@ -29,12 +50,16 @@ export class SpringAndDamperForce extends Force { this.forceSource.includeMass && (finalForce *= point.mass); this.vector = direction.normalize().multiplyScalar(-finalForce); - + */ // TODO known issue: // - magnitude of force is calculated twice // (ends of the spring acts on each other with the same force but opposite direction) // solution: ignore the second end of the line and calculate everything in the first end } + + protected isSecondEnd(point: Point): boolean { + return point === this.forceSource.line.pointB; + } } // ---------------------------------------------------------------- @@ -44,11 +69,15 @@ export class SpringAndDamperForceSource extends ForceSource { public k: number = DEFAULT_SPRING_AND_DAMPER_K_COEFFICIENT; public includeMass = true; // TODO move to constants + // NOTE: caching references to force speeds up access to it as each + // point have array of forces and we would need to use slower find() + public pointBForce: SpringAndDamperForce; + public constructor(world: World, public line: Line) { super(world); line.pointA.forces.push(new SpringAndDamperForce(this)); - line.pointB.forces.push(new SpringAndDamperForce(this)); + line.pointB.forces.push((this.pointBForce = new SpringAndDamperForce(this))); // NOTE: no world's refreshAwareness method is needed - spring & damper force interacts only with two 'self' points } diff --git a/src/lib/core/forces/thrust.ts b/src/lib/core/forces/thrust.ts index 504b61a..e5fdeb8 100644 --- a/src/lib/core/forces/thrust.ts +++ b/src/lib/core/forces/thrust.ts @@ -20,7 +20,8 @@ export class ThrustForce extends Force { } if (this.forceSource.pointAForce !== this) { - throw new Error('Problem to investigate'); // TODO remove it after tests on more complex objects with thrust + // TODO remove it after tests on more complex objects with thrust + throw new Error('Problem to investigate - this should never happen'); } const lineDirection: Complex = Complex.createPolar(this.forceSource.line.getUnitAngle()); diff --git a/src/lib/examples/abstract-example.ts b/src/lib/examples/abstract-example.ts index 8db0bbf..bce74a6 100644 --- a/src/lib/examples/abstract-example.ts +++ b/src/lib/examples/abstract-example.ts @@ -7,7 +7,7 @@ export abstract class AbstractExample { public log: Array<[string, string]> = []; public world: World; - protected constructor() { + public constructor() { this.world = new World(); this.createScene(); this.world.refreshAwareness(); diff --git a/src/lib/examples/constants.ts b/src/lib/examples/constants.ts new file mode 100644 index 0000000..c3e436a --- /dev/null +++ b/src/lib/examples/constants.ts @@ -0,0 +1,3 @@ +// Copyright (c) 2018-2020 Robert Rypuła - https://github.com/robertrypula + +export const DEFAULT_EXAMPLE_FPS = 60; diff --git a/src/lib/examples/example-advanced-wheel.spec.ts b/src/lib/examples/example-advanced-wheel.spec.ts new file mode 100644 index 0000000..fde2272 --- /dev/null +++ b/src/lib/examples/example-advanced-wheel.spec.ts @@ -0,0 +1,34 @@ +// Copyright (c) 2018-2020 Robert Rypuła - https://github.com/robertrypula + +import { ExampleAdvancedWheel } from '@'; + +import { ExampleExecutionDetails } from '@examples/models'; +import { getExampleExecutionDetails } from '@examples/tools'; + +describe('Advanced example - wheel', (): void => { + it('should properly calculate wheel position', (): void => { + const exampleExecutionDetails: ExampleExecutionDetails = getExampleExecutionDetails( + ExampleAdvancedWheel, + (example: ExampleAdvancedWheel): string => + example.world.physics.time.toFixed(3) + 's: ' + example.wheel.center.position.toStringXY() + ); + + expect(exampleExecutionDetails.singleRunLog).toEqual([ + '0.017s: 2.000 2.999', + '1.017s: 1.269 2.157', + '2.017s: -1.008 1.058', + '3.017s: -4.006 -0.241', + '4.017s: -2.011 -0.511', + '5.017s: -2.035 -0.197', + '6.017s: -4.041 -0.373', + '7.017s: -2.314 -0.459', + '8.017s: -2.052 -0.437', + '9.017s: -2.199 -0.542', + '10.017s: -2.980 -0.529', + '11.017s: -3.751 -0.531', + '12.017s: -3.830 -0.544', + '13.017s: -3.577 -0.531', + '14.017s: -3.328 -0.530' + ]); + }); +}); diff --git a/src/lib/examples/models.ts b/src/lib/examples/models.ts new file mode 100644 index 0000000..2bf9a35 --- /dev/null +++ b/src/lib/examples/models.ts @@ -0,0 +1,15 @@ +// Copyright (c) 2018-2020 Robert Rypuła - https://github.com/robertrypula + +import { AbstractExample } from '@examples/abstract-example'; + +export interface ExampleExecutionDetails { + runTime: { + average: number; + max: number; + min: number; + }; + runTimes: number[]; + singleRunLog: string[]; +} + +export type ExampleFactory = new () => AbstractExample; diff --git a/src/lib/examples/node/cli.ts b/src/lib/examples/node/cli.ts index 32415be..57aae0c 100644 --- a/src/lib/examples/node/cli.ts +++ b/src/lib/examples/node/cli.ts @@ -1,8 +1,18 @@ // Copyright (c) 2018-2020 Robert Rypuła - https://github.com/robertrypula +import { ExampleAdvancedWheel } from '@examples/example-advanced-wheel'; +import { ExampleExecutionDetails } from '@examples/models'; +import { getExampleExecutionDetails } from '@examples/tools'; + export class CliNodeExample { public constructor() { + const exampleExecutionDetails: ExampleExecutionDetails = getExampleExecutionDetails( + ExampleAdvancedWheel, + (example: ExampleAdvancedWheel): string => + example.world.physics.time.toFixed(3) + 's: ' + example.wheel.center.position.toStringXY() + ); + /*tslint:disable-next-line:no-console*/ - console.log('Hello World!'); + console.log(exampleExecutionDetails); } } diff --git a/src/lib/examples/tools.ts b/src/lib/examples/tools.ts new file mode 100644 index 0000000..211c34b --- /dev/null +++ b/src/lib/examples/tools.ts @@ -0,0 +1,45 @@ +// Copyright (c) 2018-2020 Robert Rypuła - https://github.com/robertrypula + +import { AbstractExample } from '@examples/abstract-example'; +import { DEFAULT_EXAMPLE_FPS } from '@examples/constants'; +import { ExampleExecutionDetails } from '@examples/models'; +import { getTime } from '@shared/tools'; + +export const getExampleExecutionDetails = ( + exampleFactory: new () => T, + logHandler: (example: T) => string = null, + runs = 1, + seconds = 15, + fps = DEFAULT_EXAMPLE_FPS +): ExampleExecutionDetails => { + const dt: number = 1 / fps; + const timeStart: number = getTime(); + const exampleExecutionDetails: ExampleExecutionDetails = { + runTime: { + average: null, + max: null, + min: null + }, + runTimes: [], + singleRunLog: [] + }; + + for (let run = 0; run < runs; run++) { + const timeSubStart: number = getTime(); + const example: T = new exampleFactory(); + + for (let second = 0; second < seconds; second++) { + for (let frame = 0; frame < fps; frame++) { + example.animationFrame(dt); + + run === 0 && frame === 0 && logHandler && exampleExecutionDetails.singleRunLog.push(logHandler(example)); + } + } + exampleExecutionDetails.runTimes.push(getTime() - timeSubStart); + } + exampleExecutionDetails.runTime.average = (getTime() - timeStart) / runs; + exampleExecutionDetails.runTime.max = Math.max(...exampleExecutionDetails.runTimes); + exampleExecutionDetails.runTime.min = Math.min(...exampleExecutionDetails.runTimes); + + return exampleExecutionDetails; +}; diff --git a/src/lib/examples/web/web-runner.ts b/src/lib/examples/web/web-runner.ts index a050fb4..a97b110 100644 --- a/src/lib/examples/web/web-runner.ts +++ b/src/lib/examples/web/web-runner.ts @@ -4,6 +4,9 @@ import { CanvasRenderer, getTime } from '@'; import { AbstractExample } from '@examples/abstract-example'; +import { DEFAULT_EXAMPLE_FPS } from '@examples/constants'; +import { ExampleFactory } from '@examples/models'; +import { getExampleExecutionDetails } from '@examples/tools'; import * as domUtils from '@examples/web/dom-utils'; export class WebRunner { @@ -12,7 +15,7 @@ export class WebRunner { protected previousTime: number = null; protected canvasRenderer: CanvasRenderer; - public constructor(protected exampleFactory: new () => AbstractExample) { + public constructor(protected exampleFactory: ExampleFactory) { domUtils.getByTagName('html').classList.add('web-runner'); domUtils.getById('simple-forces-root').innerHTML = require('./web-runner.html'); @@ -23,14 +26,14 @@ export class WebRunner { document.addEventListener('keydown', (event: KeyboardEvent): void => this.example.keyboardEvent(event.code, true)); document.addEventListener('keyup', (event: KeyboardEvent): void => this.example.keyboardEvent(event.code, false)); - this.animationFrame(); + 1 ? this.animationFrame() : setTimeout(this.offlineExampleExecution.bind(this), 0); // TODO for development } public animationFrame(): void { - const currentTime = getTime(); - let dt = this.previousTime === null ? 0 : currentTime - this.previousTime; + const currentTime: number = getTime(); + let dt: number = this.previousTime === null ? 0 : currentTime - this.previousTime; - dt = 0.016; // TODO only in development, constant delta between animation frames + dt = 1 / DEFAULT_EXAMPLE_FPS; // TODO check it, probably it would be better to keep constant dt between frames this.example.animationFrame(dt); this.canvasRenderer.render(); this.log(); @@ -39,6 +42,11 @@ export class WebRunner { window.requestAnimationFrame(this.animationFrame.bind(this)); } + protected offlineExampleExecution(): void { + /*tslint:disable-next-line:no-console*/ + console.log(getExampleExecutionDetails(this.exampleFactory, null, 8)); + } + protected log(): void { if (this.logElement) { this.logElement.innerHTML = this.example.log.map(logItem => logItem.join(': ')).join('\n');