├── bin └── dennard.js ├── .npmignore ├── package.json ├── .gitignore ├── LICENSE ├── src ├── index.js ├── win.js ├── unix.js └── utilities.js └── README.md /bin/dennard.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | require('../src/index').dennard() 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | src 3 | test 4 | .vscode 5 | appveyor.yml 6 | .babelrc 7 | .gitignore 8 | yarn-error.log -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dennard", 3 | "version": "0.2.0", 4 | "description": "Tiny Node cli tool to see a local app's memory footprint, cross-platform", 5 | "bin": { 6 | "dennard": "./bin/dennard.js" 7 | }, 8 | "main": "src/dennard.js", 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "author": "Felix Rieseberg ", 13 | "license": "MIT", 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/felixrieseberg/dennard.git" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | 29 | lib 30 | !test/lib 31 | 32 | yarn-error.log -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Felix Rieseberg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const readline = require('readline') 2 | 3 | const { getLogTable } = require('./utilities') 4 | 5 | function dennard() { 6 | let dennard 7 | 8 | if (process.platform === 'win32') { 9 | ({ 10 | dennard 11 | } = require('./win')) 12 | } else { 13 | ({ 14 | dennard 15 | } = require('./unix')) 16 | } 17 | 18 | const nameCandidateArgs = process 19 | .argv 20 | .filter((v) => v !== '-w' && v !== '--watch') 21 | const name = nameCandidateArgs[nameCandidateArgs.length - 1] 22 | 23 | const isWatch = process.argv.find((v) => v === '-w' || v === '--watch') 24 | 25 | if (!isWatch) { 26 | console.log(getLogTable(dennard(name))) 27 | } else { 28 | let lines = [] 29 | 30 | const runner = () => { 31 | const logTable = getLogTable(dennard(name)) 32 | 33 | // Clear 34 | lines.slice(1).forEach(() => { 35 | readline.moveCursor(process.stdout, 0, -1) 36 | readline.clearLine(process.stdout, 0) 37 | }) 38 | 39 | lines = logTable.split('\n') 40 | process.stdout.write(logTable) 41 | } 42 | 43 | runner() 44 | setInterval(runner, 2000) 45 | } 46 | } 47 | 48 | module.exports = { 49 | dennard 50 | } -------------------------------------------------------------------------------- /src/win.js: -------------------------------------------------------------------------------- 1 | const { execSync } = require('child_process') 2 | 3 | function getPsCmd(name = '') { 4 | const gp = `Get-Process -Name ${name}` 5 | const so = `Select-Object` 6 | const format = `Name='WorkingSet';Expression={($_.WorkingSet/1MB)}` 7 | const errorAction = `-ErrorAction SilentlyContinue` 8 | 9 | return `${gp} ${errorAction} | ${so} Id,Name,@{${format}}` 10 | } 11 | 12 | function getProcessResult(name = '') { 13 | const cmd = getPsCmd(name) 14 | 15 | return execSync(`powershell.exe "${cmd}"`).toString() 16 | } 17 | 18 | function analyzeResult(result = '') { 19 | const processes = result 20 | .split('\n') 21 | .map((k) => k.trim()) 22 | .slice(3) 23 | .map((v) => /^(\d*) (.*) {1,99}(\d*\.?\d*)$/gi.exec(v)) 24 | .filter((v) => !!v) 25 | .map((m) => ({ 26 | pid: m[1], 27 | name: m[2], 28 | megabytes: parseInt(m[3], 10) 29 | })) 30 | const total = processes.reduce((c, { megabytes }) => c + megabytes, 0) 31 | 32 | return { processes, summary: { total } } 33 | } 34 | 35 | function dennard(name = '') { 36 | const result = getProcessResult(name) 37 | 38 | return analyzeResult(result) 39 | } 40 | 41 | module.exports = { 42 | dennard 43 | } 44 | -------------------------------------------------------------------------------- /src/unix.js: -------------------------------------------------------------------------------- 1 | const { execSync } = require('child_process') 2 | const { tmpdir } = require('os') 3 | const path = require('path') 4 | const fs = require('fs') 5 | 6 | /** 7 | * Return the pids matching the given name 8 | * 9 | * @param {string} name 10 | */ 11 | function getPids(name = '') { 12 | const cmd = `ps x -ao pid,command | grep "${name}" | awk '{print $1}'` 13 | const raw = execSync(cmd) 14 | const pids = raw 15 | .toString() 16 | .split('\n') 17 | .filter((p) => p) 18 | .filter((p) => parseInt(p, 10) < (process.pid - 1)) 19 | 20 | return pids 21 | } 22 | 23 | function getTempPath() { 24 | const tmp = path.join(tmpdir(), 'dennard') 25 | 26 | try { 27 | fs.mkdirSync(tmp) 28 | } catch (error) { 29 | // Already exists 30 | } 31 | 32 | return path.join(tmp, `${Date.now()}.json`) 33 | } 34 | 35 | function getDennard(pids = []) { 36 | const tmpFile = getTempPath() 37 | const pidParam = pids.map((p) => `-p ${p}`).join(' ') 38 | const cmd = `sudo footprint ${pidParam} --summary --json ${tmpFile}` 39 | 40 | execSync(cmd) 41 | 42 | return tmpFile 43 | } 44 | 45 | function analyzeDennardFile(file = '') { 46 | const content = fs.readFileSync(file, 'utf8') 47 | const parsed = JSON.parse(content) 48 | const processes = [] 49 | const bytesPerPage = parseInt(parsed['bytes per unit'], 10) 50 | const total = parseInt(parsed['total footprint'], 10) * bytesPerPage / 1024 / 1024 51 | 52 | for (const proc of parsed.processes) { 53 | const { name, footprint, pid } = proc 54 | const total = bytesPerPage * parseInt(footprint, 10) 55 | const megabytes = Math.round(total / 1024 / 1024 * 100) / 100 56 | 57 | processes.push({ 58 | name, 59 | pid, 60 | megabytes 61 | }) 62 | } 63 | 64 | fs.unlinkSync(file) 65 | 66 | return { processes, summary: { total } } 67 | } 68 | 69 | function dennard(name = '') { 70 | const pids = getPids(name) 71 | 72 | if (!pids || pids.length === 0) { 73 | console.warn(`No processes found for ${name}.`) 74 | process.exit() 75 | } 76 | 77 | const file = getDennard(pids) 78 | const data = analyzeDennardFile(file) 79 | 80 | return data 81 | } 82 | 83 | module.exports = { 84 | dennard 85 | } 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dennard 2 | 3 | A tiny 0-dependencies cross-platform Node.js module that shows the memory 4 | footprint for applications. Named after Robert Dennard, inventor of DRAM. 5 | 6 | ## Installation 7 | 8 | ``` 9 | npm i -g dennard 10 | ``` 11 | 12 | ## Usage 13 | 14 | ```sh 15 | dennard name-of-app [-w|--watch] 16 | ``` 17 | 18 | `dennard` will wildcard-match all processes with the given name. 19 | 20 | :warning: On macOS, the tool needs to run as `sudo`. Internally, this module 21 | calls `footprint`, which requires `sudo` privileges. 22 | 23 | ```sh 24 | dennard Chrome 25 | ┌──────────────────────────────┬───────────┬───────────────┐ 26 | │ name │ pid │ megabytes │ 27 | ├──────────────────────────────┼───────────┼───────────────┤ 28 | │ Google Chrome Helper │ 22801 │ 359.97 │ 29 | │ Google Chrome │ 21721 │ 260.6 │ 30 | │ Google Chrome Helper │ 21725 │ 242.6 │ 31 | │ Google Chrome Helper │ 21730 │ 151.89 │ 32 | │ Google Chrome Helper │ 36459 │ 130.96 │ 33 | │ Google Chrome Helper │ 21739 │ 96.16 │ 34 | │ Google Chrome Helper │ 21737 │ 75.79 │ 35 | │ Google Chrome Helper │ 21746 │ 62.71 │ 36 | │ Google Chrome Helper │ 21740 │ 56.5 │ 37 | │ Google Chrome Helper │ 36125 │ 44.46 │ 38 | │ Google Chrome Helper │ 21736 │ 37.01 │ 39 | │ Google Chrome Helper │ 21738 │ 28.68 │ 40 | │ Google Chrome Helper │ 57516 │ 22.93 │ 41 | │ crashpad_handler │ 21723 │ 1.59 │ 42 | │ AlertNotificationService │ 21728 │ 1.16 │ 43 | └──────────────────────────────┴───────────┴───────────────┘ 44 | 45 | Total memory footprint: 1469.1MB 46 | ``` 47 | 48 | ## Memory Information 49 | 50 | On Windows, `dennard` returns the size of the "[working set][working-set]". 51 | The working set consists of the pages of memory that were recently referenced 52 | by the process. 53 | 54 | On macOS and Linux, `dennard` gathers the sum of dirty/anonymous allocations 55 | in one or more processes along with their attributable kernel resources 56 | (KPRVT). Shared allocations only contribute to the footprint once, regardless of 57 | the number of times that they are mapped into any number of processes. The goal 58 | is for the resulting number to be as close as possible to what the macOS 59 | `Activity Monitor` reports as `Memory` usage. 60 | 61 | ## License 62 | MIT, please see `LICENSE` for details 63 | 64 | [working-set]: https://docs.microsoft.com/en-us/windows/desktop/memory/working-set 65 | -------------------------------------------------------------------------------- /src/utilities.js: -------------------------------------------------------------------------------- 1 | const UNIX_CHARS = { 2 | top: '─', 3 | topMid: '┬', 4 | topLeft: '┌', 5 | topRight: '┐', 6 | bottom: '─', 7 | bottomMid: '┴', 8 | bottomLeft: '└', 9 | bottomRight: '┘', 10 | left: '│', 11 | leftMid: '├', 12 | hMid: '─', 13 | midMid: '┼', 14 | right: '│', 15 | rightMid: '┤', 16 | vMid: '│' 17 | } 18 | 19 | const TABLE_CHARS = process.platform !== 'win32' 20 | ? UNIX_CHARS 21 | : UNIX_CHARS 22 | 23 | function repeatStr(str, length) { 24 | return str.padEnd(length, str) 25 | } 26 | 27 | function getHeaderLine(lengths = []) { 28 | return lengths.reduce((previous, current, i) => { 29 | const endChar = i === lengths.length - 1 30 | ? TABLE_CHARS.topRight 31 | : TABLE_CHARS.topMid 32 | 33 | return previous += repeatStr(TABLE_CHARS.top, current + 2) + endChar 34 | }, TABLE_CHARS.topLeft) 35 | } 36 | 37 | function getDividerLine(lengths = []) { 38 | return lengths.reduce((previous, current, i) => { 39 | const endChar = i === lengths.length - 1 40 | ? TABLE_CHARS.rightMid 41 | : TABLE_CHARS.midMid 42 | 43 | return previous += repeatStr(TABLE_CHARS.hMid, current + 2) + endChar 44 | }, TABLE_CHARS.leftMid) 45 | } 46 | 47 | function getBottomLine(lengths = []) { 48 | return lengths.reduce((previous, current, i) => { 49 | const endChar = i === lengths.length - 1 50 | ? TABLE_CHARS.bottomRight 51 | : TABLE_CHARS.bottomMid 52 | 53 | return previous += repeatStr(TABLE_CHARS.hMid, current + 2) + endChar 54 | }, TABLE_CHARS.bottomLeft) 55 | } 56 | 57 | function getLogTable({ processes, summary } = result) { 58 | const columns = {} 59 | const lengths = [] 60 | const lines = [] 61 | const total = Math.round(summary.total * 100) / 100 62 | 63 | if (processes.length === 0) { 64 | return `No processes found.` 65 | } 66 | 67 | processes.forEach((p) => { 68 | Object.keys(p).forEach((k) => { 69 | columns[k] = columns[k] || [] 70 | columns[k].push(p[k].toString()) 71 | }) 72 | }) 73 | 74 | // Calculate lengths 75 | Object.keys(columns).forEach((k) => { 76 | const length = Math.max(...(columns[k].map((el) => el.length)), k.length) + 4 77 | lengths.push(length) 78 | }) 79 | 80 | Object.keys(columns).forEach((k, i, arr) => { 81 | const isLastColumn = i === arr.length - 1 82 | const endChar = isLastColumn ? TABLE_CHARS.right : TABLE_CHARS.vMid 83 | 84 | // Names 85 | lines[0] = lines[0] || `${TABLE_CHARS.left} ` 86 | lines[0] += k.padEnd(lengths[i]) + ` ${endChar} ` 87 | 88 | // Columns 89 | columns[k].forEach((p, ii) => { 90 | lines[ii + 1] = lines[ii + 1] || `${TABLE_CHARS.left} ` 91 | lines[ii + 1] += p.padEnd(lengths[i]) + ` ${TABLE_CHARS.vMid} ` 92 | }) 93 | }) 94 | 95 | lines.splice(0, 0, getHeaderLine(lengths)) 96 | lines.splice(2, 0, getDividerLine(lengths)) 97 | lines.splice(lines.length, 0, getBottomLine(lengths)) 98 | lines.push(`\n Total memory footprint: ${total}MB\n`) 99 | return lines.join('\n') 100 | } 101 | 102 | module.exports = { 103 | getLogTable 104 | } 105 | --------------------------------------------------------------------------------