From ca013f019ff7a5c5a4fef10ea8d6c959d0da719f Mon Sep 17 00:00:00 2001 From: Revone Date: Thu, 2 Nov 2023 11:38:29 +0800 Subject: [PATCH] [benchmark] pretty html report for benchmark test --- CHANGELOG.md | 2 +- package-lock.json | 20 +- package.json | 6 +- .../data-structures/queue/queue.test.ts | 16 +- test/performance/index.ts | 86 ++++- test/performance/utils/index.ts | 2 + test/performance/utils/json2html.ts | 329 ++++++++++++++++++ test/performance/utils/number.ts | 10 + .../binary-tree/rb-tree.test.ts | 2 +- 9 files changed, 445 insertions(+), 28 deletions(-) create mode 100644 test/performance/utils/index.ts create mode 100644 test/performance/utils/json2html.ts create mode 100644 test/performance/utils/number.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index fcb34d0..67e5894 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ All notable changes to this project will be documented in this file. - [Semantic Versioning](https://semver.org/spec/v2.0.0.html) - [`auto-changelog`](https://github.com/CookPete/auto-changelog) -## [v1.41.2](https://github.com/zrwusa/data-structure-typed/compare/v1.35.0...main) (upcoming) +## [v1.41.3](https://github.com/zrwusa/data-structure-typed/compare/v1.35.0...main) (upcoming) ### Changes diff --git a/package-lock.json b/package-lock.json index f684c58..2853e08 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,18 @@ { "name": "data-structure-typed", - "version": "1.41.2", + "version": "1.41.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "data-structure-typed", - "version": "1.41.2", + "version": "1.41.3", "license": "MIT", "devDependencies": { "@types/benchmark": "^2.1.3", "@types/jest": "^29.5.5", "@types/node": "^20.8.2", + "@types/underscore": "^1.11.12", "@typescript-eslint/eslint-plugin": "^6.7.4", "@typescript-eslint/parser": "^6.7.4", "auto-changelog": "^2.4.0", @@ -35,7 +36,8 @@ "ts-node": "^10.9.1", "tsup": "^7.2.0", "typedoc": "^0.25.1", - "typescript": "^5.2.2" + "typescript": "^5.2.2", + "underscore": "^1.13.6" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -2089,6 +2091,12 @@ "integrity": "sha512-g7CK9nHdwjK2n0ymT2CW698FuWJRIx+RP6embAzZ2Qi8/ilIrA1Imt2LVSeHUzKvpoi7BhmmQcXz95eS0f2JXw==", "dev": true }, + "node_modules/@types/underscore": { + "version": "1.11.12", + "resolved": "https://registry.npmjs.org/@types/underscore/-/underscore-1.11.12.tgz", + "integrity": "sha512-beX81q12OQo809WJ/UYCvUDvJR3YQ4wtehYYQ6eNw5VLyd+KUNBRV4FgzZCHBmACbdPulH9F9ifhxzFFU9TWvQ==", + "dev": true + }, "node_modules/@types/yargs": { "version": "17.0.29", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.29.tgz", @@ -9026,6 +9034,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/underscore": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", + "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==", + "dev": true + }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", diff --git a/package.json b/package.json index 93ad398..4995f60 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "data-structure-typed", - "version": "1.41.2", + "version": "1.41.3", "description": "Data Structures of Javascript & TypeScript. Binary Tree, BST, Graph, Heap, Priority Queue, Linked List, Queue, Deque, Stack, AVL Tree, Tree Multiset, Trie, Directed Graph, Undirected Graph, Singly Linked List, Doubly Linked List, Max Heap, Max Priority Queue, Min Heap, Min Priority Queue.", "main": "dist/cjs/index.js", "module": "dist/mjs/index.js", @@ -60,6 +60,7 @@ "@types/benchmark": "^2.1.3", "@types/jest": "^29.5.5", "@types/node": "^20.8.2", + "@types/underscore": "^1.11.12", "@typescript-eslint/eslint-plugin": "^6.7.4", "@typescript-eslint/parser": "^6.7.4", "auto-changelog": "^2.4.0", @@ -83,7 +84,8 @@ "ts-node": "^10.9.1", "tsup": "^7.2.0", "typedoc": "^0.25.1", - "typescript": "^5.2.2" + "typescript": "^5.2.2", + "underscore": "^1.13.6" }, "keywords": [ "data", diff --git a/test/performance/data-structures/queue/queue.test.ts b/test/performance/data-structures/queue/queue.test.ts index 3a777c3..1546ad2 100644 --- a/test/performance/data-structures/queue/queue.test.ts +++ b/test/performance/data-structures/queue/queue.test.ts @@ -5,16 +5,16 @@ import * as Benchmark from 'benchmark'; export const suite = new Benchmark.Suite(); suite - .add('push', () => { + .add('push 1000000', () => { const queue = new Queue(); for (let i = 0; i < 1000000; i++) { queue.push(i); } }) - // .add('shift', () => { - // const queue = new Queue(); - // for (let i = 0; i < 10000; i++) { - // queue.push(i); - // queue.shift(); - // } - // }) \ No newline at end of file + .add('push & shift 1000000', () => { + const queue = new Queue(); + for (let i = 0; i < 1000000; i++) { + queue.push(i); + queue.shift(); + } + }) \ No newline at end of file diff --git a/test/performance/index.ts b/test/performance/index.ts index c1b01c2..bf808d0 100644 --- a/test/performance/index.ts +++ b/test/performance/index.ts @@ -2,43 +2,103 @@ import * as Benchmark from 'benchmark'; import * as path from 'path'; import * as fs from 'fs'; import * as fastGlob from 'fast-glob'; +import {numberFix, render} from "./utils"; const reportDistPath = 'benchmark'; const testDir = path.join(__dirname, 'data-structures'); const testFiles = fastGlob.sync(path.join(testDir, '**', '*.test.ts')); -const report: {[key: string] : any} = {}; +const report: { [key: string]: any } = {}; -let i = 0; +let testFileCount = 0, suiteCount = 0, completedCount = 0; testFiles.forEach((file: string) => { - i++; + testFileCount++; console.log(`testing file ${file}`); const testName = path.basename(file, '.test.ts'); const testFunction = require(file); const {suite} = testFunction; if (suite) { + suiteCount++; suite.on('cycle', (event: any) => { - console.log(String(event.target)); + console.log(event.target.toString()); }); suite.on('complete', function (this: Benchmark.Suite) { + completedCount++; console.log('Fastest is ' + this.filter('fastest').map('name')); report[testName] = this.map((test: Benchmark) => ({ - name: test.name, - periodMS: test.times.period * 1000, - hz: test.hz, - count: test.count, - mean: test.stats.mean, - deviation: test.stats.deviation, + 'test name': test.name, + 'time consumption (ms)': numberFix((test.times.period * 1000), 2), + 'executions per second': numberFix(test.hz, 2), + 'executed times': numberFix(test.count, 2), + 'sample arithmetic mean (secs)': numberFix(test.stats.mean, 2), + 'sample deviation': numberFix(test.stats.deviation, 2), })); + + // report[testName] = this; - console.log('----i', i, testFiles.length) - if (testFiles.length === i) { - if (!fs.existsSync(reportDistPath)) fs.mkdirSync(reportDistPath, { recursive: true }); + console.log(`test file count: ${testFileCount}. suite count: ${suiteCount}. completed suite count: ${completedCount}`) + if (completedCount === suiteCount) { + if (!fs.existsSync(reportDistPath)) fs.mkdirSync(reportDistPath, {recursive: true}); const filePath = path.join(reportDistPath, 'report.json'); + const htmlFilePath = path.join(reportDistPath, 'report.html'); fs.writeFileSync(filePath, JSON.stringify(report, null, 2)); + let html = ` + + + + Title + + + +
` + for (const r in report) { + if (report.hasOwnProperty(r)) { + html += render(report[r], { + '<>': 'table', + 'html': [ + { + '<>': 'tr', + 'html': [ + {'<>': 'td', 'html': '${name}'}, + {'<>': 'td', 'html': '${periodMS}'}, + {'<>': 'td', 'html': '${mean}'} + ] + } + ] + }); + } + } + + html += `
+ + `; + fs.writeFileSync(htmlFilePath, html); console.log('Performance test report file generated') } }) diff --git a/test/performance/utils/index.ts b/test/performance/utils/index.ts new file mode 100644 index 0000000..a1884f0 --- /dev/null +++ b/test/performance/utils/index.ts @@ -0,0 +1,2 @@ +export * from './json2html'; +export * from './number'; \ No newline at end of file diff --git a/test/performance/utils/json2html.ts b/test/performance/utils/json2html.ts new file mode 100644 index 0000000..e7d8bce --- /dev/null +++ b/test/performance/utils/json2html.ts @@ -0,0 +1,329 @@ +import * as _ from "underscore"; + +function toggleJS(options?: { plainHtml?: boolean }): string { + if (options?.plainHtml) { + return ""; + } else { + return "onclick=\"j2h.toggleVisibility(this);return false\""; + } +} + +function makeLabelDiv( + options: any, + level: number, + keyname: string | number, + datatype?: string +): string { + if (typeof keyname === "number") { + return `
${keyname} 
`; + } else if (typeof keyname === "string") { + if (datatype === "array") { + return `
${keyname}
`; + } else if (datatype === "object") { + return `
${keyname}:
`; + } else { + return `
${keyname}:
`; + } + } else { + return ""; + } +} + +function getContentClass(keyname: string | number): string { + if (typeof keyname === "string") { + return "content"; + } else { + return ""; + } +} + +function isPlainObject(val: any): boolean { + let lastKey: string | undefined; + let lastOwnKey: string | undefined; + for (const key in val) { + if (val.hasOwnProperty(key)) { + lastOwnKey = key; + } + } + for (const key in val) { + lastKey = key; + } + return lastOwnKey === lastKey; +} + +function isLeafValue(val: any): boolean { + return ( + _.isNumber(val) || + _.isString(val) || + _.isBoolean(val) || + _.isDate(val) || + _.isNull(val) || + _.isUndefined(val) || + _.isNaN(val) || + _.isFunction(val) || + !isPlainObject(val) + ); +} + +function isLeafObject(obj: any): boolean { + if (!_.isObject(obj)) { + return false; + } + for (const key in obj) { + const val = obj[key]; + if (!isLeafValue(val)) { + return false; + } + } + return true; +} + +function isTable(arr: any[]): boolean { + if (!_.isArray(arr)) { + return false; + } + if (arr.length === 0 || !_.isObject(arr[0])) { + return false; + } else { + let nonCompliant = _.detect(arr, (row) => !isLeafObject(row)); + if (nonCompliant) { + return false; + } else { + const cols = _.keys(arr[0]); + nonCompliant = _.detect(arr, (row: object) => !_.isEqual(cols, _.keys(row))); + if (nonCompliant) { + return false; + } else { + return true; + } + } + } +} + +function drawTable(arr: any[]): string { + function drawRow(headers: string[], rowObj: any): string { + return ( + "" + + headers.map((header) => rowObj[header]).join("") + + "" + ); + } + const cols = _.keys(arr[0]); + const content = arr.map((rowObj) => drawRow(cols, rowObj)); + const headingHtml = "" + cols.join("") + ""; + const contentHtml = "" + content.join("") + ""; + return "" + headingHtml + contentHtml + "
"; +} + +function _render( + name: string, + data: any, + options: any, + level: number, + altrow: number +): string { + const contentClass = getContentClass(name); + if (_.isArray(data)) { + const title = makeLabelDiv(options, level, `${name} (${data.length})`, "array"); + let subs: string; + if (isTable(data)) { + subs = drawTable(data); + } else { + subs = "
" + data + .map((val: any, idx: number) => _render(idx.toString(), val, options, level + 1, idx % 2)) + .join("
") + "
"; + } + return `
+ ${title} +
${subs}
+
`; + } else if (isLeafValue(data)) { + const title = makeLabelDiv(options, level, name); + if (_.isFunction(data)) { + return `${title}  -function() can't _render-`; + } else if (!isPlainObject(data)) { + if (_.isFunction(data.toString)) { + return `${title}  ${data.toString()}`; + } else { + return `${title}  -instance object, can't render-`; + } + } else { + return `${title}  ${data}`; + } + } else { + const title = makeLabelDiv(options, level, name, "object"); + let count = 0; + const subs = "
" + Object.entries(data) + .map(([key, val]) => _render(key, val, options, level + 1, count++ % 2)) + .join("
") + "
"; + const inner = `
+ ${title} +
${subs}
+
`; + return `${level === 0 ? '
' : ''} + ${inner} + ${level === 0 ? '
' : ''}`; + } +} + +export function render(json: any, options: any): string { + // return `${head}${_render('', json, options, 0, 0)}`; + return `${_render('', json, options, 0, 0)}`; +} + +const head = ` +`; diff --git a/test/performance/utils/number.ts b/test/performance/utils/number.ts new file mode 100644 index 0000000..f1c3dd4 --- /dev/null +++ b/test/performance/utils/number.ts @@ -0,0 +1,10 @@ +export function numberFix(number: number, decimalPlaces: number): string { + + if (number > 10000 || number < 0.001) { + const [mantissa, exponent] = number.toExponential().split('e'); + const formattedMantissa = Number(mantissa).toFixed(decimalPlaces); + return `${formattedMantissa}e${exponent}`; + } else { + return number.toFixed(decimalPlaces); + } +} \ No newline at end of file diff --git a/test/unit/data-structures/binary-tree/rb-tree.test.ts b/test/unit/data-structures/binary-tree/rb-tree.test.ts index 513b9fd..238e6a5 100644 --- a/test/unit/data-structures/binary-tree/rb-tree.test.ts +++ b/test/unit/data-structures/binary-tree/rb-tree.test.ts @@ -427,7 +427,7 @@ describe('RedBlackTree', () => { }); it('should fix the tree after insertion and deletion', () => { - for (let i = 0; i < 1000; i++) { + for (let i = 0; i < 100; i++) { tree.insert(getRandomInt(-100, 1000)); tree.delete(getRandomInt(-100, 1000)); }