diff --git a/README.md b/README.md index fb26609..242987c 100644 --- a/README.md +++ b/README.md @@ -5,11 +5,11 @@ [![npm version](https://badge.fury.io/js/bleak-detector.svg)](https://www.npmjs.com/package/bleak-detector) [![Coverage Status](https://coveralls.io/repos/github/plasma-umass/BLeak/badge.svg)](https://coveralls.io/github/plasma-umass/BLeak) -BLeak automatically finds, ranks, and diagnoses memory leaks in the client-side of web applications. +[BLeak](http://bleak-detector.org/) automatically finds, ranks, and diagnoses memory leaks in the client-side of web applications. BLeak uses a short developer-provided script to drive the application in a loop through specific visual states (e.g., the inbox view and email view of a mail client) as an oracle to find memory leaks. In our experience, BLeak's precision is often **100%** (e.g., no false positives), and fixing the leaks it finds reduces heap growth by **94%** on average on a corpus of real production web apps. -For more information please see the [preprint of our academic paper](https://github.com/plasma-umass/BLeak/blob/master/paper.pdf), which will appear at PLDI 2018. +For more information please see [the BLeak website](http://bleak-detector.org/) and the [preprint of our academic paper](https://github.com/plasma-umass/BLeak/blob/master/paper.pdf), which will appear at PLDI 2018. ## Prerequisites diff --git a/html/index.html b/html/index.html index fbfa138..1651449 100644 --- a/html/index.html +++ b/html/index.html @@ -27,6 +27,6 @@ - + - \ No newline at end of file + diff --git a/package.json b/package.json index 8098127..d4051b4 100644 --- a/package.json +++ b/package.json @@ -6,16 +6,18 @@ "bleak": "./dist/node/cli/bleak.js" }, "scripts": { - "build": "npm-run-all -p build:node build:viewer -s rollup:viewer make_dist", + "build": "npm-run-all -p build:node build:viewer -s rollup:viewer rollup:viewer:production make_dist", "build:node": "tsc -p tsconfig.node.json", "build:viewer": "tsc", "rollup:viewer": "rollup -c", + "rollup:viewer:production": "rollup -c --environment BUILD:production", "test": "npm-run-all -s build nyc:test", "nyc:test": "nyc mocha --require source-map-support/register build/node/test", - "watch": "npm-run-all -s build -p tsc:viewer:watch tsc:watch rollup:viewer:watch make_dist:watch", + "watch": "npm-run-all -s build -p tsc:viewer:watch tsc:watch rollup:viewer:watch rollup:viewer:production:watch make_dist:watch", "tsc:watch": "tsc -w", "tsc:viewer:watch": "tsc -w -p tsconfig.node.json", "rollup:viewer:watch": "rollup -w -c", + "rollup:viewer:production:watch": "rollup -w -c --environment BUILD:production", "benchmark": "npm-run-all build:node -s run:benchmark", "run:benchmark": "node --max-old-space-size=8192 build/node/benchmarks/benchmark.js", "make_dist": "node build/node/scripts/make_dist.js", @@ -80,6 +82,7 @@ "rollup-plugin-node-resolve": "^3.0.2", "rollup-plugin-replace": "^2.0.0", "rollup-plugin-sourcemaps": "^0.4.2", + "rollup-plugin-uglify": "^3.0.0", "source-map-support": "^0.5.3", "typescript": "^2.7.1" }, diff --git a/rollup.config.js b/rollup.config.js index fa43e21..a8f43de 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -3,15 +3,17 @@ import buble from 'rollup-plugin-buble'; import resolve from 'rollup-plugin-node-resolve'; import commonjs from 'rollup-plugin-commonjs'; import replace from 'rollup-plugin-replace'; +import uglify from 'rollup-plugin-uglify'; import {join} from 'path'; const inBase = join(__dirname, 'build', 'browser', 'src'); const outBase = join(__dirname, 'build', 'browser'); +const PRODUCTION = process.env['BUILD'] === 'production'; export default { input: join(inBase, 'viewer', 'index.js'), output: [{ - file: join(outBase, 'viewer.js'), + file: join(outBase, PRODUCTION ? 'viewer.min.js' : 'viewer.js'), sourcemap: true, strict: true, globals: { @@ -24,6 +26,7 @@ export default { external: ['d3', 'jquery'], plugins: [ sourcemaps(), + PRODUCTION && uglify(), buble({ transforms: { // Assumes all `for of` statements are on arrays or array-like items. @@ -45,7 +48,7 @@ export default { }), replace({ // Production for production builds. - 'process.env.NODE_ENV': JSON.stringify( 'development' ) + 'process.env.NODE_ENV': JSON.stringify( PRODUCTION ? 'production' : 'development' ) }) - ] + ].filter(Boolean) }; diff --git a/scripts/make_dist.ts b/scripts/make_dist.ts index b33ab83..d0cf5e5 100644 --- a/scripts/make_dist.ts +++ b/scripts/make_dist.ts @@ -67,7 +67,7 @@ async function main(): Promise { promises.push(copyDir(htmlFolder, path.join(distFolder, 'viewer'))); const viewerSrcFolder = path.join(buildFolder, 'browser'); - ['viewer.js', 'viewer.js.map'].forEach((file) => { + ['viewer.js', 'viewer.js.map', 'viewer.min.js', 'viewer.min.js.map'].forEach((file) => { promises.push(copy(path.join(viewerSrcFolder, file), path.join(distFolder, 'viewer', file))); }); promises.push(copy(path.resolve('node_modules', 'd3', 'build', 'd3.min.js'), path.join(distFolder, 'viewer', 'd3.min.js'))); diff --git a/src/viewer/components/app.tsx b/src/viewer/components/app.tsx index a97fa7a..b30f09e 100644 --- a/src/viewer/components/app.tsx +++ b/src/viewer/components/app.tsx @@ -41,6 +41,70 @@ export default class App extends React.Component<{}, AppState> { }; } + private async _tryDisplayFile(result: string, startingPercent: number): Promise { + const bleakResults = BLeakResults.FromJSON(JSON.parse(result)); + const sourceFileManager = await SourceFileManager.FromBLeakResults(bleakResults, (completed, total) => { + const percent = startingPercent + (completed / total) * (100 - startingPercent); + this.setState({ + progress: percent, + progressMessage: `${completed} of ${total} source files formatted...` + }); + }); + const sourceFiles = sourceFileManager.getSourceFiles(); + const stackTraces = StackTraceManager.FromBLeakResults(sourceFileManager, bleakResults); + this.setState({ + state: ViewState.DISPLAYING_FILE, + bleakResults, + sourceFileManager, + stackTraces, + selectedLocation: new Location(sourceFiles[0], 1, 1, true) + }); + } + + private _loadFromUrl(url: string): void { + this.setState({ + state: ViewState.PROCESSING_FILE, + progress: 10, + progressMessage: "Downloading results file ...", + errorMessage: null + }); + // 40% + const xhr = new XMLHttpRequest(); + xhr.open('GET', url); + xhr.onprogress = (e) => { + const p = e.loaded / e.total; + this.setState({ progress: 10 + (p * 40) }); + }; + xhr.onload = async (e) => { + try { + this.setState({ progress: 50 }); + this._tryDisplayFile(xhr.responseText, 50); + } catch (e) { + this.setState({ + state: ViewState.WAIT_FOR_FILE, + errorMessage: `${e}` + }); + } + }; + xhr.onerror = (e) => { + this.setState({ + state: ViewState.WAIT_FOR_FILE, + errorMessage: `${e}` + }); + }; + xhr.send(); + } + + public componentDidMount() { + // Check for url parameter. + const hash = window.location.hash; + const urlIndex = hash.indexOf('url='); + if (urlIndex !== -1) { + const url = hash.slice(urlIndex + 4); + this._loadFromUrl(url); + } + } + private _onFileSelect() { const input = this.refs['file_select'] as HTMLInputElement; const files = input.files; @@ -55,23 +119,7 @@ export default class App extends React.Component<{}, AppState> { const reader = new FileReader(); reader.onload = async (e) => { try { - const bleakResults = BLeakResults.FromJSON(JSON.parse((e.target as FileReader).result as string)); - const sourceFileManager = await SourceFileManager.FromBLeakResults(bleakResults, (completed, total) => { - const percent = 10 + (completed / total) * 90; - this.setState({ - progress: percent, - progressMessage: `${completed} of ${total} source files formatted...` - }); - }); - const sourceFiles = sourceFileManager.getSourceFiles(); - const stackTraces = StackTraceManager.FromBLeakResults(sourceFileManager, bleakResults); - this.setState({ - state: ViewState.DISPLAYING_FILE, - bleakResults, - sourceFileManager, - stackTraces, - selectedLocation: new Location(sourceFiles[0], 1, 1, true) - }); + this._tryDisplayFile((e.target as FileReader).result as string, 10); } catch (e) { this.setState({ state: ViewState.WAIT_FOR_FILE, diff --git a/yarn.lock b/yarn.lock index 9714bfd..de2058d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1233,6 +1233,10 @@ commander@2.9.0: dependencies: graceful-readlink ">= 1.0.0" +commander@~2.13.0: + version "2.13.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.13.0.tgz#6964bca67685df7c1f1430c584f07d7597885b9c" + commondir@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" @@ -3321,6 +3325,12 @@ rollup-plugin-sourcemaps@^0.4.2: rollup-pluginutils "^2.0.1" source-map-resolve "^0.5.0" +rollup-plugin-uglify@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/rollup-plugin-uglify/-/rollup-plugin-uglify-3.0.0.tgz#a34eca24617709c6bf1778e9653baafa06099b86" + dependencies: + uglify-es "^3.3.7" + rollup-pluginutils@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/rollup-pluginutils/-/rollup-pluginutils-2.0.1.tgz#7ec95b3573f6543a46a6461bd9a7c544525d0fc0" @@ -3468,7 +3478,7 @@ source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.1: version "0.5.6" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412" -source-map@^0.6.0, source-map@~0.6.0: +source-map@^0.6.0, source-map@~0.6.0, source-map@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" @@ -3689,6 +3699,13 @@ ua-parser-js@^0.7.9: version "0.7.17" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.17.tgz#e9ec5f9498b9ec910e7ae3ac626a805c4d09ecac" +uglify-es@^3.3.7: + version "3.3.9" + resolved "https://registry.yarnpkg.com/uglify-es/-/uglify-es-3.3.9.tgz#0c1c4f0700bed8dbc124cdb304d2592ca203e677" + dependencies: + commander "~2.13.0" + source-map "~0.6.1" + uglify-js@^2.6: version "2.8.29" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd"