make-mp-data 1.4.4 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -15,8 +15,8 @@ const { domainSuffix, domainPrefix } = require('./defaults');
15
15
  /** @typedef {import('../types').Config} Config */
16
16
  /** @typedef {import('../types').EventConfig} EventConfig */
17
17
  /** @typedef {import('../types').ValueValid} ValueValid */
18
- /** @typedef {import('../types').EnrichedArray} EnrichArray */
19
- /** @typedef {import('../types').EnrichArrayOptions} EnrichArrayOptions */
18
+ /** @typedef {import('../types').HookedArray} hookArray */
19
+ /** @typedef {import('../types').hookArrayOptions} hookArrayOptions */
20
20
  /** @typedef {import('../types').Person} Person */
21
21
  /** @typedef {import('../types').Funnel} Funnel */
22
22
 
@@ -34,14 +34,15 @@ RNG
34
34
  /**
35
35
  * the random number generator initialization function
36
36
  * @param {string} seed
37
+ * @returns {Chance}
37
38
  */
38
39
  function initChance(seed) {
39
40
  if (process.env.SEED) seed = process.env.SEED; // Override seed with environment variable if available
40
41
  if (!chanceInitialized) {
41
42
  globalChance = new Chance(seed);
42
- if (global.MP_SIMULATION_CONFIG) global.MP_SIMULATION_CONFIG.chance = globalChance;
43
43
  chanceInitialized = true;
44
44
  }
45
+ return globalChance;
45
46
  }
46
47
 
47
48
  /**
@@ -50,12 +51,11 @@ function initChance(seed) {
50
51
  */
51
52
  function getChance() {
52
53
  if (!chanceInitialized) {
53
- const seed = process.env.SEED || global.MP_SIMULATION_CONFIG?.seed;
54
+ const seed = process.env.SEED || "";
54
55
  if (!seed) {
55
- return new Chance();
56
+ return new Chance(); // this is a new RNG and therefore not deterministic
56
57
  }
57
- initChance(seed);
58
- return globalChance;
58
+ return initChance(seed);
59
59
  }
60
60
  return globalChance;
61
61
  }
@@ -242,64 +242,6 @@ function integer(min = 1, max = 100) {
242
242
  };
243
243
 
244
244
 
245
- /**
246
- * Creates a function that generates a weighted list of items
247
- * with a higher likelihood of picking a specified index and clear second and third place indices.
248
- *
249
- * @param {Array} items - The list of items to pick from.
250
- * @param {number} [mostChosenIndex] - The index of the item to be most favored.
251
- * @returns {function} - A function that returns a weighted list of items.
252
- */
253
- function pickAWinner(items, mostChosenIndex) {
254
- const chance = getChance();
255
-
256
- // Ensure mostChosenIndex is within the bounds of the items array
257
- if (!items) return () => { return ""; };
258
- if (!items.length) return () => { return ""; };
259
- if (!mostChosenIndex) mostChosenIndex = chance.integer({ min: 0, max: items.length - 1 });
260
- if (mostChosenIndex >= items.length) mostChosenIndex = items.length - 1;
261
-
262
- // Calculate second and third most chosen indices
263
- const secondMostChosenIndex = (mostChosenIndex + 1) % items.length;
264
- const thirdMostChosenIndex = (mostChosenIndex + 2) % items.length;
265
-
266
- // Return a function that generates a weighted list
267
- return function () {
268
- const weighted = [];
269
- for (let i = 0; i < 10; i++) {
270
- const rand = chance.d10(); // Random number between 1 and 10
271
-
272
- // 35% chance to favor the most chosen index
273
- if (chance.bool({ likelihood: 35 })) {
274
- // 50% chance to slightly alter the index
275
- if (chance.bool({ likelihood: 50 })) {
276
- weighted.push(items[mostChosenIndex]);
277
- } else {
278
- const addOrSubtract = chance.bool({ likelihood: 50 }) ? -rand : rand;
279
- let newIndex = mostChosenIndex + addOrSubtract;
280
-
281
- // Ensure newIndex is within bounds
282
- if (newIndex < 0) newIndex = 0;
283
- if (newIndex >= items.length) newIndex = items.length - 1;
284
- weighted.push(items[newIndex]);
285
- }
286
- }
287
- // 25% chance to favor the second most chosen index
288
- else if (chance.bool({ likelihood: 25 })) {
289
- weighted.push(items[secondMostChosenIndex]);
290
- }
291
- // 15% chance to favor the third most chosen index
292
- else if (chance.bool({ likelihood: 15 })) {
293
- weighted.push(items[thirdMostChosenIndex]);
294
- }
295
- // Otherwise, pick a random item from the list
296
- else {
297
- weighted.push(chance.pickone(items));
298
- }
299
- }
300
- return weighted;
301
- };
302
- }
303
245
 
304
246
 
305
247
 
@@ -359,61 +301,16 @@ function mapToRange(value, mean, sd) {
359
301
  * @param {number} step=1
360
302
  */
361
303
  function range(a, b, step = 1) {
304
+ const arr = [];
362
305
  step = !step ? 1 : step;
363
306
  b = b / step;
364
307
  for (var i = a; i <= b; i++) {
365
- this.push(i * step);
308
+ arr.push(i * step);
366
309
  }
367
- return this;
310
+ return arr;
368
311
  };
369
312
 
370
313
 
371
- /**
372
- * create funnels out of random events
373
- * @param {EventConfig[]} events
374
- */
375
- function inferFunnels(events) {
376
- const createdFunnels = [];
377
- const firstEvents = events.filter((e) => e.isFirstEvent).map((e) => e.event);
378
- const usageEvents = events.filter((e) => !e.isFirstEvent).map((e) => e.event);
379
- const numFunnelsToCreate = Math.ceil(usageEvents.length);
380
- /** @type {Funnel} */
381
- const funnelTemplate = {
382
- sequence: [],
383
- conversionRate: 50,
384
- order: 'sequential',
385
- requireRepeats: false,
386
- props: {},
387
- timeToConvert: 1,
388
- isFirstFunnel: false,
389
- weight: 1
390
- };
391
- if (firstEvents.length) {
392
- for (const event of firstEvents) {
393
- createdFunnels.push({ ...clone(funnelTemplate), sequence: [event], isFirstFunnel: true, conversionRate: 100 });
394
- }
395
- }
396
-
397
- //at least one funnel with all usage events
398
- createdFunnels.push({ ...clone(funnelTemplate), sequence: usageEvents });
399
-
400
- //for the rest, make random funnels
401
- followUpFunnels: for (let i = 1; i < numFunnelsToCreate; i++) {
402
- /** @type {Funnel} */
403
- const funnel = { ...clone(funnelTemplate) };
404
- funnel.conversionRate = integer(25, 75);
405
- funnel.timeToConvert = integer(1, 10);
406
- funnel.weight = integer(1, 10);
407
- const sequence = shuffleArray(usageEvents).slice(0, integer(2, usageEvents.length));
408
- funnel.sequence = sequence;
409
- funnel.order = 'random';
410
- createdFunnels.push(funnel);
411
- }
412
-
413
- return createdFunnels;
414
-
415
- }
416
-
417
314
 
418
315
  /*
419
316
  ----
@@ -491,7 +388,7 @@ function weighFunnels(acc, funnel) {
491
388
  * @param {number} skew=1
492
389
  * @param {number} size=100
493
390
  */
494
- function weightedRange(min, max, skew = 1, size = 50) {
391
+ function weighNumRange(min, max, skew = 1, size = 50) {
495
392
  if (size > 2000) size = 2000;
496
393
  const mean = (max + min) / 2;
497
394
  const sd = (max - min) / 4;
@@ -508,13 +405,16 @@ function weightedRange(min, max, skew = 1, size = 50) {
508
405
  return array;
509
406
  }
510
407
 
408
+ /**
409
+ * arbitrarily weigh an array of values to create repeats
410
+ * @param {Array<any>} arr
411
+ */
511
412
  function weighArray(arr) {
512
-
513
413
  // Calculate the upper bound based on the size of the array with added noise
514
414
  const maxCopies = arr.length + integer(1, arr.length);
515
415
 
516
416
  // Create an empty array to store the weighted elements
517
- let weightedArray = [];
417
+ const weightedArray = [];
518
418
 
519
419
  // Iterate over the input array and copy each element a random number of times
520
420
  arr.forEach(element => {
@@ -527,6 +427,106 @@ function weighArray(arr) {
527
427
  return weightedArray;
528
428
  }
529
429
 
430
+ /**
431
+ * Creates a function that generates a weighted array of values.
432
+ *
433
+ * @overload
434
+ * @param {Array<{value: string, weight: number}>} items - An array of weighted objects or an array of strings.
435
+ * @returns {function(): Array<string>} A function that returns a weighted array of values when called.
436
+ *
437
+ * @overload
438
+ * @param {Array<string>} items - An array of strings.
439
+ * @returns {function(): Array<string>} A function that returns a weighted array with automatically assigned random weights to each string.
440
+ */
441
+
442
+ function weighChoices(items) {
443
+ let weightedItems;
444
+
445
+ // If items are strings, assign unique random weights
446
+ if (items.every(item => typeof item === 'string')) {
447
+ const weights = shuffleArray(range(1, items.length));
448
+ weightedItems = items.map((item, index) => ({
449
+ value: item,
450
+ weight: weights[index]
451
+ }));
452
+ } else {
453
+ weightedItems = items;
454
+ }
455
+
456
+ return function generateWeightedArray() {
457
+ const weightedArray = [];
458
+
459
+ // Add each value to the array the number of times specified by its weight
460
+ weightedItems.forEach(({ value, weight }) => {
461
+ if (!weight) weight = 1;
462
+ for (let i = 0; i < weight; i++) {
463
+ weightedArray.push(value);
464
+ }
465
+ });
466
+
467
+ return weightedArray;
468
+ };
469
+ }
470
+
471
+ /**
472
+ * Creates a function that generates a weighted list of items
473
+ * with a higher likelihood of picking a specified index and clear second and third place indices.
474
+ *
475
+ * @param {Array} items - The list of items to pick from.
476
+ * @param {number} [mostChosenIndex] - The index of the item to be most favored.
477
+ * @returns {function} - A function that returns a weighted list of items.
478
+ */
479
+ function pickAWinner(items, mostChosenIndex) {
480
+ const chance = getChance();
481
+
482
+ // Ensure mostChosenIndex is within the bounds of the items array
483
+ if (!items) return () => { return ""; };
484
+ if (!items.length) return () => { return ""; };
485
+ if (!mostChosenIndex) mostChosenIndex = chance.integer({ min: 0, max: items.length - 1 });
486
+ if (mostChosenIndex >= items.length) mostChosenIndex = items.length - 1;
487
+
488
+ // Calculate second and third most chosen indices
489
+ const secondMostChosenIndex = (mostChosenIndex + 1) % items.length;
490
+ const thirdMostChosenIndex = (mostChosenIndex + 2) % items.length;
491
+
492
+ // Return a function that generates a weighted list
493
+ return function () {
494
+ const weighted = [];
495
+ for (let i = 0; i < 10; i++) {
496
+ const rand = chance.d10(); // Random number between 1 and 10
497
+
498
+ // 35% chance to favor the most chosen index
499
+ if (chance.bool({ likelihood: 35 })) {
500
+ // 50% chance to slightly alter the index
501
+ if (chance.bool({ likelihood: 50 })) {
502
+ weighted.push(items[mostChosenIndex]);
503
+ } else {
504
+ const addOrSubtract = chance.bool({ likelihood: 50 }) ? -rand : rand;
505
+ let newIndex = mostChosenIndex + addOrSubtract;
506
+
507
+ // Ensure newIndex is within bounds
508
+ if (newIndex < 0) newIndex = 0;
509
+ if (newIndex >= items.length) newIndex = items.length - 1;
510
+ weighted.push(items[newIndex]);
511
+ }
512
+ }
513
+ // 25% chance to favor the second most chosen index
514
+ else if (chance.bool({ likelihood: 25 })) {
515
+ weighted.push(items[secondMostChosenIndex]);
516
+ }
517
+ // 15% chance to favor the third most chosen index
518
+ else if (chance.bool({ likelihood: 15 })) {
519
+ weighted.push(items[thirdMostChosenIndex]);
520
+ }
521
+ // Otherwise, pick a random item from the list
522
+ else {
523
+ weighted.push(chance.pickone(items));
524
+ }
525
+ }
526
+ return weighted;
527
+ };
528
+ }
529
+
530
530
  /*
531
531
  ----
532
532
  SHUFFLERS
@@ -627,7 +627,7 @@ function validateEventConfig(events) {
627
627
  return cleanEventConfig;
628
628
  }
629
629
 
630
- function validateTime(chosenTime, earliestTime, latestTime) {
630
+ function validTime(chosenTime, earliestTime, latestTime) {
631
631
  if (!earliestTime) earliestTime = global.NOW - (60 * 60 * 24 * 30); // 30 days ago
632
632
  if (!latestTime) latestTime = global.NOW;
633
633
 
@@ -644,6 +644,17 @@ function validateTime(chosenTime, earliestTime, latestTime) {
644
644
  return false;
645
645
  }
646
646
 
647
+ function validEvent(row) {
648
+ if (!row) return false;
649
+ if (!row.event) return false;
650
+ if (!row.time) return false;
651
+ if (!row.device_id && !row.user_id) return false;
652
+ if (!row.insert_id) return false;
653
+ if (!row.source) return false;
654
+ if (typeof row.time !== 'string') return false;
655
+ return true;
656
+ }
657
+
647
658
 
648
659
  /*
649
660
  ----
@@ -651,76 +662,18 @@ META
651
662
  ----
652
663
  */
653
664
 
654
- /**
655
- * our meta programming function which lets you mutate items as they are pushed into an array
656
- * @param {any[]} arr
657
- * @param {EnrichArrayOptions} opts
658
- * @returns {EnrichArray}}
659
- */
660
- function enrichArray(arr = [], opts = {}) {
661
- const { hook = a => a, type = "", ...rest } = opts;
662
-
663
- function transformThenPush(item) {
664
- if (item === null) return false;
665
- if (item === undefined) return false;
666
- if (typeof item === 'object') {
667
- if (Object.keys(item).length === 0) return false;
668
- }
669
665
 
670
- //hook is passed an array
671
- if (Array.isArray(item)) {
672
- for (const i of item) {
673
- try {
674
- const enriched = hook(i, type, rest);
675
- if (Array.isArray(enriched)) enriched.forEach(e => arr.push(e));
676
- else arr.push(enriched);
677
-
678
- }
679
- catch (e) {
680
- console.error(`\n\nyour hook had an error\n\n`, e);
681
- arr.push(i);
682
- return false;
683
- }
684
-
685
- }
686
- return true;
687
- }
688
-
689
- //hook is passed a single item
690
- else {
691
- try {
692
- const enriched = hook(item, type, rest);
693
- if (Array.isArray(enriched)) enriched.forEach(e => arr.push(e));
694
- else arr.push(enriched);
695
- return true;
696
- }
697
- catch (e) {
698
- console.error(`\n\nyour hook had an error\n\n`, e);
699
- arr.push(item);
700
- return false;
701
- }
702
- }
703
-
704
- }
705
-
706
- /** @type {EnrichArray} */
707
- // @ts-ignore
708
- const enrichedArray = arr;
709
-
710
-
711
- enrichedArray.hookPush = transformThenPush;
712
-
713
-
714
- return enrichedArray;
715
- };
716
666
 
667
+ /**
668
+ * @param {Config} config
669
+ */
717
670
  function buildFileNames(config) {
718
671
  const { format = "csv", groupKeys = [], lookupTables = [] } = config;
719
672
  let extension = "";
720
673
  extension = format === "csv" ? "csv" : "json";
721
674
  // const current = dayjs.utc().format("MM-DD-HH");
722
- const simName = config.simulationName;
723
- let writeDir = "./";
675
+ let simName = config.simulationName;
676
+ let writeDir = typeof config.writeToDisk === 'string' ? config.writeToDisk : "./";
724
677
  if (config.writeToDisk) {
725
678
  const dataFolder = path.resolve("./data");
726
679
  if (existsSync(dataFolder)) writeDir = dataFolder;
@@ -732,13 +685,17 @@ function buildFileNames(config) {
732
685
  const writePaths = {
733
686
  eventFiles: [path.join(writeDir, `${simName}-EVENTS.${extension}`)],
734
687
  userFiles: [path.join(writeDir, `${simName}-USERS.${extension}`)],
735
- adSpendFiles: [path.join(writeDir, `${simName}-AD-SPEND.${extension}`)],
688
+ adSpendFiles: [],
736
689
  scdFiles: [],
737
690
  mirrorFiles: [],
738
691
  groupFiles: [],
739
692
  lookupFiles: [],
740
693
  folder: writeDir,
741
694
  };
695
+ //add ad spend files
696
+ if (config?.hasAdSpend) {
697
+ writePaths.adSpendFiles.push(path.join(writeDir, `${simName}-AD-SPEND.${extension}`));
698
+ }
742
699
 
743
700
  //add SCD files
744
701
  const scdKeys = Object.keys(config?.scdProps || {});
@@ -781,7 +738,6 @@ function buildFileNames(config) {
781
738
  * @param {[string, number][]} arrayOfArrays
782
739
  */
783
740
  function progress(arrayOfArrays) {
784
- // @ts-ignore
785
741
  readline.cursorTo(process.stdout, 0);
786
742
  let message = "";
787
743
  for (const status of arrayOfArrays) {
@@ -819,8 +775,9 @@ CORE
819
775
  */
820
776
 
821
777
  //the function which generates $distinct_id + $anonymous_ids, $session_ids, and created, skewing towards the present
822
- function generateUser(user_id, numDays, amplitude = 1, frequency = 1, skew = 1) {
778
+ function generateUser(user_id, opts, amplitude = 1, frequency = 1, skew = 1) {
823
779
  const chance = getChance();
780
+ const { numDays, isAnonymous, hasAvatar, hasAnonIds, hasSessionIds } = opts;
824
781
  // Uniformly distributed `u`, then skew applied
825
782
  let u = Math.pow(chance.random(), skew);
826
783
 
@@ -832,16 +789,18 @@ function generateUser(user_id, numDays, amplitude = 1, frequency = 1, skew = 1)
832
789
 
833
790
  // Clamp values to ensure they are within the desired range
834
791
  daysAgoBorn = Math.min(daysAgoBorn, numDays);
792
+ const props = person(user_id, daysAgoBorn, isAnonymous, hasAvatar, hasAnonIds, hasSessionIds);
835
793
 
836
794
  const user = {
837
795
  distinct_id: user_id,
838
- ...person(numDays),
796
+ ...props,
839
797
  };
840
798
 
841
799
 
842
800
  return user;
843
801
  }
844
802
 
803
+ let soupHits = 0;
845
804
  /**
846
805
  * build sign waves basically
847
806
  * @param {number} [earliestTime]
@@ -867,9 +826,12 @@ function TimeSoup(earliestTime, latestTime, peaks = 5, deviation = 2, mean = 0)
867
826
  let isValidTime = false;
868
827
  do {
869
828
  iterations++;
829
+ soupHits++;
870
830
  offset = chance.normal({ mean: mean, dev: chunkSize / deviation });
871
- isValidTime = validateTime(chunkMid + offset, earliestTime, latestTime);
872
- if (iterations > 10000) throw new Error("Too many iterations");
831
+ isValidTime = validTime(chunkMid + offset, earliestTime, latestTime);
832
+ if (iterations > 25000) {
833
+ throw `${iterations} iterations... exceeded`;
834
+ }
873
835
  } while (chunkMid + offset < chunkStart || chunkMid + offset > chunkEnd);
874
836
 
875
837
  try {
@@ -890,15 +852,17 @@ function TimeSoup(earliestTime, latestTime, peaks = 5, deviation = 2, mean = 0)
890
852
  * @param {string} userId
891
853
  * @param {number} bornDaysAgo=30
892
854
  * @param {boolean} isAnonymous
855
+ * @param {boolean} hasAvatar
856
+ * @param {boolean} hasAnonIds
857
+ * @param {boolean} hasSessionIds
893
858
  * @return {Person}
894
859
  */
895
- function person(userId, bornDaysAgo = 30, isAnonymous = false) {
860
+ function person(userId, bornDaysAgo = 30, isAnonymous = false, hasAvatar = false, hasAnonIds = false, hasSessionIds = false) {
896
861
  const chance = getChance();
897
862
  //names and photos
898
863
  const l = chance.letter.bind(chance);
899
864
  let gender = chance.pickone(['male', 'female']);
900
865
  if (!gender) gender = "female";
901
- // @ts-ignore
902
866
  let first = chance.first({ gender });
903
867
  let last = chance.last();
904
868
  let name = `${first} ${last}`;
@@ -926,21 +890,23 @@ function person(userId, bornDaysAgo = 30, isAnonymous = false) {
926
890
  user.name = "Anonymous User";
927
891
  user.email = l() + l() + `*`.repeat(integer(3, 6)) + l() + `@` + l() + `*`.repeat(integer(3, 6)) + l() + `.` + choose(domainSuffix);
928
892
  delete user.avatar;
929
-
930
893
  }
931
894
 
895
+ if (!hasAvatar) delete user.avatar;
896
+
932
897
  //anon Ids
933
- if (global.MP_SIMULATION_CONFIG?.anonIds) {
898
+ if (hasAnonIds) {
934
899
  const clusterSize = integer(2, 10);
935
900
  for (let i = 0; i < clusterSize; i++) {
936
901
  const anonId = uid(42);
937
902
  user.anonymousIds.push(anonId);
938
903
  }
939
-
940
904
  }
941
905
 
906
+ if (!hasAnonIds) delete user.anonymousIds;
907
+
942
908
  //session Ids
943
- if (global.MP_SIMULATION_CONFIG?.sessionIds) {
909
+ if (hasSessionIds) {
944
910
  const sessionSize = integer(5, 30);
945
911
  for (let i = 0; i < sessionSize; i++) {
946
912
  const sessionId = [uid(5), uid(5), uid(5), uid(5)].join("-");
@@ -948,6 +914,8 @@ function person(userId, bornDaysAgo = 30, isAnonymous = false) {
948
914
  }
949
915
  }
950
916
 
917
+ if (!hasSessionIds) delete user.sessionIds;
918
+
951
919
  return user;
952
920
  };
953
921
 
@@ -1022,11 +990,14 @@ module.exports = {
1022
990
 
1023
991
  initChance,
1024
992
  getChance,
1025
- validateTime,
993
+
994
+ validTime,
995
+ validEvent,
996
+
1026
997
  boxMullerRandom,
1027
998
  applySkew,
1028
999
  mapToRange,
1029
- weightedRange,
1000
+ weighNumRange,
1030
1001
  progress,
1031
1002
  range,
1032
1003
  openFinder,
@@ -1044,11 +1015,10 @@ module.exports = {
1044
1015
  shuffleOutside,
1045
1016
  interruptArray,
1046
1017
  generateUser,
1047
- enrichArray,
1048
1018
  optimizedBoxMuller,
1049
1019
  buildFileNames,
1050
1020
  streamJSON,
1051
1021
  streamCSV,
1052
- inferFunnels,
1053
- datesBetween
1022
+ datesBetween,
1023
+ weighChoices
1054
1024
  };
@@ -0,0 +1,52 @@
1
+ /*
2
+ ----
3
+ TO DOs
4
+ ----
5
+ */
6
+
7
+ //!feature: fixedTimeFunnel? if set this funnel will occur for all users at the same time ['cards charged', 'charge complete']
8
+ //!feature: churn ... is churnFunnel, possible to return, etc
9
+ //!feature: send SCD data to mixpanel (blocked on dev)
10
+ //!feature: send and map lookup tables to mixpanel (also blocked on dev)
11
+ //!bug: using --mc flag reverts to --complex for some reason
12
+
13
+
14
+ import main from "../../index.js";
15
+ import simple from '../../schemas/simple.js';
16
+
17
+ /** @typedef {import('../../types').Config} Config */
18
+
19
+ /** @type {Config} */
20
+ const noWrites = {
21
+ ...simple,
22
+ numUsers: 10_000,
23
+ numEvents: 250_000,
24
+ writeToDisk: false,
25
+ };
26
+
27
+ /** @type {Config} */
28
+ const yesWrites = {
29
+ ...noWrites,
30
+ writeToDisk: true
31
+ };
32
+
33
+ console.log('concurrency benchmarking');
34
+
35
+ const concurrency = [1, 2, 3, 4, 5];
36
+
37
+ const results = [];
38
+ for (const concurrent of concurrency) {
39
+ console.log(`concurrency: ${concurrent}`);
40
+ // @ts-ignore
41
+ const test = await main({ ...noWrites, concurrency: concurrent });
42
+ results.push({ human: test.time.human, concurrency: concurrent });
43
+ console.log(`\t\tdone: ${test.time.human}\n\n`);
44
+ }
45
+
46
+ const display = results.map((r) => {
47
+ return `concurrency: ${r.concurrency} | duration: ${r.human}`;
48
+ });
49
+
50
+ console.log(display.join('\n\n'));
51
+
52
+ debugger;
File without changes