|
| 1 | +import { NODES, TYPES } from './results-contract.mjs'; |
| 2 | + |
| 3 | +export const SUMMARY_COLUMNS = Object.freeze([ |
| 4 | + { id: 'load', label: 'Load', kind: 'write', cases: ['load'] }, |
| 5 | + { id: 'write', label: 'Write', kind: 'write', cases: ['update', 'set', 'append'] }, |
| 6 | + { id: 'read', label: 'Read', kind: 'read', cases: ['point-read', 'get', 'pk-select', 'neighbors', 'range-read', 'read'] }, |
| 7 | + { id: 'search', label: 'Search', kind: 'query', cases: ['range-query', 'range-select', 'traverse', 'exact-top10', |
| 8 | + 'term-query', 'and-query', 'aggregate', 'windows', 'group-by', 'filtered-aggregate'] }, |
| 9 | + { id: 'delete', label: 'Delete', kind: 'write', cases: ['delete'] }, |
| 10 | + { id: 'mixed', label: 'Mixed', kind: 'mixed', cases: ['mixed', 'cycle'] }, |
| 11 | +]); |
| 12 | + |
| 13 | +function summarize(results, database, column, nodes) { |
| 14 | + const sources = results.filter(result => result.database === database && result.nodes === nodes).flatMap(result => { |
| 15 | + const type = TYPES.find(type => type.id === result.type); |
| 16 | + return type.cases.filter(item => column.cases.includes(item.id)).map(item => ({ |
| 17 | + key: `${type.id}|${item.id}`, label: `${type.label} / ${item.label}`, result, |
| 18 | + entry: result.cases.find(entry => entry.id === item.id), |
| 19 | + })); |
| 20 | + }); |
| 21 | + const base = { nodes, summary: true, caseCount: sources.length, coverage: sources.map(source => source.key).sort().join(',') }; |
| 22 | + const failed = sources.filter(source => source.entry?.status === 'failed' || (!source.entry && source.result.error !== null)); |
| 23 | + if (failed.length > 0) { |
| 24 | + const error = failed.map(source => `${source.label}: ${source.entry?.error ?? source.result.error}`).join('\n'); |
| 25 | + return { ...base, status: 'failed', reason: `${failed.length} of ${sources.length} cases failed: ${error.split(/\r?\n/)[0]}`, error }; |
| 26 | + } |
| 27 | + const missing = sources.filter(source => !source.entry); |
| 28 | + if (sources.length === 0 || missing.length > 0) return { ...base, status: 'missing', |
| 29 | + reason: sources.length === 0 ? 'no recorded cases' : `${missing.length} of ${sources.length} cases not run` }; |
| 30 | + const operations = sources.reduce((sum, source) => sum + source.entry.operations, 0); |
| 31 | + const seconds = sources.reduce((sum, source) => sum + source.entry.seconds, 0); |
| 32 | + if (seconds <= 0) return { ...base, status: 'missing', reason: 'no recorded elapsed time' }; |
| 33 | + return { ...base, status: 'passed', value: operations / seconds, operations, seconds }; |
| 34 | +} |
| 35 | + |
| 36 | +// Rates describe the recorded workload: completed operations divided by its combined measured duration. |
| 37 | +export function summaryModel(results) { |
| 38 | + const databases = [...new Set(TYPES.flatMap(type => type.databases))]; |
| 39 | + const byKey = new Map(databases.flatMap(database => SUMMARY_COLUMNS.flatMap(column => NODES.map(nodes => |
| 40 | + [`${database}|${column.id}|${nodes}`, summarize(results, database, column, nodes)])))); |
| 41 | + const line = (database, column, nodes) => byKey.get(`${database}|${column.id}|${nodes}`); |
| 42 | + const result = (database, nodes) => results.find(result => result.database === database && result.nodes === nodes); |
| 43 | + const best = new Map(SUMMARY_COLUMNS.map(column => [column.id, Math.max(0, ...databases.flatMap(database => NODES.map(nodes => |
| 44 | + line(database, column, nodes))).filter(line => line.status === 'passed').map(line => line.value))])); |
| 45 | + const supports = (database, column) => TYPES.some(type => type.databases.includes(database) && |
| 46 | + type.cases.some(item => column.cases.includes(item.id))); |
| 47 | + return { summary: true, type: { id: 'all', label: 'All types', databases, cases: SUMMARY_COLUMNS }, results, line, result, best, supports, |
| 48 | + nodes: database => NODES.filter(nodes => nodes === 1 || result(database, nodes) !== undefined) }; |
| 49 | +} |
0 commit comments