bench_test.go 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978
  1. // Copyright 2022 gorse Project Authors
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. package server
  15. import (
  16. "context"
  17. "database/sql"
  18. "encoding/json"
  19. "fmt"
  20. "math/rand"
  21. "net"
  22. "net/http"
  23. "os"
  24. "runtime"
  25. "strconv"
  26. "strings"
  27. "testing"
  28. "time"
  29. "github.com/emicklei/go-restful/v3"
  30. "github.com/go-resty/resty/v2"
  31. "github.com/redis/go-redis/v9"
  32. "github.com/samber/lo"
  33. "github.com/stretchr/testify/require"
  34. "github.com/zhenghaoz/gorse/base/log"
  35. "github.com/zhenghaoz/gorse/config"
  36. "github.com/zhenghaoz/gorse/storage/cache"
  37. "github.com/zhenghaoz/gorse/storage/data"
  38. "go.mongodb.org/mongo-driver/mongo"
  39. "go.mongodb.org/mongo-driver/mongo/options"
  40. "google.golang.org/protobuf/proto"
  41. )
  42. const (
  43. numInitUsers = 10000
  44. numInitItems = 10000
  45. )
  46. var (
  47. benchDataStore string
  48. benchCacheStore string
  49. )
  50. func init() {
  51. // get environment variables
  52. env := func(key, defaultValue string) string {
  53. if value := os.Getenv(key); value != "" {
  54. return value
  55. }
  56. return defaultValue
  57. }
  58. benchDataStore = env("BENCH_DATA_STORE", "mysql://root:password@tcp(127.0.0.1:3306)/")
  59. benchCacheStore = env("BENCH_CACHE_STORE", "redis://127.0.0.1:6379/")
  60. }
  61. type benchServer struct {
  62. listener net.Listener
  63. address string
  64. RestServer
  65. }
  66. func newBenchServer(b *testing.B) *benchServer {
  67. ctx := context.Background()
  68. // retrieve benchmark name
  69. var benchName string
  70. pc, _, _, ok := runtime.Caller(1)
  71. details := runtime.FuncForPC(pc)
  72. if ok && details != nil {
  73. splits := strings.Split(details.Name(), ".")
  74. benchName = splits[len(splits)-1]
  75. } else {
  76. b.Fatalf("failed to retrieve benchmark name")
  77. }
  78. // configuration
  79. s := &benchServer{}
  80. s.Settings = &config.Settings{}
  81. s.Config = config.GetDefaultConfig()
  82. s.DisableLog = true
  83. s.WebService = new(restful.WebService)
  84. cacheStoreURL := s.prepareCache(b, benchCacheStore, benchName)
  85. dataStoreURL := s.prepareData(b, benchDataStore, benchName)
  86. s.CreateWebService()
  87. container := restful.NewContainer()
  88. container.Add(s.WebService)
  89. // open database
  90. var err error
  91. s.DataClient, err = data.Open(dataStoreURL, "")
  92. require.NoError(b, err)
  93. err = s.DataClient.Init()
  94. require.NoError(b, err)
  95. s.CacheClient, err = cache.Open(cacheStoreURL, "")
  96. require.NoError(b, err)
  97. err = s.CacheClient.Init()
  98. require.NoError(b, err)
  99. // insert users
  100. users := make([]data.User, 0)
  101. for i := 0; i < numInitUsers; i++ {
  102. users = append(users, data.User{
  103. UserId: fmt.Sprintf("init_user_%d", i),
  104. Labels: []string{
  105. fmt.Sprintf("label_%d", i),
  106. fmt.Sprintf("label_%d", i*2),
  107. },
  108. Comment: fmt.Sprintf("comment_%d", i),
  109. })
  110. }
  111. err = s.DataClient.BatchInsertUsers(ctx, users)
  112. require.NoError(b, err)
  113. // insert items
  114. items := make([]data.Item, 0)
  115. for i := 0; i < numInitItems; i++ {
  116. items = append(items, data.Item{
  117. ItemId: fmt.Sprintf("init_item_%d", i),
  118. Labels: []string{
  119. fmt.Sprintf("label%d001", i),
  120. fmt.Sprintf("label%d002", i),
  121. },
  122. Comment: fmt.Sprintf("add label for user: demo%d", i),
  123. Categories: []string{
  124. fmt.Sprintf("category%d001", i),
  125. fmt.Sprintf("category%d001", i),
  126. },
  127. Timestamp: time.Now(),
  128. IsHidden: false,
  129. })
  130. }
  131. err = s.DataClient.BatchInsertItems(ctx, items)
  132. require.NoError(b, err)
  133. // insert feedback
  134. feedbacks := make([]data.Feedback, 0)
  135. for userIndex := 0; userIndex < numInitUsers; userIndex++ {
  136. itemIndex := userIndex % numInitItems
  137. feedbacks = append(feedbacks, data.Feedback{
  138. FeedbackKey: data.FeedbackKey{
  139. FeedbackType: "feedback_type",
  140. ItemId: fmt.Sprintf("init_item_%d", userIndex),
  141. UserId: fmt.Sprintf("init_user_%d", itemIndex),
  142. },
  143. Timestamp: time.Now(),
  144. })
  145. }
  146. err = s.DataClient.BatchInsertFeedback(ctx, feedbacks, true, true, true)
  147. require.NoError(b, err)
  148. // start http server
  149. s.listener, err = net.Listen("tcp", ":0")
  150. require.NoError(b, err)
  151. s.address = fmt.Sprintf("http://127.0.0.1:%d", s.listener.Addr().(*net.TCPAddr).Port)
  152. go func() {
  153. err = http.Serve(s.listener, container)
  154. require.NoError(b, err)
  155. }()
  156. return s
  157. }
  158. func (s *benchServer) prepareData(b *testing.B, url, benchName string) string {
  159. dbName := "gorse_data_" + benchName
  160. if strings.HasPrefix(url, "mysql://") {
  161. db, err := sql.Open("mysql", url[len("mysql://"):])
  162. require.NoError(b, err)
  163. _, err = db.Exec("DROP DATABASE IF EXISTS " + dbName)
  164. require.NoError(b, err)
  165. _, err = db.Exec("CREATE DATABASE " + dbName)
  166. require.NoError(b, err)
  167. err = db.Close()
  168. require.NoError(b, err)
  169. return url + dbName + "?timeout=30s&parseTime=true"
  170. } else if strings.HasPrefix(url, "postgres://") {
  171. db, err := sql.Open("postgres", url+"?sslmode=disable&TimeZone=UTC")
  172. require.NoError(b, err)
  173. _, err = db.Exec("DROP DATABASE IF EXISTS " + dbName)
  174. require.NoError(b, err)
  175. _, err = db.Exec("CREATE DATABASE " + dbName)
  176. require.NoError(b, err)
  177. err = db.Close()
  178. require.NoError(b, err)
  179. return url + strings.ToLower(dbName) + "?sslmode=disable&TimeZone=UTC"
  180. } else if strings.HasPrefix(url, "mongodb://") {
  181. ctx := context.Background()
  182. cli, err := mongo.Connect(ctx, options.Client().ApplyURI(url+"?authSource=admin&connect=direct"))
  183. require.NoError(b, err)
  184. err = cli.Database(dbName).Drop(ctx)
  185. require.NoError(b, err)
  186. err = cli.Disconnect(ctx)
  187. require.NoError(b, err)
  188. return url + dbName + "?authSource=admin&connect=direct"
  189. } else {
  190. b.Fatal("unsupported data store type")
  191. return ""
  192. }
  193. }
  194. func (s *benchServer) prepareCache(b *testing.B, url, benchName string) string {
  195. dbName := "gorse_cache_" + benchName
  196. if strings.HasPrefix(url, "redis://") {
  197. opt, err := redis.ParseURL(url)
  198. require.NoError(b, err)
  199. cli := redis.NewClient(opt)
  200. require.NoError(b, cli.FlushDB(context.Background()).Err())
  201. return url
  202. } else if strings.HasPrefix(url, "mongodb://") {
  203. ctx := context.Background()
  204. cli, err := mongo.Connect(ctx, options.Client().ApplyURI(url+"?authSource=admin&connect=direct"))
  205. require.NoError(b, err)
  206. err = cli.Database(dbName).Drop(ctx)
  207. require.NoError(b, err)
  208. err = cli.Disconnect(ctx)
  209. require.NoError(b, err)
  210. return url + dbName + "?authSource=admin&connect=direct"
  211. } else if strings.HasPrefix(url, "postgres://") {
  212. db, err := sql.Open("postgres", url+"?sslmode=disable&TimeZone=UTC")
  213. require.NoError(b, err)
  214. _, err = db.Exec("DROP DATABASE IF EXISTS " + dbName)
  215. require.NoError(b, err)
  216. _, err = db.Exec("CREATE DATABASE " + dbName)
  217. require.NoError(b, err)
  218. err = db.Close()
  219. require.NoError(b, err)
  220. return url + strings.ToLower(dbName) + "?sslmode=disable&TimeZone=UTC"
  221. } else if strings.HasPrefix(url, "mysql://") {
  222. db, err := sql.Open("mysql", url[len("mysql://"):]+"?timeout=30s&parseTime=true")
  223. require.NoError(b, err)
  224. _, err = db.Exec("DROP DATABASE IF EXISTS " + dbName)
  225. require.NoError(b, err)
  226. _, err = db.Exec("CREATE DATABASE " + dbName)
  227. require.NoError(b, err)
  228. err = db.Close()
  229. require.NoError(b, err)
  230. return url + dbName + "?timeout=30s&parseTime=true"
  231. } else {
  232. b.Fatal("unsupported cache store type")
  233. return ""
  234. }
  235. }
  236. func (s *benchServer) Close(b *testing.B) {
  237. err := s.DataClient.Close()
  238. require.NoError(b, err)
  239. err = s.CacheClient.Close()
  240. require.NoError(b, err)
  241. }
  242. func BenchmarkInsertUser(b *testing.B) {
  243. s := newBenchServer(b)
  244. defer s.Close(b)
  245. client := resty.New()
  246. b.ResetTimer()
  247. for i := 0; i < b.N; i++ {
  248. r, err := client.R().
  249. SetBody(&data.User{
  250. UserId: fmt.Sprintf("user_%d", i),
  251. Labels: []string{
  252. fmt.Sprintf("label_%d", i*2+0),
  253. fmt.Sprintf("label_%d", i*2+1),
  254. },
  255. Comment: fmt.Sprintf("comment_%d", i),
  256. }).
  257. Post(s.address + "/api/user")
  258. require.NoError(b, err)
  259. require.Equal(b, http.StatusOK, r.StatusCode(), r.String())
  260. }
  261. b.StopTimer()
  262. }
  263. func BenchmarkPatchUser(b *testing.B) {
  264. s := newBenchServer(b)
  265. defer s.Close(b)
  266. client := resty.New()
  267. b.ResetTimer()
  268. for i := 0; i < b.N; i++ {
  269. userIndex := i % numInitUsers
  270. times := i / numInitUsers
  271. r, err := client.R().
  272. SetBody(&data.UserPatch{
  273. Labels: []string{
  274. fmt.Sprintf("label_%d_%d", userIndex*2+0, times),
  275. fmt.Sprintf("label_%d_%d", userIndex*2+1, times),
  276. },
  277. Comment: proto.String(fmt.Sprintf("comment_%d_%d", userIndex, times)),
  278. }).
  279. Patch(s.address + "/api/user/init_user_" + strconv.Itoa(userIndex))
  280. require.NoError(b, err)
  281. require.Equal(b, http.StatusOK, r.StatusCode(), r.String())
  282. }
  283. b.StopTimer()
  284. }
  285. func BenchmarkGetUser(b *testing.B) {
  286. s := newBenchServer(b)
  287. defer s.Close(b)
  288. response := make([]*resty.Response, b.N)
  289. client := resty.New()
  290. b.ResetTimer()
  291. for i := 0; i < b.N; i++ {
  292. var err error
  293. response[i], err = client.R().
  294. Get(s.address + "/api/user/init_user_" + strconv.Itoa(i%numInitUsers))
  295. require.NoError(b, err)
  296. }
  297. b.StopTimer()
  298. for i, r := range response {
  299. require.Equal(b, http.StatusOK, r.StatusCode())
  300. var ret data.User
  301. err := json.Unmarshal(r.Body(), &ret)
  302. require.NoError(b, err)
  303. require.Equal(b, "init_user_"+strconv.Itoa(i%numInitUsers), ret.UserId)
  304. }
  305. }
  306. func BenchmarkInsertUsers(b *testing.B) {
  307. s := newBenchServer(b)
  308. defer s.Close(b)
  309. for batchSize := 10; batchSize <= 1000; batchSize *= 10 {
  310. b.Run(strconv.Itoa(batchSize), func(b *testing.B) {
  311. users := make([][]data.User, b.N)
  312. for i := 0; i < b.N; i++ {
  313. users[i] = make([]data.User, batchSize)
  314. for j := range users[i] {
  315. userId := fmt.Sprintf("batch_%d_user_%d", i, j)
  316. users[i][j] = data.User{
  317. UserId: fmt.Sprintf("batch_%d_user_%d", i, j),
  318. Labels: []string{
  319. fmt.Sprintf("label_%s_0", userId),
  320. fmt.Sprintf("label_%s_1", userId),
  321. },
  322. Comment: fmt.Sprintf("comment_%s", userId),
  323. }
  324. }
  325. }
  326. client := resty.New()
  327. b.ResetTimer()
  328. for i := 0; i < b.N; i++ {
  329. r, err := client.R().
  330. SetBody(users[i]).
  331. Post(s.address + "/api/users")
  332. require.NoError(b, err)
  333. require.Equal(b, http.StatusOK, r.StatusCode(), r.String())
  334. }
  335. b.StopTimer()
  336. })
  337. }
  338. }
  339. func BenchmarkGetUsers(b *testing.B) {
  340. s := newBenchServer(b)
  341. defer s.Close(b)
  342. client := resty.New()
  343. for batchSize := 10; batchSize <= 1000; batchSize *= 10 {
  344. b.Run(strconv.Itoa(batchSize), func(b *testing.B) {
  345. response := make([]*resty.Response, b.N)
  346. b.ResetTimer()
  347. for i := 0; i < b.N; i++ {
  348. var err error
  349. response[i], err = client.R().
  350. Get(s.address + fmt.Sprintf("/api/users?n=%d", batchSize))
  351. require.NoError(b, err)
  352. }
  353. b.StopTimer()
  354. for _, r := range response {
  355. require.Equal(b, http.StatusOK, r.StatusCode())
  356. var ret UserIterator
  357. err := json.Unmarshal(r.Body(), &ret)
  358. require.NoError(b, err)
  359. require.Equal(b, batchSize, len(ret.Users))
  360. }
  361. })
  362. }
  363. }
  364. func BenchmarkDeleteUser(b *testing.B) {
  365. s := newBenchServer(b)
  366. defer s.Close(b)
  367. client := resty.New()
  368. b.ResetTimer()
  369. for i := 0; i < b.N; i++ {
  370. r, err := client.R().
  371. Delete(s.address + "/api/user/init_user_" + strconv.Itoa(i%numInitUsers))
  372. require.NoError(b, err)
  373. require.Equal(b, http.StatusOK, r.StatusCode(), r.String())
  374. }
  375. b.StopTimer()
  376. }
  377. func BenchmarkInsertItem(b *testing.B) {
  378. s := newBenchServer(b)
  379. defer s.Close(b)
  380. client := resty.New()
  381. b.ResetTimer()
  382. for i := 0; i < b.N; i++ {
  383. r, err := client.R().
  384. SetBody(&data.Item{
  385. ItemId: fmt.Sprintf("item_%v", i),
  386. Labels: []string{
  387. fmt.Sprintf("label_%d", i*2+0),
  388. fmt.Sprintf("label_%d", i*2+1),
  389. },
  390. Categories: []string{
  391. fmt.Sprintf("category_%d", i*2+0),
  392. fmt.Sprintf("category_%d", i*2+1),
  393. },
  394. IsHidden: i%2 == 0,
  395. Timestamp: time.Now(),
  396. Comment: fmt.Sprintf("comment_%d", i),
  397. }).
  398. Post(s.address + "/api/item")
  399. require.NoError(b, err)
  400. require.Equal(b, http.StatusOK, r.StatusCode(), r.String())
  401. }
  402. b.StopTimer()
  403. }
  404. func BenchmarkPatchItem(b *testing.B) {
  405. s := newBenchServer(b)
  406. defer s.Close(b)
  407. client := resty.New()
  408. b.ResetTimer()
  409. for i := 0; i < b.N; i++ {
  410. itemIndex := i % numInitItems
  411. times := i / numInitItems
  412. isHidden := false
  413. if i%2 == 0 {
  414. isHidden = true
  415. }
  416. now := time.Now()
  417. r, err := client.R().
  418. SetBody(&data.ItemPatch{
  419. Labels: []string{
  420. fmt.Sprintf("label_%d_%d", itemIndex*2+0, times),
  421. fmt.Sprintf("label_%d_%d", itemIndex*2+1, times),
  422. },
  423. Comment: proto.String(fmt.Sprintf("modified_comment_%d", i)),
  424. Categories: []string{
  425. fmt.Sprintf("category_%d_%d", i*2+0, times),
  426. fmt.Sprintf("category_%d_%d", i*2+1, times),
  427. },
  428. IsHidden: &isHidden,
  429. Timestamp: &now,
  430. }).
  431. Patch(s.address + "/api/item/init_item_" + strconv.Itoa(itemIndex))
  432. require.NoError(b, err)
  433. require.Equal(b, http.StatusOK, r.StatusCode(), r.String())
  434. }
  435. b.StopTimer()
  436. }
  437. func BenchmarkGetItem(b *testing.B) {
  438. s := newBenchServer(b)
  439. defer s.Close(b)
  440. response := make([]*resty.Response, b.N)
  441. client := resty.New()
  442. b.ResetTimer()
  443. for i := 0; i < b.N; i++ {
  444. var err error
  445. response[i], err = client.R().
  446. Get(s.address + "/api/item/init_item_" + strconv.Itoa(i%numInitItems))
  447. require.NoError(b, err)
  448. }
  449. b.StopTimer()
  450. for i, r := range response {
  451. require.Equal(b, http.StatusOK, r.StatusCode())
  452. var ret data.Item
  453. err := json.Unmarshal(r.Body(), &ret)
  454. require.NoError(b, err)
  455. require.Equal(b, "init_item_"+strconv.Itoa(i%numInitItems), ret.ItemId)
  456. }
  457. }
  458. func BenchmarkInsertItems(b *testing.B) {
  459. s := newBenchServer(b)
  460. defer s.Close(b)
  461. client := resty.New()
  462. for batchSize := 10; batchSize <= 1000; batchSize *= 10 {
  463. b.Run(strconv.Itoa(batchSize), func(b *testing.B) {
  464. b.ResetTimer()
  465. for i := 0; i < b.N; i++ {
  466. items := make([]data.Item, 0)
  467. for j := 0; j < batchSize; j++ {
  468. itemId := fmt.Sprintf("batch_%d_item_%d", i, j)
  469. items = append(items, data.Item{
  470. ItemId: itemId,
  471. Labels: []string{
  472. fmt.Sprintf("label_%d", j*2+0),
  473. fmt.Sprintf("label_%d", j*2+1),
  474. },
  475. Comment: fmt.Sprintf("comment_%d", j),
  476. })
  477. }
  478. r, err := client.R().
  479. SetBody(items).
  480. Post(s.address + "/api/items")
  481. require.NoError(b, err)
  482. require.Equal(b, http.StatusOK, r.StatusCode(), r.String())
  483. }
  484. b.StopTimer()
  485. })
  486. }
  487. }
  488. func BenchmarkGetItems(b *testing.B) {
  489. s := newBenchServer(b)
  490. defer s.Close(b)
  491. client := resty.New()
  492. for batchSize := 10; batchSize <= 1000; batchSize *= 10 {
  493. b.Run(strconv.Itoa(batchSize), func(b *testing.B) {
  494. response := make([]*resty.Response, b.N)
  495. b.ResetTimer()
  496. for i := 0; i < b.N; i++ {
  497. var err error
  498. response[i], err = client.R().
  499. Get(s.address + fmt.Sprintf("/api/items?n=%d", batchSize))
  500. require.NoError(b, err)
  501. }
  502. b.StopTimer()
  503. for _, r := range response {
  504. require.Equal(b, http.StatusOK, r.StatusCode())
  505. var ret ItemIterator
  506. err := json.Unmarshal(r.Body(), &ret)
  507. require.NoError(b, err)
  508. require.Equal(b, batchSize, len(ret.Items))
  509. }
  510. })
  511. }
  512. }
  513. func BenchmarkDeleteItem(b *testing.B) {
  514. s := newBenchServer(b)
  515. defer s.Close(b)
  516. client := resty.New()
  517. b.ResetTimer()
  518. for i := 0; i < b.N; i++ {
  519. r, err := client.R().
  520. Delete(s.address + "/api/item/init_item_" + strconv.Itoa(i%numInitItems))
  521. require.NoError(b, err)
  522. require.Equal(b, http.StatusOK, r.StatusCode(), r.String())
  523. }
  524. b.StopTimer()
  525. }
  526. func BenchmarkInsertCategory(b *testing.B) {
  527. s := newBenchServer(b)
  528. defer s.Close(b)
  529. client := resty.New()
  530. b.ResetTimer()
  531. for i := 0; i < b.N; i++ {
  532. itemIndex := i % numInitItems
  533. r, err := client.R().
  534. Put(fmt.Sprintf("%s/api/item/init_item_%d/category/category%d", s.address, itemIndex, i))
  535. require.NoError(b, err)
  536. require.Equal(b, http.StatusOK, r.StatusCode(), r.String())
  537. }
  538. b.StopTimer()
  539. }
  540. func BenchmarkDeleteCategory(b *testing.B) {
  541. s := newBenchServer(b)
  542. defer s.Close(b)
  543. client := resty.New()
  544. b.ResetTimer()
  545. for i := 0; i < b.N; i++ {
  546. itemIndex := i % numInitItems
  547. r, err := client.R().
  548. Delete(fmt.Sprintf("%s/api/item/init_item_%d/category/category%d", s.address, itemIndex, i))
  549. require.NoError(b, err)
  550. require.Equal(b, http.StatusOK, r.StatusCode(), r.String())
  551. }
  552. b.StopTimer()
  553. }
  554. func BenchmarkPutFeedback(b *testing.B) {
  555. s := newBenchServer(b)
  556. defer s.Close(b)
  557. client := resty.New()
  558. for batchSize := 10; batchSize <= 1000; batchSize *= 10 {
  559. b.Run(strconv.Itoa(batchSize), func(b *testing.B) {
  560. b.ResetTimer()
  561. for i := 0; i < b.N; i++ {
  562. feedbacks := make([]data.Feedback, 0, batchSize)
  563. for j := 0; j < batchSize; j++ {
  564. feedbacks = append(feedbacks, data.Feedback{
  565. FeedbackKey: data.FeedbackKey{
  566. FeedbackType: fmt.Sprintf("feedback_type_%d", batchSize),
  567. ItemId: fmt.Sprintf("init_item_%d", rand.Intn(numInitItems)),
  568. UserId: fmt.Sprintf("init_user_%d", rand.Intn(numInitUsers)),
  569. },
  570. Timestamp: time.Now(),
  571. })
  572. }
  573. r, err := client.R().
  574. SetBody(feedbacks).
  575. Put(s.address + "/api/feedback")
  576. require.NoError(b, err)
  577. require.Equal(b, http.StatusOK, r.StatusCode(), r.String())
  578. }
  579. b.StopTimer()
  580. })
  581. }
  582. }
  583. func BenchmarkInsertFeedback(b *testing.B) {
  584. s := newBenchServer(b)
  585. defer s.Close(b)
  586. client := resty.New()
  587. for batchSize := 10; batchSize <= 1000; batchSize *= 10 {
  588. b.Run(strconv.Itoa(batchSize), func(b *testing.B) {
  589. b.ResetTimer()
  590. for i := 0; i < b.N; i++ {
  591. feedbacks := make([]data.Feedback, 0, batchSize)
  592. for j := 0; j < batchSize; j++ {
  593. feedbacks = append(feedbacks, data.Feedback{
  594. FeedbackKey: data.FeedbackKey{
  595. FeedbackType: fmt.Sprintf("feedback_type_%d", batchSize),
  596. ItemId: fmt.Sprintf("init_item_%d", rand.Intn(numInitItems)),
  597. UserId: fmt.Sprintf("init_user_%d", rand.Intn(numInitUsers)),
  598. },
  599. Timestamp: time.Now(),
  600. })
  601. }
  602. r, err := client.R().
  603. SetBody(feedbacks).
  604. Post(s.address + "/api/feedback")
  605. require.NoError(b, err)
  606. require.Equal(b, http.StatusOK, r.StatusCode(), r.String())
  607. }
  608. b.StopTimer()
  609. })
  610. }
  611. }
  612. func BenchmarkGetFeedback(b *testing.B) {
  613. s := newBenchServer(b)
  614. defer s.Close(b)
  615. for batchSize := 10; batchSize <= 1000; batchSize *= 10 {
  616. b.Run(strconv.Itoa(batchSize), func(b *testing.B) {
  617. response := make([]*resty.Response, b.N)
  618. client := resty.New()
  619. b.ResetTimer()
  620. for i := 0; i < b.N; i++ {
  621. var err error
  622. response[i], err = client.R().
  623. Get(s.address + fmt.Sprintf("/api/feedback?n=%d", batchSize))
  624. require.NoError(b, err)
  625. }
  626. b.StopTimer()
  627. for _, r := range response {
  628. require.Equal(b, http.StatusOK, r.StatusCode())
  629. var it FeedbackIterator
  630. err := json.Unmarshal(r.Body(), &it)
  631. require.NoError(b, err)
  632. require.Equal(b, batchSize, len(it.Feedback))
  633. }
  634. })
  635. }
  636. }
  637. func BenchmarkGetUserItemFeedback(b *testing.B) {
  638. s := newBenchServer(b)
  639. defer s.Close(b)
  640. response := make([]*resty.Response, b.N)
  641. client := resty.New()
  642. b.ResetTimer()
  643. for i := 0; i < b.N; i++ {
  644. userIndex := i % numInitUsers
  645. itemIndex := i % numInitItems
  646. var err error
  647. response[i], err = client.R().
  648. Get(s.address + fmt.Sprintf("/api/feedback/init_user_%d/init_item_%d", userIndex, itemIndex))
  649. require.NoError(b, err)
  650. }
  651. b.StopTimer()
  652. for _, r := range response {
  653. require.Equal(b, http.StatusOK, r.StatusCode())
  654. var feedback []data.Feedback
  655. err := json.Unmarshal(r.Body(), &feedback)
  656. require.NoError(b, err)
  657. require.Equal(b, 1, len(feedback))
  658. }
  659. }
  660. func BenchmarkDeleteUserItemFeedback(b *testing.B) {
  661. s := newBenchServer(b)
  662. defer s.Close(b)
  663. client := resty.New()
  664. b.ResetTimer()
  665. for i := 0; i < b.N; i++ {
  666. userIndex := i % numInitUsers
  667. itemIndex := i % numInitItems
  668. r, err := client.R().
  669. Delete(s.address + fmt.Sprintf("/api/feedback/init_user_%d/init_item_%d", userIndex, itemIndex))
  670. require.NoError(b, err)
  671. require.Equal(b, http.StatusOK, r.StatusCode())
  672. }
  673. b.StopTimer()
  674. }
  675. func BenchmarkGetUserFeedback(b *testing.B) {
  676. s := newBenchServer(b)
  677. defer s.Close(b)
  678. response := make([]*resty.Response, b.N)
  679. client := resty.New()
  680. b.ResetTimer()
  681. for i := 0; i < b.N; i++ {
  682. var err error
  683. response[i], err = client.R().
  684. Get(s.address + fmt.Sprintf("/api/user/init_user_%d/feedback", i%numInitUsers))
  685. require.NoError(b, err)
  686. }
  687. b.StopTimer()
  688. for _, r := range response {
  689. require.Equal(b, http.StatusOK, r.StatusCode())
  690. var feedback []data.Feedback
  691. err := json.Unmarshal(r.Body(), &feedback)
  692. require.NoError(b, err)
  693. require.Equal(b, 1, len(feedback))
  694. }
  695. }
  696. func BenchmarkGetItemFeedback(b *testing.B) {
  697. s := newBenchServer(b)
  698. defer s.Close(b)
  699. response := make([]*resty.Response, b.N)
  700. client := resty.New()
  701. b.ResetTimer()
  702. for i := 0; i < b.N; i++ {
  703. var err error
  704. response[i], err = client.R().
  705. Get(s.address + fmt.Sprintf("/api/item/init_item_%d/feedback", i%numInitItems))
  706. require.NoError(b, err)
  707. }
  708. b.StopTimer()
  709. for _, r := range response {
  710. require.Equal(b, http.StatusOK, r.StatusCode())
  711. var feedback []data.Feedback
  712. err := json.Unmarshal(r.Body(), &feedback)
  713. require.NoError(b, err)
  714. require.GreaterOrEqual(b, 1, len(feedback))
  715. }
  716. }
  717. func BenchmarkGetRecommendCache(b *testing.B) {
  718. s := newBenchServer(b)
  719. defer s.Close(b)
  720. ctx := context.Background()
  721. for batchSize := 10; batchSize <= 1000; batchSize *= 10 {
  722. b.Run(strconv.Itoa(batchSize), func(b *testing.B) {
  723. documents := make([]cache.Document, batchSize)
  724. for i := range documents {
  725. documents[i].Id = strconv.Itoa(i)
  726. documents[i].Score = float64(i)
  727. documents[i].Categories = []string{""}
  728. }
  729. lo.Reverse(documents)
  730. err := s.CacheClient.AddDocuments(ctx, cache.PopularItems, "", documents)
  731. require.NoError(b, err)
  732. s.Config.Recommend.CacheSize = len(documents)
  733. response := make([]*resty.Response, b.N)
  734. client := resty.New()
  735. b.ResetTimer()
  736. for i := 0; i < b.N; i++ {
  737. response[i], err = client.R().Get(s.address + fmt.Sprintf("/api/popular?n=%d", batchSize))
  738. require.NoError(b, err)
  739. }
  740. b.StopTimer()
  741. for _, r := range response {
  742. require.Equal(b, http.StatusOK, r.StatusCode(), r.String())
  743. expected, err := json.Marshal(documents[:batchSize])
  744. require.NoError(b, err)
  745. require.JSONEq(b, string(expected), r.String())
  746. }
  747. })
  748. }
  749. }
  750. func BenchmarkRecommendFromOfflineCache(b *testing.B) {
  751. log.CloseLogger()
  752. s := newBenchServer(b)
  753. defer s.Close(b)
  754. ctx := context.Background()
  755. for batchSize := 10; batchSize <= 1000; batchSize *= 10 {
  756. b.Run(strconv.Itoa(batchSize), func(b *testing.B) {
  757. documents := make([]cache.Document, batchSize*2)
  758. expects := make([]string, batchSize)
  759. feedbacks := make([]data.Feedback, batchSize)
  760. for i := range documents {
  761. documents[i].Id = strconv.Itoa(i)
  762. documents[i].Score = float64(i)
  763. documents[i].Categories = []string{""}
  764. if i%2 == 0 {
  765. expects[i/2] = documents[i].Id
  766. } else {
  767. feedbacks[i/2].FeedbackType = "read"
  768. feedbacks[i/2].UserId = "init_user_1"
  769. feedbacks[i/2].ItemId = documents[i].Id
  770. }
  771. }
  772. lo.Reverse(documents)
  773. lo.Reverse(expects)
  774. err := s.CacheClient.AddDocuments(ctx, cache.OfflineRecommend, "init_user_1", documents)
  775. require.NoError(b, err)
  776. err = s.DataClient.BatchInsertFeedback(ctx, feedbacks, true, true, true)
  777. require.NoError(b, err)
  778. s.Config.Recommend.CacheSize = len(documents)
  779. response := make([]*resty.Response, b.N)
  780. client := resty.New()
  781. b.ResetTimer()
  782. for i := 0; i < b.N; i++ {
  783. response[i], err = client.R().Get(s.address + fmt.Sprintf("/api/recommend/init_user_1?n=%d", batchSize))
  784. require.NoError(b, err)
  785. }
  786. b.StopTimer()
  787. for _, r := range response {
  788. require.Equal(b, http.StatusOK, r.StatusCode(), r.String())
  789. var ret []string
  790. err = json.Unmarshal(r.Body(), &ret)
  791. require.NoError(b, err)
  792. require.Equal(b, expects[:batchSize], ret)
  793. }
  794. })
  795. }
  796. }
  797. func BenchmarkRecommendFromLatest(b *testing.B) {
  798. ctx := context.Background()
  799. log.CloseLogger()
  800. s := newBenchServer(b)
  801. defer s.Close(b)
  802. for batchSize := 10; batchSize <= 1000; batchSize *= 10 {
  803. b.Run(strconv.Itoa(batchSize), func(b *testing.B) {
  804. documents := make([]cache.Document, batchSize*2)
  805. expects := make([]string, batchSize)
  806. feedbacks := make([]data.Feedback, batchSize)
  807. for i := range documents {
  808. documents[i].Id = strconv.Itoa(i)
  809. documents[i].Score = float64(i)
  810. documents[i].Categories = []string{""}
  811. if i%2 == 0 {
  812. expects[i/2] = documents[i].Id
  813. } else {
  814. feedbacks[i/2].FeedbackType = "feedback_type"
  815. feedbacks[i/2].UserId = "init_user_1"
  816. feedbacks[i/2].ItemId = documents[i].Id
  817. }
  818. }
  819. lo.Reverse(documents)
  820. lo.Reverse(expects)
  821. err := s.CacheClient.AddDocuments(ctx, cache.LatestItems, "", documents)
  822. require.NoError(b, err)
  823. err = s.DataClient.BatchInsertFeedback(ctx, feedbacks, true, true, true)
  824. require.NoError(b, err)
  825. s.Config.Recommend.CacheSize = len(documents)
  826. response := make([]*resty.Response, b.N)
  827. client := resty.New()
  828. b.ResetTimer()
  829. for i := 0; i < b.N; i++ {
  830. response[i], err = client.R().Get(s.address + fmt.Sprintf("/api/recommend/init_user_1?n=%d", batchSize))
  831. require.NoError(b, err)
  832. }
  833. b.StopTimer()
  834. for _, r := range response {
  835. require.Equal(b, http.StatusOK, r.StatusCode(), r.String())
  836. var ret []string
  837. err = json.Unmarshal(r.Body(), &ret)
  838. require.NoError(b, err)
  839. require.Equal(b, expects[:batchSize], ret)
  840. }
  841. })
  842. }
  843. }
  844. func BenchmarkRecommendFromItemBased(b *testing.B) {
  845. ctx := context.Background()
  846. log.CloseLogger()
  847. s := newBenchServer(b)
  848. defer s.Close(b)
  849. for batchSize := 10; batchSize <= 1000; batchSize *= 10 {
  850. b.Run(strconv.Itoa(batchSize), func(b *testing.B) {
  851. // insert user feedbacks
  852. documents := make([]cache.Document, batchSize*2)
  853. for i := range documents {
  854. documents[i].Id = fmt.Sprintf("init_item_%d", i)
  855. documents[i].Score = float64(i)
  856. documents[i].Categories = []string{""}
  857. if i < s.Config.Recommend.Online.NumFeedbackFallbackItemBased {
  858. err := s.DataClient.BatchInsertFeedback(ctx, []data.Feedback{{
  859. FeedbackKey: data.FeedbackKey{
  860. FeedbackType: "feedback_type_positive",
  861. UserId: "init_user_1",
  862. ItemId: fmt.Sprintf("init_item_%d", i),
  863. },
  864. Timestamp: time.Now().Add(-time.Minute),
  865. }}, true, true, true)
  866. require.NoError(b, err)
  867. }
  868. }
  869. // insert user neighbors
  870. for i := 0; i < s.Config.Recommend.Online.NumFeedbackFallbackItemBased; i++ {
  871. err := s.CacheClient.AddDocuments(ctx, cache.ItemNeighbors, fmt.Sprintf("init_item_%d", i), documents)
  872. require.NoError(b, err)
  873. }
  874. s.Config.Recommend.CacheSize = len(documents)
  875. s.Config.Recommend.DataSource.PositiveFeedbackTypes = []string{"feedback_type_positive"}
  876. s.Config.Recommend.Online.FallbackRecommend = []string{"item_based"}
  877. response := make([]*resty.Response, b.N)
  878. client := resty.New()
  879. b.ResetTimer()
  880. for i := 0; i < b.N; i++ {
  881. var err error
  882. response[i], err = client.R().Get(s.address + fmt.Sprintf("/api/recommend/init_user_1?n=%d", batchSize))
  883. require.NoError(b, err)
  884. }
  885. b.StopTimer()
  886. for _, r := range response {
  887. require.Equal(b, http.StatusOK, r.StatusCode(), r.String())
  888. var ret []string
  889. err := json.Unmarshal(r.Body(), &ret)
  890. require.NoError(b, err)
  891. require.Equal(b, batchSize, len(ret))
  892. }
  893. })
  894. }
  895. }