collection.ts 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. import { Layout } from "../src/layout"
  2. import { createHarness, type Workload } from "./harness"
  3. type Job = {
  4. readonly id: number
  5. readonly labels: readonly number[]
  6. readonly status: "running" | "retrying"
  7. readonly value: number
  8. }
  9. const size = 1_000
  10. const iterations = 200_000
  11. const initial = Array.from({ length: size }, (_, id): Job => ({
  12. id,
  13. labels: [id],
  14. status: id === 750 ? "retrying" : "running",
  15. value: 0,
  16. }))
  17. const JobLayout = Layout.struct({
  18. id: Layout.key(Layout.number),
  19. labels: Layout.array(Layout.number),
  20. status: Layout.string,
  21. value: Layout.number,
  22. })
  23. const Jobs = Layout.collection(JobLayout, ({ members, first }) => ({
  24. labels: members(["labels"], (job) => job.labels),
  25. retry: first(["status"], (job) => job.status === "retrying"),
  26. }))
  27. const LabeledJobs = Layout.collection(JobLayout, ({ members }) => ({
  28. labels: members(["labels"], (job) => job.labels),
  29. }))
  30. const JobPlan = Layout.compile(JobLayout)
  31. const bench = createHarness({ warmup: 10_000, width: 38 })
  32. function manualValueUpdate(): Workload {
  33. const jobs = JobPlan.make(initial)
  34. const labels = new Set(initial.flatMap((job) => job.labels))
  35. const retry = jobs.get(750)!
  36. return {
  37. run(index) {
  38. const id = index % size
  39. jobs.modify(id, (job) => ({ ...job, value: index + 1 }))
  40. },
  41. consume: () => Number(labels.has(500)) + retry().id + jobs.get(iterations % size)!().value,
  42. }
  43. }
  44. function indexedValueUpdate(): Workload {
  45. const jobs = Jobs.make(initial)
  46. return {
  47. run(index) {
  48. const id = index % size
  49. jobs.modify(id, (job) => ({ ...job, value: index + 1 }))
  50. },
  51. consume: () =>
  52. Number(jobs.hasMember("labels", 500)) + jobs.first("retry")()!.id + jobs.get(iterations % size)!().value,
  53. }
  54. }
  55. function manualMemberAppend(): Workload {
  56. const jobs = JobPlan.make(initial)
  57. const labels = new Map(initial.flatMap((job) => job.labels.map((label) => [label, 1])))
  58. const members = new Map(initial.map((job) => [job.id, new Set(job.labels)]))
  59. return {
  60. run(index) {
  61. const id = index % size
  62. const label = size + index
  63. jobs.modify(id, (job) => {
  64. const next = [...job.labels.slice(-7), label]
  65. const previous = members.get(id)!
  66. const current = new Set(next)
  67. previous.forEach((member) => {
  68. if (current.has(member)) return
  69. const count = labels.get(member)!
  70. if (count === 1) labels.delete(member)
  71. if (count > 1) labels.set(member, count - 1)
  72. })
  73. current.forEach((member) => {
  74. if (!previous.has(member)) labels.set(member, (labels.get(member) ?? 0) + 1)
  75. })
  76. members.set(id, current)
  77. return { ...job, labels: next }
  78. })
  79. },
  80. consume: () => Number(labels.has(size + iterations - 1)) + jobs.get(iterations % size)!().labels.length,
  81. }
  82. }
  83. function indexedMemberAppend(): Workload {
  84. const jobs = LabeledJobs.make(initial)
  85. return {
  86. run(index) {
  87. const id = index % size
  88. const label = size + index
  89. jobs.modify(id, (job) => ({
  90. ...job,
  91. labels: [...job.labels.slice(-7), label],
  92. }))
  93. },
  94. consume: () =>
  95. Number(jobs.hasMember("labels", size + iterations - 1)) + jobs.get(iterations % size)!().labels.length,
  96. }
  97. }
  98. function manualGrowingAppend(): Workload {
  99. const jobs = JobPlan.make([{ id: 0, labels: [], status: "running", value: 0 }])
  100. const labels = new Set<number>()
  101. let label = 0
  102. return {
  103. run() {
  104. const next = label++
  105. jobs.modify(0, (job) => ({ ...job, labels: [...job.labels, next] }))
  106. labels.add(next)
  107. },
  108. consume: () => Number(labels.has(label - 1)) + jobs.get(0)!().labels.length,
  109. }
  110. }
  111. function indexedGrowingAppend(): Workload {
  112. const jobs = LabeledJobs.make([{ id: 0, labels: [], status: "running", value: 0 }])
  113. let label = 0
  114. return {
  115. run() {
  116. const next = label++
  117. jobs.modify(0, (job) => ({ ...job, labels: [...job.labels, next] }), {
  118. members: { labels: { add: [next] } },
  119. })
  120. },
  121. consume: () => Number(jobs.hasMember("labels", label - 1)) + jobs.get(0)!().labels.length,
  122. }
  123. }
  124. function automaticGrowingAppend(): Workload {
  125. const jobs = LabeledJobs.make([{ id: 0, labels: [], status: "running", value: 0 }])
  126. let label = 0
  127. return {
  128. run() {
  129. const next = label++
  130. jobs.modify(0, (job) => ({ ...job, labels: [...job.labels, next] }))
  131. },
  132. consume: () => Number(jobs.hasMember("labels", label - 1)) + jobs.get(0)!().labels.length,
  133. }
  134. }
  135. function collectionSet(indexed: boolean, changed: boolean): Workload {
  136. const jobs = indexed ? Jobs.make(initial) : JobPlan.make(initial)
  137. return {
  138. run(index) {
  139. const id = index % size
  140. const current = jobs.values()
  141. const job = current[id]
  142. jobs.set(current.with(id, { ...job, value: changed ? job.value + 1 : job.value }))
  143. },
  144. consume: () => jobs.values()[0].value + jobs.values().length,
  145. }
  146. }
  147. console.log(`Compiled collection benchmark (${size} items, ${bench.samples} samples)\n`)
  148. const value = bench.compare(iterations, [
  149. { name: "Handwritten value update", make: manualValueUpdate },
  150. { name: "Compiled indexed value update", make: indexedValueUpdate },
  151. ])
  152. const member = bench.compare(iterations, [
  153. { name: "Handwritten member append", make: manualMemberAppend },
  154. { name: "Compiled indexed member append", make: indexedMemberAppend },
  155. ])
  156. const growing = bench.compare(2_000, [
  157. { name: "Handwritten growing append", make: manualGrowingAppend },
  158. { name: "Automatic indexed growing append", make: automaticGrowingAppend },
  159. { name: "Indexed delta growing append", make: indexedGrowingAppend },
  160. ])
  161. const equivalentSet = bench.compare(2_000, [
  162. { name: "Bare keyed equivalent set", make: () => collectionSet(false, false) },
  163. { name: "Compiled indexed equivalent set", make: () => collectionSet(true, false) },
  164. ])
  165. const changedSet = bench.compare(2_000, [
  166. { name: "Bare keyed changed set", make: () => collectionSet(false, true) },
  167. { name: "Compiled indexed changed set", make: () => collectionSet(true, true) },
  168. ])
  169. console.log("\nRatios to handwritten (lower is faster)")
  170. console.log(`Value update: ${value.ratio(1, 0).toFixed(3)}x`)
  171. console.log(`Member append: ${member.ratio(1, 0).toFixed(3)}x`)
  172. console.log(`Automatic growing append: ${growing.ratio(1, 0).toFixed(3)}x`)
  173. console.log(`Delta growing append: ${growing.ratio(2, 0).toFixed(3)}x`)
  174. console.log(`Equivalent collection set: ${equivalentSet.ratio(1, 0).toFixed(3)}x`)
  175. console.log(`Changed collection set: ${changedSet.ratio(1, 0).toFixed(3)}x`)
  176. console.log(`METRIC collection_value_ratio=${value.ratio(1, 0).toFixed(6)}`)
  177. console.log(`METRIC collection_member_ratio=${member.ratio(1, 0).toFixed(6)}`)
  178. console.log(`METRIC collection_automatic_growing_ratio=${growing.ratio(1, 0).toFixed(6)}`)
  179. console.log(`METRIC collection_delta_growing_ratio=${growing.ratio(2, 0).toFixed(6)}`)
  180. console.log(`METRIC collection_equivalent_set_ratio=${equivalentSet.ratio(1, 0).toFixed(6)}`)
  181. console.log(`METRIC collection_changed_set_ratio=${changedSet.ratio(1, 0).toFixed(6)}`)
  182. bench.finish()