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

Merged
merged 20 commits into from
Feb 4, 2025
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
51 changes: 51 additions & 0 deletions .github/workflows/benchmarks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
name: Run Benchmarks

on:
issue_comment:
types:
- created

jobs:
run-benchmarks:
if: ${{ contains(github.event.comment.body, '[Run benchmarks]') && github.event.issue.pull_request }}
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
# id: benchmarks
# run: npm run test:benchmarks

- name: Post results to comment
uses: peter-evans/commit-comment@v3
with:
repository: ${{ github.repository }}
comment-id: ${{ github.event.comment.id }}
body: |
## Benchmark Results
```
┌─────────┬───────────────┬─────────────────┬─────────────────────┬────────────┐
│ (index) │ message │ fqn │ time │ iterations │
├─────────┼───────────────┼─────────────────┼─────────────────────┼────────────┤
│ 0 │ 'flushEvents' │ 'empty' │ 1.0739654541015624 │ 30 │
│ 1 │ 'flushEvents' │ 'visuals_1' │ 0.48952223459879557 │ 30 │
│ 2 │ 'flushEvents' │ 'visuals_100' │ 0.2899084726969401 │ 30 │
│ 3 │ 'flushEvents' │ 'ticks_1' │ 0.8966011683146159 │ 30 │
│ 4 │ 'flushEvents' │ 'ticks_100' │ 49.76229190826416 │ 30 │
│ 5 │ 'flushEvents' │ 'onCollide_1' │ 1.091204325358073 │ 30 │
│ 6 │ 'flushEvents' │ 'onCollide_100' │ 48.2887056350708 │ 30 │
└─────────┴───────────────┴─────────────────┴─────────────────────┴────────────┘
PalumboN marked this conversation as resolved.
Show resolved Hide resolved
```
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",
PalumboN marked this conversation as resolved.
Show resolved Hide resolved
"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 50000 -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 @@

*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
66 changes: 66 additions & 0 deletions test/benchmarks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
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('Wollok Game', () => {
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}`
PalumboN marked this conversation as resolved.
Show resolved Hide resolved
const message = 'flushEvents'

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


const time = totalTime / iterations
const deltaError = Math.max(0.1, expectedTime * 0.1) // 0.1 or 10 %
restore()

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

benchmark('empty', 0.55)
benchmark('visuals_1', 0.4)
benchmark('visuals_100', 0.3)
benchmark('ticks_1', 0.8)
benchmark('ticks_100', 44)
benchmark('onCollide_1', 0.8)
benchmark('onCollide_100', 44)
Copy link
Contributor

Choose a reason for hiding this comment

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

Are these assertions portable on different machines / browsers / JS implementations?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I just hope:

  • to have some number more or less regular in the CI
  • detect when the number changes a lot


})
})

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()
interpreter.send(message, game, interpreter.reify(1))
const endTime = performance.now()

const elapsedTime = endTime - startTime
return elapsedTime
}
Loading