├── .babelrc
├── .eslintignore
├── .eslintrc
├── .gitignore
├── .nvmrc
├── .prettierignore
├── .prettierrc.yml
├── .storybook
├── .babelrc
├── addons.js
├── config.js
└── webpack.config.js
├── .travis.yml
├── .vscode
└── settings.json
├── LICENSE
├── README.md
├── images
└── stats-demo.gif
├── jest.config.js
├── package-lock.json
├── package.json
├── src
├── Panel.ts
├── Stats.ts
├── StatsGraph.tsx
├── createStats.ts
├── index.ts
└── utils.ts
├── stories
└── index.stories.js
└── tsconfig.json
/.babelrc:
--------------------------------------------------------------------------------
1 | {"presets": ["@helpscout/zero/babel"]}
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | src/typings/**/*
2 | stories
3 | docs
4 | dist
5 | coverage
6 | other
7 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {"extends": "./node_modules/@helpscout/zero/eslint.js"}
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | .eslintcache
4 | dist
5 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .storybook
3 | .vscode
4 | stories
5 | node_modules
6 | coverage
7 | src
8 | .babelrc
9 | .eslintrc
10 | .eslintignore
11 | .prettierignore
12 | .prettierrc.yml
13 | .travis.yml
14 | rollup.config.js
15 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | package.json
2 | package-lock.json
3 | dist/
4 | coverage/
5 |
--------------------------------------------------------------------------------
/.prettierrc.yml:
--------------------------------------------------------------------------------
1 | semi: false
2 | singleQuote: true
3 | trailingComma: es5
--------------------------------------------------------------------------------
/.storybook/.babelrc:
--------------------------------------------------------------------------------
1 | { "presets": ["@helpscout/zero/babel"] }
2 |
--------------------------------------------------------------------------------
/.storybook/addons.js:
--------------------------------------------------------------------------------
1 | import '@storybook/addon-actions/register'
2 | import '@storybook/addon-links/register'
3 |
--------------------------------------------------------------------------------
/.storybook/config.js:
--------------------------------------------------------------------------------
1 | import {configure} from '@storybook/react'
2 |
3 | // automatically import all files ending in *.stories.js
4 | const req = require.context('../stories', true, /.stories.js$/)
5 | function loadStories() {
6 | req.keys().forEach(filename => req(filename))
7 | }
8 |
9 | configure(loadStories, module)
10 |
--------------------------------------------------------------------------------
/.storybook/webpack.config.js:
--------------------------------------------------------------------------------
1 | module.exports = (baseConfig, env, config) => {
2 | // Typescript
3 | config.module.rules.push({
4 | test: /\.(ts|tsx)$/,
5 | exclude: /__tests__/,
6 | use: {
7 | loader: require.resolve('babel-loader'),
8 | options: {
9 | presets: ['@helpscout/zero/babel'],
10 | },
11 | },
12 | })
13 | config.resolve.extensions.push('.ts', '.tsx')
14 | return config
15 | }
16 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - '8'
4 |
5 | cache:
6 | directories:
7 | - node_modules
8 |
9 | install:
10 | - npm install
11 |
12 | script:
13 | - npm run build
14 |
15 | branches:
16 | only:
17 | - master
18 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.formatOnSave": true
3 | }
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2018 Help Scout
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
13 | all 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
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 📊 Stats
2 |
3 | [](https://travis-ci.org/helpscout/stats)
4 | [](https://badge.fury.io/js/%40helpscout%2Fstats)
5 |
6 | > Easy performance monitoring for JavaScript / React
7 |
8 | 
9 |
10 | ## Table of Contents
11 |
12 |
13 |
14 |
15 | - [Installation](#installation)
16 | - [Usage](#usage)
17 | - [JavaScript](#javascript)
18 | - [React](#react)
19 | - [Graphs](#graphs)
20 | - [Options](#options)
21 | - [Thanks](#thanks)
22 |
23 |
24 |
25 | ## Installation
26 |
27 | Add `stats` to your project via `npm install`:
28 |
29 | ```
30 | npm install --save @helpscout/stats
31 | ```
32 |
33 | ## Usage
34 |
35 | ### JavaScript
36 |
37 | To use Stats in your JavaScript project, simply import it and instantiate!
38 |
39 | ```js
40 | import createStats from '@helpscout/stats'
41 |
42 | const stats = createStats()
43 | // Stats will automatically mount to window.document
44 |
45 | // For clean up, execute the destroy() method
46 | stats.destroy()
47 | ```
48 |
49 | ### React
50 |
51 | Stats comes with a handy `` component. To add it to your React project, import it and render it:
52 |
53 | ```jsx
54 | import React from 'react'
55 | import {StatsGraph} from '@helpscout/stats'
56 |
57 | class App extends React.Component {
58 | render() {
59 | return (
60 |
61 | ...
62 |
63 | ...
64 |
65 | )
66 | }
67 | }
68 |
69 | export default App
70 | ```
71 |
72 | `StatsGraph` cleans up after itself if it unmounts.
73 |
74 | ## Graphs
75 |
76 | - **FPS** Frames rendered in the last second. The higher the number the better.
77 | - **MB** MBytes of allocated memory. (Run Chrome with --enable-precise-memory-info)
78 | - **NODES** Number of DOM nodes in `window.document` (including iFrame nodes).
79 |
80 | ## Options
81 |
82 | Stats accepts a handful of options to adjust it's position and UI.
83 |
84 | | Prop | Type | Default | Description |
85 | | -------- | ----------------- | -------- | --------------------------- |
86 | | top | `number`/`string` | 0 | (CSS) top position. |
87 | | right | `number`/`string` | 0 | (CSS) right position. |
88 | | bottom | `number`/`string` | 0 | (CSS) bottom position. |
89 | | left | `number`/`string` | 0 | (CSS) left position. |
90 | | opacity | `number` | 0.5 | Opacity for the Stats UI. |
91 | | position | `string` | fixed | Position for the Stats UI. |
92 | | zIndex | `string` | 99999999 | `z-index` for the Stats UI. |
93 |
94 | The React `StatsGraph` uses the same options for it's `defaultProps`
95 |
96 | ## Thanks
97 |
98 | Thanks for [mrdoob](https://github.com/mrdoob) for his [stats.js](https://github.com/mrdoob/stats.js) library, which inspired this one!
99 |
--------------------------------------------------------------------------------
/images/stats-demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/helpscout/stats/b3cc292f0f8fa2faf6015a8b070b3d6f39e5f68b/images/stats-demo.gif
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | const jestConfig = require('@helpscout/zero/jest')
2 |
3 | const coverageList = [
4 | 'src/**/*.{js,jsx,ts,tsx}',
5 | '!src/createBroadcast.{js,jsx,ts}',
6 | ]
7 |
8 | module.exports = Object.assign({}, jestConfig, {
9 | collectCoverageFrom: []
10 | .concat(jestConfig.collectCoverageFrom)
11 | .concat(coverageList),
12 | // setupTestFrameworkScriptFile: '/scripts/setupTests.js',
13 | testEnvironment: 'jsdom',
14 | testURL: 'http://localhost',
15 | })
16 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@helpscout/stats",
3 | "version": "0.0.5",
4 | "description": "Easy performance monitoring for JavaScript / React",
5 | "main": "dist/index.js",
6 | "private": false,
7 | "publishConfig": {
8 | "access": "public"
9 | },
10 | "files": [
11 | "dist",
12 | "README.md",
13 | "LICENSE"
14 | ],
15 | "scripts": {
16 | "add-contributor": "zero contributors add",
17 | "prestart": "zero prestart",
18 | "postbuild": "rm -rf dist/__tests__",
19 | "build": "zero build",
20 | "clean": "rm -rf dist",
21 | "lint": "zero lint",
22 | "dev": "zero test --watchAll",
23 | "test": "npm run build",
24 | "format": "zero format",
25 | "precommit": "zero pre-commit",
26 | "coverage": "nyc report --temp-directory=coverage --reporter=text-lcov | coveralls",
27 | "release": "zero release",
28 | "version": "npm run build",
29 | "start": "npm run storybook",
30 | "storybook": "start-storybook -p 5005",
31 | "build-storybook": "build-storybook",
32 | "pretty": "zero format"
33 | },
34 | "author": "Jon Quach (https://jonquach.com)",
35 | "license": "MIT",
36 | "repository": {
37 | "type": "git",
38 | "url": "git+https://github.com/helpscout/stats.git"
39 | },
40 | "bugs": {
41 | "url": "https://github.com/helpscout/stats/issues"
42 | },
43 | "keywords": [
44 | "stats",
45 | "performance",
46 | "fps",
47 | "react"
48 | ],
49 | "devDependencies": {
50 | "@helpscout/zero": "3.0.2",
51 | "@storybook/addon-actions": "4.1.12",
52 | "@storybook/addon-links": "4.1.12",
53 | "@storybook/addons": "4.1.12",
54 | "@storybook/react": "4.1.12",
55 | "@types/react": "16.8.7",
56 | "babel-loader": "^8.0.5",
57 | "react": "16.8.4",
58 | "react-dom": "16.8.4",
59 | "typescript": "3.3.3333"
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/Panel.ts:
--------------------------------------------------------------------------------
1 | export interface Panel {
2 | dom: HTMLCanvasElement
3 | update: (min: number, max: number) => void
4 | }
5 |
6 | /**
7 | * Generates the UI (Canvas) that renders the Stat graph
8 | * @param name {string} The name of the Panel.
9 | * @param fg {string} The foreground colour (hex).
10 | * @param bg {string} The background colour (hex).
11 | */
12 | function Panel(name: string, fg: string, bg: string): Panel {
13 | let min = Infinity,
14 | max = 0,
15 | round = Math.round
16 | let PR = round(window.devicePixelRatio || 1)
17 |
18 | let WIDTH = 80 * PR,
19 | HEIGHT = 48 * PR,
20 | TEXT_X = 3 * PR,
21 | TEXT_Y = 2 * PR,
22 | GRAPH_X = 3 * PR,
23 | GRAPH_Y = 15 * PR,
24 | GRAPH_WIDTH = 74 * PR,
25 | GRAPH_HEIGHT = 30 * PR
26 |
27 | const canvas = document.createElement('canvas')
28 | canvas.width = WIDTH
29 | canvas.height = HEIGHT
30 | canvas.style.cssText = 'width:80px;height:48px'
31 |
32 | const context = canvas.getContext('2d') as CanvasRenderingContext2D
33 | context.font = 'bold ' + 9 * PR + 'px Helvetica,Arial,sans-serif'
34 | context.textBaseline = 'top'
35 |
36 | context.fillStyle = bg
37 | context.fillRect(0, 0, WIDTH, HEIGHT)
38 |
39 | context.fillStyle = fg
40 | context.fillText(name, TEXT_X, TEXT_Y)
41 | context.fillRect(GRAPH_X, GRAPH_Y, GRAPH_WIDTH, GRAPH_HEIGHT)
42 |
43 | context.fillStyle = bg
44 | context.globalAlpha = 0.9
45 | context.fillRect(GRAPH_X, GRAPH_Y, GRAPH_WIDTH, GRAPH_HEIGHT)
46 |
47 | return {
48 | dom: canvas,
49 |
50 | update: function(value, maxValue) {
51 | min = Math.min(min, value)
52 | max = Math.max(max, value)
53 |
54 | context.fillStyle = bg
55 | context.globalAlpha = 1
56 | context.fillRect(0, 0, WIDTH, GRAPH_Y)
57 | context.fillStyle = fg
58 | context.fillText(
59 | round(value) + ' ' + name + ' (' + round(min) + '-' + round(max) + ')',
60 | TEXT_X,
61 | TEXT_Y
62 | )
63 |
64 | context.drawImage(
65 | canvas,
66 | GRAPH_X + PR,
67 | GRAPH_Y,
68 | GRAPH_WIDTH - PR,
69 | GRAPH_HEIGHT,
70 | GRAPH_X,
71 | GRAPH_Y,
72 | GRAPH_WIDTH - PR,
73 | GRAPH_HEIGHT
74 | )
75 |
76 | context.fillRect(GRAPH_X + GRAPH_WIDTH - PR, GRAPH_Y, PR, GRAPH_HEIGHT)
77 |
78 | context.fillStyle = bg
79 | context.globalAlpha = 0.9
80 | context.fillRect(
81 | GRAPH_X + GRAPH_WIDTH - PR,
82 | GRAPH_Y,
83 | PR,
84 | round((1 - value / maxValue) * GRAPH_HEIGHT)
85 | )
86 | },
87 | }
88 | }
89 |
90 | export default Panel
91 |
--------------------------------------------------------------------------------
/src/Stats.ts:
--------------------------------------------------------------------------------
1 | import { defaultOptions, getTotalNodeCount, toPx } from './utils'
2 | import Panel from './Panel'
3 |
4 | function Stats(options = defaultOptions) {
5 | const { bottom, right, opacity, position, top, left, zIndex } = {
6 | ...defaultOptions,
7 | ...options,
8 | }
9 |
10 | const container = document.createElement('div')
11 | container.style.cssText = `
12 | position:${position};
13 | top:${toPx(top)};
14 | left:${toPx(left)};
15 | bottom:${toPx(bottom)};
16 | right:${toPx(right)};
17 | opacity:${opacity};
18 | z-index:${zIndex};
19 | pointer-events: none;
20 | `
21 |
22 | let beginTime = (performance || Date).now()
23 | let prevTime = beginTime
24 | let frames = 0
25 | let nodes = 0
26 | let maxNodes = 0
27 |
28 | function addPanel(panel) {
29 | container.appendChild(panel.dom)
30 | return panel
31 | }
32 |
33 | function mount() {
34 | window.document.body.appendChild(container)
35 | }
36 |
37 | function unmount() {
38 | if (container.parentNode) {
39 | container.parentNode.removeChild(container)
40 | }
41 | beginTime = (performance || Date).now()
42 | prevTime = beginTime
43 | frames = 0
44 | nodes = 0
45 | maxNodes = 0
46 | }
47 |
48 | // @ts-ignore
49 | const fpsPanel = addPanel(new Panel('FPS', '#0f0', '#020'))
50 | let memPanel
51 |
52 | // @ts-ignore
53 | if (window.performance && window.performance.memory) {
54 | // @ts-ignore
55 | memPanel = addPanel(new Panel('MB', '#0ff', '#002'))
56 | }
57 |
58 | // @ts-ignore
59 | const nodesPanel = addPanel(new Panel('NODES', '#f08', '#201'))
60 |
61 | mount()
62 |
63 | return {
64 | dom: container,
65 |
66 | mount,
67 | unmount,
68 |
69 | begin: function() {
70 | beginTime = (performance || Date).now()
71 | nodes = getTotalNodeCount()
72 |
73 | if (nodes > maxNodes) {
74 | maxNodes = nodes
75 | }
76 | },
77 |
78 | end: function() {
79 | frames++
80 |
81 | let time = (performance || Date).now()
82 |
83 | // msPanel.update(time - beginTime, 200)
84 |
85 | if (time >= prevTime + 1000) {
86 | fpsPanel.update((frames * 1000) / (time - prevTime), 100)
87 |
88 | prevTime = time
89 | frames = 0
90 |
91 | if (memPanel) {
92 | // @ts-ignore
93 | let memory = performance.memory
94 | memPanel.update(
95 | memory.usedJSHeapSize / 1048576,
96 | memory.jsHeapSizeLimit / 1048576
97 | )
98 | }
99 |
100 | nodesPanel.update(nodes, maxNodes * 2)
101 | }
102 |
103 | return time
104 | },
105 |
106 | update: function() {
107 | beginTime = this.end()
108 | },
109 | }
110 | }
111 |
112 | export default Stats
113 |
--------------------------------------------------------------------------------
/src/StatsGraph.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import createStats from './createStats'
3 | import { defaultOptions as defaultProps } from './utils'
4 |
5 | export class StatsGraph extends React.PureComponent {
6 | static defaultProps = defaultProps
7 | stats: any
8 |
9 | componentDidMount() {
10 | this.stats = createStats({ ...this.props })
11 | }
12 |
13 | componentWillUnmount() {
14 | if (this.stats) {
15 | this.stats.destroy()
16 | }
17 | }
18 |
19 | render() {
20 | return null
21 | }
22 | }
23 |
24 | export default StatsGraph
25 |
--------------------------------------------------------------------------------
/src/createStats.ts:
--------------------------------------------------------------------------------
1 | import Stats from './Stats'
2 |
3 | let __secretStats: any = null
4 | let __secretStatsRAF: any = null
5 |
6 | function destroy() {
7 | if (__secretStats && __secretStatsRAF) {
8 | cancelAnimationFrame(__secretStatsRAF)
9 | __secretStats.unmount()
10 | }
11 | }
12 |
13 | function createStats(options) {
14 | destroy()
15 | // @ts-ignore
16 | __secretStats = new Stats(options)
17 | __secretStats.destroy = destroy
18 |
19 | function tick() {
20 | __secretStats.begin()
21 | __secretStats.end()
22 | __secretStatsRAF = requestAnimationFrame(tick)
23 | }
24 |
25 | __secretStatsRAF = requestAnimationFrame(tick)
26 |
27 | return __secretStats
28 | }
29 |
30 | export default createStats
31 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import createStats from './createStats'
2 | export { default as StatsGraph } from './StatsGraph'
3 |
4 | export default createStats
5 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | export const defaultOptions = {
2 | top: 0,
3 | left: 'initial',
4 | right: 0,
5 | bottom: 'initial',
6 | opacity: 0.5,
7 | position: 'fixed',
8 | zIndex: 99999999,
9 | }
10 |
11 | export const toPx = value => (typeof value === 'number' ? `${value}px` : value)
12 |
13 | export function getTotalNodeCountFromDocument(
14 | doc: Document | null = window.document
15 | ): number {
16 | if (!doc) return 0
17 | return doc.getElementsByTagName('*').length
18 | }
19 |
20 | export function getTotalNodeCountFromFrames(): number {
21 | let i = 0
22 | let total = 0
23 | const frameNodes = window.document.getElementsByTagName('iframe')
24 | const frames = Array.from(frameNodes)
25 |
26 | while (i < frames.length) {
27 | if (frames[i].contentDocument) {
28 | total = total + getTotalNodeCountFromDocument(frames[i].contentDocument)
29 | }
30 | i++
31 | }
32 |
33 | return total
34 | }
35 |
36 | export function getTotalNodeCount(): number {
37 | const rootCount = getTotalNodeCountFromDocument(window.document)
38 | const frameCount = getTotalNodeCountFromFrames()
39 |
40 | return rootCount + frameCount
41 | }
42 |
--------------------------------------------------------------------------------
/stories/index.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { storiesOf } from '@storybook/react'
3 | import { linkTo } from '@storybook/addon-links'
4 |
5 | import { Welcome } from '@storybook/react/demo'
6 | import StatsGraph from '../src/StatsGraph'
7 |
8 | storiesOf('Welcome', module).add('to Storybook', () => (
9 |
10 |
11 |
12 |
13 | ))
14 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowJs": false,
4 | "allowSyntheticDefaultImports": true,
5 | "baseUrl": "src",
6 | "declaration": true,
7 | "experimentalDecorators": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "jsx": "react",
10 | "lib": ["es5", "dom", "es2017"],
11 | "module": "commonjs",
12 | "moduleResolution": "node",
13 | "noImplicitAny": false,
14 | "noImplicitReturns": true,
15 | "noImplicitThis": false,
16 | "noUnusedLocals": false,
17 | "outDir": "dist",
18 | "rootDirs": ["src", "stories"],
19 | "strictNullChecks": true,
20 | "suppressImplicitAnyIndexErrors": true,
21 | "target": "es5"
22 | },
23 | "exclude": ["node_modules", "dist"]
24 | }
25 |
--------------------------------------------------------------------------------