1 /*
2 * See the file LICENSE for redistribution information.
3 *
4 * Copyright (c) 2005, 2013 Oracle and/or its affiliates. All rights reserved.
5 *
6 * $Id$
7 */
8
9 #include "bench.h"
10 #include "b_workload.h"
11
12 static int b_workload_dump_verbose_stats __P((DB *, CONFIG *));
13 static int b_workload_is_del_workload __P((int));
14 static int b_workload_is_get_workload __P((int));
15 static int b_workload_is_put_workload __P((int));
16 static int b_workload_run_mixed_workload __P((DB *, CONFIG *));
17 static int b_workload_run_std_workload __P((DB *, CONFIG *));
18 static int b_workload_usage __P((void));
19 static char *b_workload_workload_str __P((int));
20
21 /*
22 * General TODO list:
23 * * The workload type. Might work better as a bitmask than the current enum.
24 * * Improve the verbose stats, so they can be easily parsed.
25 * * Think about doing automatic btree/hash comparison in here.
26 */
27 int
b_workload(argc,argv)28 b_workload(argc, argv)
29 int argc;
30 char *argv[];
31 {
32 extern char *optarg;
33 extern int optind, __db_getopt_reset;
34 CONFIG conf;
35 DB *dbp;
36 DB_ENV *dbenv;
37 int ch, ffactor, ksz;
38
39 dbenv = NULL;
40 memset(&conf, 0, sizeof(conf));
41 conf.seed = 124087;
42 srand(conf.seed);
43
44 conf.pcount = 100000;
45 conf.ts = "Btree";
46 conf.type = DB_BTREE;
47 conf.dsize = 20;
48 conf.presize = 0;
49 conf.workload = T_PUT_GET_DELETE;
50
51 __db_getopt_reset = 1;
52 while ((ch = getopt(argc, argv, "b:c:d:e:g:ik:m:op:r:t:vw:")) != EOF)
53 switch (ch) {
54 case 'b':
55 conf.cachesz = atoi(optarg);
56 break;
57 case 'c':
58 conf.pcount = atoi(optarg);
59 break;
60 case 'd':
61 conf.dsize = atoi(optarg);
62 break;
63 case 'e':
64 conf.cursor_del = atoi(optarg);
65 break;
66 case 'g':
67 conf.gcount = atoi(optarg);
68 break;
69 case 'i':
70 conf.presize = 1;
71 break;
72 case 'k':
73 conf.ksize = atoi(optarg);
74 break;
75 case 'm':
76 conf.message = optarg;
77 break;
78 case 'o':
79 conf.orderedkeys = 1;
80 break;
81 case 'p':
82 conf.pagesz = atoi(optarg);
83 break;
84 case 'r':
85 conf.num_dups = atoi(optarg);
86 break;
87 case 't':
88 switch (optarg[0]) {
89 case 'B': case 'b':
90 conf.ts = "Btree";
91 conf.type = DB_BTREE;
92 break;
93 case 'H': case 'h':
94 if (b_util_have_hash())
95 return (0);
96 conf.ts = "Hash";
97 conf.type = DB_HASH;
98 break;
99 default:
100 return (b_workload_usage());
101 }
102 break;
103 case 'v':
104 conf.verbose = 1;
105 break;
106 case 'w':
107 switch (optarg[0]) {
108 case 'A':
109 conf.workload = T_PUT_GET_DELETE;
110 break;
111 case 'B':
112 conf.workload = T_GET;
113 break;
114 case 'C':
115 conf.workload = T_PUT;
116 break;
117 case 'D':
118 conf.workload = T_DELETE;
119 break;
120 case 'E':
121 conf.workload = T_PUT_GET;
122 break;
123 case 'F':
124 conf.workload = T_PUT_DELETE;
125 break;
126 case 'G':
127 conf.workload = T_GET_DELETE;
128 break;
129 case 'H':
130 conf.workload = T_MIXED;
131 break;
132 default:
133 return (b_workload_usage());
134 }
135 break;
136 case '?':
137 default:
138 fprintf(stderr, "Invalid option: %c\n", ch);
139 return (b_workload_usage());
140 }
141 argc -= optind;
142 argv += optind;
143 if (argc != 0)
144 return (b_workload_usage());
145
146 /*
147 * Validate the input parameters if specified.
148 */
149 if (conf.pagesz != 0)
150 DB_BENCH_ASSERT(conf.pagesz >= 512 && conf.pagesz <= 65536 &&
151 ((conf.pagesz & (conf.pagesz - 1)) == 0));
152
153 if (conf.cachesz != 0)
154 DB_BENCH_ASSERT(conf.cachesz > 20480);
155 DB_BENCH_ASSERT(conf.ksize == 0 || conf.orderedkeys == 0);
156
157 /* Create the environment. */
158 DB_BENCH_ASSERT(db_env_create(&dbenv, 0) == 0);
159 dbenv->set_errfile(dbenv, stderr);
160 if (conf.cachesz != 0)
161 DB_BENCH_ASSERT(
162 dbenv->set_cachesize(dbenv, 0, conf.cachesz, 0) == 0);
163
164 #if DB_VERSION_MAJOR == 3 && DB_VERSION_MINOR < 1
165 DB_BENCH_ASSERT(dbenv->open(dbenv, "TESTDIR",
166 NULL, DB_CREATE | DB_INIT_MPOOL | DB_PRIVATE, 0666) == 0);
167 #else
168 DB_BENCH_ASSERT(dbenv->open(dbenv, "TESTDIR",
169 DB_CREATE | DB_INIT_MPOOL | DB_PRIVATE, 0666) == 0);
170 #endif
171
172 DB_BENCH_ASSERT(db_create(&dbp, dbenv, 0) == 0);
173 if (conf.pagesz != 0)
174 DB_BENCH_ASSERT(
175 dbp->set_pagesize(dbp, conf.pagesz) == 0);
176 if (conf.presize != 0 && conf.type == DB_HASH) {
177 ksz = (conf.orderedkeys != 0) ? sizeof(u_int32_t) : conf.ksize;
178 if (ksz == 0)
179 ksz = 10;
180 ffactor = (conf.pagesz - 32)/(ksz + conf.dsize + 8);
181 fprintf(stderr, "ffactor: %d\n", ffactor);
182 DB_BENCH_ASSERT(
183 dbp->set_h_ffactor(dbp, ffactor) == 0);
184 DB_BENCH_ASSERT(
185 dbp->set_h_nelem(dbp, conf.pcount*10) == 0);
186 }
187 #if DB_VERSION_MAJOR > 4 || (DB_VERSION_MAJOR == 4 && DB_VERSION_MINOR >= 1)
188 DB_BENCH_ASSERT(dbp->open(
189 dbp, NULL, TESTFILE, NULL, conf.type, DB_CREATE, 0666) == 0);
190 #else
191 DB_BENCH_ASSERT(dbp->open(
192 dbp, TESTFILE, NULL, conf.type, DB_CREATE, 0666) == 0);
193 #endif
194
195 if (conf.workload == T_MIXED)
196 b_workload_run_mixed_workload(dbp, &conf);
197 else
198 b_workload_run_std_workload(dbp, &conf);
199
200 if (b_workload_is_put_workload(conf.workload) == 0)
201 timespecadd(&conf.tot_time, &conf.put_time);
202 if (b_workload_is_get_workload(conf.workload) == 0)
203 timespecadd(&conf.tot_time, &conf.get_time);
204 if (b_workload_is_del_workload(conf.workload) == 0)
205 timespecadd(&conf.tot_time, &conf.del_time);
206
207 /* Ensure data is flushed for following measurements. */
208 DB_BENCH_ASSERT(dbp->sync(dbp, 0) == 0);
209
210 if (conf.verbose != 0)
211 b_workload_dump_verbose_stats(dbp, &conf);
212
213 DB_BENCH_ASSERT(dbp->close(dbp, 0) == 0);
214 DB_BENCH_ASSERT(dbenv->close(dbenv, 0) == 0);
215
216 /*
217 * Construct a string for benchmark output.
218 *
219 * Insert HTML in-line to make the output prettier -- ugly, but easy.
220 */
221 printf("# workload test: %s: %s<br>%lu ops",
222 conf.ts, b_workload_workload_str(conf.workload), (u_long)conf.pcount);
223 if (conf.ksize != 0)
224 printf(", key size: %lu", (u_long)conf.ksize);
225 if (conf.dsize != 0)
226 printf(", data size: %lu", (u_long)conf.dsize);
227 if (conf.pagesz != 0)
228 printf(", page size: %lu", (u_long)conf.pagesz);
229 else
230 printf(", page size: default");
231 if (conf.cachesz != 0)
232 printf(", cache size: %lu", (u_long)conf.cachesz);
233 else
234 printf(", cache size: default");
235 printf(", %s keys", conf.orderedkeys == 1 ? "ordered" : "unordered");
236 printf(", num dups: %lu", (u_long)conf.num_dups);
237 printf("\n");
238
239 if (conf.workload != T_MIXED) {
240 if (conf.message != NULL)
241 printf("%s %s ", conf.message, conf.ts);
242 TIME_DISPLAY(conf.pcount, conf.tot_time);
243 } else
244 TIMER_DISPLAY(conf.pcount);
245
246 return (0);
247 }
248
249 /*
250 * The mixed workload is designed to simulate a somewhat real
251 * usage scenario.
252 * NOTES: * rand is used to decide on the current operation. This will
253 * be repeatable, since the same seed is always used.
254 * * All added keys are stored in a FIFO queue, this is not very
255 * space efficient, but is the best way I could come up with to
256 * insert random key values, and be able to retrieve/delete them.
257 * * TODO: the workload will currently only work with unordered
258 * fixed length keys.
259 */
260 #define GET_PROPORTION 90
261 #define PUT_PROPORTION 7
262 #define DEL_PROPORTION 3
263
264 static int
b_workload_run_mixed_workload(dbp,config)265 b_workload_run_mixed_workload(dbp, config)
266 DB *dbp;
267 CONFIG *config;
268 {
269 DBT key, data;
270 size_t next_op, i, ioff, inscount;
271 char kbuf[KBUF_LEN];
272 struct bench_q operation_queue;
273
274 /* Having ordered insertion does not make sense here */
275 DB_BENCH_ASSERT(config->orderedkeys == 0);
276
277 srand(config->seed);
278 memset(&operation_queue, 0, sizeof(struct bench_q));
279
280 ioff = 0;
281 INIT_KEY(key, config);
282 memset(&data, 0, sizeof(data));
283 DB_BENCH_ASSERT(
284 (data.data = malloc(data.size = config->dsize)) != NULL);
285
286 /*
287 * Add an initial sample set of data to the DB.
288 * This should add some stability, and reduce the likelihood
289 * of deleting all of the entries in the DB.
290 */
291 inscount = 2 * config->pcount;
292 if (inscount > 100000)
293 inscount = 100000;
294
295 for (i = 0; i < inscount; ++i) {
296 GET_KEY_NEXT(key, config, kbuf, i);
297 BENCH_Q_TAIL_INSERT(operation_queue, kbuf);
298 DB_BENCH_ASSERT(dbp->put(dbp, NULL, &key, &data, 0) == 0);
299 }
300
301 TIMER_START;
302 for (i = 0; i < config->pcount; ++i) {
303 next_op = rand()%100;
304
305 if (next_op < GET_PROPORTION ) {
306 BENCH_Q_POP_PUSH(operation_queue, kbuf);
307 key.data = kbuf;
308 key.size = sizeof(kbuf);
309 dbp->get(dbp, NULL, &key, &data, 0);
310 } else if (next_op < GET_PROPORTION+PUT_PROPORTION) {
311 GET_KEY_NEXT(key, config, kbuf, i);
312 BENCH_Q_TAIL_INSERT(operation_queue, kbuf);
313 dbp->put(dbp, NULL, &key, &data, 0);
314 } else {
315 BENCH_Q_POP(operation_queue, kbuf);
316 key.data = kbuf;
317 key.size = sizeof(kbuf);
318 dbp->del(dbp, NULL, &key, 0);
319 }
320 }
321 TIMER_STOP;
322 TIMER_GET(config->tot_time);
323
324 return (0);
325 }
326
327 static int
b_workload_run_std_workload(dbp,config)328 b_workload_run_std_workload(dbp, config)
329 DB *dbp;
330 CONFIG *config;
331 {
332 DBT key, data;
333 DBC *dbc;
334 u_int32_t i;
335 int ret;
336 char kbuf[KBUF_LEN];
337
338 /* Setup a key/data pair. */
339 INIT_KEY(key, config);
340 memset(&data, 0, sizeof(data));
341 DB_BENCH_ASSERT(
342 (data.data = malloc(data.size = config->dsize)) != NULL);
343
344 /* Store the key/data pair count times. */
345 TIMER_START;
346 for (i = 0; i < config->pcount; ++i) {
347 GET_KEY_NEXT(key, config, kbuf, i);
348 DB_BENCH_ASSERT(dbp->put(dbp, NULL, &key, &data, 0) == 0);
349 }
350 TIMER_STOP;
351 TIMER_GET(config->put_time);
352
353 if (b_workload_is_get_workload(config->workload) == 0) {
354 TIMER_START;
355 for (i = 0; i <= config->gcount; ++i) {
356 DB_BENCH_ASSERT(dbp->cursor(dbp, NULL, &dbc, 0) == 0);
357 while ((dbc->c_get(dbc, &key, &data, DB_NEXT)) == 0);
358 DB_BENCH_ASSERT(dbc->c_close(dbc) == 0);
359 }
360 TIMER_STOP;
361 TIMER_GET(config->get_time);
362 }
363
364 if (b_workload_is_del_workload(config->workload) == 0) {
365 /* reset rand to reproduce key sequence. */
366 srand(config->seed);
367
368 TIMER_START;
369 if (config->cursor_del != 0) {
370 DB_BENCH_ASSERT(dbp->cursor(dbp, NULL, &dbc, 0) == 0);
371 while (
372 (ret = dbc->c_get(dbc, &key, &data, DB_NEXT)) == 0)
373 DB_BENCH_ASSERT(dbc->c_del(dbc, 0) == 0);
374 DB_BENCH_ASSERT (ret == DB_NOTFOUND);
375 } else {
376 INIT_KEY(key, config);
377 for (i = 0; i < config->pcount; ++i) {
378 GET_KEY_NEXT(key, config, kbuf, i);
379
380 ret = dbp->del(dbp, NULL, &key, 0);
381 /*
382 * Random key generation can cause dups,
383 * so NOTFOUND result is OK.
384 */
385 if (config->ksize == 0)
386 DB_BENCH_ASSERT
387 (ret == 0 || ret == DB_NOTFOUND);
388 else
389 DB_BENCH_ASSERT(ret == 0);
390 }
391 }
392 TIMER_STOP;
393 TIMER_GET(config->del_time);
394 }
395 return (0);
396 }
397
398 static int
b_workload_dump_verbose_stats(dbp,config)399 b_workload_dump_verbose_stats(dbp, config)
400 DB *dbp;
401 CONFIG *config;
402 {
403 /*
404 * It would be nice to be able to define stat as _stat on
405 * Windows, but that substitutes _stat for the db call as well.
406 */
407 #ifdef DB_WIN32
408 struct _stat fstat;
409 #else
410 struct stat fstat;
411 #endif
412 DB_HASH_STAT *hstat;
413 DB_BTREE_STAT *bstat;
414 double free_prop;
415 char path[1024];
416
417 #ifdef DB_BENCH_INCLUDE_CONFIG_SUMMARY
418 printf("Completed workload benchmark.\n");
419 printf("Configuration summary:\n");
420 printf("\tworkload type: %d\n", (int)config->workload);
421 printf("\tdatabase type: %s\n", config->ts);
422 if (config->cachesz != 0)
423 printf("\tcache size: %lu\n", (u_long)config->cachesz);
424 if (config->pagesz != 0)
425 printf("\tdatabase page size: %lu\n", (u_long)config->pagesz);
426 printf("\tput element count: %lu\n", (u_long)config->pcount);
427 if ( b_workload_is_get_workload(config->workload) == 0)
428 printf("\tget element count: %lu\n", (u_long)config->gcount);
429 if (config->orderedkeys)
430 printf("\tInserting items in order\n");
431 else if (config->ksize == 0)
432 printf("\tInserting keys with size 10\n");
433 else
434 printf(
435 "\tInserting keys with size: %lu\n", (u_long)config->ksize);
436
437 printf("\tInserting data elements size: %lu\n", (u_long)config->dsize);
438
439 if (b_workload_is_del_workload(config->workload) == 0) {
440 if (config->cursor_del)
441 printf("\tDeleting items using a cursor\n");
442 else
443 printf("\tDeleting items without a cursor\n");
444 }
445 #endif /* DB_BENCH_INCLUDE_CONFIG_SUMMARY */
446
447 if (b_workload_is_put_workload(config->workload) == 0)
448 printf("%s Time spent inserting (%lu) (%s) items: %lu/%lu\n",
449 config->message[0] == '\0' ? "" : config->message,
450 (u_long)config->pcount, config->ts,
451 (u_long)config->put_time.tv_sec, config->put_time.tv_nsec);
452
453 if (b_workload_is_get_workload(config->workload) == 0)
454 printf("%s Time spent getting (%lu) (%s) items: %lu/%lu\n",
455 config->message[0] == '\0' ? "" : config->message,
456 (u_long)config->pcount * ((config->gcount == 0) ?
457 1 : config->gcount), config->ts,
458 (u_long)config->get_time.tv_sec, config->get_time.tv_nsec);
459
460 if (b_workload_is_del_workload(config->workload) == 0)
461 printf("%s Time spent deleting (%lu) (%s) items: %lu/%lu\n",
462 config->message[0] == '\0' ? "" : config->message,
463 (u_long)config->pcount, config->ts,
464 (u_long)config->del_time.tv_sec, config->del_time.tv_nsec);
465
466 (void)snprintf(path, sizeof(path),
467 "%s%c%s", TESTDIR, PATH_SEPARATOR[0], TESTFILE);
468 #ifdef DB_WIN32
469 if (_stat(path, &fstat) == 0) {
470 #else
471 if (stat(path, &fstat) == 0) {
472 #endif
473 printf("%s Size of db file (%s): %lu K\n",
474 config->message[0] == '\0' ? "" : config->message,
475 config->ts, (u_long)fstat.st_size/1024);
476 }
477
478 if (config->type == DB_HASH) {
479 #if DB_VERSION_MAJOR < 3 || DB_VERSION_MAJOR == 3 && DB_VERSION_MINOR <= 2
480 DB_BENCH_ASSERT(dbp->stat(dbp, &hstat, NULL, 0) == 0);
481 #elif DB_VERSION_MAJOR < 4 || DB_VERSION_MAJOR == 4 && DB_VERSION_MINOR <= 2
482 DB_BENCH_ASSERT(dbp->stat(dbp, &hstat, 0) == 0);
483 #else
484 DB_BENCH_ASSERT(dbp->stat(dbp, NULL, &hstat, 0) == 0);
485 #endif
486 /*
487 * Hash fill factor is a bit tricky. Want to include
488 * both bucket and overflow buckets (not offpage).
489 */
490 free_prop = hstat->hash_pagesize*hstat->hash_buckets;
491 free_prop += hstat->hash_pagesize*hstat->hash_overflows;
492 free_prop =
493 (free_prop - hstat->hash_bfree - hstat->hash_ovfl_free)/
494 free_prop;
495 printf("%s db fill factor (%s): %.2f%%\n",
496 config->message[0] == '\0' ? "" : config->message,
497 config->ts, free_prop*100);
498 free(hstat);
499 } else { /* Btree */
500 #if DB_VERSION_MAJOR < 3 || DB_VERSION_MAJOR == 3 && DB_VERSION_MINOR <= 2
501 DB_BENCH_ASSERT(dbp->stat(dbp, &bstat, NULL, 0) == 0);
502 #elif DB_VERSION_MAJOR < 4 || DB_VERSION_MAJOR == 4 && DB_VERSION_MINOR <= 2
503 DB_BENCH_ASSERT(dbp->stat(dbp, &bstat, 0) == 0);
504 #else
505 DB_BENCH_ASSERT(dbp->stat(dbp, NULL, &bstat, 0) == 0);
506 #endif
507 free_prop = bstat->bt_pagesize*bstat->bt_leaf_pg;
508 free_prop = (free_prop-bstat->bt_leaf_pgfree)/free_prop;
509 printf("%s db fill factor (%s): %.2f%%\n",
510 config->message[0] == '\0' ? "" : config->message,
511 config->ts, free_prop*100);
512 free(bstat);
513 }
514 return (0);
515 }
516
517 static char *
b_workload_workload_str(workload)518 b_workload_workload_str(workload)
519 int workload;
520 {
521 static char buf[128];
522
523 switch (workload) {
524 case T_PUT_GET_DELETE:
525 return ("PUT/GET/DELETE");
526 /* NOTREACHED */
527 case T_GET:
528 return ("GET");
529 /* NOTREACHED */
530 case T_PUT:
531 return ("PUT");
532 /* NOTREACHED */
533 case T_DELETE:
534 return ("DELETE");
535 /* NOTREACHED */
536 case T_PUT_GET:
537 return ("PUT/GET");
538 /* NOTREACHED */
539 case T_PUT_DELETE:
540 return ("PUT/DELETE");
541 /* NOTREACHED */
542 case T_GET_DELETE:
543 return ("GET/DELETE");
544 /* NOTREACHED */
545 case T_MIXED:
546 snprintf(buf, sizeof(buf), "MIXED (get: %d, put: %d, del: %d)",
547 (int)GET_PROPORTION,
548 (int)PUT_PROPORTION, (int)DEL_PROPORTION);
549 return (buf);
550 default:
551 break;
552 }
553
554 exit(b_workload_usage());
555 /* NOTREACHED */
556 }
557
558 static int
b_workload_is_get_workload(workload)559 b_workload_is_get_workload(workload)
560 int workload;
561 {
562 switch (workload) {
563 case T_GET:
564 case T_PUT_GET:
565 case T_PUT_GET_DELETE:
566 case T_GET_DELETE:
567 return 0;
568 }
569 return 1;
570 }
571
572 static int
b_workload_is_put_workload(workload)573 b_workload_is_put_workload(workload)
574 int workload;
575 {
576 switch (workload) {
577 case T_PUT:
578 case T_PUT_GET:
579 case T_PUT_GET_DELETE:
580 case T_PUT_DELETE:
581 return 0;
582 }
583 return 1;
584 }
585
586 static int
b_workload_is_del_workload(workload)587 b_workload_is_del_workload(workload)
588 int workload;
589 {
590 switch (workload) {
591 case T_DELETE:
592 case T_PUT_DELETE:
593 case T_PUT_GET_DELETE:
594 case T_GET_DELETE:
595 return 0;
596 }
597 return 1;
598 }
599
600 static int
b_workload_usage()601 b_workload_usage()
602 {
603 (void)fprintf(stderr,
604 "usage: b_workload [-b cachesz] [-c count] [-d bytes] [-e]\n");
605 (void)fprintf(stderr,
606 "\t[-g getitrs] [-i] [-k keysize] [-m message] [-o] [-p pagesz]\n");
607 (void)fprintf(stderr, "\t[-r dup_count] [-t type] [-w type]\n");
608
609 (void)fprintf(stderr, "Where:\n");
610 (void)fprintf(stderr, "\t-b the size of the DB cache.\n");
611 (void)fprintf(stderr, "\t-c the number of elements to be measured.\n");
612 (void)fprintf(stderr, "\t-d the size of each data element.\n");
613 (void)fprintf(stderr, "\t-e delete entries using a cursor.\n");
614 (void)fprintf(stderr, "\t-g number of get cursor traverses.\n");
615 (void)fprintf(stderr, "\t-i Pre-init hash DB bucket count.\n");
616 (void)fprintf(stderr, "\t-k the size of each key inserted.\n");
617 (void)fprintf(stderr, "\t-m message pre-pended to log output.\n");
618 (void)fprintf(stderr, "\t-o keys should be ordered for insert.\n");
619 (void)fprintf(stderr, "\t-p the page size for the database.\n");
620 (void)fprintf(stderr, "\t-r the number of duplicates to insert\n");
621 (void)fprintf(stderr, "\t-t type of the underlying database.\n");
622 (void)fprintf(stderr, "\t-w the workload to measure, available:\n");
623 (void)fprintf(stderr, "\t\tA - PUT_GET_DELETE\n");
624 (void)fprintf(stderr, "\t\tB - GET\n");
625 (void)fprintf(stderr, "\t\tC - PUT\n");
626 (void)fprintf(stderr, "\t\tD - DELETE\n");
627 (void)fprintf(stderr, "\t\tE - PUT_GET\n");
628 (void)fprintf(stderr, "\t\tF - PUT_DELETE\n");
629 (void)fprintf(stderr, "\t\tG - GET_DELETE\n");
630 (void)fprintf(stderr, "\t\tH - MIXED\n");
631 return (EXIT_FAILURE);
632 }
633