run.js 11 KB

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