Explorar el Código

feat: add latency test report files & refactor latency test again

Sv443 hace 4 meses
padre
commit
a92d455ced
Se han modificado 3 ficheros con 273 adiciones y 145 borrados
  1. 2 0
      .gitignore
  2. 143 126
      src/dev/latency-test-queries.json
  3. 128 19
      src/dev/latency-test.ts

+ 2 - 0
.gitignore

@@ -14,3 +14,5 @@ node_modules/
 
 
 *.ignore.*
 *.ignore.*
 *.ignore
 *.ignore
+
+**/latency-test-reports/

+ 143 - 126
src/dev/latency-test-queries.json

@@ -1,38 +1,38 @@
 [
 [
-  "SOPHIE - BIPP",
-  "SOPHIE - ELLE",
-  "SOPHIE - LEMONADE",
-  "SOPHIE - HARD",
-  "SOPHIE - MSMSMSM",
-  "SOPHIE - VYZEE",
-  "SOPHIE - L.O.V.E.",
-  "SOPHIE - JUST LIKE WE NEVER SAID GOODBYE",
-  "SOPHIE - GET HIGHER",
-  "SOPHIE - Intro (The Full Horror)",
-  "SOPHIE - Rawwwwww",
-  "SOPHIE - Plunging Asymptote",
-  "SOPHIE - The Dome's Protection",
-  "SOPHIE - Reason Why",
-  "SOPHIE - Live In My Truth",
-  "SOPHIE - Why Lies",
-  "SOPHIE - Do You Wanna Be Alive",
-  "SOPHIE - Elegance",
-  "SOPHIE - Berlin Nightmare",
-  "SOPHIE - Gallop",
-  "SOPHIE - One More Time",
-  "SOPHIE - Exhilarate",
-  "SOPHIE - Always and Forever",
-  "SOPHIE - My Forever",
-  "SOPHIE - Love Me Off Earth",
-  "SOPHIE - It's Okay to Cry",
-  "SOPHIE - Ponyboy",
-  "SOPHIE - Faceshopping",
-  "SOPHIE - Is It Cold in the Water?",
-  "SOPHIE - Infatuation",
-  "SOPHIE - Not Okay",
-  "SOPHIE - Pretending",
-  "SOPHIE - Immaterial",
-  "SOPHIE - Whole New World/Pretend World",
+  "SOPHIE BIPP",
+  "SOPHIE ELLE",
+  "SOPHIE LEMONADE",
+  "SOPHIE HARD",
+  "SOPHIE MSMSMSM",
+  "SOPHIE VYZEE",
+  "SOPHIE L.O.V.E.",
+  "SOPHIE JUST LIKE WE NEVER SAID GOODBYE",
+  "SOPHIE GET HIGHER",
+  "SOPHIE Intro (The Full Horror)",
+  "SOPHIE Rawwwwww",
+  "SOPHIE Plunging Asymptote",
+  "SOPHIE The Dome's Protection",
+  "SOPHIE Reason Why",
+  "SOPHIE Live In My Truth",
+  "SOPHIE Why Lies",
+  "SOPHIE Do You Wanna Be Alive",
+  "SOPHIE Elegance",
+  "SOPHIE Berlin Nightmare",
+  "SOPHIE Gallop",
+  "SOPHIE One More Time",
+  "SOPHIE Exhilarate",
+  "SOPHIE Always and Forever",
+  "SOPHIE My Forever",
+  "SOPHIE Love Me Off Earth",
+  "SOPHIE It's Okay to Cry",
+  "SOPHIE Ponyboy",
+  "SOPHIE Faceshopping",
+  "SOPHIE Is It Cold in the Water?",
+  "SOPHIE Infatuation",
+  "SOPHIE Not Okay",
+  "SOPHIE Pretending",
+  "SOPHIE Immaterial",
+  "SOPHIE Whole New World/Pretend World",
   "flume highest building",
   "flume highest building",
   "flume say nothing",
   "flume say nothing",
   "flume dhlc",
   "flume dhlc",
@@ -63,26 +63,26 @@
   "flume daze 22.00",
   "flume daze 22.00",
   "flume amber",
   "flume amber",
   "flume & eprom spring",
   "flume & eprom spring",
-  "Flume SKY SKY 1.3",
-  "Flume Chalk 1.3.3",
-  "Flume All There 1.9",
-  "Flume Road To Japan",
-  "Flume Jerry 1.6",
-  "Flume n1cevib3 1.3",
-  "Flume Arrived Anxious, Left Bored 1.4",
-  "Flume Habibi",
-  "Flume Miss U",
-  "Flume No Other 1.2.2",
-  "Flume Counting Sheep (V2)",
-  "Flume Nice 2 Know U 1.5.3",
-  "Flume Why 1.3",
-  "Flume Rhinestone 1.7.2",
-  "Flume Dream 1.2.2",
-  "Flume beat 58 1.1",
-  "Flume Close 1.2",
-  "Flume One Step Closer 1.4",
-  "Flume SPOKE 2 ALIENS FINALLY 1.3",
-  "Flume Things Don't Always Go The Way You Plan 1.2",
+  "flume sky sky 1.3",
+  "flume chalk 1.3.3",
+  "flume all there 1.9",
+  "flume road to japan",
+  "flume jerry 1.6",
+  "flume n1cevib3 1.3",
+  "flume arrived anxious, left bored 1.4",
+  "flume habibi",
+  "flume miss u",
+  "flume no other 1.2.2",
+  "flume counting sheep (v2)",
+  "flume nice 2 know u 1.5.3",
+  "flume why 1.3",
+  "flume rhinestone 1.7.2",
+  "flume dream 1.2.2",
+  "flume beat 58 1.1",
+  "flume close 1.2",
+  "flume one step closer 1.4",
+  "flume spoke 2 aliens finally 1.3",
+  "flume things don't always go the way you plan 1.2",
   "Flume - Enough",
   "Flume - Enough",
   "Flume - Weekend",
   "Flume - Weekend",
   "Flume - Depth Charge",
   "Flume - Depth Charge",
@@ -183,51 +183,68 @@
   "Caravan Palace Portobello",
   "Caravan Palace Portobello",
   "Caravan Palace City Cook",
   "Caravan Palace City Cook",
   "Caravan Palace Villa Rose",
   "Caravan Palace Villa Rose",
-  "Stromae - Introduction",
-  "Stromae - Ta fête",
-  "Stromae - Bâtard",
-  "Stromae - Peace or Violence",
-  "Stromae - Te Quiero",
-  "Stromae - Tous les mêmes",
-  "Stromae - Ave cesaria",
-  "Stromae - Sommeil",
-  "Stromae - Quand c'est ?",
-  "Stromae - Je cours",
-  "Stromae - Moules frites",
-  "Stromae - Formidable",
-  "Stromae - Silence",
-  "Stromae - Carmen",
-  "Stromae - Humain à l'eau",
-  "Stromae - Alors on danse",
-  "Stromae - Papaoutai",
-  "Stromae - Merci",
-  "Stromae - Invaincu",
-  "Stromae - Santé",
-  "Stromae - La solassitude",
-  "Stromae - Fils de joie",
-  "Stromae - L'enfer",
-  "Stromae - C'est que du bonheur",
-  "Stromae - Pas vraiment",
-  "Stromae - Riez",
-  "Stromae - Mon amour",
-  "Stromae - Déclaration",
-  "Stromae - Mauvaise journée",
-  "Stromae - Bonne journée",
-  "Stromae - Mon amour",
-  "Tyler The Creator - St. Chroma (ft. Daniel Caesar)",
-  "Tyler The Creator - Rah Tah Tah",
-  "Tyler The Creator - Noid",
-  "Tyler The Creator - Darling, I (ft. Teezo Touchdown)",
-  "Tyler The Creator - Hey Jane",
-  "Tyler The Creator - I Killed You",
-  "Tyler The Creator - Judge Judy",
-  "Tyler The Creator - Sticky (ft. GloRilla, Lil Wayne & Sexyy Red)",
-  "Tyler The Creator - Take Your Mask Off (ft. Daniel Caesar & LaToiya Williams)",
-  "Tyler The Creator - Tomorrow",
-  "Tyler The Creator - Thought I Was Dead (ft. Santigold & ScHoolboy Q)",
-  "Tyler The Creator - Like Him (ft. Lola Young)",
-  "Tyler The Creator - Balloon (ft. Doechii)",
-  "Tyler The Creator - I Hope You Find Your Way Home",
+  "stromae introduction",
+  "stromae ta fête",
+  "stromae bâtard",
+  "stromae peace or violence",
+  "stromae te quiero",
+  "stromae tous les mêmes",
+  "stromae ave cesaria",
+  "stromae sommeil",
+  "stromae quand c'est?",
+  "stromae je cours",
+  "stromae moules frites",
+  "stromae formidable",
+  "stromae silence",
+  "stromae carmen",
+  "stromae humain à l'eau",
+  "stromae alors on danse",
+  "stromae papaoutai",
+  "stromae merci",
+  "stromae invaincu",
+  "stromae santé",
+  "stromae la solassitude",
+  "stromae fils de joie",
+  "stromae l'enfer",
+  "stromae c'est que du bonheur",
+  "stromae pas vraiment",
+  "stromae riez",
+  "stromae mon amour",
+  "stromae déclaration",
+  "stromae mauvaise journée",
+  "stromae bonne journée",
+  "stromae mon amour",
+  "frank ocean nikes",
+  "frank ocean ivy",
+  "frank ocean pink + white",
+  "frank ocean be yourself",
+  "frank ocean solo",
+  "frank ocean skyline to",
+  "frank ocean self control",
+  "frank ocean good guy",
+  "frank ocean nights",
+  "frank ocean solo",
+  "frank ocean pretty sweet",
+  "frank ocean facebook story",
+  "frank ocean close to you",
+  "frank ocean white ferrari",
+  "frank ocean seigfried",
+  "frank ocean godspeed",
+  "frank ocean futura free",
+  "tyler the creator st chroma",
+  "tyler the creator rah tah tah",
+  "tyler the creator noid",
+  "tyler the creator darling i",
+  "tyler the creator hey jane",
+  "tyler the creator i killed you",
+  "tyler the creator judge judy",
+  "tyler the creator sticky",
+  "tyler the creator take your mask off",
+  "tyler the creator tomorrow",
+  "tyler the creator thought i was dead",
+  "tyler the creator like him",
+  "tyler the creator balloon",
+  "tyler the creator i hope you find your way home",
   "joji attention",
   "joji attention",
   "joji slow dancing in the dark",
   "joji slow dancing in the dark",
   "joji test drive",
   "joji test drive",
@@ -240,33 +257,33 @@
   "joji rip",
   "joji rip",
   "joji xnxx",
   "joji xnxx",
   "joji ill see you in 40",
   "joji ill see you in 40",
-  "Joji - Ew",
-  "Joji - MODUS",
-  "Joji - Tick Tock",
-  "Joji - Daylight",
-  "Joji - Upgrade",
-  "Joji - Gimme Love",
-  "Joji - Run",
-  "Joji - Sanctuary",
-  "Joji - High Hopes",
-  "Joji - NITROUS",
-  "Joji - Pretty Boy",
-  "Joji - Normal People",
-  "Joji - Afterthought",
-  "Joji - Mr. Hollywood",
-  "Joji - 777",
-  "Joji - Reanimator",
-  "Joji - Like You Do",
-  "Joji - Your Man",
-  "Joji - Glimpse of Us",
-  "Joji - Feeling Like The End",
-  "Joji - Die For You",
-  "Joji - Before The Day Is Over",
-  "Joji - Dissolve",
-  "Joji - NIGHT RIDER",
-  "Joji - BLAHBLAHBLAH DEMO",
-  "Joji - YUKON",
-  "Joji - 1AM FREESTYLE",
+  "joji ew",
+  "joji modus",
+  "joji tick tock",
+  "joji daylight",
+  "joji upgrade",
+  "joji gimme love",
+  "joji run",
+  "joji sanctuary",
+  "joji high hopes",
+  "joji nitrous",
+  "joji pretty boy",
+  "joji normal people",
+  "joji afterthought",
+  "joji mr hollywood",
+  "joji 777",
+  "joji reanimator",
+  "joji like you do",
+  "joji your man",
+  "joji glimpse of us",
+  "joji feeling like the end",
+  "joji die for you",
+  "joji before the day is over",
+  "joji dissolve",
+  "joji night rider",
+  "joji blahblahblah demo",
+  "joji yukon",
+  "joji 1am freestyle",
   "pink guy hot nickel ball on a pussy",
   "pink guy hot nickel ball on a pussy",
   "pink guy are you serious",
   "pink guy are you serious",
   "pink guy white is right",
   "pink guy white is right",

+ 128 - 19
src/dev/latency-test.ts

@@ -1,39 +1,64 @@
-// NOTE:
+// REQUIREMENTS:
 // - requires the env vars HTTP_PORT and AUTH_TOKENS (at least 1 token to bypass rate limiting) to be set
 // - requires the env vars HTTP_PORT and AUTH_TOKENS (at least 1 token to bypass rate limiting) to be set
 // - requires geniURL to run in a different process (or using the command pnpm run latency-test)
 // - requires geniURL to run in a different process (or using the command pnpm run latency-test)
-//
+// 
+// NOTES:
 // - requests are sent sequentially on purpose to avoid rate limiting on genius.com's side
 // - requests are sent sequentially on purpose to avoid rate limiting on genius.com's side
+// - change settings in the `settings` object
+// - view previous latency test reports in the `reports` directory (
 
 
-import "dotenv/config";
+import { mkdir, stat, writeFile } from "node:fs/promises";
+import { dirname, join } from "node:path";
 import _axios from "axios";
 import _axios from "axios";
 import percentile from "percentile";
 import percentile from "percentile";
 import k from "kleur";
 import k from "kleur";
-import type { Stringifiable } from "svcorelib";
+import { type Stringifiable } from "svcorelib";
+import "dotenv/config";
 import queries from "./latency-test-queries.json" with { type: "json" };
 import queries from "./latency-test-queries.json" with { type: "json" };
+import { fileURLToPath } from "node:url";
 
 
 const settings = {
 const settings = {
   /** Amount of requests to send in total. */
   /** Amount of requests to send in total. */
-  amount: 10,
+  amount: 250,
   /** Base URL to send requests to. `{{QUERY}}` will be replaced with a random query from the `latency-test-queries.json` file. */
   /** Base URL to send requests to. `{{QUERY}}` will be replaced with a random query from the `latency-test-queries.json` file. */
   url: `http://127.0.0.1:${process.env.HTTP_PORT ?? 8074}/v2/search/top?q={{QUERY}}`,
   url: `http://127.0.0.1:${process.env.HTTP_PORT ?? 8074}/v2/search/top?q={{QUERY}}`,
-  /** Whether to log all requests to the console. */
-  logRequests: true,
-};
+  /** Whether to log all requests to the console (true) or just in increments of `infoLogFrequency` (false). */
+  logAllRequests: true,
+  /** Amount of requests to send before logging an info message. */
+  infoLogFrequency: 10,
+  /** Maximum timeout for each request in milliseconds. */
+  maxTimeout: 20_000,
+} as const;
 
 
+const reportsDirPath = join(dirname(fileURLToPath(import.meta.url)), "latency-test-reports");
 
 
-const axios = _axios.create({ timeout: 20_000 });
+const axios = _axios.create({ timeout: settings.maxTimeout ?? 20_000 });
+
+type LatencyTestReport = {
+  /** Local date and time string when the latency test finished. */
+  localDateTime: string;
+  /** Settings used for the latency test. */
+  settings: typeof settings;
+  /** Total time the latency test took in seconds. */
+  totalTime: number;
+  /** Calculated times in milliseconds. */
+  times: Record<
+    "min" | "avg" | "max" | "5th%" | "10th%" | "25th%" | "80th%" | "90th%" | "95th%" | "97th%" | "98th%" | "99th%",
+    number
+  >;
+};
 
 
 async function run() {
 async function run() {
-  console.log(`\n\n>>> Running latency test with ${settings.amount} requests...\n`);
-  const startTs = Date.now();
+  console.log(k.green(`\n>>> Starting latency test on ${settings.amount} sequential requests${settings.amount >= 50 ? k.yellow(" - this could take a while!") : ""}\n`));
+  const testStartTs = Date.now();
 
 
   const times = [] as number[];
   const times = [] as number[];
   for(let i = 0; i < settings.amount; i++) {
   for(let i = 0; i < settings.amount; i++) {
-    i === 0 && console.log(`> Sent 0 of ${settings.amount} requests`);
-    const start = Date.now();
+    !settings.logAllRequests && i === 0 && console.log(`> Sent 0 of ${settings.amount} requests`);
+    const reqStartTs = Date.now();
     try {
     try {
-      const url = settings.url.replace("{{QUERY}}", queries[Math.floor(Math.random() * queries.length)]);
-      settings.logRequests && console.log("    *", url);
+      const url = encodeURI(settings.url.replace("{{QUERY}}", queries[Math.floor(Math.random() * queries.length)]));
+      settings.logAllRequests && console.log(`  ${String(i + 1).padStart(digitCount(settings.amount))}.`, url);
       await axios.get(url, {
       await axios.get(url, {
         headers: {
         headers: {
           "Cache-Control": "no-cache",
           "Cache-Control": "no-cache",
@@ -42,12 +67,18 @@ async function run() {
       });
       });
     }
     }
     catch(e) {
     catch(e) {
-      console.error("Failed to send request:", e);
+      console.error(k.red("\n>> Failed to send request:"), e);
+      console.error();
     }
     }
     finally {
     finally {
-      times.push(Date.now() - start);
+      times.push(Date.now() - reqStartTs);
+
+      const elapsedStr = `${((Date.now() - testStartTs) / 1000).toFixed(1)}s elapsed`;
 
 
-      i % 10 === 0 && i !== 0 && console.log(`> Sent ${i} of ${settings.amount} requests`);
+      if(settings.logAllRequests && i % settings.infoLogFrequency === settings.infoLogFrequency - 1 && i > 0 && i !== settings.amount - 1)
+        console.log(`${" ".repeat(digitCount(settings.amount))}  > ${elapsedStr}, sent ${i + 1} of ${settings.amount} requests (${mapRange(i + 1, 0, settings.amount, 0, 100).toFixed(0)}%)`);
+      else if(i % settings.infoLogFrequency === settings.infoLogFrequency - 1 && i > 0 && i !== settings.amount - 1)
+        console.log(`> Sent ${i + 1} of ${settings.amount} requests (${elapsedStr})`);
     }
     }
   }
   }
 
 
@@ -61,12 +92,18 @@ async function run() {
     return res;
     return res;
   };
   };
 
 
+  const reportTimes = {} as Partial<LatencyTestReport["times"]>;
+
   const logVal = (label: string, value: Stringifiable, kleurFunc?: (str: string) => void) => {
   const logVal = (label: string, value: Stringifiable, kleurFunc?: (str: string) => void) => {
     const valStr = `${label}:\t${String(value).padStart(4, " ")} ms`;
     const valStr = `${label}:\t${String(value).padStart(4, " ")} ms`;
+    reportTimes[label as keyof LatencyTestReport["times"]] = Number(value);
     console.log(kleurFunc ? kleurFunc(valStr) : valStr);
     console.log(kleurFunc ? kleurFunc(valStr) : valStr);
   }
   }
 
 
-  console.log(`\n>>> Latency test finished sending all ${settings.amount} requests after ${((Date.now() - startTs) / 1000).toFixed(2)}s - Results:`);
+  const testFinishTs = Date.now();
+  const totalTime = Number(((testFinishTs - testStartTs) / 1000).toFixed(2));
+
+  console.log(`\n>>> Latency test finished sending all ${settings.amount} requests after ${totalTime}s - Results:`);
   console.log();
   console.log();
   logVal("5th%", getPerc(5, times), k.gray);
   logVal("5th%", getPerc(5, times), k.gray);
   logVal("10th%", getPerc(10, times), k.gray);
   logVal("10th%", getPerc(10, times), k.gray);
@@ -82,6 +119,78 @@ async function run() {
   logVal("avg", avg, k.bold);
   logVal("avg", avg, k.bold);
   logVal("max", max);
   logVal("max", max);
   console.log();
   console.log();
+
+  const localDateTime = Intl.DateTimeFormat(Intl.DateTimeFormat().resolvedOptions().locale, {
+    dateStyle: "short",
+    timeStyle: "long",
+  }).format(new Date(testFinishTs));
+
+  const reportData: LatencyTestReport = {
+    localDateTime,
+    settings,
+    totalTime,
+    times: reportTimes as LatencyTestReport["times"],
+  };
+
+  const reportPath = join(reportsDirPath, `report_${
+    new Date(testFinishTs).toLocaleString("en-US", {
+      year: "numeric",
+      month: "2-digit",
+      day: "2-digit",
+      hour: "2-digit",
+      hourCycle: "h24",
+      minute: "2-digit",
+      second: "2-digit",
+    }).replace(/[/:]/g, "-").replace(/, /g, "_")
+  }.json`);
+
+  try {
+    try {
+      await stat(reportsDirPath);
+    }
+    catch {
+      await mkdir(reportsDirPath);
+    }
+
+    await writeFile(reportPath, JSON.stringify(reportData, null, 2));
+    console.log(k.gray(`Wrote report to file at '${reportPath}'\n`));
+  }
+  catch(e) {
+    console.error(k.red(`Failed to write latency test report to file at '${reportPath}':`), e);
+  }
+
+  return setImmediate(() => process.exit(0));
+}
+
+/** Returns the amount of digits in a number. */
+function digitCount(num: number): number {
+  if(num === 0) return 1;
+  return Math.floor(Math.log10(Math.abs(num)) + 1);
+}
+
+/**
+ * Transforms the value parameter from the numerical range `range1min` to `range1max` to the numerical range `range2min` to `range2max`  
+ * For example, you can map the value 2 in the range of 0-5 to the range of 0-10 and you'd get a 4 as a result.
+ */
+function mapRange(value: number, range1min: number, range1max: number, range2min: number, range2max: number): number;
+/**
+ * Transforms the value parameter from the numerical range `0` to `range1max` to the numerical range `0` to `range2max`
+ * For example, you can map the value 2 in the range of 0-5 to the range of 0-10 and you'd get a 4 as a result.
+ */
+function mapRange(value: number, range1max: number, range2max: number): number;
+function mapRange(value: number, range1min: number, range1max: number, range2min?: number, range2max?: number): number {
+  // overload
+  if(typeof range2min === "undefined" || range2max === undefined) {
+    range2max = range1max;
+    range2min = 0;
+    range1max = range1min;
+    range1min = 0;
+  }
+
+  if(Number(range1min) === 0.0 && Number(range2min) === 0.0)
+    return value * (range2max / range1max);
+
+  return (value - range1min) * ((range2max - range2min) / (range1max - range1min)) + range2min;
 }
 }
 
 
 run();
 run();