{"id":23280978,"url":"https://github.com/nicholaswmin/heapstats","last_synced_at":"2025-04-06T13:35:44.699Z","repository":{"id":241981793,"uuid":"808373928","full_name":"nicholaswmin/heapstats","owner":"nicholaswmin","description":"terminal-based heap allocation plotter","archived":false,"fork":false,"pushed_at":"2024-06-21T23:43:59.000Z","size":11149,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-03-29T02:47:05.609Z","etag":null,"topics":["garbage-collector","v8"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/nicholaswmin.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-05-31T00:06:12.000Z","updated_at":"2024-06-21T23:44:03.000Z","dependencies_parsed_at":"2024-06-07T11:01:28.543Z","dependency_job_id":"e167a08d-15ea-4540-891e-ebc10a32ee4a","html_url":"https://github.com/nicholaswmin/heapstats","commit_stats":null,"previous_names":["nicholaswmin/memstat","nicholaswmin/memplot"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nicholaswmin%2Fheapstats","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nicholaswmin%2Fheapstats/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nicholaswmin%2Fheapstats/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nicholaswmin%2Fheapstats/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/nicholaswmin","download_url":"https://codeload.github.com/nicholaswmin/heapstats/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247489300,"owners_count":20947086,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["garbage-collector","v8"],"created_at":"2024-12-19T23:39:48.975Z","updated_at":"2025-04-06T13:35:44.676Z","avatar_url":"https://github.com/nicholaswmin.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![test-workflow][test-workflow-badge]][ci-test]\n\n# heapstats\n\nA terminal-based [heap allocation][gc-wiki] plotter for [Node.js][node]\n\nPlots memory usage \u0026 garbage collection stats by [V8][v8]'s\n[trace-based][tracing-gc-wiki] GC\n\n![Mocha test results showing memory usage as an ASCII timeline plot][demo-img]\n\n## Install\n\n```bash\nnpm i heapstats\n```\n\n## Usage\n\n### Testing functions\n\nRun a [leaky-prone function][leaky-func-sync] 100 times \u0026 check if it leaks:\n\n```js\nimport Heapstats from 'heapstats'\n\nconst heap = Heapstats()\n\nfor (let i = 0; i \u003c 100; i++)\n  heap.sample(() =\u003e addTwoNumbers(5, 3))\n\nconsole.log(heap.stats().plot)\n```\n\n### Testing async functions\n\nSame as above but this time it's an [async function][leaky-func-async]:\n\n```js\nimport Heapstats from 'heapstats'\n\nconst heap = Heapstats()\n\nfor (let i = 0; i \u003c 100; i++)\n  await heap.sample(() =\u003e addTwoNumbers(5, 3))\n\nconsole.log(heap.stats().plot)\n```\n\nAssuming it does leaks memory, you'll see something like this:\n\n```text\n                      Heap Allocation Timeline\n\nCur: 21.79 MB\nMax: 56.77 MB              Heap increased: 376%\n      ╷\n56.77 ┼                                 ╭─╮                                 \n45.62 ┤                            ╭────╯ │    ╭────╮     ╭────╮   \n34.46 ┤                      ╭─────╯      ╰────╯    │  ╭──╯    │\n23.31 ┤            ╭────╮  ╭─╯                      ╰──╯       ╰─  \n12.15 ┤       ╭────╯    ╰──╯                                       \n 1.00 ┼───────╯                                                             \n      ┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬──\n\nInitial: 4.57 MB                                   GC Cycles: 27\n```\n\n### Testing an indeterminate process\n\nWhen testing something indeterminate, like an HTTP request, just\ninstantiate it, do whatever work needs to be done and then get `.stats()`\nas usual.\n\nFor example:\n\n```js\n\nconst heap = Heapstats()\n\napp.get('/users', (req, res) =\u003e {\n  const users = await leakyDatabaseCall()\n\n  res.json(users)\n\n  console.log(heap.stats().plot)\n})\n```\n\nIt starts collecting memory stats when you instantiate it - that is *if* a\ngarbage collection/compaction actually takes place.\n\nNo need to call `sample(() =\u003e someFunction())` in this case.\n\n### Heap statistics\n\nApart from the ASCII plot, `heap.stats()` returns:\n\n```js\n{\n   // heap size on instantiation\n  \"initial\": 4.50,\n\n  // heap size now\n  \"current\": 21.30,\n\n  // max. heap size reached\n  \"max\": 32.00,\n\n   // diff. of initial and current, expressed as %\n  \"increasePercentage\": 2.4,\n\n   // collected stats of each garbage collection\n  \"snapshots\": [V8HeapStats, V8HeapStats ...]\n}\n```\n\nsizes are in megabytes (MB).  \n\nA `V8HeapStats` object contains garbage collection statistics as captured by\n[v8.getHeapStatistics()][v8-heap-doc].  \n\nFor more info about garbage collection in [V8][v8], [read here][oil].\n\n## As a unit-testing utility\n\nIn [Mocha][mocha], you can pass the test context like so:\n\n```js\n\nHeapstats({ test: this })\n```\n\nthe plot will auto-draw next to its test - but only if the test **fails**.\n\n\n```js\ndescribe('#addTwoNumbers() - memory profiling', function() {\n\n  beforeEach(async function() {\n    this.heap = Heapstats({ test: this }) // pass `test: this`\n\n    for (let i = 0; i \u003c 200; i++)\n      this.heap.sample(() =\u003e addTwoNumbers(2, 3))\n  })\n\n  it ('does not leak memory', function() {\n    expect(this.heap.stats().current).to.be.below(10)\n  })\n\n  it ('does not exceed 100 MB in memory usage', function() {\n    expect(this.heap.stats().max).to.be.below(100)\n  })\n\n  it ('has not increased by more than 2%', function() {\n    expect(this.heap.stats().increasePercentage).to.be.below(2)\n  })\n\n  // ... and so on ...\n})\n```\n\n\u003e [!IMPORTANT]  \n\u003e Avoid arrow functions/lambdas in Mocha `describe`/`it` callbacks, otherwise\n\u003e `this` would be lexically bound to the wrong value. Mocha itself heavily\n\u003e [discourages their use][no-mocha-arrow].  \n\n## Tail mode\n\nTo observe realtime heap statistics:\n\n\n```js\nimport Heapstats from 'heapstats'\n```\n\nand start with flag `--heapstats`, i.e:\n\n```bash\nnode app.js --heapstats\n```\n\n![animation of realtime memory usage as a line plot, in terminal][tail-img]\n\n## Configuration\n\n```js\n\nconst heap = new Heapstats({\n  // plot size\n  window: { columns: 100, rows: 25 },\n\n  // test context, if used with mocha\n  test: this,\n\n  // draw plot if test ends in:\n  plotOnTest: ['passed', 'failed']\n})\n```\n\n## Test\n\n```bash\nnpm i\n```\n\nthen:\n\n```bash\nnpm test\n```\n\nTo run a specific test in isolation, i.e `stats.spec.js`:\n\n\n```bash\nnpx mocha test/stats.spec.js --no-package --global leak\n```\n\n## Gotchas\n\n### Just avoid profiling\n\nModern GC's don't have concrete rules on when to run and how to run.  \nTheir method of operation is intentionally abstracted from the user (you)\nfor a good reason so messing with it when testing is almost certain to make\nyour unit tests [brittle and flaky][brittle].\n\nI only wrote this because I needed a visual way to triage unit-tests when I was\nmessing with [Streams][streams], which have explicit resource acquisition\nand release steps.\n\n### Flatlined stats\n\nGarbage Collectors won't run if you're using (or abusing) small-ish\namounts of memory regardless if whatever you're doing is actually causing\na leak.\n\nIn these cases you'll see a flatlined plot and a low number of\n`heap.stats().snapshots.length`.\n\nThere's always a bit of a weird dilemma in figuring out if there's no leak or\nif the GC has simply not kicked in yet.\n\nThe best way to go about it is to setup your test in a way that really\nstresses the function under test and wait for a cycle. This means you\nshould run your function `n` amount of times, not just once. The `n` here\ncan easily mean tens of thousands or even millions of iterations.\n\nIf your function is expected to use *some* memory, the GC will **have to**\neventually kick in to deallocate it - there's no other way to manually\ndeallocate memory in the language.\n\nIf you're not seeing a cycle being logged and you're impatient about the whole\nthing, you can always just force the GC to run by calling `global.gc()`.\n\n### Don't trust single cycles\n\nI've never seen a real memory leak where memory usage increases monotonically\nand linearly until it eventually runs out of memory.\n\nA real memory leak, the kind that could happen in your actual production system,\nwill usually present itself in a [\"seesaw\" pattern][leak-pattern].\n\nThis means a GC cycle takes place, memory usage drops - but it just doesn't\nquite drop entirely back to baseline.  \n\nThere's always just a little bit extra being held after each cycle.\n\nIt's only when you project this effect over an extended period of time that you\ncan clearly tell that it's a leak.\n\nDon't rely on the results of just 1 cycle.\n\n## License\n\n\u003e © 2024 Nicholas Kyriakides\n\u003e\n\u003e The MIT No Attribution License  \n\u003e\n\u003e Permission is hereby granted, free of charge, to any person obtaining a copy\n\u003e of this software and associated documentation files (the \"Software\"), to deal\n\u003e in the Software *without restriction*, including without limitation the rights\n\u003e to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n\u003e copies of the Software, and to permit persons to whom the Software is\n\u003e furnished to do so.\n\n\n[test-workflow-badge]: https://github.com/nicholaswmin/memstat/actions/workflows/tests.yml/badge.svg\n[ci-test]: https://github.com/nicholaswmin/heapstats/actions/workflows/tests.yml\n[demo-img]: https://raw.githubusercontent.com/nicholaswmin/mine/main/public/heapstats/assets/demo.png\n[tail-img]: https://raw.githubusercontent.com/nicholaswmin/mine/main/public/heapstats/assets/demo-tail.gif\n\n[nicholaswmin]: https://github.com/nicholaswmin\n[async-func]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function\n[await]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await\n[node]: https://nodejs.org/en\n[v8]: https://v8.dev/\n[oil]: https://v8.dev/blog/oilpan-library\n[gc-wiki]: https://en.wikipedia.org/wiki/Garbage_collection_(computer_science)\n[tracing-gc-wiki]: https://en.wikipedia.org/wiki/Tracing_garbage_collection\n[v8-heap-doc]: https://nodejs.org/api/v8.html#v8getheapstatsistics\n[mocha]: https://mochajs.org/\n[no-mocha-arrow]: https://mochajs.org/#arrow-functions\n[sampling]: https://en.wikipedia.org/wiki/Sampling_(statistics)\n[gc-update]: https://v8.dev/blog/trash-talk\n\n[mocha-example]: .github/examples/mocha.spec.js\n[leaky-func-sync]: .github/examples/basic.js#L7-L14\n[leaky-func-async]: .github/examples/basic-async.js#L7-L14\n\n[streams]: https://nodejs.org/en/learn/modules/backpressuring-in-streams\n[ee]: https://nodejs.org/en/learn/asynchronous-work/the-nodejs-event-emitter\n[stream-handling]: https://github.com/nodejs/help/issues/1979\n[brittle]: https://softwareengineering.stackexchange.com/a/356238/108346\n[leak-pattern]: https://www.researchgate.net/figure/Linearly-increasing-memory-leak-pattern_fig2_352479475\n\n[memlab]: https://engineering.fb.com/2022/09/12/open-source/memlab/\n[node-memwatch]: https://github.com/airbnb/node-memwatch\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnicholaswmin%2Fheapstats","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnicholaswmin%2Fheapstats","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnicholaswmin%2Fheapstats/lists"}