├── .gitignore ├── src ├── types │ └── re-reselect │ │ ├── redux.d.ts │ │ └── re-reselect.d.ts ├── spy-performance │ ├── summary │ │ ├── summary-context.ts │ │ ├── print-summary.ts │ │ └── summaries.ts │ ├── callbacks.ts │ ├── spy-functions │ │ ├── spy-thunk.ts │ │ ├── spy-reducers.ts │ │ ├── spy-function-time.ts │ │ └── spy-selector.ts │ ├── perf-spier.ts │ ├── performance-timer.ts │ └── custom-timer │ │ └── custom-timer.ts ├── resolver │ ├── redux-thunk-module.ts │ ├── redux-module.ts │ ├── re-reselect-module.ts │ └── reselect-module.ts ├── index.ts ├── spy-webpack │ └── webpack-spier.ts └── spy-jest │ └── spy-jest.ts ├── examples └── jest-example │ ├── jest.perf-spy.js │ ├── package.json │ ├── jest.config.js │ ├── custom-timer-jest.js │ ├── reselect-jest.js │ ├── redux-jest.js │ └── re-reselect-jest.js ├── tslint.json ├── tsconfig.json ├── .babelrc.js ├── resolver-fixer.js ├── .github └── workflows │ ├── test.yml │ └── release.yml ├── package.json ├── readme.md └── rollup.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vscode 3 | dist 4 | /resolver 5 | -------------------------------------------------------------------------------- /src/types/re-reselect/redux.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'redux-original' { 2 | export function combineReducers(): void; 3 | } -------------------------------------------------------------------------------- /examples/jest-example/jest.perf-spy.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const { enableSpying, perfStatsReset, getPerfSummary } = require("performance-spy"); 3 | 4 | enableSpying(); 5 | 6 | beforeEach(() => { 7 | perfStatsReset(); 8 | }); 9 | -------------------------------------------------------------------------------- /src/spy-performance/summary/summary-context.ts: -------------------------------------------------------------------------------- 1 | import { Summaries } from "./summaries"; 2 | 3 | export const GlobalSummaries = new Summaries(); 4 | 5 | export const getCurrentSummaryForUse = (): Summaries => { 6 | return GlobalSummaries; 7 | }; 8 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "jsRules": {}, 7 | "rules": { 8 | "only-arrow-functions": false 9 | }, 10 | "rulesDirectory": [] 11 | } -------------------------------------------------------------------------------- /src/resolver/redux-thunk-module.ts: -------------------------------------------------------------------------------- 1 | import rethunk from "redux-thunk-original"; 2 | export * from "redux-thunk-original"; 3 | const _window = typeof window !== "undefined" ? window : null; 4 | 5 | export default _window?.spyThunk ? _window.spyThunk() : rethunk; 6 | -------------------------------------------------------------------------------- /src/resolver/redux-module.ts: -------------------------------------------------------------------------------- 1 | import * as redux from "redux-original"; 2 | 3 | const _window = typeof window !== "undefined" ? window : null; 4 | export * from "redux-original"; 5 | export const combineReducers = _window?.spyReducerCombiner 6 | ? _window.spyReducerCombiner(redux.combineReducers) 7 | : redux.combineReducers; 8 | 9 | -------------------------------------------------------------------------------- /src/spy-performance/summary/print-summary.ts: -------------------------------------------------------------------------------- 1 | import { getCurrentSummaryForUse } from "./summary-context"; 2 | 3 | export const perfStatsReset = function () { 4 | getCurrentSummaryForUse().reset(); 5 | }; 6 | 7 | 8 | export const getPerfSummary = function () { 9 | return getCurrentSummaryForUse().getSummary(); 10 | }; 11 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { spyWebpackAliases } from "./spy-webpack/webpack-spier"; 2 | export { spyJestAliases } from "./spy-jest/spy-jest"; 3 | export { enableSpying } from "./spy-performance/perf-spier"; 4 | export { getPerfSummary, perfStatsReset } from "./spy-performance/summary/print-summary"; 5 | export { startCustomTimer } from "./spy-performance/custom-timer/custom-timer"; 6 | export { subscribeAll } from "./spy-performance/callbacks" -------------------------------------------------------------------------------- /src/resolver/re-reselect-module.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as rereselect from "re-reselect-original"; 3 | 4 | const _window = typeof window !== "undefined" ? window : null; 5 | export * from "re-reselect-original"; 6 | export const createCachedSelector = _window?.spyCreateSelectorTime 7 | ? _window.spyCachedCreatorTime(rereselect.createCachedSelector) 8 | : rereselect.createCachedSelector; 9 | 10 | export default createCachedSelector; 11 | -------------------------------------------------------------------------------- /src/resolver/reselect-module.ts: -------------------------------------------------------------------------------- 1 | import * as reselect from "reselect-original"; 2 | 3 | const _window = typeof window !== "undefined" ? window : null; 4 | export * from "reselect-original"; 5 | export const createSelectorCreator = _window?.spyCreateSelectorTime 6 | ? _window.spyCreateSelectorTime(reselect.createSelectorCreator) 7 | : reselect.createSelectorCreator; 8 | 9 | export const createSelector = _window?.spyCreateSelectorTime 10 | ? createSelectorCreator(reselect.defaultMemoize) 11 | : reselect.createSelector; 12 | -------------------------------------------------------------------------------- /examples/jest-example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jest-example", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "preinstall": "npm install ../../", 8 | "test": "jest" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "@types/jest": "^27.0.3", 14 | "jest": "^27.4.5" 15 | }, 16 | "dependencies": { 17 | "performance-spy": "file:../..", 18 | "re-reselect": "^4.0.0", 19 | "redux": "^4.1.2", 20 | "reselect": "^4.1.5" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "target": "ESNext", 5 | "module": "ESNext", 6 | "moduleResolution": "Node", 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "lib": ["dom", "es2017"], 10 | "jsx": "react", 11 | "declaration": true, 12 | "declarationDir": "./dist/types", 13 | "sourceMap": true, 14 | "removeComments": false, 15 | "noUnusedLocals": true, 16 | "noUnusedParameters": true, 17 | "baseUrl": "./" 18 | }, 19 | "include": ["src/**/*"], 20 | "exclude": ["node_modules", "dist"] 21 | } -------------------------------------------------------------------------------- /.babelrc.js: -------------------------------------------------------------------------------- 1 | const { NODE_ENV } = process.env 2 | 3 | module.exports = { 4 | presets: [ 5 | '@babel/typescript', 6 | [ 7 | '@babel/env', 8 | { 9 | targets: { 10 | browsers: ['ie >= 11'] 11 | }, 12 | exclude: ['transform-async-to-generator', 'transform-regenerator'], 13 | modules: false, 14 | loose: true 15 | } 16 | ] 17 | ], 18 | plugins: [ 19 | // don't use `loose` mode here - need to copy symbols when spreading 20 | '@babel/proposal-object-rest-spread', 21 | NODE_ENV === 'test' && '@babel/transform-modules-commonjs' 22 | ].filter(Boolean) 23 | } -------------------------------------------------------------------------------- /src/spy-performance/callbacks.ts: -------------------------------------------------------------------------------- 1 | export class Observer { 2 | subscribers: any[] 3 | constructor() { 4 | this.subscribers = []; 5 | } 6 | subscribe(fn: any) { 7 | this.subscribers.push(fn); 8 | } 9 | 10 | notify(...args: any) { 11 | this.subscribers.forEach(subscriber => { 12 | subscriber(...args) 13 | }) 14 | } 15 | } 16 | 17 | export const callbacks : {[key: string]: any} = { 18 | selector: new Observer(), 19 | } 20 | 21 | export const subscribeAll = (callbacksToSubscribe = {}) => { 22 | Object.entries(callbacksToSubscribe).forEach(([name, fn]) => { 23 | callbacks[name].subscribe(fn) 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /resolver-fixer.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const fs = require("fs"); 3 | 4 | const resolvers = fs.readdirSync(path.resolve("./src/resolver")).map((p) => path.parse(p).name); 5 | const resolverPath = path.resolve("./resolver"); 6 | 7 | for(const resolver of resolvers) { 8 | console.log(resolver); 9 | const resolverFinalPath = path.join(resolverPath, resolver); 10 | 11 | const packageData = { 12 | name: resolver, 13 | "main": "./dist/lib/index.js", 14 | "jsnext:main": "./dist/es/index.js", 15 | "module": "./dist/es/index.js", 16 | "unpkg": "./dist/dist/index.js" 17 | } 18 | 19 | fs.writeFileSync(path.join(resolverFinalPath, "./package.json"), JSON.stringify(packageData, null, "\t"), {encoding: "utf-8"}) 20 | } 21 | 22 | -------------------------------------------------------------------------------- /src/spy-webpack/webpack-spier.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-console */ 2 | 3 | interface Options { 4 | silent: boolean; 5 | } 6 | 7 | export function spyWebpackAliases(nodemodule: string, aliases: string[], options: Options = { silent: true }) { 8 | const finalAliases: {[key: string]: string} = {}; 9 | const supported = new Set(["redux-thunk", "reselect", "re-reselect", "redux"]) 10 | for(const aliasKey of aliases) { 11 | if(supported.has(aliasKey)) { 12 | finalAliases[aliasKey+"-original"] = nodemodule+"/"+aliasKey; 13 | finalAliases[aliasKey] = nodemodule+"/performance-spy/resolver/"+aliasKey+"-module"; 14 | } 15 | } 16 | 17 | if (!options.silent) { 18 | console.log("Overriden libraries by webpack performance spier", finalAliases); 19 | } 20 | 21 | return finalAliases; 22 | } -------------------------------------------------------------------------------- /src/spy-jest/spy-jest.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-console */ 2 | 3 | interface Options { 4 | silent: boolean; 5 | } 6 | 7 | export const spyJestAliases = (nodemodule: string, aliases: string[], options: Options = { silent: true }) => { 8 | const finalAliases: {[key: string]: string} = {}; 9 | const supported = new Set(["redux-thunk", "reselect", "re-reselect", "redux"]) 10 | for(const aliasKey of aliases) { 11 | if(supported.has(aliasKey)) { 12 | finalAliases[aliasKey+"-original(.*)"] = nodemodule+"/"+aliasKey+"$1"; 13 | finalAliases[aliasKey+"(.*)"] = nodemodule+"/performance-spy/resolver/"+aliasKey+"-module$1"; 14 | } 15 | } 16 | 17 | if (!options.silent) { 18 | console.log("Overriden libraries by jest performance spier", finalAliases); 19 | } 20 | 21 | return finalAliases; 22 | } -------------------------------------------------------------------------------- /src/spy-performance/spy-functions/spy-thunk.ts: -------------------------------------------------------------------------------- 1 | import { Dispatch } from "redux"; 2 | import { ThunkDispatch } from "redux-thunk"; 3 | import { spyFunctionTime } from "./spy-function-time"; 4 | 5 | export const spyThunk = function () { 6 | function createThunkMiddleware(extraArgument?: any) { 7 | const middleware = ({ 8 | dispatch, 9 | getState 10 | }: { 11 | dispatch: ThunkDispatch; 12 | getState: () => any 13 | }) => 14 | (next: Dispatch) => 15 | (action: any) => { 16 | if (typeof action === "function") { 17 | const key = action.toString().substr(0, 600); 18 | return spyFunctionTime(action, summary => summary.dispatchPerfStat, key)(dispatch, getState, extraArgument); 19 | } else if (typeof action === "object" && action.type) { 20 | return spyFunctionTime(next, summary => summary.dispatchPerfStat, action.type)(action); 21 | } 22 | 23 | return next(action); 24 | }; 25 | 26 | middleware.withExtraArgument = createThunkMiddleware; 27 | return middleware; 28 | } 29 | 30 | return createThunkMiddleware(); 31 | }; -------------------------------------------------------------------------------- /examples/jest-example/jest.config.js: -------------------------------------------------------------------------------- 1 | const { spyJestAliases } = require("performance-spy"); 2 | 3 | module.exports = { 4 | // Automatically clear mock calls and instances between every test 5 | clearMocks: true, 6 | 7 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 8 | maxWorkers: "50%", 9 | 10 | // An array of file extensions your modules use 11 | moduleFileExtensions: ["js", "jsx", "cjsx", "json"], 12 | 13 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module 14 | moduleNameMapper: { 15 | ...spyJestAliases("/node_modules", ["redux-thunk", "re-reselect", "reselect", "redux"]) 16 | }, 17 | // The root directory that Jest should scan for tests and modules within 18 | rootDir: "./", 19 | 20 | // A list of paths to directories that Jest should use to search for files in 21 | roots: [""], 22 | 23 | setupFilesAfterEnv: [ 24 | "./jest.perf-spy.js" 25 | ], 26 | testEnvironment: "jsdom", 27 | testMatch: [`**/?(*)+(-jest).js?(x)`], 28 | 29 | watchman: false 30 | }; 31 | -------------------------------------------------------------------------------- /src/spy-performance/spy-functions/spy-reducers.ts: -------------------------------------------------------------------------------- 1 | import { spyFunctionTime } from "./spy-function-time"; 2 | import { PerfStatsStuff } from "../performance-timer"; 3 | 4 | type GenericFunction = (...args: any[]) => any; 5 | 6 | function resultsChecker(_ref: any, res: any, args: IArguments, perfstat: PerfStatsStuff, key: string) { 7 | const [lastState, action] = args; 8 | if (res !== lastState) { 9 | (perfstat.getSubStat(key, "args") as PerfStatsStuff).forwardStat("action_" + action?.type, "count"); 10 | } 11 | } 12 | const spyReducers = function (reducers: {[key: string]: GenericFunction}) { 13 | const spidesReducers: {[key: string]: GenericFunction} = {}; 14 | Object.keys(reducers).forEach(name => { 15 | if (typeof reducers[name] === "function") { 16 | spidesReducers[name] = spyFunctionTime(reducers[name], summary => summary.reducersPerfStat, name, { resultsChecker }); 17 | } else { 18 | spidesReducers[name] = reducers[name]; 19 | } 20 | }); 21 | return spidesReducers; 22 | }; 23 | 24 | export const spyReducerCombiner = function (reducerCombiner: GenericFunction) { 25 | return function (...args: any[]) { 26 | args[0] = spyReducers(args[0]); 27 | return reducerCombiner(...args); 28 | }; 29 | }; -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | 6 | jobs: 7 | test: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: actions/setup-node@v1 12 | with: 13 | node-version: 12 14 | - name: Install 15 | run: | 16 | npm install 17 | env: 18 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 19 | 20 | - name: Build 21 | if: success() 22 | run: | 23 | npm run build 24 | 25 | - name: Clean package dependecies 26 | if: success() 27 | run: | 28 | rm -rf node_modules 29 | 30 | - name: Install Example 31 | if: success() 32 | run: | 33 | cd examples/jest-example 34 | npm install 35 | 36 | - name: Run Example Test 37 | if: success() 38 | run: | 39 | cd examples/jest-example 40 | npm test 41 | 42 | lint: 43 | runs-on: ubuntu-latest 44 | steps: 45 | - uses: actions/checkout@v2 46 | - uses: actions/setup-node@v1 47 | with: 48 | node-version: 12 49 | - name: Install 50 | run: | 51 | npm install 52 | env: 53 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 54 | 55 | - name: Lint 56 | if: success() 57 | run: | 58 | npm run lint 59 | -------------------------------------------------------------------------------- /src/spy-performance/perf-spier.ts: -------------------------------------------------------------------------------- 1 | import "./spy-functions/spy-function-time"; 2 | import { spyThunk } from "./spy-functions/spy-thunk"; 3 | import { spyCreateSelectorTime, spyCachedCreatorTime } from "./spy-functions/spy-selector"; 4 | 5 | import { perfStatsReset, getPerfSummary } from "./summary/print-summary"; 6 | import { startCustomTimer } from "./custom-timer/custom-timer"; 7 | import { spyReducerCombiner } from "./spy-functions/spy-reducers"; 8 | import {subscribeAll} from "./callbacks"; 9 | 10 | declare global { 11 | interface Window { 12 | perfStatsReset: typeof perfStatsReset; 13 | getPerfSummary: typeof getPerfSummary; 14 | spyThunk: typeof spyThunk; 15 | spyCreateSelectorTime: typeof spyCreateSelectorTime; 16 | spyCachedCreatorTime: typeof spyCachedCreatorTime; 17 | spyReducerCombiner: typeof spyReducerCombiner; 18 | startCustomTimer: typeof startCustomTimer; 19 | } 20 | } 21 | 22 | export const enableSpying = (callbacks = {}) => { 23 | subscribeAll(callbacks); 24 | if (typeof window !== "undefined") { 25 | window.perfStatsReset = perfStatsReset; 26 | window.getPerfSummary = getPerfSummary; 27 | window.spyThunk = spyThunk; 28 | window.spyCreateSelectorTime = spyCreateSelectorTime; 29 | window.spyCachedCreatorTime = spyCachedCreatorTime; 30 | window.startCustomTimer = startCustomTimer; 31 | window.spyReducerCombiner = spyReducerCombiner; 32 | } 33 | } -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages 3 | 4 | name: Node.js Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | push: 10 | branches: 11 | - main 12 | 13 | jobs: 14 | publish-npm: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v2 18 | - uses: actions/setup-node@v1 19 | with: 20 | node-version: 12 21 | registry-url: https://registry.npmjs.org/ 22 | - run: npm ci 23 | - run: npm version patch --no-git-tag-version 24 | - run: echo "RELEASED_PACKAGE_VERSION=$(cat package.json | jq .version)" >> $GITHUB_ENV 25 | - run: git config --global user.name 'ci' && git config --global user.email 'deploy@performance-spy.com' 26 | - run: git add package.json package-lock.json 27 | - run: git commit -m "version bump ${{env.RELEASED_PACKAGE_VERSION}}" 28 | - run: git push 29 | - run: npm run build 30 | - run: npm publish 31 | env: 32 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 33 | - uses: "marvinpinto/action-automatic-releases@latest" 34 | with: 35 | repo_token: "${{ secrets.GITHUB_TOKEN }}" 36 | prerelease: false 37 | automatic_release_tag: "${{env.RELEASED_PACKAGE_VERSION}}" 38 | -------------------------------------------------------------------------------- /src/types/re-reselect/re-reselect.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'redux-thunk-original' { 2 | 3 | } 4 | declare module 'reselect-original' { 5 | export function defaultMemoize(): void; 6 | export function createSelector(): void; 7 | export function createSelectorCreator(): void; 8 | } 9 | declare module "re-reselect-original" { 10 | export type Selector = (state: S, ...args: any[]) => R; 11 | export type Resolver = (state: S, ...args: any[]) => number | string; 12 | export type OutputSelector = (state: S, ...args: any[]) => T; 13 | export type CachedSelector = (resolver: Resolver) => OutputSelector; 14 | 15 | // Two selectors 16 | export function createCachedSelector( 17 | selectors: [ 18 | Selector, 19 | Selector 20 | ], 21 | combiner: (res1: R1, res2: R2) => T, 22 | ): CachedSelector; 23 | 24 | // Three selectors 25 | export function createCachedSelector( 26 | selectors: [ 27 | Selector, 28 | Selector, 29 | Selector 30 | ], 31 | combiner: (res1: R1, res2: R2, res3: R3) => T, 32 | ): CachedSelector; 33 | 34 | // Four selectors 35 | export function createCachedSelector( 36 | selectors: [ 37 | Selector, 38 | Selector, 39 | Selector, 40 | Selector 41 | ], 42 | combiner: (res1: R1, res2: R2, res3: R3, res4: R4) => T, 43 | ): CachedSelector; 44 | 45 | // Five selectors 46 | export function createCachedSelector( 47 | selectors: [ 48 | Selector, 49 | Selector, 50 | Selector, 51 | Selector, 52 | Selector 53 | ], 54 | combiner: (res1: R1, res2: R2, res3: R3, res4: R4, res5: R5) => T, 55 | ): CachedSelector; 56 | } -------------------------------------------------------------------------------- /examples/jest-example/custom-timer-jest.js: -------------------------------------------------------------------------------- 1 | const keyA = "measuring a"; 2 | const keyB = "measuring b"; 3 | 4 | describe("should check how long it takes to measure", () => { 5 | it("should show at least time in test", () => { 6 | const timer = startCustomTimer(keyA); 7 | 8 | const start = performance.now(); 9 | let txt = ""; 10 | for(let i=0; i<1000; i++) { 11 | txt += "a"; 12 | } 13 | const end = performance.now(); 14 | 15 | timer.end(); 16 | 17 | expect(end-start).toBeLessThanOrEqual(getPerfSummary().customTimersPerfStat[keyA].duration) 18 | }) 19 | 20 | it("should show time of both measures", () => { 21 | const timer = startCustomTimer(keyA); 22 | 23 | const start = performance.now(); 24 | let txt = ""; 25 | for(let i=0; i<1000; i++) { 26 | txt += "a"; 27 | } 28 | const end = performance.now(); 29 | 30 | timer.end(); 31 | 32 | const timer2 = startCustomTimer(keyA); 33 | 34 | const start2 = performance.now(); 35 | let txt2 = ""; 36 | for(let i=0; i<1000; i++) { 37 | txt2 += "a"; 38 | } 39 | const end2 = performance.now(); 40 | 41 | timer2.end(); 42 | 43 | expect((end-start)+(end2-start2)).toBeLessThanOrEqual(getPerfSummary().customTimersPerfStat[keyA].duration) 44 | }) 45 | 46 | it("should keep each measure in its key", () => { 47 | const timer = startCustomTimer(keyA); 48 | 49 | const start = performance.now(); 50 | let txt = ""; 51 | for(let i=0; i<1000; i++) { 52 | txt += "a"; 53 | } 54 | const end = performance.now(); 55 | 56 | timer.end(); 57 | 58 | const timer2 = startCustomTimer(keyB); 59 | 60 | const start2 = performance.now(); 61 | let txt2 = ""; 62 | for(let i=0; i<1000; i++) { 63 | txt2 += "a"; 64 | } 65 | const end2 = performance.now(); 66 | 67 | timer2.end(); 68 | 69 | expect((end-start)).toBeLessThanOrEqual(getPerfSummary().customTimersPerfStat[keyA].duration) 70 | expect((end2-start2)).toBeLessThanOrEqual(getPerfSummary().customTimersPerfStat[keyB].duration) 71 | }) 72 | }) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "performance-spy", 3 | "version": "0.0.28", 4 | "main": "./dist/lib/index.js", 5 | "jsnext:main": "./dist/es/index.js", 6 | "module": "./dist/es/index.js", 7 | "unpkg": "./dist/dist/index.js", 8 | "types": "./dist/types/index.d.ts", 9 | "files": [ 10 | "readme.md", 11 | "resolver/", 12 | "dist/" 13 | ], 14 | "exports": { 15 | ".": { 16 | "import": "./dist/es/index.js", 17 | "require": "./dist/lib/index.js" 18 | } 19 | }, 20 | "description": "Performance spy - spy on common libraries and check their performance", 21 | "homepage": "https://github.com/mentaman/performance-spy", 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/mentaman/performance-spy.git" 25 | }, 26 | "keywords": [ 27 | "performance", 28 | "profiler", 29 | "spy" 30 | ], 31 | "scripts": { 32 | "build": "rm -rf resolver && rm -rf dist && rollup -c && npm run resolver-post-build", 33 | "resolver-post-build": "node resolver-fixer.js", 34 | "lint": "tslint -c tslint.json 'src/**/*.ts'" 35 | }, 36 | "author": "", 37 | "license": "ISC", 38 | "devDependencies": { 39 | "@babel/cli": "^7.16.0", 40 | "@babel/core": "^7.16.5", 41 | "@babel/node": "^7.16.5", 42 | "@babel/plugin-external-helpers": "^7.16.5", 43 | "@babel/plugin-proposal-object-rest-spread": "^7.16.5", 44 | "@babel/plugin-transform-runtime": "^7.16.5", 45 | "@babel/preset-env": "^7.16.5", 46 | "@babel/preset-flow": "^7.16.5", 47 | "@babel/preset-typescript": "^7.16.5", 48 | "@babel/register": "^7.16.5", 49 | "@rollup/plugin-babel": "^5.3.0", 50 | "@rollup/plugin-node-resolve": "^13.1.1", 51 | "@types/node": "^17.0.2", 52 | "@types/redux": "^3.6.0", 53 | "jest": "^27.4.5", 54 | "re-reselect": "^4.0.0", 55 | "redux": "^4.0.5", 56 | "redux-thunk": "^2.4.1", 57 | "reselect": "^4.1.5", 58 | "rollup": "^2.62.0", 59 | "rollup-plugin-terser": "^7.0.2", 60 | "rollup-plugin-typescript2": "^0.31.1", 61 | "tslint": "^6.1.3", 62 | "typescript": "^4.5.4" 63 | }, 64 | "peerDependencies": { 65 | "re-reselect": "^4.0.0", 66 | "redux": "^4.0.5", 67 | "redux-thunk": "^2.4.1", 68 | "reselect": "^4.1.5" 69 | }, 70 | "dependencies": { 71 | "@babel/runtime": "^7.16.5" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/spy-performance/performance-timer.ts: -------------------------------------------------------------------------------- 1 | export type Stat = 2 | { 3 | duration?: number; 4 | count?: number; 5 | maxCount?: number; 6 | minCount?: number; 7 | moreThanFrameCount?: number; 8 | 'moreThan0.1s'?: number; 9 | } & { 10 | [key: string]: number | PerfStatsStuff 11 | } 12 | export class PerfStatsStuff { 13 | type: string; 14 | stats: {[key: string]: Stat}; 15 | 16 | constructor(type: string = "") { 17 | this.type = type; 18 | this.stats = {}; 19 | } 20 | 21 | createStat(key: string, name: string, defaultValue: number | PerfStatsStuff) { 22 | if (!this.stats[key]) { 23 | this.stats[key] = {}; 24 | } 25 | 26 | if (this.stats[key][name] === undefined) { 27 | this.stats[key][name] = defaultValue; 28 | } 29 | } 30 | 31 | statExists(key: string, name: string) { 32 | return this.stats[key] && this.stats[key][name] !== undefined; 33 | } 34 | 35 | getSubStat(key: string, name: string) { 36 | if (!this.statExists(key, name)) { 37 | this.createStat(key, name, new PerfStatsStuff()); 38 | } 39 | 40 | return this.stats[key][name]; 41 | } 42 | 43 | setStat(key: string, name: string, value: number) { 44 | this.createStat(key, name, value); 45 | this.stats[key][name] = value; 46 | } 47 | 48 | increaseStat(key: string, name: string, increaseBy: number) { 49 | this.createStat(key, name, 0); 50 | (this.stats[key][name] as number) += increaseBy; 51 | } 52 | 53 | maxStat(key: string, name: string, value: number) { 54 | this.createStat(key, name, value); 55 | this.stats[key][name] = Math.max(value, this.stats[key][name] as number); 56 | } 57 | 58 | minStat(key: string, name: string, value: number) { 59 | this.createStat(key, name, value); 60 | this.stats[key][name] = Math.min(value, this.stats[key][name] as number); 61 | } 62 | 63 | forwardStat(key: string, name: string) { 64 | this.increaseStat(key, name, 1); 65 | } 66 | 67 | addDurationStat(key: string, duration: number) { 68 | this.increaseStat(key, "duration", duration); 69 | this.forwardStat(key, "count"); 70 | if (duration > 16) { 71 | this.forwardStat(key, "moreThanFrameCount"); 72 | } 73 | if (duration > 100) { 74 | this.forwardStat(key, "moreThan0.1s"); 75 | } 76 | this.minStat(key, "minDuration", duration); 77 | this.maxStat(key, "maxDuration", duration); 78 | } 79 | 80 | reset() { 81 | this.stats = {}; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/spy-performance/custom-timer/custom-timer.ts: -------------------------------------------------------------------------------- 1 | import { getCurrentSummaryForUse } from "../summary/summary-context"; 2 | 3 | export type PerfStat = { 4 | times: number, 5 | duration: number, 6 | "moreThan0.5msTimes": number, 7 | moreThan1msTimes: number, 8 | moreThan10msTimes: number, 9 | moreThan100msTimes: number, 10 | moreThanFrameTimes: number, 11 | maxDuration: number | null, 12 | minDuration: number | null, 13 | avgTime?: number 14 | } 15 | 16 | export class CustomSpyFunctionsTimer { 17 | name: string; 18 | startTime: number; 19 | endTime?: number; 20 | duration?: number; 21 | 22 | constructor(name: string) { 23 | this.name = name; 24 | this.startTime = performance.now(); 25 | } 26 | 27 | end() { 28 | const { customTimersPerfStat } = getCurrentSummaryForUse(); 29 | 30 | this.endTime = performance.now(); 31 | this.duration = this.endTime - this.startTime; 32 | if (!customTimersPerfStat[this.name]) { 33 | customTimersPerfStat[this.name] = { 34 | times: 0, 35 | duration: 0, 36 | "moreThan0.5msTimes": 0, 37 | moreThan1msTimes: 0, 38 | moreThan10msTimes: 0, 39 | moreThan100msTimes: 0, 40 | moreThanFrameTimes: 0, 41 | maxDuration: 0, 42 | minDuration: null 43 | }; 44 | } 45 | customTimersPerfStat[this.name].times++; 46 | customTimersPerfStat[this.name].duration += this.duration; 47 | customTimersPerfStat[this.name].avgTime = 48 | customTimersPerfStat[this.name].duration / customTimersPerfStat[this.name].times; 49 | customTimersPerfStat[this.name].maxDuration = Math.max(this.duration as number, customTimersPerfStat[this.name].maxDuration as number); 50 | customTimersPerfStat[this.name].minDuration = 51 | customTimersPerfStat[this.name].minDuration === null 52 | ? this.duration 53 | : Math.min(this.duration as number, customTimersPerfStat[this.name].minDuration as number); 54 | if (this.duration > 0.5) { 55 | customTimersPerfStat[this.name]["moreThan0.5msTimes"]++; 56 | } 57 | if (this.duration > 1) { 58 | customTimersPerfStat[this.name].moreThan1msTimes++; 59 | } 60 | if (this.duration > 10) { 61 | customTimersPerfStat[this.name].moreThan10msTimes++; 62 | } 63 | if (this.duration > 16) { 64 | customTimersPerfStat[this.name].moreThanFrameTimes++; 65 | } 66 | if (this.duration > 100) { 67 | customTimersPerfStat[this.name].moreThan100msTimes++; 68 | } 69 | } 70 | } 71 | export const startCustomTimer = function (name: string): CustomSpyFunctionsTimer { 72 | return new CustomSpyFunctionsTimer(name); 73 | }; 74 | -------------------------------------------------------------------------------- /examples/jest-example/reselect-jest.js: -------------------------------------------------------------------------------- 1 | const { createSelector } = require("reselect"); 2 | const { subscribeAll } = require("performance-spy"); 3 | 4 | let selectorExample; 5 | 6 | describe("reselect performance spying", () => { 7 | beforeEach(() => { 8 | selectorExample = createSelector((a) => a, (_a, b) => b, (_resA, _resB) => 100); 9 | }) 10 | 11 | it("should find only one combiner calculation", () => { 12 | selectorExample(1, 2); 13 | 14 | const summary = getPerfSummary(); 15 | 16 | expect(summary.mostCalculatedCombiners.length).toEqual(1); 17 | }) 18 | 19 | it("should calculate only once", () => { 20 | selectorExample(1, 2); 21 | selectorExample(1, 2); 22 | 23 | const summary = getPerfSummary(); 24 | 25 | expect(summary.mostCalculatedCombiners[0].maxCount).toEqual(1); 26 | }) 27 | 28 | it("should calculate three times", () => { 29 | selectorExample(1, 2); 30 | selectorExample(1, 3); 31 | selectorExample(1, 2); 32 | 33 | const summary = getPerfSummary(); 34 | 35 | expect(summary.mostCalculatedCombiners[0].maxCount).toEqual(3); 36 | }) 37 | 38 | it("should find which param caused it to recalculate", () => { 39 | selectorExample(1, 2); 40 | selectorExample(1, 3); 41 | selectorExample(1, 2); 42 | 43 | const summary = getPerfSummary(); 44 | 45 | expect(summary.mostCalculatedCombiners[0].args.stats['1'].count).toEqual(2); 46 | }) 47 | 48 | it("let override selector for tests", () => { 49 | const summarySelector = createSelector(() => 100, () => 200, (resA, resB) => resA+resB); 50 | const combiner = summarySelector.resultFunc; 51 | 52 | const res = combiner(10, 10); 53 | expect(res).toEqual(20); 54 | }) 55 | 56 | it("should notify subscribes when selector called", () => { 57 | const callbackFn = jest.fn(); 58 | subscribeAll({ selector: callbackFn }); 59 | const summarySelector = createSelector(() => 100, () => 200, (resA, resB) => resA+resB); 60 | summarySelector(); 61 | 62 | expect(callbackFn.mock.calls.length).toEqual(1); 63 | expect(typeof callbackFn.mock.calls[0][0]).toBe("string"); 64 | expect(typeof callbackFn.mock.calls[0][1]).toBe("number"); 65 | }) 66 | 67 | it("should not notify subscribes when selector not called", () => { 68 | const callbackFn = jest.fn(); 69 | subscribeAll({ selector: callbackFn }); 70 | createSelector(() => 100, () => 200, (resA, resB) => resA+resB); 71 | 72 | expect(callbackFn.mock.calls.length).toEqual(0); 73 | }) 74 | }) -------------------------------------------------------------------------------- /examples/jest-example/redux-jest.js: -------------------------------------------------------------------------------- 1 | const { createStore, combineReducers } = require("redux"); 2 | 3 | let store; 4 | let middlewares; 5 | let initialState; 6 | 7 | describe("redux reducers performance spying", () => { 8 | beforeEach(() => { 9 | initialState = { 10 | reducerA: {}, 11 | reducerB: {} 12 | }; 13 | 14 | const reducerA = (state = [], action) => { 15 | switch (action.type) { 16 | case 'ACTION_A': 17 | return {...state, [action.key]: action.value}; 18 | default: 19 | return state 20 | } 21 | } 22 | 23 | const reducerB = (state = [], action) => { 24 | switch (action.type) { 25 | case 'ACTION_B': 26 | return {...state, [action.key]: action.value}; 27 | default: 28 | return state 29 | } 30 | } 31 | const reducers = {reducerA, reducerB}; 32 | store = createStore(combineReducers(reducers), initialState, middlewares); 33 | perfStatsReset(); 34 | }) 35 | 36 | it("should show changed reducer once", () => { 37 | store.dispatch({ 38 | type: 'ACTION_A', 39 | key: 'mykey', 40 | value: "myvalue" 41 | }); 42 | 43 | const summary = getPerfSummary(); 44 | expect(summary.mostChangedReducers[0].key).toEqual("reducerA"); 45 | expect(summary.mostChangedReducers[0].changedTimes).toEqual(1); 46 | }) 47 | 48 | it("should show changed reducers twice", () => { 49 | store.dispatch({ 50 | type: 'ACTION_A', 51 | key: 'mykey', 52 | value: "myvalue" 53 | }); 54 | store.dispatch({ 55 | type: 'ACTION_A', 56 | key: 'mykey', 57 | value: "othervalue" 58 | }); 59 | 60 | const summary = getPerfSummary(); 61 | expect(summary.mostChangedReducers[0].key).toEqual("reducerA"); 62 | expect(summary.mostChangedReducers[0].changedTimes).toEqual(2); 63 | }) 64 | 65 | it("should show changed reducers only when changed", () => { 66 | store.dispatch({ 67 | type: 'ACTION_A', 68 | key: 'mykey', 69 | value: "myvalue" 70 | }); 71 | store.dispatch({ 72 | type: 'ACTION_B', 73 | key: 'mykey', 74 | value: "othervalue" 75 | }); 76 | 77 | const summary = getPerfSummary(); 78 | expect(summary.mostChangedReducers[0].key).toEqual("reducerA"); 79 | expect(summary.mostChangedReducers[0].changedTimes).toEqual(1); 80 | }) 81 | }) -------------------------------------------------------------------------------- /examples/jest-example/re-reselect-jest.js: -------------------------------------------------------------------------------- 1 | const { createCachedSelector } = require("re-reselect"); 2 | 3 | let selectorExample; 4 | 5 | const keyOne = "a"; 6 | const keyTwo = "b"; 7 | 8 | describe("re-reselect performance spying", () => { 9 | beforeEach(() => { 10 | selectorExample = createCachedSelector( 11 | (a, _b, _key) => a, 12 | (_a, b, _key) => b, 13 | (_resA, _resB) => 100 14 | )((_a, _b, key) => key); 15 | }); 16 | 17 | it("should find only one combiner calculation", () => { 18 | selectorExample(1, 2, keyOne); 19 | selectorExample(1, 2, keyTwo); 20 | 21 | const summary = getPerfSummary(); 22 | 23 | expect(summary.mostCalculatedCombiners.length).toEqual(1); 24 | }); 25 | 26 | it("should calculate only once", () => { 27 | selectorExample(1, 2, keyOne); 28 | selectorExample(1, 2, keyOne); 29 | selectorExample(2, 3, keyTwo); 30 | selectorExample(2, 3, keyTwo); 31 | 32 | const summary = getPerfSummary(); 33 | 34 | expect(summary.mostCalculatedCombiners[0].maxCount).toEqual(1); 35 | }); 36 | 37 | it("should calculate three times in each one", () => { 38 | selectorExample(1, 2, keyOne); 39 | selectorExample(1, 3, keyOne); 40 | selectorExample(1, 2, keyOne); 41 | 42 | selectorExample(1, 2, keyTwo); 43 | selectorExample(1, 3, keyTwo); 44 | selectorExample(1, 2, keyTwo); 45 | 46 | const summary = getPerfSummary(); 47 | 48 | expect(summary.mostCalculatedCombiners[0].maxCount).toEqual(3); 49 | expect(summary.mostCalculatedCombiners[0].count).toEqual(6); 50 | }); 51 | 52 | it("should find which param caused it to recalculate", () => { 53 | selectorExample(1, 2, keyOne); 54 | selectorExample(1, 3, keyOne); 55 | selectorExample(1, 2, keyOne); 56 | 57 | selectorExample(1, 2, keyTwo); 58 | selectorExample(1, 3, keyTwo); 59 | selectorExample(1, 2, keyTwo); 60 | 61 | const summary = getPerfSummary(); 62 | 63 | expect(summary.mostCalculatedCombiners[0].args.stats["1"].count).toEqual(4); 64 | }); 65 | 66 | it("let override selector for tests", () => { 67 | const summarySelector = createCachedSelector( 68 | (a, _b, _key) => 0, 69 | (_a, b, _key) => 0, 70 | (resA, resB) => resA + resB 71 | )((_a, _b, key) => key); 72 | const combiner = summarySelector.resultFunc; 73 | 74 | const res = combiner(10, 10); 75 | expect(res).toEqual(20); 76 | }); 77 | 78 | it("should find its key", () => { 79 | selectorExample(1, 2, keyOne); 80 | 81 | const summary = getPerfSummary(); 82 | 83 | expect(summary.mostCalculatedCombiners[0].key).toEqual( 84 | "(_resA, _resB) => 100" 85 | ); 86 | expect(summary.longestSelectors[0].key).toEqual("(_resA, _resB) => 100"); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /src/spy-performance/spy-functions/spy-function-time.ts: -------------------------------------------------------------------------------- 1 | import { getCurrentSummaryForUse } from "../summary/summary-context"; 2 | import { PerfStatsStuff } from "../performance-timer"; 3 | import { Summaries } from "../summary/summaries"; 4 | import {Observer} from "../callbacks"; 5 | 6 | interface KeepFuncRefs { 7 | lastReset?: number; 8 | count?: number; 9 | lastArgs?: IArguments; 10 | lastArgsKey?: string; 11 | } 12 | 13 | export const spyFunctionTime = ( 14 | fn: (...args: any[]) => any, 15 | perfstat: (summaries: Summaries) => PerfStatsStuff, 16 | key: string, 17 | { 18 | beforeResultsChecker, 19 | resultsChecker, 20 | checkArgs = false, 21 | keepFuncRefs = {}, 22 | observer, 23 | }: { 24 | beforeResultsChecker?: (thisRef: any, args: IArguments) => void; 25 | resultsChecker?: (thisRef: any, res: any, args: IArguments, perfstat: PerfStatsStuff, key: string) => void; 26 | checkArgs?: boolean; 27 | keepFuncRefs?: KeepFuncRefs; 28 | observer?: Observer; 29 | } = {} 30 | ) => (...args: any) => { 31 | const currentSummary = getCurrentSummaryForUse(); 32 | if (keepFuncRefs.lastReset && currentSummary.getCurrentReset() > keepFuncRefs.lastReset) { 33 | keepFuncRefs.count = undefined; 34 | keepFuncRefs.lastArgs = undefined; 35 | keepFuncRefs.lastArgsKey = undefined; 36 | } 37 | 38 | keepFuncRefs.lastReset = currentSummary.getCurrentReset(); 39 | if (beforeResultsChecker) { 40 | beforeResultsChecker(this, args); 41 | } 42 | 43 | const startTime = performance.now(); 44 | const res = fn(...args); 45 | const endTime = performance.now(); 46 | const duration = endTime - startTime; 47 | observer?.notify(key, duration); 48 | perfstat(currentSummary).addDurationStat(key, duration); 49 | 50 | if (!keepFuncRefs.count) { 51 | keepFuncRefs.count = 0; 52 | } 53 | 54 | keepFuncRefs.count++; 55 | perfstat(currentSummary).maxStat(key, "maxCount", keepFuncRefs.count); 56 | 57 | if (checkArgs) { 58 | if (keepFuncRefs.lastArgs) { 59 | let wasChanged = false; 60 | 61 | for (let argIdx = 0; argIdx < args.length; argIdx++) { 62 | if (keepFuncRefs.lastArgs[argIdx] !== args[argIdx]) { 63 | (perfstat(currentSummary).getSubStat(key, "args") as PerfStatsStuff).forwardStat(argIdx.toString(), "count"); 64 | wasChanged = true; 65 | } 66 | } 67 | 68 | if (keepFuncRefs.lastArgsKey !== key) { 69 | // warning - shouldn't happen 70 | } 71 | 72 | if (!wasChanged) { 73 | // warning - shouldn't happen 74 | } 75 | } 76 | 77 | keepFuncRefs.lastArgs = args; 78 | keepFuncRefs.lastArgsKey = key; 79 | } 80 | 81 | if (resultsChecker) { 82 | resultsChecker(this, res, args, perfstat(currentSummary), key); 83 | } 84 | 85 | return res; 86 | }; 87 | -------------------------------------------------------------------------------- /src/spy-performance/summary/summaries.ts: -------------------------------------------------------------------------------- 1 | import { PerfStat } from "../custom-timer/custom-timer"; 2 | import { PerfStatsStuff, Stat } from "../performance-timer"; 3 | 4 | function reducerChangedTimes(r: Stat): number { 5 | return Object.values((r?.args as PerfStatsStuff)?.stats || {}).reduce( 6 | (total, num) => total + (num.count || 0), 7 | 0 8 | ); 9 | } 10 | 11 | const deepClone = (obj: T): T => JSON.parse(JSON.stringify(obj)); 12 | export class Summaries { 13 | selectorsPerfStat: PerfStatsStuff; 14 | dispatchPerfStat: PerfStatsStuff; 15 | reducersPerfStat: PerfStatsStuff; 16 | combinerPerfStat: PerfStatsStuff; 17 | allPerfStats: PerfStatsStuff[]; 18 | customTimersPerfStat: { [key: string]: PerfStat }; 19 | currentReset: number = -1; 20 | 21 | constructor() { 22 | this.customTimersPerfStat = {}; 23 | this.selectorsPerfStat = new PerfStatsStuff("duration_by_key"); 24 | this.dispatchPerfStat = new PerfStatsStuff("duration_by_key"); 25 | this.reducersPerfStat = new PerfStatsStuff("duration_by_key_with_args"); 26 | this.combinerPerfStat = new PerfStatsStuff("duration_by_key_with_args"); 27 | this.allPerfStats = [ 28 | this.selectorsPerfStat, 29 | this.dispatchPerfStat, 30 | this.reducersPerfStat, 31 | this.combinerPerfStat, 32 | ]; 33 | } 34 | 35 | getCurrentReset() { 36 | return this.currentReset; 37 | } 38 | 39 | reset() { 40 | this.currentReset++; 41 | this.customTimersPerfStat = {}; 42 | 43 | for (const perfStat of this.allPerfStats) { 44 | perfStat.reset(); 45 | } 46 | } 47 | 48 | getSummary() { 49 | const { 50 | combinerPerfStat, 51 | dispatchPerfStat, 52 | selectorsPerfStat, 53 | reducersPerfStat, 54 | } = this; 55 | 56 | const longestSelectors = Object.entries(selectorsPerfStat.stats) 57 | .map(([key, value]) => ({ ...value, key })) 58 | .sort((a, b) => (b.duration || 0) - (a.duration || 0)); 59 | 60 | const longestCombiners = Object.entries(combinerPerfStat.stats) 61 | .map(([key, value]) => ({ ...value, key })) 62 | .sort((a, b) => (b.duration || 0) - (a.duration || 0)); 63 | 64 | const mostCalculatedCombiners = Object.entries(combinerPerfStat.stats) 65 | .map(([key, value]) => ({ ...value, key })) 66 | .sort((a, b) => (b.maxCount || 0) - (a.maxCount || 0)); 67 | 68 | const longestDispatches = Object.entries(dispatchPerfStat.stats) 69 | .map(([key, value]) => ({ ...value, key })) 70 | .sort((a, b) => (b.duration || 0) - (a.duration || 0)); 71 | 72 | const mostChangedReducers = Object.entries(reducersPerfStat.stats) 73 | .map(([key, value]) => ({ 74 | ...value, 75 | key, 76 | changedTimes: reducerChangedTimes(value), 77 | })) 78 | .sort((a, b) => (b.changedTimes || 0) - (a.changedTimes || 0)); 79 | 80 | const customTimersPerfStat = this.customTimersPerfStat; 81 | 82 | return deepClone({ 83 | longestSelectors, 84 | longestCombiners, 85 | mostCalculatedCombiners, 86 | mostChangedReducers, 87 | longestDispatches, 88 | customTimersPerfStat, 89 | }); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/spy-performance/spy-functions/spy-selector.ts: -------------------------------------------------------------------------------- 1 | import { spyFunctionTime } from "./spy-function-time"; 2 | import {callbacks} from "../callbacks"; 3 | 4 | type GenericFunction = (...args: any[]) => any; 5 | 6 | let currentSelectorKey: string | null = null; 7 | let currentCacheSelectorKey: string | null = null; 8 | 9 | const spySelectorTime = (originalSelectorFunc: GenericFunction) => { 10 | return function () { 11 | const combinerFunc = arguments[arguments.length - 1]; 12 | let key = combinerFunc.toString().substr(0, 600); 13 | if (combinerFunc.name === "resultFuncWithRecomputations") { 14 | key = currentCacheSelectorKey; 15 | currentCacheSelectorKey = null; 16 | } 17 | currentSelectorKey = key; 18 | const spiedFunction = originalSelectorFunc(...arguments); 19 | currentSelectorKey = null; 20 | 21 | 22 | const spyFunction = spyFunctionTime(spiedFunction, (summary) => summary.selectorsPerfStat, key, {observer: callbacks.selector}); 23 | Object.defineProperty(spyFunction, "resultFunc", { 24 | get: function myProperty() { 25 | return (spiedFunction as any).resultFunc; 26 | }, 27 | set(value) { 28 | (spiedFunction as any).resultFunc = value; 29 | } 30 | }); 31 | (spyFunction as any).__originalSpiedFunction = spiedFunction; 32 | 33 | return spyFunction; 34 | }; 35 | }; 36 | 37 | const spySelectorMemoizer = (originalMemoized: GenericFunction) => { 38 | return function () { 39 | const key: string | null = currentSelectorKey; 40 | currentSelectorKey = null; 41 | if (key !== null) { 42 | arguments[0] = spyFunctionTime(arguments[0], (summary) => summary.combinerPerfStat, key, { checkArgs: true }); 43 | } 44 | const spiedFunction = originalMemoized(...arguments); 45 | return spiedFunction; 46 | }; 47 | }; 48 | 49 | export const spyCreateSelectorTime = function (originalCreateSelectorFunc: GenericFunction) { 50 | return (...args: any[]) => { 51 | args[0] = spySelectorMemoizer(args[0]); 52 | const spiedFunction = originalCreateSelectorFunc(...args); 53 | return spySelectorTime(spiedFunction); 54 | }; 55 | }; 56 | 57 | const spyCachedInnerInnerTime = function (fn: GenericFunction, key: string) { 58 | return function () { 59 | currentCacheSelectorKey = key; 60 | const spiedFunction = fn(...arguments); 61 | currentCacheSelectorKey = null; 62 | return spiedFunction; 63 | }; 64 | }; 65 | 66 | const spyCachedInnerTime = function (fn: GenericFunction, key: string) { 67 | return function () { 68 | const spiedFunction = fn(...arguments); 69 | const spyFunction = spyCachedInnerInnerTime(spiedFunction, key); 70 | Object.defineProperty(spyFunction, "resultFunc", { 71 | get: function myProperty() { 72 | return (spiedFunction as any).resultFunc; 73 | }, 74 | set(value) { 75 | (spiedFunction as any).resultFunc = value; 76 | } 77 | }); 78 | return spyFunction; 79 | }; 80 | }; 81 | 82 | export const spyCachedCreatorTime = function (selectorFunc: any) { 83 | return function () { 84 | const combinerFunc = arguments[arguments.length - 1]; 85 | 86 | const key = combinerFunc.toString().substr(0, 500); 87 | const spiedFunction = selectorFunc(...arguments); 88 | return spyCachedInnerTime(spiedFunction, key); 89 | }; 90 | }; 91 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | 2 | # Performance Spy 3 | 4 | ## How it works 5 | 6 | It'll override the implentation of your favorite libraries! 7 | 8 | - React ( still WIP ) 9 | - Redux Reducers 10 | - Redux Dispatches 11 | - Redux Thunk 12 | - Moment ( not implented yet ) 13 | - reselect 14 | - re-reselect 15 | 16 | And will tell you how much performance they take, and you can improve their usage. 17 | 18 | For example if you have in reselect a function that takes 1 seconds each time, and gets calculated 5 times instead of once, it'll show you which arguments caused the cache to expire. 19 | 20 | ## Why use it instead of profiler? 21 | 22 | 1. Profilers work in samples, they check the stacktrace a few times in a ms, but in between there are lots of things you might miss and won't be able to find with it due to it. 23 | 24 | 2. Profilers will usually be less guidy, they won't show you, this cache selector doesn't work well and broken. 25 | 26 | ## Install 27 | 28 | Install library 29 | 30 | npm i performance-spy 31 | 32 | ## Setup with webpack 33 | 34 | 1 - add to webpack.config.js: 35 | 36 | alias: { 37 | /* my aliases */ 38 | ...require("performance-spy").spyWebpackAliases(path.resolve("./node_modules"), [ 39 | "redux-thunk", 40 | "re-reselect", 41 | "reselect", 42 | "redux" 43 | ]) 44 | } 45 | 46 | it'll override redux/reselect with performance-spy libraries 47 | 48 | 2 - add to your init.js file, before any usage of one of the libraries 49 | 50 | require("performance-spy").enableSpying(); 51 | 52 | Note that step 2 shouldn't be used in production, it'll have performance impact since all the libraries functions will be used through the library.. 53 | You can allow it though to a specific user in production for research with some condition. 54 | 55 | 56 | ## Setup with jest 57 | 58 | See example [Example](https://github.com/mentaman/performance-spy/tree/main/examples/jest-example) 59 | 60 | 1 - add to jest.config.js 61 | 62 | moduleNameMapper: { 63 | // my name mappers... 64 | ...spyJesAliases("/node_modules", ["redux-thunk", "re-reselect", "reselect", "redux"]) 65 | } 66 | 67 | 2 - add to jest setup 68 | 69 | require("performance-spy").enableSpying(); 70 | 71 | 3 - to jest beforeEach 72 | 73 | beforeEach(() => { 74 | perfStatsReset(); 75 | }); 76 | 77 | ## How to research with it 78 | 79 | 1 - load a page, do some actions 80 | 81 | 2 - run in f12 console: 82 | 83 | getPerfSummary() 84 | 85 | 3 - you'll get a summary of what had happend 86 | 87 | ## Measure a specific action 88 | 89 | 1 - load a page and prepare to do your action 90 | 91 | 2 - run in f12 console 92 | 93 | perfStatsReset() 94 | 95 | 3 - do your action 96 | 97 | 4 - run in f12 console 98 | 99 | getPerfSummary 100 | 101 | 5 - you'll see a summary of this action 102 | 103 | ## Custom measures 104 | 105 | If you have a function that you know it's slow, but you don't know which part 106 | you can use 107 | 108 | const timer = startCustomTimer("measuring a") 109 | // some actions 110 | timer.end() 111 | 112 | and then you'll see it as part of getPerfSummary 113 | 114 | so you can divide and conquer your slow function until you find which part is slow: 115 | 116 | const measureHeavy = startCustomTimer("heavy") 117 | someHeavyStuff() 118 | measureHeavy.end() 119 | 120 | const measureLight = startCustomTimer("light") 121 | someLightStuff() 122 | measureLight.end() 123 | 124 | const measureSuspect = startCustomTimer("suspicious") 125 | SomeSuspiciousStuff() 126 | measureSuspect.end() 127 | 128 | and you'll get the answer which one is the slowing part? the heavy function? the suspicious? the light? 129 | 130 | 131 | you can also provide data to it, for dynamic measure, to know which id had a slow transaction 132 | 133 | const measureIdStuff = startCustomTimer("suspicious"+id) 134 | heavyFunction(id) 135 | measureIdStuff.end() 136 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import nodeResolve from "@rollup/plugin-node-resolve"; 2 | import babel from "@rollup/plugin-babel"; 3 | import typescript from "rollup-plugin-typescript2"; 4 | import { terser } from "rollup-plugin-terser"; 5 | const path = require("path"); 6 | const fs = require("fs"); 7 | 8 | import pkg from "./package.json"; 9 | 10 | const extensions = [".ts"]; 11 | const noDeclarationFiles = { compilerOptions: { declaration: false } }; 12 | 13 | const babelRuntimeVersion = pkg.dependencies["@babel/runtime"].replace( 14 | /^[^0-9]*/, 15 | "" 16 | ); 17 | 18 | const makeExternalPredicate = (externalArr) => { 19 | if (externalArr.length === 0) { 20 | return () => false; 21 | } 22 | const pattern = new RegExp(`^(${externalArr.join("|")})($|/)`); 23 | return (id) => pattern.test(id); 24 | }; 25 | 26 | function exportLib(input, output) { 27 | // CommonJS 28 | return [ 29 | { 30 | input, 31 | output: { 32 | file: output + "dist/lib/index.js", 33 | format: "cjs", 34 | indent: false, 35 | }, 36 | external: makeExternalPredicate([ 37 | ...Object.keys(pkg.dependencies || {}), 38 | ...Object.keys(pkg.peerDependencies || {}), 39 | ]), 40 | plugins: [ 41 | nodeResolve({ 42 | extensions, 43 | }), 44 | typescript({ useTsconfigDeclarationDir: true }), 45 | babel({ 46 | extensions, 47 | plugins: [ 48 | [ 49 | "@babel/plugin-transform-runtime", 50 | { version: babelRuntimeVersion }, 51 | ], 52 | ], 53 | babelHelpers: "runtime", 54 | }), 55 | ], 56 | }, 57 | // ES 58 | { 59 | input, 60 | output: { 61 | file: output + "dist/es/index.js", 62 | format: "es", 63 | indent: false, 64 | }, 65 | external: makeExternalPredicate([ 66 | ...Object.keys(pkg.dependencies || {}), 67 | ...Object.keys(pkg.peerDependencies || {}), 68 | ]), 69 | plugins: [ 70 | nodeResolve({ 71 | extensions, 72 | }), 73 | typescript({ tsconfigOverride: noDeclarationFiles }), 74 | babel({ 75 | extensions, 76 | plugins: [ 77 | [ 78 | "@babel/plugin-transform-runtime", 79 | { version: babelRuntimeVersion, useESModules: true }, 80 | ], 81 | ], 82 | babelHelpers: "runtime", 83 | }), 84 | ], 85 | }, 86 | // ES for Browsers 87 | { 88 | input, 89 | output: { 90 | file: output + "dist/es/index.mjs", 91 | format: "es", 92 | indent: false, 93 | }, 94 | plugins: [ 95 | nodeResolve({ 96 | extensions, 97 | }), 98 | typescript({ tsconfigOverride: noDeclarationFiles }), 99 | babel({ 100 | extensions, 101 | exclude: "node_modules/**", 102 | skipPreflightCheck: true, 103 | babelHelpers: "bundled", 104 | }), 105 | terser({ 106 | compress: { 107 | pure_getters: true, 108 | unsafe: true, 109 | unsafe_comps: true, 110 | warnings: false, 111 | }, 112 | }), 113 | ], 114 | }, 115 | // UMD Development 116 | { 117 | input, 118 | output: { 119 | file: output + "dist/dist/index.js", 120 | format: "umd", 121 | name: "Performance spy", 122 | indent: false, 123 | }, 124 | plugins: [ 125 | nodeResolve({ 126 | extensions, 127 | }), 128 | typescript({ tsconfigOverride: noDeclarationFiles }), 129 | babel({ 130 | extensions, 131 | exclude: "node_modules/**", 132 | babelHelpers: "bundled", 133 | }), 134 | ], 135 | }, 136 | // UMD Production 137 | { 138 | input, 139 | output: { 140 | file: output + "dist/dist/index.min.js", 141 | format: "umd", 142 | name: "Performance spy", 143 | indent: false, 144 | }, 145 | plugins: [ 146 | nodeResolve({ 147 | extensions, 148 | }), 149 | typescript({ tsconfigOverride: noDeclarationFiles }), 150 | babel({ 151 | extensions, 152 | exclude: "node_modules/**", 153 | skipPreflightCheck: true, 154 | babelHelpers: "bundled", 155 | }), 156 | terser({ 157 | compress: { 158 | pure_getters: true, 159 | unsafe: true, 160 | unsafe_comps: true, 161 | warnings: false, 162 | }, 163 | }), 164 | ], 165 | }, 166 | ]; 167 | } 168 | 169 | const resolverPath = path.resolve("./src/resolver"); 170 | const resolvers = fs.readdirSync(resolverPath).map((p) => path.parse(p).name); 171 | 172 | let resolversExports = []; 173 | for (const resolver of resolvers) { 174 | const input = path.join(resolverPath, `${resolver}.ts`); 175 | const output = `resolver/${resolver}`; 176 | console.log(input, output); 177 | resolversExports.push(...exportLib(input, output+"/")); 178 | } 179 | export default [ 180 | ...exportLib("src/index.ts", ""), 181 | ...resolversExports 182 | ]; 183 | --------------------------------------------------------------------------------