{"id":19303372,"url":"https://github.com/jsheaven/perf","last_synced_at":"2025-04-22T11:32:03.574Z","repository":{"id":65965509,"uuid":"603599409","full_name":"jsheaven/perf","owner":"jsheaven","description":"Estimates the average runtime and time-complexity (big O notation) of (a)sync algorithms","archived":false,"fork":false,"pushed_at":"2024-07-18T16:21:11.000Z","size":565,"stargazers_count":8,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-04-18T04:35:35.141Z","etag":null,"topics":["async","big-o-notation","complexity-analysis","javascript","parallel-computing","performance-testing","typescript"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jsheaven.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","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":"2023-02-19T02:05:44.000Z","updated_at":"2024-07-18T16:21:14.000Z","dependencies_parsed_at":"2024-11-09T23:27:00.666Z","dependency_job_id":"bcc35d1d-c6c7-40f3-ac11-8e7a032e5839","html_url":"https://github.com/jsheaven/perf","commit_stats":{"total_commits":8,"total_committers":1,"mean_commits":8.0,"dds":0.0,"last_synced_commit":"b8f0a82d4f61a90f8181f0a9ae3192c4420c1768"},"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jsheaven%2Fperf","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jsheaven%2Fperf/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jsheaven%2Fperf/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jsheaven%2Fperf/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jsheaven","download_url":"https://codeload.github.com/jsheaven/perf/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":250232193,"owners_count":21396588,"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":["async","big-o-notation","complexity-analysis","javascript","parallel-computing","performance-testing","typescript"],"created_at":"2024-11-09T23:26:10.750Z","updated_at":"2025-04-22T11:32:03.139Z","avatar_url":"https://github.com/jsheaven.png","language":"TypeScript","readme":"\u003ch1 align=\"center\"\u003e@jsheaven/perf\u003c/h1\u003e\n\n\u003e Estimates the average runtime and time-complexity (big O notation) of (a)sync algorithms\n\n\u003ch2 align=\"center\"\u003eUser Stories\u003c/h2\u003e\n\n1. As a developer, I want to know the performance characteristics/complexity and thus the scalability of algorithms\n\n\u003ch2 align=\"center\"\u003eFeatures\u003c/h2\u003e\n\n- ✅ Measures the runtime (duration) an algorithm (sync or async) takes\n- ✅ Estimates the time complexity of an algorithm (sync or async) in big O notation\n- ✅ Parallel execution using async generators\n- ✅ Streaming implementation, chunked\n- ✅ Available as a simple API\n- ✅ Just `1403 byte` nano sized (ESM, gizpped)\n- ✅ Tree-shakable and side-effect free\n- ✅ Runs on Windows, Mac, Linux, CI tested\n- ✅ First class TypeScript support\n- ✅ 100% Unit Test coverage\n\n\u003ch2 align=\"center\"\u003eExample usage (API)\u003c/h2\u003e\n\n\u003ch3 align=\"center\"\u003eSetup\u003c/h3\u003e\n\n- yarn: `yarn add @jsheaven/perf`\n- npm: `npm install @jsheaven/perf`\n- bun: `bun add @jsheaven/perf`\n\n\u003ch3 align=\"center\"\u003eESM\u003c/h3\u003e\n\n```ts\nimport { perf } from '@jsheaven/perf'\n\n// call your algorithm from within the candidate fn() object\n// the size is the input size - a number to use for scaling\n\n// O(1): Constant complexity.\nconst incrementByOne = {\n  name: 'LinearAdd',\n  fn: async (size, callIndex) =\u003e {\n    function incrementByOne(num) {\n      return num + 1\n    }\n    incrementByOne(size)\n  },\n}\n\n// O(n^x): Polynomial complexity\nconst bubbleSort = {\n  name: 'BubbleSort',\n  fn: async (size, callIndex) =\u003e {\n    const arr = Array.from({ length: size }).map(() =\u003e Math.random())\n    for (let i = 0; i \u003c arr.length; i++) {\n      for (let j = 0; j \u003c arr.length - 1; j++) {\n        if (arr[j] \u003e arr[j + 1]) {\n          const tmp = arr[j]\n          arr[j] = arr[j + 1]\n          arr[j + 1] = tmp\n        }\n      }\n    }\n  },\n}\n\n// pass one or more algorithms\nconst measurement = await perf([incrementByOne, bubbleSort])\n\nconsole.log('=== PERF: BubbleSort ===')\nconsole.log('runtime duration', measurement['BubbleSort'].duration, 'ms')\nconsole.log(JSON.stringify(measurement['BubbleSort'].estimatedDomains, null, 4))\n\nconsole.log('=== PERF: LinearAdd ===')\nconsole.log('runtime duration', measurement['LinearAdd'].duration, 'ms')\nconsole.log(JSON.stringify(measurement['LinearAdd'].estimatedDomains, null, 4))\n```\n\n\u003cimg src=\"screenshot.png\" height=\"300px\" /\u003e\n\n\u003ch3 align=\"center\"\u003eCommonJS\u003c/h3\u003e\n\n```ts\nconst { perf } = require('@jsheaven/perf')\n\n// same API like ESM variant\n```\n\n\u003ch2 align=\"center\"\u003eTime complexity estimation\u003c/h2\u003e\n\nBig O notation is a metric used in computer science to classify an algorithm based on its time and space complexity. It’s written as O(x) and is based on how the algorithm would scale with an increase or decrease in the amount of input data.\n\nThe time and space here is not based on the actual number of operations performed or the amount of memory used per se, but rather how the algorithm would scale with an increase or decrease in the amount of input data.\n\nThe notation represents how an algorithm will run in a worst-case scenario, in other words what the maximum time or space an algorithm could use is. The complexity is written as O(x), where x is the growth rate of the algorithm in regards to n, which is the amount of data input. Throughout the rest of this post, input will be referred to as n.\n\n\u003cimg align=\"center\" src=\"https://upload.wikimedia.org/wikipedia/commons/4/4e/Big-O_Cheatsheet.png\" height=\"300px\" /\u003e\n\n\u003ch3 align=\"center\"\u003eTypes of Big O Notations\u003c/h3\u003e\n\nThere are seven common types of big O notations. These include:\n\n1. O(1): Constant complexity.\n2. O(logn): Logarithmic complexity.\n3. O(n): Linear complexity.\n4. O(nlogn): Loglinear complexity.\n5. O(n^x): Polynomial complexity.\n6. O(X^n): Exponential time.\n7. O(n!): Factorial complexity.\n\n\u003ch4 align=\"center\"\u003eO(1): Constant Complexity\u003c/h4\u003e\n\nO(1) is known as constant complexity. This implies that the amount of time or memory does not scale with n. For time complexity, this means that n is not iterated on or recursed. Generally, a value will be selected and returned, or a value will be operated on and returned.\n\n\u003e operations are linear\n\n```ts\nconst factorizeList = (n: number, list: Array\u003cnumber\u003e) =\u003e list[n] * 2\n```\n\nFor space, no data structures can be created that are multiples of the size of n. Variables can be declared, but the number must not change with n.\n\n\u003ch4 align=\"center\"\u003eO(logn): Logarithmic Complexity\u003c/h4\u003e\n\nO(logn) is known as logarithmic complexity. The logarithm in O(logn) has a base of two. The best way to wrap your head around this is to remember the concept of halving. Every time n increases by an amount k, the time or space increases by k/2. There are several common algorithms that are O(logn) a vast majority of the time, including: binary search, searching for a term in a binary search tree and adding items to a heap.\n\n```ts\nconst needle = Math.random()\nconst haystack = Array.from({ length: size }, () =\u003e Math.random())\nhaystack.push(needle)\n\nfunction binarySearch(array: number[], target: number): number {\n  let low = 0\n  let high = array.length - 1\n\n  while (low \u003c= high) {\n    const mid = Math.floor((low + high) / 2)\n\n    if (array[mid] === target) {\n      return mid\n    } else if (array[mid] \u003c target) {\n      low = mid + 1\n    } else {\n      high = mid - 1\n    }\n  }\n\n  return -1\n}\nbinarySearch(haystack, needle)\n```\n\nO(logn) space complexity commonly happens during recursive algorithms. When a recursive call is made, all current variables get placed on the stack and new ones are created. If the number of recursive calls increases logarithmically, i.e., n is halved with every recursive call, then the space complexity will be O(logn). Space complexities of O(logn) are rarer to encounter.\n\n\u003ch4 align=\"center\"\u003eO(n): Linear Complexity\u003c/h4\u003e\n\nO(n), or linear complexity, is perhaps the most straightforward complexity to understand. O(n) means that the time/space scales 1:1 with changes to the size of n. If a new operation or iteration is needed every time n increases by one, then the algorithm will run in O(n) time.\n\n```ts\nconst haystack = Array.from({ length: size }, () =\u003e Math.random())\n\nfunction sumArray(array: number[]): number {\n  let sum = 0\n  for (let i = 0; i \u003c array.length; i++) {\n    sum += array[i]\n  }\n  return sum\n}\nsumArray(haystack)\n```\n\nWhen using data structures, if one more element is needed every time n increases by one, then the algorithm will use O(n) space.\n\n\u003ch4 align=\"center\"\u003eO(nlogn): Loglinear Complexity\u003c/h4\u003e\n\nO(nlogn) is known as loglinear complexity. O(nlogn) implies that logn operations will occur n times. O(nlogn) time is common in recursive sorting algorithms, binary tree sorting algorithms and most other types of sorts.\n\n```ts\nconst arr = Array.from({ length: size }).map(() =\u003e Math.random())\n\nconst partition = (arr: number[], low: number, high: number): number =\u003e {\n  const pivot = arr[high]\n  let i = low - 1\n\n  for (let j = low; j \u003c high; j++) {\n    if (arr[j] \u003c pivot) {\n      i++\n      const temp = arr[i]\n      arr[i] = arr[j]\n      arr[j] = temp\n    }\n  }\n\n  const temp = arr[i + 1]\n  arr[i + 1] = arr[high]\n  arr[high] = temp\n\n  return i + 1\n}\n\nconst quickSort = (arr: number[], low: number, high: number): void =\u003e {\n  if (low \u003c high) {\n    const pi = partition(arr, low, high)\n    quickSort(arr, low, pi - 1)\n    quickSort(arr, pi + 1, high)\n  }\n}\n\nquickSort(arr, 0, arr.length - 1)\n```\n\nSpace complexities of O(nlogn) are extremely rare to the point that you don’t need to keep an eye out for it. Any algorithm that uses O(nlogn) space will likely be awkward enough that it will be apparent.\n\n\u003ch4 align=\"center\"\u003eO(nˣ): Polynomial Complexity\u003c/h4\u003e\n\nO(nˣ), or polynomial complexity, covers a larger range of complexities depending on the value of x. X represents the number of times all of n will be processed for every n. Polynomial complexity is where we enter the danger zone. It’s extremely inefficient, and while it is the only way to create some algorithms, polynomial complexity should be regarded as a “warning sign” that the code can be refactored. This holds true for not just polynomial complexity, but for all following complexities we will cover.\n\nA red flag that an algorithm will run in polynomial time is the presence of nested loops. A general rule of thumb is that x will equal the number of nested loops. A major exception to this rule is when you are working with matrices and multi-dimensional arrays. Nested loops are needed to traverse these data structures and do not represent polynomial time. Polynomial time will require the number of loops to equal 2y, where y is the number of dimensions present in the data structure.\n\n```ts\nconst haystack = Array.from({ length: size }, () =\u003e Math.random())\n\nfunction findDuplicates(array: string[]): string[] {\n  const result: string[] = []\n  for (let i = 0; i \u003c array.length; i++) {\n    for (let j = i + 1; j \u003c array.length; j++) {\n      if (array[i] === array[j] \u0026\u0026 !result.includes(array[i])) {\n        result.push(array[i])\n      }\n    }\n  }\n  return result\n}\nfindDuplicates([...haystack, ...haystack])\n```\n\nFor nested loops to create polynomial time complexity, all loops must be iterating over n.\n\nPolynomial space complexity will generally be because of the creation of a matrix or multidimensional array in a function creating a table.\n\n\u003ch4 align=\"center\"\u003eO(Xⁿ): Exponential Time\u003c/h4\u003e\n\nO(Xⁿ), known as exponential time, means that time or space will be raised to the power of n. Exponential time is extremely inefficient and should be avoided unless absolutely necessary. Often O(Xⁿ) results from having a recursive algorithm that calls X number of algorithms with n-1. Towers of Hanoi is a famous problem that has a recursive solution running in O(2ⁿ).\n\n```ts\nfunction fibonacci(n: number): number {\n  if (n \u003c= 1) {\n    return n\n  }\n  return fibonacci(n - 1) + fibonacci(n - 2)\n}\nfibonacci(size)\n```\n\nYou will probably never encounter algorithms using a space complexity of O(Xⁿ) in standard software engineering. It can be encountered when using Turing machines and the arithmetic model of computation, but this is extremely hard to implement using a JavaScript example, and beyond the scope of this project. Suffice to say, if O(Xⁿ) space complexity is ever encountered, it’s likely that something has gone horribly, horribly wrong.\n\n\u003ch4 align=\"center\"\u003eO(n!): Factorial Complexity\u003c/h4\u003e\n\nO(n!), or factorial complexity, is the “worst” standard complexity that exists. To illustrate how fast factorial solutions will blow up in size, a standard deck of cards has 52 cards, with 52! possible orderings of cards after shuffling. This number is larger than the number of atoms on Earth. If someone were to shuffle a deck of cards every second from the beginning of the universe until the present second, they would not have encountered every possible ordering of a deck of cards. It is highly unlikely that any current deck of shuffled cards you encounter has ever existed in that order in history. To sum up: Factorial complexity is unimaginably inefficient.\n\n```ts\nconst haystack = Array.from({ length: size }, () =\u003e Math.random())\n\nfunction permuteArray(array: number[]): number[][] {\n  if (array.length \u003c= 1) {\n    return [array]\n  }\n\n  const result: number[][] = []\n  for (let i = 0; i \u003c array.length; i++) {\n    const [first] = array.splice(i, 1)\n    const perms = permuteArray(array)\n    for (let j = 0; j \u003c perms.length; j++) {\n      result.push([first, ...perms[j]])\n    }\n    array.splice(i, 0, first)\n  }\n  return result\n}\npermuteArray(haystack)\n```\n\n\u003ch3 align=\"center\"\u003eAccuracy of measurements and correlation\u003c/h2\u003e\n\nThe accuracy of the algorithm for estimating the time complexity of an algorithm based on the runtime measurements obtained from a series of input sizes depends on several factors, including the quality of the data and the assumptions made by the algorithm.\n\nThe algorithm for estimating the time complexity uses a \u003ca href=\"https://en.wikipedia.org/wiki/Logistic_regression\" target=\"_blank\"\u003elogarithmic regression\u003c/a\u003e to fit a curve to the data points, where the input size is the independent variable and the runtime is the dependent variable. The slope of the curve is used to estimate the growth rate of the algorithm as the input size increases, and this growth rate is mapped to a time complexity based on a series of pre-defined ranges.\n\nThis algorithm works reasonably well for many types of algorithms, particularly those that exhibit a predictable, non-random pattern of growth as the input size increases. However, there are some types of algorithms that may not fit well with this approach, such as algorithms that exhibit a non-monotonic pattern of growth or have complex, non-linear behavior.\n\nIn general, the accuracy of the algorithm depends on the quality of the data obtained from the performance measurements, as well as the assumptions made about the nature of the growth pattern. To obtain accurate estimates of the time complexity, it is important to use a wide range of input sizes, choose input sizes that reflect the expected use case of the algorithm, and carefully consider the assumptions made about the nature of the growth pattern.\n\nDespite these limitations, the algorithm for estimating the time complexity of an algorithm based on the runtime measurements can be a useful tool for gaining insight into the performance characteristics of an algorithm and making informed decisions about algorithm design and optimization.\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjsheaven%2Fperf","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjsheaven%2Fperf","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjsheaven%2Fperf/lists"}