sync-open-chrome-tab-analyze-artifact.cjs 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. #!/usr/bin/env node
  2. 'use strict';
  3. const fs = require('node:fs/promises');
  4. const path = require('node:path');
  5. function usage() {
  6. return [
  7. 'Usage: node scripts/sync-open-chrome-tab-analyze-artifact.cjs --artifact <path>',
  8. '',
  9. 'Options:',
  10. ' --artifact <path> Path to artifact.json from sync-open-chrome-tab-simulate',
  11. ' --pretty Pretty-print JSON output',
  12. ' -h, --help Show this message',
  13. ].join('\n');
  14. }
  15. function parseArgs(argv) {
  16. const result = {
  17. artifactPath: null,
  18. pretty: false,
  19. help: false,
  20. };
  21. for (let i = 0; i < argv.length; i += 1) {
  22. const arg = argv[i];
  23. if (arg === '--help' || arg === '-h') {
  24. result.help = true;
  25. continue;
  26. }
  27. if (arg === '--pretty') {
  28. result.pretty = true;
  29. continue;
  30. }
  31. if (arg === '--artifact') {
  32. const next = argv[i + 1];
  33. if (typeof next !== 'string' || next.length === 0) {
  34. throw new Error('--artifact must be a non-empty path');
  35. }
  36. result.artifactPath = next;
  37. i += 1;
  38. continue;
  39. }
  40. throw new Error('Unknown argument: ' + arg);
  41. }
  42. if (!result.help && !result.artifactPath) {
  43. throw new Error('--artifact is required');
  44. }
  45. return result;
  46. }
  47. function pickFirstMismatchClient(clients) {
  48. for (const client of clients) {
  49. if (client && client.mismatch && typeof client.mismatch === 'object') {
  50. return client;
  51. }
  52. }
  53. return null;
  54. }
  55. function pickFirstTxRejectedClient(clients) {
  56. for (const client of clients) {
  57. if (client && client.txRejected && typeof client.txRejected === 'object') {
  58. return client;
  59. }
  60. }
  61. return null;
  62. }
  63. function compareRequestedPlans(clients) {
  64. const withPlan = clients.filter((client) => Array.isArray(client?.requestedPlan) && client.requestedPlan.length > 0);
  65. if (withPlan.length < 2) {
  66. return { comparable: false, reason: 'not enough clients with requestedPlan' };
  67. }
  68. const base = withPlan[0];
  69. const minLen = withPlan.reduce((acc, client) => Math.min(acc, client.requestedPlan.length), Infinity);
  70. for (let i = 0; i < minLen; i += 1) {
  71. const baseOp = base.requestedPlan[i];
  72. for (let j = 1; j < withPlan.length; j += 1) {
  73. const other = withPlan[j];
  74. if (other.requestedPlan[i] !== baseOp) {
  75. return {
  76. comparable: true,
  77. firstDiffIndex: i,
  78. baseClient: base.instanceIndex,
  79. baseOp,
  80. otherClient: other.instanceIndex,
  81. otherOp: other.requestedPlan[i],
  82. };
  83. }
  84. }
  85. }
  86. return {
  87. comparable: true,
  88. firstDiffIndex: null,
  89. message: 'requestedPlan prefix is identical across clients',
  90. };
  91. }
  92. function summarizeClient(client) {
  93. return {
  94. session: client?.session || null,
  95. instanceIndex: client?.instanceIndex ?? null,
  96. ok: Boolean(client?.ok),
  97. cancelled: client?.cancelled === true,
  98. cancelledReason: client?.cancelledReason || null,
  99. failureType: client?.failureType || null,
  100. mismatch: client?.mismatch || null,
  101. txRejected: client?.txRejected || null,
  102. errorCount: Number(client?.errorCount || 0),
  103. requestedOps: Number(client?.requestedOps || 0),
  104. executedOps: Number(client?.executedOps || 0),
  105. lastOps: Array.isArray(client?.opLogTail) ? client.opLogTail.slice(-20) : [],
  106. errors: Array.isArray(client?.errors) ? client.errors.slice(-20) : [],
  107. };
  108. }
  109. async function main() {
  110. let args;
  111. try {
  112. args = parseArgs(process.argv.slice(2));
  113. } catch (error) {
  114. console.error(error.message);
  115. console.error('\n' + usage());
  116. process.exit(1);
  117. return;
  118. }
  119. if (args.help) {
  120. console.log(usage());
  121. return;
  122. }
  123. const artifactPath = path.resolve(args.artifactPath);
  124. const content = await fs.readFile(artifactPath, 'utf8');
  125. const artifact = JSON.parse(content);
  126. const clients = Array.isArray(artifact?.clients) ? artifact.clients : [];
  127. const firstMismatch = pickFirstMismatchClient(clients);
  128. const firstTxRejected = pickFirstTxRejectedClient(clients);
  129. const cancelledPeers = clients
  130. .filter((client) =>
  131. client?.cancelledReason === 'cancelled_due_to_peer_checksum_mismatch' ||
  132. client?.cancelledReason === 'cancelled_due_to_peer_tx_rejected'
  133. )
  134. .map((client) => ({
  135. instanceIndex: client.instanceIndex,
  136. session: client.session,
  137. peerInstanceIndex: client.peerInstanceIndex ?? null,
  138. cancelledReason: client.cancelledReason || null,
  139. }));
  140. const report = {
  141. artifactPath,
  142. runId: artifact?.runId || null,
  143. createdAt: artifact?.createdAt || null,
  144. summary: artifact?.summary || {},
  145. failFast: artifact?.failFast || {},
  146. mismatchCount: Number(artifact?.mismatchCount || 0),
  147. txRejectedCount: Number(artifact?.txRejectedCount || 0),
  148. firstMismatch: firstMismatch ? summarizeClient(firstMismatch) : null,
  149. firstTxRejected: firstTxRejected ? summarizeClient(firstTxRejected) : null,
  150. cancelledPeers,
  151. requestedPlanComparison: compareRequestedPlans(clients),
  152. clients: clients.map(summarizeClient),
  153. };
  154. if (args.pretty) {
  155. console.log(JSON.stringify(report, null, 2));
  156. } else {
  157. console.log(JSON.stringify(report));
  158. }
  159. }
  160. main().catch((error) => {
  161. console.error(error.stack || String(error));
  162. process.exit(1);
  163. });