├── .babelrc ├── .dockerignore ├── .eslintignore ├── .eslintrc ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── docs ├── 448c34a56d699c29117adc64c43affeb.woff2 ├── 89889688147bd7575d6327160d64e760.svg ├── CNAME ├── bundle.js ├── e18bbf611f2a2e43afc071aa2f4e1512.ttf ├── f4769f9bdb7466be65088239c12046d1.eot ├── fa2772327f55d8198301fdb8bcfc8158.woff ├── favicons │ └── favicon.ico ├── index.html ├── provided.js └── settings.js ├── package-lock.json ├── package.json ├── src ├── css │ ├── common.css │ ├── sidenavi.css │ └── tooltip.css ├── favicons │ └── favicon.ico ├── index.html ├── javascript │ ├── components │ │ ├── App.jsx │ │ ├── AppLogo.jsx │ │ ├── ChartHeader.jsx │ │ ├── DetailScreen.jsx │ │ ├── DetailSideBar.jsx │ │ ├── DetailView.jsx │ │ ├── DoingWorkSpinner.jsx │ │ ├── Icons.jsx │ │ ├── LoadFromGistsDialog.jsx │ │ ├── LoadFromUrlsDialog.jsx │ │ ├── RunScreen.jsx │ │ ├── RunSelectionBar.jsx │ │ ├── RunSideBar.jsx │ │ ├── SummaryScreen.jsx │ │ ├── TocElement.jsx │ │ ├── TocLink.jsx │ │ ├── TocList.jsx │ │ ├── UploadMainView.jsx │ │ ├── UploadScreen.jsx │ │ ├── UploadSideBar.jsx │ │ ├── commons.jsx │ │ ├── global │ │ │ ├── CustomTopBar.jsx │ │ │ ├── DefaultTopBar.jsx │ │ │ ├── Footer.jsx │ │ │ └── TopBar.jsx │ │ ├── lib │ │ │ ├── BadgeWithTooltip.jsx │ │ │ ├── SplitPane.jsx │ │ │ └── Tooltipped.jsx │ │ ├── multi │ │ │ ├── LineChartView.jsx │ │ │ ├── MultiRunBundle.jsx │ │ │ ├── MultiRunChartTooltip.jsx │ │ │ └── MultiRunView.jsx │ │ ├── single │ │ │ ├── BarChartView.jsx │ │ │ ├── BarDataSet.js │ │ │ ├── BarLabel.jsx │ │ │ ├── BarTooltipLabel.jsx │ │ │ ├── SingleRunBundle.jsx │ │ │ ├── SingleRunChartTooltip.jsx │ │ │ └── SingleRunView.jsx │ │ ├── summary │ │ │ ├── SummaryChangeChart.jsx │ │ │ ├── SummaryChangeLevelChart.jsx │ │ │ ├── SummaryHeader.jsx │ │ │ ├── SummaryHistogramChart.jsx │ │ │ ├── SummaryTable.jsx │ │ │ └── SummaryView.jsx │ │ └── two │ │ │ ├── DiffBarChartView.jsx │ │ │ ├── DiffBarDataSet.js │ │ │ ├── DiffLabel.jsx │ │ │ ├── TwoRunBundle.jsx │ │ │ ├── TwoRunsChartTooltip.jsx │ │ │ ├── TwoRunsHistogramChart.jsx │ │ │ └── TwoRunsView.jsx │ ├── entry.jsx │ ├── exampleBenchmark1.js │ ├── exampleBenchmark2.js │ ├── exampleBenchmark3.js │ ├── functions │ │ ├── charts.js │ │ ├── colors.js │ │ ├── parse.js │ │ └── util.js │ ├── models │ │ ├── BenchmarkBundle.js │ │ ├── BenchmarkMethod.js │ │ ├── BenchmarkRun.js │ │ ├── BenchmarkSelection.js │ │ ├── Examples.js │ │ ├── MetricExtractor.js │ │ ├── MetricType.js │ │ └── extractor │ │ │ ├── PrimaryMetricExtractor.js │ │ │ └── SecondaryMetricExtractor.js │ └── store │ │ ├── processParameters.js │ │ └── store.js ├── provided.js └── settings.js ├── test ├── functions │ ├── parse.spec.js │ └── util.spec.js └── models │ └── BenchmarkBundle.spec.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "testing": { 4 | "presets": ["es2015"] 5 | } 6 | }, 7 | "plugins": ["transform-runtime"], 8 | "presets": [ ["es2015", { "loose": false, "modules": false}], "react", "stage-0", "stage-2"] 9 | } 10 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Ignore everything 2 | * 3 | 4 | # Allow files and directories 5 | !src/* 6 | !webpack.config.js 7 | !package.json 8 | !package-lock.json 9 | !.babelrc 10 | 11 | # Ignore unnecessary files inside allowed directories 12 | # This should go after the allowed directories 13 | **/*~ 14 | **/*.log 15 | **/.DS_Store 16 | **/Thumbs.db 17 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build/ 2 | docs/ -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | // Extend existing configuration 3 | // from ESlint and eslint-plugin-react defaults. 4 | "extends": [ 5 | "eslint:recommended", "plugin:react/recommended" 6 | ], 7 | "parser": "babel-eslint", 8 | // Enable ES6 support. If you want to use custom Babel 9 | // features, you will need to enable a custom parser 10 | // as described in a section below. 11 | "parserOptions": { 12 | "ecmaVersion": 6, 13 | "sourceType": "module", 14 | "ecmaFeatures": { 15 | "jsx": true 16 | } 17 | }, 18 | "env": { 19 | "browser": true, 20 | "node": true, 21 | "es6": true, 22 | "jasmine": true 23 | }, 24 | // Enable custom plugin known as eslint-plugin-react 25 | "plugins": [ 26 | "react", 27 | "jasmine" 28 | ], 29 | "rules": { 30 | // Disable `no-console` rule 31 | "no-console": 1, 32 | // Give a warning if identifiers contain underscores 33 | "no-underscore-dangle": 1 34 | } 35 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .eslintcache 2 | build/ 3 | node_modules/ 4 | .vscode/settings.json -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine as build-deps 2 | 3 | WORKDIR /usr/src/app 4 | ENV NODE_OPTIONS=--openssl-legacy-provider 5 | COPY package.json package-lock.json ./ 6 | RUN npm install 7 | COPY . ./ 8 | RUN npm run release-build 9 | 10 | FROM nginx:alpine 11 | 12 | COPY --from=build-deps /usr/src/app/build /usr/share/nginx/html 13 | EXPOSE 80 14 | CMD ["nginx", "-g", "daemon off;"] 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JMH Visualizer 2 | 3 | Visually explore your [JMH](http://openjdk.java.net/projects/code-tools/jmh/) Benchmarks! Online version at http://jmh.morethan.io! 4 | 5 | Comes with 2 companion projects: 6 | - [Gradle plugin](https://github.com/jzillmann/gradle-jmh-report) 7 | - [Jenkins plugin](https://github.com/jenkinsci/jmh-report-plugin) 8 | 9 | 10 | ## Features 11 | 12 | - Serverless architecture - All happens locally in your browser 13 | - Visualize the benchmarks of a [single run](http://jmh.morethan.io?example=single) (one JSON file) grouped by benchmark class 14 | - Vertical bar-chart with score and score error 15 | - Link to the original JSON 16 | - Show individual runs as tooltip 17 | - Compare the benchmarks of [2 runs](http://jmh.morethan.io?example=two) (two JSON files) grouped by benchmark class 18 | - Summary of noteable changes 19 | - Vertical bar-chart from -100% to 100% 20 | - Link to the original JSON 21 | - Show score and error of both runs in tooltip 22 | - Compare the benchmarks of [multiple runs](http://jmh.morethan.io?example=multi) (n JSON files) grouped by benchmark class 23 | - Summary of noteable changes 24 | - Line chart 25 | - Show score and error on hover 26 | - Visualize secondary metrics like '·gc.alloc.rate' 27 | - Focus on subselection of charts with synced axis scales 28 | - Load benchmarks from external url or gists 29 | 30 | 31 | ## Major Changes 32 | 33 | - **Mar 2021** - 0.9.3 Support [Gists containing multiple files](https://github.com/jzillmann/jmh-visualizer/issues/33) 34 | - **Aug 2018** - 0.9 Couple of [user requested features](https://github.com/jzillmann/jmh-visualizer/milestone/6?closed=1) 35 | - **Jul 2018** - 0.8 Revamp Summary Page + Chart transitions 36 | - **Jan 2018** - 0.7.3 External URL/Gist support 37 | - **Okt 2017** - 0.7 Multi-Run support 38 | - **Aug 2017** - 0.6 Layout change & Summary support 39 | - **Jul 2017** - 0.5 Focussing of benchmarks 40 | - **May 2017** - 0.4 Secondary Metrics support 41 | - **Apr 2017** - 0.3 Error Bars & Params support 42 | - **Nov 2016** - 0.2: Add 2 run compare view 43 | - **Okt 2016** - 0.1: Initial Release 44 | 45 | 46 | ## Tips & Tricks 47 | 48 | While this app will visualize any valid JMH JSON you throw at it, you can write your benchmarks in a way that make the visualization much more enjoyable... 49 | 50 | - Put those benchmarks in a single class which you most likely wanne compare to each other 51 | - On the other hand, don't put to much stuff in a single class/chart (since readability will suffer) 52 | - Don't mix incompatible benchmark styles into one class (like mixing average and single shot is ok, but mixing avarage and throughput doesn't make much sense) 53 | - Sensibly design your package structure, your class name and you methods names, those are reflected in the auto-generated charts 54 | - Keep method names short but meaningful 55 | - The method names reflect initial sort, so if you have benchmarks called 'with1Threads, with10Threads' naming them 'with01Thread, with10Thread' will display nicer 56 | 57 | 58 | ## Parameter reference 59 | 60 | | Name | Values | What you can do with it? | Example | 61 | | ------------- | ------------- | ------------- | ------------- | 62 | | source | $url | Load a single benchmark result from the provided URL | http://jmh.morethan.io?source=https://gist.githubusercontent.com/jzillmann/7d23b2382911cc434754a23773b06598/raw/1bcad4bb64624d8a2be15114a4eee4c406c3ae95/string-concatenation_jdk7.json | 63 | | sources | $url1,$url2,.. | Load multiple benchmark results from the provided URLs | http://jmh.morethan.io?sources=https://gist.githubusercontent.com/jzillmann/7d23b2382911cc434754a23773b06598/raw/1bcad4bb64624d8a2be15114a4eee4c406c3ae95/string-concatenation_jdk7.json,https://gist.githubusercontent.com/jzillmann/866d39d43b264f507a67368f2313baca/raw/d0ae1502e8c493e6814c83f2df345fecb763c078/string-concatenation_jdk8.json | 64 | | gist | $gistId | Load a single benchmark result from the provided gist | http://jmh.morethan.io?gist=7d23b2382911cc434754a23773b06598 | 65 | | gist (multi-file)| $gistId | Load multiple benchmark results from the provided gist | http://jmh.morethan.io?gist=4c9e282fff30b5fa455ae2496acf4e05 | 66 | | gists | $gistId1,$gistId2,... | Load multiple benchmark results from the provided gists | http://jmh.morethan.io?gists=7d23b2382911cc434754a23773b06598,866d39d43b264f507a67368f2313baca | 67 | | topBar | oneOf['default', 'off, 'my custom headline'] | Control the header | [Off](http://jmh.morethan.io?gist=7d23b2382911cc434754a23773b06598&topBar=off), [Custom Headline](http://jmh.morethan.io/?gist=7d23b2382911cc434754a23773b06598&topBar=Custom%20Headline) | 68 | 69 | 70 | ## Contribute 71 | 72 | Use the [issue tracker](https://github.com/jzillmann/jmh-visualizer/issues) and/or open [pull requests](https://github.com/jzillmann/jmh-visualizer/pulls)! 73 | 74 | #### Useful Commands 75 | 76 | - ```npm install``` Download all necessary npm packages 77 | - ```npm run lint``` Lint the javascript files 78 | - ```npm run test``` Run tests 79 | - ```npm run check``` Run Lint & Test 80 | - ```npm run watch``` Continuously build the project 81 | - ```open build/index.html``` Open the build project in your default browser 82 | - ```npm run release``` Build production version 83 | - ```npm run deploy``` Build production version & move it to the github pages fodler 84 | 85 | #### Docker Build & Run 86 | 87 | ```bash 88 | docker build . -t jmh-visualizer 89 | docker run --rm -d -p 80:80 --name jmh-visualizer jmh-visualizer 90 | ``` 91 | 92 | Now you can access the UI on ```http://localhost```. 93 | 94 | #### Realease 95 | - Increase version in package.json 96 | - Update README.md in case of major releases 97 | - ```npm run deploy``` 98 | - commit & push 99 | - tag with 100 | - _git tag -a $releaseVersion -m "$releaseVersion release"_ 101 | - _git push --tags_ 102 | 103 | 104 | ## Credits 105 | 106 | http://recharts.org/ - The chart ground work 107 | 108 | http://www.favicon.cc/ - Created the favicon with 109 | 110 | Babel, webpack, react,... and many more for an enjoyable webstack! 111 | 112 | 113 | ## Donating 114 | 115 | [![Support via PayPal](https://cdn.rawgit.com/twolfson/paypal-github-button/1.0.0/dist/button.svg)](https://www.paypal.me/JohannesZillmann/5.4) 116 | 117 | -------------------------------------------------------------------------------- /docs/448c34a56d699c29117adc64c43affeb.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jzillmann/jmh-visualizer/50289e2aedf21044d4b9823a1960d23944fd4568/docs/448c34a56d699c29117adc64c43affeb.woff2 -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | jmh.morethan.io -------------------------------------------------------------------------------- /docs/e18bbf611f2a2e43afc071aa2f4e1512.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jzillmann/jmh-visualizer/50289e2aedf21044d4b9823a1960d23944fd4568/docs/e18bbf611f2a2e43afc071aa2f4e1512.ttf -------------------------------------------------------------------------------- /docs/f4769f9bdb7466be65088239c12046d1.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jzillmann/jmh-visualizer/50289e2aedf21044d4b9823a1960d23944fd4568/docs/f4769f9bdb7466be65088239c12046d1.eot -------------------------------------------------------------------------------- /docs/fa2772327f55d8198301fdb8bcfc8158.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jzillmann/jmh-visualizer/50289e2aedf21044d4b9823a1960d23944fd4568/docs/fa2772327f55d8198301fdb8bcfc8158.woff -------------------------------------------------------------------------------- /docs/favicons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jzillmann/jmh-visualizer/50289e2aedf21044d4b9823a1960d23944fd4568/docs/favicons/favicon.ico -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JMH Visualizer 6 | 7 | 8 | 9 | 10 | 11 | 12 | 21 | 22 | 23 |
24 | 25 | -------------------------------------------------------------------------------- /docs/provided.js: -------------------------------------------------------------------------------- 1 | // provided.js can be overriden in order to have a predefined-report generated. 2 | 3 | 4 | var providedBenchmarks = []; // eslint-disable-line no-unused-vars 5 | // var providedBenchmarks = ['result']; // eslint-disable-line no-unused-vars 6 | 7 | var providedBenchmarkStore = { // eslint-disable-line no-unused-vars 8 | result: [ 9 | { 10 | "benchmark": "io.morethan.javabenchmarks.datastructure.ListCreationBenchmark.arrayList", 11 | "mode": "thrpt", 12 | "threads": 1, 13 | "forks": 2, 14 | "warmupIterations": 3, 15 | "warmupTime": "1 s", 16 | "warmupBatchSize": 1, 17 | "measurementIterations": 5, 18 | "measurementTime": "1 s", 19 | "measurementBatchSize": 1, 20 | "primaryMetric": { 21 | "score": 3467631.2300505415, 22 | "scoreError": 98563.33258873208, 23 | "scoreConfidence": [ 24 | 3369067.897461809, 25 | 3566194.5626392737 26 | ], 27 | "scorePercentiles": { 28 | "0.0": 3333994.173460993, 29 | "50.0": 3474993.5017186473, 30 | "90.0": 3532339.4656069754, 31 | "95.0": 3532453.3325443356, 32 | "99.0": 3532453.3325443356, 33 | "99.9": 3532453.3325443356, 34 | "99.99": 3532453.3325443356, 35 | "99.999": 3532453.3325443356, 36 | "99.9999": 3532453.3325443356, 37 | "100.0": 3532453.3325443356 38 | }, 39 | "scoreUnit": "ops/s", 40 | "rawData": [ 41 | [ 42 | 3524193.1540743182, 43 | 3531314.663170731, 44 | 3532453.3325443356, 45 | 3520395.64538141, 46 | 3474431.2865468427 47 | ], 48 | [ 49 | 3440045.3067861893, 50 | 3333994.173460993, 51 | 3445535.4557112623, 52 | 3475555.716890452, 53 | 3398393.5659388755 54 | ] 55 | ] 56 | }, 57 | "secondaryMetrics": { 58 | } 59 | }] 60 | }; -------------------------------------------------------------------------------- /docs/settings.js: -------------------------------------------------------------------------------- 1 | var defaultSettings = { // eslint-disable-line no-unused-vars 2 | topBar: 'default' //possible values ['default', 'off', 'my custom headline'] 3 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jmh-visualizer", 3 | "version": "0.9.6", 4 | "description": "Visualize the results of your JMH benchmarks execution", 5 | "main": "index.jsx", 6 | "scripts": { 7 | "watch": "webpack -d --watch", 8 | "build": "webpack --mode development", 9 | "lint": "eslint . --ext .js --ext .jsx --cache", 10 | "test": "NODE_ENV=testing NODE_PATH=./src/javascript mocha --compilers js:babel-core/register test --recursive", 11 | "check": "npm run lint && npm run test", 12 | "release-build": "rm -rf build/* && NODE_ENV=production webpack --mode production", 13 | "release": "npm run check && npm run release-build", 14 | "deploy": "npm run release && cp -r build/* docs/", 15 | "providedZip": "npm run release && cd build && zip -r ../jmh-visualizer.zip ./*" 16 | }, 17 | "keywords": [ 18 | "JMH", 19 | "visualize", 20 | "visualization", 21 | "benchmark", 22 | "report" 23 | ], 24 | "author": "Johannes Zillmann", 25 | "license": "AGPL-3.0", 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/jzillmann/jmh-visualizer" 29 | }, 30 | "dependencies": { 31 | "@appigram/react-rangeslider": "^2.2.9", 32 | "bootstrap": "^3.4.1", 33 | "core-js": "^2.6.9", 34 | "d3-scale-chromatic": "^1.3.3", 35 | "history": "^4.7.2", 36 | "prop-types": "^15.6.2", 37 | "react": "^16.8.6", 38 | "react-bootstrap": "^0.32.4", 39 | "react-dom": "^16.8.6", 40 | "react-dropzone": "^3.13.4", 41 | "react-icons": "^2.2.7", 42 | "react-overlays": "^0.8.3", 43 | "react-scroll": "^1.8.1", 44 | "react-spinkit": "^3.0.0", 45 | "react-toggle": "^4.0.2", 46 | "react-tooltip": "^4.2.15", 47 | "react-waterfall": "^4.0.0", 48 | "recharts": "^1.3.1" 49 | }, 50 | "devDependencies": { 51 | "babel-core": "^6.26.3", 52 | "babel-eslint": "^8.2.6", 53 | "babel-loader": "^7.1.5", 54 | "babel-plugin-transform-runtime": "^6.15.0", 55 | "babel-preset-es2015": "^6.16.0", 56 | "babel-preset-react": "^6.16.0", 57 | "babel-preset-stage-0": "^6.16.0", 58 | "babel-preset-stage-2": "^6.24.1", 59 | "chai": "^3.5.0", 60 | "copy-webpack-plugin": "^4.5.2", 61 | "css-loader": "^0.25.0", 62 | "esformatter-jsx": "^7.0.1", 63 | "eslint": "^5.16.0", 64 | "eslint-plugin-jasmine": "^2.10.1", 65 | "eslint-plugin-react": "^7.14.2", 66 | "file-loader": "^1.1.11", 67 | "html-webpack-plugin": "^3.2.0", 68 | "mocha": "^5.2.0", 69 | "style-loader": "^0.21.0", 70 | "url-loader": "^1.1.1", 71 | "webpack": "^4.35.3", 72 | "webpack-cli": "^3.3.6" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/css/common.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | height: 100%; 3 | width: 100%; 4 | margin: 0; 5 | padding: 0; 6 | } 7 | 8 | a { 9 | cursor: pointer; 10 | } 11 | 12 | .clickable { 13 | cursor: pointer; 14 | } 15 | .clickable:hover { 16 | color: #b7a533; 17 | } 18 | 19 | .superscript { position: relative; top: -0.5em; font-size: 80%; } 20 | 21 | .rangeslider__label-list .rangeslider__label { 22 | position: absolute; 23 | font-size: 14px; 24 | cursor: pointer; 25 | display: inline-block; 26 | top: 23px; 27 | } 28 | 29 | .btn:focus { 30 | outline: none; 31 | } 32 | 33 | .btn.btn-primary:active { 34 | background-color: #337ab7 !important; 35 | } -------------------------------------------------------------------------------- /src/css/sidenavi.css: -------------------------------------------------------------------------------- 1 | .bs-docs-sidebar .nav>li>div>a { 2 | color: inherit; 3 | } 4 | 5 | /* sidebar */ 6 | .bs-docs-sidebar { 7 | padding-left: 5px; 8 | margin-top: 0px; 9 | margin-bottom: 20px; 10 | } 11 | 12 | /* all links */ 13 | .bs-docs-sidebar .nav>li>div { 14 | color: #999; 15 | border-left: 2px solid transparent; 16 | padding: 4px 10px; 17 | font-size: 13px; 18 | font-weight: 400; 19 | white-space: nowrap; 20 | } 21 | 22 | /* nested links */ 23 | .bs-docs-sidebar .nav .nav>li>div { 24 | padding-top: 1px; 25 | padding-bottom: 1px; 26 | padding-left: 20px; 27 | font-size: 12px; 28 | white-space: nowrap; 29 | } 30 | 31 | /* active & hover links */ 32 | .bs-docs-sidebar .nav>.active>div, 33 | .bs-docs-sidebar .nav>li>div:hover, 34 | .bs-docs-sidebar .nav>li>div:focus { 35 | color: #563d7c; 36 | text-decoration: none; 37 | background-color: transparent; 38 | border-left-color: #563d7c; 39 | } 40 | 41 | /* all active links */ 42 | .bs-docs-sidebar .nav>.active>div, 43 | .bs-docs-sidebar .nav>.active:hover>div, 44 | .bs-docs-sidebar .nav>.active:focus>div { 45 | font-weight: 700; 46 | } 47 | 48 | /* nested active links */ 49 | .bs-docs-sidebar .nav .nav>.active>div, 50 | .bs-docs-sidebar .nav .nav>.active:hover>div, 51 | .bs-docs-sidebar .nav .nav>.active:focus>div { 52 | font-weight: 500; 53 | text-decoration: none; 54 | } 55 | 56 | /* hide inactive nested list */ 57 | .bs-docs-sidebar .nav ul.nav { 58 | display: none; 59 | } 60 | /* show active nested list */ 61 | .bs-docs-sidebar .nav >.active>ul.nav { 62 | display: block; 63 | } 64 | 65 | /* Icons befor element */ 66 | .bs-docs-sidebar .nav .nav>li>div>span { 67 | display:none; 68 | } 69 | 70 | .bs-docs-sidebar .nav .nav>li>div:hover>span, 71 | .bs-docs-sidebar .nav .nav>li.active>div:focus>span { 72 | display:inline; 73 | } 74 | 75 | .bs-docs-sidebar .nav .nav>li>div>span:hover { 76 | color: green; 77 | } 78 | 79 | .bs-docs-sidebar .nav .nav>li>div>span.focused:hover { 80 | color: red; 81 | cursor: pointer; 82 | } 83 | 84 | .bs-docs-sidebar .nav .nav>li>div>span.focused { 85 | display:inline; 86 | color: green; 87 | } 88 | -------------------------------------------------------------------------------- /src/css/tooltip.css: -------------------------------------------------------------------------------- 1 | [data-tooltip] { 2 | display: inline-block; 3 | position: relative; 4 | margin-bottom: -6px; 5 | } 6 | /* Tooltip styling */ 7 | [data-tooltip]:before { 8 | content: attr(data-tooltip); 9 | display: none; 10 | position: absolute; 11 | background: #159957; 12 | color: #fff; 13 | padding: 4px 8px; 14 | font-size: 12px; 15 | min-width: 180px; 16 | text-align: center; 17 | border-radius: 4px; 18 | } 19 | /* Dynamic horizontal centering */ 20 | [data-tooltip-position="top"]:before, 21 | [data-tooltip-position="bottom"]:before { 22 | left: 50%; 23 | -ms-transform: translateX(-50%); 24 | -moz-transform: translateX(-50%); 25 | -webkit-transform: translateX(-50%); 26 | transform: translateX(-50%); 27 | } 28 | /* Dynamic vertical centering */ 29 | [data-tooltip-position="right"]:before, 30 | [data-tooltip-position="left"]:before { 31 | top: 50%; 32 | -ms-transform: translateY(-50%); 33 | -moz-transform: translateY(-50%); 34 | -webkit-transform: translateY(-50%); 35 | transform: translateY(-50%); 36 | } 37 | [data-tooltip-position="top"]:before { 38 | bottom: 100%; 39 | margin-bottom: 6px; 40 | } 41 | [data-tooltip-position="right"]:before { 42 | left: 100%; 43 | margin-left: 6px; 44 | } 45 | [data-tooltip-position="bottom"]:before { 46 | top: 100%; 47 | margin-top: 6px; 48 | } 49 | [data-tooltip-position="left"]:before { 50 | right: 100%; 51 | margin-right: 6px; 52 | } 53 | 54 | /* Tooltip arrow styling/placement */ 55 | [data-tooltip]:after { 56 | content: ''; 57 | display: none; 58 | position: absolute; 59 | width: 0; 60 | height: 0; 61 | border-color: transparent; 62 | border-style: solid; 63 | } 64 | /* Dynamic horizontal centering for the tooltip */ 65 | [data-tooltip-position="top"]:after, 66 | [data-tooltip-position="bottom"]:after { 67 | left: 50%; 68 | margin-left: -6px; 69 | } 70 | /* Dynamic vertical centering for the tooltip */ 71 | [data-tooltip-position="right"]:after, 72 | [data-tooltip-position="left"]:after { 73 | top: 50%; 74 | margin-top: -6px; 75 | } 76 | [data-tooltip-position="top"]:after { 77 | bottom: 100%; 78 | border-width: 6px 6px 0; 79 | border-top-color: #159957; 80 | } 81 | [data-tooltip-position="right"]:after { 82 | left: 100%; 83 | border-width: 6px 6px 6px 0; 84 | border-right-color: #159957; 85 | } 86 | [data-tooltip-position="bottom"]:after { 87 | top: 100%; 88 | border-width: 0 6px 6px; 89 | border-bottom-color: #159957; 90 | } 91 | [data-tooltip-position="left"]:after { 92 | right: 100%; 93 | border-width: 6px 0 6px 6px; 94 | border-left-color: #159957; 95 | } 96 | /* Show the tooltip when hovering */ 97 | [data-tooltip]:hover:before, 98 | [data-tooltip]:hover:after { 99 | display: block; 100 | z-index: 50; 101 | } -------------------------------------------------------------------------------- /src/favicons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jzillmann/jmh-visualizer/50289e2aedf21044d4b9823a1960d23944fd4568/src/favicons/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JMH Visualizer 6 | 7 | 8 | 9 | 10 | 11 | 12 | 21 | 22 | 23 |
24 | 25 | -------------------------------------------------------------------------------- /src/javascript/components/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { connect } from 'store/store.js' 4 | 5 | import TopBar from 'components/global/TopBar.jsx'; 6 | import Footer from 'components/global/Footer.jsx'; 7 | import DoingWorkSpinner from 'components/DoingWorkSpinner.jsx'; 8 | 9 | import RunSelectionBar from 'components/RunSelectionBar.jsx' 10 | import UploadScreen from 'components/UploadScreen.jsx'; 11 | import RunScreen from 'components/RunScreen.jsx'; 12 | import DetailScreen from 'components/DetailScreen.jsx'; 13 | import SummaryScreen from 'components/SummaryScreen.jsx'; 14 | 15 | /* eslint react/prop-types: 0 */ 16 | const App = ({ initialLoading, benchmarkRuns, runSelection, runView, detailedBenchmarkBundle }) => { 17 | if (initialLoading) { 18 | return (
); 19 | } 20 | let screen; 21 | if (benchmarkRuns.length == 0) { 22 | screen = 23 | } else { 24 | if (detailedBenchmarkBundle) { // Details View 25 | screen = 26 | } else { // Run View 27 | const selectedRuns = runSelection.filter(isSelected => isSelected); 28 | if (selectedRuns.length > 1 && runView === 'Summary') { 29 | screen = 30 | } else { 31 | screen = 32 | } 33 | } 34 | } 35 | 36 | return ( 37 |
38 | 39 |
40 | 41 | { screen } 42 |
43 |
44 |
45 | ); 46 | } 47 | 48 | export default connect(({ initialLoading, benchmarkRuns, runSelection, runView, detailedBenchmarkBundle }) => ({ 49 | initialLoading, 50 | benchmarkRuns, 51 | runSelection, 52 | runView, 53 | detailedBenchmarkBundle, 54 | }))(App) 55 | -------------------------------------------------------------------------------- /src/javascript/components/AppLogo.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import FaAlignLeft from 'react-icons/lib/fa/align-left' 5 | 6 | export default class MyLogo extends Component { 7 | 8 | static propTypes = { 9 | onClick: PropTypes.func, 10 | }; 11 | 12 | constructor(props, context) { 13 | super(props, context); 14 | this.handleClick = this.handleClick.bind(this); 15 | } 16 | 17 | handleClick(e) { 18 | e.preventDefault(); 19 | this.props.onClick(e); 20 | } 21 | 22 | 23 | 24 | render() { 25 | return ( 26 | 27 | JMH Visualizer 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/javascript/components/ChartHeader.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Tooltipped from 'components/lib/Tooltipped.jsx' 4 | import { getUniqueBenchmarkModes } from 'functions/parse.js' 5 | import { createMetricBadge } from 'components/commons.jsx'; 6 | 7 | //The header of a Single/Two/Multi-RunBundle 8 | /* eslint react/prop-types: 0 */ 9 | const ChartHeader = ({ benchmarkBundle, metricExtractor, children }) => { 10 | children = Array.isArray(children) ? children : [children]; 11 | const benchmarkModes = getUniqueBenchmarkModes(benchmarkBundle, metricExtractor); 12 | const benchmarkModeBadges = benchmarkModes.map(mode => createMetricBadge(mode)); 13 | 14 | return ( 15 |
16 | { children.map(child => { 17 | return ( 18 | 19 | { ' | ' } 20 | { child } 21 | 22 | ); 23 | }) } 24 |
25 | ); 26 | 27 | } 28 | 29 | export const ChartDetailHeader = ({ name, badges, children }) => { 30 | return ( 31 |
32 | { children } 33 |
34 | ); 35 | 36 | } 37 | 38 | 39 | function Header({ fullName, name, badges, children }) { 40 | return ( 41 |

42 | 43 | { name } 44 | 45 | { ' ' } 46 | { badges } 47 | { children } 48 |

49 | ); 50 | 51 | } 52 | 53 | export default ChartHeader 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/javascript/components/DetailScreen.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect, actions } from 'store/store.js' 3 | 4 | import SplitPane from 'components/lib/SplitPane.jsx' 5 | 6 | import { SortButton, ScaleButton } from 'components/Icons.jsx' 7 | import BenchmarkSelection from 'models/BenchmarkSelection.js'; 8 | import BenchmarkBundle from 'models/BenchmarkBundle.js'; 9 | import DetailSideBar from 'components/DetailSideBar.jsx'; 10 | import DetailView from 'components/DetailView.jsx' 11 | 12 | import BarChartView from 'components/single/BarChartView.jsx' 13 | import DiffBarChartView from 'components/two/DiffBarChartView.jsx' 14 | import LineChartView from 'components/multi/LineChartView.jsx' 15 | 16 | import { parseClassNameFromFullName } from 'functions/parse.js'; 17 | 18 | /* eslint react/prop-types: 0 */ 19 | const DetailScreen = ({ detailedBenchmarkBundle, benchmarkSelection, chartConfig }) => { 20 | 21 | const benchmarkBundles = benchmarkSelection.benchmarkBundles; 22 | const runNames = benchmarkSelection.runNames; 23 | 24 | const detailBundle = benchmarkBundles.find(bundle => bundle.key === detailedBenchmarkBundle) || new BenchmarkBundle({ 25 | key: detailedBenchmarkBundle, 26 | name: parseClassNameFromFullName(detailedBenchmarkBundle), 27 | methodNames: [], 28 | benchmarkMethods: [] 29 | }); 30 | const secondaryMetrics = Array.from(detailBundle.allBenchmarks().reduce((aggregate, benchmark) => { 31 | Object.keys(benchmark.secondaryMetrics).forEach(metricKey => aggregate.add(metricKey)); 32 | return aggregate; 33 | }, new Set())); 34 | 35 | 36 | let error, chartGeneratorFunction; 37 | if (runNames.length == 1) { 38 | if (detailBundle.methodNames.length == 0) { 39 | error = `No benchmark results for run ${runNames[0]}`; 40 | } else { 41 | chartGeneratorFunction = singleRunChartGenerator; 42 | } 43 | } else if (runNames.length == 2) { 44 | chartGeneratorFunction = twoRunsChartGenerator; 45 | } else { 46 | chartGeneratorFunction = multiRunChartGenerator; 47 | } 48 | 49 | let mainView; 50 | if (error) { 51 | mainView = (
{ error }
); 52 | } else { 53 | mainView = 60 | } 61 | let buttons = []; 62 | if (benchmarkSelection.runNames.length == 1) { 63 | buttons.push(); 64 | buttons.push( | ); 65 | buttons.push(); 66 | } else if (benchmarkSelection.runNames.length == 2) { 67 | buttons.push(); 68 | } else { 69 | buttons.push(); 70 | } 71 | 72 | return ( 73 | } /> 79 | ); 80 | } 81 | 82 | export default connect(({ detailedBenchmarkBundle, benchmarkRuns, runSelection, chartConfig }) => ({ 83 | detailedBenchmarkBundle, 84 | benchmarkSelection: new BenchmarkSelection(benchmarkRuns, runSelection), 85 | chartConfig 86 | }))(DetailScreen) 87 | 88 | 89 | function singleRunChartGenerator(runNames, benchmarkBundle, metricsExtractor, chartConfig) { 90 | return 91 | } 92 | 93 | function twoRunsChartGenerator(runNames, benchmarkBundle, metricsExtractor, chartConfig) { 94 | return 95 | } 96 | 97 | function multiRunChartGenerator(runNames, benchmarkBundle, metricsExtractor, chartConfig) { 98 | return ; 99 | } 100 | -------------------------------------------------------------------------------- /src/javascript/components/DetailSideBar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import FormGroup from 'react-bootstrap/lib/FormGroup' 5 | import InputGroup from 'react-bootstrap/lib/InputGroup' 6 | import FormControl from 'react-bootstrap/lib/FormControl' 7 | 8 | import BackIcon from 'react-icons/lib/md/keyboard-backspace' 9 | 10 | import { actions } from 'store/store.js' 11 | import TocList from 'components/TocList.jsx' 12 | 13 | export default class DetailSideBar extends React.Component { 14 | 15 | static propTypes = { 16 | benchmarkBundle: PropTypes.object.isRequired, 17 | benchmarkBundles: PropTypes.array.isRequired, 18 | secondaryMetrics: PropTypes.array.isRequired, 19 | buttons: PropTypes.array, 20 | }; 21 | 22 | render() { 23 | const { benchmarkBundle, benchmarkBundles, secondaryMetrics, buttons } = this.props; 24 | const benchmarkBundleOptions = benchmarkBundles.map(bundle => ); 27 | 28 | const metrics = ['Score'].concat(secondaryMetrics); 29 | 30 | return
31 | actions.goBack() }> 32 | Back.. 33 |
34 |
35 | 36 | 37 | actions.detailBenchmarkBundle(event.target.value) } defaultValue={ benchmarkBundle.key }> 38 | { benchmarkBundleOptions } 39 | 40 | 41 | 42 | { buttons } 43 |
44 | alert(category) } 48 | elementIds={ metrics } 49 | elementNames={ metrics } 50 | linkControlsCreators={ [] } /> 51 |
52 | } 53 | 54 | 55 | } -------------------------------------------------------------------------------- /src/javascript/components/DetailView.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import TocElement from 'components/TocElement.jsx' 4 | 5 | import { ChartDetailHeader } from 'components/ChartHeader.jsx' 6 | import PrimaryMetricExtractor from 'models/extractor/PrimaryMetricExtractor.js' 7 | import SecondaryMetricExtractor from 'models/extractor/SecondaryMetricExtractor.js' 8 | import { getUniqueBenchmarkModes } from 'functions/parse.js' 9 | import { createMetricBadge } from 'components/commons.jsx'; 10 | 11 | /* eslint react/prop-types: 0 */ 12 | const DetailView = ({ runNames, benchmarkBundle, secondaryMetrics, chartConfig, chartGeneratorFunction }) => { 13 | 14 | const primaryMetricExtractor = new PrimaryMetricExtractor(); 15 | const benchmarkModes = getUniqueBenchmarkModes(benchmarkBundle, primaryMetricExtractor); 16 | const benchmarkModeBadges = benchmarkModes.map(mode => createMetricBadge(mode)); 17 | 18 | const scoreMetricView = 19 | 20 | 21 | { chartGeneratorFunction(runNames, benchmarkBundle, primaryMetricExtractor, chartConfig) } 22 |
23 |
24 |
; 25 | const secondaryMetricViews = secondaryMetrics.map(secondaryMetric => { 26 | const metricExtractor = new SecondaryMetricExtractor(secondaryMetric); 27 | return ( 28 | 29 | 30 | { chartGeneratorFunction(runNames, benchmarkBundle, metricExtractor, chartConfig) } 31 |
32 |
33 |
) 34 | }); 35 | return
36 |

Details of { benchmarkBundle.key }

37 |
38 | { [scoreMetricView, ...secondaryMetricViews] } 39 |
40 | } 41 | 42 | export default DetailView; -------------------------------------------------------------------------------- /src/javascript/components/DoingWorkSpinner.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'store/store.js' 3 | import { blue } from 'functions/colors.js' 4 | var Spinner = require('react-spinkit'); 5 | 6 | /* eslint react/prop-types: 0 */ 7 | const DoingWorkSpinner = ({ initialLoading, loading }) => { 8 | if (!initialLoading && !loading) { 9 | return null; 10 | } 11 | return ( 12 | 17 | ); 18 | } 19 | 20 | export default connect(({ initialLoading, loading }) => ({ initialLoading, loading }))(DoingWorkSpinner) 21 | 22 | -------------------------------------------------------------------------------- /src/javascript/components/Icons.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { actions } from 'store/store.js' 4 | import { yellow } from 'functions/colors.js' 5 | import Tooltipped from 'components/lib/Tooltipped.jsx' 6 | 7 | import ScaleIcon from 'react-icons/lib/fa/balance-scale' 8 | import DetailsIcon from 'react-icons/lib/fa/search-plus' 9 | import SortIcon from 'react-icons/lib/fa/sort-amount-desc' 10 | 11 | import LinkIcon from 'react-icons/lib/fa/external-link' 12 | import GithubIcon from 'react-icons/lib/fa/github' 13 | import BugIcon from 'react-icons/lib/fa/bug' 14 | 15 | export { LinkIcon, GithubIcon, BugIcon } 16 | 17 | const activeColor = yellow; 18 | /* eslint react/prop-types: 0 */ 19 | 20 | export const SortButton = ({ active, action }) => { 21 | return 26 | }; 27 | 28 | export const ScaleButton = ({ active, action }) => { 29 | return 34 | }; 35 | 36 | export const DetailsButton = ({ benchmarkBundle }) => { 37 | const secondaryMetrics = new Set(); 38 | benchmarkBundle.allBenchmarks().forEach(benchmark => { 39 | Object.keys(benchmark.secondaryMetrics).forEach(secondaryMetric => { 40 | secondaryMetrics.add(secondaryMetric) 41 | }) 42 | }); 43 | 44 | return actions.detailBenchmarkBundle(benchmarkBundle.key) } /> 49 | }; 50 | 51 | function IconButton({ IconName, tooltip, active, action }) { 52 | const color = active ? activeColor : null; 53 | return ( 54 | 55 | 56 | 57 | ); 58 | } -------------------------------------------------------------------------------- /src/javascript/components/LoadFromGistsDialog.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Button from 'react-bootstrap/lib/Button' 4 | import Form from 'react-bootstrap/lib/Form' 5 | import FormGroup from 'react-bootstrap/lib/FormGroup' 6 | import FormControl from 'react-bootstrap/lib/FormControl' 7 | import ControlLabel from 'react-bootstrap/lib/ControlLabel' 8 | import Col from 'react-bootstrap/lib/Col' 9 | 10 | export default class LoadFromGistsDialog extends React.Component { 11 | 12 | constructor(props, context) { 13 | super(props, context); 14 | this.state = { 15 | gist1: '', 16 | gist2: '' 17 | }; 18 | this.handleGist1Change = this.handleGist1Change.bind(this); 19 | this.handleGist2Change = this.handleGist2Change.bind(this); 20 | this.handleSubmit = this.handleSubmit.bind(this); 21 | } 22 | 23 | handleGist1Change(event) { 24 | this.setState({ 25 | gist1: event.target.value 26 | }); 27 | } 28 | 29 | handleGist2Change(event) { 30 | this.setState({ 31 | gist2: event.target.value 32 | }); 33 | } 34 | 35 | handleSubmit() { 36 | const params = new URLSearchParams(window.location.search); 37 | params.delete('source'); 38 | params.delete('sources'); 39 | params.delete('gist'); 40 | params.delete('gists'); 41 | 42 | if (this.state.gist2) { 43 | params.set('gists', `${this.state.gist1},${this.state.gist2}`); 44 | } else { 45 | params.set('gist', this.state.gist1); 46 | } 47 | 48 | window.location.search = decodeURIComponent(params.toString()); 49 | } 50 | 51 | render() { 52 | return ( 53 |
54 | 55 | Gist 1 56 | 57 | 58 | 59 | 60 | 61 | 62 | Gist 2 (optional) 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 73 | 74 | 75 |
76 | ); 77 | } 78 | 79 | } -------------------------------------------------------------------------------- /src/javascript/components/LoadFromUrlsDialog.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Button from 'react-bootstrap/lib/Button' 4 | import Form from 'react-bootstrap/lib/Form' 5 | import FormGroup from 'react-bootstrap/lib/FormGroup' 6 | import FormControl from 'react-bootstrap/lib/FormControl' 7 | import ControlLabel from 'react-bootstrap/lib/ControlLabel' 8 | import Col from 'react-bootstrap/lib/Col' 9 | 10 | export default class LoadFromUrlsDialog extends React.Component { 11 | 12 | constructor(props, context) { 13 | super(props, context); 14 | this.state = { 15 | url1: '', 16 | url2: '' 17 | }; 18 | this.handleUrl1Change = this.handleUrl1Change.bind(this); 19 | this.handleUrl2Change = this.handleUrl2Change.bind(this); 20 | this.handleSubmit = this.handleSubmit.bind(this); 21 | } 22 | 23 | handleUrl1Change(event) { 24 | this.setState({ 25 | url1: event.target.value 26 | }); 27 | } 28 | 29 | handleUrl2Change(event) { 30 | this.setState({ 31 | url2: event.target.value 32 | }); 33 | } 34 | 35 | handleSubmit() { 36 | const params = new URLSearchParams(window.location.search); 37 | params.delete('source'); 38 | params.delete('sources'); 39 | params.delete('gist'); 40 | params.delete('gists'); 41 | 42 | if (this.state.url2) { 43 | params.set('sources', `${this.state.url1},${this.state.url2}`); 44 | } else { 45 | params.set('source', this.state.url1); 46 | } 47 | 48 | window.location.search = decodeURIComponent(params.toString()); 49 | } 50 | 51 | render() { 52 | return ( 53 |
54 | 55 | URL 1 56 | 57 | 58 | 59 | 60 | 61 | 62 | URL 2 (optional) 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 73 | 74 | 75 |
76 | ); 77 | } 78 | 79 | } -------------------------------------------------------------------------------- /src/javascript/components/RunScreen.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect, actions } from 'store/store.js' 3 | 4 | import SplitPane from 'components/lib/SplitPane.jsx' 5 | import { SortButton, ScaleButton } from 'components/Icons.jsx' 6 | 7 | import BenchmarkSelection from 'models/BenchmarkSelection.js'; 8 | import RunSideBar from 'components/RunSideBar.jsx'; 9 | import SingleRunView from 'components/single/SingleRunView.jsx'; 10 | import TwoRunsView from 'components/two/TwoRunsView.jsx'; 11 | import MultiRunView from 'components/multi/MultiRunView.jsx'; 12 | 13 | import PrimaryMetricExtractor from 'models/extractor/PrimaryMetricExtractor.js' 14 | import SecondaryMetricExtractor from 'models/extractor/SecondaryMetricExtractor.js' 15 | 16 | 17 | /* eslint react/prop-types: 0 */ 18 | const RunScreen = ({ benchmarkSelection, selectedMetric, focusedBundles, chartConfig }) => { 19 | 20 | const benchmarkBundles = benchmarkSelection.benchmarkBundles; 21 | const metricType = selectedMetric; 22 | const metricExtractor = createMetricExtractor(selectedMetric); 23 | const categories = ['Benchmarks']; 24 | const activeCategory = 'Benchmarks'; 25 | 26 | let filteredBenchmarkBundles = metricType === 'Score' ? benchmarkBundles : benchmarkBundles.filter(benchmarkBundle => benchmarkBundle.allBenchmarks().find(benchmark => metricExtractor.hasMetric(benchmark))); 27 | let sideBarBenchmarks = filteredBenchmarkBundles; 28 | if (focusedBundles.size > 0) { 29 | filteredBenchmarkBundles = filteredBenchmarkBundles.filter(benchmarkBundle => focusedBundles.has(benchmarkBundle.key)); 30 | } 31 | const metricsSet = new Set(['Score']); 32 | filteredBenchmarkBundles.forEach(benchmarkBundle => benchmarkBundle.allBenchmarks().forEach(benchmark => { 33 | Object.keys(benchmark.secondaryMetrics).forEach(metricKey => metricsSet.add(metricKey)); 34 | })); 35 | const metrics = Array.from(metricsSet); 36 | 37 | let mainView; 38 | if (benchmarkSelection.runNames.length == 1) { 39 | mainView = 46 | } else if (benchmarkSelection.runNames.length == 2) { 47 | mainView = 53 | } else { 54 | mainView = 60 | } 61 | 62 | let buttons = []; 63 | if (benchmarkSelection.runNames.length == 1) { 64 | buttons.push(); 65 | buttons.push( | ); 66 | buttons.push(); 67 | } else if (benchmarkSelection.runNames.length == 2) { 68 | buttons.push(); 69 | } else { 70 | buttons.push(); 71 | } 72 | 73 | return ( 74 | } 83 | /> 84 | ); 85 | } 86 | 87 | export default connect(({ benchmarkRuns, runSelection, selectedMetric, focusedBundles, chartConfig }) => ({ 88 | benchmarkSelection: new BenchmarkSelection(benchmarkRuns, runSelection), 89 | selectedMetric, 90 | focusedBundles, 91 | chartConfig 92 | }))(RunScreen) 93 | 94 | function createMetricExtractor(metricType) { 95 | return metricType === 'Score' ? new PrimaryMetricExtractor() : new SecondaryMetricExtractor(metricType); 96 | } 97 | -------------------------------------------------------------------------------- /src/javascript/components/RunSelectionBar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import ButtonGroup from 'react-bootstrap/lib/ButtonGroup' 4 | import SplitButton from 'react-bootstrap/lib/SplitButton' 5 | import Button from 'react-bootstrap/lib/Button' 6 | import MenuItem from 'react-bootstrap/lib/MenuItem' 7 | 8 | import { connect, actions } from 'store/store.js' 9 | 10 | function selectSingleRun(benchmarkRuns, runView, runIndex) { 11 | const runSelection = benchmarkRuns.map((run, index) => { 12 | if (index == runIndex) { 13 | return true; 14 | } else { 15 | return false; 16 | } 17 | }); 18 | actions.selectBenchmarkRuns(runSelection, runView); 19 | } 20 | 21 | function selectAll(oldRunSelection, runView) { 22 | const runSelection = oldRunSelection.map(() => true); 23 | actions.selectBenchmarkRuns(runSelection, runView); 24 | } 25 | 26 | function selectAllWithPossibleSwitchView(oldRunSelection, runView) { 27 | let runSelection; 28 | if (oldRunSelection.some(elem => !elem)) { 29 | runSelection = oldRunSelection.map(() => true); 30 | } else { 31 | runSelection = oldRunSelection; 32 | if (runView === 'Compare') { 33 | runView = 'Summary'; 34 | } else { 35 | runView = 'Compare'; 36 | } 37 | } 38 | actions.selectBenchmarkRuns(runSelection, runView); 39 | } 40 | 41 | function getPossibleRunViews(benchmarkRuns, detailedBenchmarkBundle) { 42 | if (benchmarkRuns.length < 2) { 43 | return []; 44 | } 45 | if (detailedBenchmarkBundle) { 46 | return ['Compare'] 47 | } 48 | return ['Summary', 'Compare']; 49 | } 50 | 51 | /* eslint react/prop-types: 0 */ 52 | // A selection bar for 2 or more runs, selecting either a single run or a compare view 53 | const RunSelectionBar = ({ benchmarkRuns, runSelection, runView, detailedBenchmarkBundle }) => { 54 | if (benchmarkRuns.length <= 1) { 55 | return null; 56 | } 57 | 58 | const runViews = getPossibleRunViews(benchmarkRuns, detailedBenchmarkBundle); 59 | const showAll = runSelection.reduce((showIt, showRun) => showIt && showRun); 60 | 61 | const runComponents = runSelection.map((run, index) => { 62 | const isActive = !showAll && runSelection[index]; 63 | return 70 | }); 71 | let allButton; 72 | if (runViews.length > 1) { 73 | const runViewMenuItems = runViews.map(runViewLabel => selectAll(runSelection, runViewLabel) }> 74 | { runViewLabel } 75 | ); 76 | allButton = selectAllWithPossibleSwitchView(runSelection, runView) }> 82 | { runViewMenuItems } 83 | ; 84 | } else { 85 | allButton = 88 | } 89 | 90 | return ( 91 |
92 | 93 | { runComponents } 94 | 95 | { ' ' } 96 | 97 | { allButton } 98 | 99 |
100 | ); 101 | 102 | } 103 | 104 | export default connect(({ benchmarkRuns, runSelection, runView, detailedBenchmarkBundle }) => ({ benchmarkRuns, runSelection, runView, detailedBenchmarkBundle }))(RunSelectionBar) -------------------------------------------------------------------------------- /src/javascript/components/RunSideBar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import FormGroup from 'react-bootstrap/lib/FormGroup' 5 | import InputGroup from 'react-bootstrap/lib/InputGroup' 6 | import FormControl from 'react-bootstrap/lib/FormControl' 7 | 8 | import DetailsIcon from 'react-icons/lib/fa/search-plus' 9 | import EyeIcon from 'react-icons/lib/fa/eye' 10 | 11 | import { actions } from 'store/store.js' 12 | import TocList from 'components/TocList.jsx' 13 | import Tooltipped from 'components/lib/Tooltipped.jsx' 14 | 15 | // Side bar for SingleRunView, TwoRunViews, etc... 16 | export default class RunSideBar extends React.Component { 17 | 18 | static propTypes = { 19 | benchmarkBundles: PropTypes.array.isRequired, 20 | metrics: PropTypes.array.isRequired, 21 | metricExtractor: PropTypes.object.isRequired, 22 | buttons: PropTypes.array, 23 | focusedBenchmarkBundles: PropTypes.object.isRequired, 24 | categories: PropTypes.array.isRequired, 25 | activeCategory: PropTypes.string.isRequired, 26 | }; 27 | 28 | render() { 29 | const { benchmarkBundles, metrics, metricExtractor, buttons, focusedBenchmarkBundles, categories, activeCategory } = this.props; 30 | 31 | const metricsOptions = metrics.filter(aMetric => aMetric.startsWith('·') || aMetric === 'Score').map(metric => ); 34 | 35 | const elementIds = benchmarkBundles.map(bundle => bundle.key); 36 | const elementNames = benchmarkBundles.map(bundle => bundle.name); 37 | 38 | const focusControlCreator = (elementId) => { 39 | e.stopPropagation(); 40 | actions.focusBundle(elementId) 41 | } } className={ focusedBenchmarkBundles.has(elementId) ? ' focused' : '' + ' clickable' }>{ ' ' }; 42 | const detailsControlCreator = (elementId) => ( { 43 | e.stopPropagation(); 44 | actions.detailBenchmarkBundle(elementId); 45 | } } className="clickable">{ ' ' }); 46 | 47 | return ( 48 |
49 | 50 | 51 | 1 }> 52 | { 55 | actions.selectMetric(event.target.value); 56 | } } 57 | value={ metricExtractor.metricKey } 58 | disabled={ metrics.length < 2 }> 59 | { metricsOptions } 60 | 61 | 62 | 63 | 64 | { buttons } 65 |
66 | 72 |
73 | ); 74 | } 75 | 76 | } -------------------------------------------------------------------------------- /src/javascript/components/SummaryScreen.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'store/store.js' 3 | 4 | import SplitPane from 'components/lib/SplitPane.jsx' 5 | 6 | import BenchmarkSelection from 'models/BenchmarkSelection.js'; 7 | import RunSideBar from 'components/RunSideBar.jsx'; 8 | import SummaryView from 'components/summary/SummaryView.jsx'; 9 | 10 | import PrimaryMetricExtractor from 'models/extractor/PrimaryMetricExtractor.js' 11 | import SecondaryMetricExtractor from 'models/extractor/SecondaryMetricExtractor.js' 12 | 13 | 14 | /* eslint react/prop-types: 0 */ 15 | const SummaryScreen = ({ benchmarkSelection, selectedMetric }) => { 16 | 17 | const benchmarkBundles = benchmarkSelection.benchmarkBundles; 18 | const metricType = selectedMetric; 19 | const metricExtractor = createMetricExtractor(selectedMetric); 20 | const categories = ['Benchmarks']; 21 | const activeCategory = 'Benchmarks'; 22 | 23 | let filteredBenchmarkBundles = metricType === 'Score' ? benchmarkBundles : benchmarkBundles.filter(benchmarkBundle => benchmarkBundle.allBenchmarks().find(benchmark => metricExtractor.hasMetric(benchmark))); 24 | const metricsSet = new Set(['Score']); 25 | filteredBenchmarkBundles.forEach(benchmarkBundle => benchmarkBundle.allBenchmarks().forEach(benchmark => { 26 | Object.keys(benchmark.secondaryMetrics).forEach(metricKey => metricsSet.add(metricKey)); 27 | })); 28 | const metrics = Array.from(metricsSet); 29 | 30 | let runIndices = [benchmarkSelection.runNames.length - 2, benchmarkSelection.runNames.length - 1]; 31 | let runNames = runIndices.map(i => benchmarkSelection.runNames[i]); 32 | 33 | return ( 34 | 43 | } 44 | right={ 45 | 53 | } 54 | /> 55 | ); 56 | } 57 | 58 | export default connect(({ benchmarkRuns, runSelection, selectedMetric }) => ({ 59 | benchmarkSelection: new BenchmarkSelection(benchmarkRuns, runSelection), 60 | selectedMetric, 61 | }))(SummaryScreen) 62 | 63 | function createMetricExtractor(metricType) { 64 | return metricType === 'Score' ? new PrimaryMetricExtractor() : new SecondaryMetricExtractor(metricType); 65 | } 66 | -------------------------------------------------------------------------------- /src/javascript/components/TocElement.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import { Element } from 'react-scroll' 5 | 6 | //An element in the main content of a page linked by a TocLink within a TocSideBar 7 | export default class TocElement extends React.Component { 8 | 9 | static propTypes = { 10 | name: PropTypes.string.isRequired, 11 | children: PropTypes.node.isRequired, 12 | }; 13 | 14 | render() { 15 | return ( 16 | 17 | { this.props.children } 18 | 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/javascript/components/TocLink.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import { ScrollLink } from 'react-scroll' 5 | 6 | // A link in TocSidebar pointing to a TocElement 7 | class TocLink extends React.Component { 8 | 9 | static propTypes = { 10 | children: PropTypes.node.isRequired, 11 | }; 12 | 13 | render() { 14 | return ( 15 |
  • 16 | { this.props.children } 17 |
  • 18 | ); 19 | } 20 | 21 | } 22 | 23 | const EnhancedTocLink = ScrollLink(TocLink); 24 | export { EnhancedTocLink as default } 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/javascript/components/TocList.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import { actions } from 'store/store.js' 5 | import TocLink from 'components/TocLink.jsx' 6 | 7 | import { scrollSpy, scroller } from 'react-scroll' 8 | 9 | //Constructs a sidebar with a set of controls and links to the MainView sections 10 | export default class TocList extends React.PureComponent { 11 | 12 | static propTypes = { 13 | categories: PropTypes.array.isRequired, 14 | activeCategory: PropTypes.string.isRequired, 15 | elementIds: PropTypes.array.isRequired, 16 | elementNames: PropTypes.array.isRequired, 17 | linkControlsCreators: PropTypes.array.isRequired, 18 | }; 19 | 20 | componentDidMount() { 21 | scrollSpy.update(); 22 | } 23 | 24 | scrollTo(elementId) { 25 | scroller.scrollTo(elementId, { 26 | duration: 500, 27 | delay: 50, 28 | smooth: 'linear', 29 | offset: -25 30 | }); 31 | } 32 | 33 | render() { 34 | const { categories, activeCategory, elementIds, elementNames, linkControlsCreators } = this.props; 35 | //TODO extract TocElement to own component and make onClick handlers unique: https://stackoverflow.com/a/38908620/672008 36 | return ( 37 | 65 | ); 66 | } 67 | 68 | } 69 | 70 | -------------------------------------------------------------------------------- /src/javascript/components/UploadMainView.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Dropzone from 'react-dropzone' 4 | import UploadIcon from 'react-icons/lib/fa/cloud-upload' 5 | import Alert from 'react-bootstrap/lib/Alert' 6 | 7 | import { actions } from 'store/store.js' 8 | import { blue, green } from 'functions/colors.js' 9 | 10 | // Dopzone for JSON files to upload 11 | export default class UploadMainView extends React.Component { 12 | 13 | constructor(props) { 14 | super(props); 15 | } 16 | 17 | onDrop(files) { 18 | actions.uploadFiles(files); 19 | } 20 | 21 | render() { 22 | return ( 23 | alert('Only drop valid JSON files!') } 26 | multiple={ true } 27 | accept='.json' 28 | disableClick={ true } 29 | disablePreview={ true } 30 | className="container-fluid" 31 | style={ { width: '100%', height: '81vh', borderWidth: 1, borderColor: blue, borderStyle: 'dashed', borderRadius: 25, padding: 20, textAlign: 'center', verticalAlign: 'middle' } } //TODO seems to be a bug with dropzone... rejectStyle is always taken 32 | rejectStyle={ { borderColor: green, borderWidth: 3, borderStyle: 'dotted' } } 33 | activeStyle={ { borderColor: green, borderWidth: 3, borderStyle: 'dotted' } }> 34 |
    35 |

    Dropzone

    36 |
    Drop your JMH JSON report file(s) here!
    37 |
    38 |

    39 |
    40 | 41 |
    42 |
    43 | "JMH is a Java harness for building, running, and analysing nano/micro/milli/macro benchmarks written in Java and other languages targetting the JVM." 44 |
    45 |
    46 | Use this tool to visually explore your benchmark results! Simply upload* any JMH result files (in JSON format). 47 |
    48 |
    49 |
    50 |
    51 |
    52 | * Your data stays locally in your browser, it is not send to any server! 53 |
    54 |
    55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/javascript/components/UploadScreen.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import SplitPane from 'components/lib/SplitPane.jsx' 4 | import UploadMainView from 'components/UploadMainView.jsx'; 5 | import UploadSideBar from 'components/UploadSideBar.jsx'; 6 | 7 | const UploadScreen = () => { 8 | return ( 9 | } right={ } /> 10 | ); 11 | } 12 | 13 | export default UploadScreen; -------------------------------------------------------------------------------- /src/javascript/components/UploadSideBar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Modal from 'react-bootstrap/lib/Modal' 4 | import PointingHandIcon from 'react-icons/lib/fa/hand-o-right' 5 | 6 | import { actions } from 'store/store.js' 7 | import LoadFromUrlsDialog from 'components/LoadFromUrlsDialog.jsx' 8 | import LoadFromGistsDialog from 'components/LoadFromGistsDialog.jsx' 9 | 10 | //Sidebar for the upload view 11 | export default class UploadSideBar extends React.Component { 12 | 13 | constructor(props, context) { 14 | super(props, context); 15 | 16 | this.state = { 17 | urlDialogVisible: false, 18 | gistDialogVisible: false, 19 | }; 20 | 21 | this.showUrlDialog = this.showUrlDialog.bind(this); 22 | this.hideUrlDialog = this.hideUrlDialog.bind(this); 23 | this.showGistDialog = this.showGistDialog.bind(this); 24 | this.hideGistDialog = this.hideGistDialog.bind(this); 25 | } 26 | 27 | showUrlDialog() { 28 | this.setState({ 29 | urlDialogVisible: true 30 | }); 31 | } 32 | 33 | hideUrlDialog() { 34 | this.setState({ 35 | urlDialogVisible: false 36 | }); 37 | } 38 | 39 | showGistDialog() { 40 | this.setState({ 41 | gistDialogVisible: true, 42 | }); 43 | } 44 | 45 | hideGistDialog() { 46 | this.setState({ 47 | gistDialogVisible: false 48 | }); 49 | } 50 | 51 | render() { 52 | return
    53 |
    54 |
    55 | Open File Dialog 56 | { 61 | actions.uploadFiles([...event.target.files]); 62 | } } 63 | style={ { opacity: 0.0, position: 'absolute', top: 0, left: 0, bottom: 0, right: 0 } } /> 64 |
    65 |
    66 |
    67 |
    68 | 69 | { ' ' } 70 | Load Single Run Example 71 |
    72 |
    73 | 74 | { ' ' } 75 | Load Two Runs Example 76 |
    77 |
    78 | 79 | { ' ' } 80 | Load Multi Run Example 81 |
    82 |
    83 |
    84 | 85 | { ' ' } 86 | Load from URL(s) 87 |
    88 |
    89 | 90 | { ' ' } 91 | Load from Gist(s) 92 |
    93 |
    94 | 95 | 96 | 97 | Load JMH benchmarks from external URL(s) 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | Load JMH benchmarks from external Gist(s) 108 | 109 | 110 | 111 | 112 | 113 | 114 |
    115 | } 116 | } -------------------------------------------------------------------------------- /src/javascript/components/commons.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import BadgeWithTooltip from 'components/lib/BadgeWithTooltip.jsx' 4 | 5 | import { getMetricType } from 'models/MetricType.js' 6 | 7 | export function createMetricBadge(metricKey) { 8 | const metricType = getMetricType(metricKey); 9 | if (!metricType) { 10 | return null; 11 | } 12 | return 13 | } -------------------------------------------------------------------------------- /src/javascript/components/global/CustomTopBar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | /* eslint react/prop-types: 0 */ 4 | const CustomTopBar = ({ title }) => { 5 | return ( 6 |

    { title }

    7 | ); 8 | } 9 | 10 | export default CustomTopBar; -------------------------------------------------------------------------------- /src/javascript/components/global/DefaultTopBar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Navbar from 'react-bootstrap/lib/Navbar' 4 | import Nav from 'react-bootstrap/lib/Nav' 5 | import NavItem from 'react-bootstrap/lib/NavItem' 6 | import Dropdown from 'react-bootstrap/lib/Dropdown' 7 | import MenuItem from 'react-bootstrap/lib/MenuItem' 8 | import Popover from 'react-bootstrap/lib/Popover' 9 | import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger' 10 | 11 | import { LinkIcon } from 'components/Icons.jsx' 12 | 13 | import AppLogo from 'components/AppLogo.jsx'; 14 | import DoingWorkSpinner from 'components/DoingWorkSpinner.jsx'; 15 | 16 | export default class DefaultTopBar extends React.Component { 17 | 18 | onReset() { 19 | window.onbeforeunload = null; 20 | window.location = window.location.href.split('#')[0].split('?')[0]; 21 | } 22 | 23 | render() { 24 | const aboutPopover = ( 25 | 26 |

    27 | JMH Visualizer will render charts out of your JMH Benchmarks. All it needs 28 | are your benchmark results in JSON format. 29 |

    30 |
    31 | ); 32 | 33 | const showReset = providedBenchmarks.length == 0; // eslint-disable-line no-undef 34 | 35 | return ( 36 | 37 | 38 | 39 | 40 | 41 | 42 | { showReset > 0 && 43 | Reset & Upload New 44 | } 45 | { showReset > 0 && 46 | 47 | } 48 | { ' Feedback & Bug Reports ' } 49 | { ' Code @ Github ' } 50 | 51 | { ' JMH ' } 52 | { ' JMH Samples' } 53 | 54 | 59 | About 60 | 61 | 62 | 63 | 64 | 65 | 70 | 71 | ); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/javascript/components/global/Footer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Grid from 'react-bootstrap/lib/Grid'; 4 | import Row from 'react-bootstrap/lib/Row'; 5 | import Col from 'react-bootstrap/lib/Col'; 6 | import Panel from 'react-bootstrap/lib/Panel' 7 | 8 | import { connect } from 'store/store.js' 9 | import { GithubIcon, BugIcon } from 'components/Icons.jsx' 10 | 11 | 12 | /* eslint react/prop-types: 0 */ 13 | const Footer = ({ topBar }) => { 14 | 15 | if (topBar === 'default') { 16 | return null; 17 | } 18 | return ( 19 | 20 | 21 | 22 | 23 | JMH Visualizer { process.env.version } 24 | 25 | 26 | 27 | { ' | ' } 28 | 29 | 30 | 31 | 32 | 33 | ); 34 | 35 | } 36 | 37 | export default connect(({ settings }) => ({ 38 | topBar: settings.topBar, 39 | }))(Footer) -------------------------------------------------------------------------------- /src/javascript/components/global/TopBar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { connect } from 'store/store.js' 4 | 5 | import DefaultTopBar from 'components/global/DefaultTopBar.jsx'; 6 | import CustomTopBar from 'components/global/CustomTopBar.jsx'; 7 | 8 | /* eslint react/prop-types: 0 */ 9 | const TopBar = ({ topBar }) => { 10 | 11 | switch (topBar) { 12 | case 'default': 13 | return ; 14 | case 'off': 15 | return null; 16 | default: 17 | return ; 18 | } 19 | } 20 | 21 | export default connect(({ settings }) => ({ 22 | topBar: settings.topBar, 23 | }))(TopBar) -------------------------------------------------------------------------------- /src/javascript/components/lib/BadgeWithTooltip.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Badge from 'react-bootstrap/lib/Badge' 4 | import Tooltipped from 'components/lib/Tooltipped.jsx' 5 | 6 | /* eslint react/prop-types: 0 */ 7 | const BadgeWithTooltip = ({ name, tooltip, children = [] }) => { 8 | return ( 9 | 10 | 11 | { name } 12 | { children } 13 | 14 | 15 | ); 16 | } 17 | 18 | export default BadgeWithTooltip; 19 | -------------------------------------------------------------------------------- /src/javascript/components/lib/SplitPane.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import Grid from 'react-bootstrap/lib/Grid' 5 | import Row from 'react-bootstrap/lib/Row' 6 | import Col from 'react-bootstrap/lib/Col' 7 | 8 | import AutoAffix from 'react-overlays/lib/AutoAffix'; 9 | 10 | export default function SplitPane(props) { 11 | 12 | return 13 | 14 | 15 | { props.left } 16 | 17 | 18 | 19 |
    20 | { props.right } 21 |
    22 |
    23 | 24 |
    25 |
    ; 26 | } 27 | 28 | SplitPane.propTypes = { 29 | left: PropTypes.object.isRequired, 30 | right: PropTypes.object.isRequired, 31 | }; -------------------------------------------------------------------------------- /src/javascript/components/lib/Tooltipped.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import '../../../css/tooltip.css'; 4 | 5 | // Wraps a componen and puts a simple text hover tooltip to it. Parameters are: 6 | // tooltip='Hello World' 7 | // position=[left|right|bottom|top] 8 | // (disabled=true) 9 | export default function Tooltipped(options) { 10 | if (options.disabled) { 11 | return options.children; 12 | } 13 | 14 | return ( 15 | { options.children } 16 | ) 17 | 18 | } -------------------------------------------------------------------------------- /src/javascript/components/multi/LineChartView.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import { ResponsiveContainer, LineChart, XAxis, YAxis, Tooltip, CartesianGrid, LabelList, Legend, Line, ErrorBar } from 'recharts'; 5 | 6 | import { scaleOrdinal } from 'd3-scale'; 7 | import { schemeCategory10 } from 'd3-scale-chromatic'; 8 | const lineColors = scaleOrdinal(schemeCategory10).range(); 9 | 10 | import MultiRunChartTooltip from 'components/multi/MultiRunChartTooltip.jsx' 11 | import { tooltipBackground } from 'functions/colors.js' 12 | import { tickFormatter } from 'functions/charts.js' 13 | import { shouldRound, round } from 'functions/util.js' 14 | import { formatNumber } from 'functions/util.js' 15 | 16 | export default class LineChartView extends React.Component { 17 | 18 | static propTypes = { 19 | runNames: PropTypes.array.isRequired, 20 | benchmarkBundle: PropTypes.object.isRequired, 21 | metricExtractor: PropTypes.object.isRequired, 22 | logScale: PropTypes.bool.isRequired 23 | }; 24 | 25 | constructor(props) { 26 | super(props); 27 | this.state = { 28 | activeLine: null 29 | }; 30 | } 31 | 32 | shouldComponentUpdate(nextProps, nextState) { // eslint-disable-line no-unused-vars 33 | return this.props.runNames[0] !== nextProps.runNames[0] 34 | || this.props.benchmarkBundle.key !== nextProps.benchmarkBundle.key 35 | || this.props.metricExtractor.metricKey !== nextProps.metricExtractor.metricKey 36 | || this.props.logScale !== nextProps.logScale 37 | || this.state.activeLine !== nextState.activeLine; 38 | } 39 | 40 | activateLineFromLegend(params) { 41 | this.activateLine(params.dataKey); 42 | } 43 | 44 | activateLine(benchmarkMethodKey) { 45 | this.setState({ 46 | activeLine: benchmarkMethodKey, 47 | }); 48 | } 49 | 50 | deactivateLine() { 51 | this.setState({ 52 | activeLine: null 53 | }); 54 | } 55 | 56 | render() { 57 | const { runNames, benchmarkBundle, metricExtractor, logScale } = this.props; 58 | const { activeLine } = this.state; 59 | const shouldRoundScores = shouldRound(benchmarkBundle.benchmarkMethods, metricExtractor); 60 | 61 | let scale, domain; 62 | if (logScale) { 63 | scale = 'log'; 64 | domain = ['auto', 'auto']; 65 | } 66 | 67 | const dataSet = runNames.map((runName, runIndex) => { 68 | const runObject = { 69 | name: runName, 70 | }; 71 | benchmarkBundle.benchmarkMethods.map(benchmarkMethod => { 72 | const benchmark = benchmarkMethod.benchmarks[runIndex]; 73 | if (benchmark && metricExtractor.hasMetric(benchmark)) { 74 | const score = round(metricExtractor.extractScore(benchmark), shouldRoundScores); 75 | const scoreError = round(metricExtractor.extractScoreError(benchmark), shouldRoundScores); 76 | const minMax = metricExtractor.extractMinMax(benchmark).map(minOrMax => round(minOrMax, shouldRoundScores)); 77 | const scoreUnit = metricExtractor.extractScoreUnit(benchmark); 78 | let errorBarInterval = 0 79 | if (!isNaN(scoreError)) { 80 | errorBarInterval = [score - minMax[0], minMax[1] - score]; 81 | } 82 | runObject[benchmarkMethod.key] = score; 83 | runObject.scoreUnit = scoreUnit; 84 | runObject[benchmarkMethod.key + '-scoreError'] = scoreError; 85 | runObject[benchmarkMethod.key + '-minMax'] = minMax; 86 | runObject[benchmarkMethod.key + '-errorBarInterval'] = errorBarInterval; 87 | runObject[benchmarkMethod.key + '-label'] = score.toLocaleString() + ' ' + scoreUnit; 88 | runObject[benchmarkMethod.key + '-errorLabel'] = scoreError.toLocaleString() + ' ' + scoreUnit; 89 | } 90 | }); 91 | return runObject; 92 | }); 93 | 94 | const lines = benchmarkBundle.benchmarkMethods.filter(benchmarkMethod => (!logScale || isInAllRuns(runNames, benchmarkMethod))).map((benchmarkMethod, i) => { 95 | const isActive = activeLine === benchmarkMethod.key; 96 | const strokeWidth = isActive ? 7 : 3; 97 | const strokeOpacity = !activeLine || isActive ? 1 : 0.1; 98 | let label, errorBarStrokeWIdth; 99 | if (isActive) { 100 | label = } />; 101 | errorBarStrokeWIdth = 1; 102 | } else { 103 | errorBarStrokeWIdth = 0; 104 | } 105 | const errorBar = (); 106 | 107 | return 119 | { label } 120 | { errorBar } 121 | ; 122 | }); 123 | 124 | const tooltip = activeLine ? undefined : } wrapperStyle={ { backgroundColor: tooltipBackground, opacity: 0.95 } } />; 125 | return ( 126 | 127 | 128 | 129 | 130 | 131 | 132 | { tooltip } 133 | { lines } 134 | 135 | 136 | ); 137 | } 138 | } 139 | 140 | function isInAllRuns(runNames, benchmarkMethod) { 141 | for (let index = 0; index < runNames.length; index++) { 142 | if (!benchmarkMethod.benchmarks[index]) { 143 | return false; 144 | } 145 | } 146 | return true; 147 | } 148 | 149 | function Label(params) { 150 | if (!params.value) { 151 | return null; 152 | } 153 | let textAnchor; 154 | if (params.index == 0) { 155 | textAnchor = 'start'; 156 | } else if (params.index == params.runCount - 1) { 157 | textAnchor = 'end'; 158 | } else { 159 | textAnchor = 'middle'; 160 | } 161 | const value = params.value && params.value.constructor === Array ? params.value[1] : params.value; 162 | return 171 | { formatNumber(value, params.shouldRoundScores) } 172 | 173 | } 174 | -------------------------------------------------------------------------------- /src/javascript/components/multi/MultiRunBundle.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import ChartHeader from 'components/ChartHeader.jsx' 5 | import { DetailsButton, ScaleButton } from 'components/Icons.jsx' 6 | import LineChartView from 'components/multi/LineChartView.jsx' 7 | 8 | // The view for a bunch of benchmarks, usually all of a benchmark class 9 | export default class MultiRunBundle extends React.Component { 10 | 11 | static propTypes = { 12 | runNames: PropTypes.array.isRequired, 13 | benchmarkBundle: PropTypes.object.isRequired, 14 | metricExtractor: PropTypes.object.isRequired, 15 | chartConfig: PropTypes.object.isRequired, 16 | }; 17 | 18 | constructor(props) { 19 | super(props); 20 | this.state = { 21 | logScale: props.chartConfig.logScale 22 | }; 23 | } 24 | 25 | UNSAFE_componentWillReceiveProps(nextProps) { 26 | if (nextProps.chartConfig.logScale !== this.state.logScale) { 27 | this.setState({ logScale: nextProps.chartConfig.logScale }); 28 | } 29 | } 30 | 31 | toggleLogScale() { 32 | this.setState({ 33 | logScale: !this.state.logScale 34 | }); 35 | } 36 | 37 | render() { 38 | const { runNames, benchmarkBundle, metricExtractor } = this.props; 39 | const { logScale } = this.state; 40 | 41 | return ( 42 | 43 |
    44 | 45 | 46 | 47 | 48 |
    49 | 50 |
    51 |
    52 | ); 53 | } 54 | } 55 | 56 | -------------------------------------------------------------------------------- /src/javascript/components/multi/MultiRunChartTooltip.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Table from 'react-bootstrap/lib/Table' 4 | 5 | import { formatNumber } from 'functions/util.js' 6 | import { blue, red } from 'functions/colors.js' 7 | 8 | // Tooltip for LineChartView 9 | export default class MultiRunChartTooltip extends Component { 10 | 11 | static propTypes = { 12 | label: PropTypes.any, 13 | roundScores: PropTypes.bool, 14 | payload: PropTypes.arrayOf(PropTypes.shape({ 15 | name: PropTypes.any, 16 | payload: PropTypes.any, 17 | value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), 18 | unit: PropTypes.any, 19 | })), 20 | }; 21 | 22 | render() { 23 | const { label, payload, roundScores } = this.props; 24 | if (payload.length == 0) { 25 | return null; 26 | } 27 | const tableRows = payload.map(dataPoint => 28 | { dataPoint.name } 29 | { formatNumber(dataPoint.value, roundScores) } 30 | { formatNumber(dataPoint.payload[dataPoint.name + '-minMax'][0], roundScores) } 31 | { formatNumber(dataPoint.payload[dataPoint.name + '-minMax'][1], roundScores) } 32 | { formatNumber(dataPoint.payload[dataPoint.name + '-scoreError'], roundScores) } 33 | { dataPoint.payload.scoreUnit } 34 | ); 35 | return ( 36 |
    37 |
    38 |

    { label }

    39 |
    40 |
    41 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | { tableRows } 58 | 59 |
    BenchmarkScoreMinMaxScore ErrorUnit
    60 |
    61 |
    62 | ); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/javascript/components/multi/MultiRunView.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import Badge from 'react-bootstrap/lib/Badge' 5 | 6 | import TocElement from 'components/TocElement.jsx' 7 | import MultiRunBundle from 'components/multi/MultiRunBundle.jsx' 8 | 9 | export default class MultiRunView extends React.Component { 10 | 11 | static propTypes = { 12 | runNames: PropTypes.array.isRequired, 13 | benchmarkBundles: PropTypes.array.isRequired, 14 | metricExtractor: PropTypes.object.isRequired, 15 | chartConfig: PropTypes.object.isRequired, 16 | }; 17 | 18 | render() { 19 | const { runNames, benchmarkBundles, metricExtractor, chartConfig } = this.props; 20 | 21 | const elements = []; 22 | elements.push( 23 |
    24 | Comparing 25 | { ' ' } 26 | 27 | { benchmarkBundles.length } 28 | benchmark classes for 29 | { ' ' + runNames.length } runs on metric ' 30 | { metricExtractor.metricKey }'. 31 |
    32 | ); 33 | 34 | benchmarkBundles.forEach(benchmarkBundle => { 35 | elements.push( 36 | 42 | ); 43 | }); 44 | 45 | 46 | return
    47 | { elements } 48 |
    49 | } 50 | 51 | } -------------------------------------------------------------------------------- /src/javascript/components/single/BarChartView.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import { ResponsiveContainer, BarChart, XAxis, YAxis, Tooltip, CartesianGrid, LabelList, Legend, Bar, ErrorBar } from 'recharts'; 5 | 6 | import { createDataSetFromBenchmarks } from 'components/single/BarDataSet.js' 7 | import BarLabel from 'components/single/BarLabel.jsx'; 8 | import SingleRunChartTooltip from 'components/single/SingleRunChartTooltip.jsx'; 9 | import { tickFormatter } from 'functions/charts.js' 10 | import { blue, green, lightBlack, tooltipBackground, barColors } from 'functions/colors.js' 11 | 12 | // Gathered report for one benchmark class 13 | export default class BarChartView extends React.Component { 14 | 15 | static propTypes = { 16 | benchmarkBundle: PropTypes.object.isRequired, 17 | metricExtractor: PropTypes.object.isRequired, 18 | dataMax: PropTypes.number, 19 | chartConfig: PropTypes.object.isRequired 20 | }; 21 | 22 | render() { 23 | const { benchmarkBundle, metricExtractor, dataMax, chartConfig } = this.props; 24 | const { logScale, sort } = chartConfig; 25 | const dataSet = createDataSetFromBenchmarks(benchmarkBundle, metricExtractor, sort); 26 | 27 | const domainMax = dataMax && dataMax > 0 ? Math.round(dataMax) : 'auto'; 28 | let scale, domainMin, chartMarginRight; 29 | if (logScale) { 30 | scale = 'log'; 31 | domainMin = dataMax && dataMax > 0 ? 0.1 : 'auto'; 32 | chartMarginRight = 90; 33 | } else { 34 | scale = 'linear'; 35 | domainMin = 0; 36 | chartMarginRight = 45; 37 | } 38 | 39 | const { paramNames } = dataSet; 40 | const chartHeight = 100 + dataSet.data.length * dataSet.barGroups.length * 36; 41 | const maxMethodNameLength = dataSet.data.map((element) => element.name.length).reduce((previous, current) => Math.max(previous, current), 32); 42 | const singleBar = dataSet.barGroups.length == 1; 43 | const bars = dataSet.barGroups.map((barGroup, i) => { 44 | return 53 | 54 | 59 | 60 | }); 61 | 62 | return ( 63 |
    64 | 65 | 70 | 71 | 72 | 73 | } 76 | cursor={ { stroke: green, strokeWidth: 2 } } 77 | wrapperStyle={ { backgroundColor: tooltipBackground, opacity: 0.95 } } 78 | paramNames={ paramNames } 79 | /> 80 | 81 | { bars } 82 | 83 | 84 | { paramNames.length > 0 && 85 |
    Parameter Names: { paramNames.join(':') }

    86 | } 87 |
    88 | ); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/javascript/components/single/BarDataSet.js: -------------------------------------------------------------------------------- 1 | 2 | // A dataset for a BarChart 3 | export default class BarDataSet { 4 | constructor(options) { 5 | this.metricKey = options.metricKey; 6 | this.scoreUnit = options.scoreUnit; 7 | this.barGroups = options.barGroups; 8 | this.data = options.data; 9 | this.dataMax = options.dataMax; 10 | this.roundScores = options.roundScores; 11 | this.paramNames = options.paramNames; 12 | } 13 | } 14 | 15 | import { groupBy, shouldRound, round, formatNumber } from 'functions/util.js' 16 | import { getMetricType } from 'models/MetricType.js' 17 | 18 | // The datasets will differ in case the benchmark-class uses params or not: 19 | // 0 - no param => standard 20 | // 1 - one param, single method => convert to (0) 21 | // 2 - one param, multi methods => Bar per param Value 22 | // 3 - 2 params, single methods => convert to (2) 23 | // 4 - 2+ params, multi methods => combine params & convert to (2) 24 | // 5 - 3+ params, single methods => combine params & convert to (0) 25 | export function createDataSetFromBenchmarks(benchmarkBundle, metricExtractor, sort) { 26 | const benchmarkMethods = benchmarkBundle.benchmarkMethods; 27 | const methodCount = benchmarkBundle.methodNames.length; 28 | const metricType = metricExtractor.extractType(benchmarkMethods[0].benchmarks[0]); 29 | const params = benchmarkMethods[0].params; //TODO get from collection as well ? 30 | const scoreUnit = metricExtractor.hasMetric(benchmarkMethods[0].benchmarks[0]) ? metricExtractor.extractScoreUnit(benchmarkMethods[0].benchmarks[0]) : ''; 31 | 32 | if (!params) { 33 | //case 0 34 | return createBarDataSet(benchmarkMethods, metricExtractor, sort, (method) => method.name, () => `${metricType} ${scoreUnit}`, []); 35 | } else { // all other cases 36 | const paramNames = params.map(param => param[0]); 37 | if (paramNames.length == 1) { 38 | if (methodCount == 1) { 39 | // case 1 40 | return createBarDataSet(benchmarkMethods, metricExtractor, sort, (method) => `${method.params[0][0]} = ${method.params[0][1]}`, () => `${metricType} ${scoreUnit}`, []); 41 | } else { 42 | // case 2 43 | return createBarDataSet(benchmarkMethods, metricExtractor, sort, (method) => method.name, (method) => { 44 | // Kind of a hack. 45 | // The tests of a class aren't homogenous but the code is build on that assumption. 46 | // While this prevents an inital error, it might lead to a break down later down the road. 47 | // The sane option would probably be to distribute the benchmark methods to in different bundles (and have different charts) 48 | if (!method.params) { 49 | return ""; 50 | } 51 | return method.params[0][1]; 52 | }, paramNames); 53 | } 54 | } else if (paramNames.length == 2 && methodCount == 1) { 55 | // case 3 56 | return createBarDataSet(benchmarkMethods, metricExtractor, sort, (method) => `${method.params[0][0]} = ${method.params[0][1]}`, (method) => method.params[1][1], [paramNames[1]]); 57 | } else { 58 | if (methodCount > 1) { 59 | // case 4 60 | return createBarDataSet(benchmarkMethods, metricExtractor, sort, (method) => method.name, (method) => method.params.map(param => param[1]).join(':'), paramNames); 61 | } else { 62 | // case 5 63 | const barName = paramNames.join(':'); 64 | return createBarDataSet(benchmarkMethods, metricExtractor, sort, (method) => method.params.map(param => param[1]).join(':'), () => barName, []); 65 | } 66 | } 67 | } 68 | } 69 | 70 | 71 | //Each benchmark can have multiple bar's attached 72 | function createBarDataSet(benchmarkMethods, metricExtractor, sort, groupFunction, barGroupFunction, paramNames) { 73 | var dataMax = 0; 74 | var scoreUnit; 75 | const shouldRoundScores = shouldRound(benchmarkMethods, metricExtractor); 76 | const groupedBenchmarks = groupBy(benchmarkMethods, groupFunction); 77 | const barGroups = new Set(); 78 | let metricType; 79 | const data = groupedBenchmarks.map((benchmarkGroup, i) => { 80 | const benchmarkName = benchmarkGroup.key; 81 | const dataObject = { 82 | index: i, 83 | name: benchmarkName, 84 | }; 85 | benchmarkGroup.values.forEach(benchmarkMethod => { 86 | const [benchmark] = benchmarkMethod.benchmarks; 87 | if (metricExtractor.hasMetric(benchmark)) { 88 | const score = round(metricExtractor.extractScore(benchmark), shouldRoundScores); 89 | const minMax = metricExtractor.extractMinMax(benchmark).map(minOrMax => round(minOrMax, shouldRoundScores)); 90 | const scoreError = round(metricExtractor.extractScoreError(benchmark), shouldRoundScores); 91 | let errorBarInterval = 0 92 | if (!isNaN(scoreError)) { 93 | errorBarInterval = [score - minMax[0], minMax[1] - score]; 94 | } 95 | scoreUnit = metricExtractor.extractScoreUnit(benchmark); 96 | dataMax = metricExtractor.extractMinMax(benchmark)[1]; 97 | 98 | const barGroup = barGroupFunction(benchmarkMethod); 99 | barGroups.add(barGroup); 100 | dataObject[barGroup] = score; 101 | dataObject[barGroup + 'MinMax'] = minMax; 102 | dataObject[barGroup + 'Error'] = scoreError; 103 | dataObject[barGroup + 'ErrorBarInterval'] = errorBarInterval; 104 | if (metricExtractor.hasHistogram(benchmark)) { 105 | dataObject[barGroup + 'SubScoresHistogram'] = metricExtractor.extractRawDataHistogram(benchmark); 106 | } else { 107 | dataObject[barGroup + 'SubScores'] = metricExtractor.extractRawData(benchmark); 108 | } 109 | dataObject[barGroup + 'Label'] = formatNumber(score, shouldRoundScores) + ' ' + scoreUnit; 110 | 111 | if (!metricType) { 112 | metricType = getMetricType(metricExtractor.extractType(benchmark)); 113 | } 114 | } 115 | }); 116 | 117 | return dataObject; 118 | }); 119 | 120 | if (sort && metricType && barGroups.size == 1) { 121 | const scoreKey = barGroups.values().next().value; 122 | data.sort((a, b) => { 123 | if (metricType.increaseIsGood) { 124 | return b[scoreKey] - a[scoreKey]; 125 | } else { 126 | return a[scoreKey] - b[scoreKey]; 127 | } 128 | }); 129 | } 130 | 131 | return new BarDataSet({ 132 | metricKey: metricExtractor.metricKey, 133 | scoreUnit: scoreUnit, 134 | barGroups: [...barGroups], 135 | data: data, 136 | dataMax: dataMax, 137 | roundScores: shouldRoundScores, 138 | paramNames: paramNames 139 | }); 140 | } -------------------------------------------------------------------------------- /src/javascript/components/single/BarLabel.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import { blue } from 'functions/colors.js' 5 | 6 | export default function BarLabel({ x, y, width, height, value, textAnchor }) { 7 | return ( 8 | 9 | 19 | { value } 20 | 21 | 22 | ); 23 | } 24 | 25 | BarLabel.propTypes = { 26 | x: PropTypes.number.isRequired, 27 | y: PropTypes.number.isRequired, 28 | width: PropTypes.number.isRequired, 29 | height: PropTypes.number.isRequired, 30 | value: PropTypes.string.isRequired, 31 | textAnchor: PropTypes.string.isRequired, 32 | } -------------------------------------------------------------------------------- /src/javascript/components/single/BarTooltipLabel.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import { blue } from 'functions/colors.js' 5 | 6 | export default function BarTooltipLabel({ x, y, width, height, value, textAnchor }) { 7 | return ( 8 | 9 | 19 | { value.toLocaleString() } 20 | 21 | 22 | ); 23 | } 24 | 25 | BarTooltipLabel.propTypes = { 26 | x: PropTypes.number.isRequired, 27 | y: PropTypes.number.isRequired, 28 | width: PropTypes.number.isRequired, 29 | height: PropTypes.number.isRequired, 30 | value: PropTypes.number.isRequired, 31 | textAnchor: PropTypes.string.isRequired, 32 | } -------------------------------------------------------------------------------- /src/javascript/components/single/SingleRunBundle.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import Collapse from 'react-bootstrap/lib/Collapse' 5 | import Button from 'react-bootstrap/lib/Button' 6 | 7 | import ChartHeader from 'components/ChartHeader.jsx' 8 | import { DetailsButton, SortButton, ScaleButton } from 'components/Icons.jsx' 9 | import BarChartView from 'components/single/BarChartView.jsx' 10 | 11 | // The view for a bunch of benchmarks, usually all of a benchmark class 12 | export default class SingleRunBundle extends React.Component { 13 | 14 | static propTypes = { 15 | benchmarkBundle: PropTypes.object.isRequired, 16 | metricExtractor: PropTypes.object.isRequired, 17 | chartConfig: PropTypes.object.isRequired, 18 | dataMax: PropTypes.number 19 | }; 20 | 21 | constructor(props) { 22 | super(props); 23 | this.state = { 24 | sort: props.chartConfig.sort, 25 | logScale: props.chartConfig.logScale, 26 | showJson: false, 27 | }; 28 | } 29 | 30 | UNSAFE_componentWillReceiveProps(nextProps) { 31 | if (nextProps.chartConfig.sort !== this.state.sort) { 32 | this.setState({ sort: nextProps.chartConfig.sort }); 33 | } 34 | if (nextProps.chartConfig.logScale !== this.state.logScale) { 35 | this.setState({ logScale: nextProps.chartConfig.logScale }); 36 | } 37 | } 38 | 39 | toggleSort() { 40 | this.setState({ 41 | sort: !this.state.sort 42 | }); 43 | } 44 | 45 | toggleLogScale() { 46 | this.setState({ 47 | logScale: !this.state.logScale 48 | }); 49 | } 50 | 51 | toggleShowJson() { 52 | this.setState({ 53 | showJson: !this.state.showJson 54 | }); 55 | } 56 | 57 | render() { 58 | 59 | const { benchmarkBundle, metricExtractor, dataMax } = this.props; 60 | const { sort, logScale, showJson } = this.state; 61 | const benchmarks = benchmarkBundle.allBenchmarks(); 62 | 63 | return ( 64 |
    65 | 66 | 67 | 68 | 69 | 70 |
    71 | 77 |
    78 | 81 | 82 |
    83 |
    { JSON.stringify(benchmarks, null, '\t') }
    84 | 87 |
    88 |
    89 |
    90 | ); 91 | } 92 | } 93 | 94 | -------------------------------------------------------------------------------- /src/javascript/components/single/SingleRunChartTooltip.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { BarChart, Bar, XAxis, YAxis, LabelList } from 'recharts'; 4 | import Table from 'react-bootstrap/lib/Table' 5 | 6 | import BarTooltipLabel from 'components/single/BarTooltipLabel.jsx'; 7 | import { round, formatNumber } from 'functions/util.js' 8 | import { blue, red } from 'functions/colors.js' 9 | 10 | 11 | 12 | export default class SingleRunChartTooltip extends Component { 13 | 14 | static propTypes = { 15 | label: PropTypes.any, 16 | paramNames: PropTypes.array, 17 | scoreUnit: PropTypes.string, 18 | roundScores: PropTypes.bool, 19 | payload: PropTypes.arrayOf(PropTypes.shape({ 20 | name: PropTypes.any, 21 | payload: PropTypes.any, 22 | value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), 23 | unit: PropTypes.any, 24 | })), 25 | }; 26 | 27 | render() { 28 | const { label, payload, scoreUnit, roundScores, paramNames } = this.props; 29 | if (!payload || payload.length == 0) { 30 | return null; 31 | } 32 | 33 | // Assemble table headers showing score, error, etc... 34 | const tableHeaders = []; 35 | if (payload.length > 1) { 36 | tableHeaders.push({ paramNames.join(':') }); 37 | } 38 | tableHeaders.push(Score); 39 | tableHeaders.push(Min); 40 | tableHeaders.push(Max); 41 | tableHeaders.push(Error); 42 | tableHeaders.push(Unit); 43 | 44 | // Assemble table rows showing score, error, etc... per bar 45 | const tableRows = payload.map(barPayload => { 46 | const score = formatNumber(barPayload.payload[barPayload.dataKey], roundScores); 47 | const minMax = barPayload.payload[barPayload.dataKey + 'MinMax']; 48 | const min = formatNumber(minMax[0], roundScores); 49 | const max = formatNumber(minMax[1], roundScores); 50 | const columnValues = []; 51 | if (payload.length > 1) { 52 | columnValues.push({ barPayload.dataKey }); 53 | } 54 | columnValues.push({ score }); 55 | columnValues.push({ min }); 56 | columnValues.push({ max }); 57 | columnValues.push({ formatNumber(barPayload.payload[barPayload.dataKey + 'Error'], roundScores) }); 58 | columnValues.push({ scoreUnit }); 59 | return { columnValues } 60 | }); 61 | 62 | //Assemble iteration charts showing the raw iteration data per run 63 | const iterationCharts = payload.map(barPayload => { 64 | const forkScores = barPayload.payload[barPayload.dataKey + 'SubScores']; 65 | const forkHistograms = barPayload.payload[barPayload.dataKey + 'SubScoresHistogram']; 66 | if (forkHistograms) { 67 | const tooltipWidth = forkHistograms[0].length * 54; 68 | return forkHistograms.filter((elem, i) => i < 2).map((forkData, forkIndex) => { 69 | const histogramCharts = forkData.filter((elem, sampleRunIndex) => sampleRunIndex < 2).map((sampleRuns, runIndex) => { 70 | const sampleRunData = sampleRuns.map(pair => { 71 | return { 72 | score: pair[0], 73 | occurence: pair[1] 74 | } 75 | } 76 | ); 77 | return 83 | 84 | 85 | 86 | 87 | }); 88 | return
    89 |
    90 |
    { `Fork ${forkIndex} / ${forkHistograms.length}` }
    91 |
    { histogramCharts }
    92 |
    { `Showing ${histogramCharts.length} runs from ${forkHistograms[forkIndex].length} ...` }
    93 |
    94 | }); 95 | } 96 | if (forkScores) { 97 | const forkScoreDatas = forkScores.map((iterationScoreArray) => { 98 | return iterationScoreArray.map((element) => { 99 | return { 100 | data: round(element, roundScores), 101 | } 102 | }) 103 | }); 104 | const tooltipWidth = forkScores[0].length * 54 105 | 106 | return forkScoreDatas.map((data, index) =>
    107 | 112 | 116 | 117 | 118 | 119 |
    120 | ) 121 | } 122 | return null; 123 | }); 124 | 125 | return ( 126 |
    127 |
    128 |

    { label }

    129 |
    130 | 135 | 136 | { tableHeaders } 137 | 138 | 139 | { tableRows } 140 | 141 |
    142 |
    143 |
    Raw Data
    144 |
    145 |
    146 | { iterationCharts } 147 |
    148 |
    149 |
    150 | ); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/javascript/components/single/SingleRunView.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import Badge from 'react-bootstrap/lib/Badge' 5 | 6 | import Toggle from 'react-toggle' 7 | import "react-toggle/style.css" 8 | 9 | import Tooltipped from 'components/lib/Tooltipped.jsx' 10 | import TocElement from 'components/TocElement.jsx' 11 | import SingleRunBundle from 'components/single/SingleRunBundle.jsx' 12 | import { getUniqueBenchmarkModesAccrossBundles } from 'functions/parse.js' 13 | 14 | export default class SingleRunView extends React.Component { 15 | 16 | static propTypes = { 17 | runName: PropTypes.string.isRequired, 18 | benchmarkBundles: PropTypes.array.isRequired, 19 | focusedBundles: PropTypes.object.isRequired, 20 | metricExtractor: PropTypes.object.isRequired, 21 | chartConfig: PropTypes.object.isRequired, 22 | }; 23 | 24 | constructor(props) { 25 | super(props); 26 | this.state = { 27 | axisScalesSync: true 28 | }; 29 | } 30 | 31 | changeScalesSync() { 32 | this.setState({ 33 | axisScalesSync: !this.state.axisScalesSync, 34 | }); 35 | } 36 | 37 | render() { 38 | const { runName, focusedBundles, benchmarkBundles, metricExtractor, chartConfig } = this.props; 39 | const { axisScalesSync } = this.state; 40 | 41 | let synchronizeAxisScalesToggle; 42 | let dataMax; 43 | if (focusedBundles.size > 1) { 44 | const benchmarkModes = getUniqueBenchmarkModesAccrossBundles(benchmarkBundles, metricExtractor); 45 | const axisScalesSyncPossible = benchmarkModes.length == 1; 46 | const switchTooltip = axisScalesSyncPossible ? `Sync Axis Scales: ${axisScalesSync ? 'on' : 'off'}` : `No Axis Scale syncing possible because of multiple benchmark modes: ${benchmarkModes}!`; 47 | synchronizeAxisScalesToggle =
    48 | 49 | 54 | 55 |
    ; 56 | if (axisScalesSync && axisScalesSyncPossible) { 57 | dataMax = 0; 58 | benchmarkBundles.forEach(benchmarkBundle => benchmarkBundle.allBenchmarks().forEach(benchmark => { 59 | dataMax = Math.max(dataMax, metricExtractor.extractMinMax(benchmark)[1]); 60 | })); 61 | } 62 | } 63 | 64 | const elements = []; 65 | elements.push( 66 |
    67 | 68 | { benchmarkBundles.length } 69 | 70 | { ` different benchmark classes for single run '${runName}' and metric '${metricExtractor.metricKey}' detected!` } 71 | { synchronizeAxisScalesToggle } 72 |
    73 | ); 74 | 75 | benchmarkBundles.forEach(bundle => { 76 | elements.push( 77 | 82 | ); 83 | }); 84 | 85 | 86 | return
    87 | { elements } 88 |
    89 | } 90 | 91 | } -------------------------------------------------------------------------------- /src/javascript/components/summary/SummaryChangeChart.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { ResponsiveContainer, Tooltip, Cell, PieChart, Pie } from 'recharts'; 4 | 5 | import { blue, green, red, tooltipBackground, yellow } from 'functions/colors.js' 6 | 7 | /* eslint react/prop-types: 0 */ 8 | // A Pie chart giving a quick overview on number of increases/decreases/no-change from run1 vs run2 9 | const SummaryChangeChart = ({ benchmarkDiffs, minDeviation }) => { 10 | 11 | const chartData = benchmarkDiffs.reduce((changeTracker, benchmarkDiff) => { 12 | if (benchmarkDiff.scoreDiff < -minDeviation) { 13 | changeTracker[0].count++; 14 | } else if (benchmarkDiff.scoreDiff > minDeviation) { 15 | changeTracker[2].count++; 16 | } else { 17 | changeTracker[1].count++; 18 | } 19 | return changeTracker; 20 | }, [ 21 | { 22 | name: `Declined (<-${minDeviation}%)`, 23 | count: 0 24 | }, 25 | { 26 | name: `Unchanged (+-${minDeviation}%)`, 27 | count: 0 28 | }, 29 | { 30 | name: `Improved (>+${minDeviation}%)`, 31 | count: 0 32 | } 33 | ]); 34 | 35 | return ( 36 | 37 | 38 | 50 | 51 | 52 | 53 | 54 | 59 | 60 | 61 | ) 62 | } 63 | 64 | export default SummaryChangeChart; 65 | -------------------------------------------------------------------------------- /src/javascript/components/summary/SummaryChangeLevelChart.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { ResponsiveContainer, Tooltip, Radar, RadarChart, PolarGrid, PolarAngleAxis, PolarRadiusAxis } from 'recharts'; 4 | 5 | import { green, red, yellow, tooltipBackground } from 'functions/colors.js' 6 | 7 | /* eslint react/prop-types: 0 */ 8 | // A Radar chart giving a quick overview on of the severity of all increases/decrease 9 | const SummaryChangeLevelChart = ({ minDeviation, benchmarkDiffs }) => { 10 | const benchmarkDiffLevels = benchmarkDiffs.reduce((levelTracker, benchmarkDiff) => { 11 | const levelOfImprovement = getLevelOfImprovement(minDeviation, benchmarkDiff.scoreDiff); 12 | const levelOfDecline = getLevelOfDecline(minDeviation, benchmarkDiff.scoreDiff); 13 | if (levelOfImprovement > 0) { 14 | levelTracker[`level${levelOfImprovement}`].increase++; 15 | } 16 | if (levelOfDecline > 0) { 17 | levelTracker[`level${levelOfDecline}`].decrease++; 18 | } 19 | return levelTracker; 20 | }, { 21 | level1: levelObject(minDeviation, 1), 22 | level2: levelObject(minDeviation, 2), 23 | level3: levelObject(minDeviation, 3), 24 | level4: levelObject(minDeviation, 4), 25 | level5: levelObject(minDeviation, 5), 26 | }); 27 | 28 | const data = Object.keys(benchmarkDiffLevels).map(key => benchmarkDiffLevels[key]); 29 | const cx = '50%'; 30 | const maxChangeCount = data.reduce((maxChange, levelObject) => { 31 | const change = Math.max(levelObject.increase, levelObject.decrease); 32 | return Math.max(maxChange, change); 33 | }, 0); 34 | 35 | return ( 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 48 | 49 | 50 | ) 51 | } 52 | 53 | export default SummaryChangeLevelChart; 54 | 55 | function levelObject(minDeviation, level) { 56 | const min = minDeviation + ((level - 1) * 10); 57 | return { 58 | name: `${min}+%`, 59 | increase: 0, 60 | decrease: 0, 61 | }; 62 | } 63 | 64 | function getLevelOfImprovement(minDeviation, scoreDiff) { 65 | if (scoreDiff < minDeviation) return 0; 66 | for (var level = 1; level < 6; level++) { 67 | const levelMax = minDeviation + level * 10; 68 | if (scoreDiff < levelMax) { 69 | return level; 70 | } 71 | } 72 | return 5; 73 | } 74 | 75 | function getLevelOfDecline(minDeviation, scoreDiff) { 76 | if (scoreDiff > -minDeviation) return 0; 77 | for (var level = 1; level < 6; level++) { 78 | const levelMax = -minDeviation - level * 10; 79 | if (scoreDiff > levelMax) { 80 | return level; 81 | } 82 | } 83 | return 5; 84 | } -------------------------------------------------------------------------------- /src/javascript/components/summary/SummaryHeader.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Grid from 'react-bootstrap/lib/Grid'; 4 | import Row from 'react-bootstrap/lib/Row'; 5 | import Col from 'react-bootstrap/lib/Col'; 6 | import Badge from 'react-bootstrap/lib/Badge'; 7 | 8 | import Slider from '@appigram/react-rangeslider'; 9 | import '@appigram/react-rangeslider/lib/index.css'; 10 | 11 | import SummaryChangeChart from 'components/summary/SummaryChangeChart.jsx'; 12 | import SummaryChangeLevelChart from 'components/summary/SummaryChangeLevelChart.jsx'; 13 | import SummaryHistogramChart from 'components/summary/SummaryHistogramChart.jsx'; 14 | 15 | /* eslint react/prop-types: 0 */ 16 | const SummaryHeader = ({ benchmarkDiffs, minDeviation, runName1, runName2, metricKey, numberOfBenchmarkBundles, changeMinDeviationFunction }) => { 17 | 18 | return ( 19 | 20 | 21 | 22 |
    23 | 24 | 25 |
    26 | Comparing 27 | { ' ' } 28 | 29 | { benchmarkDiffs.length } 30 | 31 | { ' results out of ' } 32 | 33 | { numberOfBenchmarkBundles } 34 | benchmark classes for ' 35 | { runName1 }' and ' 36 | { runName2 }' on metric ' 37 | { metricKey }'. 38 | 39 | 40 |
    41 | Ignoring deviations below 42 | { ' ' + minDeviation }% 43 |
    44 | value + '%' } 52 | /> 53 | 54 | 55 |
    56 |
    57 | ); 58 | } 59 | 60 | export default SummaryHeader; -------------------------------------------------------------------------------- /src/javascript/components/summary/SummaryHistogramChart.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import { ResponsiveContainer, BarChart, Bar, ReferenceLine, XAxis, YAxis, CartesianGrid, Tooltip, Legend, Cell, Surface, Symbols } from 'recharts'; 5 | 6 | import { green, red, blue, yellow } from 'functions/colors.js' 7 | 8 | /* eslint react/prop-types: 0 */ 9 | class SummaryHistogramChart extends React.Component { 10 | 11 | static propTypes = { 12 | benchmarkDiffs: PropTypes.array.isRequired, 13 | }; 14 | 15 | constructor(props) { 16 | super(props); 17 | this.state = { 18 | disabledLabels: ['errorDiff'] 19 | }; 20 | } 21 | 22 | switchLabelActivation(dataKey) { 23 | if (this.state.disabledLabels.includes(dataKey)) { 24 | this.setState({ 25 | disabledLabels: this.state.disabledLabels.filter(obj => obj !== dataKey) 26 | }); 27 | } else { 28 | this.setState({ disabledLabels: this.state.disabledLabels.concat(dataKey) }); 29 | } 30 | } 31 | 32 | render() { 33 | const { benchmarkDiffs } = this.props; 34 | const { disabledLabels } = this.state; 35 | 36 | const data = benchmarkDiffs.map((benchmarkDiff, i) => ({ 37 | idx: i, 38 | name: `${benchmarkDiff.bundleName}#${benchmarkDiff.benchmarkMethod.name}(${benchmarkDiff.benchmarkMethod.params ? benchmarkDiff.benchmarkMethod.params.map(param => param[0] + '=' + param[1]).join(':') : ''})`, 39 | scoreDiff: Math.max(-100, Math.min(100, benchmarkDiff.scoreDiff)), 40 | errorDiff: Math.max(-100, Math.min(100, benchmarkDiff.scoreErrorDiff)), 41 | score1stRun: benchmarkDiff.score1stRun, 42 | score2ndRun: benchmarkDiff.score2ndRun, 43 | scoreError1stRun: benchmarkDiff.scoreError1stRun, 44 | scoreError2ndRun: benchmarkDiff.scoreError2ndRun, 45 | scoreUnit: benchmarkDiff.scoreUnit 46 | })); 47 | 48 | const dataSets = [{ dataKey: 'scoreDiff', color: green }, { dataKey: 'errorDiff', color: blue }]; 49 | 50 | return ( 51 | 52 | 58 | 59 | 60 | 61 | data[idx] ? data[idx].name : 'N/A' } 65 | formatter={ tooltipFormat } /> 66 | 72 | 73 | { 74 | dataSets.filter(elem => !disabledLabels.includes(elem.dataKey)).map(activeElem => 75 | 76 | { data.map(entry => ( 77 | 78 | )) } 79 | 80 | ) 81 | } 82 | 83 | 84 | ) 85 | } 86 | 87 | barColor(dataKey, dataEntry) { 88 | if (dataKey === 'scoreDiff') { 89 | if (dataEntry.scoreDiff > 0) { 90 | return green; 91 | } else { 92 | return red; 93 | } 94 | } else { 95 | if (dataEntry.errorDiff > 0) { 96 | return blue; 97 | } else { 98 | return yellow; 99 | } 100 | } 101 | } 102 | 103 | renderCusomizedLegend({ payload }) { 104 | return ( 105 |
    106 | { payload.map(entry => { 107 | const { dataKey, color } = entry; 108 | const active = this.state.disabledLabels.includes(dataKey); 109 | const style = { 110 | marginRight: 10, 111 | color: active ? "#AAA" : "#000" 112 | }; 113 | 114 | return ( 115 | this.switchLabelActivation(dataKey) } style={ style }> 116 | 117 | 118 | { active && () } 119 | 120 | { dataKey } 121 | 122 | ); 123 | }) } 124 |
    125 | ); 126 | } 127 | } 128 | 129 | export default SummaryHistogramChart; 130 | 131 | function tooltipFormat(value, name, props) { 132 | const { payload } = props; 133 | const valueString = value != null ? value : 'N/A'; 134 | let rawValueString; 135 | if (name === 'scoreDiff') { 136 | rawValueString = `${payload.score1stRun.toLocaleString()} | ${payload.score2ndRun.toLocaleString()} ${payload.scoreUnit}`; 137 | } else { 138 | rawValueString = `${payload.scoreError1stRun.toLocaleString()} | ${payload.scoreError2ndRun.toLocaleString()}`; 139 | } 140 | return `${valueString}% (${rawValueString})`; 141 | } 142 | 143 | -------------------------------------------------------------------------------- /src/javascript/components/summary/SummaryTable.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Table from 'react-bootstrap/lib/Table' 4 | 5 | import UpIcon from 'react-icons/lib/fa/location-arrow' 6 | 7 | import { actions } from 'store/store.js' 8 | import { green, red, yellow } from 'functions/colors.js' 9 | import Tooltipped from 'components/lib/Tooltipped.jsx' 10 | 11 | /* eslint react/prop-types: 0 */ 12 | const SummaryTable = ({ name, benchmarkDiffs, lastRunIndex }) => { 13 | 14 | if (benchmarkDiffs.length == 0) { 15 | return null; 16 | } 17 | 18 | const headers = ['#', 'Benchmark', 'Params', 'Mode/Unit', 'Score', 'Error'].map(header => { header }) 19 | 20 | const rows = benchmarkDiffs.map((benchmarkDiff, i) => { 21 | let color; 22 | let icon; 23 | if (benchmarkDiff.scoreDiff == 0) { 24 | color = yellow; 25 | icon = ; 26 | } else if (benchmarkDiff.scoreDiff > 0) { 27 | color = green; 28 | icon = ; 29 | } else { 30 | color = red; 31 | icon = ; 32 | } 33 | 34 | return 35 | { i } 36 | 37 | 38 | actions.detailBenchmarkBundle(benchmarkDiff.bundleKey) }> 40 | { benchmarkDiff.bundleName + ' - ' + benchmarkDiff.benchmarkMethod.name + ' ' } 41 | { icon } { benchmarkDiff.scoreDiff }% 42 | 43 | 44 | 45 | 46 | { benchmarkDiff.benchmarkMethod.params ? benchmarkDiff.benchmarkMethod.params.map(param => param[0] + '=' + param[1]).join(':') : '' } 47 | 48 | 49 | { benchmarkDiff.benchmarkMethod.benchmarks[lastRunIndex].mode + ' in ' + benchmarkDiff.scoreUnit } 50 | 51 | 52 |
    { benchmarkDiff.score1stRun.toLocaleString() }
    53 |
    { benchmarkDiff.score2ndRun.toLocaleString() }
    54 | 55 | 56 |
    { benchmarkDiff.scoreError1stRun.toLocaleString() }
    57 |
    { benchmarkDiff.scoreError2ndRun.toLocaleString() }
    58 | 59 | 60 | }); 61 | 62 | return ( 63 |
    64 |

    { `${name} (${benchmarkDiffs.length})` }

    65 | 66 | 67 | { headers } 68 | 69 | 70 | { rows } 71 | 72 |
    73 |
    74 | ); 75 | } 76 | 77 | export default SummaryTable; -------------------------------------------------------------------------------- /src/javascript/components/summary/SummaryView.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import SummaryHeader from 'components/summary/SummaryHeader.jsx' 5 | import SummaryTable from 'components/summary/SummaryTable.jsx' 6 | import { getMetricType } from 'models/MetricType.js' 7 | import { flatten } from 'functions/util.js' 8 | import { shouldRound, round } from 'functions/util.js' 9 | 10 | 11 | export default class SummaryView extends React.Component { 12 | 13 | static propTypes = { 14 | runNames: PropTypes.array.isRequired, 15 | benchmarkBundles: PropTypes.array.isRequired, 16 | runIndex: PropTypes.array.isRequired, 17 | minDeviation: PropTypes.number.isRequired, 18 | metricExtractor: PropTypes.object.isRequired, 19 | }; 20 | 21 | constructor(props) { 22 | super(props); 23 | this.state = { 24 | minDeviation: this.props.minDeviation 25 | }; 26 | } 27 | 28 | changeMinDeviation(number) { 29 | if (number != this.state.minDeviation) { 30 | this.setState({ 31 | minDeviation: number 32 | }); 33 | } 34 | } 35 | 36 | render() { 37 | const { runNames, runIndex, benchmarkBundles, metricExtractor } = this.props; 38 | const { minDeviation } = this.state; 39 | 40 | const benchmarkDiffs = flatten(benchmarkBundles.map(benchmarkBundle => benchmarkBundle.benchmarkMethods.map(benchmarkMethod => { 41 | const shouldRoundScores = shouldRound(benchmarkBundle.benchmarkMethods, metricExtractor); 42 | const firstRunBenchmark = benchmarkMethod.benchmarks[runIndex[0]]; 43 | const secondRunBenchmark = benchmarkMethod.benchmarks[runIndex[1]]; 44 | 45 | if (firstRunBenchmark && secondRunBenchmark && metricExtractor.hasMetric(firstRunBenchmark) && metricExtractor.hasMetric(secondRunBenchmark)) { 46 | const metricType = getMetricType(metricExtractor.extractType(firstRunBenchmark)); 47 | const score1stRun = round(metricExtractor.extractScore(firstRunBenchmark), shouldRoundScores); 48 | const score2ndRun = round(metricExtractor.extractScore(secondRunBenchmark), shouldRoundScores); 49 | const scoreError1stRun = round(metricExtractor.extractScoreError(firstRunBenchmark), shouldRoundScores); 50 | const scoreError2ndRun = round(metricExtractor.extractScoreError(secondRunBenchmark), shouldRoundScores); 51 | const scoreUnit = metricExtractor.extractScoreUnit(firstRunBenchmark); 52 | 53 | let scoreDiff; 54 | if (metricType && metricType.increaseIsGood) { 55 | // i.e. for throughput decrease is an increase, its worse basically 56 | scoreDiff = round((score2ndRun - score1stRun) / score1stRun * 100, shouldRoundScores); 57 | } else { 58 | scoreDiff = round((score1stRun - score2ndRun) / score2ndRun * 100, shouldRoundScores); 59 | } 60 | let scoreErrorDiff = round((scoreError1stRun - scoreError2ndRun) / scoreError2ndRun * 100, shouldRoundScores); 61 | 62 | return { 63 | bundleKey: benchmarkBundle.key, 64 | bundleName: benchmarkBundle.name, 65 | benchmarkMethod: benchmarkMethod, 66 | score1stRun: score1stRun, 67 | score2ndRun: score2ndRun, 68 | scoreError1stRun: scoreError1stRun, 69 | scoreError2ndRun: scoreError2ndRun, 70 | scoreUnit: scoreUnit, 71 | scoreDiff: scoreDiff, 72 | scoreErrorDiff: scoreErrorDiff 73 | } 74 | } 75 | }).filter(element => element !== undefined))); 76 | 77 | const improvedBenchmarkDiffs = benchmarkDiffs.filter(element => (element.scoreDiff != 0 && element.scoreDiff >= minDeviation)).sort((a, b) => b.scoreDiff - a.scoreDiff); 78 | const declinedBenchmarkDiffs = benchmarkDiffs.filter(element => (element.scoreDiff != 0 && element.scoreDiff <= -minDeviation)).sort((a, b) => a.scoreDiff - b.scoreDiff); 79 | const unchangedBenchmarkDiffs = benchmarkDiffs.filter(element => (element.scoreDiff == 0 || (element.scoreDiff < minDeviation && element.scoreDiff > -minDeviation))).sort((a, b) => b.scoreDiff - a.scoreDiff); 80 | 81 | return ( 82 |
    83 | 92 |
    93 | 94 | 95 | 96 |
    ) 97 | } 98 | 99 | } -------------------------------------------------------------------------------- /src/javascript/components/two/DiffBarChartView.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { ResponsiveContainer, BarChart, XAxis, YAxis, Tooltip, CartesianGrid, Legend, Bar, LabelList, ReferenceLine, Cell } from 'recharts'; 4 | 5 | import { createDataSetFromBenchmarks } from 'components/two/DiffBarDataSet.js' 6 | import DiffLabel from 'components/two/DiffLabel.jsx'; 7 | import TwoRunsChartTooltip from 'components/two/TwoRunsChartTooltip.jsx'; 8 | import { red, green, yellow, tooltipBackground } from 'functions/colors.js' 9 | 10 | // Chart showing increase/decrease in % for the benchmarks of a class from 2 runs. 11 | export default class DiffBarChartView extends React.Component { 12 | 13 | static propTypes = { 14 | runNames: PropTypes.array.isRequired, 15 | benchmarkBundle: PropTypes.object.isRequired, 16 | metricExtractor: PropTypes.object.isRequired, 17 | sort: PropTypes.bool.isRequired, 18 | }; 19 | 20 | render() { 21 | const { runNames, benchmarkBundle, metricExtractor, sort } = this.props; 22 | const dataSet = createDataSetFromBenchmarks(benchmarkBundle, metricExtractor, sort); 23 | const maxMethodNameLength = dataSet.data.map((element) => element.name.length).reduce((previous, current) => Math.max(previous, current), 32); 24 | const chartHeight = 100 + dataSet.data.length * 45; 25 | 26 | if (dataSet.data.length == 0) { 27 | return
    No data for comparision!
    28 | } 29 | 30 | return ( 31 | 32 | 38 | 39 | 40 | 41 | 42 | } cursor={ { stroke: green, strokeWidth: 2 } } wrapperStyle={ { backgroundColor: tooltipBackground, opacity: 0.95 } } /> 43 | 44 | 50 | { dataSet.data.map((entry, index) => { 51 | const color = dataSet.data[index].scoreDiff > 0 ? green : red; 52 | return 53 | }) } 54 | 55 | 56 | 57 | 58 | ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/javascript/components/two/DiffBarDataSet.js: -------------------------------------------------------------------------------- 1 | import { getMetricType } from 'models/MetricType.js' 2 | 3 | import { shouldRound, round } from 'functions/util.js' 4 | 5 | export function createDataSetFromBenchmarks(benchmarkBundle, metricExtractor, sort) { 6 | 7 | const shouldRoundScores = shouldRound(benchmarkBundle.benchmarkMethods, metricExtractor); 8 | const data = benchmarkBundle.benchmarkMethods.map((benchmarkMethod, i) => { 9 | const firstRunBenchmark = benchmarkMethod.benchmarks[0]; 10 | const secondRunBenchmark = benchmarkMethod.benchmarks[1]; 11 | 12 | if (firstRunBenchmark && secondRunBenchmark && metricExtractor.hasMetric(firstRunBenchmark) && metricExtractor.hasMetric(secondRunBenchmark)) { 13 | const scoreUnit = metricExtractor.extractScoreUnit(firstRunBenchmark); 14 | const metricType = getMetricType(metricExtractor.extractType(firstRunBenchmark)); 15 | const score1stRun = round(metricExtractor.extractScore(firstRunBenchmark), shouldRoundScores); 16 | const score2ndRun = round(metricExtractor.extractScore(secondRunBenchmark), shouldRoundScores); 17 | const scoreError1stRun = round(metricExtractor.extractScoreError(firstRunBenchmark), shouldRoundScores); 18 | const scoreError2ndRun = round(metricExtractor.extractScoreError(secondRunBenchmark), shouldRoundScores); 19 | 20 | let scoreDiff; 21 | if (metricType && metricType.increaseIsGood) { 22 | // i.e. for throughput decrease is an increase, its worse basically 23 | scoreDiff = round((score2ndRun - score1stRun) / score1stRun * 100, shouldRoundScores); 24 | } else { 25 | scoreDiff = round((score1stRun - score2ndRun) / score2ndRun * 100, shouldRoundScores); 26 | } 27 | 28 | return { 29 | index: i, 30 | name: benchmarkMethod.key, 31 | scoreDiff: scoreDiff, 32 | scoreUnit: scoreUnit, 33 | score1stRun: score1stRun, 34 | score2ndRun: score2ndRun, 35 | scoreError1stRun: scoreError1stRun, 36 | scoreError2ndRun: scoreError2ndRun, 37 | } 38 | } 39 | }).filter((element) => element !== undefined); 40 | 41 | if (sort) { 42 | data.sort((a, b) => b.scoreDiff - a.scoreDiff); 43 | } 44 | 45 | return { 46 | data: data, 47 | roundScores: shouldRoundScores, 48 | } 49 | } -------------------------------------------------------------------------------- /src/javascript/components/two/DiffLabel.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import { yellow } from 'functions/colors.js' 5 | 6 | export default function DiffLabel({ x, y, width, height, value, textAnchor }) { 7 | const xPosShift = value > 0 ? 6 : - (value.toString().length * 7); 8 | const xPos = x + width + xPosShift; 9 | return ( 10 | 11 | 21 | { value } 22 | 23 | 24 | ); 25 | } 26 | 27 | DiffLabel.propTypes = { 28 | x: PropTypes.number.isRequired, 29 | y: PropTypes.number.isRequired, 30 | width: PropTypes.number.isRequired, 31 | height: PropTypes.number.isRequired, 32 | value: PropTypes.string.isRequired, 33 | textAnchor: PropTypes.string.isRequired, 34 | } -------------------------------------------------------------------------------- /src/javascript/components/two/TwoRunBundle.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import Collapse from 'react-bootstrap/lib/Collapse' 5 | import Button from 'react-bootstrap/lib/Button' 6 | 7 | import ChartHeader from 'components/ChartHeader.jsx' 8 | import { DetailsButton, SortButton } from 'components/Icons.jsx' 9 | import DiffBarChartView from 'components/two/DiffBarChartView.jsx' 10 | 11 | // The view for a bunch of benchmarks, usually all of a benchmark class 12 | export default class TwoRunBundle extends React.Component { 13 | 14 | static propTypes = { 15 | runNames: PropTypes.array.isRequired, 16 | benchmarkBundle: PropTypes.object.isRequired, 17 | metricExtractor: PropTypes.object.isRequired, 18 | chartConfig: PropTypes.object.isRequired, 19 | }; 20 | 21 | constructor(props) { 22 | super(props); 23 | this.state = { 24 | sort: props.chartConfig.sort, 25 | showJson1: false, 26 | showJson2: false, 27 | }; 28 | } 29 | 30 | UNSAFE_componentWillReceiveProps(nextProps) { 31 | if (nextProps.chartConfig.sort !== this.state.sort) { 32 | this.setState({ sort: nextProps.chartConfig.sort }); 33 | } 34 | } 35 | 36 | 37 | toggleSort() { 38 | this.setState({ 39 | sort: !this.state.sort 40 | }); 41 | } 42 | 43 | toggleShowJson1() { 44 | this.setState({ 45 | showJson1: !this.state.showJson1, 46 | showJson2: false 47 | }); 48 | } 49 | 50 | toggleShowJson2() { 51 | this.setState({ 52 | showJson1: false, 53 | showJson2: !this.state.showJson2 54 | }); 55 | } 56 | 57 | render() { 58 | const { runNames, benchmarkBundle, metricExtractor } = this.props; 59 | const { sort, showJson1, showJson2 } = this.state; 60 | 61 | const benchmarks1 = benchmarkBundle.benchmarksFromRun(0); 62 | const benchmarks2 = benchmarkBundle.benchmarksFromRun(1); 63 | let newBenchmarks = []; 64 | let removedBenchmarks = []; 65 | let hasSomethingToCompare = false; 66 | benchmarkBundle.benchmarkMethods.forEach(benchmarkMethod => { 67 | if (benchmarkMethod.benchmarks[0] === null || !metricExtractor.hasMetric(benchmarkMethod.benchmarks[0])) { 68 | newBenchmarks.push(benchmarkMethod.name); 69 | } else if (benchmarkMethod.benchmarks[1] === null || !metricExtractor.hasMetric(benchmarkMethod.benchmarks[1])) { 70 | removedBenchmarks.push(benchmarkMethod.name); 71 | } else { 72 | hasSomethingToCompare = true; 73 | } 74 | }); 75 | 76 | var scoresChart = hasSomethingToCompare ? 77 | : null; 83 | 84 | return ( 85 |
    86 | 87 | 88 | 89 | 90 |
    91 | { scoresChart } 92 |
    93 | { removedBenchmarks.length > 0 && 94 |
    95 | Removed benchmarks: 96 | { ' ' + removedBenchmarks.join(', ') } 97 |
    98 |
    99 |
    } 100 | { newBenchmarks.length > 0 && 101 |
    102 | New benchmarks: 103 | { ' ' + newBenchmarks.join(', ') } 104 |
    105 |
    106 |
    } 107 | 110 | 113 | 114 |
    115 |
    { JSON.stringify(benchmarks1, null, '\t') }
    116 | 119 |
    120 |
    121 | 122 |
    123 |
    { JSON.stringify(benchmarks2, null, '\t') }
    124 | 127 |
    128 |
    129 |
    130 | ); 131 | } 132 | } 133 | 134 | -------------------------------------------------------------------------------- /src/javascript/components/two/TwoRunsChartTooltip.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Table from 'react-bootstrap/lib/Table' 4 | 5 | import { formatNumber } from 'functions/util.js' 6 | import { blue, red, green } from 'functions/colors.js' 7 | 8 | export default class TwoRunsChartTooltip extends Component { 9 | 10 | static propTypes = { 11 | label: PropTypes.any, 12 | runNames: PropTypes.array, 13 | roundScores: PropTypes.bool, 14 | payload: PropTypes.arrayOf(PropTypes.shape({ 15 | name: PropTypes.any, 16 | payload: PropTypes.any, 17 | value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), 18 | unit: PropTypes.any, 19 | })), 20 | }; 21 | 22 | render() { 23 | const { label, payload, runNames, roundScores } = this.props; 24 | if (payload == null || payload.length == 0) { 25 | return null; 26 | } 27 | const score1 = payload[0].payload.score1stRun; 28 | const score2 = payload[0].payload.score2ndRun; 29 | const scoreError1 = payload[0].payload.scoreError1stRun; 30 | const scoreError2 = payload[0].payload.scoreError2ndRun; 31 | const scoreChange = score2 - score1; 32 | const scoreErrorChange = scoreError2 - scoreError1; 33 | const scoreUnit = payload[0].payload.scoreUnit; 34 | 35 | return ( 36 |
    37 |
    38 |

    { label }

    39 |
    0 ? green : red } }> { payload[0].payload.scoreDiff + ' %' }
    40 |
    41 |
    42 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 |
    RunScoreErrorUnit
    { runNames[0] }{ formatNumber(score1, roundScores) }{ formatNumber(scoreError1, roundScores) }{ scoreUnit }
    { runNames[1] }{ formatNumber(score2, roundScores) }{ formatNumber(scoreError2, roundScores) }{ scoreUnit }
    Change{ (scoreChange > 0 ? '+' : '') + formatNumber(scoreChange, roundScores) }{ (scoreErrorChange > 0 ? '+' : '') + formatNumber(scoreErrorChange, roundScores) }{ scoreUnit }
    76 |
    77 |
    78 | ); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/javascript/components/two/TwoRunsHistogramChart.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import { ResponsiveContainer, BarChart, XAxis, YAxis, Tooltip, Bar, ReferenceLine } from 'recharts'; 5 | 6 | import { getMetricType } from 'models/MetricType.js' 7 | import { flatten } from 'functions/util.js' 8 | import { shouldRound, round } from 'functions/util.js' 9 | import { blue, green, yellow, tooltipBackground } from 'functions/colors.js' 10 | 11 | // Shows a histogram as bar-charts with change bars from -100% to +100% 12 | export default class TwoRunsHistogramChart extends React.Component { 13 | 14 | static propTypes = { 15 | benchmarkBundles: PropTypes.array.isRequired, 16 | metricExtractor: PropTypes.object.isRequired, 17 | }; 18 | 19 | render() { 20 | const { benchmarkBundles, metricExtractor } = this.props; 21 | 22 | const benchmarkDiffs = flatten(benchmarkBundles.map(benchmarkBundle => benchmarkBundle.benchmarkMethods.map((benchmarkMethod, i) => { 23 | const shouldRoundScores = shouldRound(benchmarkBundle.benchmarkMethods, metricExtractor); 24 | let benchmarkKey = benchmarkBundle.key + '#' + benchmarkMethod.name; 25 | if (benchmarkMethod.params) { 26 | benchmarkKey += ' [' + benchmarkMethod.params.map(param => param[0] + '=' + param[1]).join(':') + ']'; 27 | } 28 | 29 | const firstRunBenchmark = benchmarkMethod.benchmarks[0]; 30 | const secondRunBenchmark = benchmarkMethod.benchmarks[1]; 31 | 32 | if (firstRunBenchmark && secondRunBenchmark && metricExtractor.hasMetric(firstRunBenchmark) && metricExtractor.hasMetric(secondRunBenchmark)) { 33 | const metricType = getMetricType(metricExtractor.extractType(firstRunBenchmark)); 34 | const score1stRun = round(metricExtractor.extractScore(firstRunBenchmark), shouldRoundScores); 35 | const score2ndRun = round(metricExtractor.extractScore(secondRunBenchmark), shouldRoundScores); 36 | 37 | let scoreDiff; 38 | if (metricType && metricType.increaseIsGood) { 39 | // i.e. for throughput decrease is an increase, its worse basically 40 | scoreDiff = round((score2ndRun - score1stRun) / score1stRun * 100, shouldRoundScores); 41 | } else { 42 | scoreDiff = round((score1stRun - score2ndRun) / score2ndRun * 100, shouldRoundScores); 43 | } 44 | 45 | return { 46 | index: i, 47 | key: benchmarkKey, 48 | scoreDiff: scoreDiff, 49 | } 50 | } 51 | }).filter((element) => element !== undefined))); 52 | 53 | const percentFrequencies = {}; 54 | benchmarkDiffs.forEach(obj => { 55 | const percentChange = Math.ceil(obj.scoreDiff / 10); 56 | if (percentFrequencies[percentChange]) { 57 | percentFrequencies[percentChange].count++; 58 | percentFrequencies[percentChange].benchmarks.push(obj.key); 59 | } else { 60 | percentFrequencies[percentChange] = { 61 | count: 1, 62 | benchmarks: [obj.key] 63 | }; 64 | } 65 | }); 66 | // console.debug(percentFrequencies); 67 | 68 | const data = []; 69 | for (let i = -100; i <= 100; i += 10) { 70 | if (i == 0) { 71 | data.push({ 72 | scoreDiff: i, 73 | count: 0, 74 | benchmarks: [] 75 | }); 76 | } else { 77 | const freq = percentFrequencies[i / 10]; 78 | data.push({ 79 | scoreDiff: i > 0 ? i - 5 : i + 5, 80 | count: freq ? freq.count : 0, 81 | benchmarks: freq ? freq.benchmarks : [] 82 | }); 83 | } 84 | } 85 | 86 | const chartHeight = 108; 87 | 88 | const tickFormatter = (value) => { 89 | if (value == -100 || value == 100 || percentFrequencies[value / 10]) { 90 | return value + '%'; 91 | } 92 | return null; 93 | }; 94 | 95 | return ( 96 |
    97 | 98 | 99 | 106 | 107 | 108 | } cursor={ { stroke: green, strokeWidth: 2 } } wrapperStyle={ { backgroundColor: tooltipBackground, opacity: 0.95 } } /> 109 | }> 116 | 117 | 118 | 119 |
    120 | ) 121 | } 122 | } 123 | 124 | function BarLabel(props) { 125 | const { payload, textAnchor, x, y, width, height } = props; // eslint-disable-line react/prop-types 126 | 127 | if (payload.count > 0) { 128 | return ( 129 | 137 | { payload.count } 138 | 139 | ); 140 | } else { 141 | return ; 142 | } 143 | } 144 | 145 | BarLabel.PropTypes = { 146 | payload: PropTypes.array.isRequired, 147 | } 148 | 149 | 150 | class ChartTooltip extends React.Component { 151 | 152 | static propTypes = { 153 | label: PropTypes.any, 154 | payload: PropTypes.arrayOf(PropTypes.shape({ 155 | name: PropTypes.any, 156 | color: PropTypes.any, 157 | payload: PropTypes.any, 158 | value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), 159 | unit: PropTypes.any, 160 | })), 161 | }; 162 | 163 | render() { 164 | const { payload } = this.props; 165 | if (payload.length == 0) { 166 | return null; 167 | } 168 | const benchmarks = payload[0].payload.benchmarks; 169 | const benchmarksComponents = benchmarks.map(benchmark =>
    170 | { benchmark } 171 |
    ); 172 | 173 | return ( 174 |
    175 |
    176 |
    { payload[0].name }
    177 |
    178 |
    179 | { benchmarksComponents } 180 |
    181 |
    182 | ); 183 | } 184 | } 185 | 186 | -------------------------------------------------------------------------------- /src/javascript/components/two/TwoRunsView.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import Badge from 'react-bootstrap/lib/Badge' 5 | 6 | import TocElement from 'components/TocElement.jsx' 7 | import TwoRunBundle from 'components/two/TwoRunBundle.jsx' 8 | 9 | export default class TwoRunsView extends React.Component { 10 | 11 | static propTypes = { 12 | runNames: PropTypes.array.isRequired, 13 | benchmarkBundles: PropTypes.array.isRequired, 14 | metricExtractor: PropTypes.object.isRequired, 15 | chartConfig: PropTypes.object.isRequired, 16 | }; 17 | 18 | render() { 19 | const { runNames, benchmarkBundles, metricExtractor, chartConfig } = this.props; 20 | 21 | const elements = []; 22 | elements.push( 23 |
    24 | Comparing 25 | { ' ' } 26 | 27 | { benchmarkBundles.length } 28 | benchmark classes for ' 29 | { runNames[0] }' and ' 30 | { runNames[1] }' on metric ' 31 | { metricExtractor.metricKey }'. 32 |
    33 | ); 34 | 35 | benchmarkBundles.forEach(benchmarkBundle => { 36 | elements.push( 37 | 43 | ); 44 | }); 45 | 46 | 47 | return
    48 | { elements } 49 |
    50 | } 51 | 52 | } -------------------------------------------------------------------------------- /src/javascript/entry.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import { Provider } from 'store/store.js' 5 | import App from 'components/App.jsx'; 6 | 7 | import 'bootstrap/dist/css/bootstrap.css'; 8 | import '../css/common.css'; 9 | import '../css/sidenavi.css'; 10 | 11 | ReactDOM.render( 12 | 13 | 14 | 15 | , document.getElementById('main')); -------------------------------------------------------------------------------- /src/javascript/functions/charts.js: -------------------------------------------------------------------------------- 1 | 2 | export const tickFormatter = (tick) => { 3 | return shortenLargeNumber(tick, 20); 4 | } 5 | 6 | function shortenLargeNumber(num, digits) { 7 | var units = ['k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'], 8 | decimal; 9 | 10 | for (var i = units.length - 1; i >= 0; i--) { 11 | decimal = Math.pow(1000, i + 1); 12 | 13 | if (num <= -decimal || num >= decimal) { 14 | return +(num / decimal).toFixed(digits) + units[i]; 15 | } 16 | } 17 | 18 | return num; 19 | } -------------------------------------------------------------------------------- /src/javascript/functions/colors.js: -------------------------------------------------------------------------------- 1 | export const blue = '#337ab7'; 2 | export const lightBlue = '#22527b'; 3 | export const red = '#b74233'; 4 | export const green = '#4B993F'; 5 | export const yellow = '#b7a533'; 6 | export const lightBlack = '#143049'; 7 | export const brown = '#b77033'; 8 | 9 | //taken from http://www.color-hex.com/color/337ab7 => 3 tints from related colors 10 | export const blues = [lightBlue, blue, '#63a0d4']; 11 | export const greens = ['#7ab733', '#a1cc70', '#c9e2ad']; 12 | export const browns = [brown, '#cc9a70', '#e2c5ad']; 13 | export const violets = ['#b7337a', '#cc70a1', '#e9c1d7']; 14 | export const yellows = [yellow, '#ccc970', '#e2e0ad']; 15 | 16 | export const reds = ['#7b2c22', red, '#d47063'] 17 | 18 | 19 | export const barColors = [ 20 | ...blues, 21 | ...greens, 22 | ...browns, 23 | ...violets, 24 | ...yellows, 25 | ...reds, 26 | ]; 27 | 28 | export const lineColors = []; 29 | 30 | for (var i = 0; i <= 3; i++) { 31 | lineColors.push(blues[i]); 32 | lineColors.push(greens[i]); 33 | lineColors.push(browns[i]); 34 | lineColors.push(violets[i]); 35 | lineColors.push(yellows[i]); 36 | } 37 | 38 | export const tooltipBackground = '#efefef'; -------------------------------------------------------------------------------- /src/javascript/functions/parse.js: -------------------------------------------------------------------------------- 1 | import BenchmarkBundle from 'models/BenchmarkBundle.js'; 2 | import BenchmarkMethod from 'models/BenchmarkMethod.js'; 3 | 4 | //TODO cleanup 5 | 6 | // Extracts the benchmarks class name 7 | export function parseClassName(benchmark) { 8 | return parseClassNameFromFullName(benchmark.benchmark); 9 | } 10 | 11 | export function parseFullClassName(benchmark) { 12 | const nameParts = benchmark.benchmark.split('.'); 13 | nameParts.pop(); //remove the method part 14 | return nameParts.join('.'); 15 | } 16 | 17 | export function parseClassNameFromFullName(fullName) { 18 | return fullName.split('.').reverse()[0]; 19 | } 20 | 21 | // Extracts the benchmarks method name 22 | export function parseMethodName(benchmark) { 23 | const splitted = benchmark.benchmark.split('.'); 24 | return splitted[splitted.length - 1]; 25 | } 26 | 27 | // Extracts the benchmarks method name 28 | export function parseBenchmarkName(benchmark) { 29 | var benchmarkName = parseMethodName(benchmark); 30 | if (benchmark.params) { 31 | const keys = Object.keys(benchmark.params); 32 | keys.forEach(key => { 33 | benchmarkName += ' ' + key + '=' + benchmark.params[key]; 34 | }); 35 | } 36 | return benchmarkName; 37 | } 38 | 39 | export function getUniqueParamValues(benchmarks, paramName) { 40 | const paramValues = new Set(); 41 | benchmarks.forEach(benchmark => { 42 | paramValues.add(benchmark.params[paramName]); 43 | }); 44 | return Array.from(paramValues); 45 | } 46 | 47 | 48 | export function getUniqueBenchmarkModes(benchmarkBundle, metricExtractor) { 49 | const modes = new Set(); 50 | benchmarkBundle.allBenchmarks().forEach(benchmark => { 51 | modes.add(metricExtractor.extractType(benchmark)); 52 | }); 53 | return Array.from(modes); 54 | } 55 | 56 | export function getUniqueBenchmarkModesAccrossBundles(benchmarkBundles, metricExtractor) { 57 | const modes = new Set(); 58 | benchmarkBundles.forEach(benchmarkBundle => benchmarkBundle.allBenchmarks().forEach(benchmark => { 59 | modes.add(metricExtractor.extractType(benchmark)); 60 | })); 61 | return Array.from(modes); 62 | } 63 | 64 | export function parseBenchmarkBundles(benchmarkRuns) { 65 | const classToBenchmarksMap = parseMultiRunBenchmarkMap(benchmarkRuns); 66 | const benchmarkBundles = []; 67 | for (let [fullName, benchmarkRunMap] of classToBenchmarksMap) { 68 | const benchmarkMethods = []; 69 | const methodNames = new Set(); 70 | for (let [key, benchmarks] of benchmarkRunMap) { 71 | const keyParts = key.split(' '); 72 | const methodName = keyParts.shift(); 73 | const params = keyParts.length > 0 ? keyParts.map(paramString => paramString.split('=')) : null; 74 | benchmarkMethods.push(new BenchmarkMethod({ 75 | name: methodName, 76 | params: params, 77 | benchmarks: benchmarks 78 | })); 79 | methodNames.add(methodName); 80 | } 81 | benchmarkBundles.push(new BenchmarkBundle({ 82 | key: fullName, 83 | name: parseClassNameFromFullName(fullName), 84 | benchmarkMethods: benchmarkMethods, 85 | methodNames: [...methodNames] 86 | })); 87 | } 88 | return benchmarkBundles; 89 | } 90 | 91 | /* 92 | * Deconstructs the given benchmark runs into following structure: 93 | * Map> 94 | * Where the benchmarks array has a null if the particular run didn't included the benchmark. 95 | */ 96 | function parseMultiRunBenchmarkMap(benchmarkRuns) { 97 | const classToBenchmarksMap = new Map(); 98 | benchmarkRuns.forEach((benchmarkRun, benchmarkRunIndex) => { 99 | benchmarkRun.benchmarks.forEach((benchmark) => { 100 | const fullClassName = parseFullClassName(benchmark); 101 | let methodMap = classToBenchmarksMap.get(fullClassName); 102 | if (methodMap === undefined) { 103 | methodMap = new Map(); 104 | classToBenchmarksMap.set(fullClassName, methodMap) 105 | } 106 | let methodName = parseBenchmarkName(benchmark); 107 | let runArray = methodMap.get(methodName); 108 | 109 | if (runArray && runArray.length == benchmarkRunIndex + 1) { 110 | // Same benchmark executed with multiple execution modes 111 | methodName = methodName + '_' + benchmark.mode; 112 | runArray = methodMap.get(methodName); 113 | } 114 | 115 | if (runArray === undefined) { 116 | runArray = []; 117 | methodMap.set(methodName, runArray) 118 | } 119 | //fill up what didn't exist before 120 | while (runArray.length < benchmarkRunIndex) { 121 | runArray.push(null) 122 | } 123 | runArray.push(benchmark) 124 | }) 125 | // fill up what didn't exist in this run 126 | for (let methodMap of classToBenchmarksMap.values()) { 127 | for (let runArray of methodMap.values()) { 128 | if (runArray.length < benchmarkRunIndex + 1) { 129 | runArray.push(null); 130 | } 131 | } 132 | } 133 | }); 134 | return classToBenchmarksMap; 135 | } 136 | -------------------------------------------------------------------------------- /src/javascript/functions/util.js: -------------------------------------------------------------------------------- 1 | export function arraysAreIdentical(arr1, arr2) { 2 | if (arr1.length !== arr2.length) return false; 3 | for (var i = 0, len = arr1.length; i < len; i++) { 4 | if (arr1[i] !== arr2[i]) { 5 | return false; 6 | } 7 | } 8 | return true; 9 | } 10 | 11 | export function groupBy(xs, key) { 12 | return xs.reduce(function (rv, x) { 13 | let v = key instanceof Function ? key(x) : x[key]; 14 | let el = rv.find((r) => r && r.key === v); 15 | if (el) { 16 | el.values.push(x); 17 | } else { 18 | rv.push({ 19 | key: v, 20 | values: [x] 21 | }); 22 | } 23 | return rv; 24 | }, []); 25 | } 26 | 27 | export function cartesianProduct(arrayOfArrays) { 28 | return arrayOfArrays.reduce((a, b) => a.map(x => b.map(y => x.concat(y))) 29 | .reduce((a, b) => a.concat(b), []), [[]]); 30 | } 31 | 32 | export function flatten(arr, result = []) { 33 | for (let i = 0, length = arr.length; i < length; i++) { 34 | const value = arr[i] 35 | if (Array.isArray(value)) { 36 | for (let i = 0, length = value.length; i < length; i++) { 37 | const value2 = value[i] 38 | if (Array.isArray(value2)) { 39 | flatten(value2, result) 40 | } else { 41 | result.push(value2) 42 | } 43 | } 44 | } else { 45 | result.push(value) 46 | } 47 | } 48 | return result 49 | } 50 | 51 | //If there is any score above 5, we do round 52 | export function shouldRound(benchmarkMethods, metricExtractor) { 53 | for (let benchmarkMethod of benchmarkMethods) { 54 | for (let benchmark of benchmarkMethod.benchmarks) { 55 | if (benchmark && metricExtractor.hasMetric(benchmark) && (metricExtractor.extractScore(benchmark) > 5)) { 56 | return true; 57 | } 58 | } 59 | } 60 | return false; 61 | } 62 | 63 | //Conditional round method 64 | export function round(number, shouldRound) { 65 | if (!shouldRound || (number < 1 && number > -1)) { 66 | return number; 67 | } 68 | return Math.round(number); 69 | } 70 | 71 | //Conditional format number method 72 | export function formatNumber(number, roundScores) { 73 | if (number || number == 0) { 74 | if (roundScores && (number > 1 || number < -1)) { 75 | return number.toLocaleString(); 76 | } else { 77 | return number; 78 | } 79 | } else { 80 | return "n/a"; 81 | } 82 | } 83 | 84 | 85 | // Takes an array of strings and returns and array of strings. Common prefixes and suffixes will be removed. 86 | export function getUniqueNames(strings) { 87 | if (strings.length == 1) { 88 | return strings.map(string => extractAfterLastSlash(string)); 89 | } 90 | 91 | var minLength = Math.min(...strings.map(string => string.length)); 92 | const startIndex = getNotMatchingStartIndex(strings, minLength); 93 | const endIndex = getNotMatchingEndIndex(strings, minLength); 94 | if (startIndex > 0 && startIndex < minLength) { 95 | strings = strings.map(string => string.substring(startIndex)); 96 | } 97 | if (endIndex > 0 && endIndex < minLength) { 98 | strings = strings.map(string => string.substring(0, string.length - endIndex)); 99 | } 100 | if (minLength - startIndex - endIndex > 20) { 101 | strings = strings.map(string => extractAfterLastSlash(string)); 102 | } 103 | return strings; 104 | } 105 | 106 | function extractAfterLastSlash(string) { 107 | const lastSlash = string.lastIndexOf('/'); 108 | if (lastSlash > 0) { 109 | return string.substring(lastSlash + 1); 110 | } else { 111 | return string; 112 | } 113 | } 114 | 115 | function getNotMatchingStartIndex(strings, minLength) { 116 | for (var i = 0; i < minLength; i++) { 117 | for (var j = 0; j < strings.length - 1; j++) { 118 | if (strings[j].charAt(i) != strings[j + 1].charAt(i)) { 119 | return i; 120 | } 121 | } 122 | } 123 | return minLength; 124 | } 125 | 126 | function getNotMatchingEndIndex(strings, minLength) { 127 | return getNotMatchingStartIndex(strings.map(string => string.split('').reverse().join('')), minLength); 128 | } -------------------------------------------------------------------------------- /src/javascript/models/BenchmarkBundle.js: -------------------------------------------------------------------------------- 1 | import { flatten } from 'functions/util.js' 2 | 3 | // Holds a collection of benchmarks, typically those from a benchmark class 4 | export default class BenchmarkBundle { 5 | 6 | constructor(options) { 7 | this.key = options.key; // com.company.ClassA 8 | this.name = options.name; //ClassA 9 | this.methodNames = options.methodNames; //unique method names (can occur multiple times because of params) 10 | this.benchmarkMethods = options.benchmarkMethods; //BenchmarkMethod(name, benchmarks[])[] 11 | } 12 | 13 | 14 | //Returns all non-null benchmarks for all runs 15 | allBenchmarks() { 16 | return flatten(this.benchmarkMethods.map(method => method.benchmarks.filter(benchmark => benchmark))); 17 | } 18 | 19 | //Returns all non-null benchmarks for a given runs 20 | benchmarksFromRun(runIndex) { 21 | return flatten(this.benchmarkMethods.map(method => method.benchmarks[runIndex]).filter(benchmark => benchmark)); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/javascript/models/BenchmarkMethod.js: -------------------------------------------------------------------------------- 1 | // Has the original benchmark results from all runs for a benchmark method 2 | export default class BenchmarkMethod { 3 | 4 | constructor(options) { 5 | this.key = createKey(options.name, options.params); // method name + params 6 | this.name = options.name; // method name (can occure multiple times with use of params) 7 | this.params = options.params; //[[key][value]...] 8 | this.benchmarks = options.benchmarks; // [{run1}, {run2}] original benchmark object per run 9 | } 10 | 11 | } 12 | 13 | function createKey(name, params) { 14 | let key = name; 15 | if (params) { 16 | key += ' [' + params.map(param => param[0] + '=' + param[1]).join(':') + ']'; 17 | } 18 | return key; 19 | } 20 | -------------------------------------------------------------------------------- /src/javascript/models/BenchmarkRun.js: -------------------------------------------------------------------------------- 1 | // Holds all benchmarks of a JMH run 2 | export default class BenchmarkRun { 3 | 4 | constructor(options) { 5 | this.name = options.name; 6 | this.benchmarks = options.benchmarks 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/javascript/models/BenchmarkSelection.js: -------------------------------------------------------------------------------- 1 | import { parseBenchmarkBundles } from 'functions/parse.js' 2 | 3 | // Selection of BenchmarkRuns with parsed BenchmarkBundles 4 | export default class BenchmarkSelection { 5 | 6 | //TODO do benchmarkBundles parsing globally one time for all ? 7 | constructor(benchmarkRuns, runSelection) { 8 | const selectedBenchmarkRuns = benchmarkRuns.filter((run, pos) => runSelection[pos]); 9 | this.benchmarkRuns = benchmarkRuns; 10 | this.runSelection = runSelection; 11 | this.runNames = selectedBenchmarkRuns.map(run => run.name); //[] - names of the selected Benchmark runs 12 | this.benchmarkBundles = parseBenchmarkBundles(selectedBenchmarkRuns); // BenchmarkBundle[] with the benchmarks from the selected runs 13 | } 14 | 15 | } -------------------------------------------------------------------------------- /src/javascript/models/Examples.js: -------------------------------------------------------------------------------- 1 | // Holds pre-defined examples 2 | export default class Examples { 3 | 4 | constructor(options) { 5 | this.singleRunExample = [options.run1]; 6 | this.twoRunsExample = [options.run1, options.run2]; 7 | this.multiRunExample = [options.run1, options.run2, options.run3]; 8 | } 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/javascript/models/MetricExtractor.js: -------------------------------------------------------------------------------- 1 | // An abstract class defining common extractions from benchmark results for score, gc, etc.. 2 | export default class MetricExtractor { 3 | 4 | constructor(metricKey) { 5 | this.metricKey = metricKey; 6 | } 7 | 8 | getMetricObject(benchmark) { // eslint-disable-line no-unused-vars 9 | throw new TypeError("Do not call abstract method foo from child."); 10 | } 11 | 12 | extractType(benchmark) { // eslint-disable-line no-unused-vars 13 | throw new TypeError("Do not call abstract method foo from child."); 14 | } 15 | 16 | hasHistogram(benchmark) { // eslint-disable-line no-unused-vars 17 | throw new TypeError("Do not call abstract method foo from child."); 18 | } 19 | 20 | hasMetric(benchmark) { // eslint-disable-line no-unused-vars 21 | return !!this.getMetricObject(benchmark); 22 | } 23 | 24 | extractScore(benchmark) { 25 | return this.getMetricObject(benchmark).score; 26 | } 27 | 28 | extractScoreError(benchmark) { 29 | return this.getMetricObject(benchmark).scoreError; 30 | } 31 | 32 | extractScoreUnit(benchmark) { 33 | return this.getMetricObject(benchmark).scoreUnit; 34 | } 35 | 36 | extractRawData(benchmark) { 37 | return this.getMetricObject(benchmark).rawData; 38 | } 39 | 40 | extractRawDataHistogram(benchmark) { 41 | return this.getMetricObject(benchmark).rawDataHistogram; 42 | } 43 | 44 | extractRawDataScores(benchmark) { // eslint-disable-line no-unused-vars 45 | throw new TypeError("Do not call abstract method foo from child."); 46 | } 47 | 48 | extractMinMax(benchmark) { // eslint-disable-line no-unused-vars 49 | const score = this.extractScore(benchmark); 50 | let min = score; 51 | let max = score; 52 | const rawDataScores = this.extractRawDataScores(benchmark); 53 | rawDataScores.forEach(rawDataScore => { 54 | min = Math.min(min, rawDataScore); 55 | max = Math.max(max, rawDataScore); 56 | }); 57 | return [min, max]; 58 | } 59 | 60 | } 61 | 62 | -------------------------------------------------------------------------------- /src/javascript/models/MetricType.js: -------------------------------------------------------------------------------- 1 | // Contains help and behavoiur definitions for a metric. 2 | // For primary metrics a metric is the benchmark mode (like thrpt, avg, etc...) 3 | // and for the seconday metric its the metric key (like '·gc.alloc.rate'). 4 | export default class MetricType { 5 | 6 | constructor(displayName, description, increaseIsGood) { 7 | this.displayName = displayName; 8 | this.description = description; 9 | this.increaseIsGood = increaseIsGood; 10 | } 11 | 12 | } 13 | 14 | const typeMap = new Map(); 15 | 16 | typeMap.set('thrpt', new MetricType('Throughput', 'Operations per unit of time - the higher the bars, the better!', true)); 17 | typeMap.set('avgt', new MetricType('Average Time', 'Average time per operation - the lower the bars, the better!', false)); 18 | typeMap.set('sample', new MetricType('Sampling Time', 'Samples the time for each operation - the lower the bars, the better!', false)); 19 | typeMap.set('ss', new MetricType('Single Shot Time', 'Measures the time for a single operation - the lower the bars, the better!', false)); 20 | 21 | 22 | typeMap.set('·gc.alloc.rate', new MetricType('Allocation Rate', 'The amount of memory allocated per time unit - the lower the bars, the better!', false)); 23 | typeMap.set('·gc.alloc.rate.norm', new MetricType('Allocation Rate per operation', 'The amount of memory allocated per operation - the lower the bars, the better!', false)); 24 | typeMap.set('·gc.churn.PS_Eden_Space', new MetricType('Eden Allocation Churn', 'The amount of memory the garbage collector frees from Eden space per time unit - the lower the bars, the better!', false)); 25 | typeMap.set('·gc.churn.PS_Eden_Space.norm', new MetricType('Eden Allocation Churn per operation', 'The amount of memory the garbage collector frees from Eden space per operation - the lower the bars, the better!', false)); 26 | typeMap.set('·gc.churn.PS_Survivor_Space', new MetricType('Survivor Allocation Churn', 'The amount of memory the garbage collector frees from Survivor space per time unit - the lower the bars, the better!', false)); 27 | typeMap.set('·gc.churn.PS_Survivor_Space.norm', new MetricType('Survivor Allocation Churn per operation', 'The amount of memory the garbage collector frees from Survivor space per operation - the lower the bars, the better!', false)); 28 | typeMap.set('·gc.count', new MetricType('GC Count', 'How many garbage collections occured - the lower the bars, the better!', false)); 29 | typeMap.set('·gc.time', new MetricType('GC Time', 'Time spend for garbage collection - the lower the bars, the better!', false)); 30 | 31 | export function getMetricType(metricKey) { 32 | const defined = typeMap.get(metricKey); 33 | if (!defined) { 34 | return new MetricType(metricKey, '...', false); 35 | } 36 | return defined; 37 | } -------------------------------------------------------------------------------- /src/javascript/models/extractor/PrimaryMetricExtractor.js: -------------------------------------------------------------------------------- 1 | import MetricExtractor from 'models/MetricExtractor.js' 2 | 3 | export default class ScoreExtractor extends MetricExtractor { 4 | 5 | constructor() { 6 | super('Score'); 7 | } 8 | 9 | getMetricObject(benchmark) { 10 | return benchmark.primaryMetric; 11 | } 12 | 13 | extractType(benchmark) { 14 | return benchmark.mode; 15 | } 16 | 17 | hasHistogram(benchmark) { 18 | return benchmark.mode === 'sample'; 19 | } 20 | 21 | extractRawDataScores(benchmark) { 22 | if (this.hasHistogram(benchmark)) { 23 | return this.extractRawDataHistogram(benchmark).flatMap( 24 | forkArrays => forkArrays.flatMap( 25 | scoresArray => scoresArray.map( 26 | timeOccurence => timeOccurence[0])) 27 | ); 28 | } 29 | return this.extractRawData(benchmark).flatMap(forkArrays => forkArrays.map(elem => elem)); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/javascript/models/extractor/SecondaryMetricExtractor.js: -------------------------------------------------------------------------------- 1 | import MetricExtractor from 'models/MetricExtractor.js' 2 | 3 | export default class SecondaryMetricExtractor extends MetricExtractor { 4 | 5 | constructor(metricKey) { 6 | super(metricKey); 7 | } 8 | 9 | getMetricObject(benchmark) { 10 | return benchmark.secondaryMetrics[this.metricKey]; 11 | } 12 | 13 | extractType(benchmark) { // eslint-disable-line no-unused-vars 14 | return this.metricKey; 15 | } 16 | 17 | hasHistogram(benchmark) { // eslint-disable-line no-unused-vars 18 | return false; 19 | } 20 | 21 | extractRawDataScores(benchmark) { 22 | return this.extractRawData(benchmark).flatMap(forkArrays => forkArrays.map(elem => elem)); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/javascript/store/processParameters.js: -------------------------------------------------------------------------------- 1 | import { getUniqueNames } from 'functions/util.js' 2 | import BenchmarkRun from 'models/BenchmarkRun.js'; 3 | 4 | 5 | export function addSettingsFromParameters(settings) { 6 | const topBar = getParameterByName('topBar'); 7 | if (topBar) { 8 | settings.topBar = topBar; 9 | } 10 | } 11 | 12 | export function getBenchmarksLoadFunctionForDefinedExamples(examples) { 13 | var example = getParameterByName('example'); 14 | if (!example) { 15 | example = getExampleFromHash(); 16 | } 17 | if (example === 'single') { 18 | return (initBenchmarksFunction) => initBenchmarksFunction(examples.singleRunExample); 19 | } else if (example === 'two') { 20 | return (initBenchmarksFunction) => initBenchmarksFunction(examples.twoRunsExample); 21 | } else if (example === 'multi') { 22 | return (initBenchmarksFunction) => initBenchmarksFunction(examples.multiRunExample); 23 | } 24 | } 25 | 26 | export function getBenchmarksLoadFunctionForSourceExamples() { 27 | const source = getParameterByName('source'); 28 | if (source) { 29 | return (benchmarkLoadFunction) => fetchFromUrls(benchmarkLoadFunction, [source]); 30 | } else { 31 | const sources = getParameterByName('sources'); 32 | if (sources) { 33 | return (benchmarkLoadFunction) => fetchFromUrls(benchmarkLoadFunction, sources.split(',')); 34 | } else { 35 | const gist = getParameterByName('gist'); 36 | if (gist) { 37 | return (benchmarkLoadFunction) => fetchFromGists(benchmarkLoadFunction, [gist]); 38 | } else { 39 | const gists = getParameterByName('gists'); 40 | if (gists) { 41 | return (benchmarkLoadFunction) => fetchFromGists(benchmarkLoadFunction, gists.split(',')); 42 | } 43 | } 44 | } 45 | } 46 | } 47 | 48 | function getParameterByName(name) { 49 | var match = RegExp('[?&]' + name + '=([^&]*)').exec(window.location.search); 50 | return match && decodeURIComponent(match[1].replace(/\+/g, ' ')); 51 | } 52 | 53 | function fetchFromUrls(benchmarkLoadFunction, urls) { 54 | const fetchedJsonByUrl = new Map(); 55 | urls.forEach((url) => { 56 | fetch(url).then((response) => { 57 | if (!response.ok) { 58 | throw Error(response.statusText); 59 | } 60 | return response.json(); 61 | }).then((json) => { 62 | fetchedJsonByUrl.set(url, json); 63 | if (fetchedJsonByUrl.size == urls.length) { 64 | const uniqueNames = getUniqueNames(urls); 65 | benchmarkLoadFunction(uniqueNames.map((name, i) => new BenchmarkRun({ 66 | name: name, 67 | benchmarks: fetchedJsonByUrl.get(urls[i]) 68 | }))); 69 | } 70 | }).catch(function (error) { 71 | alert(`Could not fetch data from ${url}: ${error}`); 72 | }); 73 | }); 74 | } 75 | 76 | function fetchFromGists(benchmarkLoadFunction, gists) { 77 | const benchmarkRuns = []; 78 | Promise.all( 79 | gists.map((gist) => { 80 | const url = `https://api.github.com/gists/${gist}`; 81 | return fetch(url) 82 | .then((response) => { 83 | if (!response.ok) { 84 | throw Error(response.statusText); 85 | } 86 | return response.json(); 87 | }) 88 | .catch((error) => { 89 | alert(`Could not fetch data from ${url}: ${error}`); 90 | }); 91 | }) 92 | ) 93 | .then((jsons) => { 94 | jsons.forEach((json) => { 95 | Object.entries(json.files).forEach(([key, value]) => { 96 | benchmarkRuns.push( 97 | new BenchmarkRun({ 98 | name: `${json.id}/${key}`, 99 | benchmarks: JSON.parse(value.content), 100 | }) 101 | ); 102 | }); 103 | }); 104 | }) 105 | .then(() => { 106 | benchmarkLoadFunction(benchmarkRuns); 107 | }); 108 | } 109 | 110 | // backwards compatibility - Jan 2018 111 | function getExampleFromHash() { 112 | const urlHash = window.location.hash; 113 | if (urlHash === '#singleRunExample') { 114 | return 'single'; 115 | } else if (urlHash === '#twoRunsExample') { 116 | return 'two'; 117 | } else if (urlHash === '#multiRunExample') { 118 | return 'multi'; 119 | } 120 | } -------------------------------------------------------------------------------- /src/javascript/store/store.js: -------------------------------------------------------------------------------- 1 | import createStore from 'react-waterfall' 2 | import { createBrowserHistory } from 'history' 3 | 4 | import { addSettingsFromParameters, getBenchmarksLoadFunctionForDefinedExamples, getBenchmarksLoadFunctionForSourceExamples } from 'store/processParameters.js'; 5 | import Examples from 'models/Examples.js'; 6 | import { exampleRun1 } from 'exampleBenchmark1.js'; 7 | import { exampleRun2 } from 'exampleBenchmark2.js'; 8 | import { exampleRun3 } from 'exampleBenchmark3.js'; 9 | import BenchmarkRun from 'models/BenchmarkRun.js'; 10 | 11 | const history = createBrowserHistory(); 12 | 13 | const examples = new Examples({ 14 | run1: new BenchmarkRun({ 15 | name: 'run1', 16 | benchmarks: exampleRun1 17 | }), 18 | run2: new BenchmarkRun({ 19 | name: 'run2', 20 | benchmarks: exampleRun2 21 | }), 22 | run3: new BenchmarkRun({ 23 | name: 'run3', 24 | benchmarks: exampleRun3 25 | }) 26 | }); 27 | 28 | // Get default settings from settings.js and enrich with parameters 29 | const settings = defaultSettings;// eslint-disable-line no-undef 30 | addSettingsFromParameters(settings); 31 | 32 | // Load benchmarks from defined source (provided || example || remote source) 33 | let benchmarkLoadFunction = null; 34 | if (providedBenchmarks.length > 0) { // eslint-disable-line no-undef 35 | benchmarkLoadFunction = (initBenchmarksFunction) => initBenchmarksFunction(providedBenchmarks.map(runName => new BenchmarkRun({ // eslint-disable-line no-undef 36 | name: runName, 37 | benchmarks: providedBenchmarkStore[runName] // eslint-disable-line no-undef 38 | }))); 39 | } else { 40 | benchmarkLoadFunction = getBenchmarksLoadFunctionForDefinedExamples(examples); 41 | if (!benchmarkLoadFunction) { 42 | benchmarkLoadFunction = getBenchmarksLoadFunctionForSourceExamples(); 43 | } 44 | } 45 | 46 | // Setup store 47 | const config = { 48 | initialState: { 49 | settings: settings, 50 | initialLoading: benchmarkLoadFunction != null, 51 | loading: false, 52 | benchmarkRuns: [], 53 | runSelection: [], // boolean[runs] 54 | runView: null, // null || Summary || Compare 55 | selectedMetric: 'Score', 56 | detailedBenchmarkBundle: null, 57 | activeCategory: 'Benchmarks', 58 | focusedBundles: new Set(), 59 | chartConfig: { 60 | sort: false, 61 | logScale: false 62 | } 63 | }, 64 | actionsCreators: { 65 | uploadFiles: async (state, actions, files, trigger) => loadBenchmarksAsync(state, trigger, () => actions.uploadFiles(files, true), () => parseBenchmarks(files)), 66 | initBenchmarks: (state, actions, benchmarkRuns) => { 67 | return stateForBenchmarks(benchmarkRuns); 68 | }, 69 | loadSingleRunExample: (state, actions, param, trigger) => loadBenchmarksAsync(state, trigger, () => actions.loadSingleRunExample(null, true), () => getExamples(examples.singleRunExample)), 70 | loadTwoRunsExample: (state, actions, param, trigger) => loadBenchmarksAsync(state, trigger, () => actions.loadTwoRunsExample(null, true), () => getExamples(examples.twoRunsExample)), 71 | loadMultiRunExample: (state, actions, param, trigger) => loadBenchmarksAsync(state, trigger, () => actions.loadMultiRunExample(null, true), () => getExamples(examples.multiRunExample)), 72 | selectMetric: (state, actions, newSelectedMetric) => ({ selectedMetric: newSelectedMetric }), 73 | focusBundle: (state, actions, benchmarkBundleName) => { 74 | const clonedFocusedBundles = new Set(state.focusedBundles) 75 | const alreadyFocused = clonedFocusedBundles.has(benchmarkBundleName); 76 | if (alreadyFocused) { 77 | clonedFocusedBundles.delete(benchmarkBundleName); 78 | } else { 79 | clonedFocusedBundles.add(benchmarkBundleName); 80 | } 81 | return { focusedBundles: clonedFocusedBundles }; 82 | }, 83 | selectCategory: (state, actions, category) => { 84 | return { activeCategory: category, focusedBundles: new Set() } 85 | }, 86 | detailBenchmarkBundle: (state, actions, benchmarkBundleKey) => { 87 | history.push('#details'); 88 | return { detailedBenchmarkBundle: benchmarkBundleKey }; 89 | }, 90 | undetailBenchmarkBundle: () => { 91 | return { detailedBenchmarkBundle: null }; 92 | }, 93 | // expects array of boolean with length of total JMH runs + the runView ('Summary', 'Compare') 94 | selectBenchmarkRuns: (state, action, runSelection, runView) => { 95 | return { runSelection: runSelection, runView: runView }; 96 | }, 97 | sort: (state) => { 98 | return { chartConfig: { ...state.chartConfig, sort: !state.chartConfig.sort } } 99 | }, 100 | logScale: (state) => { 101 | return { chartConfig: { ...state.chartConfig, logScale: !state.chartConfig.logScale } } 102 | }, 103 | goBack: () => { 104 | history.goBack(); 105 | return {}; 106 | } 107 | }, 108 | } 109 | 110 | function stateForBenchmarks(benchmarkRuns) { 111 | const runView = benchmarkRuns.length > 1 ? 'Summary' : null; 112 | const runSelection = Array(benchmarkRuns.length).fill(true) 113 | return { initialLoading: false, loading: false, benchmarkRuns: benchmarkRuns, runSelection: runSelection, runView: runView }; 114 | } 115 | 116 | async function loadBenchmarksAsync(state, trigger, triggerFunction, getBenchmarksFunction) { 117 | if (trigger) { 118 | return { loading: true } 119 | } else { 120 | await triggerFunction(); 121 | } 122 | 123 | try { 124 | const benchmarkRuns = await getBenchmarksFunction(); 125 | return stateForBenchmarks(benchmarkRuns); 126 | } catch (error) { 127 | return stateForBenchmarks([]); 128 | } 129 | } 130 | 131 | export const { Provider, connect, actions } = createStore(config); 132 | 133 | history.listen((location, action) => { 134 | if (action === 'POP') { 135 | actions.undetailBenchmarkBundle(); 136 | } 137 | }); 138 | 139 | if (benchmarkLoadFunction) { 140 | setTimeout(() => benchmarkLoadFunction(actions.initBenchmarks), 0); 141 | } 142 | 143 | function getExamples(benchmarkRuns) { 144 | return new Promise((resolve) => setTimeout(() => resolve(benchmarkRuns), 0)); 145 | } 146 | 147 | function parseBenchmarks(files) { 148 | return new Promise((resolve, reject) => { 149 | 150 | const benchmarkRuns = []; 151 | files.forEach((file) => { 152 | const reader = new FileReader(); 153 | const runName = file.name.replace('.json', ''); 154 | reader.onload = function (evt) { 155 | try { 156 | var parsedBenchmarks = JSON.parse(evt.target.result); 157 | const benchmarkRun = new BenchmarkRun({ 158 | name: runName, 159 | benchmarks: parsedBenchmarks 160 | }); 161 | benchmarkRuns.push(benchmarkRun); 162 | if (benchmarkRuns.length == files.length) { 163 | benchmarkRuns.sort((a, b) => a.name.localeCompare(b.name)); 164 | window.onbeforeunload = function () { 165 | return "You will loose the current benchmarks."; 166 | }; 167 | resolve(benchmarkRuns); 168 | } 169 | } catch (e) { 170 | alert(e); //error in the above string(in this case,yes)! 171 | reject(e); 172 | } 173 | }; 174 | reader.readAsText(file); 175 | }); 176 | }); 177 | } 178 | -------------------------------------------------------------------------------- /src/provided.js: -------------------------------------------------------------------------------- 1 | // provided.js can be overriden in order to have a predefined-report generated. 2 | 3 | 4 | var providedBenchmarks = []; // eslint-disable-line no-unused-vars 5 | // var providedBenchmarks = ['result']; // eslint-disable-line no-unused-vars 6 | 7 | var providedBenchmarkStore = { // eslint-disable-line no-unused-vars 8 | result: [ 9 | { 10 | "benchmark": "io.morethan.javabenchmarks.datastructure.ListCreationBenchmark.arrayList", 11 | "mode": "thrpt", 12 | "threads": 1, 13 | "forks": 2, 14 | "warmupIterations": 3, 15 | "warmupTime": "1 s", 16 | "warmupBatchSize": 1, 17 | "measurementIterations": 5, 18 | "measurementTime": "1 s", 19 | "measurementBatchSize": 1, 20 | "primaryMetric": { 21 | "score": 3467631.2300505415, 22 | "scoreError": 98563.33258873208, 23 | "scoreConfidence": [ 24 | 3369067.897461809, 25 | 3566194.5626392737 26 | ], 27 | "scorePercentiles": { 28 | "0.0": 3333994.173460993, 29 | "50.0": 3474993.5017186473, 30 | "90.0": 3532339.4656069754, 31 | "95.0": 3532453.3325443356, 32 | "99.0": 3532453.3325443356, 33 | "99.9": 3532453.3325443356, 34 | "99.99": 3532453.3325443356, 35 | "99.999": 3532453.3325443356, 36 | "99.9999": 3532453.3325443356, 37 | "100.0": 3532453.3325443356 38 | }, 39 | "scoreUnit": "ops/s", 40 | "rawData": [ 41 | [ 42 | 3524193.1540743182, 43 | 3531314.663170731, 44 | 3532453.3325443356, 45 | 3520395.64538141, 46 | 3474431.2865468427 47 | ], 48 | [ 49 | 3440045.3067861893, 50 | 3333994.173460993, 51 | 3445535.4557112623, 52 | 3475555.716890452, 53 | 3398393.5659388755 54 | ] 55 | ] 56 | }, 57 | "secondaryMetrics": { 58 | } 59 | }] 60 | }; -------------------------------------------------------------------------------- /src/settings.js: -------------------------------------------------------------------------------- 1 | var defaultSettings = { // eslint-disable-line no-unused-vars 2 | topBar: 'default' //possible values ['default', 'off', 'my custom headline'] 3 | } -------------------------------------------------------------------------------- /test/functions/parse.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import { parseMethodName, parseBenchmarkName, getUniqueParamValues, parseBenchmarkBundles } from '../../src/javascript/functions/parse.js' 4 | import BenchmarkRun from '../../src/javascript/models/BenchmarkRun.js' 5 | import BenchmarkBundle from '../../src/javascript/models/BenchmarkBundle.js' 6 | import BenchmarkMethod from '../../src/javascript/models/BenchmarkMethod.js' 7 | 8 | 9 | describe('functions: parseMethodName', () => { 10 | 11 | it('default', () => { 12 | 13 | expect(parseMethodName({ 14 | benchmark: "io.morethan.javabenchmarks.showcase.ParamsBenchmark.bench" 15 | })).to.equal("bench"); 16 | expect(parseMethodName({ 17 | benchmark: "ParamsBenchmark.bench" 18 | })).to.equal("bench"); 19 | 20 | }); 21 | 22 | }); 23 | 24 | describe('functions: parseBenchmarkName', () => { 25 | 26 | it('default', () => { 27 | 28 | expect(parseBenchmarkName({ 29 | benchmark: "io.morethan.javabenchmarks.showcase.ParamsBenchmark.bench" 30 | })).to.equal("bench"); 31 | expect(parseBenchmarkName({ 32 | benchmark: "ParamsBenchmark.bench" 33 | })).to.equal("bench"); 34 | 35 | }); 36 | 37 | it('singleParams', () => { 38 | 39 | expect(parseBenchmarkName({ 40 | benchmark: "io.morethan.javabenchmarks.showcase.ParamsBenchmark.bench", 41 | params: { 42 | arg: "1" 43 | } 44 | })).to.equal("bench arg=1"); 45 | expect(parseBenchmarkName({ 46 | benchmark: "io.morethan.javabenchmarks.showcase.ParamsBenchmark.bench", 47 | params: { 48 | arg: "2" 49 | } 50 | })).to.equal("bench arg=2"); 51 | 52 | }); 53 | 54 | it('multipleParams', () => { 55 | 56 | expect(parseBenchmarkName({ 57 | benchmark: "io.morethan.javabenchmarks.showcase.ParamsBenchmark.bench", 58 | params: { 59 | arg: "1", 60 | certainty: "32", 61 | gender: "male" 62 | } 63 | })).to.equal("bench arg=1 certainty=32 gender=male"); 64 | 65 | }); 66 | 67 | }); 68 | 69 | describe('functions: getUniqueParamValues', () => { 70 | 71 | it('default', () => { 72 | const benchmarks = [ 73 | { 74 | params: { 75 | arg: 1, 76 | certainty: 0 77 | } 78 | }, 79 | { 80 | params: { 81 | arg: 1, 82 | certainty: 32 83 | } 84 | }, 85 | { 86 | params: { 87 | arg: 2, 88 | certainty: 0 89 | } 90 | }]; 91 | expect(getUniqueParamValues(benchmarks, 'arg')).to.deep.equal([1, 2]); 92 | expect(getUniqueParamValues(benchmarks, 'certainty')).to.deep.equal([0, 32]); 93 | 94 | }); 95 | 96 | }); 97 | 98 | describe('functions: parseBenchmarkBundles', () => { 99 | 100 | it('default', () => { 101 | const run1 = new BenchmarkRun({ 102 | name: "1", 103 | benchmarks: [ 104 | { 105 | "benchmark": "com.A.bench", 106 | }, 107 | { 108 | "benchmark": "com.B.bench", 109 | "primaryMetric": { 110 | "score": 1 111 | } 112 | } 113 | ] 114 | 115 | }); 116 | const run2 = new BenchmarkRun({ 117 | name: "2", 118 | benchmarks: [ 119 | { 120 | "benchmark": "com.B.bench", 121 | "primaryMetric": { 122 | "score": 2 123 | } 124 | }, 125 | { 126 | "benchmark": "com.B.bench2", 127 | }, 128 | { 129 | "benchmark": "com.C.bench", 130 | } 131 | ] 132 | }); 133 | const benchmarkBundles = parseBenchmarkBundles([run1, run2]); 134 | const expectedBundles = [ 135 | new BenchmarkBundle({ 136 | key: 'com.A', 137 | name: 'A', 138 | methodNames: ['bench'], 139 | benchmarkMethods: [new BenchmarkMethod({ 140 | name: 'bench', 141 | params: null, 142 | benchmarks: [run1.benchmarks[0], null] 143 | })] 144 | }), 145 | new BenchmarkBundle({ 146 | key: 'com.B', 147 | name: 'B', 148 | methodNames: ['bench', 'bench2'], 149 | benchmarkMethods: [ 150 | new BenchmarkMethod({ 151 | name: 'bench', 152 | params: null, 153 | benchmarks: [run1.benchmarks[1], run2.benchmarks[0]] 154 | }), 155 | new BenchmarkMethod({ 156 | name: 'bench2', 157 | params: null, 158 | benchmarks: [null, run2.benchmarks[1]] 159 | }) 160 | ] 161 | }), 162 | new BenchmarkBundle({ 163 | key: 'com.C', 164 | name: 'C', 165 | methodNames: ['bench'], 166 | benchmarkMethods: [ 167 | new BenchmarkMethod({ 168 | name: 'bench', 169 | params: null, 170 | benchmarks: [null, run2.benchmarks[2]] 171 | }) 172 | ] 173 | }) 174 | ]; 175 | 176 | expect(benchmarkBundles).to.have.lengthOf(3); 177 | expect(benchmarkBundles).to.deep.equal(expectedBundles); 178 | }); 179 | 180 | }); -------------------------------------------------------------------------------- /test/functions/util.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import { cartesianProduct, flatten, getUniqueNames } from '../../src/javascript/functions/util.js' 4 | 5 | 6 | describe('functions: cartesianProduct', () => { 7 | 8 | it('default', () => { 9 | expect(cartesianProduct([['a'], ['b'], ['c']])).to.deep.equal([['a', 'b', 'c']]); 10 | expect(cartesianProduct([['a1', 'a2'], ['b'], ['c']])).to.deep.equal([['a1', 'b', 'c'], ['a2', 'b', 'c']]); 11 | expect(cartesianProduct([['a1', 'a2'], ['b1', 'b2'], ['c']])).to.deep.equal([['a1', 'b1', 'c'], ['a1', 'b2', 'c'], ['a2', 'b1', 'c'], ['a2', 'b2', 'c']]); 12 | expect(cartesianProduct([['a', 'b', 'c'], [1, 2]])).to.deep.equal([['a', 1], ['a', 2], ['b', 1], ['b', 2], ['c', 1], ['c', 2]]); 13 | }); 14 | 15 | }); 16 | 17 | describe('functions: flatten', () => { 18 | 19 | it('default', () => { 20 | expect(flatten([1, 2, 3])).to.deep.equal([1, 2, 3]); 21 | expect(flatten([[1], [2], [3]])).to.deep.equal([1, 2, 3]); 22 | expect(flatten([[1, 2, 3]])).to.deep.equal([1, 2, 3]); 23 | expect(flatten([[1, [2, 3]]])).to.deep.equal([1, 2, 3]); 24 | }); 25 | 26 | }); 27 | 28 | describe('functions: getUniqueNames', () => { 29 | 30 | it('default', () => { 31 | 32 | expect(getUniqueNames(["abc"])).to.deep.equal(["abc"]); 33 | expect(getUniqueNames(["xx/abc"])).to.deep.equal(["abc"]); 34 | expect(getUniqueNames(["abc", "cdf"])).to.deep.equal(["abc", "cdf"]); 35 | expect(getUniqueNames(["abc", "adf"])).to.deep.equal(["bc", "df"]); 36 | expect(getUniqueNames(["abc", "abf"])).to.deep.equal(["c", "f"]); 37 | expect(getUniqueNames(["abc", "abc"])).to.deep.equal(["abc", "abc"]); 38 | 39 | expect(getUniqueNames(["https://gist.githubusercontent.com/jzillmann/7d23b2382911cc434754a23773b06598/raw/1bcad4bb64624d8a2be15114a4eee4c406c3ae95/string-concatenation_jdk7.json"])) 40 | .to.deep.equal(["string-concatenation_jdk7.json"]); 41 | expect(getUniqueNames(["https://gist.githubusercontent.com/jzillmann/7d23b2382911cc434754a23773b06598/raw/1bcad4bb64624d8a2be15114a4eee4c406c3ae95/string-concatenation_jdk7.json", 42 | "https://gist.githubusercontent.com/jzillmann/866d39d43b264f507a67368f2313baca/raw/d0ae1502e8c493e6814c83f2df345fecb763c078/string-concatenation_jdk8.json"])) 43 | .to.deep.equal(["string-concatenation_jdk7", "string-concatenation_jdk8"]); 44 | expect(getUniqueNames(["https://gist.githubusercontent.com/xxx/aaa/jmh1.json", "https://gist.githubusercontent.com/xxx/aaa/jmh2.json"])) 45 | .to.deep.equal(["1", "2"]); 46 | 47 | }); 48 | 49 | }); 50 | 51 | -------------------------------------------------------------------------------- /test/models/BenchmarkBundle.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import BenchmarkBundle from '../../src/javascript/models/BenchmarkBundle.js' 4 | import BenchmarkMethod from '../../src/javascript/models/BenchmarkMethod.js' 5 | 6 | 7 | describe('functions: parseBenchmarkCollections', () => { 8 | 9 | it('default', () => { 10 | const benchmarkBundle = new BenchmarkBundle({ 11 | key: 'com.A', 12 | name: 'A', 13 | benchmarkMethods: [ 14 | new BenchmarkMethod({ 15 | name: 'bench', 16 | benchmarks: [ 17 | { 18 | "benchmark": "com.A.bench", 19 | "primaryMetric": { 20 | "score": 1 21 | } 22 | }, { 23 | "benchmark": "com.A.bench", 24 | "primaryMetric": { 25 | "score": 2 26 | } 27 | } 28 | ] 29 | }), 30 | new BenchmarkMethod({ 31 | name: 'bench2', 32 | benchmarks: [ 33 | { 34 | "benchmark": "com.A.bench2", 35 | "primaryMetric": { 36 | "score": 2.1 37 | } 38 | }, 39 | null 40 | ] 41 | }), 42 | new BenchmarkMethod({ 43 | name: 'bench3', 44 | benchmarks: [ 45 | null, 46 | { 47 | "benchmark": "com.A.bench3", 48 | "primaryMetric": { 49 | "score": 3.2 50 | } 51 | } 52 | ] 53 | }) 54 | ] 55 | }); 56 | 57 | //select all runs 58 | expect(benchmarkBundle.allBenchmarks()).to.have.lengthOf(4); 59 | expect(benchmarkBundle.allBenchmarks()).to.have.members([ 60 | benchmarkBundle.benchmarkMethods[0].benchmarks[0], 61 | benchmarkBundle.benchmarkMethods[0].benchmarks[1], 62 | benchmarkBundle.benchmarkMethods[1].benchmarks[0], 63 | benchmarkBundle.benchmarkMethods[2].benchmarks[1], 64 | ]); 65 | 66 | //select first run 67 | expect(benchmarkBundle.benchmarksFromRun(0)).to.have.lengthOf(2); 68 | expect(benchmarkBundle.benchmarksFromRun(0)).to.have.members([ 69 | benchmarkBundle.benchmarkMethods[0].benchmarks[0], 70 | benchmarkBundle.benchmarkMethods[1].benchmarks[0], 71 | ]); 72 | 73 | //select second run 74 | expect(benchmarkBundle.benchmarksFromRun(1)).to.have.lengthOf(2); 75 | expect(benchmarkBundle.benchmarksFromRun(1)).to.have.members([ 76 | benchmarkBundle.benchmarkMethods[0].benchmarks[1], 77 | benchmarkBundle.benchmarkMethods[2].benchmarks[1], 78 | ]); 79 | 80 | 81 | }); 82 | 83 | }); -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 5 | 6 | const SOURCE_DIR = path.resolve(__dirname, 'src'); 7 | const JAVASCRIPT_DIR = SOURCE_DIR + '/javascript'; 8 | const BUILD_DIR = path.resolve(__dirname, 'build'); 9 | 10 | module.exports = { 11 | context: SOURCE_DIR, 12 | resolve: { 13 | modules: [ 14 | path.resolve(JAVASCRIPT_DIR), 15 | path.resolve('./node_modules') 16 | ] 17 | }, 18 | entry: { 19 | app: './javascript/entry.jsx' 20 | }, 21 | output: { 22 | path: BUILD_DIR, 23 | filename: 'bundle.js' 24 | }, 25 | module: { 26 | rules: [ 27 | { 28 | test: /\.jsx?$/, 29 | loader: 'babel-loader', 30 | // Skip any files outside of your project's `src` directory 31 | include: [JAVASCRIPT_DIR], 32 | }, 33 | { 34 | test: /\.css$/, 35 | loader: "style-loader!css-loader" 36 | }, 37 | { 38 | test: /\.png$/, 39 | loader: "url-loader?limit=100000" 40 | }, 41 | { 42 | test: /\.jpg$/, 43 | loader: "file-loader" 44 | }, 45 | { 46 | test: /\.(woff|woff2)(\?v=\d+\.\d+\.\d+)?$/, 47 | loader: 'url-loader?limit=10000&mimetype=application/font-woff' 48 | }, 49 | { 50 | test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, 51 | loader: 'url-loader?limit=10000&mimetype=application/octet-stream' 52 | }, 53 | { 54 | test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, 55 | loader: 'file-loader' 56 | }, 57 | { 58 | test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, 59 | loader: 'url-loader?limit=10000&mimetype=image/svg+xml' 60 | } 61 | ] 62 | }, 63 | plugins: [ 64 | new HtmlWebpackPlugin({ 65 | template: 'index.html' 66 | }), 67 | new webpack.DefinePlugin({ 68 | 'process.env': { 69 | 'version': JSON.stringify(process.env.npm_package_version), 70 | 'NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development') 71 | } 72 | }), 73 | new CopyWebpackPlugin([ 74 | { 75 | from: 'favicons', 76 | to: 'favicons' 77 | }, 78 | ]), 79 | new CopyWebpackPlugin([ 80 | { 81 | from: 'provided.js', 82 | }, 83 | ]), 84 | new CopyWebpackPlugin([ 85 | { 86 | from: 'settings.js', 87 | }, 88 | ]), 89 | ] 90 | }; 91 | --------------------------------------------------------------------------------