generate-summary.ts 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. import fs from 'fs';
  2. import path from 'path';
  3. import readline from 'readline';
  4. export type DataPoint = {
  5. type: 'Point';
  6. data: {
  7. time: string;
  8. value: number;
  9. };
  10. metric: string;
  11. };
  12. export type TimeSeriesPoint = { timestamp: number; value: number };
  13. export type LoadTestSummary = {
  14. timestamp: string;
  15. script: string;
  16. productCount: number;
  17. testDuration: number;
  18. requests: number;
  19. throughput: number;
  20. requestDurationSummary: {
  21. avg: number;
  22. min: number;
  23. max: number;
  24. med: number;
  25. p90: number;
  26. p95: number;
  27. p99: number;
  28. };
  29. requestDurationTimeSeries: TimeSeriesPoint[];
  30. concurrentUsersTimeSeries: TimeSeriesPoint[];
  31. requestCountTimeSeries: TimeSeriesPoint[];
  32. };
  33. if (require.main === module) {
  34. const resultsFile = process.argv[2];
  35. generateSummary(resultsFile).then(result => {
  36. // tslint:disable-next-line:no-console
  37. console.log(JSON.stringify(result, null, 2));
  38. process.exit(0);
  39. });
  40. }
  41. /**
  42. * Reads the raw JSON file output from k6 and parses it into a summary object.
  43. */
  44. export async function generateSummary(rawResultsFile: string): Promise<LoadTestSummary> {
  45. const lineReader = readline.createInterface({
  46. input: fs.createReadStream(path.join(__dirname, 'results', rawResultsFile)),
  47. crlfDelay: Infinity,
  48. });
  49. let reqs = 0;
  50. let min = Infinity;
  51. let max = 0;
  52. let sum = 0;
  53. let startTime = 0;
  54. let endTime = 0;
  55. const durations: number[] = [];
  56. const requestDurationTimeSeries: TimeSeriesPoint[] = [];
  57. const concurrentUsersTimeSeries: TimeSeriesPoint[] = [];
  58. const requestCountTimeSeries: TimeSeriesPoint[] = [];
  59. return new Promise((resolve, reject) => {
  60. lineReader.on('line', line => {
  61. const row = JSON.parse(line);
  62. if (isDataPoint(row)) {
  63. if (row.metric === 'http_reqs') {
  64. reqs++;
  65. requestCountTimeSeries.push({ timestamp: +new Date(row.data.time), value: reqs });
  66. }
  67. if (row.metric === 'http_req_duration') {
  68. const duration = row.data.value;
  69. durations.push(duration);
  70. requestDurationTimeSeries.push({
  71. timestamp: +new Date(row.data.time),
  72. value: row.data.value,
  73. });
  74. if (duration > max) {
  75. max = duration;
  76. }
  77. if (duration < min) {
  78. min = duration;
  79. }
  80. sum += duration;
  81. }
  82. if (row.metric === 'vus') {
  83. concurrentUsersTimeSeries.push({
  84. timestamp: +new Date(row.data.time),
  85. value: row.data.value,
  86. });
  87. }
  88. if (!startTime) {
  89. startTime = +new Date(row.data.time);
  90. }
  91. endTime = +new Date(row.data.time);
  92. }
  93. });
  94. lineReader.on('close', () => {
  95. const duration = (endTime - startTime) / 1000;
  96. durations.sort((a, b) => a - b);
  97. resolve({
  98. timestamp: new Date().toISOString(),
  99. script: rawResultsFile.split('.')[0],
  100. productCount: +rawResultsFile.split('.')[2],
  101. testDuration: duration,
  102. requests: reqs,
  103. throughput: reqs / duration,
  104. requestDurationSummary: {
  105. avg: sum / reqs,
  106. min,
  107. max,
  108. med: durations[Math.round(durations.length / 2)],
  109. p90: percentile(90, durations),
  110. p95: percentile(95, durations),
  111. p99: percentile(99, durations),
  112. },
  113. requestDurationTimeSeries,
  114. concurrentUsersTimeSeries,
  115. requestCountTimeSeries,
  116. });
  117. });
  118. });
  119. }
  120. function isDataPoint(row: any): row is DataPoint {
  121. return row && row.type === 'Point';
  122. }
  123. function percentile(p: number, sortedValues: number[]): number {
  124. const ordinalRank = (p / 100) * sortedValues.length - 1;
  125. if (Number.isInteger(ordinalRank)) {
  126. return sortedValues[ordinalRank];
  127. }
  128. // if the rank is not an integer, use linear interpolation between the
  129. // surrounding values.
  130. const j = sortedValues[Math.floor(ordinalRank)];
  131. const k = sortedValues[Math.ceil(ordinalRank)];
  132. const f = ordinalRank - Math.floor(ordinalRank);
  133. return j + (k - j) * f;
  134. }