Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
60 changes: 60 additions & 0 deletions .github/workflows/cqn4sql-benchmarks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
name: cqn4sql benchmarks + visualization

on:
push:
branches: [ patrice/cds-test-perfomance ]
workflow_dispatch:

permissions:
contents: read
pages: write
id-token: write

concurrency:
group: "pages"
cancel-in-progress: true

jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 1

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm

- name: Install dependencies
run: npm ci

# this needs the cds-test PR to be merged first
# - name: Run benchmarks
# run: |
# cd ./db-service/bench/cqn4sql && CDS_BENCH=true cds test

- name: Build visualization
run: |
cd ./db-service/bench/cqn4sql
mkdir -p dist
node utils/visualize-benchmarks.js results/cqn4sql-benchmarks.json dist/index.html

- name: Upload Pages artifact
uses: actions/upload-pages-artifact@v3
with:
path: ./db-service/bench/cqn4sql/dist

deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
needs: build
runs-on: ubuntu-latest
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
34 changes: 34 additions & 0 deletions db-service/bench/cqn4sql/model/schema.cds
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
namespace my;

entity Books {
key ID : Integer;
title : String;
stock : Integer;
author : Association to Authors;
genre : Association to Genres;
}

entity BooksWithCalc : Books {
authorFullName = author.firstName || ' ' || author.lastName;
}

entity Authors {
key ID : Integer;
firstName : String;
lastName : String;
dateOfBirth : Date;
dateOfDeath : Date;
books : Association to many Books
on books.author = $self;
}

@cds.search: { books }
entity AuthorsSearchBooks : Authors {}

entity Genres {
key ID : Integer;
name : String;
parent : Association to Genres;
children : Composition of many Genres
on children.parent = $self;
}
66 changes: 66 additions & 0 deletions db-service/bench/cqn4sql/performance-benchmarks.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
'use strict'

if (process.env.CDS_BENCH === 'true') {
const cds = require('@sap/cds')
const { writeDump } = require('./utils/format-benchmarks')

let cqn4sql = require('../../lib/cqn4sql')

const {
perf,
} = cds.test

const report = (what, options) => perf.report(what, { store: true, stats: { requests: true }, ...options })

describe('cqn4sql performance benchmarks', () => {
beforeEach(async () => {
const m = await cds.load([__dirname + '/model/schema']).then(cds.linked)
const orig = cqn4sql // keep reference to original to avoid recursion
cqn4sql = q => orig(q, m)
})

after('format & write dump', () => {
writeDump({ testFile: __filename })
})

runBenchmarkFor('select simple', cds.ql`SELECT from my.Books { ID }`)

runBenchmarkFor('select wildcard', cds.ql`SELECT from my.Books { * }`)
runBenchmarkFor('select wildcard with calculated element', cds.ql`SELECT from my.BooksWithCalc { * }`)

runBenchmarkFor('expand simple', cds.ql`SELECT from my.Authors { ID, books { title } }`)
runBenchmarkFor('expand recursive (depth 3)', cds.ql`SELECT from my.Genres { ID, parent { parent { parent { name }}} }`)

runBenchmarkFor('exists simple', cds.ql`SELECT from my.Genres { ID } where exists parent`)
runBenchmarkFor('exists simple with path expression', cds.ql`SELECT from my.Genres { ID } where exists parent[parent.name = 'foo']`)
runBenchmarkFor('exists recursive (depth 3)', cds.ql`SELECT from my.Genres { ID } where exists parent.parent.parent`)

runBenchmarkFor('assoc2join simple', cds.ql`SELECT from my.Books { ID, author.firstName }`)
runBenchmarkFor('assoc2join recursive (depth 3)', cds.ql`SELECT from my.Genres { ID, parent.parent.parent.name }`)

runBenchmarkFor('@cds.search simple', SELECT.from('my.Authors').columns('ID', 'firstName', 'lastName').search('Tolkien'))
runBenchmarkFor('@cds.search deep', SELECT.from('my.AuthorsSearchBooks').columns('ID', 'firstName', 'lastName').search('The Lord of the Rings'))

runBenchmarkFor('scoped query', cds.ql`SELECT from my.Books:author { name }`)
})


function runBenchmarkFor(name, cqn) {
it(name, async () =>
report(
await perf.fn(
() => {
cqn4sql(cqn)
},
{
title: name,
warmup: {
duration: '3s',
},
duration: '10s',
},
),
),
)
}
}
1 change: 1 addition & 0 deletions db-service/bench/cqn4sql/results/cqn4sql-benchmarks.json

Large diffs are not rendered by default.

96 changes: 96 additions & 0 deletions db-service/bench/cqn4sql/results/perf-benchmarks.html

Large diffs are not rendered by default.

105 changes: 105 additions & 0 deletions db-service/bench/cqn4sql/utils/format-benchmarks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
'use strict'

const fs = require('fs')
const path = require('path')
const { execSync } = require('child_process')

function getCommitShort() {
try {
return execSync('git rev-parse --short HEAD', { stdio: ['ignore', 'pipe', 'ignore'] })
.toString()
.trim()
} catch {
const sha = process.env.GITHUB_SHA || process.env.CI_COMMIT_SHA || ''
return sha ? sha.slice(0, 7) : `unknown-${new Date().toISOString().replace(/[:.]/g, '-')}`
}
}

function compactRequests(requests) {
return {
total: requests.total,
mean: requests.mean,
min: requests.min,
max: requests.max,
stddev: requests.stddev,
p50: requests.p50,
p90: requests.p90,
p99: requests.p99,
}
}

function collectBenchmarksForFile(resultsPath, testFileBase) {
const lines = fs.existsSync(resultsPath) ? fs.readFileSync(resultsPath, 'utf8').split(/\r?\n/).filter(Boolean) : []

const benchmarks = {}
for (const line of lines) {
let obj
try {
obj = JSON.parse(line)
} catch {
continue
}
const key = Object.keys(obj)[0]
const data = obj[key]
if (!data) continue

const fromThisFile = data.file === testFileBase || (typeof key === 'string' && key.startsWith(`${testFileBase}:`))
if (!fromThisFile) continue

const title = data.title || key.split(':')[1] || key
if (data.requests) benchmarks[title] = compactRequests(data.requests) // keep only subset of "requests"
}
return benchmarks
}

/**
* Formats & writes the cumulative dump:
* dump[commit] = { date, benchmarks }
*
* @param {object} opts
* @param {string} opts.testFile absolute __filename of the calling test
* @param {string} [opts.resultsFile='results.bench']
* @param {string} [opts.dumpFile='perf-benchmarks.json']
* @returns {false | {commit:string,file:string,count:number}}
*/
function writeDump({
testFile,
resultsFile = 'results.bench',
dumpFile = 'cqn4sql-benchmarks.json',
deleteResultsFile = true,
}) {
const testFileBase = path.basename(testFile)
const resultsPath = path.resolve(process.cwd(), resultsFile)
const dumpPath = path.resolve(process.cwd() + '/results', dumpFile)

const benchmarks = collectBenchmarksForFile(resultsPath, testFileBase)
if (!Object.keys(benchmarks).length) return false

const commit = getCommitShort()
const entry = { date: new Date().toISOString(), benchmarks }

let dump = {}
if (fs.existsSync(dumpPath)) {
try {
dump = JSON.parse(fs.readFileSync(dumpPath, 'utf8')) || {}
} catch {
dump = {}
}
}
dump[commit] = entry

fs.mkdirSync(path.dirname(dumpPath), { recursive: true })
fs.writeFileSync(dumpPath, JSON.stringify(dump) + '\n', 'utf8')

if (deleteResultsFile && fs.existsSync(resultsPath)) {
try {
fs.unlinkSync(resultsPath)
} catch {
/* ignore */
}
}

return { commit, file: dumpPath, count: Object.keys(benchmarks).length }
}

module.exports = { writeDump }
Loading