[benchmark] pretty html report for benchmark test

This commit is contained in:
Revone 2023-11-02 11:38:29 +08:00
parent 9f2f0fca29
commit ca013f019f
9 changed files with 445 additions and 28 deletions

View file

@ -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

20
package-lock.json generated
View file

@ -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",

View file

@ -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",

View file

@ -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<number>();
for (let i = 0; i < 1000000; i++) {
queue.push(i);
}
})
// .add('shift', () => {
// const queue = new Queue<number>();
// for (let i = 0; i < 10000; i++) {
// queue.push(i);
// queue.shift();
// }
// })
.add('push & shift 1000000', () => {
const queue = new Queue<number>();
for (let i = 0; i < 1000000; i++) {
queue.push(i);
queue.shift();
}
})

View file

@ -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 = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
.content table {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
font-size: 16px;
}
.content table th,
.content table td {
padding: 8px 12px;
text-align: left;
border: 1px solid #ddd;
}
.content table th {
background-color: #f2f2f2;
font-weight: bold;
}
.content table tr:nth-child(odd) {
background-color: #ffffff;
}
</style>
</head>
<body>
<div id="j2h">`
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 += `</div>
</body>
</html>`;
fs.writeFileSync(htmlFilePath, html);
console.log('Performance test report file generated')
}
})

View file

@ -0,0 +1,2 @@
export * from './json2html';
export * from './number';

View file

@ -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 `<div class='index'><span class='j2h-label'>${keyname}&nbsp;</span></div>`;
} else if (typeof keyname === "string") {
if (datatype === "array") {
return `<div class='collapsible level${level}' ${toggleJS(
options
)}><span class='j2h-label'>${keyname}</span></div>`;
} else if (datatype === "object") {
return `<div class='attribute collapsible level${level}' ${toggleJS(
options
)}><span class='j2h-label'>${keyname}:</span></div>`;
} else {
return `<div class='leaf level${level}'><span class='j2h-label'>${keyname}:</span></div>`;
}
} 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 (
"<td>" +
headers.map((header) => rowObj[header]).join("</td><td>") +
"</td>"
);
}
const cols = _.keys(arr[0]);
const content = arr.map((rowObj) => drawRow(cols, rowObj));
const headingHtml = "<tr><th>" + cols.join("</th><th>") + "</th></tr>";
const contentHtml = "<tr>" + content.join("</tr><tr>") + "</tr>";
return "<table>" + headingHtml + contentHtml + "</table>";
}
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 = "<div class='altRows'>" + data
.map((val: any, idx: number) => _render(idx.toString(), val, options, level + 1, idx % 2))
.join("</div><div class='altRows'>") + "</div>";
}
return `<div class="j2h-collapse clearfix ${altrow}">
${title}
<div class="${contentClass}">${subs}</div>
</div>`;
} else if (isLeafValue(data)) {
const title = makeLabelDiv(options, level, name);
if (_.isFunction(data)) {
return `${title}<span class='j2h-value'>&nbsp;&nbsp;-function() can't _render-</span>`;
} else if (!isPlainObject(data)) {
if (_.isFunction(data.toString)) {
return `${title}<span class='j2h-value'>&nbsp;&nbsp;${data.toString()}</span>`;
} else {
return `${title}<span class='j2h-value'>&nbsp;&nbsp;-instance object, can't render-</span>`;
}
} else {
return `${title}<span class='j2h-value'>&nbsp;&nbsp;${data}</span>`;
}
} else {
const title = makeLabelDiv(options, level, name, "object");
let count = 0;
const subs = "<div>" + Object.entries(data)
.map(([key, val]) => _render(key, val, options, level + 1, count++ % 2))
.join("</div><div>") + "</div>";
const inner = `<div class="j2h-expand clearfix ${altrow}">
${title}
<div class="${contentClass}">${subs}</div>
</div>`;
return `${level === 0 ? '<div id=\'j2h\'>' : ''}
${inner}
${level === 0 ? '</div>' : ''}`;
}
}
export function render(json: any, options: any): string {
// return `${head}${_render('', json, options, 0, 0)}`;
return `${_render('', json, options, 0, 0)}`;
}
const head = `<style>
#j2h table {
border-collapse: collapse;
}
#j2h th {
color: #888;
}
#j2h table,th, td {
border: 1px solid #DDD;
padding: 10px 5px;
}
#j2h th, td {
text-align: center;
}
#j2h .content {
padding-left: 30px;
font-family: Arial;
}
#j2h .index {
font-size: 100%;
color: #999;
float: left;
}
#j2h .clearfix:after {
content: ".";
display: block;
height: 0;
clear: both;
visibility: hidden;
}
#j2h .j2h-label {
font-family: Helvetica Neue;
color: #333;
}
#j2h .j2h-value {
font-family: Arial;
color: #777;
}
#j2h .collapsible > .j2h-label:hover {
text-decoration: underline;
}
#j2h .collapsible > .j2h-label {
color: #15C;
}
#j2h .j2h-collapse > div.content {
display: none;
}
#j2h .j2h-collapse > .j2h-label {
font-weight: bold;
}
#j2h .j2h-expand > div > .j2h-label, #j2h .j2h-collapse > div > .j2h-label {
background-repeat: no-repeat;
background-position: left;
padding-left: 25px;
margin: 5px 0px 5px 15px;
display: inline-block;
}
#j2h .j2h-expand > div > .j2h-label {
width: 30px;
height: 30px;
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"><polygon points="50,10 10,90 90,90" fill="blue" /></svg>');
background-size: cover;
background-position: center;
}
#j2h .j2h-collapse > div > .j2h-label {
width: 30px;
height: 30px;
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"><polygon points="50,10 10,90 90,90" fill="blue" /></svg>');
background-size: cover;
background-position: center;
}
#j2h .j2h-collapse > span.collapsible:before {
border-radius: 2px;
border-color: #A44;
border-style: solid;
border-width: 1px;
color: #A44;
content: '+';
display: inline-block;
line-height: 7px;
margin: 0 2px;
overflow: hidden;
padding: 1px;
font-size: 11px;
}
#j2h .j2h-expand > span.collapsible:before {
border: none;
color: #A44;
content: '-';
display: inline-block;
line-height: 7px;
margin: 4px;
overflow: hidden;
padding: 1px;
font-size: 11px;
}
#j2h.level0 {
font-size: 25px;
}
#j2h .level1 {
font-size: 22px;
}
#j2h .leaf {
color: #666;
display: inline;
}
#j2h .altRows:nth-child(odd) { background-color:#ddd; }
#j2h .altRows:nth-child(even) { background-color:#fff; }
#j2h tr:nth-child(odd) { background-color:#eee; }
#j2h tr:nth-child(even) { background-color:#fff; }
</style>
<script type="text/javascript">
j2h = {
toggleVisibility: function(el, name) {
j2h.toggleClass(el.parentElement,'j2h-collapse j2h-expand');
},
classRe: function(name) {
return new RegExp('(?:^|\\s)'+name+'(?!\\S)');
},
addClass: function(el, name) {
el.className += " "+name;
},
removeClass: function(el, name) {
var re = j2h.classRe(name);
el.className = el.className.replace(j2h.classRe(name) , '' )
},
hasClass: function(el, name) {
var re = j2h.classRe(name);
return j2h.classRe(name).exec(el.className);
},
toggleClass: function(el, name) {
var names = name.split(/\s+/);
for (n in names) {
if (j2h.hasClass(el, names[n])) {
j2h.removeClass(el, names[n]);
} else {
j2h.addClass(el, names[n]);
}
}
}
};
</script>`;

View file

@ -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);
}
}

View file

@ -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));
}