Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optimization wollok game #328

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
3 changes: 2 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,8 @@
"time",
"timeEnd",
"group",
"groupEnd"
"groupEnd",
"table"
]
}
],
Expand Down
30 changes: 30 additions & 0 deletions .github/workflows/benchmarks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Run Benchmarks

on: [pull_request]

jobs:
run-benchmarks:
if: ${{ contains(github.event.pull_request.body, '[Run benchmarks]') }}
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Read .nvmrc
run: echo "##[set-output name=NVMRC;]$(cat .nvmrc)"
id: nvm
- name: Use Node.js (.nvmrc)
uses: actions/setup-node@v3
with:
node-version: "${{ steps.nvm.outputs.NVMRC }}"
- name: Install dependencies
run: npm install

- name: Run benchmarks
run: npm run test:benchmarks | tail -n +7 > bench-results.txt
continue-on-error: true

- name: Post results to comment
uses: peter-evans/commit-comment@v3
with:
body-path: 'bench-results.txt'
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "wollok-ts",
"version": "4.1.10",
"wollokVersion": ":master",
"wollokVersion": ":optimization-wollok-game",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️

"description": "TypeScript based Wollok language implementation",
"repository": "https://github.com/uqbar-project/wollok-ts",
"license": "MIT",
Expand All @@ -15,7 +15,7 @@
"prepare": "ts-node scripts/fetchLanguage.ts && npm run buildWRE && npm run copy:translations",
"copy:translations": "cp ./language/src/resources/validationMessages/*.json ./src/validator",
"diagnostic": "tsc --noEmit --diagnostics --extendedDiagnostics",
"test": "npm run test:lint && npm run test:unit && npm run test:sanity && npm run test:examples && npm run test:validations && npm run test:typeSystem && npm run test:printer",
"test": "npm run test:lint && npm run test:unit && npm run test:sanity && npm run test:examples",
"test:lint": "eslint .",
"test:coverage": "nyc --reporter=lcov npm run test",
"test:unit": "mocha --parallel -r ts-node/register/transpile-only test/**/*.test.ts",
Expand All @@ -33,6 +33,7 @@
"test:wtest": "mocha --delay -t 10000 -r ts-node/register/transpile-only test/wtest.ts",
"test:printer": "mocha --parallel -r ts-node/register/transpile-only test/printer.test.ts",
"test:parser": "mocha -r ts-node/register/transpile-only test/parser.test.ts",
"test:benchmarks": "mocha -t 999999 -r ts-node/register/transpile-only test/benchmarks.ts",
"lint:fix": "eslint . --fix",
"validate:wollokVersion": "ts-node scripts/validateWollokVersion.ts",
"prepublishOnly": "npm run validate:wollokVersion && npm run build && npm test",
Expand Down
21 changes: 19 additions & 2 deletions src/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { BOOLEAN_MODULE, CLOSURE_EVALUATE_METHOD, CLOSURE_MODULE, CLOSURE_TO_STRING_METHOD, INITIALIZE_METHOD, KEYWORDS, NUMBER_MODULE, OBJECT_MODULE, STRING_MODULE, VOID_WKO, WOLLOK_BASE_PACKAGE } from './constants'
import { getPotentiallyUninitializedLazy } from './decorators'
import { count, is, isEmpty, last, List, match, notEmpty, otherwise, valueAsListOrEmpty, when, excludeNullish } from './extensions'
import { RuntimeObject, RuntimeValue } from './interpreter/runtimeModel'
import { Execution, NativeFunction, RuntimeObject, RuntimeValue } from './interpreter/runtimeModel'
import { Assignment, Body, Class, CodeContainer, Describe, Entity, Environment, Expression, Field, If, Import, Literal, LiteralValue, Method, Module, Name, NamedArgument, New, Node, Package, Parameter, ParameterizedType, Problem, Program, Reference, Referenciable, Return, Self, Send, Sentence, Singleton, Super, Test, Throw, Try, Variable } from './model'

export const LIBRARY_PACKAGES = ['wollok.lang', 'wollok.lib', 'wollok.game', 'wollok.vm', 'wollok.mirror']
Expand Down Expand Up @@ -477,4 +477,21 @@ export const showParameter = (obj: RuntimeObject): string =>
`"${obj.getShortRepresentation().trim() || obj.module.fullyQualifiedName}"`

export const getMethodContainer = (node: Node): Method | Program | Test | undefined =>
last(node.ancestors.filter(parent => parent.is(Method) || parent.is(Program) || parent.is(Test))) as unknown as Method | Program | Test
last(node.ancestors.filter(parent => parent.is(Method) || parent.is(Program) || parent.is(Test))) as unknown as Method | Program | Test

/**
* NATIVES
*/
export const compilePropertyMethod = (method: Method): NativeFunction => {
const message = method.name
return method.parameters.length == 0
? compileGetter(message)
: compileSetter(message)
}

export const compileGetter = (message: string): NativeFunction => function* (self: RuntimeObject): Execution<RuntimeValue> {
return self.get(message)
}
export const compileSetter = (message: string): NativeFunction => function* (self: RuntimeObject, value: RuntimeObject): Execution<void> {
self.set(message, value)
}
14 changes: 9 additions & 5 deletions src/interpreter/runtimeModel.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { v4 as uuid } from 'uuid'
import { BOOLEAN_MODULE, CLOSURE_EVALUATE_METHOD, CLOSURE_MODULE, DATE_MODULE, DICTIONARY_MODULE, EXCEPTION_MODULE, INITIALIZE_METHOD, KEYWORDS, LIST_MODULE, NUMBER_MODULE, OBJECT_MODULE, PAIR_MODULE, RANGE_MODULE, SET_MODULE, STRING_MODULE, TO_STRING_METHOD, VOID_WKO, WOLLOK_BASE_PACKAGE, WOLLOK_EXTRA_STACK_TRACE_HEADER } from '../constants'
import { get, is, last, List, match, otherwise, raise, when } from '../extensions'
import { assertNotVoid, getExpressionFor, getMethodContainer, getUninitializedAttributesForInstantiation, isNamedSingleton, isVoid, loopInAssignment, showParameter, superMethodDefinition, targetName } from '../helpers'
import { assertNotVoid, compilePropertyMethod, getExpressionFor, getMethodContainer, getUninitializedAttributesForInstantiation, isNamedSingleton, isVoid, loopInAssignment, showParameter, superMethodDefinition, targetName } from '../helpers'
import { Assignment, Body, Catch, Class, Describe, Entity, Environment, Expression, Field, Id, If, Literal, LiteralValue, Method, Module, Name, New, Node, Program, Reference, Return, Self, Send, Singleton, Super, Test, Throw, Try, Variable } from '../model'
import { Interpreter } from './interpreter'

Expand Down Expand Up @@ -317,11 +317,15 @@ export class Evaluation {

// Set natives
environment.forEach(node => {
if (node.is(Method) && node.isNative())
evaluation.natives.set(node, get(natives, `${node.parent.fullyQualifiedName}.${node.name}`)!)
if (node.is(Method))
if (node.isNative())
evaluation.natives.set(node, get(natives, `${node.parent.fullyQualifiedName}.${node.name}`)!)
else if (node.fromProperty) {
evaluation.natives.set(node, compilePropertyMethod(node))
node.compiled = true
}
})


// Instanciate globals
const globalSingletons = environment.descendants.filter((node: Node): node is Singleton => isNamedSingleton(node))
for (const module of globalSingletons)
Expand Down Expand Up @@ -459,7 +463,7 @@ export class Evaluation {
protected *execMethod(node: Method): Execution<RuntimeValue> {
yield node

if (node.isNative()) {
if (node.hasNativeImplementation) {
const native = this.natives.get(node)
if (!native) throw new Error(`Missing native for ${node.parent.fullyQualifiedName}.${node.name}`)

Expand Down
9 changes: 8 additions & 1 deletion src/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -659,6 +659,8 @@ export class Method extends Node {

override parent!: Module

compiled = false

constructor({ isOverride = false, parameters = [], ...payload }: Payload<Method, 'name'>) {
super({ isOverride, parameters, ...payload })
}
Expand All @@ -672,8 +674,13 @@ export class Method extends Node {
}

isAbstract(): this is { body: undefined } { return !this.body }
isNative(): this is { body?: Body } { return this.body === KEYWORDS.NATIVE }
isConcrete(): this is { body: Body } { return !this.isAbstract() && !this.isNative() }
isNative(): this is { body?: Body } { return this.body === KEYWORDS.NATIVE }

get hasNativeImplementation(): boolean { return this.isNative() || this.compiled }

@cached
get fromProperty(): boolean { return this.isSynthetic && this.parameters.length < 2 && !!this.parent.lookupField(this.name) }

@cached
get hasVarArgs(): boolean { return !!last(this.parameters)?.isVarArg }
Expand Down
90 changes: 50 additions & 40 deletions src/wre/game.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,44 @@
import { GAME_MODULE } from '../constants'
import { assertIsNumber, assertIsNotNull, Execution, Natives, RuntimeObject, RuntimeValue } from '../interpreter/runtimeModel'
import { assertIsNotNull, assertIsNumber, Evaluation, Execution, NativeFunction, Natives, RuntimeObject, RuntimeValue } from '../interpreter/runtimeModel'
const { round } = Math


/**
* Avoid to invoke getters method from properties by accessing directly to the variable
*/
const getter = (message: string): NativeFunction => function* (obj: RuntimeObject): Execution<RuntimeValue> {
const method = obj.module.lookupMethod(message, 0)!
return method.isSynthetic ? obj.get(message)! : yield* this.invoke(method, obj)
}

const getPosition = getter('position')
const getX = getter('x')
const getY = getter('y')

const getObjectsIn = function* (this: Evaluation, position: RuntimeObject, ...visuals: RuntimeObject[]): Execution<RuntimeObject> {
const result: RuntimeObject[] = []

const x = (yield* getX.call(this, position))?.innerNumber
const y = (yield* getY.call(this, position))?.innerNumber

if (x == undefined || y == undefined) throw new RangeError('Position without coordinates')

const roundedX = round(x)
const roundedY = round(y)
for (const visual of visuals) {
const otherPosition = (yield* getPosition.call(this, visual))!
const otherX = (yield* getX.call(this, otherPosition))?.innerNumber
const otherY = (yield* getY.call(this, otherPosition))?.innerNumber

if (otherX == undefined || otherY == undefined) continue // Do NOT throw exception

if (roundedX == round(otherX) && roundedY == round(otherY))
result.push(visual)
}

return yield* this.list(...result)
}

const game: Natives = {
game: {
*addVisual(self: RuntimeObject, positionable: RuntimeObject): Execution<void> {
Expand Down Expand Up @@ -30,33 +67,7 @@

*getObjectsIn(self: RuntimeObject, position: RuntimeObject): Execution<RuntimeValue> {
const visuals = self.get('visuals')!
const result: RuntimeObject[] = []
const x = position.get('x')?.innerNumber
const y = position.get('y')?.innerNumber


if(x != undefined && y != undefined) {
const roundedX = round(x)
const roundedY = round(y)
for(const visual of visuals.innerCollection!) {

// Every visual understand position(), it is checked in addVisual(visual).
// Avoid to invoke method position() for optimisation reasons.
// -> If method isSynthetic then it is a getter, we can access to the field directly
const method = visual.module.lookupMethod('position', 0)!
const otherPosition = method.isSynthetic ? visual.get('position') :yield* this.invoke(method, visual)

const otherX = otherPosition?.get('x')?.innerNumber
const otherY = otherPosition?.get('y')?.innerNumber

if(otherX == undefined || otherY == undefined) continue

if(roundedX == round(otherX) && roundedY == round(otherY))
result.push(visual)
}
}

return yield* this.list(...result)
return yield* getObjectsIn.call(this, position, ...visuals.innerCollection!)
},

*say(self: RuntimeObject, visual: RuntimeObject, message: RuntimeObject): Execution<void> {
Expand All @@ -71,26 +82,25 @@
*colliders(self: RuntimeObject, visual: RuntimeObject): Execution<RuntimeValue> {
assertIsNotNull(visual, 'colliders', 'visual')

const position = (yield* this.send('position', visual))!
const visualsAtPosition: RuntimeObject = (yield* this.send('getObjectsIn', self, position))!

yield* this.send('remove', visualsAtPosition, visual)
const visuals = self.get('visuals')!
const otherVisuals = visuals.innerCollection!.filter(obj => obj != visual)
const position = (yield* getPosition.call(this, visual))!

return visualsAtPosition
return yield* getObjectsIn.call(this, position, ...otherVisuals)
},

*title(self: RuntimeObject, title?: RuntimeObject): Execution<RuntimeValue> {
if(!title) return self.get('title')
if (!title) return self.get('title')
self.set('title', title)
},

*width(self: RuntimeObject, width?: RuntimeObject): Execution<RuntimeValue> {
if(!width) return self.get('width')
if (!width) return self.get('width')
self.set('width', width)
},

*height(self: RuntimeObject, height?: RuntimeObject): Execution<RuntimeValue> {
if(!height) return self.get('height')
if (!height) return self.get('height')
self.set('height', height)
},

Expand Down Expand Up @@ -135,9 +145,9 @@

const game = this.object(GAME_MODULE)!
const sounds = game.get('sounds')
if(sounds) yield* this.send('remove', sounds, self)
if (sounds) yield* this.send('remove', sounds, self)

self.set('status', yield * this.reify('stopped'))
self.set('status', yield* this.reify('stopped'))

Check warning on line 150 in src/wre/game.ts

View check run for this annotation

Codecov / codecov/patch

src/wre/game.ts#L150

Added line #L150 was not covered by tests
},

*pause(self: RuntimeObject): Execution<void> {
Expand All @@ -161,7 +171,7 @@
},

*volume(self: RuntimeObject, newVolume?: RuntimeObject): Execution<RuntimeValue> {
if(!newVolume) return self.get('volume')
if (!newVolume) return self.get('volume')

const volume: RuntimeObject = newVolume
assertIsNumber(volume, 'volume', 'newVolume', false)
Expand All @@ -172,7 +182,7 @@
},

*shouldLoop(self: RuntimeObject, looping?: RuntimeObject): Execution<RuntimeValue> {
if(!looping) return self.get('loop')
if (!looping) return self.get('loop')
self.set('loop', looping)
},

Expand Down
68 changes: 68 additions & 0 deletions test/benchmarks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { should } from 'chai'
import { resolve } from 'path'
import { restore, stub } from 'sinon'
import { PROGRAM_FILE_EXTENSION } from '../src'
import { interpret } from '../src/interpreter/interpreter'
import natives from '../src/wre/wre.natives'
import { buildEnvironment } from './assertions'

should()

describe('Benchmarks', () => {
const results: any[] = []

after(() => console.table(results))

describe('flushEvents', () => {

function benchmark(fqn: string, expectedTime = 0) {
it(fqn, async () => {
stub(console)
const iterations = 30

const program = `games.${fqn}`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

una pavada, pero no falta subir los programas?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Están en Language

const message = 'flushEvents'

let totalTime = 0
for (let index = 0; index < iterations; index++)
totalTime += await measure(program, message)


const time = totalTime / iterations
const deltaError = expectedTime * 0.15 // 15 %
restore()

// console.info(`${message} - ${fqn} - ${time} ms (${iterations} iterations)`)
results.push({ message, fqn, time, iterations })
time.should.be.closeTo(expectedTime, deltaError)
})
}

benchmark('empty', 6)
benchmark('visuals_1', 4.5)
benchmark('visuals_100', 4)
benchmark('ticks_1', 12)
benchmark('ticks_100', 637)
benchmark('onCollide_1', 11)
benchmark('onCollide_10_same_position', 5000)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Este caso se debería poder optimizar reutilizando la lista de colliders:

  • Sabiendo que si un collider choca con otro, el otro también choca con él
  • Ahora se está iterando todos los visuales por cada uno, pero en este ejemplo termina armando siempre la misma lista de colliders

Ahora es medio paja hacerlo porque cada colisión es independiente del resto, habría que cambiar el modelo actual para centralizar el manejo de los colliders. Prefiero hacerlo en la próxima iteración.

benchmark('onCollide_100_diff_positions', 675)

})
})

async function measure(programFQN: string, message: string): Promise<number> {
const environment = await buildEnvironment(`**/*.${PROGRAM_FILE_EXTENSION}`, resolve('language', 'benchmarks'))
const interpreter = interpret(environment, natives)

interpreter.run(programFQN)
const game = interpreter.object('wollok.game.game')

interpreter.send(message, game, interpreter.reify(0)) // Fill caches
const startTime = performance.now()
for (let ms = 1; ms < 10; ms++)
interpreter.send(message, game, interpreter.reify(ms))
const endTime = performance.now()

const elapsedTime = endTime - startTime
return elapsedTime
}
Loading