run.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391
  1. /* eslint-disable
  2. camelcase,
  3. no-return-assign,
  4. no-undef,
  5. no-unused-vars,
  6. */
  7. // TODO: This file was created by bulk-decaffeinate.
  8. // Fix any style issues and re-enable lint.
  9. /*
  10. * decaffeinate suggestions:
  11. * DS101: Remove unnecessary use of Array.from
  12. * DS102: Remove unnecessary code created because of implicit returns
  13. * DS202: Simplify dynamic range loops
  14. * DS205: Consider reworking code to avoid use of IIFEs
  15. * DS207: Consider shorter variations of null checks
  16. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  17. */
  18. const DocUpdaterClient = require('../../acceptance/js/helpers/DocUpdaterClient')
  19. // MockTrackChangesApi = require "../../acceptance/js/helpers/MockTrackChangesApi"
  20. // MockWebApi = require "../../acceptance/js/helpers/MockWebApi"
  21. const assert = require('assert')
  22. const async = require('async')
  23. const insert = function (string, pos, content) {
  24. const result = string.slice(0, pos) + content + string.slice(pos)
  25. return result
  26. }
  27. const transform = function (op1, op2) {
  28. if (op2.p < op1.p) {
  29. return {
  30. p: op1.p + op2.i.length,
  31. i: op1.i,
  32. }
  33. } else {
  34. return op1
  35. }
  36. }
  37. class StressTestClient {
  38. constructor(options) {
  39. if (options == null) {
  40. options = {}
  41. }
  42. this.options = options
  43. if (this.options.updateDelay == null) {
  44. this.options.updateDelay = 200
  45. }
  46. this.project_id = this.options.project_id || DocUpdaterClient.randomId()
  47. this.doc_id = this.options.doc_id || DocUpdaterClient.randomId()
  48. this.pos = this.options.pos || 0
  49. this.content = this.options.content || ''
  50. this.client_id = DocUpdaterClient.randomId()
  51. this.version = this.options.version || 0
  52. this.inflight_op = null
  53. this.charCode = 0
  54. this.counts = {
  55. conflicts: 0,
  56. local_updates: 0,
  57. remote_updates: 0,
  58. max_delay: 0,
  59. }
  60. DocUpdaterClient.subscribeToAppliedOps((channel, update) => {
  61. update = JSON.parse(update)
  62. if (update.error != null) {
  63. console.error(new Error(`Error from server: '${update.error}'`))
  64. return
  65. }
  66. if (update.doc_id === this.doc_id) {
  67. return this.processReply(update)
  68. }
  69. })
  70. }
  71. sendUpdate() {
  72. const data = String.fromCharCode(65 + (this.charCode++ % 26))
  73. this.content = insert(this.content, this.pos, data)
  74. this.inflight_op = {
  75. i: data,
  76. p: this.pos++,
  77. }
  78. this.resendUpdate()
  79. return (this.inflight_op_sent = Date.now())
  80. }
  81. resendUpdate() {
  82. assert(this.inflight_op != null)
  83. DocUpdaterClient.sendUpdate(this.project_id, this.doc_id, {
  84. doc: this.doc_id,
  85. op: [this.inflight_op],
  86. v: this.version,
  87. meta: {
  88. source: this.client_id,
  89. },
  90. dupIfSource: [this.client_id],
  91. })
  92. return (this.update_timer = setTimeout(() => {
  93. console.log(
  94. `[${new Date()}] \t[${this.client_id.slice(
  95. 0,
  96. 4
  97. )}] WARN: Resending update after 5 seconds`
  98. )
  99. return this.resendUpdate()
  100. }, 5000))
  101. }
  102. processReply(update) {
  103. if (update.op.v !== this.version) {
  104. if (update.op.v < this.version) {
  105. console.log(
  106. `[${new Date()}] \t[${this.client_id.slice(
  107. 0,
  108. 4
  109. )}] WARN: Duplicate ack (already seen version)`
  110. )
  111. return
  112. } else {
  113. console.error(
  114. `[${new Date()}] \t[${this.client_id.slice(
  115. 0,
  116. 4
  117. )}] ERROR: Version jumped ahead (client: ${this.version}, op: ${
  118. update.op.v
  119. })`
  120. )
  121. }
  122. }
  123. this.version++
  124. if (update.op.meta.source === this.client_id) {
  125. if (this.inflight_op != null) {
  126. this.counts.local_updates++
  127. this.inflight_op = null
  128. clearTimeout(this.update_timer)
  129. const delay = Date.now() - this.inflight_op_sent
  130. this.counts.max_delay = Math.max(this.counts.max_delay, delay)
  131. return this.continue()
  132. } else {
  133. return console.log(
  134. `[${new Date()}] \t[${this.client_id.slice(
  135. 0,
  136. 4
  137. )}] WARN: Duplicate ack`
  138. )
  139. }
  140. } else {
  141. assert(update.op.op.length === 1)
  142. this.counts.remote_updates++
  143. let external_op = update.op.op[0]
  144. if (this.inflight_op != null) {
  145. this.counts.conflicts++
  146. this.inflight_op = transform(this.inflight_op, external_op)
  147. external_op = transform(external_op, this.inflight_op)
  148. }
  149. if (external_op.p < this.pos) {
  150. this.pos += external_op.i.length
  151. }
  152. return (this.content = insert(this.content, external_op.p, external_op.i))
  153. }
  154. }
  155. continue() {
  156. if (this.updateCount > 0) {
  157. this.updateCount--
  158. return setTimeout(() => {
  159. return this.sendUpdate()
  160. }, this.options.updateDelay * (0.5 + Math.random()))
  161. } else {
  162. return this.updateCallback()
  163. }
  164. }
  165. runForNUpdates(n, callback) {
  166. if (callback == null) {
  167. callback = function () {}
  168. }
  169. this.updateCallback = callback
  170. this.updateCount = n
  171. return this.continue()
  172. }
  173. check(callback) {
  174. if (callback == null) {
  175. callback = function () {}
  176. }
  177. return DocUpdaterClient.getDoc(
  178. this.project_id,
  179. this.doc_id,
  180. (error, res, body) => {
  181. if (error != null) {
  182. throw error
  183. }
  184. if (body.lines == null) {
  185. return console.error(
  186. `[${new Date()}] \t[${this.client_id.slice(
  187. 0,
  188. 4
  189. )}] ERROR: Invalid response from get doc (${doc_id})`,
  190. body
  191. )
  192. }
  193. const content = body.lines.join('\n')
  194. const { version } = body
  195. if (content !== this.content) {
  196. if (version === this.version) {
  197. console.error(
  198. `[${new Date()}] \t[${this.client_id.slice(
  199. 0,
  200. 4
  201. )}] Error: Client content does not match server.`
  202. )
  203. console.error(`Server: ${content.split('a')}`)
  204. console.error(`Client: ${this.content.split('a')}`)
  205. } else {
  206. console.error(
  207. `[${new Date()}] \t[${this.client_id.slice(
  208. 0,
  209. 4
  210. )}] Error: Version mismatch (Server: '${version}', Client: '${
  211. this.version
  212. }')`
  213. )
  214. }
  215. }
  216. if (!this.isContentValid(this.content)) {
  217. const iterable = this.content.split('')
  218. for (let i = 0; i < iterable.length; i++) {
  219. const chunk = iterable[i]
  220. if (chunk != null && chunk !== 'a') {
  221. console.log(chunk, i)
  222. }
  223. }
  224. throw new Error('bad content')
  225. }
  226. return callback()
  227. }
  228. )
  229. }
  230. isChunkValid(chunk) {
  231. const char = 0
  232. for (let i = 0; i < chunk.length; i++) {
  233. const letter = chunk[i]
  234. if (letter.charCodeAt(0) !== 65 + (i % 26)) {
  235. console.error(
  236. `[${new Date()}] \t[${this.client_id.slice(0, 4)}] Invalid Chunk:`,
  237. chunk
  238. )
  239. return false
  240. }
  241. }
  242. return true
  243. }
  244. isContentValid(content) {
  245. for (const chunk of Array.from(content.split('a'))) {
  246. if (chunk != null && chunk !== '') {
  247. if (!this.isChunkValid(chunk)) {
  248. console.error(
  249. `[${new Date()}] \t[${this.client_id.slice(0, 4)}] Invalid content`,
  250. content
  251. )
  252. return false
  253. }
  254. }
  255. }
  256. return true
  257. }
  258. }
  259. const checkDocument = function (project_id, doc_id, clients, callback) {
  260. if (callback == null) {
  261. callback = function () {}
  262. }
  263. const jobs = clients.map(client => cb => client.check(cb))
  264. return async.parallel(jobs, callback)
  265. }
  266. const printSummary = function (doc_id, clients) {
  267. const slot = require('cluster-key-slot')
  268. const now = new Date()
  269. console.log(
  270. `[${now}] [${doc_id.slice(0, 4)} (slot: ${slot(doc_id)})] ${
  271. clients.length
  272. } clients...`
  273. )
  274. return (() => {
  275. const result = []
  276. for (const client of Array.from(clients)) {
  277. console.log(
  278. `[${now}] \t[${client.client_id.slice(0, 4)}] { local: ${
  279. client.counts.local_updates
  280. }, remote: ${client.counts.remote_updates}, conflicts: ${
  281. client.counts.conflicts
  282. }, max_delay: ${client.counts.max_delay} }`
  283. )
  284. result.push(
  285. (client.counts = {
  286. local_updates: 0,
  287. remote_updates: 0,
  288. conflicts: 0,
  289. max_delay: 0,
  290. })
  291. )
  292. }
  293. return result
  294. })()
  295. }
  296. const CLIENT_COUNT = parseInt(process.argv[2], 10)
  297. const UPDATE_DELAY = parseInt(process.argv[3], 10)
  298. const SAMPLE_INTERVAL = parseInt(process.argv[4], 10)
  299. for (const doc_and_project_id of Array.from(process.argv.slice(5))) {
  300. ;(function (doc_and_project_id) {
  301. const [project_id, doc_id] = Array.from(doc_and_project_id.split(':'))
  302. console.log({ project_id, doc_id })
  303. return DocUpdaterClient.setDocLines(
  304. project_id,
  305. doc_id,
  306. [new Array(CLIENT_COUNT + 2).join('a')],
  307. null,
  308. null,
  309. error => {
  310. if (error != null) {
  311. throw error
  312. }
  313. return DocUpdaterClient.getDoc(
  314. project_id,
  315. doc_id,
  316. (error, res, body) => {
  317. let runBatch
  318. if (error != null) {
  319. throw error
  320. }
  321. if (body.lines == null) {
  322. return console.error(
  323. `[${new Date()}] ERROR: Invalid response from get doc (${doc_id})`,
  324. body
  325. )
  326. }
  327. const content = body.lines.join('\n')
  328. const { version } = body
  329. const clients = []
  330. for (
  331. let pos = 1, end = CLIENT_COUNT, asc = end >= 1;
  332. asc ? pos <= end : pos >= end;
  333. asc ? pos++ : pos--
  334. ) {
  335. ;(function (pos) {
  336. const client = new StressTestClient({
  337. doc_id,
  338. project_id,
  339. content,
  340. pos,
  341. version,
  342. updateDelay: UPDATE_DELAY,
  343. })
  344. return clients.push(client)
  345. })(pos)
  346. }
  347. return (runBatch = function () {
  348. const jobs = clients.map(
  349. client => cb =>
  350. client.runForNUpdates(SAMPLE_INTERVAL / UPDATE_DELAY, cb)
  351. )
  352. return async.parallel(jobs, error => {
  353. if (error != null) {
  354. throw error
  355. }
  356. printSummary(doc_id, clients)
  357. return checkDocument(project_id, doc_id, clients, error => {
  358. if (error != null) {
  359. throw error
  360. }
  361. return runBatch()
  362. })
  363. })
  364. })()
  365. }
  366. )
  367. }
  368. )
  369. })(doc_and_project_id)
  370. }