Skip to content

Commit

Permalink
Merge pull request #328 from uqbar-project/optimization-wollok-game
Browse files Browse the repository at this point in the history
Optimization wollok game
  • Loading branch information
PalumboN authored Feb 4, 2025
2 parents 3dca90a + 66f7025 commit 578480c
Show file tree
Hide file tree
Showing 8 changed files with 188 additions and 50 deletions.
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'
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 @@ -34,6 +34,7 @@
"test:file": "mocha -r ts-node/register/transpile-only",
"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 @@ 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
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 './utils'

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}`
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.2
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.5)
benchmark('ticks_1', 12)
benchmark('ticks_100', 657)
benchmark('onCollide_1', 11)
benchmark('onCollide_10_same_position', 5000)
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
}

Check warning on line 68 in test/benchmarks.ts

View workflow job for this annotation

GitHub Actions / build

Newline not allowed at end of file

0 comments on commit 578480c

Please sign in to comment.