Skip to content

Commit

Permalink
Set up a performance testing pipeline
Browse files Browse the repository at this point in the history
  • Loading branch information
mauriciopoppe committed Dec 17, 2023
1 parent 36591c6 commit cab36a4
Show file tree
Hide file tree
Showing 4 changed files with 194 additions and 76 deletions.
22 changes: 10 additions & 12 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
with:
fetch-depth: 0

- uses: actions/setup-node@v3.1.1
- uses: actions/setup-node@v4

- name: Install dependencies
run: npm install
Expand All @@ -29,7 +29,7 @@ jobs:
with:
fetch-depth: 0

- uses: actions/setup-node@v3.1.1
- uses: actions/setup-node@v4

- name: Install dependencies
run: npm install
Expand All @@ -44,21 +44,19 @@ jobs:
path: |
test/e2e/**/*.png
license:
perf:
runs-on: ubuntu-latest

env:
FOSSA_API_KEY: ${{ secrets.FOSSA_API_KEY }}

steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0

- name: Run Fossa and upload data
if: env.FOSSA_API_KEY != ''
uses: fossa-contrib/fossa-action@v1
with:
fossa-api-key: ${{ env.FOSSA_API_KEY }}
- uses: actions/setup-node@v4

- name: Install dependencies
run: npm install

- name: Perf
run: npm run perf:pipeline

34 changes: 34 additions & 0 deletions design/pipeline.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Compile-Eval-Render Pipeline for Intervals

> Summary of the render process that happens on every tick (pan/zoom) in the graph.
As of v1.24.0 the render pipeline consists of:

- Compile + Eval
- compile `fn` to a *-eval function (see [interval-arithmetic-eval](https://github.com/mauriciopoppe/interval-arithmetic-eval)
or [built-in-math-eval](https://github.com/mauriciopoppe/built-in-math-eval))
- The [above is cached]() so that when `functionPlot` is invoked it isn't
the compilation isn't run many times.
- [use `nSamples`](https://github.com/mauriciopoppe/function-plot/blob/b46e07c3281bce5b6bff00050ba3d6a16795a483/src/evaluate.ts#L40)
to create [`nSamples` equally distinct points](https://github.com/mauriciopoppe/function-plot/blob/b46e07c3281bce5b6bff00050ba3d6a16795a483/src/samplers/interval.ts#L17),
e.g. `[x_0, x_1, ..., x_{n_samples - 1}]`
- In `O(n)`, iterate over all the points and evaluate each them against the compiled function evaluator (created in the preparation stage),
return a data structure that encodes the result (`Array<Array<[Interval, Interval]> | null>`)
- Render
- In `O(n)` iterate over all the results and create a [`<path d={rectanglePaint} />`](https://github.com/mauriciopoppe/function-plot/blob/b46e07c3281bce5b6bff00050ba3d6a16795a483/src/graph-types/interval.ts#L96)
where `rectanglePaint` is a [series of commands](https://github.com/mauriciopoppe/function-plot/blob/b46e07c3281bce5b6bff00050ba3d6a16795a483/src/graph-types/interval.ts#L68)
of the form `M <x> <y> v <width>` which move to `(x, y)` and paint a rectangle of width `width`.

## Perf stats

Using `npm run perf:pipeline`:

```
compile and eval 1000 x 1,163 ops/sec ±0.26% (98 runs sampled)
compile and eval 1000 x 1,172 ops/sec ±0.46% (97 runs sampled)
compile and eval 1000 x 1,164 ops/sec ±0.23% (96 runs sampled)
drawPath 1000 x 8,398 ops/sec ±0.58% (99 runs sampled)
drawPath 1000 x 8,392 ops/sec ±0.53% (97 runs sampled)
drawPath 1000 x 8,266 ops/sec ±0.55% (96 runs sampled)
```
134 changes: 70 additions & 64 deletions src/graph-types/interval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,73 +4,79 @@ import evaluate from '../evaluate'
import utils from '../utils'

import { Chart } from '../index'
import { Interval, FunctionPlotDatum } from '../types'
import { Interval, FunctionPlotDatum, FunctionPlotScale } from '../types'

export default function interval(chart: Chart) {
let minWidthHeight: number
const xScale = chart.meta.xScale
const yScale = chart.meta.yScale

function clampRange(vLo: number, vHi: number, gLo: number, gHi: number) {
// issue 69
// by adding the option `invert` to both the xAxis and the `yAxis`
// it might be possible that after the transformation to canvas space
// the y limits of the rectangle get inverted i.e. gLo > gHi
//
// e.g.
//
// functionPlot({
// target: '#playground',
// yAxis: { invert: true },
// // ...
// })
//
if (gLo > gHi) {
const t = gLo
gLo = gHi
gHi = t
}
const hi = Math.min(vHi, gHi)
const lo = Math.max(vLo, gLo)
if (lo > hi) {
// no overlap
return [-minWidthHeight, 0]
}
return [lo, hi]
function clampRange(minWidthHeight: number, vLo: number, vHi: number, gLo: number, gHi: number) {
// issue 69
// by adding the option `invert` to both the xAxis and the `yAxis`
// it might be possible that after the transformation to canvas space
// the y limits of the rectangle get inverted i.e. gLo > gHi
//
// e.g.
//
// functionPlot({
// target: '#playground',
// yAxis: { invert: true },
// // ...
// })
//
if (gLo > gHi) {
const t = gLo
gLo = gHi
gHi = t
}
const hi = Math.min(vHi, gHi)
const lo = Math.max(vLo, gLo)
if (lo > hi) {
// no overlap
return [-minWidthHeight, 0]
}
return [lo, hi]
}

const line = function (points: Interval[][], closed: boolean) {
let path = ''
const range = yScale.range()
const minY = Math.min.apply(Math, range)
const maxY = Math.max.apply(Math, range)
for (let i = 0, length = points.length; i < length; i += 1) {
if (points[i]) {
const x = points[i][0]
const y = points[i][1]
let yLo = y.lo
let yHi = y.hi
// if options.closed is set to true then one of the bounds must be zero
if (closed) {
yLo = Math.min(yLo, 0)
yHi = Math.max(yHi, 0)
}
// points.scaledDX is added because of the stroke-width
const moveX = xScale(x.lo) + (points as any).scaledDx / 2
const viewportY = clampRange(
minY,
maxY,
isFinite(yHi) ? yScale(yHi) : -Infinity,
isFinite(yLo) ? yScale(yLo) : Infinity
)
const vLo = viewportY[0]
const vHi = viewportY[1]
path += ' M ' + moveX + ' ' + vLo
path += ' v ' + Math.max(vHi - vLo, minWidthHeight)
export function createPathD(
xScale: FunctionPlotScale,
yScale: FunctionPlotScale,
minWidthHeight: number,
points: Array<[Interval, Interval]>,
closed: boolean
) {
let path = ''
const range = yScale.range()
const minY = Math.min.apply(Math, range)
const maxY = Math.max.apply(Math, range)
for (let i = 0, length = points.length; i < length; i += 1) {
if (points[i]) {
const x = points[i][0]
const y = points[i][1]
let yLo = y.lo
let yHi = y.hi
// if options.closed is set to true then one of the bounds must be zero
if (closed) {
yLo = Math.min(yLo, 0)
yHi = Math.max(yHi, 0)
}
// points.scaledDX is added because of the stroke-width
const moveX = xScale(x.lo) + (points as any).scaledDx / 2
const viewportY = clampRange(
minWidthHeight,
minY,
maxY,
isFinite(yHi) ? yScale(yHi) : -utils.infinity(),
isFinite(yLo) ? yScale(yLo) : utils.infinity()
)
const vLo = viewportY[0]
const vHi = viewportY[1]
path += ' M ' + moveX + ' ' + vLo
path += ' v ' + Math.max(vHi - vLo, minWidthHeight)
}
return path
}
return path
}

export default function interval(chart: Chart) {
const xScale = chart.meta.xScale
const yScale = chart.meta.yScale

function plotLine(selection: Selection<any, FunctionPlotDatum, any, any>) {
selection.each(function (d) {
Expand All @@ -81,7 +87,7 @@ export default function interval(chart: Chart) {
const innerSelection = el.selectAll(':scope > path.line').data(evaluatedData)

// the min height/width of the rects drawn by the path generator
minWidthHeight = Math.max(evaluatedData[0].scaledDx, 1)
const minWidthHeight = Math.max(evaluatedData[0].scaledDx, 1)

const cls = `line line-${index}`
const innerSelectionEnter = innerSelection.enter().append('path').attr('class', cls).attr('fill', 'none')
Expand All @@ -92,8 +98,8 @@ export default function interval(chart: Chart) {
.attr('stroke-width', minWidthHeight)
.attr('stroke', utils.color(d, index) as any)
.attr('opacity', closed ? 0.5 : 1)
.attr('d', function (d: Interval[][]) {
return line(d, closed)
.attr('d', function (d: Array<[Interval, Interval]>) {
return createPathD(xScale, yScale, minWidthHeight, d, closed)
})

if (d.attr) {
Expand Down
80 changes: 80 additions & 0 deletions src/perf/interval-pipeline.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/**
* interval-pipeline evaluates the performance of the compile, eval, render pipeline,
* the design is at /design/pipeline.md
*/

// @ts-ignore
import Benchmark from 'benchmark'
import { scaleLinear } from 'd3-scale'

import { FunctionPlotDatum, FunctionPlotOptionsAxis } from '../types'
import { createPathD } from '../graph-types/interval'
import interval from '../samplers/interval'

function createData(nSamples: number) {
const width = 500
const height = 300
const xDomain: [number, number] = [-5, 5]
const yDomain: [number, number] = [-5, 5]
const xScale = scaleLinear().domain(xDomain).range([0, width])
const yScale = scaleLinear().domain(yDomain).range([height, 0])
const d: FunctionPlotDatum = {
fn: '1/x',
fnType: 'linear'
}
const xAxis: FunctionPlotOptionsAxis = { type: 'linear' }
const yAxis: FunctionPlotOptionsAxis = { type: 'linear' }
const samplerParams = {
d,
range: xDomain,
xScale,
yScale,
xAxis,
yAxis,
nSamples
}
const data = interval(samplerParams)
return { data, xScale, yScale }
}

function compileAndEval() {
const compileAndEval = new Benchmark.Suite()
const nSamples = 1000
compileAndEval
.add(`compile and eval ${nSamples}`, function () {
createData(nSamples)
})
// add listeners
.on('cycle', function (event) {
console.log(String(event.target))
})
.on('complete', function () {
console.log('Fastest is ' + this.filter('fastest').map('name'))
})
.run({ async: false })
}

function drawPath() {
const compileAndEval = new Benchmark.Suite()
const nSamples = 1000
const { xScale, yScale, data } = createData(nSamples)
compileAndEval
.add(`drawPath ${nSamples}`, function () {
createPathD(xScale, yScale, 1 /* minWidthHeight, dummy = 1 */, data[0], false /* closed */)
})
// add listeners
.on('cycle', function (event) {
console.log(String(event.target))
})
.on('complete', function () {
console.log('Fastest is ' + this.filter('fastest').map('name'))
})
.run({ async: false })
}

function main() {
compileAndEval()
drawPath()
}

main()

0 comments on commit cab36a4

Please sign in to comment.