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
8 changes: 7 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,12 @@ 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 }

fromProperty(): boolean { return this.isSynthetic && !!this.parent.lookupField(this.name) }

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

@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, 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: NativeFunction = function* (position: RuntimeObject, ...visuals: RuntimeObject[]): Execution<RuntimeValue> {
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 @@ const game: Natives = {

*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 @@ const game: Natives = {
*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: Natives = {

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'))
},

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

*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 @@ const game: Natives = {
},

*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
67 changes: 67 additions & 0 deletions test/benchmarks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
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)
benchmark('visuals_100', 4)
benchmark('ticks_1', 11)
benchmark('ticks_100', 637)
benchmark('onCollide_1', 11)
benchmark('onCollide_100', 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++)

Check warning on line 61 in test/benchmarks.ts

View workflow job for this annotation

GitHub Actions / build

Trailing spaces not allowed

Check warning on line 61 in test/benchmarks.ts

View workflow job for this annotation

GitHub Actions / build

Trailing spaces not allowed
interpreter.send(message, game, interpreter.reify(ms))
const endTime = performance.now()

const elapsedTime = endTime - startTime
return elapsedTime
}
Loading