├── .eslintrc.js
├── .gitignore
├── LICENSE
├── README.md
├── README.zh-CN.md
├── bin
└── index.js
├── package.json
├── src
├── analyzer
│ └── index.js
├── cli
│ └── index.js
├── index.js
├── output
│ └── index.js
├── performance
│ └── index.js
└── util
│ └── index.js
└── test
├── Emitter.test.js
└── Util.test.js
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "extends": "standard"
3 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules/
3 | /dist/
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | /test/unit/coverage/
8 | selenium-debug.log
9 | package-lock.json
10 | config.json
11 | exp.json
12 | note
13 |
14 | # Editor directories and files
15 | .idea
16 | .vscode
17 | *.suo
18 | *.ntvs*
19 | *.njsproj
20 | *.sln
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 liyanfeng(pod4g)
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 | **English** | [中文](./README.zh-CN.md)
2 |
3 |

4 |
5 | 🚀 A statistical analysis tool for performance testing
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | ## Hiper
14 |
15 | The name is short for **Hi** **per**formance Or **Hi**gh **per**formance
16 |
17 | ## Important
18 |
19 | Hi guys, Please present your issue in English
20 |
21 | 请使用英语提issue
22 |
23 | ## Install
24 |
25 | ``` bash
26 | npm install hiper -g
27 |
28 | # or use yarn:
29 | # yarn global add hiper
30 | ```
31 |
32 | ## The output
33 |
34 | Notice: `It takes period (m)s to load ...`. the `period` means **This test takes time**. So -n go up and the period go up. not a bug
35 |
36 | 
37 |
38 | ## PerformanceTiming
39 |
40 | 
41 |
42 | | Key | Value |
43 | | :----------------------------- | :------------------------------------------- |
44 | | DNS lookup time | domainLookupEnd - domainLookupStart |
45 | | TCP connect time | connectEnd - connectStart |
46 | | TTFB | responseStart - requestStart |
47 | | Download time of the page | responseEnd - responseStart |
48 | | After DOM Ready download time | domComplete - domInteractive |
49 | | White screen time | domInteractive - navigationStart |
50 | | DOM Ready time | domContentLoadedEventEnd - navigationStart |
51 | | Load time | loadEventEnd - navigationStart |
52 |
53 | https://developer.mozilla.org/en-US/docs/Web/API/PerformanceTiming
54 |
55 | ## Usage
56 |
57 | ```bash
58 | hiper --help
59 |
60 | Usage: hiper [options] [url]
61 |
62 | 🚀 A statistical analysis tool for performance testing
63 |
64 | Options:
65 |
66 | -v, --version output the version number
67 | -n, --count specified loading times (default: 20)
68 | -c, --config load the configuration file
69 | -u, --useragent to set the useragent
70 | -H, --headless [b] whether to use headless mode (default: true)
71 | -e, --executablePath use the specified chrome browser
72 | --no-cache disable cache (default: false)
73 | --no-javascript disable javascript (default: false)
74 | --no-online disable network (defalut: false)
75 | -h, --help output usage information
76 | ```
77 |
78 | For instance
79 |
80 | ```bash
81 | # We can omit the protocol header if has omitted, the protocol header will be `https://`
82 |
83 | # The simplest usage
84 | hiper baidu.com
85 |
86 | # if the url has any parameter, surround the url with double quotes
87 | hiper "baidu.com?a=1&b=2"
88 |
89 | # Load the specified page 100 times
90 | hiper -n 100 "baidu.com?a=1&b=2"
91 |
92 | # Load the specified page 100 times without `cache`
93 | hiper -n 100 "baidu.com?a=1&b=2" --no-cache
94 |
95 | # Load the specified page 100 times without `javascript`
96 | hiper -n 100 "baidu.com?a=1&b=2" --no-javascript
97 |
98 | # Load the specified page 100 times with `headless = false`
99 | hiper -n 100 "baidu.com?a=1&b=2" -H false
100 |
101 | # Load the specified page 100 times with set `useragent`
102 | hiper -n 100 "baidu.com?a=1&b=2" -u "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36"
103 | ```
104 |
105 | ## Config
106 |
107 | #### Support `.json` and `.js` config
108 |
109 | 1. **json**
110 |
111 | ```javascript
112 | {
113 | // options Pointing to a specific chrome executable, this configuration is generally not required unless you want to test a specific version of chrome
114 | "executablePath": "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
115 | // required The url you want to test
116 | "url": "https://example.com",
117 | // options Cookies required for this test. It's usually a cookie for login information Array | Object
118 | "cookies": [{
119 | "name": "token",
120 | "value": "9+cL224Xh6VuRT",
121 | "domain": "example.com",
122 | "path": "/",
123 | "size": 294,
124 | "httpOnly": true
125 | }],
126 | // options default: 20 Test times
127 | "count": 100,
128 | // options default: true Whether to use headless mode
129 | "headless": true,
130 | // options default: false Disable cache
131 | "noCache": false,
132 | // options default: false Disable javascript
133 | "noJavascript": false,
134 | // options default: false Disable network
135 | "noOnline": false,
136 | // options Set the useragent information
137 | "useragent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36",
138 | // options Set the viewport information
139 | "viewport": {
140 | // options
141 | "width": 375,
142 | // options
143 | "height": 812,
144 | // options default: 1 devicePixelRatio
145 | "deviceScaleFactor": 3,
146 | // options default: false Whether to simulate mobile
147 | "isMobile": false,
148 | // options default: false Whether touch events are supported
149 | "hasTouch": false,
150 | // options default: false Is it horizontal or not
151 | "isLandscape": false
152 | }
153 | }
154 | ```
155 |
156 | 2. **js**
157 |
158 | Having a JS file for config allows people to use ENV variables. For example, let's say I want to test the site on an authenticated state. I can pass some cookie that is used to identify me through ENV variables and having a JS based config file makes this simple. For example
159 |
160 | ```javascript
161 |
162 | module.exports = {
163 | ....
164 | cookies: [{
165 | name: 'token',
166 | value: process.env.authtoken,
167 | domain: 'example.com',
168 | path: '/',
169 | httpOnly: true
170 | }],
171 | ....
172 | }
173 | ```
174 |
175 | ``` bash
176 | # Load the above configuration file (Let's say this file is under /home/)
177 | hiper -c /home/config.json
178 |
179 | # Or you can also use JS files for configuration
180 | hiper -c /home/config.js
181 | ```
182 |
183 | ## Pain point
184 |
185 | After we have developed a project or optimized the performance of a project,
186 |
187 | how do we measure the performance of this project?
188 |
189 | A common approach is to look at the data in the `performance` and `network` in the `Dev Tool`, record a few key performance metrics, and refresh them a few times before looking at those performance metrics,
190 |
191 | Sometimes we find that due to the small sample size, the current **Network/CPU/Memory** load is heavily impacted, and sometimes the optimized project is slower than before the optimization.
192 |
193 | If there is a tool, request web page many times, and then taking out the various performance indicators averaging, we can **very accurately** know the optimization is positive or negative.
194 |
195 | In addition, you can also make a comparison and get **accurate data** about **how much you have optimized**. This tool is designed to solve the pain point.
196 |
197 | > At the same time, this tool is also a good tool for us to learn about the "browser's process of load and rendering" and "performance optimization", so that we don't get wrong conclusions when there are too few samples
198 |
199 | ## Roadmap
200 |
201 | 1. Better documentation
202 | 2. i18n
203 | 3. Increase the analysis statistics of resource items loaded on the page
204 | 4. Statistical reports can be generated
205 | 5. Data visualization
206 |
207 | ## Contributing
208 |
209 | 1. Fork it
210 | 2. Create your feature branch (git checkout -b my-new-feature)
211 | 3. Commit your changes (git commit -am 'Add some feature')
212 | 4. Push to the branch (git push origin my-new-feature)
213 | 5. Create new Pull Request
214 |
215 | ## License
216 |
217 | [MIT](http://opensource.org/licenses/MIT)
218 |
219 | Welcome Star and PR
220 |
221 | Copyright (c) 2018 liyanfeng(pod4g)
222 |
223 |
224 |
225 |
--------------------------------------------------------------------------------
/README.zh-CN.md:
--------------------------------------------------------------------------------
1 | [English](./README.md) | **中文**
2 |
3 | 
4 |
5 | 🚀 令人愉悦的性能统计分析工具
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | ## Hiper
14 |
15 | 可以看成 **Hi** **per**formance的缩写 或者 **Hi**gh **per**formance的缩写
16 |
17 | ## 注意事项
18 |
19 | 请使用英语提issue
20 |
21 | ## 安装
22 |
23 | ``` bash
24 | npm install hiper -g
25 |
26 | # 或者使用 yarn:
27 | # yarn global add hiper
28 | ```
29 |
30 | ## 输出
31 |
32 | 注意: `It takes period (m)s to load ...`. 这个 `period` 是**运行本次测试所用时间**. 因此,-n 越大,这个数越大
33 |
34 | 
35 |
36 | ## 性能指标
37 |
38 | 
39 |
40 | | Key | Value |
41 | | :----------------------------- | :------------------------------------------- |
42 | | DNS查询耗时 | domainLookupEnd - domainLookupStart |
43 | | TCP连接耗时 | connectEnd - connectStart |
44 | | 第一个Byte到达浏览器的用时 | responseStart - requestStart |
45 | | 页面下载耗时 | responseEnd - responseStart |
46 | | DOM Ready之后又继续下载资源的耗时 | domComplete - domInteractive |
47 | | 白屏时间 | domInteractive - navigationStart |
48 | | DOM Ready 耗时 | domContentLoadedEventEnd - navigationStart |
49 | | 页面加载总耗时 | loadEventEnd - navigationStart |
50 |
51 | https://developer.mozilla.org/zh-CN/docs/Web/API/PerformanceTiming
52 |
53 | ## 使用
54 |
55 | ```bash
56 | hiper --help
57 |
58 | Usage: hiper [options] [url]
59 |
60 | 🚀 令人愉悦的性能统计分析工具
61 |
62 | Options:
63 |
64 | -v, --version 输出版本号
65 | -n, --count 指定加载次数(默认20次)
66 | -c, --config 载入指定的配置文件
67 | -u, --useragent 设置useragent
68 | -H, --headless [b] 是否使用无头模式(默认为true)
69 | -e, --executablePath 使用指定的Chrome浏览器
70 | --no-cache 禁用缓存(默认为false)
71 | --no-javascript 禁用JavaScript (默认为false)
72 | --no-online 禁用网络(默认为false)
73 | -h, --help 输出帮助信息
74 | ```
75 |
76 | 用例
77 |
78 | ```bash
79 | # 当我们省略协议头时,默认会在url前添加`https://`
80 |
81 | # 最简单的用法
82 | hiper baidu.com
83 |
84 | # 如何url中含有任何参数,请使用双引号括起来
85 | hiper "baidu.com?a=1&b=2"
86 |
87 | # 加载指定页面100次
88 | hiper -n 100 "baidu.com?a=1&b=2"
89 |
90 | # 禁用缓存加载指定页面100次
91 | hiper -n 100 "baidu.com?a=1&b=2" --no-cache
92 |
93 | # 禁JavaScript加载指定页面100次
94 | hiper -n 100 "baidu.com?a=1&b=2" --no-javascript
95 |
96 | # 使用GUI形式加载指定页面100次
97 | hiper -n 100 "baidu.com?a=1&b=2" -H false
98 |
99 | # 使用指定useragent加载网页100次
100 | hiper -n 100 "baidu.com?a=1&b=2" -u "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36"
101 | ```
102 |
103 | ## 配置
104 |
105 | ### 支持 `.json` 和 `.js` 格式的配置文件
106 |
107 | 1. **json**
108 |
109 | ```javascript
110 | {
111 | // options 指向Chrome可执行程序,一般不需要配置此项,除非你想测试某个特定版本的Chrome
112 | "executablePath": "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
113 | // required 要测试的url
114 | "url": "https://example.com",
115 | // options 本次测试需要用到的Cookies,通常是登录信息(即你测试的页面需要登录) Array | Object
116 | "cookies": [{
117 | "name": "token",
118 | "value": "9+cL224Xh6VuRT",
119 | "domain": "example.com",
120 | "path": "/",
121 | "size": 294,
122 | "httpOnly": true
123 | }],
124 | // options 测试次数 默认为20次
125 | "count": 100,
126 | // options 是否使用无头模式 默认为true
127 | "headless": true,
128 | // options 是否禁用缓存 默认为false
129 | "noCache": false,
130 | // options 是否禁掉JavaScript 默认为false
131 | "noJavascript": false,
132 | // options 是否禁掉网络 默认为false
133 | "noOnline": false,
134 | // options 设置指定的useragent信息
135 | "useragent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36",
136 | // options 设置视口信息
137 | "viewport": {
138 | // options
139 | "width": 375,
140 | // options
141 | "height": 812,
142 | // options devicePixelRatio 默认为1
143 | "deviceScaleFactor": 3,
144 | // options 是否模拟成mobile 默认为false
145 | "isMobile": false,
146 | // options 是否支持touch时间 默认为false
147 | "hasTouch": false,
148 | // options 是否是横屏模式 默认为false
149 | "isLandscape": false
150 | }
151 | }
152 | ```
153 |
154 | 2. **js**
155 |
156 | 配置的JS文件允许人们使用ENV变量。例如,假设你想在经过身份验证的状态下测试站点。你可以通过ENV变量传递一些用于标识你的cookie,有一个基于JS的配置文件使这变得很简单。例如
157 |
158 | ```javascript
159 | module.exports = {
160 | ....
161 | cookies: [{
162 | name: 'token',
163 | value: process.env.authtoken,
164 | domain: 'example.com',
165 | path: '/',
166 | httpOnly: true
167 | }],
168 | ....
169 | }
170 | ```
171 |
172 | ``` bash
173 | # 载入上述配置文件(假设配置文件在/home/下)
174 | hiper -c /home/config.json
175 |
176 | # 或者你也可以使用js文件作为配置文件
177 | hiper -c /home/config.js
178 | ```
179 |
180 | ## 痛点
181 |
182 | 我们开发完一个项目或者给一个项目做完性能优化以后,如何来衡量这个项目的性能是否达标?
183 |
184 | 我们的常见方式是在Dev Tool中的performance和network中看数据,记录下几个关键的性能指标,然后刷新几次再看这些性能指标。
185 |
186 | 有时候我们发现,由于样本太少,受当前「网络」、「CPU」、「内存」的繁忙程度的影响很重,有时优化后的项目反而比优化前更慢。
187 |
188 | 如果有一个工具,一次性地请求N次网页,然后把各个性能指标取出来求平均值,我们就能非常准确地知道这个优化是「正优化」还是「负优化」。
189 |
190 | 并且,也可以做对比,拿到「具体优化了多少」的准确数据。这个工具就是为了解决这个痛点的。
191 |
192 | > 同时,这个工具也是学习「浏览器加载渲染网页过程」和「性能优化」的一个利器,因此我们也可以把他作为一个强大的学习辅助工具,不至于让我们在样本过少的情况下得到错误的结论。
193 |
194 |
195 | ## 蓝图
196 |
197 | 1. 更好的文档
198 | 2. 国际化
199 | 3. 页面依赖资源的统计分析
200 | 4. 生成性能统计报告
201 | 5. 数据可视化
202 |
203 | ## 如何贡献
204 |
205 | 1. Fork it
206 | 2. Create your feature branch (git checkout -b my-new-feature)
207 | 3. Commit your changes (git commit -am 'Add some feature')
208 | 4. Push to the branch (git push origin my-new-feature)
209 | 5. Create new Pull Request
210 |
211 | ## 协议
212 |
213 | [MIT](http://opensource.org/licenses/MIT)
214 |
215 | 欢迎Star和PR
216 |
217 | Copyright (c) 2018 liyanfeng(pod4g)
218 |
219 |
220 |
221 |
--------------------------------------------------------------------------------
/bin/index.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | const path = require('path')
3 | const fork = require('child_process').fork
4 | const argv = process.argv.slice(2)
5 |
6 | fork(path.resolve(__dirname, '../src/index.js'), argv, {
7 | env: {
8 | NODE_NO_WARNINGS: 1
9 | }
10 | })
11 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "_args": {
3 | "count": 20,
4 | "headless": true,
5 | "noCache": false,
6 | "noJavascript": false,
7 | "noOnline": false,
8 | "noBanner": false
9 | },
10 | "author": "liyanfeng(pod4g)",
11 | "email": "pod4dop@gmail.com",
12 | "name": "hiper",
13 | "version": "0.0.16",
14 | "description": "🚀 A statistical analysis tool for performance testing",
15 | "keywords": [
16 | "statistical",
17 | "analysis",
18 | "performance",
19 | "testing",
20 | "tool",
21 | "cli",
22 | "web",
23 | "frontend",
24 | "webpages",
25 | "pages",
26 | "pagespeed",
27 | "websites",
28 | "audits",
29 | "network",
30 | "headless",
31 | "broswer"
32 | ],
33 | "homepage": "https://github.com/pod4g/hiper",
34 | "main": "./src/index.js",
35 | "engines": {
36 | "node": ">=8.0.0",
37 | "npm": ">=5.0.0"
38 | },
39 | "scripts": {
40 | "test": "jest",
41 | "lint": "eslint benchmarks lib test",
42 | "authors": "git log --format='%aN <%aE>' | sort -u"
43 | },
44 | "bin": {
45 | "hiper": "./bin/index.js"
46 | },
47 | "license": "MIT",
48 | "dependencies": {
49 | "chalk": "^2.4.1",
50 | "clear": "^0.1.0",
51 | "clui": "^0.3.6",
52 | "commander": "^2.15.1",
53 | "figlet": "^1.2.0",
54 | "puppeteer": "^1.4.0",
55 | "semver": "^5.5.0"
56 | },
57 | "devDependencies": {
58 | "eslint": "^4.19.1",
59 | "eslint-config-airbnb-base": "^12.1.0",
60 | "eslint-config-standard": "^11.0.0",
61 | "eslint-plugin-import": "^2.12.0",
62 | "eslint-plugin-node": "^6.0.1",
63 | "eslint-plugin-promise": "^3.8.0",
64 | "eslint-plugin-standard": "^3.1.0",
65 | "jest": "^23.1.0"
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/analyzer/index.js:
--------------------------------------------------------------------------------
1 | const Util = require('../util')
2 | /**
3 | * https://developer.mozilla.org/zh-CN/docs/Web/API/Document/readyState
4 | * https://stackoverflow.com/questions/13346746/document-readystate-on-domcontentloaded
5 | * readyState = interactive,可以近似地作为DOM Ready事件
6 | * readyState = complete,可以近似地作为load事件
7 | * DOM Ready事件在「interactive」和「complete」之间执行
8 | * readyState的值解释:
9 | * loading 文档仍在加载
10 | * interactive 文档已经完成加载,文档已被解析,但是诸如图像,样式表和框架之类的子资源仍在加载
11 | * complete 文档和所有子资源已经全部加载完毕。loads事件即将出发。
12 | */
13 | class Analyzer {
14 | constructor (data) {
15 | this.data = data
16 | }
17 | /**
18 | * @param {Number} domainLookupStart 返回用户代理对当前文档所属域进行DNS查询开始的时间。
19 | * 如果此请求没有DNS查询过程,如长连接,资源cache,甚至是本地资源等。 那么就返回 fetchStart的值
20 | * @param {Number} domainLookupEnd 返回用户代理对结束对当前文档所属域进行DNS查询的时间。
21 | * 如果此请求没有DNS查询过程,如长连接,资源cache,甚至是本地资源等。那么就返回 fetchStart的值
22 | * @returns {Number} DNS查询耗时
23 | */
24 | getDNSTime (domainLookupStart, domainLookupEnd) {
25 | return domainLookupEnd - domainLookupStart
26 | }
27 |
28 | /**
29 | * @param {Number} connectStart 返回用户代理向服务器服务器请求文档,开始建立连接的那个时间,
30 | * 如果此连接是一个长连接,又或者直接从缓存中获取资源(即没有与服务器建立连接)。
31 | * 则返回domainLookupEnd的值
32 | * @param {Number} connectEnd 返回用户代理向服务器服务器请求文档,
33 | * 建立连接成功后的那个时间,如果此连接是一个长连接,又或者直接从缓存中获取资源(即没有与服务器建立连接)。
34 | * 则返回domainLookupEnd的值
35 | * @returns {Number} TCP链接耗时
36 | */
37 | getTCPTime (connectStart, connectEnd) {
38 | return connectEnd - connectStart
39 | }
40 | /**
41 | * @param {Number} responseStart 返回用户代理从服务器、缓存、本地资源中,接收到第一个字节数据的时间
42 | * @param {Number} responseEnd 返回用户代理接收到最后一个字符的时间,和当前连接被关闭的时间中,更早的那个。
43 | * 同样,文档可能来自服务器、缓存、或本地资源
44 | * @returns {Number} 网页本身的下载耗时
45 | */
46 | getDownloadTime (responseStart, responseEnd) {
47 | return responseEnd - responseStart
48 | }
49 |
50 | /**
51 | *
52 | * @param {Number} domInteractive 准备加载新页面的起始时间
53 | * @param {Number} domComplete readyState = complete的时候
54 | * @returns {Number} 解析DOM Tree耗时
55 | * 这个说法有点儿不严谨,这个只能当做dom加载完毕以后,子资源的下载耗时,名字起的容易让人误解
56 | */
57 | getAfterDOMReadyTheDownloadTimeOfTheRes (domInteractive, domComplete) {
58 | return domComplete - domInteractive
59 | }
60 | /**
61 | *
62 | * @param {Number} domInteractive 准备加载新页面的起始时间
63 | * @param {Number} responseStart 返回用户代理从服务器、缓存、本地资源中,接收到第一个字节数据的时间
64 | * @returns {Number} 白屏时间
65 | */
66 | getWhiteScreenTime (navigationStart, domInteractive) {
67 | return domInteractive - navigationStart
68 | }
69 | /**
70 | *
71 | * @param {*} navigationStart
72 | * @param {*} domContentLoadedEventEnd
73 | */
74 | getDOMReadyTime (navigationStart, domContentLoadedEventEnd) {
75 | return domContentLoadedEventEnd - navigationStart
76 | }
77 | /**
78 | *
79 | * @param {Number} navigationStart 准备加载新页面的起始时间
80 | * @param {Number} loadEventEnd 文档触发load事件结束后的时间。如果load事件没有触发,那么该接口就返回0
81 | * @returns {Number} DOM Ready耗时
82 | */
83 | getLoadTime (navigationStart, loadEventEnd) {
84 | return loadEventEnd - navigationStart
85 | }
86 | getAverage (total, length) {
87 | return total / length
88 | }
89 | /**
90 | *
91 | * @param {Number} requestStart
92 | * @param {Number} responseStart
93 | * @return {Number} TTFB
94 | * TTFB (Time To First Byte),是最初的网络请求被发起到从服务器接收到第一个字节这段时间,
95 | * 它包含了 TCP连接时间,发送HTTP请求时间和获得响应消息第一个字节的时间。 - 上面是百度百科的解释
96 | * 但是在chrome上,只包含刚开始发送request到接收到第一个byte的时间,
97 | * 发送request前面的预操作则不算在内
98 | */
99 | getTTFB (requestStart, responseStart) {
100 | return responseStart - requestStart
101 | }
102 | statistics (data = this.data) {
103 | if (!data) {
104 | return
105 | }
106 |
107 | if (!Array.isArray(data)) {
108 | data = [ data ]
109 | }
110 |
111 | let length = data.length // 分析次数
112 |
113 | // DNS查询耗时
114 | let totalDNSTime = 0
115 | // TCP链接耗时
116 | let totalTCPTime = 0
117 | // TTFB
118 | let totalTTFBTime = 0
119 | // donwload资源耗时
120 | let totalDownloadTime = 0
121 | // 解析dom树耗时
122 | let totalAfterDOMReadyTheDownloadTimeOfTheRes = 0
123 | // 白屏时间
124 | let totalWhiteScreenTime = 0
125 | // domready时间
126 | let totalDOMReadyTime = 0
127 | // onload时间
128 | let totalLoadTime = 0
129 |
130 | for (let item of data) {
131 | let { total } = JSON.parse(item)
132 | // console.log(entries)
133 | let {
134 | navigationStart,
135 | domainLookupStart,
136 | domainLookupEnd,
137 | connectStart,
138 | connectEnd,
139 | requestStart,
140 | responseStart,
141 | responseEnd,
142 | // domLoading,
143 | domInteractive,
144 | // domContentLoadedEventStart,
145 | domContentLoadedEventEnd,
146 | domComplete,
147 | // loadEventStart,
148 | loadEventEnd
149 | } = total.timing
150 |
151 | totalDNSTime += this.getDNSTime(domainLookupStart, domainLookupEnd)
152 | totalTCPTime += this.getTCPTime(connectStart, connectEnd)
153 | totalTTFBTime += this.getTTFB(requestStart, responseStart)
154 | totalDownloadTime += this.getDownloadTime(responseStart, responseEnd)
155 | totalAfterDOMReadyTheDownloadTimeOfTheRes += this.getAfterDOMReadyTheDownloadTimeOfTheRes(domInteractive, domComplete)
156 | totalWhiteScreenTime += this.getWhiteScreenTime(navigationStart, domInteractive)
157 | totalDOMReadyTime += this.getDOMReadyTime(navigationStart, domContentLoadedEventEnd)
158 | totalLoadTime += this.getLoadTime(navigationStart, loadEventEnd)
159 | }
160 |
161 | // console.log('DNS lookup time:', Util.formatMSToHumanReadable(this.getAverage(totalDNSTime, length)))
162 | // console.log('TCP connect time:', Util.formatMSToHumanReadable(this.getAverage(totalTCPTime, length)))
163 | // console.log('TTFB:', Util.formatMSToHumanReadable(this.getAverage(totalTTFBTime, length)))
164 | // console.log('Download time of the page:', Util.formatMSToHumanReadable(this.getAverage(totalDownloadTime, length)))
165 | // console.log('After DOM Ready the download time of resources:', Util.formatMSToHumanReadable(this.getAverage(totalAfterDOMReadyTheDownloadTimeOfTheRes, length)))
166 | // console.log('White screen time:', Util.formatMSToHumanReadable(this.getAverage(totalWhiteScreenTime, length)))
167 | // console.log('DOM Ready time:', Util.formatMSToHumanReadable(this.getAverage(totalDOMReadyTime, length)))
168 | // console.log('Load time:', Util.formatMSToHumanReadable(this.getAverage(totalLoadTime, length)))
169 | // console.log('DNS查询耗时:', Util.formatMSToHumanReadable(this.getAverage(totalDNSTime, length)))
170 | // console.log('TCP连接耗时:', Util.formatMSToHumanReadable(this.getAverage(totalTCPTime, length)))
171 | // console.log('TTFB:', Util.formatMSToHumanReadable(this.getAverage(totalTTFBTime, length)))
172 | // console.log('页面下载耗时:', Util.formatMSToHumanReadable(this.getAverage(totalDownloadTime, length)))
173 | // console.log('白屏时间:', Util.formatMSToHumanReadable(this.getAverage(totalWhiteScreenTime, length)))
174 | // console.log('DOM Ready耗时:', Util.formatMSToHumanReadable(this.getAverage(totalDOMReadyTime, length)))
175 | // console.log('DOM Ready之后继续进行资源下载的耗时:', Util.formatMSToHumanReadable(this.getAverage(totalAfterDOMReadyTheDownloadTimeOfTheRes, length)))
176 | // console.log('Load时间:', Util.formatMSToHumanReadable(this.getAverage(totalLoadTime, length)))
177 | // console.log(`\n`)
178 |
179 | return {
180 | total: {
181 | dnsTime: Util.formatMSToHumanReadable(this.getAverage(totalDNSTime, length)),
182 | tcpTime: Util.formatMSToHumanReadable(this.getAverage(totalTCPTime, length)),
183 | TTFB: Util.formatMSToHumanReadable(this.getAverage(totalTTFBTime, length)),
184 | pageDownloadTime: Util.formatMSToHumanReadable(this.getAverage(totalDownloadTime, length)),
185 | whiteScreenTime: Util.formatMSToHumanReadable(this.getAverage(totalWhiteScreenTime, length)),
186 | DOMReadyTime: Util.formatMSToHumanReadable(this.getAverage(totalDOMReadyTime, length)),
187 | afterDOMReadyDownloadTime: Util.formatMSToHumanReadable(this.getAverage(totalAfterDOMReadyTheDownloadTimeOfTheRes, length)),
188 | loadTime: Util.formatMSToHumanReadable(this.getAverage(totalLoadTime, length))
189 | }
190 | }
191 | }
192 | }
193 |
194 | module.exports = Analyzer
195 |
--------------------------------------------------------------------------------
/src/cli/index.js:
--------------------------------------------------------------------------------
1 | const program = require('commander')
2 | const pjson = require('../../package.json')
3 | const path = require('path')
4 | const Util = require('../util')
5 |
6 | const {
7 | _args,
8 | version,
9 | description
10 | } = pjson
11 |
12 | module.exports = class Cli {
13 | parseJSONFile (filePath) {
14 | filePath = path.isAbsolute(filePath) ? filePath : path.join(process.cwd(), filePath)
15 | let data = null
16 | try {
17 | data = require(filePath)
18 | if (data) {
19 | let { noBanner, noCache, noJavascript, noOnline } = data
20 | data.banner = !noBanner
21 | data.cache = !noCache
22 | data.javascript = !noJavascript
23 | data.online = !noOnline
24 | delete data.noBanner
25 | delete data.noCache
26 | delete data.noJavascript
27 | delete data.noOnline
28 | }
29 | } catch (error) {
30 | console.log(error)
31 | }
32 | return data
33 | }
34 |
35 | headless (b) {
36 | if (b === 'true') b = true
37 | if (b === 'false') b = false
38 | return b
39 | }
40 |
41 | monitor () {
42 | let url = null
43 | program
44 | .version(version, '-v, --version')
45 | .usage('[options] [url]')
46 | .description(description)
47 | .arguments('')
48 | .action(u => url = u) // eslint-disable-line
49 | .option('-n, --count ', 'specified loading times (default: 20)', parseInt)
50 | .option('-c, --config ', 'load the configuration file', this.parseJSONFile)
51 | .option('-u, --useragent ', 'to set the useragent')
52 | .option('-H, --headless [b]', 'whether to use headless mode (default: true)', this.headless)
53 | .option('-e, --executablePath ', 'use the specified chrome browser')
54 | .option('--no-banner', 'disable banner (default: false)')
55 | .option('--no-cache', 'disable cache (default: false)')
56 | .option('--no-javascript', 'disable javascript (default: false)')
57 | .option('--no-online', 'disable network (defalut: false)')
58 | .parse(process.argv)
59 |
60 | let {
61 | executablePath,
62 | count,
63 | config,
64 | headless,
65 | useragent,
66 | banner,
67 | cache,
68 | javascript,
69 | online
70 | } = program
71 |
72 | if (!config) config = {}
73 |
74 | url = Util.urlNormalize(url || config.url)
75 | // 给cli参数赋予默认值
76 | if (!count) {
77 | count = config.count || _args.count
78 | }
79 |
80 | if (useragent == null) {
81 | useragent = config.useragent
82 | }
83 |
84 | if (headless == null) {
85 | headless = config.headless || _args.headless
86 | }
87 |
88 | if (banner == null) {
89 | banner = config.banner || !_args.banner
90 | }
91 |
92 | if (cache == null) {
93 | cache = config.cache || !_args.noCache
94 | }
95 |
96 | if (javascript == null) {
97 | javascript = config.javascript || !_args.noJavascript
98 | }
99 |
100 | if (online == null) {
101 | online = config.online || !_args.noOnline
102 | }
103 |
104 | if (executablePath == null) {
105 | executablePath = config.executablePath
106 | }
107 |
108 | if (config.viewport) {
109 | config.viewport.deviceScaleFactor = config.viewport.deviceScaleFactor || 1
110 | config.viewport.isMobile = config.viewport.isMobile || false
111 | config.viewport.hasTouch = config.viewport.hasTouch || false
112 | config.viewport.isLandscape = config.viewport.isLandscape || false
113 | }
114 |
115 | if (config.cookies && !Array.isArray(config.cookies)) {
116 | config.cookies = [config.cookies]
117 | }
118 |
119 | let opts = Object.assign(config, {
120 | executablePath,
121 | url,
122 | count,
123 | headless,
124 | useragent,
125 | banner,
126 | cache,
127 | javascript,
128 | online
129 | })
130 |
131 | global.__hiper__ = opts
132 |
133 | return opts
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | const semver = require('semver')
2 | const chalk = require('chalk')
3 | const requiredNodeVersion = require('../package.json').engines.node
4 |
5 | if (!semver.satisfies(process.version, requiredNodeVersion)) {
6 | console.log(chalk.red(
7 | `\n[Hiper] Minimum Node version not met:` +
8 | `\nYou are using Node ${process.version}, but Hiper ` +
9 | `requries Node ${requiredNodeVersion}.\nPlease upgrade your Node version.\n`
10 | ))
11 | process.exit(1)
12 | }
13 |
14 | // 接受cli参数
15 | // 装配opts
16 | // 调用broswer拿到数据
17 | // 调用分析模块
18 | // 调用output
19 | /**
20 | * Module dependencies.
21 | */
22 | // 命令行对象
23 | const Cli = require('../src/cli')
24 | const Outputer = require('../src/output')
25 | // 性能数据生成对象
26 | const Performance = require('../src/performance')
27 | // 统计分析对象
28 | const Analyzer = require('../src/analyzer')
29 | const cli = new Cli()
30 | const performance = new Performance()
31 | const analyzer = new Analyzer()
32 | const outputer = new Outputer()
33 |
34 | // 监听命令行
35 | let opts = cli.monitor()
36 | performance.run(opts).then(async statisticData => {
37 | let data = await analyzer.statistics(statisticData)
38 | // console.log('data:', data)
39 | outputer.output(data)
40 | })
41 |
42 | // console.log(JSON.stringify(opts))
43 |
--------------------------------------------------------------------------------
/src/output/index.js:
--------------------------------------------------------------------------------
1 | const figlet = require('figlet')
2 | const clui = require('clui')
3 | const clc = require('cli-color')
4 | const Line = clui.Line
5 |
6 | module.exports = class Outputer {
7 | output (data) {
8 | if (!data) return
9 | let { total } = data
10 | let {
11 | dnsTime,
12 | tcpTime,
13 | TTFB,
14 | pageDownloadTime,
15 | whiteScreenTime,
16 | DOMReadyTime,
17 | afterDOMReadyDownloadTime,
18 | loadTime
19 | } = total
20 | if (global.__hiper__.banner) {
21 | console.log('\n')
22 | console.log(figlet.textSync('Hiper'))
23 | console.log('\n')
24 | // console.log(`🚀 加载 ${global.__hiper__.url} ${global.__hiper__.count} 次 用时 ${(global.__hiper__.runInterval) / 1000} s`)
25 | console.log(`🚀 It takes ${(global.__hiper__.runInterval) / 1000} s to load \`${global.__hiper__.url}\` ${global.__hiper__.count} times`)
26 | console.log('\n')
27 | }
28 | new Line()
29 | .padding(2)
30 | .column('Run interval', 32)
31 | .column(`${(global.__hiper__.runInterval) / 1000} s`, 20, [clc.cyan])
32 | .fill()
33 | .output()
34 | new Line()
35 | .padding(2)
36 | .column('Total load times', 32)
37 | .column(global.__hiper__.count.toString(), 20, [clc.cyan])
38 | .fill()
39 | .output()
40 | new Line()
41 | .padding(2)
42 | .column('DNS lookup time', 32)
43 | .column(dnsTime, 20, [clc.cyan])
44 | .fill()
45 | .output()
46 | new Line()
47 | .padding(2)
48 | .column('TCP connect time', 32)
49 | .column(tcpTime, 20, [clc.cyan])
50 | .fill()
51 | .output()
52 | new Line()
53 | .padding(2)
54 | .column('TTFB', 32)
55 | .column(TTFB, 20, [clc.cyan])
56 | .fill()
57 | .output()
58 | new Line()
59 | .padding(2)
60 | .column('Download time of the page', 32)
61 | .column(pageDownloadTime, 20, [clc.cyan])
62 | .fill()
63 | .output()
64 | new Line()
65 | .padding(2)
66 | .column('After DOM Ready download time', 32)
67 | .column(afterDOMReadyDownloadTime, 20, [clc.cyan])
68 | .fill()
69 | .output()
70 | new Line()
71 | .padding(2)
72 | .column('White screen time', 32)
73 | .column(whiteScreenTime, 20, [clc.cyan])
74 | .fill()
75 | .output()
76 | new Line()
77 | .padding(2)
78 | .column('DOM Ready time', 32)
79 | .column(DOMReadyTime, 20, [clc.cyan])
80 | .fill()
81 | .output()
82 | new Line()
83 | .padding(2)
84 | .column('Load time', 32)
85 | .column(loadTime, 20, [clc.cyan])
86 | .fill()
87 | .output()
88 | if (global.__hiper__.banner) {
89 | console.log('\n')
90 | }
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/src/performance/index.js:
--------------------------------------------------------------------------------
1 | const puppeteer = require('puppeteer')
2 |
3 | module.exports = class Performance {
4 | constructor (opts) {
5 | this.opts = opts
6 | }
7 |
8 | async run (opts = this.opts) {
9 | let startTimestamp = Date.now()
10 | let {
11 | executablePath,
12 | url,
13 | count,
14 | headless,
15 | useragent,
16 | viewport,
17 | cookies,
18 | cache,
19 | javascript,
20 | online
21 | } = opts
22 |
23 | let launchOpts = {
24 | headless
25 | // args: ['--unlimited-storage', '--full-memory-crash-report']
26 | }
27 |
28 | if (executablePath) {
29 | launchOpts.executablePath = executablePath
30 | }
31 |
32 | const browser = await puppeteer.launch(launchOpts)
33 | let tab = await browser.newPage()
34 | let loadTasks = []
35 | let loadEvents = []
36 | let settingTasks = [
37 | tab.setCacheEnabled(cache),
38 | tab.setJavaScriptEnabled(javascript),
39 | tab.setOfflineMode(!online),
40 | tab.setRequestInterception(false)
41 | ]
42 | if (cookies) {
43 | settingTasks.push(tab.setCookie(...cookies))
44 | }
45 | if (viewport) {
46 | settingTasks.push(tab.setViewport(viewport))
47 | }
48 | if (useragent) {
49 | settingTasks.push(tab.setUserAgent(useragent))
50 | }
51 | await Promise.all(settingTasks)
52 | for (let i = 0; i < count; i++) {
53 | loadTasks.push(
54 | tab.goto(url, { timeout: 172800000, waitUntil: 'load' })
55 | )
56 | let loadHandler = () => {
57 | loadEvents.push(tab.evaluate(() => {
58 | let total = window.performance
59 | let entries = total.getEntries()
60 | return JSON.stringify({ total, entries })
61 | }))
62 | tab.removeListener('load', loadHandler)
63 | }
64 | tab.on('load', loadHandler)
65 | }
66 | await Promise.all(loadTasks)
67 | let performances = await Promise.all(loadEvents)
68 | setTimeout(() => browser.close())
69 | global.__hiper__.runInterval = Date.now() - startTimestamp
70 | // console.log(`跑完 ${global.__hiper__.url} 全部性能测试用时:${(Date.now() - startTimestamp) / 1000}s`)
71 | // console.log(`\n---------------------- 🚀 各项指标平均耗时(${global.__hiper__.count}次)----------------------\n`)
72 | return performances
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/util/index.js:
--------------------------------------------------------------------------------
1 | module.exports = class Util {
2 | /**
3 | * @param {Number} ms
4 | * 把毫秒数转化为人类可读的字符串
5 | */
6 | static formatMSToHumanReadable (ms, readable = true) {
7 | let ret = `${(ms).toFixed(2)} ms`
8 | if (!readable) return ret
9 | const ONE_SECOND = 1000
10 | const ONE_MINUTE = 60 * ONE_SECOND
11 | const ONE_HORE = 60 * ONE_MINUTE
12 | // 小于1秒,那么用毫秒为单位
13 | if (ms >= ONE_SECOND && ms < ONE_MINUTE) {
14 | // 大于一秒小于一分钟,用秒作为单位
15 | ret = `${(ms / 1000).toFixed(2)} s`
16 | } else if (ms >= ONE_MINUTE && ms < ONE_HORE) {
17 | // 大于一分钟,小于一小时,用分钟作单位
18 | ret = `${(ms / 1000 / 60).toFixed(2)} m`
19 | } else if (ms >= ONE_HORE) {
20 | // 大于一个小时,用小时作单位
21 | ret = `${(ms / 1000 / 60 / 60).toFixed(2)} h`
22 | }
23 | return ret
24 | }
25 |
26 | static urlNormalize (url) {
27 | if (!url) return ''
28 | if (url.startsWith('//')) {
29 | return `https:${url}`
30 | }
31 | if (!/^https?:\/\//.test(url)) {
32 | return `https://${url}`
33 | }
34 | return url
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/test/Emitter.test.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-undef */
2 | const EventEmitter = require('events').EventEmitter
3 | const emitter = new EventEmitter()
4 | emitter.setMaxListeners(200)
5 | test('EventEmitter', () => {
6 | let count = 0
7 | let i = 0
8 | for (i = 0; i < 300; i++) {
9 | emitter.on('test', () => {
10 | count++
11 | })
12 | }
13 | emitter.emit('test')
14 | console.log('执行次数:', count)
15 | expect(count).toBe(i)
16 | })
17 |
--------------------------------------------------------------------------------
/test/Util.test.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-undef */
2 | const Util = require('../src/util')
3 | test('Util.formatMSToHumanReadable', () => {
4 | expect(Util.formatMSToHumanReadable(331)).toBe('331.00 ms')
5 | expect(Util.formatMSToHumanReadable(1000)).toBe('1.00 s')
6 | expect(Util.formatMSToHumanReadable(2112)).toBe('2.11 s')
7 | expect(Util.formatMSToHumanReadable(2116)).toBe('2.12 s')
8 | expect(Util.formatMSToHumanReadable(3600000)).toBe('1.00 h')
9 | expect(Util.formatMSToHumanReadable(3600011)).toBe('1.00 h')
10 | expect(Util.formatMSToHumanReadable(7200000)).toBe('2.00 h')
11 | expect(Util.formatMSToHumanReadable(7200000)).toBe('2.00 h')
12 | })
13 |
14 | test('Util.urlNormalize', () => {
15 | expect(Util.urlNormalize(null)).toBe('')
16 | expect(Util.urlNormalize(void 0)).toBe('')
17 | expect(Util.urlNormalize('')).toBe('')
18 | expect(Util.urlNormalize('http://www.diaox2.com')).toBe('http://www.diaox2.com')
19 | expect(Util.urlNormalize('https://www.diaox2.com')).toBe('https://www.diaox2.com')
20 | expect(Util.urlNormalize('www.diaox2.com')).toBe('http://www.diaox2.com')
21 | expect(Util.urlNormalize('//www.diaox2.com')).toBe('http://www.diaox2.com')
22 | })
23 |
--------------------------------------------------------------------------------