utils.ts 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  1. import * as fs from "fs/promises";
  2. import path from "path";
  3. import { spawnSync } from "child_process";
  4. /** Persistent benchmark data(mapping of `Version -> Data`) */
  5. type Bench = {
  6. [key: string]: {
  7. /** Benchmark result for compute units consumed */
  8. computeUnits: ComputeUnits;
  9. };
  10. };
  11. /** `instruction name -> compute units consumed` */
  12. export type ComputeUnits = { [key: string]: number };
  13. /**
  14. * How much of a percentage difference between the current and the previous data
  15. * should be significant. Any difference above this number should be noted in
  16. * the benchmark file.
  17. */
  18. export const THRESHOLD_PERCENTAGE = 1;
  19. /** Path to the benchmark Markdown files */
  20. export const BENCH_DIR_PATH = "../../bench";
  21. /** Utility class to handle benchmark data related operations */
  22. export class BenchData {
  23. /** Benchmark data filepath */
  24. static #PATH = "bench.json";
  25. /** Benchmark data */
  26. #data: Bench;
  27. constructor(data: Bench) {
  28. this.#data = data;
  29. }
  30. /** Open the benchmark data file */
  31. static async open() {
  32. let bench: Bench;
  33. try {
  34. const benchFile = await fs.readFile(BenchData.#PATH, {
  35. encoding: "utf8",
  36. });
  37. bench = JSON.parse(benchFile);
  38. } catch {
  39. bench = {};
  40. }
  41. return new BenchData(bench);
  42. }
  43. /** Save the benchmark data file */
  44. async save() {
  45. await fs.writeFile(BenchData.#PATH, JSON.stringify(this.#data, null, 2));
  46. }
  47. /** Get the stored results based on version */
  48. get(version: string) {
  49. return this.#data[version];
  50. }
  51. /** Get unreleased version results */
  52. getUnreleased() {
  53. return this.get("unreleased");
  54. }
  55. /** Get all versions */
  56. getVersions() {
  57. return Object.keys(this.#data);
  58. }
  59. /** Compare and update compute units changes */
  60. compareComputeUnits(
  61. newComputeUnitsResult: ComputeUnits,
  62. oldComputeUnitsResult: ComputeUnits,
  63. changeCb: (
  64. ixName: string,
  65. newComputeUnits: number,
  66. oldComputeUnits: number
  67. ) => void,
  68. noChangeCb?: (ixName: string, computeUnits: number) => void
  69. ) {
  70. let needsUpdate = false;
  71. // Compare compute units changes
  72. for (const ixName in newComputeUnitsResult) {
  73. const oldComputeUnits = oldComputeUnitsResult[ixName];
  74. const newComputeUnits = newComputeUnitsResult[ixName];
  75. if (!oldComputeUnits) {
  76. console.log(`New instruction '${ixName}'`);
  77. needsUpdate = true;
  78. changeCb(ixName, newComputeUnits, NaN);
  79. continue;
  80. }
  81. const percentage = THRESHOLD_PERCENTAGE / 100;
  82. const oldMaximumAllowedDelta = oldComputeUnits * percentage;
  83. const newMaximumAllowedDelta = newComputeUnits * percentage;
  84. const delta = newComputeUnits - oldComputeUnits;
  85. const absDelta = Math.abs(delta);
  86. if (
  87. absDelta > oldMaximumAllowedDelta ||
  88. absDelta > newMaximumAllowedDelta
  89. ) {
  90. // Throw in CI
  91. if (process.env.CI) {
  92. throw new Error(
  93. [
  94. `Compute units for instruction '${ixName}' has changed more than ${THRESHOLD_PERCENTAGE}% but is not saved.`,
  95. "Run `anchor test --skip-lint` in tests/bench and commit the changes.",
  96. ].join(" ")
  97. );
  98. }
  99. console.log(
  100. `Compute units change '${ixName}' (${oldComputeUnits} -> ${newComputeUnits})`
  101. );
  102. needsUpdate = true;
  103. changeCb(ixName, newComputeUnits, oldComputeUnits);
  104. } else {
  105. noChangeCb?.(ixName, newComputeUnits);
  106. }
  107. }
  108. return { needsUpdate };
  109. }
  110. /** Bump benchmark data version to the given version */
  111. bumpVersion(newVersion: string) {
  112. const versions = Object.keys(this.#data);
  113. const unreleasedVersion = versions[versions.length - 1];
  114. if (this.#data[newVersion]) {
  115. console.error(`Version '${newVersion}' already exists!`);
  116. process.exit(1);
  117. }
  118. // Add the new version
  119. this.#data[newVersion] = this.get(unreleasedVersion);
  120. // Delete the unreleased version
  121. delete this.#data[unreleasedVersion];
  122. // Add new unreleased version
  123. this.#data[unreleasedVersion] = this.#data[newVersion];
  124. }
  125. /**
  126. * Loop through all of the markdown files and run the given callback before
  127. * saving the file.
  128. */
  129. static async forEachMarkdown(
  130. cb: (markdown: Markdown, fileName: string) => void
  131. ) {
  132. const fileNames = await fs.readdir(BENCH_DIR_PATH);
  133. const markdownFileNames = fileNames.filter((n) => n.endsWith(".md"));
  134. for (const fileName of markdownFileNames) {
  135. const markdown = await Markdown.open(path.join(BENCH_DIR_PATH, fileName));
  136. cb(markdown, fileName);
  137. await markdown.save();
  138. }
  139. // Format
  140. spawnSync("yarn", [
  141. "run",
  142. "prettier",
  143. "--write",
  144. path.join(BENCH_DIR_PATH, "*.md"),
  145. ]);
  146. }
  147. }
  148. /** Utility class to handle markdown related operations */
  149. export class Markdown {
  150. /** Unreleased version string */
  151. static #UNRELEASED_VERSION = "[Unreleased]";
  152. /** Markdown filepath */
  153. #path: string;
  154. /** Markdown text */
  155. #text: string;
  156. constructor(path: string, text: string) {
  157. this.#path = path;
  158. this.#text = text;
  159. }
  160. /** Open the markdown file */
  161. static async open(path: string) {
  162. const text = await fs.readFile(path, { encoding: "utf8" });
  163. return new Markdown(path, text);
  164. }
  165. /** Create a markdown table */
  166. static createTable(...args: string[]) {
  167. return new MarkdownTable([args]);
  168. }
  169. /** Save the markdown file */
  170. async save() {
  171. await fs.writeFile(this.#path, this.#text);
  172. }
  173. /** Change version table with the given table */
  174. updateTable(version: string, table: MarkdownTable) {
  175. const md = this.#text;
  176. let titleStartIndex = md.indexOf(`[${version}]`);
  177. if (titleStartIndex === -1) {
  178. titleStartIndex = md.indexOf(Markdown.#UNRELEASED_VERSION);
  179. }
  180. const startIndex = titleStartIndex + md.slice(titleStartIndex).indexOf("|");
  181. const endIndex = startIndex + md.slice(startIndex).indexOf("\n\n");
  182. this.#text =
  183. md.slice(0, startIndex) + table.toString() + md.slice(endIndex + 1);
  184. }
  185. /** Bump the version to the given version */
  186. bumpVersion(newVersion: string) {
  187. newVersion = `[${newVersion}]`;
  188. if (this.#text.includes(newVersion)) {
  189. console.error(`Version '${newVersion}' already exists!`);
  190. process.exit(1);
  191. }
  192. const startIndex = this.#text.indexOf(`## ${Markdown.#UNRELEASED_VERSION}`);
  193. const endIndex =
  194. startIndex + this.#text.slice(startIndex).indexOf("\n---") + 4;
  195. let unreleasedSection = this.#text.slice(startIndex, endIndex);
  196. // Update unreleased version to `newVersion`
  197. const newSection = unreleasedSection.replace(
  198. Markdown.#UNRELEASED_VERSION,
  199. newVersion
  200. );
  201. // Reset unreleased version changes
  202. unreleasedSection = unreleasedSection
  203. .split("\n")
  204. .map((line, i) => {
  205. // First 4 lines don't change
  206. if ([0, 1, 2, 3].includes(i)) return line;
  207. const regex = /\|.*\|.*\|(.*)\|/;
  208. const result = regex.exec(line);
  209. const changeStr = result?.[1];
  210. if (!changeStr) {
  211. if (line.startsWith("#")) return line;
  212. else if (line.startsWith("---")) return line + "\n";
  213. else return "";
  214. }
  215. return line.replace(changeStr, "-");
  216. })
  217. .join("\n");
  218. // Update the text
  219. this.#text =
  220. this.#text.slice(0, startIndex) +
  221. unreleasedSection +
  222. newSection +
  223. this.#text.slice(endIndex);
  224. }
  225. }
  226. /** Utility class to handle markdown table related operations */
  227. class MarkdownTable {
  228. /** Markdown rows stored as array of arrays */
  229. #rows: string[][];
  230. constructor(rows: string[][]) {
  231. this.#rows = rows;
  232. this.insert("-", "-", "-");
  233. }
  234. /** Insert a new row to the markdown table */
  235. insert(...args: string[]) {
  236. this.#rows.push(args);
  237. }
  238. /** Convert the stored rows to a markdown table */
  239. toString() {
  240. return this.#rows.reduce(
  241. (acc, row) =>
  242. acc + row.reduce((acc, cur) => `${acc} ${cur} |`, "|") + "\n",
  243. ""
  244. );
  245. }
  246. }