├── .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 | [](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 |
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 =>
25 | { bundle.name }
26 | );
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 |
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 |
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 selectSingleRun(benchmarkRuns, runView, index) }>
68 | { benchmarkRuns[index].name }
69 |
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 = selectAll(runSelection, runView) }>
86 | { runViews[0] }
87 |
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 =>
32 | { metric }
33 | );
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 |
38 | { categories.map(category =>
39 |
44 |
45 | { category === activeCategory ? elementIds.map((elementId, i) =>
54 |
60 | )
61 | : '' }
62 |
63 | ) }
64 |
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 |
72 |
77 |
82 |
83 |
88 |
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 |
66 |
67 |
68 |
69 |
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 | Benchmark
49 | Score
50 | Min
51 | Max
52 | Score Error
53 | Unit
54 |
55 |
56 |
57 | { tableRows }
58 |
59 |
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 |
79 | Show JSON
80 |
81 |
82 |
83 |
{ JSON.stringify(benchmarks, null, '\t') }
84 |
85 | Collapse
86 |
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 |
108 | Show JSON 1
109 |
110 |
111 | Show JSON 2
112 |
113 |
114 |
115 |
{ JSON.stringify(benchmarks1, null, '\t') }
116 |
117 | Collapse
118 |
119 |
120 |
121 |
122 |
123 |
{ JSON.stringify(benchmarks2, null, '\t') }
124 |
125 | Collapse
126 |
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 | Run
50 | Score
51 | Error
52 | Unit
53 |
54 |
55 |
56 |
57 | { runNames[0] }
58 | { formatNumber(score1, roundScores) }
59 | { formatNumber(scoreError1, roundScores) }
60 | { scoreUnit }
61 |
62 |
63 | { runNames[1] }
64 | { formatNumber(score2, roundScores) }
65 | { formatNumber(scoreError2, roundScores) }
66 | { scoreUnit }
67 |
68 |
69 | Change
70 | { (scoreChange > 0 ? '+' : '') + formatNumber(scoreChange, roundScores) }
71 | { (scoreErrorChange > 0 ? '+' : '') + formatNumber(scoreErrorChange, roundScores) }
72 | { scoreUnit }
73 |
74 |
75 |
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 |
--------------------------------------------------------------------------------