├── .gitattributes ├── .github └── workflows │ ├── pages.yml │ └── release.yml ├── .gitignore ├── .vscode ├── settings.json └── tasks.json ├── CHANGELOG.md ├── CNAME ├── LICENSE ├── README.md ├── declare.d.ts ├── package.json ├── pnpm-lock.yaml ├── src ├── 404.html ├── App.module.styl ├── App.tsx ├── declare.d.ts ├── icons │ ├── Add.svg │ ├── Browse.svg │ ├── Create.svg │ ├── Drag.svg │ ├── GitHub.svg │ ├── Logo.svg │ ├── Remove.svg │ ├── Settings.svg │ ├── Share.svg │ ├── Start.svg │ └── Stop.svg ├── index.html ├── layout │ ├── Main │ │ ├── Footer │ │ │ ├── Footer.module.styl │ │ │ ├── Footer.tsx │ │ │ └── index.ts │ │ ├── Header │ │ │ ├── Header.module.styl │ │ │ ├── Header.tsx │ │ │ └── index.ts │ │ ├── Layout │ │ │ ├── Layout.module.styl │ │ │ ├── Layout.tsx │ │ │ └── index.ts │ │ └── index.ts │ └── Suite │ │ ├── Content │ │ ├── After.tsx │ │ ├── Before.tsx │ │ ├── Content.module.styl │ │ ├── Content.tsx │ │ ├── Tests.tsx │ │ └── index.ts │ │ ├── Footer │ │ ├── Footer.module.styl │ │ ├── Footer.tsx │ │ └── index.ts │ │ ├── Header │ │ ├── Header.module.styl │ │ ├── Header.tsx │ │ ├── SettingsModal.module.styl │ │ ├── SettingsModal.tsx │ │ └── index.ts │ │ ├── Layout │ │ ├── Layout.module.styl │ │ ├── Layout.tsx │ │ └── index.ts │ │ ├── Section │ │ ├── Section.module.styl │ │ ├── Section.tsx │ │ └── index.ts │ │ └── index.ts ├── lib │ ├── benchmark │ │ ├── benchmark_src.js │ │ ├── index.ts │ │ ├── steadfast-worker.ts │ │ ├── t-table.ts │ │ ├── utils.ts │ │ └── worker.ts │ ├── mote.ts │ ├── sotore │ │ ├── equal.ts │ │ ├── index.ts │ │ ├── middleware │ │ │ ├── assign.ts │ │ │ ├── compute.ts │ │ │ ├── index.ts │ │ │ ├── persistent.ts │ │ │ └── react.ts │ │ └── react.ts │ ├── tsCompiler.ts │ ├── useHash.ts │ └── withId.tsx ├── models │ ├── suite.ts │ └── suiteBase64.ts ├── pages │ ├── Landing.tsx │ ├── NotFound.tsx │ ├── Suite.tsx │ └── UnderConstruction.tsx └── ui │ ├── Button │ ├── Button.module.styl │ ├── Button.tsx │ └── index.ts │ ├── ClotGroup │ ├── ClotGroup.module.styl │ ├── ClotGroup.tsx │ └── index.ts │ ├── CodeBlock │ ├── CodeBlock.module.styl │ ├── CodeBlock.tsx │ ├── index.ts │ └── theme.ts │ ├── Form │ ├── Context.ts │ ├── Form.tsx │ ├── FormTextField.module.styl │ ├── FormTextField.tsx │ └── index.ts │ ├── Group │ ├── Group.module.styl │ ├── Group.tsx │ └── index.ts │ ├── Icon │ ├── Icon.tsx │ └── index.ts │ ├── Link │ ├── Link.module.styl │ ├── Link.tsx │ └── index.ts │ ├── Modal │ ├── Modal.module.styl │ ├── Modal.tsx │ └── index.ts │ ├── Monaco │ ├── Monaco.tsx │ └── index.ts │ ├── TestCase │ ├── TestCase.module.styl │ ├── TestCase.strings.ts │ ├── TestCase.tsx │ └── index.ts │ ├── TestCases │ ├── TestCases.tsx │ └── index.ts │ ├── Text │ ├── Text.module.styl │ ├── Text.tsx │ ├── Title.module.styl │ ├── Title.tsx │ └── index.ts │ └── TextField │ ├── TextField.module.styl │ ├── TextField.tsx │ └── index.ts ├── tsconfig.json └── vite.config.ts /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/workflows/pages.yml: -------------------------------------------------------------------------------- 1 | name: Build and Deploy 2 | on: [ push ] 3 | 4 | jobs: 5 | build: 6 | concurrency: ci-${{ github.ref }} 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - name: Checkout 🛎️ 11 | uses: actions/checkout@v2 12 | 13 | - uses: pnpm/action-setup@v2.0.1 14 | name: Install pnpm 15 | id: pnpm-install 16 | with: 17 | version: 7 18 | run_install: false 19 | 20 | - name: Get pnpm store directory 21 | id: pnpm-cache 22 | run: | 23 | echo "::set-output name=pnpm_cache_dir::$(pnpm store path)" 24 | 25 | - uses: actions/cache@v3 26 | name: Setup pnpm cache 27 | with: 28 | path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }} 29 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 30 | restore-keys: | 31 | ${{ runner.os }}-pnpm-store- 32 | 33 | - name: Install and Build 🔧 34 | run: | 35 | pnpm install 36 | pnpm build 37 | 38 | - name: Deploy 🚀 39 | uses: JamesIves/github-pages-deploy-action@v4.2.5 40 | with: 41 | branch: gh-pages 42 | folder: dist 43 | clean: true -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: 5 | - main 6 | jobs: 7 | release-please: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: google-github-actions/release-please-action@v3 11 | with: 12 | release-type: node 13 | package-name: release-please-action -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # parcel-bundler cache (https://parceljs.org/) 61 | .cache 62 | 63 | # next.js build output 64 | .next 65 | 66 | # nuxt.js build output 67 | .nuxt 68 | 69 | # vuepress build output 70 | .vuepress/dist 71 | 72 | # Serverless directories 73 | .serverless 74 | 75 | # FuseBox cache 76 | .fusebox/ 77 | 78 | dist/ 79 | *.tmp/ 80 | .DS_Store -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "dev", 8 | "type": "npm", 9 | "script": "dev", 10 | "problemMatcher": [] 11 | }, 12 | { 13 | "label": "build", 14 | "type": "npm", 15 | "script": "build", 16 | "group": "build" 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [1.1.0](https://github.com/ubenchan/frontend/compare/v1.0.0...v1.1.0) (2022-09-11) 4 | 5 | 6 | ### Features 7 | 8 | * GitHub link ([0eae3ba](https://github.com/ubenchan/frontend/commit/0eae3ba7b8c3fd63193237b43f1c351677b8eb17)) 9 | * suite config modal ([e147702](https://github.com/ubenchan/frontend/commit/e14770283b6a0ec9b0533b87677f527c2bb94496)) 10 | 11 | ## 1.0.0 (2022-08-29) 12 | 13 | 14 | ### Features 15 | 16 | * description ([7cfc8a5](https://github.com/ubenchan/frontend/commit/7cfc8a510bf3478e74914ed758d9be3b28080a94)) 17 | * initial ([da2e366](https://github.com/ubenchan/frontend/commit/da2e366dfd20abb4017d0a9e3016604ca7ba815d)) 18 | * migrate to vite ([28f05c6](https://github.com/ubenchan/frontend/commit/28f05c6138342fd4f34bf761514bc6ed7faf7979)) 19 | * Product Hunt badge ([3115a47](https://github.com/ubenchan/frontend/commit/3115a470479fc01778477181311eb977dfc8a638)) 20 | * readme ([c8ca250](https://github.com/ubenchan/frontend/commit/c8ca250298319315a7ad2662302ed499e0968dc7)) 21 | * release ([96c7f5a](https://github.com/ubenchan/frontend/commit/96c7f5af57f8d2c81b748791ff2b178c6fd8fa1a)) 22 | * release action ([e3073a2](https://github.com/ubenchan/frontend/commit/e3073a280e66965c18ed25da139ad8d295b2bcec)) 23 | * spa redirect ([e7e3a4d](https://github.com/ubenchan/frontend/commit/e7e3a4deb73bad1f6687391b2de8ad473a041f26)) 24 | 25 | 26 | ### Bug Fixes 27 | 28 | * code font ([ea02d14](https://github.com/ubenchan/frontend/commit/ea02d141c4b9ecddf6a6b9d1f3220488ce9b9e4f)) 29 | * typo ([6db7f81](https://github.com/ubenchan/frontend/commit/6db7f81c944cb681d5e1cd4535b3d8e4bf585efd)) 30 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | uben.ch -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Vlad Afonin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![](https://repository-images.githubusercontent.com/529920925/75b5186d-bfd6-4990-ba49-83f5233dbd28)](https://uben.ch) 2 | 3 |
4 | 5 | [![Source license](https://img.shields.io/github/license/ubenchan/frontend?color=151515&label=©&labelColor=151515)](https://github.com/ubenchan/frontend/blob/main/LICENSE) ![TS version](https://img.shields.io/github/package-json/dependency-version/ubenchan/frontend/dev/typescript?color=151515&logo=typescript&label) ![React version](https://img.shields.io/github/package-json/dependency-version/ubenchan/frontend/dev/react?color=151515&logo=react&label) [![PRs Welcome](https://img.shields.io/static/v1?label=PRs&message=Welcome&color=9ad638&labelColor=151515)](https://github.com/ubenchan/frontend/pulls) 6 | 7 |
8 | 9 | ## Key features 10 | 11 |
12 | 13 | - **Pretty UI:** Responsive user friendly interface 14 | - **Monaco editor:** The most powerful editor from VSCode in your browser 15 | - **Web workers:** Rewritten from scratch Benchmark.js engine with modern technologies 16 | - **Sucrase TS compiler:** Write test suites on favourite language 17 | 18 |
19 | 20 |
21 | 22 | [![UBenchan - Beautiful browser benchmarks | Product Hunt](https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=357622)](https://www.producthunt.com/posts/ubenchan?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-ubenchan) 23 | 24 |
25 | -------------------------------------------------------------------------------- /declare.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'svg-sprite-loader/plugin.js' 2 | declare module 'webpack-bundle-analyzer' 3 | declare module 'vite-plugin-css-modules' 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ubenchan-frontend", 3 | "version": "1.1.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "type": "module", 7 | "scripts": { 8 | "dev": "vite", 9 | "build": "vite build", 10 | "tsc": "tsc --noEmit" 11 | }, 12 | "devDependencies": { 13 | "@types/node": "^18.7.13", 14 | "@types/react": "^18.0.17", 15 | "@types/react-dom": "^18.0.6", 16 | "@types/use-sync-external-store": "^0.0.3", 17 | "@vitejs/plugin-react": "^2.0.1", 18 | "cnj": "^1.0.3", 19 | "git-rev-promises": "^1.1.0", 20 | "lz-ts": "^1.1.2", 21 | "nanoid": "^4.0.0", 22 | "react": "^18.2.0", 23 | "react-dom": "^18.2.0", 24 | "react-refresh": "^0.14.0", 25 | "react-router-dom": "^6.3.0", 26 | "resolvable-promise": "^2.0.3", 27 | "stylus": "^0.59.0", 28 | "typescript": "^4.8.2", 29 | "use-sync-external-store": "^1.2.0", 30 | "vite": "^3.0.9", 31 | "vite-plugin-svg-sprite": "^0.2.6", 32 | "vite-plugin-ts-alias": "^0.1.1" 33 | }, 34 | "dependencies": { 35 | "merge-anything": "^5.0.4", 36 | "monaco-editor": "^0.34.0", 37 | "sucrase": "^3.25.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Single Page Apps for GitHub Pages 6 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /src/App.module.styl: -------------------------------------------------------------------------------- 1 | 2 | *, ::before, ::after { 3 | box-sizing: border-box 4 | -webkit-tap-highlight-color: transparent 5 | } 6 | 7 | :root { 8 | --theme-green: #9ad638 9 | --theme-orange: #ffb74d 10 | --theme-red: #ef6950 11 | 12 | // Background 13 | --theme-bg-fill: #151515 14 | --theme-bg-fill-hover: lighten(#333, 5%) 15 | --theme-bg-fill-active: lighten(#333, 10%) 16 | 17 | --theme-bg-text: #fff 18 | --theme-bg-text-hover: darken(#fff, 10%) 19 | --theme-bg-text-active: darken(#fff, 15%) 20 | 21 | --theme-bg-text_secondary: #aaa 22 | --theme-bg-text_secondary-hover: darken(#ccc, 10%) 23 | --theme-bg-text_secondary-active: darken(#ccc, 15%) 24 | 25 | --theme-delimiter: #000 26 | 27 | // Surface 28 | --theme-surface-fill: #1f1f1f 29 | --theme-surface-text: #fff 30 | --theme-surface-text-hover: darken(#fff, 10%) 31 | 32 | // Primary 33 | --theme-primary-fill: #ef6950 34 | --theme-primary-fill-hover: darken(#ef6950, 10%) 35 | --theme-primary-fill-active: darken(#ef6950, 15%) 36 | 37 | --theme-primary-text: #fff 38 | --theme-primary-text-hover: darken(#fff, 10%) 39 | --theme-primary-text-active: darken(#fff, 15%) 40 | 41 | // Secondary 42 | --theme-secondary-fill: #555 43 | --theme-secondary-text: #fff 44 | --theme-secondary-fill-hover: darken(#555, 10%) 45 | 46 | --theme-secondary-text-hover: darken(#fff, 10%) 47 | --theme-secondary-fill-active: darken(#555, 15%) 48 | --theme-secondary-text-active: darken(#fff, 15%) 49 | } 50 | 51 | html { 52 | min-height: 100% 53 | background: var(--theme-bg-fill) 54 | color: var(--theme-bg-text) 55 | display: flex 56 | flex-direction: column 57 | } 58 | 59 | body { 60 | display: contents 61 | } 62 | 63 | :global(#root) { 64 | display: flex 65 | flex-direction: column 66 | flex: 1 67 | overflow-x: hidden 68 | } 69 | 70 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import { BrowserRouter, RouteObject, useRoutes } from 'react-router-dom' 4 | 5 | import { Modal } from 'ui/Modal' 6 | import { Suite } from 'page/Suite' 7 | import { Landing } from 'page/Landing' 8 | import { NotFound } from 'page/NotFound' 9 | import { UnderConstruction } from 'page/UnderConstruction' 10 | 11 | import './App.module.styl' 12 | 13 | const routes: RouteObject[] = [ 14 | { path: '/', element: }, 15 | { path: '/suite', element: }, 16 | { path: '/browse', element: }, 17 | { path: '*', element: }, 18 | ] 19 | 20 | function App() { 21 | return useRoutes(routes) 22 | } 23 | 24 | createRoot(document.getElementById('root')!).render( 25 | 26 | 27 | 28 | 29 | 30 | 31 | , 32 | ) 33 | -------------------------------------------------------------------------------- /src/declare.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.module.styl' 2 | declare module '*.module.css' 3 | 4 | declare module 'monaco-editor/*' 5 | 6 | declare module '*.svg' { 7 | const res: string 8 | export default res 9 | } 10 | -------------------------------------------------------------------------------- /src/icons/Add.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/Browse.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/Create.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/Drag.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/GitHub.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/Logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/Remove.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/Settings.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/Share.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/Start.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/Stop.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | UBenchan 8 | 9 | 10 | 11 | 37 | 38 | 39 | 40 |
41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/layout/Main/Footer/Footer.module.styl: -------------------------------------------------------------------------------- 1 | .footer { 2 | display: flex 3 | justify-content: space-between 4 | align-items: flex-end 5 | flex-wrap: wrap 6 | gap: 12px 7 | } 8 | 9 | .controls { 10 | flex: 1 11 | } 12 | 13 | .logo { 14 | height: 80px 15 | width: 80px 16 | } 17 | 18 | .title { 19 | text-align: right 20 | display: flex 21 | gap: 12px 22 | align-items: flex-end 23 | } 24 | -------------------------------------------------------------------------------- /src/layout/Main/Footer/Footer.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from 'ui/Link' 2 | import { Button } from 'ui/Button' 3 | import { Group } from 'ui/Group' 4 | import { Text, Title } from 'ui/Text' 5 | 6 | import Logo from 'icon/Logo.svg' 7 | import Browse from 'icon/Browse.svg' 8 | import Create from 'icon/Create.svg' 9 | import GitHub from 'icon/GitHub.svg' 10 | 11 | import { Icon } from 'ui/Icon' 12 | 13 | import { $footer, $controls, $title, $logo } from './Footer.module.styl' 14 | 15 | export const Footer = () => { 16 | return ( 17 |
18 | 19 | 22 | 25 | 33 | 34 | 35 |
36 |
37 | ubenchan 38 | 39 | by  40 | 41 | mytecor 42 | 43 | 44 |
45 | 46 | 47 |
48 |
49 | ) 50 | } 51 | -------------------------------------------------------------------------------- /src/layout/Main/Footer/index.ts: -------------------------------------------------------------------------------- 1 | export { Footer } from './Footer' 2 | -------------------------------------------------------------------------------- /src/layout/Main/Header/Header.module.styl: -------------------------------------------------------------------------------- 1 | .header { 2 | display: flex 3 | flex-wrap: wrap 4 | gap: 7.5vw 5 | } 6 | 7 | 8 | .example { 9 | --width: 400px 10 | --height: 204px 11 | --scale: 1.75 12 | border-radius: 12px 13 | pointer-events: none 14 | 15 | min-width: var(--width) 16 | margin-left: auto 17 | margin-right: calc(var(--width) * (var(--scale) - 1) - 7.5vw - 48px * var(--scale)) 18 | margin-bottom: calc(var(--height) * (var(--scale) - 1) + 7.5vw) 19 | 20 | transform-origin: left top 21 | transform: scale(var(--scale)) 22 | 23 | user-select: none 24 | } 25 | -------------------------------------------------------------------------------- /src/layout/Main/Header/Header.tsx: -------------------------------------------------------------------------------- 1 | import { Test } from 'model/suite' 2 | import { FC, ReactNode } from 'react' 3 | import { TestCases } from 'ui/TestCases' 4 | import { Title } from 'ui/Text' 5 | 6 | import { $header, $example } from './Header.module.styl' 7 | 8 | type Props = { 9 | children: ReactNode 10 | } 11 | 12 | const tests: Test[] = [ 13 | { 14 | id: 'example_fastest', 15 | source: '/*\nCreating a fumo\n*/', 16 | hz: 100, 17 | }, 18 | { 19 | id: 'example_slower', 20 | source: '/* You are\nlooking for what\nfumo is */', 21 | hz: 0, 22 | }, 23 | ] 24 | 25 | export const Header: FC = (props) => { 26 | return ( 27 |
28 | {props.children} 29 | 30 | 31 |
32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /src/layout/Main/Header/index.ts: -------------------------------------------------------------------------------- 1 | export { Header } from './Header' 2 | -------------------------------------------------------------------------------- /src/layout/Main/Layout/Layout.module.styl: -------------------------------------------------------------------------------- 1 | 2 | .layout { 3 | flex: 1 4 | 5 | display: flex 6 | flex-direction: column 7 | justify-content: space-between 8 | 9 | padding: 7.5vw 10 | } -------------------------------------------------------------------------------- /src/layout/Main/Layout/Layout.tsx: -------------------------------------------------------------------------------- 1 | import { FC, ReactNode } from 'react' 2 | import { $layout } from './Layout.module.styl' 3 | 4 | type Props = { 5 | children: ReactNode 6 | } 7 | 8 | export const Layout: FC = (props) => { 9 | return
{props.children}
10 | } 11 | -------------------------------------------------------------------------------- /src/layout/Main/Layout/index.ts: -------------------------------------------------------------------------------- 1 | export { Layout } from './Layout' 2 | -------------------------------------------------------------------------------- /src/layout/Main/index.ts: -------------------------------------------------------------------------------- 1 | export { Layout } from './Layout' 2 | export { Header } from './Header' 3 | export { Footer } from './Footer' 4 | -------------------------------------------------------------------------------- /src/layout/Suite/Content/After.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { CodeBlock } from 'ui/CodeBlock' 3 | import { useFilter } from 'lib/sotore' 4 | import { suite, updateTearddown } from 'model/suite' 5 | import { Section } from 'lay/Suite' 6 | 7 | export const After: FC = () => { 8 | const [value] = useFilter(suite, 'teardown') 9 | 10 | return ( 11 |
12 | 13 |
14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /src/layout/Suite/Content/Before.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { CodeBlock } from 'ui/CodeBlock' 3 | import { useFilter } from 'lib/sotore' 4 | import { suite, updateSetup } from 'model/suite' 5 | import { Section } from 'lay/Suite' 6 | 7 | export const Before: FC = () => { 8 | const [value] = useFilter(suite, 'setup') 9 | 10 | return ( 11 |
12 | 13 |
14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /src/layout/Suite/Content/Content.module.styl: -------------------------------------------------------------------------------- 1 | 2 | .content { 3 | flex: 1 4 | 5 | display: flex 6 | flex-direction: column 7 | gap: 16px 8 | } 9 | -------------------------------------------------------------------------------- /src/layout/Suite/Content/Content.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { After } from './After' 3 | import { Tests } from './Tests' 4 | import { Before } from './Before' 5 | 6 | import { $content } from './Content.module.styl' 7 | 8 | export const Content: FC = () => { 9 | return ( 10 |
11 | 12 | 13 | 14 |
15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /src/layout/Suite/Content/Tests.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { useFilter } from 'lib/sotore' 3 | import { 4 | addTest, 5 | removeTest, 6 | runTest, 7 | stopTest, 8 | suite, 9 | updateTestValue, 10 | } from 'model/suite' 11 | import { Section } from 'lay/Suite' 12 | import { Button } from 'ui/Button' 13 | 14 | import Add from 'icon/Add.svg' 15 | import { TestCases } from 'ui/TestCases' 16 | 17 | export const Tests: FC = () => { 18 | const [tests] = useFilter(suite, 'tests') 19 | 20 | return ( 21 |
22 | 23 | 26 | 27 | 28 | 35 |
36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /src/layout/Suite/Content/index.ts: -------------------------------------------------------------------------------- 1 | export { Content } from './Content' 2 | -------------------------------------------------------------------------------- /src/layout/Suite/Footer/Footer.module.styl: -------------------------------------------------------------------------------- 1 | .footer { 2 | text-align: center 3 | margin-top: 16px 4 | } 5 | -------------------------------------------------------------------------------- /src/layout/Suite/Footer/Footer.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { Text } from 'ui/Text' 3 | import { $footer } from './Footer.module.styl' 4 | 5 | export const Footer: FC = () => { 6 | return ( 7 |
8 | 9 | ubenchan ∞  10 | 11 | dev 12 |
13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /src/layout/Suite/Footer/index.ts: -------------------------------------------------------------------------------- 1 | export { Footer } from './Footer' 2 | -------------------------------------------------------------------------------- /src/layout/Suite/Header/Header.module.styl: -------------------------------------------------------------------------------- 1 | .header { 2 | display: flex 3 | flex-wrap: wrap 4 | align-items: flex-start 5 | gap: 12px 6 | padding: 16px 7 | } 8 | 9 | .logo { 10 | height: 64px 11 | width: 64px 12 | vertical-align: middle 13 | } 14 | 15 | .title { 16 | display: flex 17 | flex-direction: column 18 | } 19 | 20 | .controls { 21 | margin-left: auto 22 | } -------------------------------------------------------------------------------- /src/layout/Suite/Header/Header.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useCallback, useRef, useState } from 'react' 2 | 3 | import Share from 'icon/Share.svg' 4 | import Settings from 'icon/Settings.svg' 5 | import Stop from 'icon/Stop.svg' 6 | import Start from 'icon/Start.svg' 7 | import Logo from 'icon/Logo.svg' 8 | import { Icon } from 'ui/Icon' 9 | import { Button } from 'ui/Button' 10 | import { Text, Title } from 'ui/Text' 11 | import { Link } from 'ui/Link' 12 | import { ClotGroup } from 'ui/ClotGroup' 13 | import { useFilter } from 'lib/sotore' 14 | import { runAllTests, stopAllTests, suite } from 'model/suite' 15 | import { Modal } from 'ui/Modal' 16 | 17 | import { $header, $logo, $title, $controls } from './Header.module.styl' 18 | import { SettingsModal } from './SettingsModal' 19 | 20 | export const Header: FC = () => { 21 | const [title, author, running] = useFilter( 22 | suite, 23 | 'title', 24 | 'author', 25 | 'running', 26 | ) 27 | const [shareModalVisible, setShareModalVisible] = useState(false) 28 | const [configModalVisible, setConfigModalVisible] = useState(false) 29 | const shareModalTimerRef = useRef() 30 | 31 | const handleShare = useCallback(async () => { 32 | const url = window.location.toString() 33 | 34 | try { 35 | await navigator.permissions 36 | .query({ name: 'clipboard-read' as PermissionName }) 37 | .catch((e) => e) 38 | await navigator.clipboard.writeText(url) 39 | } catch (e) { 40 | window.prompt('Browser is not supporting, copy manually:', url) 41 | } 42 | 43 | clearTimeout(shareModalTimerRef.current) 44 | setShareModalVisible(true) 45 | 46 | shareModalTimerRef.current = setTimeout(() => { 47 | setShareModalVisible(false) 48 | }, 1000) 49 | }, []) 50 | 51 | const handleConfig = useCallback(() => { 52 | setConfigModalVisible(true) 53 | }, []) 54 | 55 | return ( 56 |
57 | 58 | 59 | 60 | 61 | / 62 |
63 | {title} 64 | by {author} 65 |
66 | 67 | 68 | 74 | ) : ( 75 | 78 | )} 79 | 80 | 81 | 85 | 86 | Copied! 87 | 88 |
89 | ) 90 | } 91 | -------------------------------------------------------------------------------- /src/layout/Suite/Header/SettingsModal.module.styl: -------------------------------------------------------------------------------- 1 | 2 | .surface { 3 | background: var(--theme-bg-fill) 4 | border-radius: 12px 5 | padding: 16px 6 | display: flex 7 | flex-direction: column 8 | gap: 16px 9 | border: 2px solid var(--theme-bg-text) 10 | } 11 | 12 | .title { 13 | display: flex 14 | justify-content: center 15 | } 16 | 17 | .content { 18 | display: flex 19 | flex-direction: column 20 | gap: 12px 21 | } 22 | 23 | .controls { 24 | display: flex 25 | justify-content: flex-end 26 | } -------------------------------------------------------------------------------- /src/layout/Suite/Header/SettingsModal.tsx: -------------------------------------------------------------------------------- 1 | import { useSelector } from 'lib/sotore' 2 | import { suite } from 'model/suite' 3 | import { FC } from 'react' 4 | import { Button } from 'ui/Button' 5 | import { Form, TextField } from 'ui/Form' 6 | import { Modal } from 'ui/Modal' 7 | import { Title } from 'ui/Text' 8 | 9 | import { 10 | $title, 11 | $surface, 12 | $content, 13 | $controls, 14 | } from './SettingsModal.module.styl' 15 | 16 | type Props = { 17 | visible: boolean 18 | onDismiss(value: boolean): void 19 | } 20 | 21 | export const SettingsModal: FC = (props) => { 22 | const { visible, onDismiss } = props 23 | 24 | const config = useSelector(suite, ({ title, author }) => ({ title, author })) 25 | 26 | const handleApply = (values: Record) => { 27 | suite.lay(values) 28 | onDismiss(false) 29 | } 30 | 31 | return ( 32 | 33 |
34 |
35 | Suite config 36 |
37 | 38 |
39 | 40 | 41 |
42 | 43 | 48 |
49 |
50 | ) 51 | } 52 | -------------------------------------------------------------------------------- /src/layout/Suite/Header/index.ts: -------------------------------------------------------------------------------- 1 | export { Header } from './Header' 2 | -------------------------------------------------------------------------------- /src/layout/Suite/Layout/Layout.module.styl: -------------------------------------------------------------------------------- 1 | .layout { 2 | flex: 1 3 | 4 | padding: 16px 5 | display: flex 6 | flex-direction: column 7 | } -------------------------------------------------------------------------------- /src/layout/Suite/Layout/Layout.tsx: -------------------------------------------------------------------------------- 1 | import { FC, ReactNode } from 'react' 2 | import { $layout } from './Layout.module.styl' 3 | 4 | type Props = { 5 | children: ReactNode 6 | } 7 | 8 | export const Layout: FC = (props) => { 9 | return
{props.children}
10 | } 11 | -------------------------------------------------------------------------------- /src/layout/Suite/Layout/index.ts: -------------------------------------------------------------------------------- 1 | export { Layout } from './Layout' 2 | -------------------------------------------------------------------------------- /src/layout/Suite/Section/Section.module.styl: -------------------------------------------------------------------------------- 1 | .header { 2 | padding: 12px 3 | display: flex 4 | justify-content: space-between 5 | align-items: flex-end 6 | } 7 | -------------------------------------------------------------------------------- /src/layout/Suite/Section/Section.tsx: -------------------------------------------------------------------------------- 1 | import { FC, ReactNode, useMemo } from 'react' 2 | import { Group } from 'ui/Group' 3 | import { Title } from 'ui/Text' 4 | 5 | import { $header } from './Section.module.styl' 6 | 7 | type Props = { 8 | title: string 9 | children: ReactNode 10 | } 11 | 12 | export const Section: FC & { 13 | Controls: FC<{ children: ReactNode }> 14 | } = (props) => { 15 | const { children, title } = props 16 | 17 | const { content, controls } = useMemo(() => { 18 | if (!Array.isArray(children)) 19 | return { 20 | content: children, 21 | } 22 | 23 | return { 24 | content: children.filter((item) => { 25 | return item.type !== Section.Controls 26 | }), 27 | controls: children.find((item) => { 28 | return item.type === Section.Controls 29 | }), 30 | } 31 | }, [children]) 32 | 33 | return ( 34 |
35 |
36 | {title} 37 | {controls} 38 |
39 | 40 | {content} 41 |
42 | ) 43 | } 44 | 45 | Section.Controls = (props) => { 46 | return {props.children} 47 | } 48 | -------------------------------------------------------------------------------- /src/layout/Suite/Section/index.ts: -------------------------------------------------------------------------------- 1 | export { Section } from './Section' 2 | -------------------------------------------------------------------------------- /src/layout/Suite/index.ts: -------------------------------------------------------------------------------- 1 | export { Layout } from './Layout' 2 | export { Header } from './Header' 3 | export { Content } from './Content' 4 | export { Section } from './Section' 5 | export { Footer } from './Footer' 6 | -------------------------------------------------------------------------------- /src/lib/benchmark/benchmark_src.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Benchmark.js 3 | * Copyright 2010-2016 Mathias Bynens 4 | * Based on JSLitmus.js, copyright Robert Kieffer 5 | * Modified by John-David Dalton 6 | * Available under MIT license 7 | */ 8 | 9 | /** Used to assign each benchmark an incremented id. */ 10 | var counter = 0 11 | 12 | /** Used to detect primitive types. */ 13 | var rePrimitive = /^(?:boolean|number|string|undefined)$/ 14 | 15 | /** Used to make every compiled test unique. */ 16 | var uidCounter = 0 17 | 18 | /** Used to avoid hz of Infinity. */ 19 | var divisors = { 20 | '1': 4096, 21 | '2': 512, 22 | '3': 64, 23 | '4': 8, 24 | '5': 0, 25 | } 26 | 27 | /** 28 | * T-Distribution two-tailed critical values for 95% confidence. 29 | * For more info see http://www.itl.nist.gov/div898/handbook/eda/section3/eda3672.htm. 30 | */ 31 | var tTable = { 32 | '1': 12.706, 33 | '2': 4.303, 34 | '3': 3.182, 35 | '4': 2.776, 36 | '5': 2.571, 37 | '6': 2.447, 38 | '7': 2.365, 39 | '8': 2.306, 40 | '9': 2.262, 41 | '10': 2.228, 42 | '11': 2.201, 43 | '12': 2.179, 44 | '13': 2.16, 45 | '14': 2.145, 46 | '15': 2.131, 47 | '16': 2.12, 48 | '17': 2.11, 49 | '18': 2.101, 50 | '19': 2.093, 51 | '20': 2.086, 52 | '21': 2.08, 53 | '22': 2.074, 54 | '23': 2.069, 55 | '24': 2.064, 56 | '25': 2.06, 57 | '26': 2.056, 58 | '27': 2.052, 59 | '28': 2.048, 60 | '29': 2.045, 61 | '30': 2.042, 62 | infinity: 1.96, 63 | } 64 | 65 | /** 66 | * Critical Mann-Whitney U-values for 95% confidence. 67 | * For more info see http://www.saburchill.com/IBbiology/stats/003.html. 68 | */ 69 | var uTable = { 70 | '5': [0, 1, 2], 71 | '6': [1, 2, 3, 5], 72 | '7': [1, 3, 5, 6, 8], 73 | '8': [2, 4, 6, 8, 10, 13], 74 | '9': [2, 4, 7, 10, 12, 15, 17], 75 | '10': [3, 5, 8, 11, 14, 17, 20, 23], 76 | '11': [3, 6, 9, 13, 16, 19, 23, 26, 30], 77 | '12': [4, 7, 11, 14, 18, 22, 26, 29, 33, 37], 78 | '13': [4, 8, 12, 16, 20, 24, 28, 33, 37, 41, 45], 79 | '14': [5, 9, 13, 17, 22, 26, 31, 36, 40, 45, 50, 55], 80 | '15': [5, 10, 14, 19, 24, 29, 34, 39, 44, 49, 54, 59, 64], 81 | '16': [6, 11, 15, 21, 26, 31, 37, 42, 47, 53, 59, 64, 70, 75], 82 | '17': [6, 11, 17, 22, 28, 34, 39, 45, 51, 57, 63, 67, 75, 81, 87], 83 | '18': [7, 12, 18, 24, 30, 36, 42, 48, 55, 61, 67, 74, 80, 86, 93, 99], 84 | '19': [7, 13, 19, 25, 32, 38, 45, 52, 58, 65, 72, 78, 85, 92, 99, 106, 113], 85 | '20': [ 86 | 8, 87 | 14, 88 | 20, 89 | 27, 90 | 34, 91 | 41, 92 | 48, 93 | 55, 94 | 62, 95 | 69, 96 | 76, 97 | 83, 98 | 90, 99 | 98, 100 | 105, 101 | 112, 102 | 119, 103 | 127, 104 | ], 105 | '21': [ 106 | 8, 107 | 15, 108 | 22, 109 | 29, 110 | 36, 111 | 43, 112 | 50, 113 | 58, 114 | 65, 115 | 73, 116 | 80, 117 | 88, 118 | 96, 119 | 103, 120 | 111, 121 | 119, 122 | 126, 123 | 134, 124 | 142, 125 | ], 126 | '22': [ 127 | 9, 128 | 16, 129 | 23, 130 | 30, 131 | 38, 132 | 45, 133 | 53, 134 | 61, 135 | 69, 136 | 77, 137 | 85, 138 | 93, 139 | 101, 140 | 109, 141 | 117, 142 | 125, 143 | 133, 144 | 141, 145 | 150, 146 | 158, 147 | ], 148 | '23': [ 149 | 9, 150 | 17, 151 | 24, 152 | 32, 153 | 40, 154 | 48, 155 | 56, 156 | 64, 157 | 73, 158 | 81, 159 | 89, 160 | 98, 161 | 106, 162 | 115, 163 | 123, 164 | 132, 165 | 140, 166 | 149, 167 | 157, 168 | 166, 169 | 175, 170 | ], 171 | '24': [ 172 | 10, 173 | 17, 174 | 25, 175 | 33, 176 | 42, 177 | 50, 178 | 59, 179 | 67, 180 | 76, 181 | 85, 182 | 94, 183 | 102, 184 | 111, 185 | 120, 186 | 129, 187 | 138, 188 | 147, 189 | 156, 190 | 165, 191 | 174, 192 | 183, 193 | 192, 194 | ], 195 | '25': [ 196 | 10, 197 | 18, 198 | 27, 199 | 35, 200 | 44, 201 | 53, 202 | 62, 203 | 71, 204 | 80, 205 | 89, 206 | 98, 207 | 107, 208 | 117, 209 | 126, 210 | 135, 211 | 145, 212 | 154, 213 | 163, 214 | 173, 215 | 182, 216 | 192, 217 | 201, 218 | 211, 219 | ], 220 | '26': [ 221 | 11, 222 | 19, 223 | 28, 224 | 37, 225 | 46, 226 | 55, 227 | 64, 228 | 74, 229 | 83, 230 | 93, 231 | 102, 232 | 112, 233 | 122, 234 | 132, 235 | 141, 236 | 151, 237 | 161, 238 | 171, 239 | 181, 240 | 191, 241 | 200, 242 | 210, 243 | 220, 244 | 230, 245 | ], 246 | '27': [ 247 | 11, 248 | 20, 249 | 29, 250 | 38, 251 | 48, 252 | 57, 253 | 67, 254 | 77, 255 | 87, 256 | 97, 257 | 107, 258 | 118, 259 | 125, 260 | 138, 261 | 147, 262 | 158, 263 | 168, 264 | 178, 265 | 188, 266 | 199, 267 | 209, 268 | 219, 269 | 230, 270 | 240, 271 | 250, 272 | ], 273 | '28': [ 274 | 12, 275 | 21, 276 | 30, 277 | 40, 278 | 50, 279 | 60, 280 | 70, 281 | 80, 282 | 90, 283 | 101, 284 | 111, 285 | 122, 286 | 132, 287 | 143, 288 | 154, 289 | 164, 290 | 175, 291 | 186, 292 | 196, 293 | 207, 294 | 218, 295 | 228, 296 | 239, 297 | 250, 298 | 261, 299 | 272, 300 | ], 301 | '29': [ 302 | 13, 303 | 22, 304 | 32, 305 | 42, 306 | 52, 307 | 62, 308 | 73, 309 | 83, 310 | 94, 311 | 105, 312 | 116, 313 | 127, 314 | 138, 315 | 149, 316 | 160, 317 | 171, 318 | 182, 319 | 193, 320 | 204, 321 | 215, 322 | 226, 323 | 238, 324 | 249, 325 | 260, 326 | 271, 327 | 282, 328 | 294, 329 | ], 330 | '30': [ 331 | 13, 332 | 23, 333 | 33, 334 | 43, 335 | 54, 336 | 65, 337 | 76, 338 | 87, 339 | 98, 340 | 109, 341 | 120, 342 | 131, 343 | 143, 344 | 154, 345 | 166, 346 | 177, 347 | 189, 348 | 200, 349 | 212, 350 | 223, 351 | 235, 352 | 247, 353 | 258, 354 | 270, 355 | 282, 356 | 293, 357 | 305, 358 | 317, 359 | ], 360 | } 361 | 362 | /** Used for `Array` and `Object` method references. */ 363 | var arrayRef = [], 364 | objectProto = Object.prototype 365 | 366 | /** Native method shortcuts. */ 367 | var abs = Math.abs, 368 | clearTimeout = context.clearTimeout, 369 | floor = Math.floor, 370 | log = Math.log, 371 | max = Math.max, 372 | min = Math.min, 373 | pow = Math.pow, 374 | push = arrayRef.push, 375 | setTimeout = context.setTimeout, 376 | shift = arrayRef.shift, 377 | slice = arrayRef.slice, 378 | sqrt = Math.sqrt, 379 | toString = objectProto.toString, 380 | unshift = arrayRef.unshift 381 | 382 | /** Detect DOM document object. */ 383 | var doc = isHostType(context, 'document') && context.document 384 | 385 | /** Used to access Wade Simmons' Node.js `microtime` module. */ 386 | var microtimeObject = require('microtime') 387 | 388 | /** Used to access Node.js's high resolution timer. */ 389 | var processObject = isHostType(context, 'process') && context.process 390 | 391 | /** Used to prevent a `removeChild` memory leak in IE < 9. */ 392 | var trash = doc && doc.createElement('div') 393 | 394 | /** Used to integrity check compiled tests. */ 395 | var uid = 'uid' + +_.now() 396 | 397 | /** Used to avoid infinite recursion when methods call each other. */ 398 | var calledBy = {} 399 | 400 | /** 401 | * An object used to flag environments/features. 402 | * 403 | * @static 404 | * @memberOf Benchmark 405 | * @type Object 406 | */ 407 | var support = {} 408 | 409 | ;(function () { 410 | /** 411 | * Detect if running in a browser environment. 412 | * 413 | * @memberOf Benchmark.support 414 | * @type boolean 415 | */ 416 | support.browser = 417 | doc && isHostType(context, 'navigator') && !isHostType(context, 'phantom') 418 | 419 | /** 420 | * Detect if the Timers API exists. 421 | * 422 | * @memberOf Benchmark.support 423 | * @type boolean 424 | */ 425 | support.timeout = 426 | isHostType(context, 'setTimeout') && isHostType(context, 'clearTimeout') 427 | 428 | /** 429 | * Detect if function decompilation is support. 430 | * 431 | * @name decompilation 432 | * @memberOf Benchmark.support 433 | * @type boolean 434 | */ 435 | try { 436 | // Safari 2.x removes commas in object literals from `Function#toString` results. 437 | // See http://webk.it/11609 for more details. 438 | // Firefox 3.6 and Opera 9.25 strip grouping parentheses from `Function#toString` results. 439 | // See http://bugzil.la/559438 for more details. 440 | support.decompilation = 441 | Function( 442 | ( 443 | 'return (' + 444 | function (x) { 445 | return { x: '' + (1 + x) + '', y: 0 } 446 | } + 447 | ')' 448 | ) 449 | // Avoid issues with code added by Istanbul. 450 | .replace(/__cov__[^;]+;/g, ''), 451 | )()(0).x === '1' 452 | } catch (e) { 453 | support.decompilation = false 454 | } 455 | })() 456 | 457 | /** 458 | * Timer object used by `clock()` and `Deferred#resolve`. 459 | * 460 | * @private 461 | * @type Object 462 | */ 463 | var timer = { 464 | /** 465 | * The timer namespace object or constructor. 466 | * 467 | * @private 468 | * @memberOf timer 469 | * @type {Function|Object} 470 | */ 471 | ns: Date, 472 | 473 | /** 474 | * Starts the deferred timer. 475 | * 476 | * @private 477 | * @memberOf timer 478 | * @param {Object} deferred The deferred instance. 479 | */ 480 | start: null, // Lazy defined in `clock()`. 481 | 482 | /** 483 | * Stops the deferred timer. 484 | * 485 | * @private 486 | * @memberOf timer 487 | * @param {Object} deferred The deferred instance. 488 | */ 489 | stop: null, // Lazy defined in `clock()`. 490 | } 491 | 492 | /*------------------------------------------------------------------------*/ 493 | 494 | /** 495 | * The Benchmark constructor. 496 | * 497 | * Note: The Benchmark constructor exposes a handful of lodash methods to 498 | * make working with arrays, collections, and objects easier. The lodash 499 | * methods are: 500 | * [`each/forEach`](https://lodash.com/docs#forEach), [`forOwn`](https://lodash.com/docs#forOwn), 501 | * [`has`](https://lodash.com/docs#has), [`indexOf`](https://lodash.com/docs#indexOf), 502 | * [`map`](https://lodash.com/docs#map), and [`reduce`](https://lodash.com/docs#reduce) 503 | * 504 | * @constructor 505 | * @param {string} name A name to identify the benchmark. 506 | * @param {Function|string} fn The test to benchmark. 507 | * @param {Object} [options={}] Options object. 508 | * @example 509 | * 510 | * // basic usage (the `new` operator is optional) 511 | * var bench = new Benchmark(fn); 512 | * 513 | * // or using a name first 514 | * var bench = new Benchmark('foo', fn); 515 | * 516 | * // or with options 517 | * var bench = new Benchmark('foo', fn, { 518 | * 519 | * // displayed by `Benchmark#toString` if `name` is not available 520 | * 'id': 'xyz', 521 | * 522 | * // called when the benchmark starts running 523 | * 'onStart': onStart, 524 | * 525 | * // called after each run cycle 526 | * 'onCycle': onCycle, 527 | * 528 | * // called when aborted 529 | * 'onAbort': onAbort, 530 | * 531 | * // called when a test errors 532 | * 'onError': onError, 533 | * 534 | * // called when reset 535 | * 'onReset': onReset, 536 | * 537 | * // called when the benchmark completes running 538 | * 'onComplete': onComplete, 539 | * 540 | * // compiled/called before the test loop 541 | * 'setup': setup, 542 | * 543 | * // compiled/called after the test loop 544 | * 'teardown': teardown 545 | * }); 546 | * 547 | * // or name and options 548 | * var bench = new Benchmark('foo', { 549 | * 550 | * // a flag to indicate the benchmark is deferred 551 | * 'defer': true, 552 | * 553 | * // benchmark test function 554 | * 'fn': function(deferred) { 555 | * // call `Deferred#resolve` when the deferred test is finished 556 | * deferred.resolve(); 557 | * } 558 | * }); 559 | * 560 | * // or options only 561 | * var bench = new Benchmark({ 562 | * 563 | * // benchmark name 564 | * 'name': 'foo', 565 | * 566 | * // benchmark test as a string 567 | * 'fn': '[1,2,3,4].sort()' 568 | * }); 569 | * 570 | * // a test's `this` binding is set to the benchmark instance 571 | * var bench = new Benchmark('foo', function() { 572 | * 'My name is '.concat(this.name); // "My name is foo" 573 | * }); 574 | */ 575 | function Benchmark(name, fn, options) { 576 | var bench = this 577 | 578 | // Allow instance creation without the `new` operator. 579 | if (!(bench instanceof Benchmark)) { 580 | return new Benchmark(name, fn, options) 581 | } 582 | // Juggle arguments. 583 | if (_.isPlainObject(name)) { 584 | // 1 argument (options). 585 | options = name 586 | } else if (_.isFunction(name)) { 587 | // 2 arguments (fn, options). 588 | options = fn 589 | fn = name 590 | } else if (_.isPlainObject(fn)) { 591 | // 2 arguments (name, options). 592 | options = fn 593 | fn = null 594 | bench.name = name 595 | } else { 596 | // 3 arguments (name, fn [, options]). 597 | bench.name = name 598 | } 599 | setOptions(bench, options) 600 | 601 | bench.id || (bench.id = ++counter) 602 | bench.fn == null && (bench.fn = fn) 603 | 604 | bench.stats = cloneDeep(bench.stats) 605 | bench.times = cloneDeep(bench.times) 606 | } 607 | 608 | /** 609 | * The Deferred constructor. 610 | * 611 | * @constructor 612 | * @memberOf Benchmark 613 | * @param {Object} clone The cloned benchmark instance. 614 | */ 615 | function Deferred(clone) { 616 | var deferred = this 617 | if (!(deferred instanceof Deferred)) { 618 | return new Deferred(clone) 619 | } 620 | deferred.benchmark = clone 621 | clock(deferred) 622 | } 623 | 624 | /** 625 | * The Event constructor. 626 | * 627 | * @constructor 628 | * @memberOf Benchmark 629 | * @param {Object|string} type The event type. 630 | */ 631 | function Event(type) { 632 | var event = this 633 | if (type instanceof Event) { 634 | return type 635 | } 636 | return event instanceof Event 637 | ? _.assign( 638 | event, 639 | { timeStamp: +_.now() }, 640 | typeof type == 'string' ? { type: type } : type, 641 | ) 642 | : new Event(type) 643 | } 644 | 645 | /** 646 | * The Suite constructor. 647 | * 648 | * Note: Each Suite instance has a handful of wrapped lodash methods to 649 | * make working with Suites easier. The wrapped lodash methods are: 650 | * [`each/forEach`](https://lodash.com/docs#forEach), [`indexOf`](https://lodash.com/docs#indexOf), 651 | * [`map`](https://lodash.com/docs#map), and [`reduce`](https://lodash.com/docs#reduce) 652 | * 653 | * @constructor 654 | * @memberOf Benchmark 655 | * @param {string} name A name to identify the suite. 656 | * @param {Object} [options={}] Options object. 657 | * @example 658 | * 659 | * // basic usage (the `new` operator is optional) 660 | * var suite = new Benchmark.Suite; 661 | * 662 | * // or using a name first 663 | * var suite = new Benchmark.Suite('foo'); 664 | * 665 | * // or with options 666 | * var suite = new Benchmark.Suite('foo', { 667 | * 668 | * // called when the suite starts running 669 | * 'onStart': onStart, 670 | * 671 | * // called between running benchmarks 672 | * 'onCycle': onCycle, 673 | * 674 | * // called when aborted 675 | * 'onAbort': onAbort, 676 | * 677 | * // called when a test errors 678 | * 'onError': onError, 679 | * 680 | * // called when reset 681 | * 'onReset': onReset, 682 | * 683 | * // called when the suite completes running 684 | * 'onComplete': onComplete 685 | * }); 686 | */ 687 | function Suite(name, options) { 688 | var suite = this 689 | 690 | // Allow instance creation without the `new` operator. 691 | if (!(suite instanceof Suite)) { 692 | return new Suite(name, options) 693 | } 694 | // Juggle arguments. 695 | if (_.isPlainObject(name)) { 696 | // 1 argument (options). 697 | options = name 698 | } else { 699 | // 2 arguments (name [, options]). 700 | suite.name = name 701 | } 702 | setOptions(suite, options) 703 | } 704 | 705 | /*------------------------------------------------------------------------*/ 706 | 707 | /** 708 | * A specialized version of `_.cloneDeep` which only clones arrays and plain 709 | * objects assigning all other values by reference. 710 | * 711 | * @private 712 | * @param {*} value The value to clone. 713 | * @returns {*} The cloned value. 714 | */ 715 | var cloneDeep = _.partial(_.cloneDeepWith, _, function (value) { 716 | // Only clone primitives, arrays, and plain objects. 717 | if (!_.isArray(value) && !_.isPlainObject(value)) { 718 | return value 719 | } 720 | }) 721 | 722 | /** 723 | * Creates a function from the given arguments string and body. 724 | * 725 | * @private 726 | * @param {string} args The comma separated function arguments. 727 | * @param {string} body The function body. 728 | * @returns {Function} The new function. 729 | */ 730 | function createFunction() { 731 | // Lazy define. 732 | createFunction = function (args, body) { 733 | var result, 734 | anchor = freeDefine ? freeDefine.amd : Benchmark, 735 | prop = uid + 'createFunction' 736 | 737 | runScript( 738 | (freeDefine ? 'define.amd.' : 'Benchmark.') + 739 | prop + 740 | '=function(' + 741 | args + 742 | '){' + 743 | body + 744 | '}', 745 | ) 746 | result = anchor[prop] 747 | delete anchor[prop] 748 | return result 749 | } 750 | // Fix JaegerMonkey bug. 751 | // For more information see http://bugzil.la/639720. 752 | createFunction = 753 | support.browser && 754 | (createFunction('', 'return"' + uid + '"') || _.noop)() == uid 755 | ? createFunction 756 | : Function 757 | return createFunction.apply(null, arguments) 758 | } 759 | 760 | /** 761 | * Delay the execution of a function based on the benchmark's `delay` property. 762 | * 763 | * @private 764 | * @param {Object} bench The benchmark instance. 765 | * @param {Object} fn The function to execute. 766 | */ 767 | function delay(bench, fn) { 768 | bench._timerId = _.delay(fn, bench.delay * 1e3) 769 | } 770 | 771 | /** 772 | * Destroys the given element. 773 | * 774 | * @private 775 | * @param {Element} element The element to destroy. 776 | */ 777 | function destroyElement(element) { 778 | trash.appendChild(element) 779 | trash.innerHTML = '' 780 | } 781 | 782 | /** 783 | * Gets the name of the first argument from a function's source. 784 | * 785 | * @private 786 | * @param {Function} fn The function. 787 | * @returns {string} The argument name. 788 | */ 789 | function getFirstArgument(fn) { 790 | return ( 791 | (!_.has(fn, 'toString') && 792 | (/^[\s(]*function[^(]*\(([^\s,)]+)/.exec(fn) || 0)[1]) || 793 | '' 794 | ) 795 | } 796 | 797 | /** 798 | * Computes the arithmetic mean of a sample. 799 | * 800 | * @private 801 | * @param {Array} sample The sample. 802 | * @returns {number} The mean. 803 | */ 804 | function getMean(sample) { 805 | return ( 806 | _.reduce(sample, function (sum, x) { 807 | return sum + x 808 | }) / sample.length || 0 809 | ) 810 | } 811 | 812 | /** 813 | * Gets the source code of a function. 814 | * 815 | * @private 816 | * @param {Function} fn The function. 817 | * @returns {string} The function's source code. 818 | */ 819 | function getSource(fn) { 820 | var result = '' 821 | if (isStringable(fn)) { 822 | result = String(fn) 823 | } else if (support.decompilation) { 824 | // Escape the `{` for Firefox 1. 825 | result = _.result(/^[^{]+\{([\s\S]*)\}\s*$/.exec(fn), 1) 826 | } 827 | // Trim string. 828 | result = (result || '').replace(/^\s+|\s+$/g, '') 829 | 830 | // Detect strings containing only the "use strict" directive. 831 | return /^(?:\/\*+[\w\W]*?\*\/|\/\/.*?[\n\r\u2028\u2029]|\s)*(["'])use strict\1;?$/.test( 832 | result, 833 | ) 834 | ? '' 835 | : result 836 | } 837 | 838 | /** 839 | * Checks if an object is of the specified class. 840 | * 841 | * @private 842 | * @param {*} value The value to check. 843 | * @param {string} name The name of the class. 844 | * @returns {boolean} Returns `true` if the value is of the specified class, else `false`. 845 | */ 846 | function isClassOf(value, name) { 847 | return value != null && toString.call(value) == '[object ' + name + ']' 848 | } 849 | 850 | /** 851 | * Host objects can return type values that are different from their actual 852 | * data type. The objects we are concerned with usually return non-primitive 853 | * types of "object", "function", or "unknown". 854 | * 855 | * @private 856 | * @param {*} object The owner of the property. 857 | * @param {string} property The property to check. 858 | * @returns {boolean} Returns `true` if the property value is a non-primitive, else `false`. 859 | */ 860 | function isHostType(object, property) { 861 | if (object == null) { 862 | return false 863 | } 864 | var type = typeof object[property] 865 | return !rePrimitive.test(type) && (type != 'object' || !!object[property]) 866 | } 867 | 868 | /** 869 | * Checks if a value can be safely coerced to a string. 870 | * 871 | * @private 872 | * @param {*} value The value to check. 873 | * @returns {boolean} Returns `true` if the value can be coerced, else `false`. 874 | */ 875 | function isStringable(value) { 876 | return ( 877 | _.isString(value) || 878 | (_.has(value, 'toString') && _.isFunction(value.toString)) 879 | ) 880 | } 881 | 882 | /** 883 | * A wrapper around `require` to suppress `module missing` errors. 884 | * 885 | * @private 886 | * @param {string} id The module id. 887 | * @returns {*} The exported module or `null`. 888 | */ 889 | function require(id) { 890 | try { 891 | var result = freeExports && freeRequire(id) 892 | } catch (e) {} 893 | return result || null 894 | } 895 | 896 | /** 897 | * Runs a snippet of JavaScript via script injection. 898 | * 899 | * @private 900 | * @param {string} code The code to run. 901 | */ 902 | function runScript(code) { 903 | var anchor = freeDefine ? define.amd : Benchmark, 904 | script = doc.createElement('script'), 905 | sibling = doc.getElementsByTagName('script')[0], 906 | parent = sibling.parentNode, 907 | prop = uid + 'runScript', 908 | prefix = 909 | '(' + 910 | (freeDefine ? 'define.amd.' : 'Benchmark.') + 911 | prop + 912 | '||function(){})();' 913 | 914 | // Firefox 2.0.0.2 cannot use script injection as intended because it executes 915 | // asynchronously, but that's OK because script injection is only used to avoid 916 | // the previously commented JaegerMonkey bug. 917 | try { 918 | // Remove the inserted script *before* running the code to avoid differences 919 | // in the expected script element count/order of the document. 920 | script.appendChild(doc.createTextNode(prefix + code)) 921 | anchor[prop] = function () { 922 | destroyElement(script) 923 | } 924 | } catch (e) { 925 | parent = parent.cloneNode(false) 926 | sibling = null 927 | script.text = code 928 | } 929 | parent.insertBefore(script, sibling) 930 | delete anchor[prop] 931 | } 932 | 933 | /** 934 | * A helper function for setting options/event handlers. 935 | * 936 | * @private 937 | * @param {Object} object The benchmark or suite instance. 938 | * @param {Object} [options={}] Options object. 939 | */ 940 | function setOptions(object, options) { 941 | options = object.options = _.assign( 942 | {}, 943 | cloneDeep(object.constructor.options), 944 | cloneDeep(options), 945 | ) 946 | 947 | _.forOwn(options, function (value, key) { 948 | if (value != null) { 949 | // Add event listeners. 950 | if (/^on[A-Z]/.test(key)) { 951 | _.each(key.split(' '), function (key) { 952 | object.on(key.slice(2).toLowerCase(), value) 953 | }) 954 | } else if (!_.has(object, key)) { 955 | object[key] = cloneDeep(value) 956 | } 957 | } 958 | }) 959 | } 960 | 961 | /*------------------------------------------------------------------------*/ 962 | 963 | /** 964 | * Handles cycling/completing the deferred benchmark. 965 | * 966 | * @memberOf Benchmark.Deferred 967 | */ 968 | function resolve() { 969 | var deferred = this, 970 | clone = deferred.benchmark, 971 | bench = clone._original 972 | 973 | if (bench.aborted) { 974 | // cycle() -> clone cycle/complete event -> compute()'s invoked bench.run() cycle/complete. 975 | deferred.teardown() 976 | clone.running = false 977 | cycle(deferred) 978 | } else if (++deferred.cycles < clone.count) { 979 | clone.compiled.call(deferred, context, timer) 980 | } else { 981 | timer.stop(deferred) 982 | deferred.teardown() 983 | delay(clone, function () { 984 | cycle(deferred) 985 | }) 986 | } 987 | } 988 | 989 | /*------------------------------------------------------------------------*/ 990 | 991 | /** 992 | * A generic `Array#filter` like method. 993 | * 994 | * @static 995 | * @memberOf Benchmark 996 | * @param {Array} array The array to iterate over. 997 | * @param {Function|string} callback The function/alias called per iteration. 998 | * @returns {Array} A new array of values that passed callback filter. 999 | * @example 1000 | * 1001 | * // get odd numbers 1002 | * Benchmark.filter([1, 2, 3, 4, 5], function(n) { 1003 | * return n % 2; 1004 | * }); // -> [1, 3, 5]; 1005 | * 1006 | * // get fastest benchmarks 1007 | * Benchmark.filter(benches, 'fastest'); 1008 | * 1009 | * // get slowest benchmarks 1010 | * Benchmark.filter(benches, 'slowest'); 1011 | * 1012 | * // get benchmarks that completed without erroring 1013 | * Benchmark.filter(benches, 'successful'); 1014 | */ 1015 | function filter(array, callback) { 1016 | if (callback === 'successful') { 1017 | // Callback to exclude those that are errored, unrun, or have hz of Infinity. 1018 | callback = function (bench) { 1019 | return bench.cycles && _.isFinite(bench.hz) && !bench.error 1020 | } 1021 | } else if (callback === 'fastest' || callback === 'slowest') { 1022 | // Get successful, sort by period + margin of error, and filter fastest/slowest. 1023 | var result = filter(array, 'successful').sort(function (a, b) { 1024 | a = a.stats 1025 | b = b.stats 1026 | return ( 1027 | (a.mean + a.moe > b.mean + b.moe ? 1 : -1) * 1028 | (callback === 'fastest' ? 1 : -1) 1029 | ) 1030 | }) 1031 | 1032 | return _.filter(result, function (bench) { 1033 | return result[0].compare(bench) == 0 1034 | }) 1035 | } 1036 | return _.filter(array, callback) 1037 | } 1038 | 1039 | /** 1040 | * Converts a number to a more readable comma-separated string representation. 1041 | * 1042 | * @static 1043 | * @memberOf Benchmark 1044 | * @param {number} number The number to convert. 1045 | * @returns {string} The more readable string representation. 1046 | */ 1047 | function formatNumber(number) { 1048 | number = String(number).split('.') 1049 | return ( 1050 | number[0].replace(/(?=(?:\d{3})+$)(?!\b)/g, ',') + 1051 | (number[1] ? '.' + number[1] : '') 1052 | ) 1053 | } 1054 | 1055 | /** 1056 | * Invokes a method on all items in an array. 1057 | * 1058 | * @static 1059 | * @memberOf Benchmark 1060 | * @param {Array} benches Array of benchmarks to iterate over. 1061 | * @param {Object|string} name The name of the method to invoke OR options object. 1062 | * @param {...*} [args] Arguments to invoke the method with. 1063 | * @returns {Array} A new array of values returned from each method invoked. 1064 | * @example 1065 | * 1066 | * // invoke `reset` on all benchmarks 1067 | * Benchmark.invoke(benches, 'reset'); 1068 | * 1069 | * // invoke `emit` with arguments 1070 | * Benchmark.invoke(benches, 'emit', 'complete', listener); 1071 | * 1072 | * // invoke `run(true)`, treat benchmarks as a queue, and register invoke callbacks 1073 | * Benchmark.invoke(benches, { 1074 | * 1075 | * // invoke the `run` method 1076 | * 'name': 'run', 1077 | * 1078 | * // pass a single argument 1079 | * 'args': true, 1080 | * 1081 | * // treat as queue, removing benchmarks from front of `benches` until empty 1082 | * 'queued': true, 1083 | * 1084 | * // called before any benchmarks have been invoked. 1085 | * 'onStart': onStart, 1086 | * 1087 | * // called between invoking benchmarks 1088 | * 'onCycle': onCycle, 1089 | * 1090 | * // called after all benchmarks have been invoked. 1091 | * 'onComplete': onComplete 1092 | * }); 1093 | */ 1094 | function invoke(benches, name) { 1095 | var args, 1096 | bench, 1097 | queued, 1098 | index = -1, 1099 | eventProps = { currentTarget: benches }, 1100 | options = { onStart: _.noop, onCycle: _.noop, onComplete: _.noop }, 1101 | result = _.toArray(benches) 1102 | 1103 | /** 1104 | * Invokes the method of the current object and if synchronous, fetches the next. 1105 | */ 1106 | function execute() { 1107 | var listeners, 1108 | async = isAsync(bench) 1109 | 1110 | if (async) { 1111 | // Use `getNext` as the first listener. 1112 | bench.on('complete', getNext) 1113 | listeners = bench.events.complete 1114 | listeners.splice(0, 0, listeners.pop()) 1115 | } 1116 | // Execute method. 1117 | result[index] = _.isFunction(bench && bench[name]) 1118 | ? bench[name].apply(bench, args) 1119 | : undefined 1120 | // If synchronous return `true` until finished. 1121 | return !async && getNext() 1122 | } 1123 | 1124 | /** 1125 | * Fetches the next bench or executes `onComplete` callback. 1126 | */ 1127 | function getNext(event) { 1128 | var cycleEvent, 1129 | last = bench, 1130 | async = isAsync(last) 1131 | 1132 | if (async) { 1133 | last.off('complete', getNext) 1134 | last.emit('complete') 1135 | } 1136 | // Emit "cycle" event. 1137 | eventProps.type = 'cycle' 1138 | eventProps.target = last 1139 | cycleEvent = Event(eventProps) 1140 | options.onCycle.call(benches, cycleEvent) 1141 | 1142 | // Choose next benchmark if not exiting early. 1143 | if (!cycleEvent.aborted && raiseIndex() !== false) { 1144 | bench = queued ? benches[0] : result[index] 1145 | if (isAsync(bench)) { 1146 | delay(bench, execute) 1147 | } else if (async) { 1148 | // Resume execution if previously asynchronous but now synchronous. 1149 | while (execute()) {} 1150 | } else { 1151 | // Continue synchronous execution. 1152 | return true 1153 | } 1154 | } else { 1155 | // Emit "complete" event. 1156 | eventProps.type = 'complete' 1157 | options.onComplete.call(benches, Event(eventProps)) 1158 | } 1159 | // When used as a listener `event.aborted = true` will cancel the rest of 1160 | // the "complete" listeners because they were already called above and when 1161 | // used as part of `getNext` the `return false` will exit the execution while-loop. 1162 | if (event) { 1163 | event.aborted = true 1164 | } else { 1165 | return false 1166 | } 1167 | } 1168 | 1169 | /** 1170 | * Checks if invoking `Benchmark#run` with asynchronous cycles. 1171 | */ 1172 | function isAsync(object) { 1173 | // Avoid using `instanceof` here because of IE memory leak issues with host objects. 1174 | var async = args[0] && args[0].async 1175 | return ( 1176 | name == 'run' && 1177 | object instanceof Benchmark && 1178 | (((async == null ? object.options.async : async) && support.timeout) || 1179 | object.defer) 1180 | ) 1181 | } 1182 | 1183 | /** 1184 | * Raises `index` to the next defined index or returns `false`. 1185 | */ 1186 | function raiseIndex() { 1187 | index++ 1188 | 1189 | // If queued remove the previous bench. 1190 | if (queued && index > 0) { 1191 | shift.call(benches) 1192 | } 1193 | // If we reached the last index then return `false`. 1194 | return (queued ? benches.length : index < result.length) 1195 | ? index 1196 | : (index = false) 1197 | } 1198 | // Juggle arguments. 1199 | if (_.isString(name)) { 1200 | // 2 arguments (array, name). 1201 | args = slice.call(arguments, 2) 1202 | } else { 1203 | // 2 arguments (array, options). 1204 | options = _.assign(options, name) 1205 | name = options.name 1206 | args = _.isArray((args = 'args' in options ? options.args : [])) 1207 | ? args 1208 | : [args] 1209 | queued = options.queued 1210 | } 1211 | // Start iterating over the array. 1212 | if (raiseIndex() !== false) { 1213 | // Emit "start" event. 1214 | bench = result[index] 1215 | eventProps.type = 'start' 1216 | eventProps.target = bench 1217 | options.onStart.call(benches, Event(eventProps)) 1218 | 1219 | // End early if the suite was aborted in an "onStart" listener. 1220 | if (name == 'run' && benches instanceof Suite && benches.aborted) { 1221 | // Emit "cycle" event. 1222 | eventProps.type = 'cycle' 1223 | options.onCycle.call(benches, Event(eventProps)) 1224 | // Emit "complete" event. 1225 | eventProps.type = 'complete' 1226 | options.onComplete.call(benches, Event(eventProps)) 1227 | } 1228 | // Start method execution. 1229 | else { 1230 | if (isAsync(bench)) { 1231 | delay(bench, execute) 1232 | } else { 1233 | while (execute()) {} 1234 | } 1235 | } 1236 | } 1237 | return result 1238 | } 1239 | 1240 | /** 1241 | * Creates a string of joined array values or object key-value pairs. 1242 | * 1243 | * @static 1244 | * @memberOf Benchmark 1245 | * @param {Array|Object} object The object to operate on. 1246 | * @param {string} [separator1=','] The separator used between key-value pairs. 1247 | * @param {string} [separator2=': '] The separator used between keys and values. 1248 | * @returns {string} The joined result. 1249 | */ 1250 | function join(object, separator1, separator2) { 1251 | var result = [], 1252 | length = (object = Object(object)).length, 1253 | arrayLike = length === length >>> 0 1254 | 1255 | separator2 || (separator2 = ': ') 1256 | _.each(object, function (value, key) { 1257 | result.push(arrayLike ? value : key + separator2 + value) 1258 | }) 1259 | return result.join(separator1 || ',') 1260 | } 1261 | 1262 | /*------------------------------------------------------------------------*/ 1263 | 1264 | /** 1265 | * Aborts all benchmarks in the suite. 1266 | * 1267 | * @name abort 1268 | * @memberOf Benchmark.Suite 1269 | * @returns {Object} The suite instance. 1270 | */ 1271 | function abortSuite() { 1272 | var event, 1273 | suite = this, 1274 | resetting = calledBy.resetSuite 1275 | 1276 | if (suite.running) { 1277 | event = Event('abort') 1278 | suite.emit(event) 1279 | if (!event.cancelled || resetting) { 1280 | // Avoid infinite recursion. 1281 | calledBy.abortSuite = true 1282 | suite.reset() 1283 | delete calledBy.abortSuite 1284 | 1285 | if (!resetting) { 1286 | suite.aborted = true 1287 | invoke(suite, 'abort') 1288 | } 1289 | } 1290 | } 1291 | return suite 1292 | } 1293 | 1294 | /** 1295 | * Adds a test to the benchmark suite. 1296 | * 1297 | * @memberOf Benchmark.Suite 1298 | * @param {string} name A name to identify the benchmark. 1299 | * @param {Function|string} fn The test to benchmark. 1300 | * @param {Object} [options={}] Options object. 1301 | * @returns {Object} The suite instance. 1302 | * @example 1303 | * 1304 | * // basic usage 1305 | * suite.add(fn); 1306 | * 1307 | * // or using a name first 1308 | * suite.add('foo', fn); 1309 | * 1310 | * // or with options 1311 | * suite.add('foo', fn, { 1312 | * 'onCycle': onCycle, 1313 | * 'onComplete': onComplete 1314 | * }); 1315 | * 1316 | * // or name and options 1317 | * suite.add('foo', { 1318 | * 'fn': fn, 1319 | * 'onCycle': onCycle, 1320 | * 'onComplete': onComplete 1321 | * }); 1322 | * 1323 | * // or options only 1324 | * suite.add({ 1325 | * 'name': 'foo', 1326 | * 'fn': fn, 1327 | * 'onCycle': onCycle, 1328 | * 'onComplete': onComplete 1329 | * }); 1330 | */ 1331 | function add(name, fn, options) { 1332 | var suite = this, 1333 | bench = new Benchmark(name, fn, options), 1334 | event = Event({ type: 'add', target: bench }) 1335 | 1336 | if ((suite.emit(event), !event.cancelled)) { 1337 | suite.push(bench) 1338 | } 1339 | return suite 1340 | } 1341 | 1342 | /** 1343 | * Creates a new suite with cloned benchmarks. 1344 | * 1345 | * @name clone 1346 | * @memberOf Benchmark.Suite 1347 | * @param {Object} options Options object to overwrite cloned options. 1348 | * @returns {Object} The new suite instance. 1349 | */ 1350 | function cloneSuite(options) { 1351 | var suite = this, 1352 | result = new suite.constructor(_.assign({}, suite.options, options)) 1353 | 1354 | // Copy own properties. 1355 | _.forOwn(suite, function (value, key) { 1356 | if (!_.has(result, key)) { 1357 | result[key] = _.isFunction(_.get(value, 'clone')) 1358 | ? value.clone() 1359 | : cloneDeep(value) 1360 | } 1361 | }) 1362 | return result 1363 | } 1364 | 1365 | /** 1366 | * An `Array#filter` like method. 1367 | * 1368 | * @name filter 1369 | * @memberOf Benchmark.Suite 1370 | * @param {Function|string} callback The function/alias called per iteration. 1371 | * @returns {Object} A new suite of benchmarks that passed callback filter. 1372 | */ 1373 | function filterSuite(callback) { 1374 | var suite = this, 1375 | result = new suite.constructor(suite.options) 1376 | 1377 | result.push.apply(result, filter(suite, callback)) 1378 | return result 1379 | } 1380 | 1381 | /** 1382 | * Resets all benchmarks in the suite. 1383 | * 1384 | * @name reset 1385 | * @memberOf Benchmark.Suite 1386 | * @returns {Object} The suite instance. 1387 | */ 1388 | function resetSuite() { 1389 | var event, 1390 | suite = this, 1391 | aborting = calledBy.abortSuite 1392 | 1393 | if (suite.running && !aborting) { 1394 | // No worries, `resetSuite()` is called within `abortSuite()`. 1395 | calledBy.resetSuite = true 1396 | suite.abort() 1397 | delete calledBy.resetSuite 1398 | } 1399 | // Reset if the state has changed. 1400 | else if ( 1401 | (suite.aborted || suite.running) && 1402 | (suite.emit((event = Event('reset'))), !event.cancelled) 1403 | ) { 1404 | suite.aborted = suite.running = false 1405 | if (!aborting) { 1406 | invoke(suite, 'reset') 1407 | } 1408 | } 1409 | return suite 1410 | } 1411 | 1412 | /** 1413 | * Runs the suite. 1414 | * 1415 | * @name run 1416 | * @memberOf Benchmark.Suite 1417 | * @param {Object} [options={}] Options object. 1418 | * @returns {Object} The suite instance. 1419 | * @example 1420 | * 1421 | * // basic usage 1422 | * suite.run(); 1423 | * 1424 | * // or with options 1425 | * suite.run({ 'async': true, 'queued': true }); 1426 | */ 1427 | function runSuite(options) { 1428 | var suite = this 1429 | 1430 | suite.reset() 1431 | suite.running = true 1432 | options || (options = {}) 1433 | 1434 | invoke(suite, { 1435 | name: 'run', 1436 | args: options, 1437 | queued: options.queued, 1438 | onStart: function (event) { 1439 | suite.emit(event) 1440 | }, 1441 | onCycle: function (event) { 1442 | var bench = event.target 1443 | if (bench.error) { 1444 | suite.emit({ type: 'error', target: bench }) 1445 | } 1446 | suite.emit(event) 1447 | event.aborted = suite.aborted 1448 | }, 1449 | onComplete: function (event) { 1450 | suite.running = false 1451 | suite.emit(event) 1452 | }, 1453 | }) 1454 | return suite 1455 | } 1456 | 1457 | /*------------------------------------------------------------------------*/ 1458 | 1459 | /** 1460 | * Executes all registered listeners of the specified event type. 1461 | * 1462 | * @memberOf Benchmark, Benchmark.Suite 1463 | * @param {Object|string} type The event type or object. 1464 | * @param {...*} [args] Arguments to invoke the listener with. 1465 | * @returns {*} Returns the return value of the last listener executed. 1466 | */ 1467 | function emit(type) { 1468 | var listeners, 1469 | object = this, 1470 | event = Event(type), 1471 | events = object.events, 1472 | args = ((arguments[0] = event), arguments) 1473 | 1474 | event.currentTarget || (event.currentTarget = object) 1475 | event.target || (event.target = object) 1476 | delete event.result 1477 | 1478 | if (events && (listeners = _.has(events, event.type) && events[event.type])) { 1479 | _.each(listeners.slice(), function (listener) { 1480 | if ((event.result = listener.apply(object, args)) === false) { 1481 | event.cancelled = true 1482 | } 1483 | return !event.aborted 1484 | }) 1485 | } 1486 | return event.result 1487 | } 1488 | 1489 | /** 1490 | * Returns an array of event listeners for a given type that can be manipulated 1491 | * to add or remove listeners. 1492 | * 1493 | * @memberOf Benchmark, Benchmark.Suite 1494 | * @param {string} type The event type. 1495 | * @returns {Array} The listeners array. 1496 | */ 1497 | function listeners(type) { 1498 | var object = this, 1499 | events = object.events || (object.events = {}) 1500 | 1501 | return _.has(events, type) ? events[type] : (events[type] = []) 1502 | } 1503 | 1504 | /** 1505 | * Unregisters a listener for the specified event type(s), 1506 | * or unregisters all listeners for the specified event type(s), 1507 | * or unregisters all listeners for all event types. 1508 | * 1509 | * @memberOf Benchmark, Benchmark.Suite 1510 | * @param {string} [type] The event type. 1511 | * @param {Function} [listener] The function to unregister. 1512 | * @returns {Object} The current instance. 1513 | * @example 1514 | * 1515 | * // unregister a listener for an event type 1516 | * bench.off('cycle', listener); 1517 | * 1518 | * // unregister a listener for multiple event types 1519 | * bench.off('start cycle', listener); 1520 | * 1521 | * // unregister all listeners for an event type 1522 | * bench.off('cycle'); 1523 | * 1524 | * // unregister all listeners for multiple event types 1525 | * bench.off('start cycle complete'); 1526 | * 1527 | * // unregister all listeners for all event types 1528 | * bench.off(); 1529 | */ 1530 | function off(type, listener) { 1531 | var object = this, 1532 | events = object.events 1533 | 1534 | if (!events) { 1535 | return object 1536 | } 1537 | _.each(type ? type.split(' ') : events, function (listeners, type) { 1538 | var index 1539 | if (typeof listeners == 'string') { 1540 | type = listeners 1541 | listeners = _.has(events, type) && events[type] 1542 | } 1543 | if (listeners) { 1544 | if (listener) { 1545 | index = _.indexOf(listeners, listener) 1546 | if (index > -1) { 1547 | listeners.splice(index, 1) 1548 | } 1549 | } else { 1550 | listeners.length = 0 1551 | } 1552 | } 1553 | }) 1554 | return object 1555 | } 1556 | 1557 | /** 1558 | * Registers a listener for the specified event type(s). 1559 | * 1560 | * @memberOf Benchmark, Benchmark.Suite 1561 | * @param {string} type The event type. 1562 | * @param {Function} listener The function to register. 1563 | * @returns {Object} The current instance. 1564 | * @example 1565 | * 1566 | * // register a listener for an event type 1567 | * bench.on('cycle', listener); 1568 | * 1569 | * // register a listener for multiple event types 1570 | * bench.on('start cycle', listener); 1571 | */ 1572 | function on(type, listener) { 1573 | var object = this, 1574 | events = object.events || (object.events = {}) 1575 | 1576 | _.each(type.split(' '), function (type) { 1577 | ;(_.has(events, type) ? events[type] : (events[type] = [])).push(listener) 1578 | }) 1579 | return object 1580 | } 1581 | 1582 | /*------------------------------------------------------------------------*/ 1583 | 1584 | /** 1585 | * Aborts the benchmark without recording times. 1586 | * 1587 | * @memberOf Benchmark 1588 | * @returns {Object} The benchmark instance. 1589 | */ 1590 | function abort() { 1591 | var event, 1592 | bench = this, 1593 | resetting = calledBy.reset 1594 | 1595 | if (bench.running) { 1596 | event = Event('abort') 1597 | bench.emit(event) 1598 | if (!event.cancelled || resetting) { 1599 | // Avoid infinite recursion. 1600 | calledBy.abort = true 1601 | bench.reset() 1602 | delete calledBy.abort 1603 | 1604 | if (support.timeout) { 1605 | clearTimeout(bench._timerId) 1606 | delete bench._timerId 1607 | } 1608 | if (!resetting) { 1609 | bench.aborted = true 1610 | bench.running = false 1611 | } 1612 | } 1613 | } 1614 | return bench 1615 | } 1616 | 1617 | /** 1618 | * Creates a new benchmark using the same test and options. 1619 | * 1620 | * @memberOf Benchmark 1621 | * @param {Object} options Options object to overwrite cloned options. 1622 | * @returns {Object} The new benchmark instance. 1623 | * @example 1624 | * 1625 | * var bizarro = bench.clone({ 1626 | * 'name': 'doppelganger' 1627 | * }); 1628 | */ 1629 | function clone(options) { 1630 | var bench = this, 1631 | result = new bench.constructor(_.assign({}, bench, options)) 1632 | 1633 | // Correct the `options` object. 1634 | result.options = _.assign({}, cloneDeep(bench.options), cloneDeep(options)) 1635 | 1636 | // Copy own custom properties. 1637 | _.forOwn(bench, function (value, key) { 1638 | if (!_.has(result, key)) { 1639 | result[key] = cloneDeep(value) 1640 | } 1641 | }) 1642 | 1643 | return result 1644 | } 1645 | 1646 | /** 1647 | * Determines if a benchmark is faster than another. 1648 | * 1649 | * @memberOf Benchmark 1650 | * @param {Object} other The benchmark to compare. 1651 | * @returns {number} Returns `-1` if slower, `1` if faster, and `0` if indeterminate. 1652 | */ 1653 | function compare(other) { 1654 | var bench = this 1655 | 1656 | // Exit early if comparing the same benchmark. 1657 | if (bench == other) { 1658 | return 0 1659 | } 1660 | var critical, 1661 | zStat, 1662 | sample1 = bench.stats.sample, 1663 | sample2 = other.stats.sample, 1664 | size1 = sample1.length, 1665 | size2 = sample2.length, 1666 | maxSize = max(size1, size2), 1667 | minSize = min(size1, size2), 1668 | u1 = getU(sample1, sample2), 1669 | u2 = getU(sample2, sample1), 1670 | u = min(u1, u2) 1671 | 1672 | function getScore(xA, sampleB) { 1673 | return _.reduce( 1674 | sampleB, 1675 | function (total, xB) { 1676 | return total + (xB > xA ? 0 : xB < xA ? 1 : 0.5) 1677 | }, 1678 | 0, 1679 | ) 1680 | } 1681 | 1682 | function getU(sampleA, sampleB) { 1683 | return _.reduce( 1684 | sampleA, 1685 | function (total, xA) { 1686 | return total + getScore(xA, sampleB) 1687 | }, 1688 | 0, 1689 | ) 1690 | } 1691 | 1692 | function getZ(u) { 1693 | return ( 1694 | (u - (size1 * size2) / 2) / 1695 | sqrt((size1 * size2 * (size1 + size2 + 1)) / 12) 1696 | ) 1697 | } 1698 | // Reject the null hypothesis the two samples come from the 1699 | // same population (i.e. have the same median) if... 1700 | if (size1 + size2 > 30) { 1701 | // ...the z-stat is greater than 1.96 or less than -1.96 1702 | // http://www.statisticslectures.com/topics/mannwhitneyu/ 1703 | zStat = getZ(u) 1704 | return abs(zStat) > 1.96 ? (u == u1 ? 1 : -1) : 0 1705 | } 1706 | // ...the U value is less than or equal the critical U value. 1707 | critical = maxSize < 5 || minSize < 3 ? 0 : uTable[maxSize][minSize - 3] 1708 | return u <= critical ? (u == u1 ? 1 : -1) : 0 1709 | } 1710 | 1711 | /** 1712 | * Reset properties and abort if running. 1713 | * 1714 | * @memberOf Benchmark 1715 | * @returns {Object} The benchmark instance. 1716 | */ 1717 | function reset() { 1718 | var bench = this 1719 | if (bench.running && !calledBy.abort) { 1720 | // No worries, `reset()` is called within `abort()`. 1721 | calledBy.reset = true 1722 | bench.abort() 1723 | delete calledBy.reset 1724 | return bench 1725 | } 1726 | var event, 1727 | index = 0, 1728 | changes = [], 1729 | queue = [] 1730 | 1731 | // A non-recursive solution to check if properties have changed. 1732 | // For more information see http://www.jslab.dk/articles/non.recursive.preorder.traversal.part4. 1733 | var data = { 1734 | destination: bench, 1735 | source: _.assign( 1736 | {}, 1737 | cloneDeep(bench.constructor.prototype), 1738 | cloneDeep(bench.options), 1739 | ), 1740 | } 1741 | 1742 | do { 1743 | _.forOwn(data.source, function (value, key) { 1744 | var changed, 1745 | destination = data.destination, 1746 | currValue = destination[key] 1747 | 1748 | // Skip pseudo private properties and event listeners. 1749 | if (/^_|^events$|^on[A-Z]/.test(key)) { 1750 | return 1751 | } 1752 | if (_.isObjectLike(value)) { 1753 | if (_.isArray(value)) { 1754 | // Check if an array value has changed to a non-array value. 1755 | if (!_.isArray(currValue)) { 1756 | changed = true 1757 | currValue = [] 1758 | } 1759 | // Check if an array has changed its length. 1760 | if (currValue.length != value.length) { 1761 | changed = true 1762 | currValue = currValue.slice(0, value.length) 1763 | currValue.length = value.length 1764 | } 1765 | } 1766 | // Check if an object has changed to a non-object value. 1767 | else if (!_.isObjectLike(currValue)) { 1768 | changed = true 1769 | currValue = {} 1770 | } 1771 | // Register a changed object. 1772 | if (changed) { 1773 | changes.push({ destination: destination, key: key, value: currValue }) 1774 | } 1775 | queue.push({ destination: currValue, source: value }) 1776 | } 1777 | // Register a changed primitive. 1778 | else if (!_.eq(currValue, value) && value !== undefined) { 1779 | changes.push({ destination: destination, key: key, value: value }) 1780 | } 1781 | }) 1782 | } while ((data = queue[index++])) 1783 | 1784 | // If changed emit the `reset` event and if it isn't cancelled reset the benchmark. 1785 | if ( 1786 | changes.length && 1787 | (bench.emit((event = Event('reset'))), !event.cancelled) 1788 | ) { 1789 | _.each(changes, function (data) { 1790 | data.destination[data.key] = data.value 1791 | }) 1792 | } 1793 | return bench 1794 | } 1795 | 1796 | /** 1797 | * Displays relevant benchmark information when coerced to a string. 1798 | * 1799 | * @name toString 1800 | * @memberOf Benchmark 1801 | * @returns {string} A string representation of the benchmark instance. 1802 | */ 1803 | function toStringBench() { 1804 | var bench = this, 1805 | error = bench.error, 1806 | hz = bench.hz, 1807 | id = bench.id, 1808 | stats = bench.stats, 1809 | size = stats.sample.length, 1810 | pm = '\xb1', 1811 | result = bench.name || (_.isNaN(id) ? id : '') 1812 | 1813 | if (error) { 1814 | var errorStr 1815 | if (!_.isObject(error)) { 1816 | errorStr = String(error) 1817 | } else if (!_.isError(Error)) { 1818 | errorStr = join(error) 1819 | } else { 1820 | // Error#name and Error#message properties are non-enumerable. 1821 | errorStr = join( 1822 | _.assign({ name: error.name, message: error.message }, error), 1823 | ) 1824 | } 1825 | result += ': ' + errorStr 1826 | } else { 1827 | result += 1828 | ' x ' + 1829 | formatNumber(hz.toFixed(hz < 100 ? 2 : 0)) + 1830 | ' ops/sec ' + 1831 | pm + 1832 | stats.rme.toFixed(2) + 1833 | '% (' + 1834 | size + 1835 | ' run' + 1836 | (size == 1 ? '' : 's') + 1837 | ' sampled)' 1838 | } 1839 | return result 1840 | } 1841 | 1842 | /*------------------------------------------------------------------------*/ 1843 | 1844 | /** 1845 | * Clocks the time taken to execute a test per cycle (secs). 1846 | * 1847 | * @private 1848 | * @param {Object} bench The benchmark instance. 1849 | * @returns {number} The time taken. 1850 | */ 1851 | function clock() { 1852 | var options = Benchmark.options, 1853 | templateData = {}, 1854 | timers = [{ ns: timer.ns, res: max(0.0015, getRes('ms')), unit: 'ms' }] 1855 | 1856 | // Lazy define for hi-res timers. 1857 | clock = function (clone) { 1858 | var deferred 1859 | 1860 | if (clone instanceof Deferred) { 1861 | deferred = clone 1862 | clone = deferred.benchmark 1863 | } 1864 | var bench = clone._original, 1865 | stringable = isStringable(bench.fn), 1866 | count = (bench.count = clone.count), 1867 | decompilable = 1868 | stringable || 1869 | (support.decompilation && 1870 | (clone.setup !== _.noop || clone.teardown !== _.noop)), 1871 | id = bench.id, 1872 | name = bench.name || (typeof id == 'number' ? '' : id), 1873 | result = 0 1874 | 1875 | // Init `minTime` if needed. 1876 | clone.minTime = 1877 | bench.minTime || (bench.minTime = bench.options.minTime = options.minTime) 1878 | 1879 | // Compile in setup/teardown functions and the test loop. 1880 | // Create a new compiled test, instead of using the cached `bench.compiled`, 1881 | // to avoid potential engine optimizations enabled over the life of the test. 1882 | var funcBody = deferred 1883 | ? 'var d#=this,${fnArg}=d#,m#=d#.benchmark._original,f#=m#.fn,su#=m#.setup,td#=m#.teardown;' + 1884 | // When `deferred.cycles` is `0` then... 1885 | 'if(!d#.cycles){' + 1886 | // set `deferred.fn`, 1887 | 'd#.fn=function(){var ${fnArg}=d#;if(typeof f#=="function"){try{${fn}\n}catch(e#){f#(d#)}}else{${fn}\n}};' + 1888 | // set `deferred.teardown`, 1889 | 'd#.teardown=function(){d#.cycles=0;if(typeof td#=="function"){try{${teardown}\n}catch(e#){td#()}}else{${teardown}\n}};' + 1890 | // execute the benchmark's `setup`, 1891 | 'if(typeof su#=="function"){try{${setup}\n}catch(e#){su#()}}else{${setup}\n};' + 1892 | // start timer, 1893 | 't#.start(d#);' + 1894 | // and then execute `deferred.fn` and return a dummy object. 1895 | '}d#.fn();return{uid:"${uid}"}' 1896 | : 'var r#,s#,m#=this,f#=m#.fn,i#=m#.count,n#=t#.ns;${setup}\n${begin};' + 1897 | 'while(i#--){${fn}\n}${end};${teardown}\nreturn{elapsed:r#,uid:"${uid}"}' 1898 | 1899 | var compiled = (bench.compiled = clone.compiled = createCompiled( 1900 | bench, 1901 | decompilable, 1902 | deferred, 1903 | funcBody, 1904 | )), 1905 | isEmpty = !(templateData.fn || stringable) 1906 | 1907 | try { 1908 | if (isEmpty) { 1909 | // Firefox may remove dead code from `Function#toString` results. 1910 | // For more information see http://bugzil.la/536085. 1911 | throw new Error( 1912 | 'The test "' + 1913 | name + 1914 | '" is empty. This may be the result of dead code removal.', 1915 | ) 1916 | } else if (!deferred) { 1917 | // Pretest to determine if compiled code exits early, usually by a 1918 | // rogue `return` statement, by checking for a return object with the uid. 1919 | bench.count = 1 1920 | compiled = 1921 | decompilable && 1922 | (compiled.call(bench, context, timer) || {}).uid == 1923 | templateData.uid && 1924 | compiled 1925 | bench.count = count 1926 | } 1927 | } catch (e) { 1928 | compiled = null 1929 | clone.error = e || new Error(String(e)) 1930 | bench.count = count 1931 | } 1932 | // Fallback when a test exits early or errors during pretest. 1933 | if (!compiled && !deferred && !isEmpty) { 1934 | funcBody = 1935 | (stringable || (decompilable && !clone.error) 1936 | ? 'function f#(){${fn}\n}var r#,s#,m#=this,i#=m#.count' 1937 | : 'var r#,s#,m#=this,f#=m#.fn,i#=m#.count') + 1938 | ',n#=t#.ns;${setup}\n${begin};m#.f#=f#;while(i#--){m#.f#()}${end};' + 1939 | 'delete m#.f#;${teardown}\nreturn{elapsed:r#}' 1940 | 1941 | compiled = createCompiled(bench, decompilable, deferred, funcBody) 1942 | 1943 | try { 1944 | // Pretest one more time to check for errors. 1945 | bench.count = 1 1946 | compiled.call(bench, context, timer) 1947 | bench.count = count 1948 | delete clone.error 1949 | } catch (e) { 1950 | bench.count = count 1951 | if (!clone.error) { 1952 | clone.error = e || new Error(String(e)) 1953 | } 1954 | } 1955 | } 1956 | // If no errors run the full test loop. 1957 | if (!clone.error) { 1958 | compiled = bench.compiled = clone.compiled = createCompiled( 1959 | bench, 1960 | decompilable, 1961 | deferred, 1962 | funcBody, 1963 | ) 1964 | result = compiled.call(deferred || bench, context, timer).elapsed 1965 | } 1966 | return result 1967 | } 1968 | 1969 | /*----------------------------------------------------------------------*/ 1970 | 1971 | /** 1972 | * Creates a compiled function from the given function `body`. 1973 | */ 1974 | function createCompiled(bench, decompilable, deferred, body) { 1975 | var fn = bench.fn, 1976 | fnArg = deferred ? getFirstArgument(fn) || 'deferred' : '' 1977 | 1978 | templateData.uid = uid + uidCounter++ 1979 | 1980 | _.assign(templateData, { 1981 | setup: decompilable ? getSource(bench.setup) : interpolate('m#.setup()'), 1982 | fn: decompilable ? getSource(fn) : interpolate('m#.fn(' + fnArg + ')'), 1983 | fnArg: fnArg, 1984 | teardown: decompilable 1985 | ? getSource(bench.teardown) 1986 | : interpolate('m#.teardown()'), 1987 | }) 1988 | 1989 | // Use API of chosen timer. 1990 | if (timer.unit == 'ns') { 1991 | _.assign(templateData, { 1992 | begin: interpolate('s#=n#()'), 1993 | end: interpolate('r#=n#(s#);r#=r#[0]+(r#[1]/1e9)'), 1994 | }) 1995 | } else if (timer.unit == 'us') { 1996 | if (timer.ns.stop) { 1997 | _.assign(templateData, { 1998 | begin: interpolate('s#=n#.start()'), 1999 | end: interpolate('r#=n#.microseconds()/1e6'), 2000 | }) 2001 | } else { 2002 | _.assign(templateData, { 2003 | begin: interpolate('s#=n#()'), 2004 | end: interpolate('r#=(n#()-s#)/1e6'), 2005 | }) 2006 | } 2007 | } else if (timer.ns.now) { 2008 | _.assign(templateData, { 2009 | begin: interpolate('s#=(+n#.now())'), 2010 | end: interpolate('r#=((+n#.now())-s#)/1e3'), 2011 | }) 2012 | } else { 2013 | _.assign(templateData, { 2014 | begin: interpolate('s#=new n#().getTime()'), 2015 | end: interpolate('r#=(new n#().getTime()-s#)/1e3'), 2016 | }) 2017 | } 2018 | // Define `timer` methods. 2019 | timer.start = createFunction( 2020 | interpolate('o#'), 2021 | interpolate('var n#=this.ns,${begin};o#.elapsed=0;o#.timeStamp=s#'), 2022 | ) 2023 | 2024 | timer.stop = createFunction( 2025 | interpolate('o#'), 2026 | interpolate('var n#=this.ns,s#=o#.timeStamp,${end};o#.elapsed=r#'), 2027 | ) 2028 | 2029 | // Create compiled test. 2030 | return createFunction( 2031 | interpolate('window,t#'), 2032 | 'var global = window, clearTimeout = global.clearTimeout, setTimeout = global.setTimeout;\n' + 2033 | interpolate(body), 2034 | ) 2035 | } 2036 | 2037 | /** 2038 | * Gets the current timer's minimum resolution (secs). 2039 | */ 2040 | function getRes(unit) { 2041 | var measured, 2042 | begin, 2043 | count = 30, 2044 | divisor = 1e3, 2045 | ns = timer.ns, 2046 | sample = [] 2047 | 2048 | // Get average smallest measurable time. 2049 | while (count--) { 2050 | if (unit == 'us') { 2051 | divisor = 1e6 2052 | if (ns.stop) { 2053 | ns.start() 2054 | while (!(measured = ns.microseconds())) {} 2055 | } else { 2056 | begin = ns() 2057 | while (!(measured = ns() - begin)) {} 2058 | } 2059 | } else if (unit == 'ns') { 2060 | divisor = 1e9 2061 | begin = (begin = ns())[0] + begin[1] / divisor 2062 | while ( 2063 | !(measured = (measured = ns())[0] + measured[1] / divisor - begin) 2064 | ) {} 2065 | divisor = 1 2066 | } else if (ns.now) { 2067 | begin = +ns.now() 2068 | while (!(measured = +ns.now() - begin)) {} 2069 | } else { 2070 | begin = new ns().getTime() 2071 | while (!(measured = new ns().getTime() - begin)) {} 2072 | } 2073 | // Check for broken timers. 2074 | if (measured > 0) { 2075 | sample.push(measured) 2076 | } else { 2077 | sample.push(Infinity) 2078 | break 2079 | } 2080 | } 2081 | // Convert to seconds. 2082 | return getMean(sample) / divisor 2083 | } 2084 | 2085 | /** 2086 | * Interpolates a given template string. 2087 | */ 2088 | function interpolate(string) { 2089 | // Replaces all occurrences of `#` with a unique number and template tokens with content. 2090 | return _.template(string.replace(/\#/g, /\d+/.exec(templateData.uid)))( 2091 | templateData, 2092 | ) 2093 | } 2094 | 2095 | /*----------------------------------------------------------------------*/ 2096 | 2097 | // Detect Chrome's microsecond timer: 2098 | // enable benchmarking via the --enable-benchmarking command 2099 | // line switch in at least Chrome 7 to use chrome.Interval 2100 | try { 2101 | if ((timer.ns = new (context.chrome || context.chromium).Interval())) { 2102 | timers.push({ ns: timer.ns, res: getRes('us'), unit: 'us' }) 2103 | } 2104 | } catch (e) {} 2105 | 2106 | // Detect Node.js's nanosecond resolution timer available in Node.js >= 0.8. 2107 | if (processObject && typeof (timer.ns = processObject.hrtime) == 'function') { 2108 | timers.push({ ns: timer.ns, res: getRes('ns'), unit: 'ns' }) 2109 | } 2110 | // Detect Wade Simmons' Node.js `microtime` module. 2111 | if ( 2112 | microtimeObject && 2113 | typeof (timer.ns = microtimeObject.now) == 'function' 2114 | ) { 2115 | timers.push({ ns: timer.ns, res: getRes('us'), unit: 'us' }) 2116 | } 2117 | // Pick timer with highest resolution. 2118 | timer = _.minBy(timers, 'res') 2119 | 2120 | // Error if there are no working timers. 2121 | if (timer.res == Infinity) { 2122 | throw new Error('Benchmark.js was unable to find a working timer.') 2123 | } 2124 | // Resolve time span required to achieve a percent uncertainty of at most 1%. 2125 | // For more information see http://spiff.rit.edu/classes/phys273/uncert/uncert.html. 2126 | options.minTime || (options.minTime = max(timer.res / 2 / 0.01, 0.05)) 2127 | return clock.apply(null, arguments) 2128 | } 2129 | 2130 | /*------------------------------------------------------------------------*/ 2131 | 2132 | /** 2133 | * Computes stats on benchmark results. 2134 | * 2135 | * @private 2136 | * @param {Object} bench The benchmark instance. 2137 | * @param {Object} options The options object. 2138 | */ 2139 | function compute(bench, options) { 2140 | options || (options = {}) 2141 | 2142 | var async = options.async, 2143 | elapsed = 0, 2144 | initCount = bench.initCount, 2145 | minSamples = bench.minSamples, 2146 | queue = [], 2147 | sample = bench.stats.sample 2148 | 2149 | /** 2150 | * Adds a clone to the queue. 2151 | */ 2152 | function enqueue() { 2153 | queue.push( 2154 | _.assign(bench.clone(), { 2155 | _original: bench, 2156 | events: { 2157 | abort: [update], 2158 | cycle: [update], 2159 | error: [update], 2160 | start: [update], 2161 | }, 2162 | }), 2163 | ) 2164 | } 2165 | 2166 | /** 2167 | * Updates the clone/original benchmarks to keep their data in sync. 2168 | */ 2169 | function update(event) { 2170 | var clone = this, 2171 | type = event.type 2172 | 2173 | if (bench.running) { 2174 | if (type == 'start') { 2175 | // Note: `clone.minTime` prop is inited in `clock()`. 2176 | clone.count = bench.initCount 2177 | } else { 2178 | if (type == 'error') { 2179 | bench.error = clone.error 2180 | } 2181 | if (type == 'abort') { 2182 | bench.abort() 2183 | bench.emit('cycle') 2184 | } else { 2185 | event.currentTarget = event.target = bench 2186 | bench.emit(event) 2187 | } 2188 | } 2189 | } else if (bench.aborted) { 2190 | // Clear abort listeners to avoid triggering bench's abort/cycle again. 2191 | clone.events.abort.length = 0 2192 | clone.abort() 2193 | } 2194 | } 2195 | 2196 | /** 2197 | * Determines if more clones should be queued or if cycling should stop. 2198 | */ 2199 | function evaluate(event) { 2200 | var critical, 2201 | df, 2202 | mean, 2203 | moe, 2204 | rme, 2205 | sd, 2206 | sem, 2207 | variance, 2208 | clone = event.target, 2209 | done = bench.aborted, 2210 | now = +_.now(), 2211 | size = sample.push(clone.times.period), 2212 | maxedOut = 2213 | size >= minSamples && 2214 | (elapsed += now - clone.times.timeStamp) / 1e3 > bench.maxTime, 2215 | times = bench.times, 2216 | varOf = function (sum, x) { 2217 | return sum + pow(x - mean, 2) 2218 | } 2219 | 2220 | // Exit early for aborted or unclockable tests. 2221 | if (done || clone.hz == Infinity) { 2222 | maxedOut = !(size = sample.length = queue.length = 0) 2223 | } 2224 | 2225 | if (!done) { 2226 | // Compute the sample mean (estimate of the population mean). 2227 | mean = getMean(sample) 2228 | // Compute the sample variance (estimate of the population variance). 2229 | variance = _.reduce(sample, varOf, 0) / (size - 1) || 0 2230 | // Compute the sample standard deviation (estimate of the population standard deviation). 2231 | sd = sqrt(variance) 2232 | // Compute the standard error of the mean (a.k.a. the standard deviation of the sampling distribution of the sample mean). 2233 | sem = sd / sqrt(size) 2234 | // Compute the degrees of freedom. 2235 | df = size - 1 2236 | // Compute the critical value. 2237 | critical = tTable[df || 1] || tTable.infinity 2238 | // Compute the margin of error. 2239 | moe = sem * critical 2240 | // Compute the relative margin of error. 2241 | rme = (moe / mean) * 100 || 0 2242 | 2243 | _.assign(bench.stats, { 2244 | deviation: sd, 2245 | mean: mean, 2246 | moe: moe, 2247 | rme: rme, 2248 | sem: sem, 2249 | variance: variance, 2250 | }) 2251 | 2252 | // Abort the cycle loop when the minimum sample size has been collected 2253 | // and the elapsed time exceeds the maximum time allowed per benchmark. 2254 | // We don't count cycle delays toward the max time because delays may be 2255 | // increased by browsers that clamp timeouts for inactive tabs. For more 2256 | // information see https://developer.mozilla.org/en/window.setTimeout#Inactive_tabs. 2257 | if (maxedOut) { 2258 | // Reset the `initCount` in case the benchmark is rerun. 2259 | bench.initCount = initCount 2260 | bench.running = false 2261 | done = true 2262 | times.elapsed = (now - times.timeStamp) / 1e3 2263 | } 2264 | if (bench.hz != Infinity) { 2265 | bench.hz = 1 / mean 2266 | times.cycle = mean * bench.count 2267 | times.period = mean 2268 | } 2269 | } 2270 | // If time permits, increase sample size to reduce the margin of error. 2271 | if (queue.length < 2 && !maxedOut) { 2272 | enqueue() 2273 | } 2274 | // Abort the `invoke` cycle when done. 2275 | event.aborted = done 2276 | } 2277 | 2278 | // Init queue and begin. 2279 | enqueue() 2280 | invoke(queue, { 2281 | name: 'run', 2282 | args: { async: async }, 2283 | queued: true, 2284 | onCycle: evaluate, 2285 | onComplete: function () { 2286 | bench.emit('complete') 2287 | }, 2288 | }) 2289 | } 2290 | 2291 | /*------------------------------------------------------------------------*/ 2292 | 2293 | /** 2294 | * Cycles a benchmark until a run `count` can be established. 2295 | * 2296 | * @private 2297 | * @param {Object} clone The cloned benchmark instance. 2298 | * @param {Object} options The options object. 2299 | */ 2300 | function cycle(clone, options) { 2301 | options || (options = {}) 2302 | 2303 | var deferred 2304 | if (clone instanceof Deferred) { 2305 | deferred = clone 2306 | clone = clone.benchmark 2307 | } 2308 | var clocked, 2309 | cycles, 2310 | divisor, 2311 | minTime, 2312 | bench = clone._original, 2313 | count = clone.count 2314 | 2315 | // Continue, if not aborted between cycles. 2316 | if (clone.running) { 2317 | // `minTime` is set to `Benchmark.options.minTime` in `clock()`. 2318 | cycles = ++clone.cycles 2319 | clocked = clock(clone) 2320 | minTime = clone.minTime 2321 | 2322 | if (cycles > bench.cycles) { 2323 | bench.cycles = cycles 2324 | } 2325 | } 2326 | // Continue, if not errored. 2327 | if (clone.running) { 2328 | // Compute the seconds per operation. 2329 | var period = clocked / count 2330 | // Compute the ops per second. 2331 | clone.hz = 1 / period 2332 | // Avoid working our way up to this next time. 2333 | clone.initCount = count 2334 | // Do we need to do another cycle? 2335 | clone.running = clocked < minTime 2336 | 2337 | if (clone.running) { 2338 | // Tests may clock at `0` when `initCount` is a small number, 2339 | // to avoid that we set its count to something a bit higher. 2340 | if (!clocked && (divisor = divisors[clone.cycles]) != null) { 2341 | count = floor(4e6 / divisor) 2342 | } 2343 | // Calculate how many more iterations it will take to achieve the `minTime`. 2344 | if (count <= clone.count) { 2345 | count += Math.ceil((minTime - clocked) / period) 2346 | } 2347 | clone.running = count != Infinity 2348 | } 2349 | } 2350 | // Should we exit early? 2351 | let event = Event('cycle') 2352 | clone.emit(event) 2353 | if (event.aborted) { 2354 | clone.abort() 2355 | } 2356 | // Figure out what to do next. 2357 | if (clone.running) { 2358 | // Start a new cycle. 2359 | clone.count = count 2360 | 2361 | cycle(clone) 2362 | } else { 2363 | // We're done. 2364 | clone.emit('complete') 2365 | } 2366 | } 2367 | 2368 | /*------------------------------------------------------------------------*/ 2369 | 2370 | /** 2371 | * Runs the benchmark. 2372 | * 2373 | * @memberOf Benchmark 2374 | * @param {Object} [options={}] Options object. 2375 | * @returns {Object} The benchmark instance. 2376 | * @example 2377 | * 2378 | * // basic usage 2379 | * bench.run(); 2380 | * 2381 | * // or with options 2382 | * bench.run({ 'async': true }); 2383 | */ 2384 | function run(options) { 2385 | var bench = this, 2386 | event = Event('start') 2387 | 2388 | // Set `running` to `false` so `reset()` won't call `abort()`. 2389 | bench.running = false 2390 | bench.reset() 2391 | bench.running = true 2392 | 2393 | bench.count = bench.initCount 2394 | bench.times.timeStamp = +_.now() 2395 | bench.emit(event) 2396 | 2397 | if (!event.cancelled) { 2398 | options = { 2399 | async: 2400 | ((options = options && options.async) == null 2401 | ? bench.async 2402 | : options) && support.timeout, 2403 | } 2404 | 2405 | // For clones created within `compute()`. 2406 | if (bench._original) { 2407 | if (bench.defer) { 2408 | Deferred(bench) 2409 | } else { 2410 | cycle(bench, options) 2411 | } 2412 | } 2413 | // For original benchmarks. 2414 | else { 2415 | compute(bench, options) 2416 | } 2417 | } 2418 | return bench 2419 | } 2420 | 2421 | /*------------------------------------------------------------------------*/ 2422 | 2423 | // Firefox 1 erroneously defines variable and argument names of functions on 2424 | // the function itself as non-configurable properties with `undefined` values. 2425 | // The bugginess continues as the `Benchmark` constructor has an argument 2426 | // named `options` and Firefox 1 will not assign a value to `Benchmark.options`, 2427 | // making it non-writable in the process, unless it is the first property 2428 | // assigned by for-in loop of `_.assign()`. 2429 | _.assign(Benchmark, { 2430 | /** 2431 | * The default options copied by benchmark instances. 2432 | * 2433 | * @static 2434 | * @memberOf Benchmark 2435 | * @type Object 2436 | */ 2437 | options: { 2438 | /** 2439 | * A flag to indicate that benchmark cycles will execute asynchronously 2440 | * by default. 2441 | * 2442 | * @memberOf Benchmark.options 2443 | * @type boolean 2444 | */ 2445 | async: false, 2446 | 2447 | /** 2448 | * A flag to indicate that the benchmark clock is deferred. 2449 | * 2450 | * @memberOf Benchmark.options 2451 | * @type boolean 2452 | */ 2453 | defer: false, 2454 | 2455 | /** 2456 | * The delay between test cycles (secs). 2457 | * @memberOf Benchmark.options 2458 | * @type number 2459 | */ 2460 | delay: 0.005, 2461 | 2462 | /** 2463 | * Displayed by `Benchmark#toString` when a `name` is not available 2464 | * (auto-generated if absent). 2465 | * 2466 | * @memberOf Benchmark.options 2467 | * @type string 2468 | */ 2469 | id: undefined, 2470 | 2471 | /** 2472 | * The default number of times to execute a test on a benchmark's first cycle. 2473 | * 2474 | * @memberOf Benchmark.options 2475 | * @type number 2476 | */ 2477 | initCount: 1, 2478 | 2479 | /** 2480 | * The maximum time a benchmark is allowed to run before finishing (secs). 2481 | * 2482 | * Note: Cycle delays aren't counted toward the maximum time. 2483 | * 2484 | * @memberOf Benchmark.options 2485 | * @type number 2486 | */ 2487 | maxTime: 5, 2488 | 2489 | /** 2490 | * The minimum sample size required to perform statistical analysis. 2491 | * 2492 | * @memberOf Benchmark.options 2493 | * @type number 2494 | */ 2495 | minSamples: 5, 2496 | 2497 | /** 2498 | * The time needed to reduce the percent uncertainty of measurement to 1% (secs). 2499 | * 2500 | * @memberOf Benchmark.options 2501 | * @type number 2502 | */ 2503 | minTime: 0, 2504 | 2505 | /** 2506 | * The name of the benchmark. 2507 | * 2508 | * @memberOf Benchmark.options 2509 | * @type string 2510 | */ 2511 | name: undefined, 2512 | 2513 | /** 2514 | * An event listener called when the benchmark is aborted. 2515 | * 2516 | * @memberOf Benchmark.options 2517 | * @type Function 2518 | */ 2519 | onAbort: undefined, 2520 | 2521 | /** 2522 | * An event listener called when the benchmark completes running. 2523 | * 2524 | * @memberOf Benchmark.options 2525 | * @type Function 2526 | */ 2527 | onComplete: undefined, 2528 | 2529 | /** 2530 | * An event listener called after each run cycle. 2531 | * 2532 | * @memberOf Benchmark.options 2533 | * @type Function 2534 | */ 2535 | onCycle: undefined, 2536 | 2537 | /** 2538 | * An event listener called when a test errors. 2539 | * 2540 | * @memberOf Benchmark.options 2541 | * @type Function 2542 | */ 2543 | onError: undefined, 2544 | 2545 | /** 2546 | * An event listener called when the benchmark is reset. 2547 | * 2548 | * @memberOf Benchmark.options 2549 | * @type Function 2550 | */ 2551 | onReset: undefined, 2552 | 2553 | /** 2554 | * An event listener called when the benchmark starts running. 2555 | * 2556 | * @memberOf Benchmark.options 2557 | * @type Function 2558 | */ 2559 | onStart: undefined, 2560 | }, 2561 | 2562 | /** 2563 | * Platform object with properties describing things like browser name, 2564 | * version, and operating system. See [`platform.js`](https://mths.be/platform). 2565 | * 2566 | * @static 2567 | * @memberOf Benchmark 2568 | * @type Object 2569 | */ 2570 | platform: context.platform || 2571 | require('platform') || { 2572 | description: (context.navigator && context.navigator.userAgent) || null, 2573 | layout: null, 2574 | product: null, 2575 | name: null, 2576 | manufacturer: null, 2577 | os: null, 2578 | prerelease: null, 2579 | version: null, 2580 | toString: function () { 2581 | return this.description || '' 2582 | }, 2583 | }, 2584 | 2585 | /** 2586 | * The semantic version number. 2587 | * 2588 | * @static 2589 | * @memberOf Benchmark 2590 | * @type string 2591 | */ 2592 | version: '2.1.4', 2593 | }) 2594 | 2595 | _.assign(Benchmark, { 2596 | filter: filter, 2597 | formatNumber: formatNumber, 2598 | invoke: invoke, 2599 | join: join, 2600 | runInContext: runInContext, 2601 | support: support, 2602 | }) 2603 | 2604 | // Add lodash methods to Benchmark. 2605 | _.each( 2606 | ['each', 'forEach', 'forOwn', 'has', 'indexOf', 'map', 'reduce'], 2607 | function (methodName) { 2608 | Benchmark[methodName] = _[methodName] 2609 | }, 2610 | ) 2611 | 2612 | /*------------------------------------------------------------------------*/ 2613 | 2614 | _.assign(Benchmark.prototype, { 2615 | /** 2616 | * The number of times a test was executed. 2617 | * 2618 | * @memberOf Benchmark 2619 | * @type number 2620 | */ 2621 | count: 0, 2622 | 2623 | /** 2624 | * The number of cycles performed while benchmarking. 2625 | * 2626 | * @memberOf Benchmark 2627 | * @type number 2628 | */ 2629 | cycles: 0, 2630 | 2631 | /** 2632 | * The number of executions per second. 2633 | * 2634 | * @memberOf Benchmark 2635 | * @type number 2636 | */ 2637 | hz: 0, 2638 | 2639 | /** 2640 | * The compiled test function. 2641 | * 2642 | * @memberOf Benchmark 2643 | * @type {Function|string} 2644 | */ 2645 | compiled: undefined, 2646 | 2647 | /** 2648 | * The error object if the test failed. 2649 | * 2650 | * @memberOf Benchmark 2651 | * @type Object 2652 | */ 2653 | error: undefined, 2654 | 2655 | /** 2656 | * The test to benchmark. 2657 | * 2658 | * @memberOf Benchmark 2659 | * @type {Function|string} 2660 | */ 2661 | fn: undefined, 2662 | 2663 | /** 2664 | * A flag to indicate if the benchmark is aborted. 2665 | * 2666 | * @memberOf Benchmark 2667 | * @type boolean 2668 | */ 2669 | aborted: false, 2670 | 2671 | /** 2672 | * A flag to indicate if the benchmark is running. 2673 | * 2674 | * @memberOf Benchmark 2675 | * @type boolean 2676 | */ 2677 | running: false, 2678 | 2679 | /** 2680 | * Compiled into the test and executed immediately **before** the test loop. 2681 | * 2682 | * @memberOf Benchmark 2683 | * @type {Function|string} 2684 | * @example 2685 | * 2686 | * // basic usage 2687 | * var bench = Benchmark({ 2688 | * 'setup': function() { 2689 | * var c = this.count, 2690 | * element = document.getElementById('container'); 2691 | * while (c--) { 2692 | * element.appendChild(document.createElement('div')); 2693 | * } 2694 | * }, 2695 | * 'fn': function() { 2696 | * element.removeChild(element.lastChild); 2697 | * } 2698 | * }); 2699 | * 2700 | * // compiles to something like: 2701 | * var c = this.count, 2702 | * element = document.getElementById('container'); 2703 | * while (c--) { 2704 | * element.appendChild(document.createElement('div')); 2705 | * } 2706 | * var start = new Date; 2707 | * while (count--) { 2708 | * element.removeChild(element.lastChild); 2709 | * } 2710 | * var end = new Date - start; 2711 | * 2712 | * // or using strings 2713 | * var bench = Benchmark({ 2714 | * 'setup': '\ 2715 | * var a = 0;\n\ 2716 | * (function() {\n\ 2717 | * (function() {\n\ 2718 | * (function() {', 2719 | * 'fn': 'a += 1;', 2720 | * 'teardown': '\ 2721 | * }())\n\ 2722 | * }())\n\ 2723 | * }())' 2724 | * }); 2725 | * 2726 | * // compiles to something like: 2727 | * var a = 0; 2728 | * (function() { 2729 | * (function() { 2730 | * (function() { 2731 | * var start = new Date; 2732 | * while (count--) { 2733 | * a += 1; 2734 | * } 2735 | * var end = new Date - start; 2736 | * }()) 2737 | * }()) 2738 | * }()) 2739 | */ 2740 | setup: _.noop, 2741 | 2742 | /** 2743 | * Compiled into the test and executed immediately **after** the test loop. 2744 | * 2745 | * @memberOf Benchmark 2746 | * @type {Function|string} 2747 | */ 2748 | teardown: _.noop, 2749 | 2750 | /** 2751 | * An object of stats including mean, margin or error, and standard deviation. 2752 | * 2753 | * @memberOf Benchmark 2754 | * @type Object 2755 | */ 2756 | stats: { 2757 | /** 2758 | * The margin of error. 2759 | * 2760 | * @memberOf Benchmark#stats 2761 | * @type number 2762 | */ 2763 | moe: 0, 2764 | 2765 | /** 2766 | * The relative margin of error (expressed as a percentage of the mean). 2767 | * 2768 | * @memberOf Benchmark#stats 2769 | * @type number 2770 | */ 2771 | rme: 0, 2772 | 2773 | /** 2774 | * The standard error of the mean. 2775 | * 2776 | * @memberOf Benchmark#stats 2777 | * @type number 2778 | */ 2779 | sem: 0, 2780 | 2781 | /** 2782 | * The sample standard deviation. 2783 | * 2784 | * @memberOf Benchmark#stats 2785 | * @type number 2786 | */ 2787 | deviation: 0, 2788 | 2789 | /** 2790 | * The sample arithmetic mean (secs). 2791 | * 2792 | * @memberOf Benchmark#stats 2793 | * @type number 2794 | */ 2795 | mean: 0, 2796 | 2797 | /** 2798 | * The array of sampled periods. 2799 | * 2800 | * @memberOf Benchmark#stats 2801 | * @type Array 2802 | */ 2803 | sample: [], 2804 | 2805 | /** 2806 | * The sample variance. 2807 | * 2808 | * @memberOf Benchmark#stats 2809 | * @type number 2810 | */ 2811 | variance: 0, 2812 | }, 2813 | 2814 | /** 2815 | * An object of timing data including cycle, elapsed, period, start, and stop. 2816 | * 2817 | * @memberOf Benchmark 2818 | * @type Object 2819 | */ 2820 | times: { 2821 | /** 2822 | * The time taken to complete the last cycle (secs). 2823 | * 2824 | * @memberOf Benchmark#times 2825 | * @type number 2826 | */ 2827 | cycle: 0, 2828 | 2829 | /** 2830 | * The time taken to complete the benchmark (secs). 2831 | * 2832 | * @memberOf Benchmark#times 2833 | * @type number 2834 | */ 2835 | elapsed: 0, 2836 | 2837 | /** 2838 | * The time taken to execute the test once (secs). 2839 | * 2840 | * @memberOf Benchmark#times 2841 | * @type number 2842 | */ 2843 | period: 0, 2844 | 2845 | /** 2846 | * A timestamp of when the benchmark started (ms). 2847 | * 2848 | * @memberOf Benchmark#times 2849 | * @type number 2850 | */ 2851 | timeStamp: 0, 2852 | }, 2853 | }) 2854 | 2855 | _.assign(Benchmark.prototype, { 2856 | abort: abort, 2857 | clone: clone, 2858 | compare: compare, 2859 | emit: emit, 2860 | listeners: listeners, 2861 | off: off, 2862 | on: on, 2863 | reset: reset, 2864 | run: run, 2865 | toString: toStringBench, 2866 | }) 2867 | 2868 | /*------------------------------------------------------------------------*/ 2869 | 2870 | _.assign(Deferred.prototype, { 2871 | /** 2872 | * The deferred benchmark instance. 2873 | * 2874 | * @memberOf Benchmark.Deferred 2875 | * @type Object 2876 | */ 2877 | benchmark: null, 2878 | 2879 | /** 2880 | * The number of deferred cycles performed while benchmarking. 2881 | * 2882 | * @memberOf Benchmark.Deferred 2883 | * @type number 2884 | */ 2885 | cycles: 0, 2886 | 2887 | /** 2888 | * The time taken to complete the deferred benchmark (secs). 2889 | * 2890 | * @memberOf Benchmark.Deferred 2891 | * @type number 2892 | */ 2893 | elapsed: 0, 2894 | 2895 | /** 2896 | * A timestamp of when the deferred benchmark started (ms). 2897 | * 2898 | * @memberOf Benchmark.Deferred 2899 | * @type number 2900 | */ 2901 | timeStamp: 0, 2902 | }) 2903 | 2904 | _.assign(Deferred.prototype, { 2905 | resolve: resolve, 2906 | }) 2907 | 2908 | /*------------------------------------------------------------------------*/ 2909 | 2910 | _.assign(Event.prototype, { 2911 | /** 2912 | * A flag to indicate if the emitters listener iteration is aborted. 2913 | * 2914 | * @memberOf Benchmark.Event 2915 | * @type boolean 2916 | */ 2917 | aborted: false, 2918 | 2919 | /** 2920 | * A flag to indicate if the default action is cancelled. 2921 | * 2922 | * @memberOf Benchmark.Event 2923 | * @type boolean 2924 | */ 2925 | cancelled: false, 2926 | 2927 | /** 2928 | * The object whose listeners are currently being processed. 2929 | * 2930 | * @memberOf Benchmark.Event 2931 | * @type Object 2932 | */ 2933 | currentTarget: undefined, 2934 | 2935 | /** 2936 | * The return value of the last executed listener. 2937 | * 2938 | * @memberOf Benchmark.Event 2939 | * @type Mixed 2940 | */ 2941 | result: undefined, 2942 | 2943 | /** 2944 | * The object to which the event was originally emitted. 2945 | * 2946 | * @memberOf Benchmark.Event 2947 | * @type Object 2948 | */ 2949 | target: undefined, 2950 | 2951 | /** 2952 | * A timestamp of when the event was created (ms). 2953 | * 2954 | * @memberOf Benchmark.Event 2955 | * @type number 2956 | */ 2957 | timeStamp: 0, 2958 | 2959 | /** 2960 | * The event type. 2961 | * 2962 | * @memberOf Benchmark.Event 2963 | * @type string 2964 | */ 2965 | type: '', 2966 | }) 2967 | 2968 | /*------------------------------------------------------------------------*/ 2969 | 2970 | /** 2971 | * The default options copied by suite instances. 2972 | * 2973 | * @static 2974 | * @memberOf Benchmark.Suite 2975 | * @type Object 2976 | */ 2977 | Suite.options = { 2978 | /** 2979 | * The name of the suite. 2980 | * 2981 | * @memberOf Benchmark.Suite.options 2982 | * @type string 2983 | */ 2984 | name: undefined, 2985 | } 2986 | 2987 | /*------------------------------------------------------------------------*/ 2988 | 2989 | _.assign(Suite.prototype, { 2990 | /** 2991 | * The number of benchmarks in the suite. 2992 | * 2993 | * @memberOf Benchmark.Suite 2994 | * @type number 2995 | */ 2996 | length: 0, 2997 | 2998 | /** 2999 | * A flag to indicate if the suite is aborted. 3000 | * 3001 | * @memberOf Benchmark.Suite 3002 | * @type boolean 3003 | */ 3004 | aborted: false, 3005 | 3006 | /** 3007 | * A flag to indicate if the suite is running. 3008 | * 3009 | * @memberOf Benchmark.Suite 3010 | * @type boolean 3011 | */ 3012 | running: false, 3013 | }) 3014 | 3015 | _.assign(Suite.prototype, { 3016 | abort: abortSuite, 3017 | add: add, 3018 | clone: cloneSuite, 3019 | emit: emit, 3020 | filter: filterSuite, 3021 | join: arrayRef.join, 3022 | listeners: listeners, 3023 | off: off, 3024 | on: on, 3025 | pop: arrayRef.pop, 3026 | push: push, 3027 | reset: resetSuite, 3028 | run: runSuite, 3029 | reverse: arrayRef.reverse, 3030 | shift: shift, 3031 | slice: slice, 3032 | sort: arrayRef.sort, 3033 | splice: arrayRef.splice, 3034 | unshift: unshift, 3035 | }) 3036 | 3037 | /*------------------------------------------------------------------------*/ 3038 | 3039 | // Expose Deferred, Event, and Suite. 3040 | _.assign(Benchmark, { 3041 | Deferred: Deferred, 3042 | Event: Event, 3043 | Suite: Suite, 3044 | }) 3045 | 3046 | /*------------------------------------------------------------------------*/ 3047 | 3048 | // Add lodash methods as Suite methods. 3049 | _.each(['each', 'forEach', 'indexOf', 'map', 'reduce'], function (methodName) { 3050 | var func = _[methodName] 3051 | Suite.prototype[methodName] = function () { 3052 | var args = [this] 3053 | push.apply(args, arguments) 3054 | return func.apply(_, args) 3055 | } 3056 | }) 3057 | 3058 | // Avoid array-like object bugs with `Array#shift` and `Array#splice` 3059 | // in Firefox < 10 and IE < 9. 3060 | _.each(['pop', 'shift', 'splice'], function (methodName) { 3061 | var func = arrayRef[methodName] 3062 | 3063 | Suite.prototype[methodName] = function () { 3064 | var value = this, 3065 | result = func.apply(value, arguments) 3066 | 3067 | if (value.length === 0) { 3068 | delete value[0] 3069 | } 3070 | return result 3071 | } 3072 | }) 3073 | 3074 | // Avoid buggy `Array#unshift` in IE < 8 which doesn't return the new 3075 | // length of the array. 3076 | Suite.prototype.unshift = function () { 3077 | var value = this 3078 | unshift.apply(value, arguments) 3079 | return value.length 3080 | } 3081 | 3082 | export default Benchmark 3083 | -------------------------------------------------------------------------------- /src/lib/benchmark/index.ts: -------------------------------------------------------------------------------- 1 | import { SteadfastWorker } from './steadfast-worker' 2 | import { T_TABLE } from './t-table' 3 | import { calculatePopulationVariance, getTimerResolution } from './utils' 4 | 5 | export type WorkerData = { 6 | count?: number 7 | compiled: string 8 | } 9 | 10 | export type Bench = { 11 | setup?: string 12 | fn: string 13 | teardown?: string 14 | timeout?: number 15 | } 16 | 17 | export type CompiledBench = { 18 | value: string 19 | timeout?: number 20 | signal?: AbortSignal 21 | maxTime?: number 22 | } 23 | 24 | export const worker = new SteadfastWorker( 25 | () => new Worker(new URL('./worker?worker', import.meta.url)), 26 | ) 27 | 28 | export enum Inner { 29 | ShouldReturn = 'INNER_BENCHMARK_SHOULD_RETURN', 30 | Counter = 'INNER_BENCHMARK_COUNTER', 31 | Start = 'INNER_BENCHMARK_START', 32 | Elapsed = 'INNER_BENCHMARK_ELAPSED', 33 | } 34 | 35 | export const getMinTime = async () => { 36 | const res: number = await worker.call( 37 | { compiled: getTimerResolution.toString() }, 38 | 10000, 39 | ) 40 | 41 | return res / 2 / 0.01 42 | } 43 | 44 | export const validate = async (options: Bench) => { 45 | const { setup, fn, teardown, timeout } = options 46 | 47 | const returned = await worker.call( 48 | { 49 | compiled: `async () => { 50 | const console = new Proxy({}, { 51 | get(prop) { 52 | throw Error('Console is not allowed in benchmarks') 53 | } 54 | }) 55 | 56 | ;${setup};${fn};${teardown} 57 | 58 | return '${Inner.ShouldReturn}' 59 | }`, 60 | }, 61 | timeout, 62 | ) 63 | 64 | if (returned !== Inner.ShouldReturn) { 65 | throw Error('`return` statement is not allowed') 66 | } 67 | } 68 | 69 | export const compile = async (options: Bench): Promise => { 70 | await validate(options) 71 | 72 | const { setup, fn, teardown } = options 73 | 74 | return `async (${Inner.Counter}) => { 75 | ${setup} 76 | 77 | const ${Inner.Start} = performance.now() 78 | 79 | while (${Inner.Counter}--) { 80 | ${fn} 81 | } 82 | 83 | const ${Inner.Elapsed} = performance.now() - ${Inner.Start} 84 | ;${teardown} 85 | 86 | return ${Inner.Elapsed} 87 | }` 88 | } 89 | 90 | let minTime: number | undefined 91 | 92 | export const run = async (options: CompiledBench) => { 93 | const { value, signal, timeout, maxTime = 1000 } = options 94 | 95 | if (!minTime) { 96 | minTime = await getMinTime() 97 | } 98 | 99 | let sum = 0 100 | let count = 1 101 | 102 | const samples: number[] = [] 103 | 104 | let running = true 105 | let timer: NodeJS.Timeout | string | number | undefined 106 | 107 | if (signal) { 108 | if (signal.aborted) { 109 | running = false 110 | } 111 | 112 | signal.onabort = () => { 113 | running = false 114 | clearTimeout(timer) 115 | } 116 | } 117 | 118 | if (running) { 119 | setTimeout(() => { 120 | running = false 121 | }, maxTime) 122 | } 123 | 124 | while (running) { 125 | const clocked = await worker.call( 126 | { count, compiled: value }, 127 | timeout, 128 | ) 129 | 130 | if (clocked < minTime) { 131 | count <<= 1 132 | } else { 133 | const value = clocked / count 134 | sum += value 135 | samples.push(value) 136 | } 137 | } 138 | 139 | const size = samples.length 140 | 141 | const avg = sum / size 142 | const sem = calculatePopulationVariance(samples, avg) ** 0.5 / size 143 | 144 | const moe = sem * (T_TABLE[size - 1] ?? T_TABLE[T_TABLE.length - 1]) 145 | 146 | return { 147 | hz: 1000 / avg, 148 | rme: moe / avg, 149 | count, 150 | } 151 | } 152 | 153 | export const bench = async (options: Bench & Omit) => { 154 | return run({ ...options, value: await compile(options) }) 155 | } 156 | -------------------------------------------------------------------------------- /src/lib/benchmark/steadfast-worker.ts: -------------------------------------------------------------------------------- 1 | import Resolvable from 'resolvable-promise' 2 | 3 | export class SteadfastWorker extends EventTarget { 4 | private current!: Worker 5 | private id = 0 6 | 7 | constructor(public creator: () => Worker) { 8 | super() 9 | this.restart() 10 | } 11 | 12 | restart() { 13 | this.current?.terminate() 14 | 15 | this.current = this.creator() 16 | this.current.onmessage = (e) => { 17 | this.dispatchEvent(new MessageEvent('message', e as any)) 18 | } 19 | this.current.onerror = (e) => { 20 | this.dispatchEvent(new Event('error', e.error)) 21 | } 22 | } 23 | 24 | call(value: T, timeout: number = 10000) { 25 | const { id } = this 26 | const promise = new Resolvable() 27 | 28 | function handleMessage({ data: [rid, value] }: any) { 29 | if (rid !== id) return 30 | 31 | if (value instanceof Error) { 32 | promise.reject(value) 33 | } else { 34 | promise.resolve(value) 35 | } 36 | } 37 | 38 | const terminator = setTimeout(() => { 39 | this.restart() 40 | promise.reject(Error('Worker call time out')) 41 | }, timeout) 42 | 43 | this.addEventListener('message', handleMessage) 44 | this.current.postMessage([this.id++, value]) 45 | 46 | return promise.finally(() => { 47 | clearTimeout(terminator) 48 | 49 | this.removeEventListener('message', handleMessage) 50 | }) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/lib/benchmark/t-table.ts: -------------------------------------------------------------------------------- 1 | export const T_TABLE = [ 2 | 1270.65, 3 | 430.26, 4 | 318.24, 5 | 277.64, 6 | 257.06, 7 | 244.69, 8 | 236.46, 9 | 230.6, 10 | 226.21, 11 | 222.82, 12 | 220.1, 13 | 217.88, 14 | 216.04, 15 | 214.48, 16 | 213.14, 17 | 211.99, 18 | 210.98, 19 | 210.09, 20 | 209.3, 21 | 208.6, 22 | 207.96, 23 | 207.39, 24 | 206.86, 25 | 206.39, 26 | 205.96, 27 | 205.55, 28 | 205.18, 29 | 204.84, 30 | 204.52, 31 | 204.23, 32 | 196, 33 | ] 34 | -------------------------------------------------------------------------------- /src/lib/benchmark/utils.ts: -------------------------------------------------------------------------------- 1 | export function getTimerResolution() { 2 | let samples = [] 3 | let count = 32 4 | 5 | while (count--) { 6 | let diff 7 | let start = performance.now() 8 | 9 | do { 10 | diff = performance.now() - start 11 | } while (!diff) 12 | 13 | samples.push(diff) 14 | } 15 | 16 | return samples.reduce((sum, time) => sum + time) / samples.length 17 | } 18 | 19 | export const calculatePopulationVariance = (samples: number[], avg: number) => { 20 | let variance = 0 21 | 22 | for (const sample of samples) { 23 | const diff = sample - avg 24 | variance += diff * diff 25 | } 26 | 27 | return variance 28 | } 29 | -------------------------------------------------------------------------------- /src/lib/benchmark/worker.ts: -------------------------------------------------------------------------------- 1 | addEventListener('message', async ({ data: [id, value] }) => { 2 | try { 3 | let fn = Function('count', `return (${value.compiled})(count)`) 4 | 5 | postMessage([id, await fn(value.count)]) 6 | } catch (e) { 7 | postMessage([id, e]) 8 | } 9 | }) 10 | -------------------------------------------------------------------------------- /src/lib/mote.ts: -------------------------------------------------------------------------------- 1 | import { 2 | memo, 3 | useRef, 4 | useState, 5 | useEffect, 6 | useCallback, 7 | createElement, 8 | Dispatch, 9 | ReactNode, 10 | CSSProperties, 11 | FunctionComponent, 12 | HTMLAttributes, 13 | } from 'react' 14 | 15 | export function withMote>( 16 | comp: FunctionComponent, 17 | prefix?: string, 18 | ) { 19 | let connected = new Map() 20 | let forceUpdate: Dispatch = (x: any) => {} 21 | 22 | function Updater({ 23 | name, 24 | visible = true, 25 | ...newState 26 | }: T & { name: string; visible?: boolean }) { 27 | useEffect(() => { 28 | if (prefix) { 29 | name = `${prefix}_${name}` 30 | } 31 | 32 | let state = { 33 | visible, 34 | ...newState, 35 | } 36 | connected.set(name, state) 37 | forceUpdate({}) 38 | 39 | return () => { 40 | state.visible = false 41 | connected.delete(name) 42 | forceUpdate({}) 43 | } 44 | }) 45 | 46 | return null 47 | } 48 | 49 | Updater.displayName = `WithMote(${comp.name})` 50 | Updater.List = memo(() => { 51 | forceUpdate = useState()[1] 52 | 53 | return Array.from(connected.entries(), ([key, state]) => { 54 | return createElement(comp, { ...state, key }) 55 | }) as any 56 | }) 57 | 58 | return Updater 59 | } 60 | 61 | export function useMoteState({ 62 | visible, 63 | duration, 64 | exited, 65 | entered, 66 | exiting, 67 | entering, 68 | apear, 69 | unmount, 70 | }: StateHookProps) { 71 | let forceUpdate = useState()[1] 72 | 73 | let state = useRef({ 74 | timer: null as any, 75 | mounted: apear || !unmount || null, 76 | }).current 77 | 78 | if (visible) { 79 | state.mounted = true 80 | } 81 | 82 | let ref = useCallback( 83 | (node) => { 84 | if (node) { 85 | if (!apear) done() 86 | else enter() 87 | } 88 | 89 | function enter() { 90 | ;(visible ? entering : exiting)?.(node) 91 | 92 | clearTimeout(state.timer) 93 | state.timer = setTimeout(done, duration) 94 | } 95 | 96 | function done() { 97 | ;(visible ? entered : exited)?.(node) 98 | 99 | if (unmount && !visible) { 100 | state.mounted = null 101 | forceUpdate({}) 102 | } 103 | } 104 | }, 105 | [visible], 106 | ) 107 | 108 | return [ref, state.mounted] as [typeof ref, typeof state.mounted] 109 | } 110 | 111 | const raf = (cb: (...args: Array) => any) => 112 | requestAnimationFrame(() => requestAnimationFrame(cb)) 113 | 114 | export function useMote(options: HookProps) { 115 | let cn = useRef() 116 | 117 | function setCn(node: HTMLDivElement, state: State) { 118 | node.classList.remove(options[cn.current!]!) 119 | let current = options[(cn.current = state)] 120 | if (current) node.classList.add(current) 121 | options.callback?.(node, state) 122 | } 123 | 124 | return useMoteState({ 125 | ...options, 126 | entering(node) { 127 | setCn(node, 'enter') 128 | raf(() => setCn(node, 'enterActive')) 129 | }, 130 | exiting(node) { 131 | setCn(node, 'exit') 132 | raf(() => setCn(node, 'exitActive')) 133 | }, 134 | entered(node) { 135 | setCn(node, 'enterDone') 136 | }, 137 | exited(node) { 138 | setCn(node, 'exitDone') 139 | }, 140 | } as HookProps) 141 | } 142 | 143 | export function Mote({ 144 | children, 145 | className, 146 | onClick, 147 | style, 148 | delay = 0, 149 | easing, 150 | duration, 151 | ...options 152 | }: MoteProps) { 153 | let [ref, mounted] = useMote({ 154 | ...options, 155 | duration: delay + duration!, 156 | }) 157 | 158 | return ( 159 | mounted && 160 | createElement( 161 | 'div', 162 | { 163 | className, 164 | onClick, 165 | style: { 166 | transitionDuration: duration + 'ms', 167 | transitionTimingFunction: easing, 168 | transitionDelay: delay + 'ms', 169 | ...style, 170 | }, 171 | ref, 172 | }, 173 | children, 174 | ) 175 | ) 176 | } 177 | 178 | type State = 179 | | 'enter' 180 | | 'enterActive' 181 | | 'exit' 182 | | 'exitActive' 183 | | 'enterDone' 184 | | 'exitDone' 185 | type Callback = (node: HTMLDivElement) => void 186 | 187 | export interface StateHookProps { 188 | visible?: boolean 189 | duration?: number 190 | apear?: true 191 | unmount?: true 192 | exited?: Callback 193 | entered?: Callback 194 | exiting?: Callback 195 | entering?: Callback 196 | callback?(node: HTMLDivElement, state: State): void 197 | } 198 | 199 | export interface HookProps extends StateHookProps { 200 | enter?: string 201 | enterActive?: string 202 | enterDone?: string 203 | exit?: string 204 | exitActive?: string 205 | exitDone?: string 206 | } 207 | 208 | export interface MoteProps extends HookProps, HTMLAttributes { 209 | children: ReactNode 210 | delay?: number 211 | duration?: number 212 | easing?: string 213 | style?: CSSProperties & Record 214 | } 215 | -------------------------------------------------------------------------------- /src/lib/sotore/equal.ts: -------------------------------------------------------------------------------- 1 | export function equal(a: any, b: any) { 2 | if (Object.is(a, b)) { 3 | return true 4 | } 5 | 6 | if ( 7 | typeof a !== 'object' || 8 | typeof b !== 'object' || 9 | a === null || 10 | b === null 11 | ) { 12 | return false 13 | } 14 | 15 | const keysA = Object.keys(a) 16 | if (keysA.length !== Object.keys(b).length) { 17 | return false 18 | } 19 | 20 | for (let prop of keysA) { 21 | if (!Object.is(a[prop], b[prop])) { 22 | return false 23 | } 24 | } 25 | 26 | return true 27 | } 28 | -------------------------------------------------------------------------------- /src/lib/sotore/index.ts: -------------------------------------------------------------------------------- 1 | export type IState = Record 2 | 3 | export interface IListener { 4 | (newState: State, oldState: State): void 5 | } 6 | 7 | export interface Sotore { 8 | init(): State 9 | subscribe(listener: IListener): () => void 10 | get(): State 11 | lay(patch: Partial, action?: string): void 12 | set(newState: State | ((prevState: State) => State), action?: string): void 13 | } 14 | 15 | /** 16 | * 17 | * @param initial initail store state 18 | * @returns store instance 19 | */ 20 | export function sotore(initial: State) { 21 | const listeners = new Set>() 22 | let state = initial 23 | 24 | const store: Sotore = { 25 | init: () => initial, 26 | subscribe: (listener) => { 27 | listeners.add(listener) 28 | 29 | return () => listeners.delete(listener) 30 | }, 31 | get: () => state, 32 | lay: (patch, action) => store.set({ ...state, ...patch }, action), 33 | set: (newState) => { 34 | let prevState = state 35 | 36 | if (typeof newState === 'function') { 37 | state = (newState as (prevState: State) => State)(state) 38 | } else { 39 | state = newState 40 | } 41 | 42 | if (process.env.NODE_ENV === 'development' && state === prevState) { 43 | console.error('[sotore] State mutation is not allowed') 44 | } 45 | 46 | for (let handler of listeners) { 47 | handler(state, prevState) 48 | } 49 | }, 50 | } 51 | 52 | return store 53 | } 54 | 55 | export * from './equal' 56 | export * from './react' 57 | export * from './middleware' 58 | -------------------------------------------------------------------------------- /src/lib/sotore/middleware/assign.ts: -------------------------------------------------------------------------------- 1 | import { IState } from '..' 2 | 3 | export function assign( 4 | store: Store, 5 | handler: (store: Store) => Methods, 6 | ) { 7 | return Object.assign(store, handler(store)) 8 | } 9 | -------------------------------------------------------------------------------- /src/lib/sotore/middleware/compute.ts: -------------------------------------------------------------------------------- 1 | import { IState, Sotore } from '../' 2 | 3 | export function compute( 4 | store: Sotore & Methods, 5 | hook: (newState: State, action?: string) => State, 6 | ) { 7 | const { set } = store 8 | 9 | store.set = (newState, action) => { 10 | set((prevState) => { 11 | if (typeof newState === 'function') { 12 | newState = (newState as (prevState: State) => State)(prevState) 13 | } 14 | 15 | return hook(newState, action) 16 | }, action) 17 | } 18 | 19 | return store 20 | } 21 | -------------------------------------------------------------------------------- /src/lib/sotore/middleware/index.ts: -------------------------------------------------------------------------------- 1 | export * from './react' 2 | export * from './assign' 3 | export * from './compute' 4 | export * from './persistent' 5 | -------------------------------------------------------------------------------- /src/lib/sotore/middleware/persistent.ts: -------------------------------------------------------------------------------- 1 | import { IState, Sotore } from '../' 2 | 3 | function asIs(state: State) { 4 | return state 5 | } 6 | 7 | interface IConfig { 8 | name: string 9 | deserialize?: (state: JSONState) => State 10 | serailize?: (state: State) => JSONState 11 | storage?: Storage 12 | } 13 | 14 | export function persistent( 15 | store: Sotore, 16 | options: IConfig, 17 | ) { 18 | const { 19 | name, 20 | serailize = asIs, 21 | deserialize = asIs, 22 | storage = globalThis.localStorage, 23 | } = options 24 | 25 | // Init 26 | try { 27 | const dataString = storage.getItem(name) ?? '' 28 | 29 | store.set(deserialize(JSON.parse(dataString)), 'persistent') 30 | } catch (e) {} 31 | 32 | // Subscribe for updates 33 | store.subscribe(() => { 34 | const serailized = serailize(store.get()) 35 | storage.setItem(name, JSON.stringify(serailized)) 36 | }) 37 | 38 | return store 39 | } 40 | -------------------------------------------------------------------------------- /src/lib/sotore/middleware/react.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IMapper, 3 | useSelector, 4 | useFilter, 5 | Sotore, 6 | IState, 7 | ISelection, 8 | } from '../' 9 | 10 | export function reactHooks( 11 | store: Sotore & Methods, 12 | ) { 13 | return Object.assign(store, { 14 | useSelector: >( 15 | selector: IMapper, 16 | equalityFn?: (a: Selection, b: Selection) => boolean, 17 | ) => useSelector(store, selector, equalityFn), 18 | 19 | useFilter: (...props: Selection) => 20 | useFilter(store, ...props), 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /src/lib/sotore/react.ts: -------------------------------------------------------------------------------- 1 | import { useDebugValue, useMemo } from 'react' 2 | import { useSyncExternalStoreWithSelector } from 'use-sync-external-store/with-selector.js' 3 | 4 | import { equal } from './equal' 5 | import { Sotore, IState } from './index' 6 | 7 | export type ISelection = 8 | | Partial 9 | | State[keyof State] 10 | | (keyof State)[] 11 | | number 12 | | string 13 | | boolean 14 | 15 | export interface IMapper< 16 | Snapshot extends IState, 17 | Selection extends ISelection 18 | > { 19 | (newState: Snapshot): Selection 20 | } 21 | 22 | export type IMapTuple = { 23 | [K in keyof Tuple]: Tuple[K] extends keyof Object ? Object[Tuple[K]] : never 24 | } 25 | 26 | export function useSelector< 27 | Snapshot extends IState, 28 | Selection extends ISelection 29 | >( 30 | store: Sotore, 31 | selector: IMapper, 32 | equalityFn: (a: Selection, b: Selection) => boolean = equal, 33 | ) { 34 | const { get, subscribe } = store 35 | 36 | const slice = useSyncExternalStoreWithSelector( 37 | subscribe, 38 | get, 39 | get, 40 | selector, 41 | equalityFn, 42 | ) 43 | 44 | useDebugValue(slice) 45 | 46 | return slice 47 | } 48 | 49 | export function useFilter< 50 | Snapshot extends IState, 51 | Selection extends (keyof Snapshot)[] 52 | >(store: Sotore, ...props: Selection) { 53 | const { get, subscribe } = store 54 | 55 | const selector = useMemo(() => { 56 | const filter = props.join(',') 57 | 58 | return Function(`{${filter}}`, `return [${filter}]`) as IMapper< 59 | Snapshot, 60 | IMapTuple 61 | > 62 | }, props) 63 | 64 | const slice = useSyncExternalStoreWithSelector( 65 | subscribe, 66 | get, 67 | get, 68 | selector, 69 | equal, 70 | ) 71 | 72 | useDebugValue(slice) 73 | 74 | return slice 75 | } 76 | -------------------------------------------------------------------------------- /src/lib/tsCompiler.ts: -------------------------------------------------------------------------------- 1 | import { transform } from 'sucrase' 2 | 3 | export const compileTypeScript = (source: string) => { 4 | return transform(source, { 5 | transforms: ['typescript'], 6 | }).code 7 | } 8 | -------------------------------------------------------------------------------- /src/lib/useHash.ts: -------------------------------------------------------------------------------- 1 | import { useLayoutEffect } from 'react' 2 | 3 | export const useHash = (cb: (event?: HashChangeEvent) => void) => { 4 | useLayoutEffect(() => { 5 | cb() 6 | 7 | window.addEventListener('hashchange', cb) 8 | 9 | return () => { 10 | window.removeEventListener('hashchange', cb) 11 | } 12 | }, []) 13 | } 14 | -------------------------------------------------------------------------------- /src/lib/withId.tsx: -------------------------------------------------------------------------------- 1 | import { FC, memo } from 'react' 2 | 3 | type WithId = 4 | Prop extends (...args: infer A) => infer R 5 | ? (id: string, ...args: A) => R 6 | : Prop 7 | 8 | type OptionalPropertyNames = 9 | { [K in keyof T]-?: ({} extends { [P in K]: T[K] } ? K : never) }[keyof T]; 10 | 11 | 12 | type MapProps = { 13 | [K in keyof Props]: K extends `on${string}`? WithId : Props[K] 14 | } & { 15 | [K in keyof Props as OptionalPropertyNames extends K ? K : never]: K extends `on${string}`? WithId : never 16 | } & { 17 | id: string 18 | } 19 | 20 | export const withId =

(component: FC

) => { 21 | const Component = component as any 22 | 23 | return memo>((props: Record) => { 24 | const handlers: Record = {} 25 | 26 | for (const key in props) { 27 | const value = props[key] 28 | 29 | if (key.startsWith('on') && typeof value === 'function') { 30 | handlers[key] = (...args: any[]) => value(props.id, ...args) 31 | } 32 | } 33 | 34 | return 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /src/models/suite.ts: -------------------------------------------------------------------------------- 1 | import { bench } from 'lib/benchmark' 2 | import { sotore } from 'lib/sotore' 3 | import { compileTypeScript } from 'lib/tsCompiler' 4 | import { useHash } from 'lib/useHash' 5 | import { useEffect, useRef } from 'react' 6 | import { deserialize, serialize } from './suiteBase64' 7 | import { nanoid } from 'nanoid' 8 | 9 | export type Test = { 10 | id: string 11 | source: string 12 | hz?: number 13 | rme?: number 14 | abort?: () => void 15 | } 16 | 17 | export const suite = sotore({ 18 | title: 'Unnamed suite', 19 | author: 'Anonymous', 20 | running: false, 21 | setup: '// Setup\n\n', 22 | tests: [ 23 | { 24 | id: nanoid(), 25 | source: '// Test case\n\n', 26 | } as Test, 27 | ], 28 | teardown: '// Teardown\n\n', 29 | }) 30 | 31 | const { lay, get, set, subscribe } = suite 32 | 33 | export const updateSetup = (setup: string) => { 34 | lay({ setup }) 35 | } 36 | 37 | export const updateTearddown = (teardown: string) => { 38 | lay({ teardown }) 39 | } 40 | 41 | export const updateTestValue = (id: string, source: string) => { 42 | const { tests } = get() 43 | 44 | lay({ 45 | tests: tests.map((item) => { 46 | if (item.id !== id) { 47 | return item 48 | } 49 | 50 | return { 51 | ...item, 52 | hz: undefined, 53 | rme: undefined, 54 | source, 55 | } 56 | }), 57 | }) 58 | } 59 | 60 | export const updateTest = (id: string, patch: Partial) => { 61 | const { tests } = get() 62 | 63 | lay({ 64 | tests: tests.map((item) => { 65 | if (item.id !== id) { 66 | return item 67 | } 68 | 69 | return { 70 | ...item, 71 | hz: undefined, 72 | rme: undefined, 73 | ...patch, 74 | } 75 | }), 76 | }) 77 | } 78 | 79 | export const addTest = () => { 80 | const { tests } = get() 81 | 82 | lay({ 83 | tests: [ 84 | { 85 | id: nanoid(), 86 | source: '// Test case\n\n', 87 | }, 88 | ...tests, 89 | ], 90 | }) 91 | } 92 | 93 | export const removeTest = (id: string) => { 94 | const { tests } = get() 95 | 96 | lay({ 97 | tests: tests.filter((item) => item.id !== id), 98 | }) 99 | } 100 | 101 | export const stopTest = (id: string) => { 102 | const { tests } = get() 103 | const test = tests.find((test) => test.id === id) 104 | 105 | if (test?.abort) { 106 | test.abort() 107 | } 108 | } 109 | 110 | export const stopAllTests = () => { 111 | const { tests } = get() 112 | 113 | lay({ running: false }) 114 | 115 | for (const test of tests) { 116 | if (test.abort) { 117 | test.abort() 118 | } 119 | } 120 | } 121 | 122 | export const runTest = async (id: string) => { 123 | const { setup, tests, teardown } = get() 124 | const test = tests.find((test) => test.id === id) 125 | 126 | if (!test) { 127 | return 128 | } 129 | 130 | const controller = new AbortController() 131 | 132 | updateTest(id, { 133 | abort: () => controller.abort(), 134 | }) 135 | 136 | const { hz, rme } = await bench({ 137 | setup: compileTypeScript(setup), 138 | fn: compileTypeScript(test.source), 139 | teardown: compileTypeScript(teardown), 140 | signal: controller.signal, 141 | }).catch((e) => { 142 | return { 143 | hz: undefined, 144 | rme: undefined, 145 | } 146 | }) 147 | 148 | updateTest(id, { 149 | hz, 150 | rme, 151 | abort: undefined, 152 | }) 153 | } 154 | 155 | export const runAllTests = async () => { 156 | const { tests } = get() 157 | 158 | lay({ running: true }) 159 | 160 | for (const test of tests) { 161 | if (get().running) { 162 | await runTest(test.id) 163 | } 164 | } 165 | 166 | lay({ running: false }) 167 | } 168 | 169 | export const useHashSuite = () => { 170 | const changedRef = useRef() 171 | 172 | useHash(() => { 173 | const { hash } = location 174 | 175 | if (!changedRef.current) { 176 | const newState = deserialize(hash.slice(1)) 177 | if (newState) set(newState) 178 | } 179 | changedRef.current = false 180 | }) 181 | 182 | useEffect(() => { 183 | const setUrlFromState = () => { 184 | changedRef.current = true 185 | const url = new URL(location.toString()) 186 | url.hash = '#' + serialize(get()) 187 | location.replace(url) 188 | } 189 | 190 | setUrlFromState() 191 | 192 | return subscribe(setUrlFromState) 193 | }, []) 194 | } 195 | -------------------------------------------------------------------------------- /src/models/suiteBase64.ts: -------------------------------------------------------------------------------- 1 | import { suite } from 'model/suite' 2 | import { compressToURI, decompressFromURI } from 'lz-ts' 3 | import { nanoid } from 'nanoid' 4 | 5 | type StoreData = { 6 | title: string 7 | author: string 8 | before: string 9 | tests: string[] 10 | after: string 11 | } 12 | 13 | const isStringsArray = (arr: any): arr is string[] => { 14 | return Array.isArray(arr) && arr.every((test) => typeof test === 'string') 15 | } 16 | 17 | export const deserialize = ( 18 | content: string, 19 | ): ReturnType | null => { 20 | try { 21 | const data = JSON.parse(decompressFromURI(content)) 22 | 23 | if (typeof data !== 'object') { 24 | return null 25 | } 26 | 27 | const { title, author, before, tests, after } = data 28 | 29 | if (!isStringsArray([title, author, before, after])) { 30 | return null 31 | } 32 | 33 | if (!isStringsArray(tests)) { 34 | return null 35 | } 36 | 37 | return { 38 | title, 39 | author, 40 | running: false, 41 | setup: before, 42 | teardown: after, 43 | tests: tests.map((source) => { 44 | return { 45 | id: nanoid(), 46 | source, 47 | running: false, 48 | } 49 | }), 50 | } 51 | } catch (e) { 52 | return null 53 | } 54 | } 55 | 56 | export const serialize = (content: ReturnType) => { 57 | const { title, author, setup: before, tests, teardown: after } = content 58 | 59 | let data: StoreData = { 60 | title, 61 | author, 62 | before, 63 | after, 64 | tests: tests.map((test) => test.source), 65 | } 66 | 67 | return compressToURI(JSON.stringify(data)) 68 | } 69 | -------------------------------------------------------------------------------- /src/pages/Landing.tsx: -------------------------------------------------------------------------------- 1 | import { Layout, Footer, Header } from 'lay/Main' 2 | import { FC } from 'react' 3 | 4 | export const Landing: FC = () => { 5 | return ( 6 | 7 |

8 | Your
9 | benchmark
10 | tool. 11 |
12 | 13 |