├── .gitignore ├── README.md ├── pace.js ├── package.json ├── screenshot.png └── test ├── advanced.js └── simple.js /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | node_modules 15 | npm-debug.log -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Pace 2 | ==== 3 | 4 | A node.js module that outputs a progress bar and other metrics to the command-line. 5 | It was originally conceived to help measure the 'pace' of long running scripts. 6 | We've used it to optimize scripts that would have taken hours to complete down 7 | to minutes, without having to wait the hours before knowing that the script 8 | could use some optimization. 9 | 10 | Installation 11 | ------------ 12 | ``` 13 | $ npm install pace 14 | ``` 15 | 16 | Example 17 | ------- 18 | Running the following code: 19 | 20 | ```js 21 | var total = 50000, 22 | count = 0, 23 | pace = require('pace')(total); 24 | 25 | while (count++ < total) { 26 | pace.op(); 27 | 28 | // Cause some work to be done. 29 | for (var i = 0; i < 1000000; i++) { 30 | count = count; 31 | } 32 | } 33 | ``` 34 | 35 | Will cause output to your console similar to: 36 | 37 | ![Sample progress bar output](https://github.com/cpsubrian/pace/raw/master/screenshot.png) 38 | 39 | Usage 40 | ----- 41 | ### `Pace` object ### 42 | The module exports a factory function to generate instances of `Pace` objects. 43 | So `require('pace')()` creates an instance of `Pace`, passing 44 | `options` to the constructor. 45 | 46 | ### Options ### 47 | Options can either be an object literal, or an integer. If its an integer then 48 | it is the same as passing options with only the `total` specified. 49 | 50 | ```js 51 | require('pace')(100); 52 | 53 | // Same as 54 | 55 | require('pace')({total: 100}); 56 | ``` 57 | 58 | Supported Options: 59 | 60 | * `total` - The total number of operations that _YOUR_ script will execute. 61 | * `maxBurden` - The maximum 'burden' that the progress bar should incur. See more about burden below. 62 | * `showBurden` - Mostly for debugging. Show the current burden / skipped steps with the other metrics. 63 | 64 | ### pace.op([count]) ### 65 | Signal to pace that an operation was completed in your script by calling 66 | `pace.op()`. 67 | 68 | If you would rather track the progress in your own logic, you can call 69 | `pace.op()` where `` is the current operation interation 70 | (for example step # 50 of a 100 step process). 71 | 72 | ### pace.total ### 73 | If your script has a dynamic amount of work to do (for example, depending on the 74 | results of previous operation there may be more steps to complete), you can 75 | freely change the value of pace.total. Just set the value like: `pace.total = 200`. 76 | 77 | Burden 78 | ------ 79 | Depending on how intensive your operations are, calculating, formatting, and 80 | printing the progress bar might be much more expensive than the work you 81 | are doing. It would be silly if printing a progress bar caused your 82 | job to take significantly longer than it would have otherwise. _Pace_ tracks 83 | a stat called 'burden', which is basically a percentage of the overall 84 | execution time that is being spent inside the progress bar logic itself. 85 | 86 | The default `maxBurden` is `0.5`, which translates to `0.5% of the total execution 87 | time`. If this low burden is causing you to see progress reported less 88 | often than you would prefer, you can raise it to something like `20` (20%) via 89 | the `maxBurden` option. 90 | 91 | Examples 92 | -------- 93 | The `test/` folder contains some simple test scripts you can run to see the 94 | progress bar in action. 95 | 96 | 97 | - - - 98 | 99 | ### Developed by [Terra Eclipse](http://www.terraeclipse.com) 100 | Terra Eclipse, Inc. is a nationally recognized political technology and 101 | strategy firm located in Aptos, CA and Washington, D.C. 102 | 103 | - - - 104 | 105 | ### License: MIT 106 | Copyright (C) 2012 Terra Eclipse, Inc. ([http://www.terraeclipse.com](http://www.terraeclipse.com)) 107 | 108 | Permission is hereby granted, free of charge, to any person obtaining a copy 109 | of this software and associated documentation files (the "Software"), to deal 110 | in the Software without restriction, including without limitation the rights 111 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 112 | copies of the Software, and to permit persons to whom the Software is furnished 113 | to do so, subject to the following conditions: 114 | 115 | The above copyright notice and this permission notice shall be included in 116 | all copies or substantial portions of the Software. 117 | 118 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 119 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 120 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 121 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 122 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 123 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 124 | SOFTWARE. 125 | -------------------------------------------------------------------------------- /pace.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Pace 3 | * 4 | * A progress bar for the command-line. 5 | * 6 | * Example usage: 7 | * 8 | * var total = 50000, 9 | * count = 0, 10 | * pace = require('pace')(total); 11 | * 12 | * while (count++ < total) { 13 | * pace.op(); 14 | * 15 | * // Cause some work to be done. 16 | * for (var i = 0; i < 1000000; i++) { 17 | * count = count; 18 | * } 19 | * } 20 | */ 21 | 22 | // Module dependencies. 23 | var charm = require('charm'); 24 | 25 | /** 26 | * Pace 'class'. 27 | */ 28 | function Pace(options) { 29 | options = options || {}; 30 | 31 | // Total number of items to process. 32 | if (!options.total) { 33 | throw new Error('You MUST specify the total number of operations that will be processed.'); 34 | } 35 | this.total = options.total; 36 | 37 | // Current item number. 38 | this.current = 0; 39 | 40 | // Maximum percent of total time the progressbar is allowed to take during processing. 41 | // Defaults to 0.5% 42 | this.max_burden = options.maxBurden || 0.5; 43 | 44 | // Whether to show current burden %. 45 | this.show_burden = options.showBurden || false; 46 | 47 | // Internal time tracking properties. 48 | this.started = false; 49 | this.size = 50; 50 | this.inner_time = 0; 51 | this.outer_time = 0; 52 | this.elapsed = 0; 53 | this.time_start = 0; 54 | this.time_end = 0; 55 | this.time_left = 0; 56 | this.time_burden = 0; 57 | this.skip_steps = 0; 58 | this.skipped = 0; 59 | this.aborted = false; 60 | 61 | // Setup charm. 62 | this.charm = charm(); 63 | this.charm.pipe(process.stdout); 64 | 65 | // Prepare the output. 66 | this.charm.write("\n\n\n"); 67 | } 68 | 69 | /** 70 | * Export a factory function for new pace instances. 71 | */ 72 | module.exports = function(options) { 73 | if (typeof options === 'number') { 74 | options = { 75 | total: options 76 | }; 77 | } 78 | return new Pace(options); 79 | }; 80 | 81 | /** 82 | * An operation has been emitted. 83 | */ 84 | Pace.prototype.op = function op(count) { 85 | if (count) { 86 | this.current = count; 87 | } 88 | else { 89 | this.current++; 90 | } 91 | 92 | if (this.burdenReached()) { 93 | return; 94 | } 95 | 96 | // Record the start time of the whole task. 97 | if (!this.started) { 98 | this.started = new Date().getTime(); 99 | } 100 | 101 | // Record start time. 102 | this.time_start = new Date().getTime(); 103 | 104 | this.updateTimes(); 105 | this.clear(); 106 | this.outputProgress(); 107 | this.outputStats(); 108 | this.outputTimes(); 109 | 110 | // The task is complete. 111 | if (this.current >= this.total) { 112 | this.finished(); 113 | } 114 | 115 | // Record end time. 116 | this.time_end = new Date().getTime(); 117 | this.inner_time = this.time_end - this.time_start; 118 | }; 119 | 120 | /** 121 | * Update times. 122 | */ 123 | Pace.prototype.updateTimes = function updateTimes() { 124 | this.elapsed = this.time_start - this.started; 125 | if (this.time_end > 0) { 126 | this.outer_time = this.time_start - this.time_end; 127 | } 128 | if (this.inner_time > 0 && this.outer_time > 0) { 129 | // Set Current Burden 130 | this.time_burden = (this.inner_time / (this.inner_time + this.outer_time)) * 100; 131 | 132 | // Estimate time left. 133 | this.time_left = (this.elapsed / this.current) * (this.total - this.current); 134 | 135 | if (this.time_left < 0) this.time_left = 0; 136 | } 137 | // If our "burden" is too high, increase the skip steps. 138 | if (this.time_burden > this.max_burden && (this.skip_steps < (this.total / this.size))) { 139 | this.skip_steps = Math.floor(++this.skip_steps * 1.3); 140 | } 141 | }; 142 | 143 | /** 144 | * Move the cursor back to the beginning and clear old output. 145 | */ 146 | Pace.prototype.clear = function clear() { 147 | this.charm.erase('line').up(1).erase('line').up(1).erase('line').write("\r"); 148 | }; 149 | 150 | /** 151 | * Output the progress bar. 152 | */ 153 | Pace.prototype.outputProgress = function outputProgress() { 154 | this.charm.write('Processing: '); 155 | this.charm.foreground('green').background('green'); 156 | for (var i = 0; i < ((this.current / this.total) * this.size) - 1 ; i++) { 157 | this.charm.write(' '); 158 | } 159 | this.charm.foreground('white').background('white'); 160 | while (i < this.size - 1) { 161 | this.charm.write(' '); 162 | i++; 163 | } 164 | this.charm.display('reset').down(1).left(100); 165 | }; 166 | 167 | /** 168 | * Output numerical progress stats. 169 | */ 170 | Pace.prototype.outputStats = function outputStats() { 171 | this.perc = (this.current/this.total)*100; 172 | this.perc = padLeft(this.perc.toFixed(2), 2); 173 | this.charm.write(' ').display('bright').write(this.perc + '%').display('reset'); 174 | this.total_len = formatNumber(this.total).length; 175 | this.charm.write(' ').display('bright').write(padLeft(formatNumber(this.current), this.total_len)).display('reset'); 176 | this.charm.write('/' + formatNumber(this.total)); 177 | 178 | // Output burden. 179 | if (this.show_burden) { 180 | this.charm.write(' ').display('bright').write('Burden: ').display('reset'); 181 | this.charm.write(this.time_burden.toFixed(2) + '% / ' + this.skip_steps); 182 | } 183 | 184 | this.charm.display('reset').down(1).left(100); 185 | }; 186 | 187 | /** 188 | * Output times. 189 | */ 190 | Pace.prototype.outputTimes = function outputTimes() { 191 | // Output times. 192 | var hours = Math.floor(this.elapsed / (1000 * 60 * 60)); 193 | var min = Math.floor(((this.elapsed / 1000) % (60 * 60)) / 60); 194 | var sec = Math.floor((this.elapsed / 1000) % 60); 195 | 196 | this.charm.write(' ').display('bright').write('Elapsed: ').display('reset'); 197 | this.charm.write(hours + 'h ' + min + 'm ' + sec + 's'); 198 | 199 | if (this.time_left){ 200 | hours = Math.floor(this.time_left / (1000 * 60 * 60)); 201 | min = Math.floor(((this.time_left / 1000) % (60 * 60)) / 60); 202 | sec = Math.ceil((this.time_left / 1000) % 60); 203 | 204 | this.charm.write(' ').display('bright').write('Remaining: ').display('reset'); 205 | this.charm.write(hours + 'h ' + min + 'm ' + sec + 's'); 206 | } 207 | }; 208 | 209 | /** 210 | * The progress has finished. 211 | */ 212 | Pace.prototype.finished = function finished() { 213 | this.charm.write("\n\n"); 214 | this.charm.write('Finished!'); 215 | this.charm.write("\n\n"); 216 | }; 217 | 218 | /** 219 | * Check if the burden threshold has been reached. 220 | */ 221 | Pace.prototype.burdenReached = function burdenReached() { 222 | // Skip this cycle if the burden has determined we should. 223 | if ((this.skip_steps > 0) && (this.current < this.total)) { 224 | if (this.skipped < this.skip_steps) { 225 | this.skipped++; 226 | return true; 227 | } 228 | else { 229 | this.skipped = 0; 230 | } 231 | } 232 | return false; 233 | }; 234 | 235 | 236 | /** 237 | * Utility functions. 238 | */ 239 | 240 | // Left-pad a string. 241 | function padLeft(str, length, pad) { 242 | pad = pad || ' '; 243 | while (str.length < length) 244 | str = pad + str; 245 | return str; 246 | } 247 | 248 | // Ported from php.js. Same has php's number_format(). 249 | function formatNumber(number, decimals, dec_point, thousands_sep) { 250 | number = (number + '').replace(/[^0-9+\-Ee.]/g, ''); 251 | var n = !isFinite(+number) ? 0 : +number, 252 | prec = !isFinite(+decimals) ? 0 : Math.abs(decimals), 253 | sep = (typeof thousands_sep === 'undefined') ? ',' : thousands_sep, 254 | dec = (typeof dec_point === 'undefined') ? '.' : dec_point, 255 | s = '', 256 | toFixedFix = function (n, prec) { 257 | var k = Math.pow(10, prec); 258 | return '' + Math.round(n * k) / k; 259 | }; 260 | // Fix for IE parseFloat(0.55).toFixed(0) = 0; 261 | s = (prec ? toFixedFix(n, prec) : '' + Math.round(n)).split('.'); 262 | if (s[0].length > 3) { 263 | s[0] = s[0].replace(/\B(?=(?:\d{3})+(?!\d))/g, sep); 264 | } 265 | if ((s[1] || '').length < prec) { 266 | s[1] = s[1] || ''; 267 | s[1] += new Array(prec - s[1].length + 1).join('0'); 268 | } 269 | return s.join(dec); 270 | } 271 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Brian Link ", 3 | "name": "pace", 4 | "description": "Command-line progress bar and progress metrics. Helps you measure the 'pace' of a long-running script.", 5 | "version": "0.0.4", 6 | "homepage": "http://cantina.github.com", 7 | "repository": { 8 | "type": "git", 9 | "url": "git://github.com/cpsubrian/pace.git" 10 | }, 11 | "main": "pace.js", 12 | "scripts": {}, 13 | "dependencies": { 14 | "charm": "~0.1.0" 15 | }, 16 | "devDependencies": {}, 17 | "optionalDependencies": {}, 18 | "engines": { 19 | "node": "*" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cpsubrian/pace/b22cecc6cb751e4fb42a92313a96fe8ba0d4bab4/screenshot.png -------------------------------------------------------------------------------- /test/advanced.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Advanced test of pace.js. 3 | * 4 | * Set the current position in op() and also randomly increase the total. 5 | */ 6 | 7 | var total = 50000, 8 | current = 0, 9 | pace = require('../')(total); 10 | 11 | while (current++ < total) { 12 | if (Math.random() > 0.9) { 13 | pace.op(current); 14 | } 15 | 16 | if (Math.random() < 0.05 && total <= 50000) { 17 | total += Math.floor(Math.random() * 100); 18 | pace.total = total; 19 | } 20 | 21 | // Cause some work to be done. 22 | for (var i = 0; i < 1000000; i++) { 23 | current = current; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test/simple.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Simple test of pace.js. 3 | * 4 | * Play with maxBurden to see how it effects total execution speed and the 5 | * progress bar refresh rate. 6 | */ 7 | 8 | var total = 50000, 9 | count = 0, 10 | pace = require('../')({total: total, showBurden: true, maxBurden: 0.5}); 11 | 12 | while (count++ < total) { 13 | pace.op(); 14 | 15 | // Cause some work to be done. 16 | for (var i = 0; i < 1000000; i++) { 17 | count = count; 18 | } 19 | } 20 | --------------------------------------------------------------------------------