From 56bbefa0c5de32058672272baf68fbe4d01f6586 Mon Sep 17 00:00:00 2001 From: Bob den Os Date: Fri, 1 Dec 2023 13:47:20 +0100 Subject: [PATCH 1/3] Initial performance test implementation --- test/perf/actions.js | 184 +++++++++++++++++++++++ test/perf/index.js | 41 ++++++ test/perf/package.json | 23 +++ test/perf/perf.test.js | 220 ++++++++++++++++++++++++++++ test/perf/srv/analytics-service.cds | 1 + test/perf/srv/server.js | 77 ++++++++++ test/perf/srv/travel-service.cds | 1 + test/perf/srv/travel-service.js | 1 + test/perf/srv/workarounds.js | 1 + test/perf/user.js | 200 +++++++++++++++++++++++++ 10 files changed, 749 insertions(+) create mode 100644 test/perf/actions.js create mode 100644 test/perf/index.js create mode 100644 test/perf/package.json create mode 100644 test/perf/perf.test.js create mode 100644 test/perf/srv/analytics-service.cds create mode 100644 test/perf/srv/server.js create mode 100644 test/perf/srv/travel-service.cds create mode 100644 test/perf/srv/travel-service.js create mode 100644 test/perf/srv/workarounds.js create mode 100644 test/perf/user.js diff --git a/test/perf/actions.js b/test/perf/actions.js new file mode 100644 index 00000000..3acf2eb7 --- /dev/null +++ b/test/perf/actions.js @@ -0,0 +1,184 @@ +const sleep = n => new Promise(resolve => setTimeout(resolve, n)) + +module.exports.landingPage = async function (user, options) { + const travelList = { + name: 'landingPageList', + SELECT: { + from: { ref: ['processor', 'Travel'] }, + count: true, + columns: [ + { ref: ['BeginDate'] }, + { ref: ['CurrencyCode_code'] }, + { ref: ['Description'] }, + { ref: ['EndDate'] }, + { ref: ['HasActiveEntity'] }, + { ref: ['HasDraftEntity'] }, + { ref: ['IsActiveEntity'] }, + { ref: ['TotalPrice'] }, + { ref: ['TravelID'] }, + { ref: ['TravelStatus_code'] }, + { ref: ['TravelUUID'] }, + { ref: ['to_Agency_AgencyID'] }, + { ref: ['to_Customer_CustomerID'] }, + { + ref: ['DraftAdministrativeData'], expand: [ + { ref: ['DraftUUID'] }, + { ref: ['InProcessByUser'] }, + { ref: ['LastChangedByUser'] }, + ] + }, { + ref: ['TravelStatus'], expand: [ + { ref: ['code'] }, + { ref: ['name'] }, + ] + }, { + ref: ['to_Agency'], expand: [ + { ref: ['AgencyID'] }, + { ref: ['Name'] }, + ] + }, { + ref: ['to_Customer'], expand: [ + { ref: ['CustomerID'] }, + { ref: ['LastName'] }, + ] + }, + ], + orderBy: [ + { ref: ['TravelID'], sort: 'desc' } + ], + where: [ + { ref: ['IsActiveEntity'] }, + '=', + { val: false }, + 'or', + { ref: ['SiblingEntity', 'IsActiveEntity'] }, + '=', + { val: null } + ], + limit: { + rows: { val: 30 }, + offset: { val: 0 } + } + } + } + + const agencyPopup = { + name: 'landingPageAgencyPopup', + SELECT: { + from: { ref: ['processor', 'TravelAgency'] }, + count: true, + columns: [ + { ref: ['AgencyID'] }, + { ref: ['City'] }, + { ref: ['CountryCode_code'] }, + { ref: ['EMailAddress'] }, + { ref: ['Name'] }, + { ref: ['PhoneNumber'] }, + { ref: ['PostalCode'] }, + { ref: ['Street'] }, + { ref: ['WebAddress'] }, + ], + orderby: [ + { ref: ['AgencyID'] }, + ], + limit: { + rows: { val: 58 } + } + } + } + + const customerPopup = { + name: 'landingPageCustomerPopup', + SELECT: { + from: { ref: ['processor', 'Passenger'] }, + count: true, + columns: [ + { ref: ['City'] }, + { ref: ['CountryCode_code'] }, + { ref: ['CustomerID'] }, + { ref: ['EMailAddress'] }, + { ref: ['FirstName'] }, + { ref: ['LastName'] }, + { ref: ['PhoneNumber'] }, + { ref: ['PostalCode'] }, + { ref: ['Street'] }, + { ref: ['Title'] }, + ], + orderby: [ + { ref: ['CustomerID'] }, + ], + limit: { + rows: { val: 58 } + } + } + } + + const travelStatusPopover = { + name: 'landingPageStatusPopover', + SELECT: { + from: { ref: ['processor', 'TravelStatus'] }, + columns: [ + { ref: ['code'] }, + { ref: ['name'] }, + ], + orderby: [ + { ref: ['CustomerID'] }, + ], + limit: { + rows: { val: 100 } + } + } + } + + const { scroll = {}, filter = {} } = options + + const { search, editStatus, agency, customer, travelStatus } = filter + + // Load page before starting to apply the filters + if (search || editStatus || agency || customer || travelStatus) { + await user.exec(travelList) + await sleep(1000) + } + + if(search) { + travelList.SELECT.search = search + } + + if (agency) { + const speed = agency.speed || 2000 + await user.exec(agencyPopup) + await sleep(speed) + const search = agency.search + if (search) { + agencyPopup.SELECT.search = search + await user.exec(agencyPopup) + await sleep(speed) + } + } + + if (customer) { + const speed = customer.speed || 2000 + await user.exec(customerPopup) + await sleep(speed) + const search = customer.search + if (search) { + customerPopup.SELECT.search = search + await user.exec(customerPopup) + await sleep(speed) + } + } + + if (travelStatus) { + const speed = travelStatus.speed || 1000 + await user.exec(travelStatusPopover) + await sleep(speed) + } + + const scrollSpeed = scroll.speed || 2000 + const scrollAmount = scroll.rows || 0 + for (let offset = 0; offset < scrollAmount; offset += 30) { + travelList.SELECT.limit.offset.val = offset + await user.exec(travelList) + await sleep(scrollSpeed) + } +} diff --git a/test/perf/index.js b/test/perf/index.js new file mode 100644 index 00000000..8759e66d --- /dev/null +++ b/test/perf/index.js @@ -0,0 +1,41 @@ +// Lower highwater mark to get more frequent updates on the request +require('stream').Readable.setDefaultHighWaterMark(false, 1 << 7) + +const http = require('http') +const { spawn } = require('child_process') + +const server = http.createServer((req, res) => { + if (req.url !== '/start') { + res.writeHead(404) + return res.end() + } + if(req.method !== 'GET') { + res.writeHead(200) + return res.end() + } + try { + const jest = spawn('npx', ['jest', '--forceExit'], { cmd: __dirname, stdio: 'pipe' }) + res.writeHead(200) + res.on('close', () => jest.kill()) + jest.stdout.pipe(res) + jest.stderr.pipe(res) + jest.on('exit', code => { + if (code) { + res.write(`Process exited with code ${code}`) + } + res.end() + }) + } catch (e) { + console.error(e) + res.end(e) + } +}) + +const port = process.env.PORT || 4005 +server.listen(port, (err) => { + if (err) { + console.error(err) + return process.exit(1) + } + console.log(`listening on port: ${port}`) +}) \ No newline at end of file diff --git a/test/perf/package.json b/test/perf/package.json new file mode 100644 index 00000000..4aef1ff0 --- /dev/null +++ b/test/perf/package.json @@ -0,0 +1,23 @@ +{ + "name": "@capire/sflight-performance", + "version": "1.0.0", + "private": true, + "description": "CAP flight demo scenario performance tests", + "license": "SAP SAMPLE CODE LICENSE", + "repository": "https://github.com/SAP-samples/cap-sflight", + "engines": { + "node": ">=16" + }, + "scripts": { + "start": "node index.js" + }, + "dependencies": { + "@sap/cds": ">=7.0.0", + "axios": "^1", + "jest": "^29.0.2" + }, + "jest": { + "testEnvironment": "node", + "testTimeout": 3600000 + } +} \ No newline at end of file diff --git a/test/perf/perf.test.js b/test/perf/perf.test.js new file mode 100644 index 00000000..269e9da4 --- /dev/null +++ b/test/perf/perf.test.js @@ -0,0 +1,220 @@ +const { User } = require('./user') +const actions = require('./actions') + +const dbs = [{ + name: 'sqlite', + requires: { + db: { + kind: 'sqlite', + impl: '@cap-js/sqlite', + } + } +}, { + name: 'sqlite (disk)', + requires: { + db: { + kind: 'sqlite', + impl: '@cap-js/sqlite', + credentials: { + database: 'perf-test' + } + } + } +}, { + name: 'hana hdb (new)', + requires: { + db: { + pool: { + max: 100 + }, + kind: 'hana', + impl: '@cap-js/hana', + driver: 'hdb', + vcap: { + label: 'hana' + } + } + } +}, { + name: 'hana hana-client (new)', + requires: { + db: { + pool: { + max: 100 + }, + kind: 'hana', + impl: '@cap-js/hana', + driver: 'hana-client', + vcap: { + label: 'hana' + } + } + } +}, { + name: 'hana hdb (old)', + requires: { + db: { + kind: 'hana', + sqlDialect: 'hana', + vcap: { + label: 'hana' + } + } + } +}, { + name: 'postgres', + requires: { + db: { + pool: { + max: 100, + acquireTimeoutMillis: 60 * 1000, + }, + kind: 'postgres', + impl: '@cap-js/postgres', + dialect: "postgres", + schema_evolution: false, + vcap: { + label: 'postgresql-db' + } + } + } +}] + +const protocols = [ + // { name: 'okra', to: ['odata'] }, + // { name: 'odata', to: ['odata'] }, + // { name: 'rest', to: ['rest'] }, + { name: 'graphql', to: ['graphql'] } +] + +const loads = [ + { name: 'baseline', users: 1 << 0 }, + // { name: 'xs', users: 1 << 2 }, + // { name: 's', users: 1 << 4 }, + // { name: 'm', users: 1 << 7 }, + { name: 'l', users: 1 << 10 }, + // REVISIT: Requires workers to create enough load */ + // { name: 'xl', users: 1 << 12 }, + // { name: 'xxl', users: 1 << 16 }, +] + + +const configs = loads.map(l => protocols.map(p => dbs.map(d => Object.assign({ + // folders: { srv: "test/perf/srv" } +}, p, d, l, { name: [p.name, d.name, l.name] })))).flat(2) + +const results = {} + +const sleep = n => new Promise(resolve => setTimeout(resolve, n)) + +describe.each(configs)('$name.0 $name.1', (config) => { + const getUser = () => new User(config.to[0]) + + beforeAll(async () => { + const admin = getUser() + const ping = async () => { + let up = false + while (!up) { + try { + await admin.get('__ping__', '/') + up = true + } catch (e) { /**/ } + } + } + + try { + await ping() + await admin.post('__config__', '/config', JSON.stringify(config)) + } catch (e) { + let s = performance.now() + await ping() + console.log(`restarted ${config.name} after ${performance.now() - s}ms`) + s = performance.now() + await admin.exec({ name: '__ready__', SELECT: { columns: [{ ref: ['TravelUUID'] }], limit: { rows: { val: 1 } }, from: { ref: ['processor', 'Travel'] } } }) + console.log(`deployed ${config.name} after ${performance.now() - s}ms`) + } + }) + + test('landingPage (scroll: 1000)', async () => { + const proms = [] + const users = [] + for (let i = 0; i < config.users; i++) { + await sleep(100) + const user = getUser() + users.push(user) + proms.push(actions.landingPage(user, { scroll: { rows: 1000 } })) + } + await Promise.allSettled(proms) + + const requests = {} + users.forEach(user => user.timings.forEach(timing => { + const name = timing.name + let cur = requests[name] + if (!cur || cur.duration > timing.duration) { + const total = cur?.total || 0 + const calls = cur?.calls || 0 + cur = requests[name] = timing + cur.total = total + cur.calls = calls + } + cur.total += timing.duration + cur.calls++ + })) + results[config.name[0]] ??= {} + results[config.name[0]][config.name[1]] ??= {} + results[config.name[0]][config.name[1]][config.name[2]] ??= requests + }) +}); + +afterAll(() => { + const header = '| |' + dbs.map(db => db.name).join('|') + '|\n|---|' + dbs.map(() => '---').join('|') + '|' + let requests + + const calls = {} + const tables = {} + for (const load of loads) { + for (const protocol of protocols) { + let prefix = `| ${protocol.name} |` + for (const db of dbs) { + const result = results[protocol.name][db.name][load.name] + if (!requests) requests = Object.keys(result) + + for (const request of requests) { + tables[request] ??= {} + const table = tables[request][load.name] = tables[request][load.name] || [header] + + if (prefix) { + table.push(prefix) + } + const res = result?.[request] || { duration: NaN, total: NaN, calls: NaN } + table[table.length - 1] = `${table[table.length - 1]} ${res.duration >>> 0} ms (avg: ${(res.total / res.calls) >>> 0} ms) | ` + calls[request] = (calls[request] || 0) + res.calls + } + prefix = '' + } + } + } + + // Order requests by number of times called + requests = requests.sort((a, b) => calls[a] - calls[b]) + + const fs = require('fs') + const fd = fs.createWriteStream(__dirname + '/performance.md') + + const write = (str) => { + process.stdout.write(str) + fd.write(str) + } + + write('--------------------------------\n') + requests.forEach(request => { + write(`### ${request}\n`) + + const table = tables[request] + loads.forEach(load => { + write(`
\n${load.name} (${load.users} users)\n\n${table[load.name].join('\n')}\n
\n`) + }) + }) + + fd.close() +}) diff --git a/test/perf/srv/analytics-service.cds b/test/perf/srv/analytics-service.cds new file mode 100644 index 00000000..23746d71 --- /dev/null +++ b/test/perf/srv/analytics-service.cds @@ -0,0 +1 @@ +using from '../../../srv/analytics-service'; diff --git a/test/perf/srv/server.js b/test/perf/srv/server.js new file mode 100644 index 00000000..f21f8aaf --- /dev/null +++ b/test/perf/srv/server.js @@ -0,0 +1,77 @@ +// process.env.DEBUG = 'trace' + +// Inject trace trailers to all http requests +const http = require('http') + +const ServerResponse = http.ServerResponse.prototype + +ServerResponse.__setHeader = ServerResponse.setHeader +ServerResponse.setHeader = function (a, b) { + if (a.toLowerCase() === 'content-length') + return + return this.__setHeader(a, b) +} + +ServerResponse.__writeHead = ServerResponse.writeHead +ServerResponse.writeHead = function (a, b, c) { + c = c || {} + if (this.req.method !== 'HEAD' && a >= 200 && a < 300) { + c.Trailer = 'Server-Timing' + } + return this.__writeHead(a, b, c) +} + +ServerResponse.__end = ServerResponse.end +ServerResponse.end = function () { + const perf = this.req._perf + if (perf) { + const timings = {} + perf.forEach(e => { + if (!e.stop) { + e.stop = performance.now() + } + const name = e.details[0].replace('@cap-js/', '') + timings[name] = (timings[name] || 0) + (e.stop - e.start) + }) + this.addTrailers({ + 'Server-Timing': Object.keys(timings).map(k => `${k};dur=${timings[k]}`).join() + }) + } + + return this.__end(...arguments) +} + +// Inject configuration endpoint +const cds = require('@sap/cds') +cds.env.protocols.graphql = { path: '/gql', impl: '@cap-js/graphql' } + +cds.options.to = cds.env.to +cds.requires.middlewares = true + +cds.on('bootstrap', async app => { + const fs = require('fs') + app.post('/config', (req) => { + const prefix = `{"sql":{"dialect":"plain"},"[perf]":` + const suffix = `}` + const fd = fs.createWriteStream(cds.root + '/.cdsrc.json') + fd.write(prefix) + req.pipe(fd, { end: false }) + req.on('end', () => { + fd.write(suffix) + // Don't respond as the server restart will reset the connection + }) + }) + + cds.requires.db.pool ??= {} + cds.requires.db.pool.max = 1 + + await cds.tx(async () => cds.deploy(cds.options?.from?.[0] || '*')).catch(e => { + console.log(e) + }) + if(cds.db?.pools?._factory?.options?.max) { + cds.db.pools._factory.options.max = 100 + } + cds.disconnect() +}) + +process.on('exit', () => cds.disconnect()) diff --git a/test/perf/srv/travel-service.cds b/test/perf/srv/travel-service.cds new file mode 100644 index 00000000..b4c029d5 --- /dev/null +++ b/test/perf/srv/travel-service.cds @@ -0,0 +1 @@ +using from '../../../srv/travel-service'; diff --git a/test/perf/srv/travel-service.js b/test/perf/srv/travel-service.js new file mode 100644 index 00000000..58d29a0c --- /dev/null +++ b/test/perf/srv/travel-service.js @@ -0,0 +1 @@ +module.exports = require('../../../srv/travel-service') diff --git a/test/perf/srv/workarounds.js b/test/perf/srv/workarounds.js new file mode 100644 index 00000000..b9bbf42b --- /dev/null +++ b/test/perf/srv/workarounds.js @@ -0,0 +1 @@ +module.exports = require('../../../srv/workarounds') diff --git a/test/perf/user.js b/test/perf/user.js new file mode 100644 index 00000000..d677215b --- /dev/null +++ b/test/perf/user.js @@ -0,0 +1,200 @@ +const ssl = false + +const axios = require('axios') +const http = ssl ? require('https') : require('http') + +const endpoint = process.env.ENDPOINT || 'http://localhost:4004' + +module.exports.User = class User { + constructor(target) { + this.timings = [] + this.agent = new http.Agent({ keepAlive: true }) + + switch (target) { + case 'odata': this.exec = this.exec_odata + break + case 'rest': this.exec = this.exec_rest + break + case 'graphql': this.exec = this.exec_graphql + break + default: throw new Error(`Unknown protocol ${target}`) + } + } + + async exec_odata(query) { + // Just calls rest + const result = await this.exec_rest(query) + return result.value + } + + async exec_rest(query) { + if (query.SELECT) { + const columns = (cols = []) => { + return { + $select: cols.filter(c => !c.expand).map(c => c.ref.join('/')), + $expand: cols.filter(c => c.expand).map(c => { + const sub = columns(c.expand) + const s = sub.$select + const e = sub.$expand + const h = s.length || e.length + const b = s.length && e.length + return `${c.ref.join('/')}${h ? '(' : ''}${s.length ? '$select=' + s : ''}${b ? ';' : ''}${e.length ? '$expand=' + e : ''}${h ? ')' : ''}` + }) + } + } + + const { $select, $expand } = columns(query.SELECT.columns) + const $top = query.SELECT.limit?.rows?.val + const $skip = query.SELECT.limit?.offset?.val + const $orderby = query.SELECT.orderBy?.map(c => `${c.ref.join('/')} ${c.sort || 'asc'}`) + + const paramets = [ + $select?.length ? '$select=' + $select : undefined, + $expand?.length ? '$expand=' + $expand : undefined, + typeof $top === 'number' ? '$top=' + $top : undefined, + typeof $skip === 'number' ? '$skip=' + $skip : undefined, + $orderby?.length ? '$orderby=' + $orderby : undefined + ].filter(a => a) + + const result = await this.get(query.name, `/${query.SELECT.from.ref.join('/')}${paramets.length ? '?' + paramets.join('&') : ''}`) + return result.data + } + } + + async exec_graphql_schema() { + if (this._graphl_schema) return this._graphl_schema + const query = ` + query schema { __schema { types { ...FullType } } } + fragment FullType on __Type { kind name description + fields(includeDeprecated: false) { + name type { ...TypeRef } + } + } + fragment TypeRef on __Type { kind name + ofType { kind name + ofType { kind name + ofType { kind name + ofType { kind name + ofType { kind name + ofType { kind name + ofType { kind name + }}}}}}}} + ` + this.__proto__._graphl_schema = this.post('__SCHEMA__', '/gql', { query }) + return this.exec_graphql_schema() + } + + async exec_graphql(query) { + if (query.SELECT) { + const services = { + processor: 'TravelService', + analytics: 'AnalyticsService', + } + const ref = [...query.SELECT.from.ref] + ref[0] = services[ref[0]] + const path = ref.join('{') + + const top = query.SELECT.limit?.rows?.val + const skip = query.SELECT.limit?.offset?.val + + const filter = '' + + const args = [] + if (typeof top === 'number') args.push(`top:${top}`) + if (typeof skip === 'number') args.push(`skip:${skip}`) + if (filter) args.push(`filter:${top}`) + + const schema = await this.exec_graphql_schema() + const getType = name => { + return schema.data.data.__schema.types.find(t => t.name === name) + } + + const walkPath = (ref, parent) => { + let i = 0 + let cur = parent + if (!cur) { + cur = getType(ref[i++]) + } + for (; i < ref.length; i++) { + cur = getType(cur.fields.find(f => f.name === ref[i]).type.name) + } + return cur + } + + const columns = (parent, cols = []) => { + const ret = [] + const hasNodes = parent.fields.find(f => f.name === 'nodes') + if (hasNodes) { + ret.push('nodes{') + parent = getType(hasNodes.type.ofType.name) + } + for (let i = 0; i < cols.length; i++) { + const col = cols[i] + if (col.expand) { + ret.push(`${col.ref[0]} {${columns(walkPath(col.ref, parent), col.expand)}}`) + } else { + ret.push(col.ref[0]) + } + } + if (hasNodes) { + ret.push('}') + } + return ret.join(' ') + } + + const cols = columns(walkPath(ref), query.SELECT.columns) + + const gql = `{${path}${args.length ? `(${args})` : ''}{${cols}}${ref.map(() => '').join('}')}}` + const result = await this.post(query.name, '/gql', { query: gql }) + const errors = result.data.errors + if (errors) { + throw new Error(`graphql errors:\n ${errors.map(e => e.message).join('\n ')}`) + } + return ref.reduce((l, c) => l[c], result.data.data).nodes + } + } + + async timing(fn, name, ...args) { + const s = performance.now() + const result = await fn(...args) + const dur = performance.now() - s + const trailers = result.request.res.trailers["server-timing"] + ?.split(',') + .map(e => { + const s = e.split(';') + return ` ${s[0]}(${s[1].slice(4)} ms)` + }) + .join('\n') + + this.timings.push({ + name: name, + url: args[0], + duration: dur, + details: trailers || null, + }) + + return result + } + + async get(name, path, options) { + return this.timing(async (path, options = {}) => { + options.agent = this.agent + options.headers ??= {} + options.headers.authorization = 'Basic YWxpY2U6' + const res = await axios.get(endpoint + path, options) + if (res.status > 300) throw new Error(`Request failed with code ${res.status}`) + return res + }, name, path, options) + } + + async post(name, path, body, options) { + return this.timing(async (path, body, options = {}) => { + options.agent = this.agent + options.headers ??= {} + options.headers.authorization = 'Basic YWxpY2U6' + const res = await axios.post(endpoint + path, body, options) + if (res.status > 300) throw new Error(`Request failed with code ${res.status}`) + return res + }, name, path, body, options) + } +} \ No newline at end of file From 16586c9b88960deaa108fccdb8d4915ce2f21d47 Mon Sep 17 00:00:00 2001 From: Bob den Os Date: Thu, 21 Mar 2024 12:45:09 +0100 Subject: [PATCH 2/3] Fix service add hcql add sqlite old --- test/perf/actions.js | 6 +----- test/perf/index.js | 2 +- test/perf/package.json | 43 +++++++++++++++++++++-------------------- test/perf/perf.test.js | 43 ++++++++++++++++++++++++++++++----------- test/perf/srv/server.js | 7 ++----- test/perf/user.js | 14 ++++++++++++++ 6 files changed, 72 insertions(+), 43 deletions(-) diff --git a/test/perf/actions.js b/test/perf/actions.js index 3acf2eb7..c408650c 100644 --- a/test/perf/actions.js +++ b/test/perf/actions.js @@ -8,7 +8,6 @@ module.exports.landingPage = async function (user, options) { count: true, columns: [ { ref: ['BeginDate'] }, - { ref: ['CurrencyCode_code'] }, { ref: ['Description'] }, { ref: ['EndDate'] }, { ref: ['HasActiveEntity'] }, @@ -16,10 +15,7 @@ module.exports.landingPage = async function (user, options) { { ref: ['IsActiveEntity'] }, { ref: ['TotalPrice'] }, { ref: ['TravelID'] }, - { ref: ['TravelStatus_code'] }, { ref: ['TravelUUID'] }, - { ref: ['to_Agency_AgencyID'] }, - { ref: ['to_Customer_CustomerID'] }, { ref: ['DraftAdministrativeData'], expand: [ { ref: ['DraftUUID'] }, @@ -140,7 +136,7 @@ module.exports.landingPage = async function (user, options) { await sleep(1000) } - if(search) { + if (search) { travelList.SELECT.search = search } diff --git a/test/perf/index.js b/test/perf/index.js index 8759e66d..4faea13d 100644 --- a/test/perf/index.js +++ b/test/perf/index.js @@ -9,7 +9,7 @@ const server = http.createServer((req, res) => { res.writeHead(404) return res.end() } - if(req.method !== 'GET') { + if (req.method !== 'GET') { res.writeHead(200) return res.end() } diff --git a/test/perf/package.json b/test/perf/package.json index 4aef1ff0..9e2e5509 100644 --- a/test/perf/package.json +++ b/test/perf/package.json @@ -1,23 +1,24 @@ { - "name": "@capire/sflight-performance", - "version": "1.0.0", - "private": true, - "description": "CAP flight demo scenario performance tests", - "license": "SAP SAMPLE CODE LICENSE", - "repository": "https://github.com/SAP-samples/cap-sflight", - "engines": { - "node": ">=16" - }, - "scripts": { - "start": "node index.js" - }, - "dependencies": { - "@sap/cds": ">=7.0.0", - "axios": "^1", - "jest": "^29.0.2" - }, - "jest": { - "testEnvironment": "node", - "testTimeout": 3600000 - } + "name": "@capire/sflight-performance", + "version": "1.0.0", + "private": true, + "description": "CAP flight demo scenario performance tests", + "license": "SAP SAMPLE CODE LICENSE", + "repository": "https://github.com/SAP-samples/cap-sflight", + "engines": { + "node": ">=16" + }, + "scripts": { + "start": "node index.js", + "start:watch": "cds watch --profile perf" + }, + "dependencies": { + "@sap/cds": ">=7.0.0", + "axios": "^1", + "jest": "^29.0.2" + }, + "jest": { + "testEnvironment": "node", + "testTimeout": 3600000 + } } \ No newline at end of file diff --git a/test/perf/perf.test.js b/test/perf/perf.test.js index 269e9da4..c9267c33 100644 --- a/test/perf/perf.test.js +++ b/test/perf/perf.test.js @@ -2,7 +2,7 @@ const { User } = require('./user') const actions = require('./actions') const dbs = [{ - name: 'sqlite', + name: 'sqlite (new)', requires: { db: { kind: 'sqlite', @@ -10,7 +10,7 @@ const dbs = [{ } } }, { - name: 'sqlite (disk)', + name: 'sqlite disk (new)', requires: { db: { kind: 'sqlite', @@ -20,6 +20,26 @@ const dbs = [{ } } } +}, { + name: 'sqlite (old)', + requires: { + db: { + kind: 'legacy-sqlite', + credentials: { + url: ":memory:" + } + } + } +}, { + name: 'sqlite disk (old)', + requires: { + db: { + kind: 'legacy-sqlite', + credentials: { + url: "perf-test" + } + } + } }, { name: 'hana hdb (new)', requires: { @@ -50,7 +70,7 @@ const dbs = [{ } } } -}, { +}, /*{ name: 'hana hdb (old)', requires: { db: { @@ -61,7 +81,7 @@ const dbs = [{ } } } -}, { +}, */{ name: 'postgres', requires: { db: { @@ -81,18 +101,19 @@ const dbs = [{ }] const protocols = [ - // { name: 'okra', to: ['odata'] }, - // { name: 'odata', to: ['odata'] }, - // { name: 'rest', to: ['rest'] }, - { name: 'graphql', to: ['graphql'] } + { name: 'okra', to: ['odata'] }, + { name: 'odata', to: ['odata'] }, + { name: 'rest', to: ['rest'] }, + { name: 'graphql', to: ['graphql'] }, + { name: 'hcql', to: ['hcql'] }, ] const loads = [ { name: 'baseline', users: 1 << 0 }, // { name: 'xs', users: 1 << 2 }, // { name: 's', users: 1 << 4 }, - // { name: 'm', users: 1 << 7 }, - { name: 'l', users: 1 << 10 }, + { name: 'm', users: 1 << 7 }, + // { name: 'l', users: 1 << 10 }, // REVISIT: Requires workers to create enough load */ // { name: 'xl', users: 1 << 12 }, // { name: 'xxl', users: 1 << 16 }, @@ -176,7 +197,7 @@ afterAll(() => { for (const protocol of protocols) { let prefix = `| ${protocol.name} |` for (const db of dbs) { - const result = results[protocol.name][db.name][load.name] + const result = results?.[protocol.name]?.[db.name]?.[load.name] || {} if (!requests) requests = Object.keys(result) for (const request of requests) { diff --git a/test/perf/srv/server.js b/test/perf/srv/server.js index f21f8aaf..dd4ca018 100644 --- a/test/perf/srv/server.js +++ b/test/perf/srv/server.js @@ -1,4 +1,4 @@ -// process.env.DEBUG = 'trace' +process.env.DEBUG = 'trace' // Inject trace trailers to all http requests const http = require('http') @@ -68,10 +68,7 @@ cds.on('bootstrap', async app => { await cds.tx(async () => cds.deploy(cds.options?.from?.[0] || '*')).catch(e => { console.log(e) }) - if(cds.db?.pools?._factory?.options?.max) { - cds.db.pools._factory.options.max = 100 - } - cds.disconnect() + await cds.run('CREATE UNIQUE INDEX IF NOT EXISTS FAST_LANDING_PAGE_INDEX ON sap_fe_cap_travel_Travel(TravelID DESC, TravelUUID ASC)') }) process.on('exit', () => cds.disconnect()) diff --git a/test/perf/user.js b/test/perf/user.js index d677215b..18f36491 100644 --- a/test/perf/user.js +++ b/test/perf/user.js @@ -17,6 +17,8 @@ module.exports.User = class User { break case 'graphql': this.exec = this.exec_graphql break + case 'hcql': this.exec = this.exec_hcql + break default: throw new Error(`Unknown protocol ${target}`) } } @@ -154,6 +156,18 @@ module.exports.User = class User { } } + async exec_hcql(query) { + if (query.SELECT) { + const service = query.SELECT.from.ref.shift() + const name = query.name + query.name = undefined + const result = await this.post(name, `/${service}/`, query) + query.SELECT.from.ref.unshift(service) + query.name = name + return result.data + } + } + async timing(fn, name, ...args) { const s = performance.now() const result = await fn(...args) From 2b5862eb7ee3628e50b626b74d971ad0343a63ad Mon Sep 17 00:00:00 2001 From: Bob den Os Date: Thu, 21 Mar 2024 15:46:38 +0100 Subject: [PATCH 3/3] Fix max pool size --- test/perf/perf.test.js | 6 +++--- test/perf/srv/server.js | 10 ++++++++-- test/perf/srv/services.cds | 1 + 3 files changed, 12 insertions(+), 5 deletions(-) create mode 100644 test/perf/srv/services.cds diff --git a/test/perf/perf.test.js b/test/perf/perf.test.js index c9267c33..a88b7e96 100644 --- a/test/perf/perf.test.js +++ b/test/perf/perf.test.js @@ -70,7 +70,7 @@ const dbs = [{ } } } -}, /*{ +}, { name: 'hana hdb (old)', requires: { db: { @@ -81,7 +81,7 @@ const dbs = [{ } } } -}, */{ +}, { name: 'postgres', requires: { db: { @@ -102,7 +102,7 @@ const dbs = [{ const protocols = [ { name: 'okra', to: ['odata'] }, - { name: 'odata', to: ['odata'] }, + { name: 'odata', to: ['odata'], features: { odata_new_adapter: true } }, { name: 'rest', to: ['rest'] }, { name: 'graphql', to: ['graphql'] }, { name: 'hcql', to: ['hcql'] }, diff --git a/test/perf/srv/server.js b/test/perf/srv/server.js index dd4ca018..9e2a0273 100644 --- a/test/perf/srv/server.js +++ b/test/perf/srv/server.js @@ -63,12 +63,18 @@ cds.on('bootstrap', async app => { }) cds.requires.db.pool ??= {} - cds.requires.db.pool.max = 1 + + const isSqlite = cds.requires.db.kind.toLowerCase().indexOf('sqlite') > -1 + if (isSqlite) + cds.requires.db.pool.max = 1 await cds.tx(async () => cds.deploy(cds.options?.from?.[0] || '*')).catch(e => { console.log(e) }) - await cds.run('CREATE UNIQUE INDEX IF NOT EXISTS FAST_LANDING_PAGE_INDEX ON sap_fe_cap_travel_Travel(TravelID DESC, TravelUUID ASC)') + + if (isSqlite) { + await cds.run('CREATE UNIQUE INDEX IF NOT EXISTS FAST_LANDING_PAGE_INDEX ON sap_fe_cap_travel_Travel(TravelID DESC, TravelUUID ASC)') + } }) process.on('exit', () => cds.disconnect()) diff --git a/test/perf/srv/services.cds b/test/perf/srv/services.cds new file mode 100644 index 00000000..c8bf8b0e --- /dev/null +++ b/test/perf/srv/services.cds @@ -0,0 +1 @@ +using from '../../../app/services';