benchmark.ts 8.2 KB

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