├── .github └── workflows │ └── test.yml ├── .gitignore ├── .prettierignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── chrome.json ├── index.d.ts ├── index.js ├── package.json ├── scripts ├── clean-chrome-config.js └── cli.js ├── src ├── @types │ ├── browserOptions.ts │ ├── report.ts │ ├── resource.ts │ └── taskEvent.ts ├── create-chrome-trace.js ├── generate-html-file.js ├── lib.js ├── processor.js ├── reporter.js └── utils.js ├── temp └── chrome │ └── .gitkeep ├── tests ├── __mock__ │ ├── 13kb.js │ ├── 19kb.js │ ├── 7kb.js │ ├── rti-trace.json │ ├── test-launch.js │ └── test.html ├── estimo.test.js ├── generate-html-file.test.js ├── reporter.test.js └── utils.test.js └── yarn.lock /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | push: 4 | jobs: 5 | full: 6 | runs-on: ${{ matrix.os }} 7 | strategy: 8 | matrix: 9 | node: [18, 20] 10 | os: [macos-latest, ubuntu-latest, windows-latest] 11 | name: OS ${{ matrix.os }} Node.js ${{ matrix.node }} 12 | steps: 13 | - name: Install Chrome 14 | uses: browser-actions/setup-chrome@latest 15 | - name: Checkout the repository 16 | uses: actions/checkout@v2 17 | - name: Install Node.js ${{ matrix.node }} 18 | uses: actions/setup-node@v2 19 | with: 20 | node-version: ${{ matrix.node }} 21 | - name: Install dependencies 22 | run: yarn install --frozen-lockfile 23 | - name: Run unit tests 24 | run: yarn unit 25 | env: 26 | FORCE_COLOR: 2 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .idea 8 | .vscode 9 | 10 | # testing 11 | /coverage 12 | 13 | # misc 14 | .DS_Store 15 | .env.local 16 | .env.development.local 17 | .env.test.local 18 | .env.production.local 19 | .eslintcache 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | 24 | temp/*.html 25 | temp/*.json 26 | temp/chrome/* 27 | !temp/chrome/.gitkeep -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | tests/__mock__/*.js 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | cache: yarn 3 | 4 | matrix: 5 | include: 6 | - name: 'Chrome Stable, Node 12' 7 | node_js: '12' 8 | addons: 9 | chrome: stable 10 | - name: 'Chrome Stable, Node 14' 11 | node_js: '14' 12 | addons: 13 | chrome: stable 14 | - name: 'Without Chrome, Node 12' 15 | node_js: '12' 16 | - name: 'Without Chrome, Node 14' 17 | node_js: '14' 18 | - name: 'Without Chrome, Node 12, WindowsOS' 19 | env: 20 | - YARN_GPG=no 21 | os: windows 22 | node_js: '12' 23 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## 3.0.3 4 | 5 | - `tests` dir excluded from the module bundle. 6 | - Upgraded dependencies. 7 | 8 | ## 3.0.2 9 | 10 | - Upgraded dependencies. 11 | 12 | ## 3.0.1 13 | 14 | - Upgraded project dependencies. 15 | 16 | ## 3.0.0 17 | 18 | - Dropped support for node-12 and migrated to esm (min node version is 14) 19 | 20 | ## 2.3.6 21 | 22 | - Hotfix for `find-chrome-bin` while supporting node < 16 23 | 24 | ## 2.3.5 25 | 26 | - Upgraded project dependencies. 27 | 28 | - Fixed npm security issue. 29 | 30 | ## 2.3.4 31 | 32 | - Removed npm `install` script. From this version, `estimo` won't be looking for or downloading Chrome after npm install. It will be happening on the first launch. 33 | 34 | ## 2.3.3 35 | 36 | - Fixed npm security issues. 37 | 38 | - Upgraded project dependencies. 39 | 40 | ## 2.3.2 41 | 42 | - Fixed npm security issues. 43 | 44 | - Upgraded project dependencies. 45 | 46 | ## 2.3.1 47 | 48 | - Now using `find-chrome-bin` to find chromium binary. 49 | 50 | - Upgraded project dependencies. 51 | 52 | ## 2.3.0 53 | 54 | **Changed**: 55 | 56 | - Replaced yargs with commander which lead to removing 15 dependencies and reducing package size on 146kB. 57 | 58 | - Fixed bug with network emulation. 59 | 60 | - Simplified wording in documentation. 61 | 62 | ## 2.2.9 63 | 64 | **Changed**: 65 | 66 | - Fixed issue when chrome detection script fails on win10 (thanks to [@BePo65](https://github.com/BePo65)) 67 | 68 | - Fixed npm security issues. 69 | 70 | ## 2.2.8 71 | 72 | **Changed**: 73 | 74 | - Increased stability on slow CI's. 75 | 76 | - Dropped support for Node.js 10. 77 | 78 | - Fixed npm security issues. 79 | 80 | ## 2.2.7 81 | 82 | **Changed**: 83 | 84 | - Migrated on forked version of trace parcer with patches to avoid errors with navigationStart event. Which is increase stability with newest chromium releases (thanks @sitespeed.io). 85 | 86 | - Removed `LATEST_STABLE_CHROME_VERSION` and fixed `chromeDetection` script to work with newest chromium releases. 87 | 88 | ## 2.2.6 89 | 90 | **Changed**: 91 | 92 | - Upgraded `puppeteer-core` to v9.1.0 which fixes BrowserFetcher error on Mac M1. 93 | 94 | - Removed temporary error when running on Mac M1. 95 | 96 | ## 2.2.5 97 | 98 | **Changed**: 99 | 100 | - Added temporary error until puppeteer will fully support M1 Mac ([#6641](https://github.com/puppeteer/puppeteer/issues/6641)). 101 | 102 | - Prevented install-script from failure if some error appeared. 103 | 104 | ## 2.2.4 105 | 106 | **Changed**: 107 | 108 | - Handle `CHROMIUM_EXECUTABLE_PATH` env variable as a source of information to Chromium binary. 109 | 110 | - Upgraded project dependencies. 111 | 112 | ## 2.2.3 113 | 114 | **Changed**: 115 | 116 | - Now, it'll show an error when couldn't find Chrome executable path. 117 | 118 | - Upgraded project dependencies. 119 | 120 | ## 2.2.2 121 | 122 | **Changed**: 123 | 124 | - Fix npm security issue. 125 | 126 | ## 2.2.1 127 | 128 | **Changed**: 129 | 130 | - Fix npm security issue. 131 | 132 | ## 2.2.0 133 | 134 | **Added**: 135 | 136 | - `diff` - option which enable [`Diff Mode`](https://github.com/mbalabash/estimo#diff-mode). 137 | 138 | It will be useful for you if you want to understand how performance metrics are changed between a few versions of js libraries. 139 | 140 | ## 2.1.2 141 | 142 | **Changed**: 143 | 144 | - Updated project dependencies. 145 | 146 | ## 2.1.1 147 | 148 | **Changed**: 149 | 150 | - Updated project docs. 151 | 152 | ## 2.1.0 153 | 154 | **Added**: 155 | 156 | - `runs` - option which you can use to run estimo N times and get median value as a result. 157 | 158 | **Changed**: 159 | 160 | - Fixed broken types. 161 | 162 | - Updated dependencies. 163 | 164 | - Removed debug logging. 165 | 166 | - Removed useless tests. 167 | 168 | - Fixed unhandled exceptions. 169 | 170 | ## 2.0.4 171 | 172 | - Fix estimo types. 173 | 174 | - Remove `process.exit` for plugable use cases. 175 | 176 | - Get `MIN_CHROME_VERSION` from environment or use predefined version. 177 | 178 | ## 2.0.3 179 | 180 | - Fix npm security issue. 181 | 182 | ## 2.0.2 183 | 184 | - Add temporary fix for Chrome 80 revision. 185 | 186 | - Upgrade `puppeteer-core` to **2.1.0**. 187 | 188 | - Enhance NODE_ENV `ESTIMO_DEBUG` output. 189 | 190 | - Don't remove temp files in `ESTIMO_DEBUG` mode. 191 | 192 | - Add Chrome revision info on npm install hook. 193 | 194 | - Style refactoring. 195 | 196 | - Update tests. 197 | 198 | ## 2.0.1 199 | 200 | - Use `PUPPETEER_EXECUTABLE_PATH` to find chrome execute path if variable available. 201 | 202 | ## 2.0.0 203 | 204 | - Add page-mode for processing web pages by url. 205 | - Change processing logic and split it apart for js files and web pages. 206 | - Add check for inexistent local js files. 207 | - Add `device` option for chrome device emulation. 208 | - Add `width`, `height` options for custom viewport emulation. 209 | - Add `userAgent` option for custom userAgent emulation. 210 | - Add `IncognitoBrowserContext` support for better performance. 211 | - Change `-l` argument to `-r` (**CLI API**). 212 | - Change `library` field in result output to `name` (**JS API**). 213 | - Add debug log via `ESTIMO_DEBUG=true`. 214 | - Add `TypeScript` typings for better DX. 215 | - Update tests. 216 | - Update project documentation. 217 | 218 | ## 1.1.6 219 | 220 | - Fix path resolving to chrome binary when using npx. 221 | 222 | ## 1.1.5 223 | 224 | - Change chrome detection script for not executing if `ESTIMO_DISABLE=true`. 225 | 226 | ## 1.1.4 227 | 228 | - Fix error on windows. 229 | - Fix Travis CI error (`Build terminated after build exited successfully`). 230 | - Update tests. 231 | 232 | ## 1.1.3 233 | 234 | - Fix chrome version check. 235 | - Remove debug code. 236 | 237 | ## 1.1.1 238 | 239 | - Fix security issue with npm packages. 240 | 241 | ## 1.1.0 242 | 243 | - Change Travis CI config for launching test with and without suitable chrome. 244 | - Fix `FP`, `FCP`, `FMP`, `LHError` errors. 245 | - Fix memory error on CI (`'--no-sandbox', '--disable-setuid-sandbox'`). 246 | - Add support for env variables from puppeteer. 247 | - Add script for local chrome detection. 248 | - Drop `puppeteer` support and use `puppeteer-core` instead. 249 | - Add multiple files processing. 250 | - Update project documentation. 251 | - Add usage examples. 252 | 253 | ## 1.0.0 254 | 255 | - Drop `perf-timeline` support and use `puppeteer` instead. 256 | - Drop `bigrig` support and use `tracium` instead. 257 | - Change js/cli api. 258 | - Update project documentation. 259 | 260 | ## 0.1.9 261 | 262 | - Fix path resolving to `perf-timeline` binary. 263 | 264 | ## 0.1.8 265 | 266 | - Update project documentation. 267 | - Code-style refactoring. 268 | 269 | ## 0.1.7 270 | 271 | - Replace `node-npx` to `cross-spawn`. 272 | 273 | ## 0.1.6 274 | 275 | - Run `perf-timeline` via `node-npx`. 276 | - Add Travis CI. 277 | 278 | ## 0.1.5 279 | 280 | - Show processing time when using CLI. 281 | - Add fields description for `estimo` result. 282 | - Add documentation for Network/CPU Emulation. 283 | 284 | ## 0.1.4 285 | 286 | - Fix file path resolving when using CLI. 287 | 288 | ## 0.1.3 289 | 290 | - Initial release. 291 | 292 | ## 0.1 293 | 294 | - PoC implementation. 295 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Maksim Balabash 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Estimo 2 | 3 | Estimo is a tool for measuring parse / compile / execution time of javascript. 4 | 5 | This tool can emulate CPU throttling, Network conditions, use Chrome Device emulation and more for measuring javascript performance. 6 | 7 | _Inspired by [Size Limit](https://github.com/ai/size-limit). Thanks [@ai](https://github.com/ai/) and [@aslushnikov](https://github.com/aslushnikov) for support._ 8 | 9 | ## Why? 10 | 11 | **JavaScript** is the **most expensive part** of our frontend. 12 | 13 | ![3.5 seconds to process 170 KB of JS and 0.1 second for 170 KB of JPEG. @Addy Osmani](https://cdn.evilmartians.com/front/posts/storeon-redux-in-173-bytes/js_vs_jpeg-85dbf5c.png) 14 | 15 | **3.5 seconds** to process 170 KB of JS and **0.1 second** for 170 KB of JPEG. 16 | 17 | ## Usage 18 | 19 | **JS API** 20 | 21 | ```js 22 | const path = require('path') 23 | const estimo = require('estimo') 24 | 25 | ;(async () => { 26 | const report = await estimo(path.join(__dirname, 'examples', 'example.js')) 27 | console.log(report) 28 | })() 29 | ``` 30 | 31 | **CLI** 32 | 33 | ```sh 34 | estimo -r ./examples/example.js 35 | ``` 36 | 37 | **Output** 38 | 39 | ```js 40 | ;[ 41 | { 42 | name: 'example.js', 43 | parseHTML: 1.01, 44 | styleLayout: 34.08, 45 | paintCompositeRender: 1.4, 46 | scriptParseCompile: 1.04, 47 | scriptEvaluation: 8.11, 48 | javaScript: 9.15, 49 | garbageCollection: 0, 50 | other: 8.2, 51 | total: 53.84 52 | } 53 | ] 54 | ``` 55 | 56 | ## Fields Description 57 | 58 | - **name** - Resource name (file name or web url). 59 | 60 | - **parseHTML** - Time which was spent for `ParseHTML`, `ParseAuthorStyleSheet` events. 61 | 62 | - **styleLayout** - Time which was spent for `ScheduleStyleRecalculation`, `UpdateLayoutTree`, `InvalidateLayout`, `Layout` events. 63 | 64 | - **paintCompositeRender** - Time which was spent for `Animation`, `HitTest`, `PaintSetup`, `Paint`, `PaintImage`, `RasterTask`, `ScrollLayer`, `UpdateLayer`, `UpdateLayerTree`, `CompositeLayers` events. 65 | 66 | - **scriptParseCompile** - Time which was spent for `v8.compile`, `v8.compileModule`, `v8.parseOnBackground` events. 67 | 68 | - **scriptEvaluation** - Time which was spent for `EventDispatch`, `EvaluateScript`, `v8.evaluateModule`, `FunctionCall`, `TimerFire`, `FireIdleCallback`, `FireAnimationFrame`, `RunMicrotasks`, `V8.Execute` events. 69 | 70 | - **javaScript** - Time which was spent for both event groups (**scriptParseCompile** and **scriptEvaluation**). 71 | 72 | - **garbageCollection** - Time which was spent for `MinorGC`, `MajorGC`, `BlinkGC.AtomicPhase`, `ThreadState::performIdleLazySweep`, `ThreadState::completeSweep`, `BlinkGCMarking` events. 73 | 74 | - **other** - Time which was spent for `MessageLoop::RunTask`, `TaskQueueManager::ProcessTaskFromWorkQueue`, `ThreadControllerImpl::DoWork` events. 75 | 76 | - **total** - Total time. 77 | 78 | ## Time 79 | 80 | **All metrics are in milliseconds**. 81 | 82 | We measure system-cpu time. The number of seconds that the process has spent on the CPU. 83 | 84 | We not including time spent waiting for its turn on the CPU. 85 | 86 | ## Multiple Runs 87 | 88 | All results measured in time. It means that results could be unstable depends on available on your device resources. 89 | 90 | You can use `runs` option to run estimo N times and get median value as a result. 91 | 92 | **JS API** 93 | 94 | ```js 95 | const report = await estimo(['/path/to/examples/example.js'], { runs: 10 }) 96 | ``` 97 | 98 | **CLI** 99 | 100 | ```sh 101 | estimo -r /path/to/examples/example.js -runs 10 102 | ``` 103 | 104 | ## Diff Mode 105 | 106 | You can compare metrics of an origin file with others its versions to understand consequences on performance. 107 | 108 | We take the first file as a baseline. All rest files will be compared with the baseline. 109 | 110 | **JS API** 111 | 112 | ```js 113 | const report = await estimo(['lib-1.0.5.js', 'lib-1.1.0.js'], { diff: true }) 114 | ``` 115 | 116 | **CLI** 117 | 118 | ```sh 119 | estimo -r lib-1.0.5.js lib-1.1.0.js -diff 120 | ``` 121 | 122 | **Output** 123 | 124 | ```js 125 | ;[ 126 | { 127 | name: 'lib-1.0.5.js', 128 | parseHTML: 1.48, 129 | styleLayout: 44.61, 130 | paintCompositeRender: 2.19, 131 | scriptParseCompile: 1.21, 132 | scriptEvaluation: 9.63, 133 | javaScript: 10.84, 134 | garbageCollection: 0, 135 | other: 9.95, 136 | total: 69.06 137 | }, 138 | { 139 | name: 'lib-1.1.0.js', 140 | parseHTML: 2.97, 141 | styleLayout: 61.02, 142 | paintCompositeRender: 2.11, 143 | scriptParseCompile: 2.11, 144 | scriptEvaluation: 19.28, 145 | javaScript: 21.39, 146 | garbageCollection: 0, 147 | other: 15.49, 148 | total: 102.98, 149 | diff: { 150 | parseHTML: '2.97 (+50.17% 🔺)', 151 | styleLayout: '61.02 (+26.9% 🔺)', 152 | paintCompositeRender: '2.11 (-3.8% 🔽)', 153 | scriptParseCompile: '2.11 (+42.66% 🔺)', 154 | scriptEvaluation: '19.28 (+50.06% 🔺)', 155 | javaScript: '21.39 (+49.33% 🔺)', 156 | garbageCollection: '0 (N/A)', 157 | other: '15.49 (+35.77% 🔺)', 158 | total: '102.98 (+32.94% 🔺)' 159 | } 160 | } 161 | ] 162 | ``` 163 | 164 | ## Additional Use Cases 165 | 166 | ### CPU Throttling Rate 167 | 168 | The CPU Throttling Rate Emulation allows you to simulate CPU performance. 169 | 170 | - **cpuThrottlingRate** (default: `1`) - Sets the CPU throttling rate. The number represents the slowdown factor (e.g., 2 is a "2x" slowdown). 171 | 172 | **JS API**: 173 | 174 | ```js 175 | await estimo('/path/to/example.js', { cpuThrottlingRate: 4 }) 176 | ``` 177 | 178 | **CLI**: 179 | 180 | ```sh 181 | estimo -r ./examples/example.js -cpu 4 182 | ``` 183 | 184 | ### Network Emulation 185 | 186 | The Network Emulation allows you to simulate a specified network conditions. 187 | 188 | - **emulateNetworkConditions** (default: `undefined`) - One of [puppeteer network conditions descriptor](https://pptr.dev/#?product=Puppeteer&version=v11.0.0&show=api-puppeteernetworkconditions). 189 | 190 | **JS API**: 191 | 192 | ```js 193 | await estimo('/path/to/example.js', { emulateNetworkConditions: 'Slow 3G' }) 194 | ``` 195 | 196 | **CLI**: 197 | 198 | ```sh 199 | estimo -r ./examples/example.js -net Slow\ 3G 200 | ``` 201 | 202 | ### Chrome Device Emulation 203 | 204 | The Chrome Device Emulation allow you to simulate a specified device conditions. 205 | 206 | - **device** (default: `undefined`) - One of [puppeteer devices descriptor](https://pptr.dev/#?product=Puppeteer&version=v11.0.0&show=api-puppeteerdevices). 207 | 208 | **JS API** 209 | 210 | ```js 211 | const report = await estimo('/path/to/example.js', { device: 'Galaxy S5' }) 212 | ``` 213 | 214 | **CLI** 215 | 216 | ```sh 217 | estimo -r ./examples/examples.js -device Galaxy\ S5 218 | ``` 219 | 220 | When using CLI, for device names with spaces you should use symbols escaping. 221 | 222 | ### Changing default timeout 223 | 224 | You can specify how long estimo should wait for page to load. 225 | 226 | - **timeout** (default: `20000`) - Sets timeout in ms. 227 | 228 | **JS API**: 229 | 230 | ```js 231 | await estimo('/path/to/example.js', { timeout: 90000 }) 232 | ``` 233 | 234 | **CLI**: 235 | 236 | ```sh 237 | estimo -r ./examples/example.js -timeout 90000 238 | ``` 239 | 240 | ### Multiple Resources 241 | 242 | **JS API** 243 | 244 | ```js 245 | const report = await estimo([ 246 | '/path/to/libs/example.js', 247 | '/path/to/another/example/lib.js' 248 | ]) 249 | ``` 250 | 251 | **CLI** 252 | 253 | ```sh 254 | estimo -r /path/to/example.js https://unpkg.com/react@16/umd/react.development.js 255 | ``` 256 | 257 | ### Pages 258 | 259 | You can use all features not only with js files, but with web pages too. 260 | 261 | We will wait for navigation to be finished when the `load` event is fired. 262 | 263 | **JS API** 264 | 265 | ```js 266 | const report = await estimo('https://www.google.com/') 267 | ``` 268 | 269 | **CLI** 270 | 271 | ```sh 272 | estimo -r https://www.google.com/ 273 | ``` 274 | 275 | ## Install 276 | 277 | ```js 278 | npm i estimo 279 | ``` 280 | 281 | or 282 | 283 | ```js 284 | yarn add estimo 285 | ``` 286 | 287 | ## How? 288 | 289 | It uses [puppeteer](https://github.com/GoogleChrome/puppeteer) to generate Chrome Timelines. Which can be transformed in human-readable shape by [Tracium](https://github.com/aslushnikov/tracium). 290 | 291 | We will use your local **Chrome** if it suitable for using with Estimo. 292 | 293 | **Keep in mind** there result depends on your device and available resources. 294 | 295 | ## Who Uses Estimo 296 | 297 | - [Size Limit](https://github.com/ai/size-limit) 298 | 299 | ## Contributing 300 | 301 | Pull requests, feature ideas and bug reports are very welcome. We highly appreciate any feedback. 302 | -------------------------------------------------------------------------------- /chrome.json: -------------------------------------------------------------------------------- 1 | { "executablePath": "", "browser": "" } -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import { BrowserOptions } from './src/@types/browserOptions' 2 | import { Report } from './src/@types/report' 3 | 4 | export declare function estimo(resources: string[], browserOptions: BrowserOptions): Report[] 5 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import estimo from './src/lib.js' 2 | 3 | export { processor } from './src/processor.js' 4 | export { generatePrettyReport } from './src/reporter.js' 5 | 6 | export default estimo 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "estimo", 3 | "version": "3.0.3", 4 | "description": "Evaluates how long the browser will execute your javascript code", 5 | "main": "index.js", 6 | "types": "./index.d.ts", 7 | "type": "module", 8 | "engines": { 9 | "node": ">=18" 10 | }, 11 | "license": "MIT", 12 | "author": "mbalabash ", 13 | "scripts": { 14 | "unit": "ava --timeout=4m --exit", 15 | "test": "eslint . && yarn unit", 16 | "clean": "rimraf ./temp/*.{html,json} && node ./scripts/clean-chrome-config.js" 17 | }, 18 | "bin": { 19 | "estimo": "./scripts/cli.js" 20 | }, 21 | "dependencies": { 22 | "@sitespeed.io/tracium": "^0.3.3", 23 | "commander": "^12.0.0", 24 | "find-chrome-bin": "2.0.2", 25 | "nanoid": "5.0.7", 26 | "puppeteer-core": "22.6.5" 27 | }, 28 | "devDependencies": { 29 | "@logux/eslint-config": "^52.0.2", 30 | "ava": "6.1.2", 31 | "eslint": "^8.57.0", 32 | "eslint-config-standard": "^17.1.0", 33 | "eslint-plugin-import": "^2.29.1", 34 | "eslint-plugin-n": "^16.6.2", 35 | "eslint-plugin-node": "^11.1.0", 36 | "eslint-plugin-node-import": "^1.0.4", 37 | "eslint-plugin-perfectionist": "^2.9.0", 38 | "eslint-plugin-prefer-let": "^3.0.1", 39 | "eslint-plugin-promise": "^6.1.1", 40 | "prettier": "^3.2.5", 41 | "rimraf": "^5.0.5", 42 | "typescript": "^5.4.5" 43 | }, 44 | "files": [ 45 | "scripts/**", 46 | "temp/chrome/.gitkeep", 47 | "CHANGELOG.md", 48 | "chrome.json", 49 | "index.d.ts", 50 | "index.js", 51 | "README.md", 52 | "src/**" 53 | ], 54 | "eslintConfig": { 55 | "extends": "@logux/eslint-config", 56 | "rules": { 57 | "no-console": "off" 58 | } 59 | }, 60 | "eslintIgnore": [ 61 | "tests/__mock__/*.js", 62 | "README.md" 63 | ], 64 | "ava": { 65 | "concurrency": 1, 66 | "files": [ 67 | "tests/**/*.js", 68 | "!tests/__mock__/**" 69 | ] 70 | }, 71 | "prettier": { 72 | "arrowParens": "avoid", 73 | "quoteProps": "as-needed", 74 | "semi": false, 75 | "singleQuote": true, 76 | "trailingComma": "none" 77 | }, 78 | "repository": { 79 | "type": "git", 80 | "url": "https://github.com/mbalabash/estimo.git" 81 | }, 82 | "homepage": "https://github.com/mbalabash/estimo#readme", 83 | "bugs": { 84 | "url": "https://github.com/mbalabash/estimo/issues" 85 | }, 86 | "preferGlobal": true, 87 | "keywords": [ 88 | "chrome", 89 | "tracium", 90 | "puppeteer", 91 | "devtools", 92 | "size-limit", 93 | "performance" 94 | ] 95 | } 96 | -------------------------------------------------------------------------------- /scripts/clean-chrome-config.js: -------------------------------------------------------------------------------- 1 | import { dirname, join } from 'node:path' 2 | import { fileURLToPath } from 'node:url' 3 | 4 | import { writeFile } from '../src/utils.js' 5 | 6 | const __dirname = dirname(fileURLToPath(import.meta.url)) 7 | const chromeConfigPath = join(__dirname, '..', 'chrome.json') 8 | 9 | export async function cleanChromeConfig() { 10 | await writeFile(chromeConfigPath, '{ "executablePath": "", "browser": "" }') 11 | } 12 | 13 | cleanChromeConfig() 14 | -------------------------------------------------------------------------------- /scripts/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { Command } from 'commander' 3 | import { resolve } from 'node:path' 4 | 5 | import estimo from '../index.js' 6 | import { isUrl } from '../src/utils.js' 7 | 8 | const program = new Command() 9 | program 10 | .requiredOption( 11 | '-r, --resources ', 12 | 'javascript files and/or web pages' 13 | ) 14 | .option( 15 | '-device ', 16 | 'puppeteer device descriptor to enable device emulation' 17 | ) 18 | .option('-cpu ', 'slowdown factor to enable cpu throttling') 19 | .option( 20 | '-net ', 21 | 'puppeteer network conditions descriptor to enable network emulation' 22 | ) 23 | .option('-runs ', 'sets how many times estimo will run') 24 | .option( 25 | '-timeout ', 26 | 'sets how long estimo will wait for page load (ms)' 27 | ) 28 | .option('-diff', 'compare metrics of a first resource against others') 29 | 30 | program.parse(process.argv) 31 | 32 | const options = program.opts() 33 | const settings = { 34 | cpuThrottlingRate: options.Cpu ? parseInt(options.Cpu, 10) : 1, 35 | device: options.Device || false, 36 | diff: options.Diff || false, 37 | emulateNetworkConditions: options.Net, 38 | runs: options.Runs ? parseInt(options.Runs, 10) : 1, 39 | timeout: options.Timeout || 20000 40 | } 41 | 42 | ;(async () => { 43 | let resources = options.resources.map(lib => 44 | isUrl(lib) ? lib : resolve(lib) 45 | ) 46 | let report = null 47 | 48 | try { 49 | let startTime = Date.now() 50 | report = await estimo(resources, settings) 51 | let finishTime = Date.now() 52 | 53 | console.log(report) 54 | console.log(`Done in ${parseInt(finishTime - startTime, 10)} ms.`) 55 | } catch (error) { 56 | console.error(error) 57 | process.exit(1) 58 | } 59 | 60 | return report 61 | })() 62 | -------------------------------------------------------------------------------- /src/@types/browserOptions.ts: -------------------------------------------------------------------------------- 1 | export interface BrowserOptions { 2 | width?: number 3 | height?: number 4 | userAgent?: string 5 | device?: string 6 | 7 | emulateNetworkConditions?: string 8 | emulateCpuThrottling?: number 9 | 10 | headless?: boolean 11 | timeout?: number 12 | executablePath?: string 13 | 14 | runs?: number 15 | diff?: boolean 16 | } 17 | -------------------------------------------------------------------------------- /src/@types/report.ts: -------------------------------------------------------------------------------- 1 | export interface Report { 2 | name: string 3 | parseHTML: number 4 | styleLayout: number 5 | paintCompositeRender: number 6 | scriptParseCompile: number 7 | scriptEvaluation: number 8 | javaScript: number 9 | garbageCollection: number 10 | other: number 11 | total: number 12 | 13 | diff?: { 14 | parseHTML: string 15 | styleLayout: string 16 | paintCompositeRender: string 17 | scriptParseCompile: string 18 | scriptEvaluation: string 19 | javaScript: string 20 | garbageCollection: string 21 | other: string 22 | total: string 23 | } 24 | } -------------------------------------------------------------------------------- /src/@types/resource.ts: -------------------------------------------------------------------------------- 1 | export interface Resource { 2 | name: string 3 | tracePath: string 4 | htmlPath?: string 5 | url?: string 6 | } 7 | -------------------------------------------------------------------------------- /src/@types/taskEvent.ts: -------------------------------------------------------------------------------- 1 | export interface TaskEvent { 2 | kind: 3 | 'parseHTML' | 4 | 'styleLayout' | 5 | 'paintCompositeRender' | 6 | 'scriptParseCompile' | 7 | 'scriptEvaluation' | 8 | 'garbageCollection' | 9 | 'other' 10 | /** 11 | * Monotonic start time in milliseconds 12 | */ 13 | startTime: number 14 | /** 15 | * Monotonic end time in milliseconds 16 | */ 17 | endTime: number 18 | /** 19 | * Task duration in milliseconds, a.k.a. "wall time" 20 | */ 21 | duration: number 22 | /** 23 | * Time spent in the task at the current level of the task tree 24 | */ 25 | selfTime: number 26 | /** 27 | * Original trace event object associated with the task 28 | */ 29 | event: object 30 | /** 31 | * An array of child tasks 32 | */ 33 | children: TaskEvent[] 34 | /** 35 | * A parent task if anya parent task if any 36 | */ 37 | parent?: TaskEvent 38 | } 39 | -------------------------------------------------------------------------------- /src/create-chrome-trace.js: -------------------------------------------------------------------------------- 1 | import { nanoid } from 'nanoid' 2 | import puppeteer from 'puppeteer-core' 3 | 4 | import { resolvePathToTempDir } from './utils.js' 5 | 6 | const defaultBrowserOptions = { 7 | headless: true, 8 | timeout: 20000 9 | } 10 | 11 | const chromeLaunchArgs = [ 12 | '--no-sandbox', 13 | '--incognito', 14 | '--disable-setuid-sandbox', 15 | '--disable-dev-shm-usage' 16 | ] 17 | 18 | async function createBrowserEntity(options) { 19 | if (options.executablePath.length === 0) { 20 | throw new Error( 21 | `Chromium revision is not found or downloaded. Check that access to file system is permitted or file an issue here: https://github.com/mbalabash/estimo` 22 | ) 23 | } 24 | 25 | if (options.width && options.height) { 26 | chromeLaunchArgs.push(`--window-size=${options.width},${options.height}`) 27 | } 28 | let browserConfig = { 29 | args: chromeLaunchArgs, 30 | executablePath: options.executablePath, 31 | headless: options.headless, 32 | ignoreDefaultArgs: ['--disable-extensions'] 33 | } 34 | if (process.env.ESTIMO_DEBUG) { 35 | browserConfig.dumpio = true 36 | } 37 | 38 | return await puppeteer.launch(browserConfig) 39 | } 40 | 41 | async function createPageEntity(context, options) { 42 | let page = await context.newPage() 43 | 44 | if (options.emulateNetworkConditions) { 45 | await page.emulateNetworkConditions( 46 | puppeteer.networkConditions[options.emulateNetworkConditions] 47 | ) 48 | } 49 | if (options.cpuThrottlingRate) { 50 | await page.emulateCPUThrottling(options.cpuThrottlingRate) 51 | } 52 | if (options.userAgent) { 53 | await page.setUserAgent(options.userAgent) 54 | } 55 | if (options.width && options.height) { 56 | await page.setViewport({ 57 | height: options.height, 58 | width: options.width 59 | }) 60 | } 61 | if (options.device) { 62 | if (puppeteer.devices[options.device]) { 63 | await page.emulate(puppeteer.devices[options.device]) 64 | } else { 65 | throw new Error(`${options.device} - unknown Device option!`) 66 | } 67 | } 68 | 69 | page.on('error', msg => { 70 | throw msg 71 | }) 72 | 73 | return page 74 | } 75 | 76 | export async function createChromeTrace(resources, browserOptions) { 77 | let options = { ...defaultBrowserOptions, ...browserOptions } 78 | let resourcesWithTrace = [] 79 | let browser 80 | let context 81 | let page 82 | 83 | try { 84 | browser = await createBrowserEntity(options) 85 | context = await browser.createBrowserContext() 86 | 87 | for (let item of resources) { 88 | page = await createPageEntity(context, options) 89 | 90 | let traceFile = resolvePathToTempDir(`${nanoid()}.json`) 91 | 92 | await page.tracing.start({ path: traceFile }) 93 | await page.goto(item.url, { timeout: options.timeout }) 94 | await page.tracing.stop() 95 | await page.close() 96 | 97 | resourcesWithTrace.push({ ...item, tracePath: traceFile }) 98 | } 99 | } catch (error) { 100 | console.error(error) 101 | } finally { 102 | if (browser) { 103 | let pages = await browser.pages() 104 | await Promise.all(pages.map(item => item.close())) 105 | 106 | if (context) { 107 | await context.close() 108 | } 109 | 110 | await browser.close() 111 | } 112 | } 113 | 114 | return resourcesWithTrace 115 | } 116 | -------------------------------------------------------------------------------- /src/generate-html-file.js: -------------------------------------------------------------------------------- 1 | import { nanoid } from 'nanoid' 2 | 3 | import { 4 | existsAsync, 5 | getLibraryName, 6 | getUrlToHtmlFile, 7 | isUrl, 8 | resolvePathToTempDir, 9 | writeFile 10 | } from './utils.js' 11 | 12 | export function createHtmlContent(library) { 13 | return ` 14 | 15 | 16 | 17 | 18 | 19 | Estimo Template 20 | 21 | 22 | ${``} 23 |

Estimo

24 | 25 | ` 26 | } 27 | 28 | export async function generateHtmlFile(htmlContent) { 29 | let fileName 30 | 31 | try { 32 | fileName = resolvePathToTempDir(`${nanoid()}.html`) 33 | await writeFile(fileName, htmlContent) 34 | } catch (error) { 35 | console.error(error) 36 | } 37 | 38 | return fileName 39 | } 40 | 41 | export async function prepareLibrariesForEstimation(libraries) { 42 | let resources = [] 43 | 44 | for (let lib of libraries) { 45 | let isFileExist = await existsAsync(lib) 46 | if (!isUrl(lib) && !isFileExist) { 47 | throw new Error(`${lib} - file isn't exist!`) 48 | } 49 | 50 | let htmlContent = createHtmlContent(lib) 51 | let html = await generateHtmlFile(htmlContent) 52 | 53 | let name = getLibraryName(lib) 54 | let url = getUrlToHtmlFile(html) 55 | resources.push({ htmlPath: html, name, url }) 56 | } 57 | 58 | return resources 59 | } 60 | -------------------------------------------------------------------------------- /src/lib.js: -------------------------------------------------------------------------------- 1 | import { processor } from './processor.js' 2 | import { 3 | checkEstimoArgs, 4 | findChromeBinary, 5 | splitResourcesForEstimo 6 | } from './utils.js' 7 | 8 | export async function estimo(resources = [], browserOptions = {}) { 9 | if (process.env.ESTIMO_DISABLE) process.exit() 10 | checkEstimoArgs(resources, browserOptions) 11 | let reports = [] 12 | 13 | try { 14 | let { executablePath } = await findChromeBinary() 15 | browserOptions.executablePath = executablePath 16 | 17 | let { libraries, pages } = splitResourcesForEstimo(resources) 18 | 19 | if (libraries.length > 0) { 20 | reports = reports.concat( 21 | await processor(libraries, browserOptions, 'js-mode') 22 | ) 23 | } 24 | 25 | if (pages.length > 0) { 26 | reports = reports.concat( 27 | await processor(pages, browserOptions, 'page-mode') 28 | ) 29 | } 30 | } catch (error) { 31 | console.error(error) 32 | console.log( 33 | 'Please, file an issues related to estimo here: https://github.com/mbalabash/estimo' 34 | ) 35 | } 36 | 37 | return reports 38 | } 39 | 40 | export default estimo 41 | -------------------------------------------------------------------------------- /src/processor.js: -------------------------------------------------------------------------------- 1 | import { createChromeTrace } from './create-chrome-trace.js' 2 | import { prepareLibrariesForEstimation } from './generate-html-file.js' 3 | import { generatePrettyReport } from './reporter.js' 4 | import { 5 | createDiff, 6 | estimoMedianExecutor, 7 | median, 8 | removeAllFiles 9 | } from './utils.js' 10 | 11 | async function reportsProcessor(reports, browserOptions) { 12 | let runs = browserOptions.runs || 1 13 | let diffMode = browserOptions.diff || false 14 | let result = [] 15 | 16 | try { 17 | Object.values(reports).forEach(resourceReports => { 18 | if (runs > 1) { 19 | result.push( 20 | median(resourceReports, report => report.total, estimoMedianExecutor) 21 | ) 22 | } else { 23 | result.push(resourceReports[0]) 24 | } 25 | }) 26 | 27 | if (diffMode && result.length > 1) { 28 | let baseline = result[0] 29 | for (let i = 1; i < result.length; i += 1) { 30 | result[i].diff = { 31 | garbageCollection: `${result[i].garbageCollection} (${createDiff( 32 | result[i].garbageCollection, 33 | baseline.garbageCollection 34 | )})`, 35 | javaScript: `${result[i].javaScript} (${createDiff( 36 | result[i].javaScript, 37 | baseline.javaScript 38 | )})`, 39 | other: `${result[i].other} (${createDiff( 40 | result[i].other, 41 | baseline.other 42 | )})`, 43 | paintCompositeRender: `${ 44 | result[i].paintCompositeRender 45 | } (${createDiff( 46 | result[i].paintCompositeRender, 47 | baseline.paintCompositeRender 48 | )})`, 49 | parseHTML: `${result[i].parseHTML} (${createDiff( 50 | result[i].parseHTML, 51 | baseline.parseHTML 52 | )})`, 53 | scriptEvaluation: `${result[i].scriptEvaluation} (${createDiff( 54 | result[i].scriptEvaluation, 55 | baseline.scriptEvaluation 56 | )})`, 57 | scriptParseCompile: `${result[i].scriptParseCompile} (${createDiff( 58 | result[i].scriptParseCompile, 59 | baseline.scriptParseCompile 60 | )})`, 61 | styleLayout: `${result[i].styleLayout} (${createDiff( 62 | result[i].styleLayout, 63 | baseline.styleLayout 64 | )})`, 65 | total: `${result[i].total} (${createDiff( 66 | result[i].total, 67 | baseline.total 68 | )})` 69 | } 70 | } 71 | } 72 | } catch (error) { 73 | console.error(error) 74 | } 75 | 76 | return result 77 | } 78 | 79 | export async function processor(sources, browserOptions, mode) { 80 | let runs = browserOptions.runs || 1 81 | let reports = [] 82 | let result = [] 83 | 84 | try { 85 | let resources = [] 86 | if (mode === 'js-mode') { 87 | resources = await prepareLibrariesForEstimation(sources) 88 | } else { 89 | resources = sources.map(page => ({ name: page, url: page })) 90 | } 91 | 92 | for (let i = 0; i < runs; i += 1) { 93 | resources = await createChromeTrace(resources, browserOptions) 94 | reports = reports.concat(await generatePrettyReport(resources)) 95 | await removeAllFiles(resources.map(item => item.tracePath)) 96 | } 97 | if (mode === 'js-mode') { 98 | await removeAllFiles(resources.map(item => item.htmlPath)) 99 | } 100 | 101 | let sortedReports = {} 102 | reports.forEach(report => { 103 | if (!sortedReports[report.name]) { 104 | sortedReports[report.name] = [] 105 | sortedReports[report.name].push(report) 106 | } else { 107 | sortedReports[report.name].push(report) 108 | } 109 | }) 110 | result = await reportsProcessor(sortedReports, browserOptions) 111 | } catch (error) { 112 | console.error(error) 113 | } 114 | 115 | return result 116 | } 117 | -------------------------------------------------------------------------------- /src/reporter.js: -------------------------------------------------------------------------------- 1 | import tracium from '@sitespeed.io/tracium' 2 | 3 | import { readFile } from './utils.js' 4 | 5 | export async function generateTasksReport(pathToTraceFile) { 6 | let tasks = [] 7 | 8 | try { 9 | let tracelog = JSON.parse(await readFile(pathToTraceFile)) 10 | tasks = tracium.computeMainThreadTasks(tracelog, { flatten: true }) 11 | } catch (error) { 12 | console.error(error) 13 | } 14 | 15 | return tasks 16 | } 17 | 18 | export function formatTime(time) { 19 | return +parseFloat(time).toFixed(2) 20 | } 21 | 22 | export function getEventsTime(events) { 23 | let time = events.reduce((acc, cur) => acc + cur.selfTime, 0) 24 | return formatTime(Math.round(time * 100) / 100) 25 | } 26 | 27 | export async function generatePrettyReport(resources) { 28 | let reports = [] 29 | 30 | try { 31 | for (let item of resources) { 32 | let tasks = await generateTasksReport(item.tracePath) 33 | 34 | let htmlTime = getEventsTime( 35 | tasks.filter(({ kind }) => kind === 'parseHTML') 36 | ) 37 | let styleTime = getEventsTime( 38 | tasks.filter(({ kind }) => kind === 'styleLayout') 39 | ) 40 | let renderTime = getEventsTime( 41 | tasks.filter(({ kind }) => kind === 'paintCompositeRender') 42 | ) 43 | let compileTime = getEventsTime( 44 | tasks.filter(({ kind }) => kind === 'scriptParseCompile') 45 | ) 46 | let evaluationTime = getEventsTime( 47 | tasks.filter(({ kind }) => kind === 'scriptEvaluation') 48 | ) 49 | let garbageTime = getEventsTime( 50 | tasks.filter(({ kind }) => kind === 'garbageCollection') 51 | ) 52 | let otherTime = getEventsTime( 53 | tasks.filter(({ kind }) => kind === 'other') 54 | ) 55 | 56 | reports.push({ 57 | garbageCollection: garbageTime, 58 | javaScript: formatTime(compileTime + evaluationTime), 59 | name: item.name, 60 | other: otherTime, 61 | paintCompositeRender: renderTime, 62 | parseHTML: htmlTime, 63 | scriptEvaluation: evaluationTime, 64 | scriptParseCompile: compileTime, 65 | styleLayout: styleTime, 66 | total: formatTime( 67 | htmlTime + 68 | styleTime + 69 | renderTime + 70 | compileTime + 71 | evaluationTime + 72 | garbageTime + 73 | otherTime 74 | ) 75 | }) 76 | } 77 | } catch (error) { 78 | console.error(error) 79 | } 80 | 81 | return reports 82 | } 83 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | import { findChrome } from 'find-chrome-bin' 2 | import fs from 'node:fs' 3 | import path from 'node:path' 4 | import { fileURLToPath } from 'node:url' 5 | import { promisify } from 'node:util' 6 | import puppeteer from 'puppeteer-core' 7 | import { PUPPETEER_REVISIONS } from 'puppeteer-core/lib/cjs/puppeteer/revisions.js' 8 | 9 | const __dirname = path.dirname(fileURLToPath(import.meta.url)) 10 | const chromeTempPath = path.join(__dirname, '..', 'temp', 'chrome') 11 | const chromeConfigPath = path.join(__dirname, '..', 'chrome.json') 12 | 13 | const write = promisify(fs.writeFile) 14 | const read = promisify(fs.readFile) 15 | const unlink = promisify(fs.unlink) 16 | 17 | export function resolvePathToTempDir(fileName, tempDir = '../temp/') { 18 | return path.join(__dirname, tempDir, fileName) 19 | } 20 | 21 | export function getUrlToHtmlFile(file) { 22 | return `file://${path.resolve(file)}` 23 | } 24 | 25 | export function getLibraryName(lib) { 26 | if (/^https?/.test(lib)) { 27 | return lib.substring(lib.lastIndexOf('/') + 1) 28 | } 29 | return path.basename(lib) 30 | } 31 | 32 | export function isJsFile(p) { 33 | let JS_FILES = /\.m?js$/i 34 | return JS_FILES.test(path.extname(path.basename(p))) 35 | } 36 | 37 | export function isUrl(p) { 38 | let WEB_URLS = /^(https?|file):/ 39 | return WEB_URLS.test(p) 40 | } 41 | 42 | export function splitResourcesForEstimo(resources) { 43 | let items = Array.isArray(resources) ? resources : [resources] 44 | let libraries = [] 45 | let pages = [] 46 | 47 | items.forEach(item => { 48 | if (isJsFile(item)) { 49 | libraries.push(item) 50 | } else if (isUrl(item) && !isJsFile(item)) { 51 | pages.push(item) 52 | } else { 53 | throw new TypeError( 54 | 'Estimo works only with resources which are (paths to Js files) OR (urls to Web pages) ( OR >)' 55 | ) 56 | } 57 | }) 58 | 59 | return { libraries, pages } 60 | } 61 | 62 | export function checkEstimoArgs(resources, browserOptions) { 63 | if (typeof resources !== 'string' && !Array.isArray(resources)) { 64 | throw new TypeError( 65 | 'The first argument should be String or Array which contains a path to the resource (Js file or Web page).' 66 | ) 67 | } 68 | if (Array.isArray(resources)) { 69 | if (resources.length === 0) { 70 | throw new TypeError( 71 | 'All resources should be represented as a path to the resource (Js file or Web page).' 72 | ) 73 | } 74 | resources.forEach(item => { 75 | if (typeof item !== 'string') { 76 | throw new TypeError( 77 | 'All resources should be represented as a path to the resource (Js file or Web page).' 78 | ) 79 | } 80 | }) 81 | } 82 | if ( 83 | typeof browserOptions !== 'object' || 84 | browserOptions === null || 85 | (typeof browserOptions === 'object' && 86 | browserOptions.constructor !== Object) 87 | ) { 88 | throw new TypeError( 89 | 'The second argument should be an Object which contains browser options (see https://github.com/mbalabash/estimo#additional-use-cases).' 90 | ) 91 | } 92 | } 93 | 94 | export function createDiff(current, base) { 95 | if (current === 0 && base === 0) { 96 | return 'N/A' 97 | } 98 | if (current === 0 && base !== 0) { 99 | return '-100%' 100 | } 101 | 102 | let value = ((current - base) / current) * 100 103 | let formatted = (Math.sign(value) * Math.ceil(Math.abs(value) * 100)) / 100 104 | 105 | if (value > 0) { 106 | return `+${formatted}% 🔺` 107 | } 108 | if (value === 0) { 109 | return `${formatted}%` 110 | } 111 | return `${formatted}% 🔽` 112 | } 113 | 114 | const defaultMedianAccessor = element => element 115 | const defaultMedianExecutor = (a, b) => (a + b) / 2 116 | export function median( 117 | array, 118 | accessor = defaultMedianAccessor, 119 | executor = defaultMedianExecutor 120 | ) { 121 | let sortedArray = array.sort((a, b) => accessor(a) - accessor(b)) 122 | let { length } = sortedArray 123 | let mid = parseInt(length / 2, 10) 124 | 125 | if (length % 2) { 126 | return sortedArray[mid] 127 | } 128 | let low = mid - 1 129 | let hight = mid 130 | 131 | return executor(sortedArray[low], sortedArray[hight]) 132 | } 133 | 134 | export function estimoMedianExecutor(reportA, reportB) { 135 | if (reportA.name !== reportB.name) { 136 | throw new Error( 137 | 'Both the first report name and the second report name should be the same!' 138 | ) 139 | } 140 | 141 | let calc = (a, b) => +((a + b) / 2).toFixed(2) 142 | 143 | return { 144 | garbageCollection: calc( 145 | reportA.garbageCollection, 146 | reportB.garbageCollection 147 | ), 148 | javaScript: calc(reportA.javaScript, reportB.javaScript), 149 | name: reportA.name, 150 | other: calc(reportA.other, reportB.other), 151 | paintCompositeRender: calc( 152 | reportA.paintCompositeRender, 153 | reportB.paintCompositeRender 154 | ), 155 | parseHTML: calc(reportA.parseHTML, reportB.parseHTML), 156 | scriptEvaluation: calc(reportA.scriptEvaluation, reportB.scriptEvaluation), 157 | scriptParseCompile: calc( 158 | reportA.scriptParseCompile, 159 | reportB.scriptParseCompile 160 | ), 161 | styleLayout: calc(reportA.styleLayout, reportB.styleLayout), 162 | total: calc(reportA.total, reportB.total) 163 | } 164 | } 165 | 166 | export async function readFile(filePath) { 167 | let content 168 | 169 | try { 170 | if (!fs.existsSync(filePath)) { 171 | throw new Error(`${filePath} - file isn't exist!`) 172 | } 173 | content = await read(filePath, 'utf8') 174 | } catch (error) { 175 | console.error(error) 176 | } 177 | 178 | return content 179 | } 180 | 181 | export async function writeFile(filePath, content) { 182 | try { 183 | await write(filePath, content) 184 | } catch (error) { 185 | console.error(error) 186 | } 187 | } 188 | 189 | export async function deleteFile(filePath) { 190 | try { 191 | await unlink(filePath) 192 | } catch (error) { 193 | console.error(error) 194 | } 195 | } 196 | 197 | export async function removeAllFiles(files) { 198 | if (process.env.ESTIMO_DEBUG) { 199 | return 200 | } 201 | 202 | try { 203 | for (let file of files) { 204 | if (typeof file === 'string') { 205 | await deleteFile(file) 206 | } 207 | } 208 | } catch (error) { 209 | console.error(error) 210 | } 211 | } 212 | 213 | export function existsAsync(filePath) { 214 | return fs.promises.stat(filePath).catch(() => false) 215 | } 216 | 217 | export async function findChromeBinary() { 218 | try { 219 | let configData = JSON.parse(await readFile(chromeConfigPath)) 220 | if (configData.executablePath.length > 0) { 221 | return configData 222 | } 223 | 224 | let chromeInfo = await findChrome({ 225 | download: { 226 | path: chromeTempPath, 227 | puppeteer, 228 | revision: PUPPETEER_REVISIONS.chrome 229 | } 230 | }) 231 | await writeFile(chromeConfigPath, JSON.stringify(chromeInfo)) 232 | 233 | return chromeInfo 234 | } catch (error) { 235 | console.info() 236 | console.error(error) 237 | console.info() 238 | return { browser: '', executablePath: '' } 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /temp/chrome/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbalabash/estimo/1dacda001034b1803f6f3b50cb9cb87234470677/temp/chrome/.gitkeep -------------------------------------------------------------------------------- /tests/__mock__/13kb.js: -------------------------------------------------------------------------------- 1 | /** @license React v16.8.6 2 | * react.production.min.js 3 | * 4 | * Copyright (c) Facebook, Inc. and its affiliates. 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | */ 9 | 'use strict';(function(N,q){"object"===typeof exports&&"undefined"!==typeof module?module.exports=q():"function"===typeof define&&define.amd?define(q):N.React=q()})(this,function(){function N(a,b,d,g,p,c,e,h){if(!a){a=void 0;if(void 0===b)a=Error("Minified exception occurred; use the non-minified dev environment for the full error message and additional helpful warnings.");else{var n=[d,g,p,c,e,h],f=0;a=Error(b.replace(/%s/g,function(){return n[f++]}));a.name="Invariant Violation"}a.framesToPop=1; 10 | throw a;}}function q(a){for(var b=arguments.length-1,d="https://reactjs.org/docs/error-decoder.html?invariant="+a,g=0;g=b){d=a;break}a=a.next}while(a!==c);null===d?d=c:d=== 12 | c&&(c=n,u());b=d.previous;b.next=d.previous=n;n.next=d;n.previous=b}}function F(){if(-1===k&&null!==c&&1===c.priorityLevel){x=!0;try{do Q();while(null!==c&&1===c.priorityLevel)}finally{x=!1,null!==c?u():C=!1}}}function ta(a){x=!0;var b=G;G=a;try{if(a)for(;null!==c;){var d=l();if(c.expirationTime<=d){do Q();while(null!==c&&c.expirationTime<=d)}else break}else if(null!==c){do Q();while(null!==c&&!H())}}finally{x=!1,G=b,null!==c?u():C=!1,F()}}function ea(a,b,d){var g=void 0,p={},c=null,e=null;if(null!= 13 | b)for(g in void 0!==b.ref&&(e=b.ref),void 0!==b.key&&(c=""+b.key),b)fa.call(b,g)&&!ha.hasOwnProperty(g)&&(p[g]=b[g]);var h=arguments.length-2;if(1===h)p.children=d;else if(1I.length&&I.push(a)}function T(a,b,d,g){var c=typeof a;if("undefined"===c||"boolean"===c)a=null;var e=!1;if(null=== 15 | a)e=!0;else switch(c){case "string":case "number":e=!0;break;case "object":switch(a.$$typeof){case y:case wa:e=!0}}if(e)return d(g,a,""===b?"."+U(a,0):b),1;e=0;b=""===b?".":b+":";if(Array.isArray(a))for(var f=0;fa;a++)b["_"+String.fromCharCode(a)]=a;if("0123456789"!==Object.getOwnPropertyNames(b).map(function(a){return b[a]}).join(""))return!1;var d={};"abcdefghijklmnopqrst".split("").forEach(function(a){d[a]=a});return"abcdefghijklmnopqrst"!==Object.keys(Object.assign({},d)).join("")?!1:!0}catch(g){return!1}}()?Object.assign:function(a,b){if(null===a||void 0===a)throw new TypeError("Object.assign cannot be called with null or undefined");var d=Object(a);for(var c,e=1;e=L-d)if(-1!==b&&b<=d)c=!0;else{A||(A=!0,Y(aa));w=a;z=b;return}if(null!==a){Z=!0;try{a(c)}finally{Z=!1}}};var aa=function(a){if(null!==w){Y(aa);var b=a-L+B;bb&&(b=8),B=bb?sa.postMessage(void 0):A||(A=!0,Y(aa))};P=function(){w=null;K=!1;z=-1}}var Oa= 25 | 0,ma={current:null},R={current:null};e={ReactCurrentDispatcher:ma,ReactCurrentOwner:R,assign:J};J(e,{Scheduler:{unstable_cancelCallback:function(a){var b=a.next;if(null!==b){if(b===a)c=null;else{a===c&&(c=b);var d=a.previous;d.next=b;b.previous=d}a.next=a.previous=null}},unstable_shouldYield:function(){return!G&&(null!==c&&c.expirationTimeb){d=g;break}g=g.next}while(g!==c);null===d?d=c:d===c&&(c=a,u());b=d.previous;b.next=d.previous=a;a.next=d;a.previous=b}return a},unstable_runWithPriority:function(a,b){switch(a){case 1:case 2:case 3:case 4:case 5:break;default:a= 27 | 3}var d=f,c=k;f=a;k=l();try{return b()}finally{f=d,k=c,F()}},unstable_next:function(a){switch(f){case 1:case 2:case 3:var b=3;break;default:b=f}var d=f,c=k;f=b;k=l();try{return a()}finally{f=d,k=c,F()}},unstable_wrapCallback:function(a){var b=f;return function(){var d=f,c=k;f=b;k=l();try{return a.apply(this,arguments)}finally{f=d,k=c,F()}}},unstable_getFirstCallbackNode:function(){return c},unstable_pauseExecution:function(){},unstable_continueExecution:function(){null!==c&&u()},unstable_getCurrentPriorityLevel:function(){return f}, 28 | unstable_IdlePriority:5,unstable_ImmediatePriority:1,unstable_LowPriority:4,unstable_NormalPriority:3,unstable_UserBlockingPriority:2},SchedulerTracing:{__interactionsRef:null,__subscriberRef:null,unstable_clear:function(a){return a()},unstable_getCurrent:function(){return null},unstable_getThreadID:function(){return++Oa},unstable_subscribe:function(a){},unstable_trace:function(a,b,d){return d()},unstable_unsubscribe:function(a){},unstable_wrap:function(a){return a}}});var fa=Object.prototype.hasOwnProperty, 29 | ha={key:!0,ref:!0,__self:!0,__source:!0},la=/\/+/g,I=[];r={Children:{map:function(a,b,d){if(null==a)return a;var c=[];W(a,c,null,b,d);return c},forEach:function(a,b,d){if(null==a)return a;b=ia(null,null,b,d);V(a,xa,b);ja(b)},count:function(a){return V(a,function(){return null},null)},toArray:function(a){var b=[];W(a,b,null,function(a){return a});return b},only:function(a){S(a)?void 0:q("143");return a}},createRef:function(){return{current:null}},Component:t,PureComponent:O,createContext:function(a, 30 | b){void 0===b&&(b=null);a={$$typeof:Ba,_calculateChangedBits:b,_currentValue:a,_currentValue2:a,_threadCount:0,Provider:null,Consumer:null};a.Provider={$$typeof:Aa,_context:a};return a.Consumer=a},forwardRef:function(a){return{$$typeof:Da,render:a}},lazy:function(a){return{$$typeof:Ga,_ctor:a,_status:-1,_result:null}},memo:function(a,b){return{$$typeof:Fa,type:a,compare:void 0===b?null:b}},useCallback:function(a,b){return m().useCallback(a,b)},useContext:function(a,b){return m().useContext(a,b)}, 31 | useEffect:function(a,b){return m().useEffect(a,b)},useImperativeHandle:function(a,b,d){return m().useImperativeHandle(a,b,d)},useDebugValue:function(a,b){},useLayoutEffect:function(a,b){return m().useLayoutEffect(a,b)},useMemo:function(a,b){return m().useMemo(a,b)},useReducer:function(a,b,d){return m().useReducer(a,b,d)},useRef:function(a){return m().useRef(a)},useState:function(a){return m().useState(a)},Fragment:r,StrictMode:X,Suspense:Ea,createElement:ea,cloneElement:function(a,b,d){null===a|| 32 | void 0===a?q("267",a):void 0;var c=void 0,e=J({},a.props),f=a.key,k=a.ref,h=a._owner;if(null!=b){void 0!==b.ref&&(k=b.ref,h=R.current);void 0!==b.key&&(f=""+b.key);var l=void 0;a.type&&a.type.defaultProps&&(l=a.type.defaultProps);for(c in b)fa.call(b,c)&&!ha.hasOwnProperty(c)&&(e[c]=void 0===b[c]&&void 0!==l?l[c]:b[c])}c=arguments.length-2;if(1===c)e.children=d;else if(1 { 9 | // const reports = await estimo(['https://news.ycombinator.com/news', 'https://www.google.com/'], {runs: 7}) 10 | // const reports = await estimo([localJsFile,'https://github.githubassets.com/assets/compat-bootstrap-6e7ff7ac.js'], {runs: 6}) 11 | const reports = await estimo([localJsFile,'https://github.githubassets.com/assets/compat-bootstrap-6e7ff7ac.js'], {runs: 4, diff: true}) 12 | console.log(reports) 13 | })() 14 | -------------------------------------------------------------------------------- /tests/__mock__/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Estimo Template 7 | 8 | 9 |

Estimo

10 |

Evaluates how long the browser will execute your javascript code.

11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /tests/estimo.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import path from 'node:path' 3 | import { fileURLToPath } from 'node:url' 4 | 5 | import { cleanChromeConfig } from '../scripts/clean-chrome-config.js' 6 | import estimo from '../src/lib.js' 7 | import { findChromeBinary, getUrlToHtmlFile } from '../src/utils.js' 8 | 9 | const __dirname = path.dirname(fileURLToPath(import.meta.url)) 10 | const delay = ms => new Promise(resolve => setTimeout(resolve, ms)) 11 | 12 | test('estimo - should works properly with mixed resources', async t => { 13 | let chromeInfo = await findChromeBinary() 14 | 15 | let page = getUrlToHtmlFile(path.join(__dirname, '__mock__', 'test.html')) 16 | let lib = path.join(__dirname, '__mock__', '19kb.js') 17 | 18 | let reports = await estimo([lib, page], { 19 | executablePath: chromeInfo.executablePath 20 | }) 21 | 22 | t.is(reports[0].name, '19kb.js') 23 | t.is( 24 | typeof reports[0].parseHTML === 'number' && reports[0].parseHTML >= 0, 25 | true 26 | ) 27 | t.is( 28 | typeof reports[0].styleLayout === 'number' && reports[0].styleLayout >= 0, 29 | true 30 | ) 31 | t.is( 32 | typeof reports[0].paintCompositeRender === 'number' && 33 | reports[0].paintCompositeRender >= 0, 34 | true 35 | ) 36 | t.is( 37 | typeof reports[0].scriptParseCompile === 'number' && 38 | reports[0].scriptParseCompile >= 0, 39 | true 40 | ) 41 | t.is( 42 | typeof reports[0].scriptEvaluation === 'number' && 43 | reports[0].scriptEvaluation >= 0, 44 | true 45 | ) 46 | t.is( 47 | typeof reports[0].javaScript === 'number' && reports[0].javaScript > 0, 48 | true 49 | ) 50 | t.is( 51 | typeof reports[0].garbageCollection === 'number' && 52 | reports[0].garbageCollection >= 0, 53 | true 54 | ) 55 | t.is(typeof reports[0].other === 'number' && reports[0].other >= 0, true) 56 | t.is(typeof reports[0].total === 'number' && reports[0].total > 0, true) 57 | 58 | t.is(reports[1].name, page) 59 | t.is( 60 | typeof reports[1].parseHTML === 'number' && reports[1].parseHTML >= 0, 61 | true 62 | ) 63 | t.is( 64 | typeof reports[1].styleLayout === 'number' && reports[1].styleLayout >= 0, 65 | true 66 | ) 67 | t.is( 68 | typeof reports[1].paintCompositeRender === 'number' && 69 | reports[1].paintCompositeRender >= 0, 70 | true 71 | ) 72 | t.is( 73 | typeof reports[1].scriptParseCompile === 'number' && 74 | reports[1].scriptParseCompile >= 0, 75 | true 76 | ) 77 | t.is( 78 | typeof reports[1].scriptEvaluation === 'number' && 79 | reports[1].scriptEvaluation >= 0, 80 | true 81 | ) 82 | t.is( 83 | typeof reports[1].javaScript === 'number' && reports[1].javaScript > 0, 84 | true 85 | ) 86 | t.is( 87 | typeof reports[1].garbageCollection === 'number' && 88 | reports[1].garbageCollection >= 0, 89 | true 90 | ) 91 | t.is(typeof reports[1].other === 'number' && reports[1].other >= 0, true) 92 | t.is(typeof reports[1].total === 'number' && reports[1].total > 0, true) 93 | 94 | await cleanChromeConfig() 95 | }) 96 | 97 | test('estimo - should works properly with custom RUNS option', async t => { 98 | let chromeInfo = await findChromeBinary() 99 | 100 | let lib1 = path.join(__dirname, '__mock__', '19kb.js') 101 | let page1 = getUrlToHtmlFile(path.join(__dirname, '__mock__', 'test.html')) 102 | 103 | let reports1 = await estimo([lib1], { 104 | executablePath: chromeInfo.executablePath, 105 | runs: 3 106 | }) 107 | await delay(1000) 108 | let reports2 = await estimo([page1], { 109 | executablePath: chromeInfo.executablePath, 110 | runs: 2 111 | }) 112 | 113 | t.is(reports1.length, 1) 114 | t.is( 115 | typeof reports1[0].parseHTML === 'number' && reports1[0].parseHTML >= 0, 116 | true 117 | ) 118 | t.is( 119 | typeof reports1[0].styleLayout === 'number' && reports1[0].styleLayout >= 0, 120 | true 121 | ) 122 | t.is( 123 | typeof reports1[0].paintCompositeRender === 'number' && 124 | reports1[0].paintCompositeRender >= 0, 125 | true 126 | ) 127 | t.is( 128 | typeof reports1[0].scriptParseCompile === 'number' && 129 | reports1[0].scriptParseCompile >= 0, 130 | true 131 | ) 132 | t.is( 133 | typeof reports1[0].scriptEvaluation === 'number' && 134 | reports1[0].scriptEvaluation >= 0, 135 | true 136 | ) 137 | t.is( 138 | typeof reports1[0].javaScript === 'number' && reports1[0].javaScript > 0, 139 | true 140 | ) 141 | t.is( 142 | typeof reports1[0].garbageCollection === 'number' && 143 | reports1[0].garbageCollection >= 0, 144 | true 145 | ) 146 | t.is(typeof reports1[0].other === 'number' && reports1[0].other >= 0, true) 147 | t.is(typeof reports1[0].total === 'number' && reports1[0].total > 0, true) 148 | 149 | t.is(reports2.length, 1) 150 | t.is( 151 | typeof reports2[0].parseHTML === 'number' && reports2[0].parseHTML >= 0, 152 | true 153 | ) 154 | t.is( 155 | typeof reports2[0].styleLayout === 'number' && reports2[0].styleLayout >= 0, 156 | true 157 | ) 158 | t.is( 159 | typeof reports2[0].paintCompositeRender === 'number' && 160 | reports2[0].paintCompositeRender >= 0, 161 | true 162 | ) 163 | t.is( 164 | typeof reports2[0].scriptParseCompile === 'number' && 165 | reports2[0].scriptParseCompile >= 0, 166 | true 167 | ) 168 | t.is( 169 | typeof reports2[0].scriptEvaluation === 'number' && 170 | reports2[0].scriptEvaluation >= 0, 171 | true 172 | ) 173 | t.is( 174 | typeof reports2[0].javaScript === 'number' && reports2[0].javaScript > 0, 175 | true 176 | ) 177 | t.is( 178 | typeof reports2[0].garbageCollection === 'number' && 179 | reports2[0].garbageCollection >= 0, 180 | true 181 | ) 182 | t.is(typeof reports2[0].other === 'number' && reports2[0].other >= 0, true) 183 | t.is(typeof reports2[0].total === 'number' && reports2[0].total > 0, true) 184 | 185 | await cleanChromeConfig() 186 | }) 187 | 188 | test('estimo - should works properly in DIFF MODE', async t => { 189 | let chromeInfo = await findChromeBinary() 190 | 191 | let lib1 = path.join(__dirname, '__mock__', '19kb.js') 192 | let lib2 = path.join(__dirname, '__mock__', '7kb.js') 193 | 194 | let reports = await estimo([lib1, lib2], { 195 | diff: true, 196 | executablePath: chromeInfo.executablePath, 197 | runs: 2 198 | }) 199 | 200 | t.is(reports.length, 2) 201 | t.is(typeof reports[0].diff === 'undefined', true) 202 | t.is(typeof reports[1].diff === 'object', true) 203 | 204 | t.is( 205 | typeof reports[0].parseHTML === 'number' && reports[0].parseHTML >= 0, 206 | true 207 | ) 208 | t.is( 209 | typeof reports[0].styleLayout === 'number' && reports[0].styleLayout >= 0, 210 | true 211 | ) 212 | t.is( 213 | typeof reports[0].paintCompositeRender === 'number' && 214 | reports[0].paintCompositeRender >= 0, 215 | true 216 | ) 217 | t.is( 218 | typeof reports[0].scriptParseCompile === 'number' && 219 | reports[0].scriptParseCompile >= 0, 220 | true 221 | ) 222 | t.is( 223 | typeof reports[0].scriptEvaluation === 'number' && 224 | reports[0].scriptEvaluation >= 0, 225 | true 226 | ) 227 | t.is( 228 | typeof reports[0].javaScript === 'number' && reports[0].javaScript > 0, 229 | true 230 | ) 231 | t.is( 232 | typeof reports[0].garbageCollection === 'number' && 233 | reports[0].garbageCollection >= 0, 234 | true 235 | ) 236 | t.is(typeof reports[0].other === 'number' && reports[0].other >= 0, true) 237 | t.is(typeof reports[0].total === 'number' && reports[0].total > 0, true) 238 | 239 | t.is( 240 | typeof reports[1].parseHTML === 'number' && reports[1].parseHTML >= 0, 241 | true 242 | ) 243 | t.is( 244 | typeof reports[1].styleLayout === 'number' && reports[1].styleLayout >= 0, 245 | true 246 | ) 247 | t.is( 248 | typeof reports[1].paintCompositeRender === 'number' && 249 | reports[1].paintCompositeRender >= 0, 250 | true 251 | ) 252 | t.is( 253 | typeof reports[1].scriptParseCompile === 'number' && 254 | reports[1].scriptParseCompile >= 0, 255 | true 256 | ) 257 | t.is( 258 | typeof reports[1].scriptEvaluation === 'number' && 259 | reports[1].scriptEvaluation >= 0, 260 | true 261 | ) 262 | t.is( 263 | typeof reports[1].javaScript === 'number' && reports[1].javaScript > 0, 264 | true 265 | ) 266 | t.is( 267 | typeof reports[1].garbageCollection === 'number' && 268 | reports[1].garbageCollection >= 0, 269 | true 270 | ) 271 | t.is(typeof reports[1].other === 'number' && reports[1].other >= 0, true) 272 | t.is(typeof reports[1].total === 'number' && reports[1].total > 0, true) 273 | 274 | t.is( 275 | typeof reports[1].diff.parseHTML === 'string' && 276 | reports[1].diff.parseHTML.length > 0, 277 | true 278 | ) 279 | t.is( 280 | typeof reports[1].diff.styleLayout === 'string' && 281 | reports[1].diff.styleLayout.length > 0, 282 | true 283 | ) 284 | t.is( 285 | typeof reports[1].diff.paintCompositeRender === 'string' && 286 | reports[1].diff.paintCompositeRender.length > 0, 287 | true 288 | ) 289 | t.is( 290 | typeof reports[1].diff.scriptParseCompile === 'string' && 291 | reports[1].diff.scriptParseCompile.length > 0, 292 | true 293 | ) 294 | t.is( 295 | typeof reports[1].diff.scriptEvaluation === 'string' && 296 | reports[1].diff.scriptEvaluation.length > 0, 297 | true 298 | ) 299 | t.is( 300 | typeof reports[1].diff.javaScript === 'string' && 301 | reports[1].diff.javaScript.length > 0, 302 | true 303 | ) 304 | t.is( 305 | typeof reports[1].diff.garbageCollection === 'string' && 306 | reports[1].diff.garbageCollection.length > 0, 307 | true 308 | ) 309 | t.is( 310 | typeof reports[1].diff.other === 'string' && 311 | reports[1].diff.other.length > 0, 312 | true 313 | ) 314 | t.is( 315 | typeof reports[1].diff.total === 'string' && 316 | reports[1].diff.total.length > 0, 317 | true 318 | ) 319 | 320 | await cleanChromeConfig() 321 | }) 322 | -------------------------------------------------------------------------------- /tests/generate-html-file.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import fs from 'node:fs' 3 | import path from 'node:path' 4 | import { fileURLToPath } from 'node:url' 5 | 6 | import { 7 | createHtmlContent, 8 | generateHtmlFile, 9 | prepareLibrariesForEstimation 10 | } from '../src/generate-html-file.js' 11 | import { removeAllFiles, resolvePathToTempDir } from '../src/utils.js' 12 | 13 | const __dirname = path.dirname(fileURLToPath(import.meta.url)) 14 | 15 | test('should properly prepare resources for Estimo', async t => { 16 | let lib1 = path.join(__dirname, '__mock__', '19kb.js') 17 | let lib2 = path.join(__dirname, '__mock__', '13kb.js') 18 | let lib3 = 'https://unpkg.com/react@16/umd/react.development.js' 19 | 20 | t.deepEqual(await prepareLibrariesForEstimation([]), []) 21 | 22 | let resources = await prepareLibrariesForEstimation([lib1, lib2, lib3]) 23 | t.is(resources[0].name, '19kb.js') 24 | t.is(resources[0].url.includes('file://'), true) 25 | t.is(resources[0].url.includes('temp'), true) 26 | t.is(resources[0].url.includes('.html'), true) 27 | t.is(resources[0].htmlPath.includes('temp'), true) 28 | t.is(resources[0].htmlPath.includes('.html'), true) 29 | 30 | t.is(resources[1].name, '13kb.js') 31 | t.is(resources[1].url.includes('file://'), true) 32 | t.is(resources[1].url.includes('temp'), true) 33 | t.is(resources[1].url.includes('.html'), true) 34 | t.is(resources[1].htmlPath.includes('temp'), true) 35 | t.is(resources[1].htmlPath.includes('.html'), true) 36 | 37 | t.is(resources[2].name, 'react.development.js') 38 | t.is(resources[2].url.includes('file://'), true) 39 | t.is(resources[2].url.includes('temp'), true) 40 | t.is(resources[2].url.includes('.html'), true) 41 | t.is(resources[2].htmlPath.includes('temp'), true) 42 | t.is(resources[2].htmlPath.includes('.html'), true) 43 | 44 | await removeAllFiles(resources.map(item => item.htmlPath)) 45 | await removeAllFiles(resources.map(item => item.tracePath)) 46 | }) 47 | 48 | test('should throw an error for not existed local js files', async t => { 49 | let error = await t.throwsAsync( 50 | prepareLibrariesForEstimation(['some/not/existed/file.js']) 51 | ) 52 | t.is(error.message, `some/not/existed/file.js - file isn't exist!`) 53 | }) 54 | 55 | test('should properly generate content for html file', t => { 56 | let lib1 = 'https://unpkg.com/react@16/umd/react.development.js' 57 | 58 | t.is( 59 | createHtmlContent(lib1), 60 | ` 61 | 62 | 63 | 64 | 65 | 66 | Estimo Template 67 | 68 | 69 | 70 |

Estimo

71 | 72 | ` 73 | ) 74 | }) 75 | 76 | test('should properly create html file for one library', async t => { 77 | let lib1 = 'https://unpkg.com/react@16/umd/react.development.js' 78 | let htmlFile = await generateHtmlFile(createHtmlContent(lib1)) 79 | 80 | t.is(fs.existsSync(htmlFile), true) 81 | t.is(htmlFile, resolvePathToTempDir(path.basename(htmlFile))) 82 | 83 | await removeAllFiles([htmlFile]) 84 | }) 85 | 86 | test('should properly create html for few libraries', async t => { 87 | let lib1 = 'https://unpkg.com/react@16/umd/react.development.js' 88 | let lib2 = 89 | 'https://cdnjs.cloudflare.com/ajax/libs/react/16.8.6/umd/react.production.min.js' 90 | 91 | let htmlFile1 = await generateHtmlFile(createHtmlContent(lib1)) 92 | let htmlFile2 = await generateHtmlFile(createHtmlContent(lib2)) 93 | 94 | t.is(fs.existsSync(htmlFile1), true) 95 | t.is(htmlFile1, resolvePathToTempDir(path.basename(htmlFile1))) 96 | 97 | t.is(fs.existsSync(htmlFile2), true) 98 | t.is(htmlFile2, resolvePathToTempDir(path.basename(htmlFile2))) 99 | 100 | await removeAllFiles([htmlFile1, htmlFile2]) 101 | }) 102 | -------------------------------------------------------------------------------- /tests/reporter.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | 3 | import { formatTime, getEventsTime } from '../src/reporter.js' 4 | 5 | test('[formatTime]: should properly format time', t => { 6 | t.is(formatTime(11.2223123131231), 11.22) 7 | t.is(formatTime('11.226'), 11.23) 8 | t.is(formatTime(11), 11.0) 9 | }) 10 | 11 | test('[getEventsTime]: should properly calculate time which spent by some group of tasks', t => { 12 | let events1 = [{ selfTime: 11.11 }, { selfTime: 2.43 }, { selfTime: 7.16 }] 13 | let events2 = [{ selfTime: 80.0 }] 14 | let events3 = [ 15 | { selfTime: 21.3 }, 16 | { selfTime: 43.0 }, 17 | { selfTime: 0.16 }, 18 | { selfTime: 9.41 }, 19 | { selfTime: 0.40003 }, 20 | { selfTime: 0.0002 } 21 | ] 22 | let events4 = [] 23 | 24 | t.is(getEventsTime(events1), 20.7) 25 | t.is(getEventsTime(events2), 80.0) 26 | t.is(getEventsTime(events3), 74.27) 27 | t.is(getEventsTime(events4), 0) 28 | }) 29 | -------------------------------------------------------------------------------- /tests/utils.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import path from 'node:path' 3 | import { fileURLToPath } from 'node:url' 4 | 5 | import { cleanChromeConfig } from '../scripts/clean-chrome-config.js' 6 | import { 7 | checkEstimoArgs, 8 | createDiff, 9 | findChromeBinary, 10 | getLibraryName, 11 | getUrlToHtmlFile, 12 | isJsFile, 13 | isUrl, 14 | readFile, 15 | resolvePathToTempDir, 16 | splitResourcesForEstimo 17 | } from '../src/utils.js' 18 | 19 | const __dirname = path.dirname(fileURLToPath(import.meta.url)) 20 | 21 | test('[resolvePathToTempDir]: should properly resolve path to file in temp directory', t => { 22 | let fileName = 'someFile.txt' 23 | let customTempDir = '../test/__mock__/' 24 | 25 | t.is( 26 | resolvePathToTempDir(fileName), 27 | path.join(__dirname, '../temp/', fileName) 28 | ) 29 | 30 | t.is( 31 | resolvePathToTempDir(fileName, customTempDir), 32 | path.join(__dirname, customTempDir, fileName) 33 | ) 34 | }) 35 | 36 | test('[getUrlToHtmlFile]: should properly generate url to local file', t => { 37 | let fileName = 'index.html' 38 | t.is( 39 | getUrlToHtmlFile(resolvePathToTempDir(fileName)), 40 | `file://${path.join(__dirname, '../temp/', fileName)}` 41 | ) 42 | }) 43 | 44 | test('[createDiff]: should properly create diff', async t => { 45 | t.is(createDiff(100894, 110894), '-9.92% 🔽') 46 | t.is(createDiff(0.2021099999999999, 0.10210999999999999), '+49.48% 🔺') 47 | t.is(createDiff(2.5658984375, 2.1658984375), '+15.59% 🔺') 48 | t.is(createDiff(2.7680084375000003, 2.2680084375000003), '+18.07% 🔺') 49 | t.is(createDiff(1000, 100), '+90% 🔺') 50 | }) 51 | 52 | test('[getLibraryName]: should properly extract library name', async t => { 53 | t.is(getLibraryName('http://qwe.asd/myLib.js'), 'myLib.js') 54 | t.is(getLibraryName('http://qwe.asd/myLib/some/dir/lib.js'), 'lib.js') 55 | t.is(getLibraryName('https://qwe.asd/myLib.js'), 'myLib.js') 56 | t.is(getLibraryName('https://qwe.asd/myLib/core.js'), 'core.js') 57 | t.is(getLibraryName('./dir/dev/lib/index.js'), 'index.js') 58 | t.is(getLibraryName('/Users/dev/project/myLib.js'), 'myLib.js') 59 | t.is(getLibraryName('../myLib.js'), 'myLib.js') 60 | }) 61 | 62 | test('[isJsFile]: should properly detect js file names', async t => { 63 | t.is(isJsFile('http://qwe.asd/myLib.js'), true) 64 | t.is(isJsFile('https://qwe.asd/myLib.js'), true) 65 | t.is(isJsFile('temp/dir/core.js'), true) 66 | t.is(isJsFile('index.js'), true) 67 | t.is(isJsFile('./dev/project/myLib.mjs'), true) 68 | 69 | t.is(isJsFile('qwe/asd.css'), false) 70 | t.is(isJsFile('cvxvx/qw.html'), false) 71 | }) 72 | 73 | test("[isUrl]: should properly detect web url's", async t => { 74 | t.is(isUrl('http://qwe.asd/myLib.js'), true) 75 | t.is(isUrl('https://qwe.asd/myLib.js'), true) 76 | t.is(isUrl('http://qwe.asd/qwe.css'), true) 77 | t.is(isUrl('https://qwe.asd/zxc.html'), true) 78 | 79 | t.is(isUrl('qwe/asd/'), false) 80 | t.is(isUrl('ftp://domain.to/'), false) 81 | t.is(isUrl('index.js'), false) 82 | }) 83 | 84 | test("[splitResourcesForEstimo]: should properly split input to js files and non-js web url's", async t => { 85 | t.deepEqual( 86 | splitResourcesForEstimo(['https://qwe.asd/myLib.js', 'index.js']), 87 | { 88 | libraries: ['https://qwe.asd/myLib.js', 'index.js'], 89 | pages: [] 90 | } 91 | ) 92 | 93 | t.deepEqual(splitResourcesForEstimo(['http://qwe.asd/myLib.js']), { 94 | libraries: ['http://qwe.asd/myLib.js'], 95 | pages: [] 96 | }) 97 | 98 | t.deepEqual( 99 | splitResourcesForEstimo(['http://example.com/', 'https://example.com/']), 100 | { 101 | libraries: [], 102 | pages: ['http://example.com/', 'https://example.com/'] 103 | } 104 | ) 105 | 106 | t.deepEqual( 107 | splitResourcesForEstimo([ 108 | 'http://qwe.asd/qwe.css', 109 | 'https://qwe.asd/zxc.html', 110 | 'http://qwe.asd/myLib.js', 111 | 'index.js' 112 | ]), 113 | { 114 | libraries: ['http://qwe.asd/myLib.js', 'index.js'], 115 | pages: ['http://qwe.asd/qwe.css', 'https://qwe.asd/zxc.html'] 116 | } 117 | ) 118 | 119 | t.deepEqual(splitResourcesForEstimo([]), { libraries: [], pages: [] }) 120 | 121 | let error = t.throws(() => 122 | splitResourcesForEstimo(['ftp://domain.to/', 'qwe/asd/']) 123 | ) 124 | t.is( 125 | error.message, 126 | 'Estimo works only with resources which are (paths to Js files) OR (urls to Web pages) ( OR >)' 127 | ) 128 | }) 129 | 130 | test('[checkEstimoArgs]: should properly handle input args', async t => { 131 | t.is( 132 | t.throws(() => checkEstimoArgs(123)).message, 133 | 'The first argument should be String or Array which contains a path to the resource (Js file or Web page).' 134 | ) 135 | t.is( 136 | t.throws(() => checkEstimoArgs(undefined)).message, 137 | 'The first argument should be String or Array which contains a path to the resource (Js file or Web page).' 138 | ) 139 | t.is( 140 | t.throws(() => checkEstimoArgs(null)).message, 141 | 'The first argument should be String or Array which contains a path to the resource (Js file or Web page).' 142 | ) 143 | t.is( 144 | t.throws(() => checkEstimoArgs({})).message, 145 | 'The first argument should be String or Array which contains a path to the resource (Js file or Web page).' 146 | ) 147 | 148 | t.is( 149 | t.throws(() => checkEstimoArgs([])).message, 150 | 'All resources should be represented as a path to the resource (Js file or Web page).' 151 | ) 152 | t.is( 153 | t.throws(() => checkEstimoArgs(['lib', 'vc', 123])).message, 154 | 'All resources should be represented as a path to the resource (Js file or Web page).' 155 | ) 156 | 157 | t.is( 158 | t.throws(() => checkEstimoArgs(['lib'], [])).message, 159 | 'The second argument should be an Object which contains browser options (see https://github.com/mbalabash/estimo#additional-use-cases).' 160 | ) 161 | t.is( 162 | t.throws(() => checkEstimoArgs(['lib'], new Date())).message, 163 | 'The second argument should be an Object which contains browser options (see https://github.com/mbalabash/estimo#additional-use-cases).' 164 | ) 165 | t.is( 166 | t.throws(() => checkEstimoArgs(['lib'], null)).message, 167 | 'The second argument should be an Object which contains browser options (see https://github.com/mbalabash/estimo#additional-use-cases).' 168 | ) 169 | t.is( 170 | t.throws(() => checkEstimoArgs(['lib'], undefined)).message, 171 | 'The second argument should be an Object which contains browser options (see https://github.com/mbalabash/estimo#additional-use-cases).' 172 | ) 173 | t.is( 174 | t.throws(() => checkEstimoArgs(['lib'], 123)).message, 175 | 'The second argument should be an Object which contains browser options (see https://github.com/mbalabash/estimo#additional-use-cases).' 176 | ) 177 | }) 178 | 179 | test('should set location setting for downloaded or local chrome', async t => { 180 | let chromeInfo = await findChromeBinary() 181 | let configData = JSON.parse( 182 | await readFile(path.join(__dirname, '..', 'chrome.json')) 183 | ) 184 | 185 | t.is( 186 | typeof chromeInfo === 'object' && Object.keys(chromeInfo).length === 2, 187 | true 188 | ) 189 | t.is(configData.browser.length > 0, true) 190 | t.is(configData.executablePath.length > 0, true) 191 | t.is(configData.executablePath === chromeInfo.executablePath, true) 192 | 193 | await cleanChromeConfig() 194 | }) 195 | --------------------------------------------------------------------------------