keyed.ts 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. import { createComputed, createRoot } from "solid-js"
  2. import { createStore, reconcile } from "solid-js/store"
  3. import { Keyed } from "../src"
  4. import { createHarness, type Workload } from "./harness"
  5. type Item = {
  6. readonly id: number
  7. readonly value: number
  8. }
  9. const bench = createHarness()
  10. const results: Array<{ readonly name: string; readonly ratio: number }> = []
  11. function initial(size: number) {
  12. return Array.from({ length: size }, (_, id): Item => ({ id, value: 0 }))
  13. }
  14. function project(values: readonly Item[]) {
  15. return values.reduce((total, value) => total + value.id + value.value, 0)
  16. }
  17. function quarkDirect(size: number, aggregate: boolean): Workload {
  18. const values = initial(size)
  19. const keyed = Keyed.make<Item, number>({
  20. key: (item) => item.id,
  21. equivalent: (left, right) => left.value === right.value,
  22. })
  23. keyed.set(values)
  24. const target = keyed.slots()[Math.floor(size / 2)]
  25. let sink = aggregate ? project(keyed.values()) : target().value
  26. const dispose = aggregate
  27. ? keyed.values.subscribe((next) => (sink = project(next)))
  28. : target.subscribe((value) => (sink = value.value))
  29. return {
  30. run: (index) => keyed.update({ id: Math.floor(size / 2), value: index + 1 }),
  31. consume: () => sink,
  32. dispose,
  33. }
  34. }
  35. function solidDirect(size: number, aggregate: boolean): Workload {
  36. let run = (_index: number) => {}
  37. let consume = () => 0
  38. let dispose = () => {}
  39. createRoot((rootDispose) => {
  40. dispose = rootDispose
  41. const [values, setValues] = createStore(initial(size))
  42. const target = Math.floor(size / 2)
  43. let sink = aggregate ? project(values) : values[target].value
  44. if (aggregate) createComputed(() => (sink = project(values)))
  45. else createComputed(() => (sink = values[target].value))
  46. run = (index) => setValues(target, reconcile({ id: target, value: index + 1 }))
  47. consume = () => sink
  48. })
  49. return { run, consume, dispose }
  50. }
  51. function quarkNoSubscriber(size: number): Workload {
  52. const keyed = Keyed.make<Item, number>({
  53. key: (item) => item.id,
  54. equivalent: (left, right) => left.value === right.value,
  55. })
  56. keyed.set(initial(size))
  57. const target = Math.floor(size / 2)
  58. return {
  59. run: (index) => keyed.update({ id: target, value: index + 1 }),
  60. consume: () => keyed.slots()[target]().value,
  61. }
  62. }
  63. function solidNoSubscriber(size: number): Workload {
  64. const [values, setValues] = createStore(initial(size))
  65. const target = Math.floor(size / 2)
  66. return {
  67. run: (index) => setValues(target, reconcile({ id: target, value: index + 1 })),
  68. consume: () => values[target].value,
  69. }
  70. }
  71. function solidPathWriteNoSubscriber(size: number): Workload {
  72. const [values, setValues] = createStore(initial(size))
  73. const target = Math.floor(size / 2)
  74. return {
  75. run: (index) => setValues(target, "value", index + 1),
  76. consume: () => values[target].value,
  77. }
  78. }
  79. function quarkDense(size: number): Workload {
  80. const keyed = Keyed.make<Item, number>({
  81. key: (item) => item.id,
  82. equivalent: (left, right) => left.value === right.value,
  83. })
  84. keyed.set(initial(size))
  85. let sink = project(keyed.values())
  86. const dispose = keyed.values.subscribe((values) => (sink = project(values)))
  87. return {
  88. run: (index) => keyed.set(initial(size).map((item) => ({ ...item, value: index + 1 }))),
  89. consume: () => sink,
  90. dispose,
  91. }
  92. }
  93. function solidDense(size: number): Workload {
  94. let run = (_index: number) => {}
  95. let consume = () => 0
  96. let dispose = () => {}
  97. createRoot((rootDispose) => {
  98. dispose = rootDispose
  99. const [values, setValues] = createStore(initial(size))
  100. let sink = project(values)
  101. createComputed(() => (sink = project(values)))
  102. run = (index) => setValues(reconcile(initial(size).map((item) => ({ ...item, value: index + 1 }))))
  103. consume = () => sink
  104. })
  105. return { run, consume, dispose }
  106. }
  107. function quarkUnstable(size: number): Workload {
  108. const keyed = Keyed.make<Item, number>({ key: (item) => item.id })
  109. keyed.set(initial(size))
  110. let sink = project(keyed.values())
  111. const dispose = keyed.values.subscribe((values) => (sink = project(values)))
  112. return {
  113. run(index) {
  114. const offset = (index + 1) * size
  115. keyed.set(initial(size).map((item) => ({ id: item.id + offset, value: index })))
  116. },
  117. consume: () => sink,
  118. dispose,
  119. }
  120. }
  121. function solidUnstable(size: number): Workload {
  122. let run = (_index: number) => {}
  123. let consume = () => 0
  124. let dispose = () => {}
  125. createRoot((rootDispose) => {
  126. dispose = rootDispose
  127. const [values, setValues] = createStore(initial(size))
  128. let sink = project(values)
  129. createComputed(() => (sink = project(values)))
  130. run = (index) => {
  131. const offset = (index + 1) * size
  132. setValues(reconcile(initial(size).map((item) => ({ id: item.id + offset, value: index }))))
  133. }
  134. consume = () => sink
  135. })
  136. return { run, consume, dispose }
  137. }
  138. function compare(name: string, iterations: number, quark: () => Workload, solid: () => Workload) {
  139. console.log(`\n${name}`)
  140. const result = bench.compare(iterations, [
  141. { name: `Quark ${name}`, make: quark },
  142. { name: `Solid ${name}`, make: solid },
  143. ])
  144. results.push({ name, ratio: result.ratio(0, 1) })
  145. }
  146. console.log(`Keyed integration benchmark (${bench.samples} samples)`)
  147. compare(
  148. "direct no subscribers 1000",
  149. 200_000,
  150. () => quarkNoSubscriber(1_000),
  151. () => solidNoSubscriber(1_000),
  152. )
  153. compare(
  154. "adversarial direct path write 1000",
  155. 200_000,
  156. () => quarkNoSubscriber(1_000),
  157. () => solidPathWriteNoSubscriber(1_000),
  158. )
  159. compare(
  160. "subscribed values 10",
  161. 100_000,
  162. () => quarkDirect(10, true),
  163. () => solidDirect(10, true),
  164. )
  165. compare(
  166. "subscribed values 100",
  167. 25_000,
  168. () => quarkDirect(100, true),
  169. () => solidDirect(100, true),
  170. )
  171. compare(
  172. "subscribed values 1000",
  173. 2_500,
  174. () => quarkDirect(1_000, true),
  175. () => solidDirect(1_000, true),
  176. )
  177. compare(
  178. "subscribed values 10000",
  179. 250,
  180. () => quarkDirect(10_000, true),
  181. () => solidDirect(10_000, true),
  182. )
  183. compare(
  184. "dense update 1000",
  185. 250,
  186. () => quarkDense(1_000),
  187. () => solidDense(1_000),
  188. )
  189. compare(
  190. "unstable keys 100",
  191. 1_000,
  192. () => quarkUnstable(100),
  193. () => solidUnstable(100),
  194. )
  195. console.log("\nRatios to Solid (lower is faster)")
  196. results.forEach((result) => {
  197. console.log(`${result.name.padEnd(34)} ${result.ratio.toFixed(3)}x`)
  198. console.log(`METRIC ${result.name.replaceAll(/[^a-z0-9]+/g, "_")}_ratio=${result.ratio.toFixed(6)}`)
  199. })
  200. bench.finish()