├── .github └── workflows │ └── main.yml ├── .gitignore ├── .npmrc ├── .nvmrc ├── .prettierrc ├── .vscode └── settings.json ├── README.md ├── package-lock.json ├── package.json ├── src ├── App │ ├── gh-button.scss │ ├── index.scss │ └── index.tsx ├── benchmark │ └── index.tsx ├── common │ └── reset.css ├── favicon.ico ├── index.html └── index.tsx └── tsconfig.json /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Build and Deploy 2 | on: [push] 3 | jobs: 4 | build-and-deploy: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - name: Checkout 🛎️ 8 | uses: actions/checkout@v2.3.1 # If you're using actions/checkout@v2 you must set persist-credentials to false in most cases for the deployment to work correctly. 9 | with: 10 | persist-credentials: false 11 | 12 | - name: Install and Build 🔧 # This example project is built using npm and outputs the result to the 'build' folder. Replace with the commands required to build your project, or remove this step entirely if your site is pre-built. 13 | run: | 14 | npm install 15 | npm run build 16 | 17 | - name: Deploy 🚀 18 | uses: JamesIves/github-pages-deploy-action@3.7.1 19 | with: 20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 21 | BRANCH: gh-pages # The branch the action should deploy to. 22 | FOLDER: dist # The folder the action should deploy. 23 | CLEAN: true # Automatically remove deleted files from the deploy branch 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | .parcel-cache 3 | node_modules 4 | .DS_Store -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | save-exact=true 3 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v20.11.0 -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "semi": false, 4 | "singleQuote": true, 5 | "arrowParens": "avoid", 6 | "printWidth": 100, 7 | "trailingComma": "none" 8 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true 3 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # What Code Is Faster? 2 | 3 | **A browser-based tool for speedy and correct JS performance comparisons!** 4 | 5 | - Minimalistic UI 6 | - Code editor with IntelliSense 7 | - All state is saved to URL - copy it and share with friends in no time! 8 | - Automatically determines the number of iterations needed for a proper measurement — no hard-coding! 9 | - Prevents dead code elimination and compile-time eval. optimizations from ruining your test! 10 | - Verifies correctness (functions must compute the same value, be deterministic, depend on their inputs) 11 | - Warms up functions before measuring (to give time for JIT to compile & optimize them) 12 | 13 | Try it online! 14 | 15 | ## How Does It Work? 16 | 17 | Benchmarked functions are written as _reducers_, i.e. taking a previous value and returning some other value. The runtime executes your functions in a tight loop against some random initial value, saving the final value to a global variable (thus producing a _side effect_), so that no smart compiler could optimize out our computation! 18 | 19 | So you must also provide a random initial value (not [something like that](https://xkcd.com/221/)) and ensure that your reducers follow some simple rules. **Those rules are programmatically enforced** — so you won't shoot yourself in the foot. Check the examples to get a sense of how to write a benchmark. 20 | 21 | The rules: 22 | 23 | 1. The result of a function must depend on its input — and only on its input! You cannot return the same value again and again, or return some random values — there should be some genuine non-throwable computation on a passed input. 24 | 25 | 2. Given the same input, the output of the functions must be all the same. The comparison should be fair — we want to compare different implementations of exactly the same thing! 26 | 27 | ## Examples 28 | 29 | - Array push vs. assign to last index 30 | - BigInt (64-bit) vs. number (increment) 31 | - Math.hypot or Math.sqrt? 32 | - Do local function declarations affect performance? 33 | - Do closures affect performance (vs. free functions)? 34 | - For..of loop over Set vs. Array 35 | - For..of loop over Object.values vs. Map.forEach (large integer keys) 36 | - Map vs. Object (lookup) 37 | - Map vs. Object (getting keys) 38 | - Null or undefined? (equality check) 39 | - Arguments passing: spread vs. call() vs. apply() 40 | - JSON.parse() vs. eval() 41 | - Array vs TypedArray Dot Product 42 | - instanceof or constructor check? 43 | - Copying: arr.slice() vs. [...arr] 44 | 45 | - ..Add your own? Pull Requests are welcome! 46 | 47 | ## Extended Configuration 48 | 49 | In case you test functions operate on differently typed inputs, you might need to provide distinct initial values and provide a customized comparison function, otherwise it won't pass the soundness check. Here is an example: 50 | 51 | ```js 52 | benchmark('bigint vs number (addition)', { 53 | initialValues() { 54 | const seed = 1000000 + (Math.random() * 1000) | 0 55 | return { 56 | bigint: BigInt(seed), 57 | number: seed, 58 | } 59 | }, 60 | equal(a, b) { 61 | return BigInt(a) === BigInt(b) 62 | } 63 | }, { 64 | bigint(prev) { 65 | return prev + 1n 66 | }, 67 | number(prev) { 68 | return prev + 1 69 | } 70 | }) 71 | ``` 72 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "what-code-is-faster", 3 | "version": "1.0.0", 4 | "description": "A browser-based tool for speedy and correct JS perf comparisons", 5 | "private": true, 6 | "browserslist": [ 7 | "since 2017-06" 8 | ], 9 | "scripts": { 10 | "start": "rm -rf .parcel-cache .cache dist && parcel src/index.html", 11 | "build": "rm -rf .parcel-cache .cache dist && parcel build --public-url ./ src/index.html" 12 | }, 13 | "author": "Vitaly Gordon", 14 | "license": "MIT", 15 | "devDependencies": { 16 | "@parcel/transformer-sass": "2.11.0", 17 | "@types/deep-equal": "1.0.1", 18 | "@types/pako": "1.0.1", 19 | "@types/react": "^16.9.56", 20 | "@types/react-dom": "^16.9.9", 21 | "@types/react-github-button": "0.1.0", 22 | "buffer": "6.0.3", 23 | "path-browserify": "1.0.1", 24 | "process": "0.11.10", 25 | "util": "0.12.5" 26 | }, 27 | "dependencies": { 28 | "@monaco-editor/react": "3.7.2", 29 | "deep-equal": "2.0.5", 30 | "monaco-editor": "0.21.2", 31 | "pako": "2.0.2", 32 | "panic-overlay": "1.0.42", 33 | "parcel": "^2.11.0", 34 | "prettier": "^2.1.2", 35 | "prop-types": "15.7.2", 36 | "react": "^17.0.1", 37 | "react-dom": "^17.0.1", 38 | "react-github-button": "0.1.11", 39 | "sass": "1.29.0", 40 | "typescript": "^4.0.5" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/App/gh-button.scss: -------------------------------------------------------------------------------- 1 | // Source: https://github.com/benjycui/react-github-button 2 | 3 | .github-btn { 4 | font: bold 11px/14px "Helvetica Neue", Helvetica, Arial, sans-serif; 5 | height: 20px; 6 | overflow: hidden; 7 | } 8 | .gh-btn, 9 | .gh-count, 10 | .gh-ico { 11 | float: left; 12 | } 13 | .gh-btn, 14 | .gh-count { 15 | padding: 2px 5px 2px 4px; 16 | color: #333; 17 | text-decoration: none; 18 | white-space: nowrap; 19 | cursor: pointer; 20 | border-radius: 3px; 21 | } 22 | .gh-btn { 23 | background-color: #eee; 24 | background-image: -webkit-gradient( 25 | linear, 26 | left top, 27 | left bottom, 28 | color-stop(0, #fcfcfc), 29 | color-stop(100%, #eee) 30 | ); 31 | background-image: -webkit-linear-gradient(top, #fcfcfc 0, #eee 100%); 32 | background-image: -moz-linear-gradient(top, #fcfcfc 0, #eee 100%); 33 | background-image: -ms-linear-gradient(top, #fcfcfc 0, #eee 100%); 34 | background-image: -o-linear-gradient(top, #fcfcfc 0, #eee 100%); 35 | background-image: linear-gradient(to bottom, #fcfcfc 0, #eee 100%); 36 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fcfcfc', endColorstr='#eeeeee', GradientType=0); 37 | background-repeat: no-repeat; 38 | border: 1px solid #d5d5d5; 39 | } 40 | .gh-btn:hover, 41 | .gh-btn:focus { 42 | text-decoration: none; 43 | background-color: #ddd; 44 | background-image: -webkit-gradient( 45 | linear, 46 | left top, 47 | left bottom, 48 | color-stop(0, #eee), 49 | color-stop(100%, #ddd) 50 | ); 51 | background-image: -webkit-linear-gradient(top, #eee 0, #ddd 100%); 52 | background-image: -moz-linear-gradient(top, #eee 0, #ddd 100%); 53 | background-image: -ms-linear-gradient(top, #eee 0, #ddd 100%); 54 | background-image: -o-linear-gradient(top, #eee 0, #ddd 100%); 55 | background-image: linear-gradient(to bottom, #eee 0, #ddd 100%); 56 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#eeeeee', endColorstr='#dddddd', GradientType=0); 57 | border-color: #ccc; 58 | } 59 | .gh-btn:active { 60 | background-image: none; 61 | background-color: #dcdcdc; 62 | border-color: #b5b5b5; 63 | box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15); 64 | } 65 | .gh-ico { 66 | width: 14px; 67 | height: 14px; 68 | margin-right: 4px; 69 | background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2ZXJzaW9uPSIxLjEiIGlkPSJMYXllcl8xIiB4PSIwcHgiIHk9IjBweCIgd2lkdGg9IjQwcHgiIGhlaWdodD0iNDBweCIgdmlld0JveD0iMTIgMTIgNDAgNDAiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMTIgMTIgNDAgNDAiIHhtbDpzcGFjZT0icHJlc2VydmUiPjxwYXRoIGZpbGw9IiMzMzMzMzMiIGQ9Ik0zMiAxMy40Yy0xMC41IDAtMTkgOC41LTE5IDE5YzAgOC40IDUuNSAxNS41IDEzIDE4YzEgMC4yIDEuMy0wLjQgMS4zLTAuOWMwLTAuNSAwLTEuNyAwLTMuMiBjLTUuMyAxLjEtNi40LTIuNi02LjQtMi42QzIwIDQxLjYgMTguOCA0MSAxOC44IDQxYy0xLjctMS4yIDAuMS0xLjEgMC4xLTEuMWMxLjkgMC4xIDIuOSAyIDIuOSAyYzEuNyAyLjkgNC41IDIuMSA1LjUgMS42IGMwLjItMS4yIDAuNy0yLjEgMS4yLTIuNmMtNC4yLTAuNS04LjctMi4xLTguNy05LjRjMC0yLjEgMC43LTMuNyAyLTUuMWMtMC4yLTAuNS0wLjgtMi40IDAuMi01YzAgMCAxLjYtMC41IDUuMiAyIGMxLjUtMC40IDMuMS0wLjcgNC44LTAuN2MxLjYgMCAzLjMgMC4yIDQuNyAwLjdjMy42LTIuNCA1LjItMiA1LjItMmMxIDIuNiAwLjQgNC42IDAuMiA1YzEuMiAxLjMgMiAzIDIgNS4xYzAgNy4zLTQuNSA4LjktOC43IDkuNCBjMC43IDAuNiAxLjMgMS43IDEuMyAzLjVjMCAyLjYgMCA0LjYgMCA1LjJjMCAwLjUgMC40IDEuMSAxLjMgMC45YzcuNS0yLjYgMTMtOS43IDEzLTE4LjFDNTEgMjEuOSA0Mi41IDEzLjQgMzIgMTMuNHoiLz48L3N2Zz4="); 70 | background-size: 100% 100%; 71 | background-repeat: no-repeat; 72 | } 73 | .gh-count { 74 | position: relative; 75 | display: none; /* hidden to start */ 76 | margin-left: 4px; 77 | background-color: #fafafa; 78 | border: 1px solid #d4d4d4; 79 | } 80 | .gh-count:hover, 81 | .gh-count:focus { 82 | color: #4183c4; 83 | } 84 | .gh-count:before, 85 | .gh-count:after { 86 | content: ""; 87 | position: absolute; 88 | display: inline-block; 89 | width: 0; 90 | height: 0; 91 | border-color: transparent; 92 | border-style: solid; 93 | } 94 | .gh-count:before { 95 | top: 50%; 96 | left: -3px; 97 | margin-top: -4px; 98 | border-width: 4px 4px 4px 0; 99 | border-right-color: #fafafa; 100 | } 101 | .gh-count:after { 102 | top: 50%; 103 | left: -4px; 104 | z-index: -1; 105 | margin-top: -5px; 106 | border-width: 5px 5px 5px 0; 107 | border-right-color: #d4d4d4; 108 | } 109 | .github-btn-large { 110 | height: 30px; 111 | } 112 | .github-btn-large .gh-btn, 113 | .github-btn-large .gh-count { 114 | padding: 3px 10px 3px 8px; 115 | font-size: 16px; 116 | line-height: 22px; 117 | border-radius: 4px; 118 | } 119 | .github-btn-large .gh-ico { 120 | width: 20px; 121 | height: 20px; 122 | } 123 | .github-btn-large .gh-count { 124 | margin-left: 6px; 125 | } 126 | .github-btn-large .gh-count:before { 127 | left: -5px; 128 | margin-top: -6px; 129 | border-width: 6px 6px 6px 0; 130 | } 131 | .github-btn-large .gh-count:after { 132 | left: -6px; 133 | margin-top: -7px; 134 | border-width: 7px 7px 7px 0; 135 | } 136 | -------------------------------------------------------------------------------- /src/App/index.scss: -------------------------------------------------------------------------------- 1 | html, body, #root { 2 | height: 100%; 3 | font-family: 'Courier New', Courier, monospace; 4 | } 5 | 6 | * { 7 | box-sizing: border-box; 8 | } 9 | 10 | .app { 11 | height: 100%; 12 | display: flex; 13 | flex-direction: column; 14 | padding: 20px; 15 | 16 | > header { 17 | font-weight: bold; 18 | margin-bottom: 0.5em; 19 | font-size: 25px; 20 | position: relative; 21 | display: flex; 22 | 23 | h1 { 24 | flex: 1; 25 | } 26 | 27 | .menu { 28 | padding-right: 160px; 29 | position: relative; 30 | top: -4px; 31 | a { 32 | color: black; 33 | font-weight: normal; 34 | margin-left: 30px; 35 | font-size: 15px; 36 | text-decoration: none; 37 | opacity: 0.75; 38 | } 39 | a:hover { 40 | opacity: 1; 41 | font-weight: bold; 42 | } 43 | } 44 | 45 | .github-btn { 46 | position: absolute; 47 | right: 0; 48 | top: -5px; 49 | font-family: Courier, monospace; 50 | } 51 | } 52 | 53 | > .editor { 54 | flex: 1; 55 | overflow: hidden; 56 | border: 1px solid rgba(0,0,0,0.25); 57 | } 58 | 59 | > .error { 60 | color: red; 61 | font-weight: bold; 62 | margin-top: 1em; 63 | } 64 | 65 | > .progress { 66 | margin-top: 0px; 67 | 68 | &::after { 69 | content: ''; 70 | display: block; 71 | width: calc(var(--length)*100%); 72 | height: 20px; 73 | background: hsl(calc(120 + var(--length)*720), 100%, 50%); 74 | transition: all 0.1s linear; 75 | } 76 | } 77 | 78 | > .results { 79 | 80 | margin-top: 20px; 81 | 82 | .result { 83 | 84 | border-bottom: 2px solid transparent; 85 | 86 | .name { 87 | padding-right: 1em; 88 | font-weight: bold; 89 | white-space: nowrap; 90 | } 91 | 92 | .time { 93 | width: 10em; 94 | text-align: right; 95 | padding-right: 1em; 96 | white-space: nowrap; 97 | } 98 | 99 | .bar { 100 | width: 99%; 101 | position: relative; 102 | overflow: hidden; 103 | 104 | &::after { 105 | content: ''; 106 | background: hsl(calc(var(--color)*270 + 70), 80%, 50%); 107 | display: block; 108 | width: calc(var(--length)*100%); 109 | height: 100%; 110 | position: absolute; 111 | } 112 | } 113 | } 114 | } 115 | 116 | > .run-me { 117 | cursor: pointer; 118 | margin-top: 20px; 119 | height: 50px; 120 | font-size: 40px; 121 | font-weight: bold; 122 | font-family: 'Courier New', Courier, monospace; 123 | } 124 | 125 | > .run-me:disabled { 126 | font-size: 20px; 127 | } 128 | } -------------------------------------------------------------------------------- /src/App/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useMemo, useEffect } from 'react' 2 | import Editor, { EditorDidMount } from '@monaco-editor/react' 3 | import GithubButton from 'react-github-button' 4 | import pako from 'pako' 5 | 6 | import { 7 | InitialValuesFunction, 8 | EqualFunction, 9 | TestFunction, 10 | BenchmarkInput, 11 | Results, 12 | measureExecutionTime 13 | } from '../benchmark' 14 | 15 | import './gh-button.scss' 16 | import './index.scss' 17 | 18 | const defaultTitle = 'What Code Is Faster?' 19 | 20 | let defaultCode = `benchmark('${defaultTitle}', function initialValue() { 21 | return Math.random() 22 | }, { 23 | hypot(prev) { 24 | const a = (prev + 1) % 1 25 | const b = (prev - 1) % 1 26 | return Math.hypot(a, b); 27 | }, 28 | sqrt(prev) { 29 | const a = (prev + 1) % 1 30 | const b = (prev - 1) % 1 31 | return Math.sqrt(a*a + b*b); 32 | } 33 | }) 34 | ` 35 | 36 | function bytesToStr(bytes: Uint8Array): string { 37 | return bytes.reduce((s, n) => s + String.fromCharCode(n), '') 38 | } 39 | 40 | function compress(str: string): string { 41 | const bytes = pako.deflate(str, { level: 9 }) 42 | return btoa(bytesToStr(bytes)) 43 | } 44 | 45 | function decompress(base64: string): string { 46 | const arr = new Uint8Array([...atob(base64)].map(c => c.charCodeAt(0))) 47 | return bytesToStr(pako.inflate(arr)) 48 | } 49 | 50 | const codeParam = new URL(document.location.toString()).searchParams.get('code') 51 | if (codeParam) { 52 | try { 53 | defaultCode = decompress(codeParam) || defaultCode 54 | } catch (e) { 55 | defaultCode = `// Failed to decode URL: ${(e as Error).toString()}` 56 | } 57 | } 58 | 59 | export function App() { 60 | const [error, setError] = useState() 61 | const [state, setState] = useState({ 62 | title: defaultTitle, 63 | benchmarkInput: null as BenchmarkInput | null 64 | }) 65 | 66 | const [progress, setProgress] = useState<{ 67 | text: string 68 | time: number 69 | }>() 70 | 71 | const [results, setResults] = useState() 72 | 73 | function updateWithCode(code: string) { 74 | try { 75 | const urlParam = `?code=${encodeURIComponent(compress(code))}` 76 | const url = window.location.href.replace(/\?code=.*$/, '') 77 | 78 | if ((url + urlParam).length > 2048) { 79 | setError(new Error("Code is too long, won't be able to save it in the URL")) 80 | } else { 81 | history.replaceState({}, '', urlParam) 82 | } 83 | 84 | const fn = new Function('benchmark', code) 85 | fn(function benchmark( 86 | name: string, 87 | opts: { initialValues: InitialValuesFunction, equal?: EqualFunction } | (() => any), 88 | tests: Record 89 | ) { 90 | document.title = name 91 | if (typeof opts === 'function') { // normalize legacy opts format (it was just a single InitialValueFunction) 92 | const initialValue = opts 93 | opts = { 94 | initialValues() { 95 | const result: Record = {} 96 | const seed = initialValue() 97 | for (const name of Object.keys(tests)) result[name] = seed 98 | return result 99 | } 100 | } 101 | } 102 | setState({ 103 | title: name, 104 | benchmarkInput: { tests: Object.values(tests), ...opts } 105 | }) 106 | }) 107 | setError(undefined) 108 | } catch (e) { 109 | console.error(e) 110 | setError(e as Error) 111 | } 112 | } 113 | 114 | const handleEditorDidMount: EditorDidMount = (getCode, editor) => { 115 | editor.onDidChangeModelContent(() => { 116 | updateWithCode(getCode()) 117 | }) 118 | } 119 | 120 | useEffect(() => { 121 | updateWithCode(defaultCode) 122 | }, []) 123 | 124 | async function run() { 125 | try { 126 | if (state.benchmarkInput) { 127 | setResults( 128 | await measureExecutionTime(state.benchmarkInput, (text: string, time: number) => { 129 | setProgress({ text, time }) 130 | }) 131 | ) 132 | setProgress(undefined) 133 | } 134 | } catch (e) { 135 | setError(e as Error) 136 | } 137 | } 138 | 139 | useEffect(() => { 140 | results?.forEach(r => console.log(r.name + ' outputs:', r.outputs)) 141 | }, [results]) 142 | 143 | const longestMinExecutionTime = useMemo(() => { 144 | return results ? Math.max(...results.map(x => x.minExecutionTime)) : 0 145 | }, [results]) 146 | 147 | return ( 148 |
149 |
150 |

{state.title}

151 |
152 | README.md 153 | Examples 154 |
155 | 156 |
157 |
158 | 169 |
170 | {progress && progress.time > 0 && ( 171 |
175 | )} 176 | {error && ( 177 |
178 | {error.constructor.name}: {error.message} 179 |
180 | )} 181 | {results && ( 182 | 183 | 184 | {results.map((result, i) => ( 185 | 186 | 187 | 188 | 198 | ))} 199 | 200 |
{result.name}{result.minExecutionTime.toFixed(4)} µs 197 |
201 | )} 202 | 205 |
206 | ) 207 | } 208 | -------------------------------------------------------------------------------- /src/benchmark/index.tsx: -------------------------------------------------------------------------------- 1 | import deepEqual from 'deep-equal' 2 | 3 | export type InitialValuesFunction = () => any 4 | export type TestFunction = (input: any) => any 5 | export type EqualFunction = (a: any, b: any) => any 6 | export type BenchmarkInput = { initialValues: InitialValuesFunction; equal?: EqualFunction, tests: TestFunction[] } 7 | 8 | export type Microseconds = number 9 | 10 | export type ProgressCallback = (status: string, progress: number) => void 11 | export type Results = Array<{ 12 | name: string 13 | minExecutionTime: Microseconds 14 | executionTimes: Microseconds[] 15 | outputs: any[] 16 | }> 17 | 18 | function defaultEqual(a: any, b: any) { 19 | // TODO: provide custom comparator to deepEqual 20 | if (typeof a === 'number' && typeof b === 'number') { 21 | return Math.abs(a - b) < 0.00000001 22 | } else { 23 | return deepEqual(a, b) 24 | } 25 | } 26 | 27 | export function checkSoundness({ initialValues, equal = defaultEqual, tests }: BenchmarkInput) { 28 | if (deepEqual(initialValues(), initialValues())) { 29 | throw new Error('initialValue() must return random values!') 30 | } 31 | if (!tests.length) { 32 | throw new Error('Define at least one test function') 33 | } 34 | for (const test of tests) { 35 | if (typeof test !== 'function') { 36 | throw new Error(`All tests must be functions!`) 37 | } 38 | if (!test.name) { 39 | throw new Error('All test functions must have names') 40 | } 41 | const seed = initialValues() 42 | if (!equal(test(seed[test.name]), test(seed[test.name]))) { 43 | throw new Error( 44 | `${test.name}() must depend only on its input (seems that it returns random results...)` 45 | ) 46 | } 47 | if (equal(test(initialValues()[test.name]), test(initialValues()[test.name]))) { 48 | throw new Error( 49 | `${test.name}() must depend on its input (seems that it returns the same result regardless of that...)` 50 | ) 51 | } 52 | } 53 | const seed = initialValues() 54 | const result0 = tests[0](seed[tests[0].name]) 55 | for (const test of tests.slice(1)) { 56 | const result = test(seed[test.name]) 57 | if (!equal(result, result0)) { 58 | throw new Error(`The output of ${test.name}() must be the same as of ${tests[0].name}()`) 59 | } 60 | } 61 | } 62 | 63 | const microsecondsPerSample = 100 * 1000 64 | 65 | async function quicklyDetermineRoughAverageExecutionTime( 66 | test: TestFunction, 67 | theInitialValue: any 68 | ): Promise { 69 | let numCycles = 32 70 | let input = theInitialValue 71 | while (true) { 72 | let t0 = performance.now() 73 | input = theInitialValue 74 | for (let i = 0; i < numCycles; i++) { 75 | input = test(input) 76 | } 77 | // Increase numCycles two-fold until execution time is more than 50ms 78 | const execTimeMs = performance.now() - t0 79 | if (execTimeMs > 50) { 80 | return (execTimeMs * 1000) / numCycles // to µs 81 | } 82 | numCycles *= 2 83 | produceSideEffect(input) 84 | await sleep(1) 85 | } 86 | } 87 | 88 | export async function measureExecutionTime( 89 | input: BenchmarkInput, 90 | onProgress: ProgressCallback = (status: string, progress: number) => { } 91 | ) { 92 | checkSoundness(input) 93 | 94 | const warmupRuns = 3 95 | const warmupSamples = 5 96 | const measureSamples = 50 97 | const totalNumSamples = input.tests.length * (warmupSamples * warmupRuns + measureSamples) 98 | let samplesCollected = 0 99 | 100 | const cyclesPerSample: { [key: string]: Microseconds } = {} 101 | const initialValues = input.initialValues() 102 | 103 | for (const test of input.tests) { 104 | onProgress(`Preparing ${test.name}()...`, 0) 105 | const avgTime = await quicklyDetermineRoughAverageExecutionTime(test, initialValues[test.name]) // µs 106 | cyclesPerSample[test.name] = Math.ceil(microsecondsPerSample / avgTime) 107 | } 108 | 109 | for (let i = 0; i < warmupRuns; i++) { 110 | const results = await runSampling( 111 | input.tests, 112 | warmupSamples, 113 | cyclesPerSample, 114 | initialValues, 115 | (name, i) => { 116 | onProgress(`Warming up ${name}(), sample #${i}...`, ++samplesCollected / totalNumSamples) 117 | } 118 | ) 119 | for (const result of results) { 120 | cyclesPerSample[result.name] = microsecondsPerSample / result.minExecutionTime 121 | } 122 | } 123 | 124 | return runSampling(input.tests, measureSamples, cyclesPerSample, initialValues, (name, i) => { 125 | onProgress(`Measuring ${name}(), sample #${i}...`, ++samplesCollected / totalNumSamples) 126 | }) 127 | } 128 | 129 | async function runSampling( 130 | tests: BenchmarkInput['tests'], 131 | numSamples: number, 132 | cyclesPerSample: { [key: string]: Microseconds }, 133 | initialValues: Record, 134 | onProgress = (name: string, sample: number) => { } 135 | ) { 136 | let samplesCollected = 0 137 | let input: any = null 138 | const results: Results = [] 139 | 140 | for (const test of tests) { 141 | const numCycles = cyclesPerSample[test.name] 142 | const executionTimes: Microseconds[] = [] 143 | const outputs: any[] = [] 144 | input = initialValues[test.name] 145 | 146 | for (let i = 0; i < numSamples; i++) { 147 | let t0 = performance.now() 148 | 149 | for (let j = 0; j < numCycles; j++) { 150 | input = test(input) 151 | } 152 | 153 | onProgress(test.name, i) 154 | 155 | executionTimes.push(((performance.now() - t0) * 1000) / numCycles) 156 | outputs.push(input) 157 | await sleep(50) 158 | } 159 | 160 | results.push({ 161 | name: test.name, 162 | minExecutionTime: Math.min(...executionTimes), 163 | executionTimes, 164 | outputs 165 | }) 166 | } 167 | 168 | return results.sort((a, b) => a.minExecutionTime - b.minExecutionTime) 169 | } 170 | 171 | function produceSideEffect(value: any) { 172 | // @ts-ignore 173 | const array = globalThis.__benchmarkSideEffect || (globalThis.__benchmarkSideEffect = []) 174 | array.push(value) 175 | } 176 | 177 | function sleep(ms: number) { 178 | return new Promise(resolve => setTimeout(resolve, ms)) 179 | } 180 | -------------------------------------------------------------------------------- /src/common/reset.css: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, 7 | body, 8 | div, 9 | span, 10 | applet, 11 | object, 12 | iframe, 13 | h1, 14 | h2, 15 | h3, 16 | h4, 17 | h5, 18 | h6, 19 | p, 20 | blockquote, 21 | pre, 22 | a, 23 | abbr, 24 | acronym, 25 | address, 26 | big, 27 | cite, 28 | code, 29 | del, 30 | dfn, 31 | em, 32 | img, 33 | ins, 34 | kbd, 35 | q, 36 | s, 37 | samp, 38 | small, 39 | strike, 40 | strong, 41 | sub, 42 | sup, 43 | tt, 44 | var, 45 | b, 46 | u, 47 | i, 48 | center, 49 | dl, 50 | dt, 51 | dd, 52 | ol, 53 | ul, 54 | li, 55 | fieldset, 56 | form, 57 | label, 58 | legend, 59 | table, 60 | caption, 61 | tbody, 62 | tfoot, 63 | thead, 64 | tr, 65 | th, 66 | td, 67 | article, 68 | aside, 69 | canvas, 70 | details, 71 | embed, 72 | figure, 73 | figcaption, 74 | footer, 75 | header, 76 | hgroup, 77 | menu, 78 | nav, 79 | output, 80 | ruby, 81 | section, 82 | summary, 83 | time, 84 | mark, 85 | audio, 86 | video { 87 | margin: 0; 88 | padding: 0; 89 | border: 0; 90 | font-size: 100%; 91 | font: inherit; 92 | vertical-align: baseline; 93 | } 94 | /* HTML5 display-role reset for older browsers */ 95 | article, 96 | aside, 97 | details, 98 | figcaption, 99 | figure, 100 | footer, 101 | header, 102 | hgroup, 103 | menu, 104 | nav, 105 | section { 106 | display: block; 107 | } 108 | body { 109 | line-height: 1; 110 | } 111 | ol, 112 | ul { 113 | list-style: none; 114 | } 115 | blockquote, 116 | q { 117 | quotes: none; 118 | } 119 | blockquote:before, 120 | blockquote:after, 121 | q:before, 122 | q:after { 123 | content: ""; 124 | content: none; 125 | } 126 | table { 127 | border-collapse: collapse; 128 | border-spacing: 0; 129 | } 130 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xpl/what-code-is-faster/a86a9125a6d600259cfa5ca93b9f7d3886223da9/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | What Code Is Faster? 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import 'panic-overlay' 2 | 3 | import React from 'react' 4 | import { render } from 'react-dom' 5 | 6 | import './common/reset.css' 7 | import { App } from './App' 8 | 9 | render(, document.getElementById('root')) 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "noImplicitAny": true, 14 | "strict": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react" 22 | }, 23 | "include": [ 24 | "src" 25 | ], 26 | "exclude": [ 27 | "node_modules" 28 | ] 29 | } 30 | --------------------------------------------------------------------------------