diff options
author | Ulrich Drepper <drepper@redhat.com> | 2002-11-26 22:50:54 +0000 |
---|---|---|
committer | Ulrich Drepper <drepper@redhat.com> | 2002-11-26 22:50:54 +0000 |
commit | 76a50749f7af5935ba3739e815aa6a16ae4440d1 (patch) | |
tree | c16eac47f220f03fea74d80ef0a4e774809e94b7 /nptl/sockperf.c | |
parent | 6938e63f714b15c377d8cbf8e97b6f15b0e1b692 (diff) | |
download | glibc-76a50749f7af5935ba3739e815aa6a16ae4440d1.tar.gz |
Initial revisioncvs/initial
2002-11-26 Ulrich Drepper <drepper@redhat.com>
* allocatestack.c (queue_stack): Don't remove stack from list here.
Do it in the caller. Correct condition to prematurely terminate
loop to free stacks.
(__deallocate_stack): Remove stack from list here.
2002-11-26 Ulrich Drepper <drepper@redhat.com>
* Makefile (tests): Add tst-stack1.
* tst-stack1.c: New file.
* allocatestack.c (allocate_stack): Initialize the TCB on a user
provided stack.
* pthread_attr_getstack.c: Return bottom of the thread area.
2002-11-25 Ulrich Drepper <drepper@redhat.com>
* Makefile (libpthread-routines): Add pt-allocrtsig and
pthread_kill_other_threads.
* pt-allocrtsig.c: New file.
* pthread_kill_other_threads.c: New file.
* sysdeps/unix/sysv/linux/allocrtsig.c: Add additional aliases for
all three functions.
* sysdeps/unix/sysv/linux/Makefile (sysdep_routines): Remove
allocrtsig.
* sysdeps/unix/sysv/linux/Versions (libc:GLIBC_PRIVATE): Export
__libc_current_sigrtmin_private, __libc_current_sigrtmax_private,
and __libc_allocate_rtsig_private.
* Versions (libpthread): Export pthread_kill_other_threads_np,
__libc_current_sigrtmin, and __libc_current_sigrtmax.
2002-11-24 Ulrich Drepper <drepper@redhat.com>
* allocatestack.c (allocate_stack): stackaddr in attribute points to
the end of the stack. Adjust computations.
When mprotect call fails dequeue stack and free it.
* pthread_attr_setstack.c: Store top of the stack in stackaddr
attribute.
* pthread_getattr_np.c: Likewise.
* descr.h (IS_DETACHED): Add some more parenthesis to prevent
surprises.
2002-11-23 Ulrich Drepper <drepper@redhat.com>
* sysdeps/pthread/pthread.h (pthread_self): __THROW must come before
attribute definitions. Patch by Luca Barbieri <ldb@ldb.ods.org>.
2002-11-22 Ulrich Drepper <drepper@redhat.com>
* pthread_getspecific.c: Optimize access to first 2nd-level array.
* pthread_setspecific.c: Likewise.
2002-11-21 Ulrich Drepper <drepper@redhat.com>
* sysdeps/unix/sysv/linux/i386/createthread.c: Remove CLONE_ flags
definitions. Get them from the official place.
* sysdeps/unix/sysv/linux/i386/fork.c: Likewise.
* sysdeps/unix/sysv/linux/i386/createthread.c: Update CLONE_* flags.
Use new CLONE_ flags in clone() calls.
* sysdeps/unix/sysv/linux/fork.c: Use ARCH_FORK to actually fork.
* sysdeps/unix/sysv/linux/i386/fork.c: New file.
* Versions: Add pthread_* functions for libc.
* forward.c: New file.
* sysdeps/pthread/Makefile (libpthread-sysdeps_routines): Add
errno-loc.
* herrno.c: New file.
* res.c: New file.
* Makefile (libpthread-routines): Remove sem_post, sem_wait,
sem_trywait, and sem_timedwait. Add herrno and res.
* sem_init.c: Don't initialize lock and waiters members.
* sem_open.c: Likewise.
* sem_post.c: Removed.
* sem_wait.c: Removed.
* sem_trywait.c: Removed.
* sem_timedwait.c: Removed.
* sysdeps/unix/sysv/linux/i386/i486/lowlevelsem.S: Complete rewrite.
Includes full implementations of sem_post, sem_wait, sem_trywait,
and sem_timedwait.
* sysdeps/unix/sysv/linux/i386/lowlevelsem.h (lll_sem_post): Adjust
for new implementation.
* sysdeps/unix/sysv/linux/internaltypes.h (struct sem): Remove lock
and waiters fields.
* tst-sem3.c: Improve error message.
* tst-signal3.c: Likewise.
* init.c (__pthread_initialize_minimal): Use set_tid_address syscall
to tell the kernel about the termination futex and to initialize tid
member. Don't initialize main_thread.
* descr.h (struct pthread): Remove main_thread member.
* cancelllation.c (__do_cancel): Remove code handling main thread.
The main thread is not special anymore.
* allocatestack.c (__reclaim_stacks): Mark stacks as unused. Add
size of the stacks to stack_cache_actsize.
* pt-readv.c: Add missing "defined".
* pt-sigwait.c: Likewise.
* pt-writev.c: Likewise.
2002-11-09 Ulrich Drepper <drepper@redhat.com>
* Versions: Export __connect from libpthread.
Patch by Luca Barbieri <ldb@ldb.ods.org>.
* Makefile (libpthread-routines): Add pt-raise.
* sysdeps/unix/sysv/linux/raise.c: New file.
* sysdeps/unix/sysv/linux/pt-raise.c: New file.
* sysdeps/generic/pt-raise.c: New file.
* pthread_cond_init.c: Initialize all data elements of the condvar
structure. Patch by Luca Barbieri <ldb@ldb.ods.org>.
* pthread_attr_init.c: Actually implement 2.0 compatibility version.
* pthread_create.c: Likewise.
* Makefile (tests): Add tst-key1, tst-key2, tst-key3.
* tst-key1.c: New file.
* tst-key2.c: New file.
* tst-key3.c: New file.
* Versions: Export pthread_detach for version GLIBC_2.0.
Reported by Saurabh Desai <sdesai@austin.ibm.com>.
2002-11-08 Ulrich Drepper <drepper@redhat.com>
* pthread_key_create.c: Terminate search after an unused key was found.
Patch by Luca Barbieri <ldb@ldb.ods.org>.
* sysdeps/unix/sysv/linux/i386/pthread_once.S: Return zero.
Patch by Luca Barbieri <ldb@ldb.ods.org>.
2002-10-10 Ulrich Drepper <drepper@redhat.com>
* sysdeps/unix/sysv/linux/i386/i486/lowlevelsem.S: Use slow generic
dynamic lookup for errno in PIC.
* allocatestack.c (get_cached_stack): Rearrange code slightly to
release the stack lock as soon as possible.
Call _dl_allocate_tls_init for TCB from the cache to re-initialize
the static TLS block.
(allocate_stack): Call _dl_allocate_tls_init for user-provided stack.
* cancellation.c: Renamed from cancelation.c.
* Makefile: Adjust accordingly.
* pthreadP.h (CANCELLATION_P): Renamed from CANCELATION_P.
* cleanup_defer.c: Use CANCELLATION_P.
* pthread_testcancel.c: Likewise.
* descr.h: Fix spelling in comments.
* init.c: Likewise.
* pthread_getattr_np.c: Likewise.
* pthread_getschedparam.c: Likewise.
* pthread_setschedparam.c: Likewise.
* Versions: Likewise.
* pt-pselect.c: New file.
* Makefile (libpthread-routines): Add pt-pselect.
* Versions: Add pselect.
* tst-cancel4.c: New file.
* Makefile (tests): Add tst-cancel4.
2002-10-09 Ulrich Drepper <drepper@redhat.com>
* pthread_mutex_lock.c: Always record lock ownership.
* pthread_mutex_timedlock.c: Likewise.
* pthread_mutex_trylock.c: Likewise.
* pt-readv.c: New file.
* pt-writev.c: New file.
* pt-creat.c: New file.
* pt-msgrcv.c: New file.
* pt-msgsnd.c: New file.
* pt-poll.c: New file.
* pt-select.c: New file.
* pt-sigpause.c: New file.
* pt-sigsuspend.c: New file.
* pt-sigwait.c: New file.
* pt-sigwaitinfo.c: New file.
* pt-waitid.c: New file.
* Makefile (libpthread-routines): Add pt-readv, pt-writev, pt-creat,
pt-msgrcv, pt-msgsnd, pt-poll, pt-select, pt-sigpause, pt-sigsuspend,
pt-sigwait, pt-sigwaitinfo, and pt-waitid.
* Versions: Add all the new functions.
* tst-exit1.c: New file.
* Makefile (tests): Add tst-exit1.
* sem_timedwait.c: Minor optimization for more optimal fastpath.
2002-10-08 Ulrich Drepper <drepper@redhat.com>
* pt-fcntl.c: Only enable asynchronous cancellation for F_SETLKW.
* pthread_join.c: Enable asynchronous cancellation around lll_wait_tid
call. pthread_join is an official cancellation point.
* pthread_timedjoin.c: Likewise.
* pthread_cond_wait.c: Revert order in which internal lock are dropped
and the condvar's mutex are retrieved.
* pthread_cond_timedwait.c: Likewise.
Reported by dice@saros.East.Sun.COM.
2002-10-07 Ulrich Drepper <drepper@redhat.com>
* pthreadP.h: Cut out all type definitions and move them...
* sysdeps/unix/sysv/linux/internaltypes.h: ...here. New file.
* pthreadP.h: Include <internaltypes.h>.
* sysdeps/unix/sysv/linux/i386/lowlevelsem.h (lll_sem_post): Little
performance tweaks.
* sem_trywait.c: Shuffle #includes around to get right order.
* sem_timedwait.c: Likewise.
* sem_post.c: Likewise.
* sem_wait.c: Likewise.
* nptl 0.3 released.
* Makefile (tests): Add tst-signal3.
* tst-signal3.c: New file.
2002-10-05 Ulrich Drepper <drepper@redhat.com>
* sysdeps/unix/sysv/linux/i386/lowlevelsem.h: Tell the compiler that
the asms modify the sem object.
(__lll_sem_timedwait): Now takes struct sem* as first parameter.
* sysdeps/unix/sysv/linux/i386/bits/semaphore.h (sem_t): Don't expose
the actual members.
* pthreadP.h (struct sem): New type. Actual semaphore type.
* semaphoreP.h: Include pthreadP.h.
* sem_getvalue.c: Adjust to sem_t change.
* sem_init.c: Likewise.
* sem_open.c: Likewise.
* sem_post.c: Likewise.
* sem_timedwait.c: Likewise.
* sem_trywait.c: Likewise.
* sem_wait.c: Likewise.
2002-10-04 Ulrich Drepper <drepper@redhat.com>
* Makefile (tests): Add tst-basic2, tst-exec1, tst-exec3, tst-exec3.
* tst-basic2.c: New file.
* tst-exec1.c: New file.
* tst-exec2.c: New file.
* tst-exec3.c: New file.
* tst-fork1.c: Remove extra */.
* nptl 0.2 released. The API for IA-32 is complete.
Diffstat (limited to 'nptl/sockperf.c')
-rw-r--r-- | nptl/sockperf.c | 594 |
1 files changed, 594 insertions, 0 deletions
diff --git a/nptl/sockperf.c b/nptl/sockperf.c new file mode 100644 index 0000000000..d29a6ee26a --- /dev/null +++ b/nptl/sockperf.c @@ -0,0 +1,594 @@ +#define _GNU_SOURCE +#include <argp.h> +#include <complex.h> +#include <errno.h> +#include <error.h> +#include <fcntl.h> +#include <gd.h> +#include <inttypes.h> +#include <pthread.h> +#include <signal.h> +#include <stdbool.h> +#include <stddef.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <unistd.h> +#include <sys/param.h> +#include <sys/poll.h> +#include <sys/socket.h> +#include <sys/un.h> + + +#define size_x 320 +#define size_y 240 + + +#define PATH "/tmp/s.sockperf" + + +struct thread_param +{ + unsigned int from; + unsigned int to; + unsigned int nserv; +}; + +struct coord +{ + unsigned int x; + unsigned int y; + complex double z; +}; + + +/* We use 64bit values for the times. */ +typedef unsigned long long int hp_timing_t; + + +static unsigned int nclients = 2; +static unsigned int nservers = 2; + +static bool timing; +static int points; + + +static complex double top_left = -0.7 + 0.2i; +static complex double bottom_right = -0.5 - 0.0i; + + +static int colors[256]; +static gdImagePtr image; +static pthread_mutex_t image_lock; + +static int sock; + + +static void * +client (void *arg) +{ + struct thread_param *param = arg; + unsigned int cnt; + unsigned int nserv = param->nserv; + int clisock[nserv]; + struct pollfd servpoll[nserv]; + struct sockaddr_un servaddr; + socklen_t servlen; + struct coord c; + + bool new_coord (void) + { + if (cnt >= param->to) + return false; + + unsigned int row = cnt / size_x; + unsigned int col = cnt % size_x; + + c.x = col; + c.y = row; + c.z = (top_left + + ((col + * (creal (bottom_right) - creal (top_left))) / size_x) + + (_Complex_I * (row * (cimag (bottom_right) - cimag (top_left))) + / size_y)); + + ++cnt; + + return true; + } + + + for (cnt = 0; cnt < nserv; ++cnt) + { + servpoll[cnt].fd = socket (AF_UNIX, SOCK_STREAM, 0); + if (clisock < 0) + { + puts ("cannot create socket in client"); + return NULL; + } + + memset (&servaddr, '\0', sizeof (servaddr)); + servaddr.sun_family = AF_UNIX; + strncpy (servaddr.sun_path, PATH, sizeof (servaddr.sun_path)); + servlen = offsetof (struct sockaddr_un, sun_path) + strlen (PATH) + 1; + + + int err; + while (1) + { + err = TEMP_FAILURE_RETRY (connect (servpoll[cnt].fd, &servaddr, + servlen)); + if (err != -1 || errno != ECONNREFUSED) + break; + + pthread_yield (); + } + + if (err == -1) + { + printf ("cannot connect: %m (%d)\n", errno); + exit (1); + } + + servpoll[cnt].events = POLLOUT; + servpoll[cnt].revents = 0; + } + + cnt = param->from; + + new_coord (); + bool z_valid = true; + + while (1) + { + int i; + int n = poll (servpoll, nserv, -1); + if (n == -1) + { + puts ("poll returned error"); + break; + } + + bool cont = false; + for (i = 0; i < nserv && n > 0; ++i) + if (servpoll[i].revents != 0) + { + if (servpoll[i].revents == POLLIN) + { + unsigned int vals[3]; + if (TEMP_FAILURE_RETRY (read (servpoll[i].fd, &vals, + sizeof (vals))) + != sizeof (vals)) + { + puts ("read error in client"); + return NULL; + } + + pthread_mutex_lock (&image_lock); + + gdImageSetPixel (image, vals[0], vals[1], vals[2]); + ++points; + + pthread_mutex_unlock (&image_lock); + + servpoll[i].events = POLLOUT; + } + else + { + if (servpoll[i].revents != POLLOUT) + printf ("revents: %hd != POLLOUT ???\n", + servpoll[i].revents); + + if (z_valid) + { + if (TEMP_FAILURE_RETRY (write (servpoll[i].fd, &c, + sizeof (c))) != sizeof (c)) + { + puts ("write error in client"); + return NULL; + } + cont = true; + servpoll[i].events = POLLIN; + + z_valid = new_coord (); + if (! z_valid) + /* No more to do. Clear the event fields. */ + for (i = 0; i < nserv; ++i) + if (servpoll[i].events == POLLOUT) + servpoll[i].events = servpoll[i].revents = 0; + } + else + servpoll[i].events = servpoll[i].revents = 0; + } + + --n; + } + else if (servpoll[i].events != 0) + cont = true; + + if (! cont && ! z_valid) + break; + } + + c.x = 0xffffffff; + c.y = 0xffffffff; + for (cnt = 0; cnt < nserv; ++cnt) + { + TEMP_FAILURE_RETRY (write (servpoll[cnt].fd, &c, sizeof (c))); + close (servpoll[cnt].fd); + } + + return NULL; +} + + +static void * +server (void *arg) +{ + struct sockaddr_un cliaddr; + socklen_t clilen; + int clisock = TEMP_FAILURE_RETRY (accept (sock, &cliaddr, &clilen)); + + if (clisock == -1) + { + puts ("accept failed"); + return NULL; + } + + while (1) + { + struct coord c; + + if (TEMP_FAILURE_RETRY (read (clisock, &c, sizeof (c))) != sizeof (c)) + { + printf ("server read failed: %m (%d)\n", errno); + break; + } + + if (c.x == 0xffffffff && c.y == 0xffffffff) + break; + + unsigned int rnds = 0; + complex double z = c.z; + while (cabs (z) < 4.0) + { + z = z * z - 1; + if (++rnds == 255) + break; + } + + unsigned int vals[3] = { c.x, c.y, rnds }; + if (TEMP_FAILURE_RETRY (write (clisock, vals, sizeof (vals))) + != sizeof (vals)) + { + puts ("server write error"); + return NULL; + } + } + + close (clisock); + + return NULL; +} + + +static const char *outfilename = "test.png"; + + +static const struct argp_option options[] = + { + { "clients", 'c', "NUMBER", 0, "Number of client threads" }, + { "servers", 's', "NUMBER", 0, "Number of server threads per client" }, + { "timing", 'T', NULL, 0, + "Measure time from startup to the last thread finishing" }, + { NULL, 0, NULL, 0, NULL } + }; + +/* Prototype for option handler. */ +static error_t parse_opt (int key, char *arg, struct argp_state *state); + +/* Data structure to communicate with argp functions. */ +static struct argp argp = +{ + options, parse_opt +}; + + +int +main (int argc, char *argv[]) +{ + int cnt; + FILE *outfile; + struct sockaddr_un servaddr; + socklen_t servlen; + int remaining; + + /* Parse and process arguments. */ + argp_parse (&argp, argc, argv, 0, &remaining, NULL); + + + pthread_t servth[nservers * nclients]; + pthread_t clntth[nclients]; + struct thread_param clntparam[nclients]; + + + image = gdImageCreate (size_x, size_y); + if (image == NULL) + { + puts ("gdImageCreate failed"); + return 1; + } + + for (cnt = 0; cnt < 255; ++cnt) + colors[cnt] = gdImageColorAllocate (image, 256 - cnt, 256 - cnt, + 256 - cnt); + /* Black. */ + colors[cnt] = gdImageColorAllocate (image, 0, 0, 0); + + + sock = socket (AF_UNIX, SOCK_STREAM, 0); + if (sock < 0) + error (EXIT_FAILURE, errno, "cannot create socket"); + + memset (&servaddr, '\0', sizeof (servaddr)); + servaddr.sun_family = AF_UNIX; + strncpy (servaddr.sun_path, PATH, sizeof (servaddr.sun_path)); + servlen = offsetof (struct sockaddr_un, sun_path) + strlen (PATH) + 1; + + if (bind (sock, &servaddr, servlen) == -1) + error (EXIT_FAILURE, errno, "bind failed"); + + listen (sock, SOMAXCONN); + + pthread_mutex_init (&image_lock, NULL); + + + struct sigaction sa; + sa.sa_handler = SIG_IGN; + sigemptyset (&sa.sa_mask); + sa.sa_flags = 0; + + clockid_t cl; + struct timespec start_time; + if (timing) + { + if (clock_getcpuclockid (0, &cl) != 0 + || clock_gettime (cl, &start_time) != 0) + timing = false; + } + + /* Start the servers. */ + for (cnt = 0; cnt < nservers * nclients; ++cnt) + { + if (pthread_create (&servth[cnt], NULL, server, NULL) != 0) + { + puts ("pthread_create for server failed"); + exit (1); + } + } + + for (cnt = 0; cnt < nclients; ++cnt) + { + clntparam[cnt].from = cnt * (size_x * size_y) / nclients; + clntparam[cnt].to = MIN ((cnt + 1) * (size_x * size_y) / nclients, + size_x * size_y); + clntparam[cnt].nserv = nservers; + + if (pthread_create (&clntth[cnt], NULL, client, &clntparam[cnt]) != 0) + { + puts ("pthread_create for client failed"); + exit (1); + } + } + + + /* Wait for the clients. */ + for (cnt = 0; cnt < nclients; ++cnt) + if (pthread_join (clntth[cnt], NULL) != 0) + { + puts ("client pthread_join failed"); + exit (1); + } + + /* Wait for the servers. */ + for (cnt = 0; cnt < nclients * nservers; ++cnt) + if (pthread_join (servth[cnt], NULL) != 0) + { + puts ("server pthread_join failed"); + exit (1); + } + + + if (timing) + { + struct timespec end_time; + + if (clock_gettime (cl, &end_time) == 0) + { + end_time.tv_sec -= start_time.tv_sec; + end_time.tv_nsec -= start_time.tv_nsec; + if (end_time.tv_nsec < 0) + { + end_time.tv_nsec += 1000000000; + --end_time.tv_sec; + } + + printf ("\nRuntime: %lu.%09lu seconds\n%d points computed\n", + (unsigned long int) end_time.tv_sec, + (unsigned long int) end_time.tv_nsec, + points); + } + } + + + outfile = fopen (outfilename, "w"); + if (outfile == NULL) + error (EXIT_FAILURE, errno, "cannot open output file '%s'", outfilename); + + gdImagePng (image, outfile); + + fclose (outfile); + + unlink (PATH); + + return 0; +} + + +/* Handle program arguments. */ +static error_t +parse_opt (int key, char *arg, struct argp_state *state) +{ + switch (key) + { + case 'c': + nclients = strtoul (arg, NULL, 0); + break; + + case 's': + nservers = strtoul (arg, NULL, 0); + break; + + case 'T': + timing = true; + break; + + default: + return ARGP_ERR_UNKNOWN; + } + + return 0; +} + + +static hp_timing_t +get_clockfreq (void) +{ + /* We read the information from the /proc filesystem. It contains at + least one line like + cpu MHz : 497.840237 + or also + cpu MHz : 497.841 + We search for this line and convert the number in an integer. */ + static hp_timing_t result; + int fd; + + /* If this function was called before, we know the result. */ + if (result != 0) + return result; + + fd = open ("/proc/cpuinfo", O_RDONLY); + if (__builtin_expect (fd != -1, 1)) + { + /* XXX AFAIK the /proc filesystem can generate "files" only up + to a size of 4096 bytes. */ + char buf[4096]; + ssize_t n; + + n = read (fd, buf, sizeof buf); + if (__builtin_expect (n, 1) > 0) + { + char *mhz = memmem (buf, n, "cpu MHz", 7); + + if (__builtin_expect (mhz != NULL, 1)) + { + char *endp = buf + n; + int seen_decpoint = 0; + int ndigits = 0; + + /* Search for the beginning of the string. */ + while (mhz < endp && (*mhz < '0' || *mhz > '9') && *mhz != '\n') + ++mhz; + + while (mhz < endp && *mhz != '\n') + { + if (*mhz >= '0' && *mhz <= '9') + { + result *= 10; + result += *mhz - '0'; + if (seen_decpoint) + ++ndigits; + } + else if (*mhz == '.') + seen_decpoint = 1; + + ++mhz; + } + + /* Compensate for missing digits at the end. */ + while (ndigits++ < 6) + result *= 10; + } + } + + close (fd); + } + + return result; +} + + +int +clock_getcpuclockid (pid_t pid, clockid_t *clock_id) +{ + /* We don't allow any process ID but our own. */ + if (pid != 0 && pid != getpid ()) + return EPERM; + +#ifdef CLOCK_PROCESS_CPUTIME_ID + /* Store the number. */ + *clock_id = CLOCK_PROCESS_CPUTIME_ID; + + return 0; +#else + /* We don't have a timer for that. */ + return ENOENT; +#endif +} + + +#define HP_TIMING_NOW(Var) __asm__ __volatile__ ("rdtsc" : "=A" (Var)) + +/* Get current value of CLOCK and store it in TP. */ +int +clock_gettime (clockid_t clock_id, struct timespec *tp) +{ + int retval = -1; + + switch (clock_id) + { + case CLOCK_PROCESS_CPUTIME_ID: + { + + static hp_timing_t freq; + hp_timing_t tsc; + + /* Get the current counter. */ + HP_TIMING_NOW (tsc); + + if (freq == 0) + { + freq = get_clockfreq (); + if (freq == 0) + return EINVAL; + } + + /* Compute the seconds. */ + tp->tv_sec = tsc / freq; + + /* And the nanoseconds. This computation should be stable until + we get machines with about 16GHz frequency. */ + tp->tv_nsec = ((tsc % freq) * UINT64_C (1000000000)) / freq; + + retval = 0; + } + break; + + default: + errno = EINVAL; + break; + } + + return retval; +} |