Skip to content

Commit

Permalink
perf(grid): replace generators with iterable-returning functions
Browse files Browse the repository at this point in the history
Turns out generators are slow, replacing them by plain iterables (arrays) improves performance by
about 50%
  • Loading branch information
flauwekeul committed Apr 22, 2021
1 parent 1aa8f73 commit 20655d1
Show file tree
Hide file tree
Showing 7 changed files with 96 additions and 39 deletions.
32 changes: 32 additions & 0 deletions playground/benchmark.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
declare const Benchmark: any

export const createSuite = () => {
const suite = new Benchmark.Suite()
const runButton = document.createElement('button')

suite.on('start', function () {
console.clear()
})
suite.on('cycle', function (event) {
console.log(`${event.target}`)
})
suite.on('complete', function () {
console.log(`Fastest: ${this.filter('fastest').map('name')}`)
})

runButton.type = 'button'
runButton.innerHTML = 'Run benchmark'
runButton.style.margin = '20px 0'
runButton.addEventListener('click', () => {
runButton.disabled = true
runButton.innerHTML = 'Running benchmark…'
suite.run({ async: true })
suite.on('complete', function () {
runButton.disabled = false
runButton.innerHTML = 'Run benchmark'
})
})
document.body.prepend(runButton)

return suite
}
46 changes: 32 additions & 14 deletions playground/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { at, createHex, createHexPrototype, Grid, Hex, move, PointyCompassDirection, repeat } from '../dist'
import { createSuite } from './benchmark'
import { render } from './render'

interface CustomHex extends Hex {
Expand All @@ -14,24 +15,41 @@ const hexPrototype = createHexPrototype<CustomHex>({
// const hex = createHex(hexPrototype, { q: 4, r: 3 })

Grid.of(hexPrototype)
// .rectangle({ width: 3, height: 3 })
.traverse(
at({ q: 1, r: 0 }),
move(PointyCompassDirection.E),
move(PointyCompassDirection.SE),
move(PointyCompassDirection.W),
at({ q: 0, r: 0 }),
repeat(
2,
repeat(4, move(PointyCompassDirection.E)),
move(PointyCompassDirection.SE),
repeat(4, move(PointyCompassDirection.W)),
move(PointyCompassDirection.SW),
),
repeat(4, move(PointyCompassDirection.E)),
)
.traverse(at({ q: 1, r: 1 }), repeat(3, move(PointyCompassDirection.E)))
.rectangle({ width: 10, height: 10 })
.each((hex) => {
hex.svg = render(createHex(hexPrototype, hex))
console.log(hex)
// console.log(hex)
})
.run()

// createSuite()
// .add('without prototype', function () {
// rectangle2(hexPrototype, { width: 5, height: 5 })
// })
// .add('with prototype', function () {
// rectangle(hexPrototype, { width: 5, height: 5 })
// })
const grid = Grid.of(hexPrototype)
createSuite()
.add('rectangle', function () {
grid.rectangle({ width: 5, height: 5 }).run()
})
.add('traverse', function () {
grid
.traverse(
at({ q: 0, r: 0 }),
repeat(
2,
repeat(4, move(PointyCompassDirection.E)),
move(PointyCompassDirection.SE),
repeat(4, move(PointyCompassDirection.W)),
move(PointyCompassDirection.SW),
),
repeat(4, move(PointyCompassDirection.E)),
)
.run()
})
5 changes: 1 addition & 4 deletions src/grid/functions/at.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,6 @@ import { Traverser } from '../types'
// yield createHex(Object.getPrototypeOf(currentHex), coordinates)
// }

export const at = (coordinates: HexCoordinates): Traverser =>
function* next() {
yield coordinates
}
export const at = (coordinates: HexCoordinates): Traverser => () => [coordinates]

export const start = at
9 changes: 4 additions & 5 deletions src/grid/functions/move.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ import { CompassDirection, Traverser } from '../types'
// yield createHex(Object.getPrototypeOf(currentHex), nextCoordinates)
// }

export const move = (direction: CompassDirection): Traverser =>
function* next(currentCoordinates) {
const { q, r } = DIRECTION_COORDINATES[direction]
yield { q: currentCoordinates.q + q, r: currentCoordinates.r + r }
}
export const move = (direction: CompassDirection): Traverser => {
const { q, r } = DIRECTION_COORDINATES[direction]
return (currentCoordinates) => [{ q: currentCoordinates.q + q, r: currentCoordinates.r + r }]
}
15 changes: 10 additions & 5 deletions src/grid/functions/repeat.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { HexCoordinates } from '../../hex'
import { Traverser } from '../types'

// todo: looks a lot like Grid.traverse()
Expand All @@ -15,14 +16,18 @@ import { Traverser } from '../types'
// }
// }

export const repeat = (amount: number, command: Traverser): Traverser =>
function* next(currentCoordinates) {
let coordinates = currentCoordinates
export const repeat = (amount: number, ...commands: Traverser[]): Traverser => (currentCoordinates) => {
const result: HexCoordinates[] = []
let coordinates = currentCoordinates

for (let i = 0; i < amount; i++) {
for (let i = 0; i < amount; i++) {
for (const command of commands) {
for (const nextCoordinates of command(coordinates)) {
coordinates = nextCoordinates
yield coordinates
result.push(coordinates)
}
}
}

return result
}
23 changes: 14 additions & 9 deletions src/grid/grid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ interface InternalTraverser<T extends Hex> {
(this: Grid<T>): GridGenerator<T>
}

// eslint-disable-next-line @typescript-eslint/no-empty-function
function* infiniteTraverser<T extends Hex>(): GridGenerator<T> {}
const infiniteTraverser = <T extends Hex>(): GridGenerator<T> => []

export class Grid<T extends Hex> {
static of<T extends Hex>(hexPrototype: T, traverser?: InternalTraverser<T>) {
Expand All @@ -22,6 +21,7 @@ export class Grid<T extends Hex> {
}

clone(traverser = this.traverser) {
// bind(this) in case the traverser is a "regular" (generator) function
return Grid.of(this.hexPrototype, traverser.bind(this))
}

Expand All @@ -33,20 +33,22 @@ export class Grid<T extends Hex> {
// fixme: use generic functions for these kinds of operations
// something like https://github.com/benji6/imlazy or https://github.com/lodash/lodash/wiki/FP-Guide
each(fn: (hex: T) => void) {
const each: InternalTraverser<T> = function* each() {
const each: InternalTraverser<T> = () => {
for (const hex of this.traverser()) {
fn(hex)
yield hex
}
return this.traverser()
}
return this.clone(each)
}

map(fn: (hex: T) => T) {
const map: InternalTraverser<T> = function* map() {
const map: InternalTraverser<T> = () => {
const result: T[] = []
for (const hex of this.traverser()) {
yield fn(hex)
result.push(fn(hex))
}
return result
}
return this.clone(map)
}
Expand All @@ -66,7 +68,8 @@ export class Grid<T extends Hex> {
return this // or clone()? todo: when to return clone and when not?
}

const traverse: InternalTraverser<T> = function* traverse() {
const traverse: InternalTraverser<T> = () => {
const result: T[] = []
const hasTraversedBefore = this.traverser !== infiniteTraverser
const previousHexes = [...this.traverser()]
let coordinates: HexCoordinates = previousHexes[previousHexes.length - 1] || { q: 0, r: 0 }
Expand All @@ -75,11 +78,13 @@ export class Grid<T extends Hex> {
for (const nextCoordinates of command(coordinates)) {
coordinates = nextCoordinates
if (hasTraversedBefore && !previousHexes.some((prevCoords) => equals(prevCoords, coordinates))) {
return // todo: or continue? or make this configurable?
return result // todo: or continue? or make this configurable?
}
yield createHex(this.hexPrototype, coordinates)
result.push(createHex(this.hexPrototype, coordinates))
}
}

return result
}

return this.clone(traverse)
Expand Down
5 changes: 3 additions & 2 deletions src/grid/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,12 @@ export enum FlatCompassDirection {

export type CompassDirection = PointyCompassDirection | FlatCompassDirection

export type GridGenerator<T extends Hex> = Generator<T, void>
export type GridGenerator<T extends Hex> = Iterable<T>

// export interface Traverser<T extends Hex> {
// (currentHex: T): GridGenerator<T>
// }
export interface Traverser {
(currentCoordinates: HexCoordinates): Generator<HexCoordinates, void>
(currentCoordinates: HexCoordinates): Iterable<HexCoordinates>
}
}

0 comments on commit 20655d1

Please sign in to comment.