test-incremental-parser.mjs 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. import { parser } from '../../frontend/js/features/source-editor/lezer-latex/latex.mjs'
  2. import * as fs from 'node:fs'
  3. import * as path from 'node:path'
  4. import { fileURLToPath } from 'node:url'
  5. import { TreeFragment } from '@lezer/common'
  6. import minimist from 'minimist'
  7. import { seed, random } from './random.mjs'
  8. const argv = minimist(process.argv.slice(2))
  9. const NUMBER_OF_OPS = argv.ops || 1000
  10. const CSV_OUTPUT = argv.csv || false
  11. const SEED = argv.seed
  12. if (SEED) {
  13. seed(SEED)
  14. }
  15. const __dirname = path.dirname(fileURLToPath(import.meta.url))
  16. const examplesDir = path.join(
  17. __dirname,
  18. '../../test/unit/src/LezerLatex/examples'
  19. )
  20. const folder = examplesDir
  21. for (const file of fs.readdirSync(folder).sort()) {
  22. if (!/\.tex$/.test(file)) continue
  23. const name = /^[^.]*/.exec(file)[0]
  24. const content = fs.readFileSync(path.join(folder, file), 'utf8')
  25. runPerformanceTests(name, content)
  26. }
  27. function runPerformanceTests(name, content) {
  28. const insertEnd = writeTextAt(
  29. content,
  30. content.length,
  31. content.substring(0, NUMBER_OF_OPS)
  32. )
  33. const insertBeginning = writeTextAt(
  34. content,
  35. 0,
  36. content.substring(0, NUMBER_OF_OPS)
  37. )
  38. const insertMiddle = writeTextAt(
  39. content,
  40. Math.floor(content.length / 2),
  41. content.substring(0, NUMBER_OF_OPS)
  42. )
  43. const randomDelete = randomDeletions(content, NUMBER_OF_OPS)
  44. const middleDelete = deletionsFromMiddle(content, NUMBER_OF_OPS)
  45. const randomInsert = randomInsertions(content, NUMBER_OF_OPS)
  46. if (CSV_OUTPUT) {
  47. console.log(
  48. [
  49. name,
  50. insertBeginning.average,
  51. insertMiddle.average,
  52. insertEnd.average,
  53. randomInsert.average,
  54. randomDelete.average,
  55. middleDelete.average,
  56. content.length,
  57. ].join(',')
  58. )
  59. } else {
  60. console.log({
  61. name,
  62. insertAtEnd: insertEnd.average,
  63. insertAtBeginning: insertBeginning.average,
  64. insertAtMiddle: insertMiddle.average,
  65. randomDelete: randomDelete.average,
  66. middleDelete: middleDelete.average,
  67. randomInsert: randomInsert.average,
  68. docLength: content.length,
  69. })
  70. }
  71. }
  72. function timedChanges(document, changes, changeFn) {
  73. let totalParseTime = 0
  74. // Do a fresh parse to get TreeFragments
  75. const initialTree = parser.parse(document)
  76. let fragments = TreeFragment.addTree(initialTree)
  77. let currentDoc = document
  78. for (let i = 0; i < changes; ++i) {
  79. const change = changeFn(currentDoc, i)
  80. currentDoc = change.text
  81. // Do a timed parse
  82. const start = performance.now()
  83. fragments = TreeFragment.applyChanges(fragments, [change.range])
  84. const tree = parser.parse(currentDoc, fragments)
  85. fragments = TreeFragment.addTree(tree, fragments)
  86. const end = performance.now()
  87. totalParseTime += end - start
  88. }
  89. return {
  90. total: totalParseTime,
  91. average: totalParseTime / changes,
  92. ops: changes,
  93. fragments: fragments.length,
  94. }
  95. }
  96. // Write and parse after every character insertion
  97. function writeTextAt(document, position, text) {
  98. return timedChanges(document, text.length, (currentDoc, index) =>
  99. insertAt(currentDoc, position + index, text[index])
  100. )
  101. }
  102. function randomInsertions(document, num) {
  103. return timedChanges(document, num, currentDoc =>
  104. insertAt(currentDoc, Math.floor(random() * currentDoc.length), 'a')
  105. )
  106. }
  107. function randomDeletions(document, num) {
  108. return timedChanges(document, num, currentDoc =>
  109. deleteAt(currentDoc, Math.floor(random() * currentDoc.length), 1)
  110. )
  111. }
  112. function deletionsFromMiddle(document, num) {
  113. const deletionPoint = Math.floor(document.length / 2)
  114. const deletions = Math.min(num, deletionPoint - 1)
  115. return timedChanges(document, deletions, (currentDoc, index) =>
  116. deleteAt(currentDoc, deletionPoint - index, 1)
  117. )
  118. }
  119. function insertAt(document, position, text) {
  120. const start = document.substring(0, position)
  121. const end = document.substring(position)
  122. return {
  123. text: start + text + end,
  124. range: {
  125. fromA: position,
  126. toA: position,
  127. fromB: position,
  128. toB: position + text.length,
  129. },
  130. }
  131. }
  132. function deleteAt(document, position, length = 1) {
  133. const start = document.substring(0, position)
  134. const end = document.substring(position + length)
  135. return {
  136. text: start + end,
  137. range: {
  138. fromA: position,
  139. toA: position + length,
  140. fromB: position,
  141. toB: position,
  142. },
  143. }
  144. }