benchmark.ts 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323
  1. import autocannon, { printResult } from 'autocannon'
  2. import { program } from 'commander'
  3. import { writeJson } from 'fs-extra/esm'
  4. import { Video, VideoPrivacy } from '@peertube/peertube-models'
  5. import {
  6. createMultipleServers,
  7. doubleFollow,
  8. killallServers,
  9. PeerTubeServer,
  10. setAccessTokensToServers
  11. } from '@peertube/peertube-server-commands'
  12. let servers: PeerTubeServer[]
  13. // First server
  14. let server: PeerTubeServer
  15. let video: Video
  16. let threadId: number
  17. program
  18. .option('-o, --outfile [outfile]', 'Outfile')
  19. .option('--grep [string]', 'Filter tests you want to execute')
  20. .description('Run API REST benchmark')
  21. .parse(process.argv)
  22. const options = program.opts()
  23. const outfile = options.outfile
  24. run()
  25. .catch(err => console.error(err))
  26. .finally(() => {
  27. if (servers) return killallServers(servers)
  28. })
  29. function buildAuthorizationHeader () {
  30. return {
  31. Authorization: 'Bearer ' + server.accessToken
  32. }
  33. }
  34. function buildAPHeader () {
  35. return {
  36. Accept: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
  37. }
  38. }
  39. function buildJSONHeader () {
  40. return {
  41. 'Content-Type': 'application/json'
  42. }
  43. }
  44. async function run () {
  45. console.log('Preparing server...')
  46. await prepare()
  47. const tests = [
  48. {
  49. title: 'AP - account peertube',
  50. path: '/accounts/peertube',
  51. headers: buildAPHeader(),
  52. expecter: (body, status) => {
  53. return status === 200 && body.startsWith('{"@context":')
  54. }
  55. },
  56. {
  57. title: 'AP - video',
  58. path: '/videos/watch/' + video.uuid,
  59. headers: buildAPHeader(),
  60. expecter: (body, status) => {
  61. return status === 200 && body.startsWith('{"@context":')
  62. }
  63. },
  64. {
  65. title: 'Misc - webfinger peertube',
  66. path: '/.well-known/webfinger?resource=acct:peertube@' + server.host,
  67. expecter: (body, status) => {
  68. return status === 200 && body.startsWith('{"subject":')
  69. }
  70. },
  71. {
  72. title: 'API - unread notifications',
  73. path: '/api/v1/users/me/notifications?start=0&count=0&unread=true',
  74. headers: buildAuthorizationHeader(),
  75. expecter: (_body, status) => {
  76. return status === 200
  77. }
  78. },
  79. {
  80. title: 'API - me',
  81. path: '/api/v1/users/me',
  82. headers: buildAuthorizationHeader(),
  83. expecter: (body, status) => {
  84. return status === 200 && body.startsWith('{"id":')
  85. }
  86. },
  87. {
  88. title: 'API - videos list',
  89. path: '/api/v1/videos',
  90. expecter: (body, status) => {
  91. return status === 200 && body.startsWith('{"total":10')
  92. }
  93. },
  94. {
  95. title: 'API - video get',
  96. path: '/api/v1/videos/' + video.uuid,
  97. expecter: (body, status) => {
  98. return status === 200 && body.startsWith('{"id":')
  99. }
  100. },
  101. {
  102. title: 'API - video captions',
  103. path: '/api/v1/videos/' + video.uuid + '/captions',
  104. expecter: (body, status) => {
  105. return status === 200 && body.startsWith('{"total":4')
  106. }
  107. },
  108. {
  109. title: 'API - video threads',
  110. path: '/api/v1/videos/' + video.uuid + '/comment-threads',
  111. expecter: (body, status) => {
  112. return status === 200 && body.startsWith('{"total":10')
  113. }
  114. },
  115. {
  116. title: 'API - video replies',
  117. path: '/api/v1/videos/' + video.uuid + '/comment-threads/' + threadId,
  118. expecter: (body, status) => {
  119. return status === 200 && body.startsWith('{"comment":{')
  120. }
  121. },
  122. {
  123. title: 'HTML - video watch',
  124. path: '/videos/watch/' + video.uuid,
  125. expecter: (body, status) => {
  126. return status === 200 && body.includes('<title>my super')
  127. }
  128. },
  129. {
  130. title: 'HTML - video embed',
  131. path: '/videos/embed/' + video.uuid,
  132. expecter: (body, status) => {
  133. return status === 200 && body.includes('embed')
  134. }
  135. },
  136. {
  137. title: 'HTML - homepage',
  138. path: '/',
  139. expecter: (_body, status) => {
  140. return status === 200
  141. }
  142. },
  143. {
  144. title: 'API - config',
  145. path: '/api/v1/config',
  146. expecter: (body, status) => {
  147. return status === 200 && body.startsWith('{"client":')
  148. }
  149. },
  150. {
  151. title: 'API - views with token',
  152. method: 'PUT',
  153. headers: {
  154. ...buildAuthorizationHeader(),
  155. ...buildJSONHeader()
  156. },
  157. body: JSON.stringify({ currentTime: 2 }),
  158. path: '/api/v1/videos/' + video.uuid + '/views',
  159. expecter: (body, status) => {
  160. return status === 204
  161. }
  162. },
  163. {
  164. title: 'API - views without token',
  165. method: 'POST',
  166. headers: buildJSONHeader(),
  167. body: JSON.stringify({ currentTime: 2 }),
  168. path: '/api/v1/videos/' + video.uuid + '/views',
  169. expecter: (body, status) => {
  170. return status === 204
  171. }
  172. }
  173. ].filter(t => {
  174. if (!options.grep) return true
  175. return t.title.includes(options.grep)
  176. })
  177. const finalResult: any[] = []
  178. for (const test of tests) {
  179. console.log('Running against %s.', test.path)
  180. const testResult = await runBenchmark(test)
  181. Object.assign(testResult, { title: test.title, path: test.path })
  182. finalResult.push(testResult)
  183. console.log(printResult(testResult))
  184. }
  185. if (outfile) await writeJson(outfile, finalResult)
  186. }
  187. function runBenchmark (options: {
  188. path: string
  189. method?: string
  190. body?: string
  191. headers?: { [ id: string ]: string }
  192. expecter: Function
  193. }) {
  194. const { method = 'GET', path, body, expecter, headers } = options
  195. return new Promise((res, rej) => {
  196. autocannon({
  197. url: server.url + path,
  198. method,
  199. body,
  200. connections: 20,
  201. headers,
  202. pipelining: 1,
  203. duration: 10,
  204. requests: [
  205. {
  206. onResponse: (status, body) => {
  207. if (expecter(body, status) !== true) {
  208. console.error('Expected result failed.', { body, status })
  209. throw new Error('Invalid expectation')
  210. }
  211. }
  212. }
  213. ]
  214. }, (err, result) => {
  215. if (err) return rej(err)
  216. return res(result)
  217. })
  218. })
  219. }
  220. async function prepare () {
  221. servers = await createMultipleServers(3, {
  222. rates_limit: {
  223. api: {
  224. max: 5_000_000
  225. },
  226. login: {
  227. max: 5_000_000
  228. },
  229. signup: {
  230. max: 5_000_000
  231. },
  232. ask_send_email: {
  233. max: 5_000_000
  234. },
  235. receive_client_log: {
  236. max: 5_000_000
  237. },
  238. plugins: {
  239. max: 5_000_000
  240. },
  241. well_known: {
  242. max: 5_000_000
  243. },
  244. feeds: {
  245. max: 5_000_000
  246. },
  247. activity_pub: {
  248. max: 5_000_000
  249. },
  250. client: {
  251. max: 5_000_000
  252. }
  253. }
  254. }, { nodeArgs: [ '--inspect' ] })
  255. server = servers[0]
  256. await setAccessTokensToServers(servers)
  257. await doubleFollow(servers[0], servers[1])
  258. await doubleFollow(servers[0], servers[2])
  259. const attributes = {
  260. name: 'my super video',
  261. category: 2,
  262. nsfw: true,
  263. licence: 6,
  264. language: 'fr',
  265. privacy: VideoPrivacy.PUBLIC,
  266. support: 'please give me a coffee',
  267. description: 'my super description\n'.repeat(10) + ' * list1\n * list 2\n * list 3',
  268. tags: [ 'tag1', 'tag2', 'tag3' ]
  269. }
  270. for (let i = 0; i < 10; i++) {
  271. await server.videos.upload({ attributes: { ...attributes, name: 'my super video ' + i } })
  272. }
  273. const { data } = await server.videos.list()
  274. video = data.find(v => v.name === 'my super video 1')
  275. for (let i = 0; i < 10; i++) {
  276. const text = 'my super first comment'
  277. const created = await server.comments.createThread({ videoId: video.id, text })
  278. threadId = created.id
  279. const text1 = 'my super answer to thread 1'
  280. const child = await server.comments.addReply({ videoId: video.id, toCommentId: threadId, text: text1 })
  281. const text2 = 'my super answer to answer of thread 1'
  282. await server.comments.addReply({ videoId: video.id, toCommentId: child.id, text: text2 })
  283. const text3 = 'my second answer to thread 1'
  284. await server.comments.addReply({ videoId: video.id, toCommentId: threadId, text: text3 })
  285. }
  286. for (const caption of [ 'ar', 'fr', 'en', 'zh' ]) {
  287. await server.captions.add({
  288. language: caption,
  289. videoId: video.id,
  290. fixture: 'subtitle-good2.vtt'
  291. })
  292. }
  293. }